JavaScript的小秘密

对象


JavaScript 中所哟变量均可以看成对象使用,除了两个例外 null 和 undefined。程序员

false.toString(); // false
[1, 2, 3].toString(); // "1,2,3"

funciton Fun(){}
Fun.num = 1;
Fun.num; // 1
复制代码

一个常见的误解是数字的字面值不能看成对象使用。这是由于JavaScript解析器的一个错误,他试图将点操做符解析为浮点数字字面值的一部分。数组

7.toString(); // SyntaxError
复制代码

有一些变通的方法可让数字的字面值看起来像对象。浏览器

7..toString(); // 第二个点能够正常解析
7 .toString(); // 注意点前面的空格
(7).toString(); // 2 先被计算
复制代码

对象做为数据类型

JavaScript的对象能够做为哈希表使用,主要用来保存命名的键与值的对应关系。缓存

使用对象的字面语法 {} 能够建立一个简单对象。这个新建立的对象从 Object.prototype 继承下面,没有任何自定义属性。bash

var obj = {} // 一个空对象

// 一个对象,拥有12的自定义属性'test'
var obj2 = {
    test: 12
}
复制代码

访问属性

有两种方式来访问对象的属性,点操做符或者中括号操做符。闭包

var obj = {
    name: 'kitten'
}
obj.name; // kitten
obj[name]; // kitten

var name = 'name'
obj[name]; // kitten
复制代码

两种语法是等价的,可是中括号操做符在下面两种状况下依然有效app

  • 动态设置属性
  • 属性名不是一个有效的变量名(好比:属性名中包含空格,或者属性名是 JS 的关键词)

删除属性

删除属性的惟一方法是使用 delete 操做符;设置属性为 undefined 或者 null 并不能真正的删除属性, 而仅仅是移除了属性和值的关联。异步

var obj = {
    a: 1,
    b: 2,
    c: 3
};
obj.a = undefined;
obj.b = null;
delete obj.c;
for (var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i, '' + obj[i]);
    }
}
复制代码

上面的输出结果有 a undefined 和 b null ,只有 c 被真正的删除了,因此从输出结果中消失。模块化

属性名的语法

var test = {
    'case': 'I am a keyword so I must be notated as a string',
    delete: 'I am a keyword too so me' // 出错:SyntaxError
}
复制代码

对象的属性名可使用字符串或者普通字符声明。可是因为 JavaScript 解析器的另外一个错误设计, 上面的第二种声明方式在 ECMAScript 5 以前会抛出 SyntaxError 的错误。 这个错误的缘由是 delete 是 JavaScript 语言的一个关键词;所以为了在更低版本的 JavaScript 引擎下也能正常运行, 必须使用字符串字面值声明方式。函数


原型

JavaScript 不包含传统的类继承模型,而是使用 prototype 原型模型。

因为 JavaScript 是惟一一个被普遍使用的基于原型继承的语言,因此理解两种继承模式的差别是须要必定时间的。 第一个不一样之处在于 JavaScript 使用 原型链 的继承方式。

function Fun() {
    this.value  = 97;
}
Fun.prorotype = {
    method: function() {}
};
function Demo() {}

// 设置Demo的prototype属性为Fun的示例对象
Demo.prototype = new Fun();
Demo.prototype.fun = 'Hello啊';

// 修正Demo.prototype.constructor 为 Demo 自己
Demo.prototype.constructor = Demo;
var test = new Demo(); // 建立一个Demo的一个新实例

// 原型链
test [Demo的示例]
    Demo.prototype [Fun的实例]
        { fun: 'Hello啊' }
        Foo.prototype
            { method: ...}
                Object.prototype
                    {toString:... /* etc. */}
复制代码

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

属性查找

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

到查找到达原型链的顶部,也就是 Object.prototype 可是仍然没有找到指定的属性,就会返回 undefined。

原型属性

当原型属性用来建立原型链时,能够把任何类型的值赋给它(prototype)。 然而将原子类型赋给 prototype 的操做将会被忽略。

function Fun() {}
Fun.prototype = 1; // 无效
复制代码

而将对象赋值给 prototype,正如上面的例子所示,将会动态的建立原型链。

性能

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

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

扩展内置类型的原型

一个错误特性被常用,那就是扩展 Object.prototype 或者其余内置类型的原型对象。

