徹底理解JavaScript中的this關鍵字

this關鍵字

前言

王福朋老師的 JavaScript原型和閉包系列 文章看了不下三遍了,做爲一個初學者,每次看的時候都會有一種 "大徹大悟" 的感受,而看完以後卻老是一臉懵逼。原型與閉包 能夠說是 JavaScirpt 中理解起來最難的部分了,也是這門面向對象語言很重要的部分,固然,我也只是瞭解到了一些皮毛,對於 JavaScript OOP 更是缺少經驗。這裏我想總結一下 Javascript 中的 this 關鍵字,王福朋老師的在文章裏也花了大量的篇幅來說解 this 關鍵字的使用,能夠說 this 關鍵字也是值得重視的。javascript

做者:正偉html

原文連接:this關鍵字java

一個問題

一道很常見的題目:下面代碼將會輸出的結果是什麼?node

const obj1 = {
  a: 'a in obj1',
  foo: () => { console.log(this.a) }
}

const obj2 = {
  a: 'a in obj2',
  bar: obj1.foo
}

const obj3 = {
  a: 'a in obj3'
}

obj1.foo()  // 輸出 ??
obj2.bar()  // 輸出 ??
obj2.bar.call(obj3)  // 輸出 ??
複製代碼

在弄明白 this 關鍵字以前,也許很難來回答這道題。git

那先從上下文環境提及吧~github

上下文環境

咱們都知道,每個 代碼段 都會執行在某一個 上下文環境 當中,而在每個代碼執行以前,都會作一項 "準備工做",也就是生成相應的 上下文環境,因此每個 上下文環境 均可能會不同。瀏覽器

上下文環境 是什麼?咱們能夠去看王福朋老師的文章(連接在文末),講解的很清楚,這裏再也不贅述。閉包

代碼段 能夠分爲三種:app

  • 全局代碼
  • 函數體
  • eval 代碼

與之對應的 上下文環境 就有:函數

  • 全局上下文
  • 函數上下文

elav 就不討論了,不推薦使用)

固然,這和 this 又有什麼關係呢?this 的值就是在爲代碼段作 "準備工做" 時賦值的,能夠說 this 就是 上下文環境 的一部分,而每個不一樣的 上下文環境 可能會有不同的 this值。

這裏我大膽的將 this 關鍵字的使用分爲兩種狀況:

  1. 全局上下文的 this
  2. 函數上下文的 this

(你也能夠選擇其餘的方式分類。固然,這也不重要了)

全局上下文中的 this

在全局執行上下文中(在任何函數體外部),this 都指向全局對象:

// 在瀏覽器中, 全局對象是 window
console.log(this === window) // true

var a = 'Zavier Tang'
console.log(a) // 'Zavier Tang'
console.log(window.a) // 'Zavier Tang'
console.log(this.a) // 'Zavier Tang'

this.b = 18
console.log(b) // 18
console.log(window.b) // 18
console.log(this.b) // 18

// 在 node 環境中,this 指向global
console.log(this === global) // true
複製代碼

注意:在任何函數體外部,都屬於全局上下文,this 都指向全局對象(window / global)。在對象的內部,也是在全局上下文,this 一樣指向全局對象(window / global)

window.a = 10
var obj = {
  x: this.a,
  _this: this
}
obj.x  // 10
obj._this === this  // true
複製代碼

函數上下文中的 this

在函數內部,this 的值取決與函數被調用的方式。

this 的值在函數定義的時候是肯定不了的,只有函數調用的時候才能肯定 this 的指向。實際上 this 最終指向的是那個調用它的對象。(也不必定正確)

1. 全局函數

對於全局的方法調用,this 指向 window 對象(node下爲 global ):

var foo = function () {
  return this
}
// 在瀏覽器中
foo() === window // true

// 在 node 中
foo() === global //true
複製代碼

但值得注意的是,以上代碼是在 非嚴格模式 下。然而,在 嚴格模式 下,this 的值將保持它進入執行上下文的值:

var foo = function () {
 "use strict"
  return this
}

foo() // undefined
複製代碼

即在嚴格模式下,若是 this 沒有被執行上下文定義,那它爲 undefined

