JavaScript 中变量的作用域是啥?

Posted

技术标签:

【中文标题】JavaScript 中变量的作用域是啥?【英文标题】:What is the scope of variables in JavaScript?JavaScript 中变量的作用域是什么? 【发布时间】:2022-01-20 00:49:15 【问题描述】:

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

【问题讨论】:

这里有另一个很好的link 来记住这个问题:“Explaining JavaScript scope and closures”。 这里有一篇文章很好地解释了它。 Everything you need to know about Javascript variable scope Kyle Simpson 的previously mentioned 电子书可以在 Github 上阅读,它告诉你所有关于 JavaScript 作用域和闭包的知识。你可以在这里找到它:github.com/getify/You-Dont-Know-JS/blob/master/… 它是 "You don't know JS" book series 的一部分,非常适合所有想了解更多 JavaScript 的人。 var 规则。 javascript不需要'const'和'let'的“添加”,这违背了它的精神。 - 我知道这两个不是你问题的一部分 - 在看到这么多“推动”它们之后不得不添加这个。 【参考方案1】:

TLDR

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

四个作用域是:

    全局 - 一切可见 函数 - 在函数(及其子函数和块)中可见 块 - 在块(及其子块)内可见 模块 - 在模块中可见

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

概述

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

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

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

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

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

在 JavaScript 中决定 scope 和 identifier 的三个相关因素:

    如何声明标识符 声明标识符的位置 无论您是在strict mode 还是non-strict mode

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

    varletconst 函数参数 Catch 块参数 函数声明 命名函数表达式 在全局对象上隐式定义的属性(即,在非严格模式下遗漏了 varimport 声明 eval

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

    全球背景 函数体 普通方块 控制结构的顶部(例如循环、if、while 等) 控制结构体 模块

声明样式

变量

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

let 和 const

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

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

function f() 
    function g() 
        console.log(x)
    
    let x = 1
    g()

f() // 1 because x is hoisted even though declared with `let`!

函数参数名称

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

函数声明

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

命名函数表达式

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

全局对象的隐式定义属性

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

评估

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)

以下内容将为yz 引发ReferenceError,但不会为x 引发ReferenceError,因为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 不同),因此整数 04 被打印出来。:

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

使用letfor 循环顶部声明的变量的作用域为循环体:

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

catch 块参数的作用域是 catch-block 主体:

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

它是如何工作的

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

在 JavaScript 中,每个函数对象都有一个隐藏的 [[Environment]] 引用,它是对在其中创建它的 execution context(堆栈帧)中的 lexical environment 的引用。

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

请注意,新的执行上下文和函数对象的词法环境之间的链接称为closure。

因此,在 JavaScript 中,作用域是通过外部引用以“链”形式链接在一起的词法环境来实现的。这个词法环境链称为作用域链,标识符解析由searching up the chain 进行,以获取匹配的标识符。

找出more。

【讨论】:

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

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

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

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

【讨论】:

作用域链是 [memory] ​​Closures... 的另一个术语,供那些阅读这里学习/进入 javascript 的人使用。【参考方案3】:

全局声明的变量具有全局范围。在函数中声明的变量的作用域是该函数,并且是同名的影子全局变量。

(我确信真正的 JavaScript 程序员可以在其他答案中指出许多微妙之处。特别是我遇到了 this page this 的确切含义。希望 this more introductory link 是足以让你开始。)

【讨论】:

我什至不敢开始回答这个问题。作为一名真正的 Javascript 程序员,我知道答案会很快失控。不错的文章。 @Triptych:我知道你所说的事情失控是什么意思,但是添加一个答案。我只是通过几次搜索得到了上述内容...由具有实际经验的人写的答案必然会更好。请更正我的任何肯定是错误的答案! 不知何故 Jon Skeet 负责我在 Stack Overflow 上最受欢迎的答案。【参考方案4】:

老式 JavaScript

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

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

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


现代 JavaScript

most recent JavaScript specs 现在还允许第三个作用域:

    块范围:标识符是“已知的”from the top of the scope they are declared within,但在声明行之后才能分配或取消引用(读取)。这个过渡时期被称为“时间死区”。

如何创建块范围变量?

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

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 代码并使用基于节点的转译器(如 traceur shell scriptBabel),则可以安全地使用 let 语句。而且因为您的浏览器只会知道转译后的代码,所以性能缺陷应该是有限的。

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

这些浏览器根本不支持let

Internet Explorer 10 及以下版本 Firefox 43 及以下 Safari 9 及以下版本 Android 浏览器 4 及以下版本 Opera 27 及以下 40丁目及以下 Opera MiniBlackberry 浏览器 的任何版本


如何跟踪浏览器支持

有关在您阅读此答案时哪些浏览器支持let 语句的最新概述,请参阅this Can I Use page


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

【讨论】:

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

这是一个例子:

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

您需要研究闭包,以及如何使用它们来制作private members。

【讨论】:

【参考方案6】:

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

Here is a good article on the subject.

【讨论】:

【参考方案7】:

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

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

【讨论】:

是的,但是使用安全吗?我的意思是,如果我的代码将在 WebKit 中运行,我会实际选择这个实现吗? @Python:不,WebKit 不支持let 我想唯一有效的用途是如果您知道所有客户都将使用 Mozilla 浏览器,就像公司内部系统一样。 或者,如果您使用 XUL 框架进行编程,Mozilla 的界面框架,您使用 css、xml 和 javascript 构建。 @GazB 即使这是一个可怕的想法!因此,今天您知道您的客户正在使用 Mozilla,然后发布了一份新备忘录,说明他们现在正在使用其他东西。 IE。我们的支付系统很糟糕的原因...您必须使用 IE8,而不能使用 IE9、IE10 或 Firefox 或 Chrome,因为它完全无法正常工作...【参考方案8】:

最初由Brendan Eich 设计时,JavaScript 中作用域的想法来自HyperCard 脚本语言HyperTalk。

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

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

this 绑定将指向包含对象。有时作用域或执行上下文会发生变化,但包含对象不会发生变化,例如在声明的函数中,包含对象可能是 window 或构造函数。

这些执行上下文是在控制转移的任何时候创建的。当代码开始执行时,控制权就会转移,这主要是通过函数执行来完成的。

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

范围在技术上是“执行上下文” 上下文构成存储变量的环境堆栈 堆栈顶部优先(底部是全局上下文) 每个函数都会创建一个执行上下文(但并不总是一个新的 this 绑定)

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

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

【讨论】:

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

1) 有一个全局作用域、一个函数作用域以及 with 和 catch 作用域。变量通常没有“块”级作用域——with 和 catch 语句将名称添加到它们的块中。

2) 范围由函数一直嵌套到全局范围。

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

编辑:ECMAAScript 6 (Harmony) 被指定支持 let,我知道 chrome 允许使用“harmony”标志,所以也许它确实支持它..

Let 将成为对块级作用域的支持,但您必须使用关键字来实现它。

编辑:根据 Benjamin 指出 cmets 中的 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 是一个 noop 如果变量已经在函数中定义,它也是一个 noop 相对于从对象导入的名称!对已经定义的名称稍加注意会使这更安全。因此,我个人永远不会使用 with。

【讨论】:

这里有一些错误,因为 JavaScript 确实有块作用域的形式。 我的耳朵(眼睛)是张开的,本杰明——我上面的陈述是我一直在处理 Javascript 范围,但它们不是基于阅读规范。我希望你不是指 with 语句(它是一种对象作用域的形式),或者 Mozilla 的特殊 'let' 语法。 好吧,with 语句 块作用域的一种形式,但 catch 子句是一种更常见的形式(有趣的是,v8 使用 @ 实现了 catch 987654325@) - 这几乎是 JavaScript 本身中唯一的块作用域形式(即函数、全局、try/catch、with 及其派生类),但是宿主环境有不同的作用域概念 - 例如浏览器中的内联事件和 NodeJS 的 vm 模块。 Benjamin——据我所见, with 和 catch 都只将对象引入当前范围(以及属性),但是在各自的块结束后,变量被重置。但是例如,在 catch 中引入的新变量将具有封闭函数/方法的范围。 这正是块作用域的含义:)【参考方案10】:

我发现许多 JavaScript 新手很难理解在语言中默认情况下继承是可用的,并且到目前为止,函数范围是唯一的范围。我为去年年底编写的美化器 JSPretty 提供了一个扩展。代码中的功能颜色函数作用域并始终将颜色与该作用域中声明的所有变量相关联。当具有来自一个范围的颜色的变量在不同的范围中使用时,可以直观地演示闭包。

在以下位置试用该功能:

http://prettydiff.com/jspretty.xhtml?c=white&jsscope

查看演示:

http://prettydiff.com/jspretty.xhtml?c=white&jsscope&s=http://prettydiff.com/lib/markup_beauty.js

查看代码:

http://prettydiff.com/lib/jspretty.js https://github.com/austincheney/Pretty-Diff/blob/master/lib/jspretty.js

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

【讨论】:

不适用于 Firefox 26。我粘贴代码或加载文件,单击执行,但没有任何反应。 作用域和继承是两个不同的东西。【参考方案11】:

JavaScript 只有两种作用域:

    全局范围:全局只是一个窗口级别的范围。在这里,变量存在于整个应用程序中。 函数作用域:在函数中用var 关键字声明的变量具有函数作用域。

每当调用函数时,都会创建一个变量范围对象(并包含在范围链中),然后是 JavaScript 中的变量。

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

作用域链 -->

    窗口级别 - aouter 函数位于作用域链的顶层。 当外部函数调用一个新的variable scope object(并包含在作用域链中)时,在其中添加了变量b

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

【讨论】:

不确定为什么这不是公认的答案。实际上只有功能范围(在 ECMA6 之前没有“本地范围”)和全局绑定【参考方案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);

【讨论】:

【参考方案13】:

全球范围:

全局变量就像全球明星(成龙,纳尔逊曼德拉)。您可以从应用程序的任何部分访问它们(获取或设置值)。全局函数就像全局事件(新年、圣诞节)。您可以从应用程序的任何部分执行(调用)它们。

//global variable
var a = 2;

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

本地范围:

