文章出處

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");

    }

 

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

  看來本主題又沒法寫完,其實本來我寫本文是想講new,prototype,call(apply)以及this,沒想講變量定義就講了這么多,算了,先發表出來吧,吃了晚飯接著寫,希望今天寫完。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()