zoukankan      html  css  js  c++  java
  • JavaScript面向对象程序设计之继承(一)

    JavaScript面向对象程序设计之继承(一)

    1. 原型链式继承

    1.1 原型模式

    原型模式是JavaScript中创建对象的一种最常见的方式。JavaScript是一种弱类型的语言,没有类的概念,也不是一种面向对象的语言。但是,在JavaScript中,借助函数的原型(也就是prototype)可以实现类的功能。

    使用原型模式创建对象的基本做法如下:

    function Person (name) {
        this.name = name // 私有属性
    }
    // 公共方法
    Person.prototype.sayName = function () {
        console.log(this.name)
    }
    
    // 创建实例
    var personA = new Person('A')
    console.log(personA.name) // A
    personA.sayName() // A
    
    var personB = new Person('B')
    console.log(personB.name) // B
    personB.sayName() // B
    

    在以上代码中,两个实例既拥有各自不同的属性 name, 又共享了 公有的方法 sayName(),这样就实现了类似于强类型语言中类的概念。

    这种功能的实现,就得益于构造函数的prototype属性。下图展示了构造函数、原型、实例三者之间的关系。

    每个构造函数都有一个属性 prototype,prototype属性指向一个对象,该对象被构造函数的所有实例所共享,我们称这个对象为构造函数的原型

    原型与构造函数之间通过prototype属性和constructor属性相互联系。

    实例与原型之间通过一个 _proto_属性(是浏览器中存在的一个虚拟的属性,各个浏览器的实现不同,谷歌中为_proto_)产生联系。

    一个构造函数创建的不同的实例都可以通过 _proto_属性 访问到它的原型,这就是为什么上面的示例中两个示例可以访问到共同的 sayName() 方法的原因。

    1.2 原型链式继承

    1.2.1 原型链与原型链式继承

    构造函数的原型并不是固定不变的,我们可以人为的切断构造函数与其原型之间的联系,也可以给构造函数指定自定义的原型。

    function Person (name) {
        this.name = name
    }
    Person.prototype = {
        constructor: Person,
        sayName: function () {
            console.log(this.name + '-hello')
        }
    }
    var personA = new Person('A')
    personA.sayName() // A-hello
    
    

    上面的代码我们自己给Person构造函数指定了一个原型,并添加了一个sayName()的公共方法。

    原型链的实现就是基于原型可以重写这一基本前提。

    我们结合下图来说明什么是原型链,它与JavaScript中实现类之间的继承的关系。

    上图中,绿色线条所形成的链条即被我们称之为原型链。基于原型链,我们可以实现一种继承方式。

    以下是一段基于原型链实现继承代码示例,为了区别于Java等语言中的class关键字,这里以Type表示类型,同时以 Super表示父,以Sub表示子,将SuperType称之为超类型,将SubType称之为子类型。

    function SuperType () {
        this.superName = 'superName'
    }
    SuperType.prototype.sayName = function () {
        console.log(this.name)
    } 
    function SubType (subName) {
        this.name = subName || 'subName'
    }
    SubType.prototype = new SuperType()
    var subA = new SubType('A')
    subA.sayName() // A
    console.log(subA.superName) // superName
    

    以上代码中,我们并没有为SubType定义sayName() 方法,SubType中也没有superName属性,但SubType创建的示例subA却可以使用sayName()方法打印出 自己的name属性,同时可以访问到并非自身定义的 superName属性,这是因为我们将SuperType的一个实例指定为了SubType的原型,因此subA与new SuperType()与 SuperType.prototype之间就有了一条原型链,subA因此可以访问到链条能触及的所有属性和方法。

    这就是原型链实现继承的一个基本方式。

    1.2.2 原型链式继承的缺陷

    尽管,在理解了原型链后,原型链式的继承方式变得很好理解,使用也很简单。但是,原型链式的继承方式却并非完美的实现继承的解决方案。原型链式的继承也存在其自身的问题。让我们看一段代码:

    function SuperType () {
        this.element = ['A', 'B', 'C']
    }
    SuperType.prototype.printElement = function () {
        console.log(this.element)
    }
    function SubType () {
    }
    SubType.prototype = new SuperType()
    
    var subA = new SubType()
    subA.element.push('D')
    subA.printElement() // ["A", "B", "C", "D"]
    var subB = new SubType()
    subB.element.push('E')
    subB.printElement() // ["A,", "B", "C", "D","E"]
    

    让我们感到奇怪的是,两个不同的实例分别向element属性中各自添加不同元素后,subB中打印的element属性中竟然包含了subA添加的元素。

    这也很好理解,因为两个实例都继承自SuperType的同一个实例构成的原型,所以他们共享了超类型构造函数中的element属性,因此,理所应当的,前一个实例对element元素所作的更改会体现在后一个实例上。

    但是,这并不是我们想要的结果。

    我们知道,使用原型模式创建对象时,会把私有属性(方法)和共有属性(方法)分开定义,私有属性定义在构造函数中,公有属性定义在原型中。因此,显然,当我们使用原型链实现继承时,我们不仅继承了超类型实例的原型中的公有属性,也继承了其构造函数中定义的私有属性。并且本应该是私有的属性,却因为它成了子类型的原型,而变成了子类型的公有属性。

    这是一个令人头疼的问题。

    原型链式继承的另一个问题是,子类型的构造函数中,无法给超类型传递参数。这也局限了这种方式在实际开发中的应用。

    2. 借用构造函数

    2.1 借用构造函数实现继承

    为了解决原型链式继承所带来的问题,开发人员使用了一种新的技术,这种技术被称为借用构造函数的技术。其具体实现方法如下:

    function SuperType (name) {
        this.name = name || 'superName'
        this.element = ['A', 'B', 'C']
    }
    function SubType (name) {
        SuperType.apply(this, arguments) // SuperType.call(this, name)
    }
    
    var subA = new SubType('subA')
    subA.element.push('D') 
    console.log(subA.element) // ["A", "B", "C", "D"] 
    console.log(subA.name) // subA
    var subB = new SubType('subB')
    subB.element.push('E')
    console.log(subB.element) // ["A", "B", "C", "E"] 
    console.log(subB.name) // subB
    

    根据程序的执行现象,我们可以看到,SubType的两个实例,分别向element属性中添加了不同的元素,从打印结果发现,subA中添加的元素并没有反映到subB中,SubType的两个实例在继承了SuperType中的自有属性的同时,又各自保留了其属性的副本。

    另外,在SubType构造函数中,调用SuperType构造函数时,子类型可以给超类型传递参数。

    借用构造函数看似简单,却解决了原型链式继承中存在的两个让人头疼的问题。

    实际上,借用构造函数的思想与我们平时编码时常用的技巧 函数的提取 类似,下面我们通过一段代码来简要的讲解一下这个过程(以下代码部分为伪代码,在此不探讨其合理性):

    function SubType () {
        this.propertyA = 'propertyA'
        this.propertyB = 'propertyB'
        this.propertyC = 'propertyC'
        this.propertyD = 'propertyD'
        this.propertyE = 'propertyE'
    }
    

    以上是一个子类型的构造函数,其中有诸多属性,在许多其他的子类型中,这些属性也被需要。

    这跟函数提取的场景很类似,在一个函数中,某些代码具备一定的复用性,此时,我们很容易的想到,把这些代码提取到一个单独的函数中,此后只要有需要的函数,都可以复用这些代码。这一步可以简单实现为如下:

    function SubType () {
       SuperType()
    }
    function SuperType () {
        this.propertyA = 'propertyA'
        this.propertyB = 'propertyB'
        this.propertyC = 'propertyC'
        this.propertyD = 'propertyD'
        this.propertyE = 'propertyE'
    }
    

    SubType构造函数内部调用SuperType构造函数,将SuperType中的属性复用到SubType中。

    但是,如只是简单的提取,则会产生一个问题,SuperType中的this在SuperType被定义时,即已确定。SuperType被定义在全局作用域下,因此this指向全局作用域(一般是window)。简单的提取调用后,则SubType中实际上会变成这样:

    function SubType () {
        window.propertyA = 'propertyA'
        window.propertyB = 'propertyB'
        window.propertyC = 'propertyC'
        window.propertyD = 'propertyD'
        window.propertyE = 'propertyE'
    }
    

    当创建SubType的新实例时,通过调用相应的属性,会得到如下结果:

    function SubType () {
       SuperType()
    }
    function SuperType () {
        this.propertyA = 'propertyA'
        this.propertyB = 'propertyB'
        this.propertyC = 'propertyC'
        this.propertyD = 'propertyD'
        this.propertyE = 'propertyE'
    }
    
    var subA = new SubType()
    console.log(subA.propertyA) // undefined
    console.log(window.propertyA) // propertyA
    

    这与以上的描述一致。

    为了让SuperType在调用时,其包含的属性能够正确的复用到SubType中,只需要将SuperType的执行环境绑定到SubType上即可。使用apply()方法和call()方法都可以很容易实现这一步。上述实现就变为:

    function SubType () {
       SuperType.apply(this,arguments)
    }
    function SuperType () {
        this.propertyA = 'propertyA'
        this.propertyB = 'propertyB'
        this.propertyC = 'propertyC'
        this.propertyD = 'propertyD'
        this.propertyE = 'propertyE'
    }
    
    var subA = new SubType()
    console.log(subA.propertyA) // propertyA
    console.log(window.propertyA) // propertyA
    

    以上的过程或许不够准确,但的确可以帮助理解借用构造函数的实现思路。

    由以上的代码示例,我们可以看到,借用构造函数的的确确是解决了原型链链式继承方法的缺陷。

    • 每个实例都可以保持超类型中自有属性的私有性,每个子类实例中都可以保有超类型中自有属性的一个副本,子类实例之间对继承而来的自有属性的操作不会相互干扰;
    • 子类型的构造函数可以向超类型的构造函数中传递参数;

    2.2 借用构造函数的缺陷

    但是,借用构造函数中也存在着令人头疼的问题。

    细心的读者会发现,在以上的关于借用构造函数讲解示例中,竟没有出现一次公共方法的调用。没错,这正是问题所在。

    简单来说就是,我(借用构造函数)做不到啊。

    也许你会进行一些尝试,我给超类型的原型定义公共方法行不行呢?我直接在超类型构造函数上定义一个方法行不行呢?一起来看下面这两段代码:

    示例一:给超类型的原型定义公共方法

    function SuperType (name) {
        this.name = name || 'superName'
        this.element = ['A', 'B', 'C']
    }
    SuperType.prototype.sayName = function () {
        console.log(this.name)
    }
    function SubType (name) {
        SuperType.apply(this, arguments) // SuperType.call(this, name)
    }
    
    var subA = new SubType('subA')
    subA.sayName() // Uncaught TypeError: subA.sayName is not a function
    

    毫不吃惊,程序执行出错, subA.sayName不是一个function;

    借用构造函数的本质仅仅是将超类型中的属性复制一份到子类型中,并将其属性的执行环境绑定到子类型上,因此子类型在执行完超类型构造函数那一刻,子类型和超类型之间就切断了联系,子类型的实例又怎么可能访问到超类型的原型方法呢。

    实例二:直接在超类型构造函数上定义方法

    function SuperType (name) {
        this.name = name || 'superName'
        this.element = ['A', 'B', 'C']
        this.sayName = function () {
            console.log(this.name)
        }
    }
    function SubType (name) {
        SuperType.apply(this, arguments) // SuperType.call(this, name)
    }
    var subA = new SubType('subA')
    subA.sayName() // 'subA'
    var subB = new SubType('subB')
    subB.sayName() // 'subB'
    

    看似可行,实则???

    console.log(subA.sayName === subB.sayName) // false
    

    呃。。。。。

    虽然都实现了相同的功能,但两个方法并不是同一个方法。还是上面说的,借用构造函数仅仅只是复制了一份超类型中的属性和方法,这并不是复用,借用构造函数无法实现公共方法的复用。

    基于借用构造函数的以下两个缺陷:

    • 无法定义子类型可复用的公共方法;
    • 无法访问超类型的原型;

    借用构造函数在实际应用中很少单独使用。

    3. 组合继承

    3.1 组合继承

    虽然,原型链模式和借用构造函数模式都无法完美实现继承,但所幸二者的缺陷可以互补。自然而然的,一种相对完美的解决方案出现了,即组合继承。

    将原型链式继承和借用构造函数继承组合起来,使用原型链模式实现对超类型的公共属性和公共方法的继承,使用借用构造函数模式实现对超类型中自有属性的继承。这样,既通过在原型上定义方法实现了函数的复用,又能够保证每个子类实例都能保有一份超类型中的自有属性。

    组合继承的实现方法如下:

    function SuperType (name) {
        this.name = name || 'superName'
        this.element = ['A', 'B', 'C']
    }
    SuperType.prototype.sayName = function () {
        console.log(this.name)
    }
    SuperType.prototype.printElement = function () {
        console.log(this.element)
    }
    function SubType (name) {
        SuperType.apply(this, arguments)
    }
    SubType.prototype = new SuperType ()
    var subA = new SubType('subA')
    subA.element.push('subA')
    subA.sayName() // subA
    subA.printElement() // ["A", "B", "C", "subA"]
    var subB = new SubType('subB')
    subB.element.push('subB')
    subB.sayName() // subB
    subB.printElement() // ["A", "B", "C", "subB"]
    console.log(subA.sayName === subB.sayName) // true
    

    以上是一个组合继承的示例,根据打印结果可以看到,组合模式既可以复用超类型的公用方法,子类型实例中又可以保有各自的私有属性。

    组合继承避免了原型链式继承和借用构造函数继承的缺陷,融合了它们的优点,是JavaScript中最常用的一种继承模式。

    3.2 组合继承的缺陷

    嗯。。。

    组合继承也有缺陷

    下次分享——JavaScript面向对象程序设计之继承(二)

    组合式继承的缺陷

    原型式继承

    寄生式继承

    寄生组合式继承

    声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。对于本博客如有任何问题,可发邮件与我沟通,我的QQ邮箱是:3074596466@qq.com
  • 相关阅读:
    7387. 【2021.11.16NOIP提高组联考】数析送命题
    js 数组的基本操作
    界面跳转已打开界面强制刷新
    Topshelf安装Windows服务
    np_utils.to_categorical
    SQLServer数据库的.ldf文件太大怎办?
    Maven报错Please ensure you are using JDK 1.4 or above and not a JRE解决方法!
    [学习笔记]设计模式之Factory Method
    [学习笔记]设计模式之Singleton
    [学习笔记]设计模式之Abstract Factory
  • 原文地址:https://www.cnblogs.com/CherishTheYouth/p/CherishTheYouth_20210128.html
Copyright © 2011-2022 走看看