手记

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

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

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

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

核心思想是:

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

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

// 父类构造函数
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 程序开发设计中比较常用的继承模式了。

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

二、继承-原型式继承

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

  • 以一个对象实例来做模板进行复制,并且是借助原型链模式进行特殊复制
  • 这种复制的方式会有一些特别的地方,例如,引用类型的值问题也是无法解决,复制可以借助 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 发现一些有价值的东西

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

三、继承-寄生式继承

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

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

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

// 原型式继承的关键-复制
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 属性是没有变化的,可以将一些共享的属性放在里面来形成复制。
  • 这里需要注意如果需要给添加的新函数传参的话,是不可以在”克隆“的时候传的,需要在外面使用的时候传。

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

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

终于到了主角了。

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

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

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

// 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/0045cd01e0be

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

总结

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

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

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

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

参考内容

  1. 红宝书,javascript 高级程序设计第三版
3人推荐
随时随地看视频
慕课网APP