面向对象在javascript中的三大特征之继承

时间:2021-05-02 19:52:45

继承


在JavaScript中的继承的实质就是子代可以拥有父代公开的一些属性和方法,在js编程时,我们一般将相同的属性放到父类中,然后在子类定义自己独特的属性,这样的好处是减少代码重复。继承是面向对象的基础,是代码重用的一种重要机制。
——此文整理自 《jQuery 开发从入门到精通》 ,这是本好书,讲的很详细,建议购买阅读。

继承的作用


实现继承的主要作用是:
① 子类实例可以共享超类属性和方法。
② 子类可以覆盖和扩展超类属性和方法。

继承的分类


在JavaScript中是不支持类的概念,使用构造器机制来实现类的特性。
JavaScript中类的继承不止一种,主要包括:类继承(构造函数继承),原型继承,实例继承,复制继承,克隆继承,混合继承,多重继承等。

类继承
类继承也叫构造函数继承,其表现形式是在子类中执行父类的构造函数。实现本质:比如把一个构造函数A的方法赋值为另一个构造函数B,然后调用该方法,使构造函数A在构造函数B内部执行,这是构造函数B就拥有了构造函数A中定义的属性和方法。这就是B类继承A类。示例如下:
  function A(x){
    this.x = x;
    this.say = function() {
      console.log(this.x + ' say');
    }
  }
  function B(x,y) {
    this.m = A; // 把构造函数A作为一个普通函数引给临时方法m()
    this.m(x); // 把当前参数作为值传给构造函数A,并且执行
    delete this.m; // 清除临时方法
    this.y = y;
    this.call = function(){
      console.log(this.y);
    }
  }

  // 测试类继承
  var a = new A(1);
  var b = new B(2,3);
  a.say(); // 1 say
  b.say(); // 2 say
  b.call(); // 3
上面的实现方式很巧妙对吧,但是这种设计方式太随意,缺乏严密性。严禁的设计模式应该考虑到各种可能存在的情况和类继承关系中的互相耦合性。所以一般我们尽可能把子类自身的方法放在原型里去实现。下面这种创建方式会更好:
function A(x){
  this.x = x;
  this.say = function() {
    console.log(this.x + ' say');
  }
}
function B(x,y){
  this.y = y;
  A.call(this,x); // 在构造函数B内调用超类A,实现绑定,用call来实现借用
}
B.prototype = new A(); // 设置原型链,建立继承关系
B.prototype.constructor = B; // 恢复B的原型对象的构造函数为B,如果不操作就会指向A
B.prototype.gety = function(){ // 给类B添加新的方法
  return this.y;
}

// 测试 类继承
var a = new A('A');
var b = new B('Bx','By');
a.say(); // A say
b.say(); // Bx say
console.log(b.gety()); // By
console.log(B.prototype.__proto__ === A.prototype); // true;
console.log(b.constructor === B); // true  这里也可以把B中的 B.prototype.constructor = B; 去除,结果为false
在js中实现类继承,需要设置3点:
① 在子类构造函数结构体内,使用函数call()调用父类构造函数,把子类的参数传递给调用函数如上面的例子:A.call(this,x) 这样子类可以继承父类的所有属性和方法。
② 在子类和父类之间建立原型链,如上例:B.prototype = new A() 为了实现类的继承必须保证他们原型链上的上下级关系。即设置子类的prototype 属性指向父类的一个实例即可。
③ 恢复子类原型对象的构造函数, 如上例:B.prototype.constructor = B 

在类继承中,call() 和 apply() 方法被频繁使用,它们之间的功能和用法都是相同的,唯一区别就是第2个参数类型不同。如果深入,请看我之前的博客:http://blog.csdn.net/tyro_java/article/details/51020720

此处需要提一下,就是类的构造函数中的成员,一般称之为本地成员,而类的原型成员就是类的原型中的成员,此处我们只考虑原型中的成员继承。本地成员继承可以用call 和 apply。下面我们来看下类继承的原型成员的继承封装函数:
在函数体内,首先定义一个空函数F(),用来实现功能中转,把它的原型指向父类的原型,把空函数的实例传递给子类的原型,这样就避免了直接实例化父类引发的内存问题,因为实际开发中,父类可能很大,实例化后,会占用很大一部分内存。
// 定义一个继承函数
function extend(Sub,Sup){ // 有两个入口参数,Sub是子类,Sup是父类
  var F = function(){}; // 建立一个临时的构造函数
  F.prototype = Sup.prototype; // 把临时构造函数的原型指向父类的原型
  Sub.prototype = new F(); // 实例化临时类,此处相当于把子类的原型指向父类的实例
  Sub.prototype.constructor = Sub; // 恢复子类的构造函数
  Sub.sup = Sup.prototype; // 在子类中定义一个本地属性存储超类原型,可避免子类和超类耦合
  if(Sup.prototype.constructor === Object.prototype.constructor) { // 检测超类构造器是否为Object构造器
    Sup.prototype.constructor = Sup;
  }
}
// 下面定义两个类用来测试上面的继承函数
function A(x,y) {
  this.x = x;
  this.y = y;
}
A.prototype.add = function() {
  return (this.x-0) + (this.y-0); // 此处-0 的目的是确保字符串类型可转成数值型
}
A.prototype.minus = function() {
  return this.x - this.y;
}
function B(x,y) {
  A.apply(this,[x,y]);
}
// 开始实现类继承中的原型成员继承
extend(B,A);
// 为了不与A类中的代码耦合可以单独为B定义一个同名的add
B.prototype.add = function() {
  return B.sup.add.call(this); // 避免代码耦合
}
// 测试继承
var b = new B(1,2);
console.log(b.minus()); // -1
console.log(b.add()); // 3

原型继承
原型继承是js中最通用的继承方式,不用实例化对象,通过直接定义对象,并被其他对象引用,这样形成的一种继承关系,其中引用对象被称为原型对象。
function A(){
  this.color = 'red';
}
function B(){}
function C(){}
B.prototype = new A();
C.prototype = new B();
// 测试原型继承
var c = new C();
console.log(c.color); // red
原型继承显得很简单,不需要每次构造都调用父类的构造函数,也不需要通过复制属性的方式就能快速实现继承。但它也存在一些缺点:
① 每个类型只有一个原型,所以不支持多重继承
② 不能很好的支持多参数或动态参数的父类,显得不够灵活。
③ 占用内存多,每次继承都需要实例化一个父类,这样会存在内存占用过多的问题。

实例继承
实例化类可创建新的实例对象,这个实例对象将继承类的所有特征。
function Arr() {
  var a = new Array();
  return a;
}
var arr = new Arr();
arr[0] = 1;
arr[1] = 2;
console.log(arr); // [1,2]
console.log(Array.isArray(arr)); // true
console.log(arr instanceof Array); // true
console.log(arr instanceof Arr); // false
通过构造函数中完成对类的实例化操作,然后返回实例对象,这就是实例继承的由来。实例继承可实现对所有对象的继承,包括自定义类,核心对象和DOM对象。但是也有一些缺点
① 实例继承无法传递动态参数,它是封闭在函数体内试下你,不能通过call和apply来实现动态传参。
② 实例继承只返回一个对象,不支持多重继承
③ 实例继承对象它仍然保持与原对象的实例关系,无法实现继承对象是封装类的实例。如:console.log(arr instanceof Arr); // false

复制继承
复制继承就是利用for in 遍历对象成员,逐一复制给另一个对象。通过这种方式来实现继承。

function A(){
  this.color = 'red';
}
A.prototype.say = function() {
  console.log(this.color);
}
var a = new A();
var b = {};
// 开始拷贝
for(var item in a) {
  b[item] = a[item];
}
// 开始测试
console.log(b.color); // red
b.say(); // red
我们把它封装一下:
Function.prototype.extend = function(obj){
  for(item in obj){
    this.constructor.prototype[item] = obj[item];
  }
}
function A(){
  this.color = 'green';
}
A.prototype.say = function(){
  console.log(this.color);
}
// 测试
var b = function(){};
b.extend(new A());
b.say(); // green
复制继承实际上是通过反射机制复制类对象中的可枚举属性和方法来模拟继承。这种可以实现多继承。但也有缺点:
① 由于是反射机制,不能继承非枚举类型的属性和方法。对于系统核心对象的只读方法和属性也无法继承。
② 执行效率差,这样的结构越庞大,低效就越明显。
③ 如果当前类型包含同名成员,这些成员会被父类的动态复制给覆盖。
④ 多重继承中,复制继承不能清晰描述父类和子类的相关性。
⑤ 在实例化后才能遍历成员,不够灵活,也不支持动态参数
⑥ 复制继承仅仅是简单的引用赋值,如果父类成员包含引用类型,那么也会带来很多副作用,如不安全,容易遭受污染等。

克隆继承
通过对象克隆方式继承,可以避免赋值对象成员带来的低效。
为Function对象扩展一个clone方法。该方法可把参数对象赋值给一个空的构造函数的原型对象,然后返回实例化后的对象,这样该对象就拥有构造哈数包含的所有成员了。
Function.prototype.clone = function(obj){
  function Temp(){};
  Temp.prototype = obj;
  return new Temp();
}
function A(){
  this.color = 'purple';
}
var o = Function.clone(new A());
console.log(o.color); // purple


混合继承
混合继承是把多种继承方式一起使用,发挥各个优势,来实现各种复杂的应用。最常见的就是把类继承和原型继承一起使用。
function A(x,y){
  this.x = x;
  this.y = y;
}
A.prototype.add = function(){
  return (this.x-0) + (this.y-0);
}
function B(x,y){
  A.call(this,x,y);
}
B.prototype = new A();

// 测试
var b = new B(2,1);
console.log(b.x); // 2
console.log(b.add()); // 3

多重继承
继承一般包括单向继承和多向继承,单向继承模式较为简单,每个子类有且仅有一个超类,多重继承是一个比较复杂的继承模式。一个子类可拥有多个超类。JavaScript原型继承不支持多重继承,但可通过混合模式来实现多重继承。下面让类C来继承类A和类B:
function A(x){
  this.x = x;
}
A.prototype.hi = function(){
  console.log('hi');
}
function B(y){
  this.y = y;
}
B.prototype.hello = function(){
  console.log('hello');
}
// 给Function增加extend方法
Function.prototype.extend = function(obj) {
  for(var item in obj) {
    this.constructor.prototype[item] = obj[item];
  }
}
// 在类C内部实现继承
function C(x,y){
  A.call(this,x);
  B.call(this,y);
};
C.extend(new A(1));
C.extend(new B(2));

// 通过复制继承后,C变成了一个对象,不再是构造函数了,可以直接调用
C.hi(); // hi
C.hello(); // hello
console.log(C.x); // 1
console.log(C.y); // 2
一个类继承另一个类会导致他们之间产生耦合,在js中提供多种途径来避免耦合的发生如 掺元类
掺元类是一种比较特殊的类,一般不会被实例化或者调用,定义掺元类的目的只是向其他类提供通用的方法。
// 定义一个掺元类
function F(x,y){
  this.x = x;
  this.y = y;
}
F.prototype = {
  getx:function(){
    return this.x;
  },
  gety:function(){
    return this.y;
  }
}

// 原型拷贝函数
function extend(Sub,Sup){ // Sub 子类 , Sup 掺元类
  for(var item in Sup.prototype){
    if(!Sub.prototype[item]){ // 如果子类没有存在同名成员
      Sub.prototype[item] = Sup.prototype[item]; // 那么复制掺元类成员到子类原型对象中
    }
  }
}

// 定义两个子类 A,B
function A(x,y){
  F.call(this,x,y);
}
function B(x,y){
  F.call(this,x,y);
}

// 调用extend函数实现原型属性,方法的拷贝
extend(A,F);
extend(B,F);
console.log(A.prototype);

// 测试继承结果
var a = new A(2,3);
console.log(a.x); // 2
console.log(a.getx()); // 2
console.log(a.gety()); // 3

var b = new B(1,2);
console.log(b.x); // 1
console.log(b.getx()); // 1
console.log(b.gety()); // 2
通过此种方式把多个子类合并到一个子类中,就实现了多重继承。