浅析JavaScript Clone

  • Post author:
  • Post category:java




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。ECMAScript不支持任何创建自定义类型的机制,就好比C语言的自定义结构体,这是不支持的。

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

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

【例】

        var car = null;
        if (car != null) {
            //操作
        }

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



1.2. let、const


  • const

    :常量是块级作用域,很像使用 let 语句定义的变量。const定义的变量不能重新声明,不能通过赋值改变


  • let

    :语句声明一个

    块级作用域

    的本地变量,并且可选的将其初始化为一个值

        let x = 1;
        if (x === 1) {
            let x = 2;
            console.log(x);
            // expected output: 2
        }
        console.log(x);
        // expected output: 1



1.3. 堆内存、栈内存

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

当变量复制引用类型值的时候,它是一个指针,指向存储在

堆内存

中的对象(堆内存中的对象无法直接访问,要通过这个对象在堆内存中的地址访问,再通过地址去查值(RHS查询,试图获取变量的源值),所以引用类型的值是按引用访问)

变量的值也就是这个指针(我的意思是这个指针是原始值)是存储在栈上的,当变量obj1复制变量的值给变量obj2时,obj1,obj2只是一个保存在栈中的指针,指向同一个存储在堆内存中的对象,所以当通过变量obj1操作堆内存的对象时,obj2也会一起改变

源于别人的博客



2. shallowCopy

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

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

let obj1 = { a: 1 };
let obj2 = obj1;
obj2.a = 2;
console.log(obj1); //{a:2}
console.log(obj2); //{a:2}



2.1. Object.assign()



Object.assign()


方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。如果目标对象中的属性具有相同的键,则这些属性将被源对象中的属性覆盖。(有多个源对象时)后面的源对象的属性将类似地覆盖前面的源对象的属性。该方法只会拷贝

源对象自身的 并且 可枚举的

属性到目标对象。

该方法使用源对象的

[[Get]]

(获得值)和目标对象的

[[Set]]

(设置值)(这两个在上一篇文章讲过),所以它会调用相关

getter



setter

。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用

Object.getOwnPropertyDescriptor(obj, prop)



Object.defineProperty()


  1. Object.getOwnPropertyDescriptor(obj, prop)

    :返回指定对象上一个

    自有属性

    对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)其中obj——需要查找的目标对象;prop——目标对象内属性名称(字符串)

  2. Object.defineProperty(obj, prop, descriptor)

    :方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象;其中obj——要在其上定义属性的对象。prop——要定义或修改的属性的名称。descriptor——将被定义或修改的属性描述符。返回值是被传递给函数的对象。

String类型和 Symbol 类型的属性都会被拷贝。

在出现错误的情况下,例如,如果属性不可写,会引发TypeError,如果在引发错误之前添加了任何属性,则可以更改target对象。

注意,Object.assign 不会在那些source对象值为 null 或 undefined 的时候抛出错误。

        let target = {
            a: 1,
            b: 2
        };
        let source = {
            b: 4,
            c: 5
        };
        let returnedTarget = Object.assign(target, source);
        target.a = 111;
        console.log(target);
        //  output: Object { a: 111, b: 4, c: 5 }
        console.log(returnedTarget);
        //  output: Object { a: 111, b: 4, c: 5 }

输出结果相互影响说明这是一个浅拷贝

  1. 继承属性和不可枚举属性是不能拷贝的
  2. 属性的数据属性/访问器属性
  3. 可以拷贝Symbol类型
  4. 原始类型会被包装为对象
  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", obj1); //obj1 {a: {…}, sym: Symbol(1), innumerable: "不可枚举属性"}
        console.log("obj2", obj2); //obj2 {a: {…}, sym: Symbol(1)}


MDN——Object.defineProperty()


合并对象或者合并具有相同属性的对象

        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. 拓展运算符

        let obj1 = {
            a: 1,
            b: {
                c: 1,
            }
        };
        let obj2 = {
            ...obj1
        };
        obj1.a = 2;
        console.log(obj2); //{a: 1,b:{c: 1,}}
        console.log(obj1.a === obj2.a); //false
        console.log(obj1.b === obj2.b); //true
        // 拓展运算符对基本数据类型直接创建新值,对引用数据类型shallowcopy
        let obj1 = {
            a: 1,
            b: {
                c: 1,
                d: ["a", {
                    e: 1
                }]
            }
        };
        let obj2 = {
            ...obj1
        };
        obj1.b.d[1].e = 2;
        console.log(obj1.a === obj2.a);
        console.log(obj1.b.d[1].e === obj2.b.d[1].e); //true
        // 说明拓展运算符只适用于对基本数据类型的值进行shallowcopy

扩展运算符Object.assign()有同样的缺陷,对于值是对象的属性无法完全拷贝成2个不同对象(只是拷贝一份引用),但是如果属性都是基本类型的值的话,使用扩展运算符更加方便。



2.3. Array.prototype.slice()


MDN——Array.prototype.slice()


slice()

方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(

包括 begin,不包括end

)。原始数组不会被改变。


语法



arr.slice([begin[, end]])

也就是说只写一个参数的时候是begin,若省略两个参数将会从头到尾索引


  • begin

    (可选);提取起始处的索引(从 0 开始),从该索引开始提取原数组元素。

    如果该参数为负数,则表示从原数组中的倒数第几个元素开始提取,

    slice(-2)

    表示提取原数组中的倒数第二个元素到最后一个元素(包含最后一个元素)。如果省略

    begin

    ,则

    slice

    从索引 0 开始。如果

    begin

    大于原数组的长度,则会返回空数组。

  • end

    (可选):提取终止处的索引(从 0 开始),在该索引处结束提取原数组元素。

    slice

    会提取原数组中索引从

    begin



    end

    的所有元素(包含

    begin

    ,但不包含

    end

    )。

    slice(1,4)

    会提取原数组中从第二个元素开始一直到第四个元素的所有元素 (索引为 1, 2, 3的元素)。如果该参数为负数, 则它表示在原数组中的倒数第几个元素结束抽取。

    slice(-2,-1)

    表示抽取了原数组中的倒数第二个元素到最后一个元素(不包含最后一个元素,也就是只有倒数第二个元素)。如果 end 被省略,则

    slice

    会一直提取到原数组末尾。如果 end 大于数组的长度,

    slice

    也会一直提取到原数组末尾。
  • 返回值:一个含有被提取元素的新数组


描述

:slice 不会修改原数组,只会返回一个shallowCopy了原数组中的元素的一个新数组。原数组的元素会按照下述规则拷贝:如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。如果向两个数组任一中添加了新元素,则另一个不会受到影响。



2.4.

Array.prototype.concat()


语法



var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])

valueN可选将数组和/或值连接成新数组。如果省略了valueN参数参数,则concat会返回一个它所调用的已存在的数组的浅拷贝。 返回值:新的 Array 实例。

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)
        console.log(new_array); //["Hui", {name: "Dong"}, 1, 2, 3, 4, 5, 6, 7]



2.5.

Array.from()

Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

        console.log(Array.from('foo'));
        // expected output: Array ["f", "o", "o"]A
        Array.from(('Tian'), x => x + "1")
        // expected return: (4) ["T1", "i1", "a1", "n1"]



3. deepClone



3.1. JSON实现

可以先了解

JSON.stringify()

(将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串)和

JSON.parse()

(解析JSON字符串,构造由字符串描述的JavaScript值或对象)

简而言之就是先使用stringify将对象转为JSON字符串再使用parse解析成对象

        let obj1 = {
            a: 0,
            b: {
                c: 0
            }
        }
        let obj2 = Object.assign({}, obj1);
        console.log(JSON.stringify(obj2)); //{"a":0,"b":{"c":0}}

        obj1.a = 1;
        console.log(JSON.stringify(obj1)); //{"a":1,"b":{"c":0}}
        console.log(JSON.stringify(obj2)); //{"a":0,"b":{"c":0}}

        obj2.a = 2;
        console.log(JSON.stringify(obj1)); //{"a":1,"b":{"c":0}}
        console.log(JSON.stringify(obj2)); //{"a":2,"b":{"c":0}}

        obj2.b.c = 3;
        console.log(JSON.stringify(obj1)); //{"a":1,"b":{"c":3}}
        console.log(JSON.stringify(obj2)); //{"a":2,"b":{"c":3}}

        // DeepClone
        obj1 = {
            a: 0,
            b: {
                c: 0
            }
        }
        let obj3 = JSON.parse(JSON.stringify(obj1));

        console.log(obj3.a === obj1.a); //true
        console.log(obj3.b === obj1.b); //true
        console.log(obj3 === obj1); //false

        obj1.a = 4;
        obj1.b.c = 4;
        console.log(JSON.stringify(obj3)); //{"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型的数据

        let obj1 = {
            a: {
                b: 1
            }
        }

        function deepClone(obj) {
            let cloneObj = {}; //在堆内存中新建一个对象
            for (let key in obj) { //遍历参数的键
                if (typeof obj[key] === 'object') {
                    cloneObj[key] = deepClone(obj[key]) //值是对象就再次调用函数
                } else {
                    cloneObj[key] = obj[key] //基本类型直接复制值
                }
            }
            return cloneObj
        }
        let obj2 = deepClone(obj1);
        obj1.a.b = 2;
        console.log(obj2); //{a:{b:1}}



3.3. 第三方库实现


语法1



jQuery.extend( target [, object1 ] [, objectN ] )



target

→类型: Object,如果附加的对象被传递给这个方法将那么它将接收新的属性,如果它是唯一的参数将扩展jQuery的命名空间。


object1

→类型: Object,它包含额外的属性合并到第一个参数


objectN

→类型: Object,包含额外的属性合并到第一个参数


语法2



jQuery.extend( [deep ], target, object1 [, objectN ] )



deep

→类型: Boolean,如果是true,合并成为递归(又叫做深拷贝)。


target

→类型: Object,对象扩展。这将接收新的属性。


object1

→类型: Object,一个对象,它包含额外的属性合并到第一个参数.


objectN

→类型: Object,包含额外的属性合并到第一个参数



3.4. Structured Clone 结构化克隆算法

参考:

jessezhao1990→JavaScript 深拷贝

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



4. 关于上一篇文章——Functions Setter

参考:

MDN解释

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

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

    默认为 undefined

语法:

{set prop(val) { . . . }}



{set [expression](val) { . . . }}

prop:要绑定到给定函数的属性名。

val:用于保存尝试分配给prop的值的变量的一个别名。

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

        const language = { //定义language为一个对象
            set current(name) { //我认为current就像一个函数,或者说language的一个方法
                this.log.push(name); //把name推到log数组中去
                console.log(name);
            },
            log: [], //是个数组
        };
        language.current = "EN";
        language.current = "FA";
        console.log(language.log); //["EN", "FA"]
        console.log(language.name); //undefined,为什么?



5. 附加


  1. map()

    方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

  2. 箭头函数表达式

    的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

  3. Promise

    对象用于表示一个异步操作的最终完成 (或失败), 及其结果值。
  4. 三目运算:

    var num = 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就没有了联系。文中给出了部分方法并且考虑不全,对于一些特殊的场景还需重新考虑。



版权声明:本文为qq_42842786原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。