源码系列—delegates

486 阅读4分钟

这是我参与更文挑战的第10天,活动详情查看:更文挑战

简要介绍

delegates 是由 TJ 所开发的一个用于实现简单委托的工具包,在 Koa 中有使用到该工具。Koa 通过使用 delegatescontext.requestcontext.response 内的属性都委托到 context 上,从而让相关方法的调用更加简便,例如context.request.query方法可以直接写成 context.query

delegates 简单使用

在项目中通过npm i delegates安装该依赖包后,就能够使用 delegates 了,简单示例如下所示:

var delegate = require('delegates');

var obj = {};
obj.request = {
    foo: function(){
        console.log('do something ...');
    },
    get name() {
        return this._name
    },
    set name(val) {
        this._name = val
    }
};
// 将 obj.request 的相关属性委托到 obj 上,使调用更加简便
delegate(obj, 'request').method('foo').getter('name').setter('name');
obj.foo();  // do something ...
obj.name = 'LvLin'; 
console.log(obj.name); // 'LvLin'

源码分析

delegates 源码总共 157 行,实现了一个 Delegator 类,该类具备构造函数、静态方法 auto 和原型方法methodaccessgettersetterfluent

module.exports = Delegator;
// 构造函数
function Delegator(proto, target) {...}
// 静态方法
Delegator.auto = function(proto, targetProto, targetProp) {...}
// 原型方法
Delegator.prototype.method = function(name) {...}
Delegator.prototype.access = function(name) {...}
Delegator.prototype.getter = function(name) {...}
Delegator.prototype.setter = function(name) {...}
Delegator.prototype.fluent = function(name) {...}

构造函数

构造函数首先判断当前函数是否被 new 调用,如果不是就创建一个 Delegator 实例返回,所以我们在通过delegate(proto, target) 调用时,等同于new delegate(proto, target)

然后定义了各项属性,如下所示:

function Delegator(proto, target) {
   // 判断是否用 new 调用,如果不是就自行使用 new 创建实例
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  this.proto = proto;
  this.target = target;
  this.methods = [];
  this.getters = [];
  this.setters = [];
  this.fluents = [];
}

原型方法

method 方法用于将 target 上的方法通过闭包的方式绑定到proto上,最后返回 delegator 实例对象,以便实现链式调用,源码如下:

Delegator.prototype.method = function(name){
  var proto = this.proto;
  var target = this.target;
  this.methods.push(name); // 存入 methods 数组中

 // 以闭包的方式,将对 proto 方法的调用转为对 this[target] 上相关方法的调用
 // apply 改变 this 的指向为 this[target]
  proto[name] = function(){
    return this[target][name].apply(this[target], arguments);
  };
  // 返回 delegator 实例对象,从而实现链式调用
  return this;
};

gettersetter 方法用于将属性(gettersetterproto[target][name] 绑定到 proto[name]access是同时包含了gettersetter。源码如下:

Delegator.prototype.access = function(name){
  
  return this.getter(name).setter(name);
};

Delegator.prototype.getter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.getters.push(name); // 将属性名称存入对应类型的数组

  // 利用 __defineGetter__ 设置 proto 的 getter,
  // 使得访问 proto[name] 获取到的是 proto[target][name] 的值
  proto.__defineGetter__(name, function(){
    return this[target][name];
  });
  // 返回 delegator 实例,实现链式调用
  return this;
};

Delegator.prototype.setter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.setters.push(name); // 将属性名称存入对应类型的数组

  // 利用 __defineSetter__ 设置 proto 的 setter,
  // 实现给 proto[name] 赋值时,实际改变的是 proto[target][name] 的值
  proto.__defineSetter__(name, function(val){
    return this[target][name] = val;
  });
// 返回 delegator 实例,实现链式调用
  return this;
};

delegates 是通过对象的__defineGetter____defineSetter__方法配置对象的gettersetter,但是这两个方法已经从 Web 标准中删除,建议不要再使用该特性。目前推荐使用的是 Object.prototype.definePrototype。关于settergetter以及Object.prototype.definePrototype的知识,可以通过我的这篇文章进行了解。

fluent方法用于将普通属性 proto[target][name] 委托到 proto[name]上,使得通过 proto[name]proto[target][name]进行访问和修改,源码如下所示:

Delegator.prototype.fluent = function (name) {
  var proto = this.proto;
  var target = this.target;
  this.fluents.push(name); // 将属性名称存入对应类型的数组

  proto[name] = function(val){
    // 通过 val 是否为 null 或 undefined 判断是取值还是赋值
    if ('undefined' != typeof val) {
      // 赋值,修改 proto[target][name] 的值,返回 proto
      this[target][name] = val;
      return this;
    } else {
      // 取值,直接获取 proto[target][name]的值
      return this[target][name];
    }
  };

  return this;
};

可以看到,如果需要获取或修改一个普通属性proto[target][name],只能以函数的形式进行获取或调用,如下所示:

var obj = {};
obj.request = {
    name: '' 
};

delegate(obj, 'request').fluent('name');
obj.name('LvLin');
console.log(obj.name()) // LvLin

静态方法 auto

该方法接受一个对象 proto,一个内部对象 targetProto,内部对象的属性名 targetProp,实现自动将 prototargetProp 对象的属性委托到 proto 上,targetProp 提供委托的判断依据。源码分析如下所示:

Delegator.auto = function(proto, targetProto, targetProp){
  // 构建一个 delegator 实例
  var delegator = Delegator(proto, targetProp);
  // 获取到 targetProto 上的所有属性
  var properties = Object.getOwnPropertyNames(targetProto);
  // 遍历属性,判断以何种方式委托到 proto
  for (var i = 0; i < properties.length; i++) {
    var property = properties[i];
    // 获取该属性的属性描述符
    var descriptor = Object.getOwnPropertyDescriptor(targetProto, property);
    // 存储描述符的情况,调用相关 api 实现委托
    if (descriptor.get) {
      delegator.getter(property);
    }
    if (descriptor.set) {
      delegator.setter(property);
    }
	// 数据描述符的情况
    if (descriptor.hasOwnProperty('value')) {
      var value = descriptor.value;
      // 如果是函数,进行 method 委托
      if (value instanceof Function) {
        delegator.method(property);
      } else {
       // 否则进行 getter 委托
        delegator.getter(property);
      }
      // 如果可以重写,则进行 setter 委托
      // 这里感觉有点问题,如果是 Function 类型的话,就不应该再做这步判断才是
      // 但是如果是用 Object.defineProperty 定义,writable 默认值为 false
      if (descriptor.writable) {
        delegator.setter(property);
      }
    }
  }
};

关于对象属性描述符的相关知识,我在这篇文章中有进行详细的说明,欢迎阅读~

总结

delegate 将对象属性分为四类:fluentgettersettermethod,分别提供相应的接口实现对象属性的委托操作,同时还提供了简便的accessauto方法。

delegate 中的 api 最后都返回调用该 api 的 delegator 实例对象,从而实现多个 api 的链式调用。

资料

node-delegates 源码,by TJ Holowaychuk

Object.prototype.__defineGetter__(),by MDN