3Dプログラミング入門講座・その25:シャローコピーディープコピーからマネージドクラスの模倣


シャローコピーとはデータそのものではなくデータの住所の情報のコピーです
したがってデータが書き換えられたときシャローコピーされたほかの変数も変えられてしまいます
ディープコピーはデータそのものの情報の直接のコピーで変更されたときほかの変数からでも
情報は変わりませんつまり
シャローコピーは同一人物を違う角度から見たもの
ディープコピーはもう一人そっくりな人物を複製してそれぞれを見たもの
になります
以下にコードを示します


<script src="./javascripts/x3dom/jquery-3.7.1.min.js"></script>


function enter(){

  var data = [["apple","orange","lemon"],{name: "taro", occupation: "singer"}];
  var hoge = data;
  data[0].splice(1,0,"tomato");
  console.log(JSON.stringify(data));//[["apple","tomato","orange","lemon"],{"name":"taro","occupation":"singer"}]
  console.log(JSON.stringify(hoge));//[["apple","tomato","orange","lemon"],{"name":"taro","occupation":"singer"}]

  var hage = structuredClone(data);
  data[1]["nation"] = "japan";
  console.log(JSON.stringify(data));//[["apple","tomato","orange","lemon"],{"name":"taro","occupation":"singer","nation":"japan"}]
  console.log(JSON.stringify(hoge));//[["apple","tomato","orange","lemon"],{"name":"taro","occupation":"singer","nation":"japan"}]
  console.log(JSON.stringify(hage));//[["apple","tomato","orange","lemon"],{"name":"taro","occupation":"singer"}]

  var numdata = {value: 1};
  console.log(JSON.stringify(numdata));//{"value":1}
  console.log(numdata);//{"value":666}
  numdata["value"] = 666;

  enter2();
}

let defprp = {name: "Taro", nation: "Japan"};
class Person {
  constructor(p = defprp) {
    this.property = structuredClone(p); 
  }
  clone() {
    return new Person(this.property);
  }
  greet() {
    return `Hello, I'm ${this.property.name}.`;
  }
}

function enter2(){

  const originalPerson = new Person({name: "Jiro", nation: "Japan", score: 95});
  const clonedPerson = originalPerson.clone();
  originalPerson.property.score = 100;
  console.log(clonedPerson.property.score);//95
  console.log(clonedPerson instanceof Person);//true
  console.log(clonedPerson.greet());//Hello, I'm Jiro.


  const taro = new Person({name: "Taro"});
  const jiro = new Person({name: "Jiro"});
  Person.prototype.sing = function(song) {
    return `${this.property.name} is singing "${song}".`;
  };

  console.log(taro.sing("Happy Birthday")); // "Taro is singing "Happy Birthday"."
  console.log(jiro.sing("Twinkle Star"));   // "Jiro is singing "Twinkle Star"."

  enter3();

}

const Person2defaultProperties = {
  profile: { name: "Default Name", age: 25 },
  settings: { theme: "light", notifications: true }
};

class Person2 {
  constructor(p) {
    this.property = $.extend(true, {}, Person2defaultProperties, p); 
  }
  clone() {
    return new Person2(this.property);
  }
  merge(p){
    this.property = $.extend(true, {}, this.property, p);
    return this;
  }
  greet() {
    return `Hello, I'm ${this.property.profile.name}.`;
  }
}

const taroData = {
  variablename: "taroData",
  profile: { name: "Taro", nation: "Japan", items: {0: "ball", 1: "glove"} },
  settings: { theme: "red" }
};
/*
itemsにmapやfilterを使いたい場合には配列として
const taroData = {
  profile: { name: "Taro", nation: "Japan", items: ["ball", "glove"] },
  settings: { theme: "red" }
};
こうしてね
*/
const jiroData = {
  variablename: "jiroData",
  profile: { name: "Jiro", nation: "Japan" },
  settings: { theme: "dark" }
};

const zakoData = {
  variablename: "zakoData",
  profile: { name: "Zako", nation: "Japan" },
  settings: { theme: "blue" }
};


Person2.prototype.sing = function(song) {
  return `${this.property.profile.name} is singing "${song}".`;
};

function enter3(){

  const taro = new Person2(taroData);
  const jiro = new Person2().merge(jiroData);

  console.log(taro.property);
/*
taro.property = {
  profile: { age: 25, items: {0: 'ball', 1: 'glove'}, name: "Taro", nation: "Japan" },
  settings: { notifications: true, theme: "red" }
};
*/
  console.log(jiro.property);
/*
jiro.property = {
  profile: {age: 25, name: "Jiro", nation: "Japan" },
  settings: {notifications: true, theme: "dark" }
};
*/
  console.log(taro.sing("Happy Birthday")); // "Taro is singing "Happy Birthday"."
  console.log(jiro.sing("Twinkle Star"));   // "Jiro is singing "Twinkle Star"."

  enter4();
}


/**
 * MyManagedClassの結果はオブジェクトとして返されるが、配列型が必要な場合はtoArrayIfIndexedを使用。
 * 例: const items = toArrayIfIndexed(instance.property.profile.items, true);
 */
/**
 * MyManagedClassは配列をオブジェクトとして処理します。
 * 配列型が必要な場合、toArrayIfIndexedを使用して変換してください。
 * 例:
 * const instance = new MyManagedClass({ items: [{ id: 1 }, { id: 2 }] });
 * const itemsArray = toArrayIfIndexed(instance.property.items, true); // [{ id: 1 }, { id: 2 }]
// 配列プロパティの取得
const instance = new MyManagedClass({ profile: { items: [{ id: 1 }, { id: 2 }] } });
const items = toArrayIfIndexed(instance.property.profile.items, true);
items.forEach(item => console.log(item.id)); // 1, 2

// トップレベルが配列
const arrayInstance = new MyManagedClass([{ id: 1 }, { id: 2 }]);
const array = toArrayIfIndexed(arrayInstance.property, true);
console.log(array); // [{ id: 1 }, { id: 2 }] */
/**
 * 数字文字列のキーを持つオブジェクトを配列に整形する
 * @param {Object} obj - 変換対象のオブジェクト
 * @param {boolean} [force=false] - キーが数字文字列でない場合も強制的に配列に変換
 * @param {String} [sparseHandling = 'keep'] - 空白配列のコンパクト化の選択
 * @returns {Array|Object} 配列(変換可能な場合)または元のオブジェクト
 */
function toArrayIfIndexed(obj, force = false, sparseHandling = 'keep') {
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
  const keys = Object.keys(obj);
  const isIndexed = keys.every((key, i) => String(i) === key);
  if (!isIndexed && !force && process.env.NODE_ENV !== 'production') {
    console.warn('Non-sequential keys detected; consider using force=true or sparseHandling="compact"');
  }
  const maxIndex = keys.length > 0 ? Math.max(...keys.map(Number)) + 1 : 0;
  const result = new Array(maxIndex);
  for (const key of keys) {
    result[Number(key)] = obj[key];
  }
  if (sparseHandling === 'compact') {
    return result.filter(x => x !== undefined);
  }
  return result;
}

// フォールバック用の簡易クローン関数
function fallbackClone(item, seen = new WeakSet()) {
  if (item === null || typeof item !== 'object') return item;
  if (seen.has(item)) {
    throw new Error('Circular reference detected in fallbackClone');
  }
  seen.add(item);
  if (typeof item.clone === 'function') {
    return item.clone();
  }
  if (Array.isArray(item)) return item.map(item => fallbackClone(item, seen));
  if (item instanceof Date) return new Date(item);
  if (item instanceof Map) return new Map(fallbackClone([...item], seen));
  if (item instanceof Set) return new Set(fallbackClone([...item], seen));
  if (item instanceof RegExp) return new RegExp(item);
  const cloned = {};
  for (const key in item) {
    cloned[key] = fallbackClone(item[key], seen);
  }
  return cloned;
}

// 安全なstructuredCloneラッパー
function safeStructuredClone(item) {
  try {
    return structuredClone(item);
  } catch (e) {
    return fallbackClone(item);
  }
}

function recursiveClone(item, seen = new WeakSet()) {
  // 循環参照のチェック
  if (item && typeof item === 'object' && seen.has(item)) {
    throw new Error('Circular reference detected');
  }
  if (item && typeof item === 'object') {
    seen.add(item);
  }

  // 1. clone() メソッドを持つ場合
  if (item && typeof item.clone === 'function') {
    return item.clone();
  }

  // 2. 配列の場合
  if (Array.isArray(item)) {
    return item.map(element => recursiveClone(element, seen));
  }

  // 3. プレーンなオブジェクトの場合
  if (item && typeof item === 'object' && item.constructor === Object) {
    const clonedObject = {};
    for (const key in item) {
      clonedObject[key] = recursiveClone(item[key], seen);
    }
    return clonedObject;
  }

  // 4. その他(プリミティブ値など)
  return safeStructuredClone(item);
}

