如何用 Object.defineProperty 为对象属性设置只读或不可枚举权限并不只看表面做法,关键还要理解相关条件、限制和后续影响。
正确设置只读属性需同时设 writable: false 和 configurable: false;仅 writable: false 时仍可通过 defineProperty 改回可写;enumerable: false 控制遍历可见性,不影响直接访问;Vue 2 中 defineProperty 无法监听新增属性、数组索引赋值等,存在固有局限。
直接设 writable: false 不够,必须同时确保 configurable: false,否则后续仍可通过 Object.defineProperty 重新打开写权限。只设 writable: false 后,属性值确实无法被赋值(严格模式下报 TypeError: Cannot assign to read only property),但只要 configurable: true,就能再次调用 Object.defineProperty 把 writable 改回 true。
实操建议:
writable: false 和 configurable: false
configurable: true(极少用)configurable: true 才能成功调用 Object.defineProperty
const obj = { a: 1 };Object.defineProperty(obj, 'a', { writable: false, configurable: false, value: 1});obj.a = 2; // 无效(非严格模式)或报错(严格模式)Object.defineProperty(obj, 'a', { writable: true }); // TypeError: Cannot redefine property: a
enumerable: false 控制该属性是否出现在 for...in、Object.keys()、JSON.stringify() 等遍历操作中。它和 writable、configurable 正交,可以单独设置,不影响读写行为。
常见误区:
enumerable: false 不等于“隐藏”——仍可通过 obj.prop 直接访问,也能用 Object.getOwnPropertyDescriptor 查到for...in 会跳过 enumerable: false 属性,但会遍历原型链上 enumerable: true 的属性Object.assign({}, obj) 只拷贝自身且 enumerable: true 的属性,所以 enumerable: false 属性不会被复制const obj = {};Object.defineProperty(obj, 'hidden', { value: 'secret', enumerable: false, writable: true, configurable: true});console.log(Object.keys(obj)); // []console.log(obj.hidden); // 'secret'
这类组合常用于定义内部状态字段、缓存键、或向库暴露“只读接口但不污染遍历结果”的属性。比如封装一个带缓存的计算属性:
writable: false)Object.keys() 或序列化中(enumerable: false)configurable: false),防止被意外覆盖注意:如果属性初始值是对象或数组,writable: false 只冻结引用,不深冻结内容;需额外用 Object.freeze。
const cache = {};Object.defineProperty(obj, '_cache', { value: cache, writable: false, enumerable: false, configurable: false});// 后续仍可修改 cache 内部:cache.x = 1;// 若需完全冻结,应写成 value: Object.freeze({})
Vue 2 用 Object.defineProperty 实现数据劫持,但它有硬性限制:无法检测对象新增/删除属性,也无法监听数组索引赋值(如 arr[0] = x)或 length 变更。这些限制不是写法问题,而是 API 本身能力边界。
实际开发中容易踩的坑:
vm.obj.newKey = val),不会触发视图更新Object.defineProperty 手动加的属性,若没走 Vue 的 set 方法,也不会进入依赖收集push 被重写了,但直接写索引或 splice 某些用法仍可能漏掉响应现代项目优先用 Proxy 替代,但若维护旧代码,就得接受这些约束,并始终用 Vue.set 或 this.$set 添加响应式属性。