前言
作用域链是js的一个概念,和执行上下文相关。关于执行上下文的描述,在上一篇文章《js没那么简单(1)-- 执行上下文》已经说了。
讨论作用域链的意义在于:
- js的作用域关系和作用域链息息相关
- 作用域链是闭包的基础
- 作用域链对后面理解垃圾回收也有一定关系
静态vs动态
讨论作用域链之前,不妨先来搞几个头脑风暴。关于静态和动态,我们经常会听到,js是一门动态语言,我们也经常会听到,js是一门静态作用域语言,我们还经常听到,CMD是一种动态模块规范。于是乎,我们发现,静态和动态,似乎,在不同语义下,对同个语言描述也是不同的。
- 我们之所以说js是动态编程语言,是基于js的变量定义描述的,意思是,js可以在运行时变量类型是允许改变的。噢,很好理解,因为js的var确实支持运行时不同类型的改变。如一下代码:
var a = 1
a = 'string'
这是可以的。
- 我们之所以说js是静态作用域,是因为我们发现js的作用域好像不是在运行时定义。仿佛是在我们写好代码就定义好了。如以下代码:
var a = 2;
function back() {
console.log(a);
}
function next() {
var a = 3;
back(); // 2 but not 3
}
next()
确实是:有点闭包那味,但是却和闭包不同。
- 我们之所以说CMD是动态模块规范,是因为require导入的模块,竟然能运行时改变。如以下代码:
let moduleAorB = null;
if () {
moduleAorB = require('a')
} else {
moduleAorB = require('b')
}
相反,举一反三,我们知道import必须在一开始导入,且不给更改,所以import是静态模块。
这时候。我们渐渐懂了静态和动态的含义了,他讲的是在一开始就定义,或者在运行时才确定的一个边界。那这个和我们这次要聊的作用域链有关系吗?是有关系的,因为js是静态作用域。
词法作用域
词法作用域的意思是:指在词法阶段确定的作用域,叫词法作用域。通过前面文章描述。我们知道词法阶段是编译原理一开始解析代码的阶段,所以词法解析意味着代码写完就确定了作用域,所以我们说词法作用域就是静态作用域。
所以我们前面的例子:
var a = 2;
function back() {
console.log(a);
}
function next() {
var a = 3;
back(); // 2 but not 3
}
next()
结果之所以是2,而不是3 的原因。是因为back函数,在声明时就确定了其所在上级作用域是全局作用域。所以在执行时候,即使执行作用域是在next里面,依然先访问到自己作用域的上级作用域。这是因为他在词法阶段就确定了其上级作用域是全局作用域。
这是词法作用域的基本特性。
作用域链
再一次提到作用域链,那这一次。我们直接认为,作用域链是执行上下文里面的一个变量[[scope]]。而我们前面提到,执行上下文是在词法阶段确定的结构。所以作用域链也是在词法阶段确定的关系。
在v8源码中,scope链变量在词法解析被描述为如链表一般的结构,可以通过out_scope拿到该环境下的上一层作用域链。
在简单情况下。我们可以将其抽象的看成是执行上下文中的一个变量[[scope]],scope可以通过outer拿到上一层的作用域链。
那么假如一个声明在全局环境下的函数,其上下文环境应该如图所示:
作用域链最大的意义在于,定义环境所能访问的上级环境。进而会有后续闭包的说法。
var a = 2;
function back() {
console.log(a);
}
function next() {
var a = 3;
back(); // 2 but not 3
}
next()
对于上面这段代码执行时的上下文顺序跟作用域顺序是不一样的,在next运行后,back也进入执行上下文,他们的上下文关系是这样的:
但是他们的作用域链关系是这样的:
也就是说,两个函数的直接上级作用域都是全局上下文,原因是因为他们都是声明在全局环境中。假如你希望next函数访问到back函数的上下文,你应该在back函数中声明next函数。那这个时候next称做back的闭包。关于闭包的细节,不仅仅在于我们这里简单去在函数中声明另一个函数。更在于在垃圾回收中,如何让闭包不被回收,是更值得探讨的问题。
另外,在浏览器控制台,可以通过打断点,看到当前所处点作用域链集合:
关于作用链,我的认识就是这样。the end