《JavaScript设计模式》——2.3 传宗接代——继承

简介: “类的原型对象的作用就是为类的原型添加共有方法,但类不能直接访问这些属性和方法,必须通过原型prototype来访问。而我们实例化一个父类的时候,新创建的对象复制了父类的构造函数内的属性与方法并且将原型proto指向了父类的原型对象,这样就拥有了父类的原型对象上的属性与方法,并且这个新创建的对象可直接访问到父类原型对象上的属性与方法。

本节书摘来自异步社区《JavaScript设计模式》一书中的第2章,第2.3节,作者:张容铭著,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.3 传宗接代——继承

“小白,看继承呢?”小铭忙完自己的事情走过来。

“是呀,刚才学习类,发现每个类都有3个部分,第一部分是构造函数内的,这是供实例化对象复制用的,第二部分是构造函数外的,直接通过点语法添加的,这是供类使用的,实例化对象是访问不到的,第三部分是类的原型中的,实例化对象可以通过其原型链间接地访问到,也是为供所有实例化对象所共用的。然而在继承中所涉及的不仅仅是一个对象。”

“对呀,不过继承这种思想却很简单,如千年文明能够流传至今靠的就是传承,将这些有用的文化一年一年地流传下来,又如我们祖先一代一代地繁衍,才有了今天的我们。所以继承涉及的不仅仅是一个对象。如人类的传宗接代,父母会把自己的一些特点传给孩子,孩子具有了父母的一些特点,但又不完全一样,总会有自己的特点,所以父母与孩子又是不同的个体。”

“可是JavaScript并没有继承这一现有的机制,它又是如何实现的呢?”

2.3.1 子类的原型对象——类式继承
“对呀,也正因为JavaScript少了这些显性的限制才使得其具有了一定的灵活性,所以我们可以根据不同的需求实现多样式的继承。比如常见的类式继承。”

// 类式继承
// 声明父类
function SuperClass(){
  this.superValue = true;
}
// 为父类添加共有方法
SuperClass.prototype.getSuperValue = function(){
  return this.superValue;
};
// 声明子类
function SubClass(){
  this.subValue = false;
}
// 继承父类
SubClass.prototype = new SuperClass();
// 为子类添加共有方法
SubClass.prototype.getSubValue = function (){
  return this.subValue;
};

“很像,真的很像!”小白很惊讶。

“像什么?”小铭不解地问。

“刚才看过的封装呀,不同的是这里声明了2个类,而且第二个类的原型prototype被赋予了第一个类的实例。”小白解释道。

“很对,继承很简单,就是声明2个类而已,不过类式继承需要将第一个类的实例赋值给第二个类的原型。但你知道为何要这么做么?”

“类的原型对象的作用就是为类的原型添加共有方法,但类不能直接访问这些属性和方法,必须通过原型prototype来访问。而我们实例化一个父类的时候,新创建的对象复制了父类的构造函数内的属性与方法并且将原型proto指向了父类的原型对象,这样就拥有了父类的原型对象上的属性与方法,并且这个新创建的对象可直接访问到父类原型对象上的属性与方法。如果我们将这个新创建的对象赋值给子类的原型,那么子类的原型就可以访问到父类的原型属性和方法。”小白还有些不自信。

“对,你分析得很准确。补充一点,你说的新创建的对象不仅仅可以访问父类原型上的属性和方法,同样也可访问从父类构造函数中复制的属性和方法。你将这个对象赋值给子类的原型,那么这个子类的原型同样可以访问父类原型上的属性和方法与从父类构造函数中复制的属性和方法。这正是类式继承原理。”

“原来是这样,但是我们要如何使用子类呢?”小白问道。

“使用很简单,像下面这样即可。”小铭说。

var instance = new SubClass();
console.log(instance.getSuperValue());  //true
console.log(instance.getSubValue());    //false

“另外,我们还可以通过instanceof来检测某个对象是否是某个类的实例,或者说某个对象是否继承了某个类。这样就可以判断对象与类之间的继承关系了。”小铭补充说。

“instanceof?它如何就知道对象与类之间的继承关系呢?”小白不解。

“instanceof是通过判断对象的prototype链来确定这个对象是否是某个类的实例,而不关心对象与类的自身结构。”

“原来是这样。”于是小白写下测试代码。

console.log(instance instanceof SuperClass);   //true
console.log(instance instanceof SubClass);    //true
console.log(SubClass instanceof SuperClass);  //false

“我们说subClass继承superClass,可是为什么SubClass instanceof SuperClass得到的结果是false呢?”小白不解。

