Created
July 18, 2018 08:10
-
-
Save YozhEzhi/8b99951c0ca9280756baac687afddf4f to your computer and use it in GitHub Desktop.
Exploring ES6 remarks. Part I.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // The repeat() method repeats strings: | |
| > 'doo '.repeat(3) | |
| 'doo doo doo ' | |
| // From indexOf to startsWith: | |
| if (str.indexOf('x') === 0) {} // ES5 | |
| if (str.startsWith('x')) {} // ES6 | |
| // From indexOf to endsWith: | |
| function endsWith(str, suffix) { // ES5 | |
| var index = str.indexOf(suffix); | |
| return index >= 0 | |
| && index === str.length-suffix.length; | |
| } | |
| str.endsWith(suffix); // ES6 | |
| // From indexOf to includes: | |
| if (str.indexOf('x') >= 0) {} // ES5 | |
| if (str.includes('x')) {} // ES6 | |
| // From Array.indexOf to Array.findIndex | |
| const arr = ['a', NaN]; | |
| arr.indexOf(NaN); // -1 in ES5 | |
| arr.findIndex(x => Number.isNaN(x)); // 1 in ES6 | |
| // From Array.slice() to Array.from() or the spread operator. | |
| var arr1 = Array.prototype.slice.call(arguments); // ES5 | |
| const arr2 = Array.from(arguments); // ES6 | |
| // If a value is iterable (as all Array-like DOM data structure are by now), | |
| // you can also use the spread operator (...) to convert it to an Array: | |
| const arr1 = [...'abc']; // ['a', 'b', 'c'] | |
| const arr2 = [...new Set().add('a').add('b')]; // ['a', 'b'] | |
| // Number.isInteger(num) checks whether num is an integer (a number without a | |
| // decimal fraction): | |
| > Number.isInteger(1.05) | |
| false | |
| > Number.isInteger(1) | |
| true | |
| > Number.isInteger(-3.1) | |
| false | |
| > Number.isInteger(-3) | |
| true | |
| Math.trunc() removes the decimal fraction of a number: | |
| > Math.trunc(3.1) | |
| 3 | |
| > Math.trunc(3.9) | |
| 3 | |
| > Math.trunc(-3.1) | |
| -3 | |
| > Math.trunc(-3.9) | |
| -3 | |
| // In ECMAScript 5, you may have used strings to represent concepts such as colors. | |
| // In ES6, you can use symbols and be sure that they are always unique. | |
| // Every time you call Symbol('Red'), a new symbol is created. | |
| // Therefore, COLOR_RED can never be mistaken for another value. | |
| // That would be different if it were the string 'Red'. | |
| const COLOR_RED = Symbol('Red'); | |
| const COLOR_ORANGE = Symbol('Orange'); | |
| const COLOR_YELLOW = Symbol('Yellow'); | |
| const COLOR_GREEN = Symbol('Green'); | |
| const COLOR_BLUE = Symbol('Blue'); | |
| const COLOR_VIOLET = Symbol('Violet'); | |
| function getComplement(color) { | |
| switch (color) { | |
| case COLOR_RED: | |
| return COLOR_GREEN; | |
| case COLOR_ORANGE: | |
| return COLOR_BLUE; | |
| case COLOR_YELLOW: | |
| return COLOR_VIOLET; | |
| case COLOR_GREEN: | |
| return COLOR_RED; | |
| case COLOR_BLUE: | |
| return COLOR_ORANGE; | |
| case COLOR_VIOLET: | |
| return COLOR_YELLOW; | |
| default: | |
| throw new Exception('Unknown color: ' + color); | |
| } | |
| } | |
| const obj = { | |
| [Symbol('my_key')]: 1, | |
| enum: 2, | |
| nonEnum: 3 | |
| }; | |
| Object.defineProperty(obj, 'nonEnum', { enumerable: false }); | |
| `Object.getOwnPropertyNames()` ignores symbol-valued property keys: | |
| > Object.getOwnPropertyNames(obj) | |
| ['enum', 'nonEnum'] | |
| `Object.getOwnPropertySymbols()` ignores string-valued property keys: | |
| > Object.getOwnPropertySymbols(obj) | |
| [Symbol(my_key)] | |
| `Reflect.ownKeys()` considers all kinds of keys: | |
| > Reflect.ownKeys(obj) | |
| [Symbol(my_key), 'enum', 'nonEnum'] | |
| Object.keys() only considers enumerable property keys that are strings: | |
| > Object.keys(obj) | |
| ['enum'] | |
| // Destructuring helps with processing return values: | |
| const [all, year, month, day] = /^(\d\d\d\d)-(\d\d)-(\d\d)$/.exec('2999-12-31'); | |
| // You can use destructuring to swap values. That is something that engines | |
| // could optimize, so that no Array would be created. | |
| [a, b] = [b, a]; | |
| // In ECMAScript 6, you can (ab)use default parameter values to achieve more concise code: | |
| /** | |
| * Called if a parameter is missing and | |
| * the default value is evaluated. | |
| */ | |
| function mandatory() { | |
| throw new Error('Missing parameter'); | |
| } | |
| function foo(mustBeProvided = mandatory()) { | |
| return mustBeProvided; | |
| } | |
| // Interaction: | |
| > foo() | |
| Error: Missing parameter | |
| > foo(123) | |
| 123 | |
| // In ES5, you had to use an IIFE if you wanted to keep a variable local: | |
| (function () { // open IIFE | |
| var tmp = ···; | |
| ··· | |
| }()); // close IIFE | |
| console.log(tmp); // ReferenceError | |
| // In ECMAScript 6, you can simply use a block and a let or const declaration: | |
| { // open block | |
| let tmp = ···; | |
| ··· | |
| } // close block | |
| console.log(tmp); // ReferenceError | |
| // Replace an IIFE with a module: | |
| var my_module = (function () { | |
| // Module-private variable: | |
| var countInvocations = 0; | |
| function myFunc(x) { | |
| countInvocations++; | |
| ··· | |
| } | |
| // Exported by module: | |
| return { | |
| myFunc: myFunc | |
| }; | |
| }()); | |
| // This module pattern produces a global variable and is used as follows: | |
| my_module.myFunc(33); | |
| // In ECMAScript 6, modules are built in, which is why the barrier | |
| // to adopting them is low: | |
| // my_module.js | |
| // Module-private variable: | |
| let countInvocations = 0; | |
| export function myFunc(x) { | |
| countInvocations++; | |
| ··· | |
| } | |
| // This module does not produce a global variable and is used as follows: | |
| import { myFunc } from 'my_module.js'; | |
| myFunc(33); | |
| // Сalling hasOwnProperty via dispatch can cease to work properly if | |
| // Object.prototype.hasOwnProperty is overridden. | |
| var obj1 = { hasOwnProperty: 123 }; | |
| obj1.hasOwnProperty('toString'); // TypeError: Property 'hasOwnProperty' is not a function | |
| // hasOwnProperty may also be unavailable via dispatch if Object.prototype is | |
| // not in the prototype chain of an object. | |
| var obj2 = Object.create(null); | |
| obj2.hasOwnProperty('toString'); // TypeError: Object has no method 'hasOwnProperty' | |
| // In both cases, the solution is to make a direct call to hasOwnProperty: | |
| var obj1 = { hasOwnProperty: 123 }; | |
| Object.prototype.hasOwnProperty.call(obj1, 'hasOwnProperty'); // true | |
| var obj2 = Object.create(null); | |
| Object.prototype.hasOwnProperty.call(obj2, 'toString'); // false | |
| // Arrow functions versus normal functions. | |
| // An arrow function is different from a normal function in only two ways: | |
| // - The following constructs are lexical: arguments, super, this, new.target | |
| // - It can’t be used as a constructor: Normal functions support new via the | |
| // internal method [[Construct]] and the property prototype. | |
| // Arrow functions have neither, which is why new (() => {}) throws an error. | |
| // Apart from that, there are no observable differences between an arrow | |
| // function and a normal function. | |
| // For example, typeof and instanceof produce the same results: | |
| > typeof (() => {}) | |
| 'function' | |
| > () => {} instanceof Function | |
| true | |
| > typeof function () {} | |
| 'function' | |
| > function () {} instanceof Function | |
| true | |
| // ================================ | |
| // New OOP features besides classes | |
| // ================================ | |
| // You can use Object.assign() to add properties to this in a constructor: | |
| class Point { | |
| constructor(x, y) { | |
| Object.assign(this, {x, y}); | |
| } | |
| } | |
| // Providing default values for object properties. | |
| // Create a fresh object, copy the defaults into it and then copy options | |
| // into it, overriding the defaults. | |
| // Object.assign() returns the result of these operations, | |
| // which we assign to options. | |
| const DEFAULTS = { | |
| logLevel: 0, | |
| outputFormat: 'html' | |
| }; | |
| function processContent(options) { | |
| options = Object.assign({}, DEFAULTS, options); // (A) | |
| // other code here; | |
| } | |
| // Adding methods to objects. | |
| Object.assign(SomeClass.prototype, { | |
| someMethod(arg1, arg2) { | |
| ··· | |
| }, | |
| anotherMethod() { | |
| ··· | |
| } | |
| }); | |
| // Cloning objects. | |
| // This way of cloning is also somewhat dirty, because it doesn’t preserve | |
| // the property attributes of orig. | |
| function clone(orig) { | |
| return Object.assign({}, orig); | |
| } | |
| // If you want the clone to have the same prototype as the original, | |
| // you can use Object.getPrototypeOf() and Object.create(): | |
| function clone(orig) { | |
| const origProto = Object.getPrototypeOf(orig); | |
| return Object.assign(Object.create(origProto), orig); | |
| } | |
| // Object.is() provides a way of comparing values that is a bit more | |
| // precise than ===. It works as follows: | |
| > Object.is(NaN, NaN) | |
| true | |
| > Object.is(-0, +0) | |
| false | |
| // Everything else is compared as with ===. | |
| // Find NaN in Arrays. | |
| // indexOf() does not handle NaN well: | |
| > [0,NaN,2].indexOf(NaN); | |
| -1 | |
| // But Object.is() will: | |
| function myIndexOf(arr, elem) { | |
| return arr.findIndex(x => Object.is(x, elem)); | |
| } | |
| myIndexOf([0, NaN, 2], NaN); // 1 | |
| // If an object obj inherits a property prop that is read-only then | |
| // you can’t assign to that property: | |
| const proto = Object.defineProperty({}, 'prop', { | |
| writable: false, | |
| configurable: true, | |
| value: 123, | |
| }); | |
| const obj = Object.create(proto); | |
| obj.prop = 456; // TypeError: Cannot assign to read-only property | |
| // But we can force the creation of an own property by defining a property: | |
| Object.defineProperty(obj, 'prop', {value: 456}); | |
| console.log(obj.prop); // 456 | |
| // Prototypes. | |
| // Each object in JavaScript starts a chain of one or more objects, | |
| // a so-called prototype chain. Each object points to its successor, | |
| // its prototype via the internal slot [[Prototype]] (which is null if | |
| // there is no successor). That slot is called internal, because it only | |
| // exists in the language specification and cannot be directly accessed from | |
| // JavaScript. In ECMAScript 5, the standard way of getting the prototype | |
| // p of an object obj is: | |
| var p = Object.getPrototypeOf(obj); | |
| // There is no standard way to change the prototype of an existing object, | |
| // but you can create a new object obj that has the given prototype p: | |
| var obj = Object.create(p); | |
| // Oldschool __proto__ | |
| // The main reason why __proto__ became popular was because it enabled the | |
| // only way to create a subclass MyArray of Array in ES5: Array instances | |
| // were exotic objects that couldn’t be created by ordinary constructors. | |
| // Therefore, the following trick was used: | |
| function MyArray() { | |
| var instance = new Array(); // exotic object | |
| instance.__proto__ = MyArray.prototype; | |
| return instance; | |
| } | |
| MyArray.prototype = Object.create(Array.prototype); | |
| MyArray.prototype.customMethod = function (···) { ··· } | |
| // ECMAScript 6 enables getting and setting the property __proto__ | |
| // via a getter and a setter stored in Object.prototype. If you were to | |
| // implement them manually, this is roughly what it would look like: | |
| Object.defineProperty(Object.prototype, '__proto__', { | |
| get() { | |
| const _thisObj = Object(this); | |
| return Object.getPrototypeOf(_thisObj); | |
| }, | |
| set(proto) { | |
| if (this === undefined || this === null) { | |
| throw new TypeError(); | |
| } | |
| if (!isObject(this)) { | |
| return undefined; | |
| } | |
| if (!isObject(proto)) { | |
| return undefined; | |
| } | |
| const status = Reflect.setPrototypeOf(this, proto); | |
| if (! status) { | |
| throw new TypeError(); | |
| } | |
| }, | |
| }); | |
| function isObject(value) { | |
| return Object(value) === value; | |
| } | |
| // The getter and the setter for __proto__ in the ES6 spec: | |
| get Object.prototype.__proto__ | |
| set Object.prototype.__proto__ | |
| // Recommendations on __proto__: | |
| // - Use Object.getPrototypeOf() to get the prototype of an object. | |
| // - Prefer Object.create() to create a new object with a given prototype. | |
| // Avoid Object.setPrototypeOf(), which hampers performance on many engines. | |
| // - I actually like __proto__ as an operator in an object literal. It is | |
| // useful for demonstrating prototypal inheritance and for creating dict | |
| // objects. However, the previously mentioned caveats do apply. | |
| // ================================ | |
| // Classes | |
| // ================================ | |
| // Class declarations are not hoisted. | |
| foo(); // works, because `foo` is hoisted | |
| function foo() {} | |
| new Bar(); // ReferenceError | |
| class Bar {} | |
| // The reason for this limitation is that classes can have an extends | |
| // clause whose value is an arbitrary expression. That expression must | |
| // be evaluated in the proper “location”, its evaluation can’t be hoisted. | |
| // Workaround: | |
| function functionThatUsesBar() { | |
| new Bar(); | |
| } | |
| functionThatUsesBar(); // ReferenceError | |
| class Bar {} | |
| functionThatUsesBar(); // OK | |
| // [[Prototype]] is an inheritance relationship between objects, | |
| // while `prototype` is a normal property whose value is an object. | |
| // The property `prototype` is only special w.r.t. the `new` operator | |
| // using its value as the prototype for instances it creates. | |
| // In a derived class, you must call super() before you can use this: | |
| class Foo {} | |
| class Bar extends Foo { | |
| constructor(num) { | |
| const tmp = num * 2; // OK | |
| this.num = num; // ReferenceError | |
| super(); | |
| this.num = num; // OK | |
| } | |
| } | |
| // Implicitly leaving a derived constructor without calling super() | |
| // also causes an error: | |
| class Foo {} | |
| class Bar extends Foo { | |
| constructor() {} | |
| } | |
| const bar = new Bar(); // ReferenceError | |
| // You can now create your own exception classes (that will inherit the | |
| // feature of having a stack trace in most engines): | |
| class MyError extends Error {} | |
| throw new MyError('Something happened!'); | |
| // Private data for classes. | |
| // Private data via constructor environments. | |
| // In this implementation, we store action and counter in the environment | |
| // of the class constructor. An environment is the internal data structure, | |
| // in which a JavaScript engine stores the parameters and local variables | |
| // that come into existence whenever a new scope is entered (e.g. via a | |
| // function call or a constructor call): | |
| class Countdown { | |
| constructor(counter, action) { | |
| Object.assign(this, { | |
| dec() { | |
| if (counter < 1) return; | |
| counter--; | |
| if (counter === 0) { | |
| action(); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| > const c = new Countdown(2, () => console.log('DONE')); | |
| > c.dec(); | |
| > c.dec(); // DONE | |
| // Pros: | |
| // - The private data is completely safe | |
| // - The names of private properties won’t clash with the names of other | |
| // private properties (of superclasses or subclasses). | |
| // Cons: | |
| // - The code becomes less elegant, because you need to add all methods to the | |
| // instance, inside the constructor (at least those methods that need access | |
| // to the private data). | |
| // - Due to the instance methods, the code wastes memory. If the methods were | |
| // prototype methods, they would be shared. | |
| // Private data via a naming convention. | |
| // The following code keeps private data in properties whose names a marked | |
| // via a prefixed underscore: | |
| class Countdown { | |
| constructor(counter, action) { | |
| this._counter = counter; | |
| this._action = action; | |
| } | |
| dec() { | |
| if (this._counter < 1) return; | |
| this._counter--; | |
| if (this._counter === 0) { | |
| this._action(); | |
| } | |
| } | |
| } | |
| // Pros: | |
| // - Code looks nice. | |
| // - We can use prototype methods. | |
| // Cons: | |
| // - Not safe, only a guideline for client code. | |
| // - The names of private properties can clash. | |
| // Private data via WeakMap. | |
| // There is a technique involving WeakMaps that combines the advantage of | |
| // safety with the advantage of being able to use prototype methods. | |
| // This technique is demonstrated in the following code: we use the WeakMaps | |
| // _counter and _action to store private data. | |
| const _counter = new WeakMap(); | |
| const _action = new WeakMap(); | |
| class Countdown { | |
| constructor(counter, action) { | |
| _counter.set(this, counter); | |
| _action.set(this, action); | |
| } | |
| dec() { | |
| let counter = _counter.get(this); | |
| if (counter < 1) return; | |
| counter--; | |
| _counter.set(this, counter); | |
| if (counter === 0) { | |
| _action.get(this)(); | |
| } | |
| } | |
| } | |
| // Each of the two WeakMaps _counter and _action maps objects to their private | |
| // data. Due to how WeakMaps work that won’t prevent objects from being | |
| // garbage-collected. As long as you keep the WeakMaps hidden from the | |
| // outside world, the private data is safe. | |
| // If you want to be even safer, you can store WeakMap.prototype.get and | |
| // WeakMap.prototype.set in variables and invoke those (instead of the methods, dynamically): | |
| const set = WeakMap.prototype.set; | |
| // and use: | |
| set.call(_counter, this, counter); | |
| // instead of: _counter.set(this, counter); | |
| // Then your code won’t be affected if malicious code replaces those methods | |
| // with ones that snoop on our private data. However, you are only protected | |
| // against code that runs after your code. There is nothing you can do if | |
| // it runs before yours. | |
| // Pros: | |
| // - We can use prototype methods. | |
| // - Safer than a naming convention for property keys. | |
| // - The names of private properties can’t clash. | |
| // - Relatively elegant. | |
| // Con: | |
| // - Code is not as elegant as a naming convention. | |
| // Private data via symbols. | |
| const _counter = Symbol('counter'); | |
| const _action = Symbol('action'); | |
| class Countdown { | |
| constructor(counter, action) { | |
| this[_counter] = counter; | |
| this[_action] = action; | |
| } | |
| dec() { | |
| if (this[_counter] < 1) return; | |
| this[_counter]--; | |
| if (this[_counter] === 0) { | |
| this[_action](); | |
| } | |
| } | |
| } | |
| // Each symbol is unique, which is why a symbol-valued property key will | |
| // never clash with any other property key. Additionally, symbols are | |
| // somewhat hidden from the outside world, but not completely: | |
| const c = new Countdown(2, () => console.log('DONE')); | |
| console.log(Object.keys(c)); | |
| // [] | |
| console.log(Reflect.ownKeys(c)); | |
| // [ Symbol(counter), Symbol(action) ] | |
| // Pros: | |
| // - We can use prototype methods. | |
| // - The names of private properties can’t clash. | |
| // Cons: | |
| // - Code is not as elegant as a naming convention. | |
| // - Not safe: you can list all property keys (including symbols!) of an | |
| // object via Reflect.ownKeys(). | |
| // Simple mixins. | |
| // It would be nice if we could include the tool classes like this: | |
| // Invented ES6 syntax: | |
| class Employee extends Storage, Validation, Person { ··· } | |
| // Such templates are called abstract subclasses or mixins. | |
| // One way of implementing a mixin in ES6 is to view it as a function whose | |
| // input is a superclass and whose output is a subclass extending that | |
| // superclass: | |
| const Storage = Sup => class extends Sup { | |
| save(database) { ··· } | |
| }; | |
| const Validation = Sup => class extends Sup { | |
| validate(schema) { ··· } | |
| }; | |
| // Here, we profit from the operand of the extends clause not being a fixed | |
| // identifier, but an arbitrary expression. With these mixins, Employee is | |
| // created like this: | |
| class Employee extends Storage(Validation(Person)) { ··· } | |
| // Gist example https://gist.github.com/sebmarkbage/fac0830dbb13ccbff596 | |
| // The details of classes. | |
| // Classes have inner names. | |
| const fac = function me(n) { | |
| if (n > 0) { | |
| // Use inner name `me` to | |
| // refer to function | |
| return n * me(n-1); | |
| } else { | |
| return 1; | |
| } | |
| }; | |
| console.log(fac(3)); // 6 | |
| // The name me of the named function expression becomes a lexically bound | |
| // variable that is unaffected by which variable currently holds the function. | |
| // Interestingly, ES6 classes also have lexical inner names that you can | |
| // use in methods (constructor methods and regular methods): | |
| class C { | |
| constructor() { | |
| // Use inner name C to refer to class: | |
| console.log(`constructor: ${C.prop}`); | |
| } | |
| logProp() { | |
| // Use inner name C to refer to class: | |
| console.log(`logProp: ${C.prop}`); | |
| } | |
| } | |
| C.prop = 'Hi!'; | |
| const D = C; | |
| C = null; | |
| // C is not a class, anymore: | |
| new C().logProp(); // TypeError: C is not a function | |
| // But inside the class, the identifier C still works: | |
| > new D().logProp(); // constructor: Hi! logProp: Hi! | |
| // ================================ | |
| // Collections | |
| // ================================ | |
| // Access both elements and their indices while looping over an Array (the | |
| // square brackets before of mean that we are using destructuring): | |
| const arr = ['a', 'b']; | |
| for (const [index, element] of arr.entries()) { | |
| console.log(`${index}. ${element}`); | |
| } | |
| // Output: | |
| // 0. a | |
| // 1. b | |
| // Looping over the [key, value] entries in a Map (the square brackets before | |
| // of mean that we are using destructuring): | |
| const map = new Map([ | |
| [false, 'no'], | |
| [true, 'yes'], | |
| ]); | |
| for (const [key, value] of map) { | |
| console.log(`${key} => ${value}`); | |
| } | |
| // Output: | |
| // false => no | |
| // true => yes | |
| // for-of only works with iterable values | |
| // Array-like, but not iterable! | |
| const arrayLike = { length: 2, 0: 'a', 1: 'b' }; | |
| for (const x of arrayLike) { // TypeError | |
| console.log(x); | |
| } | |
| for (const x of Array.from(arrayLike)) { // OK | |
| console.log(x); | |
| } | |
| // Iteration variables: const declarations versus var declarations | |
| const arr = []; | |
| for (var elem of [0, 1, 2]) { | |
| arr.push(() => elem); | |
| } | |
| console.log(arr.map(f => f())); // [2, 2, 2] | |
| // `elem` exists in the surrounding function: | |
| console.log(elem); // 2 | |
| for (const elem of [0, 1, 2]) { | |
| arr.push(() => elem); // save `elem` for later | |
| } | |
| console.log(arr.map(f => f())); // [0, 1, 2] | |
| // `elem` only exists inside the loop: | |
| console.log(elem); // ReferenceError: elem is not defined | |
| // A let declaration works the same way as a const declaration | |
| // (but the bindings are mutable). | |
| // Caching computed results via WeakMaps. | |
| // With WeakMaps, you can associate previously computed results with objects, | |
| // without having to worry about memory management. The following function | |
| // countOwnKeys is an example: it caches previous results in the WeakMap cache. | |
| const cache = new WeakMap(); | |
| function countOwnKeys(obj) { | |
| if (cache.has(obj)) { | |
| console.log('Cached'); | |
| return cache.get(obj); | |
| } else { | |
| console.log('Computed'); | |
| const count = Object.keys(obj).length; | |
| cache.set(obj, count); | |
| return count; | |
| } | |
| } | |
| // If we use this function with an object obj, you can see that the result is | |
| // only computed for the first invocation, while a cached value is used for the | |
| // second invocation: | |
| > const obj = { foo: 1, bar: 2}; | |
| > countOwnKeys(obj) | |
| Computed | |
| 2 | |
| > countOwnKeys(obj) | |
| Cached | |
| 2 | |
| // Managing listeners. | |
| const _objToListeners = new WeakMap(); | |
| function addListener(obj, listener) { | |
| if (! _objToListeners.has(obj)) { | |
| _objToListeners.set(obj, new Set()); | |
| } | |
| _objToListeners.get(obj).add(listener); | |
| } | |
| function triggerListeners(obj) { | |
| const listeners = _objToListeners.get(obj); | |
| if (listeners) { | |
| for (const listener of listeners) { | |
| listener(); | |
| } | |
| } | |
| } | |
| const obj = {}; | |
| addListener(obj, () => console.log('hello')); | |
| addListener(obj, () => console.log('world')); | |
| triggerListeners(obj); | |
| // Output: | |
| // hello | |
| // world | |
| // The advantage of using a WeakMap here is that, once an object is | |
| // garbage-collected, its listeners will be garbage-collected, too. | |
| // In other words: there won’t be any memory leaks. | |
| // Keeping private data via WeakMaps. | |
| const _counter = new WeakMap(); | |
| const _action = new WeakMap(); | |
| class Countdown { | |
| constructor(counter, action) { | |
| _counter.set(this, counter); | |
| _action.set(this, action); | |
| } | |
| dec() { | |
| let counter = _counter.get(this); | |
| if (counter < 1) return; | |
| counter--; | |
| _counter.set(this, counter); | |
| if (counter === 0) { | |
| _action.get(this)(); | |
| } | |
| } | |
| } | |
| // ================================ | |
| // Iterables | |
| // ================================ | |
| // Example how to return iterables. | |
| function objectEntries(obj) { | |
| let iter = Reflect.ownKeys(obj)[Symbol.iterator](); | |
| return { | |
| [Symbol.iterator]() { | |
| return this; | |
| }, | |
| next() { | |
| let { done, value: key } = iter.next(); | |
| if (done) { | |
| return { done: true }; | |
| } | |
| return { value: [key, obj[key]] }; | |
| } | |
| }; | |
| } | |
| const obj = { first: 'Jane', last: 'Doe' }; | |
| for (const [key,value] of objectEntries(obj)) { | |
| console.log(`${key}: ${value}`); | |
| } | |
| // Output: | |
| // first: Jane | |
| // last: Doe | |
| // zip(...iterables) | |
| function zip(...iterables) { | |
| const iterators = iterables.map(i => i[Symbol.iterator]()); | |
| let done = false; | |
| return { | |
| [Symbol.iterator]() { | |
| return this; | |
| }, | |
| next() { | |
| if (!done) { | |
| const items = iterators.map(i => i.next()); | |
| done = items.some(item => item.done); | |
| if (!done) { | |
| return { value: items.map(i => i.value) }; | |
| } | |
| // Done for the first time: close all iterators | |
| for (const iterator of iterators) { | |
| if (typeof iterator.return === 'function') { | |
| iterator.return(); | |
| } | |
| } | |
| } | |
| // We are done | |
| return { done: true }; | |
| } | |
| } | |
| } | |
| const zipped = zip(['a', 'b', 'c'], ['d', 'e', 'f', 'g']); | |
| for (const x of zipped) { | |
| console.log(x); | |
| } | |
| // Output: | |
| // ['a', 'd'] | |
| // ['b', 'e'] | |
| // ['c', 'f'] | |
| // ================================ | |
| // Generators | |
| // ================================ | |
| // Generators are functions that can be paused and resumed (think cooperative | |
| // multitasking or coroutines), which enables a variety of applications. | |
| // You can use generators to tremendously simplify working with Promises. | |
| // Let’s look at a Promise-based function fetchJson() and how it can be | |
| // improved via generators. | |
| function fetchJson(url) { | |
| return fetch(url) | |
| .then(request => request.text()) | |
| .then(text => JSON.parse(text)) | |
| .catch(error => console.log(`ERROR: ${error.stack}`)); | |
| } | |
| async function fetchJson(url) { | |
| try { | |
| let request = await fetch(url); | |
| let text = await request.text(); | |
| return JSON.parse(text); | |
| } | |
| catch (error) { | |
| console.log(`ERROR: ${error.stack}`); | |
| } | |
| } | |
| fetchJson('http://example.com/some_file.json').then(obj => console.log(obj)); | |
| // Generators can receive input from next() via yield. That means that you can | |
| // wake up a generator whenever new data arrives asynchronously and to | |
| // the generator it feels like it receives the data synchronously. | |
| // Generators can play 3 roles: | |
| // 1. Iterators (data producers). | |
| // Each `yield` can return a value via next(), which means that generators can | |
| // produce sequences of values via loops and recursion. Due to generator objects | |
| // implementing the interface Iterable, these sequences can be processed by any | |
| // ES6 construct that supports iterables. | |
| // Two examples are: `for-of` loops and the spread operator (`...`). | |
| // 2. Observers (data consumers). | |
| // `yield` can also receive a value from `next()` (via a parameter). | |
| // That means that generators become data consumers that pause until a new value | |
| // is pushed into them via `next()`. | |
| // 3. Coroutines (data producers and consumers). | |
| // Given that generators are pausable and can be both data producers and data | |
| // consumers, not much work is needed to turn them into coroutines | |
| // (cooperatively multitasked tasks). | |
| // yield* considers end-of-iteration values | |
| // Most constructs that support iterables ignore the value included in the | |
| // end-of-iteration object (whose property done is true). Generators provide | |
| // that value via return. The result of yield* is the end-of-iteration value: | |
| function* genFuncWithReturn() { | |
| yield 'a'; | |
| yield 'b'; | |
| return 'The result'; | |
| } | |
| function* logReturned(genObj) { | |
| const result = yield* genObj; | |
| console.log(result); // (A) | |
| } | |
| // If we want to get to line A, we first must iterate over all values yielded | |
| // by logReturned(): | |
| > [...logReturned(genFuncWithReturn())] | |
| The result | |
| [ 'a', 'b' ] | |
| // Iterating over trees | |
| // Iterating over a tree with recursion is simple, writing an iterator for | |
| // a tree with traditional means is complicated. That’s why generators shine | |
| // here: they let you implement an iterator via recursion. As an example, | |
| // consider the following data structure for binary trees. It is iterable, | |
| // because it has a method whose key is Symbol.iterator. That method is a | |
| // generator method and returns an iterator when called. | |
| class BinaryTree { | |
| constructor(value, left=null, right=null) { | |
| this.value = value; | |
| this.left = left; | |
| this.right = right; | |
| } | |
| /** Prefix iteration */ | |
| * [Symbol.iterator]() { | |
| yield this.value; | |
| if (this.left) { | |
| yield* this.left; | |
| // Short for: yield* this.left[Symbol.iterator]() | |
| } | |
| if (this.right) { | |
| yield* this.right; | |
| } | |
| } | |
| } | |
| // The following code creates a binary tree and iterates over it via for-of: | |
| const tree = new BinaryTree('a', | |
| new BinaryTree('b', | |
| new BinaryTree('c'), | |
| new BinaryTree('d')), | |
| new BinaryTree('e')); | |
| for (const x of tree) { | |
| console.log(x); | |
| } | |
| // Output: | |
| // a | |
| // b | |
| // c | |
| // d | |
| // e | |
| // Sending values via next() | |
| // If you use a generator as an observer, you send values to it via next() | |
| // and it receives those values via yield: | |
| function* dataConsumer() { | |
| console.log('Started'); | |
| console.log(`1. ${yield}`); // (A) | |
| console.log(`2. ${yield}`); | |
| return 'result'; | |
| } | |
| // Let’s use this generator interactively. First, we create a generator object: | |
| > const genObj = dataConsumer(); | |
| // We now call genObj.next(), which starts the generator. | |
| // Execution continues until the first yield, which is where the generator pauses. | |
| // The result of next() is the value yielded in line A (undefined, because yield | |
| // doesn’t have an operand). In this section, we are not interested in what next() | |
| // returns, because we only use it to send values, not to retrieve values. | |
| > genObj.next() | |
| Started | |
| { value: undefined, done: false } | |
| // We call next() two more times, in order to send the value 'a' to the first | |
| // yield and the value 'b' to the second yield: | |
| > genObj.next('a') | |
| 1. a | |
| { value: undefined, done: false } | |
| > genObj.next('b') | |
| 2. b | |
| { value: 'result', done: true } | |
| // The result of the last next() is the value returned from dataConsumer(). | |
| // done being true indicates that the generator is finished. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment