4

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

 2 years ago
source link: https://segmentfault.com/a/1190000041286061
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

距离上一篇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做个简单的源码分析和原理介绍,最关键的还是原型链的图解部分,希望能对读者有帮助。

欢迎大家关注专栏,也希望大家对于喜爱的文章,能够不吝点赞和收藏,对于行文风格和内容有任何意见的,都欢迎私信交流。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK