在这之前已经有了解过原型的这部分内容,但是对于怎么实现继承,有哪些方式,这一块还不是很清楚。昨天看了一遍,也大概了解了,在这里进行一下记录。

在说 原型 之前,我们先说一下,构造函数。

构造函数

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    };
}
var person = new Person("zcj",24,"software enginner");

在 JS 中,一切皆对象,函数也是一个对象,所以也可以直接使用 this.name 给属性赋值。

我们注意到,这里用到了 new 来创建了对象。任何函数,只要通过 new 操作符来调用,那它就会变成构造函数,如果不通过 new 操作符来调用,那它就跟普通的函数没有什么两样。

通过new创建实例对象,会经历下面几个过程:

  1. 创建一个新对象。
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象。

可以通过下面两种方式来验证实例对象是不是 Person 的实例。

  1. person.constructor == Person

    通过 new 创建出来的对象,有一个 constructor (构造函数)属性,该属性指向 构造函数。我们可以通过这个来做一个简单的判断。

  2. 还有一种更可靠的方法,因为 constructor 毕竟只是一个属性而已,很容易就可以被就修改,我们可以通过 instanceof 来验证。

    person instanceof Person

原型

我们创建的每个对象或者函数(当然,函数也是对象),都有一个 prototype 属性,这个就是 原型。好了,讲完了。。。。。。。

prototype 往小了说,就是一个属性,这个属性是一个指针,指向一个对象,而且同一个构造函数创建出来的实例的 prototype 指向的是同一个对象。使用原型对象的好处是可以让所有的实例对象共享它所包含的属性和方法。而且原型对象还有一个特殊的成员变量,constructor 这个会重新指向 构造函数。关系可以参考下图

graph LR
实例A --prototype--> 原型对象
实例B --prototype--> 原型对象
原型对象 --constructor--> 构造函数
构造函数 --prototype--> 原型对象

而且原型对象的属性,在实例中是可以直接使用的,比如:

function Person(){
    
}
Person.prototype.run='跑';
var person = new Person();
console.log(person.run); // 结果输出 跑

但是如果实例对象中已经有这个同名的属性就会覆盖掉原型对象中的属性,这个跟 Java 中是差不多的,子类定义了同名的属性,是会覆盖掉父类的同名属性。其实这个问题是因为查找的问题,当使用一个属性时,如果实例对象中已经就有这个属性的话就直接使用,没有再到原型对象中查找。

我们可以使用 hasOwnProperty() 来检测一个属性是来自 实例对象,还是 原型对象中,如果返回的是 true ,则说明这个属性来自 实例对象。

我们还可以用一个包含所有属性和方法的对象字面量来重写整个原型对象

function Person(){
    
}
Person.prototype={
    name:'zcj',
    age: 23,
    sayName: function(){
        alert(this.name)
    }
}

但是这样写有个问题,就是 原型对象的 constructor 就指向 Object 了(因为创建的对象默认继承 Object,或者说是 Object 的子类)。如果构造函数比较重要,我们可以直接把 constructor 指向 构造函数。但是这种方式重设 constructor 属性,会导致它的 Enumerable 特性被设置为 true。

创建对象

工厂模式

function createPreson(name,age,job){
    var o=new Object();
    o.name=name;
    o.age=age;
    o.job=age;
    return o;
}

var person = createPerson("zhangsan",29,"Softwar Enginner");

直接就在内部创建了一个对象,赋值后就返回了。但是这些都是没有灵魂的,虽然都拥有相同的属性,但是却无法进行对象识别。

构造函数模式

我们之前讲过,在 JS 中,万物皆对象,一个方法也是对象,我们可以给这个对象的属性赋值,然后使用 new ,把这个方法当做构造函数来创建对象。

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job =job;
}
var person = new Person("zhangsan",23,"Software Engger");

使用构造函数创建出来的对象,可以通过 instanceof 来验证对象类型。

构造函数模式虽然好用,但是也有缺点,就是每个方法都要在每个实例上重新创建一遍。这个 Java 不同,JS 中,方法也是对象。虽然可以通过把方法提到全局,然后再进行引用的方式进行解决,但是实在不方便。

原型模式

function Person(){
    
}

Person.prototype.name = "zhangsan";
Person.prototype.age = 23;
Person.prototype.sayName = function(){
    alert(this.name);
}
var person = new Person();
person.sayName();   // zhangsan
var person1 =new Person();
person1.sayName();  //zhangsan

使用原型模式的效果很明显,原型对象里的属性全部共用了。但是缺点也很明显,所有实例中的原型对象都是同一个,这样就会导致,一处修改,到处影响的效果,这显然不是我们想要的效果。

我们希望的是私有的属性不被影响,公有的属性或方法能够共用。

