我来重新学习js 的面向对象(part 5)

这是最后的最后了,我会顺便总结一下各种继承方式的学习和理解。(老板要求什么的,管他呢)

一、继承-组合继承、伪经典继承

image

图片来自:http://www.joyme.com/xinwen/201711/22209132.html

这是一种将原型链和借用构造函数的技术结合起来的一种继承模式。不是假合体,是真合体!

核心思想是:

  • 使用原型链实现对原型属性和方法的继承。
  • 通过借用改造函数来实现对实例属性的继承。

很像之前说过的组合使用构造函数模式和原型模式

// 父类构造函数
function Food(name) {
  this.name = name;
  this.colors = ["red", "blue"];
}
// 父类原型对象的方法
Food.prototype.sayName = function() {
  console.log("我是" + this.name);
};
// 子类构造函数
function Fruit(name, place) {
  // 在构造函数里面调用父类搞糟函数,实现属性继承
  Food.call(this, name);
  this.place = place;
}
// 将父类的实例赋值给子类的原型对象,实现方法继承
Fruit.prototype = new Food();
// 添加子类原型对象的方法
Fruit.prototype.sayPlace = function() {
  console.log(this.place);
};

var food1 = new Fruit("苹果", "非洲");
food1.colors.push("black");
console.log(food1.colors); // 返回 [ 'red', 'blue', ' black' ]
food1.sayName(); // 返回 我是苹果
food1.sayPlace(); // 返回 非洲

var food2 = new Fruit("香蕉", "亚洲");
food2.colors.push("yellow");
console.log(food2.colors); // 返回 [ 'red', 'blue', 'yellow' ]
food2.sayName(); // 返回 我是香蕉
food2.sayPlace(); // 返回 亚洲
  • 可以看到超类构造函数 Food里的属性(namecolors)和超类构造函数的原型对象的方法( sayName )都能够被继承,并且对于引用类型的值也不会出现相互影响的情况,而子类构造函数的属性(place)和子类构造函数的原型对象的方法( sayPlace)也能够很好的使用,不会被覆盖,他们相互共享又相互独立。
  • 这里的属性继承是通过 call 方式,将父类的属性放到子类的构造函数里面,也就是借用构造函数模式。
  • 这里的方法继承是通过将父类的实例放到子类的原型对象上,也就是原型链模式。

也存在一些问题

  • 它需要调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部,
  • 也需要重写 constructor 属性,因为原型对象被重写了,constructor就丢失了
// 。。。。。。。。
// 子类构造函数
function Fruit(name, place) {
  // 在构造函数里面调用父类搞糟函数,实现属性继承
  Food.call(this, name); // 第二次调用父类构造函数
  this.place = place;
}
// 将父类的实例赋值给子类的原型对象,实现方法继承
Fruit.prototype = new Food(); // 第一次调用父类构造函数
Fruit.prototype.constrcutor=Fruit;//因重写原型而失去constructor属性,所以要对constrcutor重新赋值
// 添加子类原型对象的方法
Fruit.prototype.sayPlace = function() {
  console.log(this.place);
};
// 。。。。。。。

在一般情况下,这是我们在 javascript 程序开发设计中比较常用的继承模式了。

基于以上原因,我们需要引入寄生组合式继承来解决它的存在的问题,实现完美的继承。但是在了解它之前,需要先了解寄生式继承,而了解寄生式继承之前,需要了解原型式继承,他们是一个接一个的推导出来的。

二、继承-原型式继承

image

图片来自:http://acg.shunwang.com/2014/0702/61262.shtml

核心思想是借助原型可以基于已有的对象创建新对象,同时不必因此创建自定义类型。

  • 以一个对象实例来做模板进行复制,并且是借助原型链模式进行特殊复制
  • 这种复制的方式会有一些特别的地方,例如,引用类型的值问题也是无法解决,复制可以借助 es5语法也可以不借助,前者更加强大一些。
// 原型式继承的关键-复制
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

var food1 = {
  name: "苹果",
  colors: ["red", "blue"]
};
// 继承
var food2 = object(food1);

food2.name = "香蕉";
food2.colors.push("black");

//。。。。。。无限增殖

console.log(food1.name); // 返回 苹果
console.log(food2.name); // 返回 香蕉
console.log(food1.colors); // 返回 [ 'red', 'blue', 'black' ]
console.log(food2.colors); // 返回 [ 'red', 'blue', 'black' ]

2.1 使用 es5的新语法:Object.create()

Object.create()方法会创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

Object.create()是es5新增的,用来规范原型式继承。

如果单纯使用的话,效果跟之前的差别不大,参考下面例子:

var food1 = {
  name: "苹果",
  colors: ["red", "blue"]
};

var food2 = Object.create(food1);
food2.name = "香蕉";
food2.colors.push("black");

console.log(food1.name); // 返回 苹果
console.log(food2.name); // 返回 香蕉
console.log(food1.colors); // 返回 [ 'red', 'blue', 'black' ]
console.log(food2.colors); // 返回 [ 'red', 'blue', 'black' ]

如果注意使用它的第二个参数的话,差别就不一样了:


image
var food1 = {
  name: "苹果",
  colors: ["red", "blue"]
};

var food2 = Object.create(food1, {
  name: { value: "香蕉" },
  colors: { // !!!!!
    value: ["red", "blue", "black"]
  }
});

console.log(food1.name); // 返回 苹果
console.log(food2.name); // 返回 香江
console.log(food1.colors); // 返回 [ 'red', 'blue' ]  !!!!!
console.log(food2.colors); // 返回 [ 'red', 'blue', 'black' ]

可以看到引用类型的数值不会被共享,实现了很好的继承效果。

出现这个情况主要是因为如果使用 push 的话,还是操作同一个内存指针,使用Object.create的话,会重新添加到新创建对象的可枚举属性,不是同一个内存指针了。

2.2 发现一些有价值的东西

image

图片来自:http://www.cifnews.com/article/20498

参考 mdn 里面的介绍,会发现一些更有价值的东西,可以用 Object.create实现类式继承:

// Shape - 父类(superclass)
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父类的方法
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// Rectangle - 子类(subclass)
function Rectangle() {
  Shape.call(this); // call super constructor.
}

// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
  • Object.create会将参数里的对象添加到它返回的新对象的原型对象里面去,这样首先生成了一个新对象,并且该对象的原型对象是参数里的值,即Shape.prototype,新对象是临时的,暂时看不到,这个临时的新对象里面就包含了父类原型对象。
  • 这里将Object.create返回的新对象放到子类的原型对象里面,这样子类就拥有了父类的原型对象,也就实现了方法的继承。
  • 手动设置一个子类的原型对象的 constructor,是为了重新指定子类的构造函数名字,这样子类实例对象就可以查看到他的构造函数是谁,证明是某个实例来自于哪一个构造函数,这样代码和结构都会清晰。
  • 属性的继承还是有 call 实现。

还有更屌炸飞的东西,如果你希望能继承到多个对象,则可以使用混入的方式。

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do a thing
};
  • Object.assign 会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。
  • Object.assign 是在 ES2015 引入的,且可用 polyfilled。要支持旧浏览器的话,可用使用jQuery.extend()或者 _.assign()

与时俱进,红宝书《javascript 高级程序设计第三版》 也并不是无敌的,当然,一下子知识量太大,我们吸收不了,所以这里不展开细说。

三、继承-寄生式继承

在引入寄生组合式继承之前,需要了解什么是寄生式继承。

image

图片来自:https://2ch.hk/b/arch/2017-01-08/res/143930826.html

寄生式继承的思路跟寄生构造函数模式和工厂模式很类似,核心思想是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真得是它做了所有工作一样返回对象。

感觉像是原型式继承的升级版!

// 原型式继承的关键-复制
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

function createFood(original) {
  var clone = object(original);
  clone.sayName = function(name) {
    console.log(name);
  };
  return clone;
}

var food1 = {
  name: "苹果"
};
var food2 = createFood(food1);
console.log(food2.name); // 返回苹果
food2.sayName("香蕉"); // 返回香蕉
  • 可以看到 name 属性是没有变化的,可以将一些共享的属性放在里面来形成复制。
  • 这里需要注意如果需要给添加的新函数传参的话,是不可以在”克隆“的时候传的,需要在外面使用的时候传。

这是一种比较简单的实现继承的方式,在不考虑自定义类型和构造函数的情况下,也算是一种有用的模式。

四、继承-寄生组合式继承

终于到了主角了。

image

图片来自:https://www.9yread.com/book/10001318

寄生组合式继承的核心思想是:

  • 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
  • 其背后的思路是不必为了指定子类型的原型而调用超类型的构造函数。
  • 使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

好复杂的解释,先看看代码吧:

// object 函数可以用 Object.create 来代替。
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

// 这里是关键
function inheritPrototype(subType, superType) {
  // ①将超类原型放到一个临时的对象里面(创建超类型圆形的副本)
  var prototype = object(superType.prototype);
  // ②重新指定这个临时对象的constructor 为 子类构造函数
  prototype.constructor = subType;
  // ③将这个临时对象赋值给子类的原型对象
  subType.prototype = prototype;
}

function Food(name) {
  this.name = name;
  this.colors = ["red", "blue"];
}

Food.prototype.sayName = function() {
  console.log(this.name);
};

function Fruit(name, place) {
  Food.call(this, name);
  this.place = place;
}

inheritPrototype(Fruit, Food);
Fruit.prototype.sayPlace = function() {
  console.log(this.place);
};

var food1 = new Fruit("苹果", "非洲");
var food2 = new Fruit("香蕉", "亚洲");
console.log(food1.sayName()); // 返回 苹果
console.log(food1.sayPlace()); // 返回 非洲

food1.colors.push("black");
console.log(food1.colors); // 返回 [ 'red', 'blue', 'black' ]
console.log(food2.colors); // 返回 [ 'red', 'blue' ]

console.log(food1 instanceof Fruit); // 返回 true
console.log(food1 instanceof Food); // 返回 true
console.log(Fruit.prototype.isPrototypeOf(food1)); // 返回 true
console.log(Food.prototype.isPrototypeOf(food1)); // 返回 true

object 函数可以用 Object.create来代替。

借助这个图理解一下,这种继承模式拆开来看就是寄生式(复制)+组合式(原型链+构造函数)

image

图片来自https://www.jianshu.com/p/0045cd01e0be

  • 原型链没被切断,是因为是用了寄生(复制)的方式来进行超类原型对象的复制,整个复制的话,会保存它的原型链,然后将这个复制出来的原型对象直接赋值给子类,所以原型链是完整的。
  • 没有出现之前组合继承的两次调用问题,是因为它有一个中间临时过渡的对象,省去了一次调用构造父类函数的机会。
  • 没有出现引用类型的值共享问题,是因为在寄生(复制)之后才可以用原型链+构造函数的,这样就很好的隔离了超类和子类的引用类型的值的问题了。

总结

几乎涵盖了所有 javascript 的继承模式了:

image

图片来自:https://zhuanlan.zhihu.com/p/41656666

有几点是我觉得可以总结一下,前人栽树,后人乘凉:

  1. 书不要读死,如果单纯读《javascript 高级程序设计第三版》是不可能完整了解 javascript 的,起码在面向对象这部分是不行的,很多网上的大(zhuang)牛(bi)都会叫你认真阅读这本书,但是对于初学者来说,基本是很难理解得到作者的思路和意思的,不是资质问题,是阅历和经验和知识含量不足的限制。
  2. 看不懂,不要紧,多看,多查阅资料,记得用 google 查,baidu 只会让你多了解一些广告罢了。
  3. 网上的文章质量也是参差不齐的,就算是我这篇装逼文,也是我自己觉得很好,但是未必能够面面俱到,但是人生本来就难以面面俱到,不是吗?重要的是,我用我的经验写了,你能看明白一些是一些,看不明白就当饭后尔尔罢了,不用纠结。
  4. 要自己做实验,自己输出一些结果,对比理论,对比别人的结果和分析,这样才能理解得好一些。
  5. 学习第一次发现完全懵逼的话,就尝试去组织一个脉络结构,就好像我这样,尝试做一个故事代入,一环扣一环来理解,虽然《javascript 高级程序设计第三版》这本书里面也有,但是感觉后面开始省略很多一部分了,以致迷失了。
  6. 不要怕,多学习,莫道前路无知己,天下谁人不识君,加油加油,也是自勉。

参考内容

  1. 红宝书,javascript 高级程序设计第三版

原文转载:
https://www.godblessyuan.com/2018/08/%E6%88%91%E6%9D%A5%E9%87%8D%E6%96%B0%E5%AD%A6%E4%B9%A0%20js%20%E7%9A%84%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1-5.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容