百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

宇宙厂:Vue3.0 为何用 Proxy 替代 defineProperty?

suiw9 2025-03-19 01:59 4 浏览 0 评论

家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

1. 什么是 Object.defineProperty

1.1 Object.defineProperty 基本用法

Object.defineProperty() 允许精确添加或修改对象属性。通过赋值添加的普通属性会在枚举属性时(例如 for...in、Object.keys() 等)出现,值可以被更改,也可以被删除。

defineProperty() 方法允许更改额外细节,以使其不同于默认值。默认情况下,使用 Object.defineProperty() 添加的属性是不可写、不可枚举和不可配置的。此外,Object.defineProperty() 使用 [[DefineOwnProperty]] 内部方法,而不是 [[Set]],因此即使属性已经存在也不会调用 setter。


Object.defineProperty(obj, prop, descriptor)

方法每一个参数定义如下:

  • obj:要定义属性的对象。
  • prop:一个字符串或 Symbol,指定了要定义或修改的属性键。
  • descriptor:要定义或修改的属性的描述符,包括:configurable(如是否可删除)、enumerable、writable、get、set 等等。

下面示例使用 Object.defineProperty 进行对象属性定义:

const obj = {};
// 1. 使用 null 原型:没有继承的属性
const descriptor = Object.create(null);
descriptor.value = "static";

// 默认情况下,不可枚举、不可配置、不可写
// obj.key ="static modified" 赋值后依然是 "static"
Object.defineProperty(obj, "key", descriptor);

// 2. 使用一个包含所有属性的临时对象字面量来明确其属性
Object.defineProperty(obj, "key2", {
  enumerable: false,
  configurable: false,
  writable: false,
  value: "static",
});

// 3. 重复利用同一对象
function withValue(value) {
  const d =
    withValue.d ||
    (withValue.d = {
      enumerable: false,
      writable: false,
      configurable: false,
      value,
    });

  // 避免重复赋值
  if (d.value !== value) d.value = value;
  return d;
}
// 然后
Object.defineProperty(obj, "key", withValue("static"));

// 如果 freeze 可用,防止添加或删除对象原型属性
// (value、get、set、enumerable、writable、configurable)
(Object.freeze || Object)(Object.prototype);

1.2 Object.defineProperty 优缺点

Object.defineProperty 的主要优点包括:

  • Object.defineProperty() 方法可以对属性的行为方式进行细粒度的控制
  • 允许设置只读属性,防止意外修改
  • 开发者可以决定某个属性是否在枚举期间出现,从而实现特定的功能
  • 允许开发者使属性不可删除,从而保证核心属性的安全。

当然,Object.defineProperty 也有不足之处,主要体现在:

  • Object.defineProperty() 不能很好地处理数组,因为无法捕获修改索引值或长度属性以及动态属性(动态 getter 是一种没有为 property 显式定义 getter,而是在访问属性时动态创建的),详情可以看这篇文章(https://vue3js.cn/interview/vue3/proxy.html#一、object-defineproperty)。同时,也不支持嵌套对象,这意味着不会观察到嵌套对象的任何更改

下面是对象示例:

const obj = {
    foo: "foo",
    bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok

下面是数组示例:

const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
    defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop()  // no ok
arrDate[0] = 99 // ok

基于对象和数组的以上局限性,Vue2 增加了 set、delete API,并且对数组 api 方法进行一个重写。

  • Object.defineProperty() 的语法很冗长,可能会增加可读性,影响代码的模块化和可重用性。

而掌握 Object.defineProperty 的关键在于透彻理解属性描述符的属性。 例如,正确地将 writable 属性设置为 false 可以确保属性值在整个程序中保持不变,从而减少出现错误的机会。

比如下面的示例将 π 置为常量后将无法修改:

let constantObj = {};
Object.defineProperty(constantObj, 'pi', {
  value: 3.14159,
  writable: false
});

console.log(constantObj.pi);
// Outputs 3.14159
constantObj.pi = 3;
// Attempting to change the value
console.log(constantObj.pi);
// Still outputs 3.14159

注意:Vue 3 改用了 Proxy 。Proxy 可以拦截对象属性读取、赋值和删除操作,从而能够在属性发生变化时触发相应的更新。对于数组,Proxy 可以拦截数组的修改操作,比如: push、pop、splice 等,从而能够在数组发生变化时触发相应的更新。


此外,Proxy 还可以拦截对象的原型方法和构造函数调用,从而可以对对象的所有操作进行拦截和处理。

2. 什么是 Proxy

JavaScript 的 Proxy 对象是一项强大的功能,使开发者能够拦截和自定义对对象执行的操作,例如:属性查找、赋值、枚举和函数调用。 这种多功能工具允许开发人员创建更高效、更灵活的代码,同时还提高代码的可维护性。

Proxy 遵循以下语法规范:

const p = new Proxy(target, handler)
  • target:要使用 Proxy 包装的目标对象,可以是任何类型的对象,包括原生数组,函数,甚至另一个代理。
  • handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

值得一提的是,handler 对象是一个容纳一批特定属性的占位符对象,包含有 Proxy 的各个捕获器(trap)。而且所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

常见的捕获器包括:

  • getPrototypeOf()
  • setPrototypeOf()
  • isExtensible()
  • preventExtensions()
  • getOwnPropertyDescriptor()
  • defineProperty()
  • has()
  • get()
  • set()
  • deleteProperty()
  • ownKeys()
  • apply()
  • construct()

在以下例子中,使用了一个原生 JavaScript 对象,Proxy 会将所有应用到它的操作转发到这个对象上。

let target = {};
let p = new Proxy(target, {});

p.a = 37;
// 操作转发到目标

console.log(target.a);
// 37. 操作已经被正确地转发

3.Proxy 与 Object.defineProperty 主要区别

Proxy 与 Object.defineProperty 一个主要区别在于抽象级别。 Proxy 在对象周围创建一个新层,可以 Hook 任何属性,而无需预先显式定义, 而 Object.defineProperty 直接修改对象并要求相关属性在定义时就存在。

此外,Proxies 涵盖了广泛的 property 操作,而 Object.defineProperty 则专注于 attribute 属性操作。

关于 Proxy 和 Object.defineProperty 还需要弄清楚一个常见错误,即将 Object.defineProperty 用于复杂的动态对象,期望新属性的反应行为可能会导致意外结果,因为 Object.defineProperty 只影响现有属性。

比如下面的示例:

let object = {};
Object.defineProperty(object, 'property', {
    value: 42,
    writable: false
});
object.newProperty = 100;
console.log(object.newProperty);
// 输出: 100
// newProperty 的行为不受 Object.defineProperty 控制

实现新属性反应性(Reactivity)的正确方法是使用 Proxy,或者为每个新属性动态实现 Object.defineProperty。比如下面的 Proxy 示例:

let targetObject = {message: 'Hello, world'};
let handler = {
    set: function(target, prop, value) {
        if (prop === 'newProperty') {
            target[prop] = value * 2;
        } else {
            target[prop] = value;
        }
    }
};

let proxy = new Proxy(targetObject, handler);
proxy.newProperty = 100;
console.log(proxy.newProperty);
// 输入: 200

4. 使用 Proxy 的场景

4.1 验证对象属性

考虑创建一个需要具有某些有条件所需属性的严格架构的对象,可以通过使用代理包装对象并在 set 中实施验证检查来管理,从而确保只有有效数据进入对象。

比如下面的代码示例使用 Proxy 实现 set 方法,验证对象的属性是否在指定的 schema 中:

let schema = {
    id: {
        type: 'number',
        required: true
    },
    comment: {
        type: 'string',
        required: false
    }
};
let handler = {
    set: function (target, key, value) {
        if (schema[key] && typeof value !== schema[key].type) {
            throw new Error(`Type ${typeof value} is not assignable to type ${schema[key].type}`);
        } else if (schema[key] && schema[key].required && value === undefined) {
            throw new Error(`${key} is required.`);
        }
        target[key] = value;
        return true;
    }
};

let movie = new Proxy({}, handler);

4.2 对象级访问控制

Proxy 对象可以有效控制对象属性的访问,通常可用于提供对象的只读视图或限制可访问的对象属性的范围。

比如下面的代码示例表示访问 password 属性后则会抛出错误:

let personDetails = {
    firstName: 'John',
    lastName: 'Doe',
    password: '12345!'
};

let handler = {
    get: function (target, prop) {
        if (prop === 'password') {
            throw new Error('Access to password is denied');
        }
        return target[prop];
    }
};
let proxy = new Proxy(personDetails, handler);
console.log(proxy.password);
// 抛出错误
console.log(proxy.firstName);
// 输出: 'John'

又或者下面的代码示例在修改元素属性之前做精确的控制,从而属性相互覆盖:

function assignIfNotExists(target, source){
    for (let prop in source) {
        if (!target.hasOwnProperty(prop)) {
            target[prop] = source[prop];
        }
    }
}
let data = {username: 'Zach'};
let userInput = {username: 'JohnDoe', password: 'secret'};
// Avoid overwriting 'username' in data object
assignIfNotExist(data, userInput);

4.3 数据绑定和观察者

Proxy 可以帮助构建数据绑定解决方案,比如:当应用程序的状态发生变化时,开发者可能希望跟踪变化并做出响应,比如: Vue.js 就是一个很好的示例。

let state = {
    count: 0
};
let handler = {
    set: function (target, property, value) {
        target[property] = value;
        console.log(`State has changed. New ${property}: ${value}`);
        return true;
    }
};
let proxy = new Proxy(state, handler);
proxy.count = 2;
// 输出: State has changed. New count: 2

以上代码示例,JavaScript Proxy 提供了对对象交互的精确控制,从而实现复杂行为、验证、访问控制等等。 然而,由于 Proxy 的复杂性,考虑使用 Proxy 的开销也同样重要。 因此,Proxy 的使用应该针对特定的挑战,其独特的功能可以显著提高系统操作和可读性。

5.Proxy 与 Object.defineProperty 深入比较

5.1 性能

JavaScript Proxy 比 Object.defineProperty 消耗的时间稍多, Proxy 本质上应用了一个额外的抽象层(处理程序),从而可能会使操作比 Object.defineProperty 更慢。

比如下面的代码示例:

let object = {};
Object.defineProperty(object, 'property', {
    value: 42,
    writable: false
});

object 中的 property 属性值必须是常量,直接访问 property 非常简单快捷。 而对于 Proxy 来说,在获取对象值之前有一个额外的检查和验证过程:

let targetObject = {property: 42};
let handler = {
    get: function(target, prop) {
        return target[prop];
    }
};
let proxy = new Proxy(targetObject, handler);

5.2 代码复杂性和可读性

Object.defineProperty 重点关注属性级别, 当需要控制属性是否可以修改、配置甚至枚举时则是理想选择,同时 Object.defineProperty 的用法直接且有针对性的,使代码更容易阅读和理解。

然而,Proxy 在提供更高级别的抽象方面表现出色。 Proxy 对象可以针对整个对象,而不仅仅是单个属性,从而允许开发人员以更高级的方式拦截和重新定义对象的默认行为。

然而,Proxy 中的处理程序可能会造成复杂性,因为总是需要通过一个额外的中间层。 其他开发者也需要对 Proxy 概念有更多的了解才能轻松阅读 Proxy 代码。

5.3 模块化和可重用性

在模块化和可重用性方面,当想要为更大范围甚至整个应用程序定义全局处理程序行为时,Proxy 通常会发挥作用。 Proxy 通常提供一种极好的方法来将特定的控制行为封装在单独的处理程序中。 这样,同一个处理程序可以与多个目标对象重复使用。

相反,Object.defineProperty 允许模块化和保护单个对象属性,对于以模块化方式定义、保护或控制对象的属性非常重要。

Proxy 提供了更多的可能性,捕获更多的动作,并提供对对象的更多控制。 然而,它们也会带来性能成本,需要了解它们的用法,并且可能会使调试变得复杂。

另一方面,Object.defineProperty 虽然不如代理那么强大和灵活,但提供了一种简单、直接且易于调试的方法。

6.Proxy 常见方法

6.1 Proxy 转为普通对象

const proxy  = {"name":"高级前端进阶"}

const proxyObj = new Proxy(proxy, {
  get: (target, prop) => prop in target ? target[prop] : 37
});

console.log(proxyObj.a)
// 输出 37
console.log(proxyObj.name)
// 输出 ` 高级前端进阶 `
console.log(JSON.stringify(proxyObj))
// 输出 {"name":"晴天"}

值得注意的是,使用 JSON.parse(JSON.stringify(proxyObj)) 方法会删除任何不能字符串化的内容,比如:类、函数、回调等。

如果确实需要,可以考虑使用 Lodash 的 cloneDeep 函数,该方法在将 Proxy 对象转换为 POJO(The Plain Old JavaScript Object) 的同时保持对象结构方面确实做得很好。

 convertProxyObjectToPojo(proxyObj) {
  return _.cloneDeep(proxyObj);
}

6.2 Proxy 监听数组元素变化

以下示例表示 Proxy 确实能监听到数组元素的变更,这与 defineProperty 是有差别的,至于监听嵌套对象属性变化可以自行验证。

function get(target, prop, receiver) {
  console.log('target:' + target);
  console.log('property:' + prop);
  return Reflect.get(target, prop, receiver);
}

var handler = {
  'get': get
};

// 为数组添加 Proxy
var proxy = new Proxy([1,2,3,4,5], handler );

console.log('Result => beep:' + proxy.beep );
// target: 1,2,3,4,5
// property: beep
// Result => beep: undefined
console.log('Result => -123:' + proxy[ -123] );
// target: 1,2,3,4,5
// property: -123
// proxy:16 Result => -123: undefined
console.log(proxy.fill( 1) );
// target: 1,2,3,4,5
// property: fill
// target: 1,2,3,4,5
// property: length
// Proxy(Array) {0: 1, 1: 1, 2: 1, 3: 1, 4: 1}
console.log('Result => 0:' + proxy[ 0 ] );
// target: 1,1,1,1,1
// property: 0
// Result => 0: 1
var arr1 = [10, 20, 30, 40, 50];
Object.setPrototypeOf(arr1, proxy);
console.log('Result => beep:' + arr1.beep );
console.log('Result => -123:' + arr1[ -123 ] );
console.log(arr1.fill( 100) );
// 输出 (5) [100, 100, 100, 100, 100]
console.log('Result => 0:' + arr1[ 0 ] );

6.3 Proxy 监听嵌套对象

var validator = {
  get(target, key) {
    if (typeof target[key] === 'object' && target[key] !== null) {
        // 如果是对象则继续创建 Proxy
      return new Proxy(target[key], validator)
    } else {
      return target[key];
    }
  },
  set (target, key, value) {
    console.log(target);
    // 输出 {salary: 8250, Proffesion: '.NET Developer'}
    console.log(key);
    // 输出 salary
    console.log(value);
    // 输出 foo
    return true
  }
}
var person = {
      firstName: "alfred",
      lastName: "john",
      inner: {
        salary: 8250,
        Proffesion: ".NET Developer"
      }
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'
// 这一句代码会先访问 proxy.inner 属性,发现是 Object
// 然后会继续访问 salary 属性

参考资料

https://borstch.com/blog/objects-in-javascript-properties-methods-and-prototypes

https://borstch.com/blog/proxies-vs-objectdefineproperty-when-to-use-which

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

https://blog.javascripttoday.com/blog/deep-dive-proxies-in-javascript/


https://vue3js.cn/interview/vue3/proxy.html#二、proxy


https://vue3js.cn/interview/vue3/proxy.html#一、object-defineproperty

https://juejin.cn/post/7306783965532717108

https://www.youtube.com/watch?app=desktop&v=_k3WiANNB4U

https://segmentfault.com/q/1010000043053833

https://www.30secondsofcode.org/js/s/dynamic-getter-setter-proxy/

https://gist.github.com/kgryte/713ab40f36c128bc1d52

https://stackoverflow.com/questions/41299642/how-to-use-javascript-proxy-for-nested-objects

相关推荐

5款Syslog集中系统日志常用工具对比推荐

一、为何要集中管理Syslog?Syslog由Linux/Unix系统及其他网络设备生成,广泛分布于整个网络。因其包含关键信息,可用于识别网络中的恶意活动,所以必须对其进行持续监控。将Sys...

跨平台、多数据库支持的开源数据库管理工具——DBeaver

简介今天给大家推荐一个开源的数据库管理工具——DBeaver。它支持多种数据库系统,包括Mysql、Oracle、PostgreSQL、SLQLite、SQLServer等。DBeaver的界面友好...

强烈推荐!数据库管理工具:Navicat Premium 16.3.2 (64位)

NavicatPremium,一款集数据迁移、数据库管理、SQL/查询编辑、智能设计、高效协作于一体的全能数据库开发工具。无论你是MySQL、MariaDB、MongoDB、SQLServer、O...

3 年 Java 程序员还玩不转 MongoDB,网友:失望

一、什么场景使用MongoDB?...

拯救MongoDB管理员的GUI工具大赏:从菜鸟到极客的生存指南

作为一名在NoSQL丛林中披荆斩棘的数据猎人,没有比GUI工具更称手的瑞士军刀了。本文将带你围观五款主流MongoDB管理神器的特性与暗坑,附赠精准到扎心的吐槽指南一、MongoDBCompass:...

mongodb/redis/neo4j 如何自己打造一个 web 数据库可视化客户端?

前言最近在做neo4j相关的同步处理,因为产线的可视化工具短暂不可用,发现写起来各种脚本非常麻烦。...

solidworks使用心得,纯干货!建议大家收藏

SolidWorks常见问题...

统一规约-关乎数字化的真正实现(规范统一性)

尽管数字化转型的浪潮如此深入人心,但是,对于OPCUA和TSN的了解却又甚少,这难免让人质疑其可实现性,因为,如果缺乏统一的语义互操作规范,以及更为具有广泛适用的网络与通信,则数字化实际上几乎难以具...

Elasticsearch节点角色配置详解(Node)

本篇文章将介绍如下内容:节点角色简介...

产前母婴用品分享 篇一:我的母婴购物清单及单品推荐

作者:DaisyH8746在张大妈上已经混迹很久了,有事没事看看“什么值得买”已渐渐成了一种生活习惯,然而却从来没有想过自己要写篇文章发布上来,直到由于我产前功课做得“太过认真”(认真到都有点过了,...

比任何人都光彩照人的假期!水润、紧致的肌肤护理程序

图片来源:谜尚愉快的假期临近了。身心振奋的休假季节。但是不能因为这种心情而失去珍贵的东西,那就是皮肤健康。炙热的阳光和强烈的紫外线是使我们皮肤老化的主犯。因此,如果怀着快乐的心情对皮肤置之不理,就会使...

Arm发布Armv9边缘AI计算平台,支持运行超10亿参数端侧AI模型

中关村在线2月27日消息,Arm正式发布Armv9边缘人工智能(AI)计算平台。据悉,该平台以全新的ArmCortex-A320CPU和领先的边缘AI加速器ArmEthos-U85NPU为核心...

柔性——面向大规模定制生产的数字化实现的基本特征

大规模定制生产模式的核心是柔性,尤其是体现在其对定制的要求方面。既然是定制,并且是大规模的定制,对于制造系统的柔性以及借助于数字化手段实现的柔性,就提出了更高的要求。面向大规模定制生产的数字化业务管控...

创建PLC内部标准——企业前进的道路

作者:FrankBurger...

标准化编程之 ----------- 西门子LPMLV30测试总结

PackML乃是由OMAC开发且被ISA所采用的自动化标准TR88.00.02,能够更为便捷地传输与检索一致的机器数据。PackML的主要宗旨在于于整个工厂车间倡导通用的“外观和感觉”,...

取消回复欢迎 发表评论: