记录-对象有哪些继承方式

Posted 林恒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记录-对象有哪些继承方式相关的知识,希望对你有一定的参考价值。

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

1. 原型链

温故而知新:

构造函数、原型和实例的关系:
  每个构造函数都有一个原型对象,原型有一个属性指回构造函数,实例有一个内部指针指向原型。

思考:如果原型是另一个类型的实例呢?
  那就意味着这个原型本身有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本思想。
实现原型链涉及如下代码模式:

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;


let instance = new SubType()
console.log(instance.getSuperValue()); //true

 以上的代码定义了两个类型:SuperType和SubType。这两个类型分别定义了一个属性和方法。两个类型的主要区别是:SubType通过创建SuperType的实例并将其赋值给自己的原型SubType.prototype实现了对SuperType的继承。这个赋值重写了SubType最初的原型,将其替换成SuperType的实例。这意味着SuperType实例可以访问的所有属性和方法也会存在于SubType.prototype。这样实现继承后,紧接着又给SuperType的实例添加了一个新的方法。最后又创建了SubType的实例并调用了它继承的getSuperValue方法。

默认原型

  默认情况下,所有引用类型都继承自Object,这也是通过原型链实现的。任何函数默认的原型都是一个Object的实例,这意味着这个实例有一个内部指针指向Object.prototype。这也是为什么自定义类型能够继承包括toString()、valueOf在内部的所有默认方法的原因。
  SubType继承SuperType,而SuperType继承Object。在调用instance.toString时,实际上调用的是保存在Object.prototype上的方法。

原型与继承的关系

  原型与继承的关系可以通过两种方式来确定:

使用instanceof操作符:

如果一个实例的原型中出现过相应的构造函数,则instanceof返回true

    console.log(instance instanceof  Object); //true
    console.log(instance instanceof  SuperType); //true
    console.log(instance instanceof  SubType); //true

从技术上讲,instance是Object、SuperType、SubType的实例,因为instance的原型链中包含这些构造函数的原型。结果就是instanceof对所有这些构造函数都返回true。

使用isPrototypeOf()方法

原型链中的每个原型都可以调用这个方法,这要原型链中包含这个原型,这个方法就会返回true

    console.log(Object.prototype.isPrototypeOf(instance)); //true
    console.log(SuperType.prototype.isPrototypeOf(instance)); //true
    console.log(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;


// 覆盖已有的方法
SubType.prototype.getSuperValue = function()
    return false;


let instance = new SubType()
console.log(instance.getSuperValue()); //false

以上代码新增两个方法。第一个方法getSubValue()是SubType的新方法。而第二个方法getSuperValue()是原型链上已经存在但在这里被遮蔽的方法。;后面在SubType实例上调用getSuperValue()时调用的是这个方法。而SuperValue的实例仍然会调用最初的方法。重点在于上述两个方法都是在把原型赋值为SuperType的实例之后定义的。
  另一个要理解的重点,以对象字面量方式创建原型方法,会破环之前的原型链。因为这样相当于重写原型链。

   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()
            return this.subproperty;
        ,
        someOtherMethod()
            return false;
        
    

    let instance = new SubType()
    console.log(instance.getSuperValue()); //instance.getSuperValue is not a function

子类的原型在被赋值为SuperType的实例后,又被一个对象字面量覆盖了。覆盖后的原型是一个Object的实例,而不再是SuperType的实例。因此,之前的原型链就断了。SubType和SuperType之间也没有关系了。

原型链的额问题

原先的实例属性变成了原型属性
  原型包含的引用值会在所有实例间共享,在使用原型实现继承时,原型实际上变成了另一个类型的实例。

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

    
    // 继承SuperType
    SubType.prototype = new SuperType()

    let instance1  = new SubType()
    instance1.colors.push("black");
    console.log(instance1.colors); //["red", "blue", "green", "black"]

    let instance2 = new SubType()
    console.log(instance2.colors); //["red", "blue", "green", "black"]

SuperType构造函数定义了一个colors属性,其中包含一个数组(引用值)。每个SuperType的实例都会有自己的colors属性,包含自己的数组。但是当SubType通过原型继承SuperType后,SubType.prototype变成了SuperType的一个实例,因而也获得了自己的colors属性。这类似于创建了SubType.prototype.colors属性。最终结果是,SubType的所有实例都会共享这个colors属性。这一点通过instance1.colors上的属性也能反应到instance2.colors上就可以看出。

子类型在实例化时不能给夫类型的构造函数传参
  我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数。再加上原型中包含引用值的问题,就导致原型链基本不会被单独使用。

2. 盗用构造函数

  盗用构造函数是用来解决原型包含引用值导致的继承问题。基本思路:在子类构造函数中调用父类构造函数。因为函数是在特定上下文中执行代码的简单对象,所以可以使用apply()和call()方法以新创建的对象上下文执行构造函数。

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


function SubType()
    // 继承SuperType
    /* *********** */
    SuperType.call(this);
    /* *********** */


let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]

let instance2 = new SubType()
console.log(instance2.colors); //["red", "blue", "green"]

以上代码展示了盗用构造函数的调用。通过使用call()或者apply()方法,SuperType构造函数在为SubType的实例创建的新对象的上下文中执行了。这相当于新的SubType对象上运行了SuperType函数中的所有初始化代码。结果就是每个实例都会有自己的colors属性。

1. 优点

  可以在子类构造函数中向父类构造函数传参

function SuperType(name)
    this.name = name;


function SubType()
    // 继承SuperType并传参
    SuperType.call(this,"Nicholas");
    // 实例属性
    this.age = 29


let instance = new SubType()
console.log(instance.name); //Nicholas
console.log(instance.age); //29

 SuperType构造函数接受一个参数name,然后将它赋值给一个属性。在SubType构造函数中调用SuperType构造函数时传入这个参数,实际上会在SubType的实例上定义name属性。为确保SuperType构造函数不会覆盖SubType定义的属性,可以在调用父类构造函数之后再给子类实例添加额外的属性。

2. 缺点

必须在构造函数中定义方法,因此函数不能重用。
子类不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。
由于存在以上问题,所以盗用构造函数基本上也不能单独使用。

3. 组合继承  

组合继承也叫伪经典继承,综合了原型链和盗用构造函数,将两者的优点集中起来。基本思路是:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

/* 组合继承 */

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

SuperType.prototype.sayName = function()
    console.log(this.name);

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


// 继承方法  
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function()
    console.log(this.age);


let instance1 = new SubType("Nicholas",29)
instance1.colors.push("blcak");
console.log(instance1.colors); //["red", "blue", "green", "blcak"]
instance1.sayName() //Nicholas
instance1.sayAge(); //29

let instance2 = new SubType("Greg",27)
console.log(instance2.colors); //["red", "blue", "green"]
instance2.sayName(); //Greg
instance2.sayAge(); //27

SuperType构造函数定义了两个属性name和colors,而它的原型上也定义了一个方法叫sayName()。SubType构造函数调用了SuperType构造函数,传入name参数,然后又定义了自己的属性age。此外,SubType.prototype也被赋值为SuperType的实例。原型赋值之后,又在这个原型上添加了新方法sayAge()。这样就可以创建两个SubType实例,让这两个实例都有自己的属性,包括colors,同时还共享相同的方法。
  组合继承弥补了原型链和盗用构造函数的不足,是JavaScript中使用最多的继承模式。而且组合继承也保留了instanceof操作符和isPrototypeOf()方法识别合成对象的能力。
缺点:
  父类构造函数会被调用两次,一次是在创建子类原型时调用,一次是在子类构造函数中调用。
  本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。

4. 原型式继承

  2006年Douglas Crockford的文章:《JavaScript中的原型式继承》中介绍了一种不涉及严格意义上构造函数的继承方法。作者的出发点是即使不自定义类型也可以通过原型实现对象之间的信息共享。文章给出了一个函数:

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

  这个object方法会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。本质上,object()是对传入对象执行了一次浅复制。

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


