成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

JavaScript基礎(chǔ)系列--打敗this

BoYang / 913人閱讀

摘要:然后繼續(xù)往后看,后面你會得到答案的想馬上驗(yàn)證可以拖到最后到底是什么的確定是在的創(chuàng)建階段,而的創(chuàng)建發(fā)生在瀏覽器第一次加載的時候或者調(diào)用函數(shù)的時候具體可參見之前寫過的一篇文章基礎(chǔ)系列執(zhí)行環(huán)境與作用域鏈。

最近重溫了一遍《你不知道的JavaScript--上卷》,其中第二部分關(guān)于this的講解讓我收獲頗多,所以寫一篇讀書筆記記錄總結(jié)一番。

消除誤解--this指向自身

由于this的英文釋義,許多人都會將其理解成指向函數(shù)自身(JavaScript 中的所有函數(shù)都
是對象),但是實(shí)際上this并不像我們所想的那樣指向函數(shù)自身,我們可以通過下面的栗子驗(yàn)證一下~

function foo(num) {
    console.log( "foo: " + num );
    // 記錄foo 被調(diào)用的次數(shù)
    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 被調(diào)用了多少次?
console.log( foo.count ); // 0 -- WTF?

上述栗子的本意是想記錄foo被調(diào)用的次數(shù)

假設(shè)this指向函數(shù)本身,那么this.countfoo.count應(yīng)該是foo函數(shù)對象的同一個屬性,那么最終得到的foo.count應(yīng)該是4

然而實(shí)際上,最終得到的foo.count0,也就是說foo.count初始化之后就沒有再改變過了,所以this.countfoo.count是相互獨(dú)立的,互不影響;所以結(jié)論是:this并不是指向函數(shù)本身

那么這個里面的this到底是指向什么呢?你可以思考一下,寫下你的答案。然后繼續(xù)往后看,后面你會得到答案的~~想馬上驗(yàn)證可以拖到最后...

this到底是什么

this的確定是在Execution Context的創(chuàng)建階段,而Execution Context的創(chuàng)建發(fā)生在瀏覽器第一次加載script的時候或者調(diào)用函數(shù)的時候----具體可參見之前寫過的一篇文章JavaScript基礎(chǔ)系列---執(zhí)行環(huán)境與作用域鏈。

所以this 是在運(yùn)行時進(jìn)行綁定的,并不是在編寫時綁定,它的上下文取決于函數(shù)調(diào)用時的各種條件,this的綁定和函數(shù)聲明的位置沒有任何關(guān)系,只取決于函數(shù)的調(diào)用方式;this的指向并沒有一個固定的說法,需要分情況而論。

要想明確this指向什么,需要通過尋找函數(shù)的調(diào)用位置來判斷函數(shù)在執(zhí)行過程中會如何綁定this,從而確定this的指向。

尋找調(diào)用位置

尋找調(diào)用位置就是尋找“函數(shù)被調(diào)用的位置”,但是做起來并沒有這么簡單,因?yàn)槟承┚幊棠J娇赡軙[藏真正的調(diào)用位置,這種時候很容易出錯。

最重要的是要分析調(diào)用棧(就是為了到達(dá)當(dāng)前執(zhí)行位置所調(diào)用的所有函數(shù)),我們關(guān)心的調(diào)用位置就在當(dāng)前正在執(zhí)行的函數(shù)的前一個調(diào)用中,下面用栗子來幫助理解:

function baz() {
    debugger
    // 當(dāng)前調(diào)用棧是:baz
    // 因此,當(dāng)前調(diào)用位置是全局作用域
    console.log( "baz" );
    bar(); // <-- bar 的調(diào)用位置
}
function bar() {
    debugger
    // 當(dāng)前調(diào)用棧是baz -> bar
    // 因此,當(dāng)前調(diào)用位置在baz 中
    console.log( "bar" );
    foo(); // <-- foo 的調(diào)用位置
}
function foo() {
    debugger
    // 當(dāng)前調(diào)用棧是baz -> bar -> foo
    // 因此,當(dāng)前調(diào)用位置在bar 中
    console.log( "foo" );
}
baz(); // <-- baz 的調(diào)用位置

如果條件允許,可以使用開發(fā)者工具進(jìn)行觀察,將會更加直觀。

baz函數(shù)是在全局作用域中調(diào)用的,baz函數(shù)的調(diào)用棧為baz,所以baz函數(shù)的調(diào)用位置是全局作用域

bar函數(shù)是在baz函數(shù)中調(diào)用的,bar函數(shù)的調(diào)用棧為baz -> bar,當(dāng)正在執(zhí)行的是bar函數(shù)時,其前一個調(diào)用是baz,所以bar函數(shù)的調(diào)用位置是baz函數(shù)中的bar();位置

foo函數(shù)是在bar函數(shù)中調(diào)用的,foo函數(shù)的調(diào)用棧為baz -> bar -> foo,當(dāng)正在執(zhí)行的是foo函數(shù)時,其前一個調(diào)用是bar,所以foo函數(shù)的調(diào)用位置是bar函數(shù)中的foo();位置

this的綁定規(guī)則

找到調(diào)用位置后該如何確定this的指向呢?這是有規(guī)則可循的,下面我們就來看看這四條規(guī)則,了解了規(guī)則后,確定this的步驟就變成:找到調(diào)用位置,然后判斷需要應(yīng)用四條規(guī)則中的哪一條,根據(jù)規(guī)則得出this的指向。

默認(rèn)綁定

首先要介紹的是最常用的函數(shù)調(diào)用類型:獨(dú)立函數(shù)調(diào)用。這種調(diào)用是直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用的,它的調(diào)用位置是全局作用域,于是this指向全局對象??梢园堰@條規(guī)則看作是無法應(yīng)用其他規(guī)則時的默認(rèn)規(guī)則。

我們看下面的代碼:

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

首先我們要知道一件事,聲明在全局作用域中的變量(比如上述代碼中的var a = 2)就是全局對象的一個同名屬性。它們本質(zhì)上就是同一個東西,并不是通過復(fù)制得到的,就像一個硬幣的兩面一樣。

在代碼中,foo()是在全局作用域中直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用的,所以foo函數(shù)調(diào)用時應(yīng)用this的默認(rèn)綁定,因此this指向全局對象;既然this指向全局對象,那么this.a便是全局變量a,所以打印的結(jié)果為2。

注意:嚴(yán)格模式下,禁止this關(guān)鍵字指向全局對象,此時this會綁定到undefined;所以當(dāng)函數(shù)定義在嚴(yán)格模式下或函數(shù)內(nèi)的代碼運(yùn)行在嚴(yán)格模式下時,其中的this綁定的是undefined;特別注意如果僅僅是函數(shù)的調(diào)用語句運(yùn)行在嚴(yán)格模式下,那么不受影響,該函數(shù)內(nèi)的this仍然綁定到全局對象

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

foo函數(shù)定義在嚴(yán)格模式下,所以this綁定到了`undefined

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

foo函數(shù)內(nèi)部為嚴(yán)格模式,所以this綁定到了undefined

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

嚴(yán)格模式的標(biāo)識在foo函數(shù)的定義之后,foo函數(shù)未定義在嚴(yán)格模式下,僅僅是foo函數(shù)的調(diào)用語句foo()運(yùn)行在嚴(yán)格模式下,所以this仍然可以綁定到全局對象

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

僅僅是foo函數(shù)的調(diào)用語句foo()運(yùn)行在嚴(yán)格模式下,所以this仍然可以綁定到全局對象

溫馨提示:通常來說你不應(yīng)該在代碼中混合使用嚴(yán)格模式和n非嚴(yán)格模式。整個程序要么嚴(yán)格要么非嚴(yán)格。然而,有時候你可能會用到第三方庫,其嚴(yán)格程度和你的代碼有所不同,因此一定要注意這類兼容性細(xì)節(jié)。

隱式綁定

第二條規(guī)則是考慮函數(shù)調(diào)用位置是否有上下文對象,或者說該函數(shù)是否被某個對象“擁有”或者“包含”(僅僅是這么理解一下),如果函數(shù)調(diào)用位置有上下文對象,那么隱式綁定規(guī)則會把該函數(shù)中的this綁定到這個上下文對象

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

首先需要注意的是foo函數(shù)的聲明方式,及其之后是如何被當(dāng)作引用屬性添加到obj中的。但是無論是直接在obj中定義還是先定義再添加為引用屬性,這個函數(shù)嚴(yán)格來說都不屬于obj對象;然而,調(diào)用位置會使用obj上下文來引用函數(shù),因此你可以說函數(shù)被調(diào)用時obj 對象“擁有”或者“包含”它。

當(dāng)函數(shù)調(diào)用位置有上下文對象時,隱式綁定規(guī)則會把該函數(shù)中的this綁定到這個上下文對象。所以上面的例子中,調(diào)用foo()this被綁定到obj,那么this.aobj.a 是一樣的,打印的結(jié)果便是2

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

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

上述對象引用鏈為 :obj1->obj2,只有最后一層會影響調(diào)用位置,也就是只有obj2會影響調(diào)用位置,所以foo函數(shù)的調(diào)用位置的上下文對象為obj2this綁定到obj2

注意:有些情況下會出現(xiàn)隱式丟失,意思就是被隱式綁定的函數(shù)丟失綁定對象,也就是說它會應(yīng)用默認(rèn)綁定,從而把this綁定到全局對象或者undefined上(取決于是否是嚴(yán)格模式)

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

上面例子中,雖然barobj.foo的一個引用,但是實(shí)際上,它引用的是foo 函數(shù)本身,相當(dāng)于var bar = foo;。因此此時的bar()其實(shí)是一個不帶任何修飾的函數(shù)調(diào)用,所以會應(yīng)用了默認(rèn)綁定,綁定到全局對象

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

參數(shù)傳遞其實(shí)就是一種隱式賦值,因此我們傳入函數(shù)時也會被隱式賦值,所以將obj.foo傳遞給doFoo函數(shù)的參數(shù)fn,相當(dāng)于fn = foo,所以doFoo函數(shù)內(nèi)部的fn()其實(shí)是一個不帶任何修飾的函數(shù)調(diào)用,所以會應(yīng)用了默認(rèn)綁定,綁定到全局對象

內(nèi)置函數(shù)setTimeout的結(jié)果也是一樣的?;卣{(diào)函數(shù)丟失this綁定是非常常見的,之后我們會介紹如何通過固定this來修復(fù)這個問題。

顯式綁定

就像我們剛才看到的那樣,在分析隱式綁定時,我們必須在一個對象內(nèi)部包含一個指向函數(shù)的屬性,并通過這個屬性間接引用函數(shù),從而把this 間接(隱式)綁定到這個對象上。那么如果我們不想在對象內(nèi)部包含函數(shù)引用,而想在某個對象上強(qiáng)制調(diào)用函數(shù),該怎么做呢?

可以使用函數(shù)的call(..)apply(..) 方法。嚴(yán)格來說,JavaScript 的宿主環(huán)境有時會提供一些非常特殊的函數(shù),它們并沒有這兩個方法。但是這樣的函數(shù)非常罕見,JavaScript 提供的絕大多數(shù)函數(shù)以及你自己創(chuàng)建的所有函數(shù)都可以使用call(..)apply(..) 方法。

這兩個方法是如何工作的呢?它們的第一個參數(shù)是一個對象,它們會把這個對象綁定到this,接著在調(diào)用函數(shù)時指定這個this;因?yàn)槟憧梢灾苯又付?b>this的綁定對象,因此我們稱之為顯式綁定。(如果沒有傳遞第一個參數(shù),也就是沒有直接指定this,那么this將綁定到全局對象或者undefined上)

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

通過foo.call(..),我們可以在調(diào)用foo時強(qiáng)制把它的this綁定到obj上。

如果你傳入了一個原始值(字符串類型、布爾類型或者數(shù)字類型)來當(dāng)作this的綁定對象,這個原始值會被轉(zhuǎn)換成它的對象形式(也就是new String(..)、new Boolean(..) 或者new Number(..))。這通常被稱為“裝箱”。但是在嚴(yán)格模式下this不會被強(qiáng)制轉(zhuǎn)換為一個對象,也就是說傳入原始值來當(dāng)做this的綁定對象,那么它不會轉(zhuǎn)換為對象形式

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,但是其內(nèi)部的foo()仍然是一個不帶任何修飾的函數(shù)調(diào)用,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

我們創(chuàng)建了函數(shù)bar,并在它的內(nèi)部手動調(diào)用了foo.call(obj),因此強(qiáng)制把foothis 綁定到了obj,無論之后如何調(diào)用函數(shù)bar,this始終綁定到obj。

一般來說,可以創(chuàng)建一個可重復(fù)使用的硬綁定輔助函數(shù):

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
// 簡單的輔助綁定函數(shù)
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函數(shù)就可以將foo函數(shù)的this始終綁定為obj,由于硬綁定是一種非常常用的模式,所以在ES5中提供了內(nèi)置的方法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(..)會返回一個硬編碼的新函數(shù),調(diào)用這個新函數(shù)時會把原始函數(shù)的this綁定到傳入bind(..)的參數(shù)上并調(diào)用原始函數(shù),所以foo.bind( obj )會返回一個新函數(shù),然后被賦值給bar,調(diào)用bar時會把foo中的this綁定到obj,并且調(diào)用foo函數(shù)。

API調(diào)用的“上下文”

除了上面說的硬綁定可以強(qiáng)制給this一個綁定,第三方庫的許多函數(shù),以及JavaScript語言和宿主環(huán)境中許多新的內(nèi)置函數(shù),都提供了一個可選的參數(shù),通常被稱為“上下文”(context),其作用和bind(..) 一樣,確保你的回調(diào)函數(shù)使用指定的this。

比如說:

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

array.forEach(function(currentValue, index, arr), thisValue)方法用于調(diào)用數(shù)組的每個元素,并將元素傳遞給回調(diào)函數(shù),它的第二個參數(shù)thisValue就可以指定回調(diào)函數(shù)中的this(如果這個參數(shù)為空,那么this將綁定到全局對象或者undefined上);forEach內(nèi)部實(shí)際上就是通過call(..) 或者apply(..) 實(shí)現(xiàn)了顯式綁定

其他函數(shù)還有array.maparray.filter,array.every,array.some

new綁定

最后一條this的綁定規(guī)則,在講解它之前我們首先需要澄清一個非常常見的關(guān)于JavaScript 中函數(shù)和對象的誤解。

在傳統(tǒng)的面向類的語言中,“構(gòu)造函數(shù)”是類中的一些特殊方法,使用new初始化類時會調(diào)用類中的構(gòu)造函數(shù)。通常的形式是這樣的:

    something = new MyClass(..);

JavaScript也有一個new操作符,使用方法看起來也和那些面向類的語言一樣,但是,JavaScriptnew的機(jī)制實(shí)際上和面向類的語言完全不同

首先我們重新定義一下JavaScript中的“構(gòu)造函數(shù)”:在JavaScript中,構(gòu)造函數(shù)只是一些使用new操作符時被調(diào)用的函數(shù),它們并不會屬于某個類,也不會實(shí)例化一個類。實(shí)際上,它們甚至都不能說是一種特殊的函數(shù)類型,它們只是被new操作符調(diào)用的普通函數(shù)而已。(ES6中的Class只是語法糖而已)

自定義函數(shù)和內(nèi)置對象函數(shù)(比如Number(..))都可以用new來調(diào)用,這種函數(shù)調(diào)用被稱為構(gòu)造函數(shù)調(diào)用。這里有一個重要但是非常細(xì)微的區(qū)別:實(shí)際上并不存在所謂的“構(gòu)造函數(shù)”,只有對于函數(shù)的“構(gòu)造調(diào)用”。

使用new來調(diào)用函數(shù),或者說發(fā)生構(gòu)造函數(shù)調(diào)用時,會自動執(zhí)行下面的操作:

創(chuàng)建(或者說構(gòu)造)一個全新的對象

這個新對象會被執(zhí)行[[Prototype]]鏈接([[Prototype]]指向構(gòu)造函數(shù)的原型對象

這個新對象會綁定到該構(gòu)造函數(shù)中的this

執(zhí)行構(gòu)造函數(shù)中的代碼

如果該構(gòu)造函數(shù)沒有返回其他對象,那么會自動返回這個新對象

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

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

使用new來調(diào)用foo(..)時,我們會構(gòu)造一個新對象(賦值給了變量bar)并把它綁定到foo函數(shù)中的this上,foo函數(shù)中的this綁定的就是對象bar

綁定規(guī)則的優(yōu)先級

在了解了四種綁定規(guī)則后,我們需要了解一下他們之間的優(yōu)先級,因?yàn)橛袝r候會出現(xiàn)符合多種規(guī)則的情況。

毫無疑問,默認(rèn)綁定的優(yōu)先級是四條規(guī)則中最低的,所以我們可以先不考慮它。

隱式綁定和顯式綁定哪個優(yōu)先級更高?我們來測試一下:

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

可以明顯看出,顯示綁定優(yōu)先于隱式綁定,也就是說在判斷時應(yīng)當(dāng)先考慮是否可以應(yīng)用顯式綁定

那么隱式綁定和new綁定哪個優(yōu)先級更高?我們也來測試一下:

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綁定,相當(dāng)于vra bar = new foo(4);
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

可以看到new綁定比隱式綁定優(yōu)先級高,那么現(xiàn)在還需要知道new綁定和顯式綁定誰的優(yōu)先級更高,由于newcall/apply 無法一起使用,因此無法通過new foo.call(obj1) 來直接進(jìn)行測試,而硬綁定是顯示綁定的一種,所以我們使用硬綁定來測試它倆的優(yōu)先級:

在看代碼之前先回憶一下硬綁定是如何工作的。Function.prototype.bind(..) 會創(chuàng)建一個新的包裝函數(shù),這個函數(shù)會忽略它當(dāng)前的this綁定(無論綁定的對象是什么),并把我們提供的對象綁定到this上。

這樣看起來硬綁定(也是顯式綁定的一種)似乎比new 綁定的優(yōu)先級更高,應(yīng)該無法使用new來控制this綁定,那實(shí)際上是如何的呢?來讓代碼揭曉答案:

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函數(shù)中的的this被硬綁定到obj1上,但是new bar(3)并沒有像我們前面預(yù)計的那樣把obj1.a修改為3,這說明使用new來調(diào)用bar()的時候,bar函數(shù)中的this綁定的不是obj1(否則obj1.a應(yīng)該被修改為3),所以使用new仍然可以控制this綁定,實(shí)際上此時bar函數(shù)中的this綁定的是一個新對象,這個新對象最后賦值給了baz,所以baz.a的值為3。

為什么與預(yù)想的不同?因?yàn)?b>ES5 中內(nèi)置的Function.prototype.bind(..)方法的內(nèi)部會進(jìn)行判斷,會判斷硬綁定函數(shù)是否是被new調(diào)用,如果是的話就會使用新創(chuàng)建的this替換硬綁定的this。

所以new綁定的優(yōu)先級高于顯示綁定。

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])方法,可以傳入?yún)?shù)序列,當(dāng)綁定函數(shù)被調(diào)用時,這些參數(shù)將置于實(shí)參之前傳遞給被綁定的函數(shù),所以bind(..)的功能之一就是可以把除了第一個參數(shù)(第一個參數(shù)用于綁定this)之外的其他參數(shù)都傳給下層的函數(shù)(這種技術(shù)稱為“部分應(yīng)用”,是“柯里化”的一種)。

正是由于bind(...)的這一功能,如果我們在new中使用硬綁定函數(shù),那么就可以預(yù)先設(shè)置函數(shù)的一些參數(shù),這樣在使用new進(jìn)行初始化時就可以只傳入其余的參數(shù),這也就是為什么有些時候會在new中使用硬綁定函數(shù)的原因,看個例子:

function foo(p1,p2) {
    this.val = p1 + p2;
}
// 之所以使用null 是因?yàn)樵诒纠形覀儾⒉魂P(guān)心硬綁定的this是什么
// 反正使用new的時候this會被修改
var bar = foo.bind( null, "p1" );//傳入預(yù)先設(shè)置的參數(shù)p1
var baz = new bar( "p2" );//只需傳入剩余的參數(shù)p2
baz.val; // p1p2
優(yōu)先級總結(jié)

綜上所述,優(yōu)先級如下:
new綁定 > 顯示綁定 > 隱式綁定 > 默認(rèn)綁定

那么我們在確定this的時候就可以根據(jù)下面的步驟來:

函數(shù)是否使用new調(diào)用(new綁定)?如果是的話this綁定的是新創(chuàng)建的對象。

var bar = new foo()

函數(shù)是否通過call、apply(顯式綁定)或者硬綁定bind調(diào)用?如果是的話,this綁定的是指定的對象。

var bar = foo.call(obj2)

函數(shù)是否在某個上下文對象中調(diào)用(隱式綁定)?如果是的話,this綁定的是那個上下文對象。

var bar = obj1.foo()

如果都不是的話,使用默認(rèn)綁定。如果在嚴(yán)格模式下,就綁定到undefined,否則綁定到全局對象。

var bar = foo()

對于正常的函數(shù)調(diào)用來說,理解了這些知識就可以明白this的綁定原理了,不過……凡事總有例外!??!

綁定的特殊情況

在某些場景下this的綁定行為會出乎意料,你認(rèn)為應(yīng)當(dāng)應(yīng)用其他綁定規(guī)則時,實(shí)際上應(yīng)用的可能是默認(rèn)綁定規(guī)則。

被忽略的this

如果你把null或者undefined作為this的綁定對象傳入call、apply 或者bind,這些值在調(diào)用時會被忽略,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則:

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

那么什么情況下你會傳入null呢?一種非常常見的做法是使用apply(..)來“展開”一個數(shù)組,并當(dāng)作參數(shù)傳入一個函數(shù)(ES6中可以直接使用...操作符)。類似地,bind(..)可以對參數(shù)進(jìn)行柯里化(預(yù)先設(shè)置一些參數(shù)),這種方法有時非常有用:

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

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

這兩種方法都需要傳入一個參數(shù)當(dāng)作this的綁定對象。如果函數(shù)并不關(guān)心this的話,你仍然需要傳入一個占位值,這時null可能是一個不錯的選擇,就像代碼所示的那樣。

然而,總是使用null來忽略this綁定可能產(chǎn)生一些副作用。如果某個函數(shù)確實(shí)使用了this(比如第三方庫中的一個函數(shù)),那默認(rèn)綁定規(guī)則會把this綁定到全局對象(在瀏覽器中這個對象是window),這將導(dǎo)致不可預(yù)計的后果(比如修改全局對象。顯而易見,這種方式可能會導(dǎo)致許多難以分析和追蹤的bug。

一種“更安全”的做法是傳入一個特殊的對象,把this綁定到這個對象不會對你的程序產(chǎn)生任何副作用。就像網(wǎng)絡(luò)(以及軍隊(duì))一樣,我們可以創(chuàng)建一個DMZdemilitarized zone,非軍事區(qū))對象,如果我們在忽略this綁定時總是傳入一個DMZ對象,那就什么都不用擔(dān)心了,因?yàn)槿魏螌τ?b>this的使用都會被限制在這個空對象中,不會對全局對象產(chǎn)生任何影響。

由于這個DMZ對象完全是一個空對象,可以使用一個特殊的變量名來表示它,比如?(這是數(shù)學(xué)中表示空集合符號的小寫形式)。在JavaScript中創(chuàng)建一個空對象最簡單的方法都是Object.create(null),Object.create(null){}很像,但是并不會創(chuàng)建Object.prototype這個委托,所以它比{}“更空”,所以之前的例子可以改為:

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

另一個需要注意的是,你有可能(有意或者無意地)創(chuàng)建一個函數(shù)的“間接引用”,在這種情況下,調(diào)用這個函數(shù)會應(yīng)用默認(rèn)綁定規(guī)則。

間接引用最容易在賦值時發(fā)生:

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

賦值表達(dá)式的返回值是要賦的值,所以p.foo = o.foo的返回值是目標(biāo)函數(shù)的引用,即foo函數(shù)的引用,因此調(diào)用位置是foo()而不是p.foo()或者o.foo()。根據(jù)我們之前說過的,這里會應(yīng)用默認(rèn)綁定。

軟綁定

之前我們已經(jīng)看到過,硬綁定這種方式可以把this強(qiáng)制綁定到指定的對象(除了使用new時),防止函數(shù)調(diào)用應(yīng)用默認(rèn)綁定規(guī)則。問題在于,硬綁定會大大降低函數(shù)的靈活性,使用硬綁定之后就無法使用隱式綁定或者顯式綁定來修改this。

如果可以給默認(rèn)綁定指定一個全局對象和undefined以外的值,那就可以實(shí)現(xiàn)和硬綁定相同的效果,同時保留隱式綁定或者顯式綁定修改this的能力。

可以通過一種被稱為軟綁定的方法來實(shí)現(xiàn)我們想要的效果:

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

除了軟綁定之外,softBind(..) 的其他原理和ES5內(nèi)置的bind(..) 類似。它會對指定的函數(shù)進(jìn)行封裝,首先檢查調(diào)用時的this,如果this綁定到全局對象或者undefined,那就把指定的默認(rèn)對象obj綁定到this,否則不會修改this。此外,這段代碼還支持可選的柯里化(詳情請查看之前和bind(..)相關(guān)的介紹),看看軟綁定的實(shí)例:

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 <---- 應(yīng)用了軟綁定,this本來綁定到全局對象,通過軟綁定綁定到了obj

可以看到,軟綁定版本的foo()可以手動將this綁定到obj2或者obj3上,但如果應(yīng)用默
認(rèn)綁定,則會將this綁定到obj

特殊的箭頭函數(shù)

我們之前介紹的四條規(guī)則已經(jīng)可以包含所有正常的函數(shù)。但是ES6中介紹了一種無法使用這些規(guī)則的特殊函數(shù)類型:箭頭函數(shù)。

箭頭函數(shù)并不是使用function關(guān)鍵字定義的,而是使用被稱為“胖箭頭”的操作符=>定義的。箭頭函數(shù)不使用this的四種標(biāo)準(zhǔn)規(guī)則,而是根據(jù)外層(函數(shù)或者全局)作用域來決定this。

我們來看看箭頭函數(shù)的詞法作用域:

function foo() {
    // 返回一個箭頭函數(shù)
    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()內(nèi)部創(chuàng)建的箭頭函數(shù)會捕獲調(diào)用foo()時的this。由于foo()this綁定到obj1bar(引用箭頭函數(shù))的this也會綁定到obj1,箭頭函數(shù)的綁定無法被修改。(new也不行?。?/p>

箭頭函數(shù)最常用于回調(diào)函數(shù)中,例如事件處理器或者定時器:

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

箭頭函數(shù)可以像bind(..)一樣確保函數(shù)的this被綁定到指定對象,此外,其重要性還體現(xiàn)在它用更常見的詞法作用域取代了傳統(tǒng)的this機(jī)制。實(shí)際上,在ES6之前我們就已經(jīng)在使用一種幾乎和箭頭函數(shù)完全一樣的模式:

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和箭頭函數(shù)看起來都可以取代bind(..),但是從本質(zhì)上來說,它們想替代的是this機(jī)制,如果你經(jīng)常編寫this風(fēng)格的代碼,但是絕大部分時候都會使用self = this或者箭頭函數(shù)來否定this機(jī)制,那你或許應(yīng)當(dāng):

只使用詞法作用域并完全拋棄錯誤this風(fēng)格的代碼;

完全采用this風(fēng)格,在必要時使用bind(..),盡量避免使用self = this和箭頭函數(shù)。

當(dāng)然,包含這兩種代碼風(fēng)格的程序可以正常運(yùn)行,但是在同一個函數(shù)或者同一個程序中混合使用這兩種風(fēng)格通常會使代碼更難維護(hù),并且可能也會更難編寫。

疑問解答

先來說一下最前面的一個例子的真實(shí)情況:

function foo(num) {
    console.log( "foo: " + num );
    // 記錄foo 被調(diào)用的次數(shù)
    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 被調(diào)用了多少次?
console.log( foo.count ); // 0 -- WTF?

先說結(jié)論,this指向全局對象,而this.countNaN。
通過分析我們可以知道foo的調(diào)用位置是全局作用域,然后foo處于非嚴(yán)格模式,所以this指向全局對象,由于this.count的值一開始為undefined,然后進(jìn)行this.count++;的操作,所以變成NaN

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

嚴(yán)格模式下,禁止this關(guān)鍵字指向全局對象,此時this會綁定到undefined;所以當(dāng)函數(shù)定義在嚴(yán)格模式下或函數(shù)內(nèi)的代碼運(yùn)行在嚴(yán)格模式下時,其中的this綁定的是undefined;特別注意如果僅僅是函數(shù)的調(diào)用語句運(yùn)行在嚴(yán)格模式下,那么不受影響,該函數(shù)內(nèi)的this仍然綁定到全局對象

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

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

setTimeout(foo,100);//Window

foo的函數(shù)體處于嚴(yán)格模式下,為什么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-關(guān)于this的問題中,找到一段備注:

備注:在嚴(yán)格模式下,setTimeout( )的回調(diào)函數(shù)里面的this仍然默認(rèn)指向window對象, 并不是undefined

但是這個僅僅是告訴了我們結(jié)論,并沒有給出為什么。經(jīng)過思考,我給出我自己的猜想,也不知道對不對:

我們知道setTimout是掛在Window下的方法,所以調(diào)用時實(shí)際上是Window.setTimout,是通過Window對象調(diào)用的,一般認(rèn)為setTimout的偽代碼是下面這樣:

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

但是通過前文的介紹,我們知道

直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用的,它的調(diào)用位置是全局作用域,非嚴(yán)格模式下綁定到全局對象,嚴(yán)格模式下綁定到undefined

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

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

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

所以我猜想,setTimout的偽代碼是下面這樣:

function setTimeout(fn,delay) {
    // 等待delay 毫秒
    //直接執(zhí)行fn內(nèi)的代碼,而不是調(diào)用fn(相當(dāng)于把fn中的代碼粘貼到此處) 
}

基于這種猜想,我們來看前面的測試代碼:

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

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

相當(dāng)于下面這樣:

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

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

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

//相當(dāng)于
"use strict";
Window = {
    setTimeout: function(){
        // 等待100毫秒
        console.log(this);
    }
}//通過Window調(diào)用setTimeout,this指向Window


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

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

//相當(dāng)于
Window = {
    setTimeout: function(){
        // 等待100毫秒
        "use strict";
        foo();
    }//通過Window調(diào)用setTimeout,其內(nèi)部調(diào)用了foo,而且僅僅是foo的調(diào)用處于嚴(yán)格模式,所以foo中的this指向Window
}

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

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

//相當(dāng)于
Window = {
    setTimeout: function(){
        // 等待100毫秒
        foo();
    }//通過Window調(diào)用setTimeout,其內(nèi)部調(diào)用了foo,但是foo的函數(shù)體處于嚴(yán)格模式,所以foo中的this指向undefined
}

似乎一切也說的過去,不過我暫時沒有找到權(quán)威性的資料來證實(shí),自己先這樣理解一下,如果不對,還請大家指正!

尾聲

以前對this真是不清不楚,這次徹底的順了一遍之后清晰多了,每天進(jìn)步一點(diǎn)點(diǎn),加油~

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/95579.html

相關(guān)文章

  • ES2015入門系列10-類 classes

    摘要:我們繼續(xù),這次來聊聊類。,編寫代碼角色基類判斷角色是否死亡升級受到傷害攻擊普通攻擊攻擊了造成了點(diǎn)傷害攻擊,有概率是用必殺攻擊必殺攻擊使用必殺攻擊了造成了點(diǎn)傷害游戲世界權(quán)利的游戲初始化英雄怪物集合,模擬簡單的游戲關(guān)卡。 OK, 我們繼續(xù),這次來聊聊類。 內(nèi)有 Jon Snow大戰(zhàn)異鬼, ? 熟悉后端的朋友們對類肯定都不陌生,如下面一段PHP的代碼: class Human { pr...

    Wuv1Up 評論0 收藏0
  • ES2015入門系列11-模塊 Modules

    摘要:先來介紹下語法官方示例代碼模塊中對象暴露只需要即可,可以是任何類型的對象。手動導(dǎo)入模塊下某個對象,需要和模塊中定義的名字對應(yīng),順序無關(guān)。 看一下官方介紹: Language-level support for modules for component definition. JS在ES2015開始原生支持模塊化開發(fā),我們之前也曾借助于諸如: AMD CommonJS 等的模塊加載...

    HitenDev 評論0 收藏0
  • 記一次作死 —— 被 Leetcode 封禁

    摘要:不過好消息是,在事件發(fā)生的二十四小時以后,我發(fā)現(xiàn)我的賬號解禁了,哈哈哈哈。 本文最初發(fā)布于我的個人博客:咀嚼之味 從昨天凌晨四點(diǎn)起,我的 Leetcode 賬號就無法提交任何代碼了,于是我意識到我的賬號大概是被封了…… 起因 我和我的同學(xué) @xidui 正在維護(hù)一個項(xiàng)目 xidui/algorithm-training。其實(shí)就是收錄一些算法題的解答,目前主要對象就是 Leetcode。...

    dackel 評論0 收藏0
  • 歷史首次!聯(lián)發(fā)科打敗高通成為全球第一,憑什么?

    摘要:據(jù)調(diào)研機(jī)構(gòu)數(shù)據(jù),年第三季度,全球智能手機(jī)芯片市場占有率中,聯(lián)發(fā)科力壓高通,歷史首次登頂全球第一。年月,聯(lián)發(fā)科發(fā)布全球首款十核處理器,以及它的升級版。聯(lián)發(fā)科本月表示,其最新的旗艦芯片將于明年第一季度發(fā)布,希望在農(nóng)歷新年前推出。在被喊了一年的MTK YES后,聯(lián)發(fā)科終于迎來了自己的YES時刻。據(jù)調(diào)研機(jī)構(gòu)Counterpoint數(shù)據(jù),2020年第三季度,全球智能手機(jī)芯片市場占有率中,聯(lián)發(fā)科力壓高通...

    Tecode 評論0 收藏0

發(fā)表評論

0條評論

閱讀需要支付1元查看
<