雖然個人主技術棧是 React 的,可是每當面試的時候,面試官幾乎都會問你說一下 React 和 Vue 的區別,在說道雙向數據綁定的時候,面試官會下意識的問一句,你說一下 Vue 的雙向數據綁定的原理,這個時候 Object.defineProperty 就出場了,可是在 Vue3.0 中,Proxy 取代了 Object.defineProperty,成爲雙向綁定的底層原理,這個時候 Proxy 就顯得尤其重要。前端
本篇文章先以 Object.defineProperty 做爲引入,以後講解 Proxy,最後比較二者之間的優劣。git
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。es6
該方法接受三個參數,第一個參數是 obj:要定義屬性的對象,第二個參數是 prop:要定義或修改的屬性的名稱或 Symbol,第三個參數是 descriptor:要定義或修改的屬性描述符。github
const obj = {};
Object.defineProperty(obj, "property", {
value: 18,
});
console.log(obj.property); // 18
複製代碼
雖然咱們能夠直接添加屬性和值,可是使用這種方式,咱們能進行更多的配置。面試
函數的第三個參數 descriptor 所表示的屬性描述符有兩種形式:數據描述符和存取描述符。數據描述符是一個具備值的屬性,該值能夠是可寫的,也能夠是不可寫的。存取描述符是由 getter 函數和 setter 函數所描述的屬性。一個描述符只能是這二者其中之一;不能同時是二者。編程
這兩種同時擁有下列兩種鍵值:數組
const obj = { property: 24 };
Object.defineProperty(obj, "property", {
configurable: true,
});
delete obj["property"]; // true
obj; // {}
// 改變狀態
const obj = { property: 24 };
Object.defineProperty(obj, "property", {
configurable: false,
});
delete obj["property"]; // false
obj; // {'property': 24}
複製代碼
const obj = {
property1: 24,
property2: 34,
property3: 54,
};
Object.defineProperty(obj, "property1", {
enumerable: true,
});
for (i in obj) {
console.log(i);
}
// property1
// property2
// property3
// 改狀態
Object.defineProperty(obj, "property1", {
enumerable: false,
});
for (i in obj) {
console.log(i);
}
// property2
// property3
複製代碼
數據描述符還具備如下可選鍵值:markdown
const obj = {};
Object.defineProperty(obj, "property1", {
value: 18,
});
obj; // {'property1': 18}
複製代碼
const obj = {};
Object.defineProperty(obj, "property1", {
value: 18,
writable: false,
});
obj.property1 = 24;
obj; // {'property1': 18}
// 改變狀態
const obj = {};
Object.defineProperty(obj, "property1", {
value: 18,
writable: true,
});
obj.property1 = 24;
obj; // {'property1': 24}
複製代碼
存取描述符還具備如下可選鍵值:app
const obj = {};
Object.defineProperty(obj, "property1", {
get(value) {
return value;
},
set(newValue) {
value = newValue;
},
});
複製代碼
Object.defineProperty 只能重定義獲取和設置的行爲,而 Proxy 至關於一個升級,它重定義了更多的行爲,接下來咱們對其進行深刻講解。函數
首先,Proxy 是一個構造函數,能夠經過 new 來建立它的實例,其接受兩個參數,一個是 target:要使用 Proxy 包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。另一個是 handler:一個一般以函數做爲屬性的對象,各屬性中的函數分別定義了在執行各類操做時代理實例的行爲。
let p = new Proxy(target, handler);
複製代碼
handler 中的全部方法都是可選的,若是沒有定義哪一個方法,那就保留原對象的默認行爲。
用於攔截對象的讀取操做。該方法接收三個參數,target:目標對象,property:被獲取的屬性名,receiver:Proxy 或者繼承 Proxy 的對象。
let person = {
name: "Jack",
};
let p = new Proxy(person, {
get(target, property) {
if (property in target) {
return target[property];
} else {
throw Error("不存在該屬性");
}
},
});
p.name; // Jack
p.age; // Uncaught Error: 不存在該屬性
複製代碼
查看第三個屬性
let person = {
name: "Jack",
};
let p = new Proxy(person, {
get(target, property, receiver) {
return receiver;
},
});
p.name === p; // true
複製代碼
上面代碼中,p 對象的 name 屬性是由 p 對象提供的,因此 receiver 指向 proxy 對象。
用於設置屬性值操做的捕獲器。該方法接收四個參數,target:目標對象,property:將被設置的屬性名或 Symbol,value:新屬性值,receiver:最初被調用的對象。一般是 proxy 自己,但 handler 的 set 方法也有可能在原型鏈上,或以其餘方式被間接地調用(所以不必定是 proxy 自己)。
let person = {};
let p = new Proxy(person, {
set(target, property, value, receiver) {
target[property] = value;
},
});
p.name = 2;
複製代碼
當給 Proxy 的實例添加屬性的時候,就會調用 set()方法。
下面咱們來看一下 set()方法的第四個參數,通常狀況下都是指向 proxy 自己。
let person = {};
let p = new Proxy(person, {
set(target, property, value, receiver) {
target[property] = receiver;
},
});
p.name = 2;
p.name === p; // true
複製代碼
可是也有其餘狀況,咱們借用阮一峯:ECMAScript 6 入門-Proxy中談到的例子:
const handler = {
set: function (obj, prop, value, receiver) {
obj[prop] = receiver;
},
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);
myObj.foo = "bar";
myObj.foo === myObj; // true
複製代碼
上面代碼中,設置 myObj.foo 屬性的值時,myObj 並無 foo 屬性,所以引擎會到 myObj 的原型鏈去找 foo 屬性。myObj 的原型對象 proxy 是一個 Proxy 實例,設置它的 foo 屬性會觸發 set 方法。這時,第四個參數 receiver 就指向原始賦值行爲所在的對象 myObj。
值得注意一點的是,當該對象不可配置不可編寫的時候,那麼 set()方法將不起做用。
const obj = {};
Object.defineProperty(obj, "foo", {
value: 18,
writable: false,
});
let p = new Proxy(obj, {
set(target, property, value, receiver) {
target[property] = value;
},
});
p.foo = 28;
p.foo; // 18
複製代碼
用於攔截函數的調用。該方法接受三個參數,target:目標對象(函數)。thisArg:被調用時的上下文對象。argumentsList:被調用時的參數數組。
當 Prxoy 的實例當作函數使用時,就會執行該方法。
let p = new Proxy(function () {}, {
apply(target, thisArg, argumentsList) {
console.log("Hello Word");
},
});
p(); // Hello Word
複製代碼
是針對 in 操做符的代理方法。該方法接受兩個參數,target:目標對象。prop:須要檢查是否存在的屬性。
let p = new Proxy(
{},
{
has(target, prop) {
console.log(target, prop); // {} 'a'
},
}
);
console.log("a" in p); // false
複製代碼
has()方法只對 in 運算符有效,對 for...in...運算沒有實際做用。
let p = new Proxy(
{ value: 18 },
{
has(target, prop) {
if (prop === "value" && target[prop] < 20) {
console.log("數值小於20");
return false;
}
return prop in target;
},
}
);
"value" in p; // 數值小於20 false
for (let a in p) {
console.log(p[a]); // 18
}
複製代碼
從上述例子能夠看出,has 方法攔截只對 in 運算符有效,對 for...in...來講沒有攔截效果。
用於攔截 new 操做符. 爲了使 new 操做符在生成的 Proxy 對象上生效,用於初始化代理的目標對象自身必須具備[[Construct]]內部方法(即 new target 必須是有效的)。該方法接收三個參數,target:目標對象。argumentsList:constructor 的參數列表。newTarget:最初被調用的構造函數。
let p = new Proxy(function () {}, {
construct(target, argumentsList, newTarget) {
return { value: argumentsList[0] };
},
})(new p(1)).value; // 1
複製代碼
construct()方法必須返回一個對象,否則不報錯。
let p = new Proxy(function () {}, {
construct(target, argumentsList, newTarget) {
console.log("Hello Word");
},
});
new p(); // Uncaught TypeError: 'construct' on proxy: trap returned non-object ('undefined')
複製代碼
用於攔截對對象屬性的 delete 操做。該方法接受兩個參數。target:目標對象。property:待刪除的屬性名。
let p = new Proxy(
{},
{
deleteProperty(target, property) {
console.log("called: " + property);
return true;
},
}
);
delete p.a; // "called: a"
複製代碼
若是這個方法拋出錯誤或者返回 false,當前屬性就沒法被 delete 命令刪除。注意,目標對象自身的不可配置(configurable)的屬性,不能被 deleteProperty 方法刪除,不然報錯。
用於攔截對對象的 Object.defineProperty() 操做。該方法接受三個參數,target:目標對象。property:待檢索其描述的屬性名。descriptor:待定義或修改的屬性的描述符。
let p = new Proxy(
{},
{
defineProperty: function (target, prop, descriptor) {
console.log("called: " + prop);
return true;
},
}
);
let desc = { configurable: true, enumerable: true, value: 10 };
Object.defineProperty(p, "a", desc); // "called: a"
複製代碼
若是 defineProperty()方法內部沒有任何操做,只返回 false,致使添加新屬性老是無效。注意,這裏的 false 只是用來提示操做失敗,自己並不能阻止添加新屬性。
注意,若是目標對象不可擴展(non-extensible),則 defineProperty()不能增長目標對象上不存在的屬性,不然會報錯。另外,若是目標對象的某個屬性不可寫(writable)或不可配置(configurable),則 defineProperty()方法不得改變這兩個設置。
用法是攔截 Object.getOwnPropertyDescriptor(),必須返回一個 object 或 undefined。該方法接受兩個參數,target:目標對象。prop:返回屬性名稱的描述。
let p = new Proxy(
{ a: 20 },
{
getOwnPropertyDescriptor: function (target, prop) {
console.log("called: " + prop);
return { configurable: true, enumerable: true, value: 10 };
},
}
);
console.log(Object.getOwnPropertyDescriptor(p, "a").value); // "called: a"
// 10
複製代碼
是一個代理(Proxy)方法,當讀取代理對象的原型時,該方法就會被調用。該方法只接受一個參數,target:被代理的目標對象。返回值必須是一個對象或者 null。 觸發該方法的條件總共有 5 種:
let proto = {};
let p = new Proxy(
{},
{
getPrototypeOf(target) {
return proto;
},
}
);
Object.getPrototypeOf(p) === proto; // true
複製代碼
用於攔截對對象的 Object.isExtensible()。該方法接受一個參數。target:目標對象。返回值必須返回一個 Boolean 值或可轉換成 Boolean 的值。
let p = new Proxy(
{},
{
isExtensible: function (target) {
console.log("called");
return true;
},
}
);
console.log(Object.isExtensible(p)); // "called"
// true
複製代碼
注意,該方法有一個強制的約束,即 Object.isExtensible(proxy) 必須同 Object.isExtensible(target)返回相同值。也就是必須返回 true 或者爲 true 的值,返回 false 和爲 false 的值都會報錯。
let p = new Proxy(
{},
{
isExtensible: function (target) {
return false;
},
}
);
Object.isExtensible(p); // Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
複製代碼
用來攔截對象自身屬性的讀取操做。該方法接受一個參數,target:目標對象。
具體攔截以下:
該方法有幾個約束條件:
let target = {
a: 1,
b: 2,
c: 3,
};
let p = new Proxy(target, {
ownKeys(target) {
return ["a"];
},
});
Object.keys(p); // ['a']
複製代碼
用於設置對 Object.preventExtensions()的攔截。該方法接受一個參數,target:所要攔截的目標對象。該方法返回一個布爾值。該方法有個限制,即若是目標對象是可擴展的,那麼只能返回 false。
let p = new Proxy(
{},
{
preventExtensions: function (target) {
return true;
},
}
);
Object.preventExtensions(p); // Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
複製代碼
上面代碼中,proxy.preventExtensions()方法返回 true,但這時 Object.isExtensible(proxy)會返回 true,所以報錯。
爲了防止出現這個問題,一般要在 proxy.preventExtensions()方法裏面,調用一次 Object.preventExtensions()。
let p = new Proxy(
{},
{
preventExtensions: function (target) {
console.log("called");
Object.preventExtensions(target);
return true;
},
}
);
console.log(Object.preventExtensions(p)); // "called"
// false
複製代碼
用來攔截 Object.setPrototypeOf()。該方法接受兩個參數,target:被攔截目標對象。prototype:對象新原型或爲 null。若是成功修改了[[Prototype]], setPrototypeOf 方法返回 true,不然返回 false。
var handler = {
setPrototypeOf(target, proto) {
throw new Error("Changing the prototype is forbidden");
},
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
複製代碼
上面代碼中,只要修改 target 的原型對象,就會報錯。
注意,若是目標對象不可擴展(non-extensible),setPrototypeOf()方法不得改變目標對象的原型。
最主要的區別實際上是應用到 Vue 的雙向數據綁定中,其實也有因爲 Vue 的雙向數據綁定,才讓這兩個方法愈來愈多的進入到人們的視野中。
在此咱們不深刻講解雙向數據綁定,因此分別用這兩個方法實現個簡單版的雙向數據綁定,來看看二者之間的區別。
const obj = {};
Object.defineProperty(obj, "text", {
get: function () {
console.log("get val");
},
set: function (newVal) {
console.log("set val:" + newVal);
document.getElementById("input").value = newVal;
document.getElementById("span").innerHTML = newVal;
},
});
const input = document.getElementById("input");
input.addEventListener("keyup", function (e) {
obj.text = e.target.value;
});
複製代碼
能夠看出來這個簡單版的透露出Object.defineProperty一個很明顯的缺點,就是隻能對對象的一個屬性進行監聽,若是想要對對象的全部屬性監聽的話,就要去遍歷,而且還有一個問題就是沒法去監聽數組,可是仍是有優勢的,優勢就是兼容性好,這也是爲何Vue2.0優先選擇了Object.defineProperty的緣由。
const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};
const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver);
if (key === 'text') {
input.value = value;
p.innerHTML = value;
}
return Reflect.set(target, key, value, receiver);
},
});
input.addEventListener('keyup', function(e) {
newObj.text = e.target.value;
});
複製代碼
從上述能夠看出Proxy能夠對整個對象進行攔截,而且其能返回一個新的對象,除此以外其能對數組進行攔截。並且最直觀的就是其有13個攔截方式。可是其最致命的問題就是兼容性很差,並且沒法用polyfill磨平,所以尤大大才聲明須要等到下個大版本(3.0)才能用Proxy重寫。
相關文章:
以爲還能夠的,麻煩走的時候能給點個贊,你們一塊兒學習和探討!
還能夠關注個人博客但願能給個人github上點個Start,小夥伴們必定會發現一個問題,個人全部用戶名幾乎都與番茄有關,由於我真的很喜歡吃番茄❤️!!!
想跟車不迷路的小夥還但願能夠關注公衆號 前端老番茄 或者掃一掃下面的二維碼👇👇👇。
我是一個編程界的小學生,您的鼓勵是我不斷前進的動力,😄但願能一塊兒加油前進。
本文使用 mdnice 排版