以下是我遇到的一些經典的JS面試題,結合我自己的理解寫的詳解,主要參考高程一書,歡迎大家批評指正 1. 答:運行結果為列印undefined。 首先,以上代碼完全運行的話需要引擎,編譯器,作用域的配合操作,(引擎負責整個JavaScript程式的編譯及執行過程,編譯器負責詞法分析及代碼生成等,作用域 ...
以下是我遇到的一些經典的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);
首先說明一點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。