详解JS的继承(三)--|详解JS的继承(三)-- 图解Es6的Extend

前言 距离上一篇js的继承系列已经过去了四年,时不时还有新的读者评论和回复,开心之余也想着更新一下内容,因为当时的内容里没有涉及到es6的 extend 实现,所以现在抽空补上。 当然,如果是0基础的同学或者对于基本的继承有些遗忘的同学,可以先回顾一下前两篇:
详解js中的继承(一)
详解js中的继承(二)
正文 基础回顾 & 预备知识 为了使后面的学习过程更丝滑,在开始之前,一起再回顾一下这个构造函数-原型对象-实例模型:
当访问 a 的属性时,会先从a本身的属性(或方法)去找,如果找不到,会沿着 __proto__ 属性找到原型对象A.prototype,在原型对象上查找对应的属性(或方法);如果再找不到,继续沿着原型对象的__proto__ 继续找,这也就是最早我们介绍过的原型链的内容。

function A (){ this.type = 'A' } const a = new A();

详解JS的继承(三)--|详解JS的继承(三)-- 图解Es6的Extend
文章图片

当然,图上的原型链可以继续找,我们知道 A 虽然是函数,但是本质也是 Object ,沿着__proto__ 属性 不断上溯,最终会返回 null ;
a.__proto__ === A.prototype; // true a.__proto__.__proto__ === Object.prototype; // true a.__proto__.__proto__.__proto__ === null; // true

extend实现源码解析 进入正题, 学过 es6 的同学都知道,可以通过关键字 extend 直接实现继承,比如:
// 首先创建一个Animal类 class Animal { name: string; constructor(theName: string) { this.name = theName; }; move(distanceInMeters: number = 0) { console.log(`Animal moved ${distanceInMeters}m.`); } }// 子类Dog继承于Animal class Dog extends Animal { age: number; constructor(name: string, age: number) { super(name); this.age = age; } bark() { console.log('Woof! Woof!'); } }const dog = new Dog('wangwang', 12); dog.bark(); // 'Woof! Woof!' dog.move(10); //`Animal moved 10m.`

那么这个 extend 究竟做了哪些事情呢? 这里借助安装 typescript 这个 npm 包,然后在本地运行 tsc [文件路径] ,把ts以及es6的代码转换成原生js的代码来进行研究,(当然也有个缺点是转换的代码为了追求代码极简 有时可能会影响可读性 比如 undefined 写作 void 0 之类的),上面的代码转换之后长这样:
// 第一部分 var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); // 第二部分 // 首先创建一个Animal类 var Animal = /** @class */ (function () { function Animal(theName) { this.name = theName; } ; Animal.prototype.move = function (distanceInMeters) { if (distanceInMeters === void 0) { distanceInMeters = 0; } console.log("Animal moved ".concat(distanceInMeters, "m.")); }; return Animal; }()); // 第三部分 // 子类Dog继承于Animal var Dog = /** @class */ (function (_super) { __extends(Dog, _super); function Dog(name, age) { var _this = _super.call(this, name) || this; _this.age = age; return _this; } Dog.prototype.bark = function () { console.log('Woof! Woof!'); }; Dog.prototype.move = function (distanceInMeters) { if (distanceInMeters === void 0) { distanceInMeters = 5; } console.log("Dog moved ".concat(distanceInMeters, "m.")); }; return Dog; }(Animal)); // 第四部分 无需解析 var dog = new Dog('wangwang', 12); dog.bark(); // 'Woof! Woof!' dog.move(10); // Dog moved 10m.

代码看起来有些复杂,我们按照代码注释里,各部分内容复杂程度从简单到复杂进行分析:
  • 先看第二部分,首先是用匿名立即执行函数(IIFE)包裹了一层,这一点我们在聊闭包的时候说过,这样写的好处是避免污染到全局命名空间;然后在内部,就是之前第一篇说过的构造函数-原型对象的经典模型-- 属性放在构造函数里,方法绑定在原型对象上, 所以这一部分其实就是 es6的Class 对应的原生js写法;
  • 第三部分, Dog 类的写法和第二部分大体相同,但是还是有几处区别:
    • _super.call(this, name)_super 代表父类,所以这一步是使用父类的构造函数生成一个对象,之后再根据自身的构造函数,修改该对象;
    • __extends 方法,也是本文的核心内容。
      详解JS的继承(三)--|详解JS的继承(三)-- 图解Es6的Extend
      文章图片

    • 最后来介绍第一部分,也就是__extends 的具体实现。这部分的外层也是一个简单的避免重复定义以及匿名立即执行函数(IIFE),这一点就不赘述了。 核心内容是 extendStatics 的实现:
      • 首先介绍下 Object.setPrototypeOf 这个方法,这个方法的作用是为某个对象重新指定原型,用法如下:
        Object.setPrototypeOf(d, b) // 等价于d.__proto__ = b;

        后续每个 || 分隔符后面,都可以理解为一种 polyfill 写法,只是为了兼容不同的执行环境;
      • 接下来返回一个新的函数,前面提到,直接转换过来的可能有点晦涩,所以我在这里稍微整理成可读性更强的写法:
        return function (d, b) { // 当b不是构造函数或者null时,抛出错误 if (typeof b !== "function" && b !== null) { throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); }// 修改d原型链指向 extendStatics(d, b); // 模拟原型链的继承 function Temp() { this.constructor = d; } if(b === null){ d.prototype = {}; // Object.create(null) 此时返回一个新的空对象{} } else { Temp.prototype = b.prototype; var temp = new Temp(); d.prototype = temp; } }; 此处第一个 `if` 比较好理解,不多解释;

接下来的 extendStatics(d, b) 也介绍了效果是 d.__proto__ = b;
再接着就是比较有意思了,为了方便大家看懂,还是画一下相关的关系图:
详解JS的继承(三)--|详解JS的继承(三)-- 图解Es6的Extend
文章图片

首先, d和b 各自独立(当然)这里请注意!!!,我们用大写字母B和D分表表示b和d的构造函数,而b和d本身也可能还是一个函数,也还有自己对应的原型对象,只是图上没有标出。(眼神不太好或者不太仔细的同学务必要认真 否则很容易理解出错)
举个例子,前文的 Animal 对应图上的b, 那么 B 则对应 Function , 即 Animal.__proto__ = Function.prototype , 但是与此同时,Animal 还有自己的原型对象Animal.protptype
执行extendStatics(d, b) 后,原型关系如下(D的构造函数和原型对象变成不可访问了,所以用灰色表示):
详解JS的继承(三)--|详解JS的继承(三)-- 图解Es6的Extend
文章图片

再接着 执行以下代码之后:
function Temp() { this.constructor = d; } Temp.prototype = b.prototype; var temp = new Temp(); d.prototype = temp;

结构图如下:
详解JS的继承(三)--|详解JS的继承(三)-- 图解Es6的Extend
文章图片

从图上可以看到,这个临时变量temp最后变成了d的原型对象, 同时也是一个b的实例。 这一点和我们最早学过的原型链继承其实是类似的,区别在于多了一个 d.__proto__ = b .
那么,如果执行 var dog = new Dog('wangwang', 12); 其实,这里的 Dog 就对应上图的 d , dog 的原型链其实就是 dog.__proto__ === temp ,再向上也就是 b.prototype ,自然也就可以调用到定义在b.prototype 的方法了。
自测环节 那么在完成 extend 之后,回答几个问题,测试下自己的理解程度。
Q1: 首先,属性是怎么继承的,和ES5有何区别?
A1: extend是通过调用父类的方法创建初始对象,在此基础上,再根据子类的构造函数对该对象进行调整; ES5 的继承(组合继承),实质是先创造子类的实例对象 this ,再利用 call 或者 apply ,将父类的属性添加到 this .
Q2: dog 是如何调用到 move 方法的?
A2: 这个问题其实就是前面刚刚分析的原型链模型,方法的查找顺序是: dog.move(不存在) > dog.__proto__(temp变量).move (不存在) > dog.__proto__.__proto__.move (找到)
Q3: 多出来的d.__proto__ = b 有何作用?
A3: 可以继承父类的静态方法,例如添加方法: Animail.sayHello = function() {console.log('hello')}; ,那么Dog.sayHello() 同样生效,可以参照上图进行理解,查找顺序: d.hello(不存在) > d.__proto__.hello (找到)
小结 本文是继承系列的后续文章,主要针对ES6Extend做个简单的源码分析和原理介绍,最关键的还是原型链的图解部分,希望能对读者有帮助。
欢迎大家关注专栏,也希望大家对于喜爱的文章,能够不吝点赞和收藏,对于行文风格和内容有任何意见的,都欢迎私信交流。
【详解JS的继承(三)--|详解JS的继承(三)-- 图解Es6的Extend】(想来外企的小伙伴欢迎私信或者添加主页联系方式咨询详情~)

    推荐阅读