これは JavaScript Advent Calendar 2014 の17日目(12/17)の投稿です。初学者向けの内容です。(ていうか自分が初学者です..)
自分が JavaScript をやり始めたとき、
- 変数に何がはいってるのか、変数の受け渡しでやりとりされるのは何なのか
- 関数の return で返ってくるものが何なのか
が良く分からず困惑したので、その時のことを思い出しながら挙動を再確認してみました。
1. 変数へ値を代入した場合の挙動
JavaScript では、変数に型はありませんが、値には 基本型 と オブジェクト型 の2種類があります。
A. 基本型
値そのもの が渡されます。
① 値として「数値」を代入した場合
代入しているのは2行目です。
var num1 = 999;
var num2 = num1; // num2 には値そのものが渡されている
num1 = 0; // num1 の値を後から変更
console.log(num1); // 0 が出力される
console.log(num2); // 999 が出力される(num1 の変更の影響を受けていない)
② 値として「文字列」を代入した場合
代入しているのは2行目です。
var str1 = 'あああ';
var str2 = str1; // str2 には値そのものが渡されている
str1 = 'いいい'; // str1 の値を後から変更
console.log(str1); // いいい が出力される
console.log(str2); // あああ が出力される(str1 の変更の影響を受けていない)
③ 値として「論理値」を代入した場合
代入しているのは2行目です。
var flag1 = true;
var flag2 = flag1; // flag2 には値そのものが渡されている
flag1 = false; // flag1 の値を後から変更
console.log(flag1); // false が出力される
console.log(flag2); // true が出力される(flag1 の変更の影響を受けていない)
基本型には上記以外もありますが割愛。
B. オブジェクト型
アドレス(参照) が渡されます。
① 値として「配列」を代入した場合
代入しているのは2行目です。
var arr1 = [1, 1, 1];
var arr2 = arr1; // arr2 にはアドレスが渡されている
arr1[0] = 99; // arr1 の中身を後から変更
console.log(arr1); // [99, 1, 1] が出力される
console.log(arr2); // こちらも [99, 1, 1] が出力される(arr1 の変更の影響を受けている)
② 値として「オブジェクト(key-value)」を代入した場合
代入しているのは2行目です。
var obj1 = {name:'山田', age: 32};
var obj2 = obj1; // obj2 にはアドレスが渡されている
obj1.name = '高橋'; // obj1 の中身を後から変更
console.log(obj1); // {name: "高橋", age: 32} が出力される
console.log(obj2); // こちらも {name: "高橋", age: 32} が出力される(obj1 の変更の影響を受けている)
③ 値として「関数」を代入した場合
代入しているのは4行目です。
var func1 = function(){}; // 空の関数を定義
func1.num = 999; // func1 のプロパティ num に値をセット
var func2 = func1; // func2 にはアドレスが渡されている
func1.num = 0; // func1 の中身を後から変更
console.log(func1.num); // 0 が出力される
console.log(func2.num); // こちらも 0 が出力される(func1 の変更の影響を受けている)
オブジェクト型には上記以外もありますが割愛。
2. 変数の再定義
上記の例 1-B-①②③ では arr1[0] = ○○
obj1.name = ○○
func1.num = ○○
という風に 中身の要素にアクセスして 値を変更していました。この場合は、変数 arr
obj1
func1
に格納されている アドレス(参照) には何の影響もありません。
ですが、もし arr1 = ○○
obj1 = ○○
func1 = ○○
とすると、それは単に 変数の再定義 なので アドレス(参照)が新しいものへ上書き されます。検証用コードを下記に示します。
① 配列
var arr1 = [1, 1, 1];
var arr2 = arr1; // arr2 にはアドレスが渡されている
arr1 = []; // arr1 に [] へのアドレスが上書きで格納される
console.log(arr1); // [] が出力される
console.log(arr2); // [1, 1, 1] が出力される(arr2 に格納されていたアドレスは維持されている)
② オブジェクト(key-value)
var obj1 = {name:'山田', age: 32};
var obj2 = obj1; // obj2 にはアドレスが渡されている
obj1 = {}; // obj1 に {} へのアドレスが上書きで格納される
console.log(obj1); // {} が出力される
console.log(obj2); // {name: "山田", age: 32} が出力される(obj2 に格納されていたアドレスは維持されている)
③ 関数
var func1 = function(){};
func1.num = 999;
var func2 = func1; // func2 にはアドレスが渡されている
func1 = function(){}; // func1 に function(){} へのアドレスが上書きで格納される
console.log(func1.num); // undefined:未定義エラー(numというプロパティは存在しない)
console.log(func2.num); // 999 が出力される(func2 に格納されていたアドレスは維持されている)
こう見ると、JavaScript で変数 hoge
に hoge = ○○
と値を代入する際は 基本型とオブジェクト型で、挙動の差異が無い ということになります。
3. 関数の return 値を受け取った際の挙動
ここでは 配列 の return についてのみ述べますが オブジェクト(key-value) 、 関数 の return についても同じ話です。
配列を return する関数 rtnArray
を定義し、その関数を用いて arr1
arr2
という2つの配列を生成してみます。
// 配列を retrun する関数を定義
function rtnArray(){
return [1, 1, 1];
}
// 先ほどの関数を用いて配列を生成
var arr1 = rtnArray();
var arr2 = rtnArray();
arr1[0] = 99; // 1つ目の配列の中身を変更
console.log(arr1); // [99, 1, 1] が出力される
console.log(arr2); // [1, 1, 1] が出力される(つまり、arr1 と arr2 は独立している)
つまり、6行目と7行目で次のように書いてある処理は、
arr1 = rtnArray();
arr2 = rtnArray();
次のように書くのと同義、ということになります。
arr1 = [1, 1, 1];
arr2 = [1, 1, 1];
つまり 関数の ruturn を受け取る 変数の再定義 がされています。
ここまで理解すると、次のようなコードで、同じ count()
という関数を呼び出していても、conter1
と counter2
では 別々のメモリ領域にあるローカル変数 localNum
を参照している ということが分かります。
// 関数(function)を return する関数を定義
var count = function(initNum) {
var localNum = initNum;
return function() {
return localNum++;
};
};
// 初期値を 1 として カウンターを作成
var counter1 = count(1);
console.log(counter1()); // 1 が出力される
console.log(counter1()); // 2 が出力される
console.log(counter1()); // 3 が出力される
// 初期値を 9 として カウンターを作成
var counter2 = count(9);
console.log(counter2()); // 9 が出力される
console.log(counter2()); // 10 が出力される
console.log(counter2()); // 11 が出力される
console.log(counter1()); // 4 が出力される(2つの独立したカウンターが実現できている)
ちなみに上記の localNum
は関数(メソッド)経由でしかアクセスできません。 関数閉包(クロージャ) と呼ばれるものです。
おわりに
JavaScript では 変数が何をさしているのか を頭にいれておかないと、既存のコードを読む際に混乱するので、このあたりは意識していきたいところです。(自戒)
あまり役に立たない記事ですみません.. よろしければこちらもどうぞ。
⇒ サーバサイドエンジニアでも押えておきたい!コアJavaScriptの基本知識