defineProperty和Proxy数据劫持

前言 在js中常见的数据劫持有两种,一种是Object.definePropert,在Vue2.*版本中作为数据双向绑定的基础;另一种是ES2015中新增的Proxy,即将在Vue3中做数据数据双向绑定的基础

严格来讲Proxy应该被称为『代理』而非『劫持』,不过由于作用有很多相似之处,我们在下文中就不再做区分,统一叫『劫持』。
基于数据劫持的当然还有已经凉透的Object.observe方法,已被废弃。
Object.definePropert 在搞清楚Object.definePropert之前我们先要了解一下Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
  1. 写法:Object.getOwnPropertyDescriptor(obj, prop)
  2. 参数:obj-需要查找的目标对象;prop-目标对象内属性名称
  3. 返回值:如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined。
{ configurable: true,// 属性是否可以被操作,比如删除。 默认true enumerable: true,// 检测的属性值是否可以被更改,默认是true value: 2,// 该属性的值 writable: true,// 当且仅当指定对象的属性可以被枚举出时,默认true。 }

然后我们在使用definePropert做一些劫持,了解一下configurable,enumerable,value,writable的作用
// value let obj ={ a: 123, b: 234, c: function() { console.log('do ...') } } Object.defineProperty(obj, 'b', { value: 1214341 }) console.log(obj.b) // 1214341// writable let obj ={ a: 123, b: 234, c: function() { console.log('do ...') } } Object.defineProperty(obj, 'b', { writable: false }) obj.b = 'jsbin' console.log(obj.b)// 234// configurable let obj ={ b: 234 } Object.defineProperty(obj, 'b', { configurable: false }) delete obj.b console.log(obj.b) // 234// enumerable let obj = { b: 123, c: 456, fn: function () {} } Object.defineProperty(obj, 'b', { enumerable: false, }) for(let key in obj) { console.log(`key-----${obj[key]}`) }

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
  1. 语法:Object.defineProperty(obj, prop, descriptor)
  2. 参数:obj-要在其上定义属性的对象, prop-要定义或修改的属性的名称,descriptor- 将被定义或修改的属性描述符
{ enumerable: true,// 检测的属性值是否可以被更改,默认是true configurable: true,// 属性是否可以被操作,比如删除。 默认true get: function(){},// 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined set: function(){}// 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined }

我们在上述阐述的defineProperty和getOwnPropertyDescriptor的返回值,我们统称为“属性描述符”
对象里目前存在的属性描述符有两种主要形式:==数据描述符==和==存取描述符==。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
defineProperty和Proxy数据劫持
文章图片
image
let obj = { b: 123, c: 456, fn: function () {} } let _newValue = https://www.it610.com/article/obj.b Object.defineProperty(obj,'b', {// 使用该方法get,set必须同事存在 enumerable: true, configurable: true, writable: true, get: function(){ return _newValue }, set: function(newValue){ return _newValue = https://www.it610.com/article/newValue } })obj.b = 90 console.log(obj.b)

上面代码执行结果如下:

defineProperty和Proxy数据劫持
文章图片
image 就是说数据描述符中不能出现get,set;存取描述符中不能出现writable;并且在==存取描述中get和set要同时出现==;如果没有了get则访问别劫持的对象属性会显undefined;反之set方法没有,设置对象属性值不会生效
let obj = { b: 123, } let _newValue = https://www.it610.com/article/obj.b Object.defineProperty(obj,'b', { enumerable: true, configurable: true, set: function(newValue){ return _newValue = https://www.it610.com/article/newValue } })console.log(obj.b)// undefinedObject.defineProperty(obj,'b', { enumerable: true, configurable: true, get: function(){ return _newValue }, }) obj.b = 90 console.log(obj.b) // 123

数据劫持实现简版数据双向绑定
/** * 遍历所有属性 * @param {Object} data 遍历对象 */ function observe(data) { if (!data || typeof data !== 'object') { return; } Object.keys(data).forEach(function (key) { defineReactive(data, key, data[key]); }); }/** * 劫持监听数据 * @param {Object} data 监听对象 * @param {String} key 对象键名 * @param {String, Number} val对象键值 */ function defineReactive(data, key, val) { observe(val); // 如果子属性为object也进行遍历监听 Object.defineProperty(data, key, { configurable: false, enumerable: true, get: function () { //在Watcher初始化实例的时候回触发对应属性的get函数 return val }, set: function (newValue) { if (val === newValue) { return } val = newValue rander(val) } }) }function rander(value) { let dom = document.getElementById('app') console.log(value) dom.innerHTML = value }let obj = { b: 'I am jsbin' }observe(obj) rander(obj.b)

由上面的例子可以看出,使用defineProperty做数据劫持实现数据双向绑定,要做被检测对象的循环处理,且无法实现数组的检测绑定,检测数组则使用装饰着模式
let arrOld = Array.prototype let arrC = Object.create(arrOld) let arr = ['push'] // 装饰者模式 arr.forEach(function(method) { arrC[method] = function() { console.log('监听到数据') return arrOld[method].apply(this, arguments); } }); function rander(value) { let dom = document.getElementById('app') console.log(value) dom.innerHTML = value } let arrinfo = [1,2,3] arrinfo.__proto__ = arrC

Proxy Proxy 可以理解成在目标对象之前进行拦截,访问该对象属性需要先过拦截这一步骤。因此提供了一种机制,可以对外界的访问进行过滤和读写。
  1. 官方定义: Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
  2. 基本语法: let p = new Proxy(target, handler);
  3. 参数
target: 需要伪装(代理)的数据,该数据可以是任何类型的的对象,原生数组函数,也可以是另一个代理 handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器,理解为过滤数据的方法) *handler.apply() *handler.construct() *handler.defineProperty() *handler.deleteProperty() *handler.enumerate() *handler.get() *handler.getOwnPropertyDescriptor() *handler.getPrototypeOf() *handler.has() *handler.isExtensible() *handler.ownKeys() *handler.preventExtensions() *handler.set() *handler.setPrototypeOf()

//目标对象 let people = { name: 'jsBin', age: 18, }// handler拦截(伪装)数据的方法 let handler = { /** * handler.get() 方法用于拦截对象的读取属性操作。 * @param {Any} target 目标数据 * @param {String} property 被获取的属性名 * @param {Object} receiver Proxy或者继承Proxy的对象 */ get: function(target, property, receiver) { switch (property) { case 'name': return 'name:' + target[property]; break; case 'age': return 'age:' + target[property]; break; default: return '这个值没有定义 undefined' } },/** * handler.set() 方法用于拦截设置属性值的操作 * @param {*} target 目标数据 * @param {*} property 被设置的属性名 * @param {*} value 被设置的新值 * @param {*} receiver 最初被调用的对象。通常是proxy本身,但handler的set方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是proxy本身) */ set: function(target, property, value, receiver) { if(property === 'age' && typeof value !== "number") { console.log('传入数据格式不真确') } else { console.log(arguments) return Reflect.set(...arguments) } } }let p = new Proxy(people, handler) p.age = 4324 console.log(p.age)

问题1:对于对象检测只能检测一层
【defineProperty和Proxy数据劫持】问题2:监听数组,使用数组方法触发2次

    推荐阅读