萬盛學電腦網

 萬盛學電腦網 >> 腳本專題 >> javascript >> 深入理解javascript作用域和閉包

深入理解javascript作用域和閉包

 作用域

作用域是一個變量和函數的作用范圍,javascript中函數內聲明的所有變量在函數體內始終是可見的,在javascript中有全局作用域和局部作用域,但是沒有塊級作用域,局部變量的優先級高於全局變量,通過幾個示例來了解下javascript中作用域的那些“潛規則”(這些也是在前端面試中經常問到的問題)。

1. 變量聲明提前
示例1:

1 2 3 4 5 6 var scope="global"; function scopeTest(){   console.log(scope);   var scope="local"  } scopeTest(); //undefined

此處的輸出是undefined,並沒有報錯,這是因為在前面我們提到的函數內的聲明在函數體內始終可見,上面的函數等效於:

1 2 3 4 5 6 7 var scope="global"; function scopeTest(){   var scope;   console.log(scope);   scope="local"  } scopeTest(); //local

注意,如果忘記var,那麼變量就被聲明為全局變量了。

2. 沒有塊級作用域

和其他我們常用的語言不同,在Javascript中沒有塊級作用域:

1 2 3 4 5 6 7 8 9 10 11 12 function scopeTest() {   var scope = {};   if (scope instanceof Object) {     var j = 1;     for (var i = 0; i < 10; i++) {       //console.log(i);     }     console.log(i); //輸出10   }   console.log(j);//輸出1    }

在javascript中變量的作用范圍是函數級的,即在函數中所有的變量在整個函數中都有定義,這也帶來了一些我們稍不注意就會碰到的“潛規則”:

1 2 3 4 5 6 var scope = "hello"; function scopeTest() {   console.log(scope);//①   var scope = "no";   console.log(scope);//② }

在①處輸出的值竟然是undefined,簡直喪心病狂啊,我們已經定義了全局變量的值啊,這地方不應該為hello嗎?其實,上面的代碼等效於:

1 2 3 4 5 6 7 var scope = "hello"; function scopeTest() {   var scope;   console.log(scope);//①   scope = "no";   console.log(scope);//② }

聲明提前、全局變量優先級低於局部變量,根據這兩條規則就不難理解為什麼輸出undefined了。

作用域鏈

在javascript中,每個函數都有自己的執行上下文環境,當代碼在這個環境中執行時,會創建變量對象的作用域鏈,作用域鏈是一個對象列表或對象鏈,它保證了變量對象的有序訪問。
作用域鏈的前端是當前代碼執行環境的變量對象,常被稱之為“活躍對象”,變量的查找會從第一個鏈的對象開始,如果對象中包含變量屬性,那麼就停止查找,如果沒有就會繼續向上級作用域鏈查找,直到找到全局對象中:

作用域鏈的逐級查找,也會影響到程序的性能,變量作用域鏈越長對性能影響越大,這也是我們盡量避免使用全局變量的一個主要原因。

閉包

基礎概念
作用域是理解閉包的一個前提,閉包是指在當前作用域內總是能訪問外部作用域中的變量。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 function createClosure(){   var name = "jack";   return {     setStr:function(){       name = "rose";     },     getStr:function(){       return name + ":hello";     }   } } var builder = new createClosure(); builder.setStr(); console.log(builder.getStr()); //rose:hello

上面的示例在函數中返回了兩個閉包,這兩個閉包都維持著對外部作用域的引用,因此不管在哪調用總是能夠訪問外部函數中的變量。在一個函數內部定義的函數,會將外部函數的活躍對象添加到自己的作用域鏈中,因此上面實例中通過內部函數能夠訪問外部函數的屬性,這也是javascript模擬私有變量的一種方式。

注意:由於閉包會額外的附帶函數的作用域(內部匿名函數攜帶外部函數的作用域),因此,閉包會比其它函數多占用些內存空間,過度的使用可能會導致內存占用的增加。

閉包中的變量

在使用閉包時,由於作用域鏈機制的影響,閉包只能取得內部函數的最後一個值,這引起的一個副作用就是如果內部函數在一個循環中,那麼變量的值始終為最後一個值。

1 2 3 4 5 6 7 8 //該實例不太合理,有一定延遲因素,此處主要為了說明閉包循環中存在的問題 function timeManage() {   for (var i = 0; i < 5; i++) {     setTimeout(function() {       console.log(i);     },1000)   }; }

上面的程序並沒有按照我們預期的輸入1-5的數字,而是5次全部輸出了5。再來看一個示例:

1 2 3 4 5 6 7 8 9 function createClosure(){   var result = [];   for (var i = 0; i < 5; i++) {     result[i] = function(){       return i;     }   }   return result; }

調用createClosure()[0]()返回的是5,createClosure()[4]()返回值仍然是5。通過以上兩個例子可以看出閉包在帶有循環的內部函數使用時存在的問題:因為每個函數的作用域鏈中都保存著對外部函數(timeManage、createClosure)的活躍對象,因此,他們都引用著同一變量i,當外部函數返回時,此時的i值為5,所以內部的每個函數i的值也為5。
那麼如何解決這個問題呢?我們可以通過匿名包裹器(匿名自執行函數表達式)來強制返回預期的結果:

1 2 3 4 5 6
copyright © 萬盛學電腦網 all rights reserved