你不知道的JavaScript上卷之做用域與閉包·讀書筆記

date: 16.12.8 Thursday

第一章 做用域是什麼

LHS:賦值操做的目標是誰?
好比:閉包

a = 2;

RHS:誰是賦值操做的源頭?
好比:函數

console.log(2);

做用域嵌套:遍歷嵌套做用域鏈的規則:引擎從當前的執行做用域開始查找變量,若是找不到,就向上一級繼續查找。當抵達最外層的全局做用域時,不管是否找到都會中止。
異常:爲何區分LHS和RHS是一件重要的事情?
若是RHS查詢在全部嵌套的做用域中遍尋不到所需的變量,引擎就會拋出ReferenceError異常。
當引擎在執行LHS查詢時,若是在頂層做用域也沒法找到目標變量,全局做用域就會建立一個具備該名稱的變量,並將其返回給引擎。(非嚴格模式下)
若是RHS查詢找到了一個變量,但你嘗試對這個變量的值進行不合理的操做,好比試圖對一個非函數類型的值進行函數調用,或者引用null或undefined類型的值中的屬性,引擎會拋出TypeError.
ReferenceError同做用域判別失敗相關,TypeError則表明做用域判別成功但對結果的操做是非法或不合理的。code

第二章 詞法做用域

  • 詞法做用域對象

詞法做用域就是定義在詞法階段的做用域。詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏來決定的,所以當詞法分析器處理代碼時會保持做用域不變。
做用域查找會在找到第一個匹配的標識符時中止:遮蔽效應。(全局變量可使用window.a來訪問)ip

  • 欺騙詞法內存

eval():能夠對一段包含一個或多個聲明的代碼字符串進行演算,並藉此來修改已經存在的詞法做用域(在運行時)作用域

function foo(str, a){
  eval( str );
  console.log(a,b);
}
var b = 2
foo("var b = 3;",1); //1,3

with關鍵字:本質上是用過講一個對象的引用看成做用域來處理,將對象的屬性看成做用域中的標識符來處理,從而建立了一個新的詞法做用域。開發

function foo(obj) {
  with (obj) {
    a = 2;
  }
}

var o1 = {
  a:3
};
var o2 = {
  b:3
};

foo(o1);
console.log( o1.a ); //2

foo(o2);
console.log( o2.a ); // undefined
console.log(a); //2---很差,a被泄露到全局做用域上了。

第三章 函數做用域和塊做用域

函數做用域的含義指,屬於這個函數的所有變量均可以在整個函數的範圍內使用及複用。
規避衝突:字符串

function foo() {
  function bar(a) {
    i = 3;  //不當心懂了for循環所屬做用域中的i
    console.log( a + i );
  }

  for (var i=0; i<10; i++) {
    bar( i*2 ); //進入死循環。
  }
}
foo();
  • 全局命名空間:當程序加載了多個第三方庫時,若是他們沒有妥善的將內部私有的函數或變量隱藏起來,就很容易產生衝突。回調函數

  • 模塊管理

爲了避免污染做用域,可使用包裝函數來解決這個問題。包裝函數的聲明以(function.. 開始。包裝函數會自動運行,是一個表達式。
IIFE:當即執行函數表達式(Immediately Invoked Function Expression)

var a = 2;
(function foo(){
  var a = 3;
  console.log(a); //3
  })();    //防止了foo這個名稱污染了做用域

console.log(a); //2

匿名函數表達式的利弊

setTimeout( function() {
  console.log("+1s,WTF!")
  },100);

行內函數表達式

setTimeout( function haveName() {
  console.log("+1s,WTF!")
  },100);

塊做用域:幾乎形同虛設,只能靠開發者自覺了。在塊做用域內聲明的變量都會屬於外部做用域。表面上看如此,但若是深刻探究。
用with從對象中建立出的做用域僅在with聲明中而非外部做用域中有效。
try/catch的catch分句會建立一個塊做用域,其聲明的變量僅在catch中有效。
let關鍵字能夠將變量綁定到所在的任意做用域中。let聲明附屬於一個新的做用域而不是當前的函數做用域(也不屬於全局做用域)。

var foo = true;

if (foo) {
  let bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

console.log(bar); //ReferenceError

第四章 提高

先有雞仍是先有蛋的問題:
Demo1:

a = 2;
var a;
console.log(a); //2

Demo2:

console.log(a); //undefined
var a = 2;

事實是先有蛋(聲明)後有雞(賦值)。實際處理以下:
demo1實際:

var a;
a = 2;
console.log(a);

demo2實際:

var a;
console.log(a);
a = 2;

只有聲明自己會被提高,而賦值或者其餘運行邏輯會留在本地。

foo(); //TypeError
var foo = function bar() {
  // ...
};

demo3:

foo(); // TypeError
bar(); //ReferenceError

var foo = function bar(){
  // ...
}

上述代碼提高後實際理解形式:

var foo;
foo();
bar();

foo = function() {
  var bar = ..self..
  //...
}

提高過程函數優先,而後纔是變量:

foo(); //1
var foo;
function foo() {
  console.log(1);
}
foo = function() {
  console.log(2);
}

上述代碼會被理解成如下形式:

function foo() {
  console.log(1);
}
foo();
foo = function() {
  console.log(2);
};

儘管var foo出如今function foo()以前,但它是重複的聲明,所以被忽略。由於函數聲明會被提高到普通變量以前。
聲明自己會被提高,但包括函數表達式的賦值在內的賦值操做並不會提高。

第五章 做用域閉包

當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外進行。

function foo() {
  var a = 2;

  function bar() {
    console.log(a);
  }

  return bar;
}
var baz = foo();
baz(); //2 這就是閉包的效果

函數bar()詞法做用域可以訪問foo()的內部做用域。而後咱們將bar()函數自己看成一個值類型進行傳遞。咱們將bar所引用的函數對象自己看成返回值。
在foo()執行後,其返回值賦值給變量baz並調用baz(),其實是經過不一樣的標識符引用調用了內部的函數bar()。
bar()顯然能夠被正常執行。但在這個例子中,它在本身定義的詞法做用域之外的地方執行。
在foo()執行後,一般會期待foo()的整個內部做用域都被銷燬,由於引擎有垃圾回收器用來釋放再也不使用的內存空間。因爲foo()的內容不會再被使用,因此會被回收。
而閉包的神奇做用是阻止此事發生。事實上內部做用域依舊存在,由於bar()自己在使用。
拜bar()所聲明的位置所賜,它擁有涵蓋foo()內部做用域的閉包,使得該做用域可以一直存活,以供bar()在以後任什麼時候間進行引用。
bar()依然持有對該做用域的引用,而這個引用就叫作閉包。

固然,不管使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時均可以觀察到閉包。

var fn;

function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }
  fn = baz; //將baz分配給全局變量
}

