深入理解JavaScript之this全面解析
在之前的章節裡我們知道,this 是在函式執行時繫結的,它只與函式在哪被呼叫有關係
1.1 呼叫位置
在理解 this 的繫結之前,我們先理解 this 的呼叫位置,
呼叫位置就是函式在程式碼中被呼叫的位置(而不是宣告位置)
通常來說,尋找呼叫位置就是尋找“函式被呼叫的位置”,看起來很簡單,但是某些程式設計模式會隱藏真正的呼叫位置。
尋找呼叫位置,實際上就是尋找分析呼叫棧(就是為了到達當前執行的函式位置所呼叫的所有函式)
我們所關心的呼叫位置就在當前正在執行的函式的前一個呼叫中
下面看個例子
function baz() { //當前函式位置是baz //呼叫棧:全域性-->baz //呼叫位置是當前函式位置的上一個位置,全域性作用域呼叫了baz(),所以呼叫位置是全域性作用域 console.log("baz"); bar(); //bar函式被呼叫 } function bar() { //當前函式位置是bar //呼叫棧:全域性-->baz-->bar //呼叫位置是當前位置的上一個位置,baz()呼叫了bar(),所以呼叫位置是baz() console.log("bar"); foo(); //foo函式被呼叫 } function foo() { //當前函式位置是foo //呼叫棧:全域性-->baz-->bar-->foo //呼叫位置是當前位置的上一個位置,bar()呼叫了foo(),所以呼叫位置是bar() console.log("foo"); } baz(); //baz被呼叫
一般而言,都是從全域性作用域中逐一推斷出呼叫棧的位置。
可以將呼叫棧想象成一個函式呼叫鏈,就像我們在上面程式碼裡面分析的那樣,但是這種方法非常麻煩並且容易出錯,,另一個檢視呼叫棧的方法是通過瀏覽器的除錯工具。當今絕大多數瀏覽器都內建了開發者工具,其中包括JavaScript偵錯程式。
就本例來說,你可以在foo(...)中設定一個斷點,或者直接在foo(...)中第一行程式碼中插入一個debugger語句,在執行程式碼時,偵錯程式會在那個位置暫停,同時會顯示當前位置的函式呼叫列表,這就是你的呼叫棧,然後找到棧中第二個元素,這就是真正的呼叫位置。
下面我們就chrome瀏覽器為例,看看如何設定斷點,並檢視呼叫棧。
1、用chrome開啟需要除錯的JS頁面
2、按下F12,開啟“開發者工具”
3、點選開發者工具中的"sources"開啟需要除錯的JS頁面
4、滑鼠單擊,程式碼行號就能設定斷點了,如下圖我們在19行處的bar(...)函式中設定了斷點,程式執行到19行處便會停止。
5、點選 "call stack" 檢視當前設定斷點函式的呼叫棧(注意是當前函式的上一個函式才是此函式的呼叫位置)如下圖,我們在bar(...)處設定了斷點,"call stack"中顯示了三個呼叫棧,從下到上依次呼叫,有一個藍色圖示的是當前設定了斷點的函式,上一個函式baz(...)才是呼叫bar(...)的呼叫位置
1.2 繫結規則
我們接下來看看,在函式的執行過程中呼叫位置是如何決定 this 的繫結物件的。
在JavaScript中有四個 this 的繫結規則,但是,應用哪一條規則要看 this函式 的呼叫位置。
1.2.1 預設繫結
首先要介紹的是最常用的函式呼叫型別:獨立函式呼叫。可以把這條規則看作是當其他規則無法應用時才會應用的一條規則。
看看下面的程式碼
function foo() {
console.log(this.a);
}
var a=2;
foo(); //2
你應該注意到的一件事是
宣告在全域性作用域中的變數(此例中是 var a=2)是全域性物件的一個同名屬性
接下來我們看到當呼叫了 foo(...) 函式時,this.a 被解釋成了全域性變數a 。這是為什麼? 因為在本例中,函式呼叫時應用了 this 的預設繫結,因此 this 指向了全域性物件。
那麼我們如何知道此處是應用了 this 的預設繫結規則呢?
我們首先來看看,foo函式的呼叫棧,通過分析我們可以得出,是全域性物件呼叫了foo(...)函式,程式碼中的foo(...)是直接使用不帶任何修飾的函式引用進行呼叫的。因此只能使用預設繫結,而不能使用其他規則
如果使用嚴格模式"strict mode",那麼全域性物件將無法應用預設繫結, this會繫結到"undefined"
function foo() {
"use strict";
console.log(this.a);
}
var a=2;
foo(); //TypeError:this is undefined
預設繫結規則:
非嚴格模式下-----------------------------------------發生獨立函式呼叫(函式直接使用不帶任何修飾的函式引用進行呼叫)且呼叫位置是全域性作用域時,this 繫結全域性物件。
嚴格模式(strict more)下----------------------------this 與函式呼叫位置無關,不適用預設繫結規則
function foo() {
console.log(this.a);
}
var a=2;
(function () {
"use strict"
foo(); //2
})();
注意:通常來說JavaScript程式碼中要麼非嚴格,要麼嚴格,不提倡有一些嚴格,有一些不嚴格,但是在引用類庫中,可能會遇到,非嚴格與嚴格並存的情況,這個時候要注意到這種相容性的小細節
1.2.2 隱式繫結
另一條需要考慮的是,呼叫位置是否存在上下文物件。或者說 this函式 是否被某個物件擁有或者包含。這個說法不夠嚴謹。
我們舉個例子來說明
function foo() {
console.log(this.a);
}
var obj={
a:2,
foo:foo
}
obj.foo();
首先要注意的是,foo(...)的宣告方式,及其是如何被新增為obj的引用屬性的。但是無論是直接在obj中定義還是先定義再新增為引用屬性,都改變不了foo(...)嚴格來說不屬於obj物件。
然而呼叫位置會使用 obj 的上下文來引用函式,因此你可以說函式被呼叫時 obj 物件包含或者擁有它。
無論你怎麼稱呼這個模式,當foo(...)函式被呼叫時,它的落腳點確實指向 obj 物件。當函式引用有上下文物件時,隱式繫結規則會把函式呼叫中的 this 繫結到這個上下文物件。因為呼叫foo(...)時 this 被繫結到obj,因此this.a和obj.a是一樣的。
那假如它有多個上下文物件呢?比如這個函式處在一個物件屬性引用鏈中
物件屬性引用鏈只有最頂層或者說最後一層會呼叫位置
看下面的例子
function foo() {
console.log(this.a);
}
var obj2={
a:3,
foo:foo
};
var obj1={
a:2,
obj2:obj2
};
obj1.obj2.foo(); //結果為3,物件屬性引用鏈
foo被obj2呼叫,但是引用方式卻是通過物件屬性引用鏈來實現的,"obj1.obj2.foo"。按照我們的隱式繫結規則來看,this 只會繫結最後一層的物件,於是此例中 this 與obj2 進行了繫結,輸出的為 obj2 中的a
存在的問題:隱式丟失---------被隱式繫結的函式會丟失繫結物件,它會應用預設繫結從而把this繫結到全域性物件(非嚴格模式)或者undefined(嚴格模式)上
看下面的一段程式碼
function foo() {
console.log(this.a);
}
var obj={
a:2,
foo:foo
};
var bar=obj.foo; //函式別名,將foo()賦給了bar()
var a=3;
bar(); //3,此處bar()沒有新增別的修飾詞,是獨立函式呼叫,所以應用預設繫結。發生了隱式丟失
我們剛開始時讓foo()函式隱式繫結obj,但是當"var bar=obj.foo"時,bar 就已經算obj.foo的另一種引用了,bar引用的是foo()函式的本身,那麼此時的"bar()"其實是一種不帶任何修飾的函式呼叫,因此發生了預設繫結。輸出全域性物件a的值
另一種更加隱蔽的隱式丟失發生在:傳遞引數期間也會發生隱式丟失----------即回撥函式傳入引數時
function foo() {
console.log(this.a);
}
var obj={
a:2,
foo:foo
};
function doFoo(fn) { //foo被當成引數傳入
fn(); //foo被呼叫
}
var a=3;
doFoo(obj.foo); //傳入的是obj.foo,引數傳遞中發生了隱式丟失
這裡同樣發生了隱式丟失,且是在引數傳遞的過程中發生了隱式丟失,doFoo(obj.foo)傳入的同樣是foo()函式本身,而且在呼叫時,是不帶有任何裝飾(上下文物件)的呼叫,因此 this 應用預設繫結。
如果把函傳入語言的內建函式而不是你定義的函式,比如傳入setTimeout(),會怎樣?
結果也是一樣的。
function foo() {
console.log(this.a);
}
var obj={
a:2,
foo:foo
};
var a=3;
setTimeout(obj.foo,0); //setTimeout傳入的是foo函式本身,且是不帶任何裝飾的呼叫,因此是預設繫結
setTimeout()函式和以下的虛擬碼類似
function setTimeout(fn,delay){
.....//delay延時
fn(); //呼叫fn
}
就如同這個函式被定義在之前的程式碼裡執行的一樣。
實際上使用回撥函式時,丟失this的繫結是非常常見的,你無法控制回撥函式的執行方式,因此就沒有辦法控制會影響繫結的呼叫位置,但是可以通過固定this解決這個問題。
1.2.3 顯示繫結
在之前我們講到了隱式繫結,在分析隱式繫結時,我們必須在一個物件內部包含一個指向函式的屬性,並通過這個屬性間接引用函式,從而把這個 this 間接(隱式)繫結到這個物件上
既然有了隱式繫結,那麼就有顯示繫結,就像當我們不想在物件內部包含函式引用(在物件內部,建立此函式屬性),想要在某個物件上強制呼叫該函式,這就是隱式繫結。
JavaScript中的“所有”函式都有一些有用的特性(原型),可以用來解決這個問題,具體點來說是使用我們之前接觸過的"call(...)"---強制繫結this物件,以及"apply(...)"方法。
嚴格來說,JavaScript的宿主環境有時會提供一些非常特別的函式,它們並沒有這兩個方法(就是某一些函式用不了這兩個方法),但是這種函式非常罕見,JavaScript大多數函式都可以使用"call(...)"和"apply(...)"方法
接下來我們看看這兩個函式是如何工作的
函式.方法名(物件)
它們第一個引數是物件,它們會把這個物件繫結到this,接著在呼叫函式時指定這個this。如此便將此函式的this和物件繫結到了一起,這就是顯示繫結的由來
call與apply的作用相同,區別在於,兩者傳入的引數不同
看下面的程式碼
function foo() {
console.log(this.a);
}
var obj={
a:2
};
var a=3;
foo.call(obj); //2,call將foo函式中的this顯示繫結到obj物件
通過foo.call(...)我們在呼叫foo的時候強制把它的this繫結到obj上。
如果你正在call(...)中傳入的不是一個物件,而是一個原始值(字串型別、布林型別或者數字型別)來當做this的繫結物件,這個物件會轉化成它的物件形式(也就是new String(...)、new Boolean(...)或者new Number(...))-------------------原始值物件轉化為它的物件形式稱之為“裝箱”
儘管顯式繫結很牛逼!!!但是在我們不清楚繫結物件時,仍然無法解決我們的隱式繫結丟失的問題
但是顯式繫結的一個變種可以解決這個問題
1 硬繫結
思考以下程式碼
function foo() {
console.log(this.a);
}
var obj={
a:2
};
var bar=function () {
foo.call(obj); //在bar()中使用call方法將obj物件練到了一起
}
bar(); //2
setTimeout(bar(),10) //2
bar.call(window); //2
在bar(...)函式中將foo函式顯式繫結在obj中,無論bar(...)在哪被呼叫,它的內部都在使foo(...)顯式繫結 obj 物件。因此無論如何呼叫bar(...),它繫結的物件都在obj中。這就是硬繫結
硬繫結:隱式丟失的主要原因是在隱式繫結的過程中,我們this的物件發生了改變,那麼我們只需要將this在呼叫之前提前繫結到合適的物件就行。
硬繫結典型的應用場景:
- 建立包裹函式:傳入所有的引數並返回接收到的所有值
function foo(something) {
console.log(this.a,something); //輸出,輸入的引數
return this.a+something; //返回接收到的引數,值為a+引數
}
var obj={
a:2
};
var bar=function () { //建立包裹函式
return foo.apply(obj,arguments);
};
var b=bar(3); //2 3
console.log(b); //5(2+3)
所謂包裹函式其實就是封裝函式,把該函式重要的內容封裝起來,只需要知道怎麼用即可,使用者接觸不到核心的函式(foo(...)以及"obj")。
如上圖中包裹函式為bar(...),使用者真正接觸到的只有bar(...)而沒有foo(...)以及"obj",使用者只需要知道bar(...)的用法,傳入的引數即可。
- 建立i可以重複使用的輔助函式:
function foo(something) {
console.log(this.a,something); //輸出,輸入的引數
return this.a+something; //返回接收到的引數,值為a+引數
}
function bind(fn,obj) { //建立了捆綁函式,使傳入的函式fn與物件obj實現硬繫結
return function () {
return fn.apply(obj,arguments); //實現了硬繫結
}
}
var obj={
a:2
};
var bar=bind(foo,obj);
var b=bar(3); //2 3
console.log(b); //5
由於在硬繫結是非常常用的模式,因此在ES5中一共了內建的方法:Function.prototype.bind,它的用法和用法跟我們上個程式碼中建立的捆綁函式"bind(...)"類似
function foo(something) {
console.log(this.a,something); //輸出,輸入的引數
return this.a+something; //返回接收到的引數,值為a+引數
}
var obj={
a:2
};
var bar=foo.bind(obj); //函式.bind(this的繫結物件)
var b=bar(3); //2 3
console.log(b); //5
bind(...)會返回一個硬編碼的新函式,它會把引數(obj)設定為this的上下文物件並呼叫原始函式(foo)。
bind(...)還有一個非常重要的作用便是實現柯里化:bind(...)能把除了第一個引數(用於繫結this)之外的引數傳遞給下層的函式
2 API呼叫的上下文
第三方類庫的許多函式,以及JavaScript語言和宿主環境中有許多新的內建函式,都提供了一個可選的引數,通常稱之為“上下文”,其作用和bind(...)一樣,確保你的回撥函式使用指定的this。
舉例來說:
function foo(el,id) {
console.log(el,this.id);
};
var obj={
id:"awesome"
};
[1,2,3].forEach(foo,obj); //傳入的引數.forEach(函式,物件);
//1 awesome
//2 awesome
//3 awesome
1.2.4 new繫結
這是最後一條規則“new繫結規則”,在講解這條規則之前,首先讓我們澄清一個非常常見的關於javascript中函式和物件的誤解。
在傳統的面向物件的語言中,“建構函式”是類中的一些特殊方法,使用 new 初始化類時會呼叫類中的建構函式。通常的形式是這樣的:
something = new Myclass(...);
在就JavaScript中也有一個 new 操作符,使用方法看起來也跟面向物件的語言一樣,學過面嚮物件語言的開發者會認為這個機制跟面向物件中的機制一樣,然而兩者天差地別。
為什麼呢???
首先我們先理解一下JavaScript中的建構函式
- 在JavaScript中建構函式只是一個普通的函式
- 它們不屬於某個類,也不會例項某個類
- 在使用 new 操作會呼叫的普通函式
舉例說明:
ES5.1中是這麼描述Number(...)作為建構函式的
15.7.2 Numbar建構函式
當 Numbar 物件在 new 表示式中被呼叫時,它是一個建構函式:它會初始化新建立的物件
所以包括內建物件函式(比如Numbar(...))在內的所有函式都可以用new來呼叫,這種函式稱為建構函式呼叫
也就是說:在JavaScript中沒有所謂的“建構函式”,只有對於函式的“構造呼叫”
我們來分析一下使用 new 呼叫函式----發生函式“構造呼叫”的過程
- 建立(構造)一個全新的物件
- 這個物件會被執行“原型”連線
- 這個物件會被繫結到函式呼叫的this
- 如果函式中沒有返回其他物件,那麼 new 表示式中的函式呼叫會自動返回這個新物件。
舉例說明使用 new 呼叫函式的過程。
function foo(a) {
this.a=a;
}
var bar=new foo(2);
console.log(bar.a);
使用 new 來呼叫foo(...)函式時,我們會構造一個新物件並把它繫結到foo(...)中呼叫的 this 上去---------------這就是new繫結
若還有不明白的 new 可在https://www.cnblogs.com/faith3/p/6209741.html中參考
1.3 優先順序
我們已經介紹完了四條this繫結的規則:
- 預設繫結----------不帶有任何修飾的獨立函式呼叫
- 隱式繫結----------帶有上下文物件的呼叫(方法呼叫)
- 顯式繫結----------call、apply以及API顯式呼叫(間接呼叫)
- new繫結-----------new構造呼叫
那麼如果符合多條應用規則,我們該用哪條規則呢?
首先我們不用考慮“預設繫結”,這肯定是在後的了,只有在不符合其餘三條規則下,才會應用“預設規則”
接下來我們比較下“隱式繫結”與“顯式繫結”的優先順序,誰更高呢?
看程式碼:
function foo() {
console.log(this.a);
}
var obj1={
a:2,
foo:foo
};
var obj2={
a:3,
foo:foo
};
//隱式繫結開始
obj1.foo(); //2
obj2.foo(); //3
//應用隱式繫結,又用顯示繫結
obj1.foo.call(obj2); //3
obj2.foo.call(obj1); //2
我們建立了foo(...)函式,其中有 this ,“obj1”和“obj2”物件,他們中的"a"值分別為2以及3。我們首先應用了隱式繫結,可以看出輸出結果正確,接下來我們用上隱式繫結"帶有上下文的引用"再用顯式繫結"call方法呼叫",很明顯“obj1”輸出的是“obj2”的值,這正是顯式呼叫比隱式呼叫優先順序高的最有利證據。
顯式繫結比隱式繫結優先順序高
接下來我們需要搞清楚,new繫結和隱式繫結,哪個優先順序更高。
看接下來的程式碼
function foo(something) {
this.a=something; //將穿傳入的引數變成a屬性
}
var obj1={
foo:foo
};
var obj2={};
obj1.foo(2);
console.log(obj1.a); //2
obj1.foo.call(obj2,3); //顯式繫結
console.log(obj2.a); //3
var bar=new obj1.foo(4);
console.log(obj1.a); //2 隱式繫結
console.log(bar.a); //4 new繫結將隱式繫結的a改變了
從中可以看出
new繫結比隱式繫結優先順序更高。
那麼new繫結與顯式繫結,誰的優先順序更高呢?
new與call/apply無法同時使用,因此無法通過new foo.call(obj1)來直接進行測試,但是我們可以使用硬繫結來測試它們的優先順序
在看接下來的程式碼時,讓我們來回憶,硬繫結是如何工作的?Function.prototype.bind(...)會建立一個新的包裝函式,這個函式會忽略它當前的 this 繫結(無論繫結的物件是什麼),並把我們提供的物件繫結到 this 上。
接下來我們通過程式碼來看看,new和硬繫結誰的優先順序更高。
function foo(something) {
this.a=something; //將穿傳入的引數變成a屬性
}
var obj1={};
var bar=foo.bind(obj1); //在bar中我們將obj1與foo物件強制繫結到了一起
bar(2);
console.log(obj1.a); //2
//按道理來講,使用bar作為構建呼叫時,obj1也應該和foo繫結到一起
var baz=new bar(3);
console.log(obj1.a); //2 obj1.a的值沒有改變,證明硬繫結被解開了
console.log(baz.a); //3 證明產生的新物件將硬繫結的this解開指向了新物件,否則obj1.a=3
出乎意料!bar被硬繫結到obj1上,但是new bar(3)並沒有想我們預計(new繫結的優先順序比硬繫結的優先順序低)的那樣改變obj1.a的值為3。相反new繫結修改了硬繫結(到obj1的)呼叫bar(...)中的this,因為使用了new繫結,我們得到一個名字為baz的新物件,修改了baz.a=3。這也就意味著
new繫結比硬繫結優先級別更高
new看起來無法修改硬繫結的this,然而事實卻不一樣,在JavaScript中,會首先判斷硬繫結函式是否被new呼叫,如果是的話就會使用新建立的this替換硬繫結的this。
那麼為什麼要在new中使用硬繫結呢?這是因為在new中使用bind(...)函式可以將傳入的引數,除了物件之外,傳遞給下一層的函式。
舉例:
function foo(p1,p2) {
this.val=p1+p2;
}
//之所以用null,是因為此處我們不用管this硬繫結的物件
//反正不管是什麼,this的值都會被修改
var bar=foo.bind(null,"p1");
var baz=new bar("p2");
baz.val; //p1p2
1.4 this的引用順序
現在我們可以根據之前的結論來判斷this的繫結順序了
①函式是否在new中呼叫(new繫結),是的話this繫結的是新建立的物件。
var baz=new foo()
②函式是否通過call、apply或者硬繫結(在一個函式A中,物件obj使用call或者是apply永遠與this函式繫結到了一起,無論何時在哪呼叫函式A,this指向的都是物件obj)呼叫?如果是的話,this繫結的是指定的物件。
var bar=foo.call(obj1);
③函式是否在某個上下文物件中被呼叫(隱式繫結)?是的話,this指向這個上下文物件
var bar=obj1.foo();
④如果都不是的話,就是用預設繫結,在嚴格模式下,繫結"undefined",否者就繫結全域性物件。
var bar=foo();
1.5 例外的繫結
規則總有例外,在這裡也一樣,在某些場景下的this繫結可能不按照我們之前的優先順序來,而是直接綁定了預設規則。
1.5.1 被忽略的this
在你把"undefined"或者"null"作為this的繫結物件傳入"call(...)"、"apply(...)"以及"bind(...)"時,這些值會被忽略,而this會應用預設繫結。
var a=2;
function foo() {
console.log(this.a);
}
foo.call(null); //2
foo.call(undefined); //2
foo.apply(null); //2
foo.apply(undefined); //2
那麼什麼情況下,你需要往顯式繫結方法中傳入"null"呢?
一種常見的做法是使用apply(...)來“展開”一個數組,並當作引數來傳入下一個函式,類似的,bind(...)可以對引數進行柯里化(預先放置一個引數)。
柯里化:把接受多個引數的函式變為只接受開始的第一個引數的函式,並且返回接受餘下的引數且返回結果的新函式。通俗點解釋是:接受一個單一可以預期的引數,返回一個正確結果,http://www.cnblogs.com/pigtail/p/3447660.html參考這篇文章
下面我們來看看,如何用apply(....)“展開”一個數組
function foo(a,b) {
console.log("a:"+a+"b:"+b);
}
foo.apply(null,[2,3]); //a:2 b:3 把陣列展開成引數
如何用bind(...)實現柯里化
function foo(a,b) {
console.log("a:"+a+"b:"+b);
}
var bar=foo.bind(null,2); //a=2;
bar(3); //a:2 b:3
如上我們可以看到,原本傳入的foo的引數為"a""b"但是在使用bind(...),以及傳入的繫結物件是"null"之後我們在foo.bind()中就不需要將"a""b"的值完全輸入。
這也就相當於變相的縮小了函式的使用範圍,例:原本foo函式可以用於"a""b"值為任意值,但是在"var bar=foo.bind(null,2)"之後,我們使用"bar"只能用在'a=2"的場合----------------這就是柯里化
這兩種方法都需要傳入一個引數作為繫結物件,當我們不關心函式繫結的this=時,你仍然需要傳入一個引數才能使用這個函式,那麼“null”是一個正確的選擇
注意!!!儘管這種做法有時候很實用,但是在this已經實現繫結時,忽略此this會導致,this繫結
結果出錯(繫結成全域性物件)
更安全的this
那麼有沒有別的更安全的做法忽略掉這次的this呢?
其實有種做法比傳入"null"還要安全,這就是"DMZ"
這種做法是傳入一個特殊的物件,把this繫結到這個物件不會對你的程式起到任何的副作用。這個物件是"DMZ"-------一個非空的委託物件。
如果我們將"DMZ"作為物件傳入this以達到忽略this的目的,那麼,任何對this的使用都會被限制在這個空物件裡,對外界沒有任何影響
在JavaScript中建立這個空物件最簡單的方法是:object.create(null)。這個與{}很像,單不同的是它不會建立object.prototype這個委託,它比{}更空。
function foo(a,b) {
console.log("a:"+a+"b:"+b);
}
var ø=Object.create(null); //建立DMZ空物件
foo.apply(ø,[1,2]); //利用DMZ空物件傳入引數到this函式中,對引數進行展開
//利用bind進行柯里化
var bar=foo.bind(ø,2);
bar(3);
儘管我們可以將"DMZ"物件名更改為我們喜歡的名字,但是仍然是建議大家使用ø
1.5.2 間接引用
另外一個經常容易犯的錯誤便是,你可能有意無意的建立一個函式的“間接引用”,在這種情況下this會預設繫結。
IIFE中賦值
function foo() {
console.log(this.a);
}
var a=4;
var obj={
a:3,
foo:foo
};
var baz={
a:2
};
(baz.foo=obj.foo)(); //4
賦值表示式p.foo=o.foo的返回值是目標函式的引用,因此呼叫位置是foo()而不是o.foo()或者p.foo()。因此這裡使用預設繫結
函式賦值
function foo() {
console.log(this.a);
}
var a=4;
var obj={
a:3,
foo:foo
};
var bar=obj.foo;
bar(); //4
注意這裡!!!預設繫結輸出的是不是"undefined"並不取決於呼叫this的位置,而取決於this函式體(這裡是foo函式)是否為嚴格模式。
1.5.3 軟繫結
之前我們有提到硬繫結,強制使this指向一個我們希望指向的物件,隱式繫結、顯式繫結都無法改變它(除了new繫結以外)。這樣儘管很好,但是卻犧牲了函式的靈活性,使用硬繫結就無法使用顯式繫結或者隱式繫結修改this指向的物件,只能通過改變硬繫結函式內的函式體。
如果給預設繫結物件指定一個全域性物件和"undefined"以外的值,就可以實現和硬繫結同樣的效果,同時隱式繫結和顯式繫結還可以修改this。
這種用預設繫結實現硬繫結功能的做法,我們稱之為-----------軟繫結
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕獲所有 curried 引數
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
除了軟繫結之外,softBind()的其他原理與ES5中的bind(...)類似。
- 對指定函式進行封裝
- 檢查使用的this是否繫結到全域性物件或者是"undefined"
- 是的話就把預設物件obj繫結到this上
- 不是的話,就不會修改this
- 此函式支援柯里化
softBind()例子
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
//隱式繫結開始
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
//顯式繫結開始
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 應用了軟繫結
可以看到軟繫結的foo()可以手動將this繫結到obj2或者obj3的身上,但是如果應用預設繫結,則會將this繫結到obj身上。
1.6 this詞法
之前我們介紹了this繫結的四種規則,然而在JavaScript中並不是所有的函式體都遵循著這四個規則,其中箭頭函式便是如此。
箭頭函式並不是用function關鍵字定義的,而是使用被稱為“胖箭頭”的操作符=>定義的函式(使用胖箭頭代替function宣告),胖箭頭不適用this的四條規則,胖箭頭函式的this是根據外層作用域來決定的
function foo() {
return (a)=>{
console.log(this.a);
}
}
var obj1={
a:2
};
var obj2={
a:3
};
var bar=foo.call(obj1);
bar.call(obj2); //2不是3
foo(...)內部建立的胖箭頭會捕獲呼叫時foo()的this。由於foo()的this繫結到obj1,bar的this也繫結到obj1,胖箭頭的this無法修改,即使是new繫結也不行。
胖箭頭一般用於回撥函式中,例如事件處理器或者定時器
function foo() {
setTimeout(()=>{
console.log(this.a);
},100);
}
var obj={
a:2
};
foo.call(obj); //2
利用箭頭函式可以像硬繫結bind(...)那樣,確保this繫結到指定物件,不同的是它是利用作用域。而我們在寫程式碼時,儘量使用一種設計模式-----要麼用四條規則繫結,要麼使用胖箭頭作用域繫結。
總結:
this在JavaScript的繫結物件,根據以下步驟查詢
①new呼叫?繫結建立的新物件---------------建構函式呼叫
②由call()、apply()、bind()呼叫?繫結到指定物件------------間接呼叫
③由上下文物件呼叫?繫結到上下文物件-------------------------方法呼叫
④函式直接使用不帶任何修飾的引用?繫結到全域性物件--------獨立呼叫
能解決隱式丟失的只有:硬繫結(bind(...))
當使用間接引用時,很容易發生隱式丟失,要仔細看,一般發生了隱式丟失,都會應用預設繫結。
當想要安全的忽略掉this繫結時,那麼久傳入一個"DMZ"物件,建立方法:
var ø=Object.create(null)。
ES6中的胖箭頭函式不在四條this繫結規則之內,胖箭頭函式一旦繫結之後new繫結都無法改變繫結物件,它的this繫結只於上一層的作用域有關,這是用作用域影響this繫結。這與ES6之前的"self=this"一樣。