HTML5 基础知识,第 5 部分 ECMAScript 6 面向对象设计_介绍ECMAScript 6(简称 ES6)面向对象设计的知识与实践

本文介绍了ECMAScript 6(ES6)的面向对象设计,包括let和const命令、箭头函数、类和模块等核心概念。详细阐述了类的构造方法、原型方法、静态方法、继承、混入模式以及模块的使用,帮助开发者理解ES6在JavaScript中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、ECMAScript 6的来龙去脉

ECMAScript 6(简称 ES6)是 JavaScript 语言的新标准版本,它的第一版本6.0,也称ECMAScript 2015(简称 ES2015)已经在 2015 年 6 月正式发布了。其目标为使得 JavaScript 语言可以用来编写复杂的大型应用程序,既可以作为前端开发语言,运行于桌面系统的浏览器环境,及移动终端App的WebView控件环境,或第三方平台的微信小程序环境等;又可以作为后端服务器开发语言,运行于Node.js服务器环境。使其成为跨平台与跨端(前后端,终端)的 企业级开发语言。

1.1 ECMAScript 和 JavaScript 的关系 、及其发展

JavaScript起源于互联网领域初始阶段的领导者Netscape 公司,并运行于该公司的Netscape浏览器中。1996 年 11 月,该公司决定将 JavaScript 提交给标准化组织 ECMA(欧洲计算机制造商协会,European Computer Manufacturers Association),希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。

ECMAScript 1.0 是 1997 年发布的,接下来的两年,连续发布了 ECMAScript 2.0(1998 年 6 月)和 ECMAScript 3.0(1999 年 12 月)。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。很多初学者一开始学习 JavaScript,其实就是在学 3.0 版的语法。

2007 年 10 月,ECMAScript 4.0 版草案发布,各方对于是否通过这个标准,发生了严重分歧,2008 年 7 月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现有功能改善的一小部分,发布为ECMAScript 3.1。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。2009 年 12 月,ECMAScript 5.0 版正式发布。

2011 年 6 月,ECMAScript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。ECMAScript 5.1为ES5的最终版。

2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。

1.2 ES6 与 ECMAScript 2015 的关系

在 2015 年 6 月,ES6 的第一个版本发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015),标准委员会决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,只要用年份标记就可以了。

2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,对应年份的ES2017、ES2018、ES2019也特指该年发布的正式版本的语言标准。

ES6是一个泛指,含义是 ECMAScript 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 、ES2018、ES2019等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。

二、let 和 const 命令

2.1 let 命令

ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

{
    let a = 10;
    var b = 1;
}

a   // ReferenceError: 在代码块外访问a,表示a没有定义;
b   // 1,在代码块外访问b,表示b为1;

上面代码在代码块之中,分别用let和var声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。在这里表示,let声明的变量只在它所在的代码块有效。而var命令声明的变量,在全局范围内都有效。

for循环的循环变量,就很合适使用let命令:

for (let i = 0; i < 10; i++) {
     // 循环体内代码;
 }

console.log(i);   // ReferenceError: i 没有定义

上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。

let声明的变量是块作用域的, 它们仅存在于当前块中。使用var声明的私有变量是函数作用域的,如先前介绍函数表达式时所见。

左括号“ {”和右括号“}”之间的范围是一个块。 如果您来自Java或C / C ++的背景,那么块作用域的概念将非常熟悉。 在这些语言中,程序员引入了块只是为了定义范围。 但是,在JavaScript中,由于没有与之关联的范围,因此有必要习惯性地引入块。 但是,ES6允许您使用let关键字创建块作用域变量。 如您在前面的示例中所看到的,在块内创建的变量a在该块内可用。 在声明块作用域变量时,通常建议在块的顶部添加let声明。

来看另一个示例,以清楚地区分函数和块作用域:

function swap(a,b){       // <--函数范围从这里开始
    if(a>0 && b>0){       // <--块范围从这里开始
        let tmp=a;
        a=b;
        b=tmp;
    }                     // <--块作用域到此结束
    console.log(a,b);
    console.log(tmp);     //未定义tmp,因为它仅在块作用域中可用
    return [a,b];
}
swap(1,2);

如上所见,tmp是用let声明的,并且仅在定义它的块中可用。 出于所有实践目的,应该最大限度地使用块作用域变量。 除非尝试执行某些非常具体的操作,否则必须使用var声明,请确保使用块范围的变量。 但是,错误地使用let关键字可能会导致很多问题。 首先,不能使用let关键字在同一函数或块范围内重新声明同一变量:

function blocker(x){
    if(x){
        let zcj;
        let zcj;     // 重复声明变量“zcj”
    }
}

在ES6中,由let关键字声明的变量被提升到块作用域。 不过,在声明变量之前引用该变量是错误的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

2.2 const 命令

ES6中引入的另一个关键字是const。 用const关键字声明的变量创建对值的只读引用。 这并不意味着引用所保存的值是不可变的。 但是,变量标识符不能重新分配。 常量具有块范围作用域,就像使用let关键字创建的变量一样。 同样,必须在声明变量时为其分配一个值。

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

const car = {}
car.tyres = 4

在这里,将{}分配给一个常量car。 分配后,该引用将不再变化。 在ES6中,应该执行以下操作:

  • 尽可能使用const,用于所有其值不变的变量。
  • 避免使用var

三、箭头函数

函数是一种特殊的数据类型。 但是,事实证明,这还不止于此:函数实际上是对象。 有一个称为Function()的内置引用类型的构造函数,它允许使用另一种(但不一定推荐)创建函数的方式。

以下示例显示了定义函数的三种方法:

function sum(a, b) {         // 函数声明
    return a + b;
}
sum(1, 2);                   // 3

var sum = function (a, b) {  // 函数表达式
    return a + b;
};
sum(1, 2)                    // 3

var sum = new Function('a', 'b', 'return a + b;');
sum(1, 2)                    // 3

使用Function()构造函数时,首先传递参数名称(作为字符串),然后传递函数主体的源代码(再次作为字符串)。 JavaScript引擎需要评估这个传递的源代码并为此创建新函数。 此源代码评估与eval()函数具有相同的缺点,因此应尽可能避免使用Function()构造函数定义函数。

在JavaScript中,传统函数扮演多个角色。 它们是非方法函数(又名子例程或函数),方法(对象的一部分)和构造函数。 当函数执行 子程序的职责时,有一个动态this的小问题。 由于未在对象上调用子程序,因此在严格模式下未定义其值,否则将其设置为全局范围。 这使得编写回调函数很困难。 考虑以下示例:

var greeter = {
    default: "你好 ",
    greet: function (names){
        names.forEach(function(name) {
            console.log(this.default + name);    //无法读取未定义的“default”属性
        })
    }
}
console.log(greeter.greet(['世界', '天堂']));

以上代码正在将一个子例程传递给names数组上的forEach()函数。 该子例程具有一个this的不确定值,并且不幸的是,它无法访问外部方法的此属性。 显然,此子例程需要一个词法,这是从greet方法的周围范围派生的。 传统上,为了解决此限制,为此将词法分配给一个变量,然后该子例程可通过闭包访问该变量。

前面的示例修订如下:

var greeter = {
    default: "你好 ",
    greet: function (names){
        let that = this
        names.forEach(function(name) {
            console.log(that.default + name);
        })
    }
}
console.log(greeter.greet(['世界', '天堂']));

箭头函数的一个重要方面是它们的行为不同于通常函数, 差异是细微的但也是重要的,箭头函数没有它们自己的this值。 箭头函数中的this的值是从封闭的作用域继承的。

使用箭头函数来改变前面的示例以使用词法:

var greeter = {
    default: "Hello ",
    greet: function (names){
        names.forEach(name=> {
            console.log(this.default + name);  //词法“this”可用于此子例程
        })
    }
}
console.log(greeter.greet(['世界', '天堂']));

3.1 基本用法

ES6 允许使用“箭头”(=>)定义函数。

var f = x => x;

// 等同于
var f = function (x) {
    return x;
};

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

