# 每天閱讀一個 npm 模組(7)- delegates

elvinnn發表於2018-09-08

系列文章:

  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');
  // ...
複製程式碼

關於我:畢業於華科,工作在騰訊,elvin 的部落格 歡迎來訪 ^_^

相關文章