這篇文章主要和大家一起聊一聊JavaScript作用域和作用域鏈,什麼是JavaScript作用域和作用域鏈,有興趣的小夥伴們可以參考一下
每種程式語言,其變數都有一定的有效範圍,超過這個範圍之後,變數就失效了,這就是變數的作用域。從數學的角度來看,就是自變數的域。
作用域是變數的可存取範圍,即作用域控制變數與函數的可見度和生命週期。在 JavaScript 中, 物件和函數同樣也是變量,變數在聲明他們的函數體以及這個函數體嵌套的任意函數體內部都是有定義的。
一、靜態作用域和動態作用域
靜態作用域
是指聲明的作用域是根據程式正文在編譯時就決定的,也稱為詞法作用域。大多數現代程式設計語言都是採用靜態作用域規則,JavaScript就是採用的這種作用域。
採用靜態作用域的語言中,基本上都是最內嵌套作用域規則:由一個聲明引進的標識符在這個聲明所在的作用域裡可見,而且在其內部嵌套的每個作用域裡也可見,除非它被嵌套於內部的另一個對同名識別碼的宣告所掩蓋。
為了找出某個給定的識別碼所引用的對象,應該在目前最內層作用域裡尋找。如果找到了一個聲明,也就可以找到該標識符所引用的物件。否則我們就到直接的外層作用域去查找,並繼續向外順序地檢查外層作用域,直到到達程式的最外嵌套層次,也就是全域物件宣告所在的作用域。如果在所有層次上都沒有找到有關聲明,那麼這個程式就有錯誤。如下:
function cha(){ var name = "xiao;" function chb() { function chc() { console.log(name); } } }
首先函數從chb()搜索有沒有name的定義,然後繼續一層一層的向上搜索,最後在cha()中搜到了name的定義,如果沒有搜到,則會報錯。
2、動態作用域
動態作用域的語言中,程式中某個變數所引用的物件是在程式執行時刻根據程式的控制流程資訊來決定的。
二、JavaScript的作用域
JavaScript中有兩種作用域,分別為全域作用域和局部作用域。
1、全域作用域(Global Scope)
在程式碼中任何位置都是有定義的。即使在html 頁面中嵌套的一段js程式碼中定義了一個全域變量,在引用的js檔案中仍能存取到該變數。這就很有可能會造成全域變數的污染。
以下三種情況的變數都會被視為全域變數
(1)最外層的函數和最外層的變數擁有全域作用域
(2)未經定義而直接賦值的變數自動被宣告為擁有全域作用域
(3)所有window物件的屬性擁有全域作用域
2、局部作用域(Local Scope)
局部作用域一般只能在固定的程式碼片段中才能訪問,如函數內部的變數(函數作用域)
var name = "xuxiaoping"; function echoName() { var firstname = "xu";//局部作用域 secondname = "xiao";//全局作用域 function echoFirstName() { console.log(first name);//xu } console.log(secondname); return echoFirstName; } console.log(name);//全局作用域 var f = echoName(); f(); console.log(firstname); console.log(secondname);
結果為:
xuxiaoping
xiao
xu//內層函數可以存取外層函數的變數
undenfined //在函數外部無法存取函數的內部變數
xiao
JavaScript將全域變數附加到了window物件上,成為了window物件的屬性。
3、函數作用域
區塊級作用域:任何一對花括號中的語句集都屬於一個區塊,在這之中定義的所有變數在程式碼區塊外都是不可見的。大多數類別C語言都是有區塊級作用域的。
然而JavaScript的有個重要的特點就是沒有區塊級作用域。
function echoi() { for(var i = 0;i<10;i++){ ;//console.log(i); } if(true){ var str = "hello"; } console.log(i); console.log(str); } echoi();
輸出結果為:
10
hello
可見,在for語句外(也可以是if,while),區塊中定義的變數i仍然是可以訪問的。也就是說,JavaScript並不支援區塊級作用域,它只支援函數作用域,而且在一個函數中的任何位置定義的變數在該函數中的任何地方都是可見的。作為一個一開始程式設計就學習學習C和java的人來說,這有點難以適應。據我測試PHP也是這樣的。
當然可以利用JavaScript的閉包的特性,模擬個區塊級作用域
function echoi() { (function() { for(var i = 0;i<10;i++){ //console.log(i); } })(); if(true){ var str = "hello"; } console.log(i); console.log(str); } echoi();
结果为:i undefined
这样就隔离了变量的定义。在js中,为了防止命名冲突,应该尽量避免使用全局变量和全局函数,因此这种闭包的用的特别的多。
4、JavaScript 变量生命周期
JavaScript 变量生命周期在它声明时初始化。
局部变量在函数执行完毕后销毁。
全局变量在页面关闭后销毁。
三、JavaScript的作用域链
一看是链,大概就可以跟数据结构中的链表相结合起来
在JavaScript中,函数也是对象,实际上,JavaScript里一切都是对象。函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。
当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象填充。例如定义下面这样一个函数:
function add(num1,num2) { var sum = num1 + num2; return sum; }
在函数add创建时,它的作用域链中会填入一个全局对象,该全局对象包含了所有全局变量,如下图所示(注意:图片只例举了全部变量中的一部分):
函数add的作用域将会在执行时用到。例如执行如下代码:
var total = add(5,10);
执行此函数时会创建一个称为“运行期上下文(execution context)”的内部对象,运行期上下文定义了函数执行时的环境。每个运行期上下文都有自己的作用域链,用于标识符解析,当运行期上下文被创建时,而它的作用域链初始化为当前运行函数的[[Scope]]所包含的对象。
这些值按照它们出现在函数中的顺序被复制到运行期上下文的作用域链中。它们共同组成了一个新的对象,叫“活动对象(activation object)”,该对象包含了函数的所有局部变量、命名参数、参数集合以及this,然后此对象会被推入作用域链的前端,当运行期上下文被销毁,活动对象也随之销毁。新的作用域链如下图所示:
在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,也就是从活动对象开始搜索,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没找到继续搜索作用域链中的下一个对象,如果搜索完所有对象都未找到,则认为该标识符未定义。函数执行过程中,每个标识符都要经历这样的搜索过程。
四、作用域链和代码优化
从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢。如上图所示,因为全局变量总是存在于运行期上下文作用域链的最末端,因此在标识符解析的时候,查找全局变量是最慢的。所以,在编写代码的时候应尽量少使用全局变量,尽可能使用局部变量。一个好的经验法则是:如果一个跨作用域的对象被引用了一次以上,则先把它存储到局部变量里再使用。例如下面的代码:
function changeColor(){ document.getElementById("btnChange").onclick=function() { document.getElementById("targetCanvas").style.backgroundColor="red"; }; }
这个函数引用了两次全局变量document,查找该变量必须遍历整个作用域链,直到最后在全局对象中才能找到。这段代码可以重写如下:
function changeColor(){ var doc=document; doc.getElementById("btnChange").onclick=function(){ doc.getElementById("targetCanvas").style.backgroundColor="red"; }; }
这段代码比较简单,重写后不会显示出巨大的性能提升,但是如果程序中有大量的全局变量被从反复访问,那么重写后的代码性能会有显著改善。
五、with改变作用域链
数每次执行时对应的运行期上下文都是独一无二的,所以多次调用同一个函数就会导致创建多个运行期上下文,当函数执行完毕,执行上下文会被销毁。每一个运行期上下文都和一个作用域链关联。一般情况下,在运行期上下文运行的过程中,其作用域链只会被 with 语句和 catch 语句影响。
with语句是对象的快捷应用方式,用来避免书写重复代码。例如:
function initUI(){ with(document){ var bd=body, links=getElementsByTagName("a"), i=0, len=links.length; while(i < len){ update(links[i++]); } getElementById("btnInit").onclick=function(){ doSomething(); }; } }
這裡使用width語句來避免多次書寫document,看起來更有效率,實際上產生了效能問題。
當程式碼運行到with語句時,運行期上下文的作用域鏈暫時改變了。一個新的可變物件被創建,它包含了參數指定的物件的所有屬性。這個物件將被推入作用域鏈的頭部,這意味著函數的所有局部變數現在處於第二個作用域鏈物件中,因此存取代價更高了。如下圖所示:
因此在程式中應避免使用with語句,在這個例子中,只要簡單的把document儲存在一個局部變數中就可以提升效能。
總結
1.變數的作用域就是變數在哪些範圍有效。
2、變數的作用域鏈就是被創建的作用域中物件的集合。
以上就是本文的全部內容,希望對大家學習javascript程式設計有所幫助。