您好,登錄后才能下訂單哦!
開發者的javascript造詣取決于對【動態】和【異步】這兩個詞的理解水平。
this
是javascript關鍵字之一,是javascript能夠實現面向對象編程的核心概念。用得好能讓代碼優雅高端,風騷飄逸,用不好也絕對是坑人坑己利器。我們常常會在一些資料中看到對this
的描述是:
this
是一個特殊的與Execution Contexts相關的對象,用于指明當前代碼執行時的Execution Contexts,this
在語句執行進入一個Execution Contexts時被賦值,且在代碼執行過程中不可再改變。
注:Execution Contexts也就是我們常聽到的"上下文"或"執行環境"。
看不懂?看不懂就對了,我也看不懂。
對于this
的指向,我們常會聽到這樣一個原則——this是一個指針,指向當前調用它的對象。但實際使用中,我們卻發現有時候很難知道當前調用它的是哪個對象,從而引發了一系列的誤用和奇怪現象。
今天,我們就換一種思路,試試如何從語言的角度一步一步地去理解this
,你會發現:
只要你能聽懂中國話,就意味著你能理解this
javascript是一門程序設計語言,也就是說,它是一種語言,是語言,就有語法特性。如果拋開this
的原理和編程中的用法,僅從語文的層面去理解,它的本質就是代詞
。什么是代詞?漢語中的你
,我
,他
,你們
,我們
,他們
這一類的詞語就是代詞。代詞并不具體指某一個具體的事物,但結合上下文,就可以知道這類詞語代替的是誰。
比如下面這幾句描述的語境:
小結一下:
代詞,用于指代某個具體事物,當結合上下文時,就可以知道其具體的指向。換句話說,有了上下文時,代詞就有了具體的意義。
this
在javascript語言中的意義,就如同代詞
在漢語中的意義是一樣的。
在ES6出現前,javascript中的作用域只分為全局作用域和函數作用域兩種。(以下部分暫不討論嚴格模式)。
全局作用域中的this
是指向window對象
的,但window對象
上卻并沒有this
這個屬性:
函數作用域中的this
也是有指向的(本例中指向window對象
),我們知道函數的原型鏈是會指向Object
的,所以函數本身可以被當做一個對象來看待,但遺憾的是函數
的原型鏈上也沒有this
這個屬性:
綜上所述,this
可以直觀地理解為:
this與函數相關,是函數在運行時解釋器自動為其賦值的一個局部常量。
this
這是有可能發生的。很多初學者會發現,自己在編寫javascript代碼時并沒有用到this,但是也并不影響自己編寫代碼。前面提到過上下文信息的意義在于讓代詞明確其指向,那么如果一段話的上下文中并沒有使用代詞,在語文中我們就不需要聯系上下文就能理解這段話;同理,如果函數的函數體中并沒有使用this
關鍵字來指代任何對象,或者不需要關注其調用對象,那實際上就算不確定this
的指向,函數的執行過程也不會有歧義。
/**
*數據加工轉換類的函數,對開發者來說更關注結果,而并不在乎是誰在調用。
*/
function addNumber(a,b) {
return a + b;
}
無論是計算機對象調用addNumber方法,或是算盤對象調用addNumber方法,甚至是人類對象通過心算調用addNumber方法,都無所謂,因為我們關注的是結果,而不是它怎么來的。
this
有時候我們編寫的代碼是需要用到一些關于調用對象
的信息的,但由于不熟悉this
的用法,許多開發者使用了另一種變通的方式,也就是顯式傳參。比如我們在一個方法中,需要打出上下文對象的名字,下面兩種編寫方式都是可以實現的。
//方式一.使用this
invoker.whoInvokeMe = function(){
console.log(this.name);
}
//方式二.不使用this
function whoInvokeMe2(invoker){
console.log(invoker.name);
}
方式二的方式并不是語法錯誤,可以讓開發者避開了因為對this
關鍵字的誤用而引發的混亂,同樣也避開了this
所帶來的對代碼的抽象能力和簡潔性,同時會造成一些性能上的損失,畢竟這樣做會使得每次調用函數時需要處理更多的參數,而這些參數本可以通過內置的this
獲取到。
提到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
,不要在自己的代碼里搞出他們分別是誰的尷尬,小心被隊友活埋。
================================我是華麗的分割線======================================
javascript中有四條關于this
指向的基本規則。今天,我們將一起通過【碼農視角】和【語文老師視角】來分別解讀這些規則,你會發現他們理解起來其實很自然。
瀏覽器中的全局對象,指的是window
對象。這一規則指的就是我們在全局作用域或者函數作用域中使用function
關鍵字直接聲明或使用函數表達式賦值給標識符的方式創建的函數。為了在調用時在內存中找到所聲明的方法,我們需要一個標識符來指向它的位置,具名函數可以通過它的名字找到,匿名函數則需要通過標識符來找到。作為函數調用的實質,就是通過方法名直或標識符找到函數并執行它。
一般什么樣的函數我們會這樣定義呢?
就是那些不關注調用者的函數,比如上面舉例的addNumber()方法,這類函數往往是將一步或幾步業務邏輯組合在一起,起一個新的名字便于管理和重用,而并不關注使用者到底是誰。
語文老師解讀版:
很好理解,當你想描述一個動作卻不知道或者不關注具體是誰做的,代詞就指向有的人
。
比如臧克家同學在作文里寫的這樣:
有的人活著,但是他已經死了;
有的人死了,但是他還活著;
上文中的他指誰?指有的人;那有的人是誰?隨便,愛誰誰。
上文中我們看到函數的作用域鏈上是包含Object
對象的,所以函數可以被當做對象來理解。當函數作為對象被賦值在另一個對象的屬性上時,這個對象的屬性值里會保存函數的地址,因為用函數作為賦值運算的右值時是一個引用類型賦值。如果這個函數正好又是一個匿名函數,那么執行時只能通過對象屬性中記錄的地址信息來找到這個函數在內存中的位置,從而執行它。所以當函數作為方法調用時,this
中包含的信息的本質是這個函數執行時是怎么被找查找到的。答案就是:通過this所指向的這個對象的屬性找到的。
一般什么樣的函數我們會這樣定義呢?
作為方法定義的函數,往往是另一個抽象合集的具體實現。比如前例的addNumber()
這個方法,只是將兩個數字相加這樣一個抽象動作,至于是誰通過什么方式來執行這個計算過程,無所謂,它可以概括所有對象將兩個數字相加并給出結果這一動作。可如果它作為一個對象方法來調用時,就有了更明確的現實指向意義:
Computer.addNumber()
表達了計算機通過軟硬件聯合作用而給出結果的過程Calculator.addNumber()
表達了計算器通過簡易硬件計算給出結果的過程Abacus.addNumber()
表達了算盤通過加減珠子的方式給出結果的過程語文老師解讀版:
當你想知道一個代詞具體指的是誰時,當然需要聯系上下文語境進行理解。
作為構造函數使用,就是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一遍,在最后寫上自己的名字。"。如果李雷是喜歡抄襲的人之一,那么他就掌握了"抄襲"這個方法,那你覺得他每次抄完作業后在署名的地方應該寫自己的名字"李雷"還是寫這一類人的總稱"喜歡抄襲的人"呢?
抬杠的那個同學,我記住你了!放學別走!
call
/bind
/apply
這三個方法是javascript動態性的重要組成部分,后續的篇章會有詳細的講解。這里只看一下API用法,了解一下其對于this指向的影響:
這個規則很好理解,就是說函數執行時遇到函數體里有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
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。