ES6之Class笔记

158 阅读9分钟

本文是我读阮一峰大佬的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作为对象时
  1. 在普通方法中指定父类的原型对象
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)

  1. 在静态方法中指向父类

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__属性

  1. 子类的__proto__属性,表示构造函数的继承,总是指向父类。
  2. 子类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允许继承原生构造函数定义子类