写点什么

面试官问:JS 的继承

作者:若川
  • 2022 年 6 月 27 日
  • 本文字数:6803 字

    阅读完需:约 22 分钟

大家好,我是若川为了能帮助到更多对源码感兴趣、想学会看源码、提升自己前端技术能力的同学。我倾力最近组织了源码共读活动,已进行三个月,大家一起交流学习,共同进步,很多人都表示收获颇丰。


这是面试官问系列的第五篇,旨在帮助读者提升JS基础知识,包含new、call、apply、this、继承相关知识。<br>面试官问系列文章如下:感兴趣的读者可以点击阅读。<br>1.面试官问:能否模拟实现JS的new操作符<br>2.面试官问:能否模拟实现JS的bind方法<br>3.面试官问:能否模拟实现JS的call和apply方法<br>4.面试官问:JS的this指向<br>5.面试官问:JS的继承<br>


用过React的读者知道,经常用extends继承React.Component


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


点击这里查看 React github源码


面试官可以顺着这个问JS继承的相关问题,比如:ES6class继承用 ES5 如何实现。据说很多人答得不好。<br/>

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

要弄懂 extends 继承之前,先来复习一下构造函数、原型对象和实例之间的关系。代码表示:


function F(){}var f = new F();// 构造器F.prototype.constructor === F; // trueF.__proto__ === Function.prototype; // trueFunction.prototype.__proto__ === Object.prototype; // trueObject.prototype.__proto__ === null; // true
// 实例f.__proto__ === F.prototype; // trueF.prototype.__proto__ === Object.prototype; // trueObject.prototype.__proto__ === null; // true
复制代码


笔者画了一张图表示:



ES6 extends 继承做了什么操作

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


// ES6class Parent{    constructor(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.age;    }}let parent = new Parent('Parent');let child = new Child('Child', 18);console.log('parent: ', parent); // parent:  Parent {name: "Parent"}Parent.sayHello(); // helloparent.sayName(); // my name is Parentconsole.log('child: ', child); // child:  Child {name: "Child", age: 18}Child.sayHello(); // hellochild.sayName(); // my name is Childchild.sayAge(); // my age is 18
复制代码


其中这段代码里有两条原型链,不信看具体代码。


// 1、构造器原型链Child.__proto__ === Parent; // trueParent.__proto__ === Function.prototype; // trueFunction.prototype.__proto__ === Object.prototype; // trueObject.prototype.__proto__ === null; // true// 2、实例原型链child.__proto__ === Child.prototype; // trueChild.prototype.__proto__ === Parent.prototype; // trueParent.prototype.__proto__ === Object.prototype; // trueObject.prototype.__proto__ === null; // true
复制代码


一图胜千言,笔者也画了一张图表示,如图所示:



结合代码和图可以知道。ES6 extends 继承,主要就是:


  • 把子类构造函数(Child)的原型(__proto__)指向了父类构造函数(Parent),

  • 把子类实例child的原型对象(Child.prototype) 的原型(__proto__)指向了父类parent的原型对象(Parent.prototype)。


这两点也就是图中用不同颜色标记的两条线。


  • 子类构造函数Child继承了父类构造函数Parent的里的属性。使用super调用的(ES5则用call或者apply调用传参)。也就是图中用不同颜色标记的两条线。


看过《JavaScript 高级程序设计-第 3 版》 章节6.3继承的读者应该知道,这2和3小点,正是寄生组合式继承,书中例子没有第1小点1和2小点都是相对于设置了__proto__链接。那问题来了,什么可以设置了__proto__链接呢。

newObject.createObject.setPrototypeOf可以设置__proto__

说明一下,__proto__这种写法是浏览器厂商自己的实现。再结合一下图和代码看一下的newnew出来的实例的__proto__指向构造函数的prototype,这就是new做的事情。摘抄一下之前写过文章的一段。面试官问:能否模拟实现JS的new操作符,有兴趣的读者可以点击查看。

new做了什么:

  1. 创建了一个全新的对象。

  2. 这个对象会被执行[[Prototype]](也就是__proto__)链接。

  3. 生成的新对象会绑定到函数调用的this

  4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。

  5. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。

Object.create ES5提供的

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.setPrototypeOf ES6提供的

Object.setPrototypeOf MDN


Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 nullObject.setPrototypeOf(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, superCtor) {  if (ctor === undefined || ctor === null)    throw new ERR_INVALID_ARG_TYPE('ctor', 'Function', ctor);
if (superCtor === 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);}
复制代码

ES6extendsES5版本实现

知道了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;}// newfunction object(){ 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', 18);console.log('parent: ', parent); // parent: Parent {name: "Parent"}Parent.sayHello(); // helloparent.sayName(); // my name is Parentconsole.log('child: ', child); // child: Child {name: "Child", age: 18}Child.sayHello(); // hellochild.sayName(); // my name is Childchild.sayAge(); // my age is 18
复制代码


我们完全可以把上述ES6的例子通过babeljs转码成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;}
// ES6var 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(); // helloparent.sayName(); // my name is Parentconsole.log('child: ', child); // child: Child {name: "Child", age: 18}Child.sayHello(); // hellochild.sayName(); // my name is Childchild.sayAge(); // my age is 18
复制代码


如果对 JS 继承相关还是不太明白的读者,推荐阅读以下书籍的相关章节,可以自行找到相应的pdf版本。

推荐阅读 JS 继承相关的书籍章节

《JavaScript 高级程序设计第 3 版》-第 6 章 面向对象的程序设计,6 种继承的方案,分别是原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。图灵社区本书地址,后文放出github链接,里面包含这几种继承的代码demo


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


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


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


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

总结

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


  • 子类构造函数的__proto__指向父类构造器,继承父类的静态方法。

  • 子类构造函数的prototype__proto__指向父类构造器的prototype,继承父类的方法。

  • 子类构造器里调用父类构造器,继承父类的属性。行文到此,文章就基本写完了。文章代码和图片等资源放在这里github inhertdemo展示es6-extends,结合console、source面板查看更佳。


读者发现有不妥或可改善之处,欢迎评论指出。另外觉得写得不错,可以点赞、评论、转发,也是对笔者的一种支持。


最后可以持续关注我 @若川。欢迎与我交流,参与 源码共读 活动,每周大家一起学习 200 行左右的源码,共同进步。

发布于: 刚刚阅读数: 7
用户头像

若川

关注

还未添加个人签名 2018.09.11 加入

https://lxchuan12.gitee.io

评论

发布
暂无评论
面试官问:JS的继承_JavaScript_若川_InfoQ写作社区