JAVASCRIPT GETTER-SETTER 金字塔

330 阅读6分钟

André Staltz - JavaScript Getter-Setter Pyramid

The cornerstone of JavaScript is the function. It is a flexible abstraction that works as the basis for other abstractions, such as Promises, Iterables, Observables, and others.

JavaScript 的基石是函数。函数是其他抽象概念的基础,例如承诺(Promise)、可迭代对象(Iterables)、可观察对象(Observables)等等。

functions.png

函数提供的好处:

  • 惰性 / 可重用性(laziness / reusability)
  • 实现灵活性(implementation flexibility)

函数是惰性的(除非被调用否则不会执行),所以才能被重用。函数的使用者并不关心函数的内部实现,这意味着可以灵活地使用多种方式实现函数。

getters.png

getter 是一种函数,它不需要传递参数但返回值。 例如 Math.random()Date.now() 等。

function getUser() {
  return { name: "Jack", age: "25" };
}

console.log(getUser().name); // "Jack"

getter 内部的计算也可以是抽象的,因为在 JavaScript中函数是可以作为参数传递的。

function add(getX, getY) {
  return function getZ() {
    const x = getX();
    const y = getY();
    return x + y;
  };
}

const getTen = () => 10;
const getTenPlusRandom = add(getTen, Math.random);

console.log(getTenPlusRandom()); // 10.9862
console.log(getTenPlusRandom()); // 10.5137

setters.png

setter 是一种函数,它传递一个参数但没有返回值。 例如 console.log(x)document.write(x) 等。

function fancyConsoleLog(str) {
  console.log("⭐ " + str + " ⭐");
}

console.log(fancyConsoleLog("Hello World!")); // "⭐ Hello World! ⭐"

getter-getters.png

getter-getter 是一种特殊的 getter,它的返回值是另一个 getter。 例如,展示 2 的幂的数字序列 getGetNextPowerOfTwo():

function getGetNextPowerOfTwo() {
  let i = 2;
  return function getNextPowerOfTwo() {
    const next = i;
    i = i * 2;
    return next;
  };
}

let getNextPowerOfTwo = getGetNextPowerOfTwo();
console.log(getNextPowerOfTwo()); // 2
console.log(getNextPowerOfTwo()); // 4
console.log(getNextPowerOfTwo()); // 8

setter-setters.png

setter-setter 是一种特殊的 setter,它的传递参数是另一个 setter。 虽然 setter 不是抽象的,但是 setter-setter 是能够表示传递参数 setter 的参数值的抽象的。

setter-setter 提供的好处:

  • 控制反转(inversion of control)
  • 异步性(asynchronicity)

当使用 setter-setter 时,是它本身决定何时调用传递参数 setter 的。因此自然也可以是异步去调用。

function setSetTen(setTen) {
  setTen(10);
}

setSetTen(console.log); // 10

iterables.png

Iterable 可迭代对象是一种特殊的 getter-getter,它的返回值是另一个会返回 donevalue 属性的 getter。

使用可迭代对象(iterable)可以让我们知道何时停止迭代。

function getGetNextPowerOfTwoUntil16() {
  let i = 2;
  return function getNextPowerOfTwo() {
    const next = i;
    if (next > 16) return { done: true };
    i = i * 2;
    return { done: false, value: next };
  };
}

let getNextPowerOfTwoUntil16 = getGetNextPowerOfTwoUntil16();
console.log(getNextPowerOfTwoUntil16()); // { done: false, value: 2 }
console.log(getNextPowerOfTwoUntil16()); // { done: false, value: 4 }
console.log(getNextPowerOfTwoUntil16()); // { done: false, value: 8 }
console.log(getNextPowerOfTwoUntil16()); // { done: false, value: 16 }
console.log(getNextPowerOfTwoUntil16()); // { done: true }

ES6 迭代器(iterator) 则是在此基础上封装得更加规范:

  • getter-getter 封装成 { [Symbol.iterator]: getter-getter }
  • getter 封装成 { next: getter }
const getGetNextPowerOfTwoUntil16 = {
  [Symbol.iterator]: () => {
    let i = 2;
    return {
      next: () => {
        const next = i;
        if (next > 16) return { done: true };
        i = i * 2;
        return { done: false, value: next };
      }
    };
  }
}

const getNextPowerOfTwoUntil16Iterator = getGetNextPowerOfTwoUntil16[Symbol.iterator]();
for (let result = getNextPowerOfTwoUntil16Iterator.next(); !result.done; result = getNextPowerOfTwoUntil16Iterator.next()) {
  console.log(result.value); // 2 -> 4 -> 8 -> 16
}

// or use for-let-of
for (let value of getGetNextPowerOfTwoUntil16) {
  console.log(value); // 2 -> 4 -> 8 -> 16
}

为了更方便地创建可迭代对象(iterable),ES6 提供了 生成器(generator) 语法糖。

function* getGetNextPowerOfTwoUntil16() {
  let i = 2;
  while (true) {
    if (i > 16) return;
    yield i;
    i = i * 2;
  }
}

for (let value of getGetNextPowerOfTwoUntil16()) {
  console.log(value); // 2 -> 4 -> 8 -> 16
}

promises.png

虽然 setter-setter 功能强大,但是由于控制反转(IoC),它将变得非常不可预测。setter-setter 可以是同步的也可以是异步的,并且在这过程中可能生产一个值或多个值。

Promise 承诺是一种特殊的 setter-setter,它为函数执行的异步性和值的生成约束了规范。

  • setter 永远不会被同步调用
  • setter 最多被调用一次
  • 提供可选的第二个 setter 来处理错误
const setTenPromise = new Promise(function setSetTen(setTen) {
  setTen(10);
});

console.log("before Promise.then");
console.log(setTenPromise.then(console.log));
console.log("after Promise.then");

// before Promise.then
// Promise {...}
// after Promise.then
// 10

为了更方便地创建 Promise,ES6 提供了 async-await 语法糖。

async function setTenPromise() {
  return await 10;
}

console.log("before Promise.then");
console.log(await setTenPromise());
console.log("after Promise.then");

// before Promise.then
// 10
// after Promise.then

observables.png

Observable 可观察对象是一种特殊的 setter-setter,它为函数执行的异步性和值的生成约束了规范。

可观察对象(Observable)不同于在 ES 规范中标准化的可迭代对象(Iterable),虽然 Observable 被认为是 TC39 的提议(proposal),但是这个提议还在不断变化中。

在下面例子中,我们假设可观察对象(Observable)采用的是 Fantasy Observable 规范,像 RxJS 这样的库也是采用这个规范。

可观察对象(Observable)和可迭代对象(Iterable)具有对称性:

Iterable 可迭代对象

  • 是对象
  • 拥有 iterate(迭代)方法,方法名为 Symbol.iterator(迭代器符号)
  • iterate(迭代)方法是 Iterator(迭代器对象)的 getter
  • Iterator(迭代器对象)拥有名为 next 方法的 getter

Observable 可观察对象

  • 是对象
  • 拥有 observe(观察)方法,方法名为 subscribe(订阅)
  • observe(观察)方法是 Observer(观察者对象)的 setter
  • Observer(观察者对象)拥有名为 next 方法的 setter

观察者对象(Observer) 除了 next 还拥有另外两个方法 completeerror,分别表示成功完成和失败抛错。complete 方法相当于迭代器对象(Iterator)的 done 方法,error 方法相当于迭代器对象(Iterator)抛错。

像 Promise 一样,可观察对象(Observable)中状态的变化具有不可逆性。一旦观察者对象(Observer)调用 completeerror 方法后,就不再允许调用 next 方法。

const setSetNextPowerOfTwoUntil16 = {
  subscribe: (observer) => {
    let i = 2;
    try {
      while (i <= 16) {
        observer.next(i);
        i = i * 2;
      }
      observer.complete();
    } catch(err) {
      observer.error(err);
    }
  }
}

