JavaScript

イマドキのJavaScriptの書き方2018

PySpa統合思念体です。これからJavaScriptを覚えるなら、「この書き方はもう覚えなくていい」(よりよい代替がある)というものを集めてみました。

ES6以降の難しさは、旧来の書き方にプラスが増えただけではなく、大量の「旧来の書き方は間違いを誘発しやすいから非推奨」というものを作り出した点にあります。5年前、10年前の本やウェブがあまり役に立たちません。なお、書き方が複数あるものは、好き嫌いは当然あると思いますが、あえて過激に1つに絞っているところもあります。なお、これはこれから新規に学ぶ人が、過去のドキュメントやコードを見た時に古い情報を選別するためのまとめです。残念ながら、今時の書き方のみで構成された書籍などが存在しないからです。

たぶん明示的に書いていても読み飛ばす人はいると思いますが、すでに書いている人向けではありません。これから書くコードをこのスタイルにしていくのは別にいいと思いますが、既存のコードをすべて書き直せとか(後方互換性があるのでその必要性はありません)そういうものではありません。コードベースの安定が第一です。ちなみに、古い例と書いてある例の中に、僕が昨日まで書いていたコードもありますが、PySpaの総意で古い判定がされたものもあります。

TL;DR

もはや別言語です。

本エントリーの位置づけ

12/30に追加

  • 新しい機能の紹介というよりも、「もはやこのバッドノウハウはいらない」と、未来に引き継ぐべきではない切り捨てるテクニックの紹介です
  • 新しくこれからJavaScriptを学ぶ人が知るべき文法という体裁にしています(制御構文等、変わらない部分は紹介していません)。
  • TypeScriptも基本的にESと歩調を合わせて機能追加されているので、TypeScriptを使う人もターゲットです。

また、近年はJavaScriptの新機能はコミュニティからの提案で行われるプロセスになっています。stage1からstage4まで受け入れのレベルがあり、stage4は次期新バージョン(毎年6月に発行)に内定したことを意味します。本エントリーではstage3以下の機能については基本的に触れません。

互換性

12/30に追加

これらの書き方ですが、実際にどれだけのブラウザで対応しているのかが気になる方も多いようです。調べるには次のサイトが役立ちます。

軽く整理するとこのエントリーで紹介している文法は、次の環境では動作します。

  • IEを除く現行ブラウザ(スマホもデスクトップも)
  • Node.js 6.5以降

IE11や、未だに10%ぐらいシェアのあるAndroid 4.4以下は未サポートのものが多いです。あと、GoogleのJavaScriptを解釈するBotはChrome 41相当という説明が中の人がしていたりしますが、Chrome 41だと機能が制約されます(リンク先は41より後に追加されたJS機能一覧)。const/letはできるが、class構文はないということで、おそらくIE11相当の機能にとどめているのではないかと推測されます。そのため、インターネット上で公開する場合は次に紹介するBabelを使うのが原則として必要でしょう。

環境構築編

Babel

使わないとかTypeScriptとかも選択肢は一応ありますが、たとえpercelを使っていても避けられないのがBabelです。

Babelは昔は単独のツールだったものが、プラグインで拡張できるようになり、preset-es2015などのいくつかの設定を統括するプラグインが登場し、最終的にpreset-envというものに集約されました。なので、preset-es2015など、別の書き方をしている本や資料は古いです。

古い.babelrc
{
  "presets": ["es2015"]
}

とりあえず最新の文法を解釈するようにするには、次のような設定ファイルを書きます。

今時の.babelrc
{
  "presets": ["@babel/preset-env"]
}

ブラウザのバージョンなど、「どの環境で動かしたいか?」という条件で出力フォーマットを設定できます。

今時の.babelrc
{
  "presets": [
    ["@babel/preset-env", {
      "targets": {
        "node": "6.10"
      }
    }]
  ]
}

当然、"ie": "11"という指定もできるので、指定された実行環境がES6ネイティブ対応じゃなくても、これから紹介するようなことはだいたいできるでしょう。

もちろん、JSそのものをいじりたい(新しい文法を考案して提案するために実験したい)という人はこの限りではありません(が初心者向けユースケースではないので省略)。

変数・リテラル

constをまず使う

昔は変数宣言で使えるのはvarのみでした。

古い書き方
var name = "@wozozo";

今後、まっさきに使うべきはconstです。varは全部とりあえずconstに置き換えて、上書きがどうしても必要なところだけletにします。

何はともあれconst
const name = "@wozozo";

関数型言語と同じで、変わる必要がないものは「もう変わらない」と宣言することで、脳内でコードをエミュレーションするときのメモリ消費を下げることができます。

変数を変更する場合はletを使います。

変更がある変数はlet
// 変更前
let name;
if (mode === "slack") {
    name = "youichi";
} else if (mode === "twitter") {
    name = "@wozozo";
}

なお、C++のconstを知っている人からすると違和感があるかもしれませんが、constで宣言した変数に格納された配列は、代入し直すのはできませんが、配列に要素を追加することはできます。オブジェクトの属性変更もできます。そのため、使える場所はかなり広いです。

なお、varも、グローバルスコープに変数を置く場合にはまだ使えますが、後述するように本当の意味でのグローバルスコープを扱う機会は減っているので基本的に「使わない」で良いでしょう。

変数のスコープ

以前は{}はブロックにはなりましたが、変数の範囲とはリンクしていませんでした。

古いコード
for (var i = 0; i < 10; i++) {
    // do something
}
console.log(i); // -> 10

letconstはこのブロックの影響を受けます。

新しいコード
for (let i = 0; i < 10; i++) {
    // do something
}
console.log(i); // ReferenceError: i is not defined

ifとかforとか制御構文を使わずに、おもむろに{}のブロックを書いてスコープを制限することもできます。スコープが狭くなると、変数の影響範囲が小さくなるのでコードの理解がしやすくなります。若者であれば記憶力は強いので良いですが、歳をとるとだんだん弱ってくるのです。また、若くても二日酔いの時もあるでしょうし、風邪ひいたり疲れている時もあると思うので、頑張らないで理解できるように常にしておくのは意味があります。

文字列の結合

従来は他の言語でいうprintf系のようなものがなく、文字列を+で結合したり、配列に入れて.join()で結合したりしましたが、これは古いコードです。

古いコード
console.log("[Debug]:" + variable);

いまどきは文字列テンプレートリテラルというのがありますので、これを使います。printfのような数値の変換などのフォーマットはなく、あくまでも文字列結合をスマートにやるためのものです。もちろん、数が決まらない配列などは従来どおり.join()を使います。

新しいコード
console.log(`[Debug]: ${variable}`);

オブジェクトのコピー

Reduxでimmutable.jsを使わない、という状況ではよくオブジェクトのコピーが発生します。

古いコード
var destObj = {};
for (var key in srcObj) {
  if (srcObj.hasOwnProperty(k)) {
    destObj[k] = srcObj[k];
  }
}

今時はObject.assign()というクラスメソッドを使います。

新しいコード
const destObj = {};
Object.assign(destObj, srcObj);

ECMAScript 2018では、オブジェクトのスプレッド演算子サポートが公式で入りました。さらに2018年6月からは次のようになります。このピリオド(スプレッド演算子)3つは後の配列や引数のところでも出てきます。また、このスプレッド演算子は最後に説明する分割代入の「残りの要素」を扱う時にも使います。

新しいコード
const destObj = {...srcObj};

クラス宣言

昔は関数とprototypeという属性をいじくり回してクラスを表現していました。正確には処理系的にはクラスではないのですが、コードのユーザー視点では他の言語のクラスと同等なのでクラスとしてしまいます。

古いクラスの表現
// 関数だけどコンストラクタ
function SmallAnimal() {
    this.animaltype = "ポメラニアン";
}

// こうやって継承
SmallAnimal.prototype = new Parent();

// こうやってメソッド
SmallAnimal.prototype.say = function() {
    console.log(this.animalType + "だけどMSの中に永らく居たBOM信者の全身の毛をむしりたい");
};

var smallAnimal = new SmallAnimal();
smallAnimal.say();
// ポメラニアンだけどMSの中に永らく居たBOM信者の全身の毛をむしりたい

