亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

簡(jiǎn)介

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

JavaScript 秘密花園不是用來(lái)教你 JavaScript。為了更好的理解這篇文章的內(nèi)容, 你需要事先學(xué)習(xí) JavaScript 的基礎(chǔ)知識(shí)。在 Mozilla 開(kāi)發(fā)者網(wǎng)絡(luò)中有一系列非常棒的 JavaScript 學(xué)習(xí)向?qū)?/a>。

關(guān)于作者

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

貢獻(xiàn)者

中文翻譯

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

許可

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

對(duì)象

對(duì)象使用和屬性

JavaScript 中所有變量都可以當(dāng)作對(duì)象使用,除了兩個(gè)例外 nullundefined。

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

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

一個(gè)常見(jiàn)的誤解是數(shù)字的字面值(literal)不能當(dāng)作對(duì)象使用。這是因?yàn)?JavaScript 解析器的一個(gè)錯(cuò)誤, 它試圖將點(diǎn)操作符解析為浮點(diǎn)數(shù)字面值的一部分。

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

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

2..toString(); // 第二個(gè)點(diǎn)號(hào)可以正常解析
2 .toString(); // 注意點(diǎn)號(hào)前面的空格
(2).toString(); // 2先被計(jì)算

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

JavaScript 的對(duì)象可以作為哈希表使用,主要用來(lái)保存命名的鍵與值的對(duì)應(yīng)關(guān)系。

使用對(duì)象的字面語(yǔ)法 - {} - 可以創(chuàng)建一個(gè)簡(jiǎn)單對(duì)象。這個(gè)新創(chuàng)建的對(duì)象從 Object.prototype 繼承下面,沒(méi)有任何自定義屬性。

var foo = {}; // 一個(gè)空對(duì)象

// 一個(gè)新對(duì)象,擁有一個(gè)值為12的自定義屬性'test'
var bar = {test: 12}; 

訪問(wèn)屬性

有兩種方式來(lái)訪問(wèn)對(duì)象的屬性,點(diǎn)操作符或者中括號(hào)操作符。

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

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

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

兩種語(yǔ)法是等價(jià)的,但是中括號(hào)操作符在下面兩種情況下依然有效

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

刪除屬性

刪除屬性的唯一方法是使用 delete 操作符;設(shè)置屬性為 undefined 或者 null 并不能真正的刪除屬性, 而僅僅是移除了屬性和值的關(guān)聯(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]);
    }
}

上面的輸出結(jié)果有 bar undefinedfoo null - 只有 baz 被真正的刪除了,所以從輸出結(jié)果中消失。

屬性名的語(yǔ)法

var test = {
    'case': 'I am a keyword so I must be notated as a string',
    delete: 'I am a keyword too so me' // 出錯(cuò):SyntaxError
};

對(duì)象的屬性名可以使用字符串或者普通字符聲明。但是由于 JavaScript 解析器的另一個(gè)錯(cuò)誤設(shè)計(jì), 上面的第二種聲明方式在 ECMAScript 5 之前會(huì)拋出 SyntaxError 的錯(cuò)誤。

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

原型

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

雖然這經(jīng)常被當(dāng)作是 JavaScript 的缺點(diǎn)被提及,其實(shí)基于原型的繼承模型比傳統(tǒng)的類繼承還要強(qiáng)大。 實(shí)現(xiàn)傳統(tǒng)的類繼承模型是很簡(jiǎn)單,但是實(shí)現(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 是唯一一個(gè)被廣泛使用的基于原型繼承的語(yǔ)言,所以理解兩種繼承模式的差異是需要一定時(shí)間的。

第一個(gè)不同之處在于 JavaScript 使用原型鏈的繼承方式。

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

function Bar() {}

// 設(shè)置Bar的prototype屬性為Foo的實(shí)例對(duì)象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

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

var test = new Bar() // 創(chuàng)建Bar的一個(gè)新實(shí)例

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

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

屬性查找

當(dāng)查找一個(gè)對(duì)象的屬性時(shí),JavaScript 會(huì)向上遍歷原型鏈,直到找到給定名稱的屬性為止。

到查找到達(dá)原型鏈的頂部 - 也就是 Object.prototype - 但是仍然沒(méi)有找到指定的屬性,就會(huì)返回 undefined。

原型屬性

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

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

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

性能

如果一個(gè)屬性在原型鏈的上端,則對(duì)于查找時(shí)間將帶來(lái)不利影響。特別的,試圖獲取一個(gè)不存在的屬性將會(huì)遍歷整個(gè)原型鏈。

并且,當(dāng)使用 for in 循環(huán)遍歷對(duì)象的屬性時(shí),原型鏈上的所有屬性都將被訪問(wèn)。

擴(kuò)展內(nèi)置類型的原型

一個(gè)錯(cuò)誤特性被經(jīng)常使用,那就是擴(kuò)展 Object.prototype 或者其他內(nèi)置類型的原型對(duì)象。

這種技術(shù)被稱之為 monkey patching 并且會(huì)破壞封裝。雖然它被廣泛的應(yīng)用到一些 JavaScript 類庫(kù)中比如 Prototype, 但是我仍然不認(rèn)為為內(nèi)置類型添加一些非標(biāo)準(zhǔn)的函數(shù)是個(gè)好主意。

擴(kuò)展內(nèi)置類型的唯一理由是為了和新的 JavaScript 保持一致,比如 Array.forEach。

總結(jié)

在寫(xiě)復(fù)雜的 JavaScript 應(yīng)用之前,充分理解原型鏈繼承的工作方式是每個(gè) JavaScript 程序員必修的功課。 要提防原型鏈過(guò)長(zhǎng)帶來(lái)的性能問(wèn)題,并知道如何通過(guò)縮短原型鏈來(lái)提高性能。 更進(jìn)一步,絕對(duì)不要擴(kuò)展內(nèi)置類型的原型,除非是為了和新的 JavaScript 引擎兼容。

hasOwnProperty 函數(shù)

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

hasOwnProperty 是 JavaScript 中唯一一個(gè)處理屬性但是查找原型鏈的函數(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 可以給出正確和期望的結(jié)果,這在遍歷對(duì)象的屬性時(shí)會(huì)很有用。 沒(méi)有其它方法可以用來(lái)排除原型鏈上的屬性,而不是定義在對(duì)象自身上的屬性。

hasOwnProperty 作為屬性

JavaScript 不會(huì)保護(hù) hasOwnProperty 被非法占用,因此如果一個(gè)對(duì)象碰巧存在這個(gè)屬性, 就需要使用外部hasOwnProperty 函數(shù)來(lái)獲取正確的結(jié)果。

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

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

// 使用其它對(duì)象的 hasOwnProperty,并將其上下文設(shè)置為foo
({}).hasOwnProperty.call(foo, 'bar'); // true

結(jié)論

當(dāng)檢查對(duì)象上某個(gè)屬性是否存在時(shí),hasOwnProperty唯一可用的方法。 同時(shí)在使用 for in loop 遍歷對(duì)象時(shí),推薦總是使用 hasOwnProperty 方法, 這將會(huì)避免原型對(duì)象擴(kuò)展帶來(lái)的干擾。

for in 循環(huán)

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

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

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

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

使用 hasOwnProperty 過(guò)濾

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

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

一個(gè)廣泛使用的類庫(kù) Prototype 就擴(kuò)展了原生的 JavaScript 對(duì)象。 因此,當(dāng)這個(gè)類庫(kù)被包含在頁(yè)面中時(shí),不使用 hasOwnProperty 過(guò)濾的 for in 循環(huán)難免會(huì)出問(wèn)題。

總結(jié)

推薦總是使用 hasOwnProperty。不要對(duì)代碼運(yùn)行的環(huán)境做任何假設(shè),不要假設(shè)原生對(duì)象是否已經(jīng)被擴(kuò)展了。

函數(shù)

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

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

函數(shù)聲明

function foo() {}

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

foo(); // 正常運(yùn)行,因?yàn)閒oo在代碼運(yùn)行前已經(jīng)被創(chuàng)建
function foo() {}

函數(shù)賦值表達(dá)式

var foo = function() {};

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

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

由于 var 定義了一個(gè)聲明語(yǔ)句,對(duì)變量 foo 的解析是在代碼運(yùn)行之前,因此 foo 變量在代碼運(yùn)行時(shí)已經(jīng)被定義過(guò)了。

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

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

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

var foo = function bar() {
    bar(); // 正常運(yùn)行
}
bar(); // 出錯(cuò):ReferenceError

bar 函數(shù)聲明外是不可見(jiàn)的,這是因?yàn)槲覀円呀?jīng)把函數(shù)賦值給了 foo; 然而在 bar 內(nèi)部依然可見(jiàn)。這是由于 JavaScript 的 命名處理 所致, 函數(shù)名在函數(shù)內(nèi)總是可見(jiàn)的。

this 的工作原理

JavaScript 有一套完全不同于其它語(yǔ)言的對(duì) this 的處理機(jī)制。 在種不同的情況下 ,this 指向的各不相同。

全局范圍內(nèi)

this;

當(dāng)在全部范圍內(nèi)使用 this,它將會(huì)指向全局對(duì)象。

函數(shù)調(diào)用

foo();

這里 this 也會(huì)指向全局對(duì)象。

方法調(diào)用

test.foo(); 

這個(gè)例子中,this 指向 test 對(duì)象。

調(diào)用構(gòu)造函數(shù)

new foo(); 

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

顯式的設(shè)置 this

function foo(a, b, c) {}

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

當(dāng)使用 Function.prototype 上的 call 或者 apply 方法時(shí),函數(shù)內(nèi)的 this 將會(huì)被 顯式設(shè)置為函數(shù)調(diào)用的第一個(gè)參數(shù)。

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

常見(jiàn)誤解

盡管大部分的情況都說(shuō)的過(guò)去,不過(guò)第一個(gè)規(guī)則(譯者注這里指的應(yīng)該是第二個(gè)規(guī)則,也就是直接調(diào)用函數(shù)時(shí),this 指向全局對(duì)象) 被認(rèn)為是JavaScript語(yǔ)言另一個(gè)錯(cuò)誤設(shè)計(jì)的地方,因?yàn)樗?strong>從來(lái)就沒(méi)有實(shí)際的用途。

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

一個(gè)常見(jiàn)的誤解是 test 中的 this 將會(huì)指向 Foo 對(duì)象,實(shí)際上不是這樣子的。

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

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

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

方法的賦值表達(dá)式

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

var test = someObject.methodTest;
test();

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

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

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

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

new Bar().method();

當(dāng) method 被調(diào)用時(shí),this 將會(huì)指向 Bar 的實(shí)例對(duì)象。

閉包和引用

閉包是 JavaScript 一個(gè)非常重要的特性,這意味著當(dāng)前作用域總是能夠訪問(wèn)外部作用域中的變量。 因?yàn)?函數(shù) 是 JavaScript 中唯一擁有自身作用域的結(jié)構(gòu),因此閉包的創(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ù)返回兩個(gè)閉包,函數(shù) increment 和函數(shù) get。 這兩個(gè)函數(shù)都維持著 對(duì)外部作用域 Counter 的引用,因此總可以訪問(wèn)此作用域內(nèi)定義的變量 count.

為什么不可以在外部訪問(wèn)私有變量

因?yàn)?JavaScript 中不可以對(duì)作用域進(jìn)行引用或賦值,因此沒(méi)有辦法在外部訪問(wèn) count 變量。 唯一的途徑就是通過(guò)那兩個(gè)閉包。

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

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

循環(huán)中的閉包

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

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

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

當(dāng) console.log 被調(diào)用的時(shí)候,匿名函數(shù)保持對(duì)外部變量 i 的引用,此時(shí) for循環(huán)已經(jīng)結(jié)束, i 的值被修改成了 10.

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

避免引用錯(cuò)誤

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

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

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

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

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

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

arguments 對(duì)象

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

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

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

轉(zhuǎn)化為數(shù)組

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

Array.prototype.slice.call(arguments);

這個(gè)轉(zhuǎn)化比較,在性能不好的代碼中不推薦這種做法。

傳遞參數(shù)

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

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

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

function Foo() {}

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

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

    // 結(jié)果: 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));
};

自動(dòng)更新

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

因此,改變形參的值會(huì)影響到 arguments 對(duì)象的值,反之亦然。

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 對(duì)象總會(huì)被創(chuàng)建,除了兩個(gè)特殊情況 - 作為局部變量聲明和作為形式參數(shù)。

argumentsgetterssetters 方法總會(huì)被創(chuàng)建;因此使用 arguments 對(duì)性能不會(huì)有什么影響。 除非是需要對(duì) arguments 對(duì)象的屬性進(jìn)行多次訪問(wèn)。

譯者注MDC 中對(duì) strict mode 模式下 arguments 的描述有助于我們的理解,請(qǐng)看下面代碼:

// 闡述在 ES5 的嚴(yán)格模式下 `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);

然而,的確有一種情況會(huì)顯著的影響現(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 不再是一個(gè)單純的內(nèi)聯(lián)函數(shù) inlining譯者注:這里指的是解析器可以做內(nèi)聯(lián)處理), 因?yàn)樗枰浪约汉退恼{(diào)用者。 這不僅抵消了內(nèi)聯(lián)函數(shù)帶來(lái)的性能提升,而且破壞了封裝,因此現(xiàn)在函數(shù)可能要依賴于特定的上下文。

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

構(gòu)造函數(shù)

JavaScript 中的構(gòu)造函數(shù)和其它語(yǔ)言中的構(gòu)造函數(shù)是不同的。 通過(guò) new 關(guān)鍵字方式調(diào)用的函數(shù)都被認(rèn)為是構(gòu)造函數(shù)。

在構(gòu)造函數(shù)內(nèi)部 - 也就是被調(diào)用的函數(shù)內(nèi) - this 指向新創(chuàng)建的對(duì)象 Object。 這個(gè)新創(chuàng)建的對(duì)象的 prototype 被指向到構(gòu)造函數(shù)的 prototype。

如果被調(diào)用的函數(shù)沒(méi)有顯式的 return 表達(dá)式,則隱式的會(huì)返回 this 對(duì)象 - 也就是新創(chuàng)建的對(duì)象。

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

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

var test = new Foo();

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

顯式的 return 表達(dá)式將會(huì)影響返回結(jié)果,但僅限于返回的是一個(gè)對(duì)象。

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

function Test() {
    this.value = 2;

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

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

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

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

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

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

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

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

工廠模式

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

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

new Bar();
Bar();

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

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

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

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

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

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

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

我們常聽(tīng)到的一條忠告是不要使用 new 關(guān)鍵字來(lái)調(diào)用函數(shù),因?yàn)槿绻浭褂盟蜁?huì)導(dǎo)致錯(cuò)誤。

為了創(chuàng)建新對(duì)象,我們可以創(chuàng)建一個(gè)工廠方法,并且在方法內(nèi)構(gòu)造一個(gè)新對(duì)象。

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 的調(diào)用方式不容易出錯(cuò),并且可以充分利用私有變量帶來(lái)的便利, 但是隨之而來(lái)的是一些不好的地方。

  1. 會(huì)占用更多的內(nèi)存,因?yàn)樾聞?chuàng)建的對(duì)象不能共享原型上的方法。
  2. 為了實(shí)現(xiàn)繼承,工廠方法需要從另外一個(gè)對(duì)象拷貝所有屬性,或者把一個(gè)對(duì)象作為新創(chuàng)建對(duì)象的原型。
  3. 放棄原型鏈僅僅是因?yàn)榉乐惯z漏 new 帶來(lái)的問(wèn)題,這似乎和語(yǔ)言本身的思想相違背。

總結(jié)

雖然遺漏 new 關(guān)鍵字可能會(huì)導(dǎo)致問(wèn)題,但這并不是放棄使用原型鏈的借口。 最終使用哪種方式取決于應(yīng)用程序的需求,選擇一種代碼書(shū)寫(xiě)風(fēng)格并堅(jiān)持下去才是最重要的。

作用域與命名空間

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

function test() { // 一個(gè)作用域
    for(var i = 0; i < 10; i++) { // 不是一個(gè)作用域
        // count
    }
    console.log(i); // 10
}

譯者注如果 return 對(duì)象的左括號(hào)和 return 不在一行上就會(huì)出錯(cuò)。

// 譯者注:下面輸出 undefined
function add(a, b) {
    return 
        a + b;
}
console.log(add(1, 2));

JavaScript 中沒(méi)有顯式的命名空間定義,這就意味著所有對(duì)象都定義在一個(gè)全局共享的命名空間下面。

每次引用一個(gè)變量,JavaScript 會(huì)向上遍歷整個(gè)作用域直到找到這個(gè)變量為止。 如果到達(dá)全局作用域但是這個(gè)變量仍未找到,則會(huì)拋出 ReferenceError 異常。

隱式的全局變量

// 腳本 A
foo = '42';

// 腳本 B
var foo = '42'

上面兩段腳本效果不同。腳本 A 在全局作用域內(nèi)定義了變量 foo,而腳本 B 在當(dāng)前作用域內(nèi)定義變量 foo

再次強(qiáng)調(diào),上面的效果完全不同,不使用 var 聲明變量將會(huì)導(dǎo)致隱式的全局變量產(chǎn)生。

// 全局作用域
var foo = 42;
function test() {
    // 局部作用域
    foo = 21;
}
test();
foo; // 21

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

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

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

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

局部變量

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

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

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

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

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

變量聲明提升(Hoisting)

JavaScript 會(huì)提升變量聲明。這意味著 var 表達(dá)式和 function 聲明都將會(huì)被提升到當(dāng)前作用域的頂部。

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];
    }
}

上面代碼在運(yùn)行之前將會(huì)被轉(zhuǎn)化。JavaScript 將會(huì)把 var 表達(dá)式和 function 聲明提升到當(dāng)前作用域的頂部。

// var 表達(dá)式被移動(dòng)到這里
var bar, someValue; // 缺省值是 'undefined'

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

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

bar(); // 出錯(cuò):TypeError,因?yàn)?bar 依然是 'undefined'
someValue = 42; // 賦值語(yǔ)句不會(huì)被提升規(guī)則(hoisting)影響
bar = function() {};

test();

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

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

如果沒(méi)有提升規(guī)則(hoisting)的知識(shí),下面的代碼看起來(lái)會(huì)拋出異常 ReferenceError

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

實(shí)際上,上面的代碼正常運(yùn)行,因?yàn)?var 表達(dá)式會(huì)被提升到全局作用域的頂部。

var SomeImportantThing;

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

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

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

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

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

名稱解析順序

JavaScript 中的所有作用域,包括全局作用域,都有一個(gè)特別的名稱 this 指向當(dāng)前對(duì)象。

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

比如,當(dāng)訪問(wèn)函數(shù)內(nèi)的 foo 變量時(shí),JavaScript 會(huì)按照下面順序查找:

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

命名空間

只有一個(gè)全局作用域?qū)е碌某R?jiàn)錯(cuò)誤是命名沖突。在 JavaScript中,這可以通過(guò) 匿名包裝器 輕松解決。

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

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

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

匿名函數(shù)被認(rèn)為是 表達(dá)式;因此為了可調(diào)用性,它們首先會(huì)被執(zhí)行。

( // 小括號(hào)內(nèi)的函數(shù)首先被執(zhí)行
function() {}
) // 并且返回函數(shù)對(duì)象
() // 調(diào)用上面的執(zhí)行結(jié)果,也就是函數(shù)對(duì)象

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

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

結(jié)論

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

另外,使用全局變量被認(rèn)為是不好的習(xí)慣。這樣的代碼容易產(chǎn)生錯(cuò)誤并且維護(hù)成本較高。

數(shù)組

數(shù)組遍歷與屬性

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

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

遍歷

為了達(dá)到遍歷數(shù)組的最佳性能,推薦使用經(jīng)典的 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]);
}

上面代碼有一個(gè)處理,就是通過(guò) l = list.length 來(lái)緩存數(shù)組的長(zhǎng)度。

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

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

length 屬性

length 屬性的 getter 方式會(huì)簡(jiǎn)單的返回?cái)?shù)組的長(zhǎng)度,而 setter 方式會(huì)截?cái)?/strong>數(shù)組。

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

foo.length = 6;
foo; // [1, 2, 3]

譯者注: 在 Firebug 中查看此時(shí) foo 的值是: [1, 2, 3, undefined, undefined, undefined] 但是這個(gè)結(jié)果并不準(zhǔn)確,如果你在 Chrome 的控制臺(tái)查看 foo 的結(jié)果,你會(huì)發(fā)現(xiàn)是這樣的: [1, 2, 3] 因?yàn)樵?JavaScript 中 undefined 是一個(gè)變量,注意是變量不是關(guān)鍵字,因此上面兩個(gè)結(jié)果的意義是完全不相同的。

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

length 設(shè)置一個(gè)更小的值會(huì)截?cái)鄶?shù)組,但是增大 length 屬性值不會(huì)對(duì)數(shù)組產(chǎn)生影響。

結(jié)論

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

Array 構(gòu)造函數(shù)

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

[1, 2, 3]; // 結(jié)果: [1, 2, 3]
new Array(1, 2, 3); // 結(jié)果: [1, 2, 3]

[3]; // 結(jié)果: [3]
new Array(3); // 結(jié)果: [] 
new Array('3') // 結(jié)果: ['3']

// 譯者注:因此下面的代碼將會(huì)使人很迷惑
new Array(3, 4, 5); // 結(jié)果: [3, 4, 5] 
new Array(3) // 結(jié)果: [],此數(shù)組長(zhǎng)度為 3

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

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

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

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

結(jié)論

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

類型

相等與比較

