Metaprogramming in ES2015

75307af344414a724528f5ba3920d01c?s=47 WM
October 23, 2016

Metaprogramming in ES2015

@JSDC.TW 2016

75307af344414a724528f5ba3920d01c?s=128

WM

October 23, 2016
Tweet

Transcript

  1. Metaprogramming in ES2015 @kidwm JSDC 2016 1

  2. 2 Meta- programming

  3. Metaprogramming The art of writing of computer programs with the

    ability to treat programs as their data. It means that a program could be designed to read, generate, analyse or transform other programs, and even modify itself while running. Wikipedia ” 3
  4. Metaprogramming 1. Program transformation • Babel 2. Dynamic execution of

    expressions that contain programming commands, often composed from strings. • eval() 3. Expose the internals of the run-time engine to the programming code through application programming interfaces 4
  5. Introspection The ability of a program to examine the type

    or properties of an object at runtime Wikipedia ” 5
  6. Reflection The ability of a computer program to examine, introspect,

    and modify its own structure and behavior at runtime. Wikipedia ” 6
  7. Reflection in JavaScript • Operators: typeof , instanceof , and

    delete • Function# name and Function# length • Function# bind , Function# call , and Function# apply • All of the available methods on Object are Reflection 7
  8. 8 Symbols

  9. Symbol() // prints "Symbol()" to the console 'symbol' // TypeError:

    Symbol is not a constructor Symbols New primitive type in ECMAScript console.log(Symbol()); assert(typeof Symbol() === ) new Symbol(); 9
  10. Symbol('foo') notEqual // prints "Symbol(foo)" toString() Symbols 1. Constants representing

    concepts 2. Unique property keys assert. (Symbol('foo'), Symbol('foo')); console.log(Symbol('foo')); assert(Symbol('foo'). === 'Symbol(foo)'); 10
  11. Boolean(sym) === !!sym Object(sym) Object.keys(sym) Symbols are not coercible to

    primitives const sym = Symbol('desc'); const str1 = '' + sym; // TypeError const str2 = `${sym}`; // TypeError const str3 = +Symbol() // TypeError const str4 = String(sym); // 'Symbol(desc)' const str5 = sym.toString(); // 'Symbol(desc)' ; ; ; 11
  12. Symbol.for('foo') Global Symbols registry var myObj = {}; var fooSym

    = ; var otherSym = Symbol.for('foo'); myObj[fooSym] = 'baz'; myObj[otherSym] = 'bing'; assert(fooSym === otherSym); assert(myObj[fooSym] === 'bing' && myObj[otherSym] === 'bing'); 12
  13. notEqual iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo') Global Symbols registry is cross-realm iframe

    = document.createElement('iframe'); iframe.src = String(window.location); document.body.appendChild(iframe); assert. (iframe.contentWindow.Symbol, Symbol); assert( ); 13
  14. Symbol.keyFor Symbols registry query var localFooSymbol = Symbol('foo'); var globalFooSymbol

    = Symbol.for('foo'); assert( (localFooSymbol) === undefined); assert(Symbol.keyFor(globalFooSymbol) === 'foo'); assert(Symbol.for(Symbol.keyFor(globalFooSymbol)) === Symbol.for('foo')); 14
  15. Symbols as keys of properties 1. Symbols cannot be read

    using existing reflection tools before ES2015 • Object.getOwnPropertySymbols() 2. Symbols are not private • Object.getOwnSymbols() , Reflect.ownKeys(obj) 3. Enumerable Symbols can be copied to other objects • Object.assign(newObject, objectWithSymbols) 15
  16. // foo1 and foo2 could both be `Symbol()` Symbols used

    as unique property keys var foo1 = Symbol('foo'); var foo2 = Symbol('foo'); var object = { [foo1]: 1, [foo2]: 2, }; assert(object[foo1] === 1 && object[foo2] === 2); 16
  17. Symbols will never conflict with Object string keys Public properties

    usually have string keys, but for non-public properties in inheritance hierarchies, accidental name clashes can become a problem. Against unauthorized access, use WeakMap() for private data in classes. Symbols having unique identities makes them ideal as keys of meta-level properties 17
  18. Symbol.iterator Symbol.iterator const obj = { data: [ 'hello', 'world'

    ], [ ]() { const self = this; let index = 0; return { next() { return index < self.data.length ? { value: self.data[index++] } : { done: true }; }};}}; for (const x of obj) { console.log(x);} 18
  19. Symbol.hasInstance Symbol.hasInstance class MyClass { static [ ](lho) { return

    Array.isArray(lho); } } assert([] instanceof MyClass); 19
  20. Symbol.toPrimitive Symbol.toPrimitive class Answer { [ ](hint) { if (hint

    === 'string') { return 'fourtytwo';} else if (hint === 'number') { return 42;} else { return 42;}}} var answer = new Answer(); +answer === 42; Number(answer) === 42; ''+answer === 'fourtytwo'; String(answer) === 'fourtytwo'; 20
  21. Symbol.toStringTag Symbol.toStringTag default string description of an object: ‘[object ‘+obj[Symbol.toStringTag]+’]’

    class Collection { get [ ]() { return 'Collection';} } var x = new Collection(); Object.prototype.toString.call(x) === '[object Collection]' 21
  22. Symbol.species Symbol.species determine the type for derived objects, default return

    constructor of "this". class Foo extends Array {get [ ]() {return this}} class Bar extends Array {get [Symbol.species]() {return Array}} assert(new Foo().map(function(){}) instanceof Foo); assert(new Bar().map(function(){}) instanceof Bar); assert(new Bar().map(function(){}) instanceof Array); 22
  23. Symbol.species Promises use a variant of the species pattern for

    static methods such as Promise.all(iterable), to return Promise from it’s prototype methods class TimeoutPromise extends Promise { static get [Symbol.species]() { return Promise; } } 23
  24. Symbol.isConcatSpreadable Symbol.isConcatSpreadable class Collection extends Array { get [ ]()

    { return false;} } var array = [3, 4]; var collection = new Collection(5, 6); var spreadableTest = [1,2].concat(array).concat(collection); assert.deepEqual(spreadableTest, [1, 2, 3, 4, <Collection>]); 24
  25. Symbol.search String Symbols • Symbol.match • Symbol.replace • Symbol.search •

    Symbol.split class MySearch { constructor(value) { this.value = value; } [ ](string) { return string.indexOf(this.value);}} 'foobar'.search(new MySearch('foo')); // 0 25
  26. 26 Reflect

  27. Reflect new global Object that provides introspection methods • Newer

    versions of common existing Object/Function methods // Object.keys, Object.getOwnPropertyNames, Function.prototype.apply, etc • Different return values: Boolean instead of modified object • Operators as functions ESlint has a prefer-reflect rule 27
  28. Reflect.construct(target, arguments, newTarget=target) Reflect.construct() The new operator as a function

    : Object the optional parameter newTarget points to the constructor that started the current chain of constructor calls. // new.target 28
  29. Reflect.construct Reflect.construct() // ES5 style factory: function greetingFactory(name) { var

    instance = Object.create(Greeting.prototype); Greeting.call(instance, name); return instance; } // ES2015: const greetingFactory = (name) => (Greeting, [name]); 29
  30. Reflect.deleteProperty(target, propertyKey) Reflect.deleteProperty() The delete operator as a function :

    boolean The delete operator also “works” for non-object references(i.e. variables). And throws TypeError in strict mode if you try to delete a non-configurable own property. 30
  31. Reflect.get(target, propertyKey, receiver=target) Reflect.get() : any // Non-objects throws TypeError

    Reflect.get(1, 'foo'); // throws TypeError Reflect.get(false, 'foo'); // throws TypeError // These old styles don't throw assert(1['foo'] === undefined); assert(false['foo'] === undefined); 31
  32. Reflect.get Reflect.get Reflect.get() The optional parameter receiver is needed when

    get reaches a getter later in the prototype chain. Then it provides the value for this . var myObject = { foo: 1, bar: 2, get baz() { return this.foo + this.bar; },} assert( (myObject, 'baz') === 3); var myReceiverObject = { foo: 4, bar: 4,}; assert( (myObject, 'baz', myReceiverObject) === 8); 32
  33. Reflect.has(target, propertyKey) Reflect.has Reflect.has() The in operator as a function.

    : boolean myObject = {foo: 1,}; Object.setPrototypeOf(myObject, { bar: 2, get baz() { return 3;}, }); assert( (myObject, 'baz') === true); 33
  34. Reflect.set(target, propKey, value, receiver=target) Reflect.set Reflect.set() : boolean Like Reflect.get(),

    throw on non-objects, and receiver argument which acts as the this value if target[propKey] is a setter function. var myObject = { foo: 1, set bar(v) { return this.foo = v;}} var myReceiverObject = {foo: 0}; assert( (myObject, 'bar', 1, myReceiverObject)); assert(myReceiverObject.foo === 1); 34
  35. Reflect.apply(target, thisArgument, argumentsList) Reflect.apply Reflect.apply() Same as Function.prototype.apply(), but shorter

    : any const func = () => {}; func.apply = () => {throw new Error('Aha got you!');} func.apply(thisArg, argArray) // have own 'apply' property Function.prototype.apply.call(func, thisArg, argArray) // safe (func, thisArg, argArray); // shorter than above 35
  36. Reflect.defineProperty(target, propertyKey, propDesc) Reflect.defineProperty Reflect.defineProperty() Similar to Object.defineProperty (), will

    throw a TypeError for invalid targets. : boolean Reflect.defineProperty(1, 'foo') // Throw TypeError console.log( ({}, 'foo', { value: 123, writable: false, configurable: false })); // true 36
  37. Reflect.getOwnPropertyDescriptor(target, propertyKey) Reflect.getOwnPropertyDescriptor Reflect.getOwnPropertyDescriptor() Same as Object.getOwnPropertyDescriptor(), throws for invalid

    target : PropDesc | undefined Reflect.getOwnPropertyDescriptor(1, 'foo') // throws TypeError var obj = {}; Object.defineProperty(obj, 'hidden', { value: true, enumerable: false, }); var desc = (obj, 'hidden'); 37
  38. Reflect.getPrototypeOf(target) Reflect.getPrototypeOf Reflect.getPrototypeOf() Same as Object.getPrototypeOf(), throws for invalid target

    : Object | Null var myObj = new FancyThing(); assert( (myObj) === FancyThing.prototype); Object.getPrototypeOf(1); // undefined Reflect.getPrototypeOf(1); // TypeError 38
  39. Reflect.setPrototypeOf(target, proto) Reflect.setPrototypeOf Reflect.setPrototypeOf() Similar with Object.setPrototypeOf(), throw TypeError with

    non-object : boolean var myObj = new FancyThing(); assert( (myObj, Others.prototype) === true); assert(Reflect.getPrototypeOf(myObj) === Others.prototype); Object.freeze(myObj); assert(Reflect.setPrototypeOf(myObj) === false); 39
  40. Reflect.isExtensible(target) Reflect.isExtensible Reflect.isExtensible() Replacement of Object.isExtensible(), throw TypeError with non-object

    : boolean var obj = {}; var NonExtensible = Object.preventExtensions({}); assert( (obj) === true); assert(Reflect.isExtensible(NonExtensible) === false); Reflect.isExtensible(1); // throws TypeError Reflect.isExtensible(false); // throws TypeError 40
  41. Reflect.preventExtensions(target) Reflect.preventExtensions Reflect.preventExtensions() Replacement of Object.preventExtensions(), throw TypeError with non-object

    : boolean var myObject = {}; var ExtendedObj = magicalExtension({}); assert( (myObject) === true); assert(Reflect.preventExtensions(ExtendedObj) === false); Reflect.preventExtensions(1); // throws TypeError Reflect.preventExtensions(false); // throws TypeError 41
  42. Reflect.ownKeys(target) Reflect.ownKeys Reflect.ownKeys() Object.getOwnPropertyNames() and Object.getOwnPropertySymbols() combine, returns all own

    property keys (strings and symbols) in an Array. : Array var myObject = { foo: 1, bar: 2, [Symbol.for('baz')]: 3,}; assert.deepEqual( (myObject), ['foo', 'bar', Symbol.for('baz')] ); 42
  43. 43 Proxies

  44. new Proxy(target, handler) Proxy new global constructor to intercept and

    customize operations performed on objects, wraps the target with handler hooks const proxy = ; If the handler doesn’t intercept an operation then it is performed on the target. That is, it acts as a fallback for the handler. In a way, the proxy wraps the target, so have an impact on performance . 44
  45. new Proxy Proxy Intercession var myObject = {}; var proxiedMyObject

    = (myObject, {/*handler hooks*/}); assert(myObject !== proxiedMyObject); myObject.foo = true; assert(proxiedMyObject.foo === true); proxiedMyObject.bar = true; assert(myObject.bar === true); 45
  46. apply construct ownKeys get set has defineProperty deleteProperty getOwnPropertyDescriptor setPrototypeOf

    getPrototypeOf isExtensible preventExtensions Proxy Handler Hooks const proxy = new Proxy({}, { : Reflect.apply, : Reflect.construct, : Reflect.ownKeys, : Reflect.get, : Reflect.set, : Reflect.has, : Reflect.defineProperty, : Reflect : Reflect.getOwnPropertyDescriptor, : Reflect.setPrototypeOf, : Reflect : Reflect.isExtensible, : Reflect.preventExtensions,}); 46
  47. new Proxy Proxies as prototypes const proto = ({}, {

    get(target, propertyKey, receiver) { console.log('GET '+propertyKey); return target[propertyKey]; }}); const obj = Object.create(proto); obj.bla; // Output: GET bla 47
  48. new Proxy Access negative Array indices by Proxy ⋯ function

    createArray(...elements) { const handler = { get(target, propKey, receiver) { const index = Number(propKey); if (index < 0) propKey = String(target.length + index); return Reflect.get(target, propKey, receiver); }}; const target = []; target.push(...elements); return (target, handler); } 48
  49. ⋯Access negative Array indices by Proxy const arr = createArray('a',

    'b', 'c'); console.log(arr[-1]); // c In the same way, you can use Proxy to exapnd the capability and make composition of other primitives like Map or Set 49
  50. has: () => true return proxy return proxy Using Proxy

    to make chainable API ⋯ function urlBuilder(domain) { var parts = []; var proxy = new Proxy(function () { var returnValue = domain + '/' + parts.join('/'); parts = []; return returnValue; }, { , get: (object, prop) => { parts.push(prop); ;}, }); } 50
  51. ⋯Using Proxy to make chainable API Accessing a restful web

    service var api = urlBuilder('http://api.example.com'); assert(api.products.toys.dolls() === 'http://api.example.com/products/toys/dolls') Use this same pattern to make a tree traversal fluent API like: myDomIsh.div.span.i().textContent 51
  52. Reflect.has Reflect.get Using Proxy to implement a hook of "method

    missing" ⋯ function Foo() { return new Proxy(this, { get: function (object, property) { return (object, property) ? (object, property) : () => console.log('Not existed' + property + ' property!'); }});} 52
  53. ⋯Using Proxy to implement a hook of "method missing" Foo.prototype.bar

    = () => console.log('you called bar.'); foo = new Foo(); foo.bar(); //=> you called bar. foo.this_method_does_not_exist(); // Not existed this_method_does_not_exist property!' 53
  54. Using Proxy to put parameters into function name ⋯ const

    bc = new Proxy({}, { get: (object, methodName) => { var parts = methodName.match(/base(\d+)toBase(\d+)/); var from = parts && parts[1]; var to = parts && parts[2]; if (!parts || from > 36 || to > 36 || from < 2 || to < 2) throw new Error(methodName + ' is not a function'); return (string) => parseInt(string, from).toString(to); }}); 54
  55. ⋯Using Proxy to put parameters into function name bc.base16toBase2('deadbeef') ===

    '11011110101011011011111011101111'; bc.base2toBase16('11011110101011011011111011101111') === 'deadbeef'; This pattern could be also used for dynamic finder: Users.find_by_first_name_and_last_name('Keith', 'David'); 55
  56. Using Proxy to Implement deprecated Object.observe • use handlers: set

    , preventExtensions , deleteProperty and defineProperty • Object.observe could mutate an object, where as Proxy has to return a new one • the unobserve function of Proxy is not a global • proxy-object-observe on npm 56
  57. Recap 1. Symbols are all about Reflection within implementation 2.

    Reflect is all about Reflection through introspection 3. Proxy is all about Reflection through intercession 4. Not yet as advanced, No Operator Overloading 5. Useful for Tooling or Testing 57
  58. Thanks! @kidwm Ref: • Metaprogramming in ES6 by Keith Cirkel

    • Exploring ES6 by Dr. Axel Rauschmayer 58