this到底指向哪裡

寒東設計師發表於2018-04-10

為什麼要用this

      回答這個問題我們就先看看如果不使用this會出現什麼問題。試想下面程式碼如果不使用this應該怎麼寫:

function speak(){
    var name = this.name
    console.log(`Hello I am ${name}`)
}
var me = {
    name: 'a',
    speak: speak
}
var you = {
    name: 'b',
    speak: speak
}
me.speak()  //Hello I am a
you.speak()  //Hello I am b
複製程式碼

      this可以在同一個執行環境中使用不同的上下文物件。它其實提供了一種更加優雅的方式來隱式“傳遞”一個物件引用,因此可以使API設計的更加簡潔且易於複用。

this到底是誰

      this既不是自身也不是當前函式的作用域。我們可以通過程式碼來測試。

  • 判斷是不是自身
function fn(){
    console.log(this.name)
}
fn.name = 'xxx'
fn()  //undefined
複製程式碼
  • 判斷是不是作用域
function foo() { 
    var a = 2;
    this.bar(); 
}
function bar() { 
    console.log( this.a );
}
foo(); //undefined
複製程式碼

      那麼this到底是誰呢?其實不一定,this是執行時繫結的,所以取決於函式的執行上下文。

當一個函式被呼叫時,會建立一個活動記錄(執行上下文)。這個記錄會包含函式在哪裡被呼叫(呼叫棧)、函式的呼叫方法、傳入的引數等資訊,this也是這裡的一個屬性。

如何判斷this指向

      確定this指向就是確定函式的執行上下文,也就是“誰呼叫的它”,有以下幾種判斷方式:

獨立函式呼叫

function foo(){
    console.log(this.a)
}
var a = 2
foo()  // 2
複製程式碼

      這種直接呼叫的方式this指向全域性物件,如果是在瀏覽器就指向window

物件上下文(隱式繫結)

function foo() { 
    console.log( this.a );
}
var obj = { 
    a: 2,
    foo: foo
};
obj.foo(); // 2
複製程式碼

      foo雖然被定義在全域性作用域,但是呼叫的時候是通過obj上下文引用的,可以理解為在foo呼叫的那一刻它被obj物件擁有。所以this指向obj
      這裡有兩個問題:

  • 鏈式呼叫

      鏈式呼叫的情況下只有最後一層才會影響呼叫位置,例如:

obj1.obj2.obj3.fn() //這裡的fn中的this指向obj3
複製程式碼
  • 引式丟失
function foo() { 
    console.log( this.a );
}
var obj = { 
    a: 2,
    foo: foo 
};
var bar = obj.foo; // 函式別名!
var a = "xxxxx"
bar(); // xxxxx
複製程式碼

      這裡的bar其實是引用了obj.foo的地址,這個地址指向的是一個函式,也就是說bar的呼叫其實符合“獨立函式呼叫”規則。所以它的this不是obj

回撥函式其實就是隱式丟失

      稍微改一下上面的程式碼:

function foo() { 
    console.log( this.a );
}
var obj = { 
    a: 2,
    foo: foo 
};
var a = "xxxxx"
setTimeout( obj.foo ,100); // xxxxx
複製程式碼

      我們看到,回撥函式雖然是通過obj引用的,但是this也不是obj了。其實內建的setTimeout()函式實現和下面的虛擬碼類似:

function setTimeout(fn, delay){
    //等待delay毫秒
    fn()
}
複製程式碼

      其實這段程式碼隱藏這一個操作就是fn=obj.foo,這和上面例子中的bar=obj.foo異曲同工。

顯式繫結

      顯式繫結的說法是和隱式繫結相對的,指的是通過callapplybind顯式地更改this指向。
      這三個方法第一個引數是this要指向的物件。
      注意,如果你給第一個引數傳遞一個值(字串、布林、數字)型別的話,這個值會被轉換成物件形式(呼叫new String(..)、new Boolean(..)、new Number(..))。
      這三個方法中的bind方法比較特殊,它可以延遲方法的執行,這可以讓我們寫出更加靈活的程式碼。它的原理也很容易模擬:

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
複製程式碼

注意:如果第一個引數傳入null或者undefined,這個值會被忽略,相當於符合獨立函式呼叫規則

new繫結

      Js中new與傳統的面向類的語言機制不同,Js中的“建構函式”其實和普通函式沒有任何區別。
      其實當我們使用new來呼叫函式的時候,發生了下列事情:

  • 建立一個全新的物件
  • 這個新物件會被執行“原型”連結
  • 這個新物件會被繫結到呼叫的this
  • 如果函式沒有物件型別的返回值,這個物件會被返回

      其中,第三步繫結了this,所以“建構函式”和原型中的this永遠指向new出來的例項。

優先順序

      以上四條判斷規則的權重是遞增的。判斷的順序為:

  • 函式是new出來的,this指向例項
  • 函式通過call、apply、bind繫結過,this指向繫結的第一個引數
  • 函式在某個上下文物件中呼叫(隱式繫結),this指向上下文物件
  • 以上都不是,this指向全域性物件

嚴格模式下的差異

      以上所說的都是在非嚴格模式下成立,嚴格模式下的this指向是有差異的。

  • 獨立函式呼叫: this 指向undefined
  • 物件上的方法: this 永遠指向該物件
  • 其他沒有區別

箭頭函式中的this

      箭頭函式不是通過function關鍵字定義的,也不使用上面的this規則,而是“繼承”外層作用域中的this指向。
      其實一起雖然沒有箭頭函式,我們也經常做和箭頭函式一樣效果的事情,比如說:

function foo() {
    var self = this; 
    setTimeout( function(){
        console.log( self );
    }, 100 );
}
複製程式碼

getter與setter中的this

      es6中的gettersetter函式都會把this繫結到設定或獲取屬性的物件上。

function sum() {
  return this.a + this.b + this.c;
}
var o = {
  a: 1,
  b: 2,
  c: 3,
  get average() {
    return (this.a + this.b + this.c) / 3;
  }
};
Object.defineProperty(o, 'sum', { get: sum, enumerable: true, configurable: true} );
console.log(o.average, o.sum); // logs 2, 6
複製程式碼

參考資料

MDN - this
《你不知道的javascript 上卷》

相關文章