JS原理之混入和委托理论

175 阅读6分钟

显示混入

function mixin (sourceObj, targetObj) {
  for (const key in sourceObj) {
    // 只会在不存在的情况下复制
    if (!targetObj.hasOwnProperty(key)) {
      targetObj[key] = sourceObj[key]
    }
  }
  console.log('target', targetObj)
  return targetObj
}

const vehicle = {
  engine: 1,
  ignition: () => { console.log('Turning on my engine') },
  drive: () => {
    vehicle.ignition()
    console.log('Steering and moving forward!')
  }
}

const car = {
  wheels: 4,
  drive: () => {
    vehicle.drive.call(this) // 显示多态
    console.log('Rolling on all ' + car.wheels + ' wheels!')
  }
}
const newCar = mixin(vehicle, car)
newCar.drive()
  • 现在newCar就有一份vehicle属性和函数副本了
  • 函数实际上没有被复制,复制的是函数引用。所以,newCar中的属性ignition只是从vehicle中复制过来对于ignition()函数的引用
  • engines属性才直接被复制了
  • newCar本身的属性被没有被重写,从而保留和和car中定义相同的属性
  • vehicle.drive.call(this)这里为显示多态,使用绝对引用,通过名称显示制定vehicle对象,并且调用它的drive函数
  • 复制操作结束后,newCar就和vehicle分离了,向newCar添加属性不会影响vehicle, 反之亦然

寄生继承

// "传统的JavaScript类"vehicle
function Vehicle () {
    this.engines = 1
}
Vehicle.prototype.ignition = function () {
    console.log('Turning on my engine')
}
Vehicle.prototype.drive = function () {
    this.ignition()
    console.log('Steering and moving forward')
}
// "寄生类"car
function Car () {
    // 首先,生成car
    const car = new Vehicle()
    // 赋值给car
    car.wheels = 4
    // 覆盖Vehicle的dirve函数
    car.drive = function () {
        Vehicle.prototype.drive.call(this)
        console.log('Rolling on all ' + this.wheels + ' wheels!')
    }
    return car
}

const newCar = new Car()
newCar.drive()
  • 我们首先复制一份Vehicle对象(父类)的定义,然后混入子对象(子类)的定义,最后用这个复合对象构建实例
  • 调用new Car的时候会创建一个新对象并且绑定到Carthis上,也可以使用Car() 来生成新的对象

隐式混入

const Something = {
    cool: function() {
        this.greeting = 'hello World'
        this.count = this.count ? this.count + 1 : 1
    }
}
Something.cool()
Something.greeting
Something.count

const Another =  {
    cool: function () {
        // 隐式的把Something混入Another
        Something.cool.call(this)
    }
}
Another.cool()
Another.greeting()
Another.count
  • 利用call()方法改变this的指向,来达到借用Something.cool()函数的目的
  • 最终Something.cool()的赋值操作都应用在Another对象上,而不是Something对象上,即count值不是共享的

总结

  • 类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。JavaScript 也有类似的语法,但是和其他语言中的类完全不同。
  • 类意味着复制。传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。
  • 多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父类,但是本质上引用的其实是复制的结果。JavaScript 并不会(像类那样)自动创建对象的副本。
  • 混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆弱的语法,让代码更加难懂并且难以维护
  • 显式混入实际上无法完全模拟类的复制行为,因为对象(和函数!别忘了函数也是对象)只能复制引用,无法复制被引用的对象或者函数本身。忽视这一点会导致许多问题
  • 总地来说,在 JavaScript 中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会埋下更多的隐患。

类理论

假设我们需要在软件中建模一些类似的任务(“XYZ”、“ABC”等)

如果使用类,那设计方法可能是这样的:定义一个通用父(基)类,可以将其命名为Task,在Task类中定义所有任务都有的行为。接着定义子类XYZABC,它们都继承自Task并且会添加一些特殊的行为来处理对应的任务

类设计模式鼓励在继承的时候使用方法重写和多态.比如说在XYZ任务中重写Task中定义的一些通用方法,甚至在添加新行为时通过super调用这个方法的原始版本。你会发现许多行为可以先“抽象”到父类然后再用子类进行特殊化(重写)

下面是对应的伪代码

class Task {
    id;
    // 构造函数
    Task(ID){
        id = ID
    }
    outputTask(){
        ouput(id)
    }
}

class XYZ  inherits Task {
    label;
    // 构造函数
    XYZ(ID, Label){
        super(ID)
        label = Label
    }
    outputTask() {
        super()
        output(label)
    }
}

委托理论

首先你会定义一个名为Task的对象(和许多JavaScript开发者告诉你的不同,它既不是类也不是函数),它会包含所有任务都可以使用(写作使用,读作委托)的具体行为。接着,对于每个任务(“XYZ”、“ABC”)你都会定义一个对象来存储对应的数据和行为。你会把特定的任务对象都关联到Task功能对象上,让它们在需要的时候可以进行委托。

基本上你可以想象成,执行任务“XYZ”需要两个兄弟对象XYZTask协作完成。但是我们并不需要把这些行为放在一起,通过类的复制,我们可以把它们分别放在各自独立的对象中,需要时可以允许XYZ对象委托给Task

下面是对应的代码, 在这段代码中,TaskXYZ 并不是类或者函数,它们是对象. XYZ通过Object.create(..)创建,它的[[Prototype]]委托了Task对象

Task  = {
     setID: function(ID) {
        this.id = ID
    },
    outputID: function() {
        console.log(this.id)
    }
}

// 让xyz委托Task
XYZ = Object.create(Task)
XYZ.prepareTask = function(ID, Label) {
    this.setID(ID)
    this.label = Label
}
XYZ.outputTaskDetetails = function() {
    this.outputID()
    console.log(this.label)
}

// ABC = Object.create(TaSK)
  • idlabel数据成员都是直接存储在XYZ上而不是Task.通常来说,在[[Prototype]]委托中最好把状态保存在委托者(XYZABC)而不是委托目标Task
  • 在类设计模式中,我们故意让父类Task和子类XYZ中都有outputTask方法,这样就可以利用重写(多态)的优势。在委托行为中则恰好相反:我们会尽量避免在[[Prototype]]链的不同级别中使用相同的命名.这个设计模式要求尽量少使用容易被重写的通用方法名,提倡使用更有描述性的方法名,尤其是要写清相应对象行为的类型。这样做实际上可以创建出更容易理解和维护的代码,
  • this.setID(ID)函数,XYZ中的方法首先会寻找XYZ自身是否有setID(..),但是XYZ中并没有这个方法名,因此会通过[[Prototype]]委托关联到Task继续寻找,这时就可以找到setID(..)方法。此外,由于调用位置触发了this的隐式绑定规则,因此虽然setID(..)方法在Task中,运行时this仍然会绑定到XYZ,这正是我们想要的。换句话说,我们和XYZ进行交互时可以使用Task中的通用方法,因为XYZ委托了Task
  • 总的来说,委托行为意味着某些对象在找不到属性或者方法引用时会把这请求委托给另外一个对象