1. 程式人生 > >一道JS面試題所引發的"血案",透過現象尋本質,再從本質看現象

一道JS面試題所引發的"血案",透過現象尋本質,再從本質看現象

覺得本人寫的不算很爛的話,可以登入關注一下我的GitHub部落格,新手寫東西寫的不好之處,還望見諒,畢竟水平有限,寫東西只為交流提高,一起學習,還望大神多加指點,指出紕漏,和提出寶貴的意見,部落格會堅持寫下去。

今天同學去面試,做了兩道面試題,全部做錯了,發過來給我看,我一眼就看出來了,因為這種題我做過,至於為什麼結果是那樣,我也之前沒有深究過,他問我為什麼,我也是一臉的懵逼,不能從根源上解釋問題的原因,所以並不能完全讓他信服。今天就藉著這個機會深扒一下,如果沒有耐心可以點選右上角,以看小說的心態看技術文章,走馬觀花,不加思考,這樣的量變並不能帶來質的改變。花上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宣告的全域性變數namewindow.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()());

 

控制檯自己輸出一下看看,我想此時你的心情一定是這樣的:

在弄明白這些問題之前,我們先弄清楚全域性環境下的thisvar宣告的全域性變數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 物件。

全域性物件描述

  1. 全域性物件是預定義的物件,作為 JavaScript 的全域性函式和全域性屬性的佔位符。通過使用全域性物件,可以訪問所有其他所有預定義的物件、函式和屬性。全域性物件不是任何物件的屬性,所以它沒有名稱。
  1. 在頂層 JavaScript 程式碼中,可以用關鍵字 this 引用全域性物件。但通常不必用這種方式引用全域性物件,因為全域性物件是作用域鏈的頭,這意味著所有非限定性的變數和函式名都會作為該物件的屬性來查詢。例如,當JavaScript 程式碼引用 parseInt() 函式時,它引用的是全域性物件的 parseInt 屬性。全域性物件是作用域鏈的頭,還意味著在頂層 JavaScript 程式碼中宣告的所有變數都將成為全域性物件的屬性。
  1. 全域性物件只是一個物件,而不是類。既沒有建構函式,也無法例項化一個新的全域性物件。
  1. 在 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程式碼分三種類型:
  1. Global Code,即全域性的、不在任何函式裡面的程式碼,例如:一個js檔案、嵌入在HTML頁面中的js程式碼等。
  2. Eval Code,即使用eval()函式動態執行的JS程式碼。
  3. 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可訪問的變數有間接的聯