深入探討 JavaScript
請支持這本書:購買捐款
(廣告,請不要封鎖。)

10 保護物件不被變更



在本章中,我們將探討如何保護物件不被變更。範例包括:防止新增屬性以及防止屬性被變更。

  所需知識:屬性屬性

對於本章,你應熟悉屬性屬性。如果你不熟悉,請查看§9「屬性屬性:簡介」

10.1 保護層級:防止擴充、密封、凍結

JavaScript 有三種保護物件的層級

10.2 防止物件擴充

Object.preventExtensions<T>(obj: T): T

此方法的運作方式如下

讓我們在範例中使用 Object.preventExtensions()

const obj = { first: 'Jane' };
Object.preventExtensions(obj);
assert.throws(
  () => obj.last = 'Doe',
  /^TypeError: Cannot add property last, object is not extensible$/);

不過,我們仍然可以刪除屬性

assert.deepEquals(
  Object.keys(obj), ['first']);
delete obj.first;
assert.deepEquals(
  Object.keys(obj), []);

10.2.1 檢查物件是否可延伸

Object.isExtensible(obj: any): boolean

檢查 obj 是否可延伸,例如

> const obj = {};
> Object.isExtensible(obj)
true
> Object.preventExtensions(obj)
{}
> Object.isExtensible(obj)
false

10.3 封裝物件

Object.seal<T>(obj: T): T

此方法的說明

下列範例示範封裝會讓物件無法延伸,且其屬性不可設定。

const obj = {
  first: 'Jane',
  last: 'Doe',
};

// Before sealing
assert.equal(Object.isExtensible(obj), true);
assert.deepEqual(
  Object.getOwnPropertyDescriptors(obj),
  {
    first: {
      value: 'Jane',
      writable: true,
      enumerable: true,
      configurable: true
    },
    last: {
      value: 'Doe',
      writable: true,
      enumerable: true,
      configurable: true
    }
  });

Object.seal(obj);

// After sealing
assert.equal(Object.isExtensible(obj), false);
assert.deepEqual(
  Object.getOwnPropertyDescriptors(obj),
  {
    first: {
      value: 'Jane',
      writable: true,
      enumerable: true,
      configurable: false
    },
    last: {
      value: 'Doe',
      writable: true,
      enumerable: true,
      configurable: false
    }
  });

我們仍然可以變更屬性 .first 的值

obj.first = 'John';
assert.deepEqual(
  obj, {first: 'John', last: 'Doe'});

但我們無法變更其屬性

assert.throws(
  () => Object.defineProperty(obj, 'first', { enumerable: false }),
  /^TypeError: Cannot redefine property: first$/);

10.3.1 檢查物件是否已封裝

Object.isSealed(obj: any): boolean

檢查 obj 是否已封裝,例如

> const obj = {};
> Object.isSealed(obj)
false
> Object.seal(obj)
{}
> Object.isSealed(obj)
true

10.4 凍結物件

Object.freeze<T>(obj: T): T;
const point = { x: 17, y: -5 };
Object.freeze(point);

assert.throws(
  () => point.x = 2,
  /^TypeError: Cannot assign to read only property 'x'/);

assert.throws(
  () => Object.defineProperty(point, 'x', {enumerable: false}),
  /^TypeError: Cannot redefine property: x$/);

assert.throws(
  () => point.z = 4,
  /^TypeError: Cannot add property z, object is not extensible$/);

10.4.1 檢查物件是否已凍結

Object.isFrozen(obj: any): boolean

檢查 obj 是否已凍結,例如

> const point = { x: 17, y: -5 };
> Object.isFrozen(point)
false
> Object.freeze(point)
{ x: 17, y: -5 }
> Object.isFrozen(point)
true

10.4.2 凍結是淺層的

Object.freeze(obj) 只會凍結 obj 及其屬性。它不會凍結這些屬性的值,例如

const teacher = {
  name: 'Edna Krabappel',
  students: ['Bart'],
};
Object.freeze(teacher);

// We can’t change own properties:
assert.throws(
  () => teacher.name = 'Elizabeth Hoover',
  /^TypeError: Cannot assign to read only property 'name'/);

// Alas, we can still change values of own properties:
teacher.students.push('Lisa');
assert.deepEqual(
  teacher, {
    name: 'Edna Krabappel',
    students: ['Bart', 'Lisa'],
  });

10.4.3 實作深度凍結

如果我們要深度凍結,我們需要自己實作

function deepFreeze(value) {
  if (Array.isArray(value)) {
    for (const element of value) {
      deepFreeze(element);
    }
    Object.freeze(value);
  } else if (typeof value === 'object' && value !== null) {
    for (const v of Object.values(value)) {
      deepFreeze(v);
    }
    Object.freeze(value);
  } else {
    // Nothing to do: primitive values are already immutable
  } 
  return value;
}

重新檢視前一節的範例,我們可以檢查 deepFreeze() 是否真的深度凍結

const teacher = {
  name: 'Edna Krabappel',
  students: ['Bart'],
};
deepFreeze(teacher);

assert.throws(
  () => teacher.students.push('Lisa'),
  /^TypeError: Cannot add property 1, object is not extensible$/);

10.5 進一步閱讀