LoginSignup
61
81

More than 5 years have passed since last update.

ES2015/2016+ 記法 個人的メモ

Last updated at Posted at 2016-11-26

以前社内向けに書いていた個人的メモをせっかくなので公開します。
昔ながらのJavaScriptなら分かるけど、暫くフロントエンドから離れてた人向けな記事です。
と言いつつ自分もまだ経験浅いので間違っていたら指摘お願いします。


モダンなフロントエンドフレームワークはES2015/2016+の記述が当たり前のように出てきます。
reactとかangular2とか触る前に新しい記法を頭に入れておきましょう。

そもそもECMAScript is 何って話はしないです。

文字読むのが面倒な人はこの辺をどうぞ。(解説動画)
Learn ES6 (ECMAScript 2015) - Course by @johnlindquist @eggheadio
ES2015 Crash Course

ブラウザ対応状況

ブラウザの対応状況は以下のサイトで確認できます。
ECMAScript 6 compatibility table
IE以外の主要ブラウザのES2015対応はほぼ完了していますが、
実際のサービスに導入したい場合は、babel等のトランスパイラでES5記述に変換してあげる必要があるでしょう。

ES2015

※比較的よく見かけるものを列挙しただけなのでこれが全てではないです。

アロー関数

こんな関数があったとします。

var greeting = function(message, name) {
  return message + name;
}

ES2015では以下のように書けます。

var greeting = (message, name) => {
  return message + name;
}

関数が1つのreturn文のみであれば、{}returnは省略できます。

var greeting = (message, name) => message + name;

引数が1つだけなら、()は省略できます。

var greeting = message => message;

引数がない場合は、()は省略できません。

var greeting = () => 'hello';

{}で囲ったオブジェクトを直接returnする場合は、更に()で囲みます。

var greeting = (message, name) => ({ msg: message, name: name });

要はアロー関数は無名関数の新しい記法です。
しかしthisの扱いが微妙に異なります。以下は"Hello, John"と出力させたいコードです。

var man = {
  name: "John",

  handleMessage: function (message, handler) {
    handler(message);
  },

  receive: function () {
    // console.log(this.name); // これは動く
    this.handleMessage("Hello, ", function (message) {
      // うまく動かない
      console.log(message + this.name);
    })
  }
}

man.receive();

しかしこれはうまく動きません。
10行目のthisはJavaScriptの不可解な仕様でグローバルオブジェクトを指してしまいます。
そのため今までは以下のようなテクニックで回避してきました。

var man = {
  name: "John",

  handleMessage: function (message, handler) {
    handler(message);
  },

  receive: function () {
    // thisを無名関数の外で変数に格納
    var that = this;
    this.handleMessage("Hello, ", function (message) {
      console.log(message + that.name);
    })
  }
}

man.receive();

アロー関数を使うと以下のように書けます。

var man = {
  name: "John",

  handleMessage: function (message,handler){
  handler(message);
  },

  receive: function () {
    this.handleMessage("Hello, ", message => console.log(message + this.name))
  }
}

man.receive();

無名関数内のthisも、意図した値を指しています。

そもそも従来のthisは、それが何を指し示すのかは文脈によって異なるというシロモノでした。
グローバルスコープで呼び出された場合、

function hello() {
  console.log(this);
}
hello();

thisはグローバルオブジェクトを指します。
一方オブジェクトのメソッドの中で呼びだされた場合、

var obj = {
  value: 'hey',
  hello: function() {
    console.log(this.value);
  }
}
obj.hello(); // hey

thisはそのオブジェクトを指します。
他にもあり混乱するので、自分はこちらの記事をよく見直します。
JavaScriptの「this」は「4種類」?? - Qiita

さて、アロー関数はそれが定義された場所によって関数内部のthisの値が固定されます。

var obj = {
    name: "John",
    hello: function () {

        var funcA = function () {
            console.log(this.name);
        };

        funcA(); //グローバルスコープで呼び出し

        var funcB = () => {
            console.log(this.name);
        };

        funcB(); //グローバルスコープで呼び出し
    }
};

obj.hello();

9行目も15行目も、どちらもグローバルスコープで呼び出しています。
funcA()の出力結果は環境にもよりますが、thisがグローバルオブジェクトになるのでthis.name"John"にはなりません。
一方アロー関数の場合はどう呼び出されようが関係ありません。定義された場所、この場合objオブジェクトがthisとなりますので、this.name"John"となります。

クラス構文

従来の記法でクラスっぽいことを書こうとすると以下のように書けます。

function User(username, email) {
  this.username = username;
  this.email = email;
}

User.prototype.changeEmail = function(newEmail) {
  this.email = newEmail;
}

var user = new User('John', 'dummy@example.com');

user.changeEmail('john@example.com');

console.log(user);

JavaScriptにクラスはありませんので、プロトタイプチェーンでオブジェクトの参照を繋げていきます。

これはES2015で以下のように書けます。

class User {
  constructor(username, email) {
    this.username = username;
    this.email = email;
  }

  changeEmail(newEmail) {
    this.email = newEmail;
  }
}

var user = new User('John', 'dummy@example.com');

user.changeEmail('john@example.com');

console.log(user);

classconstructorと書いてますが、JavaScriptにはクラスはありません。
あくまで従来の記法のシンタックスシュガーです。
他には継承でおなじみのextendsクラス名.メソッド名で呼び出せるstaticメソッド的な記法もありますが、普段オブジェクト指向言語に触れている方であれば特に問題は無いでしょう。

変数宣言(let, const)

今までJavaScriptの変数宣言といえばvarでした。
ES2015では新たにletconstが追加されました。

let

letはvarに似てますが、再宣言出来ません。

var hoge = 'hey';
var hoge = 'yo'; // OK
console.log(hoge);

let fuga = 'hey';
let fuga = 'yo'; // NG
console.log(fuga);

const

constは定数です。再宣言も再代入もできません。

var hoge = 'hey';
hoge = 'yo'; // OK
console.log(hoge);


const fuga = 'hey';
fuga = 'yo'; // NG
console.log(fuga);

varとの違い

let/constが登場するまで、JavaScriptにはif文やfor文におけるブロックスコープが存在しませんでした

if (true) {
  var hoge = 'hey'; 
}

for (var i = 0; i < 3; i++) {
  var fuga = 'yo';
}

console.log(hoge);  // ==> 'hey'
console.log(i);     // ==> 3
console.log(fuga);  // ==> 'yo'

let/constにすると

if (true) {
  const hoge = 'hey'; 
}

for (let i = 0; i < 3; i++) {
  const fuga = 'yo';
}

console.log(hoge);  // ==> hoge is not defined
console.log(i);     // ==> i is not defined
console.log(fuga);  // ==> fuga is not defined

全てnot definedになります。ブロックスコープが有効になっている証拠です。

もう一つ、let/constは「ホイストされない」という特徴があります。
ホイストとは変数の宣言が巻き上げられることです。

console.log(hoge); // ==> undefined
var hoge = 'hey';

こうすると出力結果はundefinedになります。
しかしよくよく考えると違和感があります。1行目時点ではhogeは宣言すらしていないのだから、hoge is not defined になりそうなものです。
これはvarはホイストされる特徴があるからです。上のコードは以下と同じです。

var hoge;
console.log(hoge); // ==> undefined
hoge = 'hey';

let/constならホイストは起きません。

console.log(hoge);
let hoge = 'hey'; // ==> hoge is not defined

Template String

バッククオートで囲んで変数の文字列展開が可能になりました。従来のように「+」で変数と文字列を延々と繋げる必要はありません。

const name = "John";
console.log(`Hello, ${name}.`); // ==> Hello, John

プロパティの省略

例えば以下の様なコードがあります。

function getPerson() {
  let name = 'John';
  let age = 25;

  return {
    name: name,
    age: age
  };
}

console.log(getPerson().name); // ==> John

ES2015ではオブジェクトのプロパティのキー名と値の変数名が等しい場合、以下のように省略して書けます。

