-
在本章节中,我们会通过class类的继承做法extends来实现继承,相对于过往在原型链章节所学的各种继承方式,便利程度有着飞跃性的提升
-
类继承的关键因素
super关键词
是如何使用的?Babel工具是如何将ES6的类继承转为ES5的旧写法?阅读这类转化过的源码,我们需要注意哪些技巧?在该篇章中,都能得到解答
-
一、类通过extends实现继承特性
-
在以前,实现类的继承是一件麻烦的事情
-
需要手动设置原型链,并且要修正构造函数指针,这是操作层面上的麻烦
-
构造函数和方法的继承需要分别处理,继承逻辑分散在多个地方,降低了代码的内聚性
-
如果继承结构更复杂,或者需要多重继承等,这种方式的代码将更加难以理解和维护
-
// 父类构造函数
function Person(name) {
this.name = name;
}
// 父类方法
Person.prototype.greet = function() {
console.log('Hello, my name is ' + this.name);
};
// 子类构造函数
function Student(name, grade) {
Person.call(this, name); // 调用父类构造函数,实现属性继承
this.grade = grade;
}
// 设置子类的原型为父类的实例来实现方法继承
Student.prototype = Object.create(Person.prototype);
// 修正构造函数指针
Student.prototype.constructor = Student;
// 添加或重写子类的方法
Student.prototype.study = function() {
console.log(this.name + ' is studying in grade ' + this.grade);
};
// 使用
var student = new Student('XiaoYu', 1);
student.greet(); // 正常工作:Hello, my name is XiaoYu
student.study(); // 正常工作:XiaoYu is studying in grade 1
-
在ES6之后,我们实现类继承只需要通过
extends关键字
即可-
使用
extends
关键字明确了继承关系 -
能够明确的继承内容(来自父类)有:构造函数、实例方法、静态方法、属性和访问器方法(getters 和 setters)、原型链
-
// 使用class定义父类
class Person {
constructor(name,age){
this.name = name
this.age = age
}
}
// 使用extends实现继承 Student(前者)继承自Person(后者)
class Student extends Person {
}
-
但这里还有一个需要注意的点,我们虽然利用extends将Person与Student之间形成了继承关系
-
但我们要如何在Student中拿到Person中的这些内容呢?
-
拿最基础的属性来举例,我如何在Student中拿到Person中的name和age这两个属性呢?
-
在ES5之前,我们通过了很多方式实现类似继承的效果,最核心的要素是:在子类中不能够出现父类的逻辑,不然就谈不上是继承
-
-
想要实现这个效果,需要我们的另一个
super关键字
,这些都是配套的
1.1 super关键字
-
super
关键字在类的继承中起重要作用。主要用于调用父类的构造函数、方法和访问父类的属性。从而做到子类能够重用和扩展父类的功能-
注意:在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数!(子类又称为派生类)
-
super的使用位置有三个:
子类的构造函数
、实例方法
、静态方法
-
-
在未处理的情况下,会造成重复的代码,而继承就是要解决这种情况,使复用率提升
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
// 使用extends实现继承
class Student {
constructor(name, age) {
this.name = name
this.age = age
}
}
var stu = new Student('XiaoYu',21)
console.log(stu)
-
一旦我们使用了extends继承,但不使用父类,这说明该继承是没有必要的,而编译器此时就会进行报错提醒我们
-
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
-
直译下来就是:
引用错误: 在访问 “this ”或从派生构造函数返回之前,必须调用派生类中的超级构造函数
-
后半句话很简单,意思是必须调用
super()
,也就是派生类中的超级构造函数,相当于必须使用到父类 -
而前半句话则是界定了范围,这个范围一共两个时刻
-
使用
this
关键字之前必须调用super()
,这是因为在 JavaScript 的类继承机制中,如果一个类继承自另一个类,派生类(子类)的构造函数必须先调用super()
,这样才能正确地初始化父类的构造函数。this
关键字引用当前对象的实例,而在父类构造函数成功调用之前,当前对象实际上还没有被创建。因此,试图在调用super()
之前使用this
会导致引用错误 -
从派生构造函数返回之前必须调用
super()
,不过使用return返回的时候,我们一般都是在最后面才进行使用
-
//...Person类
class Student extends Person{
constructor(name, age) {
this.name = name
this.age = age
}
}
var stu = new Student('XiaoYu',21)
console.log(stu)
-
这两个主要的原因,是导致报错的原因,也是
super关键字
通常写在最前面的原因-
使用super关键字的方式主要是两种:
-
调用构造函数(一般就是使用属性)
-
调用方法(实例方法 or 静态方法)
-
//调用 父对象/父类 的构造函数constructor
super([arguments])
//调用 父对象/父类 上的方法
super.functionOnParent([arguments])
-
对于这个能够调用父类构造函数上的哪些属性,在编译器中都是会进行提示的
图18-1 父类构造函数上属性的代码提示
-
通过
super关键字
我们调用父类的构造函数,将name
和age
属性正确地设置在派生类(即Student
)的实例,确保了从父类继承的属性在子类实例上被正确初始化-
而来自父类的所有没有进行限制的方法(实例方法、静态方法)在super关键字使用后就可以直接使用了,这是因为这些方法都位于原型链上,我们在stu实例对象中进行使用该方法,本质上是去原型链中寻找方法进行调用,而非复制到每个实例上,关于这点可以用
Object.getOwnPropertyDescriptor方法
进行检验 -
这就是在继承当中,想要重复利用逻辑的方式,和我们ES5中实现的
寄生组合式继承
非常的像,但从使用和理解角度来看,方便了特别多
-
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
// 使用extends实现继承
class Student extends Person {
constructor(name,age) {
super(name, age)将name与age来调用父类的constructor,且super需要写在this的前面
// this.name = name
// this.age = age
}
}
var stu = new Student('XiaoYu', 21)
console.log(stu)//Student { name: 'XiaoYu', age: 21 }
二、super关键字的其他用法
-
通过原型链进行查找调用的优势很大,如果不满意父类的方法(不想在子类中使用),可以直接在子类中重写方法,在调用子类方法时就会优先调用子类自身的
重写方法
而不是父类方法,该调用顺序在讲解原型链的时候有进行说明-
在重写方法的时候,只有很少的情况下是对父类的内容全都不满意的,所以将所有的内容都进行了修改增加
-
但我们大多数情况下,是部分满意,部分不满意,也就是我们不全部修改,我们只修改部分。那这种难道也要大动干戈进行重写吗?那这样不就又增加了子类的工作量了,不可避免的重复父类已有的部分,父类的作用不久又被削弱了吗?
-
在这点上,我们有更好的方式,那就是在子类的重写方法中使用super关键字调用父类的对应方法,这个方法包含了
静态方法
和实例方法
-
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
static running(){
console.log('逻辑1');
console.log('逻辑2');
console.log('逻辑3');
}
}
// 使用extends实现继承
class Student extends Person {
constructor(name,age) {
super(name, age)
}
//重写静态方法
static running1(){
//父类方法融为子类方法的一部分
super.running()
console.log('逻辑4');
console.log('逻辑5');
console.log('逻辑6');
}
}
Student.running1()//逻辑1-6
三、ES6转ES5代码
-
Class转ES5源码阅读
-
浏览器会随着时间的推移而不断发展,从而延伸出多种版本
-
新代码语法支持就是版本升级的其中一个功能点,越新的浏览器支持的语法越前沿,而大多数的用户的浏览器并不会一直处在当前最新版本,甚至会处在一个较为落后的阶段,从而和代码产生兼容性的问题
-
这导致很多新的语法代码是没办法在这些地方(落后版本的浏览器)运行的,但好在我们有工具babel能够解决我们的这个问题
-
3.1 Babel
Babel 是一个广泛使用的 JavaScript 编译器,主要用于将采用最新 ECMAScript 标准编写的 JavaScript 代码转换为向后兼容的版本(例如ES5版本)。从而做到让开发者可以使用最新的 JavaScript 语法和功能,而无需担心兼容性问题,特别是在旧版浏览器和环境中
-
Babel作为一个工具来说,在大多数情况下,我们是不需要深入学习的
-
在这里,我们需要了解什么是"工具",在编程当中,有许许多多的"工具",通常是以第三方库的形式存在,我们不需要了解该效果是如何具体实现的,只需要了解如何使用以及效果如何
-
对于重要的常见工具来说,我们会了解实现的流程步骤,当出现问题的时候方便排查对应哪里出现问题,而babel就属于
重要工具
的范畴
-
-
Babel官网地址:Babel · Babel (babeljs.io)
-
Babel严格来说,不单纯是重要工具,还是一整套的
工具链
,该特性来自于它的插件和预设,也是Babel的强大之处,我们可以使用或创建插件来扩展编译器的功能。此外,还有预设(presets)功能,这是一组预定义的插件集合,用于支持特定的编程环境或标准,如@babel/preset-env
。在Babel原有的基础上提供了更多的上限可能 -
而Babel的处理流程,是很类似我们一开始讲解的V8引擎处理JS代码的操作,但Babel的核心在于第二步的处理转换上,第一步的解析为AST树只是为了方便第二步的处理数据,第三步则是处理好后进行还原为正常使用的代码
-
解析(Parsing):Babel 首先将源代码解析成一个抽象语法树(AST),这个树结构描述了代码的语法结构
-
转换(Transforming):在这一步,Babel 会遍历 AST 并应用各种转换插件,这些插件可以修改、添加或删除节点,从而实现语法的转换
-
生成(Generating):最后,Babel 将更新后的 AST 转换回 JavaScript 代码,这个过程可能包括代码的美化和源码映射(source map)的生成
-
图18-2 Babel工具
-
转化后的代码其实是比较多的,可读性和结构性是远不如原有代码来得好的
-
可以看到14行的class继承代码经过转化,暴增到133行
-
但并不是说14行的ES6代码需要一百多行的ES5代码才能实现,而是在这个转化过程当中,Babel会额外的进行边界处理,包括但不限于处理不同浏览器的兼容性问题、保持语义的一致性,以及包括一些运行时支持以确保新特性的正确执行
-
代码的"复杂度"有绝大部分是来自这些边界处理,导致代码量提升,但就理解难度来说并不会太大,比如
/*#__PURE__*/
的纯函数注释以及到处充斥的立即执行函数 -
在 ES6 中,模块的每个文件自然具有局部作用域,而在 ES5 中,要实现模块作用域通常需要额外的封装,而这些
立即执行函数
可以防止变量泄露到全局作用域,也可以维护模块之间的独立性,防止内部代码与外界的全局代码产生冲突 -
纯函数因其少依赖外部状态的特性,在tree-shaking(摇树)操作中尤为有用。这是一种性能优化技术,主要用于删除代码中未使用的模块或导出,从而减少应用程序的最终文件大小。在构建过程中,工具如Webpack、Vite或Rollup会分析应用程序的导入语句,标记并移除未被引用的代码,确保只有必需的代码被包含在最终输出中。这一过程就像摇晃一棵树使未连接的叶子掉落,以去除多余的部分
-
-
其中还有很多思想的体现,都是之前有学习到的部分所结合起来的
图18-3 ES6代码转为ES5源码
-
整体的转化代码非常多,我们省略掉其中的部分来进行阅读
-
通常在阅读源码时,我们可以自行尝试传递内容进去,看内容是如何变化的,如下方代码中最后一行的
var stu = new Student("小余",20,100,110)
-
我们在这里可以看到大量的边界判断处理,有对null的判定,有对类型的检验以及判断浏览器支不支持
Reflect
,Reflect是ES6+的语法,在后续我们会讲解到,这里暂时跳过
-
-
经过Babel转化,一共有多出来几个主要的判定边界函数,让我们来分析一下:
-
_inherits 函数:确保子类(
SubType
)正确地继承父类(SuperType
)的原型方法。使用Object.create
创建一个以父类的原型为原型的新对象,确保原型链的正确设置以及将将子类的prototype
的constructor
属性修复为指向子类本身,保持继承链的完整性。这是非常完整的原型式继承
部分,在前面我们也有实现过 -
_createSuper 函数:生成一个用于从子类构造函数中调用父类构造函数的超类引用,判断是否可以使用
Reflect.construct
来调用父类构造器,这提供了正确设置新对象原型链的现代方法(ES6+方法),如果不支持Reflect.construct
,则回退到使用Super.apply
,这是旧式的继承方式,也就是我们曾经实现过的借用寄生继承
-
_classCallCheck 函数:确保构造函数仅通过
new
操作符调用,防止将类作为普通函数调用,如果不是通过new
调用,则抛出错误,保证构造函数的使用正确性。在ES6之前没有强制性要求,但ES6后的Class只能用new进行调用,在经过转化后,Babel通过工具函数来实现该作业 -
_createClass 函数:用于定义类的实例方法和静态方法,将方法添加到
prototype
上以实现实例方法,直接添加到构造函数上实现静态方法,对两种不同的方法进行实际的区分和实现
-
"use strict";
function _inherits(subClass, superClass) {
//判断类型不是函数且不是null的时候,抛出异常
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
//边界的判断:superClass && superClass.prototype,superClass有值的时候才会执行后面原型的部分,防止内容是null会报错
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true },
});
Object.defineProperty(subClass, "prototype", { writable: false });//设置subClass这个属性里面的原型里不可写,不能够继续赋值新值,因为改掉了值就无法实现继承了
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf
? Object.setPrototypeOf.bind()
: function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {//当if (typeof Proxy === "function") return true的时候,就会执行这里面的内容
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {//否则就执行这个
result = Super.apply(this, arguments);//绑定了子类的this,然后将argument传递了进去
}
return _possibleConstructorReturn(this, result);
};
}
//...省略
function _isNativeReflectConstruct() {
//判断当前的浏览器支不支持Reflect,这Proxy跟Reflect都是ES6的内容
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(
Reflect.construct(Boolean, [], function () { })
);
return true;
} catch (e) {
return false;
}
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf.bind()//绑定到一个特定的对象上,这样可以让这个函数只能在这个特定对象上运行
: function _getPrototypeOf(o) {//判断你的浏览器支持哪种方式,是__proto__还是getPrototypeOf
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
//...省略
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {//判断传入的内容是不是constructor
throw new TypeError("Cannot call a class as a function");
}
}
//...省略
var Person = /*#__PURE__*/ (function () {
function Person(name, age) {
_classCallCheck(this, Person);
this.name = name;
this.age = age;
}
_createClass(
Person,
[//实例方法
{
key: "running",
value: function running() { },
},
{
key: "eating",
value: function eating() { },
},
],
[//静态方法
{
key: "randomPerson",
value: function randomPerson() { },
},
]
);
return Person;
})();
//继承回顾
function inherit(SubType, SuperType) {
SubType.prototype = Object.create(SubType.prototype)
SubType.prototype.constructor = SubType
}
//核心代码
debugger
var Student = /*#__PURE__*/ (function (_Person) {
_inherits(Student, _Person);//这里的Student可以提前使用,因为下面的Student函数会作用域提升
var _super = _createSuper(Student);
function Student(name, age, sno, score) {
var _this;
_classCallCheck(this, Student);//检查这个this,让其不当作普通函数进行调用,而Student就是传入constructor形参的部分
_this = _super.call(this, name, age);//借用构造函数
_this.sno = sno;
_this.score = score;
return _this;
}
//实例方法
_createClass(
Student,
[
{
key: "studying",
value: function studying() { },
//静态方法
},
],
[
{
key: "randomStudent",
value: function randomStudent() { },
},
]
);
return Student;
})(Person);//这种()(Person)的 IIFE 方式是为了创建一个独立的作用域,保护类定义不被外部直接访问,从而产生变量冲突等问题
var stu = new Student("小余",20,100,110)
3.2 阅读源码的技巧与习惯
-
在源码中会涉及大量的编程思想以及技巧,所以如果需要阅读源码,需要对数据结构与算法有一定的学习
-
在此基础上,从一段内容中所能主动提取出来的信息就会更丰富,更易理解源码作者的想法
-
从源码中可以学到很多的良好规范,比如命名规范,书写规范,封装规范等信息
-
-
阅读源码的主要难度在于以下几点:
-
内容太多导致的心浮气躁
-
在源码中进行跳转阅读时,会出现忘记之前阅读的位置在哪,导致想要回顾的思路中断,这个问题可以通过vscode中的Bookmarks插件来解决,该插件是书签功能,在我们要进行跳转阅读前可以先在原地打一个标签,方便后续回顾
-
忘记阅读源码函数所传递进来的参数是什么,以及我们的目的是什么。这个需要不断的对自己强调,掌握初心和目标才不易迷失在茫茫源码中
-
读完函数还是不知道该函数对于整体的作用是做什么的,这很大概率上是经验的不够,之前没有见过很难联想起来,但在如今AI发展迅速的时代,可以利用AI来辅助阅读,难度会下降很多
-
debugger打断点进行验证自己的想法和源码的思路是否相符
-
-
阅读源码的好处
:阅读源码带来的收获其实是潜移默化的,作用很大,但并不直接体现出来,比如:理解代码工作原理、提高代码质量、发现和理解新技术、提高调试和问题解决技能。这些收获是直接作用于核心竞争力上的部分