闭包是如何产生的

85 阅读4分钟

闭包

为什么会产生闭包

上下文

函数在代码执行阶段会创建一个执行上下文,执行上下文分为函数执行上下文,全局执行上下文

函数执行栈

函数执行赖以生存的环境,在函数执行过程中会创建函数执行上下文,并推入执行栈中

上下文生命周期

  • 全局执行上下文: 存活到页面关闭

  • 函数执行上下文,函数一旦退出执行上下文就会被销毁

    函数是如何创建上下文的

    我们都知道函数在执行阶段会被解析为执行上下文推入执行栈,环境对象中包含了变量环境(函数形参,var声明的变量),词法环境

    function setName(name) {
      var age = 24;
    }
    ​
    setName('zhangsan ')
    ​
    // 上述代码会在代码编译阶段,编译成
    ​
    let environment = {
      variable: {
        name: undefined,
        age: undefined,
        outer: window,
        this: undefined
      },
      code: `name = 'zhangsan' age =24; console.log(age)`
    }
    ​
    // 代码执行阶段, 比如先执行 name = 'zhangsan';
    environment = {
      variable: {
        name: 'zhangsan',
        age: undefined,
         outer: window,
        this: undefined,
      },
      code: `age =24;console.log(age)`
    }
    ​
    // 继续执行 age = 24
    environment = {
      variable: {
        name: 'zhangsan',
        age: 24,
         outer: window,
        this: undefined,
      },
      code: ``
    }
    ​
    

    函数执行的过程

    创建执行上下文

    在函数执行时将会创建一个执行上下文并推入栈中

    function getName() {
      getAge();
    }
     
    function getAge() {
      return '444'
    }
    ​
    getName();
    ​
    

    执行上文有哪些内容

    • 变量环境: 存放变量
    • outer: 用来存放对上级作用域的引用
    • this: 用来存放函数执行的目标对象
    • 词法环境: 存放函数内部可执行代码
    const environment1 = {
      variable: {
      },
      code: `getAge()`
    }
    ​
    const environment2 = {
        variable: {
          outer: environment1
        },
        code: `return 444`
      }
    }
    ​
    const globalEnvironment = {
       variable: {
         getName: function getName,
         getAge: function getAge,
      },
      code: `getName()`
    }
    ​
    

    函数执行过程

    全局上下文进入执行栈
    const stack = [];
    stack.push(globalEnvironment);
    
    getName 函数进入执行栈

    执行到getName()的时候

    stack.push(environment2)
    

    此时栈中存在2个环境对象

    stack = [globalEnvironment, environment1];
    
    getAge函数进入执行栈

    执行到getAge()的时候

    stack.push(environment2)
    

    此时栈内存中存在3个对象

    stack = [globalEnvironment, environment1, environment2];
    
执行上下文被销毁存在的问题

当environment2执行完毕,就会被栈中弹出,并且销毁environment2中的环境变量,继续执行environment1,当environment1执行完以后就会被栈弹出,依次类推globalEnvironment执行完毕以后也会被弹出

我们再来看下面这段代码 并且分析一下面的代码

function getName() {
  var name = 'zhangsan'
  function getAge() {
    return name;
  }
  
  return getAge;
}

var getAge = getName();
getAge();
const stack = [];
const globalEnvironment = {
  variable: {
     getName: function getName,
    getAge: undefined,
  },
  code: `getAge = getName(); getAge()`
}

// 当页面打开时
stack.push(globalEnvironment);

// 执行 getAge = getName()时创建environment1活动对象
const environment1 = {
  variable: {
    name: undefined,
    getAge: function getAge
  },
  code: `name = zhangsan; return getAge`
}
stack.push(environment1);

// 执行getAge()时
stack.pop();

const environment2 = {
  variable: {
  },
  code: `return name`
}  

stack.push(nvironment2);
函数执行上下文延迟销毁解决问题

如果我们的函数执行上文延迟销毁是不是就可以解决了变量查找中问题, 但是虽然解决了变量查找问题,但是函数延迟销毁会带来执行栈内存溢出问题

closure属性解决函数执行上下文销毁问题

我们发现如果这样执行的时候,当执行到return name的时候,如果我们按照查找量规则去查找会发现找不到这个变量,因为此时环境对象environment1已经不在栈中了如果这段代码执行报错,就违背了在js中函数可以作为函数的返回值

我们别忘了其实函数是一个特殊对象,因此其实函数是可以添加属性的,如果我们给函数添加一个closure属性,即使函数上下文被销毁,但是函数并没有被销毁

function getName() {
  var name = 'zhangsan'
  function getAge() {
    return 'name'// 内部函数引用了name变量 -> getName.closure = { a }
  }
  
  return getAge;
}

var getAge = getName();
getAge();

因此我们需要对下面这段代码改变执行策略

function getName() {
  var name = 'zhangsan'
  function getAge() {
    return 'name';
  }
  
  return getAge;
}

const environment1 = {
  variable: {
    name: undefined,
    getAge: function getAge
  },
  code: `name = zhangsan; return getAge`,
 
}

getName.closure = { name }; // 在编译阶段快速扫描,发现name被内部函数引用,因此会被添加到closure中

stack.push(environment1);

stack.pop();

const environment2 = {
  variable: {
  },
  code: `return name`
}  

stack.push(nvironment2);

每次变量查找的时候优先查找外部函数的closure属性,如果存在停止查找,如果没有查找到,沿着词法作用域进行查找

优化执行上文创建

我们完全可以在函数进行编译阶段进行快速扫描生成执行上文中的变量环境初始化以及上层函数的引用,v8其实就是这么实现的,通过编译器快速扫描,如果发现了内部函数引用了当前变量,那么久将当前变量进行复制并且存到堆中