function getPerson() {
  let name = 'John';
  let age = 25;

  return { name, age };
}

console.log(getPerson().name); // ==> John

別の例を見てみましょう。

function greet(person) {
  let name = person.name;
  let age = person.age;
  console.log(`Hello, ${name}. You are ${age}.`); // ==> Hello, John, You are 25.
}
greet({name: 'John', age: 25});

ES2015では以下のように書けます。

function greet({name, age}) {
  console.log(`Hello, ${name}. You are ${age}.`); // ==> Hello, John, You are 25.
}
greet({name: 'John', age: 25});

とにかく{}で囲まれている変数はプロパティの一部が省略されていると覚えておきましょう。

メソッド定義

前節の例で出てきたgetPerson()の返り値にメソッドを含めるようにしてみましょう。

function getPerson() {
  let name = 'John';
  let age = 25;

  return { 
    name, 
    age,
    greet: function () {
      return `Hello, ${this.name}`;
    }
  };
}

console.log(getPerson().greet()); // ==> Hello, John

これはES2015では以下の様にfunctionを省略して書けます。

function getPerson() {
  let name = 'John';
  let age = 25;

  return { 
    name, 
    age,
    greet() {
      return `Hello, ${this.name}`;
    }
  };
}

console.log(getPerson().greet()); // ==> Hello, John

プロパティのキー名変数指定

今まではオブジェクトのキー値に変数を指定したい場合は以下のようにする必要がありました。

var myKey = "this_is_key"
var obj = {};
obj[myKey] = myValue; // ==> {"this_is_key": "this_is_value"}

以下のようにするのはダメでした。

var myValue = "this_is_value";
var myKey = "this_is_key";
var obj = {myKey: myValue}; // ==> {myKey: "this_is_value"}

ES2015では以下のように書けます。

var myValue = "this_is_value";
var myKey = "this_is_key";
var obj = {[myKey]: myValue}; // ==> {"this_is_key": "this_is_value"}

export/import

指定したファイル(またはモジュール)から関数、オブジェクト、プリミティブに至るまでエクスポートします。
エクスポートした内容は他のファイルからインポートして使えるようになります。
詳しくはexport - MDNimport - MDNをご覧ください。
ブラウザで利用するにはwebpack等モジュールローダーが必要です。
ポイントだけ書いておきます。

名前付きエクスポートをインポート

// module "my-module.js"
export function show(message) {
  console.log(message);
}
const myName = 'John';
export { myName };

show関数とmyName変数がエクスポートされています。
exportは関数宣言時に書くことも出来ますし、特定の変数も{}で囲んでexportすることも出来ます。
これらをインポートして他のファイルで使うには以下のように書きます。

import { show, myName } from 'my-module';
show('hey');              // ==> hey
console.log(myName);      // ==> John

デフォルトエクスポートをインポート

// module "my-module.js"
export default function show(message) {
  console.log(message);
}

export defaultキーワードを付けると、デフォルトエクスポートになります。
これをインポートするには、以下のように書けます。

import cube from 'my-module';
show('hey');              // ==> hey

importの後ろの変数を{}で囲む必要がなくなりました。

分割代入

let a, b;
[a, b] = [1, 2]
console.log(a) // ==> 1
console.log(b) // ==> 2

そんなに見かけません。

スプレッドオペレータ

こちらはよく見かける割にとっつきにくいのでしっかりと抑えましょう。
以下の様な関数を考えてみます。

function sum(x, y, z) {
  return x + y + z;
}
console.log(sum(1,2,3)); // ==> 6

sumの引数を可変長に対応させるには、argumentsオブジェクトを活用したりと少し手間でした。
ES2015では以下のように書けます。

function sum(...numbers) {
  // console.log(numbers);
}
sum(1,2,3);

この...がスプレッドオペレータです。
2行目をコメントインすると、numbersには[1, 2, 3]が格納されていることがわかります。
可変長の引数が配列で格納されて渡ってくるので、Array.prototype.reduce()で加算処理を実装してあげれば良いです。

