【JavaScript由浅入深】实现继承的方法
一、ES5实现继承
1.1 原型链继承
// 定义父类构造函数
function Person(name, age) {
this.name = name
this.age = age
}
// 父类原型上添加内容
Person.prototype.running = function() {
console.log(`${this.name} running`)
}
// 3.定义子类构造函数
function Student(name, age, sno) {
this.name = name
this.age = age
//name,age删除,则只能继承person中的name,age
this.sno = sno
}
// 方法一:直接将父类的prototype赋值给子类的prototype
// 缺点:子类与父类共享一个原型对象,修改了任意一个,另一个也被修改了
// Student.prototype = Person.prototype
// 创建一个父类的实例对象(new Person()),并且作为子类的原型对象
//缺点:1)子类的实例对象继承过来的属性是在原型上的,无法打印
// 2)没有完美的实现属性的继承(子类的实例对象可以从弗雷继承属性,也可以拥有自己的属性)
let p = new Person("zhang", 20)
Student.prototype = p
// 5.在子类原型添加内容
Student.prototype.studying = function() {
console.log(`${this.name} studying`)
}
let stu1 = new Student("lisa", 18, 111)
let stu2 = new Student("bob", 25, 112)
stu1.studying()
stu2.running()
console.log(stu1.name, stu1.age)
console.log(stu2)
原型链继承的内存图:
原型链继承的弊端:
-
目前有一个很大的弊端:某些属性其实是保存在p对象上的;
- 第一,我们通过直接打印对象是看不到这个属性的;
- 第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;
- 第三,不能给Person传递参数(让每个stu有自己的属性),因为这个对象是一次性创建的(没办法定制化)
1.2 借用构造函数
- 借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数.
- 因为函数可以在任意的时刻被调用;
- 因此通过
apply()
和call()
方法也可以在新创建的对象上执行构造函数;
// 定义person构造函数
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function() {
console.log(`${this.name} running`)
}
// 定义student类
function Student(name, age, sno) {
// 借用构造函数
Person.call(this, name, age)
this.sno = sno
}
Student.prototype.studying = function() {
console.log(`${this.name} studying`)
}
let stu1 = new Student("lisa", 18, 111)
let stu2 = new Student("bob", 25, 112)
console.log(stu1.name, stu1.age)
console.log(stu2)
console.log(stu2.__proto__);
借用构造函数的弊端:
- 不能继承父类的原型对象上的方法
- 此时调用Person原型上的方法
stu2.running()
控制台会报错:Uncaught TypeError: stu2.running is not a function
1.3 组合式继承
- 原型链继承和构造函数继承都存在各自的问题和优势,结合两种继承方式便生成了组合继承
// 定义person构造函数
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function() {
console.log(`${this.name} running`)
}
// 定义student类
function Student(name, age, sno) {
// 借用构造函数
Person.call(this, name, age)
this.sno = sno
}
// 原型链方法
var p = new Person("zhang", 18)
Student.prototype = p
Student.prototype.studying = function() {
console.log(`${this.name} studying`)
}
let stu1 = new Student("lisa", 18, 111)
let stu2 = new Student("bob", 25, 112)
stu1.studying()
stu2.running()
console.log(stu1.name, stu1.age)
console.log(stu2)
组合式继承的弊端:
- 调用两次父类构造函数
- 一次在创建子类原型的时候;
- 另一次在子类构造函数内部(也就是每次创建子类实例的时候);
- 所有的子类实例事实上会拥有两份父类的属性
- 一份在当前的实例自己里面
- 另一份在子类对应的原型对象中
1.4 寄生组合式继承
组合寄生式继承可完美解决上述问题,这也是ES6之前所有继承方式中最优的继承方式
完整代码如下:
// 创建对象的过程
function createObject(o) {
function F() { }
F.prototype = o
return new F()
}
// 将Subtype和Supertype联系在一起
// 寄生式函数
function inherit(Subtype, Supertype) {
Subtype.prototype = createObject(Supertype.prototype)
Object.defineProperty(Subtype.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype
})
}
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function () {
console.log(`${this.name} running`)
}
// 定义student类
function Student(name, age, sno) {
// 借用构造函数
Person.call(this, name, age)
this.sno = sno
}
inherit(Student, Person)
Student.prototype.studying = function() {
console.log(`${this.name} studying`)
}
// 创建实例对象
let stu1 = new Student("lisa", 18, 111)
let stu2 = new Student("bob", 25, 112)
stu1.studying()
stu2.running()
console.log(stu1.name, stu1.age)
console.log(stu2)
优化:利用Object.create()
// 寄生式函数
function inherit(Subtype, Supertype) {
Subtype.prototype = Object.create(Supertype.prototype)
}
二、ES6实现继承
在ES6中新增了使用extends关键字,可以方便的帮助我们实现继承;
extends
关键字用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。- 语法:
class ChildClass extends ParentClass { ... }
- super 关键字用于访问对象字面量或类的原型([[Prototype]])上的属性,或调用父类的构造函数。
class Person {
constructor (name, age) {
this.name = name
this.age = age
}
running() {
console.log(this.name + "running")
}
}
class Student extends Person{
constructor(name, age, sno) {
super(name, age)
this.sno = sno
}
studying() {
console.log(this.name + "studying");
}
}