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

以爲本人寫的不算很爛的話,能夠登陸關注一下個人GitHub博客,博客會堅持寫下去。javascript

今天同窗去面試,作了兩道面試題,所有作錯了,發過來給我看,我一眼就看出來了,由於這種題我作過,至於爲何結果是那樣,我也以前沒有深究過,他問我爲何,我也是一臉的懵逼,不能從根源上解釋問題的緣由,因此並不能徹底讓他信服。今天就藉着這個機會深扒一下,若是沒有耐心能夠點擊右上角,以看小說的心態看技術文章,蜻蜓點水,不加思考,這樣的量變並不能帶來質的改變。花上10+分鐘認真閱讀我相信你會受益不淺,沒收穫你買把武昌火車站同款菜刀砍我?。由於我是寫完這篇博客再回頭寫這段話的,在寫的過程當中也學到了不少,因此在此分享一下共同窗習。html

登高自卑,與君共勉。前端

下面一塊兒看看這道題,同窗微信發給我截圖:
java

若是看的不太清楚,我把代碼敲一遍,給你們看看:git

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,咱們先看看答案:github

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 的全局函數和全局屬性的佔位符。經過使用全局對象,能夠訪問全部其餘全部預約義的對象、函數和屬性。全局對象不是任何對象的屬性,因此它沒有名稱。

  2. 在頂層 JavaScript 代碼中,能夠用關鍵字 this 引用全局對象。但一般沒必要用這種方式引用全局對象,由於全局對象是做用域鏈的頭,這意味着全部非限定性的變量和函數名都會做爲該對象的屬性來查詢。例如,當JavaScript 代碼引用 parseInt() 函數時,它引用的是全局對象的 parseInt 屬性。全局對象是做用域鏈的頭,還意味着在頂層 JavaScript 代碼中聲明的全部變量都將成爲全局對象的屬性。

  3. 全局對象只是一個對象,而不是類。既沒有構造函數,也沒法實例化一個新的全局對象。

  4. 在 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

  1. 全局執行環境即window對象所建立的,局部執行環境是函數執行過程當中建立的。

  2. 全局對象,能夠訪問全部其餘全部預約義的對象、函數和屬性。

  3. 當javascript代碼被瀏覽器載入後,默認最早進入的是一個全局執行環境。

  4. 明白了執行上下文和做用域的一些概念,知道其中的運行機制和原理。**

咱們再回頭看看這兩個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;

  1. 全局變量會掛載在window對象下,會成爲window下的一個屬性。

  2. 若是你沒有使用嚴格模式並給一個未聲明的變量賦值的話,JS會自動建立一個全局變量。**

那麼用var聲明的全局變量賦值和未聲明的全局變量賦值到底有什麼不一樣呢?這裏再也不是理解理解這道面試題的重點,想深刻探究能夠看看這篇文章:javascript中加var和不加var的區別 你真的懂嗎.

該回頭了,好累?,再來看看這道面試題:

![](http://odssgnnpf.bkt.clouddn.com/1440757221622060.jpg)
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);

這個例子提示一下,關於函數參數的傳遞賦值問題。
JS是按值傳遞仍是按引用傳遞?
這裏我就很少作解答了,你們自行揣摩。

以上關於this解答來自波同窗的引用,我這裏就偷了個懶在,直接拿來引用。
原文地址:前端基礎進階(五):全方位解讀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」

完畢~寫的有點囉嗦,只是儘可能想說明白,講清一些概念的東西,反正我是收穫不少,你呢?

參考文章:
JavaScript 全局對象
原生JS執行環境與做用域深刻理解
理解Javascript_12_執行模型淺析
前端基礎進階(二):執行上下文詳細圖解
前端基礎進階(五):全方位解讀this

相關文章
相關標籤/搜索