Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

揭秘变量提升 #70

Open
husky-dot opened this issue Jul 2, 2019 · 0 comments
Open

揭秘变量提升 #70

husky-dot opened this issue Jul 2, 2019 · 0 comments

Comments

@husky-dot
Copy link
Owner

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

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

变量提升是一个陈旧且令人困惑的术语。甚至在 ES6
之前:变量提升的意思究竟是“提升至当前作用域顶部”还是“从嵌套的代码块中提升到最近的函数或脚本作用域中”?还是两者都有?

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

1. 声明:作用域与激活

可以将声明分为两个方面:

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

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

**“Duplicates”**描述是否可以在同一作用域内声明两次。

**“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);
}

原文:https://2ality.com/2019/05/unpacking-hoisting.html

交流

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

https://github.com/qq449245884/xiaozhi

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant