ES5的继承和ES6的继承有什么区别?让Babel来告诉你
如果以前问我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
指向,这使用call
、apply
方法都行:
// 子类
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.prototype
和Sup.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继承
接下来我们使用ES6
的class
关键字来实现上面的例子:
// 父类
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}岁`)
}
在ES6
的class
里super
有两种含义,当做函数调用的话它代表父类的构造函数,只能在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
编译后的代码来总结了ES5
和ES6
继承的5个区别,可能还有一些其他的,有兴趣可以自行了解。
关于class
的详细信息可以看这篇继承class继承。
示例代码在https://github.com/wanglin2/es5-es5-inherit-example。
ES5的继承和ES6的继承有什么区别?让Babel来告诉你的更多相关文章
- 详解ES5和ES6的继承
ES5继承 构造函数.原型和实例的关系:每一个构造函数都有一个原型对象,每一个原型对象都有一个指向构造函数的指针,而每一个实例都包含一个指向原型对象的内部指针, 原型链实现继承 基本思想:利用原型让一 ...
- ES5与ES6的继承
JavaScript本身是一种神马语言: 提到继承,我们常常会联想到C#.java等面向对象的高级语言(当然还有C++),因为存在类的概念使得这些语言在实际的使用中抽象成为一个对象,即面向对象.Jav ...
- ES5和ES6的继承
ES5继承 构造函数.原型和实例的关系:每一个构造函数都有一个原型对象,每一个原型对象都有一个指向构造函数的指针,而每一个实例都包含一个指向原型对象的内部指针, 原型链实现继承 基本思想:利用原型让一 ...
- es5继承和es6类和继承
es6新增关键字class,代表类,其实相当于代替了es5的构造函数 通过构造函数可以创建一个对象实例,那么通过class也可以创建一个对象实列 /* es5 创建一个person 构造函数 */ f ...
- ES5和ES6的继承对比
ES5的继承实现,这里以最佳实践:寄生组合式继承方式来实现.(为什么是最佳实践,前面有随笔讲过了,可以参考) function Super(name) { this.name = name; } Su ...
- es6继承 vs js原生继承(es5)
最近在看es2015的一些语法,最实用的应该就是继承这个新特性了.比如下面的代码: $(function(){ class Father{ constructor(name, age){ this.n ...
- ES6 extends继承及super使用读书笔记
extends 继承 extends 实现子类的继承 super() 表示父类的构造函数, 子类必须在 constructor中调用父类的方法,负责会报错. 子类的 this 是父类构造出来的, 再在 ...
- ES6 class继承
ES6 class继承 class类的继承 class可以通过extends关键字实现继承,这笔ES5的通过修改原型连实现继承要清晰和方便很多. class Point{ } class ColorP ...
- 04面向对象编程-02-原型继承 和 ES6的class继承
1.原型继承 在上一篇中,我们提到,JS中原型继承的本质,实际上就是 "将构造函数的原型对象,指向由另一个构造函数创建的实例". 这里,我们就原型继承的概念,再进行详细的理解.首先 ...
随机推荐
- k8s入门之namespace(三)
namespace的作用就是用来隔离资源,将同一集群中的资源划分为相互隔离的组.同一名称空间内的资源名称要唯一,但不同名称空间时没有这个要求.有些k8s资源对象与名称空间没有关系,例如 Storage ...
- vite创建vue3+ts项目流程
vite+vue3+typescript搭建项目过程 vite和vue3.0都出来一段时间了,尝试一下搭vite+vue3+ts的项目 相关资料网址 vue3.0官网:https://v3.vue ...
- C++ atomic 和 memory ordering 笔记
如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的.简单来说,编译器以及 CPU 的一些行为,会影响到程序的执行结果: 即使是简单的语 ...
- [cf]Codeforces Round #784(Div 4)
由于一次比赛被虐得太惨,,生发开始写blog的想法,于是便有了这篇随笔(找了个近期的cf比赛练练手(bushi))第一次写blog,多多包涵. 第二场cf比赛,第一场打的Div2,被虐太惨,所以第二场 ...
- 学习HTTP——HTTPS
前言 因为工作需要,需要用到大量的关于 HTTP 协议的知识,目前掌握的关于 HTTP 请求以及协议的知识都是零散的,打算针对知识盲区系统的学习一些,理清概念. 为什么会出现 HTTPS 因为 HTT ...
- K8S面试应知必回
目录 面试不要不懂装懂,不会就是不会,不可能每个人都接触过所有的知识! 1. 基础问题 1.1 Service是怎么关联Pod的?(课程Service章节) 1.2 HPA V1 V2的区别 1.3 ...
- 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 ...
- awk内建函数
内建函数 length() 获得字符串长度 cat score.txt Marry 2143 78 84 77 Jack 2321 66 78 45 Tom 2122 48 77 71 Mike 25 ...
- mysql配置与存储引擎与字段类型与约束条件
目录 字符编码与配置文件 存储引擎 创建表的完整语法 字段类型 整型 浮点型 字符类型 数字的含义 枚举与集合 日期类型 约束条件 字符编码与配置文件 在MySQL5.X系列中,显示的字符编码有多种, ...
- 每天一个 HTTP 状态码 206
206 Partial Content 206 Partial Content 是当客户端请求时使用了 Range 头部,服务器端回复的响应,表示只响应一部分内容. 实例 请求: GET /favor ...