一、什么是作用域
什么是作用域?
作用域指的是程序源代码中定义变量的区域。它规定了当前执行代码对变量的执行权限。
在 JavaScript 这门语言中,采用了词法作用域(即静态作用域),而不是动态作用域。
二、静态作用域与动态作用域
诶,既然提到了这两个作用域,那两者的差别在哪里呢?
在词法作用域中,函数的作用域在函数定义的时候就决定了。
而在动态作用域中,函数的作用域在函数调用的时候才决定。
让我们来看个例子去理解一下这两句话:
let tmp = 0;
function foo() {
console.log(tmp);
}
function bar() {
let tmp = 1;
foo();
}
bar();
- 当 JavaScript 采用静态作用域:
- 先执行
foo
函数,先从foo
函数内部查找含有局部变量tmp
,如果没有,则根据书写的位置,查找上面一层的代码,即tmp
等于 0,所以打印结果为 0.
- 先执行
- 当 JavaScript 采用动态作用域:
- 先执行
foo
函数,依然是从foo
函数内部查找是否有局部变量tmp
,如果没有,则从调用函数的作用域bar
函数内部进行查找,所以打印结果 1.
- 先执行
思考题
那么接下来让我们看一个例子:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope(); // local scope
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()(); // local scope
在这两段代码中,打印结果均为local scope
。
原因也简单,因为JavaScript
采用的是词法作用域,函数的作用域基于函数创建的位置。
而在《JavaScript 权威指南》中的回答则是:
JavaScript 函数执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数f()
定义在这个作用域链里,其中的变量scope
一定是局部变量,不管何时何地执行函数f()
,这个绑定在执行f()
时依然有效。
也就是说,词法作用域与动态作用域,决定的是作用域链的顺序。
虽然两段代码执行的结果一样,但这两段代码究竟有什么不同呢?这是值得我们思考的问题。
三、可执行代码
为了回答上面的问题,我们需要先了解 JavaScript 的可执行代码的类型有哪些?
- 全局代码
- 函数代码
- eval 代码
当执行到一个函数的时候,就会进行准备工作,即所谓的“执行上下文”。每个上下文都有一个关联的对象,而这个上下文中定义的所有变量和函数都存在于这个对象中。变量或函数的上下文决定了它们可以访问哪些数据。
四、上下文执行栈
当我们写的函数越来越多的时候,我们需要用上下文执行栈去管理创建的执行上下文。
当 JS 引擎执行代码时,最先遇到全局代码,所以初始化的时候首先向执行上下文执行栈(ECStack
)压入一个全局执行上下文,这里用globalContext
表示。
:::info
ECStack = [
globalContext
]
:::
此时遇到下面的代码:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
通常来说,每个函数调用都会有自己的上下文,当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文执行栈会弹出该函数上下文,将控制权返还给之前的上下文。所以上述的代码处理过程大致如下:
// 伪代码
// fun1()
ECStack.push(<fun1> functionContext);
// fun1中调用了fun2,还创建了fun2的执行上下文
ECStack.push(<fun2> functionContext);
// fun2调用了fun3
ECStack.push(<fun3> functionContext);
// fun3执行完毕
ECStack.pop();
// fun2执行完毕
ECStack.pop();
// fun1执行完毕
ECStack.pop();
// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext
五、解答思考题
现在我们已经了解了执行上下文是如何处理执行上下文,是时候回答第二节的思考题所提出的疑问了。
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?
答案就是执行上下文栈的变化不一样。
第一段代码变化如下:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
第二段代码变化如下:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
以上即是概括性的回答。
如果为了详尽讲解两个函数执行上的区别,我们需要去探究一下执行上下文包含了哪些内容,敬请期待下一篇文章。