JavaScript的拷贝(克隆)问题

涉及的面试题:什么是浅拷贝?如何实现?什么是深拷贝?如何实现

一 数据类型

数据分为基本数据类型(String,Number,Boolean,Null,Undefined,Symbol)和对象数据类型

  • 基本数据类型的特点:直接存储在栈(Stack)中的数据
  • 引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里。

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

深拷贝和浅拷贝只是针对Object和Array这样的引用数据类型的。
浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用
深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”
对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方,另一方也会被改变。通常开发中,我们并不希望出现这样的问题,我们可以是用浅拷贝来解决这个问题
let a = {
    age: 1
}
let b = a
a.age = 2
console.log(b.age)//2
赋值和浅拷贝的区别

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此两个对象是联动。

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性时内存地址(引用类型),拷贝的就是内存地址,因此如果其实一个对象改变了这个地址,就会影响到另一个对象,即默认拷贝构造函数只是对对象进行浅拷贝赋值(逐个成员依次拷贝),即只复制对象空间而不复制资源。

我们先看两个例子,对比赋值和浅拷贝会给原对象带来哪些改变?

//赋值
var obj1 = {
    'name': 'zhangsan',
    'age': '18',
    'language': [1, [2, 3],
        [4, 5]
    ]
};
var obj2 = obj1;
obj2.name = 'list';
obj2.language[1] = ["二", "三"]
console.log('obj1', obj1)
console.log('obj2', obj2)

在这里插入图片描述

var obj1 = {
    'name': 'zhangsan',
    'age': '18',
    'language': [1, [2, 3],
        [4, 5]
    ]
};

var obj3 = Copy(obj1);
obj3.name = 'list';
obj3.language[1] = ['二', '三'];

function Copy(src) {
    let dast = {};
    for (let prop in src) {
        if (src.hasOwnProperty(prop)) {
            dast[prop] = src[prop];
        }
    }
    return dast;
}
console.log('obj1', obj1)
console.log('obj3', obj3)

在这里插入图片描述

上面的例子中,obj1是原始数据,obj2是赋值操作得到的,而obj3是浅拷贝得到的。我们可以清晰看到对原始数据的影响,具体请看下表:
在这里插入图片描述

浅拷贝的实现方式
1 Object.assign()

Object.assign()方法可以把任意多个源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。

var obj = {
    a: {
        name: 'Fanfei',
        age: 22
    }
};

var newobj = Object.assign({}, obj);
newobj.a.name = "Jason";
console.log(obj.a.name) //Jason

注意:如果obj只有一层的时候,是深拷贝

var obj = {
    myName: 'FanFei'
};

var newobj = Object.assign({}, obj);
newobj.myName = "Jason";
console.log(obj) //{ myName: 'FanFei' }
2 运算符…
let a = {
    age: 1
}
let b = {...a
}
a.age = 22
console.log(b.age) //1
3 Array的concat()
let arr = [1, 3, {
    name: 'Fanfei'
}];
let arr2 = arr.concat()
arr2[2].name = "Jason"
console.log(arr) //[ 1, 3, { name: 'Jason' } ]

修改对象会修改原对象的,但是第一层上的基本数据类型修改不会影响

let arr = [1, 3, {
    name: 'Fanfei'
}];
let arr2 = arr.concat()
arr2[1] = 4
console.log(arr) //[ 1, 3, { name: 'Fanfei' } ]
console.log(arr2) //[ 1, 4, { name: 'Fanfei' } ]
4 Array的slice()
let arr = [1, 3, {
    name: 'Fanfei'
}];
let arr2 = arr.slice()
arr2[2].name = 'Jason'
console.log(arr) //[ 1, 3, { name: 'Jason' } ]
console.log(arr2) //[ 1, 3, { name: 'Jason' } ]

修改对象会修改原对象的,但是第一层上的基本数据类型修改不会影响

