JS 你必须要理解的作用域

943 阅读7分钟

前言

作用域是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提升的只是声明阶段

作用域是在函数定义时候确定的,与你的代码结构有关

执行上下文和作用域是不同的概念,执行上下文运行时才能确定,一个作用域可以对应多个执行上下文环境

作用域链访问规则:内层作用域可以访问外层作用域,作用域的尾端永远都是全局执行环境对应的变量对象

如果有问题或者描述不清的朋友们,欢迎留言一起探讨,如果本文有给你们帮助请帮忙点个赞。