对象
1. 对象简介
- 对象就是一个属性的无序集合,每个属性都有名和值。属性名通常是字符串,也可以是
Symbol
。 - JavaScript 对象也可以从其它对象继承属性,这个其它对象叫做“原型”。
- JavaScript 对象是动态的,可以动态地添加和删除属性。
- 在 JavaScript 中,任何不是字符串、数值、符号或
true
、false
、null
、undefined
的值都是对象。 - 对象不能包含两个同名属性。
- 除名字和值外,每个属性还有 3 个特性:
writeable
:可写特性,指定是否可以设置属性的值。enumerable
:可枚举特性,指定是否可以在for/in
循环中返回属性的名字。configurable
:可配置特性,指定是否可以删除属性,以及是否可修改其特性。
2. 创建对象
2.1 对象字面量
- 对象字面量最简形式就是包含在一对大括号中的键值对。
- 对象字面量是一个表达式,每次求值都会创建并初始化一个新的、不一样的对象。
- 字面量每次被求值的时候,它的每个属性的值也会被求值。
let empty = {}
let point = { x: 0, y: 0 }
let p2 = {
x: point.x,
y: point.y
}
2.2 使用 new 创建对象
new
操作符用于创建和初始化一个新对象。new
关键字后面必须跟一个构造函数(constructor)。
let o = new Object(); // 创建一个空对象
let a = new Array(); // 创建一个空数组
let d = new Date(); // 创建一个当前时间的日期对象
2.3 原型
- 几乎每个 JavaScript 对象都有与之关联的另一个对象,该对象称为原型(prototype)。
- 通过字面量创建的所有对象都有相同的原型对象,可以通过
Object.prototype
引用这个原型对象。 - 通过
new
关键字和构造函数创建的对象,使用构造函数的prototype
属性值作为它们的原型,如通过new Object()
创建的对象,使用Object.prototype
作为原型,使用new Array()
创建的对象,使用Array.prototype
作为原型。 Object.prototype
是为数不多没有原型的对象,因为它不继承任何属性。其它原型对象都是常规对象,都有自己的原型。
注意
几乎所有对象都有原型,但只有少数对象有 prototype
属性。
2.4 Object.create()
Object.create()
用于创建一个新对象,并 使用第一个参数作为新对象的原型:
let o1 = Object.create({x: 1, y: 2}) // o1 继承属性 x 和 y
o1.x + o1.y // 3
传入 null
可以创建一个没有原型的对象。该对象不会继承任何属性,连 toString()
方法都没有。
如果想创建一个普通的空对象,类似于 {}
或 new Object()
返回的对象,传入 Object.prototype
即可:
let o2 = Object.create(Object.prototype)
Object.create()
可以接收可选的第二个参数,用于描述新对象的属性。
Object.create()
的一个用途是防止对象被第三方函数不小心修改,此时不要把对象直接传给函数,而要传入一个继承自它的对象。设置该对象的属性不会影响原始对象:
let o = { x: "don't change this value" }
library.function(Object.create(o)) // 防止意外修改
3. 查询和设置属性
要查询一个属性的值,可以使用 .
或 []
操作符,点操作符右边必须是一个命名属性的简单标识符,如果使用方括号,方括号中的表达式必须能求值为一个字符串或一个可以转换为字符串或符号的值。
要设置一个属性,与查询一样,同样使用点操作符或方括号操作符。
3.1 作为关联数组的对象
关联数组:以字符串而非数值作为索引,类似于数组的对象,如散列表、映射、字典。
JavaScript 对象是关联数组,这种方式增加了访问对象属性的灵活性。因为通过点操作符访问属性,属性名是标识符,不能动态操作,而通过方括号,属性名通过字符串表示,可以动态操作:
let addr = ''
for (let i = 0; i < 4; i++) {
addr += customer[`address${i}`] + '\n';
}
3.2 继承
- 每次通过
new
操作符创建一个类的实例,都会创建从某个原型对象继承属性的对象。 - 查询某个对象的属性时,如果不是自有属性,则会从原型对象上找,仍未找到,继续向上一级原型对象查找,这种链式结构也叫做原型链。
- 赋值某个可拓展对象的属性时,会查询原型链确定是否允许赋值。如果是可改自有属性,则直接修改;如果是继承的只读属性,则不允许赋值;否则,会隐藏继承的属性,在对象上创建一个新属性。
let unitcircle = { r: 1 };
let c = Object.create(unitcircle); // c继承了属性r
c.x = 1;
c.y = 1; // c定义了两个自有属性
c.r = 2; // 覆盖了继承的属性r
console.log(unitcircle.r); // 1,原型不受影响
注意
- 查询属性时会用到原型链,而设置属性时不影响原型链。
- 如果对象 o 继承了一个通过设置方法定义的访问器属性,赋值该属性时,对象 o 会调用该设置方法,而不是直接创建新属性。
4. 删除属性
delete
操作符用于从对象中移除属性,它唯一的操作数应该是一个属性访问表达式。delete
操作符只删除自有属性,不删除继承的属性。delete
不会删除configurable
特性为false
的属性(不可配置属性),如通过变量或函数声明的全局变量上的属性。
delete Object.prototype // 属性不可配置,不能删除
var x = 1; // 声明为全局变量
delete globalThis.x // 不能删除
5. 测试属性
- 可以使用
in
、hasOwnProperty()
或propertyIsEnumerable()
查询对象是否有某一个属性。 in
操作符可以查询对象的自有属性和继承属性,要求左边是属性名,右边是一个对象。hasOwnProperty()
方法用于查询对象的自有属性,对于继承的属性,返回false
。propertyIsEnumerable()
方法查询对象的自有属性,且要求属性有enumerable
特性,是可枚举的。
let o = { x: 1}
o.propertyIsEnumerable("x") // true
o.propertyIsEnumerable("toString") // false,toString不是自有属性
6. 枚举属性
for/in
循环可以遍历对象的可枚举(自有或继承)属性,对象继承的内置方法不可枚举,代码给对象添加的属性默认可枚举。
可以获得属性名数组的 4 个方法:
Object.keys()
:返回对象可枚举的自有属性,不包含名字是符号的属性、不可枚举属性和继承属性。Object.getOwnPropertyNames()
:与Object.keys()
类似,区别在于不可枚举的自有属性也会返回,只要属性名是字符串。Object.getOwnPropertySymbols()
:返回名字是符号的自有属性,无论是否可枚举。Reflect.ownKeys()
:返回所有自有属性,无论是否可枚举,包括符号属性,相当于Object.getOwnPropertyNames()
和Object.getOwnPropertySymbols()
取并集。
7. 拓展对象
Object.assign()
接收两个或多个对象作为其参数,第一个参数是目标对象,后端参数是来源对象。- 对每个来源对象,会把该对象的可枚举自有属性(包括符号属性)复制到目标对象身上。
- 按参数列表顺序进行复制,对同名的属性,后者覆盖前者。
- 如果一个来源对象有获取方法或者目标对象有设置方法,复制期间这些方法会被调用,但方法本身不会被复制。
8. 序列化对象
- 对象序列化是把对象状态转换为字符串的过程,之后可以从中恢复对象的状态。
- 函数
JSON.stringify()
和JSON.parse()
用于序列化和恢复 JavaScript 对象。 - 可以序列化的值包括对象,数组,字符串,有限数值,
true
,false
和null
。NaN
、Infinity
、-Infinity
会被序列化为null
。 - 函数、
undefined
、RegExp
和Error
对象不能被序列化或恢复。 JSON.stringify()
只序列化对象的可枚举自有属性。
let o = { x: 1, y: {z: [false, null, '']} }
let s = JSON.stringify(o) // s == '{ "x": 1, "y": {"z": [false, null, '']]} }'
let p = JSON.parse(s) // p == { x: 1, y: {z: [false, null, '']} }
9. 对象方法
9.1 toString() 方法
toString()
方法不接受参数,返回调用它的对象的字符串表示。每当需要把一个对象转为字符串时,就会调用该方法。- 默认的
toString()
不会包含很多信息,通常会被重写。
let point = {
x: 1,
y: 2,
toString: function() {
return `${this.x}, ${this.y}`
}
}
String(point) // 1,2
9.2 toLocalString() 方法
toLocalString()
方法返回对象的本地化字符串表示。- Object 定义的默认
toLocalString()
没有实现任何本地化,而是简单调用toString()
并返回。 Date
和Number
定义了自己的toLocalString()
,可以按照本地惯例格式化数值、日期和时间。
9.3 valueOf() 方法
valueOf()
方法在需要把对象转为非字符串的原始值时被调用。- Object 定义的默认
valueOf()
会返回对象本身,通常需要重写该方法以返回原始值。
let point = {
x: 3,
y: 4,
valueOf: function () {
return Math.hypot(this.x, this.y);
},
};
Number(point) // 5
point > 4 // true
9.4 toJSON() 方法
Object.prototype
上并未定义 toJSON()
方法,但调用 JSON.stringify()
序列化对象时会从原型上查找该方法,如果存在该方法,则会序列化该方法的返回值,而不是原始对象。
10. 对象字面量拓展语法
10.1 简写属性
ES6 之后,如果属性名跟变量名相同,可以采用简写形式:
let x = 1
let obj = { x }
10.2 计算的属性名
当属性名需要通过变量或函数求值得到时,ES6 之前只能动态添加该属性,ES6 之后可以直接在对象字面量中计算属性名:
const PROPERTY_NAME = "p1";
function computePropertyName() {
return "p" + 2;
}
// 计算的属性名
let p = {
[PROPERTY_NAME]: 1,
[computePropertyName()]: 2,
};
p.p1 + p.p2 // 3
可以在方括号中加入任意表达式,求值结果会作用到属性名。
10.3 符号作属性名
将符号赋值给变量或常量,可以用计算属性语法将该符号作为属性名:
const extension = Symbol("my extension symbol")
let o = {
[extension]: {}
}
o[extension].x = 1
- 符号除了作为属性名,不能用它们做任何事。
- 符号是原始值,不是对象,因此
Symbol()
不是构造函数,不能使用new
调用。 Symbol()
返回的值不等于其它任何符号或其它值。- 使用符号不是为了安全,而是为对象定义安全的拓展机制。
10.4 拓展操作符
利用拓展操作符可以把已有对象的属性复制到新对象中:
let position = { x: 1, y: 2 }
let demensions = { width: 100, height: 200 }
let rect = { ...position, ...demensions }
rect.x + rect.width // 101
注意
- 如果拓展对象和被拓展对象有一个同名属性,那么这个属性的值由后面的对象确定。
- 拓展操作符只拓展对象的自有属性,不拓展任何继承属性。
10.5 简写方法
- 函数作为对象属性时称为方法。
- ES6 之后,允许一种省略
function
关键字的简写方法:
// ES6 之前
let square = {
area: function() {
return this.side * this.side;
},
side: 10
}
// 简写形式
let square = {
area() {
return this.side * this.side;
},
// 属性名可以是字符串、计算属性
"printArea"() {
console.log(this.side);
},
side: 10
}
说明
- 使用简写形式定义方法,属性名可以是字面量允许的任何形式,除了标识符,还可以是字符串字面量和计算的属性名。
10.6 属性的访问方法和设置方法
- 对象定义访问器属性是一个或两个访问器方法:一个获取方法(getter)和一个设置方法(setter)。
- 当程序访问访问器属性的值时,会调用获取方法(不传参数),该方法返回的值就是属性的值;当设置一个访问器属性的值时,会调用设置方法,该方法会设置属性的值,方法的返回值会被忽略。
- 如果一个属性既有获取方法又有设置方法,则该属性是一个可读写属性;如果只有获取方法,则是只读属性;如果只有设置方法,则是只写属性,获取该属性会返回
undefined
。 - 访问器属性通过一个或两个方法定义,方法名就是属性名。除了前缀是
get
和set
之外,看起来与简写方法没有区别。 - 与普通属性一样,访问器属性是可以继承的。
- 使用场景:写入属性时进行合理性检查,每次读取返回不同的值等。
let p = {
x: 1.0,
y: 1.0,
// r是可读写访问器属性
get r() {
return Math.hypot(this.x, this.y);
},
set r(newValue) {
let oldValue = Math.hypot(this.x, this.y);
let ratio = newValue / oldValue;
this.x *= ratio;
this.y *= ratio;
},
// 只读访问器属性
get theta() {
return Math.atan2(this.y, this.x);
},
};