面向对象的三大特性:封装、继承和多态。上一篇我们简单的了解了封装的过程,也就是把对象的属性和方法封装到一个函数中,这一篇讲一下JavaScript中继承的实现,继承是面向对象中非常重要的特性,它可以帮助我们提高代码的复用性。继承主要的思想就是将重复的代码逻辑抽取到分类中,子类只需要通过继承分类,就可以使用分类中的方法,但是在实现JavaScript继承之前,需要先了解一个重要的知识点“原型链”。
在上一篇JavaScript面向对象—对象的创建和操作中已经简单的了解过了JavaScript中对象的原型和函数的原型,当我们从一个对象上获取属性时,如果在当前对象自身没有找到该属性的话,就会去它原型上面获取,如果原型中也没有找到就会去它原型的原型上找,沿着这么一条线进行查找,那么这条线就是我们所说的原型链了。
示例代码:
const obj = { name: 'curry', age: 30}obj.__proto__ = {}obj.__proto__.__proto__ = {}obj.__proto__.__proto__.__proto__ = { height: 1.83 }console.log(obj.height) // 1.83对应的内存中的查找过程:

当通过原型链查找某个属性时,一直找不到的话会一直查找下去么?肯定是不会的,JavaScript的原型链也是有尽头的,这个尽头就是Object的原型。
事实上,不管是对象还是函数,它们原型链的尽头都是Object的原型,也称之为顶层原型,我们可以打印看看这个顶层原型长什么样。
(1)打印Object的原型
console.log(Object.prototype)

(2)Object原型的特殊之处
如果我们再次打印Object.prototype的原型,这个原型属性已经指向了null;
console.log(Object.prototype.__proto__) // null并且在Object.prototype上有很多默认的属性和方法,像toString、hasOwnProperty等;
(3)上一篇中讲到当使用new操作符调用构造函数时,其对象的[[prototype]]会指向该构造函数的原型prototype,其实Object也是一个构造函数,因为我们可以使用new操作符来调用它,创建一个空对象。
示例代码:
const obj = new Object()obj.name = 'curry'obj.age = 30console.log(obj.__proto__ === Object.prototype) // trueconsole.log(obj.__proto__) // [Object: null prototype] {}console.log(obj.__proto__.__proto__) // null内存表现:

(4)总结
toString方法了;如果需要实现继承,那么就可以利用原型链来实现了。
Person和子类Student;// 定义Person父类公共的属性function Person() { this.name = 'curry' this.age = 30}// 定义Person父类的公共方法Person.prototype.say = function() { console.log('I am ' + this.name)}// 定义Student子类特有的属性function Student() { this.sno = 101111}// 实现继承的核心:将父类的实例化对象赋值给子类的原型Student.prototype = new Person()// 定义Student子类特有的方法Student.prototype.studying = function() { console.log(this.name + ' studying')}// 实例化Studentconst stu = new Student()console.log(stu.name) // curryconsole.log(stu.age) // 30console.log(stu.sno) // 101111stu.say() // I am currystu.studying() // curry studying内存表现:

缺点:
针对方案一的缺点,可以借用构造函数来进行优化。
// 定义Person父类公共的属性function Person(name, age) { this.name = name this.age = age}// 定义Person父类的公共方法Person.prototype.say = function() { console.log('I am ' + this.name)}// 定义Student子类特有的属性function Student(name, age, sno) { // 通过call调用Person父类,创建自己的name和age属性 Person.call(this, name, age) this.sno = sno}// 实现继承的核心:将父类的实例化对象赋值给子类的原型Student.prototype = new Person()// 定义Student子类特有的方法Student.prototype.studying = function() { console.log(this.name + ' studying')}// 实例化Studentconst stu1 = new Student('curry', 30, 101111)const stu2 = new Student('kobe', 24, 101112)console.log(stu1) // Person { name: 'curry', age: 30, sno: 101111 }console.log(stu2) // Person { name: 'kobe', age: 24, sno: 101112 }内存表现:

缺点:
new Person(),一次在Person.call();通过上面两种方案,我们想实现继承的目的是重复利用另外一个对象的属性和方法,如果想解决方案二中的缺点,那么就要减少Person的调用次数,避免去执行
new Person(),而解决的办法就是可以新增一个对象,让该对象的原型指向Person的原型即可。
(1)对象的原型式继承
将对象的原型指向构造函数的原型的过程就叫做对象的原型式继承,主要可以通过以下三种方式实现:
封装一个函数,将传入的对象赋值给构造函数的原型,最后将构造函数的实例化对象返回;
function createObj(o) { // 定义一个Fn构造函数 function Fn() {} // 将传入的对象赋值给Fn的原型 Fn.prototype = o // 返回Fn的实例化对象 return new Fn()}const protoObj = { name: 'curry', age: 30}const obj = createObj(protoObj) // 得到的obj对象的原型已经指向了protoObjconsole.log(obj.name) // curryconsole.log(obj.age) // 30console.log(obj.__proto__ === protoObj) // true改变上面方法中的函数体实现,使用Object.setPrototypeOf()方法来实现,该方法设置一个指定的对象的原型到另一个对象或null;
function createObj(o) { // 定义一个空对象 const newObj = {} // 将传入的对象赋值给该空对象的原型 Object.setPrototypeOf(newObj, o) // 返回该空对象 return newObj}直接使用Object.create()方法,该方法可以创建一个新对象,使用现有的对象来提供新创建的对象的__proto__;
const protoObj = { name: 'curry', age: 30}const obj = Object.create(protoObj)console.log(obj.name) // curryconsole.log(obj.age) // 30console.log(obj.__proto__ === protoObj) // true(2)寄生组合式继承的实现
寄生式继承就是将对象的原型式继承和工厂模式进行结合,即封装一个函数来实现继承的过程。而这样结合起来实现的继承,又可以称之为寄生组合式继承。下面就看看具体的实现过程吧。
Person父类的对象,将其赋值给Student子类的原型;Student子类的实例对象的类型都是Person,可以通过重新定义constructor来优化;// 定义Person父类公共的属性function Person(name, age) { this.name = name this.age = age}// 定义Person父类的公共方法Person.prototype.say = function() { console.log('I am ' + this.name)}// 定义Student子类特有的属性function Student(name, age, sno) { // 通过call调用Person父类,创建自己的name和age属性 Person.call(this, name, age) this.sno = sno}// 调用Object.create方法生成一个原型指向Person原型的对象,并将这个对象赋值给Student的原型Student.prototype = Object.create(Person.prototype)// 定义Student原型上constructor的值为StudentObject.defineProperty(Student.prototype, 'constructor', { configurable: true, enumerable: false, writable: true, value: Student})// 定义Student子类特有的方法Student.prototype.studying = function() { console.log(this.name + ' studying')}// 实例化Studentconst stu1 = new Student('curry', 30, 101111)const stu2 = new Student('kobe', 24, 101112)console.log(stu1) // Student { name: 'curry', age: 30, sno: 101111 }console.log(stu2) // Student { name: 'kobe', age: 24, sno: 101112 }内存表现:

总结:
多个地方用到了继承,可以将上面的核心代码赋值在一个函数里面,如果不想用Object.create(),也可以使用上面封装的createObj函数;
function createObj(o) { function Fn() {} Fn.prototype = o return new Fn()}/** * @param {function} SubClass * @param {function} SuperClass */function inherit(SubClass, SuperClass) { SubClass.prototype = createObj(SuperClass.prototype) Object.defineProperty(SubClass.prototype, 'constructor', { configurable: true, enumerable: false, writable: true, value: SubClass })}寄生组合式实现继承的原理其实就是创建一个空对象用于存放子类原型上的方法,并且这个对象的原型指向父类的原型,在ES6中推出的class的实现原理就在这;