这样会破坏封装。虽然它被普遍的应用到一些 JavaScript 类库中好比P rototype ,可是我不认为为内置类型添加一些非标准的函数是个好主意。

扩展内置类型的惟一理由是为了和新的 JavaScript 保持一致,好比 Array.forEach

总结

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

hasOwnProperty 函数

为了判断一个对象是否包含自定义属性而不是原型链上的属性, 咱们须要使用继承自 Object.prototype 的 hasOwnProperty 方法。

hasOwnProperty 是 JavaScript 中惟一一个处理属性可是不查找原型链的函数。

// 修改Object.prototype
Object.prototype.value = 1
var obj = {
    s: undefined
};
obj.value; // 1
's' in obj; // true

obj.hasOwnProperty('s'); // true
obj.hasOwnProperty('value'); // false
复制代码

只有 hasOwnProperty 能够给出正确和指望的结果,这在遍历对象的属性时会颇有用。 没有其它方法能够用来排除原型链上的属性,而不是定义在对象自身上的属性。

hasOwnProperty 做为属性

JavaScript 不会保护 hasOwnProperty 被非法占用,所以若是一个对象碰巧存在这个属性, 就须要使用外部的 hasOwnProperty 函数来获取正确的结果。

var obj = {
    hasOwnProperty: function() {
        return false;
    },
    str: 'Here be dragons'
};

obj.hasOwnProperty('str'); // 老是返回 false

// 使用其它对象的 hasOwnProperty,并将其上下文设置为obj
({}).hasOwnProperty.call(obj, 'str'); // true
复制代码

结论

当检查对象上某个属性是否存在时,hasOwnProperty 是惟一可用的方法。 同时在使用 for in loop 遍历对象时,推荐老是使用 hasOwnProperty 方法, 这将会避免原型对象扩展带来的干扰。


for in 循环

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

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

var obj = {num: 2};
for(var i in obj) {
    console.log(i); // 输出两个属性:value 和 num
}
复制代码

因为不可能改变 for in 自身的行为,所以有必要过滤出那些不但愿出如今循环体中的属性, 这能够经过 Object.prototype 原型上的 hasOwnProperty 函数来完成。

使用 hasOwnProperty 过滤

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

这个版本的代码是惟一正确的写法。因为咱们使用了 hasOwnProperty,因此此次只输出 num。 若是不使用 hasOwnProperty,则这段代码在原生对象原型(好比 Object.prototype)被扩展时可能会出错。

总结

推荐老是使用 hasOwnProperty。不要对代码运行的环境作任何假设,不要假设原生对象是否已经被扩展了。


函数f(x)


函数声明与表达式

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

函数声明

function fun() {}
复制代码

上面的方法会在执行前被解析(hoisted),所以它存在于当前上下文的任意一个地方, 即便在函数定义体的上面被调用也是对的。

fun(); // 正常运行,由于fun在代码运行前已经被建立
function fun() {}
复制代码

函数赋值表达式

var fun = function() {};
复制代码

这个例子把一个匿名的函数赋值给变量 fun。

fun; // 'undefined'
fun(); // 出错:TypeError
var fun = function() {};
复制代码

因为 var 定义了一个声明语句,对变量 fun 的解析是在代码运行以前,所以 fun 变量在代码运行时已经被定义过了。

可是因为赋值语句只在运行时执行,所以在相应代码执行以前, fun 的值缺省为 undefined。

命名函数的赋值表达式

另一个特殊的状况是将命名函数赋值给一个变量。

var demo = function fun() {
    fun(); // 正常运行
}
fun(); // 出错:ReferenceError
复制代码

fun 函数声明外是不可见的,这是由于咱们已经把函数赋值给了 demo; 然而在 fun 内部依然可见。这是因为 JavaScript 的 命名处理所致, 函数名在函数内老是可见的。

注意:在IE8及IE8如下版本浏览器bar在外部也是可见的,是由于浏览器对命名函数赋值表达式进行了错误的解析, 解析成两个函数 demo 和 fun


this 的工做原理

JavaScript 有一套彻底不一样于其它语言的对 this 的处理机制。 在五种不一样的状况下 ,this 指向的各不相同。

全局范围内

this;
复制代码

当在所有范围内使用 this,它将会指向全局对象。

函数调用

fun();
复制代码

这里 this 也会指向全局对象。(浏览器中运行的 JavaScript 脚本,这个全局对象是 window。)

ES5 注意: 在严格模式下(strict mode),不存在全局变量。 这种状况下 this 将会是 undefined。

方法调用

test.foo();
复制代码

这个例子中,this 指向 test 对象。

调用构造函数

new foo(); 
复制代码

若是函数倾向于和 new 关键词一块使用,则咱们称这个函数是 构造函数。 在函数内部,this 指向新建立的对象。

显式的设置 this

function fun(a, b, c) {}

var obj = {};
fun.apply(obj, [1, 2, 3]); // 数组将会被扩展,以下所示
fun.call(obj, 1, 2, 3); // 传递到fun的参数是:a = 1, b = 2, c = 3
复制代码

常见误解

尽管大部分的状况都说的过去,可是直接调用函数时,this 指向全局对象。被认为是JavaScript语言另外一个错误设计的地方,由于它历来就没有实际的用途。

Fun.method = function() {
    function test() {
        // this 将会被设置为全局对象(浏览器环境中也就是 window 对象)
    }
    test();
}
复制代码

一个常见的误解是 test 中的 this 将会指向 Fun 对象,实际上不是这样子的。

为了在 test 中获取对 Fun 对象的引用,咱们须要在 method 函数内部建立一个局部变量指向 Fun 对象。

Fun.method = function() {
    var _this = this;
    function test() {
        // 使用 _this 来指向 Fun 对象
    }
    test();
}
复制代码

_this 只是咱们随意起的名字,不过这个名字被普遍的用来指向外部的 this 对象。 在 闭包 一节,咱们能够看到 _this 能够做为参数传递。

方法的赋值表达式

另外一个看起来奇怪的地方是函数别名,也就是将一个方法赋值给一个变量。

var test = someObject.methodTest;
test();
复制代码

上例中,test 就像一个普通的函数被调用;所以,函数内的 this 将再也不被指向到 someObject 对象。

虽然 this 的晚绑定特性彷佛并不友好,但这确实是基于原型继承赖以生存的土壤。

function Fun() {}
Fun.prototype.method = function() {};

function Test() {}
Test.prototype = Foo.prototype;

new Test().method();
复制代码

当 method 被调用时,this 将会指向 Test 的实例对象。


闭包和引用

闭包是 JavaScript 一个很是重要的特性,这意味着当前做用域老是可以访问外部做用域中的变量。 由于 函数 是 JavaScript 中惟一拥有自身做用域的结构,所以闭包的建立依赖于函数。

模拟私有变量

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

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

var obj = Counter(4);
obj.increment();
obj.get(); // 5
复制代码

这里,Counter 函数返回两个闭包,函数 increment 和函数 get。 这两个函数都维持着 对外部做用域 Counter 的引用,所以总能够访问此做用域内定义的变量 count.

为何不能够在外部访问私有变量

由于 JavaScript 中不能够对做用域进行引用或赋值,所以没有办法在外部访问 count 变量。 惟一的途径就是经过那两个闭包。

var obj = new Counter(4);
obj.hack = function() {
    count = 1337;
};
复制代码

上面的代码不会改变定义在 Counter 做用域中的 count 变量的值,由于 foo.hack 没有 定义在那个做用域内。它将会建立或者覆盖全局变量 count。

循环中的闭包

一个常见的错误出如今循环中使用闭包,假设咱们须要在每次循环中调用循环序号

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

上面的代码不会输出数字 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);
}
复制代码

外部的匿名函数会当即执行,并把 i 做为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。

当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。

有另外一个方法完成一样的工做,那就是从匿名包装器中返回一个函数。这和上面的代码效果同样。

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

固然,使用ES6中的let关键字是十分方便的,由于 let 关键字所在的代码块内有效,并且有暂时性死区的约束。

for(let i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}
复制代码

arguments 对象

JavaScript 中每一个函数内都能访问一个特别变量 arguments。这个变量维护着全部传递到这个函数中的参数列表。

arguments 变量不是一个数组(Array)。 尽管在语法上它有数组相关的属性 length,但它不从 Array.prototype 继承,实际上它是一个对象(Object)。

