js中的继承实现

123 阅读5分钟

继承实现方式

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()替代。

生成原型链的不同方式

  1. 字面量/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原型也为不可变

  1. 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

  1. 使用super指代父类原型对象

作为函数调用,指代父类构造函数;作为对象,指代父类的原型对象(普通方法中)或者父类(静态方法中)

const obj = {
    toString: () => super.toString(),  // 等价于 Object.prototype.toString()
};

obj.toString();  // [object Object]
  1. 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
  1. 原生构造函数的继承

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 特性兼容

参考: js对象的__proto__属性原生构造函数的继承