从编译角度看 JavaScript 的值传递

459 阅读4分钟

当我们在声明赋值一个对象的时候,发生了什么

看一下这样一条表达式:

let obj = { name: "name" };

此时提出一个问题:解释一下这条表达式发生了什么?

回答:声明了 obj 这个变量,并创建了一个对象,并把它赋值给了 obj

这时候换一个问法:obj 这个变量保存了什么?

回答:保存了 { name: "name" } 这个对象的地址。

有没有发现一丝丝的微妙之处?是的,对象的内容和对象的地址,实际上还是有一些区别的。那么区别在哪呢?

再问一个问题:对象保存在哪?

回答:保存在堆空间里。

结合刚才我们的问题,会不会产生一些想法呢?

实际上,对象的地址保存在栈空间,对象的内容保存在堆空间,可以看一下下面这张图:

image.png

可以看到,栈空间里,obj 这个变量保存了一个地址,而这个地址指向了 { name: "name" } 这个对象的内容。也就是说,这里实际上有两个映射:obj → 对象的地址,对象的地址 → 对象的内容。

(实际上变量名映射的是一个地址,这个地址再指向变量值,不过在此我们先不讨论)

到这里,我们对值传递有一个大概的认识了。

什么是值传递

在《JavaScript高级程序设计(第四版)》的第10章《函数》,作者为我们标注了这样一个提醒:

注意 ECMAScript中的所有参数都按值传递的。不可能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用。

那么值传递是什么意思呢?

我们可以看一下这样一个例子:

let outerObj = { name: "outerObj" };
foo(outerObj);
console.log(outerObj.name); // outerObj

function foo(innerObj) {
  // 修改 innerObj 的属性
  innerObj.name = "innerObj";
  console.log(outerObj.name); // innerObj
  console.log(innerObj.name); // innerObj
  console.log(outerObj === innerObj); // true

  // 修改 innerObj 的值
  innerObj = { name: "newObj" };
  console.log(outerObj.name); // innerObj
  console.log(innerObj.name); // newObj
  console.log(outerObj === innerObj); // false

  // 修改 outerObj 的属性
  outerObj.name = "outerObj";
  console.log(outerObj.name); // outerObj
  console.log(innerObj.name); // newObj
  console.log(outerObj === innerObj); // false
}

我们发现,在 foo 里直接修改 innerObjname,会导致 outerObjname 也会改变。而把 innerObj 赋值为一个新值的时候,innerObj 后续的操作都不会导致 outerObj 的改变,此时比较 outerObj === innerObj 的结果为 false

这是因为,一开始我们修改 innerObj 的属性的时候,由于 outerObj 的值(也就是对象的地址)复制给了 innerObj,因此 innerObjouterObj 实际上只是 恰好”保存了指向同一个对象的地址,而不是共用一个地址

(此文的图皆省略了作用域/执行上下文的关系)

image.png

而当我们为 innerObj 赋了一个新值的时候,innerObj 所保存的值变为了一个新的地址,因此后续 outerObjinnerObj 就“一刀两断”了。

image.png

从编译角度看 JavaScript 的值传递

首先,让我们回到第一个例子,并稍微加点内容:

let obj = { name: "name" };
let newObj;
newObj = obj;

AST explorer 将上面的内容转换成 AST后,让我们把目光放在 newObj = obj 这部分上:

image.png

可以看到,在 AST 后,newObj 的身份是 leftobj 的身份是 right

那么这两个身份有什么区别呢?

在《你不知道的Javascript(上卷)》里,有这样一段话:

LHS和RHS的含义是“赋值操作的左侧或右侧”并不一定意味着就是“=赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”。

在我们的例子中,对 newObj 的引用正是 LHS,而 obj 则是 RHS。如果引用的内容或许还是有些不好理解,可以再结合我们前面所讲过的内容,我们也可以理解为,LHS 找的是变量名这样一个容器,而 RHS 所找的则是变量的值,也就是对象的地址。

最后,让我们从编译的角度解释一下 newObj = obj

newObj 进行 LHS,找到 newObj 这个变量名,对 obj 进行 RHS,找到 obj 这个变量的值,最后,将 obj 的值保存在 newObj 这个变量名里。

image.png