摘要:他的組成如下對的就是你關(guān)注的那個變量對象作用域鏈跟閉包相關(guān)由于是單線程的,一次只能發(fā)生一件事情,其他事情會放在指定上下文棧中排隊。
閉包和this,是兩個相當(dāng)高頻的考點(diǎn),然而你有沒有想過,實際上他們兩個都跟同一個知識點(diǎn)相關(guān)?
有請我們的這篇文章的主角,執(zhí)行上下文
執(zhí)行上下文可以簡單理解執(zhí)行上下文是js代碼執(zhí)行的環(huán)境,當(dāng)js執(zhí)行一段可執(zhí)行代碼時,會創(chuàng)建對應(yīng)的執(zhí)行上下文。他的組成如下
executionContextObj = { this: 對的就是你關(guān)注的那個this, VO:變量對象, scopeChain: 作用域鏈,跟閉包相關(guān) }
由于JS是單線程的,一次只能發(fā)生一件事情,其他事情會放在指定上下文棧中排隊。js解釋器在初始化執(zhí)行代碼時,會創(chuàng)建一個全局執(zhí)行上下文到棧中,接著隨著每次函數(shù)的調(diào)用都會創(chuàng)建并壓入一個新的執(zhí)行上下文棧。函數(shù)執(zhí)行后,該執(zhí)行上下文被彈出。
五個關(guān)鍵點(diǎn):
單線程
同步執(zhí)行
一個全局上下文
無限制函數(shù)上下文
每次函數(shù)調(diào)用創(chuàng)建新的上下文,包括調(diào)用自己
創(chuàng)建階段
初始化作用域鏈
創(chuàng)建變量對象
創(chuàng)建arguments
掃描函數(shù)聲明
掃描變量聲明
求this
執(zhí)行階段
初始化變量和函數(shù)的引用
執(zhí)行代碼
this在函數(shù)執(zhí)行時,this 總是指向調(diào)用該函數(shù)的對象。要判斷 this 的指向,其實就是判斷 this 所在的函數(shù)屬于誰。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
function foo() { console.log( this.a ); } var a = 2; foo(); // 2
注意
//接上 var bar = foo a = 3 bar() // 3不是2
通過這個例子可以更加了解this是函數(shù)調(diào)用時才確定的
再繞一點(diǎn)
function foo() { console.log( this.a ); } function doFoo(fn) { this.a = 4 fn(); } var obj = { a: 2, foo: foo }; var a =3 doFoo( obj.foo ); // 4
而
function foo() { this.a = 1 console.log( this.a ); } function doFoo(fn) { this.a = 4 fn(); } var obj = { a: 2, foo: foo }; var a =3 doFoo( obj.foo ); // 1
這是為什么呢?是因為優(yōu)先讀取foo中設(shè)置的a,類似作用域的原理嗎?
通過打印foo和doFoo的this,可以知道,他們的this都是指向window的,他們的操作會修改window中的a的值。并不是優(yōu)先讀取foo中設(shè)置的a
因此如果把代碼改成
function foo() { setTimeout(() => this.a = 1,0) console.log( this.a ); } function doFoo(fn) { this.a = 4 fn(); } var obj = { a: 2, foo: foo }; var a =3 doFoo( obj.foo ); // 4 setTimeout(obj.foo,0) // 1
上面的代碼結(jié)果可以證實我們的猜測。
a = 4 function A() { this.a = 3 this.callA = function() { console.log(this.a) } } A() // 返回undefined, A().callA會報錯。callA被保存在window上 var a = new A() a.callA() // 3,callA在new A返回的對象里
大家應(yīng)該都很熟悉,令this指向傳遞的第一個參數(shù),如果第一個參數(shù)為null,undefined或是不傳,則指向全局變量
a = 3 function foo() { console.log( this.a ); } var obj = { a: 2 }; foo.call( obj ); // 2 foo.call( null ); // 3 foo.call( undefined ); // 3 foo.call( ); // 3 var obj2 = { a: 5, foo } obj2.foo.call() // 3,不是5! //bind返回一個新的函數(shù) function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = a: 2 }; var bar = foo.bind( obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5
箭頭函數(shù)比較特殊,沒有自己的this,它使用封閉執(zhí)行上下文(函數(shù)或是global)的 this 值。
var x=11; var obj={ x:22, say:()=>{ console.log(this.x); //this指向window } } obj.say();// 11 obj.say.call({x:13}) // 11 x = 14 obj.say() // 14 //對比一下 var obj2={ x:22, say() { console.log(this.x); //this指向obj2 } } obj2.say();// 22 obj2.say.call({x:13}) // 13
指向被綁定的dom元素
document.body.addEventListener("click",function(){ console.log(this) } ) // 點(diǎn)擊網(wǎng)頁 // ...
HTML標(biāo)簽的屬性中是可能寫JS的,這種情況下this指代該HTML元素。
變量對象
變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域,存儲了上下文中定義的變量和函數(shù)聲明。
變量對象式一個抽象的概念,在不同的上下文中,表示不同的對象
全局執(zhí)行上下文中,變量對象就是全局對象。
在頂層js代碼中,this指向全局對象,全局變量會作為該對象的屬性來被查詢。在瀏覽器中,window就是全局對象。
var a = 1 console.log(window.a) // 1 console.log(this.a) // 1
函數(shù)上下文中,變量對象VO就是活動對象AO。
初始化時,帶有arguments屬性。
函數(shù)代碼分成兩個階段執(zhí)行
進(jìn)入執(zhí)行上下文時
此時變量對象包括
形參
函數(shù)聲明,會替換已有變量對象
變量聲明,不會替換形參和函數(shù)
函數(shù)執(zhí)行
根據(jù)代碼修改變量對象的值
舉個例子
function test (a,c) { console.log(a, b, c, d) // 5 undefined [Function: c] undefined var b = 3; a = 4 function c () { } var d = function () { } console.log(a, b, c, d) // 4 3 [Function: c] [Function: d] var c = 5 console.log(a, b, c, d) // 4 3 5 [Function: d] } test(5,6)
來分析一下過程
1.創(chuàng)建執(zhí)行上下文時
VO = {
arguments: {0:5}, a: 5, b: undefined, c: [Function], //函數(shù)C覆蓋了參數(shù)c,但是變量聲明c無法覆蓋函數(shù)c的聲明 d: undefined, // 函數(shù)表達(dá)式?jīng)]有提升,在執(zhí)行到對應(yīng)語句之前為undefined
}
執(zhí)行代碼時
通過最后的console可以發(fā)現(xiàn),函數(shù)聲明可以被覆蓋
作用域鏈先了解一下作用域
變量與函數(shù)的可訪問范圍,控制著變量及函數(shù)的可見性與生命周期。分為全局作用域和局部作用域。
全局作用域:
在代碼中任何地方都能訪問到的對象擁有全局作用域,有以下幾種:
在最外層定義的變量;
全局對象的屬性
任何地方隱式定義的變量(未定義直接賦值的變量),在任何地方隱式定義的變量都會定義在全局作用域中,即不通過 var 聲明直接賦值的變量。
局部作用域:
JavaScript的作用域是通過函數(shù)來定義的,在一個函數(shù)中定義的變量只對這個函數(shù)內(nèi)部可見,稱為函數(shù)(局部)作用域
作用域鏈?zhǔn)且粋€對象列表,用以檢索上下文代碼中出現(xiàn)的標(biāo)識符。
標(biāo)識符可以理解為變量名稱,參數(shù),函數(shù)聲明。
函數(shù)在定義的時候會把父級的變量對象AO/VO的集合保存在內(nèi)部屬性[[scope]]中,該集合稱為作用域鏈。
自由變量指的是不在函數(shù)內(nèi)部聲明的變量。
當(dāng)函數(shù)需要訪問自由變量時,會順著作用域鏈來查找數(shù)據(jù)。子對象會一級一級的向上查找父對象的變量,父對象的變量對子對象是可見的,反之不成立。
作用域鏈就是在所有內(nèi)部環(huán)境中查找變量的鏈?zhǔn)奖怼?/p>
可以直接的說,JS采用了詞法作用域(靜態(tài)作用域),JS的函數(shù)運(yùn)行在他們被定義的作用域中,而不是他們被執(zhí)行的作用域。可以舉一個例子說明:
var s = 3 function a () { console.log(s) } function b () { var s = 6 a() } b() // 3,不是6
如果js采用動態(tài)作用域,打印出來的應(yīng)該是6而不是3,這個例子說明了js是靜態(tài)作用域。
函數(shù)作用域鏈的偽代碼:
function foo() { function bar() { ... } } foo.[[scope]] = [ globalContext.VO ]; bar.[[scope]] = [ fooContext.AO, globalContext.VO ];
函數(shù)在運(yùn)行激活的時候,會先復(fù)制[[scope]]屬性創(chuàng)建作用域鏈,然后創(chuàng)建變量對象VO,然后將其加入到作用域鏈。
executionContextObj: { VO:{}, scopeChain: [VO, [[scope]]] }閉包
閉包按照mdn的定義是可以訪問自由變量的函數(shù)。自由變量前面提到過,指的是不在函數(shù)內(nèi)部聲明的變量。
function a() { var num = 1 function b() { console.log(num++) } return b } var c1 = a() c1() // "1" c1() // "2" var c2 = a() c2() // "1" c2() // "2"
寫的不是很嚴(yán)謹(jǐn)。可能省略了一些過程
運(yùn)行函數(shù)a
創(chuàng)建函數(shù)a的VO,包括變量num和函數(shù)b
定義函數(shù)b的時候,會保存a的變量對象VO和全局變量對象到[[scope]]中
返回函數(shù)b,保存到c1
運(yùn)行c1
創(chuàng)建c1的作用域鏈,該作用域鏈保存了a的變量對象VO
創(chuàng)建c1的VO
運(yùn)行c1,這是發(fā)現(xiàn)需要訪問變量num,在當(dāng)前VO中不存在,于是通過作用域鏈進(jìn)行訪問,找到了保存在a的VO中的num,對它進(jìn)行操作,num的值被設(shè)置成2
再次運(yùn)行c1,重復(fù)第二步的操作,num的值設(shè)置成3
通過上面的運(yùn)行結(jié)果,我們可以觀察到,c2所訪問num變量跟c1訪問的num變量不是同一個變量。我們可以修改一下代碼,來確認(rèn)自己的猜想
function a() { var x = {y : 4} function b() { return x } return b } var c1 = a() var c2 = a() c1 === c2() // false
因此我們可以確定,閉包所訪問的變量,是每次運(yùn)行父函數(shù)都重新創(chuàng)建,互相獨(dú)立的。
注意,同一個函數(shù)中創(chuàng)建的自由變量是可以在不同的閉包共享的
function a() { var x = 0 function b() { console.log(x++) } function c() { console.log(x++) } return { b, c } } var r = a() r.b() // 0 r.c() // 1
補(bǔ)充一個查看作用域鏈和閉包的技巧
打開chrome控制臺
console.dir(r.b) f b() { [[Scopes]]: [ {x:0}, {type: "global", name: "", object: Window} ] }最后
最后,我們再來總結(jié)一下執(zhí)行上下文的過程,加深下印象
var scope = "global scope"; function checkscope(a){ var scope2 = "local scope"; } checkscope(5);
創(chuàng)建全局變量globalContext.VO.
將全局變量VO保存為作用域鏈,設(shè)置到函數(shù)的內(nèi)部屬性[[scope]]
checkscope.[[scope]] = [ globalContext.VO ];
創(chuàng)建函數(shù)執(zhí)行上下文,將checkscope函數(shù)執(zhí)行上下文壓入執(zhí)行上下文棧
ECStack = [ checkscopeContext, globalContext ];
第一步是復(fù)制[[scope]],創(chuàng)建作用域鏈
checkscopeContext = { Scope: checkscope.[[scope]], }
第二步是創(chuàng)建活動對象AO
checkscopeContext = { AO: { arguments: { 0: 5 length: 1 }, a: 5 scope2: undefined }, Scope: checkscope.[[scope]], }
第三步是將活動對象AO放入作用域鏈頂端
checkscopeContext = { AO: { arguments: { 0: 5 length: 1 }, a: 5 scope2: undefined }, Scope: [AO, checkscope.[[scope]]], }
第四步,求出this,上下文創(chuàng)建階段結(jié)束
這里的this等于window
隨著函數(shù)執(zhí)行,修改AO的值
AO: { arguments: { 0: 5 length: 1 }, a: 5 scope2: "local scope" },
函數(shù)上下文從執(zhí)行上下文棧彈出
ECStack = [ globalContext ];
文章寫的比較長,涉及的范圍也比較廣,可能有不少的錯誤,希望大家可以指正。
本文章為前端進(jìn)階系列的一部分,
歡迎關(guān)注和star本博客或是關(guān)注我的github
深入理解ES6箭頭函數(shù)中的this
你不知道的JS上卷
JavaScript深入之執(zhí)行上下文棧
理解JavaScript的作用域鏈
JavaScript深入之變量對象
深入理解JavaScript系列(12):變量對象(Variable Object)
了解JavaScript的執(zhí)行上下文
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/94089.html
摘要:醞釀許久之后,筆者準(zhǔn)備接下來撰寫前端面試題系列文章,內(nèi)容涵蓋瀏覽器框架分鐘搞定常用基礎(chǔ)知識前端掘金基礎(chǔ)智商劃重點(diǎn)在實際開發(fā)中,已經(jīng)非常普及了。 這道題--致敬各位10年阿里的前端開發(fā) - 掘金很巧合,我在認(rèn)識了兩位同是10年工作經(jīng)驗的阿里前端開發(fā)小伙伴,不但要向前輩學(xué)習(xí),我有時候還會選擇另一種方法逗逗他們,拿了網(wǎng)上一道經(jīng)典面試題,可能我連去阿里面試的機(jī)會都沒有,但是我感受到了一次面試1...
摘要:面試題實現(xiàn)結(jié)果,題的核心就是問的的柯里化先說說什么是柯里化,看過許多關(guān)于柯里化的文章,始終搞不太清楚,例如柯里化是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)最初函數(shù)的第一個參數(shù)的函數(shù),并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)的技術(shù)。 面試題:實現(xiàn)add(1)(2)(3) //結(jié)果 = 6,題的核心就是問的js的柯里化 先說說什么是柯里化,看過許多關(guān)于柯里化的文章,始終搞不太清楚,例如...
摘要:函數(shù)被轉(zhuǎn)化之后得到柯里化函數(shù),能夠處理的所有剩余參數(shù)。因此柯里化也被稱為部分求值。那么函數(shù)的柯里化函數(shù)則可以如下因此下面的運(yùn)算方式是等價的。而這里對于函數(shù)參數(shù)的自由處理,正是柯里化的核心所在。額外知識補(bǔ)充無限參數(shù)的柯里化。 showImg(https://segmentfault.com/img/remote/1460000008493346); 柯里化是函數(shù)的一個比較高級的應(yīng)用,想要...
摘要:原文鏈接有大量平均水平左右的工人可被選擇參與進(jìn)來這意味著好招人有成熟的大量的程序庫可供選擇這意味著大多數(shù)項目都是既有程序庫的拼裝,標(biāo)準(zhǔn)化程度高而定制化場景少開發(fā)工具測試工具問題排查工具完善,成熟基本上沒有團(tuán)隊愿意在時間緊任務(wù)重的項目情況 原文鏈接:http://pfmiles.github.io/blog/java-groovy-mixed/ 有大量平均水平左右的工人可被選擇、參與...
摘要:閉包利用的,其實就是作用域嵌套情況下,內(nèi)部作用域可以訪問外部作用域這一特性。之所以要將閉包和垃圾回收策略聯(lián)系在一起,是因為這涉及到閉包的一些重要特性,如變量暫時存儲和內(nèi)存泄漏。因為匿名函數(shù)回調(diào)的閉包實際引用的是變量,而非變量的值。 本文旨在總結(jié)在js學(xué)習(xí)過程中,對閉包的思考,理解,以及一些反思,如有不足,還請大家指教 閉包---closure 閉包是js中比較特殊的一個概念,其特殊之處...
閱讀 3665·2021-11-15 11:37
閱讀 2320·2021-09-24 10:39
閱讀 2450·2021-07-25 21:37
閱讀 1439·2019-08-30 15:56
閱讀 2585·2019-08-30 15:55
閱讀 952·2019-08-30 15:54
閱讀 2124·2019-08-30 14:21
閱讀 855·2019-08-30 11:24