执行上下文与执行上下文栈
首先介绍下执行上下文和执行上下文栈的原理,从而引出变量提升和闭包的底层原理。
这里先简单提下变量提升的原理和含义,具体底层实现请继续往下看。
变量提升与函数提升
-
提升的内部原理
函数在运行的时候,会首先创建执行上下文,然后将执行上下文入栈,然后当此执行上下文处于栈顶时,开始运行执行上下文。
在创建执行上下文的过程中会做三件事:创建变量对象,创建作用域链,确定 this 指向,
其中创建变量对象的过程中,首先会为 arguments 创建一个属性,值为 arguments,然后会扫码 function 函数声明,创建一个同名属性,值为函数的引用,接着会扫码 var 变量声明,创建一个同名属性,值为 undefined,这就是变量提升。
-
变量声明提升
- 通过var定义(声明)的变量, 在定义语句之前就可以访问到
- 值: undefined
- 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
-
函数声明提升
- 通过function声明的函数, 在之前就可以直接调用
- 值: 函数定义(对象)
- 如果是通过函数表达式定义一个函数,属于变量提升,不属于函数提升,不可直接调用
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
-
问题: 变量提升和函数提升是如何产生的?
- 先有变量提升, 再有函数提升(函数提升优先级高于变量提升,同名的话函数会覆盖变量)
执行上下文
-
代码分类(位置)
- 全局代码
- 函数代码
-
全局执行上下文
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量==>undefined, 添加为window的属性
- function声明的全局函数==>赋值(fun), 添加为window的方法
- this==>赋值(window)
- 开始执行全局代码
-
函数执行上下文
- 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象
- 对局部数据进行预处理
- 形参变量==>赋值(实参)==>添加为执行上下文的属性
- arguments==>赋值(实参列表), 添加为执行上下文的属性
- var定义的局部变量==>undefined, 添加为执行上下文的属性
- function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
- this==>赋值(调用函数的对象)
- 开始执行函数体代码
生命周期
执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段
创建阶段
创建阶段即当函数被调用,但未执行任何其内部代码之前
创建阶段做了三件事:
- 确定 this 的值,也被称为
This Binding(this的值是在执行的时候才能确认) - LexicalEnvironment(词法环境) 组件被创建
- VariableEnvironment(变量环境) 组件被创建
- 创建作用域链
词法环境
词法环境有两个组成部分:
- 全局环境:是一个没有外部环境的词法环境,其外部环境引用为
null,有一个全局对象,this的值指向这个全局对象 - 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了
arguments对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
// 标识符绑定在这里
outer: <null> // 对外部环境的引用
}
}
FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Declarative", // 函数环境
// 标识符绑定在这里 // 对外部环境的引用
outer: <Global or outer function environment reference>
}
}
变量环境
变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性
在 ES6 中,词法环境和变量环境的区别在于前者用于存储函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var )绑定
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
执行上下文如下
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: { // 词法环境
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: { // 变量环境
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
let和const定义的变量a和b在创建阶段没有被赋值,为< uninitialized >,但var声明的变量从在创建阶段被赋值为undefined,这就是变量提升的实际原因
执行阶段
在这阶段,执行变量赋值、代码执行
如果 Javascript 引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配 undefined 值
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
执行上下文栈
-
在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
-
在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
-
在函数执行上下文创建后, 将其添加到栈中(压栈)
-
在当前函数执行完后,将栈顶的对象移除(出栈)
-
当所有的代码执行完后, 栈中只剩下window
面试题(妙啊)
/*
测试题1: 先变量提升,再函数提升
*/
function a() {}
var a;
console.log(typeof a) //’function‘
/*
测试题2:
*/
if (!(b in window)) {
var b = 1;
}
console.log(b) //undefined
/*
测试题3:
*/
var c = 1
function c(c) {
console.log(c)
var c = 3
}
c(2) //报错!!! 秒啊!!!
//以上代码相当于
var c
function c(c) {
console.log(c)
var c = 3
}
c=1
c(2)
//先变量提升,再函数提升,再赋值!!!
复习
- 理解
- 执行上下文: 由js引擎自动创建的对象, 包含对应作用域中的所有变量属性
- 执行上下文栈: 用来管理产生的多个执行上下文
- 分类:
- 全局: window
- 函数: 对程序员来说是透明的
- 生命周期
- 全局 : 准备执行全局代码前产生, 当页面刷新/关闭页面时死亡
- 函数 : 调用函数时产生, 函数执行完时死亡
- 包含哪些属性:
- 全局 :
- 用var定义的全局变量 ==>undefined
- 使用function声明的函数 ===>function
- this ===>window
- 函数
- 用var定义的局部变量 ==>undefined
- 使用function声明的函数 ===>function
- this ===> 调用函数的对象, 如果没有指定就是window
- 形参变量 ===>对应实参值
- arguments ===>实参列表的伪数组
- 全局 :
- 执行上下文创建和初始化的过程
- 全局:
- 在全局代码执行前最先创建一个全局执行上下文(window)
- 收集一些全局变量, 并初始化
- 将这些变量设置为window的属性
- 函数:
- 在调用函数时, 在执行函数体之前先创建一个函数执行上下文
- 收集一些局部变量, 并初始化
- 将这些变量设置为执行上下文的属性
- 全局:
作用域与作用域链
作用域
-
理解
- 就是一块"地盘", 一个代码段所在的区域
- 它是静态的(相对于上下文对象), 在编写代码时就确定了
- 这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
静态作用域:输出1
动态作用域:输出2
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); //1
- 分类
- 全局作用域
- 函数作用域
- 没有块作用域(ES6有了)
- 作用
- 隔离变量,不同作用域下同名变量不会有冲突
- 为什么要有块级作用域
- 解决变量覆盖问题
- 解决循环变量变成全局变量
作用域与执行上下文
- 区别1
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文环境是在调用函数时, 函数体代码执行之前创建
- 区别2
- 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
- 执行上下文环境是动态的, 调用函数时创建, 函数调用结束时上下文环境就会被释放
- 联系
- 执行上下文环境(对象)是从属于所在的作用域
- 全局上下文环境==>全局作用域
- 函数上下文环境==>对应的函数使用域
作用域链
- 理解
- 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
- 查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
- 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
- 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
完整执行过程
如下例子:
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
执行过程如下:
1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [
globalContext.VO
];
2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
];
3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
5.第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
总结:
- 函数被创建,保存作用域链到 内部属性[[scope]]
- 创建 checkscope 函数执行上下文,并压栈
- 函数执行前的准备工作:
- 复制函数[[scope]]属性创建作用域链,
- 初始化活动对象,加入形参、函数声明、变量声明
- 将活动对象压入 checkscope 作用域链顶端
- 执行函数
- 出栈
所以闭包的底层原理就可以知道了
闭包底层原理
从执行上下文的角度来看:
压栈入栈顺序为:全局入栈—fn1入栈—fn1出栈—fn2入栈—fn2出栈—全局出栈
问题:fn1的执行上下文已经被销毁了,为什么fn2还可以访问fn1的变量a??
理由:fn2的作用域链还保存着fn1的活动对象
fn2Context = {
Scope: [AO, fn1Context.AO, globalContext.VO],
}
详细介绍下闭包
理解闭包
我想实现a++的功能,但是我不想让外部直接操作a(避免外部随意修改a),所以我封装了一个内部函数来实现a++的功能,并把这个内部函数暴露出去,外部可以调用我这个内部函数来实现a++的功能,但是无法直接接触到a
闭包=「函数」+「函数内部能访问到的变量」
-
如何产生闭包?
- 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
-
闭包到底是什么?
- 使用chrome调试查看
- 理解一: 闭包是嵌套的内部函数(绝大部分人)
- 理解二: 包含被引用变量(函数)的对象(极少数人)
- 注意: 闭包存在于嵌套的内部函数中
-
产生闭包的条件?
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
常见的闭包
-
将函数作为另一个函数的返回值
-
将函数作为实参传递给另一个函数调用
📘Foo调用几次就产生几个闭包
function Foo() {
var x = 0;
function Foo1() {
++x;
console.log(x);
};
return Foo1;
};
var a = new Foo();
a();//1
a();//2
var b = new Foo();
b();//1
注意:这种情况,闭包的多个实例都是共享同一个 x 变量。
var Foo = (function() {
var x = 0;
function Foo() {}
Foo.prototype.increment = function() {
++x;
console.log(x);
};
return Foo;
})();
var a = new Foo();
a.increment();//1
a.increment();//2
var b = new Foo();
a.increment();//3
闭包的作用
- 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
- 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
-
函数执行完后, 函数内部声明的局部变量是否还存在?
一般不存在,存在于闭包中的变量才可能存在
如果定义了一个变量来保存,则闭包中的局部变量会存在 var f=fun1(); f();,直到f成为垃圾对象即f=null时,才不存在
如果直接调用,则不存在了fun1();
-
在函数外部能直接访问函数内部的局部变量吗?
不能,但是可以通过闭包让外部操作局部变量(暴露函数)
闭包的生命周期
- 产生: 在嵌套内部函数定义完时就产生了(不是在调用)
- 死亡: 在嵌套的内部函数成为垃圾对象时
<script type="text/javascript">
function fun1() {
//问题2: 此时闭包产生了吗? --产生了(函数提升,内部函数对象已经创建了)
var a = 3;
function fun2() {
a++;
console.log(a);
}
return fun2;
}
//问题1: 此时闭包产生了吗? --产生了
var f = fun1();
//问题3: 此时闭包释放了吗? --没有
f();
f();
//问题4: 此时闭包释放回收了吗? --没有
//问题5: 如何让闭包释放回收呢?
f=null; //闭包死亡(包含闭包的函数对象成为垃圾对象)
</script>
闭包的应用
- 封装私有变量:通过闭包可以创建私有变量,只有内部函数可以访问和修改这些变量,外部无法直接访问。这种方式可以实现数据的封装和隐藏,提高代码的安全性和可维护性。
- 延迟执行:通过闭包可以实现延迟执行某个函数,可以在外部函数执行完毕后,仍然可以访问外部函数的变量和参数,实现一些异步操作。
- 保存状态:闭包可以保存函数的状态,即使函数执行完毕后,闭包仍然可以访问和修改函数的内部变量和参数。这在一些需要记住上下文状态的场景中非常有用,比如计数器、缓存等。
- 高阶函数的应用:闭包在高阶函数中经常被使用,可以用来创建一些特定的函数,比如柯里化函数、偏函数等,实现单例模式,实现缓存。
- 事件处理程序:在事件处理程序中,闭包可以用来保存事件的状态和上下文信息,以便在事件触发时使用。
-
定义JS模块
-
具有特定功能的js文件
-
将所有的数据和功能都封装在一个函数内部(私有的)
-
只向外暴露一个包含n个方法的对象或函数
-
模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-
方法一:
-
方法二:更佳
-
-
模块化: 封装一些数据以及操作数据的函数, 向外暴露一些行为
-
函数柯里化
// 假设我们有一个求长方形面积的函数
function getArea(width, height) {
return width * height
}
// 如果我们碰到的长方形的宽老是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)
// 我们可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
return height => {
return width * height
}
}
const getTenWidthArea = getArea(10)
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)
// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)
- 循环遍历加监听
- JS框架(jQuery)大量使用了闭包
闭包的缺点及解决
-
缺点
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
- 容易造成内存泄露
-
解决
- 能不用闭包就不用
- 及时释放 : f = null; //让内部函数对象成为垃圾对象
面试题(妙啊)
//面试题1
var x = 10;
function fn() {
console.log(x); //10 因为作用域在函数定义时就已经确定了,fn的上级是window
}
function show(f) {
var x = 20;
f();
}
show(fn);
//面试题2
var fn = function () {
console.log(fn) //function () {console.log(fn) }
}
fn()
var obj = {
fn2: function () {
console.log(this.fn2) //function () {console.log(fn2);console.log(fn2);}
console.log(fn2) //报错,找不到fn2
}
}
obj.fn2()
复习
- 理解:
- 作用域: 一块代码区域, 在编码时就确定了, 不会再变化
- 作用域链: 多个嵌套的作用域形成的由内向外的结构, 用于查找变量
- 分类:
- 全局
- 函数
- js没有块作用域(在ES6之前)
- 作用
- 作用域: 隔离变量, 可以在不同作用域定义同名的变量不冲突
- 作用域链: 查找变量
- 区别作用域与执行上下文
- 作用域: 静态的, 编码时就确定了(不是在运行时), 一旦确定就不会变化了
- 执行上下文: 动态的, 执行代码时动态创建, 当执行结束消失
- 联系: 执行上下文环境是在对应的作用域中的