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

2207

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


5
这里有另一个不错的链接,可以帮助你理解这个问题:"解释JavaScript作用域和闭包"。 - Full-Stack Software Engineer
10
这是一篇很好地解释了JavaScript变量作用域的文章。关于JavaScript变量作用域,你需要知道的一切 - Saurab Parakh
3
Kyle Simpson的电子书对于了解JavaScript作用域和闭包相关知识非常有帮助。你可以在Github上找到它,并阅读其中的内容。这本电子书是“你不知道的JavaScript”系列书籍的一部分,该系列适合所有想更深入了解JavaScript的人。点击此链接可获取更多信息:https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/README.md - 3rik82
var 规则。JavaScript 不需要 "添加" 'const' 和 'let',这些与它的精神相悖。- 我知道这两个不是你问题的一部分 - 在看到这么多人 "推荐" 它们之后,我不得不加上这句话。 - iAmOren
你应该使用let来限定作用域,var是不必要的;它只会让代码变得混乱。https://hackernoon.com/why-you-shouldnt-use-var-anymore-f109a58b9b70 - Henrik Erlandsson
27个回答

2682

简述

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

四个作用域如下:

  1. 全局 - 所有内容可见
  2. 函数 - 函数内可见(及其子函数和块)
  3. 块 - 块内可见(及其子块)
  4. 模块 - 模块内可见

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

概述

作用域是代码库中标识符有效的区域。

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

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

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

标识符解析仅朝一个方向进行:向外。通过这种方式,外部词法环境无法“看到”内部词法环境。

在 JavaScript 中,确定 标识符作用域 有三个相关因素:

  1. 如何声明标识符
  2. 标识符在哪里声明
  3. 你是否处于严格模式非严格模式

标识符可以通过以下方式之一声明:

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

标识符可以在以下位置之一声明:

  1. 全局上下文
  2. 函数体
  3. 普通块
  4. 控制结构顶部(例如,循环,if,while等)
  5. 控制结构主体
  6. 模块

声明样式

var

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

let和const

使用 letconst 声明的标识符具有块级作用域,除非它们直接在全局上下文中声明,在这种情况下,它们具有全局作用域。
注意:letconstvar 都会提升。这意味着它们的逻辑定义位置是其封闭范围(块或函数)的顶部。但是,使用 letconst 声明的变量在源代码中声明点之后控制流程才能读取或赋值。这个过渡期被称为暂时性死区。

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

函数参数名称

函数参数名称作用域限定于函数体。请注意,这方面有些复杂。声明为默认参数的函数会封闭参数列表而非函数体。

函数声明

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

命名函数表达式

命名函数表达式作用域为其本身(例如,用于递归目的)。

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

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

eval

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

示例

下面的代码会抛出一个ReferenceError异常,因为变量名xyz在函数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)

以下代码会抛出 ReferenceError 错误,其中 yz 会报错,但 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五次,然后为循环之外的console.log再次打印5

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

下面的代码不会抛出ReferenceError,因为变量x的可见性不受块的限制;然而,它将打印undefined,因为变量没有被初始化(因为if语句)。

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

let 关键字声明的变量在 for 循环顶部声明时,其作用域仅限于循环体内:

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

以下代码会抛出一个ReferenceError,因为x的可见性受到块的限制:

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

letconst 在全局上下文中不会向全局对象添加属性,但仍具有全局作用域:

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

295
这段内容的大意是:这并不全面,但以下这组JavaScript作用域技巧可能是必须掌握的,才能有效地阅读现代JavaScript。我的翻译如下:虽然不完整,但以下是可能是必学的JavaScript作用域技巧集合,只有掌握了这些技巧,才能有效地理解现代JavaScript代码。 - Kenan Banks
154
一个高评价的回答,不确定为什么。它只是一堆没有适当解释的例子,然后似乎混淆了原型继承(即属性解析)与作用域链(即变量解析)。有一个全面(而准确)关于作用域和属性解析的解释在comp.lang.javascript [FAQ notes]中。 - RobG
114
它因为对广大程序员有用且易于理解而备受好评,尽管存在一些小的语用错误。你贴出的链接虽然对一些专业人士有用,但对如今大多数编写JavaScript的人来说却难以理解。如果需要,可以通过编辑回答来修复任何专业术语问题。 - Kenan Banks
8
@triptych — 我只会编辑回答来修复小错误,而不是大问题。将 “scope” 更改为 “property” 可以解决错误,但在没有非常明确的区分继承和作用域的情况下混淆它们仍然存在问题。 - RobG
26
如果你在外部作用域定义了一个变量,然后在函数内使用if语句定义了一个同名的变量,即使不执行if分支,该变量也会被重新定义。例如:http://jsfiddle.net/3CxVm/ - Chris S
显示剩余19条评论

243

Javascript使用作用域链来确定给定函数的作用域。通常只有一个全局作用域,每个定义的函数都有自己的嵌套作用域。在另一个函数内定义的任何函数都有一个与外部函数关联的本地作用域。它始终是源代码中的位置定义了作用域。

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

解析变量时,JavaScript从最内部的作用域开始向外搜索。


2
作用域链是闭包的另一个术语,对于那些在这里学习/进入JavaScript的人来说。 - New Alexandria

115

全局声明的变量具有全局作用域。在函数内声明的变量仅限于该函数,并遮蔽了同名的全局变量。

(我相信真正的JavaScript程序员在其他答案中可以指出许多微妙之处。特别是我看到了关于this在任何时候确切含义的这个页面。但愿这个更简单的链接足以让你入门。)


8
我害怕开始回答这个问题。作为一名真正的JavaScript程序员,我知道答案可能很快变得复杂。好文章。 - Kenan Banks
11
@Triptych: 我知道你所说的事情可能失控,但还是请回答一下吧。我只是通过几次搜索得到以上信息...一个由有实际经验的人编写的答案肯定会更好。请纠正我任何绝对错误的回答! - Jon Skeet
6
一些方式上,Jon Skeet 对我在 Stack Overflow 上最受欢迎的回答负有责任。 - Kenan Banks

80

传统的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 scriptBabel),则可以安全使用let语句。由于您的浏览器只会知道转换后的代码,因此性能问题应该是有限的。

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

    以下是一些完全不支持let的浏览器:

    • Internet explorer 10及以下版本
    • Firefox 43及以下版本
    • Safari 9及以下版本
    • Android browser 4及以下版本
    • Opera 27及以下版本
    • Chome 40及以下版本
    • 任何版本的Opera Mini和Blackberry Browser

enter image description here


如何跟踪浏览器支持情况

如果您想了解当前浏览器是否支持let语句,请参考this Can I Use page,该页面会及时更新。


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


2
“IS NOT known” 是具有误导性的,因为变量由于提升而在那里声明。 - Oriol
@Oriol:终于改进了我的答案,并解决了变量提升问题。感谢指出我需要改进的地方。我还进行了其他一些改进。 - John Slegers
1
这很有帮助,谢谢!我认为更具体地说明您所说的“现代JavaScript”和“老派JavaScript”会更有帮助;我认为它们分别对应ECMAScript 6 / ES6 / ECMAScript 2015和早期版本? - Jon Schneider
1
@JonSchneider:没错!当我说“老派JavaScript”时,我确实是指ECMAScript 5,而当我提到“现代JavaScript”时,我指的是ECMAScript 6(又称为ECMAScript 2015)。虽然大多数人只想知道(1)块级作用域和函数作用域之间的区别,(2)哪些浏览器支持块级作用域,以及(3)是否安全地在他们正在处理的项目中使用块级作用域,但我认为在这里详细说明并不是非常重要。因此,我专注于解决这些问题。 - John Slegers
1
@JonSchneider: (续) 尽管如此,我刚刚添加了一个链接到Smashing Magazine的一篇关于ES6 / ES2015的文章,供那些想要了解JavaScript在过去几年中添加了哪些功能的人学习...以及其他任何可能想知道我所说的“现代JavaScript”是什么的人。 - John Slegers
显示剩余4条评论

43
这是一个例子:
<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>

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


34

29
Brendan Eich最初设计JavaScript时,作用域的想法来自HyperCard脚本语言HyperTalk
在这种语言中,显示类似于一堆索引卡。有一个称为背景的主卡片。它是透明的,可以看作是底部卡片。该基础卡片上的任何内容都与放置在其上方的卡片共享。放置在顶部的每个卡片都有自己的内容,它优先于以前的卡片,但仍可以访问之前的卡片。
这正是 JavaScript 作用域系统的设计方式,只不过使用了不同的名称。在 JavaScript 中,卡片被称为执行上下文ECMA。每个上下文都包含三个主要部分:变量环境、词法环境和 this 绑定。回到卡片参考,词法环境包含堆栈中较低位置的所有内容。当前上下文位于堆栈顶部,任何在此处声明的内容都将存储在变量环境中。在命名冲突的情况下,变量环境将具有优先权。
this 绑定将指向包含对象。有时,在不更改包含对象的情况下,作用域或执行上下文会发生变化,例如在一个声明函数中,包含对象可能是 window 或构造函数。
每当控制权转移时就会创建这些执行上下文。当代码开始执行时,控制权就会转移,这主要是通过函数执行完成的。这就是技术解释。在实践中,重要的是要记住,在 JavaScript 中。
  • 作用域在技术上被称为"执行上下文"
  • 上下文形成了一个环境栈,变量存储在其中
  • 栈顶具有优先权(底部是全局上下文)
  • 每个函数都创建一个执行上下文(但不总是新的this绑定)

将其应用于此页面上的先前示例之一(5. "闭包"),可以跟踪执行上下文的堆栈。 在此示例中,堆栈中有三个上下文。 它们由外部上下文定义,由var six调用的立即调用函数中的上下文以及var six的立即调用函数内部返回的函数中的上下文。

i)外部上下文。 它具有变量环境a = 1
ii)IIFE上下文,它具有词法环境a = 1,但变量环境a = 6占据了堆栈中的优先权
iii)返回的函数上下文,它具有词法环境a = 6,并在调用时引用该值。

enter image description here


1
JavaScript 真的受 Hypertalk 的启发吗?我不记得 Hypertalk 有如此有趣的作用域,但这种灵感或许可以解释 JavaScript 的奇怪的运算符重载,例如 10 == "10.0" 和 10 == "10",但 "10.0" != "10"。虽然 Hypertalk 的运算符行为更加有趣。 - supercat
1
@supercat - 是的先生。大约在这个时候,我一直在研究Internet Explorer的起源(可以追溯到Mosaic),试图弄清楚为什么IE10会成为一个安全问题,并将其中的一部分研究结果发送给了Jonathan Sampson。也许巧合的是,他们很快就开发了Edge,并删除了许多建议中的安全问题。不过,这篇文章实际上有点过时了,因为最近对EcmaScript的迭代和微任务的包含,在某些情况下在幕后创建了一个稍微更复杂的内存管理模型。 - Travis J
1
@supercat - 对于一些仍然可用的参考资料,“我开始研究像Logo、Smalltalk、Self和HyperTalk这样的语言,HyperTalk是Bill Atkinson为HyperCard开发的语言”-Brendan Eich,“JavaScript(其创造者Brendan Eich受到HyperTalk[32]的启发)”-维基引用他的书。这是我写给微软Jonathan的电子邮件: https://jsfiddle.net/fwchpvrj/ - Travis J
1
可能有一些概念上的灵感,但是我曾经使用过Hypertalk和Javascript,我没有看到它们之间任何设计上的共性。Hypercard堆栈直接影响包含系统的能力是因为当遇到不熟悉的命令或函数时,Hypercard会搜索一个类型为XCMD或(如果记忆无误)XFCN的资源,其名称与不熟悉的命令或函数的名称匹配,并且 - 如果找到一个 - 将其作为代码资源加载到内存中并调用它。按设计,任何在... - supercat
“当时丹尼并不知道他的HyperCard书籍给了我多少灵感,但它在1995年JavaScript开发期间一直放在我的桌子上。”“虽然HyperTalk的自然语言语法仍然新鲜在我的脑海中,但下一个大事情更加沉重,特别是考虑到另一个目标:编写Java小程序的脚本。”“尽管最初不如HyperCard的消息灵活(其处理程序启发了onEvent命名约定),但JavaScript事件让HTML作者从远程服务器接管用户交互,并快速响应用户手势和浏览器操作。” - Travis J
显示剩余10条评论

29

在 "Javascript 1.7"(Mozilla 对 Javascript 的扩展)中,也可以使用 let 语句 声明块级作用域变量:

 var a = 4;
 let (a = 3) {
   alert(a); // 3
 }
 alert(a);   // 4

2
是的,但使用它安全吗?我的意思是,如果我的代码将在WebKit中运行,我会真正选择这个实现吗? - IgorGanapolsky
11
不,WebKit不支持 let - kennytm
或者,如果您正在使用XUL框架进行编程,这是Mozilla的接口框架,您可以使用css、xml和javascript构建。 - Gerard ONeill
2
@GazB,即使那是一个可怕的想法!所以今天你知道你的客户正在使用Mozilla,然后出现了一份新备忘录,说明现在他们正在使用其他东西。也就是说,我们支付系统糟糕的原因是...你必须使用IE8,而不是IE9或IE10或Firefox或Chrome,因为它根本不起作用... - buzzsawddog
@buzzsawddog 我同意,也讨厌只限于一个浏览器的系统,但不幸的是这种情况确实会发生。我只是想说,在那个时候,据我所知,这是唯一有效的用法。 :) - GazB
显示剩余3条评论

19

1) JavaScript有全局作用域、函数作用域以及with和catch语句的作用域。通常没有块级别的作用域,但是with和catch语句会将变量名添加到它们的块中。

2) 作用域由函数嵌套直到全局作用域。

3) 通过原型链解析属性。with语句将对象属性名称带入由with块定义的词法作用域中。

编辑:ECMAAScript 6(Harmony)规范支持let,我知道Chrome允许使用“harmony”标志,因此可能支持它。

let将支持块级作用域,但必须使用该关键字才能实现。

编辑:根据评论中Benjamin指出的with和catch语句,我已经修改了帖子,并添加了更多内容。 with和catch语句都将变量引入其各自的块中,这就是块级作用域。这些变量被别名为传递给它们的对象的属性。

 //chrome (v8)

 var a = { 'test1':'test1val' }
 test1   // error not defined
 with (a) { var test1 = 'replaced' }
 test1   // undefined
 a       // a.test1 = 'replaced'

编辑:澄清示例:

test1作用域限定于with块,但是被别名为a.test1。'var test1'在上层词法环境(函数或全局)中创建一个新的变量test1,除非它是a的属性--而它是。

哎呀!使用“with”要小心--就像如果变量在函数中已经定义,那么var无操作一样,它对从对象导入的名称也是无效的!提前告知该名称已经被定义会让这个过程更加安全。因为这个原因,我个人不会使用with。


你在这里犯了一些错误,其中之一是JavaScript确实有块级作用域形式。 - Benjamin Gruenbaum
本人耳朵(眼睛)张开了,Benjamin -- 我之前所说的是我对JavaScript作用域的处理方式,但并非基于规范阅读。希望你不是指with语句(这是一种对象作用域形式)或Mozilla的特殊“let”语法。 - Gerard ONeill
嗯,“with”语句确实是一种块级作用域的形式,但“catch”子句是更常见的形式(有趣的事实是,v8使用“with”实现了“catch”)-这几乎是JavaScript本身中唯一的块级作用域形式(即函数、全局、try/catch、with及其衍生物),然而,宿主环境具有不同的作用域概念-例如浏览器中的内联事件和NodeJS的vm模块。 - Benjamin Gruenbaum
从我所看到的,不论是 with 还是 catch 都只是将对象引入当前作用域(以及其属性),但在相应的代码块结束后,变量就会被重置。例如,在 catch 中引入的新变量将具有封闭函数/方法的作用域。 - Gerard ONeill
2
这正是块级作用域的含义 :) - Benjamin Gruenbaum
显示剩余2条评论

14

内联处理程序

前端开发人员经常遇到的一个常见问题是HTML中内联事件处理程序可见的作用域 - 例如,使用

<button onclick="foo()"></button>
< p > on*属性可以引用的变量范围必须是以下之一:< /p>
  • 全局变量(工作中的内联处理程序几乎总是引用全局变量)
  • 文档的属性(例如,作为独立变量的querySelector将指向document.querySelector;很少见)
  • 附加处理程序的元素的属性(与上面类似;很少见)

否则,当调用处理程序时,您将收到ReferenceError。例如,如果内联处理程序引用在window.onload$(function() {内定义的函数,则引用将失败,因为内联处理程序只能引用全局作用域中的变量,而函数不是全局的:

window.addEventListener('DOMContentLoaded', () => {
  function foo() {
    console.log('foo running');
  }
});
<button onclick="foo()">click</button>

由于内联处理程序被调用两个with块中, 一个是document,另一个是元素,因此可以将文档的属性和处理程序所附加到的元素的属性作为独立变量引用。这些处理程序内部的变量作用域链非常不直观,因此工作事件处理程序可能需要使函数全局化(并且应该避免不必要的全局污染should probably be avoided)。

由于内联处理程序内部的作用域链非常奇怪,并且内联处理程序需要全局污染才能工作,有时还需要在传递参数时进行丑陋的字符串转义,因此最好避免使用它们。相反,使用JavaScript(例如addEventListener)而不是HTML标记来附加事件处理程序。

function foo() {
  console.log('foo running');
}
document.querySelector('.my-button').addEventListener('click', foo);
<button class="my-button">click</button>

模块 (<script type="module">)

另外,与普通的<script>标签不同的是,ES6模块中的代码运行在其自己的私有作用域中。在普通的<script>标签顶部定义的变量是全局的,因此您可以在其他<script>标签中引用它,如下所示:

<script>
const foo = 'foo';
</script>
<script>
console.log(foo);
</script>

但是ES6模块的顶层不是全局的。在ES6模块的顶部声明的变量只能在该模块内部可见,除非该变量被显式地export,或者将其分配给全局对象的属性。

<script type="module">
const foo = 'foo';
</script>
<script>
// Can't access foo here, because the other script is a module
console.log(typeof foo);
</script>

ES6模块的顶层类似于普通

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接