defineProperty和proxy的对比

本文对比了Vue中用于数据绑定的两种方法:defineProperty和Proxy。defineProperty通过Object.defineProperty实现属性的劫持,而Proxy则提供更广泛的拦截能力,包括get、set及其他操作。文章详细介绍了两者的工作原理,并通过实例展示了如何使用它们实现数据监听。

defineProperty和proxy的对比

“数据绑定”这个词是现在市面上流行框架必懂的概念,“数据绑定”的关键在于监听数据的变化,主要的实现方法是defineProperty(数据劫持)和proxy(代理)这两种方法,vue2.0实现数据绑定正是使用前者,vue3.0使用是后者,今天就通过本文带领大家一起来对比这两种方法!

什么是数据劫持

数据劫持比较好理解,通常我们利用Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,从而进行进一步操作。

// 这是将要被劫持的对象
const data = {
  name: '',
};

function say(name) {
  if (name === 'tim') {
    console.log('tim给大家推荐一本好书');
  } else if (name === 'peter') {
    console.log('戏我演过很多,可游戏我只玩贪玩懒月');
  } else {
    console.log('来做我的兄弟');
  }
}

// 遍历对象,对其属性值进行劫持
Object.keys(data).forEach(function(key) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      console.log('get');
    },
    set: function(newVal) {
      // 当属性值发生变化时我们可以进行额外操作
      console.log(`大家好,我系${newVal}`);
      say(newVal);
    },
  });
});

data.name = 'peter';
//大家好,我系peter
//戏我演过很多,可游戏我只玩贪玩懒月

definePropety

我们都知道,Object.defineProperty的作用就是劫持一个对象的属性,通常我们对属性的getter和setter方法进行劫持,在对象的属性发生变化时进行特定的操作。

我们就对对象obj的text属性进行劫持,在获取此属性的值时打印’get val’,在更改属性值的时候对DOM进行操作,这就是一个极简的双向绑定。

var value = 1;
Object.defineProperty({}, "num", {
    get : function(){
      return value;
    },
    set : function(newValue){
      value = newValue;
    },
    enumerable : true,
    configurable : true
});

get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。

set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。

enumerable

当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。

Setters 和 Getters

之所以讲到 defineProperty,是因为我们要使用存取描述符中的 get 和 set,这两个方法又被称为 getter 和 setter。由 getter 和 setter 定义的属性称做”存取器属性“。

当程序查询存取器属性的值时,JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。从某种意义上讲,这个方法负责“设置”属性值。可以忽略 setter 方法的返回值。

var obj = {}, value = null;
Object.defineProperty(obj, "num", {
    get: function(){
        console.log('执行了 get 操作')
        return value;
    },
    set: function(newValue) {
        console.log('执行了 set 操作')
        value = newValue;
    }
})

obj.value = 1 // 执行了 set 操作

console.log(obj.value); // 执行了 get 操作 // 1

watch API

既然可以监控数据的改变,那我可以这样设想,即当数据改变的时候,自动进行渲染工作。举个例子:

HTML 中有个 span 标签和 button 标签

<span id="container">1</span>
<button id="button">点击加 1</button>

当点击按钮的时候,span 标签里的值加 1。

传统的做法是:

document.getElementById('button').addEventListener("click", function(){
    var container = document.getElementById("container");
    container.innerHTML = Number(container.innerHTML) + 1;
});

如果使用了 defineProperty:

var obj = {
    value: 1
}

// 储存 obj.value 的值
var value = 1;

Object.defineProperty(obj, "value", {
    get: function() {
        return value;
    },
    set: function(newValue) {
        value = newValue;
        document.getElementById('container').innerHTML = newValue;
    }
});

document.getElementById('button').addEventListener("click", function() {
    obj.value += 1;
});

然而,现在的写法,我们还需要单独声明一个变量存储 obj.value 的值,因为如果你在 set 中直接 obj.value = newValue 就会陷入无限的循环中。此外,我们可能需要监控很多属性值的改变,要是一个一个写,也很累呐,所以我们简单写个 watch 函数。使用效果如下:

