作用域和闭包

作用域

全局作用域
函数作用域
块级作用域(ES6 新增)

//ES6 块级作用域
if (true) {
  let x = 100;
}
console.log(x); // 会报错

this

this取什么值,是在函数执行时确定的,不是在函数定义时确定的;

this的不同应用场景,如何取值:

当做普通函数被调用      window
使用call apply bind   传什么绑定什么
作为对象方法调用        对象本身
在class方法中调用      实例本身
箭头函数              取值是取上级作用域的this.

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

图片
  • AO:Activive Object,即函数的活动对象。
  • VO:Variable Object,即变量对象。

AO 和 VO 的关系:

AO 可以理解为 VO 的一个实例,也就是 VO 是一个构造函数,然后 VO(Context) === AO,所以 VO 提供的是一个函数中所有变量数据的模板。

图片
函数执行上下文中作用域链和变量对象的创建过程
// 1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [
    globalContext.VO
];

// 2.执行 checkscope 函数,创建 checkscope 函数执行上下文,
//   checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
    checkscopeContext,
    globalContext
];

// 3.checkscope 函数并不立刻执行,开始做准备工作,
//   第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
    Scope: checkscope.[[scope]],
}

// 4.第二步:用 arguments 创建活动对象,
//   随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    }Scope: checkscope.[[scope]],
}

// 5.第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

// 6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}

// 7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
    globalContext
];

闭包

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

闭包 = 函数 + 函数能够访问的自由变量

闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找 不是在执行的地方!!!

作用域应用的特殊情况,有两种表现:

  1. 函数作为参数被传递
  2. 函数作为返回值被返回

ECMAScript中,闭包指的是:

  • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  • 从实践角度:以下函数才算是闭包: 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回) 在代码中引用了自由变量
示例
var data = [];

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

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

// 答案是都是 3,让我们分析一下原因:

// 当执行到 data[0] 函数之前,此时全局上下文的 VO 为:
globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

// 当执行 data[0] 函数的时候,data[0] 函数的作用域链为:
data[0]Context = {
    Scope: [AO, globalContext.VO]
}

/*
 data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,
 所以打印的结果就是 3。

 data[1] 和 data[2] 是一样的道理。
*/

所以让我们改成闭包看看:

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。

 data[1] 和 data[2] 是一样的道理。
*/

自由变量

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

一个变量在当前作用域没有定义,但被使用了,向上级作用域,一层一层依次寻找,直至找到为止
如果到全局作用域都没找到,则报错 xx is not defined

实际开发中闭包的应用

闭包隐藏数据,只提供 API

function createCache() {
  const data = {}; // 闭包中的数据,被隐藏,不被外界访问
  return {
    set: function (key, val) {
      data[key] = val;
    },
    get: function (key) {
      return data[key];
    },
  };
}

const c = createCache();
c.set("a", 100);
console.log(c.get("a"));

手写 bind 函数 链接

参考文章:JavaScript 深入之作用域链open in new window 图解 Javascript 中的 VO,AO,作用域open in new window 浅谈 js 执行的 AO/VOopen in new window JavaScript深入之闭包open in new window

最后更新时间:
贡献者: DESKTOP-ER5718D\zt