【javaScript】深入理解闭包、作用域、上下文环境三者之间的关系

351 阅读5分钟

先说结论(开幕雷击)

执行内部函数,在即将要绑定上下文与变量时,通过作用域链去寻找外层的变量,有闭包机制时,外层变量不会消失

执行上下文与执行上下文栈

  • 变量提升与函数提升
    • 变量提升: 在变量定义语句之前, 就可以访问到这个变量(undefined)
    • 函数提升: 在函数定义语句之前, 就执行该函数
    • 两者同时存在时,先有变量提升, 再有函数提升
  • 理解
    • 执行上下文: 由js引擎自动创建的对象, 包含对应作用域中的所有变量属性
    • 执行上下文栈: 用来管理产生的多个执行上下文
  • 分类:
    • 全局: window
    • 函数: 对程序员来说是透明的
  • 生命周期
    • 全局 : 准备执行全局代码前产生, 当页面刷新/关闭页面时死亡
    • 函数 : 调用函数时产生, 函数执行完时死亡
  • 包含哪些属性:
    • 全局 :
      • 用var定义的全局变量 ==>undefined
      • 使用function声明的函数 ===>function
      • this ===>window 注:同样是执行函数定义,注意下面这两种的不同
    function f(){ //提前声明f函数
        //todo
   }
    var f = function() { //提前声明变量f,原地赋值函数体
         //todo
    }
    
  • 函数
    • 用var定义的局部变量 ==>undefined
    • 使用function声明的函数 ===>function
    • this ===> 调用函数的对象, 如果没有指定就是window(也可以call、apply更改执行该函数的对象,后文讲到继承静态属性时会有这两个函数的应用
    • 形参变量 ===>对应实参值
    • arguments ===>实参列表的伪数组
  • 执行上下文创建和初始化的过程
    • 全局:
      • 全局代码执行前最先创建一个全局执行上下文(window)
      • 收集一些全局变量, 并初始化
      • 将这些变量设置为window的属性
    • 函数:
      • 在调用函数时, 在执行函数体之前先创建一个函数执行上下文
      • 收集一些局部变量, 并初始化
      • 将这些变量设置为执行上下文的属性

作用域与作用域链

  • 理解:
    • 作用域: 一块代码区域, 在编码时就确定了, 不会再变化(相比于上下文环境,作用域链时静态的,后面有更加详细论述)
    • 作用域链: 多个嵌套的作用域形成的由内向外的结构, 用于查找变量
  • 分类:
    • 全局
    • 函数
    • js没有块作用域(在ES6之前),什么是块作用域?如
    if(e){
        //这里就是块作用域,java中有块作用域的概念
    }
  • 作用
    • 作用域: 隔离变量, 可以在不同作用域定义同名的变量不冲突
    • 作用域链: 查找变量
  • 区别作用域与执行上下文
    • 作用域: 静态的, 编码时就确定了(不是在运行时), 一旦确定就不会变化了
    • 执行上下文: 动态的, 执行代码时动态创建, 当执行结束消失
    • 联系: 执行上下文环境是在对应的作用域中的,通过作用域链将变量绑定到执行上下文环境上
  • 小问题:如果执行上下文环境都是window的话,那么局部变量和全局变量就都是window的属性了,执行全局代码时,不久可以访问局部数据了?有大问题!
  • 小问题回答:如果执行上下文环境都是window的话,那么局部变量和全局变量就都是window的属性了。在局部函数之前,执行全局代码,局部函数的执行上下文环境还没生成呢!在局部函数之后,执行全局代码,之前绑定的局部变量已经销毁了呀,no problem!

闭包

  • 理解:

    • 当嵌套的内部函数引用了外部函数的变量时就产生了闭包
    • 通过chrome工具得知: 闭包本质内部函数中的一个对象, 这个对象中包含引用的变量属性
  • 作用:

    • 延长局部变量的生命周期
    • 让函数外部能操作内部的局部变量
  • 深入理解:

    • 通过作用域链执行上下文环境绑定,使得a一直可以被找到,有了闭包机制,不至于待fn1完成时,变量a就消失了
    • 什么时候会出现多个不会互相干扰的闭包?外部函数多次执行时,显然,外部变量都不一样了 function fn1() { var a = 2; function fn2() { a++; console.log(a); } return fn2; } var f = fn1(); f(); //3 f(); //4 f(); //5
  • 小小补充: 子函数fn2释放,但是子函数没有成为垃圾对象,因为return了,又有东西指向它了(return 函数)

function fn1() {
    var a = 2;
    var fn2 = function() { //区别
      a++;
      console.log(a);
    }
    return fn2;
  }
  var f = fn1();
  f(); //3
  f(); //4
  f(); //5

  • 闭包应用:
    • 模块化: 封装一些数据以及操作数据的函数, 向外暴露一些行为
    • 循环遍历加监听
    • JS框架(jQuery)大量使用了闭包
  • 闭包生命周期:
    • 开始:执行内部函数定义时(不是调用)
    • 结束:null,成为垃圾对象后
  • 缺点:
    • 变量占用内存的时间可能会过长
    • 可能导致内存泄露
    • 解决:
      • 及时释放 : f = null; //让内部函数对象成为垃圾对象

内存溢出与内存泄露

  1. 内存溢出
  • 一种程序运行出现的错误
  • 当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误
  1. 内存泄露
  • 占用的内存没有及时释放
  • 内存泄露积累多了就容易导致内存溢出
  • 常见的内存泄露:
    • 没有及时清理的计时器或回调函数
    • 闭包
    • 意外的全局变量(在局部函数内部,不用var去声明变量,而是直接对变量赋值)
    function f() {
        a = 123;
        console.log(a); //123
    }
    f();
    console.log(a); //123