javascript中变量的范围是什么?它们在函数内部和外部具有相同的作用域吗?或者它甚至重要吗?另外,如果变量是全局定义的,它们存储在哪里?
JavaScript 中变量的作用域是什么?
TLDR
JavaScript 具有词法(也称为静态)范围和闭包。这意味着您可以通过查看源代码来判断标识符的范围。
这四个范围是:
- 全局 - 一切可见
- 函数 - 在函数内可见(及其子函数和块)
- 块 - 在块(及其子块)内可见
- module - 在module内可见
在全局和module作用域的特殊情况之外,使用var
(函数作用域)、let
(块作用域)和const
(块作用域)声明变量。大多数其他形式的标识符声明在严格模式下都具有块作用域。
概述
范围是标识符有效的代码库区域。
词法环境是标识符名称和与其关联的值之间的映射。
范围由词法环境的链接嵌套形成,嵌套中的每一级对应于祖先执行上下文的词法环境。
这些链接的词法环境形成了一个作用域“链”。标识符解析是沿着这条链搜索匹配标识符的过程。
标识符解析只发生在一个方向:向外。这样,外部词汇环境就无法“看到”内部词汇环境。
在决定JavaScript 中标识符的范围时,有三个相关因素:
可以声明标识符的一些方法:
var
,let
和const
- 功能参数
- 捕获块参数
- 函数声明
- 命名函数表达式
- 全局对象上隐式定义的属性(即,
var
在非严格模式下丢失) import
声明eval
可以声明一些位置标识符:
- 全球背景
- 函数体
- 普通块
- 控制结构的顶部(例如,循环、if、while 等)
- 控制结构体
- module
声明样式
无功
使用声明的标识符var
具有函数作用域,除非它们直接在全局上下文中声明,在这种情况下,它们被添加为全局对象的属性并具有全局作用域。它们在eval
函数中的使用有单独的规则。
让和常量
使用声明let
并const
具有块作用域的标识符,除了在全局上下文中直接声明时,在这种情况下它们具有全局作用域。
注:let
、const
和var
均已吊起。这意味着它们定义的逻辑位置是它们的封闭范围(块或函数)的顶部。但是,在控制通过源代码中的声明点之前,不能读取或分配使用let
和声明的变量const
。过渡期称为时间死区。
function f() {
function g() {
console.log(x)
}
let x = 1
g()
}
f() // 1 because x is hoisted even though declared with `let`!
函数参数名称
函数参数名称的作用域是函数体。请注意,这有点复杂。声明为默认参数的函数靠近参数列表,而不是函数体。
函数声明
函数声明在严格模式下有块作用域,在非严格模式下有函数作用域。注意:非严格模式是一组复杂的紧急规则,基于不同浏览器的古怪历史实现。
命名函数表达式
命名函数表达式的作用域是它们自身(例如,为了递归)。
全局对象上隐式定义的属性
在非严格模式下,全局对象上隐式定义的属性具有全局作用域,因为全局对象位于作用域链的顶部。在严格模式下,这些是不允许的。
评估
在eval
字符串中,使用 using 声明的变量var
将放置在当前作用域中,或者,如果eval
间接使用,则作为全局对象的属性。
例子
以下将抛出一个 ReferenceError 因为名称x
, y
, 和z
在函数之外没有意义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)
以下内容将为y
and抛出 ReferenceError z
,但不会为 for抛出一个 ReferenceError x
,因为 的可见性x
不受块的约束。定义控制结构的体块一样if
,for
和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
在下面,x
在循环外可见,因为var
具有函数作用域:
for(var x = 0; x < 5; ++x) {}
console.log(x) // 5 (note this is outside the loop!)
...由于这种行为,您需要小心关闭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
以下打印undefined
因为x
是块范围的。回调是一一异步运行的。新行为let
变量意味着每个匿名函数关闭了一个名为不同的变量x
(不像它会用做var
),所以整数0
通过4
印:
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
以下不会抛出 aReferenceError
因为 的可见性x
不受块的约束;但是,它会打印,undefined
因为变量尚未初始化(因为if
语句)。
if(false) {
var x = 1
}
console.log(x) // here, `x` has been declared, but not initialised
在for
循环顶部声明的变量usinglet
的作用域是循环体:
for(let x = 0; x < 10; ++x) {}
console.log(typeof x) // undefined, because `x` is block-scoped
以下将抛出 aReferenceError
因为 的可见性x
受块约束:
if(false) {
let x = 1
}
console.log(typeof x) // undefined, because `x` is block-scoped
使用var
, let
or声明的变量const
都作用于module:
// 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) // undefined, because `x` is scoped to the function
Catch 块参数的范围限定为 catch 块主体:
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
它是如何工作的
范围被定义为标识符有效的代码词法区域。
在 JavaScript 中,每个函数对象都有一个隐藏的[[Environment]]
引用,它是对创建它的执行上下文(堆栈帧)的词法环境的引用。
当你调用一个函数时,隐藏的[[Call]]
方法被调用。该方法创建一个新的执行上下文并在新的执行上下文和函数对象的词法环境之间建立链接。它通过将[[Environment]]
函数对象上的值复制到新执行上下文的词法环境上的外部引用字段来完成此操作。
请注意,新的执行上下文和函数对象的词法环境之间的这种链接称为闭包。
因此,在 JavaScript 中,作用域是通过外部引用以“链”链接在一起的词法环境来实现的。这个词法环境链称为作用域链,标识符解析通过在链上搜索匹配的标识符来进行。
了解更多。
Javascript 使用作用域链来建立给定函数的作用域。通常有一个全局作用域,并且定义的每个函数都有自己的嵌套作用域。在另一个函数中定义的任何函数都有一个链接到外部函数的局部作用域。定义范围的始终是源中的位置。
作用域链中的元素基本上是一个带有指向其父作用域的指针的 Map。
解析变量时,javascript 从最内部的范围开始向外搜索。
全局声明的变量具有全局作用域。在函数内声明的变量的作用域是该函数,并隐藏同名的全局变量。
(我敢肯定,真正的 JavaScript 程序员可以在其他答案中指出许多微妙之处。特别是我在任何时候都看到了这个页面,关于究竟this
是什么意思。希望这个更具介绍性的链接足以让你开始.)
老派 JavaScript
传统上,JavaScript 实际上只有两种作用域:
我不会详细说明这一点,因为已经有许多其他答案解释了这种差异。
现代 JavaScript
在最近JavaScript的功能现在也允许第三范围:
- 块作用域:标识符从它们在 中声明的作用域顶部是“已知的” ,但在它们的声明行之后才能分配或取消引用(读取)。这个过渡期被称为“时间死区”。
如何创建块作用域变量?
传统上,您可以像这样创建变量:
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 代码并使用基于 Node 的转译器(如traceur shell 脚本或Babel),则可以安全地使用该
let
语句。并且因为您的浏览器只会知道转译的代码,所以性能缺陷应该是有限的。如果您正在编写客户端 JavaScript 代码并且不使用转译器,则需要考虑浏览器支持。
这些是一些根本不支持的浏览器
let
:- Internet Explorer 10及以下
- Firefox 43及以下
- Safari 9及以下
- Android 浏览器 4及以下
- Opera 27及以下
- Chome 40及以下
- 任何版本的Opera Mini和黑莓浏览器
如何跟踪浏览器支持
有关let
在您阅读此答案时哪些浏览器支持该声明的最新概述,请参阅此Can I Use
页面。
(*) 全局和功能范围的变量可以在声明之前进行初始化和使用,因为 JavaScript 变量是被提升的。这意味着声明总是在作用域的顶部。
下面是一个例子:
<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>
您将需要调查闭包,以及如何使用它们来创建私有成员。