类的创建和继承以及在ES5/6中的区别
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
。这就比较能看出来问题了。我个人理解是,F
的prototype
是animal
对象,而其上是没有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。