JavaScript:类(class) - Journing
source link: https://www.cnblogs.com/Journing/p/17002793.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
在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
,会返回一个布尔值:
- 如果
className
是obj
原型链上的类,返回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
继承classB
,classA
是子类,classB
是父类;
原型高于extends
时刻记住,JS的继承,是依靠原型来实现的;
关键字extends
虽然确立了两个类的父子关系,但是这只是一开始确立子类的父原型;
但是父原型是可以中途被修改的,此时子类调用方法,是沿着原型链去寻找的,而不是沿着子类父类的关键字声明去寻找的,这和Java是不一样的:
如图所示,C extends A
确立了C一开始的父原型是A.prototype
,c.show()
调用的也是父类A
的方法;
但是后面修改c
的父原型为B.prototype
,c.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.prototype
的value
值复制给字面量对象的隐藏属性[[Prototype]]
; - 然后再执行
constructor
构造器,没有构造器就算了; - 返回
this
指针给变量a
,即a
此时引用该字面量对象了;
从结果上看,在执行构造器时,字面量对象就已经有原型了,以及属性name
,且值初始化为tomA
;
然后才对属性name
重新赋值为jerryA
;
然而,构造器中对属性的重新赋值,从一开始就决定好了,只是在执行到这句赋值语句之前,暂存在字面量对象中;
现在再来看一下派生类创建对象的过程;
执行let b = new B()
的大致流程如下:
- 首先调用
B.prototype
的特性[[Prototype]]
创建一个字面量对象,同时this
指针指向这个字面量对象; - 然后执行类
B()
的定义,B
定义的普通属性成为字面量对象的属性并初始化,B.prototype
的value
值复制给字面量对象的隐藏属性[[Prototype]]
; - 然后再执行
constructor
构造器(没有显式定义构造器会提供默认构造器),第一句super()
,开始进入类A()
的定义;- 暂存
B
的属性值,转而赋值为A
定义的值,A.prototype
的value
值复制给B.__proto__
的隐藏属性[[Prototype]]
; - 然后执行
constructor
构造器(基类没有构造器就算了); - 返回
this
指针; - 丢弃
A
赋值的属性值,重新使用暂存的B
的属性值;
- 暂存
- 继续执行
constructor
构造器剩下的语句; - 返回
this
指针给变量b
,即b
引用该字面量对象了;
通过基类和派生类创建对象的流程对比,可以发现主要区别在于类的属性的赋值上;
属性值从一开始就已经暂存好:
- 如果构造器
constructor
中有赋值,则暂存这个值; - 如果构造器没有,则暂存类定义中的值;
- 不管父类及其原型链上同名的属性在中间进行过几次赋值,最终都会重新覆盖为最开始就暂存好的值;
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK