面向对象的三大特征、封装、继承、多肽,js中同样有这三种特征,js是一门弱语言,俗称解释性语言,通常来说比起高级语言,他没有严格的类型约束,为了让代码写得更健壮,维护性更强,因此有了ts约束,而继承是能让代码更加通用,让你的代码更加的抽象。

往往在项目中都会看到有用class,或者OOP思想去组织业务代码,本篇只做项目中常用到的继承以及对不同继承方式的回顾,也是再次加深对继承的一些理解,希望你在项目中有些帮助和思考。

正文开始...

构造函数

我们通过构造函数构建对象

function Animal(name) {
  this.name = name;
  this.getName = function () {
    return this.name;
  };
}
const tigger = new Animal('tigger');
const cat = new Animal('cat');

我们通过new 构造函数()方式新建了两个对象tiggercat,其实我们会发现,相当于有多少对象,我就要实例化多少个对象。并且实例化的对象都相互独立,互不影响。现在我想triggercat拥有同样的属性或者方法呢?

可以利用原型链prototype共享方法,

...
Animal.prototype.say = function() {
  console.log('hello,'+this.name);
}
cat.say(); // hello, cat
tigger.say(); // hello,trigger

当使用new Animal('cat')或者new Animal('tigger'),你会发现同样的事情,我们实例化了多次,因为这样做,tiggercat并不相等,那么如何可减少内存开销呢。

我们可以利用单件模式一个全局变量去处理,举个例子

let instance;
function Animal(name) {
  this.name = name;
  this.getName = function () {
    return this.name;
  };
  if (!instance) {
    instance = this;
  }
  this.getInstance = function () {
    return instance;
  };
}
const cat = new Animal('cat').getInstance();
const trigger = new Animal('trigger').getInstance();

console.log(cat === trigger); // true

或者在构造函数上绑定一个静态属性,这样比定义全局变量要好得多,推荐下面这种方式

function Animal(name) {
  this.name = name;
  this.getName = function () {
    return this.name;
  };
  if (!Animal.instance) {
    Animal.instance = this;
  }
  return Animal.instance;
}
const cat = new Animal('cat');
const trigger = new Animal('trigger');
console.log(cat === trigger); // true

但是这样我们会发现const trigger = new Animal('trigger')实际上无论实例化多少个,都只会返回首次实例化的对象,对于不同场景还是得特殊处理。

自定义一个数组,完全继承数组所有特性

function MyArray() {
  this.ret = [];
}
MyArray.prototype = new Array();
// 指定构造函数
MyArray.prototype.constructor = MyArray;
var mine = new MyArray();
console.log(mine instanceof Array); // true
// 以上等价
MyArray.prototype.isPrototypeOf(mine); // true

constructor

查找对象的构造函数

function Print() {}
const print = new Print();
console.log(Print.prototype.constructor === print.constructor); // true
console.log(Object.getPrototypeOf(print) === Print.prototype); // true

判断print的构造函数是不是Print

...
print instanceof Print  // true

也可以用这个来代替

...
Print.prototype.isPrototypeOf(print); // true

原型继承法

所有对象共享一个原型对象,基于构建器工作模式,将父类的prototype直接赋值给子类的prototype

// 父构造函数
function Parent() {
  this.ParentName = 'parent';
}
Parent.prototype.cname = '123';
Parent.prototype.getName = function () {
  console.log(this.cname); // 666
};
// 子构造函数
function Child() {
  this.childname = 'childname';
  // this.ParentName = '888';
}
Child.prototype = Parent.prototype;
// Child.prototype.cname = '666'; 会修改父类的cname
const c = new Child();
console.log(c.ParentName, c.childname, c.cname);
// undefined childname  123

从打印里我们可以看出,子类可以访问父类prototype上的属性或者prototype方法,但是父类自身属性或者自身方法不能访问,但是,我们注意到如果子类prototype属性有父类相同的prototype属性名时,此时子类会覆盖父类prototype的属性。子类自身属性与父类自身属性同名时,此时子类访问就会有值,访问的是自身属性,c.ParentName打印就会是888

于此同时子类prototype修改会同时修改父类的prototype

临时构造器

现在我有一个需求,子类只继承父类的prototype,不需要继承父类自身本身的属性,举个栗子佐证下

  function extends(Child, Parent) {
      const F = function() {};
      F.prototype = Parent.prototype;
      Child.prototype = new F();
      // 将Child的构造函数指定成Child
      Child.prototype.constructor = Child;
  }
  function Parent() {
    this.parentName = '123'
  }
  Parent.prototype.age = 18;
  function Child() {
    this.childName = 'childname'
  }
  // Child.prototype.age = 666; // 并不会修改父类age属性
  extend(Child, Parent);
  const c = new Child();
  console.log(c.age, c.childName, c.parentName)
  // 18, childname,undefined

我们可以发现实际上利用extends方法,利用了一个中间的F构造函数,通过F.prototype = Parent.prototype,然后将Child.prototype = new F(),与上面原型继承不同的是,修改子类prototype与父类相同的属性时,并不会修改父类prototype的属性。本质上就是借鸡生蛋,借用了Fprototype,不直接修改父类的prototype

原型属性拷贝继承

将父类的prototype属性值拷贝给子类

function extends(Child, Parent) {
  const c_proto = Child.prototype;
  const p_proto = Parent.prototype;
  for (let key in p_proto) {
    c_proto[key] = p_proto[key]
  }
}
function Child () {
  this.name = 'child'
}
function Parent() {
  this.name = 'parent'
}
Parent.prototype.money = 100;
extends(Child, Parent);
const c = new Child();
console.log(c.money, c.name) // 100, child

注意,只会继承父类prototype属性,父类自身属性并不会继承,因此这种与临时构造器功能上如出一辙,子类并不能修改父类自身的属性。

寄生继承

function extends2(target) {
  const F = function () {};
  F.prototype = target;
  return new F();
}
function Parent() {
  this.name = 'parent';
}
Parent.prototype.age = 100;
const child = extends2(Parent.prototype);
const parent = new Parent();
// child.__protototype__.age = 88;
console.log(child.age, child.name); // 100, undefined
console.log(parent.age, parent.name); // 100, parent

这种继承本质上仍然是用利用父类的prototype赋值给了一个中间构造函数Fprototype,他的弊端是并不能访问父类的自身属性与自身方法, 但是child.__protototype__.age会修改父类的prototype上的同名属性。

构造函数继承,利用 call 继承【构造器继承】

// 父构造函数
function Parent() {
  this.name = 'parent';
  this.say = function () {
    console.log('hello,' + this.name);
  };
}
Parent.prototype.age = 10;
// 子构造函数
function Child() {
  Parent.call(this);
}
const c = new Child();
console.log(c.name); // parent
console.log(c.age); // undefined
console.log(c.say()); // hello parent

我们注意到c.age返回的是undefined,因为age不是构造函数本身的属性或者方法,在构造函数prototype的方法或者属性无法访问,如果我需要访问呢?

function Parent() {
  this.name = 'parent';
  this.say = function () {
    console.log('hello,' + this.name);
  };
}
Parent.prototype.age = 10;
function Child() {
  Parent.call(this);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.age = 888;
const c = new Child();
const p = new Parent();
console.log(c.age); // 888
console.log(c.name); // parent
console.log(p.age); // 10

我们就加了一行代码实现了Child.prototype = Object.create(Parent.prototype),这种方式子类与父类的耦合非常低,子类修改与父类同名prototype的属性并不会影响父类。

原型链继承

实际上还有一种更简单的继承,让子类的prototype等于父类的实例,也称为原型链继承

function Parent() {
  this.name = 'parent';
  this.say = function () {
    console.log('hello,' + this.name);
  };
}
function Child() {}
Child.prototype = new Parent();
const c = new Child();
const p = new Parent();
console.log(c.name); // 'parent'

多重继承

function A() {
  this.a = 11;
}
function B() {
  this.b = 22;
}
function C() {
  A.call(this);
  B.call(this);
}
C.prototype = Object.create(A.prototype);
C.prototype.constructor = C;
// 合并B的prototype
Object.assigin(C.prototype, B.prototype);
const c = new C();

extends 继承

class Parent {
  constructor() {
    this.name = 'Maic';
  }
  getName() {
    return this.name;
  }
}
class Child extends Parent {
  constructor() {
    super();
    this.age = 10;
  }
}
const c = new Child();
console.log(c.name); // Maic
console.log(c.getName()); // Maic
console.log(c.age); // 10

注意constructor中有super()调用

构造函数的变体,es6 的 class

// utils.js
class Utils {
  static instance = null;
  formateDate() {}
  formateUrl() {
    console.log('formateUrl');
  }
  static getInstance() {
    if (!this.instance) {
      this.instance = new Utils();
    }
    return this.instance;
  }
}
export default Utils.getInstance();

引入utils.js

import Utils from './utils';

console.log(Utils.formateUrl());

总结

1、obj instanceof A判断一个对象的构造函数(A 是否是 obj 的构造函数),如果是则返回true、不是返回false

2、A.prototype.isPrototypeOf(obj)判断构造函数A是不是obj实例对象的构造函数

3、常用的几种继承、原型继承法临时构造器原型属性拷贝继承寄生继承构造器继承【call】原型链继承extends继承 4、call父类构造函数在子类构造函数调用call实现继承,父类除了了自身属性和自身方法能被继承访问,父类原型的方法子类无法访问

5、Child.prototype = Object.create(Parent.prototype)实现继承父类

扫二维码,关注公众号
专注前端技术,分享web技术
加作者微信
扫二维码 备注 【加群】
教你聊天恋爱,助力脱单
微信扫小程序二维码,助你寻他/她
专注前端技术,分享Web技术
微信扫小程序二维码,一起学习,一起进步
前端面试大全
海量前端面试经典题,助力前端面试