一道 JS 面試題所引發的 "血案",透過現象尋本質,再從本質看現象
今天同學去面試,做了兩道面試題,全部做錯了,發過來給我看,我一眼就看出來了,因為這種題我做過,至于為什么結果是那樣,我也之前沒有深究過,他問我為什么,我也是一臉的懵逼,不能從根源上解釋問題的原因,所以并不能完全讓他信服。今天就借著這個機會深扒一下,如果沒有耐心可以點擊右上角,以看小說的心態看技術文章,走馬觀花,不加思考,這樣的量變并不能帶來質的改變。花上10+分鐘認真閱讀我相信你會受益匪淺,沒收獲你買把武昌火車站同款菜刀砍我 :smile: 。因為我是寫完這篇文章再回頭寫這段話的,在寫的過程中也學到了很多,所以在此分享一下共同學習。
登高自卑,與君共勉。
下面一起看看這道題,同學微信發給我截圖:
如果看的不太清楚,我把代碼敲一遍,給大家看看:
var name = "jay"; //一看這二逼就是周杰倫的死忠粉
var person = {
name: "kang",
pro: {
name: "Michael",
getName: function() {
return this.name;
}
}
};
console.log(person.pro.getName());
var pepole = person.pro.getName;
console.log(pepole());
這里我就不賣關子了,不少童鞋也應該遇到過做過類似的題目,就是考察 this ,我們先看看答案:
console.log(person.pro.getName());//Michael
console.log(pepole());//jay
第一個很簡單, this 就是指向 person.pro 的引用,那么 this.name 就是 person.pro.name ,于是第一個就是輸出 Michael ,再來看看第二個就蹊蹺了,和第一個明明是一樣的方法,為什么輸出的結果是 jay 呢?
既然我們知道結果是jay了,反著推理一步步來,不難推出調用 people() 這個方法時候的 this.name 就相當于和 var name = "jay" ,var聲明的全局變量和全局環境下的this的變量有什么聯系呢?;那么這個 this 到底是什么,總得是一個具體東西吧?
我們一步步分析, this.name 這個 this 有一個 name 屬性,很明顯就是一個對象,那具體是什么對象呢?this的指向是在函數被調用的時候確定的,于是有人說就是 Window 對象,沒錯是沒錯,確實是 Window 對象,然后 var name 聲明的全局變量 name 和 window.name 是相同的作用;但是你只 只知其然,而不知其所以然 ,學深一門語言就是要有刨根問底的精神,打破砂鍋問到底, 知其然還要知其所以然 。
我們就先驗證一下,那個 this 到底是不是 window 對象吧。我們把代碼稍微調整一下,輸出 this 。
var name = "jay"; //一看這二逼就是周杰倫的死忠粉
var person = {
name: "kang",
pro: {
name: "Michael",
getName: function() {
console.log(this);
return this.name;
}
}
};
console.log(person.pro.getName());
var pepole = person.pro.getName;
console.log(pepole());
看看控制臺輸出,確實沒錯就是window對象。
再來看看var name聲明的name和window.name是否相等呢?
var name;
console.log(name===window.name)
確實是一樣的,類型和值沒有任何的不同。
好滴,那么你說this就是window對象,至于為什么是這樣你也不清楚,是否永遠是這樣呢?我們看看這段代碼輸出又會是咋樣呢?
'use strict';
var name = "jay"; //一看這二逼就是周杰倫的死忠粉
var person = {
name: "kang",
pro: {
name: "Michael",
getName: function() {
console.log(this);
return this.name;
}
}
};
console.log(person.pro.getName());
var pepole = person.pro.getName;
console.log(pepole());
還會是跟上面一樣的結果嗎?我們拭目以待.
看到結果沒: Cannot read property 'name' of undefined ,這是什么意思想必大家已經很清楚了,此時的 this 成了 undefined 了, undefined 當然也就沒有 name 這個屬性,所以瀏覽器報錯了。那么為什么會這樣呢?
同樣換種寫法再來看看這段代碼輸出什么呢?
var name = "jay";
var person = {
name : "kang",
getName : function(){
return function(){
return this.name;
};
}
};
console.log(person.getName()());
控制臺自己輸出一下看看,我想此時你的心情一定是這樣的:
在弄明白這些問題之前,我們先弄清楚 全局環境下的this , var聲明的全局變量 和 window對象 之間的聯系與區別:
先看四個簡單的例子對比,均在js非嚴格模式測試,也就是沒有聲明'use strict':
demo1:
var name="jawil";
console.log(name);
console.log(window.name)
console.log(this.name)
demo2:
name="jawil";
console.log(name);
console.log(window.name)
console.log(this.name)
demo3:
window.name="jawil";
console.log(name);
console.log(window.name)
console.log(this.name)
demo4:
this.name="jawil";
console.log(name);
console.log(window.name)
console.log(this.name)
其實這四個demo是一個意思,輸出的結果沒有任何差別,為什么沒有差別呢?因為他們在同一個環境,也就是全局環境下:
我們換一種在不同的環境下執行這段代碼看一看結果:
demo5:
var name="jawil";
var test={
name:'jay',
getName:function(){
console.log(name);
console.log(window.name)
console.log(this.name)
}
}
test.getName();
最后結果一次輸出為:
console.log(name);//jawil
console.log(window.name)//jawil
console.log(this.name)//jay
因為此處的 this 不再指向 全局對象 了,所以結果肯定不同,我們先來看看 全局對象 和 全局環境下的this ,暫不考慮 其他環境下的this 。
那么又有人會問什么是全局環境,什么又是全局對象,全局對象該怎么理解?
題外話
其實我們看技術文章,總覺得似懂非懂,一知半解,不是看不懂代碼,而是因為很多時候我們對一些概念沒有比較深入的了解,但是也沒有去認真繼續下去考究,這也不能怪我們,畢竟開發時候不太深入這些概念對我們業務也沒啥影響,但是我發現我自己寫東西時候,不把概念說清楚,總不能讓人信服和徹底明白你講的是什么玩意,我想寫博客最大的好處可以讓自己進一步提高,更深層次的理解你所學過的東西,你講的別人都看不懂,你確認你真的懂了嗎?
說到全局環境,我們就會牽扯到另一個概念那就是執行環境和函數的作用域
既然扯到這么深,就順便扯扯執行環境和作用域,這些都是js這門語言的重點和難點,沒有一定的沉淀很難去深入探討這些東西的.
函數的每次調用都有與之緊密相關的作用域和執行環境。從根本上來說,作用域是基于函數的,而執行環境是基于對象的(例如:全局執行環境即全局對象window)。
我們還是先說一說全局對象吧,因為全局執行環境是基于全局對象的。
JavaScript 全局對象
全局屬性和函數可用于所有內建的 JavaScript 對象。
全局對象描述
- 全局對象是預定義的對象,作為 JavaScript 的全局函數和全局屬性的占位符。通過使用全局對象,可以訪問所有其他所有預定義的對象、函數和屬性。全局對象不是任何對象的屬性,所以它沒有名稱。
- 在頂層 JavaScript 代碼中,可以用關鍵字 this 引用全局對象。但通常不必用這種方式引用全局對象,因為全局對象是作用域鏈的頭,這意味著所有非限定性的變量和函數名都會作為該對象的屬性來查詢。例如,當JavaScript 代碼引用 parseInt() 函數時,它引用的是全局對象的 parseInt 屬性。全局對象是作用域鏈的頭,還意味著在頂層 JavaScript 代碼中聲明的所有變量都將成為全局對象的屬性。
- 全局對象只是一個對象,而不是類。既沒有構造函數,也無法實例化一個新的全局對象。
- 在 JavaScript 代碼嵌入一個特殊環境中時,全局對象通常具有環境特定的屬性。實際上,ECMAScript 標準沒有規定全局對象的類型,JavaScript 的實現或嵌入的 JavaScript 都可以把任意類型的對象作為全局對象,只要該對象定義了這里列出的基本屬性和函數。例如,在允許通過 LiveConnect 或相關的技術來腳本化 Java 的 JavaScript 實現中,全局對象被賦予了這里列出的 java 和 Package 屬性以及 getClass() 方法。而在客戶端 JavaScript 中,全局對象就是 Window 對象,表示允許 JavaScript 代碼的 Web 瀏覽器窗口。
例子
在 JavaScript 核心語言中,全局對象的預定義屬性都是不可枚舉的,所有可以用 for/in 循環列出所有隱式或顯式聲明的全局變量,如下所示:
上一篇博客我就講到遍歷對象屬性的三種方法:
for-in 循環、 Object.keys() 以及 Object.getOwnPropertyNames() 不同的區別,想要了解可以細看我這篇博客: 傳送門
var variables = "";
for (var name in this)
{
variables += name + "<br />";
}
document.write(variables);
再回過頭來談談執行環境和函數的作用域
一開始要明白的
- 首先,我們要知道執行環境和作用域是兩個完全不同的概念。
- 函數的每次調用都有與之緊密相關的作用域和執行環境。從根本上來說,作用域是基于函數類型的(當然函數也是對象,這里我們細分一下),而執行環境是基于對象類型的(例如:全局執行環境即window對象)。
- 換句話說,作用域涉及到所被調用函數中的變量訪問,并且不同的調用場景是不一樣的。執行環境始終是this關鍵字的值,它是擁有當前所執行代碼的對象的引用。每個執行環境都有一個與之關聯的變量對象,環境中定義的所有變量和函數都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數據時會在后臺使用它。
一些概念
1. 執行環境(也稱執行上下文–execution context)
首先來說說js中的執行環境,所謂執行環境(有時也稱環境)它是JavaScript中最為重要的一個概念。執行環境定義了變量或函數有權訪問的其他數據 ,決定了它們各自的行為。而每個執行環境都有一個與之相關的變量對象,環境中定義的所有變量和函數都保存在這個對象中。
當JavaScript解釋器初始化執行代碼時,它首先默認進入全局執行環境,從此刻開始,函數的每次調用都會創建一個新的執行環境。
每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中(execution stack)。在函數執行完后,棧將其環境彈出,把控制權返回給之前的執行環境。ECMAScript程序中的執行流正是由這個便利的機制控制著。執行環境可以分為創建和執行兩個階段。在創建階段,解析器首先會創建一個變量對象(variable object,也稱為活動對象activation object),它由定義在執行環境中的變量、函數聲明、和參數組成。在這個階段,作用域鏈會被初始化,this的值也會被最終確定。在執行階段,代碼被解釋執行。
1.1可執行的JavaScript代碼分三種類型:
- Global Code,即全局的、不在任何函數里面的代碼,例如:一個js文件、嵌入在HTML頁面中的js代碼等。
- Eval Code,即使用eval()函數動態執行的JS代碼。
- Function Code,即用戶自定義函數中的函數體JS代碼。
不同類型的JavaScript代碼具有不同的Execution Context
Demo:
<script type="text/javascript">
function Fn1(){
function Fn2(){
alert(document.body.tagName);//BODY
//other code...
}
Fn2();
}
Fn1();
//code here
</script>
1.2執行環境小結
當javascript代碼被瀏覽器載入后,默認最先進入的是一個全局執行環境。當在全局執行環境中調用執行一個函數時,程序流就進入該被調用函數內,此時JS引擎就會為該函數創建一個新的執行環境,并且將其壓入到執行環境堆棧的頂部。瀏覽器總是執行當前在堆棧頂部的執行環境,一旦執行完畢,該執行環境就會從堆棧頂部被彈出,然后,進入其下的執行環境執行代碼。這樣,堆棧中的執行環境就會被依次執行并且彈出堆棧,直到回到全局執行環境。
此外還要注意一下幾點:
- 單線程
- 同步執行
- 唯一的全局執行環境
- 局部執行環境的個數沒有限制
- 每次某個函數被調用,就會有個新的局部執行環境為其創建,即使是多次調用的自身函數(即一個函數被調用多次,也會創建多個不同的局部執行環境)。
2. 作用域(scope)
當代碼在一個環境中執行時, 會創建變量對象的一個作用域鏈(scope chain 。作用域鏈的用途是保證對執行環境有權訪問的所有變量和函數的有序訪問。
作用域鏈包含了執行環境棧中的每個執行環境對應的變量對象.
通過作用域鏈,可以決定變量的訪問和標識符的解析。
注意:全局執行環境的變量對象始終都是作用域鏈的最后一個對象。
在訪問變量時,就必須存在一個可見性的問題( 內層環境可以訪問外層中的變量和函數,而外層環境不能訪問內層的變量和函數 )。更深入的說,當訪問一個變量或調用一個函數時,JavaScript引擎將不同執行環境中的變量對象按照規則 構建一個鏈表 ,在訪問一個變量時,先在鏈表的第一個變量對象上查找,如果沒有找到則繼續在第二個變量對象上查找,直到搜索到全局執行環境的變量對象即 window對象 。這也就形成了 Scope Chain 的概念。
作用域鏈圖,清楚的表達了執行環境與作用域的關系(一一對應的關系),作用域與作用域之間的關系(鏈表結構,由上至下的關系)。
Demo:
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColors(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 這里可以訪問color, anotherColor, 和 tempColor
}
// 這里可以訪問color 和 anotherColor,但是不能訪問 tempColor
swapColors();
}
changeColor();
// 這里只能訪問color
console.log("Color is now " + color);
上述代碼一共包括三個執行環境:全局執行環境、changeColor()的局部執行環境、swapColors()的局部執行環境。
- 全局環境有一個變量color和一個函數changecolor();
- changecolor()函數的局部環境中具有一個anothercolor屬性和一個swapcolors函數,當然,changecolor函數中可以訪問自身以及它外圍(即全局環境)中的變量;
- swapcolor()函數的局部環境中具有一個變量tempcolor。在該函數內部可以訪問上面的兩個環境(changecolor和window)中的所有變量,因為那兩個環境都是它的父執行環境。
上述代碼的作用域鏈如下圖所示:
從上圖發現。內部環境可以通過作用域鏈訪問所有的外部環境,但是外部環境不能訪問內部環境中的任何變量和函數。
標識符解析(變量名或函數名搜索)是沿著作用域鏈一級一級地搜索標識符的過程。搜索過程始終從作用域鏈的前端開始,然后逐級地向后(全局執行環境)回溯,直到找到標識符為止。
3.執行環境與作用域的區別與聯系
執行環境為全局執行環境和局部執行環境,局部執行環境是函數執行過程中創建的。
作用域鏈是基于執行環境的變量對象的,由所有執行環境的變量對象(對于函數而言是活動對象,因為在函數執行環境中,變量對象是不能直接訪問的,此時由活動對象(activation object,縮寫為AO)扮演VO(變量對象)的角色。)共同組成。
當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈。作用域鏈的用途:是保證對執行環境有權訪問的所有變量和函數的有序訪問。作用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。
4.小練習
<script type="text/javascript">
(function(){
a= 5;
console.log(window.a);//undefined
var a = 1;//這里會發生變量聲明提升
console.log(a);//1
})();
</script>
window.a之所以是undefined,是因為var a = 1;發生了變量聲明提升。相當于如下代碼:
<script type="text/javascript">
(function(){
var a;//a是局部變量
a = 5;//這里局部環境中有a,就不會找全局中的
console.log(window.a);//undefined
a = 1;//這里會發生變量聲明提升
console.log(a);//1
})();
</script>
更多關于變量提升和執行上下文詳細解說這里就不多少了,不然越扯越深,有興趣可以看看這篇圖解,淺顯易懂:
相信大家看到這里,也很累了,但是也有收獲,大概有了一些深刻印象,對概念也有一些比較深入的理解了。
這里我就稍微總結一下,上面講了一些什么,對接下來的解析應該有很大的幫助。
1. 瀏覽器的全局對象是window
2. 全局執行環境即window對象所創建的,局部執行環境是函數執行過程中創建的。
3. 全局對象,可以訪問所有其他所有預定義的對象、函數和屬性。
4. 當javascript代碼被瀏覽器載入后,默認最先進入的是一個全局執行環境。
5. 明白了執行上下文和作用域的一些概念,知道其中的運行機制和原理。
我們再回頭看看這兩個demo比較,我們解釋清楚這個demo執行的結果。
demo1:
var name="jawil";
console.log(name);//jawil
console.log(window.name)//jawil
console.log(this.name)//jawill
demo2:
name="jawil";
console.log(name);//jawil
console.log(window.name)//jawil
console.log(this.name)//jawil
好,從例子看以看出,這兩個name都是全局屬性,未通過var聲明的變量a和通過var聲明的變量b,都可以通過this和window訪問到.
我們可以在控制臺打印出windowd對象,發現name成了window對象的一個屬性:
var name="jawil";
console.log(window);
name2="test";
console.log(window);
這是其實一個作用域和上下文的問題。在JavaScript中,this指向當前的上下文,而var定義的變量值在當前作用域中有效。JavaScript有兩種作用域,全局作用域和局部作用域。局部作用域就是在一個函數里。var關鍵字使用來在當前作用于中創建局部變量的,而在瀏覽器中的JavaScript全局作用域中使用var語句時,會把申明的變量掛在window上,而全局作用域中的this上下文恰好指向的又是window,因此在全局作用域中var申明的變量和window上掛的變量,即this可訪問的變量有間接的聯系,但沒有直接聯系,更不是一樣的。
上面的分析我們知道了,全局變量,全局環境下的this,還有全局對象之間的關系了,具體總結一下就是:
1. 全局環境的this會指向全局對象window,此時this===window;
2. 全局變量會掛載在window對象下,會成為window下的一個屬性。
3. 如果你沒有使用嚴格模式并給一個未聲明的變量賦值的話,JS會自動創建一個全局變量。
那么用var聲明的全局變量賦值和未聲明的全局變量賦值到底有什么不同呢?這里不再是理解理解這道面試題的重點,想深入探究可以看看這篇文章: javascript中加var和不加var的區別 你真的懂嗎 .
該回頭了,好累 :tired_face: ,再來看看這道面試題:
var name = "jay"; //一看這二逼就是周杰倫的死忠粉
var person = {
name: "kang",
pro: {
name: "Michael",
getName: function() {
return this.name;
}
}
};
console.log(person.pro.getName());
var pepole = person.pro.getName;
console.log(pepole());
最后就成了為什么person.pro.getName()的this是person.pro而pepole()的this成了window對象。這里我們就要了解this的運行機制和原理。
在這里,我們需要得出一個非常重要一定要牢記于心的結論, this的指向,是在函數被調用的時候確定的 。也就是執行上下文被創建時確定的。因此我們可以很容易就能理解到,一個函數中的this指向,可以是非常靈活的。
在一個函數上下文中,this由調用者提供,由調用函數的方式來決定。
如果調用者函數,被某一個對象所擁有,那么該函數在調用時,內部的this指向該對象。如果函數獨立調用,那么該函數內部的this,則指向undefined。但是在非嚴格模式中,當this指向undefined時,它會被自動指向全局對象。
person.pro.getName()中,getName是調用者,他不是獨立調用,被對象person.pro所擁有,因此它的this指向了person.pro。而pepole()作為調用者,盡管他與person.pro.getName的引用相同,但是它是獨立調用的,因此this指向undefined,在非嚴格模式,自動轉向全局window。
再來看一個例子,來加深理解這段話:
var a = 20;
function getA() {
return this.a;
}
var foo = {
a: 10,
getA: getA
}
console.log(foo.getA()); // 10
靈機一動,再來一個。如下例子。
function foo() {
console.log(this.a)
}
function active(fn) {
fn(); // 真實調用者,為獨立調用
}
var a = 20;
var obj = {
a: 10,
getA: foo
}
active(obj.getA);
這個例子提示一下,關于函數參數的傳遞賦值問題。
這里我就不多做解答了,大家自行揣摩。
以上關于this解答來自波同學的引用,我這里就偷了個懶在,直接拿來引用。
最后把知道面試題梳理一下:
console.log(person.pro.getName());//Michael
var pepole = person.pro.getName;
console.log(pepole());//jay
person.pro.getName()中,getName是調用者,他不是獨立調用,被對象person.pro所擁有,因此它的this指向了person.pro,所以this.name=person.pro.name="Michael";
而pepole()作為調用者,盡管他與person.pro.getName的引用相同,但是它是獨立調用的,因此this指向undefined,在非嚴格模式,自動轉向全局window。
這道題實在非嚴格模式下,所以this指向了window,又因為全局變量掛載在window對象下,所以this.name=window.name=“jay”
完畢~寫的有點啰嗦,只是盡量想說明白,講清一些概念的東西,反正我是收獲很多,你呢?
參考文章:
來自:https://github.com/jawil/blog/issues/3