要说 JavaScript 什么最难,对象首当其冲。
对象的含义
ECMA-262 对于对象的官方定义是:
无序属性的集合,其属性可以包括基本值、对象或者函数。
对象的属性是由键对值(key: value)形式定义的,当 value 值为函数时,则称为方法。通常我们将对象的特征称之为属性,对象的行为称之为方法。
比如将我这个人作为一个对象,我拥有很多属性,姓名、性别、年龄,还有很多方法,比如能吃能睡能打麻将。
我在刚开始学 JavaScript 的时候,一直弄不清什么是构造函数、原型对象和实例,还有他们之间的关系,在总结创建对象的几种方法之前,先聊一聊这三个东西。
构造函数、原型对象、实例
定义
构造函数
构造函数就是用 new 操作符调用的普通函数,构造函数与普通函数的 唯一区别 在于:构造函数用 new 调用来创建对象,例如 Object、Array、Function等,都是构造函数。
使用构造函数的 目的 就是:所有用同一个构造函数创建的实例对象都具有同样的属性和方法。
一般构造函数名的 首字母大写 用以区别,比如下面代码中的 Person。
|
|
实例对象
实例对象,通常简称为 实例,是通过 new 调用构造函数创建出的对象,例如上述代码中的 Echo 就是实例化出来的一个对象,实例拥有构造函数中的所有属性与方法。
原型对象
所有函数都有一个 prototype 属性,指向这个构造函数的原型对象,这个原型对象里的属性和方法是所有实例共享的。
三者的关系
函数的 prototype 属性,指向这个构造函数的原型对象。
原型对象的 constructor 属性,指向它的构造函数。
实例中的 [[prototype]] (除 IE 外的浏览器也支持 __proto__ 属性)指向原型对象。
|
|
在上述代码中,属性 name 和 age,方法 eat 是构造函数中定义的,实例 Echo 自有属性 sex,实例 Mike 自有属性 job,原型对象中共享属性 score 和方法 sayHi。后在实例 Echo 中添加同名自有属性 score,在调用属性时,先自有属性后共享属性。改变原型对象中的属性,所有实例的该属性同时改变。
检测属性
要判断对象是否具有某个属性,通常有两种方法。
in 操作符
in 操作符会检查对象自有属性和原型属性。
|
|
hasOwnProperty 方法
hasOwnProperty 方法只检查对象的自有属性。
|
|
创建对象的方法
字面量
创建对象最简单的方式就是字面量。
|
|
在使用字面量方式创建对象时,可以在定义对象时添加属性,也可以先创建一个空对象,后续再添加属性,属性名 可以是 变量 的形式,也可以是 字符串 形式。
|
|
优点: 创建方式简单,代码量少,可读性好。
缺点: 代码不具有复用性,比如说还有个人跟我一样的年纪跟我一样爱吃排骨,那我们又要创建一个新的对象,定义一样的属性,这样会造成代码的冗余。
通过字面量方式直接创建的实例对象,该实例的 __proto__ 属性指向 Object,即实例的原型对象为 Object 对象。
内置构造函数 Object
|
|
实际上,字面量方式创建对象也是通过 Object 类型创建的,只是表示方式不一样而已。
工厂模式
工厂模式通过创建一个函数定义属性和方法,所有通过这个函数创建的实例对象,自动拥有函数中所有的属性和方法。
|
|
优点: 可以方便地创建多个具有相同属性和方法的对象。
缺点: 通过工厂模式创建的函数与实例,原型对象均为 Object,这样我们就无法判断实例的实际类型。
自定义构造函数
|
|
与工厂模式的区别:
- 没有在函数内显性创建对象
- 通过 this 对属性和方法赋值
- 没有 return 语句
之所以构造函数方法会与工厂模式有这些区别,关键还在于 new 操作符。
new 操作符的作用:
- 创建一个新的对象
- this 指向这个对象
- 往对象添加属性和方法(执行代码)
- 返回对象
优点: 所有用同一个构造函数创建的实例对象都具有同样的属性和方法,解决了对象识别问题。
缺点: 实际上并没有消除代码冗余,构造函数中的属性和方法会在每个实例中重新创建一遍,不同实例之间的属性和方法相互独立,这样就存在内存浪费问题,而事实上没有必要创建多个完成同一任务的方法。
原型模式
单纯使用原型对象来创建实例对象也需要创建一个构造函数,当然这个构造函数可以是空的。通过原型对象添加属性和方法:
|
|
可以看到通过原型对象定义的方法和属性是共享的。除此之外,还有一种更简洁的字面量形式定义原型对象的属性和方法。
|
|
使用字面量形式必须注意: 指明该原型对象的构造函数,否则实例的 __proto__ 属性指向 Object。
优点: 对于不同实例的同一个属性和方法其实都是同一个内存地址,避免了内存浪费,提高运行效率。
缺点: 共享引用类型(对象)时,一个实例的改变会影响另一个实例,我们有的时候需要这种共享性,有的时候会恼怒这种共享性。
|
|
组合使用构造函数模式和原型模式
这种创建对象的方式是目前使用最广泛的,它结合了构造函数模式与原型模式的优点,利用构造函数定义实例属性,利用原型对象定义方法和共享属性。
|
|
其他方法
在《JavaScript 高级程序设计》中还提到了三种其他创建对象的方法:动态原型模式、寄生构造函数模式、稳妥构造函数模式。由于这三种方式并不常用,因此简单了解一下即可。
动态原型模式
通过检查检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
寄生构造函数模式
构造函数的内部与工厂模式相同,使用 new 调用构造函数。
稳妥构造函数模式
形式上与工厂模式相同。