一次搞懂原型链和继承😉
前言
最近正值秋招,陆陆续续投递了一些简历,发现自己很多基础知识都已经忘记,或是之前压根没有看过🤣。为此,复习总结一波关于 JavaScript
的八股合集,一方面是为了帮助自己更好的 拿捏
面试笔试及巩固基础,另外一方面是为帮助到也需要复习关于 JavaScript
的八股的同学😊。
原型链
原型链由来
JavaScript
这门语言就是基于 原型
这种设计模式来设计的,所以 JavaScript
中才会出现 原型链
这个概念。
基于 原型链
, JavaScript
这门语言实现 面对对象编程
,让 JavaScript对象
拥有 封装、继承和多态
等众多面对对象特性。
原型链概念
构造函数
(构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与new
一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。首字母要大写)prototype
(每个函数都有一个prototype
属性,函数的prototype
属性指向了一个对象,prototype 对象
用于放某同一类型实例的共享属性和方法,实质上是为了内存着想。)
_proto_
(每一个JavaScript对象
(除了null
)都具有的一个属性,叫__proto__
,这个属性会指向该对象的原型)
constructor
(每个原型对象都有一个constructor
属性指向关联的构造函数)
实例、构造函数、原型对象的关系
(构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。)
原型链四道关卡
- 第一关,什么是原型?
原型就是一个对象,就是 构造函数.prototype
代表的这个对象。
- 第二关,什么是原型链?
一个个对象通过 __proto__属性
串连起来,形成的 链式结构
就是 原型链
。
- 第三关,理解这张原型链图
这张图初看可能会觉得很复杂,感觉各种关系,好多线条~
但是不要怕,首先我们可以发现它是有三列的,第一列是我们使用 new
实例化的对象,第二列是构造函数(首字母大写),第三列是原型对象(通过 实例对象._proto_
或者 构造函数.prototype
,我们可以拿到这个对象)
然后根据 _proto_属性
来看看这张图有多少条原型链吧,你能找出来嘛?哈哈哈哈哈哈~
- f1 -> Foo.prototype -> Object.prototype -> null
- o1 -> Object.prototype -> null
- Foo -> Function.prototype -> Object.prototype -> null
- Object -> Function.prototype -> Object.prototype -> null
- Function -> Function.prototype -> Object.prototype -> null
我们可以发现以下几点 关键点 ,帮助我们解决原型链指向问题,妈妈再也不会担心你不会原型链了~
原型链的起点
要么是实例对象
(如12条原型链),要么是构造函数
(如345条原型链)。原型链的终点
一定是null
。new Object()生成的实例对象
以及其他原型对象
(例如 Foo.prototype 和 Function.prototype ,除了 Object.prototype ),这两种对象的原型都是Object.prototype
。构造函数
(Foo、Object、Function)的原型都是Function.prototype
。
- 第四关,常见面试题,你会了嘛?
- 如何理解原型及原型链
- 如何重写原型
- 原型链指向
- 如何手写实现一个 new
function create() { let Con = [].shift.call(arguments); // 获取构造函数 // obj.__proto__ = Con.prototype; 这种写法obj.__proto__ 已弃用,影响性能,可见mdn let obj = Object.create(Con.prototype) // 创建一个对象,并设置该对象的原型 let result = Con.apply(obj, arguments); // 绑定 `this` 并执行构造函数 return result instanceof Object ? result : obj; // 确保返回值为对象 }
继承
继承由来
为了让代码实现共享,提高代码的重用性,所以有了继承这个概念
继承解决了数据和逻辑的复制。通过继承,对象之间可以共享属性和方法,而不需要手动在每一个对象上添加属性和方法。
原型链继承
- 什么是原型链继承?
function Parent () {
this.balance = 100000000; // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) { // 你爹花钱的本领
this.balance -= number;
console.log(`花了${number}块`);
console.log(`余额${this.balance}块`);
}
function Child () { // 上帝之手造好了生产你的构造函数
}
Child.prototype = new Parent(); // 上帝之手把你和你爹强行绑定血缘关系,基因遗传,将Child的原型对象换成new Parent()生成的对象
var child1 = new Child(); // 你出生了,并通过原型链,继承了你爹的余额以及你爹花钱的本领
console.log(child1.balance); // 你也有可以获取到balance这个属性
child1.useMoney(6800000); // 你花680w在上海浦东新区北蔡镇买了套100平的大房子
上面代码就是原型链继承,有个 Parent
构造函数,有个 balance
余额属性,Parent.prototype
上有个 useMoney
方法,然后有个 Child
构造函数。
然后我们重点来看最后四句代码,下面将一句一句解析
Child.prototype = new Parent();
这句代码将
换成了
注意:[[Prototype]
就是实例对象上的__proto__
属性
即 Child.prototype
这个属性,原来存放的是 { constructor: ƒ Child(), [[Prototype]]: Object }
,现在变成了 { [[Prototype]]: Parent }
var child1 = new Child();
实例化一个 child1
对象,打印它
如果我们不继承 Parent
应该是这样
即 child1.__proto__
这个属性等价与 Child.prototype
(如果不理解这句,请看知识点一:原型链), child1.__proto__
原来存放的是 { constructor: ƒ Child(), [[Prototype]]: Object }
,现在变成了 { [[Prototype]]: Parent }
至此,我们清楚了 Child.prototype
和 child1
继承了 Parent
后的变化,与没有继承有什么区别。
console.log(child1.balance);
,如果我们在这句代码前面打印一下 child1
可以发现 child1
这个对象上是没有 balance
这个属性的,通过 原型链即__proto__
查找,上一级,发现上一级 Parent
上有这个属性,便拿过来了,所以这里的 balance 其实是 Parent 的。
child1.useMoney(6800000);
,同上,我们可以 child1
上是没有 useMoney 这个方法的,我们拿的其实是 child1.__proto__.__proto__
上的这个方法
-
原型链继承的特点
-
改变子构造函数的原型对象即
Child.prototype = new Parent();
-
所有实例对象共用同一个原型对象的属性和方法
-
-
原型链继承有哪些问题?
- 因为所有的实例对象(child1、child2...)用的都是同一个原型的属性,如果这个属性是引用类型,一个实例修改,其他地方都会修改
- 实例化的时候,不能向
Parent
传参 - 子类的原型上多了不需要的父类属性,存在内存上的浪费
盗用构造函数继承
- 什么是盗用构造函数继承?
function Parent () {
this.balance = 100000000; // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) { // 你爹花钱的本领
this.balance -= number;
console.log(`花了${number}块`);
console.log(`余额${this.balance}块`);
}
function Child () { // 上帝之手造好了生产你的构造函数
Parent.call(this); // 执行了 Parent 这个构造函数,call替换上下文环境为Child
}
var child1 = new Child(); // 你出生了,继承了你爹的余额
console.log(child1.balance); // 你也有balance这个属性
child1.useMoney(6800000); // 报错,你没有继承这个能力呜呜呜
上面代码就是盗用构造函数继承,与原型链继承的区别,我们一眼就可以看出来,改变原型对象那句代码没有了。我们在 Child
这个构造函数里面加了要一句这个代码:
Parent.call(this);
这句代码在 new Child();
时执行,就是执行了 Parent
这个构造函数,但是通过 .call(this)
,将执行 Parent
的上下文换成了 Child
的上下文,相当于在 new Child();
生成的这个新的对象里面执行了一句 this.balance = 100000000;
,然后我们把我这个 new
出来的对象赋给了 child1
,所以 child1
这个对象就会有 Parent
的属性,就像继承过来一样~这样就解决了上面原型链继承共享属性的问题了。
但是, child1.useMoney(6800000);
报错了
对的,我们没有继承到 Parent
上的方法,我们只是继承了 Parent
上的属性
-
盗用构造函数继承的特点
-
在子构造函数
Child
中执行Parent.call(this);
-
避免了引用类型的属性被所有实例共享
-
可以在
Child
中向Parent
传参即Parent.call(this, name, sex, ...);
-
无法继承
Parent
的方法,需要在Child
中定义
-
-
盗用构造函数继承有哪些问题?
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
组合继承
- 什么是组合继承?
function Parent () {
this.balance = 100000000; // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) { // 你爹花钱的本领
this.balance -= number;
console.log(`花了${number}块`);
console.log(`余额${this.balance}块`);
}
function Child () { // 上帝之手造好了生产你的构造函数
Parent.call(this); // 执行了 Parent 这个构造函数,call替换上下文环境为Child
}
Child.prototype = new Parent(); // 上帝之手把你和你爹强行绑定血缘关系,基因遗传,将Child的原型对象换成new Parent()生成的对象
Child.prototype.constructor = Child; // 不加上这句,Child.prototype.constructor就是new Parent()这个实例对象的原型对象上的constructor了,即ƒ Parent (name) {...}
var child1 = new Child(); // 你出生了,继承了你爹的余额
console.log(child1.balance); // 你也有balance这个属性
child1.useMoney(6800000); // 你花680w在上海浦东新区北蔡镇买了套100平的大房子
我们可以发现组合继承就是原型链继承和盗用构造函数继承的组合。代码就不是解释了,就是上面两个的结合,大家一看就能懂~
-
组合继承的特点
-
在子构造函数
Child
中执行Parent.call(this);
-
改变子构造函数的原型对象即
Child.prototype = new Parent();
-
避免了引用类型的属性被所有实例共享,继承
Parent
的方法
-
-
组合继承有哪些问题?
- 即原型链继承的遗留问题,子类的原型对象上多了不需要的父类属性,存在内存上的浪费
-
补充
我在学习完成第六种寄生式组合继承后,想到个办法解决组合继承这个
多余父类属性的问题
,就是将Child.prototype = new Parent();
这句代码换成Child.prototype = Parent.prototype;
,这样就可以解决这个问题,如下图:child1 -> Child.prototype -> Object.prototype -> null
而且这样还比寄生式组合继承少套了一层原型,大家可以对比一下,下图是寄生式组合继承
child1 -> Child.prototype -> Parent.prototype -> Object.prototype -> null
虽然解决了这个
多余父类属性的问题
,但是破坏了原型链
,本来Child
继承了Parent
,那么Child
的原型就应该是Parent
(Child.prototype -> Parent.prototype
)。
原型式继承
- 什么是原型式继承?
function Parent () {
this.balance = 100000000; // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) { // 你爹花钱的本领
this.balance -= number;
console.log(`花了${number}块`);
console.log(`余额${this.balance}块`);
}
var parent = new Parent();
function createObj(Parent) { // 接受的式一个实例对象,即Parent的实例对象parent
function Child(){}
Child.prototype = Parent;
return new Child();
}
var child1 = createObj(parent)
我们可以发现这不就是 Object.create
(只传入一个参数的情况) ,而且很像原型链继承,感觉就是一个 变种
。把继承的操作都封装在 createObj
这个函数中,并直接返回一个 Child
的实例对象, 我们就不能去定义 Child
的特有属性和方法了,实例化的 child1、child2...
就只会默认继承 Parent
的属性和方法,而且不像上面三种继承方法都会带上 Parent
和 Child
的属性或方法。
-
原型式继承的特点
-
改变子构造函数的原型对象即
Child.prototype = Parent;
-
所有实例对象共用同一个原型对象的属性和方法
-
将传入的对象作为创建的对象的原型,实例化的
child1、child2...
就只会默认继承Parent
的属性和方法 -
适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合
-
-
原型式继承有哪些问题?
- 因为所有的实例对象(child1、child2...)用的都是同一个原型的属性,如果这个属性是引用类型,一个实例修改,其他地方都会修改
- 实例化的时候,不能向
Parent
传参 - 子类的原型上多了不需要的父类属性,存在内存上的浪费
寄生式继承
- 什么是寄生式继承?
function object(Parent) {
function Child(){}
Child.prototype = Parent;
return new Child();
}
function createAnother(Parent){
let Child = object(Parent); // 通过调用函数创建一个新对象
Child.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return Child; // 返回这个对象
}
这怎么还调用了上面的原型式继承?确实,这感觉就是原型式的增强版。我们在原型式继承的时候不是发现 把继承的操作都封装在 object 这个函数中,并直接返回一个 Child 的实例对象, 我们就不能去定义 Child 的特有属性和方法了
,然后寄生式继承,就是在原型式外面再套一个函数,然后以某种方式增强这个对象,比如添加 Child.sayHi = function() {}
这个方法
-
寄生式继承的特点
-
改变子构造函数的原型对象即
Child.prototype = Parent;
-
所有实例对象共用同一个原型对象的属性和方法
-
可以对
Child
增强,添加属性或者方法
-
-
寄生式继承有哪些问题?
- 因为所有的实例对象(child1、child2...)用的都是同一个原型的属性,如果这个属性是引用类型,一个实例修改,其他地方都会修改
- 实例化的时候,不能向
Parent
传参 - 子类的原型上多了不需要的父类属性,存在内存上的浪费
寄生式组合继承
- 什么是寄生式组合继承?
function Parent () {
this.balance = 100000000; // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) { // 你爹花钱的本领
this.balance -= number;
console.log(`花了${number}块`);
console.log(`余额${this.balance}块`);
}
function Child () { // 上帝之手造好了生产你的构造函数
Parent.call(this); // 执行了 Parent 这个构造函数,call替换上下文环境为Child
}
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(Child, Parent) {
var prototype = object(Parent.prototype); // 创建一个prototype对象,prototype对象的原型对象为Parent.prototype
prototype.constructor = Child; // prototype对象的constructor指向Child
Child.prototype = prototype; // 将Child的原型对象替换成我们创建好的新的prototype对象
}
inheritPrototype(Child, Parent); // 继承
var child1 = new Child(); // 你出生了,继承了你爹的余额以及你爹花钱的本领
console.log(child1.balance); // 你也有balance这个属性
child1.useMoney(6800000); // 你花680w在上海浦东新区北蔡镇买了套100平的大房子
我们的组合继承已经很好了,但是他有个问题,组合继承会调用两次父构造函数,一次是设置子类型实例的原型的时候 Child.prototype = new Parent();
一次是在创建子类型实例的时候, new Child
时回执行 Parent.call(this, name);
,最后造成子实例对象的原型对象上多了不需要的父类属性,存在内存上的浪费。如下图所示:
所以就有了寄生式组合继承,就是为了将两次变成一次,依然达到继承父构造函数的属性的原型上的方法。
我们优化的是 Child.prototype = new Parent();
这句,原来这句代码确实让我可以拿到 Parent
原型对象上的方法,但是这样是直接 new
了一个 Parent
实例,挂到 Child.prototype
上,这里就又把Parent
的属性创建了一遍。
所以我们首先var prototype = object(Parent.prototype);
通过原型式创建出来 prototype
这个对象,这个对象的原型是 Parent.prototype
,自然我们可以通过 prototype
这个对象获取到Parent.prototype
上的方法了。 prototype.constructor = Child;
这一步将 prototype对象
的 constructor
指向 Child
,解决了默认 constructor
丢失的问题,Child.prototype = prototype;
, 将 Child
的原型对象替换成我们创建好的新的 prototype对象
,大功告成。
-
寄生式组合继承的特点
- 将父类的原型赋值给了子类的原型,并且将构造函数设置为子类
-
寄生式组合继承有哪些问题?
我暂时发现啥问题hhhhhhhh
《JavaScript高级程序设计》里说:寄生式组合继承可以算是引用类型继承的最佳模式。
-
补充一种写法
借用Object.create来写
function Parent () {
this.balance = 100000000; // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) { // 你爹花钱的本领
this.balance -= number;
console.log(`花了${number}块`);
console.log(`余额${this.balance}块`);
}
function Child () { // 上帝之手造好了生产你的构造函数
Parent.call(this); // 执行了 Parent 这个构造函数,call替换上下文环境为Child
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
var child1 = new Child(); // 你出生了,继承了你爹的余额以及你爹花钱的本领
console.log(child1.balance); // 你也有balance这个属性
child1.useMoney(6800000); // 你花680w在上海浦东新区北蔡镇买了套100平的大房子
Class继承
- 什么是Class继承?
class Parent {
constructor() {
this.balance = 100000000;
}
useMoney(number) { // 你爹花钱的本领
this.balance -= number;
console.log(`花了${number}块`);
console.log(`余额${this.balance}块`);
}
}
class Child extends Parent {
constructor() {
super()
}
}
let child1 = new Child()
console.log(child1.balance); // 你也有balance这个属性
child1.useMoney(6800000); // 你花680w在上海浦东新区北蔡镇买了套100平的大房子
child instanceof Parent // true
-
Class继承的特点
class
、constructor
、extends
、super
等关键字- 核心在于使用
extends
表明继承自哪个父类,并且在子类构造函数中必须调用super
,因为这段代码可以看成Parent.call(this)
-
Class继承如何实现的?
推荐文章
- 冴羽的JavaScript深入之从原型到原型链
- 为什么是
person1.prototype.constructor
而不是person1.constructor
指向构造函数?如果你这些概念很混乱,这篇文章,清晰好懂,能清楚这些东西为什么要这样设计,虽然这样设计不好懂,但是里面自有他的学问 - JavaScript 高级程序设计第 4 版PDF版
- 冴羽的JavaScript深入之继承的多种方式和优缺点
总结
通过对 原型链
和 继承
这两个概念的深入学习和回顾,我发现自己学的很浅,很多知识还需要去补充完善。学海无涯,大家一起加油~😊😊😊
转载自:https://juejin.cn/post/7151728566688808968