js继承的几种实现

在学习继承时要对js的原型对象/原型链/构造函数/this的指向有一定的理解
使用以下例子来辅助学习,首先定义两个构造函数,希望Son能继承Father

function Father() {
	this.name = 'father'
	this.color = ['red', 'blue']
	this.age = 40
	Father.prototype.sayHello = function () {
		console.log('hello')
 	}
}
function Son() {
  	this.name = 'Son'
}

原型链继承

在es6之前声明的构造函数主要有以下特点:函数名大写(es6类名也要大写),实例的私有变量使用this定义,一般的函数方法是实例之间公用的,所以会定义在函数的原型对象上,这样每个实例对象都能共用相同的方法。

直接使用例子中的代码

function Father() {
	this.name = 'father'
	this.color = ['red', 'blue']
	this.age = 40
	Father.prototype.sayHello = function () {
		console.log('hello')
 	}
}
function Son() {
  	this.name = 'Son'
}

通过原型链继承的关键代码就是:

Son.prototype = new Father()

/* 同时因为Son.prototype被重新定义了,
他的constructor指向了通过Father原型对象指向Father了如需利用constructor,
我们可以手动修复这个问题
*/
Son.prototype.constructor = Son

修改Son的原型对象,使Son的原型对象指向Father的实例。这样Son的实例在调用属性或者方法时,自己没有的属性或方法会去Son构造函数的原型对象上去找,这时候就找到了Father的实例上,而Father的实例又会指向Father的构造函数的原型对象上,这样我们就通过原型链实现了继承。

接下来测试一下

	const p1 = new Son()
	console.log(p1.name, p1.age, p1.color) // Son 10  ['red', 'blue']
	p1.sayHello()  // 'hello'

乍一看没什么问题了,但是还是存在许多问题(听君一席话,如听一席话)
首先是Father实例中如果有引用属性,在原型链继承方式下,Son的实例都会共用该引用属性。(不明白的可以去了解一下堆/栈/传值/传址这些知识)

	const p2 = new Son()
	p2.color.push('black')
	console.log('p2.color') // ['red', 'blue','black']
	console.log('p1.color') // ['red', 'blue','black']
	/* 如果使用p2.color = ['red', 'blue','black']这种形式来修改,
	其实是给p2实例上添加了一个color属性,会遮蔽原型上的color,
	并不会修改原型上的color属性,此时p1的color仍是 ['red', 'blue']
	*/

原型链继承第二个问题,无法给父类型的构造函数传参,因为传参发生在new的时候,在修改Son的原型对象时Father已经new了。这两个问题导致原型链继承不会单独使用。

盗用构造函数继承

使用这种方式继承需要对call/apply/bind这种强绑定修改this指向的方法有一定了解。

直接使用例子中的Father代码

function Father() {
	this.name = 'father'
	this.color = ['red', 'blue']
	this.age = 40
	Father.prototype.sayHello = function () {
		console.log('hello')
 	}
}

在创建Son构造函数时,我们要先利用call/apply执行一下Father的构造函数,这样在创建Son实例时,就会为实例添加父类所有的属性。

function Son() {
        Father.call(this)
        this.name = 'son'
      }

测试:

const p1 = new Son()
const p2 = new Son()

p1.color.push('black')
console.log(p1.color)   //  ['red', 'blue','black']
console.log(p2.color)	//  ['red', 'blue']

p1.sayHello() // Uncaught TypeError: p1.sayHello is not a function

通过这种方式的继承的构造函数其实等用于这样:

function Son() {
       	// Father.call(this)
       	this.name = 'father'
		this.color = ['red', 'blue']
		this.age = 40
		Father.prototype.sayHello = function () {
		console.log('hello')
 		}
 		
        this.name = 'son' //  注意私有变量要写在下方,不然会被父类构造方法覆盖
      }

原封不动的将Father构造函数放在了Son构造函数中执行,会为每一个Son的实例添加一个color属性,所以彼此直接并无关联。这种新式也能解决原型链继承无法给父类传参的问题。如下我们简单改造一下函数即可证明这一点

	function Father(fatherName) {
        this.name = fatherName
    }
    function Son(name) {
       Father.call(this, name)
    }
    const p1 = new Son('father')
    console.log(p1.name) // 'father' 

但是这种形式也有一些问题,主要缺点就是父类的构造函数中的方法没法复用,我们知道想要方法复用需要定义在构造函数的原型对象上,通过调用父类的构造函数,父类的方法只有在构造函数中定义,子类才能获取到,最终方法相当于定义在了子类的构造函数中。父类定义在原型对象上的方法,当子类执行时相当于给父类原型对象添加一个方法,而子类并不能获取到。简单来说就是,函数不能重用,子类不能访问父类的原型所以这种继承方式我们也不会单独使用。

组合继承

组合继承综合了原型链继承和盗用构造函数继承,将两者方式结合起来,利用原型链来继承父类原型上的属性和方法,利用盗用构造函数来继承实例属性,既能复用方法,也能让每个实例有自己的属性。

function Father() {
	this.name = 'father'
	this.color = ['red', 'blue']
	this.age = 40
	Father.prototype.sayHello = function () {
		console.log('hello')
 	}
}
function Son() {
 	Father.call(this)
  	this.name = 'Son'
}

Son.prototype = new Father()
Son.prototype.constructor = Son

测试

const p1 = new Son()
const p2 = new Son()

p1.color.push('black')
console.log(p1.color)   //  ['red', 'blue','black']
console.log(p2.color)	//  ['red', 'blue']

p1.sayHello() // 'hello'

这种继承弥补了原型链和盗用构造函数的不足,但是也出一些冗余属性。

其他一些继承类型

原型式继承,寄生式继承,寄生式组合继承 感兴趣的可以深入研究一下

ES6类

ES6的类的写法类似java的类的定义,但是它其实是ES5语法糖的形式,背后仍然使用的原型和构造函数的概念
使用类来重写上面的例子 私有变量写在constructor中,通用方法直接写在constructor同级,此外还可以加入一些静态方法,使用static修饰。继承使用extends关键字,写法上只允许继承一个类或者一个构造函数,constructor中super()(执行父类的构造函数)一般放在第一行,不允许在super之前出现this。

class Father {
	constructor() {
		this.name = 'father'
		this.color = ['red', 'blue']
		this.age = 40
	}
	sayHello() {
          console.log('hello')
     }
}
class Son extends Father {
	constructor() {
		super()  // 不要在调用super()之前引用this,否则会抛出ReferenceError
		this.name = 'Son'
	}
}

测试

const p1 = new Son()
const p2 = new Son()

p1.color.push('black')
console.log(p1.color)   //  ['red', 'blue','black']
console.log(p2.color)	//  ['red', 'blue']

p1.sayHello() // 'hello'

类还是语法糖的形式,我们还是要理解他们背后的原理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值