组合模式

组合模式就是组合了 构造模式 和 原型模式。

使用 构造函数来定义实例属性,使用原型模式来定义方法和共享的属性。写起来大概是这个样子的。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ['shelby','court']
}
Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name)
    }
}
var person1 = new Person('nicholas', 34, 'software engineer')
var person2 = new Person('greg', 32, 'doctor')

person1.friends.push('van')
alert(person1.friends)      //'shelby,court,van'
alert(person2.friends)      //'shelby,court'
alert(person1.friends === person2.friends)      // false
alert(person1.sayName === person2.sayName)      // true

动态原型模式

动态原型是在 组合模式 上进一步演化,它把所有的信息的封装在构造函数内。通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    
    if(typeof this.sayName != 'function'){
        Peron.prototype.sayName = function(){
            alert(this.name)
        }
    }
}

这里有一个点要注意,由于原型的修改是立即生效的。这里也不能使用对象字面量重写原型对象,因为如果在已经创建了实例的情况下重写原型,那么就会切断现有实例和新原型的关系。

寄生构造模式

在前面几种模式都不适用的情况下,可以使用 寄生构造函数模式。这种模式跟工厂模式很像,但是他是使用 new 来创建对象的。

functioin Person(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    }
    return o;
}
var person = new Person('zhangsan',23,'software engineer')

一开始看是不是有点懵逼。。。。这个吊东西有什么用?这个不是跟工厂模式差不多嘛。

其实这个也可以看成的 工厂模式 和 构造函数模式的组合。

这种场景一般是用在扩展或者修改原有的对象上。比如,我们想要创建一个有额外方法的特殊数组,由于不能直接修改 Array 构造函数。因此可以用这个模式。

function SpecialArray(){
    var values = new Array();
    values.push.apply(vales,arguments);
    //添加方法
    values.toPipedString = function(){
        return this.jon("|");
    }
    return values;
}
var colors = new SpecialArray('red','blue','green')
alert(colors.toPipedString()); // 'red|blue|green'

还有一点要补充的,就是用这种类型创造出来的对象是不支持对象检查的,跟在外面创建出来的对象没什么区别,所以也不能使用 instanceof 来检查类型。由于存在上述的问题,建议能用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

这个模式跟 寄生构造模式 比较像。但是,不使用 new 操作符调用构造函数,在新创建对象的实例方法中不引用 this。

function Person(name, age, job){
    var o = new Object();
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(name);
    }
    return o;
}

这种模式下,除了调用 sayName() 方法外,没有其他方法访问 name 的值,这样创建出来的对象是一个稳妥对象,即使其他代码修改了 name 这个属性,也没有别的办法获取到 当初传入构造函数的原始值。稳妥构造函数模式提供的这种安全性,是它非常适合在某些安全执行环境,例如: ADsafe 和 Caja 提供的环境下使用。

继承

很多 OO 语言中都支持两种继承方式,接口继承和实现继承。但在 JS 中是没有接口继承的。而且实现继承主要是依靠原型链来实现的。

原型链

原型链的基本实现过程是,让原型对象指向另一个其他类型的实例,这样的话,原型对象中就包含了指向另一个类型的原型对象的指针,如果这个原型又包含另一个类型的原型对象的指针,这个链条就可以一直延续下去。原型链大概就是这个意思了。

graph LR
A --原型--> B实例
B实例 --原型--> C实例
C实例 --原型--> ...

大致代码如下:

function SuperType(){
    thisproperty = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}
function SubType(){
    this.subproperty = false;
}
//继承 SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
    return this.subproperty;
}
var instance = new SubType();
alert(instance.geSuperValue);   // true

有一点要注意,由于SubType 的原型对象替换为 SuperType 的实例,所以 SubType 的 constructor 指向的是 SuperType 的构造函数。

通过原型链,我们可以得到类似其他 OO 语言的继承效果。如

alert(instance instanceof Object);      //true
alert(instance instanceof SuperType);   //true
alert(instance instanceof SubType);     //true

我们可以说 instance 是 Object 、SuperType、 SubType 的实例。使用 isPrototypeOf() 也会返回同样的结果。

alert(Object.prototype.isPrototypeOf(instance));
alert(SuperType.prototype.isPrototypeOf(instance));
alert(SubType.prototype.isPrototypeOf(instance));

还有一点,在替换原型对象的时候,会导致已有的实例跟后面更换实例之间关系的断裂,所以实例要在替换原型对象之后创建。想要添加或修改方法也要放在替换原型对象之后,否则替换原型对象后可能会产生影响。

