【学习笔记】JavaScript - 深浅拷贝

拷贝的问题主要是针对引用类型javascript

浅拷贝和深拷贝区别

对于这个问题,首先让咱们先简单回顾一下 JavaScript 的基本知识vue

一、JavaScript 包含两种不一样数据类型的值:基本类型(原始值)引用类型java

基本类型有如下几种,具体以下:
string、number、boolean、null、undefined、symbol、bigIntgit

引用类型具体有:
Object(Object、Array、Function...)github

在将一个值赋给变量时,解析器必须肯定这个值是基本类型仍是引用类型算法

  • 基本数据类型是按值访问的,由于能够操做保存在变量中的实际的值
  • 引用类型的值是保存在内存中的对象,栈内存存储的是变量的标识符以及对象在堆内存中的存储地址。JavaScript 不容许直接访问内存中的位置,即不能直接操做对象的内存空间。所以在操做对象时其实是对操做对象的引用而不是实际的对象。当须要访问引用类型(如对象、数组等)的值时,首先从栈中得到该对象的地址指针,而后再从对应的堆内存中取得所需的数据

二、JavaScript 的变量存储方式 -- 栈(stack)堆(heap)segmentfault

  • :自动分配内存空间,系统自动释放,里面存放的是基本类型的值和引用类型的地址指针
  • :动态分配内存,大小不定,也不会自动释放,里面存放引用类型的值

image.png

三、JavaScript 值传递与址传递
基本类型与引用类型最大的区别实际就是传值与传址的区别数组

  • 值传递:基本类型采用的是值传递
let a = 1;
let b = a;
b++;
console.log(a, b) // 1, 2
复制代码
  • 址传递:引用类型则是地址传递,将存放在栈内存中的地址赋值给接收的变量
let a = ['a', 'b', 'c'];
let b = a; 
b.push('d');
console.log(a) // ['a', 'b', 'c', 'd']
console.log(b) // ['a', 'b', 'c', 'd']
复制代码

分析:markdown

  • a 是数组是引用类型,赋值给 b 就是将 a 的地址赋值给 b,所以 a 和 b 指向同一个地址(该地址都指向了堆内存中引用类型的实际的值)
  • 当 b 改变了这个值的同时,由于 a 的地址也指向了这个值,故 a 的值也跟着变化,就比如 a 租了一间房,将房间的地址给了 b,b 经过地址找到了房间,那么 b 对房间作的任何改变对 a 来讲确定一样是可见的

那么如何解决上面出现的问题,这里就引出了浅拷贝或者深拷贝了。JS 的基本类型不存在浅拷贝仍是深拷贝的问题,主要是针对引用类型数据结构

浅拷贝:拷贝的级别浅。浅拷贝是指复制对象时只对第一层键值对进行复制,若对象内还有对象则只能复制嵌套对象的地址指针

  • 缺点:当有一个属性是引用值(数组或对象)时,按照这种克隆方式,只是把这个引用值的指向赋给了新的目标对象,即一旦改变了源对象或目标对象的引用值属性,另外一个也会跟着改变

深拷贝:拷贝级别更深。深拷贝是指复制对象时是彻底拷贝,即便嵌套了对象,拷贝后二者也相互不影响,修改一个对象的属性不会影响另外一个。原理实际上是递归把那些值是对象的属性再次进入对象内部进行复制

浅拷贝

sliceconcat
如果数组,数组元素均为基本数据类型,可利用数组的一些方法如 sliceconcat 返回一个新数组的特性来实现拷贝(此时至关于深拷贝)

若数组的元素是引用类型(Object,Array),sliceconcat 对对象数组的拷贝仍是浅拷贝,拷贝以后数组各个元素的指针仍是指向相同的存储地址

let arr = ['one', 'two', 'three'];
let newArr = arr.concat();
newArr.push('four')

console.log(arr)    // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]

let arr = ['one', 'two', 'three'];
let newArr = arr.slice();
newArr.push('four')

console.log(arr)    // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]

let arr = [{a:1}, 'two', 'three'];
let newArr = arr.concat();
newArr[0].a = 2;

console.log(arr)    // [{a: 2},"two","three"]
console.log(newArr) // [{a: 2},"two","three"]
复制代码

Object assign()
该方法能够把任意多个的源对象自身的可枚举属性拷贝给目标对象,而后返回目标对象。Object assign() 对对象的拷贝仍是浅拷贝

let arr = {
  a: 'one', 
  b: 'two', 
  c: 'three'
};

let newArr = Object.assign({}, arr)
newArr.d = 'four'
console.log(arr);    // {a: "one", b: "two", c: "three"}
console.log(newArr); // {a: "one", b: "two", c: "three", d: "four"}

let arr = {
  a: 'one', 
  b: 'two', 
  c: {a: 1}
};

let newArr = Object.assign({}, arr)
newArr.c.a = 3;
console.log(arr);    // {a: "one", b: "two", c: {a: 3}}
console.log(newArr); // {a: "one", b: "two", c: {a: 3}}
复制代码

Object.assign 原理及其实现

浅拷贝封装

原理:遍历对象,而后把属性和属性值放在一个新对象并返回

function clone(obj) {
  // 只拷贝对象
  if (typeof src !== 'object') return;
  // 根据 obj 的类型判断是新建一个数组仍是对象
  let newObj = Obejct.prototype.toString.call(obj) == '[object Array]' ? [] : {};
  for(let prop in newObj) {
    if(newObj.hasOwnProperty(prop)) {
      newObj[prop] = obj[src];
    }
  }
  return newObj;
}
复制代码

深拷贝

JSON.parse(JSON.stringify(arr)):不只适用于数组还适用于对象

let a = {
  name: "tn",
  book: {
    title: "JS",
    price: "45"
  }
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// name: "tn",
// book: {title: "JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "JS", price: "55"}
// } 

console.log(b);
// {
// name: "tn",
// book: {title: "JS", price: "45"}
// } 
复制代码

改变变量 a 中的引用属性后对 b 没有任何影响,这就是深拷贝的魔力

对数组深拷贝以后,改变原数组页不会影响到拷贝以后的数组

// 木易杨
let a = [0, "1", [2, 3]];
let b = JSON.parse(JSON.stringify( a.slice(1) ));
console.log(b); // ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a); // [0, "99", [4, 3]]

console.log(b); // ["1", [2, 3]]
复制代码

但该方法有局限性

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
  • 不能正确处理 new Date()
  • 不能处理正则

undefinedsymbol函数 会被直接忽略

// 木易杨
let obj = {
  name: "tn",
  a: undefined,
  b: Symbol("tn"),
  c: function() {}
}

let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn"}
复制代码

循环引用状况下会报错

let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3
  }
}
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON
复制代码

new Date 状况下转换结果不正确

new Date(); // Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)

JSON.stringify(new Date()); // ""2018-12-24T02:59:25.776Z""

JSON.parse(JSON.stringify(new Date())); // "2018-12-24T02:59:41.523Z"
复制代码

解决方法转成字符串或者时间戳

let date = (new Date()).valueOf();
JSON.stringify(date); // "1625905818735"
JSON.parse(JSON.stringify(date)); // 1625905818735
复制代码

正则状况下

let obj = {
  name: "tn",
  a: /'123'/
}
console.log(obj); // {name: "tn", a: /'123'/}
let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn", a: {}}
复制代码

ES6 扩展运算符[...]:不只适用于数组还适用于对象,只有原始值能够深拷贝,当含有引用值时进行浅拷贝

lodash 的深拷贝函数

深拷贝封装

原理:在拷贝时判断一下属性值的类型,如果对象则递归调用深拷贝函数,深拷贝是彻底拷贝了原对象的内容并寄存在新的内存空间,指向新的内存地址

function deepClone1(src, target) {
  var target = target || {};
  for (let prop in src) {
    if (src.hasOwnProperty(prop)) {
      if(src[prop] !== null && typeof(src[prop]) === 'object') {
        target[prop] = Object.prototype.toString.call(src[prop]) == '[object Array]' ? [] : {};
        deepClone(src[prop], target[prop]);
      } else {
        target[prop] = src[prop];
      }
    }
  }
  return target;
}

// test
var a = {
  name: "tn",
  book: {
    title: "JS",
    price: "45"
  },
  a1: undefined,
  a2: null,
  a3: 123
}

var b = deepClone(a);
console.log(b);
// {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {
// title: "JS", 
// price: "45"
// },
// name: "tn"
// }
复制代码

循环引用

咱们知道 JSON 没法深拷贝循环引用,遇到这种状况会抛出异常

使用哈希表

其实就是循环检测,设置一个数组或哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回

function deepClone2(src, hash = new WeakMap()) {
  var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
  if (hash.has(src)) return hash.get(src); // 新增代码,查哈希表
  hash.set(src, target); // 新增代码,哈希表设值
  for (let prop in src) {
    if (src.hasOwnProperty(prop)) {
      if(src[prop] !== null && typeof(src[prop]) === 'object') {
        target[prop] = deepClone2(src[prop], hash);
      } else {
        target[prop] = src[prop];
      }
    }
  }
  return target;
}

var a = {
  name: "tn",
  book: {
    title: "JS",
    price: "45"
  },
  a1: undefined,
  a2: null,
  a3: 123
};
a.circleRef = a;
var b = deepClone2(a);
console.log(b);
// {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {title: "JS", price: "45"},
// circleRef: {name: "tn", book: {…}, a1: undefined, a2: null, a3: 123, …},
// name: "tn"
// }
复制代码

使用数组

上面使用了 ES6 中的 WeakMap 来处理,在 ES5 下可使用数组来处理

function deepClone2(src, uniqueList) {
  var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
  if (!uniqueList) uniqueList = []; // 新增代码,初始化数组
  // 数据已经存在,返回保存的数据
  var uniqueData = find(uniqueList, src);
  if (uniqueData) {
     return uniqueData.target;
  };
  // 数据不存在,保存源数据,以及对应的引用
  uniqueList.push({
      source: src,
      target: target
  });
  for (let prop in src) {
    if (src.hasOwnProperty(prop)) {
      if(src[prop] !== null && typeof(src[prop]) === 'object') {
        target[prop] = deepClone2(src[prop], uniqueList);
      } else {
        target[prop] = src[prop];
      }
    }
  }
  return target;
}

// 用上面用例测试 OK
复制代码

如今已经很完美的解决了循环引用的状况,但其实还有一种状况是引用丢失,咱们看下面的例子

var obj1 = {};
var obj2 = {a: obj1, b: obj1};

obj2.a === obj2.b; // true

var obj3 = deepClone1(obj2);
obj3.a === obj3.b; // false
复制代码

引用丢失在某些状况下是有问题的,如上面的对象 obj2,obj2 的键值 a 和 b 同时引用了同一个对象 obj1,使用 deepClone1 进行深拷贝后就丢失了引用关系变成了两个不一样的对象

其实上面的 deepClone2 已经解决了这个问题,由于存储了已拷贝过的对象

var obj3 = deepClone2(obj2);
obj3.a === obj3.b; // true
复制代码

拷贝 Symbol

SymbolES6 下才有,须要一些方法来检测出 Symble 类型

  • 方法一:Object.getOwnPropertySymbols(...)

    该方法能够查找一个给定对象的符号属性时返回一个 ?symbol 类型的数组。注意,每一个初始化的对象都是没有本身的 symbol 属性的,所以这个数组可能为空,除非你已经在对象上设置了 symbol 属性(来自MDN)

    var obj = {};
    var a = Symbol("a"); // 建立新的 symbol 类型
    var b = Symbol.for("b"); // 从全局的 symbol 注册?表设置和取得symbol
    
    obj[a] = "localSymbol";
    obj[b] = "globalSymbol";
    
    var objectSymbols = Object.getOwnPropertySymbols(obj);
    
    console.log(objectSymbols.length); // 2
    console.log(objectSymbols)         // [Symbol(a), Symbol(b)]
    console.log(objectSymbols[0])      // Symbol(a)
    复制代码

    思路就是先查找有没有 Symbol 属性,若是查找到则先遍历处理 Symbol 状况,而后再处理正常状况

    function deepClone3(src, hash = new WeakMap()) {
      var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
      if (hash.has(src)) return hash.get(src); // 新增代码,查哈希表
      hash.set(src, target); // 新增代码,哈希表设值
    
      let symKeys = Object.getOwnPropertySymbols(src); // 查找
      if (symKeys.length) { // 查找成功
        symKeys.forEach(symKey => {
          if (src[prop] !== null && typeof(src[prop]) === 'object') {
            target[symKey] = deepClone3(src[symKey], hash); 
          } else {
            target[symKey] = src[symKey];
          }    
        });
      }
    
      for (let prop in src) {
        if (src.hasOwnProperty(prop)) {
          if(src[prop] !== null && typeof(src[prop]) === 'object') {
            target[prop] = deepClone3(src[prop], hash);
          } else {
            target[prop] = src[prop];
          }
        }
      }
      return target;
    }
    
    var a = {
      name: "tn",
      book: {
        title: "JS",
        price: "45"
      },
      a1: undefined,
      a2: null,
      a3: 123
    };
    var sym1 = Symbol("a"); // 建立新的symbol类型
    var sym2 = Symbol.for("b"); // 从全局的symbol注册?表设置和取得symbol
    
    a[sym1] = "localSymbol";
    a[sym2] = "globalSymbol";
    
    var b = deepClone3(a);
    console.log(b);
    
    // {
    // a1: undefined
    // a2: null
    // a3: 123,
    // book: {title: "JS", price: "45"},
    // circleRef: {name: "tn", book: {…}, a1: undefined, a2: null, a3: 123, …},
    // name: "tn",
    // [Symbol(a)]: "localSymbol",
    // [Symbol(b)]: "globalSymbol"
    // }
    复制代码
  • 方法二:Reflect.ownKeys(...)

    返回一个由目标对象自身的属性键组成的数组。它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))(来自MDN)

    Reflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ]
    Reflect.ownKeys([]); // ["length"]
    
    var sym = Symbol.for("comet");
    var sym2 = Symbol.for("meteor");
    var obj = {[sym]: 0, "str": 0, "773": 0, "0": 0,
              [sym2]: 0, "-1": 0, "8": 0, "second str": 0};
    Reflect.ownKeys(obj); // [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
    // 注意顺序
    // Indexes in numeric order, 
    // strings in insertion order, 
    // symbols in insertion order
    复制代码
    function deepClone3(src, hash = new WeakMap()) {
      var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
      if (hash.has(src)) return hash.get(src); 
      hash.set(src, target);
      
      Reflect.ownKeys(src).forEach(key => { // 改动
        if (src[key] !== null && typeof(src[key]) === 'object') {
          target[key] = deepClone3(src[key], hash); 
        } else {
          target[key] = src[key];
        }  
      });
      return target;
    }
    // 测试 ok
    复制代码

    这里使用了 Reflect.ownKeys() 获取全部的键值,同时包括 Symbol,对 src 遍历赋值便可

破解递归爆栈

上面使用的都是递归方法,可是有个问题是可能会爆栈,错误提示以下

// RangeError: Maximum call stack size exceeded
复制代码

详情请参考这篇文章:深拷贝的终极探索(99%的人都不知道)

库实现

上面的方式能够知足基本的场景的需求,如有更复杂的需求可本身实现。一些框架和库的也有对应的解决方案,如:jQuery.extend()lodash

应用场景

浅拷贝
对于一层结构的 ArrayObject 想要拷贝一个副本时使用 vuemixin 是浅拷贝的一种复杂型式

深拷贝
复制深层次的 object 数据结构,如想对某个数组或对象的值进行修改,但又要保留原数组或对象的值不被修改,此时就能够用深拷贝来建立一个新的数组或对象

参考资料

javascript中的深拷贝和浅拷贝?
JavaScript 如何完整实现深度Clone对象?
ithub lodash源码
MDN 结构化克隆算法 jQuery v3.2.1 源码

相关文章
相关标签/搜索