this 關鍵字做爲 JavaScript 中自動定義的特殊標識符,是咱們不得不去面對、瞭解的知識點,不少初學者對 this 關鍵字可能會有含糊不清的感受,但其實稍微理一理, this 並不複雜、不混亂。面試
概述中咱們說了 this 是 JavaScript 中的一個特殊關鍵字,this 和執行上下文相關,當一個函數被調用時,會創建一個活動記錄,也稱爲執行環境。這個記錄包含函數是從何處(call-stack)被調用的,函數是如何被調用的,被傳遞了什麼參數等信息。這個記錄的屬性之一,就是在函數執行期間將被使用的 this 引用。 可是本文中並不許備往深層次分析其究竟是什麼,而是說明在應用場景和淺層原理中分析 this 究竟是什麼。 在分析以前,咱們先來看一看當初的咱們爲何會用 this。bash
如下是一段代碼實例,其中用到了 this
。app
let me = {
name: 'seymoe'
}
function toUpperCase() {
return this.name.toUpperCase()
}
toUpperCase.call(me) // 'SEYMOE'
複製代碼
固然以上代碼也徹底能夠不用 this
而採用傳參的形式實現。函數
let me = {
name: 'seymoe'
}
function toUpperCase(person) {
return person.name.toUpperCase()
}
toUpperCase(me) // 'SEYMOE'
複製代碼
在這裏爲何用 this
而不用傳參的形式,是由於 this
機制用更優雅的方式隱含的傳遞一個對象的引用,能夠擁有更乾淨的 API 設計和簡單複用。使用模式越複雜,經過明確參數傳遞執行環境和傳遞 this
執行環境相比,就越複雜,固然以上只是一個應用場景之一。post
this 不是編寫時綁定,而是運行時綁定。它依賴於函數調用的上下文條件。this 綁定和函數聲明的位置無關,反而和函數被調用的方式有關,被調用的這個位置就叫調用點。因此咱們分析 this 是什麼的時候就必須分析調用棧(使咱們到達當前執行位置而被調用的全部方法的堆棧)和調用點。學習
function baz() {
// 調用棧是‘baz’,調用點是全局做用域
console.log('baz')
bar() 2. // bar的調用點
}
function bar() {
// 調用棧是‘baz - bar’,調用點位於baz的函數做用域內
console.log('bar')
}
baz() // 1. baz的調用點
複製代碼
以上應該比較容易理解 baz
和 bar
函數相對應的調用點和調用棧。ui
如今咱們知道了調用點,而調用點決定了函數調用期間 this 指向哪裏的四種規則,因此排好隊一個一個來分析吧~this
顧名思義,就是 this 沒有其餘規則適用時的默認規則,獨立函數調用就是最多見的狀況。spa
var a = 2
function foo() {
console.log(this.a)
}
function bar() {
foo()
}
foo() // 2
bar() // 2
複製代碼
foo
是一個直白的毫無修飾的函數引用調用,因此默認綁定了全局對象,固然若是是嚴格模式 "use strict"
this 將會是 undefined
。設計
注意: 雖然是基於調用點,但只要foo的內容沒在嚴格模式下,那就默認綁定全局對象。
var a = 2
function foo() {
console.log(this.a)
}
(function (){
"use strict";
foo() // 2
})()
複製代碼
調用點是否擁有一個環境對象,或(擁有者、容器對象)。
function foo() {
console.log(this.a)
}
let obj = {
a: 2,
foo: foo
}
obj.foo() // 2
複製代碼
當一個方法引用存在一個環境對象,隱含規則爲該對象應該被用於這個函數調用的this綁定。
隱含綁定的狀況下,容易出現丟失的狀況!當隱含綁定丟失了它的綁定,意味着它會回退到默認綁定,下面是例子:
var a = 3
function foo() {
console.log(this.a)
}
let obj = {
a: 2,
foo: foo
}
let bar = obj.foo
bar() // 3
// 另外一種微妙的狀況
function doFoo(fn) {
fn && fn()
}
doFoo(obj.foo) // 3
複製代碼
函數的參數傳遞只是一種隱含的賦值,fn是foo函數的一個引用,而調用fn則是毫無掩飾的調用一個函數,默認綁定規則。
隱含綁定須要咱們改變對象自身包含一個函數的引用來使 this 隱含的綁定到這個對象上,默認綁定也是不肯定的狀況,可是不少時候咱們但願可以明確的使一個函數調用時使用某個特定對象做爲 this 綁定,而不在這個對象上放置一個函數引用屬性。
這個時候,call
和 apply
就該上場了。
JavaScript 中幾乎全部的函數都能訪問這兩個方法,這兩個方法接收的第一個參數都是一個用於 this 的對象,以後用這個指定的 this 來調用函數,這種方式就叫明確綁定。
function foo() {
console.log(this.a)
}
let obj = {
a: 2
}
foo.call(obj) // 2
複製代碼
一種明確綁定的變種能夠保證一個函數始終被obj調用,不管如何也不會改變,這種方式叫硬綁定,經過 bind
方法實現。
var obj = {
a: 2
}
function foo(something) {
console.log(this.a, something)
return this.a + something
}
var bar = foo.bind(obj)
bar(' is a number.') // 2 ,'is a number.'
複製代碼
咱們注意到採用 bind
方式進行硬綁定時,該方法返回一個函數,這和 call
和 apply
是有所區別的。
傳統面嚮對象語言中,經過 new
操做符調用構造函數會生成一個類實例。在 JavaScript 中其實沒有構造器、類的概念,new 調用的函數僅僅只是一個函數,只是被 new
調用時改變了行爲。因此不存在構造器函數,只存在函數的構造器調用。
new
操做符調用時會建立一個全新對象,鏈接原型鏈,並將這個新建立的對象設置爲函數調用的 this 綁定,(默認狀況)自動返回這個全新對象。
function Foo(a) {
this.a = a
}
let bar = new Foo(2)
console.log(bar.a) // 2
複製代碼
以上的規則在適用時存在優先級,級別以下:
硬綁定 > new 綁定 > 明確綁定 > 隱含綁定 > 默認綁定
因此咱們已經可以總結出斷定 this 的通常流程了。
斷定 this 通常流程
new
調用,this 是新構建的對象;call
、apply
或bind
,this 是明確指定的對象;undefined
,不然是global(全局)對象。單獨將箭頭函數中的 this 列出來是由於並不能由於 this 在箭頭函數中就有特殊的指向,而是由於箭頭函數不會像普通函數去使用 this, 箭頭函數的 this 和外層的 this 保持一致。這種保持一致是強力的,沒法經過 call
、apply
或 bind
來改變指向。
const obj = {
a: () => {
console.log(this)
}
}
obj.a() // window
obj.a.bind({})() // window
複製代碼
最後,下面這個簡單的測驗,並非很繞很難的面試題,有興趣不妨作作,評論區回覆答案一塊兒探討一下~
var a = 1
var obj = {
a: 2,
}
var bar
obj.foo = foo
bar = obj.foo
function foo() {
var a = 3
console.log(this.a)
}
foo() // 1. ???
;(function (a) {
"use strict";
foo() // 2. ???
bar.bind(a).call(obj) // 3. ???
})(this)
obj.foo() // 4. ???
obj.foo.call(this) // 5. ???
bar() // 6. ???
bar.apply(obj) // 7. ???
var b = new foo() // 8. ???
console.log(b.a) // 9. ???
複製代碼
寫做是一個學習的過程,嘗試寫這個系列也主要是爲了鞏固 JavaScript 基礎,並嘗試理解其中的一些知識點,以便能靈活運用。本篇同步發佈在「端技」公衆號,若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!
整個系列會持續更新,不會完結。