濃いコピーと薄いコピーを手書きする方法を知りませんか? (2023年最新)

序文

JavaScript プログラミングでは、データをコピーする必要があることがよくありますが、ディープ コピーをいつ使用するか、シャロー コピーをいつ使用するかについては、検討する価値があります。


浅いコピー


浅いコピーはオブジェクトの最上位構造のコピーであり、新しいオブジェクトが作成されますが、この新しいオブジェクトは元のオブジェクトのヒープ メモリ参照のみをコピーし、オブジェクト自体はコピーしません。浅いコピーでは、新しいオブジェクトと元のオブジェクトが同じヒープ メモリを共有します。つまり、元のオブジェクトの属性値が変更されると、新しいオブジェクトの属性値も変更され、その逆も同様です。

オブジェクトの割り当て 

object.assign は ES6 のオブジェクトのメソッドです。このメソッドは、JS オブジェクトのマージなど、さまざまな目的に使用できます。用途の 1 つは、浅いコピーを実行することです。このメソッドの最初のパラメータはコピーされるターゲット オブジェクトで、次のパラメータはコピーされるソース オブジェクトです (複数のソースである場合もあります)。

object.assign の構文は次のとおりです。 Object.assign(target, ...sources)

欠点がある 

  • オブジェクトの継承されたプロパティはコピーされません
  • オブジェクトの列挙不可能なプロパティはコピーされません
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);
console.log('obj2',obj2);

 

上記のコードからわかるように、object.assign を使用してシンボル型オブジェクトをコピーすることもできますが、オブジェクトの 2 番目の層属性 obj1.ab に到達すると、前者の値の変更は後者の 2 番目の属性にも影響します。この属性の値は、共通ヒープ メモリへのアクセスに問題がまだ存在することを示しており、このメソッドはそれ以上コピーできず、浅いコピー機能のみを完了することを意味します。

スプレッド演算子

JavaScript のスプレッド演算子を利用して、オブジェクトの構築中に浅いコピー機能を完了します。 

扩展运算符的语法为:`let cloneObj = { ...obj };`
/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj)  //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj)  //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果

スプレッド演算子には object.assign と同じ欠陥があり、実装された浅いコピー機能も同様ですが、属性がすべて基本型の値である場合は、スプレッド演算子を使用して浅いコピーを実行する方が便利です。

concat コピー配列 

配列 concat メソッドは実際には浅いコピーであるため、参照型を含む配列を接続する場合は、元の配列の要素の属性を変更することに注意する必要があります。これは、コピー後に接続された配列に影響を与えるためです。ただし、 concat 配列の浅いコピーにのみ使用でき、使用シナリオは比較的限られています。コードを以下に示します。 

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);  // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]

 スライスコピー配列

slice 方法も非常に限られています它仅仅针对数组类型slice方法会返回一个新的数组对象、このオブジェクトは、メソッドの最初の 2 つのパラメーターによって元の配列インターセプトの開始時刻と終了時刻を決定し、元の配列に影響を与えたり変更したりすることはありません。

slice 的语法为:arr.slice(begin, end);
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);  //[ 1, 2, { val: 1000 } ]

上記のコードからわかるように、これがそれです浅拷贝的限制所在了——它只能拷贝一层对象もし存在对象的嵌套,那么浅拷贝将无能为力そこで、この問題を解決するために誕生したのが、多層オブジェクトの入れ子の問題を解決し、コピーを完全に実現できるディープコピーです。

アイデア        

  1. 基本タイプの基本コピーを作成する
  2. 参照データ型用の新しいストレージを作成し、オブジェクト属性のレイヤーをコピーします (元のオブジェクトと同じメモリ空間を引き続き参照します)。

成し遂げる 


