前言
面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。
大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class(ES6 引入了class 语法),而是通过“原型对象”(prototype)实现。
(下面这段引用个人感觉对于原型链的理解很重要)
ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。它的主要思想就是利用构造函数,原型和实例之间的关系。来实现一个引用类型继承另一个引用类型的属性和方法。我们知道,每个函数都有一个
prototype属性,这个属性指向该函数的原型对象。而当这个函数被当作构造函数进行实例化的时候,它的实例内部有一个[[Prototype]](__proto__)属性,这个属性指向这个构造函数的原型对象。如果将一个构造函数的原型等于另一个实例,那么这个构造函数的原型就会包含一个指向另一个原型对象的指针。这种原型和实例之间的关系,正是我们借用原型链实现继承的基本思想。
知道了原型链形成的原理,下面将为大家介绍JavaScript中常见的实现继承的方式,并分析其优缺点。
一、原型链继承
这一方式的关键在于:子类型的原型为父类型的一个实例对象。
//父类型
function Person(name, age) {
this.name = name,
this.age = age,
this.hobby = ['music', 'sport', 'movie']
this.setName = function () { }
}
Person.prototype.setAge = function () { }
//子类型
function Student(grade) {
this.grade = grade
this.setScore = function () { }
}
Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象
Student.prototype.constructor = Student
var s1 = new Student(2)
var s2 = new Student(3)
console.log(s1,s2)
但这种方式实现的本质是通过将子类的原型指向了父类的实例,所以子类的实例就可以通过__proto__访问到 Student.prototype 也就是Person的实例,这样就可以访问到父类的私有方法,然后再通过__proto__指向父类的prototype就可以获得到父类原型上的方法。于是做到了将父类的私有、公有方法和属性都当做子类的公有属性
子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性和方法,我们都知道在操作基本数据类型的时候操作的是值,在操作引用数据类型的时候操作的是地址,如果说父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2。
s1.hobby.push('politics')
console.log(s1.hobby, s2.hobby);
console.log(s1.__proto__ === Object.getPrototypeOf(s2)); // true
console.log(s1.__proto__.__proto__ === s2.__proto__.__proto__); // true
s1 中的 hobby 属性发生变化,与此同时,s2 中的 hobby 属性也会跟着变化。
tips:
另外注意一点的是,我们需要在子类中添加新的方法或者是重写父类的方法时候,切记一定要放到替换原型的语句之后。
Student.prototype = new Person()
Student.prototype.sayHello = function() {}
缺点:
- 来自原型对象的所有属性被所有实例共享
- 创建子类实例时,无法向父类构造函数传参
二、借用构造函数继承
这一方式的关键在于:在子类型构造函数中通过 call() 调用父类型构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.setName = () => {}
}
Person.prototype.setAge = () => {}
function Student(name, age, grade) {
Person.call(this, name, age)
/* this.name = name
this.age = age */
this.grade = grade
}
let std1 = new Student('Louis', 24, 3)
缺点:
- 只能继承父类的实例属性和方法,不能继承父类原型属性和方法
- 无法实现函数的复用,每个子类都有父类实例函数的副本,影响性能
三、组合继承(原型链 + 借用构造函数)
这一方式的关键在于:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过父类实例作为子类原型,实现函数复用
function SuperClass(name, age) {
this.name = name;
this.age = age;
}
SuperClass.prototype.sayHello = function () {
console.log("i m super");
};
function SubClass(name, age, color) {
SuperClass.call(this, name, age);
this.color = color
this.setColor = () => {}
}
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass; //重新添加构造函数指向
const instance = new SubClass('Louis', 22, 'blue');
缺点
- 构造函数被调用两次,创建了两个超类实例
四、寄生式组合继承
(目前最完美的继承实现方式)
function SuperClass(name, age) {
this.name = name;
this.age = age;
}
SuperClass.prototype.say = function () {
console.log("i m super");
};
function SubClass(name, age, color) {
SuperClass.call(this, name, age);
this.color = color
this.setColor = function() {}
}
//继承函数
/* const Fn = function () {};
Fn.prototype = SuperClass.prototype;
SubClass.prototype = new Fn(); */
// 以上三行代码等同于:
SubClass.prototype = Object.create(SuperClass.prototype)
// 还原constructor, 否则此时 constructor 指向的是 SupClass
SubClass.prototype.constructor = SubClass;
const instance = new SubClass();
五、ES6中class的继承
ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。
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)
console.log(s1)
s1.showName()