javascript語法裏一些難點問題

談談javascript語法裏一些難點問題 

1)    引子

  前不久我建立的技術羣裏一位MM問了一個這樣的問題,她貼出的代碼如下所示:

複製代碼
var a = 1;

function hehe()

{

         window.alert(a);

         var a = 2;

         window.alert(a);

}

hehe();
複製代碼

  執行結果如下所示:

  第一個alert:

 

  第二個alert:

 

  這是一個令人詫異的結果,爲什麼第一個彈出框顯示的是undefined,而不是1呢?這種疑惑的原理我描述如下:

  一個頁面裏直接定義在script標籤下的變量是全局變量即屬於window對象的變量,按照javascript作用域鏈的原理,當一個變量在當前作用域下找不到該變量的定義,那麼javascript引擎就會沿着作用域鏈往上找直到在全局作用域裏查找,按上面的代碼所示,雖然函數內部重新定義了變量的值,但是內部定義之前函數使用了該變量,那麼按照作用域鏈的原理在函數內部變量定義之前使用該變量,javascript引擎應該會在全局作用域裏找到變量定義,而實際情況卻是變量未定義,這到底是怎麼回事呢?

  當時羣裏很多人都給出了問題的解答,我也給出了我自己的解答,其實這個問題很久之前我的確研究過,但是剛被問起了我居然還是有個卡殼期,在加上最近研究javascriptMVC的寫法,發現自己讀代碼時候對new 、prototype、apply以及call的用法任然要體味半天,所以我覺得有必要對javascript基礎語法裏比較難理解的問題做個梳理,其實寫博客的一個很大的好處就是寫出來的知識邏輯會比你在腦子裏反覆梳理的邏輯映像更加的深刻。

       下面開始本文的主要內容,我會從基礎知識一步步講起。

2)    Javascript的變量

  Java語言裏有一句很經典的話:在java的世界裏,一切皆是對象

  Javascript雖然跟java沒有半點毛關係,但是很多會使用javascript的朋友同樣認爲:在javascript的世界裏,一切也皆是對象

  其實javascript語言和java語言一樣變量是分爲兩種類型:基本數據類型和引用類型。

  基本類型是指:Undefined、Null、Boolean、Number和String;而引用類型是指多個指構成的對象,所以javascript的對象指的是引用類型。在java裏能說一切是對象,是因爲java語言裏對所有基本類型都做了對象封裝,而這點在javascript語言裏也是一樣的,所以提在javascript世界裏一切皆爲對象也不爲過。

  但是實際開發裏如果我們對基本類型和引用類型的區別不是很清晰,就會碰到我們很多不能理解的問題,下面我們來看看下面的代碼:

複製代碼
    var str = "sharpxiajun";

    str.attr01 = "hello world";

    console.log(str);//  運行結果:sharpxiajun

    console.log(str.attr01);// 運行結果:undefined
複製代碼

 

  運行之,我們發現作爲基本數據類型,我們沒法爲這個變量添加屬性,當然方法也同樣不可以,例如下面的代碼:

複製代碼
    str.ftn = function(){

        console.log("str ftn");

    }

    str.ftn();
複製代碼

  運行之,結果如下圖所示:

 

 當我們使用引用類型時候,結果就和上面完全不同了,大家請看下面的代碼:

    var obj1 = new Object();

    obj1.name = "obj1 name";

    console.log(obj1.name);// 運行結果:obj1 name

  javascript裏的基本類型和引用類型的區別和其他語言類似,這是一個老調長談的問題,但是在現實中很多人都理解它,但是卻很難應用它去理解問題。

   Javascript裏的基本變量是存放在棧區的(棧區指內存裏的棧內存),它的存儲結構如下圖所示:


   
javascript裏引用變量的存儲就比基本類型存儲要複雜多,引用類型的存儲需要內存的棧區和堆區(堆區是指內存裏的堆內存)共同完成,如下圖所示:

  在javascript裏變量的存儲包含三個部分:

    部分一:棧區的變量標示符;

    部分二:棧區變量的值;

    部分三:堆區存儲的對象。

  變量不同的定義,這三個部分也會隨之發生變化,下面我來列舉一些典型的場景:

  場景一:如下代碼所示

    var qqq;

    console.log(qqq);// 運行結果:undefined

 

  運行結果是undefined,上面的代碼的標準解釋就是變量被命名了,但是還未初始化,此時在變量存儲的內存裏只擁有棧區的變量標示符而沒有棧區的變量值,當然更沒有堆區存儲的對象。

  場景二:如下代碼所示

    var qqq;

    console.log(qqq);// 運行結果:undefined

    console.log(xxx);

 

  運行之,結果如下圖所示:

 

  會提示變量未定義。在任何語言裏變量未定義就使用都是違法的,我們看到javascript裏也是如此,但是我們做javascript開發時候,經常有人會說變量未定義也是可以使用,怎麼我的例子裏卻不能使用了?那麼我們看看下面的代碼:

複製代碼
    xxx = "outer xxx";

    console.log(xxx);// 運行結果:outer xxx

    function testFtn(){

        sss = "inner sss";

        console.log(sss);// 運行結果:outer sss

    }

    testFtn();

    console.log(sss);//運行結果:outer sss

    console.log(window.sss);//運行結果:outer sss
複製代碼

  在javascript定義變量需要使用var關鍵字,但是javascript可以不使用var預先定義好變量,在javascript我們可以直接賦值給沒有被var定義的變量,不過此時你這麼操作變量,不管這個操作是在全局作用域裏還是在局部作用域裏,變量最終都是屬於window對象,我們看看window對象的結構,如下圖所示:

 

  由這兩個場景我們可以知道在javascript裏的變量不能正常使用即報出“xxx is not defined”錯誤(這個錯誤下,後續的javascript代碼將不能正常運行)只有當這個變量既沒有被var定義同時也沒有進行賦值操作纔會發生,而只有賦值操作的變量不管這個變量在那個作用域裏進行的賦值,這個變量最終都是屬於全局變量即window對象

  由上面我列舉的兩個場景我們來理解下引子裏網友提出的問題,下面我修改一下代碼,如下所示:

複製代碼
    //var a = 1;

    function hehe()

    {

        console.log(a);

        var a = 2;

        console.log(a);

    }

    hehe();
複製代碼

  結果如下圖所示:

 

  我再改下代碼:

複製代碼
    //var a = 1;

    function hehe()

    {

        console.log(a);

       // var a = 2;

        console.log(a);

    }

    hehe();
複製代碼

  運行之,結果如下所示:

 

  對比二者代碼以及引子裏的代碼,我們發現問題的關鍵是var a=2所引起的。在代碼一里我註釋了全局變量的定義,結果和引子裏代碼的結果一致,這說明函數內部a變量的使用和全局環境是無關的,代碼二里我註釋了關鍵代碼var a = 2,代碼運行結果發生了變化,程序報錯了,的確很讓人困惑,困惑之處在於局部作用域裏變量定義的位置在變量第一次使用之後,但是程序沒有報錯,這不符合javascript變量未定義既要報錯的原理。

  其實這個變量任然被定義即內存存儲裏有了標示符,只不過沒有被賦值,代碼一則說明,內部變量a已經和外部環境無關,怎麼回事?如果我們按照代碼運行是按照順序執行的邏輯來理解,這個代碼也就沒法理解。

  其實javascript裏的變量和其他語言有很大的不同,javascript的變量是一個鬆散的類型,鬆散類型變量的特點是變量定義時候不需要指定變量的類型,變量在運行時候可以隨便改變數據的類型,但是這種特性並不代表javascript變量沒有類型,當變量類型被確定後javascript的變量也是有類型的。但是在現實中,很多程序員把javascript鬆散類型理解爲了javascript變量是可以隨意定義即你可以不用var定義,也可以使用var定義,其實在javascript語言裏變量定義沒有使用var,變量必須有賦值操作,只有賦值操作的變量是賦予給window,這其實是javascript語言設計者提升javascript安全性的一個做法。

  此外javascript語言的鬆散類型的特點以及運行時候隨時更改變量類型的特點,很多程序員會認爲javascript變量的定義是在運行期進行的,更有甚者有些人認爲javascript代碼只有運行期,其實這種理解是錯誤的,javascript代碼在運行前還有一個過程就是:預加載,預加載的目的是要事先構造運行環境例如全局環境,函數運行環境,還要構造作用域鏈(關於作用域鏈和環境,本文後續會做詳細的講解),而環境和作用域的構造的核心內容就是指定好變量屬於哪個範疇,因此在javascript語言裏變量的定義是在預加載完成而非在運行時期。

  所以,引子裏的代碼在函數的局部作用域下變量a被重新定義了,在預加載時候a的作用域範圍也就被框定了,a變量不再屬於全局變量,而是屬於函數作用域,只不過賦值操作是在運行期執行(這就是爲什麼javascript語言在運行時候會改變變量的類型,因爲賦值操作是在運行期進行的),所以第一次使用a變量時候,a變量在局部作用域裏沒有被賦值,只有棧區的標示名稱,因此結果就是undefined了。

  不過賦值操作也不是完全不對預加載產生影響,預加載時候javascript引擎會掃描所有代碼,但不會運行它,當預加載掃描到了賦值操作,但是賦值操作的變量有沒有被var定義,那麼該變量就會被賦予全局變量即window對象。

  根據上面的內容我們還可以理解下javascript兩個特別的類型:undefined和null,從javascript變量存儲的三部分角度思考,當變量的值爲undefined時候,那麼該變量只有棧區的標示符,如果我們對undefined的變量進行賦值操作,如果值是基本類型,那麼棧區的值就有值了,如果棧區是對象那麼堆區會有一個對象,而棧區的值則是堆區對象的地址,如果變量值是null的話,我們很自然認爲這個變量是對象,而且是個空對象,按照我前面講到的變量存儲的三部分考慮:當變量爲null時候,棧區的標示符和值都會有值,堆區應該也有,只不過堆區是個空對象,這麼說來null其實比undefined更耗內存了,那麼我們看看下面的代碼:

複製代碼
    var ooo = null;

    console.log(ooo);// 運行結果:null

    console.log(ooo == undefined);// 運行結果:true

    console.log(ooo == null);// 運行結果:true

    console.log(ooo === undefined);// 運行結果:false

    console.log(ooo === null);// 運行結果:true
複製代碼

 

  運行之,結果很震驚啊,null居然可以和undefined相等,但是使用更加精確的三等號“===”,發現二者還是有點不同,其實javascript裏undefined類型源自於null即null是undefined的父類,本質上null和undefined除了名字這個馬甲不同,其他都是一樣的,不過要讓一個變量是null時候必須使用等號“=”進行賦值了。

  當變量爲undefined和null時候我們如果濫用它javascript語言可能就會報錯,後續代碼會無法正常運行,所以javascript開發規範裏要求變量定義時候最好馬上賦值,賦值好處就是我們後面不管怎麼使用該變量,程序都很難因爲變量未定義而報錯從而終止程序的運行,例如上文裏就算變量是string基本類型,在變量定義屬性程序還是不會報錯,這是提升程序健壯性的一個重要手段,由引子的例子我們還知道,變量定義最好放在變量所述作用域的最前端,這麼做也是保證代碼健壯性的一個重要手段。

  下面我們再看一段代碼:

複製代碼
    var str;

    if (undefined != str && null != str && "" != str){

        console.log("true");

    }else{

        console.log("false");

    }

    if (undefined != str && "" != str){

        console.log("true");

    }else{

        console.log("false");

    }

    if (null != str && "" != str){

        console.log("true");

    }else{

        console.log("false");

    }

    if (!!str){

        console.log("true");

    }else{

        console.log("false");

    }

    str = "";

    if (!!str){

        console.log("true");

    }else{

        console.log("false");

    }
複製代碼

 

  運行之,結果都是打印出false。

  使用雙等號“==”,undefined和null是一回事,所以第一個if語句的寫法完全多餘,增加了不少代碼量,而第二種和第三種寫法是等價,究其本質前三種寫法本質都是一致的,但是現實中很多程序員會選用寫法一,原因就是他們還沒理解undefined和null的不同,第四種寫法是更加完美的寫法,在javascript裏如果if語句的條件是undefined和null,那麼if判斷的結果就是false,使用!運算符if計算結果就是true了,再加一個就是false,所以這裏我建議在書寫javascript代碼時候判斷代碼是否爲未定義和null時候最好使用!運算符。

  代碼四里我們看到當字符串被賦值了,但是賦值是個空字符串時候,if的條件判斷也是false,javascript裏有五種基本類型,undefined、null、boolean、Number和string,現在我們發現除了Number都可以使用!來判斷if的ture和false,那麼基本類型Number呢?

複製代碼
    var num = 0;

    if (!!num){

        console.log("true");

    }else{

        console.log("false");

    }
複製代碼

 

  運行之,結果是false。

  如果我們把num改爲負數或正數,那麼運行之的結果就是true了。

  這說明了一個道理:我們定義變量初始化值的時候,如果基本類型是string,我們賦值空字符串,如果基本類型是number我們賦值爲0,這樣使用if語句我們就可以判斷該變量是否是被使用過了。

  但是當變量是對象時候,結果卻不一樣了,如下代碼:

複製代碼
    var obj = {};

    if (!!obj){

        console.log("true");

    }else{

        console.log("false");

    }
複製代碼

 

  運行之,代碼是true。

  所以在定義對象變量時候,初始化時候我們要給變量賦予null,這樣if語句就可以判斷變量是否初始化過。

  其實if加上!運算判斷對象的現象還有玄機,這個玄機要等我把場景三講完才能說清楚哦。

  場景三:複製變量的值和函數傳遞參數

  首先看看這個場景的代碼:

複製代碼
    var s1 = "sharpxiajun";

    var s2 = s1;

    console.log(s1);//// 運行結果:sharpxiajun

    console.log(s2);//// 運行結果:sharpxiajun

    s2 = "xtq";

    console.log(s1);//// 運行結果:sharpxiajun

    console.log(s2);//// 運行結果:xtq
複製代碼

 

  上面是基本類型變量的賦值,我們再看看下面的代碼:

複製代碼
    var obj1 = new Object();

    obj1.name = "obj1 name";

    console.log(obj1.name);// 運行結果:obj1 name

    var obj2 = obj1;

    console.log(obj2.name);// 運行結果:obj1 name

    obj1.name = "sharpxiajun";

    console.log(obj2.name);// 運行結果:sharpxiajun
複製代碼

 

  我們發現當複製的是對象,那麼obj1和obj2兩個對象被串聯起來了,obj1變量裏的屬性被改變時候,obj2的屬性也被修改。

  函數傳遞參數的本質就是外部的變量複製到函數參數的變量裏,我們看看下面的代碼:

複製代碼
    function testFtn(sNm,pObj){

        console.log(sNm);// 運行結果:new Name

        console.log(pObj.oName);// 運行結果:new obj

        sNm = "change name";

        pObj.oName = "change obj";

    }

    var sNm = "new Name";

    var pObj = {oName:"new obj"};

    testFtn(sNm,pObj);

    console.log(sNm);// 運行結果:new Name

    console.log(pObj.oName);// 運行結果:change obj
複製代碼

 

  這個結果和變量賦值的結果是一致的。

  在javascript裏傳遞參數是按值傳遞的

  上面函數傳參的問題是很多公司都愛面試的問題,其實很多人都不知道javascript傳參的本質是怎樣的,如果把上面傳參的例子改的複雜點,很多朋友都會栽倒到這個面試題下。

爲了說明這個問題的原理,就得把上面講到的變量存儲原理綜合運用了,這裏我把前文的內容再複述一遍,兩張圖,如下所示:

 

  這是基本類型存儲的內存結構。

 

  這是引用類型存儲的內存結構。

  還有個知識,如下:

  在javascript裏變量的存儲包含三個部分:

  部分一:棧區的變量標示符;

  部分二:棧區變量的值;

  部分三:堆區存儲的對象。

  在javascript裏變量的複製(函數傳參也是變量賦值)本質是傳值,這個值就是棧區的值,而基本類型的內容是存放在棧區的值裏,所以複製基本變量後,兩個變量是獨立的互不影響,但是當複製的是引用類型時候,複製操作還是複製棧區的值,但是這個時候值是堆區對象的地址,因爲javascript語言是不允許操作堆內存,因此堆內存的變量並沒有被複制,所以複製引用對象複製的值就是堆內存的地址,而複製雙方的兩個變量使用的對象是相同的,因此複製的變量其中一個修改了對象,另一個變量也會受到影響。

  原理講完了,下面我列舉一個拔高的例子,代碼如下:

複製代碼
    var ftn1 = function(){

        console.log("test:ftn1");

    };

    var ftn2 = function(){

        console.log("test:ftn2");

    };

    function ftn(f){

       f();

       f = ftn2;

    }

    ftn(ftn1);// 運行結果:test:ftn1

    console.log("====================華麗的分割線======================");

    ftn1();// 運行結果:test:ftn1
複製代碼

 

  這個代碼是很早之前有位朋友考我的,我當時答對了,但是我是蒙的,問我的朋友答錯了,其實當時我們兩個都沒搞懂其中緣由,我朋友是這麼分析的他認爲f是函數的參數,屬於函數的局部作用域,因此更改f的值,是沒法改變ftn1的值,因爲到了外部作用域f就失效了,但是這種解釋很難說明我上文裏給出的函數傳參的實例,其實這個問題答案就是函數傳參的原理,只不過這裏加入了個混淆因素函數,在javascript函數也是對象,局部作用域裏f = ftn2操作是將f在棧區的地址改爲了ftn2的地址,對外部的ftn1和ftn2沒有任何改變。

  記住:javascript裏變量複製和函數傳參都是在傳遞棧區的值

  棧區的值除了變量複製起作用,它在if語句裏也會起到作用,當棧區的值爲undefined、null、“”(空字符串)、0、false時候,if的條件判斷則是爲false,我們可以通過!運算符計算,因此當我們的代碼如下:

複製代碼
    var obj = {};

    if (!!obj){

        console.log("true");

    }else{

        console.log("false");

    }
複製代碼

3)    作用域鏈相關的問題

  作用域鏈是javascript語言裏非常紅的概念,很多學習和使用javascript語言的程序員都知道作用域鏈是理解javascript裏很重要的一些概念的關鍵,這些概念包括this指針,閉包等等,它非常紅的另一個重要原因就是作用域鏈理解起來太難,就算有人真的感覺理解了它,但是碰到很多實際問題時候任然會是丈二和尚摸不到頭腦,例如上篇引子裏講到的例子,本篇要講的主題就是作用域鏈,再無別的內容,希望看完本文的朋友能有所收穫。

  講作用域鏈首先要從作用域講起,下面是百度百科裏對作用域的定義:

作用域在許多程序設計語言中非常重要。

通常來說,一段程序代碼中所用到的名字並不總是有效/可用的,而限定這個名字的可用性的代碼範圍就是這個名字的作用域。

作用域的使用提高了程序邏輯的局部性,增強程序的可靠性,減少名字衝突。

 

  在我最擅長的服務端語言java裏也有作用域的概念,java裏作用域是以{}作爲邊界,不過在純種的面嚮對象語言裏我們沒必要把作用域研究的那麼深,也沒必要思考複雜的作用域嵌套問題,因爲這些語言關於作用域的深度運用並不會給我們編寫的代碼帶來多大好處。但是在javascript裏卻大不相同,如果我們不能很好的理解javascript的作用域我們就沒辦法使用javascript編寫出複雜的或者規模宏大的程序。

  由百度百科裏的定義,我們知道作用域的作用是保證變量的名字不發生衝突,用現實的場景來理解有個人叫做張三,張三雖然只是一個名字,但是認識張三的人根據名字就能唯一確認這個人到底是誰,但是這個世界上叫做張三的人可不止一個,特別是兩個叫張三的人有交集的時候我們就要有個辦法明確指定這個張三絕不是另外一個張三,這時我們可能會根據兩大張三年齡的差異來區分:例如一個張三叫大張三,相對的另外一個張三叫小張三了。編程語言裏的作用域其實就是爲了做類似的標記,作用域會設定一個範圍,在這個範圍裏我們是不會弄錯變量的真實含義。

  前面我講到在java裏通過{}來設置作用域,在{}裏面的變量會得到保護,這種保護就是不讓{}裏的變量被外部變量混淆和污染。那麼{}的方式適合於javascript嗎?我們看看下面的例子:

複製代碼
    var s1 = "sharpxiajun";

    function ftn(){

        var s2 = "xtq";

        console.log(this);// 運行結果: window

        console.log("s1:" + this.s1 + ";s2:" + this.s2);//運行結果:s1:sharpxiajun;s2:undefined

        console.log("s1:" + this.s1 + ";s2:" + s2);// 運行結果:s1:sharpxiajun;s2:xtq

    }

    ftn();
複製代碼

 

  在javascript世界裏有一個大的作用域環境,這個環境就是window,window環境不需要我們自己使用什麼方式構建,頁面加載時候頁面會自動構造的,上面代碼裏有一個大括號,這個大括號是對函數的定義,運行之,我們發現函數作用域內部定義的s2變量是不能被window對象訪問的,因此s2變量是被{}保護起來了,它的生命週期和這個函數的生命週期有關。

  由這個例子是不是說明在javascript裏,變量也是被{}保護起來了,在javascript語言裏還有非函數的{},我們再看看下面的例子:

複製代碼
    if (true){

        var a = "aaaa";

    }

    console.log(a);// 運行結果:aaaa
複製代碼

 

  我們發現javascript裏{}有時是起不到定義作用域的功能。這也說明javascript裏的作用域定義是和其他語言例如java不同的。

  在javascript裏作用域有一個專門的定義execution context,有的書裏把這個名字翻譯成執行上下文,有的書籍裏把它翻譯成執行環境,我更傾向於後者執行環境,下文我提到的執行環境就是execution context。這個命名非常形象,這個形象體現在execution這個單詞,execution含義就是執行,我們來想想javascript裏那些情況是執行:

  情況一:當頁面加載時候在script標籤下的javascript代碼會按順序執行,而這些能被執行的代碼都是屬於window的變量或函數;

  情況二:當函數的名字後面加上小括號(),例如ftn(),這也是在執行,不過它執行的是函數。

  如此說來,javascript裏的執行環境有兩類一類是全局執行環境,即window代表的全局環境,一類是函數代表的函數執行環境,這也就是我們常說的局部作用域

執行環境在javascript語言裏並非是一個抽象的概念,而是有具體的實現,這個實現其實是個對象,這個對象也有個名字叫做variable object,這個變量有的書裏翻譯爲變量對象,這是直譯,有的書裏把它稱爲上下文變量,這裏我還是傾向於後者上下文變量,下文裏提到的上下文變量就是指代variable object。上下文變量存儲的是上下文變量所處執行環境裏定義的所有的變量和函數。

  全局執行環境的上下文變量是可以訪問到的,它就是window對象,所以我們說window能代表全局作用域是有道理的,但是局部作用域即函數的執行環境裏的上下文變量是代碼不能訪問到的,不過javascript引擎在處理數據時候會使用到它。

  在javascript語言裏還有一個概念,它的名字叫做execution context stack,翻譯成中文就是執行環境棧,每個要被執行的函數都會先把函數的執行環境壓入到執行環境棧裏,函數執行完畢後,這個函數的執行環境就會被執行環境棧彈出,例如上面的例子:函數執行時候函數的執行環境會被壓入到執行環境棧裏,函數執行完畢,執行環境棧會把這個環境彈出,執行環境棧的控制權就會交由全局環境,如果函數後面還有代碼,那麼代碼就是接着執行。如果函數裏嵌套了函數,那麼嵌套函數執行完畢後,執行環境棧的控制權就交由了外部函數,然後依次類推,最後就是全局執行環境了。

  講到這裏我們大名鼎鼎的作用域鏈要登場了,函數的執行環境被壓入到執行環境棧裏後,函數就要執行了,函數執行的第一步不是執行函數裏的第一行代碼而是在上下文變量裏構造一個作用域鏈,作用域鏈的英文名字叫做scope chain,作用域鏈的作用是保證執行環境裏有權訪問的變量和函數是有序的,這個概念裏有兩個關鍵意思:有權訪問和有序,我們看看下面的代碼:

  

複製代碼
 var b1 = "b1";

    function ftn1(){

        var b2 = "b2";

        var b1 = "bbb";

        function ftn2(){

            var b3 = "b3";

            b2 = b1;

            b1 = b3;

            console.log("b1:" + b1 + ";b2:" + b2 + ";b3:" + b3);// 運行結果:b1:b3;b2:bbb;b3:b3

        }

        ftn2();

    }

    ftn1();

