TLDR
JavaScript 具有词法(也称为静态)作用域和闭包。这意味着您可以通过查看源代码来判断标识符的范围。
四个作用域是:
- 全局 - 一切可见
- 函数 - 在函数(及其子函数和块)中可见
- 块 - 在块(及其子块)内可见
- 模块 - 在模块中可见
在全局和模块范围的特殊情况之外,变量使用var(函数范围)、let(块范围)和const(块范围)声明。大多数其他形式的标识符声明在严格模式下都有块范围。
概述
范围是标识符有效的代码库区域。
词法环境是标识符名称和与之关联的值之间的映射。
作用域由词法环境的链接嵌套构成,嵌套中的每一层对应于祖先执行上下文的词法环境。
这些链接的词法环境形成了一个作用域“链”。标识符解析是沿着这条链搜索匹配标识符的过程。
标识符解析只发生在一个方向:向外。这样,外部词汇环境就无法“看到”内部词汇环境。
在 JavaScript 中决定 scope 和 identifier 的三个相关因素:
- 如何声明标识符
- 声明标识符的位置
- 无论您是在strict mode 还是non-strict mode
可以声明标识符的一些方式:
-
var、let 和 const
- 函数参数
- Catch 块参数
- 函数声明
- 命名函数表达式
- 在全局对象上隐式定义的属性(即,在非严格模式下遗漏了
var)
-
import 声明
eval
可以声明一些位置标识符:
- 全球背景
- 函数体
- 普通方块
- 控制结构的顶部(例如循环、if、while 等)
- 控制结构体
- 模块
声明样式
变量
使用var 声明的标识符具有函数范围,除非它们直接在全局上下文中声明,在这种情况下,它们作为属性添加到全局对象上并具有全局范围。在eval 函数中使用它们有单独的规则。
let 和 const
使用let 和const 声明的标识符具有块范围,除非它们直接在全局上下文中声明,在这种情况下它们具有全局范围。
注意:let、const 和 varare all hoisted。这意味着它们的逻辑定义位置是它们封闭范围(块或函数)的顶部。但是,在控制通过源代码中的声明点之前,无法读取或分配使用 let 和 const 声明的变量。过渡时期被称为时间死区。
function f() {
function g() {
console.log(x)
}
let x = 1
g()
}
f() // 1 because x is hoisted even though declared with `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) // 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 引发ReferenceError,但不会为x 引发ReferenceError,因为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
以下打印 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
以下内容不会抛出ReferenceError,因为x 的可见性不受块的限制;但是,它将打印undefined,因为变量尚未初始化(因为if 语句)。
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
catch 块参数的作用域是 catch-block 主体:
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
它是如何工作的
范围定义为标识符有效的lexical 代码区域。
在 JavaScript 中,每个函数对象都有一个隐藏的 [[Environment]] 引用,它是对在其中创建它的 execution context(堆栈帧)中的 lexical environment 的引用。
当你调用一个函数时,隐藏的[[Call]] 方法被调用。此方法创建一个新的执行上下文,并在新的执行上下文和函数对象的词法环境之间建立链接。它通过将函数对象上的 [[Environment]] 值复制到新执行上下文的词法环境中的 outer reference 字段中来实现这一点。
请注意,新的执行上下文和函数对象的词法环境之间的链接称为closure。
因此,在 JavaScript 中,作用域是通过外部引用以“链”形式链接在一起的词法环境来实现的。这个词法环境链称为作用域链,标识符解析由searching up the chain 进行,以获取匹配的标识符。
找出more。