1. 程式人生 > >一文梳理JavaScript中的this

一文梳理JavaScript中的this

最近零零碎碎看了許多關於this的文章,本著“好記性不如爛筆頭”的思想,特在這裡整理一下this有關的知識點。【長文警告!!!】 接下來,筆者將按照以下目錄對**this**進行闡述: - this是什麼? - this指向 - this在全域性範圍內 - this在物件的建構函式內 - this在物件的方法內 - this在簡單函式內 - this在箭頭函式內 - this在一個事件偵聽器內 - this繫結規則 - 預設繫結 - 隱式繫結 - 顯示繫結(this修改) - 優先順序 - 箭頭函式 ### 1. this是什麼? this是JavaScript的一個關鍵字,但它時常蒙著面紗讓人無法捉摸,許多對this不明就裡的同學,常常會有這樣的錯誤認知: - ~~this在函式內指向函式自身~~ - ```javascript function foo(num){ console.log("foo: " + num); //記錄foo被呼叫次數 this.count++; } foo.count = 0; for(let i=0; i<10; i++){ if(i > 5){ foo(i); } } console.log(foo.count); // 0, this並沒有指向foo函式,foo.count沒有進行任何操作 ``` - ~~this在函式內指向函式的作用域~~ - ```javascript function foo(){ var a = 2; this.bar(); } function bar(){ console.log(this.a); } foo();// undefined, window物件沒有bar這一屬性 ``` ### 2. this指向 this的指向取決於他所處的環境. 大致上,可以分為下面的6種情況: - this在全域性範圍內 - this在物件的建構函式內 - this在物件的方法內 - this在一個簡單的函式內 - this在箭頭函式內 - this在一個事件偵聽器內 #### 2.1 this在全域性範圍內 this在全域性範圍內繫結什麼呢?這個相信只要學過JS,應該都知道答案。如果不知道,同學真的應該反思自己的學習態度和方法是否存在問題了。話不多說,直接上程式碼,一探究竟,揭開this在全域性範圍下的真面目: ```javascript console.log(this); // Window ``` 不出意外,**this在全域性範圍內指向window物件**()。通常, 在全域性環境中, 我們很少使用this關鍵字, 因此對它也沒那麼在意. 讓我們繼續看下一個環境. #### 2.2 this在物件的建構函式內 當我們使用new建立建構函式的例項時會發生什麼呢?以這種方式呼叫建構函式會經歷以下四個步驟: - 建立一個空物件; - 將建構函式的作用域賦給新物件(this指向了這個新物件),繼承函式的原型; - 執行建構函式中的程式碼; - 返回新物件。 看完上面的內容,大家想必也知道this在物件的建構函式內的指向了吧!**當你使用new關鍵字建立一個物件的新的例項時, this關鍵字指向這個例項** . 舉個栗子: ```javascript function Human (age) { this.age = age; } let greg = new Human(22); let thomas = new Human(24); console.log(greg); // this.age = 22 console.log(thomas); // this.age = 24 // answer Person { age:22} Person { age:24} ``` #### 2.3 this在物件方法內 方法是與物件關聯的函式的通俗叫法, 如下所示: ```javascript let o = { sayThis(){ console.log(this); } } ``` ![](https://img2020.cnblogs.com/blog/1083040/202006/1083040-20200620100732629-1489724266.png) 如上所示,**在物件的任何方法內的this都是指向物件本身** . 好了,繼續下一個環境! #### 2.4 this在簡單函式內 可能看到這裡,許多同學心裡會有疑問,什麼是簡單函式? 其實簡單函式大家都很熟悉,就像下面一樣,以相同形式編寫的匿名函式也被認為是簡單函式(非箭頭函式)。 ```javascript function hello(){ console.log("hello"+this); } ``` 這裡需要注意,**在瀏覽器中,不管函式宣告在哪裡,匿名或者不匿名,只要不是直接作為物件的方法,this指向始終是window物件**(除非使用call,apply,bind修改this指向)。 舉個栗子說明一下: ```javascript // 顯示函式,直接定義在sayThis方法內,this指向依舊不變 function simpleFunction() { console.log(this); } var o = { sayThis() { simpleFunction(); } } simpleFunction(); // Window o.sayThis(); // Window // 匿名函式 var o = { sayThis(){ (function(){consoloe.log(this);})(); } } o.sayThis();// Window ``` 對於初學者來說,this在簡單函式內的表現時常讓他們懵逼不已,~~難道this不應該指向物件本身?~~這個問題曾經也出現在我的腦海裡過,沒錯,在寫程式碼時我也踩過這個坑。 通常的,當我們要在物件方法內呼叫函式,而這個函式需要用到this時,我們都會建立一個變數來儲存物件中的this的引用. 通常, 這個變數名稱叫做self或者that。具體說下所示: ```javascript const o = { doSomethingLater() { const self = this; setTimeout(function() { self.speakLeet(); }, 1000); }, speakLeet() { console.log(`1337 15 4W350M3`); } } o.doSomethingLater(); // `1337 15 4W350M3` ``` 心細的同學可能已經發現,這裡的簡單函式沒有將箭頭函式包括在內,那麼下一個環境是什麼想必也能猜到啦,那麼現在進入下一個環境,看看this指向什麼。 #### 2.5 this在箭頭函式內 和簡單函式表現不太一樣,**this在箭頭函式中總是跟它在箭頭函式所在作用域的this一樣(在它直接作用域). 所以, 如果你在物件中使用箭頭函式, 箭頭函式中的this總是指向這個物件本身, 而不是指向Window.** 下面我們使用箭頭函式,重寫一下上面的案例: ```javascript const o = { doSomethingLater() { setTimeout(() => this.speakLeet(), 1000); }, speakLeet() { console.log(`1337 15 4W350M3`); } } o.doSomethingLater(); // `1337 15 4W350M3` ``` 最後,讓我們來看看最後一種環境 - 事件偵聽器. #### 2.6 this在事件偵聽器內 **在事件偵聽器內, this被繫結的是觸發這個事件的元素**: ```javascript let button = document.querySelector('button'); button.addEventListener('click', function() { console.log(this); // button }); ``` ### 3. this繫結規則 事實上,只要記住上面this在不同環境的繫結值,足以應付大部分工作。然而,好學的同學總是會忍不住想說,為什麼呢?對,為什麼this在這些情況下繫結這些值呢?**學習,我們不能只知其然,而不知所以然**。所以,現在就讓我們來探尋,this值獲取的真相吧。 現在,讓我們回憶一下,在講什麼是this的時候,我們說到“~~this的繫結取決於他所處的環境~~”。這句話其實不是十分準確,準確的說,**this不是編寫時繫結,而是執行時繫結**。它**依賴於函式呼叫的上下文條件**。**this繫結和函式宣告的位置無關,反而和函式被呼叫的方式有關**。 當一個函式被呼叫時,會建立一個活動記錄,也稱為執行環境。這個記錄包含函式是從何處(call-stack)被呼叫的,函式是 如何被呼叫的,被傳遞了什麼引數等資訊。這個記錄的屬性之一,就是在函式執行期間將被使用的this引用。**this實際上是在函式被呼叫時建立的一個繫結,它指向什麼是完全由函式被呼叫的呼叫點來決定的**。 #### 僅僅是規則 現在我們將注意力轉移到呼叫點 如何 決定在函式執行期間this指向哪裡。 你必須考察call-site並判定4種規則中的哪一個適用。我們將首先獨立的解釋一下這4種規則中的每一種,之後我們來展示一下如果有多種規則可以適用呼叫點時,它們的優先順序。 #### 3.1 預設繫結規則 第一種規則來源於函式呼叫的最常見的情況:獨立函式呼叫。可以認為這種this規則是在沒有其他規則適用時的預設規則。我們給它一個稱呼“預設繫結”. 現在來看這段程式碼: ```javascript function foo(){ console.log(this); } var a = 2; demo(); // 2 ``` 當foo()被呼叫時,this.a解析為我們的全域性變數a。為什麼?因為在這種情況下,對此方法呼叫的this實施了 預設繫結,所以使this指向了全域性物件。 在我們的程式碼段中,foo()是被一個直白的,毫無修飾的函式引用呼叫的。沒有其他的我們將要展示的規則適用於這裡,所以 預設繫結 在這裡適用。 **如果strict mode在這裡生效,那麼對於 預設繫結 來說全域性物件是不合法的,所以this將被設定為undefined。** ```javascript 'use strict' function foo(){ console.log(this.a); // TypeError: Cannot read property 'a' of undefined } const a = 1; foo(); ``` ```javascript function foo(){ 'use strict' console.log(this.a); // TypeError: Cannot read property 'a' of undefined } const a = 1; foo(); ``` 微妙的是,即便所有的this繫結規則都是完全基於呼叫點,如果foo()的 內容 沒有在strint mode下執行,對於 預設繫結 來說全域性物件是 唯一 合法的;foo()的call-site的strict mode狀態與此無關。 ```javascript function foo(){ console.log(this.a); } var a = 1; (function(){ 'use strict'; foo(); // 1 })(); ``` **注意**: 在程式碼中故意混用strict mode和非strict mode通常是讓人皺眉頭的。你的程式整體可能應當不是 Strict 就是非Strict。然而,有時你可能會引用與你的 Strict 模式不同的第三方包,所以對這些微妙的相容性細節要多加小心。 #### 3.2 隱式繫結 另一種要考慮的規則是:呼叫點是否有一個環境物件(context object),也稱為擁有者(owning)或容器(containing)物件。 讓我們來看這段程式碼: ```javascript function foo() { console.log(this.a); } let o = { a: 2, foo, } o.foo(); // 2 ``` 這裡,我們注意到foo函式被宣告然後作為物件o的方法,無論foo()是否一開始就在obj上被宣告,還是後來作為引用新增(如上面程式碼所示),都是這個 函式 被obj所“擁有”或“包含”。這裡,呼叫點使用obj環境來引用函式,所以可以說 obj物件在函式被呼叫的時間點上“擁有”或“包含”這個 函式引用。 **當一個方法引用存在一個環境物件時,隱式繫結 規則會說:是這個物件應當被用於這個函式呼叫的this繫結。** 只有物件屬性引用鏈的最後一層是影響呼叫點的。比如: ```javascript function foo(){ console.log(this.a); } var obj1 = { a:2, obj2:obj2 }; var obj2 = { a:42, foo:foo }; obj1.obj2.foo(); // 42 ``` **隱式繫結的隱患** 當一個 隱含繫結丟失了它的繫結,這通常意味著它會退回到 預設繫結, 根據strict mode的狀態,結果不是全域性物件就是undefined。 下面來看這段程式碼: ```javascript function foo(){ console.log(this.a); } var obj = { a:2, foo }; var bar = obj.foo; var a = "Global variable"; bar(); // "Global variable" ``` 儘管bar似乎是obj.foo的引用,但實際上它只是另一個foo自己的引用而已。另外,起作用的呼叫點是bar(),一個直白,毫無修飾的呼叫,因此 預設繫結 適用於這裡。 這種情況發生的更加微妙,更常見,更意外的方式,是當我們考慮傳遞一個回撥函式時: ```javascript function foo(){ console.log(this.a); } function doFoo(fn){ fn(); } var obj = { a:2, foo, }; var a = "Global variable"; dooFoo(obj.foo); // "Global variable" ``` 引數傳遞僅僅是一種隱含的賦值,而且因為我們在傳遞一個函式,它是一個隱含的引用賦值,所以最終結果和我們前一個程式碼段一樣。同樣的,語言內建,如setTimeout也一樣,如下所示 ```javascript function foo(){ console.log(this.a); } var obj = { a:2, foo, }; var a = "Global variable"; setTimeout(obj.foo, 100); // "Global variable" ``` 把這個粗糙的setTimeout()假想實現當做JavaScript環境內建的實現的話: ```javascript function setTimeout(fn, delay){ // 等待delay毫秒 fn(); } ``` 正如我們看到的, 隱含繫結丟失了它的繫結是十分常見的,不管哪一種意外改變this的方式,你都不能真正地控制你的回撥函式引用將如何被執行,所以你(還)沒有辦法控制呼叫點給你一個故意的繫結。但是我們可以使用顯示繫結強行固定this。 #### 3.3 顯示繫結 我們看到隱含繫結,需要我們不得不改變目標物件使它自身包含一個對函式的引用,而後使用這個函式引用屬性來間接地(隱含地)將this繫結到這個物件上。 但是,如果你想強制一個函式呼叫使用某個特定物件作為this繫結,而不在這個物件上放置一個函式引用屬性呢? js有提供call()、apply()方法,ES5中也提供了內建的方法 Function.prototype.bind,可以引用一個物件時進行強制繫結呼叫。 考慮這段程式碼: ```javascript function foo(){ console.log(this.a); } var obj = { a:2, }; foo.call(obj); // 2 ``` 通過foo.call(..)使用 明確繫結 來呼叫foo,允許我們強制函式的this指向obj。 如果你傳遞一個簡單原始型別值(string,boolean,或 number型別)作為this繫結,那麼這個原始型別值會被包裝在它的物件型別中(分別是new String(..),new Boolean(..),或new Number(..))。這通常稱為“boxing(封箱)”。 **注意**: 就this繫結的角度講,call(..)和apply(..)是完全一樣的。它們確實在處理其他引數上的方式不同,但那不是我們當前關心的。 單獨依靠call和apply,仍然可能出現函式“丟失”自己原本的this繫結,或者被第三方覆蓋等問題。 但有一個技巧可以避免出現這些問題 考慮這段程式碼: ```javascript function foo(){ console.log(this.a); } var obj = { a:2 }; var bar = function(){ foo.call(obj); } bar(); // 2 setTimeout(bar, 100); // 2 bar.call(window); // 2 ``` 我們建立了一個函式bar(),在它的內部手動呼叫foo.call(obj),由此強制this繫結到obj並呼叫foo。無論你過後怎樣呼叫函式bar,它總是手動使用obj呼叫foo。這種繫結即明確又堅定,該方法被開發者稱為 **硬繫結**(顯示繫結的變種)(hard binding) 用硬繫結將一個函式包裝起來的最典型的方法,是為所有傳入的引數和傳出的返回值建立一個通道: ```javascript function foo(something){ console.log(this.a, something); return this.a + something; } var obj = { a:2 }; var bar = function() { return foo.apply(obj, arguments); } var b = bar(3); console.log(b); // 5 ``` 另一種表達這種模式的方法是建立一個可複用的幫助函式: ```javascript function foo(something){ console.log(this.a, something); return this.a + something; } function bind(fn, obj){ return function(){ return fn.apply(obj, arguments); }; } var obj = { a:2}; var bar = bind(foo, obj); var b = bar(3); console.log(b); // 5 ``` 由於 硬繫結 是一個如此常用的模式,它已作為ES5的內建工具提供,即前文提到的Function.prototype.bind: ```javascript function foo(something){ console.log(this.a, something); return this.a + something; } var obj = { a:2}; var bar = foo.bind(obj); var b = bar(); cobsole.log(b); // 5 ``` bind(..)返回一個硬編碼的新函式,它使用你指定的this環境來呼叫原本的函式。 **注意**: 在ES6中,bind(..)生成的硬繫結函式有一個名為.name的屬性,它源自於原始的 目標函式(target function)。舉例來說:bar = foo.bind(..)應該會有一個bar.name屬性,它的值為"bound foo",這個值應當會顯示在呼叫棧軌跡的函式呼叫名稱中。 #### 3.4new 繫結 第四種也是最後一種this繫結規則 當在函式前面被加入new呼叫時,也就是構造器呼叫時,下面這些事情會自動完成: - 一個全新的物件會憑空建立(就是被構建) - 這個新構建的物件會被接入原形鏈([[Prototype]]-linked) - 這個新構建的物件被設定為函式呼叫的this繫結 - 除非函式返回一個它自己的其他 物件,這個被new呼叫的函式將 自動 返回這個新構建的物件。 考慮這段程式碼: ```javascript function foo(a){ console.log(this.a); } var bar = new foo(2); console.log(bar.a); // 2 ``` 通過在前面使用new來呼叫foo(..),我們構建了一個新的物件並這個新物件作為foo(..)呼叫的this。 new是函式呼叫可以繫結this的最後一種方式,我們稱之為 new繫結(new binding)。 #### 3.5 優先順序 - new繫結 - 顯示繫結 - 隱式繫結 - 預設繫結(嚴格模式下會繫結到undefined) ### 4. 箭頭函式 箭頭函式並非使用function關鍵字進行定義,而是通過所謂的“大箭頭”操作符:=>,所以不會使用上面所講解的this四種標準規範,箭頭函式從封閉它的(function或global)作用域採用this繫結,即箭頭函式會繼承自外層函式呼叫的this繫結。 執行 `fruit.call(apple)`時,箭頭函式this已被繫結,無法再次被修改。 ```javascript function fruit(){ return () => { console.log(this.name); } } var apple = { name: '蘋果' } var banana = { name: '香蕉' } var fruitCall = fruit.call(apple); fruitCall.call(banana); // 蘋果 ``` ### 5. 小結 this是JavaScript的一個關鍵字,this不是編寫時繫結,而是執行時繫結。它依賴於函式呼叫的上下文條件。this繫結和函式宣告的位置無關,反而和函式被呼叫的方式有關。為執行中的函式判定this繫結需要找到這個函式的直接呼叫點。找到之後,4種規則將會以 這個 優先順序施用於呼叫點: - 被new呼叫?使用新構建的物件。 - 被call或apply(或 bind)呼叫?使用指定的物件。 - 被持有呼叫的環境物件呼叫?使用那個環境物件。 - 預設:strict mode下是undefined,否則就是全域性對 與這4種繫結規則不同,ES6的箭頭方法使用詞法作用域來決定this繫結,這意味著它們採用封閉他們的函式呼叫作為this繫結(無論它是什麼)。它們實質上是ES6之前的self = this程式碼的語法替代品。 參考文章: [深入理解JavScript中的this](https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651226850&idx=2&sn=b1c7c98d69eabcbeefcea1406294f864&chksm=bd495b668a3ed270f6bd69109fccab46d7968c99c1ab45c2207d8723ee2fd6effe792204f94e&mpshare=1&scene=23&srcid=0618PQw7U0iYelQsWBrWYdoz&sharer_sharetime=1592450380900&sharer_shareid=1bd329d1209e00c54d33930f46bb6eb3#rd) [詳解JavaScript中的this](https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651230163&idx=1&sn=1a55521b1ec68036dd512192d9fc7dd1&chksm=bd4956578a3edf41bf74899fc6a2232cf5744b1e3235445dbd27e98854fb4ca82917a64f2c0d&mpshare=1&scene=23&srcid=06187NwO2wjUH7h7vm0BiGg7&sharer_sharetime=1592450325509&sharer_shareid=1bd329d1209e00c54d33930f46bb6eb3#rd) [你不懂this:豁然開朗](https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651223924&idx=1&sn=d04be77d8a1e59a5cac285128161e7f1&chksm=bd49aef08a3e27e69ab27f47ea151d462620810ef05cbb558ce9418c507f01c24d9e13ed7d95&mpshare=1&scene=23&srcid=0618mD3Jvm3CK6qzdTjXx066&sharer_sharetime=1592450407747&sharer_shareid=1bd329d1209e00c54d33930f46bb6eb3#rd) [你不懂this:this是什麼?](https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651223888&idx=1&sn=a3dafa468028808fd12fc9d08629e0b7&chksm=bd49aed48a3e27c283003de0be24d633999b005c8735fca49e1eca4f793df5249b653b7f5f2d&scene=21#wechat_r