浅析JavaScript Clone

430 阅读9分钟

1. JavaScript数据类型

现在的ECMAScript有7种基本数据类型和引用数据类型。参考:JavaScript 数据类型和数据结构

7 种原始类型:

  1. Boolean
  2. Null
  3. Undefined
  4. Number
  5. BigInt
  6. String
  7. Symbol

和引用数据类型

  1. Object

这里只写了一种,但是还有很多其他也是引用数据类型,比如Array、Function、Date、RegExp、Error。

ES6增加了一种基本数据类型Symbol,数据类型 “symbol” 是一种原始数据类型,该类型的性质在于这个类型的值可以用来创建匿名的对象属性。该数据类型通常被用作一个对象属性的键值——当你想让它是私有的时候,详见 MDN——symbol

BigInt数据类型,参考 MDN——BigInt,可对原本超出数值范围的值进行运算。

1.1. typeof返回值

返回八种数据类型,参考:MDN——typeof

null会返回object,function会返回function

返回值(字符串)
"undefined"
"object"
"boolean"
"number"
"bigint"
"string"
"symbol"
"function"

注:强调typeof是一个操作符而非函数,括号可略,null之所以会返回object是因为null最初是作为空对象的占位符的使用的,被认为是空对象的引用。

实际上undefined值派生自null值,所以undefined == null //true

构造函数的括号也可省略:console.log(new Number);// [Number: 0]

如果定义的变量将来用于保存对象,那么最好将该变量初始化为null,这样只要检查null值就可以知道相应的变量是否已经保存了一个对象的引用。

注:尽管null和undefined有特殊关系,但他们完全不同,任何情况都没有必要把一个变量值显式地设置为undefined,但对null并不适用,只要意在保存对象的变量还没有真正保存对象,就应该明确保存变量为null值。这样不仅体现null作为空对象指针的惯例,也有助于进一步区分null和undefined。

1.2. 堆内存、栈内存

https://ythdong.gitee.io/blog_image/JavaScript/堆栈.jpg

当变量复制引用类型值的时候,它是一个指针,指向存储在堆内存中的对象(堆内存中的对象无法直接访问,要通过这个对象在堆内存中的地址访问,再通过地址去查值(RHS查询,试图获取变量的源值),所以引用类型的值是按引用访问)
变量的值也就是这个指针(我的意思是这个指针是原始值)是存储在栈上的,当变量obj1复制变量的值给变量obj2时,obj1、obj2只是一个保存在栈中的指针,指向同一个存储在堆内存中的对象,所以当通过变量obj1操作堆内存的对象时,obj2也会一起改变

源于别人的博客

2. shallowCopy

曾经在面试中遇到过这个问题,面试官问我深拷 贝与浅拷贝的区别。

基本数据类型并没有深浅拷贝之分。但引用类型数据的浅拷贝会创建一个新对象,它有着被拷贝对象属性值的一份精确拷贝。拷贝的是内存地址,所以其中一个值的变化会在另一个上面反映出来。

下面实际中浅拷贝的方法。

2.1. Object.assign()

  • 具有相同键时后面的会覆盖前面的;
  • 只会拷贝 源对象自身的 且 可枚举的 属性到目标对象;
  • (接上一句)所以继承属性、不可枚举属性不能拷贝;
  • String、Symbol 类型的属性都会被拷贝;
  • 原始类型会被包装为对象;
  • 异常会打断后续拷贝任务(例如属性不可写,会引发 TypeError)。

该方法使用源对象的[[Get]](获得值)和目标对象的[[Set]](设置值),所以它会调用相关 gettersetter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含 getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用 Object.getOwnPropertyDescriptor(obj, prop)Object.defineProperty()

  1. Object.getOwnPropertyDescriptor(obj, prop):返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)其中obj——需要查找的目标对象;prop——目标对象内属性名称(字符串)
  2. Object.defineProperty(obj, prop, descriptor):方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象;其中obj——要在其上定义属性的对象。prop——要定义或修改的属性的名称。descriptor——将被定义或修改的属性描述符。返回值是被传递给函数的对象。
    let target = {
        a: 1,
        b: 2
    };
    let source = {
        b: 4,
        c: 5
    };
    let returnedTarget = Object.assign(target, source);
    target.a = 111;
    console.log(target);// { a: 111, b: 4, c: 5 }
    console.log(returnedTarget);// { a: 111, b: 4, c: 5 }
    let obj1 = {
        a: {
            b: 1
        },
        sym: Symbol(1)
    }
    Object.defineProperty(obj1, 'innumerable', {
        value: '不可枚举属性',
        enumerable: false
    })
    let obj2 = {};
    Object.assign(obj2, obj1);
    obj1.a.b = 2;
    console.log(obj1); // {a: {…}, sym: Symbol(1), innumerable: "不可枚举属性"}
    console.log(obj2); // {a: {…}, sym: Symbol(1)}
    const o1 = {
        a: 1
    }
    const o2 = {
        a: 2
    }
    const o3 = {
        a: 3
    }
    const obj = Object.assign(o1, o2, o3);
    console.log(obj); //{a: 3}
    console.log(o1); //{a: 3}
    const o4 = {
        a: 32
    };
    const obj2 = Object.assign(o1, o2, o3, o4);
    console.log(obj2); //{a: 32}

2.2. 拓展运算符

拓展运算符对基本数据类型直接创建新值,对引用数据类型shallowcopy。

    let obj = { a: 1, b: { c: 1 } }
    let obj2 = { ...obj }
    obj.a = 2
    obj.b.c = 2
    console.log(obj)  // { a: 2, b: { c: 2 } }
    console.log(obj2);// { a: 1, b: { c: 2 } }

扩展运算符Object.assign()有同样的缺陷,对于引用数据类型只是shallowCopy。 但是处理的数据都是基本类型的值的话挺方便。

2.3. Array的slice()

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

语法arr.slice([begin[, end]])。若省略两个参数将会从头到尾索引

slice 不会修改原数组,只会返回一个shallowCopy的新数组。对于字符串、数字及布尔值来说(区别于 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。对于数组元素是引用数据类型,仍是浅拷贝。

    let arr1 = [1, "hui", [2], { any: "any" }];
    let arr2 = arr1.slice();
    arr1[0] = 2;
    arr1[1] = "HUI";
    arr1[2][0] = 3;
    arr1[3].any = "ANY";
    console.log(arr1);
    console.log(arr2);

2.4. Array的concat()

old_array.concat(value1[, value2[, ...[, valueN]]])。数组和或值将被连接成新数组。如果省略参数,则concat会返回一个它所调用的已存在的数组的浅拷贝。 concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

    let old_array = ["Hui", {
        name: "Dong"
    }];
    let arr1 = [1, 2, 3];
    let arr2 = [4, 5];
    let valueN = [6, 7];
    let new_array = old_array.concat(arr1, arr2, ...valueN)
    old_array[1].name = "Hui";
    console.log(old_array); // [ 'Hui', { name: 'Hui' } ]
    console.log(new_array); 
    // [ 'Hui', { name: 'Dong' }, 1, 2, 3, 4, 5, 6, 7 ]

2.5. Array.from()

从一个类数组或可迭代对象创建一个新的,浅拷贝的数组实例。

    console.log(Array.from('foo'));
    // [ 'f', 'o', 'o' ]
    console.log(Array.from(('Tian'), x => x + "1"));
    // [ 'T1', 'i1', 'a1', 'n1' ]
    const set = new Set(['foo', 'bar', 'baz', 'foo']);
    console.log(Array.from(set));
    // [ 'foo', 'bar', 'baz' ]
    const map = new Map([[1, 2], [2, 4], [4, 8]]);
    console.log(Array.from(map));
    // [[1, 2], [2, 4], [4, 8]]

    const mapper = new Map([['1', 'a'], ['2', 'b']]);
    console.log(Array.from(mapper.values()));
    // ['a', 'b'];
    console.log(Array.from(mapper.keys()));
    // ['1', '2'];

2.6. 实现浅拷贝

代码参考 kaiwu.lagou.com/course/cour…

    const shallowCopy = (target) => {
        if (typeof target === 'object' && target !== null) {
            const cloneTarget = Array.isArray(target) ? [] : {};
            for (let prop in target) {
                if (target.hasOwnProperty(prop)) {
                    cloneTarget[prop] = target[prop];
                }
            }
            return cloneTarget;
        } else {
            return target;
        }
    }

3. deepClone

3.1. JSON

JSON.stringify() (序列化)和 JSON.parse() (解析)。

    old_obj = {
        a: 0,
        b: {
            c: 0
        }
    }
    let new_obj = JSON.parse(JSON.stringify(old_obj));
    old_obj.a = 4;
    old_obj.b.c = 4;
    console.log(old_obj); // { a: 4, b: { c: 4 } }
    console.log(new_obj); // { a: 0, b: { c: 0 } }

3.2. 递归实现

两个方法,第二个源于yeyan1996的博客

    function deepClone (origin, target1) {
        let target2 = target1 || {};
        for (let prop in origin) {
            if (origin.hasOwnProperty(prop)) {
                if (origin[prop] !== 'null' && typeof (origin[prop]) == 'object') {
                    if (
                        Object.prototype.toString.call(origin[prop]) == '[object Array]'
                    ) {
                        target2[prop] = [];
                    } else {
                        target2[prop] = {};
                    }
                    deepClone(origin[prop], target2[prop]);
                } else {
                    target2[prop] = origin[prop];
                }
            }
        }
        return target2;
    }

注意其中传target时,必须是个object型的数据。

3.3. jQuery、lodash

3.4. MessageChannel

参考:

    function structuralClone(obj) {
        return new Promise(resolve => {
            const {
                port1,
                port2
            } = new MessageChannel();
            port2.onmessage = ev => resolve(ev.data);
            port1.postMessage(obj);
        })
    }
    let obj = {
        per: {
            name: '田甜'
        },
        fun: ['HFUT', '电信']
    };
    structuralClone(obj).then(res => {
        obj.per.name = "哈哈";
        obj.fun[1] = "YAMA";
        console.log(res);
    })
    console.log(obj);

4. 关于 Setter

当尝试设置属性时,set语法将对象属性绑定到要调用的函数。

  • get一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为 undefined。
  • set一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined

表达式:从 ES6 开始,还可以使用一个计算属性名的表达式绑定到给定的函数。

    const language = {
        set current (name) { //我认为current就像一个函数,或者说language的一个方法
            this.log.push(name);
            console.log(name);
        },
        log: [],
    };
    language.current = "EN";
    language.current = "FA";
    console.log(language.log); //["EN", "FA"]

5. 附加

  1. map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
  2. 箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。在箭头函数中this的值取决于它的上一层非箭头函数的
  3. 三目运算:console.log(1 > 0 ? ("10" > "9" ? 1 : 0) : 2); // 输出0。数字字符串与数字比会转换为数字,但是字符串与字符串比会从左到右逐位相比,首先1就不大于9,所以输出0,再举个例子:"21">"199"//输出true因为2先和1比直接就true了。结合三目运算可以简化上述自己实现的克隆方法,如果把Object.prototype.toString这样的方法用变量来代替会更加简洁。
    function deepClone (origin, target) {
        var target = target || {};
        for (var prop in origin) {
            if (origin.hasOwnProperty(prop)) {
                origin[prop] !== 'null' && typeof (origin[prop]) == 'object' ? (Object.prototype.toString.call(
                    origin[prop]) ==
                    '[object Array]' ? target[prop] = [] : target[prop] = {}, deepClone(origin[prop], target[
                        prop])) : target[
                        prop] = origin[prop];
            }
        }
        return target;
    }

6. Clone总结

基本数据类型没有深浅拷贝拷贝之分,对于引用数据类型,浅拷贝指的是拷贝引用,所以拷贝之后会有两个对象同时指向一个内存。

深拷贝则是完全复制其相应的键和值,拷贝之后两个object就没有了联系。文中给出了部分方法并且考虑不全,对于一些特殊的场景还需重新考虑。