一、前言
在JavaScript中,继承是一种重要的概念,它允许我们创建新的对象,使其从一个现有的对象(父类)继承属性和方法。继承可以提供代码的重用性和可扩展性,使我们能够更高效地开发和组织代码。
JavaScript的继承机制有多种实现方式,包括原型链继承、构造函数继承和class继承等。每种方式都有其特定的应用场景和用法,可以根据具体情况选择合适的继承方式。
无论是初学者还是有一定经验的开发者,掌握JavaScript继承的概念和原理都是必不可少的。理解继承的工作原理,可以帮助我们更好地设计和组织代码,提高代码的可读性和可维护性。
在接下来的学习中,我们将深入了解JavaScript继承的各种实现方式以及它们的优缺点,探索如何合理地运用继承来优化我们的代码。无论是面向对象的开发还是函数式编程,都离不开继承的重要角色,所以务必牢牢掌握继承的概念和应用。让我们一起踏上学习JavaScript继承的旅程吧!
二、前置知识
在学习继承之前,首先得先理解原型链相关知识点(什么是原型?什么是原型链?原型链查找机制?)。
我觉得学习过数据结构里的链表理解起来就很简单,原型链的查找机制类似。我根据自己的理解,手绘了一张原型图(画的不太好看hhh),跟着这张图开始讲解吧!
首先我们使用const fn = new Fn()创建一个实例对象,既然是实例对象,实例对象也是对象的一种,每个对象都有自己的隐式原型__proto__,每个构造函数都有自己的显式原型prototype,对象的__proto__会指向构造函数的显式原型prototype。所以,可以得出fn.proto = Fn.prototype。
小插曲---new原理:
- 首先创建一个空对象.
- 将这个空对象的隐式原型__proto__指向构造函数的显式原型prototype。
- 改变构造函数的this指向,将构造函数的this指向这个空对象。
- 若构造函数返回一个对象,则返回这个对象,否则返回这个空对象。
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。
根据上述的规律,我们可以轻松得出:
- Function.prototype.constructor === Function
- Function.prototype.proto === Object.prototype
- Object.prototype.constructor === Object
- 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指向,但会有两个问题:
- 只能继承父类私有的属性或者方法(因为是把Parent当做普通函数执行,和其原型上的属性和方法没有关系)
- 父类私有的变为子类私有的。
function Child() {
Parent.call(this)
}
(3)组合继承
为了能让子类拿到父类prototype上的属性或方法,需要将方式(1)和方式(2)相结合,但是会有两个问题:
- 会调用两次父类构造函数。(第一次是方式1创建父类实例时调用,第二次是方式二采用call进行调用)。
- 所有子类实例上会拥有两份父类的属性。
(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实现继承的方式,如有其他见解,欢迎评论区留言。
欢迎关注公众号---代码分享站