はじめに

みなさんこんにちは。カルテットコミュニケーションズでフロントエンドの開発を担当している松岡です。

みなさんは、自分の開発環境のブラウザでは正常に動作しているのにシステムのユーザーさんから「ここのボタンを押しても無反応なんですけど」といったような問い合わせを受けた事はないでしょうか。ユーザーさんの利用ブラウザを聞き出して動作を再現してみると、開発者ツールにJavaScriptのエラーが!「あちゃー、ここの部分は○○オブジェクトを使ったけど、このブラウザでは使えなかったのか!!」なんて経験、私は何回かあります。

「なんで特定のブラウザで動作しないの?」という問題には意外と大きな背景があります。あれこれ調べるのが面倒だから「○○オブジェクトはこのブラウザでは使えない」みたいな断片的な知識を頼りにコーディングしちゃってませんか?

今回はこの意外と大きい背景の部分と、コーディングの時に意識するべきポイントについてまとめたものを記事として紹介させていただきます。

背景を理解しよう

PHPなどの言語ではバージョンごとにバイナリが配布され、そのバージョンに応じたシンタックスや関数を利用する事ができます。一方JavaScirptは全く違う仕組みで、Chrome/Firefox/Node.jsなどそれぞれの実行環境ごとにJavaScriptのソースコードを解釈し実行するためのエンジンを持っています。

ECMAScript

実行環境(エンジン)ごとにJavaScriptの挙動がバラバラだとしたら世界中の開発者が困ってしまいますよね。そんな事がないようにEcma Internationalという団体がJavaScriptの仕様を定め、実行環境はそれに則る形で追加の機能を実装しています。(仕様の解釈の違いによる挙動のばらつきが存在するのも事実ですが)Ecma Internationalが定めたJavaScriptの仕様の事を ECMAScript と呼びます。

  • この記事では、ブラウザやNode.jsおよびそれぞれのエンジンの事を総称して 実行環境 と記載しています
  • また実行環境において、仕様に定められた要件を満たしその機能を搭載する事を 実装 と記載しています

ECMA-262

もう少し掘り下げると、Ecma Internationalとは情報通信システムの分野における国際的な標準化団体です。この団体が定める ECMA-262 という規格番号のものがECMAScriptです。

ECMAScriptは毎年改訂版をリリースするルールがあり、いくつかの版(Edition)が存在します。

エディション リリース リリース内容
6th 2015年 クラス、モジュール、イテレータ等
7th 2016年 冪乗、Array#includes等
8th 2017年 async/await、Trailing Comma等
9th 2018年 Spread Properties、Promise#finally等

Wikipedia より引用
https://ja.wikipedia.org/wiki/ECMAScript

直近でリリースされたEditionは下記サイトで公開されています。
https://www.ecma-international.org/publications/standards/Ecma-262.htm

技術系の投稿サイトなどで「es6でクラスが使えるようになった」みたいな表現を見たことがありませんか?これはつまり、

  • ECMAScript 6th Editionでクラス構文が仕様として策定された
  • 実行環境が次々とクラス構文を解釈できるように実装を進めた
  • クラス構文が気軽に使えるようになった

という事ですね。

TC39

Ecma Internationalには Technical Commitee 39 というECMAScript策定のための専門委員会が存在し、定期的にミーティングが開催されます。このミーティングのメンバーにはブラウザベンダーなど企業も含まれます。個々の新しい機能の提案について、議論が収束しメンバーの合意が得られるとECMAScriptのドラフトが更新されます。

ドラフトはTC39のGitHubリポジトリで管理されています。
https://github.com/tc39/ecma262

READMEに human-readable version と記載されているこちらが読みやすいですね。
https://tc39.github.io/ecma262/

策定プロセス

「議論が収束しメンバーの合意が得られると」の部分をもう少し掘り下げてみると、個々の新しい機能が提案されてからECMAScriptのドラフトが更新されるまでに、いくつかの段階(ステージ)を踏む必要があります。それぞれの機能ごとにチャンピオンと呼ばれる進行役が付き、TC39のミーティングの場でプレゼンテーションを行いステージアップを図ります。

ステージ 名前 補足
0 Strawman アイデアとしての段階
1 Proposal 仕様として提案、ユースケースや課題の認識
2 Draft ドラフトとして策定
3 Candidate ブラウザなどで試験的に実装
4 Finished 仕様策定の準備が完了した状態

The EC39 Process より引用
https://tc39.github.io/process-document/

