函数的创建

一个函数的创建会在 Heap 堆内存中开辟一块空间来存储函数。对象在创建会在堆内存中存储对象的键值对,而函数在堆内存中会存储三部分东西:

  • 作用域:[[scope]]
  • 函数字符串
  • 键值对

例如:

  1. let a = [1,2];
  2. function fn(b) {
  3. b[0] = 3;
  4. b = [4];
  5. b[1] = 5;
  6. console.log(b);
  7. }
  8. fn(a);
  9. console.log(a);

前端基石:函数的底层执行机制 - 图1
对象虽然包含函数,但是函数的创建和其他对象的创建在存储内容上存在一些差别:

  1. 在 Heap 堆内存中分配一块空间(16进制地址)。
  2. 存储相关的内容:
    1. 函数对象存储三部分
      1. 创建函数的时候声明作用域[[scope]],函数在那个上下文中创建,其作用域就是谁。
      2. 把函数体中的代码当做字符串存储起来。
      3. 把函数当做普通对象存储一些键值对
    2. 其余对象存储键值对
  3. 把创建的内存地址赋值给对应的变量或者函数,以便后续的操作。

函数的执行

1. 创建私有上下文

函数一旦执行,就会创建一个全新的私有上下文「进栈」,函数每一次执行 都是重新形成一个全新的私有上下文,和之前执行产生的上下文没有必然的联系,函数中的代码都是在私有上下文中进行执行的。函数进栈执行会创建一个私有变量对象 AO(Active Object),这里区分开 VO。AO 是 VO 的一个分支。在私用上下文中创建的变量都会存储在 AO 中,例如形参、变量提升和函数中定义的变量。

2. 完成初始化操作

完函数进栈后,正式开始执行之前,会进行一些初始化的操作:

  • 初始化作用域链<<自己的私有上下文,作用域>>。
  • 初始化 this (箭头函数没有这一步)。
  • 初始化 arguments (箭头函数没有这一步)。
  • 形参赋值。
  • 变量提升。

    3. 代码执行

    将堆内存中存在的代码字符串从上往下的顺序进行执行。

4. 出栈释放或者保留

正常情况下,代码执行完成之后,私有上下文出栈被回收。但是遇到特殊情况,如果当前私有上下文执行完成之后中的某个东西被执行上下文以外的东西占用,则当前私有上下文就不会出栈释放,也就是形成了不被销毁的上下文,闭包。除了这上面两种情况还有一种情况是,上下文没有被占用,但是要紧接着被用一次,这样没有用完之前是不能释放的,用完在释放,这样就形成了一个临时不被释放 )。

函数每一次执行都是重新形成一个全新的私有上下文,意思就是如果一个函数被执行100次,也会对上面的步骤走100次,并且形成100个执行上下文。正常情况下,函数执行完就会出栈被释放,但是如果不能被释放(闭包),就会一直被保存在内存中,当达到栈内存的极限时就会出现爆栈。

还是用这段代码为例:

  1. let a = [1,2];
  2. function fn(b) {
  3. b[0] = 3;
  4. b = [4];
  5. b[1] = 5;
  6. console.log(b);
  7. }
  8. fn(a);
  9. console.log(a);

前端基石:函数的底层执行机制 - 图2