前言
作用域是JS中非常重要的一个知识点,这个特性与我们的日常开发息息相关。很多小伙伴知道这个知识点,却没有理解,没有掌握。希望大家通过这篇文章,对作用域的理解更上一层楼。
作用域(scope)
什么是作用域
作用域规定了JS如何存储变量,以及如何在找到这些变量,即规定了变量的作用范围。
JS中只有全局作用域和函数作用域,ES6之后新增了块级作用域。
执行环境
执行环境是JS中极为重要的一个概念,定义了变量或函数有权访问其他数据。每个执行环境都有一个关联的 **变量对象(varibale object)**存放着该环境中定义的变量和函数。我们的代码无法访问这个对象,但是解析器会使用它。
全局执行环境是最外层的执行环境。在Web浏览器中,全局执行环境被认为是Window对象。
每个函数拥有自己的执行环境,当执行流进入一个函数,函数的环境被推入一个环境栈中。在函数执行之后,栈将环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是由这个机制控制着。
全局作用域和函数作用域
1 全局作用域
看名字就知道,全局作用域是最大的一层作用域,在该作用域内定义的变量,函数可以被全局访问。通常挂在window下的属性,定义在最外层的变量或函数都属于全局作用域。当我们定义大量的属性或函数在全局作用域中,很可能会造成环境污染,命名冲突等各种问题,这也是全局作用域的一个弊端。
2 函数作用域
每个函数都有自己对应的作用域,不同的函数作用域相互独立,互不干扰。用过Jquery的小伙伴应该都知道,所有的代码都放在(function(){....})()中,就是为了与其他的JS脚本隔离,避免冲突。
function a() {
var p = 1;
console.log(p);
};
function b() {
var p = 2;
console.log(p);
};
a(); // 1
b(); // 2
函数a和函数b中定义了同名变量,互不干扰。
块级作用域
ES6中新增了块级作用域,可以通过let和const进行变量声明。那let和const和var又什么区别呢?
1 变量提升
function a() {
console.log(b);
var b;
}
a(); // undefined
明明在定义变量b之前就已经打印了b,却没有报错。因为用var定义的变量存在声明提升,函数a等同于:
function a() {
var b;
console.log(b);
}
**2 let **
let和var的用法一致,区别在于用var和用let定义的变量所属作用域不同
function a() {
if(true) {
var c = 5;
console.log(c);
}
console.log(c);
}
function b() {
if(true) {
let c = 5;
console.log(c);
}
console.log(c);
}
a(); // 5 和 5
b(); // 5 和 "ReferenceError: c is not defined
通过这个栗子可以看到,用var定义的变量c在函数a内部都可以被访问,通过let定义的变量c只能在if代码块中可以被访问。
3 const
const和let用法差不多,用const定义的变量也属于块级作用域。区别在于用const定义的变量需要初始化,const定义的是常量,其值不可修改(const记录的是指针,不是指内容不可修改,是指针不可修改。如果const定义的是一个对象,对象的值是可以修改的)
let a;
const b; // SyntaxError: Missing initializer in const declaration
变量的生命周期
1 声明:在作用域中注册变量
2 初始化:分配内存,创建绑定,变量自动被初始化为undefind
3 赋值:给初始化过的变量赋值
var的生命周期
来个栗子:
function a() {
console.log(b);
var b = 5;
}
a(); // undefind
通过var定义的变量,其声明和初始化阶段不受其在该作用域内的位置影响。运行a函数,打印输出undefind,说明在执行console之前,b已经完成了声明和初始化阶段。
函数的生命周期
a(2); // 2
function a(b) {
console.log(b);
}
根据输出结果反推可以知道,在进入a所属作用域时,a的声明,初始化,赋值便阶段开始执行,即函数不依赖于声明位置。
let的生命周期
let和var对于变量的处理方式不同,区别在于let的声明和初始化阶段被分开了
function a() {
console.log(c);
var c = 5;
}
function b() {
console.log(c);
let c = 5;
}
a(); // undefind
b(); // ReferenceError: c is not defined
在b函数作用域内迅速声明变量c,但是与var不同,并不是立马进入初始化阶段,而是进入一个暂时性死区,变量c在此时属于未定义状态,所以执行console语句会抛出错误,在执行完console语句之后,才会对c初始化和赋值。也就是说let在声明阶段和初始化阶段之间存在一个暂时性死区,处于这个阶段的变量不可被访问。
Note: const和class与let生命周期相同
作用域链
var v1 = 1;
function f1() {
var v2 = 2;
console.log(v1)
function f2() {
var v3 = 3;
console.log(v2);
}
f2();
console.log(v3);
}
f1(); // 1, 2, Uncaught ReferenceError: v3 is not defined
当代码进入执行环境时,会创建执行环境关联的变量对象的作用域链。当前作用域内如果没有对象,会一层一层往外层作用域寻找,知道最外层作用域,即全局作用域。
解析:执行f1, 开始构建作用域链,该作用域链的前端指向f1的变量对象,下一端指向外层作用域。当执行函数f2时,开始创建f2的作用域链,链的前端指向f2的变量对象,下一端指向f1的作用域,尾端指向全局作用域。全局执行环境的变量对象始终是作用域链的最后一个对象。
作用域和执行上下文
很多小伙伴经常吧作用域和执行上下文混淆,这是两个不同的概念。作用域在定义时已经确定,无法改变,而执行上下文是在运行时确定的,并可以改变。
JS的执行分为2个阶段:解释和执行阶段
解释阶段
1 词法分析
2 语法分析
3 作用域规则确定
作用域规则在函数定义的时候已经确定,作用域的访问规则是由代码结构确定的
执行阶段
1 创建执行上下文规则
2 执行函数代码
一个作用域可以对应多个执行上下文环境,取决于调用方式和调用者。比如this指向,在调用时才能确定
总结
JS只有2种作用域,全局作用域和函数作用域,ES6中推出的let和const定义的变量属于块级作用域
let和const和var的生命周期不同:var的变量提升会同时提升声明阶段和初始化阶段,let和const也有声明提升,但是声明阶段和初始化阶段是分开的,再声明阶段和初始化阶段之间有一个暂时性死区,出去此阶段的变量不可被访问,也就是说let和const提升的只是声明阶段
作用域是在函数定义时候确定的,与你的代码结构有关
执行上下文和作用域是不同的概念,执行上下文运行时才能确定,一个作用域可以对应多个执行上下文环境
作用域链访问规则:内层作用域可以访问外层作用域,作用域的尾端永远都是全局执行环境对应的变量对象
如果有问题或者描述不清的朋友们,欢迎留言一起探讨,如果本文有给你们帮助请帮忙点个赞。