Get to know metaprogramming in JavaScript

This article is shared from the Huawei Cloud Community " Metaprogramming to Make Code More Descriptive, Expressive, and Flexible " by Ye Yiyi.

background

In the second half of last year, I added many technical books to my WeChat bookshelf in various categories, and read some of them intermittently.

Reading without a plan will yield little results.

As the new year begins, I’m ready to try something else, like a reading week. Set aside 1 to 2 non-consecutive weeks each month to read a book in its entirety.

Although this "how to play" is common and rigid, it is effective. I have been reading it for three months.

There are two reading plans for April, and the "JavaScript You Don't Know" series has come to an end.

 

Already read books : "The Way to Simple Architecture", "Node.js in a Simple and Easy Way", "JavaScript You Don't Know (Volume 1)", "JavaScript You Don't Know (Volume 2)".

 

 

Current Reading Week book : "JavaScript You Don't Know (Volume 2)".

 

metaprogramming

function name

There are many ways to express a function in a program, and it is not always clear what the "name" of the function should be.

More importantly, we need to determine whether the "name" of the function is simply its name attribute (yes, functions have an attribute called name), or whether it points to its lexically bound name, such as function bar(){. bar in .}.

The name attribute is used for metaprogramming purposes.

By default the function's lexical name (if any) is also set to its name attribute. In fact, the ES5 (and previous) specification does not formally require this behavior. The setting of the name attribute is non-standard, but still relatively reliable. This has been standardized in ES6.

In ES6, there is now a set of derivation rules that can reasonably assign a value to the name attribute of a function, even if the function does not have a lexical name available.

for example:

var abc = function () {
  // ..
};

abc.name; // "abc"

Here are a few other forms of name derivation (or lack thereof) in ES6:

(function(){ .. });                      // name:
(function*(){ .. });                     // name:
window.foo = function(){ .. };            // name:
class Awesome {
    constructor() { .. }                  // name: Awesome
    funny() { .. }                        // name: funny
}

var c = class Awesome { .. };             // name: Awesome
var o = {
    foo() { .. },                          // name: foo
    *bar() { .. },                        // name: bar
    baz: () => { .. },                    // name: baz
    bam: function(){ .. },               // name: bam
    get what() { .. }, // name: get what
    set fuz() { .. },                    // name: set fuz
    ["b" + "iz"]:
      function(){ .. },                // name: biz
    [Symbol( "buz" )]:
      function(){ .. }                 // name: [buz]
};

var x = o.foo.bind( o );                 // name: bound foo
(function(){ .. }).bind( o );             // name: bound
export default function() { .. }     // name: default
var y = new Function();              // name: anonymous
where GeneratorFunction =
    function*(){}.  proto  .constructor;
var z = new GeneratorFunction();     // name: anonymous

By default, the name property is not writable, but it is configurable, which means that it can be modified manually using Object.defineProperty(..) if necessary.

meta attribute

Meta-attributes provide special meta-information in the form of attribute access that cannot be obtained by other methods.

Taking new.target as an example, the keyword new is used as the context for attribute access. Obviously, new itself is not an object, so this function is very special. When new.target is used inside a constructor call (a function/method triggered by new), new becomes a virtual context, allowing new.target to point to the target constructor that calls new.

This is a clear example of a metaprogramming operation, because its purpose is to determine from inside the constructor call what the original new target was, generally speaking for introspection (checking type/structure) or static property access.

For example, you might want to take different actions inside a constructor depending on whether it's called directly or via a subclass:

class Parent {
  constructor() {
    if (new.target === Parent) {
      console.log('Parent instantiated');
    } else {
      console.log('A child instantiated');
    }
  }
}

class Child extends Parent {}

var a = new Parent();
// Parent instantiated

var b = new Child();
// A child instantiated

The constructor() inside the Parent class definition is actually given the lexical name of the class (Parent), even though the syntax implies that the class is a separate entity from the constructor.

public symbol

JavaScript pre-defines some built-in symbols called public symbols (Well-Known Symbol, WKS).

These symbols are defined mainly to provide specialized meta-properties so that these meta-properties can be exposed to JavaScript programs to gain more control over JavaScript behavior.

Symbol.iterator

Symbol.iterator represents a special location (attribute) on any object. The language mechanism automatically finds a method at this location. This method constructs an iterator to consume the value of this object. Many object definitions have a default value for this symbol.

However, you can also define your own iterator logic for arbitrary object values ​​by defining the Symbol.iterator property, even if this overrides the default iterator. The metaprogramming aspect here is that we define a behavioral attribute that can be used by other parts of JavaScript (i.e. operators and loop constructs) when dealing with the defined object.

for example:

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

for (var v of arr) {
  console.log(v);
}
// 4 5 6 7 8 9

// Define an iterator that only produces values ​​at odd index values
arr[Symbol.iterator] = function* () {
  where idx = 1;
  do {
    yield this[idx];
  } while ((idx += 2) < this.length);
};

for (var v of arr) {
  console.log(v);
}
// 5 7 9

Symbol.toStringTag and Symbol.hasInstance

One of the most common metaprogramming tasks is to introspect a value to find out what kind it is, usually to determine what operations are appropriate to perform on it. For objects, the most commonly used introspection techniques are toString() and instanceof.

In ES6, you can control the behavior of these operations:

function Foo(greeting) {
  this.greeting = greeting;
}

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

Object.defineProperty(Foo, Symbol.hasInstance, {
  value: function (inst) {
    return inst.greeting == 'hello';
  },
});

var a = new Foo('hello'),
  b = new Foo('world');

b[Symbol.toStringTag] = 'cool';

a.toString(); // [object Foo]
String(b); // [object cool]
a instanceof Foo; // true

b instanceof Foo; // false

The @@toStringTag notation of the prototype (or the instance itself) specifies the string value used when [object] is stringified.

The @@hasInstance notation is a method on the constructor function that accepts an instance object value and returns true or false to indicate whether the value can be considered an instance.

Symbol.species

Which constructor to use (Array(..) or a custom subclass) when creating a subclass of Array and want to define inherited methods (such as slice(..)). By default, calling slice(..) on an instance of an Array subclass creates a new instance of this subclass.

This requirement can be metaprogrammed by overriding the default @@species definition of a class:

class Cool {
  // Defer @@species to subclasses
  static get [Symbol.species]() {
    return this;
  }

  again() {
    return new this.constructor[Symbol.species]();
  }
}

class Fun extends Cool {}

class Awesome extends Cool {
  //Force @@species to be specified as the parent constructor
  static get [Symbol.species]() {
    return Cool;
  }
}

var a = new Fun(),
  b = new Awesome(),
  c = a.again(),
  d = b.again();

c instanceof Fun; // true
d instanceof Awesome; // false
d instanceof Cool; // true

The default behavior of Symbol.species on built-in native constructors is to return this. There is no default value on the user class, but as shown, this behavioral feature is easy to simulate.

If you need to define a method for generating new instances, use new this.constructor[Symbol.species](..) pattern metaprogramming instead of hardcoding new this.constructor(..) or new XYZ(..). Inheriting classes can then customize Symbol.species to control which constructor generates these instances.

acting

One of the most obvious new metaprogramming features in ES6 is the Proxy feature.

A proxy is a special object you create that "encapsulates" another ordinary object - or stands in front of this ordinary object. You can register a special processing function (that is, trap) on the proxy object. This program will be called when performing various operations on the proxy. These handlers have the opportunity to perform additional logic in addition to forwarding operations to the original target/encapsulated object.

An example of a trap handler function you can define on a proxy is get, which intercepts the [[Get]] operation when you try to access an object's properties.

var obj = { a: 1 },
  handlers = {
    get(target, key, context) {
      // Note: target === obj,
      // context === pobj
      console.log('accessing: ', key);
      return Reflect.get(target, key, context);
    },
  },
  pobj = new Proxy(obj, handlers);

obj.a;
// 1
pobj.a;
// accessing: a
// 1

We declare a get(..) processing function naming method on the handlers (the second parameter of Proxy(..)) object, which accepts a target object reference (obj), key attribute name ("a") and body literals and self/receiver/agent (pobj).

Agency limitations

A wide set of basic operations that can be performed on objects can be handled through these metaprogramming function traps. But there are some operations that cannot (at least for now) be intercepted.

var obj = { a:1, b:2 },
handlers = { .. },
pobj = new Proxy( obj, handlers );
typeof obj;
String( obj );

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

Summarize

Let’s summarize the main contents of this article:

  • 在ES6之前,JavaScript已经有了不少的元编程功能,而ES6提供了几个新特性,显著提高了元编程能力。
  • From function name derivation for anonymous functions to meta-properties that provide information about how a constructor is called, you can look deeper into the structure of your program's runtime than ever before. By exposing symbols, you can override original features, such as type conversion from objects to native types. Proxies can intercept and customize various underlying operations of objects, and Reflect provides tools to simulate them.
  • The original author recommends: First, focus on understanding how the core mechanism of this language works. And once you really understand how JavaScript itself works, it's time to start using these powerful metaprogramming capabilities to further apply the language.

Click to follow and learn about Huawei Cloud’s new technologies as soon as possible~

Linus took matters into his own hands to prevent kernel developers from replacing tabs with spaces. His father is one of the few leaders who can write code, his second son is the director of the open source technology department, and his youngest son is a core contributor to open source. Huawei: It took 1 year to convert 5,000 commonly used mobile applications Comprehensive migration to Hongmeng Java is the language most prone to third-party vulnerabilities. Wang Chenglu, the father of Hongmeng: open source Hongmeng is the only architectural innovation in the field of basic software in China. Ma Huateng and Zhou Hongyi shake hands to "remove grudges." Former Microsoft developer: Windows 11 performance is "ridiculously bad " " Although what Laoxiangji is open source is not the code, the reasons behind it are very heartwarming. Meta Llama 3 is officially released. Google announces a large-scale restructuring
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4526289/blog/11054218