この記事は JavaScript Advent Calendar 2018 4日目の記事です。
昨日は@sasurai_usagi3さんで「CoffeeScriptからJavaScriptに移行する」でした。栄枯盛衰を感じます。「CoffeeScript」でググろうとしたらGoogleさんが「CoffeeScript オワコン」とサジェストしてきて悲しい気持ちになりました。
明日は@todays-mitsuiさんで「Ramda とか?について」です。
はじめに
今回はGithubの33個のJavaScriptの概念という記事がかなり良記事だったので、その記事に乗っていたサイトたちを元にそれぞれの章の解説を書いてみました。
これらの概念を知らなくてもJavaScriptを書くことは多分出来ると思いますが、知っておくと何かと便利かと思います。
元サイトたちは参考文献に載せておくのでさらに詳しく知りたい人はそちらを見てください。
もう2018年も終わってしまうというこのタイミングでES5を念頭においたコードを書くということは無いと思うので、例や解説などは全てES2015(ES6)以降を想定して書いておきます。過去の遺産についてはあまり触れません。インツァァネッツォ✝️イクス✝️プローラー?
IE「あ,お久しぶり!…またブラウザの検索?」
— So Takamoto (JK) (@tkmtSo) 2013年3月7日
自「いや,IEからでないと見られないページがあって」
IE「そうなんだ! …ねえ,通常使うブラウザにする?」
自「いいよ,今のままで」
IE「そう…あと初期設定が済んでないけど,」
自「いいよ,また今度で」
IE「そっか…」
なお、文中では#00という表記を00章の意味で用いています。#0だと「0. ES2015以降の書き方についてのおさらい」を指します。
ライセンス&クレジット
この記事はCC BY 4.0ライセンスで提供されます。ご利用はご自由にどうぞ。
文責:わたせ
Twitter: @tsin1rou Qiita: @tsin1rou
誤字脱字の他、根本的な誤り、もっと良い実装や例の提案などがあればコメント欄もしくは上記twitterまでおしらせいただけると幸いです。
なんとJavaScriptの知識にかけてはこの人の右に出るものはいないと言われているazuさんにチェックしていただくことができました。週ごとにJavaScriptの最新情報を届けてくれるJSer.infoの運営やJavaScriptの入門書の執筆などをされていらっしゃる方です。本当にありがとうございます。
チェックしていただいた後に書き加えた部分もあるのでミスがあればだいたいその部分です。
後編について
あまりにも長くなったので前編と後編に分けました。後編の内容はこんな感じです。
-
- Prototype Chain
-
- Object.create & Object.assign
-
- Array.prototypeの便利な関数たち
-
- サイドエフェクトと純粋関数
-
- クロージャー
-
- 高階関数(HOF)
-
- 再帰
-
- コレクションとジェネレーター
-
- Promise
-
- async/await
-
- データ構造
-
- 計算時間
-
- アルゴリズム
-
- ポリモーフィズム
-
- デザインパターン
-
- カリー化と部分適用
-
- 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 関数名 = (
引数
)=>{ 処理 }
と書きます。こうすることで関数名が被ってもエラーが出てお知らせしてくれるようになりました。ま、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
true
とfalse
-
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"
x
とy
を変えてもa
とb
は変わりません。これは当たり前という感じ。
変数 | 値 |
---|---|
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.の型に合わせて
- Stringへの変換
- Booleanへの変換
- 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
||
&&
!
の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の場合を除く)
また、==
はnull
とundefined
の型変換を行わないという特別ルールがあります。
null == 0 // false
5. ==と===の違い
結論から言うと可能な限り===
を使うべきです。
4.でもみた通り、==
は暗黙の型変換を行うので意図せぬ挙動の原因となります。
===
は同じ型・同じ値の場合のみtrue
を返すのでより厳密な比較ができます。
false == 0 // true
false === 0 // false
0 == "" // true
0 === "" // false
null
とundefined
は==
においてそれぞれにのみ一致します。
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. 文と式
JavaScriptのソースコードはStatement
とExpression
という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にはStatementとしてのfunctionとExpressionとしてのfunctionがありますが、StatementとしてのfunctionはJavaScriptの黒歴史として葬り去られたので基本的に使用しません。(function)
-> 関数宣言 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"
しかし今ではモジュールがありますし、またconst
やlet
はブロックスコープが適用されるので即時関数の出番はほぼ無くなったと言えるでしょう。
const hogehoge = 'hoge';
{
const hogehoge = 'fuga';
console.log(hogehoge); // -> "fuga"
}
console.log(hogehoge); // -> "hoge"
モジュール
この章の例はあくまでモジュールを紹介するためだけに書きました。多くの場合、これを参考にするのではなく#14で紹介するclass
を使う方が妥当かと思われます。
モジュールは別ファイルに分けて作成します。
let cnt = 0;
export const inc = () => ++cnt;
別ファイルでexportしたものをimportで呼び出すことができます。
import { inc } from './hogehoge.js';
console.log(inc()); // -> 1
console.log(inc()); // -> 2
名前空間を分けたい場合は次のように書きます。
import * as counter from './hogehoge.js';
console.log(counter.inc()); // -> 1
console.log(counter.inc()); // -> 2
import { inc as hogehogeIncrement } from './hogehoge.js';
console.log(hogehogeIncrement()); // -> 1
console.log(hogehogeIncrement()); // -> 2
上記のようなhogehoge.js
の書き方をすると内部の変数は同じものになっていて、どこから呼び出しても同じ値が返ってきます。
let cnt = 0;
console.log('hogehoge');
export const inc = () => ++cnt;
import { inc } from 'hogehoge.js';
console.log('foo');
inc();
import { inc } from 'hogehoge.js';
console.log('bar');
inc();
import './foo.js';
import './bar.js';
// -> 'hogehoge'
// -> 'foo'
// -> 1
// -> 'bar'
// -> 2
インスタンスを切り分けたい場合は次の節に出てくるclass
を利用します。
export default
モジュールが1つのオブジェクトしかexport
しない場合はexport default
を使うことが多いです。
class Hogehoge {
constructor(){
this.count = 0;
}
inc(){
return ++this.count;
}
}
export default Hogehoge;
import Hoge from './module';
const counter = new Hoge();
console.log(counter.inc()); // -> 1
console.log(counter.inc()); // -> 2
上記のようにclassのexport
でよく使うように思います。
import
とは違い、require()
にはdefault
を標準で読み込んでくれるような機能はないので、何らかの事情でrequire()
を使ってdefault
をimport
する場合は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
とかのタイマー系関数は指定時間経過後にイベントループがタイマーフェーズに戻ってくるまで実行されません。
コールバックというのはまあ要するに関数の事で、関数の実行が終わった際に実行する別の関数の事を特別にそう呼びます。相手に電話して「〜〜のタイミングで〇〇に掛け直してね」と言うイメージ。
例えば、A
B
C
D
の4つのタイマーをセットした場合を考えてみます。
setTimeout(A, 100);
setTimeout(B, 200);
setTimeout(C, 300);
setTimeout(D, 400);
タイマーがセットされると、登録されたコールバックはタイマーフェーズ用のキューに昇順で並べられます。
A | B | C | D |
---|
さて、main.jsの他の処理が終わるまで時間がかかり、タイマーフェーズが始まる際にすでに250ms経過していたとします。すると今回のタイマーフェーズではA
B
が実行され、C
の時間をチェックしてまだ到達していなければそこでタイマーフェーズが終了します。他にも、「Timerフェーズで使っていい時間」の制限もあるため、あまりにも長い処理を行なった場合は残りのタイマーは(時間がきていたとしても)スキップされ、次のタイマーフェーズまで先送りされます。
未解決のコールバック
イベントループの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
setTimeout
とsetInterval
は共にブラウザで使用できるタイマーです。あまりにも古い関数なので引数の順番がキモい。
setTimeout(callback, delay)
という感じで使います。delay
ミリ秒以降にcallbackが実行されます。
setInterval(callback, interval)
の方はinterval
ミリ秒ごとにcallbackが実行されます。
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)
とします。
イベントループはコールスタックが一度空になってから動き始めるのでエラーのデバッグが若干面倒です。
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エンジンはまずソースコードの構文解析を行い、抽象構文木を構築します。
var a;
{
"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. ビット演算子
コンピューターは0
と1
で成り立っているのですが、その2進数表現で出てくる演算がビット演算です。
JavaScriptにもビット演算子が存在します。何に使うのか分かりませんが。競技プログラミングとか組み込み系とかをJavaScriptでやる人がいれば役立つかもしれません。
&
ANDというのは2つの入力が共に1
だったら1
を出力する演算です。
入力1 | 入力2 | 出力 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
&
演算子はこのAND演算を2進数のそれぞれの桁に対して行います。
例えば3&6
を考えてみると、3
は0011
であり6
は0110
なので
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は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は入力を反転する演算です。マイクラで言うとレッドストーントーチです。
入力 | 出力 |
---|---|
0 | 1 |
1 | 0 |
8 | 4 | 2 | 1 | 10進数 | |
---|---|---|---|---|---|
入力 | 0 | 0 | 1 | 1 | 3 |
出力 | 1 | 1 | 0 | 0 | 12? |
上のように4bitでの演算であれば~3
は12
になるように思えます。しかし実は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演算は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
シフト演算子
シフト演算子<<
>>
>>>
を使えばビットを左右に動かすことができます。
例えば9
は1001
なので9<<2
は100100
、つまり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
なので4
、rwx
だと111
で7
となります。
特定のフラグが立っているかどうかを調べるには&
を使います。
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になるので。)
// 自分自身としか一致しないユニークなオブジェクトを生成する
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
の前に書きます。するとエディタが下のような情報を出してくれます。
また、下のようにメソッドの前に書けば...
/**
* 挨拶します。
* @returns {String} hello sentence
*/
sayHello(){
return `Hello! I am ${this.name}.`;
}
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
call
もapply
も呼び出す際に何が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
をコンストラクタとして新しいオブジェクトを生成します。
個人的にはnew
はclass
と常にセットで使用し、それ以外の場合は使用しないという取り決めが好きです。
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
後編へ続く
参考文献(前編)
- 33 Fundamentals Every JavaScript Developer Should Know
- GitHub - leonardomso/33-js-concepts: 📜 33 concepts every JavaScript developer should know.
- この書籍について · JavaScriptの入門書 #jsprimer
- JavaScript初級者から中級者になろう — uhyohyo.net
- GitHub - getify/You-Dont-Know-JS: A book series on JavaScript. @YDKJS on twitter.
- ES2015(ES6) 入門 - Qiita
- イマドキのJavaScriptの書き方2018 - Qiita
- Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks & more
- Primitive Types | Flow
- Explaining Value vs. Reference in Javascript – codeburst
- 【Javascript】値渡しと参照渡しについてあらためてまとめてみる - Qiita
- JavaScript type coercion explained – freeCodeCamp.org
- What you need to know about Javascript's Implicit Coercion - DEV Community 👩💻👨💻
- JavaScript — Double Equals vs. Triple Equals – codeburst
- The old "var"
- JavaScript初級者のためのコーディングガイド - Qiita
- 本当にvarは駄目な子なのか? - Qiita
- 文と式 · JavaScriptの入門書 #jsprimer
- JavaScript Statements Reference
- JavaScriptの { } を理解する - Qiita
- {x: 1} は何と見なされるか —— JavaScriptコンソールによる違い - Qiita
- When (and why) you should use ES6 arrow functions — and when you shouldn’t
- 関数宣言 vs 関数式 | ES2015+ - Qiita
- Do ES6 Modules make the case of IIFEs obsolete? - Hashnode
- 関数式 - JavaScript | MDN
- Essential JavaScript: Mastering Immediately-invoked Function Expressions
- ES6のexportについて - Qiita
- JavaScriptのexport defaultアンチパターンについて、検証してみた - Qiita
- export defaultは import { default }で受け取れる - Qiita
- Node.js event loop workflow & lifecycle in low level | Void Canvas
- Node.jsでのイベントループの仕組みとタイマーについて - 技術探し
- JavaScript Event Loop Explained – Frontend Weekly – Medium
- The Node.js Event Loop, Timers, and process.nextTick() | Node.js
- Concurrency model and Event Loop - JavaScript | MDN
- Node.jsのイベントループを理解する | POSTD
- Philip Roberts: What the heck is the event loop anyway? | JSConf EU - YouTube
- What is the JS Event Loop and Call Stack? · GitHub
- The JavaScript Event Loop
- Timers | Node.js v11.1.0 Documentation
- Canvas と requestAnimationFrame でアニメーション - Qiita
- Abstract Syntax Tree · Understanding V8 for Javascript Developers
- Understanding V8’s Bytecode – DailyJS – Medium
- JavaScriptでASTを使ってコードをインジェクションしてみる - Qiita
- JavaScript AST Walker
- v8 "Launching ignition and Turbofan" 和訳 - kakts-log
- Node8.3.0でデフォルトになるTF/Iに関わるベンチマークについて - 技術探し
- Ignition: An Interpreter for V8 [BlinkOn] - Google スライド
- Franziska Hinkelmann: JavaScript engines - how do they even? | JSConf EU 2017 - YouTube
- ビット演算子 - JavaScript | MDN
- JavaScript Bitwise Operators - w3resource
- Programming with JS: Bitwise Operations – Hacker Noon
- Using JavaScript’s Bitwise Operators in Real Life – codeburst
- ビット演算入門 - Qiita
- 実際のところ「ブラウザを立ち上げてページが表示されるまで」には何が起きるのか - Qiita
- Document - Web API インターフェイス | MDN
- Element - Web API インターフェイス | MDN
- ライブラリを使わない素のJavaScriptでDOM操作 - Qiita
- Better JavaScript with ES6, Pt. II: A Deep Dive into Classes ― Scotch
- 【JSでデザインパターン】ファクトリ編 - Qiita
- JavaScriptオブジェクトの作成パターン ES6時代のベストプラクティス - WPJ
- ES2015 の Class で private なインスタンス変数 - Qiita
- JavaScript の this を理解する多分一番分かりやすい説明 - Qiita
- JavaScriptの「this」は「4種類」?? - Qiita
- アロー関数 - JavaScript | MDN
- JavaScript For Beginners: the ‘new’ operator – codeburst
- instanceof - JavaScript リダイレクト 1 | MDN