起因
此文仅是针对于“友好"挑战"掘金全体前端(后端也看过来呗)” 这篇引战文的一个引申思考。
本人认为,讨论编程语言,需要针对应用场景和各自的强弱项。很多算法网站的实现(比如codewars、LeetCode)都提供多种语言版本的校验,本身编程语言设计思路就不同,所以去比较语法、执行效率这些,意义似乎并不是很大。我大PHP是...咳咳,你们什么都没看见,此处个人看法,如果得罪,请多多见谅。
本人曾经写过服务端代码,目前在做前端开发,写过一些java、nodejs,原生app没有写过,但风口的flutter也有粗浅涉猎。
所以,个人认为不论后端或者前端,或者所谓的全栈,大家都是开发者,只是针对的业务不同,技术的层面不同而已。对大多数人而言,我觉得,在知识碰撞中的共鸣一定是多于彼此的分歧的。
以上文章的作者,虽然表面说自己没有引战,也没有歧视。但是从他发表的沸点及文章,处处的流露出对javascript这门语言的不屑。(不好意思,我就是这么觉得的,你就算是再洗地,也不会洗白我对你的观点)
对于某些别有用心的人士,我并不好评价他们是出于什么目的,吸引关注或是哗众取宠,其实都无所谓。但他可能还意识不到,故意分化我们程序员,让大家互相嘲讽甚至言语伤害,这样为人行径,实在太过恶劣。
所以,也有了我这第一篇带着一丝愤怒的文章。
好了,言归正传,让我们一起看看javascript究竟是怎么得罪这位简介十分了得的“大牛”。
⚠以下内容,纯属个人理解,可能错误较多,️各位如有不适,请酌情食用⚠
题目
var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };
console.log(a.x); // undefined
console.log(b.x); // {n:2}
思考
此题来源可能是出自某个公司经典面试题。单从结果来看,就是一道js内存引用的问题。但是实际上,和一些道友和小伙伴一起讨论之后发现,似乎真相并没有辣么简单。
首先可能涉及到的知识点如下:
- javascript的赋值
- javascript的执行顺序
- javascript表达式
- javascript变量标志符
- javascript变量值
好吧,看到这里,其实挺基础的。但是还没有涉及到什么cpu、寄存器、底层编译,吧啦吧啦……
最主要的,这些都是javascript的语言特征。脱离了javascript,那……咱们也不用聊了。
所以吧,今天也不会说什么底层原理。(其实底层那种深渊巨兽,我也搞不懂)
相信有很多小伙伴也已经解释了这道题为什么会有如下答案。
但是唯一不能征服那位“铁头大牛”的地方就在于a.x = a = { n: 2 }这一行的解释。
这位大牛,从内存一直谈到汇编、机器码,甚至谷歌的V8引擎。各位小伙伴也是努力的给他做了解释。
有人试图用伪代码来为他说明:
let c = a;
a = {n:2};
a.x = c;
然而,他似乎并不买账,并坚持认为这“不符合编译器优化原则”。
好吧,看到这里我确实被唬住了。
不过秉着对技术的兴趣,我也开始分析起这里究竟是怎么回事。
第一波分析
代码执行顺序:
javascript的执行顺序,是自上而下,由左至右的。
以一个简单的短路逻辑运算符为例:
let num;
console.log(num = 1 && 2); //2
console.log(num); //2
让我们拆解一下
- 当代码执行到log函数时,传入的参数是一个赋值语句,
num = 1 && 2这是一个赋值语句。 - 在执行过程中,javascript解释器,碰到变量标识符
num后的=时,会尝试获取后面的变量值或表达式。 1 && 2是一个表达式,最终执行的结果,是返回2num被赋值为2。
由此,可以很直观的看出,代码执行的顺序是从左至右的。那么第二问题就来了,num在执行过程中究竟有没有被赋值为1呢?
猜想如下:
num = 1 = 2
似乎看起来,没什么毛病了,但是我个人觉得,如果赋值语句设计成这样,还叫优化的话,那也太……
所以,干脆我秉着站在“javascript编译器作者肯定比我nb”的思想下,大胆猜测一次,这些站在顶尖的工程师们,他们早我一万年就想到了各种优化
所以暂时得出十(mo)分(leng)肯(liang)定(ke)的结论,num 铁定没有在赋值过程中被赋值为1的情况!!!
(不要打我,不要打我o(╥﹏╥)o)
第二波分析
那么,使用以上推论,我也大胆假设了一下。在给a.x赋值的这段代码。
执行顺序应该是如下:
a.x = a = { n: 2 }a.x =a = {n:2}赋值并返回{n:2}此时,变量标志符a断开与{n:1}的连接a.x = {n:2}
至此,a的值(引用对象地址)已经变为{n:2}。所以,这里也和上面num推论的赋值情况是一样的,并不会出现a.x会先赋值为a再赋值为{n:2}的情况。
(而且,如果真的出现a.x = a这一步,那么会出现一个有趣的情况,也正是因为这个情况,让我大胆的相信,那些站在顶尖的工程师,不会连这一步都没考虑到的。究竟是什么,我们后面再说。)
为了佐证一下我这些“狂妄”的“猜想”,我请出了一个神器——chrome的devtool。
step 1

