JavaScript Day 19. by value ( 传值 ) 与 by reference ( 传址 )

上一篇说到 JavaScript 原始型别与物件型别,我想今天试着来讨论「传值」与「传址」;在其他程序语言可能可以决定要「传值」还是「传址」,但在 JavaScript 中我们没办法选择,原始型别与物件型别会有他们个别的传递行为。

在 JavaScript 中,值的传递分为两种:

  • 传值:Call by value 或 Pass by value / 原始型别传值呼叫
  • 传址:Call by reference 或 Pass by reference / 物件型别传参考呼叫

原始型别传值 ( Pass by value )

前一篇有提到,JavaScript 中除了物件以外的所有型别,都是原始型别,也有提到分别有哪几种,这里就不再赘述了。原始型别也称为「纯值」,并且会以「传值 ( Pass by value )」的方式传递。

直接来看看范例:

let cat = '喵'; // 定义 cat 的内容为'喵'字串
let newCat;
newCat = cat; // 将 newCat 指定等於 cat 字串
console.log(newCat); // '喵'

newCat = '呼噜';    // 再将 newCat 的内容更动为'呼噜'
console.log(cat); // '喵'
console.log(newCat); // 打印 newCat 出现变更後的内容'呼噜'

这个范例看起来与我自己认知的部分没有什麽出入,假如以这个例子来说明什麽是「 Pass by value」,我这边可以理解的是,不能受到改变的就是「原始型别 / Pass by value ( 传值 )」,不过为了怕自己遗忘,这边也还是稍微解释一下。

前一篇我们也有提到过,原始型别也有「纯值」的意思,他是不可变动的,以上面的范例来说,我们可以更改 cat = '喵' 这边的 cat 变数,但没有办法更改 '喵' 这个值。在电脑的空间里面有一个大型的空间称做「记忆体」,每一个空间都有他的位置,为了能够让我们取用,因此会有变数的存在,於是我们可以使用变数指向这些记忆体位置,宣告变数并赋予值,这样的行为就是向电脑要一个记忆体空间来存值。

let cat = '喵' 来说,变数 cat 指向电脑中某记忆体的位置,并在这个记忆体储存 '喵' 这个值,我们另外又宣告了一个 newCat,虽然 catnewCat 的值是一样的,但其实 newCat 另外在不同的记忆体位置,只是复制了 cat 的值,他们两个仍然是各自存在於独立的记忆体位置,因此後来我们又给 newCat 一个新的值的时候 cat 仍然不会被变动。

另外,这里要注意的是,newCat 的值之所以变成 '呼噜',是因为 newCat 改变了记忆体的指向,不是改变了 '呼噜' 这个值,值是不能被变动的。像上面这样的过程,我们可以称做「传值」。

再来一个简易的范例,可以当作稳固概念用:

let a = 50;
let b = 50;

a === b ;  // true

50 为原始型别,ab 互相比较的是彼此的值,因此这里回传 true


物件型别传参考传址 ( Pass by reference )

物件型别是以「传址 ( Pass by reference )」的方式传递。一个物件型别的变数,被存在某个有地址的「位置」,而这个「记忆体位置」则存在於这个变数中,因此以「记忆体位置为参考」,并在变数间传递存取的行为,就称为「传参考呼叫」。

范例:

let arr = [1,2,3];
let newArr = arr;
console.log(arr2); // [1,2,3]

arr[0] = 2; 
console.log(arr); // [2,2,3]
console.log(newArr); // [2,2,3]

来解析一下上面这一段范例,我们建立了一个阵列 arr,这个阵列指向了一个新的记忆体位置,这时候我们再建立一个阵列 newArr,并且让 newArr 等於 arr,此时 newArr 会指向 arr 的记忆体位置,因此之後不论我们怎麽修改,console.log 都会呈现 newArr === arr 这样的结果,因为两个记忆体指向是在同一个位置。

不过这里还是要提到有一个例外,就是当我们直接用等号赋予新的值,就等於是建立一个新的记忆体位置,此时 arrnewArr 就不会指向同一个位置,因此回传 false

let arr = [1,2,3];
let newArr = arr;
console.log(newArr); // [1,2,3]

arr = [4,5,6]; // 这边赋予了新的记忆体位置
console.log(newArr); // [1,2,3]
console.log(newArr === arr); // false

