JavaScript权威指南第6章 对象

18 篇文章 0 订阅
2 篇文章 0 订阅

6.1 对象简介

对象是一种复合值,它汇聚多个值(原始值或其他对象)并允许我们按名字存储和获取这些值。对象是一个属性的无序集合,每个属性都有名字和值。属性名通常是字符串(也可以是符号),因此可以说对象把字符串映射成值。这种字符串到值得映射曾经有很多种叫法,包括“散列”“散列表”“字典”或“关联数组”等熟悉的基本数据结构。不过,对象不仅仅是简单的字符串到值的映射。除了维持自己的属性之外,JavaScript对象也可以是从其他对象继承属性,这个其他对象称为其“原型”。对象的方法通常是继承来的属性。而这种“原型式继承”也是JavaScript的主要特征。

JavaScript对象是动态的,即可以动态添加和删除属性。不过,可以用对象来模拟静态类型语言中的静态对象和“结构体”。对象也可以用于表示一组字符串(忽略字符串到值的映射中的值)。

在JavaScript中,任何不是字符串、数值、符号或true、false、null、undefined的值都是对象。即使字符串、数值和布尔值不是对象,它们的行为也类似不可修改的对象。

对象是可修改的,是按引用操作而不是按值操作的如果变量x指向一个对象,则代码let y=x;执行后,变量y保存的是同一个对象的引用,而不是该对象的副本。通过变量y对这个对象所做的任何修改,在变量x上都是可见的。

属性有一个名字和一个值。属性名可以是任意字符串,包括空字符串(或任意符号),但对象不能包含两个同名的属性。值可以是任意JavaScript值,或者是设置函数或获取函数(或两个函数同时存在)。

有时候,区分直接定义在对象上的属性和那些从原型上继承的属性很重要。JavaScript使用术语“自有属性”指代非继承属性。

除了名字和值之外,每个属性还有3个属性特征:

  • writable(可写特性指定是否可以设置属性的值)
  • enumertble(可枚举)特性指定是否可以在for/in循环中返回属性的名字。
  • configurable(可配置)特性指定是否可以删除属性,以及是否可修改其特性。

很多JavaScript内置对象拥有只读、不可枚举或不可配置的属性。不过,默认情况下,我们所创建对象的所有属性都只是可写、可枚举和可配置的。

6.2 创建对象

对象可以通过对象字面量、new关键字和Object.create()函数来创建。

6.2.1 对象字面量

创建对象最简单的方式是在JavaScript代码中直接包含对象字面量。对象字面量的最简单形式是包含在一对花括号中的一组逗号分隔的“名:值”对。属性名和JavaScript标识符或字符串字面量(允许空字符串)。属性值是任何JavaScript表达式,这个表达式的值(可以是原始值或对象值)会变成属性的值。下面看几个实例:

let empty={};
let point={x:0,y:0};
let p2={x:point.x,y:point.y+1};
let book={
    "main title":"JavaScript",
    "sub-title":"The Definitive Guide",
    for:"all audiences",
    author:{
       firstname:"David",
       surname:"Flanagan"
    }
};

对象字面量最后一个属性后面的逗号是合法的,有些编程风格指南鼓励添加这些逗号,以便将来在对象字面量末尾再添加新属性时不会导致语法错误。

对象字面量是一个表达式,每次求值都会创建一个并初始化一个新的、不一样的对象。字面量每次被求值的时候,它的每个属性的值也会重新被求值。这意味着同一个对象字面量如果出现在循环体中,或出现在被重复调用的函数体内,可以创建很多新对象,且这些对象属性的值可能不同。

6.2.2 使用new 创建对象

new操作数用于创建和初始化一个新对象。new关键字后面必须跟一个函数调用。以这种方式使用的函数被称为构造函数(constructor),目的是初始化新创建的对象。JavaScript为内置的类型提供了构造函数。例如:

let o=new Object();        //创建一个空对象,与{}相同
let a=new Array();         //创建一个空数组,与[]相同
let d=new Date();          //创建一个表示当前时间的日期对象
let r=new Map();           //创建一个映射对象,用于存储键/值映射

除了内置的构造函数,我们经常需要定义自己的构造函数来初始化新创建的对象。

6.2.3 原型

几乎每一个JavaScript对象都有另一个与之关联的对象。这另一个对象被称作原型,第一个对象从这个原型继承属性。

通过对象字面量创建的所有对象都有相同的原型对象,在JavaScript代码中可以通过Object.prototype引用这个原型对象。使用new关键字和构造函数调用创建的对象,使用构造函数propotype属性的值作为它们的原型。换句话说,使用new Object()创建的对象继承自Object.propotype,与通过{}创建的对象一样。类型地,通过new Array()创建地对象以Array.propotype为原型,通过new Date()创建地对象以Date.propotype为原型。对于JavaScript初学者,这一块很容易被迷惑。记住:几乎所有对象都有原型,但只有少数对象有propotype属性。正是这些有propotype属性的对象为所有其他对象定义了原型。

==Object.propotype是为数不多地没有原型的对象,因为它不继承任何属性。==其他原型对象都是常规对象,都有自己的原型。多数内置构造函数(和多数用户定义的构造函数)都继承自Object.propotype。例如,Date.propotype从Object.propotype继承属性,因此通过new Date()创建的日期对象从Date.propotype和Object.propotype继承属性。这种原型对象链接起来的序列被称为原型链。

6.2.4 Object.create()

Object.create()用于创建一个新对象,使用其第一个参数作为新对象的原型。

传入null可以创建一个没有原型的新对象。不过,这样创建的新对象不会继承任何东西,连toString()这种基本方法都没有(意味着不能对该对象使用+操作符)

如果想要创建一个普通的空对象(类似{}或new Object()返回的对象),传入Object.propotype:

let o3=Object.create(Object.prototype)

Object.create()的一个用途是防止对象被某个第三方库函数意外(但非恶意)修改。这种情况下,不要直接把对象传给库函数,而要传入一个继承自它的对象。如果函数读取这个对象的属性,可以传到继承的值。而如果它设置这个对象的属性,则修改不会影响原始对象。

let o={x:"don't change this value"}
library.function(Object.create(o))      //防止意外修改

6.3 查询和设置属性

要创建或设置属性,与查询属性一样,可以使用点或方括号,只是要把它们放到赋值表达式的左边:

book.edition=7;                      //为book创建一个“edition”属性
book["main title"]="ECMAScript"      //修改“main title”的属性

6.3.1 作为关联数组的对象

如前所示,下面两个JavaScript表达式的值相同:

Object.propertype
Object["propertype"]

第二种方法使用方括号和字符串,看起来像访问数组,只不过是以字符串而非数值作为索引的数组。这种数组也被称为关联数组(或散列、映射、字典)JavaScript对象是关联数组,本节解释为什么这一点很重要。

在C、C++、Java及类似的强类型语言中,对象只有固定数量的属性,且这些属性的名字必须事先定义。JavaScript是松散类型语言,并没有遵守这个规则,即JavaScript程序可以为任意对象创建任意数量的属性。不过,在使用.操作符访问对象的属性时,属性名是通过标识符来表示的。标识符必须直接书写在JavaScript程序中,它们不是一种数据类型,因此不能被程序操作。

在通过花括号([])这种数组表示访问对象属性时,属性名是通过字符串来表示的。字符串是一种JavaScript数组类型,因此可以在程序运行期间修改和创建。例如,可以在JavaScript中这样写:

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[stockname]=shares;
}

由于用户是在运行时输入股票名字,不可能提前知道属性名。既然不可能在写程序时就知道属性名,那就没办法使用.操作符访问portfolio对象的属性。不过,可以使用[]操作符,因为它使用字符串值(字符串是动态的,可以在运行时修改)而不是标识符(标识符是静态的,必须硬编码到程序中)来命名属性。

第5章介绍过for/in循环。这个JavaScript语句的威力在结合关联数组一起使用时可以明显地体现出来。以下代码演示了如何计算投资组合的总价值:

function computeValue(portfolio){
    let total=0.0;
    for(let stock in portfolio){             //对于投资组合中的每只股票
        let shares=portfolio[stock];         //取得股票数量
        let price=getQuote(stock);           //查询股价
        total+=shares*price;                 //把单只股票价值加到总价值上
    }
    return total;                            //返回总价值
}

JavaScript对象经常像这样作为关联数组使用,理解其原理非常重要。

6.3.2 继承

JavaScript对象有一组“自有属性”,同时也从它们的原型对象继承一组属性。要理解这一点,必须更详细地分析属性存取。本节的示例将使用Object.create()函数以指定原型来创建对象。不过在第9章我们将看到,每次new创建一个类的实例,都会从某个原型对象继承属性的对象。

假设要从对象o中查询属性x。如果o没有叫这个名字的自有属性,则会从o的原型对象查询属性x。如果原型对象也没有叫这个名字的自有属性,但它有自己的原型,则会继续查询这个原型的原型。这个过程一直持续,直至找到属性x或者查询到一个原型为null的对象。可见,对象通过其propotype属性创建了一个用于继承属性的链条或链表:

let o={};                       //o从Object.propertyep继承对象方法
o.x=1;                          //现在它有了自有属性x
let p=Object.create(o);         //p从o和Object.propertype继承属性
p.y=2;                          //而且有一个自有属性y
let q=Object.create(p);         //q从p、o和Object.propertype继承属性
q.z=3;                          //且有一个自有属性z
let f=q.toString();             //toString()继承自Object.
q.x+q.y                         //=>3;x和y分别继承自o和p

现在假设你为对象o的x属性赋值。如果o有一个名为x的自有(非继承)属性,这次赋值就会修改已有x属性的值。否则,这次赋值会在对象o上创建一个名为x的新属性。如果o之前继承了x属性,那么现在这个继承的属性会被新创建的同名属性隐藏。

属性赋值查询原型只为确定是否可以赋值。如果o继承了一个名为x的只读属性,则不允许赋值。不过,如果允许赋值,则只会在原始对象上创建或设置只读属性,而不会修改原型链中的对象。查询属性时会用到原型链,而设置属性时不影响原型链是一个重要的JavaScript特性,利用这一点,可以选择性地覆盖继承的属性:

let unitcircle={r:1};
let c=Object.create(unitcircle);        //c继承对象unitcircle
c.x=1;c.y=1;                            //c定义了两个自有属性
c.r=2;                                  //c覆盖了它继承的属性
unitcircle.r                            //原型不受影响
=>1

属性赋值要么失败要么在原始对象上创建或设置属性的规则有一个例外。如果o继承了属性x,而该属性是一个通过设置方法定义的访问器属性,那么就会调用该设置方法而不会在o上创建新属性x。要注意,此时会在对象o上而不是在定义该属性的原型对象上调用设置方法。因此如果这个设置方法定义了别的属性,那也会在o上定义同样的属性,但仍然不会修改原型链。

6.3.3 属性访问错误

查询不存在的属性不是错误。如果在o的自有属性和继承属性中都没找到属性x,则属性访问表达式o.x的求值结果为undefined。

然而查询不存在对象的属性则是错误。因为null和undefined值没有属性,查询这两个值得属性是错误。

如果.得左边是null或undefined,则属性访问表达式会失败。因此在写类型book.author.surname这样的表达式时,要确保book和book.author是有定义的。以下是两种防止这类问题的写法:

let surname=undefined;
if(book){
    if(book.author){
        surname=book.author.surname;
    }
}

相对于:

let surname=book?.author?.surname;

