僕らは JavaScript を知らない - データ型と参照 Data Type and Reference
参考
- https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch2.md#value-vs-reference
- https://www.webprofessional.jp/how-javascript-references-work/
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
データ型
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;