ES6(ECMAScript 2015)新增了对 Reflect 和 Proxy 对象的支持,使得我们能够便捷地进行元编程。让我们通过示例来学习它们的用法。
1、什么是元编程
元编程 无异于 编程中的魔法!如果编写一个“能够读取、修改、分析、甚至生成新程序”的程序将会如何?是不是听起来很神奇、很强大?
维基百科这样描述元编程:元编程 是一种编程技术,编写出来的计算机程序能够将其他程序作为数据来处理。意味着可以编写出这样的程序:它能够读取、生成、分析或者转换其它程序,甚至在运行时修改程序自身。
2、什么是反射
反射 是元编程的一个分支。反射又有三个子分支:
- 自省(Introspection):代码能够自我检查、访问内部属性,我们可以据此获得代码的底层信息。
- 自我修改(Self-Modification):顾名思义,代码可以修改自身。
- 调解(Intercession):字面意思是“代他人行事”。在元编程中,调解的概念类似于包装(wrapping)、捕获(trapping)、拦截(intercepting)。
2.1:自省
在 ES6 引入 Reflect 对象 之前,我们也可以实现自省。下面是读取程序结构的示例:
var users = {
'Tom': 32,
'Bill': 50,
'Sam': 65
};
Object.keys(users).forEach(name => {
const age = users[name];
console.log(`User ${name} is ${age} years old!`);
});
我们读取了 users 对象的结构并以键值对的形式打印出来。
User Tom is 32 years old!
User Bill is 50 years old!
User Sam is 65 years old!
2.2:自我修改
以一个包含修改其自身的方法的 blog 对象为例:
var blog = {
name: 'freeCodeCamp',
modifySelf: function(key, value) {blog[key] = value}
}
blog.modifySelf('author', 'Tapas');
2.3:调解
元编程中的 调解 指的是改变其它对象的语义。在 ES6 之前,可以用 Object.defineProperty() 方法来改变对象的语义:
var sun = {};
Object.defineProperty(sun, 'rises', {
value: true,
configurable: false,
writable: false,
enumerable: false
});
console.log('sun rises', sun.rises); //sun rises true
sun.rises = false;
console.log('sun rises', sun.rises); //sun rises true
如你所见,我们创建了一个普通对象 sun,之后改变了它的语义:为其定义了一个不可写的 rises 属性。所以修改无效。
ES6 为我们提供了 Reflect 对象(Reflect API)来实现 自省,还提供了 Proxy 对象帮助我们实现 调解。我们要尽力避免 自我修改,所以本文不会过多谈及这一点。
需要说明的是,元编程并不是由 ES6 引入的,JavaScript 语言从一开始就支持元编程,ES6 只是让它变得更易于使用。
3、Proxy
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
术语 | 含义 |
---|---|
handler | 包含捕捉器(trap)的占位符对象,可译为处理器对象 |
traps | 提供属性访问的方法。这类似于操作系统中捕获器的概念。 |
target | 被 Proxy 代理虚拟化的对象。它常被作为代理的存储后端。根据目标验证关于对象不可扩展性或不可配置属性的不变量(保持不变的语义)。 |
先定义一个包含“捕获器”函数的 handler 对象,再使用这个 handler 和目标对象来创建一个代理对象,这个代理对象会应用 handler 中的自定义行为。
基本语法
const p = new Proxy(target, handler)
参数 | 作用 |
---|---|
target | 要使用 Proxy 包装的目标对象,可以是任何类型的对象,包括原生数组,函数,甚至另一个代理 |
handler | 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为 |
基础示例
在以下简单的例子中,当对象中不存在属性名时,默认返回值为 37。下面的代码以此展示了 get handler 的使用场景。
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
无操作转发代理
在以下例子中,我们使用了一个原生 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 > 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
扩展构造函数
方法代理可以轻松地通过一个新构造函数来扩展一个已有的构造函数。这个例子使用了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
4、Reflect
是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handler (en-US) 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。
与大多数全局对象不同 Reflect 并非一个构造函数,所以不能通过 new 运算符对其进行调用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。
Reflect 对象提供了以下静态方法,这些方法与 proxy handler 方法的命名相同。
静态方法 | 功能 |
---|---|
Reflect.apply(target, thisArgument, argumentsList) | 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。 |
Reflect.construct(target, argumentsList[, newTarget]) | 对构造函数进行 new 操作,相当于执行 new target(…args)。 |
Reflect.defineProperty(target, propertyKey, attributes) | 和 Object.defineProperty() 类似。如果设置成功就会返回 true |
Reflect.deleteProperty(target, propertyKey) | 作为函数的delete操作符,相当于执行 delete target[name]。 |
Reflect.get(target, propertyKey[, receiver]) | 获取对象身上某个属性的值,类似于 target[name]。 |
Reflect.getOwnPropertyDescriptor(target, propertyKey) | 类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符,否则返回 undefined。 |
Reflect.getPrototypeOf(target) | 类似于 Object.getPrototypeOf()。 |
Reflect.has(target, propertyKey) | 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。 |
Reflect.isExtensible(target) | 类似于 Object.isExtensible(). |
Reflect.ownKeys(target) | 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable 影响). |
Reflect.preventExtensions(target) | 类似于 Object.preventExtensions()。返回一个Boolean。 |
Reflect.set(target, propertyKey, value[, receiver]) | 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。 |
Reflect.setPrototypeOf(target, prototype) | 设置对象原型的函数。返回一个 Boolean,如果更新成功,则返回 true。 |
检测一个对象是否存在特定属性
const duck = {
name: 'Maurice',
color: 'white',
greeting: function() {
console.log(`Quaaaack! My name is ${this.name}`);
}
}
Reflect.has(duck, 'color');
// true
Reflect.has(duck, 'haircut');
// false
返回这个对象自身的属性
Reflect.ownKeys(duck);
// [ "name", "color", "greeting" ]
为这个对象添加一个新的属性
Reflect.set(duck, 'eyes', 'black');
// returns "true" if successful
// "duck" now contains the property "eyes: 'black'"
等等,现在问题来了:既然 Object 或 Function 对象中已经有这些方法了,为什么还要引入新的 API 呢?
困惑吗?让我们一探究竟。
- 集中在一个命名空间: JavaScript 已经支持对象反射,但是这些 API 没有集中到一个命名空间中。从 ES6 开始,它们被集中到 Reflect 对象中。与其他全局对象不同,Reflect 不是一个构造函数,不能使用 new 操作符来调用它,也不能将它当做函数来调用。Reflect 对象中的方法和 math 对象中的方法一样是 静态 的。
- 易于使用:Object 对象中的 自省 方法在操作失败的时候会抛出异常,这给开发者增加了处理异常的负担。也许你更倾向于把操作结果当做布尔值来处理,而不是去处理异常,借助 Reflect 对象就可以做到。