速習

ESNext

Ryo Utsunomiya(@ryo511)

Gutenberg MeetUp vol.2

2018-05-09

自己紹介

  • 宇都宮 諒(うつのみや りょう)​
  • 株式会社 一休 宿泊事業本部 マーケティング部
  • 職種は「フロントエンドエンジニア」
  • 主なお仕事
  • ​WordPress歴
    • ​ユーザとして3年くらい
    • 開発者として半年くらい
  • ​Twitter: @ryo511
  • 個人ブログ( https://ryo511.info/ )

一休コンシェルジュ

  • 上質なホテル・旅館を紹介するメディア
  • powered by WordPress!

アジェンダ

  1. ESNextとは

  2. ES5落穂拾い

  3. ES2015+

  4. ESNextとGutenberg

ECMAScript

  • JavaScriptの仕様は、ECMAScriptという名前で標準化されている
  • ブラウザで実装されているバージョンとしては以下が代表的
    • ES3(IE6〜8あたり)
    • ES5(IE9以上)
    • ES6/ES2015(IE11は非対応、主要ブラウザは対応済み)
  • ES2015以降は、毎年新しいバージョンが出る
    • 今年はES2018

ESNext

  • Gutenberg Handbookでは、ES2018相当のことを「ESNext」と呼んでいる

ブラウザの新API !== ESNext

  • Webブラウザの新しいAPIは、ECMAScriptとは別に標準化されている
    • Web Components, Service Worker, Web Payments等
  • こららはJavaScriptから利用できるAPIだが、JavaScriptの仕様に入っている機能ではない

ESNextは「今そこにある未来」

  • ECMAScriptの標準化には、原則として、2つ以上のブラウザでの実装が必要
  • = 標準化された機能は最低でも2つのブラウザで動く
    • Chrome, Firefox, Edge, Safariの各最新版はES2015相当は実装済み
    • ES2016+2017も7割ほど実装済み
  • 今日解説する機能は、一部を除いて手元のブラウザで動作するもの
  • ただし、IE11を除く
  • 詳しい対応状況は https://kangax.github.io/compat-table/ が参考になる

なぜESNextを使うのか?

  • 新しい機能は、従来のJavaScriptの問題点を解決するものであることが多い
    • 例:コールバック地獄 => Promise => async/await
  • より短く、書き味の良い構文が導入されることも多い
    • 例: function(){} と書くより ()=>{} と書く方が楽
  • => ESNextを使うと、より良いJavaScriptを、より簡単に書くことができる

ESNextの導入方針

  • 全ての構文やメソッドを一度に取り入れる必要はない
  • ES5でも実用的なアプリケーションは作れる

ES5落穂拾い

  • ES2015の前に、ES5で追加された機能をおさらい
  • オブジェクト
    • getter/setterの定義
    • メソッド: Object.keys, Object.defineProperty等
  • 配列
    • Array.isArray
    • Array.forEach
    • Array.reduce
    • Array.map
    • Array.filter

ES5: Object.defineProperty

  • オブジェクトのカスタムプロパティを定義できる
  • プロパティに値がセットされるのを契機に別メソッドを呼ぶような実装が可能
  • Vue.jsのリアクティブシステムのコアはこのメソッド
const object1 = {};

// 書き込み不可プロパティを定義
Object.defineProperty(object1, 'property1', {
  value: 42,
  writable: false
});

// 書き込み不可(strictモードではエラーになる)
object1.property1 = 77;

console.log(object1.property1); // 42

ES5: getter/setter

  • シンプルな構文でカスタムプロパティが定義できる
var user = {
  firstName: 'John',
  lastName: 'Smith',
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  }
  set fullName(value) {
    var names = value.split(' ');
    this.firstName = names[0];
    this.lastName = names[1];
  }
};

console.log(user.fullName); // John Smith

user.fullName = 'Jack Johnson';
console.log(user.firstName, user.lastName); // Jack, Johnson

ES5: Object.keys

  • オブジェクト自身のキーを列挙する
var user = {
  firstName: 'John',
  lastName: 'Smith',
};

console.log(Object.keys(user)); // firstName, lastName

// Object.keys + Array.forEachによるオブジェクトの走査
Object.keys(user).forEach(function(key) {
  console.log(user[key]); // John, Smith
});

