JS Advance --- class

337 阅读10分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

使用构造函数形式创建 ,不仅仅和编写普通的函数过于相似,而且代码并不容易理解

在ES6 (ECMAScript2015) 新的标准中使用了class关键字来直接定义类

但是类本质上依然只是构造函数、原型链的语法糖而已

唯一的区别是类必须使用 new 调用, 无法像一个普通函数那样被调用

类的创建

可以使用两种方式来声明类: 类声明类表达式

类声明

class Person {}

类表达式

const Person = class {}

类的特点

class Person {}

console.log(Person.prototype) // => {}
console.log(typeof Person) // => function
// ES6本质上还是function Person { .... }

// 所以类上依旧存在name属性,值为类名
console.log(Person.name) // Person

const per = new Person()
console.log(per.__proto__  === Person.prototype) // => true
// 函数表达式也是有name属性的
const Person = class {}
console.log(Person.name)

类中定义的属性和方法之间可以使用换行进行分割,也可以使用分号使用分割,但是不可以使用逗号进行分割

class User {
  constructor(name, age) {
    this.name = name;
    this.age = age
  }; // 使用分号进行分割 success

  printName() {
    console.log(name)
  } // 直接换行进行分割 success
  
  printAge() {
    console.log(age)
  }, // 使用逗号进行分割 error
  
  sayHello() {
    console.log('Hi~')
  }
}

类的构造函数

我们在使用构造函数来创建实例的时候,经常需要传入一些参数

而在类中,将类的声明和类的构造函数进行了分离

每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor

当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor

如果我们没有显示声明类的构造函数的时候,会存在如下的默认构造函数

// 这个默认构造函数的返回值就是所创建的那个实例对象
constructor() {}

当我们显示声明了自己的构造函数的时候,会覆盖默认的构造函数,而开始使用我们自己的构造函数

每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常

构造函数所做的功能和使用ES5构造方法创建函数的功能是一致的

  1. 在内存中创建一个新的对象(空对象)

  2. 这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性

  3. 构造函数内部的this,会指向创建出来的新对象

  4. 执行构造函数的内部代码(函数体代码)

  5. 如果构造函数没有返回对象,则返回创建出来的新对象

    如果构造函数返回了对象,那么所创建出来的就是返回的那个对象

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
}

const per = new Person('Klaus', 23)

console.log(per) // => { name: 'Klaus', age: 23 }
console.log(per instanceof Person) // => true
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    return {}
  }
}

const per = new Person('Klaus', 23)

// 如果构造函数返回了对象,那么生成的就是返回的那个对象
console.log(per) // => {}

// 此时返回的那个对象并不是Person的实例对象
console.log(per instanceof Person) // => false

class filelds

以前,我们定义实例属性,只能写在类的 constructor 方法里面

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
}

ES6中提供了一种新的在类中定义属性的方式,即class fields

class Person {
  // address 就是属性的class fields
  // 可以直接赋值(如:address = 23)
  // 也可以直接定义,不赋值(如:address), 此时的默认值就是undefined
  address
  
  // 使用class fields形式定义的属性和方法可以被定义在类的任何位置
  // 但是约定俗成 在class fields定义的属性和方法中
  // 属性 一般定义在构造函数的上边
  // 方法 一般定义在构造函数的下边

  constructor(name, age, address) {
    this.name = name
    this.age = age
    // 为class fields 属性赋值
    this.address = address
  }
}

const per = new Person('Klaus', 24, 'shanghai')
console.log(per) // => { address: 'shanghai', name: 'Klaus', age: 24 }

使用class fields方式定义的属性和在构造函数中定义的属性,在功能和表现形式上都是一致的

class fields的一个应用场景就是 定义类的静态属性

不使用 class fields

class Person {}

Person.name = 'Klaus';

使用 class fields

class Person {
  static name = 'Klaus';
}

new.target

ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

function Person() {
  console.log(new.target)
}

Person() // => undefined
new Person() // => Person函数的代码体
class Person {
  constructor() {
    console.log(new.target)
  }
}

new Person() // => Person类对应的代码体
// 我们可以通过new.target来限制某些实例只能通过其子类的构造函数来创建
// 不能通过其父类的构造函数来创建
class Person {
  constructor(name, age) {
    if (new.target === Person) {
      throw new TypeError('Person can not be instantiated')
    }

    this.name = name
    this.age = age
  }
}

class Student extends Person {
  constructor(name, age) {
    super(name, age)
  }
}

console.log(new Student('Klaus', 23)) // success
console.log(new Person('Klaus', 23)) // error

类的方法

实例方法

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  // 这里定义的方法是类的实例的方法
  // 这些方法会被定义到类的prototype上
  running() {
    console.log('running')
  }
}

const per = new Person('Klaus', 23)

per.running() // => running
Person.prototype.running() // => running

// 类中所有定义的方法,都是不可枚举的(non-enumerable)
// 类中所有的属性,默认都是可枚举的
console.log(per)

访问器方法

class Person {
  // 定义私有属性
  #address = '上海市'

  constructor(name, age) {
    this.name = name
    this.age = age
  }
    
  // 访问器方法在操作上看上去和操作属性没什么区别
  // 但是其本质是方法,所以编译后,访问器方法是定义在Person.prototype上的
  
  // 使用访问器方法 我们可以对某一个属性的设置和取值操作进行拦截操作
   
  // 访问器 --- 获取 --- 取值函数
  get address() {
    return this.#address
  }

  // 访问器 --- 设置 --- 存值函数
  set address(v) {
    this.#address = v
  }
}

const per = new Person('Klaus', 23)

console.log(per.address) // => 上海市
per.address = '广州市'
console.log(per.address) // => 广州市

静态方法

类的静态方法又被称之为类方法

和类的实例方法和类的访问器方法不同的是类的静态方法是定义在类本身上的

只能通过类来进行调用,不能通过类的实例来进行调用

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  static running() {
    console.log('running')
  }
}

const per = new Person('Klaus', 23)

Person.running() // => runnning
per.running() // error

继承

在ES6中新增了使用extends关键字,可以方便的帮助我们实现继承

// 父类 -- 超类
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  eatting() {
    console.log('eatting')
  }

  static running() {
    console.log('running')
  }
}

// 子类 --- 派生类
class Student extends Person {
  // 如果子类中没有定义构造函数,会存在默认的构造函数
  // 这个构造函数会自动去调用父类的构造函数进行初始化操作
  /*
     constructor() {
       super(...arguments)
     }
  */
}

const stu = new Student('Klaus', 23)

console.log(stu.name, stu.age) // => Klaus, 23

// 子类继承了父类的实例方法
stu.eatting() // => eatting

// 子类同样也可以继承父类的静态方法和访问器方法
Student.running() // => running
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  eatting() {
    console.log('eatting')
  }

  static running() {
    console.log('running')
  }
}

class Student extends Person {
  constructor(name, age, sno) {
    // 注意:在子(派生)类的构造函数中使用this或者返回默认对象(即创建出来的实例对象)之前,必须先通过super调用父类的构造函数
    super(name, age)
    this.sno = sno
  }
}

const stu = new Student('Klaus', 23, 1810166)

console.log(stu.name, stu.age, stu.sno) // => Klaus, 23, 1810166

super

在ES6中,必须在子类的构造函数中调用super方法以初始化父类实例

也就是说在子类的构造函数中,只有先调用了super方法才可以使用this关键字

class Parent {
  constructor(name) {
    this.name = name
  }
}

class Student extends Parent {
  constructor(name, age) {
    // 在子类中必须先调用super方法后
    // 才可以使用this关键字
    super(name)
    // 此时this其实就是子类的实例对象
    this.age = age
  }
}
// 以下是使用babel进行ES6转ES5代码后的部分继承代码
var Parent = function Parent() {
  _classCallCheck(this, Parent);
};

var Student = /*#__PURE__*/ (function (_Parent) {
  _inherits(Student, _Parent);

  var _super = _createSuper(Student);

  function Student(name) {
    // 子类实例 --- 默认为undefined
    var _this;

    _classCallCheck(this, Student);

    // 1. 调用父类构造函数(在调用的时候this为子类实例)
    // 所以虽然调用的是父类的构造方法,但是对于的属性依旧是存在子类实例自身上的
    // 并不存在于父类实例或父类原型上
    _this = _super.call(this);
    
    // 2. 只有调用super方法后,子类实例才会有值,才可以在子类实例上添加子类自身独有的属性
    // 所以在ES6的class中,子类必须先调用super方法初始化子类实例后,才可以使用this关键字
    _this.name = name;
    return _this;
  }

  return Student;
})(Parent);

在ES6中,super既可以作为函数进行调用,也可以作为对象进行使用

super作为函数进行使用

class Parent {
  constructor(name) {
    this.name = name
  }
}

class Student extends Parent {
  constructor(name, age) {
    // super作为方法使用代表着父类的构造函数
    // 而且如果super作为函数使用,那么只能在子类的构造函数中使用
    super(name)
    this.age = age
  }
}

