前言
作用域是指程序中能定义变量或者函数的范围,可以理解为地盘,是一个抽象的概念。
作用域分为静态作用域和动态作用域,在JavaScript中采用的是静态作用域,也称词法作用域。
正文
静态作用域
静态作用域是指声明的作用域在程序被定义的时候就确定的,在JavaScript中,当函数定义的时候,函数作用域就被创建了,此时的作用域就是静态作用域。
看如下代码:
'use strict';
var a = 1;
function fn1() {
console.log(a);
};
function fn2() {
var a = 2;
fn1(); // 1
};
fn2();
复制代码
当函数fn1被调用的时候,fn1函数内部会去找变量a,先从函数内部开始找,如果没找到,则从定义fn1函数的上一层作用域找,结果找到了a,值为1。所以上面的代码运行结果是会在控制台打印1。
作用域链
首先需要知道几个概念:
每个函数内部都有一个[[scope]]的属性(这个属性仅供js引擎使用),当函数被创建的时候,[[scope]]属性会保存创建该函数的作用域中的所有对象(Variable Obejct)。
每个函数被调用的时候,会产生一个执行上下文的内部对象,这个对象里面包含了:
- 作用域链([[scope]])
- 活动对象(Active Object)
- this
说明: 执行上下文在函数执行完毕之后,会自动销毁,他里面的作用域链是复制的函数内部的属性[[scope]]。
通过下面代码的执行过程,来深入理解作用域链:
'use strict';
var a = 1;
function fn1(x) {
var b = 2;
return (a + b + x);
};
fn1(1);
复制代码
执行过程如下:
1、创建全局执行上下文环境(Global Execution context,下面简称globalEC),全局执行上下文压入执行上下文栈(Execution context stack,下面简称ECStack)。
// 伪代码
//创建全局执行上下文环境
globalEC = {}
//全局执行上下文压入执行栈中
ECStack = [
globalEC
]
复制代码
2、全局执行上下文环境初始化,即预解析阶段,会进行函数提升与变量提升
// 伪代码
//全局上下文环境初始化,GO代表的是全局作用域中保存的变量
globalEC = {
this: [global],
GO: {
[global]: [global],
this: [global],
a: undefined,
fn1: function() {//...}
}
}
// 函数fn1被创建时,fn1内部的[[scope]]被赋值
fn1.[[scope]] = [globalEC.GO]
复制代码
3、进入执行阶段,代码按预解析后的顺序执行,并给变量赋值
// 伪代码,边执行边给变量赋值
globalEC = {
this: [global],
GO: {
[global]: [global],
this: [global],
a: 1,
fn1: function() {//...}
}
}
复制代码
4、当执行fn1()时,创建fn1的执行上下文(fn1 Execution context,下面简称fn1EC),fn1函数的执行上下文压入栈中,处于活动状态
// 伪代码
//创建fn1的执行上下文
fn1EC = {}
//fn1函数的执行上下文压栈
ECStack = [
fn1Ec,
globalEC
]
复制代码
5、fn1函数进入预编译阶段,产生活动对象AO,活动对象AO被保存到作用域链的顶端,
// 伪代码
//fn1函数进入预编译阶段,产生活动对象(Active object,下面简称AO)
fn1EC = {
this: undefined, // 在严格模式下为undefined,非严格模式下为全局对象
AO: {
arguments: {
0: undefined,
length: 1
},
b: undefined
},
[[scope]]: [fn1EC.AO, globalEC.GO]
}
复制代码
6、当fn1函数进入执行阶段
// 伪代码
fn1EC = {
this: undefined, // 在严格模式下为undefined,非严格模式下为全局对象
AO: {
arguments: {
0: 1,
length: 1
},
b: 2
},
[[scope]]: [fn1EC.AO, globalEC.GO]
}
复制代码
7、fn1函数内部引用了变量a, 但是内部并没有定义,此时就会沿着从 AO -> GO 这条作用域链去找,于是在全局作用域 GO 中找到了a的值。如果一直到全局作用域没有找到,那么此时就会报错了。
8、当fn1函数执行完毕,fn1的执行上下文出栈,并销毁,此时回到全局执行上下文环境。
// 伪代码,fn1函数的执行上下文出栈,此时回到全局执行上下文环境
ECStack = [
globalEC
]
复制代码
说明:fn1函数中this的值,会因不同的执行上下文环境而变化。
以上就是上面代码的执行过程,代码无任何意义,只为理解执行上下文的作用域链。
总结
- JavaScript中的函数的作用域是在函数定义的时候就已经创建了,属于静态作用域,也叫词法作用域。
- 同一个作用域下,不同的调用会产生不同的执行上下文。
- 作用域只是一个抽象的概念,具体变量的赋值是在代码的执行阶段,在执行上下文中。