让你一次吃透ES6 Proxy

726 阅读7分钟

我们总是在各种前端面试题中发现Proxy,那么这个Proxy到底是什么呢?今天带你一篇文章吃透Proxy。

Proxy概述

Proxy对象用于创建一个对象的代理,从而实现基本操作和拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

术语

  • handler

包含捕捉器(trap)的占位符对象,可译为处理器对象。

  • traps

提供属性访问的方法。这类似于操作系统中捕获器的概念。

  • target

被Proxy代理虚拟化的对象。它常被作为代理的存储后端。根据目标验证关于对象不可扩展性或不可配置属性的 不变量(保持不变的语义)。

语法

const p = new Proxy(target, handler);

参数

  • target

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

  • handler

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

方法

Proxy.revocable()

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

handler对象的方法

handler对象是一个容纳一批特定属性的占位符对象。它包含有Proxy的各个捕获器(trap)。

所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

handler.getPrototypeOf()

Object.getPrototypeOf()方法的捕捉器。

用途:是一个代理(Proxy)方法,当读取代理对象的原型时,该方法就会被调用。

基本用法:

var obj = {};
var proto = {};
var handler = {
    getPrototypeOf(target) {
        console.log(target === obj);   // true
        console.log(this === handler); // true
        return proto;
    }
};

var p = new Proxy(obj, handler);
console.log(Object.getPrototypeOf(p) === proto);    // true

handler.setPrototypeOf()

Object.setPrototypeOf()方法的捕捉器。

用途:设置一个指定的对象的原型 (即, 内部[[Prototype]]属性)到另一个对象或 null。

基本用法:

var handlerThrows = {
  setPrototypeOf(target, newProto) {
    throw new Error('custom error');
  }
}

var newProto = {}, target = {};

var p1 = new Proxy(target, handlerThrows);
Object.setPrototypeOf(p1, newProto); // throws new Error("custom error")
Reflect.setPrototypeOf(p1, newProto); // throws new Error("custom error")

handler.isExtensible()

Object.isExtensible()方法的捕捉器。

用途:判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。

基本用法:

var p = new Proxy({}, {
  isExtensible: function(target) {
    console.log('called');
    return true;//也可以return 1;等表示为true的值
  }
});

console.log(Object.isExtensible(p)); // "called"
                                     // true

handler.preventExtensions()

Object.preventExtensions()方法的捕捉器。

用途:让一个对象变的不可扩展,也就是永远不能再添加新的属性。

基本用法:

var p = new Proxy({}, {
  preventExtensions: function(target) {
    console.log('called');
    Object.preventExtensions(target);
    return true;
  }
});

console.log(Object.preventExtensions(p)); // "called"
                                          // false

handler.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor()方法的捕捉器。

用途:返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

基本用法:

var p = new Proxy({ a: 20}, {
  getOwnPropertyDescriptor: function(target, prop) {
    console.log('called: ' + prop);
    return { configurable: true, enumerable: true, value: 10 };
  }
});

console.log(Object.getOwnPropertyDescriptor(p, 'a').value); // "called: a"
                                                            // 10

handler.defineProperty()

Object.defineProperty方法的捕捉器。

用途:会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

基本用法:

var p = new Proxy({}, {
  defineProperty: function(target, prop, descriptor) {
    console.log('called: ' + prop);
    return true;
  }
});

var desc = { configurable: true, enumerable: true, value: 10 };
Object.defineProperty(p, 'a', desc); // "called: a"

handler.has()

in操作符的捕捉器。

用途:如果指定的属性在指定的对象或其原型链中,则in运算符返回true。

基本用法:

var p = new Proxy({}, {
  has: function(target, prop) {
    console.log('called: ' + prop);
    return true;
  }
});

console.log('a' in p); // "called: a"
                       // true

handler.get()

属性读取操作的捕获器。

用途:用于拦截对象的读取属性操作。

基本用法: 当对象中不存在属性名时,默认返回值为37.

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

handler.set()

属性设置操作的捕获器。

用途:设置属性值操作。

基本用法:

var p = new Proxy({}, {
  set: function(target, prop, value, receiver) {
    target[prop] = value;
    console.log('property set: ' + prop + ' = ' + value);
    return true;
  }
})

console.log('a' in p);  // false

p.a = 10;               // "property set: a = 10"
console.log('a' in p);  // true
console.log(p.a);       // 10

handler.deleteProperty()

delete操作符的捕获器。

用途:用于删除对象的某个属性;如果没有志向这个属性的引用,那么它最终会被释放。

基本用法:

var p = new Proxy({}, {
  deleteProperty: function(target, prop) {
    console.log('called: ' + prop);
    return true;
  }
});

delete p.a; // "called: a"

handler.ownKeys()

Object.getOwnPropertyNames方法和Object.getOwnPropertySymbols方法的捕获器。

用途:返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但包括Symbol值作为名称的属性)组成的数组。

基本用法:

var p = new Proxy({}, {
  ownKeys: function(target) {
    console.log('called');
    return ['a', 'b', 'c'];
  }
});

console.log(Object.getOwnPropertyNames(p)); // "called"
                                            // [ 'a', 'b', 'c' ]

handler.apply()

用途:用于拦截函数的调用。

拦截目标对象的以下操作:

  • proxy(...args);
  • Function.prototype.apply()和Function.prototype.call()
  • Reflect.apply()

基本用法:

var p = new Proxy(function() {}, {
  apply: function(target, thisArg, argumentsList) {
    console.log('called: ' + argumentsList.join(', '));
    return argumentsList[0] + argumentsList[1] + argumentsList[2];
  }
});

console.log(p(1, 2, 3)); // "called: 1, 2, 3"
                         // 6

handler.construct()

用途:用于拦截new操作符,为了使new操作服在生产的Proxy对象上生效,用于初始化 代理的目标对象自身必须具有[[Construct]]内部方法(即 new target 必须是有效的)。

基本用法:

var p = new Proxy(function() {}, {
  construct: function(target, argumentsList, newTarget) {
    console.log('called: ' + argumentsList.join(', '));
    return { value: argumentsList[0] * 10 };
  }
});

console.log(new p(1).value); // "called: 1"
                             // 10

基本示例:

想要对Proxy掌握得更透彻,推荐手敲一下示例!

无操作转发代理

在以下例子中,我们使用了一个原生JavaScript对象,代理会将所有应用到它的操作转发到这个对象上。

let target = {};
let p = new Proxy(target, {});

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

console.log(target.a); // 37

验证

通过代理,你可以轻松地验证向一个对象的传值。下面的代码借此展示了set handler的作用。

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 > 150) {
        throw new RangeError('The age seems invalid');
      }
    }
    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 = 200;
// 抛出异常: Uncaught RangeError: The age seems invalid

扩展构造函数

方法代理可以轻松地通过一个新构造函数来扩展一个已有的构造函数。这个例子使用了construct和apply。

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

操作DOM节点

有时,我们可能需要互换两个不同的元素的属性或类名,下面的代码以此我哦目标,展示了set handler的使用场景。

let view = new Proxy({
  selected: null
}, {
  set: function(obj, prop, newval) {
    let oldval = obj[prop];

     if (prop === 'selected') {
      if (oldval) {
        oldval.setAttribute('aria-selected', 'false');
      }
      if (newval) {
        newval.setAttribute('aria-selected', 'true');
      }
    }
    // 默认行为是存储被传入 setter 函数的属性值
    obj[prop] = newval;
    return true;
  }
});

let item1 = view.selected = document.getElementById('item-1');
console.log(item1.getAttribute('aria-selected')); // 'true'

let item2 = view.selected = document.getElementById('item-2');
console.log(item1.getAttribute('aria-selected')); // 'false'
console.log(item2.getAttribute('aria-selected')); // 'true

值修正及附加属性

以下products代理会计算传值并根据需要转换为数组。这个代理对象同时支持一个叫做 latestBrowser的附加属性,这个属性可以同时作为getter和setter。

let products = new Proxy({
  browsers: ['Internet Explorer', 'Netscape']
}, {
  get: function(obj, prop) {
    // 附加一个属性
    if (prop === 'latestBrowser') {
      return obj.browsers[obj.browsers.length - 1];
    }
    return obj[prop];
  },
  set: function(obj, prop, value) {
    // 附加属性
    if (prop === 'latestBrowser') {
      obj.browsers.push(value);
      return;
    }
    // 如果类型是字符串,则进行转换
    if (typeof value === 'string') {
      value = [value];
    }

    obj[prop] = value;
    return true;
  }
});

console.log(products.browsers); // ['Internet Explorer', 'Netscape']
products.browsers = 'Firefox'; // 如果不小心传入了一个字符串
console.log(products.browsers); // ['Firefox'] 得到的依旧是一个数组

products.latesBrowser = 'Chrome';
console.log(products.browsers); // ['Firefox', 'Chrome'];
console.log(products.latestBrowser); // 'Chrome';

通过属性查找数组中的特定对象

以下代理为数组扩展了一些实用工具。如你所见,通过Proxy我们可以灵活的“定义”属性,而不需要使用Object.defineProperties方法。 下面例子可以用于单元格来查找表格中一行。在这种情况下,target是table.row.

let products = new Proxy([
  { name: 'FireFox'    , type: 'browser' },
  { name: 'Chrome'  , type: 'browser' },
  { name: 'Thunderbird', type: 'mailer' }
], {
  get: function(obj, prop) {
    // 默认行为是返回属性值, prop ?通常是一个整数
    if (prop in obj) {
      return obj[prop];
    }

    // 获取 products 的 number; 它是 products.length 的别名
    if (prop === 'number') {
      return obj.length;
    }

    let result, types = {};

    for (let product of obj) {
      if (product.name === prop) {
        result = product;
      }
      if (types[product.type]) {
        types[product.type].push(product);
      } else {
        types[product.type] = [product];
      }
    }

    // 通过 name 获取 product
    if (result) {
      return result;
    }

    // 通过 type 获取 products
    if (prop in types) {
      return types[prop];
    }

    // 获取 product type
    if (prop === 'types') {
      return Object.keys(types);
    }

    return undefined;
  }
});

console.log(products[0]); // { name: 'FireFox', type: 'browser' }
console.log(products['FireFox']); // { name: 'FireFox', type: 'browser' }
console.log(products.number); // 3
console.log(products.browser); // [{ name: 'FireFox', type: 'browser' }, { name: 'Chrome', type: 'browser'}]
console.log(products.types); // ['browser', 'mailer'];
console.log(products['Proxy']); // undefined

结语

由于篇幅有限,很多方法都只有基本用法,具体在开发中怎样去灵活运用,同学们可以结合示例自己来实践哟。

一行代码三个错.jpeg