Javascript高级编程

    原文引用地址:http://bonsaiden.github.io/JavaScript-Garden/zh/javascript

    对象java

    javascript中全部变量都是对象,除了两个例外null和undefined。git

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

function Foo(){}
Foo.bar = 1;
Foo.bar; // 1

    一个常见的误解时数字的字面值不是对象。这是由于javascript解析器的一个错误,它试图将点操做符解析为浮点数字面值的一部分。程序员

2.toString();//SyntaxError

    有不少变通方法可让数字的字面值看起来像对象。github

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

    对象做为数据类型ajax

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

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

var foo = {}; // 一个空对象
// 一个新对象,拥有一个值为12的自定义属性'test'
var bar = {test: 12}; 

    访问属性浏览器

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

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

var get = 'name';
foo[get]; // kitten

foo.1234; // SyntaxError
foo['1234']; // works

    两种语法是等价的,可是中括号操做符在下面两种状况下依然有效-动态设置属性-属性名不是一个有效的变量名(好比属性名中包含空格,或者属性名是js的关键字)

    删除属性

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

var obj = {
    bar: 1,
    foo: 2,
    baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;

for(var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i, '' + obj[i]);
    }
}

    上面的输出结果有bar undefined和foo null -只有 baz 被真正的删除了,因此从输出结果中消失。

    属性名的语法

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解析器的另外一个错误设计,上面的第二种声明方式在ECMAScript5以前会抛出SyntaxError的错误。

    这个错误的缘由是delete是javascript语言的一个关键字;所以为了在更低版本的javascript也能正常运行,必须使用字符串字面值声明方式。

    原型

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

    虽然这常常被看成是javascript的缺点被说起,其实基于原型的继承模型比传统的类继承还要强大。实现传统的类继承模型是很简单,可是实现javascript多种的原型继承则要困难的多。

    第一个不一样之处在于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会向上遍历原型链,知道找到给定名称的属性为止。
    到查找到达原型链的顶部-也就是Object.prototype - 可是仍然没有找到指定的属性,就会返回undefined。

    原型属性

    当圆形属性用来建立原型链时,能够把任何类型的值赋给它(prototype)。

    然而将原子类型赋给prototype的操做将会被忽略。

function Foo() {}
Foo.prototype = 1; // 无效

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

    性能

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

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

    扩展内置类型的原型

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

    这种技术被称为monkey patching而且会破坏封装。虽然它被普遍的应用到一些javascript类库中好比prototype,可是我仍然认为为内置类型添加一些非标准的函数不是个好主意。

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

    总结

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

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

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

// 修改Object.prototype
Object.prototype.bar = 1; 
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true

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

    hasOwnProperty做为属性

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

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

foo.hasOwnProperty('bar'); // 老是返回 false

// 使用其它对象的 hasOwnProperty,并将其上下文设置为foo
({}).hasOwnProperty.call(foo, 'bar'); // true

    结论

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

    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
}

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

    使用hasOwnProperty 过滤

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

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

    一个普遍使用的类库prototype就扩展了原生的javascript对象。所以当这个类库被包含在页面中时,不使用hasOwnProperty 过滤的for in 循环不免会出问题。

    总结

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

    函数

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

    函数声明

function foo() {}

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

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

    函数赋值表达式

var foo = function() {};

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

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

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

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

    命名函数的赋值表达式

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

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

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

    this的工做原理

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

    全局范围内 

 this

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

    函数调用

foo();

    这里this也会指向全局对象

    方法调用

test.foo();

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

    调用构造函数

 new foo();

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

    显示的设置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。

    常见误解

    尽管大部分的状况说的过去,不过第一个规则(这里指的是应该是第二个规则,也就是直接调用函数时,this指向全局对象)被认为是javascript语言另外一个错误设计的地方,由于它历来就没有实际用途。

Foo.method = function() {
    function test() {
        // this 将会被设置为全局对象(译者注:浏览器环境中也就是 window 对象)
    }
    test();
}

    一个常见的误解是test中的this将会指向foo对象,实际上不是这个样子的。
    为了在test中获取对foo对象的引用,咱们须要在method函数内部建立一个局部变量指向foo对象。

Foo.method = function() {
    var that = this;
    function test() {
        // 使用 that 来指向 Foo 对象
    }
    test();
}

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

    方法的赋值表达式

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

var test = someObject.methodTest;
test();

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

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

function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;

new Bar().method();

    当method被调用时,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

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

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

var foo = new Counter(4);
foo.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的一个拷贝。

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

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

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

    arguments对象

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

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

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

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

Array.prototype.slice.call(arguments);

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

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

function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // do stuff here
}

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

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

Foo.method = function() {
    var args = Array.prototype.slice.call(arguments);
    Foo.prototype.method.apply(args[0], args.slice(1));
};

    自动更新

    arguments 对象为其内部属性以及函数形式参数建立 getter 和 setter 方法。
    所以,改变形参的值会影响到 arguments 对象的值,反之亦然。

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

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

    var d = c;
    d = 9;
    c; // 3
}
foo(1, 2, 3);

    性能真相

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

    arguments 的 getters 和 setters 方法总会被建立;所以使用 arguments 对性能不会有什么影响。 除非是须要对 arguments 对象的属性进行屡次访问。
    在 MDC 中对 strict mode 模式下 arguments 的描述有助于咱们的理解,请看下面代码

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

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

function foo() {
    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++) {
        foo(); // Would normally be inlined...
    }
}

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

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

    JavaScript 中的构造函数和其它语言中的构造函数是不一样的。 经过 new 关键字方式调用的函数都被认为是构造函数。
    在构造函数内部 - 也就是被调用的函数内 - this 指向新建立的对象 Object。 这个新建立的对象的 prototype 被指向到构造函数的 prototype。
    若是被调用的函数没有显式的 return 表达式,则隐式的会返回 this 对象 - 也就是新建立的对象。

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

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

var test = new Foo();

    上面代码把 Foo 做为构造函数调用,并设置新建立对象的 prototype 为 Foo.prototype。
    显式的 return 表达式将会影响返回结果,但仅限于返回的是一个对象。

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

function Test() {
    this.value = 2;

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

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

function Bar() {
    return new Number(2);
}
new Bar().constructor === Number

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

(new Test()).value === undefined
(new Test()).foo === 1

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

function Foo() {
    this.bla = 1; // 获取设置全局参数
}
Foo(); // undefined

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

    为了避免使用 new 关键字,构造函数必须显式的返回一个值。

function Bar() {
    var value = 1;
    return {
        method: function() {
            return value;
        }
    }
}
Bar.prototype = {
    foo: function() {}
};

new Bar();
Bar();

    上面两种对 Bar 函数的调用返回的值彻底相同,一个新建立的拥有 method 属性的对象被返回, 其实这里建立了一个闭包。
    还须要注意, new Bar() 并不会改变返回对象的原型(译者注:也就是返回对象的原型不会指向 Bar.prototype)。 由于构造函数的原型会被指向到刚刚建立的新对象,而这里的 Bar 没有把这个新对象返回(译者注:而是返回了一个包含 method 属性的自定义对象)。
    在上面的例子中,使用或者不使用 new 关键字没有功能性的区别。
    上面两种方式建立的对象不能访问 Bar 原型链上的属性,以下所示:

var bar1 = new Bar();
typeof(bar1.method); // "function"
typeof(bar1.foo); // "undefined"

var bar2 = Bar();
typeof(bar2.method); // "function"
typeof(bar2.foo); // "undefined"

    经过工厂模式建立新对象

    上面两种方式建立咱们常听到的一条忠告是不要使用 new 关键字来调用函数,由于若是忘记使用它就会致使错误。对象不能访问 Bar 原型链上的属性,以下所示:
    为了建立新对象,咱们能够建立一个工厂方法,而且在方法内构造一个新对象。

function Foo() {
    var obj = {};
    obj.value = 'blub';

    var private = 2;
    obj.someMethod = function(value) {
        this.value = value;
    }

    obj.getPrivate = function() {
        return private;
    }
    return obj;
}

    虽然上面的方式比起 new 的调用方式不容易出错,而且能够充分利用私有变量带来的便利, 可是随之而来的是一些很差的地方。

    1.会占用更多的内存,由于新建立的对象不能共享原型上的方法。
    2.为了实现继承,工厂方法须要从另一个对象拷贝全部属性,或者把一个对象做为新建立对象的原型。
    3.放弃原型链仅仅是由于防止遗漏 new 带来的问题,这彷佛和语言自己的思想相违背。

    总结
    虽然遗漏 new 关键字可能会致使问题,但这并非放弃使用原型链的借口。 最终使用哪一种方式取决于应用程序的需求,选择一种代码书写风格并坚持下去才是最重要的。

    做用域与命名空间

    尽管 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);

    foo 和 i 是函数 test 内的局部变量,而对bar 的赋值将会覆盖全局做用域内的同名变量。

    变量声明提高

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

bar();
var bar = 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 = {};
}

    在 Nettuts+ 网站有一篇介绍 hoisting 的文章,其中的代码颇有启发性。

// 译者注:来自 Nettuts+ 的一段代码,生动的阐述了 JavaScript 中变量声明提高规则
var myvar = 'my value';  

(function() {  
    alert(myvar); // undefined  
    var myvar = 'local value';  
})();  

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

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

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

  1. 当前做用域内是否有 var foo 的定义。
  2. 函数形式参数是否有使用 foo 名称的。
  3. 函数自身是否叫作 foo
  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 foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]

foo.length = 6;
foo; // [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);
    结论
    应该尽可能避免使用数组构造函数建立新数组。推荐使用数组的字面语法。它们更加短小和简洁,所以增长了代码的可读性。
    类型
    typeof操做符
     typeof 操做符(和  instanceof 一块儿)或许是 JavaScript 中最大的设计缺陷, 由于几乎不可能从它们那里获得想要的结果。
    尽管  instanceof 还有一些极少数的应用场景, typeof 只有一个实际的应用( 译者注:这个实际应用是用来检测一个对象是否已经定义或者是否已经赋值), 而这个应用却不是用来检查对象的类型。
    javascript类型表格
Value               Class      Type
-------------------------------------
"foo"               String     string
new String("foo")   String     object
1.2                 Number     number
new Number(1.2)     Number     object
true                Boolean    boolean
new Boolean(true)   Boolean    object
new Date()          Date       object
new Error()         Error      object
[1,2,3]             Array      object
new Array(1, 2, 3)  Array      object
new Function("")    Function   function
/abc/g              RegExp     object (function in Nitro/V8)
new RegExp("meow")  RegExp     object (function in Nitro/V8)
{}                  Object     object
new Object()        Object     object
    上面表格中,Type 一列表示  typeof 操做符的运算结果。能够看到,这个值在大多数状况下都返回 "object"。
    Class 一列表示对象的内部属性  [[Class]] 的值。
    为了获取对象的  [[Class]],咱们须要使用定义在  Object.prototype 上的方法 toString
    对象的类定义
    JavaScript 标准文档只给出了一种获取  [[Class]] 值的方法,那就是使用  Object.prototype.toString
function is(type, obj) {
    var clas = Object.prototype.toString.call(obj).slice(8, -1);
    return obj !== undefined && obj !== null && clas === type;
}

is('String', 'test'); // true
is('String', new String('test')); // true
    上面例子中, Object.prototype.toString 方法被调用, this 被设置为了须要获取 [[Class]] 值的对象。
     Object.prototype.toString 返回一种标准格式字符串,因此上例能够经过  slice 截取指定位置的字符串,以下所示:
Object.prototype.toString.call([])    // "[object Array]"
Object.prototype.toString.call({})    // "[object Object]"
Object.prototype.toString.call(2)    // "[object Number]"
    这种变化能够从 IE8 和 Firefox 4 中看出区别,以下所示:
// IE8
Object.prototype.toString.call(null)    // "[object Object]"
Object.prototype.toString.call(undefined)    // "[object Object]"

// Firefox 4
Object.prototype.toString.call(null)    // "[object Null]"
Object.prototype.toString.call(undefined)    // "[object Undefined]"
    测试为定义变量
    上面代码会检测  foo 是否已经定义;若是没有定义而直接使用会致使  ReferenceError 的异常。 这是 typeof 惟一有用的地方。
    结论
    为了检测一个对象的类型,强烈推荐使用  Object.prototype.toString 方法; 由于这是惟一一个可依赖的方式。正如上面表格所示, typeof 的一些返回值在标准文档中并未定义, 所以不一样的引擎实现可能不一样。
    除非为了检测一个变量是否已经定义,咱们应尽可能避免使用  typeof 操做符。
    instanceof操做符
     instanceof 操做符用来比较两个操做数的构造函数。只有在比较自定义的对象时才有意义。 若是用来比较内置类型,将会和 typeof 操做符 同样用处不大。
    比较自定义对象
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();

new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true

// 若是仅仅设置 Bar.prototype 为函数 Foo 自己,而不是 Foo 构造函数的一个实例
Bar.prototype = Foo;
new Bar() instanceof Foo; // false
    instanceof比较内置类型
new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true

'foo' instanceof String; // false
'foo' instanceof Object; // false
    有一点须要注意, instanceof 用来比较属于不一样 JavaScript 上下文的对象(好比,浏览器中不一样的文档结构)时将会出错, 由于它们的构造函数不会是同一个对象。
    结论
     instanceof 操做符应该仅仅用来比较来自同一个 JavaScript 上下文的自定义对象。 正如 typeof 操做符同样,任何其它的用法都应该是避免的。
    类型转换
    JavaScript 是弱类型语言,因此会在任何可能的状况下应用强制类型转换。
// 下面的比较结果是:true
new Number(10) == 10; // Number.toString() 返回的字符串被再次转换为数字

10 == '10';           // 字符串被转换为数字
10 == '+10 ';         // 同上
10 == '010';          // 同上 
isNaN(null) == false; // null 被转换为数字 0
                      // 0 固然不是一个 NaN(译者注:否认之否认)

// 下面的比较结果是:false
10 == 010;
10 == '-10';
    为了不上面复杂的强制类型转换,强烈推荐使用 严格的等于操做符。 虽然这能够避免大部分的问题,但 JavaScript 的弱类型系统仍然会致使一些其它问题。
    内置类型的构造函数
    内置类型(好比  Number 和  String)的构造函数在被调用时,使用或者不使用  new 的结果彻底不一样。
new Number(10) === 10;     // False, 对象与数字的比较
Number(10) === 10;         // True, 数字与数字的比较
    使用内置类型  Number 做为构造函数将会建立一个新的  Number 对象, 而在不使用 new 关键字的  Number 函数更像是一个数字转换器。
    另外,在比较中引入对象的字面值将会致使更加复杂的强制类型转换。
    最好的选择是把要比较的值显式的转换为三种可能的类型之一。
    转换为字符串
'' + 10 === '10'; // true
    将一个值加上空字符串能够轻松转换为字符串类型。
    转换为数字
+'10' === 10; // true
    使用一元的加号操做符,能够把字符串转换为数字。
    字符串转换为数字的经常使用方法:
+'010' === 10
Number('010') === 10
parseInt('010', 10) === 10  // 用来转换为整数

+'010.2' === 10.2
Number('010.2') === 10.2
parseInt('010.2', 10) === 10
    转换为布尔型
    经过使用 否 操做符两次,能够把一个值转换为布尔型。
!!'foo';   // true
!!'';      // false
!!'0';     // true
!!'1';     // true
!!'-1'     // true
!!{};      // true
!!true;    // true
    核心
    为何不要使用eval
     eval 函数会在当前做用域中执行一段 JavaScript 代码字符串。
var foo = 1;
function test() {
    var foo = 2;
    eval('foo = 3');
    return foo;
}
test(); // 3
foo; // 1
    可是  eval 只在被直接调用而且调用函数就是  eval 自己时,才在当前做用域中执行。
var foo = 1;
function test() {
    var foo = 2;
    var bar = eval;
    bar('foo = 3');
    return foo;
}
test(); // 2
foo; // 3
    上面的代码等价于在全局做用域中调用  eval,和下面两种写法效果同样:
// 写法一:直接调用全局做用域下的 foo 变量
var foo = 1;
function test() {
    var foo = 2;
    window.foo = 3;
    return foo;
}
test(); // 2
foo; // 3

// 写法二:使用 call 函数修改 eval 执行的上下文为全局做用域
var foo = 1;
function test() {
    var foo = 2;
    eval.call(window, 'foo = 3');
    return foo;
}
test(); // 2
foo; // 3
    在任何状况下咱们都应该避免使用  eval 函数。99.9% 使用 eval 的场景都有不使用  eval 的解决方案。
    假装的eval
     定时函数 setTimeout 和  setInterval 均可以接受字符串做为它们的第一个参数。 这个字符串老是在全局做用域中执行,所以 eval 在这种状况下没有被直接调用。
    安全问题
     eval 也存在安全问题,由于它会执行任意传给它的代码, 在代码字符串未知或者是来自一个不信任的源时,绝对不要使用 eval 函数。
    结论
    绝对不要使用  eval,任何使用它的代码都会在它的工做方式,性能和安全性方面受到质疑。 若是一些状况必须使用到  eval 才能正常工做,首先它的设计会受到质疑,这不该该是首选的解决方案, 一个更好的不使用  eval 的解决方案应该获得充分考虑并优先采用。
    undefined和null
    JavaScript 有两个表示‘空’的值,其中比较有用的是  undefined
     undefined 是一个值为  undefined 的类型。
    这个语言也定义了一个全局变量,它的值是  undefined,这个变量也被称为  undefined。 可是这个变量不是一个常量,也不是一个关键字。这意味着它的值能够轻易被覆盖。
    下面的状况会返回  undefined 值:
  • 访问未修改的全局变量 undefined
  • 因为没有定义 return 表达式的函数隐式返回。
  • return 表达式没有显式的返回任何内容。
  • 访问不存在的属性。
  • 函数参数没有被显式的传递值。
  • 任何被设置为 undefined 值的变量。
    处理undefined值得改变
    因为全局变量  undefined 只是保存了  undefined 类型实际值的副本, 所以对它赋新值不会改变类型 undefined 的值。
    然而,为了方便其它变量和  undefined 作比较,咱们须要事先获取类型  undefined 的值。
    为了不可能对  undefined 值的改变,一个经常使用的技巧是使用一个传递到 匿名包装器的额外参数。 在调用时,这个参数不会获取任何值。
var undefined = 123;
(function(something, foo, undefined) {
    // 局部做用域里的 undefined 变量从新得到了 `undefined` 值

})('Hello World', 42);
    另一种达到相同目的方法是在函数内使用变量声明。
var undefined = 123;
(function(something, foo) {
    var undefined;
    ...

})('Hello World', 42);
    这里惟一的区别是,在压缩后而且函数内没有其它须要使用  var 声明变量的状况下,这个版本的代码会多出 4 个字节的代码。
    null的用处
    JavaScript 中的  undefined 的使用场景相似于其它语言中的 null,实际上 JavaScript 中的 null 是另一种数据类型。
    它在 JavaScript 内部有一些使用场景(好比声明原型链的终结  Foo.prototype = null),可是大多数状况下均可以使用 undefined 来代替。
    自动分号插入
    尽管 JavaScript 有 C 的代码风格,可是它不强制要求在代码中使用分号,实际上能够省略它们。
    JavaScript 不是一个没有分号的语言,偏偏相反上它须要分号来就解析源代码。 所以 JavaScript 解析器在遇到因为缺乏分号致使的解析错误时,会自动在源代码中插入分号。
var foo = function() {
} // 解析错误,分号丢失
test()
    自动插入分号,解析器从新解析
var foo = function() {
}; // 没有错误,解析继续
test()
    自动的分号插入被认为是 JavaScript 语言最大的设计缺陷之一,由于它能改变代码的行为。
    工做原理
    下面的代码没有分号,所以解析器须要本身判断须要在哪些地方插入分号。
(function(window, undefined) {
    function test(options) {
        log('testing!')

        (options.list || []).forEach(function(i) {

        })

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        )

        return
        {
            foo: function() {}
        }
    }
    window.test = test

})(window)

(function(window) {
    window.someLibrary = {}
})(window)
     下面是解析器"猜想"的结果。
(function(window, undefined) {
    function test(options) {

        // 没有插入分号,两行被合并为一行
        log('testing!')(options.list || []).forEach(function(i) {

        }); // <- 插入分号

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        ); // <- 插入分号

        return; // <- 插入分号, 改变了 return 表达式的行为
        { // 做为一个代码段处理
            foo: function() {} 
        }; // <- 插入分号
    }
    window.test = test; // <- 插入分号

// 两行又被合并了
})(window)(function(window) {
    window.someLibrary = {}; // <- 插入分号
})(window); //<- 插入分号
    解析器显著改变了上面代码的行为,在另一些状况下也会作出错误的处理。
    前置括号
    在前置括号的状况下,解析器不会自动插入分号。
log('testing!')
(options.list || []).forEach(function(i) {})
    上面代码被解析器转换为一行。 
log('testing!')(options.list || []).forEach(function(i) {})
    结论
    建议绝对不要省略分号,同时也提倡将花括号和相应的表达式放在一行, 对于只有一行代码的  if 或者 else 表达式,也不该该省略花括号。 这些良好的编程习惯不只能够提到代码的一致性,并且能够防止解析器改变代码行为的错误处理。
    其余
    setTimeout和setInterval
    因为 JavaScript 是异步的,可使用  setTimeout 和  setInterval 来计划执行函数。
function foo() {}
var id = setTimeout(foo, 1000); // 返回一个大于零的数字
    当  setTimeout 被调用时,它会返回一个 ID 标识而且计划在未来大约 1000 毫秒后调用 foo 函数。  foo 函数只会被执行一次。
    基于 JavaScript 引擎的计时策略,以及本质上的单线程运行方式,因此其它代码的运行可能会阻塞此线程。 所以无法确保函数会在 setTimeout 指定的时刻被调用。
    做为第一个参数的函数将会在全局做用域中执行,所以函数内的  this 将会指向这个全局对象。
function Foo() {
    this.value = 42;
    this.method = function() {
        // this 指向全局对象
        console.log(this.value); // 输出:undefined
    };
    setTimeout(this.method, 500);
}
new Foo();
    setInterval的堆调用
     setTimeout 只会执行回调函数一次,不过  setInterval - 正如名字建议的 - 会每隔 X 毫秒执行函数一次。 可是却不鼓励使用这个函数。
    当回调函数的执行被阻塞时, setInterval 仍然会发布更多的回调指令。在很小的定时间隔状况下,这会致使回调函数被堆积起来。
function foo(){
    // 阻塞执行 1 秒
}
setInterval(foo, 1000);
    上面代码中, foo 会执行一次随后被阻塞了一分钟。
    在  foo 被阻塞的时候, setInterval 仍然在组织未来对回调函数的调用。 所以,当第一次  foo 函数调用结束时,已经有 10 次函数调用在等待执行。
    处理可能的阻塞调用
    最简单也是最容易控制的方案,是在回调函数内部使用  setTimeout 函数。  
function foo(){
    // 阻塞执行 1 秒
    setTimeout(foo, 1000);
}
foo();
    这样不只封装了  setTimeout 回调函数,并且阻止了调用指令的堆积,能够有更多的控制。 foo 函数如今能够控制是否继续执行仍是终止执行。
    手工清空定时器
    能够经过将定时时产生的 ID 标识传递给  clearTimeout 或者  clearInterval 函数来清除定时, 至于使用哪一个函数取决于调用的时候使用的是 setTimeout 仍是  setInterval
var id = setTimeout(foo, 1000);
clearTimeout(id);
    清除全部定时器
    因为没有内置的清除全部定时器的方法,能够采用一种暴力的方式来达到这一目的。
// 清空"全部"的定时器
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}
    可能还有些定时器不会在上面代码中被清除( 译者注:若是定时器调用时返回的 ID 值大于 1000), 所以咱们能够事先保存全部的定时器 ID,而后一把清除。
    隐藏使用eval
     setTimeout 和  setInterval 也接受第一个参数为字符串的状况。 这个特性绝对不要使用,由于它在内部使用了 eval
function foo() {
    // 将会被调用
}

function bar() {
    function foo() {
        // 不会被调用
    }
    setTimeout('foo()', 1000);
}
bar();
    因为  eval 在这种状况下不是被 直接调用,所以传递到 setTimeout 的字符串会自全局做用域中执行; 所以,上面的回调函数使用的不是定义在  bar 做用域中的局部变量 foo
    建议不要在调用定时器函数时,为了向回调函数传递参数而使用字符串的形式。
function foo(a, b, c) {}

// 不要这样作
setTimeout('foo(1,2, 3)', 1000)

// 可使用匿名函数完成相同功能
setTimeout(function() {
    foo(a, b, c);
}, 1000)
    结论
    绝对不要使用字符串做为  setTimeout 或者  setInterval 的第一个参数, 这么写的代码明显质量不好。当须要向回调函数传递参数时,能够建立一个匿名函数,在函数内执行真实的回调函数。
    另外,应该避免使用  setInterval,由于它的定时执行不会被 JavaScript 阻塞。
相关文章
相关标签/搜索