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

1,912 阅读3分钟

在这篇文章中,我们将讲解 Object.definePropertyProxy,对比它们的异同,帮助你理解它们。

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.crate创建一个隐式原型为空的对象

//重写数组方法
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.definePropertyProxy
作用范围只能劫持单个属性可以代理整个对象
劫持方式只能劫持已有属性可以拦截所有属性操作
递归代理需要手动递归可以按需递归
对数组的支持无法劫持数组方法可以代理数组方法
可拦截操作只支持 getset支持多达 13 种操作
可冻结属性支持冻结对象的某些属性