JavaScript 的原型模式及其继承方式

前端2016-12-190 篇评论 JavaScript

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 内部进行的操作有所不同,大致过程如下:

  1. 首先创建一个空对象,并使它继承 Person.prototype 对象。其操作大致可看做是对 Person.prototype 对象的克隆。
  2. 通过普通方式调用 Person 函数,并让其内部 this 指向第一步创建的新对象。
  3. 构造函数调用始终返回一个对象类型的值,如果未指定返回值或者返回值类型不为对象类型,则返回第一步创建的对象。但如果指定返回值是一个对象类型的话则直接将其返回。

这个新创建的对象的原型就是 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.prototypeA.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,因而实现了基于原型链的继承。

但这种继承方式存在两个问题:

  1. 针对引用类型的原型属性,对其原型属性的修改会影响到其所有实例。
  2. 子类无法向父类的构造函数传递参数,而不对其它对象实例造成影响。

通常不会单独使用此类继承方式。

借用构造函数

通过调用构造函数的 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 日。

评论区

发表评论
用户名
(必填)
电子邮箱
(必填)
个人网站
(选填)
评论内容
Copyright © 2017 dremy.cn
皖ICP备16015002号