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

1) 引子

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

var a = 1;

function hehe()

{

         window.alert(a);

         var a = 2;

         window.alert(a);

}

hehe();

執行結果以下所示:html

第一個alert:前端

clipboard.png

第二個alert:java

clipboard.png

這是一個使人詫異的結果,爲何第一個彈出框顯示的是undefined,而不是1呢?這種疑惑的原理我描述以下:程序員

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

當時羣裏不少人都給出了問題的解答,我也給出了我本身的解答,其實這個問題好久以前個人確研究過,可是剛被問起了我竟然仍是有個卡殼期,在加上最近研究javascriptMVC的寫法,發現本身讀代碼時候對new 、prototype、apply以及call的用法任然要體味半天,因此我以爲有必要對javascript基礎語法裏比較難理解的問題作個梳理,其實寫博客的一個很大的好處就是寫出來的知識邏輯會比你在腦子裏反覆梳理的邏輯映像更加的深入。安全

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

2) Javascript的變量

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

Javascript雖然跟java沒有半點毛關係,可是不少會使用javascript的朋友一樣認爲:在javascript的世界裏,一切也皆是對象函數

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

基本類型是指: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();

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

clipboard.png

當咱們使用引用類型時候,結果就和上面徹底不一樣了,你們請看下面的代碼:

var obj1 = new Object();

obj1.name = "obj1 name";

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

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

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

clipboard.png

clipboard.png

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

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

  • 部分二:棧區變量的值;部分一:棧區的變量標示符;
  • 部分二:棧區變量的值;
  • 部分三:堆區存儲的對象。

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

場景一:以下代碼所示:

var qqq;

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

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

場景二:以下代碼所示:

var qqq;

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

console.log(xxx);

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

clipboard.png

會提示變量未定義。在任何語言裏變量未定義就使用都是違法的,咱們看到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對象的結構,以下圖所示:

clipboard.png

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

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

//var a = 1;

function hehe()

{

    console.log(a);

    var a = 2;

    console.log(a);

}

hehe();

結果以下圖所示:

clipboard.png

我再改下代碼:

//var a = 1;

function hehe()

{

    console.log(a);

   // var a = 2;

    console.log(a);

}

hehe();

運行之,結果以下所示:

clipboard.png

對比兩者代碼以及引子裏的代碼,咱們發現問題的關鍵是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傳參的本質是怎樣的,若是把上面傳參的例子改的複雜點,不少朋友都會栽倒到這個面試題下。

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

clipboard.png

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

clipboard.png

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

還有個知識,以下:

在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,沒想講變量定義就講了這麼多,算了,先發表出來吧,吃了晚飯接着寫,但願今天寫完。

原文出處:談談javascript語法裏一些難點問題(一)

相關文章
相關標籤/搜索