双向绑定 Proxy 与 Object.defineProperty
文章出自:掘金 https://juejin.cn/post/6891577820821061646
作者:willghy
Object.defineProperty
ES5 提供了
Object.defineProperty
方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
语法
Object.defineProperty(obj, prop, descriptor)
举例
const target = {};
Object.defineProperty(target, "num", {
value: 1,
configurable: true,
writable: true,
enumerable: true,
});
console.log(target); // { num:1 }
属性说明
-
configurable
当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
-
enumerable
当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。默认为 false。数据描述符还具有以下可选键值:
-
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
-
writable
当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。默认为 false。
-
get
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined。
-
set
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined。
描述符
描述符分为数据描述符和存取描述符,这两个只能取其中之一,不能两者存在
描述符默认值
拥有布尔值的键 configurable、enumerable 和 writable 的默认值都是 false。属性值和函数的键 value、get 和 set 字段的默认值为 undefined。
描述符可拥有的键值
configurable | enumerable | value | writable | get | set |
---|---|---|---|---|---|
数据描述符 | YES | YES | YES | YES | NO |
存取描述符 | YES | YES | NO | NO | YES |
Setters 和 Getters
get和set这两个方法又被称为 getter 和 setter。由 getter 和 setter 定义的属性称做“存取器属性”。
当程序查询存取器属性的值时,JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。从某种意义上讲,这个方法负责“设置”属性值。可以忽略 setter 方法的返回值。
封装一个对象监听
function Archiver() {
let value = null;
let 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 }]
watch API
基于Object.defineProperty封装的属性监听,正是Vue2.x的双向绑定实现的原理
HTML 中有个 span 标签和 button 标签,实现点击button后,span的内容+1
<span id="container">1</span>
<button id="button">点击加 1</button>
传统的DOM操作方法
document.getElementById('button').addEventListener("click", function(){
var container = document.getElementById("container");
container.innerHTML = Number(container.innerHTML) + 1;
});
使用Object.defineProperty
使用Object.defineProperty监听的好处就是直接修改obj.value即可,相当于obj.value绑定在了DOM渲染层
const obj = { value: 1 };
let value = 1;
Object.defineProperty(obj, "value", {
get() {
return value;
},
set(newValue) {
value = newValue;
document.getElementById("container").innerHTML = newValue;
},
});
document.getElementById('button').addEventListener("click", function() {
obj.value += 1;
});
上面代码需要额外声明value,如果要监控很多个属性,那就要写一大堆额外的变量,可以封装一个watch函数,达到类似如下的调用
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) {
let value = obj[name];
Object.defineProperty(obj, name, {
get() {
return value;
},
set(newValue) {
value = newValue;
func(newValue);
},
});
}
Proxy
Proxy
对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)
使用 defineProperty 只能重定义属性的读取(get)和设置(set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 in、delete、函数调用等更多行为。
Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,ES6 原生提供 Proxy 构造函数,用来生成Proxy实例
语法
const p = new Proxy(target, handler)
属性说明
-
target
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
-
handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
const proxyObj = new Proxy(
{},
{
get(obj, prop) {
return obj[prop];
},
set(obj, prop, value) {
obj[prop] = value;
},
}
);
proxyObj.num = 2 // set操作
proxyObj.num // get 2
proxy除了对get和set的拦截以外,还有大量可拦截的方法,比如Reflect.has(等同于in),Reflect.ownKeys(类似Object.keys,Reflect.ownKeys不受到enumerable限制,Object.keys会受到enumerable限制)
const proxyObj = new Proxy(
{},
{
ownKeys(target) {
return [];
},
has(target, key) {
if (key[0] === "_") {
return false;
}
return Reflect.has(target, key);
},
}
);
Reflect.ownKeys(proxyObj); // [];
proxyObj.abc = 1;
proxyObj._abc = 2;
Reflect.has(proxyObj, "abc"); // true
Reflect.has(proxyObj, "_abc"); // false
通过Proxy重写watch
function watch(target, func) {
const proxy = new Proxy(target, {
get(obj, prop) {
return obj[prop];
},
set(obj, prop, value) {
target[prop] = value;
func(prop, value);
},
});
return proxy;
}
watch(obj, (key, value) => {
if (key === "value") {
document.getElementById("container").innerHTML = value;
}
});
document.getElementById("button").addEventListener("click", function () {
newObj.value += 1;
});
基于双向绑定的优劣比较
-
Object.definedProperty
作用是劫持一个对象的属性,劫持属性的getter和setter方法,在对象的属性发生变化时进行特定的操作。而 Proxy 劫持的是整个对象。 -
Proxy
会返回一个代理对象,我们只需要操作新对象即可,而 Object.defineProperty只能遍历对象属性直接修改。 -
Object.definedProperty
不支持数组,更准确的说是不支持数组的各种API,因为如果仅仅考虑arry[i] = value 这种情况,是可以劫持的,但是这种劫持意义不大。而Proxy
可以支持数组的各种API。 -
尽管 Object.defineProperty
有诸多缺陷,但是其兼容性要好于Proxy
-
PS: Vue2.x 使用 Object.defineProperty
实现数据双向绑定,V3.0 则使用了Proxy
原文始发于微信公众号(前端24):双向绑定 Proxy 与 Object.defineProperty
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/51292.html