js继承是一种允许我们在已有类的基础上创建新类的机制,为子类提供了更高的灵活性,可以重用父类的方法和属性。
1、原型链继承
// 提供父类(继承谁,提供继承的属性)
function Person(name) {
this.name = name || 'sun'
this.work = function(){
console.log(`${name}正在加班`)
}
this.works = ['文档', '代码']
}
Person.prototype.end = function(){
console.log(`${name}下班了`)
}
function People(){}
People.prototype = new Person() // 继承父类
//重写People.prototype的constructor属性,使其执行自己的构造函数People
People.prototype.constructor = People
// 创建实例
let people = new People()
people.name = 'du'
console.log(people.name) // du
let people1 = new People()
console.log(people1.name) // sun
但是存在一些问题(首先我们要清楚js中引用类型数据的存储方式)
通过改变原型指向实现了继承。这导致了一个问题,因为我们改变原型指向的同时,直接初始化了属性,这样继承过来的属性的值都是一样的了。这是个问题,如果我们想要改变继承过来的值,只能重新调用对象的属性进行重新赋值,这又导致我们上边的初始化失去了意义
a) 引用类型属性被所有实例共享
let people2 = new People()
people2.works.push('测试')
console.log(people2.works) // ['文档', '代码', '测试']
let people3 = new People()
console.log(people3.works) // ['文档', '代码', '测试']
- 1)在上面代码中我们先访问people2的works属性并添加一个值,但是people2本身并没有这个属性,所以又接着去原型里面找,找到并添加值
紧接着当people3访问works属性时,同理去原型上找,但是此时原型上的works属性已经被修改 - 2)People.prototype = new Person()意思就是People.prototype 有了this.works这个属性,而不是People本身有这个属性,我们打印了people2是一个空对象{},它的__proto__才有this.works里面的属性。打印了people3,同理。说明了这个this.works不是new People()独有的属性,而是prototype上共有的属性,所以一个实例属性更改,另一个实例属性也更改。
b) 不能动态的向Person()传参
People.prototype = new Person('小李') // 继承父类
// 这个很好理解,此时当我们通过new People()创建实例时会发现,所有的实例的name属性的值都是一样的
2、构造函数继承
在子类型构造函数的内部调用父类构造函数,通过使用call()和apply()方法可以在新创建的对象上执行构造函数。
function Person(name = 'sun') {
this.works = ['文档', '代码']
this.work = function(){
console.log(`${name}正在加班`)
}
}
function People(name) {
Person.call(this, name)
// Person.apply(this, arguments)
}
let people = new People('sun')
people.works.push('测试')
console.log(people.works) // ["文档", "代码", "测试"]
let people1 = new People('张')
people1.works.push('运行')
console.log(people1.works) // ["文档", "代码", "运行"]
自己运行代码之后就能发现此时已经解决了原型链继承的通病:
- 避免了引用类型属性被所有实例共享
- 可以动态的在People中向Person传参数
重点在于Person.call(this, name)这句话。call()或apply(),实际上是在新创建的People实例的环境下调用了People构造函数,改变了Person内部this的指向,此时我们打印people和people1都会发现work和works属性都变成了自己独有的属性,而不是在_proto_上
但是同样的有一个问题:
从代码可以看到我们从每次通过new Peope()创建实例的时候,this.work这个方法都是打印谁正在加班,作用都是一样的,所示是不是很浪费内存,这是就可以考虑将这种共有同样作用的属行放在原型的,所以引出了原型链、构造函数组合继承
3、组合继承(原型链+构造函数)
用原型链实现对原型属性和方法的继承,用借用构造函数继承来实现对实例属性的继承
function Person(name = 'sun') {
this.works = ['文档', '代码']
}
// 将共有的方法放在原型上
Person.prototype.work = function(){
console.log(`${this.name}正在加班`)
}
function People(name, age){
Person.call(this, name)
// Person.apply(this, arguments)
this.age = age
}
// 重点:
//构造函数方式是不能继承原型属性/方法的 (原型中定义的方法和属性对于子类是不可见的)。
//原型链继承的方式就可以继承原型的属性/方法
People.prototype = new Person()
//重写People.prototype的constructor属性,使其执行自己的构造函数People
//People.prototype.constructor = People
let people = new People('sun', 23)
console.log(people)
console.log(people.age) // 23
console.log(people.works) // ["文档", "代码"]
let people1 = new People('张', 25)
console.log(people1)
console.log(people1.age) //25
console.log(people1.works) // ["文档", "代码"]
当我们执行代码的时候就可以发现,当注释掉这段代码 People.prototype = new Person() 直接创建实例的时候,打印people和people1时候,利用原型链的知识层层往上寻找work属性并未找到,但是执行了 People.prototype = new Person() 之后就找到了
原来构造函数方式是不能直接继承原型属性/方法 (原型中定义的方法和属性对于子类是不可见的)。原型链继承的方式就可以继承原型的属性和方法
- 我们打印people会发现创建的时候和其_proto_里面存在相同的属性works,造成了资源的浪费和占用内存,原因是调用两次父构造函数Person
People.prototype = new Person()
// 创建实例时调用下面的代码
let people = new People('sun', 23)
Person.call(this, name)
4、原型式继承(object.create())
用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象object.create()就是这个原理。类似于复制一个对象,用函数来包装。
一句话描述:创建对象的原型=传入的对象
缺点:
- 1、所有实例都会继承原型上的属性。
- 2、无法实现复用。(新实例属性都是后面添加的)
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
let person = {
name: 'sun',
age: 24,
work: ['文档', '测试']
}
let person1 = createObj(person)
let person2 = createObj(person)
person1.name = 'liu'
console.log(person1.name) // liu, 此时的name为自身的属性
console.log(person2.name) // sun 此时的name为原型上的属性
person1.worl.push('测试')
console.log(person1) // ["文档", "测试", "测试"]
console.log(person2) // ["文档", "测试", "测试"]
缺点很明显,跟原型链有着同样问题:引用类型的属性值始终都会共享相应的值。
5、寄生式继承()
就是给原型式继承外面套了个壳子
function createObj(obj){
let newObj = Object.create(obj)
newObj.work = ['文档','工作']
newObj.sayHello = function() {
console.log('hello')
}
return newObj
}
let obj = {
name: 'sun',
}
let person1 = createObj(obj)
person1.work.push('测试')
console.log(person1.work) // ["文档", "工作", "测试"]
let person2 = createObj(obj)
console.log(person2.work) // ["文档", "工作"]
确定很明显,sayHello每次创建对象都会被执行。
6、寄生组合式继承(常用)
解决组合继承里实例上和其原型上存在相同的属性
function Person(name = 'sun') {
this.name = name
this.works = ['文档', '代码']
}
// 将共有的方法放在原型上
Person.prototype.work = function(){
console.log(`${this.name}正在加班`)
}
function People(name){
Person.call(this, name)
// Person.apply(this, arguments)
}
// 组合继承,看上面第三点(原理及存在的问题)
//People.prototype = new Person()
// 寄生组合继承
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
function instantiation(People, Person) {
// 继承原型上的属性和方法
People.prototype = createObj(Person.prototype)
//重写People.prototype的constructor属性,使其执行自己的构造函数People(修复实例,不然还是会指向Person)
People.prototype.constructor = People
}
instantiation(People, Person)
let people1 = new People('李四')
console.log(people1) //看下面的打印结果
从截图我们可以看到我们可以看到新的实例people1的属性并没有和其_proto_的属性重复,并且constructor属性的指向也没有混乱