[譯]ES6 中的元編程: 第三部分 —— 代理(Proxies)

ES6 中的元編程: 第三部分 —— 代理(Proxies)

這是個人 ES6 元編程系列的第三部分,也是最後一部分,還記得這個系列的文章我一年以前就開始動筆了,而且承諾不會花一年才寫完,但現實就是我還真花費了如此多的時間去完成。在最後這篇文章中,咱們要看看多是 ES6 中最酷的反射特性:代理(Proxy)。因爲反射和本文的部份內容有關,若是你還沒讀過上一篇講述 ES6 Reflect API 的文章,以及更早的、講述 ES6 Symbols 的文章,先倒回去閱讀一下,這樣才能更好地理解本文。和其餘部分同樣,我先引用一下在第一部分提到過的觀點:前端

  • Symbols 是 實現了的反射(Reflection within implementation)—— 你將 Symbols 應用到你已有的類和對象上去改變它們的行爲。
  • Reflect 是 經過自省(introspection)實現反射(Reflection through introspection) —— 一般用來探索很是底層的代碼信息。
  • Proxy 是 經過調解(intercession)實現反射(Reflection through intercession) —— 包裹對象並經過自陷(trap)來攔截對象行爲。

所以,Proxy 是一個全新的全局構造函數(相似 Date 或者 Number),你能夠傳遞給其一個對象,以及一些鉤子(hook),它能爲你返回一個 新的 對象,新的對象使用這些充滿魔力的鉤子包裹了老對象。如今,你擁有了代理,但願你喜歡它,我也高興你回到這個系列中來。node

關於代理,有不少須要闡述的。但對新手來講,先讓咱們看看怎麼建立一個代理。react

建立代理

Proxy 構造函數接受兩個參數,其一是你想要代理的初始對象,其二是一系列處理鉤子(handler hooks)。咱們先忽略第二個鉤子參數,看看怎麼爲現有對象建立代理。線索即在代理這個名字中:它們維持了一個你建立對象的引用,可是若是你有了一個原始對象的引用,任何你和原始對象的交互,都會影響到代理,相似地,任何你對代理作的改變,反過來也都會影響到原始對象。換句話說,Proxy 返回了一個包裹了傳入對象的新對象,可是任何你對兩者的操做,都會影響到它們彼此。爲了證明這一點,請看代碼:android

var myObject = {};
var proxiedMyObject = new Proxy(myObject, {/*以及一系列處理鉤子*/});

assert(myObject !== proxiedMyObject);

myObject.foo = true;
assert(proxiedMyObject.foo === true);

proxiedMyObject.bar = true;
assert(myObject.bar === true);複製代碼

目前爲止,咱們什麼目的也沒達到,相較於直接使用被代理對象,代理並不能提供任何額外收益。只有用上了處理鉤子,咱們才能在代理上作一些有趣的事兒。ios

代理的處理鉤子

處理鉤子是一系列的函數,每個鉤子都有一個具體名字以供代理識別,每個鉤子也控制了你如何和代理交互(所以,也控制了你和被包裹對象的交互)。處理鉤子勾住了 JavaScript 的 「內置方法」,若是你對此感受熟悉,是由於咱們在 上一篇介紹 Reflect API 的文章 中提到了內置方法。git

是時候鋪開來講代理了。我把代理放到系列的最後一部分的重要緣由是:因爲代理和反射就像一對苦命鴛鴦交織在一塊兒,所以咱們須要先知道反射是如何工做的。如你所見,每個代理鉤子都對應到一個反射方法,反之亦然,每個反射方法都有一個代理鉤子。完整的反射方法及對應的代理處理鉤子以下:es6

  • apply (以一個 this 參數和一系列 arguments(參數序列)調用函數)
  • construct(以一系列 arguments 及一個可選的、指明瞭原型的構造函數調用一個類函數或者構造函數)
  • defineProperty (在對象上定義一個屬性,並聲明該屬性中諸如對象可迭代性這樣的元信息)
  • getOwnPropertyDescriptor (得到一個屬性的 「屬性描述子」:描述子包含了諸如對象可迭代性這樣的元信息)
  • deleteProperty (從對象上刪除某個屬性)
  • getPrototypeOf (得到某實例的原型)
  • setPrototypeOf (設置某實例的原型)
  • isExtensible (判斷一個對象是不是 「可擴展的」,亦即判斷是否能夠爲其添加屬性)
  • preventExtensions (阻止對象被擴展)
  • get (獲得對象的某個屬性)
  • set (設置對象的某個屬性)
  • has (在不斷言(assert)屬性值的狀況下,判斷對象是否含有某個屬性)
  • ownKeys (得到某個對象自身全部的 key,排除掉其原型上的 key)

反射那一部分中(再囉嗦一遍,若是你沒看過,趕快去看),咱們已經瀏覽過上述全部方法了(並附帶有例子)。代理用相同的參數集實現了每個方法。實際上, 代理的默認行爲實際上已經實現了對每一個處理程序鉤子的反射調用(其內部機制對於不一樣的 JavaScript 引擎可能會有所區別,但對於沒有說明的鉤子,咱們只須要認爲它和對應的反射方法行爲一致便可)。這也意味着,任何你沒有指定的鉤子,都具備和默認情況一致的行爲,就像它從未被代理過同樣:github

// 咱們新建立了代理,並定義了與默認建立時同樣的行爲
proxy = new Proxy({}, {
  apply: Reflect.apply,
  construct: Reflect.construct,
  defineProperty: Reflect.defineProperty,
  getOwnPropertyDescriptor: Reflect.getOwnPropertyDescriptor,
  deleteProperty: Reflect.deleteProperty,
  getPrototypeOf: Reflect.getPrototypeOf,
  setPrototypeOf: Reflect.setPrototypeOf,
  isExtensible: Reflect.isExtensible,
  preventExtensions: Reflect.preventExtensions,
  get: Reflect.get,
  set: Reflect.set,
  has: Reflect.has,
  ownKeys: Reflect.ownKeys,
});複製代碼

如今,我能夠深刻到每一個代理鉤子的工做細節中去了,可是基本上都是複製粘貼反射中的例子(只須要修改不多的部分)。若是隻是介紹每一個鉤子的功能,對代理來講就不太公平,由於代理是去實現一些炫酷用例的。因此,本文剩餘內容都將爲你展現經過代理完成的炫酷的東西,甚至是一些你沒了代理就沒法完成的事。web

同時,爲了讓內容更具交互性,我爲每一個例子都建立一個小的庫來展現對應的功能。我會給出每一個例子對應的代碼倉庫連接。npm

用代理來......

構建一個可無限連接(chainable)的 API

之前面的例子爲基礎 —— 咱們仍使用 [[Get]] 自陷:只須要再施加一點魔法,咱們就能構建一個擁有無數方法的 API,當你最終調用其中某個方法時,將返回全部你被你連接的值。fluent API(流暢 API) 爲 web 請求構建了各個 URL,Chai 這類的測試框架將各個英文單詞連接組成高可讀的測試斷言,經過這些,咱們知道可無限連接的 API 是多麼有用。

爲了實現這個 API,咱們就須要鉤子勾住 [[Get]],將取到的屬性保存到數組中。代理 ( Proxy ) 將包裝一個函數,返回全部檢索到的支持的Array,並清空數組,以即可以重用它。咱們也會勾住 [[HasProperty]],由於咱們想告訴 API 的使用者,任何屬性都是存在的。

function urlBuilder(domain) {
  var parts = [];
  var proxy = new Proxy(function () {
    var returnValue = domain + '/' + parts.join('/');
    parts = [];
    return returnValue;
  }, {
    has: function () {
      return true;
    },
    get: function (object, prop) {
      parts.push(prop);
      return proxy;
    },
  });
  return proxy;
}
var google = urlBuilder('http://google.com');
assert(google.search.products.bacon.and.eggs() === 'http://google.com/search/products/bacon/and/eggs')複製代碼

你也能夠用相同的模式實現一個樹遍歷的 fluent API,這相似於你在 jQuery 或者 React 中看到的選擇器:

function treeTraverser(tree) {
  var parts = [];
  var proxy = new Proxy(function (parts) {
    let node = tree; // 從樹的根節點開始
    for (part of parts) {
      if (!node.props || !node.props.children || node.props.children.length === 0) {
        throw new Error(`Node ${node.tagName} has no more children`);
      }
      // 若是該部分是一個子節點,就深刻到該子節點進行下一次遍歷
      let index = node.props.children.findIndex((child) => child.tagName == part);
      if(index === -1) {
        throw new Error(`Cannot find child: ${part} in ${node.tagName}`);
      }
      node = node.props.children[index];
    }
    return node.props;
  }, {
    has: function () {
      return true;
    },
    get: function () {
      parts.push(prop);
      return proxy;
    }
  });
  return proxy;
}
var myDomIsh = treeTraverserExample({
  tagName: 'body',
  props: {
    children: [
      {
        tagName: 'div',
        props: {
          className: 'main',
          children: [
            {
              tagName: 'span',
              props: {
                className: 'extra',
                children: [
                  { tagName: 'i', props: { textContent: 'Hello' } },
                  { tagName: 'b', props: { textContent: 'World' } },
                ]
              }
            }
          ]
        }
      }
    ]
  }
});
assert(myDomIsh.div.span.i().textContent === 'Hello');
assert(myDomIsh.div.span.b().textContent === 'World');複製代碼

我已經發布了一個更加可複用的版本到 github.com/keithamus/p… 上,npm 上也有其同名的包。

實現一個 「方法缺失」 鉤子

許多其餘的編程語言都容許你使用一個內置的反射方法去重寫一個類的行爲,例如,在 PHP 中有 __call,在 Ruby 中有 method_missing,在 Python 中則有 __getattr__。JavaScript 缺少這個機制,但如今咱們有了代理去實現它。

在開始介紹代理的實現以前,咱們先看下 Ruby 是怎麼作的,來從中得到一些靈感:

class Foo
  def bar()
    print "you called bar. Good job!"
  end
  def method_missing(method)
    print "you called `#{method}` but it doesn't exist!"
  end
end

foo = Foo.new
foo.bar()
#=> you called bar. Good job!
foo.this_method_does_not_exist()
#=》 you called this_method_does_not_exist but it doesn't exist!複製代碼

對於任何存在方法,在此例中是 bar,該方法可以按預期被執行。對於不存在方法,好比 foo 或者 this_method_does_not_exist,在調用時會被 method_missing 替代。另外,method_missing 接受方法名做爲第一個參數,這對於判斷用戶意圖很是有用。

咱們能夠經過混入 ES6 Symbol 實現相似的功能:使用一個函數包裹類,該函數將返回使用了 get[[Get]])自陷的代理,或者說是攔截了 get 行爲的代理:

function Foo() {
  return new Proxy(this, {
    get: function (object, property) {
      if (Reflect.has(object, property)) {
        return Reflect.get(object, property);
      } else {
        return function methodMissing() {
          console.log('you called ' + property + ' but it doesn\'t exist!');
        }
      }
    }
  });
}

Foo.prototype.bar = function () {
  console.log('you called bar. Good job!');
}

foo = new Foo();
foo.bar();
// you called bar. Good job!
foo.this_method_does_not_exist();
// you called this_method_does_not_exist but it doesn't exist!複製代碼

當你有若干方法功能很是相似,而且能夠從函數名推測功能間的差別性,上面的作法就很是有用。將函數的功能區分從參數轉移到函數名,將帶來更好的可讀性。做爲此的一個例子 —— 你能夠快速輕易地建立一個單位轉換 API,如貨幣或者是進制的轉化:

const baseConvertor = new Proxy({}, {
  get: function baseConvert(object, methodName) {
    var methodParts = methodName.match(/base(\d+)toBase(\d+)/);
    var fromBase = methodParts && methodParts[1];
    var toBase = methodParts && methodParts[2];
    if (!methodParts || fromBase > 36 || toBase > 36 || fromBase < 2 || toBase < 2) {
      throw new Error('TypeError: baseConvertor' + methodName + ' is not a function');
    }
    return function (fromString) {
      return parseInt(fromString, fromBase).toString(toBase);
    }
  }
});

baseConvertor.base16toBase2('deadbeef') === '11011110101011011011111011101111';
baseConvertor.base2toBase16('11011110101011011011111011101111') === 'deadbeef';複製代碼

固然,你也能夠手動建立總計 1296 組合狀況的方法,或者單獨經過一個循環來建立這些方法,可是這二者都須要用更多的代碼來完成。

一個更加具體的例子是 Ruby on Rails 中的 ActiveRecord,其源於 「動態查找(dynamic finders)」。ActiveRecord 基本上實現了 「method_missing」 來容許你根據列查詢一個表。使用函數名做爲查詢關鍵字,避免了使用傳遞一個複雜對象來建立查詢語句:

Users.find_by_first_name('Keith'); # [ Keith Cirkel, Keith Urban, Keith David ]
Users.find_by_first_name_and_last_name('Keith', 'David');  # [ Keith David ]複製代碼

在 JavaScript 中,咱們也能實現相似功能:

function RecordFinder(options) {
  this.attributes = options.attributes;
  this.table = options.table;
  return new Proxy({}, function findProxy(methodName) {
    var match = methodName.match(new RegExp('findBy((?:And)' + this.attributes.join('|') + ')'));
    if (!match){
      throw new Error('TypeError: ' + methodName + ' is not a function');
    }
  });
});複製代碼

和其餘例子同樣,我已經寫了一個關於此的庫放到了 github.com/keithamus/p…,npm 上也能夠到同名的包。

getOwnPropertyNamesObject.keysin 等全部迭代方法中隱藏全部的屬性

咱們可使用代理讓一個對象的全部的屬性都隱藏起來,除非是要得到屬性的值。下面羅列了全部 JavaScript 中你能夠判斷某屬性是否存在於一個對象的方法:

  • Reflect.hasObject.hasOwnPropertyObject.prototype.hasOwnProperty 以及 in 運算符所有使用了 [[HasProperty]]。代理能夠經過 has 攔截它。
  • Object.keys/Object.getOwnPropertyNames 都使用了 [[OwnPropertyKeys]]。代理能夠經過 ownKeys 進行攔截。
  • Object.entries (一個即將到來的 ES2017 特性),也使用了 [[OwnPropertyKeys]],代理仍然能夠經過 ownKeys 進行攔截。
  • Object.getOwnPropertyDescriptor 使用了 [[GetOwnProperty]]。特別特別讓人興奮的是,代理能夠經過 getOwnPropertyDescriptor 進行攔截。
var example = new Proxy({ foo: 1, bar: 2 }, {
  has: function () { return false; },
  ownKeys: function () { return []; },
  getOwnPropertyDescriptor: function () { return false; },
});
assert(example.foo === 1);
assert(example.bar === 2);
assert('foo' in example === false);
assert('bar' in example === false);
assert(example.hasOwnProperty('foo') === false);
assert(example.hasOwnProperty('bar') === false);
assert.deepEqual(Object.keys(example), [ ]);
assert.deepEqual(Object.getOwnPropertyNames(example), [ ]);複製代碼

老實說,我也沒有發現這個模式有特別大的用處。可是,我仍是建立了一個關於此的一個庫,並放在了github.com/keithamus/p…,它能讓你單獨地設置某個屬性不可見了,而不是一鍋端地讓全部屬性不可見。

實現一個觀察者模式,也稱做 Object.observe

對新規範所添加的內容一直敏銳追蹤的人們可能已經注意到了, Object.observe 開始被考慮歸入 ES2016 了。Object.observe 的擁護者已經開始計劃 起草包含有有 Object.observe 的提案,他們對此有一個很是好的理由:草案初衷就是要幫助框架做者解決數據綁定(Data Binding)的問題。如今,隨着 React 和 Polymer 1.0 的發佈,數據綁定框架有所降溫,不可變數據(immutable data)開始變得流行。

慶幸的是,代理讓諸如 Object.observe 這樣的規範變得多餘,如今咱們能夠經過代理實現一個更加底層的 Object.observe。爲了更加接近 Object.observe 所具備的特性,咱們須要鉤住 [[Set]][[PreventExtensions]][[Delete]] 以及 [[DefineOwnProperty]] 這些內置方法 —— 代理分別可使用 setpreventExtensionsdeletePropertydefineProperty 進行攔截:

function observe(object, observerCallback) {
  var observing = true;
  const proxyObject = new Proxy(object, {
    set: function (object, property, value) {
      var hadProperty = Reflect.has(object, property);
      var oldValue = hadProperty && Reflect.get(object, property);
      var returnValue = Reflect.set(object, property, value);
      if (observing && hadProperty) {
        observerCallback({ object: proxyObject, type: 'update', name: property, oldValue: oldValue });
      } else if(observing) {
        observerCallback({ object: proxyObject, type: 'add', name: property });
      }
      return returnValue;
    },
    deleteProperty: function (object, property) {
      var hadProperty = Reflect.has(object, property);
      var oldValue = hadProperty && Reflect.get(object, property);
      var returnValue = Reflect.deleteProperty(object, property);
      if (observing && hadProperty) {
        observerCallback({ object: proxyObject, type: 'delete', name: property, oldValue: oldValue });
      }
      return returnValue;
    },
    defineProperty: function (object, property, descriptor) {
      var hadProperty = Reflect.has(object, property);
      var oldValue = hadProperty && Reflect.getOwnPropertyDescriptor(object, property);
      var returnValue = Reflect.defineProperty(object, property, descriptor);
      if (observing && hadProperty) {
        observerCallback({ object: proxyObject, type: 'reconfigure', name: property, oldValue: oldValue });
      } else if(observing) {
        observerCallback({ object: proxyObject, type: 'add', name: property });
      }
      return returnValue;
    },
    preventExtensions: function (object) {
      var returnValue = Reflect.preventExtensions(object);
      if (observing) {
        observerCallback({ object: proxyObject, type: 'preventExtensions' })
      }
      return returnValue;
    },
  });
  return { object: proxyObject, unobserve: function () { observing = false } };
}

var changes = [];
var observer = observe({ id: 1 }, (change) => changes.push(change));
var object = observer.object;
var unobserve = observer.unobserve;
object.a = 'b';
object.id++;
Object.defineProperty(object, 'a', { enumerable: false });
delete object.a;
Object.preventExtensions(object);
unobserve();
object.id++;
assert.equal(changes.length, 5);
assert.equal(changes[0].object, object);
assert.equal(changes[0].type, 'add');
assert.equal(changes[0].name, 'a');
assert.equal(changes[1].object, object);
assert.equal(changes[1].type, 'update');
assert.equal(changes[1].name, 'id');
assert.equal(changes[1].oldValue, 1);
assert.equal(changes[2].object, object);
assert.equal(changes[2].type, 'reconfigure');
assert.equal(changes[2].oldValue.enumerable, true);
assert.equal(changes[3].object, object);
assert.equal(changes[3].type, 'delete');
assert.equal(changes[3].name, 'a');
assert.equal(changes[4].object, object);
assert.equal(changes[4].type, 'preventExtensions');複製代碼

正如你所看到的,咱們用一小段代碼實現了一個相對完整的 Object.observe。該實現和規範之間的差別在於,Object.observe 是可以改變對象的,而代理則返回了一個新對象,而且 unobserver 函數也不是全局的。

和其餘例子同樣,我也寫了關於此的一個庫並放在了 github.com/keithamus/p… 以及 npm 上。

獎勵關卡:可撤銷代理

代理還有最後一個大招:一些代理能夠被撤銷。爲了建立一個可撤銷的代理,你須要使用 Proxy.revocable(target, handler) (而不是 new Proxy(target, handler)),而且,最終返回一個結構爲 {proxy, revoke()} 的對象來替代直接返回一個代理對象。例子以下:

function youOnlyGetOneSafetyNet(object) {
  var revocable = Proxy.revocable(object, {
    get(target, property) {
      if (Reflect.has(target, property)) {
        return Reflect.get(target, property);
      } else {
        revocable.revoke();
        return 'You only get one safety net';
      }
    }
  });
  return revocable.proxy;
}

var myObject = youOnlyGetOneSafetyNet({ foo: true });

assert(myObject.foo === true);
assert(myObject.foo === true);
assert(myObject.foo === true);

assert(myObject.bar === 'You only get one safety net');
myObject.bar // TypeError
myObject.bar // TypeError
Reflect.has(myObject, 'bar') // TypeError複製代碼

遺憾的是,你能夠看到例子中最後一行的右側,若是代理已經被撤銷,任何在代理對象上的操做都會拋出 TypeError —— 即使這些操做句柄尚未被代理。我以爲這多是可撤銷代理的一種能力。若是全部的操做都能與對應的 Reflect 返回一致(這會使得代理冗餘,並讓對象表現得好像從未設置過代理同樣),將使該特性更加有用。這個特性被放在了本文壓軸部分,也是由於我也不真正肯定可撤回代理的具體用例。

總結

我但願這篇文章讓你認識到代理是一個強大到難以想象的工具,它彌補了 JavaScript 內部曾經的缺失。在方方面面,Symbol、Reflect、以及代理都爲 JavaScript 開啓了新的篇章 —— 就如同 const 和 let,類和箭頭函數那樣。const 和 let 再也不讓代碼顯得混亂骯髒,類和箭頭函數讓代碼更簡潔,Symbol、Reflect、和 Proxy 則開始給予開發者在 JavaScript 中進行底層的元編程。

這些新的元編程工具不會在短期內放慢發展的速度:EcamScript 的新版本正逐漸完善,並添加了更多有趣的行爲,例如 Reflect.isCallableReflect.isConstructor 的提案,亦或 stage 0 關於 Reflect.type 的提案,亦或 function.sent 這個元屬性的提案
,亦或這個包含了更多函數元屬性的提案。這些新的 API 也激發了一些關於新特性的有趣討論,例如 這個關於添加 Reflect.parse 的提案,就引發了關於建立一個 AST(Abstract Syntax Tree:抽象語法樹)標準的討論。

你是怎麼看待新的 Proxy API 的?已經計劃用在你的項目裏面了?能夠在 Twitter 上給我留言讓我知道你的想法,我是 @keithamus


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索