前端百題斬—賦值、淺拷貝、深拷貝大PK
寫(xiě)該系列文章的初衷是“讓每位前端工程師掌握高頻知識(shí)點(diǎn),為工作助力”。
相信老鐵們不管是在學(xué)習(xí)還是面試過(guò)程中,都會(huì)遇到賦值、淺拷貝、深拷貝,特別是淺拷貝和深拷貝,我記憶比較深刻的遇到這個(gè)問(wèn)題有兩次:
一次系統(tǒng)寫(xiě)出bug就是因?yàn)閷?duì)深淺拷貝理解不清楚;
百度面試。
1 賦值
賦值指的就是將一個(gè)變量直接賦值給另一個(gè)變量,如下所示:
- const a1 = 10;
- const a2 = a1;
- console.log(a2); // 10
- const b1 = {
- m: 10,
- n: 20
- };
- const b2 = b1;
- console.log(b2); // { m: 10, n: 20 }
如上所示,賦值就是將一個(gè)值賦給另一個(gè)值,在賦值過(guò)程中要注意兩點(diǎn):
- 對(duì)于基本類(lèi)型賦值就是在棧內(nèi)存中開(kāi)辟一個(gè)新的存儲(chǔ)區(qū)域來(lái)存儲(chǔ)新的變量;
- 對(duì)于引用類(lèi)型賦值,就是將該引用類(lèi)型的地址,該地址指向堆中的同一值。
2 淺拷貝
2.1 基本實(shí)現(xiàn)
淺拷貝指的就是循環(huán)遍歷對(duì)象一遍,將該對(duì)象上的屬性賦值到另一個(gè)對(duì)象上。在這個(gè)過(guò)程中屬性值為基本類(lèi)型則拷貝的就是基本類(lèi)型的值;若該值為引用類(lèi)型,則拷貝的就是就是一個(gè)內(nèi)存地址。
- function clone(source) {
- if (!(typeof source === 'object' && source !== null)) {
- return source;
- }
- const target = {}; // 只考慮Object類(lèi)型
- for (let [key, value] of Object.entries(source)) {
- target[key] = value;
- }
- return target;
- }
- const obj = {
- a: 10,
- b: {
- m: 20
- }
- };
- const cloneObj = clone(obj);
- cloneObj.a = 20;
- cloneObj.b.m = 30;
- console.log(obj); // { a: 10, b: { m: 30 } }
- console.log(cloneObj); // { a: 20, b: { m: 30 } }
上述就是簡(jiǎn)單的淺拷貝過(guò)程,可以看到淺拷貝就是將原始對(duì)象中的值遍歷一層,然后賦值給一個(gè)新的對(duì)象。在遍歷過(guò)程中可以獲取到一下信息:
- 遍歷到a屬性的時(shí)候,其是一個(gè)基本類(lèi)型,所以會(huì)在棧內(nèi)存中創(chuàng)建一個(gè)新的存儲(chǔ)區(qū)域來(lái)存儲(chǔ)變量。
- 遍歷到b屬性的時(shí)候,由于其為引用類(lèi)型,其會(huì)在棧內(nèi)存中存儲(chǔ)器堆地址,從而指向堆內(nèi)存中的同一對(duì)象。
- 當(dāng)通過(guò)淺拷貝創(chuàng)建的對(duì)象cloneObj中的a屬性和b.m屬性重新賦值,可以發(fā)現(xiàn)a屬性值不一樣,但b.m屬性值卻發(fā)生了變化,從而驗(yàn)證了上述1、2兩條分析。
2.2 進(jìn)階
既然本章我們講了淺拷貝,那么不得不了解Object.assign(),該方法就是一個(gè)淺拷貝的過(guò)程,用于對(duì)象的合并,將源對(duì)象(source)的所有可枚舉屬性,復(fù)制到目標(biāo)對(duì)象(target)。
2.2.1 基礎(chǔ)
要實(shí)現(xiàn)一個(gè)函數(shù)首先應(yīng)該了解一個(gè)函數(shù),對(duì)于該方法的基本使用就不再贅述,下面主要講幾個(gè)注意點(diǎn):
- 如果目標(biāo)對(duì)象與源對(duì)象有同名屬性(或多個(gè)源對(duì)象有同名屬性),則后面的屬性會(huì)覆蓋前面的屬性;
- 如果只有一個(gè)參數(shù),Object.assign會(huì)直接返回該參數(shù)。如果該參數(shù)不是對(duì)象,則會(huì)先轉(zhuǎn)為對(duì)象,然后再返回;(注意:由于undefined和null無(wú)法轉(zhuǎn)為對(duì)象,將它們作為參數(shù)會(huì)報(bào)錯(cuò))
- 非對(duì)象參數(shù)出現(xiàn)在源對(duì)象位置,這些參數(shù)會(huì)轉(zhuǎn)化為對(duì)象,如果無(wú)法轉(zhuǎn)成對(duì)象便跳過(guò)(所以u(píng)ndefined和null不會(huì)報(bào)錯(cuò))。(注意:字符串會(huì)以數(shù)組形式復(fù)制到目標(biāo)對(duì)象,其它不會(huì))
- 只復(fù)制源對(duì)象的自身屬性(不復(fù)制繼承屬性),也不復(fù)制不可枚舉的屬性;
- 屬性名為Symbol值的屬性也會(huì)被Object.assign復(fù)制。
2.2.2 實(shí)現(xiàn)
上面闡述了主要的注意點(diǎn),下面我們就來(lái)實(shí)現(xiàn)一下Object.assign(),實(shí)現(xiàn)步驟如下所示:
- 對(duì)目標(biāo)對(duì)象進(jìn)行判斷,不能為null和undefined;
- 將目標(biāo)轉(zhuǎn)換為對(duì)象(防止string、number等);
- 獲取后續(xù)源對(duì)象自身中的可枚舉對(duì)象(包含Symbol)復(fù)制到目標(biāo)對(duì)象;
- 返回該處理好的目標(biāo)對(duì)象;
- 利用Object.defineProperty()將該函數(shù)配置為不可枚舉的掛載到Object上。
- function ObjectAssign(target, ...sources) {
- // 對(duì)第一個(gè)參數(shù)進(jìn)行判斷,不能為undefined和null
- if (target === undefined || target === null) {
- throw new TypeError('cannot convert first argument to object');
- }
- // 將第一個(gè)參數(shù)轉(zhuǎn)換為對(duì)象
- const targetObj = Object(target);
- // 將源對(duì)象(source)自身的所有可枚舉屬性復(fù)制到目標(biāo)對(duì)象(target)
- for (let i = 0; i < sources.length; i++) {
- let source = sources[i];
- // 對(duì)于undefined和null在源對(duì)象中不會(huì)報(bào)錯(cuò),會(huì)直接跳過(guò)
- if (source !== undefined && source !== null) {
- // 將源角色轉(zhuǎn)換成對(duì)象
- // 需要將源角色自身的可枚舉屬性(包含Symbol值的屬性)進(jìn)行復(fù)制
- // Reflect.ownKeys(obj) 返回一個(gè)數(shù)組,包含對(duì)象自身的所有屬性,不管屬性名是Symbol還是字符串,也不管是否可枚舉
- const keysArrays = Reflect.ownKeys(Object(source));
- for (let nextIndex = 0; nextIndex < keysArrays.length; nextIndex++) {
- const nextKey = keysArrays[nextIndex];
- // 去除不可枚舉屬性
- const desc = Object.getOwnPropertyDescriptor(source, nextKey);
- if (desc !== undefined && desc.enumerable) {
- targetObj[nextKey] = source[nextKey];
- }
- }
- }
- }
- return targetObj;
- }
- // 由于掛載到Object的assign是不可枚舉的,直接掛載上去是可枚舉的,所以采用這種方式
- if (typeof Object.myAssign !== 'function') {
- Object.defineProperty(Object, "myAssign", {
- value: ObjectAssign,
- writable: true,
- enumerable: false,
- configurable: true
- });
- }
- const target = {
- a: 10
- };
- const source1 = {
- b: 20,
- c: 30
- };
- const source2 = {
- c: 40
- };
- console.log(Object.assign(target, source1, source2)); // { a: 10, b: 20, c: 40 }
- console.log(Object.myAssign(target, source1, source2)); // { a: 10, b: 20, c: 40 }
3 深拷貝
深拷貝其實(shí)就是淺拷貝的進(jìn)階版,因?yàn)闇\拷貝只循環(huán)遍歷了一層數(shù)據(jù),對(duì)于引用類(lèi)型拷貝的是對(duì)象的地址,但是深拷貝會(huì)進(jìn)行多層的遍歷,將所有數(shù)據(jù)進(jìn)行數(shù)據(jù)層面的拷貝。下面就利用三種方式實(shí)現(xiàn)深拷貝。(這篇文章寫(xiě)得很好,大家可以一起看一下)
3.1 乞丐版
首先來(lái)看一下最簡(jiǎn)單的深拷貝方式,就是利用JSON.stringify()和JSON.parse(),但是該方式其實(shí)是存在很多問(wèn)題的:
- 不能正確處理正則表達(dá)式,其會(huì)變?yōu)榭諏?duì)象;
- 不能正確處理函數(shù),其變?yōu)閡ndefined;
- 不能正常輸出值為undefined的內(nèi)容。
- function cloneDeep(source) {
- return JSON.parse(JSON.stringify(source));
- }
- const obj = {
- a: 10,
- b: undefined,
- c: /\w/g,
- d: function() {
- return true;
- }
- };
- console.log(obj); // { a: 10, b: undefined, c: /\w/g, d: [Function: d] }
- console.log(cloneDeep(obj)); // { a: 10, c: {} }
3.2 遞歸版
既然乞丐版有這么多問(wèn)題,那么就嘗試一下“淺拷貝+遞歸”的方式實(shí)現(xiàn)一下。
- function cloneDeep(source) {
- // 如果輸入的為基本類(lèi)型,直接返回
- if (!(typeof source === 'object' && source !== null)) {
- return source;
- }
- // 判斷輸入的為數(shù)組還是對(duì)象,進(jìn)行對(duì)應(yīng)的創(chuàng)建
- const target = Array.isArray(source) ? [] : {};
- for (let [key, value] of Object.entries(source)) {
- // 此處應(yīng)該去除一些內(nèi)置對(duì)象,根據(jù)需要可以自己去除,本初只去除了RegExp對(duì)象
- if (typeof value === 'object' && value !== null && !(value instanceof RegExp)) {
- target[key] = cloneDeep(value);
- }
- else {
- target[key] = value;
- }
- }
- return target;
- }
- const obj = {
- a: 10,
- b: undefined,
- c: /\w/g,
- d: function() {
- return true;
- },
- e: {
- m: 20,
- n: 30
- }
- };
- const result = cloneDeep(obj);
- result.e.m = 100;
- console.log('拷貝前:', obj);
- console.log('拷貝后:', result);
輸出結(jié)果如下所示:
3.3 循環(huán)方式
利用遞歸的方式實(shí)現(xiàn)深拷貝,其實(shí)是存在爆棧的風(fēng)險(xiǎn)的,下面就將遞歸的方式改為循環(huán)的方式。
- // 循環(huán)方式
- function cloneDeep(source) {
- if (!(typeof source === 'object' && source !== null)) {
- return source;
- }
- const root = Array.isArray(source) ? [] : {};
- // 定義一個(gè)棧
- const loopList = [{
- parent: root,
- key: undefined,
- data: source,
- }];
- while (loopList.length > 0) {
- // 深度優(yōu)先
- const node = loopList.pop();
- const parent = node.parent;
- const key = node.key;
- const data = node.data;
- // 初始化賦值目標(biāo),key為undefined則拷貝到父元素,否則拷貝到子元素
- let res = parent;
- if (typeof key !== 'undefined') {
- res = parent[key] = Array.isArray(data) ? [] : {};
- }
- for (let [childKey, value] of Object.entries(data)) {
- if (typeof value === 'object' && value !== null && !(value instanceof RegExp)) {
- loopList.push({
- parent: res,
- key: childKey,
- data: value
- });
- } else {
- res[childKey] = value;
- }
- }
- }
- return root;
- }
- const obj = {
- a: 10,
- b: undefined,
- c: /\w/g,
- d: function() {
- return true;
- },
- e: {
- m: 20,
- n: 30
- }
- };
- const result = cloneDeep(obj);
- result.e.m = 100;
- console.log('拷貝前:', obj);
- console.log('拷貝后:', result);
輸出結(jié)果如下所示: