前言
回顾一下,上节我们讨论了:
- 什么是上下文,上下文的作用是什么?
- 如何管理上下文?
- 什么情况下会创建上下文?
- 全局上下文的创建过程?
你能回答几个,如果没有答案可以前往查看:
上节我们没有讨论 函数执行上下文 的创建,这节我们来细说。
函数上下文与 全局上下文类似,但又有所不同。
还记得吗,我们在上节中说过,当函数声明 在被 赋值 时,会 创建一个函数对象 ,该对象内部会 保存 函数 声明 时的上下文环境。
此节我们会详细的讨论函数的作用域,并在此基础上,自然而然的推出所谓的 闭包 机制
作用域
看看MDN怎说:
作用域是当前的执行上下文
这句话更加准确来说,作用域是 执行上下文中的文本环境 (或者叫词法环境)
其实非常简单的道理,上节我们说:
执行上下文的作用是: 正在执行的代码在当前执行上下文中查找需要的变量
所谓的作用域就是指执行时查找变量的区域,因此自然而然就是执行上下文
作用域分类:
- 全局作用域 (全局执行上下文,上节讲解)
- 函数作用域 (函数执行上下文,本节讲解)
- 块级作用域 (下节讲解)
函数作用域
函数作用域就是函数的 执行上下文
更准确来说是 文本环境 Lexical Environment
下面的讲解对三者不作区分
函数对象
- 函数的作用域,取决于 函数执行上下文的创建。
- 同时,函数的作用域也去与函数对象生成时 [[scope]] 属性保存的上下文有关。
下面对这两点进行解释:
函数对象在函数 赋值时创建
函数的赋值有两种情况:
- 在生成上下文时,函数声明会在此法环境中进行整体提升,也就是赋值
- 在函数执行时,才对指向函数的变量进行赋值
非顶级函数声明 的 函数对象 在函数语句 执行的时候创建。
函数对象中的 ‘体内’([[scope]]属性) 保存了 创建对象时的当前执行上下文
因此函数的作用域 取决于 函数对象创建 时的位置,而与调用时无关
函数的作用域与调用位置和方式无关
函数执行上下文中的
this指向与调用方式有关(姑且不谈)
有关函数对象如何决定函数的作用域,我们接着往下说
函数执行上下文
函数的执行上下文,只有
函数调用时,才会被创建并压入 执行上下文调用栈的栈顶
我们结合上节的知识,分析如下代码的执行流程:
function foo() {
console.log(a);
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
该段代码被js引擎解析
1. 第一步 在执行上下文栈中 压入 全局执行上下文
经过一系列步骤,全局上下文被创建在栈中
- 在
全局scope中没有保存任何信息 - 在
全局对象中保存了内置默认对象和foobara, 其中a为undefined foo和bar在赋值过程中创建了函数对象,该对象内部保存了 当前执行上下文 即此刻的全局执行上下文
1. 开始执行代码
全局执行上下文生成好后,开始执行代码,第一行 `a = 2` 将 `全局对象` 中的 `a` 从 `undefined` 修改为 `2`
然后 `执行bar`
2. 创建 bar函数执行上下文
在执行bar前一刻,生成 函数执行上下文 并压入执行上下文栈的 栈顶
函数执行上下文的生成步骤与全局上下文有些许不同:
- 函数上下文的
Lexical Environment中只有函数scope,没有全局对象 - 函数上下文的 outer 会 指向 函数对象中保存的上下文 ,从而形成
作用域链
在bar函数执行上下文中
this Binding指向全局对象window(关于this指向问题以后讲解)scope中保存了a值为undefined- 执行上下文指向
全局执行上下文(从函数对象中得知)
3. 开始执行 bar
bar的函数执行上下文生成后,就可以开始执行代码,第一行代码将 bar 执行上下文中的 a 从 undefined 修改为 3
第二行代码开始 执行foo
4. 创建 foo 函数执行上下文
foo函数执行前,先创建其执行上下文,压入 ECS (执行上下文栈)的栈顶
该函数的执行上下文很简单
This Binding指向scope中空空如也- 执行上下文指向
全局执行上下文(从函数对象中得知)
5. 开始执行 foo
输出a, 则沿着 foo 函数的上下文 开始寻找a ,
在 foo 的 scope 中 没有 a ,于是找到 其上下文指向的 全局上下文,在全局上下文的 scope 中没有找到,接着在 全局上下文的 全局对象 中找到 a = 2 , 所以输出 2
6. 弹出栈顶
foo 执行结束其上下文首先弹出栈顶
紧接着bar执行结束其上下文弹出栈顶
最后回到全局执行上下文来到栈顶的状态
闭包
所谓的闭包,用一句话解释:
函数执行结束后,本该被释放掉的执行上下文被保存了下来
下面就来解释这句话
本该释放的函数执行上下文中,保存了函数执行时所有的状态(即变量)
我们如何保存这个上下文?
上文中有这样一段描述:
函数对象生成时, 会保存其所处的执行上下文
这里需要提一嘴,这里所谓的保存,指的不是将那块内存里的内容拷贝,而是说指向那块内存
因此,一个简单的思路是,在一个函数的体内,创建另一个函数,该函数的对象在创建时,会保存父函数的执行上下文
function foo() {
let a = 2;
function bar() {
console.log(++a);
}
return bar;
}
let bar = foo();
bar(); //<p align=left>3</p>
bar(); //4
整个执行上下文的流程不再做分析,我们直接来到foo函数的执行上下文:
- 创建foo函数的执行上下文时,会将
bar函数声明到其scope中, 而它指向一个函数对象,该对象的体内([[scope]]属性保存了当前执行上下文,也就是foo函数的执行上下文, 我们最后又将bar函数作为返回值,保存在了全局上下文 - 函数
foo的上下文被弹出栈,但其在内存中生成存储数据的对象并没有按照常规失去引用,而是继续被全局保存的bar引用着,因此它并没消失 - 我们调用
bar函数,生成bar的执行上下文,但其上下文中没有a,则沿着[[scope]]找到了foo当时生成的执行上下文,让其加一
总结上文,闭包的原理总结一句话:
本该被释放的空间由于没有失去引用,继续保存并可以被访问
我们只有利用声明函数一种方式去引用函数的上下文吗?答案是否定的
比如我们可以用一个对象,保存我们想要保存 的内容,然后将其返回给全局:
function foo() {
let a = 2;
return { a } //es6对象语法糖 等于 {a : a}
}
let bar = foo();
console.log(++bar.a); //3
console.log(++bar.a); //4
这就是闭包的原理,闭包应该说是底层的原理导致的一个自然而言的现象,而闭包又有很多有用的应用,但其实它并不难。
至于js引擎的垃圾回收机制,我们以后再讲。