Wonderful! WordPress

JavaScript の object の copy で忘れないように

JavaScript の object のコピーは参照渡し

またしてもいまさらながら、JavaScript のオブジェクトのコピーは参照渡しだということは、忘れようもないことなのだけれど。

JavaScript の場合、文字列、数値、真偽値、null などはそれ以上単純化されないシンプルな値としてプリミティブ値とされ、そうじゃない、配列、連想配列、Date()、Function() などなど値を複数持つことができるものはみんなオブジェクトということ。ただし、nulltypeofobject が出てくるんだな、これが。

で、そのオブジェクトを別の変数に代入したりなんかして、いわゆるコピーした場合には、そのコピーは参照渡しであるということは忘れてはいけないこと、というか忘れようもないことなのだけれど。
参照渡しというのは、実際の値が複製されるのではなく、そのオブジェクトへのアドレスがコピーされるということで、要はコピーされた変数が複数あっても、それらが紐付けられているメモリ上の対象は実は同じ一つのもの、ということだと。

まぁ、これは実際にやってみれば話が早くわかりやすいのである。
多次元の連想配列をつくって、それを別の変数へとコピーし、元の変数と複製された変数、それぞれに要素の値を変更してみると。
結果は下記のごとく当然のことながら、元々同じ一つのものに紐付けられているのだから、両方とも同じように変更されて同じ内容を表示するということになる。

const obja = {
	a:'a',
	b:'b',
	c:{
		d:'d',
		e: {
			f: 'f',
			g: 'g'
		}
	}
},
objb = obja;

obja.b = 'z';
objb.c.d = 'x';

console.log( 'obja:', obja );
/*obja:
{
    a: "a",
    b: "z",
    c: {
        d: "x",
        e: {
            f: "f",
            g: "g"
        }
    }
}*/
console.log( 'objb:', objb );
/*objb:
{
    a: "a",
    b: "z",
    c: {
        d: "x",
        e: {
            f: "f",
            g: "g"
        }
    }
}*/
JavaScript
CopyExpand

でもって、これは連想配列に限ったことではなく、シンプルな配列でも全く同じ。同じオブジェクトなのだから。

配列のコピーにおける slice() と concat()

それなら、参照渡しではなく、値渡しによる完全な複製を作りたい場合はどうするかということになる。
とりあえず配列だけに限ってということであれば、すぐに slice()concat() にたどり着く。

const elema = [ 'a', 'b', 'c', 'd', 'e', 'g', 'h', 'i', 'k', 'j' ];

let elemb = elema.slice();

elema[1] = 'z';
elemb[7] = 'x';

console.log( elema );
// 出力 [ "a", "z", "c", "d", "e", "g", "h", "i", "k", "j" ]    
console.log( elemb );
// 出力 [ "a", "b", "c", "d", "e", "g", "h", "x", "k", "j" ]

// concat() の場合も結果は同じ
elemb = elema.concat();
JavaScript
CopyExpand

と、いうことで、slice() concat() も希望通りの結果が得られる。
だが、しかし、これは一次元の配列とその要素に連想配列を含まない場合だけに限ってのことである。

MDN Web Docsslice()concat() のページを見ると、それら両方のページに下記のごとく同様の説明書きがある。

「元の配列を変更せず、元の配列から要素をシャローコピー (1 段階の深さのコピー) した新しい配列を返します。元の配列の要素は以下のように返される配列にコピーされます。
(実際のオブジェクトではない) オブジェクトの参照については、オブジェクトの参照を新しい配列にコピーします。元の配列も新しい配列も同じオブジェクトを参照します。参照されたオブジェクトが修正された場合、その変更は新しい配列と元の配列の両方に現れます。」

要は、完全な値渡しの複製としてコピーされるのは一次元のプリミティブな値だけということになる。さっそくやってみる。

