作用域和作用域链
什么是作用域
作用域指的是源代码中定义变量的区域,即一种规则,规定了如何查找变量,确定当前执行代码对变量的访问权限。
词法作用域
JS采用的是词法作用域,又称为动态作用域。
- 静态作用域: 在函数定义的时候就决定了;
- 动态作用域: 在函数调用的时候决定。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 1
假设JavaScript采用静态作用域,让我们分析下执行过程:
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
假设JavaScript采用动态作用域,让我们分析下执行过程:
执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。
前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。
可执行代码
JS的可执行代码有三种:
- 全局代码
- 函数代码
- eval代码
执行上下文
当JS执行一段可执行的代码时,就会创建对应的执行上下文,它有三个重要的属性:
- 变量对象(Variable object)
- 作用域链
- this
执行上下文栈
为了高校有序地管理多个上下文,JS引擎会创建上下文栈来管理执行上下文。
JS开始解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候,会先向执行上下文栈压入一个全局的执行上下文:
ECStack = [];
ECStack = [
globalContext
];
假设现在JS遇到了这段代码:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
则上下文栈会先压入fun1,由于fun1调用了fun2,所以将fun2压入栈,由于fun2调用了fun3,所以压入fun3
// 伪代码
// 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();
变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
JS在我们直观的眼中,觉得是一行一行执行的,其实它是一段一段执行的,这就跟变量提升有关系了。
变量提升
变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a = 2
上面这行代码会被JS理解为var a
和a=2
。var a
就是一个声明。
console.log(a); // undefined
var a = 2;
它会被理解成这个意思
var a;
console.log(a);
a = 2;
但是注意,如果你不使用var
的话,可能会发生这样的情况
console.log(a); // ReferenceError: a is not defined
a = 2;
另外要特别注意:函数声明会被提升到普通变量之前。
foo(); // 1
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
由于函数声明会被提升到普通变量之前,则实际情况为:
function foo(){
console.log(1);
}
foo();
foo = function(){
console.log(2);
}
为什么普通变量会不执行呢?
三种变量对象
- 函数的所有形参(如果是函数上下文)
- 函数声明:如果变量对象已经存在相同名称的属性,则覆盖这个属性(这个变量)
- 变量声明:如果变量名称跟已经声明的形参或函数相同,则这个变量不会干扰到他们。
所以上面就可以解释,为什么普通变量的声明’不执行’。
作用域链
JS在查找变量时,会先从当前上下文的变量对象(Variable object, VO)中查找,如果没找到,就会从父级执行上下文的变量对象中查找,一直查找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
作用域闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数时在当前词法作用域外执行的。
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
let baz = foo();
baz(); // 2
bar 在自己定义的词法作用域以外的地方执行,但是它仍然可以访问自己的词法域定义的a变量,这就是闭包。
闭包就是能够读取其他函数内部变量的函数。
举一个常见的运用闭包解决问题的栗子:
for(var i=0;i<=5;i++){
setTimeout(function timer(){
console.log(i)
})
}
// 由于延迟函数的回调会在循环结束时才执行,所以当i等于6时就会推出循环,然后执行timer()函数,此时timer()读取的i为6,所以打印的结果是5个6.
// for()中声明i,其实是一个全局i
for(var i = 0;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
})
})(i)
}
p.s.使用let声明变量
关于this
等后期看完书在补充。