摘要:自由變量指的是不在函數內部聲明的變量。作用域鏈就是在所有內部環境中查找變量的鏈式表。閉包的形式閉包的過程寫的不是很嚴謹。
要弄懂這個問題首先要搞清楚一個概念, 執行上下文。
執行上下文執行上下文是什么?
可以簡單理解執行上下文是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
上面的代碼結果可以證實我們的猜測。
用new構造就指向新對象: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返回的對象里apply/call/bind
大家應該都很熟悉,令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
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.
創建checkscope函數
將全局變量VO保存為作用域鏈,設置到函數的內部屬性[[scope]]
checkscope.[[scope]] = [ globalContext.VO ];
執行checkscope函數
創建函數執行上下文,將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 ];
文章寫的比較長,涉及的范圍也比較廣,可能有不少的錯誤,希望大家可以指正。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95903.html
摘要:引言滿滿的干貨,面試必系列,參考大量資料,并集合自己的理解以及相關的面試題,對核心知識點中的作用域閉包上下文進行了梳理。本篇重點介紹閉包和。所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。 showImg(https://segmentfault.com/img/bVbo4hv?w=1800&h=1000); 引言 滿滿的干貨,面試必bei系列,參考大量資料,并集...
摘要:內部的稱為內部函數或閉包函數。過度使用閉包會導致性能下降。,閉包函數分為定義時,和運行時。循環會先運行完畢,此時,閉包函數并沒有運行。閉包只能取得外部函數中的最后一個值。事件綁定種的匿名函數也是閉包函數。而對象中的閉包函數,指向。 閉包概念解釋: 閉包(也叫詞法閉包或者函數閉包)。 在一個函數parent內聲明另一個函數child,形成了嵌套。函數child使用了函數parent的參數...
摘要:閉包的學術定義先來參考下各大權威對閉包的學術定義百科閉包,又稱詞法閉包或函數閉包,是引用了自由變量的函數。所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。 前言 上一章講解了閉包的底層實現細節,我想大家對閉包的概念應該也有了個大概印象,但是真要用簡短的幾句話來說清楚,這還真不是件容易的事。這里我們就來總結提煉下閉包的概念,以應付那些非專人士的心血來潮。 閉包的學術...
摘要:一般來講,函數執行完畢后,局部活動對象就會被銷毀,內存中僅保存全局作用域,但是閉包的情況有所不同理解閉包的前提先理解另外兩個內容作用域鏈垃圾回收作用域鏈當代碼在執行過程中,會創建變量對象的一個作用域鏈。 閉包是javascript語言的一個難點,也是它的特色,很多高級應用都要依靠閉包來實現。個人的理解是:函數中嵌套函數。 閉包的定義及其優缺點 閉包是指有權訪問另一個函數作用域中的變量的...
閱讀 2418·2021-11-16 11:44
閱讀 1877·2021-10-12 10:12
閱讀 2160·2021-09-22 15:22
閱讀 3008·2021-08-11 11:17
閱讀 1505·2019-08-29 16:53
閱讀 2653·2019-08-29 14:09
閱讀 3474·2019-08-29 14:03
閱讀 3301·2019-08-29 11:09