來梳理一下JavaScript中this的脈絡(luò)
一. this的概念
在Java中,this的概念很明確:指的就是該類對象,并可以通過this來操縱對象屬性,比如:
-
案例一
class A{ private int age; public int getAge(int age){ return this.age; } //...構(gòu)造函數(shù)等 } //***** A a = new A(18).getAge(17); //result: 18由于創(chuàng)建一個對象是一個開辟內(nèi)存空間的操作,所以一個對象的this是不可以修改的。甚至于我會冒出一句話:一個對象與它的this......
但是在JavaScript中,不同于Java中類與對象的概念,它更加強(qiáng)調(diào)于函數(shù)與對象的概念,所以我們要探討的是函數(shù)中的this指向。
-
案例二
global.value = 2; var add = function(a, b) { return (a + b); }; var myObject = { value:1, sum: function() { // this.value: 1 function helper() { // this.value: 2 return add(this.value,this.value); } return helper(); } }; console.log(myObject.sum()); //result: 4為什么結(jié)果不是2? 可能我們第一個想到的問題是為什么不是2,而后才會去想為什么會是4。因為這里的this似乎更像是指向的myObject,而this.value也應(yīng)該指向的是myObject.value。
想要解答這個問題,我們需要對JavaScript中的this進(jìn)行更深層次的探討
二、函數(shù)中this的探討
2. 1 決定this對象綁定的因素
我們將以上代碼進(jìn)行修改以更好的進(jìn)行探討
-
案例三
global.value = 2; var add = function(a, b) { return (a + b); }; var myObject = { value:1, sum: function() { console.log(this.value); //this.value: 1 let that = this; function helper() { console.log(this.value); //this.value: 2 this改變了??!! return add(that.value,that.value); } return helper(); } }; console.log(myObject.sum()); //result: 2一個很重要的現(xiàn)象:this改變了!,或者我們可以用一個更嚴(yán)謹(jǐn)?shù)恼Z言:this綁定的對象改變了!
這個現(xiàn)象向我們證明了一件事:函數(shù)中的this不是固定的,它不像Java中那樣在一個類中的this永遠(yuǎn)指向創(chuàng)建它的對象。結(jié)合我們上一篇作用域的知識,JavaScript引擎的兩個階段:
- 編譯階段
- 執(zhí)行階段
可以得出結(jié)論:函數(shù)中this綁定對象的確定是在執(zhí)行階段!
所以函數(shù)中this的對象綁定必然和該函數(shù)的執(zhí)行密切相關(guān)。然而函數(shù)的執(zhí)行也遠(yuǎn)遠(yuǎn)不是我們所想的那般簡單,但總結(jié)一下就是在哪如何被執(zhí)行,將其拆解就是兩個重要信息:
-
函數(shù)的調(diào)用位置
在程序中的哪個位置執(zhí)行,或者說在哪個位置被調(diào)用?我們將這個執(zhí)行位置稱為函數(shù)調(diào)用位置。
-
函數(shù)的調(diào)用方式
函數(shù)是怎么被調(diào)用的?是獨立調(diào)用還是被其它對象調(diào)用?
2.2 尋找規(guī)律
我們已經(jīng)知道了this對象綁定的決定性因素,現(xiàn)在我們對其進(jìn)行嘗試來尋找this對象綁定的規(guī)律。
第一條因素:函數(shù)的調(diào)用位置
執(zhí)行是this綁定的先決條件,但是在哪調(diào)用也很重要,舉個例子
-
案例四
global.a = 2; global.b = 2; function sum() { return this.a + this.b; } console.log(sum()); //result: 4 global.a = 3; console.log(sum()); //result: 5可以看出調(diào)用位置的重要性,因為綁定的契機(jī)是函數(shù)調(diào)用而不是函數(shù)聲明
當(dāng)然執(zhí)行或者調(diào)用也尤為重要,這也會引出我們后續(xù)會遇到的問題:多次的執(zhí)行或者調(diào)用函數(shù)會使得該函數(shù)this綁定的對象不斷改變,也就是this綁定對象的對象丟失問題。
第二條因素:函數(shù)的調(diào)用方式
1. 獨立調(diào)用(默認(rèn)綁定)
-
案例五
lobal.a = 2; global.b = 2; function sum() { return this.a+this.b; } console.log(sum()); //result: 4 //this綁定的對象是全局global!或許單看這個案例感受并不明顯,因為沒有其它元素的干擾,我們可以向上觀察案例三,"單節(jié)點"
helper()執(zhí)行時this綁定的對象也是全局global。
由此我們可以得出:獨立的函數(shù)調(diào)用this綁定的對象是全局global
當(dāng)然也有例外:在函數(shù)聲明使用嚴(yán)格模式的情況下,獨立的函數(shù)調(diào)用this綁定的對象是undefined
-
案例六
function foo() { "use strict"; //在聲明中使用嚴(yán)格模式無法將this綁定到全局 console.log(this); // undefined console.log(this.a); //TypeError: Cannot read property 'a' of undefined } global a = 2; foo(); //報錯
我們將這種函數(shù)獨立調(diào)用的this綁定方式稱為:默認(rèn)綁定
2. 被其它對象調(diào)用(隱式綁定)
首先我們可以向上觀察案例三,當(dāng)中的myObject.sum()的操作后,函數(shù)sum的this被綁定到了myObject中,我們可以對這個代碼進(jìn)行擴(kuò)展來進(jìn)行規(guī)律探索
-
案例七
global.value = 2; const add = function(a, b) { return (a + b); }; const inner = { value: 1, sum: function() { // this.value: 1 return add(this.value, this.value); } }; const outer = { value: 10, // this.value: 10 inner: inner }; console.log(outer.inner.sum()); //result: 2可以看到最終函數(shù)
sum中的this還是綁定到了對象inner上。
由以上可以得出:被其它對象調(diào)用的函數(shù)會將該函數(shù)的this綁定到調(diào)用它的對象。
我們將這種this綁定方式稱為:隱式綁定
而且我們也可以由outer.inner.sum()的this綁定結(jié)果知道隱式綁定的綁定優(yōu)先級高于默認(rèn)綁定,因為顯示sum在這里其實也被體現(xiàn)了,但是最終的結(jié)果還是偏向于隱式綁定。
2.3 打破規(guī)律
1. 規(guī)律的本質(zhì)
事實上,以上我們所摸索出的規(guī)律也不過只是規(guī)律罷了,如果我們探索其本質(zhì)不過還是一個內(nèi)存指針問題。
譬如:
默認(rèn)綁定不過是因為它實際運行的區(qū)域是在全局,所以this指向的也是全局地址。
隱式綁定不過是因為它是被一個對象調(diào)用,運行的區(qū)域在對象,所以this指向的也是對象地址。
2. 使用apply和call打破規(guī)律(顯示綁定)
-
函數(shù)
call的官方定義:function.call(thisArg, arg1, arg2, ...)thisArg:可選的:在function函數(shù)運行時使用的this值。arg1, arg2, ...:指定的參數(shù)列表。 -
函數(shù)
apply的官方定義:func.apply(thisArg, [argsArray])thisArg:必選的。在func函數(shù)運行時使用的this值。argsArray:可選的。一個數(shù)組或者類數(shù)組對象,其中的數(shù)組元素將作為單獨的參數(shù)傳給func函數(shù)。
由于我們可以明確的指定this綁定的對象,所以它又稱為顯示綁定。
那么call和apply這么做的目的是什么?難道是為了去修改this綁定而去修改this綁定?這顯然是不合理的。
事實上它是有實際的存在意義的。不過在介紹意義之前我們得介紹一個概念:
“類似數(shù)組”arguments
函數(shù)被調(diào)用時,會獲得一個“免費”配送的參數(shù)=>“類似數(shù)組”arguments,它接收了該函數(shù)的參數(shù)列表里的所有參數(shù),并存有參數(shù)長度length。我們可以通過arguments來訪問這些參數(shù)。
-
案例八
現(xiàn)在我們要根據(jù)argumens來設(shè)計一個函數(shù),這個函數(shù)的功能是:返回傳入的最大數(shù),如果這個數(shù)不大于我們預(yù)先設(shè)定好的某個值則返回這個值。
我們很容易就能想到以下方案:
function getMax() { const Min_Max = 60; const result = Math.max(1, 2, 3); if (result <= Min_Max) { return Min_Max; } else { return result; } } console.log(getMax()); //result: 60問題這甚至連個健康的代碼都算不上!因為它的輸入?yún)?shù)從一開始就是寫死的,這種代碼可以說是毫無靈活性。那么導(dǎo)致它失去靈活性的原因是什么?我們來觀察下
Math.max的官方定義:Math.max(value1[,value2, ...])value1, value2, ...:一組數(shù)值可以看到,它的參數(shù)只能是一個一個的單個數(shù)值,我們可以試想一下,如果
Math.max能接收數(shù)組參數(shù)并返回該數(shù)組內(nèi)的最大值。那是不是能提高代碼質(zhì)量,舉個錯誤的例子:// 此為錯誤代碼?。?! 僅舉例衍生 function getMax() { const Min_Max = 60; const arr = new Array(arguments); //用法錯誤?。。? arr.push(Min_Max); return Math.max(arr); //用法錯誤?。?! } console.log(getMax(1,2,3)); //result: 60上述代碼語法層面是錯誤的,但卻代表了我們的美好展望,因為從這和前一個代碼比較起來簡直靈活很多了。要實現(xiàn)這個美好展望,我們需要解決兩個問題:
- arguments 如何轉(zhuǎn)化為數(shù)組
Math.max如何參數(shù)接收數(shù)組
幸運的是,apply能夠解決這兩個問題:
function getMax() {
const Min_Max = 60;
//因為arguments是一個“類似數(shù)組”而不是一個數(shù)組結(jié)構(gòu)
//所以我們需要將它轉(zhuǎn)化成數(shù)組然后進(jìn)行數(shù)組操作
const arr = Array.prototype.slice.apply(arguments); //arguments:1,2,3
arr.push(Min_Max);
return Math.max.apply(this,arr);
}
console.log(getMax(1,2,3)); //result: 60
這里apply的作用顯示的淋漓盡致。極大的利用到了函數(shù)Math.max本身的特質(zhì),精簡了代碼邏輯。如果你還有不懂,可以看我們對它的進(jìn)一步剖析。
-
問題一的解答:
Array.prototype.slice.apply(arguments);是如何將“類似數(shù)組”轉(zhuǎn)化成數(shù)組結(jié)構(gòu)?其實我們通過2.3中apply的定義就已經(jīng)知道apply會將函數(shù)slice中的this綁定到arguments上,但是僅僅這些我們可能還是不太能理解這個過程。對此,我自己實現(xiàn)了一下函數(shù)
slice:(源碼與此有很大不同,點此鏈接查看源碼)Array.prototype._slice = function(start, end) { var result = new Array(); start = start || 0; end = end || this.length; for (let i = start; i < end; i++) { result.push(this[i]); } return result; }; let arr = [1,2,3,4]; console.log(arr._slice(2)); // result: [3,4]怎么樣,現(xiàn)在是不是就很好理解了,其實整個的轉(zhuǎn)換數(shù)組分兩個步驟:
將新數(shù)組中的this綁定到arguments
遍歷this(也就是arguments)中的變量生成新數(shù)組
-
問題二的解答:
得益于
apply的定義,apply直接就能將數(shù)組向下傳遞給max的arguments,所以這個問題也迎刃而解。
這就是apply中關(guān)于this的妙用,其實相應(yīng)的call也能達(dá)到相同的效果。
2. ES6的進(jìn)階
其實綜合整個案例八,最大的痛點還是這個arguments,如果arguments從一開始就是個數(shù)組,我們也無需進(jìn)行這么繁瑣的轉(zhuǎn)換數(shù)組操作了。
于是在ES6中有了對于函數(shù)的新擴(kuò)展:rest參數(shù)與數(shù)組的擴(kuò)展運算符
-
rest參數(shù)
ES6 引入 rest 參數(shù)(形式為
...變量名),用于獲取函數(shù)的多余參數(shù),這樣就不需要使用arguments對象了。rest 參數(shù)搭配的變量是一個數(shù)組,該變量將多余的參數(shù)放入數(shù)組中。 -
數(shù)組的擴(kuò)展運算符
擴(kuò)展運算符(spread)是三個點(
...)。它好比 rest 參數(shù)的逆運算,將一個數(shù)組轉(zhuǎn)為用逗號分隔的參數(shù)序列。現(xiàn)在我們來重寫一下案例八:
-
案例九
function getMax(...args) { //...args 為rest參數(shù),傳入時直接為數(shù)組 const Min_Max = 60; args.push(Min_Max); return Math.max(...args);//數(shù)組的擴(kuò)展運算符傳參 } //普通傳參 const result = getMax(1,2,3); //數(shù)組的擴(kuò)展運算符傳參 const result = getMax(...[1,2,3]); //result賦值二選一 console.log((result)); // result:60怎么樣,是不是方便了很多。
2.4 this的補(bǔ)充
new 中的this綁定
可能你會覺得我前三種形式已經(jīng)把所有this綁定的情況說完了,但事實上,不要忘了本質(zhì),this綁定的本質(zhì)在于函數(shù)在哪如何被執(zhí)行,構(gòu)造函數(shù)的調(diào)用也屬于這個范疇。并且這也是我們生活中普遍用到的一種this綁定方式
-
案例十
function hello() { console.log(`hello${this.name}`); } function Obj(name){ this.name = name; } Obj.prototype.intorduce = hello; const pig = new Obj('大哥'); const dog = new Obj('小弟'); pig.intorduce(); // hello大哥 dog.intorduce(); // hello小弟沒錯就是這樣,可能乍一看會很容易理解,并且使用上也不會出現(xiàn)紕漏,但其實在這個new的過程中會涉及到一些JavaScript對象原型的知識。
比如說上述:
const person = new Obj('大哥',hello)我們將這個過程分為以下幾個步驟:
在我們對一個構(gòu)造函數(shù)使用new關(guān)鍵字時,javaScript在執(zhí)行階段執(zhí)行到該語句時會根據(jù)這個函數(shù)創(chuàng)建一個對象。
隨后這個對象會和函數(shù)的原型進(jìn)行連接。
隨后會把該構(gòu)造函數(shù)調(diào)用的this指向該對象,并執(zhí)行函數(shù)內(nèi)相應(yīng)邏輯
構(gòu)造函數(shù)將這個對象返回
由此,我們得以改變了構(gòu)造函數(shù)中的this指向以達(dá)成自己構(gòu)建對象的目的
而且由于我們在第二步中對象同函數(shù)進(jìn)行了原型連接,所以在上述案例中被同構(gòu)造函數(shù)構(gòu)造出的對象都能共享introduce方法,而不需要在每個對象中都去創(chuàng)建這個函數(shù)導(dǎo)致無謂的內(nèi)存損耗。
那么為什么普通變量沒有置于該函數(shù)的原型中呢?原因很簡單,如果在這個個函數(shù)的原型中存放普通變量,那它就會成為一個所有對象的公有變量,但是問題在于,由于每個對象都可以像獲取這個變量一樣去輕而易舉的改變這個變量,以至于它也不能被當(dāng)作一個公共常量存在。所以它存在的意義幾乎沒有什么意義。
那為什么上述中的函數(shù)hello要置于構(gòu)造函數(shù)的原型中呢?因為函數(shù)相對相比于一個變量而言更加靈活,事實上這也是封裝函數(shù)的意義所在,即:重復(fù)的邏輯,不同的結(jié)果。