壹 ❀ 引
在本文之前我已經花了兩個篇幅專門介紹了JavaScript中的閉包與this,正好今早地鐵上看到了兩道面試題,試着做了下發現挺有意思,所以想單獨寫一篇文章來記錄解析過程。若你對於閉包與this有所了解,不妨先看自己的理解是否正確,若你對於這部分知識欠缺,還是建議先閱讀我前面的兩篇文章,鏈接在下:
一篇文章看懂JS閉包,都要2020年了,你怎么能還不懂閉包?
js 五種綁定徹底弄懂this,默認綁定、隱式綁定、顯式綁定、new綁定、箭頭函數綁定詳解
那么本文開始。
貳 ❀ 題一
/*非嚴格模式*/ var name = 'window' var obj1 = { name: '聽風是風', fn1: function () { console.log(this.name) }, fn2: () => console.log(this.name), fn3: function () { return function () { console.log(this.name) } }, fn4: function () { return () => console.log(this.name) } } var obj2 = { name: '行星飛行' }; obj1.fn1();//? obj1.fn1.call(obj2);//? obj1.fn2();//? obj1.fn2.call(obj2);//? obj1.fn3()();//? obj1.fn3().call(obj2);//? obj1.fn3.call(obj2)();//? obj1.fn4()();//? obj1.fn4().call(obj2);//? obj1.fn4.call(obj2)();//?
答案就不統一貼了,大家可以自己輸出,這里直接開始解析:
第一個輸出聽風是風,fn1調用前有一個obj1,this為隱式綁定指向obj1,因此讀取到obj1的name屬性。
第二個輸出行星飛行,在介紹this的文章中已經提到,顯式綁定優先級高於隱式綁定,所以此時的this指向obj2,讀取了obj2的name屬性。
第三個輸出window,在介紹this一文中我們已經知道箭頭函數並沒有自己的this,它的this指向由上層執行上下文中的this決定,那為什么上層執行上下文是window呢?
我在介紹JavaScript執行上下文的文章中已經提到,JavaScript中的上下文分為全局執行上下文,函數執行上下文與eval執行上下文(eval不作考慮)。而不管是全局上下文或函數上下文的創建,大致都包含了確認this指向,創建詞法環境,創建變量環境三步。
也就是說,this屬於上下文中的一部分,很明顯對象obj1並不是一個函數,它並沒有權利創建自己的上下文,所以沒有自己的this,那么它的外層是誰呢?當然是全局window啦,所以這里的this指向window。
第四個輸出window,在this介紹一文中已經提到,箭頭函數的this由外部環境決定,且一旦綁定無法通過call,apply或者bind再次改變箭頭函數的this,所以這里雖然使用了call方法但依舊無法修改,所以this還是指向window。
第五個輸出window,這個在閉包一文中已經提到了這個例子,obj1.fn3()()其實可以改寫成這樣:
var fn = obj1.fn3(); fn();
先執行了fn3方法,返回了一個閉包fn,而fn執行時本質上等同於window.fn(),屬於this默認綁定,所以this指向全局對象。
第六個輸出行星飛行,同樣是先執行fn3返回一個閉包,但閉包執行時使用了call方法修改了this,此時指向obj2,這行代碼等同於:
var fn = obj1.fn3(); fn.call(obj2);//顯式綁定
第七個輸出window,obj1.fn3.call(obj2)()修改一下其實是這樣,fn被調用時本質上還是被window調用:
var fn = obj1.fn3.call(obj2); window.fn();//默認綁定
第八個輸出聽風是風,fn4同樣是返回一個閉包,只是這個閉包是一個箭頭函數,所以箭頭函數的this參考fn4的this即可,很明顯此次調用fn4的this指向obj1。
var fn = obj1.fn4(); window.fn();//無法改變箭頭函數this
第九個輸出聽風是風,改寫代碼其實是這樣,顯式綁定依舊無法改變箭頭函數this:
var fn = obj1.fn4(); fn.call(obj2);//顯式綁定依舊無法改變this
第十個輸出行星飛行,前文已經說了,雖然無法直接改變箭頭函數的this,但可以通過修改上層上下文的this達到間接修改箭頭函數this的目的:
var fn = obj1.fn4.call(obj2);//fn4的this此時指向obj2 window.fn();//隱式綁定無法改變箭頭函數this,this與fn4一樣
OK,題目一解析完畢,我們接着看題目二,其實沒有太大區別,只是兩個對象是以構造函數創建罷了。
叄 ❀ 題二
/*非嚴格模式*/ var name = 'window' function Person(name) { this.name = name; this.fn1 = function () { console.log(this.name); }; this.fn2 = () => console.log(this.name); this.fn3 = function () { return function () { console.log(this.name) }; }; this.fn4 = function () { return () => console.log(this.name); }; }; var obj1 = new Person('聽風是風'); console.dir(obj1); var obj2 = new Person('行星飛行'); obj1.fn1(); obj1.fn1.call(obj2); obj1.fn2(); obj1.fn2.call(obj2); obj1.fn3()(); obj1.fn3().call(obj2); obj1.fn3.call(obj2)(); obj1.fn4()(); obj1.fn4().call(obj2); obj1.fn4.call(obj2)();
我們開始解析第二題:
第一個輸出聽風是風,與第一題一樣,這里同樣是隱式綁定,this指向new出來的對象obj1。
第二個輸出行星飛行,顯式綁定,this指向obj2。
第三個你是不是覺得是window,很遺憾,這里的箭頭函數指向了obj1,輸出聽風是風。
哎?不對啊,第一題同樣是訪問對象中的箭頭函數,由於對象沒有上下文,所以指向全局window,怎么到這里就不是全局了,new 出來的obj1與我們直接創建的對象有何區別?這就得從new一個函數發生了什么與閉包概念說起,我們先來看個簡單的例子1:
function Fn(){ var name = '聽風是風'; this.sayName = function () { console.log(name); }; }; var obj = new Fn(); obj.sayName();//?
請問obj.sayName能否訪問到構造函數中的name屬性?答案是能,這里的sayName方法其實就是一個閉包,它訪問了外層函數Fn中的自由變量name,並在new過程中由構造函數Fn返回,我們可以嘗試打印obj並查看sayName方法:
可以看到在scopes字段中保存了一個closure閉包,因為它的存在,返回的閉包obj.sayName才能繼續訪問此變量。
而我們知道new一個構造函數時,其實可以理解為就是新建了一個對象,並將構造器屬性以及構造函數原型都賦予給了此對象,並最終返回,我們簡單模擬其實是這樣,例子2:
function Fn(){ var name = '聽風是風'; var obj = {}; obj.sayName = function () { console.log(name); }; return obj; }; var obj = Fn();
同樣是打印返回的obj查看sayName方法,可以看到也存在閉包:
那我們回顧到上面的箭頭函數,是不是用閉包就能解釋通,返回的箭頭函數同樣保存了構造函數的上下文,而箭頭函數的this指向由上層上下文中的this決定,構造函數在new的過程中this指向了obj1,於是箭頭函數的this同樣也指向了obj1。
讓我們回顧一遍什么是閉包?閉包是使用了外層作用域自由變量的函數,很遺憾,JavaScript似乎並未將構造器屬性歸為自由變量,所以這里並不能用閉包解釋,看這個例子3:
function Fn(){ this.name = '聽風是風'; this.sayName = function () { console.log(this.name); }; }; var obj = new Fn(); console.log(obj);
我們打印obj對象並查看sayName方法,可以看到並不是一個閉包:
不知道大家有沒有理解我想表達的觀點,在上面展示的例子1例子2中,返回的函數如果是訪問name這樣的變量就構成了閉包,但例子3中訪問this.name這類構造器屬性卻不構成閉包。
即便如此,我們通過前面三個小例子已經證明了new操作返回的對象有權訪問構造函數內部作用域,同理,對象中的箭頭函數一樣可訪問,這種關系類似於閉包卻又不是閉包,希望大家多多體會。(若大家無法很好理解還是直接當成閉包吧)
花了比較大的篇幅解釋第三個,第三個說清楚了后面的都好展開了。
那么第四個輸出聽風是風,我們改寫代碼其實是這樣:
var arrowFn = obj1.fn2;//箭頭函數this指向obj1 arrowFn.call(obj2);//箭頭函數this無法直接改變
第五個輸出window,與題一相同,返回閉包本質上被window調用,this被修改。
第六個輸出行星飛行,返回閉包后利用call方法顯式綁定指向obj2。
第七個輸出window,返回閉包還是被window調用。
第八個輸出聽風是風,返回閉包是箭頭函數,this同樣會指向obj1,雖然返回后也是window調用,但箭頭函數無法被直接修改,還是指向obj1。
第九個輸出聽風是風,箭頭函數無法被直接修改。
第十個輸出行星飛行,箭頭函數可通過修改外層作用域this指向從而達到間接修改的目的。
肆 ❀ 總
那么到這里兩道題分析完畢,我們來做個總結。
題一與題二雖然都是對象,但通過new創建出來的對象與對象直接量還是有所區別,這一點就體現在了對象中的箭頭函數中。相比普通對象,new操作符的對象保存了構造函數上下文中的this指向,導致箭頭函數並不會指向window。
箭頭函數相比普通函數,箭頭函數的this比較吃軟飯,外層上下文中的this指向誰它便指向誰,同時我們無法直接修改箭頭函數的this。而普通函數的this可以被隱式,顯式多種手段修改,並滿足一定優先級。
我們了解到構造函數得到的實例對象所包含的函數嚴格意義上並不是閉包,雖然它與閉包非常相似。
如果大家對於解析有所疑問,歡迎留言,我會第一時間回復,那么本文就寫到這里。
最后補一個,如果大家對於new一個函數的過程有疑慮,建議閱讀博主這篇文章 js new一個對象的過程,實現一個簡單的new方法