新聞中心
Object.defineProperty()
作用:在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性,并返回這個(gè)對(duì)象。

成都創(chuàng)新互聯(lián)公司專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于成都網(wǎng)站制作、網(wǎng)站建設(shè)、開(kāi)江網(wǎng)絡(luò)推廣、成都微信小程序、開(kāi)江網(wǎng)絡(luò)營(yíng)銷、開(kāi)江企業(yè)策劃、開(kāi)江品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運(yùn)營(yíng)等,從售前售中售后,我們都將竭誠(chéng)為您服務(wù),您的肯定,是我們最大的嘉獎(jiǎng);成都創(chuàng)新互聯(lián)公司為所有大學(xué)生創(chuàng)業(yè)者提供開(kāi)江建站搭建服務(wù),24小時(shí)服務(wù)熱線:13518219792,官方網(wǎng)址:www.cdcxhl.com
1. 基本使用
語(yǔ)法:??Object.defineProperty(obj, prop, descriptor)??
參數(shù):
- 要添加屬性的對(duì)象
- 要定義或修改的屬性的名稱或 [?
?Symbol??] - 要定義或修改的屬性描述符
看一個(gè)簡(jiǎn)單的例子
let person = {}
let personName = 'lihua'
//在person對(duì)象上添加屬性namep,值為personName
Object.defineProperty(person, 'namep', {
//但是默認(rèn)是不可枚舉的(for in打印打印不出來(lái)),可:enumerable: true
//默認(rèn)不可以修改,可:wirtable:true
//默認(rèn)不可以刪除,可:configurable:true
get: function () {
console.log('觸發(fā)了get方法')
return personName
},
set: function (val) {
console.log('觸發(fā)了set方法')
personName = val
}
})
//當(dāng)讀取person對(duì)象的namp屬性時(shí),觸發(fā)get方法
console.log(person.namep)
//當(dāng)修改personName時(shí),重新訪問(wèn)person.namep發(fā)現(xiàn)修改成功
personName = 'liming'
console.log(person.namep)
// 對(duì)person.namep進(jìn)行修改,觸發(fā)set方法
person.namep = 'huahua'
console.log(person.namep)
\
通過(guò)這種方法,我們成功監(jiān)聽(tīng)了person上的name屬性的變化。
2.監(jiān)聽(tīng)對(duì)象上的多個(gè)屬性
上面的使用中,我們只監(jiān)聽(tīng)了一個(gè)屬性的變化,但是在實(shí)際情況中,我們通常需要一次監(jiān)聽(tīng)多個(gè)屬性的變化。
這時(shí)我們需要配合Object.keys(obj)進(jìn)行遍歷。這個(gè)方法可以返回obj對(duì)象身上的所有可枚舉屬性組成的字符數(shù)組。(其實(shí)用for in遍歷也可以)
下面是該API一個(gè)簡(jiǎn)單的使用效果:
var obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
利用這個(gè)API,我們就可以遍歷劫持對(duì)象的所有屬性 但是如果只是上面的思路與該API的簡(jiǎn)單結(jié)合,我們就會(huì)發(fā)現(xiàn)并達(dá)不到效果,下面是我寫(xiě)的一個(gè)錯(cuò)誤的版本:
Object.keys(person).forEach(function (key) {
Object.defineProperty(person, key, {
enumerable: true,
configurable: true,
// 默認(rèn)會(huì)傳入this
get() {
return person[key]
},
set(val) {
console.log(`對(duì)person中的${key}屬性進(jìn)行了修改`)
person[key] = val
// 修改之后可以執(zhí)行渲染操作
}
})
})
console.log(person.age)
看起來(lái)感覺(jué)上面的代碼沒(méi)有什么錯(cuò)誤,但是試著運(yùn)行一下吧~你會(huì)和我一樣棧溢出。這是為什么呢?讓我們聚焦在get方法里,我們?cè)谠L問(wèn)person身上的屬性時(shí),就會(huì)觸發(fā)get方法,返回person[key],但是訪問(wèn)person[key]也會(huì)觸發(fā)get方法,導(dǎo)致遞歸調(diào)用,最終棧溢出。
這也引出了我們下面的方法,我們需要設(shè)置一個(gè)中轉(zhuǎn)Obsever,來(lái)讓get中return的值并不是直接訪問(wèn)obj[key]。
let person = {
name: '',
age: 0
}
// 實(shí)現(xiàn)一個(gè)響應(yīng)式函數(shù)
function defineProperty(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`訪問(wèn)了${key}屬性`)
return val
},
set(newVal) {
console.log(`${key}屬性被修改為${newVal}了`)
val = newVal
}
})
}
// 實(shí)現(xiàn)一個(gè)遍歷函數(shù)Observer
function Observer(obj) {
Object.keys(obj).forEach((key) => {
defineProperty(obj, key, obj[key])
})
}
Observer(person)
console.log(person.age)
person.age = 18
console.log(person.age)
3.深度監(jiān)聽(tīng)一個(gè)對(duì)象
那么我們?nèi)绾谓鉀Q對(duì)象中嵌套一個(gè)對(duì)對(duì)象的情況呢?其實(shí)在上述代碼的基礎(chǔ)上,加上一個(gè)遞歸,就可以輕松實(shí)現(xiàn)啦~
我們可以觀察到,其實(shí)Obsever就是我們想要實(shí)現(xiàn)的監(jiān)聽(tīng)函數(shù),我們預(yù)期的目標(biāo)是:只要把對(duì)象傳入其中,就可以實(shí)現(xiàn)對(duì)這個(gè)對(duì)象的屬性監(jiān)視,即使該對(duì)象的屬性也是一個(gè)對(duì)象。
我們?cè)赿efineProperty()函數(shù)中,添加一個(gè)遞歸的情況:
function defineProperty(obj, key, val) {
//如果某對(duì)象的屬性也是一個(gè)對(duì)象,遞歸進(jìn)入該對(duì)象,進(jìn)行監(jiān)聽(tīng)
if(typeof val === 'object'){
observer(val)
}
Object.defineProperty(obj, key, {
get() {
console.log(`訪問(wèn)了${key}屬性`)
return val
},
set(newVal) {
console.log(`${key}屬性被修改為${newVal}了`)
val = newVal
}
})
}
當(dāng)然啦,我們也要在observer里面加一個(gè)遞歸停止的條件:
function Observer(obj) {
//如果傳入的不是一個(gè)對(duì)象,return
if (typeof obj !== "object" || obj === null) {
return
}
// for (key in obj) {
Object.keys(obj).forEach((key) => {
defineProperty(obj, key, obj[key])
})
// }
}
其實(shí)到這里就差不多解決了,但是還有一個(gè)小問(wèn)題,如果對(duì)某屬性進(jìn)行修改時(shí),如果原本的屬性值是一個(gè)字符串,但是我們重新賦值了一個(gè)對(duì)象,我們要如何監(jiān)聽(tīng)新添加的對(duì)象的所有屬性呢?其實(shí)也很簡(jiǎn)單,只需要修改set函數(shù):
set(newVal) {
// 如果newVal是一個(gè)對(duì)象,遞歸進(jìn)入該對(duì)象進(jìn)行監(jiān)聽(tīng)
if(typeof val === 'object'){
observer(key)
}
console.log(`${key}屬性被修改為${newVal}了`)
val = newVal
}
到這里我們就完成啦~
4.監(jiān)聽(tīng)數(shù)組
那么如果對(duì)象的屬性是一個(gè)數(shù)組呢?我們要如何實(shí)現(xiàn)監(jiān)聽(tīng)?請(qǐng)看下面一段代碼:
let arr = [1, 2, 3]
let obj = {}
//把a(bǔ)rr作為obj的屬性監(jiān)聽(tīng)
Object.defineProperty(obj, 'arr', {
get() {
console.log('get arr')
return arr
},
set(newVal) {
console.log('set', newVal)
arr = newVal
}
})
console.log(obj.arr)//輸出get arr [1,2,3] 正常
obj.arr = [1, 2, 3, 4] //輸出set [1,2,3,4] 正常
obj.arr.push(3) //輸出get arr 不正常,監(jiān)聽(tīng)不到push
我們發(fā)現(xiàn),通過(guò)??push??方法給數(shù)組增加的元素,set方法是監(jiān)聽(tīng)不到的。
事實(shí)上,通過(guò)索引訪問(wèn)或者修改數(shù)組中已經(jīng)存在的元素,是可以出發(fā)get和set的,但是對(duì)于通過(guò)push、unshift增加的元素,會(huì)增加一個(gè)索引,這種情況需要手動(dòng)初始化,新增加的元素才能被監(jiān)聽(tīng)到。另外,通過(guò) pop 或 shift 刪除元素,會(huì)刪除并更新索引,也會(huì)觸發(fā) setter 和 getter 方法。
在Vue2.x中,通過(guò)重寫(xiě)Array原型上的方法解決了這個(gè)問(wèn)題,此處就不展開(kāi)說(shuō)了,有興趣的uu可以再去了解下~
Proxy
是不是感覺(jué)有點(diǎn)復(fù)雜?事實(shí)上,在上面的講述中,我們還有問(wèn)題沒(méi)有解決:那就是當(dāng)我們要給對(duì)象新增加一個(gè)屬性時(shí),也需要手動(dòng)去監(jiān)聽(tīng)這個(gè)新增屬性。
也正是因?yàn)檫@個(gè)原因,使用vue給 data 中的數(shù)組或?qū)ο笮略鰧傩詴r(shí),需要使用 vm.$set 才能保證新增的屬性也是響應(yīng)式的。
可以看到,通過(guò)Object.definePorperty()進(jìn)行數(shù)據(jù)監(jiān)聽(tīng)是比較麻煩的,需要大量的手動(dòng)處理。這也是為什么在Vue3.0中尤雨溪轉(zhuǎn)而采用Proxy。接下來(lái)讓我們一起看一下Proxy是怎么解決這些問(wèn)題的吧~
1.基本使用
語(yǔ)法:??const p = new Proxy(target, handler)??
參數(shù):
- target:要使用 ?
?Proxy?? 包裝的目標(biāo)對(duì)象(可以是任何類型的對(duì)象,包括原生數(shù)組,函數(shù),甚至另一個(gè)代理) - handler:一個(gè)通常以函數(shù)作為屬性的對(duì)象,各屬性中的函數(shù)分別定義了在執(zhí)行各種操作時(shí)代理 ?
?p?? 的行為。
通過(guò)Proxy,我們可以對(duì)??設(shè)置代理的對(duì)象??上的一些操作進(jìn)行攔截,外界對(duì)這個(gè)對(duì)象的各種操作,都要先通過(guò)這層攔截。(和defineProperty差不多)
先看一個(gè)簡(jiǎn)單例子
//定義一個(gè)需要代理的對(duì)象
let person = {
age: 0,
school: '西電'
}
//定義handler對(duì)象
let hander = {
get(obj, key) {
// 如果對(duì)象里有這個(gè)屬性,就返回屬性值,如果沒(méi)有,就返回默認(rèn)值66
return key in obj ? obj[key] : 66
},
set(obj, key, val) {
obj[key] = val
return true
}
}
//把handler對(duì)象傳入Proxy
let proxyObj = new Proxy(person, hander)
// 測(cè)試get能否攔截成功
console.log(proxyObj.age)//輸出0
console.log(proxyObj.school)//輸出西電
console.log(proxyObj.name)//輸出默認(rèn)值66
// 測(cè)試set能否攔截成功
proxyObj.age = 18
console.log(proxyObj.age)//輸出18 修改成功
可以看出,Proxy代理的是整個(gè)對(duì)象,而不是對(duì)象的某個(gè)特定屬性,不需要我們通過(guò)遍歷來(lái)逐個(gè)進(jìn)行數(shù)據(jù)綁定。
值得注意的是:之前我們?cè)谑褂肙bject.defineProperty()給對(duì)象添加一個(gè)屬性之后,我們對(duì)對(duì)象屬性的讀寫(xiě)操作仍然在對(duì)象本身。
但是一旦使用Proxy,如果想要讀寫(xiě)操作生效,我們就要對(duì)Proxy的實(shí)例對(duì)象
proxyObj進(jìn)行操作。另外,MDN上明確指出set()方法應(yīng)該返回一個(gè)布爾值,否則會(huì)報(bào)錯(cuò)?
?TypeError??。
2.輕松解決Object.defineProperty中遇到的問(wèn)題
在上面使用Object.defineProperty的時(shí)候,我們遇到的問(wèn)題有:
1.一次只能對(duì)一個(gè)屬性進(jìn)行監(jiān)聽(tīng),需要遍歷來(lái)對(duì)所有屬性監(jiān)聽(tīng)。這個(gè)我們?cè)谏厦嬉呀?jīng)解決了。
2. 在遇到一個(gè)對(duì)象的屬性還是一個(gè)對(duì)象的情況下,需要遞歸監(jiān)聽(tīng)。
3. 對(duì)于對(duì)象的新增屬性,需要手動(dòng)監(jiān)聽(tīng)
4. 對(duì)于數(shù)組通過(guò)push、unshift方法增加的元素,也無(wú)法監(jiān)聽(tīng)
這些問(wèn)題在Proxy中都輕松得到了解決,讓我們看看以下代碼。
檢驗(yàn)第二個(gè)問(wèn)題
在上面代碼的基礎(chǔ)上,我們讓對(duì)象的結(jié)構(gòu)變得更復(fù)雜一些。
let person = {
age: 0,
school: '西電',
children: {
name: '小明'
}
}
let hander = {
get(obj, key) {
return key in obj ? obj[key] : 66
}, set(obj, key, val) {
obj[key] = val
return true
}
}
let proxyObj = new Proxy(person, hander)
// 測(cè)試get
console.log(proxyObj.children.name)//輸出:小明
console.log(proxyObj.children.height)//輸出:undefined
// 測(cè)試set
proxyObj.children.name = '菜菜'
console.log(proxyObj.children.name)//輸出: 菜菜
可以看到成功監(jiān)聽(tīng)到了children對(duì)象身上的name屬性(至于為什么children.height是undefined,可以再討論一下)
檢驗(yàn)第三個(gè)問(wèn)題
這個(gè)其實(shí)在基本使用里面已經(jīng)提到了,訪問(wèn)的proxyObj.name就是原本對(duì)象上不存在的屬性,但是我們?cè)L問(wèn)它的時(shí)候,仍然們可以被get攔截到。
檢驗(yàn)第四個(gè)問(wèn)題
let subject = ['高數(shù)']
let handler = {
get(obj, key) {
return key in obj ? obj[key] : '沒(méi)有這門學(xué)科'
}, set(obj, key, val) {
obj[key] = val
//set方法成功時(shí)應(yīng)該返回true,否則會(huì)報(bào)錯(cuò)
return true
}
}
let proxyObj = new Proxy(subject, handler)
// 檢驗(yàn)get和set
console.log(proxyObj)//輸出 [ '高數(shù)' ]
console.log(proxyObj[1])//輸出 沒(méi)有這門學(xué)科
proxyObj[0] = '大學(xué)物理'
console.log(proxyObj)//輸出 [ '大學(xué)物理' ]
// // 檢驗(yàn)push增加的元素能否被監(jiān)聽(tīng)
proxyObj.push('線性代數(shù)')
console.log(proxyObj)//輸出 [ '大學(xué)物理', '線性代數(shù)' ]
至此,我們之前的問(wèn)題完美解決。
3.Proxy支持13種攔截操作
除了get和set來(lái)攔截讀取和賦值操作之外,Proxy還支持對(duì)其他多種行為的攔截。下面是一個(gè)簡(jiǎn)單介紹,想要深入了解的可以去MDN上看看。
- get(target, propKey, receiver):攔截對(duì)象屬性的讀取,比如proxy.foo和proxy['foo']。
- set(target, propKey, value, receiver):攔截對(duì)象屬性的設(shè)置,比如proxy.foo = v或proxy['foo'] = v,返回一個(gè)布爾值。
- has(target, propKey):攔截propKey in proxy的操作,返回一個(gè)布爾值。
- deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個(gè)布爾值。
- ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循環(huán),返回一個(gè)數(shù)組。該方法返回目標(biāo)對(duì)象所有自身的屬性的屬性名,而Object.keys()的返回結(jié)果僅包括目標(biāo)對(duì)象自身的可遍歷屬性。
- getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對(duì)象。
- defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個(gè)布爾值。
- preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個(gè)布爾值。
- getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個(gè)對(duì)象。
- isExtensible(target):攔截Object.isExtensible(proxy),返回一個(gè)布爾值。
- setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個(gè)布爾值。如果目標(biāo)對(duì)象是函數(shù),那么還有兩種額外操作可以攔截。
- apply(target, object, args):攔截 Proxy 實(shí)例作為函數(shù)調(diào)用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
- construct(target, args):攔截 Proxy 實(shí)例作為構(gòu)造函數(shù)調(diào)用的操作,比如new proxy(...args)。
4. Proxy中有關(guān)this的問(wèn)題
雖然Proxy完成了對(duì)目標(biāo)對(duì)象的代理,但是它不是??透明代理??,也就是說(shuō):即使handler為空對(duì)象(即不做任何代理),他所代理的對(duì)象中的this指向也不是該對(duì)象,而是proxyObj對(duì)象。讓我們來(lái)看一個(gè)例子:
let target = {
m() {
// 檢查this的指向是不是proxyObkj
console.log(this === proxyObj)
}
}
let handler = {}
let proxyObj = new Proxy(target, handler)
proxyObj.m()//輸出:true
target.m()//輸出:false
可以看到,被代理的對(duì)象target內(nèi)部的this指向了proxyObj。這種指向有時(shí)候就會(huì)導(dǎo)致問(wèn)題出現(xiàn),我們來(lái)看看下面一個(gè)例子:
const _name = new WeakMap();
class Person {
//把person的name存儲(chǔ)到_name的name屬性上
constructor(name) {
_name.set(this, name);
}
//獲取person的name屬性時(shí),返回_name的name
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxyObj = new Proxy(jane, {});
proxyObj.name // undefined
在上面的例子中,由于jane對(duì)象的name屬性的獲取依靠this的指向,而this又指向proxyObj,所以導(dǎo)致了無(wú)法正常代理。
除此之外,有的js內(nèi)置對(duì)象的內(nèi)部屬性,也依靠正確的this才能獲取,所以Proxy 也無(wú)法代理這些原生對(duì)象的屬性。請(qǐng)看下面一個(gè)例子:
const target = new Date();
const handler = {};
const proxyObj = new Proxy(target, handler);
proxyObj.getDate();
// TypeError: this is not a Date object.
可以看到,通過(guò)proxy代理訪問(wèn)Date對(duì)象中的getDate方法時(shí)拋出了一個(gè)錯(cuò)誤,這是因?yàn)間etDate方法只能在Date對(duì)象實(shí)例上面拿到,如果this不是Date對(duì)象實(shí)例就會(huì)報(bào)錯(cuò)。那么我們要如何解決這個(gè)問(wèn)題呢?只要手動(dòng)把this綁定在Date對(duì)象實(shí)例上即可,請(qǐng)看下面一個(gè)例子:
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target);
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
完結(jié)撒花
至此,我的總結(jié)就結(jié)束啦~ 文章不是很全面,還有很多地方?jīng)]有講到,比如:
- Proxy常常搭配?
?Reflect??使用 - 我們常?
?Object.create()??方法把Proxy實(shí)例對(duì)象添加到Object的原型對(duì)象上,這樣我們就可以直接Object.proxyObj了 - 有興趣的uu可以在Proxy的get和set里加上輸出試試,你會(huì)發(fā)現(xiàn)在我們調(diào)用push方法時(shí),?
?get和set會(huì)分別輸出兩次??,這是為什么呢?
學(xué)無(wú)止境,讓我們一起努力叭~
參考文章:
1.Proxy 與Object.defineProperty介紹與對(duì)比
2.MDN Proxy
名稱欄目:一文搞懂Vue3.0為什么采用Proxy
文章源于:http://www.fisionsoft.com.cn/article/coooiod.html


咨詢
建站咨詢
