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

IT技术 javascript function variables scope var
2021-01-05 21:29:57

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

6个回答

TLDR

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

这四个范围是:

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

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

概述

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

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

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

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

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

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

  1. 如何声明标识符
  2. 声明标识符的地方
  3. 无论你是在严格模式还是非严格模式

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

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

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

  1. 全球背景
  2. 函数体
  3. 普通块
  4. 控制结构的顶部(例如,循环、if、while 等)
  5. 控制结构体
  6. module

声明样式

无功

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

让和常量

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

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

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

函数参数名称

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

函数声明

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

命名函数表达式

命名函数表达式的作用域是它们自身(例如,为了递归)。

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

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

评估

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

例子

以下将抛出一个 ReferenceError 因为名称x, y, 和z在函数之外没有意义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,但不会为 for抛出一个 ReferenceError x,因为 的可见性x不受块的约束。定义控制结构的体块一样ifforwhile,行为类似。

{
    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

for循环顶部声明的变量usinglet的作用域是循环体:

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, letor声明的变量const都作用于module:

// 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

Catch 块参数的范围限定为 catch 块主体:

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

了解更多

甚至还不是全面的,但这可能是人们必须知道的一组 Javascript 范围技巧,甚至需要有效地阅读现代 javascript。
2021-02-15 21:29:57
@RobG 它受到高度评价,因为它对广泛的程序员有用且易于理解,尽管有一些小问题。您发布的链接虽然对某些专业人士有用,但对于当今编写 Javascript 的大多数人来说却难以理解。请随时通过编辑答案来解决任何命名问题。
2021-02-24 21:29:57
评价很高的答案,不知道为什么。这只是一堆没有正确解释的例子,然后似乎混淆了原型继承(即属性解析)和作用域链(即变量解析)。在 comp.lang.javascript FAQ notes 中对范围和属性解析进行了全面(且准确)的解释
2021-03-01 21:29:57
如果您在外部作用域中定义了一个变量,然后使用 if 语句在函数内定义了一个同名变量,即使没有到达分支,它也会被重新定义。一个例子 - jsfiddle.net/3CxVm
2021-03-02 21:29:57
@triptych——我只编辑答案来解决小问题,而不是大问题。将“范围”更改为“属性”将修复错误,但不会解决混合继承和范围而没有明确区分的问题。
2021-03-05 21:29:57

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

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

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

作用域链是 [memory] Closures 的另一个术语……对于那些在这里阅读以学习/进入 javascript 的人。
2021-02-24 21:29:57

全局声明的变量具有全局作用域。在函数内声明的变量的作用域是该函数,并隐藏同名的全局变量。

(我敢肯定,真正的 JavaScript 程序员可以在其他答案中指出许多微妙之处。特别是我在任何时候都看到了这个页面,关于究竟this什么意思。希望这个更具介绍性的链接足以让你开始.)

我什至不敢开始回答这个问题。作为一名真正的 Javascript 程序员,我知道答案可能会很快失控。不错的文章。
2021-02-06 21:29:57
不知何故,Jon Skeet 负责我在 Stack Overflow 上最受欢迎的答案。
2021-02-13 21:29:57
@Triptych:我知道您对事情失控的意思,但无论如何添加答案。我只是通过几次搜索得到了上述内容……由具有实际经验的人写的答案肯定会更好。请纠正我的任何答案,尽管这绝对是错误的!
2021-03-04 21:29:57

老派 JavaScript

传统上,JavaScript 实际上只有两种作用域:

  1. 全局范围:变量在整个应用程序中都是已知的,从应用程序开始(*)
  2. 函数作用域:变量在声明它们的函数中是已知,从函数的开头(*)

我不会详细说明这一点,因为已经有许多其他答案解释了这种差异。


现代 JavaScript

最近JavaScript的功能现在也允许第三范围:

  1. 块作用域:标识符从它们在 中声明的作用域顶部是“已知的” ,但在它们的声明行之后才能分配或取消引用(读取)。这个过渡期被称为“时间死区”。

如何创建块作用域变量?

传统上,您可以像这样创建变量:

var myVariable = "Some text";

块作用域变量是这样创建的:

let myVariable = "Some text";

那么功能作用域和块作用域有什么区别呢?

要了解功能范围和块范围之间的区别,请考虑以下代码:

// i IS NOT known here
// j IS NOT known here
// k IS known here, but undefined
// l IS NOT known here

function loop(arr) {
    // i IS known here, but undefined
    // j IS NOT known here
    // k IS known here, but has a value only the second time loop is called
    // l IS NOT known here

    for( var i = 0; i < arr.length; i++ ) {
        // i IS known here, and has a value
        // j IS NOT known here
        // k IS known here, but has a value only the second time loop is called
        // l IS NOT known here
    };

    // i IS known here, and has a value
    // j IS NOT known here
    // k IS known here, but has a value only the second time loop is called
    // l IS NOT known here

    for( let j = 0; j < arr.length; j++ ) {
        // i IS known here, and has a value
        // j IS known here, and has a value
        // k IS known here, but has a value only the second time loop is called
        // l IS NOT known here
    };

    // i IS known here, and has a value
    // j IS NOT known here
    // k IS known here, but has a value only the second time loop is called
    // l IS NOT known here
}

loop([1,2,3,4]);

for( var k = 0; k < arr.length; k++ ) {
    // i IS NOT known here
    // j IS NOT known here
    // k IS known here, and has a value
    // l IS NOT known here
};

for( let l = 0; l < arr.length; l++ ) {
    // i IS NOT known here
    // j IS NOT known here
    // k IS known here, and has a value
    // l IS known here, and has a value
};

loop([1,2,3,4]);

// i IS NOT known here
// j IS NOT known here
// k IS known here, and has a value
// l IS NOT known here

在这里,我们可以看到我们的变量j只在第一个 for 循环中是已知的,而不是之前和之后。然而,我们的变量i在整个函数中都是已知的。

另外,请考虑块作用域变量在声明之前是未知的,因为它们没有被提升。您也不允许在同一个块中重新声明同一个块作用域变量。这使得块范围的变量比全局或功能范围的变量更不容易出错,全局或功能范围的变量被提升并且在多个声明的情况下不会产生任何错误。


今天使用块作用域变量安全吗?

今天使用是否安全,取决于您的环境:

  • 如果您正在编写服务器端 JavaScript 代码 ( Node.js ),则可以安全地使用该let语句。

  • 如果您正在编写客户端 JavaScript 代码并使用基于浏览器的转译器(如Traceurbabel-standalone),您可以安全地使用该let语句,但是您的代码在性能方面可能不是最佳的。

  • 如果您正在编写客户端 JavaScript 代码并使用基于 Node 的转译器(如traceur shell 脚本Babel),则可以安全地使用该let语句。并且因为您的浏览器只会知道转译的代码,所以性能缺陷应该是有限的。

  • 如果您正在编写客户端 JavaScript 代码并且不使用转译器,则需要考虑浏览器支持。

    这些是一些根本不支持的浏览器let

    • Internet Explorer 10及以下
    • Firefox 43及以下
    • Safari 9及以下
    • Android 浏览器 4及以下
    • Opera 27及以下
    • Chome 40及以下
    • 任何版本的Opera Mini黑莓浏览器

在此处输入图片说明


如何跟踪浏览器支持

有关let在您阅读此答案时哪些浏览器支持该声明的最新概述,请参阅Can I Use页面


(*) 全局和功能范围的变量可以在声明之前进行初始化和使用,因为 JavaScript 变量是被提升的这意味着声明总是在作用域的顶部。

这很有帮助,谢谢!我认为具体说明“现代 JavaScript”和“老派 JavaScript”的含义会更有帮助;我认为这些分别对应于 ECMAScript 6 / ES6 / ECMAScript 2015 和早期版本?
2021-02-12 21:29:57
@JonSchneider:正确!在我说“老派 JavaScript”的地方,我在谈论 ECMAScript 5,在我提到“现代 JavaScript”的地方,我在谈论 ECMAScript 6(又名 ECMAScript 2015)。不过,我认为在这里详细介绍并没有那么重要,因为大多数人只想知道 (1) 块作用域和功能作用域之间有什么区别,(2) 哪些浏览器支持块作用域以及 (3)今天对他们正在从事的任何项目使用块作用域是否安全。所以我的回答集中在解决这些问题上。
2021-02-17 21:29:57
“IS NOT known”是一种误导,因为变量是由于提升而在那里声明的。
2021-02-18 21:29:57
上面的示例具有误导性,变量 'i' 和 'j' 在块外是未知的。'Let' 变量仅在该特定块中而不是在该块之外具有作用域。Let 还有其他优点,您不能再次重新声明变量,并且它持有词法范围。
2021-02-27 21:29:57
@JonSchneider :(续)不过,我只是添加了一个链接,指向一篇关于 ES6/ES2015 的 Smashing Magazine 文章,供那些想要了解更多关于过去几年 JavaScript 添加了哪些功能的人......可能想知道我所说的“现代 JavaScript”是什么意思。
2021-03-02 21:29:57

下面是一个例子:

<script>

var globalVariable = 7; //==window.globalVariable

function aGlobal( param ) { //==window.aGlobal(); 
                            //param is only accessible in this function
  var scopedToFunction = {
    //can't be accessed outside of this function

    nested : 3 //accessible by: scopedToFunction.nested
  };

  anotherGlobal = {
    //global because there's no `var`
  }; 

}

</script>

您将需要调查闭包,以及如何使用它们来创建私有成员