第六章 对象
这里的对象和类不要混为一谈!
对象是js
最基本的数据类型,前几章我们已经多次看到它了.因为对js
语言来说对象实在是太重要了,所有理解对象的详细工作机制也是非常重要的 ,本章就来详细讲解对象.一开始我们先正式地介绍一下对象,接下来几节将结合实践讨论创建对象和查询,设置,删除,测试以及枚举对象的属性.在关注属性的几节之后,接着会讨论如何扩展,序列化对象,以及在对象上定义重要方法.本章最后一节比较长,主要讲解es6
和这门语言的新进版本的对象字面量语法.
6.1对象介绍
对象是一种复合值,它汇聚多个值(原始值或其他对象)并允许我们按名字存储和获取这些值.对象是一个属性的无序集合,每个属性都名字的值.属性名通常是字符串*也可以是符号(6.10.3节),因此可以说对象把字符串映射为值.这种字符串到值的映射曾经有很多种叫法,**包括"散列",“散列表”,“字典”,或"关联数组"**等熟悉的基本数据结构.不过,对象不仅仅是简单的字符串到值的映射.除了维护自己的属性之外,js
对象也可以从其他对象继承属性,这个其他对象称为其"原型".对象的方法通常是继承来的属性,而这种"原型式继承"也是js
的主要特性.
js
对象是动态的,即可以动态的添加和删除属性.不过,可用对象来模拟静态类型语言中的静态对象和"结构体".对象也可用于表示一组字符串(忽略字符串到值的映射中的值).
在js
中,任何不是字符串,数值,符号或true,false,null,undefined的值都是对象.即使字符串,数值和布尔值不是对象,他们的行为也类似不可修改的对象.
在3.8节介绍过对象是可以修改的,是按引用操作而不是按值操作的.如果变量x指向一个对象,则代码let y = x
执行功后,变量y保存的是同一个对象的引用,而不是该对象的副本.通过变量y对这个对象所做的任何修改,在x上都是可见的.
与对象相关的最常见的操作包括创建对象,以及设置,查询,删除,测试和枚举他们的值.这些基本操作将在本章开头几节介绍.之后几节将讨论更高级的主题.
属性有一个名字和一个值.属性名可以是任意的字符串,包括空字符串(或任意符号),但对象不能包含两个同名的属性.
值可以是任意js
值,或者是设置函数或者获取函数(或两个函数同时存在).6.10.6节将学习设置函数或获取函数.
有时候,区分直接定义在对象上的属性和那些从原型链对象上继承的属性很重要.js
使用术语"自有属性"代指非继承属性
处理名字和值之外,每个属性还有3个属性特性(property attribute):
- writable(可写)特性执行是否可以设置属性的值
- enumerable(可枚举)特性指的是否可以在for/in循环中返回属性的名字.
- configurable(可配置)特性指是否可以删除属性,以及是否可修改其特性.
很多js
内置对象拥有只读,不可枚举或不可配置的属性.不过,默认情况下,我们所创建的对象的所有属性都是可写,可枚举,可配置的(14.1节)将介绍为对象指定非默认的属性特性值的计算.
6.2创建对象
对象可以通过对象字面量,new关键字和Object.create()
函数来创建.接下来分别介绍这种技术.
6.2.1对象字面量
创建对象最简单的方式是在js
代码中直接包含对象字面量.对象字面量的最简单形式是包含在一对花括号中的一组逗号分割的"名:值"对.属性名是js
标识符或字符串字面量(允许空字符串).属性值是任何js
表达式,这个表达式的值(可以是原始值或对象值)会变成属性的值.
let empty = {};// 没有属性的对象
let point = {x:0,y:0}// 包含两个数值属性
let p2 = { x:point.x,y:point.y+1}// 值比较复杂
let book = {"maiin title":'javascript',// 属性名包含空格"sub title":"the definitive guide"// 和连字符,因此使用字符串字面量for:"all audiences",// for是保留字,但没有引号author:{// 这个属性的 值 本身是一个对象firstname:"dada",surname:"faklg"}
}
对象字面量最后一个属性后面的逗号是合法的,有些编程风格指南鼓励添加这些逗号,以便将来在对象字面量末尾再添加新属性时不会导致语法错误
对象字面量是一个表达式,每次求值都会创建并初始化一个新的,不一样的对象.字面量每次被求值的时候,他的每个属性的值也会被求值.这意味着同一个对象字面量如果出现在循环体中,或出现在被重复调用的函数体内,可以创建很多新对象,且这些对象属性的值可能不同.
前面展现的对象字面量使用了简单的语法,这种语法是在js
最初始的版本中规定的.这门语法最近的版本新增了很多新的对象字面量特性.将在6.10节介绍
6.2.2使用new关键字创建对象
new操作符用于创建和初始化一个 新对象.new关键字后面必须跟一个函数调用.以这种方式使用的函数被称为构造函数(constructor),目的是初始胡新创建的对象,js
为内置的类型提供了构造函数.例如:
let o = new Object();// 创建一个空对象,与{}相同
let a = new Array(); // 创建一个空数组,与[] 相同
let d = new Date(); // 创建一个表示当前时间的日期对象
let r = new Map(); // 创建一个映射对象,用于存储键/值映射
除了内置的构造函数,我们经常需要定义自己的构造函数来初始化新创建的对象.相关内容将第九章介绍.
6.2.3原型
在介绍第三种创建对象技术之前,必须暂定一下,先介绍原型.几乎每个js
对象都有另一个与之关联的对象.这另一个对象被称为原型(prototype),第一个对象从这个原型继承属性.
通过对象字面量创建的所有对象都用相同的原型对象,js
代码中可以通过Object.prototype
引用这个原型对象.
使用new关键字和构造函数调用创建的对象,使用构造函数.prototype属性的值(对象)作为他们的原型.
换句话说,使用new Object()创建的对象继承自Object.prototype
,与通过{}创建的对象一样.
类似的,通过new Array()创建的对象以Array.prototype作为原型,通过new Date()创建的对象以Date.prototype为原型.对于初学者来说,这一块很容易迷惑.
记住:几乎所有对象都有原型,但只有少数对象有prototype属性.正是这些有prototype属性的对象为所有其他对象定义了原型.
Object.prototype
是为数不多的没有原型的对象,因为他不继承任何属性.其他原型对象都是常规对象,都有自己的原型.多数内置构造函数(和多数用户定义的构造函数)的原型都继承自Object.prototype
.例如,Date.prototype从Object.prototype继承属性,因此通过new Date()创建的日期对象从Date.prototype和Object.prototype
继承属性.这种原型对象链接起来的序列被称为原型链.
6.3.2节将介绍属性继承的原理.第九章将会更详细的解释原型与构造函数之间的关系,将展示如何定义新的对象"类",包括编写构造函数以及将其prototype属性设置为一个原型对象,让通过该构造函数创建的"实例"继承这个原型对象的属性.另外,14.3节还将介绍如何查询(甚至修改)一个对象的原型.
6.2.4 Object.create()
Object.create()
用于创建一个新对象,使用其第一个参数作为新对象的原型:
let o1 = Object.create({x:1,y:2});// o1继承属性x和y
o1.x + o1.y // 3
传入null可以创建一个没有原型的新对象.不过,这样创建的新对象不会继承任何东西.连toString()这种基本方法都没有(意味着不能对该对象应用+操作符):
let o2 = Object.create(null);// o2不继承任何属性或方法
如果想创建一个普通的空对象(类似{}或new Object()返回的对象),传入Object.protype:
let o3 = Object.create(`Object.prototype`);// o3与{}或new Object()类似
能够以任意原型创建新对象是一种十分强大的技术,本章多处都会使用Object.create()
(Object.create()
还可以接受可选的第二个参数,用于描述新对象的属性.这个参数属于高级特性,将在14.1节介绍).
Object.create()
的一个用途是防止对象被某个第三方函数意外(但非恶意)修改.这种情况下,不要直接把对象直接传给库函数,而要传入一个继承自他的对象.如果函数读取这个对象的属性,可以读取到继承的值.而如果他设置这个对象的属性,则修改不会影响原始对象.
let o = {x:"don't change this value"}
libaray.function(Obejct.create(o))// 防止意外被修改
要理解其中的原理,需要知道js
中属性查询和设置的过程.这些都是下一节的内容.
6.3查询和设置属性
要获取一个属性的值,可以使用4.4节介绍的(.)或方括号([])操作符。左边应该是一个表达式,其值为一个对象。如果使用点操作符,右边必须是一个命名属性的简单标识符。如果使用方括号,方括号中的值必须是一个表达式,表达式的结果为包含目的属性名的字符串:
let author = book.author;// 获得book的author属性
let title = book["main title"];// 取得book的"main title"属性
要创建或设置属性,与查询属性一样,可以使用点或方括号,只是要把他们放到赋值表达式的左边:
book.edition = 7;// 为book创建一个edition属性
book["main title"] = "ecmascript" // 修改main title的值
使用方括号的时候,我们说过其中的表达式必须求值为一个字符串.更准确的说法是,该表达式必须求值为一个字符串或一个可以转换为字符串或符号的值(参加6.10.3),例如我们会 在第七章看到在方括号中使用数字是很常见的.
6.3.1作为关联数组的对象
如前所述,下面两个js
表达式的值相同
object.property
object[property]
第一种语法使用点和标识符,与在c或java中访问结构体或对象的静态字段的语法类似.第二种语法使用方括号和字符串,看起来像访问数组,只不过是以字符串而非数值作为索引的数组.这种数组也被称为关联数组(或散列,映射,字段).js
对象是关联数组,本节解释为什么很重要.
在c,c++,java及类似的强类型语言中,对象只有固定数量的属性,且这些属性的名字必须事先定义.js
是松散类型语言,并没有遵循这个规则,即js
程序可以为任意对象创建任意数量的属性.不过,在使用.操作符访问对象的属性时,属性名是通过标识符来表示的.标识符必须直接书写在js
程序中,他们不是一种数据类型,因此不能被程序操作.
在通过方括号([])这种数组表示法访问对象属性时,属性名是通过字符串来表示的.字符串是一种js
数组类型,因此可以在程序运行期间修改和创建.例如,可以在js
中这样书写:
let addr = ""
for(let i =0;i<4;i++){addr += customer[`address${i}`]+"\n"
}
这段代码读取并拼接了customer对象的属性address0,address1,address2和address3.
这个简单的示例演示了使用数组表示法通过字符串表达式访问对象属性的灵活性.这段代码也可以使用点表达式法重写,但某些场景只有使用数组表示法才行得通.例如,假设你在写一个程序,利用网络资源计算用户在股市上的投资的价值.这个程序允许用户添加自己持有的每支股票的名称和数量.假设使用名为portfolio的对象来保存这些信息,该对象对每支股票都有一个属性,其中每个属性名都是股票的名字,而属性值是该股票的数量.因此如果一个用户持有50股IBM股票,则portfolio.ibm属性的值就是50
function addstock(portfolio,stockname,shares){portfolio[stocname] = shares;
}
由于用于是在运行的时候输出股票的名字,不可能提前知道属性名.既然不可能在写程序的时候就知道属性名,那就没法使用.操作符访问portfolio对象的属性不过,可以使用[]操作符,因为他使用字符串值(字符串是动态的,可以在运行时修改)而不是标识符(标识符是静态的,必须硬编码到程序中)来命名属性
第五章曾介绍过for/in循环(稍后6.6节还会看到).这个js
语法的威力在结合关联数组一起使用时可以明显体现出来.以下代码演示了如何计算投资组合的总价值:
function computeValue(portfolio){let total = 0.0for(let stock in portfolio){// 对于投资组合中的每只股票let shares = portfolio[stock];// 取得股票的数量let price = getQuete(stock);//查询股票total+=shares*price;}return total;// 返回总价值
}
js
对象经常像这样作为关联数组使用,理解其原理非常重要.不过在es6
及之后的版本中,使用Map类(将在11.1.2节介绍)通常比使用普通对象更好.
6.3.2继承
js
对象有一组"自有属性",同时也从他们的原型对象继承一组属性.要理解这一点,必须更详细地分析属性存取.本节的示例将使用Object.create()
函数以指定原型来创建对象.不过在第九章我们将看到,每次通过new创建一个类的实例,都会创建从某个原型对象继承属性的对象.
假设要从对象o中查询属性x.如果o没有叫这个名字的自有属性,则会从o的原型对象(记住,所有对象都有原型,但大多数对象没有prototype属性.即便不能通过代码直接访问对象的原型,js
继承机制任然照常运行,了解背后的细节,可以参考14.3节)查询属性x.
如果原型对象上也没有叫这个名字的自有属性,但他有自己的原型,则会继续查询这个原型的原型.这个过程一直持续,直到找到属性x或者查询到一个原型为null的对象.可见,对象通过其prototype属性创建了一个用于继承属性的链条或链表:
let o = {}// o从`Object.prototype`继承对象方法
o.x = 1;// 现在他有了自有属性x
let p = Object.create(o); // p从o的`Object.prototype`继承属性
p.y = 2;// 而且有一个自有属性y
let q = Object.create(p); // q从p,o和`Object.prototype`继承属性
q.z = 3;// 且有一个自有属性z
let f = q.toString();// toString继承自`Object.prototype`
q.x + q.y;// 3 x和y分别继承自o和p
现在假设你为对象o的x属性赋值.如果o有一个名为x的自有(非继承)属性,这次赋值就会修改已有x属性的值.否则,这次赋值会在对象o上创建一个名为x的新属性.如果o之前继承了属性x,那么现在这个继承的属性会被新创建的同名属性隐藏(就近原则).
属性赋值查询原型链只为确定是否允许赋值.如果o继承了一个名为x的只读属性,则不允许赋值(关于什么情况下可以设置属性可以参考6.3.3节)不过,如果允许赋值,则只会在原始对象上创建或设置属性,而不会修改原型链中的对象.查询属性时会用到原型链,而设置属性时不影响原型链是一个重要的js
特性,利用这一点,可以选择性的覆盖继承的属性:
let unitcircle = {r:1};// c继承自的对象
let c = Object.create(unitcircle);//c继承了属性r
c.x = 1;c.y=1;//c定义了两个自有属性
c.r = 2;//c覆盖了他继承的属性
unitcircle.r // 1原型不受影响
属性赋值要么失败要么在原始对象上创建或设置属性的规则有一个例外.如果o继承了属性x,而该属性是一个通过设置方法定义的访问器属性(参见6.10.6),那么就会调用该设置方法而不会在o上创建新属性x.要注意,此时会在对象o上而不是在定义该属性的原型对象上调用设置方法.因此如果这个设置方法定义了别的属性,那么也会在o上定义同样的属性,但仍然不会修改原型链.
6.3.3属性访问错误
属性访问表达式并不总是会返回或设置值.本节解释查询或设置属性时可能出现的情况.
查询不存在的属性不是错误.如果在o的自有属性和继承属性中都没有找到属性x,则属性访问表达式o.x的求值结果为undefined.例如,book对象有一个"sub-title"属性,没有"subtitle"属性:
book.subtille // undefined :属性不存在
然而,查询不存在的对象的属性则是错误.因为null和undefined值没有属性,查询这两个值的属性是错误.继续前面的示例:
let len = book.subtitle.length;// TypeError:undefined没有length属性
如果.的左边是null或undefined,则属性访问表达式会失败.因此在写类似book.author.surname这样的表达式时,要确保book和book.author是有定义的.以下是两种防止这类问题的写法:
// 简单但是麻烦的技术
let surname = undefined;
if(book){if(book.author){surname = book.author.surname}
}
// 取得surname,null或undefined的简洁惯用技术
surname = book && book.author&&book.author.surname;
// ?? 如果不是null或undefined继续
// surname = book??book.author??book.author.surname
如果不理解这个惯用表达式为什么以防止TypeError异常,可以回头看看4.10.1节中的内容.
正如4.4.1节介绍的,es2020通过?.支持条件式属性访问,用他可以把前面的赋值表达式改写成:
let surname = book?.author?.surname;
尝试在null或undefined上设置属性也会导致TypeError.而且,尝试在其他值上设置属性也不总是会成功,因为有些属性是只读的,不能设置,有些对象不允许添加新属性.在严格模式下(5.6.3节),只要尝试设置属性失败就会抛出TypeError.在非严格模式下,这些失败通常是静默失败.
关于属性赋值什么时候成功,什么时候失败的规则很容易理解,但却不容易只用简单几句话说清楚.尝试在对象o上设置属性p在以下情况会失败
- o有一个只读自有属性p:不可能设置只读属性.
- o有一个只读继承属性p:不可能用同名自有属性隐藏只读继承属性.
- o没有自有属性p,o没有继承通过设置方法定义的属性p,o的extensible特性(参加14.2节)是false.因为p在o上并不存在,如果没有要调用的设置方法,那么p必须要添加到o上.但如果不可扩展(extensible为false),则不能在他上面定义新属性
6.4删除属性
delete操作符(参见4.13.4节)用于从对象中移除属性.他唯一的操作数应该是一个属性访问表达式.令人惊讶的是,delete并不操作属性的值,而是操作属性的本身:
delete book.author;// book对象现在没有author属性了
delete book["main title"]// 现在也没有"main title"属性了
delete操作符只删除自有属性,不删除继承属性(要删除继承属性,必须从定义属性的原型对象上删除.这样做会影响继承该原型的所有对象).
如果delete操作成功或没有影响,(如删除不存在的属性),则delete表达式求值为true.对非属性访问表达式(无意义的)使用delete,同样也会求值为true:
let o = {x:1}// o有自有属性x和继承属性toString
delete o.x;//true
delete o.x;// true什么也不做,但是返回true
delete o.toString // true 什么也不做,toString不是自有属性
delete 1;// true 无意义,但仍然返回true
delete不会删除cofigurable特性为false的属性.与通过变量声明或函数声明创建的全局对象的属性一样.某些内置对象的属性也是不可配置的.在严格模式下,尝试删除不可配置的属性会导致TypeError,在非严格模式下,delete直接求值为false.
// 在严格模式下,以下所有删除操作都会抛出TypeError,而不会返回false
delete Objec.prototype // false,属性不可配置
var x = 1;// 声明一个全局变量
delete globalThis.x // false:不能删除这个属性
function f(){}// 声明一个全局函数
delete globalThis.f // false 也不能删除这个属性
在非严格模式下删除全局对象可配置的属性时,可以省略对全局对象的引用,只在delete操作符后面加上属性名:
globalThis.x = 1;// 创建可配置的全局属性(没let或var)
delete x;// true:这个属性可删除
在严格模式下,如果操作数是一个像x这样的非限定标识符,delete会抛出SyntaxError,即必须写出完整的属性访问表达式:
delete x;// 在严格模式下报SyntaxError
delte globalThis.x // 这样可以
6.5测试属性
js
对象可以别想象为一组属性,实际开发中经常需要测试这组属性的成员关系,即检查对象是否有一个给定名字的属性.为此,可以使用in操作符,或者hasOwnProperty(),propertyIsEnumerable()方法,或者直接查询响应属性.下面的示例都使用字符串作为属性名,但这些实例也适用于符号属性(6.10.3节)
in操作符要求左边是一个属性名,右边是一个对象.如果对象有包含响应名字的自有属性或继承属性,将返回true:
let o = {x:1}
"x" in o;// true
"y" in o // false o没有属性:"y"
"toString" in o;// true:o继承了toString方法
对象的hasOwnProperty()方法用于测试对象是否有给定名字的属性.对继承的属性,他返回false:
let o = {x:1}
o.hasOwnProperty("x");// true o有自有属性x
o.hasOwnProperty("y");// false o有自有属性y
o.hasOwnProperty("toString");// false toString是继承属性
propertyIsEnumerable()方法细化了hasOwnProperty()测试.如果传入的命名属性是自有属性且这个属性的enumerable特性为true,这个方法都会返回true.某些内置属性是不可枚举的.使用常规的js
代码创建的属性都是可枚举的,除非使用14.1节的技术他们限制为不可枚举:
let o = {x:1}
o.propertyIsEnumerable("x")// true,o有一个可枚举属性x
o.propertyIsEnumerable("toString")// false:toString不是自有属性
`Object.prototype`.propertyIsEnumerable("toString")// false toString不可枚举
除了使用in操作符,通常简单的属性查询配合!==确保其不是定义的就可以了:
let o = {x:1}
o.x !== undefined // true o有属性值
o.y !== undefined // false o没有属性y
o.toString !== undefined // true o继承了toString属性
但有一件事in操作符可以做,而简单的属性访问技术做不到.in可以区分不存在的属性和存在但被设置为undefined的属性:
let o = {x:undefined}// 把属性显示设置为undefined
o.x !== undefined // false 属性x存在但值是undefined
o.y !== undefined // false o没有属性y,不存在
"x" in o // true 属性x存在
"y" in o // false:属性y不存在
delete o.x // 删除属性x
"x" in o // false:属性x不存在
6.6枚举属性
处理测试属性是否存在,有时候也需要遍历或获取对象的所有属性.为此有几种不同的实现方式.
5.4.5节介绍的for/in循环对指定对象的每个可枚举(自有或继承)属性都会运行一次循环体,将属性的名字赋给循环变量.对象继承的内置方法是不可枚举的,但你的代码添加给对象的属性默认是可枚举的,例如:
let o = {x:1,y:2,z:3}// 3可枚举自有属性
o.propertyIsEnumerable("toString")// false,toString不可枚举(也不是自有属性)
for(let p in o){console.log(p)// x,y,z,没有toString
}
为防止通过for/in枚举继承的属性,可以在循环体内添加一个显示测试:
for(let p in o){if(!o.hasOwnProperty(p))continue;//跳过继承属性
}
for(let p in o){if(typeof o[p]==="function")continue;// 跳过所有方法.
}
除了使用for/in循环,有时候可以先获取对象所有属性名的数组,然后再通过for/of循环遍历该数组.有4个函数可以用来获取属性名数组:
-
Object.keys()返回对象可枚举自有属性名的数组,不包含不可枚举属性,继承属性或名字是符号的属性(6.10.3)
-
Object.getOwnPropertyNames()与Object.keys()类似.,但也会返回不可枚举自有属性名的数组,只要他们的名字是字符串
-
Object.getOwnPropertySymbols()返回名字是符号的自有属性,无论是否可枚举
-
Reflect.ownKeys()返回所有属性名,包括可枚举和不可枚举属性,以及字符串属性和符号属性(参见14.6节)
6.6.1属性枚举顺序
es6
正式定义了枚举对象自有属性的顺序.Object.keys(),Object.getOwnPerpertyNames(),Object.getOwnPerpertySymbols(),Reflect.ownKeys()及js
ON.stringify()等相关方法都按照下面的顺序列出属性,另外也受限于他们要列出不可枚举属性还是字符串属性或符号属性.
- 先列出名字为非负整数的字符串属性,按照数值顺序从大到最小.这条规则意味着数组和类数组对象的属性会按照顺序被枚举
- 在列出类数组索引的所有属性之后,在列出所有剩下字符串的名字(包括看起来像负数或浮点数的名字)的属性.这些属性按照他们添加到对象的先后顺序列出.对于在对象字面量中定义的属性,按照他们在字面量中出现的顺序列出
- 最后,名字为符号对象的属性按照他们添加到对象的先后顺序列出
for/in循环的枚举顺序并不想上述枚举函数那么严格,但实现通常会按照上面描述的顺序枚举自有属性,然后再沿原型链上溯,以同样的顺序枚举每个原型对象的属性.不要要注意,已经有同名属性被枚举过了,甚至如果有一个同名属性是不可枚举的,那这个属性不会枚举了.
6.7拓展对象
在js
程序中,把一个对象的属性复制到另一个对象上是很常见的,使用下面的代码很容易做到:
let target = {x:1},souce = {y:2,z:2};
for(let key of Object.keys(source)){target[key] = source[key];
}
targer // {x:1,y:2,z:3}
因为这个是个常见操作,各种js
框架纷纷为此定义了辅助函数,通常会命名为extend().最终,在es6
中,这个能力已Object.assign()的形式进入了核心js
语言.
Object.assign()接受两个或多个对象作为其参数.他会修改并返回第一个参数,第一个参数是目标对象,但不会修改第二以及后续参数,那些都是来源对象.对于每个来源对象,他会把该对象的可枚举自有属性(包括名字为符号的属性)赋值到目标对象.他按照参数列表顺序逐个处理来源对象,第一个来源对象的属性会覆盖目标对象的同名属性,而第二个来源对象(如果有)的属性会覆盖第一个源来对象的同名属性.
Object.assgin()以普通的属性获取和设置方式复制属性,因此如果一个来源对象有获取方法或目标对象有设置方法,则他们会在复制期间被调用,但这些方法本身不会被复制.
将属性从一个对象分配到另一个对象的一个原因是,如果有一个默认对象为很多属性定义了默认值,并且如果该对象中不存在同名属性,可以将这些默认属性复制到另一个对象中.但是,想下面这样简单的使用Object.assign()不会达到目的:
Object.assign({},defaults);// 用defaults覆盖o的所有属性
此时,需要创建一个新对象,先把默认值复制到新对象中,然后在使用o的属性覆盖那些默认值:
o = Object.assign({},defaults,o);
在后面6.10.4节我们会看到,使用扩展操作符…也可以表达这种对象复制和覆盖操作:
o = {...defaults,...o};
为了避免额外的对象创建和复制,也可以重写一版Object.assign(),只赋值那些不存在的属性:
// 与Object.assign()类似,但不覆盖已经存在的属性
function merge(target,...source){for(let source of sources){for(let key of Object.key(source)){if(!(key in target)){// 这里跟Object.assign()不同target[key] = source[key];}}}return taregt
}
Object.assign({x:1},{x:2,y:2},{y:3,z:4})// {x:2,y:3,z:4}
merge({x:1},{x:2,y:2},{y:3,z:4}) // {x:1,y:2,z:4}
编写类似merge()的属性操作辅助方法很简单.例如,可以写一个restrict()函数,用于从一个对象中删除另一个模板对象没有的属性.或者写一个subtract()函数,用于从一个对象中删除另一对象包含的所有属性.
6.8序列化对象
对象序列化(serialization)是把队形的状态转换为字符串的过程,之后可以从中恢复对象的状态.函数js
ON.stringify()和js
ON.parse()用于序列化和恢复js
对象.这两个函数使用js
ON数据交换格式js
ON表示JavaScript Object Notation(js
对象表示法),其语法与js
对象和数组字面量非常类似:
let o = {x:1,y:{z:[false,null,""]}} // 定义一个测试对象
let s = `js`ON.stringify(o)// s == '{"x":1,"y":{"z":false,null:""}}'
let p = `js`ON.parse(s);// p == {x:1,y:{z:[false,null,""]}}
js
ON语法是js
语法的子集,不能表示所有js
的值.可以序列化和恢复的值包括对象,数组,字符串,有限数值,true,false和null.NaN,Infinity和-Infinity会被序列化为null.日期对象会被序列化为ISO格式的日期字符串(参见Date.tojs
ON()函数),但js
ON.parse()会保持其字符形式,不会恢复原始的日期对象.函数,RegExp和Error对象以及undefined值不能被序列化或恢复.js
ON.stringify()只序列化对象的可枚举自有属性.如果属性值无法序列化,则该属性会从输出的字符串中删除.js
ON.stringify()和js
ON.parse()都接受可选的第二个参数,用于自定义序列化及恢复操作.例如,可以通过这个参数指定要序列化那些属性,或者在序列化或字符串化过程中如何在转换某些值.11.6节包含这两个函数的完整介绍.
6.9对象方法
如前所述,所有js
对象(除了那些显示创建为没有原型的)都从**Object.prototype
继承属性**.这些继承的属性主要是方法,因为他们几乎无处不在,所以对js
程序而言特别重要.例如,前面我们已经看到过hasOwnProperty()和propertyIsEnumerable()方法了(而且我们也介绍了几个定义在Object构造函数上的静态方法,例如Object.create()
和Object.keys()).本节讲解了Object.prototype
上定义的几个通用方法,但这些很有可能被更特定的实现锁取代.后面几节我们将展示在同一个对象上定义这些方法的示例.在第九章,我们还给学习如果为整个对象的类定义更加通用的方法.
6.9.1toString()方法
toString()方法不接受参数,返回表示调用他的对象的值的字符串.每当需要把一个对象装换为字符串时,js
就会调用该方法.例如,在使用+操作符拼接一个字符串和一个对象时,或者把一个对象转入期望字符串参数的方法时.
默认的toString()方法并不能提供太多信息(但可以用于确定对象的类,如14.4.3节所示)例如,下面这行代码只会得到字符串[object Object]:
let s = {x:1,y:1}.toString();// s == "[object Object]"
由于这个默认方法不会显示太有用的信息,很多类都会重新定义自己的toString()方法.例如,在把数组转换为字符串时,可以得到数组元素的一个列表,每个元素也都会转换为字符串.而把函数转换为字符串时,可以得到函数的源代码.可以像下面这样定义自己的toString()方法:
let point = {x:1,y:2,toString:funnction(){return `(${this.x},${this.y})`}
}
String(point);// "(1,2)用于转换字符串"
6.9.2toLocaleString()方法
除了基本的toString()方法之外,对象也都有一个toLocaleString()方法.这个方法的用途是返回对象的本地化字符串表示.Object定义的默认toLocaleString()方法本身没有实现任何本地化,而是简单的调用了toString()并返回该值.Date和Number类定义了自己的toLocaleString()方法,尝试根据本地惯例格式化数值,日期和时间,数组也定义了一个与toString()类似的toLocaleString()方法,只不过他会调用每个数组元素的toLocaleString()方法,而不是调用toString()方法.对于掐面的point对象,我们也可以如法炮制:
let point = {x:1000,y:2000,toString:function(){return this.x+","+this.y},toLocaleString:function(){this.x.toLocaleString()+","+this.y.toLocaleString()}
}
point.toString()// (1000,2000)
point.toLocaleString()//(1,000, 2,000)注意千分位分隔符
11.7节介绍的国际化类可以用于实现toLocaleString()方法
6.9.3valueOf()方法
valueOf()方法与toString()方法类似,但会在js
需要把对象转换为某些非字符串原始值(通常是数值)时被调用.如果在需要原始值的上下文中使用了对象,js
会自动调用这个对象的valueOf()方法.默认的valueOf()方法并没有做什么,因此一些内置类定义了自己的valueOf().Date类定义的valueOf()方法可以将日期转换为数值,这样就让日期对象可通过<和>操作来进行比较.类似地,对于point对象,我们也可以定义一个返回原点与当前点之间距离的valueOf():
let point = {x:3,y:4,valueOf:function(){return Math.hypot(this.x,this.y)}
}
Number(point)// 5 valueOf()用于转换为数值
point>4
point>5 false
point<6 true
6.9.4tojsON()
方法
Object.prototype
tojson方法,但JSON.stringify()方法(参加6.8节)会从要序列化的对象上寻找tojson方法.如果要序列化的对象上存在这个方法,就会调用他,然后序列化该方法的返回值,而不是原始对象.Date类(参见11.4节)定义了自己的tojson方法,返回一个表示日期的序列化字符串,同样,我们也可以给point对象定义这个方法:
let point = {x:1,y:2,toString:functino(){return this.x+","+this.y},to`js`ON:function(){return this.toString()}
}
`js`ON.stringify([point])
6.10对象字面量扩展语法
最近的js
版本从几个方面扩展了对象字面量语法,下面将讲解这个扩展
6.10.1简写属性
假设变量x和y中保存着值,而你想创建一个具有属性x和y且值分别为相应变量值的对象.如果你使用基本的对象字面量语法,需要把每个标识符重复两次:
let x = 1,y =2;
let o ={x:x,y:y}
o.x+o.y // 3
在es6
之后,可以删除其中的分号和一份标识符,得到非常简洁的代码
let x =1,y=2
let o = {x,y}
o.x + o.y //3
6.10.2计算的属性名
有时候,我们需要创建一个具有特定属性的对象,但该属性的名字不是编译时可以直接写在源代码中的常量.相反,你需要的这个属性名保存在一个变量里,或者是调用的某个函数的返回值.不能对这或在那个属性使用基本对象字面量,为此,你必须先创建一个对象,然后再为它添加想要的属性:
const PROPERY_NAME = "p1"
function computePropertyName(){return "p"+2}
let o = {}
o[PROPERY_NAME] = 1
o[computePropertyName()] = 2
而使用es6
称为计算属性的特性可以更简单的创建类似对象,这个特性可以让你直接把前面代码中的方括号放在对象字面量中:
const PROPERY_NAME = "p1"
function computePropertyName(){return "p"+2}
let p = {[PROPERTY_NAME]:1,[computePropertyName()]:2
}
p.p1+p.p2
有了这个语法,就可以在方括号中加入任意js
表达式,对这个表达式求值得到的结果(必要时候转换字符串)会用作属性的名字:
一个可能需要计算属性的场景是,有一个js
代码库,需要个这个库传入一个包含一组特定属性的对象,而这组属性的名字在该库是以常量形式定义的.如果通过代码来创建要传给该库的这个对象,可以硬编码他的属性名,但是这样有可能把属性名写错,同时存在因为版本升级,而修改了属性名而导致的错配问题.此时,使用库自身定义的属性名常量,通过计算属性语法来创建这个对象会让你的代码更牢靠.
6.10.3符号作为属性名
计算属性语法也让另一非常重要的对象字面量特性称为可能,在es6
之后,属性名可以是字符串或符号,如果把符号赋值给一个变量或常量,那么可以使用计算属性语法将该符号作为属性名:
const extension = Symbol("my extension symbol")
let o = {[extension]:{/**/}
}
o[entension].x = 0;// 这个属性不会与o的其他属性冲突
如3.6节所解释,符号是不透明值,除了用作属性名之外,不能用他们做任何事情.不过,每个符号都与其他符号不同,这意味着符号非常适合用创建唯一属性名.创建新符号需要调用Symbol()工厂函数(符号是原始值,不是对象,因此Symbol()不是构造函数,不能使用new调用),Symbol()返回的值不等于任何其他符号或其他值.可以给Symbol()传一个字符串,在把符号转换为字符串时会用到这个字符串.但这个字符串的作用仅限于辅助调试,使用相同字符串参数创建的两个符号依旧是不同的符号.
使用符号不是为了安全,而是js
对象定义安全的扩展机制,如果你从不受控制的第三方代码得到一个对象,然后需要为该对象添加一些自己的属性,但有不希望你的属性与该对象原有的任何属性冲突,那就可以放心地使用符号作为属性名.,而且,这样一来,你也不用担心第三方代码会意外修改你以符号命名的属性(当然,第三方代码可以使用Objecty.getOwnPerpertySymbols()找到你使用的符号,然后修改或删除你的属性.这也是符号不是一种安全机制的原因)
6.10.4扩展操作符
es2018及之后,可以在对象字面量中使用"拓展操作符"…把已有对象的属性复制到新对象中:
let position = {x:0,y:0}
let dimensions = {width:100,height:75};
let rect = {...position,...dimension}
rect.x + rect.y + rect.width + rect.height // 175
这段代码把position和dimension对象的属性"拓展"到了react对象字面量中,就像直接把他们的属性写在了花括号中一样,注意,这个…语法经常被称为扩展操作符,但却不是真正意义上的js
操作符,实际上,他是仅在对象字面量中有效的一种特殊语法(在其他js
上下文中,三个点有其他用途.只有在对象字面量中,三个点才会产生这种把一个对象的属性复制到另一个对象中的插值行为)
如果拓展对象和被扩展对象有一个同名属性,那么这个属性的值由后面的对象决定:
let o = {x:1}
let p = {x:0,...o};
p.x // 1
另外要注意,扩展操作符只扩展对象的自有属性,不扩展任何继承属性:
let o = Object.create({x:1})// o继承属性x
let p = {...o};
p.x // undefined
最后,还有一点需要注意,虽然扩展操作符在你的代码中只是一个三个小圆点,但他可能给js
解释器带来了巨大的工作量.如果对象有n个属性,把这个属性扩展到另一个对象可能是O(n)操作,.这意味着,如果在循环或递归函数中通过…操作符像一个大对象不断追加属性,则很可能你是在写一个低效的o(n^2)算法.随着n越来越大,这个算法可能会成为性能瓶颈.
6.10.5简写方法
在把函数定义为对象属性时,我们称该函数为 方法(第八章和第九章包含更多关于方法的内容).在es6
之前,需要想定义对象的其他属性一样,通过函数定义表达式在对象字面量中定义一个方法:
let square ={area:function(){return this.side * this.side}side:10
}
square.area() // 100
但在es6
中,对象字面量语法(也包括第九章将介绍的类定义语法)尽管拓展,允许一种省略function关键字和冒号的简写方法,结果代码如下:
let square = {area(){return this.side*this.side},side:10
}
square.area() // 100
这两端代码是等价的,都会给对象字面量添加一个名为area的属性,都会把该属性的值设置为指定函数.这种简写语法让人一看就知道area()是方法,而不是像side一样的数据属性.
在使用这种简写语法来写方法时,属性名可以是对象字面量运行的任何形式,除了像上面的area一样的常规js
标识符之外,也可以使用字符串字面量和计算的属性名,包括符号属性名:
const METHOD_NAME = "m"
const symbol = Symbol()
let weirMethods = {"method With Spaces"(x){return x+1},[METHOD_NAME](x){return x+2},[symbol](x){return x+3}
}
weirMethods["method With Spaces"](1) // 2
weirMethods[METHOD_NAM](1) // 3
weirMethods[symbol](1) // 4
使用符号作为方法名并没有看起来那么稀罕,为了让对象可迭代(以便for/of循环中使用),必须使用Symbol.iterator为它定义一个方法,地12章给出定义这个方法的实例.
6.10.6属性的获取方法与设置方法
到目前为止,本章讨论的所有对象属性都是数据属性,即有一个名字和普通的值.处理数据属性之外,js
还支持为对象定义访问器属性(accessor property),这种属性不是一个值,而是一个或两个访问器方法:一个获取方法(getter)和一个设置方法(setter)
当程序查询一个访问器属性的值时,js
会调用获取方法(不传递参数).这个方法的返回值就是属性访问表达式的值.当程序设置一个访问属性的值时,js
会调用设置方法,传入赋值语句右边的值,从某种意义上说,这个方法负责"设置"属性的值.设置方法的返回值会被忽略.
如果一个属性既有获取方法也有设置方法,则该属性是一个可读写属性.如果只有一个获取方法,那他就是只读属性.如果只有一个设置方法,那他就是只写属性(这种属性通过数据属性是无法实现的),读取这种属性始终会得到undefined.
访问器属性可以通过对字面量的一个扩展语法来定义(与前面我们看到的其他es6
扩展不同,获取方法和设置方法是在ES5引入的):
let o = {// 一个普通的数据属性dataProp:value,//通过一对函数定义的一个访问器属性get accessorProp(){return this.dataProp;},set accessorProp(value){this.dataProp = value}
}
访问器属性是通过一个或两个方法来定义的,方法名就是属性名.除了前缀是get和set之外,这两个方法看起来就像是用es6
简写语法定义的普通方法一样(在es6
中,也可以使用计算的属性名来定义获取方法和设置方法,只要把get和set后面的属性名替换为用方括号的表达式即可)
上面定义的访问器方法只是简单的获取和设置了一个数值属性的值,这种情况使用数据属性或访问器属性都是可以的.
不过我们可以看一个有趣的示例,例如下面这个表示2d笛卡尔坐标点的对象.这个对象用普通数据属性保存点x和y坐标,用访问器属性给出与这点等价的极坐标:
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/oldvaluethis.x *= ratiothis.y *= ratio}// theta是一个定义了获取方法的只读访问器属性get theta(){return Math.atan2(this.y,this.x)}
}
p.r // Math.SQRT2
p.theta // Math.PI/4
注意这个示例的获取和设置方法中使用了关键字this。js
会将这些函数作为定义他们的对象的方法来调用.这意味着在这些函数体内,this引用的是表示坐标点的对象p。因此访问器属性r的获取方法可以通过this.x和this.y来引用坐标点的x和y属性.方法和this关键字将在8.2.2节中介绍.
与数据属性一样,访问器属性也是可以继承的.因此,可以把上面定义的对象p作为其他点的原型.可以给对象定义自己的x和y属性,而他们将继承r和theta属性:
let q = Object.create(p)// 一个继承获取和设置方法的新对象
q.x = 3;q.y = 4;// 创建q的自有数据属性
q.r // 5使用继承的访问器属性
q.theta // Math.atan2(4,3)
以上代码使用访问器属性定义了一个API,提供了一个数据集的两种表示(笛卡尔坐标和极坐标).使用访问器属性的其他场景还有写入属性时进行合理性检查,以及每次读取属性时返回不同的值:
// 这个对象保证序号严格递增
const serialnum = {// 这个数据属性保存下一个序号// 属性名中的_提示他仅在内部使用_n:0,// 返回当前值并递增get next(){return this._n++;}set next(n){if(n>this._n)this._n = n;else throw new Error("serial number can only be set to a larger value")}
}
serialnum.next = 10 // 设置起始序号
serialnum.next // 10
serialnum.next // 11 每次读取next都得到不同的值
最后再看一个通过获取方法实现"魔法"属性的示例
// 这个对象的访问器属性返回随机数值
const random = {get octet(){return math.floor(Math.random()*256)},get uint16(){return math.floor(Math.random()*65535)},get int16(){return math.floor(Math.random()*65535)-32768}
}
6.11小结
学习对象的属性