1

今天整理面试题的时候看见一道题叫讲一下继承,虽然继承以前也看过书,也在用,但是居然无法总结性、系统地回答这个问题,于是赶紧把《JavaScript设计模式》扒拉出来看看。

为什么需要继承

在设计类的时候,能够减少重复性代码,并且能尽量弱化对象间的耦合。可以在现有类的基础上进行设计并充分利用已经具备的各种方法,而对设计的修改也更为轻松。
继承一共有三种方式:类式继承、原型式继承、掺元类

类式继承

JavaScript可以被装扮成使用类式继承的语言。通过用函数来声明类、用关键字 new 来创建实例,JavaScript中的对象也能模仿Java或C++中的对象。
首先创造一个简单的类声明,及对该类的实例化:

// 创建 Person 类
function Person (name) {
    this.name = name;
}

Person.prototype.getName = function () {
    return this.name;
};

// 实例化 Person 类
var reader = new Person ('John Smith');
reader.getName();   // John Smith

创建继承 Person 的类 Author :

// 创建继承 Person 的类 Author
function Author (name,books) {
    Person.call(this,name);
    this.book = books;
}

// 派生子类
Author.prototype = new Person();
Author.prototype.constructor = Author;
Author.prootype.getBooks = function () {
    return this.book;
};

在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性是一个指向 prototype 属性所在函数的指针。
为了简化类的声明。可以把派生子类的整个过程包装在一个名为 extend 的函数中,作用是基于一个给定的类结构创建一个新的类。

// extend 函数
function extend(subClass,superClass) {
    var F = function() {};
    F.prototype = superClass.prototype;
    subClass.prototype = new F();
    subClass.prototype.constructor = subClass;
    
    // 确保超类的 constructor 属性被设置正确
    subClass.superclass = superClass.prototype;
    if(superClass.prototype.constructor == Object.prototype.constructor) {
        superClass.prototype.constructor = superClass;
    }
}

那么 Author 的继承可以改写为:

function Author (name,books) {
    Author.superClass.contructor.call(this,name);
    this.books = books;
}
extend(Author,Person);   // 调用 extend 函数

Author.prootype.getBooks = function () {
    return this.book;
};

原型式继承

原型式继承和类式继承截然不同。在学习原型式继承时,最好忘掉自己关于类和实例的一切知识,只从对象的角度来考虑。
在使用原型式继承时,不需要用类来定义对象的结构,只需直接创建一个对象即可。这个对象随后可以被新的对象重用,这得益于原型链查找的工作机制,该对象被称为原型对象。
下面使用原型式继承来重新设计 Person 和 Author

// Person 原型对象
var Person = {
    name: 'default name',
    getName: function() {
        return this.name;
    }
};

Person 现在是一个对象字面量,其中定义了所有类 Person 对象都要具备的属性和方法,并为他们提供了默认值。clone 函数可以用来创建新的类 Person 对象,该函数会创建一个空对象,而该对象的原型对象被设置为 Person。

// Author 原型对象
var Author = clone(Person);
Author.books = [];   // Default value
Author.getBooks = function () {
    return this.books;
};

一个克隆并非原型对象的一份完全独立的副本,它只是一个以那个对象为原型对象的空对象。对继承而来的成员有读和写的不对等性:

var authorClone = clone(Author);
alert(authorClone.name);   // default name (连接到 Person.name)
authorClone.name = 'new name';
alert(authorClone.name);   // new name (连接到 authorClone.name)

authorClone.books.push('new book');   // 在这里,想authorClone.books数组添加
                                      // 新元素实际上是把这个元素添加到
                                      // Author.books数组中。
authorClone.books = [];
authorClone.books.push('new book');                                

这也就说明了为什么必须为通过引用传递的数据类型的属性创建新的副本。在以上例子中,向authorClone.books数组添加新元素实际上是把这个元素添加到Author.books数组中。这可不是什么好事,因为对那个值的修改不仅会影响到 Author,而且会影响到所有机场了Author但还未改写那个属性的默认值的对象。在改变所有那些数组和对象的成员之前,必须先为其创建新的副本。

类似继承和原型式继承的对比

包括JavaScript程序员在内的整个程序员群体对类式继承都比较熟悉。
原型式继承更能节约内存。在原型链中查找成员的方式使得所有克隆出来的对象都共享每个属性和唯一一份实例,只有在直接设置了某个克隆出来的对象的属性和方法时,情况才会有所变化。而类似继承方式中创建的每一个对象在内存中都有自己的一套属性的副本。


puhongru
581 声望58 粉丝

立志成为一名合格的前端开发工程师