# 每天阅读一个 npm 模块(7)- delegates

5,022 阅读5分钟

系列文章:

  1. 每天阅读一个 npm 模块(1)- username
  2. 每天阅读一个 npm 模块(2)- mem
  3. 每天阅读一个 npm 模块(3)- mimic-fn
  4. 每天阅读一个 npm 模块(4)- throttle-debounce
  5. 每天阅读一个 npm 模块(5)- ee-first
  6. 每天阅读一个 npm 模块(6)- pify

因为准备深入地探究 koa 及相关的生态,所以接下来一段时间阅读的 npm 模块都会和 koa 密切相关 ^_^

一句话介绍

今天阅读的模块是 delegates,它由大名鼎鼎的 TJ 所写,可以帮我们方便快捷地使用设计模式当中的委托模式(Delegation Pattern),即外层暴露的对象将请求委托给内部的其他对象进行处理,当前版本是 1.0.0,周下载量约为 364 万。

用法

delegates 基本用法就是将内部对象的变量或者函数绑定在暴露在外层的变量上,直接通过 delegates 方法进行如下委托,基本的委托方式包含:

  • getter:外部对象可以直接访问内部对象的值
  • setter:外部对象可以直接修改内部对象的值
  • access:包含 getter 与 setter 的功能
  • method:外部对象可以直接调用内部对象的函数
const delegates = require('./index');

const petShop = {
  dog: {
    name: '旺财',
    age: 1,
    sex: '猛汉',
    bar() {
      console.log('bar!');
    }
  },
}

// 将内部对象 dog 的属性、函数
// 委托至暴露在外的 petShop 上
delegates(petShop, 'dog')
  .getter('name')
  .setter('age')
  .access('sex')
  .method('bar');

// 访问内部对象属性
console.log(petShop.name)
// => '旺财'

// 修改内部对象属性
petShop.age = 2;
console.log(petShop.dog.age)
// => 2

// 同时访问和修改内部对象属性
console.log(petShop.sex)
// => '猛汉'
petShop.sex = '公主';
console.log(petShop.sex);
// => '公主'

// 调用内部对象函数
petShop.bar();
// 'bar!'

除了上面这种方式之外,还可以在外部对象上添加类似 jQuery 风格的函数,即:

  • 函数不传参数的时候,获取对应的值
  • 函数传参数的时候,修改对应的值
const delegates = require('./index');

const petShop = {
  dog: {
    name: '旺财',
  },
}

delegates(petShop, 'dog')
  .fluent('name');

// 不传参数,获取内部属性
console.log(petShop.name());

// 传参数,修改内部属性
// 还可以链式调用
console.log(
    petShop.name('二哈')
    	.name('蠢二哈')
    	.name();
);

源码学习

初始化

// 源码 7 - 1
function Delegator(proto, target) {
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  this.proto = proto;
  this.target = target;
  this.methods = [];
  this.getters = [];
  this.setters = [];
  this.fluents = [];
}

this 对象中 methods | getters | setters | flaunts 均为数组,用于记录委托了哪些属性和函数。

上述初始化函数的第一行值得引起注意: 如果 this 不是 Delegator 的实例的话,则调用 new Delegator(proto, target)。通过这种方式,可以避免在调用初始化函数时忘记写 new 造成的问题,因为此时下面两种写法是等价的:

  • let x = new Delegator(petShop, 'dog')
  • let x = Delegator(petShop, 'dog')

另外讲一讲在调用 new 时主要做了以下事情:

  1. 将构造函数内的 this 指向新创建的空对象 {}
  2. 执行构造函数体
  3. 如果构造函数有显示返回值,且该值为对象的话,则返回对象的引用
  4. 如果构造函数没有显示返回值或者显示返回值不是对象(例如显示返回值为 1, 'haha' 等)的话,则返回 this

getter

// 源码 7-2
Delegator.prototype.getter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.getters.push(name);

  proto.__defineGetter__(name, function(){
    return this[target][name];
  });

  return this;
};

上面代码中的关键在于 __defineGetter__ 的使用,它可以在已存在的对象上添加可读属性,其中第一个参数为属性名,第二个参数为函数,返回值为对应的属性值:

const obj = {};
obj.__defineGetter__('name', () => 'elvin');

console.log(obj.name);
// => 'elvin'

obj.name = '旺财';
console.log(obj.name);
// => 'elvin'
// 我怎么能被改名叫旺财呢!

需要注意的是尽管 __defineGetter__ 曾被广泛使用,但是已不被推荐,建议通过 Object.defineProperty 实现同样功能,或者通过 get 操作符实现类似功能:

const obj = {};
Object.defineProperty(obj, 'name', {
  value: 'elvin',
});

Object.defineProperty(obj, 'sex', {
  get() {
    return 'male';
  }
});

const dog = {
  get name() {
    return '旺财';
  }
};

Github 上已有人提出相应的 PR#20,不过因为 TJ 已经离开了 Node.js 社区,所以估计也不会更新这个仓库了。

setter

// 源码 7-3
Delegator.prototype.setter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.setters.push(name);

  proto.__defineSetter__(name, function(val){
    return this[target][name] = val;
  });

  return this;
};

上述代码与 getter 几乎一模一样,不过使用的是 __defineSetter__,它可以在已存在的对象上添加可读属性,其中第一个参数为属性名,第二个参数为函数,参数为传入的值:

const obj = {};
obj.__defineSetter__('name', function(value) {
  this._name = value;
});

obj.name = 'elvin';
console.log(obj.name, obj._name);
// undefined 'elvin'

同样地,虽然 __defineSetter__ 曾被广泛使用,但是已不被推荐,建议通过 Object.defineProperty 实现同样功能,或者通过 set 操作符实现类似功能:

const obj = {};
Object.defineProperty(obj, 'name', {
  set(value) {
    this._name = value;
  }
});

const dog = {
  set(value) {
    this._name = value;
  }
};

method

// 源码 7-4
Delegator.prototype.method = function(name){
  var proto = this.proto;
  var target = this.target;
  this.methods.push(name);

  proto[name] = function(){
    return this[target][name].apply(this[target], arguments);
  };

  return this;
};

method 的实现也十分简单,只需要注意这里 apply 函数的第一个参数是内部对象 this[target],从而确保了在执行函数 this[target][name] 时,函数体内的 this 是指向对应的内部对象。

其它 delegates 提供的函数如 fluent | access 都是类似的,就不重复说明了。

koa 中的使用

在 koa 中,其核心就在于 context 对象,许多读写操作都是基于它进行,例如:

  • ctx.header 获取请求头

  • ctx.method 获取请求方法

  • ctx.url 获取请求 URL

  • ...

这些对请求参数的获取都得益于 koa 中 context.request 的许多属性都被委托在了 context 上:

// Koa 源码 lib/context.js
delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .access('path')
  .access('url')
  .getter('headers')
  .getter('ip');
  // ...

又例如:

  • ctx.body 设置响应体
  • ctx.status 设置响应状态码
  • ctx.redirect() 请求重定向
  • ...

这些对响应参数的设置都得益于 koa 中 context.response 的许多属性和方法都被委托在了 context 上:

// Koa 源码 lib/context.js
delegate(proto, 'response')
  .method('redirect')
  .method('vary')
  .access('status')
  .access('body')
  .getter('headerSent')
  .getter('writable');
  // ...

关于我:毕业于华科,工作在腾讯,elvinn 的博客 欢迎来访 ^_^