Web開発10年選手へ(ECMAScriptの勉強会ネタ)

Pocket

Webの開発を長らくやっていると、新しい知識を増やすことなく知っている知識だけを頼って惰性でモノづくりをしてしまいがちです。それでも何とかなっているかも知れません。そもそもサポートしないといけないブラウザに古いIEがあったりすると「新しい機能を使っていけない」になるため、新しい技術を学ぶモチベーションも上がりません。しかし、ブラウザの性能も日々向上しており、JavaScriptにも知らぬ間に新しい仕様が追加されています。クラウドサービスでNode.jsでの実装を指定してある場合もあります。どんな分野でも、たまに知識の底上げをしないといつの間にか時代に取り残されてしまいます。

という事で、ECMAScriptの勉強会を実施しようかな、と思い、その時に使うネタを公開しようと思った次第です。

ECMAScript(エクマスクリプト)とは?

JavaScriptの標準のこと。
Ecma Internationalという情報通信システムの分野における国際的な標準化団体で標準化手続きが進められている。

バージョンについて(ECMAScript6?ES2015?)

ECMAScriptは、もともとバージョン1、2、3…といった感じでバージョンアップしていた。
ECMAScript6を策定している途中で、バージョンの付け方を西暦にして1年毎に小出しにしようと決まった。
だから、ECMAScript2015はECMAScript6とも呼ばれる。ES2015と短縮して呼ばれることもある。

バージョン 呼ばれ方(ググるキーワード) 公開日
1 ECMAScript1 1997年6月
2 ECMAScript2 1998年6月
3 ECMAScript3 1999年12月
4 ECMAScript4 放棄
5 ECMAScript5 = ES5 2009年12月
5.1 ECMAScript5.1 2011年6月
2015 ECMAScript6 = ECMAScript2015 = ES2015 2015年6月
2016 ECMAScript2016 = ES2016 2016年6月
2017 ECMAScript2017 = ES2017 策定中

標準だから使って大丈夫?

ブラウザの実装は各ベンダーに任されているので必ずしも標準に沿っているとは限らない。昔はIEとFireFoxとSafariとで動きやサポートしている機能が違っていたりしてけっこう地獄だった。最近は昔ほど方言はひどくない。

ただ、すべてのブラウザが最新の仕様をキャッチアップしているわけでもなく、また、逆に標準化前に先行して機能を実装しているブラウザなどもある。さらに、ガラケーやゲーム機などのブラウザやIEなどは進化が止まったりするので、どのJavaScriptの機能を使用するかはターゲットユーザの動作環境を考慮する必要がある。

ブラウザ毎のサポート状況は次のページが分かりやすい。
ECMAScript 6 compatibility table(http://kangax.github.io/compat-table/es6/)

以下、細かいのは略しつつ、主だったものを紹介。JavaScriptのthisとかprototypeとかコンストラクタ関数とかの認識が怪しい人は別途、勉強しましょう。

ECMAScript2015以前

use strict

"use strict";
// ↑コードの先頭
function hoge() {
    "use strict";
    // ↑関数の先頭
    return 0;
}

構文チェックが厳格になる。
ES5以降の仕様に従ってコーディングするなら書いた方が良い。

getter、setter

var person = (function() {
  var _age = 0;
  return {
    get age() {
      return _age;
    },
    set age(value) {
      _age = value;
    }
  }
}());

文字でプロパティにアクセス

var obj = { hoge: "ほげ"};
console.log(obj.hoge);
console.log(obj["hoge"]);

Objectに追加

// Object.defineProperty
var obj = {};
Object.defineProperty(obj, "age", {value : 37,
                               writable : true,
                               enumerable : true,
                               configurable : true}); // {age: 37}
// Object.defineProperties
var person = {};
Object.defineProperties(person, {
                            age: {value : 37,
                               writable : true,
                               enumerable : true,
                               configurable : true}, 
                            name: {value : "ken",
                               writable : true,
                               enumerable : true,
                               configurable : true}}); // {age: 37, name: "ken"}
// Object.getPrototypeOf
Object.getPrototypeOf(new Object()) === Object.prototype; // true

// Object.create
Object.create(Object.prototype, {
  age: {value : 37,
     writable : true,
     enumerable : true,
     configurable : true}, 
  name: {value : "ken",
     writable : true,
     enumerable : true,
     configurable : true}
}); // {age: 37, name: "ken"}

var o = Object.create(null);
o.prop = 1;
console.out(o.prop); // 1
o.toString();
// ↑ Uncaught TypeError: o.toString is not a function
/*
 new Hoge()はコンストラクタ関数の呼び出し。
 Object.createは第一引数のプロトタイプを引き継いだobjectを作る。第二引数でプロパティも追加できる。
 Object.createの第一引数にnullを渡すとprototypeを持っていないobjectが出来るので、toStringすら呼べない。
*/

// Object.keys, Object.getOwnPropertyNames
Object.keys({key1: 123, key2: "abc"}); // ["key1", "key2"]
Object.keys(["apple", "orange", "banana"]); // ["0", "1", "2"]
Object.getOwnPropertyNames({key1: 123, key2: "abc"}); // ["key1", "key2"]
Object.getOwnPropertyNames(["apple", "orange", "banana"]); // ["0", "1", "2", "length"]
Object.keys(Object); // []
Object.getOwnPropertyNames(Object.prototype); // ["__defineGetter__", "__defineSetter__", "hasOwnProperty", "__lookupGetter__", "__lookupSetter__", "propertyIsEnumerable", "toString", "valueOf", "__proto__", "constructor", "toLocaleString", "isPrototypeOf"]

// Object.seal, Object.isSealed
/* sealするとプロパティを消したり増やしたりできなくなる。値の変更は可能 */
var obj = {
  a: "x",
  b: "y"
};
Object.isSealed(obj); // false
Object.seal(obj);
Object.isSealed(obj); // true
obj.a = "f";
obj.c = "z";
delete obj.b; // false
Object.getOwnPropertyNames(obj); // ["a", "b"]
obj.a // "f"

// Object.freeze, Object.isFrozen
var obj = {
  a: "x",
  b: "y"
};
Object.isFrozen(obj); // false
Object.freeze(obj);
Object.isFrozen(obj); // true
obj.a = "f";
obj.c = "z";
delete obj.b; // false
Object.getOwnPropertyNames(obj); // ["a", "b"]
obj.a // "x"
/*
 ちなみにstrictモードだと操作できないプロパティを操作しようとしたところで例外が発生する。
 Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>'
 Uncaught TypeError: Cannot add property c, object is not extensible
 Uncaught TypeError: Cannot delete property 'b' of #<Object>
*/

// Object.preventExtensions, Object.isExtensible
var obj = {};
Object.isExtensible(obj); // true
Object.defineProperty(obj, "one", { value: 1 }); // {one: 1}
Object.preventExtensions(obj);
Object.isExtensible(obj); // false
Object.defineProperty(obj, "two", { value: 2 });
// ↑ Uncaught TypeError: Cannot define property two, object is not extensible

Arrayに追加

// Array.isArray(obj)
Array.isArray([1]); // true
Array.isArray(new Array()); // true
Array.isArray(Array.of(1, 2, 3)); // true

// Array.prototype.indexOf
// Array.prototype.lastIndexOf
[1, 2, 3].indexOf(2); // 1
[1, "hoge", "piyo"].indexOf("piyo"); // 2
[1, 1, 2, 2, 3].indexOf(2); // 2
[1, 1, 2, 2, 3].lastIndexOf(2); // 3

// Array.prototype.every
// Array.prototype.some
var member = [{name: "tom", age: 18}, {name: "ken", age: 21}, {name: "jessie", age: 28}];
function isAdult(v) { return v.age >= 20; }
member.some(isAdult); // true
member.every(isAdult); // false

// Array.prototype.forEach
member.forEach(function(item) { console.log(item.name); } );
//tom
//ken
//jessie

// Array.prototype.map
member.map(function(item) { return "name: " + item.name + ", age: " + item.age; } );
//["name: tom, age: 18", "name: ken, age: 21", "name: jessie, age: 28"]

// Array.prototype.reduce
// Array.prototype.reduceRight
member.reduce(function(accumulator, item) { return accumulator + item.age; }, 0); //67

member.reduce(function(accumulator, item) { return accumulator.concat(item.name); }, []);
//["tom", "ken", "jessie"]
member.reduceRight(function(accumulator, item) { return accumulator.concat(item.name); }, []);
//["jessie", "ken", "tom"]

// Array.prototype.filter
member.filter(isAdult);
//[{name: "ken", age: 21}, {name: "jessie", age: 28}]

member.filter(function(item) { return !isAdult(item); } ); // 大人じゃない、はこうするしかない?
//[{name: "tom", age: 18}]

JSON

var jsonstr = JSON.stringify({hoge:"piyo"}); // "{"hoge":"piyo"}"
var obj = JSON.parse(jsonstr); // {hoge:"piyo"}

Dateに追加

var now = Date.now(); // new Date().getTime();
var isoString = new Date().toISOString(); // "2017-09-12T16:46:24.647Z"
var json = new Date().toJSON(); // "2017-09-12T16:48:30.553Z"

Stringに追加

var trim = "   aaa   ".trim(); // "aaa"

Functionに追加

// fun.bind(thisArg[, arg1[, arg2[, ...]]]);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

function f() {
  console.log(this);
}
f(); // Window
f.bind(new Array())(); // []

function f() {
  "use strict";
  console.log(this);
}
f(); // undefined

undefined,NaN,Infinityが不変に

ECMAScript2015

末尾再帰の最適化
仕様としては盛り込まれているけどSafari以外は未だに実装されていない。

"use strict";
(function f(n){
  if (n <= 0) {
    return  "foo";
  }
  return f(n - 1);
}(1e6)) === "foo";
// ↑ Uncaught RangeError: Maximum call stack size exceeded
//     at f (:2:12)
//     at f (:6:10)
//     at f (:6:10)
//     at f (:6:10)
//     at f (:6:10)
//     at f (:6:10)
//     at f (:6:10)
//     at f (:6:10)
//     at f (:6:10)
//     at f (:6:10)

デフォルト引数

function f(a, b=1000) {
    return a + b;
}
f(1, 2); // 3
f(1); // 1001

可変長引数と繰り返しの展開

function a(...a) {
    return a.length;
}
a(1, 2, 3); // 3

function b(a, b, c) {
    return a + b + c;
}
b(...[1, 2, 3]); // 6

オブジェクトリテラルの拡張

var a = 'foo', b = 42, c = {};
{a, b, c}; // {a: "foo", b: 42, c: {…}}

var o = { method(arg) { return arg + 1 } };
o.method(5); // 6

var prop = "p";
var o = { [prop] : 8 };
// o.prpo; undefined
o.p; // 8

テンプレートリテラル

var s = "sunday";
var t = "tuesday";
var w = `${s}\nmonday\n${t}`;
console.log(w);
// sunday
// monday
// tuesday

function tag(strings, ...values) {
  console.log(strings[0]);
  console.log(strings[1]);
  console.log(values[0]);
  console.log(values[1]);
  return "hogehoge";
}
tag`${s}\nmonday\n${t}`;

function tag2(strings, ...values) {
  console.log(strings[0]); // "saturday "
  console.log(strings[1]); // " monday "
  console.log(strings[2]); // " wednesday"
  console.log(values[0]); // "sunday"
  console.log(values[1]); // "tuesday"
  return "hogehoge";
}
tag2`saturday ${s} monday ${t} wednesday`; // "hogehoge"

for ofループ
iterableオブジェクトに対してループする。

let iterable = [10, 20, 30];

for (let value of iterable) {
  console.log(value);
}
// 10
// 20
// 30

for (const str of "こんにちは") {
    console.log(str);
}
// こ
// ん
// に
// ち
// は

バイナリ、8進、テンプレートのリテラル

var octal = 0o10;
var bin = 0b10;

var name = "tom";
console.log(`my name is ${name}.`); // "my name is tom."

分割代入構文

var x = [1, 2, 3, 4, 5];
var [a, b, c] = x;
console.log(`${a} ${b} ${c}`); // "1 2 3"

var a, b;
[a, b] = [1, 2];
console.log(`${a} ${b}`); // 1 2

var e, f;
[e=5, f=7] = [1];
console.log(`${e} ${f}`); // 1 7

function f() {
  return [3, 4];
}
var a, b; 
[a, b] = f();
console.log(`${a} ${b}`); // 3 4
var c;
[, c] = f();
console.log(c); // 4

var o = {p: 42, q: true};
var {p, q} = o;
console.log(`${p} ${q}`); // 42 true

new.target

function F() { console.log(new.target === this.constructor); }
new F(); // true
F(); // false

const,let

const C = "定数";
C = "a"; // ← Uncaught TypeError: Assignment to constant variable.

(function() {
  var v = 1;
  let l = 2;
  {
    var v = 10;
    let l = 20;
    console.log(v); // 10
    console.log(l); // 20
  }
  console.log(v); // 10
  console.log(l); // 2
})();

for (let i = 1; i <= 5; i++) {
  // 略
}

アロー関数

var func = (x, y) => { return x + y; };
console.log(func(11, 22)); // 33

// アロー関数は var func = function(x, y) { return x + y; } と違って、
// this, arguments, super, new.target を束縛しない。

Class
あれ?いつのまに使えるようになったんだ?AltJS要らないかも。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  get info() {
    return this.speakSelfIntroduction();
  }

  static totalAge(a, b) {
    return a.age + b.age;
  }

  speakSelfIntroduction() {
    return `My name is ${this.name}. ${this.age} years old`;
  }
}

const tom = new Person("tom", 5);
const ken = new Person("ken", 10);
console.log(tom.name); // "tom"
console.log(tom.info); // "My name is tom. 5 years old"
console.log(ken.speakSelfIntroduction()); // "My name is ken. 10 years old"
console.log(Person.totalAge(tom, ken)); // 15

class Taro extends Person {
  constructor(age) {
    super("taro", age);
  }
}
const taro = new Taro(53);
console.log(taro.info); // "My name is taro. 53 years old"

ジェネレータ

function* gen(i) {
    yield i++;
    yield i++;
    yield i++;
    yield i++;
    yield i++;
}
const g = gen(10);
console.log(g.next()); // {value: 10, done: false}
console.log(g.next()); // {value: 11, done: false}
console.log(g.next()); // {value: 12, done: false}
console.log(g.next()); // {value: 13, done: false}
console.log(g.next()); // {value: 14, done: false}
console.log(g.next()); // {value: undefined, done: true}
console.log(g.next()); // {value: undefined, done: true}

function* gen2(i) {
    yield* gen(i);
    yield i--;
    yield i--;
}
const g2 = gen2(10);
console.log(g2.next()); // {value: 10, done: false}
console.log(g2.next()); // {value: 11, done: false}
console.log(g2.next()); // {value: 12, done: false}
console.log(g2.next()); // {value: 13, done: false}
console.log(g2.next()); // {value: 14, done: false}
console.log(g2.next()); // {value: 10, done: false}
console.log(g2.next()); // {value: 9, done: false}
console.log(g2.next()); // {value: undefined, done: true}
console.log(g2.next()); // {value: undefined, done: true}