step 2

step 3

step 4

step 5

step 6

step 7

step 8

首先要提一点就是,chrome devtool 在打断点的时候,会有一个比较有趣的现象。就是当你用鼠标“选择”一段表达式的时候,它其实是会去执行的。
所以,我在step 3和step 4的时候,先是选择了a = {n:2},然后又选择了整句a.x = a = {n:2}
那么再回到step 5的时候,此时的a,就已经变成了{n:2}。
而step 6的时候,变量标志符所保存的值仍旧是{n:1}(变量标志符a保存的旧值)
而当整段赋值语句结束后,我又看了一眼b,也就是step 7的结果。这个时候,b的值就变成了{n:1, x:{n:2}}
最后再看一眼a,emmm....已经变成{n:2}了,自然a.x也不存在了。
到这一步,我觉得才似乎有点拨开云雾的样子了。
难道你认为到这就完了吗?
no no no 作妖就要作全套的。
接着往下看,这些过程其实大家都明白。用chrome的devtool只不过是斧正一番。
所以,不甘心的我又稍微改动了一下代码:
var a = { n: 1 };
var b = a;
a = a.x = { n: 2 };
console.log(a.x); // undefined
console.log(b.x); // {n:2}
好了,现在我们再思路跳回到赋值上,执行顺序应该如下:
a =a.x = {n:2}此时a的结果仍旧是{n:1}(这还用说吗?😂)a = {n:2}变量标志符a断开与{n:1}的连接
好了,作到这里,我觉得其实也已经不用再用devtool来执行一次断点调试了。
而,我上面的猜想也得到了以下几点佐证:
-
赋值的时候,碰到
=并不是一定立刻就将其后面的值(基本类型值、引用值、字面量、表达式)取回来塞给前面的变量标志符。 -
当
=后面是一个复杂语句(表达式)的时候,是需要先进行表达式的计算(就像num = 1 && 2)。 -
而当表达式也是复杂语句的时候,则遵循之前的原则,继续判断后者是否是一个复杂语句(表达式),直到得到最终一个不可分词(计算)的值。至此,再向前(从右至左)赋值,到语句结束。
到此,整个延伸思考,到此结束。
不是彩蛋的彩蛋:
上面文中提到,让我觉得写javascript解释器的人都站在顶尖。他们对优化的理解,不是一朝一夕的领悟。那么,怎么体现呢?
来看,假如a.x = a这句成立:

呵,我觉得,玩展开对象,我可以玩到天荒地老……
这里简单的说,就是出现了一个循环引用。相比之下,这样复杂的操作,我觉得是比较耗费性能的。如果在赋值语句中出现这样的情况。如果,真的在a.x = a = {n:2}中出现了这一步,我觉得,说优化的,怕不是个……
写在最后,一些无关痛痒的话:
虽然关注掘金很久,但是苦于没有什么好给大家学习的东西,所以一直都处于默默索取的状态。而,这是我的第一篇文,带着些许怒意,原因很多。
因为对方的诡辩,内心的不屑和轻视,让我也有些头脑不冷静的嘲讽了他几句。但反思一下,自己是否有扎实的基础,过硬的逻辑来辩驳呢?所以,这一篇文,也是给我自己的反思,心中也是希望自己可以能够走的更长远吧。
虽然我很生气,也没法搬出什么二进制、汇编、寄存器、cpu(我也只知道这么多名词了🤷♀️)来证明Javascript其实并不混乱,它也没想象中那么不堪。但是,我仍然会尽我微薄之力,维护这一份我作为程序员,作为开发者,作为一个喜爱Javascript这门语言的人的尊严。
在这里,也希望喜欢Javascript,喜欢前端各位朋友,不要自轻自贱。前端其实很包容,因为有很多人是从后端、服务端、app端、甚至其它位置转来的,也感谢掘金社区,给了我们一个能够有对等讨论机会的空间。
最后,再给那个所谓的“大牛”一份忠告:
“你说的没错,我们确实还有很多知识需要去学习。不过,学习的成果,并不是用来炫耀自己的资本,也不是你嘲讽他人的理由。任何人都有自己不擅长的领域,但是,我们可以不断的探索和学习!”