問題一覧
1
以下の操作はクラス自体にプロパティを割り当てている。 これをクラス独自のやり方で実装するとどうなるか? class User {} User.method = function () { alert(this === User); }; User.method(); // true
クラス全体にメソッドを割り当てることもできます。このようなメソッドは static(静的) と呼ばれます。 クラス宣言の中では、次のように static キーワードを付けます。 class User { static staticMethod() { alert(this === User); } } User.staticMethod(); // true
2
staticメソッドはどういう場合に使用されるか? 例を3つ挙げよ
通常 static メソッドは、クラスには属するが、特定のオブジェクトには属さない関数を実装するのに使用されます。 1. 比較用のメソッド 例えば、Article オブジェクトがあり、それらを比較するための関数が必要とします。 自然な解決策は、静的メソッド Article.compare を追加することです。: class Article { constructor(title, date) { this.title = title; this.date = date; } static compare(articleA, articleB) { return articleA.date - articleB.date; } } // usage let articles = [ new Article("Mind", new Date(2019, 1, 1)), new Article("Body", new Date(2019, 0, 1)), new Article("JavaScript", new Date(2019, 11, 1)) ]; articles.sort(Article.compare); alert( articles[0].title ); // Body ここでは、Article.compare は記事を比較する手段として、記事の “上位” にいます。記事のメソッドと言うよりはクラス全体のメソッドです。 2 ファクトリーメソッド 別の例は、いわゆる “ファクトリー” メソッドです。 記事の作成には複数の方法が必要です: 与えられたパラメータ(title, date など)による作成 今日の日付の空の記事の作成 … 最初の方法はコンストラクタで実装することができます。そして、2 つ目の方法としてはクラスの静的メソッドを作ることができます。 ここでの Article.createTodays() を見てください: class Article { constructor(title, date) { this.title = title; this.date = date; } static createTodays() { // 思い出してください, this = Article return new this("Today's digest", new Date()); } } let article = Article.createTodays(); alert( article.title ); // Todays digest これで今日のダイジェストを作成する必要があるたびに、Article.createTodays() を呼べます。 3 データベース関連クラスの操作メソッド 静的メソッドは、次のように、データベース関連のクラスでデータベースの検索/保存/削除のためにも使用されます。: // Article は記事を管理するための特別なクラスと仮定します // 記事を削除するための static メソッド: Article.remove({ id: 12345 });
3
以下と同じ操作をクラス独自のやり方で実装せよ class Article { } Article.publisher = "Ilya Kantor";
静的プロパティも可能で、通常のクラスプロパティと同じように見えますが、先頭に static が付きます。 class Article { static publisher = "Ilya Kantor"; } alert(Article.publisher); // Ilya Kantor
4
静的プロパティやメソッドを定義したクラスを継承した場合どうなるか?
プロトタイプチェーンが形成されるので、継承先のクラスでも静的プロパティやメソッドを継承し、使えるようになる。 class Animal { static planet = "Earth"; constructor(name, speed) { this.speed = speed; this.name = name; } run(speed = 0) { this.speed += speed; alert(`${this.name} runs with speed ${this.speed}.`); } static compare(animalA, animalB) { return animalA.speed - animalB.speed; } } // Inherit from Animal class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } } let rabbits = [ new Rabbit("White Rabbit", 10), new Rabbit("Black Rabbit", 5) ]; rabbits.sort(Rabbit.compare); rabbits[0].run(); // Black Rabbit runs with speed 5. alert(Rabbit.planet); // Earth
5
継承すると継承元の静的プロパティが利用できるのはなぜか? 以下を例にextendの機能も交えて、クラス側とオブジェクト側の観点で解説せよ class Animal { static planet = "Earth"; constructor(name, speed) { this.speed = speed; this.name = name; } run(speed = 0) { this.speed += speed; alert(`${this.name} runs with speed ${this.speed}.`); } static compare(animalA, animalB) { return animalA.speed - animalB.speed; } } // Inherit from Animal class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } } let rabbits = [ new Rabbit("White Rabbit", 10), new Rabbit("Black Rabbit", 5) ]; rabbits.sort(Rabbit.compare); rabbits[0].run(); // Black Rabbit runs with speed 5. alert(Rabbit.planet); // Earth
クラス側の解説 extendすることで、 Rabbitクラス(関数ともいう)の[[prototype]]がAnimalクラス(関数)になる。 extendsはコンストラクタ関数間のプロトタイプチェーンの掲載 (つまるところ関数オブジェクトのプロパティの継承) とプロトタイプ間のチェーン形成 インスタンス→Rabbit.prototype → Animal.prototype を行う class Animal {} class Rabbit extends Animal {} // 静的 alert(Rabbit.__proto__ === Animal); // true // 通常のメソッド alert(Rabbit.prototype.__proto__ === Animal.prototype); // true
6
オブジェクト指向プログラミングでは、プロパティとメソッドは 2 つのグループに分けられます そのグループとはなにか?
内部インタフェース: クラスの他のメソッドからアクセス可能だが、外側からはアクセスできないメソッドやプロパティ。 外部インタフェース: 外部のクラスからもアクセス可能なメソッドやプロパティ。 コーヒーメーカーで例えるなら、内部に隠されているもの: ボイラーチューブや発熱体など、は内部インタフェースです。 内部インタフェースはオブジェクトが機能するために使われ、その構成要素はお互いに使用されます。例えば、ボイラーチューブは発熱体に取り付けられます。 しかし、コーヒーメーカーの外側からは、誰もそこに届かないよう保護カバーで閉ざされています。 内部の詳細は隠されており、アクセスできません。私たちは、外部インタフェースを介してのみその機能を利用できます。
7
オブジェクトを使用するにあたって注目すべき要素はなにか?
外部インターフェース 内部でどのように動いているか完全に分からないかもしれませんが、問題ありません
8
外部インターフェースや内部インタフェースを実現するために、JavaScriptはどのような種類のプロパティが存在するか? 3種類あげよ
パブリック(public): どこからでもアクセス可能です。これらは外部インタフェースになります。 今まで、私たちはパブリックなプロパティとメソッドのみを使用していました。 プライベート(private): クラス内部からのみアクセスできます。これらは内部インタフェース用です。 プロテクト(protected)” : これは、クラス及び、そのクラスを継承したサブクラスの内部からのみアクセス可能であることを意味します。これも内部インタフェースには役立ちます。通常は、継承しているクラスを適切に拡張できるよう、それらにアクセスさせたいため、ある意味ではプライベートよりも広く知られています。 ※ちなみにJavaScriptでは実装されてないので、言語レベルで制御はされず、運用としての実装になる。。
9
protectedプロパティはどのようにして定義するか?
_をつける。 class CoffeeMachine { _waterAmount = 0; set waterAmount(value) { if (value < 0) { value = 0; } this._waterAmount = value; } get waterAmount() { return this._waterAmount; } constructor(power) { this._power = power; } } // コーヒーメーカーを生成 let coffeeMachine = new CoffeeMachine(100); // 水を追加 coffeeMachine.waterAmount = -10; // _waterAmount は -10 ではなく、 0 になります
10
クラスで読み取り専用のプロパティを作るにはどうするか?
セッターはなしで、ゲッターだけ利用するようにすれば読み取り専用になる。 例:powerプロパティが読み取り専用 class CoffeeMachine { // ... constructor(power) { this._power = power; } get power() { return this._power; } } // コーヒーメーカーを作成 let coffeeMachine = new CoffeeMachine(100); alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W coffeeMachine.power = 25; // Error (setter はないので) プロパティフラグとかでも同じことできるけどこっちのほうがらく
11
以下のコードで、????の部分は何が出力されるか? class CoffeeMachine { // ... constructor(power) { this._power = power; } get power() { return this._power; } } // コーヒーメーカーを作成 let coffeeMachine = new CoffeeMachine(100); alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W alert(coffeeMachine._power); //????
100が出力される。 protectedは言語レベルでサポートされてない。 powerプロパティで操作してね!お願い!ってことやね。
12
プライベートなプロパティ はどうやって実装するか?
プライベートは # から始める必要があります。それらはクラス内部からのみアクセス可能です。 例えば、ここではプライベートな #waterLimit プロパティを追加し、水量をチェックするロジックを別のメソッドに抜き出しています: また、メソッドもプライベートにしている。 class CoffeeMachine { #waterLimit = 200; #fixWaterAmount(value) { if (value < 0) return 0; if (value > this.#waterLimit) return this.#waterLimit; } setWaterAmount(value) { this.#waterLimit = this.#fixWaterAmount(value); } } let coffeeMachine = new CoffeeMachine(); // can't access privates from outside of the class coffeeMachine.#fixWaterAmount(123); // Error coffeeMachine.#waterLimit = 1000; // Error ちなみにクロージャとかweakmapを使うと プライベートなプロパティを実装できるっぽいけど上記の方が楽!!
13
パブリックなプロパティの実装方法は? またプライベートなプロパティとパブリックなプロパティは名前衝突するか?
なにもしなければパブリック。 プライベートフィールドはパブリックなものと衝突しません。 プライベートな #waterAmount とパブリックな waterAmount フィールド両方を同時にもつことができます。 例えば、#waterAmount のアクセサとなる waterAmount を作りましょう。: class CoffeeMachine { #waterAmount = 0; get waterAmount() { return this.#waterAmount; } set waterAmount(value) { if (value < 0) value = 0; this.#waterAmount = value; } } let machine = new CoffeeMachine(); machine.waterAmount = 100; alert(machine.#waterAmount); // Error
14
CoffeeMachineクラスで#waterAmountプロパティを定義し継承した。 以下のアラートはどうなるか? class Nanka extends CoffeeMachine{ method() { alert( this.#waterAmount ); //? } }
プライベートは継承先でも使えない プロテクテドならいける。
15
privateとprotectedどっちがいいの?
protected privateは新しめの機能なので、ブラウザによってはサポートされてなかったりするし、 継承先で使えないという制限は厳しすぎなので、protectedがちょうどいい。
16
privateを利用する上での制限事項をあげよ
this['#name'] は期待通り動作しません。 試したらundefined がかえってきた。 this.#nameはいける。 Private フィールドは特別です。 これは private であることを維持するための、構文上の制限になります。 外側から使われないから内部で気をつけるだけだけど 文字列でアクセスできないのはどうなんだろな Object.entries(obj)とかでプロパティ情報とってきて以下のようにクローンしようとしてもプライベートなプロパティはコピーできないかも? obj[key] = value; いやそもそも外部からアクセスできないか
17
コーヒーマシンの例のように、外部インタフェースと内部インタフェースを切り離すことを、なんというか?
カプセル化
18
カプセル化の利点とは?
1 外部から変更されずらくする。 外部からの変更を意図していないものが勝手に変更された場合、結果は予測不可能です。 2 利用者へ通知せず、内部のコード変更が容易 もし内部インタフェースを厳密に区切ると、クラスの開発者は利用者へ通知しなくても内部のプロパティとメソッドを自由に変更することができます。 3 ドキュメント化しやすい。 実装の詳細が隠されていて、シンプルかつ良くドキュメント化された外部インタフェースが利用可能であることは常に便利です。
19
組み込みであるArrayクラスを継承して、別のクラスを作成した。 以下の例では、powerArrayオブジェクトのarrに対して継承元のメソッドを利用してフィルターを行っている。 戻り値のインスタンスのクラスはpowerArrayか? (戻り値のプロパティにisEmptyは含まれているか?) また、????の部分はどうなるか? class PowerArray extends Array { isEmpty() { return this.length === 0; } } ↓スーパー定義のコンストラクタが使える。 let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false let filteredArr = arr.filter((item) => item >= 10); alert(filteredArr); // 10, 50 alert(filteredArr.isEmpty()); //????
戻り値のインスタンスのクラスはpowerArrayとなる。 Arrayのメソッドなのに、Arrayのインスタンスがかえってくるわけではない! よって????の部分はエラーにはならず処理が動き、emptyどないので、falseとなる。 isEmptyメソッドはクラスメソッド(プロトタイプメソッド)なので、 つまり、返却されたオブジェクトの[[prototype]]がPowerArray.prototype のまま変わってないということがわかる。 なぜこういう動きとなるのか次問題にて解説
20
前問のように filter や map といった組み込みのメソッドは、継承されたクラス と同じクラスの新しいオブジェクトを返しますのは内部的にどうなっているか?
実行コンテキストとなるオブジェクトのconstructorプロパティを利用して、インスタンスしているから。 また、実行コンテキストのconstructorプロパティとは、実行コンテキストであるオブジェクトのクラス(コンストラクタ関数)のことである。 前問の例では以下の通りとなる。 arr.constructor === PowerArray;
21
組み込み関数のmapやfilterなどで オブジェクトのコンストラクタを利用する動作を変更するにはどうするか? 例えば 継承されたオブジェクトではなく、Array型を返却するようにしたいときどうするか? また、Map や Set などの他のコレクションも同様に動作するか?
特別な静的な getter Symbol.species を追加することができます。 getter Symbol.species では、利用するコンストラクタ関数を指定します。 これがあると、filterやmap関数は↑のゲッター関数で指定されたコンストラクタ関数を利用して値を返すようになる! なので、Array型を返したい場合は Arrayクラス=コンストラクタ関数なので、これを指定すればよい。 class PowerArray extends Array { isEmpty() { return this.length === 0; } // 組み込みのメソッドは、これをコンストラクタとして使います!! static get [Symbol.species]() { return Array; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false // filter はコンストラクタとして arr.constructor[Symbol.species] を使って新しい配列を作ります let filteredArr = arr.filter(item => item >= 10); // filteredArr は PowerArray ではなく Array です alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function また、Map や Set などの他のコレクションも同様に動作する。 これらも Symbol.species を使用しています。
22
組み込みクラス同士の継承と 通常のクラス同士の継承との違いをあげよ
通常のクラス同士の継承は、 コンストラクタ関数自体のプロトタイプの継承と コンストラクタ関数.プロトタイプオブジェクトの継承の二つがあった。 だけど組み込み同士の場合 コンストラクタ関数自体のプロトタイプの継承がない。 なので、静的なフィールドは継承されない。 まあ確かにいらないか
23
組み込みクラスを親としたカスタムクラスの継承は静的フィードの継承はある?
それはある。試したら動いたので。 組み込み同士がないだけ。
24
instanceof演算子とはなにか?
左辺のオブジェクトが右辺のクラス(コンストラクタ関数)に属するかチェックする時に利用する演算子。 継承も考慮される。 構文は次の通りです: obj instanceof Class それは obj が Class (または、それを継承しているクラス)に属している場合に true を返します。 class Rabbit {} let rabbit = new Rabbit(); // Rabbit クラスのオブジェクト? alert( rabbit instanceof Rabbit ); // true
25
instanceof演算子の右辺はクラスだが、他にも何が動作するか?
当然コンストラクタ関数もいける。 class の代わり function Rabbit() {} alert( new Rabbit() instanceof Rabbit ); // true また Array のような組み込みクラスでも動作します。: let arr = [1, 2, 3]; alert( arr instanceof Array ); // true alert( arr instanceof Object ); // true
26
obj instanceOf Class は内部的にどのような動作となるか?
左辺のオブジェクトのプロトタイプチェーンのうち、 右辺のClass.prototypeとマッチするかチェックしている。 言い換えると、以下のような比較を行います: obj.__proto__ == Class.prototype obj.__proto__.__proto__ == Class.prototype obj.__proto__.__proto__.__proto__ == Class.prototype ... // いずれかが true の場合、 true が返却されます // そうでない場合、チェーンの末尾に到達すると false を返します
27
instanceof 演算子の内部の振る舞いを変更するには?
右辺のクラス側に、 静的メソッド [Symbol.hasInstance](obj)を追加した場合、比較時、このメソッドが呼ばれるようになる。 そのメソッドの引数は左辺のオブジェクトであり、trueを返せば、プロトタイプチェーンのチェックをせずに戻り値をtrueとすることができる。 例えば、以下では、オブジェクトにとあるプロパティが存在していれば、継承関係なく、trueとみなしてしまっている。 // catEat プロパティをもつものは animal と想定する // instanceOf チェックを設定 class Animal { static [Symbol.hasInstance](obj) { if (obj.canEat) return true; } } let obj = { canEat: true }; alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj)が呼ばれます
28
obj instanceof Classを objA.isPrototypeOf(objB)を利用して書き換えるとどうなる?
objA.isPrototypeOf(objB)はobjAがobjBのプロトタイプチェーンに含まれるかチェックなので、 いいかえると Class.prototype.isPrototypeOf(obj) になる。 オブジェクトでもいけるけど、 プロトタイプ側が利用するメソッドだとと覚えておこう! Class.prototypeがobjのプロトタイプチェーンであるか? っていみ
29
オブジェクトのtoStringメソッドについて、以下のコードがある。アラートされる内容はなにか? let obj = {}; alert(obj); // ??? alert(obj.toString()); //????
[object Object]
30
オブジェクトのインスタンスメソッドであるtoStringメソッド = Object.prototype.toString を、Objectのインスタンスではなく、別の型のオブジェクトやプリミティブを実行コンテキストとして実行するとどうなるか?
だいたいtoStringは配列とかだったら要素が出るようにArray.prototype でオーバーライドされてカスタマイズされている。 しかしObject.prototype.toString をArrayのインスタンスを実行コンテキストとして呼び出すとカスタマイズされてない処理が行われる。 数値の場合、それは [object Number] になります。 真偽値の場合、[object Boolean] になります。 null の場合: [object Null] undefined の場合: [object Undefined] 配列の場合: [object Array] …など (カスタマイズ可能). デモを見てみましょう: // 使いやすくするために toString メソッドを変数にコピー let objectToString = Object.prototype.toString; // これの型はなに? let arr = []; // call関数を使用している。 alert( objectToString.call(arr) ); // [object Array]
31
Object.prototype.toStringについて、 利用するオブジェクトで振る舞いを変更するにはどうするか?
特別なオブジェクトプロパティ Symbol.toStringTag を使用してカスタマイズできます。 例: let user = { [Symbol.toStringTag]: 'User' }; //Object.prototype.toStringを短縮してかいてる alert( {}.toString.call(user) ); // [object User] // user.toString()でも変わんなさそう // 環境固有のオブジェクトとクラスのtoStringTag: alert( window[Symbol.toStringTag]); // window alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest ほとんどの環境固有のオブジェクトには、このようなプロパティがあります。これはいくつかのブラウザ固有の例です。: alert( {}.toString.call(window) ); // [object Window] alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest]
32
{}.toStringはどのような用途で利用できるか?
型やクラスのチェックメソッド
33
型やクラスのチェックができるメソッドや演算子を3種類特徴とともにあげよ
typeof 演算子 プリミティブ型が対象 console.log(typeof 42); // Expected output: "number" {}.toString メソッド プリミティブ, 組み込みオブジェクト, Symbol.toStringTag をもつオブジェクト が対象 instanceof 演算子 オブジェクトが対象 ご覧のように、{}.toString は技術的には “より高度な” typeof です。 そして、instanceof 演算子は、クラス階層を扱っていて継承を考慮したクラスのチェックをしたい場合に本当に輝きます。
34
なぜ下の instanceof は true を返すのでしょう? a が B() によって作られたものでないことは簡単に分かります。 function A() {} function B() {} A.prototype = B.prototype = {}; let a = new A(); alert( a instanceof B ); // true
aはたしかにBコンストラクタ関数によってインスタンスされたオブジェクトではない。 しかし、内部的には a instanceof B の処理は aのプロトタイプチェーンがB.prototype に属しているかチェックしている。 a.__proto__ はA.prototypeである。 また、A.prototype==B.prototype となるように同じ空のオブジェクトを代入していることから a.__proto__ == B.prototype が成り立つ。
35
以下の例でsayHiMixinオブジェクトのメソッドをUserクラスに持たせたい場合はどうするか? // mixin let sayHiMixin = { sayHi() { alert("Hello " + this.name); }, sayBye() { alert("Bye " + this.name); } }; // 使い方: class User { constructor(name) { this.name = name; } }
インスタンスメソッドなので、User.prototypeに対し、 Object.assignでメソッドをアサインさせてやれば良い // メソッドをコピー Object.assign(User.prototype, sayHiMixin); // これで User は sayHi できます new User("Dude").sayHi(); // Hi Dude!
36
ミックスインとはなにか?
継承は一つのオブジェクトからのみ行えるが、 複数のオブジェクトから継承したいってときに利用できる。 JavaScriptは多重継承をサポートしていませんが、Object.assignなどでプロトタイプにプロパティをコピーすることでミックスインが実装できます。
37
文字列で定義されたイベントタイプに対し、関数の登録(onメソッド)、登録した関数の削除(offメソッド)、関数を実行する機能(triggerメソッド)の三メソッドを備えたミックスイン(eventMixin)を定義せよ。 クラスは以下の通りミックスインしたメソッドをデリゲートやラップして利用もできるようなシンプルな設計とする。 // クラスを作成 class Menu { choose(value) { this.trigger("select", value); } } // プロトタイプにmixin を追加 Object.assign(Menu.prototype, eventMixin); let menu = new Menu(); // 選択時にハンドラを呼び出し menu.on("select", value => alert("Value selected: " + value)); // イベントのトリガ => 上のハンドラを実行し次を表示 // Value selected: 123 menu.choose("123"); // 選択された値
まず登録する関数はどのように管理するか? _eventHandersオブジェクトを定義し、 プロパティにはeventNameをもつようにする。 各eventNameプロパティの値には関数をリスト形式で管理する。 要するに以下のように登録できる。 _eventHanders[evemtName] = 呼び出すハンドラのリスト(一つのイベントネームに対し、複数ハンドラを登録可能!) onメソッドは以上の登録処理を行い offメソッドはハンドラのリストから対象ハンドラを削除する。 トリガ機能は、 eventNameを引数にとり、管理している関数を呼び出す。 ハンドラ呼び出す際の引数の渡せるようサポートする。 trigger(eventName, ...args) 答えは以下 let eventMixin = { /** * イベントの購読, 使い方: * menu.on('select', function(item) { ... } */ on(eventName, handler) { if (!this._eventHandlers) this._eventHandlers = {}; if (!this._eventHandlers[eventName]) { this._eventHandlers[eventName] = []; } this._eventHandlers[eventName].push(handler); }, /** * 購読のキャンセル 使い方: * menu.off('select', handler) */ off(eventName, handler) { let handlers = this._eventHandlers && this._eventHandlers[eventName]; if (!handlers) return; for(let i = 0; i < handlers.length; i++) { if (handlers[i] == handler) { handlers.splice(i--, 1); } } }, /** * イベントを生成してデータをアタッチ * this.trigger('select', data1, data2); */ trigger(eventName, ...args) { if (!this._eventHandlers || !this._eventHandlers[eventName]) { return; // イベントに対応するハンドラがない場合 } // ハンドラ呼び出し this._eventHandlers[eventName].forEach(handler => handler.apply(this, args)); } }; つまるところミックスインというのは処理の骨格を実装できる。 それを扱うクラスはそのミックスインした処理をラップしたりして扱うことができる。
38
以下はtry catchの例だが、どのように動作するか? try { setTimeout(function() { noSuchVariable; // スクリプトはここで死にます }, 1000); } catch (e) { alert( "won't work" ); }
setTimeoutは非同期なので、キャッチされない。 スケジュールされた関数の内側の例外をキャッチするためには、その関数の中に try..catch が必要です。: setTimeout(function() { try { noSuchVariable; // try..catch がエラーをハンドリングします! } catch (e) { alert( "error is caught here!" ); } }, 1000);
39
すべての組み込みのエラーに対して、catch ブロック内のエラーオブジェクトは 主に3つのプロパティをもっている。 なにか?
name エラー名です。未定義変数の場合、それは "ReferenceError" です。 message エラー詳細に関するテキストメッセージです。 ほとんどの環境では、その他非標準のプロパティが利用可能です。最も広く使われ、サポートされているのは以下です: stack※非標準 現在のコールスタックです: エラーに繋がったネスト呼び出しのシーケンスに関する情報を持つ文字列です。デバッグ目的で使われます。 try { lalala; // エラー, 変数が宣言されていません! } catch(err) { alert(err.name); // ReferenceError alert(err.message); // lalala is not defined alert(err.stack); // ReferenceError: lalala is not defined at (...スタック呼び出し) // 全体としてエラーを表示する事もできます // エラーは "name: message" として文字列に変換されます alert(err); // ReferenceError: lalala is not defined }
40
エラーオブジェクトをalertなどで文字列に変換するとどうなるか?
// 全体としてエラーを表示する事もできます // エラーは "name: message" として文字列に変換されます alert(err); // ReferenceError: lalala is not defined
41
catchする際、エラーの情報が必要ない場合はどのように記載できるか?
エラーの詳細が必要ない場合、catch はそれを省略できます: try { // ... } catch { // <-- (err) なし // ... }
42
try catch構文はどのような場合に利用されるか?
潜在的なエラーが見込まれる場合 例えば、ネットワーク経由でサーバまたは別のソースから受信したデータをデコードするために JSON.parse(受信データ)を利用したときなど。 受診データが不正であった場合のエラーをキャッチできる。 let json = "{ bad json }"; try { let user = JSON.parse(json); // <-- エラーが起きたとき... alert( user.name ); // 動作しません } catch (e) { // ...実行はここに飛びます alert( "Our apologies, the data has errors, we'll try to request it one more time." ); alert( e.name ); alert( e.message ); }
43
エラーを生成するにはどうするか?
throw 演算子はエラーを生成します。 構文は次の通りです: throw <error object>
44
エラーオブジェクトとして扱うことのできるオブジェクトはなにか?
技術的には、エラーオブジェクトとしてなんでも使うことができます。たとえ、数値や文字列のようなプリミティブでもOKです。しかし、name と message プロパティを持つオブジェクトを使うのがベターです(組み込みのエラーと互換性をいくらか保つために)。 通常は組み込みの Error クラスを継承しているエラーオブジェクトがよい! →instanceof演算子で判断利用が可能のため
45
標準エラーのために用意された組み込みエラーオブジェクトはなにがあるか? またコンストラクタの構文はどうなるか?
Error, SyntaxError, ReferenceError, TypeError などです。我々もエラーオブジェクトを作るのにそれらが使えます。 構文は次の通りです: let error = new Error(message); // or let error = new SyntaxError(message); let error = new ReferenceError(message); // ... 組み込みのエラー(任意のオブジェクトではなく、エラーのみ)では、name プロパティはコンストラクタの名前と全く同じになります。そして message は引数から取られます。 例: let error = new Error("Things happen o_O"); alert(error.name); // Error alert(error.message); // Things happen o_O
46
SyntaxErrorはどういうときにつかうか?
プロパティがあることを期待しているのに、ない場合はSyntaxErrorでいいかも? リファレンスエラーは明示的に投げることなさそう JSON.parseが生成するエラーはSyntaxError です。 try { JSON.parse("{ bad json o_O }"); } catch(e) { alert(e.name); // SyntaxError alert(e.message); // Unexpected token o in JSON at position 0 } そして、我々のケースでは、ユーザは必ず "name" を持っていると仮定するので、name の欠落もまた構文エラーとして扱います。 なので、それをスローするようにしましょう: let json = '{ "age": 30 }'; // 不完全なデータ try { let user = JSON.parse(json); // <-- エラーなし if (!user.name) { throw new SyntaxError("Incomplete data: no name"); // (*) } alert( user.name ); } catch(e) { alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name }
47
単純にトライキャッチを行うことの考慮事項をあげよ また対策方法をあげよ
全てのエラーを黙殺させてしまうため、 想定してないエラーも握りつぶされてしまう可能性がある。 キャッチは想定していたエラーだけを処理し、想定してないオブジェクトは“再スロー” するべき let json = '{ "age": 30 }'; // 不完全なデータ try { let user = JSON.parse(json); if (!user.name) { throw new SyntaxError("Incomplete data: no name"); } blabla(); // 予期しないエラー alert( user.name ); } catch(e) { if (e.name == "SyntaxError") { alert( "JSON Error: " + e.message ); } else { throw e; // 再スロー (*) } }
48
try catch構造におけるもう一つのコード句をあげよ またその説明をせよ
finally を持つ場合があります。 もし存在する場合、それはすべてのケースで実行します。: 拡張された構文はこのようになります。: try { ... コードを実行しようとします ... } catch(e) { ... エラーを処理します ... } finally { ... 常に実行します ... }
49
finally句はどのような時に利用されるか?
finally 句は try..catch の前に何かを開始して、どのような結果であれファイナライズをしたいときに頻繁に使われます。 例えば計測に関しては、正常ケースでも異常ケースでも、最後に計測完了の処理を行う必要がある。 その際にfinallyがつかえる
50
catchをしないtry…finally 構造はどのように動作するか?
エラーはキャッチせず、投げられっぱなしになる。→上位側でキャッチ可能、キャッチしない場合はいつもの感じのエラーとなる。 ただし構文の外に出る前にfinallyが実行される。 catch 句がない try..catch 構造も役立ちます。私たちはここでエラーを正しく処理したくないが、開始した処理が完了したことを確認したいときに使います。 function func() { // (計測など)完了させる必要のあるなにかを開始する try { // ... } finally { // すべてが死んでいても完了させる } } 上のコードでは、try の内側のエラーは常に抜けます。なぜなら catch がないからです。しかし finally は実行フローが外部に移る前に機能します。
51
try..catch の外側で致命的なエラーが起きた場合、そのような出来事に反応する方法はなにがあるか? 例えば以下をできるようにしたい。 エラーをログに記録したり、ユーザーに何かを見せたり(通常はエラーメッセージが表示されません) アラートさせてエラー出たことを開発者にわかりやすくしたり!!
ブラウザでは関数を特別な window.onerror プロパティに代入することができます。それはキャッチしていないエラーの場合に実行されます。 構文: window.onerror = function(message, url, line, col, error) { // ... }; message エラーメッセージ url エラーが起きたスクリプトのURL line, col エラーが起きた行と列番号 error エラーオブジェクト 例: <script> window.onerror = function(message, url, line, col, error) { alert(`${message}\n At ${line}:${col} of ${url}`); }; function readData() { badFunc(); // おっと、何かがおかしいです! } readData(); </script> グローバルハンドラー window.onerror の役割は、通常スクリプトの実行の回復ではありません – プログラミングエラーの場合、恐らくそれは不可能なので開発者にエラーメッセージを送ります。
52
2つのコードの断片を比較してみてください。 1つ目は try..catch のあとにコードを実行するために finally を使います: try { work work } catch (e) { handle errors } finally { 作業場所のクリーンアップ処理 } 2つ目は try..catch の直後にクリーンアップする処理を置きます: try { work work } catch (e) { handle errors } 作業場所のクリーンアップ処理 私たちは、処理が開始された後には、それがエラーかどうかは関係なく必ずクリーンアップが必要です。 finally を使うことの利点はあるでしょうか?それとも両方のコードは同じでしょうか?もし利点がある場合はそれが関係する例を挙げてください。
finallyはtryやcatchブロック内からの他の処理への飛び出しがあったとしても、その前に実行される。 finallyは最後に実行する保証がある。 その違いは、関数内のコードを見ると明らかになります。 もし try..catch の “飛び出し” がある場合、振る舞いは異なります。 例えば、try..catch の中で return がある場合です。finally 句は try..catch が どのような終わり方の場合にでも 動作します。たとえ、return 文経由でさえも。 function f() { try { alert('start'); return "result"; } catch (e) { /// ... } finally { alert('cleanup!'); } } f(); // cleanup! …もしくは次のように throw がある場合: function f() { try { alert('start'); throw new Error("an error"); } catch (e) { // ... if("can't handle the error") { throw e; } } finally { alert('cleanup!') } } f(); // cleanup! ここで finally はクリーンアップを保証します。もし f の終わりにコードをおいた場合は実行されない場合があります。
53
カスタムなエラーオブジェクトを作成する際のベストプラクティスは?
エラーは message, name 、望ましくは stack のような基本のエラープロパティをサポートすべきです。 ですが、他にも独自のプロパティを持つかもしれません。例えば HttpError オブジェクトであれば、 404, 403 もしくは 500 といった値をとる statusCode プロパティです。 JavaScript は任意の引数で throw できるので、技術的にはカスタムのエラークラスは Error から継承する必要はありません。 しかし、継承しているとエラーオブジェクトを識別する obj instanceof Error を使えるようになります。そのため、継承しておくほうのがベターです。 アプリケーションを開発するにつれ、独自のエラーが自然に階層を形成します。たとえば、 HttpTimeoutError は HttpError を継承する、といったように。
54
Jsonを読み込み、データのチェックを行った。 必須のフィールドがない場合やフォーマットが誤っている場合はSyntaxError ではなく、カスタムなエラーを投げたいとする。 このような場合はどうするか? またエラーをキャッチしたときの、該当のクラスのえらーかのチェックはどつやってやるか?
カスタムエラーを作成する。 データのチェック(バリデート)でエラーがおきたので、バリデーションエラーとする。 class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } // Usage function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new ValidationError("No field: age"); } if (!user.name) { throw new ValidationError("No field: name"); } return user; } // try..catch での動作例 try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Invalid data: " + err.message); // Invalid data: No field: name } else if (err instanceof SyntaxError) { // (*) alert("JSON Syntax Error: " + err.message); } else { throw err; // 知らないエラーなので、再スロー (**) } }
55
バリデーションエラーは汎用的なので、もっと詳しくエラーを定義していきたい。 例えば、プロパティがない場合のエラーはどうするか?
プロパティが存在しなかったり、誤ったフォーマット(age が文字列値のような)など。ここで、存在しないプロパティに対するより具体的なクラス PropertyRequiredError を作りましょう。欠落しているプロパティについての追加の情報を保持します。 class PropertyRequiredError extends ValidationError { constructor(property) { super("No property: " + property); this.name = "PropertyRequiredError"; this.property = property; } }
56
前問ではエラーオブジェクトのコンストラクタで this.name に対して文字列で設定していた。 ※どのエラーオブジェクトでも同様にオーバーライドして定義していた。 上記をシンプルに実装するにはどうするか?
バリデーションエラークラスなど、上位エラーオブジェクトでthis.constructor.name;を利用する。 ↑これはオブジェクトの[[prototype]]のもちもののプロパティである。 class ValidationError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } }
57
以下コードでは、エラー発生の可能性がある関数readUserを呼び出している。 構文的にはただしくは、オブジェクト指向の考え方として好ましくない部分があるので、指摘せよ try { ... readUser() // 潜在的なエラーの原因 ... } catch (err) { if (err instanceof ValidationError) { // バリデーションエラーの処理 } else if (err instanceof SyntaxError) { // シンタックスエラーの処理 } else { throw err; // 未知のエラー、再スロー } }
例外を扱う際の考え方として、 例外を出す外側で例外のチェックをする場合は、エラーとしての考え方もより抽象的である方がよい。 例えばreadUserは ValidationErrorやSyntaxErrorを出すが、 これは外側の上位レベルでは、あまり関心のない出来事である。 抽象度を上げて考えると、ただ読み込みに失敗したとだけわかればよい。 そのため、上位レベルではエラーも同様に抽象化するべきである。
58
エラーを抽象化する際に行うテクニックをなんというか? また、どのようなテクニックか?
例外のラッピングと言われるテクニック 汎用的な “データ読み込み” エラーを表現する新しいクラス ReadError を作り、 ValidationError や SyntaxError が内部で発生したらReadErrorを作り再スローする。 ReadError オブジェクトは自身の cause プロパティに元のエラーへの参照を保持させておく。 以下全容 class ReadError extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = 'ReadError'; } } class ValidationError extends Error { /*...*/ } class PropertyRequiredError extends ValidationError { /* ... */ } function validateUser(user) { if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } } function readUser(json) { let user; try { user = JSON.parse(json); } catch (err) { if (err instanceof SyntaxError) { throw new ReadError("Syntax Error", err); } else { throw err; } } try { validateUser(user); } catch (err) { if (err instanceof ValidationError) { throw new ReadError("Validation Error", err); } else { throw err; } } } try { readUser('{bad json}'); } catch (e) { if (e instanceof ReadError) { alert(e); // Original error: SyntaxError: Unexpected token b in JSON at position 1 alert("Original error: " + e.cause); } else { throw e; } } 外部のコードは instanceof ReadError をチェックするだけですんだ!
59
setTimeoutなどの関数を利用しなくても 非同期に実行されるブラウザの処理の例をあげよ、
例えばスクリプトとモジュールの読み込みです 指定された src でスクリプトをロードする関数 loadScript(src) を見てください。: function loadScript(src) { // <script> タグを作りページに追加します // 指定された src のスクリプトの読み込みを開始し、完了時に実行します let script = document.createElement('script'); script.src = src; document.head.append(script); } これは、指定された src をもつ、新たに動的に作成された <script src="…"> をドキュメントに挿入します。ブラウザは自動的に読み込みを開始し、完了時に実行します。 使用例: // スクリプトを読み込み、実行する loadScript('/my/script.js'); スクリプトは “非同期に” 実行され、すぐに読み込みを開始しますが、後で(関数がすでに終了した後)実行されます。
60
loadScript('/my/script.js'); 定義は以下のとおり function loadScript(src) { // <script> タグを作りページに追加します // 指定された src のスクリプトの読み込みを開始し、完了時に実行します let script = document.createElement('script'); script.src = src; document.head.append(script); } などの非同期関数を実行したあとに任意の処理を行いたいときはどうするか?
コールバック処理を実装する! loadScript に2つ目の引数に、スクリプトが読み込まれたときに実行する callback 関数を追加しましょう: ちょっと難しいがスクリプトの読み込みが完了したらonloadイベントがはしるため、そこに関数を登録しておくことで、コールバック処理を実装できる。 function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); document.head.append(script); } loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { alert(`Cool, the ${script.src} is loaded`); alert( _ ); // ロードされたスクリプトで宣言されている関数 });
61
非同期実行された関数がエラーとなってしまう場合がある。 以下処理でエラー時の考慮をいれたい。 どのように実装できるか? function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); document.head.append(script); }
読みこみスクリプトでエラーがあればオンエラーイベントが起動するので、関数を登録する。 正常ケースでも異常ケースでも受け取ったコールバック関数を起動させることで、 コールバック関数内部で 正常ケースと異常ケースを分けて定義させるのがよい。 またJavaScript においてよく使われるテクニックである function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); } これは成功時に callback(null, script) を呼び、それ以外の場合には callback(error) を呼びます。 使用方法: loadScript('/my/script.js', function(error, script) { if (error) { // エラー処理 } else { // スクリプトの読み込みが成功 } });
62
コールバック処理を多重に実装する際以下のようになってしまった。 この現象をなんというか? またどのようにして解決するか? loadScript('1.js', function(error, script) { if (error) { handleError(error); } else { // ... loadScript('2.js', function(error, script) { if (error) { handleError(error); } else { // ... loadScript('3.js', function(error, script) { if (error) { handleError(error); } else { // ...すべてのスクリプトが読み込まれるまで続く (*) } }); } }) } }); 上記のコードでは: 1.js をロードし、エラーがなければ 2.js をロードします。エラーがなければ 3.js をロードします。エラーがなければ – 他の何か (*) を行います。
これは “コールバック地獄” や “破滅のピラミッド” と呼ばれます。 Promiseを使えば解決できる。
63
非同期関数に対して状態の把握や、ハンドリングを行うオブジェクトはなにか?
Promiseオブジェクト
64
Promiseオブジェクトのコンストラクタ構文をかけ
let promise = new Promise(function(resolve, reject) { // executor });
65
promiseオブジェクトのコンストラクタ構文にある、executor関数の説明せよ let promise = new Promise(function(resolve, reject) { // executor (生成コード, "シンガー") });
コンストラクタの引数にはexecutor関数を定義する。 new promise が作成されると、executor は自動的に実行されます。 executorの実行部では、 非同期メソッドを記述し、 JavaScript側から提供される関数 (executor関数実行時、Promseのコンストラクタから渡されてそう。) resolve, rejectを呼び出すことによって、 promiseオブジェクトのstateの管理を行うことができる。 例えばsetTimeoutでresolve 前問であったようなスクリプト読み込みにおける、onload イベントにてresolve
66
executor 関数の引数である二つの関数の説明をせよ
resolve関数は resolve(value)で呼び出す。 ジョブが正常に終了した場合。結果は valueになる。 reject関数は reject(error)で呼び出す。 エラーが発生した場合、error はエラーオブジェクトを指定する。 →これらは、promiseのresultに登録され、コールバック関数でうけとれる。 つまり正常終了とエラーの判断は、 executor関数内で自分で定義する
67
Promoseオブジェクトはどのような内部プロパティ(直接参照できない)をもつか?二つ挙げよ
state – 最初は "pending(保留中)" であり、その後 resolve が呼ばれると "fulfilled(完了)" 、もしくは reject が呼ばれると "rejected(拒否)" に変更されます。 result – 初期値は undefined です。その後、resolve(value) が呼ばれると value に、reject(error) が呼ばれると error になります
68
結局ところexecutor関数では、promiseの状態をどのように変化させるか?
成功ならstate:”fulfilled” result:value(resolve呼ぶ時のパラメータ) 失敗ならstate:”rejected” result:error(reject呼ぶ時のエラーオブジェクト) 例 let promise = new Promise(function(resolve, reject) { // promise が作られたとき、関数は自動的に実行されます // 1秒後、ジョブが "done!" という結果と一緒に完了したことを合図します setTimeout(() => resolve("done!"), 1000); });
69
いままで、promiseオブジェクトでexecutor関数によって非同期処理の状態管理をおこなった。 では、これらの結果をうけてハンドリングしたい場合はどうするか?
.then, .catch, .finally を使って、promiseの結果をうけてから、処理をはじめることがきます。サブスクライブするともいう。 これらは即pendingのプロミスを返すため、以降のコードはそのまま動いていく。 サブスクライブした処理は、 promiseの状態次第でコールバック処理が行われる。 正確には、 promise が pending の場合、.then/catch/finally ハンドラは結果を待ちます。 promiseのstateが変わったら、現在の処理が完了後に処理されるようになる。
70
promiseの.then構文について説明せよ また正常終了の時だけあつかいたいときは?
thenはコールバックを登録するだけで、 待つわけではないので注意!! then() メソッドは常に新しい Promise インスタンスを返す なのでプロミスチェーンのthenは最後までしっかり実行される。 コールバックの登録のみ行われ、それぞれプロミスの状態が変わり次第の実行となる! thenには成功用とエラー用の二つの関数をパラメータとしてわたし、登録できる promise.then( function(result) { /* 成功した結果を扱う */ }, function(error) { /* エラーを扱う */ } ); .then の最初の引数の関数は、promise が解決(resolve)されたときに実行され、resolve(value)のvalueを受け取ります。 .then の2つ目の引数の関数は、promise が拒否(reject)されたときに実行され、reject(エラーオブジェクト) のエラーオブジェクトを受け取ります。 例えば、これは正常に解決された promise の動きです: let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve("done!"), 1000); }); doneの値が下のresultに設定される! // resolve は .then の最初の関数を実行する promise.then( result => alert(result), // 1秒後に "done!" を表示 error => alert(error) // 実行されない ); もし、正常完了の場合だけを扱いたい場合は、.then には引数を1つの関数だけを指定します: let promise = new Promise(resolve => { setTimeout(() => resolve("done!"), 1000); }); promise.then(alert); // 1秒後に "done!" を表示
71
promiseオブジェクトの.catchについて説明せよ
エラーにのみ関心がある場合は、 .catch(function) が使えます。 .then(null, function) と全く同じ。 let promise = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // .catch(f) は promise.then(null, f) と同じです promise.catch(alert); // 1秒後に "Error: Whoops!" を表示 .catch(f) の呼び出しは、.then(null, f) の完全な類似物であり、単に簡略化したものです。
72
promiseオブジェクトの.finallyについて説明せよ
正常でもエラーでも呼び出されるし、 引数なしのハンドラを登録しとけばいいので、なんかの処理のファイナライザをかくのに最適 あとプロミスオブジェクトはおなじのをかえす .finally(f) 呼び出しは、Promise が完了したとき(解決または拒否になったとき)に必ず実行されるという意味で .then(f, f) に似ています。 finally はクリーンアップを実行するのに便利なハンドラです。例えば、結果がどちらであっても、もう不要となったので読み込み中のインジケータを停止するなどです。 finallyに登録するハンドラは引数なし。 このようになります: new Promise((resolve, reject) => { /* 時間のかかる処理を行い、その後 resolve/reject を呼び出す */ }) // 成功か失敗かは関係なく、promise が確定したときに実行されます .finally(() => 読込中のインジケータを停止する ) // したがって、読み込み中のインジケータは結果/エラーを処理する前に必ず停止されます .then(result => 結果を表示する, err => エラーを表示する) 重要! finallyはpromiseオブジェクトをそのまま引き継いで返すので、上記の例の通り.thenや.catchを実行できることを認識せよ! また上記例のfinallyやthenは、待つわけではなく購読するだけで、現在のコード実行はおこなっていく。 なのでfinallyの処理が完了したらthenが動くように購読されている。 finally は promise の結果を処理する手段ではないので、これは非常に便利です。
73
promiseオブジェクトについて FulfilledまたはRejectedの状態をなんというか?
Settledという
74
以下はコールバック機能を実装した関数である。 以下をpromiseを利用して再実装せよ function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`Script load error ` + src)); document.head.append(script); }
関数はpromiseオブジェクトを返すことで、 非同期処理を管理でき、.thenメソッドが使用可能となる。 スクリプトの読み込みやエラーがあったところで、コールバック関数をよんでいたが、resolve, rejectを呼んで、ステータスを変更するだけでよい。 function loadScript(src) { return new Promise(function(resolve, reject) { let script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = () => reject(new Error("Script load error: " + src)); document.head.append(script); }); }
75
function loadScript(src) { return new Promise(function(resolve, reject) { let script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = () => reject(new Error("Script load error: " + src)); document.head.append(script); }); に対し、使用方法を記述せよ
loadScriptをよぶことで、promiseオブジェクトを作成し、 thenメソッドにて、成功時、エラー時の処理を購読! 以下例のように複数のハンドラを定義可能 let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); promise.then( script => alert(`${script.src} is loaded!`), error => alert(`Error: ${error.message}`) ); promise.then(script => alert('Another handler...'));
76
コールバックとpromiseを比較した特徴を説明せよ
Promiseは使いやすい。 Promise では .then を何度でも呼び出すことができ、ハンドラを複数登録できる。
77
以下の構文ではthenの引数にalert関数を渡している。 どのように動くか? let promise = new Promise(function(resolve, reject) { resolve(1); }); promise.then(alert);
以下無名関数とほぼおなじ。 promise.then( result => alert(result), ); alert関数は引数をとり、その引数にはvalueが入ってくるので、alert関数だけでもそりゃ動く。
78
promiseチェーンとはなにか? thenメソッドで返却されるpromiseオブジェクトはどのような状態か
promise.thenによるチェーン構造のことである。 thenメソッドは、promiseオブジェクトを返すので、チェーン化できる。 最初はpending の状態のpromiseである。 購読した関数の処理が全部終わると、 thenメソッドで作成した promiseが更新される。(status:resolvedとして)、返却した値はresultに設定される。 thenにて作成されるプロミスのかわりに、プロミスを返却することもできる。 なので、次の .then はその結果と一緒に呼ばれます。 new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); // (*) }).then(function(result) { // (**) alert(result); // 1 return result * 2; }).then(function(result) { // (***) alert(result); // 2 return result * 2; }).then(function(result) { alert(result); // 4 return result * 2; }); ただ、thenチェーンで非同期メソッドを利用してないので、あんまこれは使われないかも
79
チェーンではなく一つのpromiseオブジェクトに対して複数のハンドラを設定するには?
技術的には単一の Promise に複数の .then を追加することもできます。これはチェーンではありません 例: let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); }); promise.then(function(result) { alert(result); // 1 return result * 2; }); promise.then(function(result) { alert(result); // 1 return result * 2; }); promise.then(function(result) { alert(result); // 1 return result * 2; }); ここで行ったことは、1つの promise に対して複数のハンドラの設定です。これらは結果を相互に渡しません。代わりにそれぞれが独立して処理をします。
80
promiseチェーンにおいて、 非同期メソッドを利用するとしたらどのような作りになるか?
ハンドルにて返す値は プリミティブだけでなく promiseかthenableが指定できる。 promiseかthenableを返すと promiseなら非同期処理の状態と結果が thenで作成されてたpromise に登録される。 thenableならthenメソッドに定義したexecutorが実行され、状態と結果が thenで作成されてたpromise に登録される。 ※thenは同期的にpendingのpromiseオブジェクトを返すので、そのpromiseを更新する感じ。 new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); }).then(function(result) { alert(result); // 1 return new Promise((resolve, reject) => { // (*) setTimeout(() => resolve(result * 2), 1000); }); }).then(function(result) { // (**) alert(result); // 2 return new Promise((resolve, reject) => { setTimeout(() => resolve(result * 2), 1000); }); }).then(function(result) { alert(result); // 4 }); 1秒たったら処理して1秒たったら処理してみたいな感じ
81
サードパーティなどのコードではthenにおけるハンドラ関数でpromiseオブジェクトを返さないことがるらしい。 何を返すことがあるか?
“thenable” オブジェクトという、メソッド .then をサポートしたオブジェクトを返すことがある。 thenableのthenメソッドでは、 非同期処理を記述でき、resolve,reject引数をとり、コールバックとして任意のタイミングで呼び出しできるので、 promiseオブジェクトで定義していた、executorなどの処理と同等の内容を記載でき、代わりとして利用できる。 thenでthenableを返すと内部のほうでthenの処理が実行され、結果が作成されていたpromiseに登録される。 thenを実装しているからといって、そのthenメソッドをこちらで直接呼ぶわけではなく、組み込み処理によって呼ばれて管理されている。 既存のクラスに対して、thenメソッドを付与するだけで使えるのが利点。 また、プロパティや外部変数も利用できるのがメリット 以下のとおり new Promise(resolve => resolve(1)) .then(result => { return new Thenable(result); // (*) }) .then(alert); // 1000ms 後に 2 を表示 この思想は、サードパーティライブラリが彼ら自身の “promise 互換な” オブジェクトを実装できるというものです。それらは拡張されたメソッドのセットを持つことができ、promiseとも互換がある。 Thenableの作り方は以下のとおり class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // function() { native code } // 1秒後に this.num*2 で resolve する setTimeout(() => resolve(this.num * 2), 1000); // (**) } }
82
画像のコードをはどっちのアラートが優先されて表示されるか?
aaaaaaが優先されて表示され、 次にasが表示される。 やっぱexecuterは同期的に動く
83
promiseチェーンを利用する際のベストプラクティスは?
thenに一つは非同期アクションを含めて、 promiseかthenableで返すこと。 thenの中で同期的な処理しかしないなら別のthenや他のコードに混ぜれると思う
84
以下処理を再利用可能な関数へと分割せよ fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) .then(githubUser => new Promise(function(resolve, reject) { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); })) // 3秒後にトリガされます .then(githubUser => alert(`Finished showing ${githubUser.name}`));
フェッチからjson化の流れが二回あるのでloadJson として共通化できる。 thenメソッドの処理を戻り値としてるので、プロミスがかえる function loadJson(url) { return fetch(url) .then(response => response.json()); } loadJsonをラップするGitHub用のユーザ取得メソッドを定義(ここはやらなくてもいいが、やったほうが綺麗な気がする) function loadGithubUser(name) { return loadJson(`https://api.github.com/users/${name}`); } ↑で十分再利用関数として定義できた。 ついでに その後のアバター画像表示処理はそのまま関数宣言におきかえる function showAvatar(githubUser) { return new Promise(function(resolve, reject) { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); }); } 利用する時はこんな感じになる 3000); }); } // Use them: loadJson('/article/promise-chaining/user.json') .then(user => loadGithubUser(user.name)) .then(showAvatar) .then(githubUser => alert(`Finished showing ${githubUser.name}`)); // ... ↑見やすくなった!!
85
promiseのエラーハンドリングについて、catchの動きについて述べよ 以下を例とする。 fetch('https://no-such-server.blabla') // rejects .then(response => response.json()) .catch(err => alert(err)) // TypeError: failed to fetch (エラーメッセージ内容は異なる場合があります) 全てのチェーンでエラーハンドリングをしたい場合はどこに、catchを設置すれば良いか
promise が reject されると、制御は最も近い reject ハンドラに移ります。この動きは実際に非常に便利です。 ご覧の通り、.catch は直後である必要はありません。1つまたは複数の .then の後に現れるかもしれません。 なので、上の例でいうと、 fetch('https://no-such-server.blabla') // この処理でrejects される .then(response => response.json())←のハンドラ実行はスキップされる。 また、サイトはすべて問題ありませんが、レスポンスが有効な JSON でない可能性もあります。すべてのエラーをキャッチする最も簡単な方法はチェーンの末尾に .catch を追加することです。 fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) .then(githubUser => new Promise((resolve, reject) => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); })) .catch(error => alert(error.message)); 通常、この .catch は呼ばれません。ですが、上の promise のいずれかがreject した場合(ネットワーク問題 or 無効な json など)、それをキャッチします。
86
以下の例では、rejectを呼びださず、エラーを投げている。 どのような処理となるか? new Promise(function(resolve, reject) { throw new Error("Whoops!"); }).catch(alert);
executor と promise ハンドラのコードは 見えないtry catchをもっており、投げられたエラーは勝手にrejectしたものとして扱ってくれる! ↑既存コードを変えなくていいので便利!! ただし注意! 暗黙的やtry catch をもっているということで、try の中でのエラーしか扱ってくれない ! どういうことかというと、 非同期の処理はtry内の構文でエラーが発生しても、エラー発生時はtry構文外となってしまう。 →setTimeoutでエラーを発生させたとしても、エラーはキャッチされない。 なので、同期的なエラーしか自動的にrejectされたものとして扱ってくれないので注意 これは次のと同じように動作します: new Promise(function(resolve, reject) { reject(new Error("Whoops!")); }).catch(alert); // Error: Whoops! executor にある "見えない try..catch" はエラーを自動的にキャッチし reject された promise として扱っています。 これは executor だけでなくハンドラの中でも同様です。.then ハンドラの中で throw した場合、promise の reject を意味するので、コントロールは最も近いエラーハンドラにジャンプします。 ここにその例があります: new Promise(function(resolve, reject) { resolve("ok"); }).then(function(result) { throw new Error("Whoops!"); // promise を rejects }).catch(alert); // Error: Whoops! また、これは throw だけでなく同様にプログラムエラーを含む任意のエラーに対してです: new Promise(function(resolve, reject) { resolve("ok"); }).then(function(result) { blabla(); // このような関数はありません }).catch(alert); // ReferenceError: blabla is not defined 最後の .catch は明示的な reject だけでなく、上記のハンドラのような偶発的なエラーもキャッチします。
87
以下のようなコードの場合どう動くか? new Promise(function(resolve, reject) { throw new Error("Whoops!"); }).catch(function(error) { alert("The error is handled, continue normally"); }).then(() => alert("Next successful handler runs"));
キャッチされてから、promiseが戻り、 そのままthenの処理が行われる。 .catch の中で throw する場合、制御は次の最も近いエラーハンドラに移ります。 そして、エラーを処理して正常に終了すると、次に最も近い成功した .then ハンドラに続きます。
88
以下処理はキャッチした際に、再スローをおこなっている。 その後どう動くか? new Promise(function(resolve, reject) { throw new Error("Whoops!"); }).catch(function(error) { // (*) if (error instanceof URIError) { // エラー処理 } else { alert("Can't handle such error"); throw error; // ここで投げられたエラーは次の catch へジャンプします } }).then(function() { ここうごく?? }).catch(error => { // (**) alert(`The unknown error has occurred: ${error}`); // 何も返しません => 実行は通常通りに進みます });
thenのここうごく??のところは動かず、スルーされる、 エラーの場合は一番近いcatchがうごく!
89
エラーが処理されない場合何がおきるでしょう?例えば、次のようにチェーンの終わりに.catch を追加し忘れている場合です: new Promise(function() { noSuchFunction(); // ここでエラー(このような関数はない) }) .then(() => { // 1つ以上の成功した promise ハンドラ }); // .catch が末尾にありません!
いったん同期的な処理が全部実行される。 そのあとマイクロタスクキューをさがしてキャッチされてないなーってわかれば、 スタックされたままのrejectとなり、通常のエラーとして扱われ、コンソールでエラーが出力される。 しかし、非同期処理がエラーになるだけなので処理落ちはしないはず エラーの場合、promise は “rejected” になり、実行は最も近い reject ハンドラにジャンプします。ですが、上の例にそのようなハンドラはありません。そのため、エラーは “スタック” します(行き詰まります)。 実際、コード内の通常の未処理のエラーと同様、このような場合は何かが誤っていることを意味します。 通常のエラーが発生し、try..catch でキャッチされない場合何が起こるでしょうか?スクリプトはコンソールにメッセージを表示し終了します。同様のことが、未処理の promise の reject でも発生します。 JavaScript エンジンはこのような reject を追跡し、その場合にはグローバルエラーを生成します。上の例を実行すると、コンソールでエラーを見ることができます。
90
前問のような未処理のrejectにたいして、グローバルエラーが発生したときに、任意の処理を起動させるためなにができるか?
ブラウザでは、イベント unhandledrejection を使ってキャッチできます。: イベントエラーハンドリングされていない Promise が拒否されたときにグローバルスコープに送られるイベントです! window.addEventListener('unhandledrejection', function(event) { // イベントオブジェクトは2つの特別なプロパティを持っています: alert(event.promise); // [object Promise] - エラーを生成した promise alert(event.reason); // Error: Whoops! - 未処理のエラーオブジェクト }); new Promise(function() { throw new Error("Whoops!"); }); // エラーを処理する catch がない このイベントは HTML 標準 の一部です。 エラーが発生し .catch がない場合、unhandledrejection ハンドラが発火し、エラーに関する情報を持っている event オブジェクトが渡ります。なので、その情報を使い、何かをすることができます。 通常、このようなエラーはリカバリ不可なので、最善の方法はユーザにその問題を知らせ、サーバへそのインシデントについて報告することです。
91
.catch はトリガされると思いますか?またその理由を説明できますか? new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert); どのように書き直せばよいか?
解答: いいえ、実行されません: new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert); チャプターの中で言った通り、関数コードの周りには "暗黙の try..catch" があります。そのため、すべての同期エラーは処理されます。 しかし、ここではエラーは executor が実行中でなく、その後に生成されます。したがって、promise はそれを処理できません。 通常のエラーとして扱われてしまう。 期待した動作をさせたいなら、setTimeoutで渡す関数の中にtry catch構文をかき、 キャッチしたらrejectするようにせよ。
92
Promise.allを利用するとなにができるか?
promise.allは同期的に全ての処理完了待つわけではなく、 引数のイテラブルからpromiseを一つにまとめ、新しいpromiseとして管理する。 全てのpromiseが完了したタイミングで、promiseのステータスやリザルト値が更新される。 このpromiseのresultは、結果の配列が設定される。 なので、このpromise にthenなどでコールバックを登録することで、 全ての非同期処理を並列に実行し、全てが完了したタイミングでコールバックを起動することができる! 並列に複数の promise を実行し、すべてが準備できるまで待ちたいとしましょう。 例えば、平行で複数の URL をダウンロードし、すべてが完了したらコンテンツを処理する場合です。 これが Promise.all の目的です。 構文は次の通りです: let promise = Promise.all(iterable); Promise.all は iterable(反復可能, 通常は promise の配列)を取り、新しい promise を返します。 新しい promise は、配列の各 promise がすべて解決され、それらの結果を配列に持つと解決(resolve)されます。
93
以下コードはどのようにうごくか? Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 3000)), // 1 new Promise((resolve, reject) => setTimeout(() => resolve(2), 2000)), // 2 new Promise((resolve, reject) => setTimeout(() => resolve(3), 1000)) // 3 ]).then(alert);
並列で動き、三秒後に全て終わる。 promiseがかえってくるので、.thenでresult値をアラートする。 result値は配列となっており、順番も定義順となるので [1,2,3]となる。
94
Promise.allのよくある使い方として、 複数のurlからデータ取得を並行で行う際の例を示せ 複数のurlは配列で管理されているものとする
fetch関数は非同期実行を行う関数でpromiseを返す関数である。 配列をmap関数で、それぞれfetch関数を実行し、promiseの配列を取得する。 その後、Promise.allで確認する。 一般的なやり方は、処理データの配列を promise の配列にマップし、それを Promise.all にラップすることです。 例えば、URL の配列がある場合、次のようにしてすべてをフェッチできます: let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://api.github.com/users/jeresig' ]; // 各 url を promise の fetch(github url) へマップする let requests = urls.map(url => fetch(url)); // Promise.all はすべてのジョブが解決されるまで待ちます Promise.all(requests) .then(responses => responses.forEach( response => alert(`${response.url}: ${response.status}`) ));
95
Promise.allで、いずれかのpromiseがrejectされた場合はどうなるか?
いずれかの promise が reject された場合、Promise.all により返却された promise は即座にエラーと一緒に reject します。 つまり、Promise.allによって返却されるpromiseはrejectされた状態となる。 また、その他のresolveしたpromiseは無視される。つまり、rejectされた時点で完了をまたなくなる。 そのため、catchすることができる。 && みたいなかんじ 例: Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).catch(alert); // Error: Whoops! ここでは、2つ目の promise が2秒後に reject されます。それは即座に Promise.all の reject に繋がるため、catch が実行されます。: reject されたエラーは Promise.all 全体の結果になります。 エラー時の場合, 他の promise は無視されます ある promise が reject がされると、Promise.all は即座に reject し、配列の他の promise を完全に忘れます。これらの結果は無視されます。
96
Promise.all(iterable) で、通常はpromiseの配列を扱うが、 promiseでない配列の場合はどうなるか?
そのまま戻り値のPromiseオブジェクトのresolveに結果が格納される。 通常、Promise.all(iterable) は promise の iterable (ほとんどの場合は配列)を受け付けます。しかし、もしそれらのオブジェクトが promise ではない場合、Promise.resolve でラップします。 →executorやハンドラでのプリミティブでの返却はラップされる! 例えば、ここでは結果は [1, 2, 3] になります: Promise.all([ new Promise((resolve, reject) => { setTimeout(() => resolve(1), 1000) }), 2, // Promise.resolve(2) と扱われる 3 // Promise.resolve(3) と扱われる ]).then(alert); // 1, 2, 3 したがって、必要に応じて準備済みの値を Promise.all に渡せます。
97
Promise.allだとrejectされてしまうとその時点で待機がおわり、結果のみが返却されてしまう。 rejectも含め、他のpromiseも終わるまで待機させたい場合は?
Promise.allSettled は結果に関わらずすべての promise が解決するまで待ちます。 戻り値はpromiseで、resultの中身はオブジェクトの配列となる。 →エラーは容認されてしまう! 容認されてもいいなら! そのオブジェクト内容は以下の通り。 成功したレスポンスの場合: {status:"fulfilled", value:result} エラーの場合: {status:"rejected", reason:error} 例えば、複数のユーザに対する情報をフェッチしたいとします。たとえ1リクエストが失敗しても、他の結果はほしいです。 Promise.allSettled を使いましょう: let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://no-such-url' ]; Promise.allSettled(urls.map(url => fetch(url))) .then(results => { // (*) results.forEach((result, num) => { if (result.status == "fulfilled") { alert(`${urls[num]}: ${result.value.status}`); } if (result.status == "rejected") { alert(`${urls[num]}: ${result.reason}`); } }); }); 上の行 (*) の results は以下になります: [ {status: 'fulfilled', value: ...レスポンス...}, {status: 'fulfilled', value: ...レスポンス...}, {status: 'rejected', reason: ...エラーオブジェクト...} ] そのため、各 promise に対してステータスと 値 or エラー を取得します。
98
Promise.allSetteledの制限事項をあげよ
ブラウザはサポートしてない。
99
Promise.allSettledのポリフィルを作成せよ
引数がプロミスのイテラブルを期待する関数を作る。 受け取ったプロミスをそれぞれ.thenでコールバックを登録する。 そのコールバック処理では、成功用、失敗用のオブジェクトを生成する ↑配列内のプロミスに対するコールバックの登録はMap関数を利用して行う。 .thenの戻り値はpromiseなので、それを配列として取得することになる。 生成されるオブジェクトはpromiseの結果に登録される。 上記処理をPromose.allに渡して返す。 if (!Promise.allSettled) { const rejectHandler = reason => ({ status: 'rejected', reason }); const resolveHandler = value => ({ status: 'fulfilled', value }); Promise.allSettled = function (promises) { const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler)); return Promise.all(convertedPromises); }; } Promise.resolve(p)の部分はpromiseじゃなくてプリミティブが配列の中に入ってきた時の対応 値がpromiseの場合は、そのまま返すので、この使い方で問題ない
100
Promise.raceについて説明せよ
これは Promise.all と同様ですが、すべてが完了するのを待つのではなく、最初の結果(またはエラー)のみを待ちます。 構文です: let promise = Promise.race(iterable); 例えば、ここでは結果は 1 になります: Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1 なので、最初の結果/エラーが Promise.race 全体の結果になります。最初の確定した promise が “レースに勝った” 後、それ以外の結果/エラーは無視されます。