前言:当受尽了原型链和构造函数的折磨,并且模拟的类似类仍然不尽人意且存在漏洞之时,你是否想过放弃。诚然,在ES6之前,一切的类似类都得这么一步一步去构造,但是当ES6出现,引入class关键字之后,开发者就再也不必在原型链上左右横跳,直接使用这一语法糖即可。本文将深入类,走进js全新的类的世界。
一、类定义
类似于定义函数,定义类的方式也主要是两种:类声明和类表达。
1 //类声明
2 const Person{};
3 //类表达
4 const Person = class{};
类与函数不同一个的地方在于函数声明可以提前,而类声明不会提前。
1 console.log(FunctionDeclaration); //ƒ FunctionDeclaration(){}
2 function FunctionDeclaration(){};
3 console.log (ClassDeclaration); //Uncaught ReferenceError
4 class ClassDeclaration{}
类的构成可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但是这些方法都不是必需的,构造一个空类照样有效。
1 class Foo{
2 constructor(){
3
4 }
5 get exampleSelf(){
6 return this
7 }
8 static classSelf(){
9 console.log (this);
10 }
11 }
12 const myFoo = new Foo;
13 console.log (myFoo.exampleSelf); //Foo {}
14 Foo.classSelf(); //class Foo{...}
值得注意的是,多数编程风格的类名的首字母都需要大写,js也同样建议这样命名。
二、类构造函数
constructor关键字用于在类的内部创建类的构造函数。constructor会在new操作符创建类的新实例时被调用执行内部的代码。
既然提到了new在实例化对象时会调用constructor,那么就必然回到那个经典的问题:new在进行实例化时究竟发生了什么?
- 在内存中创建一个新对象
- 将新对象内部的[[prototype]]指针赋值为构造函数的prototype
- 构造函数内部的this指向这个新对象(指向实例)
- 执行构造函数内部的代码
- 如果构造函数返回非空对象,则返回该对象,否则返回刚创建的对象。
了解了new的实例化机制,那么对于后面的很多例子打印出来的结果都会十分清晰其原理。
tips:类在实例化时传入的参数会传进构造器函数(constructor)作为参数,如果不需要参数,则类名后的小括号都可以省略。
三、类是特殊的函数
在ECMAScript中并没有类的这个类型,那么类究竟是怎么来的呢?其实类也是通过构造函数和原型链实现的,并非脱离了之前一直困扰我们的那俩,只是有大佬将他们进行了整合封装,才让我们开发者脱离苦海。
由此可知,类其实也是一种特殊的函数。
类也有显式的prototype属性,其上面也有constructor属性指向类自身,这就像极了构造函数。
1 class Foo2{};
2 console.log (Foo2.prototype); //{constructor: ƒ}
3 console.log (Foo2.prototype.constructor); //class Foo2{}
当然也可以像构造函数一样,使用instanceOf操作符检索构造函数原型是是否存在于实例的原型链中。
1 class Foo2{};
2 const foo2 = new Foo2;
3 console.log (foo2 instanceof Foo2); //true
在类的问题中,有一个比较容易把人绕晕的一个点就是用类创建实例和用类的构造器函数创建实例。
因为类本身在new的时候就会被当作构造函数,虽然类中的构造函数(constructor)同名,也称为构造函数,但是却不会被当成构造函数,如果要使用constructor进行实例化也是可行的,但是使用instanceOf进行实例检查的结果会发生反转。这也正是容易弄混的一个点。
1 class Person{}
2
3 let p1 = new Person;
4 console.log (p1.constructor===Person); //true
5 console.log (p1 instanceof Person); //true
6 console.log (p1 instanceof Person.constructor); //false
7
8 let p2 = new Person.constructor;
9 console.log (p2.constructor===Person); //false
10 console.log (p2 instanceof Person); //false
11 console.log (p2 instanceof Person.constructor); //true
四、实例、原型与静态类
1、实例
每次new之后都会执行构造函数(constructor),在构造函数内,可以为新创建的实例this添加自由属性(由new实例化的过程可知 constructor中的this指向实例),这也是构造函数的一个非常重要的功能。
每个实例都对应一个唯一的成员对象,即所有成员都不会再原型上共享。
1 class Person {
2 constructor() {
3 this.name = new String('Jack');
4 this.sayName = () => console.log(this.name);
5 this.nicknames = ['Jake', 'J-Dog']
6 }
7 }
8 let p1 = new Person(),
9 p2 = new Person();
10 p1.sayName(); // Jack
11 p2.sayName(); // Jack
12 console.log(p1.name === p2.name);// false
13 console.log(p1.sayName === p2.sayName);// false
14 console.log(p1.nicknames === p2.nicknames); // false
15 p1.name = p1.nicknames[0];
16 p2.name = p2.nicknames[1];
17 p1.sayName(); // Jake
18 p2.sayName(); // J-Dog
2、原型
要实现实例间能像原型一样共享方法,类也有相应的方法。
在类中直接定义的方法会作为原型方法。
1 class Person {
2 showName(){
3 console.log ('prototype');
4 }
5 }
6 const p1 = new Person();
7 Person.prototype.showName() //prototype
tips:虽然可以在类中直接定义方法,但是不能在类中直接添加属性加到原型上,会报错。
3、静态类方法
静态类成员在类定义中使用static关键字作为前缀,静态成员中,this指向类自身。
1 class Person {
2 constructor(){
3 this.showThis = ()=>{
4 console.log ('instance',this);
5 }
6 }
7 showThis(){
8 console.log ('prototype',this);
9 }
10 static showThis(){
11 console.log ('class',this);
12 }
13 }
14 const p = new Person;
15 p.showThis(); //instance Person {showThis: ƒ}
16 Person.prototype.showThis(); //prototype {constructor: ƒ, showThis: ƒ}
17 Person.showThis(); //class Person {...}
五、类中的迭代器
类支持在原型和类本身上定义生成器方法。
1 class Person {
2 *createNicknameIterator() {
3 yield 'Jack';
4 yield 'Jake';
5 yield 'J-Dog';
6 }
7 static *createJobIterator() {
8 yield 'Butcher';
9 yield 'Baker';
10 yield 'Candlestick maker';
11 }
12 }
13 let jobIter = Person.createJobIterator();
14 console.log(jobIter.next().value); // Butcher
15 console.log(jobIter.next().value); // Baker
16 console.log(jobIter.next().value); // Candlestick maker
17 let p = new Person();
18 let nicknameIter = p.createNicknameIterator();
19 console.log(nicknameIter.next().value); // Jack
20 console.log(nicknameIter.next().value); // Jake
21 console.log(nicknameIter.next().value); // J-Dog