搞不懂JS中賦值·淺拷貝·深拷貝的請看這裏

圖片描述

前言

爲何寫拷貝這篇文章?同事有一天提到了拷貝,他說賦值就是一種淺拷貝方式,另外一個同事說賦值和淺拷貝並不相同。
我也有些疑惑,因而我去MDN搜一下拷貝相關內容,發現並無關於拷貝的實質概念,沒有辦法只能經過實踐了,同時去看一些前輩們的文章總結了這篇關於拷貝的內容,本文也屬於公衆號【程序員成長指北】學習路線中【JS必知必會】內容。javascript

數據類型與堆棧的關係

基本類型與引用類型

  • 基本類型:undefined,null,Boolean,String,Number,Symbol
  • 引用類型:Object,Array,Date,Function,RegExp等

存儲方式

  • 基本類型:基本類型值在內存中佔據固定大小,保存在棧內存中(不包含閉包中的變量)

16bd2282836bad2c?w=370&h=346&f=jpeg&s=17652

  • 引用類型:引用類型的值是對象,保存在堆內存中。而棧內存存儲的是對象的變量標識符以及對象在堆內存中的存儲地址(引用),引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中得到實體。

16bd228c2ad68a18?w=758&h=256&f=jpeg&s=24273

注意:前端

  1. 閉包中的變量並不保存在棧內存中,而是保存在堆內存中。這一點比較好想,若是閉包中的變量保存在了棧內存中,隨着外層中的函數從調用棧中銷燬,變量確定也會被銷燬,可是若是保存在了堆內存中,內存函數仍能訪問外層已銷燬函數中的變量。看一段對應代碼理解下:
function A() {
  let a = 'koala'
  function B() {
      console.log(a)
  }
  return B
}
  1. 本篇所講的淺拷貝和深拷貝都是對於引用類型的,對於基礎類型不會有這種操做。

賦值操做

基本數據類型複製

看一段代碼java

let a ='koala';
let b = a;
b='程序員成長指北';
console.log(a); // koala

基本數據類型複製配圖:程序員

16bd22955385ae3e?w=698&h=289&f=png&s=10920

結論:在棧內存中的數據發生數據變化的時候,系統會自動爲新的變量分配一個新的之值在棧內存中,兩個變量相互獨立,互不影響的。api

引用數據類型複製

看一段代碼數組

let a = {x:'kaola', y:'kaola1'}
let b = a;
b.x = '程序員成長指北';
console.log(a.x); // 程序員成長指北

引用數據類型複製配圖:閉包


結論:引用類型的複製,一樣爲新的變量b分配一個新的值,報錯在棧內存中,不一樣的是這個變量對應的具體值不在棧中,棧中只是一個地址指針。兩個變量地址指針相同,指向堆內存中的對象,所以b.x發生改變的時候,a.x也發生了改變。koa

淺拷貝

淺拷貝定義:

不知道的api我通常比較喜歡看MDN,淺拷貝的概念MDN官方並無給出明肯定義,可是搜到了一個函數Array.prototype.slice,官方說它能夠實現原數組的淺拷貝。
對於官方給的結論,咱們經過兩段代碼驗證一下,並總結出淺拷貝的定義。函數

  • 第一段代碼:
var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[0] = 2;
console.log(a); // [ 1, 3, 5, { x: 1 } ];
console.log(b); // [ 2, 3, 5, { x: 1 } ];

從輸出結果能夠看出,淺拷貝後,數組a[0]並不會隨着b[0]改變而改變,說明a和b在棧內存中引用地址並不相同。學習

  • 第二段代碼
var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[3].x = 2;
console.log(a); // [ 1, 3, 5, { x: 2 } ];
console.log(b); // [ 1, 3, 5, { x: 2 } ];

從輸出結果能夠看出,淺拷貝後,數組中對象的屬性會根據修改而改變,說明淺拷貝的時候拷貝的已存在對象的對象的屬性引用。

  • 淺拷貝定義

經過這個官方的slice淺拷貝函數分析淺拷貝定義

新的對象複製已有對象中非對象屬性的值和對象屬性的引用。若是這種說法不理解換一種一個新的對象直接拷貝已存在的對象的對象屬性的引用,即淺拷貝。

淺拷貝實例

Object.assign

  • 語法:
語法:Object.assign(target, ...sources)

ES6中拷貝對象的方法,接受的第一個參數是拷貝的目標target,剩下的參數是拷貝的源對象sources(能夠是多個)

  • 舉例說明:
let target = {};
let source = {a:'koala',b:{name:'程序員成長指北'}};
Object.assign(target ,source);
console.log(target); // { a: 'koala', b: { name: '程序員成長指北' } }
source.a = 'smallKoala';
source.b.name = '程序員成長指北哦'
console.log(source); // { a: 'smallKoala', b: { name: '程序員成長指北哦' } }
console.log(target); // { a: 'koala', b: { name: '程序員成長指北哦' } }

從打印結果能夠看出,Object.assign是一個淺拷貝,它只是在根屬性(對象的第一層級)建立了一個新的對象,可是對於屬性的值是對象的話只會拷貝一份相同的內存地址。

  • Object.assign注意事項
  1. 只拷貝源對象的自身屬性(不拷貝繼承屬性)
  2. 它不會拷貝對象不可枚舉的屬性
  3. undefinednull沒法轉成對象,它們不能做爲Object.assign參數,可是能夠做爲源對象
Object.assign(undefined) // 報錯
Object.assign(null) // 報錯

let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
  1. 屬性名爲 Symbol 值的屬性,能夠被Object.assign拷貝。

Array.prototype.slice

