JS作用域

145 阅读3分钟

1 作用域是什么

1.1 定义

作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是当前执行代码对变量的访问权限。

1.2 类型

作用域有两种:词法作用域(也叫静态作用域)、动态作用域。

词法作用域
特点:函数的作用域在函数定义时决定的

let value = 1;

function foo() {
    console.log(value);
}

function bar() {
    let value = 2;
    foo();
}

bar();

打印的结果是1,因为JS使用的是词法作用域。

动态作用域
特点:动态作用域则是在函数调用的时决定的

// scope.bash
value=1
function foo() {
    echo $value;
}
function bar() {
    local value=2;
    foo;
}
bar

打印的结果是2,因为bash使用的是动态作用域

2 语言实现

从上面的描述可知作用域是一种规范,就像CommonJS规范一样。使用这种规范的语言还需要自己实现。 那JS是如何实现的?

之前学习了JS执行上下文,里边有全局执行上下文、函数执行上下文,对应全局作用域、函数作用域。ES6增加了块级作用域,使用let const声明就行。
defaultName是块级作用域的,外面不可以访问;如果用var,能访问(变量环境) Xnip2022-10-08_21-19-54.jpg

ES6是如何实现块级作用域的?
我们需要从执行上下文来看。

function greet(nick) {
    let str = 'hello ';
    if(nick === '') {
        let defaultName = 'unknown';
        str += defaultName;
    }else{
        str += nick;
    }
    console.log(str);
}
greet('');

执行过程中的函数上下文的词法环境变化:

Xnip2022-10-09_09-35-35.jpg

  • 里边忽略了变量从uninitializedundefined的变化,即初始化过程
  • 【JS执行上下文】文章中,有用到伪代码来表示结构,此时的词法环境是栈,但不清楚整体是什么结构,所以就用图。

我们可以从VS Code debug模式中观察到块级作用域的变量的一些变化过程: Xnip2022-10-08_21-10-44.jpg if执行完毕后就销毁了 Xnip2022-10-08_21-15-22.jpg

3 作用域链

function foo() {
    console.log(message);
}

function wrapper() {
    let message = 'wrapper函数声明的变量';
    foo();
}
let message = '全局变量';

wrapper();

结果:全局变量

为什么foo函数中没有message还能打印出来?
因为JS除了能访问本作用域内的变量,还能访问父级作用域的变量。

为什么foo函数中读取message的结果是全局的而不是wrapper函数中的值?
因为JS采用的是词法作用域,函数的作用域是在声明而不是调用时确定的。
对应到上下文,词法环境中的outer指向的声明函数时的作用域对应的上下文。

Xnip2022-10-09_20-34-36.jpg

4 闭包

JS函数对象的内部状态不仅要包含函数代码,还要包括函数定义时所在作用域的引用。这种函数对象与作用域(及一组变量的绑定)组合起来解析函数变量的机制,在计算机可惜文献中被称为闭包(closure)。

严格来讲,所有JS函数函数都是闭包。但由于多数函数调用与函数定义都在同一个作用域内,所以闭包的存在无关紧要。闭包真正指的关注的时候,是定义函数与调用函数的作用域不同的时候。最常见的情形就是一个函数返回了在它内部定义的嵌套函数

— JavaScript权威指南 第七版


function wrapper() {
    let message = 'wrapper函数声明的变量';
    function foo() {
        console.log(message);
    }
    return foo;
}
let message = '全局变量';

let foo = wrapper();

foo();

结果:wrapper函数声明的变量 (这个也可以用上面的作用域链解释)

参考文章