JavaScript作用域和执行上下文
一、 作用域
作用域是用来规定变量和函数可访问范围的一套规则,js的作用域有两种,一种是全局作用域,另一种是函数作用域。我们知道在es6之前,js是没有块级作用域的,除了全局作用域,就只有函数能创建作用域。es6新出现的let和const定义的变量都具有块级作用域效果,我们接下来会谈到。下面分别介绍全局作用域和函数作用域。
1.1 全局作用域
全局变量和方法有三种定义方式,如下:
var a = 1; //最外层声明变量
windows.b = 2; //window对象添加属性
function fn(){
c = 3; //在非严格模式下未定义直接赋值的属性
}
第一种在最外层声明变量和方法,第二种给window对象添加属性和方法,第三种在非严格模式下未定义直接赋值的属性和方法(严格模式下会报错)。全局变量和方法可以在代码的任意地方被访问,但是为了多人协作开发中避免冲突或者考虑性能的优化角度,我们应该尽量避免定义全局变量和方法。
1.2 函数作用域
函数作用域中声明的变量和方法只能被下层的作用域访问,不能被其它作用域访问(包括全局作用域),看下面的例子
function fn1(){
var a = 1;
}
function fn2(){
console.log(a) ; //a is not defined
}
console.log(a) ; //a is not defined
fn2();
因为fn1和fn2两个函数不同作用域,所以fn2无法访问fn1的变量,全局作用域同样无法访问fn1的变量。函数作用域里的变量和方法可以在函数内部作用域被访问,如下
function fn(){
var a = 1;
function bar(){
console.log(a); //1
}
bar();
}
fn();
函数bar在fn中,包含在fn作用域中,所以bar可以访问fn的变量。
1.3 let和const
es6之前js是没有块级作用域的,看一下下面的例子
for (var i = 0; i < 10; i++) {
console.log(i); // 0, 1, 2, 3, 4 ... 9
}
console.log(i); //10
i在for循环结束后i并没有被回收,而是继续留在内存中,但是我们把var换成let再试试
for (let i = 0; i < 10; i++) {
console.log(i); // 0, 1, 2, 3, 4 ... 9
}
console.log(i); // i is not defined
这时候i不可以在被访问了,但是我们要注意let不能在同一块下声明相同的变量,下面的代码就会报错
let a = 1;
let a = 2; // 报错:Uncaught SyntaxError: Identifier 'a' has already been declared
而var重复定义不会报错,若之前定义过的变量后面在声明就会被忽略(注意是声明而不是赋值)
var a = 1;
var a = 2;
console.log(a); // 2
const和let相似都是定义块级作用域,但const定义的是常量,意味着它无法被修改,并且也不能重复定义
const c = 1;
c = 2; // 报错:Uncaught TypeError: Assignment to constant variable.
1.4 作用域链
作用域链是由当前执行环境与上层执行环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。我们来看下面的例子
var a = 1;
function fn(){
var b = a + 1;
function bar(){
var c = b + 1;
console.log(c);
}
bar();
}
fn();
bar函数在访问b时,发现自己的作用域里并没有这个变量,所以回到上一级作用域也就是fn中去找,在fn中找到了b变量,但是因为变量b由a赋值,fn发现自己作用域没有a变量,又会到全局作用域去找(事实上bar访问b时,b已经赋值完成,这样说只是方便理解)。这个过程中使用的作用域我们可以看成一条链,长下面的样子:
bar() --> fn() --> global
当bar作用域找不到自己想要的东西时就会到上一级作用域找,上一级作用域找不到再到上上一级作用域找,一直到全局作用域,所有的作用域链的末端都是全局作用域,如果全局作用域也找不到,程序会报undefined错误。这下面还有一个例子,如果你能理解为什么会是这样,作用域这块应该差不多掌握了
var a = 1;
function fn1(){
console.log(a); // 1
}
function fn2(){
var a = 2;
fn1();
}
fn2();
fn1的上一级作用域是全局作用域,不是fn2作用域,因为fn1和fn2两个作用域并不是包含关系,所以fn1无法访问fn2作用域的变量
二、执行上下文
代码运行到不同阶段会有不同的执行上下文,执行上下文可以理解为当前代码的运行环境。上下文环境主要分为全局环境和函数环境,每个函数执行的上下文环境都不同,上下文是在执行阶段才创建的,而上面所说的作用域是在编译阶段就已经确定的。
2.1 上下文的生命周期
当调用一个函数时,一个新的上下文就会被创建,一个上下文的生命周期主要分为两个阶段:创建阶段和执行阶段。创建阶段主要是创建变量对象,确认作用域链,以及确认this的指向;执行阶段主要是完成变量的赋值、函数的引用及执行其它代码。我们看下面的例子
function fn(){
console.log(a); // undefined
console.log(bar); // ƒ bar(){ console.log(a) }
console.log(test); // undefined
var a = 1;
function bar(){
console.log(a)
}
var test = function(){
console.log(a)
}
}
fn();
上面其实就是变量提升的情况,在上下文创建的阶段,我们用var声明的变量或函数会自动提升到作用域的顶部并赋值为undefined,所以a和test会输出undefined。但函数声明不一样,在创建阶段会直接赋值给引用,所以上面打印出了函数的内容,也就是说无论我们在哪定义函数,只要在相同作用域下,我们可以在任意一个地方调用函数。上面的代码的执行顺序可以等价于
function fn(){
function bar(){
console.log(a)
}
var a = undefined;
var test = undefined;
console.log(a); // undefined
console.log(bar); // ƒ bar(){ console.log(a) }
console.log(test); // undefined
a = 1;
test = function(){
console.log(a)
}
}
fn();
另外注意,函数声明的优先级高于变量声明的优先级,而且同名的函数会覆盖函数和变量,但是同名的变量不会覆盖函数
console.log(fn) // function fn(){ console.log(2) }
var fn = 1;
function fn(){ console.log(1) }
function fn(){ console.log(2) }
console.log(fn); // 1
上面代码的实际执行顺序为
function fn(){ console.log(1) }
function fn(){ console.log(2) }
console.log(fn) // function fn(){ console.log(2) }
fn = 1;
console.log(fn); // 1
2.2 函数调用栈
上下文主要分为全局上下文和函数上下文,js调用栈的方式保存不同上下文环境,这个栈就是函数调用栈。函数调用栈规定了js代码的执行顺序。栈底永远是全局上下文,栈顶则是当前执行的上下文。我们看之前的一个例子
var a = 1;
function fn(){
var b = a + 1;
function bar(){
var c = b + 1;
console.log(c);
}
bar();
}
fn();
下面是函数调用栈变化的过程
最开始全局上下文环境被创建并入栈;接着调用fn函数,fn的上下文环境被创建并入栈;fn函数调用了bar函数,bar函数的上下文环境被创建并入栈;bar函数执行完,bar的上下文环境出栈;fn函数执行完,fn函数的上下文环境出栈;最后只剩全局上下文环境。
本文主要简单讲解了JavaScript作用域和执行上下文,若有不对的地方,欢迎指出。
参考书籍:《JavaScript核心技术开发解密》