ES5 实现继承的最佳实践

1,183 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

核心描述

  • ES5 实现继承的最佳实践
    • 定义新的构造函数,并在内部用 call() 调用希望继承的构造函数,并绑定 this,目的是为了可以复用“父类”的构造函数传参等初始化操作
    • 借助中间函数 F() 实现原型链继承,最好通过封装的 inherits 函数完成,目的是为了能够继承“父类”的的原型链
    • 继续在新的构造函数的原型上定义新方法 tips:此处的“父类”并非真正的 Class 类型
  • 代码示例
// 父类声明开始
function Parent(props){
    this.name = props.name || 'none'
}

Parent.prototype.sayHello = function () {
    console.log(`${this.name} say: Hello !`)
}

// 父类声明结束
// 子类声明开始
function Child(props){
    Parent.call(this, props)
    this.age = props.age || 18
}

// 继承方法封装
function inherits(ChildClass, ParentClass){
    const F = function (){}
    F.prototype = ParentClass.prototype;
    ChildClass.prototype = new F();
    ChildClass.prototype.constructor = ChildClass
}

// 实现原型链继承
inherits(Child, Parent)

// 拓展子类方法
Child.prototype.getAge = function(){
    return this.age
}

// 调用示意
const child = new Child({name: "小王", age: 20})
// 输出子类中定义的年龄
console.log('child age:', child.getAge())
// 调用父类中定义的 sayHello 方法
child.sayHello()


知识拓展

  • 原型与原型链

    • ES5 的原型继承以及 ES6 中我们说的 Class 继承,本质都是基于 Javascript 语言的原型模式,所以了解原型机原型链是有帮助我们理解 ES5 实现继承的原理
    • 原型:每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。这个对象就是通过调用构造函数创建的对象的原型。
    • 原型链:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型对象本身是另一个类型的实例,那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链,而原型链的对顶层指向了 Object.prototype , 此原型对象的原型 proto 为 null。
  • ES6 的继承最佳实践,示例参考了 ES5 的实现

// 父类
class Parent {
    constructor(props) {
        this.name = props.name || 'none'
    }

    sayHello(){
        console.log(`${this.name} say: Hello!`)
    }
}
// 子类
class Child extends Parent {
    constructor(props){
        super(props)
        this.age = props.age || 18
    }

    getAge(){
        return this.age
    }
}

// 调用示例
const child = new Child({name: '小张', age: 20})
console.log('child age:', child.getAge())
child.sayHello()
  • Babel 转换 ES6 之后的 ES5 继承,通过 babeljs.io 转换之后代码如下

    "use strict";
    
    function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
    
    function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
    
    function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
    
    function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
    
    function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } 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 _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
    
    function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
    
    function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
    
    function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
    
    function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
    
    // 父类
    var Parent = /*#__PURE__*/function () {
      function Parent(props) {
        _classCallCheck(this, Parent);
    
        this.name = props.name || 'none';
      }
    
      _createClass(Parent, [{
        key: "sayHello",
        value: function sayHello() {
          console.log("".concat(this.name, " say: Hello!"));
        }
      }]);
    
      return Parent;
    }(); // 子类
    
    
    var Child = /*#__PURE__*/function (_Parent) {
      _inherits(Child, _Parent);
    
      var _super = _createSuper(Child);
    
      function Child(props) {
        var _this;
    
        _classCallCheck(this, Child);
    
        _this = _super.call(this, props);
        _this.age = props.age || 18;
        return _this;
      }
    
      _createClass(Child, [{
        key: "getAge",
        value: function getAge() {
          return this.age;
        }
      }]);
    
      return Child;
    }(Parent); // 调用示例
    
    
    var child = new Child({
      name: '小张',
      age: 20
    });
    console.log('child age:', child.getAge());
    child.sayHello();
    
    • 主要关注 Child 是如何继承以及调用父类的方法的实现
    • 继承:是通过 _inherits 的方法
    • 调用父类的方法:是通过 _createSuper 以及其返回的对象 _super.call 的方式实现
  • 其他

    • 通过 ES5 的实现,以及 babel 转换 ES6 到 ES5 的结果,可以看出,核心使用的继承方式原理都是类似
    • babel 转换后,更多从项目工程的角度完善了边界
    • 对于实际开发而言,我们只需要了解原型和原型链的本质,以及 ES5 实现原型继承的一种,并不需要去死记硬背各种不完全的实用的继承方式。
    • 既没有实际意义,也并不会对项目带来什么价值。
    • 最后,如果只是从应用角度,直接使用 ES6 的方式即可,从示例中也能看出其更精简易读。如果是想深究原理,则更建议从 Javascript 这门基于原型实现的面向对象的语言设计层面去了解原型及继承的关系,会有不一样的收获。

参考资料

浏览知识共享许可协议

本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。