如果您在美国,您可能认识臭名昭著的名人金卡戴珊(她不知何故设法制作了小报)。但是美国以外的人不会认出她。她是当地的明星,与她的领土息息相关。

局部变量就像局部星星。您只能在范围内访问它们(获取或设置值)。本地函数就像本地事件 - 您只能在该范围内执行(庆祝)。如果你想从范围之外访问它们,你会得到一个引用错误

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    

Check this article for in-depth understanding of scope

【讨论】:

【参考方案14】:

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

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

引用他的电子书:

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

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

“范围级别”的这种想法解释了为什么“this”可以通过新创建的范围进行更改,如果它在嵌套函数中进行查找。 这是一个包含所有这些细节的链接,Everything you wanted to know about javascript scope

【讨论】:

【参考方案15】:

内联处理程序

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

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

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

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

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

window.addEventListener('DOMContentLoaded', () => 
  function foo() 
    console.log('foo running');
  
);
&lt;button onclick="foo()"&gt;click&lt;/button&gt;

document 的属性和处理程序附加到的元素的属性也可以作为内联处理程序内的独立变量引用,因为内联处理程序被调用 inside of two with blocks,一个用于 document,一个用于元素。这些处理程序中变量的作用域链是extremely unintuitive,一个工作的事件处理程序可能需要一个全局函数(以及不必要的全局污染should probably be avoided)。

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

function foo() 
  console.log('foo running');

document.querySelector('.my-button').addEventListener('click', foo);
&lt;button class="my-button"&gt;click&lt;/button&gt;

模块 (&lt;script type="module"&gt;)

另一方面,与在顶层运行的普通&lt;script&gt; 标签不同,ES6 模块内的代码在其自己的私有范围内运行。在普通&lt;script&gt; 标记顶部定义的变量是全局变量,因此您可以在其他&lt;script&gt; 标记中引用它,如下所示:

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

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

<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 模块的顶层类似于普通&lt;script&gt; 中顶层IIFE 的内部。模块可以引用任何全局变量,除非模块是为它明确设计的,否则任何东西都不能引用模块内部的任何东西。

【讨论】:

【参考方案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 作用域的全部内容,除了:

try/catch 只为异常变量本身引入新作用域,其他变量没有新作用域 with-clause 显然是另一个例外,但非常不鼓励使用 with-clause (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with)

因此,您可以看到 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);

这可能看起来违反直觉,但从命令式语言设计者的角度来看,这是有道理的。

【讨论】:

【参考方案17】:

现代 Js、ES6+、“const”和“let

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

const 应该用于95% 的情况。它使得变量 reference 不能改变。数组、对象和 DOM 节点属性可以更改,并且应该是 const

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

块范围意味着变量只能在声明它的括号内可用。这扩展到内部范围,包括在您的范围内创建的匿名函数。

【讨论】:

关于var的问题。 "const" 和 "let" = javascript 的破坏者,不在问题之列,也不应该在 javascript 中... 您的意见不会改变答案的有效性。问题是关于范围界定。 constlet 是定义范围的 var 的现代替代品。提出问题时都不存在【参考方案18】:

试试这个奇怪的例子。在下面的示例中,如果 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】:

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

【讨论】:

(自发布答案很久以来)块范围;developer.mozilla.org/en/docs/Web/JavaScript/Reference/…【参考方案20】:

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

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

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

【讨论】:

【参考方案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 
    

【讨论】:

const 和 let 不在问题之列。你为什么要提起他们?问题是关于 vars... const 和 let 很烦人并破坏 javascript。【参考方案22】:

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

Scope 收集并维护所有已声明标识符(变量)的查找列表,并对当前执行的代码如何访问这些标识符(变量)实施一套严格的规则。

范围是一组通过标识符名称查找变量的规则。

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

【讨论】:

【参考方案23】:

ES5 及更早版本:

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

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

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

示例:

// 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 记录到控制台时会发生以下情况:

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

ES6 (ES 2015) 及更早版本:

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

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

varlet/const 之间的最大区别在于 var 是函数作用域,而 let/const 是块作用域。下面是一个例子来说明这一点:

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,因为使用 let 声明的变量是块范围的。它们不再存在于各自的块之外,因此无法在 if 块之外访问变量。

【讨论】:

【参考方案24】:

(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?
)()

【讨论】:

【参考方案25】:

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

【讨论】:

【参考方案26】:

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

【讨论】:

const 和 let 不是问题的一部分。为什么要提起他们?就个人而言,它们不属于 javascript...【参考方案27】:

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

    全局作用域:在全局作用域内声明的变量可以非常流畅的在程序的任何地方使用。例如:

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

    函数作用域或局部作用域:在这个作用域中声明的变量只能在它自己的函数中使用。例如:

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

【讨论】:

以上是关于JavaScript 中变量的作用域是啥?的主要内容,如果未能解决你的问题,请参考以下文章

如何通过 c:set 将参数传递给 jsp:include? JSP中变量​​的作用域是啥?

python里面变量作用域是啥?

你不知道的JavaScript-作用域是什么

Javascript中的作用域问题

作用域是什么?

vuejs methods中的方法互相调用时变量的作用域是