尝试在null或undefined上设置属性也会导致TypeError。而且,尝试在其他值上设置属性也不总算会成功,因为有些属性是只读的,不能设置,而有些对象不允许添加新属性。在严格模式下,只要尝试设置属性失败就会抛出TypeError。在非严格模式下,这些失败通常是静默失败。

关于数学赋值什么时候成功、什么时候失败的规则很容易理解,但却不容易只用简单几句话说清楚。尝试在对象o上设置属性p在以下情况下会失败。

  • o有一个只读属性p:不可能设置只读属性。
  • o有一个只读继承属性:不可能用同名自有属性隐藏只读属性。
  • o没有自有属性p,o没有继承通过设置方法定义的属性p,o的extensible特性是false。因为p在o上并不存在,如果没有要调用的设置方法,那么p必须要添加到o上。但如果o不可扩展,则不能在它上面定义新属性。

6.4 删除属性

delete操作符用于从对象中移除属性。它唯一的操作数应该是一个属性访问表达式。令人惊讶的是,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                   //删除属性x
delete o.x
=>true                   //什么也不做(x不存在)但仍然返回true
delete o.toString
=>true                   //什么也不做(toString不是自有属性)
delete 1
=>true                   //无意义,但仍然返回true
let p={x:1}
let q=Object.create(p)
delete(q.x)
=>true

delete不会删除configurable特性为false的属性。与通过变量声明或函数创建的全局对象的属性一样,某些内置对象的属性也是不可配置的在严格模式下,尝试删除不可配置的属性会导致TypeError。在非严格模式下,delete直接求值为false

delete Object.prototype
=>false                           //属性不可配置
var x=1
delete globalThis.x             
=>false                           //不能删除这个属性
function f2() {}                  //声明一个全局函数
delete globalThis.f2                      
=>false                           //也不能删除这个属性

在非严格模式下删除全局对象可配置的属性时,可以省略对全局对象的引用,只在delete操作符后面加上属性名:

globalThis.x=1;            //创建可配置的全局属性(没有let或var)
delete x;
=>true                     //这个属性可以删除

在严格模式下,如果操作数是一个像x这样的非限定标识符,delete会抛出SyntaxError,即必须写出完整的属性访问表达式:

delete x;                      //在严格模式下报SyntaxError
delete globalThis.x            //这样可以

6.5 测试属性

JavaScript对象可以被想象成一组属性,实际开发中经常需要测试这组属性的关系,即检查对象是否有一个给定名字的属性。为此,可以使用in操作符,或者hasOwnProperty()、propertyIsEnumerable()方法,或者直接查询相应属性。

in操作符要求左边是一个属性名,右边是一个地下。如果对象有包含相应名字的自有属性或继承属性,将返回true:

let o={x:1}
"x" in o
=>true                    //o有自有属性"x"        
"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                          //o继承了toString属性

propertyEnumerable()方法细化了hasOwnProperty()测试。如果传入的命名属性是自有属性且这个属性的enumerable特性为true,这个方法会返回true。某些内置属性是可枚举的,除非使用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.y!==undefined
=>false
o.toString!==undefined
=>true

但有一件事in操作符可以做,而简单的属性访问技术做不到。in可以区分不存在的属性可存在但被设置为undefined的属性。来看下面的代码:

let o={x:undefined}
o.x!==undefined
=>false
o.y!==undefined
=>false
"x" in o
=>true                        //属性x存在
"y" in o
=>false                       //属性y不存在
delete o.x
=>true                        //删除属性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()返回对象可枚举自有属性名的数组。不包含不可枚举属性、继承属性或名字是符号的属性。
  • Object.getOwnPropertyNames()与Object.keys()类似,但也会返回不可枚举自有属性名的数组,只要它们的名字是字符串。
  • Object.getOwnPropertySymbols()返回名字是符号的自有属性,无论是否可枚举。
  • Reflect.ownKeys()返回所有属性名,包括可枚举和不可枚举属性,以及字符串属性和符号属性。

6.6.1 属性枚举顺序

ES6正式定义了枚举对象自有属性的顺序。Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Reflect.ownKeys()及JSON.stringify等相关方法都按照下面的顺序列出属性,另外也受限于它们要列出不可枚举属性还是列出字符串属性或符号属性。

  • 先列出名字为非负整数的字符串属性,按照数值顺序从最小到最大。这条规则意味着数组和类数组对象的属性会按照顺序被枚举。
  • 先列出类数组索引的所有属性之后,再列出所有剩下的字符串名字(包括看起来像负数或浮点数的名字)的属性。这些属性按照它们添加到对象的先后顺序列出。对于在对象字面量中定义的属性,按照它们在字面量中出现的顺序列出。
  • 最后,名字为符号对象的属性按照它们添加到对象的先后顺序列出。

for/in循环的枚举属性并不像上述枚举函数那么严格,但实现通常会按照上面描述的顺序枚举自有属性,然后再沿原型链上溯,以同样的顺序枚举每个原型对象的属性。不过要注意,如果已经有同名属性被枚举过了,甚至如果有一个同名属性是不可枚举的,那这个属性就不会枚举了。

6.7 扩展对象

在JavaScript中,把一个对象的属性复制到另一个对象上是很常见的。使用下面的代码很容易做到:

let target={x:1},source={y:2,z:3}
for(let key of Object.keys(source)){
    target[key]=source[key];
}
target
=>{x: 1, y: 2, z: 3}

但因为这是个常见操作,各种JavaScript框架纷纷为此定义了辅助函数,通常会命名为extend()。最终,在ES6中,这个能力以Object.assign()的形式进入了核心JavaScript语言。

Object.assign()接收两个或多个对象作为其参数。它会修改并返回第一个参数,第一个参数是目标对象,但不会修改第二个及后续参数,那些都是来源对象。对于每个来源对象,它会把该对象的可枚举自有属性(包括名字为符号的属性)复制到目标对象。它按照参数列表顺序逐个处理来源对象,第一个来源对象的属性会覆盖目标对象的同名属性,而第二个来源对象(如果有)的属性会覆盖第一个来源对象的同名属性。

Object.assign()以普通的属性和设置方式复制属性,因此如果一个来源对象有获取方法或目标对象有设置方法,则它们会在复制期间被调用,但这些方法本身不会被复制。

将属性从一个对象分配到另一个对象的一个原因是,如果有一个默认对象为很多属性定义了默认值,并且该对象不存在同名属性,可以将这些默认属性赋值到另一个对象中。但是,想下面这样简单地使用Object.assign()不会达到目的:

Object.assign(o,defaults);               //用defaults覆盖o的所有属性

此时,需要创建一个新对象,先把默认值复制到新对象中,然后再使用o的属性覆盖那些默认值:

o=Object.assign({},defaults,o);

使用扩展操作符…也可以表达这种对象复制和覆盖操作:

o={...defaults, ...o};

为了避免额外的对象创建和复制,也可以重写一版Object.assign(),只复制那些不存在的属性:

function merge(target,...sources){
    for(let source of sources){
        for(let key of Object.keys(source)){
            if(!(key in target)){
                target[key]=source[key];
            }
        }
    }
    return target;
}
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 序列化对象

对象序列化是把对象的状态转换为字符串的过程,之后可以从中恢复对象的状态。函数JSON.toStringify()和JSON.parse()用于序列化和恢复JavaScript对象。这两个函数使用JSON数据交换个数。JSON表示JavaScript Object Notation(JavaScript对象表示法),其语法于JavaScript对象和数组字面量非常类似:

let o={x:1,y:{z:[false,null,""]}};
let s=JSON.stringify(o);
s
=>"{\"x\":1,\"y\":{\"z\":[false,null,\"\"]}}"
let p=JSON.parse(s);
p
=>{x: 1, y: {}}

JSON语法是JavaScript语法的子集,不能表示所有JavaScript的值。可以序列化和恢复的值包括对象、数组、字符串、有限数值、true、false和null。NaN、Infinity和-Infinity会被序列化为null。日期对象会被序列化为ISO格式的日期字符串(参见Date.toJSON()函数),但JSON.parse()会保持其字符串形式,不会恢复原始的日期对象。函数、RegExp和Error对象以及undefined值不能被序列化或恢复。JSON.stringfy()只序列化对象的可枚举自有属性。如果属性值无法序列化,则该属性会从输出的字符串中删除。JSON.stringfy()和JSON.parse()都接收可选的第二个参数,用于自定义序列化及恢复操作。例如,可以通过这个参数指定要序列化哪些属性,或者在序列化或字符串化过程中如何转换某些值。

6.9 对象方法

6.9.1 toString()方法

toString()方法不接收参数,返回调用它的对象的值的字符串。每当需要把一个对象转换为字符串时,JavaScript就会调用该对象的这个方法。例如,在使用+操作符拼接一个字符串和一个对象时,或者把一个对象传入期待字符串参数的方法时。

默认的toString()方法并不能提供太多信息(但可以用于确定对象的类)。例如,下面这行代码只会得到字符串“[object object]”:

let s={x:1,y:1}.toString();
s
=>"[object Object]"

由于这个默认方法不会显示太有用的信息,很多类都会重新定义自己的toString()方法。例如,在把数组转换为字符串时,可以得到数组元素的一个列表,每个元素也都会转换为字符串。而把函数转换为字符串时,可以得到源代码。

可以像下面这样定义自己的toString()方法:

let point={
    x:1,
    y:2,
    toString:function(){
        return `(${this.x},${this.y})`;}
};
String(point)
=>"(1,2)"             //toString()用于转换为字符串

6.9.2 toLocaleString()方法

除了基本的toString()方法之外,对象也都有一个toLocaleString()方法。这个方法返回对象的本地化字符串表示。Object定义的默认toLocaleString()方法本身没有实现任何本地化,而是简单地调用toString()并返回该值。Date和Number类定义了自己的toLocaleString()方法,尝试根据本地惯例格式化数组、日期和时间。数组也定义了一个与toString()类似的toLocaleString()方法,只不过它会调用每个数组元素的toLocaleString()方法,只不过它会调用每个数组元素的toLocaleString()方法,而不是调用它们toString()方法。对于前面的point对象,我们也可以加法炮制:

let point={
    x:1000,
    y:2000,
    toString:function(){ return `(${this.x},${this.y})`;},
    toLocaleString:function(){
        return `(${this.x.toLocaleString()},${this.y.toLocaleString()})`;
    }
};
point.toString()
=>"(1000,2000)"
point.toLocaleString()
=>"(1,000,2,000)

6.9.3 valueOf()方法

valueOf()方法与toString()方法很相似,但会在JavaScript需要把对象转换为某些非字符串原始值(通常是数值)时被调用。如果在需要原始值的上下文中使用了对象,JavaScript会自动调用这个对象的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
=>true
point>5
=>false
point<6
=>true

6.9.4 toJSON()方法

Object.prototype实际上未定义toJSON()方法,但JSON.stringify()方法会从序列化的对象上寻找toJSON()方法。如果序列化的对象上存在这个方法,就会调用它,然后序列化该方法的返回值,而不是原始对象。Date类定义了自己的toJSON()方法,返回一个表示日期的序列化字符串。

6.10 对象字面量扩展语法

6.10.1 简写属性

假设变量x和y中保存着值,而你想创建一个具有属性x和y且值分别为相应变量值的对象。如果使用基本的对象字面量语法,需要每个标识符重复两次:

let x=1,y=2;
let o={
    x:x,
    y:y
};

在ES6及之后,可以删掉其中的分号和一份标识符。