这还有一个问题。就是在创建子类型时,不能向超类的构造函数传递参数。或者说,没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。因为此时子类型的原型对象指向的是超类型的一个实例,一要一改动这个实例,所有的子类型实例对象都会受影响。这样就相当于要修改一个超类的属性就会影响所有子类型的实例。

所以一般情况下,也很少会单独使用原型链。

借用构造函数

为解决上面说到的修改超类实例(也就是子类型的原型)导致所有子类型的实例对象都受影响,开发人员使用一种叫 借用构造函数的技术(有时也叫 伪造对象 或者 经典继承)。这种技术的基本思想很简单,就是在子类型的构造函数内调用超类型的构造函数,使子类型也拥有和超类型同名的属性来覆盖超类型的属性。因为函数 只不过是在特定环境中执行代码的对象,使用 apply() , call() 方法可以在新创建的对象上执行构造函数。

function SuperType(){
    this.colors = ['red','blue','green'];
}
function SubType(){
    //调用 父类的 构造函数 来实现继承
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push('black');
alert(instance1.colors);        //'red,blue,green,blcak'

var instance2 = new SubType();
alert(instance2.colors);        //'red,blue,green'

调用超类型的构造函数就是借用的过程,通过超类型的构造函数来产生相同的属性,以此覆盖掉超类型的实例(对子类型来说是原型对象)的属性。

由于我们手动调用超类型的构造函数,所以我们甚至可以往里面传参数,产生出我们想要的属性值。这里就不赘述了,直接怼就行。

这样是挺好的,但是既然只使用了构造函数来创建对象,那还是会遇到构造模式的坑就是 方法都在构造函数内定义,函数的复用(每个实例对象都会拥有相同的方法,但是每个都是不同的对象,没法复用)就成问题了。而且由于超类的原型对于子类型来说是不可见的(由于没有使用原型链,只是调用超类型的构造函数来产生相同的属性),所以后面的继承都只能使用构造函数模式了。

所以借用构造函数也是比较少用的。

组合继承

没错,组合继承又来了! 既然单纯使用原型链模式或者单纯使用构造函数都有问题,那就组合吧。

组合继承有时候也称 为经典继承。主要思想就是使用原型链来实现对原型属性和方法的继承,使用构造函数来实现对实例属性的继承。

function SuperType(name){
    this.name = name;
    this.colors = ['red','blue','green'];
}

SuperType.prototype.sayName = function(){
    alert(this.name);
}

function SubType(name,age){
    //继承属性
    SuperType.call(this,name)
    
    this.age = age;
}

//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
}

var instance1 = new SubType('Nicholas',29);
instance1.colors.push('black');
alert(instance1.colors);        //'red,blue,green,black'
instance1.sayName();            //'nicholas'
instance1.sayAge();             // 29

var instance2 = new SubType('greg',27);
alert(instance2.colors);        //'red,blue,green,black'
instance2.sayName();            //'greg'
instance2.sayAge();             //27

这样避免了单独使用的缺陷,融合了他们的有点,成为 JS 中最常用的继承模式。而且,instanceof 和 isPrototypeOf() 也能够识别。

原型式继承

这种方法并没有使用严格意义上的构造函数。思路是通过借助原型可以基于已有的对象创建对象,同时还不必因此创建自定义类型。

function Person(){
    
}

var o = new Person();

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

var o1 = object(o);
console.log(o1 instanceof Person); //true

这是一种简单的继承,把传进来的对象作为原型。并使用 new 创建出对象。

在 ES 5 中,新增了 Object.create() 方法规范了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选)一个为新对象定义额外属性的对象。在传入一个参数的情况下, Object.create() 与 object() 方法行为相同。

寄生式继承

寄生式继承和原型继承紧密相关,思路和工厂模式类似。

function createAnother(original){
    var clone = object(original);       // 创建对象
    clone.sayHi = function(){           // 增强对象
        alert('hi');
    }
    return clone;
}

但是有一个和构造函数模式一样的问题。就是函数服用的问题。

寄生组合式继承

我们前面说过 组合继承的最常用的 继承模式,但是也存在缺点,就是会调用两次超类型的构造函数。一次是在创建子类型原型的时候,一次是在子类型构造函数内部。组合继承模式就是调多一次超类型的构造函数,创建出与超类型相同的属性来覆盖超类型的属性。

但现在,我们找到了解决方法。没错,就是 寄生组合式继承!

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后将结果指定给子类型的原型。

function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype);    //创建对象
    prototype.constructor = subType;                //增强对象
    subtype.prototype = prototype;                  //指定对象
}

完整的代码:

function SuperType(name){
    this.name = name;
    this.colors = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
}
function SubType(name,age){
    SuperType.call(this.name);
    this.age=age;
}
inheritPrototype(SubType,SuperType);

SubType.prototype.sayAge = function(){
    alert(this.age);
}