個々の機能は Proposal と呼ばれています。TC39では

  • ステージ1〜3Active Proposals
  • ステージ4Finished Proposals
  • チャンピオンが降りるなどの原因で進捗が見込まれないものを Inactive Proposals

として位置づけています。

ProposalsはGitHubで管理されているため、進捗状況を随時見る事ができます。
https://github.com/tc39/proposals

TC39での議論を経て Finished Proposalsに位置づけられた機能は、正式な機能としてECMAScriptのドラフトに追加 されます。そして毎年改訂版をリリースするルールにより、次期Editionに策定される事になります。

技術系の投稿サイトなどで「○○の機能はまだProposalsだからうんぬん…」みたいな表現を見たことがありませんか?これはつまり、

  • TC39の議論でステージ 1〜4 のいずれかの状態
  • 1〜3 であれば仕様が変わったりInactive Proposalsになる可能性がある
    • 恒久的に使える訳ではない
  • 4 であれば仕様が固まっている
    • 実行環境で実装されれば恒久的に使える

という事ですね。

ECMAScriptの実装状況を知ろう

ECMAScriptに策定された(およびその見込みがある)機能は、Chrome/Firefox/Node.jsなどの実行環境で順次実装されていきます。実装のタイミングはそれぞれの実行環境により異なります。

Compatibility table

ECMAScript compatibility tableでEditionごとに対応・非対応の実行環境を調べる事ができます。 http://kangax.github.io/compat-table/

策定見込みの機能が実装されるケース

String.prototype.matchAll は2018/11現在、Proposalsステージ3となっています。ECMAScript compatibility tableでEdition nextを見ると、CH69の列に Flag の記載があり、Chrome Ver69においてフラグを切り替える事でこの機能が利用可能になる事が分かります。

matchall

実際にこの機能をChromeで試してみましょう。

  • Chromeを開きURLに chrome://flags/ と入力
  • Search flags"harmony" と入力
  • Experimental JavaScriptEnabled に変更し RELAUNCH NOW をクリック

harmony

デベロッパーツールでConsoleタブを開き、matchAllの実装があるか試してみましょう。

experiments-enabled

Experimental JavaScriptDisabled に戻すとmatchAllは無効化されます。

experiments-disabled

ECMAScriptの次期Edition(に策定されるかもしれない)機能を予習する時は、このフラグが役立ちそうですね。

Platform status

Chrome/Firefox/Edge/Node.jsなどは、実装状況の検索/一覧サイトを提供しています。サイトを利用すると「この機能はどのバージョンやビルドで実装されたのか」など詳細を知る事ができます。

ここまでのまとめ

JavaScriptの仕様「ECMAScript」には策定プロセスが定められていて、TC39のミーティングを経てプロセスのステージが進みます。プロセスが成熟するとそれぞれの実行環境が追随する形で追加の機能を実装していきますが プロセスの成熟度や実行環境の開発のタイミングにより、実装状況はまちまちになります。 これが「なんで特定のブラウザで動作しないの?」の背景です。

(前述の通り、仕様の解釈の違いによる挙動のばらつきなども存在するため全ての要因という訳ではありませんのでご留意ください)

コーディングの時に意識するべきポイント

では、コーディングの時に何に気をつけるべきでしょうか。ここから先は主観になりますが、私の考え方を紹介します。

ターゲットとする実行環境を把握

社内の開発チームだけが利用するツールと、社内外に不特定多数の利用者が存在するようなプロダクトでは、状況が大きく違ってきます。Webシステムであれば Google Analytics などを活用しデバイスの利用状況を把握しましょう。その上で「最新版のChromeだけ」「日本国内のシェア10%以上のブラウザ、かつ直近の2バージョン」などターゲットとする実行環境を決定しましょう。

プロダクトにかけられる作業コストの見積り

新規のプロダクトであれば webpack などモジュールバンドラーやそのプラグインを利用すれば、かなり自由に開発環境を整える事ができます。一方でHTMLにJavaScriptが埋め込まれていたり、バックエンドのレンダリング処理と結合度が高かったり、開発環境を簡単に変えられないプロダクトも存在します。どれだけ作業コストをかけられるプロダクトなのかも大きなポイントとなります。

1)実行環境ごとの実装状況を考慮しながらコーディングするケース

ECMAScriptの機能 利用可能かどうか 補足
prototype拡張 ターゲットの実装による
グローバルオブジェクト ターゲットの実装による
式の利用 ターゲットの実装による
Proposalsの機能 ターゲットの実装による

