深入浅出:理解 Object.defineProperty 和 Proxy

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.definePropertyProxy,我们可以实现一个简易的响应式数据原理。下面我们展示如何使用这两种方法来实现数据的响应式。

使用 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.definePropertyProxy

除了上面代码我们可以看出一些特点,还有一个Object.defineProperty的专属优势,那就是它可以设置冻结属性的操作,请看下面:

Object.defineProperty(obj, "a", {
  writable: false, //是否可修改
  configurable: false, //是否可配置(删除)
  enumerable: false, //是否可枚举(不能被遍历到,遍历器遍历不到,可console.log)
});

接下来我们可以总结一下Object.definePropertyProxy的区别:

特性 Object.defineProperty Proxy
作用范围 只能劫持单个属性 可以代理整个对象
劫持方式 只能劫持已有属性 可以拦截所有属性操作
递归代理 需要手动递归 可以按需递归
对数组的支持 无法劫持数组方法 可以代理数组方法
可拦截操作 只支持 getset 支持多达 13 种操作
可冻结属性 支持冻结对象的某些属性