重新温习了下JavaScript高级程序设计第六章,有些知识不常用已经忘记,这次重新拾起。
所谓的面向对象,其中最重要的一个特征就是类的概念,通过类可以创建多个具有相同属性或方法的对象。但是JavaScript在ES6之前是没有类的概念的(本次不讨论ES6)。
但是没有类的概念不意味着我们不需要使用到类,所以在JavaScript中,我们通过“曲线救国”的方法来实现类的概念。
工厂模式
工厂模式是最简单的一种形式。通过函数来将相同的属性和方法抽象出来,通过return
的形式来返回一个对象。
function createObj (name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
return o;
}
let person1 = createObj('jack', 12, 'web');
let person2 = createObj('tom', 13, 'web');
通过定义createObj
一个方法,传入公共参数,在函数中new
一个对象并返回。
这种方式虽然解决了创建多个相似对象的问题,但是有一个明显的缺点就是,无法识别对象,也就是无法知道该对象的类型,在说的直白一点,就是没办法拿到具体的constructor
。
优点:
- 简单
- 解决了创建多个相似对象的问题
缺点:
- 无法得知对象的类型(constructor返回的都是一个空对象)
构造函数模式
这里的构造函数模式就很好的解决前面工厂模式所产生的问题。
JavaScript里有一些内置的原生构造函数,如Array
、Date
等。我们可以自定义一个构造函数,从而达到利用构造函数实现创建对象的目的。
我们通过构造函数将前面工厂模式的代码改下如下:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.jon = job;
this.sayName = () => {
console.log(this.name);
}
}
let person1 = new Person('jack', 12, 'web');
let person2 = new Person('tom', 13, 'doctor');
这里我们发现和前面工厂模式有明显的区别:
- 没有显式的创建对象;
- 通过
this
给当前调用对象赋值; - 没有
return
语句; - 通过
new
关键字实例化对象。
这里我们要明确一点,构造函数本身也是函数。通过这种方式创建对象实例,会经过以下四个步骤:
- 创建一个对象;
- 将构造函数的作用域赋给新对象;
- 执行构造函数中的代码;
- 返回新对象。
这里person1
和person2
都是Person
构造函数的实例。
constructor
在构造函数的实例中,存储着一个constructor
属性,指向构造函数,在这里就是指向了Person
。
person1.constructor === Person; // true
person2.constructor === Person; // true
constructor
最初是来用来标识对象类型的;在这里我们发现,前面工厂函数无法得知对象类型的问题也就解决了。
instanceof
instanceof
可以用来检测对象类型,比constructor
更可靠一些。
由于我们这些实例也是Object
的实例(所有对象均集成自Object,不许抬杠哈),所以下面情况是合理的:
person1 instanceof Object; // true
person1 instanceof Person; // true
person2 instanceof Object; // true
person2 instanceof Person; // true
利用自定义构造函数意味着我们可以将它的实例标识为一种特定的类型,这正是构造函数胜过工厂模式的地方。
这里,构造函数虽然解决了工厂模式的缺点,但是也引入了另外一种问题。
前面我们的Person
构造函数里面定义了一个sayName
方法,而该方法在每次实例化属性的时候都需要创建一遍,因为这里定义的方法都是Function
实例,等同于下面的写法:
sayName = new Function('console.log(this.name)')
所以很明显,这种方法并不完美,因为以这种方法创建的函数,会导致不同的作用域链和标志符解析;另外就是同样的方法我们不希望创建两个不同的东西。
当然,我们可以通过引用指向的关系,来解决这个问题:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
很简单其实,我们在全局作用域下定义了一个sayName
方法,并在构造函数中指向该全局方法。
这样就解决了前面提到的问题,但是同意,并不完美,因为在全局下定义一些函数,在需要定义很多全局方法的情况下并不推荐,并且该全局函数只能被某个对象调用,这样就没有封装性了。
优点:
- 简单;
- 可以标识实例对象的类型,解决了工厂模式的缺点。
缺点:
- 每个方法都需要在创建对象的时候重新创建一遍(虽然可以解决,但是并不完美)。
原型模式
针对前面构造函数模式的问题,我们可以使用原型模式来解决(通过原型链概念)。
因为创建的每一个函数都有property
(原型)属性,该属性是一个指针,指向一个对象。
function Person() {}
Person.prototype.name = 'jack';
Person.prototype.age = 12;
Person.prototype.job = 'doctor';
Person.prototype.sayName = function() {
console.log(this.name);
}
let person1 = new Person();
let person2 = new Person();
person1.sayName === person2.sayName; // true
这里我们很明显的发现,这里通过远行模式解决了构造函数的缺点(在构造函数中,每个方法都要在实例化的时候重新创建一遍)。
但是,原型模式也有缺点:如果我们仔观察上面的代码,就会发现,我们在prototype中把一些属性写死了,很明显,违背了封装性。
所以,这里,我们又要用到一个新的模式:组合模式
组合模式
创建自定义类型的最常见的方法就是组合使用构造函数和原型模式。构造函数用于定义实例属性,而原型模式用于定义方法和共享的属性。最大限度的节省内存,集两种模式之所长。
直接看代码:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
let person1 = new Person('jack', 12, 'web');
let person2 = new Person('tom', 123, 'doctor');
person1.name; // 'jack'
person2.name; // 'tom'
person1.sayName === person2.sayName; // true
所以,我们发现组合模式相对来说才是最好的方法。结合了构造函数和原型模式的优点,既可以定义实例属性,又可以定义公共方法,而又不必在每次实例化的时候重新创建公共方法。