TLDR
JavaScript 具有词法(也称为静态)作用域和闭包。这意味着您可以通过查看源代码来了解标识符的范围。
这四个范围是:
- 全局 - 一切可见
- 函数 - 在函数(及其子函数和块)内可见
- 块 - 在块(及其子块)内可见
- 模块 - 在模块内可见
除了全局和模块作用域的特殊情况之外,变量的声明使用var
(函数范围),let
(块作用域),以及const
(块范围)。大多数其他形式的标识符声明在严格模式下具有块作用域。
Overview
范围是标识符有效的代码库区域。
词法环境是标识符名称和与其关联的值之间的映射。
作用域由词法环境的链接嵌套构成,嵌套中的每个级别对应于祖先执行上下文的词法环境。
这些链接的词汇环境形成了范围“链”。标识符解析是沿着这条链搜索匹配标识符的过程。
标识符解析仅发生在一个方向:向外。这样,外部词汇环境就无法“看到”内部词汇环境。
决定的因素有3个scope https://en.wikipedia.org/wiki/Scope_(computer_science) of an 标识符 https://www.ecma-international.org/ecma-262/10.0/index.html#sec-names-and-keywords在 JavaScript 中:
- 标识符是如何声明的
- 声明标识符的位置
- 无论你是在严格模式 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode or 非严格模式 https://developer.mozilla.org/en-US/docs/Glossary/Sloppy_mode
声明标识符的一些方法:
-
var
, let
and const
- 功能参数
- catch 块参数
- 函数声明
- 命名函数表达式
- 全局对象上隐式定义的属性(即,遗漏了
var
在非严格模式下)
-
import
声明
eval
可以声明一些位置标识符:
- 全球背景
- 函数体
- 普通块
- 控制结构的顶部(例如循环、if、while 等)
- 控制结构体
- Modules
声明样式
var
声明使用的标识符var
有功能范围,除了直接在全局上下文中声明它们时,在这种情况下它们将作为属性添加到全局对象上并具有全局作用域。它们的使用有单独的规则eval
功能。
让和常量
声明使用的标识符let
and const
具有块作用域,除非它们直接在全局上下文中声明,在这种情况下它们具有全局作用域。
Note: let
, const
and var
都被吊起 https://stackoverflow.com/a/31222689/38522。这意味着它们定义的逻辑位置是其封闭范围(块或函数)的顶部。但是,使用声明的变量let
and const
在控制通过源代码中的声明点之前无法读取或分配。过渡期被称为暂时死区。
function f() {
function g() {
console.log(x)
}
let x = 1
g()
}
f() // 1 because x is hoisted even though declared with `let`!
函数参数名称
函数参数名称的范围仅限于函数体。请注意,这有一点复杂。声明为默认参数的函数关闭参数表 https://stackoverflow.com/questions/61208843/where-are-arguments-positioned-in-the-lexical-environment/,而不是函数体。
函数声明
函数声明在严格模式下具有块作用域,在非严格模式下具有函数作用域。注意:非严格模式是一组复杂的紧急规则,基于不同浏览器的古怪历史实现。
命名函数表达式
命名函数表达式的范围仅限于其自身(例如,出于递归的目的)。
全局对象上隐式定义的属性
在非严格模式下,全局对象上隐式定义的属性具有全局作用域,因为全局对象位于作用域链的顶部。在严格模式下,这些是不允许的。
eval
In eval
字符串,使用声明的变量var
将被放置在当前范围内,或者,如果eval
间接使用,作为全局对象的属性。
Examples
下面将抛出一个 ReferenceError 因为名称x
, y
, and 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)
以下将抛出一个 ReferenceError :y
and z
,但不适合x
,因为可见度x
不受块的限制。定义控制结构体的块,例如if
, for
, and 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
以下不会抛出ReferenceError
因为可见度x
不受块的约束;但是,它会打印undefined
因为变量尚未初始化(因为if
陈述)。
if(false) {
var x = 1
}
console.log(x) // here, `x` has been declared, but not initialised
在 a 顶部声明的变量for
循环使用let
作用域为循环体:
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
or 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
and 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
它是如何工作的
范围定义为lexical https://stackoverflow.com/a/1047479/38522标识符有效的代码区域。
在 JavaScript 中,每个函数对象都有一个隐藏的[[Environment]]
引用是对词汇环境 https://www.ecma-international.org/ecma-262/10.0/index.html#sec-lexical-environments of the 执行上下文 https://www.ecma-international.org/ecma-262/10.0/index.html#sec-execution-contexts(堆栈框架)在其中创建它。
当你调用一个函数时,隐藏的[[Call]]
方法被调用。此方法创建一个新的执行上下文,并在新的执行上下文和函数对象的词法环境之间建立链接。它通过复制[[Environment]]
函数对象上的值,转换为外部参考 https://www.ecma-international.org/ecma-262/10.0/index.html#sec-lexical-environments新执行上下文的词法环境中的字段。
请注意,新的执行上下文和函数对象的词法环境之间的这种链接称为closure https://stackoverflow.com/a/111114/38522.
因此,在 JavaScript 中,作用域是通过外部引用以“链”形式链接在一起的词法环境来实现的。这个词法环境链称为作用域链,标识符解析通过以下方式发生:沿着链条向上搜索 https://www.ecma-international.org/ecma-262/10.0/index.html#sec-getidentifierreference以获得匹配的标识符。
查出more https://github.com/getify/You-Dont-Know-JS/tree/2nd-ed/scope-closures.