“前面说了,instanceof是判断前面的对象是否是后面类(对象)的实例,它并不表示两者的继承,这一点你不要弄混,其次我们看看前面的代码,你看我们在实现subClass继承superClass时是通过将superClass的实例赋值给subClass的原型prototype,所以说SubClass.prototype继承了superClass。”小铭解释说。

于是小白半信半疑地写下测试代码。

console.log(SubClass.prototype instanceof SuperClass);  //true
“真的是这样。”小白惊呼。

“是呀。这也是类式继承的一个特点。问你一个问题,你所创建的所有对象都是谁的实例?”

“Object吗?”小白回答说。

“对,正是JavaScript为我们提供的原生对象Object,所以你看下面的检测代码返回的就是true。”

console.log(instance instanceof Object);    //true
“哦,这么说Object就是所有对象的祖先了。”小白笑着说。

“哈哈,可是你知道吗,这种类式继承还有2个缺点。其一,由于子类通过其原型prototype对父类实例化,继承了父类。所以说父类中的共有属性要是引用类型,就会在子类中被所有实例共用,因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其他子类,比如你看下面的代码。”

function SuperClass(){
  this.books = ['JavaScript', 'html', 'css'];
}
function SubClass(){}
SubClass.prototype = new SuperClass();
var instance1 = new SubClass();
var instance2 = new SubClass();
console.log(instance2.books);  // ["JavaScript", "html", "css"] 
instance1.books.push('设计模式');
console.log(instance2.books);  // ["JavaScript", "html", "css", "设计模式"]

“instance1的一个无意的修改就会无情地伤害了instance2的book属性,这在编程中很容易埋藏陷阱。其二,由于子类实现的继承是靠其原型prototype对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化。”

“那我们要如何解决这些问题呢?”小白好奇地追问。

2.3.2 创建即继承——构造函数继承
“别着急,JavaScript是灵活的,自然也会有其他继承方法来解决,比如常见的构造函数继承。”

//构造函数式继承
// 声明父类
function SuperClass(id){
  // 引用类型共有属性
  this.books = ['JavaScript', 'html', 'css'];
  // 值类型共有属性
  this.id = id;
}
// 父类声明原型方法
SuperClass.prototype.showBooks = function(){
  console.log(this.books);
}
// 声明子类
function SubClass(id){ 
  // 继承父类
  SuperClass.call(this, id);
}
// 创建第一个子类的实例
var instance1 = new SubClass(10);
// 创建第二个子类的实例
var instance2 = new SubClass(11);
instance1.books.push("设计模式");
console.log(instance1.books);    // ["JavaScript", "html", "css", "设计模式"] 
console.log(instance1.id);    // 10 
console.log(instance2.books);   // ["JavaScript", "html", "css"] 
console.log(instance2.id);     // 11 
instance1.showBooks();      // TypeError

“小白,注意这里。SuperClass.call(this, id);这条语句是构造函数式继承的精华,由于call这个方法可以更改函数的作用环境,因此在子类中,对superClass调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给this绑定属性的,因此子类自然也就继承了父类的共有属性。由于这种类型的继承没有涉及原型prototype,所以父类的原型方法自然不会被子类继承,而如果要想被子类继承就必须要放在构造函数中,这样创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用的原则。为了综合这两种模式的优点,后来有了组合式继承。”

2.3.3 将优点为我所用——组合继承
“组合继承是不是说将这两种继承模式综合到一起呀?那么它又是如何做到的呢?”

“别着急,我们先总结一下之前两种模式的特点,类式继承是通过子类的原型prototype对父类实例化来实现的,构造函数式继承是通过在子类的构造函数作用环境中执行一次父类的构造函数来实现的,所以只要在继承中同时做到这两点即可,看下面的代码。”

// 组合式继承
// 声明父类
function SuperClass(name){
  // 值类型共有属性
  this.name = name;
  // 引用类型共有属性
  this.books = ["html", "css", "JavaScript"];
}
// 父类原型共有方法
SuperClass.prototype.getName = function(){
  console.log(this.name);
};
// 声明子类
function SubClass(name, time){ 
  // 构造函数式继承父类name属性
  SuperClass.call(this, name);
  // 子类中新增共有属性
  this.time = time;
}
// 类式继承 子类原型继承父类
SubClass.prototype = new SuperClass();
// 子类原型方法
SubClass.prototype.getTIme = function(){
  console.log(this.time);
};

“小白看到没,在子类构造函数中执行父类构造函数,在子类原型上实例化父类就是组合模式,这样就融合了类式继承和构造函数继承的优点,并且过滤掉其缺点,你测试看看。”

于是小白写下测试代码。

var instance1 = new SubClass("js book", 2014);
instance1.books.push("设计模式");  
console.log(instance1.books);   // ["html", "css", "JavaScript", "设计模式"] 
instance1.getName();       // js book
instance1.getTime();       // 2014 
var instance2 = new SubClass("css book", 2013);
console.log(instance2.books);   // ["html", "css", "JavaScript"] 
instance2.getName();       // css book 
instance2.getTime();       // 2013

“真的是这样呀,”小白兴奋地说,“子类的实例中更改父类继承下来的引用类型属性如books,根本不会影响到其他实例,并且子类实例化过程中又能将参数传递到父类的构造函数中,如name。这种模式真的很强大,所以这应该是继承中最完美的版本吧?”

“还不是,因为我们在使用构造函数继承时执行了一遍父类的构造函数,而在实现子类原型的类式继承时又调用了一遍父类构造函数。因此父类构造函数调用了两遍,所以这还不是最完美的方式。”

“难道还有更好的方式么?”

“那当然,JavaScript很灵活嘛。不过在学习这种方式之前我们先学习一个简单而很常用的方式。”

2.3.4 洁净的继承者——原型式继承
“2006年道格拉斯·克罗克福德发表一篇《JavaScript中原型式继承》的文章,他的观点是,借助原型prototype可以根据已有的对象创建一个新的对象,同时不必创建新的自定义对象类型。大师的话理解起来可能很困难,不过我们还是先看一下他实现的代码吧。”

// 原型是继承
function inheritObject(o){
  // 声明一个过渡函数对象
  function F(){}
  // 过渡对象的原型继承父对象
  F.prototype = o;
  // 返回过渡对象的一个实例,该实例的原型继承了父对象
  return new F();
}

“这种方式怎么和类式继承有些像呢?”

“对,它是对类式继承的一个封装,其实其中的过渡对象就相当于类式继承中的子类,只不过在原型式中作为一个过渡对象出现的,目的是为了创建要返回的新的实例化对象。”

“如果是这样,是不是类式继承中的问题在这里也会出现呢?”小白追问。

“是这样的,”小铭接着说,“不过这种方式由于F过渡类的构造函数中无内容,所以开销比较小,使用起来也比较方便。当然如果你感觉有必要可以将F过渡类缓存起来,不必每次创建一个新过渡类F。当然这种顾虑也是不必要的。随着对这种思想的深入,后来就出现的Object.create()的方法。”

“创建的新对象会不会影响到父类对象呢?”于是小白写下测试用例(测试代码)。

var book = {
  name: "js book",
  alikeBook: ["css book", "html book"]
};
var newBook = inheritObject(book);
newBook.name = "ajax book";
newBook.alikeBook.push("xml book");
var otherBook = inheritObject(book);
otherBook.name = "flash book";
otherBook.alikeBook.push("as book");
console.log(newBook.name);      //ajax book 
console.log(newBook.alikeBook);    //["css book", "html book", "xml book", "as book"] 
console.log(otherBook.name);    //flash book 
console.log(otherBook.alikeBook);  //["css book", "html book", "xml book", "as book"] 
console.log(book.name);       //js book
console.log(book.alikeBook);     //["css book", "html book", "xml book", "as book"]

“跟类式继承一样,父类对象book中的值类型的属性被复制,引用类型的属性被共用。”小白感叹道。

“然而道格拉斯·克罗克福德推广的继承并不只这一种,他在此基础上做了一些增强而推出一种寄生式继承。”

2.3.5 如虎添翼——寄生式继承
“寄生式继承?这还头一次听说,它是怎么实现的?”

“不着急,大师对该模式论述的话我们就不深究了,我们还是看看这种继承的实现吧。”

// 寄生式继承
// 声明基对象
var book = {
  name: "js book",
  alikeBook: ["css book", "html book"]
};
function createBook(obj){
  // 通过原型继承方式创建新对象
  var o = new inheritObject(obj);    
  // 拓展新对象
  o.getName = function(){        
    console.log(name);
  };  
  // 返回拓展后的新对象
  return o;              
}

“看懂了吗?”小铭问小白,“其实寄生式继承就是对原型继承的第二次封装,并且在这第二次封装过程中对继承的对象进行了拓展,这样新创建的对象不仅仅有父类中的属性和方法而且还添加新的属性和方法。”

“哦,这种类型的继承果如其名,寄生大概指的就是像寄生虫一样寄托于某个对象内部生长。当然寄生式继承这种增强新创建对象的继承思想也是寄托于原型继承模式吧。”

“嗯,是这个道理,而这种思想的作用也是为了寄生组合式继承模式的实现。”

2.3.6 终极继承者——寄生组合式继承
“寄生组合式继承?”小白好奇地问道。

“嗯,之前我们学习了组合式继承,那时候我们将类式继承同构造函数继承组合使用,但是这种方式有一个问题,就是子类不是父类的实例,而子类的原型是父类的实例,所以才有了寄生组合式继承。但是你知道是哪两种模式的组合么?”

“寄生当然是寄生式继承,寄生式继承依托于原型继承,原型继承又与类式继承相像,另外一种就不应该是这些模式了,所以另外一种继承模式应该是构造函数继承了吧。当然,子类不是父类实例的问题是由于类式继承引起的。”小白回答道。

“对,正是这两种继承,但是这里寄生式继承有些特殊,这里它处理的不是对象,而是类的原型。我们再次来看看道格拉斯·克罗克福德对寄生式继承的一个改造。”

/**
 * 寄生式继承 继承原型
 * 传递参数 subClass  子类
 * 传递参数 superClass 父类
 **/ 
function inheritPrototype(subClass, superClass){
  // 复制一份父类的原型副本保存在变量中
  var p = inheritObject(superClass.prototype);  
  // 修正因为重写子类原型导致子类的constructor属性被修改
  p.constructor = subClass;  
  // 设置子类的原型      
  subClass.prototype = p;        
}

“组合式继承中,通过构造函数继承的属性和方法是没有问题的,所以这里我们主要探究通过寄生式继承重新继承父类的原型。我们需要继承的仅仅是父类的原型,不再需要调用父类的构造函数,换句话说,在构造函数继承中我们已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们通过原型继承便可得到,但是这么直接赋值给子类会有问题的,因为对父类原型对象复制得到的复制对象p中的constructor指向的不是subClass子类对象,因此在寄生式继承中要对复制对象p做一次增强,修复其constructor属性指向不正确的问题,最后将得到的复制对象p赋值给子类的原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。”

“看上去好复杂呀。”小白惊叹道。

“所以你要去测一测呀。测试很简单,与组合模式相比只有一个地方做了修改,就是子类原型继承父类原型这一处,测试看看吧。”

“好吧。”于是小白写下了测试用例。

// 定义父类           
function SuperClass(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
// 定义父类原型方法
SuperClass.prototype.getName = function(){
  console.log(this.name);
};
// 定义子类
function SubClass(name, time){ 
  // 构造函数式继承
  SuperClass.call(this, name);
  // 子类新增属性
  this.time = time;
}
// 寄生式继承父类原型
inheritPrototype(SubClass, SuperClass);
// 子类新增原型方法
SubClass.prototype.getTime = function(){
  console.log(this.time);
};
// 创建两个测试方法
var instance1 = new SubClass("js book", 2014);
var instance2 = new SubClass("css book", 2013);

小白首先创建了父类,以及父类的原型方法,然后创建子类,并在构造函数中实现构造函数式继承,然后又通过寄生式继承了父类原型,最后又对子类添加了一些原型方法。

最后小白测试了一下,结果如下:

instance1.colors.push("black");
console.log(instance1.colors);   //["red", "blue", "green", "black"]
console.log(instance2.colors);   //["red", "blue", "green"]
instance2.getName();         //css book
instance2.getTime();          //2013

“现在你明白了吧,其实这种方式继承如图2-2所示,其中最大的改变就是对子类原型的处理,被赋予父类原型的一个引用,这是一个对象,因此这里有一点你要注意,就是子类再想添加原型方法必须通过prototype.对象,通过点语法的形式一个一个添加方法了,否则直接赋予对象就会覆盖掉从父类原型继承的对象了。”
screenshot

相关文章
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
31 0
|
1月前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承利用原型链查找属性,节省内存但不支持私有成员。类继承通过ES6的class和extends实现,支持私有成员但占用更多内存。两者各有优势,适用于不同场景。
19 0
|
3月前
|
JavaScript
|
3月前
|
JavaScript 前端开发
原型继承在 JavaScript 中是如何工作
原型继承在 JavaScript 中是如何工作
20 0
|
3月前
|
JavaScript 前端开发
JS实现继承的6种方式
JS实现继承的6种方式
|
4月前
|
设计模式 JavaScript 数据安全/隐私保护
js设计模式之工厂模式
js设计模式之工厂模式
33 0
|
4月前
|
JavaScript 前端开发
Javascript如何实现继承?
Javascript如何实现继承?
45 0
|
3月前
|
设计模式 前端开发 算法
【面试题】 ES6 类聊 JavaScript 设计模式之行为型模式(二)
【面试题】 ES6 类聊 JavaScript 设计模式之行为型模式(二)
|
1月前
|
设计模式 JavaScript 前端开发
JavaScript中继承的优缺点
JavaScript中继承的优缺点
13 3
|
1月前
|
JavaScript 前端开发
如何在 JavaScript 中实现继承?
如何在 JavaScript 中实现继承?
11 2