三、ECMAScript 6 语法简介(4)

本章概要

    • 类的定义
    • 访问器属性
    • 静态方法
    • 类的继承
  • 模块

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语句只能在模块的顶层使用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小熊猫呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值