JS作用域和闭包
总的来说可以分为以下几块内容区理解:
词法作用域
动态作用域 (动态作用域与this机制挂钩)
全局作用域
函数作用域
块级作用域 (es6+)
前言基础
我们先了解下js引擎(解释器/编译器结构), js引擎可以理解为根据ECMAScript定义的语言标准来动态执行JavaScript字符串...
js 引擎基础
js 执行环境
整个解析过程可分为: 语法检查阶段
-> 运行时阶段
语法检查阶段(一)
语法检查阶段: 分为 词法分析和语法分析
JS解释器先把JavaScript代码(字符串)的字符流按照ECMAScript标准转换为记号流,
分解为词法单元块.
// 比如 var a = 2, 词法分析后的结果
[
"var": "keyword",
"a": "identifier",
"=": "assignment",
"2": "integer",
";": "eos" (end of statement)
]
// 然后根据解析的词法结构结合标准与法生成 AST
{
operation: "=",
left: {
keyword: "var",
right: "a"
}
right: "2"
}
// 遍历这颗抽象语法树(其中的一些操作还没去深入研究, 反正可以做很多事情, 什么eslint, babel 转化等),
最后直接会转化为机器指令(分配内存空间)
# 当语法检查出现错误时, 会抛出错误...
运行时阶段(二)
运行时阶段: 预解析 和 代码执行
- 先预解析(一)
# 1.创建执行上下文环境: 先是全局, 然后是函数,会被依次push到执行栈(一块内存来管理上下文)
// 上下文环境包括:
变量对象VO: 优先级依次是 arguments声明, function声明, var声明
作用域链Scope(词法环境): 由当前变量对象以及上层父级作用域构成的一条链表结构, 便于变量查询
this值: 表示当前上下文对象, 程序进入上下文就会确定下来
# 2.为上下文中的变量对象(VO)赋值
函数形参: undefined
函数声明: 如果变量对象已经包含了相同名字的属性,则会替换它的值,
此时函数的标识符在环境中已存在(变量提升的原因)
变量声明: undefined
// 例子就不举了...QAQ
- 再代码执行(二)
进入执行代码阶段
预解析阶段的初始化属性:
`变量对象`的undefined值可能会被覆盖 => 变为活动对象(AO)
`作用域链`可能会改变
`this值`也可能会改变
作用域
作用域[[scope]]: 可以理解为变量的生命周期,有效范围
; 也可以理解为js中用来访问变量
的一套规则...
词法作用域
上面为甚么提到 js引擎解析代码的过程, 因为语法检查阶段可以看做是理解词法作用域的基础;
词法作用域就是定义在词法阶段的作用域, JavaScript采用的是词法作用域
, 变量,函数的作用域在函数定义的时候就决定
词法作用域只由变量,函数被声明时所处的位置决定
// 第一层
var a = 1
function foo(a) {
// 第二层
var b = a * 2;
function bar(c) {
// 第三层
console.log(a, b, c);
}
bar(b * 3);
}
foo(2) // 2, 4, 12
分析一下: 这里有个嵌套的作用域
第一层: 全局作用域, 两个声明, foo, a
第二层: foo的函数作用域, 三个声明, a, b, bar
第三层: bar的函数作用域, 一个声明, c
作用域的这种嵌套结构和互相之间的位置关系给 js 引擎提供了的位置信息;
js引擎可以用这些信息来查找标识符声明的位置;
特性: 作用域查找从运行时所处的最内部作用域开始,逐级向上进行,直到遇见第一个匹配的标识符为止
动态作用域
动态作用域主要是js程序在运行时决定的, 与 this 机制相关; 动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它从何处调用
...
函数的作用域是在函数调用的时候才决定, 与上下文挂钩
作用域链
当查找变量标识符
的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
作用域链在函数创建
和运行时
是会发生改变的...
函数创建时
函数有一个内部属性 [[scope]],当函数创建的时候, 就会保存所有父变量对象到其中;
function foo() {
function bar() {
// ...
}
}
// scope:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
函数运行时
函数运行时, 进入函数上下文,创建 VO/AO, 就会将活动对象添加到作用链的顶端
Scope = [AO].concat([[Scope]])
总结一下作用域链创建过程(ES3规范下, ES5的规范会在后续ES6的内容中讲到)
var v1 = 'global'
function fn() {
var v2 = 'local'
return v2
}
fn()
语法检查阶段
- 1.函数 fn 被 声明创建,保存作用域链到内部属性[[scope]]
fn.[[scope]] = [
globalContext.VO
]
- 2.函数 fn 被执行,创建 fn的函数上下文, 上下文被压入执行栈
ECStack = [
fnContext,
globalContext
]
- 3.创建作用域链Scope...复制函数的 scope 属性
fnContext = {
Scope: fn.[[scope]]
}
- 4.添加AO, 即初始化VO(arguments,func声明, 变量声明)
fnContext = {
AO: {
arguments: {
length: 0
},
v2: undefined
},
Scope: fn.[[scope]]
}
- 5.AO 添加到 Scope 的顶端
fnContext = {
AO: {
arguments: {
length: 0
},
v2: undefined
},
Scope: [AO, fn.[[scope]]]
}
运行时阶段
- 6.准备工作完毕, 开始执行代码
fnContext = {
AO: {
arguments: {
length: 0
},
v2: 'local' // 为 AO 属性赋值
},
Scope: [AO, fn.[[scope]]]
}
- 7.查找 v2 的值并返回, 函数执行完毕, 弹出调用栈
ECStack = [
globalContext
]
闭包
MDN 闭包解释
ECMAScript中,闭包指的是:
理论角度:
所有的函数都是闭包.
因为它们都在创建的时候就将上层上下文的数据保存起来了.即使是全局变量也是如此,
因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
实践角度:(以下函数才算是闭包)
1.即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
2.在代码中引用了自由变量
闭包实例
var v = "global";
function outer(){
var v = "local";
function inner(){
return v;
}
return inner;
}
var foo = outer();
foo();
# 代码执行过程分析:
1.进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
2.全局执行上下文初始化
3.执行outer函数,创建 outer 函数执行上下文,,outer的执行上下文入栈
4.outer 执行上下文初始化,创建变量对象、作用域链、this等
5.outer 函数执行完毕,outer 执行上下文出栈
6.执行 inner 函数,创建 inner 函数执行上下文,inner 执行上下文被压栈
7.inner 执行上下文初始化,创建变量对象、作用域链、this等
8.inner函数执行完毕,inner函数上下文出栈
# 当 inner函数 执行时, outer已经出栈, 如何获取 outer 作用域内的值呢?
答案是: 通过作用域链 Scope
其实 inner 在执行的时候会它的上下文维护这样一个属性
innerContext = {
Scope: [AO, outerContext.AO, globalContext.VO]
}
所以, 即时 outer 销毁了, 内存中依然会存一份outerContext.AO
闭包解题思路
分析函数调用
分析上下文语义
globalContext = {
VO: {
arguments: {}
变量: '',
},
Scope: []
}
funcContext = {
AO: {
arguments: {}
变量: '',
},
Scope: [AO, ..., globalContext.VO]
}
总结
- 作用域: 变量对象所能访问的区域
执行上下文 = {
变量对象 = {
arguments = {},
func 声明,
var 声明
},
Scope: [],
this
}
词法作用域(声明时)
动态作用域(运行时)
- 作用域链
Scope: [AO, fnContext.AO, ..., globalContext.VO]
一条能访问上层执行上下文变量对象的链表
- 闭包
能够访问自由变量(非参数和自己内部的变量)的函数;
理论上: 所有函数
实践上: 引用了上层上下文变量对象的函数
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。