function simpleDeepMerge(target, source, seen = new WeakSet(), deep = true) {
    if (!deep) {
        return Object.assign({}, target, source);
    }    
    // ターゲットが有効なオブジェクトであることを確認
    // ターゲットがオブジェクトでなければ、ソースをディープコピーして返す(上書き)
    if (target === null || typeof target !== 'object') {
        // ターゲットが無効な場合、処理を継続する意味がないので、ソースをそのまま返すか、safeStructuredCloneを使う
        return safeStructuredClone(source); 
    }
    
    // ソースが有効なオブジェクトであることを確認
    if (source === null || typeof source !== 'object') {
        return target; // マージするものがないため、ターゲットをそのまま返す
    }
    
    // 1. 循環参照チェックと登録
    // この層での処理が確定したら登録
    if (seen.has(target) || seen.has(source)) {
        throw new Error('Circular reference detected');
    }
    seen.add(target);
    seen.add(source);

    // 配列をオブジェクトに変換するヘルパー関数
    const arrayToObject = arr => ({ ...arr });


    if (Array.isArray(target) && process.env.NODE_ENV !== 'production') {
      console.warn('Target array converted to object in simpleDeepMerge. Use toArrayIfIndexed to convert back.');
    }


    // 配列処理の統一: 配列はすべてインデックス付きのプレーンなオブジェクトとして扱う
    if (Array.isArray(target)) {
        // targetはマージのターゲットであり、型を維持するため、新しいオブジェクトに変換
        // 既存の target の内容を維持しつつ、オブジェクトとしてマージするために、新しいオブジェクトを作る
        target = Object.assign({}, target); 
    }
    if (Array.isArray(source)) {
        source = arrayToObject(source);
    }
    
    // 2. マージロジック
    for (const key in source) {
        const sourceValue = source[key];
        const targetValue = target[key];

        // ターゲットの値がオブジェクトで、ソースの値もオブジェクトの場合にのみ再帰する
        if (sourceValue && typeof sourceValue === 'object' && sourceValue.constructor === Object) {
            
            // ターゲットの値がオブジェクトでなければ、新しい空のオブジェクトで初期化
            if (!targetValue || typeof targetValue !== 'object' || Array.isArray(targetValue)) {
                 target[key] = {}; 
            }
            // 配列をオブジェクトとして扱うため、ここでは Array.isArray(targetValue) のチェックは削除しても良いが、
            // ターゲットが配列として存在していた場合は既に Object.assign({}, target) でオブジェクトになっているはず
            
            simpleDeepMerge(target[key], sourceValue, seen); // 再帰

        } else if (typeof sourceValue?.clone === 'function') {
            // カスタムクラスは clone() で上書き
            target[key] = sourceValue.clone();
            
        } else {
            // プリミティブ、その他のオブジェクトは safeStructuredClone でディープコピーして上書き
            target[key] = safeStructuredClone(sourceValue);
        }
    }
    
    return target;
}

function enter4(){

  // 使用例: 複数の型とネスト構造を持つ配列
  const nestedHoge = [
    new Person2(taroData), 
    ['a', ['b', new Person2(jiroData)]], 
    { x: 1, y: 2 }
  ];

  const clonedHage = recursiveClone(nestedHoge);
  nestedHoge[1][0] = 'changed';

  console.log(nestedHoge);
/*
  const nestedHoge = [
    new Person2(taroData), 
    ['cahnged', ['b', new Person2(jiroData)]], 
    { x: 1, y: 2 }
  ];
*/
  console.log(clonedHage);
/*
  const clonedHage = [
    new Person2(taroData), 
    ['a', ['b', new Person2(jiroData)]], 
    { x: 1, y: 2 }
  ];
*/

  const personMap = {
    leader: new Person2(taroData),
    member: {
      subleader: new Person2(jiroData),
      bench: new Person2(zakoData)
    }
  };

  const clonedPM = recursiveClone(personMap);
  const tmpP = personMap.leader.clone();
  personMap.leader = personMap.member.bench.clone();
  personMap.member.bench = tmpP.clone();

  console.log(personMap);
/*
  const personMap = {
    leader: new Person2(zakoData),
    member: {
      subleader: new Person2(jiroData),
      bench: new Person2(taroData)
    }
  };
*/
  console.log(clonedPM);
/*
  const clonedPM = {
    leader: new Person2(taroData),
    member: {
      subleader: new Person2(jiroData),
      bench: new Person2(zakoData)
    }
  };
*/

  enter5();
}

const MyManagedClassDefaultProperty = Object.freeze({
  variablename: "MyManagedClassDefaultProperty",
  profile: Object.freeze({ name: "Default Name", age: 25 }),
  settings: Object.freeze({ theme: "light", notifications: true })
});

class MyManagedClass {
  constructor(p, deep = true) {
    if(deep) {
      const target = recursiveClone(MyManagedClassDefaultProperty);
      this.property = simpleDeepMerge(target, p);
    }
    else {
      this.property = Object.assign({}, MyManagedClassDefaultProperty, p);
    }
  }
  clone() {
    return new MyManagedClass(this.property);
  }
  merge(p, deep = true) {
    // 1. コピー先のベースとなるプロパティを決定する
    let baseProperty;
    if (deep) {
      // ディープコピーしてからマージするために、まず自身のpropertyをディープコピーする
      baseProperty = recursiveClone(this.property);
    
      // 2. 新しいプロパティオブジェクト (baseProperty) に新しいデータ (p) をディープマージする
      //    (simpleDeepMergeがターゲットをインプレイスで更新すると仮定)
      simpleDeepMerge(baseProperty, p); 
    } else {
      // シャローマージ: Object.assignでthis.propertyのシャローコピーにpのプロパティを上書き
      baseProperty = Object.assign({}, this.property, p);
    }
    // 3. 不変性を維持するため、新しいプロパティを持つ新しいインスタンスを返す
    return new MyManagedClass(baseProperty, deep);
  }
  isThisType(rh){
    return rh instanceof MyManagedClass; 
  }
  toString(){
    try {
      // JSON.stringify(データ, リプレイサー, スペース数)
      // スペース数に「2」を指定することで、JSONは2スペースでインデントされます。
      const jsonString = JSON.stringify(this.property, null, 2); 
        
      // JSON文字列の各行の先頭に、カスタムヘッダーに合わせたインデント(ここでは2スペース)を追加
      // 最初の中括弧{は除く
      const indentedJson = jsonString.split('\n')
                                     .map((line, index) => (index > 0 ? '  ' : '') + line)
                                     .join('\n');
        
      // ヘッダーと整形されたJSONを結合
      if(this.property.variablename) return `MyManagedClass Instance (${this.property.variablename}) {\n  "property": ${indentedJson}\n}`;
      else return `MyManagedClass Instance {\n  "property": ${indentedJson}\n}`;
    } catch (e) {
      // 循環参照などが含まれる場合の処理
      return `MyManagedClass Instance [Serialization Error: ${e.message}]`;
    }
  }

  //ここにそのほかのメソッドを記述

}

function enter5(){

  const data = new MyManagedClass(taroData);

  const elm = document.getElementById('TDATA');
  elm.value = data.toString();
  console.log(data.toString());
/*
MyManagedClass Instance (taroData) {
  "property": {
    "variablename": "taroData",
    "profile": {
      "name": "Taro",
      "age": 25,
      "nation": "Japan",
      "items": {
        "0": "ball",
        "1": "glove"
      }
    },
    "settings": {
      "theme": "red",
      "notifications": true
    }
  }
}
*/

  const MyManagedClassMap = {
    leader: new MyManagedClass(taroData),
    member: {
      subleader: new MyManagedClass(jiroData),
      bench: new MyManagedClass(zakoData)
    }
  };

  const clonedMM = recursiveClone(MyManagedClassMap);
  const tmpM = MyManagedClassMap.leader.clone();
  MyManagedClassMap.leader = MyManagedClassMap.member.bench.clone();
  MyManagedClassMap.member.bench = tmpM.clone();

  console.log(MyManagedClassMap);
/*
  const MyManagedClassMap = {
    leader: new MyManagedClass(zakoData),
    member: {
      subleader: new MyManagedClass(jiroData),
      bench: new MyManagedClass(taroData)
    }
  };
*/
  console.log(clonedMM);
/*
  const clonedPM = {
    leader: new MyManagedClass(taroData),
    member: {
      subleader: new MyManagedClass(jiroData),
      bench: new MyManagedClass(zakoData)
    }
  };
*/

}


とこのようにstructuredClone(data)でディープコピーしたhageは
data[1]["nation"] = "japan";
の影響を受けずにその項目がなくdataとhageはお互い独立しています
var hoge = data;は正確にはシャローコピーではなくもっと密接な参照の代入ですが
似たような話です
またconsole.logについて
オブジェクトの参照が保持され、展開時に最新の値が評価される場合があるので、後に変更した
numdata["value"] = 666;
も反映されてしまいますが、
JSON.stringifyはその時点のオブジェクトをシリアライズして文字列に変換するために
{"value":1}と正確に表示されます

ですのでhoge = data[i];
としたいときにシャローコピーの罠を回避するために
function getArrayElement(ary, i){
 return structuredClone(ary[i]);
}
hoge = getArrayElement(data, i);
とすると余計なバグに悩まされなくなります

structuredCloneはメソッドはコピーしてくれないので
クラスに応用するのならば
constructor(p = defprp) {
 this.property = structuredClone(p);
}
コピーコンストラクタを利用します
しかしこの方法でも適切にマージ(統合)ができません

またjQueryのディープマージ機能を使うならば
Person2,enter3のようになります
taro.property = {
 profile: { age: 25, items: {0: 'ball', 1: 'glove'}, name: "Taro", nation: "Japan" },
 settings: { notifications: true, theme: "red" }
};
デフォルトプロパティに比べてマージされて
taro.property.profile.name,taro.property.settings.themeが更新されて
taro.property.profile.items,taro.property.profile.nationが追加されて
taro.property.profile.age,taro.property.settings.notificationsの
デフォルト設定が残りました

あと3JSのこのような記述は
const backuniforms = THREE.UniformsUtils.clone( shader.uniforms );
backuniforms[ 'Threshold' ].value = 0.1;
backuniforms[ 'Fill' ].value = true;
backuniforms[ 'Color' ].value = new THREE.Vector4(0.2, 0.6, 0.6, 1.0);
backuniforms[ 'Sampler' ].value = tex;
backuniforms[ 'Opacity' ].value = 0.5;
backuniforms[ 'uDirLightPos' ].value = dlight.position;
backuniforms[ 'uDirLightColor' ].value = new THREE.Vector4(dlight.color.r, dlight.color.g, dlight.color.b, 1.0);
backuniforms[ 'uDirLightIntensity' ].value = 2.0;
backuniforms[ 'uAmbientLightColor' ].value = new THREE.Vector4(alight.color.r, alight.color.g, alight.color.b, 1.0);

このように書くとエレガントになります

const newValues = {
 Threshold: 0.1,
 Fill: true,
 Color: new THREE.Vector4(0.2, 0.6, 0.6, 1.0),
 Sampler: tex,
 Opacity: 0.5,
 uDirLightPos: dlight.position,
 uDirLightColor: new THREE.Vector4(dlight.color.r, dlight.color.g, dlight.color.b, 1.0),
 uDirLightIntensity: 2.0,
 uAmbientLightColor: new THREE.Vector4(alight.color.r, alight.color.g, alight.color.b, 1.0)
};
// 1. まず元のUniformsをディープコピー(Three.jsの方法で)
const backuniforms = THREE.UniformsUtils.clone(shader.uniforms);
// 2. newValuesオブジェクトをループし、backuniformsの対応する.valueを更新
for (const key in newValues) {
 if (backuniforms[key] && backuniforms[key].hasOwnProperty('value')) {
  backuniforms[key].value = newValues[key];
 }
}

enter4ではfunction recursiveClone(item)を使って
抽象化されたディープコピーを行っています
最終的に
nestedHoge[1][0]に'changed'
clonedHage[1][0]に'a'
が入っておりネストのディープコピーが成功したようです
personMapは最終的にリーダーとベンチが入れ替わっていて
clonedPMはクローンしたときの状態が保全されています

Javascriptによるマネージドクラスの模倣


/**
//################################################################################################################################
 * MyManagedClass : マネージドクラスの模倣クラスのクラステンプレート
 * MyManagedClass.property にクラスのデータを一括管理させる
 * ディープ/シャローマージ対応
 * マージするときに配列をオブジェクトに自動変換します(キーの衝突/結合の複雑さの回避)
 * 配列にしたい場合はfunction toArrayIfIndexed(obj, force = false, sparseHandling = 'keep')を
 * 逐次利用してください
 * 注意:MyManagedClass.property の要素にMyManagedClass自身を含めると循環参照の危険があり避けてください
 */

/**
 * MyManagedClassの結果はオブジェクトとして返されるが、配列型が必要な場合はtoArrayIfIndexedを使用。
 * 例: const items = toArrayIfIndexed(instance.property.profile.items, true);
 */
/**
 * MyManagedClassは配列をオブジェクトとして処理します。
 * 配列型が必要な場合、toArrayIfIndexedを使用して変換してください。
 * 例:
 * const instance = new MyManagedClass({ items: [{ id: 1 }, { id: 2 }] });
 * const itemsArray = toArrayIfIndexed(instance.property.items, true); // [{ id: 1 }, { id: 2 }]
// 配列プロパティの取得
const instance = new MyManagedClass({ profile: { items: [{ id: 1 }, { id: 2 }] } });
const items = toArrayIfIndexed(instance.property.profile.items, true);
items.forEach(item => console.log(item.id)); // 1, 2

// トップレベルが配列
const arrayInstance = new MyManagedClass([{ id: 1 }, { id: 2 }]);
const array = toArrayIfIndexed(arrayInstance.property, true);
console.log(array); // [{ id: 1 }, { id: 2 }] */
/**
 * 数字文字列のキーを持つオブジェクトを配列に整形する
 * @param {Object} obj - 変換対象のオブジェクト
 * @param {boolean} [force=false] - キーが数字文字列でない場合も強制的に配列に変換
 * @param {String} [sparseHandling = 'keep'] - 空白配列のコンパクト化の選択
 * @returns {Array|Object} 配列(変換可能な場合)または元のオブジェクト
 */
function toArrayIfIndexed(obj, force = false, sparseHandling = 'keep') {
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
  const keys = Object.keys(obj);
  const isIndexed = keys.every((key, i) => String(i) === key);
  if (!isIndexed && !force && process.env.NODE_ENV !== 'production') {
    console.warn('Non-sequential keys detected; consider using force=true or sparseHandling="compact"');
  }
  const maxIndex = keys.length > 0 ? Math.max(...keys.map(Number)) + 1 : 0;
  const result = new Array(maxIndex);
  for (const key of keys) {
    result[Number(key)] = obj[key];
  }
  if (sparseHandling === 'compact') {
    return result.filter(x => x !== undefined);
  }
  return result;
}

// フォールバック用の簡易クローン関数
function fallbackClone(item, seen = new WeakSet()) {
  if (item === null || typeof item !== 'object') return item;
  if (seen.has(item)) {
    throw new Error('Circular reference detected in fallbackClone');
  }
  seen.add(item);
  if (typeof item.clone === 'function') {
    return item.clone();
  }
  if (Array.isArray(item)) return item.map(item => fallbackClone(item, seen));
  if (item instanceof Date) return new Date(item);
  if (item instanceof Map) return new Map(fallbackClone([...item], seen));
  if (item instanceof Set) return new Set(fallbackClone([...item], seen));
  if (item instanceof RegExp) return new RegExp(item);
  const cloned = {};
  for (const key in item) {
    cloned[key] = fallbackClone(item[key], seen);
  }
  return cloned;
}

// 安全なstructuredCloneラッパー
function safeStructuredClone(item) {
  try {
    return structuredClone(item);
  } catch (e) {
    return fallbackClone(item);
  }
}

function recursiveClone(item, seen = new WeakSet()) {
  // 循環参照のチェック
  if (item && typeof item === 'object' && seen.has(item)) {
    throw new Error('Circular reference detected');
  }
  if (item && typeof item === 'object') {
    seen.add(item);
  }

  // 1. clone() メソッドを持つ場合
  if (item && typeof item.clone === 'function') {
    return item.clone();
  }

  // 2. 配列の場合
  if (Array.isArray(item)) {
    return item.map(element => recursiveClone(element, seen));
  }

  // 3. プレーンなオブジェクトの場合
  if (item && typeof item === 'object' && item.constructor === Object) {
    const clonedObject = {};
    for (const key in item) {
      clonedObject[key] = recursiveClone(item[key], seen);
    }
    return clonedObject;
  }

  // 4. その他(プリミティブ値など)
  return safeStructuredClone(item);
}

function simpleDeepMerge(target, source, seen = new WeakSet(), deep = true) {
    if (!deep) {
        return Object.assign({}, target, source);
    }    
    // ターゲットが有効なオブジェクトであることを確認
    // ターゲットがオブジェクトでなければ、ソースをディープコピーして返す(上書き)
    if (target === null || typeof target !== 'object') {
        // ターゲットが無効な場合、処理を継続する意味がないので、ソースをそのまま返すか、safeStructuredCloneを使う
        return safeStructuredClone(source); 
    }
    
    // ソースが有効なオブジェクトであることを確認
    if (source === null || typeof source !== 'object') {
        return target; // マージするものがないため、ターゲットをそのまま返す
    }
    
    // 1. 循環参照チェックと登録
    // この層での処理が確定したら登録
    if (seen.has(target) || seen.has(source)) {
        throw new Error('Circular reference detected');
    }
    seen.add(target);
    seen.add(source);

    // 配列をオブジェクトに変換するヘルパー関数
    const arrayToObject = arr => ({ ...arr });


    if (Array.isArray(target) && process.env.NODE_ENV !== 'production') {
      console.warn('Target array converted to object in simpleDeepMerge. Use toArrayIfIndexed to convert back.');
    }


    // 配列処理の統一: 配列はすべてインデックス付きのプレーンなオブジェクトとして扱う
    if (Array.isArray(target)) {
        // targetはマージのターゲットであり、型を維持するため、新しいオブジェクトに変換
        // 既存の target の内容を維持しつつ、オブジェクトとしてマージするために、新しいオブジェクトを作る
        target = Object.assign({}, target); 
    }
    if (Array.isArray(source)) {
        source = arrayToObject(source);
    }
    
    // 2. マージロジック
    for (const key in source) {
        const sourceValue = source[key];
        const targetValue = target[key];

        // ターゲットの値がオブジェクトで、ソースの値もオブジェクトの場合にのみ再帰する
        if (sourceValue && typeof sourceValue === 'object' && sourceValue.constructor === Object) {
            
            // ターゲットの値がオブジェクトでなければ、新しい空のオブジェクトで初期化
            if (!targetValue || typeof targetValue !== 'object' || Array.isArray(targetValue)) {
                 target[key] = {}; 
            }
            // 配列をオブジェクトとして扱うため、ここでは Array.isArray(targetValue) のチェックは削除しても良いが、
            // ターゲットが配列として存在していた場合は既に Object.assign({}, target) でオブジェクトになっているはず
            
            simpleDeepMerge(target[key], sourceValue, seen); // 再帰

        } else if (typeof sourceValue?.clone === 'function') {
            // カスタムクラスは clone() で上書き
            target[key] = sourceValue.clone();
            
        } else {
            // プリミティブ、その他のオブジェクトは safeStructuredClone でディープコピーして上書き
            target[key] = safeStructuredClone(sourceValue);
        }
    }
    
    return target;
}

const MyManagedClassDefaultProperty = Object.freeze({
  variablename: "MyManagedClassDefaultProperty",
  profile: Object.freeze({ name: "Default Name", age: 25 }),
  settings: Object.freeze({ theme: "light", notifications: true })
});

class MyManagedClass {
  constructor(p, deep = true) {
    if(deep) {
      const target = recursiveClone(MyManagedClassDefaultProperty);
      this.property = simpleDeepMerge(target, p);
    }
    else {
      this.property = Object.assign({}, MyManagedClassDefaultProperty, p);
    }
  }
  clone() {
    return new MyManagedClass(this.property);
  }
  merge(p, deep = true) {
    // 1. コピー先のベースとなるプロパティを決定する
    let baseProperty;
    if (deep) {
      // ディープコピーしてからマージするために、まず自身のpropertyをディープコピーする
      baseProperty = recursiveClone(this.property);
    
      // 2. 新しいプロパティオブジェクト (baseProperty) に新しいデータ (p) をディープマージする
      //    (simpleDeepMergeがターゲットをインプレイスで更新すると仮定)
      simpleDeepMerge(baseProperty, p); 
    } else {
      // シャローマージ: Object.assignでthis.propertyのシャローコピーにpのプロパティを上書き
      baseProperty = Object.assign({}, this.property, p);
    }
    // 3. 不変性を維持するため、新しいプロパティを持つ新しいインスタンスを返す
    return new MyManagedClass(baseProperty, deep);
  }
  isThisType(rh){
    return rh instanceof MyManagedClass; 
  }
  toString(){
    try {
      // JSON.stringify(データ, リプレイサー, スペース数)
      // スペース数に「2」を指定することで、JSONは2スペースでインデントされます。
      const jsonString = JSON.stringify(this.property, null, 2); 
        
      // JSON文字列の各行の先頭に、カスタムヘッダーに合わせたインデント(ここでは2スペース)を追加
      // 最初の中括弧{は除く
      const indentedJson = jsonString.split('\n')
                                     .map((line, index) => (index > 0 ? '  ' : '') + line)
                                     .join('\n');
        
      // ヘッダーと整形されたJSONを結合
      if(this.property.variablename) return `MyManagedClass Instance (${this.property.variablename}) {\n  "property": ${indentedJson}\n}`;
      else return `MyManagedClass Instance {\n  "property": ${indentedJson}\n}`;
    } catch (e) {
      // 循環参照などが含まれる場合の処理
      return `MyManagedClass Instance [Serialization Error: ${e.message}]`;
    }
  }

  //ここにそのほかのメソッドを記述

}

const taroData = {
  variablename: "taroData",
  profile: { name: "Taro", nation: "Japan", items: {0: "ball", 1: "glove"} },
  settings: { theme: "red" }
};

const jiroData = {
  variablename: "jiroData",
  profile: { name: "Jiro", nation: "Japan" },
  settings: { theme: "dark" }
};

const zakoData = {
  variablename: "zakoData",
  profile: { name: "Zako", nation: "Japan" },
  settings: { theme: "blue" }
};

function enter5(){

  const data = new MyManagedClass(taroData);

  const elm = document.getElementById('TDATA');
  elm.value = data.toString();
  console.log(data.toString());
/*
MyManagedClass Instance (taroData) {
  "property": {
    "variablename": "taroData",
    "profile": {
      "name": "Taro",
      "age": 25,
      "nation": "Japan",
      "items": {
        "0": "ball",
        "1": "glove"
      }
    },
    "settings": {
      "theme": "red",
      "notifications": true
    }
  }
}
*/

  const MyManagedClassMap = {
    leader: new MyManagedClass(taroData),
    member: {
      subleader: new MyManagedClass(jiroData),
      bench: new MyManagedClass(zakoData)
    }
  };

  const clonedMM = recursiveClone(MyManagedClassMap);
  const tmpM = MyManagedClassMap.leader.clone();
  MyManagedClassMap.leader = MyManagedClassMap.member.bench.clone();
  MyManagedClassMap.member.bench = tmpM.clone();

  console.log(MyManagedClassMap);
/*
  const MyManagedClassMap = {
    leader: new MyManagedClass(zakoData),
    member: {
      subleader: new MyManagedClass(jiroData),
      bench: new MyManagedClass(taroData)
    }
  };
*/
  console.log(clonedMM);
/*
  const clonedPM = {
    leader: new MyManagedClass(taroData),
    member: {
      subleader: new MyManagedClass(jiroData),
      bench: new MyManagedClass(zakoData)
    }
  };
*/

}



【自己管理型オブジェクト設計】

本モデルは、JavaScriptで型安全なディープコピーと
不変性の制御を実現するデザインパターンです。

## 1. マネージドクラスとしての役割 (MyManagedClass)

MyManagedClass は、自身のデータ(状態)をデフォルト値に
基づいて一元管理する「マネージドクラス」として機能します。

・カプセル化と初期化: コンストラクタ内で カスタム関数 の simpleDeepMerge() を利用し
 デフォルト値と入力データを確実にディープ/シャローマージして this.property を初期化します。
 これにより、外部からの不正な変更や参照共有(シャローコピー)を防ぎ
 オブジェクトの健全性を保ちます。

・状態の更新: merge() メソッドもディープ/シャローマージを実行するため、既存のインスタンスの状態を安全かつ確実に更新できます。

## 2. 型を維持するディープコピー戦略 (.clone() メソッド)

外部からコピーが要求された際、クラス自身が clone() メソッドを通じてコピー方法を制御します。

