JavaScript数据类型AND深拷贝和浅拷贝的不归路

380 阅读6分钟

本文将向读者介绍在js中数据的拷贝,旨在让读者能在工作中或者面试中遇到相似的问题能够应用起来

首先声明一下,这批文章仅代表我自己对js的一些理解。由于笔者水平有限,如果有出错之出,还望多多谅解,指正。谢谢。当然如果你看的不爽...那么---------你顺着网线过来打我呀O(∩_∩)O哈哈~

正文开始------------------

数据类型

说到js的数据拷贝就不得不提js的数据类型。我们知道的7中数据类型:
Number、String、Boolean、Undefined、Null、Object 和es6新增的Symbol

(null undefined number boolean )都是基本类型,存栈里的数据

Null: 不存在的,没有的数据对象

Undefined: 变量声明了却没有初始化的数据。表示"缺少值",就是此处应该有一个值,但是还没有定义。

number: 顾名思义就是一个数字类型,number是存放在栈里的数据

string:string比较特殊,它其实是存在堆里的,我们拿到的只是一个地址引用。如果对js比较了解的话,那么会知道

js中string是不可变的,我们没有任何一个方法可以改变一个字符串。可以认为string是 行为与基本类型相似的不可变引用类型

Object 是引用类型,存堆里的地址

Objecty下面又有三员大将,它们都是 Object.prototype 下的熟悉

Array: 一个存放数据的集合,js中 数组可以存放任何数据。

Function: 函数,其实也是数据。我们可以把一个函数赋值给另一个变量

object: 对象,没啥好说的。

Symbol的话是一个es6新增对象,用Symbol可以创建唯一的变量名。

这个array有点意思,在有的语言数组就是数组,但是js里它确实对象,数组的key就是它的下标

 // instanceof 是判断xxx 是否xxx的实例
    Object instanceof Object // true
    Array instanceof Object // true
    Function instanceof Object // true

接下来重点来了,请大家看一段代码:

    Number.constructor // ƒ Function() { [native code] }
    String.constructor // ƒ Function() { [native code] }
    Boolean.constructor // ƒ Function() { [native code] }
    Object.constructor // ƒ Function() { [native code] }
    Symbol.constructor //ƒ Function() { [native code] }

大家看到这个有陷入深深的沉思吗?为啥他们都有 constructor,为啥他们都有方法可以去调用。而且如果用 刚刚这个 instanceof 去判断会发现,string、boolean、number...都是object 的实例。

那么你知道是为什么吗?

你没猜错。在js中所以数据都是对象。all in of object。这个英文怕不怕。
接下来再看下面的代码:

    Number.prototype.six = () => { console.log(666) }
    const num = 123
    num.six() // 666

这里能给number原型添加一个方法,并且定义的一个number 可以去掉用这个方法。已经说明了js中number 其实是基于对象。
string、Boolean 等都是如此
顺便解释一下js堆和栈的一点概念
栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。

拷贝

js的拷贝是有点复杂的,涉及到引用类型的话

    let a = 1
    let b = '2'
    a = b
    a = 3
    console.log(a, b) // 3 2

这是js中基本类型的复制,没有任何问题,我们就不多扯了。就是覆盖就好。

我们来看看js中引用类型的拷贝

    let obj = {
        a: 1,
        b: {
            b: 1
        }
    }
    let arr = [1, 2, 3]
    let arr2 = arr
    let obj2 = obj
    
    arr[1] = '666'
    obj.a = '777'
    console.log(obj.a) // 666
    console.log(obj2.a) // 666
    console.log(arr[1]) // 777
    console.log(arr2[1]) // 777

这就是引用类型的尿性,有的时候很烦,有的时候却很有用。因为js中引用类型给你访问的是一个地址。这个地址对应着数据的位置,我们像复制普通类型一样复制引用类型,就等于直接把地址给了别人。改动的时候大家改的其实是同一个数据。
那么问题来了,我就是复制一个引用类型的所以数据,但是我不想被我复制的对象收到影响怎么办。

浅拷贝

    let obj = {
        a: 1,
        b: {
            b: 1
        }
    }
    let arr = [1, 2, 3]
    let obj2 = { ...obj }
    let arr2 = [ ...arr ]
    obj2.a = '666'
    arr2[1] = '777'
    console.log(obj.a) // 1
    console.log(obj2.a) // 666
    console.log(arr[1]) // 1
    console.log(arr2[1]) // 777

好像没问题了,似乎很简单的样子啊。
我们接着上面的代码

    ...
    obj2.b.b = '888'
    console.log(obj.b.b) // { b: '888' } 
    console.log(obj2.b.b) // { b: '888' }

震惊!似乎又出现了刚刚的问题。没错这就是因为 ...是es6的扩展运算符。他只是拷贝了第一层变量。后面的依然还是直接复制地址。

深拷贝

先来一个简单的,百度一搜索一大堆的看看

function clone(params) {
    var obj = {};
    for(var i in params) {
        if (params.hasOwnProperty(i)) {
            if (typeof params[i] === 'object') {
                obj[i] = clone(params[i]); // 通过判断是否对象而进行递归
            } else {
                obj[i] = params[i];
            }
        }
    }
    return obj;
}
let obj = {
    a: 1,
    b: {
        b: 1
    }
}
let obj2 = clone(obj)
obj2.b.b = '888'
console.log(obj.b.b, obj2.b.b) // 1  888

这就实现了一个简单的深拷贝,但是它有一些问题。主要就是考虑的不够严谨,比如一些数据没有做到兼容。比如set、 map、weakset、weakmap、array... 是不是感觉很麻烦。当然其实我们有个简单的方法。

function clone2(params) {
    return JSON.parse(JSON.stringify(params));
}

平时我一般工作中拷贝一些json类型的数据就用这个....简单粗暴。
这个方法其实也有一些问题。就是没法克隆 函数和正则匹配等.当然如果只是简单的数据还是可以的。

这个方法是我在前端早读课中的一篇文章看到的一个方法。这里就厚颜无耻的clone了下来,如果大家有兴趣的话可以去关注前端早读课。看那篇关于js对象拷贝的文章。

function cloneForce(x) {
    // =============
    const uniqueList = []; // 用来去重
    // =============

    let root = {};

    // 循环数组
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 深度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 当第一次循环的时候key是undefined,所以靠谱到第一级,后面的都是拷贝到子级
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }

        // =============
        let uniqueData = find(uniqueList, data);
        if (uniqueData) {
            parent[key] = uniqueData.target;
            continue; // 判断数据是否存在,如果存在就不继续这次循环了
        }

        // 数据不存在
        // 保存源数据,在拷贝数据中对应的引用
        uniqueList.push({
            source: data,
            target: res,
        });
        // =============

        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 如果有object 就push禁 loopList,进行下一次转换
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}
function find(arr, item) {
    for(let i = 0; i < arr.length; i++) {
        if (arr[i].source === item) {
            return arr[i];
        }
    }

    return null;
}

最后在向大家介绍一种神奇的方法。

let obj = {a:1, b: {b:1}}
var channel = new MessageChannel();
        var port1 = channel.port1;
        var port2 = channel.port2;
        port1.onmessage = function(event) {
            let obj2 =  event.data
			obj2.b.b = '666'
			console.log(obj.b.b,obj2.b.b) // 1  666
        }
        port2.onmessage = function(event) {
            console.log("port2收到来自port1的数据:" + event.data);
        }

        port2.postMessage(obj);

是不是很神奇。不过这个也无法解决对象循环引用的问题。并且它是异步的。