Lesson 09-New Features in ES6~ES10 Proxy, Reflect, Promise Objects, and Iteration

Proxy

Proxy is used to modify the default behavior of certain operations, akin to metaprogramming, i.e., programming the language itself. It sets up an interception layer in front of the target object, requiring all external access to pass through this layer, allowing for filtering and rewriting of access. The term “Proxy” implies acting as an agent, handling certain operations on behalf of the target, and can be translated as “proxy handler.”

var obj = new Proxy({}, {
  get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`);
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting ${propKey}!`);
    return Reflect.set(target, propKey, value, receiver);
  },
  apply: function(target, thisBinding, args) {
    return args[0];
  },
  has (target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  },
  construct: function(target, args) {
    return {value: args[1]};
  },
  deleteProperty (target, key) {
    invariant(key, 'delete');
    delete target[key];
    return true;
  },
  defineProperty (target, key, descriptor) {
    return false;
  },
  getOwnPropertyDescriptor (target, key) {
    if (key[0] === '_') {
      return;
    }
    return Object.getOwnPropertyDescriptor(target, key);
  },
  getPrototypeOf(target) {
    return proto;
  },
  isExtensible: function(target) {
    console.log("called");
    return true;
  },
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  },
  preventExtensions: function(target) {
    return true;
  },
  setPrototypeOf (target, proto) {
    throw new Error('Changing the prototype is forbidden');
  }
});

Proxy-Supported Interception Operations

Proxy supports the following 13 interception operations:

  • get(target, propKey, receiver): Intercepts property reads, e.g., proxy.foo or proxy['foo'].
  • set(target, propKey, value, receiver): Intercepts property writes, e.g., proxy.foo = v or proxy['foo'] = v, returning a boolean.
  • has(target, propKey): Intercepts propKey in proxy, returning a boolean.
  • deleteProperty(target, propKey): Intercepts delete proxy[propKey], returning a boolean.
  • ownKeys(target): Intercepts Object.getOwnPropertyNames(proxy), Object.getOwnPropertySymbols(proxy), Object.keys(proxy), and for...in loops, returning an array of property names. Unlike Object.keys(), it includes all own properties, not just enumerable ones.
  • getOwnPropertyDescriptor(target, propKey): Intercepts Object.getOwnPropertyDescriptor(proxy, propKey), returning the property descriptor.
  • defineProperty(target, propKey, propDesc): Intercepts Object.defineProperty(proxy, propKey, propDesc) and Object.defineProperties(proxy, propDescs), returning a boolean.
  • preventExtensions(target): Intercepts Object.preventExtensions(proxy), returning a boolean.
  • getPrototypeOf(target): Intercepts Object.getPrototypeOf(proxy), returning an object.
  • isExtensible(target): Intercepts Object.isExtensible(proxy), returning a boolean.
  • setPrototypeOf(target, proto): Intercepts Object.setPrototypeOf(proxy, proto), returning a boolean.
  • apply(target, object, args): Intercepts function calls on a Proxy instance, e.g., proxy(...args), proxy.call(object, ...args), or proxy.apply(...).
  • construct(target, args): Intercepts constructor calls, e.g., new proxy(...args).

Proxy.revocable()

Proxy.revocable() returns an object with a proxy property (a Proxy instance) and a revoke function to cancel the proxy. After calling revoke, accessing the proxy throws an error.

this Issue

While Proxy can intercept access to a target object, it is not a fully transparent proxy. Without interception, it cannot guarantee identical behavior to the target object, primarily because this inside the target object points to the Proxy instance.

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m()  // true

Web Service Client Proxy Interception

function createWebService(baseUrl) {
  return new Proxy({}, {
    get(target, propKey, receiver) {
      return () => httpGet(baseUrl + '/' + propKey);
    }
  });
}

Reflect

Reflect Concept

  1. Moves some internal Object methods (e.g., Object.defineProperty) to the Reflect object. Currently, some methods exist on both Object and Reflect, but new methods will only be added to Reflect, allowing access to internal language methods.
  2. Modifies some Object method results for consistency. For example, Object.defineProperty(obj, name, desc) throws an error if a property cannot be defined, while Reflect.defineProperty(obj, name, desc) returns false.
  3. Converts Object operations to function behavior. For instance, name in obj and delete obj[name] are imperative, whereas Reflect.has(obj, name) and Reflect.deleteProperty(obj, name) are functional.
  4. Reflect methods correspond one-to-one with Proxy methods, allowing Proxy to call Reflect methods for default behavior, serving as a foundation for customization. Regardless of how Proxy alters behavior, Reflect provides the default.
// Old approach
Object.defineProperty(target, property, attributes);
// New approach
Reflect.defineProperty(target, property, attributes)

// Old approach
'assign' in Object // true
// New approach
Reflect.has(Object, 'assign') // true

// Old approach
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
// New approach
Reflect.apply(Math.floor, undefined, [1.75]) // 1

Reflect Static Methods

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

Implementing the Observer Pattern with Proxy

const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

Promise Objects

Basic Usage of Promises

Promise.resolve('foo')
// Equivalent to
new Promise(resolve => resolve('foo'))

const p = Promise.reject('Error occurred');
// Equivalent to
const p = new Promise((resolve, reject) => reject('Error occurred'))

function getFoo () {
  return new Promise(function (resolve, reject){
    resolve('foo');
  });
}
const g = function* () {
  try {
    const foo = yield getFoo();
    console.log(foo);
  } catch (e) {
    console.log(e);
  }
};
function run (generator) {
  const it = generator();
  function go(result) {
    if (result.done) return result.value;
    return result.value.then(function (value) {
      return go(it.next(value));
    }, function (error) {
      return go(it.throw(error));
    });
  }
  go(it.next());
}
run(g);

Promise Methods

  • Promise.try(): Handles exceptions.
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
  • Promise.prototype.then(): Adds callbacks for when the Promise state changes.
  • Promise.prototype.catch(): Specifies the callback for errors.
  • Promise.prototype.finally(): Specifies an operation to run regardless of the Promise’s final state.
  • Promise.all(): Wraps multiple Promises into a new Promise, resolving only when all input Promises resolve.
// Create an array of Promises
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});
  • Promise.race(): Wraps multiple Promises into a new Promise, adopting the state of the first Promise to settle (resolve or reject).
  • Promise.allSettled(): Wraps multiple Promises, resolving when all input Promises settle (either fulfilled or rejected).
  • Promise.any(): Wraps multiple Promises, resolving when any input Promise fulfills; rejects only if all input Promises reject.

Iterator and for…of Loop

An Iterator is a mechanism providing a unified access interface for various data structures. Any data structure with an Iterator interface can be traversed.

The traversal process is as follows:

  1. Create a pointer object pointing to the start of the data structure. The Iterator is essentially a pointer.
  2. The first call to the pointer’s next method moves it to the first member.
  3. Subsequent calls to next move the pointer to the next member.
  4. Continue calling next until the pointer reaches the end.
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}

Native data structures with Iterator interfaces:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • Function arguments object
  • NodeList object
var str = new String("hi");
[...str] // ["h", "i"]
str[Symbol.iterator] = function() {
  return {
    next: function() {
      if (this._first) {
        this._first = false;
        return { value: "bye", done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
};
[...str] // ["bye"]
str // "hi"

let obj = {
  * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};
for (let x of obj) {
  console.log(x);
}

Iterator Object’s return() and throw()

function readLinesSync(file) {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          return { done: false };
        },
        return() {
          file.close();
          return { done: true };
        }
      };
    },
  };
}

for…of Loop

var arr = ['a', 'b', 'c', 'd'];
for (let a in arr) {
  console.log(a); // 0 1 2 3
}
for (let a of arr) {
  console.log(a); // a b c d
}

Generator

Basic Usage

var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
  a.forEach(function (item) {
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  });
};
for (var f of flat(arr)){
  console.log(f);
}

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};
[...myIterable] // [1, 2, 3]

Before ES6, asynchronous programming typically used the following methods:

  • Callback functions
  • Event listeners
  • Publish/subscribe
  • Promise objects

Share your love