揭秘变量提高

原文:2ality.com/2019/05/unp…html

译者:前端小智前端

为了保证可读性,本文采用意译而非直译。git

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!github

引用 ES6 规范做者 Allen Wirfs-Brock一条最近的推特:ide

变量提高是一个陈旧且使人困惑的术语。甚至在 ES6 以前:变量提高的意思到底是“提高至当前做用域顶部”仍是“从嵌套的代码块中提高到最近的函数或脚本做用域中”?仍是二者都有?函数

受 Allen 启发,本文提出了一种不一样的方法来描述变量声明。学习

1. 声明:做用域与激活

能够将声明分为两个方面:spa

  • 做用域:在哪里能够看到声明的变量? 这是一个静态特征。
  • 激活:我何时能够访问变量? 这是一个动态特征:有些变量只要咱们进入其做用域,就能够访问。 有的,咱们必须等到执行到它们的声明。

下表总结了不一样声明的方式如何处理上述两个方面。code

**“Duplicates”**描述是否能够在同一做用域内声明两次。cdn

**“Global prop.”**表示一个在 script 中的声明,当全局做用域中被执行时,是否会向全局对象添加属性。

TDZ 表示暂时性死区(稍后解释)。 函数声明在严格模式下是块做用域的(例如在模块内部),但在非严格模式下是函数做用域。

2. const 和 let :暂时性死区

对于JavaScript,TC39 须要决定若是在声明以前访问其直接做用域中的常量会发生什么:

{
  console.log(x); // 这里会发生什么?
  const x;
}
复制代码

主要有两种种状况:

  1. 打印 undefined

  2. 报错

第一种不会出现,由于 x 是一个常量,若是打印 undefined,在声明前和声明后它将拥有不一样的值,x 就不是常量了。

let 和 const 都会出现第二种状况,就是会报错。进入变量做用域与执行声明之间的这段时间被称为该变量的 临时死区(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声明以前使用 myVar 变量,但咱们也能够调用func(),前提是必须等到myVar的临时死区结束。

函数声明与提早激活

函数声明老是在进入它的做用域时执行,无论它位于做用域的什么位置。这使得可以在函数foo()声明以前调用它:

assert.equal(foo(), 123); // ok,相等
function foo() { return 123; }
复制代码

提早激活 foo()意味着楼上的代码等价于

function foo() { return 123; }
assert.equal(foo(), 123);
复制代码

若是用 constlet 声明一个函数,它就不会被提早激活:在下面的例子中,你只能在 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() {
   console.log(MY_STR)
}
复制代码

上述会报错:

若是你在 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是在constlet以前声明变量的一种较老的方法。考虑下面的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);
}
复制代码

交流

干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。

github.com/qq449245884…

我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,便可看到福利,你懂的。

相关文章
相关标签/搜索