javascript中变量的范围是什么?它们在函数内部和外部具有相同的范围吗?或者它甚至重要吗?另外,如果变量是全局定义的,它们存储在哪里?
var
规则。 javascript不需要'const'和'let'的“添加”,这违背了它的精神。 - 我知道这两个不是你问题的一部分 - 在看到这么多“推”它们之后不得不添加这个。
TLDR
JavaScript 具有词法(也称为静态)作用域和闭包。这意味着您可以通过查看源代码来判断标识符的范围。
四个范围是:
全局 - 对任何事物都可见 功能 - 在函数(及其子函数和块)内可见 块 - 在块(及其子块)内可见 模块 - 在模块内可见
在全局和模块范围的特殊情况之外,使用 var
(函数范围)、let
(块范围)和 const
(块范围)声明变量。大多数其他形式的标识符声明在严格模式下具有块范围。
概述
范围是标识符有效的代码库区域。
词法环境是标识符名称和与之关联的值之间的映射。
范围由词汇环境的链接嵌套构成,嵌套中的每一级对应于祖先执行上下文的词汇环境。
这些链接的词法环境形成了一个作用域“链”。标识符解析是沿着该链搜索匹配标识符的过程。
标识符解析只发生在一个方向:向外。这样,外部词汇环境就无法“看到”内部词汇环境。
决定 JavaScript 中 identifier 的 scope 的三个相关因素:
如何声明标识符 声明标识符的位置 处于严格模式还是非严格模式
可以声明标识符的一些方法:
var、let 和 const 函数参数 Catch 块参数 函数声明 命名函数表达式 在全局对象上隐式定义的属性(即,在非严格模式下丢失 var) import 语句 eval
可以声明一些位置标识符:
全局上下文 函数体 普通块 控制结构的顶部(例如,循环、if、while 等) 控制结构体 模块
声明样式
变量
使用 var
声明的标识符具有函数范围,除非它们直接在全局上下文中声明,在这种情况下,它们作为属性添加到全局对象上并具有全局范围。在 eval
函数中使用它们有单独的规则。
让和常量
使用 let
和 const
声明的标识符具有块范围,除非它们直接在全局上下文中声明,在这种情况下它们具有全局范围。
注意:let
、const
和 var
are all hoisted。这意味着它们的逻辑定义位置是它们封闭范围(块或函数)的顶部。但是,在控制通过源代码中的声明点之前,无法读取或分配使用 let
和 const
声明的变量。过渡时期被称为时间死区。
function f() { function g() { console.log(x) } let x = 1 g() } f() // 1 因为 x 被提升了,即使用 `let` 声明了!
函数参数名称
函数参数名称的范围是函数体。请注意,这有点复杂。声明为默认参数的函数靠近 parameter list,而不是函数体。
函数声明
函数声明在严格模式下具有块作用域,在非严格模式下具有函数作用域。注意:非严格模式是基于不同浏览器古怪的历史实现的一组复杂的紧急规则。
命名函数表达式
命名函数表达式的范围仅限于自身(例如,出于递归的目的)。
全局对象上隐式定义的属性
在非严格模式下,全局对象上隐式定义的属性具有全局范围,因为全局对象位于范围链的顶部。在严格模式下,这些是不允许的。
评估
在 eval
字符串中,使用 var
声明的变量将被放置在当前范围内,或者,如果 eval
被间接使用,则作为全局对象的属性。
例子
以下将引发 ReferenceError,因为名称 x
、y
和 z
在函数 f
之外没有任何意义。
function f() { var x = 1 let y = 1 const z = 1 } console.log(typeof x) // 未定义(因为 var 有函数作用域!) console.log(typeof y) // 未定义(因为主体函数的块是块)console.log(typeof z)//未定义(因为函数的主体是块)
以下将为 y
和 z
引发 ReferenceError,但不会为 x
引发 ReferenceError,因为 x
的可见性不受块的限制。定义控制结构体(如 if
、for
和 while
)的块的行为类似。
{ var x = 1 let y = 1 const z = 1 } console.log(x) // 1 console.log(typeof y) // 未定义,因为 `y` 有块作用域 console.log(typeof z) // 未定义因为 `z` 有块作用域
在下文中,x
在循环外可见,因为 var
具有函数范围:
for(var x = 0; x < 5; ++x) {} console.log(x) // 5(注意这是在循环之外!)
...由于这种行为,您需要小心关闭在循环中使用 var
声明的变量。这里只声明了一个变量 x
的实例,它在逻辑上位于循环之外。
以下将 5
打印五次,然后为循环外的 console.log
打印 5
第六次:
for(var x = 0; x < 5; ++x) { setTimeout(() => console.log(x)) // 关闭逻辑上位于封闭范围顶部的 `x`循环 } console.log(x) // 注意:在循环外可见
以下打印 undefined
因为 x
是块范围的。回调是异步运行的。 let
变量的新行为意味着每个匿名函数都关闭一个名为 x
的不同变量(与 var
不同),因此会打印 0
到 4
的整数。:
for(let x = 0; x < 5; ++x) { setTimeout(() => console.log(x)) // `let` 声明在每次迭代的基础上重新声明,因此闭包捕获不同的变量 } console.log(typeof x) // 未定义
以下不会抛出 ReferenceError
,因为 x
的可见性不受块的限制;但是,它将打印 undefined
,因为变量尚未初始化(因为 if
语句)。
if(false) { var x = 1 } console.log(x) // 这里,`x` 已经被声明了,但是没有被初始化
使用 let
在 for
循环顶部声明的变量的作用域为循环体:
for(let x = 0; x < 10; ++x) {} console.log(typeof x) // 未定义,因为 `x` 是块作用域的
以下将引发 ReferenceError
,因为 x
的可见性受到块的限制:
if(false) { let 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
声明的变量将作为属性添加到全局对象:
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) // 未定义,因为 `x` 的作用域是函数
捕获块参数的范围为捕获块主体:
try {} catch(e) {} console.log(typeof e) // 未定义,因为 `e` 的作用域是 catch 块
命名函数表达式的范围仅限于表达式本身:
(function foo() { console.log(foo) })() console.log(typeof foo) // 未定义,因为 `foo` 的作用域是它自己的表达式
在非严格模式下,全局对象上隐式定义的属性是全局范围的。在严格模式下,您会收到错误消息。
x = 1 // 全局对象上隐式定义的属性(没有“var”!) console.log(x) // 1 console.log(window.hasOwnProperty('x')) // true
在非严格模式下,函数声明具有函数范围。在严格模式下,它们具有块范围。
'use strict' { function foo() {} } console.log(typeof foo) // 未定义,因为 `foo` 是块作用域
它是如何在引擎盖下工作的
范围定义为标识符有效的代码的 lexical 区域。
在 JavaScript 中,每个函数对象都有一个隐藏的 [[Environment]]
引用,它是对创建它的 execution context(堆栈帧)的 lexical environment 的引用。
调用函数时,会调用隐藏的 [[Call]]
方法。此方法创建一个新的执行上下文,并在新的执行上下文和函数对象的词法环境之间建立链接。它通过将函数对象上的 [[Environment]]
值复制到新执行上下文的词法环境中的 outer reference 字段中来实现这一点。
请注意,新的执行上下文和函数对象的词法环境之间的链接称为 closure。
因此,在 JavaScript 中,作用域是通过外部引用以“链”链接在一起的词法环境来实现的。这个词法环境链称为作用域链,标识符解析由 searching up the chain 进行,以获取匹配的标识符。
找出more。
Javascript 使用范围链来建立给定函数的范围。通常有一个全局范围,并且定义的每个函数都有自己的嵌套范围。在另一个函数中定义的任何函数都具有链接到外部函数的局部范围。定义范围的始终是源中的位置。
作用域链中的元素基本上是一个带有指向其父作用域的指针的 Map。
解析变量时,javascript 从最里面的范围开始向外搜索。
全局声明的变量具有全局范围。在函数中声明的变量的作用域是该函数,并隐藏同名的全局变量。
(我敢肯定,真正的 JavaScript 程序员可以在其他答案中指出许多微妙之处。特别是我遇到了 this page,了解 this
的确切含义。希望 this more introductory link 足以理解不过你开始了。)
老派 JavaScript
传统上,JavaScript 实际上只有两种类型的作用域:
全局范围:变量在整个应用程序中都是已知的,从应用程序的开始 (*) 功能范围:变量在它们声明的函数中是已知的,从函数的开始 (*)
我不会详细说明这一点,因为已经有许多其他答案可以解释差异。
现代 JavaScript
most recent JavaScript specs 现在还允许第三个范围:
块作用域:标识符在声明它们的作用域的顶部是“已知的”,但在声明行之后才能分配或取消引用(读取)。这个过渡时期被称为“时间死区”。
如何创建块范围变量?
传统上,您可以像这样创建变量:
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 代码并使用基于浏览器的转译器(如 Traceur 或 babel-standalone),则可以安全地使用 let 语句,但是您的代码可能在性能方面并非最佳。
如果您正在编写客户端 JavaScript 代码并使用基于节点的转译器(如 traceur shell 脚本或 Babel),则可以安全地使用 let 语句。而且因为您的浏览器只会知道转译的代码,所以性能缺陷应该是有限的。
如果您正在编写客户端 JavaScript 代码并且不使用转译器,则需要考虑浏览器支持。以下是一些完全不支持 let 的浏览器: Internet explorer 10 及以下 Firefox 43 及以下 Safari 9 及 Android 浏览器 4 及以下 Opera 27 及 Chome 40 及以下 任何版本的 Opera Mini 和黑莓浏览器
Internet Explorer 10 及以下版本
火狐 43 及以下
Safari 9 及以下
安卓浏览器4及以下
Opera 27 及以下
丁目40以下
任何版本的 Opera Mini 和黑莓浏览器
https://i.stack.imgur.com/J9kEC.png
如何跟踪浏览器支持
有关在您阅读此答案时哪些浏览器支持 let
语句的最新概述,请参阅 this Can I Use
page。
(*) 全局和函数范围的变量可以在声明之前初始化和使用,因为 JavaScript 变量是 hoisted。 这意味着声明总是位于范围的顶部。
这是一个例子:
<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。
在“Javascript 1.7”(Mozilla 对 Javascript 的扩展)中,还可以使用 let
statement 声明块范围变量:
var a = 4;
let (a = 3) {
alert(a); // 3
}
alert(a); // 4
let
。
最初由 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 的环境,这是调用时警报中引用的值。
https://i.stack.imgur.com/v45hL.png
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,除非它是 a 的属性 -- 它就是。
哎呀!小心使用 'with' - 就像 var 是一个 noop 如果变量已经在函数中定义,它也是一个 noop 相对于从对象导入的名称!对已经定义的名称稍加注意会使这更安全。因此,我个人永远不会使用 with。
with
语句 是 块作用域的一种形式,但 catch
子句是一种更常见的形式(有趣的是,v8 使用 with
实现了 catch
) - 这几乎就是只有 JavaScript 本身中的块作用域形式(即函数、全局、try/catch、with 及其派生类),但是宿主环境具有不同的作用域概念 - 例如浏览器中的内联事件和 NodeJS 的 vm 模块。
我发现许多 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 个嵌套函数,但目前不为全局变量着色。
内联处理程序
前端编码人员经常遇到的一个尚未描述的非常常见的问题是 HTML 中的内联事件处理程序可见的范围 - 例如,使用
<button onclick="foo()"></button>
on*
属性可以引用的变量的范围必须是:
全局(工作内联处理程序几乎总是引用全局变量)
文档的属性(例如,作为独立变量的 querySelector 将指向 document.querySelector;很少见)
处理程序附加到的元素的属性(如上;罕见)
否则,您将在调用处理程序时收到 ReferenceError。因此,例如,如果内联处理程序引用了定义在 inside window.onload
或 $(function() {
的函数,则引用将失败,因为内联处理程序只能引用全局范围内的变量,并且该函数不是全局的:
window.addEventListener('DOMContentLoaded', () => { function foo() { console.log('foo running'); } });
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);
模块 (