派遣で働くエンジニアのスキルアップを応援するサイト

PRODUCED BY RECRUIT

プリミティブ型・オブジェクト型と参照について理解を深めよう【第6回】JavaScriptを学ぶ上でつまずきやすいポイントを理解するための連載

JavaScriptが持つ「2つの型」の動作を図で解説します。オブジェクト型では「参照」についても確認していきましょう。イメージすることが難しいトピックですが、理解を深めると、不安なくコードを書くことができます。

今回の学び

  • JavaScriptが持つ型は、2種類
  • 「プリミティブ型」と「オブジェクト型」の動作の特徴
  • ミスしやすい。関数の引数による動作
【筆者】佐藤正志さん
契約社員、客員研究員、フリーランスなどさまざまな立場で開発現場を経験。フロントエンド、バックエンド開発、チームリードやOJTの育成担当まで幅広くこなしてきた。現在プログラミングトレーニング事業を行うサークルアラウンド株式会社を立ち上げ、経営や現場開発を行いながらOJT経験をもとに後進の育成に励み、個人や法人に向けたプログラミングのトレーニングを行っている。ノウハウのコンテンツ化にも取り組んでおり、著書に『ステップアップJavaScript』(翔泳社)がある。

*記事内に使用しているサンプルコードは、以下からも確認できます
https://github.com/CircleAround/engineer_style_js/blob/main/06/06_type.js

JavaScriptにおける2つの型

JavaScriptにおける型は、大きく分けると以下の2種類です。

代入や操作時の動作に違いがあるため、それぞれの特徴や扱い方を確認しておきましょう。整理しておかないと、思わぬ誤りに繋がることも。注意が必要です。

プリミティブ型

プリミティブ型のことを理解するために、まずは文字列を例に見ていきましょう。そのほかのプリミティブ型も同様の性質を持つと考えていいです。

let str1 = 'Hello'; // *1
const str2 = str1; // *2
console.log(str1, str2); // => Hello Hello 

簡単なコードですがstr1からstr2へ文字列を代入しました(※2)。このとき、str2にはstr1の文字列のコピーが作られます。もちろん2つの文字列の中身は同じものです。

続けて str1.concatを使ってその結果を改めてstr1に代入します(※3)。

str1.concatでstr1そのものが変更されることはなく、新しい文字列が生成されて戻り値となります。

str1 = str1.concat('World'); // *3

// 以下の二つは違う値を示している
console.log(str1, str2); // => HelloWorld Hello

str1とstr2は無関係のため、str1だけが新たな値になりました。

ここまで、幾つかの簡単な操作を行いました。以下の3つのポイントを確認しておきましょう。

【1】
※1 でメモリ上に作られた “Hello” の文字列の内容は最後まで変更されない

【2】
※2 の代入では内容のコピーが起きている

【3】
※3 のメソッドの操作においてもメモリ上に新たに“Hello World”の文字列が作られ、”Hello” は変更されない

この動作は生成された実体が変更されない、Immutableの動作です。プリミティブ型はこのようなImmutable性を特徴とします。多くの他の言語でも文字列はこのようなImmutableな動作をすることが多いです。

*以上の動作は、内部実装の詳細や最適化などを考えると厳密には違う動作をしている可能性がありますが、プログラムを読み書きする際に、この考え方で矛盾がないよう作られています

プリミティブ型の性質

・変数の代入の時に新しくメモリ領域を確保した「コピー」が行われ、別の実体として扱われる

・Immutableの性質があるので、編集操作では新しい実体を作成する

オブジェクト型

次はオブジェクト型の動作についてです。具体的なコードを見る前に「参照」について確認していきましょう。

■「参照」

メモリ内にあるオブジェクトを指し示すためのIDとして使われる値です。オブジェクト型を扱う場合には、変数には参照が代入されていて、その参照を介してオブジェクトの実体を操作します。

const obj1 = {message: "Hello"};

例えば上記のような宣言をすると、以下のようなイメージで情報が作られます。

■オブジェクトの操作は「参照」を介している

const obj1 = {message: "Hello"}; // *1
const obj2 = obj1; // *2

obj1をobj2へ代入しました(※2)。このとき代入されるのは「参照」なので、obj1とobj2は参照を介して、※1で生成された同一のオブジェクトを指しています。

続けてobj1のmessageの値を変更しました。このとき「参照」を介して※1で生成されたオブジェクトが変更されます。

obj1.message = "Hi"; // *3

console.log(obj2.message); // => Hi

結果、同一のオブジェクトへの参照を持つobj2のmessageも変更されました。

オブジェクト型の性質

・変数の代入には、参照のコピーが行われ、同じ実体を共有する

・編集操作では新しい実体を作成せず、自身が変更される(オブジェクト型でも工夫することでImmutableにできるが、基本的にはMutable)

ミスしやすい。関数の引数における動作

プリミティブ型、オブジェクト型の代入について整理できていないと、以下のようなケースで勘違いしてしまうことがあります。「関数の引数に渡すときに、代入が起きている」と考えるとわかりやすいでしょう。

function concatWorld(str) {
  str = str.concat('World'); // str1が 'HelloWorld'になりそうに思うかもしれない
}

const str1 = 'Hello';
concatWorld(str1);
console.log(str1); // => 'Hello' // 変更はされない

上記のコードでは、str1の指す変数の値は変更されることはありません。concatWorld関数の引数に渡されるのはstr1のコピーであるためです。コピーを操作したとしてもコピー元には変更が反映されません。

今回のまとめ

  • JavaScriptにおける2つの型は、プリミティブ型とオブジェクト型
  • プリミティブ型は、生成された実体が変更されない、Immutableの性質がある
  • オブジェクト型は、編集操作では新しい実体を作成せず自身が変更されるMutableの性質がある

JavaScriptでステップアップを目指す方へ

計6回の連載をお読みいただきありがとうございました。

JavaScriptは基本の文法がわかりやすい一方で、若干癖のある言語だとも思います。加えて「過去の実装も含めてブラウザ上で動作する」という制約があるため、古い仕様がずっと残り続け、「同じことを表現するのに、さまざまな方法がある」そんな状態になりました。

「調べた書き方」が現在も推奨される書き方なのか、よくわからないことも多々あります。このような背景から、言語を扱い始めるころは、理解に苦しむことも多いでしょう。

本連載の中では、特につまずきやすいポイントを整理したつもりですので、一つずつ理解していくと、自信を持ってコードを書けることが増えると思います。

特に「非同期処理」のような、動作の脳内イメージが重要だと考える箇所には、図を多めに用いて、コードを書いているときの私の脳内イメージを伝えようとチャレンジしました。うまく伝わっていたら嬉しいです。

また、動作するコードをなるべくリンクしましたので、ぜひ実際の動作を追って、血肉にしていただきたいです。

連載をざっとご覧いただいた方も、改めて最初から読み直すなどしていただくと、知識が定着して上手に使えるようになることもあると思います。以下のリンクから改めてご覧いただければ幸いです。

▼JavaScriptを学ぶ上でつまずきやすいポイントを理解するための連載
・JS特有の「this」の使い方6つをおさえよう
・2つの利用シーンから理解する「クロージャ」活用法
・実行処理の図を見て「非同期処理」について知ろう
・Promiseを理解しasync/await で読みやすい「非同期処理」を書く
・Ajaxの利点や書き方

.ul { list-style: none; } /*小見出しのデザイン*/

機能としてのサーバーと、物理的なマシンとしてのサーバー

-->
'); $backTopNav.appendTo($('.no-entry .entry-content')); } ////////////////////////////////////////////////////////// /* /* カテゴリ **/ function setLocalNav(){ // 右カラム:イベントレポートのナビ生成 var $reportNav = $('#js-report-nav'); if ( $reportNav.length > 0 ) { var reportCatParentIndex = 2; var reportCats = itseCategory[reportCatParentIndex][1]; $.each(reportCats,function(index, val) { $('
  • '+itseCategoryText[reportCatParentIndex][1][index]+'
  • ').appendTo($reportNav); }); } // 右カラム:コラム・連載のナビ生成 var $columnNav = $('#js-column-nav'); if ( $columnNav.length > 0 ) { var columnCatParentIndex = 1; var columnCats = itseCategory[columnCatParentIndex][1]; $.each(columnCats,function(index, val) { $('
  • '+itseCategoryText[columnCatParentIndex][1][index]+'
  • ').appendTo($columnNav); }); } } function setClassCategories(){ $('.categories a').each(function(){ if(!$(this).hasClass('.urllist-category-link')){ $(this).addClass('urllist-category-link category-'+$(this).text()); } }); } setLocalNav(); setClassCategories(); ////////////////////////////////////////////////////////// /* /* カレンダー **/ var eventData; var Calendar = function (elm) { this.$elm = elm; } Calendar.prototype.init = function () { this.$calendar = null; this.calendarDate = {}; this.settings = { year: 0, month: 0, weekValue: ['æ—¥', '月', '火', 'æ°´', '木', '金', '土'], areaClass: 'calendar', activeDateClass: 'active-date', prevNavClass: 'prev', nextNavClass: 'next', sunClass: 'sun', satClass: 'sat', todayClass: 'today', emptyClass: '', headerFormat: 'yå¹´m月', emptyValue: ' ' } var date = this.getDate(); this.calendarData = { year: date.year, month: date.month, date: date }; this.today = date.days; this.currentMonth = date.month; this.currentYear = date.year; this.setup(); } Calendar.prototype.getDate = function () { var date = new Date(); var toYear = date.getFullYear(); var toMonth = date.getMonth() + 1; var toDays = date.getDate(); var result = {}; result = { year: toYear, month: toMonth, days: toDays }; return result; }; Calendar.prototype.getPrevDate = function () { var result = { year: this.calendarData.year, month: this.calendarData.month }; if (result.month === 1) { result.year--; result.month = 12; } else { result.month--; } return result; }; Calendar.prototype.getNextDate = function () { var result = { year: this.calendarData.year, month: this.calendarData.month }; if (result.month === 12) { result.year++; result.month = 1; } else { result.month++; } return result; }; Calendar.prototype.zeroFormat = function (v, n) { var vl = String(v).length; if (n > vl) { return (new Array((n - vl) + 1).join(0)) + v; } else { return v; } }; Calendar.prototype.weekClass = function (weekNumber, targetDate) { var date = this.getDate(); var classArr = []; var classStr = ''; if (weekNumber === 0) { classArr.push(this.settings.sunClass); } else if (weekNumber === 6) { classArr.push(this.settings.satClass); } if (targetDate && targetDate === '' + date.year + this.zeroFormat(date.month, 2) + this.zeroFormat(date.days, 2)) { classArr.push(this.settings.todayClass); } if (classArr.length > 0) { classStr = ' class="' + classArr.join(' ') + '"'; } return classStr; }; Calendar.prototype.createBaseHTML = function () { var htmlStr = ''; htmlStr += '\n
    \n'; htmlStr += '
    \n'; htmlStr += '
    \n'; htmlStr += '
    \n'; htmlStr += '
    \n'; htmlStr += '
    \n'; htmlStr += '
    \n'; htmlStr += '
    \n'; htmlStr += '
    \n'; this.$calendar = $(htmlStr); this.$calendar.addClass(this.settings.areaClass); this.$calendar.find('.calendar-month').addClass(this.settings.activeDateClass); this.$calendar.find('.calendar-prev').addClass(this.settings.prevNavClass); this.$calendar.find('.calendar-next').addClass(this.settings.nextNavClass); }; Calendar.prototype.createBody = function () { var self = this; var year = this.calendarData.year; var month = this.calendarData.month; var lastdays = new Date(year, month, 0); var forDate = new Date(year, month - 1, 1); var emptyCell = '
  • ' + this.settings.emptyValue + '
  • \n'; var rowCount = 1; var htmlStr = ''; var i; var j; var len; htmlStr = '\n'; htmlStr += '\n'; htmlStr += '\n'; this.$calendar.find('.calendar-body').html(htmlStr); }; Calendar.prototype.setHeader = function () { var headerText = this.settings.headerFormat.replace('y', this.calendarData.year).replace('m', this.calendarData.month); this.$calendar.find('.calendar-month').html(headerText); }; Calendar.prototype.setup = function () { var self = this; this.createBaseHTML(); this.setHeader(); this.createBody(); // ヘッダー内、左右のリンクをクリックしたときの処理 this.$calendar.find('.js-change-month a').on('click', function () { var mode = $(this).attr('href').replace(/^#/, ''); if (mode === 'prev') { self.prevMonth(); } else if (mode === 'next') { self.nextMonth(); } return false; }); this.$elm.append(this.$calendar); }; Calendar.prototype.changeCalendar = function (year, month) { var date = this.getPrevDate(); if (year && month && String(year).match(/^[0-9]{4}$/) && String(month).match(/^[0-1]?[0-9]$/)) { this.calendarData.year = Number(year); this.calendarData.month = Number(month); this.setHeader(); this.createBody(); } }; Calendar.prototype.prevMonth = function () { var date = this.getPrevDate(); this.changeCalendar(date.year, date.month); }; Calendar.prototype.nextMonth = function () { var date = this.getNextDate(); this.changeCalendar(date.year, date.month); }; ////////////////////////////////////////////////////////// var getEventData = function () { var d = $.Deferred(); // dataを作る var data = {}; eventSchedule.forEach(function(v, i){ var date = new Date(v.date); var event = { category: v.category, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), url: "http://www.itstaffing.jp" + v.url, title: v.title, }; data[i+1] = event; }); // ここまで d.resolve(data); return d.promise(); } if ($("#calendar").length > 0) { var getData = getEventData(); getData.then(function (data) { eventData = data; //console.log(eventData); var calendar = new Calendar($("#calendar")); calendar.init(); }); } //}; }); }();