JavaScript原型与继承

在上一篇创建对象的基础上继续思考。对象创建出来了,可是每次都要重新创建对象也太麻烦了。学生也是人,没必要重头写学生,完全可以由人扩展来。这就引出了继承。

原型链

js通过什么方法来实现继承呢?就是将对象的原型替换成另一个类的实例。由于原型链的存在,新的对象就有了很多属性和方法而不用我们重头再来创建。
例子:

function Person(name) {
    this.name = name;
    if (typeof this.sayName !== "function") {
        Person.prototype.sayName = function() {
            return this.name;
        };
    };
}

function Student(score) {
    this.score = score;
}

Student.prototype = new Person("foo");
Student.prototype.constructor = Student;

var student = new Student(100);

alert(student.name);    //"foo"

alert(student.sayName());   //"foo"

Student的构造函数并没有定义name属性,所有由new Student()产生的对象并没有name属性。而且Student原型里也没有这个属性。可是当我们把Student的原型换成一个Person实例的时候,这个实例中是有name属性的,也就是Student的原型中有了这个属性,所以student就能访问name属性了。这就是继承的含义,我们不需要再为Student定义name属性,只需要让他继承已有的类就行了。
那这个sayName方法我们是添加在Person的原型上的,Person实例(也就是Student原型)上是没有这个方法的,为什么Student的实例能访问这个方法呢。这就是原型链的神奇了。我在你身上找不到,就去你的原型上找。
这样我们产生的对象通过继承就有了:1.自己构造函数所产生的属性和方法 2.父类实例的属性和方法 3.父类原型上定义的属性和方法。
如果组合使用构造函数和原型模式或者动态原型模式创建父类实例的话,父类实例上只有属性,方法都在父类原型上。

现在我们来思考一个问题。这样产生的对象符合我们的要求吗。首先明确一点。我们一般要产生的对象是每个实例都有自己独自的属性(而不是共享),而方法则希望能共享。
那通过继承产生的是这样的对象吗?可以看到,由于我们子类原型上定义的有属性,所以所有的子类实例都会共享原型上的属性,这并不是我们想要的。
那要想每个子类实例都有自己的属性怎么做呢。我们知道构造函数可以做到这一点(通过构造函数产生的实例都有自己的属性)。于是就产生了一个技巧:借用构造函数。

借用构造函数与组合继承

借用构造函数的思想就是通过在子类的构造函数中定义原本在子类原型上的属性来实现每个子类实例都有自己的属性。直接在子类构造函数中定义就行。可是我们观察一下会发现,子类的原型就是父类通过构造函数产生的实例啊,也就是子类原型上的属性全部是父类构造函数来定义的。
那我们只需要在子类中“借用”父类的构造函数来定义和父类实例一毛一样的属性不就省去了自己一个一个定义的麻烦了吗。
看例子:

function Person(name) {
    this.name = name;
    if (typeof this.sayName !== "function") {
        Person.prototype.sayName = function() {
            return this.name;
        };
    };
}

function Student(name, score) {
    Person.call(this, name);
    this.score = score;
}

Student.prototype = new Person("foo");
Student.prototype.constructor = Student;
var student = new Student("bar", 100);

alert(student.name);        //"bar"

通过借用构造函数每个Student实例都有了自己的name属性。就自然覆盖了原型(Person实例)中的name属性,而方法还仍然在Person原型中。完美。
这种继承方式就是组合继承。组合继承就是将属性定义到子类构造函数中(可以借用父类构造函数),将方法定义到原型链上,将原型链技术和借用构造函数技术组合起来所以叫组合继承。
这就是我们以后经常使用的继承模式。
要注意的一点是,我们看待继承时眼光不能太狭隘,单看这个例子可能会想有必要这么麻烦来继承吗,直接动态原型模式产生子类对象不就完了。可是当我们有千千万万子类要定义的时候你就知道为什么需要继承了。

原型式继承

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

只要我们传递给这个函数一个参数,这个函数就能返回一个对象实例,这个对象实例的原型就是我们给的参数,也就是返回的实例继承了传递进去的参数。

这样返回的对象好不好呢?属性是否独立,方法是否共享?恩,可以做到。
我们可以在F()构造函数内借用o的构造函数,再在o的原型或者F的原型(就是o)上添加方法,可是我们要是真的这么做了那和组合继承又有什么区别呢?唯一的好处好像就是封装起来了,以后继承就方便了,直接调用这个函数就好了。
可是事实上JavaScript帮我们封装好了,但是并不是按我们想的那样封装,就是对最原始的函数(上面代码中的函数)进行了封装,所以叫做原型继承嘛,没有组合。我们直接调用Object.create(o)就相当于调用了上述方法。可是这样产生的属性是共享的(原型继承嘛),所以搞不明白这种封装有什么用。原型式继承很少单独使用,大多数时候使用组合继承。

寄生式继承

又是一种没什么意思的继承

function createStudent(person) {
    var student = Object.create(person);
    student.sayScore = function() {
        alert(100);
    };

    return student;
}

比原型式继承增强了一点,可是这种方式产生的对象每个实例都有独立的方法。所以也不是很有用。

寄生组合式继承

现在我们再来思考组合继承。我们子类每个实例都有自己的属性,完全覆盖了子类原型(父类实例)中的属性。那我们要这个父类实例干什么呢,我们想要的不过是通过这个父类实例找到父类原型,然后调用原型中的方法。所以我们真的需要创建一个父类实例来作为子类原型吗?我们完全可以将父类原型复制一份来作为子类的原型,这样我们就不需要顺着原型链往上爬,也不需要创建一个没有用的父类实例来作为子类原型。
由此产生了寄生组合式继承。
例如我们想Student继承Person

function Person(name) {
    this.name = name;

    if (typeof this.sayName !== "function") {
        Person.prototype.sayName = function() {
            return this.name;
        };
    }
}

function Student(name, score) {
    Person.call(this, name);
    this.score = score;
}

Student.prototype = Object(Person.prototype);
Student.prototype.constructor = Student;

var student = new Student("foo", 100);

这才是真正的完美! 你的原型就是我的原型。

发布了25 篇原创文章 · 获赞 46 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/huster1446/article/details/62418618