一. ES6 定义类

1.1. 认识 class 定义类

我们会发现,按照前面的构造函数形式创建 ,不仅仅和编写普通的函数过于相似,而且代码并不容易理解。

  • 在 ES6(ECMAScript2015)新的标准中使用了 class 关键字来直接定义类;

  • 但是类本质上依然是前面所讲的构造函数、原型链的语法糖而已;

  • 所以学好了前面的构造函数、原型链更有利于我们理解类的概念和继承关系;

那么,如何使用 class 来定义一个类呢?

  • 可以使用两种方式来声明类:类声明和类表达式;
1
2
3
4
5
// 类的声明
class Person {}

// 类的表达式
const Student = class {};

接着我们就可以使用 new 操作符调用类:

1
2
3
const p1 = new Person();
const p2 = new Person();
console.log(p1, p2);

我们来研究一下类的一些特性:

  • 你会发现它和我们的构造函数的特性其实是一致的;
1
2
3
4
5
6
const p = new Person();
console.log(Person); // [class Person]
console.log(Person.prototype); // {}
console.log(Person.prototype.constructor); // [class Person]
console.log(p.__proto__ === Person.prototype); // true
console.log(typeof Person); // function

1.2. 类的构造函数

如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?

  • 每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的 constructor;

  • 当我们通过 new 操作符,操作一个类的时候会调用这个类的构造函数 constructor;

  • 每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;

1
2
3
4
5
6
7
8
9
10
class Person {
constructor(name, age, height) {
this.name = name;
this.age = age;
this.height = height;
}
}

const p1 = new Person("why", 18, 1.88);
console.log(p1);

当我们通过 new 关键字操作类的时候,会调用这个 constructor 函数,并且执行如下操作:

  • 1.在内存中创建一个新的对象(空对象);

  • 2.这个对象内部的[[prototype]]属性会被赋值为该类的 prototype 属性;

  • 3.构造函数内部的 this,会指向创建出来的新对象;

  • 4.执行构造函数的内部代码(函数体代码);

  • 5.如果构造函数没有返回非空对象,则返回创建出来的新对象;

1.3. 类的方法定义

1.3.1. 实例方法

在上面我们定义的属性都是直接放到了 this 上,也就意味着它是放到了创建出来的新对象中:

  • 在前面我们说过对于实例的方法,我们是希望放到原型上的,这样可以被多个实例来共享;

  • 这个时候我们可以直接在类中定义;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
constructor(name, age, height) {
this.name = name;
this.age = age;
this.height = height;
}
running() {
console.log(this.name + " running~");
}
eating() {
console.log(this.name + " eating~");
}
}

const p1 = new Person("why", 18, 1.88);
console.log(p1);
p1.running();
p1.eating();
// [ 'constructor', 'running', 'eating' ]

console.log(Object.getOwnPropertyNames(Person.prototype));

我们也可以查看它们的属性描述符:

  • 会发现它们的 enumerable 都是为 false 的;
1
console.log(Object.getOwnPropertyDescriptors(Person.prototype));

1.3.2. 访问器方法

我们之前讲对象的属性描述符时有讲过对象可以添加 setter 和 getter 函数的,那么类也是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
constructor(name) {
this._name = name;
}
set name(newName) {
console.log("调用了name的setter方法");
this._name = newName;
}
get name() {
console.log("调用了name的getter方法");
return this._name;
}
}

const p = new Person("why");
console.log(p.name);
p.name = "kobe";
console.log(p.name);

但是和直接在对象中定义不同的是,类中的 setter 和 getter 方法是放到原型上的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const p = new Person("why");
console.log(p.name);
p.name = "kobe";
console.log(p.name);

const obj = {
_name: "",
set name(newName) {
this._name = newName;
},
get name() {
return this._name;
}
};

console.log(obj);
console.log(Object.getOwnPropertyDescriptors(p.__proto__));

1.3.3. 静态方法

静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用 static 关键字来定义:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
constructor(age) {
this.age = age;
}
static create() {
return new Person(Math.floor(Math.random() * 100));
}
}

for (let i = 0; i < 10; i++) {
console.log(Person.create());
}

二. ES6 类的继承

2.1. extends 关键字

前面我们花了很大的篇幅讨论了在 ES5 中实现继承的方案,虽然最终实现了相对满意的继承机制,但是过程却依然是非常繁琐的。

在 ES6 中新增了使用 extends 关键字,可以方便的帮助我们实现继承:

1
2
class Person {}
class Student extends 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
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {
console.log(this.name + " running~");
}
eating() {
console.log(this.name + " eating~");
}
}

class Student extends Person {
constructor(name, age, sno) {
super(name, age);
this.sno = sno;
}
studying() {
console.log(this.name + " studying~");
}
}

const stu = new Student("why", 18, 111);

2.2. super 关键字

我们会发现在上面的代码中我使用了一个 super 关键字,这个 super 关键字有不同的使用方式:

  • 注意:在子(派生)类的构造函数中使用 this 或者返回默认对象之前,必须先通过 super 调用父类的构造函数!

  • super 的使用位置有三个:子类的构造函数、实例方法、静态方法;

1
2
3
4
5
// 调用 父对象/父类 的构造函数
super([arguments]);

// 调用 父对象/父类 上的方法
super.functionOnParent([arguments]);

下面的代码会报错,因为我们没有调用 super:

1
2
3
4
5
6
7
8
class Person {}
class Student extends Person {
constructor(sno) {
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
}
}

const stu = new Student();

我们可以在子类的方法中调用父类的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
eating() {
console.log(this.name + " eating~");
}
static create() {
console.log("Person create");
}
}

class Student extends Person {
// 方法的重写
eating() {
console.log("做完作业~");
super.eating();
}
static create() {
super.create();
console.log("Student create");
}
}

const stu = new Student();
stu.eating();
Student.create();

2.3. 继承内置类

我们也可以让我们的类继承自内置类,比如 Array:

1
2
3
4
5
6
7
8
9
10
11
class HYArray extends Array {
lastItem() {
return this[this.length - 1];
}
}

const array = new HYArray(10, 20, 30);
console.log(array.lastItem());
array.filter(item => {
console.log(item);
});

调用 Array 的方法返回 Array 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class HYArray extends Array {
lastItem() {
return this[this.length - 1];
}
static get [Symbol.species]() {
return Array;
}
}

const array = new HYArray(10, 20, 30);
console.log(array.lastItem());

const newArr = array.filter(item => {
console.log(item);
});
console.log(newArr instanceof HYArray); // false
console.log(newArr instanceof Array); // true

三. ES 对象的增强

ES6 中对 对象字面量 进行了增强,称之为 Enhanced object literals(增强对象字面量)。

3.1. 属性的简写

在开发中,对象中的属性可能会经常来自变量,并且变量的名称和属性的名称是相同的,这个时候我们可以使用简写:

  • 英文称之为 Property Shorthand
1
2
3
4
5
6
7
8
const name = "why";
const age = 18;

// ES5的写法
const obj1 = { name: name, age: age };

// ES6的增强写法
const obj2 = { name, age };

3.2. 方法的简写

另外一个对象增强的写法是针对方法的:

  • 英文称之为 Method Shorthand
1
2
3
4
5
6
7
8
9
10
11
// ES5的方法写法
const info1 = {
foo: function () {},
bar: function () {}
};

// ES6的增强写法
const info2 = {
foo() {},
bar() {}
};

3.3. 计算属性名

在 ES5 中,如果一个对象中属性的名称来自一个变量,或者需要其他的方法计算得到,那么我们需要这样来做:

1
2
3
const name = "three";
const obj = { one: 1, two: 2 };
obj[name] = 3;

在 ES6 中,我们可以直接在字面量中编写计算属性名:

英文称之为 Computed Property Names

1
2
3
const name = "three";
const obj = { one: 1, two: 2, [name]: 3 };
console.log(obj);

四. 解构 Destructuring

ES6 中新增了一个从数组或对象中方便获取数据的方法,称之为解构 Destructuring。

4.1. 数组的解构

数组的解构就是从数组中获取我们需要的数组:

1
2
3
const names = ["abc", "cba", "nba"];
const [name1, name2, name3] = names;
console.log(name1, name2, name3);

数组的解构必须按照数据的顺序依次获取,如果我们只想获取第二个和第三个,那么可以有如下语法:

1
2
const [, nameb, namec] = names;
console.log(nameb, namec);

如果我们希望解构出来一个元素,其他元素继续放到另外一个数组中:

1
2
const [namea, ...newNames] = names;
console.log(namea, newNames);

如果我们解构的数据数量大于数组中原本的数据数量,那么会返回 undefined:

1
2
const [namex, namey, namez, namem] = names;
console.log(namem); // undefined

我们可以在解构出来的数据为 undefined 的时候,给它一个默认值:

1
2
const [namex, namey, namez, namem = "aaa"] = names;
console.log(namem); // aaa

4.2. 对象的解构

对象的解构和数组的解构是相似的,不同之处在于:

  • 数组中的元素是按照顺序排列的,并且我们只能根据顺序来确定需要获取的数据;

  • 对象中的数据由 key 和 value 组成,我们可以通过 key 来获取想要的 value;

1
2
3
const obj = { name: "why", age: 18, height: 1.88 };
const { name, age, height } = obj;
console.log(name, age, height);

因为对象是可以通过 key 来解构的,所以它对顺序、个数都没有要求:

1
2
3
4
5
const { height, name, age } = obj;
console.log(name, age, height);

const { age, height } = obj;
console.log(age, height);

如果我们对变量的名称不是很满意,那么我们可以重新命名:

1
2
const { name: whyName, age: whyAge } = obj;
console.log(whyName, whyAge);

我们也可以给变量一个默认值:

1
2
const { name, address = "广州市" } = obj;
console.log(name, address);

4.3. 解构的使用

解构目前在开发中使用是非常多的:

  • 比如在开发中拿到一个变量时,自动对其进行解构使用;

  • 比如对函数的参数进行解构;

解构的使用1

解构的使用2


文章转载于coderwhy | JavaScript 高级系列(十二) - ES6~ES13-类和对象