在生成 上下文環境 時

  • 若方法被 window(或 global )對象調用,即執行 window.foo(),那 this 將會被定義爲 window(或 global );
  • 若被普通對象調用,即執行 obj.foo(),那 this 將會被定義爲 obj 對象;(後面會討論)
  • 但若未被對象調用(上面分別是被 window 對象和普通對象 obj 調用),即直接執行 foo(),在非嚴格模式下,this 的值默認指向全局對象 window(或 global ),在嚴格模式下,this 將保持爲 undefined

經過 this 調用全局變量:

var a = 'global this'

var foo = function () {
  console.log(this.a)
}
foo() // 'global this'
複製代碼
var a = 'global this'

var foo = function () {
  this.a = 'rename global this' // 修改全局變量 a
  console.log(this.a)
}
foo() // 'rename global this'
複製代碼

因此,對於全局的方法調用,this 指向的是全局對象 window (或global ),即調用方法的對象。(注意嚴格模式的不一樣)

函數在全局上下文中調用, foo() 能夠看做是 window.foo(),只不過在嚴格模式下有所限制。

2. 做爲對象的方法

當函數做爲對象的方法調用時,它的 this 值是調用該函數的對象。也就是說,函數的 this 值是在函數被調用時肯定的,在定義函數時肯定不了(箭頭函數除外)。

var obj = {
  name: 'Zavier Tang',
  foo: function () {
    console.log(this)
    console.log(this.name)
  }
}

obj.foo() // Object {name: 'Zavier Tang', foo: function} // 'Zavier Tang'

//foo函數不是做爲obj的方法調用
var fn = obj.foo // 這裏foo函數並無執行
fn() // Window {...} // undefined
複製代碼

this 的值同時也只受最靠近的成員引用的影響:

//接上面代碼
var o = {
  name: 'Zavier Tang in object o',
  fn: fn,
  obj: obj
}
o.fn() // Object {name: 'Zavier Tang in object o', fn: fn, obj: obj} // 'Zavier Tang in object o'
o.obj.foo() // Object {name: 'Zavier Tang', foo: function} // 'Zavier Tang'
複製代碼

在原型鏈中,this 的值爲當前對象:

var Foo = function () {
  this.name = 'Zavier Tang'
  this.age = 20
}

// 在原型上定義函數
Foo.prototype.getInfo = function () {
  console.log(this.name)
  console.log(this.age)
  console.log(this === tang)
}

var tang = new Foo()
tang.getInfo() // "Zavier Tang" // 20 // true
複製代碼

雖然這裏調用的是一個繼承方法,但 this 所指向的依然是 tang 對象。

也能夠看做是對象 tang 調用了 getInfo 方法,this 指向了 tang。即 this 指向了調用它的那個對象。

參考:《Object-Oriented JavaScript》(Second Edition)

3. 做爲構造函數

若是函數做爲構造函數,那函數當中的 this 即是構造函數即將 new 出來的對象:

var Foo = function () {
  this.name = 'Zavier Tang',
  this.age = 20,
  this.year = 1998,
  console.log(this)
}

var tang = new Foo()

console.log(tang.name) // 'Zavier Tang'
console.log(tang.age) // 20
console.log(tang.year) // 1998
複製代碼

Foo 不做爲構造函數調用時,this 的指向即是前面討論的,指向全局變量:

// 接上面代碼
Foo() // window {...}
複製代碼

構造函數一樣能夠看做是一個普通的函數(只不過函數名稱第一個字母大寫了而已咯),可是在用 new 關鍵字調用構造函數建立對象時,它與普通函數的行爲不一樣罷了。

4. 函數調用 applycallbind

當一個函數在其主體中使用 this 關鍵字時,能夠經過使用函數繼承自Function.prototypecallapply 方法將 this 值綁定到特定對象。即 this 的值就取傳入對象的值:

var obj1 = { name: 'Zavier1' }

var obj2 = { name: 'Zavier2' }

var foo = function () {
  console.log(this)
  console.log(this.name)
}
foo.apply(obj1) // Ojbect {name: 'Zavier1'} //'Zavier1'
foo.call(obj1) // Ojbect {name: 'Zavier1'} //'Zavier1'

foo.apply(obj2) // Ojbect {name: 'Zavier2'} //'Zavier2'
foo.call(obj2) // Ojbect {name: 'Zavier2'} //'Zavier2'
複製代碼

