ES5ES6-的继承

60次阅读

共计 4064 个字符,预计需要花费 11 分钟才能阅读完成。

原型链

  • 构造函数 / 原型 / 实例 的关系
  • 每个构造函数 (constructor) 都有一个原型对象 (prototype),原型对象都包含一个指向构造函数的指针,而实例(instance) 都包含一个指向原型对象内部的指针
  • 如果试图使用某个对象 (实例) 的某个属性或方法,会首先在对象内部寻找该属性,如果找不到去该对象的原型(instance.prototype)里面去找,如果还找不到就继续沿着 __proto__ 这个链条往上找,直到找到 Object.prototype 为止
  • javascript 里面一切皆对象,所以都可以从这个链条去出发
  • JavaScript 的继承不同于传统面向对象是靠类实现继承,而是通过原型链实现继承

ES5 继承

  • 拷贝式继承(通过深拷贝实现继承)
  • 原型式继承

    • 缺点:只能继承原型方法
  • 借用构造函数继承

    • 缺点:只能继承实例属性
  • 组合式继承

    • 缺点:无论在什么情况下,都会调用两次构造函数(创建父类实例的时候,在子类构造函数内不调用父类构造函数时)
  • 组合寄生式继承 (比较完美的继承,但不能继承父类静态方法、静态属性)
function Parent() {}

function Child() {
    // 继承父类实例属性
    Parent.call(this) // 如果父类有参数写在 this 后面
}

// 继承父类原型方法
Child.prototype = Object.create(Parent.prototype)

// 修正子类原型的 constructor 指向
Child.prototype.constructor = Child 
两个注意点:
  1. Object.create(proto, [propertiesObject]) MDN

    创建一个新对象,使用现有的对象来提供新创建的的对象的 __proto__
    
    Object.create(null) // 创建一个没有原型的空对象
    第二个参数可添加属性描述符
    
    js 高级程序设计用一下代码代替的这个方法
    function createObject(P) {var F = function() {}
        F.prototype = P.prototype
        return new F()}
  2. 为什么要修正子类原型的 constructor 指向?阮一峰

    简单总结一下:任何一个 prototype 对象都有一个 constructor 属性,指向它的构造函数
    更重要的是,每一个实例也有一个 constructor 属性,默认调用 prototype 的 constructor 属性
    
    如果没有修正的那行代码,结果如下
    var c = new C()
    c.constructor === Child // false
    Child.prototype.constructor === Child // false
    
    c.constructor === Parent // true
    Child.prototype.constructor === Parent // true
    
    这显然会导致继承链的混乱(c 明明是用构造函数 Child 生成的),因此我们必须手动纠正

ES6 继承

ES6 的继承本质上还是借助原型链实现继承

  • class extends 关键字
class Parent {static sayAge() {return '18'}
      constructor(name) {this.name = 'name'}
}

class Child extends Parent {constructor(name, age) {
        /**
         * 如果写 constructor 必须调用 super 方法,这是因为子类自己的 this 对象,必须先通过父类构造函数完成塑造
         * 不写 super 就得不到 this 对象,new 的时候就会报错
         * Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
         * 
         * ES5 实质上先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面 (Parent.call(this))
         * ES6 实质上先将父类实例对象的属性和方法,加到 this 上面(必须先调用 super 方法),然后用子类构造函数修改 this
        */
        super(name, age)
          this.age = age
    }
}
// 注意点:es6 的继承可以继承父类的静态方法和静态属性,而 ES5 的继承不行

Babel 转码后的 ES6 继承代码

// 为方便观看,对代码做了一些美化和省略处理
"use strict";

// 检查子类是否调用了 super 方法
function _possibleConstructorReturn(self, call) {if (call && (_typeof(call) === "object" || typeof call === "function")) {return call;}
  return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {if (self === void 0) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called"
    );
  }
  return self;
}

// 获取子类的原型链指向的对象即父类
function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf
    : function _getPrototypeOf(o) {return o.__proto__ || Object.getPrototypeOf(o);
      };
  return _getPrototypeOf(o);
}

// 核心继承方法
function _inherits(subClass, superClass) {
  // ...
  // 同 es5 继承的那一段
  subClass.prototype = Object.create(superClass.prototype, {
    constructor: { 
        value: subClass, // 修正 constructor 指向
        writable: true, 
        configurable: true 
    }
  });
  // 实现静态属性和方法的继承 原理为:Child.__proto__ = Parent
  // 即子类 (子类现在相当于实例) 的在往上的 prototype = Parent,即子类可以使用父类的静态属性和方法
  if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p;
      return o;
    };
  return _setPrototypeOf(o, p);
}

// ... 省略类的创建及检测等方法

var Parent =
  /*#__PURE__*/
  (function() {
    _createClass(Parent, null, [
      {
        key: "sayAge",
        value: function sayAge() {return "18";}
      }
    ]);

    function Parent(name) {_classCallCheck(this, Parent);

      this.name = "name";
    }

    return Parent;
  })();

var Child =
  /*#__PURE__*/
  (function(_Parent) {_inherits(Child, _Parent);

    function Child(name, age) {
      var _this;

      _classCallCheck(this, Child);
      
      // 下面一行代码即 调用 super 的效果,如果不调用 super 子类将没有 this
      _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name, age));

      /***
        * 如果注释源码中的 super 将编译为如下代码
        * _this.age = age;
        * return _possibleConstructorReturn(_this);
        * 因为没有调用 super 子类还没有 this,所以下一行直接报错
        *
        * 如果源码中不写 _this.age = age
        * 将直接进入 _assertThisInitialized 方法,然后报错,没有调用 super 方法
      */

      _this.age = age;
      return _this;
    }

    return Child;
  })(Parent);

var c = new Child();
// 如果不用 babel 转码,直接在浏览器里运行,不写 super,结果如下
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  • 原生构造函数不可通过 extends 实现继承 ES6 阮一峰
  • 因为子类拿不到 原生父类内部的对象,即是通过 call 也不行

new 运算符

上面说了继承,那产生实例的操作符 new 是什么原理?

var obj = {}
obj.__proto__ = Child.prototype
F.call(obj)

// 1. 创建一个空对象
// 2. 将这个空对象的 __proto__ 属性 指向了构造函数的 prototype 属性 上 ==> 继承原型属性方法
// 3. 将构造函数的 this 指针替换成了 obj(实例),再调用 构造函数       ===> 继承实例属性方法

与原型链有关的几个方法

  1. hasOwnProperty : 该方法只会查找对象本身是否有某属性,不会去原型链上寻找
  2. A.isPropertyOf(instanceA) : 判断 A 是不是 instanceA 的原型对象
  3. instanceof : 判断对象是不是某个构造函数的实例

正文完
 0