随着JavaScript的不断发展,开发者对于这门语言的开发越来越深入。为此,ES6 引入了 proxy 和 reflect API,给开发者对于JavaScript的底层操作,提供了更多的选择和可能。

通过这两个API,我们可以拦截JavaScript方法中的一些默认行为,从而在这些默认行为的基础上进行相关操作以及调整(比如,属性查找,赋值,枚举,函数调用等等)。这样,就相当于在语言层面上重新定义了相关方法。

接下来,我们来看看这两个API的相关内容。

一、Proxy

从字面意思来说,Proxy 表示代理的意思,这里可以理解为通过 Proxy 这个 API 在原有的基础上代理某些操作。

由于 Porxy 是针对JavaScript的底层操作,因此,也被成为 “元编程”(Meta programming)。它的基本语法如下:

var proxy = new Proxy(target, handler);

通过 Proxy 构造函数创建了一个实例,该实例接收两个参数,target 表示要拦截的对象。而 handler 也是一个对象,其中定义拦截的内容。

比如,最简单的拦截,获取属性:

var obj = {};

var proxy = new Proxy({}, {
get: function(target, key) {
return 10;
}
});

console.log(obj.a); // undefined
console.log(obj.b); // undefined

console.log(proxy.a); // 10
console.log(proxy.b); // 10

上面的代码中,obj 对象上由于没有给属性 ab 赋值,所以它们的值都是 undefined。然后,我们通过 Proxy 重新定义了 get 方法,并且在方法中返回了 10。虽然 proxy 对象上也没有给属性 ab 赋值,但它们的值却都是 10。

文章开头说到,是通过改变某些方法的默认行为,因此,我们来看看是哪些方法,怎么用?

1.1 set(target, key, value, receiver)

该方法主要用于拦截属性的设置。它的使用如下:

var proxy = new Proxy({}, {
set: function(target, key, value) {
if (isNaN(value)) {
throw new Error('proxy对象的属性值必须为数字');
}
}
});

proxy.a = 5;
proxy.b = 0.2;
proxy.c = 'c'; // 报错:proxy对象的属性值必须为数字

上面的代码,通过 Proxy 重新定义了 set 方法。规定 proxy 对象的属性值必须得是数字,否者就报错。

1.2 get(target, key, receiver)

该方法主要用于拦截属性的读取。它的使用如下:

var obj = {};

var proxy = new Proxy({}, {
get: function(target, key) {
if (key in target) {
return target[key];
} else {
throw new Error('对象proxy不存在' + key + '属性');
}
}
});

console.log(obj.a); // undefined

proxy.a = 5;
console.log(proxy.a); // 5
console.log(proxy.b); // 报错:对象proxy不存在b属性

默认情况下,如果对象上没有设置相应的属性,读取这个属性值会得到一个 undefined,比如上面代码中的 obj.a 。虽然返回的值 undefined 意思为 未定义,但总感觉有些别扭。

于是,我们通过 Proxy 重新定义了 get 方法。有值就读取对应的值,没有值就抛出错误。

1.3 has(target, key)

该方法主要用于拦截 HasProperty 操作。我们通常都是用 in 操作符来判断一个属性是否存在于对象中,如果在对象本身,或者原型上找到这个属性,则该操作将返回 true。反之,如果不找到相应的对象,则返回 false。如:

var person = {
name: 'cyclone',
age: '29'
};

console.log('age' in person); // true

in 的底层操作主要是应用了HasProperty操作。 所以,has(target, key) 的使用如下:

var person = {
name: 'cyclone',
age: '29'
};

var proxy = new Proxy(person, {
get: function(target, key) {
if (key === 'age') {
return '保密哦!';
}
return target[key];
},
has: function(target, key) {
if (key === 'age') {
return false;
}
return key in target;
}
});

console.log(proxy.name); // "cyclone"
console.log(proxy.age); // "保密哦!"
console.log('age' in proxy); // false

这里的代码有点特别。我们在 handler 中定义了两个拦截操作,有 gethas。可以看到,我们可以正常读取 name 属性,但是 age 属性被隐藏起来。

接着,我们使用 in 操作符来探测 age 属性。因为我们在拦截行为中进行了判断处理,所以外部便找不到 age 属性(返回 false)。

