继承实现方式
1. 原型链继承
更改原型对象的指向实现继承
function Film() {
this.name = '你好,李焕英!';
}
Film.prototype.getName = function() {
return this.name;
};
function Comedy() {
this.type = '喜剧片';
}
// 指定父类实例为子类实例的原型
Comedy.prototype = new Film();
// new 创建新实例对象经过了以下几步:
// 1.创建一个新对象
// 2.将新对象的proto指向构造函数的prototype原型
// 3.将构造函数的作用域赋值给新对象 (也就是this指向新对象)
// 4.执行构造函数中的代码(为这个新对象添加属性)
// 5.返回新的对象
Comedy.prototype.constructor = Comedy;
Comedy.prototype.getType = function() {
return this.type;
};
const comedy = new Comedy();
comedy.type; // '喜剧片'
comedy.getName(); // '你好,李焕英!'
comedy instanceof Film; // true
comedy instanceof Object; // true, js中所有类型都继承原型链顶端Object的实例
优点: 继承父类的属性(方法);实例既是子类的实例,也是父类的实例
缺陷: 单一继承; 所有实例共享父类实例的引用类型属性(方法),若属性被修改会影响所有实例; 无法向父类构造函数传参
2. 构造函数继承
借调父类构造函数实现继承
function Comedy(name, type) {
// 借调父类构造函数来增强子类实例
Film.apply(this, name);
this.type = type;
}
const comedy = new Comedy('你好,李焕英!', '喜剧片');
comedy.name; // '你好,李焕英!'
comedy instanceof Film; // false
优点: 可实现多继承,借调多个构造函数; 不存在引用类型属性共享问题,可以向父类构造函数传参
缺点: 无法获取父类原型上的属性,实例只是子类的实例
3. 组合继承(常用)
结合原型链继承和构造函数继承
function Comedy(name, type) {
// 借调父类构造函数来增强子类实例
Film.apply(this, name);
this.type = type;
}
Comedy.prototype = new Film();
Comedy.prototype.constructor = Comedy;
const comedy = new Comedy('你好,李焕英!', '喜剧片');
comedy instanceof Film; // true
优点: 继承了父类原型的属性,实现了构造函数传参;实例及是子类的实例,也是父类的实例
缺点: 调用了两次父类构造函数(耗内存)
4. 原型式继承
借助原型基于已有的对象创建新对象(相当于浅拷贝)
function Film(obj) {
function Comedy() {};
Comedy.prototype = obj;
return new Comedy();
}
const obj = {
name: '国产电影',
types: ['喜剧片', '动作片', '爱情片'],
};
const comedy1 = Film(obj);
comedy1.types.push('战争片');
const comedy2 = Film(obj);
comedy2.types; // ['喜剧片', '动作片', '爱情片', '战争片']
// Es6 Object.create实现同样继承效果
const comedy1 = Object.create(obj);
comedy1.types.push('战争片');
const comedy2 = Object.create(obj);
comedy2.types; // ['喜剧片', '动作片', '爱情片', '战争片']
缺点: 无法向父类构造函数传参, 如果父类是普通对象,引用类型属性依然被子例共享
5. 寄生组合式继承
解决组合继承两次调用父类的问题
function Comedy(name, type) {
// 借调父类构造函数来增强子类实例
Film.apply(this, name);
this.type = type;
}
(function() {
// 基于原型式继承,借助中间层构造函数实现继承,避免了组合继承中两次调用父类构造函数的问题
const Foo = function() {};
Foo.prototype = Film.prototype;
Comedy.prototype = new Foo();
})()
const comedy = new Comedy('你好,李焕英!', '喜剧片');
comedy instanceof Film; // true
修复了组合继承的问题
6. ES6类继承
使用extends关键字,实现组合继承方式同等的效果
// es6类声明
class Film {
constructor(name) {
this.name = name;
}
getName() {
console.log(this.name);
}
}
typeof Film; // 'function', 类声明只是构造函数的语法糖
Film === Film.prototype.constructor // true, 类本身指向构造函数
//ES5模拟类的实现(也称为自定义的类型创建)
function Film(name) {
this.name = name;
}
Film.prototype.getName = function() {
return this.name;
};
--------------------------
class Comedy extends Film {
constructor(type) {
super(name); // 等同于Film.call(this, name);
this.type = type;
}
getType() {
console.log(this.type);
}
getName() {
super.getName(); // '你好,李焕英!', 调用父类被覆盖的方法
console.log('重写父类方法');
}
}
const comedy = new Comedy('你好,李焕英!', '喜剧片');
comedy instanceof Film; // true
Object.getPrototypeOf(Comedy) === Film // true
继承与原型链
js对象包含一个__proto__属性(访问器属性,用于读取对象内部的[[Prototype]]);
通过obj.__proto__设置/读取一个对象的原型是不可取的,这会非常慢且严重影响性能
设置,使用Object.create()替代; 读取,使用Object.getPrototypeOf()替代。
生成原型链的不同方式
- 字面量/new Fun创建对象, 生成原型链
const a = ["yo", "whadup", "?"];
const obj = { a: 1 };
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype = {
addVertex: function(v){
this.vertices.push(v);
}
};
const g = new Graph();
// 原型链:
// a ---> Array.prototype ---> Object.prototype ---> null
// obj ---> Object.prototype ---> null
// g ---> Graph.prototype ---> Object.prototype ---> null
// Graph ---> Function.prototype ---> Object.prototype ---> null
所有对象都是Object的实例,都从Object.prototype继承属性;处于安全考虑(放置原型被污染),Object.prototype的原型被设计为不可变,只能为null,特别的,浏览器中,window、location原型也为不可变
- Object.create()创建对象,生成原型链
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
ES6-class
- 使用super指代父类原型对象
作为函数调用,指代父类构造函数;作为对象,指代父类的原型对象(普通方法中)或者父类(静态方法中)
const obj = {
toString: () => super.toString(), // 等价于 Object.prototype.toString()
};
obj.toString(); // [object Object]
- class中的继承链
class A {}
class B extends A {}
继承链
1. B.__proto === A // B作为一个对象继承自A
2. B.prototype.__proto === A.prototype(B.prototype === new A()) // B作为一个构造函数继承自A的实例
-----等价于基于原型链的继承-----
function A() {}
function B() {}
B.prototype = new A()
B.prototype.constructor = B
A.__proto__ === Function.prototype // true
B.prototype.__proto === A.prototype // true
A.prototype.__proto__ === Object.prototype // true
- 原生构造函数的继承
es6-class继承可实现原生构造函数的继承;es5原型链继承无法继承原生构造函数
ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取(原生构造函数的this无法绑定),导致无法继承原生的构造函数;ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。
总结
原型链查找属性比较消耗性能,查找不存在的属性时会遍历整个原型链,应尽量避免这样的操作
if(!g.hasOwnProperty(addVertex)) {
不执行操作
}
prototype 和 Object.getPrototypeOf()
prototype 是用于类的,而 Object.getPrototypeOf() 是用于实例的(instances),两者功能一致
const a1 = new A();
const a2 = new A();
// Object.getPrototypeOf(a1).doSomething === Object.getPrototypeOf(a2).doSomething === A.prototype.doSomething
// a1.doSomething() 等价于 Object.getPrototypeOf(a1).doSomething.call(a1) 等价于 A.prototype.doSomething.call(a1))
结论:
请注意原型链的长度,并在必要时将其分解,以避免可能的性能问题。此外,原生原型不应该被扩展,除非它是为了与新的 JavaScript 特性兼容