源码共读15:面试官:JS的继承

80 阅读7分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

这是面试官问系列第五篇,链接: juejin.cn/post/684490…

前言:

React经常用extends继承React.Component

// 部分源码
function Component(props, context, updater) {
  // ...
}
Component.prototype.setState = function(partialState, callback){
    // ...
}
const React = {
    Component,
    // ...
}
// 使用
class index extends React.Component{
    // ...
}

顺着这个问JS的继承的相关问题,比如:ES6的class 继承用ES5如何实现?

构造函数、原型对象与实例之间的关系

先来复习一下构造函数、原型对象和实例之间的关系:

function F(){}
var f = new F();
// 构造器
F.prototype.contructor === F;
F.__proto__ === Function.prototype;
Function.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;

// 实例
f.__proto__ === F.prototype;
F.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;

ES6 extends继承做了什么操作?

我们先看这段包含静态方法的es6的继承代码:

class Parent {
    contructor(name) {
        this.name = name;   
    }
    static sayHello() {
        console.log(hello);
    }
    sayName() {
        console.log('my name is' + this.name);
        return this.name;
    }
}

class Child extends Parent {
    constructor(name, age) {
         super(name);
         this.age = age;
    }
    sayAge() {
        console.log('my age is' + this.age);
        return this.name;
    }
}
let parent = new Parent('Parent'); 
let child = new Child('Child',  1);
console.log(parent); // parent: Parent { name: "Parent" }
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log(child); // child: Child { name: "Child", age: 1 }
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 1

其中这段代码有两条原型链,看具体代码:

// 1.构造器原型链
Child.__proto__ === Parent; // true
Parent.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 2.实例原型链
child.__proto__ === Child.prototype; // true
child.prototype.__proto__ === Parent.prototype; // true
Parent.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true

画一张图示意:

暂时无法在飞书文档外展示此内容

截屏2024-11-17 09.47.02.png 结合代码和图,可以总结出:

  1. 把子类构造函数Child的原型(proto)指向父类构造函数Parent。
  2. 把子类实例child的原型(Child.prototype)的原型(proto)指向父类构造器Parent的原型对象(Parent.prototype)。
  3. 子类构造器Child继承父类构造器中的属性。使用super()调用(ES5中使用call或apply调用参数)。

在《JavaScript高级程序设计-第3版》章节6.3,这2、3点正是**寄生组合式继承。书中没有提及第1小点。

那么问题来了,什么可以设置__proto__链接呢?

new 、Object.create和Object.setPrototypeOf可以设置__proto__

说明一下,__proto__这种写法是浏览器厂商自己实现的。结合一下图和代码看一下new,new出来的实例的__proto__指向构造函数的prototype。这就是new做的事情。参考之前的文章。传送门:能否模拟实现JS的new操作符

new做了什么事情:

  1. 创建一个全新对象 2. 这个对象会被执行[Prototype]链接 3. 生成的新对象会绑定到函数调用的this 4. 通过new创建的每个对象都会被[[Prototype]]链接到这个函数的prototype上 5. 如果函数没有返回对象类型Object(包含Function、Array、Date、RegExg、Error),那么new表达式中函数调用会自动返回这个新对象

Object.create提供的

Object.create(proto, [propertiesObject])方法创建一个新对象,使用现有对象来提供新对象的__proto__。它接受两个参数,不过第二个可选参数是属性描述符(不常用,默认是undefined)。对于不支持ES5的浏览器,MDN上提供了ployfill方案(MDN Object.create())。

// 简版:也正是使用了new会设置__proto__链接的原理
if (typeof Object.create !== 'function') {
  Object.create = function(proto) {
      function F() {}
      F.prototype = proto;
      return new F();
  }
}

Object.setPropertiesOf ES6提供的

setPrototypeOf

Object.setPropertiesOf()方法设置一个指定对象的原型(即,内部的[[Prototype]]属性)到另一个对象或null。Object.setPropertiesOf(obj, prototype)

`ployfill`
// 仅适合Chrome和Firefox,在IE不工作
Object.setProtoTypeOf = Object.setPrototypeOf || function(obj, proto) {
   obj.__proto__ = proto;
   return obj;
}

nodejs源码就是利用这个实现继承的工具函数的: nodejs utils inherits

function inherits(ctor, suprtCtor) {
    if (ctor === undefined || ctor === null) {
        throw new ERR_INVALID_ARG_TYPE ( 'ctor' , 'Function' , ctor);
    }
    if (suprtCtor === undefined || superCtor === null) {
        throw new ERR_INVALID_ARG_TYPE ( 'superCtor' , 'Function' , superCtor);
    }
    if (superCtor.prototype === undefined) {
        throw new ERR_INVALID_ARG_TYPE ( 'superCtor.prototype' , 'Object' , superCtor. prototype );
    }
    Object.defineProperty(ctor, 'super_', {
        value: superCtor,
        writable: true,
        configurable: true
    });
    Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
}

ES6的extends 的ES5版本实现

知道了ES6 extends继承做了什么操作和设置__proto__的相关知识点之后,我们使用把ES6版本用ES5来实现一下,

也就是实现寄生组合式继承:

// ES5实现ES6的extends继承
function Parent(name) {
    this.name = name;
}
Parent.sayHello = function() {
    console.log('hello');
}
Parent.prototype.sayName = function() {
    console.log('my name is ' + this.name);
    return this.name;
}

function Child(name, age) {
    // 相当于super
    Parent.call(this, name);
    this.age = age;
}
// new
function object(proto) {
    function F() {};
    F.prototype = proto;
    return new F();
}
function _inherits(Child, Parent) {
    // Object.create
    Child.prototype = Object.create(Parent.prototype);
    // __proto__
    // Child.prototype.__proto__ = Parent.prototype
    Child.prototype.constructor = Child;
    // ES6
    // Object.setPrototypeOf(Child, Parent);
    // __proto__
    Child.__proto__ = Parent;
}
_inherits(Child, Parent);
Child.prototype.sayAge = function() {
    console.log('my age is ' + this.age);
    return this.age;
}
var parent = new Parent('Parent');
var child = new Child('Child', 1);
console.log(parent); // parent: Parent { name: "Parent" }
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log(child); // child: Child { name: "Child", age: 1 }
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 1

将上述代码ES6例子通过babel转码为ES5来查看,更为严谨:

// 对转换后的代码进行了简要的注释
"use strict";
// 主要是对当前环境支持Symbol和不支持Symbol的typeof处理
function _typeof(obj) {
    if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
        _typeof = function _typeof(obj) {
            return typeof obj;
        };
    } else {
        _typeof = function _typeof(obj) {
            return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };
    }
    return _typeof(obj);
}
// _possibleConstructorReturn 判断Parent。call(this, name)函数返回值 是否为null或者函数或者对象。
function _possibleConstructorReturn(self, call) {
    if (call && (_typeof(call) === "object" || typeof call === "function")) {
        return call;
    }
    return _assertThisInitialized(self);
}
// 如何 self 是void 0 (undefined) 则报错
function _assertThisInitialized(self) {
    if (self === void 0) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return self;
}
// 获取__proto__
function _getPrototypeOf(o) {
    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
    };
    return _getPrototypeOf(o);
}
// 寄生组合式继承的核心
function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {
        throw new TypeError("Super expression must either be null or a function");
    }
    // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 
    // 也就是说执行后 subClass.prototype.__proto__ === superClass.prototype; 这条语句为true
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            writable: true,
            configurable: true
        }
    });
    if (superClass) _setPrototypeOf(subClass, superClass);
}
// 设置__proto__
function _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
        o.__proto__ = p;
        return o;
    };
    return _setPrototypeOf(o, p);
}
// instanceof操作符包含对Symbol的处理
function _instanceof(left, right) {
    if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
        return right[Symbol.hasInstance](left);
    } else {
        return left instanceof right;
    }
}

function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}
// 按照它们的属性描述符 把方法和静态属性赋值到构造函数的prototype和构造器函数上
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);
    }
}
// 把方法和静态属性赋值到构造函数的prototype和构造器函数上
function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    return Constructor;
}

// ES6
var Parent = function () {
    function Parent(name) {
        _classCallCheck(this, Parent);
        this.name = name;
    }
    _createClass(Parent, [{
        key: "sayName",
        value: function sayName() {
            console.log('my name is ' + this.name);
            return this.name;
        }
    }], [{
        key: "sayHello",
        value: function sayHello() {
            console.log('hello');
        }
    }]);
    return Parent;
}();

var Child = function (_Parent) {
    _inherits(Child, _Parent);
    function Child(name, age) {
        var _this;
        _classCallCheck(this, Child);
        // Child.__proto__ => Parent
        // 所以也就是相当于Parent.call(this, name); 是super(name)的一种转换
        // _possibleConstructorReturn 判断Parent.call(this, name)函数返回值 是否为null或者函数或者对象。
        _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
        _this.age = age;
        return _this;
    }
    _createClass(Child, [{
        key: "sayAge",
        value: function sayAge() {
            console.log('my age is ' + this.age);
            return this.age;
        }
    }]);
    return Child;
}(Parent);

var parent = new Parent('Parent');
var child = new Child('Child', 18);
console.log('parent: ', parent); // parent:  Parent {name: "Parent"}
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log('child: ', child); // child:  Child {name: "Child", age: 18}
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 18

参考的书籍资料:

《JavaScript高级程序设计 - 第三版》- 第六章 面向对象程序设计,6种继承方案:原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合继承

《JavaScript面向对象编程- 第二版》- 第六章 继承,12种继承的方式:1.原型链继承法(仿传统)、2.仅从原型继承法、3.临时构造器继承法、4.原型属性拷贝法、5.全属性拷贝继承(浅拷法)、6.深拷贝法、7.原型继承法、8.扩展与增强模式、9.多重继承法、10.寄生继承法、11.构造器借用法、12.构造器借用与属性拷贝法

《ES6标准入门》 - 第21章class继承

《深入理解ES6》第九章 JavaScript中的类

《你不知道的JavaScript - 上卷》第六章 行为委托和附录A ES6中class

总结:

继承对于JavaScript来说就是子类拥有父类的属性、方法和静态方法。子类可以利用原型链查找,也可以子类调用父类,或者从父类拷贝一份到子类等方案。继承方法有很多种,重点在于必须理解并熟悉这些对象、原型和构造器的工作方式。剩下就简单了,寄生组合式继承是开发者使用比较多的,回顾寄生组合式继承,主要的要点有三点:

  1. 子类构造函数的__proto__指向父类构造函数,继承父类的静态方法。
  2. 子类构造函数的prototype的__proto__指向父类构造函数的原型对象prototype,继承父类的方法。
  3. 子类构造函数调用父类构造器,继承父类的属性。

此文章为2024年11月Day1源码共读,生活在阴沟里,也要记得仰望星空。