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

2022-08-29 21:52:31

javascript中变量的范围是什么?它们在函数内部是否具有相同的作用域,而不是在函数外部?还是这很重要?另外,如果变量是全局定义的,则变量存储在哪里?


答案 1

断续器

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

这四个作用域是:

  1. 全局 - 一切可见
  2. 函数 - 在函数(及其子函数和块)中可见
  3. 块 - 在块(及其子块)中可见
  4. 模块 - 在模块中可见

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

概述

作用域是代码库的区域,标识符在其上有效。

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

作用域由词法环境的链接嵌套形成,嵌套中的每个级别对应于祖先执行上下文的词法环境。

这些链接的词汇环境形成了一个范围“链”。标识符解析是沿着此链搜索匹配标识符的过程。

标识符解析仅发生在一个方向上:向外。通过这种方式,外部词法环境无法“看到”到内部词汇环境中。

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

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

声明标识符的一些方法:

  1. varletconst
  2. 函数参数
  3. 捕获块参数
  4. 函数声明
  5. 命名函数表达式
  6. 全局对象上隐式定义的属性(即,在非严格模式下丢失)var
  7. import语句
  8. eval

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

  1. 全球背景
  2. 功能体
  3. 普通块
  4. 控制结构的顶部(例如,循环、如果、同时等)
  5. 控制结构体
  6. 模块

声明样式

瓦尔

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

let and const

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

注:,且全部吊装。这意味着它们的逻辑定义位置是其封闭范围(块或函数)的顶部。但是,使用 声明的变量,在控件通过源代码中的声明点之前,无法读取或分配到 的变量。过渡期称为时空盲区。letconstvarletconst

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

函数参数名称

函数参数名称的作用域为函数体。请注意,这有点复杂。声明为默认参数的函数在参数列表上关闭,而不是函数的主体。

函数声明

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

命名函数表达式

命名函数表达式的作用域限定为自身(例如,出于递归目的)。

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

在非严格模式下,全局对象上隐式定义的属性具有全局作用域,因为全局对象位于作用域链的顶部。在严格模式下,不允许这样做。

评估

在字符串中,使用 声明的变量将放置在当前作用域中,或者,如果间接使用,则将其作为全局对象的属性。evalvareval

例子

下面将抛出一个引用错误,因为名称 ,和 在函数之外没有任何意义。xyzf

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)

下面将抛出一个 ReferenceError for 和 ,但不是 for ,因为 的可见性不受块的约束。定义控制结构主体(如 、 和 )的块的行为类似。yzxxifforwhile

{
    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

在下文中,在循环外部可见,因为具有函数作用域:xvar

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

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

以下打印 ,五次,然后为循环外部打印第六次:55console.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

以下打印是因为是块范围的。回调以异步方式逐个运行。变量的新行为意味着每个匿名函数在名为 的不同变量上关闭(与 它本来可以做的不同),因此将打印整数到:undefinedxletxvar04

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

以下不会抛出一个,因为的可见性不受块的约束;但是,它将打印,因为变量尚未初始化(由于语句)。ReferenceErrorxundefinedif

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

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

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

下面将抛出一个,因为 的可见性受块的约束:ReferenceErrorx

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

使用 声明的变量,或者全部作用域为模块:varletconst

// 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中,范围是通过外部引用在“链”中链接在一起的词法环境来实现的。这个词法环境链称为范围链,标识符解析是通过在链上搜索匹配的标识符来实现的。

了解更多


答案 2

Javascript使用范围链来建立给定函数的范围。通常有一个全局作用域,并且定义的每个函数都有自己的嵌套作用域。在另一个函数中定义的任何函数都有一个链接到外部函数的局部作用域。始终是源中定义范围的位置。

作用域链中的元素基本上是一个 Map,其中包含指向其父作用域的指针。

解析变量时,javascript 从最里面的范围开始,向外搜索。