国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

JavaScript內部原理系列-閉包(Closures)

gghyoo / 1698人閱讀

摘要:對于采用面向堆棧模型來存儲局部變量的系統(tǒng)而言,就意味著當函數(shù)調用結束后,其局部變量都會從堆棧中移除。因為它也是函數(shù)的局部變量,也會隨著的返回而移除。

概要

本文將介紹一個在JavaScript經常會拿來討論的話題 —— 閉包(closure)。閉包其實已經是個老生常談的話題了; 有大量文章都介紹過閉包的內容(其中不失一些很好的文章,比如,擴展閱讀中Richard Cornford的文章就非常好), 盡管如此,這里還是要試著從理論角度來討論下閉包,看看ECMAScript中的閉包內部究竟是如何工作的。

正如在此前文章中提到的,這些文章都是系列文章,相互之間都是有關聯(lián)的。因此,為了更好的理解本文要介紹的內容, 建議先去閱讀下第四章 - 作用域鏈和 第二章 - 變量對象。

概論

在討論ECMAScript閉包之前,先來介紹下函數(shù)式編程(與ECMA-262-3 標準無關)中一些基本定義。 然而,為了更好的解釋這些定義,這里還是拿ECMAScript來舉例。

眾所周知,在函數(shù)式語言中(ECMAScript也支持這種風格),函數(shù)即是數(shù)據。就比方說,函數(shù)可以保存在變量中,可以當參數(shù)傳遞給其他函數(shù),還可以當返回值返回等等。 這類函數(shù)有特殊的名字和結構。

定義
  

函數(shù)式參數(shù)(“Funarg”) —— 是指值為函數(shù)的參數(shù)。

如下例子:

function exampleFunc(funArg) {
  funArg();
}

exampleFunc(function () {
  alert("funArg");
});

上述例子中funarg的實參是一個傳遞給exampleFunc的匿名函數(shù)。

反過來,接受函數(shù)式參數(shù)的函數(shù)稱為 高階函數(shù)(high-order function 簡稱:HOF)。還可以稱作:函數(shù)式函數(shù) 或者 偏數(shù)理的叫法:操作符函數(shù)。 上述例子中,exampleFunc 就是這樣的函數(shù)。

此前提到的,函數(shù)不僅可以作為參數(shù),還可以作為返回值。這類以函數(shù)為返回值的函數(shù)稱為 _帶函數(shù)值的函數(shù)(functions with functional value or function valued functions)。

(function functionValued() {
  return function () {
    alert("returned function is called");
  };
})()();

可以以正常數(shù)據形式存在的函數(shù)(比方說:當參數(shù)傳遞,接受函數(shù)式參數(shù)或者以函數(shù)值返回)都稱作 第一類函數(shù)(一般說第一類對象)。 在ECMAScript中,所有的函數(shù)都是第一類對象。

接受自己作為參數(shù)的函數(shù),稱為 自應用函數(shù)(auto-applicative function 或者 self-applicative function):

(function selfApplicative(funArg) {

  if (funArg && funArg === selfApplicative) {
    alert("self-applicative");
    return;
  }

  selfApplicative(selfApplicative);

})();

以自己為返回值的函數(shù)稱為 自復制函數(shù)(auto-replicative function 或者 self-replicative function)。 通常,“自復制”這個詞用在文學作品中:

(function selfReplicative() {
  return selfReplicative;
})();

在函數(shù)式參數(shù)中定義的變量,在“funarg”激活時就能夠訪問了(因為存儲上下文數(shù)據的變量對象每次在進入上下文的時候就創(chuàng)建出來了):

function testFn(funArg) {

  // 激活funarg, 本地變量localVar可訪問
  funArg(10); // 20
  funArg(20); // 30

}

testFn(function (arg) {

  var localVar = 10;
  alert(arg + localVar);

});

然而,我們知道(特別在第四章中提到的),在ECMAScript中,函數(shù)是可以封裝在父函數(shù)中的,并可以使用父函數(shù)上下文的變量。 這個特性會引發(fā) funarg問題。

Funarg問題

在面向堆棧的編程語言中,函數(shù)的本地變量都是保存在 堆棧上的, 每當函數(shù)激活的時候,這些變量和函數(shù)參數(shù)都會壓棧到該堆棧上。