function sum(...numbers) {
  return numbers.reduce(function(prev, current) {
    return prev + current;
  });
}
console.log(sum(1,2,3));

アロー関数を使えばもっと短く書けますね。

function sum(...numbers) {
  return numbers.reduce((prev, current) => prev + current);
}
console.log(sum(1,2,3));

/* 以下のように1行でも書けます
const sum = (...numbers) => numbers.reduce((prev, current) => prev + current);
*/

スプレッドオペレータは「配列」と「引数一覧」を変換してくれるような役割です。
上の例では引数一覧を配列として渡してあげましたが、
以下のように配列を引数一覧として渡してあげることも可能です。

function sum(x, y, z) {
  return x + y + z;
}
let nums = [1,2,3];
console.log(sum(...nums)); // ==> 6

注意点があります。
可変長引数に続けて別の引数も渡してあげたい場合、以下のようにするのはNGです。

function sum(...numbers, foo) {
  console.log(foo);
  return numbers.reduce((prev, current) => prev + current);
}
console.log(sum(1,2,3,'value'));

上記コードはエラーになります。
別の引数も渡したい場合は、スプレッドオペレータより前に置く必要があります。

function sum(foo, ...numbers) {
  console.log(foo);
  return numbers.reduce((prev, current) => prev + current);
}
console.log(sum('value',1,2,3)); // ==> 6

デフォルト引数

PHP等他の言語と同様に、デフォルト引数を設定できるようになりました。

function multiply(a, b = 1) {
  return a*b;
}
multiply(5); // ==> 5

シンボル(symbol)

symbolはユニークで不変なデータ型で、オブジェクトのプロパティ識別子として使われたりします。
ライブラリの開発者でなければ直接触る頻度は低いかもしれません。
以下のようにしてsymbolを生成します。

const sym1 = Symbol();

引数にそのsymbolの説明を付けることも出来ます。

const sym2 = Symbol("foo");

同じfooという引数で新たにsymbolを作成してみます。

const sym3 = Symbol("foo");

たとえ引数が同じでも、毎回新しいsymbolを生成します。

console.log(sym2 === sym3); // ==> false

主な用途はオブジェクトのキーです。

const sym1 = Symbol();
let obj = {};
// オブジェクトのキーとして使える
obj[sym1] = 'hoge';

symbolは一度作ったらそれ自身とでしか等しくなりません。

const sym1 = Symbol();
const sym2 = Symbol("foo");
const sym3 = Symbol("foo");

let obj = {};
obj[sym1] = 'hoge';

console.log(obj[sym1]);  // ==> "hoge"
console.log(obj[sym2]);  // ==> undefined
console.log(obj[sym3]);  // ==> undefined
console.log(obj['foo']); // ==> undefined
console.log(obj.foo);    // ==> undefined

更に特徴として、シンボルをキーとしたオブジェクトに保存された値は列挙不能になります。
まずは普通に文字列をキーとしたオブジェクトをfor ... inで列挙してみます。

let obj = {
  'key1': 'value1',
  'key2': 'value2',
  'key3': 'value3'
}
for (var prop in obj) {
  console.log(obj[prop]);
}
// "value1"
// "value2"
// "value3"

全て列挙できます。
次にシンボルをキーとしたオブジェクトを列挙してみます。

const sym1 = Symbol();
const sym2 = Symbol("foo");
const sym3 = Symbol("foo");

let obj = {
  [sym1]: 'v1',
  [sym2]: 'v2',
  [sym3]: 'v3'
};

// console.log(obj[sym1]); // これはOK

for (var prop in obj) {
  console.log(obj[prop]);
}

何も出力されません。
これらの特徴から、オブジェクトに特殊なメソッドを追加したり、ArrayやDate等ビルトインクラスのprototypeを安全に拡張することが出来ます。(ライブラリの開発でもなければ弄る必要は無いかと思いますが)

また、ES2015以前では開発者に公開されていなかった言語内部の振る舞いを表すビルトインsymbolもあります。
一覧はこちらに掲載されています。
そのうちの一つにSymbol.iteratorがありますが、これは次に説明するイテレータで登場します。

Iterator

イテレータは、以下のようなオブジェクトのことです。

const iterator = {
  next() {
    return { value: 3, done: false }
  }
};

{value: 値, done: 真偽値}を返すnext()メソッドを持つオブジェクトです。
doneプロパティは、イテレータから値を順番に取り出し終えたかどうかを表します。
このイテレータを持つオブジェクトのことをイテラブルな(反復可能な)オブジェクトと言います。
具体的には以下の様なオブジェクトのことです。

const iterator = {
  next() {
    return { value: 3, done: false }
  }
};

// イテラブルオブジェクト
const iterableObject = {};
iterableObject[Symbol.iterator] = () => iterator;

8行目にあるように、イテラブルなオブジェクトはビルトインsymbolの一つであるSymbol.iteratorキーをプロパティとして持ちます。
そして[Symbol.iterator]()メソッドを実行すると、イテレータを返します。

さて、イテラブルなオブジェクトとは反復可能なオブジェクトのことです。
1から10までの値を出力できるようなイテラブルオブジェクトを作ってみます。

const iterableObject = {};
iterableObject[Symbol.iterator] = () => {
  const iterator = {};
  let count = 1;
  iterator.next = () => {
    let iteratorResult;
    if (count <= 10) {
      iteratorResult = {value: count++, done: false};
    } else {
      iteratorResult = {value: undefined, done: true};
    }
    return iteratorResult;
  };
  return iterator;
};

// イテラブルなオブジェクトからイテレータを取得する
const iterator = iterableObject[Symbol.iterator]();
let iteratorResult;
while(true){
  iteratorResult = iterator.next();
  // 完了したらループを抜ける
  if(iteratorResult.done) break;
  console.log(iteratorResult.value);
}

whileループでも書けますが、ES2015からはイテレータから値を取り出すのに便利なfor ... of構文があります。

for(let v of iterableObject) {
  console.log(v);
}

実はArrayStringはイテラブルなオブジェクです。つまり、for ... of構文が使えます。
Iteratorの解説に関しては
JavaScript の イテレータ を極める! - Qiita
の記事が非常に参考になります。(上記コードの一部はES2015式に書き直したものです)

次に紹介するGeneratorもイテラブルなオブジェクトです。

Generator

Generatorは処理を途中で中断したり、後から再開することもできる関数です。
中断・再開のためにiteratorオブジェクトが返されます。

function* greet(){
  console.log(`You called 'next()'`);
}

let greeter = greet();
let next = greeter.next(); // ==> You called 'next()'
console.log(next);         // ==> {value: undefined, done: true}

function *とすることでGenerator関数を定義できます。
Generator関数の中身はnextプロパティによって実行されます。返り値はvaluedoneがあることから分かるように、イテレータオブジェクトです。
Generatorはyiledまで処理が実行されると、一旦そこでストップしイテレータリザルトとして値が返されます。

function* greet(){
  console.log(`You called 'next()'`);
  yield "hello";
}

let greeter = greet();
let next = greeter.next(); // ==> You called 'next()'
console.log(next);         // ==> {value: "hello", done: false}
let done = greeter.next();
console.log(done);         // ==> {value: undefined, done: true}

.next()が実行されるたびにyiledまで進むイメージです。
これ以上yieldで止まること無くGenerator関数の最後のステップまで到達するとdoneがtrueになります。
以下のようなステップ関数を定義しておくと、Generator関数のテストに1ステップごとに値を確認できて便利です。

function* greet(){
  console.log('step1');
  yield 'hello';
  console.log('step2');
  yield 'world';
}

// ステップ関数
const stepper = fn => mock => fn.next(mock).value;
const step = stepper(greet());

console.log(step()); // hello
console.log(step()); // world
console.log(step()); // undefined

Generatorは実際にステップごとに動いている様子を見たほうが理解しやすいと思うので、以下のような解説動画を参照すると良いかと思います。