1.4 defineProperty(target, key)

该方法主要用于拦截 Object.defineProperty 操作。默认情况下,Object.defineProperty 是在一个对象上定义一个新的属性,或修改一个已经存在的属性。如:

var person = {};

Object.defineProperty(person, 'name', {
value: 'cyclone'
});

Object.defineProperty(person, 'age', {
value: 29
});

console.log(obj); // {name: "cyclone", age: 29}

于是,defineProperty(target, key) 方法的拦截操作可以写成这样:

var person = {
name: 'cyclone'
};

var proxy = new Proxy(person, {
defineProperty: function(target, key, value) {
if (key === 'name') {
return false;
}
Object.defineProperty(target, key, {value: value});
}
});

proxy.name = 'yix';
proxy.job = 'designer';

console.log(proxy.name); // "cyclone"
console.log(proxy.job);
// {value: "designer", writable: true, enumerable: true, configurable: true}

上面的代码,尝试着覆盖 name 属性,结果失败了。但却成功的增加了 job 属性。

1.5 deleteProperty(target, key)

该方法主要用于拦截 delete 操作。我们知道,delete 操作符可用于删除对象属性,当删除成功时返回 true,否者将返回 false。并且,严格模式下,当删除一个不可配置的属性,delete 操作符将会抛出一个错误。

我们还知道,一般前面以下划线 _ 开头的属性,被称为内部属性。

现在我不使用严格模式,也能实现删除以下划线 _ 开头的属性,并将会报错的效果。只需要这样:

var person = {
name: 'cyclone',
_age: '29'
};

var proxy = new Proxy(person, {
deleteProperty: function(target, key) {
if (key[0] === '_') {
return false;
}
return delete target[key];
}
});

console.log(delete proxy.name); // true
console.log(proxy.name); // undefined

console.log(delete proxy._age); // false
console.log(proxy._age); // 29

这里首先删除了 name 属性,可以看到删除成功并返回了 true,同时再次读取 name 属性时为 undefined。

然后,我们再删除 _age 属性。结果删除不成功并返回了 false(为了方便后面读取,所以这里没用 throw Error),所以后面也能正常读取到 _age 属性。

1.6 setPrototypeOf(target, proto)

该方法主要用于拦截 Object.setPrototypeOf() 操作。默认情况下,Object.setPrototypeOf() 是将对象的原型(即对象的[[Prototype]]内部属性)设置为另一个对象或者null。如:

var person = {
showName: function() {
return 'yix';
}
};

console.log(person.__proto__); // {}

Object.setPrototypeOf(person, window);
console.log(person.__proto__); // window

Object.setPrototypeOf(person, null);
console.log(person.__proto__); // undefined

可以看到,person 的原型是普通的对象。然后我们将 person 原型设置为 window,此时通过内置属性 __proto__ 再去读取它的原型,便得到 window。最后,将 person 原型设置为 null,因为 null 比较特殊,所以,此时得到是 undefined

接着,我们来看 setPrototypeOf(target, proto) 的使用:

var person = {
showName: function() {
return 'yix';
}
};

var proxy = new Proxy(person, {
setPrototypeOf: function(target, proto) {
throw new Error('该实例不允许改变原型');
}
});

Object.setPrototypeOf(proxy, window); // 报错:该实例不允许改变原型

上面的代码,希望将 person 对象的原型改为 window,结果被拦截了,并抛出一个错误。

1.7 getPrototypeOf(target)

该方法主要用于拦截 Object.getPrototypeOf() 操作。 Object.getPrototypeOf() 的功能是获取指定对象的原型(即内部属性[[Prototype]])。

所以,getPrototypeOf(target) 方法可以这样用:

var person = {
showName: function() {
return 'yix';
}
};

var proxy = new Proxy(person, {
getPrototypeOf: function(target) {
throw new Error('不能获取该实例的原型');
}
});

Object.getPrototypeOf(proxy); // 报错:不能获取该实例的原型

在这里,我们禁止了通过 Object.getPrototypeOf() 来读取原型。

1.8 apply(target, object, args)

该方法主要用于拦截函数的调用。 它的用法如下:

function getData() {
return [1, 'a'];
}

