本文首發在個人我的博客:muyunyun.cn/css
《你不知道的JavaScript》系列叢書給出了不少顛覆以往對JavaScript認知的點, 讀完上卷,受益不淺,因而對其精華的知識點進行了梳理。html
做用域是一套規則,用於肯定在何處以及如何查找變量。程序員
JavaScript是一門編譯語言。在傳統編譯語言的流程中,程序中一段源代碼在執行以前會經歷三個步驟,統稱爲「編譯」。es6
var a = 2;
會被分解爲var、a、=、二、;
做用域 分別與編譯器、引擎進行配合完成代碼的解析編程
對於 var a = 2
這條語句,首先編譯器會將其分爲兩部分,一部分是 var a
,一部分是 a = 2
。編譯器會在編譯期間執行 var a,而後到做用域中去查找 a 變量,若是 a 變量在做用域中尚未聲明,那麼就在做用域中聲明 a 變量,若是 a 變量已經存在,那就忽略 var a 語句。而後編譯器會爲 a = 2 這條語句生成執行代碼,以供引擎執行該賦值操做。因此咱們平時所提到的變量提高,無非就是利用這個先聲明後賦值的原理而已!設計模式
對於 var a = 10
這條賦值語句,其實是爲了查找變量 a, 而且將 10 這個數值賦予它,這就是 LHS
查詢。 對於 console.log(a)
這條語句,其實是爲了查找 a 的值並將其打印出來,這是 RHS
查詢。數組
爲何區分 LHS
和 RHS
是一件重要的事情?
在非嚴格模式下,LHS 調用查找不到變量時會建立一個全局變量,RHS 查找不到變量時會拋出 ReferenceError。 在嚴格模式下,LHS 和 RHS 查找不到變量時都會拋出 ReferenceError。安全
做用域共有兩種主要的工做模型。第一種是最爲廣泛的,被大多數編程語言所採用的詞法做用域( JavaScript 中的做用域就是詞法做用域)。另一種是動態做用域,仍有一些編程語言在使用(好比Bash腳本、Perl中的一些模式等)。閉包
詞法做用域是一套關於引擎如何尋找變量以及會在何處找到變量的規則。詞法做用域最重要的特徵是它的定義過程發生在代碼的書寫階段(假設沒有使用 eval() 或 with )。來看示例代碼:app
function foo() {
console.log(a); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar()複製代碼
詞法做用域讓foo()中的a經過RHS引用到了全局做用域中的a,所以會輸出2。
而動態做用域只關心它們從何處調用。換句話說,做用域鏈是基於調用棧的,而不是代碼中的做用域嵌套。所以,若是 JavaScript 具備動態做用域,理論上,下面代碼中的 foo() 在執行時將會輸出3。
function foo() {
console.log(a); // 3
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar()複製代碼
對於函數表達式一個最熟悉的場景可能就是回調函數了,好比
setTimeout( function() {
console.log("I waited 1 second!")
}, 1000 )複製代碼
這叫做匿名函數表達式
。函數表達式能夠匿名,而函數聲明則不能夠省略函數名。匿名函數表達式書寫起來簡單快捷,不少庫和工具也傾向鼓勵使用這種風格的代碼。但它也有幾個缺點須要考慮。
始終給函數表達式命名是一個最佳實踐:
setTimeout( function timeoutHandler() { // 我有名字了
console.log("I waited 1 second!")
}, 1000 )複製代碼
考慮如下代碼:
a = 2;
var a;
console.log(a); // 2複製代碼
考慮另一段代碼
console.log(a); // undefined
var a = 2;複製代碼
咱們習慣將 var a = 2; 看做一個聲明,而實際上 JavaScript 引擎並不這麼認爲。它將 var a 和 a = 2 看成兩個單獨的聲明,第一個是編譯階段的任務,而第二個是執行階段的任務。
這意味着不管做用域中的聲明出如今什麼地方,都將在代碼自己被執行前首先進行處理。能夠將這個過程形象地想象成全部的聲明(變量和函數)都會被「移動」到各自做用域的最頂端,這個過程稱爲提高。
能夠看出,先有聲明後有賦值。
再來看如下代碼:
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};複製代碼
這個代碼片斷通過提高後,實際上會被理解爲如下形式:
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
};複製代碼
這段程序中的變量標識符 foo() 被提高並分配給全局做用域,所以 foo() 不會致使 ReferenceError。可是 foo 此時並無賦值(若是它是一個函數聲明而不是函數表達式就會賦值
)。foo()因爲對 undefined 值進行函數調用而致使非法操做,所以拋出 TypeError 異常。另外即時是具名的函數表達式,名稱標識符(這裏是 bar )在賦值以前也沒法在所在做用域中使用。
以前寫過關於閉包的一篇文章深刻淺出JavaScript之閉包(Closure)
要說明閉包,for 循環是最多見的例子。
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 )
}複製代碼
正常狀況下,咱們對這段代碼行爲的預期是分別輸出數字 1~5,每秒一次,每次一個。但實際上,這段代碼在運行時會以每秒一次的頻率輸出五次6。
它的缺陷在於:根據做用域的工做原理,儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以實際上只有一個i。所以咱們須要更多的閉包做用域。咱們知道IIFE會經過聲明並當即執行一個函數來建立做用域,咱們來進行改進:
for (var i = 1; i <= 5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log(j);
}, j*1000 )
})();
}複製代碼
還能夠對這段代碼進行一些改進:
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout( function timer() {
console.log(j);
}, j*1000 )
})(i);
}複製代碼
在迭代內使用 IIFE 會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代中都會含有一個具備正確值的變量供咱們訪問。
咱們使用 IIFE 在每次迭代時都建立一個新的做用域。換句話說,每次迭代咱們都須要一個塊做用域。咱們知道 let 聲明能夠用來劫持塊做用域,那咱們能夠進行這樣改:
for (var i = 1; i <= 5; i++) {
let j = i;
setTimeout( function timer() {
console.log(j);
}, j*1000 )
}複製代碼
本質上這是將一個塊轉換成一個能夠被關閉的做用域。
此外,for循環頭部的 let 聲明還會有一個特殊行爲。這個行爲指出每一個迭代都會使用上一個迭代結束時的值來初始化這個變量。
for (let i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 )
}複製代碼
以前寫過一篇深刻淺出JavaScript之this。咱們知道this是在運行時進行綁定的,並非在編寫時綁定,它的上下文取決於函數調用時的各類條件。this的綁定和函數聲明的位置沒有任何關係,只取決於函數的調用方式。
來看下面這段代碼的問題:
var obj = {
id: "awesome",
cool: function coolFn() {
console.log(this.id);
}
};
var id = "not awesome";
obj.cool(); // awesome
setTimeout( obj.cool, 100); // not awesome複製代碼
obj.cool() 與 setTimeout( obj.cool, 100 ) 輸出結果不同的緣由在於 cool() 函數丟失了同 this 之間的綁定。解決方法最經常使用的是 var self = this;
var obj = {
count: 0,
cool: function coolFn() {
var self = this;
if (self.count < 1) {
setTimeout( function timer(){
self.count++;
console.log("awesome?");
}, 100)
}
}
}
obj.cool(); // awesome?複製代碼
這裏用到的知識點是咱們很是熟悉的詞法做用域。self 只是一個能夠經過詞法做用域和閉包進行引用的標識符,不關心 this 綁定的過程當中發生了什麼。
ES6 中的箭頭函數引人了一個叫做 this 詞法的行爲:
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout( () => {
this.count++;
console.log("awesome?");
}, 100)
}
}
}
obj.cool(); // awesome?複製代碼
箭頭函數棄用了全部普通 this 綁定規則,取而代之的是用當前的詞法做用域覆蓋了 this 原本的值。所以,這個代碼片斷中的箭頭函數只是"繼承"了 cool() 函數的 this 綁定。
可是箭頭函數的缺點就是由於其是匿名的,上文已介紹過具名函數比匿名函數更可取的緣由。並且箭頭函數將程序員們常常犯的一個錯誤給標準化了:混淆了 this 綁定規則和詞法做用域規則。
箭頭函數不只僅意味着能夠少寫代碼。本書的做者認爲使用 bind() 是更靠得住的方式。
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout( () => {
this.count++;
console.log("more awesome");
}.bind( this ), 100)
}
}
}
obj.cool(); // more awesome複製代碼
函數在執行的過程當中,能夠根據下面這4條綁定規則來判斷 this 綁定到哪。
fn.apply( obj, arguments )
)書中對4條綁定規則的優先級進行了驗證,得出如下的順序優先級:
若是你把 null 或者 undefined 做爲 this 的綁定對象傳入 call、apply 或者 bind,這些值在調用時會被忽略,實際應用的是默認規則。
何時會傳入 null/undefined 呢?一種很是常見的作法是用 apply(..) 來「展開」一個數組,並看成參數傳入一個函數。相似地,bind(..) 能夠對參數進行柯里化(預先設置一些參數),以下代碼:
function foo(a, b) {
console.log( "a:" + a + ", b:" + b );
}
// 把數組"展開"成參數
foo.apply(null, [2, 3]); // a:2, b:3
// 使用 bind(..) 進行柯里化
var bar = foo.bind( null, 2);
bar(3); // a:2, b:3複製代碼
其中 ES6 中,能夠用 ... 操做符代替 apply(..) 來「展開」數組,可是 ES6 中沒有柯里化的相關語法,所以仍是須要使用 bind(..)。
使用 null 來忽略 this 綁定可能產生一些反作用。若是某個函數(好比第三庫中的某個函數)確實使用了 this ,默認綁定規則會把 this 綁定到全局對象,這將致使不可預計的後果。更安全的作法是傳入一個特殊的對象,一個 「DMZ」 對象,一個空的非委託對象,即 Object.create(null)。
function foo(a, b) {
console.log( "a:" + a + ", b:" + b );
}
var ø = Object.create(null);
// 把數組"展開"成參數
foo.apply( ø, [2, 3]); // a:2, b:3
// 使用 bind(..) 進行柯里化
var bar = foo.bind( ø, 2);
bar(3); // a:2, b:3複製代碼
JavaScript中的對象有字面形式(好比var a = { .. }
)和構造形式(好比var a = new Array(..)
)。字面形式更經常使用,不過有時候構造形式能夠提供更多選擇。
做者認爲「JavaScript中萬物都是對象」的觀點是不對的。由於對象只是 6 個基礎類型( string、number、boolean、null、undefined、object )之一。對象有包括 function 在內的子對象,不一樣子類型具備不一樣的行爲,好比內部標籤 [object Array] 表示這是對象的子類型數組。
思考一下這個對象:
function anotherFunction() { /*..*/ }
var anotherObject = {
c: true
};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // 引用,不是複本!
c: anotherArray, // 另外一個引用!
d: anotherFunction
};
anotherArray.push( myObject )複製代碼
如何準確地表示 myObject 的複製呢?
這裏有一個知識點。
對於 JSON 安全的對象(就是能用 JSON.stringify 序列號的字符串)來講,有一種巧妙的複製方法:
var newObj = JSON.parse( JSON.stringify(someObj) )複製代碼
我認爲這種方法就是深複製。相比於深複製,淺複製很是易懂而且問題要少得多,ES6 定義了 Object.assign(..) 方法來實現淺複製。 Object.assign(..) 方法的第一個參數是目標對象,以後還能夠跟一個或多個源對象。它會遍歷一個或多個源對象的全部可枚舉的自由鍵並把它們複製到目標對象,最後返回目標對象,就像這樣:
var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true複製代碼
JavaScript 有一些近似類的語法元素(好比 new 和 instanceof), 後來的 ES6 中新增了一些如 class 的關鍵字。可是 JavaScript 實際上並無類。類是一種設計模式,JavaScript 的機制其實和類徹底不一樣。
思考下面的代碼:
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();複製代碼
咱們如何找出 a 的「祖先」(委託關係)呢?
a instanceof Foo; // true
(對象 instanceof 函數)Foo.prototype.isPrototypeOf(a); // true
(對象 isPrototypeOf 對象)Object.getPrototypeOf(a) === Foo.prototype; // true
(Object.getPrototypeOf() 能夠獲取一個對象的 [[Prototype]]) 鏈;a.__proto__ == Foo.prototype; // true
來看下面的代碼:
var foo = {
something: function() {
console.log("Tell me something good...");
}
};
var bar = Object.create(foo);
bar.something(); // Tell me something good...複製代碼
Object.create(..)會建立一個新對象 (bar) 並把它關聯到咱們指定的對象 (foo),這樣咱們就能夠充分發揮 [[Prototype]] 機制的爲例(委託)而且避免沒必要要的麻煩 (好比使用 new 的構造函數調用會生成 .prototype 和 .constructor 引用)。
Object.create(null) 會建立一個擁有空連接的對象,這個對象沒法進行委託。因爲這個對象沒有原型鏈,因此 instanceof 操做符沒法進行判斷,所以老是會返回 false 。這些特殊的空對象一般被稱做「字典」,它們徹底不會受到原型鏈的干擾,所以很是適合用來存儲數據。
咱們並不須要類來建立兩個對象之間的關係,只須要經過委託來關聯對象就足夠了。而Object.create(..)不包含任何「類的詭計」,因此它能夠完美地建立咱們想要的關聯關係。
此書的第二章第6部分就把面對類和繼承
和行爲委託
兩種設計模式進行了對比,咱們能夠看到行爲委託是一種更加簡潔的設計模式,在這種設計模式中能感覺到Object.create()
的強大。
來看一段 ES6中Class 的例子
class Widget {
constructor(width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where){
if (this.$elem) {
this.$elem.css({
width: this.width + "px",
height: this.height + "px"
}).appendTo($where);
}
}
}
class Button extends Widget {
constructor(width, height, label) {
super(width, height);
this.label = label || "Default";
this.$elem = $("<button>").text(this.label)
}
render($where) {
super($where);
this.$elem.click(this.onClick.bind(this));
}
onClick(evt) {
console.log("Button '" + this.label + "' clicked!")
}
}複製代碼
除了語法更好看以外,ES6還有如下優勢
可是 class 就是完美的嗎?在傳統面向類的語言中,類定義以後就不會進行修改,因此類的設計模式就不支持修改。但JavaScript 最強大的特性之一就是它的動態性,在使用 class 的有些時候仍是會用到 .prototype 以及碰到 super (指望動態綁定然而靜態綁定) 的問題,class 基本上都沒有提供解決方案。
這也是本書做者但願咱們思考的問題。