首頁  >  文章  >  web前端  >  js作用域及作用域鏈概念理解及使用_基礎知識

js作用域及作用域鏈概念理解及使用_基礎知識

WBOY
WBOY原創
2016-05-16 17:37:101064瀏覽

(1)作用域

一個變數的作用域(scope)是程式原始碼中定義的這個變數的區域。

1. 在JS中使用的是詞法作用域(lexical scope)

不在任何函數內宣告的變數(函數內省略var的也算全域)稱為全域變數(global scope)
在函數內宣告的變數具有函數作用域(function scope),屬於局部變數

局部變數優先權高於全域變數

var name="one";
function test(){
 var name="two";
 console.log(name); //two
}
test();

函數內省略var的,會影響全域變量,因為它實際上已經被重寫成全域變數

var name="one";
function test(){
 name="two";
 
}
test();
console.log(name); //two

函數作用域,就是說函數是作用域的基本單位,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(); //因为是函数级作用域,所以可以访问到name="one"

當然了,js裡邊還使用到了高階函數,其實可以理解成巢狀函數

function test1(){
 var name = "one";
 return function (){
  console.log(name);
 }
}
test1()();

test1()之後將呼叫外層函數,傳回了一個內層函數,再繼續(),就對應呼叫執行了內層函數,所以就輸出 ”one"
巢狀函數涉及到了閉包,後面再談..這裡內層函數可以存取到外層函數中聲明的變數name,這就涉及到了作用域鏈機制

2. JS中的聲明提前

js中的函數作用域是指在函數內宣告的所有變數在函數體內始終是可見的。而且,變數在宣告之前就可以使用了,這種情況就叫做宣告提前(hoisting)
tip:宣告提前是在js引擎預編譯時就進行了,在程式碼執行前已經有宣告提前的現象產生了

var name="one";
function test(){
 console.log(name); //undefined
 var name="two";
 console.log(name); //two
}

test();

上邊就達到下面的效果

var name="one";
function test(){
 var name;
 console.log(name); //undefined
 name="two";
 console.log(name); //two
}

test();

再試試把var去掉?這是函數內的name已經變成了全域變量,所以不再是undefined

var name="one";
function test(){
 console.log(name); //one
 name="two";
 console.log(name); //two
}

test();

3. 值得注意的是,上面提到的都沒有傳參數,如果test有參數,又如何呢?

function test(name){
 console.log(name); //one
 name="two";
 console.log(name); //two
}

var name = "one";
test(name);
console.log(name); // one

之前說過,基本型別是按值傳遞的,所以傳進test裡面的name其實只是一個副本,函數回傳之後這個副本就被清除了。
千萬不要以為函數裡邊的name="two"把全域name修改了,因為它們是兩個獨立的name

(2)作用域鏈

上面提到的高階函數就牽涉到了作用域鏈

function test1(){
 var name = "one";
 return function (){
  console.log(name);
 }
}
test1()();

1. 引入一大段話來解釋:
每一段js程式碼(全域程式碼或函數)都有一個與之關聯的作用域鏈(scope chain)。

這個作用域鍊是一個物件清單或是鍊錶,這組物件定義了這段程式碼中「作用域中」的變數。

當js需要查找變數x的值的時候(這個過程稱為變數解析(variable resolution)),它會從鏈的第一個物件開始查找,如果這個物件有一個名為x的屬性,則會直接使用這個屬性的值,如果第一個物件中沒有名為x的屬性,js會繼續尋找鏈上的下一個物件。如果第二個物件仍然沒有名為x的屬性,則會繼續尋找下一個,以此類推。如果作用域鏈上沒有任何一個物件含有屬性x,那麼就認為這段程式碼的作用域鏈上不存在x,最後拋出一個引用錯誤(ReferenceError)異常。

2. 作用域鏈舉例:

在js最頂層程式碼中(也就是不包括任何函數定義內的程式碼),作用域鏈由一個全域物件組成。

在不包含巢狀的函數體內,作用域鏈上有兩個對象,第一個是定義函數參數和局部變數的對象,第二個是全域對象。

在一個嵌套的函數體內,作用域上至少有三個物件。

3. 作用域鏈建立規則:

當定義一個函數時(注意,是定義的時候就開始了),它實際上保存一個作用域鏈。

當呼叫這個函數時,它會建立一個新的物件來儲存它的參數或局部變量,並將這個物件加入儲存到那個作用域鏈上,同時建立一個新的更長的表示函數呼叫作用域的「鏈」。

對於巢狀函數來說,情況又有所變化:每次呼叫外部函數的時候,內部函數又會重新定義一次。因為每次呼叫外部函數的時候,作用域鏈都是不同的。內部函數在每次定義的時候都要微妙的差異---在每次呼叫外部函數時,內部函數的程式碼都是相同的,而且關聯這段程式碼的作用域鏈也不相同。

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

上边是个嵌套函数,相应的应该是作用域链上有三个对象
那么在调用的时候,需要查找name的值,就在作用域链上查找

当成功调用test1()的时候,顺序为 test1()->test()->全局对象window 因为在test1()上就找到了name的值three,所以完成搜索返回
当成功调用test1()的时候,顺序为 test2()->test()->全局对象window 因为在test2()上没找到name的值,所以找test()中的,找到了name的值two,就完成搜索返回

还有一个例子有时候我们会犯错的,面试的时候也经常被骗到。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<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>

为什么?
根据作用域链中变量的寻找规则:

b.addEventListener("click",function(){ 
      alert("Button"+i);
    },false);

这里有一个函数,它是匿名函数,既然是函数,那就在作用域链上具有一个对象,这个函数里边使用到了变量i,它自然会在作用域上寻找它。
查找顺序是 这个匿名函数 -->外部的函数buttonInit() -->全局对象window

匿名函数中找不到i,自然跑到了buttonInit(), ok,在for中找到了,

这时注册事件已经结束了,不要以为它会一个一个把i放下来,因为函数作用域之内的变量对作用域内是一直可见的,就是说会保持到最后的状态

当匿名函数要使用i的时候,注册事件完了,i已经变成了4,所以都是Button4

那怎么解决呢?

给它传值进去吧,每次循环时,再使用一个匿名函数,把for里边的i传进去,匿名函数的规则如代码

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<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添加到作用域链的头部,然后执行statement,最后把作用域链恢复到原始状态

简单用法:

比如给表单中各个项的值value赋值

一般可以我们直接这样

var f = document.forms[0];
f.name.value = "";
f.age.value = "";
f.email.value = "";

引入with后(因为使用with会产生一系列问题,所以还是使用上面那张形式吧)

with(document.forms[0]){
f.name.value = "";
f.age.value = "";
f.email.value = "";
}

另外,假如 一个对象o具有x属性,o.x = 1;
那么使用

with(o){
 x = 2;
}

就可以转换成 o.x = 2;
假如o没有定义属性x,它的功能就只是相当于 x = 2; 一个全局变量罢了。

因为with提供了一种读取o的属性的快捷方式,但他并不能创建o本身没有的属性。

要理解变量的作用域范围就得先理解作用域链
用var关键字声明一个变量时,就是为该变量所在的对象添加了一个属性。
作用域链:由于js的变量都是对象的属性,而该对象可能又是其它对象的属性,而所有的对象都是window对象的属性,所以这些对象的关系可以看作是一条链
链头就是变量所处的对象,链尾就是window对象

看下面的代码:

复制代码 代码如下:

function t() {
var a;
function t2() {
var b;
}
}

js中函数也是对象,所以变量a所在的对象是t,t又在window对象中,所以a的作用域链如下
t--window
那么b所以在的对象即t2,t2又包含在t中,t又在window对象,所以b的作用域链如下
t2--t--window
明白了作用域链下面就开始变量的作用域分析了
1 javascript 没有var的变量都为全局变量,且为window对象的属性
复制代码 代码如下:

function test1() {
//执行这个句的时候它会找作用域对象,这个函数就是作用域链中的第一个对象,但这个对象中没有相关的var语句
//于里就找作用域链的第二个对象,即全局对象,而全局对象中也没有相关的var语句
//由于没有相关的var语句,js隐式在函数地声明了变量即var all;
all = 30;
alert(all);
}
test1();
alert(all);
alert(window.all);

2 函數內(函數內的函數除外)定義的變數在整個函數內部都有效
複製程式碼 程式碼如下:

function test2() {
var t = 0;
//在for的條件裡定義變量,這個變更的作用域鏈物件就是這個函數
//因此在整個的函數裡它是有效的
for (var i = 0; i t = i;
}
alert(i);
}
test2();

3 函數內部的變數取代全域同名變數
複製程式碼 程式碼如下:

var t = "bb";
function test() {
//執行t的時候,它會先找作用域鏈對象,由於它定義在函數內部,所以這個函數就是它的作用域鏈的第一個對象
//而在這個物件裡又有t的定義,所以t就是局部變數了,它替換了全域變數t
//t只是此時有定義,但並沒有賦值,賦值在下一行,所以這裡輸出了undefined
alert(t);
var t = "aa";
alert(t);
}
test();

4 沒塊的作用域
複製程式碼 程式碼如下:

if (true) {
//在區塊中定義了一個變量,它的作用域鏈的第一個物件就是全域物件window
var tmp = 0;
}
//tmp的作用域鏈的第一個物件就是全域物件window,而上面又有全域物件中相關的var語句,因此輸出0
alert(tmp);


以下內容來自讀網上博客的總結,當筆記使用,只記重點,同時非常感謝樂於分享的博主們,是你們讓我站在了巨人的肩旁上!
1、
複製程式碼 程式碼如下:

var temp = (function(){
var name ="test";
return function(){
 alert(name);
}
})();

以上程式碼片段是我們jser常見到的寫法,是傳說中的閉包。 眾所周知:呼叫 temp();會彈出 “ test”;該過程可以有以下三條理論作為依據來解釋:

1)js 作用域只和函數的界定符相關,函數與函數的巢狀形成了作用域鏈;
2)作用域鏈的建立規則是複製上一層環境的作用域鏈,並將指向本環境變數物件的指標放到鏈首;
3)在Javascript中,如果一個物件不再被引用,那麼這個物件就會被GC回收。如果兩個物件互相引用,而不再被第3者所引用,那麼這兩個互相引用的物件也會被回收。

如果看了以上3條還不明白,可看接下來結合理論對程式碼的詳細解釋:
首先外層函數執行完,被銷毀;但是外層函數的作用域鏈被複製到內層函數的作用域鏈裡,組成內層函數的作用域鏈的一部分,記住是複製,不是引用(依據第2條),所以內層函數仍然可以存取到name;由於傳回的內層函數被temp 引用,所以當外層函數執行完被銷毀後,內層函數雖然作為外層函數的一部分,但依然存在,正如第3條依據那樣,它被第三者引用了;傳說中的閉包也就是這個理
陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn