对象

一、理解对象

ECMA-262将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。这因为如此,可以把ECMAScript的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或函数。

1、理解对象

创建自定义对象的通常方式是创建Object的一个新实例,然后再给它添加属性和方法,如下列所示:

let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function () {
    console.log(this.name);
}

这个例子创建了一个名为person的对象,而且有三个属性(name、age和job)和一个方法(sayName())。sayName()方法会显示this.name的值,这个属性会解析为person.name。早期JavaScript开发者频繁使用这种方式创建新对象。最近以来,对象字面量变得更流行的方式。以上如果使用字面量对象创建则是:

let person = {
  name:"Nicholas",
  age:29,
  job:"Software Engineer",
  sayName(){
    console.log(this.name);
  }
}

这两个创建对象的例子是等价的。

 

1、属性的类型

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

属性分两种:数据属性和访问器属性。

1、数据属性

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

a、[[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true。

b、[[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true。

c、[[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是true。

d、[[Value]]:包含属性实际的值。这就是那个读取和写入属性值的位置。这个特性的默认值为undefined。

 

在以上的两种创建对象的例子中将属性显式添加到对象之后,[[Configurable]]、[[Enumerable]]、[[Writable]]都会被设置为true,[[Value]]特性会被设置为指定的值。

let  person={
  name:"Nicholas"
}

创建一个名为name的属性,并赋予了一个值“Nicholas”。这意味着[[Value]]特性会设置为“Nicholas”,之后对这个值的任何修改都会被保存这个位置。

2、Object.defineProperty()

要修改属性的默认特性,就是必须使用Object.definePropertry()方法。这个方法接收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"

注意:一个属性被定义为不可配置之后,就不能再变回可配置的了。再次调用Object.defineProperty()并修改任何非writable属性会导致错误。

let person={};
Object.defineProperty(person,"name",{
  configurable:false,
  value:"Nicholas"
});
//抛出错误
Object.defineProperty(person,"name",{
  configurable:true,
  value:"Nicholas"
});

虽然可以对同一个属性多次调用Object.defineProperty(),但在把configurable设置false后会受限制。

 

注意:在调用Object.defineProperty()时,configurable、enumerable和writable的值如果不指定,则都默认为false。多数情况下,可以能不需要Object.defineProperty()提供的这么强大的设置,但要理解JavaScript对象,就要理解这些概念。

 

3、访问器属性

访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。

在读取访问属性时,会调用获取函数,这个函数的责任是返回一个有效的值。

在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。

访问器属性有4特性描述它们的行为:

a、[[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true。

b、[[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true。

c、[[Get]]:获取函数,在读取属性时调用。默认值为undefined。

d、[[Set]]:设置函数,在写入属性时调用。默认值为undefined。

 

注意:访问器属性是不能直接定义的,必须使用Object.defineProperty()。

let book={
    year_:2017,
    edition:1
};
Object.defineProperty(book,"year",{
    get() {
        return this.year_;
    },
    set(newVal) {
        if (newVal>2017){
            this.year_ = newVal;
            this.edition +=newVal-2017;
        }
    }
});
book.year=2018;
console.log(book.edition);  //2
console.log(book.year);     //2018
console.log(book.year_);    //2018

对象book有两个默认属性:year_和edition。year_中的下划线常用来表示该属性并不希望在对象方法的外部被访问。year被定义为一个访问器属性,其中获取函数简单返回year_的值,而设置函数会做一些计算以决定正确版本(edition)。

获取函数与设置函数不一定都要定义。只定义获取函数意味着属性时只读,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性时不能读取的,非严格模式下读取会返回undefined,严格模式下,则抛出错误。

在不支持Object.defineProperty()的浏览器中没有方法修改[[Configurable]]或[[Enumerable]]

 

 

2、定义多个属性

对在一个对象上同时定义多个属性的可能性是非常大。为此,ECMAScript提供了Object.defineProperties()方法。这个方法可以通过多个描述符一次性定义多个属性。

它接收两个参数:1、要为之添加或修改属性的对象;2、一个描述对象。其属性与要添加或修改的属性一一对应。

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

3、读取属性的特性

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

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

});

let descriptor_1 = Object.getOwnPropertyDescriptor(book,"year_");
console.log(descriptor_1);
console.log(typeof descriptor_1.get);
/*打印
{
  value: 2020,
  writable: false,
  enumerable: false,
  configurable: false
}
undefined
*/
let descriptor_2 = Object.getOwnPropertyDescriptor(book,"year");
console.log(descriptor_2.value)
console.log(descriptor_2.enumerable);
console.log(typeof descriptor_2.get);
/*打印
undefined
false
function
*/

ECMAScript 2017新增了Object.getOwnPropertyDescriptors()静态方法。这个方法实际上会在每个自有属性上调用Object.getOwnPropertyDescriptor()并在一个新对象中返回它们。

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

});

let descriptor = Object.getOwnPropertyDescriptors(book);
console.log(descriptor);
/**打印
{
  year_: {
    value: 2020,
    writable: false,
    enumerable: false,
    configurable: false
  },
  edition: { value: 1, writable: false, enumerable: false, configurable: false },
  year: {
    get: [Function: get],
    set: [Function: set],
    enumerable: false,
    configurable: false
  }
}
*/

4、合并对象

JavaScript开发者经常觉得“合并”(merge)两个对象很有用。更具体的说,就是把源对象所有的本地属性一起复制到目标对象上。有时候这种操作也被称为“混入”(mixin),因为目标对象通过混入源对象得到增强。

ECMAScript 6专门为合并对象提供了Object.assign()方法。这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回true)和自有(Object.hasOwnProperty()返回true)属性复制到目标对象。以字符串和符合为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象的[[Get]]取得属性的值,然后使用目标对象的[[Set]]设置属性的值。

/**
 * 多个源对象
 *
 */
dest={};
result = Object.assign(dest,{a:'foo'},{b:'bar'});
console.log(result);      //{a:foo,b:bar}

/**
 * 获取函数与设置函数
 */
dest = {
    set a(val){
        console.log(`Invoked dest setter with param ${val}`)
    }
};
src={
    get a(){
        console.log(`Invoked src getter`);
        return 'foo';
    }
};
Object.assign(dest,src);
//调用src的获取方法
//调用dest的设置方法并传入参数“foo"
//因为这里的设置函数不执行赋值操作
//所有实际上并没有把值转移过来
console.log(dest);  //{set a(val){...}}

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);
    }
}
result = 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
//改动src的a对象,dest的a对象也会被改动
src.a.name = "Nicholas";
console.log(dest.a);  //{ name: 'Nicholas' }

如果赋值期间出错,则操作会终止并退出,同时抛出错误。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);
    }
}
result = 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
//改动src的a对象,dest的a对象也会被改动
src.a.name = "Nicholas";
console.log(dest.a);  //{ name: 'Nicholas' }

5、对象标识及相等判定

在ECMAScript 6之前,有些特殊情况即使是===操作符也是无能为力:

//这些是===符合预期的情况
console.log(true===1);  //false
console.log({}==={});   //false
console.log("2"===2);   //false
//这些情况在不同JavaScript引擎中表现不同,但仍被认为相等
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

为了改善这类情况,ECMAScript 6规范新增了Object.is(),这个方法与===很像,但不同的是Object.is()考虑到边界情形。这个方法必须接收两个参数:

console.log(Object.is(true,1));  //false
console.log(Object.is({},{}));   //false
console.log(Object.is("1",1));   //false
//正确的0,-0,+0相等/不相等判定
console.log(Object.is(+0,-0));   //false
console.log(Object.is(+0,0));   //true
console.log(Object.is(0,-0));   //false
//正确的NaN相等判定
console.log(Object.is(NaN,NaN));  //true

如果要检查超过两个值,递归地利用相等性传递即可:

function recursivelyCheckEqual(x,...rest){
  return Object.is(x,rest[0])&&(rest.length<2||recursivelyCheckEqual(...rest));
}

6、增强的对象语法

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

ECMAScript新增的对象增强语法对class类定义同样是有效的。

1、属性值的简写

let name = "Nicholas";
let person ={
  name:name 
}
console.log(person);  //{name:"Nicholas"}
为此,简写属性名语法出现了。简写属性名只要使用变量名(不用再写冒号)就会自动被解释为同名的属性键。如果没有找到同名变量,则会抛出ReferenceError。

以下简写的代码与以上的是等价的

let name="Nicholas";
let person= {
  name
}
console.log(person);  //{name:"Nicholas
"}

代码压缩程序会在不同作用域间保留属性名,以防找不到引用。以下代码为例:

function makePerson(name) {
    return {
        name
    }

}
let person = makePerson('Matt');
console.log(person.name);   //Matt
在这里,即使参数标识符只限定于函数作用域,编译器也会保留初始的name标识符。如果Goole Closure编译器压缩,那么函数参数会被压缩,而属性名不变:

function makePerson(a) {
    return {
        name:a
    }

}
let person = makePerson('Matt');
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'}
//如果直接在对象中定义
let namekey = 'name'
let book={
    namekey:'Matt'
}
console.log(book.name);//undefined

有了可计算属性,就可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时将其作为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

简写方法名对获取函数和设置函数也是适用的:

let person={
  name_:'',
  get name(){
    return this.name_;
  },
  set name(name){
    this.name_=name;
  }
  sayName(){
    console.log(`My name is ${this.name_}`);
  }
}
person.name='Matt';
person.sayName(); //My name is Matt

简写方法名与可计算属性键相互兼容:

const methodKey='sayName';
let person={
  [methodKey](name) {
    console.log(`My name is ${name}`);
  }
}
person.sayName('Matt'); //My name is Matt

7、对象解构

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

下面的例子是两段等价的代码,首先是不使用对象解构:

//不使用对象解构
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 {name:name,age:age} = person;
console.log(name);
console.log(age);

对象的属性名在左,解构的变量名在右。

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

//使用简写语法
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 person={
    name:'Matt',
    age:27
}
let {name,job='Software engineer'} = person;
console.log(name);  //Matt
console.log(job);   //Software engineer

解构在内部使用函数ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据ToObject()的定义),null与undefined不能被解构,或则会抛出错误。

let  {length} = "foobar";
console.log(length);   //6
let {constructor:c} = 4;
console.log(c===Number);   //true

let {_} = null;       //TypeError
let {_}= undefined;   //TypeError

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

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

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

2、部分解构

需要注意的是,涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分:

let person = {
  name:'Matt',
  age:27
};
let personName,personBar,personAge;
try{
  //person.foo是undefined,因此会抛出错误
  ({name:personName,foo:{bar:personBar},age:personAge}=person);
}catch(e){}
console.log(personName,personBar,personAge);//Matt,undefined,undefined

3、参数上下文匹配

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

let person = {
  name:'Matt',
  age:27
};
let personName,personBar,personAge;
try{
  //person.foo是undefined,因此会抛出错误
  ({name:personName,foo:{bar:personBar},age:personAge}=person);
}catch(e){}
console.log(personName,personBar,personAge);//Matt,undefined,undefined

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值