精读JavaScript模式(九),JS类式继承与现代继承模式其二
壹 ❀ 引
二零一九年的三月二十号,我记录了精读JavaScript模式(八)这篇读书笔记,今天是二零二零年三月十五号,相差五天,其实也算时隔一年,我重新拿起了这本书。当前为什么没继续写下去呢,主要还是因为自己JavaScript基础薄弱,理解书中的知识需要耗费大量时间,所以与其在这耗时间,不如打好基础,这也是为什么后面一年内我写了不少深入学习JavaScript相关博客的原因。为啥现在又捡起来读呢,主要原因是上上周参与程序设计,对于一个工厂函数类图的光理解就花了一天,工厂函数,设计模式,我立马想起JavaScript模式不就是讲设计模式的书吗,在写这篇博客前,我还特意把第八篇讲类式继承的文章又读了一遍,不得不说!在学习了原型,call、apply、this等相关知识后,理解起来真的是非常顺利,那么本文将接着第八篇未说完的知识继续介绍类式继承,让我们开始。
贰 ❀ 类式继承的默认模式与构造函数模式
贰 ❀ 壹 复习默认模式与构造函数模式
先让我们对于第八篇的类式继承模式做个简单的复习,需求是这样,有两个构造函数Parent与Child:
function Parent(name){
this.name = name || 'echo';
};
Parent.prototype.sayName = function (){
console.log(this.name);
};
function Child(name){};
可以看到Parent中存在一个构造器属性(或者叫实例属性)this.name
以及原型属性sayName
,现在要求Child创造的实例都能获得Parent的属性。
这里我们引出了类式继承模式的第一种---默认模式,实现很简单,也就是将Child的原型指向由Parent创建的实例:
//类式继承--默认模式
Child.prototype = new Parent();
var child = new Child();
child.sayName();// echo
这种模式的缺陷是,Child除了继承了Parent的原型属性,还继承了new Parent()
之后的实例属性name
,这导致我们的Child并不支持传参,比如:
var child = new Child('听风是风');
child.sayName();// echo
这是因为Child函数中并无类似于this.name
相关代码,所以它在创建实例时,并没有修改name属性的权利。怎么办呢,这里我们可以借用Parent函数中的this.name
代码,怎么借用呢?我们可以通过构造函数中this指向实例的特性来做到这一点。
这里我们得先修改Child函数,Parent函数不用变,如下:
function Child(name) {
Parent.apply(this,arguments);
};
var child = new Child('听风是风');
console.log(chid.name);//听风是风
你看,在new Child()
时,函数内部的this指向实例child,我们利用apply方法,在调用Parent方法的同时,将Parent内部的this指向了child,从而达到了动态给实例child添加name属性的目的。
此时的类式继承仍然存在缺陷,比如当我们执行如下代码就会报错:
child.sayName()// 报错,并不存在sayName方法
原因是上述构造函数继承只是单纯继承了Parent的构造器属性,并未继承Parent的原型属性。
贰 ❀ 贰 类式继承--构造函数模式plus版
有同学可能想到了,我把默认模式与构造函数模式一起用,不就达到又能动态设置name属性,又能继承Parent原型的目的了,比如:
function Child(name) {
Parent.apply(this,arguments);
};
Child.prototype = new Parent();
var child = new Child('听风是风');
child.sayName();//听风是风
但这样的缺陷非常明显,其一,我们调用了两次构造函数Parent,其二,我们设置了两次name属性,打印实例child就非常清楚了:
一个name是自己的属性,还有一个name是我们在new Parent
时绑在Child原型上的属性。
虽然目的达到了,只是看起来不那么美观,而对于这个问题,书中再无给出优化方案,这里我给出优化方法,如下:
function Child(name) {
var child = Object.create(Parent.prototype);
Parent.apply(child, arguments);
return child;
};
var child = new Child('听风是风');
child.sayName();//听风是风
这里我们利用了构造函数如果有手动返回对象,则new运算符调用时会得到手动返回对象的特性,在创建手动返回对象child时我们优先继承了构造函数Parent的原型,再使用apply继承Parent的构造器属性。
聪明的同学马上想到了,这不就是一个简易版的模拟new运算符的方法吗!你看,我们学来学去,用到的知识始终是这一大块,我们将上面的Child方法修改成一个模拟new的方法,如下:
function Parent(name) {
this.name = name || 'echo';
};
Parent.prototype.sayName = function () {
console.log(this.name);
};
//模拟的new运算符
function new_(Parent, ...rest) {
var child = Object.create(Parent.prototype);
var child_ = Parent.apply(child, rest);
return typeof child_ === 'object' ? child_ : child;
};
//这是通过模拟new调用出来的实例
var child = new_(Parent, '听风是风');
child.sayName();//听风是风
//这是正常new得到的实例
var kid = new Parent('听风是风');
kid.sayName();//听风是风
console.log(child, kid);
通过打印两种方法得到的实例,可以看到效果是一模一样:
叁 ❀ 共享原型模式
共享原型模式不需要借用构造函数,正如同它的名字一样,就是将需要继承的属性统统加在原型上,而不是this上,之后我们只要将子对象的原型设置为父对象的原型即可。
function inherit(Child, Parent) {
Child.prototype = Parent.prototype;
};
这种模式的原型链非常短,所以在查找属性时会更快,缺点也非常明显,因为大家都共用的是同一条原型链,所以不管是父对象还是子对象,只要一方修改了原型属性,双方都会受到影响。
function inherit(Child, Parent) {
Child.prototype = Parent.prototype;
};
function Parent() { };
Parent.prototype.name = '听风是风';
function Child() { };
// Child继承Parent的原型
inherit(Child, Parent);
// Child修改原型
Child.prototype.name = '听风是风加油';
var parent = new Parent();
console.log(parent.name);//听风是风加油
肆 ❀ 临时(代理)构造函数模式
肆 ❀ 壹 一个简单的临时构造函数模式
共享原型模式固然好用,缺点咱们也说了,就是父子原型太紧密,修改起来影响很大。有没有什么好的做法弥补这个问题呢,那么就轮到临时构造函数模式出场了!
所谓临时构造函数模式,本质上就是通过创建一个空函数作为中间函数,空函数的原型将指向父对象的原型,而子对象的原型指向空函数的实例:
function inherit(Child, Parent) {
var F = function () { };
F.prototype = Parent.prototype;
Child.prototype = new F();
};
这种模式与我们一开始说的默认模式最大的区别在于,中间函数只继承了Parent的原型,所以Child的实例最终也只会继承Parent的原型属性。
来看一个完整的例子:
function inherit(Child, Parent) {
var F = function () { };
F.prototype = Parent.prototype;
Child.prototype = new F();
};
function Parent() {
this.name = 'Adam';
};
Parent.prototype.say = function () {
console.log('听风是风很棒');
};
function Child() { };
// Child继承Parent的原型
inherit(Child, Parent);
// Child修改原型
Child.prototype.say = function () {
console.log('听风是风还得继续加油');
};
// 并没有影响父对象的原型
var parent = new Parent();
parent.say();//听风是风很棒
你看,通过中间函数,我们让Child成功继承了Parent的原型,同时还隔断了两者原型的直接关系,你可以在Child原型上畅心所欲的添加属性,这都不会影响到Parent。
那么这种做法有啥用呢?比如我在js 手动实现bind方法,超详细思路分析!这篇文章中就使用了代理构造函数模式,又一个知识点对应上去了!
肆 ❀ 贰 存储父类(Superclass)
在上一种模式基础上,我们还可以添加一个指向原始父对象原型的引用,这就像其它预言中访问超类(Superclass)一样,有时候会特别方便。有同学就要问了,超类是啥,这里我们直接引用百度的解释:
超类在软件术语中,被继承的类一般称为“超类”,也有叫做父类。是继承中非常重要的概念,它和子类一起形象地描述了继承的层次关系。
这里对应到JavaScript中来,就是被继承的父对象。
书中推荐将存储父类原型的属性名称为uber
,因为super
是一个保留字。我们来看一个储存了父类原型的升级版临时构造函数模式:
function inherit(Child, Parent) {
var F = function () { };
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.uber = Parent.prototype;
};
function Parent() {
this.name = 'Adam';
};
Parent.prototype.say = function () {
console.log('听风是风很棒');
};
function Child() { };
// Child继承Parent的原型
inherit(Child, Parent);
var child = new Child();
child.say();//听风是风很棒
Child.uber.say();//听风是风很棒
肆 ❀ 叁 重置构造函数引用
上述的临时构造函数继承模式已经非常完美了,我们还需要做最后一件事,那就是重置构造函数Child 的constructor指向。
如果我们不重置Child的constructor指向会存在这样一个问题,就是Child创建的实例的constructor全部会指向Parent,这会导致一种实例都是Parent创建的错觉,还是上面的代码,我们输出如下代码:
console.log(child.constructor.name);//Parent
console.log(child.constructor === Parent);//true
我们来输出child,如下图:
这是因为构造函数Child的原型指向本质上还是Parent.prototype
,而我们知道每个构造函数原型的constructor属性都指向构造函数自己。
虽然我们使用了临时构造函数过度,但是当查找某个属性时,原型链还是会查找到构造函数Parent,从而获取了Parent原型的constructor的name字段。
重置构造函数的引用也非常简单,如下:
function inherit(Child, Parent) {
var F = function () { };
F.prototype = Parent.prototype;
//继承原型
Child.prototype = new F();
//储存父对象原型
Child.uber = Parent.prototype;
//重置子对象的constructor指向
Child.prototype.constructor = Child;
};
然后我们还是创建实例,再次输出constructor的name字段,可以看到这下就完全没问题了:
console.log(child.constructor.name);//Child
console.log(child.constructor === Child);//true
原书中指出,如果你想使用类式继承,代理函数
或者称之为代理构造函数
模式是目前最棒的做法。
最后,上述代理构造函数模式还存在一个问题,就是我们每次要让一个Child对象继承Parent对象时,每调用一次inherit
方法都会创建一个新的空函数Fn,最后一步的优化就是封装inherit
函数,达到只创建一次空函数,以后使用只是修改空函数原型的目的,如下:
var inherit = (function () {
var F = function () { };
return function (Child, Parent) {
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.uber = Parent.prototype;
Child.prototype.constructor = Child;
};
}());
伍 ❀ 总
那么到这里,关于类式继承的相关知识就介绍完毕了。这一章节的知识主要分为两大块,前者是介绍类式继承,后者则是介绍现代继承相关的知识,所谓现代继承模式,就是没有使用类的概念,这块我们在下篇文章中介绍。
如果你对于文中原型相关的知识理解起来有些吃力,特别推荐阅读博主关于原型相关的两篇博客,相信一定会让你有所收获,链接如下:
JS 疫情宅在家,学习不能停,七千字长文助你彻底弄懂原型与原型链,武汉加油!!中国加油!!(破音)
JS 究竟是先有鸡还是有蛋,Object与Function究竟谁出现的更早,Function算不算Function的实例等问题杂谈
对于文中的知识存在难以理解,或者有错误的地方,欢迎大家留言讨论,我会在第一时间回复大家。
那么本文结束。
精读JavaScript模式(九),JS类式继承与现代继承模式其二的更多相关文章
- 精读JavaScript模式(八),JS类式继承
一.前言 这篇开始主要介绍代码复用模式(原书中的第六章),任何一位有理想的开发者都不愿意将同样的逻辑代码重写多次,复用也是提升自己开发能力中重要的一环,所以本篇也将从“继承”开始,聊聊开发中的各种代码 ...
- js类式继承模式学习心得
最近在学习<JavaScript模式>,感觉里面的5种继承模式写的很好,值得和大家分享. 类式继承模式#1--原型继承 方法 让子函数的原型来继承父函数实例出来的对象 <script ...
- JavaScript进阶(九)JS实现本地文件上传至阿里云服务器
JS实现本地文件上传至阿里云服务器 前言 在前面的博客< JavaScript进阶(八)JS实现图片预览并导入服务器功能>(点击查看详情)中,实现了JS将本地图片文件预览并上传至阿里云服务 ...
- JavaScript中的类式继承和原型式继承
最近在看<JavaScript设计模式>这本书,虽然内容比较晦涩,但是细品才发现此书内容的强大.刚看完第四章--继承,来做下笔记. 书中介绍了三种继承方式,类式继承.原型式继承和掺元类继承 ...
- js原生设计模式——2面向对象编程之继承—new类式继承
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8&qu ...
- js原生继承之——类式继承实例(推荐使用)
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8&qu ...
- 深入JavaScript对象(Object)与类(class),详细了解类、原型
JavaScript基于原型的对象机制 JavaScript原型上的哪些事 一.JavaScript基于原型的对象机制 JavaScript对象是基于原型的面向对象机制.在一定程度上js基于原型的对象 ...
- 浅谈MVVM模式和MVP模式——Vue.js向
浅谈MVVM模式和MVP模式--Vue.js向 传统前端开发的MVP模式 MVP开发模式的理解过程 首先代码分为三层: model层(数据层), presenter层(控制层/业务逻辑相关) view ...
- javascript类式继承模式#2——借用构造函数
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- javascript类式继承模式#4——共享原型
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
随机推荐
- BTC-实现
BTC-实现 Transaction-based ledger(比特币是基于交易的账本模式) Account-based ledger(以太坊是基于账户的账本模式) UTXO Unspent Tran ...
- AvaloniaUI 取消标题栏,无边框无最大最小化,
AvaloniaUI 取消标题栏,无边框无最大最小化, 创建一个Window控件 并且在Window中添加以下代码 ExtendClientAreaToDecorationsHint="Tr ...
- 如何使用单纯的`WebAssembly`
一般来说在.net core使用WebAssembly 都是Blazor ,但是Blazor渲染界面,.net core也提供单纯的WebAssembly这篇博客我将讲解如何使用单纯的WebAssem ...
- Laravel : 模糊查询 where orWhere
Banner::where('title', 'like', "%{$keyword}%")->orWhere('introduce', 'like', "%{$k ...
- Linux-运行级别-init
- TCP连接状态的多种判断方法
前言 在TCP网络编程模型中,无论是客户端还是服务端,在网络编程的过程中都需要判断连接的对方网络状态是否正常.在linux系统中,有很多种方式可以判断连接的对方网络是否已经断开. 通过错误码和信号 ...
- [转帖]如何不耍流氓的做运维之-SHELL脚本
https://www.cnblogs.com/luoahong/articles/8504691.html 前言 大家都是文明人,尤其是做运维的,那叫一个斯文啊.怎么能耍流氓呢?赶紧看看,编写SHE ...
- MySQL 8.2.0部署安装验证
MySQL 8.2.0部署安装验证 背景 昨天捯饬了半天Oracle23c Free版本发现自己白忙活了. 然后想着继续看一下 MySQL8.2. 看看会不会又继续白忙活 下载与安装 https:// ...
- [转帖]麒麟v10上部署TiDBv5.1.2生产环境的最佳实践
https://tidb.net/book/tidb-monthly/2022/2022-07/usercase/tidb-v5-1-2 前言 笔者最近在一个银行项目中做 PoC 测试,由于客户选择 ...
- [转帖]Nginx应用调优案例
https://bbs.huaweicloud.com/blogs/146367 [摘要] 1 问题背景nginx的应用程序移植到TaiShan服务器上,发现业务吞吐量没有达到硬件预期,需要做相应调优 ...