徹底搞懂 JS 中 this 機制

this 是什麼

理解this之前, 先糾正一個觀點,

this 既不指向函數自身,也不指函數的詞法作用域。如果僅通過this的英文解釋,太容易產生誤導了。它實際是在函數被調用時才發生的綁定,也就是說this具體指向什麼,取決於你是怎麼調用的函數。

徹底搞懂 JS 中 this 機制

this 的四種綁定規則

this的4種綁定規則分別是:默認綁定、隱式綁定、顯示綁定、new 綁定。優先級從低到高。

默認綁定

什麼叫默認綁定,即沒有其他綁定規則存在時的默認規則。這也是函數調用中最常用的規則。

來看這段代碼:

function foo() {
} console.log( this.a );var a = 2;
foo(); //打印的是什麼?

foo()

打印的結果是2。

因為foo()是直接調用的(獨立函數調用),沒有應用其他的綁定規則,這裡進行了默認綁定,將全局對象綁定this上,所以this.a 就解析成了全局變量中的a,即2。

注意:在嚴格模式下(strict mode),全局對象將無法使用默認綁定,即執行會報undefined的錯誤

function foo() {
"use strict"; console.log( this.a );
}var a = 2;

foo(); // Uncaught TypeError: Cannot read property 'a' of undefined

隱式綁定

除了直接對函數進行調用外,有些情況是,函數的調用是在某個對象上觸發的,即調用位置上存在上下文對象。

function foo() {
console.log( this.a );
}var a = 2;var obj = {
a: 3, foo: foo
};
obj.foo(); // ?

obj.foo()

打印的結果是3。

這裡foo函數被當做引用屬性,被添加到obj對象上。這裡的調用過程是這樣的:

獲取obj.foo屬性 -> 根據引用關係找到foo函數,執行調用

所以這裡對foo的調用存在上下文對象obj,this進行了隱式綁定,即this綁定到了obj上,所以this.a被解析成了obj.a,即3。

多層調用鏈

function foo() {
console.log( this.a );
}var a = 2;var obj1 = {
a: 4, foo: foo
};var obj2 = {
a: 3, obj1: obj1
};
obj2.obj1.foo(); //?

obj2.obj1.foo()

打印的結果是4。

同樣,我們看下函數的調用過程:

先獲取obj1.obj2 -> 通過引用獲取到obj2對象,再訪問 obj2.foo -> 最後執行foo函數調用

這裡調用鏈不只一層,存在obj1、obj2兩個對象,那麼隱式綁定具體會綁哪個對象。這裡原則是獲取最後一層調用的上下文對象,即obj2,所以結果顯然是4(obj2.a)。

隱式丟失(函數別名)

注意:這裡存在一個陷阱,大家在分析調用過程時,要特別小心

先看個代碼:

function foo() {
console.log( this.a );
}var a = 2;var obj = {
a: 3, foo: foo
};var bar = obj.foo;
bar(); //?

bar() 打印的結果是2。/<font>

為什麼會這樣,obj.foo 賦值給bar,那調用

bar()

為什麼沒有觸發隱式綁定,使用的是默認綁定呢。

這裡有個概念要理解清楚,obj.foo 是引用屬性,賦值給bar的實際上就是foo函數(即:bar指向foo本身)。

那麼,實際的調用關係是:通過bar找到foo函數,進行調用。整個調用過程並沒有obj的參數,所以是默認綁定,全局屬性a。

隱式丟失(回調函數)

function foo() {
console.log( this.a );
}var a = 2;var obj = {
a: 3, foo: foo
};
setTimeout( obj.foo, 100 ); // ?

打印的結果是2。/<font>

同樣的道理,雖然參傳是

obj.foo

,因為是引用關係,所以傳參實際上傳的就是foo對象本身的引用。對於

setTimeout

的調用,還是 setTimeout -> 獲取參數中foo的引用參數 -> 執行 foo 函數,中間沒有obj的參與。這裡依舊進行的是默認綁定。


顯示綁定

相對隱式綁定,this值在調用過程中會動態變化,可是我們就想綁定指定的對象,這時就用到了顯示綁定。

顯示綁定主要是通過改變對象的prototype關聯對象,這裡不展開講。具體使用上,可以通過這兩個方法call(...)或apply(...)來實現(大多數函數及自己創建的函數默認都提供這兩個方法)。

call與apply是同樣的作用,區別只是其他參數的設置上

function foo() {
console.log( this.a );
}var a = 2;var obj1 = {
a: 3,
};var obj2 = {
a: 4,
};
foo.call( obj1 ); // ?foo.call( obj2 ); // ?

打印的結果是3, 4。

這裡因為顯示的申明瞭要綁定的對象,所以this就被綁定到了obj上,打印的結果自然就是obj1.a 和obj2.a。

硬綁定

function foo() {
console.log( this.a );
}var a = 2;var obj1 = {
a: 3,
};var obj2 = {
a: 4,

};var bar = function(){
foo.call( obj1 );
}
bar(); // 3setTimeout( bar, 100 ); // 3bar.call( obj2 ); // 這是多少

前面兩個(函數別名、回調函數)打印3,因為顯示綁定了,沒什麼問題。

最後一個打印是3。

這裡需要注意下,雖然bar被顯示綁定到obj2上,對於bar,function(){...} 中的this確實被綁定到了obj2,而foo因為通過

foo.call( obj1 )

已經顯示綁定了obj1,所以在foo函數內,this指向的是obj1,不會因為bar函數內指向obj2而改變自身。所以打印的是obj1.a(即3)。


new 綁定

js中的new操作符,和其他語言中(如JAVA)的new機制是不一樣的。js中,它就是一個普通函數調用,只是被new修飾了而已。

使用new來調用函數,會自動執行如下操作:

  1. 如果函數沒有返回其他對象,那麼new表達式中的函數調用會自動返回這個新對象。

從第三點可以看出,this指向的就是對象本身。

看個代碼:

function foo(a) {
this.a = a;
}var a = 2;var bar1 = new foo(3);console.log(bar1.a); // ?var bar2 = new foo(4);console.log(bar2.a); // ?

最後一個打印是3, 4。

因為每次調用生成的是全新的對象,該對象又會自動綁定到this上,所以答案顯而易見。

綁定規則優先級

上面也說過,這裡在重複一下。優先級是這樣的,以按照下面的順序來進行判斷:

數是否在new中調用(new綁定)?如果是的話this綁定的是新創建的對象。
數是否通過call、apply(顯式綁定)或者硬綁定調用?如果是的話,this綁定的是 指定的對象。
數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this綁定的是那個上下文對象。
果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到undefined,否則綁定到 全局對象。 var bar = foo()

規則例外

在顯示綁定中,對於null和undefined的綁定將不會生效。

代碼如下:

function foo() {
console.log( this.a );
}
foo.call( null ); // 2foo.call( undefined ); // 2

這種情況主要是用在不關心this的具體綁定對象(用來忽略this),而傳入null實際上會進行默認綁定,導致函數中可能會使用到全局變量,與預期不符。

所以對於要忽略this的情況,可以傳入一個空對象ø,該對象通過

Object.create(null)

創建。這裡不用{}的原因是,ø是真正意義上的空對象,它不創建Object.prototype委託,{}和普通對象一樣,有原型鏈委託關係。

1. 這裡傳null的一種具體使用場景是函數柯里化的使用

擴展:箭頭函數

最後,介紹一下ES6中的箭頭函數。通過“=>”而不是function創建的函數,叫做箭頭函數。它的this綁定取決於外層(函數或全局)作用域。

case 1 (正常調用)

  • 普通函數

function foo(){ 
console.log( this.a );
}var a = 2;var obj = {
a: 3, foo: foo
};
obj.foo(); //3
  • 箭頭函數

var foo = () => { 
console.log( this.a );
}var a = 2;var obj = {
a: 3, foo: foo
};
obj.foo(); //2foo.call(obj); //2 ,箭頭函數中顯示綁定不會生效

case 2 (函數回調)

  • 普通函數

function foo(){
return function(){ console.log( this.a );
}
}var a = 2;var obj = {
a: 3, foo: foo
};var bar = obj.foo();
bar(); //2
  • 箭頭函數

function foo(){
return () => { console.log( this.a );
}
}var a = 2;var obj = {
a: 3, foo: foo
};var bar = obj.foo();
bar(); //3

通過上面兩個列子,我們看到箭頭函數的this綁定只取決於外層(函數或全局)的作用域/<font>,對於前面的4種綁定規則是不會生效的。它也是作為this機制的一種替換,解決之前this綁定過程各種規則帶來的複雜性。

注意:對於ES6之前,箭頭函數的替換版本是這樣的

// es6function foo(){
return () => { console.log( this.a );
}
}var a = 2;var obj = {
a: 3, foo: foo
};var bar = obj.foo();
bar(); //3

通過上面兩個列子,我們看到箭頭函數的this綁定只取決於外層(函數或全局)的作用域/<font>,對於前面的4種綁定規則是不會生效的。它也是作為this機制的一種替換,解決之前this綁定過程各種規則帶來的複雜性。

注意:對於ES6之前,箭頭函數的替換版本是這樣的

// es6function foo(){
return () => { console.log( this.a );
}
}// es6之前的替代方法function foo(){
var self = this; return () => { console.log( self.a );
}
}

總結

我們在使用js的過程中,對於this的理解往往覺得比較困難,再調試過程中有時也會出現一些不符合預期的現象。很多時候,我們都是通過一些變通的方式(如:使用具體對象替換this)來規避的問題。可問題一直存在那兒,我們沒有真正的去理解和解決它。

本文主要參考了《你不知道的JavaScript(上卷)》,對this到底是什麼,具體怎麼綁定的,有什麼例外情況以及ES6中的一個優化方向,來徹底搞清楚我們一直使用的this到底是怎麼玩的。


分享到:


相關文章: