乐趣区

关于es6:详解JS的继承三-图解Es6的Extend

前言

间隔上一篇 js 的继承系列曾经过来了四年,时不时还有新的读者评论和回复,开心之余也想着更新一下内容,因为过后的内容里没有波及到 es6 的 extend 实现,所以当初抽空补上。当然,如果是 0 根底的同学或者对于根本的继承有些忘记的同学,能够先回顾一下前两篇:

详解 js 中的继承(一)

详解 js 中的继承(二)

注释

根底回顾 & 准备常识

为了使前面的学习过程更丝滑,在开始之前,一起再回顾一下这个 构造函数 - 原型对象 - 实例 模型:

当拜访 a 的属性时,会 先从 a 自身的属性(或办法)去找,如果找不到,会沿着 __proto__ 属性找到 原型对象 A.prototype,在 原型对象 上查找对应的属性(或办法);如果再找不到,持续沿着原型对象的__proto__ 持续找,这也就是最早咱们介绍过的原型链的内容。

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


当然,图上的原型链能够持续找,咱们晓得 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 办法,也是本文的核心内容。

    • 最初来介绍第一局部,也就是 __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;

再接着就是比拟有意思了,为了不便大家看懂,还是画一下相干的关系图:

首先, d 和 b 各自独立(当然)这里请留神!!!,咱们用大写字母 B 和 D 分表示意 b 和 d 的构造函数,而 b 和 d 自身也可能还是一个函数,也还有本人对应的原型对象,只是图上没有标出。(眼神不太好或者不太认真的同学务必要认真 否则很容易了解出错)

举个例子,前文的 Animal 对应图上的 b, 那么 B 则对应 Function , 即 Animal.__proto__ = Function.prototype,然而与此同时,Animal 还有本人的原型对象Animal.protptype

执行extendStatics(d, b) 后,原型关系如下(D 的构造函数和原型对象变成不可拜访了,所以用灰色示意):

再接着 执行以下代码之后:

function Temp() { this.constructor = d;}
Temp.prototype = b.prototype;var temp = new Temp(); 
d.prototype = temp;

结构图如下:

从图上能够看到,这个 长期变量 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做个简略的源码剖析和原理介绍,最要害的还是原型链的图解局部,心愿能对读者有帮忙。

欢送大家关注专栏,也心愿大家 对于青睐的文章,可能不吝点赞和珍藏,对于行文格调和内容有任何意见的,都欢送私信交换。

(想来外企的小伙伴欢送私信或者增加主页联系方式征询详情~)

退出移动版