精读JavaScript模式(八),JS类式继承
一、前言
这篇开始主要介绍代码复用模式(原书中的第六章),任何一位有理想的开发者都不愿意将同样的逻辑代码重写多次,复用也是提升自己开发能力中重要的一环,所以本篇也将从“继承”开始,聊聊开发中的各种代码复用模式。
其实在上一章,我感觉这本书后面很多东西是我不太理解的,但我还是想坚持读完,在以后知识逐渐积累,我会回头来完善这些概念,算是给以前的自己答疑解惑。
二、类式继承VS现代继承模式
1.什么是类式继承
谈到类式继承或者类classical,大家都有所耳闻,例如在java中,每个对象都是一个指定类的实例,且一个对象不能在不存在对应类的情况下存在。
而在JS中其实并没有原生的类的概念,且JS的对象都可以随意的创建修改,并不需要依赖类。如果真要说,JS也有与类相似的构造函数,其语法也是通过new运算符得到一个实例。
假设工厂要生产一批杯子,接到的图纸信息是,杯子高12cm,杯口直径8cm,按照常识,我们不可能按照信息一个个的去做,最好的方法是直接做一个模具出来,然后灌浆批量生产。
这里每个杯子就是一个对象,一个实例,而这个提前定义好杯子信息的模具就是一个“类”,通过这个模具(类),我们就可以快速生产多个继承了模具信息(高度,直径等)的杯子(实例)了。
//不合理做法
let cup1 = {
height:12,
diameter:8
};
let cup2 = {
height:12,
diameter:8
};
// ......
let cupn1000= {
height:12,
diameter:8
}; //构造函数的做法
function MakeCup () {
this.length = 12,
this.diameter = 8;
};
let cup1 = new MakeCup();
let cup2 = new MakeCup();
//.........
let cup1000 = new MakeCup();
在上述代码中,MakeCup就是一个包含了所有实例共有信息的“类”,当然在JS中,我们更喜欢将这个类称为构造函数,毕竟MakeCup只是一个函数,而这种做法也只是与类相似,在这里我们将这种实现方式称为“类式继承”。
虽然我们在讨论类式继承,但还是尽量避免使用类这个字,在JS中构造函数或者constructor更为精准,毕竟每个人对于类的理解可能不同,将类与构造函数混合在一起容易混淆。
2.1类式继承1--默认模式(借用原型)
现在有下面两个构造函数Child()与Parent(),要求是通过Child来创建一个实例,并且这个实例要获得构造函数Parent的属性。我们假设通过inherit函数实现了需求。
function Parent(name) {
this.name = name || 'Adam';
};
Parent.prototype.say = function () {
console.log(this.name);
}; //空的child构造函数
function Child(name) {}; //继承
inherit(Child, Parent);
那么这个inherit函数如何实现,第一种思路,我们通过new Parent()得到一个实例,然后将Child函数的prototype指向该实例。
function inherit(C, P) {
C.prototype = new P();
}
inherit(Child, Parent); let kid = new Child();
kid.say();//Adam
很明显,构造函数Child继承了构造函数Parent的属性,所以由构造函数Child创建的实例自然也继承了这些属性,那么这个过程中间到底发生了什么?我们尝试跟踪原型链。
提前说明,为了方便理解,我们就假设对象啊,原型啊,都在同一空间内,当我们new Parent()时,就得到了一个实例,此时在内存中也新开了一个空间存放这个实例(下图中的2区域)。
构造函数Parent的原型链
现在我们尝试访问say()方法,但是2号空间并没有这个方法,但是通过_proto_指向Parent构造函数的prototype属性时,居然可以访问这个方法(1区域),这也是为什么我们总在前面说,建议将所有实例都需要用到的属性添加在prototype上,因为这样在每次new时,不用每次新开内存时都创建一次。
我们再来看看在使用inherit函数后,再使用let kid = new Child()创建实例时发生了什么,如下图。
继承之后的原型链
一开始Child构造函数是空的,什么属性都没有(上图1区域),当inherit函数执行时,Child函数的prototype属性指向了new Parent()对象,也就是2区域。
当我们new Child()得到一个实例kid并使用say方法时,由于自身没有,只能顺着_proto_找到了new Parent(),结果此对象也没有,重复了我们上面的图解步骤,继续顺着_proto_找到了Parent.prototype,终于找到了say()方法。
当say()方法被调用时,我们输出this.name,而此时this指向的是new Child(),结果new Child()又没有这个name属性,跟say一样,再找到2,再到1区域,顺利输出了Adam。这样是不是很清晰了呢?
我们再来为实例kid加点属性,看看原型链的变化,如下图
let kid = new Child();
kid.name = 'Patrick'
kid.say();//Patrick
继承并给实例添加属性后的原型链
当我们为实例添加了name属性时,其实只是为new Child()添加了name属性(区域3),并不会影响到new Parent(),这也是为什么说,每个实例都是一个独立的个体。当我们再次寻找say()方法时,还是一样的顺着_proto_找到了Parent.prototype,而当我们调用say方法输出name属性时,由于当前this指向kid,且kid自己有了name属性,于是顺利输出了Patrick。
而当我们delete kid.name删除掉之前赋值的Patrick时,再次调用,可以发现又输出了Adam,所以原型链继承就是,先从自己身上找,找不到,顺着_proto_向上,直至找到null停止(原型链的顶端是null)。
2.1原型链的优点与弊端
原型链继承的坏处在于,在继承父对象中你想要的属性的同时,也会继承父对象你不想要的属性,比如上方代码,我只想要父对象原型链上的say方法,结果你还是把构造函数中的name属性打包给我了。
上面这种模式的第二个坏处是,我不能给我最终的实例kid传递形参,假设我想最终输出时间跳跃,要么kid.name = ‘时间跳跃’,要么在父构造函数时就传递好参数Parent(‘时间跳跃’)。但这样我们得不停的修改Parent对象。
let kid = new Child('时间跳跃');
kid.say();//Adan
但如果一个属性或方法需要复用,它还是应该被添加在构造函数的原型prototype上;两点理由,第一,加在原型链上,new实例时不需要反复创建属性造成内存浪费,第二,简化构造函数的属性能减轻对不需要这些属性的实例的困扰,这也是原型链继承的好处。
3.类式继承2---借用构造函数
我们在上个例子中,遇到了无法通过子对象传参到父对象的问题,我们修改Child构造函数,如下,就可以实现子对象传参了。
function Child(a, b, c, d) {
Parent.apply(this, arguments);
};
let kid = new Child('时间跳跃');
console.log(kid.name);//时间跳跃
实现原理很简单,当我们new Child()时,通过apply再次应用了Parent函数,但Parent执行时此时的this指向了Child,也就是说Child想有name属性,可是我没有this.name的赋值操作,于是通过apply改变this的原理,借用了Parent函数中的this.name = name || 'Adam'这句代码,变相的来为Child构造函数添加属性,它等同于Child.name = '时间跳跃' || 'Adam'。
注意,此处只是借用这句代码来为Child构造函数添加属性,并没有修改Parent构造函数的属性,我们尝试输出Parent的实例,可以发现name属性仍为Adam。
let parent = new Parent();
let kid = new Child('时间跳跃');
console.log(kid, parent);//时间跳跃 Adam
我们在上面原型链的例子中,Child的实例去继承Parent的属性,说是继承,其实是通过原型链去找,虽然能拿到,但本质上这个属性还是别人的,自己手里没有,哪天Parent心情不好,把name属性给删了,Child啃老的行为也基本到头了。
但下面Child构造函数中使用apply的做法就不同了,我直接借用Parent的代码来为自己添加只属于自己的name属性,管你Parent怎么操作name属性,都跟我不相关。如果说第一种继承是引用,那么这种做法就更像是复制,我复制你有的属性,就不用引用了。
有点授人以鱼不如授人以渔的寓意,也有点深浅拷贝的意思。
我稍微修改了上面的代码,使用原型链指向继承得到了实例kid与使用call复制属性得到的实例son,分别输出了它们的hasOwnProperty判断,这里答案应该能明白了。
function Parent(name) {
this.name = ["echo", "时间跳跃", "听风是风"];
};
Parent.prototype.say = function() {
console.log(this.name);
};
//得到一个实例
let parent = new Parent();
function Child() {};
//修改Chilkd的原型指向
Child.prototype = parent;
function Son() {
Parent.call(this);
};
let kid = new Child();
let son = new Son();
console.log(parent.hasOwnProperty('name'));//true
console.log(kid.hasOwnProperty('name'));//false
console.log(son.hasOwnProperty('name'));//true
照理说,实例parent与实例son的name属性是自身的,不像kid这个没骨气的是靠引用地址借来的,我们分别修改三个实例的name属性,这段代码是我自己改的,当出个题,看看下面三个console分别输出什么,学继承,也当原型链的题来考考自己。
function Parent() {
this.name = ["echo", "时间跳跃", "听风是风"];
}; Parent.prototype.say = function() {
console.log(this.name);
}; let parent = new Parent();
function Child() {}; Child.prototype = parent; let kid = new Child(); function Son() {
Parent.call(this);
}; let son = new Son();
parent.name.push('二狗子');
son.name.push('狗剩');
kid.name.push('狗蛋');
console.log(parent.name);//?
let parent1 = new Parent();
let kid1 = new Child();
console.log(parent1.name);//?
console.log(kid1.name);//?
有没有觉得使用call或者apply的构造函数方式很厉害,但这种模式也有自己的弊端,虽然它借用了父构造函数的属性创建代码,很遗憾它并没办法继承父构造函数的prototype属性。我们写个简单的例子:
function Parent(name) {
this.name = name || "Adam";
};
Parent.prototype.say = function () {
console.log(this.name);
};
function Child (name) {
Parent.apply(this,arguments);
};
let kid = new Child('Patrick');
console.log(kid)//undefined
跟上面一样,我们通过原型图来看看这段代码继承关系。
尽管我们通过改变this指向为kid创建了name属性,但当找say方法时,由于此时的this指向Child,而Child的prototype并没有提供这个方法,所以无法找到。
有了解过new一个函数究竟发生了什么的同学,应该能发现借用构造函数的类式继承,做的就是我们模拟new运算符过程的一部分,具体可以阅读博主这篇文章:new一个对象的过程,实现一个简单的new方法
3.1利用构造函数模式实现多继承
利用构造函数加apply的方式,我们可以同时继承多个构造函数的属性,像这样:
function Cat () {
this.legs = 4;
this.say = function () {
console.log('喵~')
}
};
function Bird() {
this.wings = 2;
this.fly = true;
}
function CatWings() {
Cat.apply(this);
Bird.apply(this);
};
let miao = new CatWings();
console.dir(miao);
简直不能在方便,那么到这里位置,我们大概介绍了类式继承,默认模式,也就是构造函数的property指向你需要继承的实例,构造函数模式(结合call或apply)。
第二种构造函数模式的弊端在于不能继承原型,而添加在原型上的往往又是可复用的方法,这点比较遗憾。
但它也有好处,例如它能获得父对象成员的拷贝,不存在子对象修改能影响父对象的风险。那么这个遗憾我们能不能解决呢,如果在构造函数的模式上继承原型呢。下面的一种模式来解决这个问题。
JS模式这本书我可能最近,至少一周需要放放了,昨天跟组长说我们现在前端ES6规范都没用,确实low了点,所以我这边想尽快把ES6实践到项目中,这几天打算把ES6过一遍,所以想写写ES6的笔记。反正不管学什么,只要愿意学,总是没坏处的。
我为什么要写这段话呢,说的像我有很多读者,要提前说明一样。其实根本没人看我的博客啊...
那么这篇就写到这里了,接下来先放置一下,这本书还剩下两章,我会坚持读完,接下来好好学习一下ES6,为四月项目重构做准备。
精读JavaScript模式(八),JS类式继承的更多相关文章
- js类式继承模式学习心得
最近在学习<JavaScript模式>,感觉里面的5种继承模式写的很好,值得和大家分享. 类式继承模式#1--原型继承 方法 让子函数的原型来继承父函数实例出来的对象 <script ...
- 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类式继承模式#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/ ...
- javascript类式继承模式#3——借用和设置原型
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- javascript类式继承模式#1——默认模式
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- JS原型继承和类式继承
前言 一个多月前,卤煮读了一篇翻译过来的外国人写的技术博客.此君在博客中将js中的类(构造)继承和原型继承做了一些比较,并且得出了结论:建议诸位在开发是用原型继承.文中提到了各种原型继承的优点,详细的 ...
随机推荐
- MUI动态生成轮播图片
$$.ajax({ url:'http://localhost:8080/api/v1/food/listFeatureFood', type:'Get', xhrFields: {withCrede ...
- Unity3D编辑器扩展(六)——模态窗口
前面我们已经写了5篇关于编辑器的,这是第六篇,也是最后一篇: Unity3D编辑器扩展(一)——定义自己的菜单按钮 Unity3D编辑器扩展(二)——定义自己的窗口 Unity3D编辑器扩展(三)—— ...
- python列表和字符串的三种逆序遍历方式
python列表和字符串的三种逆序遍历方式 列表的逆序遍历 a = [1,3,6,8,9] print("通过下标逆序遍历1:") for i in a[::-1]: print( ...
- python装饰器同时支持有参数和无参数的练习题
''' 预备知识: …… @decorator def f(*args,**kwargs): pass # 此处@decorator 等价于 f = decorator(f) @decorator2 ...
- [转]MYSQL性能查看(命中率,慢查询)
网上有很多的文章教怎么配置MySQL服务器,但考虑到服务器硬件配置的不同,具体应用的差别,那些文章的做法只能作为初步设置参考,我们需要根据自己的情况进行配置优化,好的做法是MySQL服务器稳定运行了一 ...
- finalize方法的使用
finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法.这个方法在gc启动,该对象被回收的时候被调用.其实gc可以回收大部分的对象(凡是new出来的对象,g ...
- WPF 开发备忘录
运营日: select t.* from (select ab.*, bs.station_cn_name, bd.device_name from audit_tvm_cas ...
- text-decoration:[ text-decoration-line ] || [ text-decoration-style ] || [ text-decoration-color ] 默认值:
css3中字体装饰,多样化的界面效果,: [ text-decoration-line ]:指定文本装饰的种类.相当于CSS2.1的 text-decoration 属性, 可取值:none | un ...
- [转] Customizing OpenStack RBAC policies
http://www.florentflament.com/blog/customizing-openstack-rbac-policies.html OpenStack uses a role ba ...
- 透彻讲解,Java线程的6种状态及切换
Java中线程的状态分为6种. 1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法.2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running) ...