ES6 系列之 defineProperty 与 proxy
文章目录1. defineProperty1.1 descriptor1.2 Setters 和 Getters1.3 watch API1. definePropertyES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。语法:Object.defineProperty(obj, prop, desc
文章目录
1. defineProperty
ES5 提供了 Object.defineProperty
方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
语法:Object.defineProperty(obj, prop, descriptor)
obj: 要在其上定义属性的对象。
prop: 要定义或修改的属性的名称。
descriptor: 将被定义或修改的属性的描述符。
举例:
var obj = {};
Object.defineProperty(obj, "num", {
value : 1,
writable : true,
enumerable : true,
configurable : true
});
// 对象 obj 拥有属性 num,值为 1
虽然我们可以直接添加属性和值,但是使用这种方式,我们能进行更多的配置。
1.1 descriptor
函数的第三个参数 descriptor
所表示的属性描述符有两种形式:数据描述符和存取描述符。
两者均具有以下两种键值:
configurable
: 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。enumerable
:当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:
value
: 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。writable
: 当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false
存取描述符同时具有以下可选键值:
get
:一个给属性提供getter
的方法,如果没有 getter 则为 undefined。该方法返回值就是属性值
。默认为 undefined。set
:一个给属性提供setter
的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。
默认为 undefined。
注意:属性描述符必须是数据描述符或者存取描述符两种形式之一,不能同时是两者。
1.2 Setters 和 Getters
defineProperty
中的 get
和 set
,这两个方法又被称为 getter
和 setter
。由 getter
和 setter
定义的属性称做存取器属性
。
当程序查询存取器属性的值时,JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。
当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。
举例:
var obj = {}, value = null;
Object.defineProperty(obj, "num", {
get: function(){
console.log('执行了 get 操作')
return value;
},
set: function(newValue) {
console.log('执行了 set 操作')
value = newValue;
}
})
obj.num = 1 // 执行了 set 操作
console.log(obj.num); // 执行了 get 操作 // 1
封装一下监控数据改变的方法:
function Archiver() {
var value = null;
// archive n. 档案
var archive = [];
Object.defineProperty(this, 'num', {
get: function() {
console.log('执行了 get 操作')
return value;
},
set: function(value) {
console.log('执行了 set 操作')
value = value;
archive.push({ val: value });
}
});
this.getArchive = function() { return archive; };
}
var arc = new Archiver();
arc.num; // 执行了 get 操作
arc.num = 11; // 执行了 set 操作
arc.num = 13; // 执行了 set 操作
console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]
1.3 watch API
既然可以监控数据的改变,就可以实现当数据改变的时候,自动进行渲染工作。
我们可以通过watch
函数监听对象的某个属性值的改变,来执行dom的变化。
var obj = {
value: 1
}
watch(obj, "value", function(newvalue){
document.getElementById('container').innerHTML = newvalue;
})
document.getElementById('button').addEventListener("click", function(){
obj.value += 1
});
watch函数:
function watch(obj, name, func){
var value = obj[name];
Object.defineProperty(obj, name, {
get: function() {
return value;
},
set: function(newValue) {
value = newValue;
func(value)
}
});
if (value) obj[name] = value
}
1.4 defineProperty 的缺点
Object.defineProperty
不支持数组的拦截
2. proxy
使用 defineProperty
只能重定义属性的读取(get)
和设置(set)
行为,到了 ES6
,提供了 Proxy
,可以重定义更多的行为,比如 in
、delete
、函数调用
等更多行为。
Proxy
直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty
。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。我们来看看它的语法:
var proxy = new Proxy(target, handler);
- target —— 是要包装的对象,可以是任何东西,包括函数。
- handler —— 代理配置:带有“钩子”(“traps”,即拦截操作的方法)的对象。比如 get 钩子用于读取 target 属性,set 钩子写入 target 属性等等。
对 proxy
进行操作,如果在 handler
中存在相应的钩子,则它将运行,并且 Proxy 有机会对其进行处理,否则将直接对 target
进行处理。
2.1 handler
Proxy
是一种特殊的“奇异对象”。它没有自己的属性。如果 handler
为空,则透明地将操作转发给 target
。
要激活更多功能,让我们添加钩子。
对于对象的大多数操作,JavaScript 规范中都有一个所谓的“内部方法”,它描述了最底层的工作方式。 例如 [[Get]]
,用于读取属性的内部方法, [[Set]]
,用于写入属性的内部方法,等等。这些方法仅在规范中使用,我们不能直接通过方法名调用它们。
Proxy
钩子会拦截这些方法的调用。它们在代理规范和下表中列出。
对于每个内部方法,此表中都有一个钩子:可用于添加到 new Proxy
时的 handler
参数中以拦截操作的方法名称:
内部方法 | Handler 方法 | 何时触发 |
---|---|---|
[[Get]] | get | 读取属性 |
[[Get]] | set | 写入属性 |
[[HasProperty]] | has | in 运算符 |
[[Delete]] | deleteProperty | delete 操作 |
[[Call]] | apply | proxy 对象作为函数被调用 |
[[Construct]] | construct | new 操作 |
[[GetPrototypeOf]] | getPrototypeOf | Object.getPrototypeOf |
[[SetPrototypeOf]] | setPrototypeOf | Object.setPrototypeOf |
[[IsExtensible]] | isExtensible | Object.isExtensible |
[[PreventExtensions]] | preventExtensions | Object.preventExtensions |
[[DefineOwnProperty]] | defineProperty | Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] | getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor, for…in, Object.keys/values/entries |
[[OwnPropertyKeys]] | ownKeys | Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for…in, Object/keys/values/entries |
其中大多数用于返回值:
- [[Set]] 如果值已成功写入,则必须返回 true,否则返回 false。
- [[Delete]] 如果已成功删除该值,则必须返回 true,否则返回 false。
- ……依此类推,我们将在下面的示例中看到更多内容。
2.1.1 不使用 handler
首先,让我们创建一个没有任何钩子的代理:
let target = {};
let proxy = new Proxy(target, {}); // 空的handler对象
proxy.test = 5; // 写入 Proxy 对象 (1)
alert(target.test); // 返回 5,test属性出现在了 target 上!
alert(proxy.test); // 还是 5,我们也可以从 proxy 对象读取它 (2)
for(let key in proxy) alert(key); // 返回 test,迭代也正常工作! (3)
由于没有钩子,所有对 proxy 的操作都直接转发给 target。
- 写入操作
proxy.test=
会将值写入target
。 - 读取操作
proxy.test
会从target
返回对应的值。 - 迭代
proxy
会从target
返回对应的值。
我们可以看到,没有任何钩子,proxy 是一个 target 的透明包装.
2.1.2 带 “get” 钩子的默认值
要拦截读取操作,handler
应该有 get(target, property, receiver)
方法。
target
—— 是目标对象,该对象作为第一个参数传递给 new Proxy,property
—— 目标属性名,receiver
—— 如果目标属性是一个 getter 访问器属性
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // 默认值
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (没有这样的元素)
2.1.3 使用 “set” 钩子进行验证
set(target, property, value, receiver):
target
—— 是目标对象,该对象作为第一个参数传递给new Proxy
,property
—— 目标属性名称,value
—— 目标属性要设置的值,receiver
—— 与 get 钩子类似,仅与 setter 访问器相关。
如果写入操作成功,set
钩子应该返回 true
,否则返回 false
(触发 TypeError
)。
假设我们想要一个专门用于数字的数组。如果添加了其他类型的值,则应该抛出一个错误:
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // 拦截写入操作
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError (proxy 的 `set` 操作返回 false)
对于 set
操作, 它必须在成功写入时返回 true
。
2.1.4 使用 “ownKeys” 和 “getOwnPropertyDescriptor” 进行迭代
Object.keys
,for..in
循环和大多数其他遍历对象属性的方法都使用 [[OwnPropertyKeys]]
内部方法(由 ownKeys
钩子拦截) 来获取属性列表。
在下面的示例中,我们使用 ownKeys
钩子拦截 for..in
对 user
的遍历,还使用 Object.keys
和 Object.values
来跳过以下划线 _
开头的属性:
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" 过滤掉 _password
for(let key in user) alert(key); // name,然后是 age
// 对这些方法同样有效:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
2.1.5 “has” 钩子
has(target, property)
target
—— 是目标对象,作为第一个参数传递给 new Proxyproperty
—— 属性名称
示例如下
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end
}
});
alert(5 in range); // true
alert(50 in range); // false
2.2 Reflect
Reflect
是一个内置对象,可简化的创建 Proxy
。
以前的内部方法,比如[[Get]]
,[[Set]]
等等都只是规范,不能直接调用。
Reflect
对象使调用这些内部方法成为可能。它的方法是内部方法的最小包装。
这是 Reflect
执行相同操作和调用的示例:
操作 | Reflect 调用 | 内部方法 |
---|---|---|
obj[prop] | Reflect.get(obj, prop) | [[Get]] |
obj[prop] = value | Reflect.set(obj, prop, value) | [[Set]] |
delete obj[prop] | Reflect.deleteProperty(obj, prop) | [[Delete]] |
new F(value) | Reflect.construct(F, value) | [[Construct]] |
… | … | … |
例如:
let user = {};
Reflect.set(user, 'name', 'John');
alert(user.name); // John
Reflect
允许我们使用函数(Reflect.construct
,Reflect.deleteProperty
,……)执行操作(new
,delete
,……)。
对于每个可被 Proxy
捕获的内部方法,Reflect
都有一个对应的方法 Reflect,其名称和参数与 Proxy
钩子相同。
因此,我们可以用 Reflect
来将操作转发到原始对象。
在此示例中,钩子get
和 set
透明地(好像它们都不存在)将读/写操作转发到对象,并显示一条消息:
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET name=Pete"
- Reflect.get 读取一个对象属性
- Reflect.set 写入对象属性,成功返回 true ,否则返回 false
如果钩子想要将调用转发给对象,则只需使用相同的参数调用 Reflect.<method>
就足够了。
在大多数情况下,我们可以不使用 Reflect
完成相同的事情,例如,使用Reflect.get(target, prop, receiver)
读取属性可以替换为 target[prop]
。尽管有一些细微的差别。
2.2.1 为什么 Reflect.get 更好
我们有一个带有一个 _name
属性和一个 getter 的对象 user
。
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Expected: Admin
alert(admin.name); // 输出:Guest (?!?)
读取 admin.name
应该返回 "Admin"
,而不是 "Guest"
!
问题实际上出在代理中,在 (*)
行。
- 当我们读取
admin.name
,由于admin
对象自身没有对应的的属性,搜索将转到其原型userProxy
。 - 从代理读取
name
属性时,get
钩子会触发并从原始对象返回target[prop]
属性,在(*)
行 - 当调用
target[prop]
时,若prop
是一个getter
,它将在this=target
上下文中运行其代码。因此,结果是来自原始对象target
的this._name
即来自user
。
为了解决这种情况,我们需要 get
钩子的第三个参数 receiver
。它保证传递正确的 this
给 getter
。在我们的情况下是 admin
。Reflect.get
可以做到的。如果我们使用它,一切都会正常运行。
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
现在 receiver
,保留了对正确 this
的引用(即admin
)的引用,该引用将在 (*)
行中使用Reflect.get
传递给getter
。
Reflect
调用的命名方式与钩子完全相同,并且接受相同的参数。它们是通过这种方式专门设计的。
因此, return Reflect...
会提供一个安全的提示程序来转发操作,并确保我们不会忘记与此相关的任何内容。
更多推荐
所有评论(0)