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值。bar 和snaf都是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类不能被实例化,只能用于继承