精读JavaScript模式(八),JS类式继承

1、前言前端

这篇开始主要介绍代码复用模式(原书中的第六章),任何一位有理想的开发者都不肯意将一样的逻辑代码重写屡次,复用也是提高本身开发能力中重要的一环,因此本篇也将从“继承”开始,聊聊开发中的各类代码复用模式。java

其实在上一章,我感受这本书后面不少东西是我不太理解的,但我仍是想坚持读完,在之后知识逐渐积累,我会回头来完善这些概念,算是给之前的本身答疑解惑。app

2、类式继承VS现代继承模式函数

1.什么是类式继承性能

 谈到类式继承或者类classical,你们都有所耳闻,例如在java中,每一个对象都是一个指定类的实例,且一个对象不能在不存在对应类的状况下存在。学习

而在JS中其实并无原生的类的概念,且JS的对象均可以随意的建立修改,并不须要依赖类。若是真要说,JS也有与类类似的构造函数,其语法也是经过new运算符获得一个实例。this

假设工厂要生产一批杯子,接到的图纸信息是,杯子高12cm,杯口直径8cm,按照常识,咱们不可能按照信息一个个的去作,最好的方法是直接作一个模具出来,而后灌浆批量生产。spa

这里每一个杯子就是一个对象,一个实例,而这个提早定义好杯子信息的模具就是一个“类”,经过这个模具(类),咱们就能够快速生产多个继承了模具信息(高度,直径等)的杯子(实例)了。prototype

//不合理作法
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只是一个函数,而这种作法也只是与类类似,在这里咱们将这种实现方式称为“类式继承”。code

虽然咱们在讨论类式继承,但仍是尽可能避免使用类这个字,在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并无提供这个方法,因此没法找到。

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,为四月项目重构作准备。

相关文章
相关标签/搜索