關於作者
這篇文章的作者是兩位 Stack Overflow 用戶, 伊沃·韋特澤爾 Ivo Wetzel(寫作) 和 張易江 Zhang Yi Jiang(設計)。
貢獻者
- Caio Romão (拼寫檢查)
- Andreas Blixt (語言修正)
中文翻譯
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
對象
對象使用和屬性
JavaScript 中所有變量都是對象,除了兩個例外
null
和
undefined
。
false.toString(); // 'false'
[1, 2, 3].toString(); // '1,2,3'
function Foo(){}
Foo.bar = 1;
Foo.bar; // 1
一個常見的誤解是數字的字面值(literal)不是對象。這是因爲 JavaScript 解析器的一個錯誤,它試圖將點操作符解析爲浮點數字面值的一部分。
2.toString(); // 出錯:SyntaxError
有很多變通方法可以讓數字的字面值看起來像對象。
2..toString(); // 第二個點號可以正常解析
2 .toString(); // 注意點號前面的空格
(2).toString(); // 2先被計算
對象作爲數據類型
JavaScript 的對象可以作爲哈希表使用,主要用來保存命名的鍵與值的對應關係。
使用對象的字面語法 - {}
- 可以創建一個簡單對象。這個新創建的對象從 Object.prototype
繼承下面,沒有任何自定義屬性。
var foo = {}; // 一個空對象
// 一個新對象,擁有一個值爲12的自定義屬性'test'
var bar = {test: 12};
訪問屬性
有兩種方式來訪問對象的屬性,點操作符或者中括號操作符。
var foo = {name: 'Kitten'}
foo.name; // kitten
foo['name']; // kitten
var get = 'name';
foo[get]; // kitten
foo.1234; // SyntaxError
foo['1234']; // works
兩種語法是等價的,但是中括號操作符在下面兩種情況下依然有效 - 動態設置屬性 - 屬性名不是一個有效的變量名(譯者注:比如屬性名中包含空格,或者屬性名是 JS 的關鍵詞)
刪除屬性
刪除屬性的唯一方法是使用 delete
操作符;設置屬性爲 undefined
或者 null
並不能真正的刪除屬性,而僅僅是移除了屬性和值的關聯。
var obj = {
bar: 1,
foo: 2,
baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;
for(var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i, '' + obj[i]);
}
}
上面的輸出結果有 bar undefined
和 foo null
- 只有 baz
被真正的刪除了,所以從輸出結果中消失。
屬性名的語法
var test = {
'case': 'I am a keyword so I must be notated as a string',
delete: 'I am a keyword too so me' // 出錯:SyntaxError
};
對象的屬性名可以使用字符串或者普通字符聲明。但是由於 JavaScript 解析器的另一個錯誤設計,上面的第二種聲明方式在 ECMAScript 5 之前會拋出
SyntaxError
的錯誤。
這個錯誤的原因是 delete
是 JavaScript 語言的一個關鍵詞;因此爲了在更低版本的 JavaScript 引擎下也能正常運行,必須使用字符串字面值聲明方式。
原型
JavaScript 不包含傳統的類繼承模型,而是使用 prototypal 原型模型。
雖然這經常被當作是 JavaScript 的缺點被提及,其實基於原型的繼承模型比傳統的類繼承還要強大。實現傳統的類繼承模型是很簡單,但是實現 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 使用原型鏈的繼承方式。
注意: 簡單的使用 Bar.prototype = Foo.prototype
將會導致兩個對象共享相同的原型。 因此,改變任意一個對象的原型都會影響到另一個對象的原型,在大多數情況下這不是希望的結果。
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// 設置Bar的prototype屬性爲Foo的實例對象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';
// 修正Bar.prototype.constructor爲Bar本身
Bar.prototype.constructor = Bar;
var test = new Bar() // 創建Bar的一個新實例
// 原型鏈
test [Bar的實例]
Bar.prototype [Foo的實例]
{ foo: 'Hello World' }
Foo.prototype
{method: ...};
Object.prototype
{toString: ... /* etc. */};
上面的例子中,test
對象從 Bar.prototype
和 Foo.prototype
繼承下來;因此,它能訪問
Foo
的原型方法 method
。同時,它也能夠訪問那個定義在原型上的
Foo
實例屬性 value
。需要注意的是 new Bar()
不會創造出一個新的
Foo
實例,而是重複使用它原型上的那個實例;因此,所有的 Bar
實例都會共享相同的
value
屬性。
注意: 不要使用 Bar.prototype = Foo
,因爲這不會執行 Foo
的原型,而是指向函數
Foo
。 因此原型鏈將會回溯到 Function.prototype
而不是 Foo.prototype
,因此
method
將不會在 Bar 的原型鏈上。
屬性查找
當查找一個對象的屬性時,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性爲止。
到查找到達原型鏈的頂部 - 也就是 Object.prototype
- 但是仍然沒有找到指定的屬性,就會返回
undefined。
原型屬性
當原型屬性用來創建原型鏈時,可以把任何類型的值賦給它(prototype)。然而將原子類型賦給 prototype 的操作將會被忽略。
function Foo() {}
Foo.prototype = 1; // 無效
而將對象賦值給 prototype,正如上面的例子所示,將會動態的創建原型鏈。
擴展內置類型的原型
一個錯誤特性被經常使用,那就是擴展 Object.prototype
或者其他內置類型的原型對象。
這種技術被稱之爲 monkey patching 並且會破壞封裝。雖然它被廣泛的應用到一些 JavaScript 類庫中比如 Prototype, 但是我仍然不認爲爲內置類型添加一些非標準的函數是個好主意。
擴展內置類型的唯一理由是爲了和新的 JavaScript 保持一致,比如
Array.forEach
。
總結
在寫複雜的 JavaScript 應用之前,充分理解原型鏈繼承的工作方式是每個 JavaScript 程序員必修的功課。要提防原型鏈過長帶來的性能問題,並知道如何通過縮短原型鏈來提高性能。更進一步,絕對不要擴展內置類型的原型,除非是爲了和新的 JavaScript 引擎兼容。
hasOwnProperty
函數
爲了判斷一個對象是否包含自定義屬性而不是原型鏈上的屬性,我們需要使用繼承自
Object.prototype
的 hasOwnProperty
方法。
注意: 通過判斷一個屬性是否 undefined
是不夠的。 因爲一個屬性可能確實存在,只不過它的值被設置爲
undefined
。
hasOwnProperty
是 JavaScript 中唯一一個處理屬性但是不查找原型鏈的函數。
// 修改Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};
foo.bar; // 1
'bar' in foo; // true
foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true
只有 hasOwnProperty
可以給出正確和期望的結果,這在遍歷對象的屬性時會很有用。 沒有其它方法可以用來排除原型鏈上的屬性,而不是定義在對象自身上的屬性。
hasOwnProperty
作爲屬性
JavaScript 不會保護 hasOwnProperty
被非法佔用,因此如果一個對象碰巧存在這個屬性,就需要使用外部的
hasOwnProperty
函數來獲取正確的結果。
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Here be dragons'
};
foo.hasOwnProperty('bar'); // 總是返回 false
// 使用其它對象的 hasOwnProperty,並將其上下爲設置爲foo
{}.hasOwnProperty.call(foo, 'bar'); // true
結論
當檢查對象上某個屬性是否存在時,hasOwnProperty
是唯一可用的方法。同時在使用
for in
loop 遍歷對象時,推薦總是使用 hasOwnProperty
方法,這將會避免原型對象擴展帶來的干擾。
for in
循環
和 in
操作符一樣,for in
循環同樣在查找對象屬性時遍歷原型鏈上的所有屬性。
注意: for in
循環不會遍歷那些 enumerable
設置爲
false
的屬性;比如數組的 length
屬性。
// 修改 Object.prototype
Object.prototype.bar = 1;
var foo = {moo: 2};
for(var i in foo) {
console.log(i); // 輸出兩個屬性:bar 和 moo
}
由於不可能改變 for in
自身的行爲,因此有必要過濾出那些不希望出現在循環體中的屬性,這可以通過 Object.prototype
原型上的
hasOwnProperty
函數來完成。
注意: 由於 for in
總是要遍歷整個原型鏈,因此如果一個對象的繼承層次太深的話會影響性能。
使用 hasOwnProperty
過濾
// foo 變量是上例中的
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
這個版本的代碼是唯一正確的寫法。由於我們使用了 hasOwnProperty
,所以這次只輸出
moo
。如果不使用 hasOwnProperty
,則這段代碼在原生對象原型(比如 Object.prototype
)被擴展時可能會出錯。
一個廣泛使用的類庫 Prototype 就擴展了原生的 JavaScript 對象。因此,但這個類庫被包含在頁面中時,不使用
hasOwnProperty
過濾的 for in
循環難免會出問題。
總結
推薦總是使用 hasOwnProperty
。不要對代碼運行的環境做任何假設,不要假設原生對象是否已經被擴展了。
函數
函數聲明與表達式
函數是JavaScript中的一等對象,這意味着可以把函數像其它值一樣傳遞。一個常見的用法是把匿名函數作爲回調函數傳遞到異步函數中。
函數聲明
function foo() {}
上面的方法會在執行前被 解析(hoisted),因此它存在於當前上下文的任意一個地方,即使在函數定義體的上面被調用也是對的。
foo(); // 正常運行,因爲foo在代碼運行前已經被創建
function foo() {}
函數賦值表達式
var foo = function() {};
這個例子把一個匿名的函數賦值給變量 foo
。
foo; // 'undefined'
foo(); // 出錯:TypeError
var foo = function() {};
由於 var
定義了一個聲明語句,對變量 foo
的解析是在代碼運行之前,因此 foo
變量在代碼運行時已經被定義過了。
但是由於賦值語句只在運行時執行,因此在相應代碼執行之前, foo
的值缺省爲
undefined。
命名函數的賦值表達式
另外一個特殊的情況是將命名函數賦值給一個變量。
var foo = function bar() {
bar(); // 正常運行
}
bar(); // 出錯:ReferenceError
bar
函數聲明外是不可見的,這是因爲我們已經把函數賦值給了 foo
;然而在 bar
內部依然可見。這是由於 JavaScript 的
命名處理 所致,函數名在函數內總是可見的。
this
的工作原理
JavaScript 有一套完全不同於其它語言的對 this
的處理機制。在五種不同的情況下 ,this
指向的各不相同。
函數調用
foo();
這裏 this
也會指向全局對象。
ES5 注意: 在嚴格模式下(strict mode),不存在全局變量。 這種情況下 this
將會是
undefined
。
方法調用
test.foo();
這個例子中,this
指向 test
對象。
顯式的設置 this
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]); // 數組將會被擴展,如下所示
foo.call(bar, 1, 2, 3); // 傳遞到foo的參數是:a = 1, b = 2, c = 3
當使用 Function.prototype
上的 call
或者 apply
方法時,函數內的
this
將會被 顯式設置爲函數調用的第一個參數。
因此函數調用的規則在上例中已經不適用了,在foo
函數內 this
被設置成了
bar
。
注意: 在對象的字面聲明語法中,this
不能用來指向對象本身。 因此
var obj = {me: this}
中的 me
不會指向 obj
,因爲
this
只可能出現在上述的五種情況中。 譯者注:這個例子中,如果是在瀏覽器中運行,obj.me
等於
window
對象。
常見誤解
儘管大部分的情況都說的過去,不過第一個規則(譯者注:這裏指的應該是第二個規則,也就是直接調用函數時,this
指向全局對象)被認爲是JavaScript語言另一個錯誤設計的地方,因爲它從來就沒有實際的用途。
Foo.method = function() {
function test() {
// this 將會被設置爲全局對象(譯者注:瀏覽器環境中也就是 window 對象)
}
test();
}
一個常見的誤解是 test
中的 this
將會指向 Foo
對象,實際上不是這樣子的。
爲了在 test
中獲取對 Foo
對象的引用,我們需要在 method
函數內部創建一個局部變量指向
Foo
對象。
Foo.method = function() {
var that = this;
function test() {
// 使用 that 來指向 Foo 對象
}
test();
}
that
只是我們隨意起的名字,不過這個名字被廣泛的用來指向外部的 this
對象。在
閉包 一節,我們可以看到 that
可以作爲參數傳遞。
方法的賦值表達式
另一個看起來奇怪的地方是函數別名,也就是將一個方法賦值給一個變量。
var test = someObject.methodTest;
test();
上例中,test
就像一個普通的函數被調用;因此,函數內的 this
將不再被指向到 someObject
對象。
雖然 this
的晚綁定特性似乎並不友好,但是這確實基於原型繼承賴以生存的土壤。
function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
當 method
被調用時,this
將會指向 Bar
的實例對象。
閉包和引用
閉包是 JavaScript 一個非常重要的特性,這意味着當前作用域總是能夠訪問外部作用域中的變量。因爲 函數 是 JavaScript 中唯一擁有自身作用域的結構,因此閉包的創建依賴於函數。
模擬私有變量
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
這裏,Counter
函數返回兩個閉包,函數 increment
和函數 get
。 這兩個函數都維持着對外部作用域
Counter
的引用,因此總可以訪問此作用域內定義的變量 count
.
爲什麼不可以在外部訪問私有變量
因爲 JavaScript 中不可以對作用域進行引用或賦值,因此沒有辦法在外部訪問 count
變量。唯一的途徑就是通過那兩個閉包。
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
上面的代碼不會改變定義在 Counter
作用域中的 count
變量的值,因爲
foo.hack
沒有定義在那個作用域內。它將會創建或者覆蓋全局變量 count
。
循環中的閉包
一個常見的錯誤出現在循環中使用閉包,假設我們需要在每次循環中調用循環序號
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
上面的代碼不會輸出數字 0
到 9
,而是會輸出數字 10
十次。
當 console.log
被調用的時候,匿名函數保持對外部變量 i
的引用,此時
for
循環已經結束, i
的值被修改成了 10
.
爲了得到想要的結果,需要在每次循環中創建變量 i
的拷貝。
避免引用錯誤
爲了正確的獲得循環序號,最好使用 匿名包裹器(譯者注:其實就是我們通常說的自執行匿名函數)。
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
外部的匿名函數會立即執行,並把 i
作爲它的參數,此時函數內 e
變量就擁有了 i
的一個拷貝。
當傳遞給 setTimeout
的匿名函數執行時,它就擁有了對 e
的引用,而這個值是不會被循環改變的。
有另一個方法完成同樣的工作;那就是從匿名包裝器中返回一個函數。這和上面的代碼效果一樣。
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
arguments
對象
JavaScript 中每個函數內都能訪問一個特別變量 arguments
。這個變量維護着所有傳遞到這個函數中的參數列表。
注意: 由於 arguments
已經被定義爲函數內的一個變量。 因此通過 var
關鍵字定義
arguments
或者將 arguments
聲明爲一個形式參數, 都將導致原生的 arguments
不會被創建。
arguments
變量不是一個數組(Array
)。儘管在語法上它有數組相關的屬性
length
,但它不從 Array.prototype
繼承,實際上它是一個對象(Object
)。
因此,無法對 arguments
變量使用標準的數組方法,比如 push
, pop
或者
slice
。雖然使用 for
循環遍歷也是可以的,但是爲了更好的使用數組方法,最好把它轉化爲一個真正的數組。
轉化爲數組
下面的代碼將會創建一個新的數組,包含所有 arguments
對象中的元素。
Array.prototype.slice.call(arguments);
這個轉化比較慢,在性能不好的代碼中不推薦這種做法。
傳遞參數
下面將參數從一個函數傳遞到另一個函數,是推薦的做法。
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// do stuff here
}
另一個技巧是同時使用 call
和 apply
,創建一個快速的解綁定包裝器。
function Foo() {}
Foo.prototype.method = function(a, b, c) {
console.log(this, a, b, c);
};
// Create an unbound version of "method"
// 輸入參數爲: this, arg1, arg2...argN
Foo.method = function() {
// 結果: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};
譯者注:上面的
Foo.method
函數和下面代碼的效果是一樣的:
Foo.method = function() {
var args = Array.prototype.slice.call(arguments);
Foo.prototype.method.apply(args[0], args.slice(1));
};
自動更新
arguments
對象爲其內部屬性以及函數形式參數創建 getter 和 setter 方法。
因此,改變形參的值會影響到 arguments
對象的值,反之亦然。
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
性能真相
arguments
對象總會被創建,除了兩個特殊情況 - 作爲局部變量聲明和作爲形式參數。而不管它是否有被使用。
arguments
的 getters 和 setters 方法總會被創建;因此使用
arguments
對性能不會有什麼影響。除非是需要對 arguments
對象的屬性進行多次訪問。
ES5 提示: 這些 getters 和 setters 在嚴格模式下(strict mode)不會被創建。
譯者注:在
MDC 中對 strict mode
模式下 arguments
的描述有助於我們的理解,請看下面代碼:
// 闡述在 ES5 的嚴格模式下 `arguments` 的特性
function f(a) {
"use strict";
a = 42;
return [a, arguments[0]];
}
var pair = f(17);
assert(pair[0] === 42);
assert(pair[1] === 17);
然而,的確有一種情況會顯著的影響現代 JavaScript 引擎的性能。這就是使用 arguments.callee
。
function foo() {
arguments.callee; // do something with this function object
arguments.callee.caller; // and the calling function object
}
function bigLoop() {
for(var i = 0; i < 100000; i++) {
foo(); // Would normally be inlined...
}
}
上面代碼中,foo
不再是一個單純的內聯函數
inlining(譯者注:這裏指的是解析器可以做內聯處理),因爲它需要知道它自己和它的調用者。這不僅抵消了內聯函數帶來的性能提升,而且破壞了封裝,因此現在函數可能要依賴於特定的上下文。
因此強烈建議大家不要使用 arguments.callee
和它的屬性。
ES5 提示: 在嚴格模式下,arguments.callee
會報錯 TypeError
,因爲它已經被廢除了。
構造函數
JavaScript 中的構造函數和其它語言中的構造函數是不同的。通過 new
關鍵字方式調用的函數都被認爲是構造函數。
在構造函數內部 - 也就是被調用的函數內 - this
指向新創建的對象 Object
。這個新創建的對象的
prototype
被指向到構造函數的 prototype
。
如果被調用的函數沒有顯式的 return
表達式,則隱式的會返回 this
對象 - 也就是新創建的對象。
function Foo() {
this.bla = 1;
}
Foo.prototype.test = function() {
console.log(this.bla);
};
var test = new Foo();
上面代碼把 Foo
作爲構造函數調用,並設置新創建對象的 prototype
爲 Foo.prototype
。
顯式的 return
表達式將會影響返回結果,但僅限於返回的是一個對象。
function Bar() {
return 2;
}
new Bar(); // 返回新創建的對象
function Test() {
this.value = 2;
return {
foo: 1
};
}
new Test(); // 返回的對象
譯者注:new Bar()
返回的是新創建的對象,而不是數字的字面值 2。因此
new Bar().constructor === Bar
,但是如果返回的是數字對象,結果就不同了,如下所示
function Bar() {
return new Number(2);
}
new Bar().constructor === Number
譯者注:這裏得到的
new Test()
是函數返回的對象,而不是通過new
關鍵字新創建的對象,因此:
(new Test()).value === undefined
(new Test()).foo === 1
如果 new
被遺漏了,則函數不會返回新創建的對象。
function Foo() {
this.bla = 1; // 獲取設置全局參數
}
Foo(); // undefined
雖然上例在有些情況下也能正常運行,但是由於 JavaScript 中
this
的工作原理,這裏的 this
指向全局對象。
工廠模式
爲了不使用 new
關鍵字,構造函數必須顯式的返回一個值。
function Bar() {
var value = 1;
return {
method: function() {
return value;
}
}
}
Bar.prototype = {
foo: function() {}
};
new Bar();
Bar();
上面兩種對 Bar
函數的調用返回的值完全相同,一個新創建的擁有 method
屬性的對象被返回,其實這裏創建了一個閉包。
還需要注意, new Bar()
並不會改變返回對象的原型(譯者注:也就是返回對象的原型不會指向
Bar.prototype
)。因爲構造函數的原型會被指向到剛剛創建的新對象,而這裏的 Bar
沒有把這個新對象返回(譯者注:而是返回了一個包含
method
屬性的自定義對象)。
在上面的例子中,使用或者不使用 new
關鍵字沒有功能性的區別。
譯者注:上面兩種方式創建的對象不能訪問
Bar
原型鏈上的屬性,如下所示:
var bar1 = new Bar();
typeof(bar1.method); // "function"
typeof(bar1.foo); // "undefined"
var bar2 = Bar();
typeof(bar2.method); // "function"
typeof(bar2.foo); // "undefined"
通過工廠模式創建新對象
我們常聽到的一條忠告是不要使用 new
關鍵字來調用函數,因爲如果忘記使用它就會導致錯誤。
爲了創建新對象,我們可以創建一個工廠方法,並且在方法內構造一個新對象。
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 支持一對花括號創建的代碼段,但是並不支持塊級作用域;而僅僅支持 函數作用域。
function test() { // 一個作用域
for(var i = 0; i < 10; i++) { // 不是一個作用域
// count
}
console.log(i); // 10
}
注意: 如果不是在賦值語句中,而是在 return 表達式或者函數參數中,{...}
將會作爲代碼段解析, 而不是作爲對象的字面語法解析。如果考慮到
自動分號插入,這可能會導致一些不易察覺的錯誤。
譯者注:如果
return
對象的左括號和 return
不在一行上就會出錯。
// 譯者注:下面輸出 undefined
function add(a, b) {
return
a + b;
}
console.log(add(1, 2));
JavaScript 中沒有顯式的命名空間定義,這就意味着所有對象都定義在一個全局共享的命名空間下面。
每次引用一個變量,JavaScript 會向上遍歷整個作用域直到找到這個變量爲止。如果到達全局作用域但是這個變量仍未找到,則會拋出 ReferenceError
異常。
隱式的全局變量
// 腳本 A
foo = '42';
// 腳本 B
var foo = '42'
上面兩段腳本效果不同。腳本 A 在全局作用域內定義了變量 foo
,而腳本 B 在當前作用域內定義變量
foo
。
再次強調,上面的效果完全不同,不使用 var
聲明變量將會導致隱式的全局變量產生。
// 全局作用域
var foo = 42;
function test() {
// 局部作用域
foo = 21;
}
test();
foo; // 21
在函數 test
內不使用 var
關鍵字聲明 foo
變量將會覆蓋外部的同名變量。起初這看起來並不是大問題,但是當有成千上萬行代碼時,不使用
var
聲明變量將會帶來難以跟蹤的 BUG。
// 全局作用域
var items = [/* 數組 */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// subLoop 函數作用域
for(i = 0; i < 10; i++) { // 沒有使用 var 聲明變量
// 幹活
}
}
外部循環在第一次調用 subLoop
之後就會終止,因爲 subLoop
覆蓋了全局變量 i
。在第二個
for
循環中使用 var
聲明變量可以避免這種錯誤。聲明變量時絕對不要遺漏
var
關鍵字,除非這就是期望的影響外部作用域的行爲。
局部變量
JavaScript 中局部變量只可能通過兩種方式聲明,一個是作爲函數參數,另一個是通過
var
關鍵字聲明。
// 全局變量
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// 函數 test 內的局部作用域
i = 5;
var foo = 3;
bar = 4;
}
test(10);
foo
和 i
是函數 test
內的局部變量,而對 bar
的賦值將會覆蓋全局作用域內的同名變量。
變量聲明提升(Hoisting)
JavaScript 會提升變量聲明。這意味着 var
表達式和 function
聲明都將會被提升到當前作用域的頂部。
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
上面代碼在運行之前將會被轉化。JavaScript 將會把 var
表達式和 function
聲明提升到當前作用域的頂部。
// var 表達式被移動到這裏
var bar, someValue; // 缺省值是 'undefined'
// 函數聲明也會提升
function test(data) {
var goo, i, e; // 沒有塊級作用域,這些變量被移動到函數頂部
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // 出錯:TypeError,因爲 bar 依然是 'undefined'
someValue = 42; // 賦值語句不會被提升規則(hoisting)影響
bar = function() {};
test();
沒有塊級作用域不僅導致 var
表達式被從循環內移到外部,而且使一些 if
表達式更難看懂。
在原來代碼中,if
表達式看起來修改了全部變量 goo
,實際上在提升規則被應用後,卻是在修改局部變量。
如果沒有提升規則(hoisting)的知識,下面的代碼看起來會拋出異常 ReferenceError
。
// 檢查 SomeImportantThing 是否已經被初始化
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
實際上,上面的代碼正常運行,因爲 var
表達式會被提升到全局作用域的頂部。
var SomeImportantThing;
// 其它一些代碼,可能會初始化 SomeImportantThing,也可能不會
// 檢查是否已經被初始化
if (!SomeImportantThing) {
SomeImportantThing = {};
}
譯者注:在 Nettuts+ 網站有一篇介紹 hoisting 的文章,其中的代碼很有啓發性。
// 譯者注:來自 Nettuts+ 的一段代碼,生動的闡述了 JavaScript 中變量聲明提升規則
var myvar = 'my value';
(function() {
alert(myvar); // undefined
var myvar = 'local value';
})();
名稱解析順序
JavaScript 中的所有作用域,包括全局作用域,都有一個特別的名稱
this
指向當前對象。
函數作用域內也有默認的變量
arguments
,其中包含了傳遞到函數中的參數。
比如,當訪問函數內的 foo
變量時,JavaScript 會按照下面順序查找:
- 當前作用域內是否有
var foo
的定義。 - 函數形式參數是否有使用
foo
名稱的。 - 函數自身是否叫做
foo
。 - 回溯到上一級作用域,然後從 #1 重新開始。
注意: 自定義 arguments
參數將會阻止原生的 arguments
對象的創建。
命名空間
只有一個全局作用域導致的常見錯誤是命名衝突。在 JavaScript中,這可以通過 匿名包裝器 輕鬆解決。
(function() {
// 函數創建一個命名空間
window.foo = function() {
// 對外公開的函數,創建了閉包
};
})(); // 立即執行此匿名函數
匿名函數被認爲是 表達式;因此爲了可調用性,它們首先會被執行。
( // 小括號內的函數首先被執行
function() {}
) // 並且返回函數對象
() // 調用上面的執行結果,也就是函數對象
有一些其他的調用函數表達式的方法,比如下面的兩種方式語法不同,但是效果一模一樣。
// 另外兩種方式
+function(){}();
(function(){}());
結論
推薦使用匿名包裝器(譯者注:也就是自執行的匿名函數)來創建命名空間。這樣不僅可以防止命名衝突,而且有利於程序的模塊化。
另外,使用全局變量被認爲是不好的習慣。這樣的代碼傾向於產生錯誤和帶來高的維護成本。
數組
數組遍歷與屬性
雖然在 JavaScript 中數組是是對象,但是沒有好的理由去使用
for in
循環 遍歷數組。相反,有一些好的理由不去使用 for in
遍歷數組。
注意: JavaScript 中數組不是 關聯數組。 JavaScript 中只有對象 來管理鍵值的對應關係。但是關聯數組是保持順序的,而對象不是。
由於 for in
循環會枚舉原型鏈上的所有屬性,唯一過濾這些屬性的方式是使用
hasOwnProperty
函數,因此會比普通的 for
循環慢上好多倍。
遍歷
爲了達到遍歷數組的最佳性能,推薦使用經典的 for
循環。
var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
console.log(list[i]);
}
上面代碼有一個處理,就是通過 l = list.length
來緩存數組的長度。
雖然 length
是數組的一個屬性,但是在每次循環中訪問它還是有性能開銷。 可能最新的 JavaScript 引擎在這點上做了優化,但是我們沒法保證自己的代碼是否運行在這些最近的引擎之上。
實際上,不使用緩存數組長度的方式比緩存版本要慢很多。
length
屬性
length
屬性的 getter 方式會簡單的返回數組的長度,而 setter 方式會截斷數組。
var foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]
foo.length = 6;
foo; // [1, 2, 3]
譯者注: 在 Firebug 中查看此時 foo
的值是: [1, 2, 3, undefined, undefined, undefined]
但是這個結果並不準確,如果你在 Chrome 的控制檯查看
foo
的結果,你會發現是這樣的: [1, 2, 3]
因爲在 JavaScript 中 undefined
是一個變量,注意是變量不是關鍵字,因此上面兩個結果的意義是完全不相同的。
// 譯者注:爲了驗證,我們來執行下面代碼,看序號 5 是否存在於 foo 中。
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 false
foo[5] = undefined;
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 true
爲 length
設置一個更小的值會截斷數組,但是增大 length
屬性值不會對數組產生影響。
結論
爲了更好的性能,推薦使用普通的 for
循環並緩存數組的 length
屬性。使用 for in
遍歷數組被認爲是不好的代碼習慣並傾向於產生錯誤和導致性能問題。
Array
構造函數
由於 Array
的構造函數在如何處理參數時有點模棱兩可,因此總是推薦使用數組的字面語法 - []
- 來創建數組。
[1, 2, 3]; // 結果: [1, 2, 3]
new Array(1, 2, 3); // 結果: [1, 2, 3]
[3]; // 結果: [3]
new Array(3); // 結果: []
new Array('3') // 結果: ['3']
// 譯者注:因此下面的代碼將會使人很迷惑
new Array(3, 4, 5); // 結果: [3, 4, 5]
new Array(3) // 結果: [],此數組長度爲 3
譯者注:這裏的模棱兩可指的是數組的兩種構造函數語法
由於只有一個參數傳遞到構造函數中(譯者注:指的是 new Array(3);
這種調用方式),並且這個參數是數字,構造函數會返回一個
length
屬性被設置爲此參數的空數組。需要特別注意的是,此時只有 length
屬性被設置,真正的數組並沒有生成。
譯者注:在 Firebug 中,你會看到 [undefined, undefined, undefined]
,這其實是不對的。在上一節有詳細的分析。
var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 數組還沒有生成
這種優先於設置數組長度屬性的做法只在少數幾種情況下有用,比如需要循環字符串,可以避免 for
循環的麻煩。
new Array(count + 1).join(stringToRepeat);
譯者注: new Array(3).join('#')
將會返回 ##
結論
應該儘量避免使用數組構造函數創建新數組。推薦使用數組的字面語法。它們更加短小和簡潔,因此增加了代碼的可讀性。
類型
相等與比較
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
上面的表格展示了強制類型轉換,這也是使用 ==
被廣泛認爲是不好編程習慣的主要原因,由於它的複雜轉換規則,會導致難以跟蹤的問題。
此外,強制類型轉換也會帶來性能消耗,比如一個字符串爲了和一個數字進行比較,必須事先被強制轉換爲數字。
嚴格等於操作符
嚴格等於操作符由三個等號組成:===
不像普通的等於操作符,嚴格等於操作符不會進行強制類型轉換。
"" === "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
上面的結果更加清晰並有利於代碼的分析。如果兩個操作數類型不同就肯定不相等也有助於性能的提升。
比較對象
雖然 ==
和 ===
操作符都是等於操作符,但是當其中有一個操作數爲對象時,行爲就不同了。
{} === {}; // false
new String('foo') === 'foo'; // false
new Number(10) === 10; // false
var foo = {};
foo === foo; // true
這裏等於操作符比較的不是值是否相等,而是是否屬於同一個身份;也就是說,只有對象的同一個實例才被認爲是相等的。這有點像 Python 中的
is
和 C 中的指針比較。
結論
強烈推薦使用嚴格等於操作符。如果類型需要轉換,應該在比較之前顯式的轉換,而不是使用語言本身複雜的強制轉換規則。
typeof
操作符
typeof
操作符(和
instanceof
一起)或許是 JavaScript 中最大的設計缺陷,因爲幾乎不可能從它們那裏得到想要的結果。
儘管 instanceof
還有一些極少數的應用場景,typeof
只有一個實際的應用(譯者注:這個實際應用是用來檢測一個對象是否已經定義或者是否已經賦值),而這個應用卻不是用來檢查對象的類型。
注意: 由於 typeof
也可以像函數的語法被調用,比如 typeof(obj)
,但這並是一個函數調用。 那兩個小括號只是用來計算一個表達式的值,這個返回值會作爲
typeof
操作符的一個操作數。 實際上不存在名爲 typeof
的函數。
JavaScript 類型表格
Value Class Type
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function in Nitro/V8)
new RegExp("meow") RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object
上面表格中,Type 一列表示 typeof
操作符的運算結果。可以看到,這個值在大多數情況下都返回 "object"。
Class 一列表示對象的內部屬性 [[Class]]
的值。
JavaScript 標準文檔中定義: [[Class]]
的值只可能是下面字符串中的一個:
Arguments
, Array
, Boolean
, Date
,
Error
, Function
, JSON
, Math
,
Number
, Object
, RegExp
, String
.
爲了獲取對象的 [[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]"
ES5 提示: 在 ECMAScript 5 中,爲了方便,對 null
和 undefined
調用
Object.prototype.toString
方法, 其返回值由 Object
變成了 Null
和
Undefined
。
譯者注:這種變化可以從 IE8 和 Firefox 4 中看出區別,如下所示:
// IE8
Object.prototype.toString.call(null) // "[object Object]"
Object.prototype.toString.call(undefined) // "[object Object]"
// Firefox 4
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
測試爲定義變量
typeof foo !== 'undefined'
上面代碼會檢測 foo
是否已經定義;如果沒有定義而直接使用會導致 ReferenceError
的異常。這是
typeof
唯一有用的地方。
結論
爲了檢測一個對象的類型,強烈推薦使用 Object.prototype.toString
方法;因爲這是唯一一個可依賴的方式。正如上面表格所示,typeof
的一些返回值在標準文檔中並未定義,因此不同的引擎實現可能不同。
除非爲了檢測一個變量是否已經定義,我們應儘量避免使用 typeof
操作符。
instanceof
操作符
instanceof
操作符用來比較兩個操作數的構造函數。只有在比較自定義的對象時纔有意義。如果用來比較內置類型,將會和
typeof
操作符 一樣用處不大。
比較自定義對象
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true
// 如果僅僅設置 Bar.prototype 爲函數 Foo 本身,而不是 Foo 構造函數的一個實例
Bar.prototype = Foo;
new Bar() instanceof Foo; // false
instanceof
比較內置類型
new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true
'foo' instanceof String; // false
'foo' instanceof Object; // false
有一點需要注意,instanceof
用來比較屬於不同 JavaScript 上下文的對象(比如,瀏覽器中不同的文檔結構)時將會出錯,因爲它們的構造函數不會是同一個對象。
結論
instanceof
操作符應該僅僅用來比較來自同一個 JavaScript 上下文的自定義對象。正如
typeof
操作符一樣,任何其它的用法都應該是避免的。
類型轉換
JavaScript 是弱類型語言,所以會在任何可能的情況下應用強制類型轉換。
// 下面的比較結果是:true
new Number(10) == 10; // Number.toString() 返回的字符串被再次轉換爲數字
10 == '10'; // 字符串被轉換爲數字
10 == '+10 '; // 同上
10 == '010'; // 同上
isNaN(null) == false; // null 被轉換爲數字 0
// 0 當然不是一個 NaN(譯者注:否定之否定)
// 下面的比較結果是:false
10 == 010;
10 == '-10';
ES5 提示: 以 0
開頭的數字字面值會被作爲八進制數字解析。 而在 ECMAScript 5 嚴格模式下,這個特性被移除了。
爲了避免上面複雜的強制類型轉換,強烈推薦使用嚴格的等於操作符。雖然這可以避免大部分的問題,但 JavaScript 的弱類型系統仍然會導致一些其它問題。
內置類型的構造函數
內置類型(比如 Number
和 String
)的構造函數在被調用時,使用或者不使用 new
的結果完全不同。
new Number(10) === 10; // False, 對象與數字的比較
Number(10) === 10; // True, 數字與數字的比較
new Number(10) + 0 === 10; // True, 由於隱式的類型轉換
使用內置類型 Number
作爲構造函數將會創建一個新的 Number
對象,而在不使用 new
關鍵字的
Number
函數更像是一個數字轉換器。
另外,在比較中引入對象的字面值將會導致更加複雜的強制類型轉換。
最好的選擇是把要比較的值顯式的轉換爲三種可能的類型之一。
轉換爲字符串
'' + 10 === '10'; // true
將一個值加上空字符串可以輕鬆轉換爲字符串類型。
轉換爲數字
+'10' === 10; // true
使用一元的加號操作符,可以把字符串轉換爲數字。
譯者注:字符串轉換爲數字的常用方法:
+'010' === 10
Number('010') === 10
parseInt('010', 10) === 10 // 用來轉換爲整數
+'010.2' === 10.2
Number('010.2') === 10.2
parseInt('010.2', 10) === 10
轉換爲布爾型
通過使用 否 操作符兩次,可以把一個值轉換爲布爾型。
!!'foo'; // true
!!''; // false
!!'0'; // true
!!'1'; // true
!!'-1' // true
!!{}; // true
!!true; // true
核心
爲什麼不要使用 eval
eval
函數會在當前作用域中執行一段 JavaScript 代碼字符串。
var foo = 1;
function test() {
var foo = 2;
eval('foo = 3');
return foo;
}
test(); // 3
foo; // 1
但是 eval
只在被直接調用並且調用函數就是 eval
本身時,纔在當前作用域中執行。
var foo = 1;
function test() {
var foo = 2;
var bar = eval;
bar('foo = 3');
return foo;
}
test(); // 2
foo; // 3
譯者注:上面的代碼等價於在全局作用域中調用
eval
,和下面兩種寫法效果一樣:
// 寫法一:直接調用全局作用域下的 foo 變量
var foo = 1;
function test() {
var foo = 2;
window.foo = 3;
return foo;
}
test(); // 2
foo; // 3
// 寫法二:使用 call 函數修改 eval 執行的上下文爲全局作用域
var foo = 1;
function test() {
var foo = 2;
eval.call(window, 'foo = 3');
return foo;
}
test(); // 2
foo; // 3
在任何情況下我們都應該避免使用 eval
函數。99.9% 使用 eval
的場景都有不使用
eval
的解決方案。
僞裝的 eval
定時函數
setTimeout
和 setInterval
都可以接受字符串作爲它們的第一個參數。這個字符串總是在全局作用域中執行,因此
eval
在這種情況下沒有被直接調用。
安全問題
eval
也存在安全問題,因爲它會執行任意傳給它的代碼,在代碼字符串未知或者是來自一個不信任的源時,絕對不要使用
eval
函數。
結論
絕對不要使用 eval
,任何使用它的代碼都會在它的工作方式,性能和安全性方面受到質疑。如果一些情況必須使用到 eval
才能正常工作,首先它的設計會受到質疑,這不應該是首選的解決方案,一個更好的不使用
eval
的解決方案應該得到充分考慮並優先採用。
undefined
和 null
JavaScript 有兩個表示‘空’的值,其中比較有用的是 undefined
。
undefined
的值
undefined
是一個值爲 undefined
的類型。
這個語言也定義了一個全局變量,它的值是 undefined
,這個變量也被稱爲 undefined
。但是這個變量不是一個常量,也不是一個關鍵字。這意味着它的值可以輕易被覆蓋。
ES5 提示: 在 ECMAScript 5 的嚴格模式下,undefined
不再是 可寫的了。 但是它的名稱仍然可以被隱藏,比如定義一個函數名爲 undefined
。
下面的情況會返回 undefined
值:
- 訪問未修改的全局變量
undefined
。 - 由於沒有定義
return
表達式的函數隱式返回。 return
表達式沒有顯式的返回任何內容。- 訪問不存在的屬性。
- 函數參數沒有被顯式的傳遞值。
- 任何被設置爲
undefined
值的變量。
處理 undefined
值的改變
由於全局變量 undefined
只是保存了 undefined
類型實際值的副本,因此對它賦新值不會改變類型
undefined
的值。
然而,爲了方便其它變量和 undefined
做比較,我們需要事先獲取類型 undefined
的值。
爲了避免可能對 undefined
值的改變,一個常用的技巧是使用一個傳遞到匿名包裝器的額外參數。在調用時,這個參數不會獲取任何值。
var undefined = 123;
(function(something, foo, undefined) {
// 局部作用域裏的 undefined 變量重新獲得了 `undefined` 值
})('Hello World', 42);
另外一種達到相同目的方法是在函數內使用變量聲明。
var undefined = 123;
(function(something, foo) {
var undefined;
...
})('Hello World', 42);
這裏唯一的區別是,在壓縮後並且函數內沒有其它需要使用 var
聲明變量的情況下,這個版本的代碼會多出 4 個字節的代碼。
譯者注:這裏有點繞口,其實很簡單。如果此函數內沒有其它需要聲明的變量,那麼
var
總共 4 個字符(包含一個空白字符) 就是專門爲 undefined
變量準備的,相比上個例子多出了 4 個字節。
null
的用處
JavaScript 中的 undefined
的使用場景類似於其它語言中的 null,實際上 JavaScript 中的
null
是另外一種數據類型。
它在 JavaScript 內部有一些使用場景(比如聲明原型鏈的終結 Foo.prototype = null
),但是大多數情況下都可以使用
undefined
來代替。
自動分號插入
儘管 JavaScript 有 C 的代碼風格,但是它不強制要求在代碼中使用分號,實際上可以省略它們。
JavaScript 不是一個沒有分號的語言,恰恰相反上它需要分號來就解析源代碼。因此 JavaScript 解析器在遇到由於缺少分號導致的解析錯誤時,會自動在源代碼中插入分號。
var foo = function() {
} // 解析錯誤,分號丟失
test()
自動插入分號,解析器重新解析。
var foo = function() {
}; // 沒有錯誤,解析繼續
test()
自動的分號插入被認爲是 JavaScript 語言最大的設計缺陷之一,因爲它能改變代碼的行爲。
工作原理
下面的代碼沒有分號,因此解析器需要自己判斷需要在哪些地方插入分號。
(function(window, undefined) {
function test(options) {
log('testing!')
(options.list || []).forEach(function(i) {
})
options.value.test(
'long string to pass here',
'and another long string to pass'
)
return
{
foo: function() {}
}
}
window.test = test
})(window)
(function(window) {
window.someLibrary = {}
})(window)
下面是解析器"猜測"的結果。
(function(window, undefined) {
function test(options) {
// 沒有插入分號,兩行被合併爲一行
log('testing!')(options.list || []).forEach(function(i) {
}); // <- 插入分號
options.value.test(
'long string to pass here',
'and another long string to pass'
); // <- 插入分號
return; // <- 插入分號, 改變了 return 表達式的行爲
{ // 作爲一個代碼段處理
foo: function() {}
}; // <- 插入分號
}
window.test = test; // <- 插入分號
// 兩行又被合併了
})(window)(function(window) {
window.someLibrary = {}; // <- 插入分號
})(window); //<- 插入分號
注意: JavaScript 不能正確的處理 return
表達式緊跟換行符的情況, 雖然這不能算是自動分號插入的錯誤,但這確實是一種不希望的副作用。
解析器顯著改變了上面代碼的行爲,在另外一些情況下也會做出錯誤的處理。
前置括號
在前置括號的情況下,解析器不會自動插入分號。
log('testing!')
(options.list || []).forEach(function(i) {})
上面代碼被解析器轉換爲一行。
log('testing!')(options.list || []).forEach(function(i) {})
log
函數的執行結果極大可能不是函數;這種情況下就會出現
TypeError
的錯誤,詳細錯誤信息可能是 undefined is not a function
。
結論
建議絕對不要省略分號,同時也提倡將花括號和相應的表達式放在一行,對於只有一行代碼的 if
或者
else
表達式,也不應該省略花括號。這些良好的編程習慣不僅可以提到代碼的一致性,而且可以防止解析器改變代碼行爲的錯誤處理。
其它
setTimeout
和 setInterval
由於 JavaScript 是異步的,可以使用 setTimeout
和 setInterval
來計劃執行函數。
注意: 定時處理不是 ECMAScript 的標準,它們在 DOM (文檔對象模型) 被實現。
function foo() {}
var id = setTimeout(foo, 1000); // 返回一個大於零的數字
當 setTimeout
被調用時,它會返回一個 ID 標識並且計劃在將來大約 1000 毫秒後調用
foo
函數。 foo
函數只會被執行一次。
基於 JavaScript 引擎的計時策略,以及本質上的單線程運行方式,所以其它代碼的運行可能會阻塞此線程。因此沒法確保函數會在
setTimeout
指定的時刻被調用。
作爲第一個參數的函數將會在全局作用域中執行,因此函數內的
this
將會指向這個全局對象。
function Foo() {
this.value = 42;
this.method = function() {
// this 指向全局對象
console.log(this.value); // 輸出:undefined
};
setTimeout(this.method, 500);
}
new Foo();
注意: setTimeout
的第一個參數是函數對象,一個常犯的錯誤是這樣的
setTimeout(foo(), 1000)
, 這裏回調函數是 foo
的返回值,而不是foo
本身。 大部分情況下,這是一個潛在的錯誤,因爲如果函數返回
undefined
,setTimeout
也不會報錯。
setInterval
的堆調用
setTimeout
只會執行回調函數一次,不過 setInterval
- 正如名字建議的 - 會每隔
X
毫秒執行函數一次。但是卻不鼓勵使用這個函數。
當回調函數的執行被阻塞時,setInterval
仍然會發布更多的回調指令。在很小的定時間隔情況下,這會導致回調函數被堆積起來。
function foo(){
// 阻塞執行 1 秒
}
setInterval(foo, 1000);
上面代碼中,foo
會執行一次隨後被阻塞了一分鐘。
在 foo
被阻塞的時候,setInterval
仍然在組織將來對回調函數的調用。因此,當第一次
foo
函數調用結束時,已經有 10 次函數調用在等待執行。
處理可能的阻塞調用
最簡單也是最容易控制的方案,是在回調函數內部使用 setTimeout
函數。
function foo(){
// 阻塞執行 1 秒
setTimeout(foo, 1000);
}
foo();
這樣不僅封裝了 setTimeout
回調函數,而且阻止了調用指令的堆積,可以有更多的控制。 foo
函數現在可以控制是否繼續執行還是終止執行。
手工清空定時器
可以通過將定時時產生的 ID 標識傳遞給 clearTimeout
或者 clearInterval
函數來清除定時,至於使用哪個函數取決於調用的時候使用的是
setTimeout
還是 setInterval
。
var id = setTimeout(foo, 1000);
clearTimeout(id);
清除所有定時器
由於沒有內置的清除所有定時器的方法,可以採用一種暴力的方式來達到這一目的。
// 清空"所有"的定時器
for(var i = 1; i < 1000; i++) {
clearTimeout(i);
}
可能還有些定時器不會在上面代碼中被清除(譯者注:如果定時器調用時返回的 ID 值大於 1000),因此我們可以事先保存所有的定時器 ID,然後一把清除。
隱藏使用 eval
setTimeout
和 setInterval
也接受第一個參數爲字符串的情況。這個特性絕對不要使用,因爲它在內部使用了
eval
。
注意: 由於定時器函數不是 ECMAScript 的標準,如何解析字符串參數在不同的 JavaScript 引擎實現中可能不同。 事實上,微軟的 JScript 會使用
Function
構造函數來代替 eval
的使用。
function foo() {
// 將會被調用
}
function bar() {
function foo() {
// 不會被調用
}
setTimeout('foo()', 1000);
}
bar();
由於 eval
在這種情況下不是被直接調用,因此傳遞到
setTimeout
的字符串會自全局作用域中執行;因此,上面的回調函數使用的不是定義在 bar
作用域中的局部變量
foo
。
建議不要在調用定時器函數時,爲了向回調函數傳遞參數而使用字符串的形式。
function foo(a, b, c) {}
// 不要這樣做
setTimeout('foo(1,2, 3)', 1000)
// 可以使用匿名函數完成相同功能
setTimeout(function() {
foo(a, b, c);
}, 1000)
注意: 雖然也可以使用這樣的語法 setTimeout(foo, 1000, a, b, c)
, 但是不推薦這麼做,因爲在使用對象的屬性方法時可能會出錯。 (譯者注:這裏說的是屬性方法內,this
的指向錯誤)
結論
絕對不要使用字符串作爲 setTimeout
或者 setInterval
的第一個參數,這麼寫的代碼明顯質量很差。當需要向回調函數傳遞參數時,可以創建一個匿名函數,在函數內執行真實的回調函數。
另外,應該避免使用 setInterval
,因爲它的定時執行不會被 JavaScript 阻塞。
Copyright © 2011. Built with Node.js using a jade template. Hosted by Cramer Development.