var f = () => 8;
// 等同于
var f = function () { return 8 };

var sum = (parm1, parm2) => parm1 + parm2;
// 等同于
var sum = function(parm1, parm2) {
    return parm1 + parm2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

  var result = (parm1,parm2) => { return parm1 + parm2; }

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上圆括号,否则会报错。

let student = id => ({ id: id, name: "张三" });

如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

let f = () => void doesNotReturn();

3.2 注意事项

箭头函数有几个使用注意点:

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象;
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误;
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替;
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。

上述注意事项中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。

function fn() {
    setTimeout(() => {
        console.log('id:', this.id);
    }, 1000);
}

var id = 21;
fn.call({ id: 42 });       //42

上面代码中,setTimeout()的参数是一个箭头函数,这个箭头函数的定义生效是在fn函数生成时,而它的真正执行要等到 1000 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象({id: 42}),所以输出的是42。

箭头函数可以让setTimeout()里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域

箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

// ES6
function fn() {
    setTimeout(() => {
       console.log('id:', this.id);
    }, 1000);
}

// ES5
function fn() {
    var _this = this;
    setTimeout(function () {
       console.log('id:', _this.id);
    }, 1000);
}

上面代码中,清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this

四、类和模块

JavaScript是一种基于原型的语言,并支持原型继承。 在HTML5 基础知识、第 4 部分中,讨论了对象的原型属性以及JavaScript中原型继承的工作方式。 ES6引入了类, 如果您来自传统的面向对象语言(例如Java),则将立即涉及到众所周知的类概念。 但是,它们在JavaScript中并不相同。 JavaScript中的类是在ES5中讨论的原型继承的语法糖。

在这里介绍的ES6类和模块,使面向对象编程(OOP)和继承显得更容易。如果您来自传统的面向对象编程语言的学习,那么原型继承可能对您来说有点不合适。 ES6为您提供了更传统的语法,以使您熟悉JavaScript中的原型继承。

在尝试深入研究类之前,展示一下为什么应该在ES5原型继承语法上使用ES6中的语法。在以下代码段中,将创建一组简单的Person、Student和PartTimeStudent的类层次结构。 首先,将看到ES5原型继承,其编写方式如下:

清单 1. 人、学生、兼职学生相应构造函数Person、Student、PartTimeStudent及ES5原型继承

        var Person = function(name) {
            if(!(this instanceof Person)) {
                throw new Error("Person 是一个构造函数");
            }
            this.name = name;
        };

        Person.prototype.giveBirth = function() {
            // ...给出person的出生日期
        };

        var Student = function(name, homework) {
            if(!(this instanceof Student)) {
                throw new Error("Student 是一个构造函数");
            }
            Person.call(this, name);
            this.homework = homework;
        };
        Student.prototype = Object.create(Person.prototype);
        Student.prototype.constructor = Student;
        Student.prototype.doHomework = function() {
            // ...Student 做作业
        };

        var PartTimeStudent = function(name, homework, department) {
            if(!(this instanceof PartTimeStudent)) {
                throw new Error("PartTimeStudent 是一个构造函数");
            }
            Student.call(this, name, homework);
            this.department = department;
        };
        PartTimeStudent.prototype = Object.create(Student.prototype);
        PartTimeStudent.prototype.constructor = PartTimeStudent;
        PartTimeStudent.prototype.startWorking = function() {
            // ...PartTimeStudent 开始工作
        };

现在,来看一下使用ES6类语法的等效代码:

清单2. 人、学生、兼职学生的类层次结构,使用ES6编写代码

        class Person {
            constructor(name) {
                this.namet = name;
            }
            giveBirth() {
                // ... 给出person的出生日期
            }
        }
        class Student extends Person {
            constructor(name, homework) {
                super(name);
                this.homework = homework;
            }
            doHomework() {
                // ...Student 做作业
            }
        }
        class PartTimeStudent extends Student {
            constructor(name, homework, department) {
                super(name, homework);
                this.department = department;
            }
            startWorking() {
                // ...PartTimeStudent 开始工作
            }
        }

如果您观察前面的两个清单的代码段,那么,显然第二个清单示例非常简洁。 如果您已知Java程序设计语言,就会感到宾至如归。 但是,要记住的重要一件事是,类没有向语言引入任何新的面向对象的继承模型,而是带来了一种创建对象和处理继承的更好的方法。

ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

4.1 定义类

在内部,类是特殊函数。 就像可以使用函数表达式和声明来定义函数一样,也可以定义类。 定义类的一种方法是使用类声明。

可以使用class关键字和类的名称。 此语法与Java的语法非常相似。

        class Car {
            constructor(model, year) {
                this.model = model;
                this.year = year;
            }
        }
        console.log(typeof Car);         //"function"

为了确定类是特殊函数的事实,如果在控制台日志中记录获得Car类的类型,则将得到一个函数类型。

类和常规函数之间有重要区别。 常规函数具有提升功能,而类却没有。 当在作用域的范围内声明了常规函数时,该函数立即可用,意思是在执行代码之前会先读取函数声明。 这个称为提升,意味着可以在范围内的任何地方声明一个常规函数,并且该函数将可用。 但是,类不具有提升作用, 它们只有在声明后才可用。

常规函数与类的声明与调用情况如下:

normalFunction();              // 先使用,函数调用
function normalFunction() {}   // 后声明,声明函数

var ford = new Car();          // 类实例化引用错误,不能在声明之前使用该类
class Car {}                   // 声明类

定义类的另一种方法是使用类表达式 类表达式(如函数表达式)可能具有名称,也可能没有名称。

以下示例显示了一个匿名类表达式:

const Car = class {
    constructor(model, year){
        this.model = model;
        this.year = year;
    }
}

如果为类表达式命名,则该名称是该类主体的局部名称,在外部不可用:

const NamedCar = class Car{
    constructor(model, year){
        this.model = model;
        this.year = year;
    }
    getName() {
        return Car.name;
    }
}
const ford = new NamedCar();
console.log(ford.getName());   // Car
console.log(ford.name);        // ReferenceError: name 未定义

如上所见,在这里,将为类命名为Car。 该名称在类的主体内可用,但是当我们尝试在类外部访问它时,会出现引用错误。

分隔类的成员时不能使用逗号。 分号是有效的。 这很有趣,因为ES6忽略分号,并且有关在ES6中使用分号的争论也很激烈。 考虑以下代码片段作为示例:

class NoCommas {
    method1(){}
    member1;       // 这将被忽略,可用于分隔类成员
    member2,       // 这是一个错误
    method2(){}
}

定义后,可以通过新关键字而不是函数调用来使用类; 如下所示:

class Car {
    constructor(model, year){
        this.model = model;
        this.year = year;
    }
}

const fiesta = new Car('嘉年华','2020');

4.2 构造方法

到目前为止,已经在示例中使用了constructor函数。 constructor是一种特殊方法,称为构造方法,用于创建和初始化使用该类创建的对象。 一个类中只能有一个构造方法。 构造方法与普通的构造函数不同。类构造方法可以通过super()调用其父类构造方法。 当后续研究继承时,将对此进行进行讨论。

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

class Point {
}

// 等同于
class Point {
     constructor() {}
}

上面代码中,定义了一个空的类Point,JavaScript 引擎会自动为它添加一个空的constructor方法。

constructor方法默认返回实例对象(即this)。类必须使用new调用,否则会报错。这是它跟ES5普通构造函数的一个主要区别,后者不用new也可以执行。

class Circle {
    constructor(r) {
        this.r = r
    }
}

Circle(5)
// TypeError: Class constructor Circle cannot be invoked without 'new'

4.3 原型方法

原型方法是定义在该类的prototype属性上,它们被该类的实例继承。

原型方法也可以具有getter和setter方法。 getter和setter的语法与ES5相同:

class Car {
    constructor(model, year){
        this.model = model;
        this.year = year;
    }
    get model(){
        return this.model;
    }
    set model(val){
        this.model = val;
    }
    calculateCurrentValue(){
        return "7000"
    }                                                                                              }
}
const fiesta = new Car('嘉年华','2010');
console.log(fiesta.model);

