作用域(Scope) |
- |
window/global Scope |
全域作用域 |
#function Scope | ##函數作用域 |
##Block Scope | 區塊作用域(ES6) |
#eval Scope | eval作用域 |
作用域定義了一套規則,這套規則定義了引擎如何在目前作用域以及巢狀作用域根據識別碼來查詢變數。反過來說N個作用域組成的作用域鏈決定了函數作用域內標識符查找到的值。
所以我們可以總結為:作用域(Scope)確定了當前上下文內定義的變數的可見性,即更下一層作用域可以存取。並且作用域鏈(Scope Chain)也確定了在當前上下文中如何查找標識符的值。
Scope分為Lexical Scope和Dynamic Scope。 Lexical Scope如同字面意思,即詞法階段所定義的Scope。換種說法,作用域是根據原始程式碼中變數和區塊的位置,在詞法分析器(lexer)處理原始程式碼時設定。 javascript 採用的就是詞法作用域。
變數的存取規則:
let a = 1
function foo () {
let b = 1 + a
let c = 2
console.log(b) // 2
}
console.log(c) // error 全局作用无法访问到 c
foo()
作用域鏈,是由當前環境與上層環境的一系列作用域共同組成,它保證了目前執行環境對符合存取權限的變數和函數的有序存取。
上面解釋的稍微有些晦澀,對於我這樣大腦不好使的就需要在大腦裡重複的'讀'幾次才能明白。那麼作用域鍊是乾嘛的呢?簡單的說作用域鏈就是解析標識符的,負責返回表達式執行時所依賴變數的值。再簡單點回答:作用域鏈就是用來找出變數的,作用域鍊是由一系列作用域串連起來的。 在函數執行過程中,每遇到一個變量,都會經歷一次標識符解析過程以決定從哪裡獲取和儲存資料。這個過程從作用域鏈頭部,也就是目前執行函數的作用域開始(下圖中從左向右),查找同名的標識符,如果找到了就回傳這個標識符對應的值,如果沒找到繼續搜尋作用域鏈中的下一個作用域,如果搜尋完所有作用域都未找到,則認為該識別碼未定義。函數執行過程中,每個標識符值得解析都要經歷這樣的搜尋過程。
為了具象化分析問題,我們可以假設作用域鍊是一個陣列(Scope Array),陣列成員有一系列變數物件組成。我們可以在數組這個單向通道中,也就是上圖模擬從左向右查詢變數物件中的標識符,這樣就可以存取到上一層作用域中的變數了。直到最頂層(全域作用域),一旦找到,即停止查找。所以內層的變數可以屏蔽外層的同名變數。想想如果變數不是按從內向外的查找,那整個語言設計會變得N複雜了(我們需要設計一套複雜的雞寶寶找食物的規則)#還是上面的栗子:
let a = 1
let b = 2
function foo () {
let b = 3
function too () {
console.log(a) // 1
console.log(b) // 3
}
too()
}
foo()
作用域巢狀結構是這樣的:
栗子中,当 javascript 引擎执行到函数 too 时, 全局、函数 foo、函数 too 的上下文分别会被创建。上下文内包含它们各自的变量对象和作用域链(注意: 作用域链包含可访问到的上层作用域的变量对象,在上下文创建阶段根据作用域规则被收集起来形成一个可访问链),我们设定他们的变量对象分别为VO(global),VO(foo), VO(too)。而 too 的作用域链,则同时包含了这三个变量对象,所以 too 的执行上下文可如下表示:
too = {
VO: {...}, // 变量对象
scopeChain: [VO(too), VO(foo), VO(global)], // 作用域链
}
我们可以直接用scopeChain
来表示作用域链数组,数组的第一项scopeChain[0]为作用域链的最前端(当前函数的变量对象),而数组的最后一项,为作用域链的最末端(全局变量对象 window )。所有作用域链的最末端都为全局变量对象。
再举个栗子:
let a = 1
function foo() {
console.log(a)
}
function too() {
let a = 2
foo()
}
too() // 1
这个栗子如果对作用域的特点理解不透彻很容易以为输出是2。但其实最终输出的是 1。 foo() 在执行的时候先在当前作用域内查找变量 a 。然后根据函数定义时的作用域关系会在当前作用域的上层作用域里查找变量标识符 a,所以最后查到的是全局作用域的 a 而不是 foo函数里面的 a 。
变量对象、执行上下文会在后面介绍。
闭包
在 JavaScript 中,函数和函数声明时的词法作用域形成闭包。我们来看个闭包的例子
let a = 1
function foo() {
let a = 2
function too() {
console.log(a)
}
return too
}
foo()() // 2
这是一个闭包的栗子,一个函数执行后返回另一个可执行函数,被返回的函数保留有对它定义时外层函数作用域的访问权。foo()()
调用时依次执行了 foo、too 函数。too 虽然是在全局作用域里执行的,但是too定义在 foo 作用域里面,根据作用域链规则取最近的嵌套作用域的属性 a = 2。
再拿农场的故事做比如。农场主发现还有一种方法会更节约成本,就是让每个鸡妈妈作为家庭成员的‘饲养员’, 从而改变了之前的‘饲养结构’。
关于闭包会在后面的章节里也会有介绍。
从作用域链的结构可以发现,javascript
引擎在查找变量标识符时是依据作用域链依次向上查找的。当标识符所在的作用域位于作用域链的更深的位置,读写的时候相对就慢一些。所以在编写代码的时候应尽量少使用全局代码,尽可能的将全局的变量缓存在局部作用域中。
不加强记忆很容记错作用域与后面将要介绍的执行上下文的区别。代码的执行过程分为编译阶段和解释执行阶段。始终应该记住javascript
作用域在源代码的编码阶段就确定了,而作用域链是在编译阶段被收集到执行上下文的变量对象里的。所以作用域、作用域链都是在当前运行环境内代码执行前就确定了。这里暂且不过多的展开执行上下文的概念,可以关注后续文章。
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
PromiseA+的实现步骤详解
EasyCanvas绘图库在Pixeler项目开发中使用实战总结