JavaScriptの勉強中:その9 Metaprogramming

週末だー!ということで本の続きを読む。

bufferings.hatenablog.com

## 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() コンストラクター関数は、このプロパティを読み取り専用で持っている
  • Arraymap() 関数などは、このプロパティからコンストラクターを取得している
  • それによって継承先のクラスを使って結果を返すことができる

へー。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]

他にも MapSet はこのプロパティを持ってるみたいね:

> 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

  • Arrayconcat をするときに 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章だけど、長そうな章たちだな。