JavaScript 中变量的范围是什么?

2024-11-02 20:49:00
admin
原创
45
摘要:问题描述:javascript 中变量的作用域是什么?它们在函数内部和外部的作用域是否相同?或者这有关系吗?此外,如果变量是全局定义的,它们存储在哪里?解决方案 1:结论JavaScript 具有词法(也称为静态)作用域和闭包。这意味着您可以通过查看源代码来判断标识符的作用域。这四个范围是:全局 - 一切皆可...

问题描述:

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


解决方案 1:

结论

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

这四个范围是:

  1. 全局 - 一切皆可见

  2. 功能 - 在功能(及其子功能和块)内可见

  3. 块 - 在块(及其子块)内可见

  4. 模块 - 在模块内可见

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

概述

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

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

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

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

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

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

  1. 如何声明标识符

  2. 标识符声明的位置

  3. 无论你是处于严格模式还是非严格模式

声明标识符的一些方法如下:

  1. varletconst

  2. 函数参数

  3. 捕获块参数

  4. 函数声明

  5. 命名函数表达式

  6. var全局对象上隐式定义的属性(即在非严格模式下缺失)

  7. import语句

  8. eval

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

  1. 全球背景

  2. 函数主体

  3. 普通方块

  4. 控制结构的顶部(例如 loop、if、while 等)

  5. 控制结构体

  6. 模块

声明样式

变量

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

let 和 const

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

