面试官问:JS 的继承

我不是女神ヾ 2022-01-30 23:29 354阅读 0赞

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

  1. // 部分源码
  2. function Component(props, context, updater) {
  3. // ...
  4. }
  5. Component.prototype.setState = function(partialState, callback){
  6. // ...
  7. }
  8. const React = {
  9. Component,
  10. // ...
  11. }
  12. // 使用
  13. class index extends React.Component{
  14. // ...
  15. }

React github源码:https://github.com/facebook/react/blob/master/packages/react/src/ReactBaseClasses.js。

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

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

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

代码表示:

  1. function F(){}
  2. var f = new F();
  3. // 构造器
  4. F.prototype.constructor === F; // true
  5. F.__proto__ === Function.prototype; // true
  6. Function.prototype.__proto__ === Object.prototype; // true
  7. Object.prototype.__proto__ === null; // true
  8. // 实例
  9. f.__proto__ === F.prototype; // true
  10. F.prototype.__proto__ === Object.prototype; // true
  11. Object.prototype.__proto__ === null; // true

笔者画了一张图表示:1551235053689716.jpg

ES6 extends 继承做了什么操作

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

  1. // ES6
  2. class Parent{
  3. constructor(name){
  4. this.name = name;
  5. }
  6. static sayHello(){
  7. console.log('hello');
  8. }
  9. sayName(){
  10. console.log('my name is ' + this.name);
  11. return this.name;
  12. }
  13. }
  14. class Child extends Parent{
  15. constructor(name, age){
  16. super(name);
  17. this.age = age;
  18. }
  19. sayAge(){
  20. console.log('my age is ' + this.age);
  21. return this.age;
  22. }
  23. }
  24. let parent = new Parent('Parent');
  25. let child = new Child('Child', 18);
  26. console.log('parent: ', parent); // parent: Parent {name: "Parent"}
  27. Parent.sayHello(); // hello
  28. parent.sayName(); // my name is Parent
  29. console.log('child: ', child); // child: Child {name: "Child", age: 18}
  30. Child.sayHello(); // hello
  31. child.sayName(); // my name is Child
  32. child.sayAge(); // my age is 18

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

  1. // 1、构造器原型链
  2. Child.__proto__ === Parent; // true
  3. Parent.__proto__ === Function.prototype; // true
  4. Function.prototype.__proto__ === Object.prototype; // true
  5. Object.prototype.__proto__ === null; // true
  6. // 2、实例原型链
  7. child.__proto__ === Child.prototype; // true
  8. Child.prototype.__proto__ === Parent.prototype; // true
  9. Parent.prototype.__proto__ === Object.prototype; // true
  10. Object.prototype.__proto__ === null; // true

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

结合代码和图可以知道, 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__链接呢。

设置 proto

newObject.createObject.setPrototypeOf可以设置 __proto__

说明一下, __proto__这种写法是浏览器厂商自己的实现。

再结合一下图和代码看一下的 newnew出来的实例的 __proto__指向构造函数的 prototype,这就是 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()

  1. // 简版:也正是应用了new会设置__proto__链接的原理。
  2. if(typeof Object.create !== 'function'){
  3. Object.create = function(proto){
  4. function F() {}
  5. F.prototype = proto;
  6. return new F();
  7. }
  8. }

Object.setPrototypeOf:ES6提供的

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

  1. `ployfill`
  2. // 仅适用于Chrome和FireFox,在IE中不工作:
  3. Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
  4. obj.__proto__ = proto;
  5. return obj;
  6. }

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

  1. function inherits(ctor, superCtor) {
  2. if (ctor === undefined || ctor === null)
  3. throw new ERR_INVALID_ARG_TYPE('ctor', 'Function', ctor);
  4. if (superCtor === undefined || superCtor === null)
  5. throw new ERR_INVALID_ARG_TYPE('superCtor', 'Function', superCtor);
  6. if (superCtor.prototype === undefined) {
  7. throw new ERR_INVALID_ARG_TYPE('superCtor.prototype',
  8. 'Object', superCtor.prototype);
  9. }
  10. Object.defineProperty(ctor, 'super_', {
  11. value: superCtor,
  12. writable: true,
  13. configurable: true
  14. });
  15. Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
  16. }

extends的ES5版本实现

