Re:从零写一个基于JS Proxy的断言库[JavaScript]

什么是断言库,如何使用它们,或是如何写一个本身的断言库。javascript

这篇文章的主要目的是展现构建一个简易JS测试库的过程。该库有着本身的测试函数,也能够传入自定义的测试函数,支持链式调用。咱们先来实现库的基本功能,随后会使用js proxy来提升库的性能。java

什么是断言库

若是你曾使用过 mocha,chai,assert 或是 jest,断言库对你来讲确定不会陌生。简单来讲,咱们使用断言库中的断言函数来测试一个值或表达式是否如预期地同样获得正确的结果。git

它们如何工做

断言函数的命名也很语义化,咱们能很容易地看出下面的代码在进行什么样的测试。github

expect(true).to.equal(false)   // 抛出错误
expect(1+1).to.equal(2)        // 经过测试
复制代码

若是没有错误抛出,那代码只是静静地跑了一遍。在 jest 中,没有提供回调地断言测试默认为经过测试。浏览器

it('should equal true', () => {
  expect(true).toBe(true);
});
it('nothing to test here...');      // 经过测试
复制代码

从零书写断言函数

如今咱们开始书写本身地断言库,咱们用该库来进行数据地有效性。app

enforce(2).largerThan(0).smallerThan(5);

enforce(description).anyOf({
  isEmpty: true,
  largetThan: 20
});
复制代码

咱们向 enforce 函数中传入待测试地值或表达式,随后用一系列断言函数来验证数据是否符合咱们地要求。函数

整理思路

咱们须要完成的有如下几点:性能

  1. 接受待测试的值😒
  2. 支持链式调用
  3. 数据不符合要求是抛出错误
  4. 可接受自定义断言函数

咱们先来建立几个验证函数测试

const rules = {
  isArray: (value) => Array.isArray(value),
  largerThan: (value, compare) => value > compare,
  smallerThan: (value, compare) => value < compare,
  isEmpty: (value) => !value || Object.keys(value).length === 0
};
复制代码

在 enforce(value) 的返回值中,须要能访问到 value 的值。enforce(value) 后接的断言函数的返回值也须要能访问到 value 的值,而且理论上链的调用能够无限长。ui

class Enforce {
  constructor(custom = {}) {
    this.rules = Object.assign({}, rules, custom);
    return this.enforce;
  }
  enforce = (value) => {
    this.value = value;
    return this.rules;
  }
}
const enforce = new Enforce();
enforce()
// {isArray: ƒ, largerThan: ƒ, smallerThan: ƒ, isEmpty: ƒ}
复制代码

此时 enforce 的返回值中的函数还不能访问 value 的值,也不能链式调用咱们须要返回一个 object,该 object 的键为 rules 的名称,值为函数引用,这样在enforce 返回的 object 中,enforce().largerThan会返回函数引用,咱们只须要提供参数即可以运行该函数。

function ruleRunner(rule, wrapped, value, ...args) {
  const result = rule(value, ...args);
  if (result !== true) {
    throw Error(`Validation failed for ${rule.name}`);
  }
  return wrapped;
}

// 接受待测试的值
function enforce(value) {
  const wrapped = {};
  Object.keys(rules).forEach( fnName => {
    // 遍历 rules 中的规则,塞进 object 中,键即为函数名,值为一个函数
    // 函数中已经传入了须要运行的测试函数 rules[fnName]
    // 为了能够无限链式调用,ruleRunner的返回值 wrapped 也被传入
    // 固然还有 value 
    // args 是为了如 largerThan 或自定义测试函数等 除了value值外,另外须要的参照值
    wrapped[fnName] = (...args) => ruleRunner(rules[fnName], wrapped, value, ...args);
  } );
  return wrapped;
}

enforce();
// {isArray: f, largerThan: f, smallerThan: f, isEmpty: f}
// 此时,enforce 的返回值为 object
// enforce(1).largetThan 返回函数引用,咱们只须要给予参数,函数就会运行

enforce(55).largetThan(11);
// {isArray: f, largerThan: f, smallerThan: f, isEmpty: f}

enforce(55).largetThan(11).smallerThan(99);
// {isArray: f, largerThan: f, smallerThan: f, isEmpty: f}

