- 原文地址:Unpacking hoisting
- 原文做者:Dr. Axel Rauschmayer
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:DEARPORK
- 校对者:csming1995, Jalan
引用 ES6 规范做者 Allen Wirfs-Brock 一条最近的推特:html
变量提高是一个陈旧且使人困惑的术语。甚至在 ES6 以前:变量提高的意思到底是“提高至当前做用域顶部”仍是“从嵌套的代码块中提高到最近的函数或脚本做用域中”?仍是二者都有?前端
受 Allen 启发,本文提出了一种不一样的方法来描述变量声明。android
我建议将声明分为两个方面:ios
下面的表格总结了不一样的声明如何处理这两个方面。“Duplicates”表示一个变量名是否容许在同一做用域声明两次。“Global prop.”表示一个在 script 标签(模块的前身)中的声明,在全局做用域中被执行时,是否会向全局对象添加属性。TDZ 意思是暂时死区(咱们稍后解释)。函数声明在严格模式下是块做用域(例如在模块内部),但在非严格模式下是函数做用域。git
如下部分更加详细地描述了其中一些结构的行为。程序员
const
和 let
:暂时死区对于 JavaScript,TC39 须要决定若是在声明以前访问其直接做用域中的常量会发生什么:github
{
console.log(x); // 这里会发生什么?
const x;
}
复制代码
一些可能的方案是:后端
undefined
。方案(1)被否决,由于这种方案在该语言中没有先例。所以这对于 JavaScript 程序员并不直观。ide
方案(2)被否决,由于这样 x
将不是一个常量 —— 在声明前和声明后它将拥有不一样的值。函数
let
与 const
同样使用了方案(3),因此它们工做方式类似而且很容易在它们之间切换。
进入变量做用域与执行声明之间的这段时间被称为该变量的暂时死区(TDZ):
ReferenceError
。undefined
—— 若是没有初始化的话。如下代码阐释了暂时死区:
if (true) { // 进入 `tmp` 的做用域,TDZ 开始
// `tmp` 未被初始化:
assert.throws(() => (tmp = 'abc'), ReferenceError);
assert.throws(() => console.log(tmp), ReferenceError);
let tmp; // TDZ 结束
assert.equal(tmp, undefined);
}
复制代码
下一个例子代表暂时死区是真的暂时的
(与时间有关):
if (true) { // 进入 `myVar` 做用域,TDZ 开始
const func = () => {
console.log(myVar); // 稍后执行
};
// 咱们在 TDZ 中:
// 访问 `myVar` 形成 `ReferenceError`
let myVar = 3; // TDZ 结束
func(); // OK,在 TDZ 外调用
}
复制代码
即便 func()
声明位于 myVar
声明以前且使用了该变量,咱们仍然能够调用 func()
。可是咱们必须等到 myVar
的暂时死区结束以后。
不管一个函数声明在做用域内的什么位置,它都会在进入做用域时执行。这使你能够在 foo()
函数声明前调用它。
assert.equal(foo(), 123); // OK
function foo() { return 123; }
复制代码
提早激活 foo()
意味着上述代码至关于:
function foo() { return 123; }
assert.equal(foo(), 123);
复制代码
若是你用 const
或 let
声明一个函数,那么它就不会被提早激活:在下面的例子中,你只能在 bar()
声明后调用它。
assert.throws(
() => bar(), // 声明前
ReferenceError);
const bar = () => { return 123; };
assert.equal(bar(), 123); // 声明后
复制代码
即便函数 g()
并未提早激活,它仍能够被前面的函数 f()
(在同一做用域中)调用 —— 只要咱们遵照如下规则:f()
必须在声明 g()
以后调用。
const f = () => g();
const g = () => 123;
// 咱们在 g() 声明后调用 f():
assert.equal(f(), 123);
复制代码
模块中的函数一般在模块体执行完后才被调用。因此在模块中,你不多须要担忧函数的顺序。
最后,注意提早激活是怎样自动执行以维持上述规则的:当进入一个做用域时,在任何函数被调用前,全部的函数声明都会被先执行。
若是你依赖提早激活在声明前调用一个函数,那你须要注意它并不能访问未提早激活的数据。
funcDecl();
const MY_STR = 'abc';
function funcDecl() {
assert.throws(
() => MY_STR,
ReferenceError);
}
复制代码
若是你在 MY_STR
声明以后调用 funcDecl()
问题将不复存在。
咱们已经看到提早激活有一个陷阱,你能够在不使用它的状况下得到大部分好处。所以,最好避免提早激活。但我对此说法并不是十分认同,如前所述,我常用函数声明,由于我喜欢它们的语法。
类声明不会提早激活:
assert.throws(
() => new MyClass(),
ReferenceError);
class MyClass {}
assert.equal(new MyClass() instanceof MyClass, true);
复制代码
为何呢?看看下面的类声明:
class MyClass extends Object {}
复制代码
extends
是可选的。它的操做数是个表达式。所以,你能够这样作:
const identity = x => x;
class MyClass extends identity(Object) {}
复制代码
计算这样的表达式必须在它被引用的地方完成。其它行为都会令人困惑。这解释了为何类声明不提早激活。
var
:变量提高(部分提早激活)var
是 const
和 let
(如今更建议使用这两种方式)以前一种更老的声明变量的方式。考虑如下 var
声明。
var x = 123;
复制代码
这个声明包含两个部分:
var x
:用 var
声明的变量的做用域是最里面的包围函数,而不是最里面的包围块,就像大多数其余声明同样。这样的变量在它做用域的开头就已经激活并以 undefined
初始化。x = 123
:赋值老是在适当位置执行。如下代码演示了 var
:
function f() {
// 部分提早激活:
assert.equal(x, undefined);
if (true) {
var x = 123;
// 赋值已经执行
assert.equal(x, 123);
}
// 做用域为函数做用域,非块级做用域。
assert.equal(x, 123);
}
复制代码
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。