类 | 类构造函数、实例、原型和类成员

回顾

定义类的方式

  • 类声明
1
class Person{}

类声明与函数声明不同之处在于:

  • 函数声明有声明提前,类声明没有

  • 函数受函数作用域限制,而类受块作用域限制

1
2
3
4
5
6
7
{
function fn() {}
class ClassFn{}
}

console.log(fn); // fn() {}
console.log(ClassFn); // Uncaught ReferenceError: ClassFn is not defined
  • 类表达式
1
const Animal = class{};

类表达式的名称是可选的

在把类表达式赋值给变量后,可以通过name 属性取得类表达式的名称字符串

但不能在类表达式作用域外部访问这个标识符

1
2
3
4
5
6
7
8
9
10
11
12
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name);
}
}

let p = new Person();

p.identify(); // PersonName PersonName

console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined

类的构成

类可以包含构造函数方法实例方法获取函数设置函数静态类方法,但都不是必须的,空的类定义照样有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 空类定义,有效
class Foo {}
// 有构造函数的类,有效
class Bar {
constructor() {}
}
// 有获取函数的类,有效
class Baz {
get myBaz() {}
}
// 有静态方法的类,有效
class Qux {
static myQux() {}
}

类构造函数

constructor关键词用于在类定义块内部创建类的构造函数(constructor会告诉解释器,在new操作符的时候应该调用这个函数创建新的实例)

构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数

实例化

使用new 操作符实例化Person 的操作等于使用new 调用其构造函数

补课:new操作符会发生什么?包含手写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal {}

class Person {
constructor() {
console.log('person ctor');
}
}

class Vegetable {
constructor() {
this.color = 'orange';
}
}

let a = new Animal();

let p = new Person(); // person ctor

let v = new Vegetable();
console.log(v.color); // orange
  • 类实例化时传入的参数会用作构造函数的参数(如果不需要参数,则类名后面的括号也是可选的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
constructor(name) {
console.log(arguments.length);
this.name = name || null;
}
}

let p1 = new Person; // 0
console.log(p1.name); // null

let p2 = new Person(); // 0
console.log(p2.name); // null

let p3 = new Person('Jake'); // 1
console.log(p3.name); // Jake
  • 默认情况下,类构造函数会在执行之后返回this 对象

    构造函数返回的对象会被用作实例化的对象,如果没有什么引用新创建的this对象,那么这个对象会被销毁

    不过,如果返回的不是this对象,而是其他对象,那么这个对象不会通过instanceof 操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
constructor(override) {
this.foo = 'foo';
if (override) {
return {
bar: 'bar'
};
}
}
}

let p1 = new Person(),
p2 = new Person(true);

console.log(p1); // Person{ foo: 'foo' }
console.log(p1 instanceof Person); // true

console.log(p2); // { bar: 'bar' }
console.log(p2 instanceof Person); // false

// 因为p2返回的不是this对象,而是 {bar: 'bar'}, 因此instance检测不出p2和Person类的关联
  • 类构造函数与构造函数的主要区别是:调用类构造函数必须使用new 操作符

    普通构造函数如果不使用new调用,那么就会以全局的this(通常是window)作为内部对象

    调用类构造函数时如果忘了使用new则会抛出错误

1
2
3
4
5
6
7
function Person() {}
class Animal {}

// 把window 作为this 来构建实例
let p = Person();
let a = Animal();
// TypeError: class constructor Animal cannot be invoked without 'new'
  • 实例化类构造函数后可以在实例上引用构造函数
1
2
3
4
5
6
7
8
9
10
class Person {}

// 使用类创建一个新实例
let p1 = new Person();

p1.constructor();
// TypeError: Class constructor Person cannot be invoked without 'new'

// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor();

类是特殊的函数

  • typeof检测类为function
1
2
3
class Person{}

typeof Person // 'function'
  • 类有prototype属性,原型有constructor属性指向类自身
1
2
3
4
class Person {}

console.log(Person.prototype); // {constructor: f()}
console.log(Person.prototype.constructor === Person) // true
  • instaceof操作符可以用于检查构造函数的原型是否存在于实例的原型链中
1
2
3
4
5
class Person{}

let p = new Person();

console.log(p instanceof Person); // true
  • 类本身在使用new操作符时会被当成构造函数,但是类的constructor方法不会被当作构造函数,因此instanceof操作符会返回false,但是创建实例时直接将类构造函数当作普通构造函数来使用,instanceof操作符返回true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{}

let p1 = new Person() // 直接将类使用构造函数调用

console.log(p1.constructor === Person) // true
console.log(p1 instanceof Person) // true
console.log(p1 instanceof Person.constructor); // false 类的constructor方法不会被当作构造函数

let p2 = new Person.constructor(); // 直接将类构造函数当作普通构造函数来使用,就满足普通构造函数的特点了

console.log(p2.constructor === Person) // false
console.log(p2 instanceof Person) // false
console.log(p2 instanceof Person.constructor); // true

/*
通俗来说,谁调用new操作符生成实例的,那么谁就是实例的构造函数
实例.constructor === 谁
实例 instanceof 谁 === true
*/
  • 类可以作为参数传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 类可以像函数一样在任何地方定义,比如在数组中
let classList = [
class {
constructor(id) {
this.id_ = id;
console.log(`instance ${this.id_}`);
}
}
];

function createInstance(classDefinition, id) {
return new classDefinition(id);
}

let foo = createInstance(classList[0], 3141); // instance 3141
  • 与立即调用函数类似,类可以立即实例化
1
2
3
4
5
6
7
let p = new class Person {
constructor(name) {
console.log(name);
}
} ('Katrina'); // Katrina

console.log(p); // Person {}

实例、原型和类成员

实例成员

  • 每个实例都对应着一个唯一的成员对象,这意味着所有成员都不会在原型上共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {
constructor() {
this.name = 'Katrina';
this.age = 18;
this.sayName = function() {
console.log(this.name);
};
this.friends = ['Kate', 'Lisa'];
}
}

let p1 = new Person();
let p2 = new Person();

p1.sayName(); // 'Katrina'
p2.sayName(); // 'Katrina'

console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.friends === p2.friends); // false

p1.name = p1.friends[0];
p2.name = p2.friends[1];

p1.sayName(); // 'Kate'
p2.sayName(); // 'Lisa'

原型方法和访问器

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
constructor() {
this.name = 'Katrina';
this.age = 18;
// 添加到this 的所有内容都会存在于不同的实例上
this.sayName = function() {
console.log(this.name);
};
this.friends = ['Kate', 'Lisa'];
}

// 在类块中定义的所有内容都会定义在类的原型上
sayHello() {
console.log('Hello');
};
}

Person.prototype.sayHello(); // Hello

let p1 = new Person();
let p2 = new Person();

console.log(p1.sayHello === p2.sayHello); // true
  • 可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据
1
2
3
4
5
class Person {
name: 'Kate';
}

// Uncaught SyntaxError: Unexpected identifier
  • 类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const symbolKey = Symbol('symbolKey');

class Person {
stringKey() {
console.log('invoked stringKey');
}
[symbolKey]() {
console.log('invoked symbolKey');
}
['computed' + 'Key']() {
console.log('invoked computedKey');
}
}

let p = new Person();

p.stringKey(); // invoked stringKey
p[symbolKey](); // invoked symbolKey
p.computedKey(); // invoked computedKey
  • 类定义也支持获取和设置访问器。语法与行为跟普通对象一样
1
2
3
4
5
6
7
8
9
10
11
12
class Person {
set name(newName) {
this.name_ = newName;
}
get name() {
return this.name_;
}
}

let p = new Person();
p.name = 'Katrina';
console.log(p.name); // 'Katrina'

静态类方法

可以在类上定义静态方法

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

与原型成员类似,静态成员每个类上只能有一个

静态类成员在类定义中使用static关键字作为前缀

在静态成员中,this引用类自身,而构造函数的this是指向实例的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
constructor() {
// 添加到this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance', this);
}

// 定义在类的原型对象上
locate() {
console.log('prototype', this);
}

// 定义在类本身上
static locate() {
// this指向Peron类本身
console.log('class', this);
}
}

let p = new Person();

p.locate(); // instance Perons{}
Person.protype.locate(); // prototype {constructor: ... }
Person.locate(); // class, class Person {}
  • 静态类方法适合作为实例工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
constructor(age) {
this.age_ = age;
}

sayAge() {
console.log(this.age_);
}

static create() {
// 使用随机年龄创建并返回一个Person 实例
return new Person(Math.floor(Math.random()*100));
}
}

console.log(Person.create()); // Person { age_: ... }

非函数原型和类成员

  • 虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加
1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}

// 在类上定义数据成员
Person.greeting = 'My name is';

// 在原型上定义数据成员
Person.prototype.name = 'Jake';
let p = new Person();
p.sayName(); // My name is Jake

迭代器与生成器方法

  • 类定义语法支持在原型类本身上定义生成器方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {
// 在原型上定义生成器方法
*createNicknameIterator() {
yield 'Jack';
yield 'Jake';
yield 'J-Dog';
}

// 在类上定义生成器方法
static *createJobIterator() {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker';
}
}

let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); // Candlestick maker

let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog
  • 因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类实例变成可迭代对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}

*[Symbol.iterator]() {
yield *this.nicknames.entries();
}
}

let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}


  • 或者可以只返回迭代器实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}

[Symbol.iterator]() {
return this.nicknames.entries();
}
}

let p = new Person();

for (let [idx, nickname] of p) {
console.log(nickname);
}
// Jack
// Jake
// J-Dog