JS笔记《class》

68 阅读6分钟

概述

  • ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。可以将class看作是一个语法糖,是构造函数的另一种写法。只是让对象原型的写法更加清晰、更像面向对象编程
class Person{
  constructor(name, age){
    this.name = name;
    this.age = age;
  }

  toString(){
    return `name:${this.name}, age: ${this.age}`;
  }
}

console.log(typeof Person)  // function
console.log(Person.prototype.constructor === Person)  // true

let p = new Person('张三', 20);
console.log(p)  // Person {name: '张三', age: 20}
  • class中定义的所有方法,其实都定义在类(函数)的prototype上,且是不可枚举的。
// ES6 class
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayName() {
    console.log(this.name);
  }
}
console.log(Person.prototype) // {sayName: f}
// 遍历对象自身可枚举的属性
Object.keys(Person.prototype) // []  
// 遍历对象自身不可枚举的属性
Object.getOwnPropertyNames(Person.prototype)  //  ['constructor', 'sayName']  

// ES5写法
function Person2() {}
Person2.prototype.sayName = function () {}
// 遍历对象自身可枚举的属性
Object.keys(Person2.prototype) // ['sayName']
// 遍历对象自身不可枚举的属性
Object.getOwnPropertyNames(Person2.prototype)  //  ['constructor', 'sayName']  
  • 类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
class Foo {
  constructor() {
    return Object.create(null);
  }
}

Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'

constructor()

  • 此方法是类的默认方法,通过new生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显示定义,会默认添加一个空的。
  • 默认返回实例对象(即this),但也可以指定返回另一个对象。
class Foo {
  constructor() {
    return Object.create(null);
  }
}

new Foo() instanceof Foo
// false

类的实例

  • 类的属性和方法,除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

let point = new Point(2, 3);

point.hasOwnProperty('x') // true  实例属性
point.hasOwnProperty('y') // true  实例属性
point.hasOwnProperty('toString') // false  实例没有toString方法
point.__proto__.hasOwnProperty('toString') // true 原型对象上有
  • 与ES5一样,所有实例对象共享一个原型对象。也就意味着通过实例改写原型,会影响到所有实例对象。
let p1 = new Point(2,3);
let p2 = new Point(3,2);

Object.getPrototypeOf(p1) === Object.getPrototypeOf(p2)  //true

实例属性新写法

  • ES2022 为类的实例属性又规定了一种新写法。实例属性现在除了可以定义在constructor()方法里面的this上面,也可以定义在类内部的最顶层。
class Foo {
  bar = 'hello';  // 实例属性
  baz = 'world';  // 实例属性

  constructor(name) {
    this.name = name;  // 实例属性
  }
}

let f = new Foo('张三');
console.log(f)  // Foo {bar: 'hello', baz: 'world', name: '张三'}

setter和getter

  • 在类的内部可以使用getset关键字,对某个属性设置存值和取值函数,拦截该属性的存取行为。存取函数是设置在属性的属性描述对象上的。
class Foo {
  bar = 'hello'; // 实例属性
  baz = 'world'; // 实例属性

  constructor(name) {
    this.name = name; // 实例属性
  }

  get age() {
    return 100;
  }

  set age(value) {
    console.log(value)
  }
}

let f = new Foo()
f.age = 20;   // 20
f.age         // 100

class表达式

  • 与函数一样,类也可以使用表达式的形式定义。
// 命名类表达式
// 类的名字是Me,只能在类内部使用,指代当前类
// 在Class外部,只能使用MyClass引用
const myClass = class Me{}

// 匿名类表达式
const MyClass = class { /* ... */ };

// 立即执行类
let person = new class {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}('张三');

person.sayName(); // "张三"

静态方法

  • 类相当于实例的原型,所有在类中定义的方法都会被实例继承。如果在一个方法前加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用。
class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello' 静态方法

var foo = new Foo();
foo.classMethod()  // TypeError: foo.classMethod is not a function,实例调用报错
  • 如果静态方法包含this,则this指向的是类,而不是实例。
  • 静态方法还可以与非静态方法重名。
class Foo {
  static bar() {
    this.baz();  // 指向Foo,所以调用的是Foo.baz,也就是静态方法baz
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world');
  }
}

Foo.bar() // hello

静态属性

  • 静态属性指的是类本身的属性,而不是定义在实例对象(this)上的属性。
// 老写法
class Foo {
  // ...
}
Foo.prop = 1;


// 新写法
class Foo {
  static prop = 1;
}

Foo.prop  // 1

静态块

  • 在类的内部设置一个代码块,在类生成时运行且只运行一次,主要作用是对静态属性进行初始化。以后,新建类的实例时,这个块就不运行了。
  • 每个类允许有多个静态块,每个静态块中只能访问之前声明的静态属性。静态块的内部不能有return语句。静态块内部的this指向当前类。
class CountFac{
  static x = 0;

  static{  // 静态块,只运行一次
    this.x += 100;  // 初始化x从100开始
  }

  inc(){
    CountFac.x++;
  }

  get value(){
    return CountFac.x;
  }
}

let counter = new CountFac();
counter.inc();
counter.value // 101

let counter2 = new CountFac();
counter2.inc();
counter2.value  // 102  x是静态属性,所有实例共用

私有属性、方法

  • 写法是属性、方法名之前使用#表示。只能在类的内部使用,在外部使用会报错。
  • 如果读取一个不存在的私有属性、私有方法,在类的内部也会报错。
  • 私有属性、方法的前面也可以加上static,表示这是一个静态的私有属性或方法。
class CountFac{
  #count = 0;  // 私有属性

  #countInc(){   // 私有方法
    this.#count++;
  }
  get value(){
    return this.#count;
  }

  inc(){
    this.#countInc()  // 只能在类内部调用私有方法
  }
}

let counter = new CountFac();
counter.inc();
counter.#count   // 报错,编译都无法通过
counter.value    // 1  只能使用实例属性来获取

in 运算符

  • 判断某个对象是否有某个私有属性。它不会报错,而是返回一个布尔值。
class C {
  #brand;

  static isC(obj) {
    return #brand in obj;
  }
}
C.isC(new C()) // true
  • 判断私有属性时,in只能用在类的内部。要判断的私有属性一定要先声明,否则报错。
class A {
  m() {
    console.log(#foo in this); // 报错
  }
}

注意事项

  • 类的内部默认就是严格模式
  • 类不存在变量提升,必须先声明,后使用。
  • 本质上类只是函数的一层包装,所以也具有name属性。
class Point {}
Point.name // "Point"
  • 如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。
class Foo {
  constructor(...args) {
    this.args = args;
  }
  // 添加了Symbol.iterator实例属性,就会返回一个Foo类的默认遍历器
  * [Symbol.iterator]() {  // Generator 函数
    for (let arg of this.args) {
      yield arg;
    }
  }
}

// for of遍历相当于执行遍历器,依次返回的就是遍历器yield后面的值
for (let x of new Foo('hello', 'world')) {
  console.log(x);
}
// hello
// world
  • 类的方法内部如果含有this,它默认指向类的实例。一旦单独使用该方法,很可能报错。
class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;  
// 解构出来,全局环境执行的话会报错,类是严格模式,所以类中的this会指向undefined

printName(); // TypeError: Cannot read property 'print' of undefined