我们先看题目,求以下代码运行结果:
JavaScript
var a = {n:1};
var o = a;
a.x = a = {n:2};
console.log(o, a);
连续赋值是C系语言特有的小魔术,多数情况下它无伤大雅,因为每段赋值之间并不互相干涉。
但是一个语句中包含多个副作用,并且让代码结果依赖其执行顺序,这可以说是公认糟糕到不能更糟糕的一种实践了。在古典的C/C++中,标准将它们解释为"未定义行为"。所以在C++社区,研究此类问题往往遭到群体唾弃,毕竟研究未定义行为已经不是在研究语言了。然而很不幸,作为C系语言的小老弟,JavaScript标准大部分写成了伪代码形式,所以确实规定了这类行为的结果。也就是说,这些离谱的行为都切切实实属于JavaScript语言的一部分。
崇拜偏、难、怪,热衷于收集和构造语言难题,并不是一种罕见的现象,甚至可以说是一种人之常情。若这些偏、难、怪语言特性背后包含了思考和权衡,那研究它们自然是一件有意义的事情,若这些特性只是未经设计无意识获得、甚至属于设计失误,那研究它们就不适合作为正式的学习内容了。对这个连续赋值来说,显然属于后者。
如今的JavaScript,语言标准已经近千页,打印出来厚度跟辞典差不多,这样的体量已经远超过大部分人个人学习能力的极限,普通工程师把精力全部放在学习语言特性,甚至是边角特性,是得不偿失的。聚焦于提升用语言特性构筑一个的解决方案的能力,才是工程师提升自己的正确道路。
故此问题的结论是毫无疑问的糟粕。但是若要认真研究这个问题,其研究过程还是有一定意义的。造成这个问题难以理解的本质是运行阶段和语法分析阶段的行为分裂。
大部分同学会下意识地认为,连续赋值a.x = a = {n:2}
可以等效于以下结构:
JavaScript
a = {n:2}
a.x = a;
或者等效于反过来:
JavaScript
a.x = a;
a = {n:2}
但实际上,这两个等效的结论都不对。我们无法把a.x = a = {n:2}
拆开成两个语句来得到完全相同的运行结果。这是非常违反直觉的。
要想真正解释这个问题的运行结果,就需要对编译原理有基本的概念和理解。
对于具备编译原理基础的同学,我们可以用两句话来解释JavaScript中的连续赋值现象:
在语法分析阶段,JavaScript的
=
是一个右结合的运算符。在运行时,JavaScript始终遵循从左到右计算的规则。
语法上的右结合,意味着 a = {n:2}
是一个整体,它的计算结果会被赋值给 a.x
。
而运行时从左到右,意味着 a.x
首先被计算,此时 a
仍然是原本的对象,所以赋值发生在原本的 a
对象的 x
属性上。
我们回到面试题的话题,讨论一下为什么我认为这样的题目为什么不适合作为面试题?
我认为有以下原因
- 与实际工作脱节,绝大多数工程师一生都不会遇到类似代码
- 导向错误,以不良实践代码作为考点,诱导候选人工作中追求偏怪代码
- 没有区分度,只有会或者不会,没有中间结果,也没有讨论空间,甚至没办法引导候选人思考
- 传播广泛,背题者众多,绝大多数能答对的应聘人,都不是靠分析而是靠背答案答对
这些语言的边边角角不失为一种茶余饭后消遣或者有趣的谈资,但若是以此作为面试问题来考核应聘人,则是一种对工作的失责。更严重的,若真把这些代码用于实际开发,那这样的程序员应该遭到立刻辞退。