// ↑と同等のES3コード
for(var key in user) {
  // このチェックがないとprototypeのプロパティも列挙してしまう
  if(user.hasOwnProperty(key)) {
    console.log(user[key]);
  }
}

ES5: Array.isArray

  • ES3までのJSでは、配列とオブジェクトの区別は面倒だった
// ES3
console.log(Object.prototype.toString.call([]) === '[object Array]'); // true
console.log(Object.prototype.toString.call({}) === '[object Array]'); //false

// ES5
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false

ES5: Array.forEach

  • 配列の各要素に対して処理を行う
var a = [1,2,3,4,5];
a.forEach(function(n) {
  console.log(n); // 1,2,3,4,5
});

ES5: Array.flter

  • 配列を一定条件で絞り込み
var a = [1,2,3,4,5];
var even = a.filter(function(n) {
  return n % 2 === 0; // 偶数のみ絞り込み
});
console.log(even); // 2,4

ES5: Array.map

  • 配列の各要素に処理を行って新しい配列を返す
var a = [1,2,3,4,5];
var double = a.map(function(n) {
  return n * 2; // 配列の全要素を2倍にする
});
console.log(double); // 2,4,6,8,10

ES5: Array.reduce

  • 配列をもとに新しい値を作り出す
var a = [1,2,3,4,5];
var sum = a.map(function(accumulator, current) {
  return accumulator+  current;
});
console.log(sum); // 15

ES5: filter+map+reduce

  • 配列メソッドはチェインすることで真価を発揮する
var a = [1,2,3,4,5];

var evenDoubleSum = a.filter(function(n) {
  return n % 2 === 0; // 偶数のみ絞り込み => [2,4]
}).map(function(n) {
  return n * 2; // 2倍 => [4,8]
}).reduce(function(accumulator, current) {
  return accumulator + current; // 4 + 8
});

console.log(evenDoubleSum); // 12

// ES2015のアロー関数式を使うとよりシンプルに
const evenDoubleSum = a.filter(n => n % 2 === 0)
  .map(n => n * 2)
  .reduce((a, c) => a + c);

ES2015〜

  • ES2015〜には新機能が大量にあるので、よく使うものをピックアップして紹介
  • アロー関数式
  • const, let
  • class
  • テンプレートリテラル
  • 配列の分割代入
  • オブジェクトの分割代入
  • モジュール
  • Priomise, async/await(ES2017)
  • この他にも、ジェネレータ、Symbol、Map等々

ES2015: アロー関数式

  • 関数を短く書ける
  • 無名関数の書き味が劇的に向上
// ES5
var hello = function() { return 'Hello' };
// ES2015: アロー関数式の基本形、function() を ()=>と省略できる
const hello = () => { return 'Hello' };
console.log(hello()); // Hello

// ES5
var greet = function(greeting) { return greeting };
// ES2015:引数が1つの場合は括弧を省略可能
const greet = greeting => { return greeting };
console.log(greeting('Good morning')); // 'Good morning'

// ES5
var greetWithName = function(greeting, name) {
  return greeting + ', ' + name + '!';
};
// ES2015:関数本体が1行の場合は、{return}を省略可能
const greetWithName = (greeting, name) => `${greeting}, ${name}!`;
console.log(greetWithName('Good morning', 'John')); // Good morning, John!

ES2015: 関数のデフォルト引数

  • 関数のデフォルト引数を設定できる
// ES5
function greet(name, greeting) {
  if (greeting === undefined) greeting = 'Hello'; // greetingのデフォルト値設定
  return greeting + ', ' + name + '!';
}

// ES2015
// greetingが渡されなかった(undefined)場合はHelloになる
function greet(name, greeting = 'Hello') {
  return `${greeting}, ${name}!`; // テンプレートリテラル(後述)
}

console.log(greet('John')); // Hello, John
console.log(greet('John', 'Good morning')); // Good morning, John

ES2015: 余り物パラメータ

  • 関数パラメータの「余り」をまとめて取得できる
// ES5
function sum() {
  var n = 0;
  // argumentsはArrayではないので、for文で走査する
  for (var i = 0; i < arguments.length; i += 1) {
    n += arguments[i];
  }
  return n;
}

// ES2015
function sum(...args) {
  // argsはArrayなので、配列操作メソッドが呼べる
  return args.reduce((a, c) => a + c);
}

console.log(sum(1)); // 1
console.log(sum(2, 3)); // 5

ES2015: const, let

  • const, letはブロックスコープ
    
  • constで定義した変数は再代入不可
// ES5
var a = 'a';
a = 'b'; // OK

for (var i = 0; i < 5; i += 1) {
  console.log(i);
}
console.log(i); // 4

// ES2015
const a = 'a';
a = 'b'; // 再代入不可エラー

for (let i = 0; i < 5; i += 1) {
  console.log(i);
}
console.log(i); // undefined

ES2015: class構文

  • オブジェクトコンストラクタの糖衣構文
    • クラスベースのオブジェクト指向ではない
// ES5
function Person(name) {
  this.name = name;
}

// ES2015
class Persoon {
  constructor(name) {
    this.name = name;
  }
}

// 使い方は同じ
const aPersoon = new Person('John');

ES2015: class + extends

  • extendsキーワードを使って、既存オブジェクトの機能を引き継いだオブジェクトを作成できる
// React.Componentを継承した新しいクラスを定義
class MyComponent extends React.Component {
  // ...
}

ES2015:テンプレートリテラル

  • 変数を文字列内に埋め込める
    
  • 改行をそのまま反映できる
// ES5
var name = 'John';
var greeting = 'Hello, ' + name + '.' + '\n' +
               'Nice to meet you!';

// ES2015
const name = 'John';
const greeting = `
Hello, ${name}.
Nice to meet you!`;

ES2015:配列の分割代入

var array = [1, 2];

// ES5
var a = array[0];
var b = array[1];

// ES2015
const [a, b] = array;

// Promise.all の結果を受け取ったりするのに便利
const [result1, result2] = await Promise.all([
  doSomething1(),
  doSomething2(),
]);

ES2015:オブジェクトの分割代入

var obj = {
  a: 1,
  b: {
    foo: 'bar',
  },
};

// ES5
var a = obj.a;
var foo = obj.b.foo;

// ES2015
const { a } = obj; // 変数 a が定義され、1が入る
const { b: { foo } } = obj; // 変数 foo が定義され、'bar'が入る

// 関数の引数でも使える
function createDate({ year, month, day }) {
  return new Date(year, month + 1, day);
}
createDate({ year: 2017, month: 5, day: 9 });

ES2015:モジュールシステム

  • モジュールシステムとは、依存するライブラリ等を読み込むための仕組み
  • ES5までは、ライブラリはグローバル変数に展開するのが普通だった
    • 例: window.$ にjQueryがロードされている、等
  • ES2015で導入されたモジュールシステム(ES Modules)では、外部モジュールを読み込むための構文が仕様化された
  • ただし、ブラウザ対応は完全ではない

ES2015:import

  • importを使うことで、外部モジュールを読み込める
// jQueryを$という変数名で読み込み
import $ from 'jqeury';

ES2015:export

  • exportを使うことで、自前の関数やオブジェクトを外部から読み込み可能にする
// math.js
const sum = ...args => args.reduce((p, c) => p + c);

export sum;

// index.js
// math.jsでexportされている関数をまとめてオブジェクトにしてmath変数に代入
import * as math from './math.js';

math.sum(1, 2); // 3

// オブジェクトの分割代入を使用すれば直接importできる
import { sum }  from './math.js';

ES2015:default export

  • import時にデフォルトで取得できるオブジェクトや関数を設定できる
// sum.js
const sum = ...args => args.reduce((p, c) => p + c);

export default sum;

// index.js
import sum from './sum.js';

ES2015:Promise(1)

  • Promiseの前提として、「コールバック地獄」という現象について知っておくと良い
$.get('http://api.example.com/auth', function(result1) {
  if (result1.isAuthenticated) {
    // ログイン済みならユーザ情報を取得
    $.get('http://api.example.com/me', function(result2) {
      if (result2.userData) {
        // ユーザーデータをどこかに送信
        $.post('http://api.example.com/log', result.userData, function(result3) {
          // さらに処理が続く...
        })
      }
    });
  }
});

ES2015:Promise(2)

  • Promiseは、「将来、処理が成功(resolve)または失敗(reject)するオブジェクト」
  • 非同期処理をカプセル化できる
// 1秒後にランダムで成功/失敗するオブジェクト
var p = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (Date.now() % 2 === 0) {
      resolve('even');
    } else {
      reject('odd');
    }
  }, 1000);
});

p.then(result => console.log(result)) // thenで設定した関数がresolveになる
  .catch(error => console.error(error)); // catchで設定した関数がrejectになる
// 1秒後に結果が表示される

ES2015:Promise(3)

  • jQuery 3系のAjax系メソッドはPromise互換
    • 2以前のjQuery.Deferredは独自仕様
  • Promiseだけだと、そこまで便利になってない 🤔
// Promiseを使ってもコールバック地獄と大差ないコード...
$.get('http://api.example.com/auth')
  .then(result1 => {
    $.get('http://api.example.com/me')
      .then(result2 => {
      // ...
  });

ES2017: async/await(1)

  • await演算子を使うと、Promiseの結果を「待つ」ことができる
  • awaitを使うには、関数にasyncキーワードが必要
// 1秒後にランダムで成功/失敗するオブジェクト
var p = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (Date.now() % 2 === 0) {
      resolve('even');
    } else {
      reject('odd');
    }
  }, 1000);
});

(async () => {
  const result = await p.catch(e => console.error(e));
  if (result) console.log(result);
})();

ES2017: async/await(2)

  • awaitを使うと、非同期処理の複雑さを低減できる!
const auth = await $.get('http://api.example.com/auth');

if (auth.isAuthenticated) {
  const userData = await $.get('http://api.example.com/me');
  if (userData) {
    const result = await $.post('http://api.example.com/log', result.userData);
    // 結果に応じた処理...
  }
}

ESNextを開発で使うには

  • ESNextはブラウザによって対応状況が異なる
  • IE11のように、新機能の追加が止まっているブラウザもある
  • 新しい構文・オブジェクト・メソッドを、非対応ブラウザでも使えるようにする必要がある
  • 主な手法は2つ
    • トランスパイル:新構文 => 旧構文の変換
    • ポリフィル:非対応ブラウザ向けに、新しいオブジェクトやメソッドを定義する(たとえば、ES2015互換のPromiseオブジェクトを独自に定義する)

ESNextのトランスパイル

  • Babelというトランスパイラを使うのが一般的
  • ESNextで書いて、ES5に変換する
    • たとえば、const/letは全てvarに変換される
  • ReactのJSXのコンパイルにもBabelを使用するので、Gutenbergプラグインを作る際は基本的にBabelを使うはず

ESNextのポリフィル(polyfill)

  • 代表的なpolyfillライブラリは下記
    • babel-polyfill
    • babel-runtime
  • このほか、特定の機能に特化したpolyfillもある
    • github/fetch等
  • Gutenbergはpolyfillライブラリを使っていない
  • プラグインでは、ビルド時にpolyfillするbabel-runtimeがおすすめ
    • babel-polyfillは複数回読み込むとエラーになるので、他のプラグインとのバッティングの恐れが

ESModulesの代替

  • モジュールシステムはモダンブラウザでも完全な実装が完了していない
  • モジュールシステムをIE11のような旧世代ブラウザでも使えるようにするツールがある
    • webpack, browserify等
  • Gutenbergはwebpackを使用
  • プラグインもwebpackがおすすめ。ビルド設定の柔軟さや周辺ライブラリの充実度が段違い

ESNextとGutenberg

  • GutenbergはReact.jsで実装されている
  • Gutenbergプラグインも、React.jsで実装する必要がある
  • React.jsのコードやGutenbergプラグイン用のAPIは、ES5でも開発できるようになっている
  • しかし、開発効率を考えると、ESNextの方が良い

まとめ

  • JavaScriptは毎年新しい機能が追加されてアップデートしている
  • Gutenbergプラグインの開発にあわせて、JavaScriptの知識をアップデートしよう!

速習ES.Next

By Ryo Utsunomiya

速習ES.Next

Gutenberg Meetup vol.2(2018-05-09)

  • 2,024