详谈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 相关文章推荐
javascript 鼠标悬浮图片显示原图 移出鼠标后原图消失(多图)
Dec 28 Javascript
js 替换功能函数,用正则表达式解决,js的全部替换
Dec 08 Javascript
关闭浏览器窗口弹出提示框并且可以控制其失效
Apr 15 Javascript
使用jQuery的attr方法来修改onclick值
Jul 07 Javascript
jQuery 复合选择器应用的几个例子
Sep 11 Javascript
微信小程序 闭包写法详细介绍
Dec 14 Javascript
Vue中引入样式文件的方法
Aug 18 Javascript
JS计算两个时间相差分钟数的方法示例
Jan 10 Javascript
详解Vue-cli webpack移动端自动化构建rem问题
Apr 07 Javascript
Vue+Mock.js模拟登录和表格的增删改查功能
Jul 26 Javascript
详解vuex状态管理模式
Nov 01 Javascript
vue使用video插件vue-video-player详解
Oct 23 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防止sql注入代码实例
2013/12/18 PHP
php使用GD创建保持宽高比缩略图的方法
2015/04/17 PHP
CodeIgniter与PHP5.6的兼容问题
2015/07/16 PHP
PHP用户验证和标签推荐的简单使用
2016/10/31 PHP
Django中通过定时任务触发页面静态化的处理方式
2018/08/29 PHP
Laravel5.4简单实现app接口Api Token认证方法
2019/08/29 PHP
javascript实现动态增加删除表格行(兼容IE/FF)
2007/04/02 Javascript
jquery显示和隐藏div特效实例
2013/02/27 Javascript
一个检测表单数据的JavaScript实例
2014/10/31 Javascript
浅谈JavaScript实现面向对象中的类
2014/12/09 Javascript
在JavaScript的AngularJS库中进行单元测试的方法
2015/06/23 Javascript
JS+CSS实现简易的滑动门效果代码
2015/09/24 Javascript
理解JavaScript中Promise的使用
2016/01/18 Javascript
angularjs的select使用及默认选中设置
2017/04/08 Javascript
webpack 2的react开发配置实例代码
2017/07/28 Javascript
原生JS实现的轮播图功能详解
2018/08/06 Javascript
示例vue 的keep-alive缓存功能的实现
2018/12/13 Javascript
jQuery实现的鼠标拖动浮层功能示例【拖动div等任何标签】
2018/12/29 jQuery
详解一个基于react+webpack的多页面应用配置
2019/01/21 Javascript
python获取指定时间差的时间实例详解
2017/04/11 Python
python 美化输出信息的实例
2018/10/15 Python
python创建文件时去掉非法字符的方法
2018/10/31 Python
Python Selenium 之关闭窗口close与quit的方法
2019/02/13 Python
Python for循环及基础用法详解
2019/11/08 Python
python交互模式基础知识点学习
2020/06/18 Python
Python+kivy BoxLayout布局示例代码详解
2020/12/28 Python
美国酒店控股公司:Choice Hotels
2018/06/15 全球购物
美国名牌手表折扣网站:Jomashop
2020/05/22 全球购物
Java的五个基础面试题
2016/02/26 面试题
旷课检讨书大全
2014/01/21 职场文书
机械设计及其自动化求职推荐信
2014/02/17 职场文书
医院见习报告范文
2014/11/03 职场文书
2015年校务公开工作总结
2015/05/26 职场文书
刮痧观后感
2015/06/05 职场文书
诗词赏析-(浣溪沙)
2019/08/13 职场文书
浅谈Redis变慢的原因及排查方法
2022/06/21 Redis