super作为对象进行使用

class Parent {
  constructor() {
  }

  print() {
    console.log('print')
  }

  static staticMethod() {
    console.log('staticMethod')
  }
}

class Student extends Parent {
  constructor() {
    super()
  }

  print() {
    // 调用父类的实例方法
    // 这里的super就是Object.create(Parent.prototype)所创建出来的那个对象
    super.print()
  }

  static subStaticMethod() {
    // 调用父类的静态方法
    // 这里的super其实就是父类自身
    super.staticMethod()
  }
}

在任何使用,super都不可以直接被使用

class Parent {
  constructor() {
  }
}

class Student extends Parent {
  constructor() {
    // super只能作为函数使用或作为对象使用(去取对应的属性)
    // 不可以直接打印和使用super关键字
    console.log(super) // error
  }
}

重写

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  eatting() {
    console.log('eatting')
  }

  static running() {
    console.log('running')
  }
}

class Student extends Person {
  constructor(name, age, sno) {
    super(name, age)
    this.sno = sno
  }

  eatting() {
    console.log('student eatting')
  }

  static running() {
    console.log('student running')
  }
}

const stu = new Student('Klaus', 23, 1810166)

stu.eatting() // => student eatting

Student.running() // => student running
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  eatting() {
    console.log('eatting')
  }

  static running() {
    console.log('running')
  }
}

class Student extends Person {
  constructor(name, age, sno) {
    super(name, age)
    this.sno = sno
  }

  eatting() {
    // 可以使用super关键字在子类方法中调用父类中对应的方法(包括实例方法和静态方法)
    super.eatting()
    console.log('student eatting')
  }

  static running() {
    super.running()
    console.log('student running')
  }
}

const stu = new Student('Klaus', 23, 1810166)

stu.eatting()
/*
  =>
    eatting
    student eatting
*/

Student.running()
/*
  =>
    eatting
    student eatting
*/

继承自内置类

在JS中Object是所有类的父类

class Person {}
// 等价于
class Person extends Obejct {}
// 也就是在JS中如果不指定父类,那么会默认将其父类给设置成Object类
// 相当于 extends Obejct 是可以省略的,是缺省值

所以我们也是可以继承自内置类

class CustomArray extends Array {
  getFirstItem() {
    return this[0]
  }

  getLastItem() {
    return this[this.length - 1]
  }
}

const customArr = new CustomArray(1, 2, 3)
console.log(customArr.getFirstItem()) // => 1
console.log(customArr.getLastItem()) // => 3

类的混入

JavaScript的类只支持单继承:也就是只能有一个父类,但是希望在JS中模拟多继承的效果

也就是在继承一个类后,再使用继承别的方法的时候,就可以使用类的混入

JS本身是没有实现类的混入的,但是可以通过JS函数式编程的特点来模拟类的混入

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
}

// 需要被混入的方法
function running() {
  console.log('running')
}

// 模拟混入
function mixins(className, mixins) {
  class DeliverClass extends Person {}

  for (let k in mixins) {
    DeliverClass.prototype[k] = mixins[k]
  }

  return DeliverClass
}

const mixinPerson = mixins('Person', {
  running
})

const per = new mixinPerson('Klaus', 23)
console.log(per.name)
console.log(per.age)
per.running()

多态

多态即为不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现

也就是不同数据类型的变量调用同一个函数,产生了不同的执行效果

// 传统的面向对象多态是有三个前提:
// 1> 必须有继承(继承是多态的前提)
// 2> 必须有重写(子类重写父类的方法)
// 3> 必须有父类引用指向子类对象(数据类型为子类的变量赋值给数据类型为父类的变量)

// 父类
class Shape {
  getArea() {}
}

// 子类
class Rectangle extends Shape {
  // 重写父类方法
  getArea() {
    console.log('getArea in Rectangle')
  }
}

class Cricle extends Shape {
  getArea() {
    console.log('getArea in Cricle')
  }
}

// 父类引用指向子类对象
const r: Shape = new Rectangle()
const c: Shape = new Cricle()

r.getArea() // => getArea in Rectangle
c.getArea() // => getArea in Cricle

但是JS中,并没有像传统面向对象语言对于多态的限制

const o = {
  getArea() {
    console.log('getArea in o')
  }
}

function Person() {}
Person.prototype.getArea = () => console.log('getArea in Person')
const per = new Person()

o.getArea() // => getArea in o
per.getArea() // => getArea in Person
function sum(a, b) {
  console.log(a + b)
}

sum(2, 3) // => 5
sum('abc', 'cba') // => abccba