JavaScript:类(class)

在JS中,类是后来才出的概念,早期创造对象的方式是new Function()调用构造函数创建函数对象;

而现在,可以使用new className()构造方法来创建类对象了;

所以在很多方面,类的使用方式,很像函数的使用方式:

但是类跟函数,还是有本质区别的,这在原型那里已经说过,不再赘述;

如何定义一个类

如下所示去定义一个类:

class className {
    // 属性properties
    property1 = 1;
    property2 = [];
    peoperty3 = {};
    property4 = function() {};
    property5 = () => {};
    
    // 构造器
    constructor(...args) {
        super();
        // code here
    };
    
    // 方法methods
    method1() {
        // code here
    };
    method2(...args) {
        //code here
    };
}

可以定义成员属性和成员方法以及构造器,他们之间都有封号;隔开;

在通过new className()创建对象obj的时候,会立即执行构造器方法;

属性会成为obj的属性,句式为赋值语句,就算等号右边是函数,它也依然是一个属性,注意与方法声明语句区别开;

方法会成为obj的原型里的方法,即放在className.prototype属性里;

像使用function一样使用class关键字

正如函数表达式一样,类也有类表达式:

还可以像传递一个函数一样,去传递一个类:

这在Java中是不可想象的,但是在JS中,就是这么灵活;

静态属性和静态方法

静态属性和静态方法,不会成为对象的属性和方法,永远都属于类本身,只能通过类去调用;

  • 定义语法

    // 直接在类中,通过static关键字定义
    class className {
        static property = ...;
        static methoed() {};
    }
    
    // 通过类直接添加属性和方法,即为静态的
    class className {};
    className.property = ...;
    className.method = function() {};
    
  • 调用语法

    类似于对象调用属性和方法,直接通过类名去调用

    className.property;
    className.method();
    

静态属性/方法,可以和普通属性/方法同名,这不会被弄混,因为他们的调用者不一样,前者是类,后者是类对象;

私有属性和私有方法

JS新增的私有特性,在属性和方法之前添加#号,使其只在类中可见,对象无法调用,只能通过类提供的普通方法去间接访问;

  • 定义和调用语法

    class className {
        // 定义,添加#号
        #property = ...;
        #method() {};
        
        // 只能在类中可见,调用也需要加#号
        getProperty() {
            return this.#property;
        }
        set property(value) {
            this.#property = value;
        }
    }
    

注意,#property是一个总体作为属性名,与property是不同的,#method同理;

在这个私有特性之前,JS采用人为约定的方式,去间接实现私有;

在属性和方法之前添加下划线_,约定这样的属性和方法,只能在类中可见,只能靠人为遵守这样的约定;

类检查instanceof

我们知道,可以用typeof关键字来获取一个变量是什么数据类型;

现在可以用instanceof关键字,来判断一个对象是什么类的实例;

语法obj instanceof className,会返回一个布尔值:

  • 如果classNameobj原型链上的类,返回true;
  • 否则,返回false;

它是怎么去判断的呢?假设现在有如下几个类:

class A {};
class B extends A {};
class C extends B {};
let c = new C();

c的原型是C.prototype

C.prototype的原型是B.prototype

B.prototype的原型是A.prototype

A.prototype的原型是Object.prototype

Object.prototype的原型是null;

原型链如上所示;

当我们执行c instanceof A的时候,它是这样的过程:

c.__proto__ === A.prototype?否,则继续;

c.__proto__.__proto__ === A.prototype?否,则继续;

c.__proto__.__proto__.__proto__ === A.prototype?是,返回true;

如果一直否的话,这个过程会持续下去,直到将c的原型链溯源到null,全都不等于A.prototype,则返回false;

也就是说,instanceof关键字,比较的是对象的原型链上的原型和目标类的prototype是否相等(原型和prototype里有constructor,但是instanceof不会比较构造器是否相等,只会比较隐藏属性[[Prototype]]);

静态方法Symbol.hasInstance

大多数类是没有实现静态方法[Symbol.hasInstance]的,如果有一个类实现了这个静态方法,那么instanceof关键字会直接调用这个静态方法;

如果类没有实现这个静态方法,那么则会按照上述说的流程去检查;

class className {
    static [Symbol.hasInstance]() {};
}

objA.isPrototypeOf(objB)

isPrototypeOf()方法,会判断objA的原型是否处在objB的原型链中,如果在则返回true,否则返回false;

objA.isPrototypeOf(objB)就相当于objB instanceof classA

反过来,objB instanceof classA就相当于classA.prototype.isPrototypeOf(objB)

继承

我们知道,JS的继承,是通过原型来实现的,现在结合原型来说一下类的继承相关内容。

关键字extends

JS中表示继承的关键字是extends,如果classA extends classB,则说明classA继承classBclassA是子类,classB是父类;

原型高于extends

时刻记住,JS的继承,是依靠原型来实现的;

关键字extends虽然确立了两个类的父子关系,但是这只是一开始确立子类的父原型;

但是父原型是可以中途被修改的,此时子类调用方法,是沿着原型链去寻找的,而不是沿着子类父类的关键字声明去寻找的,这和Java是不一样的:

如图所示,C extends A确立了C一开始的父原型是A.prototypec.show()调用的也是父类A的方法;

但是后面修改c的父原型为B.prototypec.show调用的就不是父类A的方法,而是父原型的方法;

也就是说,原型才是核心,高于extends关键字;

基类和派生类

class classA {};
class classB extends classA {};

classA这样没有继承任何类(实际上父原型是Object.prototype)的类称为基类;

classB这样继承classB的类,称为classB的派生类;

为什么要分的这么细,是因为在创建类时,他们两个的行为不同,后面会说到;

类的原型

类本身也是有原型的,就像类对象有原型一样;

可以看到,B的原型就是其父类A,而A作为基类,基类的原型是本地方法;

正因如此,B可以通过原型去调用A的静态方法/属性;

也就是说,静态方法/属性,也是可以继承的,通过类的原型去继承;

类对象的原型和类的prototype属性

在创建类对象的时候,会将类的prototype属性值复制给类对象的原型;

所以说,类对象的原型等于类的prototype属性值;

而类的prototype属性,默认就有两个属性:

  • 构造器constructor:指向类本身;
  • 原型[[Prototype]]:指向父类的prototype属性;

以及

  • 类的普通方法;

从上图中可以看出,A的prototype属性里,除构造器和原型以外,就只有一个普通方法show()

这说明,只有类的普通方法,会自动进入类的prototype属性参与继承;

也就是说,一个类对象的数据结构,如下:

  • 普通属性
  • (原型)prototype属性
    • 构造器
    • 父类的prototype属性(父原型)
    • 方法

另外,类的prototype属性是不可写的,但是类对象的原型则是可以修改的;

继承了哪些东西

当子类去继承父类的时候,到底继承到了父类的哪些东西,也即子类可以用父类的哪些内容;

从结果上来看,我们可以确定如下:

  • 子类继承父类的静态属性/方法(基于类的原型);
  • 子类对象继承父类的普通方法和构造器(基于类的prototype);
  • 子类直接将父类的普通属性作为自己的普通属性(普通属性不参与继承);

由于原型链的存在,这些继承会一路沿着原型链回溯,继承到所有祖宗类;

同名属性的覆盖

由于继承的机制,势必子类和父类可能会有同名属性的存在:

从结果上可以看到,虽然子类直接将父类的普通属性作为自己的普通属性,但是当出现同名属性,属性值会进行覆盖,最终的值采用子类自己定义的值;

同名方法的重写

与属性一样,子类和父类也可能会出现同名方法;

当然大多数情况下,是我们自己要拓展方法功能而故意同名,从而重写父类的方法;

如上所示,我们重写了父类的静态方法和普通方法;

如果是重写构造器的话,分两种情况:

// 基类重写构造器
class A {
    constructor() {
        code...
    }
}
    
// 派生类重写构造器
class B extends A() {
    constructor() {
        // 一定要先写super()
        super();
        code...
    }
}

子类的调用顺序

从上图还可以看出来,子类调用方法的顺序:

  • 先从自己的方法里调用,发现没有可调用的方法时;
  • 再沿着原型链,先从父类开始寻找方法,一直往上溯源,直到找到可调用的方法,或者没有而出错;

super关键字

类的方法里,有一个特殊的、专门用于super关键字的特殊属性[[HomeObject]],这个属性绑定super语句所在的类的对象,不会改变;

super关键字,则指向[[HomeObject]]绑定的对象的类的父类的prototype

这要求,super关键字用于派生类类的方法里,基类是不可以使用super的,因为没有父类;

当我们使用super关键字时,借助于[[HomeObject]],总是能够正确重用父类方法;

如上,super语句所在的类为B,其对象为b,即[[HomeObject]]绑定b

super则指向b的类的父原型,即A的prototype属性;

super.show()就类似于A.prototype.show(),故而最终结果如上所示;

可以简单理解成,super指向子类对象的父类的prototype

构造器constructor

终于说到构造器了,理解了构造器的具体创建对象的过程,我们就能理解关于继承的很多内容了;

先来看一下基类的构造器创建对象的过程:

执行let a = new A()时,大致流程如下:

  • 首先调用A.prototype的特性[[Prototype]]创建一个字面量对象,同时this指针指向这个字面量对象;
  • 然后执行类A()的定义,A定义的普通属性成为字面量对象的属性并初始化,A.prototypevalue值复制给字面量对象的隐藏属性[[Prototype]]
  • 然后再执行constructor构造器,没有构造器就算了;
  • 返回this指针给变量a,即a此时引用该字面量对象了;

从结果上看,在执行构造器时,字面量对象就已经有原型了,以及属性name,且值初始化为tomA

然后才对属性name重新赋值为jerryA

然而,构造器中对属性的重新赋值,从一开始就决定好了,只是在执行到这句赋值语句之前,暂存在字面量对象中;

现在再来看一下派生类创建对象的过程;

执行let b = new B()的大致流程如下:

  • 首先调用B.prototype的特性[[Prototype]]创建一个字面量对象,同时this指针指向这个字面量对象;
  • 然后执行类B()的定义,B定义的普通属性成为字面量对象的属性并初始化,B.prototypevalue值复制给字面量对象的隐藏属性[[Prototype]]
  • 然后再执行constructor构造器(没有显式定义构造器会提供默认构造器),第一句super(),开始进入类A()的定义;
    • 暂存B的属性值,转而赋值为A定义的值,A.prototypevalue值复制给B.__proto__的隐藏属性[[Prototype]];
    • 然后执行constructor构造器(基类没有构造器就算了);
    • 返回this指针;
    • 丢弃A赋值的属性值,重新使用暂存的B的属性值;
  • 继续执行constructor构造器剩下的语句;
  • 返回this指针给变量b,即b引用该字面量对象了;

通过基类和派生类创建对象的流程对比,可以发现主要区别在于类的属性的赋值上;

属性值从一开始就已经暂存好:

  • 如果构造器constructor中有赋值,则暂存这个值;
  • 如果构造器没有,则暂存类定义中的值;
  • 不管父类及其原型链上同名的属性在中间进行过几次赋值,最终都会重新覆盖为最开始就暂存好的值;
