javascript 继承详解

一、原型链法(仿传统)

事先定义三个对象:Shape、TwoDShape、Triangle

基本形式如下:

Child.prototype = new Parent();

实例如下:

function Shape() {
  this.name = "shape";
  this.toString = function() {
    return this.name;
  }
}

function TwoDShape() {
  this.name = "2D shape"
}

function Triangle(side, height) {
  this.name = "Triangle";
  this.side = side;
  this.height = height;
  this.getArea = function() {
    return this.side * this.height / 2;
  }
}

通过new Shape()构造实体,然后通过这些实体的属性完成相关的继承工作,而不是直接继承自Shape()构造器;这也确保了在继承实现之后,我们对Shape进行任何修改、重写甚至删除,都不会对TwoDShape()产生影响,因为我们所继承的只是由该该构造器所建的一个实体.

TwoDShape.prototype = new Shape();
Triangle.prototype = new TwoDShape();

对对象的prototype属性进行完全重写时,这不同于简单的功能扩展,有可能对constructor属性产生一定的负面影响,所以在完成相关的继承关系设定后,对这些对象的constructor属性进行相应的重置是一个好习惯.

TwoDShape.prototype.constructor = TwoDShape;
Triangle.prototype.constructor = Triangle;

var my = new Triangle(5,10);

my.getArea();//25
my.toString();//Triangle

Shape.prototype.getDes = function() {
  return "shape描述";
};

my.getDes();//shape描述,如果放在修改原型的前面,则报错

my.constructor;//Triangle(side, height)

my instanceof Triangle;//true
my instanceof TwoDShape;//true
my instanceof Shape;//true
my instanceof String;//false


Triangle.prototype.isPrototypeOf(my);//true
TwoDShape.prototype.isPrototypeOf(my);//true
Shape.prototype.isPrototypeOf(my);//true
String.prototype.isPrototypeOf(my);//false

二、仅从原型链继承法(将共享属性迁移到原型中去)

基本形式如下:

Child.prototype = Parent.prototype;

上面的例子中,我们用某个构造器创建对象时,其属性就被添加到this中去,这使得某些不能通过实体改变的属性出现效率低下的情况。
当我们在new Shape()创建对象时,每个实体都会有一个全新的name属性,并在内存中拥有自己独立的存储空间,事实上,我们可以将name属性添加到所有实体所共享的原型对象中去。

    function Shape() {}
    Shape.prototype.name = "Shape";

这样我们在用new Shape()新建对象时,新的对象就不再含有属于自己的name属性了,而是被添加进了该对象的原型对象中。但这只是针对对象实体中不可变属性而言的,当然,这种方式也同样适用于对象中的共享性方法。

改善上述代码
    function Shape() {}
    Shape.prototype.name = "Shape";
    Shape.prototype.toString = function() {
        return this.name;
    }

对原型对象进行扩展之前,先玩成相关的继承关系的构建,否则TwoDShape.prototype中的后续内容有可能会抹掉我们所继承来的东西

    function TwoDShape() {}

    TwoDShape.prototype = new Shape();
    TwoDShape.prototype.constructor = TwoDShape;
    TwoDShape.prototype.name = "2D shape";

    function Triangle(side, height) {
        this.side = side;
        this.height = height;
    }
    Triangle.prototype = new TwoDShape();
    Triangle.prototype.constructor = Triangle;
    Triangle.prototype.name = "Triangle";
    Triangle.prototype.getArea = function() {
        return this.side * this.height / 2;
    }
完成后我们来进行测试,结果应该和之前一致
    var my = new Triangle(5,10);

    my.getArea();//25
    my.toString();//Triangle


    Shape.prototype.getDes = function() {
      return "shape描述";
    };

    my.getDes();//shape描述,如果放在修改原型的前面,则报错

    my.constructor;//Triangle(side, height)

    my instanceof Triangle;//true
    my instanceof TwoDShape;//true
    my instanceof Shape;//true
    my instanceof String;//false


    Triangle.prototype.isPrototypeOf(my);//true
    TwoDShape.prototype.isPrototypeOf(my);//true
    Shape.prototype.isPrototypeOf(my);//true
    String.prototype.isPrototypeOf(my);//false

我们可以通过hasOwnProperty()方法来明确自身属性与其原型链属性的区别

    my.hasOwnProperty("side");//true
    my.hasOwnProperty("name");//false
仅从原型继承法-改善

我们采取以下方法对效率做一些改善:
1. 不要单独为继承关系创建新对象
2. 尽量减少运行时方法搜索,例如上述代码中的toString()方法

改善上述代码

    function Shape() {}
    Shape.prototype.name = "Shape";
    Shape.prototype.toString = function() {
        return this.name;
    }

    function TwoDShape() {}

    TwoDShape.prototype = Shape.prototype;
    TwoDShape.prototype.constructor = TwoDShape;
    TwoDShape.prototype.name = "2D shape";

    function Triangle(side, height) {
        this.side = side;
        this.height = height;
    }
    Triangle.prototype = TwoDShape.prototype;
    Triangle.prototype.constructor = Triangle;
    Triangle.prototype.name = "Triangle";
    Triangle.prototype.getArea = function() {
        return this.side * this.height / 2;
    }

测试结果仍然相同,虽然这样做效率高了,但是这样做有一个问题,由于子对象与父对象指向的是同一个对象,所以一旦子对象对其原型进行了修改,父对象也会随即被改变,甚至所有的继承关系也都是如此。
例如下面的代码:

    Triangle.prototype.name = "Triangle";

这里对该对象的name属性进行了修改,于是Shape.prototype.name也随之改变了,也就是说当我们再用new Shape()新建对象时,新对象的name属性也会是”Triangle”

var s = new Shape();
s.name;//Triangle

三、临时构造器法-new F()

基本形式如下:

function extend(Child, Parent) {
    var F = function() {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
}

创建一个空函数F(),并将其原型设置为父级构造器。然后我们既可以用new F()来创建一些不包含父对象属性的对象,同时又可以从父对象prototype属性中继承一切。
修改后的代码如下:

    function Shape() {}
    Shape.prototype.name = "Shape";
    Shape.prototype.toString = function() {
        return this.name;
    }

    function TwoDShape() {}

    var F = function() {};
    F.prototype = Shape.prototype;
    TwoDShape.prototype = new F();
    TwoDShape.prototype.constructor = TwoDShape;
    TwoDShape.prototype.name = "2D shape";

    function Triangle(side, height) {
        this.side = side;
        this.height = height;
    }

    var F = function() {};
    F.prototype = TwoDShape.prototype;
    Triangle.prototype = new F();
    Triangle.prototype.constructor = Triangle;
    Triangle.prototype.name = "Triangle";
    Triangle.prototype.getArea = function() {
        return this.side * this.height / 2;
    }

下面我们来创建一个Triangle对象,并测试其方法

var my = new Triangle(5,10);
my.getArea();//25
my.toString();//Triangle

通过这种方法,我么就可以在保持原型链的基础上使父对象的属性摆脱子对象的影响了:

my.__proto__.__proto__.__proto__.constructor;//Shape()
var s = new Shape();
s.name;//Shape

注意:尽量将要共享的属性和方法添加到原型中,然后只围绕原型构建继承关系,不鼓励将对象的自身属性纳入继承关系,其背后的根源在于如果对象自身属性被设定得太过具体,会令其丧失可重用性。

在构建继承关系的过程中,引入一个uber属性,指向父类的原型对象

即:Child.uber = Parent.prototype;

function Shape() {};
Shape.prototype.name = "shape";
/*原型链上的所有toString()方法都会被调用*/
Shape.prototype.toString = function() {
  var result = [];
  if(this.constructor.uber) {
    result[result.length] = this.constructor.uber.toString();
  }
  result[result.length] = this.name;
    return result.join(', ');
}
function TwoDShape() {}

var F = function() {};
F.prototype = Shape.prototype;
TwoDShape.prototype = new F();
TwoDShape.prototype.constructor = TwoDShape;
TwoDShape.uber = Shape.prototype;//子类的uber属性指向父类的原型

TwoDShape.prototype.name = "2D shape";

function Triangle(side, height) {
  this.side = side;
  this.height = height;
}

var F = function() {};
F.prototype = TwoDShape.prototype;
Triangle.prototype = new F();
Triangle.prototype.constructor = Triangle;
Triangle.uber = TwoDShape.prototype;//子类的uber属性指向父类的原型

Triangle.prototype.name = "Triangle";
Triangle.prototype.getArea = function() {
  return this.side * this.height / 2;
}

var my = new Triangle(5,10);
my.toString();//shape, 2D shape, Triangle
将继承部分封装成函数
function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
}

四、原型属性拷贝法

将继承部分封装成函数,对只包含基本类型的对象比较好

基本形式如下:

function extend2(Child, Parent) {
    var c = Child.prototype;
    var p = Paren.prototype;
    for(var i in p) {
        c[i] = p[i];
    }
    c.buer = p;
}

这样使得属性查找操作更多的停留在对象本身,从而减少了原型链上的查找

五、全属性拷贝法(浅拷贝)

基本形式如下:

function extendCopy(p) {
    var c = {};
    for(var i in p) {
        c[i] = p[i];
    }
    c.uber = p;
    return c;
}

Firebug后台有一个extend()函数,其代码所用的就是这种方式,另外某些流向的javascript程序库,例如jQuery的早起版本中,原型采用的就是这种基本模式。

六、深拷贝法

基本形式如下:

function deepCopy(p, c) {
    var c = c || {};
    for(var i in p) {
        if(typeof p[i] === 'object') {
            c[i] = (p[i].constructor === Array) ? [] : {};
            deepCopy(p[i], c[i]);
        } else {
            c[i] = p[i];
        }
    }
    return c;
}

在jQuery的较新版本中,继承关系的实现通常都是采用深拷贝的形式。

七、原型继承法(将父对象设置成子对象的原型)

,基本形式如下:

function Object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}

如果需要访问uber属性,则代码如下:

function object(o) {
    var n;
    function F() {};
    F.prototype = o;
    n = new F();
    n.uber = o;
    return n;
}

八、扩展与增强模式(原型继承与属性拷贝的混合)

基本形式如下:

function objectPlus(o, stuff) {
  var n;
  function F() {};
  F.prototype = o;
  n = new F();
  n.uber = o;

  for(var i in stuff) {
    n[i] = stuff[i];
  }

  return n;
}
  1. 使用原型继承的方式克隆(clone)现存对象
  2. 而对其他对象使用属性拷贝(copy)的方式

创建一个基本对象Shape,测试上述方法

var Shape = {
  name: "Shape",
  toString: function() {
    return this.name;
   }
};

var TwoDee = objectPlus(Shape, {
  name: "2D Shape",
  toString: function(){
    return this.uber.toString() + ", " + this.name;
  }
});

var Triangle = objectPlus(TwoDee, {
  name: "Triangle",
  getArea: function() {
    return this.side * this.height / 2;
  },
  side: 0,
  height: 0
});

var my = objectPlus(Triangle, {side:4, height: 4});
my.toString();//Shape, 2D Shape, Triangle, Triangle

Triangle的name属性被重复两次,是因为我们在具体实例化时是继承于Triangle对象的,所以这里多了一层继承关系,我们也可以给实例一个新的name属性,如下:

var my = objectPlus(Triangle, {side:4, height: 4, name: "my"});
my.toString();//Shape, 2D Shape, Triangle, my

九、多重继承法

基本形式如下:

function multi() {
    var n = {}, stuff, j = 0;
    len = arguments.length;

    for(j = 0; j < len; j++) {
        stuff = arguments[j];
        for(var i in stuff) {
            n[i] = stuff[i];
        }
    }
    return n;
}

多重继承里,multi()中的循环是按照对象的输入顺序进行遍历的,如果其中有两个对象拥有相同的实现,通常以后一个对象为准。

在这里,我们或许需要了解一种叫做混合插入的技术,这项技术在一些语言(比如Ruby)中非常受欢迎,我们可以将其看作一种为对象提供某些使用功能的技术,只不过,它并不是通过子对象的继承与扩展来完成的。我们所讨论的多重继承,实际上正式基于这种技术理念来实现的。也就是说,每当我们新建一个对象时,可以选择将其他对象的内容混合到我们的新对象中去,只要将它们全部传递给multi()函数,我们就可以在不建立相关继承关系树的情况下获得这些对象的功能。