(function(){
    var root = this;
    function watch(obj, name, func){
        var value = obj[name];

        Object.defineProperty(obj, name, {
            get: function() {
                return value;
            },
            set: function(newValue) {
                value = newValue;
                func(value)
            }
        });

        if (value) obj[name] = value
    }

    this.watch = watch;
})()

以上正是利用definePropety监控对象属性值的改变,并且可以根据属性值的改变,添加回调函数,应该注意到在上文中的实现里,我们多次用遍历方法遍历对象的属性,这就引出了Object.defineProperty的缺陷,只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。

proxy

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

使用 defineProperty 只能重定义属性的读取(get)和设置(set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 in、delete、函数调用等更多行为。

Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。我们来看看它的语法:

var proxy = new Proxy(target, handler);

proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

var proxy = new Proxy({}, {
    get: function(obj, prop) {
        console.log('设置 get 操作')
        return obj[prop];
    },
    set: function(obj, prop, value) {
        console.log('设置 set 操作')
        obj[prop] = value;
    }
});

proxy.time = 35; // 设置 set 操作

console.log(proxy.time); // 设置 get 操作 // 35

除了 get 和 set 之外,proxy 可以拦截多达 13 种操作,比如 has(target, propKey),可以拦截 propKey in proxy 的操作,返回一个布尔值。

// 使用 has 方法隐藏某些属性,不被 in 运算符发现
var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
console.log('_prop' in proxy); // false

又比如说 apply 方法拦截函数的调用、call 和 apply 操作。

apply 方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组,不过这里我们简单演示一下:

var target = function () { return 'I am the target'; };
var handler = {
  apply: function () {
    return 'I am the proxy';
  }
};

var p = new Proxy(target, handler);

p();
// "I am the proxy"

又比如说 ownKeys 方法可以拦截对象自身属性的读取操作。具体来说,拦截以下操作:

Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()

下面的例子是拦截第一个字符为下划线的属性名,不让它被 for of 遍历到。

let target = {
  _bar: 'foo',
  _prop: 'bar',
  prop: 'baz'
};

let handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  }
};

let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
  console.log(target[key]);
}
// "baz"

值得注意的是,proxy 的最大问题在于浏览器支持度不够,而且很多效果无法使用 poilyfill 来弥补。

watch API 优化

我们使用 proxy 再来写一下 watch 函数。使用效果如下:

(function() {
    var root = this;

    function watch(target, func) {

        var proxy = new Proxy(target, {
            get: function(target, prop) {
                return target[prop];
            },
            set: function(target, prop, value) {
                target[prop] = value;
                func(prop, value);
            }
        });

        if(target[name]) proxy[name] = value;
        return proxy;
    }

    this.watch = watch;
})()

var obj = {
    value: 1
}

var newObj = watch(obj, function(key, newvalue) {
    if (key == 'value') document.getElementById('container').innerHTML = newvalue;
})

document.getElementById('button').addEventListener("click", function() {
    newObj.value += 1
});

我们也可以发现,使用 defineProperty 和 proxy 的区别,当使用 defineProperty,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截。

关注公众号【入坑互联网】获取更多前端干货及海量学习资料!!!

【公众号送书活动】
在元旦来临之际将推出送书活动,想知道详情赶紧关注公众号,回复关键字【送书】即可知道详情

<think> 嗯,用户想了解Object.definePropertyProxy区别,这个问题在前端开发中很常见。用户可能正在学习Vue.js的响应式原理,或者需要为项目选择合适的数据劫持方案。从用户直接提问的方式看,ta更关注技术细节而非基础概念。 引用[3]提供了很好的对比框架,包括拦截维度、数组处理、新增属性支持性能四个方面。不过需要补充实际代码示例帮助理解。用户提供的几个引用中,[1][2]侧重API用法,[3][4]侧重对比分析,正好可以结合起来回答。 Object.defineProperty的局限性主要体现在:需要遍历对象所有属性,对数组变异方法需要重写,无法检测属性增减。而Proxy的优势在于:直接代理整个对象,支持13种拦截操作,完美处理数组变化。不过要注意Proxy的兼容性问题,在IE11及以下无法使用。 考虑到用户可能是中级开发者,回答时应该:1)用对比表格直观展示核心差异;2)针对Vue响应式原理说明实际应用场景;3)给出可运行的代码片段。特别要强调Proxy的handler方法列表是个关键点,比如has/deleteProperty这些Object.defineProperty无法实现的能力。 用户可能隐含的需求是想知道为什么Vue3要改用Proxy,所以需要补充性能对比Proxy在初始化时更快,但调用拦截器略慢;Object.defineProperty在初始化时递归遍历消耗更大,但触发setter更快。对于大型对象,Proxy的整体性能更好。 </think> 以下是 `Object.defineProperty` `Proxy` 的核心区别对比及详解(基于引用内容整理): --- ### **核心区别对比表** | **特性** | **Object.defineProperty** | **Proxy** | |-----------------------|----------------------------------------|-------------------------------| | **拦截维度** | **属性级别**(需逐个定义属性) | **对象级别**(代理整个对象) | | **新增属性监听** | 需手动重新调用 | **自动拦截** | | **数组处理** | 需重写数组方法(如 `push`) | **原生支持**数组操作拦截 | | **ES版本** | ES5 | ES6 | | **性能** | 较好(但初始化递归遍历消耗大) | 更优(惰性拦截,无初始化遍历)| --- ### **详细解析** #### 1. **拦截粒度不同** - **`Object.defineProperty`** 只能拦截**已定义属性**的读写操作,需显式指定每个属性(代码示例[^2][^3]): ```javascript const obj = {}; Object.defineProperty(obj, 'name', { get() { return this._name; }, set(val) { this._name = val; console.log('更新DOM'); } }); obj.name = 'Alice'; // 触发set -> 更新DOM ``` - ❌ **无法拦截**未预先定义的属性或动态新增属性。 - **`Proxy`** 代理**整个对象**,可拦截**13种操作**(包括属性增删、数组操作等): ```javascript const obj = { name: 'Bob' }; const proxy = new Proxy(obj, { get(target, key) { return target[key]; }, set(target, key, value) { target[key] = value; console.log('更新DOM'); return true; } }); proxy.age = 30; // 自动触发set -> 更新DOM(无需预先定义age) ``` --- #### 2. **数组处理能力** - **`Object.defineProperty`** 监听数组下标变更需重写数组方法(如 `push`): ```javascript const arr = []; Object.defineProperty(arr, 'push', { value: function(...args) { console.log('数组变更'); return Array.prototype.push.apply(this, args); } }); arr.push(1); // 手动重写方法触发 ``` - **`Proxy`** **原生支持**数组操作拦截(无需重写): ```javascript const arr = []; const proxyArr = new Proxy(arr, { set(target, key, value) { console.log('数组变更'); return Reflect.set(target, key, value); } }); proxyArr.push(1); // 自动触发set ``` --- #### 3. **性能差异** - **`Object.defineProperty`** 初始化时需**递归遍历所有属性**,对大型对象性能消耗大。 - **`Proxy`** **惰性拦截**:仅在访问属性时触发,初始化更快(尤其适合复杂对象)[^3]。 --- ### **应用场景** | **场景** | 推荐方案 | 原因 | |---------------------------|----------------------|------------------------------| | **兼容IE低版本** | `Object.defineProperty` | Proxy 不支持 IE11 及以下 | | **动态属性监听/Vue3响应式** | `Proxy` | 自动拦截新增/删除属性 | | **精细化控制单个属性** | `Object.defineProperty` | 属性级拦截更直接 | --- ### **总结** - **`Object.defineProperty`**: 适合**兼容性要求高**或**精确控制已知属性**的场景。 - **`Proxy`**: 适合**动态属性监听**、**数组操作**及**现代浏览器环境**,简化代码并提升性能。 > 在Vue2中使用 `Object.defineProperty` 实现响应式,而Vue3全面转向 `Proxy`,核心原因是后者解决了动态属性增删数组操作的监听缺陷[^3][^4]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值