JS 作用域與作用域鏈詳解
(1)作用域
一個變量的作用域(scope)是程序源代碼中定義的這個變量的區(qū)域。
1. 在JS中使用的是詞法作用域(lexical scope)
不在任何函數(shù)內(nèi)聲明的變量(函數(shù)內(nèi)省略var的也算全局)稱作全局變量(global scope)
在函數(shù)內(nèi)聲明的變量具有函數(shù)作用域(function scope),屬于局部變量
局部變量優(yōu)先級高于全局變量
var name="one";
function test(){
var name="two";
console.log(name); //two
}
test();
函數(shù)內(nèi)省略var的,會影響全局變量,因為它實際上已經(jīng)被重寫成了全局變量
var name="one";
function test(){
name="two";
}
test();
console.log(name); //two
函數(shù)作用域,就是說函數(shù)是一個作用域的基本單位,js不像c/c++那樣具有塊級作用域 比如 if for 等
function test(){
for(var i=0;i<10;i++){
if(i==5){
var name = "one";
}
}
console.log(name); //one
}
test(); //因為是函數(shù)級作用域,所以可以訪問到name="one"
當(dāng)然了,js里邊還使用到了高階函數(shù),其實可以理解成嵌套函數(shù)
function test1(){
var name = "one";
return function (){
console.log(name);
}
}
test1()();
test1()之后將調(diào)用外層函數(shù),返回了一個內(nèi)層函數(shù),再繼續(xù)(),就相應(yīng)調(diào)用執(zhí)行了內(nèi)層函數(shù),所以就輸出 ”one"
嵌套函數(shù)涉及到了閉包,后面再談..這里內(nèi)層函數(shù)可以訪問到外層函數(shù)中聲明的變量name,這就涉及到了作用域鏈機制
2. JS中的聲明提前
js中的函數(shù)作用域是指在函數(shù)內(nèi)聲明的所有變量在函數(shù)體內(nèi)始終是可見的。并且,變量在聲明之前就可以使用了,這種情況就叫做聲明提前(hoisting)
tip:聲明提前是在js引擎預(yù)編譯時就進行了,在代碼被執(zhí)行之前已經(jīng)有聲明提前的現(xiàn)象產(chǎn)生了
比如
var name="one";
function test(){
console.log(name); //undefined
var name="two";
console.log(name); //two
}
test();
上邊就達(dá)到了下面的效果
var name="one";
function test(){
var name;
console.log(name); //undefined
name="two";
console.log(name); //two
}
test();
再試試把var去掉?這是函數(shù)內(nèi)的name已經(jīng)變成了全局變量,所以不再是undefined
var name="one";
function test(){
console.log(name); //one
name="two";
console.log(name); //two
}
test();
3. 值得注意的是,上面提到的都沒有傳參數(shù),如果test有參數(shù),又如何呢?
function test(name){
console.log(name); //one
name="two";
console.log(name); //two
}
var name = "one";
test(name);
console.log(name); // one
之前說過,基本類型是按值傳遞的,所以傳進test里面的name實際上只是一個副本,函數(shù)返回之后這個副本就被清除了。
千萬不要以為函數(shù)里邊的name="two"把全局name修改了,因為它們是兩個獨立的name
(2)作用域鏈
上面提到的高級函數(shù)就涉及到了作用域鏈
function test1(){
var name = "one";
return function (){
console.log(name);
}
}
test1()();
1. 引入一大段話來解釋:
每一段js代碼(全局代碼或函數(shù))都有一個與之關(guān)聯(lián)的作用域鏈(scope chain)。
這個作用域鏈?zhǔn)且粋€對象列表或者鏈表,這組對象定義了這段代碼中“作用域中”的變量。
當(dāng)js需要查找變量x的值的時候(這個過程稱為變量解析(variable resolution)),它會從鏈的第一個對象開始查找,如果這個對象有一個名為x的屬性,則會直接使用這個屬性的值,如果第一個對象中沒有名為x的屬性,js會繼續(xù)查找鏈上的下一個對象。如果第二個對象依然沒有名為x的屬性,則會繼續(xù)查找下一個,以此類推。如果作用域鏈上沒有任何一個對象含有屬性x,那么就認(rèn)為這段代碼的作用域鏈上不存在x,并最終拋出一個引用錯誤(ReferenceError)異常。
2. 作用域鏈舉例:
在js最頂層代碼中(也就是不包括任何函數(shù)定義內(nèi)的代碼),作用域鏈由一個全局對象組成。
在不包含嵌套的函數(shù)體內(nèi),作用域鏈上有兩個對象,第一個是定義函數(shù)參數(shù)和局部變量的對象,第二個是全局對象。
在一個嵌套的函數(shù)體內(nèi),作用域上至少有三個對象。
3. 作用域鏈創(chuàng)建規(guī)則:
當(dāng)定義一個函數(shù)時(注意,是定義的時候就開始了),它實際上保存一個作用域鏈。
當(dāng)調(diào)用這個函數(shù)時,它創(chuàng)建一個新的對象來儲存它的參數(shù)或局部變量,并將這個對象添加保存至那個作用域鏈上,同時創(chuàng)建一個新的更長的表示函數(shù)調(diào)用作用域的“鏈”。
對于嵌套函數(shù)來說,情況又有所變化:每次調(diào)用外部函數(shù)的時候,內(nèi)部函數(shù)又會重新定義一遍。因為每次調(diào)用外部函數(shù)的時候,作用域鏈都是不同的。內(nèi)部函數(shù)在每次定義的時候都要微妙的差別---在每次調(diào)用外部函數(shù)時,內(nèi)部函數(shù)的代碼都是相同的,而且關(guān)聯(lián)這段代碼的作用域鏈也不相同。
(tip: 把上面三點理解好,記住了,最好還要能用自己的話說出來,不然就背下來,因為面試官就直接問你:請描述一下作用域鏈...)
舉個作用域鏈的實用例子:
var name="one";
function test(){
var name="two";
function test1(){
var name="three";
console.log(name); //three
}
function test2(){
console.log(name); // two
}
test1();
test2();
}
test();
上邊是個嵌套函數(shù),相應(yīng)的應(yīng)該是作用域鏈上有三個對象
那么在調(diào)用的時候,需要查找name的值,就在作用域鏈上查找
當(dāng)成功調(diào)用test1()的時候,順序為 test1()->test()->全局對象window 因為在test1()上就找到了name的值three,所以完成搜索返回
當(dāng)成功調(diào)用test1()的時候,順序為 test2()->test()->全局對象window 因為在test2()上沒找到name的值,所以找test()中的,找到了name的值two,就完成搜索返回
還有一個例子有時候我們會犯錯的,面試的時候也經(jīng)常被騙到。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" " <html xmlns=" <head>
<script type="text/javascript">
function buttonInit(){
for(var i=1;i<4;i++){
var b=document.getElementById("button"+i);
b.addEventListener("click",function(){
alert("Button"+i); //都是 Button4
},false);
}
}
window.onload=buttonInit;
</script>
</head>
<body>
<button id="button1">Button1</button>
<button id="button2">Button2</button>
<button id="button3">Button3</button>
</body>
</html>
為什么?
根據(jù)作用域鏈中變量的尋找規(guī)則:
b.addEventListener("click",function(){
alert("Button"+i);
},false);
這里有一個函數(shù),它是匿名函數(shù),既然是函數(shù),那就在作用域鏈上具有一個對象,這個函數(shù)里邊使用到了變量i,它自然會在作用域上尋找它。
查找順序是 這個匿名函數(shù) -->外部的函數(shù)buttonInit() -->全局對象window
匿名函數(shù)中找不到i,自然跑到了buttonInit(), ok,在for中找到了,
這時注冊事件已經(jīng)結(jié)束了,不要以為它會一個一個把i放下來,因為函數(shù)作用域之內(nèi)的變量對作用域內(nèi)是一直可見的,就是說會保持到最后的狀態(tài)
當(dāng)匿名函數(shù)要使用i的時候,注冊事件完了,i已經(jīng)變成了4,所以都是Button4
那怎么解決呢?
給它傳值進去吧,每次循環(huán)時,再使用一個匿名函數(shù),把for里邊的i傳進去,匿名函數(shù)的規(guī)則如代碼
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" " <html xmlns=" <head>
<script type="text/javascript">
function buttonInit(){
for(var i=1;i<4;i++){
(function(data_i){
var b=document.getElementById("button"+data_i);
b.addEventListener("click",function(){
alert("Button"+data_i);
},false);
})(i);
}
}
window.onload=buttonInit;
</script>
</head>
<body>
<button id="button1">Button1</button>
<button id="button2">Button2</button>
<button id="button3">Button3</button>
</body>
</html>
這樣就可以 Button1..2..3了
4.上述就是作用域鏈的基本描述,另外,with語句可用于臨時拓展作用域鏈(不推薦使用with)
語法形如:
with(object)
statement
這個with語句,將object添加到作用域鏈的頭部,然后執(zhí)行statement,最后把作用域鏈恢復(fù)到原始狀態(tài)
簡單用法:
比如給表單中各個項的值value賦值
一般可以我們直接這樣
var f = document.forms[0];
f.name.value = "";
f.age.value = "";
f.email.value = "";
引入with后(因為使用with會產(chǎn)生一系列問題,所以還是使用上面那張形式吧)
with(document.forms[0]){
f.name.value = "";
f.age.value = "";
f.email.value = "";
}
另外,假如 一個對象o具有x屬性,o.x = 1;
那么使用
with(o){
x = 2;
}
就可以轉(zhuǎn)換成 o.x = 2;
假如o沒有定義屬性x,它的功能就只是相當(dāng)于 x = 2; 一個全局變量罷了。
因為with提供了一種讀取o的屬性的快捷方式,但他并不能創(chuàng)建o本身沒有的屬性。
以上所述就是本文的全部內(nèi)容了,希望能夠?qū)Υ蠹覍W(xué)習(xí)javascript有所幫助。
相關(guān)文章
JavaScript 鏈?zhǔn)浇Y(jié)構(gòu)序列化詳解
這篇文章主要介紹了JavaScript 鏈?zhǔn)浇Y(jié)構(gòu)序列化詳解的相關(guān)資料,需要的朋友可以參考下2016-09-09關(guān)于jQuery $.isNumeric vs. $.isNaN vs. isNaN
本篇文章,小編將為大家介紹jQuery $.isNumeric vs. $.isNaN vs. isNaN,有需要的朋友可以參考一下2013-04-04javascript學(xué)習(xí)筆記(十五) js間歇調(diào)用和超時調(diào)用
javascript學(xué)習(xí)筆記之js間歇調(diào)用和超時調(diào)用介紹,學(xué)習(xí)js的朋友可以參考下2012-06-06JavaScipt基本教程之JavaScript語言的基礎(chǔ)
2008-01-01