双向绑定 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", {
  value1,
  configurabletrue,
  writabletrue,
  enumerabletrue,
});

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", {
    getfunction ({
      console.log("执行了 get 操作");
      return value;
    },
    setfunction (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 = { value1 };

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 = {
    value1
}

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

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!