1. Object.defineProperty vs Proxy:劫持与代理的区别
Object.defineProperty 数据劫持的传统方式
Object.defineProperty()
是 JavaScript 中用于定义或修改对象属性的 API。它的强大之处在于你可以控制对象属性的访问器(getter
)和修改器(setter
),从而实现数据劫持。
示例:
let data = { name: "小明", age: 20 };
Object.defineProperty(data, "age", {
get() {
return this._age;
},
set(newValue) {
this._age = newValue;
console.log("年龄更新为:", newValue);
},
});
data.age = 25; // 控制器生效,输出: 年龄更新为: 25
Proxy
:更强大的对象代理
Proxy
是 ES6 引入的新特性,它可以通过自定义代理对象的行为来拦截和操作对象的操作。与 Object.defineProperty()
不同,Proxy
可以拦截整个对象的所有操作,包括新增属性、删除属性等。
示例:
function reactive(target) {
let handler = {
get(target, key, receiver) {
console.log(`获取属性: ${key}`);
return Reflect.get(...arguments);
},
set(target, key, value) {
console.log(`设置属性: ${key} 为 ${value}`);
return Reflect.set(...arguments);
},
};
return new Proxy(target, handler);
}
let data = { name: "小明", age: 20 };
let proxyData = reactive(data);
proxyData.name; // 输出: 获取属性: name
proxyData.age = 30; // 输出: 设置属性: age 为 30
2. 实现一个简易的响应式系统
通过 Object.defineProperty
和 Proxy
,我们可以实现一个简易的响应式数据原理。下面我们展示如何使用这两种方法来实现数据的响应式。
使用 Object.defineProperty
实现响应式
let oldArrayPrototype = Array.prototype;
let proto = Object.create(oldArrayPrototype); //obj.create 创建一个隐式原型为空的对象
//重写数组方法
Array.from(["push", "pop", "shift", "unshift"]).forEach((method) => {
proto[method] = function () {
oldArrayPrototype[method].call(this, ...arguments);
updateView();
};
});
// 利用 Object.defineProperty 实现对象代理
function defineReactive(target, key, value) {
if (typeof value === "object" && value !== null) {
//如果是对象就进行递归
observe(value); //如果不递归,则只能劫持浅层数据
}
//数据劫持
Object.defineProperty(target, key, {
get() {
return value;
},
set(newValue) {
if (newValue !== value) {
value = newValue;
updateView(); //简单模拟响应式导致的视图更新
}
},
});
}
function observer(target) {
if (typeof target !== "object" || target == null) {
return target;
}
if (Array.isArray(target)) {
target.__proto__ = proto;
return;
}
for (let key in target) {
defineReactive(target, key, target[key]);
}
}
function updateView() {
console.log("视图更新");
}
let data = { name: "小明", age: { n: 20 }, likes: ["编程", "学习"] };
observe(data);
data.age.n = 30; // 输出: 视图更新
// 本来无法触发劫持函数中的 set,无法触发视图更新,但是重写了数组方法,导致可以生效
data.likes.push("运动"); // 输出: 视图更新
data.sex = "boy"; //可以添加,但是新增的属性不会被劫持,所以也不会触发视图更新
使用 Proxy
实现响应式
// 直接代理整个对象
function isObject(val) {
return typeof val === "object" && val !== null;
}
function reactive(target) {
return createReactiveObject(target);
}
function createReactiveObject(target) {
if (!isObject(target)) {
return target;
}
let baseHandler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver);
return isObject(result) ? reactive(result) : result;
},
set(target, key, value, receiver) {
let res = Reflect.set(target, key, value, receiver);
updateView();
return res;
},
// .... 一共有 13 个
};
let observer = new Proxy(target, baseHandler);
return observer;
}
function updateView() {
console.log("视图更新");
}
let data = { name: "小明", age: { n: 20 }, likes: ["编程", "学习"] };
let newData = reactive(data);
newData.name = "小红"; // 输出: 视图更新
newData.age = 30; // 输出: 视图更新
newData.like.push("吃饭"); //可以添加进likes中 输出:视图更新
newData.sex = "boy"; //可以添加新属性也会导致视图更新 输出:视图更新
3. 对比:Object.defineProperty
和 Proxy
除了上面代码我们可以看出一些特点,还有一个Object.defineProperty
的专属优势,那就是它可以设置冻结属性的操作,请看下面:
Object.defineProperty(obj, "a", {
writable: false, //是否可修改
configurable: false, //是否可配置(删除)
enumerable: false, //是否可枚举(不能被遍历到,遍历器遍历不到,可console.log)
});
接下来我们可以总结一下Object.defineProperty
和 Proxy
的区别:
特性 | Object.defineProperty |
Proxy |
---|---|---|
作用范围 | 只能劫持单个属性 | 可以代理整个对象 |
劫持方式 | 只能劫持已有属性 | 可以拦截所有属性操作 |
递归代理 | 需要手动递归 | 可以按需递归 |
对数组的支持 | 无法劫持数组方法 | 可以代理数组方法 |
可拦截操作 | 只支持 get 和 set |
支持多达 13 种操作 |
可冻结属性 | 支持冻结对象的某些属性 |