与JS 中的作用域一同出现的还有一个执行上下文(execution context)的概念,这两个概念容易混淆,今天就来聊聊他们。
作用域
作用域是指程序源代码中定义变量、函数的区域,它规定了变量和函数可以访问哪些数据以及他们的行为。简单来说就和我们的行政划分是一样的,假如我在湖北省,那么在湖南省就找不到我的信息。
作用域本质上是一个对象, 作用域中的变量、函数可以理解为是该对象的成员,并且 JS 是词法作用域,函数的作用域在函数定义的时候就决定了,也就是说 JS 中函数的作用域在代码编写阶段就确定了。
有一个内部属性 [[scope]]
(仅供 JS 引擎调用),它是一个数组,当函数创建的时候,就会保存所有父级变量对象到其中,这就是所谓的 作用域链 。
下面这段代码打印的是 123,充分说明了函数的作用域是在声明函数时就确定了的,否则如果在运行时才确定,那么 fn 所在就是 show 函数,继而在 test 函数作用域内,输出的就应该是 345 了。
var b = 123;
function fn() {
console.log(b);
}
function test(fn) {
let b = 345;
function show() {
fn();
};
show();
}
test(fn);
JS 中有全局作用域和函数作用域,全局作用域浏览器中就是 window 下(红色框),函数作用域就是函数内部(黄色框),ES6 新增了块级作用域(蓝色框),一对 {} 会生成一个新的作用域。作用与内部的成员不能被其父级作用域访问但可以被子级作用域访问。
在 Web 浏览器中,全局作用域被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。
在 Node环境中,全局作用域是 global 对象。
作用域的作用
安全:变量只能在特定的区域内才能被访问,外部环境不能访问内部环境的任何变量和函数,可以避免在程序其它位置意外对某个变量做出修改导致程序发生事故。
减轻命名的压力:可以在不同的作用域内定义相同的变量名,并且这些变量名不会产生冲突。
执行上下文
前端的朋友都知道 JS 代码是从上到下顺序执行的,下面这段代码就会顺序在控制台打印 1 2 3
console.log(1);
console.log(2);
console.log(3);
你可能会以为 JS 引擎会一行一行地编译并执行程序,其实不然 JS 代码是按块执行的,块的划分标准就是:全局代码、函数代码、eval代码。在分析执行的时候就按照全局代码、函数代码···这样一块一块的执行。在执行一块代码的时候就会创建执行上下文。
执行上下文有一个特点:当函数执行的时候创建执行上下文,函数执行完毕就销毁该执行上下文。
三个重要属性
- 变量对象:
每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。我们编写的代码无法访问。只有当进入一个执行环境时,这个执行上下文的变量对象才会被激活,此时成为活动对象,只有活动对象上的属性才能被访问。 - 作用域链:
当代码在一个环境中执行时,会创建变量对象的一个作用域链。保证对执行环境有权访问的所有变量和函数的有序访问。 - this
执行上下栈
我们在执行一个 JS 程序的时候肯定会有函数之间的调用关系之类的,那就说明,同一时刻会创建多个执行上下文,JS 此时就会创建执行上下文栈结构来管理所有的执行上下文。
看下面这段代码及分析。在执行 JS 代码时,最先遇到全局代码,首先创建全局执行上下文,并将全局上下文压入执行上文栈中,又遇到 test 函数调用,所以又创建函数上下文并压入栈中,函数代码执行完后再把函数执行上下文从栈中弹出销毁,继续执行全局代码发现也执行完毕,就将全局执行上下文从栈中弹出销毁,结束。这里我们可以发现全局执行上下文永远是第一个被创建压栈,最后一个弹栈销毁。
var a = 123;
function test() {
let b = 345;
}
test();
执行上下文的细节
说了这么多执行上下文,那么它到底是个什么东西呢?JS 引擎在它创建到销毁又会做些什么工作呢?
创建
此时还没有执行代码,根据函数的[[scope]]
属性创建作用域链,用 arguments 创建活动对象并初始化,形参为传入的值,内部变量声明后均为 undefined ,函数声明。如下例子
function test() {
let b = 345;
function me() { };
if (b > 200) {
console.log('b大于200')
}
}
test();
此时的活动对象为
AO = {
arguments: {
length: 0
},
b: undefined,
me: reference to function me(){},
}
再将活动对象插到作用域链的首部。
执行代码
此时的活动对象为
AO = {
arguments: {
length: 0
},
b: 345,
me: reference to function me(){},
}
变量查找
在执行console.log(b)
这一行代码时,会先从作用域链的首部开始查找即当前函数的活动对象,找到就输出值,找不到就在后边一个作用域(即父级作用域)中找,如果已经到了全局作用域,仍然找不到该变量,就会直接报错。
var b = 123;
function fn() {
console.log(b);
}
function test(fn) {
let b = 345;
function show() {
fn();
};
show();
}
test(fn);
此时我们再来分析一下这段代码:
- 函数声明后:
fn.[[scope]]
:[全局变量对象]test.[[scope]]
:[全局变量对象]show.[[scope]]
:[test 变量对象,全局变量对象]
- 开始运行后
fn 作用域链
:[fn 函数活动对象,全局活动对象]test 作用域链
:[test 函数活动对象,全局活动对象]show 作用域链
:[show 函数活动对象,test 函数活动对象,全局活动对象]
- 执行
console.log(b);
时:从 fn 函数的作用域链首部(fn 函数活动对象)开始查找变量 b,没找到,在全局活动对象中查找变量 b,找到了,打印。
销毁
test 函数执行完毕,将执行上下文弹栈销毁。
我是孤城浪人,一名正在前端路上摸爬滚打的菜鸟,欢迎你的关注。