深入理解 JavaScript 对象与属性控制
ECMA-262将对象定义为一组属性的无序集合,严格来说,这意味着对象就是一组没有特定顺序的值,对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值. 可以把js的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数
1. 理解对象
创建自定义对象的通常方式是创建Objcet的一个新实例,然后再给它添加属性和方法,如下例所示:
let person=new Object();
person.name='Nicholas';
person.age=29;
person.sayName=function(){console.log(this.name)
}
这个例子创建了一个名为person的对象,而且有2个属性(name,age和job)和一个方法(sayName( )),sayName()方法会显示this.name的值,这个属性会解析为person.name 早期,js开发者频繁使用这种方式,
现在流行的方式为使用对象字面量
let person={name:"Nicholas",age:29,sayName(){console.log(this.name)}
}
1.1属性的类型
ECMA-262使用一些内部特性来描述属性的特征,这些特性是由js实现引擎的规范定义的,因此,开发者不能在js中直接访问这些特性,为了将某个特性表示为内部特性,规范会用两个中括号把特性的名称括起来,比如[[Enumerable]] 属性分两种: 数据属性和访问器属性
1.数据属性
数据属性包含一个保存数据值的位置,值会从这个位置读取,也会写入到这个位置,数据属性有4个特性描述它们的行为
- [[Configurable]]: 表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性,默认情况下,所有直接定义在对象的属性的这个特性都是true,如前面的例子所示
- [[Enumerable]]: 表示属性是否可以通过for-in循环返回,默认情况下,所有直接定义在对象上的属性的这个特性都是true,如前面的例子所示
- [[Writable]]:表示属性的值是否可以被修改,默认情况下,所有直接定义在对象上的属性的这个特性都是true,如前面的例子所示
- [[Value]]: 包含属性实际的值,这就是前面提到的那个读取和写入属性值的位置这个特性的默认值为undefined
在像前面例子中那样将属性显示添加到对象之后,上面的属性都会被设置为true,而[[value]]也会被设置为指定的值
例如
let person={
name:'Nicholas'
}
这里,我们创建了一个名为name的属性,并给它赋予了一个值"nicholas",这意味着[[Value]]特性会被设置为"Nicholas",之后对这个值的任何修改都会保存这个位置
要修改属性的默认特性,就必须使用Object.defineProperty(obj,prop,descriptor)方法,这个方法接收三个参数,要给其添加属性的对象,属性的名称和一个描述符对象,最后一个参数,即描述符对象上的属性可以包含: configurable,enumerable,writable和value,跟相关特性的名称一 一对应,根据要修改的特性,可以设置其中一个或多个值 ,比如:
let person={ };
Object.defineProperty(person.name,{
writable: false, //属性不可被修改
value:"Nicholas"
});
console.log(person.name); //'Nicholas'
person.name='Greg';
console.log(person.name), //"nicholas"
在非严格模式下尝试给这个属性重新赋值会被忽略,在严格模式下,尝试修改只读属性的值会抛出错误,类似的规则也适用于创建不可配置的属性,比如configurable,此外,一个属性被定义为不可配置之后,就不能再变回可配置的了,再次调用Object.defineProperty()并修改任何非writable属性会导致错误:
let person={ };
Object.defineProperty(person.name,{
writable: false, //属性不可被修改
value:"Nicholas"
});
//抛出错误object.defineProperty(person,'name',{
configurable: true,
value:'Nichoselas'
});
因此,虽然可以对同一个属性多次调用Object.defineProperty(),但在把configurable设置为false之后就会受限制了可以理解为: configurable是总开关,关了之后一切锁死,
writable 是局部锁,关了就变只读
在configurable: false的前提下,只能单向关闭writable,不能重新打开
在调用Object.defineProperty()时,configurable,enumerable和ritable的值如果不指定则都默认为false,多数情况下,可能都不需要Object.defineProperty()提供的这些强大的设置,但要理解js对象,就要理解这些概念
2.访问器属性
访问器属性不包含数据值,相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的,在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值,在写入访问器属性时,会调用设置函数并传入新值,这个函数必须对数据做出什么修改,访问器有4个特性描述它们的行为
- [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性,默认情况下,所有直接定义在对象上的属性的这个特性都是true
- [[Enumerable]]: 表示属性是否可以通过for-in 循环返回,默认情况下,所有直接定义在对象上的属性的这个特性都是true
- [[Get]]: 获取函数,在读取属性时调用,默认值为undefined
- [[Set]]: 设置函数,在写入属性时调用,默认值为undefined
访问器属性是不能直接定义的,必须使用Object.defineProperty(obj,prop,descriptor);
- obj: 目标对象
- prop: 要定义或修改的属性名
- descriptor: 属性描述符对象
//定义一个对象,包含伪私有成员year_和公共成员edition
let book={
year_ : 2017,
edition: 1
}
Object.defineProperty(book,"year",{//访问器属性,返回year_的值
get(){
return this.year_; //2017
},
set(newValue){
if(newValue>2017){
this.year_=newValue;
this.edition+=newValue-2017;//1+2018-2017=2
}
}
});
book.year=2018;
console.log(book.edition); // 2
在这个例子中,对象book有两个默认属性: year_和edition,year_中的下划线常用来标识该属性并不希望在对象方法的外部被访问,另外一个属性year被定义为一个访问器属性,其中获取函数简单地返回year_的值,而设置函数会做一些计算,因此,把year属性修改为2018会导致year_变成2018,edition变成2,这就是访问器属性的典型场景,及设置一个属性值会导致一些其他变化的发生
获取函数和设置函数不一定都要定义,只定义获取函数意味着属性是只读的,尝试修改属性会被忽略,在严格模式下,尝试写入只定义了获取函数的属性会抛出错误,类似地,只有一个设置函数的属性是不能读取的,在非严格模式下去读会返回undefined,严格模式下会抛出错误
1.2. 定义多个属性 Object.defineProperties()
Object.defineProperties() 通过多个描述符一次性定义多个属性, 接受两个参数: 要为之添加或修改属性的对象和另一个描述符对象,其属性要与添加或修改的属性意义对应,比如:
let book={ };
Object.defineProperties(book,{
year_:{
value: 2017
},
editon:{
value:1
},
year:{
get: function(){
return this.year_;
}
},
set: function(newValue){
if(newValue>2017){
this.year_=newValue
this.edition+=newValue-2017;
}
}
})
let descriptor=Object.getOwnPropertyDescriptor(book,"year_");console.log(descriptor.value); //2017
console.log(descriptor.configurable); //false
console.log(typeof descriptor.get); //undefined 数据属性没有get.get
let descriptor=Object.getOwnPropertyDescriptor(book,"year");console.log(descriptor.value); //false
console.log(descriptor.enumerable); //false
console.log(typeof descriptor.get); // "function"
对于数据属性year_,value 等于原来的值,configurable是false,get是undefined.
对于访问器属性year,value是undefined,enumerable是false,get是一个指向获取函数的指针
1.3. 合并对象
1.Object.assign()方法
这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回true)和自有(Object.hasOwnProperty()返回true)属性复制到目标对象,以字符串和符号为键的属性会被复制,对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值
//简单复制
let dest,src,result;
dest={ };
src={id:'src'};
result=Object.assign(dest,src);
//Object.assign修改目标对象//也会返回修改后的目标对象
console.log(dest===result) //true
console.log(dest !== src) //true
console.log(result) //{id: src}
console.log(dest) //{id:src}
//多个源对象
dest={ };
result=Object.assign(dest,{a:'foo'},{b:'bar'});
console.log(result); // {a:foo,b:bar}
Object.assign( )实际上对每个源对象执行的是浅复制,如果多个源对象都有相同的属性,则使用最后一个复制的值,此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象,换句话说,不能在两个对象间转移获取函数和设置函数
- 会覆盖重复的属性
- 浅复制意味着只会赋值对象的引用
如果赋值期间出错,则操作会中止并退出,同时抛出错误,Object.assign( )没有"回滚"之前赋值的概念,因此它是一个尽力而为,可能只会完成部分赋值的方法
1.4. 对象表示及相等判定
在es6之前,有些特殊情况即使是===操作符也无能为力
这些是===符合预期的情况
console.log(true===1); //false
console.log({}==={}); //false
console.log('2'===2); false
这些情况在不同js引擎中表现不同,但仍被认为相等
console.log(+0 === -0); //true
console.log(+0=== 0); //true
console.log(-0 === 0); //true
要确定NaN的相等性,必须使用及其讨厌的isNaN()
console.log(NaN===NaN); //false
console.log(isNaN(NaN)); //true
为改善这类情况,es6规范新增了Object,is(), 这个方法与===很像, 但同时也考虑了上述边界情形,这个方法必须接收两个参数:
console.log(Object.is(true,1)); //false
//正确的0,-0,+0 相等/不等判定
console.log(Object.is(+0,-0)); //false
//正确的NaN相等判定console.log(Object.is(NaN,NaN)); //true
1.5 增强的对象语法
es6为定义和操作对象新增了很多及其有用的语法糖特性,这些特性都没有改变现有引擎的行为,但极大地提升了处理对象的方便程度
1. 属性值简写
let name='Matt';
let person={
//之前写法
name:name
//现在写法
name
}
2.可计算属性
在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性,换句话说,不能在对象字面量中直接动态命名属性,比如:
const nameKey='name';
const ageKey='age';
const jobKey='job';
let person={[nameKey]: 'Matt',
[ageKey]: 27,
[jobKey]: 'Software engineer'
};
console.log(person); //{name:'Matt',age:27,job:'Software engineer'}
因为被当做js表达式求值,所以可计算属性可以是复杂比的表达式,在实例化时再求值:
function getUniqueKey(key){return `${key}_${uniqueToken++}`;
}
let person={
[getUniqueKey(nameKey)] : 'Matt',
[getUniqueKey(ageKey)]: 27,
[getUniqueKey(jobKey)]:'Software engineer'
};
console.log(person); //{name_0:'Matt',age_1:27,job_2:'Software engineer'}
注意 可计算属性表达式中抛出任何错误都会中断对象创建,如果计算属性的表达式有副作用,那就要小心了,因为如果表达式抛出错误,那么之前完成的计算是不能回滚的
3.简写方法名
let person={
//一般写法
sayName: function(name){
console.log(`My name is ${name}`);
}
//简写
sayName(name){
console.log(`My name is ${name}`);
}
};
1.6. 对象解构
可以在一条一句中使用嵌套数据实现一个或多个赋值操作,简单地说,对象结构就是使用与对象匹配的结构来实现对象属性赋值
//不使用对象结构let person={name:'Matt', age: 27 }let personName=person.name, personAge=person.age; cosnole.log(personName); //Matt console.log(personAge); //27//1. 使用对象解构 let {name: personName,age: personAge}=person
console.log(personName); //Matt console.log(personAge); //27 //2. 简写语法,变量直接使用属性的名称,
let {name,age}=person; console.log(name); //Matt console.log(job); //undefined//3. 默认值 let {name,job='Software engineer'}=person; console.log(name); //Matt console.log(job); //Software engineer //4.解构并不要求必须在结构表达式中声明,不过,如果是事先声明的变量赋值,则赋值表达式,必须包含在一对括号中: let personName,personAge; //先声明
({name:personName,personAge}=person)
1.6.1嵌套解构
解构对于引用嵌套的属性或赋值目标没有限制,为此,可以通过结构来复制对象属性:
let person={
name:'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
let personCopy={ };
({name: personCopy.name,
age : personCopy.age,
job: personCopy.job
}=person);
//因为一个对象的引用被赋值给personCopy,所以修改person.job的对象属性也会影响personCopy
person.job.title='Hacker'
console.log(personCopy); //{name:'Matt',age:27,job:{title:'Hacker'}}
解构赋值可以使用嵌套结构,以匹配嵌套的属性:
//声明title变量并将person.job.title的值赋值给它let {job:{title}}=person;
console.log(title) //Software engineer
注意: 1. 在外层属性没有定义的情况下不能使用嵌套解构,无论源对象还是目标对象都一样
2. 涉及多个属性的解构赋值是一个输出无关的顺序化操作,如果一个结构表达式涉及多个赋 值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分