Tips

constの配列とオブジェクトが再代入や変更できてしまう理由【JavaScript、ミュータブル、イミュータブル、メモリ】

  • このエントリーをはてなブックマークに追加

「配列やオブジェクトの扱いがよく分かっていないので関数を上手く作れない」
「なぜか変数が思った通りのデータにならないことがある」
「ミュータブル、イミュータブル、メモリなどの専門用語を習ったことがない」

今回はそのような方に向けて変数と定数の仕組みを詳しく知ってもらおうと思います。

基本知識がない方に向けても分かるような解説にしているのはもちろん、すでに開発作業に進んでいる脱初心者の方に向けた発展的な内容まで網羅しています。

本日お話しすることは意外と初学者向けの教材やスクールでは教えてもらえないにも関わらず、実務に密接に絡んでいる超重要な内容になっています。

動画もあるのでぜひ最後まで見ていってください。

JavaScriptにおける変数と基本

まず変数とは数字、文字列、配列など、何かしらのデータを格納するものです。

JavaScriptにおける変数の書き方はこんな感じです。

let num1 = 1;
console.log("num1:", num1);

「=」を挟んで左辺に変数の名前、右辺に変数に格納するデータを記載します。

なお左辺の変数の名前の前に「let」と記載するのがJavaScriptにおけるルールになります。

変数は漢字の通り「変わるもの」なので、後から別のデータに変更したり計算したり色んなことができます。

let num1 = 1;
// すでに作ってある変数を代入して新しい変数を作成
let num2 = num1;
console.log("num1:", num1);
console.log("num2:", num2);
let num1 = 1;
let num2 = num1;
// すでに作ってある変数の中身を計算する
num2 = num2 + 1;
console.log("num1:", num1);
console.log("num2:", num2);
let arr1 = [1, 2, 3];
// すでに作ってある変数に関数を実行する
// push()は配列に新しい要素を追加する関数
arr1.push(4);
console.log("arr1:", arr1);

プログラミングにおけえる「=」は代入を意味する

変数を作ったり計算するときに「=」という記号がよく登場します。

こちら初学者の方がよく勘違いされるのですが「等しい」という意味ではありません。

let num1 = 1;
let num2 = num1;
num2 = num2 + 1;
console.log("num1:", num1);
console.log("num2:", num2);

上記のコードでnum1とnum2の値はどうなるでしょうか?

2行目の「let num2 = num1 ;」を「num2とnum1は等しい」という理解をしているのであれば、num1もnum2とも「2」という値になっていると思われるかもしれません。

しかし実際にはnum1とnum2は違う値になりました、num2にのみ3行目の足し算が実行されたためです。

この例から分かるようにプログラミングにおいて「=」は「等しい」という意味ではないのです。

プログラミングにおいて「=」は「代入」という意味になります、これを勘違いしていると色んなエラーに見舞われますので注意してください。

配列にはメモリが代入されている

変数について初学者の方が勘違いしがちな注意点をもう一つ紹介します。

let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);
console.log("arr1:", arr1);
console.log("arr2:", arr2);

先ほどと似ていますが変数で扱っているデータが配列なのが特徴です。

ちなみに3行目のpush( )は配列に対して引数の値を追加するための関数となり、[ 1, 2, 3 ] → [ 1, 2, 3, 4 ]にする効果があります。

こちらについても考えてみましょう、arr1とarr2の値はどうなるでしょうか?

先ほどもお話しましたがプログラミングにおける「=」は代入しているわけです。

ここで初学者の方であれば「arr1とarr2は違う値になる」と思われているかもしれません。

今度はなんと同じ値になりました、「さっきと言ってることが違うじゃないか」という声が聞こえてきそうですね。

結論、配列とオブジェクトは変数にメモリが代入されていることが原因です。

先ほどのコードを使って説明するとこんな感じになっているということなんです。

let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);
console.log("arr1:", arr1);
console.log("arr2:", arr2);

↓↓↓↓↓実際にはこうなる↓↓↓↓↓

let arr1 = メモリ1 = [1, 2, 3];
let arr2 = メモリ1;
メモリ1.push(4); // 結果はメモリ1 = [1, 2, 3, 4]になる
console.log("arr1:", arr1);
console.log("arr2:", arr2); 

「メモリ」というのはGoogle Chromeなどブラウザに事前に用意されている保管場所のようなものだと思ってください。

メモリはたくさん用意することができて仮で「メモリ1」という名前を付けてみました。

配列とオブジェクトにはメモリが代入されており、メモリの中身が[ 1, 2, 3 ]のような値になっています。

そのためlet arr2 = arr1とするとデータではなくメモリ1が代入されることになります。

つまりこの時点でarr1とarr2は同じメモリ1になるので、中身も同じ[ 1, 2, 3 ]になっていたんです。

さらにarr2.push(4)としているのはメモリ1に対する行為です。

ということはメモリ1であるarr1とarr2の両方にpush(4)が実行されるわけです。

そのため前段にお話していた「=は等しいではなく代入である」というのは今回についても特に変わりないです。

メモリを代入したからです。

ちなみに以下のように新しく配列の変数を作成してみるとどうでしょうか?

let arr1 = [1, 2, 3];
let arr2 = arr1;
let arr3 = [1, 2, 3];
arr3.push(4);
console.log("arr1:", arr1);
console.log("arr2:", arr2);
console.log("arr3:", arr3);

新しくarr3という変数を用意してarr1と同じ[ 1, 2, 3 ]を代入しています。

さらにpush(4)をarr3の方に対して実行するように変更しました。

arr1とarr2とarr3はどうなるでしょうか?

まずarr1とarr2は同じ値になっていて、こちらは先ほど説明した内容です。

続いてarr3についてはpush(4)が実行されて値が変わっています。

「あれ?arr3しか値が変わらないの?」と思われた方がいるかもしれません。

なぜならarr3にはarr1とarr2と同じ値を代入していたからですよね。

結論、こちらは以下のようになっています。

let arr1 = [1, 2, 3];
let arr2 = arr1;
let arr3 = [1, 2, 3];
arr3.push(4);
console.log("arr1:", arr1);
console.log("arr2:", arr2);
console.log("arr3:", arr3);

↓↓↓↓↓実際にはこうなる↓↓↓↓↓

let arr1 = メモリ1 = [1, 2, 3];
let arr2 = メモリ1;
let arr3 = メモリ2 = [1, 2, 3];
メモリ2.push(4);
console.log("arr1:", arr1);
console.log("arr2:", arr2);
console.log("arr3:", arr3);

今回もメモリを意識しないと勘違いしてしまうでしょう。

arr1とarr2は同じメモリ1が代入されています。

しかしarr3については新しく作成した変数なのでメモリ2と違うものが代入されるんです。

そうした場合にarr3.push(4)とすると、あくまでメモリ2に対するものでしかないのでarr1とarr2は特に変更されなかったわけです。

コードを見ただけだと同じ[ 1, 2, 3 ]が代入されているのですが、実際に代入されているメモリが同じかどうかは別の話なんです。

それを体験するために以下のようなコンソールログを実行してみましょう。

let arr1 = [1, 2, 3];
let arr2 = arr1;
let arr3 = [1, 2, 3];
arr3.push(4);
console.log("arr1:", arr1);
console.log("arr2:", arr2);
console.log("arr3:", arr3);
// ここを追加
console.log("arr1 === arr2:", arr1 === arr2);
console.log("arr1 === arr3:", arr1 === arr3);

arr1とarr2はtrueとなったので同じものだと言えましたが、arr1とarr3はfalseとなりました。

このことからも同じ[ 1, 2, 3 ]が代入されているからと言ってもメモリのことを考えると「同じもの」と言えるかは状況によるんですね。

JavaScriptにおける定数の基本

続いて変数と反対に定数というものがあります。

こちらも漢字のままで「一定のもの」というもので変数のように後から代入、計算などができない仕組みです。

const num3 = 100;
num3 = num3 + 1;
console.log("num3:", num3);

JavaScriptにおける定数は「const」という書き方をするのがルールです。

さらに上記のように一度作成した定数を後から変更しようとするとエラーになります。

定数は一見すると不便に見えますが値を変更できないということは、間違えた計算や代入から値を守ってくれるというメリットがあります。

そのため実務では定数を使うことの方が多いと思ってください。

配列とオブジェクトには定数でも代入できる

定数に関しても初学者の方が勘違いしがちな点を紹介します。

const arr4 = [2, 4, 6];
arr4.push(8);
console.log("arr4:", arr4);

上記のコードについてarr4はどんな結果になるでしょうか?

もしかしたらエラーになることを予想されているかもしれません。

なんとエラーにならずに配列の中身を変更することができています。

結論、こちらも変数と同じでメモリが関係しています。

先ほどのコードは以下のようになっているからです。

const arr4 = [2, 4, 6];
arr4.push(8);
console.log("arr4:", arr4);

↓↓↓↓↓実際にはこうなる↓↓↓↓↓

const arr4 = メモリ3 = [2, 4, 6];
メモリ3.push(8);
console.log("arr4:", arr4);

配列とオブジェクトにはメモリが代入されるのは定数でも同じルールになっています。

そうするとarr4にもメモリが代入されていることになります。

つまりarr4.push(8)はメモリに対する関数なので実行できてしまうわけです。

一方で以下のようなコードを書いてみます。

const arr4 = [2, 4, 6];
// ここから修正
arr4 = 100;
console.log("arr4:", arr4);

こちらはエラーになりました、「メモリ3 → 100」と言った具合にメモリを数字に変えようとしているためです。

const arr4 = メモリ3 = [2, 4, 6];
// メモリ3を100に変更するのは定数ではエラーになる
arr4 = 100;
// push(8)はメモリ3ではなく[2, 4, 6]を変更するので実行できる
// push(8)を実行すると[2, 4, 6]に変更が加わるけど、メモリはメモリ3のまま
arr4.push(8);

このように配列とオブジェクトはメモリが代入されていることは定数でも引き続き重要な考え方です。

「メモリに対する変更なのか?」「メモリの中身に対する変更なのか?」というのは意識するように心がけて下さいね。

また今回参考にした本は以下になりますのでよければどうぞ。

今回参考にした本はこちら

  • このエントリーをはてなブックマークに追加