JavaScript 有兩種方式判斷兩個(gè)值是否相等。

等于操作符

等于操作符由兩個(gè)等號(hào)組成:==

JavaScript 是弱類型語(yǔ)言,這就意味著,等于操作符會(huì)為了比較兩個(gè)值而進(jìn)行強(qiáng)制類型轉(zhuǎn)換。

""           ==   "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

上面的表格展示了強(qiáng)制類型轉(zhuǎn)換,這也是使用 == 被廣泛認(rèn)為是不好編程習(xí)慣的主要原因, 由于它的復(fù)雜轉(zhuǎn)換規(guī)則,會(huì)導(dǎo)致難以跟蹤的問(wèn)題。

此外,強(qiáng)制類型轉(zhuǎn)換也會(huì)帶來(lái)性能消耗,比如一個(gè)字符串為了和一個(gè)數(shù)字進(jìn)行比較,必須事先被強(qiáng)制轉(zhuǎn)換為數(shù)字。

嚴(yán)格等于操作符

嚴(yán)格等于操作符由個(gè)等號(hào)組成:===

不像普通的等于操作符,嚴(yán)格等于操作符不會(huì)進(jìn)行強(qiáng)制類型轉(zhuǎn)換。

""           ===   "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

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

比較對(duì)象

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

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

這里等于操作符比較的不是值是否相等,而是是否屬于同一個(gè)身份;也就是說(shuō),只有對(duì)象的同一個(gè)實(shí)例才被認(rèn)為是相等的。 這有點(diǎn)像 Python 中的 is 和 C 中的指針比較。

結(jié)論

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

typeof 操作符

typeof 操作符(和 instanceof 一起)或許是 JavaScript 中最大的設(shè)計(jì)缺陷, 因?yàn)閹缀醪豢赡軓乃鼈兡抢锏玫较胍慕Y(jié)果。

盡管 instanceof 還有一些極少數(shù)的應(yīng)用場(chǎng)景,typeof 只有一個(gè)實(shí)際的應(yīng)用(譯者注這個(gè)實(shí)際應(yīng)用是用來(lái)檢測(cè)一個(gè)對(duì)象是否已經(jīng)定義或者是否已經(jīng)賦值), 而這個(gè)應(yīng)用卻不是用來(lái)檢查對(duì)象的類型。

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 操作符的運(yùn)算結(jié)果??梢钥吹?,這個(gè)值在大多數(shù)情況下都返回 "object"。

Class 一列表示對(duì)象的內(nèi)部屬性 [[Class]] 的值。

為了獲取對(duì)象的 [[Class]],我們需要使用定義在 Object.prototype 上的方法 toString。

對(duì)象的類定義

JavaScript 標(biāo)準(zhǔn)文檔只給出了一種獲取 [[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 方法被調(diào)用,this 被設(shè)置為了需要獲取 [[Class]] 值的對(duì)象。

譯者注Object.prototype.toString 返回一種標(biāo)準(zhǔn)格式字符串,所以上例可以通過(guò) 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]"

測(cè)試為定義變量

typeof foo !== 'undefined'

上面代碼會(huì)檢測(cè) foo 是否已經(jīng)定義;如果沒(méi)有定義而直接使用會(huì)導(dǎo)致 ReferenceError 的異常。 這是 typeof 唯一有用的地方。

結(jié)論

為了檢測(cè)一個(gè)對(duì)象的類型,強(qiáng)烈推薦使用 Object.prototype.toString 方法; 因?yàn)檫@是唯一一個(gè)可依賴的方式。正如上面表格所示,typeof 的一些返回值在標(biāo)準(zhǔn)文檔中并未定義, 因此不同的引擎實(shí)現(xiàn)可能不同。

除非為了檢測(cè)一個(gè)變量是否已經(jīng)定義,我們應(yīng)盡量避免使用 typeof 操作符。

instanceof 操作符

instanceof 操作符用來(lái)比較兩個(gè)操作數(shù)的構(gòu)造函數(shù)。只有在比較自定義的對(duì)象時(shí)才有意義。 如果用來(lái)比較內(nèi)置類型,將會(huì)和 typeof 操作符 一樣用處不大。

比較自定義對(duì)象

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

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

// 如果僅僅設(shè)置 Bar.prototype 為函數(shù) Foo 本身,而不是 Foo 構(gòu)造函數(shù)的一個(gè)實(shí)例
Bar.prototype = Foo;
new Bar() instanceof Foo; // false

instanceof 比較內(nèi)置類型

new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true

'foo' instanceof String; // false
'foo' instanceof Object; // false

有一點(diǎn)需要注意,instanceof 用來(lái)比較屬于不同 JavaScript 上下文的對(duì)象(比如,瀏覽器中不同的文檔結(jié)構(gòu))時(shí)將會(huì)出錯(cuò), 因?yàn)樗鼈兊臉?gòu)造函數(shù)不會(huì)是同一個(gè)對(duì)象。

結(jié)論

instanceof 操作符應(yīng)該僅僅用來(lái)比較來(lái)自同一個(gè) JavaScript 上下文的自定義對(duì)象。 正如 typeof 操作符一樣,任何其它的用法都應(yīng)該是避免的。

類型轉(zhuǎn)換

JavaScript 是弱類型語(yǔ)言,所以會(huì)在任何可能的情況下應(yīng)用強(qiáng)制類型轉(zhuǎn)換。

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

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

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

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

內(nèi)置類型的構(gòu)造函數(shù)

內(nèi)置類型(比如 NumberString)的構(gòu)造函數(shù)在被調(diào)用時(shí),使用或者不使用 new 的結(jié)果完全不同。

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

使用內(nèi)置類型 Number 作為構(gòu)造函數(shù)將會(huì)創(chuàng)建一個(gè)新的 Number 對(duì)象, 而在不使用 new 關(guān)鍵字的 Number 函數(shù)更像是一個(gè)數(shù)字轉(zhuǎn)換器。

另外,在比較中引入對(duì)象的字面值將會(huì)導(dǎo)致更加復(fù)雜的強(qiáng)制類型轉(zhuǎn)換。

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

轉(zhuǎn)換為字符串

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

將一個(gè)值加上空字符串可以輕松轉(zhuǎn)換為字符串類型。

轉(zhuǎn)換為數(shù)字

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

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

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

+'010' === 10
Number('010') === 10
parseInt('010', 10) === 10  // 用來(lái)轉(zhuǎn)換為整數(shù)

+'010.2' === 10.2
Number('010.2') === 10.2
parseInt('010.2', 10) === 10

轉(zhuǎn)換為布爾型

通過(guò)使用 操作符兩次,可以把一個(gè)值轉(zhuǎn)換為布爾型。

!!'foo';   // true
!!'';      // false
!!'0';     // true
!!'1';     // true
!!'-1'     // true
!!{};      // true
!!true;    // true

核心

為什么不要使用 eval

eval 函數(shù)會(huì)在當(dāng)前作用域中執(zhí)行一段 JavaScript 代碼字符串。

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

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

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

譯者注上面的代碼等價(jià)于在全局作用域中調(diào)用 eval,和下面兩種寫(xiě)法效果一樣:

// 寫(xiě)法一:直接調(diào)用全局作用域下的 foo 變量
var foo = 1;
function test() {
    var foo = 2;
    window.foo = 3;
    return foo;
}
test(); // 2
foo; // 3

// 寫(xiě)法二:使用 call 函數(shù)修改 eval 執(zhí)行的上下文為全局作用域
var foo = 1;
function test() {
    var foo = 2;
    eval.call(window, 'foo = 3');
    return foo;
}
test(); // 2
foo; // 3

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

偽裝的 eval

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

安全問(wèn)題

eval 也存在安全問(wèn)題,因?yàn)樗鼤?huì)執(zhí)行任意傳給它的代碼, 在代碼字符串未知或者是來(lái)自一個(gè)不信任的源時(shí),絕對(duì)不要使用 eval 函數(shù)。

結(jié)論

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

undefinednull

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

undefined 的值

undefined 是一個(gè)值為 undefined 的類型。

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

下面的情況會(huì)返回 undefined 值:

  • 訪問(wèn)未修改的全局變量 undefined。
  • 由于沒(méi)有定義 return 表達(dá)式的函數(shù)隱式返回。
  • return 表達(dá)式?jīng)]有顯式的返回任何內(nèi)容。
  • 訪問(wèn)不存在的屬性。
  • 函數(shù)參數(shù)沒(méi)有被顯式的傳遞值。
  • 任何被設(shè)置為 undefined 值的變量。

處理 undefined 值的改變

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

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

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

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

})('Hello World', 42);

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

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

})('Hello World', 42);

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

null 的用處

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

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

自動(dòng)分號(hào)插入

盡管 JavaScript 有 C 的代碼風(fēng)格,但是它強(qiáng)制要求在代碼中使用分號(hào),實(shí)際上可以省略它們。

JavaScript 不是一個(gè)沒(méi)有分號(hào)的語(yǔ)言,恰恰相反上它需要分號(hào)來(lái)就解析源代碼。 因此 JavaScript 解析器在遇到由于缺少分號(hào)導(dǎo)致的解析錯(cuò)誤時(shí),會(huì)自動(dòng)在源代碼中插入分號(hào)。

var foo = function() {
} // 解析錯(cuò)誤,分號(hào)丟失
test()

自動(dòng)插入分號(hào),解析器重新解析。

var foo = function() {
}; // 沒(méi)有錯(cuò)誤,解析繼續(xù)
test()

自動(dòng)的分號(hào)插入被認(rèn)為是 JavaScript 語(yǔ)言最大的設(shè)計(jì)缺陷之一,因?yàn)樗?em>能改變代碼的行為。

工作原理

下面的代碼沒(méi)有分號(hào),因此解析器需要自己判斷需要在哪些地方插入分號(hào)。

(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)

下面是解析器"猜測(cè)"的結(jié)果。

(function(window, undefined) {
    function test(options) {

        // 沒(méi)有插入分號(hào),兩行被合并為一行
        log('testing!')(options.list || []).forEach(function(i) {

        }); // <- 插入分號(hào)

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

        return; // <- 插入分號(hào), 改變了 return 表達(dá)式的行為
        { // 作為一個(gè)代碼段處理
            foo: function() {} 
        }; // <- 插入分號(hào)
    }
    window.test = test; // <- 插入分號(hào)

// 兩行又被合并了
})(window)(function(window) {
    window.someLibrary = {}; // <- 插入分號(hào)
})(window); //<- 插入分號(hào)

解析器顯著改變了上面代碼的行為,在另外一些情況下也會(huì)做出錯(cuò)誤的處理

前置括號(hào)

在前置括號(hào)的情況下,解析器不會(huì)自動(dòng)插入分號(hào)。

log('testing!')
(options.list || []).forEach(function(i) {})

上面代碼被解析器轉(zhuǎn)換為一行。

log('testing!')(options.list || []).forEach(function(i) {})

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

結(jié)論

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

其它

setTimeoutsetInterval

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

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

當(dāng) setTimeout 被調(diào)用時(shí),它會(huì)返回一個(gè) ID 標(biāo)識(shí)并且計(jì)劃在將來(lái)大約 1000 毫秒后調(diào)用 foo 函數(shù)。 foo 函數(shù)只會(huì)被執(zhí)行一次

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

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

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

setInterval 的堆調(diào)用

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

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

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

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

foo 被阻塞的時(shí)候,setInterval 仍然在組織將來(lái)對(duì)回調(diào)函數(shù)的調(diào)用。 因此,當(dāng)?shù)谝淮?foo 函數(shù)調(diào)用結(jié)束時(shí),已經(jīng)有 10 次函數(shù)調(diào)用在等待執(zhí)行。

處理可能的阻塞調(diào)用

最簡(jiǎn)單也是最容易控制的方案,是在回調(diào)函數(shù)內(nèi)部使用 setTimeout 函數(shù)。

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

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

手工清空定時(shí)器

可以通過(guò)將定時(shí)時(shí)產(chǎn)生的 ID 標(biāo)識(shí)傳遞給 clearTimeout 或者 clearInterval 函數(shù)來(lái)清除定時(shí), 至于使用哪個(gè)函數(shù)取決于調(diào)用的時(shí)候使用的是 setTimeout 還是 setInterval。

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

清除所有定時(shí)器

由于沒(méi)有內(nèi)置的清除所有定時(shí)器的方法,可以采用一種暴力的方式來(lái)達(dá)到這一目的。

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

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

隱藏使用 eval

setTimeoutsetInterval 也接受第一個(gè)參數(shù)為字符串的情況。 這個(gè)特性絕對(duì)不要使用,因?yàn)樗趦?nèi)部使用了 eval。

function foo() {
    // 將會(huì)被調(diào)用
}

function bar() {
    function foo() {
        // 不會(huì)被調(diào)用
    }
    setTimeout('foo()', 1000);
}
bar();

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

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

function foo(a, b, c) {}

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

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

結(jié)論

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

另外,應(yīng)該避免使用 setInterval,因?yàn)樗亩〞r(shí)執(zhí)行不會(huì)被 JavaScript 阻塞。