Attribute description object

Overview

JavaScript provides an internal data structure to describe the properties of an object and control its behavior, such as whether the property is writable, traversable, and so on. This internal data structure is called the "attributes object" (attributes object). Each attribute has its own corresponding attribute description object, which stores some meta-information of the attribute.

The following is an example of an attribute description object.

{
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false,
  get: undefined,
  set: undefined
}

The attribute description object provides 6 meta attributes.

(1) value

value is the attribute value of the attribute, and the default is undefined.

(2) writable

writable is a boolean value, indicating whether the value of the attribute can be changed (that is, whether it is writable), and the default is true.

(3) enumerable

enumerable is a boolean value indicating whether the attribute is traversable, and the default is true. If set to false, some operations (such as for...in loop, Object.keys()) will skip this attribute.

(4) configurable

configurable is a boolean value indicating configurability, and the default is true. If set to false, certain operations will be prevented from overwriting the attribute, such as being unable to delete the attribute, nor changing the attribute description object of the attribute (except for the value attribute). In other words, the configurable attribute controls the writability of the attribute description object.

(5) get

get is a function that represents the getter of the property, and the default is undefined.

(6) set

set is a function that represents the setter of the property, and the default is undefined.

Object.getOwnPropertyDescriptor()

The Object.getOwnPropertyDescriptor() method can get the property description object. Its first parameter is the target object, and the second parameter is a string corresponding to a property name of the target object.

var obj = { p: "a" };

Object.getOwnPropertyDescriptor(obj, "p");
// Object {value: "a",
// writable: true,
// enumerable: true,
// configurable: true
//}

In the above code, the Object.getOwnPropertyDescriptor() method gets the property description object of obj.p.

Note that the Object.getOwnPropertyDescriptor() method can only be used for the properties of the object itself, not for inherited properties.

var obj = { p: "a" };

Object.getOwnPropertyDescriptor(obj, "toString");
// undefined

In the above code, toString is a property inherited by the obj object, and Object.getOwnPropertyDescriptor() cannot be obtained.

Object.getOwnPropertyNames()

The Object.getOwnPropertyNames method returns an array. The members are the property names of all the properties of the parameter object itself, regardless of whether the property is traversable or not.

var obj = Object.defineProperties(
  {},
  {
    p1: { value: 1, enumerable: true },
    p2: { value: 2, enumerable: false },
  }
);

Object.getOwnPropertyNames(obj);
// ["p1", "p2"]

In the above code, obj.p1 is traversable, and obj.p2 is not traversable. Object.getOwnPropertyNames will return them all.

This is different from the behavior of Object.keys, which only returns all the property names of the traversable properties of the object itself.

Object.keys([]); // []
Object.getOwnPropertyNames([]); // ['length']

Object.keys(Object.prototype); // []
Object.getOwnPropertyNames(Object.prototype);
// ['hasOwnProperty',
//'valueOf',
//'constructor',
//'toLocaleString',
//'isPrototypeOf',
//'propertyIsEnumerable',
//'toString']

In the above code, the length property of the array itself is not traversable, and Object.keys will not return this property. The Object.prototype in the second example is also an object, all instance objects will inherit it, and its own properties are not traversable.

Object.defineProperty(), Object.defineProperties()

The Object.defineProperty() method allows to describe an object through properties, define or modify a property, and then return the modified object. Its usage is as follows.

Object.defineProperty(object, propertyName, attributesObject);

The Object.defineProperty method accepts three parameters, as follows.

-object: the object where the attribute is located -propertyName: string, representing the property name -attributesObject: attribute description object

For example, the definition of obj.p can be written as follows.

var obj = Object.defineProperty({}, "p", {
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false,
});

obj.p; // 123

obj.p = 246;
obj.p; // 123

In the above code, the Object.defineProperty() method defines the obj.p property. Since the writable property of the property description object is false, the obj.p property is not writable. Note that the first parameter of the Object.defineProperty method here is {} (a newly created empty object), the p property is directly defined on this empty object, and then this object is returned, which is Object. Common usage of defineProperty().

If the property already exists, the Object.defineProperty() method is equivalent to updating the property description object of the property.

If you define or modify multiple properties at once, you can use the Object.defineProperties() method.

var obj = Object.defineProperties(
  {},
  {
    p1: { value: 123, enumerable: true },
    p2: { value: "abc", enumerable: true },
    p3: {
      get: function () {
        return this.p1 + this.p2;
      },
      enumerable: true,
      configurable: true,
    },
  }
);

obj.p1; // 123
obj.p2; // "abc"
obj.p3; // "123abc"

In the above code, Object.defineProperties() defines three properties of the obj object at the same time. Among them, the p3 attribute defines the value function get, that is, every time the attribute is read, this value function is called.

Note that once the value function get (or the value storage function set) is defined, the writable attribute cannot be set to true, or the value attribute can be defined at the same time, otherwise an error will be reported.

var obj = {};

Object.defineProperty(obj, "p", {
  value: 123,
  get: function () {
    return 456;
  },
});
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value

Object.defineProperty(obj, "p", {
  writable: true,
  get: function () {
    return 456;
  },
});
// TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute

In the above code, if the get attribute and the value attribute are defined at the same time, and the writable attribute is set to true, an error will be reported.

The properties in the Object.defineProperty() and Object.defineProperties() parameters describe the object. The default values ​​of the three properties of writable, configurable, and enumerable are all false.

var obj = {};
Object.defineProperty(obj, "foo", {});
Object.getOwnPropertyDescriptor(obj, "foo");
// {
// value: undefined,
// writable: false,
// enumerable: false,
// configurable: false
//}

In the above code, an empty attribute description object is used when defining obj.foo, and you can see the default value of each meta attribute.

Object.prototype.propertyIsEnumerable()

The propertyIsEnumerable() method of the instance object returns a boolean value to determine whether a property is traversable. Note that this method can only be used to determine the properties of the object itself, and it will always return false for inherited properties.

var obj = {};
obj.p = 123;

obj.propertyIsEnumerable("p"); // true
obj.propertyIsEnumerable("toString"); // false

In the above code, obj.p is traversable, and obj.toString is an inherited property.

Meta attributes

The attributes of the attribute description object are called "meta attributes" because they can be regarded as attributes that control attributes.

value

The value attribute is the value of the target attribute.

var obj = {};
obj.p = 123;

Object.getOwnPropertyDescriptor(obj, "p").value;
// 123

Object.defineProperty(obj, "p", { value: 246 });
obj.p; // 246

The above code is an example of reading or rewriting obj.p through the value attribute.

writable

The writable attribute is a boolean value that determines whether the value of the target attribute can be changed.

var obj = {};

Object.defineProperty(obj, "a", {
  value: 37,
  writable: false,
});

obj.a; // 37
obj.a = 25;
obj.a; // 37

In the above code, the writable property of obj.a is false. Then, changing the value of obj.a will have no effect.

Note that in normal mode, assigning a value to the attribute with writable as false will not report an error, but will fail silently. However, an error will be reported in strict mode, even if the a attribute is re-assigned the same value.

"use strict";
var obj = {};

Object.defineProperty(obj, "a", {
  value: 37,
  writable: false,
});

obj.a = 37;
// Uncaught TypeError: Cannot assign to read only property'a' of object

The above code is in strict mode, and any assignment to obj.a will report an error.

If the writable of a property of the prototype object is false, then the child object will not be able to customize this property.

var proto = Object.defineProperty({}, "foo", {
  value: "a",
  writable: false,
});

var obj = Object.create(proto);

obj.foo = "b";
obj.foo; //'a'

In the above code, proto is the prototype object, and its foo property is not writable. The obj object inherits proto, and this property can no longer be customized. If it is strict mode, doing so will also throw an error.

However, there is a way to circumvent this limitation by overriding the attribute description object. The reason is that in this case, the prototype chain will be completely ignored.

var proto = Object.defineProperty({}, "foo", {
  value: "a",
  writable: false,
});

var obj = Object.create(proto);
Object.defineProperty(obj, "foo", {
  value: "b",
});

obj.foo; // "b"

enumerable

enumerable (traversability) returns a boolean value indicating whether the target attribute is traversable.

In earlier versions of JavaScript, the for...in loop was based on the in operator. We know that the in operator will return true regardless of whether an attribute is an object's own or inherited.

var obj = {};
"toString" in obj; // true

In the above code, toString is not a property of the obj object itself, but the in operator also returns true, which causes the toString property to be traversed by the for...in loop.

This is obviously unreasonable, and the concept of "traversability" was later introduced. Only the traversable properties will be traversed by for...in loop. At the same time, it also stipulates that the native properties inherited by instance objects of the type toString are not traversable. This ensures that for... Availability of in loop.

Specifically, if the enumerable of an attribute is false, the following three operations will not get the attribute.

-for..in loop -Object.keys method -JSON.stringify method

Therefore, enumerable can be used to set the "secret" attribute.

var obj = {};

Object.defineProperty(obj, "x", {
  value: 123,
  enumerable: false,
});

obj.x; // 123

for (var key in obj) {
  console.log(key);
}
// undefined

Object.keys(obj); // []
JSON.stringify(obj); // "{}"

In the above code, the enumerable of the obj.x property is false, so the general traversal operation can not get the property, making it a bit like a "secret" property, but it is not a real private property, it can still be obtained directly Its value.

Note that the for...in loop includes inherited properties, and the Object.keys method does not include inherited properties. If you need to get all the properties of the object itself, regardless of whether it can be traversed, you can use the Object.getOwnPropertyNames method.

In addition, the JSON.stringify method will exclude the attribute whose enumerable is false, which can sometimes be used. If you want to exclude certain attributes from the output of the object in JSON format, you can set the enumerable of these attributes to false.

configurable

configurable (configurability) returns a boolean value that determines whether the property description object can be modified. In other words, when configurable is false, value, writable, enumerable and configurable cannot be modified.

var obj = Object.defineProperty({}, "p", {
  value: 1,
  writable: false,
  enumerable: false,
  configurable: false,
});

Object.defineProperty(obj, "p", { value: 2 });
// TypeError: Cannot redefine property: p

Object.defineProperty(obj, "p", { writable: true });
// TypeError: Cannot redefine property: p

Object.defineProperty(obj, "p", { enumerable: true });
// TypeError: Cannot redefine property: p

Object.defineProperty(obj, "p", { configurable: true });
// TypeError: Cannot redefine property: p

In the above code, the configurable of obj.p is false. Then, I changed value, writable, enumerable, and configurable, and the result was an error.

Note that writable will report an error only when false is changed to true. Changing true to false is allowed.

var obj = Object.defineProperty({}, "p", {
  writable: true,
  configurable: false,
});

Object.defineProperty(obj, "p", { writable: false });
// Successfully modified

As for value, as long as one of writable and configurable is true, changes are allowed.

var o1 = Object.defineProperty({}, "p", {
  value: 1,
  writable: true,
  configurable: false,
});

Object.defineProperty(o1, "p", { value: 2 });
// Successfully modified

var o2 = Object.defineProperty({}, "p", {
  value: 1,
  writable: false,
  configurable: true,
});

Object.defineProperty(o2, "p", { value: 2 });
// Successfully modified

In addition, when writable is false, the target attribute is assigned directly without error, but it will not succeed.

var obj = Object.defineProperty({}, "p", {
  value: 1,
  writable: false,
  configurable: false,
});

obj.p = 2;
obj.p; // 1

In the above code, writable of obj.p is false, and direct assignment to obj.p will not take effect. If it is in strict mode, an error will be reported.

Configurability determines whether the target attribute can be deleted (delete).

var obj = Object.defineProperties(
  {},
  {
    p1: { value: 1, configurable: true },
    p2: { value: 2, configurable: false },
  }
);

delete obj.p1; // true
delete obj.p2; // false

obj.p1; // undefined
obj.p2; // 2

In the above code, the configurable of obj.p1 is true, so it can be deleted, but obj.p2 cannot be deleted.

Accessor

In addition to direct definition, attributes can also be defined with accessors. Among them, the stored value function is called setter, which uses attributes to describe the object's set property; the value function is called getter, which uses attributes to describe the object's get property.

Once the accessor is defined for the target attribute, the corresponding function will be executed when accessing it. With this function, many advanced features can be implemented, such as the behavior of reading and assigning custom attributes.

var obj = Object.defineProperty({}, "p", {
  get: function () {
    return "getter";
  },
  set: function (value) {
    console.log("setter: " + value);
  },
});

obj.p; // "getter"
obj.p = 123; // "setter: 123"

In the above code, obj.p defines the get and set attributes. When obj.p gets a value, it calls get; when it assigns a value, it calls set.

JavaScript also provides another way to write accessors.

// Writing method two
var obj = {
  get p() {
    return "getter";
  },
  set p(value) {
    console.log("setter: " + value);
  },
};

In the above two ways of writing, although the reading and assignment behavior of the attribute p is the same, there are some subtle differences. The first way of writing, the configurable and enumerable of the attribute p are both false, which results in the attribute p being non-traversable; the second way of writing, the configurable and enumerable Both are true, so the attribute p` is traversable. In actual development, writing method two is more commonly used.

Note that the value function get cannot accept parameters, and the stored value function set can only accept one parameter (that is, the value of the property).

Accessors are often used when the value of an attribute depends on the internal data of the object.

var obj = {
  $n: 5,
  get next() {
    return this.$n++;
  },
  set next(n) {
    if (n >= this.$n) this.$n = n;
    else
      throw new Error("The new value must be greater than the current value");
  },
};

obj.next; // 5

obj.next = 10;
obj.next; // 10

obj.next = 5;
// Uncaught Error: The new value must be greater than the current value

In the above code, the storage and retrieval functions of the next attribute both depend on the internal attribute $n.

Copy of object

Sometimes, we need to copy all the attributes of an object to another object, which can be achieved by the following method.

var extend = function (to, from) {
  for (var property in from) {
    to[property] = from[property];
  }

  return to;
};

extend(
  {},
  {
    a: 1,
  }
);
// {a: 1}

The problem with the above method is that if the property defined by the accessor is encountered, only the value will be copied.

extend(
  {},
  {
    get a() {
      return 1;
    },
  }
);
// {a: 1}

To solve this problem, we can copy properties through the Object.defineProperty method.

var extend = function (to, from) {
  for (var property in from) {
    if (!from.hasOwnProperty(property)) continue;
    Object.defineProperty(
      to,
      property,
      Object.getOwnPropertyDescriptor(from, property)
    );
  }

  return to;
};

extend(
  {},
  {
    get a() {
      return 1;
    },
  }
);
// {get a(){ return 1} })

In the above code, the line hasOwnProperty is used to filter out inherited properties, otherwise an error may be reported because Object.getOwnPropertyDescriptor cannot read the property description object of inherited properties.

Control object status

Sometimes it is necessary to freeze the read and write status of an object to prevent the object from being changed. JavaScript provides three freezing methods, the weakest one is Object.preventExtensions, the second is Object.seal, and the strongest is Object.freeze.

Object.preventExtensions()

The Object.preventExtensions method can make an object unable to add new properties.

var obj = new Object();
Object.preventExtensions(obj);

Object.defineProperty(obj, "p", {
  value: "hello",
});
// TypeError: Cannot define property:p, object is not extensible.

obj.p = 1;
obj.p; // undefined

In the above code, after the obj object passes through Object.preventExtensions, new properties cannot be added.

Object.isExtensible()

The Object.isExtensible method is used to check whether an object uses the Object.preventExtensions method. In other words, check whether you can add attributes to an object.

var obj = new Object();

Object.isExtensible(obj); // true
Object.preventExtensions(obj);
Object.isExtensible(obj); // false

In the above code, after using the Object.preventExtensions method on the obj object, use the Object.isExtensible method again to return false, which means that no new properties can be added.

Object.seal()

The Object.seal method makes it impossible for an object to add new properties, nor delete old properties.

var obj = { p: "hello" };
Object.seal(obj);

delete obj.p;
obj.p; // "hello"

obj.x = "world";
obj.x; // undefined

In the above code, after the obj object executes the Object.seal method, it is impossible to add new properties and delete old properties.

The essence of Object.seal is to set the configurable property of the property description object to false, so the property description object can no longer be changed.

var obj = {
  p: "a",
};

// before the seal method
Object.getOwnPropertyDescriptor(obj, "p");
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: true
//}

Object.seal(obj);

// after the seal method
Object.getOwnPropertyDescriptor(obj, "p");
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: false
//}

Object.defineProperty(obj, "p", {
  enumerable: false,
});
// TypeError: Cannot redefine property: p

In the above code, after using the Object.seal method, the configurable property of the property description object becomes false, and then an error will be reported if the enumerable property is changed.

Object.seal only prohibits adding or deleting attributes, and does not affect the modification of the value of an attribute.

var obj = { p: "a" };
Object.seal(obj);
obj.p = "b";
obj.p; //'b'

In the above code, the Object.seal method is invalid for the value of the p attribute, because at this time the writability of the p attribute is determined by writable.

Object.isSealed()

The Object.isSealed method is used to check whether an object uses the Object.seal method.

var obj = { p: "a" };

Object.seal(obj);
Object.isSealed(obj); // true

At this time, the Object.isExtensible method also returns false.

var obj = { p: "a" };

Object.seal(obj);
Object.isExtensible(obj); // false

Object.freeze()

The Object.freeze method can make an object unable to add new properties, unable to delete old properties, and unable to change the value of the property, making this object actually a constant.

var obj = {
  p: "hello",
};

Object.freeze(obj);

obj.p = "world";
obj.p; // "hello"

obj.t = "hello";
obj.t; // undefined

delete obj.p; // false
obj.p; // "hello"

In the above code, after performing Object.freeze() on the obj object, modifying attributes, adding attributes, and deleting attributes are invalid. These operations do not report errors, but fail silently. If it is in strict mode, an error will be reported.

Object.isFrozen()

The Object.isFrozen method is used to check whether an object uses the Object.freeze method.

var obj = {
  p: "hello",
};

Object.freeze(obj);
Object.isFrozen(obj); // true

After using the Object.freeze method, Object.isSealed will return true, and Object.isExtensible will return false.

var obj = {
  p: "hello",
};

Object.freeze(obj);

Object.isSealed(obj); // true
Object.isExtensible(obj); // false

One purpose of Object.isFrozen is to assign values ​​to its properties after confirming that an object is not frozen.

var obj = {
  p: "hello",
};

Object.freeze(obj);

if (!Object.isFrozen(obj)) {
  obj.p = "world";
}

In the above code, after confirming that obj is not frozen, then assign a value to its attribute, and no error will be reported.

Limitations

There is a loophole in the above three methods to lock the writability of the object: you can add properties to the object by changing the prototype object.

var obj = new Object();
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
proto.t = "hello";
obj.t;
// hello

In the above code, the object obj itself cannot add attributes, but you can add attributes to its prototype object, and it can still be read on obj.

One solution is to freeze the prototype of obj.

var obj = new Object();
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
Object.preventExtensions(proto);

proto.t = "hello";
obj.t; // undefined

Another limitation is that if the attribute value is an object, the above methods can only freeze the object pointed to by the attribute, but not the content of the object itself.

var obj = {
  foo: 1,
  bar: ["a", "b"],
};
Object.freeze(obj);

obj.bar.push("c");
obj.bar; // ["a", "b", "c"]

In the above code, the obj.bar property points to an array. After the obj object is frozen, this point cannot be changed, that is, it cannot point to other values, but the pointed array can be changed.