JavaScript的this指向問題深度解析

JavaScript中的this指向問題有很多博客在解釋,仍然有很多人問。上週我們的開發團隊連續兩個人遇到相關問題,所以我不得不將關於前端構建技術的交流會延長了半個時候討論this的問題。

與我們常見的很多語言不同,JavaScript函數中的this指向並不是在函數定義的時候確定的,而是在調用的時候確定的。換句話說,函數的調用方式決定了this指向。

JavaScript中,普通的函數調用方式有三種,直接調用,方法調用和new調用。除此之外,還有一些特殊的調用方式,比如通過bind()將函數綁定到對象之後再調用、通過call()、apply()進行調用等。而es6引入了箭頭函數時,其this指向又有所不同。下面就來分析這些情況下的this指向。

直接調用

    直接調用,就是通過函數名(...)這種方式調用。這時候,函數內部的this指向全局對象,在瀏覽器中全局對象是window,在NodeJs中全局對象是global。

來看一個例子:

//簡單兼容瀏覽器和NodeJs的全局對象
const _global=typeof window===”undefined”?global:window

function test(){
console.log(this===_global); //true
}
test();

    這裏需要注意的一點是,直接調用並不是指在全局作用域下進行調用,在任何作用域下,直接通過 函數名(...)來對函數進行調用的方式,都稱爲直接調用。比如下面這個例子也是直接調用

(function(_global){這裏寫代碼片
function test(){
console.log(this===_global); //true
}
test(); //非全局作用域下的直接調用
})(typeof window===’undefined’?global:window);

bind()對直接調用的影響

還有一點需要注意的是bind()的影響。
Function.prototype.bind()的作用是將當前函數與指定的對象綁定,並返回一個新函數,這個新函數無論以什麼樣的方式調用,其this始終指向綁定的對象。還是來看例子:

const obj={};

function test(){
console.log(this===obj);
}

const testObj=test.bind(obj);
test(); //false
testObj(); //true

那麼bind()幹了啥?不妨模擬一個bind()來了解它是如何做到對this產生影響的。

const obj={};

function test(){
console.log(this===obj)
}

//自定義的函數,模擬bind()對this的影響
function myBind(func,target){
return function(){
return func.apply(target,arguments)
}
}

const testObj=myBind(test,obj);
test(); //false
testObj(); //true

從上面的示例可以看到,首先,通過閉包,保持了target,即綁定的對象;然後在調用函數的時候,對原函數使用了apply方法來指定函數的this。當然原生的bind()實現可能會不同,而且更高效。但這個示例說明了bind()的可行性。

call和apply對this的影響

上面的示例中用到了Function.prototype.apply(),與之類似的還有Function.prototype.call()。這兩方法的用法請大家自己通過鏈接去看文檔。不過,它們的第一個參數都是指定函數運行時其中的this指向。

不過使用apply和call的時候仍然需要注意,如果目錄函數本身是一個綁定了this對象的函數,那apply和call不會像預期那樣執行,比如

const obj={};

function test(){
console.log(this===obj)
}

//綁定到一個新對象,而不是obj
const testObj=test.bind({});
test.apply(obj); //true

//期望this是obj,即輸出true
//但是應爲testObj綁定了不是obj的對象,所以會輸出false
testObj.apply(obj); //false

由此可見,bind()對函數的影響是深遠的,慎用!

方法調用

方法調用是指通過對象來調用其方法函數,它是對象.方法函數(…)這樣的調用形式。這種情況下,函數中的this指向調用該方法的對象,但是,同樣需要注意bind()的影響。

const obj={
//第一種方式,定義對象的時候定義其方法
test(){
console.log(this===obj);
}
}

//第二種方式,對象定義好之後爲其附加一個方法(函數表達式)
obj.test2=function(){
console.log(this===obj);
}

//第三種方式和第二種方式原理相同
//是對象定義好之後爲其附加一個方法(函數定義)
function t(){
console.log(this===obj);
}
obj.test3=t;

//這也是爲對象附加一個方法函數
//但是這個函數綁定了一個不是obj的其他對象
obj.test4=(function(){
console.log(this===obj);
}).bind({})

obj.test(); //true
obj.test2(); //true
obj.test3(); //true

//受bind()影響,test4中的this指向不是obj
obj.test4(); //false

這裏需要注意的是,後三種方式都是預定定義函數,再將其附加給obj對象作爲其方法。再次強調,函數內部的this指向與定義無關,受調用方式的影響。

方法中this指向全局對象的情況
注意這裏說的是方法中而不是方法調用中。方法中的this指向全局對象,如果不是因爲bind(),那就一定是因爲不是用的方法調用方式,比如

const obj={
test(){
console.log(this===obj);
}
}

const t=obj.test;
t(); //false

t就是obj的test方法,但是t()調用時,其中的this指向了全局。

之所以要特別提出這種情況,主要是因爲常常將一個對象方法作爲回調傳遞給某個函數之後,卻發現運行結果與預期不符——因爲忽略了調用方式對this的影響。比如下面的例子是在頁面中對某些事情進行封裝之後特別容易遇到的問題。

class Handlers{
//這裏buttonjQueryconstructor(data, button){
this.data=data;
$button.on(“click”,this.onButtonClick);
}

   onButtonClick(e){
        console.log(this.data);
   }

}

const handlers=new Handlers(“string data”,$(“#someButton”));
//對#someButton進行點擊操作之後
//輸出undefined
//但預期是輸出string data

很顯然this.onButtonClick作爲一個參數傳入on()之後,事件觸發時,是對這個函數進行的直接調用,而不是方法調用,所以其中的this會指向全局對象。要解決這個問題有很多種方法

//這事在es5中的解決辦法之一
var _this=this;
$button.on(“click”,function(){
_this.onButtonClick();
})

//也可以通過bind()來解決
$button.on(“click”,this.onButtonClick.bind(this))

//es6中可以通過箭頭函數來處理,在jQuery中慎用
$button.on(“click”,e=>this.onButtonClick(e))

不過請注意,將箭頭函數用作jQuery的回調時造成要小心函數內對this的使用。jQuery大多數回調函數(非箭頭函數)中this
都是表示調用目標,所以可以寫$(this).text()這樣的語句,但jQuery無法改變箭頭函數的this指向,同樣的語句語義完全不同

new調用
在es6之前,每一個函數都可以當做是構造函數,通過new調用來產生新的對象(函數內無特定返回值的情況下)。而es6改變了這種狀態,雖然class定義的類用typeof運算符得到的仍然是”function”,但它不能像普通函數一樣直接調用,同時,class中定義的方法函數,也不能當作構造函數用new來調用。

而在es5中,用new調用一個構造函數,會創建一個新對象,而其中的this就指向這個新對象。這沒神馬懸念,因爲new本身就是設計來創建新對象的。

var data=’Hi’; //全局變量

function AClass(data){
this.data=data
}

var a=new AClass(‘Hello World’);
console.log(a.data) //Hello World
console.log(data) //Hi

var b=new AClass(“Hello World”);
console.log(a===b) //false

箭頭函數中的this

先來看看MDN上對間箭頭函數的說明

An arrow function expression has a shorter syntax than a function expression and does not bind its own this,arguments,super,or new.target. Arrow functions are always anonymous. These function expression are best suited for non-method functions, and they cannot be used as constructors.

這裏已經說清楚說明了,箭頭函數沒有自己的this綁定。箭頭函數中使用this,其實是直接包含它的那個函數或函數表達式的this。比如

const obj={
test(){
const arrow=()=>{
//這裏的this是test()中的this
//由test()的調用方式決定的
console.log(this===obj)
}
arrow();
},
getArrow(){
return ()=>{
//這裏的this是getArrow()中的this
//由getArrow()的調用方式決定
console.log(this===obj)
}
}
}

obj.test() //true

const arrow=obj.getArrow();
arrow(); //true

示例中的兩個this都是由箭頭函數的直接外層函數(方法)決定的,而方法函數中的this是由其調用方式決定的,所以this都是指向方法調用的對象,即obj

箭頭函數讓大家在使用閉包的時候不需要太糾結this,不需要通過像_this這樣的局部變量來臨時引用this給閉包函數使用。來看一段Babel對箭頭函數的轉義可能能加深理解:

//ES6
const obj={
getArrow(){
return ()=>{
console.log(this===obj)
}

   }

}

//ES5,由Babel轉譯
var obj={
getArrow:function getArrow(){
var _this=this;
return function(){
console.log(_this===obj)
}

    }

}

另外需要注意的是,箭頭函數不能用new調用,不能bind()到某個對象(雖然bind()方法調用沒問題,但是不會產生預期效果)。不管在什麼情況下使用箭頭函數,它本身是沒有綁定this的,它用的是直接外層函數(即包含它最近的一層函數或函數表達式)綁定的this

—–完—–

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章