原文作者若川,掘金链接:https://juejin.im/post/5c433e216fb9a049c15f841b

写于2019年2月20日,现在发到公众号声明原创,之前被《前端大全》公众号等转载阅读量超1w+,知乎掘金等累计阅读量超过1w+。

导读:文章主要通过ES6的extends,结合ES5的寄生组合继承,图文并茂的讲述JS的继承,最后还推荐了一些书籍的继承的章节,旨在让读者掌握JS的继承。

用过 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继承的相关问题,比如: ES6的 class继承用ES5如何实现。据说很多人答得不好。

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

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

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

笔者画了一张图表示:

ES6extends 继承做了什么操作

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

// ES6
class 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(); // 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

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

// 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

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

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

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

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

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

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

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

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

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

new做了什么:

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

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

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

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

  5. 如果函数没有返回对象类型 Object(包含 FunctoinArrayDateRegExgError),那么 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]]属性)到另一个对象或 null。 Object.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);
}

ES6的 extends的 ES5版本实现

知道了 ES6extends继承做了什么操作和设置 __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){// 相当于superParent.call(this, name);this.age = age;
}
// new
function object(){function F() {}F.prototype = proto;return new F();
}
function _inherits(Child, Parent){// Object.createChild.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(); // 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

我们完全可以把上述 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; 这条语句为truesubClass.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

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

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

    2.子类构造函数的 prototype的 __proto__指向父类构造器的 prototype,继承父类的方法。

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

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

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客 http://lxchuan12.github.io 使用 vuepress重构了,阅读体验可能更好些
https://github.com/lxchuan12/blog,相关源码和资源都放在这里,求个 star^_^~

微信交流群,加我微信lxchuan12,注明来源,拉您进前端视野交流群

下图是公众号二维码:若川视野,一个可能比较有趣的前端开发类公众号,目前前端内容不多

往期文章

工作一年后,我有些感悟(写于2017年)

高考七年后、工作三年后的感悟

学习 jQuery 源码整体架构,打造属于自己的 js 类库

学习underscore源码整体架构,打造属于自己的函数式编程类库

学习 lodash 源码整体架构,打造属于自己的函数式编程类库

由于公众号限制外链,点击阅读原文,或许阅读体验更佳,觉得文章不错,可以点个在看呀^_^

面试官问:JS的继承相关推荐

  1. 面试官问:能否模拟实现JS的bind方法(高频考点)

    可以点击上方的话题JS基础系列,查看往期文章 写于2018年11月21日,发布在掘金阅读量1.3w+ 前言 这是面试官问系列的第二篇,旨在帮助读者提升JS基础知识,包含new.call.apply.t ...

  2. 面试官问:能否模拟实现JS的new操作符(高频考点)

    可以点击上方的话题JS基础系列,查看往期文章 这篇文章写于2018年11月05日,new模拟实现,Object.create是面试高频考点,之前发布在掘金有近2万人阅读,现在发布到公众号声明原创. 1 ...

  3. 面试官问:JS的this指向

    写于2018年12月25日,发布在掘金上阅读量近一万,现在发布到微信公众号申明原创. 前言 这是面试官问系列的第四篇,旨在帮助读者提升JS基础知识,包含new.call.apply.this.继承相关 ...

  4. 面试官问:能否模拟实现JS的call和apply方法

    写于2018年11月30日,发布在掘金上阅读量近一万,现在发布到微信公众号申明原创.相对比较基础的知识,虽然日常开发可能用得比较少,各种源码中有很多call和apply,需要掌握. 前言 这是面试官问 ...

  5. 后处理程序文件大小的变量_【每日一题】(17题)面试官问:JS中事件流,事件处理程序,事件对象的理解?...

    关注「松宝写代码」,精选好文,每日一题 作者:saucxs | songEagle 2020,实「鼠」不易 2021,「牛」转乾坤 风劲潮涌当扬帆,任重道远须奋蹄! 一.前言 2020.12.23 立 ...

  6. js var是什么类型_面试官问你JS基本类型时他想知道什么?

    点击上方"IT平头哥联盟",选择"置顶或者星标" 一起进步- 面试的时候我们经常会被问答js的数据类型.大部分情况我们会这样回答包括: 1.基本类型(值类型或者 ...

  7. java执行sql文件_面试官问你MyBatis SQL是如何执行的?把这篇文章甩给他

    初识 MyBatis MyBatis 是第一个支持自定义 SQL.存储过程和高级映射的类持久框架.MyBatis 消除了大部分 JDBC 的样板代码.手动设置参数以及检索结果.MyBatis 能够支持 ...

  8. 大叔手记(10):别再让面试官问你单例

    大叔手记(10):别再让面试官问你单例(暨6种实现方式让你堵住面试官的嘴) ... 2012-2-19 09:03| 发布者: benben| 查看: 283| 评论: 0 摘要: 引子经常从Recr ...

  9. obj: object是什么意思_面试官问你JavaScript基本类型时他想知道什么?

    本文原载于SegmentFault专栏"前端小将" 整理编辑:SegmentFault 面试的时候我们经常会被问答js的数据类型.大部分情况我们会这样回答包括: 1.基本类型(值类 ...

最新文章

  1. Rhel6.0部署Oracle10g报错相关问题记录
  2. java 接口中 常量_讨论:Java 接口当中的 “常量接口”
  3. python2 dict 乱序_为什么我的python dict变得无序?
  4. java 计算工具类_java精确计算工具类
  5. mysql用户和权限备份_备份MySQL用户和权限
  6. 【转】日邮物流:实现智慧物流,这个云上对了!
  7. python程序写完后点哪个运行快_让 Python 代码运行更快的最佳方式!
  8. 七步从AngularJS菜鸟到专家(6):服务
  9. [转]如何处理机器学习中的不平衡类别
  10. KnockoutJS 3.X API 第四章 表单绑定(11) options绑定
  11. 梦想,没想到这么快就实现了!
  12. 汉字与区位码互转(转)
  13. 如何使用qtp检查网页中显示的文字颜色为指定的颜色
  14. ML Case Studies(0)
  15. 基于单片机的智能鱼缸控制系统设计
  16. 华为笔记本没有网线口_笔记本电脑没有网线接口的怎么办?
  17. 若依前端分离版整合mybatis-plus
  18. 转行学习IT技术要做哪些准备
  19. 36氪专访| 友盟+CEO朋新宇:大数据赛道会越来越宽,同时也会越来越头部化
  20. jquery ajax 参数放到 request playload 中

热门文章

  1. linux 挂载多余空间,linux 空间不够,磁盘挂载
  2. Django-你想知道的都在这里
  3. LockSupport的源码实现原理以及应用
  4. ubuntu设置静态ip
  5. 商品尺码规格和颜色需要支持双引号
  6. 使用ArcGIS JavaScript API 3.18 加载天地图
  7. 设计模式学习笔记-观察者模式
  8. 信息加密之信息摘要加密MD2、MD4、MD5
  9. 百度社会化分享组件使用问题
  10. 全国计算机等级考试题库二级C操作题100套(第53套)