ES6 class(1)

ES6中提供了class这个 构造函数的语法糖 来模拟类,但它并不是真正的类,实际上走的还是ES5中的构造函数模拟类的流程,只是看起来更像类了

类与对象

  • :具有相同特征的事物可以看做是一类的。
    • 比如:男生女生都是“人类”,自行车、卡车都是“车类”。
  • 对象:由 类 生产的 具体的东西 就是对象。

例子

  • “车类” 拥有共同特性 ,对每一辆车来说他们都有这些特性(属性/方法),只是值不同。
    • 比如:颜色、材质、轮子(属性),会加速、能删车(方法)。
  • 类 就像是工厂,工程生产出的就是对象
    • 比如“车类”就像是“造车工厂”,“造车工厂”造出来的每一辆车就是一个“对象”。
  • 所以我们写好一个类就相当于有了一个工厂,只要我们给工厂一些数据就能得到我们想要的对象

ES6中的类

  • class 的本质是 function
  • ES6中提供了class这个 构造函数的语法糖 来模拟类,但它并不是真正的类,实际上走的还是ES5中的构造函数模拟类的流程,只是看起来更像类了。它让对象原型的写法更加清晰、更像面向对象编程的语法
  • 使用**class创建类** (类相当于工厂)
  • 类中有用于创建对象的构造函数constructer()(构造函数相当于工厂中的接头人)
  • 实例化:类创建对象的过程(相当于造车的过程)

例子

1
2
3
4
5
6
7
8
9
10
// 车类
class Car {
// 构造函数 - (工厂中接头人)
// 实例化 - (造车的过程) => 类创建对象的过程
constructor(...args) {
console.log(args);
}
}
new Car('蓝色', 3);
// ['蓝色', 3]

注意:“剩余参数...是做聚合的,而扩展运算符...是做展开的,符号都是...,但含义不同。”(复习一下扩展运算符用于剩余参数的用法。)

例子2

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 Car {
// 构造函数 - (工厂中接头人)
// 实例化 - (造车的过程) => 类创建对象的过程
constructor(whell, color, length, width) {
// 在构造函数中通过this将属性绑定在对象身上
this.whell = whell;
this.color = color;
this.length = length;
this.width = width;
this.speed = 0;
}

// 对象的方法在类中另外声明
// 加速
speedUp() {
this.speed += 1;
}
}

const car = new Car(3,'#f00', 20, 40 )
// car通过Car类生成对象,所以Car中的this指向对象car
console.log(car.color); // #f00
console.log(car.speed); // 0
car.speedUp(); // 加速
console.log(car.speed) // 1

面向过程开发(差)

把不同的操作都封装在函数中是“面向过程开发”,复用率极低

例子:贪吃蛇小游戏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 第一条蛇
let snake = []; //存放蛇身的
let food = { x: 0y: 0 }; // 食物的坐标
function move() {
// 处理蛇的移动
}
function getFood() {
// 是否吃到食物
}
function putFood() {
// 放置食物
}
function gameover() {
// 判断游戏结束
}
function start() {
//入口函数
//初始化
}

一条蛇看起来没什么问题,但如果我们希望多一条蛇则需要整个复制修改,以此让函数们作用于不同的蛇对象,复用率极低

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 第二条蛇
let snake2 = []; //存放蛇身的
let food2 = { x: 0y: 0 }; // 食物的坐标
function move() {
// 处理蛇的移动
}
function getFood() {
// 是否吃到食物
}
function putFood() {
// 放置食物
}
function gameover() {
// 判断游戏结束
}
function start() {
//入口函数
//初始化
}

面向对象开发

  • OOP(面向对象开发)核心是封装

同一个例子,我们使用类的写法就方便很多。
创建Snake类,将函数们放入类中,通过类创建的每个对象之间都是独立的(即属性/方法都只作用于该对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Snake {
snake = [];
food = {x: 0,y: 0};
constructor() {
// ...初始化的操作
}
move() {
}
getFood() {
}
putFood() {
}
gameover() {
}
}
new Snake();
new Snake();

使用上面 ES6的类中的例子 来演示一下同一个类创建两个不同的对象

1
2
3
4
5
const car1 = new Car(3, '#f00', 20, 40);
const car2 = new Car(33, '#f00',88, 99);
console.log(car1, car2);
// Car {whell: 3, color: '#f00', length: 20, width: 40, speed: 0}
// Car {whell: 33, color: '#f00', length: 88, width: 99, speed: 0}

类的三大基本特性

  • 多态:同一个接口,不同的表现。即同一个操作用在不同的对象上会有不同的结果。(目前ES基本不支持多态)
  • 继承(笔记在后面另一篇)
  • 封装(笔记在后面另一篇)

实例:音乐播放器类

在这个例子中我们并没有真正使用到Audio构造函数,只是演示一下。

index.html:
index.html
index.js:
index.js
使用方法:调用AudioPlayer类,将 DOM元素的id 传入 AudioPlayer类 即可创建一个音乐播放器对象

代码思路

  • 创建AudioPlayer类,构造函数 接受 containner参数(即我们调用时传入的DOM元素id)
  • 在类中创建函数getSongs()获取歌曲资源地址及相关信息。
  • 在类中创建函数createElement()生成一个DOM元素用于放置“播放按钮、进度条”(假装),将DOM元素绑定在调用类创建的对象的dom属性上。
  • 在类中创建函数bindElement()给刚刚创建的元素绑定点击事件,点击则打印“开始播放”。
  • 在类中**创建函数render()**来将 创建好的DOM元素 添加到 传入的DOM元素 的后面,以显示在页面上。

注意:
类中创建的方法都要在构造函数中调用才能在创建对象时自动执行
**在构造函数中调用这些函数时使用的是this 函数名()**,因为类中的函数们是挂在通过类创建出的不同对象身上的,不是构造函数内部的,所以在构造函数中我们要调用的是对象.函数名(),而对象名是多变的,但构造函数中this指向对象,所以可以使用this 函数名()


静态方法与静态属性

  • 静态方法与静态属性 都是类自身的,通过类创建的对象不会拥有
  • 静态方法与静态属性 都只能通过类名进行调用。(不是对象名!)
  • 静态方法
    • 声明:在类中使用static关键字进行声明static 函数名 (){...} (注意函数与构造函数并排,不包含)
    • 使用:在类外通过**类名.函数名(对象名);**的方式让对象调用该静态方法。
  • 静态属性
    • 声明:在类中/外使用** 类名.属性名=属性值; **的方式进行定义。
    • 使用:在类外通过** 类名.属性名; **的方式让对象调用该静态方法。

静态方法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Car {
constructor() {
this.speed = 0;
}
speedUp() {
this.speed += 1;
}
static repair(car) {
console.log("我是修车的");
console.log("我现在要修的车是:", car);
}
}
Car.repair( '1号车');
// 我是修车的
// 我现在要修的车是: 1号车

静态属性的例子:

1
2
3
// Car.属性名 = 属性值;
Car.totalCar = 0;
console.log(Car.totalCar); // 0

静态属性的例子

使用 静态属性totalCar记录通过Car类生成了多少个对象

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
27
28
29
class Car {
static totalCar = 0; // 初始化静态属性(方式1)
constructor() {
// 构造函数中累加静态属性totalCar
Car.totalCar += 1;
}
}

// Car.totalCar = 0; // 初始化静态属性(方式2)
// 如未正确初始化则会出现 undefined + 1 导致结果未 NaN

new Car();
new Car();
new Car();
new Car();
new Car();
new Car();
new Car();
new Car();
new Car();
new Car();
new Car();
new Car();
new Car();
new Car();
new Car();
new Car();

console.log(Car.totalCar); // 16

由于每生成一个对象就会调用一次构造函数,而构造函数中 静态属性totalCar 是累加的,所以生成多少个对象totalCar 就累加多少次。

静态方法的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
static format(programmer) {
programmer.haveGirlFriend = true
programmer.hair = true;
}
}
class Programmer {
constructor() {
this.haveGirlFriend = false;
this.hair = false;
}
}
const programmer = new Programmer();
console.log(programmer); // Programmer {haveGirlFriend: false, hair: false}
Person.format(programmer);
console.log(programmer) // Programmer {haveGirlFriend: true, hair: true}
  • 通过 Programmer类 创建了一个 程序猿对象programmer ,此时programmer的 属性haveGilrFriend和hair都是false。
  • 我们让 程序猿对象programmer作为传参 调用 Person类的静态方法format,在该方法中将 程序猿对象programmer 的两个属性都改为true。
  • 此时 程序猿对象programmer 的两个属性发生了变化。

类的表达式

  • 之前我们使用的都是类的声明,就像函数声明与表达式一样,类也有表达式的定义方法
  • 语法
    • **const 常量名 = class {...}**,使用new 常量名()创建对象
    • 或者const 常量名 = class 类名 {...},一样使用new 常量名()创建对象。其中,类名===常量名,但 类名 只能在类中使用,类外用会报错!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 函数表达式
// const a = function() {
// }

// 函数声明
// function a() {
// }

// 类表达式
const Person = class P {
constructor() {
console.log(P === Person); // true
console.log("我是鸽手!!咕咕咕!!");
}
}
new Person();

// 类名 只能在类中使用,否则报错
// console.log(P); // 报错 P is not defined ,你可以打印Person
  • 使用类表达式时想要在类内部定义 静态属性/方法 建议使用 类名,防止函数名变化导致静态属性/方法失效:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Person = class P {
static staticProperty = "I am a static property"; // 静态属性定义
static staticMethod() { // 静态方法定义
console.log("I am a static method");
}

constructor() {
console.log(P.staticProperty); // 访问静态属性
P.staticMethod(); // 调用静态方法
console.log("我是鸽手!!咕咕咕!!");
}
};

new Person();
  • 有趣但少用的特点:使用类表达式定义的类可以自执行
1
2
3
4
5
6
7
// 借由P可生成自执行的类,但实际开发中很少这么用
const Person1 = new class P {
constructor() {
P.a = 1;
console.log("我是鸽手!!咕咕咕!!");
}
}();

getter和setter

  • 类似于给属性提供钩子
  • 获取属性值 和 设置属性值 的时候做一些额外的事情

回顾ES5的getter/setter

在对象字面量中书写get/set方法

  • get()将在对象的属性被获取时被触发。
  • set()将在对象的属性被设置时被触发。
  • 定义:在ES5中我们在对象字面量中定义get()/set()
  • 使用: 对象名.get方法名;对象名.set方法名=属性值; (注意不是去调用属性,是方法名!另,调用方法和普通函数的调用方法是不同的)
  • 注意:get()/set()与属性不要同名,避免造成死循环。

调用get()的错误示范

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
name: '',
get name() {
return this.name;
},
set name(val) {
this.name = val;
}
}
// 报错 get()与属性同名造成死循环
obj.name; // Uncaught RangeError: Maximum call stack size exceeded at get name [as name] 超过最大调用堆栈大小
// 栈溢出了,最常见的原因就是递归,无限循环
// 问题就出在get()的名字和属性名相同,导致调用name属性时触发的get返回的name又会触发get(),无限死循环

注意:
get()/set()与属性不要同名,避免造成死循环
20触发的是obj的get()

调用set()的正确示范

1
2
3
4
5
6
7
8
9
10
11
12
// 在对象字面量中书写get/set方法的例子
const obj = {
_name: '',
get name() {
return this._name;
},
set name(val) {
this._name = val;
}
}
obj.name = 222;
console.log(obj); // {_name: 222}

Object.defineProperty()添加属性

  • Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象
  • 语法:Object.defineProperty(对象名,"属性名",属性描述符 )
    • 属性描述符:分为 数据描述符 和 存取描述符(详细的 可选描述符 可参考MDN文档,这里只列举了例子中用到的)
      • 数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。
      • 存取描述符是由getter-setter函数对描述的属性。
      • 属性描述符 必须是这两种形式之一,不能同时是 数据描述符 和 存取描述符
        • enumerable:当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举(遍历)属性中。默认为 false。(数据描述符和存取描述符均具有该键值)
        • value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。(数据描述符具有该键值)
        • get:给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
        • set:给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值
  • 注意:默认情况下,使用 Object.defineProperty() 添加的属性值是不可遍历/修改的。
    • 而通过 赋值操作 添加的普通属性是可遍历的for...inObject.keys 方法),这些属性的值可以被改变,也可以被删除的。

数据描述符的例子:
通过Object.defineProperty()给obj对象添加属性age,属性值由value设置:

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
_name: ''
};
Object.defineProperty(obj, 'age', {
value: 19
});
var i;
// Object.defineProperty()生成的属性不可遍历
for (i in obj) {
console.log(i); // 所以只会打印_name
}
console.log(obj); // {_name: '', age: 19}

可以看到_nameage属性的颜色是不一样的,使用for...in遍历obj属性时也没有age,这是因为通过Object.defineProperty()生成的属性不可遍历。

在属性描述符中添加enumerable为true,则添加的属性可出现在对象遍历的属性中

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
_name: ''
};
Object.defineProperty(obj, 'age', {
value: 19,
enumerable: true, // 属性描述符中添加enumerable为true则新增属性可被遍历到
});
var i;
// Object.defineProperty()生成的属性不可遍历
for (i in obj) {
console.log(i); // 打印_name age
}
console.log(obj); // {_name: '', age: 19}

存取描述符的例子
通过 存取描述符 get 和 set 给调用的对象obj添加了 get() 和 set(),这里 this 指 obj,参数2 name 是 get() / set() 的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = {
_name: ''
};
Object.defineProperty(obj, 'name', {
get: function() {
console.log('正在访问name');
return this._name;
},
set: function(val) {
console.log('正在修改name');
this._name = val;
},
});
obj.name = 10;
console.log(obj.name);
// 正在修改name
// 正在访问name
// 10

ES6中的getter/setter

  • ES6中在类中定义get与set,而ES5是在对象字面量中定义的。
  • 使用:对象名.get方法名;对象名.set方法名=属性值;,调用方法和ES5中是相同的。(与 静态属性/方法 的调用方式区分开)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
constructor() {
this._name = ''
}
get name() {
console.log('正在访问name');
return `我的名字是${ this._name }`;
}
set name(val) {
console.log('正在修改name');
this._name = val;
}
}
const person = new Person();
person.name = '鸽王' ;
console.log(person.name);
// 正在修改name
// 正在访问name
// 我的名字是鸽王

用在播放器中的例子
用在播放器中的例子
效果展示
也可以在构造函数中添加init(),使用Audio构造函数
使用Audio对象


name属性获取类名

  • name属性:用于获取类的名字
  • 采用“类的声明”的类名很好理解,如果采用的是“类的表达式”,则类有名字就返回类名,没有名字时返回常量的名字
  • 开发中极少使用,了解即可。
1
2
3
4
5
6
7
8
class Person{};
console.log(Person.name);//Person

const Humen = class {};
console.log(Humen.name);//Humen

const Humen = class P{};
console.log(Humen.name);//P

new.target属性

  • new.target属性不能直接访问,会报错。只能在 类的构造函数/new调用的普通函数 中使用
  • new.target属性值 实际上是 new 后面的 函数体/构造函数体,如果没有new,则是undefined。
  • 使用new普通函数也可使用new.target属性是因为:在ES5中普通函数前加new就相当于构造函数。

用在类的构造函数中的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Car {
constructor() {
console.log(new.target);
}
}
new Car();
/*
打印出
class Car {
constructor() {
console.log(new.target);
}
}
*/

用在普通函数中,没有new的例子:

1
2
3
4
function Car() {
console.log(new.target);
}
Car(); // undefined

用在普通函数中,有new的例子:

1
2
3
4
5
6
7
8
9
function Car() {
console.log(new.target);
}
Car();
/*
ƒ Car2() {
console.log(new.target);
}
*/

实际运用

普通函数容错判断

容错判断(规定Car只能通过new调用,否则报错):

1
2
3
4
5
6
7
function Car() {
if (new.target !== Car) {
throw Error('必须使用new关键字调用Car');
}
}
Car();
// Uncaught Error: 必须使用new关键字调用Car

在以前没有new.target属性时,我们使用instanceof判断Car是否是被new关键字调用的:

1
2
3
4
5
6
7
8
// 使用instanceof判断
function Car() {
if (!(this instanceof Car)) {
throw Error('必须使用new关键字调用Car');
}
}
Car();
// Uncaught Error: 必须使用new关键字调用Car

因为this指向使用new调用Car()生成的对象,所以this的显示原型和Car是有关系的。
this instanceof Car如果是true则说明this是使用new Car()创建的对象。


类中容错判断

在类中不需要手动设置报错

1
2
3
4
5
6
7
class Car {
constructor() {
console.log(new.target);
}
}
Car()
// Uncaught TypeError: Class constructor Car cannot be invoked without 'new'
,