首頁 >web前端 >js教程 >深入解析JavaScript中的作用域

深入解析JavaScript中的作用域

青灯夜游
青灯夜游轉載
2021-05-31 17:28:372154瀏覽

本篇文章帶大家深入理解JavaScript作用域。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有幫助。

深入解析JavaScript中的作用域

這篇文章稱為筆記更為合適一些,內容來自《你不知道的JavaScript(上卷)》第一部分作用域和閉包。講的很不錯,非常值得一看。

什麼是作用域

作用域是根據名稱找出變數的一套規則

#理解作用域

先來理解一些基礎概念:

  • 引擎:從頭到尾負責整個JavaScript程式的編譯及執行過程。
  • 編譯器:負責語法分析和程式碼產生。這部分也可以看JavaScript程式碼是如何被執行的
  • 作用域:負責收集並維護由所有宣告的識別碼(變數)組成的一系列查詢,並實作一套非常嚴格的規則,確定目前執行的程式碼對這些識別碼的存取權限。

接下來來看看下面程式碼的執行程序:

var a = 2;
  • #遇見var a,編譯器 會問作用域 變數a是否存在於同一個作用域集合中。如果存在,編譯器會忽略聲明,繼續編譯;否則,會要求作用域在目前作用域集合中宣告一個新的變量,並命名為a

  • 接下來編譯器 會為引擎 產生執行階段所需的程式碼,用來處理a = 2 這個賦值運算。引擎運作時會先問作用域,目前作用域集中是否存在變數a。如果是,引擎就會使用該變數;如果不存在,引擎會繼續尋找該變數

  • 如果引擎 找到了a 變量,就會將2 賦值給它,否則引擎就拋出一個錯誤。

總結:變數的賦值操作會執行兩個動作,首先編譯器會在目前作用域中宣告一個變量,然後在執行時間引擎就會作用域中尋找該變量,如果能夠找到就對它賦值。

編譯器在編譯過程的第二步中產生了程式碼,當引擎執行它時,會透過尋找變數 a來判斷它是否已宣告過。查找的過程中由作用域協助,但是引擎執行怎麼樣的查找,會影響最終的查找結果。

在我們的例子中,引擎會為變數 a 進行 LHS 查詢,另外一個尋找的類型叫做 RHS。 ”L“ 和 "R" 分別代表一個賦值操作左側和右側。當變數出現在賦值操作的左側時進行 LHS 查詢,出現在右側時進行 RHS 查詢。

LHS:試圖找出變數的容器本身,從而可以對其賦值;RHS: 就是簡單地找出某個變數的值。
console.log(a);

對a 的引用是一個RHS 引用,因為這裡a 並沒有賦予任務值,相應地需要查找並取得a 的值,這樣才能將值傳遞給console.log(...)

a = 2;

這裡對a 的引用是LHS 引用,因為實際上我們並不關心目前的值是什麼,只是想要為= 2這個賦值運算找到目標。

funciton foo(a) {
    console.log(a)
}

foo(2);
  1. 最後一行foo 函數的呼叫需要對foo 進行RHS 引用,去找foo的值,並把它給我
  2. 程式碼中隱式的a = 2 運算可能很容易被你忽略掉,這操作發生在2 被當做參數傳遞給foo 函數時,2 會被指派給參數a,為了給參數a (隱含地) 分配值,需要進行一次LHS 查詢。
  3. 這裡還有對 a 進行的 RHS 引用,並且將得到的值傳給了 console.log(...)console.log(...) 本身也需要一個參考才能執行,因此會對console物件進行RHS 查詢,並且檢查得到的值中是否有一個叫做 log的方法。

RHS查詢在所有巢狀的作用域中遍尋不到所需的變量,引擎就會拋出 ReferenceError 例外。進行RHS查詢找到了一個變量,但是你嘗試對這個變量的值進行不合理的操作,例如試圖對一個非函數類型的值進行調用,後者引用null或undefined 類型的值中的屬性,那麼引擎會拋出一個另外一種類型的異常TypeError。
引擎執行 LHS 查詢時如果找不到該變量,則會在全域作用域中建立一個。但是在嚴格模式下,並不是自動創建一個全域變量,而是會拋出 ReferenceError 異常

#補充JS幾種常見的錯誤類型

簡單總結如下:

作用域是一套规则,用于确定在哪里找,怎么找到某个变量。如果查找的目的是对变量进行赋值,那么就会使用 LHS查询; 如果目的是获取变量的值,就会使用 RHS 查询;
JavaScript 引擎执行代码前会对其进行编译,这个过程中,像 var a = 2 这样的声明会被分解成两个独立的步骤

  • var a 在其作用域中声明变量,这会在最开始的阶段,也就是代码执行前进行

  • 接下来,a = 2 会查询 (LHS查询)变量 a 并对其进行赋值。

词法作用域

词法作用域是你在写代码时将变量写在哪里来决定的。编译的词法分析阶段基本能够知道全局标识符在哪里以及是如何声明的,从而能够预测在执行过程中如果对他们查找。

有一些方法可以欺骗词法作用域,比如 eval, with, 这两种现在被禁止使用,1是严格模式和非严格模式下表现不同 2是有性能问题, JavaScript引擎在编译阶段会做很多性能优化,而其中很多优化手段都依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到识别符,eval, with会改变作用域,所以碰到它们,引擎将无法做优化处理。

全局作用域和函数作用域

全局作用域

  • 在最外层函数和最外层函数外面定义的变量拥有全局作用域
var a = 1;
function foo() {

}

变量a 和函数声明 foo 都是在全局作用域中的。

  • 所有未定义直接赋值的变量自动声明为拥有全局作用域

var a = 1;
function foo() {
    b = 2;
}
foo();
console.log(b); // 2
  • 所有 window 对象的属性拥有全局作用域

函数作用域

函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。外部作用域无法访问函数内部的任何内容。

function foo() {
    var a = 1;
    console.log(a); // 1
}
foo();
console.log(a); // ReferenceError: a is not defined
只有函数的{}构成作用域,对象的{}以及 if(){}都不构成作用域;

变量提升

提升是指声明会被视为存在与其所出现的作用域的整个范围内。

JavaScript编译阶段是找到找到所有声明,并用合适的作用域将他们关联起来(词法作用域核心内容),所以就是包含变量和函数在内的所有声明都会在任何代码被执行前首先被处理。

每个作用域都会进行提升操作。

function foo() {
    var a;
    console.log(a); // undefined
    a = 2;
}
foo();
注意,函数声明会被提升,但是函数表达式不会被提升。

关于 块级作用域和变量提升的内容之前在 从JS底层理解var、let、const这边文章中详细介绍过,这里不再赘述。

块级作用域

我们来看下面这段代码

for(var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    })
}
console.log(`当前的i为${i}`); // 当前的i为5

上面这段代码我们希望是输出 0,1, 2, 3, 4 ,但是实际上输出的是 5,5, 5, 5, 5。我们在 for 循环的头部直接定义了变量 i,通常是因为只想在 for 循环内部的上下文中使用 i,但是实际上 此时的 i 被绑定在外部作用域(函数或全局)中。

,块级作用域是指在指定的块级作用域外无法访问。在ES6之前是没有块级作用域的概念的,ES6引入了 let 和 const。我们可以改写上面的代码,使它按照我们想要的方式运行。

for(let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    })
}
// 0 1 2 3 4
console.log(`当前的i为${i}`); // ReferenceError: i is not defined

此时 for 循环头部的 let 不仅将 i 绑定到了 for 循环的迭代中,事实上将它重新绑定到了循环的每一个迭代中,确保使用上一次循环迭代结束的值重新进行赋值。

let声明附属于一个新的作用域而不是当前的函数作用域(也不属于全局作用域)。但是其行为是一样的,可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域。
const也是可以用来创建块级作用域变量,但是创建的是固定值。

作用域链

JavaScript是基于词法作用域的语言,通过变量定义的位置就能知道变量的作用域。全局变量在程序中始终都有都定义的。局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的。

每一段 JavaScript 代码都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或者链表。当 JavaScript 需要查找变量 x 的时候(这个过程称为变量解析),它会从链中的第一个变量开始查找,如果这个对象上依然没有一个名为 x 的属性,则会继续查找链上的下一个对象,如果第二个对象依然没有名为 x 的属性,javaScript会继续查找下一个对象,以此类推。如果作用域链上没有任何一个对象包含属性 x, 那么就认为这段代码的作用域链上不存在 x, 并最终抛出一个引用错误 (Reference Error) 异常。

下面作用域中有三个嵌套的作用域。

function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log(a, b, c)
    }
    bar( b * 3);
}
foo(2);

<img src="https://img.php.cn/upload/image/252/331/635/1622453204885970.png" title="1622453204885970.png" alt="深入解析JavaScript中的作用域">

气泡1包含着整个全局作用域,其中只有一个标识符:foo;
气泡2包含着foo所创建的作用域,其中有三个标识符:a、bar 和 b;
气泡3包含着 bar所创建的作用域,其中只有一个标识符:c

执行 console.log(...),并查找 a,b,c三个变量的引用。下面我们来看看查找这几个变量的过程.
它首先从最内部的作用域,也就是 bar(..) 函数的作用域气泡开始找,引擎在这里无法找到 a,因此就会去上一级到所嵌套的 foo(...)的作用域中继续查找。在这里找到了a,因此就使用了这个引用。对b来说也一样,而对 c 来说,引擎在 bar(..) 中就找到了它。

如果 a,c都存在于 bar(...) 内部,console.log(...)就可以直接使用 bar(...) 中的变量,而无需到外面的 foo(..)中查找。作用域会在查找都第一个匹配的标识符时就停止。

在多层的嵌套作用域中可以定义同名的标识符,这叫”遮蔽效应“。

var a = &#39;外部的a&#39;;
function foo() {
    var a = &#39;foo内部的a&#39;;
    console.log(a); // foo内部的a
}
foo();

作用域与执行上下文

JavaScript的执行分为:解释和执行两个阶段

解释阶段

  • 词法分析
  • 语法分析
  • 作用域规则确定

执行阶段

  • 创建执行上下文
  • 执行函数代码
  • 垃圾回收

作用域在函数定义时就已经确定了,而不是在函数调用时确定,但执行上下文是函数执行之前创建的。

总结

  • 作用域就是一套规则,用于确定在哪里找以及怎么找到某个变量。

  • 词法作用域在你写代码的时候就确定了。JavaScript是基于词法作用域的语言,通过变量定义的位置就能知道变量的作用域。ES6引入的let和const声明的变量在块级作用域中。

  • 声明提升是指声明会被视为存在与其所出现的作用域的整个范围内。

  • 查找变量的时候会先从内部的作用域开始查找,如果没找到,就往上一级进行查找,依次类推。

  • 作用域在函数定义时就已经确定了,执行上下文是函数执行之前创建的。

更多编程相关知识,请访问:编程视频!!

以上是深入解析JavaScript中的作用域的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除