语句排序

我们在代码中表达语句的顺序没有必要与JS引擎执行它们的顺序相同。这可能看起来像是个奇怪的论断,所以我们简单地探索一下。

但在我们开始之前,我们应当对一些事情十分清楚:从程序的角度看,语言的规则/文法(参见本丛书的 类型与文法)为语句的顺序决定了一个非常可预知、可靠的行为。所以我们将要讨论的是在你的JS程序中 应当永远观察不到的东西

警告: 如果你曾经 观察到 过我们将要描述的编译器语句重排,那明显是违反了语言规范,而且无疑是那个JS引擎的Bug——它应当被报告并且修复!但是更常见的是你 怀疑 JS引擎里发生了什么疯狂的事,而事实上它只是你自己代码中的一个Bug(可能是一个“竞合状态”)——所以先检查那里,多检查几遍。在JS调试器使用断点并一行一行地步过你的代码,将是帮你在 你的代码 中找出这样的Bug的最强大的工具。

考虑下面的代码:

  1. var a, b;
  2. a = 10;
  3. b = 30;
  4. a = a + 1;
  5. b = b + 1;
  6. console.log( a + b ); // 42

这段代码没有任何异步表达(除了早先讨论的罕见的console异步I/O),所以最有可能的推测是它会一行一行地、从上到下地处理。

但是,JS引擎 有可能,在编译完这段代码后(是的,JS是被编译的——见本丛书的 作用域与闭包)发现有机会通过(安全地)重新安排这些语句的顺序来使你的代码运行得更快。实质上,只要你观察不到重排,一切都是合理的。

举个例子,引擎可能会发现如果实际上这样执行代码会更快:

  1. var a, b;
  2. a = 10;
  3. a++;
  4. b = 30;
  5. b++;
  6. console.log( a + b ); // 42

或者是这样:

  1. var a, b;
  2. a = 11;
  3. b = 31;
  4. console.log( a + b ); // 42

或者甚至是:

  1. // 因为`a`和`b`都不再被使用,我们可以内联而且根本不需要它们!
  2. console.log( 42 ); // 42

在所有这些情况下,JS引擎在它的编译期间进行着安全的优化,而最终的 可观察到 的结果将是相同的。

但也有一个场景,这些特殊的优化是不安全的,因而也是不被允许的(当然,不是说它一点儿都没优化):

  1. var a, b;
  2. a = 10;
  3. b = 30;
  4. // 我们需要`a`和`b`递增之前的状态!
  5. console.log( a * b ); // 300
  6. a = a + 1;
  7. b = b + 1;
  8. console.log( a + b ); // 42

编译器重排会造成可观测的副作用(因此绝不会被允许)的其他例子,包括任何带有副作用的函数调用(特别是getter函数),或者ES6的Proxy对象(参见本丛书的 ES6与未来)。

考虑如下代码:

  1. function foo() {
  2. console.log( b );
  3. return 1;
  4. }
  5. var a, b, c;
  6. // ES5.1 getter 字面语法
  7. c = {
  8. get bar() {
  9. console.log( a );
  10. return 1;
  11. }
  12. };
  13. a = 10;
  14. b = 30;
  15. a += foo(); // 30
  16. b += c.bar; // 11
  17. console.log( a + b ); // 42

如果不是为了这个代码段中的console.log(..)语句(只是作为这个例子中观察副作用的方便形式),JS引擎将会更加自由,如果它想(谁知道它想不想!?),它会重排这段代码:

  1. // ...
  2. a = 10 + foo();
  3. b = 30 + c.bar;
  4. // ...

多亏JS语义,我们不会观测到看起来很危险的编译器语句重排,但是理解源代码被编写的方式(从上到下)与它在编译后运行的方式之间的联系是多么微弱,依然是很重要的。

编译器语句重排几乎是并发与互动的微型比喻。作为一个一般概念,这样的意识可以帮你更好地理解异步JS代码流问题。