溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Javascript作用域和作用域鏈原理解析

發(fā)布時間:2020-10-24 03:23:05 來源:腳本之家 閱讀:144 作者:秦至 欄目:web開發(fā)

作用域和作用域鏈在Javascript和很多其它的編程語言中都是一種基礎概念。但很多Javascript開發(fā)者并不真正理解它們,但這些概念對掌握Javascript至關重要。

正確的去理解這個概念有利于你去寫更好,更高效和更簡潔的代碼,讓你成為一個更優(yōu)秀的Javascript開發(fā)者。

因此,在本文中,我將會向大家解釋清楚什么是作用域和作用域鏈,以及Javascript引擎在內部是如何通過它們操作和查找變量的。

事不宜遲,正文開始 :)

什么是作用域

Javascript中的作用域說的是變量的可訪問性和可見性。也就是說整個程序中哪些部分可以訪問這個變量,或者說這個變量都在哪些地方可見。

為什么作用域很重要

作用域最為重要的一點是安全。變量只能在特定的區(qū)域內才能被訪問,有了作用域我們就可以避免在程序其它位置意外對某個變量做出修改。

作用域也會減輕命名的壓力。我們可以在不同的作用域下面定義相同的變量名。

作用域的類型

Javascript中有三種作用域:

  • 全局作用域;
  • 函數(shù)作用域;
  • 塊級作用域;

1. 全局作用域

任何不在函數(shù)中或是大括號中聲明的變量,都是在全局作用域下,全局作用域下聲明的變量可以在程序的任意位置訪問。例如:

// 全局變量
var greeting = 'Hello World!';
function greet() {
 console.log(greeting);
}
// 打印 'Hello World!'
greet();

2. 函數(shù)作用域

函數(shù)作用域也叫局部作用域,如果一個變量是在函數(shù)內部聲明的它就在一個函數(shù)作用域下面。這些變量只能在函數(shù)內部訪問,不能在函數(shù)以外去訪問。例如:

function greet() {
 var greeting = 'Hello World!';
 console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 報錯: Uncaught ReferenceError: greeting is not defined
console.log(greeting);

3. 塊級作用域

ES6引入了let和const關鍵字,和var關鍵字不同,在大括號中使用let和const聲明的變量存在于塊級作用域中。在大括號之外不能訪問這些變量。看例子:

{
 // 塊級作用域中的變量
 let greeting = 'Hello World!';
 var lang = 'English';
 console.log(greeting); // Prints 'Hello World!'
}
// 變量 'English'
console.log(lang);
// 報錯:Uncaught ReferenceError: greeting is not defined
console.log(greeting);

上面代碼中可以看出,在大括號內使用var聲明的變量lang是可以在大括號之外訪問的。使用var聲明的變量不存在塊級作用域中。

作用域嵌套

像Javascript中函數(shù)可以在一個函數(shù)內部聲明另一個函數(shù)一樣,作用域也可以嵌套在另一個作用域中。請看例子:

var name = 'Peter';
function greet() {
 var greeting = 'Hello';
 {
  let lang = 'English';
  console.log(`${lang}: ${greeting} ${name}`);
 }
}
greet();

這里我們有三層作用域嵌套,首先第一層是一個塊級作用域(let聲明的),被嵌套在一個函數(shù)作用域(greet函數(shù))中,最外層作用域是全局作用域。

詞法作用域

詞法作用域(也叫靜態(tài)作用域)從字面意義上看是說作用域在詞法化階段(通常是編譯階段)確定而非執(zhí)行階段確定的??蠢樱?/p>

let number = 42;
function printNumber() {
 console.log(number);
}
function log() {
 let number = 54;
 printNumber();
}
// Prints 42
log();

上面代碼可以看出無論printNumber()在哪里調用console.log(number)都會打印42。動態(tài)作用域不同,console.log(number)這行代碼打印什么取決于函數(shù)printNumber()在哪里調用。

如果是動態(tài)作用域,上面console.log(number)這行代碼就會打印54。

使用詞法作用域,我們可以僅僅看源代碼就可以確定一個變量的作用范圍,但如果是動態(tài)作用域,代碼執(zhí)行之前我們沒法確定變量的作用范圍。

像C,C++,Java,Javascript等大多數(shù)編程語言都支持靜態(tài)作用域。Perl 既支持動態(tài)作用域也支持靜態(tài)作用域。

作用域鏈

當在Javascript中使用一個變量的時候,首先Javascript引擎會嘗試在當前作用域下去尋找該變量,如果沒找到,再到它的上層作用域尋找,以此類推直到找到該變量或是已經到了全局作用域。

如果在全局作用域里仍然找不到該變量,它就會在全局范圍內隱式聲明該變量(非嚴格模式下)或是直接報錯。

例如:

let foo = 'foo';
function bar() {
 let baz = 'baz';
 // 打印 'baz'
 console.log(baz);
 // 打印 'foo'
 console.log(foo);
 number = 42;
 console.log(number); // 打印 42
}
bar();

當函數(shù)bar()被調用,Javascript引擎首先在當前作用域下尋找變量baz,然后尋找foo變量但發(fā)現(xiàn)在當前作用域下找不到,然后繼續(xù)在外部作用域尋找找到了它(這里是在全局作用域找到的)。

然后將42賦值給變量number。Javascript引擎會在當前作用域以及外部作用域下一步步尋找number變量(沒找到)。

如果是在非嚴格模式下,引擎會創(chuàng)建一個number的全局變量并把42賦值給它。但如果是嚴格模式下就會報錯了。

結論:當使用一個變量的時候,Javascript引擎會循著作用域鏈一層一層往上找該變量,直到找到該變量為止。

作用域和作用域鏈是如何工作的

以上內容已經講解了作用域,作用域的類型,現(xiàn)在讓我們看下Javascript引擎是如何確定變量的作用域鏈和如何去查找變量的。

要想理解Javascript是如何進行變量查找的,必須要了解Javascript中詞法環(huán)境這個概念(請參考:理解Javascript中的執(zhí)行上下文和執(zhí)行棧)。

什么是詞法環(huán)境

所謂詞法環(huán)境就是一種標識符—變量映射的結構(這里的標識符指的是變量/函數(shù)的名字,變量是對實際對象[包含函數(shù)和數(shù)組類型的對象]或基礎數(shù)據類型的引用)。

簡單地說,詞法環(huán)境是Javascript引擎用來存儲變量和對象引用的地方。

注意:不要混淆了詞法環(huán)境和詞法作用域,詞法作用域是在代碼編譯階段確定的作用域(譯者注:一個抽象的概念),而詞法環(huán)境是Javascript引擎用來存儲變量和對象引用的地方(譯者注:一個具象的概念)。

一個詞法環(huán)境就像下面這樣:

lexicalEnvironment = {
 a: 25,
 obj: <ref. to the object>
}

只有當該作用域的代碼被執(zhí)行的時候,引擎才會為那個作用域創(chuàng)建一個新的詞法環(huán)境。詞法環(huán)境還會記錄所引用的外部詞法環(huán)境(即外部作用域)。例:

lexicalEnvironment = {
 a: 25,
 obj: <ref. to the object>
 outer: <outer lexical environemt>
}

Javascript引擎是如何進行變量查找的

現(xiàn)在我們已經知道了作用域,作用域鏈和詞法環(huán)境的概念,現(xiàn)在讓我們看下Javascript引擎是如何利用詞法環(huán)境來確定作用域和作用域鏈的。

結合例子我們來理解上面的這些概念:

let greeting = 'Hello';
function greet() {
 let name = 'Peter';
 console.log(`${greeting} ${name}`); // Hello Peter
}
greet();
{
 let greeting = 'Hello World!'
 console.log(greeting); // Hello World!
}

上述代碼加載后,首先會創(chuàng)建一個全局詞法環(huán)境,其中包含在全局范圍內聲明的變量和函數(shù)。像下面這樣:

globalLexicalEnvironment = {
 greeting: 'Hello'
 greet: <ref. to greet function>
 outer: <null>
}

這里的outer字段(也就是外部詞法環(huán)境)被設置為了null,是因為全局詞法環(huán)境已經是最頂層的詞法環(huán)境了。

然后,我們調用了greet()函數(shù),然后一個新的詞法環(huán)境會被被創(chuàng)建:

functionLexicalEnvironment = {
 name: 'Peter'
 outer: <globalLexicalEnvironment>
}

這里的outer字段被設置為了globalLexicalEnvironment,是因為他的外部作用域就是全局作用域。

然后,執(zhí)行console.log(`${greeting} ${name}`)這行代碼,Javascript引擎首先在當前函數(shù)的詞法環(huán)境中尋找變量greeting和name,但只找到了name,沒找到greeting。然后繼續(xù)在上層的詞法環(huán)境中找greeting(這里是全局作詞法環(huán)境)。最后在全局詞法環(huán)境中找到了greeting。

緊接著執(zhí)行那段在大括號里的代碼,為這個塊級創(chuàng)建一個新的詞法環(huán)境。如下:

blockLexicalEnvironment = {
 greeting: 'Hello World',
 outer: <globalLexicalEnvironment>
}

然后執(zhí)行console.log(greeting)這行代碼,首先在本層詞法環(huán)境中找greeting,OK,找到,結束。此時就不會再去外部作用域(這里是全局作用域)尋找該變量了。

注意:只有l(wèi)et和const聲明變量才會創(chuàng)建一個新的詞法環(huán)境存儲,使用var聲明的變量會被存儲在當前塊(大括號)所在的詞法環(huán)境中(全局詞法環(huán)境或是函數(shù)詞法環(huán)境中)。

結論:當一個變量被使用時,Javascript引擎會首先在當前的詞法環(huán)境中進行尋找,如果找不到就找上層詞法環(huán)境中尋找,直到找到為止。

結論

作用域就是一個變量可訪問和可見的區(qū)域,和函數(shù)一樣,Javascript的作用域也可以嵌套,Javascript引擎會層層遍歷作用域來尋找用到的變量。

Javascript使用詞法作用域,這意味著變量的作用在編譯階段就會被確定。

Javascript引擎在程序執(zhí)行期間使用詞法環(huán)境來存儲變量和函數(shù)。

作用域和作用域鏈是Javascript中的基礎概念,理解作用域和作用域鏈能讓你成為一個更優(yōu)秀的Javascript開發(fā)者。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI