美文网首页
上卷 第二部分 第四章 混合对象 类

上卷 第二部分 第四章 混合对象 类

作者: nymlc | 来源:发表于2019-07-09 19:21 被阅读0次

    类理论

    • 对象:具体的事物
    • 类:对对象的抽象,可以看做对象的模板
    • 多态:父类的行为可以被子类重写,相对性多态可以从重写行为之中引用基础行为

    就好像人就是对象。搞对象搞对象嘛。那么你怎么知道站你对面的是人而不是棵树呢?有鼻子有眼会说话等等,总结(抽象)完了就是个类,包括人的一些特征(属性)和行为(方法)
    面向对象编程强调的是数据和操作数据的行为,俩者本质上是关联的。所以好的设计就是把俩者封装起来,也被称为数据结构

    1. 类的设计模式
      从没写过类是种设计模式,我们知道的观察者模式、单例模式等等都是面向对象设计模式。
      其实类不是必须的编程基础,它是一种可选的代码抽象。Java里,类是必选的,万物皆类,C/C++里你可以选择面向类或者过程化(意大利面代码)或者混用
    2. Javascript中的类
      JS只有一些近似类的语法元素(new、instanceof),ES6之中新增了class,不过这仅仅是语法糖

    类的机制

    很多面向类的语言的标准库会提供Stack类,不过你实际上并不是直接操作它,它仅仅是个抽象的描述,你需要实例化它,然后操作实例

    1. 建造
      “类”和“实例”来源于房屋建造
      建筑师会考虑好一栋建筑的特性,多高、几个窗户、窗户在哪等等,他并不关心这个房屋造在哪,也不关心造多少,也不关心房屋内的内容(家具等),他只关心用什么结构来容纳这些内容
      建筑蓝图只是建筑计划,不是真正的建筑,我们还需要建筑工人根据蓝图造出来一栋真正的房子。
      你可以通过蓝图来知道房子的结构,只观察房子本身并不能获得这些信息(有点像逆向工程)。你想开下窗户那么蓝图是不能满足你的,必须真正的房子才行
      以上可知,类就是蓝图,我们为了得到可交互对象就必须按照类来建造(实例化)一个东西(实例)
      你不大可能使用实例类直接访问操作它的类,不过你大致可以判断这个实例对象来自哪个类
      类通过复制操作被实例化为对象(建筑本质上是对蓝图的复制)
    2. 构造函数
      类实例是由一个特殊的类方法够早的,这个方法和类名系统(构造函数),他的任务就是初始化实例所需要的所有信息(状态)
      类构造函数属于类,大多需要new来调用
    class Car {
        color = 'red'
        Car(c) {
            color = c
        }
        drive() {
            console.log('drive')
        }
    }
    
    myCar = new Car('blue') // 实际上是调用构造函数,通常使用new实例化,这样子引擎才知道你想构造一个类实例
    myCar.drive()
    

    类的继承

    父类和子类就像父母和孩子。孩子一旦出生他就是单独的个体,虽然会从父母继承一些特性,不过他也是独一无二的。同理,子类也是独一无二的相对父类而言,可以重写继承的行为和定义新行为

    1. 多态
    class Foo {
        constructor(x) {
            this.x = x
        }
        print() {
            console.log('Foo print')
        }
        output() {
            this.print()
            console.log('Foo output')
        }
    }
    
    class Bar extends Foo {
        constructor(x, y) {
            super(x)
            this.y = y
        }
        print() {
            console.log('Bar print')
        }
        // 重写继承父类的output,不过调用了super.output(),可以引用继承来的原始的方法
        // 这个技术就叫多态或者虚拟多态。也叫相对多态
        output() {
            super.output()
            console.log('Bar output')
        }
    }
    var bar = new Bar(1, 2)
    bar.output()
    
    /**
     * 
        Bar里的output通过多态引用了从Foo继承来的output方法,
        Foo的output方法又引用了print方法(请注意,这里因为JS的原因加了this.,请忽略),
        那么这里引用的print到底是Bar的还是Foo的呢?
        其实是Bar的,除非你是实例化Foo调用的output,
        也就是多态性取决于你在哪个类的实例中引用它
     * 
     */
    

    相对只是多态的一个方面。任何方法都可以引用继承层次上高层的方法(名字一不一样无所谓),之所以说相对是因为没有指定层次,相对引用上一层

    上面说到在真正的类而言,构造函数属于类,不过JS里,“类”(Foo.prototype,方法都挂在prototype上)属于构造函数(Foo)。父类和子类关系只存在于俩者构造函数对应的.prototype上,构造函数本身之间没什么关系,所以无法实现相对引用

    下图箭头代表复制。为什么呢?因为子类Bar继承的父类Foo的方法其实只是一个副本。因为子类可以重写Foo的方法,还能使用多态引用原父类方法。那必须是只能复制一份副本才能互不影响。
    所以继承、多态其实并不是子类和父类有关联,子类其实只是得到了父类的一个副本。类的继承就是复制

    类继承、实例化
    1. 多重继承
      我们以上讨论均在子类只有一个父类上,不过现实生活中后代一般都是有双亲的。所以多重继承更符合现实类比。也就是多个父类的定义会被复制到子类里。
      但是问题就来了,俩父类都有output方法,子类继承哪个?
      还有如下图所示,假如A有output方法,B、C分别重写了该方法,那么D引用output时选择B还是C的
      钻石问题
      这个问题贼复杂,之后另起一章记录

    混入

    在JS中没有类,那么自然也就没有继承时的复制,那么为了模拟类的复制,人们就想了个方法--混入。其实就是对象的复制,然后混进一些特性。就像Object.assign
    混入分为显性和隐性,显性就是很明显的把父类复制给子类,隐性就是没有那么明显,不过也做到了复制

    1. 显示混入
      其实就是俩对象,然后浅拷贝了下,模拟了下继承。多态的话就用具体父类的具体方法call调用并且显示传递this,利用this的绑定特性来模仿多态。
      值得注意的是,“继承”而来的方法复制的是引用。
    function mixin(sourceObj, targetObj) {
        // sourceObj是不能被破坏的
        for (var key in sourceObj) {
            if (!(key in targetObj)) {
                targetObj[key] = sourceObj[key]
            }
        }
        return targetObj
    }
    var Foo = {
        print() {
            console.log('Foo print')
        },
        output() {
            this.print()
            console.log('Foo output')
        }
    }
    var Car = mixin(Foo, {
        output() {
            Foo.output.call(this)
            console.log('Bar output')
        }
    })
    Foo.output()
    Car.output()
    
    • 再说多态
      Foo.output.call(this),这个就是显示多态(都指定了具体“父类”了)。因为Foo和Bar都有output方法,不显示指定的话没辙
      其实是因为俩名字一样,如果不一样就像print方法,就可以直接this.print()调用。就是因为它被“复制”过来了
      显示伪多态缺点就是在需要使用多态的地方创建函数关联(Foo.output.call(this)),和支持相对多态的语言比起来,人家在头部轻轻松松一个extend就成了(class Bar extends Foo).主要是“类”的规模大了就恶心了
    • 混合复制
      之前的mixin有个存在性校验。换个思路,我先把父类(Foo)复制一个出来,然后对这个新对象进行子类特殊化处理。这样子就可以不用存在性检查了。不过想想相对而言也没啥效率
    function mixin(sourceObj, targetObj) {
        // sourceObj是不能被破坏的
        for (var key in sourceObj) {
            targetObj[key] = sourceObj[key]
        }
        return targetObj
    }
    var Foo = {
        print() {
            console.log('Foo print')
        },
        output() {
            this.print()
            console.log('Foo output')
        }
    }
    var Car = mixin(Foo, {})
    mixin({
        output() {
            Foo.output.call(this)
            console.log('Bar output')
        }
    }, Car)
    Foo.output()
    Car.output()
    
    1. 复制的不是对象还行,是对象的话,就像这个print,父子共享的一个函数。因为复制的是引用,牵一发而动全身
    2. 显示混入其实并不是什么多厉害的东西。无非就是少了几条代码,还带来了问题
    • 寄生继承
      既是显式的也是隐式的
    function Foo() {
    
    }
    Foo.prototype.print = function() {
        console.log('Foo print')
    }
    Foo.prototype.output = function() {
        this.print()
        console.log('Foo output')
    }
    function Car() {
        var car = new Foo()
        // 关键在于这点保存了父类的引用
        var fooOutput = car.output
        // 屏蔽属性,详见下一章__proto__
        car.output = function() {
            fooOutput.call(this)
            console.log('Bar output')
        }
        return car
    }
    new Foo().output()
    // 值得注意的是,这里可以不用new,因为Car返回了Car实例,使用new的话得到的对象会被抛弃
    new Car().output()
    
    1. 隐式混入
    var Foo = {
        set() {
            this.val = 'value'
        }
    }
    var Bar = {
        set() {
            // 隐式的把Foo混入Bar
            // 其实就是通过this的绑定规则,在Bar的上下文上调用了Foo的set方法,所以Foo上的set方法赋值操作作用在Bar对象上
            // 也就是Foo的行为混入到了Bar里
            Foo.set.call(this)
        }
    }
    Bar.set()
    Foo.val // undefined
    Bar.val // value
    

    个人觉得总的来说没有什么意思,模拟类。无非就是少几行定义代码而已(当然是在ES5而言,ES6的语法糖还是不错的)

    相关文章

      网友评论

          本文标题:上卷 第二部分 第四章 混合对象 类

          本文链接:https://www.haomeiwen.com/subject/oqghxctx.html