码字,杂谈

Proxy - JavaScript

JavaScript 的 Proxy 对象是 ES2015,也就是 ES6 版本添加的。其官方定义为:

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

它本质上就是一个代理,如果学过设计模式的话,其实很好理解。我之前写过的js版本的设计模式也写过这个。有兴趣可以参照:https://github.com/jeremyjone/design-pattern-js/blob/master/src/mode/proxy-mode.js

先举一个小例子

先看一下代码:

/**
 * 使用ES6语法的Proxy类演示 代理模式的示例,明星 - 经纪人
 */

let star = {
    name: "张xx",
    age : 25,
    phone: "138123456789"
}

let agent = new Proxy(star, {
    get: function(target, key) {
        if (key === "phone") {
            return "agent phone: 13555555555";
        }
        else if (key === "price") {
            return 150000;
        }
        return target[key];
    },
    set: function(target, key, val) {
        if (key === "customPrice") {
            if (val < 10000) {
                throw new Error("价格太低");
            } else {
                target[key] = val;
                return true;
            }
        }
    }
})

// test
console.log(agent.name);
console.log(agent.phone);
console.log(agent.age);
console.log(agent.price);

agent.customPrice = 120000; // OK
console.log(agent.customPrice);

agent.customPrice = 1000; // Error
console.log(agent.customPrice);

在这里,我们有一个明星 张xx,他很忙,不可能每件事情都自己处理,所以他需要一个代理人,也就是我们平时说的经纪人。当有人希望联系到该明星时,它会直接拨打电话,这时通过代理模式,经纪人会接到电话,这样对明星来说,电话的隐私被保护了起来。

大体上,Proxy 就是这样的作用。当然,它不仅仅可以这样。

官方文档解读

语法

从上面的例子可以看到,Proxy 的语法应该如下:

const p = new Proxy(target, handler)

其中:

  • target
    要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

  • handler
    一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。


方法

Proxy 包含一个方法,其语法:

Proxy.revocable(target, handler);

作用:创建一个可撤销的Proxy对象。

官方示例:

var revocable = Proxy.revocable({}, {
  get(target, name) {
    return "[[" + name + "]]";
  }
});
var proxy = revocable.proxy;
proxy.foo;              // "[[foo]]"

revocable.revoke();

console.log(proxy.foo); // 抛出 TypeError
proxy.foo = 1           // 还是 TypeError
delete proxy.foo;       // 又是 TypeError
typeof proxy            // "object",因为 typeof 不属于可代理操作

更多内容,可以看官网,这个不是重点。


handler 对象的方法

handler 对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap)。
所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

上面这些都是标准的捕捉器,还有一些不标准的捕捉器已经被废弃并移除了。

使用

这里,我们最常使用的应该是 getset,这也是最基础的用法,不管是最开始的小例子,还是使用 Object.defineProperty 的监控方式,都是通过覆写这两个方法,达到一个代理监控的效果。

比如赋值时我们希望可以验证、重新整理数据等,都可以通过 Proxy 得到想要的结果。

基础示例

当对象中不存在属性时,返回默认值

const handler = {
    get: function(obj, prop) {
        // 当属性不存在,返回一个默认值。
        return prop in obj ? obj[prop] : 37;
    }
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b);      // 1, undefined
console.log('c' in p, p.c); // false, 37

无操作转发代理

代理可以将所有应用到它的操作转发到原生JS对象上:

let target = {}; // 原生对象
let p = new Proxy(target, {});

p.a = 37; // 操作转发到目标

console.log(target.a); // 37. 操作已经被正确地转发

同理,如果操作 target,同时也会转发到 p 上。

验证

通过对 set 覆写,达到验证的效果,这个在实际生产中用处很大。

其实上面的明星小例子已经给出了示例,它验证的是明星代言费。

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // The default behavior to store the value
    obj[prop] = value;

    // 表示成功
    return true;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

console.log(person.age);
// 100

person.age = 'young';
// 抛出异常: Uncaught TypeError: The age is not an integer

person.age = 300;
// 抛出异常: Uncaught RangeError: The age seems invalid

扩展构造函数

这个就比较高级的用法了,Proxy 可以很轻松的扩展一个已有的构造函数。

function extend(sup, base) {
  var descriptor = Object.getOwnPropertyDescriptor(
    base.prototype, "constructor"
  );
  base.prototype = Object.create(sup.prototype);
  var handler = {
    construct: function(target, args) {
      var obj = Object.create(base.prototype);
      this.apply(target, obj, args);
      return obj;
    },
    apply: function(target, that, args) {
      sup.apply(that, args);
      base.apply(that, args);
    }
  };
  var proxy = new Proxy(base, handler);
  descriptor.value = proxy;
  Object.defineProperty(base.prototype, "constructor", descriptor);
  return proxy;
}

var Person = function (name) {
  this.name = name
};

var Boy = extend(Person, function (name, age) {
  this.age = age;
});

Boy.prototype.sex = "M";

var Peter = new Boy("Peter", 13);
console.log(Peter.sex);  // "M"
console.log(Peter.name); // "Peter"
console.log(Peter.age);  // 13

一个完整的 traps 列表示例

/*
  var docCookies = ... get the "docCookies" object here:  
  https://developer.mozilla.org/zh-CN/docs/DOM/document.cookie#A_little_framework.3A_a_complete_cookies_reader.2Fwriter_with_full_unicode_support
*/

var docCookies = new Proxy(docCookies, {
  "get": function (oTarget, sKey) {
    return oTarget[sKey] || oTarget.getItem(sKey) || undefined;
  },
  "set": function (oTarget, sKey, vValue) {
    if (sKey in oTarget) { return false; }
    return oTarget.setItem(sKey, vValue);
  },
  "deleteProperty": function (oTarget, sKey) {
    if (sKey in oTarget) { return false; }
    return oTarget.removeItem(sKey);
  },
  "enumerate": function (oTarget, sKey) {
    return oTarget.keys();
  },
  "ownKeys": function (oTarget, sKey) {
    return oTarget.keys();
  },
  "has": function (oTarget, sKey) {
    return sKey in oTarget || oTarget.hasItem(sKey);
  },
  "defineProperty": function (oTarget, sKey, oDesc) {
    if (oDesc && "value" in oDesc) { oTarget.setItem(sKey, oDesc.value); }
    return oTarget;
  },
  "getOwnPropertyDescriptor": function (oTarget, sKey) {
    var vValue = oTarget.getItem(sKey);
    return vValue ? {
      "value": vValue,
      "writable": true,
      "enumerable": true,
      "configurable": false
    } : undefined;
  },
});

/* Cookies 测试 */

alert(docCookies.my_cookie1 = "First value");
alert(docCookies.getItem("my_cookie1"));

docCookies.setItem("my_cookie1", "Changed value");
alert(docCookies.my_cookie1);

实际使用

相比其他 handler,生产中更多是大量的使用 getset

比如,我们现在有一个对象,里面包含时间 date 字段。我们的需求是,希望外部有一个变量 dynamicDate 动态联动该 date 字段,当 date 字段改变时,dynamicDate 可以动态改变。

我们现在将它做成一个代理。

let target = {date: "2020-07-28"};
let dynamicDate = null;
let p = new Proxy(target, {
    get: function(obj, prop) {
        if (prop === "date") {
            dynamicDate = new Date(obj[prop]);
        }

        return obj[prop];
    },

    set: function(obj, prop, value) {
        // 减少不必要的重复赋值操作
        if (obj[prop] !== value) {
            obj[prop] = value;
        }

        return true;
    }
})

这时,当 target 的数据改变时,dynamicDatep 可以同时更新。

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注