ES2015以降のJavaScriptにキャッチアップする
教材
変数について
const/letを使う。
varは使わない。
constは再代入不可能な変数で、正確には定数ではないらしい。
たとえばオブジェクトをconstで定義すると、参照先のアドレスは一緒だが、中身が違うということが起こる。
// `const`でオブジェクトを定義している
const object = {
key: "値"
};
// オブジェクトそのものは変更できてしまう
object.key = "新しい値";
エラーの見方。
雰囲気で今まで見ていたけど、改めてちゃんと説明されると、規則性がちゃんとしている
SyntaxError: missing ) after argument list[詳細] index.js:1:13
^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^^
エラーの種類 | | 行番号:列番号
エラー内容の説明 ファイル名
型について
JavaScriptは動的型付け言語に分類される言語であるため、静的型付け言語のような変数の型はありません。 しかし、文字列、数値、真偽値といった値の型は存在します。 これらの値の型のことをデータ型と呼びます。
データ型はプリミティブ型とオブジェクトに大別される。
プリミティブ型は数値や文字列、真偽値などの基本となるデータ型。
オブジェクトはデータの集合体で、参照を経由して操作する。
- プリミティブ型(基本型)
- 真偽値(Boolean): trueまたはfalseのデータ型
- 数値(Number): 42 や 3.14159 などの数値のデータ型
- 巨大な整数(BigInt): ES2020から追加された9007199254740992nなどの任意精度の整数のデータ型
- 文字列(String): "JavaScript" などの文字列のデータ型
- undefined: 値が未定義であることを意味するデータ型
- null: 値が存在しないことを意味するデータ型
- シンボル(Symbol): ES2015から追加された一意で不変な値のデータ型
- オブジェクト(複合型)
- プリミティブ型以外のデータ
- オブジェクト、配列、関数、正規表現、Dateなど
なおnullにはtypeofがobjectになるバグがある
console.log(typeof null); // => "object"
テンプレートリテラルを使うと、文字列の中に変数を${}
を使った挿入ができる
const str = "文字列";
console.log(`これは${str}です`); // => "これは文字列です"
テンプレートリテラルはあと改行がそのまま入れられる
演算子について
厳密等価演算子(===)と等価演算子(==)は常に厳密等価演算子を使えばよさそう。
等価演算子は両辺の型が同じときはいいけども、暗黙の型変換で右辺をキャストするので、意図しない結果が生じる。
// 文字列を数値に変換してから比較
console.log(1 == "1"); // => true
// "01"を数値にすると`1`となる
console.log(1 == "01"); // => true
// 真偽値を数値に変換してから比較
console.log(0 == false); // => true
// nullの比較はfalseを返す
console.log(0 == null); // => false
// nullとundefinedの比較は常にtrueを返す
console.log(null == undefined); // => true
文字列に対して、2回NOT演算子を重ねると、空文字だった場合falseになるので、空文字判定に使うことができる。
自分で使うことはないだろうけど、昔何かのコードで見たことがある気がする
const str = "";
// 空文字列はfalsyであるため、true -> falseへと変換される
console.log(!!str); // => false
JSは動的型付け言語なので、色々勝手に型変換するのだが、その際に矛盾が出ても、undefined
があるので、カバーできている(できている?)。
undefined
自体はリテラルでもなんでもなく、JS内部で定義されているグローバル変数。
(nul
lはリテラル)
NaN(Not a Number)は型はNumber
型
関数と宣言
基本
// 関数宣言
function 関数名(仮引数1, 仮引数2) {
// 関数が呼び出されたときの処理
// ...
return 関数の返り値;
}
// 関数呼び出し
const 関数の結果 = 関数名(引数1, 引数2);
console.log(関数の結果); // => 関数の返り値
戻り値のない(void)関数でも、returnするとundefinedを返す
function fn() {
// 何も返り値を指定してない場合は`undefined`を返す
return;
// すでにreturnされているため、この行は実行されません
}
console.log(fn()); // => undefined
returnを省略してもundefinedは返す
function fn() {
}
console.log(fn()); // => undefined
呼び出し時の引数が少ないとき、指定されなかった仮引数はundefinedとして渡される。
逆に多いと、多かった引数を無視して実行される。エラーにはならない。
ES6からデフォルト引数が使える
またES6から Rest parametersで可変長引数が扱える
function fn(...args) {
// argsは引数の値が順番に入った配列
console.log(args); // => ["a", "b", "c"]
}
fn("a", "b", "c");
元々JSにはargumentsという可変長引数が扱える仕組みがあったが、仕様的にガバい点が多かったので、ES6以降は非推奨となった
JSにおいて関数はオブジェクトなので、関数式というものが使える。
が、ES6以降ではアロー関数が登場したので、そっちを使った方が良さそう。
// 関数式
const 関数名 = function() {
// 関数を呼び出したときの処理
// ...
return 関数の返り値;
};
アロー関数
// Arrow Functionを使った関数定義
const 関数名 = () => {
// 関数を呼び出したときの処理
// ...
return 関数の返す値;
};
色々省略できる
// 仮引数の数と定義
const fnA = () => { /* 仮引数がないとき */ };
const fnB = (x) => { /* 仮引数が1つのみのとき */ };
const fnC = x => { /* 仮引数が1つのみのときは()を省略可能 */ };
const fnD = (x, y) => { /* 仮引数が複数のとき */ };
// 値の返し方
// 次の2つの定義は同じ意味となる
const mulA = x => { return x * x; }; // ブロックの中でreturn
const mulB = x => x * x; // 1行のみの場合はreturnとブロックを省略できる
アロー関数には次の特徴がある。
- 名前をつけることができない(常に匿名関数)
- thisが静的に決定できる(詳細は「関数とスコープ」の章で解説します)
- functionキーワードに比べて短く書くことができる
- newできない(コンストラクタ関数ではない)
- arguments変数を参照できない
基本的にはアロー関数で書けるときはアロー関数で書く、がいいよう
文と式
式(Expression)を簡潔に述べると、値を生成し、変数に代入できるものを言います。
// 1という式の評価値を表示
console.log(1); // => 1
// 1 + 1という式の評価値を表示
console.log(1 + 1); // => 2
// 式の評価値を変数に代入
const total = 1 + 1;
// 関数式の評価値(関数オブジェクト)を変数に代入
const fn = function() {
return 1;
};
// fn() という式の評価値を表示
console.log(fn()); // => 1
文(Statement)を簡潔に述べると、処理する1ステップが1つの文と言えます。JavaScriptでは、文の末尾にセミコロン(;)を置くことで文と文に区切りをつけます。
const isTrue = true;
// isTrueという式がif文の中に出てくる
if (isTrue) {
}
一方で、式(Expression)は文(Statement)になれます。文となった式のことを式文と呼びます。 基本的に文が書ける場所には式を書けます。
その際に、式文(Expression statement)は文の一種であるため、セミコロンで文を区切っています。
式は文になれますが、先ほどのif文のように文は式になれません。
次のような、文を{と}で囲んだ部分をブロックと言います。 ブロックには、複数の文が書けます。
なるほどね
{
文;
文;
}
条件分岐について
ループ
オブジェクト
オブジェクトがどうしても辞書に見えるときがあったんだけど、どうもJSには連想配列という概念がないようで、同じことをしたければオブジェクトを使う、ということのよう。
オブジェクトリテラルのプロパティ名はクォート("や')を省略できる。
今までここで結構混乱していた。
あと分割代入と同じノリで、こんな書き方もできる
const name = "名前";
// `name`というプロパティ名で`name`の変数を値に設定したオブジェクト
const obj = {
name
};
console.log(obj); // => { name: "名前" }
オブジェクトのプロパティへのアクセスはドット記法(.)を使う方法とブラケット記法([])の二通りある。
const obj = {
key: "value"
};
// ドット記法で参照
console.log(obj.key); // => "value"
// ブラケット記法で参照
console.log(obj["key"]); // => "value"
どっちでもいいっちゃどっちでもいいが、ブラケット記法の方がキー名の制約の縛りが緩い。
基本ドット記法でいいみたい。
上述の通り、constでオブジェクト定義しても、オブジェクトの参照は変更不可能だけども、プロパティは変更できる。
変更を封じるにはObject.freeze
を使う。
"use strict";
const object = Object.freeze({ key: "value" });
// freezeしたオブジェクトにプロパティを追加や変更できない
object.key = "value"; // => TypeError: "key" is read-only
存在しないプロパティにアクセスすると、JSはundefinedを返す。
しかしネストしてるプロパティ、例えばkey.child.grandchild
みたいなプロパティにアクセスするときは、
key.child
がundefinedだと、undefinedに対して操作を行うことになるので、TypeError
の例外が返ってくる。
ES2020からの新機能だけども、オプショナルチェーンが使える
const obj = {
a: {
b: "objのaプロパティのbプロパティ"
}
};
// obj.a.b は存在するので、その評価結果を返す
console.log(obj?.a?.b); // => "objのaプロパティのbプロパティ"
// 存在しないプロパティのネストも`undefined`を返す
// ドット記法の場合は例外が発生してしまう
console.log(obj?.notFound?.notFound); // => undefined
// undefinedやnullはnullishなので、`undefined`を返す
console.log(undefined?.notFound?.notFound); // => undefined
console.log(null?.notFound?.notFound); // => undefined
マージする方法はObject.assign
を使う方法と、スプレッド構文を使う方法とがある模様。
const objectA = { a: "a" };
const objectB = { b: "b" };
const merged = {
...objectA,
...objectB
};
console.log(merged); // => { a: "a", b: "b" }
この辺はオブジェクトを連想配列として使うための機能なのかな?
オブジェクトは普通に代入すると、参照を渡すだけになってしまう
Object.assign
を使うと、オブジェクトのコピーができる
// 引数の`obj`を浅く複製したオブジェクトを返す
const shallowClone = (obj) => {
return Object.assign({}, obj);
};
const obj = { a: "a" };
const cloneObj = shallowClone(obj);
console.log(cloneObj); // => { a: "a" }
// オブジェクトを複製しているので、異なるオブジェクトとなる
console.log(obj === cloneObj); // => false
が、これは浅いコピーなので、オブジェクトをネストしたプロパティまではコピーされない。
もし深いコピーがしたければ、開発者側でそういうコードを書く必要がある。
toString()
やhasOwnProperty()
などがどう実現しているかというと、プロトタイプオブジェクトという仕組みがある。
JSのオブジェクトは全てObject
を継承しており、(※継承しない方法もあるが特殊)
Object
はObject.prototype
という特殊なプロパティを持っている。
配列について
任意のオブジェクトが配列かどうかを判断するにはArray.isArray
を使う。typeof
はobjectまでしかわからない。
ES2016からincludes
が使えるようになった
配列の結合はconcat
を使う。
ちなみに演算子を使うと悲しいことになった
const array = ["A", "B", "C"];
const array2 = ["D", "E"];
console.log(array + array2); // A,B,CD,E
ES2015から、スプレッド構文を使うと多少スタイリッシュに書ける
const array = ["A", "B", "C"];
const array2 = ["D", "E"];
console.log([...array, ...array2]); // [ 'A', 'B', 'C', 'D', 'E' ]
インデックス指定での要素の削除はArray#splice
を使う。lengthを使ったやり方もある
const array = [1, 2, 3];
array.length = 0; // 配列を空にする
console.log(array); // => []
引数なしのslice()
とconcat()
で配列のコピーがつくれる
const copiedArray = myArray.slice();
JSの配列操作のメソッドは破壊的(操作対象の配列を変更する)なものがあるが、名前から見分けることはできない
JSの高階関数、コールバックに要素・インデックス・配列そのものの3つの引数をとるとのこと。
思ってたのとちょっと違った
const array = [1, 2, 3];
array.forEach((currentValue, index, array) => {
console.log(currentValue, index, array);
});
// コンソールの出力
// 1, 0, [1, 2, 3]
// 2, 1, [1, 2, 3]
// 3, 2, [1, 2, 3]
(SwiftのForEachはElementだけなので、そっちで慣れていた)
(まあでもコールバックの第二引数以降を無視したらSwiftと一緒か)
タイトルに「ES6」と付けていたが、ES2015が今は正しいらしい。
文字列
JavaScript(ECMAScript)は文字コードとしてUnicodeを採用し、文字をエンコードする方式としてUTF-16を採用しています。
JSの文字列も、文字の配列として操作するインターフェイスがある
正規表現を使うため、二通りの方法がある
// 正規表現リテラルで正規表現オブジェクトを作成
const patternA = /パターン/フラグ;
// `RegExp`コンストラクタで正規表現オブジェクトを作成
const patternB = new RegExp("パターン文字列", "フラグ");
使い分けはまずリテラルで、変数を使いたいなど、ダメな理由があるならRegExp、で良さそう
このように、RegExpコンストラクタは文字列から正規表現オブジェクトを作成できますが、特殊文字のエスケープが必要となります。 そのため、正規表現リテラルで表現できる場合は、リテラルを利用したほうが簡潔でパフォーマンスもよいです。 正規表現のパターンに変数を利用する場合などは、RegExpコンストラクタを利用します。
正規表現、どうしても苦手意識がある
const str = "/正規表現のような文字列/";
// 正規表現で`/`からはじまり`/`で終わる文字列のパターン
const regExpLikePattern = /^\/.*\/$/;
// RegExp#testメソッドでパターンにマッチするかを判定
console.log(regExpLikePattern.test(str)); // => true
// Stringメソッドで、`/`からはじまり`/`で終わる文字列かを判定する関数
const isRegExpLikeString = (str) => {
return str.startsWith("/") && str.endsWith("/");
};
console.log(isRegExpLikeString(str)); // => true
正規表現ドキュメント
正規表現が試せるWebサービス
文字列はimmutableなので、let指定でも変更ができない。つまり削除もできない
String#replace
を使って、新しい文字列をつくることはできる。
空文字にreplaceすることで削除を表現できる……
const str = "文字列";
// "文字"を""(空文字列)へ置換することで"削除"を表現
const newStr = str.replace("文字", "");
console.log(newStr); // => "列"
ただこれ意味あるのかな? よくわからない
イマイチメリットが飲み込めてないんだけど、タグつきテンプレート関数というのがES2015から使えるみたい。
// 変数をURLエスケープするタグ関数
function escapeURL(strings, ...values) {
return strings.reduce((result, str, i) => {
return result + encodeURIComponent(values[i - 1]) + str;
});
}
const input = "A&B";
// escapeURLタグ関数を使ったタグつきテンプレート
const escapedURL = escapeURL`https://example.com/search?q=${input}&sort=desc`;
console.log(escapedURL); // => "https://example.com/search?q=A%26B&sort=desc"
これについては、そういうものがある、って理解で今はいいか
関数とスコープ
ビルトインオブジェクトには、大きく分けて2種類のものがあります。 1つ目はECMAScript仕様が定義するundefinedのような変数(「undefinedはリテラルではない」を参照)やisNaNのような関数、ArrayやRegExpなどのコンストラクタ関数です。2つ目は実行環境(ブラウザやNode.jsなど)が定義するオブジェクトでdocumentやmoduleなどがあります。 どちらもグローバルスコープに自動的に定義されているという点で大きな使い分けはないため、この章ではどちらもビルトインオブジェクトと呼ぶことにします。
グローバル変数とビルトインオブジェクトで同じ変数名を定義したら、グローバル変数が優先して参照される
意図せずにビルトインオブジェクトの変数を隠蔽してしまうリスクを犯さないためにはどうしたらいい?←グローバル変数を極力使わない
これが噂のvarの関数スコープの巻き上げか。。。
かつて(varしかなかった時代)は即時実行関数なんてものもあったらしいが、今やconst/letを使え、で歴史的遺物以外の意味はない模様
// 匿名関数を宣言 + 実行を同時に行っている
(function() {
// 関数のスコープ内でfoo変数を宣言している
var foo = "foo";
console.log(foo); // => "foo"
})();
// foo変数のスコープ外
console.log(typeof foo === "undefined"); // => true
JSは基本的に静的スコープで名前解決をする。
const x = 10; // *1
function printX() {
// この識別子`x`は常に *1 の変数`x`を参照する
console.log(x); // => 10
}
function run() {
const x = 20; // *2
printX(); // 常に10が出力される
}
run();
ただしthisは例外で、動的スコープらしい。
Reactチュートリアルでよくわからなかったところだ
JSのクロージャーの仕組みの解説。
カプセル化とかクロージャーとかその辺のプログラミング思想の話ってイマイチ体系的に学べてない気がする
this
について
トップレベルに記述されたthis
- 実行コンテキストが"Script": グローバルオブジェクト(
window
とかglobal
とか) - 実行コンテキストが"Module":
undefined
関数のthis
はアロー関数とそれ以外で異なる
アロー関数以外の関数
- メソッドじゃない(オブジェクト配下じゃない)関数は、strictモードだとundefined。strictモードじゃないと、グローバルオブジェクトを参照するように変換される親切(?)仕様
- メソッドの
this
はベースオブジェクトを参照する(これはシンプル)。自分が定義されたオブジェクトを返す。ネストされていたとしても、自分の一個上のオブジェクトを返す
thisが問題となる場合
対策
1つはメソッドとして定義されている関数はメソッドとして呼ぶということです。 メソッドをわざわざただの関数として呼ばなければそもそもこの問題は発生しません。
もう1つは、thisの値を指定して関数を呼べるメソッドで関数を実行する方法です。
call、apply、bindメソッドの3つがある
call/applyの違いは関数の引数への値の渡し方が違うだけ。
また、call/applyは元の関数定義を変更する。
bindはthisを束縛した関数を返すので、非破壊的。
Reactでbind使ったのはたぶんそういうこと。
高階関数のコールバックでthisが参照できないの、Swiftでもあった。
const that = this;
みたいにして渡した気がする
JSでは、ES2015でアロー関数が導入されて、アロー関数は自分の外のスコープのthisを探索するので、アロー関数を使えば解決する
クラス(時間なくなってきた)
ES2015からclassが登場した
class Point {
// コンストラクタ関数の仮引数として`x`と`y`を定義
constructor(x, y) {
// コンストラクタ関数における`this`はインスタンスを示すオブジェクト
// インスタンスの`x`と`y`プロパティにそれぞれ値を設定する
this.x = x;
this.y = y;
}
}
今更ながら、メソッドにfunction
が書いてないことに気づいた
ES2015から、メソッドだとfunctionが省略できるらしい
関数とメソッドに大きな違いはない、というけれど、この省略は結構大きい気がする
プロパティにGet/Setが書ける
class クラス {
// getter
get プロパティ名() {
return 値;
}
// setter
set プロパティ名(仮引数) {
// setterの処理
}
}
const インスタンス = new クラス();
インスタンス.プロパティ名; // getterが呼び出される
インスタンス.プロパティ名 = 値; // setterが呼び出される
クラスをnewしてインスタンスオブジェクトをつくるが、その際にインスタンスは元になったクラスのプロトタイプオブジェクトの参照を[[Prototype]]
に持つ。
インスタンスはプロパティ・メソッドを参照する際に[[Prototype]]
まで探索する。
この仕組みをプロトタイプチェーンと呼ぶ。
普段は、プロトタイプオブジェクトやプロトタイプチェーンといった仕組みを意識する必要はありません。 class構文はこのようなプロトタイプを意識せずにクラスを利用できるように導入された構文です。 しかし、プロトタイプベースである言語のJavaScriptではクラスをこのようなプロトタイプを使って表現していることは知っておくとよいでしょう。
JSには言語使用上クラスが存在しないが、実用上クラスの書き方をしていたので、
なんとかしてクラスをJSにも導入できないかということで、プロトタイプベースのやや特殊なクラスの実装になったという経緯があるみたい。
ES2015でclassが導入されたが、内部的には関数オブジェクトなので、単なるシンタックスシュガーであるとも言える
プロトタイプベースで、継承の仕組みも実現している。
次の順で名前解決を図る。
- instanceオブジェクト自身
- Child.prototype(instanceオブジェクトの[[Prototype]]の参照先)
- Parent.prototype(Child.prototypeオブジェクトの[[Prototype]]の参照先)
非同期処理
ちょっと重い内容なので、まずは読む。
catchメソッドはFulfilled状態(成功)のPromiseインスタンスを作成して返す。
これはなんか混乱する……
Promise.reject(new Error("エラー")).catch(error => {
console.log(error); // Error: エラー
}).then(() => {
console.log("thenのコールバック関数が呼び出される");
});
もしRejectedで返したいなら、return Promise.reject(error);
みたいにするのが必要とのこと
非同期処理とArrayの高階関数のコールバックの中でトラブったの、俺もSwiftでやったような気がする。
結局高階関数使うのやめたんだけど、対処法としては合ってたっぽい
非同期処理の実装として、3つあり、古い順に並べると、下記。
- エラーファーストコールバック
- Promise
- Async Function
ES2015まではコールバックによる方法しかなかった。
エラーファーストコールバックはその名のとおり、コールバックの第一引数にエラーを入れて、呼び出し側はまずnullチェックする、というもの。
悪名高いコールバック地獄を生むことにもなった。
その一つの解決方法として、Promiseが導入された。
SwiftのResultに相当すると思われる。
使用例。Promiseは
- Pending: 初期状態
- Fulfilled: 成功
- Rejected: 失敗
の3つの状態を持っている。
/**
* 1000ミリ秒未満のランダムなタイミングでレスポンスを疑似的にデータ取得する関数
* 指定した`path`にデータがある場合、成功として**Resolved**状態のPromiseオブジェクトを返す
* 指定した`path`にデータがない場合、失敗として**Rejected**状態のPromiseオブジェクトを返す
*/
function dummyFetch(path) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (path.startsWith("/success")) {
resolve({ body: `Response body of ${path}` });
} else {
reject(new Error("NOT FOUND"));
}
}, 1000 * Math.random());
});
}
// `then`メソッドで成功時と失敗時に呼ばれるコールバック関数を登録
// /success/data のリソースは存在するので成功しonFulfilledが呼ばれる
dummyFetch("/success/data").then(function onFulfilled(response) {
console.log(response); // => { body: "Response body of /success/data" }
}, function onRejected(error) {
// この行は実行されません
});
// /failure/data のリソースは存在しないのでonRejectedが呼ばれる
dummyFetch("/failure/data").then(function onFulfilled(response) {
// この行は実行されません
}, function onRejected(error) {
console.log(error); // Error: "NOT FOUND"
});
resolve(任意のオブジェクト)で成功したときに渡すデータが指定できて、受ける側は.then(関数)
で成功時の処理を記述する。
then()
は引数にonFulfilled/onRejectedを持っていて、onRejectedを指定すると失敗時/エラー時も記載できる。
エラーだけ書くための構文として、catch()
も用意されている。
Promiseの書き方だけで一冊本書けそうなレベルで色々あるけど、
- チェーンできる
- Promise.allで複数のPromiseをまとめられる
- Promise.raceで複数のPromiseをまとめて、その中でもっとも応答が早いものを採用できる
あたりが大事
Promiseも構文が複雑で、ごちゃごちゃする。
その解決を目指して、ES2017からasync/awaitが導入された。
といってもasyncもPromiseを使っているので、Promiseの挙動を理解する必要はある。
asyncをつけた関数は、下記の挙動となる
- Async Functionが値をreturnした場合、その返り値を持つFulfilledなPromiseを返す
- Async FunctionがPromiseをreturnした場合、その返り値のPromiseをそのまま返す
- Async Function内で例外が発生した場合は、そのエラーを持つRejectedなPromiseを返す
awaitはasync関数の中でだけ使えて、処理の完了まで後続の実行を待つ。
async function asyncMain() {
// PromiseがFulfilledまたはRejectedとなるまで待つ
await Promiseインスタンス;
// Promiseインスタンスの状態が変わったら処理を再開する
}
上手いこと使えば、Promiseチェーンのコールバック地獄を、同期処理のようなスタイルで記述できて気持ちがいい……らしい。
MapとSet
第一部終わり。
第二部まで手動かしながらやると力つくんだろうけど、あとは実戦でやってくつもりなので、スキップで……