中世のJavaScriptしか知らないマンが衝撃を受けたモダンな便利機能たち
こんにちは。とんと申します。
本稿では近代的なJavaScriptを全く知らなかった私が、ここ半年間のフロントエンド開発で体験したカルチャーショックをご紹介しようと思います。
知っている方からすれば「当たり前だろ!」と突っ込まざるを得ない内容でしょうが、何分弱輩者ゆえご容赦ください。
私と同じように近代JS全然知らないマンの方は一緒に衝撃を分かち合いましょう。
その1 constとlet
中世から現代にやってきた私がまず驚いたのがこれでした。
こ、コンスト・・・レット・・・? varは?varはどこにいったんだ!?
中世JSでは鉄板だったvarによる変数宣言も、モダンJSではどこ吹く風。
現場のソースコードは全てconstとletに置き換えられました。
// 再代入
const hoge = "ほげ";
hoge = "ほげほげ"; // 実行時にエラーが出る Uncaught TypeError: Assignment to constant variable.
// 再宣言
const hoge = "ほげ";
const hoge = "ほげほげ"; // 実行時にエラーが出る Uncaught SyntaxError: Identifier 'hoge' has already been declared
constは一度宣言したら再代入、再宣言ができない、いわゆる定数です。
const hogeObj = {hoge: "ほげ"};
hogeObj.hoge = "ほげほげ";
const hogeArr = ["ほげ"];
hogeArr[0] = "ほげほげ";
ただし、オブジェクトのプロパティや配列の中身は書き換えることができます。
let hoge = "ほげ";
hoge = "ほげほげ"; // OK
let hoge = "ほげ";
let hoge = "ほげほげ"; //NG
letは再代入だけが可能な宣言方法です。
ちょっとまてーい!constはまぁ価値がわかったけど、letはvarと一緒じゃないかって?
そう思っていた時期が私にもありました。。。(え?私だけ?)
実はvarは再代入可能なうえに再宣言もできちゃうかなり危ない奴だったのです。
var hoge = "ほげ";
var hoge = "ほげほげ";
これが通ってしまうので、もしhogeがグローバルに宣言されていたら・・・恐ろしいですね。
constとletを知るまでvarが再宣言できることを知らなかった自分に反省です。
他にもvarとconst、letにはスコープの違いがあるのですが、非常に長くなりそうなので割愛します。
詳しくは偉大なるqiita記事をご覧ください。
https://qiita.com/masarufuruya/items/096e51c3e4c36c86ae27
その2 アロー関数
モダンJSでは、functionを書かなくていいんです!
// 古JS
function plus(a, b) { return a + b; }
// モダンJS
const plus = (a, b) => a + b;
カッコイイ・・・。
定番のsetTimeoutおじいちゃんもアローで書くと一気に若返ります。
setTimeout(() => console.log("hoge"), 3000);
そんなイケイケアロー関数の恩恵を受けまくるのが、次に紹介するmapやfilterです。
その3 ま、mapとかfilterがある!
ScalaやJavaでお馴染みの関数型メソッドがJavaScriptにも実装されていました。
しかもScalaチックというかScalaそのもので、Javaのようにfilterした後にCollectors.toList()しなきゃいけないなどのイケて無さがありません。
[1, 2, 3, 4, 5].map(r => r + 1); // [2, 3, 4, 5, 6]
[1, 2, 3, 4, 5].filter(r => r > 3); // [4, 5]
["", undefined, null, 0, false, "hoge"].filter(r => r); // ["hoge"]
特に、filter君は気が利く子で、undefinedや空文字、その他もろもろを自動で省いてくれます。
気が利きすぎていて知らないと不具合を呼びそうな仕様ですが、知っていれば超絶便利ですね。
[1, 2, 3, 4].forEach((r, i) => console.log("index=" + i + " value:" + r));
// index=0 value:1 index=1 value:2 index=3 value:4 index=4 value:5
[1, 2, 3, 4].reduce((acc, v) => acc + v); // 10
[[1,2], [3,4], [5, [6,7]]].flatMap(r => r.flat());
// [1, 2, 3, 4, 5, 6, 7]
他にもforEachやreduce、flatMapなども用意されており、古のJavaScriptとはもはや別言語感があります。
const v = [1, 2, 3].find(r => r > 1); // 2
中でもお気に入りなのがfind君です。
配列から最初に見つかった要素を返してくれるイカしたやつです。彼とforEach、map、filterの四天王は頻繁に使います。
さらに、find君は後述する「Optional Chaining」と組み合わせることで無限のパワーを得ることができます。
その4 テンプレートリテラル
地味に嬉しい機能です。
バックティック文字(`)の中に${}
を書くことで変数を展開できるようになっていました。
昔ながらの+で繋げる方法よりも可読性が高くなります。
const name = "とん";
const favorite = "ラーメン";
const company = "株式会社ルトラ";
// 定番の+連結
const text1 = "私の名前は「" + name + "」。好きなものは" + favorite + "で、" + company + "という会社に勤めているよ!";
// テンプレート構文
const text2 = `私の名前は「${name}」。好きなものは${favorite}で、${company}という会社に勤めているよ!`;
const text3 = `私の名前は「${name}」。
好きなものは${favorite}で、
${company}という会社に勤めているよ!
`;
// 私の名前は「とん」。
// 好きなものはラーメンで、
// 株式会社ルトラという会社に勤めているよ!
しかも改行をそのまま出力してくれるので文章にはもってこいです。
その5 ま、MapとSetがある!
Javaでは定番のMapとSetがJavaScriptにも実装されていました。
const map = new Map();
map.set(1, "hoge");
const set = new Set();
set.add(1);
const map = new Map();
map.set(1, "hoge");
map.forEach((v, k) => console.log(`key:${k} value:${v}`));
// key:1 value:hoge
どちらもforEachで回せます。
ちなみに、Mapの場合はキーバリューを引数に記述するのですが、並びがバリューキーという順なのが初見殺しでした。
const arr = [1, 1, 2, 3, 4];
const set = new Set(arr);
if (arr.length !== set.size) alert("重複してる値があるよ!!!");
Setは重複チェックのときに重宝します。for文で書くと面倒くさい処理がスマートに記述できます。
すげー。
その6 every some includes
真偽値チェック三種の神器。
これも非常に便利です。
[1, 2, 3, 4].every(r => r < 5); // true
everyはその名の通りそれぞれが式を満たせばtrueを返してくれます。
[1, 2, 3, 4].some(r => r === 2); // true
[1, 2, 3, 4].includes(2); // true
someはどれか一つでも式を満たしていれば、includesは一つでも値が含まれていればtrueを返してくれます。
サンプルのように引数に関数を渡せるかどうかがsomeとincludesの違いです。
その7 分割代入
const [a, b] = sample();
初めて見たときはかなり混乱しました。
配列のように見えるけどそうじゃないし、なんじゃこりゃと。
デバッグしてみた結果、配列の1番目までの要素を自動で代入してくれていたことがわかりました。
以下のサンプルをご覧ください。
const sample = () => [() => console.log("a!"), () => console.log("b!")];
const [a, b] = sample();
a(); // a!が出力される
const [a] とすれば、0番目の要素しか入りません。だいぶ魔術的ですね。
ちなみに、オブジェクトでも同じことができます。
const {hoge, fuga} = {hoge: "a", fuga: "b"};
console.log(hoge); // a
console.log(fuga); // b
const {a, fuga} = {hoge: "a", fuga: "b"};
console.log(a); // undefined;
console.log(fuga); // b
ただ、オブジェクトの場合は代入元のプロパティ名と代入先の変数名が一致していないといけないので注意が必要です。
巨大なオブジェクトの中から特定のプロパティだけ欲しいんだよね~なんてときに活躍しますね。
その8 importとexport
これも初めて見たときは混乱しました。
いつの間にかJavaScriptはクラスや関数をimportできるようになっていました。
export const hoge = () => alert("hoge");
export const fuga = () => alert("fuga");
export default class Hoge { constructor(message) { this.message = message; } hello() { console.log(this.message); } }
(※シンタックスハイライトを使うとなぜか画面表示がおかしくなってしまうため直書きしています)
exportを付けているものがimportできるようになります。
さらにdefaultをつけているものは、import時に{
(波括弧)が要りません。
import {hoge, fuga} from "./test.js";
import Hoge from "./test.js";
ReactやVueなどでは必ず使います。
importとexportさんはJS固有の機能なのね!と覚えておけば、私のように混乱せずに済むと思います。。。
その9 スプレッド構文
配列やオブジェクトに...
という魔術的なシンタックスを付けることで色々できるようになっていました。
ここでは現場でよく使う二パターンのみご紹介します。
・一括引数渡し
const tooManyArgments = (a, b, c, d, e, f, g, h) => {
return console.log(a + b + c + d + e + f + g + h);
};
const argments = () => [2, 3, 4, 5, 6];
tooManyArgments(1, ...argments(), 7, 8); // 36
こんな感じでまとまった値を一気に渡すことができます。
引数の組み合わせごとに関数を作ったりすると保守性がUPしますね。
・ワンライナーでプロパティ書き換え
{...オブジェクト, プロパティ名: 値, プロパティ名: 値, プロパティ名: 値}
と書けば、書き換えたプロパティを持った新しいオブジェクトを生成してくれます。
const sample = (obj) => Object.values(obj).forEach(v => console.log(v));
const obj = {hoge: "hoge", fuga: "fuga", piyo: "piyo"};
sample({...obj, fuga: "ふが", piyo: "PIYO"}); // hoge ふが PIYO
このように、ワンライナーで書けるため様々な場面で重宝します。
const fuga = "ふが";
const piyo = "PIYO";
sample({...obj, fuga, piyo});
ちなみに、セットしたい値が変数に入っている場合は、その変数名とプロパティ名を一致させてあげれば分割代入によりプロパティ名の記述を省略できます。
Reactで大量のpropsを親から子に渡すときなどに大活躍します。
その10 Promise
みんなが知ってる消費者金融!ではありませんでした。
Promiseさんは非同期処理の終了時に、設定した後始末の実行を約束してくれる健気な子です。
let isSuccess = true;
new Promise((resolve, reject) => setTimeout(() => isSuccess ? resolve("success") : reject("fail"), 2000))
.then(message => console.log(message))
.catch(message => console.log(message));
// isSuccessがtrueならsuccess falseならfail
注意点としては、resolve()かreject()がコールされないと、Promiseさんは約束を果たすべき時がきたのかどうか判断できないため、後始末を実行してくれません。
new Promise((resolve, reject) => { throw new Error("fail"); })
.then(message => console.log(message))
.catch(e => console.log(e.message)); // fail
ただし、非同期処理中にエラーが発生した場合は、自動でreject()してくれます。気が利きますね~。
const taskA = new Promise(resolve => setTimeout(resolve, 3000, "Task A"));
const taskB = new Promise(resolve => setTimeout(reject, 2000, "Task B"));
Promise.all([taskA, taskB]).then(message => console.log(message)); // (2) ["Task A", "Task B"]
さらにPromiseさんは「全部の非同期処理が終わるまで待って、終わってから後始末始めてくんね?」というわがままな要望にも難なく答えてくれます。
const p1 = new Promise(resolve => setTimeout(resolve, 3000, "hoge"));
const p2 = new Promise(resolve => setTimeout(resolve, 2000, "fuga"));
const p3 = new Promise(resolve => setTimeout(resolve, 4000, "piyo"));
Promise.race([p1, p2, p3]).then(message => console.log(message)); // fuga
他にも、「今からこいつら競争させるから、ゴールした奴がいたら後始末よろしくね。え?まだ走ってる奴?そんなもん消しちまえ!」という残酷な命令にも従ってくれます。
どんなときでも、Promiseさんが約束を違えることはないのです。
彼が登場したおかげで、混沌に満ちたJavaScript非同期処理界に一つの秩序ができたそうです。
Promiseさん尊い…。
その11 await async
尊いPromiseさんをよりリスペクテッドなものにするawait asyncなる記法ができて、より短い記述で非同期処理が書けるようになりました。
const p = async () => "hoge";
p()
.then(message => console.log(message))
.catch(e => console.log(e.message)); // hoge
まず、関数の先頭にasyncという接頭辞をつけます。こうすると関数がPromiseを返すようになるので、前述したthenやcatchが使えるようになります。
const error = async () => { throw new Error("error!!"); };
error()
.then(message => console.log(message))
.catch(e => console.log(e.message)); // error!!
なお、resolve()、reject()の明示的なコールは必要ありません。
何か値が返ればresolve()、エラーが発生すればreject()するようになっています。
const p1 = () => new Promise(resolve => setTimeout(resolve, 3000, "hoge"));
const p2 = () => new Promise(resolve => setTimeout(resolve, 2000, "fuga"));
const test = async () => {
const p1Result = await p1();
const p2Result = await p2();
// 5秒後にhoge fugaと出力
console.log(p1Result);
console.log(p2Result);
};
そして、Promiseを返す関数は、asyncをつけた関数内であればawaitする(処理が終わるまで待つ)ことができます。
このawaitさんが使えるおかげで可読性がより向上します。
例として以下の関数が返す値の足し算をやってみます。
const p1 = () => new Promise(resolve => setTimeout(resolve, 4000, 1));
const p2 = () => new Promise(resolve => setTimeout(resolve, 2000, 2));
const p3 = () => new Promise(resolve => setTimeout(resolve, 3000, 3));
・Promise#then
let result = 0;
p1()
.then(p1Result => {
result += p1Result;
return p2();
})
.then(p2Result => {
result += p2Result;
return p3();
})
.then(p3Result => {
result += p3Result;
console.log(result); // 6
});
・Promise#all
Promise.all([p1(), p2(), p3()]).then(result => console.log(result.reduce((acc, r) => acc + r)));
・await async
const result = async () => console.log(await p1() + await p2() + await p3());
result(); // 6
await asyncが何をやっているか一番わかりやすいですね。
現場で非同期を使うようなレアなタスクを依頼されたときに、さらっとawait asyncを使えたらカッコイイですね。
その12 Optional Chaining
最近公開された激アツ機能です。
?.
というシンタックスを使うことで、nullableなプロパティに対してnull参照を気にせずアクセスできます。
const x = hoge?.fuga?.piyo;
値があればそれを返し、無ければその時点でundefinedを返します。
const hoge = {fuga: undefined};
const x = hoge?.fuga ?? "piyo"; // fugaがundefinedなので代わりにpiyoが入る
さらに、??
(null合体演算子)を使うとundefinedの代わりのデフォルト値を返すことができます。
昔ながらの||
で評価する方法との違いは、||
は左辺が0や空文字でもfalse判定となりますが、??
はnullかundefinedでしかfalseになりません。
・普通に書いた場合
const x = hoge != null && hoge.fuga[0] != null ? hoge.fuga[0].piyo : "";
・Optional Chainingで書いた場合
const x = hoge?.fuga[0]?.piyo ?? "";
・前述したfind関数との合体技
const x = employeeList.find(r => r.id === id)?.name?.firstName ?? "";
めちゃくちゃ便利じゃないですか!?
この機能のおかげで、いちいちnullチェックをせずにプロパティのチェーンができるようになり、コードを書くのが非常に楽になりました。
以上が中世JSしか知らないマンだった私が、モダンJSの経験を通して受けた衝撃の数々です。
私と同じようにまだ知らなかった方、是非試してみてください。
最後になりましたが一点ご注意を・・・。
ご紹介した機能の大半はIE11で動きません。
世界のインターネッツを支えてくれたInternet Explorerさん、本当にありがとう。
100トン
0.1t肥えという日本人上位1%の能力保持者にして孤高のラーメン好き。彼が愛でる250ccバイクはその重さゆえに原チャリにすら負けるという。自慢はここ2年間、電車で一度も座ったことがないこと。(ベルトが苦しいので)