javascript 拷贝详解

一、ECMAScript 数据类型

1、ES 变量包含两种不同类型的值:基本类型、引用类型,又称之为原始数据类型。

  基本类型:Number、String、Boolean、Null、Undefined;

  引用类型:Object、Array、Date、RegExp、Function、Math、Symbol,

  其中Number、String、Boolean又称之为基本包装类型,因为它们有自己相关的属性和方法。

2、两种类型的区别是,存储位置不同

  基本类型:直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;

  引用类型:存储在堆(heap)中的对象,占据空间大、大小不固定,如果存储在栈中,将会影响程序运行的性能;

  引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

二、浅拷贝和深拷贝

  先来看两个例子:

// 栗子一:
let num1 = 10;
let num2 = num1;
num1 = 20;
console.log(num1, num2);// 20 10

  这个小小的案例相信我们每个人都可以接受的,num1和num2保存的值是分别存放在栈中的不同位置,因为存储的是一个简单的数据段。那么接下来看下一条例子:

// 栗子二:
let arr1 = [1, 2, 3];
let arr2 = arr1;
arr1[0] = 3;
console.log(arr1, arr2);// [3, 2, 3] [3, 2, 3]

  这时候是不是就有点儿微妙的感觉了?输出的两个数组的值是一样的,这是为什么呢?

  因为数组属于引用类型,那么arr1必然会在栈中存储了一个指向当前变量存储在堆中的数据的一个地址。然后将arr1赋值给arr2,就相当于arr2也存储了一个跟arr1一摸一样的地址。既然指向的是同一块数据,那么必然导致arr1的值发生变化,arr2的值也跟着变化。说再精确一点就是arr1和arr2共享的是堆中的同一块内存。

  说这么多,那么究竟什么是浅拷贝?什么是深拷贝?

  根据上面的两个小小栗子,我们可以得出这样的结论:

浅拷贝:将一个变量赋值给另一个变量时,两个变量最终放置的位置在同一块内存中,那么就是浅拷贝。

深拷贝:将一个变量赋值给另一个变量时,两个变量最终放置的位置不在同一块内存中,那么就是深拷贝。

三、引用类型实现深拷贝

  在上面的栗子二中,我们可以看出改变其中一个,另外一个就紧跟着改变,但很多时候这不是我们想要的。很明显就是仅仅实现了浅拷贝。

  那么我们怎么解决这个问题呢?也就是如何实现引用类型的深拷贝?

//实现原理
const person = {
    name: '小黑',
    age: 20,
    sex: '公的'
}

const newPerson = {
    name: person.name,
    age: person.age,
    sex: person.sex
}

newPerson.age = 250;
console.log(person.age, newPerson.age);// 20 250

  那我们可以试者封装一个函数:

const person = {
    name: '小黑',
    age: 20,
    sex: '公的'
}

function deepClone(obj) {
    if (typeof obj !== 'object') return;
    let clone = {};
    for (let k in obj) {
        clone[k] = obj[k];
    }
    return clone;
}

const newPerson = deepClone(person);
newPerson.age = 250;
console.log(person.age, newPerson.age);// 20 250

  但是上面的代码只是实现了一层对象的深拷贝,那么要是出现下面这种情况呢?

// 其中family和hobby的键值都保存着的是一个对象
const person = {
    name: '小黑',
    age: 20,
    sex: '公的',
    family: {
        father: '大黑',
        mother: '花花',
        sister: '小花'
    },
    hobby: ['吃饭', '睡觉', '打豆豆']
}

function deepClone(obj) {
    if (typeof obj !== 'object') return;
    const clone = {};
    for (const k in obj) {
        clone[k] = obj[k];
    }
    return clone;
}

const newPerson = deepClone(person);
newPerson.family.sister = '豆豆';

console.log(person.family.sister); // 豆豆

console.log(newPerson.family.sister); // 豆豆

所以,这里就又出问题了,这里是因为 `family` 字段在 `person` 对象中保存的也是一个指针,它本身也是一个引用类型,我们可以通过最简单的方式: `JSON.stringify()`来进行转换:

const newPerson = JSON.parse(JSON.stringify(person));
newPerson.family.sister = '豆豆';

console.log(person.family.sister); // 小花

console.log(newPerson.family.sister); // 豆豆

但是如果我们将 `person` 对象中给它添加一个 `Function` 类型的字段我们来测试以下:

const person = {
    name: '小黑',
    age: 20,
    sex: '公的',
    family: {
        father: '大黑',
        mother: '花花',
        sister: '小花'
    },
    hobby: ['吃饭', '睡觉', '打豆豆'],
    func() {
        console.log(this.name);
    }
}

const newPerson = JSON.parse(JSON.stringify(person));
console.log(newPerson.func);// undefined

  所以,这里就又出问题了,你会发现 `func` 字段输出的并不是一个函数表达式 。`JSON.stringify()` 对 `Function`、`RegExp`、`Date`、`Error`等字段表达式都不会输出,这是因为 `JSON.stringify()` 只能序列化对象的可枚举的自有属性,例如如果对象是由构造函数生成的, 则使用 `JSON.parse(JSON.stringify())` 深拷贝后,会丢弃对象的 `constructor`。

  `JSON.parse(JSON.stringify())` 只能对 Number、String、Boolean、Array 等够被 JSON 直接表示的数据结构进行原样输出。

  那么我们要比较完美的解决这个问题的话,我们可以通过递归赋值,于是就出现了下面这种比较好的解决方案:

const person = {
    name: '小黑',
    age: 20,
    sex: '公的',
    family: {
        father: '大黑',
        mother: '花花',
        sister: '小花'
    },
    hobby: ['吃饭', '睡觉', '打豆豆'],
    func() {
        console.log(this.name);
    }
}

function deepClone(obj, objClone) {
    const isObj = Object.prototype.toString.call(obj);
    if (isObj !== '[object Object]') return;

    const clone = objClone || {};

    for (const [k, v] of Object.entries(obj)) {
        if (typeof v === 'object') {
            clone[k] = v instanceof Array === true ? [] : {};
            arguments.callee(v, clone[k]);
        } else {
            clone[k] = v;
        }
    }

    return clone;
}

const newPerson = deepClone(person);
person.func='hahah';

console.log(newPerson.func);// [Function: func]

  当然上面的递归函数实现深拷贝也会有问题,比如在相互引用对象时会导致死循环,如 `person.family = person`。

四、ES6 中的深拷贝

  当然如果要处理引用类型一层的深拷贝的话,ES6 中提供了多种方式可以实现:

  (1)键值对象

const obj = {
    a: 'aaa',
    b: 'bbb',
    c: 'ccc'
}

//  扩展运算符
const newObj1 = { ...obj };

// Object.assign
const newObj2 = Object.assign({}, obj);

  (2)数组

const arr = [111, 222, 333];

// 扩展运算符
const newArr1 = [...arr];

// Array.prototype.map
const newArr2 = arr.map(item => item);

// Array.prototype.flat
const newArr3 = arr.flat();

// Array.prototype.flatMap
const newArr4 = arr.flatMap(item => item);

 

posted @ 2018-01-23 15:29  Feesir  阅读(198)  评论(0编辑  收藏  举报