applycall 不一樣,使用 bind 會建立一個與 foo 具備相同函數體和做用域的函數。可是,特別要注意的是,在這個新函數中,this 將永久地被綁定到了 bind 的第一個參數,不管以後如何調用。

var f = function () {
  console.log(this.name)
}

var obj1 = { name: 'Zavier1' }
var obj2 = { name: 'Zavier2' }

var g = f.bind(obj1)
g() // 'Zavier1'

var h = g.bind(ojb2) // bind只生效一次!
h() // 'Zavier1'

var o = {
  name: 'Zavier Tang',
  f:f,
  g:g,
  h:h
}
o.f() // 'Zavier Tang'
o.g() // 'Zavier1'
o.h() // 'Zavier1'
複製代碼

到這裏,「this 最終指向的是那個調用它的對象」 這句話就不通用了,函數調用 callapplybind 方法是一個特殊狀況。下面還有一種特殊狀況:箭頭函數

5. 箭頭函數

箭頭函數是 ES6 語法的新特性,在箭頭函數中,this 的值與建立箭頭函數的上下文的 this 一致。

在全局代碼中,this 的值爲全局對象:

var foo = (() => this)
//在瀏覽器中
foo() === window // true
// 在node中
foo() === global // true
複製代碼

其實箭頭函數並無本身的 this。因此,調用 this 時便和調用普通變量同樣在做用域鏈中查找,獲取到的便是建立此箭頭函數的上下文中的 this。若建立此箭頭函數的上下文中也沒有 this,便繼續沿着做用域鏈往外查找,直到全局做用域,這時便指向全局對象(window / global)。

當箭頭函數在建立其的上下文外部被調用時,箭頭函數即是一個閉包,this 的值一樣與原上下文環境中的 this 的值一致。因爲箭頭函數自己是不存在 this,經過 callapplybind 修改 this 的指向是沒法實現的。

做爲對象的方法:

var foo = (() => this)

var obj = {
  foo: foo
}
// 做爲對象的方法調用
obj.foo() === window // true

// 用apply來設置this
foo.apply(obj) === window // true
// 用bind來設置this
foo = foo.bind(obj)
foo() === window // true
複製代碼

箭頭函數 foothis 被設置爲建立時的上下文(在上面代碼中,也就是全局對象)的 this 值,並且沒法經過其餘調用方式設定 foothis 值。

與普通函數對比,箭頭函數的 this 值是在函數建立時肯定的,並且沒法經過調用方式從新設置 this 值。普通函數中的 this 值是在調用的時候肯定的,可經過不一樣的調用方式設定 this 值。

「一個問題」的解答

回到開篇的問題上,輸出結果爲:

// undefined
// undefined
// undefined
複製代碼

由於箭頭函數是在對象 obj1 內部建立的,在對象內部屬於全局上下文(注意只有全局上下文和函數上下文),this 一樣是指向全局對象,即箭頭函數的 this 指向全局對象且沒法被修改。

在全局對象中,沒有定義變量 a,因此便輸出三個了 undefined

const obj1 = {
  a: 'a in obj1',
  foo: () => { console.log(this.a) }
}
複製代碼

總結

this 關鍵字的值取決於其所處的位置(上下文環境):

  1. 在全局環境中,this 的值指向全局對象( windowglobal )。

  2. 在函數內部,this 的取值取決於其所在函數的調用方式,也就是說 this 的值是在函數被調用的時候肯定的,在建立函數時沒法肯定(詳解:this關鍵字)。如下四種調用方式:

    1. 全局中調用:指向全局對象 window / globalfoo 至關於 window.foo 在嚴格模式下有所不一樣;

    2. 做爲對象的方法屬性:指向調用函數的對象,在調用繼承的方法時也是如此;

    3. new 關鍵字調用:構造函數只不過是一個函數名稱第一個字母大寫的普通函數而已,在用 new 關鍵字調用時,this 指向新建立的對象;

    4. call / apply / bindcall/apply/bind 能夠修改函數的 this 指向,bind 綁定的 this 指向將沒法被修改。

    固然,箭頭函數是個例外,箭頭函數自己不存在 this,而在箭頭函數中使用 this 獲取到的即是建立其的上下文中的 this


參考:

相關文章
相關標籤/搜索