let elema = [ 'a', 'b', { c: 'c', d: 'd' }, 'e', 'f', 'g', [ 'h', 'i', 'j' ] ];

let elemb = elema.slice();

elema[1] = 'y';
elema[2].c = 'z';

elemb[4] = 'x';
elemb[6][1] = 'w';

console.log( elema );
// 出力 [ "a", "y", { "c": "z", "d": "d" }, "e", "f", "g", [ "h", "w", "j" ] ]
console.log( elemb );
// 出力 [ "a", "b", { "c": "z", "d": "d" }, "e", "x", "g", [ "h", "w", "j" ] ]

// elemb = elema.concat() の結果

// elema:[ "a", "y", { "c": "z", "d": "d" }, "e", "f", "g", [ "h", "w", "j" ] ]
// elemb:[ "a", "b", { "c": "z", "d": "d" }, "e", "x", "g", [ "h", "w", "j" ] ]

// elemb = Array.from( elema ) の結果

// elema:[ "a", "y", { "c": "z", "d": "d" }, "e", "f", "g", [ "h", "w", "j" ] ]
// elemb:[ "a", "b", { "c": "z", "d": "d" }, "e", "x", "g", [ "h", "w", "j" ] ]
JavaScript
CopyExpand

一次元のプリミティブな値はそれぞれ独立しているが、一次元?の連想配列と二次元の配列の要素は共に連動して変更されてしまっている。
ちなみにもう一つ Array.from() を使う方法もあるとのことで、同じくして試してみたが、結果は3つとも全く同様だった。と、いうかこれはどれも内部的には同じことをやらせているのかもしれない。
で、当然のことながらこれら、slice()concat()Array.from() はもとより連想配列にはつかえない。

スプレッド構文(…)、Object.assign() ではいかに?

たしかスプレッド構文が使えるのは ES6 からだったと思うけれど、これでも配列のコピーはできたはず。ちなみにこれは連想配列でも使える。
が、MDN Web Docs のページを読んでいくと、「浅いコピー (プロトタイプを除く) の作成や・・・」という記述が目に入ってくる。

const obja = {
	a:'a',
	b:'b',
	c:{
		d:'d',
		e: {
			f: 'f',
			g: 'g'
		}
	}
},
objb = { ...obja },

obja.b = 'w';
obja.c.e.g = 'x';
objb.c.d = 'y';

console.log( obja );
/*
{
    a: "a",
    b: "w",
    c: {
        d: "y",
        e: {
            f: "f",
            g: "x"
        }
    }
}
*/
console.log( objb );
/*
{
    a: "a",
    b: "b",
    c: {
        d: "y",
        e: {
            f: "f",
            g: "x"
        }
    }
}
*/

let elema = [ 'a', 'b', { c: 'c', d: 'd' }, 'e', 'f', 'g', [ 'h', 'i', 'j' ] ];

let elemb = [ ...elema ];

elema[1] = 'y';
elema[2].c = 'z';
elemb[4] = 'x';
elemb[6][1] = 'w';

console.log( elema );
// 出力  [ "a", "y", { "c": "z", "d": "d" }, "e", "f", "g", [ "h", "w", "j" ] ]
console.log( elemb );
// 出力 [ "a", "b", { "c": "z", "d": "d" }, "e", "x", "g", [ "h", "w", "j" ] ]
JavaScript
CopyExpand

と、いうことでこれまた同じ結果ばかり続き、どちらにしても浅いコピーということになる。
そしてもう一つ、両方ともに使えるもので Object.assign({}, object ) がある。けれど、これもかのページの説明にも書いてあるけれど、やはり浅いシャローコピーとのことで、もしや!と思って試してはみたけれど、得られたのは全く同じ結果だけだった。おつかれさん。

JSON.parse( JSON.stringify()) か JQuery か lodash か

まぁ、これまでそんなに多次元の配列やらをコピーして使うということもなかったとは思うものの、いざ、できないの!ってことになるとなんとかそれが出来る方法を見つけたくなる。jQuery やら lodash なるライブラリを使えば出来るとのことなんだけれど、あまり外部のものをロードして使うというのは気が進まない。そうなると、選択肢はもう、JSON.parse( JSON.stringify()) しかなくなってしまうようだ。それはそれで良いのだけれど、なんだか見るからに重そうな感じがするし、若干、問題もあるとのことで。

ちなみに、Firefox においては、ver. 94 から、Chrome では ver.98 から structuredClone() というそれ用の関数を備えているそうで、試してみるとたしかに使えた。が、この記事を書いている 2021年12月 の時点では、それが使えるのは、Firefox だけである。

それにしても、現状において標準でそれができる関数があたりまえになっていないというのが、なんとも不思議な感じがする。必要とされてこなかったのだろうな~と。
まぁ、それが無いのだったら、いっそ自分で作ってしまおうかと思い、なんとかそれなりのものが出来たように思う。
基本的に、JSON.parse( JSON.stringify()) よりも速く動作しなければ意味がないので、そのあたりでちょっと時間も手間もかかってしまった。
なんてことはなく、渡されたデータが配列か連想配列かで分岐し、それぞれループさせ、その中でまた配列か連想配列が出てきたら再起させるというだけのもの。自分でいくつかテストした分においては、ちゃんと機能していたけれど、テスト数が少ないのでなにかしら不具合があるのかもしれない。

渡されたデータが配列であるか連想配列であるかを判定するのに、最もやりやすいのは Object.prototype.toString.call() を使う方法なのだろうと思う。これなら、typeof のようにどれもこれも object ではなく、配列なら [object Array] で連想配列なら [object Object] だし、Date なら [object Date] で、null なら [object Null] と、そんな具合にそれぞれ完全に判定することができる。ただ、実際にやってみたところ、これがなかなか重くなってしまう。ベンチで計測してみると、案の定、JSON.parse( JSON.stringify()) よりも鈍重になってしまったほど。

これでは自作する意味がなくなってしまうので、と、なんとかあれこれと調べては試してみる。
結果、この constructor を使う方法が一番軽くすむよう。実際に、渡す配列なり連想配列なりに、オブジェクトである、Date()、Function() などの、配列と連想配列以外のオブジェクトが含まれないと保証できる場合に限っては、typeof を使えば圧倒的に速くなった。まぁ、そこのところは、自分で使う分には状況によって使い分ければ済むことである。

しかし、それにしても思ったよりも簡単なものが出来てしまった。こんな簡単なものなら標準で備えていないわけがないと思われるので、やはりこれだと何かしら不足があるのだろうな~?と、いうわけで自分で使う分ならという前提のもの。

function clone_deep_obj( obj ) {

    if ( obj && ( Array === obj.constructor || Object === obj.constructor ) ) { 
        const okeys = Object.keys( obj ),
            okeys_len = okeys.length;

        let ans;

        if ( Array.isArray( obj ) ) {
            ans = [];

            for ( let i = 0; i < okeys_len; ++i ) {

                if ( obj[ okeys[ i ] ] && ( Array === obj[ okeys[ i ] ].constructor || Object === obj[ okeys[ i ] ].constructor ) ) {
                    ans.push( clone_deep_obj( obj[ okeys[ i ] ] ) );
                } else {
                    ans.push( obj[ okeys[ i ] ] );
                }
            }
        } else {
            ans = new Object();

            for ( let i = 0; i < okeys_len; ++i ) {

                if ( obj[ okeys[ i ] ] && ( Array === obj[ okeys[ i ] ].constructor || Object === obj[ okeys[ i ] ].constructor ) ) {
                    ans[ okeys[ i ] ] = clone_deep_obj( obj[ okeys[ i ] ] );
                } else {
                    ans[ okeys[ i ] ] = obj[ okeys[ i ] ];
                }
            }
        }
        return ans;
	} else {
        console.log( 'Target is not a Object or a Array.' );
		return false;
	}
}
JavaScript
CopyExpand

