JavaScript基礎系列--戰勝this

最近重溫了一遍《你不知道的JavaScript--上卷》,其中第二部分關於this的講解讓我收穫頗多,因此寫一篇讀書筆記記錄總結一番。編程

消除誤解--this指向自身

因爲this的英文釋義,許多人都會將其理解成指向函數自身(JavaScript 中的全部函數都
是對象),可是實際上this並不像咱們所想的那樣指向函數自身,咱們能夠經過下面的栗子驗證一下~segmentfault

function foo(num) {
    console.log( "foo: " + num );
    // 記錄foo 被調用的次數
    this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被調用了多少次?
console.log( foo.count ); // 0 -- WTF?

上述栗子的本意是想記錄foo被調用的次數數組

假設this指向函數自己,那麼this.countfoo.count應該是foo函數對象的同一個屬性,那麼最終獲得的foo.count應該是4瀏覽器

然而實際上,最終獲得的foo.count0,也就是說foo.count初始化以後就沒有再改變過了,因此this.countfoo.count是相互獨立的,互不影響;因此結論是:this並非指向函數自己安全

那麼這個裏面的this究竟是指向什麼呢?你能夠思考一下,寫下你的答案。而後繼續日後看,後面你會獲得答案的~~想立刻驗證能夠拖到最後...網絡

this究竟是什麼

this的肯定是在Execution Context的建立階段,而Execution Context的建立發生在瀏覽器第一次加載script的時候或者調用函數的時候----具體可參見以前寫過的一篇文章JavaScript基礎系列---執行環境與做用域鏈app

因此this 是在運行時進行綁定的,並非在編寫時綁定,它的上下文取決於函數調用時的各類條件,this的綁定和函數聲明的位置沒有任何關係,只取決於函數的調用方式this的指向並無一個固定的說法,須要分狀況而論。函數

要想明確this指向什麼,須要經過尋找函數的調用位置來判斷函數在執行過程當中會如何綁定this,從而肯定this的指向。工具

尋找調用位置

尋找調用位置就是尋找「函數被調用的位置」,可是作起來並無這麼簡單,由於某些編程模式可能會隱藏真正的調用位置,這種時候很容易出錯。oop

最重要的是要分析調用棧(就是爲了到達當前執行位置所調用的全部函數),咱們關心的調用位置就在當前正在執行的函數的前一個調用中,下面用栗子來幫助理解:

function baz() {
    debugger
    // 當前調用棧是:baz
    // 所以,當前調用位置是全局做用域
    console.log( "baz" );
    bar(); // <-- bar 的調用位置
}
function bar() {
    debugger
    // 當前調用棧是baz -> bar
    // 所以,當前調用位置在baz 中
    console.log( "bar" );
    foo(); // <-- foo 的調用位置
}
function foo() {
    debugger
    // 當前調用棧是baz -> bar -> foo
    // 所以,當前調用位置在bar 中
    console.log( "foo" );
}
baz(); // <-- baz 的調用位置

若是條件容許,可使用開發者工具進行觀察,將會更加直觀。

baz函數是在全局做用域中調用的,baz函數的調用棧爲baz,因此baz函數的調用位置是全局做用域

clipboard.png

bar函數是在baz函數中調用的,bar函數的調用棧爲baz -> bar,當正在執行的是bar函數時,其前一個調用是baz,因此bar函數的調用位置是baz函數中的bar();位置

clipboard.png

foo函數是在bar函數中調用的,foo函數的調用棧爲baz -> bar -> foo,當正在執行的是foo函數時,其前一個調用是bar,因此foo函數的調用位置是bar函數中的foo();位置

clipboard.png

this的綁定規則

找到調用位置後該如何肯定this的指向呢?這是有規則可循的,下面咱們就來看看這四條規則,瞭解了規則後,肯定this的步驟就變成:找到調用位置,而後判斷須要應用四條規則中的哪一條,根據規則得出this的指向。

默認綁定

首先要介紹的是最經常使用的函數調用類型:獨立函數調用。這種調用是直接使用不帶任何修飾的函數引用進行調用的,它的調用位置是全局做用域,因而this指向全局對象。能夠把這條規則看做是沒法應用其餘規則時的默認規則。

咱們看下面的代碼:

function foo() {
    console.log( this.a );
}
var a = 2;
foo(); // 2

首先咱們要知道一件事,聲明在全局做用域中的變量(好比上述代碼中的var a = 2)就是全局對象的一個同名屬性。它們本質上就是同一個東西,並非經過複製獲得的,就像一個硬幣的兩面同樣。

在代碼中,foo()是在全局做用域中直接使用不帶任何修飾的函數引用進行調用的,因此foo函數調用時應用this的默認綁定,所以this指向全局對象;既然this指向全局對象,那麼this.a即是全局變量a,因此打印的結果爲2

注意:嚴格模式下,禁止this關鍵字指向全局對象,此時this會綁定到undefined;因此當函數定義在嚴格模式下或函數內的代碼運行在嚴格模式下時,其中的this綁定的是undefined;特別注意若是僅僅是函數的調用語句運行在嚴格模式下,那麼不受影響,該函數內的this仍然綁定到全局對象

"use strict";
function foo() {
    console.log( this.a );
}
var a = 2;
foo(); // TypeError: Cannot read property 'a' of undefined

foo函數定義在嚴格模式下,因此this綁定到了`undefined

function foo() {
    "use strict";
    console.log( this.a );
}
var a = 2;
foo(); // TypeError: Cannot read property 'a' of undefined

foo函數內部爲嚴格模式,因此this綁定到了undefined

function foo() {
    console.log( this.a );
}
"use strict";
var a = 2;
foo(); // 2

嚴格模式的標識在foo函數的定義以後,foo函數未定義在嚴格模式下,僅僅是foo函數的調用語句foo()運行在嚴格模式下,因此this仍然能夠綁定到全局對象

function foo() {
    console.log( this.a );
}
var a = 2;
(function(){
   "use strict";
   foo(); // 2
})()

僅僅是foo函數的調用語句foo()運行在嚴格模式下,因此this仍然能夠綁定到全局對象

舒適提示:一般來講你不該該在代碼中混合使用嚴格模式和n非嚴格模式。整個程序要麼嚴格要麼非嚴格。然而,有時候你可能會用到第三方庫,其嚴格程度和你的代碼有所不一樣,所以必定要注意這類兼容性細節。

隱式綁定

第二條規則是考慮函數調用位置是否有上下文對象,或者說該函數是否被某個對象「擁有」或者「包含」(僅僅是這麼理解一下),若是函數調用位置有上下文對象,那麼隱式綁定規則會把該函數中的this綁定到這個上下文對象

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

首先須要注意的是foo函數的聲明方式,及其以後是如何被看成引用屬性添加到obj中的。可是不管是直接在obj中定義仍是先定義再添加爲引用屬性,這個函數嚴格來講都不屬於obj對象;然而,調用位置會使用obj上下文來引用函數,所以你能夠說函數被調用時obj 對象「擁有」或者「包含」它。

當函數調用位置有上下文對象時,隱式綁定規則會把該函數中的this綁定到這個上下文對象。因此上面的例子中,調用foo()this被綁定到obj,那麼this.aobj.a 是同樣的,打印的結果即是2

對象屬性引用鏈中只有最頂層或者說最後一層會影響調用位置,看個例子就很容易理解了:

function foo() {
    console.log( this.a );
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42

上述對象引用鏈爲 :obj1->obj2,只有最後一層會影響調用位置,也就是隻有obj2會影響調用位置,因此foo函數的調用位置的上下文對象爲obj2this綁定到obj2

注意:有些狀況下會出現隱式丟失,意思就是被隱式綁定的函數丟失綁定對象,也就是說它會應用默認綁定,從而把this綁定到全局對象或者undefined上(取決因而否是嚴格模式)

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函數別名!
var a = "oops, global"; // a 是全局對象的屬性
bar(); // "oops, global"

上面例子中,雖然barobj.foo的一個引用,可是實際上,它引用的是foo 函數自己,至關於var bar = foo;。所以此時的bar()實際上是一個不帶任何修飾的函數調用,因此會應用了默認綁定,綁定到全局對象

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    // fn 其實引用的是foo
    fn(); // <-- 調用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全局對象的屬性
doFoo( obj.foo ); // "oops, global"
setTimeout( obj.foo, 100 ); // "oops, global"

參數傳遞其實就是一種隱式賦值,所以咱們傳入函數時也會被隱式賦值,因此將obj.foo傳遞給doFoo函數的參數fn,至關於fn = foo,因此doFoo函數內部的fn()實際上是一個不帶任何修飾的函數調用,因此會應用了默認綁定,綁定到全局對象

內置函數setTimeout的結果也是同樣的。回調函數丟失this綁定是很是常見的,以後咱們會介紹如何經過固定this來修復這個問題。

顯式綁定

就像咱們剛纔看到的那樣,在分析隱式綁定時,咱們必須在一個對象內部包含一個指向函數的屬性,並經過這個屬性間接引用函數,從而把this 間接(隱式)綁定到這個對象上。那麼若是咱們不想在對象內部包含函數引用,而想在某個對象上強制調用函數,該怎麼作呢?

可使用函數的call(..)apply(..) 方法。嚴格來講,JavaScript 的宿主環境有時會提供一些很是特殊的函數,它們並無這兩個方法。可是這樣的函數很是罕見,JavaScript 提供的絕大多數函數以及你本身建立的全部函數均可以使用call(..)apply(..) 方法。

這兩個方法是如何工做的呢?它們的第一個參數是一個對象,它們會把這個對象綁定到this,接着在調用函數時指定這個this;由於你能夠直接指定this的綁定對象,所以咱們稱之爲顯式綁定。(若是沒有傳遞第一個參數,也就是沒有直接指定this,那麼this將綁定到全局對象或者undefined上)

function foo() {
    console.log( this.a );
}
var obj = {
    a:2
};
foo.call( obj ); // 2

經過foo.call(..),咱們能夠在調用foo時強制把它的this綁定到obj上。

若是你傳入了一個原始值(字符串類型、布爾類型或者數字類型)來看成this的綁定對象,這個原始值會被轉換成它的對象形式(也就是new String(..)new Boolean(..) 或者new Number(..))。這一般被稱爲「裝箱」。可是在嚴格模式下this不會被強制轉換爲一個對象,也就是說傳入原始值來當作this的綁定對象,那麼它不會轉換爲對象形式

function foo() {
    console.log( this );
}
foo.call( "cc" ); // String {"cc"}
foo.call( 6 ); // Number {6}
foo.call( true ); // Boolean {true}

"use strict"
function foo() {
    console.log( this );
}
foo.call( "cc" ); // cc
foo.call( 6 ); // 6
foo.call( true ); // true

惋惜,顯式綁定仍然沒法解決咱們以前提出的丟失綁定問題

function foo() {
    console.log( this.a );
}
var obj = {
    a:6
};
var bar = function() {
    foo();
};
bar.call(obj); // undefined

能夠看出,雖然bar經過call(..)方法顯示綁定到了obj,可是其內部的foo()仍然是一個不帶任何修飾的函數調用,this綁定到全局對象

硬綁定

顯式綁定的一個變種能夠解決這個丟失綁定問題,咱們稱這個變種爲硬綁定,下面來看看它是如何解決的:

function foo() {
    console.log( this.a );
}
var obj = {
    a:6
};
var bar = function() {
    foo.call( obj );
};
bar(); // 6
setTimeout( bar, 100 ); // 6
// 硬綁定的bar 不可能再修改它的this
bar.call( window ); // 6

咱們建立了函數bar,並在它的內部手動調用了foo.call(obj),所以強制把foothis 綁定到了obj,不管以後如何調用函數barthis始終綁定到obj

通常來講,能夠建立一個可重複使用的硬綁定輔助函數:

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
// 簡單的輔助綁定函數
function bind(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

經過bind函數就能夠將foo函數的this始終綁定爲obj,因爲硬綁定是一種很是經常使用的模式,因此在ES5中提供了內置的方法Function.prototype.bind,將上面的例子改爲該方法的形式,代碼以下:

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
var obj = {
    a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

bind(..)會返回一個硬編碼的新函數,調用這個新函數時會把原始函數的this綁定到傳入bind(..)的參數上並調用原始函數,因此foo.bind( obj )會返回一個新函數,而後被賦值給bar,調用bar時會把foo中的this綁定到obj,而且調用foo函數。

API調用的「上下文」

除了上面說的硬綁定能夠強制給this一個綁定,第三方庫的許多函數,以及JavaScript語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,一般被稱爲「上下文」(context),其做用和bind(..) 同樣,確保你的回調函數使用指定的this

好比說:

function foo(el) {
    console.log( el, this.id );
}
var obj = {
    id: "awesome"
};
// 調用foo(..) 時把this綁定到obj
[1, 2, 3].forEach( foo, obj );
// 1 "awesome"
// 2 "awesome"
// 3 "awesome"

array.forEach(function(currentValue, index, arr), thisValue)方法用於調用數組的每一個元素,並將元素傳遞給回調函數,它的第二個參數thisValue就能夠指定回調函數中的this(若是這個參數爲空,那麼this將綁定到全局對象或者undefined上);forEach內部實際上就是經過call(..) 或者apply(..) 實現了顯式綁定

其餘函數還有array.maparray.filterarray.everyarray.some

new綁定

最後一條this的綁定規則,在講解它以前咱們首先須要澄清一個很是常見的關於JavaScript 中函數和對象的誤解。

在傳統的面向類的語言中,「構造函數」是類中的一些特殊方法,使用new初始化類時會調用類中的構造函數。一般的形式是這樣的:

something = new MyClass(..);

JavaScript也有一個new操做符,使用方法看起來也和那些面向類的語言同樣,可是,JavaScriptnew的機制實際上和麪向類的語言徹底不一樣

首先咱們從新定義一下JavaScript中的「構造函數」:在JavaScript中,構造函數只是一些使用new操做符時被調用的函數,它們並不會屬於某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被new操做符調用的普通函數而已。(ES6中的Class只是語法糖而已)

自定義函數和內置對象函數(好比Number(..))均可以用new來調用,這種函數調用被稱爲構造函數調用。這裏有一個重要可是很是細微的區別:實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」

使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操做:

  • 建立(或者說構造)一個全新的對象
  • 這個新對象會被執行[[Prototype]]連接([[Prototype]]指向構造函數的原型對象
  • 這個新對象會綁定到該構造函數中的this
  • 執行構造函數中的代碼
  • 若是該構造函數沒有返回其餘對象,那麼會自動返回這個新對象

上述過程當中的this綁定就被稱爲new綁定,下面看個簡單的例子:

function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

使用new來調用foo(..)時,咱們會構造一個新對象(賦值給了變量bar)並把它綁定到foo函數中的this上,foo函數中的this綁定的就是對象bar

綁定規則的優先級

在瞭解了四種綁定規則後,咱們須要瞭解一下他們之間的優先級,由於有時候會出現符合多種規則的狀況。

毫無疑問,默認綁定的優先級是四條規則中最低的,因此咱們能夠先不考慮它。

隱式綁定和顯式綁定哪一個優先級更高?咱們來測試一下:

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

能夠明顯看出,顯示綁定優先於隱式綁定,也就是說在判斷時應當先考慮是否能夠應用顯式綁定

那麼隱式綁定和new綁定哪一個優先級更高?咱們也來測試一下:

function foo(something) {
    this.a = something;
}
var obj1 = {
    foo: foo
};
var obj2 = {};

obj1.foo( 2 );//隱式綁定
console.log( obj1.a ); // 2

var bar = new obj1.foo( 4 );//new綁定,至關於vra bar = new foo(4);
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

能夠看到new綁定比隱式綁定優先級高,那麼如今還須要知道new綁定和顯式綁定誰的優先級更高,因爲newcall/apply 沒法一塊兒使用,所以沒法經過new foo.call(obj1) 來直接進行測試,而硬綁定是顯示綁定的一種,因此咱們使用硬綁定來測試它倆的優先級:

在看代碼以前先回憶一下硬綁定是如何工做的。Function.prototype.bind(..) 會建立一個新的包裝函數,這個函數會忽略它當前的this綁定(不管綁定的對象是什麼),並把咱們提供的對象綁定到this上。

這樣看起來硬綁定(也是顯式綁定的一種)彷佛比new 綁定的優先級更高,應該沒法使用new來控制this綁定,那其實是如何的呢?來讓代碼揭曉答案:

function foo(something) {
    this.a = something;
}
var obj1 = {};

var bar = foo.bind( obj1 );
bar(2);
console.log( obj1.a ); // 2

var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

bar函數中的的this被硬綁定到obj1上,可是new bar(3)並無像咱們前面預計的那樣把obj1.a修改成3,這說明使用new來調用bar()的時候,bar函數中的this綁定的不是obj1(不然obj1.a應該被修改成3),因此使用new仍然能夠控制this綁定,實際上此時bar函數中的this綁定的是一個新對象,這個新對象最後賦值給了baz,因此baz.a的值爲3

爲何與預想的不一樣?由於ES5 中內置的Function.prototype.bind(..)方法的內部會進行判斷,會判斷硬綁定函數是不是被new調用,若是是的話就會使用新建立的this替換硬綁定的this

因此new綁定的優先級高於顯示綁定。

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])方法,能夠傳入參數序列,當綁定函數被調用時,這些參數將置於實參以前傳遞給被綁定的函數,因此bind(..)的功能之一就是能夠把除了第一個參數(第一個參數用於綁定this)以外的其餘參數都傳給下層的函數(這種技術稱爲「部分應用」,是「柯里化」的一種)。

正是因爲bind(...)的這一功能,若是咱們在new中使用硬綁定函數,那麼就能夠預先設置函數的一些參數,這樣在使用new進行初始化時就能夠只傳入其他的參數,這也就是爲何有些時候會在new中使用硬綁定函數的緣由,看個例子:

function foo(p1,p2) {
    this.val = p1 + p2;
}
// 之因此使用null 是由於在本例中咱們並不關心硬綁定的this是什麼
// 反正使用new的時候this會被修改
var bar = foo.bind( null, "p1" );//傳入預先設置的參數p1
var baz = new bar( "p2" );//只需傳入剩餘的參數p2
baz.val; // p1p2

優先級總結

綜上所述,優先級以下:
new綁定 > 顯示綁定 > 隱式綁定 > 默認綁定

那麼咱們在肯定this的時候就能夠根據下面的步驟來:

  • 函數是否使用new調用(new綁定)?若是是的話this綁定的是新建立的對象。

    var bar = new foo()
  • 函數是否經過callapply(顯式綁定)或者硬綁定bind調用?若是是的話,this綁定的是指定的對象。

    var bar = foo.call(obj2)
  • 函數是否在某個上下文對象中調用(隱式綁定)?若是是的話,this綁定的是那個上下文對象。

    var bar = obj1.foo()
  • 若是都不是的話,使用默認綁定。若是在嚴格模式下,就綁定到undefined,不然綁定到全局對象。

    var bar = foo()

對於正常的函數調用來講,理解了這些知識就能夠明白this的綁定原理了,不過……凡事總有例外!!!

綁定的特殊狀況

在某些場景下this的綁定行爲會出乎意料,你認爲應當應用其餘綁定規則時,實際上應用的多是默認綁定規則。

被忽略的this

若是你把null或者undefined做爲this的綁定對象傳入callapply 或者bind,這些值在調用時會被忽略,實際應用的是默認綁定規則:

function foo() {
    console.log( this.a );
}
var a = 2;
foo.call( null ); // 2

那麼什麼狀況下你會傳入null呢?一種很是常見的作法是使用apply(..)來「展開」一個數組,並看成參數傳入一個函數(ES6中能夠直接使用...操做符)。相似地,bind(..)能夠對參數進行柯里化(預先設置一些參數),這種方法有時很是有用:

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 把數組「展開」成參數
foo.apply( null, [2, 3] ); // a:2, b:3

// 使用 bind(..) 進行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

這兩種方法都須要傳入一個參數看成this的綁定對象。若是函數並不關心this的話,你仍然須要傳入一個佔位值,這時null多是一個不錯的選擇,就像代碼所示的那樣。

然而,老是使用null來忽略this綁定可能產生一些反作用。若是某個函數確實使用了this(好比第三方庫中的一個函數),那默認綁定規則會把this綁定到全局對象(在瀏覽器中這個對象是window),這將致使不可預計的後果(好比修改全局對象。顯而易見,這種方式可能會致使許多難以分析和追蹤的bug

一種「更安全」的作法是傳入一個特殊的對象,把this綁定到這個對象不會對你的程序產生任何反作用。就像網絡(以及軍隊)同樣,咱們能夠建立一個DMZdemilitarized zone,非軍事區)對象,若是咱們在忽略this綁定時老是傳入一個DMZ對象,那就什麼都不用擔憂了,由於任何對於this的使用都會被限制在這個空對象中,不會對全局對象產生任何影響。

因爲這個DMZ對象徹底是一個空對象,可使用一個特殊的變量名來表示它,好比ø(這是數學中表示空集合符號的小寫形式)。在JavaScript中建立一個空對象最簡單的方法都是Object.create(null)Object.create(null){}很像,可是並不會建立Object.prototype這個委託,因此它比{}「更空」,因此以前的例子能夠改成:

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 咱們的DMZ 空對象
var ø = Object.create( null );
// 把數組展開成參數
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用bind(..) 進行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

間接引用

另外一個須要注意的是,你有可能(有意或者無心地)建立一個函數的「間接引用」,在這種狀況下,調用這個函數會應用默認綁定規則。

間接引用最容易在賦值時發生:

function foo() {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

賦值表達式的返回值是要賦的值,因此p.foo = o.foo的返回值是目標函數的引用,即foo函數的引用,所以調用位置是foo()而不是p.foo()或者o.foo()。根據咱們以前說過的,這裏會應用默認綁定。

軟綁定

以前咱們已經看到過,硬綁定這種方式能夠把this強制綁定到指定的對象(除了使用new時),防止函數調用應用默認綁定規則。問題在於,硬綁定會大大下降函數的靈活性,使用硬綁定以後就沒法使用隱式綁定或者顯式綁定來修改this

若是能夠給默認綁定指定一個全局對象和undefined之外的值,那就能夠實現和硬綁定相同的效果,同時保留隱式綁定或者顯式綁定修改this的能力。

能夠經過一種被稱爲軟綁定的方法來實現咱們想要的效果:

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;//這個this是指調用softBind的函數
        // 捕獲全部 curried 參數(柯里化參數)
        var curried = [].slice.call( arguments, 1 );//arguments指傳入softBind的參數列表
        var bound = function() {
            return fn.apply(
                (!this || this === (window || global)) ? obj : this,
                curried.concat.apply( curried, arguments)
            );//這裏的this是指調用bound時的this,arguments指傳入bound的參數列表
        };
        bound.prototype = Object.create( fn.prototype );
        return bound;
    };
}

除了軟綁定以外,softBind(..) 的其餘原理和ES5內置的bind(..) 相似。它會對指定的函數進行封裝,首先檢查調用時的this,若是this綁定到全局對象或者undefined,那就把指定的默認對象obj綁定到this,不然不會修改this。此外,這段代碼還支持可選的柯里化(詳情請查看以前和bind(..)相關的介紹),看看軟綁定的實例:

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 <---- 看!!!經過上下文對象(隱式綁定)綁定到obj2

fooOBJ.call( obj3 ); // name: obj3 <---- 看!經過顯示綁定綁定到obj3

setTimeout( obj2.foo, 10 );// name: obj <---- 應用了軟綁定,this原本綁定到全局對象,經過軟綁定綁定到了obj

能夠看到,軟綁定版本的foo()能夠手動將this綁定到obj2或者obj3上,但若是應用默
認綁定,則會將this綁定到obj

特殊的箭頭函數

咱們以前介紹的四條規則已經能夠包含全部正常的函數。可是ES6中介紹了一種沒法使用這些規則的特殊函數類型:箭頭函數。

箭頭函數並非使用function關鍵字定義的,而是使用被稱爲「胖箭頭」的操做符=>定義的。箭頭函數不使用this的四種標準規則,而是根據外層(函數或者全局)做用域來決定this

咱們來看看箭頭函數的詞法做用域:

function foo() {
    // 返回一個箭頭函數
    return (a) => {
        //this 繼承自foo()
        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綁定到obj1bar(引用箭頭函數)的this也會綁定到obj1箭頭函數的綁定沒法被修改。(new也不行!)

箭頭函數最經常使用於回調函數中,例如事件處理器或者定時器:

function foo() {
    setTimeout(() => {
        // 這裏的this 在詞法上繼承自foo()
        console.log( this.a );
    },100);
}
var obj = {
    a:2
};
foo.call( obj ); // 2

箭頭函數能夠像bind(..)同樣確保函數的this被綁定到指定對象,此外,其重要性還體如今它用更常見的詞法做用域取代了傳統的this機制。實際上,在ES6以前咱們就已經在使用一種幾乎和箭頭函數徹底同樣的模式:

function foo() {
    var self = this; // lexical capture of this
    setTimeout( function(){
        console.log( self.a );
    }, 100 );
}
var obj = {
    a: 2
};
foo.call( obj ); // 2

是否是很是熟悉?

雖然self = this和箭頭函數看起來均可以取代bind(..),可是從本質上來講,它們想替代的是this機制,若是你常常編寫this風格的代碼,可是絕大部分時候都會使用self = this或者箭頭函數來否認this機制,那你或許應當:

  • 只使用詞法做用域並徹底拋棄錯誤this風格的代碼;
  • 徹底採用this風格,在必要時使用bind(..),儘可能避免使用self = this和箭頭函數。

固然,包含這兩種代碼風格的程序能夠正常運行,可是在同一個函數或者同一個程序中混合使用這兩種風格一般會使代碼更難維護,而且可能也會更難編寫。

疑問解答

先來講一下最前面的一個例子的真實狀況:

function foo(num) {
    console.log( "foo: " + num );
    // 記錄foo 被調用的次數
    this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被調用了多少次?
console.log( foo.count ); // 0 -- WTF?

先說結論,this指向全局對象,而this.countNaN
經過分析咱們能夠知道foo的調用位置是全局做用域,而後foo處於非嚴格模式,因此this指向全局對象,因爲this.count的值一開始爲undefined,而後進行this.count++;的操做,因此變成NaN

記得前面提到過下面這段話:

嚴格模式下,禁止this關鍵字指向全局對象,此時this會綁定到undefined;因此當函數定義在嚴格模式下或函數內的代碼運行在嚴格模式下時,其中的this綁定的是undefined;特別注意若是僅僅是函數的調用語句運行在嚴格模式下,那麼不受影響,該函數內的this仍然綁定到全局對象

可是測試的時候遇到一種狀況一開始讓我匪夷所思:

function foo(){
    "use strict";
    console.log(this);
}

setTimeout(foo,100);//Window

foo的函數體處於嚴格模式下,爲何this仍是綁定到全局對象Window?因而我又測試了幾種狀況:

"use strict";
function foo(){
    console.log(this);
}
setTimeout(foo,100);//Window

//---------分割線-----------

function foo(){
    console.log(this);
}
setTimeout(function(){
    "use strict";
    foo();
},100);//Window

//---------分割線-----------

function foo(){
    "use strict";
    console.log(this);
}
setTimeout(function(){
    foo();
},100);//undefined

只有最後一種狀況this綁定到undefined,其餘狀況仍然綁定到Window
MDN-Window.setTimeout-關於this的問題中,找到一段備註:

備註:在嚴格模式下,setTimeout( )的回調函數裏面的this仍然默認指向window對象, 並非undefined

可是這個僅僅是告訴了咱們結論,並無給出爲何。通過思考,我給出我本身的猜測,也不知道對不對:

咱們知道setTimout是掛在Window下的方法,因此調用時其實是Window.setTimout,是經過Window對象調用的,通常認爲setTimout的僞代碼是下面這樣:

function setTimeout(fn,delay) {
    // 等待delay 毫秒
    fn(); 
}

可是經過前文的介紹,咱們知道

直接使用不帶任何修飾的函數引用進行調用的,它的調用位置是全局做用域,非嚴格模式下綁定到全局對象,嚴格模式下綁定到undefined

根據setTimeout這種僞代碼,等待delay毫秒後,fn()就是一個不帶任何修飾的函數調用,而下面的測試確仍然指向全局對象Window

function foo(){
    "use strict";
    console.log(this);
}

Window.setTimeout(foo,100);//Window

因此我猜測,setTimout的僞代碼是下面這樣:

function setTimeout(fn,delay) {
    // 等待delay 毫秒
    //直接執行fn內的代碼,而不是調用fn(至關於把fn中的代碼粘貼到此處) 
}

基於這種猜測,咱們來看前面的測試代碼:

function foo(){
    "use strict";
    console.log(this);
}

Window.setTimeout(foo,100);//Window

至關於下面這樣:

Window = {
    setTimeout: function(){
        // 等待100毫秒
        "use strict";
        console.log(this);
    }
}

這樣一看,this天然就是指向Window;再看其餘三個測試代碼:

"use strict";
function foo(){
    console.log(this);
}
setTimeout(foo,100);//Window

//至關於
"use strict";
Window = {
    setTimeout: function(){
        // 等待100毫秒
        console.log(this);
    }
}//經過Window調用setTimeout,this指向Window


//---------分割線-----------

function foo(){
    console.log(this);
}
setTimeout(function(){
    "use strict";
    foo();
},100);//Window

//至關於
Window = {
    setTimeout: function(){
        // 等待100毫秒
        "use strict";
        foo();
    }//經過Window調用setTimeout,其內部調用了foo,並且僅僅是foo的調用處於嚴格模式,因此foo中的this指向Window
}

//---------分割線-----------

function foo(){
    "use strict";
    console.log(this);
}
setTimeout(function(){
    foo();
},100);//undefined

//至關於
Window = {
    setTimeout: function(){
        // 等待100毫秒
        foo();
    }//經過Window調用setTimeout,其內部調用了foo,可是foo的函數體處於嚴格模式,因此foo中的this指向undefined
}

彷佛一切也說的過去,不過我暫時沒有找到權威性的資料來證明,本身先這樣理解一下,若是不對,還請你們指正!

尾聲

之前對this真是不清不楚,此次完全的順了一遍以後清晰多了,天天進步一點點,加油~

相關文章
相關標籤/搜索