開發者的javascript造詣取決於對【動態】和【非同步】這兩個詞的理解水平。 [TOC] 一.this是什麼 是javascript關鍵字之一,是javascript能夠實現 面向對象編程 的核心概念。用得好能讓代碼優雅高端,風騷飄逸,用不好也絕對是坑人坑己利器。我們常常會在一些資料中看到對 的描 ...
目錄
開發者的javascript造詣取決於對【動態】和【非同步】這兩個詞的理解水平。
一.this是什麼
this
是javascript關鍵字之一,是javascript能夠實現面向對象編程的核心概念。用得好能讓代碼優雅高端,風騷飄逸,用不好也絕對是坑人坑己利器。我們常常會在一些資料中看到對this
的描述是:
this
是一個特殊的與Execution Contexts相關的對象,用於指明當前代碼執行時的Execution Contexts,this
在語句執行進入一個Execution Contexts時被賦值,且在代碼執行過程中不可再改變。
註:Execution Contexts也就是我們常聽到的"上下文"或"執行環境"。
看不懂?看不懂就對了,我也看不懂。
對於this
的指向,我們常會聽到這樣一個原則——this是一個指針,指向當前調用它的對象。但實際使用中,我們卻發現有時候很難知道當前調用它的是哪個對象,從而引發了一系列的誤用和奇怪現象。
今天,我們就換一種思路,試試如何從語言的角度一步一步地去理解this
,你會發現:
只要你能聽懂中國話,就意味著你能理解this
二.近距離看this
2.1 this的語法意義
javascript是一門程式設計語言,也就是說,它是一種語言,是語言,就有語法特性。如果拋開this
的原理和編程中的用法,僅從語文的層面去理解,它的本質就是代詞
。什麼是代詞?漢語中的你
,我
,他
,你們
,我們
,他們
這一類的詞語就是代詞。代詞並不具體指某一個具體的事物,但結合上下文,就可以知道這類詞語代替的是誰。
比如下麵這幾句描述的語境:
- 他大爺是趙本山
- 請問:誰大爺是趙本山?
- 沒法回答,因為沒有上下文約束,此處的他可能指任何人。
- 李雷來頭可不小,他大爺是趙本山
- 請問:誰大爺是趙本山?
- 很容易回答,因為前一句話使得我們能夠得知當前上下文中,"他"指的就是"李雷"。
- ___來頭可不小,他大爺是趙本山
- 請問:誰大爺是趙本山?
- 此處空格填誰,誰大爺就是趙本山。
小結一下:
代詞,用於指代某個具體事物,當結合上下文時,就可以知道其具體的指向。換句話說,有了上下文時,代詞就有了具體的意義。
this
在javascript語言中的意義,就如同代詞
在漢語中的意義是一樣的。
2.2 不同作用域中的this
在ES6出現前,javascript中的作用域只分為全局作用域和函數作用域兩種。(以下部分暫不討論嚴格模式)。
- 全局作用域中使用this
全局作用域中的this
是指向window對象
的,但window對象
上卻並沒有this
這個屬性:
- 函數作用域使用this
函數作用域中的this
也是有指向的(本例中指向window對象
),我們知道函數的原型鏈是會指向Object
的,所以函數本身可以被當做一個對象來看待,但遺憾的是函數
的原型鏈上也沒有this
這個屬性:
綜上所述,this
可以直觀地理解為:
this與函數相關,是函數在運行時解釋器自動為其賦值的一個局部常量。
2.3 javascript代碼編寫方式
a.不使用this
這是有可能發生的。很多初學者會發現,自己在編寫javascript代碼時並沒有用到this,但是也並不影響自己編寫代碼。前面提到過上下文信息的意義在於讓代詞明確其指向,那麼如果一段話的上下文中並沒有使用代詞,在語文中我們就不需要聯繫上下文就能理解這段話;同理,如果函數的函數體中並沒有使用this
關鍵字來指代任何對象,或者不需要關註其調用對象,那實際上就算不確定this
的指向,函數的執行過程也不會有歧義。
/**
*數據加工轉換類的函數,對開發者來說更關註結果,而並不在乎是誰在調用。
*/
function addNumber(a,b) {
return a + b;
}
無論是電腦對象調用addNumber方法,或是算盤對象調用addNumber方法,甚至是人類對象通過心算調用addNumber方法,都無所謂,因為我們關註的是結果,而不是它怎麼來的。
b.不使用函數自帶的this
有時候我們編寫的代碼是需要用到一些關於調用對象
的信息的,但由於不熟悉this
的用法,許多開發者使用了另一種變通的方式,也就是顯式傳參。比如我們在一個方法中,需要打出上下文對象的名字,下麵兩種編寫方式都是可以實現的。
//方式一.使用this
invoker.whoInvokeMe = function(){
console.log(this.name);
}
//方式二.不使用this
function whoInvokeMe2(invoker){
console.log(invoker.name);
}
方式二的方式並不是語法錯誤,可以讓開發者避開了因為對this
關鍵字的誤用而引發的混亂,同樣也避開了this
所帶來的對代碼的抽象能力和簡潔性,同時會造成一些性能上的損失,畢竟這樣做會使得每次調用函數時需要處理更多的參數,而這些參數本可以通過內置的this
獲取到。
c.面向對象的編程
提到this,必然會提到另一個詞語——面向對象。"面向對象"是一種編程思想,請暫時拋開封裝,繼承,多態等高大上的修飾詞帶來的負擔,純粹地感受一下這種思想本身。有的人說"面向對象"賦予了編程一種哲學的意義,它是使用程式語言的方式對現實世界進行的一種簡化抽象,現實世界的一個用戶,一種策略,一個消息,某個演算法,在面向對象的世界里均將其視為一個對象,也就是哲學意義上的無分別
,每一個對象都有其生命周期,它怎麼來,要做什麼,如何消亡,以及它與萬物之間的聯繫。
面向對象
的思想,是用程式語言勾勒現實世界框架的方式之一,它的出現不是用來為難開發者的,而是為了讓開發者能以更貼近日常生活的認知方式來提升對程式語言的理解能力。
2.4 如果沒有this
我們來看一下如果javascript中不使用this
關鍵字,對程式編寫會造成什麼影響呢?
我們先來編寫一段簡單的定義代碼:
//假設我們定義一個人的類
function Person(name){
}
// 方法-介紹你自己(使用this編寫)
Person.prototype.introduceYourselfWithThis = function () {
if (Object.hasOwnProperty.call(this, 'name')) {
return `My name is ${this.name}`;
}
return `I have no name`;
}
// 方法-介紹你自己(不使用this編寫)
Person.prototype.introduceYourself = function (invoker) {
if (Object.hasOwnProperty.call(invoker, 'name')) {
return `My name is ${invoker.name}`;
}
return `I have no name`;
}
//生成兩個實例,併為各自的name屬性賦值
var liLei = new Person();
liLei.name = 'liLei';
var hanMeiMei = new Person();
hanMeiMei.name = 'hanMeiMei';
在上面的簡單示例中,我們定義了一個不包含任何實例屬性的人
類,並使用不同的方式為其定義介紹你自己這個方法,第一種定義使用常規的面向對象寫法,使用this
獲取上下文對象,獲取實例的name
屬性;第二種定義不使用this
,而是將調用者名稱作為參數傳遞進方法。
我們在控制台進行一些簡單的使用:
那麼這兩種不同的寫法區別到底是什麼呢?
- 函數實際功能的變化
從上面的示例中不難看出,當開發中不使用this時,需要開發者自行傳入上下文對象,並將其以參數的形式在函數執行時傳入,如果傳入的invoker 對象和 this的指向一致,那麼結果就一致,如果不一致,則會造成混亂。- 從編碼角度來看
introduceYourselfWithThis()
方法只是introduceYourself(invoker)
方法的特例(當this === invoker時)。
- 從方法的含義來看
定義者希望實現自我介紹功能而編寫了introduceYourself()
方法,可是使用者在閱讀到introduceYourself()
的源碼時看到的代碼表達的意義是:**我告訴你一個名字,你把它填在'My name is __'這句話中再返回給我。而不是一個與調用對象有著緊密聯繫的自我介紹**動作。
- 從編碼角度來看
畫蛇添足的參數傳遞
在正確的使用過程中,this 和 invoker 的指向是一致的,形參invoker的定義不僅增加了函數使用的複雜度,也增加了函數運行的負擔,卻沒有為函數的執行帶來任何新的附加信息。重覆的雷同代碼
如果編碼中不使用this
,也就相當於漢語中不使用代詞,那麼我們就需要在每一個獨立的句子中使用完整的信息。為了使introduceYourself()
方法能夠正確的執行,我們需要在每一個實例生成後,為其綁定確切的實例方法,即:
var liLei = new Person();
liLei.name = 'liLei';
//定義實例方法
liLei.introduceYourself = function (){
return `My name is liLei`;
};
var hanMeiMei = new Person();
hanMeiMei.name = 'hanMeiMei';
//定義實例方法
hanMeiMei.introduceYourself = function (){
return `My name is hanMeiMei`;
}
即時不使用
this
,你也不會直接陷入無法編寫javascript代碼的境地,只是需要將所有的定義和使用場景全部具體化, 需要手動對所有的具體功能編寫具體實現,也就是"面向過程"的編程。
================================我是華麗的分割線======================================
【輕鬆一刻】
話說赤壁之戰後,一日閑來無事,孔明與劉關張三兄弟一起喝酒。孔明說,我出三道題考考各位學識修養,如何啊?三兄弟舉手贊同。
孔明:第一題,主公,赤壁之戰發生在哪裡?
劉備:赤壁啊
孔明:答對了,主公果然厲害。第二題,關將軍,雙方有多少人參戰?
關羽:聯軍5萬,曹軍20餘萬。
孔明:答對了,關將軍也是智勇雙全啊。最後一題,他們分別是誰?
張飛:我......我靠
願你能夠掌握this
,不要在自己的代碼里搞出他們分別是誰的尷尬,小心被隊友活埋。
================================我是華麗的分割線======================================
三. this的一般指向規則
javascript中有四條關於this
指向的基本規則。今天,我們將一起通過【碼農視角】和【語文老師視角】來分別解讀這些規則,你會發現他們理解起來其實很自然。
規則1——作為函數調用時,this指向全局對象
瀏覽器中的全局對象,指的是window
對象。這一規則指的就是我們在全局作用域或者函數作用域中使用function
關鍵字直接聲明或使用函數表達式賦值給標識符的方式創建的函數。為了在調用時在記憶體中找到所聲明的方法,我們需要一個標識符來指向它的位置,具名函數可以通過它的名字找到,匿名函數則需要通過標識符來找到。作為函數調用的實質,就是通過方法名直或標識符找到函數並執行它。
一般什麼樣的函數我們會這樣定義呢?
就是那些不關註調用者的函數,比如上面舉例的addNumber()方法,這類函數往往是將一步或幾步業務邏輯組合在一起,起一個新的名字便於管理和重用,而並不關註使用者到底是誰。
語文老師解讀版:
很好理解,當你想描述一個動作卻不知道或者不關註具體是誰做的,代詞就指向有的人
。
比如臧克家同學在作文里寫的這樣:
有的人活著,但是他已經死了;
有的人死了,但是他還活著;
上文中的他指誰?指有的人;那有的人是誰?隨便,愛誰誰。
規則2——作為方法調用時,this指向上下文對象
上文中我們看到函數的作用域鏈上是包含Object
對象的,所以函數可以被當做對象來理解。當函數作為對象被賦值在另一個對象的屬性上時,這個對象的屬性值里會保存函數的地址,因為用函數作為賦值運算的右值時是一個引用類型賦值。如果這個函數正好又是一個匿名函數,那麼執行時只能通過對象屬性中記錄的地址信息來找到這個函數在記憶體中的位置,從而執行它。所以當函數作為方法調用時,this
中包含的信息的本質是這個函數執行時是怎麼被找查找到的。答案就是:通過this所指向的這個對象的屬性找到的。
一般什麼樣的函數我們會這樣定義呢?
作為方法定義的函數,往往是另一個抽象合集的具體實現。比如前例的addNumber()
這個方法,只是將兩個數字相加這樣一個抽象動作,至於是誰通過什麼方式來執行這個計算過程,無所謂,它可以概括所有對象將兩個數字相加並給出結果這一動作。可如果它作為一個對象方法來調用時,就有了更明確的現實指向意義:
Computer.addNumber()
表達了電腦通過軟硬體聯合作用而給出結果的過程Calculator.addNumber()
表達了計算器通過簡易硬體計算給出結果的過程Abacus.addNumber()
表達了算盤通過加減珠子的方式給出結果的過程- ...
語文老師解讀版:
當你想知道一個代詞具體指的是誰時,當然需要聯繫上下文語境進行理解。
規則3——作為構造函數使用時,this指向生成的實例
作為構造函數使用,就是new + 構造函數名的方式調用的情況。
js引擎在調用new操作符的邏輯可以用偽代碼表示為:
new Person('liLei') = {
//生成一個新的空對象
var obj = {};
//空對象的原型鏈指向構造函數的原型對象
obj.__proto__ = Person.prototype;
//使用call方法執行構造函數並顯式指定上下文對象為新生成的obj對象
var result = Person.call(obj,"liLei");
// 如果構造函數調用後返回一個對象,就return這個對象,否則return新生成的obj對象
return typeof result === 'object'? result : obj;
}
暫不考慮構造函數有返回值的情況,那麼很容易就可以明白this
為什麼指向實例了,因為類定義函數在執行的時候顯式地綁定了this為新生成的對象
,也就是調用new操作符後得到的實例對象。
語文老師解讀版:
有些同學喜歡抄襲,抄襲這個動作可以描述為:"把一份作業Copy一遍,在最後寫上自己的名字。"。如果李雷是喜歡抄襲的人之一,那麼他就掌握了"抄襲"這個方法,那你覺得他每次抄完作業後在署名的地方應該寫自己的名字"李雷"還是寫這一類人的總稱"喜歡抄襲的人"呢?
抬杠的那個同學,我記住你了!放學別走!
規則4——使用call/apply/bind方法顯式指定this
call
/bind
/apply
這三個方法是javascript動態性的重要組成部分,後續的篇章會有詳細的講解。這裡只看一下API用法,瞭解一下其對於this指向的影響:
- func.call(this, arg1, arg2...)
- func.apply(this, [arg1, arg2...])
- func.bind(this [, arg1[, arg2[, ...]]])
這個規則很好理解,就是說函數執行時遇到函數體里有this
的語句都用顯式指定的對象來替換。
語文老師解讀版:
就是直接告訴你下文中的代詞指什麼,比如:×××憲法(以下簡稱"本法"),那讀者當然就知道後面所說的"本法"指誰。
四. 基本規則示例
為了更清晰地看到上面兩條原則的區別,我們來看一個示例:
var heroIdentity = '[Function Version]Iron Man';
function checkIdentity(){
return this.heroIdentity;
}
var obj = {
name:'Tony Stark',
heroIdentity:'[Method Version]Iron Man',
checkIdentityFromObj:checkIdentity
}
function TheAvenger(name){
this.heroIdentity = name;
this.checkIdentityFromNew = checkIdentity;
}
var tony = new TheAvenger('[New Verison]Iron Man');
console.log('1.直接調用方法時結果為:',checkIdentity());
console.log('2.通過obj.checkIdentityFromObj調用同一個方法結果為:',obj.checkIdentityFromObj());
console.log('3.new操作符生成的對象:',tony.checkIdentityFromNew());
console.log('4.call方法顯示修改this指向:',checkIdentity.call({heroIdentity:'[Call Version]Iron Man'}));
控制台輸出的結果是這樣的:
同一個方法,同一個this,調用的方式不同,得到的結果也不同。
五. 後記
在基礎面前,一切技巧都是浮雲。
如果認為明白了this的基本規則就可以為所欲為,那你就真的too young too simple了。
瞭解了基本指向規則,只能讓你在開發中自己儘可能少挖坑或者不挖坑。但是想要填別人的坑或者讀懂大師級代碼中簡潔優雅的用法,還需要更多的修煉和反思。實際應用中許多複雜的使用場景是很難一下子搞明白this
的指向以及為什麼要指定this的指向的。
筆者將在《javascript基礎修煉(3)——What's this(下)》中詳細講述開發中千奇百怪的this
。欲知後事如何,先點個贊先吧!
參考文章:
[1].js中的new()到底做了什麼
[2].ECMA-262-3 in detail. Chapter 1. Execution Contexts