JS 面向对象之继承

256 阅读16分钟

JS 里面有很多没那么好懂的内容,像是闭包,继承,this。我打算将这些内容揉碎了分别输出文档。

这篇讲继承,查看完整代码请戳:继承

为什么要有继承

从 JS 创始人 Brendan Eich 讲起

JS 也是面向对象语言,一切都是对象,必须要有一种机制,将所有对象联系起来。但 Brendan Eich 的创始人又不想在 JS 中引入"类",因为一旦引入“类”, JS 就会变成一种完整的面向对象编程语言,这显示太正式,不是创建 JS 语言的初衷。受 C++ 和 JAVA 里 new 命令的影响,他就把 new 命令引入了JS,用来从构造函数中产生一个实例对象。具体详见 JS 面向对象之封装

prototype 的引入

由于直接 new 出来的实例对象,都有自己的属性和方法的副本,无法做到数据共享,也浪费了内存。考虑到这点,Brendan Eich 决定为构造函数设置一个 prototype 属性。将所有需要共享的属性和方法,都放在这个 prototype 对象里面;那些独有的属性和方法,就放在构造函数里面。 这样实例对象创建好之后,将会有两类属性和方法,一个是自己的,一个是通过原型链(沿着 prototype )查找到的。

总结

继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展。 比如构造函数 B 想要使用构造函数 A 里的属性和函数,一种是直接复写 A 里的代码,复制粘贴一下。还有一种就是使用继承,让 B 继承 A,这样 B 就可以使用 A 里的功能

那么都有哪几种继承方式呢?分别有哪些优缺点呢?且听我慢慢道来。。。

原型链继承

语法

将子类的原型对象指向父类的实例。

Child.prototype = new Parent()

示例

function Parent(name,gender) {
	this.name = name;
	this.gender = gender;
}
Parent.prototype.getInfo = function() {
	console.info('my name is ' + this.name + '; I am a ' + this.gender )
}
function Child(age) {
	this.age = age
}
Child.prototype = new Parent("parent","boy")
var child1 = new Child(30)
console.info(child1) // Child {age:30}
child1.getInfo() // my name is parent; I am a boy
console.info(child1.name) // parent

分析:

  1. 默认 Child 实例的原型对象是指向 Child.prototype 的,经过
Child.prototype = new Parent("parent","boy")

设置, Child 的原型对象就指向了 Parent 的实例,具体原型链图如下: 原型链继承 依图可得,child 实例 proto 属性指向 parent 实例, parent 实例下的 proto 属性指向 Parent.prototype。所以 child1 既可以访问到 parent 实例的属性和方法(即 Parent 构造函数中定义的),也可以访问到 Parent.prototype 上定义的属性和方法

  1. 初次看到原型链继承,单看这名字,以为是用子类的原型对象指向父类的原型对象,即:
Child.prototype = Parent.prototype

代码修改为:

function Parent(name,gender) {
	this.name = name;
	this.gender = gender;
}
Parent.prototype.getInfo = function() {
	console.info('my name is ' + this.name + '; I am a ' + this.gender ) 
}
function Child(age) {
	this.age = age
}
Child.prototype = Parent.prototype
var child1 = new Child(30)
console.info(child1) // Child {age:30}
child1.getInfo() // my name is parent; I am a boy
console.info(child1.name) // undefined

原型链图就会变为: 错误的原型链继承 很明显,此时 Child 的实例就访问不到 Parent 构造函数里定义的属性和方法,只能访问到 Parent 原型对象上定义的属性和方法,这明显不是我们要的真正继承

原型链继承总结

function Parent(name,gender) {
	this.name = name;
	this.gender = gender;
	this.colors= ["blue","pink"] // 新增引用类型属性 colors
}
Parent.prototype.getInfo = function() {
	console.info('my name is ' + this.name + '; I am a ' + this.gender ) 
}
function Child(age) {
	this.age = age
	this.sports = ["basketball"]  // 新增引用类型属性 sports
}
var parent = new Parent("parent","boy")
Child.prototype = parent 
var child1 = new Child(30,"child1 name") // 想传入child1 name 作为该实例独有的名称
child1.gender = "girl"
child1.colors.push("green")
child1.sports.push("football")
var child2 = new Child(29,"child2 name") // 想传入child2 name 作为该实例独有的名称
console.info(child1) // Child {age: 30, sports: ["basketball", "football"], gender: "girl"}
console.info(child2) // Child {age: 29, sports: ["basketball"]}
console.log(child1.name) // parent
console.info(child2.colors) //  ["blue", "pink", "green"]