let x=1,y=2;
let o={x,y};
o.x+o.y
=>3

6.10.2 计算的属性名

有时候,我们需要创建一个具有特定属性的对象,但该属性的名字不是编译时可以直接写在源代码中的常量。相反,你需要的这个属性名保存在一个变量里,或者是调用的某个函数的返回值。不能对这种属性明保存在一个变量里,或者的调用的某个函数的返回值。不能对这种属性使用基本对象字面量。为此,必须先创建一个对象,然后再为它添加需要的属性:

const PROPERTY_NAME="p1"
function computePropertyName(){ return "p"+2; }
let o={};
o[PROPERTY_NAME]=1
=>1
o[computePropertyName()]=2;
=>2

而使用ES6称为计算属性的特性可以更简单地创建类似对象,这个特性可以让你直接把前面代码中的方括号放在对象字面量中:

o[PROPERTY_NAME]=1
o[computePropertyName()]=2;
const PROPERTY_NAME="p1"
function computePropertyName(){
    return "p"+2;
}
let p={
    [PROPERTY_NAME]:1,
    [computePropertyName()]:2
};
p.p1+p.p2
=>3

有了这个语法,就可以在方括号中加入任意JavaScript表达式。对这个表达式求值得到的结果(必要时转换为字符串)会用作属性的名字。

一个可能需要计算属性的场景是,有一个JavaScript代码库,需要给这个库传入一个包含一组特定属性的对象,而这组属性的名字在该库中是以常量形式定义的。如果通过代码来创建要传给该库的这个对象,可以硬代码它的属性名,但是这样有肯把属性名写错,同时也存在因为库版本升级而修改了属性名导致的错配问题。此时,使用库自身定义的属性名常量,通过计算属性语法来创建这个对象会让你的代码更可靠。

6.10.3 符号作为属性名

ES6及之后,属性名可以是字符串或符号。如果把符号赋值给一个变量或常量,那么可以使用计算属性语法将该符号作为属性名:

const extension=Symbol("my extension symbol");
let o={
    [extension]:{/* 这个对象中存储扩展数据 */}
};
o[extension].x=0
=>0                    //这个属性不会与o的其他属性冲突

符号是不透明值。除了用作属性名之外,不能用它们做任何事情。不过,每个符号都与其他符号不同,着意味着符号非常适合用于创建唯一属性名。创建新符号需要调用Symbol()工厂函数(符号是原始值,不是对象,因此Symbol()不是构造函数,不能使用new调用)。Symbol()返回的值不等于任何其他符号或其他值。可以给Symbol()传一个字符串,在把符号转换为字符串时会用到这个字符串。但这个字符串的作用仅限于辅助调试,使用相同字符串参数创建两个符号依旧是不同的符号。

使用符号不是为了安全,而是为了JavaScript的扩展机制。如果你从不受控的第三方代码得到一个对象,然后需要为该对象添加一些自己的属性,但又不希望你的属性与该对象的原有属性冲突,那就可以放心使用符号作为属性名。而且,这样一来,你也不用担心第三方代码会意外修改你以符号命名的属性。(当然,第三方代码可以使用Object.getOwnPropertySymbol找到你使用的符号,然后修改或删除你的属性。着也是符号不是一种安全机制的原因)。

6.10.4 扩展操作符

在ES2018及之后,可以在对象字面量中使用“扩展操作符”…把已有对象的属性复制到新对象中:

let position={x:0,y:0}
let dimensions={width:100,height:75}
let rect={...position,...dimensions}
rect.x+rect.y+rect.width+rect.height
=>175

这个…语法常被称为扩展操作符,但却不是真正意义上的JavaScript操作符。实际上,它是仅在对象字面量中有效的一种特殊语法(在其他JavaScript上下文中,三个点有其他用途。只有在对象字面量中,三个点才会产生这种把一个对象的属性复制到另一个对象中的插值行为)。

如果扩展对象有一个同名属性,这个属性的值由后面的对象决定:

let o={x:1}
let p={x:0,...o}
p.x
=>1                     //对象o的值覆盖了初始值
let q={...o,x:2}
q.x
=>2                     //值2覆盖了前面对象o的值

另外要注意,扩展操作符只扩展对象的自有属性,不扩展任何继承属性。

let o=Object.create({x:1})
let p={...o}
p.x
=>undefined

最后,还有一点需要注意,虽然扩展操作符在你的代码中只是三个小圆点,但它可能给JavaScript解释器带来巨大的工作量。如果对象有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()是方法,而不是像side一样的数据属性。

在使用这种简写语法来写方法时,属性名可以是对象字面量允许的任何形式。除了像上面的area一样的常规JavaScript标识符之外,也可以使用字符串字面量和计算的属性名,包括符号属性名:

const METHOD_NAME="m"
const symbol=Symbol();
let weirdMethods={
    "method With Spaces"(x){ return x+1; },
    [METHOD_NAME](x){ return x+2; },
    [symbol](x){ return x+3; }
};
weirdMethods["method With Spaces"](1)
=>2
weirdMethods["method With Spaces"](1)
=>2
weirdMethods[symbol](1)
=>4

使用符号作为方法名并没有那么稀罕。为了让对象可迭代,必须以符号名Symbol.iterator为它定义一个方法。

6.10.6 属性的获取方法与设置方法

到目前为止,本章讨论的所有对象属性都是数据属性,即有一个名字和一个普通的值。除了数据属性之外,JavaScript还支持为对象定义访问器属性,这种属性不是一个值,而是一个或两个访问器方法:一个获取方法(getter)和一个设置方法(setter)。

当程序查询一个访问器属性的值时,JavaScript会调用获取方法(不传参数)。这个方法的返回值就是属性访问表达式的值当程序设置一个访问器属性的值时,JavaScript会调用设置方法,传入赋值语句右边的值。从某种意义上来说,这个方法负责“设置”属性的值。设置方法的返回值会被忽略。

如果一个属性既有方法也有设置方法,则该属性是一个可读写属性。如果只有一个获取方法,那它就是只读属性。如果只有一个设置方法,那它就是只写属性(这种属性通过数据属性是无法实现的),读取这种属性会得到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和y是常规的可读写数据属性
    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 radio=newvalue/oldvalue;
        this.x*=radio;
        this.y*=radio;
    },
    
    //theta是一个只定义了获取方法的只读访问器属性
    get theta(){
       return Math.atan2(this.y,this.x);
    }
}
p.r
=>1.4142135623730951
p.theta
=>0.7853981633974483

注意这个示例的获取和设置方法中使用了关键字this。JavaScript会将这些函数作为定义它们的对象的方法来调用。着意味着在这些函数体内,this引用的是表示坐标点的对象p。因此访问器属性r的获取方法可以通过this.x和this.y来引用坐标点的x和y属性。

与数据属性一样,访问器属性也是可以继承的。因此,可以把上面定义的对象p作为其他点的原型。可以给新对象定义自己的x和y属性,而它们将继承r和theta属性:

let q=Object.create(p);
q.x=3;q.y=4;
q.r
=>5                            //可以使用继承的访问器属性
q.theta
=>0.9272952180016122           //Math.atan(4,3)

以上代码使用访问器属性定义了一个API,提供了一个数据集的两种表示(笛卡尔坐标和极坐标)。使用访问器属性的其他场景还有写入属性时进行合理性检查,以及每次读取属性时返回不同的值:

const serialnum={
    //这个数据属性保存下一个序号
    //属性名中的_提示它仅在内部使用
    _n:0,
    
    //返回当前值并递增
    get next(){
         return this._n++;
    },
    
    //把新值设置为n,但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
=>10                           //设置起始序号
serialnum.next
=>10                        
serialnum.next
11
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值