虽然个人主技术栈是 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 排版