let arr = [1, 3, {
    name: 'Fanfei'
}];
let arr2 = arr.slice()
arr2[1] = 4
console.log(arr) //[ 1, 3, { name: 'Fanfei' } ]
console.log(arr2) //[ 1, 4, { name: 'Fanfei' } ]
关于Array的slice和concat方法的补充说明:

Array的slice和concat方法不会修改原数组,只会返回一个浅复制原数组的元素的一个新数组

原数组的元素会按照下述规则拷贝:

  • 如果该元素是对象引用(不是实际的对象),slice会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中这个元素也会发生改变.
  • 对于字符串,数字以及布尔值来说(基本类型)slice会拷贝这些值到新的数组,在被的数组里修改这些值,不会影响另一个数组的。
深拷贝的实现方式
1 JSON.parse(JSON.stringify())
let a = {
    age: 1,
    job: {
        first: 'Web'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.job.first = "UI"
console.log(b.job.first) //Web

原理:用JSON.stringify()将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象就产生了,而且对象会开辟新的栈,实现深拷贝.

但是该方法也是有局限性的:

  • 会忽略undefined
  • 会忽略symbol
  • 不能序列化函数
  • 不能解决循环引用的对象

先看下不能处理函数代码:

let a = {
    age: 1,
    job: {
        first: 'Web'
    },
    add: function(params) {
        console.log(params)
    }
}
let b = JSON.parse(JSON.stringify(a))
a.job.first = "UI"
console.log(b) //{ age: 1, job: { first: 'Web' } }

b中压根就没有add这个函数
这是因为JSON.stringify()方法是将一个JavaScript值(对象或者数组)转换为一个JSON字符串,不能接受函数

不能解决循环引用的对象

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

let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

不能解决undefined 或者symbol

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'Fanfei'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) //{ name: 'Fanfei' }

通常情况下,复杂数据都是可以序列化的,但是这个函数可以解决大部分问题。

2 自己递归实现

递归方法实现深度克隆原理:遍历对象,数组直到里面都是基本数据类型,然后再去复制,就是深度拷贝.

第一种
function deepCopy(obj) {
    function isObject(o) {
        return (typeof o === 'object' || typeof o === 'function') && o != null
    }
    if (!isObject(obj)) {
        throw new Error('非对象')
    }
    let isArray = Array.isArray(obj)
    let newObj = isArray ? [...obj] : {...obj
    }
    Reflect.ownKeys(newObj).forEach(key => {
        newObj[key] = isObject(obj[key]) ? deepCopy(obj[key]) : obj[key]
    })
    return newObj
}
let obj = {
    a: [1, 2, 3],
    b: {
        c: 2,
        d: 3
    }
}
let newObj = deepCopy(obj)
newObj.b.c = 1
console.log(obj.b.c)//2
第二种

//定义检测数据类型的功能函数
function checkedType(target) {
    return Object.prototype.toString.call(target).slice(8, -1)
}
//实现深度克隆---对象/数组
function clone(target) {
    //判断拷贝的数据类型
    //初始化变量result成为最终克隆的数据
    let result, targetType = checkedType(target)
    if (targetType === 'object') {
        result = {}
    } else if (targetType === 'Array') {
        result = []
    } else {
        return target
    }
    //遍历目标数据
    for (let i in target) {
        //获取遍历数据结构的每一项值
        let value = target[i]
            //判断目标结构里的每一项值是否存在对象/数组
        if (checkedType(value) === 'object' || checkedType(value) === 'Array')
        //对象/数组嵌套了对象/数组
        {
            //继续遍历获取value值
            result[i] = clone(value)
        } else {
            //获取到value值是基本的数据类型或者是函数
            result[i] = value
        }
    }

    return result
}
函数库lodash



let _ = require('lodash');
let obj2 = {
    a: [1, 2, 3],
    b: {
        c: 2,
        d: 3
    }
}
let obj3 = _.cloneDeep(obj2)
obj3.b.c = 1
console.log(obj2)


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值