学习笔记12—Object的属性详解

目录

  • 1 属性的类型
  • 2 定义多个属性
  • 3 读取属性的特性
  • 4 合并对象
  • 5 增强的对象语法
  • 6 对象解构

1 属性的类型

ECMA-262使用一些内部特性来描述属性的特征,这些特性是由为JavaScript实现引擎的规范定义的,因此开发者不能在JavaScript中直接访问这些特性。为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,比如[[Enumerable]]。属性分两种:数据属性和访问器属性。

(1)数据属性

数据属性包含一个保存数据值的位置,值会从这个位置读取,也会写入到这个位置,数据属性有4个特性描述它们的行为:

  • [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true。
  • [[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true。
  • [[Writable]]:表示属性的值是否可以被修改,默认情况下所有直接定义在对象上的属性的这个特性都是true。
  • [[Value]]:包含属性实际的值,这就是前面提到的那个读取和写入属性值的位置。

将属性显式添加到对象之后,[[Configurable]]、[[Enumerable]]和[[Writable]]都会被设置为true,而[[Value]]特性会被设置为指定的值:

要修改属性的默认特性,就必须使用Object.defineProperty()方法,这个方法接收3个参数:要给其添加属性的对象、属性的名称和一个描述符对象。最后一个参数,即描述符对象上的属性可以包含: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"

这个例子创建了一个名为name的属性,并给他赋予了一个只读的"Nicholas",这个属性的值不能再修改了,在非严格模式下尝试给这个属性重新赋值会被忽略,在严格模式下,尝试修改只读属性的值会抛出错误。
类似的规则也适用于创建不可配置的属性:

let person={
     };
Object.defineProperty(person,"name",{
     
	configurable:false,
	value:"Nicholas"
});
console.log(person.name);//Nicholas
delete person.name;
console.log(person.name);//Nicholas

这个例子把configurable设置为false,意味着这个属性不能从对象上被删除。非严格模式下对这个属性调用delete没有效果,严格模式下会抛出错误。此外一个属性被定义为不可配置之后,就不能再变回可配置的了,再次调用Object.defineProperty()并修改任何任何非writable属性会导致错误:

let person = {
     };
Object.defineProperty(person,"name",{
     
	configurable:false,
	value:"Nicholas"
});

//Throw an error
Object.defineProperty(person,"name",{
     
	configurable:true,
	value:"Nicholas"
});

在调用Object.defineProperty()时,configurable、enumerable和writable的值如果不指定,则都会默认为false。

(2)访问器属性
访问器属性不包含数值,相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必须的,在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用设置函数并传入新值, 这个函数必须决定对数据做出什么修改。访问器属性有4个特性描述它们的行为:

  • [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true。
  • [[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true。
  • [[Get]]:获取函数,在读取属性时调用,默认值为undefined。
  • [[Set]]:设置函数,在写入属性时调用。默认值为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

在这个例子中,对象book有两个默认属性:year_和edition。year_中的下划线常用来表示该属性并不希望在对象方法的外部被访问。 另一个属性year被定义为一个访问器属性,其中获取函数简单的返回year_的值,而设置函数会做一些计算以决定正确的版本(edition)。因此,把year属性修改为2018会导致year_变成2018,edition变成2。获取函数和设置函数不一定都要定义,只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。

2 定义多个属性

在一个对象上同时定义多个属性的可能性是非常大的,为此ECMAScript提供了Object.defineProperties()方法,这个方法可以通过多个描述符一次性定义多个属性,它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应:

let book = {
     };
Object.defineProperties(book,{
     
	year_:{
     
		value:2017
	},
	edition:{
     
		value:1
	},
	year:{
     
		get(){
     
			return this.year_;
		}
	
		set(newValue){
     
			if(newValue>2017){
     
				this.year_ = newValue;
				this.edition += newValue - 2017;
			}
		}
	}
});

这段代码和上面示例一样,唯一的区别是所有属性都是同时定义的,并且数据属性的configurable、enumerable和writable特性都是false;

3 读取属性的特性

使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符,这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名,返回值是一个对象,对于访问器属性包含configurable、enumerable、get和set属性,对于数据属性包含configurable、enumerable、writable和value属性:

let book = {
     };
Object.defineProperties(book,{
     
	year_:{
     
		value:2017
	},
	eidtion:{
     
		value:1
	},
	year:{
     
		get:function(){
     
			return this.year_;
		},
		set:function(){
     
			if(newValue>2017){
     
				this.year_ = newValue;
				this.edition += newValue - 2017;
			}
		}
	}
});

let descriptor = Object.getOwnPropertyDescriptor(book,"year_");
console.log(descriptor.value);//2017
console.log(descriptor.configure);//false
console.log(typeof descriptor.get);//undefined
let descriptor = Object.getOwnPropertyDescriptor(book,"year");
console.log(descriptor.value);//undefined
console.log(descriptor.enumerable);//false
console.log(typeof descriptor.get);//function

4 合并对象

JavaScript开发者经常觉得"合并"(merge)两个对象很有用,更具体的说,就是把源对象所有的本地属性一起复制到目标对象上。有时候这种操作也被称为"混入"(mixin),因为目标对象通过混入源对象得到了增强。
ECMAScript6专门为合并对象提供了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()实际上对每个源对象执行的是浅复制,如果多个源对象都有相同的属性,则使用最后一个复制的值。此外从源对象访问器属性获取的值,比如获取函数会作为一个静态值赋给目标对象,换句话说不能在两个对象间转移获取函数和设置函数:

let dest,src,result;

//覆盖属性
dest = {
     id:'dest'};
result = Object.assign(dest,{
     id:'src1',a:'foo'},{
     id:'src2',b:'bar')};

//Object.assign会覆盖重复的属性
console.log(result);//{id:src2,a:foo,b:bar}

//可以通过目标对象上的设置函数观察到覆盖的过程
dest = {
     
	set id(x){
     
		console.log(x);
	}
};

Object.assign(dest,{
     id:'first'},{
     id:'second'},{
     id:'third'});
//first
//second
//third

//对象引用
dest = {
     };
src = {
     a:{
     }}:
Object.assign(dest,src);

//浅复制意味着只会复制对象的引用
console.log(dest); //{a:{}}
console.log(dest.a === src.a); //true

如果赋值期间出错,则操作会中止并退出,同时抛出错误,Object.assign()没有回滚之前赋值的概念,因此它是一个尽力而为、可能只会完成部分复制的方法。

let dest,src,result;

//错误处理
dest={
     };
src={
     
	a:'foo',
	get b(){
     
		//Object.assign()在调用这个获取函数时会抛出错误
		throw new Error();
	},
	c:'bar'
};

try{
     
	Object.assign(dest,src);
}catch(e){
     }

//Object.assign()没办法回滚已完成的修改
//因此在抛出错误之前,目标对象上已完成的修改会继续存在
console.log(dest);//{a:foo}

5 增强的对象语法

ECMAScript6为定义和操作对象新增了很多及其有用的语法糖特性,这些特性都没有改变现有引擎的行为,但极大地提升了处理对象的方便程度。

(1)属性值的简写
在给对象添加变量的时候,开发者经常会发现属性名和变量名是一样的:

let name = 'Matt';

let person = {
     
	name:name
};

console.log(person);//{name:'Matt'}

为此简写属性名语法出现了,简写属性名只要使用变量名就会自动被解释为同名的属性键,如果没有找到同名变量,则会抛出ReferenceError。

let name = 'Matt';
let person = {
     
	name
};

console.log(person);//{name:'Matt'}

(2)可计算属性
在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性,换句话说不能在对象字面量中直接动态命名属性:

const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';

let person = {
     };
person[nameKey] = 'Matt';
person[ageKey] = 27;
person[jobKey] = 'Software engineer';
console.log(person);//{name:'Matt',age:27,job:'Software engineer'}

有了可计算属性,就可以在对象字面量中完成动态属性赋值,中括号包围的对象属性键告诉运行时将其作为JavaScript表达式而不是字符串来求值:

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'}

因为被当作JavaScript表达式求值,所以可计算属性本身可以是复杂的表达式,在实例化时再求值:

const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;

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}`);
	}
};

person.sayName('Matt');//My name is Matt

新的简写方法的语法遵循同样的模式,但开发者要放弃给函数表达式命名。相应地,这样也可以明显缩短方法声明。

let person = {
     
	sayName(name){
     
		console.log(`My name is ${
       name}`);
	}
};

person.sayName('Matt');//My name is Matt

6 对象解构

ECMAScript新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值:

//不使用对象解构
let person = {
     
	name:'Matt',
	age:27
};

let personName = person.name,
	personAge = person.age;
console.log(personName);//'Matt'
console.log(personAge);//27

//使用对象解构
let person = {
     
	name:'Matt',
	age:27
};
let {
     name:personName,age:personAge} = person;
console.log(personName);//Matt
console.log(personAge);//27

使用解构可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作,如果想让变量直接使用属性的名称,那么可以使用简写语法:

let person = {
     
	name:'Matt',
	age:27
};
let {
     name,age} = person;
console.log(name);//Matt
console.log(age);//27

解构赋值不一定与对象的属性匹配,赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是undefined:

let person = {
     
	name:'Matt',
	age:27
};
let{
     name,job} = person;
console.log(name);//'Matt'
console.log(job);//undefined

也可以在解构赋值的同时定义默认值,这适用于前面刚提到的引用属性不存在于源对象中情况:

let person = {
     
	name:'Matt',
	age:27
};
let {
     name,job='Software engineer'} = person;
console.log(name);//Matt
console.log(job);//Software engineer

解构并不要求变量必须在解构表达式中声明,不过如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中:

let personName,personAge;

let person = {
     
	name:'Matt',
	age:27
};

{
     (name:personName,age:personAge} = person);
console.log(personName,personAge);//Matt,27

嵌套解构

解构对于引用嵌套的属性或赋值目标没有限制,为此可以通过解构来复制对象属性:

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(person);//{name:'Matt',age:27,job:{title:'Hacker'}}
console.log(personCopy);//{name:'Matt',age:27,job:{title:'Hacker'}}

解构赋值可以使用嵌套结构,以匹配嵌套的属性:

let person = {
     
	name:'Matt',
	age:27,
	job:{
     
		title:'Software engineer
	}
};

//声明title变量并将person.job.title的值赋给它
let {
     job:{
     title}} = person;
console.log(title);//'Software engineer'

在外层属性没有定义的情况下不能使用嵌套解构,无论源对象还是目标对象都一样:

let person = {
     
	job:{
     
		title:'Software engineer'
	}
};
let personCopy = {
     };

//foo在源对象上是undefined
({
     
	foo:{
     
		bar:personCopy.bar
	}
} = person);
//TypeError:Cannot destructure property 'bar' of 'undefined' or null

//job在目标对象上是undefined
({
     
	job:{
     
		title:personCopy.job.title
	}
} = person);
//TypeError:Cannot set property 'title' of undefined

在函数参数列表中也可以进行解构赋值,对参数的解构赋值不会影响arguments对象,但可以在函数签名中声明在函数体内使用局部变量:

let person = {
     
	name:'Matt',
	age:27
};

function printPerson(foo,{
     name,age},bar){
     
	console.log(arguments);
	console.log(name,age);
}

function printPerson2(foo,{
     name:personName,age:personAge},bar){
     
	console.log(arguments);
	console.log(personName,personAge);
}

printPerson('1st',person,'2nd');
//['1st',{name:'Matt',age:27},'2nd']
//'Matt',27

printPerson2('1st',person,'2nd');
//['1st',{name:'Matt',age:27},'2nd']
//'Matt',27

你可能感兴趣的:(JavaScript,js,javascript)