console.log(b1);// 運行結果:b1
複製代碼

 

 

  有這個例子我們發現,ftn2函數可以訪問變量b1,b2,這個體現了有權訪問的概念,當ftn1作用域裏改變了b1的值並且把b1變量重新定義爲ftn1的局部變量,那麼ftn2訪問到的b1就是ftn1的,ftn2訪問到b1後就不會在全局作用域裏查找b1了,這個體現了有序性。

  下面我要總結下上面講述的知識:

  本篇的小標題是:作用域鏈的相關問題,這個標題定義的含義是指作用域鏈是大名鼎鼎了,但是作用域鏈在廣大程序員的理解裏其實包含的意義已經超越了作用域鏈在javascript語言本身的定義。廣大程序員對作用域鏈的理解有兩塊一塊是作用域,而作用域在javascript語言裏指的是執行環境execution context,執行環境在javascript引擎裏是通過上下文變量體現的variable object,javascript引擎裏還有一個概念就是執行環境棧execution context stack,當某一個函數的執行環境壓入到了執行環境棧裏,這個時候就會在上下文變量裏構造一個對象,這個對象就是作用域鏈scope chain,而這個作用域鏈就是廣大程序員理解的第二塊知識,作用域鏈的作用是保證執行環境裏有權訪問的變量和函數是有序的,作用域鏈的變量只能向上訪問,變量訪問到window對象即被終止,作用域鏈向下訪問變量是不被允許的。

  很多人常常認爲作用域鏈是理解this指針的關鍵,這個理解是不正確的的,this指針構造是和作用域鏈同時發生的,也就是說在上文變量構建作用域鏈的同時還會構造一個this對象,this對象也是屬於上下文變量,而this變量的值就是當前執行環境外部的上下文變量的一份拷貝,這個拷貝里是沒有作用域鏈變量的,例如代碼:

複製代碼
    var b1 = "b1";

    function ftn1(){

        console.log(this);// 運行結果: window

        var b2 = "b2";

        var b1 = "bbb";

        function ftn2(){

            console.log(this);// 運行結果: window

            var b3 = "b3";

            b2 = b1;

            b1 = b3;

            console.log("b1:" + b1 + ";b2:" + b2 + ";b3:" + b3);// 運行結果:b1:b3;b2:bbb;b3:b3

        }

        ftn2();

    }

    ftn1();
複製代碼

 

  我們看到函數ftn1和ftn2裏的this指針都是指向window,這是爲什麼了?因爲在javascript我們定義函數方式是通過function xxx(){}形式,那麼這個函數不管定義在哪裏,它都屬於全局對象window,所以他們的執行環境的外部的執行上下文都是指向window。

  但是我們都知道現實代碼很多this指針都不是指向window,例如下面的代碼:

複製代碼
var obj = {

    name:"sharpxiajun",

    ftn:function(){

        console.log(this);// 運行結果: Object { name="sharpxiajun", ftn=function()}

        console.log(this.name);//運行結果: sharpxiajun

    }

}

obj.ftn();//
複製代碼

 

  運行之,我們發現這裏this指針指向了Object,這就怪了我前文不是說javascript裏作用域只有兩種類型:一個是全局的一個是函數,爲什麼這裏Object也是可以製造出作用域了,那麼我的理論是不是有問題啊?那我們看看下面的代碼:

複製代碼
var obj1 = new Object();

obj1.name = "xtq";

obj1.ftn = function(){

    console.log(this);// 運行結果: Object { name="xtq", ftn=function()}

    console.log(this.name);//運行結果: xtq

}

obj1.ftn();
複製代碼

   這兩種寫法是等價的,第一種對象的定義方法叫做字面量定義,而第二種寫法則是標準寫法,Object對象的本質也是個function,所以當我們調用對象裏的函數時候,函數的外部執行環境就是obj1本身,即外部執行環境上下文變量代表的就是obj1,那麼this指針也是指向了obj1。

 

  結果則是true,因爲var obj = {}相當於var obj = new Object(),雖然對象裏沒什麼內容,但是在堆區裏,對象的內存已經分配了,而變量棧區的值已經是內存地址了,所以if語句判斷就是true了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章