详谈Object.defineProperty 及实现数据双向绑定


Posted in Javascript onJuly 18, 2020

Object.defineProperty() 和 Proxy 对象,都可以用来对数据的劫持操作。何为数据劫持呢?就是在我们访问或者修改某个对象的某个属性的时候,通过一段代码进行拦截行为,然后进行额外的操作,然后返回结果。那么vue中双向数据绑定就是一个典型的应用。

Vue2.x 是使用 Object.defindProperty(),来进行对对象的监听的。

Vue3.x 版本之后就改用Proxy进行实现的。

下面我们先来理解下Object.defineProperty作用。

一: 理解Object.defineProperty的语法和基本作用。

在理解之前,我们先来看看一个普通的对象,对象它是由多个名/值对组成的无序集合。对象中每个属性对于任意类型的值。

比如现在我们想创建一个简单的对象,可以简单的如下代码:

const obj = new Object; // 或 const obj = {};
obj.name = 'kongzhi';
console.log(obj.name); // 在控制台中会打印 kongzhi
obj.xxx = function() {
 console.log(111);
}
// 调用 xxx 方法
obj.xxx(); // 在控制台中会打印 111

但是除了上面添加对象属性之外,我们还可以使用 Object.defineProperty 来定义新的属性或修改原有的属性。最终会返回该对象。

接下来我们慢慢来理解下该用法。

基本语法:

Object.defineProperty(obj, prop, descriptor);

基本的参数解析如下:

obj: 可以理解为目标对象。

prop: 目标对象的属性名。

descriptor: 对属性的描述。

那么对于第一个参数obj 和 prop参数,我们很容易理解,比如上面的实列demo,我们定义的 obj对象就是第一个参数的含义,我们在obj中定义的name属性和xxx属性是prop的含义,那么第三个参数描述符是什么含义呢?

descriptor: 属性描述符,它是由两部分组成,分别是:数据描述符和访问器描述符,数据描述符的含义是:它是一个包含属性的值,并说明这个属性值是可读或不可读的对象。访问器描述符的含义是:包含该属性的一对 getter/setter方法的对象。

下面我们继续来理解下 数据描述符 和 访问器描述符具体包含哪些配置项含义及用法。

1.1 数据描述符

const obj = {
 name: 'kongzhi'
};
// 对obj对象已有的name属性添加数据描述
Object.defineProperty(obj, 'name', {
 configurable: true | false,
 enumerable: true | false,
 value: '任意类型的值',
 writable: true | false
});
// 对obj对象添加新属性的描述
Object.defineProperty(obj, 'newAttr', {
 configurable: true | false,
 enumerable: true | false,
 value: '任意类型的值',
 writable: true | false
});

如上代码配置,数据描述符有如上configurable,enumerable,value 及 writable 配置项。

下面我们来看下 每个描述符中每个属性的含义:

1)value

属性对应的值,值的类型可以是任意类型的。比如我先定义一个obj对象,里面有一个属性 name 值为 'kongzhi', 现在我们通过如下代码改变 obj.name 的值,如下代码:

const obj = {
 name: 'kongzhi'
};
// 对obj对象已有的name属性添加数据描述
Object.defineProperty(obj, 'name', {
 value: '1122'
});
console.log(obj.name); // 输出 1122

如果上面我不设置 value描述符值的话,那么它返回的值还是 kongzhi 的。比如如下代码:

const obj = {
 name: 'kongzhi'
};
// 对obj对象已有的name属性添加数据描述
Object.defineProperty(obj, 'name', {
 
});
console.log(obj.name); // 输出 kongzhi

2)writable

writable的英文的含义是:'可写的',在该配置中它的含义是:属性的值是否可以被重写,设置为true可以被重写,设置为false,是不能被重写的,默认为false。

如下代码:

const obj = {};
Object.defineProperty(obj, 'name', {
 'value': 'kongzhi'
});
console.log(obj.name); // 输出 kongzhi
// 改写obj.name 的值
obj.name = 111;
console.log(obj.name); // 还是打印出 kongzhi

上面代码中 使用 Object.defineProperty 定义 obj.name 的值 value = 'kongzhi', 然后我们使用 obj.name 进行重新改写值,再打印出 obj.name 可以看到 值 还是为 kongzhi , 这是 Object.defineProperty 中 writable 默认为false,不能被重写,但是下面我们将它设置为true,就可以进行重写值了,如下代码:

const obj = {};
Object.defineProperty(obj, 'name', {
 'value': 'kongzhi',
 'writable': true
});
console.log(obj.name); // 输出 kongzhi
// 改写obj.name 的值
obj.name = 111;
console.log(obj.name); // 设置 writable为true的时候 打印出改写后的值 111

3)enumerable

此属性的含义是:是否可以被枚举,比如使用 for..in 或 Object.keys() 这样的。设置为true可以被枚举,设置为false,不能被枚举,默认为false.

如下代码:

const obj = {
 'name1': 'xxx'
};
Object.defineProperty(obj, 'name', {
 'value': 'kongzhi',
 'writable': true
});
// 枚举obj的属性
for (const i in obj) {
 console.log(i); // 打印出 name1
}

如上代码,对象obj本身有一个属性 name1, 然后我们使用 Object.defineProperty 给 obj对象新增 name属性,但是通过for in循环出来后可以看到 只打印出 name1 属性了,那是因为 enumerable 默认为false,它里面的值默认是不可被枚举的。但是如果我们将它设置为true的话,那么 Object.defineProperty 新增的属性也是可以被枚举的,如下代码:

const obj = {
 'name1': 'xxx'
};
Object.defineProperty(obj, 'name', {
 'value': 'kongzhi',
 'writable': true,
 'enumerable': true
});
// 枚举obj的属性
for (const i in obj) {
 console.log(i); // 打印出 name1 和 name
}

4) configurable

该属性英文的含义是:可配置的意思,那么该属性的含义是:是否可以删除目标属性。如果我们设置它为true的话,是可以被删除。如果设置为false的话,是不能被删除的。它默认值为false。

比如如下代码:

const obj = {
 'name1': 'xxx'
};
Object.defineProperty(obj, 'name', {
 'value': 'kongzhi',
 'writable': true,
 'enumerable': true
});
// 使用delete 删除属性 
delete obj.name;
console.log(obj.name); // 打印出kongzhi

如上代码 使用 delete命令删除 obj.name的话,该属性值是删除不了的,因为 configurable 默认为false,不能被删除的。但是如果我们把它设置为true,那么就可以进行删除了。

如下代码:

const obj = {
 'name1': 'xxx'
};
Object.defineProperty(obj, 'name', {
 'value': 'kongzhi',
 'writable': true,
 'enumerable': true,
 'configurable': true
});
// 使用delete 删除属性 
delete obj.name;
console.log(obj.name); // 打印出undefined

如上就是 数据描述符 中的四个配置项的基本含义。那么下面我们来看看 访问器描述符 的具体用法和含义。

1.2 访问器描述符

访问器描述符的含义是:包含该属性的一对 getter/setter方法的对象。如下基本语法:

const obj = {};
Object.defineProperty(obj, 'name', {
 get: function() {},
 set: function(value) {},
 configurable: true | false,
 enumerable: true | false
});

注意:使用访问器描述符中 getter或 setter方法的话,不允许使用 writable 和 value 这两个配置项。

getter/setter

当我们需要设置或获取对象的某个属性的值的时候,我们可以使用 setter/getter方法。

如下代码的使用demo.

const obj = {};
let initValue = 'kongzhi';
Object.defineProperty(obj, 'name', {
 // 当我们使用 obj.name 获取该值的时候,会自动调用 get 函数
 get: function() {
  return initValue;
 },
 set: function(value) {
  initValue = value;
 }
});
// 我们来获取值,会自动调用 Object.defineProperty 中的 get函数方法。
console.log(obj.name); // 打印出kongzhi
// 设置值的话,会自动调用 Object.defineProperty 中的 set方法。
obj.name = 'xxxxx';
console.log(obj.name); // 打印出 xxx

注意:configurable 和 enumerable 配置项和数据描述符中的含义是一样的。

1.3:使用 Object.defineProperty 来实现一个简单双向绑定的demo

如下代码:

<!DOCTYPE html>
 <html>
  <head>
   <meta charset="utf-8">
   <title>标题</title>
  </head>
  <body>
   <input type="text" id="demo" />
   <div id="xxx">{{name}}</div>
   <script type="text/javascript">
    const obj = {};
    Object.defineProperty(obj, 'name', {
     set: function(value) {
      document.getElementById('xxx').innerHTML = value;
      document.getElementById('demo').value = value;
     }
    });
    document.querySelector('#demo').oninput = function(e) {
     obj.name = e.target.value;
    }
    obj.name = '';
   </script>
  </body>
</html>

1.4 Object.defineProperty 对数组的监听

看如下demo代码来理解下对数组的监听的情况。

const obj = {};
let initValue = 1;
Object.defineProperty(obj, 'name', {
 set: function(value) {
  console.log('set方法被执行了');
  initValue = value;
 },
 get: function() {
  return initValue;
 }
});
console.log(obj.name); // 1
obj.name = []; // 会执行set方法,会打印信息
// 给 obj 中的name属性 设置为 数组 [1, 2, 3], 会执行set方法,会打印信息
obj.name = [1, 2, 3];
// 然后对 obj.name 中的某一项进行改变值,不会执行set方法,不会打印信息
obj.name[0] = 11;
// 然后我们打印下 obj.name 的值
console.log(obj.name);
// 然后我们使用数组中push方法对 obj.name数组添加属性 不会执行set方法,不会打印信息
obj.name.push(4);
obj.name.length = 5; // 也不会执行set方法

如上执行结果我们可以看到,当我们使用 Object.defineProperty 对数组赋值有一个新对象的时候,会执行set方法,但是当我们改变数组中的某一项值的时候,或者使用数组中的push等其他的方法,或者改变数组的长度,都不会执行set方法。

也就是如果我们对数组中的内部属性值更改的话,都不会触发set方法。

因此如果我们想实现数据双向绑定的话,我们就不能简单地使用 obj.name[1] = newValue; 这样的来进行赋值了。

那么对于vue这样的框架,那么一般会重写 Array.property.push方法,并且生成一个新的数组赋值给数据,这样数据双向绑定就触发了。

因此我们需要重新编写数组的push方法来实现数组的双向绑定,我们可以参照如下方法来理解下。

1) 重写编写数组的方法:

const arrPush = {};
// 如下是 数组的常用方法
const arrayMethods = [
 'push',
 'pop',
 'shift',
 'unshift',
 'splice',
 'sort',
 'reverse'
];
// 对数组的方法进行重写
arrayMethods.forEach((method) => {
 const original = Array.prototype[method]; 
 arrPush[method] = function() {
  console.log(this);
  return original.apply(this, arguments);
 }
});
const testPush = [];
// 对 testPush 的原型 指向 arrPush,因此testPush也有重写后的方法
testPush.__proto__ = arrPush;
testPush.push(1); // 打印 [], this指向了 testPush
testPush.push(2); // 打印 [1], this指向了 testPush

2)使用 Object.defineProperty 对数组方法进行监听操作。

因此我们需要把上面的代码继续修改下进行使用 Object.defineProperty 进行监听即可:

Vue中的做法如下, 代码如下:

function Observer(data) {
 this.data = data;
 this.walk(data);
}
var p = Observer.prototype;
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
[
 'push',
 'pop',
 'shift',
 'unshift',
 'splice',
 'sort',
 'reverse'
].forEach(function(method) {
 // 使用 Object.defineProperty 进行监听
 Object.defineProperty(arrayMethods, method, {
  value: function testValue() {
   console.log('数组被访问到了');
   const original = arrayProto[method];
   // 使类数组变成一个真正的数组
   const args = Array.from(arguments);
   original.apply(this, args);
  }
 });
});
p.walk = function(obj) {
 let value;
 for (let key in obj) {
  // 使用 hasOwnProperty 判断对象本身是否有该属性
  if (obj.hasOwnProperty(key)) {
   value = obj[key];
   // 递归调用,循环所有的对象
   if (typeof value === 'object') {
    // 并且该值是一个数组的话
    if (Array.isArray(value)) {
     const augment = value.__proto__ ? protoAugment : copyAugment;
     augment(value, arrayMethods, key);
     observeArray(value);
    }
    /* 
     如果是对象的话,递归调用该对象,递归完成后,会有属性名和值,然后对
     该属性名和值使用 Object.defindProperty 进行监听即可
     */
    new Observer(value);
   }
   this.convert(key, value);
  }
 }
}
p.convert = function(key, value) {
 Object.defineProperty(this.data, key, {
  enumerable: true,
  configurable: true,
  get: function() {
   console.log(key + '被访问到了');
   return value;
  },
  set: function(newVal) {
   console.log(key + '被重新设置值了' + '=' + newVal);
   // 如果新值和旧值相同的话,直接返回
   if (newVal === value) return;
   value = newVal;
  }
 });
}
function observeArray(items) {
 for (let i = 0, l = items.length; i < l; i++) {
  observer(items[i]);
 }
}
function observer(value) {
 if (typeof value !== 'object') return;
 let ob = new Observer(value);
 return ob;
}
function def (obj, key, val) {
 Object.defineProperty(obj, key, {
  value: val,
  enumerable: true,
  writable: true,
  configurable: true
 })
}
// 兼容不支持 __proto__的方法
function protoAugment(target, src) {
 target.__proto__ = src;
}
// 不支持 __proto__的直接修改先关的属性方法
function copyAugment(target, src, keys) {
 for (let i = 0, l = keys.length; i < l; i++) {
  const key = keys[i];
  def(target, key, src[key]);
 }
}

// 下面是测试数据
var data = {
 testA: {
  say: function() {
   console.log('kongzhi');
  }
 },
 xxx: [{'a': 'b'}, 11, 22]
};
var test = new Observer(data);
console.log(test); 
data.xxx.push(33);

以上这篇详谈Object.defineProperty 及实现数据双向绑定就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
整理8个很棒的 jQuery 倒计时插件和教程
Dec 12 Javascript
js判断字符长度以及中英文数字等
Dec 31 Javascript
js查找某元素中的所有图片地址的方法
Jan 16 Javascript
window.print打印指定div指定网页指定区域的方法
Aug 04 Javascript
解析javascript中鼠标滚轮事件
May 26 Javascript
JavaScript判断按钮被点击的方法
Dec 13 Javascript
Bootstrap整体框架之JavaScript插件架构
Dec 15 Javascript
Vue开发中整合axios的文件整理
Apr 29 Javascript
JS作用域链详解
Jun 26 Javascript
knockoutjs模板实现树形结构列表
Jul 31 Javascript
Vue下路由History模式打包后页面空白的解决方法
Jun 29 Javascript
微信小程序间使用navigator跳转传值问题实例分析
Mar 27 Javascript
webpack+vue-cil 中proxyTable配置接口地址代理操作
Jul 18 #Javascript
Vue项目前后端联调(使用proxyTable实现跨域方式)
Jul 18 #Javascript
完美解决通过IP地址访问VUE项目的问题
Jul 18 #Javascript
Vue移动端项目实现使用手机预览调试操作
Jul 18 #Javascript
vue中移动端调取本地的复制的文本方式
Jul 18 #Javascript
vue中用 async/await 来处理异步操作
Jul 18 #Javascript
vue 使用async写数字动态加载效果案例
Jul 18 #Javascript
You might like
php中邮箱地址正则表达式实现与详解
2012/04/24 PHP
详细解读PHP中接口的应用
2015/08/12 PHP
PHP使用MPDF类生成PDF的方法
2015/12/08 PHP
php实现常见图片格式的水印和缩略图制作(面向对象)
2016/06/15 PHP
通过PHP的Wrapper无缝迁移原有项目到新服务的实现方法
2020/04/02 PHP
PHP使用PDO 连接与连接管理操作实例分析
2020/04/21 PHP
DIV菜单层实现代码
2010/11/19 Javascript
AngularJS HTML编译器介绍
2014/12/06 Javascript
将页面table内容与样式另存成excel文件的方法
2015/08/05 Javascript
CSS3 3D 技术手把手教你玩转
2016/09/02 Javascript
让编辑器支持word复制黏贴、截屏的js代码
2016/10/17 Javascript
JavaScript仿微信打飞机游戏
2020/07/05 Javascript
JavaScript寄生组合式继承实例详解
2018/01/06 Javascript
Vue+Webpack完美整合富文本编辑器TinyMce的方法
2018/11/30 Javascript
js类的继承定义与用法分析
2019/06/21 Javascript
Vue项目中使用better-scroll实现菜单映射功能方法
2019/09/11 Javascript
vue之延时刷新实例
2019/11/14 Javascript
vue组件入门知识全梳理
2020/09/21 Javascript
nginx配置域名后的二级目录访问不同项目的配置操作
2020/11/06 Javascript
python连接oracle数据库实例
2014/10/17 Python
python类继承用法实例分析
2015/05/27 Python
TF-IDF与余弦相似性的应用(二) 找出相似文章
2017/12/21 Python
python3实现微型的web服务器
2019/09/03 Python
python模式 工厂模式原理及实例详解
2020/02/11 Python
Python logging模块写入中文出现乱码
2020/05/21 Python
中国跨境电子商务网站:NewFrog
2018/03/10 全球购物
精致的手工皮鞋:Shoe Embassy
2019/11/08 全球购物
波兰最大的电商平台:Allegro.pl
2021/02/06 全球购物
EJB3推出JPA的原因
2013/10/16 面试题
音乐表演专业毕业生求职信
2013/10/14 职场文书
员工薪酬激励方案
2014/06/13 职场文书
三严三实对照检查材料范文
2014/09/23 职场文书
党员四风问题对照检查材料
2014/09/27 职场文书
golang中的空接口使用详解
2021/03/30 Python
Nginx快速入门教程
2021/03/31 Servers
SQL Server实现分页方法介绍
2022/03/16 SQL Server