构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

class Point {
   constructor() {
      // ...
   }

   toString() {
      // ...
   }

   toValue() {
      // ...
   }
}

// 等同于

Point.prototype = {
   constructor() {},
   toString() {},
   toValue() {},
};

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

class Point {

   constructor(x, y) {
     this.x = x;
     this.y = y;
   }

   toString() {
     return '(' + this.x + ', ' + this.y + ')';
   }

}

var point = new Point(5, 6);
point.toString()                           // (5, 6)
point.hasOwnProperty('x')                  // true
point.hasOwnProperty('y')                  // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true

上面代码中,xy都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false

4.4 静态方法

静态方法与该类相关联,而不与该类(对象)的实例相关联。 换句话说,您只能使用类的名称来达到静态方法。 静态方法是在不实例化类的情况下调用的,并且不能在类的实例上调用它们。 静态方法在创建实用程序或辅助方法中很流行。 考虑以下代码:

class Logger {
    static log(level, message) {
        console.log(`${level} : ${message}`)
    }
}
//在类上调用静态方法
Logger.log("ERROR","The end is near")   // "ERROR : The end is near"
//不在实例上
const logger = new Logger("ERROR")
logger.log("The end is near")           // logger.log 不是一个函数

4.5 生成器方法

可以将生成器函数添加为类的一部分,它们称为生成器方法。 生成器方法很有用,因为您可以将其键定义为Symbol.iterator。 以下示例显示如何在类内部定义生成器方法:

class iterableArg {
    constructor(...args) {
        this.args = args;
    }
    * [Symbol.iterator]() {
        for (const arg of this.args) {
            yield arg;
        }
    }
}
for (const x of new iterableArg('ES6', 'wins')) {
    console.log(x);
}

4.6 继承

到目前为止,讨论了如何声明类以及可以支持的成员类型。 类的主要用途是用作创建其他子类的模板。 从类创建子类时,您将派生父类的属性并通过添加其自身的更多功能来扩展父类。

继承示例:

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(this.name + ' 通常叫声');
    }
}
class Cat extends Animal {
    speak() {
        console.log(this.name + ' 叫 喵喵.');
    }
}
var c = new Cat('小花');
c.speak();    //"小花 叫 喵喵."

在此,Animal是基类,而Cat类是从Animal类派生的。 extend子句允许您创建现有类的子类。 本示例演示子类化的语法。 让我们通过编写以下代码来进一步增强此示例:

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(this.name + ' 通常叫声');
    }
}
class Cat extends Animal {
    speak() {
        console.log(this.name + '  叫 喵喵.');
    }
}
class Lion extends Cat {
    speak() {
        super.speak();
        console.log(this.name + ' 咆哮....');
    }
}
var l = new Lion('莱尼');
l.speak();
// "莱尼 叫 喵喵."
// "莱尼 咆哮...."

在这里,使用super关键字从父类中调用函数。 以下是可以使用super关键字的三种方式:

  • 可以使用super(<params>)作为函数调用来调用父类的构造函数;
  • 可以使用super.<parentClassMethod>来访问父类方法;
  • 可以使用super.<parentClassProp>来访问父类属性;

在派生类构造函数中,必须调用super()方法。 例如,以下代码将失败:

class Base {}
class Derive extends Base {
    constructor(name){
        this.name = name; //  在super()调用之前,不允许使用“ this”
    }
}

您不能在默认的构造函数中隐藏super()方法,否则出现错误:

class Base {}
class Derive extends Base {
    constructor(){         //constructor中缺少super()调用
    }
}

treyt如果不为基类提供构造函数,则默认使用以下构造函数:

constructor() {}

对于派生类,默认构造函数如下:

constructor(...args){
    super(...args);
}

4.7 混入(Mixins)模式

JavaScript仅支持单一继承。 一个类最多只能有一个超类。 当您要创建类层次结构但又要从不同来源继承工具方法时,这是有限制的。

假设有一个场景,其中有一个Person类,再创建了一个子类Employee:

class Person {}
class Employee extends Person{}

还希望从两个实用程序类中继承函数,BackgroundCheck类负责员工的背景检查,而Onboard类处理员工的入职流程,例如打印徽章等:

class BackgroundCheck {
    check() {}
}
class Onboard {
    printBadge() { }
}

BackgroundCheck和Onboard类都是模板,它们的功能将被多次使用。 这样的模板(抽象子类)被称为混入(mixins)。

由于JavaScript无法实现多重继承,采用另一种技术来实现这一点。 在ES6中实现混入(mixins)的一种流行方法是编写一个函数,该函数将超类作为输入,并将扩展该超类的子类作为输出,例如:

class Person {}
const BackgroundCheck = Tools => class extends Tools {
    check() {}
};
const Onboard = Tools => class extends Tools {
    printBadge() {}
};
class Employee extends BackgroundCheck(Onboard(Person)){
}

从本质上讲,这意味着EmployeeBackgroundCheck的子类,而BackgroundCheck则是Onboard的子类,进一步Onboard则是Person的子类。

4.8 模块

JavaScript模块不是新的内容。 实际上,已有一段时间的库支持模块了。 不过,ES6提供了内置模块。 传统上,JavaScript的主要用途是 在浏览器中,大多数JavaScript代码都是嵌入式的,或者足够小,可以轻松管理。 现在事情变了,JavaScript项目规模越来越大。如果没有有效的将代码分布到文件和目录中的系统,代码管理将成为一场噩梦。

ES6模块是文件。 每个文件表示一个模块。 没有module关键字。 除非使用导出,否则您在模块文件中写入的任何代码都是模块本地的。 可能在一个模块中有一堆函数,并且只想导出其中一些。 可以通过两种方式导出模块功能。

首先是使用export关键字, 可以导出任何顶级声明的function, class, var, let, 或 const。

以下示例显示了server.js中的模块,在其中导出函数、类和常量。 不导出processConfig()函数,而且在任何导入此模块的文件中将无法访问未导出的函数:

//----------------server.js---------------------
export const port = 8080;
export function startServer() {
    //...启动服务器
}
export class Config {
    //...
}
function processConfig() {
    //...
}

任何有权访问server.js的代码都可以导入已导出的功能。

//--------------app.js----------------------------
import {Config, startServer, port} from 'server'
startServer(port);

在这种情况下,另一个JavaScript文件正在从服务器模块导入Config、startServer和port(带有相应响应的JavaScript文件server.js,将文件扩展名删除)

在了解ES6模块系统的同时,也需要了解ES5如何通过外部库支持它们。 ES5具有两个不兼容的模块系统,如下所示:

  • CommonJS: 这是Node.js采纳的主要标准;
  • AMD (Asynchronous Module Definition): 这比CommonJS稍微复杂一点,并且设计用于异步模块加载,而针对的是浏览器。

ES6的模块的目标是使来自任何这些系统的模块更易于使用。 ES6模块语法也已标准化,并且比其他替代方法更紧凑。

导出列表

为了替换从模块中使用export关键字标明每个要导出的函数或类,可以只用export关键字标明单个的列表,其中包含所有需要从模块中导出的事项。

export {port, startServer, Config};
const port = 8080;
function startServer() {
    //...启动服务器
}
class Config {
    //...
}
function processConfig() {
    //...
}

模块的第一行是导出列表。 可以在模块文件中包含多个导出列表,并且该列表可以出现在文件中的任何位置。 也可以在同一模块文件中混合使用导出列表和导出声明,但是只能导出一个名称一次。

在大型项目中,有时会遇到名称冲突。 假设您导入了两个模块,并且它们都导出了具有相同名称的函数。 在这种情况下,可以按以下方式重命名导入:

import {trunc as StringLib} from "../lib/string.js"
import {trunc as MathLib} from "../lib/math.js"

在这里,两个导入的模块都导出了名称trunc,因此造成了名称冲突。 我们可以使用别名来解决此冲突。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值