大家好,我是歸思君
一、引言
this可以説是前端開發中比較常見的一個關鍵字,由於其指向是在運行時才確定,所以大家在開發中判斷其方向時也會很模糊,今天就把this的指向問題拆開了,揉碎了,好好講一講。
先來看一個場景,看看該處的 this 應該指向哪:首先在 request.js 中定義一個 getAction 函數
export function getAction(url,parameter) {
let headers = {}
if (this && this.realReferer && this.realReferer !== '') {
headers.realReferer = window.location.origin + this.realReferer
}
return axios({
url: url,
method: 'get',
params: parameter,
headers
});
}
然後在 test.vue 文件中引用該 getAction函數並使用
import { getAction } from '@api/request'
export default {
methods: {
getTableData() {
getAction(this.url.requestUrl).then(res => {
//1.這個時候getAction中的this將打印出什麼
//2.在該處打印this,會輸出什麼
console.log(this);
})
},
}
}
現在有兩個問題:
- 在
test.vue中調用getAction()時,此時其內部,也就是request.js中的 this 指向什麼? - 在
getAction()then 後的箭頭函數中的 this 指向什麼?
思考一下能判斷出這兩個this的指向嗎?先賣個關子,等咱們再講完this的相關原理後再來解答這兩個問題。這篇文章會從這幾個方面講解:
- 什麼是this,this和執行上下文的關係
- this中的默認、隱式、顯式和new的綁定規則
-
箭頭函數中的this指向問題
二、什麼是this?
this 其實就是一個JavaScript語言中的一個關鍵字, 它的值是某個對象引用值,其指代的是當前執行上下文中的對象。那麼為何需要this?我們先來看看一個例子:
var testObj = { name: "testObj", print: function () { console.log(name) } } var name = "global name"; //想通過調用print()來調用testObj中的name testObj.print();//global name從結果可知,最後
print()輸出"global name", 而不是testObj中的 name。為何出現這種情況?
這是因為 JavaScript 語言的作用域鏈是由詞法作用域決定的,而詞法作用域是由代碼結構來確定的: - 當
testObj.print()執行時,這段代碼的詞法作用域是全局作用域,所以這個時候 js 引擎會去全局作用域中尋找 name,最後打印出“global name”。 -
因此為了避免這種情況,JavaScript 設計者引入了 this 機制,來調用對象的內部屬性,如下代碼:
var testObj = { name: "testObj", print: function () { console.log(this.name) } } var name = "global name"; testObj.print();//testObj最後就能夠通過
testObj.print()來調用對象內部的屬性了。
不同於詞法作用域鏈,this的指向是在運行時才能確定,實際上當執行上下文創建後,會生成一個this引用值,指向當前執行上下文對象,如下圖所示:
而 js 引擎在執行代碼時的運行時上下文主要有三種:全局執行上下文、函數執行上下文和 eval 執行上下文。不同場景的this指向如下://全局執行上下文,當前對象是window console.log(this);//window //函數執行上下文外部對象是全局對象,所以指向全局對象window function test(){ console.log(this);//window } //函數執行上下文外部對象是test,因此指向當前的對象test var test = { test: function(){ console.log(this);//test{...}對象 } } //eval執行上下文,根據默認綁定規則,指向全局對象window eval(`console.log(this); `) //window正是因為this在運行中才得以確定其指向的上下文對象,所以為了規範和標準化this的指向方式,規定了一系列的綁定規則,來決定什麼情況下this會綁定到哪個對象。下面就來聊聊this的綁定規則
三、this 綁定規則
this的綁定大致可以劃分為默認、隱式、顯式和new四種場景下的綁定規則:
1. 默認綁定
當函數被獨立調用時,會將this綁定到全局對象。瀏覽器環境下是window, 嚴格模式是undefined主要有以下幾種場景:
//1. 定義在全局對象下的函數被獨立調用 function test(){ console.log("global:", this); } test();//window //2. 定義在某個對象下的函數被獨立調用 var testObj = { test: function(){ console.log("testObj:", this); } } var testfun = testObj.test; testfun();//window //3. 定義在某個函數下的函數被獨立調用 function testFun(fn){ fn(); } testFun(testObj.test); //window2. 隱式綁定
當函數作為對象的方法被調用時,隱式綁定規則會將this綁定到調用該方法的對象,也就是"誰調用,就指向誰"。
const obj = { name: 'innerObj', fn:function(){ return this.name; } } //調用者是obj, this指向obj console.log(obj.fn());//innerObj const obj2 = { name: 'innerObj2', fn: function() { return obj.fn(); //此時是obj調用fn,所以此時this指向obj } } //調用者是obj, this指向obj console.log(obj2.fn())//innerObj現在我們可以回答引言中的問題1:在
request.js的getAction()中this指向test.vue中的全局vue對象,因為import {getAction} from '@api/request'後,相當於vue對象調用了getAction(),因此其內部的this方向符合隱式綁定規則,所以指向調用者——test.vue中的全局vue對象3. 顯式綁定
顯式綁定主要指通過call、apply和bind方法可以顯式地綁定this的值:
call 方法
語法:
function.call(thisArg, arg1, arg2, ...):
參數:thisArg表示 this 指向的上下文對象,arg1...argn表示一系列參數
功能: 無返回值立即調用 function 函數var test = { } function test2(){ console.log(this); } //此時是獨立函數,因此指向全局對象 test2();//window //call顯式綁定,將函數內部的this綁定至call中指定的引用對象 test2.call(test);//testapply 方法
語法:
function.apply(thisArg, [argsArray]):
參數:thisArg表示 this 指向的上下文對象,argsArray表示參數數組
功能: 沒有返回值, 立即調用函數
apply 和 call 的區別在於傳參,call 傳的是一系列參數,apply 傳的是參數數組var test = { } function test2(name){ console.log(this); console.log(name); } //此時是獨立函數,因此指向全局對象 test2();//window //call顯式綁定,將函數內部的this綁定至call中指定的引用對象 test2.apply(test, ["name"]);//test, name test2.call(test, "name"); //testbind 方法
語法:
function.bind(thisArg[, arg1[, arg2[, ...]]])
參數:thisArg表示 this 指向的上下文對象;arg1, arg2, ...表示 要傳遞給函數的參數。這些參數將按照順序傳遞給函數,並在調用函數時作為函數參數使用
功能: 返回原函數 function 的拷貝, 這個拷貝的 this 指向thisArgvar test = { fun: function(){ console.log(this); var test = function(){ console.log("test", this); } //1. 因為test.fun()在全局作用域中,所以屬於獨立函數調用,默認綁定規則指向全局對象 test(); //window //2. bind方法會創建一個原函數的拷貝,並將拷貝中的this指向bind參數中的上下文對象 var test2 = test.bind(this); test2();//test //3. apply方法會將this指向參數中的上下文,並立即執行函數 test.apply(this);//test } } test.fun();4. new 綁定
主要是在使用構造函數創建對象時,new 綁定規則會將 this 綁定到新創建的實例對象,因此構造函數中用 this 指向的屬性值和參數也會被賦給實例對象:
function funtest(){ this.name = "funtest" } var tete = new funtest(); console.log(tete.name); //"funtest"new 操作符實際上的操作步驟:
- 創建一個新的對象 {}
- 將構造函數中的 this 指向這個新創建的對象
- 為這個新對象添加屬性、方法等
- 返回這個新對象
等價於如下代碼:
var obj = {}
obj._proto_ = funtest.prototype
funtest.call(obj)
5. 綁定規則的優先級
上述的綁定規則有時會一起出現,因此需要判斷不同規則之間的優先級,然後再來確定其 this 指向:
a. 首先是默認綁定和隱式綁定,執行以下代碼:
function testFun(){
console.log(this);
}
var testobj = {
name:"testobj",
fun:testFun
}
//若輸出window,則證明優先級默認綁定大於隱式綁定;
//若輸出testobj,則證明優先級隱式綁定大於默認綁定;
testobj.fun()//testobj
輸出為 testobj 對象,所以隱式綁定的優先級高於默認綁定
b. 下面來看一下隱式綁定和顯式綁定,執行以下代碼:
function testFun(){
console.log(this);
}
var testobj = {
name:"testobj",
fun:testFun
}
//若輸出testobj,則證明優先級隱式綁定大於顯式綁定
//若輸出{}, 則證明優先級顯式綁定大於隱式綁定
testobj.fun.call({})//{}
結果輸出 { },説明顯式綁定優先級大於隱式綁定
c. 顯式綁定的 call, apply,bind 的優先級相同,與先後順序有關,看以下代碼:
function testFun(){
console.log(this);
}
var testobj = {
name:"testobj",
fun:testFun
}
//若輸出testobj,則證明優先級隱式綁定大於顯式綁定
//若輸出{}, 則證明優先級顯式綁定大於隱式綁定
testobj.fun.call({})//{}
testobj.fun.call(testobj)
d. 最後來看看顯式綁定和 new 綁定的優先級,執行以下代碼:
function testFun(){
console.log(this.name);
}
var testobj = {
name:"testobj",
}
testFun.call(testobj);//testobj
//new 操作符創建了一個新的對象,並將this重新指向新對象
//覆蓋了testFun原來綁定的testobj對象
var instance = new testFun();
console.log(instance.name) //undefined
從結果可知,new 綁定的優先級大於顯式綁定
最後總結一下 this 綁定的 優先級是:
fn()(全局環境)(默認綁定)< obj.fn()(隱式綁定) < fn.call(obj)=fn.apply(obj) = fn.bind(obj)(顯式綁定)< new fn()
6. 綁定的丟失
有時 this 綁定可能會在某些情況下丟失,導致 this 值的指向變得不確定:
賦值給變量後調用
當使用一個變量作為函數的引用值,並使用變量名執行函數時,會發生綁定丟失,此時 this 會默認綁定到全局對象或變成 undefined(嚴格模式下)
var lostObj = {
name: "lostObj",
fun: function(){
console.log(this);
}
}
var lostfun = lostObj.fun;
lostfun();//window
lostObj.fun();//lostObj
從結果發現,lostfun 雖然指向對象中的方法,但是在調用時發生了 this 綁定丟失。因為當賦值給變量時,對象中的 fun就失去了與對象的關聯,變成了一個獨立函數,所以此時執行 lostfun也就相當於執行獨立函數,默認綁定到全局對象。
那如果通過對象來執行呢?看如下代碼:
var lostObj = {
name: "lostObj",
fun: function(){
console.log(this);
}
}
var lostObj2 = {
name: "lostObj2",
fun: lostObj.fun
}
var lostfun = lostObj.fun;
lostfun();//window
lostObj.fun();//lostObj
lostObj2.fun();//lostObj2
同樣,一旦將方法賦值給變量後,其內部與對象的關聯就此丟失,默認綁定到全局對象。但是將變量放到對象中後,就與該對象進行關聯。所以該方法執行後的 this 執行了 lostObj2對象。
函數作為參數傳遞
將函數作為參數傳遞到新函數中,並在新函數中執行該參數函數:
var lostObj3 = {
name: "lostObj3",
fun: function(){
console.log(this.name);
}
}
var name = "global"
function doFun(fn){
fn();
}
doFun(lostObj3.fun);//global
從結果可知,當函數作為參數傳遞後,其形參 fn 被賦值為 lostObj3.fun。實際上也相當於賦值給變量後調用這種情況,而且 doFun()作為獨立函數調用,所以其 this 也就指向全局對象了
回調函數
如果將對象方法作為回調函數傳遞給其他函數,this 綁定也可能丟失
var lostObj4 = {
name: 'lostObj4',
fun: function() {
setTimeout(function() {
console.log(`Hello, ${this.name}!`);
});
}
};
lostObj4.fun(); // Hello, undefined!
因為 setTimeout 的回調函數最後會以普通函數的形式調用,所以其 this 指向的是全局對象,所以即便是 lostObj4調用 fun(),最後其內部的 this 仍然會丟失。
嵌套函數
當某個函數是嵌套在另一個函數內部的函數時,內部函數中的 this 綁定會丟失,並且會綁定到全局對象或 undefined(嚴格模式下):
var lostObj5 = {
name: 'lostObj5',
fun: function() {
function innerFun() {
console.log(`Hello, ${this.name}!`);
};
innerFun();
}
};
lostObj5.fun();// Hello, undefined!
從結果可以發現,嵌套函數 innerFun()中的 this 此時是指向全局環境。所以從這個案例可以説明作用域鏈和 this 沒有關係,作用域鏈不影響 this 的綁定。
原因是當innerFun()被調用時,是作為普通函數調用,不像 fun()屬於對象 lostObj5的內部方法而調用,因此最後其內部的 this 指向全局對象。
其實 this 丟失可以通過箭頭函數來解決,下面就來聊聊箭頭函數
四、箭頭函數中的 this
箭頭函數是 ES6 增加的一種編寫函數的方法,它用簡潔的方式來表達函數
語法:()=>{}
參數:(): 函數的參數,{}: 函數的執行體
1. 箭頭函數中的 this 指向
箭頭函數中的this是在定義時確定的,它是繼承自外層詞法作用域。而不是在運行時才確定,如以下代碼:
var testObj2 = {
name: "testObj2",
fun: function(){
setTimeout(()=>{
console.log(this);
})
}
}
var testObj3 = {
name: "testObj3",
fun: function(){
setTimeout(function(){
console.log(this);
})
}
}
//即使獨立調用函數,箭頭函數內的this指向是在定義時就已經確定
testObj2.fun();//testObj
testObj3.fun();//window
實際上箭頭函數中沒有 this 綁定,它是繼承自外層作用域的 this 值。因此在許多情況下,箭頭函數能解決 this 在運行時函數的綁定問題。
2. 箭頭函數與普通函數中的 this 差異
從 上面的例子可以看出箭頭函數和普通函數在 this 的處理上存在很大的差異,主要有:
this 綁定方式
普通函數的 this 是在運行時確定的;箭頭函數的 this 值是函數定義好後就已經確定,它繼承自包含箭頭函數的外層作用域
作用域
普通函數是具有動態作用域,其 this 值在運行時基於函數的調用方式動態確定。箭頭函數具有詞法作用域,其 this 值在定義時就已經確定,並繼承外部作用域
綁定 this 的對象
普通函數中 this 可以通過函數的調用方式(如對象方法、構造函數、函數調用等)來綁定到不同的對象,而箭頭函數沒有自己的 this 綁定;箭頭函數沒有自己的 this 綁定,它只能繼承外部作用域的 this 值,無法在運行時改變綁定對象,而且也無法通過顯式綁定來改變 this 的指向。
var testObj4 = {
arrowFun: ()=>{
console.log(this);
},
normalFun: function(){
console.log(this);
}
}
//此時箭頭函數的this繼承全局上下文的this,顯式綁定無法修改箭頭函數中的this值
testObj4.arrowFun();//window
testObj4.arrowFun.apply({});//window
testObj4.normalFun();//testObj4
testObj4.normalFun.apply({});//{}
下面我們就可以解答引言中的問題 2 了。箭頭函數中的 this 指向其上層的作用域,也就是 getAction() 中的 this 值,而從隱式綁定調用規則,當前是 vue 實例調用 getTableData()然後再調用 getAction(),因此 this 值指向當前 vue 實例。
五、 this 中的面試題
手寫實現一個 bind 函數
通過分析 bind 函數的語法和參數來:function.bind(thisArg[, arg1[, arg2[, ...]]])
- 返回值是一個函數
- 參數 thisArg 指向
我們暫時不考慮原型問題,實現如下代碼:
Function.prototype.mybind = function (thisArg) {
//1.隱式綁定,當前的this指向目標函數
var targetFn = this;
//將參數列表轉換為數組,並刪除第一個參數
var args = Array.prototype.slice.call(arguments, 1);
//2.返回值一個函數
return function bound() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
//解決返回函數使用new後,綁定this忽略問題
var _this = targetFn instanceof this ? this: thisArg;
return targetFn.apply(thisArg, finalArgs)
}
}
}
總結
文章回顧 this 的概念和 this 指向的判斷綁定規則,
- 首先是綁定規則:
- 獨立函數調用執行時,使用默認綁定規則,this 指向 window
- 當函數作為對象方法被調用,使用隱式綁定規則,this 指向這個對象
- 當函數作為構造方法時,使用 new 綁定規則,this 指向返回的對象
- apply/call/bind 要注意參數的傳遞和返回值不同
- 箭頭函數要看該箭頭函數在哪個作用域下,this 就指向誰
-
綁定規則的優先級:
fn()(全局環境)(默認綁定)< obj.fn()(隱式綁定) < fn.call(obj)=fn.apply(obj) = fn.bind(obj)(顯式綁定)< new fn()
- 此外要注意綁定失效的情況,善用箭頭函數來保證 this 的指向穩定