var proxy = new Proxy(getData, {
apply: function(target, ctx, args) {
return args;
}
});

console.log(proxy(2, 'b')); // [2, "b"]
console.log(proxy.call(null, 3, 'c')); // [3, "c"]
console.log(proxy.apply(null, [4, 'd'])); // [4, "d"]

可以看到,无论是函数的普通调用,还是通过 call 或者 apply 调用,这些统统被拦截了。

1.9 construct(target, args)

该方法主要用于拦截 new 操作符的相关操作。 它的用法如下:

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

var proxy = new Proxy(Person, {
construct: function(target, args) {
args[0] = args[0] + '_' + 'cyclone';
return args;
}
});

var p = proxy('yix'),
newP = new proxy('yix');

console.log(p); // "yix"
console.log(newP); // ["yix_cyclone"]

可以看到,正常调用 proxy ,返回的值为传入的参数。而当使用 new 调用 proxy 则被拦截了,我们对返回值进行了加工。所以,最后得到 yix_cyclone

这里特别要注意,construct 方法必须的返回对象。否者,会出现报错。这也是为什么我这里返回的是 args 而不是 args[0] 的原因。

1.10 ownKeys(target)

该方法主要用于拦截内部方法 [[OwnPropertyKeys]] 的相关操作。 涉及这些相关操作的方法有:

  • Object.keys()
  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.assign()

Object.keys(),它的功能是返回一个由给定对象的所有可枚举自身属性的属性名组成的数组。它的基本功能如下:

var arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // ["0", "1", "2"]

var person = {name: 'yix', job: 'web'};
console.log(Object.keys(person)); // ["name", "job"]

那么它对应的拦截方法是这样的:

var person = {
name: 'yix',
job: 'web',
age: 29
};

var proxy = new Proxy(person, {
ownKeys: function(target) {
return ['name', 'job'];
}
});

console.log(proxy); // Proxy {name: "yix", job: "web", age: 29}
console.log(Object.keys(person)); // ["name", "job", "age"]
console.log(Object.keys(proxy)); // ["name", "job"]

这里只能读取到 namejob 的属性,其他方法都是类似。

1.11 getOwnPropertyDescriptor(target, key)

该方法主要用于拦截 Object.getOwnPropertyDescriptor() 的相关操作。Object.getOwnPropertyDescriptor() 的功能是返回指定对象上一个自有属性对应的属性描述符。比如:

var person = {
name: 'yix'
};

var descriptor = Object.getOwnPropertyDescriptor(person, 'name');

console.log(descriptor); // {value: "yix", writable: true, enumerable: true, configurable: true}

这样就获得了一个 obj 对象有关 name 属性的一个描述符。

那么,相应的拦截行为可以这样:

var person = {
name: 'yix',
job: 'web'
};

var proxy = new Proxy(obj, {
getOwnPropertyDescriptor: function(target, key) {
if (key === 'job') {
var descriptor = Object.getOwnPropertyDescriptor(target, key);
descriptor['value'] = 'tester';
return descriptor;
}
return Object.getOwnPropertyDescriptor(target, key);
}
});

console.log(Object.getOwnPropertyDescriptor(proxy, 'name'));
// Object {value: "yix", writable: true, enumerable: true, configurable: true}

console.log(Object.getOwnPropertyDescriptor(proxy, 'job'));
// {value: "tester", writable: true, enumerable: true, configurable: true}

上面的代码中,对 name 属性使用默认行为。而对 job 属性则进行拦截,将它的属性值更改为 tester 。

1.12 isExtensible(target)

该方法主要用于拦截 Object.isExtensible() 的相关操作。Object.isExtensible() 的功能是判断一个对象是否是可扩展(是否可以在它上面添加新的属性)。比如:

var obj = {};

console.log(Object.isExtensible(obj)); // true

var seal = {name: 'yix'},
freeze = {name: 'yix'},
sealed = Object.seal(seal),
frozen = Object.freeze(freeze);

// 密封和冻结的对象,都不能扩展
console.log(Object.isExtensible(sealed)); // false
console.log(Object.isExtensible(frozen)); // false

所以,我们可以通过 isExtensible(target) 方法来拦截默认行为:

var obj = {};

var proxy = new Proxy(obj, {
isExtensible: function(target) {
target.whatever = 'xx';
return 1;
}
});

console.log(Object.isExtensible(proxy)); // true
console.log(proxy); // {whatever: "xx"}

这里,我们只是简单的在 isExtensible 方法中给 obj 新增了一个属性。因为该方法的使用还是有很强的限制的,比如:

  1. isExtensible 方法最终返回的值都会转换为布尔值(这里的 1 被转为了 true)
  2. isExtensible 方法返回的布尔值必须和 Object.isExtensible() 执行的结果一致,否者会报错(如果这里把 1 换成 0,则报错)。

1.13 preventExtensions(target)

该方法主要用于拦截 Object.preventExtensions() 的相关操作。Object.preventExtensions() 的功能是让一个对象变的不可扩展(即不能在它上面添加新的属性),并返回原来的对象。比如:

var obj = {name: 'yix'};

console.log(Object.isExtensible(obj)); // true

Object.preventExtensions(obj);

console.log(Object.isExtensible(obj)); // false

obj.job = 'web';

console.log(obj); // {name: "yix"}

由上面的代码,可以看到一开始 obj 是可扩展的。被应用了 Object.preventExtensions 后,便不能扩展了,添加 job 属性也不成功。

再来看看,preventExtensions(target) 的拦截行为:

var obj = {name: 'yix'};

var proxy = new Proxy(obj, {
preventExtensions: function(target) {
return 1;
}
});

console.log(Object.preventExtensions(proxy));
// 报错:'preventExtensions' on proxy: trap returned truish but the proxy target is extensible

这里的拦截行为报错了,因为 obj 对象是可扩展的。然后,我们把它变为不可扩展:

var obj = {name: 'yix'};

Object.preventExtensions(obj);

var proxy = new Proxy(obj, {
preventExtensions: function(target) {
target.whatever = 'xx';
return 1;
}
});

console.log(Object.preventExtensions(proxy)); // Proxy {name: "yix"}
console.log(proxy); // Proxy {name: "yix"}

此时,拦截行为不再报错了,但由于 obj 不能扩展了,所以 whatever 属性将添加失败,最终只是返回了原对象 {name: "yix"}

Proxy.revocable() - 可撤销的 proxy

一般情况下,一旦 Proxy 被创建,Proxy 就不能和 targe 解绑,上文提到的所有例子都是这种类型。但在某些特殊情况下,你只是想在暂时使用 Proxy,而在后面撤销它的相关功能。

那么,你可以通过 Proxy.revocable() 方法来创建一个可撤销的 Proxy,它返回一个对象,该对象包含以下属性:

  • proxy: 撤销的 proxy 对象
  • revoke:调用撤销 proxy 的函数

当不需要它的功能时,只需要调用 revoke() 方法即可:

var person = {
name: 'yix'
};

var {proxy, revoke} = Proxy.revocable(person, {});

console.log(proxy.name); // "yix"

revoke();

console.log(proxy.name); // 报错:Cannot perform 'get' on a proxy that has been revoked

上面的代码,创建了一个可撤销的 Proxy。这里用到了解构,将 Proxy.revocable() 方法返回对象的同名属性赋值给了 proxy 和 revoke 这两个变量。当调用 revoke() 后,有关 Proxy 的相关联系全部被切断了,所以运行 proxy.name 时将出现报错。

二、Reflect

与 Proxy 相比,虽然 Reflect API 设计的目的也是用于弥补和完善 Javascrpt 的若干缺陷。但它更侧重于将已有的 JavaScript 方法或者运算符(in、delete等)的相关操作进行重新包装,从而让这些方法看上去更统一,使用起来也更简洁。

我们直接进入主题,看看它主要重新包装了哪些方法,以及它们的用法?

2.1 Reflect.set(target, key, value[, receiver])

该方法替代类似 target[key] = val 这样的操作,用于设置对象的属性,返回一个布尔值,表示是否设置成功。

var person = {};

Reflect.set(person, 'name', 'yix');
Reflect.set(person, 'job', 'web');

console.log(person); // {name: "yix", job: "web"}

上面的代码中,person 对象设置了两个属性。

需要说明的是,该方法还接收第四个参数(可选)- receiver。这个参数的作用域是在 target 中,来看下代码:

