js中万物皆对象?其实这并不是完全正确的,js中的简单基本类型(string,number,boolean,undefined,null,symbol)本身并不是对象。但对象确实是js的基础。
js还有很多特殊的对象子类型,我们可以称之为复杂基本类型。如常见的函数和数组就是对象的子类型。它们的本质和普通的对象一样,只有多了一些特殊功能。
js中还具有一些内置对象,如String,Number,Boolean,Object,Function,Array,Date,RegExp,Error等。可以看到有些内置类型和简单基本类型名字和很相似。
const strPrimitive = "primitive string";
console.log(typeof strPrimitive); // string
const strObject = new String("object string");
console.log(typeof strObject); // object
上述代码中strPrimitive并不是一个对象,它只是一个不可变的字面量。如果需要对它进行一些操作,如length,slice等等,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(是否可配置)。还可以拓展增加getter和setter(存取器)。
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
writable为false时,只会阻止=赋值,使用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,这是一个单向操作。注意:即使是在configurable为false的情况下,也可以把writable由true变为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会返回对象包含的所有属性,无论它们是否可枚举。
getter和setter
当一个属性具有getter和setter时,该属性也被称为访问描述符。对于访问描述符来说,js会忽略它的value和writable,只关心getter和setter(以及enumberable和configurable)。如果存在getter或者setter,就不能同时存在value和writable,否则会报错。
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:false和configurable:false,那么该属性就是一个常量属性(不可修改、重定义、或者删除)
禁止拓展
使用Object.preventExtensions可以禁止一个对象添加新属性。
密封
使用Object.seal可以创建一个密封对象,密封后不仅不能添加新属性,也无法重新配置或者删除现有属性。本质就是对现有对象先调用Object.preventExtensions,然后把现有的所有属性的configurable都设为false。
冻结
使用Object.freeze可以创建一个冻结对象,它是在密封的基础上,再把现有的所有属性的writable都设为false,这就意味着该对象在密封的基础上也无法修改现有属性的值。