enforce(true).isArray();
// Uncaught Error: Validation failed for isArray
复制代码

咱们来讨论一下效率问题。 每次运行 enforce 时,咱们都会遍历一遍 rules [对rules特攻],放到另外一个函数中。若是咱们的 rules 中的规则越多,效率将会越低。

Proxy

什么是Proxy

Proxy对象用于定义基本操做的自定义行为。

栗子

const orig = {
  a: 'get',
  b: 'moving'
};
const handle = {
  // 在使用 new Proxy()以后,target 指代被代理的对象 即orig
  // key 为调用对象,运行 proxy.a 时 key 的值为 a
  get: (target, key) => {
    if ( key === 'b' ) {
      // 经过代理访问 b 属性将会返回另外的值
      return 'schwifty';
    } else if ( target.hasOwnProperty(key) ) {
      // 经过代理访问 b 之外的属性直接返回本该返回的值
      return target[key];
    } else {
      console.error('Not sure what you are looking for...');
    }
  }
};
// 用 handle 中的 get 为 orig 进行代理
const proxy = new Proxy(orig, handle);

// 使用原 object 和 proxy 的不一样
console.log(`${orig.a} ${orig.b}`);
// get moving

console.log(`${proxy.a} ${proxy.b}`);
// get schwifty

orig.c
// undefined

proxy.c
// Not sure what you are looking for...
复制代码

在 enforce 中使用 proxy

class Enforce {
  constructor(custom = {}) {
    const allRules = Object.assign({}, rules, custom);
    this.allRules = allRules;
    return this.enforce.bind(this);
  }
  enforce(value) {
    const proxy = new Proxy(this.allRules, {
      // allRules 为 this.allRules 
      get: (allRules, rule) => {
        // 判断自己的rules和自定义规则cuntom中是否存在所要运行的rule
        // 若无 返回 undefined
        if (!allRules.hasOwnProperty(rule)){
          return allRules[rule];
        }
        // 如有 返回一个函数,干函数接受参数并运行该 rule
        return (...args) => this.runRule(proxy, value, rule, ...args);
      }
    });
    // enforce 的返回值为 proxy 
    // 链上的全部运行的 rule 都经过代理来运行
    return proxy;
  }
  runRule(proxy, value, rule, ...args) {
    const isValid = this.allRules[rule].call(proxy, value, args);
    if(isValid === true){
    // 符合预期 则返回 proxy 可继续链式调用
      return proxy;
    }
    throw new Error(`${this.allRules[rule].name}: You shall not pass!`);
  }
}
const enforce = new Enforce();

enforce(55);
// Proxy {isArray: ƒ, largerThan: ƒ, smallerThan: ƒ, isEmpty: ƒ}

enforce(55).largerThan(20);
// Proxy {isArray: ƒ, largerThan: ƒ, smallerThan: ƒ, isEmpty: ƒ}

enforce(55).largerThan(200);
// Uncaught Error: largerThan: YOU SHALL NOT PASS!

enforce(55).largerThan(20).smallerThan(200)
// enforce(55).largerThan(20).smallerThan(200)

enforce(55).largerThan(20).smallerThan(200).isArray();
// Uncaught Error: isArray: YOU SHALL NOT PASS!


const enforce = new Enforce({
  hasDog: (value) => /dog/.test(value)
});

enforce('dog').hasDog();
// Proxy {isArray: ƒ, largerThan: ƒ, smallerThan: ƒ, isEmpty: ƒ, hasDog: ƒ}

enforce('cat').hasDog();
// Uncaught Error: hasDog: YOU SHALL NOT PASS!
复制代码

目前的 Enforce 类可使用自定义规则来测试数据,而且能够链式调用。

Proxy 真的有那么好吗

性能测试

下面是使用 benchmark.js 在两个版本的 enforce 之间的性能对比,能够看出Proxy版本enforce的运行时间是非Proxy版本的五分之一。

proxy-benchmark.png

兼容性

Proxy 是 ES2015 中的特性,并不兼容老版本的浏览器。

proxy-compatibility.png
若想要兼容老版本浏览器,也可使用 GoogleChrome/proxy-polyfill。proxy-polyfill 容许 IE 9+ 和 Safari 6+ 兼容 Proxy。

原文:medium.com/fiverr-engi…

相关文章
相关标签/搜索