当我们在声明赋值一个对象的时候,发生了什么
看一下这样一条表达式:
let obj = { name: "name" };
此时提出一个问题:解释一下这条表达式发生了什么?
回答:声明了 obj
这个变量,并创建了一个对象,并把它赋值给了 obj
。
这时候换一个问法:obj
这个变量保存了什么?
回答:保存了 { name: "name" }
这个对象的地址。
有没有发现一丝丝的微妙之处?是的,对象的内容和对象的地址,实际上还是有一些区别的。那么区别在哪呢?
再问一个问题:对象保存在哪?
回答:保存在堆空间里。
结合刚才我们的问题,会不会产生一些想法呢?
实际上,对象的地址保存在栈空间,对象的内容保存在堆空间,可以看一下下面这张图:
可以看到,栈空间里,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
里直接修改 innerObj
的 name
,会导致 outerObj
的 name
也会改变。而把 innerObj
赋值为一个新值的时候,innerObj
后续的操作都不会导致 outerObj
的改变,此时比较 outerObj === innerObj
的结果为 false
。
这是因为,一开始我们修改 innerObj
的属性的时候,由于 outerObj
的值(也就是对象的地址)复制给了 innerObj
,因此 innerObj
和 outerObj
实际上只是 “恰好”保存了指向同一个对象的地址,而不是共用一个地址。
(此文的图皆省略了作用域/执行上下文的关系)
而当我们为 innerObj
赋了一个新值的时候,innerObj
所保存的值变为了一个新的地址,因此后续 outerObj
和 innerObj
就“一刀两断”了。
从编译角度看 JavaScript 的值传递
首先,让我们回到第一个例子,并稍微加点内容:
let obj = { name: "name" };
let newObj;
newObj = obj;
在 AST explorer 将上面的内容转换成 AST后,让我们把目光放在 newObj = obj
这部分上:
可以看到,在 AST 后,newObj
的身份是 left
,obj
的身份是 right
那么这两个身份有什么区别呢?
在《你不知道的Javascript(上卷)》里,有这样一段话:
LHS和RHS的含义是“赋值操作的左侧或右侧”并不一定意味着就是“=赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”。
在我们的例子中,对 newObj
的引用正是 LHS,而 obj
则是 RHS。如果引用的内容或许还是有些不好理解,可以再结合我们前面所讲过的内容,我们也可以理解为,LHS 找的是变量名这样一个容器,而 RHS 所找的则是变量的值,也就是对象的地址。
最后,让我们从编译的角度解释一下 newObj = obj
:
对 newObj
进行 LHS,找到 newObj
这个变量名,对 obj
进行 RHS,找到 obj
这个变量的值,最后,将 obj
的值保存在 newObj
这个变量名里。