作用域、闭包
WARNING
作用域和执行上下文之间最大的区别是:
- 作用域在 定义时 就确定,并且 不会改变
- 执行上下文在 运行时 确定,随时 可能改变
作用域
编译原理
- 分词/词法分析(Tokenizing/Lexing)
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。
例如,考虑程序var a = 2;。这段程序通常会被分解成 为下面这些词法单元:var、a、=、2 、;。
- 分词/词法分析(Tokenizing/Lexing)
- 解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法 结构的树。
这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
- 解析/语法分析(Parsing)
- 代码生成
将 AST 转换为可执行代码的过程称被称为代码生成。
- 代码生成
定义
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。
分类
- 全局作用域
- 函数作用域
- 块级作用域
参考
- JavaScript深入之词法作用域和动态作用域 - 冴羽 GitHub
- JavaScript深入之作用域链 - 冴羽 GitHub
执行上下文
分类
- 全局执行上下文
- 函数执行上下文
- Eval函数执行上下文
阶段
- 创建
- 确定 this 的值,也被称为 This Binding
- LexicalEnvironment(词法环境) 组件被创建
- 函数声明和变量(let、const)绑定
- let和const定义的变量,保持 uninitialized(未初始化状态)
- VariableEnvironment(变量环境) 组件被创建
- 变量(var)绑定
- 被赋值为undefined
- 执行
- 变量赋值
- 代码执行
- 回收
属性
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
参考
- 理解 JavaScript 中的执行上下文和执行栈 - 木易杨 博客
- JavaScript深入之执行上下文栈 - 冴羽 GitHub
- JavaScript深入之执行上下文 - 冴羽 GitHub
提升
变量和赋值语句一起书写,在js引擎解析时,会将其拆成声明和赋值2部分,声明置顶,赋值保留在原来位置
- 函数声明会提升, 函数表达式不会被提升
- 变量声明也会置顶
- 函数会先被提升,然后才是变量(函数是一等公民)
- 声明过的变量不会重复声明,但出现在后面的函数声明还是可以覆盖前面的
- IIFE(立即执行函数表达式)函数名不允许更改
- let 进行的声明,不会在块作用域中进行提升,声明的代码被运行之前,声明并不“存在”
js
if (!('a' in window)) {
var a = 12
}
// console.log(a) undefined
js
// console.log(a) undefined
var a = 'window'
function fn() {
// console.log(a) undefined
var a = 'function'
}
fn()
// console.log(a) window
js
// console.log(a) undefined
var a = 'window'
function fn() {
// console.log(a) window
a = 'function'
}
fn()
// console.log(a) function
js
console.log(a)
a = 'window'
function fn() {
console.log(a)
a = 'function'
}
fn()
console.log(a)
// Uncaught ReferenceError: a is not defined
js
var a = 'window'
function fn() {
// console.log(a) undefined
if (!a) {
var a = 'function'
}
// console.log(a) function
}
fn()
js
var a = 1,
b = 10,
c = 100
function fn(a) {
a = 0
var b = 0
c = 0
}
fn(a)
console.log(a)
console.log(b)
console.log(c)
// 1
// 10
// 0
js
`var a = b = 'func'` 相当于
b='func'
var a = b
-----
// console.log(a, b) undefined undefined
var a = 'wina',
b = 'winb'
function foo() {
// console.log(a, b) undefined "winb"
var a = (b = 'func')
// console.log(a, b) "func" "func"
}
foo()
// console.log(a, b) "wina" "func"
js
var a = 10
;(function () {
// console.log(a) undefined
a = 5
// console.log(window.a) 10
var a = 20
// console.log(a) 20
})()
var b = {
a,
c: b,
}
// console.log(b.c) undefined
重名
js
// console.log(a) function
var a = 100
// console.log(a) 100
function a() {
console.log(a)
}
// console.log(a) 100
js
function a() {}
var a
console.log(typeof a) // function
js
// console.log(fn) fn -> 3
function fn() {
console.log(1)
}
// console.log(fn) fn -> 3
function fn() {
console.log(2)
}
// console.log(fn) fn -> 3
var fn = '100'
// console.log(fn) 100
function fn() {
console.log(3)
}
// console.log(fn) 100
参数
js
var a = 1
function foo(a) {
// console.log(a) 1
var a = 3
// console.log(a) 3
}
foo(a)
// console.log(a) 1
js
var a = 1
function foo(a) {
// console.log(a) 1
a = 3
// console.log(a) 3
}
foo(a)
// console.log(a) 1
js
var a = 1
function foo(a, b) {
console.log(a) // 1
a = 2
arguments[0] = 3
var a
console.log(a, this.a, b) // 3, 1, undefined
}
foo(a)
立即执行函数
js
var a = 'oa'
var b = 'ob'
var c = 'oc'
var d = 'od'
;(function (a, b, c) {
// console.log(a) oa
// console.log(b) ob
// console.log(c) undefined
// console.log(d) od
a = 'hello'
var b = 'world'
c = '!'
d = 'id'
// console.log(a) hello
// console.log(b) world
// console.log(c) !
// console.log(c) id
})(a, b)
// console.log(a) oa
// console.log(b) ob
// console.log(c) oc
// console.log(c) id
js
var a = 10
;(function c() {})()
// console.log(c) Uncaught ReferenceError: c is not defined
js
// console.log(b) undefined
var b = 10
// console.log(b) 10
;(function b() {
// console.log(b) function
b = 20
// console.log(b) function
})()
参考
- (面试) 彻底解决 JS 变量提升的面试题 | 一题一图,超详细包教包会 - 林一一 掘金
- js 函数和变量重名(函数和变量解析规则) - 知乎文章
闭包
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。
换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
—— MDN
函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
—— 《你不知道的JS【上】》
特点:
- 闭包可以访问当前函数以外的变量
- 即使外部函数已经返回,闭包仍能访问外部函数定义的变量
- 闭包可以更新外部变量的值
js
var n = 10
function fn() {
var n = 20
function f() {
n++
console.log(n)
}
f()
return f
}
var x = fn()
x()
x()
console.log(n)
// 21 22 23 10
js
let a = 0,
b = 0
function fn(a) {
fn = function fn2(b) {
console.log(a, b)
console.log(++a + b)
}
console.log('a', a++)
}
fn(1)
fn(2)
// a, 1
// 2, 2
// 5
js
var data = []
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i)
}
}
data[0]()
data[1]()
data[2]()
// 3 3 3
// console.log(data)
// Array [function () {
// console.log(i)
// }, function () {
// console.log(i)
// }, function () {
// console.log(i)
// }]
js
var result = []
var a = 3
var total = 0
function foo(a) {
for (var i = 0; i < 3; i++) {
result[i] = function () {
total += i * a
console.log(total)
}
}
}
foo(1)
result[0]() // 3
result[1]() // 6
result[2]() // 9
INFO
js
function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
},
}
}
var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
// undefined
// 0
// 0
// 0
var b = fun(0).fun(1).fun(2).fun(3)
// undefined
// 0
// 1
// 2
var c = fun(0).fun(1)
c.fun(2)
c.fun(3)
// undefined
// 0
// 1
// 1
参考
- 闭包 - MDN
- 深入浅出图解作用域链和闭包 - 木易杨 博客
- JS 闭包经典使用场景和含闭包必刷题 - 林一一 掘金
- 闭包的使用场景,使用闭包需要注意什么 - 瓶子君 GitHub
- 了解词法环境吗?它和闭包有什么联系? - 瓶子君 GitHub