注意: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`!

运行代码片段Hide results展开片段

函数参数名称

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

函数声明

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

命名函数表达式

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

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

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

评估

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)

运行代码片段Hide results展开片段

y以下代码将对和抛出 ReferenceError z,但对 不会x,因为 的可见性x不受块约束。定义iffor和等控制结构主体的块的while行为类似。

{
    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

运行代码片段Hide results展开片段

下面,由于具有函数范围,x因此在循环外部可见:var

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

运行代码片段Hide results展开片段

...由于这种行为,您需要小心关闭在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

运行代码片段Hide results展开片段

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

运行代码片段Hide results展开片段

以下不会抛出,ReferenceError因为的可见性x不受块的限制;但是,它会打印,undefined因为变量尚未初始化(因为语句if)。

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

运行代码片段Hide results展开片段

在循环顶部for使用声明的变量的let作用域是循环主体:

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

运行代码片段Hide results展开片段

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

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

运行代码片段Hide results展开片段

var使用、let或声明的变量const均作用于模块:

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

运行代码片段Hide results展开片段

let并且const在全局上下文中不向全局对象添加属性,但仍然具有全局范围:

let x = 1
console.log(window.hasOwnProperty('x')) // false

运行代码片段Hide results展开片段

函数参数可以认为是在函数体中声明的:

function f(x) {}
console.log(typeof x) // undefined, because `x` is scoped to the function

运行代码片段Hide results展开片段

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

try {} catch(e) {}
console.log(typeof e) // undefined, because `e` is scoped to the catch block

运行代码片段Hide results展开片段

命名函数表达式的作用域仅限于表达式本身:

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

运行代码片段Hide results展开片段

在非严格模式下,全局对象上隐式定义的属性具有全局作用域。在严格模式下,您会收到错误。

x = 1 // implicitly defined property on the global object (no "var"!)

console.log(x) // 1
console.log(window.hasOwnProperty('x')) // true

运行代码片段Hide results展开片段

在非严格模式下,函数声明具有函数作用域。在严格模式下,它们具有块作用域。

'use strict'
{
    function foo() {}
}
console.log(typeof foo) // undefined, because `foo` is block-scoped

运行代码片段Hide results展开片段

其内部工作原理

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

在 JavaScript 中,每个函数对象都有一个隐藏的引用,该引用是对其创建时的执行上下文(堆栈框架)的词法环境[[Environment]]的引用。

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

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

因此,在 JavaScript 中,作用域是通过由外部引用链接在一起的“链”中的词法环境来实现的。这个词法环境链称为作用域链,标识符解析是通过在链中向上搜索匹配的标识符进行的。

了解更多信息。

解决方案 2:

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

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

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

解决方案 3:

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

(我相信真正的 JavaScript 程序员能够在其他答案中指出许多微妙之处。特别是我偶然发现了这个关于任何时候的确切含义的页面。希望这个更具介绍性的链接足以帮助您入门。)this

解决方案 4:

老式 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**及以下版本
+ **40丁目**以下
+ 任何版本的**Opera Mini**和**Blackberry 浏览器**

在此处输入图片描述


如何跟踪浏览器支持情况

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


)全局和功能范围的变量可以在声明之前初始化和使用,因为 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>

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

解决方案 6:

据我所知,关键在于 Javascript 具有函数级别作用域,而 C 则具有更常见的块作用域。

这是一篇有关该主题的好文章。

解决方案 7:

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

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

解决方案 8:

JavaScript 中作用域的概念最初由Brendan Eich设计,源自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,这是调用时在警报中引用的值。

在此处输入图片描述

解决方案 9:

1) 有全局作用域、函数作用域以及 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 的属性——事实上它就是 a。

哎呀!使用“with”时要小心——就像如果变量已经在函数中定义,var 就是 noop 一样,对于从对象导入的名称,它也是 noop!稍微注意一下已经定义的名称会使这更安全。我个人永远不会因为这个原因使用 with。

解决方案 10:

内联处理程序

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

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

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

  • 全局(工作内联处理程序几乎总是引用全局变量)

  • 文档的属性(例如,querySelector作为独立变量将指向document.querySelector;罕见)

  • 处理程序所附加到的元素的属性(如上所示;很少见)

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

显示代码片段

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

Run code snippetHide resultsExpand snippet

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

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

显示代码片段

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

Run code snippetHide resultsExpand snippet

模块(<script type="module">

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

显示代码片段

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

Run code snippetHide resultsExpand snippet

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

Run code snippetHide resultsExpand snippet

ES6 模块的顶层与普通的 IIFE 内部的顶层类似<script>。模块可以引用任何全局变量,除非模块明确设计,否则任何东西都不能引用模块内部的任何内容。

解决方案 11:

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

尝试该功能:

请参阅以下演示:

查看代码:

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

解决方案 12:

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

  1. 全局范围:全局只不过是一个窗口级别范围。在这里,变量存在于整个应用程序中。

  2. 功能范围:在函数内用var关键字声明的变量具有功能范围。

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

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

作用域链 -->

  1. 窗口级别 -aouter功能位于范围链的顶层。

  2. 当外部函数调用一个新的variable scope object(并包含在作用域链中)时,在其内部添加变量b

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

解决方案 13:

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

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 + 
          "
Local Scope : " + Name + 
          "
Object Scope : " + this.Name + 
          "
Current document Scope : " + document.Name
         );
}


testFun.call(myObj);
})(window,document);

解决方案 14:

全球范围:

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

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

请参阅本文以深入了解范围

解决方案 15:

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

在第一个例子中,没有块作用域,因此最初声明的变量被覆盖。在第二个例子中,由于函数而产生了新的作用域,因此最初声明的变量被隐藏,并且没有被覆盖。

这就是你在 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);

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

解决方案 16:

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

因此从这个角度来看,我认为 Kyle Simpson 所著的《作用域与闭包》电子书中的一张图片会有所帮助:

图像

引用他的电子书:

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

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

这种“作用域级别”的概念解释了为什么如果在嵌套函数中查找“this”,则可以使用新创建的作用域来更改它。以下链接详细介绍了所有这些细节,您想了解的有关 JavaScript 作用域的一切

解决方案 17:

现代 Js、ES6+、' const' 和 ' let'

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

const95% 的情况都应该使用。这样变量引用就不会改变。数组、对象和 DOM 节点属性可能会改变,因此应该const

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

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

解决方案 18:

试试这个有趣的例子。在下面的例子中,如果 a 是一个初始化为 0 的数字,你会看到 0 然后是 1。但 a 是一个对象,javascript 会将 a 的指针而不是它的副本传递给 f1。结果是你两次都会收到相同的警报。

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

解决方案 20:

我的理解是,作用域有 3 个:全局作用域,全局可用;局部作用域,可用于整个函数,无论块如何;块作用域,仅适用于使用它的块、语句或表达式。全局和局部作用域用关键字“var”表示,可以在函数内或函数外,块作用域用关键字“let”表示。

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

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

解决方案 21:

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

运行代码片段Hide results展开片段

当我们尝试将变量foobar和记录foobar到控制台时,发生的情况如下:

  1. 我们尝试将 foo 打印到控制台,foo 可以在函数innerFunc本身内部找到。因此,foo 的值被解析为字符串innerFunc

  2. 我们尝试将 bar 打印到控制台,但在函数本身内部找不到 bar innerFunc。因此,我们需要爬上作用域链。我们首先查看innerFunc定义该函数的外部函数。这就是函数outerFunc。在作用域中,outerFunc我们可以找到变量 bar,它保存着字符串“outerFunc”。

  3. 在 innerFunc 中找不到 foobar。因此,我们需要沿着作用域链爬到innerFunc 作用域。这里也找不到它,我们再爬一层到全局作用域(即最外层作用域)。我们在这里找到了变量 foobar,它包含字符串 'global'。如果爬过作用域链后仍找不到该变量,JS 引擎将抛出referenceError

ES6(ES 2015)及更早版本:

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

  • let:创建一个块范围变量

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

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

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

运行代码片段Hide results展开片段

在上面的例子中, letVar 记录的值是 global,因为用 声明的变量let是块作用域的。它们在各自的块之外不复存在,因此无法在 if 块之外访问该变量。

解决方案 22:

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

  • 本地范围

  • 全球范围

下面的函数有一个局部作用域变量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 
    }
}

解决方案 23:

我真的很喜欢这个被接受的答案,但我想补充一点:

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

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

  • 如果在直接作用域中找不到变量,引擎将查询下一个外部包含作用域,继续执行直到找到或到达最外层(又称全局)作用域。

  • 是一组规则,用于确定在何处以及如何查找变量(标识符)。此查找可能是为了分配给变量(即 LHS(左侧)引用),也可能是为了检索其值(即 RHS(右侧)引用)。

  • LHS 引用来自赋值运算。与作用域相关的赋值可以通过 = 运算符或通过将参数传递给(赋值给)函数参数来实现。

  • JavaScript 引擎首先在执行代码之前对其进行编译,在此过程中,它将 var a = 2; 之类的语句拆分为两个单独的步骤:第一。首先,使用 var a 在该范围内对其进行声明。此操作在开始时执行,在代码执行之前。第二。之后,使用 a = 2 查找变量(LHS 引用)并在找到后将其赋值。

  • LHS 和 RHS 引用查找均从当前执行的范围开始,如果需要(即它们在那里找不到它们要查找的内容),它们会逐个向上查找嵌套范围,一次查找一个范围(底层),查找标识符,直到它们到达全局(顶层)并停止,找到它或找不到它。未实现的 RHS 引用会导致引发 ReferenceError。未实现的 LHS 引用会导致自动、隐式创建该名称的全局变量(如果未处于严格模式),或引发 ReferenceError(如果处于严格模式)。

  • 作用域由一系列“气泡”组成,每个气泡都充当容器或桶,标识符(变量、函数)在其中声明。这些气泡整齐地嵌套在一起,这种嵌套是在编写时定义的。

解决方案 24:

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

解决方案 25:

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

解决方案 26:

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

运行代码片段Hide results展开片段

解决方案 27:

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

  1. 全局作用域:在全局作用域中声明的变量可以在程序的任何地方非常顺利地使用。例如:

var carName = " BMW";

// code here can use carName

function myFunction() {
     // code here can use carName 
}
  1. 功能范围或局部范围:在此范围内声明的变量只能在其自己的函数中使用。例如:

// code here can not use carName
function myFunction() {
   var carName = "BMW";
   // code here can use carName
}
相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用