中阶魔法 - 传值、传参考

前情提要

艾草:「来,接球!」

(我看着眼前一颗球飞了过来,正准备伸出手接时,突然变成了两颗。)

「咦咦,为何?」

艾草:「我用魔法复制了一颗球呀!」

「这麽好用,那我能不能拿来复制房子呀?」

艾草:「没那麽好的事,如果复制大型物件像房子,魔法只能复制出它的样子,但你去改房子的摆设,也会连动影响到原本的房子,因为它们实际上还是存在同一个空间。」

「它的界线在哪呀?求教学。」

https://ithelp.ithome.com.tw/upload/images/20211005/20139066Bc9ts0BFhf.png

(不知道有没有机会复制钱钱 ($ε $ ) )


传值(by value)、传参考(by reference)

在开始介绍传值、传参考前想先补充一个小知识。

其实我们每宣告一个变数,都会有相对应的记忆体空间存放变数,也会有另一个记忆体空间存放变数的值,例如你宣告一个变数 a 值为数字 1 的话,可以想像记忆体会如下表格:

https://ithelp.ithome.com.tw/upload/images/20211005/201390667sLIPaGKRq.png

了解了记忆体空间的小概念後,让我们接着学习罗!

原始型别

在值为原始型别的情况下如果设变数定 ab 的值相同时,比较 ab 会得到以下结果:

let a = 1;
let b = 1;
console.log(a === b)//true

在原始型别的情况下,是去比较这两个是否相等。

而当将 a 的值赋予给 b 时,其实它会直接拷贝 a 的值数字 1 过来,因为是复制的关系,所以今天将 b 重新赋予一个数字型别的 2 ,也不会影响到 a

let a = 1;
let b = a;
b = 2;
console.log(a)//1

在原始型别的情况下,因为是传值的关系,所以就算我改变了变数 b 的值,也并不会连带影响到变数 a ,他们的变数记忆体指向就是两条平行线,如图:

https://ithelp.ithome.com.tw/upload/images/20211005/20139066BSFpnns7c2.jpg


物件型别(包含函式)

而物件型别(包含函式)传参考 (by reference)有什麽特性呢?

在针对物件去做比较时会发现以下情况:

let obj = {
	key:'value'
}
let newObj = {
	key:'value'
}
console.log(obj === newObj)//false

会发现里面的属性与值明明是一样的,但回传比对结果却是 false ,因为物件是传参考,所以比对的是记忆体位置,还不太了解没关系,让我们继续看下去。

let obj = {
	key:'value',
};
let newObj = obj;
console.log(obj === newObj) //true
newObj.name = "王小明";
console.log(obj)//{key: "value", name: "王小明"}

像这样明明我们改的是 newObj 却连 obj 也更动了,原因就是因为物件是透过传参考的形式,传参考代表 newObj 其实是指向 obj 的记忆体空间,如图:

https://ithelp.ithome.com.tw/upload/images/20211005/20139066JxDDqCLS0R.png

我们简单替他们的记忆体空间编码为 0x001,可以清楚地看到两者都是指向同一个记忆体空间,所以当你改动 newObj 时也会同时改动到 obj

补充:在变数宣告章节有提到关於 const 宣告原始型别难以被重新赋予值,但物件型别因为是传参考的特性,所以只要不改变记忆体空间就不会报错,如下:

//不会报错
const obj = {
	key:'value'
}
obj.name = "王小明";

//会报错
const obj = {
	key:'value'
}
obj = {};//Uncaught TypeError: Assignment to constant variable.

只要不直接赋予该变数一个新的物件 {} 大括号,只是单纯去修改物件值,不影响到记忆体空间的情况下,可以透过 const 去宣告物件型别的值。

小结:原始型别的值是透过拷贝的方式,所以并不会互相影响,物件型别会共用同一个记忆体空间,所以会互相连动。

而该如何避免这个情况呢?

我们可以透过以下几种方法:

浅层拷贝

浅层拷贝指可以将第一层的位置的记忆体位置指向其他记忆体空间,但如果物件内又包了一个物件的值,该物件的值还是会与原物件共用一个记忆体空间:

Object.assign() 语法,可以复制一个或多个物件所有的属性到新物件上,如下:

let obj = {
	key:'value'
}
let newObj = Object.assign({},obj);
console.log(obj === newObj)//false

展开运算子 ... ,透过 ES6 的新语法展开运算子也可以达成浅层拷贝:

let obj = {
	key:'value'
}
let newObj = {
...obj
};
console.log(obj === newObj)//false

深层拷贝

直接使用将物件转字串又转回物件的方式。

let obj = {
	key:'value'
}
let newObj = JSON.parse(JSON.stringify(obj));
console.log(obj === newObj)//false

总结

  • 原始型别为传值
  • 物件型别(包含函式)为传参考
  • 浅层复制 Object.assign() 、展开运算子
  • 深层复制 JSON.parse(JSON.stringify(obj))

小练习

请问以下叙述何者错误?
A 原始型别的情况下是传值,物件型别的情况是传参考
B 用 const 宣告的情况下,无论原始型别或物件型别,都不能修改任何值
C 如果想浅层拷贝可以透过展开运算子、Object.assign()语法
D 传参考指的是记忆体会指向同一个记忆体位置

解答:用 const 宣告的物件型别,因为物件传参考的特性,在不透过 = 重新赋予记忆体空间的情况下,进行属性值的新增、修改并不会报错。


参考文献

0 陷阱!0 误解!8 天重新认识 JavaScript!(iT邦帮忙铁人赛系列书)
Vue 3 实战影音课程(六角学院)


<<:  Day20-<router-link> 建立路由连结

>>:  认识JavaScrip

谢幕

今天是铁人赛最後一天了,想分享的主题也差不多了,就来个总集篇做个收场吧! 从最基本的 认识 Blaz...

Day 30 : 妈!我完赛了!

没错各位!今天是最後一天!很突然的就完赛了,我跟同学们说的时候,大家都相当错愕,前阵子才听我说要参加...

【後转前要多久】# Day23 JS - JavaScript 变数、运算

变数 JS目前有三种宣告变数的方法。 在ES5以前都用var,ES6之後推出let与const。 新...

Day11 CSS基本说明

讲完了那麽多的Html,接下来要进入到CSS环节的部分了,身为网页装潢师的它,我们先从怎麽使用它开始...

JavaScript学习日记 : Day7 - 函数(二)

ES6出现的Arrow function,看起来简短许多,但却充满许多陷阱(限制),所以充份了解箭头...