讲解 JavaScript 做用域的文章也有不少了,这里我想聊聊一些不同的东西。会的读者能够复习,不会的同窗能够了解。javascript
在 JavaScript 中,咱们能够经过
var
、let
来声明变量,也能够经过const
来定义常量。html可是在 JavaScript 中,变量的做用域一直都很复杂。java
在 JavaScript ES3 中,咱们只能经过 var
来声明变量,变量在声明时会有变量提高(hoisting),即在后面声明的变量能够被提早访问,而值默认为 undefined
。浏览器
在 ES3 中,最外层的做用域称为全局做用域。若是你在全局做用域下声明变量,这些变量都会被添加到一个全局对象 globalThis
上,成为它的一个属性。函数
这个 globalThis
在不一样环境下指代不一样的目标,好比在 Node.JS 中,globalThis
就是 global
;在浏览器下,globalThis
就是 window
,而且也能够经过 self
来访问。ui
除了全局做用域之外,ES3 还有三种局部做用域:spa
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
复制代码
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
}
复制代码
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 中引入了两个新的变量/常量定义方法:let
和 const
。由 let
和 const
声明/定义的变量/常量没有提高,而且仅在当前块中有效,也就是说,他们是块级做用域。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
复制代码
这里 const
和 let
声明/定义的变量/常量仅在 if
块中能够访问。对象
甚至,你能够直接写一个块:
{
const a = '🍐';
let b = '🍐';
}
复制代码
那么,若是混合 var
和 const
,会发生什么?
(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 来讲的。
首先,a
、b
、c
都在全局做用域下,第二个 <script>
也是在全局做用域下的,因此是能够直接访问三个变量/常量的。
可是在 ES6 中,let
和 const
即使是在全局做用域下声明/定义,也不会将其添加到全局对象上去,因此若是在第二个标签中去经过 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"
属性,里面尝试访问并打印 a
、b
、c
、d
、e
、f
六个变量/常量;而后又是一个 <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>
标签,声明定义了 d
、e
、f
三个变量/常量。
再以后,被延迟的代码块开始依次执行,先是第一个代码块,声明定义了 a
、b
、c
三个变量/常量,可是因为它是一个 module,所以全部变量/常量仅对自身 module 可见,对外部均不可访问。
最后,是被延迟的第二个代码块,执行这个代码块会先建立一个 console group,输出一个 A
,而后开始依次访问全部变量/常量。其中 a
、b
、c
处于其余 module 中,所以没法访问,而 d
、e
、f
均已声明/定义,所以能够正常访问。
注意,这里不是说 let
和 const
发生了提高,而仅仅是受到 defer
效果而使得执行顺序发生了改变。