欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

簡介

JavaScript 秘密花園是一個不斷更新,主要關心 JavaScript 一些古怪用法的文檔。 對于如何避免常見的錯誤,難以發(fā)現(xiàn)的問題,以及性能問題和不好的實踐給出建議, 初學者可以籍此深入了解 JavaScript 的語言特性。

JavaScript 秘密花園不是用來教你 JavaScript。為了更好的理解這篇文章的內容, 你需要事先學習 JavaScript 的基礎知識。在 Mozilla 開發(fā)者網(wǎng)絡中有一系列非常棒的 JavaScript 學習向導。

關于作者

這篇文章的作者是兩位 Stack Overflow 用戶, 伊沃·韋特澤爾 Ivo Wetzel(寫作) 和 張易江 Zhang Yi Jiang(設計)。

貢獻者

中文翻譯

此中文翻譯由三生石上獨立完成,博客園首發(fā),轉載請注明出處。

許可

JavaScript 秘密花園在 MIT license 許可協(xié)議下發(fā)布,并存放在 GitHub 開源社區(qū)。 如果你發(fā)現(xiàn)錯誤或者打字錯誤,請新建一個任務單或者發(fā)一個抓取請求。 你也可以在 Stack Overflow 的 JavaScript 聊天室找到我們。

對象

對象使用和屬性

JavaScript 中所有變量都可以當作對象使用,除了兩個例外 nullundefined。

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

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

一個常見的誤解是數(shù)字的字面值(literal)不能當作對象使用。這是因為 JavaScript 解析器的一個錯誤, 它試圖將點操作符解析為浮點數(shù)字面值的一部分。

2.toString(); // 出錯:SyntaxError

有很多變通方法可以讓數(shù)字的字面值看起來像對象。

2..toString(); // 第二個點號可以正常解析
2 .toString(); // 注意點號前面的空格
(2).toString(); // 2先被計算

對象作為數(shù)據(jù)類型

JavaScript 的對象可以作為哈希表使用,主要用來保存命名的鍵與值的對應關系。

使用對象的字面語法 - {} - 可以創(chuàng)建一個簡單對象。這個新創(chuàng)建的對象從 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

兩種語法是等價的,但是中括號操作符在下面兩種情況下依然有效

  • 動態(tài)設置屬性
  • 屬性名不是一個有效的變量名(譯者注比如屬性名中包含空格,或者屬性名是 JS 的關鍵詞)

刪除屬性

刪除屬性的唯一方法是使用 delete 操作符;設置屬性為 undefined 或者 null 并不能真正的刪除屬性, 而僅僅是移除了屬性和值的關聯(lián)。

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 undefinedfoo 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 解析器的另一個錯誤設計, 上面的第二種聲明方式在 ECMAScript 5 之前會拋出 SyntaxError 的錯誤。

這個錯誤的原因是 delete 是 JavaScript 語言的一個關鍵詞;因此為了在更低版本的 JavaScript 引擎下也能正常運行, 必須使用字符串字面值聲明方式。

原型

JavaScript 不包含傳統(tǒng)的類繼承模型,而是使用 prototype 原型模型。

雖然這經常被當作是 JavaScript 的缺點被提及,其實基于原型的繼承模型比傳統(tǒng)的類繼承還要強大。 實現(xiàn)傳統(tǒng)的類繼承模型是很簡單,但是實現(xiàn) JavaScript 中的原型繼承則要困難的多。 (It is for example fairly trivial to build a classic model on top of it, while the other way around is a far more difficult task.)

由于 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() // 創(chuàng)建Bar的一個新實例

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

上面的例子中,test 對象從 Bar.prototypeFoo.prototype 繼承下來;因此, 它能訪問 Foo 的原型方法 method。同時,它也能夠訪問那個定義在原型上的 Foo 實例屬性 value。 需要注意的是 new Bar() 不會創(chuàng)造出一個新的 Foo 實例,而是 重復使用它原型上的那個實例;因此,所有的 Bar 實例都會共享相同value 屬性。

屬性查找

當查找一個對象的屬性時,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性為止。

到查找到達原型鏈的頂部 - 也就是 Object.prototype - 但是仍然沒有找到指定的屬性,就會返回 undefined。

原型屬性

當原型屬性用來創(chuàng)建原型鏈時,可以把任何類型的值賦給它(prototype)。 然而將原子類型賦給 prototype 的操作將會被忽略。

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

而將對象賦值給 prototype,正如上面的例子所示,將會動態(tài)的創(chuàng)建原型鏈。

性能

如果一個屬性在原型鏈的上端,則對于查找時間將帶來不利影響。特別的,試圖獲取一個不存在的屬性將會遍歷整個原型鏈。

并且,當使用 for in 循環(huán)遍歷對象的屬性時,原型鏈上的所有屬性都將被訪問。

擴展內置類型的原型

一個錯誤特性被經常使用,那就是擴展 Object.prototype 或者其他內置類型的原型對象。

這種技術被稱之為 monkey patching 并且會破壞封裝。雖然它被廣泛的應用到一些 JavaScript 類庫中比如 Prototype, 但是我仍然不認為為內置類型添加一些非標準的函數(shù)是個好主意。

擴展內置類型的唯一理由是為了和新的 JavaScript 保持一致,比如 Array.forEach。

總結

在寫復雜的 JavaScript 應用之前,充分理解原型鏈繼承的工作方式是每個 JavaScript 程序員必修的功課。 要提防原型鏈過長帶來的性能問題,并知道如何通過縮短原型鏈來提高性能。 更進一步,絕對不要擴展內置類型的原型,除非是為了和新的 JavaScript 引擎兼容。

hasOwnProperty 函數(shù)

為了判斷一個對象是否包含自定義屬性而不是原型鏈上的屬性, 我們需要使用繼承自 Object.prototypehasOwnProperty 方法。

hasOwnProperty 是 JavaScript 中唯一一個處理屬性但是查找原型鏈的函數(shù)。

// 修改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 函數(shù)來獲取正確的結果。

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 循環(huán)

in 操作符一樣,for in 循環(huán)同樣在查找對象屬性時遍歷原型鏈上的所有屬性。

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

var foo = {moo: 2};
for(var i in foo) {
    console.log(i); // 輸出兩個屬性:bar 和 moo
}

由于不可能改變 for in 自身的行為,因此有必要過濾出那些不希望出現(xiàn)在循環(huán)體中的屬性, 這可以通過 Object.prototype 原型上的 hasOwnProperty 函數(shù)來完成。

使用 hasOwnProperty 過濾

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

這個版本的代碼是唯一正確的寫法。由于我們使用了 hasOwnProperty,所以這次輸出 moo。 如果不使用 hasOwnProperty,則這段代碼在原生對象原型(比如 Object.prototype)被擴展時可能會出錯。

一個廣泛使用的類庫 Prototype 就擴展了原生的 JavaScript 對象。 因此,當這個類庫被包含在頁面中時,不使用 hasOwnProperty 過濾的 for in 循環(huán)難免會出問題。

總結

推薦總是使用 hasOwnProperty。不要對代碼運行的環(huán)境做任何假設,不要假設原生對象是否已經被擴展了。

函數(shù)

函數(shù)聲明與表達式

函數(shù)是JavaScript中的一等對象,這意味著可以把函數(shù)像其它值一樣傳遞。 一個常見的用法是把匿名函數(shù)作為回調函數(shù)傳遞到異步函數(shù)中。

函數(shù)聲明

function foo() {}

上面的方法會在執(zhí)行前被 解析(hoisted),因此它存在于當前上下文的任意一個地方, 即使在函數(shù)定義體的上面被調用也是對的。

foo(); // 正常運行,因為foo在代碼運行前已經被創(chuàng)建
function foo() {}

函數(shù)賦值表達式

var foo = function() {};

這個例子把一個匿名的函數(shù)賦值給變量 foo。

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

由于 var 定義了一個聲明語句,對變量 foo 的解析是在代碼運行之前,因此 foo 變量在代碼運行時已經被定義過了。

但是由于賦值語句只在運行時執(zhí)行,因此在相應代碼執(zhí)行之前, foo 的值缺省為 undefined。

命名函數(shù)的賦值表達式

另外一個特殊的情況是將命名函數(shù)賦值給一個變量。

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

bar 函數(shù)聲明外是不可見的,這是因為我們已經把函數(shù)賦值給了 foo; 然而在 bar 內部依然可見。這是由于 JavaScript 的 命名處理 所致, 函數(shù)名在函數(shù)內總是可見的。

this 的工作原理

JavaScript 有一套完全不同于其它語言的對 this 的處理機制。 在種不同的情況下 ,this 指向的各不相同。

全局范圍內

this;

當在全部范圍內使用 this,它將會指向全局對象。

函數(shù)調用

foo();

這里 this 也會指向全局對象。

方法調用

test.foo(); 

這個例子中,this 指向 test 對象。

調用構造函數(shù)

new foo(); 

如果函數(shù)傾向于和 new 關鍵詞一塊使用,則我們稱這個函數(shù)是 構造函數(shù)。 在函數(shù)內部,this 指向新創(chuàng)建的對象。

顯式的設置 this

function foo(a, b, c) {}

var bar = {};
foo.apply(bar, [1, 2, 3]); // 數(shù)組將會被擴展,如下所示
foo.call(bar, 1, 2, 3); // 傳遞到foo的參數(shù)是:a = 1, b = 2, c = 3

當使用 Function.prototype 上的 call 或者 apply 方法時,函數(shù)內的 this 將會被 顯式設置為函數(shù)調用的第一個參數(shù)。

因此函數(shù)調用的規(guī)則在上例中已經不適用了,在foo 函數(shù)內 this 被設置成了 bar。

常見誤解

盡管大部分的情況都說的過去,不過第一個規(guī)則(譯者注這里指的應該是第二個規(guī)則,也就是直接調用函數(shù)時,this 指向全局對象) 被認為是JavaScript語言另一個錯誤設計的地方,因為它從來就沒有實際的用途。

Foo.method = function() {
    function test() {
        // this 將會被設置為全局對象(譯者注:瀏覽器環(huán)境中也就是 window 對象)
    }
    test();
}

一個常見的誤解是 test 中的 this 將會指向 Foo 對象,實際上不是這樣子的。

為了在 test 中獲取對 Foo 對象的引用,我們需要在 method 函數(shù)內部創(chuàng)建一個局部變量指向 Foo 對象。

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

