週末だー!ということで本の続きを読む。
## Ch. 14 Metaprogramming
今日はメタプログラミング。メタプログラミングは普段使うことはあんまりないから書かなくてもいいかなぁと思いつつ、普段使わないから書かないと全く記憶にも残らないだろうなとも思ったのでメモくらい残しておく。
### Property Attributes
データプロパティとアクセサプロパティはそれぞれ4つの属性を持つ:
- データプロパティ:
value
,writable
,enumerable
,configurable
- アクセサプロパティ:
get
,set
,enumerable
,configurable
ちなみに
value
: 値get
: getter 関数set
: setter 関数writable
: 値を変更可能かどうかenumerable
:for/in
ループやObject.keys()
で列挙可能かどうかconfigurable
: プロパティを削除したり、プロパティの属性を変更したりできるかどうか
こんな感じ:
> const sample = { ... x: 1, ... get octet() { return Math.floor(Math.random()*256); }, ... }; undefined > Object.getOwnPropertyDescriptor(sample, "x"); { value: 1, writable: true, enumerable: true, configurable: true } > Object.getOwnPropertyDescriptor(sample, "octet"); { get: [Function: get octet], set: undefined, enumerable: true, configurable: true }
Object.getOwnPropertyDescriptor()
は own property しか取得できないので、継承しているプロパティの属性を取得したい場合はプロトタイプチェーンから対象のオブジェクトを取得して使う。- 普通にプロパティを定義すると、書き込み可能、列挙可能、属性変更可能になるけど、
Object.defineProperty()
やObject.defineProperties()
を使うと属性を指定してプロパティを作成・変更できる。継承してるプロパティは変更できないので注意。 - プロパティの追加やプロパティ属性の変更をできるかは、色々な条件の組み合わせになっててめんどくさそう。今は覚えずに、そういうことをするときがあったら確認しよっと。
Object.assign()
は列挙可能なプロパティの値のみをコピーする。属性はコピーしない。つまり例えばアクセサプロパティの場合は、getter をコピーするんじゃなくて、getter が返してきた値をコピーするので注意
さっきのやつをそのまま使ってみるとデータプロパティになった。面白い:
> Object.assign({}, sample); { x: 1, octet: 107 }
### Object Extensibility
オブジェクトが拡張可能かどうかは extensible
属性で決まる。
- 通常は拡張可能
Object.isExtensible()
で確認できるObject.preventExtensions()
で拡張不可に変更できる- 一度拡張不可にするともう拡張可能には戻せない
オブジェクトの変更をできないようにするために、プロパティの configurable
writable
属性と組み合わせて使うことが多い
Object.seal()
はオブジェクトを拡張不可にすることに加えて、全ての own properties を nonconfigurable にする。- プロパティの追加や削除、属性の変更ができない
- writable なプロパティの値の変更は可能
- sealed オブジェクトを unsealed に戻すことはできない
- チェックには
Object.isSealed()
を使う
Object.freeze()
はもっと厳しい。オブジェクトを拡張不可にする・全ての own properties を nonconfigurable にすることに加えて、データプロパティをリードオンリーにする(setter を持ってるアクセサプロパティは影響を受けない)Object.isFrozen()
で確認可能
ただ、プロトタイプオブジェクトも含めて全部ロックダウンしたい場合は、チェーンをたどって適用する必要がある。
### Well-Known Symbols
Symbol.iterator と Symbol.asyncIterator
- これはもう知ってるやつだ
Symbol.hasInstance
- ES6 以降ではもし
instanceof
の右側のオブジェクトに[Symbol.hasInstance]
メソッドが定義されてたら、そのメソッドが呼ばれる
> 10 instanceof {[Symbol.hasInstance](v){console.log("called");return v > 5;}} called true > 10 instanceof {[Symbol.hasInstance](v){console.log("called");return v > 10;}} called false
へー
Symbol.toStringTag
toString()
で表示される文字列表現の部分を指定することができる。"[object Range]"
みたいに。指定してなかったら"[object Object]"
になる。
> Object.prototype.toString.call({}) '[object Object]' > Object.prototype.toString.call({[Symbol.toStringTag]:"Sample"}) '[object Sample]' > Object.prototype.toString.call({get [Symbol.toStringTag](){return "Sample2";}}) '[object Sample2]'
ふーむ。まぁ、使わなさそう。
Symbol.species
これはまたなんか面白いなー。そうそう使うことなさそうだけど。
- 例えば
Array()
コンストラクター関数は、このプロパティを読み取り専用で持っている Array
のmap()
関数などは、このプロパティからコンストラクターを取得している- それによって継承先のクラスを使って結果を返すことができる
へー。MyArray
に対して map()
を呼んだら MyArray
で返されてる:
> class MyArray extends Array {} undefined > new MyArray(1,2,3).map(v => v * 2) MyArray(3) [ 2, 4, 6 ]
コンストラクター関数にこのプロパティが定義されてるや:
> Object.getOwnPropertyDescriptor(Array, Symbol.species) { get: [Function: get [Symbol.species]], set: undefined, enumerable: false, configurable: true } > Array[Symbol.species] [Function: Array] > MyArray[Symbol.species] [Function: MyArray]
this.constructor[Symbol.species]
でコンストラクター関数を取ってるってことか。・・・ん? this.constructor
にもコンストラクター関数が入ってると思うんだけど・・・、あぁ、用途が違うから分けてるってことかな。
書き換えるときは、直接値を設定することはできなくて、 getter だけだから。でも configurable だから、こうすれば書き換えられる:
> Object.defineProperty(MyArray, Symbol.species, {value: Array}); [Function: MyArray] > MyArray[Symbol.species] [Function: Array]
Array
になった。じゃもういちど map()
呼び出してみよう:
> new MyArray(1,2,3).map(v => v * 2) [ 2, 4, 6 ]
へー。今回は MyArray
じゃなくて Array
になった。
ふーん。この場合、 this.constructor
には MyArray
が入ったままだけど、this.constructor[Symbol.species]
には Array
が入ってるってことか。
> myArray.constructor [Function: MyArray] > myArray.constructor[Symbol.species] [Function: Array]
他にも Map
や Set
はこのプロパティを持ってるみたいね:
> Object.getOwnPropertyDescriptor(Map, Symbol.species) { get: [Function: get [Symbol.species]], set: undefined, enumerable: false, configurable: true } > Object.getOwnPropertyDescriptor(Set, Symbol.species) { get: [Function: get [Symbol.species]], set: undefined, enumerable: false, configurable: true }
へー。そういうののサブクラスを作るときに覚えておいたら良いのかな。
Symbol.isConcatSpreadable
Array
のconcat
をするときに spread するかどうかを判別するためのプロパティ。- ES6 より前は
Array.isArray()
で判別してたけど、ES6 以降は、まずこのプロパティをチェックして、なければArray.isArray()
で判別する。
# 普通に配列を渡すと展開される > [].concat([1,2,3]) [ 1, 2, 3 ] # ArrayLike オブジェクトを普通に渡しても、そのまま追加される > let arrayLike = {length:2, 0:"a", 1:"b"} undefined > [].concat(arrayLike) [ { '0': 'a', '1': 'b', length: 2 } ] # Symbol.isConcatSpreadable プロパティをつけてあげると展開される > arrayLike = {length:2, 0:"a", 1:"b", [Symbol.isConcatSpreadable]: true} { '0': 'a', '1': 'b', length: 2, [Symbol(Symbol.isConcatSpreadable)]: true } > [].concat(arrayLike) [ 'a', 'b' ]
面白いなー
Pattern-Matching Symbols
string.method(pattern, arg)
は実際にはこう呼び出されてる→pattern[symbol](string, arg)
RegExp
のプロトタイプ見てみたらこうなってた:
> Object.getOwnPropertySymbols(RegExp.prototype); [ Symbol(Symbol.match), Symbol(Symbol.matchAll), Symbol(Symbol.replace), Symbol(Symbol.search), Symbol(Symbol.split) ]
へー。
例えば string.search(pattern, arg)
の pattern のところに RegExp
じゃなくて独自で Symbol.search
メソッドを実装したクラスを渡せばそれが呼び出されるみたい。やらんけど。
Symbol.toPrimitive
- オブジェクトをプリミティブに変換するのには
toString()
やvalueOf()
が使われるけど、ES6以降ではSymbol.toPrimitive
でこの処理を上書きできる:
こういうことなのかな?:
> Number(1) 1 > Number({}) NaN > Number({[Symbol.toPrimitive](){return 10;}}) 10 > String({[Symbol.toPrimitive](){return 10;}}) '10' > String({[Symbol.toPrimitive](){return "aaa";}}) 'aaa'
### Template Tags
- backtick で囲まれた文字列はテンプレートリテラルと呼ばれてるけど、関数の直後にテンプレートリテラルが続くものを “tagged template literal” と呼ぶ。
- DSL を定義するのによく使われる。GraphQL用の
gql
や、Emotion のcss
など。
tagged template literal の関数定義には特別な文法はなくて、通常の関数定義と同じ。引数が以下の通りになっている:
- 第一引数に文字列の配列
- 変数の部分で区切られている。n個変数があったら、n+1個の文字列になっている。
- 第二引数以降に変数の値
これは覚えておきたいなー。
### The Reflect API
- これまでリフレクション系の処理は
Object
の static メソッドとかでやってきたけど、分かりやすいようにReflect
というオブジェクトにもまとめ直したんかな。
### Proxy Objects
- ES6 以降
- ターゲットオブジェクトに対する処理をプロキシしてハンドラーに渡すことができる
まぁ、これもフレームワークを作るんじゃなくて、使う場合には、ほしい場面に出会うことはないだろうな。
### 今日の中で
普通に使うのは Symbol.iterator と Symbol.asyncIterator と tagged template literal くらいかな。
今日も面白かったー。残りはあと3章だけど、長そうな章たちだな。