Proxy, Reflect

var handler = {
    get: function(target, name){
        return name === "name" && target[name] === "taro" ?
            "nice to meet you taro." :
            target[name];
    }
};

var p = new Proxy({}, handler);
p.name = "hanako" 
console.log(p.name); // "hanako"
p.name = "taro"
console.log(p.name); // "nice to meet you taro."

var p = new Proxy({}, {
  set: function(target, prop, value, receiver) {
    Reflect.set(target, prop, value, receiver);
    console.log('property set: ' + prop + ' = ' + value)
    return true
  }
});
p.a = 10; // "property set: a = 10"
console.log(handler.a); // 10

// traps
// handler.getPrototypeOf()
// handler.setPrototypeOf()
// handler.isExtensible()
// handler.preventExtensions()
// handler.getOwnPropertyDescriptor()
// handler.defineProperty()
// handler.has()
// handler.get()
// handler.set()
// handler.deleteProperty()
// handler.ownKeys()
// handler.apply()
// handler.construct()

Promise
非同期処理のコールバック地獄から解放されたい時に使う。

let wait = function(millisec) {
  return new Promise((resolve, reject) => {
    setTimeout(function(){
      resolve("wait");
    }, millisec);
  }
});

wait(500).then((successMessage) => {
  console.log(successMessage); // "wait"
});