分析:

  • 在 Parent 构造函数里新增引用类型 colors 属性,在 Child 构造函数里新增引用类型 sports 属性
  • 创建了两个 Child 实例(child1 与 child2)。
  • child1 创建好之后,设置了 gender,并且给 colors 和 sports 都 push 了内容。
  • child2 创建好之后,没做任何处理
  • child1.gender = "girl" 相当于在 child1 实例上新增了 gender 属性,这样通过 child1 获取 gender 属性时,就会获取它自己的 gender 值,而不会往原型上查找
  • child1.colors.push("green") 注意这里是先获取到 colors 值,才接着 push,而要获取到 colors 只能在 parent 实例上查找到,所以相当于是往 parent 实例里的 colors 属性添加内容,这样就会影响到 parent 以及实例化出来的所有 Child 实例
  • 而 sports 是 Child 上自带的属性,所以在 child1 上 push 之后不会影响到别的实例(比如 child2 )
  • 因此 child1 打印出了 age 、sports 和新增的 gender 属性。而 child2 只打印出了 age 和 sports
  • child1.name 打印出了'parent',这是因为 child1.name 访问的实际上是原型对象 parent 上的 name。虽然 new Child 时传递了 'child1 name' 但很明显是没效果的,因为在 Child 构造函数里并没有接收 name 属性
  • child2.colors 实际上访问的也是原型对象 parent 上的 colors,而 colors 已经被 child1 给改成了 ["blue", "pink", "green"]

优点:

  • 继承了父类构造函数里的属性和方法,又继承了父类原型对象上的属性和方法

缺点:

  • 无法实现多继承(因为指定了原型对象)
  • 来自原型对象的所有属性都被共享了,当更改个引用类型的属性时,所有的实例对象都会受到影响(这点从child2.colors可以看出来)
  • 无法传递参数给父对象,比如我要设置个独有的 name 值,就只能在自己的构造函数里重新定义 name 属性,用来屏蔽父对象的 name(这点从child1.name可以看出来)

构造函数继承

语法

在子类构造函数内部使用 call 或 apply 来调用父类构造函数

function Child() {
	Parent.call(this,...arguments)
}

来达到增强子类的目的,等于复制父类的实例属性给子类

示例

function Parent(name,gender) {
	this.name = name;
	this.gender = gender;
}
function Child(age,name,gender) {
	this.age = age
	Parent.call(this,name,gender) // 等价于Parent.apply(this,[name,gender])
}
var child1 = new Child(30,"child1 name","girl")
console.info(child1) // Child {age: 30, name: "child1 name", gender: "girl"}
console.info(child1.name) // child1 name
console.info(child1 instanceof Child) // true
console.info(child1 instanceof Parent) // false

分析:

  1. 相当于在子类构造函数里,直接执行了父类构造函数。执行完后就会在子类的构造函数新增了父类所有的属性。
  2. 这个继承是借用了 call 或 apply 函数的能力。
  3. Child 的实例跟 Parent之间并无联系(这点从 intanceof Parent 可以看出来)

构造函数继承总结

function Parent(name) {
	this.name = name;
	this.gender = gender;
	this.colors= ["blue","pink"]
}
Parent.prototype.getInfo = function() {
	console.info('my name is ' + this.name + '; I am a ' + this.gender ) 
}
function Child(age,name,gender) {
	this.age = age
	this.sports = ["basketball"]  
	Parent.call(this,name,gender)
}
var child1 = new Child(30,"child1 name","girl") 
child1.gender = "girl"
child1.colors.push("green")
child1.sports.push("football")
var child2 = new Child(29,"child2 name") 
console.info(child1) // Child {age: 30, sports: ["basketball", "football"], gender: "girl", name: "child1 name", colors: ["blue", "pink", "green"]}
console.info(child2) // Child {age: 29, sports: ["basketball"], gender: undefined, name: "child2 name", colors: ["blue", "pink"]}
console.log(child1.name) // child1 name
console.info(child2.colors) //  ["blue", "pink"]
child1.getInfo() //VM1106:1 Uncaught TypeError: child1.getInfo is not a function

分析:

  • 由于是在子类构造函数里立即执行父类构造函数,所以 colors、name、gender 等属性都是子类自己的,进行任何操作都不会影响别的实例,所以执行 child1.colors.push() 并不会影响别的实例
  • getInfo 是父类原型对象上的方法,而 call 函数只是将父类构造函数的属性和方法执行了一遍,并不会复制父类原型对象上的属性和方法,所以会报错

优点: 解决了原型链继承的缺点

  • 可以有多继承(在子类构造函数里分别调用父类的构造函数即可)
  • 解决了子类实例共享父类构造函数里引用类型属性的问题(这点从 child.colors 可以看出来)
  • 可以向父类传递参数(这点从 child1.name 可以看出来)

缺点: 解决了原型链继承的缺点,但同时将原型链的优点给整没了。

  • 不能继承父类原型对象上的属性和方法
  • 子类实例跟父类之间并无联系(这点从 child1 instanceof Parent 为 false 可以看出来)
  • 无法实现函数复用,对于父类里的函数属性,每个子类都会复制一份,影响性能

组合继承

既然原型链继承跟构造函数继承分别有优缺点,那能否将这两者结合起来?

语法

// 原型链继承
Child.prototype = new Parent()
// 构造函数继承
function Child() {
	Parent.call(this,...arguments)
}
  • 使用原型链继承来保证子类能够继承父类原型对象上的属性和方法
  • 使用构造函数继承来保证子类能够继承到父类构造函数上的属性和方法 基操:
  • 通过call/apply在子类构造函数内部调用父类构造函数
  • 将子类构造函数的原型对象指向父类构造函数创建的一个匿名实例
  • 修正子类构造函数原型对象的constructor属性,将它指向子类构造函数

示例

function Parent(name){
	this.name = name;
}
Parent.prototype.getInfo = function() {
	console.info('my name is ' + this.name)
}
function Child(name) {
	Parent.call(this,name) // 构造函数继承
}
Child.prototype = new Parent() // 原型链继承,传入空参数即可,因为传入任何参数都会被屏蔽
Child.prototype.constructor = Child // 修复 child1 constructor 指向 Parent 问题
var child1 = new Child("child1 name")
console.info(child1) // Child {name: "child1 name"}
child1.getInfo() // my name is child1 name
console.info(child1.constructor) //ƒ Child(name) { Parent.call(this,name) // 构造函数继承 }

分析:

  • 通过原型链继承,来达到可以复用父类原型对象上属性和方法的效果。只要传空参数即可,因为传入任何参数都会被屏蔽
  • 通过构造函数继承,来继承父类构造函数上属性和方法,同时也屏蔽了子类实例__proto__(即父类实例)上的属性和方法
  • 通过Child.prototype.constructor = Child 实际上只是个标记,标识某个实例是由哪个构造函数产生而已。出于编程习惯,最好将它改为正确的构造函数

原型链图修改为如下(比原型链继承多出两条线,一条是 Child里 使用 call/apply 调用 Parent;还有一条是 Child.prototype(即 Parent 的实例) 的 constructor 指向 Child) 组合继承

组合继承总结

function Parent(name, colors) {
	this.name = name
	this.colors = colors
}
Parent.prototype.sports = ['basketball']
function Child(name, colors) {
	Parent.call(this, name, colors)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child

var child1 = new Child('child1', ['pink'])
child1.colors.push('blue')
child1.sports.push('football')
var child2 = new Child('child2', ['black'])

console.info(child1) // Child {name: "child1", colors: ["pink", "blue"]}
console.info(child2) // Child {name: "child2", colors: ["black"]}
console.info(child2.sports) // ["basketball", "football"]
console.info(Child.prototype) // Parent {name: undefined, colors: undefined, constructor: ƒ Child(name, colors){}}

console.info(child1 instanceof Child) // true
console.info(child1 instanceof Parent) // true

分析:

  • 两个实例上的 colors 是通过构造函数继承于父类,是复制出来的属性,每个实例之间不会互相影响
  • sports 是位于父类原型对象上的属性,是共享的,所以更改了 child1.sports 会影响到 其他 Child 实例
  • Child.prototype 是使用 new Parent 生成的,并且生成的时候没有传递参数,所以 name 和 colors 都是 undefined 。而且又将 constructor 指回 Child
  • 由于 child1 可以沿着它的原型链查找到 Child.prototype 和 Parent.prototype 。所以后面两个都为 true

优点: 相当于融合了原型链继承跟构造函数继承的优点:

  • 可以继承父类实例属性和方法,也可以继承父类原型对象上的属性和方法
  • 跟构造函数继承一样,解决了父类构造函数里引用类型属性共享问题
  • 跟构造函数继承一样,可以向父级传递参数

那这个组合继承是不是完美的呢?答案是否定的,接下来一探究竟

function Parent(name) {
	console.info(name)
	this.name = name
}
function Child(name) {
	Parent.call(this, name)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child

var child1 = new Child('child1')
console.info(child1)
console.info(Child.prototype)

结果为:

// undefined
// child1

// Child {name:'child1'}
// Parent {name:'undefined',constructor:f Child(name){}}

分析:

  • 由于 new Parent 时会调用一次 Parent 函数,此时 name 为 undefined; 而在 Child 构造函数里也会调用次 Parent 函数,并传入 child1 作为 name 参数
  • Child.prototype 是指向 Parent 实例对象,这个实例的 name 为 undefined, constructor 重新设置为 Child 了

缺点:

  • 调用了两次父类的构造函数
  • 子类实例中的属性和方法会屏蔽父类实例上的属性和方法,增加了不必要的内存

原型式继承

语法

function create(o) {
	function F() {}
	F.prototype = o
	return new F()
}
  • 在 create 函数内部会创建个临时构造函数,然后将传入的对象作为这个构造函数的原型对象,并返回这个临时构造函数的新实例。
  • 本质上,create() 是对传入的对象执行了一次浅复制。
  • 可以对返回的对象进行适当修改

示例

function create(o) {
	function F() {}
	F.prototype = o
	return new F()
}
var person = {
	name: '张三',
	colors: ['pink', 'blue']
}

var student = create(person)
student.name = '小明'
student.colors.push('yellow')
var teacher = create(person)
teacher.name = '夏老师'
teacher.colors.push('green')
console.info(student) // F {name: "小明"}
console.info(teacher) // F {name: "夏老师"}
console.info(teacher.colors) // ["pink", "blue", "yellow", "green"]

分析:

  • 将 person 作为原型对象,此时实例化出 student 跟 teacher, student 和 teacher 实例的__proto__ 属性就都指向了 person
  • student.name 相当于新建了 name 属性,不会影响 person 里的 name 属性
  • student.colors.push 首先是要先查找到 colors ,此时是在原型对象,即 person 上查找到,所以进行 push 添加内容了,就会影响到 person 以及所有的实例对象(比如 teacher)

原型式继承 ** ES5 之后的原型式继承** ES5 新增 Object.create 方法将原型式继承规范化了。

var person = {
	name: '张三',
	colors: ['pink', 'blue']
}
var student = Object.create(person)
student.name = '小明'
student.colors.push('yellow')
var teacher = Object.create(person)
teacher.name = '夏老师'
teacher.colors.push('green')
console.info(student) // F {name: "小明"}
console.info(teacher) // F {name: "夏老师"}
console.info(teacher.colors) // ["pink", "blue", "yellow", "green"]

原型式继承总结

优点:

  • 比起原型链继承,代码量较少,不用创建构造函数

缺点: 与原型链继承类似

  • 实例之间共享继承实例引用类型属性(这点从 teacher.colors 可以看出)
  • 无法给父级构造函数传递参数,只能覆盖,覆盖之后就会出现父子存在一样的属性问题,造成内存浪费

寄生式继承

语法

function createAnother(original){ 
 let clone = Object.create(original); // 通过调用函数创建一个新对象
 clone.fn = function() {};  // 以某种方式增强这个对象
 return clone; // 返回这个对象
}

就是在原型式继承的基础上再封装一层,来增强对象,之后将这个对象返回

示例

function createAnother(original){ 
 let clone = Object.create(original); // 通过调用函数创建一个新对象
 clone.sayHi = function() { // 以某种方式增强这个对象
 	console.log("hi"); 
 }; 
 return clone; // 返回这个对象
}
var person = {
	name: '张三',
	colors: ['pink', 'blue']
}
var student = createAnother(person)
student.sayHi() // hi

寄生式继承总结

跟原生式继承基本一样 优点:

  • 比起原型链继承,代码量较少,不用创建构造函数

缺点: 与原型链继承类似

  • 实例之间共享继承实例引用类型属性(这点从 teacher.colors 可以看出)
  • 无法给父级构造函数传递参数,只能覆盖,覆盖之后就会出现父子存在一样的属性问题,造成内存浪费
  • 给对象添加函数会导致函数难以重用,造成内存浪费

寄生式组合继承

语法

为了修复组合继承的缺点:

  1. 父类构造函数会被调用两次
  2. 生成了两个实例,在父类实例上产生了无用废弃的属性 ,所以引入了寄生式组合继承。 实际上通过构造函数继承已经可以实现继承父类构造函数里的属性和方法,所以只要继承父类原型对象上的属性和方法即可。
function Child() {
	Parent.call(this,...arguments)
}
Child.prototype = Object.create(Parent.prototype)

示例

function Parent(name) {
	this.name = name
}
Parent.prototype.getInfo = function () {
	console.info(this.name)
}
function Child(name) {
	this.sex = 'boy'
	Parent.call(this, name)
}
// 与组合继承的差别
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
var child1 = new Child('child1')
console.info(child1) // Child {sex: "boy", name: "child1"}
child1.getInfo() // child1
console.info(child1.__proto__) // Parent {}

分析:

  • child1 与 Parent 之间就没有关系了,仅仅是在构造函数里用 Parent.call 复制了 Parent 里的属性和方法,所以只会调用一次 Parent
  • child1 的原型对象是 Parent {} 不存在 name 属性,就不会造成内存浪费

寄生组合继承

寄生组合继承总结

function Parent(name, colors) {
	this.name = name
	this.colors = colors
}
Parent.prototype.sports = ['basketball']
function Child(name, colors) {
	Parent.call(this, name, colors)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

var child1 = new Child('child1', ['pink'])
child1.colors.push('blue')
child1.sports.push('football')
var child2 = new Child('child2', ['black'])
child2.sports = ['badminton']

console.info(child1) // Child {name: "child1", colors: ["pink", "blue"]}
console.info(child2) // Child {name: "child2", colors: ["black"], sports: ["badminton"]}

分析:

  • name 与 colors 属性都是通过构造函数复制过来的,所以改变 child1.colors 对其它实例没有影响的
  • child1.sports 是给原型链上的 sports 添加内容,此时会影响到 parent 与 child2。即 child2.sports 获取到的是 push 后的值。但执行了 child2.sports 此时相当于给 child2 对象新增了 sports 属性,会覆盖原型对象上的 sports 属性,所以此时 child2.sports 取的就是自身的属性

优点:

  • 只调用了一次父类构造函数,只创建了一份父类属性
  • 子类可以用到父类原型链上的属性和方法

class 继承

语法

class 继承主要靠两个东西: extends 与 super

class Parent {}
class Child extends Parent {
}

通过 Babel 编译后的代码:

// 加上了些注释
'use strict'
// 主要是对当前环境支持Symbol和不支持Symbol的typeof处理
function _typeof(obj) {
	'@babel/helpers - typeof'
	if (
		typeof Symbol === 'function' &&
		typeof Symbol.iterator === 'symbol'
	) {
		_typeof = function _typeof(obj) {
			return typeof obj
		}
	} else {
		_typeof = function _typeof(obj) {
			return obj &&
				typeof Symbol === 'function' &&
				obj.constructor === Symbol &&
				obj !== Symbol.prototype
				? 'symbol'
				: typeof obj
		}
	}
	return _typeof(obj)
}
// 寄生组合式继承的核心
function _inherits(subClass, superClass) {
	if (typeof superClass !== 'function' && superClass !== null) {
		throw new TypeError(
			'Super expression must either be null or a function'
		)
	}
	// Object.create()方法创建一个新对象,使用现有的对象来作为新创建的对象的原型(__proto__)。
	// 也就是说执行后 subClass.prototype.__proto__ === superClass.prototype; 这条语句为true
	subClass.prototype = Object.create(superClass && superClass.prototype, {
		constructor: { value: subClass, writable: true, configurable: true }
	})
	if (superClass) _setPrototypeOf(subClass, superClass)
}
// 设置__proto__
function _setPrototypeOf(o, p) {
	_setPrototypeOf =
		Object.setPrototypeOf ||
		function _setPrototypeOf(o, p) {
			o.__proto__ = p
			return o
		}
	return _setPrototypeOf(o, p)
}

function _createSuper(Derived) {
	var hasNativeReflectConstruct = _isNativeReflectConstruct()
	return function _createSuperInternal() {
		var Super = _getPrototypeOf(Derived),
			result
		if (hasNativeReflectConstruct) {
			var NewTarget = _getPrototypeOf(this).constructor
			result = Reflect.construct(Super, arguments, NewTarget)
		} else {
			result = Super.apply(this, arguments)
		}
		return _possibleConstructorReturn(this, result)
	}
}
// _possibleConstructorReturn 判断Parent.call(this, name)函数返回值 是否为null或者函数或者对象。
function _possibleConstructorReturn(self, call) {
	if (
		call &&
		(_typeof(call) === 'object' || typeof call === 'function')
	) {
		return call
	}
	return _assertThisInitialized(self)
}
// 如何 self 是void 0 (undefined) 则报错
function _assertThisInitialized(self) {
	if (self === void 0) {
		throw new ReferenceError(
			"this hasn't been initialised - super() hasn't been called"
		)
	}
	return self
}

function _isNativeReflectConstruct() {
	if (typeof Reflect === 'undefined' || !Reflect.construct) return false
	if (Reflect.construct.sham) return false
	if (typeof Proxy === 'function') return true
	try {
		Boolean.prototype.valueOf.call(
			Reflect.construct(Boolean, [], function () {})
		)
		return true
	} catch (e) {
		return false
	}
}
// 获取__proto__
function _getPrototypeOf(o) {
	_getPrototypeOf = Object.setPrototypeOf
		? Object.getPrototypeOf
		: function _getPrototypeOf(o) {
				return o.__proto__ || Object.getPrototypeOf(o)
		  }
	return _getPrototypeOf(o)
}
// instanceof操作符包含对Symbol的处理
function _instanceof(left, right) {
	if (
		right != null &&
		typeof Symbol !== 'undefined' &&
		right[Symbol.hasInstance]
	) {
		return !!right[Symbol.hasInstance](left)
	} else {
		return left instanceof right
	}
}

function _classCallCheck(instance, Constructor) {
	if (!_instanceof(instance, Constructor)) {
		throw new TypeError('Cannot call a class as a function')
	}
}

var Parent = function Parent() {
	_classCallCheck(this, Parent)
}

var Child = /*#__PURE__*/ (function (_Parent) {
	_inherits(Child, _Parent)

	var _super = _createSuper(Child)

	function Child() {
		_classCallCheck(this, Child)

		return _super.apply(this, arguments)
	}

	return Child
})(Parent)

核心代码是 _inherits 函数,可以看到其实跟寄生组合继承差不多,除了最后多了 _setPrototypeOf ,这句代码的作用是为了继承到父类的静态方法,这是上面这些继承方式所不具备的。也就是说 class 继承根本上还是寄生组合继承,只是多了一步继承父类的静态方法。

示例

class Parent{
    constructor(name){
        this.name = name;
    }
    static sayHello(){
        console.log('hello');
    }
    sayName(){
        console.log('my name is ' + this.name);
        return this.name;
    }
}
class Child extends Parent{
    constructor(name, age){
        super(name);
        this.age = age;
    }
    sayAge(){
        console.log('my age is ' + this.age);
        return this.age;
    }
}
let parent = new Parent('Parent');
let child = new Child('Child', 18);

console.log('parent: ', parent); // parent:  Parent {name: "Parent"}
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent

console.log('child: ', child); // child:  Child {name: "Child", age: 18}
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 18

分析:

  • 在 Parent 上定义了静态的 sayHello 方法,以及 Parent 原型上定义 sayName 方法
  • 采用 extends 使得 Child 继承 Parent
  • Child 上可以访问 Parent 的静态方法

这里面存在两条原型链:

// 1、构造器原型链
Child.__proto__ === Parent; // true
Parent.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 2、实例原型链
child.__proto__ === Child.prototype; // true
Child.prototype.__proto__ === Parent.prototype; // true
Parent.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true

原型链图: ES6 Class 继承 ES6 extends 继承,主要就是:

  • 把子类构造函数(Child)的原型(proto)指向了父类构造函数(Parent),
  • 把子类实例child的原型对象(Child.prototype) 的原型(proto)指向了父类parent的原型对象(Parent.prototype)。
  • 子类构造函数Child继承了父类构造函数Parent的里的属性。使用super调用的(ES5则用call或者apply调用传参)。

class 继承总结

现在大多采用 ES6 class 继承,它是基于 ES5 里的寄生式组合继承的语法糖