JS 的 9 种作用域,你能说出几种?

  • Post author:
  • Post category:其他


作用域想必大家都知道,就是变量生效的范围,比如函数就会生成一个作用域,声明的变量只在函数内生效。

而这样的作用域一共有 9 种,其中几种绝大多数前端都说不出来。

下面我们就一起过一遍这 9 种作用域吧,看看你知道几种:


(为了保证准确性,所有的作用域类型都是通过调试所得)

Global 作用域

通过 var 声明一个变量,打个断点,可以看到 Scope 里有 Global 类型的作用域,也就是全局作用域,里面保存了变量 a:

图片

在浏览器环境下,可以通过 a 访问全局变量,也可以通过 window.a 访问。

图片

Local 作用域

声明个函数,在函数内声明一个变量,调用这个函数的时候,可以看到 Scope 里有 Local 类型的作用域,也就是本地作用域,里面保存了变量 b:

图片

这两种作用域都很常见,没啥好说的。

Block 作用域

es6 加入了块语句,它也同样会生成作用域:

图片

如图,会把里面声明的变量 a 放到 Block 作用域内,也就是块级作用域。

if、while、for 等语句都会生成 Block 作用域:

图片

图片

前几种作用域很常规,但下面这种作用域绝大部分前端就不知道了:

Script 作用域

这段代码大家觉得会生成什么作用域:

图片

很多同学都会说,不是全局作用域么?

那这个现象你能解释么:

图片

a、b、c 如果都是全局变量,那在浏览器里就可以通过 window.xx 来访问,但结果 window.a 和 window.b 都是 undefined,而直接访问 a、b 能拿到值。

看下现在的作用域就知道了:

图片

你会发现 let、const 声明的全局变量被放到了 script 作用域,而 var 声明的变量被放到了 global 作用域。


这就是浏览器环境下用 let const 声明全局变量时的特殊作用域,script 作用域。可以直接访问这个全局变量,但是却不能通过 window.xx 访问。

所以你再看到这样的代码,就不奇怪了:

window.xxx = xxx;

这个 xxx 肯定是通过 let、const 声明的全局变量,需要手动挂到 window 上。

那上面这个 script 作用域在 node 环境里有么?

我们用 node 调试下:

模块作用域

同样的代码,在 node 环境下就没有了 Script 作用域,但是多了一个 Local 作用域:

图片

这个 Local 作用域还有 module、exports、require 等变量,这个叫做模块作用域。

这个作用域有些特殊,其实它也是函数作用域。为什么呢?后面会有解释。

说到特殊的作用域,其实还有一些:

Catch Block 作用域

Catch 语句也会生成一个特殊的作用域,Catch Block 作用域,特点是能访问错误对象:

图片

在 node 里也是一样,只不过还有一层模块作用域:

图片

有同学会问,那 finally 语句呢?

这个就没啥特殊的了,就是 Block 作用域:

图片

图片

类似的还有 With Block:

With Block 作用域

大家猜下这个 with 语句里的作用域是是啥:

图片

想必你猜到了,with 语句里的作用域就是这个对象:

图片

换成普通的对象更明显一些:

图片

Closure 作用域

闭包是 JS 的常见概念,它是一个函数返回另一个函数的形式,返回的函数引用了外层函数的变量,就会以闭包的形式保存下来。

比如这样:

function fun() {
    const a = 1;
    const b = 2;
    return function () {
        const c = 2;

        console.log(a, c);
        debugger;
    };
}

const f = fun();
f();

那闭包的变量怎么保存的呢?

通过 node 可以看到:

图片

通过 Closure 作用域保存了变量 a 的值,这个 Closure 作用域就是闭包的核心。

那为啥只保存了 a 没保存 b、c 呢?

c 是返回的函数的作用域里的,不是外部作用域,而 b 则是没用到,所以 Closure 作用域里只保存了 a。

然后执行的时候就会恢复这个 Closure 作用域:

图片

这样函数需要的外部变量都在 Closure 作用域里,啥也没丢,可以正常执行。

是不是很巧妙!

这就是闭包的核心。

当然,Closure 作用域也可以多层,比如这样:

function fun() {
    const a = 1;
    const b = 2;
    return function () {
        const c = 2;
        const d = 4;

        return function () {
            const e = 5;

            console.log(a, c, e);
        };
    };
}

const f = fun()();
f();

用到的外部变量分别在两个作用域里,那就会生成两个 Closure 作用域:

图片

只留下用到的作用域的变量 a、c。

执行的时候就会恢复这两层闭包作用域:

图片

这样函数需要的外部环境一点都不少。

理解了 Closure 作用域,就真正理解了闭包。

闭包里还有一种特殊情况,就是 eval:

上面的代码如果我改动一下,把打印语句变成 eval,会发生什么呢?

function fun() {
    const a = 1;
    const b = 2;
    return function () {
        const c = 2;
        const d = 4;

        return function () {
            const e = 5;

            eval("console.log(a, c, e);");
        };
    };
}

const f = fun()();
f();

有的同学会说,这不是一样么,都会形成闭包。

没错,都会形成闭包,但是保存的变量不一样了:

图片

你会发现它把所有外部的作用域的变量都保存到了 Closure 作用域,包括模块作用域的变量。

为什么呢?

因为它根本不会去分析字符串呀,也没法分析,万一你这段 JS 是动态从服务端获取再 eval 的呢?

没法分析!

没法分析怎么保证代码执行不出错呢?

全部保存不就行了?

所以当返回的函数有 eval 的时候,JS 引擎就会形成特别大的 Closure,会把所有的变量都放到里面。

这样再执行 eval 的时候就不会出错了:

图片

所有的变量都给你了,怎么可能出错呢?

但是这样明显性能不好,会占用更多的内存,所以闭包里尽量不要用 eval。

前面说模块作用域是特殊的函数作用域,为什么这么说呢?

这就与 node 模块的执行机制有关系了。

比如这样一段代码:

function func() {
    require;
    debugger;
}
func();

执行后发现形成了闭包:

图片

而如果不访问模块作用域的变量,就没有这一层了:

图片

我这明明没有闭包的代码呀!

这就与 node 模块的执行机制有关系了:


node 会把模块变为一个函数,它有 exports、require、module、__dirname、__filename 这五个参数,然后传入这五个参数来执行:

图片

所以

模块作用域就是个函数作用域而已!

模块里的函数引用模块作用域的变量,再执行,自然就形成了闭包。

Eval 作用域

最后一种特殊的作用域就是 eval 作用域了。

比如这样一段代码:

eval(`
    const a = 1;
    const b = 2;
    const c = 3;

    console.log(a,b,c);
    debugger;
`);

执行之后是这样的:

图片

可以看到有单独的 Eval 作用域,eval 的代码里声明的变量都在这个作用域里:

图片

总结

JS 总共有 9 种作用域,我们通过调试的方式来分析了下:


  • Global 作用域

    :全局作用域,在浏览器环境下就是 window,在 node 环境下是 global


  • Local 作用域

    :本地作用域,或者叫函数作用域


  • Block 作用域

    :块级作用域


  • Script 作用域

    :let、const 声明的全局变量会保存在 Script 作用域,这些变量可以直接访问,但却不能通过 window.xx 访问


  • 模块作用域

    :其实严格来说这也是函数作用域,因为 node 执行它的时候会包一层函数,算是比较特殊的函数作用域,有 module、exports、require 等变量


  • Catch Block 作用域

    :catch 语句的作用域可以访问错误对象


  • With Block 作用域

    :with 语句的作用域就是传入的对象的值


  • Closure 作用域

    :函数返回函数的时候,会把用到的外部变量保存在 Closure 作用域里,这样再执行的时候该有的变量都有,这就是闭包。eval 的闭包比较特殊,会把所有变量都保存到 Closure 作用域


  • Eval 作用域

    :eval 代码声明的变量会保存在 Eval 作用域

上面这些都是调试得出的,是 JS 引擎执行代码时的真实作用域。

JavaScript 的 9 种作用域,你能说出几种?