对象 -- Javascript基础探究篇(2)

396 阅读6分钟

js中万物皆对象?其实这并不是完全正确的,js中的简单基本类型(stringnumberbooleanundefinednullsymbol)本身并不是对象。但对象确实是js的基础。

js还有很多特殊的对象子类型,我们可以称之为复杂基本类型。如常见的函数和数组就是对象的子类型。它们的本质和普通的对象一样,只有多了一些特殊功能。

js中还具有一些内置对象,如StringNumberBooleanObjectFunctionArrayDateRegExpError等。可以看到有些内置类型和简单基本类型名字和很相似。

const strPrimitive = "primitive string";
console.log(typeof strPrimitive); // string

const strObject = new String("object string");
console.log(typeof strObject); // object

上述代码中strPrimitive并不是一个对象,它只是一个不可变的字面量。如果需要对它进行一些操作,如lengthslice等等,js会自动将它转换为一个String对象,从而获取这些方法和属性。所以我们并不需要使用构造形式创建一个简单基本类型。

声明方式

对象通常有两种声明方式,文字形式和构造形式:

const obj1 = {}; // 文字形式
const obj2 = new Object(); // 构造形式

属性

对象属性访问有两个形式,属性访问和键访问:

const object = {
  a: 2,
};

console.log(object.a); // 属性访问
console.log(object["a"]); // 键访问

两种语法的区别是:

  • 属性访问:要求属性名满足标识符的命名规范(标识符是指由一个字母开头,其后选择性的加上一个或者多个字母,数字或者下划线
  • 键访问:可以接受任意UTF-8/Unicode字符串作为属性名

在对象中,属性名永远都是字符串,即使我们使用其他类型值作为属性名,也会首先被转换为一个字符串,然后使用。如数组下标中使用的的确是数字,但是这些数字属性名还是会被先转换为字符串再使用。

const object = {};

object[true] = "foo"; // 等价于 object["true"] = "foo"
object[3] = "bar";   // 等价于 object["3"] = "bar"
object[object] = "baz"; // 等价于 object["[object Object]"] = "baz"

属性描述符

从ES5开始,对象的每个属性都具备属性描述符,默认的属性描述符包含四部分:value(属性值),writable(是否可写),enumerable(是否可枚举),configurable(是否可配置)。还可以拓展增加gettersetter(存取器)。

const obj = {
  a: 2,
};

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

在我们平常创建对象属性时,属性描述符除value外会使用默认值(即writable: true, enumerable: true, configurable: true)。我们也可使用Object.defineProperty手动修改属性描述符:

const obj = {};

Object.defineProperty(obj, "a", {
  value: 20,
  writable: true,
  enumerable: true,
  configurable: true,
});
// 等价于 obj.a = 20;

使用Object.defineProperty修改一个现有属性时,会将修改后的描述符和修改前的进行合并。

使用Object.defineProperty新增一个属性时可以省略一些属性描述符。如果省略value,则value默认值为undefined,其他描述符会以默认值false填充(和默认值刚好相反):

const obj = {
  a: 10,
  b: 20,
};

Object.defineProperty(obj, "b", {
  enumerable: false,
});
// b 等价于 {value: 20, writable: true, enumerable: false, configurable: true}

Object.defineProperty(obj, "c", {
  enumerable: true,
});
// c 等价于 {value: undefined, writable: false, enumerable: true, configurable: false}

writable

writable决定属性的值是否可修改。如果设置为false,在非严格模式会静默失败,在严格模式会报错。

"use strict";

const obj = {};
Object.defineProperty(obj, "a", {
  value: 20,
  writable: false,
  enumerable: true,
  configurable: true,
});

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

writablefalse时,只会阻止=赋值,使用Object.defineProperty修改属性值不会受到影响。

const obj = {};
Object.defineProperty(obj, "a", {
  value: 20,
  writable: false,
  configurable: true,
});

Object.defineProperty(obj, "a", {
  value: 30,
});
console.log(obj.a); // 30

configurable

configurable决定属性描述符是否可配置。如果设置为true,则可以使用Object.defineProperty修改属性描述符。

"use strict";

const obj = {};
Object.defineProperty(obj, "a", {
  value: 20,
  writable: true,
  enumerable: true,
  configurable: false,
});

Object.defineProperty(obj, "a", {
  value: 20,
  writable: true,
  enumerable: true,
  configurable: true,
}); // TypeError: Cannot redefine property: a

configurable一旦由true变为了false,就无法再改为true,这是一个单向操作。注意:即使是在configurablefalse的情况下,也可以把writabletrue变为false(反之报错,同样是单向操作)

configurable配置为false的属性无法使用delete删除,同样在非严格模式会静默失败,在严格模式会报错。

"use strict";

const obj = {};
Object.defineProperty(obj, "a", {
  value: 20,
  writable: true,
  enumerable: true,
  configurable: false,
});

delete obj.a; // TypeError: Cannot delete property 'a'

enumerable

enumerable决定属性是否会出现在对象属性枚举中,如for...in...循环中。如果设置为false则不会出现在枚举中,但仍然可以正常的访问。

const obj = {
  a: 10,
  b: 20,
};
Object.defineProperty(obj, "c", {
  value: 30,
  writable: true,
  enumerable: false,
  configurable: true,
});

for (const key in obj) {
  console.log(key); // a, b
}

console.log(obj.c); // 30

我们可以使用propertyIsEnumerable判断对象中的某个属性是否是可遍历的:

const obj = {
  a: 10,
};

console.log(obj.propertyIsEnumerable("a")); // true

Object.keys会返回对象包含的所有可枚举属性;Object.getOwnPropertyNames会返回对象包含的所有属性,无论它们是否可枚举。

gettersetter

当一个属性具有gettersetter时,该属性也被称为访问描述符。对于访问描述符来说,js会忽略它的valuewritable,只关心gettersetter(以及enumberableconfigurable)。如果存在getter或者setter,就不能同时存在valuewritable,否则会报错。

const obj = {
  get a() {
    return 10;
  },
};

Object.defineProperty(obj, "b", {
  get: function () {
    return this.a * 2;
  },
});
console.log(obj.a, obj.b); //10, 20

无论是在对象中使用get a()还是使用Object.defineProperty,它们实现的效果是一致的,都是添加一个getter。目前,属性a或者b都只有getter,并且返回值是固定的,无法修改属性值;如果想要修改属性值,则需要添加setter:

const obj = {
  get a() {
    return this._a;
  },
  set a(value) {
    this._a = value;
  },
};

Object.defineProperty(obj, "b", {
  get: function () {
    return this._b;
  },
  set: function (value) {
    this._b = value;
  },
});

obj.a = 30;
obj.b = 40; 
console.log(obj.a, obj.b);  // 30, 40

进阶用法

常量属性

如果将一个属性的描述符改为writable:falseconfigurable:false,那么该属性就是一个常量属性(不可修改、重定义、或者删除)

禁止拓展

使用Object.preventExtensions可以禁止一个对象添加新属性。

密封

使用Object.seal可以创建一个密封对象,密封后不仅不能添加新属性,也无法重新配置或者删除现有属性。本质就是对现有对象先调用Object.preventExtensions,然后把现有的所有属性的configurable都设为false

冻结

使用Object.freeze可以创建一个冻结对象,它是在密封的基础上,再把现有的所有属性的writable都设为false,这就意味着该对象在密封的基础上也无法修改现有属性的值。