僕らは JavaScript を知らない - データ型と参照 Data Type and Reference

参考

データ型

JavaScript は動的型付け言語と呼ばれ、変数の型は存在しません。変数はどのデータ型でも持つことが可能です。

let value = '1'; // String
value = 1; // Number
value = true; // Boolean

データ型には、次の7つの型があります:

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol
  • Object

これらのうち、 object 以外はプリミティブ型と呼ばれます。また、オブジェクトはプリミティブ型やオブジェクトの集合です。 typeof 演算子を使うと、データ型の文字列を取得することが出来ます:

typeof undefined     === "undefined"; // true
typeof true          === "boolean";   // true
typeof 1             === "number";    // true
typeof "1"           === "string";    // true
typeof { a: 1 }      === "object";    // true
typeof Symbol()      === "symbol";    // true

ただし、 null だけは "null" ではなく、 "object" を返します。これは JavaScript のバグですが、この挙動に依ったコードがあまりにも多く、これを修正することでより多くのバグを生むため、このままにされているという経緯があります。

Undefined Type

JavaScript において、変数の型は存在せず、データ型のみ存在します。例えば次のようなコードを実行したとします:

var a;

a; // undefined
b; // ReferenceError: b is not defined

これは、変数 a の宣言時の初期値が undefined 型であるためです。

しかし、 typeof 演算子は宣言していない変数 b に対して次のように動作します:

var a;

typeof a; // undefined
typeof b; // undefined

これは安全装置として働きます。例えば、グローバルネームスペースに myFunction が存在するかどうかを確かめ、存在しない場合新たに myFunction を定義するためのコードは次のようになります:

// Uncaught ReferenceError: myFunction is not defined
if (!myFunction) {
    myFunction = function() { console.log('This is my function!') }
}

// typeof 演算子によってエラーは起きない
if (typeof myFunction === undefined) {
    myFunction = function() { console.log('This is my function!') }
}

参照

C++ において次のコードでは、変数 a は変数 myNumber の参照となります。ここでいう参照とは「別名」と考えてよく、 myNumber の別名である a の変更は myNumber にも影響します。メモリ上に変数 myNumber を作成して、そのアドレスを変数 a に渡しているため、変数から変数への参照と呼ばれます。

void myFunction( int& a )
{
    a *= 2;
}
int main()
{
    int myNumber = 2;
    myFunction( myNumber );
}

対して、JavaScript では変数に対する参照はありませんが、値に対する参照を行います。 JavaScript においてオブジェクトは参照型です。2つの異なるオブジェクトは、たとえ同じプロパティを持っていたとしても等値とはみなされません。具体例を確認しましょう:

// メモリ上に `[1, 2, 3]` を作成し、そのアドレスを `a` に代入
// また、新たにメモリ上の別の場所に `[1, 2, 3]` を作成し、そのアドレスを `b` に代入
var a = [ 1, 2, 3 ];
var b = [ 1, 2, 3 ];

// アドレスが異なるので等値ではない
a === b // false

//  `b` の値のアドレスを `a` に代入
a = b

// アドレスが同じなので等値
a === b // true

C++ や他の言語とは異なり、 JavaScript のオブジェクト変数は値のアドレスを保持しているだけで、他の変数のアドレスを保持しません。変数 c d は値を共有しているので、値に対する変更はどちらの変数からでも確認することができます。アドレスの代入、参照の値渡しと呼ばれます。

var c = [ 1, 2, 3 ];

// アドレスの代入
var d = c;

// 参照する値に対する変更
d.push( 4 );

c; // [ 1, 2, 3, 4 ]
d; // [ 1, 2, 3, 4 ]

以下のコードにおいて、変数 b に対する変更は変数 a に影響を与えることはありません。なぜならアドレスが異なるからです。

var a = [ 1, 2, 3 ];
// 参照の代入
var b = a;

// 空の配列リテラルから配列を新たに作成し、そのアドレスを代入
b = [];
a; // [ 1, 2, 3 ]
b; // []

元のオブジェクトを空にしたい場合は、既存の配列に対する変更を行います:

var a = [ 1, 2, 3 ];
var b = a;

b.length = 0;

a; // []
b; // []

オブジェクトの値だけを渡したい場合は Array.prototype.slice メソッドなどを使用します。この場合、b のアドレスと c のアドレスは異なるため、c に対する変更は影響を受けません:

var a = { hoge: 'hoge' }
var b = [ a, 1 ]
var c = b.slice();
c.push( 2 );

a // { hoge: 'hoge' }
b // [ { hoge: 'hoge' }, 1 ]
c // [ { hoge: 'hoge' }, 1, 2 ]

シャローコピー (1段階の深さのコピー) なので、 オブジェクト a の値ではなくアドレスがコピーされます:

var a = { hoge: 'hoge' }
var b = [ a, 1 ]
var c = b.slice();

a.fuga = 'fuga';
c.push( 2 );

a // { hoge: 'hoge', fuga: 'fuga' }
b // [ { hoge: 'hoge', fuga: 'fuga' }, 1 ]
c // [ { hoge: 'hoge', fuga: 'fuga' }, 1, 2 ]

本来プリミティブ型は値を保持しておくことが出来ないのですが、オブジェクトが参照型であることを逆手に取ってオブジェクトでラップしてやれば、プリミティブ型も「参照渡し」することが可能で、関数内の変数 y.b に加えた変更は変数 obj.y にも反映されます:

var a = 100;

(function(x) {
    x = 50;
})(a);

a // 100;

var obj = { b: 100 };

(function(y) {
    y.b = 50;
})(obj);

obj.b // 50;