第4章 对象

本章主要内容

● JavaScript对象

● 对象的特性

● ES6中对象的新特性

对象是整个JavaScript 中一个非常重要的概念,是JavaScript 中基本的数据类型。JavaScript 是基于对象的,一个对象就是一系列相关属性的集合,而属性包含对应的名字和对应的值。一个属性的值可以是变量,也可以是函数,当为函数时也称该属性为方法。本章将讲述对象的基本概念、创建方法、属性等。

4.1 JavaScript对象

4.1.1 名词解释

JavaScript是基于对象的,那么什么是基于对象呢?

1. 基于对象

首先,一切皆对象,要以对象的概念来编程。其次,JavaScript 中有很多内置的对象,如window,所以说JavaScript是基于对象的。

2. 对象

对象就是人们要研究的任何事物,不仅能表示具体事物,还能表示抽象的规则、计划或事件。例如,人可以看作一个对象,不同的人就是不同的对象。每个人都有自己所属的属性,有肤色、年龄、性别等。当然也有自己的方法,如说话、吃饭、学习等。同样地,JavaScript 对象也有属性来定义其特征。所以简单地说,对象就是属性的无序集合,每个属性可以存一个值(原始值,对象,函数)。对象的属性指的就是用数值描述对象的状态,对象的方法指的就是对象具有可实施的动作。

一个水杯有自己的分类,如保温杯还是塑料杯等,在JavaScript中,对象也有自己的类。

3. 类

具有相同或相似的性质的对象的抽象就是类。对象的抽象就是类,类的具体化(实例化)叫作对象。在JavaScript中是通过构造函数来实现类的。

4. 面向过程

面向过程是一种以过程为中心的编程思想。面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步地实现,使用时一个一个依次调用即可。它是一种思考问题的基础方法。

5. 面向对象

面向对象是软件开发中的一种,是思考问题相对高级的方法。它把构成问题的事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。

这里给大家举个示例帮助理解记忆:

汽车发动,汽车到站。汽车发动是一个事件,汽车到站是另一个事件,在面向过程中我们关心的是事件,而不是汽车本身,针对上述两个事件,形成两个函数,之后依次调用。

对面向对象来说,我们关心的是汽车这类对象,两个事件只是这类对象所具有的行为,且对于这两个行为的顺序没有强制要求。

面向过程的思维方式是分析综合,面向对象的思维方式是构造。

4.1.2 创建对象的方法

JavaScript 拥有很多内置的对象。除此以外,也可以创建自己的对象。创建方法和其他语言大同小异。

1. 通过Object方法(JavaScript顶层构造函数)创建

语法如下:

示例:

说明:通过Object方法创建对象,实则指的是通过JavaScript的原生对象的构造方法,实例化一个新对象。

2. 通过json方式创建

此方法多用于数据的存储,语法如下:

示例1:

说明:这也是非常简单的一种方法,一般情况下不推荐使用,因为这种方法可读性不够强。请看示例2。

示例2:

说明:相比较示例 1,示例 2 中嵌套的方法可读性很强,对象 lisi 的所有属性和方法都包含在其自身内,一目了然。

以上两种方法适用于只存在一个实例的对象。当需要创建多个对象实例时,就需要通过构造函数的方法来创建。

3. 通过构造函数创建

通过构造函数来创建对象类型时,最好首字母是大写的。

语法如下:

JavaScript中没有类这一概念,需要用函数的方式来模拟类,这个函数就是构造函数。

示例:

说明:通过使用this 将传入函数的值赋值给对象的属性,this 代表谁实例化它,它就指谁。现在就可以创建多个Person对象实例了。

4.1.3 属性与方法

1. 添加属性与方法

当属性的值为函数时就是对象的方法,其他都为属性。

格式:

为对象添加属性除了上述方法外,还可以通过prototype(原型)属性来为对象添加属性。

JavaScript 中为每个函数都分配了prototype 属性,该属性是个指针,指向一个对象,作为所有实例的基类引用。

示例:

2. 方法

格式:

示例同创建对象。

3. 访问属性

格式:

4. 访问方法

格式:

4.1.4 销毁对象

销毁对象的格式如下:

JavaScript的垃圾回收机制在对象不被引用时释放内存。

删除对象上的属性:可以用delete 来删除对象上一个不是通过集成而来的属性,示例如下。

4.1.5 对象的遍历

对象的遍历使用for...in方法。

示例:

4.1.6 对象的存储方式

1)变量保存的仅仅是对象的引用地址。

2)对象保存在堆中,每创建一个对象,就开辟一块内存。

3)当JavaScript引擎检测到对象没有引用时,将把它当作垃圾,等待回收。

4)在某一时刻回收垃圾对象。

对象的存储方式如图4-1所示。

图4-1

4.1.7 instanceof

instanceof运算符用来检测某个对象是否是某个构造函数的实例。

示例:

任何对象对Object构造函数进行判断,结果都是true,因为Object()是JavaScript中的顶层构造函数。

4.2 对象的特性

4.2.1 对象的特性——封装

封装是将对象的所有组成部分组合起来,尽可能地隐藏对象的部分细节,使其受到保护,只提供有限的接口与外部发生联系。

下面介绍封装方法。

(1)工厂函数(不推荐使用)

示例:

(2)构造函数(每创建一个对象,会把相同的代码存储到内存中,会造成对内存的浪费)

示例:

(3)prototype方法(会把共享的方法或属性放到代码段中来存储,它不能共享对象)

示例1:

示例2:

(4)混合函数

这是一种最佳的方法,构造函数与prototype的结合,应根据实际情况考虑。

示例:

4.2.2 对象的特性——继承

继承是一个对象拥有另一个对象的属性与方法。所有的JavaScript 对象继承于至少一个对象,并且继承的属性可通过构造函数的prototype 对象找到。

对象的一个类可以从现有的类中派生出来,并且它拥有现有的类的方法与属性,这个过程就是继承。

父类(基类/原型)是被继承的对象,子类是继承的对象。

1. 继承优点

通过继承可以提高代码的重用性、逻辑性与可维护性。

2. 继承的方式

(1)通过原型来继承

示例:

说明:这种方式实现继承最为简单,只需让子类的prototype 属性赋值为被继承的一个实例化对象即可,之后就可以直接使用被继承类的方法和属性了。

(2)call方法

格式:

从本质上来说,call方法实际上就是要改变fun函数内的this指向。

示例:

(3)apply方法

用法基本与call方法相同,格式如下:

示例:

3. 继承的顺序

优先级:对象本身>构造函数>原型链。

示例:

4.2.3 this指针

本章很多内容中都提到了this,本节专门针对this为读者进行详细讲解。

大家肯定会困惑,为什么要在编程中用this?this到底代表什么呢?

JavaScript 是一门基于对象的语言,也就是说,一切皆对象,函数也是一个普通的对象。JS可以通过一定的设计模式来实现面向对象的编程,其中this指针就是实现面向对象的一个很重要的特性。

示例:

说明:上述示例是一个普通的函数,通过调用say函数,弹出全局变量name。

下面把上面的示例稍作修改:

说明:定义一个全局对象say并执行这个函数,函数内部使用this关键字,执行this对象时执行代码的对象是say,say 被定义在全局作用域中。JS 中所谓的全局对象,无非就是定义在window 这个对象下的一个属性而已。因此say 的所有者是window 对象。也就是说,在全局作用域下,可以通过直接使用name 去引用这个对象,也可以通过window.name去引用同一个对象。因此,this.name就可以翻译为window.name了。

两个示例最终运行结果一样,说明this.name引用的还是全局的name对象。

JavaScript 是一门动态语言,某一个函数中的this 最终代表谁取决于调用模式。接下来通过示例帮助读者更好地理解this指向问题。

1)在普通函数中,this总是指向window全局对象,代码如下:

2)当函数作为对象的方法使用时,this指向的是当前对象,代码如下:

3)在构造函数中,this指向的是该构造函数实例化出来的对象,代码如下:

4)在call和apply中,this指的是方法中传入的对象,如果apply中没有传入对象,则this指向window,代码如下:

4.2.4 对象的分类

1)内置对象:直接使用即可,不需要实例化。

内置顶层对象(global):Math();

2)本地对象:需要实例化后才能使用,如String();、Boolean();、Number();、Function();、Array();。

3)宿主对象:BOM和DOM,详细内容参考第6章。

4.3 ES6中对象的新特性

4.3.1 类的支持

前文已经提到JavaScript 中没有类,但是在ES6 中添加了对类的支持,引入了class 关键字(class在JavaScript中是保留字,在ES6新版本中,派上了用场)。

下面通过一个示例来展示类的用法:

4.3.2 变量的解构赋值

在ES6中,允许按照一定的模式,从数组和对象中提取值,对变量进行赋值,这就称为解构。下面通过一些示例来介绍解构赋值的应用。

1. 数组的解构赋值

示例:

说明:从数组中提取值时,会按照对应的位置对变量进行赋值。如果解构不成功,则变量的值就等于undefined。

2. 对象的解构赋值

解构赋值不仅可以用于数组,还可以用于对象。对象的解构赋值和数组有一个重要的不同:数组的元素是按照内容的顺序依次进行对应位置的赋值,而对象的属性是没有顺序的,变量必须与属性同名才能进行正确的赋值。

示例:

说明:上述示例中,a 是匹配的模式,c 才是变量。真正被赋值的是变量c,而不是模式a。

3. 字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

示例:

4. 函数参数的解构赋值

函数的参数也可以使用解构赋值。

示例:

说明:上述示例中,add 函数的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能接收到的参数就是x和y。

更多有关ES6中函数的相关内容可参考3.3节。

4.3.3 扩展运算符(spread)和rest参数

1. 扩展运算符

扩展语法:扩展运算符是3 个点(...),允许在需要多个参数(用于函数回调)或多个元素(用于数组文本)或多个变量(用于解构分配)的位置扩展表达式。

(1)数组中

扩展运算符的作用是将一个数组转为用逗号分隔的参数序列。

示例:

说明:从上述示例可以看出,扩展运算符可以用来替换数组中的concat函数,实现数组合并。

示例:

(2)对象中

ES6对对象也做了扩展,方法与数组类似。

示例:

说明:对对象进行扩展运算,实现的是对象的浅复制,并且是复制对象的可枚举的属性。

(3)函数中

如果一个函数的最后一个形参是以“…”为前缀的,则在函数被调用时,该形参会成为一个数组,数组中的元素都是传递给该函数的多出来的实参的值。

示例:

说明:在上述示例中,theArgs 会包含传递给函数的从第3 个实参开始到最后所有的实参(第1个实参映射到a,第2个实参映射到b)。

2.rest参数

ES6 中引入了rest 参数(…变量名)。通常,需要创建一个可变参数的函数,之前都是借助arguments对象。现在使用rest参数就可以创建可变参数的函数。

3.1.3节中有关传入参数个数不同,故进行不同操作的示例,这里就不做过多介绍了。

下面是个有关rest参数的示例:

似乎看上去使用rest 参数和arguments 对象的运行结果一样,但是它们之间还是有本质区别的。

rest参数和arguments对象的区别如下:

1)剩余参数只包含那些没有对应形参的实参。

2)arguments对象包含了传给函数的所有实参。

3)arguments 对象不是一个真实的数组,而剩余参数是真实的数组实例,即能够在其上直接使用所有的数组方法,如sort、map、forEach、pop。

4.3.4 属性的简洁表示

ES6允许在对象之中直接写变量。这时,属性名为变量名,属性值为变量的值。

示例:

属性的简洁表示用于函数的返回值非常方便,示例如下:

取值器和赋值器中的用法如下:

4.3.5 属性名表达式

ES6中,在使用字面量定义对象时,可以把表达式放到方括号中作为属性名。

示例:

4.3.6 方法的name属性

方法也是函数,所以方法也有name属性,示例如下: