Caution
お使いのブラウザはJavaScriptが実行できない状態になっております。
当サイトはWebプログラミングの情報サイトの為、
JavaScriptが実行できない環境では正しいコンテンツが提供出来ません。
JavaScriptが実行可能な状態でご閲覧頂くようお願い申し上げます。
JavaScript
中級編
- トップページ
- JavaScript中級編 - JavaScriptの参照渡しについて
JavaScriptの参照渡しについて
みなさまどうもこんにちは。
今回でJavaScript中級編最後の項目となります。ここまでお付き合い頂き誠にありがとうございました。
では『参照渡し』について解説していきたいと思います。こちらもかなりややこしい項目となりますが頑張ってください。
まずJavaScriptにおいての全ての要素は大きく分けて『プリミティブ』と『オブジェクト』に分けられます。『プリミティブ』については別の記事で詳しく解説をしていきたいと思いますが、とりあえず数値や真偽値のような根本となる値のことだと思って下さい。
『オブジェクト』についてはこちらの記事で解説した通り、名前のついたデータの集まりとなります。JavaScriptでは配列も一種の『オブジェクト』となります。関数も一種の『オブジェクト』です。
そしてJavaScriptでは要素を代入させた際の処理に『プリミティブ』と『オブジェクト』で大きな違いが存在します。
まずは通常の変数をみてみましょう。以下の変数で使っている『0』という数値は『プリミティブ』になります。
var x = 0; //『x』に『0』を代入。 var y; // 『y』を定義。 y = x; // 『y』に『x』を代入。 ++y; // 『y』の値に『1』を足す。 console.log(x); // 『0』と出力される。 console.log(y); // 『1』と出力される。
さて、これを実行させると『0』と『1』がコンソールに出力されます。これは想像通りの動きですね。
では続いて配列で同じようなことをやってみたいと思います。以下のサンプルを確認してみましょう。
var x = [0, 1]; //『x』に『[0, 1]』を代入。 var y; // 『y』を定義。 y = x; // 『y』に『x』を代入。 ++y[0]; // 『y[0]』の値に『1』を足す。つまり値は『1』となる。 console.log(x); // なぜか『[1, 1]』と出力される。 console.log(y); // 『 [1, 1]』と出力される。
上記はまず配列『x』に『[0, 1]』を代入し、その配列『x』を配列『y』に代入、その後、配列『y』の0番目の要素に『1』を足してコンソールに『x』と『y』を出力させています。
これを実行させると『[1, 1]』と2回出力されます。修正したのは『y』だけなので『[0, 1]』と『[1, 1]』が出力されそうですがそうではありません。なんだかよく分からないことが起こっていますね。
これが『参照渡し』と呼ばれる現象になります。JavaScriptでの『オブジェクト』は他のオブジェクトに代入した場合、中身がコピーされるわけではありません。実はその『オブジェクト』の参照(場所)が保存されるだけだったりします。
イメージとしては『オブジェクトが保存されているデータの置き場所』が保存されている感じです。なのでオブジェクトをオブジェクトに代入(コピー)し、その代入されたオブジェクトの値を修正すると元々のオブジェクトの値が操作されてしまいます。
先ほどのサンプルのコメントを修正してみたので確認してみてください。
var x = [0, 1]; //『x』に『[0, 1]』を代入。 var y; // 『y』を定義。 y = x; // 『y』に『x』を代入。しかし、オブジェクトの代入なので丸ごとコピーされるのではなく『参照渡し』となる。 ++y[0]; // 『y[0]』の値に『1』を足す。配列『y』の実体は配列『x』なのでオブジェクト『x[0]』の値が『1』となる。 console.log(x); // 配列『y[0]』ごしに配列『x[0]』の値が操作されたので『[1, 1]』と出力される。 console.log(y); // 実体は配列『x』なので『 [1, 1]』と出力される。
このように代入式などで『参照』(場所)が渡されることを『参照渡し』といいます。他の言語でも『参照渡し』という挙動はあるのですが、JavaScriptではかなり実感しにくい仕様となっているため思わぬミスの原因になりがちです。ちなみに先程の『プリミティブ』の時のような値がそのまま複製(コピー)される処理は『値渡し』といいます。
先程は配列で試してみましたが今度はオブジェクトで試してみましょう。以下のサンプルを確認してみてください。
var x = { //『x』に『{“hoge”: 0, “hoge”: 1}』を代入。 "hoge": 0, "hoge2": 1 }; var y; // 『y』を定義。 y = x; // 『y』に『x』を代入。しかし、オブジェクトの代入なので丸ごとコピーされるのではなく『参照渡し』となる。 ++y["hoge"]; // 『y["hoge"]』の値に『1』を足す。オブジェクト『y』の実体はオブジェクト『x』なのでオブジェクト『x["hoge"]』の値が『1』となる。 console.log(x); // オブジェクト『y["hoge"]』ごしにオブジェクト『x[“hoge"]』の値が操作されたので『{hoge: 1, hoge2: 1}』と出力される。 console.log(y); // 実体はオブジェクト『x』なので『 {hoge: 1, hoge2: 1}』と出力される。
こちらも同じように『参照渡し』が行われていますね。
では続いて関数で行ってみましょう。
var hoge = function(){ // 関数『hoge』を定義。 // 適当に処理 }; hoge2 = hoge; // 関数『hoge』を『hoge2』に代入。オブジェクトなので『参照渡し』となる。 hoge2.hoge = 0; // 関数『hoge2』に『hoge』プロパティを作成して『0』を代入。関数『hoge2』の実体は関数『hoge』なので『hoge.hoge』に『0』が代入される。 conso.log(hoge.hoge); // 関数『hoge2』ごしに『hoge.hoge』が生成されたので問題なく『0』が出力される。
いかがでしょうか。関数でも『参照渡し』が行われているようですね。
このようにJavaScriptでのオブジェクトの代入処理は『参照渡し』となりデータが複製(コピー)されるわけではありません。思わぬミスの原因になるところなので注意してください。
JavaScriptでの関数はオブジェクトとなりますので通常のオブジェクトと同じくプロパティを持つことができます。以下のサンプルを確認してみてください。
var hoge = function(){ // 関数『hoge』を定義。 // 適当に処理 }; hoge.hoge = 0; // 関数『hoge』はオブジェクトなので『hoge.hoge』というプロパティを持てる。そこに『0』を代入。 console.log(hoge.hoge); // 『0』と出力されます。
上記のサンプルは問題なく実行させることができます。
この関数オブジェクトにプロパティを作成するやり方は使い方によっては非常に強力です。この利用方法についてはまた別の記事で紹介したいと思います。
と、『参照渡し』についての解説はこんな感じになります。
「ではJavaScriptでオブジェクトを複製(コピー)したい場合はどうすればいいのか」ということになると思いますのでひとつサンプルをご紹介したいと思います。
ちょっと構文が上級者向けになってしまうので難しめの内容になりますがそこはご容赦ください。
まず配列の複製処理からです。JavaScriptでの代入式の特性をちょっと思い出してみましょう。『プリミティブは複製(コピー)されるけどオブジェクトは参照渡しになる』ということでしたね。
ということは配列を丸ごと代入するのではなく配列の要素ごとに代入処理を行えば結果、複製になるということです。『for文』と『return文』を使えばいけそうですね。
というわけで適当に関数を定義します。名前はなんでも良いのですが『replicateArray』とします。
var replicateArray = function(){ // まず関数を定義。 }
そしたら配列を引数から受け取りたいので仮引数『x』を定義します。こちらも名前はなんでも構いません。
var replicateArray = function(x){ // 仮引数『x』を定義。 }
続いて仮引数『x』の『for文』を記述しておきましょう。
var replicateArray = function(x){ // 仮引数『x』を定義。 var i; // for文で使用する変数『i』を定義。 for(i = 0; i <= x.length -1; ++i){ // 仮引数『x』の『要素数』で『for文』が実行されるよう定義。 } }
そしたら引数から受け取った配列の要素を一旦受け取りたいのでローカルの配列を定義しておきます。
var replicateArray = function(x){ // 仮引数『x』を定義。 var i, y = []; // for文で使用する変数『i』とローカルの配列『y』を定義。 for(i = 0; i <= x.length -1; ++i){ // 仮引数『x』の『要素数』で『for文』が実行されるよう定義。 } }
あとは『for文』の中で仮引数『x』の配列をローカルの配列『y』に要素ごとに代入して出来上がった配列『y』を『return文』で返させるようにします。
var replicateArray = function(x){ // 仮引数『x』を定義。 var i, y = []; // for文で使用する変数『i』とローカルの配列『y』を定義。 for(i = 0; i <= x.length -1; ++i){ // 仮引数『x』の『要素数』で『for文』が実行されるよう定義。 y[i] = x[i]; // 配列の要素ごとに代入処理を行う。 } return y; // return文で配列『y』を返す。 }
はい、これで完成です。では早速実験してみましょう。使い方はこんな感じになります。
var replicateArray = function(x){ // 仮引数『x』を定義。 var i, y = []; // for文で使用する変数『i』とローカルの配列『y』を定義。 for(i = 0; i <= x.length -1; ++i){ // 仮引数『x』の『要素数』で『for文』が実行されるよう定義。 y[i] = x[i]; // 配列の要素ごとに代入処理を行う。 } return y; // return文で配列『y』を返す。 } var x = [0, 1]; // グローバルの配列『x』を定義。 var y = replicateArray(x); // 作成した関数『replicateArray』を使って配列のコピーを行う。 ++y[0]; // 配列『y[0]』の値に『1』を足す。 console.log(x); //『[0, 1]』と出力される。 console.log(y); //『[1, 1]』と出力される。
バッチリですね。しかし、まだこの関数はやるべきことがあります。引数に配列を渡してくれなかった時の対応を念のため考えておきましょう。
JavaScriptでは配列かどうかチェックする『Array.isArray()』というメソッドが用意されています。これを使って仮引数『x』がちゃんと配列かどうかチェックする処理を加えましょう。
『Array.isArray()』の使い方は以下のような感じです。引数の『()』の中に何かを入れ、それが配列だと『true』、それ以外だと『false』が返ってきます。
Array.isArray([0, 1]); // 引数の中が配列だと『true』、それ以外だと『false』が返ってきます。
(´-`).。oO(ちなみに『Array.isArray()』はかなり新しいメソッドなのでIE8以下だと動かないっす...)
では『Array.isArray()』を組み込みしてみましょう。配列じゃなかったら「配列を入れてね!」とコンソールに出力するようにしてみます。
var replicateArray = function(x){ // 仮引数『x』を定義。 if(!Array.isArray(x)){ // 『Array.isArray(x)』が『false』だった場合に実行。『false』の時に実行させたいので『!』で真偽値を反転させる。 console.log("配列を入れてね!"); // コンソールにエラーを報告。 return; // return文で処理を終了させる。 } var i, y = []; // for文で使用する変数『i』とローカルの配列『y』を定義。 for(i = 0; i <= x.length -1; ++i){ // 仮引数『x』の『要素数』で『for文』が実行されるよう定義。 y[i] = x[i]; // 配列の要素ごとに代入処理を行う。 } return y; // return文で配列『y』を返す。 }
とりあえずはOKですね。『Array.isArray()』あたりの処理がちゃんと動いているかテストしてみます。
var replicateArray = function(x){ // 仮引数『x』を定義。 if(!Array.isArray(x)){ // 『Array.isArray(x)』が『false』だった場合に実行。『false』の時に実行させたいので『!』で真偽値を反転させる。 console.log("配列を入れてね!"); // コンソールにエラーを報告。 return; // return文で処理を終了させる。 } var i, y = []; // for文で使用する変数『i』とローカルの配列『y』を定義。 for(i = 0; i <= x.length -1; ++i){ // 仮引数『x』の『要素数』で『for文』が実行されるよう定義。 y[i] = x[i]; // 配列の要素ごとに代入処理を行う。 } return y; // return文で配列『y』を返す。 } var x = [0, 1]; // グローバルの配列『x』を定義。 var y = replicateArray(x); // 作成した関数『replicateArray』を使って配列のコピーを行う。 var z = replicateArray("hoge"); // 試しに文字列を入れてみる。 console.log(y); //『[0, 1]』と出力される。 console.log(z); //『配列を入れてね!』と出力される。
どうやらちゃんと動いているようですね。本当はもっと念入りにテストするべきなのですが今回はサンプルなのでこの辺にしておきます。
というわけでこれで引数に配列以外のものが渡されても対応ができるようになりました。
しかし、これではまだ少々甘いのです。この関数は多次元配列を考慮しておりません。多次元配列とは配列の中に配列が入っているような配列のことでしたね。上記のサンプル関数だと配列の中に配列が入っていた場合でもそのまま代入式が行われてしまうので『参照渡し』になってしまいます。
ということは『for文』の中でさらに『if文』と『Array.isArray()』を組み込ませて出し分けをする必要があるということです。
組み込みしてみます。
var replicateArray = function(x){ // 仮引数『x』を定義。 if(!Array.isArray(x)){ // 『Array.isArray(x)』が『false』だった場合に実行。『false』の時に実行させたいので『!』で真偽値を反転させる。 console.log("配列を入れてね!"); // コンソールにエラーを報告。 return; // return文で処理を終了させる。 } var i, y = []; // for文で使用する変数『i』とローカルの配列『y』を定義。 for(i = 0; i <= x.length -1; ++i){ // 仮引数『x』の『要素数』で『for文』が実行されるよう定義。 if(Array.isArray(x[i])){ // 『x[i]』が配列だったら実行。 // ここに注目してください。 } else{ // 『x[i]』が配列じゃなかったら実行。 y[i] = x[i]; // 配列の要素ごとに代入処理を行う。 } } return y; // return文で配列『y』を返す。 }
さて、ここまでできたわけですが上記のサンプルの『ここに注目してください。』という部分に注目してください。
もし『x[i]』が配列だった場合、また『for文』を使って配列の要素ごとに代入処理を行う必要があります。JavaScriptでは多次元配列が可能なので『配列の中の配列の中の配列の...』と永遠につづいてしまった場合は大変です。永遠と『for文』を書き続けなくてはならなくなります。
そんな永遠と続く記述はできないし、プログラミングとして美しくないし、というわけでこれまで書いてきた処理を思い出してみましょう。
これまでに配列の要素ごとに代入処理を行う構文を書いてきていますね。そうです、これまでに書いてきた関数『replicateArray』です。
自分自身を実行させれば多次元配列だろうとなんだろうとその数に合わせて自動でループ処理をしてくれる構文ができあがります。
これはちょっと前の記事でちょろりとお話した『再帰的処理』というやつになりますね。というわけで『ここに注目してください。』という部分に自分自身を入れてあげます。JavaScriptで関数内から自分自身を参照できる『arguments.callee』というオブジェクトがありますのでそれを使ってみましょう。
var replicateArray = function(x){ // 仮引数『x』を定義。 if(!Array.isArray(x)){ // 『Array.isArray(x)』が『false』だった場合に実行。『false』の時に実行させたいので『!』で真偽値を反転させる。 console.log("配列を入れてね!"); // コンソールにエラーを報告。 return; // return文で処理を終了させる。 } var i, y = []; // for文で使用する変数『i』とローカルの配列『y』を定義。 for(i = 0; i <= x.length -1; ++i){ // 仮引数『x』の『要素数』で『for文』が実行されるよう定義。 if(Array.isArray(x[i])){ // 『x[i]』が配列だったら実行。 y[i] = arguments.callee(x[i]); // 『arguments.callee()』で自分自身を実行させ『y[i]』に代入させる。 } else{ // 『x[i]』が配列じゃなかったら実行。 y[i] = x[i]; // 配列の要素ごとに代入処理を行う。 } } return y; // return文で配列『y』を返す。 }
これでOKですね。ではテストしてみましょう。
var replicateArray = function(x){ // 仮引数『x』を定義。 if(!Array.isArray(x)){ // 『Array.isArray(x)』が『false』だった場合に実行。『false』の時に実行させたいので『!』で真偽値を反転させる。 console.log("配列を入れてね!"); // コンソールにエラーを報告。 return; // return文で処理を終了させる。 } var i, y = []; // for文で使用する変数『i』とローカルの配列『y』を定義。 for(i = 0; i <= x.length -1; ++i){ // 仮引数『x』の『要素数』で『for文』が実行されるよう定義。 if(Array.isArray(x[i])){ // 『x[i]』が配列だったら実行。 y[i] = arguments.callee(x[i]); // 『arguments.callee()』で自分自身を実行させ『y[i]』に代入させる。 } else{ // 『x[i]』が配列じゃなかったら実行。 y[i] = x[i]; // 配列の要素ごとに代入処理を行う。 } } return y; // return文で配列『y』を返す。 } var x = [[1, [2, [3, 4]]], 5]; // グローバルの多次元配列『x』を定義。 var y = replicateArray(x); // 作成した関数『replicateArray』を使って配列のコピーを行う。 ++y[0][1][1][1]; // y[0][1][1][1]に『1』をたす。 console.log(x); //『[[1, [2, [3, 4]]], 5]』と出力される。 console.log(y); //『[[1, [2, [3, 5]]], 5]』と出力される。
いかがでしょうか。これで多次元配列でも対応できるようになりました。
ちなみに今回使った『arguments.callee』ですが、最近のJavaScriptの勧告をチェックすると非推薦になっています。
なのでちょっと前の記事でちょろりと紹介したように自分自身の関数名で実行してしまうほうが無難かもしれません。以下のような感じですね。
var replicateArray = function(x){ // 仮引数『x』を定義。 if(!Array.isArray(x)){ // 『Array.isArray(x)』が『false』だった場合に実行。『false』の時に実行させたいので『!』で真偽値を反転させる。 console.log("配列を入れてね!"); // コンソールにエラーを報告。 return; // return文で処理を終了させる。 } var i, y = []; // for文で使用する変数『i』とローカルの配列『y』を定義。 for(i = 0; i <= x.length -1; ++i){ // 仮引数『x』の『要素数』で『for文』が実行されるよう定義。 if(Array.isArray(x[i])){ // 『x[i]』が配列だったら実行。 y[i] = replicateArray(x[i]); // 『arguments.callee()』ではなく自分自身の関数名で実行。 } else{ // 『x[i]』が配列じゃなかったら実行。 y[i] = x[i]; // 配列の要素ごとに代入処理を行う。 } } return y; // return文で配列『y』を返す。 } var x = [[1, [2, [3, 4]]], 5]; // グローバルの多次元配列『x』を定義。 var y = replicateArray(x); // 作成した関数『replicateArray』を使って配列のコピーを行う。 ++y[0][1][1][1]; // y[0][1][1][1]に『1』をたす。 console.log(x); //『[[1, [2, [3, 4]]], 5]』と出力される。 console.log(y); //『[[1, [2, [3, 5]]], 5]』と出力される。
尚、今は関数名がついているから平気ですが、無名関数など再呼び出しができない関数の中で自分自身を実行させるためには『arguments.callee』を使う以外の方法はありません。なので状況によって使い分けるようにしてください。昔は再帰処理といえば『arguments.callee』というくらいだったので現存する全てのブラウザで問題なく実行することができます。
さて、では続いてオブジェクトの複製をしてみましょう、といいたいところなのですがJavaScriptで配列以外のオブジェクトを完全に複製する構文は多分ありません。
公式サイトなどでも「無理っす!」となっているくらいなのでホントに無理なんだと思います。
その理由のひとつとしてJavaScriptが『プロトタイプベース』という構造になっているということがあげられます。JavaScriptではオブジェクトのプロパティが見つからない場合、親の『prototype』オブジェクトをチェックしにいくという処理が見えないところで行われます。
その『prototype』オブジェクトも拾って、全てのオブジェクトの完全コピーはほぼ不可能といっていいです。
なので実は先程作った関数『replicateArray』も配列の中にオブジェクトが含まれていた場合は対応できません。あくまでも純粋な多次元配列のみ対応できる関数となっています。
なのでJavaScriptでオブジェクトを定義した際は複製できないもの、と考えてしまうのがベストかもしれませんね。実際、オブジェクトを複製しなくても様々な処理を構築できますので大方問題はありません。
というわけですごく長くなってしまいましたが以上となります。『参照渡し』についてはいかがでしたでしょうか。JavaScriptの闇の部分なのでお疲れになったと思います。そしてこのような駄文をここまでお読み頂き誠にありがとうございました。
続いての記事では総まとめをやっていきたいと思います。ではまたお会いしましょう。
この記事は桜舞が執筆致しました。
著者が愛する小型哺乳類 |
桜舞 春人 Sakurama HarutoISDN時代から様々なコンテンツを制作しているちょっと髪の毛が心配な東京在住のプログラマー。生粋のロングスリーパーで、10時間以上睡眠を取らないと基本的に体調が悪い。好きなだけ寝れる生活を送るのが夢。ゲームとスポーツと音楽が大好き。誰か髪の毛を分けて下さい。 |
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。