TS与JS中的类 | 青训营笔记

55 阅读7分钟

JS 中的类

ES6开始引入了class关键字,这个关键字具有正式定义类的能力

类的定义

与函数的定义类似,类的定义也有两种方式

  • 类声明

    • class User {}
      
  • 类表达式

    • const user = class User {};
      

与函数表达式相同,类表达式在定义之前不能引用

二者的不同点

  1. 函数声明可以提升,但是类声明不可以
  2. 函数受函数作用域限制,类受块作用域限制

类的构成

类中可以包含类构造函数、实例方法、获取方法、设置方法和静态方法 ,但是这些都不是必须的,也就是说你定义一个空类也是有效的

类构造函数

constructor关键字用于在类定义块内部创建类的构造函数,在使用new操作符创建类的实例对象时,会调用类的构造函数,注意这不是必须的,如果没有定义,那么默认是空函数

通过类构造函数实例化一个对象

  1. 在内存中创建一个新对象
  2. 在这个新对象内部的隐式原型指针被赋予为构造函数的原型(原型链关联起来了)
  3. 构造函数内部的 this 被赋予为这个新对象(即 this 指向的新对象)
  4. 执行构造函数内部的代码(给这个新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象(这个对象不会通过instanceof操作符检测出跟有关),否则返回创建的新对象

类构造函数与普通构造函数之间的主要区别:

调用类构造函数必须使用 new 操作符,否则会报错,但是普通构造函数你可以不用 new 去调用的情况下,this 指向全局(window)

类构造函数在实例化对象之后,他会变成一个普通的实例方法(但是仍然需要用new调用),所以它可以在实例上调用它

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

let user = new Person("join", 34);
let func = new user.constructor();
console.log(func); //Person { name: undefined, age: undefined }
console.log(user); //Person { name: 'join', age: 34 }

把类当作一个特殊的函数

// 类并不会作为一个特殊的数据类型,用typeof检查发现类是一个函数
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

console.log(typeof Person); //function

把类当作特殊的构造函数,类具有prototype属性,它有一个constructor属性指向类本身

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

console.log(Person.prototype.constructor); //[class Person]
console.log(Person.prototype); //Person {},这是一个对象
console.log(Person.prototype.constructor == Person); //true 函数==函数;进一步论证了类是一个函数,一个特殊的函数

站在函数的角度思考,通过new操作符调用类构造函数,用instanceof可以判断出实例的原型链上是否存在构造函数的原型

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let user = new Person();
console.log(user instanceof Person); //true
// 因为我们知道Person==Person.prototype.constructor
// 就相当于user instanceof Person.prototype.constructor
// 这么理解就好理解很多

仔细观察以下代码,会有一个与预期不同的现象

类本身具有普通函数一样的行为,类本身在被new调用时就会被当成构造函数

但是:类中定义的constructor不会被当成构造函数,然而如果我们,创建的实例是直接调用类的构造函数的话,那么instanceof操作符的返回情况会截然相反

// 调用类
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let user = new Person();
console.log(Person.constructor == Person.prototype.constructor); //false
console.log(user instanceof Person.constructor); //false
console.log(user instanceof Person.prototype.constructor); //true
console.log(user instanceof Person); //true
// 因为我们知道Person==Person.prototype.constructor
// 就相当于user instanceof Person.prototype.constructor
// 这么理解就好理解很多

截然相反的情况:

// 调用类的构造函数
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let p = new Person.constructor();

console.log(p instanceof Person.constructor); //true
console.log(p instanceof Person.prototype.constructor); //false
console.log(p instanceof Person); //false

与立即执行函数表达式相似,类也可以立即实例化

// 因为是一个类表达式,所以类名也是可以取消的,也就是去掉类名Person,输出结果也只是把Person去掉,其他的不影响
let p = new (class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
})("john", 18);
console.log(p); //Person { name: 'john', age: 18 }
// 这个p就是调用类返回的对象

实例、原型、类成员

实例成员

实例成员是存在于类构造函数内部,每次通过new调用时,都会执行类构造函数,在这个函数内部可以为新创建的实例(this指向的新对象)添加"自有"属性,在类构造函数执行完之后,仍然可以给实例添加新成员(这在 TS 中是不被允许的,在 TS 文件中这样做会报错)

let p = new (class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
})("john", 18);
console.log(p); //Person { name: 'john', age: 18 }

p.job = "worker";
console.log(p); //Person { name: 'john', age: 18, job: 'worker' }

