2020.12.04
ざっくり ECMAScript 2015
ざっくりと ES2015 のおさらいをしたい人向け。オカタい説明は mdn さんあたりにおまかせするとして、ゆるっとざっくり気楽にサクッとコードベースでおさらい。ES2015 は Promise や クラス、let や const、アロー関数やイテレーターやジェネレーターなど、多くの機能が追加されたよ
環境構築
- MacOS High Sierra
- ふる〜い MBP 使ってます
- nvm, ndenv, nodebrew のどれでもいいので node が動く環境を用意
- VSCode 使ってます。拡張はお好みで。
- node.js のバージョンは v14.7.0
- 適当なディレクトリを掘って、index.js を用意しています (ファイル名はお好みで)
- 動作確認は $ node index.js って叩いてます
最小限で動かすだけならこれだけでおっけいです。ここらへんの準備も面倒ならば codepen だの JSFiddle だの REPL だのを使ってもおk
es2015
es2015 は以下のような機能が追加されている(参考 Wikipedia ECMAScript)
クラス、モジュール、イテレータ、for/ofループ、Pythonスタイルのジェネレータ、アロー関数、2進数および8進数の整数リテラル、Map、Set、WeakMap、WeakSet、プロキシ、テンプレート文字列、let、const、型付き配列、デフォルト引数、Symbol、Promise、分割代入、可変長引数
この中でも主だったものを取り上げておきます。
let, const
letは再代入可能な変数宣言に使用、constは再代入不可能な変数宣言に使うよ
1 2 3 4 |
let greet = 'Hello' greet = 'Hey' console.log(greet) |
1 2 3 4 |
const greet = 'Hello' greet = 'Hey' // TypeError: Assignment to constant variable. console.log(greet) |
Class
classのコンストラクタは constructor() 呼んでインスタンスの初期化。
プロパティの初期化、インスタンスメソッド、ゲッター、セッター、スタティックメソッド、スタティックプロパティ、継承() あたりのサンプルも載せておきます(サンプルコードの質の低さにはつっこまない約束)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class Vehicle { constructor(plateNumber){ this._plateNumber = plateNumber } get plateNumber(){ return this._plateNumber } } class PassengerCar extends Vehicle{ static commonCarNaviMapData = 'MapData' constructor(plateNumber, nickname){ super(plateNumber) this._nickname = nickname } get nickname(){ return this._nickname } set nickname(nickname){ this._nickname = nickname } static getGPSdata(){ return 'GPSdata' } } const myCar = new PassengerCar(1234, 'MyBenz') console.log(myCar.plateNumber) // 1234 console.log(myCar.nickname) MyBenz myCar.nickname = 'HerBenz' console.log(myCar.nickname) // HerBenz console.log(PassengerCar.commonCarNaviMapData) // MapData console.log(PassengerCar.getGPSdata()) // GPSdata |
Promise
Promise は以下の大きな3つの前提を把握しておくと理解が早いです
- Promise を使うことによって処理を任意のタイミングで実行するようコントロールできる
- Promise は new を使ってインスタンスを作る。引数には excutor と呼ばれる関数を渡す
- executor 関数は、Promise の実装から resolve関数をreject関数を受け取る
- Promise は「Pending (保留中)」「Fulfilled (resolve 成就)」「Rejected (reject 却下)」といった状態を持つ
- Pending は Fulfilled でも Rejected でもない状態
めっちゃわかりやすい例
以下の処理の結果は御存知の通り、setTimeout関数は非同期処理のため渡した関数の完了を待たずに次の処理に移行しちゃう。したがってconsole.logで吐き出されるのは空のオブジェクトだよ
1 2 3 4 5 |
data = {} setTimeout(()=>{ data.status = "Success" },2000) console.log(data) // {} |
Promise と then( )
Promise を使った例。then( ) は対象のPromiseが成功、及び失敗した場合のコールバック関数を受け、resolve( )、reject( )に応じたどちらかを実行。このコードは成功を期待した時のコード。
1 2 3 4 5 6 7 8 9 10 11 12 |
const data = {} const promise = new Promise((resolve, reject) => { setTimeout(()=>{ data.status = 'Success' resolve() }, 2000) }) console.log(data) // {} promise.then(()=>{ console.log(data) // { status: 'Success' } }) |
then( ) の引数に、resolve 時の処理と、reject 時の両方の処理を設定した場合の例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const data = {} const promise = new Promise((resolve, reject) => { setTimeout(()=>{ data.status = 'failed' reject('error') }, 2000) }) console.log(data) // {} promise.then( value => { console.log(data) }, value => { console.log(data) // { status: 'failed'} } ) |
catch( )
catch( ) は Promise が reject された時にのみ呼ばれるよ。thenでゴリ押さないでこっちで書いたほうがお利口さん。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const data = {} const promise = new Promise((resolve, reject) => { setTimeout(()=>{ data.status = 'Failed' reject('failed') }, 2000) }) console.log(data) promise .then( value => { console.log(data) } ) .catch( value => { console.log(data) // { status: 'Failed' } } ) |
メソッドチェーン
then( )、catch( ) はメソッドチェーンで書ける。ついでに then( )はいくつでも書くことができ、書いた順に処理されるよ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const data = {} const promise = new Promise((resolve, reject) => { setTimeout(()=>{ data.status = 'Success' resolve('success') }, 2000) }).then( value => { console.log('1つ目のthen') } ).then( value => { console.log('2つ目のthen') } ).catch( value => { console.log(promise) console.log(data) } ) |
Promise.all( )
Promise.all( ) の使い所は、いくつかの非同期処理をひとまとめにし、すべての処理の結果に応じた処理を行いたい場合に使うシーンを想像するとわかりやすいよ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
const dragonData = {} const tigerData = {} const promiseDragon = new Promise((resolve, reject) => { setTimeout(()=>{ dragonData.status = 'Success' resolve('success') }, 2000) }) .then(()=>{ console.log("dragon's task Done") }) const promiseTiger = new Promise((resolve, reject) => { setTimeout(()=>{ tigerData.status = 'Success' resolve('success') }, 1000) }) .then(()=>{ console.log("tiger's task Done") }) Promise.all([promiseDragon, promiseTiger]) .then(()=>{ console.log('complete!') }) // tiger's task Done // dragon's task Done // complete! |
一つでも reject された時の処理は catch( ) でつなげてコールバックで処理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
const dragonData = {} const tigerData = {} const promiseDragon = new Promise((resolve, reject) => { setTimeout(()=>{ dragonData.status = 'Failed' reject('failed') }, 2000) }) .then(()=>{ console.log("dragon's task Done") }) const promiseTiger = new Promise((resolve, reject) => { setTimeout(()=>{ tigerData.status = 'Success' resolve('success') }, 1000) }) .then(()=>{ console.log("tiger's task Done") }) Promise.all([promiseDragon, promiseTiger]) .then(()=>{ console.log('complete!') }) .catch(()=>{ console.log('includes failed') }) // tiger's task Done // includes failed |
ざっくりPromiseの例を載せたけど、詳しく知りたかったら MDN や JavaScript Primer あたりを掘ってみるといいかも
アロー関数
アロー関数は、=> を使った匿名関数を定義するための新しい構文だよ。とりあえずコード。
1 |
() => {} |
ってちんぷんかんぷんだと思うので説明するよ
- 引数は ( ) で囲う。但し、引数が1つの場合にのみ ( ) は省略可能
- 処理の本体は { } で囲う。但し、return 文のみが本体ならば return と { } は省略可能
- 次のコードの fn と fnc は一緒の意味になるよ
1 2 3 4 5 |
const fn = param => param * 2 const fnc = (param) => {return param * 2} console.log(fn(2)) // 4 console.log(fnc(2)) // 4 |
function で定義した関数は暗黙的に this が書き換えられたりするんだけど、アロー関数はそういった挙動がないよ。(っていうと誤解を招きそうだから補足するとアロー関数内のthisは定義時に決まるよ)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const obj = { t: this, stdFn: function(){ const fn = () => { console.log(this) } fn() }, arrowFn: () => { console.log(this) } } obj.stdFn() // { t: {}, stdFn: [Function: stdFn], arrowFn: [Function: arrowFn] } obj.arrowFn() // {} console.log(obj.t) // {} // console.log での {} は ブラウザでは Window オブジェクト |
stdFn は function で定義されているので、this が obj を参照しているよ。arrowFn は暗黙的に this を書き換えてないのでグローバルオブジェクトを参照しにいく。obj.t と一緒。(ブラウザで実行するとWindowオブジェクトを参照)
module の import / export
export で 関数やオブジェクト、クラスやプリミティブな値を持つ変数などをエクスポートできるよ。また export には 名前付きのエクスポートとデフォルトエクスポートがあるよ。
- 名前付きエクスポート:1 つのモジュールで 0 個以上エクスポートできる
- デフォルトエクスポート:1 つのモジュールで 1 つエクスポートできる
名前付きエクスポート (named export)
定義済みのもののエクスポートや定義時にエクスポートの設定ができるよ。下記のコードでは関数をオブジェクトを定義しつつエクスポートし、Class は export { MyClass } でエクスポートしてるよ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
export { MyClass } export function myFunction(){ console.log('myFunction has been called') } export const myConst = { hey: "AGEAGE" } class MyClass { constructor(instanceProperty){ this.instanceProperty = instanceProperty } getInstanceProperty(){ return this.instanceProperty } } |
デフォルトエクスポート (default export)
クラス名や関数名を指定しなくてもエクスポートできる。ただし、1 ファイルにつき一つのエクスポートになるよ。
1 2 3 4 5 6 7 8 |
export default class { constructor(name){ this.name = name } getName(){ return this.name } } |
インポート (import)
インポートには様々な構文があります ( 引用: mdn import )
1 2 3 4 5 6 7 8 9 10 11 |
import defaultExport from "module-name"; import * as name from "module-name"; import { export1 } from "module-name"; import { export1 as alias1 } from "module-name"; import { export1 , export2 } from "module-name"; import { foo , bar } from "module-name/path/to/specific/un-exported/file"; import { export1 , export2 as alias2 , [...] } from "module-name"; import defaultExport, { export1 [ , [...] ] } from "module-name"; import defaultExport, * as name from "module-name"; import "module-name"; var promise = import("module-name"); |
export の例のコードを全部インポートした例
1 2 3 4 5 6 7 8 9 10 11 12 |
import { myFunction, myConst, MyClass } from "./e.js" import Car from "./Car.js" myFunction() console.log(myConst.hey) const myClass = new MyClass('MyClassInstanceProperty') console.log(myClass.getInstanceProperty()) const myCar = new Car('Benz') console.log(myCar.getName()) |
インポート周りはゴリゴリに説明を書くと記事が肥大化するのでこんくらいにしとく。ごめんけど。
テンプレート文字列
テンプレートリテラルを使うことで、複数行の文字列を改行を気にせずに扱えるよ。文字列はバッククォート ` で囲うよ。${ } で変数展開もできるよ。
1 2 3 4 5 6 7 8 |
const greet = "Hello Template Literal" const tmpLit = ` <main id="main"> <h1>${greet}</h1> </div> ` console.log(tmpLit) |
結果
<main id=”main”>
<h1>Hello Template Literal</h1>
</div>
可変長引数 (Rest parameters)
関数の仮引数名に … (ピリオド3つ→以下、スプレッド構文) を付けると、関数に渡された値が配列として受け取ることができるよ。
1 2 3 4 5 |
const flexibleFunction = (...args) => { console.log(args) // [1, 2, 3, 4,5, 6, 7, 8] } flexibleFunction(1,2,3,4,5,6,7,8) |
他の引数も渡せるけど、その場合は Rest Parameter を仮引数の最後にする必要があるよ
1 2 3 4 5 6 |
const flexibleFunction = (param, ...args) => { console.log(args) // 1 console.log(args) // [2, 3, 4,5, 6, 7, 8] } flexibleFunction(1,2,3,4,5,6,7,8) |
デフォルト引数
仮引数に値を代入する形式で定義しておくと、関数を呼び出し時に引数が指定されていなければデフォルト値が代入されるよ。
1 2 3 4 5 6 |
const defaultParam = (def=5, ault=[1,2,3]) => { console.log(def) // 5 console.log(ault) // [1,2,3] } defaultParam() |
分割代入
変数定義時に、配列リテラルのような形式で変数名を定義することで、配列を個々の変数に一括で代入できちゃいます。
1 2 3 4 5 6 7 |
const value = [1, 2, 3] const [x, y, z] = value; console.log(x) // 1 console.log(y) // 2 console.log(z) // 3 |
オブジェクトでもいけちゃう
1 2 3 4 5 6 |
const obj = {a: 5, b:10} const {a, b} = obj console.log(a) // 5 console.log(b) // 10 |
配列展開
… (スプレッド構文) で、配列リテラルの任意の位置に他の配列を展開できるよ
1 2 3 4 5 |
const arr = [1, 2, 3] const newArr = [4, ...arr, 5, 6] console.log(newArr) |
for-of
for-of の説明の前に、一旦 for-in の説明をしちゃうけど、for-inはキーが文字列で列挙可能なプロパティ全てに対して反復処理を行うよ
1 2 3 4 5 6 7 8 9 |
const obj = {x: 1, y:2, z:{a: 5}} for(const val in obj){ console.log(obj[val]) } // 1 // 2 // { a: 5 } |
それに対して、for-ofは反復可能なオブジェクトに対して反復処理を行うよ。反復可能なオブジェクトには代表として、配列、文字列、Map、Set、DOMコレクション などがあるよ
1 2 3 4 5 6 7 8 9 |
const arr = [1, 2, 3] for(const ele of arr){ console.log(ele) } // 1 // 2 // 3 |
Map
Map は Dictionary や 連想配列 と同様に Key と Value の組み合わせでできるデータ。
- 定義時に [ [ ‘key1’, ‘value1’ ], [ ‘key2’, ‘value2’ ] ] の形式でコンスタラクタに値を渡しつつ初期化できる
- get(‘key’) で要素を参照
- set(‘key’, ‘value’) で要素を追加
- delete(‘key’) で要素を削除
- clear() で全要素を削除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const map = new Map([ ['apple', 'りんご'], ['banana', 'バナナ'], ['dragon', 'ドラゴン!!!'] ]) console.log(map) console.log(map.get('dragon')) map.set('TF', '痛風') map.delete('apple') console.log(map) map.clear() console.log(map) |
結果
Map(3) { ‘apple’ => ‘りんご’, ‘banana’ => ‘バナナ’, ‘dragon’ => ‘ドラゴン!!!’ }
ドラゴン!!!
Map(3) { ‘banana’ => ‘バナナ’, ‘dragon’ => ‘ドラゴン!!!’, ‘TF’ => ‘痛風’ }
Map(0) {}
反復処理を forEach と for-of 行うパターンを載せとくよ( 他の子→ entries, keys, values )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const map = new Map([ ['apple', 'りんご'], ['banana', 'バナナ'], ['dragon', 'ドラゴン!!!'] ]) map.forEach((value, key) => { console.log(key + ': ' + value) }) for(const [key, value] of map){ console.log(key + ': ' + value) } // apple: りんご // banana: バナナ // dragon: ドラゴン!!! // apple: りんご // banana: バナナ // dragon: ドラゴン!!! |
Set
Set は値が重複しないことを保証するコレクションだよ。同じ値が無い配列っていうイメージ。ただし、配列と違って順番に意味が無いのでインデックスを使って取り出すことはできないよ
- 定義時に [ ‘value1’, ‘value2’, ‘value3’ ] の形式でコンスタラクタに値を渡しつつ初期化できる
- add(‘value’) で要素を追加
- delete(‘value’) で要素を削除
- has(‘value’) で要素の有無を true/false で確認
- 同じ値を追加しても無視される
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const set = new Set([ 'apple', 'banana', 'DRAGON!' ]) console.log(set) console.log(set.size) set.add('おっさん') set.delete('apple') console.log(set) console.log(set.has('apple')) console.log(set.has('DRAGON!')) |
結果
Set(3) { ‘apple’, ‘banana’, ‘DRAGON!’ }
3
Set(3) { ‘banana’, ‘DRAGON!’, ‘おっさん’ }
false
true
forEach での反復処理の例(for-of でもイケる)
1 2 3 4 5 6 7 8 9 10 11 |
const set = new Set([ 'apple', 'banana', 'DRAGON!' ]) set.forEach( value => { console.log(value) }) // apple // banana // DRAGON! |
WeakMap
WeakMap は Map に似ている Key と Value 形式のコレクションだよ。ただ以下の様な特徴があるよ
- Key は オブジェクトでなければならない
- キーが ガベコレ の対象になる
- 列挙することができない
- get、set、has、delete メソッドを持つ
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let user = { name: 'Pee', age: 2 } const encryptionKey = "15ED4F33FA3D" const weakMap = new WeakMap() weakMap.set(user, encryptionKey) console.log(weakMap.get(user)) user = null // 参照がはずれ、GCの対象になる |
WeakMap はオブジェクトのインスタンスに対応する一時的な値を保管する、みたいな使い方とかが良さげ。
WeakSet
WeakSet も WeakMap と同様で、値をオブジェクトで含むことが出来、同じ参照のものを含むことができないよ。参照が外れたら次のGCで破棄。用途むずい
- add、has、delete メソッドを持つ
- 参照がはずれれば GC 対象に
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
let taro = { name: '太郎', age: 15 } let jiro = { name: '二郎', age: 14 } const weakSet = new WeakSet() weakSet.add(taro) weakSet.add(jiro) weakSet.has(taro) weakSet.delete(taro) taro = null |
Symbol
Symbol( ) を使って作ったシンボルはユニークになり、他のシンボルと同じになることが無いです。従って、ユニークな識別子を使いたい場合に便利。同一のキーでの上書きなどの防止ができちゃう。ちなオブジェクトリテラル内でシンボルを使いたい場合は [ ] ブラケットを使うよ
1 2 3 4 5 6 7 8 9 10 11 |
const X = Symbol() const Y = Symbol() const obj = { [X]: 100, [Y]: 200 } console.log(obj[X]) // 100 シンボルで参照 console.log(obj["X"]) // undefined 文字列でアクセスは出来ない console.log(typeof X) // symbol |
イテレーター
イテレーターは「繰り返し」のための機構だよ。配列などの「反復処理」を思い出しますが、配列は「反復可能なオブジェクト」で、反復可能なオブジェクトは「for-of」を使って各要素への処理ができるよね。
しかし、配列は「反復子オブジェクト」では無いよ。下の図でいう緑っぽいやつが配列だけど赤っぽいやつではないんだ。反復子オブジェクトは next( ) というメソッドを呼び出すことで、次の要素を取り出すことができるオブジェクトを指すよ。
試しに、配列に対して next( ) を呼び出すと、次のコードのようになるよ
1 2 |
const iterableObject = [1, 2, 3, 4, 5] iterableObject.next() |
実行結果
iterableObject.next()
^TypeError: iterableObject.next is not a function
これに対し、values( ) や [Symbol.iterator] によって「反復子オブジェクトに変換」した例が次のコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const iterableObject = [1, 2, 3, 4, 5] const iteratorObject = iterableObject.values(); //const iteratorObject = iterableObject[Symbol.iterator](); let current = iteratorObject.next() while(!current.done){ console.log(current.value) current = iteratorObject.next() } // 1 // 2 // 3 // 4 // 5 |
next( ) を呼び出すと2つの状態を持つオブジェクトが返ってくるよ。一つは value でその値を、もう一つは done で「全ての要素を取り出したかどうかを示す真偽値」。
次のコードはさっきのコードを少し変更して、更に次の要素を取り出してみた例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const iterableObject = [1, 2, 3, 4, 5] const iteratorObject = iterableObject.values(); //const iteratorObject = iterableObject[Symbol.iterator](); let current = iteratorObject.next() while(!current.done){ console.log(current) current = iteratorObject.next() } // { value: 1, done: false } // { value: 2, done: false } // { value: 3, done: false } // { value: 4, done: false } // { value: 5, done: false } current = iteratorObject.next() console.log(current) // { value: undefined, done: true } |
この例では while を使って一直線且つ網羅的に舐めていますが、iterator を使えば任意の順番や任意の要素数単位でのアクションを行ったりすることもできるよ。
また、イテレーターの考え方はジェネレーターでも必要になるので、ジェネレーターの理解のためにもしっかり抑えておきたいよね
ジェネレーター (関数)
ジェネレーターは通常の関数と違って、その関数を呼び出してもすぐには実行されないよ。呼び出し元には、まず反復子オブジェクト(イテレーター)が返ってきて、その後に next( )メソッドを呼ぶことで、実行が進んでいくよ。
ジェネレーターの定義には、キーワードである function の後に * アスタリスクを付け、yield を使って呼び出し側に値を供給するよ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const fn = function* (){ yield 1 yield 2 yield 3 } const it = fn() console.log(it.next()) console.log(it.next()) console.log(it.next()) // 1 // 2 // 3 |
ジェネレーターとの双方向でのメッセージング
next( ) に引数を渡すと yield の評価の結果になるよ。これにより呼び出し側から任意のタイミングで値をジェネレーター側に渡すことができるよ。また、ジェネレータ内で return を呼び出すと next( ) で返ってくるオブジェクトの done が true になるよ(値が undefined で無い可能性があることに注意)
1 2 3 4 5 6 7 8 9 10 11 |
const fn = function* (){ const name = yield 'What your name?' return `Hello! ${name}` } const it = fn() console.log(it.next()) console.log(it.next('John') // { value: 'What your name?', done: false } // { value: 'Hello! John', done: true } |