that 只是我們隨意起的名字,不過這個名字被廣泛的用來指向外部的 this 對象。 在 閉包 一節(jié),我們可以看到 that 可以作為參數(shù)傳遞。

方法的賦值表達式

另一個看起來奇怪的地方是函數(shù)別名,也就是將一個方法賦值給一個變量。

var test = someObject.methodTest;
test();

上例中,test 就像一個普通的函數(shù)被調用;因此,函數(shù)內的 this 將不再被指向到 someObject 對象。

雖然 this 的晚綁定特性似乎并不友好,但這確實是基于原型繼承賴以生存的土壤。

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

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

new Bar().method();

method 被調用時,this 將會指向 Bar 的實例對象。

閉包和引用

閉包是 JavaScript 一個非常重要的特性,這意味著當前作用域總是能夠訪問外部作用域中的變量。 因為 函數(shù) 是 JavaScript 中唯一擁有自身作用域的結構,因此閉包的創(chuàng)建依賴于函數(shù)。

模擬私有變量

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

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

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

這里,Counter 函數(shù)返回兩個閉包,函數(shù) increment 和函數(shù) get。 這兩個函數(shù)都維持著 對外部作用域 Counter 的引用,因此總可以訪問此作用域內定義的變量 count.

為什么不可以在外部訪問私有變量

因為 JavaScript 中不可以對作用域進行引用或賦值,因此沒有辦法在外部訪問 count 變量。 唯一的途徑就是通過那兩個閉包。

var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
};

上面的代碼不會改變定義在 Counter 作用域中的 count 變量的值,因為 foo.hack 沒有 定義在那個作用域內。它將會創(chuàng)建或者覆蓋全局變量 count。

循環(huán)中的閉包

一個常見的錯誤出現(xiàn)在循環(huán)中使用閉包,假設我們需要在每次循環(huán)中調用循環(huán)序號

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

上面的代碼不會輸出數(shù)字 09,而是會輸出數(shù)字 10 十次。

console.log 被調用的時候,匿名函數(shù)保持對外部變量 i 的引用,此時 for循環(huán)已經結束, i 的值被修改成了 10.

為了得到想要的結果,需要在每次循環(huán)中創(chuàng)建變量 i拷貝

避免引用錯誤

為了正確的獲得循環(huán)序號,最好使用 匿名包裝器譯者注其實就是我們通常說的自執(zhí)行匿名函數(shù))。

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

外部的匿名函數(shù)會立即執(zhí)行,并把 i 作為它的參數(shù),此時函數(shù)內 e 變量就擁有了 i 的一個拷貝。

當傳遞給 setTimeout 的匿名函數(shù)執(zhí)行時,它就擁有了對 e 的引用,而這個值是不會被循環(huán)改變的。

有另一個方法完成同樣的工作,那就是從匿名包裝器中返回一個函數(shù)。這和上面的代碼效果一樣。

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

arguments 對象

JavaScript 中每個函數(shù)內都能訪問一個特別變量 arguments。這個變量維護著所有傳遞到這個函數(shù)中的參數(shù)列表。

arguments 變量不是一個數(shù)組(Array)。 盡管在語法上它有數(shù)組相關的屬性 length,但它不從 Array.prototype 繼承,實際上它是一個對象(Object)。

因此,無法對 arguments 變量使用標準的數(shù)組方法,比如 push, pop 或者 slice。 雖然使用 for 循環(huán)遍歷也是可以的,但是為了更好的使用數(shù)組方法,最好把它轉化為一個真正的數(shù)組。

轉化為數(shù)組

下面的代碼將會創(chuàng)建一個新的數(shù)組,包含所有 arguments 對象中的元素。

Array.prototype.slice.call(arguments);

這個轉化比較,在性能不好的代碼中不推薦這種做法。

傳遞參數(shù)

下面是將參數(shù)從一個函數(shù)傳遞到另一個函數(shù)的推薦做法。

function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // 干活
}

另一個技巧是同時使用 callapply,創(chuàng)建一個快速的解綁定包裝器。

function Foo() {}

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

// 創(chuàng)建一個解綁定的 "method"
// 輸入?yún)?shù)為: this, arg1, arg2...argN
Foo.method = function() {

    // 結果: Foo.prototype.method.call(this, arg1, arg2... argN)
    Function.call.apply(Foo.prototype.method, arguments);
};

譯者注:上面的 Foo.method 函數(shù)和下面代碼的效果是一樣的:

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

自動更新

arguments 對象為其內部屬性以及函數(shù)形式參數(shù)創(chuàng)建 gettersetter 方法。

因此,改變形參的值會影響到 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 對象總會被創(chuàng)建,除了兩個特殊情況 - 作為局部變量聲明和作為形式參數(shù)。

argumentsgetterssetters 方法總會被創(chuàng)建;因此使用 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);

然而,的確有一種情況會顯著的影響現(xiàn)代 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 不再是一個單純的內聯(lián)函數(shù) inlining譯者注:這里指的是解析器可以做內聯(lián)處理), 因為它需要知道它自己和它的調用者。 這不僅抵消了內聯(lián)函數(shù)帶來的性能提升,而且破壞了封裝,因此現(xiàn)在函數(shù)可能要依賴于特定的上下文。

因此強烈建議大家不要使用 arguments.callee 和它的屬性。

構造函數(shù)

JavaScript 中的構造函數(shù)和其它語言中的構造函數(shù)是不同的。 通過 new 關鍵字方式調用的函數(shù)都被認為是構造函數(shù)。

在構造函數(shù)內部 - 也就是被調用的函數(shù)內 - this 指向新創(chuàng)建的對象 Object。 這個新創(chuàng)建的對象的 prototype 被指向到構造函數(shù)的 prototype。

如果被調用的函數(shù)沒有顯式的 return 表達式,則隱式的會返回 this 對象 - 也就是新創(chuàng)建的對象。

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

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

var test = new Foo();

上面代碼把 Foo 作為構造函數(shù)調用,并設置新創(chuàng)建對象的 prototypeFoo.prototype

顯式的 return 表達式將會影響返回結果,但僅限于返回的是一個對象。

function Bar() {
    return 2;
}
new Bar(); // 返回新創(chuàng)建的對象

function Test() {
    this.value = 2;

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

譯者注new Bar() 返回的是新創(chuàng)建的對象,而不是數(shù)字的字面值 2。 因此 new Bar().constructor === Bar,但是如果返回的是數(shù)字對象,結果就不同了,如下所示

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

譯者注這里得到的 new Test()是函數(shù)返回的對象,而不是通過new關鍵字新創(chuàng)建的對象,因此:

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

如果 new 被遺漏了,則函數(shù)不會返回新創(chuàng)建的對象。

function Foo() {
    this.bla = 1; // 獲取設置全局參數(shù)
}
Foo(); // undefined

雖然上例在有些情況下也能正常運行,但是由于 JavaScript 中 this 的工作原理, 這里的 this 指向全局對象

工廠模式

為了不使用 new 關鍵字,構造函數(shù)必須顯式的返回一個值。

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

new Bar();
Bar();

上面兩種對 Bar 函數(shù)的調用返回的值完全相同,一個新創(chuàng)建的擁有 method 屬性的對象被返回, 其實這里創(chuàng)建了一個閉包。

還需要注意, new Bar()不會改變返回對象的原型(譯者注也就是返回對象的原型不會指向 Bar.prototype)。 因為構造函數(shù)的原型會被指向到剛剛創(chuàng)建的新對象,而這里的 Bar 沒有把這個新對象返回(譯者注:而是返回了一個包含 method 屬性的自定義對象)。

在上面的例子中,使用或者不使用 new 關鍵字沒有功能性的區(qū)別。

譯者注上面兩種方式創(chuàng)建的對象不能訪問 Bar 原型鏈上的屬性,如下所示:

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

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

通過工廠模式創(chuàng)建新對象

我們常聽到的一條忠告是不要使用 new 關鍵字來調用函數(shù),因為如果忘記使用它就會導致錯誤。

為了創(chuàng)建新對象,我們可以創(chuàng)建一個工廠方法,并且在方法內構造一個新對象。

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. 會占用更多的內存,因為新創(chuàng)建的對象不能共享原型上的方法。
  2. 為了實現(xiàn)繼承,工廠方法需要從另外一個對象拷貝所有屬性,或者把一個對象作為新創(chuàng)建對象的原型。
  3. 放棄原型鏈僅僅是因為防止遺漏 new 帶來的問題,這似乎和語言本身的思想相違背。

總結

雖然遺漏 new 關鍵字可能會導致問題,但這并不是放棄使用原型鏈的借口。 最終使用哪種方式取決于應用程序的需求,選擇一種代碼書寫風格并堅持下去才是最重要的。

作用域與命名空間

盡管 JavaScript 支持一對花括號創(chuàng)建的代碼段,但是并不支持塊級作用域; 而僅僅支持 函數(shù)作用域。

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

在函數(shù) test 內不使用 var 關鍵字聲明 foo 變量將會覆蓋外部的同名變量。 起初這看起來并不是大問題,但是當有成千上萬行代碼時,不使用 var 聲明變量將會帶來難以跟蹤的 BUG。

// 全局作用域
var items = [/* 數(shù)組 */];
for(var i = 0; i < 10; i++) {
    subLoop();
}

function subLoop() {
    // subLoop 函數(shù)作用域
    for(i = 0; i < 10; i++) { // 沒有使用 var 聲明變量
        // 干活
    }
}

外部循環(huán)在第一次調用 subLoop 之后就會終止,因為 subLoop 覆蓋了全局變量 i。 在第二個 for 循環(huán)中使用 var 聲明變量可以避免這種錯誤。 聲明變量時絕對不要遺漏 var 關鍵字,除非這就是期望的影響外部作用域的行為。

局部變量

JavaScript 中局部變量只可能通過兩種方式聲明,一個是作為函數(shù)參數(shù),另一個是通過 var 關鍵字聲明。

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

function test(i) {
    // 函數(shù) test 內的局部作用域
    i = 5;

    var foo = 3;
    bar = 4;
}
test(10);

fooi 是函數(shù) test 內的局部變量,而對 bar 的賦值將會覆蓋全局作用域內的同名變量。

變量聲明提升(Hoisting)

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'

// 函數(shù)聲明也會提升
function test(data) {
    var goo, i, e; // 沒有塊級作用域,這些變量被移動到函數(shù)頂部
    if (false) {
        goo = 1;

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

bar(); // 出錯:TypeError,因為 bar 依然是 'undefined'
someValue = 42; // 賦值語句不會被提升規(guī)則(hoisting)影響
bar = function() {};

test();

沒有塊級作用域不僅導致 var 表達式被從循環(huán)內移到外部,而且使一些 if 表達式更難看懂。

在原來代碼中,if 表達式看起來修改了全局變量 goo,實際上在提升規(guī)則被應用后,卻是在修改局部變量。

如果沒有提升規(guī)則(hoisting)的知識,下面的代碼看起來會拋出異常 ReferenceError。

// 檢查 SomeImportantThing 是否已經被初始化
if (!SomeImportantThing) {
    var SomeImportantThing = {};
}

實際上,上面的代碼正常運行,因為 var 表達式會被提升到全局作用域的頂部。

var SomeImportantThing;

// 其它一些代碼,可能會初始化 SomeImportantThing,也可能不會

// 檢查是否已經被初始化
if (!SomeImportantThing) {
    SomeImportantThing = {};
}

譯者注在 Nettuts+ 網(wǎng)站有一篇介紹 hoisting 的文章,其中的代碼很有啟發(fā)性。

// 譯者注:來自 Nettuts+ 的一段代碼,生動的闡述了 JavaScript 中變量聲明提升規(guī)則
var myvar = 'my value';  

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

名稱解析順序

JavaScript 中的所有作用域,包括全局作用域,都有一個特別的名稱 this 指向當前對象。

函數(shù)作用域內也有默認的變量 arguments,其中包含了傳遞到函數(shù)中的參數(shù)。

比如,當訪問函數(shù)內的 foo 變量時,JavaScript 會按照下面順序查找:

  1. 當前作用域內是否有 var foo 的定義。
  2. 函數(shù)形式參數(shù)是否有使用 foo 名稱的。
  3. 函數(shù)自身是否叫做 foo。
  4. 回溯到上一級作用域,然后從 #1 重新開始。

命名空間

只有一個全局作用域導致的常見錯誤是命名沖突。在 JavaScript中,這可以通過 匿名包裝器 輕松解決。

(function() {
    // 函數(shù)創(chuàng)建一個命名空間

    window.foo = function() {
        // 對外公開的函數(shù),創(chuàng)建了閉包
    };

})(); // 立即執(zhí)行此匿名函數(shù)

匿名函數(shù)被認為是 表達式;因此為了可調用性,它們首先會被執(zhí)行。

( // 小括號內的函數(shù)首先被執(zhí)行
function() {}
) // 并且返回函數(shù)對象
() // 調用上面的執(zhí)行結果,也就是函數(shù)對象

有一些其他的調用函數(shù)表達式的方法,比如下面的兩種方式語法不同,但是效果一模一樣。

// 另外兩種方式
+function(){}();
(function(){}());

結論

推薦使用匿名包裝器譯者注也就是自執(zhí)行的匿名函數(shù))來創(chuàng)建命名空間。這樣不僅可以防止命名沖突, 而且有利于程序的模塊化。

另外,使用全局變量被認為是不好的習慣。這樣的代碼容易產生錯誤并且維護成本較高。

數(shù)組

數(shù)組遍歷與屬性

雖然在 JavaScript 中數(shù)組是對象,但是沒有好的理由去使用 for in 循環(huán) 遍歷數(shù)組。 相反,有一些好的理由不去使用 for in 遍歷數(shù)組。

由于 for in 循環(huán)會枚舉原型鏈上的所有屬性,唯一過濾這些屬性的方式是使用 hasOwnProperty 函數(shù), 因此會比普通的 for 循環(huán)慢上好多倍。

遍歷

為了達到遍歷數(shù)組的最佳性能,推薦使用經典的 for 循環(huán)。

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 來緩存數(shù)組的長度。

雖然 length 是數(shù)組的一個屬性,但是在每次循環(huán)中訪問它還是有性能開銷。 可能最新的 JavaScript 引擎在這點上做了優(yōu)化,但是我們沒法保證自己的代碼是否運行在這些最近的引擎之上。

實際上,不使用緩存數(shù)組長度的方式比緩存版本要慢很多。

length 屬性

length 屬性的 getter 方式會簡單的返回數(shù)組的長度,而 setter 方式會截斷數(shù)組。

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 的結果,你會發(fā)現(xiàn)是這樣的: [1, 2, 3] 因為在 JavaScript 中 undefined 是一個變量,注意是變量不是關鍵字,因此上面兩個結果的意義是完全不相同的。

// 譯者注:為了驗證,我們來執(zhí)行下面代碼,看序號 5 是否存在于 foo 中。
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 false
foo[5] = undefined;
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 true

length 設置一個更小的值會截斷數(shù)組,但是增大 length 屬性值不會對數(shù)組產生影響。

結論

為了更好的性能,推薦使用普通的 for 循環(huán)并緩存數(shù)組的 length 屬性。 使用 for in 遍歷數(shù)組被認為是不好的代碼習慣并傾向于產生錯誤和導致性能問題。

Array 構造函數(shù)

由于 Array 的構造函數(shù)在如何處理參數(shù)時有點模棱兩可,因此總是推薦使用數(shù)組的字面語法 - [] - 來創(chuàng)建數(shù)組。

[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) // 結果: [],此數(shù)組長度為 3

由于只有一個參數(shù)傳遞到構造函數(shù)中(譯者注:指的是 new Array(3); 這種調用方式),并且這個參數(shù)是數(shù)字,構造函數(shù)會返回一個 length 屬性被設置為此參數(shù)的空數(shù)組。 需要特別注意的是,此時只有 length 屬性被設置,真正的數(shù)組并沒有生成。

var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 數(shù)組還沒有生成

這種優(yōu)先于設置數(shù)組長度屬性的做法只在少數(shù)幾種情況下有用,比如需要循環(huán)字符串,可以避免 for 循環(huán)的麻煩。

new Array(count + 1).join(stringToRepeat);

結論

應該盡量避免使用數(shù)組構造函數(shù)創(chuàng)建新數(shù)組。推薦使用數(shù)組的字面語法。它們更加短小和簡潔,因此增加了代碼的可讀性。

類型

相等與比較

JavaScript 有兩種方式判斷兩個值是否相等。

等于操作符

等于操作符由兩個等號組成:==

JavaScript 是弱類型語言,這就意味著,等于操作符會為了比較兩個值而進行強制類型轉換

""           ==   "0"           // false
0            ==   ""            // true
0            ==   "0"           // true
false        ==   "false"       // false
false        ==   "0"           // true
false        ==   undefined     // false
false        ==   null          // false
null         ==   undefined     // true
" \t\r\n"    ==   0             // true

上面的表格展示了強制類型轉換,這也是使用 == 被廣泛認為是不好編程習慣的主要原因, 由于它的復雜轉換規(guī)則,會導致難以跟蹤的問題。

此外,強制類型轉換也會帶來性能消耗,比如一個字符串為了和一個數(shù)字進行比較,必須事先被強制轉換為數(shù)字。

嚴格等于操作符

嚴格等于操作符由個等號組成:===

不像普通的等于操作符,嚴格等于操作符不會進行強制類型轉換。

""           ===   "0"           // false
0            ===   ""            // false
0            ===   "0"           // false
false        ===   "false"       // false
false        ===   "0"           // false
false        ===   undefined     // false
false        ===   null          // false
null         ===   undefined     // false
" \t\r\n"    ===   0             // false

上面的結果更加清晰并有利于代碼的分析。如果兩個操作數(shù)類型不同就肯定不相等也有助于性能的提升。

比較對象

雖然 ===== 操作符都是等于操作符,但是當其中有一個操作數(shù)為對象時,行為就不同了。

{} === {};                   // false
new String('foo') === 'foo'; // false
new Number(10) === 10;       // false
var foo = {};
foo === foo;                 // true

這里等于操作符比較的不是值是否相等,而是是否屬于同一個身份;也就是說,只有對象的同一個實例才被認為是相等的。 這有點像 Python 中的 is 和 C 中的指針比較。

結論

強烈推薦使用嚴格等于操作符。如果類型需要轉換,應該在比較之前顯式的轉換, 而不是使用語言本身復雜的強制轉換規(guī)則。

typeof 操作符

typeof 操作符(和 instanceof 一起)或許是 JavaScript 中最大的設計缺陷, 因為幾乎不可能從它們那里得到想要的結果。

盡管 instanceof 還有一些極少數(shù)的應用場景,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 操作符的運算結果??梢钥吹剑@個值在大多數(shù)情況下都返回 "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 中看出區(qū)別,如下所示:

// 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]"

測試為定義變量

typeof foo !== 'undefined'

上面代碼會檢測 foo 是否已經定義;如果沒有定義而直接使用會導致 ReferenceError 的異常。 這是 typeof 唯一有用的地方。

結論

為了檢測一個對象的類型,強烈推薦使用 Object.prototype.toString 方法; 因為這是唯一一個可依賴的方式。正如上面表格所示,typeof 的一些返回值在標準文檔中并未定義, 因此不同的引擎實現(xiàn)可能不同。

除非為了檢測一個變量是否已經定義,我們應盡量避免使用 typeof 操作符。

instanceof 操作符

instanceof 操作符用來比較兩個操作數(shù)的構造函數(shù)。只有在比較自定義的對象時才有意義。 如果用來比較內置類型,將會和 typeof 操作符 一樣用處不大。

比較自定義對象

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

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

// 如果僅僅設置 Bar.prototype 為函數(shù) Foo 本身,而不是 Foo 構造函數(shù)的一個實例
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 上下文的對象(比如,瀏覽器中不同的文檔結構)時將會出錯, 因為它們的構造函數(shù)不會是同一個對象。

結論

instanceof 操作符應該僅僅用來比較來自同一個 JavaScript 上下文的自定義對象。 正如 typeof 操作符一樣,任何其它的用法都應該是避免的。

類型轉換

JavaScript 是弱類型語言,所以會在任何可能的情況下應用強制類型轉換

// 下面的比較結果是:true
new Number(10) == 10; // Number.toString() 返回的字符串被再次轉換為數(shù)字

10 == '10';           // 字符串被轉換為數(shù)字
10 == '+10 ';         // 同上
10 == '010';          // 同上 
isNaN(null) == false; // null 被轉換為數(shù)字 0
                      // 0 當然不是一個 NaN(譯者注:否定之否定)

// 下面的比較結果是:false
10 == 010;
10 == '-10';

為了避免上面復雜的強制類型轉換,強烈推薦使用嚴格的等于操作符。 雖然這可以避免大部分的問題,但 JavaScript 的弱類型系統(tǒng)仍然會導致一些其它問題。

內置類型的構造函數(shù)

內置類型(比如 NumberString)的構造函數(shù)在被調用時,使用或者不使用 new 的結果完全不同。

new Number(10) === 10;     // False, 對象與數(shù)字的比較
Number(10) === 10;         // True, 數(shù)字與數(shù)字的比較
new Number(10) + 0 === 10; // True, 由于隱式的類型轉換

使用內置類型 Number 作為構造函數(shù)將會創(chuàng)建一個新的 Number 對象, 而在不使用 new 關鍵字的 Number 函數(shù)更像是一個數(shù)字轉換器。

另外,在比較中引入對象的字面值將會導致更加復雜的強制類型轉換。

最好的選擇是把要比較的值顯式的轉換為三種可能的類型之一。

轉換為字符串

'' + 10 === '10'; // true

將一個值加上空字符串可以輕松轉換為字符串類型。

轉換為數(shù)字

+'10' === 10; // true

使用一元的加號操作符,可以把字符串轉換為數(shù)字。

譯者注字符串轉換為數(shù)字的常用方法:

+'010' === 10
Number('010') === 10
parseInt('010', 10) === 10  // 用來轉換為整數(shù)

+'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 函數(shù)會在當前作用域中執(zhí)行一段 JavaScript 代碼字符串。

var foo = 1;
function test() {
    var foo = 2;
    eval('foo = 3');
    return foo;
}
test(); // 3
foo; // 1

但是 eval 只在被直接調用并且調用函數(shù)就是 eval 本身時,才在當前作用域中執(zhí)行。

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 函數(shù)修改 eval 執(zhí)行的上下文為全局作用域
var foo = 1;
function test() {
    var foo = 2;
    eval.call(window, 'foo = 3');
    return foo;
}
test(); // 2
foo; // 3

任何情況下我們都應該避免使用 eval 函數(shù)。99.9% 使用 eval 的場景都有不使用 eval 的解決方案。

偽裝的 eval

定時函數(shù) setTimeoutsetInterval 都可以接受字符串作為它們的第一個參數(shù)。 這個字符串總是在全局作用域中執(zhí)行,因此 eval 在這種情況下沒有被直接調用。

安全問題

eval 也存在安全問題,因為它會執(zhí)行任意傳給它的代碼, 在代碼字符串未知或者是來自一個不信任的源時,絕對不要使用 eval 函數(shù)。

結論

絕對不要使用 eval,任何使用它的代碼都會在它的工作方式,性能和安全性方面受到質疑。 如果一些情況必須使用到 eval 才能正常工作,首先它的設計會受到質疑,這不應該是首選的解決方案, 一個更好的不使用 eval 的解決方案應該得到充分考慮并優(yōu)先采用。

undefinednull

JavaScript 有兩個表示‘空’的值,其中比較有用的是 undefined。

undefined 的值

undefined 是一個值為 undefined 的類型。

這個語言也定義了一個全局變量,它的值是 undefined,這個變量也被稱為 undefined。 但是這個變量不是一個常量,也不是一個關鍵字。這意味著它的可以輕易被覆蓋。

下面的情況會返回 undefined 值:

  • 訪問未修改的全局變量 undefined。
  • 由于沒有定義 return 表達式的函數(shù)隱式返回。
  • return 表達式沒有顯式的返回任何內容。
  • 訪問不存在的屬性。
  • 函數(shù)參數(shù)沒有被顯式的傳遞值。
  • 任何被設置為 undefined 值的變量。

處理 undefined 值的改變

由于全局變量 undefined 只是保存了 undefined 類型實際的副本, 因此對它賦新值不會改變類型 undefined 的值。

然而,為了方便其它變量和 undefined 做比較,我們需要事先獲取類型 undefined 的值。

為了避免可能對 undefined 值的改變,一個常用的技巧是使用一個傳遞到匿名包裝器的額外參數(shù)。 在調用時,這個參數(shù)不會獲取任何值。

var undefined = 123;
(function(something, foo, undefined) {
    // 局部作用域里的 undefined 變量重新獲得了 `undefined` 值

})('Hello World', 42);

另外一種達到相同目的方法是在函數(shù)內使用變量聲明。

var undefined = 123;
(function(something, foo) {
    var undefined;
    ...

})('Hello World', 42);

這里唯一的區(qū)別是,在壓縮后并且函數(shù)內沒有其它需要使用 var 聲明變量的情況下,這個版本的代碼會多出 4 個字節(jié)的代碼。

null 的用處

JavaScript 中的 undefined 的使用場景類似于其它語言中的 null,實際上 JavaScript 中的 null 是另外一種數(shù)據(jù)類型。

它在 JavaScript 內部有一些使用場景(比如聲明原型鏈的終結 Foo.prototype = null),但是大多數(shù)情況下都可以使用 undefined 來代替。

自動分號插入

盡管 JavaScript 有 C 的代碼風格,但是它強制要求在代碼中使用分號,實際上可以省略它們。

JavaScript 不是一個沒有分號的語言,恰恰相反上它需要分號來就解析源代碼。 因此 JavaScript 解析器在遇到由于缺少分號導致的解析錯誤時,會自動在源代碼中插入分號。

var foo = function() {
} // 解析錯誤,分號丟失
test()

自動插入分號,解析器重新解析。

var foo = function() {
}; // 沒有錯誤,解析繼續(xù)
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) {})