十、寄生式继承法

基本形式如下:

function parasite(victim) {
    var that = object(victim);
    that.more = 1;
    return that;
}

该方法通过一个类似构造器的函数来创建对象。该函数会执行相应的对象拷贝,并对其进行扩展,然后返回该拷贝。

十一、构造器借用法

基本形式如下:

function Child() {
    Parent.apply(this, arguments);
}

该方法可以只继承父对象的自身属性,也可以与方法1结合使用,以便从原型中继承相关内容。它便于我们的子对象继承某个对象的具体属性(并且还有可能是引用类属性)时,选择最简单的处理方式。

十二、构造器借用与属性拷贝法(借用构造器与原型复制)

基本形式如下:

function Child() {
    Parent.apply(this, arguments);
}
extend2(Child, Parent);

该方法是方法11和方法4的结合体,它允许我们在不重复调用父对象构造器的情况下同时继承其自身属性和原型属性。

总结

实现继承的方式,大致可以分为两类:
1. 基于构造器工作的模式
2. 基于对象工作的模式
当然,我们也可以根据以下条件对继承模式进行分类:
1. 是否使用原型
2. 是否使用原型拷贝
3. 两者都有(即执行原型属性拷贝)

如何进行选择,取决于设计风格、性能需求、具体项目任务及团队。如果习惯于从类的角度解决问题,可以选择基于构造器的工作模式;如果只关心“类”的某些具体实例,可以选择基于对象的模式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaScript 中,继承是一个非常常见的概念。ES6 引入了 Class 语法糖,让继承更加易于理解和实现。在 Class 中,我们可以使用 extends 关键字来创建一个子类,使其继承父类的属性和方法。 下面我们来详细了解一下如何在 JavaScript 中使用 extends 实现继承。 ### 基础语法 首先,我们需要定义一个父类。在 ES6 中,我们可以使用 Class 来定义一个类。例如: ```javascript class Animal { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a noise.'); } } ``` 这个 Animal 类有一个构造函数和一个 speak 方法。构造函数会在创建实例时被调用,而 speak 方法则可以让动物发出一些声音。 接下来,我们来创建一个子类。使用 extends 关键字来创建子类,并使用 super() 方法调用父类的构造函数。例如: ```javascript class Dog extends Animal { constructor(name) { super(name); } speak() { console.log(this.name + ' barks.'); } } ``` 这个 Dog 类继承了 Animal 类,并覆盖了其 speak 方法。在构造函数中,我们通过 super() 方法来调用父类的构造函数,并将传递的参数传递给它。 现在,我们可以创建一个 Dog 的实例,并调用其 speak 方法: ```javascript let d = new Dog('Mitzie'); d.speak(); // Mitzie barks. ``` ### 继承父类的方法 当一个子类继承了一个父类时,它会继承父类的属性和方法。例如,在上面的例子中,Dog 类继承了 Animal 类,因此它继承了 Animal 类的 speak 方法。 当我们调用子类的方法时,如果子类没有实现该方法,它会自动调用父类的方法。例如,在上面的例子中,如果我们不覆盖 Dog 类的 speak 方法,它将调用 Animal 类的 speak 方法。 ### 覆盖父类的方法 如果一个子类需要覆盖父类的方法,我们可以在子类中重新定义该方法。例如,在 Dog 类中,我们覆盖了 Animal 类的 speak 方法,使其输出“barks”而不是“makes a noise”。 ### 调用父类的方法 有时候,我们需要在子类中调用父类的方法。我们可以使用 super 关键字来调用父类的方法。例如,在 Dog 类中,我们可以通过调用 super.speak() 来调用 Animal 类的 speak 方法。 ### 总结 继承是一个非常常见的概念,也是面向对象编程中的重要概念之一。在 JavaScript 中,我们可以使用 extends 关键字来实现继承。通过继承,子类可以继承父类的属性和方法,也可以覆盖父类的方法,并且可以调用父类的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值