以前社内向けに書いていた個人的メモをせっかくなので公開します。
昔ながらの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);
class
やconstructor
と書いてますが、JavaScriptにはクラスはありません。
あくまで従来の記法のシンタックスシュガーです。
他には継承でおなじみのextends
やクラス名.メソッド名
で呼び出せるstatic
メソッド的な記法もありますが、普段オブジェクト指向言語に触れている方であれば特に問題は無いでしょう。
変数宣言(let, const)
今までJavaScriptの変数宣言といえばvar
でした。
ES2015では新たにlet
、const
が追加されました。
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 - MDN、import - 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);
}
実はArray
やString
はイテラブルなオブジェクです。つまり、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
プロパティによって実行されます。返り値はvalue
とdone
があることから分かるように、イテレータオブジェクトです。
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としてラップされます。
async
とawait
を組み合わせて非同期処理をシンプルに書くことが出来ます。
以下のような時間のかかる関数があったとします。
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!"
resolve
をreject
にすればエラーキャッチもできていることが確認できるかと思います。
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上で動作を確認しました。