作用域·作用域链·执行上下文
执行上下文
- 执行上下文的类型
- 全局执行上下文——这是默认或者说是最基础的执行上下文,一个程序中只会存在一个全局上下文,
它在整个javascript
脚本的生命周期内都会存在于执行堆栈的最底部不会被栈弹出销毁。全局上下文会生成一个全局对象(以浏览器环境为例,这个全局对象是window
),并且将this
值绑定到这个全局对象上。 - 函数执行上下文——每当一个函数被调用时,都会创建一个新的函数执行上下文(不管这个函数是不是被重复调用的)
- 执行上下文的内容
- 执行上下文是一个抽象的概念,我们可以将它理解为一个
object
,一个执行上下文里包括以下内容:
- 变量对象
- 活动对象
- 作用域链
- 调用者信息
作用域链
上下文的代码在执行的时候,会创建变量对象的一个作用域链,作用域链决定了各级上下文的代码在访问变量和函数时的顺序,代码正在执行的上下文的变量对象始终位于作用域链的最前端。全局上下文的变量对象始终是作用域链的最后一个变量对象
var a = 10
function fn1(){
console.log(a)
}
function fn2(f){
var a = 20
f()
}
fn2(fn1) // 10
变量对象
每个执行环境都有一个表示变量的对象——变量对象,全局执行环境的变量对象始终存在,而函数这样局部环境的变量,只会在函数执行的过程中存在,在函数被调用时且在具体的函数代码运行之前,JS 引擎会用当前函数的参数列表(arguments
)初始化一个 “变量对象” 并将当前执行上下文与之关联 ,函数代码块中声明的 变量 和 函数 将作为属性添加到这个变量对象上。
活动对象
函数进入执行阶段时,原本不能访问的变量对象被激活成为一个活动对象,自此,我们可以访问到其中的各种属性。
其实变量对象和活动对象是一个东西,只不过处于不同的状态和阶段而已。
执行上下文的生命周期
执行上下文的生命周期有三个阶段,分别是:
- 创建阶段
- 执行阶段
- 销毁阶段
创建阶段
函数执行上下文的创建阶段,发生在函数调用时且在执行函数体内的具体代码之前,在创建阶段,JS 引擎会做如下操作:
- 用当前函数的参数列表(
arguments
)初始化一个 “变量对象” 并将当前执行上下文与之关联 ,函数代码块中声明的 变量 和 函数 将作为属性添加到这个变量对象上。在这一阶段,会进行变量和函数的初始化声明,变量统一定义为undefined
需要等到赋值时才会有确值,而函数则会直接定义。
有没有发现这段加粗的描述非常熟悉?没错,这个操作就是 变量声明提升(变量和函数声明都会提升,但是函数提升更靠前)。 - 构建作用域链(前面已经说过构建细节)
- 确定
this
的值
执行阶段
执行阶段中,JS 代码开始逐条执行,在这个阶段,JS 引擎开始对定义的变量赋值、开始顺着作用域链访问变量、如果内部有函数调用就创建一个新的执行上下文压入执行栈并把控制权交出……
销毁阶段
一般来讲当函数执行完成后,当前执行上下文(局部环境)会被弹出执行上下文栈并且销毁,控制权被重新交给执行栈上一层的执行上下文。
闭包
闭包是什么?
闭包指的是那些引用了另一个函数作用域中变量的函数。通常是在嵌套函数中实现的。
原理分析
函数执行时,每个执行上下文都会有一个包含其中变量的对象,全局上下文中叫变量对象,它会在代码执行期间始终存在,而函数局部上下文中的叫活动对象。只在函数执行期间存在。在定义全局函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的[[scope]]中。接着在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[scope]]来创建其作用域链。接着会创建函数的活动对象并将其推入作用域链的前端。
可以这么认为,函数执行上下文可以访问函数的作用域链,而作用域链是一个包含指针的列表,每个指针分别对应一个变量对象(活动对象
function fn1(){
var a = 1
return function(){
console.log(a)
}
}
const fn2 = fn1()
fn2() // a
fn2 = null // 将fn2设置为null会解除对匿名函数的引用,从而让垃圾回收程序可以将内存释放掉。作用域链及其他作用域也可以销毁
在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中,因此,在fn1中,匿名函数的作用域链实际上包含了fn1()的活动对象。在fn1返回匿名函数后,它的作用域链被初始化为包含fn1()的活动对象和全局变量对象。此时,fn1()的活动对象并不能在它执行完毕后销毁。因为匿名函数的作用域链中仍有对它的引用,当fn1执行完毕后,它的执行上下文的作用域链会被销毁。但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁。
闭包中的this对象
window.a = 222;
let obj = {
a: 1,
f1() {
return function () {
return a;
}
}
}
console.log(obj.f1()()) // 222
每个函数在被调用时都会自动创建this和arguments。内部函数无法直接访问外部函数的这两个变量。因此应该在外部函数显性的把这个值保存下来,内部函数才能访问到。
window.a = 222;
let obj = {
a: 1,
f1() {
let _this = this;
return function () {
return _this.a;
}
}
}
console.log(obj.f1()()) // 1