JavaScript 的秘密花园

齐博厚
2023-12-01

1、对象:

JS中所有的变量都是对象,除了两个例外 null和undefined .


一个常见的误解是数字的字面值(literal)不是对象。这是因为 JavaScript 解析器的一个错误, 它试图将点操作符解析为浮点数字面值的一部分。

ex: 2.toString()    //synaxError  原因是以为.是浮点数的点

转化方法:

2..toString(); // 第二个点号可以正常解析
2 .toString(); // 注意点号前面的空格
(2).toString(); // 2先被计算

有两种方式可以访问对象的属性,点操作符或者中括号操作符

var  s = { name : "h"}

s.name 和s["name"]均可以访问

但是中括号可以用于动态的访问,属性名中含有空格,属性名是JS关键字之类


删除属性唯一的方法是使用delete,设置属性为Null或者undefined并不能真正删除属性。仅仅是移除了属性和值的关联

2、Javascript原型链:

function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method: function() {}
};

function Bar() {}

// 设置Bar的prototype属性为Foo的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

// 修正Bar.prototype.constructor为Bar本身
Bar.prototype.constructor = Bar;

var test = new Bar() // 创建Bar的一个新实例

// 原型链
test [Bar的实例]
    Bar.prototype [Foo的实例] 
        { foo: 'Hello World' }
        Foo.prototype
            {method: ...};
            Object.prototype
                {toString: ... /* etc. */};

上面的例子中, test 对象从  Bar.prototype 和  Foo.prototype 继承下来;因此, 它能访问  Foo 的原型方法  method。同时,它也能够访问 那个定义在原型上的  Foo 实例属性  value。 需要注意的是  new Bar()  不会创造出一个新的  Foo 实例,而是 重复使用它原型上的那个实例;因此,所有的  Bar 实例都会共享 相同的  value 属性。


当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。


如果一个属性在原型链的上端,则对于查找时间将带来不利影响。特别的,试图获取一个不存在的属性将会遍历整个原型链。

并且,当使用 for in 循环遍历对象的属性时,原型链上的所有属性都将被访问。


所以:

在写复杂的 JavaScript 应用之前,充分理解原型链继承的工作方式是每个 JavaScript 程序员必修的功课。 要提防原型链过长带来的性能问题,并知道如何通过缩短原型链来提高性能。 更进一步,绝对不要扩展内置类型的原型,除非是为了和新的 JavaScript 引擎兼容。


for in循环

和 in 操作符一样,for in 循环同样在查找对象属性时遍历原型链上的所有属性。

// 修改 Object.prototype
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
    console.log(i); // 输出两个属性:bar 和 moo
}

hasownprotype:

// foo 变量是上例中的
for(var i in foo) {
    if (foo.hasOwnProperty(i)) {
        console.log(i);
    }
}

函数的声明和表达式:

函数是JavaScript中的一等对象,这意味着可以把函数像其它值一样传递。 一个常见的用法是把匿名函数作为回调函数传递到异步函数中。

var s = function () {}

上述表达的意思是,将一个匿名函数赋值给变量s

foo; // 'undefined'
foo(); // 出错:TypeError
var foo = function() {};

由于 var 定义了一个声明语句,对变量 foo 的解析是在代码运行之前,因此foo 变量在代码运行时已经被定义过了。

但是由于赋值语句只在运行时执行,因此在相应代码执行之前, foo 的值缺省为 undefined。


还有一种特别写法,将命名函数赋值给一个变量:

demo:

var foo = function bar() {
    bar(); // 正常运行
}
bar(); // 出错:ReferenceError


This的工作原理:

Javascript有一套完全不同于其他语言的this处理机制,五种不同情况下,this的指向个不相同.

1、全局对象:全部范围内使用this,会指向全局对象

2、函数调用:this也会指向全局对象

3、方法调用:this会指向被调用方

demo:

test.foo(); //this指向test

4、调用构造函数:

new f();
如果函数倾向于和 new 关键词一块使用,则我们称这个函数是 构造函数。 在函数内部,this 指向新创建的对象。

5、显示的设置this:

function foo(a, b, c) {}

var bar = {};
foo.apply(bar, [1, 2, 3]); // 数组将会被扩展,如下所示
foo.call(bar, 1, 2, 3); // 传递到foo的参数是:a = 1, b = 2, c = 3

当使用 Function.prototype 上的 call 或者 apply 方法时,函数内的 this将会被 显式设置为函数调用的第一个参数。

因此函数调用的规则在上例中已经不适用了,在foo 函数内 this 被设置成了bar


闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为 函数 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。

function Counter(start) {
    var count = start;
    return {
        increment: function() {
            count++;
        },

        get: function() {
            return count;
        }
    }
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

循环中的闭包

一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号

上面的代码不会输出数字 0 到 9,而是会输出数字 10 十次。

当 console.log 被调用的时候,匿名函数保持对外部变量 i 的引用,此时for循环已经结束, i 的值被修改成了 10.

为了得到想要的结果,需要在每次循环中创建变量 i 的拷贝


为了正确的获得循环序号,最好使用 匿名包裹器(译者注:其实就是我们通常说的自执行匿名函数)。

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}


JavaScript 中的构造函数和其它语言中的构造函数是不同的。 通过 new 关键字方式调用的函数都被认为是构造函数。

在构造函数内部 - 也就是被调用的函数内 - this 指向新创建的对象 Object。 这个新创建的对象的 prototype 被指向到构造函数的 prototype


命名空间

只有一个全局作用域导致的常见错误是命名冲突。在 JavaScript中,这可以通过 匿名包装器 轻松解决

(function() {
    // 函数创建一个命名空间

    window.foo = function() {
        // 对外公开的函数,创建了闭包
    };

})(); // 立即执行此匿名函数

匿名函数被认为是 表达式;因此为了可调用性,它们首先会被执行。

( // 小括号内的函数首先被执行
function() {}
) // 并且返回函数对象
() // 调用上面的执行结果,也就是函数对象

结论

推荐使用匿名包装器译者注:也就是自执行的匿名函数)来创建命名空间。这样不仅可以防止命名冲突, 而且有利于程序的模块化。

另外,使用全局变量被认为是不好的习惯。这样的代码倾向于产生错误和带来高的维护成本。



虽然在 JavaScript 中数组是对象,但是没有好的理由去使用 for in 循环 遍历数组。 相反,有一些好的理由不去使用 for in 遍历数组。

注意: JavaScript 中数组不是关联数组。 JavaScript 中只有对象 来管理键值的对应关系。但是关联数组是保持顺序的,而对象不是

由于 for in 循环会枚举原型链上的所有属性,唯一过滤这些属性的方式是使用 hasOwnProperty 函数, 因此会比普通的 for 循环慢上好多倍。

遍历

为了达到遍历数组的最佳性能,推荐使用经典的 for 循环。

数组的length属性

length 属性的 getter 方式会简单的返回数组的长度,而 setter 方式会截断数组。

var arr = [1, 2, 3, 4, 5, 6];
arr.length = 3;
console.log(arr);

arr.length = 6;
console.log(arr);

[123uniquefunctioneachfunction]

[123uniquefunctioneachfunction]
arr
[1, 2, 3, undefined × 3]


由于 Array 的构造函数在如何处理参数时有点模棱两可,因此总是推荐使用数组的字面语法 - [] - 来创建数组。

[1, 2, 3]; // 结果: [1, 2, 3]
new Array(1, 2, 3); // 结果: [1, 2, 3]

[3]; // 结果: [3]
new Array(3); // 结果: [] 
new Array('3') // 结果: ['3']

// 译者注:因此下面的代码将会使人很迷惑
new Array(3, 4, 5); // 结果: [3, 4, 5] 
new Array(3) // 结果: [],此数组长度为 3

JS是弱语言,因此,在进行==比较的时候

会比较两边的操作符而自动进行类型转换


setTimeout只会调用一次。setInterval则每隔X秒会调用一次。

clearTimeout可以清空定时器

settimeout设置时候,建议传入匿名函数


绝对不要使用字符串作为 setTimeout 或者 setInterval 的第一个参数, 这么写的代码明显质量很差。当需要向回调函数传递参数时,可以创建一个匿名函数,在函数内执行真实的回调函数。

另外,应该避免使用 setInterval,因为它的定时执行不会被 JavaScript 阻塞。




 类似资料: