Skip to content

Instantly share code, notes, and snippets.

@YozhEzhi
Created July 18, 2018 08:10
Show Gist options
  • Select an option

  • Save YozhEzhi/8b99951c0ca9280756baac687afddf4f to your computer and use it in GitHub Desktop.

Select an option

Save YozhEzhi/8b99951c0ca9280756baac687afddf4f to your computer and use it in GitHub Desktop.
Exploring ES6 remarks. Part I.
// 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