理解This

陳屹嶠發表於2016-03-29

Know Everything About This

這是一篇目前篇幅不長,但是寫起來相當漫長的文章。中途,我翻譯了這篇文章,非常有必要一讀。

一、是什麼?

「在函式內部,有兩個特殊的物件:arguments和this」

你可能見過這樣一個函式:

function addUp(){
  return Array.from(arguments).reduce(function (prev, curr) {
    return prev + curr
  })
}

並沒有定義過它,但是arguments直接就這麼冒出來了。this和它一樣,它們自動定義在所有函式的作用域中,即使你並沒有用它。

NZ在《JavaScript高階程式設計》裡給this下了個定義:「this引用的是函式據以執行的環境物件」。這就像是很多人強調的,不要看函式在哪裡定義,要看函式在哪裡被呼叫。

二、為什麼?

this機制可以解耦物件和函式:

function identify () {
 return this.name.toUpperCase()
}

function speak () {
  var greeting = "Hello,I`m + identify.call(this)"
  console.log(greeting) 
}

var me = {
  name: `cyq`
}

var you = {
  name: `reader`
}

identify.call(me) // CYQ
identify.call(you) // READER
 
 speak.call(me) // Hello,I`m CYQ
 speak.call(you) // Hello,I`m READER

這段程式碼可以在不同的上下文物件中重複使用identifyspeak,不用針對每個物件編寫不同版本的函式。如果沒有this,就需要給函式顯式地傳遞一個上下文物件。隨著你的模式越來越複雜,顯式傳遞上下文會讓程式碼越來越混亂。當涉及到原型委託的時候,你就會明白函式可以自動引用合適的上下文物件有多重要。

三、怎麼綁?

我在kangax部落格裡看到過可能是最短的總結:

  • The keyword “this” refers to whatever is left of the dot at call-time.

  • If there`s nothing to the left of the dot, then “this” is the root scope (e.g. Window).

  • A few functions change the behavior of “this”—bind, call and apply

  • The keyword “new” binds this to the object just created

如果看過了這個四條法則,那判斷this指向就好辦了啊,倒過來看就好了:

  1. 函式是否被new修飾(new繫結)?如果是的話,this繫結的就是新建立的物件

  2. 函式是否通過call、apply()或者bind()硬繫結(顯式繫結)?如果是的話,this繫結的就是指定的物件。

  3. 函式是否在某個上下文物件呼叫(隱式繫結)?如果是的話,this繫結的就是那個物件。

  4. 如果都不是的話,使用預設繫結。如果在嚴格模式下,就繫結undefined,否則是全域性物件。

這可能是這篇文章最核心的地方,但是鑑於這四條太常見了,我假設讀者已經知道它們了。

一、陷阱

1. 指向自身

this指向函式自身,從語法角度來說是說得通的

function foo(num) { 
  this.count++
}

foo.count = 0

for(let i = 0;i < 10;i++) {
  if(i > 5) {
    foo(i)
  }
}

console.log(foo.count) // 0

如果this指向的是函式自身的話,foo.count就該是4而不是0了。仔細看看就能明白這裡是符合第四條的預設繫結的。

2. 指向詞法作用域

不要看它是在哪定義的,也不要看它是在哪被呼叫的,看它是怎麼被呼叫的

function foo() {
  let a = 2
  bar()
}

function bar() {
  console.log(this.a)
}

foo() // ReferenceError: a is not a defined

對照第四條,預設繫結。

每當你想要把this和詞法作用域的查詢混合使用時,記得提醒自己,這是沒法實現的,箭頭函式例外。

3. 隱式丟失

隱式丟失通常包括這幾種情況:

  • 賦值運算子 : (f = foo.bar)()

  • 逗號運算子 : (1, foo.bar)()

  • 回撥函式

這一點可以說是一個龐大的工程,為此我翻譯了這篇部落格。翻譯這篇部落格這是件非常累的事兒,幸運的是,看完這篇部落格之後,我明白了很多我沒想到的東西。
所有需要了解的東西都可以在這篇部落格裡找到答案。

二、問題

1. self = this是什麼?

我們知道,當程式碼進入一個函式時,作用域會把函式的引數、this 物件和 arguments物件包括在內。因此我們不可能在一個函式裡面用this取到其他函式作用域內的this物件。

var object = {
    name: `My object`,

    getName: function() {
        return function() {
            return this.name
        }
    }
}

console.log(object.getName()())

這段程式碼會輸出的是什麼?雖然看上去很像是`My Object`,但是結果是undefined.原因上面提到了:最內層的匿名函式的作用域內的this和getName的this是兩回事。那想要輸出`My Object`的話,自然要想辦法把getName的this繫結到匿名函式下:

var me = {
    name: `cyq`,

    getName: function() {
      var self = this
        return function() {
            return self.name
        }
    }
}

console.log(object.getName()())

可以看到,self=this常用的場景就是幫我們在一個內層的函式裡找到外層函式的this物件。我們需要把外層的this繫結在一個變數上,從而讓內層函式完成某種類似於詞法作用域的查詢。我個人是很不喜歡這個寫法的,後來我發現我不是一個人:搞懂JavaScript的Function.prototype.bind

如果你瞭解過箭頭函式的話,你可能知道箭頭函式可以淘汰掉這種寫法。

2. 箭頭函式

雖然少打好幾個字元就已經足夠讓人開心了,但是箭頭函式的能力還不止於此。

var me = {
    name: `cyq`,

    getName: function() {
        return () => {
            return this.name
        }
    }
}

console.log(object.getName()()) //cyq

所以箭頭函式一定對它裡面的this做了重新設計咯?答案是否定的。

箭頭函式能實現類似詞法作用域的查詢是因為,它裡面根本就沒有this物件。前面提到,this自動定義在所有函式的作用域內,內嵌函式是不能查詢到外部的this物件的,因為自己的作用域裡就有this了。而箭頭函式這個特殊的函式直接去作用域裡找this