3

众所周知,Javascript是一门面向对象的语言,如果说针对面向对象来发问的话,我会想到两个问题,在js中,类与实例对象是如何创建的,类与实例对象又是如何实现继承的。

面向对象

如何声明一个类

ES5中,还没有类的概念,而是通过函数来声明;到了ES6,有了class关键字,则通过class来声明

      // 类的声明
      var Animal = function () {
          this.name = 'Animal';
      };

      
      // es6中class的声明
      class Animal2 {
          constructor () {
              this.name = 'Animal2';
          }

如何创建对象

1.字面量对象
2.显示的构造函数
3.Object.create

      // 第一种方式:字面量
      var o1 = {name: 'o1'};
      var o2 = new Object({name: 'o2'});
      // 第二种方式:构造函数
      var M = function (name) { this.name = name; };
      var o3 = new M('o3');
      // 第三种方式:Object.create
      var p = {name: 'p'};
      var o4 = Object.create(p);

类与继承

如何实现继承?
继承的本质就是原型链

借助构造函数实现继承

      /**
       * 借助构造函数实现继承
       */
      function Parent1 () {
          this.name = 'parent1';
      }
      Parent1.prototype.say = function () {

      };
      function Child1 () {
          Parent1.call(this); // 或Parent1.apply(this,arguments)
          this.type = 'child1';
      }
      console.log(new Child1(), new Child1().say());

重点是这句:Parent1.call(this); 在子类的构造函数里执行父类的构造函数,通过call/apply改变this指向,从而导致父类构造函数执行时的这些属性都会挂载到子类实例上去。
问题: 只能继承父类构造函数中声明的实例属性,并没有继承父类原型的属性和方法

借助原型链实现继承

      /**
       * 借助原型链实现继承
       */
      function Parent2 () {
          this.name = 'parent2';
          this.play = [1, 2, 3];
      }
      function Child2 () {
          this.type = 'child2';
      }
      Child2.prototype = new Parent2();

      var s1 = new Child2();
      var s2 = new Child2();
      console.log(s1.play, s2.play);
      s1.play.push(4);

重点就是这句: Child2.prototype = new Parent2(); 就是说 new 一个父类的实例,然后赋给子类的原型 也就是说 new Child2().__proto__ === Child2.prototype === new Parent2()当我们在new Child2()中找不到属性/方法,顺着原型链就能找到new Parent2(),这样就实现了继承。
问题: 原型链中的原型对象是共用的,子类无法通过父类创建私有属性
比如当你new两个子类s1、s2的时候,改s1的属性,s2的属性也跟着改变

组合式继承

      /**
       * 组合方式
       */
      function Parent3 () {
          this.name = 'parent3';
          this.play = [1, 2, 3];
      }
      function Child3 () {
          Parent3.call(this); // 父类构造函数执行了
          this.type = 'child3';
      }
      Child3.prototype = new Parent3(); // 父类构造函数执行了
      var s3 = new Child3(); 
      var s4 = new Child3();
      s3.play.push(4);
      console.log(s3.play, s4.play);

组合式就是原型链+构造函数继承,解决了前两种方法的问题,但也有不足:子类实例化时,父类构造函数执行了两次,所以有了下面的组合继承的优化1

组合继承的优化1

/**
       * 组合继承的优化1
       * @type {String}
       */
      function Parent4 () {
          this.name = 'parent4';
          this.play = [1, 2, 3];
      }
      function Child4 () {
          Parent4.call(this);
          this.type = 'child4';
      }
      Child4.prototype = Parent4.prototype;
      var s5 = new Child4();
      var s6 = new Child4();
      console.log(s5, s6);

      console.log(s5 instanceof Child4, s5 instanceof Parent4);
      console.log(s5.constructor);

其实就是把原型链继承的那句 Child4.prototype = new Parent4(); 改为 Child4.prototype = Parent4.prototype; 这样虽然父类构造函数只执行了一次了,但又有了新的问题: 无法判断s5是Child4的实例还是Parent4的实例 因为Child4.prototype.constructor指向了Parent4的实例;如果直接加一句 Child4.prototype.constructor = Child4 也不行,这样Parent4.prototype.constructor也指向Child4,就无法区分父类实例了。

若要判断a是A的实例 用constructor
a.__proto__.constructor === A
用instanceof则不准确, instanceof 判断 实例对象的__proto__ 是不是和 构造函数的prototype 是同一个引用。若A 继承 B, B 继承 C 在该原型链上的对象 用instanceof判断都返回ture

组合继承的优化2(推荐)

/**
       * 组合继承的优化2
       */
      function Parent5 () {
          this.name = 'parent5';
          this.play = [1, 2, 3];
      }
      function Child5 () {
          Parent5.call(this);
          this.type = 'child5';
      }
      //注意此处,用到了Object.creat(obj)方法,该方法会对传入的obj对象进行浅拷贝
      //这个方法作为一个桥梁,达到父类和子类的一个隔离
      Child5.prototype = Object.create(Parent5.prototype);
      //修改构造函数指向
      Child5.prototype.constructor = Child5

构造函数属性继承和建立子类和父类原型的链接

ES6实现继承

引入了class、extends、super关键字,在子类构造函数里调用super()方法来调用父类的构造函数。
在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

class Child6 extends Parent6 {
      constructor(x, y, color) {
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
      }
      toString() {
        return this.color + ' ' + super.toString(); // super代表父类原型,调用父类的toString()
      }
    }

class实现原理

Class充当了ES5中构造函数在继承实现过程中的作用
有prototype属性,有__proto__属性,这个属性在ES6中的指向有一些主动的修改。
同时存在两条继承链:一条实现属性继承,一条实现方法继承。

class A extends B {}
A.__proto__ === B;  //继承属性
A.prototype.__proto__ === B.prototype;  //继承方法

ES6的子类的__proto__是父类,子类的原型的__proto__是父类的原型。
但是在ES5中 A.__proto__是指向Function.prototype的,因为每一个构造函数其实都是Function这个对象构造的,ES6中子类的__proto__指向父类可以实现属性的继承。

只有函数有prototype属性,只有对象有__proto__属性 ;但函数也有__proto__属性,因为函数也是一个对象,函数的__proto__等于 Function.prototype。

extends实现原理

//原型连接
Man.prototype = Object.create(Person.prototype); 
// B继承A的静态属性
Object.setPrototypeOf(Man, Person);
//绑定this
Person.call(this);

前两句实现了原型链上的继承,最后一句实现构造函数上的继承。


hellocassiell
42 声望1 粉丝