1. 程式人生 > >從作用域鏈談閉包

從作用域鏈談閉包

重要 難點 返回 data- call 函數定義 code cee post

閉包(closure)是Javascript語言的一個難點,也是它的特色。非常多高級應用都要依靠閉包實現。

神馬是閉包

關於閉包的概念,是婆說婆有理。

因而,我就翻閱了紅皮書(p178)上對於閉包的陳述:

閉包是指有權訪問另外一個函數作用域中的變量的函數

這概念有點繞,拆分一下。從概念上說,閉包有兩個特點:
1、函數
2、能訪問另外一個函數作用域中的變量
在ES 6之前,Javascript僅僅有函數作用域的概念。沒有塊級作用域(但catch捕獲的異常 僅僅能在catch塊中訪問)的概念(IIFE能夠創建局部作用域)。每一個函數作用域都是封閉的,即外部是訪問不到函數作用域中的變量。

function getName() {
  var name = "美女的名字";
  console.log(name);     //"美女的名字"
}
function displayName() {
    console.log(name);  //報錯
}

可是為了得到美女的名字,不死心的單身汪把代碼改成了這樣:

function getName() {
  var name = "美女的名字";
  function displayName() {
    console.log(name);   
  }
  return displayName;
}
var 美女 = getName();  
美女()  //"美女的名字"

這下,美女是一個閉包了,單身汪想怎麽玩就怎麽玩了。(但並不推薦單身汪用中文做變量名的寫法。大家不要學)。

關於閉包呢。還想再說三點:
1、閉包能夠訪問當前函數以外的變量

function getOuter(){
  var date = ‘815‘;
  function getDate(str){
    console.log(str + date);  //訪問外部的date
  }
  return getDate(‘今天是:‘); //"今天是:815"
}
getOuter();

getDate是一個閉包,該函數運行時,會形成一個作用域A。A中並未定義變量date。但它能在父一級作用域中找到該變量的定義。

2、即使外部函數已經返回。閉包仍能訪問外部函數定義的變量

function getOuter(){
  var date = ‘815‘;
  function getDate(str){
    console.log(str + date);  //訪問外部的date
  }
  return getDate;     //外部函數返回
}
var today = getOuter();
today(‘今天是:‘);   //"今天是:815"
today(‘明天不是:‘);   //"明天不是:815"

3、閉包能夠更新外部變量的值

function updateCount(){
  var count = 0;
  function getCount(val){
    count = val;
    console.log(count);
  }
  return getCount;     //外部函數返回
}
var count = updateCount();
count(815); //815
count(816); //816

作用域鏈
為毛閉包就能訪問外部函數的變量呢?這就要說說Javascript中的作用域鏈了。
Javascript中有一個運行環境(execution context)的概念,它定義了變量或函數有權訪問的其它數據。決定了他們各自的行為。

每一個運行環境都有一個與之關聯的變量對象。環境中定義的全部變量和函數都保存在這個對象中。你能夠把它當做Javascript的一個普通對象。可是你僅僅能改動它的屬性,卻不能引用它。

變量對象也是有父作用域的。

當訪問一個變量時,解釋器會首先在當前作用域查找標示符,假設沒有找到,就去父作用域找。直到找到該變量的標示符或者不再存在父作用域了。這就是作用域鏈。

作用域鏈和原型繼承有點相似,但又有點小差別:假設去查找一個普通對象的屬性時,在當前對象和其原型中都找不到時,會返回undefined。但查找的屬性在作用域鏈中不存在的話就會拋出ReferenceError。

作用域鏈的頂端是全局對象。

對於全局環境中的代碼,作用域鏈僅僅包括一個元素:全局對象。所以。在全局環境中定義變量的時候,它們就會被定義到全局對象中。

當函數被調用的時候,作用域鏈就會包括多個作用域對象。

全局環境
關於作用域鏈講得略多(紅皮書上有關於作用域及運行環境的詳解)。看一個簡單地樣例:

// my_script.js
"use strict";
var foo = 1;
var bar = 2;

在全局環境中,創建了兩個簡單地變量。

如前面所說,此時變量對象是全局對象:
技術分享

運行上述代碼,my_script.js本身會形成一個運行環境。以及它所引用的變量對象。

Non-nested functions

改動一下代碼。創建一個沒有函數嵌套的函數:

"use strict";
var foo = 1;
var bar = 2;
function myFunc() {
  //-- define local-to-function variables
  var a = 1;
  var b = 2;
  var foo = 3;
  console.log("inside myFunc");
}
console.log("outside");
//-- and then, call it:
myFunc();

當myFunc被定義的時候,myFunc的標識符(identifier)就被加到了當前的作用域對象中(在這裏就是全局對象),而且這個標識符所引用的是一個函數對象(function object)。函數對象中所包括的是函數的源碼以及其它的屬性。

當中一個我們所關心的屬性就是內部屬性[[scope]]。[[scope]]所指向的就是當前的作用域對象。也就是指的就是函數的標識符被創建的時候,我們所能夠直接訪問的那個作用域對象(在這裏就是全局對象)。

技術分享

比較重要的一點是:myFunc所引用的函數對象,其本身不僅僅含有函數的代碼,而且還含有指向其被創建的時候的作用域對象

當myFunc函數被調用的時候,一個新的作用域對象被創建了。新的作用域對象中包括myFunc函數所定義的本地變量,以及其參數(arguments)。

這個新的作用域對象的父作用域對象就是在運行myFunc時我們所能直接訪問的那個作用域對象。

所以。當myFunc被運行的時候,對象之間的關系例如以下圖所看到的:

技術分享

Nested functions

如前面所說,當函數返回沒有被引用的時候,就會被垃圾回收器回收。可是對於閉包(函數嵌套是形成閉包的一種簡單方式)呢,即使外部函數返回了,函數對象仍會引用它被創建時的作用域對象。

"use strict";
function createCounter(initial) {
  var counter = initial;
  function increment(value) {
    counter += value;
  }
  function get() {
    return counter;
  }
  return {
    increment: increment,
    get: get
  };
}
var myCounter = createCounter(100);
console.log(myCounter.get());   // 返回 100
myCounter.increment(5);
console.log(myCounter.get());   // 返回 105

當調用createCounter(100)時,對象之間的關系例如以下圖所看到的:

技術分享

內嵌函數increment和get都有指向createCounter(100) scope的引用。

假設createCounter(100)沒有不論什麽返回值,那麽createCounter(100) scope不再被引用。於是就能夠被垃圾回收。

可是由於createCounter(100)實際上是有返回值的,而且返回值被存儲在了myCounter中,所以對象之間的引用關系變成了例如以下圖所看到的:

技術分享

須要用點時間思考的是:即使createCounter(100)已經返回,可是其作用域仍在,並能且僅僅能被內聯函數訪問。能夠通過調用myCounter.increment() 或 myCounter.get()來直接訪問createCounter(100)的作用域。

當myCounter.increment() 或 myCounter.get()被調用時。新的作用域對象會被創建。而且該作用域對象的父作用域對象會是當前能夠直接訪問的作用域對象。

此時,引用關系例如以下:

技術分享

當運行到return counter;時,在get()所在的作用域並沒有找到相應的標示符,就會沿著作用域鏈往上找,直到找到變量counter,然後返回該變量。

調用increment(5)則會更有意思:

技術分享

當單獨調用increment(5)時,參數value會存貯在當前的作用域對象。函數要訪問value。能立即在當前作用域找到該變量。可是當函數要訪問counter時,並沒有找到,於是沿著作用域鏈向上查找。在createCounter(100)的作用域找到了相應的標示符。increment()就會改動counter的值。

除此之外。沒有其它方式來改動這個變量。閉包的強大也在於此。能夠存貯私有數據。

Similar function objects, different scope objects

對於上面的counter演示樣例,再說點擴展的事。看代碼:

//myScript.js
"use strict";
function createCounter(initial) {
  /* ... see the code from previous example ... */
}
//-- create counter objects
var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);

myCounter1 和 myCounter2創建之後。關系圖是醬紫的:

技術分享

在上面的樣例中。myCounter1.increment和myCounter2.increment的函數對象擁有著一樣的代碼以及一樣的屬性值(name,length等等),可是它們的[[scope]]指向的是不一樣的作用域對象。

這才有了以下的結果:

var a, b;
a = myCounter1.get();   // a 等於 100
b = myCounter2.get();   // b 等於 200
myCounter1.increment(1);
myCounter1.increment(2);
myCounter2.increment(5);
a = myCounter1.get();   // a 等於 103
b = myCounter2.get();   // b 等於 205

作用域和this

作用域會存儲變量。但this並非作用域的一部分,它取決於函數調用時的方式。

從作用域鏈談閉包