關(guān)于作者
這篇文章的作者是兩位 Stack Overflow 用戶, 伊沃·韋特澤爾 Ivo Wetzel(寫(xiě)作) 和 張易江 Zhang Yi Jiang(設(shè)計(jì))。
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>。
這篇文章的作者是兩位 Stack Overflow 用戶, 伊沃·韋特澤爾 Ivo Wetzel(寫(xiě)作) 和 張易江 Zhang Yi Jiang(設(shè)計(jì))。
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 聊天室找到我們。
JavaScript 中所有變量都可以當(dāng)作對(duì)象使用,除了兩個(gè)例外 null
和 undefined
。
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ì)算
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};
有兩種方式來(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)操作符在下面兩種情況下依然有效
刪除屬性的唯一方法是使用 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 undefined
和 foo null
- 只有 baz
被真正的刪除了,所以從輸出結(jié)果中消失。
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.prototype
和 Foo.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)。
一個(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
。
在寫(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.prototype
的 hasOwnProperty
方法。
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
當(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)題。
推薦總是使用 hasOwnProperty
。不要對(duì)代碼運(yùn)行的環(huán)境做任何假設(shè),不要假設(shè)原生對(duì)象是否已經(jīng)被擴(kuò)展了。
函數(shù)是JavaScript中的一等對(duì)象,這意味著可以把函數(shù)像其它值一樣傳遞。 一個(gè)常見(jiàn)的用法是把匿名函數(shù)作為回調(diào)函數(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() {}
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。
另外一個(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
指向的各不相同。
this;
當(dāng)在全部范圍內(nèi)使用 this
,它將會(huì)指向全局對(duì)象。
foo();
這里 this
也會(huì)指向全局對(duì)象。
test.foo();
這個(gè)例子中,this
指向 test
對(duì)象。
new foo();
如果函數(shù)傾向于和 new
關(guān)鍵詞一塊使用,則我們稱這個(gè)函數(shù)是 構(gòu)造函數(shù)。
在函數(shù)內(nèi)部,this
指向新創(chuàng)建的對(duì)象。
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
。
盡管大部分的情況都說(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ù)傳遞。
另一個(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
.
因?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
。
一個(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ù)字 0
到 9
,而是會(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
的拷貝。
為了正確的獲得循環(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ù)組。
下面的代碼將會(huì)創(chuàng)建一個(gè)新的數(shù)組,包含所有 arguments
對(duì)象中的元素。
Array.prototype.slice.call(arguments);
這個(gè)轉(zhuǎn)化比較慢,在性能不好的代碼中不推薦這種做法。
下面是將參數(shù)從一個(gè)函數(shù)傳遞到另一個(gè)函數(shù)的推薦做法。
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// 干活
}
另一個(gè)技巧是同時(shí)使用 call
和 apply
,創(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));
};
arguments
對(duì)象為其內(nèi)部屬性以及函數(shù)形式參數(shù)創(chuàng)建 getter 和 setter 方法。
因此,改變形參的值會(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ù)。
arguments
的 getters 和 setters 方法總會(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
和它的屬性。
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ì)象的 prototype
為 Foo.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"
我們常聽(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)的是一些不好的地方。
new
帶來(lái)的問(wèn)題,這似乎和語(yǔ)言本身的思想相違背。雖然遺漏 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);
foo
和 i
是函數(shù) test
內(nèi)的局部變量,而對(duì) bar
的賦值將會(huì)覆蓋全局作用域內(nèi)的同名變量。
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ì)按照下面順序查找:
var foo
的定義。foo
名稱的。foo
。只有一個(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(){}());
推薦使用匿名包裝器(譯者注:也就是自執(zhí)行的匿名函數(shù))來(lái)創(chuàng)建命名空間。這樣不僅可以防止命名沖突, 而且有利于程序的模塊化。
另外,使用全局變量被認(rèn)為是不好的習(xí)慣。這樣的代碼容易產(chǎn)生錯(cuò)誤并且維護(hù)成本較高。
雖然在 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)生影響。
為了更好的性能,推薦使用普通的 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);
應(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)格等于操作符由三個(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ù)類型不同就肯定不相等也有助于性能的提升。
雖然 ==
和 ===
操作符都是等于操作符,但是當(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 中的指針比較。
強(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ì)象的類型。
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
。
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]"
typeof foo !== 'undefined'
上面代碼會(huì)檢測(cè) foo
是否已經(jīng)定義;如果沒(méi)有定義而直接使用會(huì)導(dǎo)致 ReferenceError
的異常。
這是 typeof
唯一有用的地方。
為了檢測(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
操作符 一樣用處不大。
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ì)象。
instanceof
操作符應(yīng)該僅僅用來(lái)比較來(lái)自同一個(gè) JavaScript 上下文的自定義對(duì)象。
正如 typeof
操作符一樣,任何其它的用法都應(yīng)該是避免的。
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)置類型(比如 Number
和 String
)的構(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)換為三種可能的類型之一。
'' + 10 === '10'; // true
將一個(gè)值加上空字符串可以輕松轉(zhuǎn)換為字符串類型。
+'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
通過(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ù) setTimeout
和 setInterval
都可以接受字符串作為它們的第一個(gè)參數(shù)。
這個(gè)字符串總是在全局作用域中執(zhí)行,因此 eval
在這種情況下沒(méi)有被直接調(diào)用。
eval
也存在安全問(wèn)題,因?yàn)樗鼤?huì)執(zhí)行任意傳給它的代碼,
在代碼字符串未知或者是來(lái)自一個(gè)不信任的源時(shí),絕對(duì)不要使用 eval
函數(shù)。
絕對(duì)不要使用 eval
,任何使用它的代碼都會(huì)在它的工作方式,性能和安全性方面受到質(zhì)疑。
如果一些情況必須使用到 eval
才能正常工作,首先它的設(shè)計(jì)會(huì)受到質(zhì)疑,這不應(yīng)該是首選的解決方案,
一個(gè)更好的不使用 eval
的解決方案應(yīng)該得到充分考慮并優(yōu)先采用。
undefined
和 null
JavaScript 有兩個(gè)表示‘空’的值,其中比較有用的是 undefined
。
undefined
的值undefined
是一個(gè)值為 undefined
的類型。
這個(gè)語(yǔ)言也定義了一個(gè)全局變量,它的值是 undefined
,這個(gè)變量也被稱為 undefined
。
但是這個(gè)變量不是一個(gè)常量,也不是一個(gè)關(guān)鍵字。這意味著它的值可以輕易被覆蓋。
下面的情況會(huì)返回 undefined
值:
undefined
。return
表達(dá)式的函數(shù)隱式返回。return
表達(dá)式?jīng)]有顯式的返回任何內(nèi)容。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)代替。
盡管 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)的情況下,解析器不會(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
。
建議絕對(duì)不要省略分號(hào),同時(shí)也提倡將花括號(hào)和相應(yīng)的表達(dá)式放在一行,
對(duì)于只有一行代碼的 if
或者 else
表達(dá)式,也不應(yīng)該省略花括號(hào)。
這些良好的編程習(xí)慣不僅可以提到代碼的一致性,而且可以防止解析器改變代碼行為的錯(cuò)誤處理。
setTimeout
和 setInterval
由于 JavaScript 是異步的,可以使用 setTimeout
和 setInterval
來(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í)行。
最簡(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í)行。
可以通過(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);
由于沒(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
setTimeout
和 setInterval
也接受第一個(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)
絕對(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 阻塞。