この記事では、JavaScript のディープ コピーについて詳しく説明します。

青灯夜游
リリース: 2022-10-21 19:37:55
転載
2198 人が閲覧しました

この記事では、JavaScript のディープ コピーについて詳しく説明します。

ディープコピーに関する記事はインターネット上にたくさんありますが、質はさまざまで、よく考えられていないものも多く、書き方も比較的雑で満足のいくものではありません。この記事は 完璧なディープコピー を完成させることを目的としています。読んだ後に質問がある場合は、自由に追加および改善してください。

ディープ コピーが完了したかどうかを評価するには、次の質問が実装されているかどうかを確認してください:

  • 基本的な型データを実行できるかどうかコピーされる?

  • キーと値はどちらも基本的な型です通常のオブジェクトコピーできますか?

  • #シンボルオブジェクトのキーはコピーできますか?

  • Date および RegExp オブジェクト タイプはコピーできますか?

  • Map および Set オブジェクト タイプはコピーできますか?

  • #関数

    オブジェクト タイプはコピーできますか? (通常、関数にはディープ コピーを使用しません)

  • オブジェクトの
  • プロトタイプ

    はコピーできますか?

  • 列挙不可能なプロパティ

    コピーできますか?

  • 循環参照

    コピーできますか? ##################どうやって?あなたが書いた詳細なコピーは十分に完璧ですか?

ディープ コピーの最終実装知りたい人の便宜のために、最終コード バージョンをここに直接示します。もちろん、段階的に理解したい場合は、引き続き記事の残りの部分を参照してください:

function deepClone(target) {
    const map = new WeakMap()
    
    function isObject(target) {
        return (typeof target === 'この記事では、JavaScript のディープ コピーについて詳しく説明します。ect' && target ) || typeof target === 'function'
    }

    function clone(data) {
        if (!isObject(data)) {
            return data
        }
        if ([Date, RegExp].includes(data.constructor)) {
            return new data.constructor(data)
        }
        if (typeof data === 'function') {
            return new Function('return ' + data.toString())()
        }
        const exist = map.get(data)
        if (exist) {
            return exist
        }
        if (data instanceof Map) {
            const result = new Map()
            map.set(data, result)
            data.forEach((val, key) => {
                if (isObject(val)) {
                    result.set(key, clone(val))
                } else {
                    result.set(key, val)
                }
            })
            return result
        }
        if (data instanceof Set) {
            const result = new Set()
            map.set(data, result)
            data.forEach(val => {
                if (isObject(val)) {
                    result.add(clone(val))
                } else {
                    result.add(val)
                }
            })
            return result
        }
        const keys = Reflect.ownKeys(data)
        const allDesc = Object.getOwnPropertyDescriptors(data)
        const result = Object.create(Object.getPrototypeOf(data), allDesc)
        map.set(data, result)
        keys.forEach(key => {
            const val = data[key]
            if (isObject(val)) {
                result[key] = clone(val)
            } else {
                result[key] = val
            }
        })
        return result
    }

    return clone(target)
}
ログイン後にコピー

1. JavaScript データ型のコピー原則

まず、JS データ型の図を見てください (

Object

を除き、他はすべて基本型です):

JavaScript では、基本型の値をコピーします。新しい同一のデータを直接コピーします。2 つのデータは相互に排他的で、独立しており、互いに影響しません。参照型の値(オブジェクト型)のコピーとは、オブジェクトの参照(つまり、オブジェクトが配置されているメモリアドレス、つまりオブジェクトへのポインタ)を渡すことであり、これは複数の変数を指すことに相当します。変数の 1 つがそのオブジェクトへの参照を持っている限り、変更されると、他の変数が指すオブジェクトも変更されます (これらの変数は同じオブジェクトを指しているため)。以下に示すように:

2. 深くて浅いコピー
この記事では、JavaScript のディープ コピーについて詳しく説明します。
深くて浅いコピーは、主にオブジェクト タイプと基本の値をターゲットとします。タイプ自体はまったく同じにコピーされます。コピーは 1 つで、暗いコピーと明るいコピーの区別はありません。ここでは、まずテスト用のコピー オブジェクトを提供します。この
この記事では、JavaScript のディープ コピーについて詳しく説明します。この記事では、JavaScript のディープ コピーについて詳しく説明します。 オブジェクトを使用して、作成したディープ コピー関数が完璧かどうかをテストできます:

// 测试的この記事では、JavaScript のディープ コピーについて詳しく説明します。对象
const この記事では、JavaScript のディープ コピーについて詳しく説明します。 = {
    // =========== 1.基础数据类型 ===========
    num: 0, // number
    str: '', // string
    bool: true, // boolean
    unf: undefined, // undefined
    nul: null, // null
    sym: Symbol('sym'), // symbol
    bign: BigInt(1n), // bigint

    // =========== 2.Object类型 ===========
    // 普通对象
    この記事では、JavaScript のディープ コピーについて詳しく説明します。: {
        name: '我是一个对象',
        id: 1
    },
    // 数组
    arr: [0, 1, 2],
    // 函数
    func: function () {
        console.log('我是一个函数')
    },
    // 日期
    date: new Date(0),
    // 正则
    reg: new RegExp('/我是一个正则/ig'),
    // Map
    map: new Map().set('mapKey', 1),
    // Set
    set: new Set().add('set'),
    // =========== 3.其他 ===========
    [Symbol('1')]: 1  // Symbol作为key
};

// 4.添加不可枚举属性
Object.defineProperty(この記事では、JavaScript のディープ コピーについて詳しく説明します。, 'innumerable', {
    enumerable: false,
    value: '不可枚举属性'
});

// 5.设置原型对象
Object.setPrototypeOf(この記事では、JavaScript のディープ コピーについて詳しく説明します。, {
    proto: 'proto'
})

// 6.设置loop成循环引用的属性
この記事では、JavaScript のディープ コピーについて詳しく説明します。.loop = この記事では、JavaScript のディープ コピーについて詳しく説明します。
ログイン後にコピー

この記事では、JavaScript のディープ コピーについて詳しく説明します。 オブジェクトChrome の結果にあります:

##2.1 浅いコピー

浅いコピー

: 受け入れる新しいオブジェクトを作成します再コピーまたは参照するオブジェクト値。オブジェクト属性が基本データ型の場合は、基本型の値が新しいオブジェクトにコピーされますが、属性が参照データ型の場合は、メモリ内のアドレスがコピーされます。オブジェクトの 1 つがポイントするアドレスを変更すると、そのアドレスが変更されます。記憶によれば、オブジェクトは間違いなく別のオブジェクトに影響を与えます。 この記事では、JavaScript のディープ コピーについて詳しく説明します。

まず、いくつかの浅いコピー メソッドを見てみましょう (詳細については、対応するメソッドへのハイパーリンクをクリックしてください):

メソッドメソッドを使用する

注意事項

Object.assign(target, .. .sources)説明: 1 つ以上のソース オブジェクトからターゲット オブジェクトにすべての列挙可能なプロパティの値を割り当てるために使用されます。ターゲットオブジェクトを返します。 1. オブジェクトの継承されたプロパティはコピーされません; 2. オブジェクトの非列挙プロパティはコピーされません; 3. シンボル タイプのプロパティはコピーできます。 let この記事では、JavaScript のディープ コピーについて詳しく説明します。Clone = { ...この記事では、JavaScript のディープ コピーについて詳しく説明します。 };Object.assign () ほぼ同じですが、属性がすべて基本型の値である場合、スプレッド演算子を使用して浅いコピーを実行する方が便利です。 const new_array = old_array.concat(value1[, value2[, ...[, valueN] ]])Array.prototype.slice() 配列のコピー浅いコピー、基本型値の配列に適しています

这里只列举了常用的几种方式,除此之外当然还有其他更多的方式。注意,我们直接使用=赋值不是浅拷贝,因为它是直接指向同一个对象了,并没有返回一个新对象。

手动实现一个浅拷贝:

function shallowClone(target) {
    if (typeof target === 'この記事では、JavaScript のディープ コピーについて詳しく説明します。ect' && 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;
    }
}


// 测试
const shallowCloneObj = shallowClone(この記事では、JavaScript のディープ コピーについて詳しく説明します。)

shallowCloneObj === この記事では、JavaScript のディープ コピーについて詳しく説明します。  // false,返回的是一个新对象
shallowCloneObj.arr === この記事では、JavaScript のディープ コピーについて詳しく説明します。.arr  // true,对于对象类型只拷贝了引用
ログイン後にコピー

从上面这段代码可以看出,利用类型判断(查看typeof),针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性(for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性,包含原型上的属性。查看for…in),基本就可以手工实现一个浅拷贝的代码了。

2.2 深拷贝

深拷贝:创建一个新的对象,将一个对象从内存中完整地拷贝出来一份给该新对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。

看看现存的一些深拷贝的方法:

方法1:JSON.stringify()

JSON.stringfy() 其实就是将一个 JavaScript 对象或值转换为 JSON 字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。(点这了解:JSON.stringfy()、JSON.parse())

使用如下:

function deepClone(target) {
    if (typeof target === 'この記事では、JavaScript のディープ コピーについて詳しく説明します。ect' && target !== null) {
        return JSON.parse(JSON.stringify(target));
    } else {
        return target;
    }
}

// 开头的测试この記事では、JavaScript のディープ コピーについて詳しく説明します。存在BigInt类型、循环引用,JSON.stringfy()执行会报错,所以除去这两个条件进行测试
const clonedObj = deepClone(この記事では、JavaScript のディープ コピーについて詳しく説明します。)

