前言
在js中常见的数据劫持有两种,一种是Object.definePropert,在Vue2.*版本中作为数据双向绑定的基础;另一种是ES2015中新增的Proxy,即将在Vue3中做数据数据双向绑定的基础
严格来讲Proxy应该被称为『代理』而非『劫持』,不过由于作用有很多相似之处,我们在下文中就不再做区分,统一叫『劫持』。
基于数据劫持的当然还有已经凉透的Object.observe方法,已被废弃。
Object.definePropert
在搞清楚Object.definePropert之前我们先要了解一下Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
- 写法:Object.getOwnPropertyDescriptor(obj, prop)
- 参数:obj-需要查找的目标对象;prop-目标对象内属性名称
- 返回值:如果指定的属性存在于对象上,则返回其属性描述符对象(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() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
- 语法:Object.defineProperty(obj, prop, descriptor)
- 参数:obj-要在其上定义属性的对象, prop-要定义或修改的属性的名称,descriptor- 将被定义或修改的属性描述符
{
enumerable: true, // 检测的属性值是否可以被更改,默认是true
configurable: true, // 属性是否可以被操作,比如删除。 默认true
get: function(){}, // 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined
set: function(){} // 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined
}
我们在上述阐述的defineProperty和getOwnPropertyDescriptor的返回值,我们统称为“属性描述符”
对象里目前存在的属性描述符有两种主要形式:==数据描述符==和==存取描述符==。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
let obj = {
b: 123,
c: 456,
fn: function () {}
}
let _newValue = obj.b
Object.defineProperty(obj, 'b', { // 使用该方法get,set必须同事存在
enumerable: true,
configurable: true,
writable: true,
get: function(){
return _newValue
},
set: function(newValue){
return _newValue = newValue
}
})
obj.b = 90
console.log(obj.b)
上面代码执行结果如下:
就是说数据描述符中不能出现get,set;存取描述符中不能出现writable;并且在==存取描述中get和set要同时出现==;如果没有了get则访问别劫持的对象属性会显undefined;反之set方法没有,设置对象属性值不会生效
let obj = {
b: 123,
}
let _newValue = obj.b
Object.defineProperty(obj, 'b', {
enumerable: true,
configurable: true,
set: function(newValue){
return _newValue = newValue
}
})
console.log(obj.b) // undefined
Object.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 可以理解成在目标对象之前进行拦截,访问该对象属性需要先过拦截这一步骤。因此提供了一种机制,可以对外界的访问进行过滤和读写。
- 官方定义: Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
- 基本语法: let p = new Proxy(target, handler);
- 参数
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:对于对象检测只能检测一层
问题2:监听数组,使用数组方法触发2次