BitArts Blog

ロードバイク通勤のRubyプログラマで伊豆ダイバー。の個人的なブログ。

ブラウザでうごく! ES6+ モダンJavaScript

いまさらながらES6以降のJavaScriptがどんなものなのか、確認しました。社内勉強会用にまとめたので、一応ここにも置いておきます。

ブラウザでうごく!
ES6+ モダンJavaScript


なぜES6+を学ぶのか

ブラウザで動かしたい人

  • IE対応を考えるとES5の範囲で使っていたほうがよかったので、ES6に乗り遅れた人は多いのでは
  • 別の言語といってもいいくらい進化しているので積極的に使いたい

 

TypeScriptを学びたい人

  • 中〜大型のフロントエンドやバックエンドでTypeScriptの採用が増えている
  • TypeScriptはES6以降のJavaScriptに型付け機能を追加した言語であるから、ES6自体の知識が前提
    • TypeScriptとES6を同時に学ぼうとすると範囲が広すぎて学習効率が悪い

ES6とは何か

  • IE11にも搭載されているJavaScriptの仕様は ECMAScript 5.1 (ES5)
  • JavaScriptECMAScript 6 (ES2015) で大規模に拡張された
  • その後ES2016, ES2017, ...ES2022 と毎年バージョンアップしている
  • (太古) IEでも使えるようにBabelなどでES5に変換(トランスパイル)して利用していた
  • (昔) TypeScriptの処理系にES6->ES5のトランスパイル機能が含まれている (Babel終了)
  • (現在) IEが無くなったのでもうES5を使う必要はない
  • なんと、すでに主要なブラウザでネイティブに最新のES2022が利用可能
  • ES5との区別から「ES6」と言いがちだが、実際には最新のES2022を使ってok

 

今日お話しする機能はすべてブラウザだけで利用可能です


これだけは知っておこうES6+

  • let & const (ES6)
  • オブジェクト初期化子の略記法 (ES6)
  • アロー関数 (ES6)
  • class 構文 (ES6)
  • 関数のデフォルト引数 (ES6)
  • テンプレートリテラル (ES6)
  • 分割代入 (ES6)
  • スプレッド構文 & 残余構文 (ES6)
  • for ... of 構文 (ES6)
  • Promise (ES6)
    • async / await 構文 (ES2017)
  • ESモジュール (import & export) (ES6)
  • オプショナルチェーン演算子 (ES2020)

前提知識: JavaScriptの特徴

JavaScriptは次の2つが特徴的です

オブジェクト = 連想配列

JavaScriptのオブジェクトは「クラスの実体」のことではない
RubyでいうHash(他言語だとDictionaryとか)です

なんでも変数

関数も変数に入れられる
オブジェクト(連想配列)の要素に関数を入れればメソッドになる

obj = {
  prop1: '',
  method1: function() {
    return 'hello';
  }
};
console.log(obj.method1()); //=> "hello"

let & const


変数宣言

let と const は var に代わる変数宣言構文 (ちなみにIE11でも使える)

// var (非推奨)
var num = 1;

// let (再代入可能)
let num = 1;
num = 2; // OK

// const (再代入不可)
const num = 1;
num = 2; // NG

varとlet/const: スコープの違い

注意! var と let は同じものではない

var は関数レベルのスコープ

if (true) {
  var x = 1;
};
console.log(x); //=> "1"

let / const はブロックスコープ

if (true) {
  let x = 1;
};
console.log(x); //=> Uncaught ReferenceError: x is not defined

varとlet/const: 巻き上げの違い

宣言より前に変数を参照した時の挙動が違う

var は変数が宣言され undefined が代入される

if (true) {
  console.log(x);
  var x = 1; //=> undefined
}

let / const は変数が宣言されるが初期化されない

if (true) {
  console.log(x) //=> Uncaught ReferenceError: Cannot access "x" before initialization
  let x = 1;
}

※ 実際には変数宣言は使う前に書くべきなので、あまり意識することはないが


var/let/const の使い分け

基本は const 必要な時だけ let

  • var は使わない(直感的でなく不具合を起こしやすい仕様)
  • var から let への単純な置換は挙動の違いがあるので注意
  • 基本的には let ではなく const を使う
  • どうしても再代入が必要な時だけ let を使う
    • できるだけ変数に再代入しないロジックが望ましい
      読むのに負担になるし、バグが入りやすくなるので

オブジェクト初期化子の略記法


オブジェクト初期化子の略記法

const name = "Hello";
const object = {
    name: name
};

代入先のプロパティ名と代入する変数名が同じ場合は変数名を省略できるようになった

次のように書ける

const name = "Hello";
const object = {
    name
};

分かりにくい記法だが、頻出するので知らないと悩むことになる

Ruby 3.1 でも似た記法が導入されたが、個人的にはあまり好きじゃない)


アロー関数


3種類の関数定義

関数定義は3種類の書き方ができるようになった

// 関数宣言
function calcDouble(a) {
  return a * 2;
}

// 関数式
const calcDouble = function(a) {
  return a * 2;
};

// アロー関数
const calcDouble = (a) => {
  return a * 2;
};

従来型関数宣言の特徴: 巻き上げ

関数宣言のときだけちょっと挙動が違う

関数宣言は宣言より前でも使うことができる(だいたい他の言語でも同じ)

console.log(calcDouble(5)); //=> "10"

function calcDouble(a) {
  return a * 2;
}

関数式やアロー関数は定義より前では使えない(変数なので代入前には使えない)

console.log(calcDouble(5)); //=> Uncaught ReferenceError: calcDouble is not defined

const calcDouble = function(a) => {
  return a * 2;
};

※ 他の言語にはあまりない特徴で間違いやすいので注意


関数宣言や関数式で作った関数内の this

メソッドを一度別の変数に入れて呼び出すなど、メソッド記法以外の方法で呼び出すと this が変わってしまう

this.data = "グローバル";

const func = function() {
  console.log(this.data);
};

const f = {
  data: "オブジェクト内",
  execute: func
};

f.execute(); // => "オブジェクト内"
// ↑ レシーバである「f」がthisになる

func(); //=> "グローバル"
// ↑ レシーバがないので外側のthisと同じになる

const func2 = f.execute;
func2(); //=> "グローバル"
// 同じメソッドを実行しているのに結果が違う!!

アロー関数内の this

常に宣言時のスコープの this

this.data = "グローバル";

const func = () => {
  console.log(this.data);
};

const f = {
  data: "オブジェクト内",
  execute: func
};

f.execute(); //=> "グローバル"

func(); //=> "グローバル"

アロー関数の省略形

{} や return を省略して1行で簡潔に書ける

普通の書き方

const calcDouble = (a) => {
  return a * 2;
};

省略形

const calcDouble = (a) => a * 2;

関数宣言/関数式/アロー関数の使い分け

アロー関数をおすすめ!

  • this が扱いやすい
  • 記述が簡潔
  • 省略形でさらに簡素

class 構文


ES5時代のクラス

ES5でもクラスは作れる

function User(lastName, firstName) {
  this.lastName = lastName;
  this.firstName = firstName;
}

User.prototype.getFullName = function() {
  return this.firstName + this.lastName;
}

var u = new User("土方", "歳三");
console.log(u.getFullName()); //=> "土方歳三"

使うときは普通にクラスっぽく使えるけど、定義のほうが分かりにくい。


ES6のclass宣言

普通な感じで書ける(継承もできる)オーソドックスで扱いやすいクラス機能

class User {
  constructor(lastName, firstName) {
    this.lastName = lastName;
    this.firstName = firstName;
  }

  getFullName() {
    return this.firstName + this.lastName;
  }
}

const u = new User("土方", "歳三");
console.log(u.getFullName()); //=> "土方歳三"

詳しい機能は割愛します


class式

関数と同様に式で定義することもできる

const User = class {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  ...

あまり使う機会ははなさそう

関数と同じようにクラスも動的に変数に入れられるということは覚えておいて


関数のデフォルト引数


関数のデフォルト引数

関数の引数が省略されたときにデフォルト値を使いたい場合、従来はこうやってがんばる必要があった

function multiply(a, b) {
  if (typeof b === "undefined") {
    b = 2;
  }
  return a * b;
}

それがこう書けるようになった

function multiply(a, b = 2) {
  return a * b;
}

いままでできなかったのがおかしい(便利!)


テンプレートリテラル


テンプレートリテラル

いわゆるヒアドキュメント。文字列の中に式展開できる

従来は文字列結合するしかなかった

var name = "近藤勇";
console.log("こんにちは、" + name + "さん!");

こう書けるようになった。バッククォートで囲う

const name = "近藤勇";
console.log(`こんにちは、${name}さん!`);

言語によっては数十年前から当たり前の機能だがようやく


テンプレートリテラル(応用)

改行も入れられる

が、Rubyのヒアドキュメントほど高機能ではなく、インデントを無視してくれる機能などはない

いい感じにするためには、自力で加工する必要がある

(() => {
  const html = `
    <ul>
      <li>foo</li>
      <li>bar</li>
    </ul>
  `.replace(/^\n|\s+$|^ {4}/gm, "");
  console.log(html);
})();

分割代入


配列の分割代入

配列から順番に値を取り出して別個の変数に代入

const arr = [1, 2, 3, 4, 5];
const [var1, var2] = arr;

console.log(var1); //=> "1"
console.log(var2); //=> "2"

Rubyとか他の言語でも大抵こんな感じのことはできますね


オブジェクトの分割代入

オブジェクトからプロパティを取り出して別個の変数に代入

左辺の変数名と同じ名前のプロパティを取り出して代入する

const { foo, bar } = obj;

以下と同じ意味

const foo = obj.foo;
const bar = obj.bar;

オブジェクトの中身を一気に変数に取り出して扱うのに便利

初見で理解しにくい書き方だが、多用されるので覚えておきたい
Reactなどでも多用される。知らないと理解不可能になる


分割代入: その他

他にも色々細かいことができてかなり強力な機能

ネストしたオブジェトのプロパティを取り出すこともできる(難解だが)

const nested = {
  obj: {
    foo: "hello",
    bar: "world"
  }
}

const { obj: [foo] } = nested;
console.log(foo); //=> "hello"

デフォルトを指定することもできる

// プロパティobj.fooが存在しなければ99が代入される
const { foo = 99 } = obj;

スプレッド構文 & 残余構文


配列のスプレッド構文

配列を展開して別の配列に埋め込む

配列の複製、結合などが直感的に書ける

const foo = [1, 2];

// 配列を複製
const bar = [...foo]; //=> [1, 2]

// 要素を追加した新しい配列を作成
const baz = [...foo, 3, 4]; //=> [1, 2, 3, 4]

// 配列を結合
const hoge = [...foo, ...bar]; //=> [1, 2, 1, 2]

使いやすい機能!


オブジェクトのスプレッド構文

配列の場合と同じように操作できる

const foo = { a: 1, b: 2 };

// オブジェクトを複製
const bar = { ...foo }; //=> { a: 1, b: 2 }

// プロパティを追加した新しいオブジェクトを作成
const baz = { ...foo, c: 3 }; //=> { a: 1, b: 2, c: 3 }

// オブジェクトを結合
const hoge = { ...foo, ...{ c: 3, d: 4 } }; //=> { a: 1, b: 2, c: 3, d: 4 }

引数のスプレッド構文

配列を展開して引数に渡せる

function sum(x, y, z) {
  return x + y + z;
}

const numbers = [1, 2, 3];
const result = sum(...numbers);

console.log(result); //=> "6"

引数の残余構文(レスト構文)

スプレッド構文の逆で、不定数の引数を集約する

function sum(...args) {
  let total = 0;
  for (let arg of args) {
    total += arg;
  }
  return total;
}

const result = sum(1, 2, 3);

console.log(result); //=> "6"

for ... of


ES5: foreachしたい(1) for .. in 構文

const array = ["value1", "value2", "value3"];

for (let index in array) {
  console.log(array[index]);
}
  • オブジェクトのキー(配列のインデックス)が返る
  • 順序は保証されない
    • オブジェクト(連想配列)用という感じ。配列でも使えるが不向き
    • でもオブジェクトを回すときは map などを使うことのほうが多そう

ES5: foreachしたい(2) forEach 高階関数

const array = ["value1", "value2", "value3"];

array.forEach(function(elem) {
  console.log(elem);
});
  • for文ではないので break, continue が使えない
    • ループを途中で止められない
    • returnすることでcontinue的なことはできる

インデックスを取得することができる(メリット)

const array = ["value1", "value2", "value3"];

array.forEach(function(elem, index) {
  console.log(index + ": " + elem);
});

ES6: foreachしたい(3) for .. of 構文

今までまともなforeachがなかった

ES6で普通のforeach構文が使えるようになった

const array = ["value1", "value2", "value3"];

for (let elem of array) {
  console.log(elem);
};
  • for文のバリエーションなので、break, continue も使える

foreachの使い分け

  • 配列をループして要素を処理したいときは for .. of が扱いやすい
  • インデックスがほしいときは forEach がスマート
  • オブジェクトのプロパティを処理したいときは for .. in
    • ただし map や filter のほうが適しているケースが多い

Promise


Promise以前の非同期処理(コールバック地獄)

処理が終わったらコールバック関数を呼び出す高階関数実装

非同期関数の戻り値をさらに非同期関数に渡す…、ということをしていくと、ネストがエグいことになっていく

function aFunc(val, callback) {
  // ..時間のかかる処理..
  callback(val * 2);
}

function sampleAsync() {
  aFunc(100, function(val) {
    console.log(val); //=> "200"
    aFunc(val, function(val) {
      console.log(val); //=> "400"
      aFunc(val, function(val) {
        console.log(val); //=> "800"
      });
    });
  });
}

Promiseでの解決

Promiseオブジェクトを返却する実装。メソッドチェーンで並列に書ける

const aFunc = (val) => {
  return new Promise((resolve) => {
    // ..時間のかかる処理..
    resolve(val * 2);
  });
};

const sampleAsync => () {
  aFunc(100)
  .then((val) => {
    console.log(val); //=> "200"
    return aFunc(val);
  })
  .then((val) => {
    console.log(val); //=> "400"
    return aFunc(val);
  })
  .then((val) => {
    console.log(val); //=> "800"
  });
};

async / await

ES2017からはさらに簡単に、同期関数のように自然に書くことができるようになった

await を付けてPromiseを呼ぶと結果が来るまで待ってくれる(超便利)

const sampleAsync = async () => {
  let val = await aFunc(100);
  console.log(val); //=> "200"
  val = await aFunc(val);
  console.log(val); //=> "400"
  val = await aFunc(val);
  console.log(val); //=> "800"
};

await は async のついた関数の中でのみ使用可能なので、無名関数でラップするテクニックが多用される

(async () => {
  let val = await aFunc(100);
  console.log(val);
})();

ES2022からこの制約が緩和。トップレベルからの呼び出しの場合は async 関数でラップする必要はなくなった


ESモジュール
(import & export)


従来はファイルインクルードの仕組みがなかった

従来

jsファイル内から別のjsファイルを読み込む機能が存在しなかった

外部jsファイルを取り込むには、HTMLにscriptタグを埋め込むしかなかった

<script src="js/vender/jquery.min.js"></script>

HTMLファイルと密結合になり、データはグローバルスコープで共有するしかないなど問題多い

めちゃくちゃつらかった


ESモジュール (import & export)

webpackなどモジュールバンドラー環境では以前から import と export が使えた。これなしはありえない

読み込まれる側 (hello.js)

export const sayHello = (message) => {
  alert(message);
};

読み込む側 (index.js)

import { sayHello } from "./hello.js";
sayHello("こんにちはこんにちは");

別名で読み込むこともできる

import { sayHello as shout } from "./hello.js";
shout("こんにちはこんにちは");

 

これ実は今ではブラウザでも使える!


HTMLから読み込むには

type="text/javascript" の代わりに type="module" を指定

<script type="module" src="index.js"></script>

これで import / export 宣言を含むjsが使える

または、インラインでも

<script type="module">
  import { sayHello } from "./hello.js";
  sayHello("こんにちはこんにちは");
</script>

 

つまり、

モジュールバンドラーが使えないClipkitでもスクリプトをモジュール化できるよ


デメリットは?

たくさんのjsファイルを1つにまとめて配信効率化できるのがモジュールバンドラーのメリットだった

ではモジュールバンドラーを使わないと、配信するファイルが多くなるのでパフォーマンスが問題になるのでは?

HTTP/1.1だと効率悪い。HTTP/2なら問題ない

 

もはやモジュールバンドラーを使う必要はない?

TypeScriptやJSXなどのコンパイルには必要

 

個人的にはMinifyも不要だと思う(ソースが見づらくなるし、CDNgzip化すればいい)


おまけ

One more thing...


オプショナルチェーン演算子

ES2020で追加された、地味ながら超便利機能

const user = {
  name: 'John',
  //...
};

console.log(user.profile.age); //=> Cannot read property 'age' of undefined
console.log(user.profile?.age); //=> undefined

Rubyの ぼっち演算子 (&.) と同じもの

どれほど便利かは知ってのとおり