ECMA-262将对象定义为:一组属性的无序集合。 我们可以把JS中的对象想象成一张散列表,其中的内容就是一组键值对,值的类型可以是数据或者函数。
一、理解对象
创建自定义对象的通常方式是 new
一个 Object
的新实例,然后再给这个实例添加属性和方法。
let person = new Object();
person.name = 'Macc';
person.age = 18;
person.sayHi = function(){
console.log('hi');
}
⭐现在更流行的是对象字面量方式定义对象
//对象字面量
let person = {
name:'Macc',
age:16,
sayHi:function(){
console.log('hi');
}
}
上面两个对象的属性和方法都一样,可视为两者是等价的(注意,是等价而不是相同或者说是同一个对象)。
对象的属性都有自己的特征,而这些特征决定了它们在JS中的行为。
(一)属性的类型
ECMA-262用一些内部特性来描述属性的特征。既然是内部特性,也就是说开发者在JS中是不能直接访问这些特性的。
规范使用两个中括号括住特性的名称,以此来标识其为内部特性。例如:[[Enumberable]]
对象的属性分为两种:数据属性和访问器属性。
1.数据属性
数据属性有4个特性描述其行为
特性名称 | 作用 | 默认值 |
---|---|---|
[[Configurable]] | 表示属性是否可通过 delete 删除并重新定义,是否可以修改属性的特性,是否可以把属性类型改为访问器属性。 | true |
[[Enumberable]] | 属性是否可以通过 for-in 循环返回。 | true |
[[Writable]] | 属性的值是否可以被修改。 | true |
[[value]] | 属性实际值的存放位置,读取和写入属性值的都是操作这个位置。 | undefined |
修改默认(内部)特性
上面提到过,开发者在js中是无法直接访问内部特性的。所以要修改属性的内部特性,必须要使用 Object.defineProperty()
方法。
Object.defineProperty(obj,‘propertyName’,decriptionObj)方法
参数名 | 参数类型 | 描述 |
---|---|---|
obj | Object | 要添加/修改属性的对象 |
propertyName | String | 要添加或修改的属性的名称 |
decriptionObj | Object | 描述符对象 |
描述符对象上的属性可以包含4个内部特性名称(即以内部特性的名称为属性名)。
let person = {};//定义一个person对象
//给对象添加属性
Object.defineProperty(person,'name',{
writable:false, //属性值不可修改
value:'Macc' //属性的实际值
});
console.log(person.name);//访问属性值,输出'Macc'
person.name = 'GaGa';//尝试修改属性的值
console.log(person.name);//输出'Macc'
因为person
对象的name
属性的内部特性writable
被改为false
了,表示属性的值不可以被修改,因此后面尝试将其改为GaGa的时候,修改行为被忽略(在严格模式下会报错),依旧输出原值。
若把属性的configurable
属性设为false
,则不能再改回true
,此时再调用Object.defineProperty()
方法并修改任何非writable
属性都会报错。
调用Object.defineProperty()
时,configurable、enumerable、writable
的值,若不指定,默认为false
。
let person = {
name: 'Macc',
age: 18
}
Object.defineProperty(person, 'hair', {
value: 'black',
//未指定其他值,其他值默认为false
});
let _p = Object.getOwnPropertyDescriptor(person, 'hair');
console.log(_p);
2.访问器属性
访问器属性不包含数据值。
访问器属性包含一个获取函数getter和一个设置函数setter。
- 在读取访问器属性时,会调用
getter
,getter
的责任是return
一个有效值。 - 在写入访问器属性时,会调用
setter
并传入新值,setter
决定对数据(属性的值)做什么修改。
访问器属性也有4个特性描述其行为
特性名称 | 作用 | 默认值 |
---|---|---|
[[Configurable]] | 表示属性是否可通过 delete 删除并重新定义,是否可以修改属性的特性,是否可以把属性类型改为数据属性。 | true |
[[Enumberable]] | 属性是否可以通过 for-in 循环返回。 | true |
[[Get]] | getter,在读取属性时调用。 | undefined |
[[Set]] | setter,在写入属性时调用。 | undefined |
⭐修改访问器属性也要使用Object.defineProperty()
方法。
let book = {
year_: 2017,//私有成员
edition: 1 //公共成员
}
Object.defineProperty(book, 'year', {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition = newValue - 2017;
}
}
});
book.year = 2018;
console.log(book.edition); //输出2
上面代码就是访问器属性的典型使用场景:设置一个属性的值会导致一些其他变化的发生。
getter
和 setter
不一定都要定义:
- 只定义
getter
意味着属性是只读的,尝试修改属性会被忽略; - 只定义
setter
在非严格模式下读取属性会返回undefined
。
ES5之前没有 Object.defineProperty()
方法。
3.同时定义多个属性
使用 Object.defineProperties(obj,descriptionObj)
方法
参数名 | 参数类型 | 描述 |
---|---|---|
obj | Object | 要添加/修改属性的对象 |
decriptionObj | Object | 描述符对象 |
let person = {}
Object.defineProperties(person, {
name: {
value: 'Macc'
},
age: {
value: 18
},
hair: {
get() {
return 'black';
},
set(newValue) {
//....
}
}
});
4.读取属性的特性
4.1 读取属性的某个特性
使用方法 Object.getOwnPropertyDescriptor(属性所在对象,属性名称)
,该方法 return
一个对象。
4.2 读取对象的全部自有属性的特性
使用方法 Object.getOwnPropertyDescriptors(对象)
,该方法也 return
一个对象,对象包括指定对象的所有自有属性的特性,若对象没有属性,则返回一个空对象。
该方法实际上是在每个自有属性上调用 Object.getOwnPorpertyDescriptor()
方法并在一个新对象中返回它们。
(二)合并对象
把源对象的所欲本地属性一起复制到目标对象上,这种操作叫做合并(merge),也叫做混入(mixin)。
合并对象使用的方法是 Object.assign(目标对象,源对象1, ... ,源对象n)
。
该方法会将源对象中的所有可枚举属性和自有属性复制到目标对象上。
所谓的可枚举属性指的是调用Object.prpertyIsEnumerable()
返回true
的属性;
所谓的自有属性指的是调用Object.hasOwnProperty()
返回true
的属性;
复制过程中,会使用源对象上的 [[Get]]
取得属性值,再使用目标对象上的 [[Set]]
设置属性的值。
Object.assign()
执行的是浅复制,只复制对象的引用。
若多个源对象有相同属性,则使用最后一个复制的值(即哪个源对象靠后就使用哪个的值)(覆盖)
不能在两个对象之间转移getter函数和setter函数。 从源对象访问器属性中取得的值比如getter函数,会作为一个静态的值赋值给目标对象。
若是在赋值期间出错,则操作中止并退出,抛错。但是该方法不会回滚,它是一个尽力而为,可能只完成部分复制的方法。
(三)对象标识及相等判定
在ES6之前,存在使用全等符(===
)也无能为力的情况:
- 符合预期的情况如下
表达式 | 结果 |
---|---|
true === 1 | false |
{} === {} | false |
"2" === 2 | false |
2.不同js引擎表现不同,但仍被认为相等
表达式 | 结果 |
---|---|
+0 === -0 | true |
+0 === 0 | true |
-0 === 0 | true |
- 要确定
NaN
的相等性必须使用isNaN
函数
表达式 | 结果 |
---|---|
NaN === NaN | false |
isNaN(NaN) | true |
在ES6中新增了方法 Object.is()
,该方法与全等符相似,但是考虑了上述的边界条件。该方法必须接收两个参数。
表达式 | 结果 |
---|---|
Object.is(+0,-0) | false |
Object.is(+0,0) | true |
Object.is(-0,0) | false |
Object.is(NaN,NaN) | true |
如果要使用 Object.is()
检查超多两个值,可以递归的利用相等性实现:
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length < 2 || recursivelyCheckEqual(...rest));
}
console.log(recursivelyCheckEqual(1, 2, 3, 4)); //false
(四)增强的对象语法(语法糖)
1.属性值的简写
简写属性值只要使用变量名就会自动被解释为同名属性键,若是未找到同名的变量,则抛错。
let name = 'Macc',
age = 18;
let person = {
name, //简写
//下面是以前的写法
age:age
};
console.log(person);//{name:"Macc",age:18}
2.可计算属性
在引入可计算属性前,如果想使用变量值作为属性(名),必须先声明对象,再使用中括号语法来添加属性。也即是说,不可以在对象字面量中直接动态命名属性。
const nameKey = 'name';
let person = {};//先声明对象
person[nameKey] = 'Macc';//使用中括号语法添加属性
在引入可计算属性后,就可以在对象字面量中完成动态属性赋值了。
let person = {
[nameKey]:'Macc'
}
可计算属性表达式中抛出任何错误都会中断对象的创建,且不回滚。
const nameKey = 'Macc';
const ageKey = 'age';
let person = {
[nameKey]: 'Macc',
[jobKey]: '码农', //这里会出错
[ageKey]: 18
}
console.log(person);//这里是打印不出来的,因为对象的创建被中断了。
3.简写方法名
在此之前,给对象定义方法的时候,是以下格式:
let person = {
//方法名 冒号 匿名函数表达式
sayHi:function(){
//...
}
}
现在则是:
let person = {
sayHi(){
//...
}
}
而且,简写方法名与可计算属性相互兼容。
const methodKey = 'sayHi';
let person = {
[methodKey](name){
//...
}
}
(五)对象解构
在一条语句中使用嵌套数据实现一个或多个赋值操作。
简而言之就是,使用与对象 匹配的结构来实现对象属性的赋值。
- 可以使用简写语法
let person = {
name:'Macc',
job:'码农'
};
let {name,job} = person;
console.log(name,job);//Macc,码农
- 解构赋值不一定与对象的属性匹配(赋值时可以忽略某些属性,无需一一对应)
let {name,age} = person;//无需一一对应
console.log(name,age);//"Macc",undefined
- 可在解构赋值的同时设定默认值
let {name,age:18} = person;
console.log(name,age);//"Macc",18
解构在内部使用了 ToObject()
方法把源数据解构转换为对象,也就是说在对象解构上下文中,原始值会被当成对象。null和undefined不能被解构,否则报错。
let { _ } = null;//报错
let { _ } = undefined;//报错
解构不要求变量必须在解构表达式中声明,但是如果给事先声明过的变量赋值,则表达式必须包在一对小括号中。
let personName,personAge;//事先声明变量
({name:personName,age:personAge} = person);
1.嵌套解构
首先是可以使用解构来复制对象的属性:
let person = {
name: 'Macc',
age: 18,
};
let personCopy = {}; //这里这个分号一定要记得加,不然报错
({ name: personCopy.name, age: personCopy.age } = person);
console.log(personCopy);
//{name: 'Macc', age: 18}
然后想一想,假如被复制的属性是个嵌套结构呢?解构还能用吗?答案是可以的,解构赋值可以使用嵌套结构,但是外层属性未定义时,不能使用。
let person = {
job: {
title: '码农'
}
};
let personCopy = {}; //这里这个分号一定要记得加,不然报错
let { job: { title } } = person;
console.log(title); //码农
//foo在源对象上undefined未定义,报错
({ foo: { bar: person.bar } } = person);
//job在源对象上undefined未定义,报错
({ job: { title: person.job.title } } = person);
2.部分解构
如果一个解构表达式设计多个赋值操作,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分。
3.参数上下文匹配
在函数参数列表中也可以进行解构赋值,且不会影响到 arguments
对象。
let person = {
name: 'Macc',
age: 18
};
function printPerson(foo, { name, age }, bar) {
console.log(arguments);
}
printPerson('1st', person, '2nd');