前言
在上一篇文章中讲到了什么是作用域,以及 ES6 是如何通过变量环境和词法环境来同时支持变量提升和块级作用域,在最后我们也提到了如何通过词法环境和变量环境来查找变量,这其中就涉及到作用域链的概念。
作用域链(Scope Chain)
那什么是作用域链呢? 我的理解就是,根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。 这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。 想要知道js怎么链式查找,就得先了解js的执行环境。
执行环境(execution context)
每个函数调用都有自己的上下文,每个上下文都有一个关联的变量对象(variable object), 而这个上下文中定义的所有变量和函数都存在于这个对象上。
全局上下文是最外层的执行上下文。根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的 window 对象,因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的。
举个例子:
var scope = 'global';
function fn1(){
return scope;
}
function fn2(){
return scope;
}
fn1();
fn2();
复制代码
上面代码的执行情况可以用下图来演示。
了解了环境变量,再详细讲讲作用域链。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain),并把作用域链赋值给一个特殊的内部属性([scope])。然后使用this,arguments(全局上下文中没有这个变量)和其他命名参数的值来初始化函数的活动对象(activation object)。代码正在执行的上下文的变量对象始终位于作用域链的最前端。
如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有 一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上 下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终 是作用域链的最后一个变量对象。
代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)
举个例子:
var color = 'blue';
function changeColor() {
if (color === 'blue') {
color = 'red';
} else {
color = 'blue';
}
}
changeColor();
复制代码
对这个例子而言,函数 changeColor()的作用域链包含两个对象:一个是它自己的变量对象(就是定义 arguments 对象的那个),另一个是全局上下文的变量对象。这个函数内部之所以能够访问变量 color,就是因为可以在作用域链中找到它。
闭包(closure)
了解了作用域链,接着我们就可以来聊聊闭包了。 闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。 闭包有两个作用:
- 第一个就是可以读取自身函数外部的变量(沿着作用域链寻找)
- 第二个就是让这些外部变量始终保存在内存中
this对象
关于闭包经常会看到这么一道题:
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentityFunc() {
return function() {
return this.identity;
};
}
};
console.log(object.getIdentityFunc()()); // 'The Window'
复制代码
在闭包中使用 this 会让代码变复杂。如果内部函数没有使用箭头函数定义,则 this 对象会在运行时绑定到执行函数的上下文。如果在全局函数中调用,则 this 在非严格模式下等于 window,在严格模式下等于undefined。如果作为某个对象的方法调用,则this等于这个对象。匿名函数在这种情况下不会绑定到某个对象,这就意味着 this 会指向 window,除非在严格模式下 this 是undefined。