元编程

Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running.[1]

元编程,即编写的程序可以生成、操纵其它程序,又或是在程序运行时改变其自身。在Javascript中,使用eval()运行一段以字符串存储的js代码便是元编程的一种方法。

ECMAScript 2015中引入了Proxy和Reflect两个对象,允许开发者使用它们自定义一些基本语言操作的行为,如对象属性的读取与赋值、方法的调用等。通过使用它们,便使开发者可在Javascript元级别进行编程。本文便以Proxy为主题,着重介绍Javascript中的Proxy对象及使用Proxy对象进行自定义Javascript中的基本语言操作。

Proxy

在Javascript中,Proxy对象用于定义基本操作的自定义行为。使用Proxy对象的语法为let p = new Proxy(target, handler),其中target是被代理的对象,handler是包含traps(陷阱)的占位符对象(简单的说,就是需要代理的操作的内容)。一个使用了Proxy对象代理的例子如下所示。

let obj = {
  a: 1,
};

let p = new Proxy(obj, {
  // 当访问obj对象的属性时,将进入该trap中
  get(target, prop) {
    // 当访问代理对象的属性时,将打印出下列语句
    console.log('in get trap');
    if (prop in target) {
      // 当访问代理对象存在的属性时,打印出属性的key
      console.log(prop);
    }
    return Reflect.get(target, prop);
  }
});

console.log(p.a);
console.log(p.b);
/*~
 * in get trap
 * a
 * 1
 * in get trap
 * undefined
 */

Proxy支持的操作

如上节例子代理了get操作,除此之外,Proxy对象支持以下一共13种可代理的操作:

handler.getPrototypeOf()

在读取代理对象的prototype时,将会触发该操作。触发的方式包括有读取对象的__proto__属性,使用Object.getPrototypeOf()Reflect.getPrototypeOf()Object.isPrototypeOf()以及instanceof操作。handler.getPrototypeOf()的使用如下所示:

let arr = [];
const p = new Proxy(arr, {
  getPrototypeOf(target) {
    console.log('target.__proto__ === Array.prototype:', target.__proto__ === Array.prototype);
    return Reflect.getPrototypeOf(target);
  }
});

console.log(p instanceof Array);
// target.__proto__ === Array.prototype: true
// true

handler.getPrototypeOf()包括一个参数target,它指被拦截的目标对象。getPrototypeOf必须返回一个对象或是null。当返回的值不是对象也不是null,或当target不可扩展且返回的原型不是本身的原型时,将会抛出TypeError

handler.setPrototypeOf()

在设置代理对象的prototype时,将会触发该操作。触发的方式包括有使用Object.setPrototypeOf()以及Reflect.setPrototypeOf()handler.setPrototypeOf()的使用如下所示:

let obj = {}, proto = {};

const p = new Proxy(obj, {
  setPrototypeOf(target, prototype) {
    // 不允许设置新的原型
    return false;
  }
});

console.log(Reflect.setPrototypeOf(p, proto));
// false
Object.setPrototypeOf(p, proto);
// TypeError: 'setPrototypeOf' on proxy: trap returned falsish

handler.setPrototypeOf()包括两个参数targetprototype,它们分别指的是被拦截的目标对象以及赋予目标对象的新prototype(或是null)。setPrototypeOf返回一个布尔值,代表的含义为是否成功修改了[[Prototype]]。当target不可扩展,且prototypeObject.getPrototypeOf(target)的值不相同时,将会抛出TypeError

handler.isExtensible()

在判断代理对象是否可扩展时,将会触发该操作。触发的方式包括有使用Object.isExtensible()以及Reflect.isExtensible()handler.isExtensible()的使用如下所示:

const obj = {};

const p = new Proxy(obj, {
  isExtensible(target) {
    console.log('isExtensible called');
    return Reflect.isExtensible(target);
  }
});

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

handler.isExtensible()包括一个参数target,它指的是被拦截的目标对象。isExtensible返回一个布尔值,或是转为布尔值,代表对象是否可扩展。返回的值必须与Object.isExtensible()的值相同,否则将会抛出TypeError

handler.preventExtensions()

在阻止代理对象扩展时,将会触发该操作。触发的方式包括有使用Object.preventExtensions()以及Reflect.preventExtensions()handler.preventExtensions()的使用如下所示:

const obj = {};

const p = new Proxy(obj, {
  preventExtensions(target) {
    console.log('preventExtensions called');
    Reflect.preventExtensions(target);
    return true;
  }
});

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

handler.preventExtensions()包括一个参数target,它指的是被拦截的目标对象。preventExtensions返回一个布尔值。代理的preventExtensions()只能返回true,否则将会抛出TypeError

handler.getOwnPropertyDescriptor()

在获取代理对象的属性描述时,将会触发该操作。触发的方式包括有使用Object.getOwnPropertyDescriptor()以及Reflect.getOwnPropertyDescriptor()handler.getOwnPropertyDescriptor()的使用如下所示:

let obj = { a: 1 };

const p = new Proxy(obj, {
  getOwnPropertyDescriptor(target, property) {
    return { configurable: true, enumerable: true, value: 2 };
  }
});

console.log(Object.getOwnPropertyDescriptor(p, 'a').value);
// 2

handler.getOwnPropertyDescriptor()包括两个参数targetproperty,它们分别指的是被拦截的目标对象以及属性名称的描述。getOwnPropertyDescriptor必须返回一个对象或是undefined

handler.defineProperty()

在定义代理对象的某个属性时,将会触发该操作。触发的方式包括有使用Object.defineProperty()以及Reflect.defineProperty()handler.defineProperty()的使用如下所示:

const obj = {};

const p = new Proxy(obj, {
  defineProperty(target, property, descriptor) {
    console.log(`defined property`, property);
    return Reflect.defineProperty(target, property, descriptor);
  }
});

const descriptor = { configurable: true, enumerable: true, value: 1, writable: true };

console.log(p.a);
// undefined
Object.defineProperty(p, 'a', descriptor);
// defined property a
console.log(p.a);
// 1

handler.defineProperty()包括三个参数targetproperty以及descriptor,它们分别指的是被拦截的目标对象、属性名称的描述以及属性的描述符。属性的描述符是一个对象,它分为数据描述符和存取描述符。两者都拥有configurableenumerable属性,除此外,数据描述符拥有valuewritable属性,存取描述符拥有getset属性。

handler.has()

在判断代理对象是否拥有某个属性时,将触发该操作。触发的方式包括有使用in操作、with操作以及使用Reflect.has()。另外,使用Object.create()继承的对象,使用in等操作时也将触发。handler.has()的使用如下所示:

const obj = { a: 1 };

const p = new Proxy(obj, {
  has(target, property) {
    const exists = Reflect.has(target, property);
    if (exists) {
      console.log(`${property} was exists`);
    }
    return exists;
  }
});

console.log('a' in p);
// a was exists
// true
console.log('b' in p);
// false

handler.has()包括两个参数targetproperty,它们分别指的是被拦截的目标对象以及属性名称的描述。has方法返回一个布尔值,代表属性是否存在于对象中。若目标对象不可扩展,或是某一属性被设置为不可配置(描述符中configurable设置为false)时,若返回false将会抛出TypeError

handler.get()

在读取代理对象的某个属性时,将触发该操作。触发的方式包括有属性的读取(如obj.propobj['prop'])以及使用Reflect.get()。同样,继承的对象也将触发该操作。handler.get()的使用如下所示:

const obj = { a: 1 };

const p = new Proxy(obj, {
  get(target, property) {
    return Reflect.get(target, property) * 2;
  }
});

console.log(p.a);
// 2

handler.has()包括三个参数,targetproperty以及receiver。它们分别指的是被拦截的目标对象、属性名称的描述以及Proxy或继承Proxy的对象。get方法可以返回任何类型的值,通常,该值为访问的属性的值。当属性被设置为不可写且不可配置时(描述符中configurablewritable均设为false),将会抛出TypeError。另外,若属性没有配置访问方法(即get方法为undefined)时,若不是返回undefined也将产生错误。

handler.set()

在给代理对象的某个属性赋值时,将触发该操作。触发的方式包括有对对象的属性进行赋值(如obj.prop = valueobj['prop'] = value)以及使用Reflect.set()handler.set()的使用如下所示:

const obj = { a: 1 };

const p = new Proxy(obj, {
  set(target, property, value) {
    return Reflect.set(target, property, value * 2);
  }
});

console.log(p.a);
// 1
p.a = 2;
console.log(p.a);
// 4

handler.has()包括四个参数,targetpropertyvalue以及receiver。它们分别指的是被拦截的目标对象、属性名称的描述、要设置的新值以及Proxy或继承Proxy的对象。set方法返回一个布尔值,代表此次赋值是否成功。在严格模式下,若返回false将会抛出TypeErrorset方法与get方法相同,在属性设置为不可配置且不可写,或是没有配置set方法时也将产生错误。

handler.deleteProperty()

在删除代理对象的某个属性时,将触发该操作。触发的方式包括有使用delete操作删除对象的属性(如delete obj.propdelete obj['prop'])以及使用Reflect.deleteProperty()handler.deleteProperty()的使用如下所示:

const obj = { a: 1 };

const p = new Proxy(obj, {
  deleteProperty(target, property) {
    console.log('deleting property', property);
    Reflect.deleteProperty(target, property);
  }
});

console.log(p.a);
// 1
delete p.a;
// deleting property a
console.log(p.a);
// undefined

handler.deleteProperty()包括两个参数targetproperty,它们分别指的是被拦截的目标对象以及属性名称的描述。deleteProperty方法返回一个布尔值,代表属性是否被成功删除。当属性被设置为不可配置时,将无法删除该属性。

handler.ownKeys()

在获取代理对象的所有属性的key时,将触发该操作。触发的方式包括有使用Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()以及Reflect.ownKeys()handler.ownKeys()的使用如下所示:

const obj = {};

const p = new Proxy(obj, {
  ownKeys(target) {
    return ['a', 'b', 'c'];
  },
});

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

handler.isExtensible()包括一个参数target,它指的是被拦截的目标对象。ownKeys的返回值必须为一个可枚举的对象(数组),其中存放对象拥有的属性键,且属性键必须是字符串或Symbol。返回的结果中,必须包含有对象中所有的不可配置的属性。当目标对象不可扩展时,返回的结果不可包含有除自有属性之外的值。

handler.apply()

handler.apply所代理的对象必须是可被调用的,即必须是一个函数。在调用代理的函数时,将触发该操作。触发的方式包括有调用该函数,使用Function.prototype.call()Function.prototype.apply以及Reflect.apply()handler.apply()的使用如下所示:

const add = (n1, n2) => {
  return n1 + n2;
}

const newAdd = new Proxy(add, {
  apply(target, thisArg, argArray) {
    return Reflect.apply(target, thisArg, argArray) * 2;
  }
});

console.log(add(1, 2));
// 3
console.log(newAdd(1, 2));
// 6

handler.has()包括三个参数,targetthisArg以及argumentsList。它们分别指的是被拦截的目标函数、被调用时的上下文对象及被调用时的参数数组。apply方法可以返回任何类型的值,通常即被代理的函数的返回值。

handler.construct()

在构造代理对象实例时,将触发该操作。触发的方式包括有new操作以及使用Reflect.construct()handler.construct()的使用如下所示:

class Test {
  constructor(num) {
    this.num = num;
  }
}

const ProxyTest = new Proxy(Test, {
  construct(target, argumentsList, newTarget) {
    argumentsList[0] = argumentsList[0] * 2;
    return Reflect.construct(target, argumentsList, newTarget);
  }
})

let instance = new ProxyTest(1);
console.log(instance.num);
// 2

handler.construct()包括三个参数,targetargumentsList以及newTarget。它们分别指的是被拦截的目标对象、构造函数的参数以及最初被调用的构造函数。construct方法必须返回一个对象,即新构建的实例。当返回值不是一个对象时,将会抛出TypeError

其它

上文中部分代理方法有存在许多种情况会导致JS引擎抛出TypeError但未在文章中写出,具体的情况可参考MDN。另外,Proxy对象还曾支持了handler.enumerate()代理方法,但该方法已被废弃,这里便不再提及。

可撤销的代理对象

使用Proxy.revocable()可创建一个可撤销的代理对象。Proxy.revocable()的返回值是一个对象,包括有proxyrevoke两个属性。proxy是创建的代理对象,它等同于new Proxy(target, handler)得到的结果。revoke是撤销代理的方法,执行后将撤销和他一起生成的proxy对象。创建后,当调用对象的revoke方法,就将撤销创建的代理行为。在调用revoke方法后,得到的代理对象将不再可用,任何对它的操作将抛出TypeError

const revocable = Proxy.revocable({}, {
  get(target, name) {
    return "[[" + name + "]]";
  }
});

const proxy = revocable.proxy;

console.log(proxy.foo);
// [[foo]]

revocable.revoke();

console.log(proxy.foo);
// TypeError: Cannot perform 'get' on a proxy that has been revoked

新建类的代理实例对象

上述所展示的代理方法大多都是使用在对象或者函数上,但通常有很多情况是需要将类的实例进行代理。若在每次实例化后调用Proxy对象进行代理操作将使代码过于复杂,此时就可以考虑将实例的代理放置与类构造函数中,如下所示:

class Example {
  constructor() {
    this.a = 1;

    return new Proxy(this, {
      get(target, prop) {
        if (prop in target) {
          return Reflect.get(target, prop) + 1;
        }
      },
    });
  }
}

let instance = new Example();
console.log(instance.a);
// 2

参考资料

  1. Metaprogramming - Wikipedia
  2. Proxy - MDN
  3. Meta programming - MDN