JavaScript經典面試題詳解

電裡發表於2018-08-22

以下是我遇到的一些經典的JS面試題,結合我自己的理解寫的詳解,主要參考高程一書,歡迎大家批評指正

1.

 

           var a;
           console.log(a);

 

 

:執行結果為列印undefined。

首先,以上程式碼完全執行的話需要引擎,編譯器,作用域的配合操作,(引擎負責整個JavaScript程式的編譯及執行過程,編譯器負責詞法分析及程式碼生成等,作用域負責收集並維護由所有宣告的識別符號(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權。)首先遇到var a,編譯器會詢問作用域是否已經有一個該名稱的變數存在於同一個作用域的集合中。如果是,編譯器會忽略該宣告,繼續執行編譯,否則它會要求作用域在當前作用域的集合中宣告一個新的變數,並命名為a。然後引擎會對console進行RHS查詢(RHS可以看成是對某個變數的值進行查詢,LHS查詢則是試圖找到變數的容器本身,從而可以對其賦值),檢查得到的值中是否有一個叫做log的方法,找到log方法後,引擎對a進行RHS查詢,作用域中存在之前宣告的a,但是變數a中沒有賦值,所以把它傳遞給log,會列印undefined。如果對a進行RHS查詢時沒有找到a,那麼引擎就會丟擲ReferrnceError異常,因為在任何相關的作用域中都無法找到它。接下來,如果RHS查詢找到了一個變數,但是你嘗試對這個變數的值進行不合理的操作,比如試圖對一個非函式型別的值進行函式呼叫,或著引用null或undefined型別的值中的屬性,那麼引擎會丟擲另外一種型別的異常,叫作TypeError。

 

           2.

           console.log(typeof [1, 2]);
            console.log(typeof `leipeng`);
            var i = true;
            console.log(typeof i);
            console.log(typeof 1);
            var a;
            console.log(typeof a);
            function a(){};
            console.log(typeof a);
            console.log(typeof `true`);

 

 

          

    :執行結果為依次列印object string boolean number function function string.

    引擎會在解釋JavaScript程式碼之前對齊進行編譯。編譯階段的一部分工作就是找到所有的宣告,並用合適的作用域將它們關聯起來。而宣告會從它在程式碼中出現的位置被“移動”到各自作用域的頂端,這個過程叫做提升。相較之下,當引擎執行LHS查詢時,如果在頂層(全域性作用域)中也無法找到目標變數,全域性作用域中就會建立一個具有該名稱的變數,並將其返還給引擎,前提是程式執行在非“嚴格模式”下。另外值得注意的是,每個作用域都會進行提升操作,而且函式宣告會被提升,但是函式表示式卻不會被提升。函式宣告和變數宣告都會被提升。但是一個值得注意的細節(這個細節可以出現在有多個“重複”宣告的程式碼中)是函式會首先被提升,然後才是變數。

    所以,function a(){}會被先提升到作用域頂端,當編譯到 var i =true;時,JavaScript會將其看成兩個宣告:var i;和i=true;。第一個定義宣告是在編譯階段進行的,並把宣告提前到作用域(此程式中為全域性作用域)的頂端第二個宣告會被留在原地等待執行階段。同理編譯到var a時,把宣告提升,但它是重複的宣告,因此被忽略了。所以程式碼片段會被引擎理解為如下形式:

 

           function a(){};
           var i
1.     console.log(typeof [1, 2]);
2.      console.log(typeof `leipeng`);
            i = true;
3.      console.log(typeof i);
4.      console.log(typeof 1);           
5.      console.log(typeof a);          
6.      console.log(typeof a);
7.      console.log(typeof `true`); 

    ECMAScript中有5種簡單資料型別(也稱為基本資料型別):Undefined、Null、Boolean、Number和String。還有1種複雜資料型別——Object,Object本質上是由一組無序的名值對組成的。typeof是一種用來檢測給定變數資料型別的操作符,對一個值使用typeof操作符可能返回下列某個字串:

    “undefined”——如果這個值未定義;

    “boolean”——如果這個值是布林值;

    “string”——如果這個值是字串;

    “number”——如果這個值是數值;

“object”——如果這個值是物件或null或陣列;

    “function”——如果這個值是函式。

    1.在第一個列印中要判斷的值為[1, 2],這是一個陣列,按照上面的說法,它會返回object。如果要檢測一個陣列,可以使用以下幾種方法:a.instanceof,對於一個網頁或者一個全域性作用域而言,使用instanceof操作符就能得到滿意的結果。

           var arr1=[1,2];

           console.log(arr1 instanceof Array);//true

    instanceof操作符的問題在於,它假定只有一個全域性執行環境。如果網頁中包含多個框架,那實際上就存在兩個以上不同的全域性執行環境,從而存在兩個以上不同版本的Array建構函式。如果你從一個框架向另一個框架傳入一個陣列,那麼傳入的陣列與在第二個框架中原生建立的陣列分別具有各自不同的建構函式。

    為了解決這個問題,ECMAScript 5新增了Array.isArray()方法。這個方法的目的是最終確定某個值到底是不是陣列,而不管它是在哪個全域性執行環境中建立的。這個方法的用法如下。

           var arr1=[1,2];

           console.log(Array.isArray(arr1));//true

    2.第二個要判斷的值為`leipeng` ,字串可以用雙引號或者單引號表示,這是一個字串,所以列印string

    3. 第三個要判斷的值為變數i,對i進行RHS查詢,得到之前被賦的值true,所以等同於

           console.log(typeof true);

    Boolean型別是ECMAScript中使用得最多的一種型別,該型別只有兩個字面值:true和false。這兩個值與數字值不是一回事,因此true不一定等於1,而false也不一定等於0。可以對任何資料型別的值呼叫Boolean()函式,而且總會返回一個Boolean值。或者在判斷語句的條件裡填其他資料型別的值來轉換為Bollean值,例如

for(1){

    console.log(123)//123

}

至於返回的這個值是true還是false,取決於要轉換值的資料型別及其實際值。下表給出了各種資料型別及其對應的轉換規則。

資料型別

轉換為true

轉換為false的值

Boolean

true

False

String

任何非空字串

“”(空字串)

Number

任何非零數字值(包括無窮大)

0和NaN

Object

任何物件

null

Undefined

 

undefined

 

    4. 第四個要判斷的值為1,1即為Number型別,上面解答中也說到了這兩個值與數字值不是一回事,因此true不一定等於1,而false也不一定等於0。所以列印Number

    5.6. 第五個和第六個相同,引擎會在作用域頂端找到function a(){};所以判斷的型別值為function。

    7. 要判斷的值為“true”,前面說到了字串的表示方法,可以用雙引號或者單引號表示,即“true”的型別為String

 

 

           3.

           for(i=0, j=0; i<4, j<6; i++, j++){
               k = i + j;
            }
            console.log(k);

 

 

           

    :結果為在控制檯列印10。

在for迴圈語句中一般寫法為:

           for(語句1;語句2;語句3){

              被執行的程式碼塊

}

語句1: 1.初始化變數; 2.是可選的,也可以不填;3.可以寫任意多個,與語句3中變數名對應

語句2: 1.執行條件  2. 是可選的(若不填,迴圈中必須要有break,不然死迴圈)3.如果出現多個一逗號為間隔的判斷依據,則以分號前的最後一項為準。

語句3:  1. 改變初始變數的值  2.是可選的

所以這個for迴圈的真正的執行條件是j<6 ,每執行一次迴圈i和j就加一,共執行6次迴圈,即最後i=j=5;k=10,列印10。

 

            4.

           var name = `laruence`;    
            function echo()
            {        
                console.log(name);  
            }     
            function env()
            {
                var name = `eve`;        
                echo();  
            }     
            env();

 

 

 

    :列印 laruebnce

   

    作用域負責收集並維護由所有宣告的識別符號(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權。作用域共有兩種主要的工作模型。第一種是最為普遍的,被大多數程式語言所採用的詞法作用域。另外一種叫作動態作用域,仍有一些程式語言在使用(比如Bash指令碼、Perl中的一些模式等)

需要說明的是JavaScript中的作用域是詞法作用域。當一個塊或函式巢狀在另一個塊或函式中時,就發生了作用域的巢狀。因此,在當前作用域中無法找到某個變數時,引擎就會在外層巢狀的作用域中繼續查詢,直到找到該變數,或抵達最外層的作用域(也就是全域性作用域)為止。

詞法作用域是一套關於引擎如何尋找變數以及會在何處找到變數的規則。詞法作用域最重要的特徵是它的定義過程發生在程式碼的書寫階段(假設沒有使用eval()或with)。而動態作用域並不關心函式和作用域是如何宣告以及在何處宣告的,只關心它們從何處呼叫。換句話說,作用域鏈是基於呼叫棧的,而不是程式碼中的作用域巢狀。根據下面程式碼可以看出區別:

     function fun() {
   console.log( a );
  }
  function bar() {
  var a = 0;
  fun();
  }
  var a = 1;
  bar();

 

 

以上程式碼會列印1;因為詞法作用域讓fun()中的a通過RHS引用到了全域性作用域中的a,因此會輸出1;

    但如果JavaScript具有動態作用域,理論上,上面的程式碼最終會列印0;因為當fun()無法找到a的變數引用時,會順著呼叫棧在呼叫fun()的地方查詢a,而不是在巢狀的詞法作用域鏈中向上查詢。由於fun()是在bar()中呼叫的,引擎會檢查bar()的作用域,並在其中找到值為0的變數a。

    綜上所述,詞法作用域關注函式在何處宣告,而動態作用域關注函式從何處呼叫。在本題中首先對程式碼進行編譯,題中程式碼片段會被引擎理解為以下形式;

           function echo()
           {        
              console.log(name);  
           }     
           function env()
           {
              var name = `eve`;        
              echo();  
           }     
           var name
           name = `laruence`;
            env();   

 

    首先引擎執行到env()時,在全域性作用域中進行RHS查詢,找到函式env並執行,然後在env函式作用域中對echo進行RHS查詢,但並沒有查詢到該函式,所以根據作用域巢狀原理在該作用域的外層即全域性作用域中進行查詢,找到函式echo並執行該函式,然後對console進行RHS查詢找到log方法,然後對name進行RHS查詢,在自己作用域內沒有找到然後到全域性作用域中找到name,然後對name進行LHS查詢,同理在全域性作用域中找到name=“lanuence”,然後列印laruence。

 

 

           5.

           var a = `` + 3; 
            var b = 4;
            console.log(typeof a);
            console.log(a+b);
            console.log(a-b);
            var foo = "11"+2+"1";
            console.log(foo);
            console.log(typeof foo);

 

 

          

    答:依次列印string 34 -1 1121 string

    一元加操作符以一個加號(+)表示,放在數值前面,對數值不會產生任何影響,例如:

           var num = 25;

num = +num; // 仍然是25

    不過,在對非數值應用一元加操作符時,該操作符會像Number()轉型函式一樣對這個值執行轉換。換句話說,布林值false和true將被轉換為0和1,字串值會被按照一組特殊的規則進行解析,而物件是先呼叫它們的valueOf()和(或)toString()方法,再轉換得到的值。例如:

           var o = { valueOf: function() { return -1; } };
      console.log(+`01`)//1
      console.log(typeof (+`01`))//number
      console.log(+`z`)//NaN
      console.log(+false)//0
      console.log(+o)//-1

 

 

    一元減操作符主要用於表示負數,例如將1轉換成1。下面的例子演示了這個簡單的轉換過程:

           var num = 25;

num = -num; // 變成了-25

    在將一元減操作符應用於數值時,該值會變成負數(如上面的例子所示)。而當應用於非數值時,一元減操作符遵循與一元加操作符相同的規則,最後再將得到的數值轉換為負數,如下面的例子所示:

  var o = { valueOf: function() { return -1; } };
console.log(-`01`)//-1
console.log(typeof (-`01`))//number
console.log(-`z`)//NaN
console.log(-false)//0
console.log(-o)//1

 

 

    以上用法只是把加減符號用於單個值上,當使用加減符號對多個值進行組合使用時,情況會發生變化,當兩個或多個數值進行加減操作時,其執行結果和數學運算相同(除去各邊浮點數及無窮),但當運算值存在非數值時,加減兩個運算子存在差異。

    字串之間使用加號表示把兩邊的內容進行拼接,當一個數值與字串相加時,會把數值轉換為字串然後進行拼接,當數值與其他型別值相加時會遵循與一元加操作符相同的規則。而多個值之間使用減號不會存在拼接,而是和單個數值使用減號的規則一致。例如:

           console.log(`1`+2);//12
           console.log(true+1)//2
           console.log(`1`-2);//-1
            console.log(true-1)//0

 

 

綜上所述,本題中a被一個字串+數值給賦值,所以a=“3”,型別為String

同理a+b是一個字串和數值相加,先把數值轉換為字串,然後進行拼接即列印34;

但a-b會先把a,b轉換為數值然後b取負數再相加,即3+(-4)=-1,所以列印-1

同理 “11”+2+“1”會先把2轉換為“2”,然後進行拼接即foo=“1121”,型別為字串。

 

 

 

           6.

           var x=8;
            var objA = {
                x:`good`,
                y:32
            }
            function add(x,y){
                console.log(x.y+y);
            }
            function fn(x,y){
                x.y=5;
                y(x,3);
            }
            fn(objA,add);
            console.log(objA);

 

 

          

    答:結果是依次列印8 {x:“good”,y:5};

   首先編譯器對程式碼進行編譯,先提升兩個函式表示式,然後提升宣告x,objA,所以程式碼片段會被引擎理解為如下形式:

           function add(x,y){
                console.log(x.y+y);
            }
           function fn(x,y){
                x.y=5;
                y(x,3);
            }
           var x;
           var objA;
           x=8;
           objA={
      x:‘good’,
      y:32
      }
           fn(objA,add);
            console.log(objA);

 

 

    然後引擎執行程式碼fn(objA,add),objA和add是實參被傳入函式fn中,所以相當於執行函式fn(objA,add)。在函式內部出現x.y=5。首先物件取屬性操作的優先順序最高,其次訪問物件屬性有兩種方法,一種是本題中使用的點表示法,這也是很多物件導向語言中通用的語法。不過,在JavaScript也可以使用方括號表示法來訪問物件的屬性。在使用方括號語法時,應該將要訪問的屬性以字串的形式放在方括號中,從功能上看,這兩種訪問物件屬性的方法沒有任何區別。但方括號語法的主要優點是可以通過變數來訪問屬性。而點表示法 不能使用變數來訪問屬性,所以本題中的x.y=5等同於objA.y=5,即在全域性作用域中找到objA並把其y屬性改變為5。而y(x,3)就等同於add(objA,3),即執行函式add,併為形參x,y傳入實參objA和3,而且同上面所講物件取屬性優先順序更高,所以函式add內部可以看為console.log(objA.y+3),對objA進行RHS查詢,並得到它的y屬性值為5,所以列印值為5。

    若本題中改為

           var x=8;
            var objA = {
                x:`good`,
                y:32
            }
            function add(x,y){
                console.log(x.y+y);
            }
            function fn(x,y){
                x[y]=5;//改變去屬性表示方法
               y(x,3);
            }
            fn(objA,add);
            console.log(objA);

 

 

    則會第一個列印是38,因為x[y]=5等同於objA[add]=5,即給objA新增里一個add屬性,而objA[y]還是等於35,所以console.log(x.y+y)等同於console.log(objA.y+3)即35+3等於38,所以會列印38.

 

           7.

           function changeObjectProperty (o) {
                o.siteUrl = "http://www.csser.com/";
                o = new Object();   
                o.siteUrl = "http://www.popcg.com/";
            }
            var CSSer = new Object(); 
            changeObjectProperty(CSSer);
            console.log(CSSer.siteUrl);

 

 

          

   答:列印http://www.csser.com/

    首先說明一點ECMAScript中所有函式的引數都是按值傳遞的。也就是說,把函式外部的值複製給函式內部的引數,就和把值從一個變數複製到另一個變數一樣。基本型別值的傳遞如同基本型別變數的複製一樣,而引用型別值的傳遞,則如同引用型別變數的複製一樣。訪問變數有按值和按引用兩種方式,而引數只能按值傳遞。

在向引數傳遞基本型別的值時,被傳遞的值會被複制給一個區域性變數(即命名引數,或者用ECMAScript的概念來說,就是arguments物件中的一個元素)。在向引數傳遞引用型別的值時,會把這個值在記憶體中的地址複製給一個區域性變數,因此這個區域性變數的變化會反映在函式的外部。請看下面這個例子:

 function addTen(num) {
num += 10;
return num;
}
           var count = 20;
var result = addTen(count);
alert(count); //20,沒有變化
alert(result); //30

 

 

    這裡的函式addTen()有一個引數num,而引數實際上是函式的區域性變數。在呼叫這個函式時,變數count作為引數被傳遞給函式,這個變數的值是20。於是,數值20被複制給引數num以便在addTen()中使用。在函式內部,引數num的值被加上了10,但這一變化不會影響函式外部的count變數。引數num與變數count互不相識,它們僅僅是具有相同的值。假如num是按引用傳遞的話,那麼變數count的值也將變成30,從而反映函式內部的修改。但如果使用物件,那麼情況會有一點複雜。再舉一個例子:

function setName(obj) {
obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"

 

 

    以上程式碼中建立一個物件,並將其儲存在了變數person中。然後,這個變數被傳遞到setName()函式中之後就被複制給了obj。在這個函式內部,obj和person引用的是同一個物件。換句話說,即使這個變數是按值傳遞的,obj也會按引用來訪問同一個物件。於是,當在函式內部為obj新增name屬性後,函式外部的person也將有所反映;因為person指向的物件在堆記憶體中只有一個,而且是全域性物件。有很多人錯誤地認為:在區域性作用域中修改的物件會在全域性作用域中反映出來,就說明引數是按引用傳遞的。為了證明物件是按值傳遞的,可以再看一看下面這個經過修改的例子:

          

function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"

 

    這個例子與前一個例子的唯一區別,就是在setName()函式中新增了兩行程式碼:一行程式碼為obj重新定義了一個物件,另一行程式碼為該物件定義了一個帶有不同值的name屬性。在把person傳遞給setName()後,其name屬性被設定為”Nicholas”。然後,又將一個新物件賦給變數obj,同時將其name屬性設定為”Greg”。如果person是按引用傳遞的,那麼person就會自動被修改為指向其name屬性值為”Greg”的新物件。但是,當接下來再訪問person.name時,顯示的值仍然是”Nicholas”。這說明即使在函式內部修改了引數的值,但原始的引用仍然保持未變。實際上,當在函式內部重寫obj時,這個變數引用的就是一個區域性物件了。而這個區域性物件會在函式執行完畢後立即被銷燬。

    當實參是陣列時,情況也有一些特殊,例如:

           
var a=[1,2,3];
function foo(a){
    a.push(4); //呼叫引用型別方法,改變了形參a,也改變了全域性變數a
    console.log(a); // [1,2,3,4] 此時的a是形參變數的值
    a=[5,6,7];      // 形參重新賦值不會改變全域性變數a
    console.log(a); // [5,6,7] 形參變數a
};
foo(a);
console.log(a); // [1,2,3,4]
    對照下面程式碼:
              var a=[1,2,3];
function foo(a){
    a=[5,6,7]; // 形參a被重新賦值,不會改變全域性a
    a.push(4); // 此時只改變了形參a,不會改變全域性a
    console.log(a); // [5,6,7,4]
};
foo(a);
console.log(a); // [1,2,3]

 

    綜上所述,在從一個變數向另一個變數複製基本型別值和引用型別值時,存在不同。如果從一個變數向另一個變數複製基本型別的值,會在變數物件上建立一個新值,然後把該值複製到為新變數分配的位置上。當從一個變數向另一個變數複製引用型別的值時,同樣也會將儲存在變數物件中的值複製一份放到為新變數分配的空間中。不同的是,這個值的副本實際上是一個指標,而這個指標指向儲存在堆中的一個物件。複製操作結束後,兩個變數實際上將引用同一個物件。因此,呼叫屬性或方法改變其中一個變數,就會影響另一個變數,如下面的例子所示:對引用資料型別,會發現在函式裡,形參被賦值或重新宣告之前,對形參呼叫引用資料型別的屬性(或方法)時,不僅會改變形參,還會改變全域性變數。

    所以對於本題中程式碼片段,實參CSSer是一個在物件,在函式changeObjectProperty中把CSSer值複製給o,然後對形參呼叫siterUrl屬性,不僅改變了形參o,也改變了CSSer,所以此時CSSer包含一個siteUrl屬性,並且屬性值為http://www.csser.com/,然後又將一個新物件賦給變數o,此時這個變數引用的就是一個區域性物件了,而這個區域性物件會在函式執行完畢後立即被銷燬。所以在o.siteUrl = “http://www.popcg.com/”;這一個操作裡,只會改變o的屬性,而不會改變外部CSSer的屬性。所以最後列印結果為http://www.csser.com/

 

 

           8.

           var num=5;
            function func1(){
                 var num=3;
                 var age =4;
                 function func2(){
                     console.log(num);
                     var num =`ivan`;
                     function func3(){
                       age =6; 
                     }
                     func3();
                     console.log(num);
                     console.log(age);
                }
                func2();
            }
            func1();

 

 

          

    答:結果為依次列印 undefined  ivan 6

根據本套面試題第一題及第二題中所寫的宣告提升等知識可以得首先引擎在解釋JacaScript程式碼之前首先對其編譯,編譯階段中的一部分工作就是找到所有的宣告,並用合適的作用域將它們關聯起來。引擎會在全域性作用域頂端宣告函式func1,然後宣告全域性變數num。在函式func1的作用域頂端先宣告函式func2。在函式func2內部作用域的頂端宣告函式func3,然後宣告變數num。引擎所理解的程式碼格式如下:

   

           function func1(){
              function func2(){
                  function func3(){
                     age =6; 
                  }
                  var num;
                  console.log(num);//undefined
                  num =`ivan`;
                  func3();
                  console.log(num);//ivan
                  console.log(age);//6
              }
              var num;
              var age;
              num =3;
              age =4;
              func2();
           }
           var num;
           num=5;
           func1();

 

 

    然後引擎開始從上到下執行程式碼,首先執行num=5,對num進行LHS查詢,在全域性作用域中找到變數num並將值賦給它。

然後執行函式func1,依次對變數num,age進行LHS查詢,查詢規則為從裡到外,即從自己的作用域依次查詢巢狀它的外部作用域。所以這兩個變數直接在自己作用域內找到已經被宣告的變數空間,然後把值3 ,4依次賦值給它們。

然後執行函式func2,首先執行console.log(num);對num進行RHS查詢,在func2作用域中找到被宣告的變數num,但是該變數並未賦值,所以列印結果為undefined。然後執行num=’ivan’;對num進行LHS查詢,在func2作用域中找到num並賦值為ivan。

然後執行函式func3,執行age=6;對age進行LHS查詢,在func3作用域內沒有找到該變數,然後到包裹該作用域的func2作用域中查詢該變數,找到該變數,並對其重新賦值為6。

函式func3內部程式碼執行完後,再接著執行func2內部程式碼console.log(num),對num進行RHS引用,在其所在作用域內找到被賦值為ivan的變數num,然後把得到的值傳給console.log(),即列印出ivan。

接著執行程式碼console.log(age);和上步同理,對age進行RHS查詢,在其所在作用域內沒有找到變數age,然後向上級作用域接著查詢,找到已被重新賦值為6的變數age,並把值傳遞給console.log(),所以列印結果為6。

   

           9.

           var  fn1 = `ivan`;
            var  name =`good`;
            var fn1 = function(y){
                y();
            }
            function fn1(x){       
                x(name); 
            }
            function fn2(x){
                console.log(x);
                console.log(name);
                var name = `hello`;
                console.log(name);
            }
            fn1(fn2);

 

          

    :結果為依次列印 undefined undefined hello

    根據本套面試題第一題及第二題中所寫的宣告提升等知識可以得首先引擎在解釋JacaScript程式碼之前首先對其編譯,編譯階段中的一部分工作就是找到所有的宣告,並用合適的作用域將它們關聯起來。所以引擎會在全域性作用域頂端先宣告函式fn1,然後宣告函式fn2,在fn2內部作用域的頂端宣告name。在宣告fn2下面宣告變數fn1,因為與函式fn1宣告重複,所以忽略該宣告,然後宣告變數name,然後宣告fn1,與之前宣告重複被忽略。最後結果如下:

           function fn1(x){       
              x(name); 
           }
           function fn2(x){
              var name;
              console.log(x);//undefined
              console.log(name);//undefined
              name = `hello`;
              console.log(name);//hello
           }
           // var  fn1;  宣告被忽略
           var  name;
           // var fn1;  宣告被忽略
           fn1 = `ivan`;
           name =`good`;
           fn1 = function(y){
              y();
           }
           fn1(fn2);

 

 

          

    然後引擎開始執行程式碼,首先對fn1進行LHS查詢,在全域性作用域中找到該變數,然後對其重新賦值為ivan,(需要說明的是在JavaScript中可以通過改變變數的值來改變變數的屬性)。

    然後對name進行LHS查詢,在全域性作用域內找到該變數,並賦值為good。

    然後對fn1進行LHS查詢,在全域性作用域內找到該變數,並把function(y){y();}賦值給它,並且該變了fn1的型別。

    然後執行函式fn1,其中y為形參,fn2為實參,對y(隱式的)進行LHS查詢,把fn2賦給y。

然後執行函式y(),即執行函式fn2(),fn2中存在形參x,首先對x(隱式的)進行LHS查詢,但並未查詢到所對應的實參,所以x為空。然後執行程式碼console.log(x);即列印undefined。

然後執行console.log(name),對name進行LHS查詢,在其所在作用域中找到沒有賦值的變數name,所以列印undefined。

然後執行name=“hello”,對nameRHS查詢並賦值。

然後再執行console.log(name),對name進行LHS查詢,在其所在作用域中找到被賦值為hello的變數name,所以列印hello。

           10.

           var buttons = [{name:`b1`},{name:`b2`},{name:`b3`}];
            function bind(){
                for (var i = 0; i < buttons.length; i++) {
                   buttons[i].onclick = function() {
                       console.log(i);
                   }
                }
            };
            bind();
            buttons[0].onclick();//3
            buttons[1].onclick();//3
            buttons[2].onclick();//3

 

 

          

    :執行結果為依次列印3 3 3

    上面的程式碼在迴圈裡包含著一個閉包,閉包可以簡單理解為:當函式可以記住並訪問所的詞法作用域時,就產生了閉包,即使函式是在當前詞法作用域之外執行。在for迴圈裡面的匿名函式執行 console.log(i)語句的時候,由於匿名函式裡面沒有i這個變數,所以這個i他要從父級函式中尋找i,實際情況是儘管迴圈中的五個函式是在各個迭代中分別定義的,

但是它們都被封閉在一個共享的全域性作用域中,因此實際上只有一個i,當找到這個i的時候,是for迴圈完畢的i,也就是3,所以這個bind()得到的是一三個相同的函式:

function(){console. log(3)}

    所以當執行buttons[0].onclick();和其他兩個程式時時都會列印3。

    如果要想實現理想中的列印0 1 2的效果,需要更多的閉包作用域,特別是在迴圈的過程中每個迭代都需要一個閉包作用域。而函式自呼叫會通過宣告立即執行一個函式來建立作用域。

例如:

               var buttons = [{name:`b1`},{name:`b2`},{name:`b3`}];
               function bind(){
                  for (var i = 0; i < buttons.length; i++) {
                      buttons[i].onclick = (function (i){
                         console.log(i)
                      }(i));
                  }
               };
                bind();
               buttons[0].onclick;//0
               buttons[1].onclick;//1
               buttons[2].onclick;//2

 

 

          

           11.

           function fun(n,o) {
                console.log(o)
                return {
                    fun:function(m){
                        return fun(m,n);
                    }
                };
            }
            var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);
            var b = fun(0).fun(1).fun(2).fun(3);
            var c = fun(0).fun(1);  c.fun(2);  c.fun(3);

 

 

          

    : 結果為依次列印undefined 0 0 0 undefined 0 1 2 undefined 0 1 1

    var a =fun1(0);o沒有被賦值列印undefined,a等於返回的一個物件

{fun:function(m){oturn fun1(m,0)};

                 

    a.fun(1);fun1(1,0)列印0,返回一個物件

{fun:function(m){oturn fun1(m,1)}}

    同理,a.fun(2);fun1(2,0)列印0

    同理,a.fun(3);fun1(3.0)列印0

 

    var b=fun1(0).fun(1).fun(2).fun(3);o沒有被賦值列印undefined,b等於返回的一個物件

{fun:function(m){oturn fun1(m,0)};

    .fun(1)=function(1){oturn fun1(1,0)}  列印0

    返回一個物件 {fun:function(m) {oturn fun1(m,1)}}

    .fun(2)=function(2) {oturn fun1(2,1)} 列印1

    返回一個物件 {fun:function(m) {oturn fun1(m,2)}}

    .fun(3)=function(3) {oturn fun1(3,2)} 列印2

                 

    var c=fun1(0).fun(1);

    o沒有被賦值列印undefined,c等於返回的一個物件{fun:function(m){oturn fun1(m,0)};

    .fun(1)=function(1){oturn fun1(1,0)}  列印0

    返回一個物件{fun:function(m){oturn fun1(m,1)};也就是等於c

    c.fun(2);

    c={fun:function(m){oturn fun1(m,1)};

    所以fun(2)=fun1(2,1),列印1               

    c.fun(3);

    c={fun:function(m){oturn fun1(m,1)};

    所以fun(3)=fun1(3,1),列印1 

 

 

            12.

            var name    = `lili`;
            var obj     = {
                name: `liming`,
                prop: {
                    name: `ivan`,
                    getname: function() {
                      return this.name;
                    }
                }
            };
            console.log(obj.prop.getname());//ivan
            var test = obj.prop.getname; 
            console.log(test()); //lili

 

           

    答:結果為依次列印ivan    lili

    在從一個變數向另一個變數複製基本型別值和引用型別值時,存在不同。如果從一個變數向另一個變數複製基本型別的值,會在變數物件上建立一個新值,然後把該值複製到為新變數分配的位置上。當從一個變數向另一個變數複製引用型別的值時,同樣也會將儲存在變數物件中的值複製一份放到為新變數分配的空間中。不同的是,這個值的副本實際上是一個指標,而這個指標指向儲存在堆中的一個物件。複製操作結束後,兩個變數實際上將引用同一個物件。因此,呼叫屬性或方法改變其中一個變數,就會影響另一個變數。

    在本題中,在全域性作用域中宣告瞭一個變數test,並把obj.prop.getname;賦值給它,那麼test便指向函式function() {return this.name; }。

    當引擎執行到console.log(obj.prop.getname());時對obj.prop.getname進行LHS查詢,得到函式function() {return this.name; },然後執行函式,

    首先說明this並不是指向函式自身或是函式的詞法作用域,this的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式。當一個函式被呼叫時,會建立一個活動記錄(有時候也稱為執行上下文)。這個記錄會包含函式在哪裡被呼叫(呼叫棧)、函式的呼叫方法、傳入的引數等資訊。this就是記錄的其中一個屬性,會在函式執行的過程中用到。簡單來說,this是指呼叫包含this最近的函式的物件。

    而要找到this代表什麼,首先要找到呼叫位置,呼叫位置就是函式在程式碼中被呼叫的位置(而不是宣告的位置)。首先最是要分析呼叫棧(就是為了到達當前執行位置所呼叫的所有函式)。呼叫位置就在當前正在執行的函式的前一個呼叫中。在程式碼

console.log(obj.prop.getname());

中,this所在函式是被prop呼叫的,即呼叫位置是obj的屬性prop,所以this.name可以看成obj.prop.name,即ivan。

    宣告在全域性作用域中的變數(var test = obj.prop.getname;  )就是全域性物件的一個同名屬性。它們本質上就是同一個東西,並不是通過複製得到的,就像一個硬幣的兩面一樣。接下來我們可以看到當呼叫test()時,this.name被解析成了全域性變數name。因為在本題中,函式呼叫時應用了this的預設繫結,因此this指向全域性物件。換句話說就是test的呼叫位置在全域性作用域,所以this.name就在全域性作用域中匹配,得到name=lili。

   

          

 

相關文章