所以,没法对 arguments 变量使用标准的数组方法,好比 push, pop 或者 slice。 虽然使用 for 循环遍历也是能够的,可是为了更好的使用数组方法,最好把它转化为一个真正的数组。

转化为数组

下面的代码将会建立一个新的数组,包含全部 arguments 对象中的元素。

Array.prototype.slice.call(arguments);
复制代码

这个转化比较慢,在性能很差的代码中不推荐这种作法。

使用ES6提供的Array.from()将arguments 变量这种伪数组转化为数组也是十分方便的。

Array.from(arguments);
复制代码

传递参数

下面是将参数从一个函数传递到另外一个函数的推荐作法。

function fun() {
    demo.apply(null, arguments);
}
function demo(a, b, c) {
    // 干活
}
复制代码

另外一个技巧是同时使用 call 和 apply,建立一个快速的解绑定包装器。

function Fun() {}

Fun.prototype.method = function(a, b, c) {
    console.log(this, a, b, c);
};

// 建立一个解绑定的 "method"
// 输入参数为: this, arg1, arg2...argN
Fun.method = function() {

    // 结果: Fun.prototype.method.call(this, arg1, arg2... argN)
    Function.call.apply(Fun.prototype.method, arguments);
};
复制代码

上面的 Fun.method 函数和下面代码的效果是同样的:

Fun.method = function() {
    var args = Array.prototype.slice.call(arguments);
    Fun.prototype.method.apply(args[0], args.slice(1));
};
复制代码

自动更新

arguments 对象为其内部属性以及函数形式参数建立 gettersetter 方法。

所以,改变形参的值会影响到 arguments 对象的值,反之亦然。

function fun(a, b, c) {
    arguments[0] = 2;
    a; // 2                                                           

    b = 4;
    arguments[1]; // 4

    var d = c;
    d = 9;
    c; // 3
}
fun(1, 2, 3);
复制代码

性能真相

无论它是否有被使用,arguments 对象总会被建立,除了两个特殊状况 - 做为局部变量声明和做为形式参数。

arguments 的 getterssetters 方法总会被建立;所以使用 arguments 对性能不会有什么影响。 除非是须要对 arguments 对象的属性进行屡次访问。

在 MDC 中对 strict mode 模式下 arguments 的描述有助于咱们的理解,请看下面代码:

// 阐述在 ES5 的严格模式下 `arguments` 的特性
function f(a) {
  "use strict";
  a = 42;
  return [a, arguments[0]];
}
var pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);
复制代码

然而,的确有一种状况会显著的影响现代 JavaScript 引擎的性能。这就是使用 arguments.callee。

function fun() {
    arguments.callee; // do something with this function object
    arguments.callee.caller; // and the calling function object
}

function bigLoop() {
    for(var i = 0; i < 100000; i++) {
        fun(); // Would normally be inlined...
    }
}
复制代码

上面代码中,fun 再也不是一个单纯的内联函数 inlining(译者注:这里指的是解析器能够作内联处理), 由于它须要知道它本身和它的调用者。 这不只抵消了内联函数带来的性能提高,并且破坏了封装,所以如今函数可能要依赖于特定的上下文。

所以强烈建议你们不要使用 arguments.callee 和它的属性。


构造函数

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

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

若是被调用的函数没有显式的 return 表达式,则隐式的会返回 this 对象 - 也就是新建立的对象。

function Fun() {
    this.bla = 1;
}

Fun.prototype.test = function() {
    console.log(this.bla);
};

var test = new Fun();
复制代码

上面代码把 Fun 做为构造函数调用,并设置新建立对象的 prototype 为 Fun.prototype。

显式的 return 表达式将会影响返回结果,但仅限于返回的是一个对象。

function Demo() {
    return 2;
}
new Demo(); // 返回新建立的对象

function Test() {
    this.value = 2;

    return {
        foo: 1
    };
}
new Test(); // 返回的对象
复制代码

new Demo() 返回的是新建立的对象,而不是数字的字面值 2。 所以 new Demo().constructor === Demo,可是若是返回的是数字对象,结果就不一样了,以下所示

function Demo() {
    return new Number(2);
}
new Demo().constructor === Number
复制代码

这里获得的 new Test()是函数返回的对象,而不是经过new关键字新建立的对象,所以:

(new Test()).value === undefined
(new Test()).foo === 1
复制代码

若是 new 被遗漏了,则函数不会返回新建立的对象。

function Fun() {
    this.bla = 1; // 获取设置全局参数
}
Fun(); // undefined
复制代码

虽然上例在有些状况下也能正常运行,可是因为 JavaScript 中 this 的工做原理, 这里的 this 指向***全局对象***。


做用域与命名空间

尽管 JavaScript 支持一对花括号建立的代码段,可是并不支持块级做用域; 而仅仅支持 函数做用域。

function test() { // 一个做用域
    for(var i = 0; i < 10; i++) { // 不是一个做用域
        // count
    }
    console.log(i); // 10
}
复制代码

若是 return 对象的左括号和 return 不在一行上就会出错。

// 下面输出 undefined
function add(a, b) {
    return 
        a + b;
}
console.log(add(1, 2));
复制代码

JavaScript 中没有显式的命名空间定义,这就意味着全部对象都定义在一个全局共享的命名空间下面。

每次引用一个变量,JavaScript 会向上遍历整个做用域直到找到这个变量为止。 若是到达全局做用域可是这个变量仍未找到,则会抛出 ReferenceError 异常。

隐式的全局变量

// 脚本 A
foo = '42';

// 脚本 B
var foo = '42'
复制代码

上面两段脚本效果不一样。脚本 A 在全局做用域内定义了变量 foo,而脚本 B 在当前做用域内定义变量 foo。

再次强调,上面的效果彻底不一样,不使用 var 声明变量将会致使隐式的全局变量产生。

// 全局做用域
var foo = 42;
function test() {
    // 局部做用域
    foo = 21;
}
test();
foo; // 21
复制代码

在函数 test 内不使用 var 关键字声明 foo 变量将会覆盖外部的同名变量。 起初这看起来并非大问题,可是当有成千上万行代码时,不使用 var 声明变量将会带来难以跟踪的 BUG。

// 全局做用域
var items = [/* 数组 */];
for(var i = 0; i < 10; i++) {
    subLoop();
}

function subLoop() {
    // subLoop 函数做用域
    for(i = 0; i < 10; i++) { // 没有使用 var 声明变量
        // 干活
    }
}
复制代码

外部循环在第一次调用 subLoop 以后就会终止,由于 subLoop 覆盖了全局变量 i。 在第二个 for 循环中使用 var 声明变量能够避免这种错误。 声明变量时绝对不要遗漏 var 关键字,除非这就是指望的影响外部做用域的行为。

局部变量

JavaScript 中局部变量只可能经过两种方式声明,一个是做为函数参数,另外一个是经过 var 关键字声明。

// 全局变量
var foo = 1;
var bar = 2;
var i = 2;

function test(i) {
    // 函数 test 内的局部做用域
    i = 5;

    var foo = 3;
    bar = 4;
}
test(10);
复制代码

变量声明提高(Hoisting)

JavaScript 会提高变量声明。这意味着 var 表达式和 function 声明都将会被提高到当前做用域的顶部。

Demo();
var Demo = function() {};
var someValue = 42;

test();
function test(data) {
    if (false) {
        goo = 1;

    } else {
        var goo = 2;
    }
    for(var i = 0; i < 100; i++) {
        var e = data[i];
    }
}
复制代码

上面代码在运行以前将会被转化。JavaScript 将会把 var 表达式和 function 声明提高到当前做用域的顶部。

// var 表达式被移动到这里
var bar, someValue; // 缺省值是 'undefined'

// 函数声明也会提高
function test(data) {
    var goo, i, e; // 没有块级做用域,这些变量被移动到函数顶部
    if (false) {
        goo = 1;

    } else {
        goo = 2;
    }
    for(i = 0; i < 100; i++) {
        e = data[i];
    }
}

bar(); // 出错:TypeError,由于 bar 依然是 'undefined'
someValue = 42; // 赋值语句不会被提高规则(hoisting)影响
bar = function() {};

test();
复制代码

没有块级做用域不只致使 var 表达式被从循环内移到外部,并且使一些 if 表达式更难看懂。

在原来代码中,if 表达式看起来修改了全局变量 goo,实际上在提高规则被应用后,倒是在修改局部变量。

若是没有提高规则(hoisting)的知识,下面的代码看起来会抛出异常 ReferenceError。

// 检查 SomeImportantThing 是否已经被初始化
if (!SomeImportantThing) {
    var SomeImportantThing = {};
}
复制代码

实际上,上面的代码正常运行,由于 var 表达式会被提高到全局做用域的顶部。

var SomeImportantThing;

// 其它一些代码,可能会初始化 SomeImportantThing,也可能不会

// 检查是否已经被初始化
if (!SomeImportantThing) {
    SomeImportantThing = {};
}
复制代码

名称解析顺序

JavaScript 中的全部做用域,包括全局做用域,都有一个特别的名称 this 指向当前对象。

函数做用域内也有默认的变量 arguments,其中包含了传递到函数中的参数。

好比,当访问函数内的 obj 变量时,JavaScript 会按照下面顺序查找:

  1. 当前做用域内是否有 var obj 的定义。
  2. 函数形式参数是否有使用 obj 名称的。
  3. 函数自身是否叫作 obj。
  4. 回溯到上一级做用域,而后从 1 从新开始。

命名空间

只有一个全局做用域致使的常见错误是命名冲突。在 JavaScript中,这能够经过 匿名包装器 轻松解决。

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

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

})(); // 当即执行此匿名函数
复制代码

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

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

有一些其余的调用函数表达式的方法,好比下面的两种方式语法不一样,可是效果如出一辙。

// 另外两种方式
+function(){}();
(function(){}());
复制代码

结论

推荐使用匿名包装器(也就是自执行的匿名函数)来建立命名空间。这样不只能够防止命名冲突, 并且有利于程序的模块化。另外,使用全局变量被认为是很差的习惯。这样的代码容易产生错误而且维护成本较高。

数组

数组遍历与属性

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

因为 for in 循环会枚举原型链上的全部属性,惟一过滤这些属性的方式是使用 hasOwnProperty 函数, 所以会比普通的 for 循环慢上好多倍。

遍历

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

var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
    console.log(list[i]);
}
复制代码

上面代码有一个处理,就是经过 l = list.length 来缓存数组的长度。

虽然 length 是数组的一个属性,可是在每次循环中访问它仍是有性能开销。 可能最新的 JavaScript 引擎在这点上作了优化,可是咱们无法保证本身的代码是否运行在这些最近的引擎之上。

实际上,不使用缓存数组长度的方式比缓存版本要慢不少。

length 属性

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

var arr = [1, 2, 3, 4, 5, 6];
arr.length = 3;
arr; // [1, 2, 3]

arr.length = 6;
arr; // [1, 2, 3]
复制代码

在 Firebug 中查看此时 foo 的值是: [1, 2, 3, undefined, undefined, undefined] 可是这个结果并不许确,若是你在 Chrome 的控制台查看 foo 的结果,你会发现是这样的: [1, 2, 3] 由于在 JavaScript 中 undefined 是一个变量,注意是变量不是关键字,所以上面两个结果的意义是彻底不相同的。

// 为了验证,咱们来执行下面代码,看序号 5 是否存在于 foo 中。
5 in foo; // 无论在 Firebug 或者 Chrome 都返回 false
foo[5] = undefined;
5 in foo; // 无论在 Firebug 或者 Chrome 都返回 true
复制代码

为 length 设置一个更小的值会截断数组,可是增大 length 属性值不会对数组产生影响。

结论

为了更好的性能,推荐使用普通的 for 循环并缓存数组的 length 属性。 使用 for in 遍历数组被认为是很差的代码习惯并倾向于产生错误和致使性能问题。


Array 构造函数

因为 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
复制代码

因为只有一个参数传递到构造函数中(译者注:指的是 new Array(3); 这种调用方式),而且这个参数是数字,构造函数会返回一个 length 属性被设置为此参数的空数组。 须要特别注意的是,此时只有 length 属性被设置,真正的数组并无生成。

var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 数组尚未生成
复制代码

这种优先于设置数组长度属性的作法只在少数几种状况下有用,好比须要循环字符串,能够避免 for 循环的麻烦。

new Array(count + 1).join(stringToRepeat);
复制代码

结论

应该尽可能避免使用数组构造函数建立新数组。推荐使用数组的字面语法。它们更加短小和简洁,所以增长了代码的可读性。

相关文章
相关标签/搜索