で、ちなみにベンチマークの結果はこんな感じ。それぞれ10万回のループ。きちんと回数を決めて計測して平均をだしたわけではないけれど、だいたいいつもこんな数値が出ていた。なお、使ったデータはというと、4次元まであり要素として配列を含む少し複雑な連想配列、シンプルな連想配列とそしてシンプルな配列の三つ。以下に示した。ちなみに、それぞれの配列に要素として含まれている変数 num はループにより変化させた。

let num = 0,
    objt = {
        a:'a',
        b: num,
        c:{
            d:'d',
            e: { 
                j: {
                    n: { p: 'rr', q: 'sss' },
                    k: 't',
                    l: [ 'uu', 'vvvv', 'www' ]
                },
                m: 'oooo'
            }
        },
        f: [ 'g', num, 'i' ]
    },
    objs = {
        a: 'a',
        b: num,
        c: 'defgh',
        d: [ 0, 1, 2 ],
    },
    arya = [ 'a', num, 'b', 'cdefg' ];

let start,
    end,
    elapsed,
    elapsedStr;

start = performance.now();

for( let i = 0; i < 100000; ++i ) {
    num = i;
    const copyobj = clone_deep_obj( objt );
}

end = performance.now();

elapsed = ( end - start );
elapsedStr = elapsed.toPrecision(3);
console.log( `clone_deep_obj: ${elapsedStr}` );
JavaScript
CopyExpand
clone_deep_objJSON.parse.stringifystructuredClone
Firefoxobjt240317519
Chromeobjt219346616
Firefoxobjs50.2134262
Chromeobjs86.8141
Firefoxarya20.850.4184
Chromearya40.477.0

やはりというかあたりまえなんだろうけれど、連想配列は重いな~。
Chrome における structuredClone の値は、Chrome DEV ver.99 での値。

オブジェクトの場合の === 同値演算子

あと、もう一つ大事なことを忘れてはいけない。
オブジェクトの場合、同値演算子 === での判定においては、配列、連想配列にかかわらず、同じ参照アドレスを持っているもの同士ではないと true とはならない。たとえ、含んでいる要素が全く同じでも参照しているアドレスが異なれば false となる。

const objy = { a:1 },
    objx = objy,
    objw = { a:1 },
    aryc = [1],
    aryd = [1],
    arye = aryc;

console.log( objx === objy ); // true

console.log( objw === objy ); // false

console.log( arye === aryc ); // true

console.log( aryd === aryc ); // false
JavaScript
CopyExpand

全く同じ値を持っている同士でも、false になってしまう感覚というのが、ちょっと妙な感じもするけれど、他の言語においてもこういうことはあるのだろうか。まぁ、php だとそんなことはないな。python はどうだったか。

Leave a Reply!

JavaScript is necessary to send a comment.
You can edit and delete your comment if you input a edit key.
Edit key is necessary for attesting you when you edit and delete it.
The tag of HTML cannot be used in comment.
When you comment for the first time, it is displayed after the approval of the administrator.
Because I cannot speak English so much, it takes time to answer.
Required fields are marked *.

※Please enter more than 5 characters only alphabets.
※Edit or delete are possible for 2000 days after approval.

*

♠Simplistic Comment User Editable v4.0

♠When visitors leave comments on the site this site collect the data shown in the comments form, and also the visitor’s IP address and browser user agent string to help spam detection.
♠This site does not use cookie when visitors leave comments and commenter edit comment.
♠This site uses Akismet to reduce spam. Learn how your comment data is processed.

Comments feed

Trackback URL : https://strix.main.jp/wp-trackback.php?p=169389

Sanbanse Funabashi
2010.10.24 sunrise

Top

スクロールさせるか画像をクリックすると元に戻ります。