原型方法与访问器

原型方法就是为了方便在实例中共享方法,把在类块中定义的方法作为原型方法

let p = new (class Person {
  constructor() {
    this.sayProperty = () => console.log("no");
  }
  // 在类块中定义的所有内容都会定义在类的原型上
  sayProperty() {
    console.log("yes");
  }
})("john", 18);

p.sayProperty(); //no
p.__proto__.sayProperty(); //yes

类定义也支持获取和设置访问器,语法和行为和普通对象一样

class Person {
  set name(value) {
    this.name_ = value;
  }
  get name() {
    return this.name_;
  }
}
let p = new Person();
p.name = "newName";
console.log(p.name); //newName

静态方法

可以在类上定义静态方法这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例

class Person {
  constructor() {
    // 实例方法this指向实例对象
    this.getThisDiffer = () => {
      console.log("instance", this);
    };
  }
  //原型方法this指向原型
  getThisDiffer() {
    console.log("prototype", this);
  }
  // 静态方法this指向类本身
  static getThisDiffer() {
    console.log("class", this);
  }
}
let p = new Person();

p.getThisDiffer(); //instance Person { getThisDiffer: [Function] }
p.__proto__.getThisDiffer(); //prototype Person {}
Person.getThisDiffer(); //class [class Person]

静态方法适合作为实例工厂

class Person {
  constructor(age) {
    // 实例方法this指向实例对象
    this.age = age;
    this.getThisDiffer = () => {
      console.log("instance", this);
    };
  }

  static create() {
    // 创建一个年龄[5,25)的人
    return new Person(Math.floor(Math.random() * 20 + 5));
  }
}
console.log(Person.create()); //Person { age: 9, getThisDiffer: [Function] }

非函数原型和类成员

我们可以在类的外部对类添加属性方法,甚至是原型上添加(这在TS中也是不被允许的,甚至我也觉得很奇怪)

class Person {
  constructor(age) {
    // 实例方法this指向实例对象
    this.age = age;
    this.getInfor = function () {
      return Person.name + " : " + this.age;
    };
  }

  static create() {
    // 创建一个年龄[5,25)的人
    return new Person(Math.floor(Math.random() * 20 + 5));
  }
}

Person.name = "John";

console.log(Person.create().getInfor()); //Person : 12

迭代器与生成器方法

(这里以后补充)

继承

使用extends关键字,支持继承多个类,同时也可以继承拥有[[constructor]]和原型的对象 ,所以继承普通构造函数也是可以的。

TS 中的类

面向对象思想

在基础部分,学习类,仅讨论新增的语法部分。

属性

使用属性列表来描述类中的属性

属性的初始化检查

有时候可能会忘记对属性初始化

配置 strictPropertyInitialization: true

属性的初始化位置:

  1. 构造函数中
  2. 属性默认值

属性可以修饰为可选的(可选的属性,它的初始化不是必须的)

class User {
  name: string;
  age: number;
  gender: "man" | "woman";
  pid: string | undefined; //这个属性可以有可以没有
  // pid?:string //效果一样的

  constructor(name: string, age: number, gender: "man" | "woman" = "man") {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
}
let u = new User("john", 34);
console.log(u);

属性可以修饰为只读的

class User {
  readonly id: number; //只读
  name: string;
  age: number;
  gender: "man" | "woman";
  pid: string | undefined; //这个属性可以有可以没有
  // pid?:string //效果一样的

  constructor(name: string, age: number, gender: "man" | "woman" = "man") {
    this.id = Math.random();
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
}
let u = new User("john", 34);
console.log(u);

使用访问修饰符

访问修饰符可以控制类中的某个成员的访问权限

  • public:默认的访问修饰符,公开的,所有的代码均可访问
  • private:私有的,只有在类中可以访问
  • protected:暂时不讲

Symble(在JS中,实现私有成员)

属性简写

如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写(不需要声明,但是构造函数中必须加访问修饰符)

class User {
  // 属性声明
  readonly id: number; //只读
  gender: "man" | "woman";
  pid: string | undefined; //这个属性可以有可以没有
  // pid?:string //效果一样的

  constructor(
    public name: string,
    public age: number,
    gender: "man" | "woman" = "man"
  ) {
    this.id = Math.random();
    this.gender = gender;
  }
}
let u = new User("john", 34);
console.log(u);

访问器(set get)

作用:用于控制属性的读取和赋值

其实就是setget(一般情况下是对私有属性的控制)