來梳理一下JavaScript中this的脈絡
一. this的概念
在Java中蜡饵,this的概念很明確:指的就是該類對象钠绍,并可以通過this來操縱對象屬性同欠,比如:
-
案例一
class A{ private int age; public int getAge(int age){ return this.age; } //...構造函數(shù)等 } //***** A a = new A(18).getAge(17); //result: 18
由于創(chuàng)建一個對象是一個開辟內(nèi)存空間的操作棍现,所以一個對象的this是不可以修改的调煎。甚至于我會冒出一句話:一個對象與它的this......
但是在JavaScript中,不同于Java中類與對象的概念己肮,它更加強調(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
為什么結果不是2? 可能我們第一個想到的問題是為什么不是2娄柳,而后才會去想為什么會是4寓辱。因為這里的this似乎更像是指向的myObject,而this.value也應該指向的是myObject.value赤拒。
想要解答這個問題秫筏,我們需要對JavaScript中的this進行更深層次的探討
二、函數(shù)中this的探討
2. 1 決定this對象綁定的因素
我們將以上代碼進行修改以更好的進行探討
-
案例三
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改變了!蕉朵,或者我們可以用一個更嚴謹?shù)恼Z言:this綁定的對象改變了崔涂!
這個現(xiàn)象向我們證明了一件事:函數(shù)中的this不是固定的,它不像Java中那樣在一個類中的this永遠指向創(chuàng)建它的對象始衅。結合我們上一篇作用域的知識冷蚂,JavaScript引擎的兩個階段:
- 編譯階段
- 執(zhí)行階段
可以得出結論:函數(shù)中this綁定對象的確定是在執(zhí)行階段!
所以函數(shù)中this的對象綁定必然和該函數(shù)的執(zhí)行密切相關汛闸。然而函數(shù)的執(zhí)行也遠遠不是我們所想的那般簡單蝙茶,但總結一下就是在哪如何被執(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)在我們對其進行嘗試來尋找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)用位置的重要性轴脐,因為綁定的契機是函數(shù)調(diào)用而不是函數(shù)聲明
當然執(zhí)行或者調(diào)用也尤為重要调卑,這也會引出我們后續(xù)會遇到的問題:多次的執(zhí)行或者調(diào)用函數(shù)會使得該函數(shù)this綁定的對象不斷改變,也就是this綁定對象的對象丟失問題大咱。
第二條因素:函數(shù)的調(diào)用方式
1. 獨立調(diào)用(默認綁定)
-
案例五
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
當然也有例外:在函數(shù)聲明使用嚴格模式的情況下厦瓢,獨立的函數(shù)調(diào)用this綁定的對象是undefined
-
案例六
function foo() { "use strict"; //在聲明中使用嚴格模式無法將this綁定到全局 console.log(this); // undefined console.log(this.a); //TypeError: Cannot read property 'a' of undefined } global a = 2; foo(); //報錯
我們將這種函數(shù)獨立調(diào)用的this綁定方式稱為:默認綁定
2. 被其它對象調(diào)用(隱式綁定)
首先我們可以向上觀察案例三提揍,當中的myObject.sum()
的操作后啤月,函數(shù)sum
的this被綁定到了myObject中,我們可以對這個代碼進行擴展來進行規(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綁定結果知道隱式綁定的綁定優(yōu)先級高于默認綁定,因為顯示sum
在這里其實也被體現(xiàn)了刨仑,但是最終的結果還是偏向于隱式綁定郑诺。
2.3 打破規(guī)律
1. 規(guī)律的本質(zhì)
事實上,以上我們所摸索出的規(guī)律也不過只是規(guī)律罷了杉武,如果我們探索其本質(zhì)不過還是一個內(nèi)存指針問題辙诞。
譬如:
默認綁定不過是因為它實際運行的區(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ù)不大于我們預先設定好的某個值則返回這個值。
我們很容易就能想到以下方案:
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ù)從一開始就是寫死的晰骑,這種代碼可以說是毫無靈活性。那么導致它失去靈活性的原因是什么绊序?我們來觀察下
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); //用法錯誤!S榘ぁ余指! } 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ù)組結構
//所以我們需要將它轉(zhuǎn)化成數(shù)組然后進行數(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ì),精簡了代碼邏輯贴届。如果你還有不懂靠粪,可以看我們對它的進一步剖析。
-
問題一的解答:
Array.prototype.slice.apply(arguments);
是如何將“類似數(shù)組”轉(zhuǎn)化成數(shù)組結構毫蚓?其實我們通過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
中關于this的妙用嗅虏,其實相應的call
也能達到相同的效果。
2. ES6的進階
其實綜合整個案例八上沐,最大的痛點還是這個arguments皮服,如果arguments從一開始就是個數(shù)組,我們也無需進行這么繁瑣的轉(zhuǎn)換數(shù)組操作了。
于是在ES6中有了對于函數(shù)的新擴展:rest參數(shù)與數(shù)組的擴展運算符
-
rest參數(shù)
ES6 引入 rest 參數(shù)(形式為
...變量名
)龄广,用于獲取函數(shù)的多余參數(shù)硫眯,這樣就不需要使用arguments
對象了。rest 參數(shù)搭配的變量是一個數(shù)組择同,該變量將多余的參數(shù)放入數(shù)組中两入。 -
數(shù)組的擴展運算符
擴展運算符(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ù)組的擴展運算符傳參 } //普通傳參 const result = getMax(1,2,3); //數(shù)組的擴展運算符傳參 const result = getMax(...[1,2,3]); //result賦值二選一 console.log((result)); // result:60
怎么樣紧武,是不是方便了很多剃氧。
2.4 this的補充
new 中的this綁定
可能你會覺得我前三種形式已經(jīng)把所有this綁定的情況說完了,但事實上阻星,不要忘了本質(zhì)朋鞍,this綁定的本質(zhì)在于函數(shù)在哪如何被執(zhí)行,構造函數(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)
我們將這個過程分為以下幾個步驟:
在我們對一個構造函數(shù)使用new關鍵字時,javaScript在執(zhí)行階段執(zhí)行到該語句時會根據(jù)這個函數(shù)創(chuàng)建一個對象呛讲。
隨后這個對象會和函數(shù)的原型進行連接。
隨后會把該構造函數(shù)調(diào)用的this指向該對象返奉,并執(zhí)行函數(shù)內(nèi)相應邏輯
構造函數(shù)將這個對象返回
由此贝搁,我們得以改變了構造函數(shù)中的this指向以達成自己構建對象的目的
而且由于我們在第二步中對象同函數(shù)進行了原型連接,所以在上述案例中被同構造函數(shù)構造出的對象都能共享introduce
方法芽偏,而不需要在每個對象中都去創(chuàng)建這個函數(shù)導致無謂的內(nèi)存損耗雷逆。
那么為什么普通變量沒有置于該函數(shù)的原型中呢?原因很簡單污尉,如果在這個個函數(shù)的原型中存放普通變量膀哲,那它就會成為一個所有對象的公有變量,但是問題在于被碗,由于每個對象都可以像獲取這個變量一樣去輕而易舉的改變這個變量某宪,以至于它也不能被當作一個公共常量存在。所以它存在的意義幾乎沒有什么意義锐朴。
那為什么上述中的函數(shù)hello
要置于構造函數(shù)的原型中呢兴喂?因為函數(shù)相對相比于一個變量而言更加靈活,事實上這也是封裝函數(shù)的意義所在,即:重復的邏輯衣迷,不同的結果畏鼓。