首頁 >web前端 >js教程 >JavaScript閉包其二:閉包的實現

JavaScript閉包其二:閉包的實現

黄舟
黄舟原創
2016-12-20 16:07:47817瀏覽

討論完理論部分,接下來讓我們來介紹下ECMAScript中閉包究竟是如何實現的。這裡還是有必要再次強調下:ECMAScript只使用靜態(詞法)作用域(而諸如Perl這樣的語言,既可以使用靜態作用域也可以使用動態作用域進行變數聲明)。

var x = 10;
 
function foo() {
  alert(x);
}
 
(function (funArg) {
 
  var x = 20;
 
  // 变量"x"在(lexical)上下文中静态保存的,在该函数创建的时候就保存了
  funArg(); // 10, 而不是20
 
})(foo);

技術上說,創建該函數的父級上下文的資料是保存在函數的內部屬性 [[Scope]]中的。如果你還不了解什麼是[[Scope]],建議你先閱讀前面的章節,該章節對[[Scope]]作了非常詳細的介紹。如果你對[[Scope]]和作用域鏈的知識完全理解了的話,那對閉包也就完全理解了。

根據函數創建的演算法,我們看到在ECMAScript中,所有的函數都是閉包,因為它們都是在創建的時候就保存了上層上下文的作用域鏈(除開異常的情況) (不管這個函數後續是否會啟動- [[​​Scope]]在函數創建的時候就有了):

var x = 10;
 
function foo() {
  alert(x);
}
 
// foo是闭包
foo: <FunctionObject> = {
  [[Call]]: <code block of foo>,
  [[Scope]]: [
    global: {
      x: 10
    }
  ],
  ... // 其它属性
};

如我們所說,為了優化目的,當一個函數沒有使用自由變數的話,實作可能不會保存在副作用域鏈裡。不過,在ECMA-262-3規範裡任何都沒說。因此,正常來說,所有的參數都是在創建階段保存在[[Scope]]屬性裡的。

有些實作中,允許對閉包作用域直接進行存取。例如Rhino,針對函數的[[Scope]]屬性,對應有一個非標準的__parent__屬性:

var global = this;
var x = 10;
 
var foo = (function () {
 
  var y = 20;
 
  return function () {
    alert(y);
  };
 
})();
 
foo(); // 20
alert(foo.__parent__.y); // 20
 
foo.__parent__.y = 30;
foo(); // 30
 
// 可以通过作用域链移动到顶部
alert(foo.__parent__.__parent__ === global); // true
alert(foo.__parent__.__parent__.x); // 10

所有物件都引用一個[[Scope]]

這裡還要注意的是:在ECMAScript中,同一個父上下文中建立的閉包是共用一個[[Scope]]屬性的。也就是說,某個閉包對其中[[Scope]]的變數做修改會影響到其他閉包對其變數的讀取:

var firstClosure;
var secondClosure;
 
function foo() {
 
  var x = 1;
 
  firstClosure = function () { return ++x; };
  secondClosure = function () { return --x; };
 
  x = 2; // 影响 AO["x"], 在2个闭包公有的[[Scope]]中
 
  alert(firstClosure()); // 3, 通过第一个闭包的[[Scope]]
}
 
foo();
 
alert(firstClosure()); // 4
alert(secondClosure()); // 3

這就是說:所有的內部函數都共享同一個父作用域

關於這個功能有一個非常普遍的錯誤認識,開發人員在循環語句裡創建函數(內部進行計數)的時候經常得不到預期的結果,而期望是每個函數都有自己的值。

var data = [];
 
for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}
 
data[0](); // 3, 而不是0
data[1](); // 3, 而不是1
data[2](); // 3, 而不是2

上述例子證明了 —— 同一個上下文中創建的閉包是共用一個[[Scope]]屬性的。因此上層上下文中的變數「k」是可以很容易就被改變的。

activeContext.Scope = [
  ... // 其它变量对象
  {data: [...], k: 3} // 活动对象
];
 
data[0].[[Scope]] === Scope;
data[1].[[Scope]] === Scope;
data[2].[[Scope]] === Scope;

這樣一來,在函數啟動的時候,最後使用到的k就已經變成了3了。如下所示,建立一個閉包就可以解決這個問題了:

var data = [];
 
for (var k = 0; k < 3; k++) {
  data[k] = (function _helper(x) {
    return function () {
      alert(x);
    };
  })(k); // 传入"k"值
}
 
// 现在结果是正确的了
data[0](); // 0
data[1](); // 1
data[2](); // 2

讓我們來看看上述程式碼都發生了什麼事?函數「_helper」創建出來之後,透過傳入參數「k」啟動。其傳回值也是函數,該函數保存在對應的陣列元素中。這種技術產生瞭如下效果: 在函數啟動時,每次“_helper”都會建立一個新的變數對象,其中含有參數“x”,“x”的值就是傳遞進來的“k”的值。這樣一來,返回的函數的[[Scope]]就成瞭如下所示:

data[0].[[Scope]] === [
  ... // 其它变量对象
  父级上下文中的活动对象AO: {data: [...], k: 3},
  _helper上下文中的活动对象AO: {x: 0}
];
 
data[1].[[Scope]] === [
  ... // 其它变量对象
  父级上下文中的活动对象AO: {data: [...], k: 3},
  _helper上下文中的活动对象AO: {x: 1}
];
 
data[2].[[Scope]] === [
  ... // 其它变量对象
  父级上下文中的活动对象AO: {data: [...], k: 3},
  _helper上下文中的活动对象AO: {x: 2}
];

我們看到,這時函數的[[Scope]]屬性就有了真正想要的值了,為了達到這樣的目的,我們不得不在[[Scope]]中建立額外的變數物件。要注意的是,在傳回的函數中,如果要取得「k」的值,那麼該值還是會是3。

順便提下,大量介紹JavaScript的文章都認為只有額外創建的函數才是閉包,這種說法是錯誤的。實踐得出,這種方式是最有效的,然而,從理論角度來說,在ECMAScript中所有的函數都是閉包。

然而,上述提到的方法並不是唯一的方法。透過其他方式也可以獲得正確的「k」的值,如下所示:

var data = [];
 
for (var k = 0; k < 3; k++) {
  (data[k] = function () {
    alert(arguments.callee.x);
  }).x = k; // 将k作为函数的一个属性
}
 
// 结果也是对的
data[0](); // 0
data[1](); // 1
data[2](); // 2

Funarg和return

另外一個特性是從閉包中傳回。在ECMAScript中,閉包中的回傳語句會將控制流傳回給呼叫上下文(呼叫者)。而在其他語言中,例如,Ruby,有很多中形式的閉包,相應的處理閉包返回也都不同,下面幾種方式都是可能的:可能直接返回給調用者,或者在某些情況下——直接從上下文退出。

ECMAScript標準的退出行為如下:

function getElement() {
 
  [1, 2, 3].forEach(function (element) {
 
    if (element % 2 == 0) {
      // 返回给函数"forEach"函数
      // 而不是返回给getElement函数
      alert(&#39;found: &#39; + element); // found: 2
      return element;
    }
 
  });
 
  return null;
}

然而,在ECMAScript中透過try catch可以實現如下效果:

var $break = {};
 
function getElement() {
 
  try {
 
    [1, 2, 3].forEach(function (element) {
 
      if (element % 2 == 0) {
        // // 从getElement中"返回"
        alert(&#39;found: &#39; + element); // found: 2
        $break.data = element;
        throw $break;
      }
 
    });
 
  } catch (e) {
    if (e == $break) {
      return $break.data;
    }
  }
 
  return null;
}
 
alert(getElement()); // 2

理論版本

這裡說明一下,開發人員經常錯誤將閉包簡化理解成從父上下文版本

🎜這裡說明一下,開發人員經常錯誤將閉包簡化理解成從父上下文版本返回內部函數,甚至理解成只有匿名函數才能是閉包。 🎜

再說一下,因為作用域鏈,使得所有的函數都是閉包(與函數型別無關: 匿名函數,FE,NFE,FD都是閉包)。

這裡只有一類函數除外,那就是透過Function建構器所建立的函數,因為其[[Scope]]只包含全域物件。為了更好的澄清問題,我們對ECMAScript中的閉包給出2個正確的版本定義:

ECMAScript中,閉包指的是:

從理論角度:所有的函數。因為它們都在創建的時候就將上層上下文的資料保存起來了。即使是簡單的全域變數也是如此,因為函數中存取全域變數就等於是在存取自由變量,這個時候使用最外層的作用域。

從實踐角度:以下函數才算是閉包:

即使創建它的上下文已經銷毀,它仍然存在(例如,內部函數從父函數中返回)

在程式碼中引用了自由變數

 以上就是JavaScript閉包其二:閉包的實現的內容,更多相關內容請關注PHP中文網(www.php.cn)! 


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn