Reactを習得していこうと思うと結局のところモダンなJavaScriptをちゃんと理解してないと詰みポイントが多く、今更ながらモダンなJavaScriptで結構知らない記法や環境設定で理解していない部分が多かった事に反省しました。
ちゃんと復習します、ハイ。
PHPやPythonやRubyなどはメジャーアップデートによるバージョンである程度の区切りがされていてバージョンによっては別物に近い言語のような変更がされる場合がありますが、
ブラウザ上でのみ動く言語という特殊なJavaScriptは言語自体にメジャーバージョンは存在しません。
1995年にNetscape社が開発したNetscape Navigatorブラウザに搭載したことをきっかけに、Microsoft社がJScriptという互換性のあるJavaScriptをInternet Explolerに搭載し、シェアを奪い合ったブラウザ戦争が誕生のきっかけとなります。
ブラウザのエンジンごとに異なる仕様が存在するカオスな状態だったため、各ブラウザ同士でも最低限のJavaScript仕様を統一させる標準規格というものが必要になり、
ES2015などのES(ECMAScriptの略)という標準規格によるバージョンがつく事で大まかな仕様の区切りがされ、現在はその仕様に則って各ブラウザに搭載されています。
欧州電子計算機工業会(European Computer Manufacturers Association)という団体が標準規格が定めたJavaScriptのことをECMAScriptと呼びます。
ECMAScriptはあくまで規格であり、実行する環境や言語そのものの呼び名では無い事に注意です。
ES1〜3までは1997年から毎年公開されましたが、4ではブラウザ毎の進化や分裂による仕様の違いが極限まで複雑化したことにより放棄され、セキュリティ上の脆弱性によりJavaScript機能を遮断する設定ができてしまうほどに2005年頃のGoogleMapによるインタラクティブなWEBページの実現までは長らくJavaScriptは不遇の時代を迎えました。
非動機通信を実用化したAjaxによってJavaScriptが再注目されるようになった事で再び標準仕様の必要性が高まり、2009年にES5がリリースされJSONがサポートされる様になり2015年にES6がリリースされてからは毎年バージョン名を年号に改定していこうという事になりました。
呼び名もES6からはES2015となり、2016年からは毎年ごとの標準仕様の命名としてES2016、ES2017、ES2018、ES2019となり今年リリースされたのがES2020です。
新しい規格の仕様が実装されるタイミングはブラウザごとに違うので新しいバージョンがリリースされたらその記法がすぐに使えるわけではないので、主要ブラウザのリリースノートなどを見ながら順次取り入れましょう
ECMAScriptとして大きく改定されたのがES2015による現在の主流のモダンな記法となる機能が多く追加された事でReactなどで開発する場合はこの辺をちゃんとマスターする必要性があるためおさらいします。
var val = "これまでの変数";
console.log(val); // "これまでの変数"
val = "上書いた変数";
console.log(val); // "上書いた変数"
var val = "再宣言した変数";
console.log(val); // "再宣言した変数"
これまでの変数宣言に使用していたvarは良くも悪くも上書きが可能で、コード全体を認識できている間は問題なくても複雑化していき意図しない場所で宣言した同名の変数によって上書かれ、機能が動作しないという事故リスクがありました。
let val = "let変数";
console.log(val); // --> "これまでの変数"
// 再代入による上書きは可能
val = "let上書き";
console.log(val); // --> "let上書き"
// ブロック中でローカル変数を宣言
if (true) {
let val = "ローカル変数宣言";
console.log(val); // --> ローカル変数宣言
}
// ブロック外からは、ローカル変数の参照はできない
console.log(val);
// 再定義はエラー
let val = "let再宣言";
console.log(val);
// --> Uncaught SyntaxError: Identifier 'val' has already been declared
let
ではvar
同様に上書きは可能であるものの、再宣言はエラーになる仕様なため、別の作業者が意図せず同名の変数を宣言した場合は既に宣言されている変数名に対してはエラー表示を確認できるようになっています。
const val = "const変数";
console.log(val); // --> "const変数"
// 再代入による上書きはエラー
val = "const上書き";
console.log(val);
// --> Uncaught TypeError: Assignment to constant variable.at <anonymous>:4:5
// 再宣言もエラー
const val = "const再宣言";
console.log(val);
// --> Uncaught SyntaxError: Identifier 'val' has already been declared
// 初期化していない宣言もエラー
const val;
// --> Uncaught SyntaxError: Missing initializer in const declaration
// ブロック外からの参照もできない
if (true) {
const local = 'local';
console.log(local); // --> local
}
console.log(local);
// --> ReferenceError: local is not defined
constはより厳密で、再宣言も上書きも不可能な定数として定義するための仕様になります。
まずはこの変数宣言にvar
ではなくconst
かlet
が使われているかがイマドキな書き方のわかりやすい目安になっています。
この2種類を使い分ける事でlet
では「この変数は値が上書きする前提で宣言している」const
では「最初から値の変更の予定が無く、他の場所ではこの変数名は使えない」というコードを書いた人の設計思考がより伝わりやすくなり、事故のリスクも減りました。
ただしconst
はすべてにおいて変更ができないということはなく例外が存在します。
変更できないプリミティブ型と、中身の更新が可能なオブジェクト型のケースがあることは覚えておきましょう。
変更不能なプリミティブ型 | 変更可能なオブジェクト型 |
・真偽値(true/false) ・数値(1, 0.5) ・文字列('あいうえお', 'ABC') ・未定義(undefind) ・値なし(null) | ・オブジェクト ・配列 ・関数 |
// オブジェクトの定義
const obj_A = {
name: "田中",
age: 30
}
console.log(obj_A);
// --> { name: '田中', age: 30 }
// プロパティ値の変更は可能
obj_A.name = "佐藤";
obj_A.age = 40;
console.log(obj_A);
// --> { name: '佐藤', age: 40 }
// プロパティの追加も可能
obj_A.address = "東京";
console.log(obj_A);
// --> { name: '佐藤', age: 40, address: '東京' }
// プロパティの削除も可能
delete obj_A.age;
console.log(obj_A);
// --> { name: '佐藤', address: '東京' }
// オブジェクト自体の更新はエラー
const obj_A = {
name: "鈴木",
age: 50
}
// --> SyntaxError: Identifier 'obj_A' has already been declared
テンプレート文字列はこれまで"+
"で連結させていた文字列と変数を、文字列の中で変数や関数を展開できる記法です。
const name = "山田";
let age = 20;
// ES2015以前の連結記法
console.log("私の名前は"+name+"です、今年で"+age+"歳です");
// テンプレート文字列による結合
console.log(`私の名前は${name}です、今年で${age}歳です`);
// 5年後の年齢を加算する関数
function afterYear5(age){
return age + 5;
}
// テンプレート文字列による変数と関数の結合
console.log(`5年後は${afterYear5(age)}歳で、10年後は${afterYear5(age) + 5}歳です`);
まず出力の大枠となる文字列をバッククォート(`
)で囲み、文字列内で出力したい変数や関数をドルマークと波括弧(${ }
)で囲むことで関数の返り値や変数の計算も直接展開することができます。
ES2015により関数の記述の省略が可能になりました。
これまでのfunction
の表記が=>
という記号に変わり、矢印(アロー)に見えることからアロー関数と呼ばれます。
// 従来の関数記述
function funcA(value){
return value;
}
console.log(funcA("funcA")); // --> funcA
// アロー関数
const funcB = (value) => {
return value;
}
console.log(funcB("funcB")); // --> funcB
// 引数()は省略可
const funcC = value => {
return value;
}
console.log(funcC("funcC")); // --> funcC
// 引数が2個以上の場合は()が必要
const funcD = (value1, value2) => {
return value1 + value2;
}
console.log(funcD("funcD", "実行しました"));
// --> funcD実行しました
// 単一行で完結できる場合は{}も省略可
const funcE = (num1, num2) => num1 + num2;
console.log(funcE(1, 2)); // --> 3
// ()内にまとめて省略
const funcF = (name, age) => (
{
name: name,
age: age
}
)
console.log(funcF("田中", 30));
オブジェクトや配列からプロパティを抽出して値を参照することが可能になりました。
// 分割元の配列
const array = ["山田", "30", "サッカー"];
// これまでの抽出手段
var name = array[0]; // 山田
var age = array[1]; // 30
var hobby = array[2]; // サッカー
// 分割代入による抽出
const [name, age, hobby] = array;
const message = `私の名前は${name}で、年齢は${age}歳。趣味は${hobby}です`;
console.log(message);
// --> 私の名前は山田で、年齢は30歳。趣味はサッカーです
// 抽出したい値だけ宣言
const [name, , hobby] = array;
console.log(name); // 山田
console.log(hobby); // サッカー
// スプレッド構文で抽出したい1個とあまりは別の変数に代入
const [a, ...b] = ["山田", "30", "サッカー"];
console.log(a); // 山田
console.log(b); // [30, "サッカー"]
// 分割元のオブジェクト
const profile = {
name: "山田",
age: 30
}
// これまでの抽出手段
var name = profile.name;
var age = profile.age;
// 分割代入による抽出
const { name, age } = profile;
const message = `私の名前は${name}で年齢は${age}歳です`;
console.log(message)
// --> 私の名前は山田で年齢は30歳です
// コロンで抽出元のプロパティと別名で参照することも可
const { name: newName, age: newAge} = newProfile;
const newMessage = `私の名前は${newName}で年齢は${newAge}歳です`;
console.log(newMessage);
// --> 私の名前は山田で年齢は30歳です
APIの設計上レスポンスに処理に利用するオブジェクトや配列内のデータをすべて含めたくない場合は特定のプロパティだけに限定した構造に作り替えやすくなったり、
関数の戻り値を配列として複数の値を返したい場合に直接変数として代入できるのでコードがスッキリします。
// 配列を返す関数
function a(){
return ["A", "B"];
}
// 関数の戻り値を直接変数に代入
let [x , y] = a();
console.log(x); // A
console.log(y); // B
関数の引数やオブジェクトの分割代入に対して値が設定されていない場合の初期値を事前に設定する事ができるようになりました。
// 関数の引数にデフォルト値「ゲスト」を設定
const hello = (name = "ゲスト") => {
return `こんにちは、${name}さん`;
}
// 引数を代入した場合は引数の値が参照される
console.log(hello("山田")); // --> こんにちは、山田さん
// 引数を代入しなかった場合はデフォルト値が参照される
console.log(hello()); // --> こんにちは、ゲストさん
ES2015以前は値を設定していない変数を渡した場合はundefinedが返ってきてしまう仕様でしたが、デフォルト値を設定しておくことで引数に値が無かった場合も意味不明な出力を対策できるようになります。
スプレッド構文は一見ソースを見ただけではわかりにくいですが、配列やオブジェクトに対してコピーや結合などの操作が可能です。
// オリジナルとなる配列
const arr_random = [1, 7, 5, 9, 3]
// オリジナルを複製したコピー配列
const arr_copy = [...arr_random]
// オリジナルから指定部分のみ抽出して代入
const [a, , b] = arr_random
// コピー配列の中身をソート
const arr_sort = arr_copy.sort()
// コピー配列に10を追加して複製
const arr_add = [...arr_copy, 10]
console.log(arr_random) // --> [ 1, 7, 5, 9, 3 ]
console.log(arr_copy) // --> [ 1, 3, 5, 7, 9 ]
console.log(a) // --> 1
console.log(b) // --> 5
console.log(arr_sort) // --> [ 1, 3, 5, 7, 9 ]
console.log(arr_add) // --> [ 1, 3, 5, 7, 9, 10 ]
// オリジナルとなるオブジェクト
const obj = { a: 1, b: 2, c: 3 }
// 元オブジェクトの複製
const obj2 = {...obj}
// オリジナルから指定部分だけ抽出して代入
const {a} = obj;
// 新しいキーと値を追加した新規作成
const obj3 = {...obj, d:8, e:10}
// プロパティの値を置き換えて新規作成
const obj4 = {...obj, ...{a:3, b:4}}
// オブジェクトの結合
const obj5 = { f: 6, g: 7 }
const joinObj = {...obj,...obj5}
// オブジェクトの初期化を変数で展開
const z = 26;
const obj_z = { z };
console.log(obj2) // --> { a: 1, b: 2, c: 3 }
console.log(obj3) // --> { a: 1, b: 2, c: 3, d: 8, e: 10 }
console.log(a) // --> 1
console.log(obj4) // --> { a: 3, b: 4, c: 3 }
console.log(joinObj) // --> { a: 1, b: 2, c: 3, f: 6, g: 7 }
console.log(obj_z) // --> { z: 26 }
元となる配列やオブジェクトを簡単に複製でき、複製先にも置換や追加が可能なので
元配列を破壊せずに中身を変更させたい場合などに利用できます。
map関数はfor文のようにループ処理を配列を順番に処理して、結果を新たな配列として受け取る事が可能です。
// 会計前のカート商品価格一覧
const settlement = [1000, 500, 1800, 700];
// 配列内の値を順番に確認
settlement.map((item) => console.log(item))
// 選択商品に消費税10%を追加(小数点は切り捨て)
const order = settlement.map((item) => {
return Math.floor(item * 1.1);
})
console.log(settlement);
// --> [ 1000, 500, 1800, 700 ]
console.log(order);
// --> [ 1100, 550, 1980, 770 ]
配列に対してループ処理をかける用途としてはfor-of文やforEach文も同じような処理をしてくれますが、map関数は前者2択よりも処理速度としては遅く、処理の戻り値を新しい配列として返す専用の関数なので新たな配列として処理したい場合にのみmap関数を使いましょう
filter関数はmap関数のように配列に対して処理を行う用途は同じですが、filter関数は条件に一致する値のみを配列の中から取り出すことができます。
// 得点一覧
const score = [50, 80, 96, 72, 66]
// 80点以上の得点のみ合格者用の配列に返却
const eligibility = score.filter((num) => {
return num >= 80;
})
console.log(score);
// --> [ 50, 80, 96, 72, 66 ]
console.log(eligibility);
// --> [ 80, 96 ]
padStartとpadEnd関数は前と後ろにゼロパディングなどの桁埋めを簡単に行なってくれる関数。
延長させたい文字列に対して第一引数は文字数、第二引数は延長するための文字列を設定。
なぜ今までなかった?というくらい他の言語ではあって当然というくらいの地味なものですがES2015からようやく実装されました。
// 後ろから5文字の設定で足りないものは"0"出力
const zero_num = '1'.padSet(5, 0);
console.log(zero_num); // --> 00001
// 延長するための文字列(指定がなければ半角スペース)
const space_str = 'abc'.padStart(10);
console.log(space_str); // --> " abc"
// 全10文設定、足りない文字列は"foo"
const foo_str = 'abc'.padStart(10, "foo");
console.log(foo_str); // "foofoofabc"
// 全6文設定、足りない文字列は"123456"
const num_str = 'abc'.padStart(6,"123456");
console.log(foo_str); // --> "123abc"
// 前から5文字設定で足りないものは"#"出力
const hide_num = '1'.padEnd(5, '#');
console.log(hide_num); // --> 1####
// 延長するための文字列(指定がなければ半角スペース)
const hide_num = '1'.padEnd(5);
console.log(hide_num); // --> "1 "
flat関数はES2019からできたもので、ネスト(多元構造)された配列を一元化にもどすことができます。
const arr1 = [1, [2, 3], 4];
console.log(arr1.flat()); // --> [ 1, 2, 3, 4 ]
const arr2 = [1, [2, [3, [4]]]];
console.log(arr1.flat()); // --> [ 1, 2, 3, 4 ]
これはES2021から使える予定の全置換が可能な関数です。(Chromeでは既に使えます)
いままでもreplace関数での置換は可能でしたが、グローバルオプション(/対象文字/g)を設定しなければ最初に該当する文字以外はヒットせず、若干面倒なところがありましたが、全対象専用ができるので初学者への説明もわかりやすくなります。
// 従来のグローバルオプションでの総当たり置換
console.log('JavaScript'.replace(/a/g, 'A'));
// --> JAvAScript
// replaceAll関数での
console.log('JavaScript'.replaceAll('a', 'A'));
// --> JAvAScript
この他にもClassが使えるようになったり、async/awaitなどの大事だけどちょっと難しい新機能も増えましたが、
ReactフレームワークのNextJSやVue.jsフレームワークのNuxt.jsなどを利用してく前提なら、より書きやすくわかりやすいものになっているので今回は省略します。
昨今はJavaScriptだけでサーバーサイド側の開発も可能となったこともあり
Webアプリ開発環境の手法が大きく変わってきたため、この辺りから(自分を含めた)モダンなフロントエンド開発の流れについていけなくなった勢は多いと思うのでこの辺の登場人物を整理します。
Node.jsとはRubyやPythonと同じようにサーバーサイドでのJavaScriptを実行できる環境でECMAScript以外にCommonJSという規格仕様を実装しています。(現在はほぼECMAScriptの規格に集合されています)
従来のJavaScriptの動作はブラウザ上のみに限定され、他のサーバーサイド言語同様にパソコンのファイルの読み書きやネットワーク構造にアクセスするようなOSの機能に干渉できる言語とは違い、
悪意あるサイトにアクセスした際にOS機能へのアクセスされないようにブラウザ側で制限されていましたが、Node.jsによりJavaScriptでもWebサーバが構築できるようになしました。
パッケージマネージャーとはNode.jsで動作するパッケージ管理ツールです
Node.jsの開発によりJavaScriptでWebアプリをまるごと開発できる環境が構築できるようになりましたが、jQueryライブラリのアドオン同様に偉大なる先人が作ってくれたモジュールやパッケージを利用したいのが当然の流れですが、
OS機能へのアクセスができるNode.jsの設計上、自作のフォルダにファイルをポンと置けばすべて同じように動く訳ではないためにそれらの最適な場所へのインストールと、バージョン違いによるパッケージ同士の競合エラーなどが発生しないような依存関係の管理を簡単にしてくれるのがnpm
やyarn
と呼ばれるパッケージマネージャーです。
初学者の頃はインストールコマンドにnpmとyarnのどちらも見る機会が多いので「どっちを使えばいいの?」という疑問がでがちですが、
npmはJavaScriptの公式パッケージマネージャーであり標準ツールのため信頼性があり、ドキュメントも多くNodeと一緒に複数人でバージョンを揃えやすいです。
yarnはnpmよりも高速で安全というメリットも大きいですが、非公式であるが故に自身でインストールする手間やnode自体のバージョン管理にnodenvというツールを利用している場合はnodenv-yarn-installなどでさらに依存管理の手間が発生したりなどの自身の経験を踏まえたケースもあるため、npmのデメリットを実感できるようになってからyarnに移行する方が色々と幸せになれるので初学者や複数人で開発する前提の場合はnpmの方が無難だと思います。
npmにはnpxというインストールしていないパッケージを一度だけ実行できる機能も追加され、未インストールのGitHubやGistで公開されているスクリプトをインストールしないでGitHubやGistで公開されているスクリプトを実行できるので試してみたい時に使えます
※実際にインストールしないので容量も節約できて環境も汚染されないメリットはありますが悪意あるソースを実行してしまった場合にマルウェアに感染してしまう可能性もあるので注意
一番シェアの高いモジュールバンドラーをWebpackと言えばわかる人も多いと思いますが、モジュールバンドラーとは「本番用のビルドに必要なアプリケーションコード(CSSやjsファイル)のモジュールの依存関係を解決して1つまたは複数のパッケージにバンドル(束ねる)して最適化された静的ファイルをアウトプットする」目的を持っています。
ES2015以前はモジュール機能自体がJavaScriptには無かったので、JSの中から他のJSコードを読み込む事ができず、HTMLファイルにその都度ファイルを読み込む必要がありましたが、ES2015以降はJS内部で関連するモジュールをインポートする事で保守性が高くなりました。
開発時は各モジュールと実行コードは分けられている方が管理が楽ですが、実際に本番環境で実行する場合は一つのコードにまとめられている方がリクエストの数が減り、パフォーマンスが上がるという開発者のジレンマが生まれモジュールバンドラーは解決してくれます。
あらかじめ設定ファイルを記述しておくことでビルドコマンドを実行すればモジュールバンドラーが開発段階ではバラバラだったファイル構成を本番環境に必要なファイルだけをまとめてくれるようになりました。
モジュールバンドラーとひとことで言っても内部はさまざまなローダーとプラグインで構成され、ビルドの過程でローダーとプラグインがカスタムタスクを実行することで最適化してアウトプットしてくれています。
WebpackはJavascriptファイルのみそのままの状態で扱えますが、そのまま読み込めないJavascript以外のファイル(CSSやSVGなど)や画像データはURLとしてインラインアクセスできるかたちに変換する必要があります。
Reactなどでjs内にcssをimportする時はimport './style.css';
と記述しますが、これもcss-loader
と style-loader
が適応し、各ブラウザでサポート外の記述はトランスパイラーのBabelがbabel-loader
が適応されます。
ローダーで各ライブラリやモジュールの独自コードをwebpackで扱えるものに変換してくれますが、それだけでは終わらず各プラグインがさらに細かい最適化を行なってくれています
など他にも様々なプラグインのいろんな処理が通ります。
JSの歴史で記述したように基本仕様は定められるものの、各ブラウザが新仕様をサポートするタイミングはベンダーで異なるため本番環境へのビルドの際には、様々なJSのコードを設定した内容にトランスパイル(変換)する必要があり、その役目をしてくれるのがトランスパイラです。
トランスパイラツールで有名なBabelがありますが、主にこんな役割をしてくれます。
モダンな記法であげたES2015のアロー関数で書いたスクリプトは実はIEブラウザでは使えないといった事が後から発覚するようなケースもありますが、webpackだけ入れたつもりでもさらに内部に入ってるローダー(Babel)が密かにどのブラウザでも動くコードに変換してくれていたりします。
Linterとは解析ツールの総称で、設定されたルールに基づきソースコードを静的解析しコードの品質を担保してくれるもので、トランスパイラやコンパイラのように言語の仕様や構文の規則に則ったエラーを返す以上に、
未使用の変数の存在チェックや、無名関数や型が一致しない関数は使わせないなどの厳密なチェックが行えるツールです。
JavascriptやJSX、Vueファイルなどであれば ESLint。CSSであればStylelint。テキストであればtextlintなど、ソースに対応したLintが各種があり設定によっては問題のあるコードの自動修復なども行なってくれます。
ES2015からの劇的な進化でJavaScriptもかなりかゆいところに手が届くようになり、
現在のモダンなJavaScriptの開発環境などは特にたくさんのモジュールがまとまっているReactやVueライブラリを採用して開発をしようとする際にはCLIを利用してcreate-react-app
やvue create
などのコマンド一発で環境が作れるのはとてもありがたいです。
ところが実践的なカスタマイズが必要になった際にモジュールバンドラーやトランスパイラによって実際に内部でどういった環境が構築されているのかの全体像がわからないため、結果的にどこから手直しが必要になるかわからない状態になってしまいます。
近来のフロントエンドの難しいところはReact、Vueを動かすための登場人物が多くそれぞれの細かな役割を分解しておくだけでも全体像が格段と理解がしやすくなると思います。