let person = 
    name:"Nicholas",
    friends:["Shelby",\'Court\',\'Van\']
;
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = object(person);
yetAnotherPerson.name = \'Linda\';
yetAnotherPerson.friends.push("Barbie")

console.log(person.friends); //["Shelby", "Court", "Van", "Rob", "Barbie"]

ECMAScript通过增加Object.create()方法将原型式继承的概念规范化。这个方法接受两个参数:作为新对象原型的对象,给新对象定义额外属性的对象(可选)。在只有一个参数时,Object.create()与这里的object()方法效果类似。

let person = 
    name:"Nicholas",
    friends:["Shelby",\'Court\',\'Van\']

let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda"
yetAnotherPerson.friends.push("Barbie")

console.log(person.friends); //["Shelby", "Court", "Van", "Rob", "Barbie"]

Object.create()的第二个参数与Object.defineProperties()的第二个参数一样:每个新增参数属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。

let person = 
    name:"Nicholas",
    friends:["Shelby",\'Court\',\'Van\']

let anotherPerson = Object.create(person,
    name:
        value:\'Greg\'
    
);
console.log(anotherPerson.name); //Greg

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式时一样的。

5. 寄生式继承

  寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。基本的寄生继承模式如下:

function createAnother(original)
    let clone = object(original);   //通过调用函数创建一个新对象
    clone.sayHi = function() //以某种方式增强这个对象
        console.log("HI");
    ;
    return clone;  //返沪这个对象

createAnother()函数接受一个参数,就是新对象的基准对象。这个对象original会被传给object()函数,然后将返回的新对象赋值给clone对象添加一个新方法sayHi()。最后返回这个对象。

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


function createAnother(original)
    let clone = object(original);   //通过调用函数创建一个新对象
    clone.sayHi = function() //以某种方式增强这个对象
        console.log("Hi");
    ;
    return clone;  //返沪这个对象


let person = 
    name:"Nicholas",
    friends:["Shelby",\'Court\',\'Van\']


let anotherPerson = createAnother(person);
anotherPerson.sayHi(); //Hi
 

 这个例子基于person对象返回了一个新对象。新返回的anotherPerson对象具有person的所有属性和方法,还有一个新方法叫sayHi()
  寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。
==注意:==通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。

6. 寄生式组合继承

  通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而实取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function inheritPrototype(subType,superType)
    let prototype = object(superType.prototype); //创建对象
    prototype.constructor = subType; //增强对象
    subType.prototype = prototype; //赋值对象

这个inheritPrototype()函数实现了寄生式组合继承的核心逻辑。这个函数接受到两个参数:子类构造函数和父类构造函数。这个函数内部,第一步是创建一个父类原型的一个副本。然后,给返回的prototype对象设置constructor属性,解决由于重写原型导致默认constructor丢失的问题。最后将新创建的对象赋值给子类型的原型。调用inheritPrototype()就可以实现前面例子中的子类型原型赋值:

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


function inheritPrototype(subType,superType)
    let 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()
    console.log(this.name);


function SubType(name,age)
    SuperType.call(this,name);
    this.age = age;


inheritPrototype(SubType,SuperType);

SubType.prototype.sayAge = function()
    console.log(this.age);

这里只调用了一次SuperType构造函数,避免了SubType.prototype上不必要也用不到的属性,因此可以说这个例子效率更高。而且,原型链仍然保持不变,因此,instanceof操作符和isPrototypeOf()方法正常有效。寄生式组合继承可以说是引用类型继承的最佳方式。

本文转载于:

https://blog.csdn.net/qq_39200185/article/details/115134869

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

面向对象的特征有哪些方面

面向对象是一种程序的设计方法,或者说它是一种程序设计范型,其基本思想是使用对象,类,继承,封装,消息等基本概念来进行程序设计。

它是从现实世界中客观存在的事物(即对象)出发来构造软件系统,并在系统构造中尽可能运用人类的自然思维方式,强调直接以问题域(现实世界)中的事物为中心来思考问题,认识问题,并根据这些事物的本质特点,把它们抽象地表示为系统中的对象,作为系统的基本构成单位(而不是用一些与现实世界中的事物相关比较远,并且没有对应关系的其它概念来构造系统)。这可以使系统直接地映射问题域,保持问题域中事物及其相互关系的本来面貌。

它可以有不同层次的理解:

从世界观的角度可以认为:面向对象的基本哲学是认为世界是由各种各样具有自己的运动规律和内部状态的对象所组成的;不同对象之间的相互作用和通讯构成了完整的现实世界。因此,人们应当按照现实世界这个本来面貌来理解世界,直接通过对象及其相互关系来反映世界。这样建立起来的系统才能符合现实世界的本来面目。

从方法学的角度可以认为:面向对象的方法是面向对象的世界观在开发方法中的直接运用。它强调系统的结构应该直接与现实世界的结构相对应,应该围绕现实世界中的对象来构造系统,而不是围绕功能来构造系统。

从程序设计的角度来看,面向对象的程序设计语言必须有描述对象及其相互之间关系的语言成分。这些程序设计语言可以归纳为以下几类:系统中一切皆为对象;对象是属性及其操作的封装体;对象可按其性质划分为类,对象成为类的实例;实例关系和继承关系是对象之间的静态关系;消息传递是对象之间动态联系的唯一形式,也是计算的唯一形式;方法是消息的序列。

面向对象的编程语言有封装、继承 、抽象、多态等4个主要的特征。

  • 1.封装:

封装是保证软件部件具有优良的模块性的基础,封装的目标就是要实现软件部件的“高内聚、低耦合”,防止程序相互依赖性而带来的变动影响。在面向对象的编程语言中,对象是封装的最基本单位,面向对象的封装比传统语言的封装更为清晰、更为有力。面向对象的封装就是把描述一个对象的属性和行为的代码封装在一个“模块”中,也就是一个类中,属性用变量定义,行为用方法进行定义,方法可以直接访问同一个对象中的属性。通常情况下,只要记住让变量和访问这个变量的方法放在一起,将一个类中的成员变量全部定义成私有的,只有这个类自己的方法才可以访问到这些成员变量,这就基本上实现对象的封装,就很容易找出要分配到这个类上的方法了,就基本上算是会面向对象的编程了。把握一个原则:把对同一事物进行操作的方法和相关的方法放在同一个类中,把方法和它操作的数据放在同一个类中。

例如,人要在黑板上画圆,这一共涉及三个对象:人、黑板、圆,画圆的方法要分配给哪个对象呢?由于画圆需要使用到圆心和半径,圆心和半径显然是圆的属性,如果将它们在类中定义成了私有的成员变量,那么,画圆的方法必须分配给圆,它才能访问到圆心和半径这两个属性,人以后只是调用圆的画圆方法、表示给圆发给消息而已,画圆这个方法不应该分配在人这个对象上,这就是面向对象的封装性,即将对象封装成一个高度自治和相对封闭的个体,对象状态(属性)由这个对象自己的行为(方法)来读取和改变。一个更便于理解的例子就是,司机将火车刹住了,刹车的动作是分配给司机,还是分配给火车,显然,应该分配给火车,因为司机自身是不可能有那么大的力气将一个火车给停下来的,只有火车自己才能完成这一动作,火车需要调用内部的离合器和刹车片等多个器件协作才能完成刹车这个动作,司机刹车的过程只是给火车发了一个消息,通知火车要执行刹车动作而已。

  • 2.抽象:

抽象就是找出一些事物的相似和共性之处,然后将这些事物归为一个类,这个类只考虑这些事物的相似和共性之处,并且会忽略与当前主题和目标无关的那些方面,将注意力集中在与当前目标有关的方面。例如,看到一只蚂蚁和大象,你能够想象出它们的相同之处,那就是抽象。抽象包括行为抽象和状态抽象两个方面。例如,定义一个Person类,如下:


class Person{

        String name;

        int age;

}

人本来是很复杂的事物,有很多方面,但因为当前系统只需要了解人的姓名和年龄,所以上面定义的类中只包含姓名和年龄这两个属性,这就是一种抽像,使用抽象可以避免考虑一些与目标无关的细节。我对抽象的理解就是不要用显微镜去看一个事物的所有方面,这样涉及的内容就太多了,而是要善于划分问题的边界,当前系统需要什么,就只考虑什么。

  • 3.继承:

在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并可以加入若干新的内容,或修改原来的方法使之更适合特殊的需要,这就是继承。继承是子类自动共享父类数据和方法的机制,这是类之间的一种关系,提高了软件的可重用性和可扩展性。

  • 4.多态:

多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。多态性增强了软件的灵活性和扩展性。例如,下面代码中的UserDao是一个接口,它定义引用变量userDao指向的实例对象由daofactory.getDao()在执行的时候返回,有时候指向的是UserJdbcDao这个实现,有时候指向的是UserHibernateDao这个实现,这样,不用修改源代码,就可以改变userDao指向的具体类实现,从而导致userDao.insertUser()方法调用的具体代码也随之改变,即有时候调用的是UserJdbcDao的insertUser方法,有时候调用的是UserHibernateDao的insertUser方法:


UserDao userDao = daofactory.getDao();  

userDao.insertUser(user);

比喻:人吃饭,你看到的是左手,还是右手?

以上是关于记录-对象有哪些继承方式的主要内容,如果未能解决你的问题,请参考以下文章

面向对象的特征有哪些?

《面向对象程序设计》高手进~~~~~~~~~~~~!!

Spring学习

什么是多态?

面向对象之继承

面向对象的特征有哪些方面