前端進擊的巨人(六):知否知否,須知this
常見this的誤解
- 指向函數自身(源於this英文意思的誤解)
- 指向函數的詞法作用域(部分情況)
this的應用環境
- 全局環境
無論是否在嚴格模式下,全局執行環境中(任何函數體外部)this
都指向全局對象
var name = ‘以樂之名‘;
this.name; // 以樂之名
- 函數(運行內)環境
函數內部,this的值取決於函數被調用的方式(被誰調用)
var name = ‘無名氏‘; function getName() { console.log(this.name); } getName(); // 無名氏 調用者是全局對象 var myInfo = { name: ‘以樂之名‘, getName: getName }; myInfo.getName(); // 以樂之名 調用者是myInfo對象
this的正解
"this的指向是在運行時進行綁定的,而不是代碼書寫(函數聲明)時確定!!!"
"看誰用",this的指向取決於調用者,這也是很多文章提到過的觀點。"誰調用,this指向誰",只是這句話稍有偏頗,某些情況不見得都適用。
生活栗子:你的錢並不一定是你的錢,只有當你使用消費了才是你的錢 。
("看誰用"),借出去的錢就不是你的了。。。
回到征文,我們先通過棧,來理解什麽是調用位置:JavaScript中函數的調用是以棧的方式來存儲,棧頂是正在運行的函數,函數調用時入棧,執行完成後出棧。
function foo() { // 此時的棧:全局 -> foo,調用位置在foo bar(); } function bar() { // 此時的棧:全局 -> foo -> bar,調用位置在bar baz(); } function baz() { // 此時的棧:全局 -> foo -> bar -> baz,調用位置在baz // ... } foo();
代碼中雖然函數存在多次嵌套使用,但處於棧頂的只有正在執行的函數,也即調用者只有頂層的那一個(或最後一個)。理清調用位置(調用者)有助於我們理解this
。
this的綁定規則
- 默認綁定(函數單獨調用)
- 隱式綁定(作為對象的屬性方法調用,帶有執行上下文)
- 顯示綁定(
call/apply/bind
) new
綁定(new
創建實例)- 箭頭函數綁定(ES6新增,基於詞法作用域)
默認綁定下(函數單獨調用)區分嚴格模式
- 非嚴格模式,
this
會指向全局對象(瀏覽器全局對象是window
,NodeJS全局對象是global
); - 嚴格模式,
this
指向undefined
// 非嚴格模式 function getName() { console.log(this.name); // this指向全局對象 } getName(); // "",並不會報錯,如果外部有全局變量name,則會輸出對應值 // 嚴格模式 function getName() { "use strict" console.log(this.name); // this指向undefined } getName(); // TypeError: Cannot read property ‘name‘ of undefined
TIPS: 嚴格模式中,對函數中this的影響,只在函數內聲明了嚴格模式才會存在,如果是調用時聲明嚴格模式則不會影響。
function getName() {
console.log(this.name);
}
// 調用時聲明嚴格模式
"use strict";
getName(); // ""
隱式綁定
隱式綁定中,函數一般作為對象的屬性調用,帶有調用者的執行上下文。因此this
值取決於調用者的上下文環境。如果存在多層級屬性引用,只有對象屬性引用鏈中最頂層(最後一層)會影響調用位置,而this
的值取決於調用位置。文章開頭以棧來理解調用者的例子。
function getName() {
return this.name;
}
var myInfo = {
name: ‘以樂之名‘,
getName: getName
};
var leader = {
name: ‘大神組長‘
man: myInfo
};
leader.man.getName(); // ‘以樂之名‘
// man 指向 myInfo,最頂層(最後一層)對象為 myInfo
apply/call的區別
apply/call
方法兩者類似,都可以顯示綁定this
,兩者的區別是參數傳遞的方式不同。apply/call
第一個參數都為要指定this
的對象,不同的是apply
第二個參數接受的是一個參數數組,而call
從第二個參數開始接受的是參數列表。
apply語法:func.apply(thisArg, [argsArray])
call語法:func.apply(thisArg, arg1, arg2, ...)
var numbers = [5, 6, 2, 3, 7];
// 求numbers的最大值
// apply
var max = Math.max.apply(null, numbers);
// call
var max = Math.max.apply(null, ...numbers); // ...展開運算符
TIPS: 如果thisArg為原始值(數字,字符串,布爾值),this
會指向該原始值的自動包裝對象,如Number
, String
, Boolean
等
func.apply(1);
// func中的this -> Number對象;
bind的特別(柯裏化的應用)
bind
是ES5新增的方法,跟apply/call
功能一樣,可以顯示綁定this。
bind語法:function.bind(thisArg[, arg1[, arg2[, ...]]])
bind()方法創建一個新的函數,在調用時設置this關鍵字為提供的值,並在調用新函數時,將給定參數列表作為原函數的參數序列的前若幹項。
-- 《Function.prototype.bind() | MDN》
"bind與apply/call的區別:apply/call傳入this並立即執行函數,而bind傳入this則返回一個函數,並不會立即執行,只有調用返回的函數才會執行原始函數"。
bind
方法是函數柯裏化的一種應用,看過上篇《前端進擊的巨人(五):學會函數柯裏化(curry) 》的小夥伴,應該還記得"函數柯裏化的特點:延遲執行,部分傳參,返回一個可處理剩余參數的函數"。
bind
相較apply/call
的優點,可以通過部分傳參提前對this進行一次"永久綁定",也就是說this
只需綁定一次,省卻每次執行都要進行this
綁定的操作。
function getName() {
return this.name;
}
var myInfo = {
name: ‘以樂之名‘,
job: ‘前端工程師‘
};
var getName = getName.bind(myInfo);
getName(); // ‘以樂之名‘;
getName(); // ‘以樂之名‘;
// 一次性綁定,之後調用無需再修改this
TIPS: 函數柯裏化可以用於參數預設,像一次性操作(判斷/綁定)等。
有關函數柯裏化的詳解,請回閱:《前端進擊的巨人(五):學會函數柯裏化(curry) 》。
構造函數中的this
通過new
操作符可以實現對函數的構造調用。JavaScript中本身並沒有"構造函數",一個函數如果沒有使用new
操作符調用,那麽它就是個普通函數,new Func()
實際上是對函數Func
的"構造調用"。
在了解構造函數中的this
前,有必要先了解下new
實例化對象的過程。
new實例過程
- 創建(構造)一個全新的空對象
- 這個新對象會被執行"原型"鏈接(新對象的
__proto__
會指向函數的prototype
) - 構造函數的
this
會指向這個新對象,並對this屬性進行賦值 - 如果函數沒有返回其他對象,則返回這個新對象(註意構造函數的
return
,一般不會有return
)
// 正常不帶return的構造函數
function People(name, sex) {
this.name = name;
this.sex = sex;
}
var man = new People(‘亞當‘, ‘男‘);
var woman = new People(‘夏娃‘, ‘女‘);
// 實例化對象成功
// 構造函數帶了return
function People(name, sex) {
return 1; // 返回的是Number對象
}
function People(name, sex) {
return ‘hello world‘; // 返回的是String對象
}
function People(name, sex) {
return function() {}
}
function People(name, sex) {
return {};
}
// 以上並未正確實例化對象
構造函數自定義return
,會造成new
無法完成正確的實例化操作。如果返回值為基本類型,則返回其包裝對象Number/String/Bollean
。
TIPS: 原型鏈中的this指向其實例化的對象
People.prototype.say = function() {
console.log(`我的名字:${this.name}`);
};
var man = new People(‘亞當‘, ‘男‘);
man.say(); // 我的名字:亞當
this綁定規則的優先級
顯示綁定 / new
綁定 > 隱式綁定 > 默認綁定
TIPS: new
無法跟apply/call
同時使用
this判定步驟
- 函數被
new
操作符使用(new
綁定)? YES -->this
綁定的是new
創建的新對象 - 函數通過
call/apply/bind
(顯示綁定)? YES -->this
綁定的是指定的對象 - 函數在某個上下文對象中調用(隱式綁定)? YES -->
this
綁定的是那個上下文對象 - 默認綁定,嚴格模式指向
undefined
,否則指向全局對象
ES6的箭頭函數(詞法作用域的this機制,規則之外)
箭頭函數的this
機制不同於傳統的this
機制,它采取的是另外一種機制,詞法作用域的this
判定規則。
// 例子一
var name = ‘無名氏‘;
var myInfo = {
name: ‘以樂之名‘,
getName: () => {
console.log(this.name);
}
};
var getName = myInfo.getName;
window.getName(); // 無名氏
myInfo.getName(); // 無名氏
// myInfo是在全局環境定義的,因此根據詞法作用域,this指向全局對象
// 例子二
var name = ‘無名氏‘;
var myInfo = {
name: ‘以樂之名‘,
say: () => {
setTimeout(() => {
console.log(this.name);
})
}
};
myInfo.say(); // 無名氏
// 箭頭函數通過作用域鏈來逐層查找this,最終找到全局變量myInfo,this指向全局對象
// 例子三
var name = ‘無名氏‘;
var myInfo = {
name: ‘以樂之名‘,
say: function() => {
setTimeout(() => {
console.log(this.name);
})
}
};
myInfo.say(); // 以樂之名
// 箭頭函數找到say: function(){},因此this的作用域來自myInfo
TIPS: setTimeout/setInterval/alert
的調用者都是全局對象
"箭頭函數的this
始終指向函數定義時的this
,而非執行(調用)時的this
。箭頭函數中的this
必須通過作用域鏈一層一層向外查找,來確定this
指向。"
擴展:箭頭函數的書寫規則
- 箭頭函數只能用函數表達式,不能用函數聲明式寫法(不包括匿名函數)
// 函數表達式
const getName = (name) => { return ‘myName: ‘ + name };
// 匿名函數
setTimeout((name) => {
console.log(name);
}, 1000)
- 如果參數只有一個,可不加括號
()
;如果沒有參數或多個參數需加括號()
// 只有一個參數
const getName = name => {
return `myName: ${name}`;
}
// 無參數
const getName = () => {
return ‘myName: ‘以樂之名‘;
}
// 多參數
const getName = (firstName, lastName) => {
return `myName: ${firstName} ${lastName}`;
}
- 函數體只有一個可不加花括號
{}
const getName = name => return `myName: ${name}`;
- 函數體沒有花括號
{}
,可不寫return,會自動返回
const getName = name => `myName: ${name}`;
參考文檔:
- 你不知道的JavaScript(上卷)
- 徹底理解js中this的指向,不必硬背。
- this|MDN
本文首發Github,期待Star!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。
前端進擊的巨人(六):知否知否,須知this