元编程
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.apply()
- handler.construct()
- handler.defineProperty()
- handler.deleteProperty()
- handler.get()
- handler.getOwnPropertyDescriptor()
- handler.getPrototypeOf()
- handler.has()
- handler.isExtensible()
- handler.ownKeys()
- handler.preventExtensions()
- handler.set()
- handler.setPrototypeOf()
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()
包括两个参数target
与prototype
,它们分别指的是被拦截的目标对象以及赋予目标对象的新prototype(或是null
)。setPrototypeOf
返回一个布尔值,代表的含义为是否成功修改了[[Prototype]]
。当target
不可扩展,且prototype
与Object.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()
包括两个参数target
与property
,它们分别指的是被拦截的目标对象以及属性名称的描述。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()
包括三个参数target
、property
以及descriptor
,它们分别指的是被拦截的目标对象、属性名称的描述以及属性的描述符。属性的描述符是一个对象,它分为数据描述符和存取描述符。两者都拥有configurable
与enumerable
属性,除此外,数据描述符拥有value
与writable
属性,存取描述符拥有get
与set
属性。
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()
包括两个参数target
与property
,它们分别指的是被拦截的目标对象以及属性名称的描述。has
方法返回一个布尔值,代表属性是否存在于对象中。若目标对象不可扩展,或是某一属性被设置为不可配置(描述符中configurable
设置为false
)时,若返回false将会抛出TypeError
。
handler.get()
在读取代理对象的某个属性时,将触发该操作。触发的方式包括有属性的读取(如obj.prop
或obj['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()
包括三个参数,target
、property
以及receiver
。它们分别指的是被拦截的目标对象、属性名称的描述以及Proxy或继承Proxy的对象。get
方法可以返回任何类型的值,通常,该值为访问的属性的值。当属性被设置为不可写且不可配置时(描述符中configurable
与writable
均设为false
),将会抛出TypeError
。另外,若属性没有配置访问方法(即get
方法为undefined
)时,若不是返回undefined
也将产生错误。
handler.set()
在给代理对象的某个属性赋值时,将触发该操作。触发的方式包括有对对象的属性进行赋值(如obj.prop = value
或obj['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()
包括四个参数,target
、property
、value
以及receiver
。它们分别指的是被拦截的目标对象、属性名称的描述、要设置的新值以及Proxy或继承Proxy的对象。set
方法返回一个布尔值,代表此次赋值是否成功。在严格模式下,若返回false将会抛出TypeError
。set
方法与get
方法相同,在属性设置为不可配置且不可写,或是没有配置set
方法时也将产生错误。
handler.deleteProperty()
在删除代理对象的某个属性时,将触发该操作。触发的方式包括有使用delete
操作删除对象的属性(如delete obj.prop
或delete 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()
包括两个参数target
与property
,它们分别指的是被拦截的目标对象以及属性名称的描述。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()
包括三个参数,target
、thisArg
以及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()
包括三个参数,target
、argumentsList
以及newTarget
。它们分别指的是被拦截的目标对象、构造函数的参数以及最初被调用的构造函数。construct
方法必须返回一个对象,即新构建的实例。当返回值不是一个对象时,将会抛出TypeError
。
其它
上文中部分代理方法有存在许多种情况会导致JS引擎抛出TypeError
但未在文章中写出,具体的情况可参考MDN。另外,Proxy对象还曾支持了handler.enumerate()
代理方法,但该方法已被废弃,这里便不再提及。
可撤销的代理对象
使用Proxy.revocable()
可创建一个可撤销的代理对象。Proxy.revocable()
的返回值是一个对象,包括有proxy
和revoke
两个属性。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