首页 > 其他分享 >JS高级-ES6之类与继承实现

JS高级-ES6之类与继承实现

时间:2024-10-10 09:48:03浏览次数:3  
标签:function ES6 name age JS Student 之类 父类 构造函数

  • 在本章节中,我们会通过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(),也就是派生类中的超级构造函数,相当于必须使用到父类

    • 而前半句话则是界定了范围,这个范围一共两个时刻

    1. 使用 this 关键字之前必须调用 super(),这是因为在 JavaScript 的类继承机制中,如果一个类继承自另一个类,派生类(子类)的构造函数必须先调用 super(),这样才能正确地初始化父类的构造函数。this 关键字引用当前对象的实例,而在父类构造函数成功调用之前,当前对象实际上还没有被创建。因此,试图在调用 super() 之前使用 this 会导致引用错误

    2. 从派生构造函数返回之前必须调用 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关键字的方式主要是两种:

    1. 调用构造函数(一般就是使用属性)

    2. 调用方法(实例方法 or 静态方法)

//调用 父对象/父类 的构造函数constructor
super([arguments])
//调用 父对象/父类 上的方法
super.functionOnParent([arguments])
  • 对于这个能够调用父类构造函数上的哪些属性,在编译器中都是会进行提示的

图片

 
 
图18-1  父类构造函数上属性的代码提示

  • 通过super关键字我们调用父类的构造函数,将 nameage 属性正确地设置在派生类(即 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树只是为了方便第二步的处理数据,第三步则是处理好后进行还原为正常使用的代码

    1. 解析(Parsing):Babel 首先将源代码解析成一个抽象语法树(AST),这个树结构描述了代码的语法结构

    2. 转换(Transforming):在这一步,Babel 会遍历 AST 并应用各种转换插件,这些插件可以修改、添加或删除节点,从而实现语法的转换

    3. 生成(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转化,一共有多出来几个主要的判定边界函数,让我们来分析一下:

    1. _inherits 函数:确保子类(SubType)正确地继承父类(SuperType)的原型方法。使用 Object.create 创建一个以父类的原型为原型的新对象,确保原型链的正确设置以及将将子类的 prototypeconstructor 属性修复为指向子类本身,保持继承链的完整性。这是非常完整的原型式继承部分,在前面我们也有实现过

    2. _createSuper 函数:生成一个用于从子类构造函数中调用父类构造函数的超类引用,判断是否可以使用 Reflect.construct 来调用父类构造器,这提供了正确设置新对象原型链的现代方法(ES6+方法),如果不支持 Reflect.construct,则回退到使用 Super.apply,这是旧式的继承方式,也就是我们曾经实现过的借用寄生继承

    3. _classCallCheck 函数:确保构造函数仅通过 new 操作符调用,防止将类作为普通函数调用,如果不是通过 new 调用,则抛出错误,保证构造函数的使用正确性。在ES6之前没有强制性要求,但ES6后的Class只能用new进行调用,在经过转化后,Babel通过工具函数来实现该作业

    4. _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 阅读源码的技巧与习惯

  • 在源码中会涉及大量的编程思想以及技巧,所以如果需要阅读源码,需要对数据结构与算法有一定的学习

    • 在此基础上,从一段内容中所能主动提取出来的信息就会更丰富,更易理解源码作者的想法

    • 从源码中可以学到很多的良好规范,比如命名规范,书写规范,封装规范等信息

  • 阅读源码的主要难度在于以下几点:

    1. 内容太多导致的心浮气躁

    2. 在源码中进行跳转阅读时,会出现忘记之前阅读的位置在哪,导致想要回顾的思路中断,这个问题可以通过vscode中的Bookmarks插件来解决,该插件是书签功能,在我们要进行跳转阅读前可以先在原地打一个标签,方便后续回顾

    3. 忘记阅读源码函数所传递进来的参数是什么,以及我们的目的是什么。这个需要不断的对自己强调,掌握初心和目标才不易迷失在茫茫源码中

    4. 读完函数还是不知道该函数对于整体的作用是做什么的,这很大概率上是经验的不够,之前没有见过很难联想起来,但在如今AI发展迅速的时代,可以利用AI来辅助阅读,难度会下降很多

    5. debugger打断点进行验证自己的想法和源码的思路是否相符

  • 阅读源码的好处:阅读源码带来的收获其实是潜移默化的,作用很大,但并不直接体现出来,比如:理解代码工作原理、提高代码质量、发现和理解新技术、提高调试和问题解决技能。这些收获是直接作用于核心竞争力上的部分

标签:function,ES6,name,age,JS,Student,之类,父类,构造函数
From: https://blog.csdn.net/cui137610/article/details/142789940

相关文章

  • 基于SpringBoot+MySQL+SSM+Vue.js的电影票信息管理系统(附论文)
    获取见最下方名片获取见最下方名片获取见最下方名片演示视频基于SpringBoot+MySQL+SSM+Vue.js的电影票信息管理系统(附论文)技术描述开发工具:Idea/Eclipse数据库:MySQLJar包仓库:Maven前端框架:Vue/ElementUI后端框架:Spring+SpringMVC+Mybatis+SpringBoot......
  • 基于SpringBoot+MySQL+SSM+Vue.js的二手家电管理系统(附论文)
    获取见最下方名片获取见最下方名片获取见最下方名片演示视频基于SpringBoot+MySQL+SSM+Vue.js的二手家电管理系统(附论文)技术描述开发工具:Idea/Eclipse数据库:MySQLJar包仓库:Maven前端框架:Vue/ElementUI后端框架:Spring+SpringMVC+Mybatis+SpringBoot文......
  • JS高级-ES6之模板字符串与剩余参数
    在本章节中,我们学习新的字符串拼接方式:标签模板字符串,动态效果与自由使用程度得到进一步提升函数的默认参数更好的解决方案,以及结合解构的进阶使用方式剩余参数的进一步说明,箭头函数的补充,以及展开语法对数据的处理细节是怎么样的,深拷贝还是浅拷贝,都会得到说明一、字符......
  • 2024-10-10 js 深拷贝常用方法
    1、json序列化以及反序列化leta=JSON.parse(JSON.stringify(b))2、使用lodash库插件没有的话先安装:npmilodash使用方式:import{cloneDeep}from'lodash';leta=cloneDeep(b);ps:我当前使用的版本是@4为什么要使用深拷贝?因为我们在开发中会经常进行赋值......
  • 【D3.js in Action 3 精译_032】第四章 D3 直线、曲线与弧线的绘制 + 4.1 坐标轴的创
    当前内容所在位置(可进入专栏查看其他译好的章节内容)第一部分D3.js基础知识第一章D3.js简介(已完结)1.1何为D3.js?1.2D3生态系统——入门须知1.3数据可视化最佳实践(上)1.3数据可视化最佳实践(下)1.4本章小结第二章DOM的操作方法(已完结)2.1第一......
  • 【JS】判断有效的字母异位词
    步骤长度比较:首先检查两个字符串的长度是否相等。如果长度不相等,则直接返回false,因为变位词的定义要求两个字符串必须包含相同数量的字符。创建字符计数映射:使用一个Map对象来存储第一个字符串中每个字符的出现次数。Map的键是字符,值是该字符出现的次数。统计第一个......
  • 【JS】哈希法解决两数之和
    思路使用哈希法:需要快速查询一个元素是否出现过,或者一个元素是否在集合里时本题需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,符合要求的某元素是否遍历过,也就是是否出现在这个集合。因为要返回下标,所以使用Map集合,key存放元素值,value存放元素下......
  • js学习 -2024/10/9
    今天学习了js中的一些知识DOM通过document.get...函数获取元素对象可以查阅h3school资料找对象的函数,操作对象,//根据id获取元素对象//letid=document.getElementById('back');//id.src="../img/02.png";//根据标签获取元素对象vardivss=document.getElement......
  • codeforces round 974(div.3)E(优先队列实现dijstra算法,devc++的优先队列用greater报
    解题历程:看到两边同时移动,计算最终的相遇时间,我就想到两边同时计算各点到起点的最短距离,就是使用dijstra算法,最后所有节点取两次计算的最大值,再对所有节点取最小值,就是最终答案了,可是这个思路没有考虑有马的情况,思考一番后发现可以多列一个数组记录有马的情况下的行走最短路,然后......
  • JS刷力扣-链表【持续跟新】
    力扣的链表归类2.两数相加【链表+递归】前置知识:1.链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。2.链表的入口节点称为链表的头结点也就是head。leet......