// 测试
clonedObj === この記事では、JavaScript のディープ コピーについて詳しく説明します。  // false,返回的是一个新对象
clonedObj.arr === この記事では、JavaScript のディープ コピーについて詳しく説明します。.arr  // false,说明拷贝的不是引用
ログイン後にコピー

浏览器执行结果:

この記事では、JavaScript のディープ コピーについて詳しく説明します。
从以上结果我们可知JSON.stringfy() 存在以下一些问题:

  • 执行会报错:存在BigInt类型、循环引用。

  • 拷贝Date引用类型会变成字符串。

  • 键值会消失:对象的值中为FunctionUndefinedSymbol 这几种类型,。

  • 键值变成空对象:对象的值中为MapSetRegExp这几种类型。

  • 无法拷贝:不可枚举属性、对象的原型链。

  • 补充:其他更详细的内容请查看官方文档:JSON.stringify()

由于以上种种限制条件,JSON.stringfy() 方式仅限于深拷贝一些普通的对象,对于更复杂的数据类型,我们需要另寻他路。

方法2:递归基础版深拷贝

手动递归实现深拷贝,我们只需要完成以下2点即可:

  • 对于基础类型,我们只需要简单地赋值即可(使用=)。

  • 对于引用类型,我们需要创建新的对象,并通过遍历键来赋值对应的值,这个过程中如果遇到 Object 类型还需要再次进行遍历。

function deepClone(target) {
    if (typeof target === 'この記事では、JavaScript のディープ コピーについて詳しく説明します。ect' && target) {
        let cloneObj = {}
        for (const key in target) { // 遍历
            const val = target[key]
            if (typeof val === 'この記事では、JavaScript のディープ コピーについて詳しく説明します。ect' && val) {
                cloneObj[key] = deepClone(val) // 是对象就再次调用该函数递归
            } else {
                cloneObj[key] = val // 基本类型的话直接复制值
            }
        }
        return cloneObj
    } else {
        return target;
    }
}

// 开头的测试この記事では、JavaScript のディープ コピーについて詳しく説明します。存在循环引用,除去这个条件进行测试
const clonedObj = deepClone(この記事では、JavaScript のディープ コピーについて詳しく説明します。)

// 测试
clonedObj === この記事では、JavaScript のディープ コピーについて詳しく説明します。  // false,返回的是一个新对象
clonedObj.arr === この記事では、JavaScript のディープ コピーについて詳しく説明します。.arr  // false,说明拷贝的不是引用
ログイン後にコピー

浏览器执行结果:

この記事では、JavaScript のディープ コピーについて詳しく説明します。
该基础版本存在许多问题:

  • 不能处理循环引用。

  • 只考虑了Object对象,而Array对象、Date对象、RegExp对象、Map对象、Set对象都变成了Object对象,且值也不正确。

  • 丢失了属性名为Symbol类型的属性。

  • 丢失了不可枚举的属性。

  • 原型上的属性也被添加到拷贝的对象中了。

如果存在循环引用的话,以上代码会导致无限递归,从而使得堆栈溢出。如下例子:

const a = {}
const b = {}
a.b = b
b.a = a
deepClone(a)
ログイン後にコピー

对象 a 的键 b 指向对象 b,对象 b 的键 a 指向对象 a,查看a对象,可以看到是无限循环的:
この記事では、JavaScript のディープ コピーについて詳しく説明します。
对对象a执行深拷贝,会出现死循环,从而耗尽内存,进而报错:堆栈溢出
この記事では、JavaScript のディープ コピーについて詳しく説明します。
如何避免这种情况呢?一种简单的方式就是把已添加的对象记录下来,这样下次碰到相同的对象引用时,直接指向记录中的对象即可。要实现这个记录功能,我们可以借助 ES6 推出的 WeakMap 对象,该对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。(WeakMap相关见这:WeakMap)

针对以上基础版深拷贝存在的缺陷,我们进一步去完善,实现一个完美的深拷贝

方法3:递归この記事では、JavaScript のディープ コピーについて詳しく説明します。

对于基础版深拷贝存在的问题,我们一一改进:

##Object.assign()
構文を展開します


欠陥と
Array.prototype.concat() 配列のコピー #浅いコピー、基本的な型値の配列に適しています
arr.slice([begin[, end]])
存在的问题改进方案
1. 不能处理循环引用使用 WeakMap 作为一个Hash表来进行查询
2. 只考虑了Object对象当参数为 DateRegExpFunctionMapSet,则直接生成一个新的实例返回
3. 属性名为Symbol的属性
4. 丢失了不可枚举的属性
针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys()
Reflect.ownKeys(この記事では、JavaScript のディープ コピーについて詳しく説明します。)相当于[...Object.getOwnPropertyNames(この記事では、JavaScript のディープ コピーについて詳しく説明します。), ...Object.getOwnPropertySymbols(この記事では、JavaScript のディープ コピーについて詳しく説明します。)]
4. 原型上的属性Object.getOwnPropertyDescriptors()设置属性描述对象,以及Object.create()方式继承原型链

