Edited at

JavaScriptの概念たち (前編)

この記事は JavaScript Advent Calendar 2018 4日目の記事です。

昨日は@sasurai_usagi3さんで「CoffeeScriptからJavaScriptに移行する」でした。栄枯盛衰を感じます。「CoffeeScript」でググろうとしたらGoogleさんが「CoffeeScript オワコン」とサジェストしてきて悲しい気持ちになりました。

スクリーンショット 2018-12-03 13.21.35.png

明日は@todays-mitsuiさんで「Ramda とか?について」です。


はじめに

今回はGithubの33個のJavaScriptの概念という記事がかなり良記事だったので、その記事に乗っていたサイトたちを元にそれぞれの章の解説を書いてみました。

これらの概念を知らなくてもJavaScriptを書くことは多分出来ると思いますが、知っておくと何かと便利かと思います。

元サイトたちは参考文献に載せておくのでさらに詳しく知りたい人はそちらを見てください。

もう2018年も終わってしまうというこのタイミングでES5を念頭においたコードを書くということは無いと思うので、例や解説などは全てES2015(ES6)以降を想定して書いておきます。過去の遺産についてはあまり触れません。インツァァネッツォ✝️イクス✝️プローラー?

なお、文中では#00という表記を00章の意味で用いています。#0だと「0. ES2015以降の書き方についてのおさらい」を指します。


ライセンス&クレジット

クリエイティブ・コモンズ・ライセンス

この記事はCC BY 4.0ライセンスで提供されます。ご利用はご自由にどうぞ。

文責:わたせ

Twitter: @tsin1rou Qiita: @tsin1rou

誤字脱字の他、根本的な誤り、もっと良い実装や例の提案などがあればコメント欄もしくは上記twitterまでおしらせいただけると幸いです。


なんとJavaScriptの知識にかけてはこの人の右に出るものはいないと言われているazuさんにチェックしていただくことができました。週ごとにJavaScriptの最新情報を届けてくれるJSer.infoの運営やJavaScriptの入門書の執筆などをされていらっしゃる方です。本当にありがとうございます。

チェックしていただいた後に書き加えた部分もあるのでミスがあればだいたいその部分です。


後編について

あまりにも長くなったので前編と後編に分けました。後編の内容はこんな感じです。


  • 17. Prototype Chain

  • 18. Object.create & Object.assign

  • 19. Array.prototypeの便利な関数たち

  • 20. サイドエフェクトと純粋関数

  • 21. クロージャー

  • 22. 高階関数(HOF)

  • 23. 再帰

  • 24. コレクションとジェネレーター

  • 25. Promise

  • 26. async/await

  • 27. データ構造

  • 28. 計算時間

  • 29. アルゴリズム

  • 30. ポリモーフィズム

  • 31. デザインパターン

  • 32. カリー化と部分適用

  • 33. Clean Code


0. ES2015以降の書き方についてのおさらい

初心者の人でも読めるように簡単にES2015以降のJavaScriptの書き方についてまとめておきます。

なお、コード例中に出てくるhogehoge fugafuga piyopiyoなどは「どんな名前でも良いので適当に決めた例としての名前」です。特に意味はありません。海外だとfoo bar bazなどがよく使用されます。


変数の宣言

JavaScriptを学び始めた頃、変数宣言はvarを使うと覚えた人も多いと思います。ですが、ES2015以降はもっと良い変数宣言ができたのでvarを使うのはあまりおすすめできません。

ES2015以降はconst letを使います。constは再代入ができなくなるという特徴があります。

const hoge = 'hogehoge';

let fuga = 'fugafuga';

hoge = 'piyo'; // TypeError: Assignment to constant variable
fuga = 'piyo'; // OK

また、JavaScriptは上から順番に実行されていくのでまだ出てきていない変数などは使用できません。varは謎仕様によってこの原則に逆らうことがありましたが、varのことを忘れれば全て解決です。

console.log(hoge); // ReferenceError: hoge is not defined

const hoge = 'hogehoge';


関数の宣言

functionという言葉も基本的に使いません。(アロー関数とthisの挙動が異なるのでその場面だけ使います。詳しくは#15を参照)

const 関数名 = (引数argument)=>{ 処理 }と書きます。こうすることで関数名が被ってもエラーが出てお知らせしてくれるようになりました。ま、ESLintを使えという話はあるんですけどね。

const hoge = (arg) => {

return arg * 2;
};

上のようにreturnの行しかないような関数は省略して(引数)=>(返り値)と書けます。

const hoge = (arg) => (arg * 2);

const hoge = arg => arg * 2; // さらに()を省略することも可能

時々謎の記法を見かけるかもしれませんが落ち着いて考えてみます。

const hoge = x => y => x + y; // 謎記法の例

const fuga = x => (y => x + y); // 上と同じ
const piyo = fuga(3); // piyo = y => 3 + y;
console.log(piyo(2)); // -> 5
console.log(fuga(3)(2)); // -> 5


1. コールスタック

JavaScriptでは関数を呼び出すとコールスタックと呼ばれる部分に呼び出された関数が積み上がっていきます。常に一番上の関数が実行されており、関数の実行が終わって値を返すとスタックから取り除かれ、上から2番目に載っていた関数へと戻っていきます。

例えばこんなコードがあったとします。

const hoge = x => x+5;

const fuga = x => hoge(x)*2;
console.log(fuga(3));

上の行から順番に実行され、console.log(fuga(3));の行まで来ました。

   const hoge = x => x + 5;

const fuga = x => hoge(x * 2) * 3;
-> console.log(fuga(3));

このように関数を実行する命令があると、JavaScriptの実行エンジンはコールスタックと呼ばれる場所に関数を積み上げていきます。

コールスタック

console.log(fuga(3))

main()

そして常にコールスタックの一番上を実行します。今回はfuga(3)という関数を実行する命令が再び登場するのでさらに積み上げます。

コールスタック

fuga(3)

console.log(fuga(3))

main()

fugaを実行すると今度はhogeが登場しているので積み上げます。

   const hoge = x => x + 5;

-> const fuga = x => hoge(x * 2) * 3;
console.log(fuga(3));

コールスタック

hoge(6)

fuga(3)

console.log(fuga(3))

main()

一番上にあるhoge(6)を実行します。

-> const hoge = x => x + 5;

const fuga = x => hoge(x * 2) * 3;
console.log(fuga(3));

11という返り値を得られたのでhoge(6)をコールスタックから取り除きます。

コールスタック

fuga(3)

console.log(fuga(3))

main()

一番上にあるfuga(3)の処理に戻ります。hoge(6)=11ということがわかったので33という返り値が求まります。そしてfuga(3)をコールスタックから取り除き、新たに一番上に現れたconsole.log(fuga(3))を実行してコンソールに33という表示が行われます。その後はmain()にしたがって次の行の処理へと進んでいきます。

このコールスタックはエラーを追跡する時に便利さを実感できると思います。例えばfugafuga関数がhogehoge関数を呼び出し、そこでエラーが起きたとします。

const hogehoge = a => { throw new Error('Something Wrong'); };

const fugafuga = a => hogehoge(a);
fugafuga(1);

コンソールなどに表示されるエラーにはコールスタックも表示されていると思います。

pen.js:4 Uncaught Error: Something Wrong

at hogehoge (VM191 pen.js:4)
at fugafuga (VM191 pen.js:7)
at VM191 pen.js:9

at [関数名] (... [script名]:[何行目か])という情報が呼び出しに応じて積み上がっています。下の関数が上の関数を呼び出しているという関係です。

大抵の場合は一番上を見ればエラーの原因が特定できます。


2. プリミティブ型

基本データ型というのがわかりやすい表現で好きです。

-> JavaScript プリミティブ型 (基本データ型)

JavaScriptに組み込まれている型は全部で6個あります。


  • Boolean

    truefalse


  • String

    "abcde"みたいな文字列


  • Number

    普通の数字。100


  • null


  • undefined

    nullは積極的に「無い」事を示すものですがundefinedは「何も定義されていない」状態を表します。

    -> null と undefined の違い


  • Symbol

    ES2015で導入された新しい型。ユニークなIDが生成出来る。

    -> ECMAScript6にシンボルができた理由



3. 値型、参照型

2のプリミティブ型にあるものは全て値型です。値型というのは「変数をコピーした時に中身もコピーされる」ということです。


値型

let x = 10;

let y = 'abc';

const a = x;
const b = y;

x = 5;
y = 'def';

console.log(x, y, a, b); // -> 5 "def" 10 "abc"


xyを変えてもabは変わりません。これは当たり前という感じ。

変数

x
5

y
"def"

a
10

b
"abc"


参照型

オブジェクトとか配列とかは参照型です。例えば下のような配列を宣言したとします。

const a = [0, 1, 2];

すると変数aにはアドレスがセットされます。

変数

a
<#001>

アドレス
データ

#001
[0, 1, 2]

ここで下のように変数をコピーしてみると...

const b = a;

参照だけがコピーされます。

変数

a
<#001>

b
<#001>

アドレス
データ

#001
[0, 1, 2]

つまり、aの中身を変えるとbの中身まで変わります。なるほどね🤔

a.push(99);

console.log(a, b); // -> [0, 1, 2, 99] [0, 1, 2, 99]

値渡ししたい場合は...スプレッド演算子を使って下のように書きます。

const a = [0, 1, 2];

const b = [...a];

a.push(99);

console.log(a, b); // -> [0, 1, 2, 99] [0, 1, 2]

そもそもconstなのに中身が書き換えられるなんてと思った人もいるかもしれませんが、constは変数それ自体の再代入を禁止しているだけなので中身はどうとでも書き換えることができます。

constが禁止するのは以下のような行為です。

const a = [0, 1, 2];

a = []; // -> Uncaught TypeError: Assignment to constant variable.
a = a; // -> Uncaught TypeError: Assignment to constant variable.


4. 型変換

JavaScriptの型変換は2.の型に合わせて


  1. Stringへの変換

  2. Booleanへの変換

  3. Numberへの変換

の3種類が存在します。


String

Stringへの変換は一番直感的な型変換だと思います。

String(123)           // '123'

String(-12.3) // '-12.3'
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(false) // 'false'

また暗黙の型変換として+の片方がStringの場合はもう一方もStringに変換されます。

String(''+0+1)        // '01'

String(0+''+1) // '01'
String(0+1+'') // '1' <- 0+1が先に計算される


Boolean

'' 0 -0 NaN null undefined falseのみがfalseに変換されます。他は全部trueになります。

Boolean('')           // false

Boolean(0) // false
Boolean(-0) // false
Boolean(NaN) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(false) // false
Boolean({}) // true
Boolean([]) // true

||or &&and !notの3つの論理演算子は暗黙のBoolean型変換を行います。

ただし、||&&は型変換を内部でのみ行い、返り値は元の値・型がそのまま返ってきます。

この仕様を利用して条件分岐を省略することができます。

-> JavaScriptの「&&」「||」について盛大に勘違いをしていた件 - Qiita

// player.nameが設定されていなければ'NoName'となる

const name = player.name || 'NoName';

// if文で書くとこうなる
let name;
if(player.name){
name = player.name;
} else {
name = 'NoName';
}


Number

一番色々あって面倒なのがNumberへの変換です。

Number(null)           // 0

Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
Number(" 12 ") // 12
Number("-12.34") // -12.34
Number("\n") // 0
Number(" 12s ") // NaN
Number(123) // 123

以下の演算子はNumberへの暗黙の型変換を行います。


  • 比較演算子 > < <= >=

  • ビット演算子 | & ^ ~

  • 算術演算子 - + * / % (ただし+の片方がStringの場合を除く)

  • 単項の+演算子

  • 比較演算子 == != (ただし比較対象が両方ともStringの場合を除く)

また、==nullundefinedの型変換を行わないという特別ルールがあります。

null == 0               // false


5. ==と===の違い

結論から言うと可能な限り===を使うべきです。

4.でもみた通り、==は暗黙の型変換を行うので意図せぬ挙動の原因となります。

===は同じ型・同じ値の場合のみtrueを返すのでより厳密な比較ができます。

false == 0              // true

false === 0 // false
0 == "" // true
0 === "" // false

nullundefined==においてそれぞれにのみ一致します。

null == undefined       // true

null === undefined // false

NaNは自分自身を含めて何とも一致しないというルールがあります。NaNであるかどうか確認したい場合はNumber.isNaN()を使用します。

const a = NaN;

console.log(a === a); // false
console.log(a === NaN); // false
console.log(Number.isNaN(a)); // true


typeof

どの型なのか調べる場合にはtypeofが便利です。

typeof 3                 // "number"

typeof 'abc' // "string"
typeof {} // "object"
typeof true // "boolean"
typeof undefined // "undefined"
typeof (()=>{}) // "function"


使用例

if (typeof hogehoge === 'number'){...}

const fugafuga = value => {
switch (typeof value) {
case 'string':
return 'this is string';
case 'number':
...
}
}


とはいえ、このtypeofはラッパーオブジェクト (new Number()とか) を全部"object"と判定してしまうなど一部使い勝手に問題があるので、実際はObject.prototype.toString.call()を使うことが多いと思います。詳しくは#30で。


6. スコープ

varはJavaScriptの闇の歴史の中に葬り去られたと信じているのでletおよびconstについてのみ書きます。

新しくJavaScriptを書くときにvarを使ってはいけません。

-> 本当にvarは駄目な子なのか? - Qiita

varを無視すればスコープについては非常に簡単で、{}で囲まれたブロックスコープが適用されます。ifでもforでも関数でも、{}で囲まれてさえいればスコープが発生すると考えればOKです。

const hoge = 'hogehoge';

{
console.log(hoge); // Uncaught ReferenceError: hoge is not defined
const hoge = 'fugafuga';
console.log(hoge); // -> "fugafuga"
}
console.log(hoge); // -> "hogehoge"

上記のコード3行目でエラーが発生するのに違和感を覚える人も少なくないと思います。通常の上から解釈されていく言語ならhogeがそのスコープ内で未定義だった場合、上のスコープのものが自動で使用されるのが自然でしょう。もちろんJavaScriptでも基本的には上のスコープを見にいくのですが、同名の変数が同じスコープで定義されていると代わりにエラーが出ます。

const hoge = 'hogehoge';

{
console.log(hoge); // -> "hogehoge"
}
{
// JavaScriptでは同じスコープで同名の変数が宣言されているとエラーになる
console.log(hoge); // ReferenceError
const hoge = 'fugafuga';
}

これは「宣言の巻き上げ」というJavaScriptの仕様で、全ての変数はスコープの一番上で宣言されたのと同じ扱いになります。ただし、明示的に宣言した場合にはエラーが出ません。

{

let hoge;
console.log(hoge); // -> undefined
hoge = 'hogehoge';
}
{
console.log(hoge); // ReferenceError
let hoge = 'fugafuga';
}


7. 文Statementと式Expression

JavaScriptのソースコードはStatementExpressionという2つの構文から成り立っています。StatementはJavaScriptにおける文の単位のことで、例えばif文だとif (式) 別の文という形ですね。Expressionは評価できて値が返る感じのやつです。

Statementをうまく説明できなくて申し訳ないのですが、基本的に「戻り値」がないものがStatementです。以下のような予約語を使用した構文と言えばいいですかね。StatementをExpressionが期待されている部分(関数呼び出しの際の引数など)に使用することはできません。



  • const let (var)


  • if (else) switch


  • for (do) while break continue


  • throw try catch (finally)


  • (function) functionにはStatementとしてのfunctionとExpressionとしてのfunctionがありますが、StatementとしてのfunctionはJavaScriptの黒歴史として葬り去られたので基本的に使用しません。
    -> 関数宣言 vs 関数式 | ES2015+ - Qiita

StatementとStatementの間には;セミコロンを使います。ただし、{ }で囲まれたブロック文でStatementが終わる際には;を付けてはいけません。これがif(){}構文とかwhile(){}の後ろに;が付いていない理由です。

逆にif()hoge();みたいな{}を使わないifの後ろには通常通り;が必要です。


8. 即時関数、モジュール、名前空間

即時関数(IIFE, Immediately-Invoked Function Expressions)とは言ってみれば「使い捨ての関数」です。その場で使い捨てるのでグローバル空間を汚染しない他、外部からアクセスできる変数を制御することに使われてきました。

ただし、ES2015以降はブロックスコープの導入やモジュールの登場などにより上記の利点が一切失われ、ただ互換性を保つためだけに残っている書き方です。

新たにJavaScriptを書く際は即時関数を利用する代わりにモジュールを利用してください。


即時関数

即時関数とは関数を定義してすぐその場で実行する関数のことです。本当は下の例の一番上のように書きたいところなのですが、JavaScriptの構文上これはfunction expressionではなくfunction statementとして解釈されてしまうためエラーとなります。そこで( )を使用してfunction expressionであることを示しています。

function(){

...
}(); // -> Unexpected token

(function(){
...
})(); // -> OK

(function(){
...
}()); // -> これもOK

かつてJavaScriptにはvarという変数宣言がありました。これはlet constとは異なりブロックスコープではなくfunctionスコープだったため、varで宣言した変数をローカル化するにはfunctionで囲わなければいけませんでした。

(function() {

var innerScope = 'hoge';
})();

console.log(innerScope); // ReferenceError

このとき活躍したのが上記のような即時関数で、function(){}構文で生成される関数をその場で実行することで外部からのアクセスを禁止できました。

古いにしえの時代においてはJavaScriptはライブラリを導入する際、htmlに<script src=...></script>を依存関係に気をつけながら順番に並べて導入していたため、どこでどんなグローバル変数が使われているのか分かりませんから、謎の競合を起こさないようにするために自分でJavaScriptを書く際は基本的に即時関数を使用していました。

var hogehoge = 'hoge';

(function() {
var hogehoge = 'fuga';
console.log(hogehoge); // -> "fuga"
})();

console.log(hogehoge); // -> "hoge"

しかし今ではモジュールがありますし、またconstletはブロックスコープが適用されるので即時関数の出番はほぼ無くなったと言えるでしょう。

const hogehoge = 'hoge';

{
const hogehoge = 'fuga';
console.log(hogehoge); // -> "fuga"
}

console.log(hogehoge); // -> "hoge"


モジュール

この章の例はあくまでモジュールを紹介するためだけに書きました。多くの場合、これを参考にするのではなく#14で紹介するclassを使う方が妥当かと思われます。

モジュールは別ファイルに分けて作成します。


hogehoge.js

let cnt = 0;

export const inc = () => ++cnt;

別ファイルでexportしたものをimportで呼び出すことができます。


main.js

import { inc } from './hogehoge.js';

console.log(inc()); // -> 1
console.log(inc()); // -> 2

名前空間を分けたい場合は次のように書きます。


main.js

import * as counter from './hogehoge.js';

console.log(counter.inc()); // -> 1
console.log(counter.inc()); // -> 2


main.js

import { inc as hogehogeIncrement } from './hogehoge.js';

console.log(hogehogeIncrement()); // -> 1
console.log(hogehogeIncrement()); // -> 2

上記のようなhogehoge.jsの書き方をすると内部の変数は同じものになっていて、どこから呼び出しても同じ値が返ってきます。


hogehoge.js

let cnt = 0;

console.log('hogehoge');
export const inc = () => ++cnt;


foo.js

import { inc } from 'hogehoge.js';

console.log('foo');
inc();


bar.js

import { inc } from 'hogehoge.js';

console.log('bar');
inc();


main.js

import './foo.js';

import './bar.js';
// -> 'hogehoge'
// -> 'foo'
// -> 1
// -> 'bar'
// -> 2

インスタンスを切り分けたい場合は次の節に出てくるclassを利用します。


export default

モジュールが1つのオブジェクトしかexportしない場合はexport defaultを使うことが多いです。


hogehoge.js

class Hogehoge {

constructor(){
this.count = 0;
}
inc(){
return ++this.count;
}
}
export default Hogehoge;


main.js

import Hoge from './module';

const counter = new Hoge();
console.log(counter.inc()); // -> 1
console.log(counter.inc()); // -> 2


上記のようにclassのexportでよく使うように思います。

importとは違い、require()にはdefaultを標準で読み込んでくれるような機能はないので、何らかの事情でrequire()を使ってdefaultimportする場合はrequire().defaultとする必要があります。


ブラウザでのモジュール

ブラウザでも<script src="..." type="module"></script>のようにtypeを指定すればESModulesを使用できます。※ただしIEを除く

将来的にはwebpackでバンドルする必要も無くなるのかもしれませんが、現状ではnpmモジュールの多くがESModules形式に対応していないなどの問題があるためまだしばらくwebpackの天下は続くと思います。

あとファイルをダウンロードしてimport文を見つけて別のファイルをダウンロードして...というのは遅いですからね。http/2とかhttp/3ならまだしもhttp/1.1だとリクエスト数の制限のせいで複数ファイルのダウンロードが遅いので最悪です。


9. イベントループとメッセージキュー

JavaScriptは基本的にシングルスレッドで動作しているため、本来は全てのコードが同期的に動きます。つまり、1つの処理を行なっている間は他の処理を行うことができません。しかしそれでは不便だということでイベントループとメッセージキューという仕組みがあります。これらの仕組みによって擬似的に非同期処理を実現しています。

ここで重要なのですが、イベントループはJavaScriptエンジンの外部にあるものだということに注意してください。JavaScriptを制御しているのは実はイベントループの方であり、エンジンは渡された関数を実行するだけです。また、擬似的と書いたように実際には同期的に動作しているため、コールスタックに他の処理が入っている場合はイベントループは動きません。

イベントループの細かな設計は環境によって異なりますが、この章ではNode.jsの例を見ていきます。Node.jsのイベントループのページでは以下のような図で説明されています。


イベントループ

   ┌───────────────────────────┐

┌─>│ タイマー │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ 未解決のコールバック │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘

JavaScriptエンジンにコールスタックがあるのとは対称に、イベントループにスタックはなく、代わりに複数のキューから成り立っています。


いつイベントループが動作するのか

JavaScriptを実際に走らせると、まず自分の書いたコードが上から下まで実行されます。この実行が終わったら、つまりコールスタックが空になったらイベントループに入ります。

「入ります」と書きましたが、node.jsの場合はまずイベントループを動かす必要があるかどうかのチェックが行われます。イベントループで何も行われない場合はループに入らず、プロセスの終了処理を行います。

タイマーがセットされていたり、接続の待ち受けを行なっていたりするなど何らかの処理が残っている場合にはイベントループが動き始めます。


タイマーフェーズ

イベントループはこのフェーズから始まります。

名前の通り内部にタイマーを保持しており、指定した時間が経過したコールバックを実行します。この説明を読めば分かるように、setTimeoutとかのタイマー系関数は指定時間経過後にイベントループがタイマーフェーズに戻ってくるまで実行されません。

コールバックというのはまあ要するに関数の事で、関数の実行が終わった際に実行する別の関数の事を特別にそう呼びます。相手に電話して「〜〜のタイミングで〇〇に掛け直call backしてね」と言うイメージ。

例えば、A B C Dの4つのタイマーをセットした場合を考えてみます。


timer

setTimeout(A, 100);

setTimeout(B, 200);
setTimeout(C, 300);
setTimeout(D, 400);

タイマーがセットされると、登録されたコールバックはタイマーフェーズ用のキューに昇順で並べられます。

A
B
C
D

さて、main.jsの他の処理が終わるまで時間がかかり、タイマーフェーズが始まる際にすでに250ms経過していたとします。すると今回のタイマーフェーズではA Bが実行され、Cの時間をチェックしてまだ到達していなければそこでタイマーフェーズが終了します。他にも、「Timerフェーズで使っていい時間」の制限もあるため、あまりにも長い処理を行なった場合は残りのタイマーは(時間がきていたとしても)スキップされ、次のタイマーフェーズまで先送りされます。


未解決のpendingコールバック

イベントループのpending queueに並んでいるコールバックを実行します。

pendingという名前の通り、1つのイベントループ内で処理しきるには遅すぎるものが複数のイベントループに渡って実行されます。代表例はファイルi/oやネットワーク接続。

キューが空になるか一定時間経過すると次のフェーズに移行します。


Idle & Prepareフェイズ

次のpollフェイズの準備を行います。

Node内部の処理も行われるらしいけどよく知りません。


Pollフェイズ

イベントループで一番大事だと思われるフェイズ。

JavaScriptで新しくファイルを読み込もうとしたり、外部からの接続を受け取る際にはそれらのタスクは一度watch_queueに入れられます。

このPollフェイズではwatch_queueに入っているタスクを順番に処理していきます。watch_queueが空になっても一定時間はPollフェイズのまま新しい接続を待ち受けます。


チェックフェイズ

setImmediate()のためのフェイズ。setImmediate()で追加されたコールバックたちがこのフェイズで実行されます。

setImmediate()node環境にしかない関数なのでブラウザでは使えません。


closeフェイズ

様々な処理のクリーンアップを行います。ソケットのクローズ処理(socket.on('close',()=>{}))もここで行われます。

closeフェイズが終わればもう一度イベントループを回す必要があるかどうかをチェックします。イベントループ内の各キューに何も処理が残っていなければプロセスを終了します。

なお、Promise.resolve()process.nextTick()はどのフェイズで呼び出されても次のフェイズに行く前に実行されます。同時の場合はnextTickが優先されます。


10. ブラウザで使用できるタイマー


setTimeout, setInterval

setTimeoutsetIntervalは共にブラウザで使用できるタイマーです。あまりにも古い関数なので引数の順番がキモい。

setTimeout(callback, delay)という感じで使います。delayミリ秒以降にcallbackが実行されます。

setInterval(callback, interval)の方はintervalミリ秒ごとにcallbackが実行されます。


index.js

console.log('startTime:', new Date());

setTimeout(()=>{
console.log('timeout:', new Date());
}, 1000);
const timeout = setInterval(()=>{
console.log('interval:', new Date());
}, 1000);

setTimeout(()=>{
clearInterval(timeout);
}, 5000);


$ babel-node index.js

startTime: 2018-11-04T02:54:44.630Z
timeout: 2018-11-04T02:54:45.635Z
interval: 2018-11-04T02:54:45.635Z
interval: 2018-11-04T02:54:46.640Z
interval: 2018-11-04T02:54:47.643Z
interval: 2018-11-04T02:54:48.647Z
✨ Done in 5.68s.

callbackに引数を渡したい時は

setTimeout(callback, delay, 引数1, 引数2, ...)

とするか、

setTimeout(()=>{callback(引数1, 引数2, ...)}, delay)

とします。

イベントループはコールスタックが一度空になってから動き始めるのでエラーのデバッグが若干面倒です。


index.js

setTimeout(()=>{

throw new Error('error');
}, 1000);

どのエラーが発生したのかはわかりますが、コールスタックはタイマーのものとなっています。どこでタイマーに突っ込まれたのかは分かりません。

$ babel-node index.js

/Users/watace/qiita_repos/sandbox/index.js:4
throw new Error('error');
^

Error: error
at Timeout._onTimeout (/Users/watace/qiita_repos/sandbox/index.js:2:9)
at ontimeout (timers.js:424:11)
at tryOnTimeout (timers.js:288:5)
at listOnTimeout (timers.js:251:5)
at Timer.processTimers (timers.js:211:10)
error Command failed with exit code 1.


requestAnimationFrame

ブラウザでアニメーションというと「CSSだけでハロウィン気分の404ページ作った」のようにCSSでのアニメーションがメインとなっています。

でもCSSの機能だけでは満足できない!!!

もっとJavaScriptで卍最強卍のアニメーションを作りたい!!!!

という場合に用意されているのがrequestAnimationFrameです。

requestAnimationFrame(callback)

とすると次の描画の直前にcallbackが実行されます。つまりこれを再帰させて下のコードの様にすると描画ごとに実行される関数の完成です。

const animation = ()=>{

requestAnimationFrame(animation);
// ここで描画処理
};
requestAnimationFrame(animation);

アニメーションにsetInterval(animation, 1000/60)を使うのは避けましょう。フレームの更新と実行タイミングが合わない上に、ブラウザの実装によっては別のタブを開いていても実行され続けてしまう場合があります。


11. JavaScriptエンジン

世の中にはJavaで動く「Rhino」やSafariなどで使われている「JavaScriptCore」、Firefoxの「SpiderMonkey」、Google Chromeの「V8」など様々なJavaScriptエンジンが存在しています。

これらのエンジンはJavaScriptのコードを機械語に変換する役割を果たしています。エンジンごとに様々な方法でJavaScriptを機械語に変換しているのですが、ここでは「V8」について詳しくみていきます。


抽象構文木(AST)

V8エンジンはまずソースコードの構文解析を行い、抽象構文木を構築します。


code

var a;



AST

{

"type": "Program",
"start": 0,
"end": 6,
"range": [
0,
6
],
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 6,
"range": [
0,
6
],
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 5,
"range": [
4,
5
],
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"range": [
4,
5
],
"name": "a"
},
"init": null
}
],
"kind": "var"
}
],
"sourceType": "module"
}


Ignition

V8ではバージョン5.9以降(⇒Chrome 59以降)、「Ignition」というインタプリタが使用されており、このIgnitionはASTをバイトコードに変換します。

バイトコードとは機械語を若干抽象化したようなものです。例えば下記のような関数があるとします。

const f = (a, b, c) => {

const d = c - 100;
return a + d * b;
}

これをバイトコードにするとこんな感じです。nodeを実行するときに--print-bytecodeをつけると出力してくれます。

LdaSmi #100 // accumulatorに100をコピー

Sub a2 // a2からaccumulatorを引く (c-100)
Star r0 // r0にaccumulatorをコピー (d=50)
Ldar a1 // a1からaccumulatorにコピー
Mul r0 // accumulatorにr0を掛ける (d*b)
Add a0 // accumulatorにa0を足す (a+(d*b))
Return // accumulatorの値を返します

引数はレジスタに入れて渡します。例えばf(5, 2, 150)の時のレジスタはこんな感じ。

レジスタ

a0
5

a1
2

a2
150

r0
undefined

accumulator
undefined


TurboFan

Ignitionによって生成されたバイトコードを機械語に翻訳してくれるのがTurboFanです。

機械語っていうとこんな感じのやつです。

movl rbx, [rax+0x1b]

REX.W movq r10,0x100000000
REX.W cmpq r10,rbx
...


12. ビット演算子

コンピューターは01で成り立っているのですが、その2進数表現で出てくる演算がビット演算です。

JavaScriptにもビット演算子が存在します。何に使うのか分かりませんが。競技プログラミングとか組み込み系とかをJavaScriptでやる人がいれば役立つかもしれません。


&and

ANDというのは2つの入力が共に1だったら1を出力する演算です。

入力1
入力2
出力

0
0
0

1
0
0

0
1
0

1
1
1

&演算子はこのAND演算を2進数のそれぞれの桁に対して行います。

例えば3&6を考えてみると、30011であり60110なので

8
4
2
1
10進数

入力1
0
0
1
1
3

入力2
0
1
1
0
6

出力
0
0
1
0
2

となります。

console.log(3 & 6);     // -> 2

console.log(12 & 15); // -> 12


|or

ORは2つの入力のどちらかが1だったら1を出力する演算です。

ANDというのは2つの入力が共に1だったら1を出力する演算です。

入力1
入力2
出力

0
0
0

1
0
1

0
1
1

1
1
1

例えば3|6はこんな感じです。

8
4
2
1
10進数

入力1
0
0
1
1
3

入力2
0
1
1
0
6

出力
0
1
1
1
7

console.log(3 | 6);     // -> 7

console.log(12 | 15); // -> 15


~not

NOTは入力を反転する演算です。マイクラで言うとレッドストーントーチです。

入力
出力

0
1

1
0

8
4
2
1
10進数

入力
0
0
1
1
3

出力
1
1
0
0
12?

上のように4bitでの演算であれば~312になるように思えます。しかし実はJavaScriptにおいては一番上のbitは負の数を表すという決まりがあります。したがって4bitでの1100-8+4+0+0なので答えは-4となります。

8
4
2
1
10進数

入力
0
0
1
1
3

出力
1
1
0
0
-4

console.log(~3);    // -> -4

console.log(~12); // -> -13


^xor

XOR演算は2つの入力のうちどちらかが1であれば1を出力する演算です。両方が1の時は0になります。

入力1
入力2
出力

0
0
0

1
0
1

0
1
1

1
1
0

例えば3^6はこんな感じです。

8
4
2
1
10進数

入力1
0
0
1
1
3

入力2
0
1
1
0
6

出力
0
1
0
1
5

console.log(3 ^ 6);   // -> 5

console.log(12 ^ 15); // -> 3


シフト演算子

シフト演算子<< >> >>>を使えばビットを左右に動かすことができます。

例えば91001なので9<<2100100、つまり36となります。

console.log(9 << 2);  // -> 36

>>はビットを右にずらすのですが、その際左端のビットがコピーされます。