const setNextPowerOfTwoUntil16 = setSetNextPowerOfTwoUntil16.subscribe;
setNextPowerOfTwoUntil16({
  next: value => console.log(value),
  complete: () => console.log("done")
}); // 2 -> 4 -> 8 -> 16 -> done

和 setter-setter 一样,可观察对象(Observable)会导致控制反转(IoC),因此消费端无法暂停或取消数据的产生。为了避免这种弊端,大多数可观察对象(Observable)的实现都添加了 订阅(Subscription) 这一对象来实现暂停或取消数据的产生

subscribe 订阅方法返回一个对象,这个对象拥有 unsubscribe 方法,消费端可以使用该方法暂停或取消数据的产生。因此,订阅(Subscription)不再是 setter,因为它是一个既有参数传递又有返回值的函数。

const setSetNextPowerOfTwoUntil16 = {
  subscribe: (observer) => {
    let i = 2;
    let powerOfTwoTimer = setInterval(() => {
      if (i <= 16) {
        observer.next(i);
        i = i * 2;
      } else {
        observer.complete();
        clearInterval(powerOfTwoTimer);
      }
    }, 1000);
    return {
      unsubscribe: () => {
        clearInterval(powerOfTwoTimer);
      }
    };
  }
}

const setNextPowerOfTwoUntil16 = setSetNextPowerOfTwoUntil16.subscribe;
const subscription = setNextPowerOfTwoUntil16({
  next: value => console.log(value),
  complete: () => console.log("done")
}); // 2 -> 4

setTimeout(() => {
  subscription.unsubscribe();
}, 2500);

async-iterables.png

AsyncIterable 异步可迭代对象通过使用 Promise 实现迭代值的异步传递,每次调用迭代器(Iterator)的 next 方法时,都会创建并返回一个 Promise。

function delayResolve(value) {
  return new Promise(resolve => {
    setTimeout(() => resolve(value), 1000);
  });
}

function* getGetNextPowerOfTwoUntil16() {
  let i = 2;
  while (true) {
    if (i > 16) return;
    yield delayResolve(i);
    i = i * 2;
  }
}

for (let valuePromise of getGetNextPowerOfTwoUntil16()) {
  console.log(await valuePromise); // (1s) -> 2 -> (1s) -> 4 -> (1s) -> 8 -> (1s) -> 16
}

上面的代码其实是 ES6 的使用 Promise 实现的可迭代对象(Iterable of Promise),和 ES2018 的异步可迭代对象(AsyncIterable)还是有区别的:

  • ES6 Iterable of Promise: () => (() => { done, value: Promise<value> )
  • ES2018 AsyncIterable: () => (() => Promise<{ done, value }>)

下面我们来具体实现 ES2018 可迭代异步对象(AsyncIterable),我们使用 Symbol.asyncIterator 来定义迭代方法:

function delayResolve(value) {
  return new Promise(resolve => {
    setTimeout(() => resolve(value), 1000);
  });
}

const getGetNextPowerOfTwoUntil16 = {
  [Symbol.asyncIterator]: () => {
    let i = 2;
    return {
      next: () => {
        const next = i;
        if (next > 16) return delayResolve({ done: true });
        i = i * 2;
        return delayResolve({ done: false, value: next });
      }
    };
  }
}

const getNextPowerOfTwoUntil16Iterator = getGetNextPowerOfTwoUntil16[Symbol.asyncIterator]();
let done = false;
let value = undefined;
for (let result = getNextPowerOfTwoUntil16Iterator.next(); !done; result = getNextPowerOfTwoUntil16Iterator.next()) {
  ({ done, value } = await result);
  if (!done) {
    console.log(value);
  } else {
    console.log("done");
  }
  // (1s) -> 2 -> (1s) -> 4 -> (1s) -> 8 -> (1s) -> 16 -> (1s) -> done
}

// or use await for-let-of
for await (let value of getGetNextPowerOfTwoUntil16) {
  console.log(value);
}
console.log("done");
// (1s) -> 2 -> (1s) -> 4 -> (1s) -> 8 -> (1s) -> 16 -> (1s) -> done