・型安全性の保証: clone() は必ず new MyManagedClass(...) を実行することで
 コピー後のオブジェクトが元のクラスの型情報(プロトタイプ)と
 すべてのメソッドを保持することを保証します。

・ディープコピーの委譲: 外部の汎用ユーティリティに頼るのではなく、自身が内部データ (this.property)
 を使って新しいインスタンスを作成するため、データ構造が複雑化しても高い保守性を維持できます。

## 3. 汎用的な構造走査ユーティリティ (recursiveClone)

recursiveClone 関数は、クラスに依存しないユニバーサルな処理層を提供します。

・処理の優先順位: 処理の最上位で clone() メソッドの有無を確認することで
 カスタムオブジェクトのディープコピーロジック(クラス自身によるディープコピー)を最優先します。

・汎用性と再帰性: 配列やプレーンオブジェクト(連想配列)のネスト構造を再帰的に走査し
 すべての要素を処理します。最下層のプリミティブ値は structuredClone で確実にコピーされます。

結論: この関数は、MyManagedClass のインスタンスや、ネストされた配列
他のプレーンオブジェクトが混在する複雑なデータ構造全体を
型情報を保持しつつ安全にディープコピーするための抽象的なエントリーポイントとして機能します。

## 4. データ構造の制御された正規化 (toArrayIfIndexed 関数)

・利用戦略の原則:予測可能性の最大化
recursiveCloneは、内部で利用されるsimpleDeepMergeの仕様により
ArrayがObjectへ自動変換されたデータ構造を受け取ります。しかし、toArrayIfIndexedを自動実行しないことで
ディープコピー処理における意図しない再変換(ObjectからArrayへの再成形)を排除します。
これにより、設計全体の予測可能性と副作用の抑制を最大化します。

・責務と機能の分離
recursiveCloneの責務: simpleDeepMergeによる変換後の現在のデータ構造(Object)
を忠実に複製・保持するという核となる責務に専念します。

toArrayIfIndexedの責務: ユーザーがデータを配列として利用したいと明示的に決定した際にのみ
forceやsparseHandlingオプションに基づいて、複製されたObjectを配列へ再成形(正規化)するという二次的な処理を担います。

結論: このモデルは、simpleDeepMergeによる初期変換後の構造(Object)を厳密にコピーしつつ
ユーザー制御下のtoArrayIfIndexedによってのみ配列への再成形を許可することで、データの不変性と
構造変更のタイミングと結果の透明性を両立させます。


最終的な結論
このモデルは、「オブジェクト自身にコピーの責任を負わせる (.clone())」というデザインパターンを実装しており
Undo/Redo機能や履歴管理など、データの不変性が不可欠なアプリケーション開発において、非常に堅牢で拡張性の高い基盤となります。


注意:MyManagedClass.property内にMyManagedClass自身を含ませると

  :循環参照になるので避けてください


jQueryとの依存関係は解消して独立に構築できるようになりました









 ■■■ 3Dプログラミング入門講座 ■■■ 
NAS6LIB
ポリゴンテスト解説・x3dom使い方
その1:ベクトル
その2:行列
その3:クォータニオン
その4:基本総復習・実践
その5:応用その1・メッシュアニメーション、動的テクスチャ
その6:応用その2・GLSL、カスタムシェーダー、キーボード、ファイル
その7:応用その3・ゲームプログラミング、タグの動的追加
その8:応用その4・GLSL、シェーダー、その2
その9:物理演算その1・電卓で相対性理論を解く方法
その10:物理演算その2・相対性理論的ニュートン力学
その11:物理演算その3・ケプラー方程式で惑星軌道シミュレーターを作る

その12:物理演算その4・ルンゲクッタ法で作った 相対性理論的ニュートン力学物理エンジンで惑星軌道シミュレーターを作る

その13:経路探索(A*:A-STAR)&巡回セールスマン問題 : 巨大サイズ : くろにゃんこ

その14:プログラミングにおける配列テーブルテクニック
その15:javascriptのクラス活用法
その16:透視射影公式テスト

その17:ケプラー方程式カプセルライブラリ使用法
その18:CSVファイル処理
その19:物理演算その5・重力多体問題
その20:同次座標について(3D座標系の基本の基本)
その21:おさらいコモンクラスの宣言
その22:物理エンジンライブラリ解説(ケプラー方程式・ルンゲクッタ・相対論的万有引力)
その23:サイト内のリンク先の更新確認がjavascriptで出来るようにする方法
その24:グレゴリオ暦日付計算
その25:シャローコピーディープコピーからマネージドクラスの模倣


 ■■■ THREE.JSプログラミング講座 ■■■ 
THREE.JSテスト解説・THREE.JS使い方
THREE.JS examplesをいじってみた(フレネル反射透過シェーダー)

THREE.JS (半透明シェーダー)

THREE.JS 3D演算で必要な計算(具体例)★とても重要★
THREE.JS THREE-VRM をいじってみた



<<prev グレゴリオ暦日付計算 : 工事中 next>>






戻る