事情があってレガシーな仕組みを全く変えられないプロダクトに対しては ECMAScript Compatible table などで ターゲットとなる実行環境の実装状況を把握しながらコーディングする事 がポイントとなります。

実行環境とそのバージョンごとの実装状況をいちいち調査しながらコーディングするのはとても手間がかかるため browserslist などを利用してチェックの自動化を検討したほうがいいかもしれません。

2)ポリフィルするケース

ECMAScriptの補完 利用可能かどうか 補足
prototype拡張  
グローバルオブジェクト  
式の利用  
Proposalsの機能 ブラウザ向けファイルには含まれない

core-js などのポリフィルライブラリを利用すると、ファイルを読み込むだけでECMAScript 5th Edition〜の機能(およびProposals)が補完されます。

ポリフィルを利用する場合は ポリフィルライブラリで補完される機能と補完されない機能がある事に注意する事 がポイントとなります。例えばcore-jsでは、式は補完されません。そのため、arrow function関数式や、yield式を伴うGenerator関数などは実行環境で実装されていないと利用できません。

core-jsはCommonJSのモジュールシステムを解決するrequire関数に依存しています。require関数を利用できない環境では、ブラウザ向けのファイルを読み込む利用方法が提供されています。このファイルにはProposalsの機能は含まれません。

core-js v2.5.7の利用例

$ npm install core-js
<!-- requireを利用できる環境 -->
<script src="/node_modules/core-js/index.js"></scirpt>

<!-- requireを利用できない環境 -->
<script src="/node_modules/core-js/client/core.js"></scirpt>

3)トランスパイルするケース

ECMAScriptの補完 利用可能かどうか 補足
prototype拡張  
グローバルオブジェクト  
式の利用  
Proposalsの機能 ステージ4のみ

もう少し作業コストをかけられるプロダクトではトランスパイル(あるフォーマットのファイルを解析し別のフォーマットとして出力する)が有効な手段だと思われます。メジャーなトランスパイラとしては Babel が挙げられます。

Babelはソースコードそのものを変換するため式の利用も可能です。また @babel/polyfill を通してポリフィルが行われるため(内部ではcore-jsが利用されます)、ターゲットの実行環境を意識しながらコーディングする必要はなくなります。ただしProposalsについてはステージ4しか含まれないため、3以下の機能を利用するにはポリフィルライブラリとの組み合わせが必要です。
https://babeljs.io/docs/en/babel-polyfill

トランスパイルを利用する場合は トランスパイラおよび内部のポリフィルがサポートしていないECMAScriptの機能を使わないように注意する事 がコーディングのポイントとなります。リリース情報やbugfixなどをチェックしておきたいですね。

Babel(@babel/cli v7.1)の利用例

$npm install --save-dev @babel/core @babel/cli @babel/preset-env
// src/test.js
class Person {
  sayHello (name) { return `Hello ${name}.` };
}

