JavaScript のメタプログラミングを理解する

この記事は、Ye Yiyi によるHuawei クラウド コミュニティ「コードをより記述的、表現的、柔軟にするためのメタプログラミング」から共有されたものです。

背景

昨年の後半、私はさまざまなカテゴリの専門書を WeChat の本棚に追加し、断続的にいくつかを読みました。

計画なしに本を読んでも、ほとんど成果は得られません。

新年が始まると、読書週間など、別のことに挑戦する準備ができています。毎月、連続しない 1 ~ 2 週間を確保して、本を最後まで読むようにしましょう。

この「遊び方」は一般的で堅苦しいですが、私はこれを3か月間読み続けています。

4月の読書企画は2本あり、「あなたの知らないJavaScript」シリーズは終了です。

 

既読の本 : 「シンプル アーキテクチャへの道」、「シンプルで簡単な Node.js」、「あなたの知らない JavaScript (第 1 巻)」、「あなたの知らない JavaScript (第 2 巻)」。

 

 

現在の読書週間の本 : 「あなたの知らない JavaScript (第 2 巻)」。

 

メタプログラミング

関数名

プログラム内で関数を表現する方法はたくさんありますが、関数の「名前」が何であるべきかは必ずしも明確ではありません。

さらに重要なのは、関数の「名前」が単にその name 属性であるのか (はい、関数には name という属性があります)、それとも関数 bar(){ などの字句的にバインドされた名前を指しているのかを判断する必要があります。で 。}。

name 属性はメタプログラミングの目的で使用されます。

デフォルトでは、関数の語彙名 (存在する場合) もその name 属性に設定されます。実際、ES5 (およびそれ以前) の仕様では、この動作は正式には要求されていません。 name 属性の設定は標準ではありませんが、それでも比較的信頼できます。これはES6で標準化されました。

ES6 では、関数に使用可能な語彙名がない場合でも、関数の name 属性に値を合理的に割り当てることができる一連の導出ルールが追加されました。

例えば:

var abc = 関数 () {
  // ..
};

abc.名前; // "ABC"

ES6 における名前派生の他の形式 (またはその欠落) をいくつか示します。

(関数(){ .. }); // 名前:
(関数*(){ .. }); // 名前:
window.foo = function(){ .. }; // 名前:
素晴らしいクラス {
    constructor() { .. } // 名前: 素晴らしい
    面白い() { .. } // 名前: 面白い
}

var c = クラス素晴らしい { .. }; // 名前: すごい
変数 o = {
    foo() { .. }, // 名前: foo
    *bar() { .. }, // 名前: バー
    baz: () => { .. }, // 名前: baz
    bam: function(){ .. }, // 名前: bam
    get what() { .. }, // 名前: 何を取得するか
    set fuz() { .. }, // 名前: set fuz
    ["b" + "iz"]:
      function(){ .. }, // 名前: biz
    [シンボル( "buz" )]:
      function(){ .. } // 名前: [buz]
};

var x = o.foo.bind( o ); // 名前: バインドされた foo
(function(){ .. }).bind( o ); // 名前: バインド
import デフォルト function() { .. } // 名前: デフォルト
var y = 新しい関数(); // 名前: 匿名
ここで、ジェネレーター関数 =
    関数*(){}。プロト .constructor;
var z = 新しい GeneratorFunction(); // 名前: 匿名

デフォルトでは、name プロパティは書き込み可能ではありませんが、構成は可能です。つまり、必要に応じて Object.defineProperty(..) を使用して手動で変更できます。

メタ属性

メタ属性は、他のメソッドでは取得できない特別なメタ情報を属性アクセスの形式で提供します。

new.target を例にとると、キーワード new は属性アクセスのコンテキストとして使用されます。明らかに、 new 自体はオブジェクトではないため、この関数は非常に特殊です。 new.target がコンストラクター呼び出し (new によってトリガーされる関数/メソッド) 内で使用される場合、new は仮想コンテキストとなり、new.target が new を呼び出すターゲット コンストラクターを指すことができるようになります。

これはメタプログラミング操作の明確な例です。その目的は、一般的に言えば、イントロスペクション (型/構造のチェック) または静的プロパティ アクセスのために、元の新しいターゲットが何であったかをコンストラクター呼び出しの内部から判断することだからです。

たとえば、コンストラクターが直接呼び出されるかサブクラス経由で呼び出されるかに応じて、コンストラクター内で異なるアクションを実行することができます。

親クラス {
  コンストラクター() {
    if (new.target === 親) {
      console.log('親がインスタンス化されました');
    } それ以外 {
      console.log('インスタンス化された子');
    }
  }
}

クラスの子は親 {} を拡張します

var a = 新しい親();
// インスタンス化された親

var b = 新しい子();
// インスタンス化された子

Parent クラス定義内のconstructor() には、構文がクラスがコンストラクターとは別のエンティティであることを暗示している場合でも、実際にはクラス (Parent) の語彙名が与えられます。

公共のシンボル

JavaScript では、パブリック シンボル (Well-Known Symbol、WKS) と呼ばれるいくつかの組み込みシンボルが事前定義されています。

これらのシンボルは主に、特殊なメタ プロパティを提供するために定義されており、これらのメタ プロパティを JavaScript プログラムに公開して、JavaScript の動作をより詳細に制御できるようになります。

シンボル.イテレータ

Symbol.iterator は、オブジェクト上の特別な場所 (属性) を表します。言語メカニズムは、この場所でメソッドを自動的に見つけます。このメソッドは、このオブジェクトの値を使用する反復子を構築します。多くのオブジェクト定義には、このシンボルのデフォルト値があります。

ただし、Symbol.iterator プロパティを定義することで、デフォルトのイテレータをオーバーライドする場合でも、任意のオブジェクト値に対して独自のイテレータ ロジックを定義することもできます。ここでのメタプログラミングの側面は、定義されたオブジェクトを処理するときに JavaScript の他の部分 (つまり、演算子やループ構造) で使用できる動作属性を定義することです。

例えば:

var 傷跡 = [4, 5, 6, 7, 8, 9];

for (arr の var v) {
  コンソール.ログ(v);
}
// 4 5 6 7 8 9

// 奇数のインデックス値の値のみを生成するイテレータを定義します
arr[Symbol.iterator] = function* () {
  ここで、idx = 1;
  する {
    これを生成します[idx];
  while ((idx += 2) < this.length);
};

for (arr の var v) {
  コンソール.ログ(v);
}
// 5 7 9

Symbol.toStringTag と Symbol.hasInstance

最も一般的なメタプログラミング タスクの 1 つは、値をイントロスペクトしてその種類を調べることです。通常は、その値に対してどのような操作を実行するのが適切かを判断します。オブジェクトの場合、最も一般的に使用されるイントロスペクション手法は toString() と instanceof です。

ES6 では、次の操作の動作を制御できます。

関数 Foo(挨拶) {
  this.greeting = 挨拶;
}

Foo.prototype[Symbol.toStringTag] = 'Foo';

Object.defineProperty(Foo, Symbol.hasInstance, {
  値: 関数 (inst) {
    return inst.greeting == 'こんにちは';
  }、
});

var a = new Foo('hello'),
  b = 新しい Foo('ワールド');

b[Symbol.toStringTag] = 'クール';

a.toString(); // [オブジェクト Foo]
文字列(b); // [クールなオブジェクト]
Foo のインスタンス。 // 真実

b Foo のインスタンス。 // 間違い

プロトタイプ (またはインスタンス自体) の @@toStringTag 表記は、[object] が文字列化されるときに使用される文字列値を指定します。

@@hasInstance 表記は、インスタンス オブジェクト値を受け入れ、その値がインスタンスとみなせるかどうかを示す true または false を返すコンストラクター関数のメソッドです。

シンボル.種

Array のサブクラスを作成し、継承されたメソッド (slice(..) など) を定義する場合、どのコンストラクター (Array(..) またはカスタム サブクラス) を使用するか。デフォルトでは、Array サブクラスのインスタンスでスライス(..) を呼び出すと、このサブクラスの新しいインスタンスが作成されます。

この要件は、クラスのデフォルトの @@species 定義をオーバーライドすることでメタプログラムできます。

クラスクール{
  // @@species をサブクラスに延期します
  static get [Symbol.species]() {
    これを返します。
  }

  また() {
    新しい this.constructor[Symbol.species]() を返します。
  }
}

クラス 楽しさはクールさを拡張します {}

class Awesome extends Cool {
  // @@species を親コンストラクターとして強制的に指定します
  static get [Symbol.species]() {
    クールに戻ります。
  }
}

var a = 新しい Fun()、
  b = 新しい Awesome()、
  c = a.again()、
  d = b.again();

c インスタンスオブファン; // 真実
d 素晴らしいインスタンス。 // 間違い
d インスタンスの Cool; // 真実

組み込みネイティブ コンストラクター上の Symbol.species のデフォルトの動作は、これを返すことです。ユーザー クラスにはデフォルト値はありませんが、示されているように、この動作の特徴は簡単にシミュレートできます。

新しいインスタンスを生成するメソッドを定義する必要がある場合は、新しい this.constructor(..) または new XYZ(..) をハードコーディングする代わりに、新しい this.constructor[Symbol.species](..) パターン メタプログラミングを使用します。継承したクラスは、Symbol.species をカスタマイズして、どのコンストラクターがこれらのインスタンスを生成するかを制御できます。

演技

ES6 の最も明白な新しいメタプログラミング機能の 1 つは、プロキシ機能です。

プロキシは、別の通常のオブジェクトを「カプセル化」する、またはこの通常のオブジェクトの前に立つ、作成する特別なオブジェクトです。プロキシ オブジェクトに特別な処理関数 (トラップ) を登録できます。このプログラムは、プロキシ上でさまざまな操作を実行するときに呼び出されます。これらのハンドラーには、元のターゲット/カプセル化されたオブジェクトへの転送操作に加えて、追加のロジックを実行する機会があります。

プロキシ上で定義できるトラップ ハンドラー関数の例は get です。これは、オブジェクトのプロパティにアクセスしようとしたときに [[Get]] 操作をインターセプトします。

var obj = { a: 1 }、
  ハンドラー = {
    get(ターゲット、キー、コンテキスト) {
      // 注: target === obj,
      // コンテキスト === pobj
      console.log('アクセス中: ', key);
      return Reflect.get(ターゲット, キー, コンテキスト);
    }、
  }、
  pobj = 新しいプロキシ(obj, ハンドラー);

オブジェクト;
// 1
ポブジャ;
// アクセス:
// 1

ハンドラー (Proxy(..) の 2 番目のパラメーター) オブジェクト上で get(..) 処理関数の命名メソッドを宣言します。これは、ターゲット オブジェクト参照 (obj)、キー属性名 (「a」)、本体リテラルを受け入れます。自己/受信者/エージェント (pobj)。

代理店の制限

オブジェクトに対して実行できる幅広い基本操作は、これらのメタプログラミング関数トラップを通じて処理できます。ただし、(少なくとも現時点では)傍受できない操作もいくつかあります。

var obj = { a:1, b:2 },
ハンドラー = { .. }、
pobj = 新しいプロキシ( obj, handlers );
オブジェクトのタイプ;
文字列(obj);

obj + "";
obj == pobj;
obj === pobj

要約する

この記事の主な内容を要約しましょう。

  • ES6 よりも前の JavaScript にはすでに多くのメタプログラミング機能があり、ES6 ではメタプログラミング機能を大幅に向上させるいくつかの新機能が提供されています。
  • 匿名関数の関数名の導出から、コンストラクターの呼び出し方法に関する情報を提供するメタ プロパティまで、プログラムのランタイムの構造をこれまで以上に深く調べることができます。シンボルを公開すると、オブジェクトからネイティブ型への型変換など、元の機能をオーバーライドできます。プロキシはオブジェクトのさまざまな基本操作をインターセプトしてカスタマイズでき、Reflect はそれらをシミュレートするツールを提供します。
  • オリジナルの著者は次のように推奨しています。 まず、この言語の中心となるメカニズムがどのように機能するかを理解することに集中してください。 JavaScript 自体がどのように機能するかを本当に理解したら、これらの強力なメタプログラミング機能を使用して言語をさらに応用してみましょう。

クリックしてフォローし、できるだけ早くHuawei Cloudの新しいテクノロジーについて学びましょう~

ライナスは、カーネル開発者がタブをスペースに置き換えることを阻止するために自ら問題を解決しました。 彼の父親はコードを書くことができる数少ないリーダーの 1 人であり、次男はオープンソース テクノロジー部門のディレクターであり、末息子は中核です。ファー ウェイ: 一般的に使用されている 5,000 のモバイル アプリケーションを変換するのに 1 年かかった Java はサードパーティの脆弱性が最も発生しやすい言語です。Hongmeng の父: オープンソースの Honmeng は唯一のアーキテクチャ上の革新です。中国の基本ソフトウェア分野で 馬化騰氏と周宏毅氏が握手「恨みを晴らす」 元マイクロソフト開発者:Windows 11のパフォーマンスは「ばかばかしいほど悪い」 老祥基がオープンソースであるのはコードではないが、その背後にある理由は Meta Llama 3 が正式にリリースされ、 大規模な組織再編が発表されました
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/4526289/blog/11054218