# 作用域、执行上下文与闭包

# 作用域与作用域链

# 作用域

  • 所有未定义的变量直接赋值会自动声明为全局作用域的变量(隐式全局变量可以用delete删除,var定义的则不行)

    a = 1; // 隐式全局变量 严格模式报错
    var b = 2; // 显式全局变量
    console.log(a, b); // 1 2
    delete a; // 严格模式报错
    delete b; // 严格模式报错
    console.log(b, a); // 2 a is not defined
    
  • window对象的所有属性拥有全局作用域

  • 内层作用域可以访问外层作用域,反之不行

  • var声明的变量,在除了函数作用域之外,在其他块语句中不会创建独立作用域

  • let和const声明的变量存在块语句作用域,且不会变量提升

  • 同作用域下不能重复使用let、const声明同名变量,var可以,后者覆盖前者

  • for循环的条件语句的作用域与其循环体的作用域不同,条件语句块属于循环体的父级作用域

    // 以下语句使用let声明不报错,说明为不同作用域
    for (let i = 0; i < 5; i++) {
      let i = 5;
    }
    -----------------------
    // 此语句报错,说明循环体为条件语句块的子作用域
    for (let i = 0; i < 5; i = x) { // x is not defined
      let x = 5;
    }
    

# 作用域链

  • 作用域链也就是所谓的变量查找的范围

  • 在当前作用域引用变量时,如果没有此变量,则会一路往父级作用域查找此变量,直到全局作用域,如果都没有,在非严格情况下会自动声明,所以是undefined,在严格条件下则会报错

  • 变量的查找路径依据是在创建这个作用域的地方向上查找,并非是在执行时的作用域,如下b变量的值为2。可以看出当执行到需要b变量时,当前作用域下并没有b,所以要定义这个b变量的静态作用域中寻找,即创建时候的作用域链上查找b的值

    b = 1;
    function a() {
      // 定义b,找到
      const b = 2;
      function s() {
    		// 使用到b,当前作用域并没有,向上找
        console.log(b);
      }
      return s;
    }
    const s = a();
    var b = 3;
    s(); // 2
    
  • 作用域在脚本解析阶段就已经规定好了,所以与执行阶段无关,且无法改变

# 执行上下文

  • 执行上下文在运行时确定,随时可能改变
  • 调用栈中存放多个执行上下文,按照后进先出的规则进行创建和销毁,最底部的执行上下文,也就是调用栈低的执行上下文为全局上下文,最早被压入栈中,其上下文中的this指向window,严格模式下为undefined
  • 创建执行上下文时,会绑定当前this,确定词法环境,存储当前环境下函数声明内容,变量let与const绑定但未关联任何值,确认变量环境时,绑定var的初始值为undefined
  • 在var声明之前,调用var声明的变量时值为undefined,因为创建了执行上下文,var声明的变量已经绑定初始undefined,而在let和const声明之前调用其声明的变量时,由于只绑定在了执行上下文中,但并未初始任何值,所以在声明之前调用则会抛出引用错误(即TDZ暂时性死区),这也就是函数声明与var声明在执行上下文中的提升

这里了解一下函数、变量提升

console.dir(foo); // foo() {}
function foo() {}
var foo = 5;
/*
console.dir(foo); // undefined
var foo = 5;
*/
---------
var foo = 5;
function foo() {}
console.dir(foo); // 5

从以上代码结果可以得出结论:

  • 上面代码块能够体现,在解析阶段会将函数与变量提升,且函数的优先级比var声明的变量高,因为打印的是函数声明,如果var声明的优先级高,那么应该是undefined
  • 从下面的代码块中可以看出foo在代码执行的时候被赋值为5,而函数声明在解析阶段已经结束,在执行阶段没有效果
  • 还有一点,在解析阶段,函数声明与变量声明提升之后在代码块中的位置顺序没什么关系

# 闭包

  • 所谓闭包就是函数与其词法环境(创建当前作用域时的任何局部变量)的引用。闭包可以使内部函数访问到外部函数的作用域,当函数被创建时即生成闭包

    function fn1() {
    	var name = 'h1';
      function fn2() {
        console.log(name);
      }
      return fn2;
    }
    fn1()(); // h1
    
  • 当你从函数内部返回一个内部函数时,返回的函数将会保留当前闭包,即当前词法环境

  • 闭包只会保留环境中任何变量的最后一个值,这是因为闭包所保存的是整个变量的对象

  • 闭包的作用域链包含着它自己的作用域,以及包含它父级函数的作用域和全局作用域

  • 当返回一个闭包时,保留此闭包下的所有被外部引用的对象

  • 闭包之间是独立的,在闭包环境下可以创建多个不同的闭包环境暴露给外部,从而实现不同的效果

    function markAdder(x) {
      return function(y) {
        return x + y;
      };
    }
    var add5 = makeAdder(5);
    var add10 = makeAdder(10);
    console.log(add5(2)); // 7
    console.log(add10(2)); // 12
    
  • 暴露闭包的方式不止返回内部函数一种,还可以使用回调函数产生闭包环境,或者把内部函数赋值给其他外部对象使用

  • 闭包在没有被外部使用的情况下,随执行结束销毁,如何产生闭包并且保留闭包环境的关键就在于不让其环境被垃圾回收系统自动清除,那么就要使内部环境中的引用被外部保留,这样才能保留闭包

  • 闭包虽然方便我们操作和保留内部环境,但是闭包在处理速度和内存消耗方面对脚本性能具有负面影响,除非在特定的情况下使用

这里看个有趣的东西

function foo() {
  let a = {name: 'me'};
  let b = {who: 'isMe'};
  let wm = new WeakMap();
  function bar() {
    console.log(a); // a被闭包保留
    wm.set(b, 1); // 弱引用b对象
    return wm; // wm被闭包保留
  }
  return bar;
}
const wm = foo()();
console.dir(wm); // No properties 即为空
-------------------------------------
function foo() {
  let a = {name: 'me'};
  let wm = new WeakMap();
  function bar() {
    console.log(a);
    wm.set(a, 1);
    return wm;
  }
  return bar;
}
const wm = foo()();
console.dir(wm); // 保留了对象a与其值1

从上面代码块中可以看出,bar被return到外部环境,所以其内部形成闭包,bar中使用到的变量(a, wm)都会被保留下来,但是最后打印wm的时候为空?这是因为外部并没有引用到b对象,只是通过wm弱引用保存b的值,从wm为空可以看出,闭包内部的b被清除,所以wm也自动清除b的弱引用,可以论证之前所说,闭包只保留外部用到的变量。

从上面代码块中的第二段代码,能够直接看出a就是闭包中的a,bar在外部执行时需要用到a与wm所以保留了下来。

有人可能会不解,为什么上述代码中的b也被wm.set(b, 1)引用,但是最终就没有呢,那是因为WeakMap中保留的是b的弱引用,可以理解为,wm中的b是依赖原函数中的b而存在,当wm被return时,闭包中的b,没有被任何外部所依赖,而是别人依赖它。可以这么理解:b牵着别人走,因为b没有被外面人牵着走,所以b这个链子就被断开了,也影响到b牵的人一块丢了。

最近更新时间: 2020/5/29 16:30:21