function sample() {
  return Array.from(1,2);
}
// babel.config.js
// 実行環境とバージョンを指定
const presets = [
  [
    "@babel/env",
    {
      targets: {
        edge: "10",
        ...
      },
      ...
$ ./node_modules/.bin/babel src --out-dir lib
// lib/test.js(トランスパイルされたファイル)
...
require("core-js/modules/es6.array.from");

var Person =
/*#__PURE__*/
function () {
  function Person() {
    _classCallCheck(this, Person);
  }

  _createClass(Person, [{
    key: "sayHello",
    value: function sayHello(name) {
      return "Hello ".concat(name, ".");
    }
  }]);

  return Person;
}();

function sample() {
  return Array.from(1, 2);
}

4)TypeScriptを利用するケース

ECMAScriptの補完 利用可能かどうか 補足
prototype拡張 ポリフィルライブラリと組み合わせる
グローバルオブジェクト ポリフィルライブラリと組み合わせる
式の利用 TypeScriptのシンタックス
Proposalsの機能 - TypeScriptおよびポリフィルに同等の機能があれば使用可能

新規のプロダクトなど最初から開発環境を構築するようなケースでは TypeScript を選択するのもひとつの手段です。

TypeScriptはJavaScriptの式に相当する部分をTypeScript独自のシンタックスとして提供しています。シンタックスにはECMAScriptの構文を積極的に取り入れているため、最新のECMAScriptの学習にもつながります。またTypeScriptのコンパイラ「tsc」には、出力したソースコードを利用する実行環境のECMAScriptのEditionをオプションとして指定する事ができます。

TypeScriptを利用する場合は コンパイル後のソースコードはJavaScriptであり補完されずにそのまま出力される機能の存在に注意する事 がポイントとなります。クラス構文などはTypeScriptでサポートするシンタックスのためコンパイル時に変換されますが、MapオブジェクトやArray.prototype.fromなどはJavaScriptの機能のためコンパイル時に変換されません。

以下は typescript v3.1.6 でコンパイルしたサンプルのソースコードです。

TypeScriptのシンタックスがEditionに合わせて変換される例

// foo.ts
class Foo {
  private value = 'a';
}
$ npm install typescript
$ ./node_modules/.bin/tsc --target es3 --outFile foo.js foo.ts
// foo.js(コンパイルされたファイル)
// ECMAScript 3th Editionにクラス構文がないためfunctionに変換される
var Foo = /** @class */ (function () {
    function Foo() {
        this.value = 'a';
    }
    return Foo;
}());

JavaScriptの機能が変換されない例

// foo.ts
let map = new Map();
var a = Array.of(1);
$ npm install typescript

# es3にはMap/Array.prototype.ofが存在しないためコンパイルエラーが発生
$ ./node_modules/.bin/tsc --target es3 --outFile foo.js foo.ts
$ foo.ts(1,15): error TS2552: Cannot find name 'Map'. Did you mean 'map'?
$ foo.ts(2,15): error TS2339: Property 'of' does not exist on type 'ArrayConstructor'.

# es6にはMap/Array.prototype.ofが存在するためコンパイルが通る
$ ./node_modules/.bin/tsc --target es6 --outFile foo.js foo.ts
// foo.js(コンパイルされたファイル)
// 機能が補完される訳ではなくJavaScriptとしてそのまま出力される
let map = new Map();
var a = Array.of(1);

TypeScriptを利用する場合はポリフィルライブラリと組み合わせる事で幅広い機能が利用できるようになります。

番外編)Proposalsの代替機能として自作コードを追加するケース

「汎用的に使える◯◯の機能を自作しました」といった記事を見かけた事はありませんか?「汎用的な機能だと思うけどなぜか実装されていないから」「ビルトインのオブジェクトはブラウザによって挙動が変わるから」など理由はいろいろあると思いますが、私個人は安易に自作コードを追加するのではなく ECMAScriptのProposalsで提案されている内容と重複がないか気をつけること がポイントになると考えています。

目的の重複

まずは tc39/proposals で同じ目的の機能が提案されていないかチェックしてみましょう。もしかすると開発環境で利用しているブラウザに実装されていないだけで、すでにポリフィルライブラリに実装されているかもしれません。

インターフェースの重複

「同じ目的の機能がProposalsに存在するけど事情によりポリフィルできない」といった場合は、インターフェースが重複しないように気をつけたほうが良いでしょう。同名のグローバルオブジェクトやプロトタイプ拡張を、自作コードからビルトインやポリフィルに置き換える作業は無駄なバグの温床になりがちです。仮にパラメータの数や順序、正常パターンの挙動が一致していたとしても、想定外のパラメータが渡された時の挙動や戻り値が完全一致するでしょうか?例えば異常を示す戻り値について、自作コードではundefinedを返していたところがビルトインへの置き換えによってnullやNaNに変わった場合、またその戻り値を外部パッケージのオブジェクトに渡している場合など、どんな影響が出るか想像しづらいものですよね。

ポリフィルライブラリの信頼性

外部のポリフィルライブラリを利用したからもうこれで安心…と思い込むのも要注意です。「ポリフィルライブラリです!」と謳っているライブラリの中にはECMAScriptの策定内容とインターフェースが重複していて、かつ挙動の異なるものも存在します。待望の機能がせっかくビルトインに登場したのに、それをポリフィルライブラリが微妙に挙動の異なるオブジェクトやメソッドに置き換えてしまったら混乱しますよね。参考までに、私自身はこの記事に何度か登場している core-js を信頼して使っています。

おわりに

最後まで読んでいただきありがとうございました。 認識の間違いや文章中の記載間違いなどお気づきの点があればご指摘いただけると幸いです。

冒頭にて 「○○オブジェクトはこのブラウザでは使えない」みたいな断片的な知識を頼りにコーディング と書きましたが、実は私自身が断片的な知識でコーディングする派でした。けれども、じっくり調べて理解すると どのサイトを見れば仕様が分かるのかトランスパイルを自動化する際に設定ファイルに書いてある事が分かるようになった のような嬉しい副作用がたくさん付いて来ることを知ったので、流派を変えようかなと思っています。また、調べる事で得た知識をチームで共有してワイワイする時間が大好きです。