14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

un-T factory! XA Advent Calendar 2020Advent Calendar 2020

Day 22

JavaScriptに手を出すと最初に戸惑う3つのこと

Last updated at Posted at 2020-12-21

はじめに

プログラミング言語にはそれぞれ特徴がありますが、いろんな言語を行ったり来たりしてるとルールを忘れがちです。私がそうです。

そんな数ある言語の中から、本稿ではJavaScriptの超基本的だけど最初は戸惑うかもしれないことをまとめます。

テーマは3つ。

  1. ECMAScriptとは
  2. 変数宣言の仕方
  3. 厳密等価演算子

私はこれまでJavaScriptはちょこちょこ使う程度で、使う度に「あれ?これどうだったっけ、、、(Google先生に聞く)」というルーティンがありました。こうなったものを備忘録的に残して、**「もう忘れない」**という強い意志をここに記したいと思います。

1. 「ES6?ES2015??」

JavaScriptに関することをググっていると、高確率でESとかECMAScriptという文字を見かけます。
私は「ESってなんやねん、エントリーシートか?」と疑問を持ったことを覚えています。
そもそもの部分ではありますが、JSを理解する上で重要だと思うので、改めて確認しておきましょう。

ES(= ECMAScript): JavaScriptの標準仕様

ESとは、ECMAScriptの略称で、そのECMAScriptとは、Ecma Internationalという国際的な標準化団体が策定した、JavaScriptの標準のことを指します。

標準化に際しての経緯や、とあるバージョンは途中で放棄しちゃったりなど、いろいろと歴史があって面白いのですが、長くなるのでここでは割愛します。

ECMA-262という規格番号で標準化されていますが、度々改訂が行われています。
その改訂が行われたタイミングで版番号が更新され、ES1から始まり2、3、4、5、6、…と続いている、わけではなく、このナンバリングはES5までで(厳密にはマイナーバージョンの5.1)、その先からは「毎年改訂しちゃうよーん:smirk::metal:」と張り切ったため、発行年が名前にくっつくことになっています。
つまり、実質ES6にあたるバージョンからECMAScript 2015と呼ぶことにしたのです。
ただし、ナンバリングの呼び方が消滅したわけではなく、今でも呼びやすいからという理由でES6という名前は使われています。

ということで、ES6とES2015は同じものを指します。
じゃあ、ES6って呼ぶのが間違いなのかというとそうでもないみたいで、「正式に呼ぶなら発行年呼びのほうがいいなぁ」くらいのニュアンスみたいです。

ES6 is colloquial shorthand for “ECMA-262, Edition 6”. ES6 was published as a standard in 2015. The actual title of the ES6 specification is ECMAScript 2015 Language Specification and the preferred shorthand name is ECMAScript 2015 or just ES2015.
Some ECMAScript Explanations and Stories for Daveより

2020年12月現在の最新版ECMAScript

2020年6月に改訂されたバージョン11、ECMAScript 2020が最新版です。

もちろん2021も着々と仕様策定が進んでおり、今時点で確定したものは以下の通り。

  • String.prototype.replaceAll
  • Promise.any
  • WeakRefs
  • Logical Assignment Operators
  • Numeric separators

うーん、楽しみですね(適当(白目(勉強しなきゃ(焦り))))


2. 「var?let?const?」

私は変数宣言といったら、intとかstringとか静的型付けしたくなるのですが、世の中には動的型付けなる便利(そう)な型付けも存在します。

しばしば、静的型付け言語派閥 vs 動的型付け言語派閥の宗教戦争が勃発するのは有名な話です。

動的型付けとは、プログラム実行前に型を決めておくことをせず、実行されたときの実際の値を見て型を決めるというものです。

動的型付け言語で有名なものは

  • Python
  • PHP
  • Ruby
  • LISP

などなど、いろいろあります。

何を隠そうJavaScriptもその動的型付け言語の一つで、型をあんまり意識しなくてもちょろちょろ〜っと書けちゃうわけです。

では、実際にちょろちょろ〜っと書いてみましょう。

:blush:「よっしゃ!足し算させてみよか」

:rolling_eyes:「ふむふむ…MDN Web Docsによると、宣言の仕方はvarletconstの3つか…」

:relieved:「『varは変数を宣言し、ある値に初期化することもできる。』…じゃあこれでいっか」

足し算.js
var augend = 1;
var addend = 2;

console.log(augend + addend);

:blush:「できた!!!!」

:no_good_tone1:varは推奨されてへんで

:muscle::baby:「?????」

3つある宣言

JavaScriptの変数宣言は3つ存在し、前述の通り、varlet、**const**です。

結論をいうと、**イチオシはconst**です。
ちなみに、constで宣言する際は、必ず初期値を代入する必要があることは覚えておきましょう。

それぞれの特徴を表でまとめると以下の通り。

var let const
再代入 ×
再宣言 × ×
ブロックスコープ ×

ブロックスコープという書き方が若干曖昧ですが、つまり、{}で囲んだところをブロックを呼び、その中でletもしくはconstで宣言された変数はそのブロックの中だけで有効、varはその宣言されたブロックを含む関数内もしくはグローバルスコープで有効ということです。

スコープ例
if (true) {
  var a = 1;
  let b = 2;
}

console.log(a);    // 1
console.log(b);    // Uncaught ReferenceError: b is not defined 

上記の例では、ブロック内でavarbletで宣言されており、前述の通りletで宣言するとブロック内のみで有効な変数となりますので、ブロックの外でbを出力しようとしても「bは未定義だよー」とReferenceErrorが吐かれます。

こうやってみてみると、varは宣言方法を3つ並べたときに1番最初に紹介されるし、JavaScript関係の解説サイトとかでも割と使われているし、再代入できるし、再宣言もできるし、スコープ関係ないしで、使いたくなっちゃうところですが、残念ながらvarは非推奨です。

ブラウザの対応状況によっては、letconstが使えないこともあるかもしれませんが、そんな化石プラットフォームを使っている白亜紀在住で移動手段が恐竜みたいな方々のサポートは投げ捨てていきましょう。

宣言はconst

letは、varの代わりといってもいいでしょう。

:kissing_smiling_eyes:「じゃあ、letを使えばいっか」

という考えは許されません。極刑に値します。斬首です

なぜならば、"代わり"ということは**letvar**だからです。
varが非推奨なのにletが無条件に許されるわけがありません。

| const javascript 使い方 | 検索 |
でググると大抵こんな記事を見つけます。

中身の値を変えられたくないもの、変えないことが明らかであるものに関しては、値の書き換えが禁止されているconstを用いると良いですよ(胡蝶しのぶスマイルを添えて)

constさんをそんな脇役みたいな言い方しやがって……!!

**『特別な理由がない限り、全ての変数はconstで宣言するべき』**です。

仮に、古の時代にvarで宣言していた部分を全てletで宣言したとして、間違いなく「可読性が上がった」と言えるでしょうか?

可読性の観点からいえば、再代入が行われる、もしくは行われる可能性があるコードは好ましくありません。あとからそのコードを見る人は、「値が変わるかもしれないから注意して読もう」と余計な労力をかける必要があるためです。

constであれば、再代入されることはないので、この無駄な手間とドキドキをなくすことができます。

constは『改変できない』ではない

一つ注意すべき点は、const再代入できないのであって、値を改変できないのではないということです。

みんな大好きMDNのドキュメントには以下のように書かれています。

const 宣言は、値への読み取り専用の参照を作ります。これは、定数に保持されている値は不変ではなく、その変数の識別子が再代入できないということです。たとえば、定数の中身がオブジェクトの場合、オブジェクトの内容(プロパティなど)は変更可能です。

ちょっと何言ってるかわからない状態なので、順を追って説明をしていきます。

プリミティブ型とオブジェクト型

JavaScriptのデータは、大別するとプリミティブ型オブジェクト型に分けられます。

プリミティブ型とは、メソッドを持たないデータのことで、以下の6つの型があります。

  • String(文字列)
  • Number(数値)
  • BigInt
  • Boolean(真偽値)
  • undefined
  • Symbol

オブジェクト型は、配列やmapなど、プリミティブでないものを指します。

変数の持ち方

変数に入っているデータは、その変数に代入した値が格納された領域の参照値となります。
JSの変数はC言語とかのポインタ的な感じ、と私は理解しています。(実際は違うっぽいですが)

例えば、const a = 1;という文が実行されたとき、中では以下のように処理がなされます。

  1. 変数aに値を格納するための領域が割り当てられる
  2. その領域に値を格納する
  3. 変数aに値を格納した領域の参照値(ref1)を入れる

Address.jpg

これはプリミティブ型もオブジェクト型も同様です。

constは参照値の変更を禁止している

constで宣言した際のからくりについてお話します。

まず、プリミティブ型について。

const a = 1;

a = 10000;

変数aに入っているのは「1」という数値を格納した領域の参照値です。
Address (4).jpg

再代入するためにa = 10000;とすると、今度は「10000」という数値を格納した領域の参照値を変数aに入れようとします。
constで宣言されていた場合、この変数に入れた参照値の変更ができない→結果として再代入できないということになるのです。
Address (5).jpg

ちょっとだけややこしく感じがちなのが、オブジェクト型。

const obj = [1, 2, 3];
obj = ['a', 'b', 'c'];    // Uncaught TypeError: Assignment to constant variable.

これはもちろん怒られます。['a', 'b', 'c']という配列が格納された参照値を代入しようとするためです。

しかし、以下の場合は違います。

const obj = [1, 2, 3];
obj[0] = 'a';

console.log(obj);    // ['a', 2, 3]

これはバッチリobjの中身が書き換えられています。なぜならば、objの格納先の参照値は変更していないからです。
あくまで代入したのは、配列オブジェクトobjのインデックス0の部分に対してなので、objの配列が格納されている領域は変わっていません。もちろんpush()pop()などオブジェクトに対するメソッドも問題なく使用できます。

Address (2).jpg

Address (3).jpg

定数の中身がオブジェクトの場合、オブジェクトの内容(プロパティなど)は変更可能です。

が意味することは、こういうことなんですね。

varは非推奨

さて、varは事実上非推奨です。それでも残っているのは、昔のコードとの互換性のためでしょう。
では、なぜvarは非推奨なのでしょうか。

理由としては、以下の2つがあげられます。

  1. 変数の中身をころころ書き換えられたり、何回も宣言されるとワケワカメになる→バグ
  2. 変数巻き上げ発生時がややこしや→バグ

要するに、**コードの可読性を上げてバグを少なくしましょうね〜**ということなんですね。

変数の書き換え

特に長いコードを書いているときに変数をvarで宣言すると、前に宣言したのにもう1回宣言してしまったり、if文などのブロック内で操作した値を再代入したことで関数全体に影響を与えてしまったりなど、意図しない動きを生じさせてしまう可能性があります。
letconstを使っておけば、意図しない変数書き換えが発生しづらくなるでしょう。

独特な挙動 『変数巻き上げ』

「初学者が戸惑うこと」の一つとして大項目で紹介しようかとも思いましたが、小項目にランクダウンした『巻き上げ』くんです。

変数の巻き上げとは、宣言された変数は文の先頭または関数の先頭で宣言されたとみなされるというものです。

:yum:「は?」

よくわかりませんね。例を示しましょう。
以下のようなコードを実行します。

var a;
console.log(a);

コンソールには**undefined**が出力されます。
これは、初期値なしでvarletで宣言した場合にはundefinedという値をとる、というルールに基づくものです。

それでは、以下のように、宣言と出力が逆になるとどうでしょう。これを実行すると何がコンソールに出力されるでしょうか。

console.log(a);
var a = 1;       // 宣言が後

正解は**undefined**です。

:yum:「は??」

つまり、以下のコードと同様の動きをするということです。

var a;    // 先頭で宣言
console.log(a);
a = 1;

:yum:「は?????」

と言われてもこれが『変数の巻き上げ』なので、こうなのだと理解するしかありません。あしからず。
関数内の変数も同様に、宣言された関数の先頭に"巻き上げ"られます。

var a = 1;

var func = () => {
  // var a;             <- このように宣言されているのと同じ
  console.log(a);    // 出力: undefined
  var a = 2;
};

func();

さて、巻き上げの説明が長くなりましたが、varの非推奨話と何が関係あるかというと、varで宣言すると仮に巻き上げが発生してundefinedになってもそれに気づかず処理が進んでしまい、バグを誘発させる恐れがあります。
一方、letconstで宣言しておくと、巻き上げが発生すると参照エラーとなり、コードの実行が止まります。

console.log(a);    // ReferenceError
const a = 1;

よって、結果論的ではありますが、letconstで宣言すれば意図しない巻き上げによるバグを防ぐことができる、ということになるわけです。
そもそもですが、巻き上げが発生するようなコードを書くとセンス/Zeroと思われるのでやめたほうがいいです。たぶん。

どうでもいいけど、このエラーが発生する領域をTemporary Dead Zoneっていうんですって。なんかカッコいいからとりあえず書いておきますね。

余談: 関数の巻き上げ

関数宣言に関しても同様に巻き上げが発生します。

chosenOne('Anakin');

function chosenOne(name) {
  if (name === 'Anakin') {
    console.log('You were the chosen one!');
  }
}

これは、関数宣言が行われる前に関数を呼び出しているように見えますが、ちゃんと関数が実行されます。
関数の巻き上げが発生しているからです。

厳密に言えば、巻き上げが発生するのは関数宣言です。
つまり、以下のような関数式は巻き上げられないということです。

chosenOne('Anakin');    // エラー

const chosenOne = function(name) {
  if (name === 'Anakin') {
    console.log('You were the chosen one!');
  }
};

3. 「等価演算子は=2つだよ(決めつけ)」

edo.js
const edoEst = answerYear => {
  const edoEstYear = '1603';

  if (edoEstYear === answerYear) {
    console.log('正解!');
  } else {
    console.log('不正解...');
  }
};

:open_mouth:「おっ、こんなところに江戸時代が始まった年のクイズ関数が宣言されてる。興奮してきたな。ちょっと答えてみよ」

回答
edoEst(1603);    // 出力: 不正解...

:confounded:「あれ?1603年じゃないっけ??おかしいなぁ」
:frowning:「あ、edoEstYearanswerYearを比較してるところの=3つになってるよ…タイポだろうなぁ直しとこ」

// 省略
  if (edoEstYear == answerYear) {
// 省略

edoEst(1603);    // 出力: 正解!

:blush:「よしよし、『正解!』って出たな!」

:cop_tone1:「……」

等価演算子は=3つのものもある

===でもちゃんと条件分岐して『不正解』を出していたわけですから、タイポではないことは明確ですが、なぜ正しい値である『1603』を入力しても不正解と判定されたのでしょうか。

===厳密等価演算子といいます。
つまり、==厳密じゃない等価演算子ということになります。

厳密じゃない等価演算子は、比較対象の型が異なる場合には型の変換をして型をあわせてから比較を行う賢い子です。
つまり、以下のこれらは全部trueになります。すごい。

1 == '1';
0 == false;
null == undefined;

const number1 = new Number(3);
number1 == 3;

一方、===は「型が同じなら比較するよ、異なるもん持ってきたら中身も見ないよ」とツンツンした子です。
つまり、上に示した例は全てfalseが返ってくることになります。

先程の江戸の例に戻ると、正答であるedoEstYearの宣言文を見ると

edoEstYear宣言部分
const edoEstYear = '1603';

右辺はシングルクオーテーションで囲んであり、文字列で定義されていることが分かります。

しかし、解答としてedoEst関数の引数を

回答
edoEst(1603);

数値で渡しており、厳密等価演算子による比較を行うと型が異なるため、比較結果がfalse、『不正解』が返ってきました。

(厳密等価演算子はPHPにもありますが、)他のプログラミング言語を扱っていると、何も考えずに==を使って比較しても、比較する値の型が異なればコンパイル時などにエラーが吐かれたりして、間違って使っても気付きやすかったりします。
JavaScriptにおける==は便利ですが、便利ゆえに問題に気付きにくくハマる原因になる可能性があります。

正確に意図したものを作るためには、型まで厳密に比較してくれる===を使うほうがプログラムの意図も伝わりやすくなり、バグも少なくなることが期待されるので、比較する際は=3つの方も思い出して一考してみるべきでしょう。

ちなみに、等価の逆、!==厳密非等価演算子も存在するのでこちらもあわせて覚えておきましょう。

まとめ

基本的なことだけど、知らないと戸惑うことをまとめてみました。

記事をまとめるにあたって、自分で調べながら再度確認しましたが、「あーそうだったっけ」とか、「そーなんだ!」というものがあり、非常に身になりました。
この記事を読んだ方にとっても、有意義な時間になったことを願います。

一つ一つのボリュームがありすぎて、それぞれ1記事にできそうなレベルのものを一つの記事にまとめたためかなり長めになってしまいました。
自分が理解できるレベルまで落とし込むのと同時に、尚且読んでて面白いと思われるような記事を書きたかったため、逆に分かりづらかったり噛み砕いた表現が多かったりしたかもしれませんがご了承ください。

参考文献

14
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?