语句排序
我们在代码中表达语句的顺序没有必要与JS引擎执行它们的顺序相同。这可能看起来像是个奇怪的论断,所以我们简单地探索一下。
但在我们开始之前,我们应当对一些事情十分清楚:从程序的角度看,语言的规则/文法(参见本丛书的 类型与文法)为语句的顺序决定了一个非常可预知、可靠的行为。所以我们将要讨论的是在你的JS程序中 应当永远观察不到的东西。
警告: 如果你曾经 观察到 过我们将要描述的编译器语句重排,那明显是违反了语言规范,而且无疑是那个JS引擎的Bug——它应当被报告并且修复!但是更常见的是你 怀疑 JS引擎里发生了什么疯狂的事,而事实上它只是你自己代码中的一个Bug(可能是一个“竞合状态”)——所以先检查那里,多检查几遍。在JS调试器使用断点并一行一行地步过你的代码,将是帮你在 你的代码 中找出这样的Bug的最强大的工具。
考虑下面的代码:
var a, b;
a = 10;
b = 30;
a = a + 1;
b = b + 1;
console.log( a + b ); // 42
这段代码没有任何异步表达(除了早先讨论的罕见的console
异步I/O),所以最有可能的推测是它会一行一行地、从上到下地处理。
但是,JS引擎 有可能,在编译完这段代码后(是的,JS是被编译的——见本丛书的 作用域与闭包)发现有机会通过(安全地)重新安排这些语句的顺序来使你的代码运行得更快。实质上,只要你观察不到重排,一切都是合理的。
举个例子,引擎可能会发现如果实际上这样执行代码会更快:
var a, b;
a = 10;
a++;
b = 30;
b++;
console.log( a + b ); // 42
或者是这样:
var a, b;
a = 11;
b = 31;
console.log( a + b ); // 42
或者甚至是:
// 因为`a`和`b`都不再被使用,我们可以内联而且根本不需要它们!
console.log( 42 ); // 42
在所有这些情况下,JS引擎在它的编译期间进行着安全的优化,而最终的 可观察到 的结果将是相同的。
但也有一个场景,这些特殊的优化是不安全的,因而也是不被允许的(当然,不是说它一点儿都没优化):
var a, b;
a = 10;
b = 30;
// 我们需要`a`和`b`递增之前的状态!
console.log( a * b ); // 300
a = a + 1;
b = b + 1;
console.log( a + b ); // 42
编译器重排会造成可观测的副作用(因此绝不会被允许)的其他例子,包括任何带有副作用的函数调用(特别是getter函数),或者ES6的Proxy对象(参见本丛书的 ES6与未来)。
考虑如下代码:
function foo() {
console.log( b );
return 1;
}
var a, b, c;
// ES5.1 getter 字面语法
c = {
get bar() {
console.log( a );
return 1;
}
};
a = 10;
b = 30;
a += foo(); // 30
b += c.bar; // 11
console.log( a + b ); // 42
如果不是为了这个代码段中的console.log(..)
语句(只是作为这个例子中观察副作用的方便形式),JS引擎将会更加自由,如果它想(谁知道它想不想!?),它会重排这段代码:
// ...
a = 10 + foo();
b = 30 + c.bar;
// ...
多亏JS语义,我们不会观测到看起来很危险的编译器语句重排,但是理解源代码被编写的方式(从上到下)与它在编译后运行的方式之间的联系是多么微弱,依然是很重要的。
编译器语句重排几乎是并发与互动的微型比喻。作为一个一般概念,这样的意识可以帮你更好地理解异步JS代码流问题。