js学习笔记:对象——继承

伪“类“继承

  • 每个构造函数有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的指针。
  • 如果让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么这个关系将层层递进下去,构成了实例与原型的链条,此为原型链

实现原型链的基本模式:

function SuperType(){
    this.property = 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();
instance.getSuperValue(); //true
  • 继承实现的本质是令SubType的原型对象等于SuperType的实例,即重写了原型对象代之以另一个类型的实例。原来存在于SuperType实例中的所有属性和方法,都存在于SubType.prototype中。
  • 在此基础上,又为SubType.prototype添加一个新方法

这里写图片描述

  • 最终结果就是SubType的实例指向SubType的原型,SubType的原型又指向SuperType的原型。

  • getSuperValue()仍然在SuperType.prototype中,但property属性是在SubType.prototype中,因为property是一个实例属性,而getSuperValue则是一个原型方法。

  • 此时instance.constructor指向的是SuperType。因为SubType的原型被重写了,instance指向SubType的原型,SubType的原型再指向SuperType的原型,而SuperType原型的constructor指针指向SuperType。


实现原型链,本质上是扩展了之前的原型搜索机制。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。
如,调用instance.getSuperValue()会经历三个步骤:

  • 搜索实例
  • 搜索SubType.prototype
  • 搜索SuperType.prototype

在找不到属性或方法时,搜索过程要层层递进一直到原型链末端才停下来。

如果我们尝试去获取对象的某个属性值,但该对象没有此属性名,那么js会试着从原型对象中获取属性值。如果那个原型对象也没有该属性,那么再从它的原型中寻找,以此类推,直到该过程最后到达终点Object.prototype。如果想要的属性完全不存在于原型链中,那么结果就是undefined。
这个过程称为委托

继承Object

  • 所有引用类型都默认继承于Object,这个继承也是通过原型链实现的
  • 所有函数的默认原型都是Object的实例,因此所有原型对象都会默认包含一个指向Object.prototype的指针。这也是所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因。

因此上面的例子其实是SubType继承了SuperType,SuperType继承了Object。当调用instance.toString()时,实际上是调用了保存在Object.prototype中的方法。

确定原型和实例的关系

  • instanceof 操作符:只要用来测试实例与原型链中出现过的构造函数,就会返回true
instance instanceof Object;  //true
instance instanceof SuperType;  //true
instance instanceof SubType;  //true

由于原型链的关系,可以说instance 是Object、SuperType、SubType中任何一个类型的实例,因此都会返回true

  • isPrototypeOf():同样,只要是原型链中出现过的原型,此方法都会返回true
Object.prototype.isPrototypeOf(instance);  //true
SuperType.prototype.isPrototypeOf(instance);  //true
SubType.prototype.isPrototypeOf(instance);  //true

谨慎地定义方法

  • 子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法,不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。不然先给原型添加方法,而后又将原型重新赋了值(等于超类的实例),就等于没有添加方法。

  • 给原型添加方法也不能使用对象字面量,不然又会重写原型,断掉继承的原型链

function SuperType(){
    this.property = 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;
    }
    someOtherMethod:function(){
        return false;
    }
}

var instance = new SubType();
instance.getSuperValue(); //出错!!

以上代码展示了刚刚把SuperType的实例赋给原型,就又将原型替换成了一个对象字面量,原型链已经被切断,SubType和SuperType没有任何关系了。

原型链的问题

包含引用类型值(如数组)属性的原型,会有所有实例共享属性的问题。本来可以利用在构造函数中定义属性来解决问题,但通过原型实现继承时,原型会变成另一个类型的实例,那么原先的实例属性就变成了原型属性了……

function SuperType(){
    this.colors = ["red","blue","green"];
}
fucntion SubType(){}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
instance1.colors; //"red","blue","green","black"

var instance2 = new SubType();
instance2.colors;  //"red","blue","green","black"
  • SuperType构造函数定义了一个包含数组(引用类型值)的属性colors,SuperType的每个实例都会有各自包含自己数组的colors属性。

  • SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType的一个实例,它也拥有了自己的colos属性,就跟专门创建了SubType.prototype.colors一样。结果就是所有SubType的实例都会共享这一个colors属性。


原型链的第二个问题是,在创建子类型的实例时,不能向超类型的构造函数中传递参数。

因此很少单独使用原型链。


借用构造函数

为了解决原型中包含引用类型值所带来的问题,使用借用构造函数技术(伪造对象/经典继承)

基本思想:在子类型构造函数的内部调用超类型构造函数。

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

//继承了SuperType
fucntion SubType(){
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
instance1.colors; //"red","blue","green","black"

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

SuperType.call(this);这一句实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数,这样就会在新SubType对象上执行SuperType()函数中定义的对象初始化代码,这样每个SubType实例就会有自己的colors属性了。

传递参数

相对于原型链而言,借用构造函数可以在子类型构造函数中向超类型构造函数传递参数

function SuperType(){
    this.name = name;
}

//继承了SuperType,同时还传递了参数
fucntion SubType(){
    SuperType.call(this"nicholas");

    //实例属性
    this.age = 29;
}

var instance = new SubType();
instance.name; //"nicholas"
instance.age;  //29

借用构造函数的问题

和构造函数模式的问题一样,属性和方法都在构造函数中定义,共享方法无从谈起。而且在超类型的原型中定义的方法,子类型也是看不见的。因此也很少单独使用借用构造函数。


组合继承

将原型链和借用构造函数的技术组合到一块。

  • 使用原型链实现对原型属性和方法的继承
  • 通过借用构造函数实现对实例属性的继承

这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性

function SuperType(){
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
}

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

    this.age = age;
}

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

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

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

这样,两个子类型的实例既分别拥有自己的属性(包括colors属性),又可以使用相同的方法了。

组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为最常用的继承模式。而且,instanceof和isPrototypeOf()也能够成功识别基于组合继承创建的对象。


原型式继承

前面那些原型+构造函数式的继承,其实是模仿其他面向对象语言继承的一种“伪类”实现。但在js里,其实也有更好的选择,因为继承实际上就是对象之间的联系而已。

在一个纯粹的原型模式中,我们将摒弃类而专注于对象。也就是一个新对象可以继承一个旧对象的属性。

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

在函数object内部:

  • 先创建了一个临时性的构造函数F()
  • 将传入的对象作为这个构造函数的原型
  • 返回这个临时类型的一个新实例

其实本质上object函数对传入的对象进行了一次浅复制。

var person = {
    name:"nicholas",
    friends:["shelby","court","van"]
}

var anotherPerson = object(person);
anotherPerson.name = "greg";
anotherPerson.friends.push("rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "linda";
yetAnotherPerson.friends.push("barbie");

person.friends; //"shelby","court","van","rob","barbie"

这种原型式继承,必须有一个对象可以作为另一个对象的基础。这个例子中,object返回的对象以person为原型,所以它的原型中就包含一个基本类型值属性name和引用类型值属性friends。这就意味着person.friends不仅属于person,也会被所有实例(anotherPerson和yetAnotherPerson)共享。

Object.create()

ES5新增的这个方法规范了原型式继承,用于创建一个使用原对象作为原型的新对象。
接收两个参数:

  • 用作原型的对象
  • 为新对象定义额外属性的对象(可选)

在传入一个参数的情况下,Object.create()与object()方法的行为相同。

var person = {
    name:"nicholas",
    friends:["shelby","court","van"]
}

var anotherPerson = Object.create(person);
anotherPerson.name = "greg";
anotherPerson.friends.push("rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "linda";
yetAnotherPerson.friends.push("barbie");

person.friends; //"shelby","court","van","rob","barbie"

Object.create()第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

var person = {
    name:"nicholas",
    friends:["shelby","court","van"]
}

var anotherPerson = Object.create(person,{
    name:{
        value:"greg"
    }
});

anotherPerson.name; //"Greg"

在没有必要兴师动众地创建构造函数,而是只想让一个对象与另一个对象保持类似的情况下,原型式继承就可以胜任。

但是,包含引用类型值的属性始终都会共享相应的值。

当然,Object.create()也可以与构造函数继承组合使用:

//Shape - superclass
function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.move = function(x, y) {
    this.x += x;
    this.y += y;
    console.info("Shape moved.");
};

// Rectangle - subclass
function Rectangle() {
  Shape.call(this); //call super constructor.
}

Rectangle.prototype = Object.create(Shape.prototype);

var rect = new Rectangle();

rect instanceof Rectangle //true.
rect instanceof Shape //true.

rect.move(1, 1); //Outputs, "Shape moved."

其中,Rectangle.prototype = Object.create(Shape.prototype);这句代码的后半部分返回一个以Shape.prototype为原型的新对象,把这个对象赋值给Rectangle.prototype,即Rectangle.prototype的原型为Shape.prototype


注意!以下两种方式不太可取:

Rectangle.prototype = Shape.prototype;

Rectangle.prototype = new Shape();
  • 第一种方式只是令Rectangle的原型直接引用Shape.prototype,而并不是联系到一起,除非你只想使用Shape对象而不使用Rectangle对象,否则这样做没有什么意义。

  • 第二中方式其实是上面提到过的继承方式:
    Object.create()与传统继承中Rectangle.prototype = new Shape()的方式有异曲同工之妙,都是使子类原型指向父类原型,但是传统方式会使Rectangle.prototype拥有Shape的实例属性:


//采用Object.create(),只是让子类的原型指向父类的原型,而不会使子类原型获得父类的实例属性
Rectangle.prototype.x;  //undefined

//改用传统继承方式
Rectangle.prototype = new Shape();
//Rectangle.prototype为Shape的一个实例,拥有实例属性
Rectangle.prototype.x;  //0

Object.create()只是单纯地指定一个新对象的原型,而如果这个新对象是另一个原型对象,那么就形成了这个原型对象和另一个原型的关联,形成了继承关系。

Object.create(null) 会创建一个不拥有原型链的对象,这样特殊的空对象完全不会受原型链的干扰,因此非常适合用来存储数据。

在ES5之前的环境中如果想实现相同的功能的话,就只能借助之前在原型式继承中定义的函数来部分实现:

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

寄生式继承

寄生式继承与原型式继承紧密相关。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装过程的函数,该函数在内部以某种方式来增强对象。

function createAnother(original){
    var clone = object(original); //通过调用函数创建一个新对象
    clone.sayHi() = function(){  //以某种方式来增强这个对象
        alert("hi");
    }
    return clone;   //返回这个对象
}
  • createAnother()函数接受了一个作为新对象基础的对象作为参数
  • 把这个对象传递给object函数,将返回的结果赋给clone
  • 为clone对象添加新方法
  • 返回clone对象

可以如下这么使用createAnother函数:

var person = {
    name:"nicholas",
    friends:["shelby","court","van"]
}

var anotherPerson = createAnother(person);
anotherPerson.sayHi();

我觉得其实和原型式继承没什么不同,只是把新建对象和为对象添加属性或方法的函数封装在一起了。

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。其中的object函数也不是必须的,任何能够返回新对象的函数都适用于此模式。

但是使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似。

寄生组合式继承

组合继承最大的问题是:无论什么情况下,都会调用两次超类构造函数:

  • 在创建子类型原型时
  • 在子类型构造函数内部
function SuperType(){
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
}

fucntion SubType(name,age){
    //继承属性
    SuperType.call(this,name); //第二次调用SuperType()

    this.age = age;
}

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

var instance1 = new SubType("nicholas",29);
  • 在第一次调用SuperType构造函数时,SubType.prototype会得到两个属性:name和colors;它们现在是SubType的原型属性

这里写图片描述

  • 当调用SubType构造函数创建新对象时,会第二次调用SuperType时,这时又在新对象上创建了两个实例属性:name和colors。于是,这两个属性就屏蔽了原型中的两个同名属性。

这里写图片描述

因此,其实有两组name和colors属性,一组在实例上,一组在SubType原型上。这就是调用两次SuperType构造函数的结果。

解决方法就是:寄生组合式继承。

寄生组合式继承的思路是:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
意思是,不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype);  //创建对象
    prototype.constructor = subType;   //增强对象
    subType.prototype = prototype;  //指定对象
}
  • 创建超类型原型的一个副本
  • 为创建的副本添加constructor属性,弥补因重写原型而失去的默认constructor属性
  • 将新创建的对象赋给子类型的原型

这样就可以替换之前例子中为子类型原型赋值的语句了。

function SuperType(){
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
}

fucntion SubType(name,age){
    //继承属性
    SuperType.call(this,name); //第二次调用SuperType()
    this.age = age;
}

//继承方法
inheritPrototype(SubType,SuperType);

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

这个例子的高效率体现在只调用了一次SuperType构造函数,因此避免了在SubType.prototype上创建不必要的、多余的属性。且与此同时,原型链保持不变,也还能正常使用instanceof和isPrototypeof()。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值