乐闻世界logo
搜索文章和话题

What is the scope of variables in javascript

7 个月前提问
3 个月前修改
浏览次数54

6个答案

1
2
3
4
5
6

在JavaScript中,变量的作用域决定了在代码的哪些部分可以访问该变量。作用域可以是全局的或者局部的。

  1. 全局作用域: 当一个变量在函数之外被声明时,它拥有全局作用域。这意味着它可以在代码的任何地方被访问和修改。全局变量在整个web页面的生命周期内都是可用的。

    例如:

    javascript
    var globalVar = "这是一个全局变量"; function demoFunction() { console.log(globalVar); // 可以访问全局变量 } demoFunction(); // 输出: 这是一个全局变量

    上面的 globalVar 就是一个全局变量,它可以在 demoFunction 函数内部被访问。

  2. 局部作用域: 当一个变量在函数内部被声明时,它拥有局部作用域,也就是说它只能在该函数内部被访问和修改。局部变量对于函数外部的代码来说是隐藏的。

    例如:

    javascript
    function demoFunction() { var localVar = "这是一个局部变量"; console.log(localVar); // 可以访问局部变量 } demoFunction(); // 输出: 这是一个局部变量 // console.log(localVar); // 错误:localVar 在这里是不可访问的

    localVar 只能在 demoFunction 函数内部被访问。

另外,JavaScript ES6 引入了两个新的关键字 letconst 来声明变量,它们提供了块级作用域(block scope)。

  1. 块级作用域: 使用 letconst 声明的变量具有块级作用域。它们只能在包含它们的代码块(如一个for循环或者if语句)内部被访问。

    例如:

    javascript
    if (true) { let blockScopedVar = "这是一个块级作用域的变量"; console.log(blockScopedVar); // 可以访问块级作用域的变量 } // console.log(blockScopedVar); // 错误:blockScopedVar 在这里是不可访问的

在开发实际应用时,通常建议尽可能使用局部作用域或块级作用域来声明变量,以避免全局变量可能带来的命名冲突和难以追踪的bug。

2024年6月29日 12:07 回复

总长DR

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 函数作用域,除非它们直接在全局上下文中声明,在这种情况下,它们将作为属性添加到全局对象上并具有全局作用域。它们在eval函数中的使用有单独的规则。

让和常量

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

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

shell
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字符串中,使用 using 声明的变量var将放置在当前范围内,或者,如果eval间接使用,则作为全局对象的属性。

例子

下面的代码将引发 ReferenceError,因为名称xy、 和z在函数之外没有任何意义f

shell
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下面的代码将为and抛出一个 ReferenceError z,但对于 ,则不会x,因为 的可见性x不受块的约束。定义控制结构体(如ifforwhile)的块的行为类似。

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

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

运行代码片段Hide results

展开片段

...由于这种行为,您需要小心关闭使用varin 循环声明的变量。这里只声明了一个变量实例x,并且它在逻辑上位于循环之外。

下面的代码打印5, 五次,然后在循环外打印5第六次:console.log

shell
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),因此会0打印整数4

shell
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

展开片段

下面的代码不会抛出 a ReferenceError,因为 的可见性x不受块的约束;但是,它会打印,undefined因为变量尚未初始化(由于该if语句)。

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

运行代码片段Hide results

展开片段

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

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

运行代码片段Hide results

展开片段

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

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

运行代码片段Hide results

展开片段

使用var,let或声明的变量const都限定在模块范围内:

shell
// module1.js var x = 0 export function f() {} //module2.js import f from 'module1.js' console.log(x) // throws ReferenceError

以下将在全局对象上声明一个属性,因为var在全局上下文中声明的变量将作为属性添加到全局对象中:

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

运行代码片段Hide results

展开片段

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

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

运行代码片段Hide results

展开片段

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

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

运行代码片段Hide results

展开片段

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

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

运行代码片段Hide results

展开片段

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

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

运行代码片段Hide results

展开片段

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

shell
x = 1 // implicitly defined property on the global object (no "var"!) console.log(x) // 1 console.log(window.hasOwnProperty('x')) // true

运行代码片段Hide results

展开片段

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

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

运行代码片段Hide results

展开片段

它是如何工作的

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

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

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

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

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

了解更多

2024年6月29日 12:07 回复

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

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

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

2024年6月29日 12:07 回复

全局声明的变量具有全局作用域。函数内声明的变量的作用域为该函数,以及同名的影子全局变量。

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

2024年6月29日 12:07 回复

老派 JavaScript

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

  1. 全局范围:从应用程序启动时起,变量在整个应用程序中都是已知的_(*)_
  2. 功能范围:变量在其声明的函数内是已知的,从函数的开头开始_(*)_

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


现代 JavaScript

最新的 JavaScript 规范现在还允许第三个范围:

  1. 块作用域:标识符在其声明的作用域顶部是“已知的” ,但在其声明行之后才能对其进行赋值或取消引用(读取)。这个过渡时期被称为“暂时死区”。

如何创建块作用域变量?

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

shell
var myVariable = "Some text";

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

shell
let myVariable = "Some text";

那么函数作用域和块作用域有什么区别呢?

要了解函数作用域和块作用域之间的区别,请考虑以下代码:

shell
// 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及以下版本
    • 火狐浏览器 43及以下版本
    • Safari 9及以下版本
    • Android 浏览器 4及以下版本
    • Opera 27及以下版本
    • 40丁目以下
    • 任何版本的Opera MiniBlackberry 浏览器

在此输入图像描述


如何跟踪浏览器支持

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


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

2024年6月29日 12:07 回复

这是一个例子:

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

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

2024年6月29日 12:07 回复

你的答案