资讯专栏INFORMATION COLUMN

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

BicycleWarrior / 729人阅读

摘要:无限增殖返回苹果返回香蕉返回返回使用的新语法方法会创建一个新对象,使用现有的对象来提供新创建的对象的。是新增的,用来规范原型式继承。这里将返回的新对象放到子类的原型对象里面,这样子类就拥有了父类的原型对象,也就实现了方法的继承。

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

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


图片来自:http://www.joyme.com/xinwen/2...

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

核心思想是:

使用原型链实现对原型属性和方法的继承。

通过借用改造函数来实现对实例属性的继承。

很像之前说过的组合使用构造函数模式和原型模式
// 父类构造函数
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 程序开发设计中比较常用的继承模式了。

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

二、继承-原型式继承


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

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

以一个对象实例来做模板进行复制,并且是借助原型链模式进行特殊复制

这种复制的方式会有一些特别的地方,例如,引用类型的值问题也是无法解决,复制可以借助 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" ]

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

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 发现一些有价值的东西


图片来自:http://www.cifnews.com/articl...

参考 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 高级程序设计第三版》 也并不是无敌的,当然,一下子知识量太大,我们吸收不了,所以这里不展开细说。
三、继承-寄生式继承

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


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

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

感觉像是原型式继承的升级版!
// 原型式继承的关键-复制
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 属性是没有变化的,可以将一些共享的属性放在里面来形成复制。

这里需要注意如果需要给添加的新函数传参的话,是不可以在”克隆“的时候传的,需要在外面使用的时候传。

这是一种比较简单的实现继承的方式,在不考虑自定义类型和构造函数的情况下,也算是一种有用的模式。
四、继承-寄生组合式继承
终于到了主角了。


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

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

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

其背后的思路是不必为了指定子类型的原型而调用超类型的构造函数。

使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

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

// 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来代替。

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


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

原型链没被切断,是因为是用了寄生(复制)的方式来进行超类原型对象的复制,整个复制的话,会保存它的原型链,然后将这个复制出来的原型对象直接赋值给子类,所以原型链是完整的。

没有出现之前组合继承的两次调用问题,是因为它有一个中间临时过渡的对象,省去了一次调用构造父类函数的机会。

没有出现引用类型的值共享问题,是因为在寄生(复制)之后才可以用原型链+构造函数的,这样就很好的隔离了超类和子类的引用类型的值的问题了。

总结

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

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

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

书不要读死,如果单纯读《javascript 高级程序设计第三版》是不可能完整了解 javascript 的,起码在面向对象这部分是不行的,很多网上的大(zhuang)牛(bi)都会叫你认真阅读这本书,但是对于初学者来说,基本是很难理解得到作者的思路和意思的,不是资质问题,是阅历和经验和知识含量不足的限制。

看不懂,不要紧,多看,多查阅资料,记得用 google 查,baidu 只会让你多了解一些广告罢了。

网上的文章质量也是参差不齐的,就算是我这篇装逼文,也是我自己觉得很好,但是未必能够面面俱到,但是人生本来就难以面面俱到,不是吗?重要的是,我用我的经验写了,你能看明白一些是一些,看不明白就当饭后尔尔罢了,不用纠结。

要自己做实验,自己输出一些结果,对比理论,对比别人的结果和分析,这样才能理解得好一些。

学习第一次发现完全懵逼的话,就尝试去组织一个脉络结构,就好像我这样,尝试做一个故事代入,一环扣一环来理解,虽然《javascript 高级程序设计第三版》这本书里面也有,但是感觉后面开始省略很多一部分了,以致迷失了。

不要怕,多学习,莫道前路无知己,天下谁人不识君,加油加油,也是自勉。

参考内容

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

原文转载:
https://www.godblessyuan.com/...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98341.html

相关文章

  • 我来重新学习 javascript 面向对象part 1)

    摘要:其实在之前的工厂模式里面,也存在这个问题,不过工厂模式更彻底,直接完全创建一个新对象,而构造函数模式的话只是方法会被重新创建。 我来重新学习 javascript 的面向对象(part 1) 很多job 的描述都说要求精通 javascript 面向对象编程,但是根据一般的套路,写精通其实就是熟练,写熟练其实就是一般,写一般其实就是懵逼! showImg(https://segment...

    myshell 评论0 收藏0
  • 我来重新学习js面向对象part 4)

    摘要:我是的可以改变函数的对象的指向抛出异常,没有这个因为子类和超类都是构造函数,那么就会有之前说的,构造函数在的时候,里面的方法函数会重复创建实例,导致资源浪费。 我来重新学习js 的面向对象(part 4) 续上一篇,随着业务越来越大,要考虑一些继承的玩意了,大千世界,各种东西我们要认识和甄别是需要靠大智慧去分门别类,生物学中把动植物按界、门、纲、目、科、属、种进行分类的方法可能是最有代...

    MAX_zuo 评论0 收藏0
  • 我来重新学习 javascript 面向对象part 3)

    摘要:二动态原型模式动态原型模式的特点是,在构造函数里面增加判断处理是否添加原型对象属性。他依然有一个严重的问题,就是原型对象和实例和构造函数之间没办法关联,这样不适合在有一定规模复杂度的程序开发中使用。 续上一集内容,有一些数据不需要共享的时候,但是又想实现共享数据处理,鱼与熊掌,都要兼得(老板就是这么霸气),那么经过工程师们的智慧交流,他们发现现实并非那么残酷,还有一些办法可取的,也就是...

    Elle 评论0 收藏0
  • 我来重新学习 javascript 面向对象part 2)

    摘要:先来说其实构造函数也有,原型对象有,实例有也有,或者更加笼统的说,所有对象都是有的。构造函数的原型对象上的会指向构造函数。由于属性是可以变更的,所以未必真的指向对象的构造函数,只是一个提示。 续上一集内容,通过构造函数的方式,成功地更新了生产技术,老板笑呵呵,工人少奔波,只是问题总比办法多,又遇到一个新问题,就是会造成一些资源的重复和浪费,那么经过工程师们的智慧交流,他们产生了一个新技...

    silvertheo 评论0 收藏0
  • 【全文】狼叔:如何正确学习Node.js

    摘要:感谢大神的免费的计算机编程类中文书籍收录并推荐地址,以后在仓库里更新地址,声音版全文狼叔如何正确的学习简介现在,越来越多的科技公司和开发者开始使用开发各种应用。 说明 2017-12-14 我发了一篇文章《没用过Node.js,就别瞎逼逼》是因为有人在知乎上黑Node.js。那篇文章的反响还是相当不错的,甚至连著名的hax贺老都很认同,下班时读那篇文章,竟然坐车的还坐过站了。大家可以很...

    Edison 评论0 收藏0

发表评论

0条评论

BicycleWarrior

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<