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


当前回答

几乎只有两种类型的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子句显然是另一个例外,但不鼓励使用with子句(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);

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

其他回答

1) 有一个全局作用域、一个函数作用域以及with和catch作用域。一般来说,变量的作用域没有“块”级别的作用域——with和catch语句为其块添加名称。

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

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

编辑:ECMAAScript 6(Harmony)被指定为支持let,我知道chrome允许一个“和谐”标志,所以也许它确实支持它。。

这将是对块级范围的支持,但必须使用关键字才能实现。

编辑:根据本杰明在评论中指出的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.test1Vartest1'在上层词法上下文(函数或全局)中创建一个新的变量test1,除非它是一个属性——它就是这样。

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

我的理解是有三个范围:全球范围,全球可用;局部范围,无论块如何,整个功能都可用;和块范围,仅对使用它的块、语句或表达式可用。全局和局部作用域用关键字“var”表示,无论是在函数内还是在函数外,块作用域都用关键字“let”表示。

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

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

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

TLDR

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

这四个范围是:

全局-所有内容都可见函数-在函数(及其子函数和块)中可见块-在块(及其子块)中可见模块-在模块内可见

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

概述

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

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

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

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

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

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

如何声明标识符声明标识符的位置无论您处于严格模式还是非严格模式

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

var、let和const功能参数Catch块参数函数声明命名函数表达式全局对象上隐式定义的财产(即,在非限定模式中缺少var)导入语句评估

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

全球背景功能体普通砌块控制结构的顶部(例如,循环、if、while等)控制结构体模块

声明样式

var

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

let和const

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

注:let、const和var均已吊装。这意味着其定义的逻辑位置是其封闭范围(块或函数)的顶部。然而,在控件通过源代码中的声明点之前,不能读取或分配使用let和const声明的变量。过渡期被称为时间死区。

函数f(){函数g(){console.log(x)}设x=1g()}f()//1,因为即使用“let”!

函数参数名称

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

函数声明

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

命名函数表达式

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

全局对象上隐式定义的财产

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

eval

在eval字符串中,使用var声明的变量将放在当前范围内,如果间接使用eval,则作为全局对象上的财产。

示例

下面将抛出ReferenceError,因为namesx、y和z在函数f之外没有意义。

函数f(){变量x=1设y=1常量z=1}console.log(typeof x)//未定义(因为var具有函数范围!)console.log(typeof y)//未定义(因为函数的主体是一个块)console.log(typeof z)//未定义(因为函数的主体是一个块)

下面将为y和z抛出ReferenceError,但不会为x抛出,因为x的可见性不受块的约束。定义if、for和while等控制结构体的块的行为类似。

{变量x=1设y=1常量z=1}console.log(x)//1console.log(typeof y)//未定义,因为“y”具有块范围console.log(typeof z)//未定义,因为“z”具有块范围

在下文中,x在循环外可见,因为var具有函数范围:

对于(var x=0;x<5;++x){}console.log(x)//5(注意这是在循环之外!)

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

以下命令为循环外的console.log打印5次,然后第六次打印5次:

对于(var x=0;x<5;++x){setTimeout(()=>console.log(x))//在逻辑上位于封闭范围顶部的“x”上关闭,在循环上方}console.log(x)//注意:在循环外可见

以下打印未定义,因为x是块范围的。回调是一个接一个异步运行的。let变量的新行为意味着每个匿名函数都在一个名为x的不同变量上关闭(与var不同),因此会打印整数0到4

for(设x=0;x<5;++x){setTimeout(()=>console.log(x))//“let”声明是在每次迭代的基础上重新声明的,因此闭包捕获不同的变量}console.log(typeof x)//未定义

以下内容不会引发ReferenceError,因为x的可见性不受块的约束;但是,它将打印undefined,因为变量尚未初始化(因为if语句)。

if(false){变量x=1}console.log(x)//此处,“x”已声明,但未初始化

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

对于(设x=0;x<10;++x){}console.log(typeof x)//未定义,因为“x”是块范围的

以下内容将引发ReferenceError,因为x的可见性受到块的约束:

if(false){设x=1}console.log(typeof x)//未定义,因为“x”是块范围的

使用var、let或const声明的变量的作用域都是模块:

// module1.js

var x = 0
export function f() {}

//module2.js

import f from 'module1.js'

console.log(x) // throws ReferenceError

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

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

全局上下文中的let和const不向全局对象添加财产,但仍具有全局范围:

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

可以考虑在函数体中声明函数参数:

函数f(x){}console.log(typeof x)//未定义,因为“x”的作用域是函数

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

尝试{}捕获(e){}console.log(typeof e)//未定义,因为“e”的作用域是catch块

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

(函数foo(){console.log(foo)})()console.log(typeoffoo)//未定义,因为“foo”的作用域是它自己的表达式

在非限定模式下,全局对象上隐式定义的财产是全局范围的。在严格模式下,会出现错误。

x=1//隐式定义全局对象的属性(无“var”!)console.log(x)//1console.log(window.hasOwnProperty('x'))//true

在非严格模式下,函数声明具有函数范围。在严格模式下,它们具有块范围。

'使用严格'{函数foo(){}}console.log(foo类型)//未定义,因为“foo”是块范围的

发动机罩下的工作原理

作用域定义为标识符有效的代码词汇区域。

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

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

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

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

了解更多信息。

几乎只有两种类型的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子句显然是另一个例外,但不鼓励使用with子句(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);

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