OOP 相关概念复习
- 封装:将数据与方法封装在类的内部, 作为一个整体对外
- 继承:两个类建立父子关系, 子类获取父类部分成员
- 多态:继承而产生的相关的不同的类,其对象对同一方法可以做出不同响应
- 重写: 子类重新编写实现继承来的方法
- 重载: 一个类拥有多个同名方法
JavaScript 没有真正意义上的类与继承,只能通过原型与原型链去模拟。
原型链
原型链是由原型对象通过 __proto__ 属性连接以实现属性共享的对象链。
prototype 是函数声明时生成的显式原型属性,而 __proto__ 是指向所属类的原型的实例属性。

规则总结:
- 所有函数(包括 Function)自身,都是 Function 的实例。
- 原型链的下一级是上一级的实例。
- 原型链的终端为 Object.prototype,其上一级为 null。
js 中的类概念
类的组成
- 构造方法: 任意一个非箭头函数
- 成员: 根据挂载位置可分为实例、原型、静态
成员可以是数据或方法
类的特性
- 动态可继承: 原型的变更会影响到相关实例对象
- 可写可枚举可配置: 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 的原理:
- 生成对象
- 链接原型
- 调用构造方法改变 this 指向
- 返回对象
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
- 方案 A: 对父类进行了不必要的实例化
- 方案 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
}
}