LLwyct 😁

类的创建和继承以及在ES5/6中的区别

JavaScript
继承

2021-05-25 22:02:00阅读量: 6

· · · 正文 · · ·

1 继承

1.1 原型链继承

说一下原型链继承把,这个方法其实让我纠结了好久,因为它与我在一篇官方的文章里讲的不一样。先直接看代码:

function Animal (name) {
  this.name = name || "Animal";
  this.sleep = function sleep () {
    console.log("sleeping");
  }
}
Animal.prototype.eat = function (food) {
  console.log(this.name + ' eating ' + food);
}
// 上面的部分是通用代码

function Cat() {}
Cat.prototype = new Animal();
let cat = new Cat("mimi");
cat.name = "mimi";
cat.eat("fish");

这是一段比较经典的原型链继承方法但是要注意,如果倒数第二行不主动再次声明name为mimi的话,会打印Animal eating fish,也就是说构造函数中的参数name=mimi,是没有用的。

在一篇比较权威的js教程中提到,尽量不要去覆盖函数的prototype属性,而是在其上做插入。比如上述代码第七行那样。如果直接覆盖会失去其原型中的constructor属性,因为即使是一个空函数,它也有它的 prototype 属性,而这个空函数的 prototype 就是一个只包含 constructor 的对象,这个 constructor 就是这个函数本身,即 F.prototype.constructor === F 返回真。

但是这里的原型链继承中,使用了这种覆盖的方法。经测试,Cat.prototype.constructor === Animal。这就比较能看出来问题了。我个人理解是,Fprototypeanimal对象,而其上是没有constructor的,所以去animal的原型上找,而animal的原型上的constructor就是Animal函数。所以我还是比较不太喜欢这种方法的。

而且这种方法有一个致命的缺点,就是如果父类的属性是引用类型,由于将父类的实例置给了子类的原型,因此不同的子类实例会使用同一个原型,引用类型会共享,一个改动,其他跟着一起变。

1.2 构造继承

使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(没有用到原型)。

function Cat(name) {
	Animal.call(this);
	this.name = name; 
}

let cat = new Cat("mimi");
cat.sleep()
// > mimi is sleeping
cat.eat("fish");
// > Uncaught TypeError: cat.eat is not a function

特点,可以实现多继承。 缺点:只能继承父类实例的属性和方法,不能继承父类原型上的属性和方法。每个子类都有父类实例函数的副本,影响性能。

核心代码是 Animal.call(this),创建子类实例时调用父类构造函数,于是子类的每个实例都会将SuperType中的属性复制一份。

我的理解,这种方法实际上没有用到原型,因为这里是用的Animal()函数去执行。所以,相当于借用Animal去创建了一个cat实例,而cat对象是没有原型链的。因此 cat.__proto__.__proto__ === Object.prototype。否则,应该是 cat.__proto__.__proto__ === Animal.prototype

1.3 实例继承、拷贝继承

不常用,略

1.4 组合继承

相当于原型链继承和构造继承的组合体。通过调用父类构造函数,继承父类的属性并保留传参的优点,然后将父类实例作为子类原型,实现函数复用。

function Cat (name) {
	Animal.call(this);
	this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
let cat = new Cat("mimi");
cat.sleep(); // mimi is sleeping
cat.eat("fish");// mimi eating fish

先说原型链继承,从名字就可以听出来,这种继承方法是有原型链在里面的,因此解决了构造继承中没有原型的问题,然后可以看到与构造继承相比,主动声明了Cat的原型,并且在原型链继承的基础上主动插入了被覆盖的构造函数constructor。

特点:可以继承实例属性/方法,也可以继承原型属性/方法。

缺点:

  • 第一次调用new Animal():给Animal.prototype写入属性name。
  • 第二次调用new Cat("mimi"):给cat写入属性name。

实例对象cat上的属性就屏蔽了其原型对象Cat.prototype的同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

1.5 寄生组合继承

通过寄生方式,砍掉父类的实例属性,这样在调用两次父类的构造时就不会初始化两次实例方法/属性。

function Cat (name) {
	Animal.call(this);
	this.name = name || 'Tom';
}

Cat.prototype = Object.create(Animal.prototype);

// Object.create's polyfill
let Super = function () {};
Super.prototype = Animal.prototype;
Cat.prototype = new Super();
//

Cat.prototype.constructor = Cat;

let cat = new Cat("mimi");
cat.sleep(); // mimi is sleeping
cat.eat("fish");// mimi eating fish

这是最成熟的方法,也是现在库实现的方法

可以看到这里用了一个空的函数在中间作为传递者,在组合继承中,子类Cat的原型就会创建一个Animal实例,由于Animal有可能内部有很多东西,那么就会需要很大内存,但其实这里是不需要的,因此这里使用一个空的函数作为中间传递者,这样创建cat实例的时候,就只需要一个空函数对象,节约了内存的。

2 instanceof

判断class的原型是否在obj的原型链上

obj instanceof class

3 isPrototypeOf

从名字就可以看出来用于判断前者是否是后者的原型链上的原型

Function.prototype.isPrototypeOf(obj)
Child.prototype.isPrototypeOf(child1)
Parent.prototype.isPrototypeOf(child1)

4 ES5 ES6中继承的区别

ES5的继承是通过先创建子类的实例,再将父类的方法或对象通过父类构造函数添加到子类上。

在ES6中,语言内部实现了extends功能。子类必须在构造函数中调用父类的super方法来创建实例。

其背后的原因是因为: 在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 [[ConstructorKind]]:"derived"。这是一个特殊的内部标签。

该标签会影响它的 new 行为:

  • 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this。

  • 但是当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作。

因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建。并且我们会收到一个报错。

这也从另一方面证实了,class并不是函数+原型的语法糖!

首要原因是:

  • class创建的函数具有特殊内部属性标记 [[FunctionKind]]:"classConstructor"

    编程语言会在许多地方检查该属性。例如,class方法只能用new调用。

  • class方法不可枚举。类定义将"prototype"中的所有方法的enumerable标志设置为false。这很好,因为如果我们对一个对象调用for..in方法,我们通常不希望class方法出现。

  • 类总是自动使用 use strict。

TOP

@copyright LLwyct 2021