学习阮一峰老师ES6系列(Class的基本语法)

40 阅读10分钟

1.类的由来

Js中,生成实例对象的传统方法是通过构造函数,如下面的代码所示:

function Point(x, y) {
	this.x = x;
	this.y = y;
}

Point.prototype.toString = function() {
	return '(' + this.x + ', ' + this.y + ')';
}

const p = new Point(1, 2);
console.log(p).  // {x: 1, y: 2};
p.toString(); // '(1, 2)'

ES6提供了class关键字,可以定义类,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程,如下所示:

class Point {
	constructor(x, y) {
		this.x = x;
		this.y = y;
	}

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

上面的代码定义了一个类,constructor()方法是构造方法,this关键字代表实例对象。在定义方法的时候不需要在前面加上function这个关键字,并且方法和方法之间不需要逗号分隔。

ES6的类,其实是构造函数的另外一种写法,使用的时候也是直接对类使用new命令

class Point {
	// ...
}

typeof Point // function
Point === Point.prototype.constructor //true

const b = new Ponint();

构造函数的prototype属性,在类上面继续存在,事实上,类上面的方法都定义在类的prototype属性上,在类的实例上面调用方法,其实就是调用原型上的方法

class Point {
	constructor() {
	}

	toString() {
	}
	
	toValue() {
	}

}

//等同于
Point.prototype = {
	constructor() {},
	toString() {},
	toValue() {},
};

用于类的方法都定义在prototype对象上,所以类的新方法可以添加在prototype对象上,Object.assign()方法可以一次性添加多个方法

class Point {
	constructor() {
	}
	
}

Object.assign(Point.prototype, {
	toString() {},
	toValue() {}
})

在类内部定义的方法,都是不可枚举的

class Point {
	constructor(x, y) {
		//
	}

	toString() {
	 //...
	}
}

Object.keys(Point.prototype); // []

Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

toString方法是Point类内部定义的方法,它是不可枚举的,这一点与ES5不一样,在ES5中tostring方法就是可枚举的

let Point = function(x, y) {
		//,,,
};

Point.prototype.toString = fucntion () {

};

Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

2.constructor()方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显性定义,一个空的constructor方法会被自动添加。

class Ponit {

}

//等同于
class Point {
	constructor() {

	}
}

constructor方法默认返回实例对象(即this),也可以指定返回另外一个对象

class Foo {
	constructor() {
		return Object.create(null);
	}
}

new Foo() instanceof Foo; //false

上面代码中,constructor()函数返回一个全新的对象,结果导致实例对象不是Foo类的实例

3.类的实例

生成类的实例的方法,也是使用new命令,但如果用class声明一个类,在实例化这个类的时候忘记加上new,像函数那样直接调用,将会报错

class Point {
	constructor(x, y) {
		this.x = x;
		this.y = y;
	}

	
}

const point = Point(2, 3); //error
const point = new Point(2, 3);

类的属性和方法,除非是显式地定义在其本身,否则都是定义在原型上

class Point {
	constructor(x, y) {
		this.x = x;
		this.y = y;
	}

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

const point = new Point(2, 3);
point.toString();

point.hasOwnProperty('x');  //true
point.hasOwnProperty('y'); //true
point.hasOwnProperty('toString'); // false
point.__proto__.hasOwnProperty('toString'); //true

上面代码中,x和y都是实例对象point自身的属性,因为定义在this对象上,而toString方法是定义在原型对象上的。

4.实例属性的新写法

ES2022为类的实例属性,又规定了一种新写法,实例属性除了可以定义在constructor方法里,还可以定义在类内部的最顶层,这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。如下所示:

class Point {
	constructor(x, y) {
		this.x = x;
		this.y = y;
	}

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

const p = new Point(2, 3);
p.value // 2

class Ponit {
	x = 1;

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

5.取值函数和存值函数

与ES5一样,在类的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为

class Myclass {
	constructor(x) {
		this.x = x;
	}

	get prop() {
		return this.x;
	}

