JavaScript中变量的作用域是什么?它们在函数内部和外部具有相同的作用域吗?或者这甚至重要吗?此外,如果定义为全局变量,变量存储在哪里?
JavaScript中变量的作用域是什么?它们在函数内部和外部具有相同的作用域吗?或者这甚至重要吗?此外,如果定义为全局变量,变量存储在哪里?
JavaScript 具有词法(也称为静态)作用域和闭包。这意味着您可以通过查看源代码来确定标识符的范围。
四个作用域如下:
除了全局和模块作用域的特殊情况外,变量使用 var
(函数作用域)、let
(块作用域)和 const
(块作用域)声明。在严格模式下,大多数其他形式的标识符声明具有块作用域。
作用域是代码库中标识符有效的区域。
词法环境是标识符名称和与其相关联的值之间的映射。
作用域由词法环境的链接嵌套形成,嵌套中的每个级别对应于祖先执行上下文的词法环境。
这些链接的词法环境形成作用域“链”。标识符解析是沿着此链搜索匹配标识符的过程。
标识符解析仅朝一个方向进行:向外。通过这种方式,外部词法环境无法“看到”内部词法环境。
在 JavaScript 中,确定 标识符 的 作用域 有三个相关因素:
标识符可以通过以下方式之一声明:
var
、let
和const
var
)import
语句eval
标识符可以在以下位置之一声明:
使用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
字符串中,使用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
和 z
会报错,但 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
五次,然后为循环之外的console.log
再次打印5
。
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
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
if(false) {
var x = 1
}
console.log(x) // here, `x` has been declared, but not initialised
let
关键字声明的变量在 for
循环顶部声明时,其作用域仅限于循环体内:
for(let x = 0; x < 10; ++x) {}
console.log(typeof x) // undefined, because `x` is block-scoped
ReferenceError
,因为x
的可见性受到块的限制:
if(false) {
let x = 1
}
console.log(typeof x) // undefined, because `x` is block-scoped
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) // undefined, because `x` is scoped to the function
捕获块参数的作用域限定于捕获块体:
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只有两种作用域:
我不会详细解释这个问题,因为已经有很多其他答案解释了它们之间的区别。
最新的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 script或Babel),则可以安全使用let
语句。由于您的浏览器只会知道转换后的代码,因此性能问题应该是有限的。
如果您正在编写客户端JavaScript代码并且不使用转换器,则需要考虑浏览器支持情况。
以下是一些完全不支持let
的浏览器:
如果您想了解当前浏览器是否支持let
语句,请参考this Can I Use
page,该页面会及时更新。
(*) 全局和函数范围的变量可以在声明之前初始化和使用,因为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>
您需要研究闭包,以及如何使用它们来创建私有成员。
将其应用于此页面上的先前示例之一(5. "闭包"),可以跟踪执行上下文的堆栈。 在此示例中,堆栈中有三个上下文。 它们由外部上下文定义,由var six调用的立即调用函数中的上下文以及var six的立即调用函数内部返回的函数中的上下文。
i)外部上下文。 它具有变量环境a = 1
ii)IIFE上下文,它具有词法环境a = 1,但变量环境a = 6占据了堆栈中的优先权
iii)返回的函数上下文,它具有词法环境a = 6,并在调用时引用该值。
在 "Javascript 1.7"(Mozilla 对 Javascript 的扩展)中,也可以使用 let
语句 声明块级作用域变量:
var a = 4;
let (a = 3) {
alert(a); // 3
}
alert(a); // 4
let
。 - kennytm1) JavaScript有全局作用域、函数作用域以及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的属性--而它是。
哎呀!使用“with”要小心--就像如果变量在函数中已经定义,那么var无操作一样,它对从对象导入的名称也是无效的!提前告知该名称已经被定义会让这个过程更加安全。因为这个原因,我个人不会使用with。
前端开发人员经常遇到的一个常见问题是HTML中内联事件处理程序可见的作用域 - 例如,使用
<button onclick="foo()"></button>
< p > on*
属性可以引用的变量范围必须是以下之一:< /p>
querySelector
将指向document.querySelector
;很少见)否则,当调用处理程序时,您将收到ReferenceError。例如,如果内联处理程序引用在window.onload
或$(function() {
内定义的函数,则引用将失败,因为内联处理程序只能引用全局作用域中的变量,而函数不是全局的:
window.addEventListener('DOMContentLoaded', () => {
function foo() {
console.log('foo running');
}
});
<button onclick="foo()">click</button>
由于内联处理程序被调用在两个with
块中, 一个是document
,另一个是元素,因此可以将文档的属性和处理程序所附加到的元素的属性作为独立变量引用。这些处理程序内部的变量作用域链非常不直观,因此工作事件处理程序可能需要使函数全局化(并且应该避免不必要的全局污染should probably be avoided)。
由于内联处理程序内部的作用域链非常奇怪,并且内联处理程序需要全局污染才能工作,有时还需要在传递参数时进行丑陋的字符串转义,因此最好避免使用它们。相反,使用JavaScript(例如addEventListener
)而不是HTML标记来附加事件处理程序。
function foo() {
console.log('foo running');
}
document.querySelector('.my-button').addEventListener('click', foo);
<button class="my-button">click</button>
<script type="module">
)另外,与普通的<script>
标签不同的是,ES6模块中的代码运行在其自己的私有作用域中。在普通的<script>
标签顶部定义的变量是全局的,因此您可以在其他<script>
标签中引用它,如下所示:
<script>
const foo = 'foo';
</script>
<script>
console.log(foo);
</script>
但是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>
var
规则。JavaScript 不需要 "添加" 'const' 和 'let',这些与它的精神相悖。- 我知道这两个不是你问题的一部分 - 在看到这么多人 "推荐" 它们之后,我不得不加上这句话。 - iAmOren