首頁 >web前端 >js教程 >javascript作用域底層作用分析

javascript作用域底層作用分析

php中世界最好的语言
php中世界最好的语言原創
2018-05-24 15:18:161409瀏覽

這次帶給大家javascript作用域底層作用分析,javascript作用域底層使用的注意事項有哪些,下面就是實戰案例,一起來看一下。

標籤: javascript


什麼是作用域(Scope)?

作用域產生於程式原始碼中定義變數的區域,在程式編碼階段就確定了。 javascript 中分為全域作用域(Global context: window/global )和局部作用域(Local Scope ,  又稱為函數作用域 Function context) 。簡單講作用域就是目前函數的產生環境上下文(注意:暫且不要與後面講的執行上下文混淆了),包含了目前函數內定義的變數以及對外層作用域的引用

javascript 中的作用域:

##函數作用域##Block Scope區塊作用域(ES6)#eval Scopeeval作用域

作用域定義了一套規則,這套規則定義了引擎如何在目前作用域以及巢狀作用域根據識別碼來查詢變數。反過來說N個作用域組成的作用域鏈決定了函數作用域內標識符查找到的值。

所以我們可以總結為:作用域(Scope)確定了當前上下文內定義的變數的可見性,即更下一層作用域可以存取。並且作用域鏈(Scope Chain)也確定了在當前上下文中如何查找標識符的值。

javascript作用域底層作用分析

Scope分為Lexical Scope和Dynamic Sc​​ope。 Lexical Scope如同字面意思,即詞法階段所定義的Scope。換種說法,作用域是根據原始程式碼中變數和區塊的位置,在詞法分析器(lexer)處理原始程式碼時設定。 javascript 採用的就是詞法作用域。

變數的存取規則:

  • 如果變數a 在函數內部定義,則函數內部其他變數具有存取變數a 的權限,但是函數外部程式碼沒有存取變數a 的權限。所以同一作用域內變數可以互相訪問,即 a、b、c 在同一個作用域他們就可以互相訪問。這就像雞媽媽有寶寶,雞寶寶可以互相打鬧,其他雞就不能跟他們打鬧了,為什麼?因為雞媽媽不容許~ o(^∀^)o 。

let a = 1
function foo () {
    let b = 1 + a
    let c = 2
    console.log(b) // 2
}
console.log(c) // error 全局作用无法访问到 c
foo()
  • 如果變數a 在全域作用域下定義(window/global),則全域作用域下的局部作用域內的執行程式碼或是說表達式都可以存取到變數a 的值。局部變數裡的同名變數(a)會截斷對全域變數 a 的存取。 (這裡的變數a 就相當於飼養員,候飼養員會在適當的時候給雞兒們投食。但是農場主為了節約成本,規定飼養員要就近給雞投食,當飼養員1離雞寶寶更近時其他飼養員就不能千里迢迢跨過鴨綠江去餵雞了。中程式碼和區塊的位置,被嵌套作用域擁有對嵌套作用域的存取權。 (這條規則說明整個農場是有規則的,不能反向的投食。)

  • 作用域鏈Scope Chain

作用域鏈,是由當前環境與上層環境的一系列作用域共同組成,它保證了目前執行環境對符合存取權限的變數和函數的有序存取。

上面解釋的稍微有些晦澀,對於我這樣大腦不好使的就需要在大腦裡重複的'讀'幾次才能明白。那麼作用域鍊是乾嘛的呢?簡單的說作用域鏈就是解析標識符的,負責返回表達式執行時所依賴變數的值。再簡單點回答:作用域鏈就是用來找出變數的,作用域鍊是由一系列作用域串連起來的。 在函數執行過程中,每遇到一個變量,都會經歷一次標識符解析過程以決定從哪裡獲取和儲存資料。這個過程從作用域鏈頭部,也就是目前執行函數的作用域開始(下圖中從左向右),查找同名的標識符,如果找到了就回傳這個標識符對應的值,如果沒找到繼續搜尋作用域鏈中的下一個作用域,如果搜尋完所有作用域都未找到,則認為該識別碼未定義。函數執行過程中,每個標識符值得解析都要經歷這樣的搜尋過程。

為了具象化分析問題,我們可以假設作用域鍊是一個陣列(Scope Array),陣列成員有一系列變數物件組成。我們可以在數組這個單向通道中,也就是上圖模擬從左向右查詢變數物件中的標識符,這樣就可以存取到上一層作用域中的變數了。直到最頂層(全域作用域),一旦找到,即停止查找。所以內層的變數可以屏蔽外層的同名變數。想想如果變數不是按從內向外的查找,那整個語言設計會變得N複雜了(我們需要設計一套複雜的雞寶寶找食物的規則)

javascript作用域底層作用分析#還是上面的栗子:

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项目开发中使用实战总结

作用域(Scope) -
window/global Scope 全域作用域
#function Scope

以上是javascript作用域底層作用分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn