本章概要
- 类
- 类的定义
- 访问器属性
- 静态方法
- 类的继承
- 模块
3.10 类
大多数面向对象的编程语言支持类和类继承的特性,而 JavaScript 不支持这些特性,只能通过其他方式模拟类的定义和类的继承。
ES6 引入 class(类)的概念,新的 class 写法让对象原型的写法更加清晰,也更像传统的面向对象编程语言的写法。
3.10.1 类的定义
在早期版本,没有类的概念,可以通过构造函数和原型混合使用的方式模拟定义一个类。如下:
function Car(sColor,iDoors)
{
this.color= sColor;
this.doors= iDoors;
}
Car.prototype.showColor=function(){
console.log(this.color);
};
let oCar=new Car("red",4);
oCar.showColor();
ES6 y引入了 class 关键字,使类的定义更接近 Java、C++ 等面向对象语言中的写法。如下:
class Car{
// 等价于Car构造函数
constructor(sColor,iDoors){
this.color= sColor;
this.doors= iDoors;
}
// 等价于Car.prototype.showColor
showColor(){
console.log(this.color);
}
}
let oCar = new Car("red",4);
oCar.showColor();
在类声明语法中,使用特殊的 constructor 方法名定义构造函数,且由于这种类使用简写语法定义方法,因此不需要添加 function 关键字。
自有属性是对象实例中的属性,并不会出现在原型上,如本例中的 color 和 doors。自有属性只能在类的构造函数(即 constructor 方法)或方法中创建,一般建议在构造函数中创建所有的自有属性,从而只通过一处就可以控制类中所有的自有属性。此处的 showColor() 方法实际上是 Car.prototype的一个方法。
与函数一样,类也可以使用表达式的形式定义。如下:
let Car = class {
// 等价于Car构造函数
constructor(sColor,iDoors){
this.color= sColor;
this.doors= iDoors;
}
// 等价于Car.prototype.showColor
showColor(){
console.log(this.color);
}
}
let oCar = new Car("red",4);
oCar.showColor();
使用类表达式,可以实现立即调用类构造函数从而创建一个类的单例对象。使用 new 调用类表达式,紧接着通过一对圆括号调用这个表达式。如下:
let car = new class {
// 等价于Car构造函数
constructor(sColor,iDoors){
this.color= sColor;
this.doors= iDoors;
}
// 等价于Car.prototype.showColor
showColor(){
console.log(this.color);
}
}("red", 4);
car.showColor();
先创建了一个匿名类表达式,然后立即执行。按照这种模式可以使用类语法创建单例,并且不会在作用域中暴露类的引用。
3.10.2 访问器属性
访问器属性是通过关键字 get 和 set 创建的,语法为关键字 get 或 set 后跟一个空格和相应的标识符,实际上是为某个属性定义取值和设置函数,在使用时以属性访问的方式使用。与自由属性不同的是,访问器属性是在原型上创建的。如下:
class Car{
constructor(sName, iDoors){
this._name= sName;
this.doors= iDoors;
}
//只读属性
get desc(){
return `${this.name} is worth having.`;
}
get name(){
return this._name;
}
set name(value){
this._name = value;
}
}
let car = new Car("Benz", 4);
console.log(car.name); // Benz
console.log(car.desc); // Benz is worth having.
car.name = "Ferrari";
console.log(car.name); // Ferrari
car.prototype.desc = "very good"; //TypeError: Cannot set property 'desc' of undefined
在构造函数中定义了一个“_name”属性,“_name”属性前面的下划线是一种常用的约定记号,用于表示只能通过对象方法访问的属性。
当访问属性name时,实际上是调用它的取值方法;当给属性name赋值时,实际上是调用它的设置方法。因为是方法实现,所以定义访问器属性时,可以添加一些访问控制或额外的代码逻辑。
如果需要只读属性,那么只提供 get() 方法即可,如本例中的 desc 属性;同理,如果需要只写属性,那么只提供 set() 方法 即可。
3.10.3 静态方法
ES6 引入了关键字 static ,用于定义静态方法。除构造函数外,类中所有的方法和访问器属性都可以用 static 关键字定义。如下:
class Car{
constructor(sName, iDoors){
this.name= sName;
this.doors= iDoors;
}
showName(){
console.log(this.name);
}
static createDefault(){
return new Car("Audi", 4);
}
}
let car = Car.createDefault();
car.showName(); // Audi
car.createDefault(); // TypeError: car.createDefault is not a function
使用 static 关键字定义的静态方法,只能通过类名访问,不能通过实例访问。此外,要注意的是,ES6 并没有提供静态属性,即不能再实例属性前添加 static 关键字。
3.10.4 类的继承
ES6 提供了 extends 关键字,这样可以很轻松地实现类的继承。如下:
class Person{
constructor(name){
this.name = name;
}
work(){
console.log("working...");
}
}
class Student extends Person{
constructor(name, no){
super(name); //调用父类的constructor(name)
this.no = no;
}
}
let stu = new Student("zhangsan", 1);
stu.work(); // woking...
Student 类通过使用关键字 extends 继承自 Person 类,Student 类称为派生类。
在 Student 的构造函数中,通过 super() 函数调用 Person 的构造函数并传入相应的参数。需要注意的是,如果在派生类中定义了构造函数,则必须调用 super() 函数,而且一定要在访问 this 之前调用。
如果在派生类中没有定义构造函数,那么当创建派生类的实例时会自动调用 super() 函数并传入所有参数。如下,定义了 Teacher 类,从 Person 类继承,在类声明中,没有定义构造函数。
class Person{
constructor(name){
this.name = name;
}
work(){
console.log("working...");
}
}
class Teacher extends Person{
//没有构造函数
}
//等价于
/*class Teacher extends Person{
constructor(...args){
super(...args);
}
}*/
let teacher = new Teacher("lisi");
teacher.work(); //working...
在派生类中,可以重写基类中的方法,这将覆盖基类中的同名方法。下面在 Student 类中重新定义 work() 方法。
class Person{
constructor(name){
this.name = name;
}
work(){
console.log("working...");
}
}
class Student extends Person{
constructor(name, no){
super(name); //调用父类的constructor(name)
this.no = no;
}
// 覆盖Person.prototype.work()方法
work(){
console.log("studying...");
}
}
let stu = new Student("zhangsan", 1);
stu.work(); // studying...
如果在 Student 的 work() 方法中需要调用基类的 work() 方法,可以使用 super 关键字调用。如下:
class Person{
constructor(name){
this.name = name;
}
work(){
console.log("working...");
}
}
class Student extends Person{
constructor(name, no){
super(name); //调用父类的constructor(name)
this.no = no;
}
// 覆盖Person.prototype.work()方法
work(){
super.work();
console.log("studying...");
}
}
let stu = new Student("zhangsan", 1);
stu.work(); // working...
// studying...
3.11 模块
在 ES5 及早期版本中,一直没有模块(module)体系,导致无法将一个复杂的应用拆分成不同的功能模块,再组合起来使用。为此,JavaScript 社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。
ES6 在语言标准的层面上,实现了模块功能,而且实现的相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
一个模块通常是一个独立的 JS 文件,该文件内部定义的变量和函数除非被导出,否则不能被外部访问。
使用 export 关键字放置在需要暴露给其它模块使用的变量、函数或类声明前面,以将它们从模块中导出。如下:
// 导出数据
export var color = "red";
export let name = "module";
export const sizeOfPage = 10;
// 导出函数
export function sum(a, b){
return a + b;
}
// 将在模块末尾进行导出
function subtract(a, b){
return a - b;
}
// 将在模块末尾进行导出
function multiply(a, b){
return a * b;
}
// 将在模块末尾进行导出
function divide(a ,b){
if(b !== 0)
return a / b;
}
// 导出类
export class Car {
constructor(sColor, iDoors){
this.color = sColor;
this.doors = iDoors;
}
showColor(){
console.log(this.color);
}
}
// 模块的私有变量
var count = 0;
// 模块私有的函数
function changeCount(){
count++;
}
// 导出multiply函数
export {multiply};
// subtract是本地名称,sub是导出时使用的名称
export {subtract as sub}
// 导出模块默认值
export default divide;
代码解释:
- 导出时可以分别对变量、函数和类进行导出,也可以将导出语句集中写在模块的尾部,当导出内容较多时,采用后者会更加清晰
- 没有添加exprot关键字而定义的变量、函数和类在模块外部是不允许被访问的
- 导出的函数和类声明都需要一个名称。如果要用到一个不同的名称导出变量、函数或类,可以使用 as 关键字指定变量、函数或类在模块外应该按照什么样子的名字来使用
- 一个模块可以导出且只能导出一个默认值,默认值是通过使用 default 关键字指定的单个变量、函数或类。非默认值的导出,需要使用一对花括号包裹名称,而默认值的导出则不需要
- 默认值的导出还可以采用下面两种语法形式:
// 第二种语法形式
// 使用default关键字导出一个函数作为模块的默认值
// 因为导出的函数被模块所代表,所以它不需要一个名称
export default function(a ,b){
if(b !== 0)
return a / b;
}
// ----------------------- //
function divide(a ,b){
if(b !== 0)
return a / b;
}
// 第三种语法形式
export {divide as default}
如果想在一条导出语句中指定多个导出(包括默认导出),那么就需要用到第三种语法形式。如下:
export {multiply, subtract as sub, divide as default};
接着看一下导入。导入是使用 import 关键字引入其他模块导出的功能。import 语句由两部分组成:要导入的标识符和标识符应当从哪个模块导入。如下:
// 导入模块默认值
import divide from "./Modules.js";
// 导入多个绑定
import {color, name, sizeOfPage} from "./Modules.js";
// 导入单个绑定
import {multiply} from "./Modules.js";
// 因Modules模块中导出subtract()函数时使用了名称sub,这里导入也要用该名称
import {sub} from "./Modules.js";
// 导入时重命名导入的函数
import {sum as add} from "./Modules.js";
// 导入类
import {Car} from "./Modules.js";
// 导入整个模块
import * as example from "./Modules.js";
console.log(color); //red
console.log(name); //module
console.log(sizeOfPage); //10
//只能用add而不能用sum
console.log(add(6, 2)); //8
console.log(sub(6, 2)); //4
console.log(multiply(6, 2)); //12
console.log(divide(6, 2)); //3
let car = new Car("black", 4);
car.showColor(); //black
console.log(example.name); //module
// 注意这里是sum而不是add
console.log(example.sum(6, 2)); // 8
// count是Modules模块的私有变量,在外部不能访问
console.log(example.count); //undefined
// changeCount()函数是Modules模块私有的函数,在外部不能访问
console.log(example.changeCount()); //TypeError: example.changeCount is not a function
代码解释:
- 导入模块时,模块文件的位置可以使用相对路径,也可以使用绝对路径。使用相对路径时,对于同一目录下的文件,要在文件前加“./”
- 导入时,可以导入单个绑定,也可以同时导入多个绑定,也可以使用 as 关键字对导入的绑定重新命名
- 对于模块非默认值的导入,需要使用一对花括号包裹名称,而默认值的导入则不需要
- 可以导入整个模块作为一个单一对象,然后所有的导出都将作为该对象的属性使用
- 多个import语句引用同一个模块,该模块也只执行一次。被导入的模块代码执行后,实例化后的模块被保存在内存中,只要另一个 import 语句引用它就可以重复使用它
提示:
export 和 import 语句必须在其它语句或函数之外使用,换句话说,import和export语句只能在模块的顶层使用。