const shallowClone = (target) => {
  // 判断目标对象是否为对象类型且不为null
  if (typeof target === 'object' && target !== null) {
    // 创建一个克隆对象 cloneTarget,如果目标对象是数组则创建一个空数组,否则创建一个空对象
    const cloneTarget = Array.isArray(target) ? []: {};
    // 遍历目标对象的属性,使用 for...in 循环,判断属性是否为对象本身的属性(而非原型链上的属性)
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
        // 给克隆对象的属性赋值,将目标对象的属性值赋给克隆对象的属性
        cloneTarget[prop] = target[prop];
      }
    }
    // 返回克隆对象
    return cloneTarget;
  } else {
    // 如果目标对象不是对象类型或为null,则直接返回目标对象
    return target;
  }
}

 

 型判定を使用し、参照型オブジェクトに対して for ループを実行してオブジェクトのプロパティを走査し、ターゲット オブジェクトのプロパティに値を割り当てることで、基本的にコードの浅いコピーを手動で実装できます。

ディープコピーの原理と実装

浅いコピーは新しいオブジェクトを作成し、元のオブジェクトの基本型の値をコピーするだけですが、参照データ型は 1 層の属性をコピーするだけであり、より深いコピーは作成できません。ディープ コピーは異なります。複雑な参照データ型の場合、ヒープ メモリ内のメモリ アドレスを完全に開き、元のオブジェクトをストレージ用に完全にコピーします。

これら 2 つのオブジェクトは互いに独立しており、影響を受けず、メモリの分離が完全に実現されます。一般に、ディープ コピーの原理は次のように要約できます。

メモリからのオブジェクトの完全なコピーがターゲット オブジェクトに与えられ、新しいオブジェクトを格納するためにヒープ メモリに新しいスペースが開かれます。新しいオブジェクトを変更しても元のオブジェクトは変更されず、オブジェクト間の真の分離が実現されます。二つ。 

Beggar バージョンの JSON.stringify() 

JSON.stringify() これは、現在の開発プロセスで最も単純なディープ コピー メソッドであり、実際にオブジェクトを文字列にシリアル化し JSON 、オブジェクト内のコンテンツを文字列に変換し、最後に JSON.parse() メソッドを 使用してJSON 文字列から新しいオブジェクトを生成します。 

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

 欠点がある

  • コピーされたオブジェクトの値がこれらのタイプ函数のいずれかである場合、キーと値のペアは JSON.stringify() シーケンスの後の文字列内で消えますundefinedsymbol
  • コピーするDate引用类型と文字列になります
  • 列挙不可能なプロパティはコピーできません
  • オブジェクトのプロトタイプ チェーンをコピーできません
  • コピーするとRegExp引用类型空のオブジェクトになります
  • オブジェクトには が含まれておりNaNJSON.parse() シリアル化の結果は次のようになりますInfinity-Infinitynull
  • オブジェクトの循環参照はコピーできません。これは对象成环(obj[key]=obj)次の場合に当てはまります。
function Obj() { 
  this.func = function () { alert(1) }; 
  this.obj = {a:1};
  this.arr = [1,2,3];
  this.und = undefined; 
  this.reg = /123/; 
  this.date = new Date(0); 
  this.NaN = NaN;
  this.infinity = Infinity;
  this.sym = Symbol(1);
} 
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{ 
  enumerable:false,
  value:'innumerable'
});
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);

 

 

上記の問題はまだ残っていますが、シンプルかつ迅速な日々の開発ニーズを満たすには十分です。

JSON.stringify メソッドを使用してディープ コピー オブジェクトを実装します。現時点ではまだ実装できない機能がたくさんありますが、このメソッドは日常の開発ニーズを満たすのに十分であり、最も簡単かつ高速です。ディープコピーが必要な、より面倒な属性に対応するその他のデータ型については、当面は JSON.stringify ではまだ不十分であるため、次のメソッドが必要です。

基本バージョン(手書き再帰実装)

以下は deepClone 関数のカプセル化の実装例です. for in を使用して入力パラメータの属性値を走査します. 値が参照型の場合は関数が再帰的に呼び出されます. 基本データの場合は関数が再帰的に呼び出されます.タイプの場合は、直接コピーされます。

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}}

 

 上記の JSON.stringify のように、再帰を使用してディープ コピーを実装できますが、完全には解決されていない次のような問題がまだいくつかあります。

  1. このディープ コピー関数は、列挙不可能なプロパティとシンボル タイプをコピーできません。
  2. このメソッドは通常の参照型の値の再帰コピーのみを実行しますが、Array、Date、RegExp、Error、Function などの参照型の値を正しくコピーすることはできません。
  3. オブジェクトのプロパティにループがあります。つまり、循環参照が解決されていません。

この基本バージョンも比較的簡単に作成でき、ほとんどのアプリケーション状況に対応できます。しかし、まだ欠陥がたくさんあるので、どうすればよいでしょうか?

心配しないでください。上級バージョンもあります

上級版(再帰実装)

上記の問題を解決するにはどうすればよいでしょうか?

  1. オブジェクトを横断できる列挙不可能なプロパティとシンボル型の場合は、Reflect.ownKeys (列挙可能なプロパティ、列挙不可能なプロパティ、シンボル型のプロパティを含むオブジェクトのすべてのプロパティを取得できます) メソッドを使用できます。
  2. パラメータが Date 型または Regexp 型の場合、新しいインスタンスが直接生成されて返されます。
  3. オブジェクトのすべてのプロパティと対応する特性を取得するには、Object.getOwnPropertyDescriptors メソッドを使用します。ちなみに、これを Object.create メソッドと組み合わせて、新しいオブジェクトを作成し、元のオブジェクトのプロトタイプ チェーンを継承します。
  4. weakMap 型をハッシュ テーブルとして使用します。weakMap は弱参照型であるため、メモリ リークを効果的に防ぐことができます。循環参照の検出に非常に役立ちます。循環がある場合、参照はweakmap に格納されている値を直接返します。

ディープコピーを実装する 

// 判断一个对象是否为复杂数据类型,即对象或函数类型,且不为 null
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

// 定义深拷贝函数 deepClone,接受两个参数:obj 为要进行深拷贝的目标对象,hash 为已经拷贝过的对象的哈希表(用于解决循环引用问题)
const deepClone = function (obj, hash = new WeakMap()) {
  // 如果目标对象是日期对象,则直接返回一个新的日期对象,避免修改原日期对象
  if (obj.constructor === Date) {
    return new Date(obj)
  }
  
  // 如果目标对象是正则对象,则直接返回一个新的正则对象,避免修改原正则对象
  if (obj.constructor === RegExp){
    return new RegExp(obj)
  }
  
  // 如果目标对象已经被拷贝过,则从 hash 中获取已经拷贝过的对象并返回,避免出现循环引用问题
  if (hash.has(obj)) {
    return hash.get(obj)
  }

  // 获取目标对象的所有属性描述符
  let allDesc = Object.getOwnPropertyDescriptors(obj)

  // 创建一个新对象 cloneObj,并将其原型链指向 obj 的原型对象
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)

  // 将目标对象和克隆对象的映射关系存入 hash 中,用于解决循环引用问题
  hash.set(obj, cloneObj)

  // 遍历目标对象的所有属性(包括字符串类型和 Symbol 类型的属性名)
  for (let key of Reflect.ownKeys(obj)) { 
    // 如果目标对象的属性值是复杂数据类型(即对象或数组),则递归调用 deepClone 函数进行深拷贝,并将拷贝结果赋值给克隆对象的对应属性
    if (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') {
      cloneObj[key] = deepClone(obj[key], hash)
    } else {
      // 如果目标对象的属性值不是复杂数据类型,则直接将其赋值给克隆对象的对应属性
      cloneObj[key] = obj[key]
    }
  }
  // 返回深拷贝后的新对象
  return cloneObj
}


// 下面是验证代码
let obj = {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '我是一个对象', id: 1 },
  arr: [0, 1, 2],
  func: function () { console.log('我是一个函数') },
  date: new Date(0),
  reg: new RegExp('/我是一个正则/ig'),
  [Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
  enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
 

要約する

この記事では、濃いコピーと浅いコピーとそれぞれの利点と欠点を紹介し、浅いものから深いものへと、手書きの濃い浅いコピー方法のアイデアと実装を段階的に説明しています。 js の基礎となる原理を深く理解する。

最後に、みんなが強くなることを願っています!注意

おすすめ

転載: blog.csdn.net/YN2000609/article/details/132410435