类与实例

类的声明

  1. // 类的声明(es5)
  2. var Animal = function () {
  3. this.name = "Animal";
  4. };
  5. // 类的声明(es6)
  6. class Animal2 {
  7. constructor() {
  8. this.name = "Animal2";
  9. }
  10. }

生成实例

  1. // 类的声明(es5)
  2. var Animal = function () {
  3. this.name = "Animal";
  4. };
  5. // 类的声明(es6)
  6. class Animal2 {
  7. constructor() {
  8. this.name = "Animal2";
  9. }
  10. }
  11. // 实例化
  12. console.log(new Animal()); // Animal { name: 'Animal' }
  13. console.log(new Animal2()); // Animal2 { name: 'Animal2' }

继承实现

本质就是原型链

概念

说到继承的概念,首先要说一个经典的例子。
先定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等,由汽车这个类可以派生出“轿车”和“货车”两个类,那么可以在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱。这样轿车和货车就是不一样的,但是二者都属于汽车这个类,这样从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系。
继承可以使得子类别具有父类的各种方法和属性,比如上面的例子中“轿车” 和 “货车” 分别继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性。在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法。
继承的基本概念就初步介绍这些,下面我们就来看看 JavaScript 中都有哪些实现继承的方法。

继承的实现方式

构造函数继承

特点: 子类实例共享父类引用属性的问题 创建子类实例时,可以向父类传递参数 可以实现多继承(call多个父类对象)

缺点: 实例并不是父类的实例,只是子类的实例 只能继承父类的实例属性和方法,不能继承原型属性/方法 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

  1. function Parent1() {
  2. this.name = "parent1";
  3. }
  4. Parent1.prototype.say = function () {};
  5. function Child1() {
  6. Parent1.call(this);
  7. this.type = "child1";
  8. }
  9. console.log(new Child1(), new Child1().say());

原型链继承

特点: 解决子类实例共享父类引用属性的问题 创建子类实例时,可以向父类传递参数 可以实现多继承(call多个父类对象)(不完美,没有父类方法)

缺点: 实例并不是父类的实例,只是子类的实例 只能继承父类的实例属性和方法,不能继承原型属性/方法 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

  1. function Parent2() {
  2. this.name = "parent2";
  3. this.play = [1, 2, 3];
  4. }
  5. function Child2() {
  6. this.type = "child2";
  7. }
  8. Child2.prototype = new Parent2();
  9. var s1 = new Child2();
  10. var s2 = new Child2();
  11. console.log(s1.play, s2.play); // [ 1, 2, 3 ] [ 1, 2, 3 ]
  12. s1.play.push(4);
  13. // TODO: 原型链中的原型对象共用
  14. console.log(s1.play, s2.play); // [ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ]
  15. console.log(s1.__proto__ === s2.__proto__); // true

组合方式继承

特点: 解决原型链实现继承的缺点

缺点: 父级构造函数执行了两次,影响性能

  1. function Parent3() {
  2. this.name = "parent3";
  3. this.play = [1, 2, 3];
  4. }
  5. function Child3() {
  6. Parent3.call(this);
  7. this.type = "child3";
  8. }
  9. Child3.prototype = new Parent3();
  10. var s3 = new Child3();
  11. var s4 = new Child3();
  12. s3.play.push(4);
  13. console.log(s3.play, s4.play); // [ 1, 2, 3, 4 ] [ 1, 2, 3 ]

组合继承 - 优化1

特点: 父类执行一次(子类实例化执行),Child4.prototype = Parent4.prototype解释(因为都是对象,都是引用类型,不会执行父级构造函数)

缺点: 如何区分一个对象是由它的子类实例化的,还是父类实例化的(s5是Child4还是Parent4直接实例化的)

  1. function Parent4() {
  2. this.name = "parent4";
  3. this.play = [1, 2, 3];
  4. }
  5. function Child4() {
  6. Parent4.call(this);
  7. this.type = "child4";
  8. }
  9. Child4.prototype = Parent4.prototype;
  10. var s5 = new Child4();
  11. var s6 = new Child4();
  12. console.log(s5, s6); // Parent4 { name: 'parent4', play: [ 1, 2, 3 ], type: 'child4' } Parent4 { name: 'parent4', play: [ 1, 2, 3 ], type: 'child4' }
  13. console.log(s5 instanceof Child4, s5 instanceof Parent4); // true true
  14. console.log(s5.constructor); // [λ: Parent4]

组合继承 - 优化2

Child5.prototype = Object.create(Parent5.prototype)解释:Object.create来创建一个中间对象,把两个原型对象区分开,这个中间对象还具备一个特性,它的原型对象是父类的原型对象,这样就可以连起来,通过给Child5的原型对象的constructor做修改,就可以正常区分开父类和子类的构造函数

  1. function Parent5() {
  2. this.name = "parent5";
  3. this.play = [1, 2, 3];
  4. }
  5. function Child5() {
  6. Parent5.call(this);
  7. this.type = "child5";
  8. }
  9. Child5.prototype = Object.create(Parent5.prototype);
  10. Child5.prototype.constructor = Child5;
  11. var s7 = new Child5();
  12. console.log(s7 instanceof Child5, s7 instanceof Parent5); // true true
  13. console.log(s7.constructor); // [λ: Child5]