這個函數在淺拷貝概念定義的時候已經進行了分析,看上文。

Array.prototype.concat

  • 語法
var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
參數:將數組和/或值鏈接成新數組
  • 舉例說明
let array = [{a: 1}, {b: 2}];
let array1 = [{c: 3},{d: 4}];
let array2=array.concat(array1);
array1[0].c=123;
console.log(array2);// [ { a: 1 }, { b: 2 }, { c: 123 }, { d: 4 } ]
console.log(array1);// [ { c: 123 }, { d: 4 } ]

Array.prototype.concat也是一個淺拷貝,只是在根屬性(對象的第一層級)建立了一個新的對象,可是對於屬性的值是對象的話只會拷貝一份相同的內存地址。

...擴展運算符

  • 語法
var cloneObj = { ...obj };
  • 舉例說明
let obj = {a:1,b:{c:1}}
let obj2 = {...obj};
obj.a=2;
console.log(obj); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}

obj.b.c = 2;
console.log(obj); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

擴展運算符也是淺拷貝,對於值是對象的屬性沒法徹底拷貝成2個不一樣對象,可是若是屬性都是基本類型的值的話,使用擴展運算符也是優點方便的地方。

補充說明:以上4中淺拷貝方式都不會改變原數組,只會返回一個淺拷貝了原數組中的元素的一個新數組。

本身實現一個淺拷貝

實現原理:新的對象複製已有對象中非對象屬性的值和對象屬性的引用,也就是說對象屬性並不複製到內存。

  • 實現代碼:
function cloneShallow(source) {
    var target = {};
    for (var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            target[key] = source[key];
        }
    }
    return target;
}
  • for in與hasOwnProperty函數說明,怕有些人小夥伴可能不清楚具體內容

for in

for...in語句以任意順序遍歷一個對象自有的、繼承的、可枚舉的、非Symbol的屬性。對於每一個不一樣的屬性,語句都會被執行。

hasOwnProperty

語法:obj.hasOwnProperty(prop)
prop是要檢測的屬性 字符串名稱或者 Symbol

該函數返回值爲布爾值,全部繼承了 Object 的對象都會繼承到 hasOwnProperty 方法,和 in 運算符不一樣,該函數會忽略掉那些從原型鏈上繼承到的屬性和自身屬性。

深拷貝操做

說了賦值操做和淺拷貝操做,你們是否是已經能想到什麼是深拷貝了,下面直接說深拷貝的定義。

深拷貝定義

深拷貝會另外拷貝一份一個如出一轍的對象,從堆內存中開闢一個新的區域存放新對象,新對象跟原對象不共享內存,修改新對象不會改到原對象。

深拷貝實例

JSON.parse(JSON.stringify())

JSON.stringify()是前端開發過程當中比較經常使用的深拷貝方式。原理是把一個對象序列化成爲一個JSON字符串,將對象的內容轉換成字符串的形式再保存在磁盤上,再用JSON.parse()反序列化將JSON字符串變成一個新的對象

  • 舉例說明:
let arr = [1, 3, {
    username: ' koala'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'smallKoala'; 
console.log(arr4);// [ 1, 3, { username: 'smallKoala' } ]
console.log(arr);// [ 1, 3, { username: ' koala' } ]

實現了深拷貝,當改變數組中對象的值時候,原數組中的內容並無發生改變。JSON.stringify()雖然能夠實現深拷貝,可是還有一些弊端好比不能處理函數等。

  • JSON.stringify()實現深拷貝注意點
  1. 拷貝的對象的值中若是有函數,undefined,symbol則通過JSON.stringify()序列化後的JSON字符串中這個鍵值對會消失
  2. 沒法拷貝不可枚舉的屬性,沒法拷貝對象的原型鏈
  3. 拷貝Date引用類型會變成字符串
  4. 拷貝RegExp引用類型會變成空對象
  5. 對象中含有NaN、Infinity和-Infinity,則序列化的結果會變成null
  6. 沒法拷貝對象的循環應用(即obj[key] = obj)

本身實現一個簡單深拷貝

深拷貝,主要用到的思想是遞歸,遍歷對象、數組直到裏邊都是基本數據類型,而後再去複製,就是深度拷貝。
實現代碼:

//定義檢測數據類型的功能函數
    function isObject(obj) {
        return typeof obj === 'object' && obj != null;
    }
   function cloneDeep(source) {

    if (!isObject(source)) return source; // 非對象返回自身
      
    var target = Array.isArray(source) ? [] : {};
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (isObject(source[key])) {
                target[key] = cloneDeep(source[key]); // 注意這裏
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

該簡單深拷貝未考慮內容:
遇到循環引用,會陷入一個循環的遞歸過程,從而致使爆棧

// RangeError: Maximum call stack size exceeded

小夥伴們有沒有什麼好辦法呢,能夠寫下代碼在評論區一塊兒討論哦!

第三方深拷貝庫

該函數庫也有提供_.cloneDeep用來作 Deep Copy(lodash是一個不錯的第三方開源庫,有好多不錯的函數,也能夠看具體的實現源碼)

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false

拷貝內容總結

用一張圖總結

16bd245dc6916200?w=800&h=241&f=png&s=23103

今天就分享這麼多,若是對分享的內容感興趣,能夠關注公衆號「程序員成長指北」,或者加入技術交流羣,你們一塊兒討論。

進階技術路線

16b8a3c3064ef334?w=1576&h=800&f=png&s=330091
加入咱們一塊兒學習吧!
16b8a3d23a52b7d0?w=940&h=400&f=jpeg&s=217901

相關文章
相關標籤/搜索