带你彻底搞懂JS继承原理

143 阅读6分钟

一、前言

在JavaScript中,继承是一种重要的概念,它允许我们创建新的对象,使其从一个现有的对象(父类)继承属性和方法。继承可以提供代码的重用性和可扩展性,使我们能够更高效地开发和组织代码。

JavaScript的继承机制有多种实现方式,包括原型链继承、构造函数继承和class继承等。每种方式都有其特定的应用场景和用法,可以根据具体情况选择合适的继承方式。

无论是初学者还是有一定经验的开发者,掌握JavaScript继承的概念和原理都是必不可少的。理解继承的工作原理,可以帮助我们更好地设计和组织代码,提高代码的可读性和可维护性。

在接下来的学习中,我们将深入了解JavaScript继承的各种实现方式以及它们的优缺点,探索如何合理地运用继承来优化我们的代码。无论是面向对象的开发还是函数式编程,都离不开继承的重要角色,所以务必牢牢掌握继承的概念和应用。让我们一起踏上学习JavaScript继承的旅程吧!

二、前置知识

在学习继承之前,首先得先理解原型链相关知识点(什么是原型?什么是原型链?原型链查找机制?)。

我觉得学习过数据结构里的链表理解起来就很简单,原型链的查找机制类似。我根据自己的理解,手绘了一张原型图(画的不太好看hhh),跟着这张图开始讲解吧!

首先我们使用const fn = new Fn()创建一个实例对象,既然是实例对象,实例对象也是对象的一种,每个对象都有自己的隐式原型__proto__,每个构造函数都有自己的显式原型prototype,对象的__proto__会指向构造函数的显式原型prototype。所以,可以得出fn.proto = Fn.prototype。

小插曲---new原理:

  1. 首先创建一个空对象.
  2. 将这个空对象的隐式原型__proto__指向构造函数的显式原型prototype。
  3. 改变构造函数的this指向,将构造函数的this指向这个空对象。
  4. 若构造函数返回一个对象,则返回这个对象,否则返回这个空对象。
function Fn(name){
  this.name = name
}

Function.prototype.myNew = (Fn) => {
  // 1.首先创建一个空对象.
  const obj = {}
  // 2.将这个空对象的隐式原型__proto__指向构造函数的显式原型prototype。
  obj.__proto__ = Fn.prototype
  // 3.改变构造函数的this指向。
  const res = Fn.apply(obj,[...arguments])
  // 4.若构造函数返回一个对象,则返回这个对象,否则返回这个空对象。
  return Object.prototype.toString.call(res) === '[object Object]' ? res : obj
}

const fn = new Fn('test')
console.log(fn.name)  // test

每个原型对象上有自己的constructor,它会指向自己的构造函数,所以,有Fn.prototype.constructor === Fn。

既然是原型对象,原型对象也是对象的一种,它也会有自己的隐式原型__proto__,函数的隐式原型会指向Object.prototype,所以有Fn.prototype.proto === Object.prototype。

函数也是对象的一种,那么Fn就会有自己的隐式原型__proto__,会有Fn.proto === Function.prototype。

根据上述的规律,我们可以轻松得出:

  1. Function.prototype.constructor === Function
  2. Function.prototype.proto === Object.prototype
  3. Object.prototype.constructor === Object
  4. Object.prototype.proto === null

最后我们可以指定原型链的最终尽头就是null。

看到这里,是不是觉得原型链也没那么难,自己也可以动手画一下,更加深刻的理解。那么原型链的查找过程就十分清晰了。当我们访问某个实例对象上的属性或方法时,先会在构造函数上查找,若查找不到,则会通过构造函数的prototype的_proto__不断地向上查找,直到查询到Object.prototype上的__proto__,若还没找到,则返回undefined。

三、深入继承原理

理解了原型链原理,我们可以慢慢深入JavaScript的继承了。我们创建一个Parent方法,有name和age属性,然后再创建一个Child方法,现在想让这个子类继承父类的属性和方法。

function Parent(){
  this.name = 'test'
  this.age = 18
}

function Child(){

}

const child = new Child()

(1)原型链继承

根据原型链的原理,最简单的方式是将Child的prototype去指向Parent的prototype,这样的话,它会现在Child里查找,查找不到就去Child.prototype的__proto__找,这时已经指向父类的prototype,可以实现继承。但这是一种错误的方式,原因就是当一个子类实例对象修改父类公有属性或方法时,其他子类实例获取属性或方法时也会受到影响。所以,我们得先创建父类的实例,让子类原型去指向父类实例,实现继承。

Child.prototype = new Parent()
Child.prototype.constructor = Child

但是,这样父类中私有或者公有的属性方法,最后都会变成子类中公有的属性和方法。

(2)借用构造函数实现继承

这种方法比较暴力,直接在子类改变父类的this指向,但会有两个问题:

  1. 只能继承父类私有的属性或者方法(因为是把Parent当做普通函数执行,和其原型上的属性和方法没有关系)
  2. 父类私有的变为子类私有的。
function Child() {
  Parent.call(this)
}

(3)组合继承

为了能让子类拿到父类prototype上的属性或方法,需要将方式(1)和方式(2)相结合,但是会有两个问题:

  1. 会调用两次父类构造函数。(第一次是方式1创建父类实例时调用,第二次是方式二采用call进行调用)。
  2. 所有子类实例上会拥有两份父类的属性。

(4)寄生组合继承

为了解决方式三的问题,我们修改原型链继承的方式,创建父类实例时,我们采用Object.create()进行创建。

小插曲---Object.create()原理:

Object.myCreate = function(p){
  const Fn = function(){}
  Fn.prototype = p
  return new Fn()
}

了解原理后可以看出使用Object.create()创建出来的对象不会继承父类属性或方法,解决方式三的两个问题。这是ES6以前实现继承较好的方式。

(5)ES6 class继承

ES6出了class继承,语法和java语言十分相像,使用extends继承,当子类需要调用父类的属性或方法,采用super关键字。

class Parent {
  constructor() {
    this.age = 18
  }
}
class Child extends Parent {
  constructor() {
    super();
    this.name = '张三'
  }
}
let o1 = new Child()
console.log(o1, o1.name, o1.age)

以上就是JavaScript实现继承的方式,如有其他见解,欢迎评论区留言。

欢迎关注公众号---代码分享站

v2-88d54674ec29c2990c2ee8c4fb228163_b.jpg