面向对象的程序设计,核心在于“对象”。在ECMAScript里面,可以把对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。
在加下来的篇幅里面,跟大家一起聊一聊如何创建对象,以及他们的优缺点,也会涉及到一些应用场景。
工厂模式
考虑到在ES6之前无法创建类,发明了一种函数,用函数来封装以特定接口创建对象的细节。
function createClerk(name, turnover, avatar) {
const o = new Object();
o.name = name;
o.turnover = turnover;
o.avatar = avatar;
o.moveFruits = function(fruits, onePlace, anotherPlace) {
// move the fruits from one place to another place
}
return o;
}
const a = createClerk('peter', 20, 'https://xxx');
const b = createClerk('tom', 30, 'https://yyy');
通过在函数createClerk()能够根据接收的参数来构建一个包含所有必要信息的Clerk对象。解决了多个相似对象的问题,但是没有解决对象识别的问题(即怎样知道一个对象的类型),对应上面的例子,即为:
// 在chrome打开Console调试
> a.constructor
// f Object() { [native code] }
> b.constructor
// f Object() { [native code] }
> const c = [1, 2]
> c.constructor
// f Array() { [native code] }
可以看出c是一个数组类型的对象,但是基于createClerk()创建的a、b无法被识别对象类型。
构造函数模式
ECMAScript中的构造函数可以用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如:
function Clerk(name, turnover, avatar) {
this.name = name;
this.turnover = turnover;
this.avatar = avatar;
this.moveFruits = function (fruits, onePlace, anotherPlace) {
// move the fruits from one place to another place
}
}
const a = new Clerk('peter', 20, 'https://xxx');
const b = new Clerk('tom', 30, 'https://yyy');
通过与 工厂模式 创建对象比较,有以下几点的不同:
- 没有显式的创建对象
- 直接将属性和方法赋给了
this对象 - 没有
return语句
同时,要创建Clerk的新实例,必须使用new操作符。这种方式调用构造函数,会经理4个步骤:
创建一个新对象 将构造函数的作用域赋给新对象(因此
this就指向了这个新对象) 执行构造函数中的代码 返回新对象
> a.constructor
// f Clerk() { [native code] }
> a instanceof Clerk
// true
> a instanceof Object
// true
构造函数虽然好用,但也并非没有缺点。主要问题就是,每个方法都要在每个实例上面重新创建一遍。
> a.moveFruits == b.moveFruits
// false
很容易想到,把moveFruits放在全局即可解决这个问题。
function Clerk(name, turnover, avatar) {
this.name = name;
this.turnover = turnover;
this.avatar = avatar;
this.moveFruits = moveFruits;
}
function moveFruits = function (fruits, onePlace, anotherPlace) {
// move the fruits from one place to another place
}
const a = new Clerk('peter', 20, 'https://xxx');
const b = new Clerk('tom', 30, 'https://yyy');
但是又带来新的问题:
- 在全局作用域中定义的函数实际上只能被某个对象调用,有点名不副实;
- 对象定义需要很多方法,就要创建多个全局函数,我们自定义的引用类型就丝毫没有封装性可言。
原型模式
我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,这个对象可以被通过函数创建的实例对象共享属性和方法。
function Clerk(){}
CLerk.prototype.name = 'peter';
Clerk.prototype.turnover = 20;
Clerk.prototype.avatar = 'https://xxx';
Clerk.prototype.moveFruits = function (fruits, onePlace, anotherPlace) {
// move the fruits from one place to another place
}
也可以简写为:
function Clerk() {}
Clerk.prototype = {
constructor: Clerk,
name: 'peter',
turnover: 20,
avatar: 'https://xxx',
moveFruits: function (fruits, onePlace, anotherPlace) {
// move the fruits from one place to another place
}
}
这样的模式解决了构造函数模式的函数实例重复创建的问题,但是也带来了一些新的问题:
- 省略了构造函数初始化传参这一环节,结果所有实例默认情况下都将取得相同的属性值;
- 最大的问题:实例共享属性
function Clerk() {}
Clerk.prototype = {
constructor: Clerk,
name: 'peter',
turnover: 20,
avatar: 'https://xxx',
interests: ['swim', 'mountaineering']
moveFruits: function (fruits, onePlace, anotherPlace) {
// move the fruits from one place to another place
}
}
const a = new Clerk();
const b = new Clerk();
a.interests.push('electron');
console.log(b.interests);
// ['swim', 'mountaineering', 'electron']
组合使用构造函数模式和原型模式
创建自定义对象最常见的方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省内存。也支持构造函数传参。
function Clerk(name, turnover, avatar) {
this.name = name;
this.turnover = turnover;
this.avatar = avatar;
this.interests = ['swim', 'mountaineering'];
}
Clerk.prototype = {
constructor: Clerk,
moveFruits: function (fruits, onePlace, anotherPlace) {
// move the fruits from one place to another place
}
}
const a = new Clerk('peter', 20, 'https://xxx');
const b = new Clerk('tom', 30, 'https://yyy');
a.interests.push('electron');
console.log(b.interests)
// ['swim', 'mountaineering']
console.log(a.moveFruits === b.moveFruits)
// false
动态原型模式
熟悉其它语言OO编程的人,看到这种模式,可能会比较困惑。动态原型就是为了解决这样的问题。把所有的信息封装在构造函数中,通过构造函数初始化原型。原理就是,通过检查某个方法是否应该有效,来决定是否需要初始化原型。
function Clerk(name, turnover, avatar) {
this.name = name;
this.turnover = turnover;
this.avatar = avatar;
this.interests = ['swim', 'mountaineering'];
if (typeof Clerk.prototype.moveFruits !== 'function') {
Clerk.prototype.moveFruits = function (fruits, onePlace, anotherPlace) {
// move the fruits from one place to another place
}
}
}
需要注意的是:使用动态原型模式时,不能使用对象字面量重写原型。Clerk.prototype = { ... },这样在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的联系。
寄生构造函数模式
这种模式的基本思维是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
function Clerk(name, turnover, avatar) {
const o = new Object();
o.name = name;
o.turnover = turnover;
o.avatar = avatar;
o.moveFruits = function (fruits, onePlace, anotherPlace) {
// move the fruits from one place to another place
console.log(this)
}
return o;
}
const a = new Clerk('peter', 20, 'https://xxx');
除了使用new操作符,跟工厂模式其实是一模一样的。构造函数在默认不返回值的情况下,默认返回新对象实例。
这种模式在特殊情况下用来为对象创建构造函数。例如:我们要创建一个具有特殊方法的Array。但是不能直接修改Array构造函数,因此可以使用这个模式。
function SpecialArray() {
const values = new Array();
values.push.apply(values, arguments);
values.toPipedString = function() {
return this.join('|');
};
return values;
}
const colors = new SpecialArray(['red', 'blue', 'green']);
console.log(colors.toPipedString());
// 'red|blue|green'
注意:通过寄生构造函数模式创建的实例和函数的原型没有关系,不能依赖instanceof来确定对象类型。
稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。与寄生构造函数模式类似,但是有两点区别:
- 不使用
new来调用构造函数 - 新创建对象的实例不引用
this
function Clerk(name, turnover, avatar) {
const o = new Object();
o.getName = function() {
return name;
}
o.getAvatar = function() {
return avatar;
}
o.getTurnover = function() {
return turnover;
}
return o;
}
可以看出,除了调用getName()方法之外,没有别的方式可以访问其数据成员。稳妥构造函数模式提供这种安全性,使得它非常适合在某些安全执行环境。
同时,与寄生构造函数模式类似,instanceof对这种对象也没有意义。