js学习笔记(执行上下文、闭包、this部分)
1、函數的準備工作
函數在執行會進行一些準備工作,如創建一個“執行上下文”環境;執行上下文可以理解為當前代碼的執行環境,它會形成一個作用域;
每個碰到可執行代碼的時候都會進行這些“準備工作”來生成執行上下文。這個“代碼段”其實分三種情況——全局代碼,函數環境,eval代碼。
- 全局環境:JavaScript代碼運行起來會首先進入該環境
- 函數環境:當函數被調用執行時,會進入當前函數中執行代碼
- eval
當代碼在執行過程中,遇到以上三種情況,都會生成一個執行上下文,放入棧中,而處于棧頂的上下文執行完畢之后,就會自動出棧
因此在一個JavaScript程序中,必定會產生多個執行上下文,JavaScript引擎會以堆棧的方式來處理它們,這個堆棧,我們稱其為函數調用棧(call stack)。棧底永遠都是全局上下文,而棧頂就是當前正在執行的上下文。
執行上下文就在棧空間先進后出,后進先出;
如這段常見的代碼:
var color = 'blue';function changeColor() {var anotherColor = 'red';function swapColors() {var tempColor = anotherColor;anotherColor = color;color = tempColor;}swapColors(); }changeColor();(1)首先在棧底的是全局上下文,全局上下文入棧之后,其中的可執行代碼開始執行,直到遇到了changeColor(),這一句激活函數changeColor創建它自己的執行上下文,然后就是changeColor的執行上下文入棧。
(2)changeColor的執行上下文入棧后,和上面進行相同的步驟,可執行代碼執行,然后碰到了swapColor(),此時swapColor創建它自己的執行上下文,入棧。
(3)這時候在swapColors的可執行代碼中,再沒有遇到其他能生成執行上下文的情況(沒有碰到可執行的代碼),因此這段代碼順利執行完畢,swapColors的上下文從棧中彈出。
(4)同理changColor也是一樣,執行后彈出,最后就只剩下了全局上下文了(全局上下文在瀏覽器窗口關閉后出棧。)(注意:函數中,遇到return能直接終止可執行代碼的執行,因此會直接將當前上下文彈出棧。)
?
2、執行上下文是什么
?關于執行上下文是什么,查了下資料,有這兩個說法:
(1)
- 變量、函數表達式——變量聲明,默認賦值為undefined;
- this——賦值;
- 函數聲明——賦值;
這三種數據的準備情況我們稱之為“執行上下文”或者“執行上下文環境”。
?(2)
? ? ? ?執行上下文可以分為兩個階段。
-
創建階段
在這個階段中,執行上下文會分別創建變量對象,建立作用域鏈,以及確定this的指向 -
代碼執行階段
創建完成之后,就會開始執行代碼,這個時候,會完成變量賦值,函數引用,以及執行其他代碼
?其實兩個都是同樣的東西,只不過第二種說法把上下文“拆解了”,對于我們初學者來說,這個更好的理解上下文究竟是什么。
?那么就用第二種說法繼續進行記錄了;
3、變量對象(關于函數聲明,變量提升)
console.log(a);//undenfiendvar a = 2;
?一開始在學習JS時候,認為代碼的執行順序應該是從上到下的,因此這段代碼的輸出應該是會出錯可是,實際情況輸出的是undefined;這是為什么?這就要從執行上下文中的變量對象創建說起了;
我們上面說到執行上下文可以分為兩個階段,在創建階段中,變量對象創建經歷這個階段:
建立arguments對象。檢查當前上下文中的參數,建立該對象下的屬性與屬性值。
檢查當前上下文的函數聲明,也就是使用function關鍵字聲明的函數。在變量對象中以函數名建立一個屬性,屬性值為指向該函數所在內存地址的引用。如果函數名的屬性已經存在,那么該屬性將會被新的引用所覆蓋。
檢查當前上下文中的變量聲明,每找到一個變量聲明,就在變量對象中以變量名建立一個屬性,屬性值為undefined。如果該變量名的屬性已經存在,為了防止同名的函數被修改為undefined,則會直接跳過,原屬性值不會被修改。
(注意下:這里是聲明,相關變量函數還沒賦值,賦值是在執行階段中進行的);
這里看出相關的定義聲明是在函數創建時候就進行了,而賦值此時還停留在“原地”,等待聲明完成,因此上面的代碼實際是這樣運作的;
var a;console.log(a);//undenfiend a = 2;變量a的位置被“移動”到了最上面,這個就是"提升”;
同時這里也有一點要注意的,是先檢查函數聲明,在檢查變量聲明的,也就是說函數是首先會被“提升”,然后才到變量的。
function test() {console.log(a);console.log(foo());var a = 1;function foo() {return 2;} }test();
分析下test此時的變量對象(創建階段) 應是這樣的: argument:{...}, foo:{...(這里是foo函數的引用)},a:undefined,
當聲明過后,進入了執行階段,test此時的變量對象(執行階段) 應是這樣的:argument:{...},foo:{...},a:1;
因此實際的函數運作應是這樣的:
function test() {function foo() {return 2;}var a;console.log(a);console.log(foo());a = 1;}test();注意的是:
未進入執行階段之前,變量對象中的屬性都不能訪問!但是進入執行階段之后,變量對象轉變為了活動對象,里面的屬性都能被訪問了,然后開始進行執行階段的操作。
問到變量對象和活動對象有什么區別,他們其實都是同一個對象,只是處于執行上下文的不同生命周期。
?
4、閉包
?一、在學習閉包前,要了解兩個概念:作用域和作用域鏈;
作用域:
-
在JavaScript中,我們可以將作用域定義為一套規則,這套規則用來管理引擎如何在當前作用域以及嵌套的子作用域中根據標識符名稱進行變量查找。
- 這里的標識符,指的是變量名或者函數名
-
JavaScript中只有全局作用域與函數作用域(因為eval我們平時開發中幾乎不會用到它,這里不討論)。
-
作用域與執行上下文是完全不同的兩個概念。我知道很多人會混淆他們,但是一定要仔細區分。
- JavaScript代碼的整個執行過程,分為兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段作用域規則會確定。執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段創建。
除了全局作用域之外,每個函數都會創建自己的作用域,作用域在函數定義時就已經確定了。而不是在函數調用時確定。;
作用域鏈就是是這套規則的具體實現;
作用域鏈,是由當前環境與上層環境的一系列變量對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。
形象一點來說就好像“糖葫蘆”那樣,相關變量就是“糖葫蘆”,作用域鏈就是木簽;
作用域鏈自下而上地將所有嵌套定義的上下文所綁定的變量對象串接到一起,使嵌套的function可以“繼承”上層上下文的變量,而并列的function之間互不干擾:
作用域鏈的最前端,始終都是當前執行代碼所在的作用域的變量對象。
來看下代碼:
var name = "test1"; function doSomething(){var anotherName = "test2";function showName(){var author ="test3";console(name)console(anotherName)// 在這里可以訪問到 name 、anotherName 、author }showName();// 在這里可以訪問到 name anotherName ,不能訪問到 author } doSomething(); // 在這里只能訪問到 name簡單來說:只能從“里”查詢到“外”進行查詢;而不能從“外”到“里”;
在 JavaScript 中,每個函數都有著自己的作用域,在每次調用一個函數的時候 ,就會進入一個函數內的作用域,而當函數執行返回以后,就返回調用前的作用域。
一定要記住的一點是:JavaScript中的函數運行在它們被定義的作用域里,而不是它們被執行的作用域里;
var name = 'test1'; function showName() {console.log(name); } function show() {var name = 'test2';showName(); } show();//test1同一個作用域下,不同的調用會產生不同的執行上下文環境,繼而產生不同的變量的值。所以,作用域中變量的值是在執行過程中產生的確定的,而作用域卻是在函數創建時就確定了。
所以,如果要查找一個作用域下某個變量的值,就需要找到這個作用域對應的執行上下文環境,再在其中尋找變量的值。
?
二、閉包
上面做了這么多鋪墊,為了就是認識和學習閉包這一重要的概念;
先直截了當的拋出閉包的定義:當函數可以記住并訪問所在的作用域(全局作用域除外)時,就產生了閉包,即使函數是在當前作用域之外執行。
簡單來說,假設函數A在函數B的內部進行定義了,并且當函數A在執行時,訪問了函數B內部的變量對象,那么B就是一個閉包。
每個函數在調用時會創建新的上下文及作用域鏈,而作用域鏈就是將外層(上層)上下文所綁定的變量對象逐一串連起來,使當前函數可以獲取外層上下文的變量、數據等。如果我們在函數中定義新的函數,同時將內層函數作為值返回,那么內層函數所包含的作用域鏈
將會一起返回,即使內層函數在其他上下文中執行,其內部的作用域鏈仍然保持著原有的數據,
而當前的上下文可能無法獲取原先外層函數中的數據,使得函數內部的作用域鏈被保護起來,從而形成“閉包”。
所以,通過閉包,我們可以在其他的執行上下文中,訪問到函數的內部變量;
var fn = null; function foo() {var a = 2;function innnerFoo() { console.log(a);}fn = innnerFoo; // 將 innnerFoo的引用,賦值給全局變量中的fn }function bar() {fn(); // 此處的保留的innerFoo的引用 }foo(); bar(); // 2在上面的例子中,foo()執行完畢之后,按照常理,其執行環境生命周期會結束,所占內存被垃圾收集器釋放。但是通過fn = innerFoo,函數innerFoo的引用被保留了下來,復制給了全局變量fn。這個行為,導致了foo的變量對象,也被保留了下來。于是,函數fn在函數bar內部執行時,依然可以訪問這個被保留下來的變量對象。所以此刻仍然能夠訪問到變量a的值;
有幾點要注意的是:
-
閉包是在函數被調用執行的時候才被確認創建的。
-
閉包的形成,與作用域鏈的訪問順序有直接關系。
-
只有內部函數訪問了上層作用域鏈中的變量對象時,才會形成閉包,因此,我們可以利用閉包來訪問函數內部的變量。
?
(關于閉包相關概念還需要多多學習了解,這里只是作為學習筆記進行記錄)
?
5、this
?在上面有提到,this的取值是執行上下文環境的一部分,每次調用函數,都會產生一個新的執行上下文環境;在函數中this到底取何值,是在函數真正被調用執行的時候確定的,函數定義的時候確定不了。
?先記錄下結論:
在一個函數上下文中,this由調用者提供,由調用函數的方式來決定。如果調用者函數,被某一個對象所擁有,那么該函數在調用時,內部的this指向該對象。如果函數獨立調用,那么該函數內部的this,則指向undefined。但是在非嚴格模式中,當this指向undefined時,它會被自動指向全局對象。
從結論中我們可以看出,想要準確確定this指向,找到函數的調用者以及區分他是否是獨立調用就變得十分關鍵。就是說this指向什么完全取決于函數在哪里被調用
?在“你不知道的JS中”說:this的綁定規則有四種;
1、默認綁定
全局環境中的this,指向它本身。因此,這也相對簡單,沒有那么多復雜的情況需要考慮。
var a = 2;function foo(){console.log(this.a) }foo()//22、隱式綁定(隱式賦值)
來看例子
var a = 20; var foo = {a: 10,getA: function () {return this.a;} } console.log(foo.getA()); // 10var test = foo.getA; console.log(test()); // 20foo.getA()中,getA是調用者,他不是獨立調用,被對象foo所擁有,因此它的this指向了foo。而test()作為調用者,盡管他與foo.getA的引用相同,但是它是獨立調用的,因此this指向undefined,在非嚴格模式,自動轉向全局window。
再來一個例子:
function foo() {console.log(this.a) }function bar(fn) {fn(); // 真實調用者,為獨立調用 }var a = 20; var obj = {a: 10,getA: foo }bar(obj.getA);在這個例子里,obj.getA作為參數傳進了bar中,此時可以看作fn=obj.getA;然后下面fn()
因此這時候也就和上面的一樣,是看作全局調用了。
3、顯式綁定(call、apply)
JavaScript內部提供了一種機制,讓我們可以自行手動設置this的指向。它們就是call與apply。所有的函數都具有著兩個方法。它們除了參數略有不同,其功能完全一樣。它們的第一個參數都為this將要指向的對象。
如下例子所示。fn并非屬于對象obj的方法,但是通過call,我們將fn內部的this綁定為obj,因此就可以使用this.a訪問obj的a屬性了。這就是call/apply的用法。
function fn() {console.log(this.a); } var obj = {a: 2 }fn.call(obj);這里的話,原本運行fn,此時的this是window,但用call把作用域強制綁定在obj上,因此最后輸出的是2;
拓展下:在es5中增加了bind方法;apply,call,bind的說明如下
- apply 、 call 、bind 三者都是用來改變函數的this對象的指向的;
- apply 、 call 、bind 三者第一個參數都是this要指向的對象,也就是想指定的上下文;
- apply 、 call 、bind 三者都可以利用后續參數傳參;
- bind?是返回對應函數,便于稍后調用;apply 、call 則是立即調用 。
bind返回的是一個函數,需要在后面添加括號才執行,當我們需要回調執行的時候,使用 bind() 方法;
在ES6中還增加了箭頭函數的寫法,使用箭頭函數,此時箭頭函數則會捕獲其所在上下文的 ?this?值,作為自己的?this?值
var a=1; var obj={a:2,fn:function(){console.log(this.a);return function(){console.log(this.a)} } }obj.fn()()//2,1var a=1; var obj={a:2,fn:function(){console.log(this.a);return ()=>{console.log(this.a)} } }obj.fn()()//2,2 obj.fn()()這是的調用者如上文說的,其實是window,因為this在被隱式綁定的時候丟失了this的上下文綁定對象,從而把this綁定到全局,之前的話一般用一個變量 var that = this;來存儲this,而在es6可以用使用箭頭函數,直接綁定上下文的this;
當然這里只是對箭頭函數的一些初步認識和了解而已;
四、new綁定(構造函數與原型方法上的this)
function Person(name, age) {this.name = name;this.age = age; }Person.prototype.getName = function() { return this.name; } var p1 = new Person('Nick', 20); p1.getName();我們已經知道,this,是在函數調用過程中確定,因此,搞明白new的過程中到底發生了什么就變得十分重要。
通過new操作符調用構造函數,會經歷以下4個階段。
- 創建一個新的對象;
- 將構造函數的this指向這個新對象;
- 指向構造函數的代碼,為這個對象添加屬性,方法等;
- 返回新對象。
因此,當new操作符調用構造函數時,this其實指向的是這個新創建的對象,最后又將新的對象返回出來,被實例對象p1接收。因此,我們可以說,這個時候,構造函數的this,指向了新的實例對象,p1。
而原型方法上的this就好理解多了,根據上邊對函數中this的定義,p1.getName()中的getName為調用者,他被p1所擁有,因此getName中的this,也是指向了p1。
?
參考資料:http://www.jianshu.com/u/10ae59f49b13
? ? ? ? ? ? ??http://www.cnblogs.com/wangfupeng1988/p/3977924.html
? ? ? ? ? ? ??http://blog.rainy.im/2015/07/04/scope-chain-and-prototype-chain-in-js/
? ? ? ? ? ? ??https://bonsaiden.github.io/JavaScript-Garden/zh/#function.closures
? ? ? ? ? ? ? http://web.jobbole.com/83642/
? ? ? ? ? ? ? https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions
? ? ? ? ? ? ? 《你不知道的js》-閉包和this部分
?
這篇文章純粹是自己學習的記錄筆記而已,里面的內容來源均有說明來源,十分感謝他們的分享。
轉載于:https://www.cnblogs.com/spezz07/p/6524480.html
總結
以上是生活随笔為你收集整理的js学习笔记(执行上下文、闭包、this部分)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 特转合规户是什么意思
- 下一篇: iOS项目之交换方法(runtime)