壹 ❀ 引
可以說this與閉包、原型鏈一樣,屬於JavaScript開發中老生常談的問題了,百度一搜,this相關的文章鋪天蓋地。可開發好幾年,被幾道this題安排明明白白的人應該不在少數(我就是其一)。我覺得this概念抽象,變化多端總是讓人暈頭轉向,但平心它並不是有多難,今天我們就從this綁定的五種場景(默認綁定、隱式綁定、顯式綁定、new綁定、箭頭函數綁定)出發,靜下心來好好聊聊這個 this,本文開始。
貳 ❀ this默認綁定
this默認綁定我們可以理解為函數調用時無任何調用前綴的情景,它無法應對我們后面要介紹的另外四種情況,所以稱之為默認綁定,默認綁定時this指向全局對象(非嚴格模式):
function fn1() { let fn2 = function () { console.log(this); //window fn3(); }; console.log(this); //window fn2(); }; function fn3() { console.log(this); //window }; fn1();
這個例子中無論函數聲明在哪,在哪調用,由於函數調用時前面並未指定任何對象,這種情況下this指向全局對象window。
但需要注意的是,在嚴格模式環境中,默認綁定的this指向undefined,來看個對比例子:
function fn() { console.log(this); //window console.log(this.name); }; function fn1() { "use strict"; console.log(this); //undefined console.log(this.name); }; var name = '聽風是風'; fn(); fn1() //TypeError: Cannot read property 'a' of undefined
再例如函數以及調用都暴露在嚴格模式中的例子:
"use strict"; var name = '聽風是風'; function fn() { console.log(this); //undefined console.log(this.name);//報錯 }; fn();
最后一點,如果在嚴格模式下調用不在嚴格模式中的函數,並不會影響this指向,來看最后一個例子:
var name = '聽風是風'; function fn() { console.log(this); //window console.log(this.name); //聽風是風 }; (function () { "use strict"; fn(); }());
叄 ❀ this隱式綁定
1.隱式綁定
什么是隱式綁定呢,如果函數調用時,前面存在調用它的對象,那么this就會隱式綁定到這個對象上,看個例子:
function fn() { console.log(this.name); }; let obj = { name: '聽風是風', func: fn }; obj.func() //聽風是風
如果函數調用前存在多個對象,this指向距離調用自己最近的對象,比如這樣:
function fn() { console.log(this.name); }; let obj = { name: '行星飛行', func: fn, }; let obj1 = { name: '聽風是風', o: obj }; obj1.o.func() //行星飛行
那如果我們將obj對象的name屬性注釋掉,現在輸出什么呢?
function fn() { console.log(this.name); }; let obj = { func: fn, }; let obj1 = { name: '聽風是風', o: obj }; obj1.o.func() //??
這里輸出undefined,大家千萬不要將作用域鏈和原型鏈弄混淆了,obj對象雖然obj1的屬性,但它兩原型鏈並不相同,並不是父子關系,由於obj未提供name屬性,所以是undefined。
既然說到原型鏈,那我們再來點花哨的,我們再改寫例子,看看下面輸出多少:
function Fn() {}; Fn.prototype.name = '時間跳躍'; function fn() { console.log(this.name); }; let obj = new Fn(); obj.func = fn; let obj1 = { name: '聽風是風', o: obj }; obj1.o.func() //?
這里輸出時間跳躍,雖然obj對象並沒有name屬性,但順着原型鏈,找到了產生自己的構造函數Fn,由於Fn原型鏈存在name屬性,所以輸出時間跳躍了。
番外------作用域鏈與原型鏈的區別:
當訪問一個變量時,解釋器會先在當前作用域查找標識符,如果沒有找到就去父作用域找,作用域鏈頂端是全局對象window,如果window都沒有這個變量則報錯。
當在對象上訪問某屬性時,首選i會查找當前對象,如果沒有就順着原型鏈往上找,原型鏈頂端是null,如果全程都沒找到則返一個undefined,而不是報錯。
2.隱式丟失
在特定情況下會存在隱式綁定丟失的問題,最常見的就是作為參數傳遞以及變量賦值,先看參數傳遞:
var name = '行星飛行'; let obj = { name: '聽風是風', fn: function () { console.log(this.name); } }; function fn1(param) { param(); }; fn1(obj.fn);//行星飛行
這個例子中我們將 obj.fn 也就是一個函數傳遞進 fn1 中執行,這里只是單純傳遞了一個函數而已,this並沒有跟函數綁在一起,所以this丟失這里指向了window。
第二個引起丟失的問題是變量賦值,其實本質上與傳參相同,看這個例子:
var name = '行星飛行'; let obj = { name: '聽風是風', fn: function () { console.log(this.name); } }; let fn1 = obj.fn; fn1(); //行星飛行
注意,隱式綁定丟失並不是都會指向全局對象,比如下面的例子:
var name = '行星飛行'; let obj = { name: '聽風是風', fn: function () { console.log(this.name); } }; let obj1 = { name: '時間跳躍' } obj1.fn = obj.fn; obj1.fn(); //時間跳躍
雖然丟失了 obj 的隱式綁定,但是在賦值的過程中,又建立了新的隱式綁定,這里this就指向了對象 obj1。
肆 ❀ this顯式綁定
顯式綁定是指我們通過call、apply以及bind方法改變this的行為,相比隱式綁定,我們能清楚的感知 this 指向變化過程。來看個例子:
let obj1 = { name: '聽風是風' }; let obj2 = { name: '時間跳躍' }; let obj3 = { name: 'echo' } var name = '行星飛行'; function fn() { console.log(this.name); }; fn(); //行星飛行 fn.call(obj1); //聽風是風 fn.apply(obj2); //時間跳躍 fn.bind(obj3)(); //echo
比如在上述代碼中,我們分別通過call、apply、bind改變了函數fn的this指向。
在js中,當我們調用一個函數時,我們習慣稱之為函數調用,函數處於一個被動的狀態;而call與apply讓函數從被動變主動,函數能主動選擇自己的上下文,所以這種寫法我們又稱之為函數應用。
注意,如果在使用call之類的方法改變this指向時,指向參數提供的是null或者undefined,那么 this 將指向全局對象。
let obj1 = { name: '聽風是風' }; let obj2 = { name: '時間跳躍' }; var name = '行星飛行'; function fn() { console.log(this.name); }; fn.call(undefined); //行星飛行 fn.apply(null); //行星飛行 fn.bind(undefined)(); //行星飛行
另外,在js API中部分方法也內置了顯式綁定,以forEach為例:
let obj = { name: '聽風是風' }; [1, 2, 3].forEach(function () { console.log(this.name);//聽風是風*3 }, obj);
番外-----call、apply與bind有什么區別?
1.call、apply與bind都用於改變this綁定,但call、apply在改變this指向的同時還會執行函數,而bind在改變this后是返回一個全新的boundFcuntion綁定函數,這也是為什么上方例子中bind后還加了一對括號 ()的原因。
2.bind屬於硬綁定,返回的 boundFunction 的 this 指向無法再次通過bind、apply或 call 修改;call與apply的綁定只適用當前調用,調用完就沒了,下次要用還得再次綁。
3.call與apply功能完全相同,唯一不同的是call方法傳遞函數調用形參是以散列形式,而apply方法的形參是一個數組。在傳參的情況下,call的性能要高於apply,因為apply在執行時還要多一步解析數組。
描述一請參照上面已有例子。
描述二請參照下方例子,我們嘗試修改 boundFunction 的 this 指向:
let obj1 = { name: '聽風是風' }; let obj2 = { name: '時間跳躍' }; var name = '行星飛行'; function fn() { console.log(this.name); }; fn.call(obj1); //聽風是風 fn(); //行星飛行 fn.apply(obj2); //時間跳躍 fn(); //行星飛行 let boundFn = fn.bind(obj1);//聽風是風 boundFn.call(obj2);//聽風是風 boundFn.apply(obj2);//聽風是風 boundFn.bind(obj2)();//聽風是風
描述三請參考以下例子:
let obj = { name: '聽風是風' }; function fn(age,describe) { console.log(`我是${this.name},我的年齡是${age},我非常${describe}!`); }; fn.call(obj,'26','帥');//我是聽風是風,我的年齡是26,我非常帥 fn.apply(obj,['26','帥']);//我是聽風是風,我的年齡是26,我非常帥
更多關於call apply bind可以閱讀博主這篇文章 js中call、apply、bind到底有什么區別?bind返回的方法還能修改this指向嗎?
伍 ❀ new綁定
准確來說,js中的構造函數只是使用new 調用的普通函數,它並不是一個類,最終返回的對象也不是一個實例,只是為了便於理解習慣這么說罷了。
那么new一個函數究竟發生了什么呢,大致分為三步:
1.以構造器的prototype屬性為原型,創建新對象;
2.將this(可以理解為上句創建的新對象)和調用參數傳給構造器,執行;
3.如果構造器沒有手動返回對象,則返回第一步創建的對象
這個過程我們稱之為構造調用,我們來看個例子:
function Fn(){ this.name = '聽風是風'; }; let echo = new Fn(); echo.name//聽風是風
在上方代碼中,構造調用創建了一個新對象echo,而在函數體內,this將指向新對象echo上(可以抽象理解為新對象就是this)。
若對於new具體過程有疑惑,或者不知道怎么手動實現一個new 方法,可以閱讀博主這篇文章 js new一個對象的過程,實現一個簡單的new方法
陸 ❀ this綁定優先級
我們先介紹前四種this綁定規則,那么問題來了,如果一個函數調用存在多種綁定方法,this最終指向誰呢?這里我們直接先上答案,this綁定優先級為:
顯式綁定 > 隱式綁定 > 默認綁定
new綁定 > 隱式綁定 > 默認綁定
為什么顯式綁定不和new綁定比較呢?因為不存在這種綁定同時生效的情景,如果同時寫這兩種代碼會直接拋錯,所以大家只用記住上面的規律即可。
function Fn(){ this.name = '聽風是風'; }; let obj = { name:'行星飛行' } let echo = new Fn().call(obj);//報錯 call is not a function
那么我們結合幾個例子來驗證下上面的規律,首先是顯式大於隱式:
//顯式>隱式 let obj = { name:'行星飛行', fn:function () { console.log(this.name); } }; obj1 = { name:'時間跳躍' }; obj.fn.call(obj1);// 時間跳躍
其次是new綁定大於隱式:
//new>隱式 obj = { name: '時間跳躍', fn: function () { this.name = '聽風是風'; } }; let echo = new obj.fn(); echo.name;//聽風是風
柒 ❀ 箭頭函數的this
ES6的箭頭函數是另類的存在,為什么要單獨說呢,這是因為箭頭函數中的this不適用上面介紹的四種綁定規則。
准確來說,箭頭函數中沒有this,箭頭函數的this指向取決於外層作用域中的this,外層作用域或函數的this指向誰,箭頭函數中的this便指向誰。有點吃軟飯的嫌疑,一點都不硬朗,我們來看個例子:
function fn() { return () => { console.log(this.name); }; } let obj1 = { name: '聽風是風' }; let obj2 = { name: '時間跳躍' }; let bar = fn.call(obj1); // fn this指向obj1 bar.call(obj2); //聽風是風
為啥我們第一次綁定this並返回箭頭函數后,再次改變this指向沒生效呢?
前面說了,箭頭函數的this取決於外層作用域的this,fn函數執行時this指向了obj1,所以箭頭函數的this也指向obj1。除此之外,箭頭函數this還有一個特性,那就是一旦箭頭函數的this綁定成功,也無法被再次修改,有點硬綁定的意思。
當然,箭頭函數的this也不是真的無法修改,我們知道箭頭函數的this就像作用域繼承一樣從上層作用域找,因此我們可以修改外層函數this指向達到間接修改箭頭函數this的目的。
function fn() { return () => { console.log(this.name); }; }; let obj1 = { name: '聽風是風' }; let obj2 = { name: '時間跳躍' }; fn.call(obj1)(); // fn this指向obj1,箭頭函數this也指向obj1 fn.call(obj2)(); //fn this 指向obj2,箭頭函數this也指向obj2
捌 ❀ 總
那么到這里,對於this的五種綁定場景就全部介紹完畢了,如果你有結合例子練習下來,我相信你現在對於this的理解一定更上一層樓了。
那么通過本文,我們知道默認綁定在嚴格模式與非嚴格模式下this指向會有所不同。
我們知道了隱式綁定與隱式丟失的幾種情況,並簡單復習了作用域鏈與原型鏈的區別。
相對隱式綁定改變的不可見,我們還介紹了顯式綁定以及硬綁定,簡單科普了call、apply與bind的區別,並提到當綁定指向為null或undefined時this會指向全局(非嚴格模式)。
我們介紹了new綁定以及new一個函數會發生什么。
最后我們了解了不太合群的箭頭函數中的this綁定,了解到箭頭函數的this由外層函數this指向決定,並有一旦綁定成功也無法再修改的特性。
希望在面試題中遇到this的你不再有所畏懼,到這里,本文結束。
對了,學完了this不妨來兩道面試題試試自己的理解情況,帶詳細解析:js 從兩道面試題加深理解閉包與箭頭函數中的this
參考
你不知道的js中關於this綁定機制的解析[看完還不懂算我輸]