窥探 JavaScript 中的变量做用域

讲解 JavaScript 做用域的文章也有不少了,这里我想聊聊一些不同的东西。会的读者能够复习,不会的同窗能够了解。javascript

在 JavaScript 中,咱们能够经过 varlet 来声明变量,也能够经过 const 来定义常量。html

可是在 JavaScript 中,变量的做用域一直都很复杂。java

目录

  • 一、热身:ES3 的做用域
  • 二、ES6 的做用域
  • 三、浏览器的全局做用域
  • 四、模块做用域

一、热身:ES3 的做用域

在 JavaScript ES3 中,咱们只能经过 var 来声明变量,变量在声明时会有变量提高(hoisting),即在后面声明的变量能够被提早访问,而值默认为 undefined浏览器

在 ES3 中,最外层的做用域称为全局做用域。若是你在全局做用域下声明变量,这些变量都会被添加到一个全局对象 globalThis 上,成为它的一个属性。函数

这个 globalThis 在不一样环境下指代不一样的目标,好比在 Node.JS 中,globalThis 就是 global;在浏览器下,globalThis 就是 window,而且也能够经过 self 来访问。ui

除了全局做用域之外,ES3 还有三种局部做用域:spa

  1. 函数做用域 在 ES3 中的函数中,使用 var 来声明变量,全部变量都会提高至函数开头,而且只能在当前函数块内部访问。
    var a = '🍐';
    (function() {
      console.log(a, b); // 🍐, undefined
      var b = '🍐';
      console.log(b); // 🍐
    })();
    console.log(a); // 🍐
    try { console.log(b) } catch(e) { console.error(e.message) } // b is not defined
    复制代码
  2. catch 做用域 在 try { ... } catch(e) { ... } finally { ... } 语句中,变量 e 仅在 catch 块中能够访问。
    try {
      try { console.log(err) } catch(e) { console.error(e.message) } // err is not defined
      throw Error('🍐');
    } catch(err) {
      console.log(err); // Error: 🍐
    } finally {
      try { console.log(err) } catch(e) { console.error(e.message) } // err is not defined
    }
    复制代码
  3. with 做用域 虽然 with 已经不推荐使用了,而且在严格模式(use strict)中已经不可使用了,可是 with 确实会创造一个局部做用域环境。在 with (obj) {} 语句中,JavaScript 会为 obj 上全部的属性都建立一个局部变量,全部这些变量都只能够在 with 块中访问。
    try { console.log(sin) } catch(e) { console.error(e.message) } // sin is not defined
    with(Math) {
      console.log(sin); // function sin() { [native code] }
    }
    try { console.log(sin) } catch(e) { console.error(e.message) } // sin is not defined
    复制代码

二、ES6 的做用域

在 ES6 中引入了两个新的变量/常量定义方法:letconst。由 letconst 声明/定义的变量/常量没有提高,而且仅在当前块中有效,也就是说,他们是块级做用域。code

当你尝试在声明变量/常量以前访问它时,它会提示你“不能在初始化以前访问”,而不是“变量未定义”。这种现象被叫作“临时死区”,而不是“变量提高”。htm

if (true) {
  try { console.log(a); } catch(e) { console.error(e.message) } // Cannot access 'a' before initialization
  try { console.log(b); } catch(e) { console.error(e.message) } // Cannot access 'b' before initialization
  const a = '🍐';
  let b = '🍐';
  console.log(a, b); // 🍐 🍐
}
try { console.log(a); } catch(e) { console.error(e.message) } // a is not defined
try { console.log(b); } catch(e) { console.error(e.message) } // b is not defined
复制代码

这里 constlet 声明/定义的变量/常量仅在 if 块中能够访问。对象

甚至,你能够直接写一个块:

{
  const a = '🍐';
  let b = '🍐';
}
复制代码

那么,若是混合 varconst,会发生什么?

(function() {
  var a = '🍐';
  {
    var b = '🍐';
    const a = '🍐🍐';
    const b = '🍐'; // Uncaught SyntaxError: Identifier 'b' has already been declared
  }
})();
复制代码

咱们能够看到,即使 a 已经声明/定义,在独立的块中也可使用 const 来覆盖,从新定义。在块中 a 的值是 🍐🍐,可是离开块后,a 的值仍是 🍐

可是,若是在这个独立的块中,使用 var 声明了一个变量 b,虽然说 b 会提高至 function 层,可是,在语法解释阶段 const b 就会失败,由于在同一个块中已经声明了 b

三、浏览器的全局做用域

在浏览器中,HTML 容许咱们使用 <script> 包裹 JavaScript 代码,而且在同一个 HTML 文档中能够放置多个 <script> 标签。

