PS : 這是一篇針對javascript garden做的筆記,更多內(nèi)容請查看原文鏈接 : http://bonsaiden.github.io/JavaScript-Garden/zh/
對象
- JavaScript 中所有變量都可以當作對象使用,除了null 和 undefined;
- 2.toString(); // 出錯:SyntaxError,因為解析器試圖將2.作為浮點數(shù)的一部分來解析,可以使用(2).toString()或者2..toString()或2 .toString();
- 使用{}可以創(chuàng)建一個新的對象 ,新的對象繼承自Object.prototype;
- 訪問對象的屬性,可以通過 “.”或者“[]”操作符,刪除屬性則通過delete操作符;
原型
- Javascript使用的是基于prototype原型模型,基于原型鏈的繼承方式;
- 屬性查找:會向上遍歷原型鏈,直到找到為止,找不到則返回undefined;
- 性能:提防原型鏈過長帶來的性能問題,盡量縮短原型鏈、不要擴展內(nèi)置類型的原型鏈;
hasOwnProperty
用途:為了判斷一個對象是否包含自定義屬性而不是原型鏈上的屬性
for in
作用:和 in操作符一樣,for in循環(huán)同樣在查找對象屬性時遍歷原型鏈上的所有屬性。(盡量使用hasOwnProperty來避免原型鏈上的屬性)
函數(shù)
函數(shù)聲明
foo(); // 正常運行,因為foo在代碼運行前已經(jīng)被創(chuàng)建
function foo() {}
函數(shù)賦值與表達式
foo; // 在代碼運行之前,缺省值為'undefined'
foo(); // 出錯:TypeError,因為foo指向的是一個函數(shù)
var foo = function() {};
var foo = function bar() {
bar(); // 正常運行
}
bar(); // 出錯:ReferenceError
this的工作原理
- 當使用 Function.prototype 上的 call 或者 apply方法時,函數(shù)內(nèi)的 this將會被 顯式設置為函數(shù)調(diào)用的第一個參數(shù)。
- 常見誤解
Foo.method = function() {
function test() {
// this 將會被設置為全局對象(譯者注:瀏覽器環(huán)境中也就是 window 對象)
}
test();
}
- 局部變量that
Foo.method = function() {
var that = this;
function test() {
// 使用 that 來指向 Foo 對象
}
test();
}
- this 的晚綁定特性,當 method 被調(diào)用時,this 將會指向 Bar 的實例對象。
function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
閉包和引用
閉包是 JavaScript 一個非常重要的特性,這意味著當前作用域總是能夠訪問外部作用域中的變量。 因為 函數(shù) 是 JavaScript 中唯一擁有自身作用域的結構,因此閉包的創(chuàng)建依賴于函數(shù)。
- 模擬私有變量
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
調(diào)用的方法如下:
var foo = Counter(4);
foo.increment();
foo.get(); // 5
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
}
上面的代碼不會改變定義在 Counter 作用域中的 count 變量的值,因為 foo.hack 沒有 定義在那個作用域內(nèi)。它將會創(chuàng)建或者覆蓋全局變量 count。
- 循環(huán)中的閉包
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
上面的代碼不會輸出數(shù)字 0到 9,而是會輸出數(shù)字 10十次。當 console.log
被調(diào)用的時候,匿名函數(shù)保持對外部變量 i的引用,此時for循環(huán)已經(jīng)結束, i 的值被修改成了 10。為了得到想要的結果,需要在每次循環(huán)中創(chuàng)建變量 i的拷。
- 避免引用錯誤
使用匿名包裝器
外部的匿名函數(shù)會立即執(zhí)行,并把 i 作為它的參數(shù),此時函數(shù)內(nèi) e 變量就擁有了 i 的一個拷貝。
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
arguments對象
JavaScript 中每個函數(shù)內(nèi)都能訪問一個特別變量 arguments。這個變量維護著所有傳遞到這個函數(shù)中的參數(shù)列表。
- 轉換為數(shù)組
Array.prototype.slice.call(arguments);
- 傳遞參數(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 = function() {
var args = Array.prototype.slice.call(arguments);
Foo.prototype.method.apply(args[0], args.slice(1));
};
- 自動更新
arguments 對象為其內(nèi)部屬性以及函數(shù)形式參數(shù)創(chuàng)建 getter 和 setter 方法。
因此,改變形參的值會影響到 arguments 對象的值,反之亦然。 - 性能
arguments 的 getters 和 setters 方法總會被創(chuàng)建;因此使用 arguments 對性能不會有什么影響。 除非是需要對 arguments 對象的屬性進行多次訪問。
ES5 提示: 這些 getters 和 setters 在嚴格模式下(strict mode)不會被創(chuàng)建。
使用 arguments.callee會顯著的影響現(xiàn)代 JavaScript 引擎的性能。
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 不再是一個單純的內(nèi)聯(lián)函數(shù) inlining(譯者注:這里指的是解析器可以做內(nèi)聯(lián)處理), 因為它需要知道它自己和它的調(diào)用者。 這不僅抵消了內(nèi)聯(lián)函數(shù)帶來的性能提升,而且破壞了封裝,因此現(xiàn)在函數(shù)可能要依賴于特定的上下文。
因此強烈建議大家不要使用 arguments.callee 和它的屬性。
構造函數(shù)
在構造函數(shù)內(nèi)部 - 也就是被調(diào)用的函數(shù)內(nèi) - this 指向新創(chuàng)建的對象 Object。 這個新創(chuàng)建的對象的 prototype 被指向到構造函數(shù)的 prototype。
如果被調(diào)用的函數(shù)沒有顯式的 return 表達式,則隱式的會返回 this 對象 - 也就是新創(chuàng)建的對象。
顯式的 return 表達式將會影響返回結果,但僅限于返回的是一個對象。
為了創(chuàng)建新對象,我們可以創(chuàng)建一個工廠方法,并且在方法內(nèi)構造一個新對象。不好的地方:
- 會占用更多的內(nèi)存,因為新創(chuàng)建的對象不能共享原型上的方法。
- 為了實現(xiàn)繼承,工廠方法需要從另外一個對象拷貝所有屬性,或者把一個對象作為新創(chuàng)建對象的原型。
- 放棄原型鏈僅僅是因為防止遺漏 new 帶來的問題,這似乎和語言本身的思想相違背。
作用域
盡管 JavaScript 支持一對花括號創(chuàng)建的代碼段,但是并不支持塊級作用域; 而僅僅支持 函數(shù)作用域。
每次引用一個變量,JavaScript 會向上遍歷整個作用域直到找到這個變量為止。 如果到達全局作用域但是這個變量仍未找到,則會拋出 ReferenceError 異常。
變量聲明提升(Hoisting): JavaScript 會提升變量聲明。這意味著 var 表達式和 function 聲明都將會被提升到當前作用域的頂部。
名稱解析順序
JavaScript 中的所有作用域,包括全局作用域,都有一個特別的名稱 this 指向當前對象。
函數(shù)作用域內(nèi)也有默認的變量 arguments,其中包含了傳遞到函數(shù)中的參數(shù)。
比如,當訪問函數(shù)內(nèi)的 foo 變量時,JavaScript 會按照下面順序查找:
- 當前作用域內(nèi)是否有 var foo 的定義。
- 函數(shù)形式參數(shù)是否有使用 foo 名稱的。
- 函數(shù)自身是否叫做 foo。
- 回溯到上一級作用域,然后從 #1 重新開始。
命名空間
只有一個全局作用域?qū)е碌某R婂e誤是命名沖突。在 JavaScript中,這可以通過 匿名包裝器 輕松解決。
(function() {
// 函數(shù)創(chuàng)建一個命名空間
window.foo = function() {
// 對外公開的函數(shù),創(chuàng)建了閉包
};
})(); // 立即執(zhí)行此匿名函數(shù)
匿名函數(shù)被認為是 表達式;因此為了可調(diào)用性,它們首先會被執(zhí)行。
( // 小括號內(nèi)的函數(shù)首先被執(zhí)行
function() {}
) // 并且返回函數(shù)對象
() // 調(diào)用上面的執(zhí)行結果,也就是函數(shù)對象
數(shù)組
數(shù)組遍歷與屬性
由于 for in 循環(huán)會枚舉原型鏈上的所有屬性,唯一過濾這些屬性的方式是使用 hasOwnProperty 函數(shù), 因此會比普通的 for 循環(huán)慢上好多倍。
length 屬性的 getter 方式會簡單的返回數(shù)組的長度,而 setter 方式會截斷數(shù)組。
為了更好的性能,推薦使用普通的 for 循環(huán)并緩存數(shù)組的 length 屬性。 使用 for in 遍歷數(shù)組被認為是不好的代碼習慣并傾向于產(chǎn)生錯誤和導致性能問題。
Array 構造函數(shù)
由于 Array 的構造函數(shù)在如何處理參數(shù)時有點模棱兩可,因此總是推薦使用數(shù)組的字面語法 - [] - 來創(chuàng)建數(shù)組。
new Array(3); 這種調(diào)用方式,并且這個參數(shù)是數(shù)字,構造函數(shù)會返回一個 length 屬性被設置為此參數(shù)的空數(shù)組。 需要特別注意的是,此時只有 length 屬性被設置,真正的數(shù)組并沒有生成。
類型
相等與比較
"" == "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ù)字。
嚴格等于操作符
如果兩個操作數(shù)類型不同就肯定不相等也有助于性能的提升。
{} === {}; // false
強烈推薦使用嚴格等于操作符。如果類型需要轉換,應該在比較之前顯式的轉換, 而不是使用語言本身復雜的強制轉換規(guī)則。
typeof 操作符
盡管 instanceof 還有一些極少數(shù)的應用場景,typeof 只有一個實際的應用(譯者注:這個實際應用是用來檢測一個對象是否已經(jīng)定義或者是否已經(jīng)賦值), 而這個應用卻不是用來檢查對象的類型。
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(2) // "[object Number]"
// 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'
為了檢測一個對象的類型,強烈推薦使用 Object.prototype.toString 方法; 因為這是唯一一個可依賴的方式。正如上面表格所示,typeof 的一些返回值在標準文檔中并未定義, 因此不同的引擎實現(xiàn)可能不同。
除非為了檢測一個變量是否已經(jīng)定義,我們應盡量避免使用 typeof 操作符。
instanceof 操作符
instanceof 操作符用來比較兩個操作數(shù)的構造函數(shù)。只有在比較自定義的對象時才有意義。 如果用來比較內(nèi)置類型,將會和 typeof 操作符 一樣用處不大。
new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true
'foo' instanceof String; // false
'foo' instanceof Object; // false
instanceof 操作符應該僅僅用來比較來自同一個 JavaScript 上下文的自定義對象。 正如 typeof 操作符一樣,任何其它的用法都應該是避免的。
類型轉換
使用內(nèi)置類型 Number 作為構造函數(shù)將會創(chuàng)建一個新的 Number 對象, 而在不使用 new 關鍵字的 Number 函數(shù)更像是一個數(shù)字轉換器。
將一個值加上空字符串可以輕松轉換為字符串類型。
使用一元的加號操作符,可以把字符串轉換為數(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 只在被直接調(diào)用并且調(diào)用函數(shù)就是 eval 本身時,才在當前作用域中執(zhí)行。
var foo = 1;
function test() {
var foo = 2;
var bar = eval;
bar('foo = 3');
return foo;
}
test(); // 2
foo; // 3
上面的代碼等價于在全局作用域中調(diào)用 eval
在任何情況下我們都應該避免使用 eval 函數(shù)。99.9% 使用 eval 的場景都有不使用 eval 的解決方案。
undefined 和 null
undefined 是一個值為 undefined 的類型。
這個語言也定義了一個全局變量,它的值是 undefined,這個變量也被稱為 undefined。 但是這個變量不是一個常量,也不是一個關鍵字。這意味著它的值可以輕易被覆蓋。
- 處理 undefined 值的改變
由于全局變量 undefined 只是保存了 undefined 類型實際值的副本, 因此對它賦新值不會改變類型 undefined 的值。
為了避免可能對 undefined 值的改變,一個常用的技巧是使用一個傳遞到匿名包裝器的額外參數(shù)。 在調(diào)用時,這個參數(shù)不會獲取任何值。
另外一種達到相同目的方法是在函數(shù)內(nèi)使用變量聲明。
null的用處
它在 JavaScript 內(nèi)部有一些使用場景(比如聲明原型鏈的終結 Foo.prototype = null),但是大多數(shù)情況下都可以使用 undefined 來代替。
自動分號插入
JavaScript 不是一個沒有分號的語言,恰恰相反上它需要分號來就解析源代碼。 因此 JavaScript 解析器在遇到由于缺少分號導致的解析錯誤時,會自動在源代碼中插入分號。
建議絕對不要省略分號,同時也提倡將花括號和相應的表達式放在一行, 對于只有一行代碼的 if 或者 else 表達式,也不應該省略花括號。 這些良好的編程習慣不僅可以提到代碼的一致性,而且可以防止解析器改變代碼行為的錯誤處理。
其它
setTimeout 和 setInterval
作為第一個參數(shù)的函數(shù)將會在全局作用域中執(zhí)行,因此函數(shù)內(nèi)的 this 將會指向這個全局對象。
function Foo() {
this.value = 42;
this.method = function() {
// this 指向全局對象
console.log(this.value); // 輸出:undefined
};
setTimeout(this.method, 500);
}
new Foo();
- setInterval 的堆調(diào)用
當回調(diào)函數(shù)的執(zhí)行被阻塞時,setInterval 仍然會發(fā)布更多的回調(diào)指令。在很小的定時間隔情況下,這會導致回調(diào)函數(shù)被堆積起來。
- 隱藏使用 eval
setTimeout 和 setInterval 也接受第一個參數(shù)為字符串的情況。 這個特性絕對不要使用,因為它在內(nèi)部使用了 eval。
function foo() {
// 將會被調(diào)用
}
function bar() {
function foo() {
// 不會被調(diào)用
}
setTimeout('foo()', 1000);
}
bar();
由于 eval 在這種情況下不是被直接調(diào)用,因此傳遞到 setTimeout 的字符串會自全局作用域中執(zhí)行; 因此,上面的回調(diào)函數(shù)使用的不是定義在 bar 作用域中的局部變量 foo。
絕對不要使用字符串作為 setTimeout 或者 setInterval 的第一個參數(shù), 這么寫的代碼明顯質(zhì)量很差。當需要向回調(diào)函數(shù)傳遞參數(shù)時,可以創(chuàng)建一個匿名函數(shù),在函數(shù)內(nèi)執(zhí)行真實的回調(diào)函數(shù)。