本文是我读阮一峰大佬的ES6class相关章节的笔记,旨在提高自己,供学习参考,查缺补漏。
class的基本语法
类的由来
js中,生成实例对象的传统方法是通过构造函数
function Point(x,y){
this.x = x
this.y = y
}
Point.prototype.toString = function(){
return '(' + this.x + ',' + this.y + ')'
}
vat p = new Point(1,2)
ES6的class基本上就是一个语法糖,绝大部分功能,ES5也能做到
class Point{
constructor(x,y){
this.x = x
this.y = y
}
toString(){
return '(' + this.x + ',' + this.y + ')'
}
}
上面代码定义了一个类,constructor()方法就是构造方法,而this关键字就代表实例对象。
ES6的类,完全可以看做构造函数的另一种写法,
class Point{
// ....
}
typeof Point // 'function'
Point === Point.prototype.constructor // true
上面代码就表明了,类的数据类型就是构造函数,类本身就指向构造函数
构造函数的prototype属性,在ES6的类上继续存在,事实上,类的所有方法都定义在类的prototype属性上面。
class Point {
constructor(){
// ....
}
toString(){
// ...
}
toValue(){
// ....
}
}
// 等同于
Point.prototype = {
constructor(){},
toString(){}
toValue(){}
}
所以 在类的实例上调用方法,其实就是调用原型上的方法
class B{}
const b = new B()
b.constructor === B.prototype.constructor // true
b是B的实例,他的constructor()方法就是B类原型的constructor()方法
由于类的方法都是定义在prototype对象上,所以类的新方法可以添加在prototype对象上,Object.assign()方法可以一次向类添加多个方法。
class Point{
constructor(){}
}
Object.assign(Point.prototype,{
toString(),
toValue()
})
prototype对象的constructor属性,直接指向类的本身
Point.prototype.constructor === Point // true
类的内部的所有方法都是不可枚举的
class Point{
constructor(){}
toString(){}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
下面这种写法是可枚举的
var Point = function(x,y){
}
Point.prototype.toString = function(){}
Object.keys(Point.prototype)
// ["toString"]
Objcet.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
contructor()方法
constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法,一个类必须有constructor方法,如果没有显示的定义,一个空的constructor方法会被默认添加
constructor()方法默认返回实例对象(this),也可以指定返回另一个对象
class Point{
constructor(){
reutrn Object.create(null)
}
}
new Point instanceof Point // false
类必须使用new调用,这也是和普通构造函数的主要区别,构造函数不用new也能调用
类的实例
生成类的实例,就用new调用类 类的属性和方法,除非显示的定义在本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)
class Point{
constructor(x,y){
this.x = x
this.y = y
}
toString(){}
}
var point = new Point(2,3)
// x和y都定义在this对象上
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
// toString是原型对象的属性(因为定义在Point类上)
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
所有的实例共享一个原型对象
var p1 = new Point(1,2)
var p2 = new Point(2,3)
p1.__proto__ === p2.__proto__ // true
可以通过实例的__proto__属性为‘类添加方法’(注意:__proto__不是语言本身的特性,谨慎使用)
var p1 = new Point(1,2)
var p2 = new Point(2,3)
p1.__proto__.printName = function (){return 'abc'}
// p1的原型就是p2的原型,所以p2也能调用这个方法
p1.printName() // 'abc'
p2.printName() // 'abc'
实例属性的新写法
ES2022规定了一种实例属性的新写法,就是 实例属性也可以定义在类内部的最顶层。
取值函数(getter)和存值函数(setter)
类的内部也可以使用get和set关键字,对某个属性的存取进行拦截
class Point{
get prop(){
return 'getter'
}
set prop(value){
console.log('setter:' + value)
}
}
let inst = new Point()
inst.prop = 123
// setter: 123
inst.prop // getter
存值函数和取值函数都是设置在属性的Descriptor对象上的
属性表达式
类的属性名可以采用表达式
let methodName = 'getArea'
class S = {
constructor(){}
// 方法名是从表达式得到的
[methodName](){}
}
Class表达式
和函数一样,类也可以用表达式的形式定义
const MyClass = class Me {
getClassName(){
return Me.name
}
}
let m = new MyClass()
m.getClassName() // Me
Me.name // 报错
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承,如果在一个方法前,添加static关键字,就表示这个方法不会被实例继承,而是直接通过类本身来调用,这就是静态方法
class Foo{
static classMethod(){
return 'hello'
}
}
Foo.classMethod() // hello
let foo = new Foo()
foo.classMethod() // 报错
如果静态方法包含this关键字,那么这个this指向类(类本身),而不是实例
class Foo{
static bar(){
this.baz() // 这里的this指向Foo类,而不是Foo的实例 等同于调用了Foo.baz()
}
static baz(){
console.log('a')
}
baz(){
console.log('b')
}
}
Foo.bar() // a
父类的静态方法可以被子类继承
静态属性
静态属性是指Class本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性
私有方法和私有属性
ES2022为Class添加了私有属性和私有方法,就是在属性或方法名之前加#
class Foo{
#count = 1 // 私有属性,只能在类的内部使用(this.#count)
// 私有方法 只能在类的内部使用
#sum(){
}
}
in运算符
直接访问类不存在的私有属性会报错,但是访问类的公开属性不会报错 ES2022改进了 in 运算符,可以用来判断私有属性
class C{
#a
static isC(obj){
if(#a in obj){
// 私有属性#a存在
return true
} else {
// 私有属性#a不存在
return false
}
}
}
⚠️注意,in 运算符对于Object.create()、Object.setPrototypeOf形成的继承,是无效的,因为这种是修改了原型链的继承,子类取不到父类的私有属性
new.target属性
用来确定构造函数是怎么调用的
class的继承
class可以通过extends关键字实现继承
class A {}
class B extends A {}
子类必须在constructor()方法中调用super(),否则就会报错。因为子类的this对象,必须先通过父类的构造函数完成塑造,得到父类同样的实例属性和方法。
私有属性和私有方法的继承
子类无法继承父类的私有属性和私有方法(PS:其实从‘私有’的这个名字也大概知道这个意思)
但是如果父类定义了一个读取私有属性的公有方法,那么子类可以通过这个方法获取到私有属性
class A {
#a = 1
getA(){
return this.#a
}
}
class B extends A {
constructor(){
super()
console.log(this.getA()) /// 1
}
}
静态属性和静态方法的继承
父类的静态属性和静态方法也会被继承。
❗但是需要注意的是:静态属性的继承是浅拷贝的
class A {
static foo = {
n:100
}
}
class B extends A {
constructor(){
super()
B.foo.n--
}
}
let b = new B()
B.foo.n // 99
A.foo.n // 99
// 因为是浅拷贝,继承的是对象的内存地址,指向的是同一个对象
Object.getPrototypeOf()
Object.getPrototypeOf()方法可以从子类上获取父类,也就是说可以用来判断一个类是否继承了另一个类。
super关键字
super关键字,既能当函数使用,又能当对象使用。
- super作为函数调用时,代表父类的构造函数
ES6规定,子类的构造函数必须执行一次super()函数。
为什么必须要调用一次super()函数呢,因为super()函数是为了形成子类的this对象,把父类的属性和方法放在这个this对象上,子类在调用super()之前是没有this对象的,所有对this对象的操作必须都放在super()之后
class A {
constructor(){
console.log(new.target.name)
}
}
class B extends A {
constructor(){
super() // super()内部的this指向B
}
}
new A() // A
new B() // B
上面的代码,虽然这里的super()代表了父类的构造函数,但是因为返回的是子类的this(子类的实例对象),所以super()内部的this代表了子类的实例,而不是父类的实例,这里的super()相当于A.prototype.constructor.call(this)(在子类的this上运行父类的构造函数)
由于super()在子类的构造函数中运行,所以子类的属性和方法还没绑定到this上(这个很好理解,上面刚提到调用super()就是为了形成子类的this对象,还没调用super(),当然不会绑定到this上),所以如果有同名的属性,此时获取的还是父类的属性
super()只能用在子类的构造函数中,用在其他函数中会报错。
- super作为对象时
- 在普通方法中指定父类的原型对象
class A {
getP(){
return 1
}
}
class B extends A {
constructor(){
super()
// 在普通方法中,指向父类的原型对象,相当于A.prototype.getP()
console.log(super.getP())
}
}
new B() // 1
❗因为指向原型对象,所以在父类实例上定义的属性或者方法,无法通过super对象获取到。
ES6规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前子类的实例。
class A {
constructor(){
this.x = 1
}
print(){
console.log(this.x)
}
}
class B extends A{
constructor(){
super()
this.x = 2
}
m(){
super.print()
}
}
const b = new B()
b.m() // 2
上面的代码中的super.print()相当于A.prototype.print(),内部的this指向B的实例。也就是说相当于super.print.call(this)
- 在静态方法中指向父类
super在静态方法之中指向父类,在普通方法之中指向父类的原型对象
(个人理解:因为静态方法不能被实例继承,只能通过类的本身去调用)
class A {
static myA(msg){
console.log('static',msg)
}
myA(msg){
console.log('intance',msg)
}
}
class B extends A {
static myB(msg){
super.myA(msg) // super指向父类,this指向当前的子类
}
myB(msg){
super.myA(msg) // super指向父类的原型对象,this指向当前的子类的实例
}
}
B.myB(1) // static 1
const b = new B()
b.myB(2) // intance 2
类的prototype属性和__proto__属性
- 子类的
__proto__属性,表示构造函数的继承,总是指向父类。 - 子类
prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A{}
class B extends A {}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
这样是因为 类的继承是通过Object.setPrototypeOf()方法实现的
// Object.setPrototypeOf的实现
Object.setPrototypeOf = function(obj,proto){
obj.__proto__ = proto
return obj
}
// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype)
// B继承A的静态属性
Object.setPrototypeOf(B, A)
作为一个对象,子类B的原型(__proto__属性)是父类A。
作为一个构造函数,子类(B)的原型对象(prototype)是父类(A)的原型对象(prototype属性)。
实例的__proto__属性
子类实例的__proto__的__proto__属性指向,父类实例的__proto__属性,也就是说子类原型的原型指向父类的原型
class A{}
class B extends A{}
const b = new B()
const a = new A()
b.__proto__.__proto__ === a.__proto__ // true
原生构造函数的继承
原生构造函数有:
String()、Number()、Boolean()、Array()、Date()、Function()、Error()、Object()、RegExp()
ES6允许继承原生构造函数定义子类