考虑这段代码:

<script> var a = '🍐'; let b = '🍐'; const c = '🍐'; </script>
<script> console.log(a); console.log(b); console.log(c); console.log(self.a); console.log(self.b); console.log(self.c); </script>
复制代码

有两个 <script> 标签,第一个里面声明/定义了三个变量/常量,第二个里面花式访问这些变量/常量,会发生什么?答案是:

🍐
🍐
🍐
🍐
undefined
undefined
复制代码

结果前 4 个输出了🍐,然后两个输出了 undefined

在前面说过,若是你在全局做用域声明了变量,它会被自动添加到全局对象上去。

可是这仅仅是针对 ES3 来讲的。

首先,abc 都在全局做用域下,第二个 <script> 也是在全局做用域下的,因此是能够直接访问三个变量/常量的。

可是在 ES6 中,letconst 即使是在全局做用域下声明/定义,也不会将其添加到全局对象上去,因此若是在第二个标签中去经过 self 访问是不存在的。

若是访问不存在的变量,会抛出异常;可是仅仅是访问不存在的属性就不要紧,所以后两个返回 undefined

四、模块做用域

考虑这段代码:

<script type="module"> var a = '🍐'; let b = '🍐'; const c = '🍐'; </script>
<script type="module"> console.group('A'); try { console.log(a) } catch(e) { console.error(e.message) } try { console.log(b) } catch(e) { console.error(e.message) } try { console.log(c) } catch(e) { console.error(e.message) } try { console.log(d) } catch(e) { console.error(e.message) } try { console.log(e) } catch(e) { console.error(e.message) } try { console.log(f) } catch(e) { console.error(e.message) } console.groupEnd(); </script>
<script defer> console.group('B'); try { console.log(a) } catch(e) { console.error(e.message) } try { console.log(b) } catch(e) { console.error(e.message) } try { console.log(c) } catch(e) { console.error(e.message) } try { console.log(d) } catch(e) { console.error(e.message) } try { console.log(e) } catch(e) { console.error(e.message) } try { console.log(f) } catch(e) { console.error(e.message) } console.groupEnd(); </script>
<script> var d = '🍐'; let e = '🍐'; const f = '🍐'; </script>
复制代码

首先,一个 <script> 标签,拥有 type="module" 属性,里面声明定义了几个变量/常量;而后,跟着一个 <script> 标签,一样拥有 type="module" 属性,里面尝试访问并打印 abcdef 六个变量/常量;而后又是一个 <script> 标签,内容与第二个几乎同样,除了 group 的内容,没有 type="module" 属性,却多了 defer 属性;最后仍是一个 <script> 标签,除了没有 type="module" 属性外,内容与第一个彻底同样。

运行结果会怎样?答案是:

┏ B
┣ a is not defined
┣ b is not defined
┣ c is not defined
┣ d is not defined
┣ e is not defined
┗ f is not defined
┏ A
┣ a is not defined
┣ b is not defined
┣ c is not defined
┣ 🍐
┣ 🍐
┗ 🍐
复制代码

猜对了吗?

这里涉及到四个知识点:受到 defer 做用的代码块会被延迟到最后执行;type="module"<script> 默认包含 defer 行为,而且里面定义的变量/常量做用域都是仅影响局部的;对于 inline 内联的 <script> 而言,defer 属性会被忽略。

先看第一个代码块,type="module",所以代码被延迟执行。

而后是第二个代码块,一样是 type="module",代码被延迟执行。

再看第三个代码块,因为这是个内联的脚本(内容直接在标签内给出而不是经过 src 属性指定),所以 defer 属性被忽略,这个脚本仍是以正常顺序执行。执行这个代码块会先建立一个 console group,输出一个 B,而后开始依次访问全部变量/常量。可是,看看上面两个代码块,都被延迟执行了,所以此时全部变量都未定义。

而后是第四个代码块,一个普通的 <script> 标签,声明定义了 def 三个变量/常量。

再以后,被延迟的代码块开始依次执行,先是第一个代码块,声明定义了 abc 三个变量/常量,可是因为它是一个 module,所以全部变量/常量仅对自身 module 可见,对外部均不可访问。

最后,是被延迟的第二个代码块,执行这个代码块会先建立一个 console group,输出一个 A,而后开始依次访问全部变量/常量。其中 abc 处于其余 module 中,所以没法访问,而 def 均已声明/定义,所以能够正常访问。

注意,这里不是说 letconst 发生了提高,而仅仅是受到 defer 效果而使得执行顺序发生了改变。

记得要点赞、分享、评论三连,更多精彩内容请关注ihap 技术黑洞!
相关文章
相关标签/搜索