你不知道的JavaScript:this全面解析

我們現(xiàn)在知道了每個函數(shù)的this是在運行的時候進行綁定的,完全取決于函數(shù)的調(diào)用位置,也就是該函數(shù)的調(diào)用方法。

關于調(diào)用位置

在理解this的綁定過程之前,我們了解一下調(diào)用位置,調(diào)用位置表示的是函數(shù)所被調(diào)用的位置,而不是其聲明的位置。

如何知道函數(shù)的調(diào)用位置,最重要的是分析函數(shù)的調(diào)用棧(即為了到達當前執(zhí)行位置所調(diào)用的所有函數(shù))。那么調(diào)用位置就是當前正在執(zhí)行函數(shù)的前一個調(diào)用中。

function baz() {
  //當前的調(diào)用棧是baz
  //當前的調(diào)用位置是全局作用域,即當前調(diào)用棧的前一個調(diào)用
  console.log('baz');
  bar();
}

function bar() {
  //當前調(diào)用棧是 baz-->bar
  //當前的調(diào)用位置是:baz
  console.log('bar');
  foo();
}

function foo() {
  //當前的調(diào)用棧是baz --> bar --> foo
  //當前調(diào)用位置是bar
}

baz(); //<-- baz的調(diào)用位置就是全局作用域

注意如上,我們是如何分析調(diào)用棧和調(diào)用位置的,因此這樣決定了this的綁定。

關于綁定規(guī)則

接下來我們看下在函數(shù)的執(zhí)行過程中,調(diào)用位置如何決定this的綁定對象。

  • 默認綁定規(guī)則

最常見的函數(shù)調(diào)用類型是獨立函數(shù)調(diào)用,如下:

function foo() {
  console.log(this.a);
}

var a = 2;

foo(); //2

在上面的代碼中,我們看到了輸出的值為2,我們知道在全局作用域中聲明的變量會變成是全局對象的一個屬性。我們在調(diào)用foo()的時候,函數(shù)調(diào)用應用了this的默認綁定,因此this指向的是的全局對象。

為什么說是默認綁定呢?因為foo()的調(diào)用是不帶有任何修飾的函數(shù)引用進行調(diào)用的,因此只能是使用默認綁定規(guī)則。

  • 隱式綁定

另外一種需要考慮的規(guī)則是調(diào)用位置是否有上下文對象,或者說是否被某個對象擁有或者包含。

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2,
  foo: foo()
}

obj.foo(); //2

看上面的代碼,注意無論直接在obj中定義foo函數(shù)還是先定義了foo函數(shù),然后添加為obj的引用,這個函數(shù)嚴格來說都不屬于obj對象。然而,調(diào)用位置會使用obj上下文來引用函數(shù)。

當函數(shù)引用有上下文對象時,隱式綁定會把函數(shù)調(diào)用中的this綁定到這個上下文對象中。所以上面的this.a和obj.a是一樣的。

有一點值得注意的就是隱式丟失,就是被隱式綁定的函數(shù)會丟失其綁定對象而會應用默認綁定,從而將this綁定到全局作用域或者undefined上,這個要取決于是否是嚴格模式。

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2,
  foo: foo
}

var bar = obj.foo;
var a = 'oops global';
bar(); //'oops global

如上,bar是obj.foo的一個引用,但是實際上其引用的是foo的本身,因此此時的bar是不帶有任何修飾符的函數(shù)調(diào)用,因此引用應用了默認綁定。

function foo() {
  console.log(this.a);
}

function doFoo(fn) {
  //其實引用的是foo
  fn(); //<--調(diào)用位置
}

var obj = {
  a: 2,
  foo: foo
}

var  a = 'oops global';

doFoo(obj.foo); //'oops global

參數(shù)傳遞其實就是一種隱式賦值,因此我們傳入函數(shù)時也會被隱式的賦值。

看到上面的例子,可以發(fā)現(xiàn)在日常使用回調(diào)函數(shù)丟失this的綁定是非常常見的。

  • 顯示綁定

前面的隱式綁定我們可以看到,函數(shù)的調(diào)用是通過一個對象內(nèi)部的一個屬性進行引用的,從而將this綁定到這個對象上。

這里我們先介紹兩個方法apply()和call(),他們的第一個參數(shù)是一個對象,這個是給this準備的,這樣在調(diào)用的時候可以將其綁定到this上。

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2
}

foo.call(obj); //2

這個應該是比較好理解的,在通過call調(diào)用的時候,強制的把它的this綁定到obj上。

然而顯示綁定仍然無法解決this丟失的問題。

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2
}

var bar = function() {
  foo.call(obj);
}

bar(); //2
setTimeout(bar, 200); //2
//硬綁定的bar不可以再修改它的this
bar.call(window); //2

如上我們用個函數(shù)包裹住foo,在函數(shù)體內(nèi)部調(diào)用foo,并顯示綁定了this到obj對象上,那么后面無論如何調(diào)用函數(shù)bar,但是foo的調(diào)用始終都是在obj上調(diào)用,這就是一種顯示的硬綁定。

我們常用的Function.prototype.bind就是一種很好的內(nèi)置方法。硬綁定的場景就是創(chuàng)建一個包裹函數(shù),負責接受參數(shù)并返回值:

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}

var obj = {
  a: 2
}

var bar = function() {
  return foo.apply(obj, arguments);
}

var b = bar(3); // 2 3
console.log(b); //5

另一種方法就是創(chuàng)建一個可以重復使用的輔助函數(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

在ES5提供的bind方法中會返回一個硬編碼的新函數(shù),它會把你指定的參數(shù)設置為this的上下文并調(diào)用原始函數(shù)。

-API調(diào)用的上下文,你可以看到很多第三方庫和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);
//這些函數(shù)實際上就是通過call或者apply實現(xiàn)了顯示的綁定。
  • new綁定

在JavaScript中平常所聲明的一些函數(shù)其實也就是我們常說的構造函數(shù),沒有什么特別的區(qū)別。因為在JavaScript中所有的函數(shù)都可以通過new操作符進行調(diào)用,實際上并不存在什么所謂的構造函數(shù),只有對函數(shù)的構造調(diào)用。

使用new操作符來調(diào)用函數(shù)的時候,會有下面的操作:
1. 創(chuàng)建一個全新的對象;
2. 這個新對象會被執(zhí)行[[Prototype]]連接。
3. 這個新對象會綁定到函數(shù)調(diào)用的this。
4. 如果函數(shù)沒有返回其它對象,那么new表達式中的函數(shù)調(diào)用會自動返回這個全新的對象。

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

console.log(bar.a); //2

如上,使用new調(diào)用foo的時候,我們會創(chuàng)造一個新對象并把它綁定到foo調(diào)用中的this上。

小結

以上四種this的綁定規(guī)則我們已經(jīng)介紹過了,我們要做的就是找到函數(shù)的調(diào)用位置并應用對應的調(diào)用規(guī)則就可以了。

綁定例外

如果將null或者是undefined作為this的綁定對象傳入call、apply、bind,這些值在調(diào)用的時候會被忽略,實際上應用的是默認綁定規(guī)則。

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

一般常見的做法是使用apply(...)來'展開'一個數(shù)組,并當做參數(shù)傳入一個函數(shù)。類似的,bind(...)可以對參數(shù)進行柯里化(預先設置一些參數(shù)),這種方法有時候非常有用。

function foo() {
  console.log("a:" + a, "b"+ b);
}

//把數(shù)組展開成參數(shù)
foo.apply(null, [2, 3]); //a: 2, b: 3

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

更安全的this是傳入一個空對象,把this綁定到這個對象上,不會對你的代碼產(chǎn)生任何的副作用。

在JavaScript中創(chuàng)建一個空對的方法是Object.create(null)。該方法和{}很像,但是不會創(chuàng)建Object.prototype這個委托,所以可以說比{}更空。

function foo(a, b){
  console.log('a:'+a, 'b:'+b);
}

//創(chuàng)建個空對象
var ? = Object.create(null);

//把數(shù)組展開成參數(shù)
foo.apply(?, [2,3]); //a: 2, b:3

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

有的時候可能會創(chuàng)建一個函數(shù)的間接引用,那么在這種情況下,調(diào)用這個函數(shù)會應用默認規(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;
p.foo(); //2

上面的賦值表達式返回的是目標函數(shù)的引用,因此調(diào)用位置是foo()而不是p.foo()或者o.foo()。

this詞法

在ES6中,我們使用了更為簡便的方法來實現(xiàn)函數(shù),那就是箭頭函數(shù),它是根據(jù)外層函數(shù)或者全局作用域來實現(xiàn)this的綁定。

function foo() {
  return (a) => {
    console.log(this.a); //this繼承自foo()
  }
}

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綁定到了obj1,bar的this也會綁定到obj1,箭頭函數(shù)的綁定無法被修改。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容