Skip to content

Latest commit

 

History

History
522 lines (385 loc) · 14.3 KB

面向对象与继承.md

File metadata and controls

522 lines (385 loc) · 14.3 KB
title date tags
JavaScript篇-面向对象与继承
2018-11-21 03:04:31 -0800

面向对象

如何声明一个类 ?

ES5 中,还没有类的概念,而是通过函数来声明,到了 ES6,有了 class 关键词,则通过 class 来声明

// ES5
var Animal = function() {
  this.name = 'Animal'
}

// ES6
class Animal {
  constructor() {
    this.name = 'Animal'
  }
}

如何创建对象 ?

  • 字面量对象

  • 显示的构造函数

  • Object.create

// 第一种方式: 字面量
var obj1 = {
  name: '彭道宽'
}
var obj2 = new Object({
  name: '彭道宽'
})

// 第二种方式: 构造函数
var Parent = function() {
  this.name = name
}
var child = new Parent('彭道宽')

// 第三种方式: Object.create
var Parent = {
  name: '彭道宽'
}
var obj4 = Object.create(Parent)

ES6 的 Class

基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

// ES5
function Point(x, y) {
  this.x = x
  this.y = y
}

Point.prototype.toString = function() {
  return `(${this.x}, ${this.y})` // (x, y)
}

var point = new Point(1, 2)
point.toString() // (1, 2)

// ES6 中利用 class 定义类
class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  toString() {
    return `(${this.x}, ${this.y})`
  }
}

var point = new Point(1, 2)
point.toString() // (1, 2)

上面代码定义了一个“类”,可以看到里面有一个 constructor方法,这就是构造方法,而 this 关键字则代表实例对象。也就是说,ES5 的构造函数 Point,对应 ES6 的 Point 类的构造方法。

class Point {
  // ...
}

console.log(typeof Point) // 'function'
console.log(Point === Point.prototype.constructor) // true

类的数据类型就是函数,类本身就指向构造函数。构造函数的 prototype 属性,在 ES6 的 “类” 上面继续存在。事实上,类的所有方法都定义在类的 prototype 属性上面。

class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于下边的代码
Point.prototype.constructor = function() {}
Point.prototype.toString = function() {}
Point.prototype.toValue = function() {}

在类的实例上面调用方法,其实就是调用原型上的方法。

class Point {}

var point = new Point()

console.log(point.constructor === Point.prototype.constructor) // true

更多 Class 的理解,请看阮一峰老师的 ECMAScript 6 入门

继承的实现

下边就列举常用的几种继承方式,搞懂这几种,应该可以混过面试了,记住: 继承的本质就是原型链

  • 原型链继承

  • 借用构造函数继承

  • 组合继承

  • 原型式继承

  • 寄生式继承

  • 寄生组合式继承

  • ES6 的 Class 继承

原型链继承

利用原型,让一个引用类型继承另一个引用类型的属性和方法;

function Parent() {
  this.property = true
}

Parent.prototype.getValue = function() {
  return this.property
}

function Child() {
  this.childProperty = false
}

Child.prototype = new Parent() // 将父类的实例赋给子类的prototype

Child.prototype.getChildValue = function() {
  return this.childProperty
}

var ch1 = new Child()
console.log(ch1.getChildValue()) // false
console.log(ch1.getValue()) // true

继承是通过创建 Parent 的实例,并将该实例赋给 Child.prototype 实现的。实现的本质是 重写原型对象,代之以一个新类型的实例。换句换说,原来存在于 Parent 的实例中的所有属性和方法,现在也存在 Child.prototype 中了。最终结果是: ch1 指向 Child 的原型, Child 的原型指向 Parent 的原型, getValue() 方法仍在 Parent.prototype 上,而 property 位于 Child.property 中,这是因为: property 是一个实例属性,而 getValue() 是一个原型方法

ch1.constructor 现在不是指向 Child ,而是指向 Parent ,这是因为 Child .prototype 被重写的缘故。实际上,不是 Child 的原型的 constructor 属性被重写,而是 Child 的原型指向了另一个对象——Parent 的原型,而这个原型对象的 constructor 属性指向的是 Parent

// 可以这么理解
// 正常情况下
Child.prototype.constructor = Child
ch1.contructor = Child

// 但是现在 Child.prototype = new Parent() 将父类的实例赋给子类的prototype之后
Child.prototype = new Parent()(new Parent()).contructor = Parent
Child.prototype.contructor = Parent
ch1.constructor = Parent

那么原型链继承的问题有哪些呢?

原型链中的原型对象是共用的,子类无法通过父类创建私有属性, 比如你 new 两个子类 child1 和 child2 的时候,你改 child1 的属性,child2 也会跟着改变,比如下边的代码

function Parent() {
  this.colors = ['red', 'yellow']
}

function Child() {}

// 子类继承父类
Child.prototype = new Parent()

var ch1 = new Child()
ch1.colors.push('black')
console.log(ch1.colors) // ['red', 'yellow', 'black']

var ch2 = new Child()
console.log(ch2.colors) // ['red', 'yellow', 'black']

你看,这就出问题了吧,因为在 Parent 构造函数中定义了一个 colors 属性,当通过原型链继承了之后,Child.prototype 就变成了 Parent 的一个实例,因此它也拥有了一个它自己的 colors 属性——就跟专门创建了一个 Child.prototype.colors 一样,那么所有 Child 的实例都会共享这个 colors 属性,而 ch1 和 ch2 都是 Child 的实例,对 ch1.colors 的修改,在 ch2.colors 中也会反映出来

借用构造函数

为了解决上边 原型链继承 存在的问题,现在使用构造函数去继承,在子类的构造函数里执行父类的构造函数, 主要通过 call / apply 去改变 this 的指向,从而导致父类构造函数执行时的这些属性都会挂载到子类实例上去

function Parent() {
  this.colors = ['red', 'yellow']
}

function Child() {
  // 子类继承了父类
  Parent.call(this)
}

var ch1 = new Child()
ch1.colors.push('black')
console.log(ch1.colors) // ['red', 'yellow', 'black']

var ch2 = new Child()
console.log(ch2.colors) // ['red', 'yellow']

组合继承

将原型链和借用构造函数的技术组合在一起。背后的思路是: 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数的服用,又能够保证每个实例都有它的属性

function Parent() {
  this.name = '彭道宽' // 这叫实例属性
}

Parent.prototype.getName = function() {} // 这叫做原型属性
function Parent(name) {
  this.name = name
  this.colors = ['red', 'yellow']
}

Parent.prototype.sayName = function() {
  console.log(this.name)
}

function Child(name, age) {
  // 借用构造函数实现继承
  Parent.call(this, name)
  this.age = age
}

// 子类通过 原型链 继承
Child.prototype = new Parent()
Child.prototype.constructor = Child // 注意, 如果没有说明,那么Child.prototype.constructor 就会是指向 Parent
Child.prototype.sayAge = function() {
  console.log(this.age)
}

var ch1 = new Child('彭道宽', 21)
ch1.colors.push('black')
ch1.sayName() // 彭道宽
ch1.sayAge() // 21
console.log(ch1.colors) // ['red', 'yellow', 'black']

var ch2 = new Child('PDK', 18)
ch1.sayName() // PDK
ch1.sayAge() // 18
console.log(ch1.colors) // ['red', 'yellow']

原型式继承

ECMAScript5 新增 Object.create()方法规范了原型式继承,这个方法接收两个参数 : 一个用作新对象原型的对象和一个为新对象定义额外属性的对象

var Parent = {
  name: 'PDK',
  friends: ['a', 'b', 'c']
}

var ch1 = Object.create(Parent)
ch1.name = 'OB-1'
ch1.friends.push('d')

var ch2 = Object.create(Parent)
ch2.name = 'OB-2'
ch2.friends.push('e')

console.log(Parent.friends) // ['a', 'b', 'c', 'd', 'e']
console.log(Parent.name) // PDK

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

寄生式继承

思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

function Parent(origin) {
  var clone = Object.create(origin) // 通过调用函数来创建一个对象
  clone.sayHi = function() {
    console.log('hi')
  }
  return clone // 返回这个对象
}

var child = {
  name: 'pdk'
}
var resClone = Parent(child)
resClone.sayHi() // "hi"

寄生组合式继承

所谓的寄生组合式继承,就是通过借用构造函数来继承属性,通过原型链的混用来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后将结果指定给自类型的原型。跟组合式继承的区别在于,他不需要在一次实例中调用两次父类的构造函数。基本模式如下:

function inheritPrototype(Child, Parent) {
  var prototype = Object.create(Parent.prototype) // 创建对象
  prototype.constructor = Child // 增强对象
  Child.prototype = prototype // 指定对象
}
function Parent(name) {
  this.name = name
  this.colors = ['red', 'yellow']
}

Parent.prototype.sayName = function() {
  console.log(this.name)
}

function Child(name, age) {
  Parent.call(this, name)
  this.age = age
}

inheritPrototype(Child, Parent)

Child.prototype.sayAge = function() {
  console.log(this.age)
}

var ch1 = new Child('彭道宽', 21)
console.log(ch1.sayage) // 21

ES6 的 Class 继承

class Parent {
  constructor(name) {
    this.name = name
  }

  doing() {
    console.log('parent doing something')
  }

  getName() {
    console.log('parent name: ', this.name)
  }
}

class Child extends Parent {
  constructor(name, parentName) {
    super(parentName)
    this.name = name
  }

  sayName() {
    console.log('child name: ', this.name)
  }
}

var ch1 = new Child('son', 'father')
ch1.sayName() // child name: son
ch1.getName() // parent name: son
ch1.doing() // parent doing something

var parent = new Parent('father')
parent.getName() // parent name: father

class 实现原理

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。

在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super 函数。

class parent {}

class Child extends Parent {
  constructor() {
    super()
  }
}

注意,super 虽然代表了父类 Parent 的构造函数,但是返回的是子类 Child 的实例,即 super 内部的 this 指的是 Child,因此 super() 在这里相当于 Parent.prototype.constructor.call(this)。

class parent {}

class Child extends Parent {}

Child.__proto__ === Parent // 继承属性
Child.prototype.__proto__ === Parent.prototype // 继承方法

extends 实现原理

//原型连接
Child.prototype = Object.create(Parent.prototype)

// B继承A的静态属性
Object.setPrototypeOf(Child, Parent)

//绑定this
Parent.call(this)

最后来两个思考题

function SuperType() {
  this.colors = ['red', 'yellow']
}

function SubType() {}

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

var instance1 = new SubType() // intance.constructor = SuperType
instance1.colors.push('black')
console.log(instance1.colors) // ['red', 'yellow', 'black']

var instance2 = new SubType()
console.log(instance2.colors) // ['red', 'yellow', 'black']

// 这里多出几道题,理解一下原型和原型链
console.log(instance1.constructor)
console.log(SubType.prototype.constructor)
console.log(SubType.prototype.__proto__ == SuperType.prototype)
console.log(instance1.__proto__ == SubType.prototype)
console.log(SubType.__proto__ == SuperType.prototype)
console.log(SubType.__proto__ == Function.prototype)
console.log(SuperType.prototype.constructor == SuperType)
console.log(SuperType.__proto__ == Function.prototype)
console.log(SuperType.prototype.__proto__ == Object.prototype)
function SuperType() {
  this.colors = ['red', 'yellow']
}

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

var instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ['red', 'yellow', 'black']

var instance2 = new SubType()
console.log(instance2.colors) // ['red', 'yellow']

// 思考一哈?
console.log(instance1.constructor)
console.log(SubType.prototype.constructor)
console.log(SubType.prototype.__proto__)
console.log(SubType.prototype.__proto__ == SuperType.prototype)
console.log(SubType.prototype.__proto__ == Object.prototype)
console.log(instance1.__proto__ == SubType.prototype)
console.log(SubType.__proto__ == SuperType.prototype)
console.log(SubType.__proto__ == Function.prototype)
console.log(SuperType.prototype.constructor == SuperType)
console.log(SuperType.__proto__ == Function.prototype)
console.log(SuperType.prototype.__proto__ == Object.prototype)

相关链接

ES6 入门 - Class 继承: http://es6.ruanyifeng.com/#docs/class-extends

博客 : https://github.com/PDKSophia/blog.io

原型与原型链 : https://github.com/PDKSophia/blog.io/blob/master/JavaScript%E7%AF%87-%E5%8E%9F%E5%9E%8B%E5%92%8C%E5%8E%9F%E5%9E%8B%E9%93%BE.md