JavaScript执行上下文、闭包与内存管理深度解

44 阅读5分钟

执行上下文

可执行代码(executable code) 的类型有全局代码、函数代码、eval代码

当 JavaScript 执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)

JavaScript 引擎创建执行上下文栈(Execution context stack,ECS)来管理执行上下文

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

函数执行上下文中作用域链和变量对象的创建过程

  • 函数创建,保存所有父级变量对象到函数内部属性 [[scope]]
  • 执行函数,创建函数执行上下文,函数执行上下文被压入执行上下文栈
  • 复制函数 [[scope]] 属性创建作用域链
  • 用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
  • 函数激活将活动对象压入作用域链顶端
  • 开始执行函数,随着函数的执行,修改 AO 的属性值
  • 函数执行完毕,函数上下文从执行上下文栈中弹出

变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

全局上下文

  1. 通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性
  2. this 引用全局对象
  3. parseInt() 函数

函数上下文

活动对象(activation object, AO)来表示变量对象

执行上下文的代码会分成两个阶段进行处理

  • 初始化
  • 进入执行上下文;
  • 代码执行;

初始化

函数上下文的变量对象初始化只包括 Arguments 对象

进入执行上下文

变量对象包括:函数的所有形参、函数声明、变量声明

函数的所有形参
  • 由名称和对应值组成的一个变量对象的属性被创建
  • 没有实参,属性值设为 undefined
AO = {
    arguments: {
        0: 1,
        length: 1
    }
}
函数声明
  • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
  • 如果变量对象已经存在相同名称的属性替换这个属性
AO = {
    a: reference to function c(){},
}
变量声明
  • 由名称和对应值(undefined)组成一个变量对象的属性被创建
  • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
AO = {
    a: undefined,
}

代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

作用域链

多个执行上下文的变量对象构成的链表就叫做作用域链。

函数创建

函数创建的时候,就会保存所有父级变量对象到函数内部属性 [[scope]]

foo.[[scope]] = [
    globalContext.VO
];

函数激活

当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。

this

Reference

  • base value 属性所在的对象 EnvironmentRecord
  • referenced name 属性的名称
  • strict reference 严格模式
  • GetBase 返回 reference 的 base value
  • IsPropertyReference 如果 base value 是一个对象,返回 true
  • MemberExpression 函数括号左边的部分
  • GetValue 返回对象属性真正的值

判断是否 Reference

  • base value 基本类型
  • referenced name 字符串属性名
  • strict mode 严格模式

如何确定 this 的值

  • 计算 MemberExpression 的结果赋值给 ref
  • 判断 ref 是不是一个 Reference 类型
    • 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
    • 如果 ref 是 Reference,并且 base value 是 Environment Record, 那么this 的值为 undefined
    • 如果 ref 不是 Reference,那么 this 的值为 undefined

this 绑定规则

调用方式this 指向示例
默认绑定全局对象foo()
隐式绑定调用对象obj.foo()
显式绑定指定对象foo.call(obj)
new 绑定新创建对象new Foo()
箭头函数词法作用域() => this.x
Function.prototype.newCall = function(context, ...parameter) {
  if (typeof context === 'object') {
    // null 指向 window
    context = context || window
  } else {
    // 不是对象创建一个对象
    context = Object.create(null)
  }
  let fn = Symbol()
  // 函数挂载对象上面
  context[fn] = this
  // 执行函数
  let res = context[fn](...parameter);
  // 删除属性
  delete context[fn]
  return res
}

Function.prototype.newApply = function (context, parameter) {
  if (typeof context === "object") {
    context = context || window;
  } else {
    context = Object.create(null);
  }
  let fn = Symbol();
  context[fn] = this;
  let res = !parameter ? context.fn() : context[fn](...parameter);
  delete context[fn];
  return res;
};
Function.prototype.newBind = function (context, ...innerArgs) {
  let that = this;
  return function (...finnalyArgs) {
    return that.call(context, ...innerArgs, ...finnalyArgs);
  };
};

闭包

闭包是指那些能够访问自由变量的函数。

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链:

data[0]Context = {
    Scope: [AO, 匿名函数 Context.AO, globalContext.VO]
}

匿名函数执行上下文的AO为:

匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。

作用

  • 保存状态
  • 实现私有变量
  • 模块化与封装
  • 回调函数与异步编程
  • 高级函数,柯里化、组合
  • React Hook

内存泄漏

  • 循环引用导致内存泄露
  • 创建的定时器未被清理
  • 全局变量或静态变量过多导致的内存泄露
  • 事件绑定没有及时处理

Chrome 开发者工具中,可以通过 Memory(内存)  面板,使用 Heap Snapshot(堆快照)来查看对象的引用关系

在设计数据结构时,尽量避免互相引用,尤其是大的复杂对象

使用 WeakMapWeakSet