Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
7
Help us understand the problem. What are the problem?

posted at

updated at

JavaScript さまざまな型に対応できる拡張機能つき clone と cloneDeep を実装しました。

GitHub

一度書いたものは二度と書きたくないので、なるべく多くのプラットホームで動くJSの汎用部品として自作のライブラリを作っていて、npm に公開しています。今回の clone と cloneDeep も root.js というところに全コード含まれています。テストコードも自作して動作確認済みです。

standard-software/partsjs: JavaScript Code Parts
https://github.com/standard-software/partsjs

経緯

lodash の cloneDeep とかでもよかったのですが、まあ勉強のためにこの程度の関数は自分で作れるだろうと思い、clone(シャローコピー) とか cloneDeep(ディープコピー)するための関数を作りはじめたのですが、

簡単だと思ってましたが、すごくはまりました。

再帰かければ簡単かと思いましたが、オブジェクト内オブジェクトや関数内オブジェクト内関数内オブジェクトなど、そういうツリー構造のcloneDeepなコードは問題なかったのですが、

いろんなオブジェクトに対する対応を考えるとかなり難しい...

ともかくも、通常のツリー構造になったオブジェクトや配列のシャローコピー、ディープコピーコードはこちらです。

ソースコード


/**
 * cloneFunction
 */
const cloneFunction = {};

cloneFunction.objectType = (source, __cloneDeep = value => value) => {
  if (_isFunction(source)) {
    return { result: false };
  }
  if (!_isObjectType(source)) {
    return { result: false };
  }

  const cloneValue = new source.constructor();
  for (let key in source) {
    if (source.hasOwnProperty(key)) {
      cloneValue[key] = __cloneDeep(source[key]);
    }
  }
  return { result: true, cloneValue } ;
}


cloneFunction.object = (source, __cloneDeep = value => value) => {
  if (!_isObject(source)) {
    return { result: false };
  }
  const cloneValue = {};
  for (let key in source) {
    cloneValue[key] = __cloneDeep(source[key]);
  }
  return { result: true, cloneValue } ;
}

cloneFunction.array = (source, __cloneDeep = value => value) => {
  if (!_isArray(source)) {
    return { result: false };
  }
  const cloneValue = [];
  for (let i = 0, l = source.length; i < l; i += 1) {
    const value = source[i];
    cloneValue.push(__cloneDeep(value))
  }
  return { result: true, cloneValue } ;
}

cloneFunction.date = (source) =>
  _isDate(source)
  ? {
    result: true,
    cloneValue: new Date(source.getTime()),
  }
  : {
    result: false,
  }

cloneFunction.regExp = (source) =>
  _isRegExp(source)
  ? {
    result: true,
    cloneValue: new RegExp(source.source),
  }
  : {
    result: false,
  }

/**
 * root.clone
 */
const _clone = (source) => {
  const __clone = (value) => {
    for (let i = 0, l = _clone.functions.length; i < l; i += 1) {
      const {
        result,
        cloneValue,
      } = _clone.functions[i](value);
      if (result) {
        return cloneValue;
      }
    }
    return value;
  }
  return __clone(source);
}
_clone.functions = [];

_clone.clear = () => {
  _clone.functions = [];
};

_clone.add = (func) => {
  _clone.functions.unshift(func);
};

_clone.reset = () => {
  _clone.clear();
  _clone.add(cloneFunction.objectType);
  _clone.add(cloneFunction.regExp)
};
_clone.reset();

const clone = (source) => {
  return _clone(source)
}

_copyProperty(_clone,
  'clear,reset,add,' +
  '',
  clone
);

/**
 * root.cloneDeep
 */
const _cloneDeep = (source) => {
  const __cloneDeep = (value) => {
    for (let i = 0, l = _cloneDeep.functions.length; i < l; i += 1) {
      const {
        result,
        cloneValue,
      } = _cloneDeep.functions[i](value, __cloneDeep);
      if (result) {
        return cloneValue;
      }
    }
    return value;
  }
  return __cloneDeep(source);
}

_cloneDeep.functions = [];

_cloneDeep.clear = (func) => {
  _cloneDeep.functions = [];
};

_cloneDeep.add = (func) => {
  _cloneDeep.functions.unshift(func);
};

_cloneDeep.reset = () => {
  _cloneDeep.clear()
  _cloneDeep.add(cloneFunction.objectType)
  _cloneDeep.add(cloneFunction.regExp)
};
_cloneDeep.reset();

clone も cloneDeep も処理が共通化されていて、主な処理は cloneFunction の中で定義されている各関数にまかせています。

cloneFunction.object や cloneFunction.array は実際には使わず、cloneFunction.objectType で object も array も対応できます。これらは再帰呼び出しもとの関数を引数でうけとっているので、再帰的に動作させることが可能。

また、cloneFunction.regExp や cloneFunction.date なども定義していて、これらはそれぞれ対応したものの clone を行う事ができます。cloneFunction.objectType で date 型も正常にクローンできるので初期設定にはいれてません。

これを clone.add() あるいは cloneDeep.add() で関数を複数登録することによって
clone cloneDeep がどんな型でも対応してシャローコピーあるいはディープコピーできるものになっています。

GitHubのテストコードの部分を細かくみてもらうと、moment オブジェクトというような複雑なものであっても独自関数を作り addメソッドで関数追加することで、対応可能です。

学びになったこと

オブジェクトのほとんどは、元オブジェクトのコンストラクタから生成して for in ownProperty でコピーすればだいたいの要件は済む事。arrayも同じコードで動くのは学べました。

また、date や regexp は自前で clone してしまう方がよいこと。moment もそのオブジェクトに限りコピーする方法を見抜いてそれを登録してしまえばいい感じになり、拡張性のある clone cloneDeep を作れてよかったです。

至らなかったこと。

【JavaScript】オブジェクトをDeepCopyするclone関数を書いた | Web活
http://webkatu.com/clone-function-to-deepcopy-object/

こちらの方の書いたコード。すごくて、おどろきました。もはや cloneDeep沼という感じ。

こちらの方が対応されている、「プロパティのディスクリプタ」や「関数クローン」「nativeオブジェクトスルー」などの機能については不理解と必要なさそうだったので取り込みませんでした。

拡張関数を追加しまくればできそうだけれども。

cloneしたい元オブジェクトに Math つっこまれていることとか、ちょっと対応しなかったです。

あと循環参照対応。
こんな状態を思いつきませんでした。

Web活の中の方はすごいです。

汎用部品を作るって大変だなと、改めて思いました。

対応したい。

WSH 対応するために、そしてディスクリプタ自体が不要と思うのでディスクリプタコピーはしないですが
循環参照対応や、Map Set Symbol 対応はいれたいかな。

現場からは以上です。

ま。サンプルソースや、軽量 clone & cloneDeep コードのご参考にでもどうぞ。

追記:対応した。

現在のバージョンでは、循環参照と Map Set は対応しています。それらがない古いJS実装(WSHとか)ではスルーしてますが、テストコードなども書いています。

https://github.com/standard-software/partsjs/blob/v5.4.0/source_code/root/clone.js
https://github.com/standard-software/partsjs/blob/v5.4.0/source_code/root/root.test.js

npm でダウンロードして使ったり、
https://www.npmjs.com/package/@standard-software/parts

CDNで簡単に使うことができます。
https://jsbin.com/poyizacevo/edit?html,js,console

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
7
Help us understand the problem. What are the problem?