なお、この仕組みをラップした自前のextends関数みたいなのを使ってクラスっぽいことを表現しようという、なんとかThe Good Partsに影響された一派も一時期いましたが、百害あって一利なしです。

今時の書き方は次のようなclassを使った書き方です。「これはシンタックスシュガーで云々」みたいに言ってくるオッサンの顔面にはパンチをしてもいいです。

新しいクラスの表現
class SmallAnimal extends Parent {
    constructor() {
        this.animaltype = "ポメラニアン";
    }

    say() {
        console.log(`${this.animalType}だけどMSの中に永らく居たBOM信者の全身の毛をむしりたい`);
    }
}

関数宣言

アロー関数のみにしていく

functionキーワードはもう捨てましょう!functionキーワードのthisの取り扱いはトラブルの元です。もう存在しなかったものとして歴史の闇に葬ります。次の書き方は古いfunctionキーワードを使っています。こういう説明を見かけたらゴミ箱にダンクシュートします。

古いの関数定義
function name(引数) {
    本体
}

今時はアロー関数を使って書いていきます。特に、今時の書き方は、JavaScriptでよく扱う、無名関数との相性が非常に高くなっています。

今時の関数定義
const name = (引数) => {
    本体
};

状況によってはカッコやreturnなどが省略できたりしますが、そこは割愛します。

即時実行関数はもう使わない

関数を作ってその場で実行することで、スコープ外に変数などが見えないようにする、というテクニックがかつてありました。即時実行関数と呼びます。今時であれば、WebPackなりBrowserifyなりRollupなりPercelなりでファイルを結合するでしょうし、昔のように1ファイルでライブラリを配ってそれを<script>タグで読み込む、というのは減ってきていると思います。

そのため、こういう書き方自体も減ってきています(もちろん、なくなってはいません)。ただ、タグ一つで読み込めるのは利用者にとっては便利なので、超絶良いフレームワーク的なライブラリができて公開したい、という気持ちが出てきてからで遅くはありません。今は無視しましょう。

古いテクニックである即時実行関数
var lib = (function() {
  var libBody = {};

  var localVariable;

  libBody.method = function() {
      console.log(localVariable);
  }
  return libBody;
})();

function(){}をかっこでくくって、その末尾に関数呼び出しのための()がさらに付いている感じです。これで、エクスポートしたい特定の変数だけをreturnで返して公開をしていました。

今時はES6スタイルであればexport { name1, name2, …, nameN };といった書き方が使えます。Browserify/Node.jsでもmodule.exports = { name1: name1, name2: name2... }となります。公開しているもの以外は非公開です。なので、堂々とグローバル空間に置いても問題ありません。

非同期処理

JavaScriptで中級以降になってくると避けられないのが非同期処理です。以前はコールバック地獄と揶揄されるような感じでした。(なお、エラー処理時にかっこなしのif文を一行で書く、というスタイルは好き嫌いの別れるところだと思うので、ここではあえて触れません)。

コールバック地獄
func1(引数, function(err, value) {
  if (err) return err;
  func2(引数, function(err, value) {
    if (err) return err;
    func3(引数, function(err, value) {
      if (err) return err;
      func4(引数, function(err, value) {
        if (err) return err;
        func5(引数, function(err, value) {
          // 最後に実行されるコードブロック
        });
      });
    });
  });
});

その後、非同期処理の待ちはPromiseを使うようになりました。これで、ネストはだいぶ浅くなるので、書きやすくなりました。

ちょっと今時の書き方
const getData = (url) => {
    fetch(url).then(resp => {
        return resp.json();
    }).then(json => {
        console.log(json);
    });
}; 

ただ、new Promise(...)と、Promiseの前にnewを付ける書き方も過去のものです。Promiseasync関数がreturnとともに作るものです。

今時はasync/awaitキーワードをアロー関数の前に付与します。functionの前にも付けられますが、functionはもはやNGワードなので忘れてください。

asyncとつくと、その関数がPromiseというクラスのオブジェクトを返すようになります。Promiseはその名の通り「重たい仕事が終わったら、あとで呼びに来るからね」という約束です。awaitは、その約束が守られるのを粛々と待ちます。

今時の非同期処理
// 非同期処理をawaitで待つ
const fetchData = async (url) => {
    const resp = await fetch(url);
    const json = await resp.json();
    console.log(json);
};

Promiseを返したメソッドでは、Promisethen()メソッドを呼ぶことで、約束を待ちましたが、可能な限りawaitを使って、then()も滅ぼしましょう。

Promise.all()などのところでPromiseは使うので、この名前を禁止することはありません。

apply()

昔は、関数に引数セットを配列で引き渡したいときはapply()というメソッドを使っていました。

古い書き方
function f(a, b, c) {
    console.log(a, b, c);
}

// a=1, b=2, c=3として実行される
f.apply(null, [1, 2, 3]);

この関数の中のthisを最初の引数で指定したりとかありましたが、関数宣言をすべてアロー関数にするのであれば、もうそういうのも過去の話です。配列展開の文法のスプレッド演算子...を使うと同じようなことができます。

新しい書き方
const f = (a, b, c) => {
    console.log(a, b, c);
};

f(...[1, 2, 3]);

なお、apply()がよく出てきていた文脈としては、関数を可変長配列にしたい、というものでした。関数の中ではargumentsという名前のオブジェクトが関数の引数を持っているのですが、これが配列のようで配列でない、ちょっと配列っぽいオブジェクトです。ちょっと使いにくいので、一旦本物の配列にする時にapply()を使ったハックがよく利用されていました。何が起きているかは理解する必要はありません。

可変長配列の古いコード
function f() {
    var list = Array.prototype.slice.call(arguments);
    console.log(list);
}
f(1, 2, 3, 4, 5, 6);
// [1, 2, 3, 4, 5, 6];

これもスプレッド演算子を使うことでわかりやすくなります。もともとの方法は関数宣言だけを見ても実際の引数の数がわかりにくいという問題がありましたが、こちらの方がわかりやすいでしょう。

可変長配列の新しいコード
const f = (a, b, ...c) => {
    console.log(a, b, c);
};
f(1, 2, 3, 4, 5, 6);
// 1, 2, [3, 4, 5, 6];

デフォルト引数

1/20追記

JavaScriptは同じ動的言語のPythonとかよりもはるかにゆるく、宣言された引数を付けずに呼び出すこともでき、その場合には変数にundefinedが設定されました。そのため、undefinedの場合は省略されたとみなして、値を設定したりしていました。

デフォルト引数の古いコード
function f(a, b, c) {
    if (c === undefined) {
        c = "default value";
    }
}

やっかいなのは、コールバック関数が末尾にあって、途中の値を省略可能にするときです。JavaScriptでは最後の引数がコールバック、というのはだいたい統一された設計指針として広まりましたが(超古代のsetTimeoutとか例外はいる)、そのためにこういう引数処理が必要になったりました。

古くてやっかいな、コールバック関数の扱い
function f(a, b, cb) {
    if (typeof b === "function") {
        cb = b;
        b = undefined;
    }
    :
}

どの引数が省略可能で、省略したら引数を代入しなおしたり・・・とか面倒ですし、同じ型の引数があったら判別できなかったりとか・・・

今時は、他の言語と同じように関数宣言のところに書くことができ、複雑なロジックを手で実装する必要はなくなりました。楽ですね。あと、コールバックですが、すでに説明したとおりにPromiseを返す手法が一般的になったので、「末尾は関数だけど途中が省略」というケースがなくなりました。めでたいですね。

新しいデフォルト引数
const f = (name="小動物", favorite="ストロングゼロ")

配列やオブジェクトは、分割代入する機能が増えましたが、それと組み合わせると、オブジェクトで柔軟なパラメータを受け取れるがデフォルト値も設定される、みたいなこともできます。以前は、オプショナルな引数はoptsという名前のオブジェクトを渡すこともよくありましたよね?今時であれば、完全省略時でもデフォルト値が設定されるし、部分的に設定も可能みたいな引数は次のように書けます。オブジェクトじゃなくて、配列にすることもできます。

分割代入を使って配列やオブジェクトを変数に展開&デフォルト値も設定
const f = ({name="小動物", drink="ストロングゼロ"}={}) => {
    :
}

thisを操作するコードは書かない

12/30追記

以前は、prototypeを操作しないで済むように、自前で継承の関数を作ったり、いろいろなメタプログラミングが行われてきました。また、同一の関数実装を流用したり、というところとかで、thisを意識したコーディングがよく行われていました。むしろ、thisを実行時に差し替えることで柔軟性を獲得してきたのがES3〜5ぐらいまでに行われてきたことです。むしろ、これらのthisの違いを知り、使いこなせるのがJavaScript上級者の第一歩ぐらいでした。

古いthisを操作するコード
// apply()の第一引数でthisを外部から指定して実行
func.apply(newThis, [1, 2, 3]);

// call()の第一引数でthisを外部から指定して実行
func.call(newThis, 1, 2, 3);

// bind()でthisを固定
func.bind(newThis);

// オブジェクトに属する関数オブジェクトは、オブジェクトがthisに代入されて実行される
var obj = {
    method: function() {
    }
};

// 何も束縛されていないとglobal名前空間(ブラウザならwindowと同じ)を表す。
// グローバル名前空間に変数追加(汚染ともいう)ができてしまう。
function global() {
    this.bucho = "show";
}

とくに一番最後のものがやっかいで、イベントハンドラをオブジェクト内部で定義したときに、そのオブジェクトを参照する方法がなくなるため、次のようなコードが世界で何億回も書かれました。きっと誇張じゃないでしょう。

今後必要なくなるイディオム
var self=this;

コメントにもあるように、jQueryにもその名残があります。jQueryのthisは、選択されているカレントノードを表します。

jQueryはthisを使って、選択されている要素にアクセスする
$('.death-march').each(function () {
  $(this).text("@moriyoshi参上");
});

もちろん、使っているフレームワークが特定の流儀を期待しているなら、それをしない方があとあと問題が起きるので、そこは仕方がないところはあります。

このようなthisの操作は今後は不要です。アロー演算子を使えば、オブジェクトの中だろうがつねに、オブジェクトのインスタンスを表すようになります。var self=thisも不要です。

なお、オブジェクトにメソッドを追加するには次の構文が使えます。functionキーワードを使って書くのと実体としては大差がありませんが、書かなくてもいい、というのは楽です。クラスもオブジェクトも常にthisがインスタンスになれば、コードを読む時の「このthis」どこから来たんだろうか?というのに頭を悩ませる必要はありません。

ES6のオブジェクトのメソッドは省略記法がある
const obj = {
    method() {
        console.log(this);
    }
}

配列、辞書

ES6では、単なる配列以外にも、Map/Setなどが増えました。これらは子供のデータをフラットにたくさん入れられるデータ構造で、ループの中で一個ずつ子供を取得する(イテレーション)できるので、iterableと呼ばれます。そのため、配列固有の操作じゃなくて、iterable共通の操作にしていくことが、2018年のESの書き方になります。

ループはfor ... ofを使う

次のコードは古の時代からのコードです。

古いループ1
var iterable = [10, 20, 30];

for (var i = 0; i < iterable.length; i++) {
  var value = iterable[i];
  console.log(value);
}

次のコードは比較的新しいのですが今となってはより新しいコードもあります。一応、現在のiterable(Array, Set, Map)のすべてで使えます。ただパフォーマンス上は遅いとされています(関数呼び出しが挟まるので)。

古いループ2
var iterable = [10, 20, 30];

iterable.forEach(value => {
  console.log(value);
});

イテレータプロトコルという言語組み込みがこれです。今後も新しいiterableが出たとしてもずっと使い続けられます。ループを回して、要素をひとつずつ取り出す・・・というコードはfor ... ofを使います。MDNのサンプルをちょっと改変。

新しいループ
const iterable = [10, 20, 30];

for (const value of iterable) {
  console.log(value);
}

こちらは関数呼び出しを伴わないフラットなコードなので、async/awaitとも一緒に使えます。配列の要素を引数にして、1つずつasyncしたい場合などです。

asyncと新しいループ
const iterable = [10, 20, 30];

for (let value of iterable) {
  await doSomething(value);
}

辞書・ハッシュ用途はオブジェクトではなくてMapを使う

古のコードはオブジェクトを、他言語の辞書やハッシュのようにつかっていました。

古いコード
var map = {
  "五反田": "約束の地",
  "戸越銀座": "TGSGNZ"
};

for (var key in map) {
    if (map.hasOwnProperty(key)) {
        console.log(key + " : " + map[key]);
    }
}

今時はMapを使います。他の言語のようにリテラルで簡単に初期化できないのはあれですが、最初の部分だけですので我慢してください。

新しいコード
const map = new Map([
  ["五反田", "約束の地"],
  ["戸越銀座", "TGSGNZ"]
]);

for (const [key, value] of map) {
    console.log(`${key} : ${value}`);
}

keyだけでループしたい場合(以前同様)はfor (const key of map.keys()), valueだけでループしたい場合はfor (const value of map.values())が使えます。

keys()メソッド、values()メソッドも、配列の実体を作っているわけではなくて、イテレータという小さいオブジェクトだけを返すので、要素数がどんなに大きくなろうとも、動作も軽いはずです。

分割代入(Destructuring Assignment)

1/20追記

オブジェクトや配列の中身を展開する方法としては、以前は一つずつ変数に代入するとかしていましたし、そもそもそういうことをしないでオブジェクトのまま扱うということをしていました。

一個ずつ取り出す。あるいは取り出さないで利用する古い記法
var thinking = {
    name: "小動物",
    mind: "Python3と寝たい",
    reason: "`raise e from cause` べんりですよ"
};
console.log(thinking.name + "だけど" + thinking.reason + " " + thinking.mind + "の理由の一つです");

var name = thinking.name;
var mind = thinking.mind;
var reason = thinking.reason;
console.log(name + "だけど" + reason + " " + mind + "理由の一つです");

分割代入を使えば、オブジェクトや配列を一気に複数の変数に展開できます。オブジェクトの場合は変数名とキー名で引き当てられます。配列は順番ですね。対応する要素が存在しなかったときのデフォルト値も自由に設定できます。

分割代入するし、デフォルト値も一緒に設定できる
const thinking = {
    name: "小動物",
    mind: "Python3と寝たい",
    reason: "`raise e from cause` べんりですよ"
};

const {name="約束の地の住人", mind, reason} = thinking;
console.log(`${name}だけど${reason} ${mind}理由の一つです`);

記法としては関数の引数のデフォルト値と同じです。

import文なり、requireなりも、一度の呼び出しで、一つしか値が帰ってきません。複数の値を取りたいときは、1つのまとめたオブジェクトをもらってから、属性アクセスしたり、何度も呼び出しをしていました。

分割代入を使わない古い記法
var path = require("path");
var readFileSync = require("fs").readFileSync;
var writeFileSync = require("fs").writeFileSync;

この記法を使えば、中間の変数を省略して、欲しいものだけを得ることもできるようになります。async/await(というか、その裏のPromise)は一つしか値を渡せませんので、この記法を使うと、複数の値を気軽に返せるようになります。

分割代入を使って一発でほしいものだけ取得
const { join } = require("path");
import { readFileSync, writeFileSync } from "fs";

5/23追記

分割代入の左辺にスプレッド演算子をおくことで、「残りの要素」を扱うこともできます。オブジェクトのスプレッド演算子はECMAScript 2018で公式の仕様に仲間入りしました。

旧来は、配列のsliceを使う方法ぐらいでしょう。オブジェクトの場合はおそらくシンプルな方法はなくて、オブジェクトをまるごとコピーしてから不要なものを削除、あるいはすべてのキーに対してループで回して必要な要素だけを新しいオブジェクトに割り当てる、といった操作が必要だったでしょう。

旧来の手法
// 配列
var rest = array.slice(2);

スプレッド演算子を使えば、よりわかりやすいく記述できます。

スプレッド演算子を利用した記法
// 配列
const [ a, b, ...rest ] = array;
// オブジェクト
const { a, b, ...rest } = obj;