一. 前言
继承是面向对象非常重要的一特性,几乎所有编程语言都拥有各自的实现方法,也成了面试必问题。下面我从实际代码出发来简述JavaScript中的继承的实现方式,分别从es5和es6两方面说明,从浅至深,希望能帮助您加深理解。
二. ES5实现方式
其核心就是明白构造函数,实例,和原型对象之间的关系。先明白以下名词和以下规则。
- (1)protype: 原型对象.
- (2)__proto__: 原型属性。
- (3)每一个构造函数都拥有一个prototype对象。
- (4) 每一个对象都拥有一个原型属性。
公用父类文件:
// Person.js
function Person(name,age) {
this.name = name || 'Person';
this.age = age || 12;
console.log('Person constructed');
}
Person.prototype = {
sayName : function() {
console.log('this Name:', this.name);
},
array: [1,2,3,4,5]
}
module.exports = Person;
(1) 原型链
核心思想:子类的原型对象(prototype)等于父类的实例。
// student.js
const Person = require("./Person");
function Student(name,age) {
this.name = name || "student";
this.age = age || 16;
}
Student.prototype = new Person();
let s = new Student('student',16);
console.log(s); // { name: 'student', age: 16 }
console.log(s.sayName()); // this name: student;
// 相当于s.__proto_ === Student.prototype; s.__proto__.__proto__ === Student.prototyp.__proto === Person.prototype
console.log(s.__proto__.__proto__); // { sayName: [Function: sayName], array: [ 1, 2, 3, 4, 5 ] }
// 问题1对应代码
let s2 = new Student('s2', 22);
let s3 = new Student('s3', 25);
s2.array.push(5)
console.log('s2.array.push[5],result is:',s2.array) // s2.array.push[5],result is: [ 1, 2, 3, 4, 5, 5 ]
console.log('s3.array is:',s3.array); // s3.array is: [ 1, 2, 3, 4, 5, 5 ]
执行结果如图:
问题1:原型链继承的问题或者缺点?
答:很明显的一点是:我们无法向父类构造参数传递参数,第二点是:实例将会共享原型上的引用对象(上例为array);
(2) 借用构造函数
核心思想:在子类的构造函数调用父类的构造函数;通过apply或bind在未来创建的新对象中执行构造函数。
/** 借用构造函数完成继承 */
const Person = require('./Person');
function Teacher(name,age) {
Person.call(this,name,age);
console.log('Teacher constructed');
}
const teacher = new Teacher('萧凯', 22);
console.log(teacher); //
// 以下会报错,问题2处对应代码
try {
teacher.sayName();
} catch (error) {
console.log(error);
}
代码运行结果如图所示:
问题2:借用构造函数存在的问题,优缺点?
答:借用构造函数解决了像父类传参的问题;缺点是无法实现函数的复用(父类型中定义的函数子类型不可见,父类型原型上定义的函数子类型不可见),参见运行图标红处。
(3)组合继承方式
核心思想:组合了原型链和借用构造函数的优点;通过原型链达到了定义到原型对象上属性和方法的复用,用借用构造函数完成了实例属性的复用。即通过原型上定义方法完成了方法的复用,又能保证每个实例拥有自己的属性。
const Person = require('./Person');
function Student (name ,age) {
Person.call(this,name,age);
this.age = 24;
console.log('Student constructed');
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
const s = new Student('student1',23);
console.log(s);
console.log(s.sayName());
// 缺点:无论在什么情况下,都会调用两次超类型构造函数,一次是在创建子类型原型的时候,一次是在子类型构造函数的内部
// 修改1, 实际是浅拷贝
console.log('--------修改1----------');
Student.prototype = Object.create(Person.prototype); // 相当于 Student.prototype.__proto__ = Person.prototype
const s2 = new Student('萧红','222');
console.log('------------------');
console.log(s2);
console.log(s2.sayName());
console.log(Student.prototype.__proto__ === Person.prototype,12345)
console.log('s2.__proto__ === Student.prototype:',s2.__proto__ === Student.prototype); // true
console.log('Student.prototype === Person.prototype',Student.prototype === Person.prototype) // false
console.log('Student.prototype.__proto__ === Person.prototype',Student.prototype.__proto__ === Person.prototype) // true
// 修改2
console.log('--------修改2----------');
Object.setPrototypeOf(Student,Person.prototype)
const s3 = new Student('萧红','222');
console.log('------------------');
const p2 = new Person('Person',11);
console.log(s3);
console.log(s3.sayName());
console.log('s2.__proto__ === Student.prototype:',s2.__proto__ === Student.prototype); // true
console.log('Student.prototype === Person.prototype',Student.prototype === Person.prototype) // false
console.log('Student.prototype.__proto__ === Person.prototype',Student.prototype.__proto__ === Person.prototype) // true
运行结果如图所示:
问题3:请问组合继承方式最大的缺点是什么?有什么优化的方法?您会怎么优化?
答:最大的缺点就是会执行两次父类的构造函数;一次是在父类构造的时候;另外一次是在构造子类原型对象的时候(参加运行结果中标红框处)。 改进方法:可以减少构造函数的调用一次,.在子类原型对象构造的时候,可以使用Object.create()和Object.setPrototypeOf()方法代替,参见代码中的修改1和修改2。
Object.create()方法,创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 ,用于实现继承。 如 a = Object.create(c), 相当于 a.__proto__ = c; 修改1中我们使用了此方法。
Object.setPrototypeOf()方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null。
语法:Object.setPrototypeOf(obj, prototype); obj为目标对象, prototype表示要设置的原型
如 Object.setPrototypeOf(a,B),相当于a.__proto__ === B; 修改2中我们使用了该方法。看图:
修改1,2代码运行图如下:
问题4:观察以下下代码,请说出运行结果?
const s2 = new Student('萧红','222');
console.log('s2.__proto__ === Student.prototype:',s2.__proto__ === Student.prototype);
console.log('Student.prototype === Person.prototype',Student.prototype === Person.prototype)
console.log('Student.prototype.__proto__ === Person.prototype',Student.prototype.__proto__ === Person.prototype)
我们直接观察运行结果:
由上得出以下结论,请谨记以下结论:
- 实例的原型属性指向其构造函数的原型对象。 即 s2.__proto__ === Student.prototype -> true;
- 子类的原型对象的原型属性指向父类的原型对象。 即 Student.prototype.proto === Person.prototype -> true
深入理解以上,相信您对原型链一定有了自己的理解,那么这儿再给出一个思考:
s2.__proto___.__proto__的结果是什么呢?
显而易见:答案是 Person.prototype,等式互换即可得到。
因为: s2.__proto__ === Student.prototype
Student.prototype.__proto__ === Person.prototype
那么 s2.__proto___.__proto__ === Person.prototype
(4)原型式继承
核心思想:不用严格意义上的构造函数。借用一个函数完成将现有的对象挂载到当前函数的原型对象上,然后返回该函数的实例完成继承。
代码如下:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
let person = {
name: "xk1",
age: 22,
hobby: ['swimming', 'eating', 'singing'],
sayName: function(){
console.log("this name is", this.name);
}
};
let student = object(person);
console.log(student.__proto__);
// 缺点如下
console.log('----缺点如下---')
student.hobby.push['Play'];
console.log("student's hobby",student.hobby);
console.log('student add hobby:play');
console.log("student's hobby",student.hobby);
console.log("person's hobby",person.hobby);
代码运行结果如下:
问题5:原型式继承有什么问题?这个obect函数可用Object类中的API替换么?
答:最明显的问题就是引用类型公用问题。参见代码标红处。可以使用Object.create()替代,其实就是Object.create完成了咱们这个object函数的功能。代码中的 student.__proto__ === person
(5) 寄生式继承
核心思想: 通过原型式得到新的对象实例,在将这个实例进行扩展。(添加函数,属性等),将这一过程封装成一个函数,然后返回这个扩展之后的实例。
我们直接看代码:
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
function createAnother(origin) {
// 通过object函数得到实例
const o = object(origin);
// 在实例上进行扩展
o.sex = 'male';
o.getSex = function() {
console.log('this name is:',this.sex);
}
return o;
}
const person = {
name: '萧红',
age: '22',
getName() {
console.log(this.name);
}
}
const student = createAnother(person);
console.log('student is:',student);
console.log('student__proto__ is:',student.__proto__);
代码运行结果:
问题6:寄生式继承存在的问题?
答:在对象上扩展方法时无法达到函数的复用;同时原型对象上的引用类型数据也会被共享。
添加以下代码:
const miniStudent = createAnother(student);
console.log(miniStudent);
console.log(miniStudent.__proto__);
运行结果是:
可以发现:miniStudent中的getSex是新扩展的;并没有复用student中的getSex方法。
(6)寄生组合式
核心思想:借用构造函数完成属性的继承,然后通过原型链的混成达到方法的继承;避免了构建子类型原型对象时调用超类的构造函数,只需要超类型的一个副本。本质上通过寄生式完成方法父类原型对象的继承,再将结果赋给子类型。
看代码
const Person = require('./Person');
function object(o) {
function F(){};
F.prototype = o;
return new F(); // f.__proto__ = o;
}
function inheritPrototype(subType,superType) {
/** 可以理解为得到父类型的原型对象 */
const prototype = object(superType.prototype); // prototype.__proto__ = superType.prototype,可以用Objec.create(superType.prototype)替换;
prototype.constructor = subType;
/** 赋给子类型的原型对象 */
subType.prototype = prototype;
}
function Student(name,age) {
// 借用构造函数完成实例属性和实例方法的继承
Person.call(this,name,age);
console.log("Student constructed");
}
// 继承方法
inheritPrototype(Student, Person);
let s = new Student('student1', '22');
console.log(s);
console.log('s.__proto__:', s.__proto__);
console.log('s.__proto__..__proto__:', s.__proto__.__proto__);
代码运行结果如图:
从图我们可以看出:寄生组合方式是es5中最合适的继承方式。
三. Es6实现继承
Es6通过class定义类,通过extends后接父类完成继承;必须在子类构造函数中调用super()函数。 代码如下:
我们必须知道以下规则:class继承拥有两条继承链,prototype和__proto__。假设类B继承A
- 子类的原型属性等于A,即B.__proto__ === A, 用于继承实例属性。
- 子类的原型对象的原型属性等于A的原型对象,即B.prototype.__proto__ === A.prototype, 用于继承方法。
class Person {
constructor (name,age) {
this.name = name || '??';
this.age = age || 0;
console.log('Person constructed');
}
setName (name) {
this.name = name;
}
getName () {
return this.name;
}
}
class Student extends Person {
constructor(name,age) {
super(name,age);
console.log('Student constructed');
}
}
class Teacher extends Person {
constructor(name,age) {
// 不调用super函数
// super(name,age)
this.name = name;
this.age = age ;
}
}
let student = new Student('萧红',22);
console.log(student);
try {
let teacher = new Teacher('王老师', 44);
} catch (error) {
console.log(error);
}
运行结果如下:
问题7:为什么必须在子类构造函数中调用super函数(),没有会怎么样?
答:如果不调用,新建实例时会报错。因为子类没有自己的this对象,而是继承父类的this对象,然后对其加工,如果不调用,就无法使用this,只有调用了super()之后,后面才能使用this关键字;如图标红处报错。
问题8: 说说怎么实现抽象类?
答:采用new.target属性;new.target属性表示如果通过new 创建实例时会返回类名的引用,否则返回undefined;在构造函数中加判断逻辑即可,如下代码。
class C {
constructor(name,age) {
if (new.target === C) {
throw new Error('本类不能实例化');
}
this.name = name;
this.age = age;
}
}
let c = new C('c',22);
console.log(c,12345); // 未能被输出
运行结果如下:
四. 总结
- 实例的原型属性指向其构造函数的原型对象。
- 子类的原型对象的原型属性指向父类的原型对象。
- class继承拥有两条继承链,__proto__用来继承属性,prototype用来继承方法。
- new.target通过new方法调用时指向类名引用,否则返回undefined
- 设置__proto__属性可以使用Object.create()和Object.setPrototypeOf()方法代替。
构造函数,prototype, __proto__,实例之间的关系如图所示
es5如图所示:
es6如图所示:
五. 片尾思考
大家思考一下怎么完成多继承?欢迎在评论区进行回答,提示:Object.assing(),作者将在下一篇文章进行解答。