摘要:前言這段時間一直在消化作用域鏈和閉包的相關知識。而作用域鏈則是這套規則這套規則的具體運行。是變量對象的縮寫那這樣放有什么好處呢我們知道作用域鏈保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。
前言:這段時間一直在消化作用域鏈和閉包的相關知識。之前看《JS高程》和一些技術博客,對于這些概念的論述多多少少不太清楚或者不太完整,包括一些大神的技術文章。這也給我的學習上造成了一些困惑,這幾個概念的理解也是始終處于一個半懂不懂的狀態。后來在某公眾號看到了@波同學的基礎文章,這應該是我所看到的最清楚,最全面,最好懂的文章了。所以我在學習之余決定寫一篇文章,總結學到的知識點,用我的理解來闡述,不足之處,見請諒解。執行上下文(Execution Context)
也叫執行環境,也可以簡稱“環境”。是JS在執行過程中產生的,當JS執行一段可執行的代碼時,就會生成一個叫執行環境的東西。JS中每個函數都會有自己的執行環境,當函數執行時,就生成了它的執行環境,執行上下文會生成函數的作用域。
除了函數有執行環境,還有全局的環境。在JS中,往往不止一個執行環境。
讓我們先來看一個栗子:
var a=10; function foo(){ var b=5; function fn(){ var c=20; var d=100; } fn(); } foo();
在這個栗子中,包括了三個執行環境:全局環境,foo()執行環境,fn()執行環境;
執行環境的處理機制在這里我們要了解到執行上下文的第一個特點:內部的環境可以訪問外部的環境,而外部的環境無法訪問內部的環境。
例如:我們可以在fn()中訪問到位于foo()中的b,在全局環境中的a,而在foo()中卻無法訪問到c或者d。
為什么會這樣,這就要了解JS處理代碼的一個機制了。
我們知道JS的處理過程是以堆棧的方式來處理,JS引擎會把執行環境一個個放入棧里,然后先放進去的后處理,后放進去的先處理,上面這個栗子,最先被放進棧中的是全局環境,然后是foo(),再是fn(),然后處理完一個拿出一個來,所以我們知道為什么foo()不能訪問fn()里的了,因為它已經走了。
執行環境的生命周期好了,了解完執行環境的的處理方式,我們要說明執行環境的生命周期。
執行環境的生命周期分為兩個階段,這兩個階段描述了執行環境在棧里面做了些什么。
創建階段創建階段;
執行階段
執行環境在創建階段會完成這么幾個任務:1.生成變量對象;2.建立作用域鏈;3.確定this指向
執行階段到了執行階段,會給變量賦值,函數引用,然后還有執行其他的代碼。
完成了這兩個步驟,執行環境就可以準備出棧,一路走好了。
以上就是執行環境的具體執行內容。上面提到了執行環境在創建階段會生成變量對象,這也是一個很重要的概念,我們下文會詳細論述。
變量對象(variable object)變量對象是什么呢?《JS高程》是這樣說的:“每個執行環境都有與之關聯的變量對象,環境中定義的所有變量和函數都保存在這個對象中。”
那變量對象里有些什么東西呢?看下文:
變量對象的內容在變量對象創建時,經過了這樣三個步驟:
生成arguments屬性;
找到function函數聲明,創建屬性;
找到var變量聲明,創建屬性
其中值得注意的是:function函數聲明的級別比var變量聲明的級別要高,所以在實際執行的過程中會先尋找function的聲明。
還需要注意的是:在執行環境的執行階段之前,變量對象中的屬性都無法訪問,這里還有一個活動對象(activation object)的概念,其實這個概念正是由進入執行階段的變量對象轉化而來。
來看一個栗子:
function foo(){ var a=10; function fn(){ return 5; } } foo();
讓我們來看看foo()函數的執行環境:
它會包括三個部分:1.變量對象;2.作用域鏈;3.this指向對象
創建階段:建立arguments
找到fn();
找到變量a,undefined;
執行階段:變量對象變成活動對象;
arguments還是它~
fn();
a=10;
以上就是變量對象的內容了,需要記住這個東西,因為會方便我們了解下文另一個重要的概念:作用域鏈。
作用域鏈(scope chain)什么是作用域鏈?《JS高程》里的文字是:“作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問。”懵不懵逼?反正我第一次看到的時候確實是懵逼了。前面我們說過作用域,那么作用域鏈是不是就是串在一起的作用域呢?并不是。
作用域和作用域鏈的關系,用@波同學的話說,作用域是一套通過標識符查找變量的規則。而作用域鏈則是這套規則這套規則的具體運行。
是不是還是有點懵逼?還是看例子吧:
function foo(){ var a=10; function fn(){ return 5; } } foo();
我們還是用上面的栗子,這次我們只看作用域鏈,根據規則,在一個函數的執行環境的作用域鏈上,會依次放入自己的變量對象,父級的變量對象,祖級的變量對象.....一直到全局的變量對象。
比如上面這個栗子,fn()的執行環境的作用域鏈上會有些什么呢?首先是自己的OV,然后是foo()的OV,接著就是全局的OV。而foo()的作用域鏈則會少一個fn()的OV。(OV是變量對象的縮寫)
那這樣放有什么好處呢?我們知道“作用域鏈保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。”有序!外層函數不能訪問內層函數的變量,而內層能夠訪問外層。正是有了這個作用域鏈,通過這個有方向的鏈,我們可以查找標識符,進而找到變量,才能實現這個特性。
閉包好了,終于要講到這個前端小萌新眼里的小boss了。在技術博客和書里翻滾了將將一周,對閉包的各種解釋把我搞得精力憔悴,懷疑人生。以至于在寫下這段關于閉包的論述時,也是內心忐忑,因為我也不確定我說的是百分之百正確。
先看看《JS高程》說的:“閉包是指有權訪問另一個函數作用域中的變量的函數。”
@波同學的說法是:“當函數可以記住并訪問所在的作用域(全局作用域除外)時,就產生了閉包,即使函數是在當前作用域之外執行。”
......
好吧其實我覺得都說的不是太清楚。讓我們這樣來理解,就是內部函數引用了外部函數的變量對象時,外部函數就是一個閉包。
還是看例子吧。
function foo(){ var a=20; return function(){ return a; } } foo()();
在這個栗子中,foo()函數內部返回了一個匿名函數,而匿名函數內部引用了外部函數foo()的變量a,由于作用域鏈,這個引用是有效的,按照JS的機制,foo()執行完畢后,執行環境會失去引用,內存會銷毀,但是由于內部的匿名函數的引用,a會被暫時保存下來,罩著a的就是閉包。
return一個匿名函數時創造一個閉包的最簡單的方式,實際上創造閉包十分靈活,再看一個栗子:
var fn = null; function foo() { var a = 2; function innnerFoo() { console.log(a); } fn = innnerFoo; } function bar() { fn(); } foo(); bar(); // 2
栗子來自@波同學;
如上,可以看到:通過把innnerFoo()賦值給全局變量fn,內部的函數在當前作用域外執行了,但是這不會影響foo形成了一個閉包。
閉包和兩個不同的案例這兩組栗子都是在各種書籍和各種博客上司空見慣了的栗子,其實跟閉包的關系不是很大,但是涉及到了函數相關的知識點,所以在這里寫下來。也算是積累。閉包和變量(見《JS高程》P181)
一個例子
function createFunction(){ var result=new Array(); for(i=0;i<10;i++){ result[i]=function(){ return i; } } return result; } alert(createFunction());
這個例子并不會如我們以為的返回從0到9的一串索引值。
當我們執行createFunction()時,函數內會return result,而我們注意到result是一個數組,而每一個result[i]呢?它返回的則是一個函數,而不是這個函數的執行結果 i。
所以我們想要返回一串索引值的時候,試著選擇result數組的其中一個,再加上圓括號讓它執行起來,像這樣:
createFunction()[2]()
這樣子就能執行了嗎?運行起來發現并沒有,執行的結果是一串的i,為什么呢?
原因是在執行createFunction()的時候,i的值已經增加到了10,即退出循環的值,而再要執行result內部的匿名函數時,它能獲取到的i就只有10了,所以不管引用多少次,i的值都會是10;
那要如何修改才能達到我們的目的呢?
function createFunction(){ var result=[]; for(i=0;i<10;i++){ result[i]=function(num){ return function(){ return num; }; }(i); } return result; } alert(createFunction()[2]());
彈出的警告和索引值一模一樣。這又是什么原因呢?
我們執行createFunction()時,把外部的匿名函數的執行結果賦值給了result,返回的result就是十個函數的數組。
而在這個外部函數里,有一個參數num,由于IIFE(立即執行函數)的緣故,循環過程中的i被賦值給了一個個的num,前后一共保存了10個num,為什么能夠保存下來呢?因為內部的匿名函數引用了num。而這外部函數就是一個閉包
接下來,當執行createFunction()[2]()時實際上是執行這個數組result的第三項,即:
function(){ return num; };
這個函數。
num值是多少呢?如前所述,正是對應的i。所以返回的值就能夠達到我們的預期了。
實際上,我認為這個例子中更重要的是自執行函數這個概念,正是有了自執行,才能形成多對對多的引用,盡管這個例子里確實存在閉包,不過我認為用這個例子來介紹閉包并不是太恰當。閉包和this
this也是JS里一個重中之重。我們知道,JS的this十分靈活的,前面已經介紹過,this的指向在函數執行環境建立時確定。函數中的this的指向是一個萌新們的難點,什么時候它是指向全局環境呢?什么時候它又是指向對象呢?注意:此處討論的是指函數中的this,全局環境下的this一般情況指向window。結論一:this的指向是在函數被調用的時候確定的
因為當一個函數調用時,一個執行環境就創建了,接著它會執行,這是執行環境的生命周期。所以this的指向是在函數被調用時確定的。
結論二:當函數執行時,如果這個函數是屬于某個對象,調用的方式是以對象的方法進行的,那么this的指向就是這個對象,而其他情況,如函數獨立調用,則基本是指向全局對象。PS:實際上這個說法不大準確,當函數獨立調用時,在嚴格模式下,this的指向時undefined,而非嚴格模式下,則時指向全局對象。
為了更好的說明,讓我們看一個例子:
var a = 20; var foo = { a: 10, getA: function () { return this.a; } } console.log(foo.getA()); // 10 var test = foo.getA; console.log(test()); // 20
在上面這個例子中,foo.getA()作為對象方法的調用,指向的自然是這個對象,而test雖然指向和foo.getA相同,但是因為是獨立調用,所以在非嚴格模式下,指向的是全局對象。
除了上面的例子,在《JS高程》中還有一個經典的例子,眾多博客文章均有討論,但是看過之后覺得解釋還是不夠清楚,至少我沒完全理解,這里我將試著用自己的語言來解釋。
var name="the window"; var object={ name:"my object", getNameFunc:function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()()); // the window
在這個帶有閉包的例子里,我們可以看到object.getNameFunc()執行的返回是一個函數,再加()執行則是一個直接調用了。所以指向的是全局對象。
如果我們想要返回變量對象怎么辦呢?
讓我們看一段代碼:
var name="the window";
var object={ name:"my object", getFunc:function(){ return this.name; } }; alert(object.getFunc()); //"my object"```
我去掉了上面例子的閉包,可以看出在方法調用的情況下,this指向的是對象,那么我們只要在閉包能訪問到的位置,同時也是在這個方法調用的同一個作用域里設置一個“中轉站”就好了,讓我們把這個位置的this賦值給一個變量來存儲,然后匿名函數調用這個變量時指向的就會是對象而不是全局對象了。
var name="the window"; var object={ name:"my object", getFunc:function(){ var that=this; return function(){ return that; }; } }; alert(object.getFunc());
that"s all
閉包的應用閉包的應用太多了,最重要的一個就是模塊模式了。不過說實話,實在還沒上路,所以這里就用一個模塊的栗子來結尾吧。(強行結尾)
(function () { var a = 10; var b = 20; function add(num1, num2) { var num1 = !!num1 ? num1 : a; var num2 = !!num2 ? num2 : b; return num1 + num2; } window.add = add; })(); add(10, 20);
我們需要知道的是,所謂模塊利用的就是閉包外部無法訪問內部,內部卻能訪問外部的特性,通過引用了指定的公共變量和方法,達到訪問私有變量和方法的目的。模塊可以保證模塊內部的私有方法和變量不被外部變量污染,進而方便更大規模的開發項目。
so,這篇文就到這里辣,寫了一個下午,最最最要感謝的是@波同學,正是讀了他出色的教程,才能讓我對JS的理解更深一點,他的每一篇技術文章都是非常用心的,事實上,我覺得我的論述仍然不夠系統清晰,想要了解得更清晰的朋友可以去簡書搜索@波同學閱讀他寫得技術文章,好了,就這樣,債見
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107068.html
摘要:在此例中,在匿名函數被返回后,它的作用域鏈初始化為包含函數的活動對象和全局變量對象。函數在執行完畢后,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象,結果就是只是的執行環境的作用域鏈會被銷毀,其活動對象會留在內存中。 寫在前面 注:這個系列是本人對js知識的一些梳理,其中不少內容來自書籍:Javascript高級程序設計第三版和JavaScript權威指南第六版,...
摘要:本期推薦文章從作用域鏈談閉包,由于微信不能訪問外鏈,點擊閱讀原文就可以啦。推薦理由這是一篇譯文,深入淺出圖解作用域鏈,一步步深入介紹閉包。作用域鏈的頂端是全局對象,在全局環境中定義的變量就會綁定到全局對象中。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周開始前端進階的第二期,本周的主題是作用域閉包,今天是第6天。 本...
摘要:變量對象也是有父作用域的。作用域鏈的頂端是全局對象。當函數被調用的時候,作用域鏈就會包含多個作用域對象。當函數要訪問時,沒有找到,于是沿著作用域鏈向上查找,在的作用域找到了對應的標示符,就會修改的值。 一、概要 對于閉包的定義(紅寶書P178):閉包就是指有權訪問另外一個函數的作用域中的變量的函數。 關鍵點: 1、閉包是一個函數 2、能夠訪問另外一個函數作用域中的變量 二、閉包特性 對...
摘要:所以,當在函數中使用全局變量的時候,所產生的代價是最大的,因為全局對象一直處于作用域鏈的最末位置,讀取局部變量是最快的。 什么是作用域 在編程語言中,作用域控制著變量與參數的可見性及生命周期,它能減少名稱沖突,而且提供了自動內存管理(javascript 語言精粹) 靜態作用域 再者,js不像其他的編程語言一樣,擁有著塊級作用域,就像下面一段代碼。 function afunction...
摘要:為了防止之后自己又開始模糊,所以自己來總結一下中關于作用域鏈和原型鏈的知識,并將二者相比較看待進一步加深理解。因此我們發現當多個作用域相互嵌套的時候,就形成了作用域鏈。原型鏈原型說完了作用域鏈,我們來講講原型鏈。 畢業也整整一年了,看著很多學弟都畢業了,忽然心中頗有感慨,時間一去不復還呀。記得從去年這個時候接觸到JavaScript,從一開始就很喜歡這門語言,當時迷迷糊糊看完了《J...
閱讀 1990·2021-09-22 16:05
閱讀 9252·2021-09-22 15:03
閱讀 2880·2019-08-30 15:53
閱讀 1697·2019-08-29 11:15
閱讀 902·2019-08-26 13:52
閱讀 2348·2019-08-26 11:32
閱讀 1797·2019-08-26 10:38
閱讀 2561·2019-08-23 17:19