	set prop(value) {
		this.x = value;
		console.log('设置为' + value);
	}

}

let inset = new Myclass(2);
inset.prop // 2
inset.prop = 3; // 设置为3 
inset.prop // 3

上面代码中,prop属性具有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了

存值函数和取值函数是设置在属性的Descript对象上的

class CustomHTMLElement {
	constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value;
  }

}

const descripter = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, 'html');
console.log(descripter); // configurable: true, enumerable: false, get: f html(), set: f html(value)

6.属性表达式

类的属性名,可以采用表达式,下面的代码中,Square类的方法名是getArea

let methodName = 'getArea';

class Square {
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}

7.class表达式

与函数一样,类也可以使用表达式的形式定义

const MyClass = class Me {
	getClassName() {
		return Me.name;
	}
}

let inst = new MyClass();
inst.getClassName() //'me'
Me.name // error

上面的代码,用表达式定义了一个类,这个类的名字是Me,但是Me只能在class内部使用,在Clss外部,这个类只能用MyClass引用,如果类的内部没有用到Me,也可以省略

const MyClass = class {
	constructor(value) {
		this.value = value;
	}

	getValue() {
		return this.value;
	}

}

const me = new MyClass(2);
me.getValue() // 2

采用Class表达式,可以写出一个立即执行的Class

let person = new class {
  constructor(name) {
    this.name = name;
  }

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

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

9.静态属性

静态属性是指Class本身的属性,即Class.propName,而不是定义在实例对象上的属性

//定义一个静态属性。
class Foo {

}
Foo.prop = 1;
Foo.prop // 1

class MyClass {
	static myStaticProp = 42;
	constructor() {
		console.log(Myclass.myStaticProp); //42
	}
}

10、私有方法和私有属性

早期解决方法

在命名上加以区别,_bar()方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。

class Widget {
	//公有方法
	foo(baz) {
		this._bar(baz);
	}

	_bar(baz) {
		return this.snaf = baz;
	}
	
}

另外一种方法是将私有方法移出类,然后再内部调用call

class Widget {
	foo(baz) {
		bar.call(this., baz);
	}

}

function bar(baz) {
	return this.snaf = baz;
}

还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。barsnaf都是Symbol值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。

const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{

  // 公有方法
  foo(baz) {
    this[bar](baz);
  }

  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }

  // ...
};

私有属性的正式写法

ES2022为class添加了私有属性的写法,方法是在属性名之前添加#

class IncreasingCounter {
  #count = 0;
  get value() {
    console.log('Getting the current value!');
    return this.#count;
  }
  increment() {
    this.#count++;
  }
}

const counter = new IncreasingCounter();
counter.value;   //Getting the current value! 0
counter.#count //error

不管在类的内部或外部,读取一个不存在的私有属性,会报错。这与公开属性的行为完全不同,如果读取一个不存在的公开属性,不会报错,只会返回undefined。

这种写法不仅可以写私有属性,也可用来写私有方法.。下面代码中,#sum就是一个私有方法,另外私有属性也可以设置getter和setter方法

class Foo {
	#a;
  #b;
  constructor(a, b) {
    this.#a = a;
    this.#b = b;
  }
  #sum() {
    return this.#a + this.#b;
  }
  printSum() {
    console.log(this.#sum());
  }

	get #x() {
		return this.#a;
	}

	set #x(value) {
		this.#a = value
	}

	printX() {
		return this.#x
	}

	setX(value) {
		return this.#x = value;
	}

}

const foo = new Foo(1, 22);
foo.#sum() //erro
foo.printSum(); //23
foo.#x //error
foo.printX() // 1

foo.setX(4);
foo.printX(); // 4

私有属性不限于this引用,只要是在类内部,实例也可以引用私有属性,下面代码表示允许从实例foo上面引用私有属性

class Foo {
  #privateValue = 42;
  static getPrivateValue(foo) {
    return foo.#privateValue;
  }
}

Foo.getPrivateValue(new Foo()); // 42

私有属性或私有方法前,也可以加上static关键字,表示这是静态的私有属性或方法,下面代码#totallyRandomNumber是私有属性,#computeRandomNumber()是私有方法,只能FakeMath这个类的内部调用,外部调用就会报错。

class FakeMath {
	static PI = 22 /7;
	static #totallyRandomNumber = 4;

	static #computeRandomNumber() {
		return FakeMath.#totallyRandomNumber;
	}

	static random() {
    console.log('I heard you like random numbers…')
    return FakeMath.#computeRandomNumber();
  }

}

FakeMath.PI // 3.142857142857143
FakeMath.random()  // 4

FakeMath.#totallyRandomNumber // 报错
FakeMath.#computeRandomNumber() // 报错

in运算符

in运算符可以用来判断某个对象是否是类的实例

//ES5
claa C {
	#brand;
	satatic isC(obj) {
		try {
			obj.#brand;
			return true;
		} catch {
			return false;
		}
	}
}

//ES6
class C {
	#brand;

	static isC(obj) {
		if (#brand in obj) {
			return true;
		} else {
			return false;
		}
	}

}

上面代码中,静态方法isC()就可以用来判断对象是否是C的实例,in运算符判断某个对象是否有私有属性#foo,它不会报错,而是返回一个布尔值。

in也可以搭配this一起使用

class A {
	#foo = 0;
	m() {
		console.log(#foo in this);
	}
}

const a = new A();
a.m() //true

子类从父类继承的私有属性,也可以使用in运算符来判断

class A {
	#foo = 0;
	static test(obj) {
		console.log(#foo in obj);
	}
}

class SubA extends A {};
A.test(new SubA); //true

in运算符对于Object.create()、Object.setPrototypeOf形成的继承,是无效的,因为这种继承不会传递私有属性

class A {
	#foo = 0;
	static test(obj) {
		console.log(#foo in obj);
	}
}

const a = new A();
const o1 = Object.create(a);
A.test(o1); //false
A.test(o1.__proto__); //true

const o2 = {};
Object.setPrototypeOf(o2, a);
A.test(o2) // false
A.test(o2.__proto__) // true

11、静态块

静态属性有一个问题就是如果它有初始化逻辑,这个逻辑要么写在类的外部,要么写在constructor()方法里面,前者将类的内部逻辑写到外部,后者则是每次新建实例都会运行一次。所以ES2022引入了静态块,允许在类的内部设置一个代码块,在类生成时运行且只运行一次,主要的作用是为了对静态属性进行初始化。

class C {
	static x = 234;
	static y;
	static z;

	static {
		try {
			const obj = doSomethingWith(this.x);
      this.y = obj.y;
      this.z = obj.z;
		}
		catch {
			this.y = ...;
			this.x = ...;
		}
	}

}

上面代码中,类的内部有一个static代码块,这就是静态块,它的好处是将y和z的初始化逻辑写入类的内部,且只运行一次。

每个类允许有多个静态块,每个静态块只能访问之前声明的静态属性,另外静态块的内部不能有return语句,静态块的内部可以使用类名或this,指代当前类

class C {
	static x = 1;
  static {
    this.x; // 1
    // 或者
    C.x; // 1
  }
}

除了静态属性初始化,静态块还有一个作用,就是将私有属性与类的外部代码分析

let getX;

class C {
	#x = 1;
	static {
		getX = obj => obj.#x;
	}

}

console.log(getX(new C())); // 1

上面的代码中,#x是私有属性,如果类外部的get X()方法想要获取这个属性,可以写在静态块里,这样只在类生成的时候定义一次。

12、类的注意点

严格模式

ES6实际把整个语言都升级到了严格模式,因为未来所有的代码都是运行在模块之中的。

不存在提升

类不存在变量提升,即类的定义在前,使用在后。因为类的继承需要保证子类必须在父类之后

new Foo(); //error
class Foo {};
{
	let Foo = class {};
  class Bar extends Foo {
  }
}

name属性

本质上ES6的类只是ES5的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性

class Point {};
Point.name // Point

Generator方法

如果某个方法之前加上星号(*),就表示该方法是一个Generator函数

class Foo {
	constructor(...args) {
		this.args = args;
	}
	*[Symbol.iterator]() {
		for (let arg of this.args) {
      yield arg;
    }
	}
}

for (let x of new Foo('hello', 'world')) {
  console.log(x);
}

this的指向

类的内部方法如果含有this,它默认指向类的实例。

class Logger {
	printName(name = 'there') {
		this.print(`Hello ${name}`);
	}

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

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read properties of undefined (reading 'print')

上面代码中,printName方法中的this,默认指向logger,但是如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于class内部是严格模式,所以this的指向是undefined),从而找不到print方法报错。

一个比较简单的解决方法是,在构造方法中绑定this

class Logger {
	constructor() {
    this.printName = this.printName.bind(this);
  }

	printName(name = 'there') {
		this.print(`Hello ${name}`);
	}

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

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read properties of undefined (reading 'print')

另外一种是使用箭头函数

13.new.target属性

new是从构造函数生成实例对象的命令,ES6为new命令引入了一个new.target属性,返回new命令作用于的那个构造函数,如果构造函数不是通过new命令或Reflect.constructor()调用的,则会返回undefined,因此这个属性可以用来确定构造函数怎么调用

function Person(name) {
	if (new.target !== undefined) {
		this.name = name;
	} else {
		throw new Error('必须使用new生成实例')
	}
}

//另外一种写法
function Person1(name) {
	if (new.target === Person) {
		this.name = name;
	} else {
		throw new Error('必须使用new生成实例')
	}
}

const person = new Person('张三'); 
const person2 = Person('zhangsan'); Uncaught Error: 必须使用 new 命令生成实例

Class内部调用new.target,返回当前Class,子类继承父类,new.target会返回子类

class Rectangle {
  constructor(length, width) {
    console.log(new.target);
    this.length = length;
    this.width = width;
  }
}

const obj = new Rectangle(3, 4);

/* 
output:
class Rectangle {
  constructor(length, width) {
    console.log(new.target);
    this.length = length;
    this.width = width;
  }
}
*/

class Rectangle {
  constructor(length, width) {
    console.log(new.target);
    this.length = length;
    this.width = width;
  }
}

class Square extends Rectangle {
  constructor(length, width) {
    super(length, width);
  }
}

const obj = new Square(3); 
/*
output:
class Square extends Rectangle {
  constructor(length, width) {
    super(length, width);
  }
}
*/

利用这个特点,可以写出不能独立使用,必须继承后才能使用的类

class Shape {
	constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

const x = new Shape(); //本类不能实例化
const y = new Rectangle()

上面的代码中,Shape类不能被实例化,只能用于继承