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 可以看作只是一个语法糖,它的绝大部分功能,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
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
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 // 继承方法
//原型连接
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