JS中的继承
面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。但Javascript之父在设计语言时,为了使语言轻便,没有引入类的概念,而是通过“原型对象”(prototype)实现。JS中常见的继承方式有基于原型链继承,基于构造函数继承和基于 class 的继承
1.原型链继承
子类型的原型为父类型的一个实例对象
模板如下, ⭕️核心就是把子类的prototype设置为父类的实例
// 父类
function Person() {}
// 子类
function Student(){}
// 继承
Student.prototype = new Person()
👇下面来看一个实例:
//父类型
function Person(name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setName = function () { }
}
Person.prototype.setAge = function () { }
//子类型
function Student(price) {
this.price = price
this.setScore = function () { }
}
// 子类型的原型为父类型的一个实例对象
Student.prototype = new Person()
var s1 = new Student(15000)
var s2 = new Student(14000)
s1.play.push(4)
console.log(s1,s2)
打印出来的结果:
可以看到只对s1进行play的修改,s2中的play也发生了变化,这是就是原型链继承的缺点父类的引用属性会被所有子类实例共享。
还有一个缺点就是子类构建实例时不能向父类传递参数。但优点是父类方法可以复用,比如上述的setAge
。
2.构造函数继承
在子类型构造函数中通用call()调用父类型构造函数
⭕️核心就是在子类(Student)里执行Person.call(this)
,将父类构造函数的内容复制给了子类的构造函数。
👇下面来看一个实例:
function Person(name, age) {
this.name = name,
this.age = age,
this.setName = function () {}
}
function Student(name, age, price) {
Person.call(this, name, age) // 相当于: this.Person(name, age)
/*this.name = name
this.age = age*/
this.price = price
}
var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Lucy', 20, 15000)
console.log(s1.setName === s2.setName)
可以发现父类的函数setName()
,在子类的实例下是不共享的,构造函数继承的方式中引用属性不会被所有实例共享,构造函数继承的方式不使用prototype继承,而是在子类里面执行父类的构造函数,相当于把父类的代码复制到子类里面执行一遍,这样做的另一个好处就是可以给父类传参。
可以发现原型链继承和构造函数继承的优缺点正好完全相反,把他们组合起来就可以得到互补的组合继承。
3.组合继承
⭕️组合继承,就是各取上面2种继承的长处,父类中的普通属性使用构造函数继承,函数 使用 原型链继承。
👇下面来看一个实例:
function Person(name, age) {
this.name = name,
this.age = age,
}
Person.prototype.setName = function(){}
function Student(name, age, price) {
Person.call(this, name, age) // 构造函数继承(继承属性)
this.price = price
}
Student.prototype = new Person() // 原型链继承(继承方法)
var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Lucy', 20, 15000)
console.log(s1.setName === s2.setName)
setName()
使用prototype继承就可以被所有子类实例共享
4.原型式继承
⭕️ object内部首先是创建了一个空的构造函数F,然后把F的prototype指向参数proto,最后返回一个F的实例对象,完成继承. 原型式继承看起来跟原型继承很像,事实上,两者因为都是基于prototype继承的,所以也有一些相同的特性,比如引用属性共享问题, 那原型式继承跟原型继承有什么区别呢? 一个比较明显的区别就是clone函数接收的参数不一定要是构造函数,也可以是其他任何对象, 这样我们就相当于是浅复制了一个对象.
function object(o){
function F(){}
F.prototype = o;
return new F();
}
使用:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
ECMAScript 5 通过新增
Object.create()
方法规范化了原型式继承。
所以ES5的Object.create()
函数,就是基于原型式继承的,上文中代码可以转变为
var yetAnotherPerson = object(person); => var yetAnotherPerson = Object.create(person);
5.寄生组合式继承
组合继承有个缺点,就是父类构造函数里面的代码会执行2遍.
- 第一次:Student.prototype = new Person()
‘new 的过程’的第三步,其实就是执行了父类构造函数。
- 第二次: Person.call(this, name, age)
call的作用是改变函数执行时的上下文。比如:A.call(B)。其实,最终执行的还是A函数,只不过是用B来调用而已。所以,你就懂了Parent.call(this,name,like) ,也就是执行了父类构造函数。
可以用寄生组合式继承来解决这个问题
function inherit(sub, super){
let prototype = clone(super.prototype)
prototype.constructor = sub
sub.prototype = prototype
}
这样我们就实现了一个寄生组合式继承的函数inherit,接下来我们来使用一下:
function Person(name){}
function Student(){
SuperType.call(this)
}
inherit(Student, Person)
我们用 inherit
函数替换了 Student.prototype = new Person()
,从而避免了执行 new Person()
.
这是一种完美的继承方式。
6.class 的继承
ES6中引入了class关键字,class可以通过extends关键字实现继承,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的
⭕️ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
👇下面来看一个实例:
class Person {
//调用类的构造方法
constructor(name, age) {
this.name = name
this.age = age
}
//定义一般的方法
showName() {
console.log("调用父类的方法")
console.log(this.name, this.age);
}
}
let p1 = new Person('kobe', 39)
console.log(p1)
//定义一个子类
class Student extends Person {
constructor(name, age, salary) {
super(name, age)//通过super调用父类的构造方法
this.salary = salary
}
showName() {//在子类自身定义方法
console.log("调用子类的方法")
console.log(this.name, this.age, this.salary);
}
}
let s1 = new Student('wade', 38, 1000000000)