理性分析 JavaScript 中的 this

在每個方法中,關鍵字 this 表示隱式參數。 —— 《Java 核心技術 卷Ⅰ》javascript


this 是什麼?

瞭解 python 的同窗可能會知道,python 構造函數中老是會出現 self 參數。這個參數用來表示建立的實例對象。java

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
複製代碼

在 JavaScript 和 Java 中這個參數被隱藏了。咱們沒必要在參數列表中顯式聲明這個參數,就能夠在函數中使用這個參數。這個參數就是 this 。python

function Student(name, score){
	this.name = name
	this.score = score
}
var studentA = new Student('a', 100)
console.log(studentA.name, studentA.score) // a 100
複製代碼

隱式參數

援引 《Java 核心技術 卷Ⅰ》 中的一句話:在每個方法中,關鍵字 this 表示隱式參數。 所謂的隱式參數,就是沒有在參數列表中顯式聲明的參數。隱式參數和參數列表中定義的顯式參數統稱爲形式參數。與形式參數相對應的是實際參數。bash

形式參數和實際參數

形式參數,簡稱形參。形參就是在定義函數的時候使用的參數,用來接收調用該函數時傳遞的參數。如上述代碼中的 name ,score 參數都是形參。閉包

實際參數,簡稱實參,實參就是調用該函數時傳遞的參數。如上述代碼中的 'a' , 100 都是實參。app

爲何 this 的值是在調用時肯定的?

《 你不知道的JavaScript(上卷)》中提了一個問題,問:爲何採用詞法做用域的 JavaScript 中的 this 的值是在調用時肯定的?函數

在理解了形參和實參以後,咱們便能很好地理解這個問題了。oop

由於 this 是一個形參,形參的值是由實參決定的。而傳參這個操做時在調用時發生的,因此 this 的值是在調用時肯定的。ui


this 的值

既然 this 的值是由實參的值決定的,那麼這個實參的值究竟是什麼呢?this

參考 《Java 核心技術 卷Ⅰ》 中的一句話:隱式參數的值是出如今函數名以前的對象。看成爲構造函數時,this 用來表示建立的實例對象。來看兩個例子:

function bar () {
  console.log(this.name)
}
var foo = {
  name: 'foo',
  bar: bar
}
foo.bar() // foo
複製代碼

this 指向函數名(bar)以前的 foo 對象

function Student(name, score){
	this.name = name
	this.score = score
}
var studentA = new Student('a', 100)
console.log(studentA.name, studentA.score) // a 100
複製代碼

this 指向建立的實例對象 studentA

call apply bind

JavaScript 也提供了幾個函數去改變 this 的值。這幾個函數都會返回一個原函數的拷貝,並在這個拷貝上傳遞 this 的值。因此從結果上看,咱們能夠看到原有的 this 會被覆蓋。

function bar () {
  console.log(this.name)
}
var foo = {
  name: 'foo',
  bar: bar
}
var obj = {
  name: 'obj',
}
foo.bar.call(obj) // obj
複製代碼

this 指向新的對象 obj 。

爲何 this 指向了全局對象?

《 你不知道的JavaScript(上卷)》中描述了一種現象:this 丟失了原來的綁定對象,指向了全局對象。書中稱爲隱式丟失。來看示例:

function foo() { 
	console.log( this.a )
}
var obj = { 
	a: 2,
	foo: foo 
}
var bar = obj.foo // 賦值操做
var a = "oops, global" 
bar() // "oops, global"
複製代碼

JavaScript 只有值傳遞,沒有引用傳遞。在賦值操做的時候,實際上是將一個引用的拷貝賦值給另一個變量。var bar = obj.foo 在這個語句中,沒有傳參操做,因此 this 的值是由 bar 函數在調用時傳遞的那個實參決定的。這個實參如未顯式指定,那麼即是指向全局對象。因此上述代碼中的 this 指向了全局對象。

一樣的,咱們在函數傳參的過程當中,常常發現隱式丟失問題,緣由也是中間發生了一次賦值操做。代碼示例以下:

var name = 'global'
function bar () {
  console.log(this.name)
}
var foo = {
  name: 'foo',
  bar: bar
}
function callFunc(func){
  func()
}
callFunc(foo.bar) // global
複製代碼

在傳參的過程當中,發生了func = foo.bar的賦值操做,致使最後 this 的值指向了全局對象。

可是若是咱們使用 bind 綁定了 this 的值,那麼在發生賦值操做時,this 的值將再也不改變。來看下面例子。

再談 bind

bind 和 call ,apply 有一點不一樣的是 call,apply 返回的是調用結果,而 bind 返回的是綁定 this 後的函數對象。那麼當綁定 this 後的函數做爲實參傳入函數時,與未綁定 this 的結果就徹底不一樣了。

來看下面的例子。

var name = 'global'
function bar () {
  console.log(this.name)
}
var foo = {
  name: 'foo',
  bar: bar
}
function callFunc(func){
  func()
}
callFunc(foo.bar.bind(foo)) // foo
複製代碼

將 bar 函數中的 this 綁定到 foo 再傳入 callFunc 函數中,最後打印的結果是 foo 。

實際上, bind 函數內部維護了一個閉包,使得調用始終發生在函數內部,來保證 this 的值不變。來看 MDN 提供的 ployfill

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable')
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 aArgs.concat(Array.prototype.slice.call(arguments)))
        };

    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype
    }
    fBound.prototype = new fNOP()

    return fBound
  }
}
複製代碼
// return 部分
 return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 aArgs.concat(Array.prototype.slice.call(arguments)))
複製代碼

在 return 的時候使用了 apply 函數來改變 this ,若未發生 new 操做,那麼這個 this 的值將綁定到 bind 函數提供的那個對象。

new 操做

當發生 new 操做時,this 將綁定到這個實例對象。 從上面這個 ploy fill 能夠看出 new 操做中的 this 值會覆蓋原有 this 的值。來看例子

function bar () {
  this.name = 'bar'
}
var foo = {
  name: 'foo',
}

var a = bar.bind(foo)
a()
console.log(foo.name) // bar
var b = new a()
console.log(b.name) // bar
複製代碼

當執行 new 操做以前,a 函數中的 this 指向 foo。當執行 new 操做以後,a 函數中的 this 指向了 b 。

new 操做會返回一個從新綁定 this 後的新對象。因此當發生 new 操做以後,原有的 this 發生了改變。具體步驟以下:

  1. 建立(或者說構造)一個全新的對象。
  2. 這個新對象會被執行 [[ 原型 ]] 鏈接。
  3. 這個新對象會綁定到函數調用的 this 。
  4. 若是函數沒有返回其餘對象,那麼 new 表達式中的函數調用會自動返回這個新對象。

箭頭函數中的 this

箭頭函數中的 this 繼承了父做用域的 this。

var name = 'global'
var foo = {
  name: 'foo',
  bar: () => {
	console.log(this.name)
  }
}

foo.bar() // global
複製代碼

箭頭函數的父做用域爲全局做用域,全局做用域的 this 指向全局對象,因此 this 指向了全局對象。

var name = 'global'
var foo = {
  name: 'foo',
  bar: function () {
	setTimeout(() => {
	  console.log(this.name)
	},100)
  }
}

foo.bar() // foo
複製代碼

箭頭函數的父做用域爲 bar 函數,在調用時,父做用域 bar 函數中的 this 指向了 foo 函數,因此箭頭函數中的 this 指向了 foo 。

嚴格模式下的 this

嚴格模式下禁止 this 指向全局對象。在嚴格模式下當 this 指向全局對象的時候會變成 undefined 。


總結

  1. this 指向建立的實例對象或函數名以前的對象。如未指定,即是指向全局對象。
  2. 因爲 call 、apply 、bind 函數會返回一個原函數的拷貝,並在這個拷貝上傳遞 this 值。因此當使用 call 、apply 、bind 函數會覆蓋原有的 this 值。
  3. new 操做能夠覆蓋 call、apply、bind 綁定的 this 值。

tips

  • 嚴格模式下禁止 this 指向全局對象。在嚴格模式下當 this 指向全局對象的時候會變成 undefined 。
  • 在發生賦值操做時,因爲引用複製, this 的值指向被賦值變量的調用對象。
  • ES 6 中新增箭頭函數,能夠繼承父做用域的 this ,能夠解決 this 隱式丟失的問題。

相關知識點

  • 詞法做用域和動態做用域
  • 閉包
  • 做用域和做用域鏈
  • 嚴格模式
  • ES6 新增特性
  • 引用傳遞和值傳遞
相關文章
相關標籤/搜索