JavaScript 是一门面向对象(Object-Oriented, OO)的语言,但是它的面向对象机制与其它的 OO 语言(如 C++、Java 等)有很大的区别,其中最大的不同点就是 JavaScript 不存在类的概念。在其它 OO 语言中,类是实现对象的基础,每一个对象都是通过类来创建的,可以说,如果没有类就没有对象。然而 JavaScript 底层就没有类的概念,那么它的面向对象机制是如何实现的呢?
原型模式是实现面向对象机制的一种方法,完全不需要使用到类的概念,每一个对象都是由另外一个对象作为其原型,并通过委托等方式来实现面向对象的那些核心概念。JavaScript 就是这样通过原型来实现面向对象机制的。
原型模式主要有以下四点原则:
首先看看如何创建一个对象
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
var person = new Person('Tom');
new Person()
与其它面向对象的语言不同,并不是创建类的实例,毕竟 JavaScript 不存在类的概念。尽管 Person
是作为一个函数来定义的,但是,通过 new
关键字来进行调用,就不是属于普通函数调用的范畴了,而是属于构造函数调用。通过构造函数调用,使得 Person
函数在 JavaScript 内部进行的操作有所不同,大致过程如下:
Person.prototype
对象。其操作大致可看做是对 Person.prototype
对象的克隆。Person
函数,并让其内部 this
指向第一步创建的新对象。这个新创建的对象的原型就是 Person.prototype
,而 Person.prototype
的原型则是 JavaScript 所规定的 Object.prototype
,因此任意对象的根原型都是 Object.prototype
由此我们能够得到一条原型链:person -> Person.prototype -> Object.prototype
。
当需要获取的属性或者方法不存在时,对象会将其委托给它的原型对象进行处理。因此,每一个对象都保存着它的原型对象。
在一些浏览器中,比如 Chrome 和 Firefox 能够将对象的原型通过 __proto__
给暴露出来,因此我们就能够通过对象的 __proto__
属性得知其原型对象。
console.log(person.__proto__);
// 输出 Person.prototype 对象
console.log(person.__proto__.__proto__);
// 输出空对象
console.log(person.__proto__.__proto__.__proto__);
// 输出 null
这样就很好的展示了上面的原型链。而原型链的作用是,当对象无法响应某个请求的时候,他就会顺着原型链把请求传递下去,直到遇到一个可以处理该请求的对象为止。 下面看一个简单的例子:
function A() {}
A.prototype.name = 'A';
var a = new A();
console.log(a.name);
// 输出:'A'
a.name = 'a'
console.log(a.name);
// 输出:'a'
可以看到,最开始在创建的 a
对象中并没有给它赋予 name
属性,但其仍然能够给出结果。其过程是:a
对象中不存在 name
属性,于是顺着 __proto__
属性将请求委托给 a
的原型 A.prototype
。A.prototype
对象中存在 name
属性,于是就将其结果返回给 a
对象,这时候就可以得出 a
对象的 name
属性值了。
而在将 name 属性赋值给 a
对象之后, a
对象中就存在 name
属性,因此不用再将请求委托给它的原型去查找,而是可以直接返回结果。
继承是面向对象编程中极为重要的一个特点,通过继承能够将不同类型对象中公有的属性和方法集中处理,避免了代码冗余的情况。在 JavaScript 中,继承是通过原型链来实现的。
而实现基于原型链的继承有多种方式,下面分别说明一下各种方式的特点与优缺点。
普通原型链继承是 JavaScript 实现继承的最基本方式,代码如下:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
};
function SubType() {
this.subproperty = false;
}
SubType.prototype = new SuperType();
Subtype.prototype.getSubvalue = function () {
return this.subproperty;
}
var instance = new SubType();
通过 SubType.prototype = new SuperType()
这一语句,将 SubType.prototype
对象设为了 SuperType
的实例,即 SubType.prototype
的原型为 SuperType.prototype
对象。而之后创建的 instance
对象为 SubType
的实例,即 instance
的原型为 SubType.prototype
。由此可得一条原型链:instance -> SubType.prototype -> SuperType.prototype
,因而实现了基于原型链的继承。
但这种继承方式存在两个问题:
通常不会单独使用此类继承方式。
通过调用构造函数的 apply()
或 call()
方法可在其他的对象上借用构造函数的操作,因此可在子类上借用父类的构造函数完成继承。
function SuperType(name) {
this.name = name;
this.colors = ['red'];
this.getColors = function () {
return this.colors;
}
}
function SubType(name) {
SuperType.call(this, name);
this.num = 1;
}
如此便可通过将父类构造函数的所有操作引入到子类的实例中进行,从而避免在引用类型的元素中造成相互影响。
但是,这样的继承方式依然存在问题,就是所有的方法都必须在构造函数中定义,否则无法实现方法的继承。这样的方式与函数复用的思想相违背,因此不常使用。
组合继承是将原型链继承与构造函数继承两种方式组合到一块,将两者优势互补。
function SuperType(name) {
this.name = name;
this.colors = ['red'];
}
SuperType.prototype.getColors = function () {
return this.colors;
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.getAge = function () {
return this.age;
};
通过原型链实现对原型属性和方法的继承,通过构造函数实现对实例属性的继承。如此便可避免在单一继承方式上出现的问题,是使用最广泛的继承方式。
ECMAScript 5 中,设立了 Object.create()
方法,通过这种方式不必预先定义构造函数就能够实现原型式继承。
var tom = {
name: 'Tom',
gender: 'male'
};
var jack = Object.create(tom);
jack.name = jack;
这样就在不创建构造函数的情况下,从一个对象创建了另一个类似的对象。其中,Object.create()
可以通过如下的形式来模拟:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
此函数的作用是:通过将原对象设为新对象的原型,随后从此原型中创建一个新的实例对象,即为继承后的新对象。新对象中不含有任何的自有属性,对新对象的属性和方法访问全部都将委托给原对象,因此实现了继承。与原型链的继承方式一样,对引用类型的原型属性的修改将会影响到其所有实例。
function SuperType(name) {
this.name = name;
this.colors = ['red'];
}
SuperType.prototype.getColors = function () {
return this.colors;
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
function inherit(subType, superType) {
subType.prototype = Object.create(superType.prototype);
subType.constructor = subType;
}
inhert(SubType, SuperType);
SubType.prototype.getAge = function () {
return this.age;
};
这种继承方式结合原型继承与借用构造函数继承的方式,与普通组合继承方式相比,避免了在 SubType.prototype
中创建不必要的实例属性,但仍不会对原型链造成改变。因此,普遍认为寄生组合继承是引用类型最理想的继承方式。
本文将时常更新。。。
最后更新日期 2016 年 12 月 19 日。