图文并茂捋清JS OOP

553 阅读3分钟

OOP 相关概念复习

  1. 封装:将数据与方法封装在类的内部, 作为一个整体对外
  2. 继承:两个类建立父子关系, 子类获取父类部分成员
  3. 多态:继承而产生的相关的不同的类,其对象对同一方法可以做出不同响应
  4. 重写: 子类重新编写实现继承来的方法
  5. 重载: 一个类拥有多个同名方法

JavaScript 没有真正意义上的类与继承,只能通过原型与原型链去模拟。

原型链

原型链是由原型对象通过 __proto__ 属性连接以实现属性共享的对象链。

prototype 是函数声明时生成的显式原型属性,而 __proto__ 是指向所属类的原型的实例属性。

规则总结:

  1. 所有函数(包括 Function)自身,都是 Function 的实例。
  2. 原型链的下一级是上一级的实例。
  3. 原型链的终端为 Object.prototype,其上一级为 null。

js 中的类概念

类的组成

  • 构造方法: 任意一个非箭头函数
  • 成员: 根据挂载位置可分为实例、原型、静态

    成员可以是数据或方法

类的特性

  1. 动态可继承: 原型的变更会影响到相关实例对象
  2. 可写可枚举可配置: es5 以后可编辑属性的这三个配置
// es5 写法
function Cat(name) {
  this.name = name; // 实例成员, 挂载于 this
}
Cat.prototype.jump = function() {}; // 原型成员, 挂载到原型对象上
Cat.generate = function() {}; // 静态成员, 挂载于类的构造方法上

// es6 提供了相应的语法糖
class Cat {
  constructor(name) {
    this.name = name;
  }
  jump() {}
  static generate() {}
}

// babel 转换 es5-loose
("use strict");
var Cat = /*#__PURE__*/ (function() {
  function Cat(name) {
    this.name = name;
  }

  var _proto = Cat.prototype;

  _proto.jump = function jump() {};

  Cat.generate = function generate() {};

  return Cat;
})();

实例化

New 的原理:

  1. 生成对象
  2. 链接原型
  3. 调用构造方法改变 this 指向
  4. 返回对象
function fakeNew(constructor) {
  var obj = {};
  obj.__proto__ = constructor.prototype;
  constructor.apply(obj, Array.from(arguments).slice(1, arguments.length));
  return obj;
}
function Foo() {}
Foo.getName = function() {
  console.log("1");
};
Foo.prototype.getName = function() {
  console.log("2");
};
// 细节: 使用参数表调用的优先级更高
new Foo.getName(); // -> 1
new Foo().getName(); // -> 2

继承

寄生继承

寄生继承+构造方法继承是 js 继承的最佳实践. es5 已经有易用的 API, 为了理解其具体实现, 这里也给出了 es3 的具体实现方案.

// es5实现
Cat.prototype = Object.create(Animal.prototype, {
  constructor: "Cat"
});

// es3实现: 创建继承中间类桥接进行桥接
const inherit = (function() {
  // 在使用较多的情况下使用 IIFE 创建闭包以避免重复创建 Temp 的开销
  function Temp() {}
  return function(Child, Parent) {
    Temp.prototype = Parent.prototype;
    Child.prototype = new Temp();
    Child.prototype.constructor = Child;
  };
})();

原型链继承

看完最佳实践回过头来看原型链继承,理解其存在的缺陷.

var Cat;
function Animal() {
  this.items = [];
}

// A) 原型链继承: 父类实例作为原型, 子类实例共享该实例
// Cat 实例 --> __proto__ --> Animal 实例 --> __proto__ --> Object: { constructor: Animal, __proto__: Object }
Cat = function() {};
Cat.prototype = new Animal();
new Cat().items.push(0);
new Cat().items; // [0];

// B) 父类原型作为原型, 略过父类构造方法
// Cat 实例 --> __proto__ --> Object: { constructor: Animal, __proto__: Object }
Cat = function() {};
Cat.prototype = Animal.prototype;
new Cat().items; // undefined

// C) 寄生继承方案
// Cat 实例 --> __proto__ --> Animal实例 --> __proto__ --> Object: { constructor: Animal, __proto__: Object }
Cat = function() {};
Cat.prototype = Object.create(Animal.prototype);
new Cat().items; // undefined
  1. 方案 A: 对父类进行了不必要的实例化
  2. 方案 B: 严格来说不能称为继承, 而是给父类新增了一个构造方法.

从原型链结果上看, A, C 结果一致; 从调用过程上看, B,C 都绕过了父类的构造方法.

寄生继承相对于原型链继承来说优势在于去除了实例化开销, 或说减少了(es3 实现使用了一个空构造方法桥接).

构造方法继承

构造方法继承是继承必不可少的一个步骤, 实现起来也很简单. 使用 call、apply 模拟 super 即可.

function Cat() {
  Animal.apply(this, arguments);
}

继承组合关系梳理

ES6 实现区别

es6 之前的继承,实质上是先创建子类的实例对象 this,然后再应用父类的构造方法。而 es6 的继承机制则不同,需要先调用 super 才能拿到父类构造的 this,否则则会报引用错误。

特别的,es6 继承中子类可以继承父类的静态方法。

class Cat extends Animal {
  constructor(name) {
    // super();
    this.name = name; // ReferenceError
  }
}