ES6 系列之 defineProperty 與 proxy

冴羽發表於2018-11-09

前言

我們或多或少都聽過“資料繫結”這個詞,“資料繫結”的關鍵在於監聽資料的變化,可是對於這樣一個物件:var obj = {value: 1
}
,我們該怎麼知道 obj 發生了改變呢?

definePropety

ES5 提供了 Object.defineProperty 方法,該方法可以在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,並返回這個物件。

語法

Object.defineProperty(obj, prop, descriptor)

引數

obj: 要在其上定義屬性的物件。prop:  要定義或修改的屬性的名稱。descriptor: 將被定義或修改的屬性的描述符。複製程式碼

舉個例子:

var obj = {
};
Object.defineProperty(obj, "num", {
value : 1, writable : true, enumerable : true, configurable : true
});
// 物件 obj 擁有屬性 num,值為 1複製程式碼

雖然我們可以直接新增屬性和值,但是使用這種方式,我們能進行更多的配置。

函式的第三個引數 descriptor 所表示的屬性描述符有兩種形式:資料描述符和存取描述符

兩者均具有以下兩種鍵值

configurable

當且僅當該屬性的 configurable 為 true 時,該屬性描述符才能夠被改變,也能夠被刪除。預設為 false。複製程式碼

enumerable

當且僅當該屬性的 enumerable 為 true 時,該屬性才能夠出現在物件的列舉屬性中。預設為 false。複製程式碼

資料描述符同時具有以下可選鍵值

value

該屬性對應的值。可以是任何有效的 JavaScript 值(數值,物件,函式等)。預設為 undefined。複製程式碼

writable

當且僅當該屬性的 writable 為 true 時,該屬性才能被賦值運算子改變。預設為 false。複製程式碼

存取描述符同時具有以下可選鍵值

get

一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。該方法返回值被用作屬性值。預設為 undefined。複製程式碼

set

一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。該方法將接受唯一引數,並將該引數的新值分配給該屬性。預設為 undefined。複製程式碼

值得注意的是:

**屬性描述符必須是資料描述符或者存取描述符兩種形式之一,不能同時是兩者。**這就意味著你可以:

Object.defineProperty({
}, "num", {
value: 1, writable: true, enumerable: true, configurable: true
});
複製程式碼

也可以:

var value = 1;
Object.defineProperty({
}, "num", {
get : function(){
return value;

}, set : function(newValue){
value = newValue;

}, enumerable : true, configurable : true
});
複製程式碼

但是不可以:

// 報錯Object.defineProperty({
}, "num", {
value: 1, get: function() {
return 1;

}
});
複製程式碼

此外,所有的屬性描述符都是非必須的,但是 descriptor 這個欄位是必須的,如果不進行任何配置,你可以這樣:

var obj = Object.defineProperty({
}, "num", {
});
console.log(obj.num);
// undefined複製程式碼

Setters 和 Getters

之所以講到 defineProperty,是因為我們要使用存取描述符中的 get 和 set,這兩個方法又被稱為 getter 和 setter。由 getter 和 setter 定義的屬性稱做”存取器屬性“。

當程式查詢存取器屬性的值時,JavaScript 呼叫 getter方法。這個方法的返回值就是屬性存取表示式的值。當程式設定一個存取器屬性的值時,JavaScript 呼叫 setter 方法,將賦值表示式右側的值當做引數傳入 setter。從某種意義上講,這個方法負責“設定”屬性值。可以忽略 setter 方法的返回值。

舉個例子:

var obj = {
}, value = null;
Object.defineProperty(obj, "num", {
get: function(){
console.log('執行了 get 操作') return value;

}, set: function(newValue) {
console.log('執行了 set 操作') value = newValue;

}
})obj.num = 1 // 執行了 set 操作console.log(obj.num);
// 執行了 get 操作 // 1複製程式碼

這不就是我們要的監控資料改變的方法嗎?我們再來封裝一下:

function Archiver() { 
var value = null;
// archive n. 檔案 var archive = [];
Object.defineProperty(this, 'num', {
get: function() {
console.log('執行了 get 操作') return value;

}, set: function(value) {
console.log('執行了 set 操作') value = value;
archive.push({
val: value
});

}
});
this.getArchive = function() {
return archive;

};

}var arc = new Archiver();
arc.num;
// 執行了 get 操作arc.num = 11;
// 執行了 set 操作arc.num = 13;
// 執行了 set 操作console.log(arc.getArchive());
// [{
val: 11
}, {
val: 13
}]
複製程式碼

watch API

既然可以監控資料的改變,那我可以這樣設想,即當資料改變的時候,自動進行渲染工作。舉個例子:

HTML 中有個 span 標籤和 button 標籤

<
span id="container">
1<
/span>
<
button id="button">
點選加 1<
/button>
複製程式碼

當點選按鈕的時候,span 標籤裡的值加 1。

傳統的做法是:

document.getElementById('button').addEventListener("click", function(){ 
var container = document.getElementById("container");
container.innerHTML = Number(container.innerHTML) + 1;

});
複製程式碼

如果使用了 defineProperty:

var obj = { 
value: 1
}// 儲存 obj.value 的值var value = 1;
Object.defineProperty(obj, "value", {
get: function() {
return value;

}, set: function(newValue) {
value = newValue;
document.getElementById('container').innerHTML = newValue;

}
});
document.getElementById('button').addEventListener("click", function() {
obj.value += 1;

});
複製程式碼

程式碼看似增多了,但是當我們需要改變 span 標籤裡的值的時候,直接修改 obj.value 的值就可以了。

然而,現在的寫法,我們還需要單獨宣告一個變數儲存 obj.value 的值,因為如果你在 set 中直接 obj.value = newValue 就會陷入無限的迴圈中。此外,我們可能需要監控很多屬性值的改變,要是一個一個寫,也很累吶,所以我們簡單寫個 watch 函式。使用效果如下:

var obj = { 
value: 1
}watch(obj, "value", function(newvalue){
document.getElementById('container').innerHTML = newvalue;

})document.getElementById('button').addEventListener("click", function(){
obj.value += 1
});
複製程式碼

我們來寫下這個 watch 函式:

(function(){ 
var root = this;
function watch(obj, name, func){
var value = obj[name];
Object.defineProperty(obj, name, {
get: function() {
return value;

}, set: function(newValue) {
value = newValue;
func(value)
}
});
if (value) obj[name] = value
} this.watch = watch;

})()複製程式碼

現在我們已經可以監控物件屬性值的改變,並且可以根據屬性值的改變,新增回撥函式,棒棒噠~

proxy

使用 defineProperty 只能重定義屬性的讀取(get)和設定(set)行為,到了 ES6,提供了 Proxy,可以重定義更多的行為,比如 in、delete、函式呼叫等更多行為。

Proxy 這個詞的原意是代理,用在這裡表示由它來“代理”某些操作,ES6 原生提供 Proxy 建構函式,用來生成 Proxy 例項。我們來看看它的語法:

var proxy = new Proxy(target, handler);
複製程式碼

proxy 物件的所有用法,都是上面這種形式,不同的只是handler引數的寫法。其中,new Proxy()表示生成一個Proxy例項,target參數列示所要攔截的目標物件,handler引數也是一個物件,用來定製攔截行為。

var proxy = new Proxy({
}, {
get: function(obj, prop) {
console.log('設定 get 操作') return obj[prop];

}, set: function(obj, prop, value) {
console.log('設定 set 操作') obj[prop] = value;

}
});
proxy.time = 35;
// 設定 set 操作console.log(proxy.time);
// 設定 get 操作 // 35複製程式碼

除了 get 和 set 之外,proxy 可以攔截多達 13 種操作,比如 has(target, propKey),可以攔截 propKey in proxy 的操作,返回一個布林值。

// 使用 has 方法隱藏某些屬性,不被 in 運算子發現var handler = { 
has (target, key) {
if (key[0] === '_') {
return false;

} return key in target;

}
};
var target = {
_prop: 'foo', prop: 'foo'
};
var proxy = new Proxy(target, handler);
console.log('_prop' in proxy);
// false複製程式碼

又比如說 apply 方法攔截函式的呼叫、call 和 apply 操作。

apply 方法可以接受三個引數,分別是目標物件、目標物件的上下文物件(this)和目標物件的引數陣列,不過這裡我們簡單演示一下:

var target = function () { 
return 'I am the target';

};
var handler = {
apply: function () {
return 'I am the proxy';

}
};
var p = new Proxy(target, handler);
p();
// "I am the proxy"複製程式碼

又比如說 ownKeys 方法可以攔截物件自身屬性的讀取操作。具體來說,攔截以下操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()

下面的例子是攔截第一個字元為下劃線的屬性名,不讓它被 for of 遍歷到。

let target = { 
_bar: 'foo', _prop: 'bar', prop: 'baz'
};
let handler = {
ownKeys (target) {
return Reflect.ownKeys(target).filter(key =>
key[0] !== '_');

}
};
let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
console.log(target[key]);

}// "baz"複製程式碼

更多的攔截行為可以檢視阮一峰老師的 《ECMAScript 6 入門》

值得注意的是,proxy 的最大問題在於瀏覽器支援度不夠,而且很多效果無法使用 poilyfill 來彌補。

watch API 優化

我們使用 proxy 再來寫一下 watch 函式。使用效果如下:

(function() { 
var root = this;
function watch(target, func) {
var proxy = new Proxy(target, {
get: function(target, prop) {
return target[prop];

}, set: function(target, prop, value) {
target[prop] = value;
func(prop, value);

}
});
return proxy;

} this.watch = watch;

})()var obj = {
value: 1
}var newObj = watch(obj, function(key, newvalue) {
if (key == 'value') document.getElementById('container').innerHTML = newvalue;

})document.getElementById('button').addEventListener("click", function() {
newObj.value += 1
});
複製程式碼

我們也可以發現,使用 defineProperty 和 proxy 的區別,當使用 defineProperty,我們修改原來的 obj 物件就可以觸發攔截,而使用 proxy,就必須修改代理物件,即 Proxy 的例項才可以觸發攔截。

ES6 系列

ES6 系列目錄地址:github.com/mqyqingfeng…

ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級作用域、標籤模板、箭頭函式、Symbol、Set、Map 以及 Promise 的模擬實現、模組載入方案、非同步處理等內容。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

來源:https://juejin.im/post/5be4f7cfe51d453339084530

相關文章