ES2016

  • Exponentiation(**) Operator
  • Array.prototype.includes()メソッド

が追加されました。
前者はべき乗のシンプルな書き方、後者は配列内にある要素が含まれているかどうかを調べるためのメソッドです。

ES2016+

async / await

async/awaitを使ってPromise処理をスマートに書くことが可能です。
まずはasyncから。

async function asyncFunction() { }

asyncキーワードを付けることでasync関数が定義できます。
このasync関数を呼び出すとPromiseが返されます。
次にawaitです。

const result = await awaitFunction();

awaitキーワードを付けることで、非同期処理が解決するまで待って、その解決値を返すことができます。
awaitを付けるとその後ろの処理はPromiseでなくてもPromiseとしてラップされます。
asyncawaitを組み合わせて非同期処理をシンプルに書くことが出来ます。
以下のような時間のかかる関数があったとします。

function awaitFunction() {
    return new Promise((resolve, reject) =>
        setTimeout(() => resolve('Async Hello world'), 1000)
    );
}

1秒後に"Async Hello world"を出力する関数です。(Promiseを返します)
従来の方法だと以下のように呼び出せますね。

function asyncFunction() {
  awaitFunction()
  .then(value => {
    console.log(value);
    console.log('done!');
  })
  .catch(e => {
    console.error(e);
    console.error('error!');
  });
}

asyncFunction(); 
// 1秒後に 
// "Async Hello world"
// "done!"

resolverejectにすればエラーキャッチもできていることが確認できるかと思います。
async/awaitを使うと以下のように書けます。

async function asyncFunction() {
  try {
    const result = await awaitFunction();
    console.log(result);
    console.log('done!');    
  } catch (e) {
    console.log(e);
    console.log('error!');
  }
}

asyncFunction();
// 1秒後に 
// "Async Hello world"
// "done!"

Promise的記述から手続き的な記述になりましたね。
これは簡単な例なのでまだ見やすいですが、複雑なものになると従来の記法ではとたんに見通しが悪くなります。
非同期処理をコールバック地獄にならずに、同期処理のように書けるのがasync/awaitの魅力です。

ちなみにGenaratorでも同じようなことができます。

Resources

特に役立ったリンクは太字にしています。

Learn ES6 (ECMAScript 2015) - egghead.io
ES2015 Crash Course - laracasts
how to use generator in node(日本語)
記事中でも紹介したES2015やGenaratorの解説動画です。

ブラウザ対応状況
ES.nextの状況もわかります。

JavaScript の イテレータ を極める!- Qiita
JavaScript の ジェネレータ を極める! - Qiita
ややこしいイテレータ、ジェネレータをわかりやすく解説した記事です。

速習ECMAScript6: 次世代の標準JavaScriptを今すぐマスター! Kindle版
書籍がよければこれも良いです。

JavaScriptの「this」は「4種類」?? - Qiita
thisのパターンを忘れることが多いのでよく見返します。

JavaScript Promiseの本
@azu_reさんによるPromiseに関する解説です。
そもそもPromiseってなんぞ?という人からなんとなくPromiseを使ってきた人におすすめです。
最新のフロントエンドのトレンドを追いたい人はフォローしてたほうが良いです。

[WIP] JavaScriptの入門書
個人的に@azu_reさんの本をの完成を応援してます。

開眼! JavaScript ―言語仕様から学ぶJavaScriptの本質
オブジェクト指向JavaScriptの原則
ES2015以前(ES5)が不安な方はオライリーの薄い本を読めば大丈夫です。
JavaScriptの良書としてJavaScript: The Good Partsがよく挙げられますが、「今から」JSをちゃんと学ぶには不向きかなと思います。

オンライントランスパイラ - babel
本記事で登場したES*記法が、実際にbabelでどのようにトランスパイルされるのか確認できます。
class記法とかasync/awaitの例を流し込んでみるとおもしろいですよ。

jsbin
本記事のサンプルはjsbin上で動作を確認しました。

61
81
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61
81