function bar() {
  fn();
}
foo();
bar(); //2

不管經過何種手段將內部函數傳遞到所在的詞法做用域外,它都會持有對原始定義做用域的引用,不管在何處執行這個函數都會使用閉包。
本質上不管什麼時候何地。若是將函數(訪問它們各自的詞法做用域)看成第一級的值類型並處處傳遞,你就會看到閉包在這類函數中的應用。(好比使用了回調函數)

for (var i=1; i<=5; i++) {
  setTimeout(function timer() {
    console.log(i);
    }, i*1000);
}

咱們預期上述代碼依次輸出1,2,3,4,5。實際會輸出五次6。由於輸出顯示的是循環結束時i的值。
由於延遲函數的回調會在循環結束後才執行。根據做用域的工做原理,實際狀況是儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以實際上只有一個i.

修改以下:

for (var i=1; i<=5; i++) {
  (function(j) {
    setTimeout( function timer() {
      console.log(j);
      }, j*1000);
    })(i);
}

再迭代中使用IIFE會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代中都會含有一個具備正確值的變量供咱們訪問。
將塊做用域和閉包聯手後:

for (let i=1; i<=5; i++) {
  setTimeout( function timer() {
    console.log(i);
    }, i*1000);
}

模塊也是利用閉包的一個好方法:

function CoolModule() {
  var something = 'cool';
  var another = [1,2,3];

  function doSomething() {
    console.log( something );
  }

  function doAnother() {
    console.log( another.join("!"));
  }
  return {
    doSomething: doSomething,
    doAnother: doAnother
  };
}

var foo = CoolModule();

foo.doSomething(); //cool
foo.doAnother(); //1!2!3

這就是JavaScript中最經常使用的模塊,doSomething()和doAnother()函數具備涵蓋模塊實例內部做用域的閉包。
總結一下,模塊模式須要兩個必要條件:
1.必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會建立一個新的模塊實例)。
2.封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有做用域中造成閉包,而且能夠訪問或者修改私有的狀態。
也能夠用單例模式來實現,這種狀況適用於只須要一個實例的情景:

var foo = (function CoolModule() {
  var something = 'cool';
  var another = [1,2,3];

  function doSomething() {
    console.log( something );
  }

  function doAnother() {
    console.log( another.join("!"));
  }
  return {
    doSomething: doSomething,
    doAnother: doAnother
  };
})();
foo.doSomething();
foo.doAnother();

模塊模式也能夠接受參數,再也不贅述。

最後總結一下閉包:
當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這是就產生了閉包。

附錄A 動態做用域

JavaScript並不具備動態做用域,它只有詞法做用域。

function foo() {
  console.log(a);
}
function bar() {
  var a = 3;
  foo();
}

var a = 2;

bar();

實際上上述代碼輸出2,由於詞法做用域讓foo()中的a經過RHS引用到了全局做用域中的a,所以會輸出2.若是JavaScript有動態做用域,那麼會輸出3,可是JavaScript並無動態做用域。


第一部分完 感謝做者Kyle Simpson和譯者趙望野,感謝自由和開源世界

相關文章
相關標籤/搜索