問題一覧
1
Proxy オブジェクトとはなにか?
Proxy オブジェクトは別のオブジェクトをラップし、プロパティやその他の読み取り/書き込みなどの操作をインターセプトします。 例えば読み取りをインターセプトすることで、存在しないプロパティ値を取得しようしたときに、undefined ではなく0を取得するなど、任意の “デフォルト値”の取得 ロジックを組むことができます。 Proxy は多くのライブラリや一部のブラウザフレームワークで使われています。 Proxy は特別な “エキゾチックオブジェクト(exotic object)” です。Proxy は独自のプロパティは持っていません。空の handler の場合は、透過的に target へ操作を転送します。
2
proxyオブジェクトのコンストラクタ構文を示せ
let proxy = new Proxy(target, handler) target – ラップするオブジェクトです。関数含め何でもOKです。 handler – プロキシ設定: 操作をインターセプトするメソッドである “トラップ” をもつオブジェクトです。 例: get トラップは target のプロパティの読み取り用、set トラップは、target へのプロパティ書き込み用、など。 proxy の操作では、handler に対応するトラップがある場合はそれが実行されます。それ以外の場合は、操作は target で実行されます。 インターセプトとは妨害の意味をもっている。 proxyでラップしたオブジェクトはハンドラーでインターセプトができる。 インターセプトできるのは内部メソッドなので、オブジェクトの標準動作などを変更したい時に利用できる
3
以下はトラップなしのproxyオブジェクトである。 ?の部分を答えよ let target = {}; let proxy = new Proxy(target, {}); // 空のハンドラ proxy.test = 5; // プロキシへの書き込み (1) alert(target.test); // ? alert(proxy.test); // ? for(let key in proxy) alert(key); // ?
proxyでラップしたオブジェクトへの操作は本体にも適用される。 なので、 alert(target.test); // 5が出力される。 プロキシ側からの読み取りも可能 alert(proxy.test); // 5, proxy からの読み取ることができます プロキシオブジェクトのイテレーション(反復)も可能。 for(let key in proxy) alert(key); //test トラップがない場合は、 proxy 上のすべての操作は target に転送されます。 書き込み操作 proxy.test= は target に値を設定します。 読み込み操作 proxy.test は target からの値を返します。 proxy のイテレートは、target からの値を返します。 トラップがない場合は proxy は target に対する透過的なラッパーです。
4
オブジェクトの操作は内部的にどのようになっているか?
オブジェクトに対するほとんどの操作に対しては、JavaScript の仕様で いわゆる “内部メソッド” と呼ばれるものがあり、仕様ではそれらがどのように動作するかを最も低レベルで説明しています。 例えば、 [[Get]] は、プロパティを読み取るための内部メソッドで、[[Set]] はプロパティを書き込むための内部メソッド、などです。 これらのメソッドは仕様でのみ使用されており、名前を使ってそれらを直接使用することはできません。
5
Proxyのコンストラクタについて ハンドラを使うとなにができるか?
プロキシのトラップは内部メソッドの呼び出しをインターセプトします。 内部メソッドはProxy specification 及び以下の画像表にリストされています。 このテーブルに、すべての内部ソッドに対するトラップがあります: 操作をインターセプトするために new Proxy の handler パラメータに追加できるメソッド名です
6
proxyオブジェクトのハンドラでトラップを仕込む場合の考慮事項を説明せよ 例えばSetやDeleteの場合は?
内部メソッドのトラップを仕込む際、 内部メソッド特有の満たすべき条件がある。 そのほとんどは戻り値に関してです: [[Set]] は値が正常に書き込まれた場合には true を、そうでなければ false を返す必要があります。 [[Delete]] は値が正常に削除された場合には true を、そうでなければ false を返す必要があります。 他にも以下のようないくつかの不変条件があります: proxy オブジェクトに適用される [[GetPrototypeOf]] は proxy オブジェクトのターゲットオブジェクトに適用される [[GetPrototypeOf]] と同じ値を返さなければなりません。 つまり、proxy のプロトタイプを参照すると、常にターゲットオブジェクトのプロトタイプが返却される必要があります。 traps はこれらの操作をインターセプトできますが、これらのルールには従う必要があります。 不変条件は、言語機能の正しさと一貫した動作を保証するものです。完全な不変条件のリストは 仕様にありますが、変なことをしない限りは違反することはないでしょう。
7
以下配列のオブジェクトに対し、 プロキシオブジェクトでトラップし 存在しないインデックスを指定して値を取得しようとしたときにundefinedではなく0を返すように変更せよ let numbers = [0, 1, 2];
オブジェクトのデフォルト値を実装するのに get を使ってみましょう。 読み取りをインターセプトするには、handler に get(target, property, receiver) が必要です。 これはプロパティが読み取られたとき、以下の引数で実行されます。: target: new Proxy の最初の引数として渡されるターゲットオブジェクトです。 property – プロパティ名, receiver --ターゲットプロパティが getter の場合、receiver はその呼び出しの中で this として使われるオブジェクトです。 通常、これは proxy オブジェクト自身(あるいは、proxy から継承している場合は、継承したオブジェクト)です。現時点ではこの引数は不要です。 存在しない値の場合 0 を返す数値配列を作ります。 通常、存在しない値を取得しようとすると undefined になりますが、ここでは通常の配列に対して、プロパティが存在しない場合に 0 を返すプロキシでラップします。: let numbers = [0, 1, 2]; numbers = new Proxy(numbers, { get(target, prop) { if (prop in target) { return target[prop]; } else { return 0; // デフォルト値 } } }); alert( numbers[1] ); // 1 alert( numbers[123] ); // 0 (このような項目はなし)
8
以下配列のオブジェクトに対し、 プロキシオブジェクトでトラップし 値に数値以外が入るとエラーとなるような 数値専用の配列を作るにはどうするか? let numbers = [0, 1, 2];
set トラップはプロパティが書き込まれたときに発生します。 set(target, property, value, receiver): target: new Proxy の最初の引数として渡されるターゲットオブジェクトです。 property: プロパティ名 value: プロパティ値, receiver: get と同様で、setter プロパティに関係します。 set トラップは設定が成功すると true を、それ以外の場合は false (TypeError が発生)を返す必要があります。 新しい値を検証するのに使って見ましょう: let numbers = []; numbers = new Proxy(numbers, { // (*) set(target, prop, val) { // プロパティの書き込みをインターセプト if (typeof val == 'number') { target[prop] = val; return true; } else { return false; } } }); numbers.push(1); // 追加成功 numbers.push(2); // 追加成功 alert("Length is: " + numbers.length); // 2 numbers.push("test"); // TypeError (プロキシの 'set' が false を返却) alert("This line is never reached (error in the line above)"); 注目してください: 配列の組み込みの機能は依然として動作します! 値は push により追加されました。length プロパティは値が追加されたときにオートインクリメントされます。プロキシは何も破壊していません。 我々はチェック処理を追加するのに push や unshift のような、値を追加する配列メソッドを上書きする必要はありません。なぜなら、それらは内部的には [[Set]] 操作を使用しており、プロキシによりインターセプトされるからです。 したがって、コードはクリーンであり簡潔です。 true を返すのを忘れないでください 上記のように、維持すべき条件があります。 set の場合、書き込みの成功に対しては true を返さなければなりません。 それを忘れたり false を返すと、操作は TypeError をトリガーします。
9
Object.keys, for..in ループ及びオブジェクトプロパティをイテレートする他のほとんどのメソッドはなんの内部メソッドを使ってプロパティのリストを取得しているか? また、その内部メソッドのトラップはなにか?
[[OwnPropertyKeys]] 内部メソッド ownKeys トラップ
10
[[OwnPropertyKeys]] 内部メソッドを使用するオブジェクトメソッドや構文をあげ、説明せよ
Object.getOwnPropertyNames(obj) は “非” シンボルキーを返します。 Object.getOwnPropertySymbols(obj) はシンボルキーを返します。 Object.keys/values() は enumerable フラグを持つ非シンボルのキー/バリュー値を返します。 for..in は enumerable フラグを持つ非シンボルキーとプロトタイプキーをループします。
11
以下のオブジェクトに対して for..in ループを行った時や Object.keys や Object.valuesを利用したとき、 アンダースコア _ で始まるプロパティをスキップしたいときどうするか? let user = { name: "John", age: 30, _password: "***" };
ownKeys トラップを使用し、配列のリストを返す。 let user = { name: "John", age: 30, _password: "***" }; user = new Proxy(user, { ownKeys(target) { return Object.keys(target).filter(key => !key.startsWith('_')); } }); ↑これだとSymbolキー取得したいときとかうまくとれない→Reflect.ownKeys(target)でやればよさそう // "ownKeys" は _password を除外します for(let key in user) alert(key); // name, then: age // これらのメソッドへも同じ影響があります: alert( Object.keys(user) ); // name,age alert( Object.values(user) ); // John,30
12
ownKeysのトラップで、もしオブジェクトに存在しないキーを返した場合、Object.keys はそれをリストしません: なぜか?? let user = { }; user = new Proxy(user, { ownKeys(target) { return ['a', 'b', 'c']; } }); alert( Object.keys(user) ); // <empty>
Object.keys は enumerable フラグを持つプロパティだけを返すからです。 それを確かめるため、すべてのメソッドに対し内部メソッド [[GetOwnProperty]] を呼び出し,ディスクリプタ を取得します。すると、ここではプロパティがないので、そのディスクリプタは空であり、enumerable フラグがありません。そのため、スキップされます。
13
以下コードのObject.keys(user)を動くようにするにはどうするか? let user = { }; user = new Proxy(user, { ownKeys(target) { return ['a', 'b', 'c']; } }); alert( Object.keys(user) ); // <empty>
内部メソッド [[GetOwnProperty]] トラップは getOwnPropertyDescriptor を仕込むことで、ディスクリプタ取得自体をインターセプトし、enumerableがtrueのディスクリプタを返却することで、動くようになる。 let user = { }; user = new Proxy(user, { ownKeys(target) { // プロパティのリストを取得するために一度だけ呼ばれます return ['a', 'b', 'c']; }, getOwnPropertyDescriptor(target, prop) { // プロパティ毎に呼ばれます return { enumerable: true, configurable: true /* ...other flags, probable "value:..."" */ }; } }); alert( Object.keys(user) ); // a, b, c 改めて留意してください: [[GetOwnProperty]] をインターセプトする必要があるのは、プロパティがオブジェクトにない場合のみです。 なので、実際はtarget[prop]が存在しない場合は、return { enumerable: true, configurable: true /* ...other flags, probable "value:..."" */ };を返すように制御みたいな感じかな?
14
アンダースコア _ で始まるプロパティやメソッドは内部的なものであるということは、広く知られた慣習です。それらはオブジェクトの外からアクセスされるべきではありません。 プロキシを活用して実装するさい どのような、トラップが必要かあげよ let user = { name: "John", _password: "secret" }; alert(user._password); // secret
取得、設定、デリート、イテレーション を制御する! 具体的には get: そのようなプロパティの読み込み時にエラーをスロー, ※ただし、オブジェクトメソッドからは_でもアクセス可能とする必要あり。→ラップ元オブジェクトのコンテキストでそのオブジェクトメソッドを起動することで、プロキシを経由しない起動とさせる。(プロキシ経由の起動だと_プロパティアクセス時にエラーがでてしまうため) set: 書き込み時にエラーをスロー, deleteProperty: 削除時にエラーをスロー, ownKeys: for..in や Object.keys のようなメソッドから _ で始まるプロパティを除外 実装は以下の通り let user = { name: "John", _password: "***" }; user = new Proxy(user, { get(target, prop) { if (prop.startsWith('_')) { throw new Error("Access denied"); } let value = target[prop]; return (typeof value === 'function') ? value.bind(target) : value; // (*) }, set(target, prop, val) { // プロパティの書き込みをインターセプト if (prop.startsWith('_')) { throw new Error("Access denied"); } else { target[prop] = val; return true; } }, deleteProperty(target, prop) { // プロパティの削除をインターセプト if (prop.startsWith('_')) { throw new Error("Access denied"); } else { delete target[prop]; return true; } }, ownKeys(target) { // プロパティのリストをインターセプト return Object.keys(target).filter(key => !key.startsWith('_')); } }); // "get" は _password の読み込みを許可しません try { alert(user._password); // Error: Access denied } catch(e) { alert(e.message); } // "set" は _password の書き込みを許可しません try { user._password = "test"; // Error: Access denied } catch(e) { alert(e.message); } // "deleteProperty" は _password の削除を許可しません try { delete user._password; // Error: Access denied } catch(e) { alert(e.message); } // "ownKeys" は _password を除外します for(let key in user) alert(key); // name
15
前問で関数プロパティの場合は ラップ元オブジェクトでバインドをおこなっている理由はなにか? get(target, prop) { if (prop.startsWith('_')) { throw new Error("Access denied"); } let value = target[prop]; return (typeof value === 'function') ? value.bind(target) : value; // (*) },
ユーザがオブジェクトメソッドを持っている場合に、_のついたプロパティにアクセスできるようにするため。 user = { // ... checkPassword(value) { // オブジェクトメソッドは _password へアクセスできなければいけません return value === this._password; } } 使用する場合は、プロキシ経由の呼び出しとなるため、thisはプロキシとなり→エラーとなってしまう。 これを回避するためプロキシ経由ではなく、 直接ラップ元オブジェクトから起動するようにバインドを行う。 ただし、これをやっちゃうと、そのメソッドを渡す際はバインドされちゃってるので、別のコンテキストで実行したい場合にうまく動かなくなるので非推奨、
16
以下オブジェクトに対し、in呼び出しをインターセプトし、範囲内であれば trueとさせたい。 どうやるか? let range = { start: 1, end: 10 }; alert(5 in range); // trueとさせたい
has トラップは in 呼び出しをインターセプトします。 has(target, property) target – new Proxy への最初の引数として渡されるターゲットオブジェクト property – プロパティ名 let range = { start: 1, end: 10 }; range = new Proxy(range, { has(target, prop) { return prop >= target.start && prop <= target.end } }); alert(5 in range); // true
17
以下は関数オブジェクトに対してデコレーションを行った例であるが、 デコレーションは完全なラッパーでないため、 ラッパー関数はプロパティの読み書き操作などは転送せず、ラップした後、name や length などの元の関数のプロパティへのアクセスは失われていることが確認できる。 これを完全なラッパーとして利用したい場合はどうするか? function delay(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms); }; } function sayHi(user) { alert(`Hello, ${user}!`); } alert(sayHi.length); // 1 (function.length は宣言された関数の引数の数を返します) sayHi = delay(sayHi, 3000);//返される関数の引数は0なので、 alert(sayHi.length); // ここは0 (引数は 0 です)つまり、ラッパー関数のプロパティであり、ラップ元のプロパティではないことに注目
プロキシを利用して透過的なラップを行う。 delayは関数オブジェクトを返すのではなく 元の関数オブジェクトをラップしたプロキシオブジェクトを返すようにする その際applyトラップを利用することで、 プロキシを関数としてのふるまいを持たせることができる。つまりプロキシ()で呼び出した時の定義ができる。 構文は以下のとおり apply(target, thisArg, args) トラップはプロキシを関数として呼び出すよう処理をします: target はターゲットオブジェクトです(JavaScript では関数はオブジェクトです), thisArg は this の値です args は引数のリストです コードは以下 function delay(f, ms) { return new Proxy(f, { apply(target, thisArg, args) { setTimeout(() => target.apply(thisArg, args), ms); // targetはsayHiオブジェクトで、呼び出し時のコンテキストと、引数を渡して起動させる } }); } function sayHi(user) { alert(`Hello, ${user}!`); } sayHi = delay(sayHi, 3000); alert(sayHi.length); // 1 (*) プロキシは length 操作をターゲットに転送します sayHi("John"); // Hello, John! (3秒後) ↑apply
18
Reflectオブジェクトとはなにか?
Reflect は Proxy の作成を簡単にするための組み込みのオブジェクトです。 [[Get]], [[Set]] やその他の内部メソッドは仕様上のものであり、直接呼び出すことはできません。 Reflectは内部メソッドのラッパーとして機能するので、プロキシのトラップの定義の時に利用することで内部メソッドをそのまま動かすことができる。 → したがって、Reflect を使って操作を元のオブジェクトに転送することができます。 例えば [[Get]]を呼び出したい時は、 Reflect.get(obj, prop)を利用することができる。 また、 Proxy でトラップ可能なすべての内部メソッドに対し、対応するReflect用のメソッドがあり、 Proxy トラップと同じ名前、同じ引数を持っている。
19
以下コードをReflectオブジェクトを利用して書き直せ let user = { _name: "Guest", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return target[prop]; } }); alert(userProxy.name); // Guest
getトラップの戻り値をreflectメソッドにするだけ Reflect.get(target, prop, receiver); let user = { _name: "Guest", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver); } }); alert(userProxy.name); // Guest
20
Reflectメソッドを利用するメリットは? 以下コードの問題点を指摘して解説せよ let user = { _name: "Guest", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return target[prop]; // (*) target = user } }); let admin = { __proto__: userProxy, _name: "Admin" }; // 期待値: Admin alert(admin.name); // 出力: Guest (?!?)
トラップではtargetは必ずラップ元オブジェクトとなるので、 上記の例でいうと、必ずUserオブジェクトと固定されてしまう。 ↑これが問題となることがある。 上記の例では、 alert(admin.name);を参照した際、プロトタイプであるプロキシから、 Userオブジェクトのnameプロパティを取得する→プロキシのトラップが発動する。 get(target, prop, receiver) { return target[prop]; // (*) target = user } →user[name]なので、以下のゲッターメソッドが動く。 get name() { return this._name; } →user[name]よびだしなので、thisはuserとなってしまい、userオブジェクトの_nameが取れる。※ここが問題 つまり、 alert(admin.name); はGuestがとれてしまう。 本来は、プロキシを経由したとしても、adminオブジェクトの_nameを参照すべきなので、この動作は微妙。 そこをreflectオブジェクトを利用すると解決できる。 第三引数のreceiverがキモである。 receiverの説明は以下の通り、 --ターゲットプロパティが getter の場合、receiver はその呼び出しの中で this として使われるオブジェクトです。通常、これは proxy オブジェクト自身(あるいは、proxy から継承している場合は、継承したオブジェクト)です。 つまり、targetはプロキシにラップされたオブジェクト、receiverはプロキシあるいは、プロキシを継承したオブジェクト。 なので、 get(target, prop, receiver) { return receiver[prop]; // (*) recieverはadmin } とすると、 adminオブジェクトをコンテキストとした呼び出しとなる。 ただし、プロキシを実行コンテキストとしてこれを呼び出した場合は、ちゃんと動くのか? 多分動きそう または以下のようにreflectをそのまま使える。 get(target, prop, receiver) { // receiver = admin return Reflect.get(target, prop, receiver); // (*) }
21
get(target, prop, receiver) { // receiver = admin return Reflect.get(target, prop, receiver); // (*) } をもっと簡単にかけ 引数の部分
関数内なのでargumentsが使える get(target, prop, receiver) { return Reflect.get(...arguments); }
22
内部スロットとはなにか?
Map, Set, Date, Promise などの多くの組み込みオブジェクトは、いわゆる “内部スロット” を使用します。 組み込みオブジェクト専用のデータ保持空間といったところか。 それらはプロパティに似ていますが、内部で仕様専用の目的で予約されています。 例えば、Map は内部スロット [[MapData]] にアイテムを保存します また、Mapのsetなどの組み込みメソッドは、[[Get]]/[[Set]] 内部メソッド経由ではなく、直接アクセスします。そのため、Proxy はインターセプトすることができません。
23
プロキシの内部スロットに関する制限事項を説明せよ 例えば、MapオブジェクトをProxyでラップし、setメソッドを動かせるようにするにはどうすればよいか?
プロキシは、内部スロットをもつオブジェクトに対して、透過的な機能を提供できない。 ※プロキシ自体に内部スロットをサポートしてないため let map = new Map(); let proxy = new Proxy(map, {}); proxy.set('test', 1); // Setメソッドは起動するが、内部スロットにアクセスできないのでエラー 内部的に、Map はすべてのデータを [[MapData]] 内部スロットに保存します。プロキシはそのようなスロットはありません。組み込みのメソッド Map.prototype.set メソッドは内部プロパティ this.[[MapData]] にアクセスしようとしますが、this=proxy なので proxy 内には見つけることができず失敗します。 ↑thisの値を元のマップオブジェクトとすれば動くので、 proxy.setメソッドにアクセスする(getトラップが発動するので、Mapオブジェクトをコンテキストとするようにすればよい。 let map = new Map(); let proxy = new Proxy(map, { get(target, prop, receiver) { let value = Reflect.get(...arguments); return typeof value == 'function' ? value.bind(target) : value; } }); proxy.set('test', 1);//このときsetプロパティの取得でgetトラップが起動する。 alert(proxy.get('test')); // これもgetトラップが起動し、マップオブジェクトがコンテキストとなるので、うまく動く
24
内部スロットがない、ありそうな型はなにか?
Array には内部スロットがありません 注目すべき例外です: 組み込みの Array は内部スロットを使用していません。Array はずっと以前から存在していたこともあり、歴史的な理由によるものです。 したがって配列をプロキシする際にはこのような問題は起こりません。
25
以下はプライベートプロパティにプロキシ経由でアクセスしようとしているができない。なぜか? class User { #name = "Guest"; getName() { return this.#name; } } let user = new User(); user = new Proxy(user, {}); alert(user.getName()); // Error どのように解決できるか?
プライベートプロパティ内部スロットを利用しているから、プロキシ経由はアクセスできない これは、プライベートフィールドが内部スロットを使用して実装されているからです。JavaScript はそれらにアクセスする際、[[Get]]/[[Set]] は使用しません。 この場合も、メソッドをバインドする方法で機能させることができます: class User { #name = "Guest"; getName() { return this.#name; } } let user = new User(); user = new Proxy(user, { get(target, prop, receiver) { let value = Reflect.get(...arguments); return typeof value == 'function' ? value.bind(target) : value; } }); alert(user.getName()); // Guest
26
取り消し可能(revocable) なプロキシとはなにか?
取り消し可能(revocable) なプロキシは、無効にすることのできるプロキシです。 プロキシからターゲットオブジェクトへの接続を閉じることができる。 接続をとじると、ターゲットが到達不可能になった場合はガベージコレクションの対象となる。
27
以下オブジェクトに対して、 取り消し可能(revocable) なプロキシを作成せよ let object = { data: "Valuable data" };
取り消し可能なプロキシの構文は以下のとおり let {proxy, revoke} = Proxy.revocable(target, handler) この呼び出しは proxy と無効にするために revoke 関数を持つオブジェクトを返します。 なのでこんな感じ let object = { data: "Valuable data" }; let {proxy, revoke} = Proxy.revocable(object, {}); // オブジェクトの代わりにプロキシをどこかに渡します alert(proxy.data); // Valuable data // 後で次のようにします revoke(); // すると、プロキシは機能しなくなります(無効化されました) alert(proxy.data); // Error revoke() 呼び出しは、プロキシからターゲットオブジェクトへのすべての内部参照を削除します。これにより繋がりがなくなります。
28
revokeメソッドとプロキシを適切に管理するにはどうするか?
proxy.revoke = revoke と設定することで、proxy に revoke メソッドをバインドすることもできます。 別の選択肢は、WeakMap を作成し、キーとして proxy を、値として対応する revoke をもたせることです。 これで、簡単に proxy に対する revoke を見つけることができます。 let revokes = new WeakMap(); let object = { data: "Valuable data" }; let {proxy, revoke} = Proxy.revocable(object, {}); revokes.set(proxy, revoke); // ..later in our code.. revoke = revokes.get(proxy); revoke(); alert(proxy.data); // Error (revoked) ここで Map の代わりに WeakMap を使用しているのは、ガベージコレクションをブロックしないようにするためです。proxy オブジェクトが “到達不可能” になった(e.g それを参照する変数がなくなった)場合、WeakMap を利用すると、不要になった revoke を一緒にメモリ上から削除することができます。
29
プロキシを活用したテクニックを紹介
異なるプロキシで複数回オブジェクトをラップし、機能の様々な側面でオブジェクトデコレートすることも可能です。
30
プロキシのインターセプトに関する制限事項をあげよ
オブジェクトの等価評価 === はインターセプトできません
31
プロキシのパフォーマンスはどうか?
パフォーマンス: ベンチマークはエンジンによりますが、通常、最も単純なプロキシを使用したプロパティへのアクセスするにも数倍時間がかかります。しかし実際にそれが問題になるのは一部の “ボトルネック” オブジェクトのみです。
32
プロキシを返すことで、“オブジェクトを監視可能にする” 関数 makeObservable(target) を作成してください。 このように動作します: function makeObservable(target) { /* your code */ } let user = {}; user = makeObservable(user); user.observe((key, value) => { alert(`SET ${key}=${value}`); }); ↑observeメソッドを呼び出すことで、関数を登録することができる。 user.name = "John"; // alerts: SET name=John 内部で、関数の配列を管理し、setトラップで、登録した関数を呼び出す仕様。 つまり、makeObservable により返却されるオブジェクトは元のオブジェクトのように見えますが、任意のプロパティ変更時に呼び出される handler 関数をセットするメソッド observe(handler) を持ちます。 プロパティを変更したときはいつでもプロパティの名前と値と一緒に handler(key, value) が呼ばれます。 P.S. このタスクでは、プロパティの書き込みにだけ注目してください。他の操作も同様の方法で実装することはできます。
解決策は2つのパートで構成されます: .observe(handler) が呼ばれたときは、後で handler が呼び出せるように、ハンドラをどこかに覚えておく必要があります。シンボルをプロパティのキーとして使用することで、ハンドラをオブジェクトに格納できます。 変更時にハンドラを呼ぶための set トラップを持つプロキシが必要です。 let handlers = Symbol('handlers'); function makeObservable(target) { // 1. ハンドラの格納場所の初期化 target[handlers] = []; // 後々の呼び出しのため、配列にハンドラ関数を格納 target.observe = function(handler) { this[handlers].push(handler); }; // 2. 変更を処理するプロキシを作成 return new Proxy(target, { set(target, property, value, receiver) { let success = Reflect.set(...arguments); // 操作をオブジェクトに転送 if (success) { // プロパティの設定でエラーがなければ // すべてのハンドラを呼び出す target[handlers].forEach(handler => handler(property, value)); } return success; } }); } let user = {}; user = makeObservable(user); user.observe((key, value) => { alert(`SET ${key}=${value}`); }); user.name = "John";
33
文字列で定義したコードを実行するにはどうするか?
組み込みの eval 関数を使うとコード文字列を実行することができます。 構文: let result = eval(code); 例: let code = 'alert("Hello")'; eval(code); // Hello コードの文字列は長かったり、改行や関数定義、変数を含んでいる可能性があります。 eval の結果は最後の文の結果です。 例: let value = eval('1+1'); alert(value); // 2 let value = eval('let i = 0; ++i'); alert(value); // 1
34
eval されたコードは外部変数を参照することができるか? let a = 1; function f() { let a = 2; eval('alert(a)'); // ?? } f();
できる。 アラートは2を出力する。 eval されたコードは現在のレキシカル環境で実行されるため、外部変数を参照することができます。:
35
evalの動作について、 strictモードとそうでない時の違いについて説明せよ
strict モードでは、eval で定義したコード内で、独自のレキシカル環境を持ちます。 なので変数宣言しても参照できない。 // 実行可能な例では、'use strict' はデフォルトで有効になっています eval("let x = 5; function f() {}"); alert(typeof x); // undefined (そのような変数はありません) // function f も見えません use strict がなければ、eval は独自のレキシカル環境を持たないので、外側から x や fを見ることができます。
36
evalは現在よく利用されているか?
現在は、eval を利用する理由はほとんどありません。もし誰かがそれを使用しているなら、モダンな言語構造、あるいは JavaScript Moduleに置き換えるよい機会です。
37
evalで外部変数を利用することがバッドプラクティスだといわれる理由をあげよ
外部変数にアクセスする機能には副作用があることに注意してください。 コードの minifier (JSを本番環境に適用する前に使われるツールで、JSを圧縮(minify)します)は最適化のために、ローカル変数をより短いものに置き換えます。 これは通常安全ですが、eval が使われている場合、圧縮されたローカル変数にはアクセスできないので、 minifierでは、evalで参照する可能性のある全ての変数に関しては圧縮しないようにしている。 なので、コード自体は実行可能だが、圧縮度がおちてしまうので、よくないといわれている。 eval の内部で外部のローカル変数を使用することは、コードのメンテナンスをより難しくするためバッドプラクティスとされています
38
evalでやりたいこと、つまり文字列で定義したコードを実行するために、コード圧縮率に迷惑をかけない安全な方法を示せ 外部変数を利用しない場合
eval されたコードが外部変数を使用していない場合、eval を window.eval(...) で呼び出してください この方法では、コードはグローバルスコープで実行されます。: let x = 1; { let x = 5; window.eval('alert(x)'); // 1 (グローバル変数) } 圧縮対象は外部のローカル変数なので、 グローバル変数は圧縮対象じゃないから圧縮率に迷惑をかけない
39
evalでやりたいこと、つまり文字列で定義したコードを実行するために、コード圧縮率に迷惑をかけない安全な方法を示せ eval されたコードがローカル変数を必要とする場合
eval を new Function に変更し、引数としてそれらを渡してください let f = new Function('a', 'alert(a)'); f(5); // 5 new Function については文字列から関数を作成する。 グローバルのレキシカル環境のみ参照可能。そのため、外部レキシカル環境の変数は見えません。 ですが、上記の例のように、引数として明示的に渡すほうがはるかに明白です。
40
カリー化とは、 f(a, b, c)として呼び出せる関数をどのようにして呼び出せるようにすることか?
f(a)(b)(c)のように呼び出せるようにすること
41
以下コードをラップし、カリー化して呼び出し可能にせよ 簡易的につくれ function sum(a, b) { return a + b; }
関数を引数にとる。 function curry(f) { // curry(f)によって変形が施されます return function(a) { return function(b) { return f(a, b); //bが指定されたら関数実行! }; }; } // 使用法 function sum(a, b) { return a + b; } let curriedSum = curry(sum); alert( curriedSum(1)(2) ); // 3
42
高度なカリー化を行う関数をかけ 使い方はこんなかんじ function sum(a, b, c) { return a + b + c; } let curriedSum = curry(sum); alert( curriedSum(1, 2, 3) ); // 6, 普通に呼び出すことも可能です alert( curriedSum(1)(2,3) ); // 6, 最初の引数をカリー化しています alert( curriedSum(1)(2)(3) ); // 6, 完全なカリー化です
function curry(func) { return function curried(...args) { if (args.length >= func.length) { return func.apply(this, args); } else { return function(...args2) { return curried.apply(this, args.concat(args2)); } } }; } 渡された引数の数によって実行するコードを制御する。 引数の数が実行する関数の引数の数以上の場合、つまり以下のような感じ alert( curriedSum(1, 2, 3) ); // 6, 普通に呼び出すことも可能です は普通にthisコンテキストで関数実行を行う 引数の数が関数の引数の数より下の場合 つまり alert( curriedSum(1)(2,3) ); // 6, 最初の引数をカリー化しています alert( curriedSum(1)(2)(3) ); // 6, 完全なカリー化です の場合は 関数を返す必要がある。 その関数は新しい引数args2をとり、 自身の関数curriedをthisコンテキストで呼び出す内容で定義する。 その際に既存の引数argsと新しい引数args2を結合して実行する。
43
カリー化に関する制限事項をのべよ
カリー化では、関数の引数の数は固定されている必要があります。 f(...args)のような、残りのパラメータを使用するような関数は、このようにはカリー化できません。 ↑前問のような引数の数で実行するか判断するロジックが定義できないから
44
askPassword関数では入力されたパスワードを検証する。 その結果引数のコールバック関数を起動する。 今回はuser.loginに定義されたアラートを実行したい。 user.loginメソッドはどのように利用できるか? function askPassword(ok, fail) { let password = prompt("Password?", ''); if (password == "rockstar") ok(); else fail(); } let user = { name: 'John', login(result) { alert( this.name + (result ? ' logged in' : ' failed to log in') ); } }; askPassword(?, ?); // ? 変更はハイライトされた箇所の修正だけにしてください。
そのままオブジェクトメソッドと引数を渡すことはできない。 理由は、その場ですぐ実行されてしまうため。 askPassword(user.login(true), user.login(false)); なので、アロー関数でラップする ラッパー関数、簡単化のためにアローを使う: askPassword(() => user.login(true), () => user.login(false)); これで外部変数から user を取得し、通常の方法で実行します。 もしくは、user をコンテキストとして使い、正しい1つ目の引数を持つ user.login からの部分関数を作ります: askPassword(user.login.bind(user, true), user.login.bind(user, false));
45
以下のコードは何が表示されるか? let user = { name: "John", hi() { alert(this.name); }, bye() { alert("Bye"); } }; user.hi(); // John // 今、name に応じて user.hi または user.bye を読んでみましょう (user.name == "John" ? user.hi : user.bye)(); // ???
(user.name == "John" ? user.hi : user.bye)に対して();でメソッド実行している。 これは実行コンテキストが失われたしまっているので、this.nameが参照できなくてエラー
46
user.hi() 呼び出しを動作させるために、JavaScriptはトリックを使います ドット '.' は何を返すか?
特別な参照型な返す。
47
特別な参照型とはなにか?
参照型は “仕様上の型” です。私たちは明示的にそれを使うことはできませんが、言語の中で内部的に使われています。 参照型の値は、3つの値の組み合わせ (base, name, strict) です。ここで: base はオブジェクトです。 name はプロパティです。 strict は use strict が効いている場合は true です。 参照型はドット . から呼び出し括弧 () へ情報を渡す目的の特別な “中間” の内部型です。 obj.method() 内の . のようなプロパティの読み取りでは、正確なプロパティ値ではなくプロパティ値とそれが取得されたオブジェクトの両方を保持する特別な “参照型” の値を返します。
48
user.hi へのプロパティアクセスの結果は、なにがかえってくるか?
user.hi へのプロパティアクセスの結果は、関数ではなく参照型です。strict mode での user.hi はこうなります: // 参照型の値 (user, "hi", true)
49
参照型に()呼び出しされとどうなるか?
参照型に対して丸括弧 () 呼び出しがされると、それらはオブジェクトとそのメソッドについての完全な情報を受け取り、正しい this (このケースでは user)をセットできます。
50
代入 hi = user.hi のような操作は、内部的に参照型はどうなるか?
代入 hi = user.hi のような他の操作は、参照型を破棄し、user.hi(関数)の値を渡します。従って、それ以降の操作は全て this を “失います”。
51
thisの値がただしく渡されるには、 関数がドット obj.method()で実行されるか、他にどんな方法があるか?
もしくは角括弧 obj[method]()構文を使って直接呼び出された場合のみ正しく渡されます プロパティ・アクセサ(ドットまたは角括弧)が参照型の値を返す。
52
以下コードはどのように動作するか? let user = { name: "John", go: function() { alert(this.name) } }; (user.go)() // ??
(user.go) の周りの括弧はここではなにもしないことに注意してください。 なので普通にオブジェクトメソッドとして動く
53
以下処理はuserオブジェクトの定義で最後にセミコロンをつけてない。 どのように解釈されてしまうか? let user = { name: "John", go: function() { alert(this.name) } } (user.go)()
これはエラーになってしまう! JavaScript 括弧 (user.go)() の前にはセミコロンを想定していないので、このようにコードを解釈します: let user = { go:... }(user.go)() 構文的にはオブジェクト { go: ...} を引数 (user.go) をもつ関数として呼びだすことができます。また、それは let user と同じ行で起こります。なので、user オブジェクトはまだ定義されていないのでエラーになります。
54
下のコードで、user.go() メソッドを4回連続で呼び出すつもりです。 アラート部分はなにになるか? let obj, method; obj = { go: function() { alert(this); } }; obj.go(); // (1) ?? (obj.go)(); // (2) ?? (method = obj.go)(); // (3) ?? (obj.go || obj.stop)(); // (4) ??
// (1) [object Object] // (2) [object Object] ここでは括弧は操作の順番を変更しません ドットが最初です。 // (3) undefined ここにより複雑な呼び出し (expression).method() があります。この呼出しはまるで2行に分割されたかのようにして動作します。: f = obj.go; // 式を計算します。 f(); // 持っているものを実行します ここで、f() は this なしの関数として実行されます。 (4) undefined (3) と似たようなもので、ドット . の左側に式を持っています メソッド呼び出し以外の操作(代入 =や ||のような)は、 this に設定できる情報を持たない通常の値にします。
55
BigIntとはなにか?
BigInt は任意の長さの整数をサポートする特別な数値型です。 bigint は n を整数リテラルの末尾に追加するか、文字列や数字などから bigint を作成する関数 BigInt を呼び出すことによって生成されます。 const bigint = 1234567890123456789012345678901234567890n; const sameBigint = BigInt("1234567890123456789012345678901234567890"); const bigintFromNumber = BigInt(10); // 10n と同じ
56
以下の答えはなにか? alert(1n + 2n); // ? alert(5n / 2n); // ? alert(1n + 2); // ?
3n 2n 整数なので小数点なし0に向かって丸められる。 エラー 普通の数値と計算するには BigInt(2)で変換する。
57
以下はどうなるか? alert( 2n > 1n ); // ? alert( 2n > 1 ); // ? alert( 1 == 1n ); // ? alert( 1 === 1n ); // ?
比較は数値もそのまま有効 true true true false