當函數(shù)返回的時候,這些參數(shù)又會從堆棧中移除。這種模型對將函數(shù)作為函數(shù)式值使用的時候有很大的限制(比方說,作為返回值從父函數(shù)中返回)。 絕大部分情況下,問題會出現(xiàn)在當函數(shù)有 自由變量的時候。

  

自由變量是指在函數(shù)中使用的,但既不是函數(shù)參數(shù)也不是函數(shù)的局部變量的變量

如下所示:

function testFn() {

  var localVar = 10;

  function innerFn(innerParam) {
    alert(innerParam + localVar);
  }

  return innerFn;
}

var someFn = testFn();
someFn(20); // 30

上述例子中,對于innerFn函數(shù)來說,localVar就屬于自由變量。

對于采用 面向堆棧模型來存儲局部變量的系統(tǒng)而言,就意味著當testFn函數(shù)調用結束后,其局部變量都會從堆棧中移除。 這樣一來,當從外部對innerFn進行函數(shù)調用的時候,就會發(fā)生錯誤(因為localVar變量已經不存在了)。

而且,上述例子在 面向堆棧實現(xiàn)模型中,要想將innerFn以返回值返回根本是不可能的。 因為它也是testFn函數(shù)的局部變量,也會隨著testFn的返回而移除。

還有一個函數(shù)對象問題和當系統(tǒng)采用動態(tài)作用域,函數(shù)作為函數(shù)參數(shù)使用的時候有關。

看如下例子(偽代碼):

var z = 10;

function foo() {
  alert(z);
}

foo(); // 10 – 靜態(tài)作用域和動態(tài)作用域情況下都是

(function () {

  var z = 20;
  foo(); // 10 – 靜態(tài)作用域情況下, 20 – 動態(tài)作用域情況下

})();

// 將foo函數(shù)以參數(shù)傳遞情況也是一樣的

(function (funArg) {

  var z = 30;
  funArg(); // 10 – 靜態(tài)作用域情況下, 30 – 動態(tài)作用域情況下

})(foo);

我們看到,采用動態(tài)作用域,變量(標識符)處理是通過動態(tài)堆棧來管理的。 因此,自由變量是在當前活躍的動態(tài)鏈中查詢的,而不是在函數(shù)創(chuàng)建的時候保存起來的靜態(tài)作用域鏈中查詢的。

這樣就會產生沖突。比方說,即使Z仍然存在(與之前從堆棧中移除變量的例子相反),還是會有這樣一個問題: 在不同的函數(shù)調用中,Z的值到底取哪個呢(從哪個上下文,哪個作用域中查詢)?

上述描述的就是兩類 funarg問題 —— 取決于是否將函數(shù)以返回值返回(第一類問題)以及是否將函數(shù)當函數(shù)參數(shù)使用(第二類問題)。

為了解決上述問題,就引入了 閉包的概念。

閉包
  

閉包是代碼塊和創(chuàng)建該代碼塊的上下文中數(shù)據的結合。

讓我們來看下面這個例子(偽代碼):

var x = 20;

function foo() {
  alert(x); // 自由變量 "x" == 20
}

// foo的閉包
fooClosure = {
  call: foo // 對函數(shù)的引用
  lexicalEnvironment: {x: 20} // 查詢自由變量的上下文
};

上述例子中,“fooClosure”部分是偽代碼。對應的,在ECMAScript中,“foo”函數(shù)已經有了一個內部屬性——創(chuàng)建該函數(shù)上下文的作用域鏈。

這里“l(fā)exical”是不言而喻的,通常是省略的。上述例子中是為了強調在閉包創(chuàng)建的同時,上下文的數(shù)據就會保存起來。 當下次調用該函數(shù)的時候,自由變量就可以在保存的(閉包)上下文中找到了,正如上述代碼所示,變量“z”的值總是10。

定義中我們使用的比較廣義的詞 —— “代碼塊”,然而,通常(在ECMAScript中)會使用我們經常用到的函數(shù)。 當然了,并不是所有對閉包的實現(xiàn)都會將閉包和函數(shù)綁在一起,比方說,在Ruby語言中,閉包就有可能是: 一個程序對象(procedure object), 一個lambda表達式或者是代碼塊。

對于要實現(xiàn)將局部變量在上下文銷毀后仍然保存下來,基于堆棧的實現(xiàn)顯然是不適用的(因為與基于堆棧的結構相矛盾)。 因此在這種情況下,上層作用域的閉包數(shù)據是通過 動態(tài)分配內存的方式來實現(xiàn)的(基于“堆”的實現(xiàn)),配合使用垃圾回收器(garbage collector簡稱GC)和 引用計數(shù)(reference counting)。 這種實現(xiàn)方式比基于堆棧的實現(xiàn)性能要低,然而,任何一種實現(xiàn)總是可以優(yōu)化的: 可以分析函數(shù)是否使用了自由變量,函數(shù)式參數(shù)或者函數(shù)式值,然后根據情況來決定 —— 是將數(shù)據存放在堆棧中還是堆中。

ECMAScript閉包的實現(xiàn)

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

var x = 10;

function foo() {
  alert(x);
}

(function (funArg) {

  var x = 20;

  // funArg的變量 "x" 是靜態(tài)保存的,在該函數(shù)創(chuàng)建的時候就保存了

  funArg(); // 10, 而不是 20

})(foo);

從技術角度來說,創(chuàng)建該函數(shù)的上層上下文的數(shù)據是保存在函數(shù)的內部屬性 [[Scope]]中的。 如果你還不了解什么是[[Scope]],建議你先閱讀第四章, 該章節(jié)對[[Scope]]作了非常詳細的介紹。如果你對[[Scope]]和作用域鏈的知識完全理解了的話,那對閉包也就完全理解了。

根據函數(shù)創(chuàng)建的算法,我們看到 在ECMAScript中,所有的函數(shù)都是閉包,因為它們都是在創(chuàng)建的時候就保存了上層上下文的作用域鏈(除開異常的情況) (不管這個函數(shù)后續(xù)是否會激活 —— [[Scope]]在函數(shù)創(chuàng)建的時候就有了):

var x = 10;

function foo() {
  alert(x);
}

// foo is a closure
foo:  = {
  [[Call]]: ,
  [[Scope]]: [
    global: {
      x: 10
    }
  ],
  ... // other properties
};

正如此前提到過的,出于優(yōu)化的目的,當函數(shù)不使用自由變量的時候,實現(xiàn)層可能就不會保存上層作用域鏈。 然而,ECMAScript-262-3標準中并未對此作任何說明;因此,嚴格來說 —— 所有函數(shù)都會在創(chuàng)建的時候將上層作用域鏈保存在[[Scope]]中。

有些實現(xiàn)中,允許對閉包作用域直接進行訪問。比如Rhino,針對函數(shù)的[[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中,同一個上下文中創(chuàng)建的閉包是共用一個[[Scope]]屬性的。 也就是說,某個閉包對其中的變量做修改會影響到其他閉包對其變量的讀?。?/p>

var firstClosure;
var secondClosure;

function foo() {

  var x = 1;

  firstClosure = function () { return ++x; };
  secondClosure = function () { return --x; };

  x = 2; // 對AO["x"]產生了影響, 其值在兩個閉包的[[Scope]]中

  alert(firstClosure()); // 3, 通過 firstClosure.[[Scope]]
}

foo();

alert(firstClosure()); // 4
alert(secondClosure()); // 3

正因為這個特性,很多人都會犯一個非常常見的錯誤: 當在循環(huán)中創(chuàng)建了函數(shù),然后將循環(huán)的索引值和每個函數(shù)綁定的時候,通常得到的結果不是預期的(預期是希望每個函數(shù)都能夠獲取各自對應的索引值)。

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

上述例子就證明了 —— 同一個上下文中創(chuàng)建的閉包是共用一個[[Scope]]屬性的。因此上層上下文中的變量“k”是可以很容易就被改變的。

如下所示:

activeContext.Scope = [
  ... // higher variable objects
  {data: [...], k: 3} // activation object
];

data[0].[[Scope]] === Scope;
data[1].[[Scope]] === Scope;
data[2].[[Scope]] === Scope;

這樣一來,在函數(shù)激活的時候,最終使用到的k就已經變成了3了。

如下所示,創(chuàng)建一個額外的閉包就可以解決這個問題了:

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = (function _helper(x) {
    return function () {
      alert(x);
    };
  })(k); // 將 "k" 值傳遞進去
}

// 現(xiàn)在就對了
data[0](); // 0
data[1](); // 1
data[2](); // 2

上述例子中,函數(shù)“_helper”創(chuàng)建出來之后,通過參數(shù)“k”激活。其返回值也是個函數(shù),該函數(shù)保存在對應的數(shù)組元素中。 這種技術產生了如下效果: 在函數(shù)激活時,每次“_helper”都會創(chuàng)建一個新的變量對象,其中含有參數(shù)“x”,“x”的值就是傳遞進來的“k”的值。 這樣一來,返回的函數(shù)的[[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}
];

我們看到,這個時候函數(shù)的[[Scope]]屬性就有了真正想要的值了,為了達到這樣的目的,我們不得不在[[Scope]]中創(chuàng)建額外的變量對象。 要注意的是,在返回的函數(shù)中,如果要獲取“k”的值,那么該值還是會是3。

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

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

var data = [];

for (var k = 0; k < 3; k++) {
  (data[k] = function () {
    alert(arguments.callee.x);
  }).x = k; // 將“k”存儲為函數(shù)的一個屬性
}

// 同樣也是可行的
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) {
      // 返回給函數(shù)"forEach",
      // 而不會從getElement函數(shù)返回
      alert("found: " + element); // found: 2
      return element;
    }

  });

  return null;
}

alert(getElement()); // null, 而不是 2

然而,在ECMAScript中通過try catch可以實現(xiàn)如下效果:

var $break = {};

function getElement() {

  try {

    [1, 2, 3].forEach(function (element) {

      if (element % 2 == 0) {
        // 直接從getElement"返回"
        alert("found: " + element); // found: 2
        $break.data = element;
        throw $break;
      }

    });

  } catch (e) {
    if (e == $break) {
      return $break.data;
    }
  }

  return null;
}

alert(getElement()); // 2
理論版本

通常,程序員會錯誤的認為,只有匿名函數(shù)才是閉包。其實并非如此,正如我們所看到的 —— 正是因為作用域鏈,使得所有的函數(shù)都是閉包(與函數(shù)類型無關: 匿名函數(shù),F(xiàn)E,NFE,F(xiàn)D都是閉包), 這里只有一類函數(shù)除外,那就是通過Function構造器創(chuàng)建的函數(shù),因為其[[Scope]]只包含全局對象。 為了更好的澄清該問題,我們對ECMAScript中的閉包作兩個定義(即兩種閉包):

ECMAScript中,閉包指的是:

從理論角度:所有的函數(shù)。因為它們都在創(chuàng)建的時候就將上層上下文的數(shù)據保存起來了。哪怕是簡單的全局變量也是如此,因為函數(shù)中訪問全局變量就相當于是在訪問自由變量,這個時候使用最外層的作用域。

從實踐角度:以下函數(shù)才算是閉包:

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

在代碼中引用了自由變量

閉包實踐

實際使用的時候,閉包可以創(chuàng)建出非常優(yōu)雅的設計,允許對funarg上定義的多種計算方式進行定制。 如下就是數(shù)組排序的例子,它接受一個排序條件函數(shù)作為參數(shù):

[1, 2, 3].sort(function (a, b) {
  ... // 排序條件
});

同樣的例子還有,數(shù)組的map方法(并非所有的實現(xiàn)都支持數(shù)組map方法,SpiderMonkey從1.6版本開始有支持),該方法根據函數(shù)中定義的條件將原數(shù)組映射到一個新的數(shù)組中:

[1, 2, 3].map(function (element) {
  return element * 2;
}); // [2, 4, 6]

使用函數(shù)式參數(shù),可以很方便的實現(xiàn)一個搜索方法,并且可以支持無窮多的搜索條件:

someCollection.find(function (element) {
  return element.someProperty == "searchCondition";
});

還有應用函數(shù),比如常見的forEach方法,將funarg應用到每個數(shù)組元素:

[1, 2, 3].forEach(function (element) {
  if (element % 2 != 0) {
    alert(element);
  }
}); // 1, 3

順便提下,函數(shù)對象的 apply 和 call方法,在函數(shù)式編程中也可以用作應用函數(shù)。 apply和call已經在討論“this”的時候介紹過了;這里,我們將它們看作是應用函數(shù) —— 應用到參數(shù)中的函數(shù)(在apply中是參數(shù)列表,在call中是獨立的參數(shù)):

(function () {
  alert([].join.call(arguments, ";")); // 1;2;3
}).apply(this, [1, 2, 3]);

閉包還有另外一個非常重要的應用 —— 延遲調用:

var a = 10;
setTimeout(function () {
  alert(a); // 10, 一秒鐘后
}, 1000);

也可以用于回調函數(shù):

...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
  // 當數(shù)據就緒的時候,才會調用;
  // 這里,不論是在哪個上下文中創(chuàng)建,變量“x”的值已經存在了
  alert(x); // 10
};
..

還可以用于封裝作用域來隱藏輔助對象:

    var foo = {};

    // initialization
    (function (object) {

      var x = 10;

      object.getX = function _getX() {
        return x;
      };

    })(foo);

    alert(foo.getX()); // get closured "x" – 10
總結

本文介紹了更多關于ECMAScript-262-3的理論知識,而我認為,這些基礎的理論有助于理解ECMAScript中閉包的概念。

擴展閱讀

JavaScript閉包(by Richard Cornford)

Funarg問題

閉包

英文原文
來自前端翻譯小站 趙靜、彭森材

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/85467.html

相關文章

  • 閉包不能吃...

    摘要:什么是閉包閉包是函數(shù)廢話閉包還是一個可以訪問函數(shù)中變量的函數(shù)。每個閉包都是引用自己詞法作用域內的變量。每次調用其中一個計數(shù)器時,通過改變這個變量的值,會改變這個閉包的詞法環(huán)境。然而在一個閉包內對變量的修改,不會影響到另外一個閉包中的變量。 為什么要寫深入理解 筆者并不是大神,只是一個在校的大三學生。開始寫深入理解系列是為了給js的一些重難點知識進行梳理,而不是每次面試之前都將這些知識重...

    stonezhu 評論0 收藏0
  • The Little JavaScript Closures

    摘要:寫在前面本文嘗試模仿的風格,介紹的閉包。本文同時也是我學習閉包的一次總結。注根據這篇文章,事實上所有函數(shù)在創(chuàng)建的時候都會形成閉包。但這種閉包并沒什么趣味,也沒什么特別的用途,所以我們更關注的是由內部函數(shù)形成的閉包。 寫在前面 本文嘗試模仿 The Little Schema 的風格,介紹 JavaScript 的閉包。本文同時也是我學習 JavaScript 閉包的一次總結。歡迎一起討...

    Heier 評論0 收藏0
  • JavaScript基礎系列---閉包及其應用

    摘要:所以,有另一種說法認為閉包是由函數(shù)和與其相關的引用環(huán)境組合而成的實體。所以本文中將以維基百科中的定義為準即在計算機科學中,閉包,又稱詞法閉包或函數(shù)閉包,是引用了自由變量的函數(shù)。 閉包(closure)是JavaScript中一個神秘的概念,許多人都對它難以理解,我也一直處于似懂非懂的狀態(tài),前幾天深入了解了一下執(zhí)行環(huán)境以及作用域鏈,可戳查看詳情,而閉包與作用域及作用域鏈的關系密不可分,所...

    leoperfect 評論0 收藏0
  • JavaScript閉包相關

    摘要:函數(shù)對象可以通過這個作用域鏈相互關聯(lián)起來,如此,函數(shù)體內部的變量都可以保存在函數(shù)的作用域內,這在計算機的文獻中被稱之為閉包。所以按照第二段所說的,嚴格意義上所有的函數(shù)都是閉包。 Like most modern programming languages, JavaScript uses lexical scoping. This means that functions are e...

    sewerganger 評論0 收藏0
  • 理解 JavaScript 閉包

    摘要:如何在初學就理解閉包你需要接著讀下去。這樣定義閉包是函數(shù)和聲明該函數(shù)的詞法環(huán)境的組合。小結閉包在中隨處可見。閉包是中的精華部分,理解它需要具備一定的作用域執(zhí)行棧的知識。 這是本系列的第 4 篇文章。 作為 JS 初學者,第一次接觸閉包的概念是因為寫出了類似下面的代碼: for (var i = 0; i < helpText.length; i++) { var item = he...

    寵來也 評論0 收藏0

發(fā)表評論

0條評論

gghyoo

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<