0000...00001001>>で2bit右にずらすと0000...00000010となり、

1111...11110111>>で2bit右にずらすと1111...11111101となります。

符号が維持できるというメリットがあります。

console.log(9 >> 2);  // -> 2

console.log(-9 >> 2); // -> -3

>>>は左端に0を追加します。

1111...11110111>>>で2bit右にずらすと0011...11111101となります。

console.log(9 >>> 2); // -> 2

console.log(-9 >>> 2);// -> 1073741821


使い方


フラグ管理

Linuxでパーミッション変えようとしたときに777とか謎の数字を打ち込むことになるのですが、これはフラグを表しています。

7というのは2進数だと111となるのですが、この一桁一桁がフラグになっています。Linuxのやつだと左からr w xですね。

r--だと100なので4rwxだと1117となります。

特定のフラグが立っているかどうかを調べるには&を使います。

const xFlag = 1;

const wFlag = 1 << 1;
const rFlag = 1 << 2;

if(flags & rFlag){ // flagsは7とか4とかの数字
// rが立っている場合に処理が走る
}
if(flags & (rFlag | wFlag) === (rFlag | wFlag){
// rとwが両方立っていれば走る
}

フラグのセットは|=なんかを使います。

let flags = 0;

// もちろん let flags = wFlag | rFlag; とかでも良い

flags |= rFlag;


部分和

競技プログラミングでは比較的bit演算が多用される気がします。そのうち一つだけ例を紹介します。

部分和というのは例えば


1 3 5という3つのカードを足してできる数字はどんなものがあるのか


という感じの問題です。答えとしては1 3 4 5 6 8 9となるのですが、これを求めるのにbit演算が使われます。

最初に0番地だけが1のビット列を用意します。


0000 0000 0001


そこにカード1の数字分だけ左ビットシフトを行い、元のビット列とorをとります。


0000 0000 0011


次のカード3の数字分だけ左ビットシフトを行い、元のビット列とorをとります。


0000 0001 1011


同じ操作を繰り返します。次は5なので5ビットシフトして...となります。


0011 0111 1011

BA98 7654 3210(番地・参考)


何番地が1なのかを調べればどんな部分和が現われるかを求められます。上のビット列では1番目、3番目、4番目、...、9番目が1になっています。

素朴な解法だとO(2^N)だったのがこの解法だとO(N)にできます。


13. DOMとレイアウトツリー

DOMはドキュメント・オブジェクト・モデルの略です。簡単にいうと要素のツリーです。

例えばこんな感じのHTMLがあるとします。

<!DOCTYPE html>

<html>
<head>
...
</head>
<body>
<div>
...
</div>
<script src="..."></script>
<script src="..."></script>
</body>
</html>

DOMは下のようになります。

DOCUMENT (always the root)

└─HTML
├─HEAD
│ └─...
└─BODY
├─DIV
│ └─...
├─SCRIPT
└─SCRIPT

ブラウザはhtml文章をパースしてDOMを生成します。画面に描画されているのはDOMなので、これを適当に編集すれば画面の表示も変わります。

JavaScriptからDOMを編集する方法は色々あるのですが、代表的なものだけ紹介します。そもそも今時はReactとかVueとか使いますからね。直接編集するようなことはあまりないと思います。


要素を取得する

よく使用されるのはgetElementById()getElementsByClassName()querySelectorAll()などです。

// <... id="hoge">みたいにidが"hoge"の要素を取得

const element1 = document.getElementById('hoge');

// <... class="foo bar baz">みたいにクラスに含まれていればまとめて取得
// 返り値はリストになる。
const element2 = document.getElementsByClassName('foo');

// cssのセレクター形式で要素を取得。返り値はリスト。
const element3 = document.querySelectorAll('#hoge'); // id="hoge"の要素を取得
const element4 = document.querySelectorAll('div.highlighted > p'); // p要素のうち直接の親が'highlighted'クラスを持つものを取得


要素の編集

これもよく使うものだけ紹介します。

const element = document.getElementById('...');

// 要素を作成
const hogehoge = document.createElement('div');

// 要素を追加
element.appendChild(hogehoge);

// 要素を削除
element.removeChild(hogehoge);
// 子要素を全削除
while(element.firstChild){
element.removeChild(element.firstChild);
}

// クラスを取得
const classList = element.classList;
if(classList.contains('fugafuga'){
// 要素にfugafugaクラスが設定されていれば走る
}

// クラスを追加・削除・切り替え
classList.add('fugafuga');
classList.remove('fugafuga');
classList.toggle('fugafuga'); // 地味にこいつが便利で重宝する


イベント関連

addEventListener()removeEventListener()を使います。

element.addEventListener('click', (event)=>{

// preventDefault()を呼ぶと通常の動作が行われなくなる。
// 例えばaタグをクリックしても移動しなくなったり。
event.preventDefault();
console.log('要素がクリックされました');
});

// removeの方は両方の引数を同じにする必要がある
const clickHandler = (event)=>{...};
element.addEventListener('click', clickHandler);
element.removeEventListener('click', clickHandler);

click以外にどんなイベントがあるかはEvent reference | MDNで確認できます。


14. ClassとFactory


Class

ES2015以降は他の言語と同じようにClassを書くことができます。引数がなかった場合の初期値を決めるのには色々流派があるのですが僕は下のようにオブジェクトを利用する方法が好きです。事実上の名前付き引数ですね。ただしデフォルト値を設定する時、方法2の||を使う方法は''とかfalse null 0とかの代入に失敗するのでその点だけは注意してください。

class Hoge {

// 引数を一つのオブジェクトにするという約束にして、argsで纏めて受け取る
constructor(args){
// 初期化処理

/* 方法1
this.state = Object.assign({
name: 'NoName',
age: 0
}, args);
*/

// 方法2
const { name, age } = args || {};
this.name = name || 'NoName';
this.age = age || 0;
}
sayHello(){
return `Hello! I am ${this.name}.`;
}
selfIntro(){
return `${this.sayHello()} And I'm ${this.age} years old.`
}
}

const hoge = new Hoge();
const fuga = new Hoge({name:'watace', age:23});

console.log(hoge.selfIntro()); // -> "Hello! I am NoName. And I'm 0 years old."
console.log(fuga.selfIntro()); // -> "Hello! I am watace. And I'm 23 years old."

extendsを使うと継承もできます。継承というのは簡単にいうと親のコピー+αを作成するということです。

class Piyo extends Hoge {

constructor(args){
// super()を使うと親のconstructorを呼び出せます
super(args);

const { nakigoe } = args || {};
this.nakigoe = nakigoe || 'piyo';
this.capital = this.nakigoe.charAt(0).toUpperCase()
+ this.nakigoe.slice(1); // 先頭を大文字にしてます
}
sayHello(){
return `${this.capital}! ${this.name} ${this.nakigoe}!`
}
}

const piyo = new Piyo({name:'watace'});
console.log(piyo.selfIntro()); // -> "Piyo! watace piyo! And I'm 0 years old."

ただしこのクラス構文はそのメンバーをEventListenerにセットするときに若干問題がおきます。

class Foo {

constructor(args){
const { name } = args || {};
this.name = name || 'NoName';
}
say(){
return `${this.name} here!`;
}
onClick(event){
console.log(this.say()); // 上のsay()を呼び出したいが...
}
}

const foo = new Foo({name:'pizza'});
const elem = document.getElementById('hoge');
elem.addEventListener('click', foo.onClick);
// クリックすると TypeError: this.say is not a function になる
// ↑この時のthisにはelemが入っているため

というのもイベントリスナーのハンドラーは呼び出されるときに呼び出し元の要素がthisになってしまうからです。

これはthisの問題なので解決方法は色々あります。が、今回はReact界隈で一番よく使用される「constructorでbind()する」方法を紹介します。

class Foo {

constructor(args){
...
// bind(this)を行うことで、呼び出された際のthisを今のthisのままにできる
this.onClick = this.onClick.bind(this);
}
...
}
...
// クリックすると目的通り"pizza here!"となる

ちなみにprivateなプロパティを持ちたい場合はSymbolを使うと良いようです。

(個人的な感想ですが、publicなプロパティでも直接編集するのはやめた方が良いと思います。インスタンスのプロパティを直接編集しているコードはどこで何をされたか分からず見た瞬間警戒心がMaxになるので。)


Symbolって何?

// 自分自身としか一致しないユニークなオブジェクトを生成する

const symbolA = Symbol();
console.log(symbolA === symbolA); // -> true
console.log(symbolA === Symbol()); // -> false

const __name__ = Symbol();

class Bar {
constructor(args){
const { name } = args || {};
this[__name__] = name || 'NoName'; // Symbolの場合
this.name = name || 'NoName'; // 通常のプロパティ
}
sayHello(){
return `Hi, I'm ${this[__name__]}`;
}
}
const bar = new Bar({name:'watace'});
bar.name = 'hoge'; // 直接プロパティを編集できる
console.log(bar.name); // -> "hoge"😢
console.log(bar.sayHello());// -> "Hi, I'm watace"👍

// __name__を知っていれば直接アクセスできる
console.log(bar[__name__]); // -> "watace"

// これを避けるにはモジュール化してclassだけをexportすればOK
// インポート先では__name__は分からない
export default Bar;

ただしシンボルはReflect.ownKeys()で列挙されてしまうとのこと。

アクセスを完全に防ぐにはWeakMapを使うのが良いらしいです。(azuさんに教えていただきました。ありがとうございます)



const __name__ = new WeakMap();

class Baz {
constructor(args){
const { name } = args || {};
__name__.set(this, name);
}
sayHello(){
return `Hi, I'm ${__name__.get(this)}`;
}
}
const baz = new Baz({name: 'watace'});
console.log(baz.sayHello()); // "Hi, I'm watace"


コラム: JSDocについて

上記のようなargsオブジェクトで全部の引数をまとめて受け取っちゃうような場合、どんなプロパティを設定すればいいのか分からなくなると思います。そこで活躍するのがドキュメントです。

class Hoge {

/**
* ここにclassの説明
* @param {Object} args ここにargsの説明
* @param {string} args.name what's your name?
* @param {number} args.age how old are you?
*/

constructor(args){
...

上記の形式でconstructorの前に書きます。するとエディタが下のような情報を出してくれます。

スクリーンショット 2018-11-11 19.09.14.png

補完も動作します。

スクリーンショット 2018-11-11 19.09.30.png

また、下のようにメソッドの前に書けば...

  /**

* 挨拶します。
* @returns {String} hello sentence
*/

sayHello(){
return `Hello! I am ${this.name}.`;
}

エディタが情報を出してくれます。

スクリーンショット 2018-11-11 19.12.59.png

JSDocで良いJavaScript生活を送りましょう😎


Factory

Factoryという大層な名前がついていますが、ざっくり言うとオブジェクト版テンプレートのことです。複雑な引数を持ったクラスを簡単に作成するために噛ませたりします。

// 年齢を半分に詐称するfactory関数

const factory = (name, age) => {
return new Hoge({name, age: age / 2});
}

決まった形式のオブジェクトを簡単に作れるようにするためにも使います。

const createIssue = (title, description)=>{

return {
title,
description,
tag: ['new'],
date: new Date(),
status: 'open'
}
}


Factoryって全然馴染みがありませんでしたが、よく考えればReduxのActionの生成はこの形式ですね。

const receiveData = data => (

{
type: 'RECEIVE_DATA',
data
}
);


15. thisとcallとapplyとbind


this

function()=>{}で何がthisになるかが異なります。


functionにおいてのthis

thisは呼び出し方に応じて変化します。呼び出されるまで何がthisになるか分かりません。

function hoge(){

console.log(this);
}

hoge(); // -> global

const obj = {
hoge
};
obj.hoge(); // -> obj

const obj2 = {
fuga: function(){
hoge();
}
};
obj2.fuga(); // -> global???!!!?!?!!?!??!!


アロー関数()=>{}においてのthis

コードを見れば何がthisになるかが分かります。これを「レキシカルなthis」と言います。

分かりやすいしミスも起きにくいので基本的にアロー関数を使用するようにしましょう。

アロー関数はどこからどうやって呼び出してもthisは常に同じものをさします。後述のapply call bindを使ってもこの制限を突破してしまうことはありません。

const hoge = () => {

//この段階でthisがglobalなのでどこで呼び出してもglobalになる
console.log(this);
};

hoge(); // -> global

const obj = {
hoge
};
obj.hoge(); // -> global

const obj2 = {
fuga: function(){
hoge();
}
};
obj2.fuga(); // -> global


callとapplyとbind

全部「何をthisにするか」を決めるための関数です。


callとapply

callapplyも呼び出す際に何がthisになるのかを明示的に決定するための関数です。

共に第一引数にthisにしたいものを入れます。callは第二引数以降に直接引数にしたいものを入れていきますが、applyは引数をまとめて配列にしたものを第二引数にするという違いがあります。

ただし共にアロー関数のthisを変えることはできません。

const hoge = (arg) => {

console.log(arg);
console.log(this);
};
function fuga(arg){
console.log(arg);
console.log(this);
}

const obj = {};
const args = ['arg1', 'arg2'];
hoge.call(obj, args);
// -> ["arg1", "arg2"]
// -> global

fuga.call(obj, args);
// -> ["arg1", "arg2"]
// -> Object {}

hoge.apply(obj, args);
// -> "arg1" (配列が展開されて渡される)
// -> global

fuga.apply(obj, args);
// -> "arg1"
// -> Object {}


bind

bindも同じように何をthisにするかを決められます。こちらは一度決めれば永続的に効果を発揮しますが、アロー関数には使えません。

const hoge = () => {

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

const obj = {};

// bindは新しい関数を返す
const hoge2 = hoge.bind(obj);
const fuga2 = fuga.bind(obj);

hoge(); // -> global (元の関数を変化させない)
fuga(); // -> global
hoge2(); // -> global (アロー関数には効かない)
fuga2(); // -> Object {}


16. newとインスタンスについて

newというのはClassの章に出てきたconst hoge = new Hoge()みたいな奴のことです。このnewは何を指示しているのでしょうか?

結論から言うとnewはインスタンスを作成するのに使用します。

class Hoge {

constructor(args){
console.log(this);
const { name } = args || {};
this.name = name || 'NoName';
}
}
const Fuga1 = (args) => {
console.log(this);
const { name } = args || {};
this.name = name || 'NoName';
}
function Fuga2(args) {
console.log(this);
const { name } = args || {};
this.name = name || 'NoName';
}
// thisの中身は?
const hoge1 = Hoge(); // -> TypeError: Class constructor Hoge cannot be invoked without 'new'
const hoge2 = new Hoge(); // -> Object { name:"NoName" }
const fuga1_1 = Fuga1(); // -> global
const fuga1_2 = new Fuga1(); // -> TypeError: Fuga1 is not a constructor
const fuga2_1 = Fuga2(); // -> global
const fuga2_2 = new Fuga2(); // -> Object { name:"NoName" }

上の実験から、newがあればconstructorの呼び出し時に新たにオブジェクトが生成されて渡され、constructor内のthisがその新しいオブジェクトを指すようになることが分かります。

class()=>{}アロー関数がそれぞれどちらかしか行えないのに対してfunctionは両方行えてしまいます。newをつけるとfunctionをコンストラクタとして新しいオブジェクトを生成します。

個人的にはnewclassと常にセットで使用し、それ以外の場合は使用しないという取り決めが好きです。


instanceof

インスタンスがどんなクラスを継承してきたのかを確かめるにはinstanceofを使用します。たくさん継承を重ねていても先祖のどこかに存在すればtrueになります。

class Hoge {

constructor(args){
...
}
}
class Fuga extends Hoge {
constructor(args){
super(args);
...
}
}
const hoge = new Hoge();
const fuga = new Fuga();
console.log(hoge instanceof Hoge); // -> true
console.log(hoge instanceof Fuga); // -> false
console.log(fuga instanceof Hoge); // -> true
console.log(fuga instanceof Fuga); // -> true


後編へ続く

-> JavaScriptの概念たち (後編)


参考文献(前編)