国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
JavaScript執(zhí)行環(huán)境+變量對象+作用域鏈+閉包

閉包真的是一個談爛掉的內(nèi)容。說到閉包,自然就涉及到執(zhí)行環(huán)境、變量對象以及作用域鏈。湯姆大叔翻譯的《深入理解JavaScript系列》很好,幫我解決了一直以來似懂非懂的很多問題,包括閉包。下面就給自己總結(jié)一下。包括參考大叔的譯文以及《JavaScript高級程序設(shè)計(第3版)》,一些例子引用自它們。

附上大叔的鏈接:《深入理解JavaScript系列》

一、執(zhí)行環(huán)境(或“執(zhí)行上下文”,意義一樣)

首先說下ECMAScript可執(zhí)行代碼的類型包括:全局代碼、函數(shù)代碼、eval_r()代碼。

每當執(zhí)行流轉(zhuǎn)到可執(zhí)行代碼時,即會進入一個執(zhí)行環(huán)境?;顒拥膱?zhí)行環(huán)境構(gòu)成一個棧:棧的底部始終是全局環(huán)境,頂部是當前活動的執(zhí)行環(huán)境。

全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境。在瀏覽器中,全局環(huán)境就是window對象,因此所有全局變量和函數(shù)都是作為window對象的屬性和方法創(chuàng)建的。

每個函數(shù)都有自己的執(zhí)行環(huán)境。當執(zhí)行流進入一個函數(shù)時,函數(shù)的環(huán)境被推入棧中。而在函數(shù)執(zhí)行之后,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境。某個執(zhí)行環(huán)境中的代碼執(zhí)行完后,該環(huán)境銷毀,保存在其中的所有變量和函數(shù)定義也隨之銷毀。而全局執(zhí)行環(huán)境直到應(yīng)用程序退出才會被銷毀。

eval的執(zhí)行環(huán)境與調(diào)用環(huán)境的執(zhí)行環(huán)境相同。

二、變量對象

我們知道變量和執(zhí)行環(huán)境有著密切的關(guān)系:

var a = 10; // 全局上下文中的變量 (function () {  var b = 20; // function上下文中的局部變量})(); alert(a); // 10alert(b); // 全局變量 "b" 沒有聲明

而且我們也知道在JS里沒有塊級作用域這一說法,ES規(guī)范指出獨立作用域只能通過函數(shù)(function)代碼類型的執(zhí)行環(huán)境創(chuàng)建。也就是說,像for循環(huán)并不能創(chuàng)建一個局部環(huán)境:

for (var k in {a: 1, b: 2}) {  alert(k);} alert(k); // 盡管循環(huán)已經(jīng)結(jié)束但變量k依然在當前作用域

既然變量與執(zhí)行環(huán)境相關(guān),那變量自己應(yīng)該知道它的數(shù)據(jù)存放在哪里,并知道如何訪問。這就引出了“變量對象”這個概念。

每個執(zhí)行環(huán)境都有一個與之關(guān)聯(lián)的變量對象,這個對象存儲著在環(huán)境中定義的以下內(nèi)容:

1. 函數(shù)的形參
2. var聲明的變量
3. 函數(shù)聲明(不包括函數(shù)表達式)

舉例來說,用一個普通對象來表示變量對象,它是執(zhí)行環(huán)境的一個屬性:

執(zhí)行環(huán)境 = {   變量對象:{       //環(huán)境中的數(shù)據(jù)    }};        

例如:

var a = 10;function test(x) {  var b = 20;};test(30);

對應(yīng)的變量對象為:

// 全局執(zhí)行環(huán)境的變量對象全局環(huán)境的變量對象= {  a: 10,  test: 指向test()函數(shù)}; // test函數(shù)執(zhí)行環(huán)境的變量對象test函數(shù)環(huán)境的變量對象 = {  x: 30,  b: 20};

 那么,不同執(zhí)行環(huán)境中的變量對象的初始化是怎樣的呢?下面詳細看一下:

全局環(huán)境中的變量對象

先看下全局對象的明確定義:

全局對象 是在進入任何執(zhí)行環(huán)境之前就已經(jīng)創(chuàng)建了的對象。
這個對象只存在一份,它的屬性在程序中的任何地方都可以訪問,全局對象的生命周期終止于程序退出那一刻。

全局對象初始創(chuàng)建階段,將Math、String等作為自身屬性,初始化如下:

globla = {   Math:   String:   ...   ...   window:globla   //引用自身};

在這里,變量對象就是全局對象自己。

函數(shù)環(huán)境中的變量對象

在函數(shù)執(zhí)行環(huán)境中,“活動對象”扮演著變量對象這個角色?;顒訉ο笫窃谶M入函數(shù)執(zhí)行環(huán)境時創(chuàng)建的,它通過函數(shù)的arguments屬性初始化:

活動對象 = {   arguments:  //是個對象,包括callee、length等屬性 }; 

理解了變量對象的初始化之后,下面就是關(guān)于變量對象的核心了。

環(huán)境中的代碼,被分為兩個階段來處理:進入執(zhí)行環(huán)境 、執(zhí)行代碼。變量對象的修改變化與這兩個階段緊密相關(guān)。

這2個階段的處理是一般行為,和環(huán)境的類型無關(guān)(即,在全局環(huán)境和函數(shù)環(huán)境中的表現(xiàn)是一樣的)。

①進入環(huán)境

當進入執(zhí)行環(huán)境時(代碼執(zhí)行之前),變量對象已包含下列屬性(上面有提到):

①函數(shù)的所有形參(如果是在函數(shù)執(zhí)行環(huán)境中。因為全局環(huán)境沒有形參。)
————由 形參名稱 和 對應(yīng)值 組成,作為變量對象的屬性。如果沒有傳遞對應(yīng)的參數(shù),將undefined作為對應(yīng)值。

②所有函數(shù)聲明(注意是聲明,函數(shù)表達式不算。)
————由 函數(shù)名 和 對應(yīng)值(函數(shù)對象)組成,作為變量對象的屬性。如果變量對象已經(jīng)存在同名的屬性,則覆蓋這個屬性。

③所有變量聲明(由var聲明的變量)
————由 變量名 和 對應(yīng)值(undefined) 組成,作為變量對象的屬性。如果變量名與已經(jīng)聲明的形參或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性。
————注意:此時的對應(yīng)值是undefined。

讓我們來看一個例子:

function test(a, b) {  alert(c);    //undefined  alert(d);   //function d() {}  alert(e);   //undefined  alert(x);   //出錯  var c = 10;  function d() {}  var e = function _e() {};  (function x() {});} test(10); //

 我們考慮當進入帶有參數(shù)10的test函數(shù)環(huán)境時(代碼執(zhí)行之前),活動對象表現(xiàn)如下:

活動對象(test) = {  a: 10,  b: undefined,    c: undefined,  d:指向函數(shù)d,  e: undefined};

注意,活動對象里不包含函數(shù)x。這是因為x是一個函數(shù)表達式而不是函數(shù)聲明,函數(shù)表達式不會影響變量對象(在這里是活動對象)。函數(shù)_e同樣是函數(shù)表達式,但是我們注意到它分配給了變量e,所以可以通過名稱e來訪問。

在這之后,將進入處理代碼的第二個階段:執(zhí)行代碼。

②執(zhí)行代碼

這個階段內(nèi),變量/活動對象已經(jīng)擁有了屬性(不過,并不是所有屬性都有值,就像上面那個例子,大部分屬性的值還是系統(tǒng)默認的undefined)。

繼續(xù)上面那個例子,活動對象在“執(zhí)行代碼”這個階段被修改如下():

AO(test) = {  a: 10,  b: undefined,  //沒有相應(yīng)該參數(shù)傳入,undefined  c: 10,    //之前是undefined  d: 指向函數(shù)d,  e: 指向函數(shù)表達式_e   //之前是undefined};

注意此時,函數(shù)表達式_e保存到了已聲明的變量e上,但函數(shù)表達式"x"本身不存在于活動對象中,也就是說,如果嘗試調(diào)用函數(shù)"x",無論在函數(shù)定義之前或之后,都會出現(xiàn)       “x is not defined”的錯誤。

理解了以上內(nèi)容之后,再來看一個例子:

alert(x); // function var x = 10;alert(x); // 10 x = 20; function x() {};alert(x); // 20

為什么第一個alert(x)的值是function,而且它還是在x聲明之前訪問的x?為什么不是10或20呢?

現(xiàn)在我們知道,函數(shù)聲明是在進入環(huán)境時填入活動對象的,同一時間,還有一個變量聲明'x',但是正如前面所說,變量聲明在順序上跟在函數(shù)聲明和形參聲明之后。即,在進入環(huán)境階段,變量聲明不會干擾變量對象中已經(jīng)存在的同名函數(shù)或形參聲明。所以,就這個例子來說,在進入環(huán)境時,變量對象的結(jié)構(gòu)如下:

變量對象 = {    x:指向函數(shù)x    //如果function x沒有已經(jīng)聲明的話,這時的x應(yīng)該是undefined};

緊接著,在代碼執(zhí)行階段,變量對象作如下修改:

變量對象['x'] = 10;變量對象['x'] = 20;//可以在第二、三個alert看到這個結(jié)果

再看一個例子:

if (true) {   var a = 1;   } else {   var b = 2;}//變量是在進入環(huán)境階段放入變量對象的,雖然else部分永遠不會執(zhí)行,//但是不管怎樣,變量b仍然存在于變量對象中。alert(a); //1alert(b); //undefined,不是b未聲明,而是b的值是undefined

另外,關(guān)于var聲明變量和不用var聲明:

大叔的譯文中指出:任何時候,變量只能通過var關(guān)鍵字才能聲明。

像a =10;這僅僅是給全局對象創(chuàng)建了一個新屬性(但它不是變量)。它之所以能成為全局對象的屬性,完全是因為全局對象===全局變量對象??蠢樱?/p>

alert(a); // undefinedalert(b); // "b" 沒有聲明,出錯 b = 10;var a = 20;

進入環(huán)境階段:

變量對象 = {  a: undefined};

可以看到,因為b不是一個變量,所以在這個階段根本就沒有b,b將只在代碼執(zhí)行階段才會出現(xiàn),但在這里,還未執(zhí)行到那就出錯了。

還有一個要注意的:var聲明的變量,相對于屬性(如a = 10;或window.a =10;),變量的[[Configurable]]特性值為false,即不能通過delete刪除,而屬性則可以。

三、作用域鏈

現(xiàn)在我們已經(jīng)知道,一個執(zhí)行環(huán)境的數(shù)據(jù)(變量、函數(shù)聲明和函數(shù)形參)作為屬性存儲在變量對象中。

同時也知道,變量對象在每次進入環(huán)境時創(chuàng)建,并填入初始值,值的更新出現(xiàn)在代碼執(zhí)行階段。

下面的內(nèi)容討論作用域鏈。

如果要簡要地描述并展示其重點,那么作用域鏈大多數(shù)與內(nèi)部函數(shù)相關(guān)。

我們可以創(chuàng)建內(nèi)部函數(shù),甚至能從父函數(shù)中返回這些函數(shù)。

var x = 10; function foo() {   var y = 20;   function bar() {    alert(x + y);  }   return bar; } foo()(); // 30

很明顯每個環(huán)境擁有自己的變量對象:對于全局環(huán)境,它是全局對象自身;對于函數(shù),它是活動對象。

作用域鏈正是內(nèi)部環(huán)境所有變量對象(包括父變量對象)的列表。此鏈用來在標識符解析中變量查找。

作用域鏈本質(zhì)上,是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。

對于上面這個例子,bar執(zhí)行環(huán)境中的作用域鏈包括:bar變量對象、foo變量對象、全局變量對象。

函數(shù)執(zhí)行環(huán)境中的作用域鏈在函數(shù)調(diào)用時創(chuàng)建,包含這個函數(shù)的活動對象和函數(shù)的[[scope]]屬性。示例如下:

活動的執(zhí)行環(huán)境 = {    變量對象: {...}, // or 活動對象    this: thisValue,    Scope: [ // 作用域鏈      // 它是所有變量對象的列表。    ]};

其中的Scope定義為:Scope = 被調(diào)用函數(shù)的活動對象 + [[scope]]。

這種標識符的解析過程,與函數(shù)的生命周期相關(guān),下面詳細討論。

(1)函數(shù)的生命周期

函數(shù)的生命周期分為創(chuàng)建和激活(調(diào)用時)兩個階段。

函數(shù)創(chuàng)建

讓我們先看看在全局環(huán)境中的變量和函數(shù)聲明(這里的變量對象就是全局對象自身,我們懂的。)

var x = 10; function foo() {  var y = 20;  alert(x + y);} foo(); // 30

函數(shù)激活時,得到了正確的也是預(yù)期中的結(jié)果。但我們注意到,變量y在函數(shù)foo中定義(意味著它在foo的活動對象中),但是x并未在foo環(huán)境中定義,相應(yīng)地,它不會添加到foo的活動對象中。那么,foo是如何訪問到變量x的?其實我們大都知道函數(shù)能訪問更高一層的環(huán)境中的變量對象,事實也是如此,而這種機制正是通過函數(shù)內(nèi)部的[[scope]]屬性實現(xiàn)的。

[[scope]]是所有父變量對象的層級鏈,處于當前函數(shù)環(huán)境,在函數(shù)創(chuàng)建時存在于其中。

注意重要的一點:[[scope]]屬性在函數(shù)創(chuàng)建時被存儲,永遠不變,直到函數(shù)銷毀。函數(shù)可以不被調(diào)用,但這個屬性一直存在。
且,與作用域鏈相比,作用域鏈是執(zhí)行環(huán)境的一個屬性,而[[scope]]是函數(shù)的屬性。

上面的例子,函數(shù)foo的[[scope]]如下:

foo.[[Scope]] = [  全局執(zhí)行環(huán)境.變量對象 // === Global];

繼續(xù),我們知道在函數(shù)調(diào)用時進入執(zhí)行環(huán)境,這時活動對象被創(chuàng)建,this、作用域鏈被確定。下面詳細考慮這個時刻。

函數(shù)激活

正如上面提到的,進入環(huán)境創(chuàng)建變量/活動對象之后,環(huán)境的Scope屬性(即作用域鏈)定義為:Scope = 變量/活動對象 +[[scope]]。

這個定義意思是:活動對象是被添加到[[scope]]前端,在作用域鏈中處理第一位。這很重要,對于標識符的查找,是從自身變量對象開始的,逐漸往父變量對象查找。

(2)通過構(gòu)造函數(shù)創(chuàng)建的函數(shù)的[[scope]]

在上面的例子中,我們看到,在函數(shù)創(chuàng)建時,函數(shù)獲得[[scope]]屬性,該屬性存儲著所有父環(huán)境的變量/活動對象。但有一個例外,那就是通過構(gòu)造函數(shù)創(chuàng)建的函數(shù)。

var x = 10; function foo() {   var y = 20;   function barFD() { // 函數(shù)聲明    alert(x);    alert(y);  }   var barFE = function () { // 函數(shù)表達式    alert(x);    alert(y);  };   var barFn = Function('alert(x); alert(y);');   barFD(); // 10, 20  barFE(); // 10, 20  barFn(); // 10, "y" is not defined } foo();

從以上例子中,我們看出問題所在:通過構(gòu)造函數(shù)創(chuàng)建的函數(shù),它的[[scope]]僅包含全局對象。

另外關(guān)于eval,實踐中很少用到eval,但有一點提示,eval代碼的環(huán)境與當前的調(diào)用環(huán)境擁有相同的作用域鏈。

(3)延長作用域鏈

有兩個能延長作用域鏈的方法:with聲明和catch語句。它們添加到作用域鏈的最前端(比被調(diào)用函數(shù)的活動對象還要靠前)。

如果發(fā)生其中一個,作用域鏈作如下修改:

Scope = withObject|catchObject +活動/變量對象 + [[Scope]]

看個例子:

var x = 10, y = 10; with ({x: 20}) {   var x = 30, y = 30;   alert(x); // 30  alert(y); // 30} alert(x); // 10alert(y); // 30
//1. x = 10,y = 10;
//2. 進入環(huán)境,對象{x:20}添加到作用域鏈的前端。
//3. 執(zhí)行代碼,x為20,變?yōu)?0,y為10,變?yōu)?0。
//4.with聲明完成后,對象被移除,那個因with對象而改變的x=30也被移除。
//最后兩個alert,x保持最初不變,y在with里已發(fā)生改變。

四、閉包

到了這里,其實如果對前面的[[scope]]和作用域鏈完全理解的話,閉包也就懂了。

大叔的譯文對閉包給出的2個定義是:

從理論角度:所有函數(shù)都是閉包。因為它們在創(chuàng)建的時候就將所有父環(huán)境的數(shù)據(jù)保存起來了。哪怕是簡單的全局變量也是如此,因為在函數(shù)中訪問全局變量就相當于在訪問自由變量(指不在參數(shù)聲明,也不在局部聲明的變量),這個時候使用最外層的作用域。

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

  ①即使創(chuàng)建它的環(huán)境銷毀,它仍然存在(比如,內(nèi)部函數(shù)從父函數(shù)返回);②在代碼中引用了自由變量。

下面我們再來具體看一下。

var x = 10;function foo() {  alert(x);}(function (funArg) {  var x = 20;  // 變量"x"在foo中靜態(tài)保存的,在該函數(shù)創(chuàng)建的時候就保存了  funArg(); // 10, 而不是20})(foo);

我們已經(jīng)知道,創(chuàng)建foo函數(shù)的父級環(huán)境(在這里是全局環(huán)境)的數(shù)據(jù)是保存在foo函數(shù)的內(nèi)部屬性[[scope]]中的。

這里還要注意的是:同一個父環(huán)境創(chuàng)建的閉包是共用一個[[scope]]屬性的。也就是說,某個閉包對其中[[scope]]的變量的修改會影響到其他閉包對其變量的讀取。

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

關(guān)于這個問題,大叔的譯文和《JS高級》里都有一個例子:

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

這就是閉包共用一個[[scope]]的問題。可以按下面的方法解決:

var data = [];for (var k = 0; k < 3; k++) {  data[k] = (function _helper(x) {    return function () {      alert(x);    };  })(k); // 傳入"k"值}// 現(xiàn)在結(jié)果是正確的了data[0](); // 0data[1](); // 1data[2](); // 2

在上例中,每次_helper都會創(chuàng)建一個新的變量對象,其中含有參數(shù)x,其值就是傳遞進來的k值。此時,返回的函數(shù)的[[scope]]如下:

data[0].[[Scope]] === [  ... // 其它變量對象  父級環(huán)境中的活動對象: {data: [...], k: 3},  _helper環(huán)境中的活動對象: {x: 0}];data[1].[[Scope]] === [  ... // 其它變量對象  父級環(huán)境中的活動對象: {data: [...], k: 3},  _helper環(huán)境中的活動對象: {x: 1}];data[2].[[Scope]] === [  ... // 其它變量對象  父級環(huán)境中的活動對象: {data: [...], k: 3},  _helper環(huán)境中的活動對象: {x: 2}];

要注意的是,如果在返回的函數(shù)中,要獲取k值,那么該值還會是3。

本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
理解 JavaScript 作用域
手把手教會你JavaScript引擎如何執(zhí)行JavaScript代碼
深入理解JavaScript作用域和作用域鏈
JavaScript作用域鏈其二:函數(shù)的生命周期
深入理解JavaScript系列(14):作用域鏈(Scope Chain)
JavaScript中的作用域鏈和閉包
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服