一尘不染

JavaScript 中变量的作用域是什么?

javascript

javascript中变量的范围是什么?它们在函数内部和外部具有相同的范围吗?或者它甚至重要吗?另外,如果变量是全局定义的,它们存储在哪里?


阅读 165

收藏
2022-02-11

共1个答案

一尘不染

TLDR

JavaScript 具有词法(也称为静态)作用域和闭包。这意味着您可以通过查看源代码来判断标识符的范围。

四个范围是:

  1. Global - visible by everything
  2. Function - visible within a function (and its sub-functions and blocks)
  3. Block - visible within a block (and its sub-blocks)
  4. Module - visible within a module

var在全局和模块范围的特殊情况之外,使用(函数范围)、let(块范围)和(块范围)声明变量const。大多数其他形式的标识符声明在严格模式下具有块范围。

概述

范围是标识符有效的代码库区域。

词法环境是标识符名称和与之关联的值之间的映射。

范围由词汇环境的链接嵌套构成,嵌套中的每一级对应于祖先执行上下文的词汇环境。

这些链接的词法环境形成了一个作用域“链”。标识符解析是沿着该链搜索匹配标识符的过程。

标识符解析只发生在一个方向:向外。这样,外部词汇环境就无法“看到”内部词汇环境。

决定JavaScript中标识符范围的三个相关因素:

  1. 如何声明标识符
  2. 声明标识符的位置
  3. 无论您是处于严格模式还是非严格模式

可以声明标识符的一些方法:

  1. var, let and const
  2. Function parameters
  3. Catch block parameter
  4. Function declarations
  5. Named function expressions
  6. Implicitly defined properties on the global object (i.e., missing out var in non-strict mode)
  7. import statements
  8. eval

可以声明一些位置标识符:

  1. Global context
  2. Function body
  3. Ordinary block
  4. The top of a control structure (e.g., loop, if, while, etc.)
  5. Control structure body
  6. Modules

声明样式

变量

使用声明的标识符var 具有函数范围,除了它们直接在全局上下文中声明时,在这种情况下,它们作为属性添加到全局对象上并具有全局范围。eval它们在函数中的使用有单独的规则。

让和常量

let使用和声明的标识符const 具有块范围,除了它们直接在全局上下文中声明时,在这种情况下它们具有全局范围。

注:let,constvar 都是吊装的。这意味着它们的逻辑定义位置是它们封闭范围(块或函数)的顶部。但是,在控制通过源代码中的声明点之前letconst无法读取或分配使用声明的变量。过渡时期被称为时间死区。

function f() {
    function g() {
        console.log(x)
    }
    let x = 1
    g()
}
f() // 1 because x is hoisted even though declared with `let`!

函数参数名称

函数参数名称的范围是函数体。请注意,这有点复杂。声明为默认参数的函数靠近参数列表,而不是函数体。

函数声明

函数声明在严格模式下具有块作用域,在非严格模式下具有函数作用域。注意:非严格模式是一组复杂的基于不同浏览器的古怪历史实现的紧急规则。

命名函数表达式

命名函数表达式的范围仅限于自身(例如,出于递归的目的)。

全局对象上隐式定义的属性

在非严格模式下,全局对象上隐式定义的属性具有全局范围,因为全局对象位于范围链的顶部。在严格模式下,这些是不允许的。

评估

eval字符串中,使用声明的变量var将被放置在当前范围内,或者,如果eval间接使用,则作为全局对象的属性。

例子

以下将引发 ReferenceError,因为名称x、、yz在函数之外没有任何意义f

function f() {
    var x = 1
    let y = 1
    const z = 1
}
console.log(typeof x) // undefined (because var has function scope!)
console.log(typeof y) // undefined (because the body of the function is a block)
console.log(typeof z) // undefined (because the body of the function is a block)

以下将为yand引发 ReferenceError z,但不会为x,因为 的可见性x不受块的约束。定义控制结构体(如iffor和)的块的while行为类似。

{
    var x = 1
    let y = 1
    const z = 1
}
console.log(x) // 1
console.log(typeof y) // undefined because `y` has block scope
console.log(typeof z) // undefined because `z` has block scope

在下面,由于具有函数范围x,因此在循环外部可见:var

for(var x = 0; x < 5; ++x) {}
console.log(x) // 5 (note this is outside the loop!)

…由于这种行为,您需要小心关闭var在循环中使用声明的变量。这里只x声明了一个变量实例,它在逻辑上位于循环之外。

以下打印5五次,然后为循环外打印5第六次:console.log

for(var x = 0; x < 5; ++x) {
    setTimeout(() => console.log(x)) // closes over the `x` which is logically positioned at the top of the enclosing scope, above the loop
}
console.log(x) // note: visible outside the loop

以下打印undefined因为x是块范围的。回调是异步运行的。变量的新行为let意味着每个匿名函数都关闭了一个名为的不同变量x(不像它会用 完成var),因此整数0通过4被打印。:

for(let x = 0; x < 5; ++x) {
    setTimeout(() => console.log(x)) // `let` declarations are re-declared on a per-iteration basis, so the closures capture different variables
}
console.log(typeof x) // undefined

以下不会抛出 aReferenceError因为可见性x不受块的限制;但是,它会打印,undefined因为变量尚未初始化(因为if语句)。

if(false) {
    var x = 1
}
console.log(x) // here, `x` has been declared, but not initialised

在循环顶部声明的变量forusinglet的作用域为循环体:

for(let x = 0; x < 10; ++x) {} 
console.log(typeof x) // undefined, because `x` is block-scoped

以下将抛出 aReferenceError因为 的可见性x受到块的限制:

if(false) {
    let x = 1
}
console.log(typeof x) // undefined, because `x` is block-scoped

var使用或声明let的变量const都作用于模块:

// module1.js

var x = 0
export function f() {}

//module2.js

import f from 'module1.js'

console.log(x) // throws ReferenceError

以下将在全局对象上声明一个属性,因为var在全局上下文中声明的变量将作为属性添加到全局对象:

var x = 1
console.log(window.hasOwnProperty('x')) // true

let并且const在全局上下文中不要向全局对象添加属性,但仍然具有全局范围:

let x = 1
console.log(window.hasOwnProperty('x')) // false

函数参数可以认为是在函数体中声明的:

function f(x) {}
console.log(typeof x) // undefined, because `x` is scoped to the function

捕获块参数的范围为捕获块主体:

try {} catch(e) {}
console.log(typeof e) // undefined, because `e` is scoped to the catch block

命名函数表达式的范围仅限于表达式本身:

(function foo() { console.log(foo) })()
console.log(typeof foo) // undefined, because `foo` is scoped to its own expression

在非严格模式下,全局对象上隐式定义的属性是全局范围的。在严格模式下,您会收到错误消息。

x = 1 // implicitly defined property on the global object (no "var"!)

console.log(x) // 1
console.log(window.hasOwnProperty('x')) // true

在非严格模式下,函数声明具有函数范围。在严格模式下,它们具有块范围。

'use strict'
{
    function foo() {}
}
console.log(typeof foo) // undefined, because `foo` is block-scoped

它是如何在引擎盖下工作的

范围定义为标识符有效的代码词汇区域。

在 JavaScript 中,每个函数对象都有一个隐藏[[Environment]]的引用,它是对创建它的执行上下文(堆栈帧)的词法环境的引用。

当你调用一个函数时,隐藏的[[Call]]方法会被调用。该方法创建一个新的执行上下文,并在新的执行上下文和函数对象的词法环境之间建立链接。它通过将[[Environment]]函数对象上的值复制到新执行上下文的词法环境的外部引用字段中来做到这一点。

因此,在 JavaScript 中,作用域是通过外部引用以“链”链接在一起的词法环境来实现的。这个词法环境链称为作用域链,标识符解析是通过在链上搜索匹配标识符来实现的。

2022-02-11