结果可以看得出来,物件型别是以「记忆体位置为参考」互相传递,而不是以值做传递的过程就称为「传址」。

上面我们给了一个简易的原始型别比较的范例,这边我们可以来看看如果是物件型别的时候,是否会得到相同的结果呢?

let obj1 = { a: 1 };
let obj2 = { a: 1 };

obj1 === obj2;  // false

可以看到这边的结果跟原始物件的时候不同,这是因为每个物件都是独立存在的实体,两个物件的记忆体位置不同,而物件型别比较的是「记忆体位置」,不是值。

Pass by sharing

除了 Pass by value 和 Pass by reference,在查找资料的过程,还遇到了觉得其实是两种综合体的 Pass by sharing,刚刚我们谈论到 Pass by reference 的例外,其实就是 Pass by sharing。

Pass by sharing 比较像是融合了 by value 和 by reference:

  • 碰到基本型别,呈现的行为是 Pass by value 的状态。
  • 碰到物件型别,如果只是针对内容改变,呈现的行为是 Pass by reference,但是如果对物件是重兴赋予值的情况,则呈现为 Pass by value。

这个部分我们可以透过范例来证实:

function test(obj) {
  obj = { number: 20 }; // 物件重新赋值
  console.log(obj); // { number: 20 }
}

let a = { number: 10 }; // obj
test(a);

console.log(a); // { number: 10 } =>  这边没有一起改变

来解析这个过程,宣告 function test(obj),接着宣告 a 变数,对 a 赋予物件 { number: 10 },把 a 丢进 test function 里,等同於 obj 复制 a,大概是长这个样子 obj = a,因为变数资料是物件型别,在 function 里面又透过 obj = { number : 20 } 重新赋予值,这时候会有新的地址对应新的物件值,obj 会有新的地址,并且指向新的值。

因此 aobj 的地址不同,指向的值也不同所以最後印出的 a{ number: 10 },没有因为 obj 重新赋值而被改变。这边有一个很大的差异是,不是透过 obj.number = 20 去改变物件的值,因为以物件型别来说这样子写的确会形成 Pass by reference,但今天使用的重新赋值方法为 obj = { number : 20 },这会改变整个 obj 的值。

最後像这样写法,他会有点像是 Pass by value,obj 复制了 a,并呈现 aobj 两个记忆体位置,互相不受到影响。

以上概念其实就是混合了 Pass by value 和 Pass by reference 两种行为,稍微解释一下如果他们分别为 by value 或是 by reference 的情况:

  • Pass by value 的情况:传参数进 function 以後,透过 obj = { number: 20 } 重新赋予值,此时会创建新的地址与值,但由於外部变数与内部变数位置不同,指向也不同,因此不会互相受到影响。
  • Pass by reference 的情况:传参数进 function 以後,透过 obj.number = 20 改变内容,由於记忆体位置与指向都是同一个,因此会互相改变与影响。

obj 重新赋值以後,以 Pass by reference 的概念来说,应该要互相影响,但实际上却有点像是 Pass by value ,像这样的情况其实较偏向 Pass by sharing。


写完这篇其实自己也反覆看了几遍,为了尽量理解每一篇看过的资料,也是反覆的测试与思考,因此这篇内容都是以自己的理解范围下去整理,如果说明有误还请各位看文的大大们,能不吝啬告知,我会非常感激的!!

参考资料:
JS 原力觉醒 Day12- 传值呼叫、传址呼叫
重新认识 JavaScript: Day 05 JavaScript 是「传值」或「传址」?
[JavaScript] Javascript中的传值 by value 与传址 by reference
JS 变数传递探讨:pass by value 、 pass by reference 还是 pass by sharing?


<<:  DAY 13 接下来的实作

>>:  Day13 - 使用Chip和ChipGroup显示搜寻项目

Day26 - 使用 Share Target Picker 分享讯息

LINE Developers:https://developers.line.biz/zh-ha...

[Day30]- 新手的CTF系列picoCTF 2019

Day30- 新手的CTF系列picoCTF 2019 正文 终於!来到最後一篇了,好感动( ´▽`...

Day 28:Google Map 显示目前位置

本篇文章同步发表在 HKT 线上教室 部落格,线上影音教学课程已上架至 Udemy 和 Youtu...

Day11 NiFi & NiFi Registry

在Day3 Pipeline 如何做版本控制 - NiFi Registry就有提到 NiFi Re...