作用域和闭包
作用域
全局作用域
函数作用域
块级作用域(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
];
闭包
闭包是指那些能够访问自由变量的函数。
闭包 = 函数 + 函数能够访问的自由变量
闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找 不是在执行的地方!!!
作用域应用的特殊情况,有两种表现:
- 函数作为参数被传递
- 函数作为返回值被返回
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 深入之作用域链 图解 Javascript 中的 VO,AO,作用域 浅谈 js 执行的 AO/VO JavaScript深入之闭包