前言
在js中常見的數(shù)據(jù)劫持有兩種,一種是Object.definePropert,在Vue2.*版本中作為數(shù)據(jù)雙向綁定的基礎(chǔ);另一種是ES2015中新增的Proxy,即將在Vue3中做數(shù)據(jù)數(shù)據(jù)雙向綁定的基礎(chǔ)
嚴(yán)格來講Proxy應(yīng)該被稱為『代理』而非『劫持』,不過由于作用有很多相似之處,我們?cè)谙挛闹芯筒辉僮鰠^(qū)分,統(tǒng)一叫『劫持』。
基于數(shù)據(jù)劫持的當(dāng)然還有已經(jīng)涼透的Object.observe方法,已被廢棄。
Object.definePropert
在搞清楚Object.definePropert之前我們先要了解一下Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor() 方法返回指定對(duì)象上一個(gè)自有屬性對(duì)應(yīng)的屬性描述符。(自有屬性指的是直接賦予該對(duì)象的屬性,不需要從原型鏈上進(jìn)行查找的屬性)
- 寫法:Object.getOwnPropertyDescriptor(obj, prop)
- 參數(shù):obj-需要查找的目標(biāo)對(duì)象;prop-目標(biāo)對(duì)象內(nèi)屬性名稱
- 返回值:如果指定的屬性存在于對(duì)象上,則返回其屬性描述符對(duì)象(property descriptor),否則返回 undefined。
{
configurable: true, // 屬性是否可以被操作,比如刪除。 默認(rèn)true
enumerable: true, // 檢測(cè)的屬性值是否可以被更改,默認(rèn)是true
value: 2, // 該屬性的值
writable: true, // 當(dāng)且僅當(dāng)指定對(duì)象的屬性可以被枚舉出時(shí),默認(rèn)true。
}
然后我們?cè)谑褂胐efinePropert做一些劫持,了解一下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() 方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性, 并返回這個(gè)對(duì)象。
- 語法:Object.defineProperty(obj, prop, descriptor)
- 參數(shù):obj-要在其上定義屬性的對(duì)象, prop-要定義或修改的屬性的名稱,descriptor- 將被定義或修改的屬性描述符
{
enumerable: true, // 檢測(cè)的屬性值是否可以被更改,默認(rèn)是true
configurable: true, // 屬性是否可以被操作,比如刪除。 默認(rèn)true
get: function(){}, // 一個(gè)給屬性提供 getter 的方法,如果沒有 getter 則為 undefined
set: function(){} // 一個(gè)給屬性提供 setter 的方法,如果沒有 setter 則為 undefined
}
我們?cè)谏鲜鲫U述的defineProperty和getOwnPropertyDescriptor的返回值,我們統(tǒng)稱為“屬性描述符”
對(duì)象里目前存在的屬性描述符有兩種主要形式:==數(shù)據(jù)描述符==和==存取描述符==。數(shù)據(jù)描述符是一個(gè)具有值的屬性,該值可能是可寫的,也可能不是可寫的。存取描述符是由getter-setter函數(shù)對(duì)描述的屬性。描述符必須是這兩種形式之一;不能同時(shí)是兩者。

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)
上面代碼執(zhí)行結(jié)果如下:

就是說數(shù)據(jù)描述符中不能出現(xiàn)get,set;存取描述符中不能出現(xiàn)writable;并且在==存取描述中g(shù)et和set要同時(shí)出現(xiàn)==;如果沒有了get則訪問別劫持的對(duì)象屬性會(huì)顯undefined;反之set方法沒有,設(shè)置對(duì)象屬性值不會(huì)生效
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
數(shù)據(jù)劫持實(shí)現(xiàn)簡(jiǎn)版數(shù)據(jù)雙向綁定
/**
* 遍歷所有屬性
* @param {Object} data 遍歷對(duì)象
*/
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function (key) {
defineReactive(data, key, data[key]);
});
}
/**
* 劫持監(jiān)聽數(shù)據(jù)
* @param {Object} data 監(jiān)聽對(duì)象
* @param {String} key 對(duì)象鍵名
* @param {String, Number} val 對(duì)象鍵值
*/
function defineReactive(data, key, val) {
observe(val); // 如果子屬性為object也進(jìn)行遍歷監(jiān)聽
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get: function () {
//在Watcher初始化實(shí)例的時(shí)候回觸發(fā)對(duì)應(yīng)屬性的get函數(shù)
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做數(shù)據(jù)劫持實(shí)現(xiàn)數(shù)據(jù)雙向綁定,要做被檢測(cè)對(duì)象的循環(huán)處理,且無法實(shí)現(xiàn)數(shù)組的檢測(cè)綁定,檢測(cè)數(shù)組則使用裝飾著模式
let arrOld = Array.prototype
let arrC = Object.create(arrOld)
let arr = ['push']
// 裝飾者模式
arr.forEach(function(method) {
arrC[method] = function() {
console.log('監(jiān)聽到數(shù)據(jù)')
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 可以理解成在目標(biāo)對(duì)象之前進(jìn)行攔截,訪問該對(duì)象屬性需要先過攔截這一步驟。因此提供了一種機(jī)制,可以對(duì)外界的訪問進(jìn)行過濾和讀寫。
- 官方定義: Proxy 對(duì)象用于定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數(shù)調(diào)用等)。
- 基本語法: let p = new Proxy(target, handler);
- 參數(shù)
target: 需要偽裝(代理)的數(shù)據(jù),該數(shù)據(jù)可以是任何類型的的對(duì)象,原生數(shù)組函數(shù),也可以是另一個(gè)代理
handler: 一個(gè)對(duì)象,其屬性是當(dāng)執(zhí)行一個(gè)操作時(shí)定義代理的行為的函數(shù)(可以理解為某種觸發(fā)器,理解為過濾數(shù)據(jù)的方法)
* 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()
// 目標(biāo)對(duì)象
let people = {
name: 'jsBin',
age: 18,
}
// handler攔截(偽裝)數(shù)據(jù)的方法
let handler = {
/**
* handler.get() 方法用于攔截對(duì)象的讀取屬性操作。
* @param {Any} target 目標(biāo)數(shù)據(jù)
* @param {String} property 被獲取的屬性名
* @param {Object} receiver Proxy或者繼承Proxy的對(duì)象
*/
get: function(target, property, receiver)
{
switch (property) {
case 'name': return 'name:' + target[property]; break;
case 'age': return 'age:' + target[property]; break;
default: return '這個(gè)值沒有定義 undefined'
}
},
/**
* handler.set() 方法用于攔截設(shè)置屬性值的操作
* @param {*} target 目標(biāo)數(shù)據(jù)
* @param {*} property 被設(shè)置的屬性名
* @param {*} value 被設(shè)置的新值
* @param {*} receiver 最初被調(diào)用的對(duì)象。通常是proxy本身,但handler的set方法也有可能在原型鏈上或以其他方式被間接地調(diào)用(因此不一定是proxy本身)
*/
set: function(target, property, value, receiver)
{
if(property === 'age' && typeof value !== "number") {
console.log('傳入數(shù)據(jù)格式不真確')
} else {
console.log(arguments)
return Reflect.set(...arguments)
}
}
}
let p = new Proxy(people, handler)
p.age = 4324
console.log(p.age)
問題1:對(duì)于對(duì)象檢測(cè)只能檢測(cè)一層
問題2:監(jiān)聽數(shù)組,使用數(shù)組方法觸發(fā)2次