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