为什么执行 let x = 1; x = x++; 后,x=1?

起因

最近在学习 JavaScript,偶然发现近期 JavaScript 中似乎多了一部分新的操作符,比如 ??=???. 等。想着学习一下,顺便温习下以往“显而易见”的一些操作符,于是就看到了逗号操作符。【说实话,这个我还真没注意过,居然还有逗号操作符😂】

学习的细节就不多说了,重要的是我看到了一段代码:

let x = 1;
x = (x++, x);
console.log(x);

// expected output: 2

然后嘞,我就给它改了改,改成了这样:👇

let x = 1;
x = x++;
console.log(x);

// my expected output: 2

一脸自信地点下了运行按钮,然而,现实给了我狠狠的一巴掌,结果居然是 1 ?

Why?

不应该先走 x = x,结果 x1;然后再走 x++,结果 x2 吗?怎么会是 1 呢?

x++ 与 ++x 的区别

不信邪的我赶紧去查了下 x++++x 的区别:

  • x++:先计算 x 的值,再将 x 的值自增 1
  • ++xx 的值先自增 1,再计算 x 的值

没错啊,计算 x = x++ 时我也是按照先计算 x 的值,再对其进行自增的呀,怎么就错了呢?

反编译 x = x++

眉头皱紧的我想到了一个办法:将 x = x++ 这句转换成一个更简单、更原生的代码片段,比如这样👇:

x = x;
x++;

如果能简化成这样的话,岂不是就能验证我的思路是没错的?然而,我没有找到这样的工具😭

怎么能轻易放弃呢?💪

我又想到,要是能看一下 x = x++ 这句在内存中到底是怎么运作的,那岂不是更方便?✌️

然而,对于 JavaScript,我还是没有找到工具😭😭

简直了!

最终,经过一番苦逼的查找与实验,我突然一拍脑门,傻逼啊,我可以用 Java 啊!

然后嘞,我就测试了一下这段代码在 Java 中的情况:

public class Test {
  public static void main(String []args) {
    int x = 1;
    x = x++;
    System.out.println(x); // 1
  }
}

果然,这个问题在 Java 中也存在!甚至,我还试了下,在 C 中、C# 中,都是存在这样的问题的!

这就好办了嘛!我大 Java,永远滴神!✌️✌️✌️

javap -c Test > _Test,成功将代码段反编译👇:

Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iload_1
       3: iinc          1, 1
       6: istore_1
       7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: iload_1
      11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      14: return
}

有点难懂😂

字节码探索

先简单学习了需要用到的字节码指令:

程序在运行时有两个区域,一个叫做局部变量表(Local Variable),一个叫做操作数栈(Operand Stack),前者的结构类似数组,用来存储局部变量,后者的数据结构是栈,用来辅助执行指令。

指令指令解释局部变量表1号位置操作数栈顶(左侧代表栈顶)操作数栈2号位置
iconst_1将1放入操作数栈顶1
istore_1栈顶元素出栈并存入局部变量表1号位置1
iload_1将局部变量表1号位置元素放入栈顶11
iinc 1, 1将局部变量表1(前)号位置元素加1(后)21
iadd将栈顶两个元素弹出相加再放入栈顶
getstatic #2调用System.out.println()方法,并将局部变量表1号位置的值作为参数
iload_1
invokevirtual #3
return返回

对 x = x++ 进行探索

为了彻底搞清楚这个问题,同时有一个对比,我写了 6 个代码片段,其核心代码如下👇:

int x= 1; x++; System.out.println(x); // 片段1
int x= 1; ++x; System.out.println(x); // 片段2
int x= 1; int y = x++; System.out.println(y); System.out.println(x); // 片段3
int x= 1; int y = ++x; System.out.println(y); System.out.println(x); // 片段4
int x= 1; x = x++; System.out.println(x); // 片段5
int x= 1; x = ++x; System.out.println(x); // 片段6

x++

先看一下 x++ 怎么运作的:

x++

理解:

  1. 将一个数值存起来
  2. 将这个存起来的数值赋给一个变量
  3. 将这个变量自增
  4. 打印

++x

再看看 ++x 是怎么运作的:

++x

理解:

  1. 将一个数值存起来
  2. 将这个存起来的数值赋给一个变量
  3. 将这个变量自增
  4. 打印

看起来 x++++x 完全一样啊?

难道说,因为没有任何变量来接收 X++ 的结果,导致 JVM 将 X++ 优化成 ++x 了?


从前面可以看到,当没有变量接收 x++/++x 的结果时,JVM 认为 x++++x 是一样的。
那么,如果我们定义一个变量来接收 x++/++x 的结果,那又会发生什么呢?


y = x++

我们先来看 y = x++ 是怎么运作的:

y=x++

理解:

  1. 将一个数值存起来
  2. 将这个存起来的数值赋给一个变量
    — 👆 int x = 1
  3. 将这个变量的数值存起来
  4. 将这个变量自增
  5. 将存起来的数值赋给一个新的变量
    — 👆 int y = x++
  6. 分别打印

有些疑惑,x++ 是先返回 x 的值(x 的值存起来),再 ++(自增),这没有问题;但是,为什么不把存起来的值先赋给 y 呢?

因为按照我的理解,y = x++ 应该等价于 👉 y = x; x++

而且算下来的值也一样啊,可是为什么字节码顺序对不上呢?


没太理解 y = x++ 的背后运作原理,那先来看一下 y = ++x 吧!应该可以对照着看出一些东西来。


y = ++x

再看看 y = ++x 是怎么运作的:

y=++x

理解:

  1. 将一个数值存起来
  2. 将这个存起来的数值赋给一个变量
    — 👆int x = 1
  3. 将这个变量自增
  4. 将变量的数值存起来
  5. 将存起来的数值赋给一个新的变量
    — 👆int y = ++x
  6. 分别打印

这下确实没问题,确实是按照我理解的顺序走的:

++(自增),再返回 x 的值(x 的值存起来),再把存起来的值赋给 y

可是,既然我理解的思路是对的,那为什么 y = x++ 的字节码顺序对不上呢?🤔


还是有困惑,那就继续看一下 x = x++ 的运作原理,再继续对比对比吧!


x = x++

看看 x = x++ 是怎么运作的:

x=x++

理解:

  1. 将一个数值存起来
  2. 将这个存起来的数值赋给一个变量
    — 👆int x = 1
  3. 将这个变量的数值存起来
  4. 将这个变量自增
    — 👆 x++
  5. 将存起来的数值赋给原变量
    — 👆x = x
  6. 打印

从上面的分析来看,似乎是先运行了 x++,再运行 x = x,同时为了保证在运行 x++ 时先返回 x 的值,JVM 将其先暂存了起来,自增完后再赋值,结果将自增的值覆盖了。

而这也是为什么最终 x 的值为 1 的原因!

但是,为什么要多此一举呢?为什么不先赋值,再自增?

咦?等等,我忽然想到了一个问题!先自增,再赋值!


再来用 x = ++x 验证一下!


x = ++x

看看 x = ++x 是怎么运作的:

x=++x

理解:

  1. 将一个数值存起来
  2. 将这个存起来的数值赋给一个变量
    — 👆int x = 1
  3. 将这个变量自增
    — 👆++x
  4. 将这个变量的数值存起来
    — 👆 x
  5. 将存起来的数值赋给原变量
    — 👆x = x
  6. 打印

没问题,先自增,再赋值!按照这样的逻辑是能这样走通的,那么,我知道了!

++ 优先级 > 赋值优先级

优先级

按照这样的优先级,不管是单纯的 x++/++x,还是带赋值的 y = x++/y = ++x/x = x++/x = ++x,都必须先运算 x++/++x

那么,当运算 x = x++ 时,按照运算符优先级:

  1. 先走 x++,但是为了保证先返回 x 的值,因此将其暂存起来,然后给 x 自增;
  2. 第二步,进行赋值操作,因为已经将需要返回的值暂存起来了,因此将暂存起来的值赋给变量,因此最终结果为 1

终于搞明白了!

总结

按照运算符的优先级不同,需要将一个表达式合理地进行分割,不能跨多个“块”运算,需要严格遵守优先级顺序。

分析

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十甫寸木南

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值