双向绑定 Proxy 与 Object.defineProperty


双向绑定 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"); // trueReflect.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
到顶部