知道了ES6 extends继承做了什么操作和设置 __proto__的知识点后,把上面 ES6例子的用 ES5就比较容易实现了,也就是说实现寄生组合式继承,简版代码就是:

  1. // ES5 实现ES6 extends的例子
  2. function Parent(name){
  3. this.name = name;
  4. }
  5. Parent.sayHello = function(){
  6. console.log('hello');
  7. }
  8. Parent.prototype.sayName = function(){
  9. console.log('my name is ' + this.name);
  10. return this.name;
  11. }
  12. function Child(name, age){
  13. // 相当于super
  14. Parent.call(this, name);
  15. this.age = age;
  16. }
  17. // new
  18. function object(){
  19. function F() {}
  20. F.prototype = proto;
  21. return new F();
  22. }
  23. function _inherits(Child, Parent){
  24. // Object.create
  25. Child.prototype = Object.create(Parent.prototype);
  26. // __proto__
  27. // Child.prototype.__proto__ = Parent.prototype;
  28. Child.prototype.constructor = Child;
  29. // ES6
  30. // Object.setPrototypeOf(Child, Parent);
  31. // __proto__
  32. Child.__proto__ = Parent;
  33. }
  34. _inherits(Child, Parent);
  35. Child.prototype.sayAge = function(){
  36. console.log('my age is ' + this.age);
  37. return this.age;
  38. }
  39. var parent = new Parent('Parent');
  40. var child = new Child('Child', 18);
  41. console.log('parent: ', parent); // parent: Parent {name: "Parent"}
  42. Parent.sayHello(); // hello
  43. parent.sayName(); // my name is Parent
  44. console.log('child: ', child); // child: Child {name: "Child", age: 18}
  45. Child.sayHello(); // hello
  46. child.sayName(); // my name is Child
  47. child.sayAge(); // my age is 18

我们完全可以把上述 ES6的例子通过 babeljs(https://babeljs.io/repl)转码成 ES5来查看,更严谨的实现。

  1. // 对转换后的代码进行了简要的注释
  2. "use strict";
  3. // 主要是对当前环境支持Symbol和不支持Symbol的typeof处理
  4. function _typeof(obj) {
  5. if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
  6. _typeof = function _typeof(obj) {
  7. return typeof obj;
  8. };
  9. } else {
  10. _typeof = function _typeof(obj) {
  11. return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
  12. };
  13. }
  14. return _typeof(obj);
  15. }
  16. // _possibleConstructorReturn 判断Parent。call(this, name)函数返回值 是否为null或者函数或者对象。
  17. function _possibleConstructorReturn(self, call) {
  18. if (call && (_typeof(call) === "object" || typeof call === "function")) {
  19. return call;
  20. }
  21. return _assertThisInitialized(self);
  22. }
  23. // 如何 self 是void 0 (undefined) 则报错
  24. function _assertThisInitialized(self) {
  25. if (self === void 0) {
  26. throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  27. }
  28. return self;
  29. }
  30. // 获取__proto__
  31. function _getPrototypeOf(o) {
  32. _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
  33. return o.__proto__ || Object.getPrototypeOf(o);
  34. };
  35. return _getPrototypeOf(o);
  36. }
  37. // 寄生组合式继承的核心
  38. function _inherits(subClass, superClass) {
  39. if (typeof superClass !== "function" && superClass !== null) {
  40. throw new TypeError("Super expression must either be null or a function");
  41. }
  42. // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
  43. // 也就是说执行后 subClass.prototype.__proto__ === superClass.prototype; 这条语句为true
  44. subClass.prototype = Object.create(superClass && superClass.prototype, {
  45. constructor: {
  46. value: subClass,
  47. writable: true,
  48. configurable: true
  49. }
  50. });
  51. if (superClass) _setPrototypeOf(subClass, superClass);
  52. }
  53. // 设置__proto__
  54. function _setPrototypeOf(o, p) {
  55. _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
  56. o.__proto__ = p;
  57. return o;
  58. };
  59. return _setPrototypeOf(o, p);
  60. }
  61. // instanceof操作符包含对Symbol的处理
  62. function _instanceof(left, right) {
  63. if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
  64. return right[Symbol.hasInstance](left);
  65. } else {
  66. return left instanceof right;
  67. }
  68. }
  69. function _classCallCheck(instance, Constructor) {
  70. if (!_instanceof(instance, Constructor)) {
  71. throw new TypeError("Cannot call a class as a function");
  72. }
  73. }
  74. // 按照它们的属性描述符 把方法和静态属性赋值到构造函数的prototype和构造器函数上
  75. function _defineProperties(target, props) {
  76. for (var i = 0; i < props.length; i++) {
  77. var descriptor = props[i];
  78. descriptor.enumerable = descriptor.enumerable || false;
  79. descriptor.configurable = true;
  80. if ("value" in descriptor) descriptor.writable = true;
  81. Object.defineProperty(target, descriptor.key, descriptor);
  82. }
  83. }
  84. // 把方法和静态属性赋值到构造函数的prototype和构造器函数上
  85. function _createClass(Constructor, protoProps, staticProps) {
  86. if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  87. if (staticProps) _defineProperties(Constructor, staticProps);
  88. return Constructor;
  89. }
  90. // ES6
  91. var Parent = function () {
  92. function Parent(name) {
  93. _classCallCheck(this, Parent);
  94. this.name = name;
  95. }
  96. _createClass(Parent, [{
  97. key: "sayName",
  98. value: function sayName() {
  99. console.log('my name is ' + this.name);
  100. return this.name;
  101. }
  102. }], [{
  103. key: "sayHello",
  104. value: function sayHello() {
  105. console.log('hello');
  106. }
  107. }]);
  108. return Parent;
  109. }();
  110. var Child = function (_Parent) {
  111. _inherits(Child, _Parent);
  112. function Child(name, age) {
  113. var _this;
  114. _classCallCheck(this, Child);
  115. // Child.__proto__ => Parent
  116. // 所以也就是相当于Parent.call(this, name); 是super(name)的一种转换
  117. // _possibleConstructorReturn 判断Parent.call(this, name)函数返回值 是否为null或者函数或者对象。
  118. _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
  119. _this.age = age;
  120. return _this;
  121. }
  122. _createClass(Child, [{
  123. key: "sayAge",
  124. value: function sayAge() {
  125. console.log('my age is ' + this.age);
  126. return this.age;
  127. }
  128. }]);
  129. return Child;
  130. }(Parent);
  131. var parent = new Parent('Parent');
  132. var child = new Child('Child', 18);
  133. console.log('parent: ', parent); // parent: Parent {name: "Parent"}
  134. Parent.sayHello(); // hello
  135. parent.sayName(); // my name is Parent
  136. console.log('child: ', child); // child: Child {name: "Child", age: 18}
  137. Child.sayHello(); // hello
  138. child.sayName(); // my name is Child
  139. child.sayAge(); // my age is 18

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

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

1、《JavaScript高级程序设计第3版》第6章——面向对象的程序设计

6种继承的方案,分别是原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。图灵社区本书地址,后文放出 github链接,里面包含这几种继承的代码 demo

2、《JavaScript面向对象编程第2版》第6章——继承

12种继承的方案:

  1. 原型链法(仿传统)
  2. 仅从原型继承法
  3. 临时构造器法
  4. 原型属性拷贝法
  5. 全属性拷贝法(即浅拷贝法)
  6. 深拷贝法
  7. 原型继承法
  8. 扩展与增强模式
  9. 多重继承法
  10. 寄生继承法
  11. 构造器借用法
  12. 构造器借用与属性拷贝法

3、《ES6标准入门》第21章——class的继承

4、《深入理解ES6》第9章——JavaScript中的类

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

总结

继承对于JS来说就是父类拥有的方法和属性、静态方法等,子类也要拥有。子类中可以利用原型链查找,也可以在子类调用父类,或者从父类拷贝一份到子类等方案。

继承方法可以有很多,重点在于必须理解并熟。

悉这些对象、原型以及构造器的工作方式,剩下的就简单了。寄生组合式继承是开发者使用比较多的。

回顾寄生组合式继承,主要就是三点:

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

行文到此,文章就基本写完了。文章代码和图片等资源放在这里github inhert和 demo展示 es6-extends,结合 consolesource面板查看更佳。

发表评论

表情:
评论列表 (有 0 条评论,354人围观)

还没有评论,来说两句吧...

相关阅读