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

What is the scope of variables in JavaScript?

提问人:lYriCAlsSH 提问时间:2/1/2009 最后编辑:John SlegerslYriCAlsSH 更新时间:6/28/2021 访问量:573133

问:

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

JavaScript 函数 变量 作用域 var

评论

5赞 Full-Stack Software Engineer 8/29/2012
这是另一个很好的链接来记住这个问题:“解释 JavaScript 范围和闭包”。
10赞 Saurab Parakh 3/25/2014
这是一篇很好地解释它的文章。您需要了解的有关 Javascript 变量范围的所有信息
3赞 3rik82 2/7/2017
前面提到的Kyle Simpson的电子书可以在Github上阅读,它告诉你所有你需要知道的关于JavaScript Scopes & Closures的信息。你可以在这里找到它:github.com/getify/You-Dont-Know-JS/blob/master/...它是“你不了解 JS”系列丛书的一部分,非常适合每个想要更多地了解 JavaScript 的人。
0赞 iAmOren 7/1/2020
var规则。JavaScript 不需要“const”和“let”的“添加”,这违背了它的精神。- 我知道这两个不是你问题的一部分 - 在看到这么多“推动”它们之后不得不添加这个。
0赞 Henrik Erlandsson 9/1/2022
您应该将 let 用于范围。Var 是不需要的;单词沙拉。hackernoon.com/why-you-should't-use-var-anymore-f109a58b9b70

答:

115赞 Jon Skeet 2/1/2009 #1

全局声明的变量具有全局范围。在函数中声明的变量的范围限定为该函数,并隐藏同名的全局变量。

(我敢肯定,真正的 JavaScript 程序员可以在其他答案中指出许多微妙之处。特别是,我遇到了这个关于任何时候的确切含义的页面。希望这个更具介绍性的链接足以让您入门。this

评论

8赞 Kenan Banks 2/1/2009
我什至不敢开始回答这个问题。作为一个真正的 Javascript 程序员,我知道答案会很快失控。好文章。
11赞 Jon Skeet 2/1/2009
@Triptych:我知道你说事情失控的意思,但无论如何请补充一个答案。我只是通过几次搜索得到了上述内容......由有实际经验的人写的答案肯定会更好。请纠正我的任何答案,这绝对是错误的!
6赞 Kenan Banks 1/10/2018
不知何故,Jon Skeet 负责我在 Stack Overflow 上最受欢迎的答案。
243赞 krosenvold 2/1/2009 #2

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

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

解析变量时,javascript 从最内层范围开始,然后向外搜索。

评论

2赞 New Alexandria 2/6/2014
作用域链是 [memory] 闭包的另一个术语......对于那些在这里阅读学习/进入 JavaScript 的人。
43赞 geowa4 2/1/2009 #3

下面是一个示例:

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

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

2682赞 Kenan Banks 2/1/2009 #4

顶级域名

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

这四个范围是:

  1. 全球 - 万物可见
  2. 函数 - 在函数(及其子函数和块)中可见
  3. 块 - 在块(及其子块)内可见
  4. 模块 - 在模块内可见

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

概述

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

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

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

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

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

在决定 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的

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

let 和 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`!

函数参数名称

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

函数声明

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

命名函数表达式

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

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

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

评估

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

例子

以下代码将抛出 ReferenceError,因为名称 、 和 在函数之外没有任何意义。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 和 ,但不会抛出 ,因为 的可见性不受块的约束。定义控制结构主体(如 、 和 )的块的行为类似。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

以下不会抛出 a,因为 的可见性不受块的约束;但是,它将打印,因为变量尚未初始化(因为语句)。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

catch 块参数的范围限定为 catch-block body:

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赞 Kenan Banks 2/1/2009
甚至还不算全面,但这可能是必须知道的一组 Javascript 范围技巧,即使是有效地阅读现代 javascript。
154赞 RobG 9/10/2012
一个高度评价的答案,不知道为什么。它只是一堆没有正确解释的例子,然后似乎混淆了原型继承(即属性解析)和作用域链(即变量解析)。comp.lang.javascript FAQ 注释中对范围和属性解析进行了全面(且准确)的解释。
114赞 Kenan Banks 9/10/2012
@RobG 它之所以受到高度评价,是因为它对广泛的程序员有用且易于理解,尽管有轻微的后果。您发布的链接虽然对某些专业人士有用,但对于当今大多数编写 Javascript 的人来说是无法理解的。请随意通过编辑答案来解决任何命名法问题。
8赞 RobG 9/10/2012
@triptych - 我只编辑答案来解决次要问题,而不是主要问题。将“scope”更改为“property”将修复错误,但不能解决在没有非常明确区分的情况下混合继承和范围的问题。
26赞 Chris S 6/13/2013
如果在外部作用域中定义一个变量,然后让 if 语句在函数内定义一个同名的变量,即使未到达分支,也会重新定义它。一个例子 - jsfiddle.net/3CxVm
29赞 kennytm 4/6/2010 #5

在“Javascript 1.7”(Mozilla 对 Javascript 的扩展)中,还可以使用 let 语句声明块范围变量:

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

评论

2赞 IgorGanapolsky 12/29/2010
是的,但使用安全吗?我的意思是,如果我的代码将在 WebKit 中运行,我会现实地选择这种实现吗?
11赞 kennytm 12/29/2010
@Python:不,WebKit 不支持 .let
0赞 GazB 10/11/2012
我想这样做的唯一有效用途是,如果您知道所有客户端都将使用Mozilla浏览器,例如公司内部系统。
0赞 Gerard ONeill 10/25/2013
或者,如果你使用XUL框架进行编程,这是Mozilla的接口框架,你可以使用css、xml和javascript进行构建。
2赞 buzzsawddog 11/23/2013
@GazB即使这是一个可怕的想法!所以今天你知道你的客户正在使用Mozilla,然后出现了一个新的备忘录,说现在他们正在使用其他东西。即我们的薪酬制度很糟糕的原因......您必须使用 IE8,而不是 IE9 或 IE10、Firefox 或 Chrome,因为它完全无法正常工作......
34赞 James McMahon 5/16/2012 #6

据我了解,关键是 Javascript 具有函数级范围,而不是更常见的 C 块范围。

这是一篇关于这个主题的好文章。

12赞 austincheney 3/22/2013 #7

我发现许多刚接触 JavaScript 的人都很难理解继承在语言中默认可用,而到目前为止,函数作用域是唯一的作用域。我为我在去年年底写的一个名为 JSPretty 的美化器提供了一个扩展。功能颜色在代码中对作用域起作用,并始终将颜色与该作用域中声明的所有变量相关联。当一个作用域中具有一个作用域中的颜色的变量在另一个作用域中使用时,可以直观地显示闭包。

在以下位置试用该功能:

在以下位置观看演示:

在以下位置查看代码:

目前,该功能支持 16 个嵌套函数的深度,但目前不为全局变量着色。

评论

2赞 mplwork 2/14/2014
不适用于 Firefox 26。我粘贴代码或加载文件,单击执行,没有任何反应。
0赞 Ben Aston 2/28/2020
作用域和继承是两个不同的东西。
6赞 Mig82 8/11/2013 #8

试试这个奇怪的例子。在下面的示例中,如果 a 是初始化为 0 的数字,则会先看到 0,然后再看到 1。除了 a 是一个对象,javascript 将向 f1 传递一个 a 的指针,而不是它的副本。结果是您两次都会收到相同的警报。

var a = new Date();
function f1(b)
{
    b.setDate(b.getDate()+1);
    alert(b.getDate());
}
f1(a);
alert(a.getDate());
19赞 Gerard ONeill 10/25/2013 #9

1)有一个全局作用域,一个函数作用域,以及with和catch作用域。变量通常没有“块”级范围 - with 和 catch 语句为其块添加名称。

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

3)通过原型链解析属性。with 语句将对象属性名称引入 with 块定义的词法范围。

编辑:ECMAAScript 6(Harmony)被规范为支持let,我知道chrome允许“harmony”标志,所以也许它确实支持它。

let 将支持块级范围,但您必须使用关键字才能实现它。

编辑:根据本杰明在评论中指出的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,除非它是 -- 的属性。

哎呀!小心使用 'with' -- 就像 var 是一个 noop 一样,如果变量已经在函数中定义,它也是一个 noop,对于从对象导入的名称来说,它也是一个 noop!对已经定义的名称稍加注意会使它更安全。因此,我个人永远不会使用。

评论

0赞 Benjamin Gruenbaum 10/25/2013
你在这里有一些错误,因为 JavaScript 确实有块范围的形式。
0赞 Gerard ONeill 10/25/2013
我的耳朵(眼睛)是睁开的,Benjamin——我上面的陈述是我对待 Javascript 范围的方式,但它们不是基于阅读规范。我希望你指的不是with语句(这是对象范围的一种形式),或者Mozilla的特殊“let”语法。
0赞 Benjamin Gruenbaum 10/26/2013
好吧,语句块范围的一种形式,但子句是一种更常见的形式(有趣的是,v8 实现了 ) - 这几乎是 JavaScript 本身中块范围的唯一形式(即 function、global、try/catch 、with 及其派生物),但是主机环境具有不同的范围概念 - 例如浏览器中的内联事件和 NodeJS 的 vm 模块。withcatchcatchwith
0赞 Gerard ONeill 10/26/2013
Benjamin -- 据我所知,with 和 catch 都只将对象引入当前范围(以及属性),但在相应的块结束后,变量被重置。但例如,在 catch 中引入的新变量将具有封闭函数/方法的范围。
2赞 Benjamin Gruenbaum 10/26/2013
这正是块范围界定的含义:)
11赞 Jhankar Mahbub 6/24/2014 #10

全球范围 :

全局变量与全球明星(成龙、纳尔逊·曼德拉)一模一样。您可以从应用程序的任何部分访问它们(获取或设置值)。全局功能类似于全局事件(新年、圣诞节)。您可以从应用程序的任何部分执行(调用)它们。

//global variable
var a = 2;

//global function
function b(){
   console.log(a);  //access global variable
}

本地范围 :

如果你在美国,你可能知道金·卡戴珊(Kim Kardashian),臭名昭著的名人(她以某种方式设法制作了小报)。但美国以外的人不会认出她。她是当地的明星,被束缚在她的领地上。

局部变量就像局部恒星。只能在作用域内访问它们(获取或设置值)。本地函数类似于本地事件 - 您只能在该范围内执行(庆祝)。如果要从范围之外访问它们,则会收到引用错误

function b(){
   var d = 21; //local variable
   console.log(d);

   function dog(){  console.log(a); }
     dog(); //execute local function
}

 console.log(d); //ReferenceError: dddddd is not defined    

查看本文以深入了解范围

11赞 Anshul 9/22/2014 #11

JavaScript 只有两种类型的范围:

  1. 全局范围:全局只不过是一个窗口级范围。在这里,变量存在于整个应用程序中。
  2. 功能范围:在带有关键字的函数中声明的变量具有功能范围var

每当调用函数时,都会创建一个变量作用域对象(并包含在作用域链中),该对象后跟 JavaScript 中的变量。

        a = "global";
         function outer(){ 
              b = "local";
              console.log(a+b); //"globallocal"
         }
outer();

示波器链 -->

  1. 窗口级别 - 和功能位于范围链的顶层。aouter
  2. 当外部函数调用一个新的(并包含在作用域链中)时,它内部添加了变量。variable scope objectb

现在,当一个变量需要时,它首先搜索最近的变量范围,如果变量不存在,它就会移动到变量范围链的下一个对象,在这种情况下是窗口级别。a

评论

1赞 SwiftMango 3/1/2015
不知道为什么这不是公认的答案。实际上只有功能范围(在 ECMA6 之前没有“局部范围”)和全局绑定
11赞 Yeasin Abedin 10/18/2014 #12

运行代码。希望这能给人一个关于范围界定的想法

Name = 'global data';
document.Name = 'current document data';
(function(window,document){
var Name = 'local data';
var myObj = {
    Name: 'object data',
    f: function(){
        alert(this.Name);
    }
};

myObj.newFun = function(){
    alert(this.Name);
}

function testFun(){
    alert("Window Scope : " + window.Name + 
          "\nLocal Scope : " + Name + 
          "\nObject Scope : " + this.Name + 
          "\nCurrent document Scope : " + document.Name
         );
}


testFun.call(myObj);
})(window,document);
4赞 koredalin 7/12/2015 #13

JS 中只有函数作用域。不阻止范围! 你也可以看到什么在吊装。

var global_variable = "global_variable";
var hoisting_variable = "global_hoist";

// Global variables printed
console.log("global_scope: - global_variable: " + global_variable);
console.log("global_scope: - hoisting_variable: " + hoisting_variable);

if (true) {
    // The variable block will be global, on true condition.
    var block = "block";
}
console.log("global_scope: - block: " + block);

function local_function() {
    var local_variable = "local_variable";
    console.log("local_scope: - local_variable: " + local_variable);
    console.log("local_scope: - global_variable: " + global_variable);
    console.log("local_scope: - block: " + block);
    // The hoisting_variable is undefined at the moment.
    console.log("local_scope: - hoisting_variable: " + hoisting_variable);

    var hoisting_variable = "local_hoist";
    // The hoisting_variable is now set as a local one.
    console.log("local_scope: - hoisting_variable: " + hoisting_variable);
}

local_function();

// No variable in a separate function is visible into the global scope.
console.log("global_scope: - local_variable: " + local_variable);

评论

0赞 Bob 3/23/2017
(自答案发布以来很长一段时间)块范围 ;developer.mozilla.org/en/docs/Web/JavaScript/Reference/......
29赞 Travis J 9/15/2015 #14

最初由 Brendan Eich 设计的 JavaScript 范围界定的想法来自 HyperCard 脚本语言 HyperTalk

在这种语言中,显示类似于一堆索引卡。有一张万事达卡被称为背景。它是透明的,可以看作是底牌。此基础卡上的任何内容都与放置在其顶部的卡共享。放置在顶部的每张卡片都有自己的内容,这些内容优先于前一张卡片,但如果需要,仍然可以访问之前的卡片。

这正是 JavaScript 范围系统的设计方式。它只是有不同的名称。JavaScript 中的卡片称为执行上下文ECMA。这些上下文中的每一个都包含三个主要部分。变量环境、词法环境和 this 绑定。回到卡片引用,词法环境包含堆栈中较低位置的先前卡片的所有内容。当前上下文位于堆栈的顶部,其中声明的任何内容都将存储在变量环境中。在命名冲突的情况下,变量环境将优先。

this 绑定将指向包含对象。有时,作用域或执行上下文会更改,而包含对象不会更改,例如在声明的函数中,其中包含对象或构造函数。window

每当传输控制权时,都会创建这些执行上下文。当代码开始执行时,控制权就会转移,这主要是通过函数执行完成的。

这就是技术解释。在实践中,重要的是要记住,在 JavaScript 中

  • 从技术上讲,作用域是“执行上下文”
  • 上下文构成了存储变量的环境堆栈
  • 堆栈的顶部优先(底部是全局上下文)
  • 每个函数都会创建一个执行上下文(但并不总是新的此绑定)

将其应用于前面的示例之一 (5.“Closure”),可以遵循执行上下文的堆栈。在此示例中,堆栈中有三个上下文。它们由外部上下文、var six 调用的立即调用的函数中的上下文以及 var six 立即调用的函数中返回函数中的上下文定义。

i) 外部环境。它有一个 a = 1 的变量环境 ii) IIFE 上下文,它有一个词法环境 a = 1
,但一个变量环境 a = 6,在堆栈
中优先 iii) 返回的函数上下文,它有一个词法环境 a = 6,这是调用时警报中引用的值。

enter image description here

评论

1赞 supercat 10/26/2020
Javascript 真的受到 Hypertalk 的启发吗?我不记得 Hypertalk 有如此有趣的范围,但灵感也许可以解释 Javascript 奇怪的运算符重载,其中 10==“10.0” 和 10==“10”,但“10.0”!=“10”。尽管 Hypertalk 的操作员表现得更有趣。
1赞 Travis J 10/27/2020
@supercat - 是的先生。大约在这个时候,我一直在研究Internet Explorer的起源(可以追溯到Mosaic),试图弄清楚为什么IE10是一个如此安全的问题,并将部分研究发送给了Jonathan Sampson。也许是巧合,不久之后,他们就开始了开发 Edge,并删除了许多建议的安全问题。不过,这篇文章实际上有点过时了,因为最近对 EcmaScript 的迭代和包含的微任务,在某些场景中,在后台的内存管理方面创建了一个稍微更复杂的模型。
1赞 Travis J 10/27/2020
@supercat - 对于一些仍然可用的参考资料,“我开始研究像 Logo 和 Smalltalk 以及 Self 和 HyperTalk 这样的语言,这是 Bill Atkinson 的 HyperCard 语言” -Brendan Eich,“JavaScript(其创建者 Brendan Eich 的灵感来自 HyperTalk[32])” -Wiki 引用了他的书。这是我写给微软乔纳森的电子邮件: jsfiddle.net/fwchpvrj
1赞 supercat 10/27/2020
可能有一些概念上的灵感,但同时使用过 Hypertalk 和 Javascript,我看不出它们之间有任何设计共性。Hypercard 堆栈之所以能够直接影响包含系统,是因为当遇到不熟悉的命令或函数时,Hypercard 会搜索类型为 XCMD 或(如果内存允许)XFCN 的资源,其名称与不熟悉的命令或函数的名称相匹配,如果找到,则将其作为代码资源加载到内存中并调用它。根据设计,任何资源都位于...
0赞 supercat 10/27/2020
...通过这种搜索可以找到当前文档。这使得 Hypercard 堆栈可以执行在语言中无法完成的事情,但这意味着堆栈不会以任何有意义的方式进行沙盒化。相比之下,Web 浏览器应该提供一个沙盒环境来处理已删除接收的内容;未能充分沙盒是由于错误造成的,而 Hypercard 没有沙盒的事实是由于设计决定不限制堆栈可以执行的任务范围。
-4赞 A. Randhawa 10/1/2015 #15

JavaScript 中有两种类型的作用域。

  1. 全局范围:在全局范围内宣布的变量可以非常顺利地在程序中的任何位置使用。例如:

    var carName = " BMW";
    
    // code here can use carName
    
    function myFunction() {
         // code here can use carName 
    }
    
  2. 函数作用域或局部作用域:在此作用域中声明的变量只能在其自己的函数中使用。例如:

    // code here can not use carName
    function myFunction() {
       var carName = "BMW";
       // code here can use carName
    }
    

评论

1赞 Sebastian Simon 3/13/2022
这个答案在发布时已经过时了,并且没有在现有答案中添加任何新内容。
9赞 jackbean818 10/30/2015 #16

几乎只有两种类型的 JavaScript 作用域:

  • 每个 VAR 声明的作用域都与最直接的封闭函数相关联
  • 如果 var 声明没有封闭函数,则它是全局范围

因此,除函数以外的任何块都不会创建新作用域。这就解释了为什么 for 循环会覆盖外部作用域变量:

var i = 10, v = 10;
for (var i = 0; i < 5; i++) { var v = 5; }
console.log(i, v);
// output 5 5

改用函数:

var i = 10, v = 10;
$.each([0, 1, 2, 3, 4], function(i) { var v = 5; });
console.log(i,v);
// output 10 10

在第一个示例中,没有块作用域,因此最初声明的变量被覆盖。在第二个示例中,由于该函数,有一个新作用域,因此最初声明的变量是 SHADOWED,而不是覆盖。

就 JavaScript 范围而言,这几乎是您需要了解的全部内容,除了:

所以你可以看到 JavaScript 的作用域实际上非常简单,尽管并不总是直观的。需要注意的几点:

  • VAR 声明被提升到示波器的顶部。这意味着无论 var 声明发生在哪里,对于编译器来说,就好像 var 本身发生在顶部一样
  • 合并同一范围内的多个 VAR 声明

所以这段代码:

var i = 1;
function abc() {
  i = 2;
  var i = 3;
}
console.log(i);     // outputs 1

相当于:

var i = 1;
function abc() {
  var i;     // var declaration moved to the top of the scope
  i = 2;
  i = 3;     // the assignment stays where it is
}
console.log(i);

这似乎有悖常理,但从命令式语言设计者的角度来看,这是有道理的。

80赞 John Slegers 2/24/2016 #17

老式 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

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

此外,请注意,块范围的变量在声明之前是未知的,因为它们没有被提升。您也不得在同一块中重新声明相同的块范围变量。这使得块作用域变量比全局变量或函数作用域变量更不容易出错,后者是悬挂的,在多个声明的情况下不会产生任何错误。


今天使用块范围变量安全吗?

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

  • 如果您正在编写服务器端 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 及以下版本
    • 丁目 40 及以下
    • 任何版本的 Opera MiniBlackberry 浏览器

enter image description here


如何跟踪浏览器支持

有关在您阅读此答案时哪些浏览器支持该声明的最新概述,请参阅此“我可以使用吗”页面let


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

评论

2赞 Oriol 8/30/2016
“未知”具有误导性,因为该变量是由于吊装而在那里声明的。
0赞 zakir 1/25/2018
上面的例子具有误导性,变量“i”和“j”在块之外是未知的。“Let”变量仅在该特定块中具有作用域,而不在该块之外。let 还有其他优点,你不能再次重新声明变量,它保留了词法范围。
1赞 Jon Schneider 3/7/2018
这很有帮助,谢谢!我认为具体说明“现代 JavaScript”和“老派 JavaScript”的含义会更有帮助;我认为这些分别对应于 ECMAScript 6 / ES6 / ECMAScript 2015 和更早的版本?
1赞 John Slegers 3/7/2018
@JonSchneider : 正确!我说“老派 JavaScript”的地方,我需要谈论的是 ECMAScript 5,而我指的是“现代 JavaScript”,我指的是 ECMAScript 6(又名 ECMAScript 2015)。不过,我认为在这里详细讨论并不那么重要,因为大多数人只是想知道 (1) 块范围和功能范围之间有什么区别,(2) 哪些浏览器支持块范围,以及 (3) 今天对他们正在从事的任何项目使用块范围是否安全。因此,我把答案的重点放在了解决这些问题上。
1赞 John Slegers 3/7/2018
@JonSchneider : (续) 尽管如此,我刚刚添加了一个链接,指向 Smashing Magazine 关于 ES6 / ES2015 的文章,供那些想了解更多关于过去几年中 JavaScript 添加了哪些功能的人......其他任何可能想知道我所说的“现代 JavaScript”是什么意思的人。
9赞 James-Jesse Drinkard 3/30/2016 #18

只是为了补充其他答案,范围是所有声明的标识符(变量)的查找列表,并强制执行一组严格的规则,说明当前正在执行的代码如何访问这些标识符(变量)。此查找可能用于分配给变量,该变量是 LHS(左侧)引用,也可能用于检索其值,即 RHS(右侧)引用。这些查找是 JavaScript 引擎在编译和执行代码时在内部执行的操作。

因此,从这个角度来看,我认为我在凯尔·辛普森 (Kyle Simpson) 的 Scopes and Closures 电子书中找到的图片会有所帮助:

image

引用他的电子书:

该建筑表示我们程序的嵌套范围规则集。第一个 建筑物的楼层代表您当前执行的范围, 无论您身在何处。建筑物的顶层是全局范围。 您可以通过查看当前楼层来解析 LHS 和 RHS 参考, 如果你没有找到它,乘电梯到下一层, 看看那里,然后是下一个,依此类推。一旦你到达顶楼 (全局范围),你要么找到你要找的东西,要么你 不要。但无论如何,你都必须停下来。

值得一提的是,“范围查找一旦找到第一个匹配项,就会停止”。

这种“作用域级别”的概念解释了为什么“this”可以通过新创建的作用域进行更改,如果它是在嵌套函数中查找的。 这是一个链接,涉及所有这些细节,你想知道的关于javascript范围的一切

4赞 mrmaclean89 9/17/2017 #19

我的理解是有 3 个范围:全局范围,全球可用;本地范围,可用于整个函数,而不考虑块;和块作用域,仅可用于使用它的块、语句或表达式。全局和局部范围用关键字“var”表示,无论是在函数内还是在函数外部,块范围用关键字“let”表示。

对于那些认为只有全局和局部范围的人,请解释为什么 Mozilla 会用一整页来描述 JS 中块范围的细微差别。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let

8赞 Gibolt 9/27/2017 #20

现代 Js、ES6+、'' 和 'constlet'

您应该对创建的每个变量使用块范围,就像大多数其他主要语言一样。 已过时。这使您的代码更安全、更易于维护。var

const95% 的病例应使用。它使变量引用无法更改。数组、对象和 DOM 节点属性可能会更改,并且很可能是 。const

let应用于任何期望重新赋值的变量。这包括在 for 循环中。如果在初始化之后更改值,请使用 。let

块作用域意味着变量仅在声明它的括号内可用。这扩展到内部作用域,包括在作用域中创建的匿名函数。

评论

0赞 iAmOren 7/1/2020
关于的问题。“const”和“let”=javascript的破坏者,不是问题,也不应该在javascript中......var
1赞 Gibolt 7/2/2020
您的意见不会改变答案的有效性。问题是关于范围界定的。 并且是定义范围的现代替代品。当被问到这个问题时,两者都不存在constletvar
2赞 Abdur Rahman 11/1/2017 #21

在 JavaScript 中,有两种类型的作用域:

  • 本地范围
  • 全球范围

Below 函数有一个局部作用域变量。并且无法从函数外部访问此变量。carName

function myFunction() {
    var carName = "Volvo";
    alert(carName);
    // code here can use carName
}

下面的类有一个全局范围变量。这个变量可以从类中的任何地方访问。carName

class {

    var carName = " Volvo";

    // code here can use carName

    function myFunction() {
        alert(carName);
        // code here can use carName 
    }
}

评论

0赞 iAmOren 7/1/2020
const 和 let 不在问题之列。你为什么要提出他们?问题是关于...const 和 let 令人讨厌并破坏 JavaScript。var
0赞 Vivek Mehta 12/15/2017 #22

在 EcmaScript5 中,主要有两个作用域,局部作用域和全局作用域,但在 EcmaScript6 中,我们主要有三个作用域,局部作用域、全局作用域和称为块作用域的新作用域

块范围的示例是:-

for ( let i = 0; i < 10; i++)
{
 statement1...
statement2...// inside this scope we can access the value of i, if we want to access the value of i outside for loop it will give undefined.
}
0赞 Davaakhuu 2/9/2018 #23

ECMAScript 6 引入了 let 和 const 关键字。这些关键字可以用来代替 var 关键字。与 var 关键字相反,let 和 const 关键字支持在块语句中声明局部范围。

var x = 10
let y = 10
const z = 10
{
  x = 20
  let y = 20
  const z = 20
  {
    x = 30
    // x is in the global scope because of the 'var' keyword
    let y = 30
    // y is in the local scope because of the 'let' keyword
    const z = 30
    // z is in the local scope because of the 'const' keyword
    console.log(x) // 30
    console.log(y) // 30
    console.log(z) // 30
  }
  console.log(x) // 30
  console.log(y) // 20
  console.log(z) // 20
}

console.log(x) // 30
console.log(y) // 10
console.log(z) // 10

评论

0赞 iAmOren 7/1/2020
const 和 let 不是问题的一部分。为什么要提出来?就个人而言,它们不属于 JavaScript......
0赞 Sebastian Simon 3/13/2022
@iAmOren 当然,它们是问题的一部分,是的,它们确实属于 JavaScript。应该废除的东西,如果有的话,是 ,但这在这里既不可行也不相关。var
3赞 Willem van der Veen 9/30/2018 #24

ES5及更早版本:

Javascript 中的变量最初是(前)词法函数范围的。术语词法范围意味着您可以通过“查看”代码来查看变量的范围。ES6

使用关键字声明的每个变量的范围都限定为函数。但是,如果在该函数中声明了其他函数,则这些函数将有权访问外部函数的变量。这称为作用域链。它的工作方式如下:var

  1. 当一个函数想要解析一个变量值时,它首先会查看自己的作用域。这是函数体,即大括号 {} 之间的所有内容(此范围内其他函数中的变量除外)。
  2. 如果它在函数体内找不到变量,它将爬到链上,并查看定义函数的函数中的变量作用域。这就是词法范围的含义,我们可以在定义此函数的代码中看到,因此只需查看代码即可确定范围链。

例:

// global scope
var foo = 'global';
var bar = 'global';
var foobar = 'global';

function outerFunc () {
 // outerFunc scope
 var foo = 'outerFunc';
 var foobar = 'outerFunc';
 innerFunc();
 
 function innerFunc(){
 // innerFunc scope
  var foo = 'innerFunc';
  console.log(foo);
  console.log(bar);
  console.log(foobar);
  }
}

outerFunc();

当我们尝试将变量 、 和 记录到控制台时会发生什么:foobarfoobar

  1. 我们尝试将 foo 记录到控制台,foo 可以在函数本身中找到。因此,foo 的值被解析为字符串 。innerFuncinnerFunc
  2. 我们尝试将 bar 记录到控制台,但在函数本身中找不到 bar。因此,我们需要攀登范围链。我们首先看一下定义函数的外部函数。这是函数。在 的范围内,我们可以找到变量 bar,它包含字符串 'outerFunc'。innerFuncinnerFuncouterFuncouterFunc
  3. 在 innerFunc 中找不到 foobar。.因此,我们需要将作用域链爬升到 innerFunc 作用域。在这里也找不到它,我们爬上了另一个层次,达到了全球范围(即最外层的范围)。我们在这里找到变量 foobar,它保存字符串 'global'。如果它在爬升作用域链后找不到变量,JS 引擎将抛出 referenceError

ES6(ES 2015) 及更早版本:

词法范围和作用域链的相同概念仍然适用于 。但是,引入了一种新的声明变量的方法。有以下几种:ES6

  • let:创建块范围变量
  • const:创建一个块范围的变量,该变量必须初始化且不能重新分配

和 / 之间的最大区别在于是函数范围,而 / 是块范围。下面是一个示例来说明这一点:varletconstvarletconst

let letVar = 'global';
var varVar = 'global';

function foo () {
  
  if (true) {
    // this variable declared with let is scoped to the if block, block scoped
    let letVar = 5;
    // this variable declared with let is scoped to the function block, function scoped
    var varVar = 10;
  }
  
  console.log(letVar);
  console.log(varVar);
}


foo();

在上面的示例中,letVar 记录值 global,因为声明的变量是块范围的。它们在各自的块之外不再存在,因此无法在 if 块之外访问变量。let

14赞 CertainPerformance 12/31/2019 #25

内联处理程序

前端编码人员经常遇到的一个尚未描述的非常常见的问题是 HTML 中内联事件处理程序可见的范围 - 例如,使用

<button onclick="foo()"></button>

属性可以引用的变量的范围必须是:on*

  • 全局(工作内联处理程序几乎总是引用全局变量)
  • 文档的属性(例如,作为独立变量将指向;罕见)querySelectordocument.querySelector
  • 处理程序附加到的元素的属性(如上所述;罕见)

否则,在调用处理程序时,将收到 ReferenceError。因此,例如,如果内联处理程序引用了在 or 中定义的函数,则引用将失败,因为内联处理程序可能只引用全局范围内的变量,并且该函数不是全局的:window.onload$(function() {

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

处理程序附加到的元素的属性和属性也可以作为内联处理程序中的独立变量引用,因为内联处理程序是在两个带有块的块中调用的,一个用于 ,一个用于元素。这些处理程序中的变量范围链非常不直观,并且工作事件处理程序可能需要一个函数是全局函数(并且可能应该避免不必要的全局污染)。documentdocument

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

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

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

另一方面,与在顶层运行的普通标签不同,ES6 模块中的代码在自己的私有范围内运行。在普通标签顶部定义的变量是全局变量,因此您可以在其他标签中引用它,如下所示:<script><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 模块的顶层类似于普通 .该模块可以引用任何全局变量,除非模块是显式为它设计的,否则任何变量都不能引用模块内的任何内容。<script>

1赞 Ahmed Khashaba 5/26/2020 #26

我真的很喜欢公认的答案,但我想补充一下:

Scope 收集并维护所有声明的标识符(变量)的查找列表,并强制执行一组严格的规则,规定当前正在执行的代码如何访问这些标识符(变量)。

作用域是一组规则,用于按标识符名称查找变量。

  • 如果在直接作用域中找不到变量,则 Engine 会查询下一个外部包含作用域,一直持续到找到或到达最外层(也称为全局)作用域。
  • 确定在何处以及如何查找变量(标识符)的规则集。此查找可能用于分配给变量,该变量是 LHS(左侧)引用,也可能用于检索其值,即 RHS(右侧)引用。
  • LHS 引用由赋值操作产生。与作用域相关的赋值可以使用 = 运算符进行,也可以通过将参数传递给(赋值给)函数参数来实现。
  • JavaScript 引擎在执行之前首先编译代码,在这样做时,它会拆分诸如 var a = 2 之类的语句;分为两个独立的步骤:第一步。首先,var a 在该范围内声明它。这是在代码执行之前的开头执行的。2nd. 稍后,a = 2 查找变量(LHS 参考)并在找到时赋值给它。
  • LHS 和 RHS 引用查找都从当前正在执行的作用域开始,如果需要(也就是说,它们在那里找不到要查找的内容),它们会沿着嵌套作用域向上工作,一次一个作用域(楼层),查找标识符,直到它们到达全局(顶层)并停止,然后找到它, 或者不要。未完成的 RHS 引用会导致引发 ReferenceError。未实现的 LHS 引用会导致自动隐式创建该名称的全局(如果未在严格模式下)或 ReferenceError(如果在严格模式下)。
  • 作用域由一系列“气泡”组成,每个气泡都充当容器或桶,其中声明了标识符(变量、函数)。这些气泡整齐地嵌套在彼此内部,并且此嵌套是在创作时定义的。
0赞 ludy 6/28/2021 #27

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

//but, like this
(function foo() {
    console.log('1:', foo) // function foo
    foo = 100
    console.log('2:', foo) // function foo, is not 100, why?
})()