log 函數(shù)的執(zhí)行結果極大可能不是函數(shù);這種情況下就會出現(xiàn) TypeError 的錯誤,詳細錯誤信息可能是 undefined is not a function。

結論

建議絕對不要省略分號,同時也提倡將花括號和相應的表達式放在一行, 對于只有一行代碼的 if 或者 else 表達式,也不應該省略花括號。 這些良好的編程習慣不僅可以提到代碼的一致性,而且可以防止解析器改變代碼行為的錯誤處理。

其它

setTimeoutsetInterval

由于 JavaScript 是異步的,可以使用 setTimeoutsetInterval 來計劃執(zhí)行函數(shù)。

function foo() {}
var id = setTimeout(foo, 1000); // 返回一個大于零的數(shù)字

setTimeout 被調用時,它會返回一個 ID 標識并且計劃在將來大約 1000 毫秒后調用 foo 函數(shù)。 foo 函數(shù)只會被執(zhí)行一次。

基于 JavaScript 引擎的計時策略,以及本質上的單線程運行方式,所以其它代碼的運行可能會阻塞此線程。 因此沒法確保函數(shù)會在 setTimeout 指定的時刻被調用。

作為第一個參數(shù)的函數(shù)將會在全局作用域中執(zhí)行,因此函數(shù)內的 this 將會指向這個全局對象。

function Foo() {
    this.value = 42;
    this.method = function() {
        // this 指向全局對象
        console.log(this.value); // 輸出:undefined
    };
    setTimeout(this.method, 500);
}
new Foo();

setInterval 的堆調用

setTimeout 只會執(zhí)行回調函數(shù)一次,不過 setInterval - 正如名字建議的 - 會每隔 X 毫秒執(zhí)行函數(shù)一次。 但是卻不鼓勵使用這個函數(shù)。

當回調函數(shù)的執(zhí)行被阻塞時,setInterval 仍然會發(fā)布更多的回調指令。在很小的定時間隔情況下,這會導致回調函數(shù)被堆積起來。

function foo(){
    // 阻塞執(zhí)行 1 秒
}
setInterval(foo, 100);

上面代碼中,foo 會執(zhí)行一次隨后被阻塞了一秒鐘。

foo 被阻塞的時候,setInterval 仍然在組織將來對回調函數(shù)的調用。 因此,當?shù)谝淮?foo 函數(shù)調用結束時,已經有 10 次函數(shù)調用在等待執(zhí)行。

處理可能的阻塞調用

最簡單也是最容易控制的方案,是在回調函數(shù)內部使用 setTimeout 函數(shù)。

function foo(){
    // 阻塞執(zhí)行 1 秒
    setTimeout(foo, 100);
}
foo();

這樣不僅封裝了 setTimeout 回調函數(shù),而且阻止了調用指令的堆積,可以有更多的控制。 foo 函數(shù)現(xiàn)在可以控制是否繼續(xù)執(zhí)行還是終止執(zhí)行。

手工清空定時器

可以通過將定時時產生的 ID 標識傳遞給 clearTimeout 或者 clearInterval 函數(shù)來清除定時, 至于使用哪個函數(shù)取決于調用的時候使用的是 setTimeout 還是 setInterval。

var id = setTimeout(foo, 1000);
clearTimeout(id);

清除所有定時器

由于沒有內置的清除所有定時器的方法,可以采用一種暴力的方式來達到這一目的。

// 清空"所有"的定時器
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}

可能還有些定時器不會在上面代碼中被清除(譯者注如果定時器調用時返回的 ID 值大于 1000), 因此我們可以事先保存所有的定時器 ID,然后一把清除。

隱藏使用 eval

setTimeoutsetInterval 也接受第一個參數(shù)為字符串的情況。 這個特性絕對不要使用,因為它在內部使用了 eval。

function foo() {
    // 將會被調用
}

function bar() {
    function foo() {
        // 不會被調用
    }
    setTimeout('foo()', 1000);
}
bar();

由于 eval 在這種情況下不是被直接調用,因此傳遞到 setTimeout 的字符串會自全局作用域中執(zhí)行; 因此,上面的回調函數(shù)使用的不是定義在 bar 作用域中的局部變量 foo

建議不要在調用定時器函數(shù)時,為了向回調函數(shù)傳遞參數(shù)而使用字符串的形式。

function foo(a, b, c) {}

// 不要這樣做
setTimeout('foo(1,2, 3)', 1000)

// 可以使用匿名函數(shù)完成相同功能
setTimeout(function() {
    foo(1, 2, 3);
}, 1000)

結論

絕對不要使用字符串作為 setTimeout 或者 setInterval 的第一個參數(shù), 這么寫的代碼明顯質量很差。當需要向回調函數(shù)傳遞參數(shù)時,可以創(chuàng)建一個匿名函數(shù),在函數(shù)內執(zhí)行真實的回調函數(shù)。

另外,應該避免使用 setInterval,因為它的定時執(zhí)行不會被 JavaScript 阻塞。