代码实现:

function deepClone(target) {
    // WeakMap作为记录对象Hash表(用于防止循环引用)
    const map = new WeakMap()

    // 判断是否为この記事では、JavaScript のディープ コピーについて詳しく説明します。ect类型的辅助函数,减少重复代码
    function isObject(target) {
        return (typeof target === 'この記事では、JavaScript のディープ コピーについて詳しく説明します。ect' && target ) || typeof target === 'function'
    }

    function clone(data) {

        // 基础类型直接返回值
        if (!isObject(data)) {
            return data
        }

        // 日期或者正则对象则直接构造一个新的对象返回
        if ([Date, RegExp].includes(data.constructor)) {
            return new data.constructor(data)
        }

        // 处理函数对象
        if (typeof data === 'function') {
            return new Function('return ' + data.toString())()
        }

        // 如果该对象已存在,则直接返回该对象
        const exist = map.get(data)
        if (exist) {
            return exist
        }

        // 处理Map对象
        if (data instanceof Map) {
            const result = new Map()
            map.set(data, result)
            data.forEach((val, key) => {
                // 注意:map中的值为この記事では、JavaScript のディープ コピーについて詳しく説明します。ect的话也得深拷贝
                if (isObject(val)) {
                    result.set(key, clone(val))
                } else {
                    result.set(key, val)
                }
            })
            return result
        }

        // 处理Set对象
        if (data instanceof Set) {
            const result = new Set()
            map.set(data, result)
            data.forEach(val => {
                // 注意:set中的值为この記事では、JavaScript のディープ コピーについて詳しく説明します。ect的话也得深拷贝
                if (isObject(val)) {
                    result.add(clone(val))
                } else {
                    result.add(val)
                }
            })
            return result
        }

        // 收集键名(考虑了以Symbol作为key以及不可枚举的属性)
        const keys = Reflect.ownKeys(data)
        // 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性以及对应的属性描述
        const allDesc = Object.getOwnPropertyDescriptors(data)
        // 结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链, 这里得到的result是对data的浅拷贝
        const result = Object.create(Object.getPrototypeOf(data), allDesc)

        // 新对象加入到map中,进行记录
        map.set(data, result)

        // Object.create()是浅拷贝,所以要判断并递归执行深拷贝
        keys.forEach(key => {
            const val = data[key]
            if (isObject(val)) {
                // 属性值为 对象类型 或 函数对象 的话也需要进行深拷贝
                result[key] = clone(val)
            } else {
                result[key] = val
            }
        })
        return result
    }

    return clone(target)
}



// 测试
const clonedObj = deepClone(この記事では、JavaScript のディープ コピーについて詳しく説明します。)
clonedObj === この記事では、JavaScript のディープ コピーについて詳しく説明します。  // false,返回的是一个新对象
clonedObj.arr === この記事では、JavaScript のディープ コピーについて詳しく説明します。.arr  // false,说明拷贝的不是引用
clonedObj.func === この記事では、JavaScript のディープ コピーについて詳しく説明します。.func  // false,说明function也复制了一份
clonedObj.proto  // proto,可以取到原型的属性
ログイン後にコピー

详细的说明见代码中的注释,更多测试希望大家自己动手尝试验证一下以加深印象。

在遍历 Object 类型数据时,我们需要把 Symbol 类型的键名也考虑进来,所以不能通过 Object.keys 获取键名或 for...in 方式遍历,而是通过Reflect.ownKeys()获取所有自身的键名(getOwnPropertyNamesgetOwnPropertySymbols 函数将键名组合成数组也行:[...Object.getOwnPropertyNames(この記事では、JavaScript のディープ コピーについて詳しく説明します。), ...Object.getOwnPropertySymbols(この記事では、JavaScript のディープ コピーについて詳しく説明します。)]),然后再遍历递归,最终实现拷贝。

浏览器执行结果:
この記事では、JavaScript のディープ コピーについて詳しく説明します。
可以发现我们的cloneObj对象和原来的この記事では、JavaScript のディープ コピーについて詳しく説明します。对象一模一样,并且修改cloneObj对象的各个属性都不会对この記事では、JavaScript のディープ コピーについて詳しく説明します。对象造成影响。其他的大家再多尝试体会哦!

【相关推荐:javascript视频教程编程视频

以上がこの記事では、JavaScript のディープ コピーについて詳しく説明します。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:csdn.net
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート