如果以前问我ES5的继承和ES6的继承有什么区别,我一定会自信的说没有区别,不过是语法糖而已,充其量也就是写法有区别,但是现在我会假装思考一下,然后说虽然只是语法糖,但也是有点小区别的,那么具体有什么区别呢,不要走开,下文更精彩!

本文会先回顾一下ES5的寄生组合式继承的实现,然后再看一下ES6的写法,最后根据Babel的编译结果来看一下到底有什么区别。

ES5:寄生组合式继承

js有很多种继承方式,比如大家耳熟能详的原型链继承构造继承组合继承寄生继承等,但是这些或多或少都有一些不足之处,所以笔者认为我们只要记住一种就可以了,那就是寄生组合式继承

首先要明确继承到底要继承些什么东西,一共有三部分,一是实例属性/方法、二是原型属性/方法、三是静态属性/方法,我们分别来看。

先来看一下我们要继承的父类的函数:

// 父类
function Sup(name) {
this.name = name// 实例属性
}
Sup.type = '午'// 静态属性
// 静态方法
Sup.sleep = function () {
console.log(`我在睡${this.type}觉`)
}
// 实例方法
Sup.prototype.say = function() {
console.log('我叫 ' + this.name)
}

继承实例属性/方法

要继承实例属性/方法,明显要执行一下Sup函数才行,并且要修改它的this指向,这使用callapply方法都行:

// 子类
function Sub(name, age) {
// 继承父类的实例属性
Sup.call(this, name)
// 自己的实例属性
this.age = age
}

能这么做的原理又是另外一道经典面试题:new操作符都做了什么,很简单,就4点:

1.创建一个空对象

2.把该对象的__proto__属性指向Sub.prototype

3.让构造函数里的this指向新对象,然后执行构造函数,

4.返回该对象

所以Sup.call(this)this指的就是这个新创建的对象,那么就会把父类的实例属性/方法都添加到该对象上。

继承原型属性/方法

我们都知道如果一个对象它本身没有某个方法,那么会去它构造函数的原型对象上,也就是__proto__指向的对象上查找,如果还没找到,那么会去构造函数原型对象的__proto__上查找,这样一层一层往上,也就是传说中的原型链,所以Sub的实例想要能访问到Sup的原型方法,就需要把Sub.prototypeSup.prototype关联起来,这有几种方法:

1.使用Object.create

Sub.prototype = Object.create(Sup.prototype)
Sub.prototype.constructor = Sub

2.使用__proto__

Sub.prototype.__proto__ = Sup.prototype

3.借用中间函数

function Fn() {}
Fn.prototype = Sup.prototype
Sub.prototype = new Fn()
Sub.prototype.constructor = Sub

以上三种方法都可以,我们再来覆盖一下继承到的Say方法,然后在该方法里面再调用父类原型上的say方法:

Sub.prototype.say = function () {
console.log('你好')
// 调用父类的该原型方法
// this.__proto__ === Sub.prototype、Sub.prototype.__proto__ === Sup.prototype
this.__proto__.__proto__.say.call(this)
console.log(`今年${this.age}岁`)
}

继承静态属性/方法

也就是继承Sup函数本身的属性和方法,这个很简单,遍历一下父类自身的可枚举属性,然后添加到子类上即可:

Object.keys(Sup).forEach((prop) => {
Sub[prop] = Sup[prop]
})

ES6:使用class继承

接下来我们使用ES6class关键字来实现上面的例子:

// 父类
class Sup {
constructor(name) {
this.name = name
} say() {
console.log('我叫 ' + this.name)
} static sleep() {
console.log(`我在睡${this.type}觉`)
}
}
// static只能设置静态方法,不能设置静态属性,所以需要自行添加到Sup类上
Sup.type = '午'
// 另外,原型属性也不能在class里面设置,需要手动设置到prototype上,比如Sup.prototype.xxx = 'xxx' // 子类,继承父类
class Sub extends Sup {
constructor(name, age) {
super(name)
this.age = age
} say() {
console.log('你好')
super.say()
console.log(`今年${this.age}岁`)
}
}
Sub.type = '懒'

可以看到一样的效果,使用class会简洁明了很多,接下来我们使用babel来把这段代码编译回ES5的语法,看看和我们写的有什么不一样,由于编译完的代码有200多行,所以不能一次全部贴上来,我们先从父类开始看:

编译后的父类

// 父类
var Sup = (function () {
function Sup(name) {
_classCallCheck(this, Sup); this.name = name;
} _createClass(
Sup,
[
{
key: "say",
value: function say() {
console.log("我叫 " + this.name);
},
},
],
[
{
key: "sleep",
value: function sleep() {
console.log("\u6211\u5728\u7761".concat(this.type, "\u89C9"));
},
},
]
); return Sup;
})(); // static只能设置静态方法,不能设置静态属性 Sup.type = "午"; // 子类,继承父类
// 如果我们之前通过Sup.prototype.xxx = 'xxx'设置了原型属性,那么跟静态属性一样,编译后没有区别,也是这么设置的

可以看到是个自执行函数,里面定义了一个Sup函数,Sup里面先调用了一个_classCallCheck(this, Sup)函数,我们转到这个函数看看:

function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

instanceof运算符是用来检测右边函数的prototype属性是否出现在左边的对象的原型链上,简单说可以判断某个对象是否是某个构造函数的实例,可以看到如果不是的话就抛错了,错误信息是不能把一个类当做函数调用,这里我们就发现第一个区别了:

区别1:ES5里的构造函数就是一个普通的函数,可以使用new调用,也可以直接调用,而ES6的class不能当做普通函数直接调用,必须使用new操作符调用

继续看自执行函数,接下来调用了一个_createClass方法:

function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}

该方法接收三个参数,分别是构造函数、原型方法、静态方法(注意不包含原型属性和静态属性),后面两个都是数组,数组里面每一项代表一个方法对象,不管是实例方法还是原型方法,都是通过_defineProperties方法设置,先来看该方法:

function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
// 设置该属性是否可枚举,设为false则for..in、Object.keys遍历不到该属性
descriptor.enumerable = descriptor.enumerable || false;
// 默认可配置,即能修改和删除该属性
descriptor.configurable = true;
// 设为true时该属性的值能被赋值运算符改变
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}

可以看到它是通过Object.defineProperty方法来设置原型方法和静态方法,而且enumerable默认为false,这就来到了第二个区别:

区别2:ES5的原型方法和静态方法默认是可枚举的,而class的默认不可枚举,如果想要获取不可枚举的属性可以使用Object.getOwnPropertyNames方法

接下来看子类编译后的代码:

编译后的子类

// 子类,继承父类
var Sub = (function (_Sup) {
_inherits(Sub, _Sup); var _super = _createSuper(Sub); function Sub(name, age) {
var _this; _classCallCheck(this, Sub); _this = _super.call(this, name);
_this.age = age;
return _this;
} _createClass(Sub, [
{
key: "say",
value: function say() {
console.log("你好"); _get(_getPrototypeOf(Sub.prototype), "say", this).call(this); console.log("\u4ECA\u5E74".concat(this.age, "\u5C81"));
}
}
]); return Sub;
})(Sup); Sub.type = "懒";

同样也是一个自执行方法,把要继承的父类构造函数作为参数传进去了,进来先调用了_inherits(Sub, _Sup)方法,虽然Sub函数是在后面定义的,但是函数声明是存在提升的,所以这里是可以正常访问到的:

function _inherits(subClass, superClass) {
// 被继承对象的必须是一个函数或null
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
// 设置原型
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true }
});
if (superClass) _setPrototypeOf(subClass, superClass);
}

这个方法先检查了父类是否合法,然后通过Object.create方法设置了子类的原型,这个和我们之前的写法是一样的,只是今天我才发现Object.create居然还有第二个参数,第二个参数必须是一个对象,对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。

这个方法的最后为我们揭晓了第三个区别:

区别3:子类可以直接通过__proto__找到父类,而ES5是指向Function.prototype

ES6:Sub.__proto__ === Sup

ES5:Sub.__proto__ === Function.prototype

为啥会这样呢,看看_setPrototypeOf方法做了啥就知道了:

function _setPrototypeOf(o, p) {
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}

可以看到这个方法把Sub.__proto__设置为了Sup,这样同时也完成了静态方法和属性的继承,因为函数也是对象,自身没有的属性和方法也会沿着__proto__链查找。

_inherits方法过后紧接着调用了一个_createSuper(Sub)方法,拉出来看看:

function _createSuper(Derived) {
return function _createSuperInternal() {
// ...
};
}

这个函数接收子类构造函数,然后返回了一个新函数,我们先跳到后面的子类构造函数的定义:

function Sub(name, age) {
var _this; // 检查是否当做普通函数调用,是的话抛错
_classCallCheck(this, Sub); _this = _super.call(this, name);
_this.age = age;
return _this;
}

同样是先检查了一下是否是使用new调用,然后我们发现这个函数返回了一个_this,前面介绍了new操作符都做了什么,我们知道会隐式创建一个对象,并且会把函数内的this指向该对象,如果没有显式的指定构造函数返回什么,那么就会默认返回这个新创建的对象,而这里显然是手动指定了要返回的对象,而这个_this来自于_super函数的执行结果,_super就是前面_createSuper返回的新函数:

function _createSuper(Derived) {
// _isNativeReflectConstruct会检查Reflect.construct方法是否可用
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
// _getPrototypeOf方法用来获取Derived的原型,也就是Derived.__proto__
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
// NewTarget === Sub
var NewTarget = _getPrototypeOf(this).constructor;
// Reflect.construct的操作可以简单理解为:result = new Super(...arguments),第三个参数如果传了则作为新创建对象的构造函数,也就是result.__proto__ === NewTarget.prototype,否则默认为Super.prototype
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}

Super代表的是Sub.__proto__,根据前面的继承操作,我们知道子类的__proto__指向了父类,也就是Sup,这里会优先使用Reflect.construct方法,相当于创建了一个父类的实例,并且这个实例的__proto__又指回了Sub.prototype,不得不说这个api真是神奇。

我们就不考虑降级情况了,那么最后会返回这个父类的实例对象。

回到Sub构造函数,_this指向的就是这个通过父类创建的实例对象,为什么要这么做呢,这其实就是第四个区别了,也是最重要的区别:

区别4:ES5的继承,实质是先创造子类的实例对象this,然后再执行父类的构造函数给它添加实例方法和属性(不执行也无所谓)。而ES6的继承机制完全不同,实质是先创造父类的实例对象this(当然它的__proto__指向的是子类的prototype),然后再用子类的构造函数修改this

这就是为啥使用class继承在constructor函数里必须调用super,因为子类压根没有自己的this,另外不能在super执行前访问this的原因也很明显了,因为调用了super后,this才有值。

子类自执行函数的最后一部分也是给它设置原型方法和静态方法,这个前面讲过了,我们重点看一下实例方法编译后的结果:

function say() {
console.log("你好"); _get(_getPrototypeOf(Sub.prototype), "say", this).call(this); console.log("\u4ECA\u5E74".concat(this.age, "\u5C81"));
}

猜你们也忘了编译前的原函数是啥样的了,请看:

say() {
console.log('你好')
super.say()
console.log(`今年${this.age}岁`)
}

ES6classsuper有两种含义,当做函数调用的话它代表父类的构造函数,只能在constructor里面调用,当做对象使用时它指向父类的原型对象,所以_get(_getPrototypeOf(Sub.prototype), "say", this).call(this)这行大概相当于Sub.prototype.__proto__.say.call(this),跟我们最开始写的ES5版本也差不多,但是显然在class的语法要简单很多。

到此,编译后的代码我们就分析的差不多了,不过其实还有一个区别不知道大家有没有发现,那就是为啥要使用自执行函数,一当然是为了封装一些变量,二其实是因为第五个区别:

区别5:class不存在变量提升,所以父类必须在子类之前定义

不信你把父类放到子类后面试试,不出意外会报错,你可能会觉得直接使用函数表达式也可以达到这样的效果,非也:

// 会报错
var Sub = function(){ Sup.call(this) }
new Sub()
var Sup = function(){} // 不会报错
var Sub = function(){ Sup.call(this) }
var Sup = function(){}
new Sub()

但是Babel编译后的无论你在哪里实例化子类,只要父类在它之后声明都会报错。

总结

本文通过分析Babel编译后的代码来总结了ES5ES6继承的5个区别,可能还有一些其他的,有兴趣可以自行了解。

关于class的详细信息可以看这篇继承class继承

示例代码在https://github.com/wanglin2/es5-es5-inherit-example

ES5的继承和ES6的继承有什么区别?让Babel来告诉你的更多相关文章

  1. 详解ES5和ES6的继承

    ES5继承 构造函数.原型和实例的关系:每一个构造函数都有一个原型对象,每一个原型对象都有一个指向构造函数的指针,而每一个实例都包含一个指向原型对象的内部指针, 原型链实现继承 基本思想:利用原型让一 ...

  2. ES5与ES6的继承

    JavaScript本身是一种神马语言: 提到继承,我们常常会联想到C#.java等面向对象的高级语言(当然还有C++),因为存在类的概念使得这些语言在实际的使用中抽象成为一个对象,即面向对象.Jav ...

  3. ES5和ES6的继承

    ES5继承 构造函数.原型和实例的关系:每一个构造函数都有一个原型对象,每一个原型对象都有一个指向构造函数的指针,而每一个实例都包含一个指向原型对象的内部指针, 原型链实现继承 基本思想:利用原型让一 ...

  4. es5继承和es6类和继承

    es6新增关键字class,代表类,其实相当于代替了es5的构造函数 通过构造函数可以创建一个对象实例,那么通过class也可以创建一个对象实列 /* es5 创建一个person 构造函数 */ f ...

  5. ES5和ES6的继承对比

    ES5的继承实现,这里以最佳实践:寄生组合式继承方式来实现.(为什么是最佳实践,前面有随笔讲过了,可以参考) function Super(name) { this.name = name; } Su ...

  6. es6继承 vs js原生继承(es5)

    最近在看es2015的一些语法,最实用的应该就是继承这个新特性了.比如下面的代码: $(function(){ class Father{ constructor(name, age){ this.n ...

  7. ES6 extends继承及super使用读书笔记

    extends 继承 extends 实现子类的继承 super() 表示父类的构造函数, 子类必须在 constructor中调用父类的方法,负责会报错. 子类的 this 是父类构造出来的, 再在 ...

  8. ES6 class继承

    ES6 class继承 class类的继承 class可以通过extends关键字实现继承,这笔ES5的通过修改原型连实现继承要清晰和方便很多. class Point{ } class ColorP ...

  9. 04面向对象编程-02-原型继承 和 ES6的class继承

    1.原型继承 在上一篇中,我们提到,JS中原型继承的本质,实际上就是 "将构造函数的原型对象,指向由另一个构造函数创建的实例". 这里,我们就原型继承的概念,再进行详细的理解.首先 ...

随机推荐

  1. k8s入门之namespace(三)

    namespace的作用就是用来隔离资源,将同一集群中的资源划分为相互隔离的组.同一名称空间内的资源名称要唯一,但不同名称空间时没有这个要求.有些k8s资源对象与名称空间没有关系,例如 Storage ...

  2. vite创建vue3+ts项目流程

    vite+vue3+typescript搭建项目过程   vite和vue3.0都出来一段时间了,尝试一下搭vite+vue3+ts的项目 相关资料网址 vue3.0官网:https://v3.vue ...

  3. C++ atomic 和 memory ordering 笔记

    如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的.简单来说,编译器以及 CPU 的一些行为,会影响到程序的执行结果: 即使是简单的语 ...

  4. [cf]Codeforces Round #784(Div 4)

    由于一次比赛被虐得太惨,,生发开始写blog的想法,于是便有了这篇随笔(找了个近期的cf比赛练练手(bushi))第一次写blog,多多包涵. 第二场cf比赛,第一场打的Div2,被虐太惨,所以第二场 ...

  5. 学习HTTP——HTTPS

    前言 因为工作需要,需要用到大量的关于 HTTP 协议的知识,目前掌握的关于 HTTP 请求以及协议的知识都是零散的,打算针对知识盲区系统的学习一些,理清概念. 为什么会出现 HTTPS 因为 HTT ...

  6. K8S面试应知必回

    目录 面试不要不懂装懂,不会就是不会,不可能每个人都接触过所有的知识! 1. 基础问题 1.1 Service是怎么关联Pod的?(课程Service章节) 1.2 HPA V1 V2的区别 1.3 ...

  7. Node.js + TypeScript + ESM +HotReload ( TypeScript 类型的 Node.js 项目从 CommJS 转为 ESM 的步骤)

    当前 Node.js 版本:v16.14.0 当前 TypeScript 版本:^4.6.3 步骤 安装必要的依赖 yarn add -D typescript ts-node @tsconfig/n ...

  8. awk内建函数

    内建函数 length() 获得字符串长度 cat score.txt Marry 2143 78 84 77 Jack 2321 66 78 45 Tom 2122 48 77 71 Mike 25 ...

  9. mysql配置与存储引擎与字段类型与约束条件

    目录 字符编码与配置文件 存储引擎 创建表的完整语法 字段类型 整型 浮点型 字符类型 数字的含义 枚举与集合 日期类型 约束条件 字符编码与配置文件 在MySQL5.X系列中,显示的字符编码有多种, ...

  10. 每天一个 HTTP 状态码 206

    206 Partial Content 206 Partial Content 是当客户端请求时使用了 Range 头部,服务器端回复的响应,表示只响应一部分内容. 实例 请求: GET /favor ...