0 条评论
请不要发布违法违规有害信息,如发现请及时举报或反馈
还没有人评论呢,速度抢占沙发!
相关文章
  • 第一步 打开jsjiami.com第二步 将源代码复制到输入框中function prmAppRouter() { var prm = arguments.length > 0 && argu...

  • 本文实例讲述了JS实现的获取银行卡号归属地及银行卡类型操作以及Luhn校验算法校验银行卡号算法。分享给大家供大家参考,具体如下: javascript代码如下 /** * Luhn校验算法校验银行卡...

  • 什么是函数式编程是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推...

  • 本文翻译自 Intercepting JavaScript Fetch API requests and responses 拦截器是可用于预处理或后处理 HTTP 请求的代码块,有助于全局错误处理、...

  • 本文简介点赞 + 关注 + 收藏 = 学会了在 《Fabric.js 使用纯色遮挡画布》 中讲到使用纯色的方式遮盖画布。如果你的常见需要使用图片来遮盖的话,fabric.js 也提供了相应的属性来配置...

  • 好家伙,本篇为《JS高级程序设计》第六章“集合引用类型”学习笔记 1.Map ECMAScript6以前,在JavaScript中实现“键/值”式存储可以使用object来方便高效地完成,也就是使用...

  • 01、JS函数基础 1.1、函数定义 函数(方法)就是一段定义好的逻辑代码,函数本身也是一个object引用对象。三种函数构造方式: 🔸① 函数申明:function 函数名(参数){代码},申明函...

  • /** * 复制器 * @param {[String]} content 需要复制的内容 */ function copier(content) { let textarea = ...

  • 写在前面 模块化开发是我们日常工作潜移默化中用到的基本技能,发展至今非常地简洁方便,但开发者们(指我自己)却很少能清晰透彻地说出它的发展背景, 发展过程以及各个规范之间的区别。故笔者决定一探乾坤,深入...

  • JS中的递归我们来看一个阶乘的代码function foo( n ){ if(n <= 1){ return 1; } return n * foo( n - 1 ); } fo...

  • JS逆向之补环境过瑞数详解 “瑞数” 是逆向路上的一座大山,是许多JS逆向者绕不开的一堵围墙,也是跳槽简历上的一个亮点,我们必须得在下次跳槽前攻克它!! 好在现在网上有很多讲解瑞数相关的文章,贴心的一...

  • 〇、前言 js 在日常开发中还是比较常用的,本文将常用的 js 方法简单汇总一下,希望对你我有一点帮助。 一、重复 / 延迟操作 1.设置固定时间间隔,重复执行(setInterval(funcRef...

  • 01-骰子游戏 游戏出自Udemy的JS课程中提到的一个游戏,课程主要是对JS部分进行详细的从0开始的讲解,本篇文章是对整个游戏的分析,包括HTMK,CSS和JS,也主要对JS进行刨析。 游戏链接:h...

  • JavaScript合集 学完HTML5+CSS3的小伙伴,学习JS时,要多敲多练多想多拓展 刚开始入门JS的时候,我们不需要纠结那么多,有些需要先记住,后面会慢慢明白为什么是这样的 JS基础部分 我...

  • 〇、简介 椭圆曲线密码学(Elliptic curve cryptography:ECC),一种建立公开密钥加密的演算法,基于椭圆曲线数学。利用有限域上椭圆曲线的点构成的 Abel 群离散对数难解性,...

  • 回调地狱是由于多个回调函数嵌套引起的: 回调函数:一个函数以参数的形式传入另一个函数。如: $.post(url,function(data){ }) 回调地狱: 上面那个是嵌套了一层,好像看上去很正...

  • 首先我们知道JavaScript引擎包括一个调用栈和堆,调用栈是代码实际执行的地方,使用执行上下文(执行环境)来完成;堆是非结构化的内存池,存储了应用程序所需要的所有对象。 执行上下文是什么? 执行上...

  • 前言this关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。简单说,this就是属性或方法“当前”所在的对象。this.property上面代码中,this就代表...

  • JavaScript 中,对于普通对象,不能直接使用 length 来获取对象的长度,因为 JavaScript 对象并不是一种有序的集合,没有长度的概念。 对于数组或者类数组对象,可以使用 .len...

  • 案例介绍 欢迎来的我的小院,我是霍大侠,恭喜你今天又要进步一点点了!我们来用JavaScript编程实战案例,做一个乘法积分游戏。乘法游戏主要通过用户输入的数值和程序计算的数值进行对比,正确积一分,错...