ES6中提供了class这个 构造函数的语法糖 来模拟类,但它并不是真正的类,实际上走的还是ES5中的构造函数模拟类的流程,只是看起来更像类了
类与对象
- 类:具有相同特征的事物可以看做是一类的。
- 比如:男生女生都是“人类”,自行车、卡车都是“车类”。
- 对象:由 类 生产的 具体的东西 就是对象。
例子
- “车类” 拥有共同特性 ,对每一辆车来说他们都有这些特性(属性/方法),只是值不同。
- 比如:颜色、材质、轮子(属性),会加速、能删车(方法)。
- 类 就像是工厂,工程生产出的就是对象。
- 比如“车类”就像是“造车工厂”,“造车工厂”造出来的每一辆车就是一个“对象”。
- 所以我们写好一个类就相当于有了一个工厂,只要我们给工厂一些数据就能得到我们想要的对象。
ES6中的类
- class 的本质是 function
- ES6中提供了class这个 构造函数的语法糖 来模拟类,但它并不是真正的类,实际上走的还是ES5中的构造函数模拟类的流程,只是看起来更像类了。它让对象原型的写法更加清晰、更像面向对象编程的语法
- 使用**
class
创建类** (类相当于工厂) - 类中有用于创建对象的构造函数
constructer()
(构造函数相当于工厂中的接头人) - 实例化:类创建对象的过程(相当于造车的过程)
例子:
1 | // 车类 |
注意:“剩余参数...
是做聚合的,而扩展运算符...
是做展开的,符号都是...
,但含义不同。”(复习一下扩展运算符用于剩余参数的用法。)
1 | // 车类 |
面向过程开发(差)
把不同的操作都封装在函数中是“面向过程开发”,复用率极低。
例子:贪吃蛇小游戏
1 | // 第一条蛇 |
一条蛇看起来没什么问题,但如果我们希望多一条蛇则需要整个复制修改,以此让函数们作用于不同的蛇对象,复用率极低:
1 | // 第二条蛇 |
面向对象开发
- OOP(面向对象开发)的核心是封装
同一个例子,我们使用类的写法就方便很多。
创建Snake类,将函数们放入类中,通过类创建的每个对象之间都是独立的(即属性/方法都只作用于该对象):
1 | class Snake { |
使用上面 ES6的类中的例子 来演示一下同一个类创建两个不同的对象:
1 | const car1 = new Car(3, '#f00', 20, 40); |
类的三大基本特性
- 多态:同一个接口,不同的表现。即同一个操作用在不同的对象上会有不同的结果。(目前ES基本不支持多态)
- 继承(笔记在后面另一篇)
- 封装(笔记在后面另一篇)
实例:音乐播放器类
在这个例子中我们并没有真正使用到Audio构造函数,只是演示一下。
index.html:
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 | class Car { |
静态属性的例子:
1 | // Car.属性名 = 属性值; |
静态属性的例子
使用 静态属性totalCar
来记录通过Car类生成了多少个对象:
1 | class Car { |
由于每生成一个对象就会调用一次构造函数,而构造函数中 静态属性totalCar
是累加的,所以生成多少个对象totalCar
就累加多少次。
静态方法的例子
1 | class Person { |
- 通过 Programmer类 创建了一个 程序猿对象programmer ,此时programmer的 属性haveGilrFriend和hair都是false。
- 我们让 程序猿对象programmer作为传参 调用 Person类的静态方法format,在该方法中将 程序猿对象programmer 的两个属性都改为true。
- 此时 程序猿对象programmer 的两个属性发生了变化。
类的表达式
- 之前我们使用的都是类的声明,就像函数声明与表达式一样,类也有表达式的定义方法
- 语法
- **
const 常量名 = class {...}
**,使用new 常量名()
创建对象 - 或者
const 常量名 = class 类名 {...}
,一样使用new 常量名()
创建对象。其中,类名===常量名
,但 类名 只能在类中使用,类外用会报错!
- **
1 | // 函数表达式 |
- 使用类表达式时想要在类内部定义 静态属性/方法 建议使用 类名,防止函数名变化导致静态属性/方法失效:
1 | const Person = class P { |
- 有趣但少用的特点:使用类表达式定义的类可以自执行:
1 | // 借由P可生成自执行的类,但实际开发中很少这么用 |
getter和setter
- 类似于给属性提供钩子
- 在 获取属性值 和 设置属性值 的时候做一些额外的事情
回顾ES5的getter/setter
在对象字面量中书写get/set方法
- get()将在对象的属性被获取时被触发。
- set()将在对象的属性被设置时被触发。
- 定义:在ES5中我们在对象字面量中定义get()/set()
- 使用:
对象名.get方法名;
或对象名.set方法名=属性值;
(注意不是去调用属性,是方法名!另,调用方法和普通函数的调用方法是不同的) - 注意:get()/set()与属性不要同名,避免造成死循环。
调用get()的错误示范:
1 | const obj = { |
注意:
get()/set()与属性不要同名,避免造成死循环
20触发的是obj的get()
调用set()的正确示范:
1 | // 在对象字面量中书写get/set方法的例子 |
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。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。
- 属性描述符:分为 数据描述符 和 存取描述符(详细的 可选描述符 可参考MDN文档,这里只列举了例子中用到的)
- 注意:默认情况下,使用 Object.defineProperty() 添加的属性值是不可遍历/修改的。
- 而通过 赋值操作 添加的普通属性是可遍历的(
for...in
或Object.keys
方法),这些属性的值可以被改变,也可以被删除的。
- 而通过 赋值操作 添加的普通属性是可遍历的(
数据描述符的例子:
通过Object.defineProperty()给obj对象添加属性age,属性值由value
设置:
1 | var obj = { |
可以看到_name
和age
属性的颜色是不一样的,使用for...in
遍历obj属性时也没有age
,这是因为通过Object.defineProperty()
生成的属性不可遍历。
在属性描述符中添加enumerable为true,则添加的属性可出现在对象遍历的属性中:
1 | var obj = { |
存取描述符的例子:
通过 存取描述符 get 和 set 给调用的对象obj添加了 get() 和 set(),这里 this 指 obj,参数2 name 是 get() / set() 的名字:
1 | var obj = { |
ES6中的getter/setter
- ES6中在类中定义get与set,而ES5是在对象字面量中定义的。
- 使用:
对象名.get方法名;
或对象名.set方法名=属性值;
,调用方法和ES5中是相同的。(与 静态属性/方法 的调用方式区分开)
例子:
1 | class Person { |
用在播放器中的例子:
也可以在构造函数中添加init(),使用Audio构造函数:
name属性获取类名
- name属性:用于获取类的名字。
- 采用“类的声明”的类名很好理解,如果采用的是“类的表达式”,则类有名字就返回类名,没有名字时返回常量的名字。
- 开发中极少使用,了解即可。
1 | class Person{}; |
new.target属性
- new.target属性不能直接访问,会报错。只能在 类的构造函数/new调用的普通函数 中使用。
- new.target属性值 实际上是 new 后面的 函数体/构造函数体,如果没有new,则是undefined。
- 使用new普通函数也可使用new.target属性是因为:在ES5中普通函数前加new就相当于构造函数。
用在类的构造函数中的例子:
1 | class Car { |
用在普通函数中,没有new的例子:
1 | function Car() { |
用在普通函数中,有new的例子:
1 | function Car() { |
实际运用
普通函数容错判断
容错判断(规定Car只能通过new调用,否则报错):
1 | function Car() { |
在以前没有new.target属性时,我们使用instanceof判断Car是否是被new关键字调用的:
1 | // 使用instanceof判断 |
因为this指向使用new调用Car()生成的对象,所以this的显示原型和Car是有关系的。this instanceof Car
如果是true则说明this是使用new Car()
创建的对象。
类中容错判断
在类中不需要手动设置报错:
1 | class Car { |