// Promise.all(iterable)
// Promise.race(iterable)
// Promise.reject(reason)
// Promise.resolve(value)
// Promise.prototype.catch(onRejected)
// Promise.prototype.then(onFulfilled, onRejected)

Symbol
for ofループを実現するために制御の都合で実装されたっぽい。
あんまり開発者的には使い道は無いような。

// new Symbol(); ← やると Uncaught TypeError: Symbol is not a constructor
console.log(Symbol("a") === Symbol("a")); // false
console.log(Symbol.for("a") === Symbol.for("a")); // true
var obj = {};
var sym = Symbol("a");
obj["a"] = "";
obj[sym] = "";
console.log(obj.hasOwnProperty("a")); // true
console.log(obj.hasOwnProperty(sym)); // true
Object.getOwnPropertySymbols(obj); // [Symbol(a)]
for (var i in obj) { console.log(i); }; // "a"

// Symbol.iterator
// Symbol.match
// Symbol.replace
// Symbol.search
// Symbol.split
// Symbol.hasInstance
// Symbol.isConcatSpreadable
// Symbol.unscopables
// Symbol.species
// Symbol.toPrimitive
// Symbol.toStringTag

Objectに追加

// Object.assign
var o1 = { a: 1, b: 1, c: 1 };
var o2 = { b: 2, c: 2 };
var o3 = { c: 3 };
var o4 = { d: 4 };
var obj = Object.assign({}, o1, o2, o3, o4);
console.log(obj); // {a: 1, b: 2, c: 3, d: 4}
var o0 = { x: 5, y: 6};
var obj2 = Object.assign(o0, o1, o2, o3, o4);
console.log(obj2); // {a: 1, b: 2, c: 3, d: 4, x: 5, y: 6}

// Object.is
Object.is('foo', 'foo'); // true
'foo' === 'foo'; // true
Object.is([], []); // false
[] === []; // false
Object.is(null, null); // true
null === null; // true
Object.is(0, -0); // false
0 === -0; // true
var a = [];
var b = a;
Object.is(a, b); // true
a === b; // true

// Object.getOwnPropertySymbols
// → Symbolで説明済み

// Object.setPrototypeOf
var a = new Object();
a.a = "a";
a.hasOwnProperty("a"); // true
Object.setPrototypeOf(a, null);
a.hasOwnProperty("a");
// ↑ Uncaught TypeError: a.hasOwnProperty is not a function
Object.setPrototypeOf(a, Object.prototype);
a.hasOwnProperty("a"); // true

Stringに追加

// String.raw
var s1 = `こんにちは \n さようなら`;
var s2 = String.raw`こんにちは \n さようなら`;
console.log(s1);
// こんにちは 
//  さようなら
console.log(s2);
// こんにちは \n さようなら

// String.fromCodePoint
String.fromCodePoint(42); // "*"
String.fromCodePoint(42, 43, 44); // "*+,"

// String.prototype.codePointAt 
// String.prototype.normalize
// String.prototype.repeat
// String.prototype.startsWith
// String.prototype.endsWith
// String.prototype.includes

Arrayに追加

// Array.from
Array.from(document.querySelectorAll("input")).forEach(e => console.log(e.value));
// ↑ inputのvalueを全部表示

// Array.of
Array.of(1, 2, 3); // [1, 2, 3]
Array(1, 2, 3); // [1, 2, 3]
Array.of(1); // [1]
Array(1); // [undefined]
Array.of(1.5); // [1.5]
Array(1.5); // Uncaught RangeError: Invalid array length

// Array.prototype.copyWithin
// Array.prototype.find
// Array.prototype.findIndex
// Array.prototype.fill
// Array.prototype.keys
// Array.prototype.values
// Array.prototype.entries

Numberに追加

// Number.isFinite
// Number.isInteger
// Number.isSafeInteger
// Number.isNaN
// Number.parseFloat
// Number.parseInt
// Number.EPSILON // 最小の数値
// Number.MIN_SAFE_INTEGER
// Number.MAX_SAFE_INTEGER

Mathに追加

// Math.clz32
// Math.imul
// Math.sign
// Math.log10
// Math.log2
// Math.log1p
// Math.expm1
// Math.cosh
// Math.sinh
// Math.tanh
// Math.acosh
// Math.asinh
// Math.atanh
// Math.trunc
// Math.fround
// Math.cbrt
// Math.hypot

Map, Set, WeakMap, WeakSet
名前の通りなので省略。

RegExpにyとuのフラグが追加された

TypedArray
Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();

ECMAScript2016

べき乗(**)

2 ** 3 === Math.pow(2, 3); // true
(-2) ** 3 === Math.pow(-2, 3); // true
(-2) ** 3; // -8
-2 ** 3;
// ↑ Uncaught SyntaxError: Unexpected token **

Arrayに追加

// Array.prototype.includes
[1, 2, 3].includes(2); // true
["a", "b", "c"].includes("b"); // true
["a", "b", "c"].includes(2); // false

ECMAScript2017

Objectに追加

// Object.entries
var obj = {a: 5, b: 7, c: 9};
console.log(Object.entries(obj));
// [Array(2), Array(2), Array(2)]
// 0: ["a", 5]
// 1: ["b", 7]
// 2: ["c", 9]
// length:3
for (var [key, value] of Object.entries(obj)) {
    console.log(key + ' ' + value);
}
// "a 5"
// "b 7"
// "c 9"
Object.entries(obj).forEach(([key, value]) => {
    console.log(key + ' ' + value);     
});
// "a 5"
// "b 7"
// "c 9"

// Object.values
console.log(Object.values(obj));
// [5, 7, 9]

// Object.getOwnPropertyDescriptors
console.log(Object.getOwnPropertyDescriptors(obj));
// {a: {…}, b: {…}, c: {…}}
// a:{value: 5, writable: true, enumerable: true, configurable: true}
// b:{value: 7, writable: true, enumerable: true, configurable: true}
// c:{value: 9, writable: true, enumerable: true, configurable: true}
// __proto__:Object

// shallow cloneできる。
Object.create(
  Object.getPrototypeOf(someobj), 
  Object.getOwnPropertyDescriptors(someobj) 
);

Stringに追加

// String.prototype.padStart, String.prototype.padEnd
"abc".padStart(8, "0"); // "00000abc"
"abc".padEnd(8, "0"); // "abc00000"

Objectリテラル、Arrayリテラル、関数の引数の末尾のカンマ無視

var obj = { a: 1, b: "a",};
var arr = [1,2,3,];
function f(p,) {}
(p,) => {};
Math.max(10, 20,);
Math.max(10, 20,,,,,);
// ↑ Uncaught SyntaxError: Unexpected token ,

async function

let wait = function(millisec) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(millisec);
    }, millisec);
  })
};

async function call() {
  var a = await wait(2000);
  var b = await wait(3000);
  return a + b;
}

call().then(v => { console.log(v); });
// 5000
// ↑ 実行してから5000ms後に表示される

arguments.caller廃止

その他

プリミティブデータ型のラッパーオブジェクトの生成はECMAScript6ではサポートされないらしい。new Booleanやnew String、new Numberが出来るのは歴史的な理由であって出来ないのが正式になる。new String(“aaa”) === “aaa”がfalseだったのでビックリした時があったけど(まあ、落ち着いて考えればあたりまえなんだけど)、ビックリしなくて済むようになるようだ。

あと、オブジェクトが持っている関数の一覧が知りたかったらコンソールでObject.getOwnPropertyNamesを呼び出すといいと思う。

Object.getOwnPropertyNames(Array);
// ["length", "name", "arguments", "caller", "prototype", "isArray", "from", "of"]

Object.getOwnPropertyNames(Array.prototype);
// ["length", "constructor", "toString", "toLocaleString", "join", "pop", "push", "reverse", "shift", "unshift", "slice", "splice", "sort", "indexOf", "lastIndexOf", "copyWithin", "find", "findIndex", "fill", "includes", "entries", "keys", "concat", "forEach", "filter", "map", "every", "some", "reduce", "reduceRight"]
Pocket

コメントを残す

メールアドレスが公開されることはありません。