var person = {
name: 'yix',
set define(value) {
this.age = value;
}
};

var p = {age: 1};

Reflect.set(person, 'age', 10, p);

console.log(p); // {age: 10}

可以看到,p 对象因为应用了 person 中的 set 方法,所以它的 age 属性值变为了 10 。

2.2 Reflect.get(target, key[, receiver])

该方法替代类似 target[key] 这样的操作,用于获取对象身上某个属性的值,返回一个属性值。

var person = {
name: 'yix',
job: 'web'
};

console.log(Reflect.get(person, 'name')); // "yix"
console.log(Reflect.get(person, 'job')); // "web"

另外,该方法还接收第四个参数(可选)- receiver。这个参数的作用域是在 target 中,来看下代码:

var person = {
name: 'yix',
job: 'web',
get join() {
return this.name + ' work as a ' + this.job;
}
};

var p = {
name: 'cyclone',
job: 'designer'
};

console.log(Reflect.get(person, 'join', p)); // "cyclone work as a designer"

可以看到,p 对象因为应用了 person 中的 get 方法,所以它最终返回了 “cyclone work as a designer” 。

2.3 Reflect.has(target, key)

该方法替代类似 key in target 这样的操作,用于判断一个对象是否存在某个属性,返回一个布尔值,表示是否存在。

var person = {
name: 'yix'
};

console.log(Reflect.has(person, 'name')); // true
console.log(Reflect.has({a: 1}, 'b')); // false

2.4 Reflect.defineProperty(target, key, desc)

该方法替代类似 Object.defineProperty() 这样的操作,用于给对象添加或修改属性,返回一个布尔值,表示属性是否被添加或修改成功。

var person = {
name: 'yix'
};

console.log(Reflect.defineProperty(person, 'name', {value: 'cyclone'})); // true
Reflect.defineProperty(person, 'job', {value: 'u guess'});

console.log(person); // {name: "cyclone", job: "u guess"}

上面代码中,首先成功的修改了 name 属性,然后再添加了 job 属性。

2.5 Reflect.deleteProperty(target, key)

该方法替代类似 delete target[key] 这样的操作,用于删除对象上的某个属性,返回一个布尔值,表示属性是否被删除成功。

var person = {
name: 'yix',
job: 'web'
};

console.log(Reflect.deleteProperty(person, 'name')); // true

console.log(person); // {job: "web"}

Object.freeze(person);

console.log(Reflect.deleteProperty(person, 'job')); // false

在这里,首先成功删除了 name 属性。接着,我们将对象冻结起来,当再次尝试删除 job 属性时,返回了 false,也就是说,删除该属性不成功。

注意,如果删除对象上的一个不存在的属性,无论这个对象是否冻结,该方法都返回 true。

2.6 Reflect.setPrototypeOf(target, prototype)

该方法替代类似 Object.setPrototypeOf() 这样的方法,用于设置目标对象的 __proto__ 属性(原型),返回一个布尔值,表示原型是否被设置成功。

var person = {};

var name = 'yix';

console.log(person.name); // undefined

console.log(Reflect.setPrototypeOf(person, window)); // true

console.log(person.name); // "yix"

一开始,因为 person 中没有定义 name 属性,所以返回了 undefined。当我们将 person 的原型设置为 window 后,程序就会从 person 对象的原型(即 window)中查询 name 的值。

2.7 Reflect.getPrototypeOf(target)

该方法替代类似 Object.getPrototypeOf() 这样的方法,用于获取目标对象的 __proto__ 属性(原型),并返回该原型,如果没有原型,则返回 null。

function Person() {}

var p = new Person();

console.log(Reflect.getPrototypeOf(p) === Person.prototype); // true

console.log(Reflect.getPrototypeOf(Object.prototype)); // null

上面代码中,p 是构造函数 Person 的实例,所以 p 的原型就是 Person.prototype

2.8 Reflect.apply(target, thisArg, args)

该方法替代类似 Function.prototype.apply() 这样的方法,用于改变函数调用的作用域。

在之前,如果要获取一个数组中的最大数或最小数,可以这样:

var arr = [2.6, 5, 1, -10];

console.log(Math.max.apply(Math, arr)); // 5
console.log(Math.min.apply(Math, arr)); // -10
console.log(Math.min.call(Math, ...arr)); // -10

但由于最后一个使用了 call 调用,所以这里使用扩展符 ...,将数组的项逐个列出,以参数的形式传递给函数。

现在,应用 Reflect.apply 方法也能达到相同的目的:

var arr = [2.6, 5, 1, -10];

console.log(Reflect.apply(Math.floor, Math, arr)); // 2
console.log(Reflect.apply(Math.max, Math, arr)); // 5
console.log(Reflect.apply(Math.min, Math, arr)); // -10

上面代码中,第一个是对数组 arr 的第一项进行向下取整。后面两个分别是取数组的最大值和最小值。

2.9 Reflect.construct(target, args[, newTarget])

该方法替代类似 new target(…args) 这样的方法,用于构造函数进行 new 操作。

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

var p1 = new Person('yix'),
p2 = Reflect.construct(Person, ['yix']);


var date1 = new Date('1988, 10, 10'),
date2 = Reflect.construct(Date, [1988, 10, 10]);

console.log(date1); // Mon Oct 10 1988 08:00:00 GMT+0800
console.log(date2); // Thu Nov 10 1988 00:00:00 GMT+0800

上面的例子中,p1p2 都是构造函数 Person 的实例,它们都有相同属性和方法。然后,我们分别用 new DateReflect.construct 创建了一个时间对象,但不知为何,后者的月份竟然多了 1 。

2.10 Reflect.ownKeys(target)

该方法替代类似 Object.getOwnPropertyNames() 与 Object.getOwnPropertySymbols() 这样的方法,用于返回目标对象的属性键组成的数组。

var person = {
name: 'yix',
job: 'web',
[Symbol.for('name')]: 'cyclone',
};

console.log(Reflect.ownKeys(person)); // ["name", "job", Symbol(name)]
console.log(Reflect.ownKeys({})); // []

2.11 Reflect.getOwnPropertyDescriptor(target, key)

该方法替代类似 Object.getOwnPropertyDescriptor() 这样的方法,用于返回目标对象的指定属性的描述符,如果属性不存在,则返回 undefined。

var person = {
name: 'yix',
job: 'web',
[Symbol.for('name')]: 'cyclone',
};

console.log(Reflect.getOwnPropertyDescriptor(person, 'name'));
// {value: "yix", writable: true, enumerable: true, configurable: true}

console.log(Reflect.getOwnPropertyDescriptor(person, 'age')); // undefined

console.log(Reflect.getOwnPropertyDescriptor([], 'length'));
// {value: 0, writable: true, enumerable: false, configurable: false}

2.12 Reflect.isExtensible(target)

该方法替代类似 Object.isExtensible() 这样的方法,用于判断一个对象是否是可扩展的,返回一个布尔值,表示可否扩展。

var person = {};

console.log(Reflect.isExtensible(person)); // true

person.name = 'yix';

console.log(person); // {name: "yix"}

// 冻结对象
Object.freeze(person);

console.log(Reflect.isExtensible(person)); // false

person.job = 'web';

console.log(person); // {name: "yix"}

一开始,person 对象是可以扩展的,所以,我们可以在该对象上添加 name 属性。当冻结它后,该对象便不能扩展了,因此,添加 job 属性也不成功。

2.13 Reflect.preventExtensions(target)

该方法替代类似 Object.preventExtensions() 这样的方法,用于阻止对象的扩展(即不能添加新属性)。返回一个布尔值,表示对象的不可扩展性是否设置成功。

var person = {};

console.log(Reflect.isExtensible(person)); // true

person.name = 'yix';

console.log(person); // {name: "yix"}

// 让对象不可扩展
console.log(Reflect.preventExtensions(person)); // true;

person.job = 'web';

console.log(person); // {name: "yix"}

上面的代码,同样是将 person 对象由可扩展变为不可扩展。

总得来说,Proxy 和 Reflect 这两个API都是 ES6 针对JavaScript的设计缺陷而引入的。在JavaScript底层操作方面,它们给开发者提供了更多的可能。Proxy 主要是拦截或重新定义原有方法的默认行为,而 Reflect 则更倾向于原有方法的封装和完善。