ES6 Class 继承

分享

简单谈谈

ES6 规定了 extends 关键字,用该关键字就可以实现继承,对比 ES5 通过修改原型链来实现继承,可以说简洁了不少。

class Point {
};

class ColorPoint extends Point {
    constructor(x, y, color) {
        // 调用父类的constructor(x, y)
        super(x, y);
        this.color = color;
    }
    
    toString() {
        // 调用父类的toString()
        return this.color + ' ' + super.toString();
    }
}

当然子类如果没有定义 constructor 方法,这个方法会被默认添加。

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
    constructor(...args) {
        super(...args);
    }
}
子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。

如上所述,如果不调用 super 方法,子类就得不到 this 对象。那么如果在调用 super 方法前使用 this ,那么就会报错。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        this.color = color;         // ReferenceError
        super(x, y);
        this.color = color;         // 正确
    }
}

父类中的静态方法,同样也会被子类继承。

class A {
    static hello() {
        console.log('hello world');
    }
}

class B extends A {
}

B.hello();                          // hello world

getPrototypeOf

用于从子类上获取父类

Object.getPrototypeOf(ColorPoint) === Point;
// true

super 关键字

super 关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

函数调用

super 作为函数调用时,代表父类的构造函数。
而且作为子类必须调用 super 函数,但是需要注意的是虽然是父类的构造函数,但返回的是子类的实例。

class A {}

class B extends A {
    constructor() {
        super();
    }
}
super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B ,因此 super() 在这里相当于 A.prototype.constructor.call(this)
作为函数时, super() 只能用在子类的构造函数之中,用在其他地方就会报错。

作为对象使用

super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

在方法中使用

class A {
    p() {
        return 2;
    }
}

class B extends A {
    constructor() {
        super();
        console.log(super.p());
        // 2
    }
}

let b = new B();

在静态方法中使用

class A {
    static p1() {
        return 2;
    }
}

class B extends A {
    static p2() {
        super.p1();
        // 2
    }
}

super 当成对象使用时,如果在子类方法中使用,super 的指向为父类的 prototype ,如果在子类的静态方法中使用则指向父类。

进一步理解:子类方法其实也是在子类的 prototype 下,所以对应关系为:在子类的 prototype 中使用 super 那么就是指向父类的 prototype ,在子类下(也就是静态方法)使用 super 那么就是指向父类。

所以 super 的表现和 this 是差不多一致的,唯一不同的是 super 是针对父类的引用。

还需要注意一点的是:如果通过 super 调用父类的方法时,方法内部的 this 是指向子类的(不论是不是在静态方法中使用)。

class Parent {
    static staticMethod() {
        console.log(this.staticType);
    }
    
    constructor() {
        this.type = 'parent';
    }

    method() {
        console.log(this.type);
    }
}
Parent.staticType = 'parent static';

class Child extends Parent {
    static staticMethod2() {
        super.staticMethod();
    }
    
    constructor() {
        super();
        this.type = 'child';
    }

    method2() {
        super.method();
    }
}
Child.staticType = 'child static';

Child.staticMethod2();                      // child static

var child = new Child();
child.method2();                            // child

proto

ES5 中每一个对象都拥有 __proto__ 属性,指向其构造函数的 prototype。通过 __proto__ 可以实现对象的继承。

ES6 中,出现了类层面上的继承,因此规定了另外一条继承链,构造函数的继承。

class A {
}

class B extends A {
}

// ES6 中新出现的构造函数的继承
B.__proto__ === A                           // true

// 与 ES5 一致,原型链的继承
B.prototype.__proto__ === A.prototype       // true

因此在 ES6 中,__proto__ 即可以用来表示原型链的继承关系,也可以用来表示构造函数的继承关系。

当然出现这条继承链的原因是因为 ES6 中继承是按照下面的模式实现的。

class A {
}

class B {
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

const b = new B();

setPrototypeOf 是这样实现的。

Object.setPrototypeOf = function (obj, proto) {
    obj.__proto__ = proto;
    return obj;
}

所以当 B 继承 A 的静态属性时,就会顺便在 B 类下设置一个 __proto__

当然如果一个类没有继承任何的类那么 __proto__ 会指向哪?

class A {
}

A.__proto__ === Function.prototype;             // true
A.prototype.__proto__ === Object.prototype;     // true
这种情况下, A 作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承 Function.prototype 。但是, A 调用后返回一个空对象(即 Object 实例),所以 A.prototype.__proto__ 指向构造函数( Object )的 prototype 属性。

原生构造函数的继承

目前常用的 ECMAScript 原生构造函数大致如下

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

ES5 中,这些构造函数是无法被继承的。

function MyArray() {
    Array.apply(this, arguments)
}

MyArray.prototype = Object.create(Array.prototype, {
    constructor: {
        value: MyArray,
        writable: true,
        configurable: true,
        enumerable: true
    }
})

上述代码生成了一个继承的 MyArray 类,但这个类的行为与 Array 完全不一致

var colors = new MyArray();
colors[0] = "red";
colors.length;          // 0

colors.length = 0;
colors[0];              // "red"
之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过 Array.apply() 或者分配给原型对象都不行。原生构造函数会忽略 apply 方法传入的 this ,也就是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。

照成这样的根本原因是:ES5 创建实例时,先由子类生成 this 对象,在用这个对象去添加父类下的方法,而父类的内部属性仅仅只能在父类生成的 this 对象下使用,这样就会导致子类生成的 this 对象访问不到父类的内部属性,导致子类的行为异常。

而在 ES6 中,构造函数生成实例时,是先由父类生成 this 对象,然后用子类的构造函数去修改/添加 this 对象下的方法或属性,这样就使得 this 对象可以获得父类的所有行为。以下例子可以说明

class MyArray extends Array {
    constructor(...args) {
        super(...args);
    }
}

var arr = new MyArray();
arr[0] = 12;
arr.length;             // 1

arr.length = 0;
arr[0];                 // undefined

上述代码就完成了对原生对象的继承。

可以定义一个 Error 的子类,指定相应的报错信息

class ExtendableError extends Error {
    constructor(message) {
        super();
        this.message = message;
        this.stack = (new Error()).stack;
        this.name = this.constructor.name;
    }
}

class MyError extends ExtendableError {
    constructor(m) {
        super(m);
    }
}

var myerror = new MyError('ll')
myerror.message;                // "ll"
myerror instanceof Error;       // true
myerror.name;                   // "MyError"
myerror.stack;
// Error
//     at MyError.ExtendableError
//     ...

继承 Object 构造函数时需要注意。

class NewObj extends Object{
  constructor(){
    super(...arguments);
  }
}

var o = new NewObj({attr: true});
o.attr === true                 // false
上面代码中,NewObj 继承了 Object ,但是无法通过 super 方法向父类 Object 传参。这是因为 ES6 改变了 Object 构造函数的行为,一旦发现 Object 方法不是通过 new Object() 这种形式调用,ES6 规定 Object 构造函数会忽略参数。

阅读更多

JavaScript 对象 & 原型

前言 这次的 why what or how 主题:JavaScript 对象 & 原型。 此类文章在百度上一搜一大把,其实不用再写了,但是本着把这个问题解释的清清楚楚明明白白,还是开始写了。 原因如下: 1. 面试宝典类文章,弄张图片一糊弄,让人觉得自己理解了。 2. 解释类文章,告诉你一堆语法,对语法一顿解释,告诉你就是这样的,还是没说清楚。 3. 很少有文章单独解释这个点!但这个点是基础!真的很重要! 所以本篇文章想说一说对象 & 原型,但为了确保能顺利理解,请先看完 JS 变量存储?栈 & 堆?NONONO!,因为该篇文章从变量存储的角度来解释 JavaScript 对象 & 原型,请确保看完,在看这篇文章。 什么是对象? 既然要说清楚,

By Breeze

浏览器下的 Event Loop

前言 javascript 是以单线程的形式运行在宿主环境下,javascript 采用了回调的形式来解决异步任务。 为什么是单线程? javascript 的最开始的出现是为了给 web 页面增添一些动态的效果,那么就避免不了获取页面上的元素信息,如果 javascript 是以多线程的形式运行在浏览器内,如果两个线程内的 javascript 同时去获取/修改,某个页面上的元素,那么浏览器该让哪个 javascript 线程拥有获取/修改该元素的权限呢?由于元素的信息会经常性的发生变化,那么又改如何去同步各个线程内所保存的元素信息呢? 所以综合以上问题, javascript 是单线程的原因就显示意见了,单线程在执行时,对于元素信息的引用在同一时间仅可能只有一个,那么以上所有的问题都不存在了。 什么是异步任务? 任何代码在执行时,都会碰到一些需要经过大量时间运算或是等待的代码,在浏览器的环境下,常见的就是 http 任务,比如:资源的加载(图片的 onload 事件),ajax 的请求(XMLHttpRequest 的 onLoad 事件)还有页面元素的点击事件以及

By Breeze

node 下的 Event Loop

前言 通过 浏览器下的 Event Loop ,可以得知 javascript 是一门事件驱动的语言, javascript 主线程通过不断的调用事件队列中的事件来完成异步任务。那么 Node 下是否也是如此? Node 下的事件队列 在 Node 官网有这样一篇文章:The Node.js Event Loop, Timers, and process.nextTick() 。 该文章主要讲述了 Node 中是如何处理以及实现 Node 下的 Event Loop。 主要包含以下内容: 事件队列 不同于浏览器,Node 下的有 6 个事件处理阶段,其执行顺序和队列名称如下: ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │

By Breeze

什么是 HTML 5?

前言 作为程序员,技术的落实与巩固是必要的,因此想到写个系列,名为 why what or how 每篇文章试图解释清楚一个问题。 这次的 why what or how 主题:现在几乎所有人都知道了 HTML5 ,那么 H5 到底相比于 HTML4 有什么区别呢? 升级版?标准版! HTML5 做为 HTML 标准的第 5 版,于 2014 年发布,相信关注过 HTML5 的发展史的朋友都知道该版本是 WHATWG 和 W3C 握手言和后诞生的,是 W3C 组织与浏览器厂商相互妥协的结果,其中的绝大部分规范都由 WHATWG 组织所制定,之后由 W3C 采纳并入标准中。

By Breeze