【JavaScript】赋值、深拷贝与浅拷贝

前言

  • ECMAScript变量包含两种不同数据类型的值:基本数据类型和引用数据类型。

    基本数据类型:名值存储在栈内存中;
    引用数据类型:名存在栈内存中,值存在于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值。
    目前基本数据类型有:Boolean、Null、Undefined、Number、String、Symbol,引用数据类型有:Object、Array、Function、RegExp、Date等

  • 深拷贝与浅拷贝的概念只存在于引用数据类型

一、赋值(Copy)

赋值是将某一数值或对象赋给某个变量的过程,分为:
1、基本数据类型:赋值,赋值之后两个变量互不影响
2、引用数据类型:赋地址值,两个变量具有相同的引用,指向同一个对象,相互之间有影响
对基本类型进行赋值操作,两个变量互不影响。

let a = "original";
let b = a;
console.log(b);  // original

a = "change";
console.log(a);   // change
console.log(b);    // original

对引用类型进行赋地址值操作,两个变量指向同一个对象,改变变量 a 之后会影响变量 b,哪怕改变的只是对象 a 中的基本类型数据。

// original
let a = {
    name: "original",
    book: {
        title: "Head First JavaScript",
        price: "50"
    }
}
let b = a;
console.log(b);
// {
//     name: "original",
//     book: {title: "Head First JavaScript", price: "50"}
// }

//copy
a.name = "change";
a.book.price = "70";
console.log(a);
// {
//     name: "change",
//     book: {title: "Head First JavaScript", price: "70"}
// }

console.log(b);
// {
//     name: "change",
//     book: {title: "Head First JavaScript", price: "70"}
// }

通常在大型的开发中并不希望某个变量后会影响到其他拷贝变量,这时我们就需要用到浅拷贝和深拷贝。

二、浅拷贝(Shallow Copy)

1、什么是浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
【JavaScript】赋值、深拷贝与浅拷贝_第1张图片
上图中,SourceObject是源对象,其中包含基本类型属性field1和引用类型属性 refObj。浅拷贝之后基本类型数据field2和filed1是不同属性,互不影响。但引用类型 refObj仍然是同一个,改变之后会对另一个对象产生影响。
简单来说可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。

2、浅拷贝使用场景

2.1 Object.assign()

Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,返回目标对象。
有些文章说Object.assign() 是深拷贝,其实这是不正确的。

// original
let a = {
    name: "original",
    book: {
        title: "Head First JavaScript",
        price: "50"
    }
}
let b = Object.assign({}, a);
console.log(b);
// {
//     name: "original",
//     book: {title: "Head First JavaScript", price: "50"}
// }

a.name = "change";
a.book.price = "70";
console.log(a);
// {
//     name: "change",
//     book: {title: "Head First JavaScript", price: "70"}
// }

console.log(b);
// {
//     name: "original",
//     book: {title: "Head First JavaScript", price: "70"}
// }

上面代码改变对象 a 之后,对象 b的基本属性保持不变。但是当改变对象 a 中的对象 book 时,对象 b 相应的位置也发生了变化。

2.2 扩展运算符Spread

利用扩展运算符可以在构造字面量对象时,进行克隆或者属性拷贝

let a = {
    name: "original",
    book: {
        title: "Head First JavaScript",
        price: "50"
    }
}
let b = {...a};
console.log(b);
// {
//     name: "original",
//     book: {title: "Head First JavaScript", price: "50"}
// }

a.name = "change";
a.book.price = "70";
console.log(a);
// {
//     name: "change",
//     book: {title: "Head First JavaScript", price: "70"}
// }

console.log(b);
// {
//     name: "original",
//     book: {title: "Head First JavaScript", price: "70"}
// }

2.3 Array.prototype.slice方法

slice不会改变原数组,slice()方法返回一个新的数组对象,这一对象是一个由begin和 end(不包括end)决定的原数组的浅拷贝。

let a = [0, "1", [2, 3]];
let b = 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", [4, 3]]

可以看出,改变 a[1]之后b[0]的值并没有发生变化,但改变a[2][0]之后,相应的b[1][0]的值也发生变化。
说明 slice()方法是浅拷贝,相应的还有concat
三、深拷贝(Deep Copy)
3.1 什么是深拷贝?
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

3.2 使用深拷贝的场景

3.2.1 JSON.parse(JSON.stringify(object))
let a = {
    name: "original",
    book: {
        title: "Head First JavaScript",
        price: "50"
    }
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
//     name: "original",
//     book: {title: "Head First JavaScript, price: "50"}
// }

a.name = "change";
a.book.price = "70";
console.log(a);
// {
//     name: "change",
//     book: {title: "Head First JavaScript", price: "70"}
// }

console.log(b);
// {
//     name: "original",
//     book: {title: "Head First JavaScript", price: "50"}
// }

完全改变变量 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]]

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

但是该方法有以下几个问题:
(1)会忽略 undefined
(2)会忽略 symbol
(3)不能序列化函数
(4)不能解决循环引用的对象
(5)不能正确处理new Date()
(6)不能处理正则
(1)(2)(3) undefinedsymbol 和函数这三种情况,会直接忽略。

let obj = {
    name: 'original',
    a: undefined,
    b: Symbol('original'),
    c: function () {
    }
}
console.log(obj);
// {
//     name: "original",
//     a: undefined,
//  b: Symbol(original),
//  c: ƒ ()
// }
let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "original"}

(4)循环引用会报错

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

(5)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();// 1545620645915
JSON.stringify(date);// "1545620673267"
JSON.parse(JSON.stringify(date));// 1545620658688

(6)正则情况下

let obj = {
    name: "original",
    a: /'123'/
}
console.log(obj);
// {name: "original", a: /'123'/}
let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "original", a: {}}

除了上面介绍的深拷贝方法,
常用的还有jQuery.extend()lodash.cloneDeep(),后面文章会详细介绍源码实现。

四、实现简单的深拷贝(递归实现)

由于进行JSON.stringify()序列化的过程中,undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
JS 提供的自有方法并不能彻底解决Array、Object的深拷贝问题,因此我们可以封装个函数实现一个支持所有类型的深拷贝。
深拷贝可以拆分成 2 步,浅拷贝 + 递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。

var arr = [1, [7, [9]], { a: '1'}, function () {}, null, undefined, NaN];
function deepCopy(arr) {
    if (typeof obj !== 'object') return;
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            if(obj[key]===null){newObj[key]=null;continue;}
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
        }
    }
    return newObj;
}
var result = deepCopy(arr);
arr[2][a]='222';

console.log(arr);
// output: [1, [7, [9]], {a:'222'} , function(){}, null, undefined, NaN];
console.log(result);
// output: [1, [7, [9]], { a: '1'}, function () {}, null, undefined, NaN]

五、总结

和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 与原数据一起改变 与原数据一起改变
浅拷贝 与原数据一起改变 与原数据一起改变
深拷贝 与原数据一起改变 与原数据一起改变
  • 如果要实现深拷贝,用什么方法来实现?

JSON.parse() + JSON.stringify()(缺点:只能处理可以被枚举的属性);
for in 循环递归遍历;
深拷贝就是能够实现真正意义上的数组和对象的拷贝。递归调用"浅拷贝"。(深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象)

  • 如果你想要实现支持setter和getter特性的拷贝,该怎么实现?

Object.defineproperties (定义属性)、Object.getOwnPropertyDescriptors(es2017,获取对象的多个属性)、Object.getOwnPropertyDescriptor(老一点,获取对象的单个属性的属性),但babel可以解决。

参考

MDN 之 Object.assign()
MDN 之展开语法
MDN 之 Array.prototype.slice()

你可能感兴趣的:(WEB)