Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Metaprogramming in ES2015

WM
October 23, 2016

Metaprogramming in ES2015

@JSDC.TW 2016

WM

October 23, 2016
Tweet

More Decks by WM

Other Decks in Technology

Transcript

  1. Metaprogramming in ES2015
    @kidwm
    JSDC 2016
    1

    View Slide

  2. 2
    Meta-
    programming

    View Slide

  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

    View Slide

  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

    View Slide

  5. Introspection
    The ability of a program to examine the type or properties of an object at
    runtime
    Wikipedia

    5

    View Slide

  6. Reflection
    The ability of a computer program to examine, introspect, and modify its own
    structure and behavior at runtime.
    Wikipedia

    6

    View Slide

  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

    View Slide

  8. 8
    Symbols

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  19. Symbol.hasInstance
    Symbol.hasInstance
    class MyClass {
    static [ ](lho) {
    return Array.isArray(lho);
    }
    }
    assert([] instanceof MyClass);
    19

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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, ]);
    24

    View Slide

  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

    View Slide

  26. 26
    Reflect

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  43. 43
    Proxies

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  58. Thanks!
    @kidwm
    Ref:
    • Metaprogramming in ES6 by Keith Cirkel
    • Exploring ES6 by Dr. Axel Rauschmayer
    58

    View Slide