js8 その他
問題一覧
1
Proxy オブジェクトは別のオブジェクトをラップし、プロパティやその他の読み取り/書き込みなどの操作をインターセプトします。 例えば読み取りをインターセプトすることで、存在しないプロパティ値を取得しようしたときに、undefined ではなく0を取得するなど、任意の “デフォルト値”の取得 ロジックを組むことができます。 Proxy は多くのライブラリや一部のブラウザフレームワークで使われています。 Proxy は特別な “エキゾチックオブジェクト(exotic object)” です。Proxy は独自のプロパティは持っていません。空の handler の場合は、透過的に target へ操作を転送します。
2
let proxy = new Proxy(target, handler) target – ラップするオブジェクトです。関数含め何でもOKです。 handler – プロキシ設定: 操作をインターセプトするメソッドである “トラップ” をもつオブジェクトです。 例: get トラップは target のプロパティの読み取り用、set トラップは、target へのプロパティ書き込み用、など。 proxy の操作では、handler に対応するトラップがある場合はそれが実行されます。それ以外の場合は、操作は target で実行されます。 インターセプトとは妨害の意味をもっている。 proxyでラップしたオブジェクトはハンドラーでインターセプトができる。 インターセプトできるのは内部メソッドなので、オブジェクトの標準動作などを変更したい時に利用できる
3
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 specification 及び以下の画像表にリストされています。 このテーブルに、すべての内部ソッドに対するトラップがあります: 操作をインターセプトするために new Proxy の handler パラメータに追加できるメソッド名です
6
内部メソッドのトラップを仕込む際、 内部メソッド特有の満たすべき条件がある。 そのほとんどは戻り値に関してです: [[Set]] は値が正常に書き込まれた場合には true を、そうでなければ false を返す必要があります。 [[Delete]] は値が正常に削除された場合には true を、そうでなければ false を返す必要があります。 他にも以下のようないくつかの不変条件があります: proxy オブジェクトに適用される [[GetPrototypeOf]] は proxy オブジェクトのターゲットオブジェクトに適用される [[GetPrototypeOf]] と同じ値を返さなければなりません。 つまり、proxy のプロトタイプを参照すると、常にターゲットオブジェクトのプロトタイプが返却される必要があります。 traps はこれらの操作をインターセプトできますが、これらのルールには従う必要があります。 不変条件は、言語機能の正しさと一貫した動作を保証するものです。完全な不変条件のリストは 仕様にありますが、変なことをしない限りは違反することはないでしょう。
7
オブジェクトのデフォルト値を実装するのに 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
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
[[OwnPropertyKeys]] 内部メソッド ownKeys トラップ
10
Object.getOwnPropertyNames(obj) は “非” シンボルキーを返します。 Object.getOwnPropertySymbols(obj) はシンボルキーを返します。 Object.keys/values() は enumerable フラグを持つ非シンボルのキー/バリュー値を返します。 for..in は enumerable フラグを持つ非シンボルキーとプロトタイプキーをループします。
11
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
Object.keys は enumerable フラグを持つプロパティだけを返すからです。 それを確かめるため、すべてのメソッドに対し内部メソッド [[GetOwnProperty]] を呼び出し,ディスクリプタ を取得します。すると、ここではプロパティがないので、そのディスクリプタは空であり、enumerable フラグがありません。そのため、スキップされます。
13
内部メソッド [[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
取得、設定、デリート、イテレーション を制御する! 具体的には 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
ユーザがオブジェクトメソッドを持っている場合に、_のついたプロパティにアクセスできるようにするため。 user = { // ... checkPassword(value) { // オブジェクトメソッドは _password へアクセスできなければいけません return value === this._password; } } 使用する場合は、プロキシ経由の呼び出しとなるため、thisはプロキシとなり→エラーとなってしまう。 これを回避するためプロキシ経由ではなく、 直接ラップ元オブジェクトから起動するようにバインドを行う。 ただし、これをやっちゃうと、そのメソッドを渡す際はバインドされちゃってるので、別のコンテキストで実行したい場合にうまく動かなくなるので非推奨、
16
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
プロキシを利用して透過的なラップを行う。 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 は Proxy の作成を簡単にするための組み込みのオブジェクトです。 [[Get]], [[Set]] やその他の内部メソッドは仕様上のものであり、直接呼び出すことはできません。 Reflectは内部メソッドのラッパーとして機能するので、プロキシのトラップの定義の時に利用することで内部メソッドをそのまま動かすことができる。 → したがって、Reflect を使って操作を元のオブジェクトに転送することができます。 例えば [[Get]]を呼び出したい時は、 Reflect.get(obj, prop)を利用することができる。 また、 Proxy でトラップ可能なすべての内部メソッドに対し、対応するReflect用のメソッドがあり、 Proxy トラップと同じ名前、同じ引数を持っている。
19
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
トラップでは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
関数内なのでargumentsが使える get(target, prop, receiver) { return Reflect.get(...arguments); }
22
Map, Set, Date, Promise などの多くの組み込みオブジェクトは、いわゆる “内部スロット” を使用します。 組み込みオブジェクト専用のデータ保持空間といったところか。 それらはプロパティに似ていますが、内部で仕様専用の目的で予約されています。 例えば、Map は内部スロット [[MapData]] にアイテムを保存します また、Mapのsetなどの組み込みメソッドは、[[Get]]/[[Set]] 内部メソッド経由ではなく、直接アクセスします。そのため、Proxy はインターセプトすることができません。
23
プロキシは、内部スロットをもつオブジェクトに対して、透過的な機能を提供できない。 ※プロキシ自体に内部スロットをサポートしてないため 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
プライベートプロパティ内部スロットを利用しているから、プロキシ経由はアクセスできない これは、プライベートフィールドが内部スロットを使用して実装されているからです。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) なプロキシは、無効にすることのできるプロキシです。 プロキシからターゲットオブジェクトへの接続を閉じることができる。 接続をとじると、ターゲットが到達不可能になった場合はガベージコレクションの対象となる。
27
取り消し可能なプロキシの構文は以下のとおり 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
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
解決策は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
できる。 アラートは2を出力する。 eval されたコードは現在のレキシカル環境で実行されるため、外部変数を参照することができます。:
35
strict モードでは、eval で定義したコード内で、独自のレキシカル環境を持ちます。 なので変数宣言しても参照できない。 // 実行可能な例では、'use strict' はデフォルトで有効になっています eval("let x = 5; function f() {}"); alert(typeof x); // undefined (そのような変数はありません) // function f も見えません use strict がなければ、eval は独自のレキシカル環境を持たないので、外側から x や fを見ることができます。
36
現在は、eval を利用する理由はほとんどありません。もし誰かがそれを使用しているなら、モダンな言語構造、あるいは JavaScript Moduleに置き換えるよい機会です。
37
外部変数にアクセスする機能には副作用があることに注意してください。 コードの minifier (JSを本番環境に適用する前に使われるツールで、JSを圧縮(minify)します)は最適化のために、ローカル変数をより短いものに置き換えます。 これは通常安全ですが、eval が使われている場合、圧縮されたローカル変数にはアクセスできないので、 minifierでは、evalで参照する可能性のある全ての変数に関しては圧縮しないようにしている。 なので、コード自体は実行可能だが、圧縮度がおちてしまうので、よくないといわれている。 eval の内部で外部のローカル変数を使用することは、コードのメンテナンスをより難しくするためバッドプラクティスとされています
38
eval されたコードが外部変数を使用していない場合、eval を window.eval(...) で呼び出してください この方法では、コードはグローバルスコープで実行されます。: let x = 1; { let x = 5; window.eval('alert(x)'); // 1 (グローバル変数) } 圧縮対象は外部のローカル変数なので、 グローバル変数は圧縮対象じゃないから圧縮率に迷惑をかけない
39
eval を new Function に変更し、引数としてそれらを渡してください let f = new Function('a', 'alert(a)'); f(5); // 5 new Function については文字列から関数を作成する。 グローバルのレキシカル環境のみ参照可能。そのため、外部レキシカル環境の変数は見えません。 ですが、上記の例のように、引数として明示的に渡すほうがはるかに明白です。
40
f(a)(b)(c)のように呼び出せるようにすること
41
関数を引数にとる。 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 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(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
(user.name == "John" ? user.hi : user.bye)に対して();でメソッド実行している。 これは実行コンテキストが失われたしまっているので、this.nameが参照できなくてエラー
46
特別な参照型な返す。
47
参照型は “仕様上の型” です。私たちは明示的にそれを使うことはできませんが、言語の中で内部的に使われています。 参照型の値は、3つの値の組み合わせ (base, name, strict) です。ここで: base はオブジェクトです。 name はプロパティです。 strict は use strict が効いている場合は true です。 参照型はドット . から呼び出し括弧 () へ情報を渡す目的の特別な “中間” の内部型です。 obj.method() 内の . のようなプロパティの読み取りでは、正確なプロパティ値ではなくプロパティ値とそれが取得されたオブジェクトの両方を保持する特別な “参照型” の値を返します。
48
user.hi へのプロパティアクセスの結果は、関数ではなく参照型です。strict mode での user.hi はこうなります: // 参照型の値 (user, "hi", true)
49
参照型に対して丸括弧 () 呼び出しがされると、それらはオブジェクトとそのメソッドについての完全な情報を受け取り、正しい this (このケースでは user)をセットできます。
50
代入 hi = user.hi のような他の操作は、参照型を破棄し、user.hi(関数)の値を渡します。従って、それ以降の操作は全て this を “失います”。
51
もしくは角括弧 obj[method]()構文を使って直接呼び出された場合のみ正しく渡されます プロパティ・アクセサ(ドットまたは角括弧)が参照型の値を返す。
52
(user.go) の周りの括弧はここではなにもしないことに注意してください。 なので普通にオブジェクトメソッドとして動く
53
これはエラーになってしまう! JavaScript 括弧 (user.go)() の前にはセミコロンを想定していないので、このようにコードを解釈します: let user = { go:... }(user.go)() 構文的にはオブジェクト { go: ...} を引数 (user.go) をもつ関数として呼びだすことができます。また、それは let user と同じ行で起こります。なので、user オブジェクトはまだ定義されていないのでエラーになります。
54
// (1) [object Object] // (2) [object Object] ここでは括弧は操作の順番を変更しません ドットが最初です。 // (3) undefined ここにより複雑な呼び出し (expression).method() があります。この呼出しはまるで2行に分割されたかのようにして動作します。: f = obj.go; // 式を計算します。 f(); // 持っているものを実行します ここで、f() は this なしの関数として実行されます。 (4) undefined (3) と似たようなもので、ドット . の左側に式を持っています メソッド呼び出し以外の操作(代入 =や ||のような)は、 this に設定できる情報を持たない通常の値にします。
55
BigInt は任意の長さの整数をサポートする特別な数値型です。 bigint は n を整数リテラルの末尾に追加するか、文字列や数字などから bigint を作成する関数 BigInt を呼び出すことによって生成されます。 const bigint = 1234567890123456789012345678901234567890n; const sameBigint = BigInt("1234567890123456789012345678901234567890"); const bigintFromNumber = BigInt(10); // 10n と同じ
56
3n 2n 整数なので小数点なし0に向かって丸められる。 エラー 普通の数値と計算するには BigInt(2)で変換する。
57
比較は数値もそのまま有効 true true true false
その他
その他
谷峻輔 · 12問 · 2年前その他
その他
12問 • 2年前LWC
LWC
谷峻輔 · 100問 · 2年前LWC
LWC
100問 • 2年前LWC2
LWC2
谷峻輔 · 28問 · 2年前LWC2
LWC2
28問 • 2年前Javascript
Javascript
谷峻輔 · 100問 · 2年前Javascript
Javascript
100問 • 2年前javascript2
javascript2
谷峻輔 · 58問 · 2年前javascript2
javascript2
58問 • 2年前js1 JavaScript の基礎
js1 JavaScript の基礎
谷峻輔 · 92問 · 2年前js1 JavaScript の基礎
js1 JavaScript の基礎
92問 • 2年前js2 コード品質、オブジェクト基本、データ型
js2 コード品質、オブジェクト基本、データ型
谷峻輔 · 100問 · 2年前js2 コード品質、オブジェクト基本、データ型
js2 コード品質、オブジェクト基本、データ型
100問 • 2年前js3 データ型
js3 データ型
谷峻輔 · 100問 · 2年前js3 データ型
js3 データ型
100問 • 2年前js4 データ型、関数の高度な機能
js4 データ型、関数の高度な機能
谷峻輔 · 100問 · 2年前js4 データ型、関数の高度な機能
js4 データ型、関数の高度な機能
100問 • 2年前js5 関数の高度な機能 オブジェクトプロパティの設定 プロトタイプ, 継承プロトタイプ, 継承、クラス
js5 関数の高度な機能 オブジェクトプロパティの設定 プロトタイプ, 継承プロトタイプ, 継承、クラス
谷峻輔 · 96問 · 2年前js5 関数の高度な機能 オブジェクトプロパティの設定 プロトタイプ, 継承プロトタイプ, 継承、クラス
js5 関数の高度な機能 オブジェクトプロパティの設定 プロトタイプ, 継承プロトタイプ, 継承、クラス
96問 • 2年前js6 クラス Promise, async/await
js6 クラス Promise, async/await
谷峻輔 · 100問 · 2年前js6 クラス Promise, async/await
js6 クラス Promise, async/await
100問 • 2年前js7 Promise, async/await、ジェネレータ, 高度なイテレーション、モジュール
js7 Promise, async/await、ジェネレータ, 高度なイテレーション、モジュール
谷峻輔 · 96問 · 2年前js7 Promise, async/await、ジェネレータ, 高度なイテレーション、モジュール
js7 Promise, async/await、ジェネレータ, 高度なイテレーション、モジュール
96問 • 2年前Javaとは〜
Javaとは〜
谷峻輔 · 83問 · 2年前Javaとは〜
Javaとは〜
83問 • 2年前Reactクイックスタート
Reactクイックスタート
谷峻輔 · 15問 · 2年前Reactクイックスタート
Reactクイックスタート
15問 • 2年前デザインパターン
デザインパターン
谷峻輔 · 5問 · 2年前デザインパターン
デザインパターン
5問 • 2年前承認申請
承認申請
谷峻輔 · 16問 · 2年前承認申請
承認申請
16問 • 2年前アプリケーションビルダー2
アプリケーションビルダー2
谷峻輔 · 100問 · 2年前アプリケーションビルダー2
アプリケーションビルダー2
100問 • 2年前アプリケーションビルダー3
アプリケーションビルダー3
谷峻輔 · 100問 · 2年前アプリケーションビルダー3
アプリケーションビルダー3
100問 • 2年前アプリケーションビルダー4
アプリケーションビルダー4
谷峻輔 · 100問 · 2年前アプリケーションビルダー4
アプリケーションビルダー4
100問 • 2年前アプリケーションビルダー5
アプリケーションビルダー5
谷峻輔 · 94問 · 2年前アプリケーションビルダー5
アプリケーションビルダー5
94問 • 2年前アプリケーションビルダー6
アプリケーションビルダー6
谷峻輔 · 100問 · 2年前アプリケーションビルダー6
アプリケーションビルダー6
100問 • 2年前アプリケーションビルダー7
アプリケーションビルダー7
谷峻輔 · 100問 · 2年前アプリケーションビルダー7
アプリケーションビルダー7
100問 • 2年前アプリケーションビルダー8
アプリケーションビルダー8
谷峻輔 · 98問 · 2年前アプリケーションビルダー8
アプリケーションビルダー8
98問 • 2年前アプリケーションビルダー9
アプリケーションビルダー9
谷峻輔 · 70問 · 2年前アプリケーションビルダー9
アプリケーションビルダー9
70問 • 2年前フロー
フロー
谷峻輔 · 42問 · 2年前フロー
フロー
42問 • 2年前レポート、ダッシュボード
レポート、ダッシュボード
谷峻輔 · 60問 · 2年前レポート、ダッシュボード
レポート、ダッシュボード
60問 • 2年前SalesCloud
SalesCloud
谷峻輔 · 18問 · 2年前SalesCloud
SalesCloud
18問 • 2年前標準機能
標準機能
谷峻輔 · 72問 · 2年前標準機能
標準機能
72問 • 2年前宣言的開発
宣言的開発
谷峻輔 · 8問 · 2年前宣言的開発
宣言的開発
8問 • 2年前数式、人力規則
数式、人力規則
谷峻輔 · 18問 · 2年前数式、人力規則
数式、人力規則
18問 • 2年前アクセスレベル
アクセスレベル
谷峻輔 · 63問 · 2年前アクセスレベル
アクセスレベル
63問 • 2年前データ型変換
データ型変換
谷峻輔 · 22問 · 2年前データ型変換
データ型変換
22問 • 2年前オブジェクト、項目
オブジェクト、項目
谷峻輔 · 91問 · 2年前オブジェクト、項目
オブジェクト、項目
91問 • 2年前アプリ、タブ
アプリ、タブ
谷峻輔 · 92問 · 2年前アプリ、タブ
アプリ、タブ
92問 • 2年前自動化
自動化
谷峻輔 · 17問 · 2年前自動化
自動化
17問 • 2年前組織、開発モデル
組織、開発モデル
谷峻輔 · 65問 · 2年前組織、開発モデル
組織、開発モデル
65問 • 2年前ボタン、アクション
ボタン、アクション
谷峻輔 · 69問 · 2年前ボタン、アクション
ボタン、アクション
69問 • 2年前インテグレーション
インテグレーション
谷峻輔 · 40問 · 2年前インテグレーション
インテグレーション
40問 • 2年前ページレイアウト、リストビュー、レコードタイプ、プロセス
ページレイアウト、リストビュー、レコードタイプ、プロセス
谷峻輔 · 46問 · 2年前ページレイアウト、リストビュー、レコードタイプ、プロセス
ページレイアウト、リストビュー、レコードタイプ、プロセス
46問 • 2年前標準オブジェクト
標準オブジェクト
谷峻輔 · 42問 · 2年前標準オブジェクト
標準オブジェクト
42問 • 2年前ServiceCloud
ServiceCloud
谷峻輔 · 16問 · 2年前ServiceCloud
ServiceCloud
16問 • 2年前ユーザ、ライセンス、プロファイル、権限セット
ユーザ、ライセンス、プロファイル、権限セット
谷峻輔 · 41問 · 2年前ユーザ、ライセンス、プロファイル、権限セット
ユーザ、ライセンス、プロファイル、権限セット
41問 • 2年前データインポート、エクスポート
データインポート、エクスポート
谷峻輔 · 3回閲覧 · 12問 · 2年前データインポート、エクスポート
データインポート、エクスポート
3回閲覧 • 12問 • 2年前Chatter
Chatter
谷峻輔 · 21問 · 2年前Chatter
Chatter
21問 • 2年前承認申請
承認申請
谷峻輔 · 15問 · 2年前承認申請
承認申請
15問 • 2年前フロー
フロー
谷峻輔 · 44問 · 2年前フロー
フロー
44問 • 2年前レポート、ダッシュボード
レポート、ダッシュボード
谷峻輔 · 65問 · 2年前レポート、ダッシュボード
レポート、ダッシュボード
65問 • 2年前SalesCloud
SalesCloud
谷峻輔 · 18問 · 2年前SalesCloud
SalesCloud
18問 • 2年前標準機能
標準機能
谷峻輔 · 72問 · 2年前標準機能
標準機能
72問 • 2年前宣言的開発
宣言的開発
谷峻輔 · 8問 · 2年前宣言的開発
宣言的開発
8問 • 2年前アクセスレベル
アクセスレベル
谷峻輔 · 52問 · 2年前アクセスレベル
アクセスレベル
52問 • 2年前数式、人力規則
数式、人力規則
谷峻輔 · 17問 · 2年前数式、人力規則
数式、人力規則
17問 • 2年前データ型変換
データ型変換
谷峻輔 · 22問 · 2年前データ型変換
データ型変換
22問 • 2年前オブジェクト、項目
オブジェクト、項目
谷峻輔 · 89問 · 2年前オブジェクト、項目
オブジェクト、項目
89問 • 2年前アプリ、タブ
アプリ、タブ
谷峻輔 · 100問 · 2年前アプリ、タブ
アプリ、タブ
100問 • 2年前自動化
自動化
谷峻輔 · 18問 · 2年前自動化
自動化
18問 • 2年前組織、開発モデル
組織、開発モデル
谷峻輔 · 60問 · 2年前組織、開発モデル
組織、開発モデル
60問 • 2年前ボタン、アクション
ボタン、アクション
谷峻輔 · 69問 · 2年前ボタン、アクション
ボタン、アクション
69問 • 2年前インテグレーション
インテグレーション
谷峻輔 · 40問 · 2年前インテグレーション
インテグレーション
40問 • 2年前ページレイアウト、リストビュー、レコードタイプ、プロセス
ページレイアウト、リストビュー、レコードタイプ、プロセス
谷峻輔 · 44問 · 2年前ページレイアウト、リストビュー、レコードタイプ、プロセス
ページレイアウト、リストビュー、レコードタイプ、プロセス
44問 • 2年前標準オブジェクト
標準オブジェクト
谷峻輔 · 32問 · 2年前標準オブジェクト
標準オブジェクト
32問 • 2年前ServiceCloud
ServiceCloud
谷峻輔 · 17問 · 2年前ServiceCloud
ServiceCloud
17問 • 2年前ユーザ、ライセンス、プロファイル、権限セット
ユーザ、ライセンス、プロファイル、権限セット
谷峻輔 · 18問 · 2年前ユーザ、ライセンス、プロファイル、権限セット
ユーザ、ライセンス、プロファイル、権限セット
18問 • 2年前データインポート、エクスポート
データインポート、エクスポート
谷峻輔 · 11問 · 2年前データインポート、エクスポート
データインポート、エクスポート
11問 • 2年前Chatter
Chatter
谷峻輔 · 21問 · 2年前Chatter
Chatter
21問 • 2年前一時
一時
谷峻輔 · 89問 · 2年前一時
一時
89問 • 2年前一時2
一時2
谷峻輔 · 29問 · 2年前一時2
一時2
29問 • 2年前js9 ドキュメント
js9 ドキュメント
谷峻輔 · 80問 · 2年前js9 ドキュメント
js9 ドキュメント
80問 • 2年前問題一覧
1
Proxy オブジェクトは別のオブジェクトをラップし、プロパティやその他の読み取り/書き込みなどの操作をインターセプトします。 例えば読み取りをインターセプトすることで、存在しないプロパティ値を取得しようしたときに、undefined ではなく0を取得するなど、任意の “デフォルト値”の取得 ロジックを組むことができます。 Proxy は多くのライブラリや一部のブラウザフレームワークで使われています。 Proxy は特別な “エキゾチックオブジェクト(exotic object)” です。Proxy は独自のプロパティは持っていません。空の handler の場合は、透過的に target へ操作を転送します。
2
let proxy = new Proxy(target, handler) target – ラップするオブジェクトです。関数含め何でもOKです。 handler – プロキシ設定: 操作をインターセプトするメソッドである “トラップ” をもつオブジェクトです。 例: get トラップは target のプロパティの読み取り用、set トラップは、target へのプロパティ書き込み用、など。 proxy の操作では、handler に対応するトラップがある場合はそれが実行されます。それ以外の場合は、操作は target で実行されます。 インターセプトとは妨害の意味をもっている。 proxyでラップしたオブジェクトはハンドラーでインターセプトができる。 インターセプトできるのは内部メソッドなので、オブジェクトの標準動作などを変更したい時に利用できる
3
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 specification 及び以下の画像表にリストされています。 このテーブルに、すべての内部ソッドに対するトラップがあります: 操作をインターセプトするために new Proxy の handler パラメータに追加できるメソッド名です
6
内部メソッドのトラップを仕込む際、 内部メソッド特有の満たすべき条件がある。 そのほとんどは戻り値に関してです: [[Set]] は値が正常に書き込まれた場合には true を、そうでなければ false を返す必要があります。 [[Delete]] は値が正常に削除された場合には true を、そうでなければ false を返す必要があります。 他にも以下のようないくつかの不変条件があります: proxy オブジェクトに適用される [[GetPrototypeOf]] は proxy オブジェクトのターゲットオブジェクトに適用される [[GetPrototypeOf]] と同じ値を返さなければなりません。 つまり、proxy のプロトタイプを参照すると、常にターゲットオブジェクトのプロトタイプが返却される必要があります。 traps はこれらの操作をインターセプトできますが、これらのルールには従う必要があります。 不変条件は、言語機能の正しさと一貫した動作を保証するものです。完全な不変条件のリストは 仕様にありますが、変なことをしない限りは違反することはないでしょう。
7
オブジェクトのデフォルト値を実装するのに 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
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
[[OwnPropertyKeys]] 内部メソッド ownKeys トラップ
10
Object.getOwnPropertyNames(obj) は “非” シンボルキーを返します。 Object.getOwnPropertySymbols(obj) はシンボルキーを返します。 Object.keys/values() は enumerable フラグを持つ非シンボルのキー/バリュー値を返します。 for..in は enumerable フラグを持つ非シンボルキーとプロトタイプキーをループします。
11
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
Object.keys は enumerable フラグを持つプロパティだけを返すからです。 それを確かめるため、すべてのメソッドに対し内部メソッド [[GetOwnProperty]] を呼び出し,ディスクリプタ を取得します。すると、ここではプロパティがないので、そのディスクリプタは空であり、enumerable フラグがありません。そのため、スキップされます。
13
内部メソッド [[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
取得、設定、デリート、イテレーション を制御する! 具体的には 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
ユーザがオブジェクトメソッドを持っている場合に、_のついたプロパティにアクセスできるようにするため。 user = { // ... checkPassword(value) { // オブジェクトメソッドは _password へアクセスできなければいけません return value === this._password; } } 使用する場合は、プロキシ経由の呼び出しとなるため、thisはプロキシとなり→エラーとなってしまう。 これを回避するためプロキシ経由ではなく、 直接ラップ元オブジェクトから起動するようにバインドを行う。 ただし、これをやっちゃうと、そのメソッドを渡す際はバインドされちゃってるので、別のコンテキストで実行したい場合にうまく動かなくなるので非推奨、
16
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
プロキシを利用して透過的なラップを行う。 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 は Proxy の作成を簡単にするための組み込みのオブジェクトです。 [[Get]], [[Set]] やその他の内部メソッドは仕様上のものであり、直接呼び出すことはできません。 Reflectは内部メソッドのラッパーとして機能するので、プロキシのトラップの定義の時に利用することで内部メソッドをそのまま動かすことができる。 → したがって、Reflect を使って操作を元のオブジェクトに転送することができます。 例えば [[Get]]を呼び出したい時は、 Reflect.get(obj, prop)を利用することができる。 また、 Proxy でトラップ可能なすべての内部メソッドに対し、対応するReflect用のメソッドがあり、 Proxy トラップと同じ名前、同じ引数を持っている。
19
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
トラップでは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
関数内なのでargumentsが使える get(target, prop, receiver) { return Reflect.get(...arguments); }
22
Map, Set, Date, Promise などの多くの組み込みオブジェクトは、いわゆる “内部スロット” を使用します。 組み込みオブジェクト専用のデータ保持空間といったところか。 それらはプロパティに似ていますが、内部で仕様専用の目的で予約されています。 例えば、Map は内部スロット [[MapData]] にアイテムを保存します また、Mapのsetなどの組み込みメソッドは、[[Get]]/[[Set]] 内部メソッド経由ではなく、直接アクセスします。そのため、Proxy はインターセプトすることができません。
23
プロキシは、内部スロットをもつオブジェクトに対して、透過的な機能を提供できない。 ※プロキシ自体に内部スロットをサポートしてないため 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
プライベートプロパティ内部スロットを利用しているから、プロキシ経由はアクセスできない これは、プライベートフィールドが内部スロットを使用して実装されているからです。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) なプロキシは、無効にすることのできるプロキシです。 プロキシからターゲットオブジェクトへの接続を閉じることができる。 接続をとじると、ターゲットが到達不可能になった場合はガベージコレクションの対象となる。
27
取り消し可能なプロキシの構文は以下のとおり 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
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
解決策は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
できる。 アラートは2を出力する。 eval されたコードは現在のレキシカル環境で実行されるため、外部変数を参照することができます。:
35
strict モードでは、eval で定義したコード内で、独自のレキシカル環境を持ちます。 なので変数宣言しても参照できない。 // 実行可能な例では、'use strict' はデフォルトで有効になっています eval("let x = 5; function f() {}"); alert(typeof x); // undefined (そのような変数はありません) // function f も見えません use strict がなければ、eval は独自のレキシカル環境を持たないので、外側から x や fを見ることができます。
36
現在は、eval を利用する理由はほとんどありません。もし誰かがそれを使用しているなら、モダンな言語構造、あるいは JavaScript Moduleに置き換えるよい機会です。
37
外部変数にアクセスする機能には副作用があることに注意してください。 コードの minifier (JSを本番環境に適用する前に使われるツールで、JSを圧縮(minify)します)は最適化のために、ローカル変数をより短いものに置き換えます。 これは通常安全ですが、eval が使われている場合、圧縮されたローカル変数にはアクセスできないので、 minifierでは、evalで参照する可能性のある全ての変数に関しては圧縮しないようにしている。 なので、コード自体は実行可能だが、圧縮度がおちてしまうので、よくないといわれている。 eval の内部で外部のローカル変数を使用することは、コードのメンテナンスをより難しくするためバッドプラクティスとされています
38
eval されたコードが外部変数を使用していない場合、eval を window.eval(...) で呼び出してください この方法では、コードはグローバルスコープで実行されます。: let x = 1; { let x = 5; window.eval('alert(x)'); // 1 (グローバル変数) } 圧縮対象は外部のローカル変数なので、 グローバル変数は圧縮対象じゃないから圧縮率に迷惑をかけない
39
eval を new Function に変更し、引数としてそれらを渡してください let f = new Function('a', 'alert(a)'); f(5); // 5 new Function については文字列から関数を作成する。 グローバルのレキシカル環境のみ参照可能。そのため、外部レキシカル環境の変数は見えません。 ですが、上記の例のように、引数として明示的に渡すほうがはるかに明白です。
40
f(a)(b)(c)のように呼び出せるようにすること
41
関数を引数にとる。 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 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(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
(user.name == "John" ? user.hi : user.bye)に対して();でメソッド実行している。 これは実行コンテキストが失われたしまっているので、this.nameが参照できなくてエラー
46
特別な参照型な返す。
47
参照型は “仕様上の型” です。私たちは明示的にそれを使うことはできませんが、言語の中で内部的に使われています。 参照型の値は、3つの値の組み合わせ (base, name, strict) です。ここで: base はオブジェクトです。 name はプロパティです。 strict は use strict が効いている場合は true です。 参照型はドット . から呼び出し括弧 () へ情報を渡す目的の特別な “中間” の内部型です。 obj.method() 内の . のようなプロパティの読み取りでは、正確なプロパティ値ではなくプロパティ値とそれが取得されたオブジェクトの両方を保持する特別な “参照型” の値を返します。
48
user.hi へのプロパティアクセスの結果は、関数ではなく参照型です。strict mode での user.hi はこうなります: // 参照型の値 (user, "hi", true)
49
参照型に対して丸括弧 () 呼び出しがされると、それらはオブジェクトとそのメソッドについての完全な情報を受け取り、正しい this (このケースでは user)をセットできます。
50
代入 hi = user.hi のような他の操作は、参照型を破棄し、user.hi(関数)の値を渡します。従って、それ以降の操作は全て this を “失います”。
51
もしくは角括弧 obj[method]()構文を使って直接呼び出された場合のみ正しく渡されます プロパティ・アクセサ(ドットまたは角括弧)が参照型の値を返す。
52
(user.go) の周りの括弧はここではなにもしないことに注意してください。 なので普通にオブジェクトメソッドとして動く
53
これはエラーになってしまう! JavaScript 括弧 (user.go)() の前にはセミコロンを想定していないので、このようにコードを解釈します: let user = { go:... }(user.go)() 構文的にはオブジェクト { go: ...} を引数 (user.go) をもつ関数として呼びだすことができます。また、それは let user と同じ行で起こります。なので、user オブジェクトはまだ定義されていないのでエラーになります。
54
// (1) [object Object] // (2) [object Object] ここでは括弧は操作の順番を変更しません ドットが最初です。 // (3) undefined ここにより複雑な呼び出し (expression).method() があります。この呼出しはまるで2行に分割されたかのようにして動作します。: f = obj.go; // 式を計算します。 f(); // 持っているものを実行します ここで、f() は this なしの関数として実行されます。 (4) undefined (3) と似たようなもので、ドット . の左側に式を持っています メソッド呼び出し以外の操作(代入 =や ||のような)は、 this に設定できる情報を持たない通常の値にします。
55
BigInt は任意の長さの整数をサポートする特別な数値型です。 bigint は n を整数リテラルの末尾に追加するか、文字列や数字などから bigint を作成する関数 BigInt を呼び出すことによって生成されます。 const bigint = 1234567890123456789012345678901234567890n; const sameBigint = BigInt("1234567890123456789012345678901234567890"); const bigintFromNumber = BigInt(10); // 10n と同じ
56
3n 2n 整数なので小数点なし0に向かって丸められる。 エラー 普通の数値と計算するには BigInt(2)で変換する。
57
比較は数値もそのまま有効 true true true false