Day 14 - Object and Arrays - Reference VS Copy

前言

JS 30 是由加拿大的全端工程师 Wes Bos 免费提供的 JavaScript 简单应用课程,课程主打 No Frameworks、No Compilers、No Libraries、No Boilerplate 在30天的30部教学影片里,建立30个JavaScript的有趣小东西。

另外,Wes Bos 也很无私地在 Github 上公开了所有 JS 30 课程的程序码,有兴趣的话可以去 fork 或下载。


本日目标

了解 JavaScript 的 Passed by ValuePassed by Reference 以及 Object 的 shallow copydeep copy


解析程序码

JS 部分

Passed by Value

在 JavaScript 里,如果变数的资料型态属於 Primitive(原生型别),则在传递变数时,采用的是 Passed by Value (传递值)。

常见的 Primitive:
  • String
  • Number
  • BigInt
  • Boolean
  • Symbol

在下面的两个例子,我们分别将agename传递给age2name2,之後改变age2name2的数值。结果原本的agename均不会受到影响,因为在传递过程是 Passed by Value,将原本存於agename的值复制一份给age2name2

let age = 100;
let age2 = age;
console.log(age,age2);

age = 200;
console.log(age,age2);
let name = 'Mes';
let name2 = name;
console.log(name,name2);

name = 'wesley';
console.log(name,name2);

  • 简易概念图

Passed by Reference

在 JavaScript 里,如果变数的资料型态属於 Object(物件型别),则在传递变数时,采用的是 Passed by Reference (传递位址)。

常见的 Object:
  • Array
  • Object

下面的例子,我们将阵列 players 传递给常数 team,之後将team[3](Poppy)改成Lux,再把两个阵列都印到 console,会发现到两个阵列的第四个位置都被修改成新的值(Lux)。

这是因为物件在传递的过程中采取的是 Passed by Reference,也就是将物件在记忆体上的位置传递给另一个物件(两者共享同一个记忆体位置),所以只要更改其中一方,另一方也会受到影响。

const players = ['Wes','Sarah','Ryan','Poppy'];
const team = players;
console.log(players,team);
    
team[3] = 'Lux';
console.log(players,team);

如果想修改阵列的值又不影响另一个阵列的话,我们可以使用slice()来复制阵列并回传新阵列(拥有自己的记忆体空间)。

下面的例子,我们呼叫players.slice()将阵列players完整的复制一份到新阵列中并传递给team2,之後再修改team2的第四个位置,这次我们发现到原来的阵列players并没有被修改到。

注意:slice()进行的是shallow copy(浅复制)!!!

const team2 = players.slice()
team2[3] = 'Lux';

console.log(players,team2)

其他修改阵列的值又不影响另一个阵列的方法有:

  • concat() : 将多个阵列进行串联和slice()一样会回传串联後的新阵列。
  • Spread : 将阵列中的元素展开并逐个放到新阵列中。
  • Array.from() : 建立一个新的 Array 实体。
const team3 = [].concat(players);

//use the new ES6 Spread
const team4 = [...players];

const team5 = Array.from(players);

另一个例子,我们宣告物件person并把它传递给captain,在captain上新增number: 90後将两个物件都印到 consle,此时会发现在captain新增的number: 90,也会被加到person上。会这样是因为物件在传递过程是 Passed by Reference,两物件同时指向一个记忆体空间。

const person = {
      name: 'Wes Bos',
      age: 80
};

const captain = person;
captain.number = 99;

console.log(person,captain);

如果不想修改到另一个物件的话,我们可以使用Object.assign(target, ...sources)来复制一个或多个物件的属性到另一个目标物件,最後回传目标物件。

下面我们利用Object.assign({},person,{number: 99,age:12});,将物件person{number:99,age:12}的属性复制到目标物件({ })。

如果复制的多个物件的属性有重复,以後面物件的属性为准进行合并。举例来说age是重复出现的属性,最後复制属性值时,要以後面出现的age:12为准合并物件。

注意 : Object.assign()做的是 shallow copy(浅复制),如果要复制的物件属性包含子物件,就会复制到子物件的参照(reference)!!!。

const person = {
      name: 'Wes Bos',
      age: 80
};

const cap2 = Object.assign({},person,{number: 99,age:12});
console.log(person,cap2);

原来的person并没有被改动到。

  • 简单概念图

Shallow Copy & Deep Copy

前面我们有提到,Object.assign()做的是 shallow copy(浅复制),若要复制的物件属性包含子物件,就会复制到子物件的参照。

所以只要去改动新物件的子物件属性值,就会连带影响原本复制的物件子属性值。

下面我们用Object.assign()复制来源物件(wes)的属性到目标物件({}),再将目标物件传递给dev物件,要留意wes的属性包含子物件属性(social)。

例子中我们修改复制来的物件属性(name)和子物件属性(social.twitter),之後把wesdev物件印到 console 会发现复制的来源物件wes的属性(name)不受影响,但wes的子物件属性(social.twitter)却被影响。

const wes = {
      name: 'Wes',
      age: 100,
      social: {
        twitter: '@wesbos',
        facebook: 'wesbos.developer'
      }
}

//shallow copy
const dev = Object.assign({},wes);
dev.name = 'Wesley';
console.log(wes,dev);

dev.social.twitter = '@coolman';
console.log(wes.social,dev.social);

如果不想改动复制来源物件的子属性值(social.twitter),我们可以将要复制的物件用JSON.stringify()先转换成 JSON String(因为是Primitive,所以是 Passed by Value),再用JSON.parse()把它还原成物件再回传给dev2

下面我们一样修改子物件的属性值,但这次的原物件子属性值并没有受到影响。

const wes = {
      name: 'Wes',
      age: 100,
      social: {
        twitter: '@wesbos',
        facebook: 'wesbos.developer'
      }
}

// deep copy
const dev2 = JSON.parse(JSON.stringify(wes));//先转成string再换回object
dev2.social.twitter = '@coolman';
console.log(wes.social,dev2.social);

补充资料:

JavaScript: var, let, const 差异
Array.prototype.concat()
Spread syntax (...)
Array.from()
Object.assign()
JSON.stringify()
JSON.parse()


<<:  AE袅袅升起的烟(香菸)-Day13

>>:  GitHub Action YAML 撰写技巧 - 环境变数(Environment Variables) 与 秘密 (Secrets)

DAY27 学习30天的c++

程序基本结构 程序的基本结构可概分为循序式结构、选择式结构,与重复式结构三种,几乎是在循序结构式的基...

[第29天]理财达人Mx. Ada-布林通道(Bollinger Band)

前言 本文说明使用TA-Lib函式库计算及呈现布林通道。 布林通道 布林通道(Bollinger B...

Day 28. 凭证绑定 Certificate Pinning 绑起来!

今天来介绍,数位凭证,讲一点点数位签章,以及最重要的凭证绑定原理及用途 避免有人透过手机使用 Bu...

Render Props ( Day20 )

「render prop」这个词指的是一种用一个其值为函式的 prop 来在 React comp...

[Day26]程序菜鸟自学C++资料结构演算法 – 合并排序法(Merge Sort)

前言:今天要来介绍第二种分割资料的排序法,就让我们来看看这个有趣的排序法吧! 合并排序: 首先会将一...