關于作者
這篇文章的作者是兩位 Stack Overflow 用戶, 伊沃·韋特澤爾 Ivo Wetzel(寫作) 和 張易江 Zhang Yi Jiang(設計)。
JavaScript 秘密花園是一個不斷更新,主要關心 JavaScript 一些古怪用法的文檔。 對于如何避免常見的錯誤,難以發(fā)現(xiàn)的問題,以及性能問題和不好的實踐給出建議, 初學者可以籍此深入了解 JavaScript 的語言特性。
JavaScript 秘密花園不是用來教你 JavaScript。為了更好的理解這篇文章的內容, 你需要事先學習 JavaScript 的基礎知識。在 Mozilla 開發(fā)者網(wǎng)絡中有一系列非常棒的 JavaScript 學習向導。
這篇文章的作者是兩位 Stack Overflow 用戶, 伊沃·韋特澤爾 Ivo Wetzel(寫作) 和 張易江 Zhang Yi Jiang(設計)。
JavaScript 秘密花園在 MIT license 許可協(xié)議下發(fā)布,并存放在 GitHub 開源社區(qū)。 如果你發(fā)現(xiàn)錯誤或者打字錯誤,請新建一個任務單或者發(fā)一個抓取請求。 你也可以在 Stack Overflow 的 JavaScript 聊天室找到我們。
JavaScript 中所有變量都可以當作對象使用,除了兩個例外 null
和 undefined
。
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先被計算
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
兩種語法是等價的,但是中括號操作符在下面兩種情況下依然有效
刪除屬性的唯一方法是使用 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 undefined
和 foo null
- 只有 baz
被真正的刪除了,所以從輸出結果中消失。
var test = {
'case': 'I am a keyword so I must be notated as a string',
delete: 'I am a keyword too so me' // 出錯:SyntaxError
};
對象的屬性名可以使用字符串或者普通字符聲明。但是由于 JavaScript 解析器的另一個錯誤設計,
上面的第二種聲明方式在 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.prototype
和 Foo.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.prototype
的 hasOwnProperty
方法。
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ù)是JavaScript中的一等對象,這意味著可以把函數(shù)像其它值一樣傳遞。 一個常見的用法是把匿名函數(shù)作為回調函數(shù)傳遞到異步函數(shù)中。
function foo() {}
上面的方法會在執(zhí)行前被 解析(hoisted),因此它存在于當前上下文的任意一個地方, 即使在函數(shù)定義體的上面被調用也是對的。
foo(); // 正常運行,因為foo在代碼運行前已經被創(chuàng)建
function foo() {}
var foo = function() {};
這個例子把一個匿名的函數(shù)賦值給變量 foo
。
foo; // 'undefined'
foo(); // 出錯:TypeError
var foo = function() {};
由于 var
定義了一個聲明語句,對變量 foo
的解析是在代碼運行之前,因此 foo
變量在代碼運行時已經被定義過了。
但是由于賦值語句只在運行時執(zhí)行,因此在相應代碼執(zhí)行之前, foo
的值缺省為 undefined。
另外一個特殊的情況是將命名函數(shù)賦值給一個變量。
var foo = function bar() {
bar(); // 正常運行
}
bar(); // 出錯:ReferenceError
bar
函數(shù)聲明外是不可見的,這是因為我們已經把函數(shù)賦值給了 foo
;
然而在 bar
內部依然可見。這是由于 JavaScript 的 命名處理 所致,
函數(shù)名在函數(shù)內總是可見的。
this
的工作原理JavaScript 有一套完全不同于其它語言的對 this
的處理機制。
在五種不同的情況下 ,this
指向的各不相同。
this;
當在全部范圍內使用 this
,它將會指向全局對象。
foo();
這里 this
也會指向全局對象。
test.foo();
這個例子中,this
指向 test
對象。
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
。
一個常見的錯誤出現(xiàn)在循環(huán)中使用閉包,假設我們需要在每次循環(huán)中調用循環(huán)序號
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
上面的代碼不會輸出數(shù)字 0
到 9
,而是會輸出數(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ù)組。
下面的代碼將會創(chuàng)建一個新的數(shù)組,包含所有 arguments
對象中的元素。
Array.prototype.slice.call(arguments);
這個轉化比較慢,在性能不好的代碼中不推薦這種做法。
下面是將參數(shù)從一個函數(shù)傳遞到另一個函數(shù)的推薦做法。
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// 干活
}
另一個技巧是同時使用 call
和 apply
,創(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)建 getter 和 setter 方法。
因此,改變形參的值會影響到 arguments
對象的值,反之亦然。
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
不管它是否有被使用,arguments
對象總會被創(chuàng)建,除了兩個特殊情況 - 作為局部變量聲明和作為形式參數(shù)。
arguments
的 getters 和 setters 方法總會被創(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
和它的屬性。
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)建對象的 prototype
為 Foo.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"
我們常聽到的一條忠告是不要使用 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
的調用方式不容易出錯,并且可以充分利用私有變量帶來的便利,
但是隨之而來的是一些不好的地方。
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);
foo
和 i
是函數(shù) test
內的局部變量,而對 bar
的賦值將會覆蓋全局作用域內的同名變量。
JavaScript 會提升變量聲明。這意味著 var
表達式和 function
聲明都將會被提升到當前作用域的頂部。
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
上面代碼在運行之前將會被轉化。JavaScript 將會把 var
表達式和 function
聲明提升到當前作用域的頂部。
// var 表達式被移動到這里
var bar, someValue; // 缺省值是 'undefined'
// 函數(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 會按照下面順序查找:
var foo
的定義。foo
名稱的。foo
。只有一個全局作用域導致的常見錯誤是命名沖突。在 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)建命名空間。這樣不僅可以防止命名沖突, 而且有利于程序的模塊化。
另外,使用全局變量被認為是不好的習慣。這樣的代碼容易產生錯誤并且維護成本較高。
雖然在 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
只有一個實際的應用(譯者注:這個實際應用是用來檢測一個對象是否已經定義或者是否已經賦值),
而這個應用卻不是用來檢查對象的類型。
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)仍然會導致一些其它問題。
內置類型(比如 Number
和 String
)的構造函數(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
將一個值加上空字符串可以輕松轉換為字符串類型。
+'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ù) setTimeout
和 setInterval
都可以接受字符串作為它們的第一個參數(shù)。
這個字符串總是在全局作用域中執(zhí)行,因此 eval
在這種情況下沒有被直接調用。
eval
也存在安全問題,因為它會執(zhí)行任意傳給它的代碼,
在代碼字符串未知或者是來自一個不信任的源時,絕對不要使用 eval
函數(shù)。
絕對不要使用 eval
,任何使用它的代碼都會在它的工作方式,性能和安全性方面受到質疑。
如果一些情況必須使用到 eval
才能正常工作,首先它的設計會受到質疑,這不應該是首選的解決方案,
一個更好的不使用 eval
的解決方案應該得到充分考慮并優(yōu)先采用。
undefined
和 null
JavaScript 有兩個表示‘空’的值,其中比較有用的是 undefined
。
undefined
的值undefined
是一個值為 undefined
的類型。
這個語言也定義了一個全局變量,它的值是 undefined
,這個變量也被稱為 undefined
。
但是這個變量不是一個常量,也不是一個關鍵字。這意味著它的值可以輕易被覆蓋。
下面的情況會返回 undefined
值:
undefined
。return
表達式的函數(shù)隱式返回。return
表達式沒有顯式的返回任何內容。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
表達式,也不應該省略花括號。
這些良好的編程習慣不僅可以提到代碼的一致性,而且可以防止解析器改變代碼行為的錯誤處理。
setTimeout
和 setInterval
由于 JavaScript 是異步的,可以使用 setTimeout
和 setInterval
來計劃執(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
setTimeout
和 setInterval
也接受第一個參數(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 阻塞。