对象不可修改的几种方案

294 阅读3分钟

const声明的常量是不允许被修改的,但是如果保存的是引用值,const只能保证栈指向堆的指针不变,不能保证引用值的内容不变

那么如何保证对象不可修改呢?

Object.defineProperty()

ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性。

Object.defineProperty(obj, prop, descriptor)

参数:

  1. obj 要定义属性的对象
  2. prop 一个字符串或 Symbol,指定了要定义或修改的属性键
  3. descriptor 要定义或修改的属性的描述符
    • 数据描述符
      • configurable: 是否可删除 / 可配置descriptor
      • enumerable:是否可枚举
      • value:该属性的值,默认undefined
      • writable:是否可写
    • 访问器描述符
      • configurable
      • enumerable
      • get:属性值获取拦截
      • set:属性值赋值拦截

描述符只能是这两种类型之一,不能同时为两者,比如当描述符既有value,也有get的时候,是冲突的

通过赋值添加的对象属性 可写 / 可删可配置 / 可遍历,通过使用 defineProperty 添加的属性 默认情况下 不可写 / 不可枚举 / 不可配置

结合writable: false和configurable: false就可以创建一个真正的常量属性

const obj = {
  a: 1,
  b: 2
}

Object.defineProperty(obj, 'a', {
  writable: false,
  configurable: false
})

obj.a = 'change';
delete obj.b;
obj.c = 'new val';

console.log(obj); // {a: 1, c: 'new val'}

可以确保对象的某个属性不可改,但不能阻止对象 新增 / 删除 属性

Proxy 监听对象操作

对 set / delete 操作进行拦截,不允许设置和删除操作

let targetObj = {
  a: 1
}

let proxyObj = new Proxy(targetObj, {
  set() {
    throw new Error('不允许修改')
  },
  deleteProperty() {
    throw new Error('不允许删除')
  }
})

proxyObj.b = 2
proxyObj.a = 2
delete(proxyObj.a)
console.log(proxyObj)

Object.preventExtensions 不可拓展

让一个对象变得不可拓展,也就是无法 新增自身的属性,但是可以修改 / 删除原有属性

并且该方法只能防止添加自有属性,但原型依然可以添加新的属性,但是不能重新赋值[[prototype]]

const obj = {
  a: 1,
  b: 2
}

Object.preventExtensions(obj)

// // 报错 Cannot define property c, object is not extensible
// obj.c = 'new val';

// 报错
// Object.defineProperty(obj, 'c', {
//   val: 'defineProperty newVal',
//   writable: false,
//   configurable: false
// })

obj.a = 'change'; // 修改成功
delete obj.b; // 删除成功

obj.__proto__.d = 'new proto val'

// 报错 #<Object> is not extensible at set __proto__ [as __proto__]
// obj.__proto__ = { 'oh': 'hello' }

console.log(obj); // {a: 'change', [[prototype]]: { 'oh': 'hello', ... }}

Object.seal() 封闭

不可新增属性,将所有现有属性标记为不可配置,不可删除原有属性,可以修改原有属性

let obj = {
  a: '1'
}
Object.seal(obj);
obj.b = 2;
delete obj.a;
console.log(obj); // {a: '1'}

Object.freeze()冻结 ☑️

无法新增 / 修改 / 删除属性,保证冻结的对象中的现有属性值是不可变

但是Object.preventExtensions / Object.seal / Object.freeze都存在两个问题:

  1. 可以通过改变原型对象,来为对象增加属性
  2. 目标对象内部的对象不会冻结
let obj = {
  a: '1',
  like: ['coding']
}
Object.freeze(obj);
obj.a = '2';
obj.c = 'proto val';
obj.like.push('liderder');
console.log(obj); // { a: '1', like: ['coding', 'liderder'], [[Prototype]]: { c: proto val, ... } }

可以手动实现一个冻结函数解决浅层冻结的问题

function deepFreeze(obj) {
  if (typeof obj != 'object') {
      throw Error('Type Error!')
  }
  Object.freeze(obj);
  for (const key in obj) {
      if (Object.hasOwnProperty.call(obj, key)) {
          if (typeof obj[key] === 'object') {
              deepFreeze(obj[key])
          }
      }
  }
}

但是仍然可以通过原型增加属性

还有什么其他方案吗?欢迎各位大佬指正