2.png 0
8i98

家事,国事,天下事,事事关心

2022-03-13

js笔记10-es6新特性2

2022-05-31 0 50 web前端社区

2.png 0 8i98

Iterator 和 for…of 循环

Iterator 迭代器

定义

迭代器是一个特殊对象,每个迭代器都有一个 next() 方法,每次调用都返回一个结果对象;结果对象有两个属性:一个是 value 表示当前迭代返回的值,另一个是 done 表示迭代是否完成。

ES5 实现

  1. function createIterator(items) {
  2. var i = 0;
  3. return {
  4. next: function () {
  5. var done = i >= items.length;
  6. var value = done ? undefined : items[i++];
  7. return { value, done };
  8. }
  9. };
  10. }
  11. const iterator = createIterator([1, 2, 3]);
  12. iterator.next(); // {value: 1, done: false}
  13. iterator.next(); // {value: 2, done: false}
  14. iterator.next(); // {value: 3, done: false}
  15. iterator.next(); // {value: undefined, done: true}

Generator 生成器

定义

生成器是返回迭代器的函数

使用

ES6 支持通过 * 与 yield 关键字实现

  1. // 声明
  2. function *createItrator() {
  3. yield 1;
  4. yield 2;
  5. yield 3;
  6. }
  7. // 表达式
  8. const createIterator = function *(){
  9. yield 1;
  10. yield 2;
  11. yield 3;
  12. }
  13. const iterator = createIterator();
  14. iterator.next(); // {value: 1, done: false}
  15. iterator.next(); // {value: 2, done: false}
  16. iterator.next(); // {value: 3, done: false}
  17. iterator.next(); // {value: undefined, done: true}
  • yield 通过它来指定调用迭代器的 next() 方法时的返回值及返回顺序。
  • 每执行一条 yield 语句之后函数就会停止执行,直到再次调用迭代器的 next() 方法

yield 的使用限制

yield 只可以在生成器的内部使用,否则会会抛出错误,即便是生成器内部的函数里使用也是如此;

  1. function *createIterator(items) {
  2. items.forEach(function() {
  3. yield item + 1; // 在内部函数中使用,导致语法错误
  4. })
  5. }
  6. // Uncaught SyntaxError: Unexpected identifier

yield 与 return 关键字一样,二者都不能穿透函数边界。

生成器对象的方法

可迭代对象和 for-of 循环

可迭代对象具有 Symbol.iterator 属性,Symbol.iterator 通过指定的函数返回一个作用域附属对象的迭代器。

  1. const arr = [1, 2, 3];
  2. for (let num of arr) {
  3. console.log(num);
  4. }

for-of 循环代码通过调用 values 数组的 Symbol.iterator 方法来获取迭代器,随后迭代器的 next() 方法被多次调用,从其对象的 value 属性读取值并存储在变量 num 中

访问默认迭代器

  1. let values = [1, 2, 3];
  2. let iterator = values[Symbol.iterator]();
  3. console.log(iterator.next()); // {value: 1, done: false}

判断是否为可迭代对象

  1. function isIterable(object) {
  2. return typeof object[Symbol.iterator] === 'function'
  3. }

创建可迭代对象

默认情况下,开发者定义的对象都是不可迭代对象,但如果给 Symbol.iterator 属性添加一个生成器,则可以将其变为可迭代对象

  1. let collection = {
  2. items: [],
  3. *[Symbol.iterator]() {
  4. for (let item of this.items) {
  5. yield item;
  6. }
  7. }
  8. }
  9. collection.item.push(1);
  10. collection.item.push(2);
  11. collection.item.push(3);
  12. for (let x of collection) {
  13. console.log(x);
  14. }

原生具备 Iterator 接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

for…of 循环

ES6 借鉴 C++、Java、C# 和 Python 语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。

一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

数组

数组原生具备iterator接口(即默认部署了Symbol.iterator属性),for...of循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。

  1. const arr = ['red', 'green', 'blue'];
  2. for(let v of arr) {
  3. console.log(v); // red green blue
  4. }
  5. const obj = {};
  6. obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);
  7. for(let v of obj) {
  8. console.log(v); // red green blue
  9. }

for...of循环可以代替数组实例的forEach方法。

  1. const arr = ['red', 'green', 'blue'];
  2. arr.forEach(function (element, index) {
  3. console.log(element); // red green blue
  4. console.log(index); // 0 1 2
  5. });

JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。

  1. var arr = ['a', 'b', 'c', 'd'];
  2. for (let a in arr) {
  3. console.log(a); // 0 1 2 3
  4. }
  5. for (let a of arr) {
  6. console.log(a); // a b c d
  7. }

与其他遍历语法的比较

以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是for循环。

  1. for (var index = 0; index < myArray.length; index++) {
  2. console.log(myArray[index]);
  3. }

这种写法比较麻烦,因此数组提供内置的forEach方法。

  1. myArray.forEach(function (value) {
  2. console.log(value);
  3. });

这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。

for...in循环可以遍历数组的键名。

  1. for (var index in myArray) {
  2. console.log(myArray[index]);
  3. }

for...in循环有几个缺点。

  • 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
  • for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  • 某些情况下,for...in循环会以任意顺序遍历键名。

总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。

for...of循环相比上面几种做法,有一些显著的优点。

  1. for (let value of myArray) {
  2. console.log(value);
  3. }
  • 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
  • 不同于forEach方法,它可以与breakcontinuereturn配合使用。
  • 提供了遍历所有数据结构的统一操作接口。

下面是一个使用 break 语句,跳出for...of循环的例子。

  1. for (var n of fibonacci) {
  2. if (n > 1000)
  3. break;
  4. console.log(n);
  5. }

上面的例子,会输出斐波纳契数列小于等于 1000 的项。如果当前项大于 1000,就会使用break语句跳出for...of循环。

async 函数

含义

基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

  1. function timeout(ms) {
  2. return new Promise((resolve) => {
  3. setTimeout(resolve, ms);
  4. });
  5. }
  6. async function asyncPrint(value, ms) {
  7. await timeout(ms);
  8. console.log(value);
  9. }
  10. asyncPrint('hello world', 50);

上面代码指定 50 毫秒以后,输出hello world

由于async函数返回的是 Promise 对象,可以作为await命令的参数。所以,上面的例子也可以写成下面的形式。

  1. async function timeout(ms) {
  2. await new Promise((resolve) => {
  3. setTimeout(resolve, ms);
  4. });
  5. }
  6. async function asyncPrint(value, ms) {
  7. await timeout(ms);
  8. console.log(value);
  9. }
  10. asyncPrint('hello world', 50);

async 函数有多种使用形式。

  1. // 函数声明
  2. async function foo() {}
  3. // 函数表达式
  4. const foo = async function () {};
  5. // 对象的方法
  6. let obj = { async foo() {} };
  7. obj.foo().then(...)
  8. // Class 的方法
  9. class Storage {
  10. constructor() {
  11. this.cachePromise = caches.open('avatars');
  12. }
  13. async getAvatar(name) {
  14. const cache = await this.cachePromise;
  15. return cache.match(`/avatars/${name}.jpg`);
  16. }
  17. }
  18. const storage = new Storage();
  19. storage.getAvatar('jake').then(…);
  20. // 箭头函数
  21. const foo = async () => {};

语法

async函数的语法规则总体上比较简单,难点是错误处理机制。

返回 Promise 对象

async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数。

  1. async function f() {
  2. return 'hello world';
  3. }
  4. f().then(v => console.log(v))
  5. // "hello world"

上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到。

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

  1. async function f() {
  2. throw new Error('出错了');
  3. }
  4. f().then(
  5. v => console.log('resolve', v),
  6. e => console.log('reject', e)
  7. )
  8. //reject Error: 出错了

Promise 对象的状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

下面是一个例子。

  1. async function getTitle(url) {
  2. let response = await fetch(url);
  3. let html = await response.text();
  4. return html.match(/<title>([\s\S]+)<\/title>/i)[1];
  5. }
  6. getTitle('https://tc39.github.io/ecma262/').then(console.log)
  7. // "ECMAScript 2017 Language Specification"

上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log

await 命令

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

  1. async function f() {
  2. // 等同于
  3. // return 123;
  4. return await 123;
  5. }
  6. f().then(v => console.log(v))
  7. // 123

上面代码中,await命令的参数是数值123,这时等同于return 123

另一种情况是,await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象。

  1. class Sleep {
  2. constructor(timeout) {
  3. this.timeout = timeout;
  4. }
  5. then(resolve, reject) {
  6. const startTime = Date.now();
  7. setTimeout(
  8. () => resolve(Date.now() - startTime),
  9. this.timeout
  10. );
  11. }
  12. }
  13. (async () => {
  14. const sleepTime = await new Sleep(1000);
  15. console.log(sleepTime);
  16. })();
  17. // 1000

上面代码中,await命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。

这个例子还演示了如何实现休眠效果。JavaScript 一直没有休眠的语法,但是借助await命令就可以让程序停顿指定的时间。下面给出了一个简化的sleep实现。

  1. function sleep(interval) {
  2. return new Promise(resolve => {
  3. setTimeout(resolve, interval);
  4. })
  5. }
  6. // 用法
  7. async function one2FiveInAsync() {
  8. for(let i = 1; i <= 5; i++) {
  9. console.log(i);
  10. await sleep(1000);
  11. }
  12. }
  13. one2FiveInAsync();

await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。

  1. async function f() {
  2. await Promise.reject('出错了');
  3. }
  4. f()
  5. .then(v => console.log(v))
  6. .catch(e => console.log(e))
  7. // 出错了

注意,上面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

  1. async function f() {
  2. await Promise.reject('出错了');
  3. await Promise.resolve('hello world'); // 不会执行
  4. }

上面代码中,第二个await语句是不会执行的,因为第一个await语句状态变成了reject

有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

  1. async function f() {
  2. try {
  3. await Promise.reject('出错了');
  4. } catch(e) {
  5. }
  6. return await Promise.resolve('hello world');
  7. }
  8. f()
  9. .then(v => console.log(v))
  10. // hello world

另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

  1. async function f() {
  2. await Promise.reject('出错了')
  3. .catch(e => console.log(e));
  4. return await Promise.resolve('hello world');
  5. }
  6. f()
  7. .then(v => console.log(v))
  8. // 出错了
  9. // hello world

错误处理

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

  1. async function f() {
  2. await new Promise(function (resolve, reject) {
  3. throw new Error('出错了');
  4. });
  5. }
  6. f()
  7. .then(v => console.log(v))
  8. .catch(e => console.log(e))
  9. // Error:出错了

上面代码中,async函数f执行后,await后面的 Promise 对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象。具体的执行机制,可以参考后文的“async 函数的实现原理”。

防止出错的方法,也是将其放在try...catch代码块之中。

  1. async function f() {
  2. try {
  3. await new Promise(function (resolve, reject) {
  4. throw new Error('出错了');
  5. });
  6. } catch(e) {
  7. }
  8. return await('hello world');
  9. }

如果有多个await命令,可以统一放在try...catch结构中。

  1. async function main() {
  2. try {
  3. const val1 = await firstStep();
  4. const val2 = await secondStep(val1);
  5. const val3 = await thirdStep(val1, val2);
  6. console.log('Final: ', val3);
  7. }
  8. catch (err) {
  9. console.error(err);
  10. }
  11. }

下面的例子使用try...catch结构,实现多次重复尝试。

  1. const superagent = require('superagent');
  2. const NUM_RETRIES = 3;
  3. async function test() {
  4. let i;
  5. for (i = 0; i < NUM_RETRIES; ++i) {
  6. try {
  7. await superagent.get('http://google.com/this-throws-an-error');
  8. break;
  9. } catch(err) {}
  10. }
  11. console.log(i); // 3
  12. }
  13. test();

上面代码中,如果await操作成功,就会使用break语句退出循环;如果失败,会被catch语句捕捉,然后进入下一轮循环。

使用注意点

第一点,前面已经说过,await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。

  1. async function myFunction() {
  2. try {
  3. await somethingThatReturnsAPromise();
  4. } catch (err) {
  5. console.log(err);
  6. }
  7. }
  8. // 另一种写法
  9. async function myFunction() {
  10. await somethingThatReturnsAPromise()
  11. .catch(function (err) {
  12. console.log(err);
  13. });
  14. }

第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

  1. let foo = await getFoo();
  2. let bar = await getBar();

上面代码中,getFoogetBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

  1. // 写法一
  2. let [foo, bar] = await Promise.all([getFoo(), getBar()]);
  3. // 写法二
  4. let fooPromise = getFoo();
  5. let barPromise = getBar();
  6. let foo = await fooPromise;
  7. let bar = await barPromise;

上面两种写法,getFoogetBar都是同时触发,这样就会缩短程序的执行时间。

第三点,await命令只能用在async函数之中,如果用在普通函数,就会报错。

  1. async function dbFuc(db) {
  2. let docs = [{}, {}, {}];
  3. // 报错
  4. docs.forEach(function (doc) {
  5. await db.post(doc);
  6. });
  7. }

Class 的基本语法

简介

类的由来

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

  1. function Point(x, y) {
  2. this.x = x;
  3. this.y = y;
  4. }
  5. Point.prototype.toString = function () {
  6. return '(' + this.x + ', ' + this.y + ')';
  7. };
  8. var p = new Point(1, 2);

上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

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

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. toString() {
  7. return '(' + this.x + ', ' + this.y + ')';
  8. }
  9. }

上面代码定义了一个“类”,可以看到里面有一个constructor()方法,这就是构造方法,而this关键字则代表实例对象。这种新的 Class 写法,本质上与本章开头的 ES5 的构造函数Point是一致的。

Point类除了构造方法,还定义了一个toString()方法。注意,定义toString()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。

ES6 的类,完全可以看作构造函数的另一种写法。

  1. class Point {
  2. // ...
  3. }
  4. typeof Point // "function"
  5. Point === Point.prototype.constructor // true

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

  1. class Bar {
  2. doStuff() {
  3. console.log('stuff');
  4. }
  5. }
  6. const b = new Bar();
  7. b.doStuff() // "stuff"

constructor 方法

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

  1. class Point {
  2. }
  3. // 等同于
  4. class Point {
  5. constructor() {}
  6. }

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

constructor()方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

  1. class Foo {
  2. constructor() {
  3. return Object.create(null);
  4. }
  5. }
  6. new Foo() instanceof Foo
  7. // false

上面代码中,constructor()函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。

类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

  1. class Foo {
  2. constructor() {
  3. return Object.create(null);
  4. }
  5. }
  6. Foo()
  7. // TypeError: Class constructor Foo cannot be invoked without 'new'

类的实例

生成类的实例的写法,与 ES5 完全一样,也是使用new命令。前面说过,如果忘记加上new,像函数那样调用Class,将会报错。

  1. class Point {
  2. // ...
  3. }
  4. // 报错
  5. var point = Point(2, 3);
  6. // 正确
  7. var point = new Point(2, 3);

取值函数(getter)和存值函数(setter)

与 ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

  1. class MyClass {
  2. constructor() {
  3. // ...
  4. }
  5. get prop() {
  6. return 'getter';
  7. }
  8. set prop(value) {
  9. console.log('setter: '+value);
  10. }
  11. }
  12. let inst = new MyClass();
  13. inst.prop = 123;
  14. // setter: 123
  15. inst.prop
  16. // 'getter'

上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。

属性表达式

类的属性名,可以采用表达式。

  1. let methodName = 'getArea';
  2. class Square {
  3. constructor(length) {
  4. // ...
  5. }
  6. [methodName]() {
  7. // ...
  8. }
  9. }

上面代码中,Square类的方法名getArea,是从表达式得到的。

Class 表达式

与函数一样,类也可以使用表达式的形式定义。

  1. const MyClass = class Me {
  2. getClassName() {
  3. return Me.name;
  4. }
  5. };

上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。

  1. let inst = new MyClass();
  2. inst.getClassName() // Me
  3. Me.name // ReferenceError: Me is not defined

上面代码表示,Me只在 Class 内部有定义。

如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。

  1. const MyClass = class { /* ... */ };

采用 Class 表达式,可以写出立即执行的 Class。

  1. let person = new class {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. sayName() {
  6. console.log(this.name);
  7. }
  8. }('张三');
  9. person.sayName(); // "张三"

上面代码中,person是一个立即执行的类的实例。

注意点

(1)严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

(2)不存在提升

类不存在变量提升(hoist),这一点与 ES5 完全不同。

  1. new Foo(); // ReferenceError
  2. class Foo {}

上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。

  1. {
  2. let Foo = class {};
  3. class Bar extends Foo {
  4. }
  5. }

上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。

(5)this 的指向

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

  1. class Logger {
  2. printName(name = 'there') {
  3. this.print(`Hello ${name}`);
  4. }
  5. print(text) {
  6. console.log(text);
  7. }
  8. }
  9. const logger = new Logger();
  10. const { printName } = logger;
  11. printName(); // TypeError: Cannot read property 'print' of undefined

静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6. Foo.classMethod() // 'hello'
  7. var foo = new Foo();
  8. foo.classMethod()
  9. // TypeError: foo.classMethod is not a function

上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

  1. class Foo {
  2. static bar() {
  3. this.baz();
  4. }
  5. static baz() {
  6. console.log('hello');
  7. }
  8. baz() {
  9. console.log('world');
  10. }
  11. }
  12. Foo.bar() // hello

上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。

父类的静态方法,可以被子类继承。

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6. class Bar extends Foo {
  7. }
  8. Bar.classMethod() // 'hello'

上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。

静态方法也是可以从super对象上调用的。

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6. class Bar extends Foo {
  7. static classMethod() {
  8. return super.classMethod() + ', too';
  9. }
  10. }
  11. Bar.classMethod() // "hello, too"

实例属性的新写法

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。

  1. class IncreasingCounter {
  2. constructor() {
  3. this._count = 0;
  4. }
  5. get value() {
  6. console.log('Getting the current value!');
  7. return this._count;
  8. }
  9. increment() {
  10. this._count++;
  11. }
  12. }

上面代码中,实例属性this._count定义在constructor()方法里面。另一种写法是,这个属性也可以定义在类的最顶层,其他都不变。

  1. class IncreasingCounter {
  2. _count = 0;
  3. get value() {
  4. console.log('Getting the current value!');
  5. return this._count;
  6. }
  7. increment() {
  8. this._count++;
  9. }
  10. }

上面代码中,实例属性_count与取值函数value()increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。

  1. class foo {
  2. bar = 'hello';
  3. baz = 'world';
  4. constructor() {
  5. // ...
  6. }
  7. }

上面的代码,一眼就能看出,foo类有两个实例属性,一目了然。另外,写起来也比较简洁。

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

  1. class Foo {
  2. }
  3. Foo.prop = 1;
  4. Foo.prop // 1

上面的写法为Foo类定义了一个静态属性prop

目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上static关键字。

  1. class MyClass {
  2. static myStaticProp = 42;
  3. constructor() {
  4. console.log(MyClass.myStaticProp); // 42
  5. }
  6. }

这个新写法大大方便了静态属性的表达。

  1. // 老写法
  2. class Foo {
  3. // ...
  4. }
  5. Foo.prop = 1;
  6. // 新写法
  7. class Foo {
  8. static prop = 1;
  9. }

上面代码中,老写法的静态属性定义在类的外部。整个类生成以后,再生成静态属性。这样让人很容易忽略这个静态属性,也不符合相关代码应该放在一起的代码组织原则。另外,新写法是显式声明(declarative),而不是赋值处理,语义更好。

私有方法和私有属性

现有的解决方案

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。

一种做法是在命名上加以区别。

  1. class Widget {
  2. // 公有方法
  3. foo (baz) {
  4. this._bar(baz);
  5. }
  6. // 私有方法
  7. _bar(baz) {
  8. return this.snaf = baz;
  9. }
  10. // ...
  11. }

上面代码中,_bar()方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。

另一种方法就是索性将私有方法移出类,因为类内部的所有方法都是对外可见的。

  1. class Widget {
  2. foo (baz) {
  3. bar.call(this, baz);
  4. }
  5. // ...
  6. }
  7. function bar(baz) {
  8. return this.snaf = baz;
  9. }

上面代码中,foo是公开方法,内部调用了bar.call(this, baz)。这使得bar()实际上成为了当前类的私有方法。

还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。

  1. const bar = Symbol('bar');
  2. const snaf = Symbol('snaf');
  3. export default class myClass{
  4. // 公有方法
  5. foo(baz) {
  6. this[bar](baz);
  7. }
  8. // 私有方法
  9. [bar](baz) {
  10. return this[snaf] = baz;
  11. }
  12. // ...
  13. };

上面代码中,barsnaf都是Symbol值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。

  1. const inst = new myClass();
  2. Reflect.ownKeys(myClass.prototype)
  3. // [ 'constructor', 'foo', Symbol(bar) ]

上面代码中,Symbol 值的属性名依然可以从类的外部拿到。

私有属性的提案

目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。

  1. class IncreasingCounter {
  2. #count = 0;
  3. get value() {
  4. console.log('Getting the current value!');
  5. return this.#count;
  6. }
  7. increment() {
  8. this.#count++;
  9. }
  10. }

上面代码中,#count就是私有属性,只能在类的内部使用(this.#count)。如果在类的外部使用,就会报错。

  1. const counter = new IncreasingCounter();
  2. counter.#count // 报错
  3. counter.#count = 42 // 报错

上面代码在类的外部,读取私有属性,就会报错。

in 运算符

try...catch结构可以用来判断是否存在某个私有属性。

  1. class A {
  2. use(obj) {
  3. try {
  4. obj.#foo;
  5. } catch {
  6. // 私有属性 #foo 不存在
  7. }
  8. }
  9. }
  10. const a = new A();
  11. a.use(a); // 报错

上面示例中,类A并不存在私有属性#foo,所以try...catch报错了。

这样的写法很麻烦,可读性很差,V8 引擎改进了in运算符,使它也可以用来判断私有属性。

  1. class A {
  2. use(obj) {
  3. if (#foo in obj) {
  4. // 私有属性 #foo 存在
  5. } else {
  6. // 私有属性 #foo 不存在
  7. }
  8. }
  9. }

上面示例中,in运算符判断当前类A的实例,是否有私有属性#foo,如果有返回true,否则返回false

in也可以跟this一起配合使用。

  1. class A {
  2. #foo = 0;
  3. m() {
  4. console.log(#foo in this); // true
  5. console.log(#bar in this); // false
  6. }
  7. }

注意,判断私有属性时,in只能用在定义该私有属性的类的内部。

  1. class A {
  2. #foo = 0;
  3. static test(obj) {
  4. console.log(#foo in obj);
  5. }
  6. }
  7. A.test(new A()) // true
  8. A.test({}) // false
  9. class B {
  10. #foo = 0;
  11. }
  12. A.test(new B()) // false

上面示例中,类A的私有属性#foo,只能在类A内部使用in运算符判断,而且只对A的实例返回true,对于其他对象都返回false

子类从父类继承的私有属性,也可以使用in运算符来判断。

  1. class A {
  2. #foo = 0;
  3. static test(obj) {
  4. console.log(#foo in obj);
  5. }
  6. }
  7. class SubA extends A {};
  8. A.test(new SubA()) // true

静态块

静态属性的一个问题是,它的初始化要么写在类的外部,要么写在constructor()方法里面。

  1. class C {
  2. static x = 234;
  3. static y;
  4. static z;
  5. }
  6. try {
  7. const obj = doSomethingWith(C.x);
  8. C.y = obj.y
  9. C.z = obj.z;
  10. } catch {
  11. C.y = ...;
  12. C.z = ...;
  13. }

上面示例中,静态属性yz的值依赖静态属性x,它们的初始化写在类的外部(上例的try...catch代码块)。另一种方法是写到类的constructor()方法里面。这两种方法都不是很理想,前者是将类的内部逻辑写到了外部,后者则是每次新建实例都会运行一次。

为了解决这个问题,ES2022 引入了静态块(static block),允许在类的内部设置一个代码块,在类生成时运行一次,主要作用是对静态属性进行初始化。

  1. class C {
  2. static x = ...;
  3. static y;
  4. static z;
  5. static {
  6. try {
  7. const obj = doSomethingWith(this.x);
  8. this.y = obj.y;
  9. this.z = obj.z;
  10. }
  11. catch {
  12. this.y = ...;
  13. this.z = ...;
  14. }
  15. }
  16. }

上面代码中,类的内部有一个 static 代码块,这就是静态块。它的好处是将静态属性yz的初始化逻辑,写入了类的内部,而且只运行一次。

每个类只能有一个静态块,在静态属性声明后运行。静态块的内部不能有return语句。

静态块内部可以使用类名或this,指代当前类。

  1. class C {
  2. static x = 1;
  3. static {
  4. this.x; // 1
  5. // 或者
  6. C.x; // 1
  7. }
  8. }

上面示例中,this.xC.x都能获取静态属性x

除了静态属性的初始化,静态块还有一个作用,就是将私有属性与类的外部代码分享。

  1. let getX;
  2. export class C {
  3. #x = 1;
  4. static {
  5. getX = obj => obj.#x;
  6. }
  7. }
  8. console.log(getX(new C())); // 1

上面示例中,#x是类的私有属性,如果类外部的getX()方法希望获取这个属性,以前是要写在类的constructor()方法里面,这样的话,每次新建实例都会定义一次getX()方法。现在可以写在静态块里面,这样的话,只在类生成时定义一次。

继承

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。

  1. class Point {
  2. }
  3. class ColorPoint extends Point {
  4. }

下面,我们在ColorPoint内部加上代码。

  1. class Point { /* ... */ }
  2. class ColorPoint extends Point {
  3. constructor(x, y, color) {
  4. super(x, y); // 调用父类的constructor(x, y)
  5. this.color = color;
  6. }
  7. toString() {
  8. return this.color + ' ' + super.toString(); // 调用父类的toString()
  9. }
  10. }

上面示例中,constructor()方法和toString()方法内部,都出现了super关键字。super在这里表示父类的构造函数,用来新建一个父类的实例对象。

ES6 规定,子类必须在constructor()方法中调用super(),否则就会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象。

  1. class Point { /* ... */ }
  2. class ColorPoint extends Point {
  3. constructor() {
  4. }
  5. }
  6. let cp = new ColorPoint(); // ReferenceError

上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super(),导致新建实例时报错。

为什么子类的构造函数,一定要调用super()?原因就在于 ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。

另一个需要注意的地方是,在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类。

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. }
  7. class ColorPoint extends Point {
  8. constructor(x, y, color) {
  9. this.color = color; // ReferenceError
  10. super(x, y);
  11. this.color = color; // 正确
  12. }
  13. }

上面代码中,子类的constructor()方法没有调用super()之前,就使用this关键字,结果报错,而放在super()之后就是正确的。

子类无法继承父类的私有属性,或者说,私有属性只能在定义它的 class 里面使用。

  1. class Foo {
  2. #p = 1;
  3. #m() {
  4. console.log('hello');
  5. }
  6. }
  7. class Bar extends Foo {
  8. constructor() {
  9. super();
  10. console.log(this.#p); // 报错
  11. this.#m(); // 报错
  12. }
  13. }

super 关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

  1. class A {}
  2. class B extends A {
  3. constructor() {
  4. super();
  5. }
  6. }

上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)

  1. class A {
  2. constructor() {
  3. console.log(new.target.name);
  4. }
  5. }
  6. class B extends A {
  7. constructor() {
  8. super();
  9. }
  10. }
  11. new A() // A
  12. new B() // B

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

  1. class A {
  2. p() {
  3. return 2;
  4. }
  5. }
  6. class B extends A {
  7. constructor() {
  8. super();
  9. console.log(super.p()); // 2
  10. }
  11. }
  12. let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

  1. class A {
  2. constructor() {
  3. this.p = 2;
  4. }
  5. }
  6. class B extends A {
  7. get m() {
  8. return super.p;
  9. }
  10. }
  11. let b = new B();
  12. b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

如果属性定义在父类的原型对象上,super就可以取到。

  1. class A {}
  2. A.prototype.x = 2;
  3. class B extends A {
  4. constructor() {
  5. super();
  6. console.log(super.x) // 2
  7. }
  8. }
  9. let b = new B();

原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

以前,这些原生构造函数是无法继承的,比如,不能自己定义一个Array的子类。

  1. function MyArray() {
  2. Array.apply(this, arguments);
  3. }
  4. MyArray.prototype = Object.create(Array.prototype, {
  5. constructor: {
  6. value: MyArray,
  7. writable: true,
  8. configurable: true,
  9. enumerable: true
  10. }
  11. });

上面代码定义了一个继承 Array 的MyArray类。但是,这个类的行为与Array完全不一致。

  1. var colors = new MyArray();
  2. colors[0] = "red";
  3. colors.length // 0
  4. colors.length = 0;
  5. colors[0] // "red"

之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。

ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。

  1. class MyArray extends Array {
  2. constructor(...args) {
  3. super(...args);
  4. }
  5. }
  6. var arr = new MyArray();
  7. arr[0] = 12;
  8. arr.length // 1
  9. arr.length = 0;
  10. arr[0] // undefined

上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。这意味着,ES6 可以自定义原生数据结构(比如ArrayString等)的子类,这是 ES5 无法做到的。

上面这个例子也说明,extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。下面就是定义了一个带版本功能的数组。

Module 模块语法

概述

  1. 什么是模块?

    将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
    块的内部数据/实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

CommonJS

Node.js 是 commonJS 规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:moduleexportsrequireglobal。实际使用时,用module.exports 定义当前模块对外输出的接口(不推荐直接用 exports),用 require 加载模块。

  1. var module={
  2. namelist:{}
  3. }
  4. var exports=new Proxy({},{
  5. set: function(target,key,receiver){
  6. target[key] = receiver;
  7. module.namelist[key] = receiver;
  8. return value;
  9. }
  10. });
  11. var require=function(name){
  12. return module.namelist[name];
  13. }
  14. exports.aaa={
  15. name:111,
  16. bbb:111
  17. }
  18. var a = require("aaa");
  19. console.log(a);

AMD

AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js

(1)使用define(function(){})函数定义模块;并且向外暴露

  1. // 定义一个没有依赖模块的data.js模块
  2. define(function(){
  3. let name = 'zhangsan';
  4. function getName(){
  5. return name;
  6. }
  7. console.log("我是data里的name:",name);
  8. // 暴露模块,推荐暴露对象,因为对象是用来存储数据的
  9. return {name,getName};
  10. });

(2)使用require(['module','module2'],function(){})定义有依赖的模块,注意,数组里面的模块名只是一个代号,不一定要写模块名;不过推荐使用模块名作为代号,这样别人一看就知道引入了哪个模块

  1. require(["./s1.js"],(data)=>{
  2. console.log(data);
  3. })

ES6 模块

export 命令

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

  1. // profile.js
  2. export var firstName = 'Michael';
  3. export var lastName = 'Jackson';
  4. export var year = 1958;

上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。

export的写法,除了像上面这样,还有另外一种。

  1. // profile.js
  2. var firstName = 'Michael';
  3. var lastName = 'Jackson';
  4. var year = 1958;
  5. export { firstName, lastName, year };

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

  1. function v1() { ... }
  2. function v2() { ... }
  3. export {
  4. v1 as streamV1,
  5. v2 as streamV2,
  6. v2 as streamLatestVersion
  7. };

上面代码使用as关键字,重命名了函数v1v2的对外接口。重命名后,v2可以用不同的名字输出两次。

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

  1. // 报错
  2. export 1;
  3. // 报错
  4. var m = 1;
  5. export m;

上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1。1只是一个值,不是接口。正确的写法是下面这样。

  1. // 写法一
  2. export var m = 1;
  3. // 写法二
  4. var m = 1;
  5. export {m};
  6. // 写法三
  7. var n = 1;
  8. export {n as m};

上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。

同样的,functionclass的输出,也必须遵守这样的写法。

  1. // 报错
  2. function f() {}
  3. export f;
  4. // 正确
  5. export function f() {};
  6. // 正确
  7. function f() {}
  8. export {f};

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

  1. export var foo = 'bar';
  2. setTimeout(() => foo = 'baz', 500);

上面代码输出变量foo,值为bar,500 毫秒之后变成baz

这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新

最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

  1. function foo() {
  2. export default 'bar' // SyntaxError
  3. }
  4. foo()

上面代码中,export语句放在函数之中,结果报错。

import 命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

  1. // main.js
  2. import { firstName, lastName, year } from './profile.js';
  3. function setName(element) {
  4. element.textContent = firstName + ' ' + lastName;
  5. }

上面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

  1. import { lastName as surname } from './profile.js';

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

  1. import {a} from './xxx.js'
  2. a = {}; // Syntax Error : 'a' is read-only;

上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。

  1. import {a} from './xxx.js'
  2. a.foo = 'hello'; // 合法操作

上面代码中,a的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

  1. import { myMethod } from 'util';

上面代码中,util是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。

注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。

  1. foo();
  2. import { foo } from 'my_module';

上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

  1. // 报错
  2. import { 'f' + 'oo' } from 'my_module';
  3. // 报错
  4. let module = 'my_module';
  5. import { foo } from module;
  6. // 报错
  7. if (x === 1) {
  8. import { foo } from 'module1';
  9. } else {
  10. import { foo } from 'module2';
  11. }

上面三种写法都会报错,因为它们用到了表达式、变量和if结构。在静态分析阶段,这些语法都是没法得到值的。

最后,import语句会执行所加载的模块,因此可以有下面的写法。

  1. import 'lodash';

上面代码仅仅执行lodash模块,但是不输入任何值。

如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

  1. import 'lodash';
  2. import 'lodash';

上面代码加载了两次lodash,但是只会执行一次。

  1. import { foo } from 'my_module';
  2. import { bar } from 'my_module';
  3. // 等同于
  4. import { foo, bar } from 'my_module';

上面代码中,虽然foobar在两个语句中加载,但是它们对应的是同一个my_module模块。也就是说,import语句是 Singleton 模式。

目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。

  1. require('core-js/modules/es6.symbol');
  2. require('core-js/modules/es6.promise');
  3. import React from 'React';

模块的整体加载

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

下面是一个circle.js文件,它输出两个方法areacircumference

  1. // circle.js
  2. export function area(radius) {
  3. return Math.PI * radius * radius;
  4. }
  5. export function circumference(radius) {
  6. return 2 * Math.PI * radius;
  7. }

现在,加载这个模块。

  1. // main.js
  2. import { area, circumference } from './circle';
  3. console.log('圆面积:' + area(4));
  4. console.log('圆周长:' + circumference(14));

上面写法是逐一指定要加载的方法,整体加载的写法如下。

  1. import * as circle from './circle';
  2. console.log('圆面积:' + circle.area(4));
  3. console.log('圆周长:' + circle.circumference(14));

注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。

  1. import * as circle from './circle';
  2. // 下面两行都是不允许的
  3. circle.foo = 'hello';
  4. circle.area = function () {};

export default 命令

从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

  1. // export-default.js
  2. export default function () {
  3. console.log('foo');
  4. }

上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

  1. // import-default.js
  2. import customName from './export-default';
  3. customName(); // 'foo'

上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。

export default命令用在非匿名函数前,也是可以的。

  1. // export-default.js
  2. export default function foo() {
  3. console.log('foo');
  4. }
  5. // 或者写成
  6. function foo() {
  7. console.log('foo');
  8. }
  9. export default foo;

上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。

下面比较一下默认输出和正常输出。

  1. // 第一组
  2. export default function crc32() { // 输出
  3. // ...
  4. }
  5. import crc32 from 'crc32'; // 输入
  6. // 第二组
  7. export function crc32() { // 输出
  8. // ...
  9. };
  10. import {crc32} from 'crc32'; // 输入

上面代码的两组写法,第一组是使用export default时,对应的import语句不需要使用大括号;第二组是不使用export default时,对应的import语句需要使用大括号。

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

  1. // modules.js
  2. function add(x, y) {
  3. return x * y;
  4. }
  5. export {add as default};
  6. // 等同于
  7. // export default add;
  8. // app.js
  9. import { default as foo } from 'modules';
  10. // 等同于
  11. // import foo from 'modules';

正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。

  1. // 正确
  2. export var a = 1;
  3. // 正确
  4. var a = 1;
  5. export default a;
  6. // 错误
  7. export default var a = 1;

上面代码中,export default a的含义是将变量a的值赋给变量default。所以,最后一种写法会报错。

同样地,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

  1. // 正确
  2. export default 42;
  3. // 报错
  4. export 42;

上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为default

有了export default命令,输入模块时就非常直观了,以输入 lodash 模块为例。

  1. import _ from 'lodash';

如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。

  1. import _, { each, forEach } from 'lodash';

对应上面代码的export语句如下。

  1. export default function (obj) {
  2. // ···
  3. }
  4. export function each(obj, iterator, context) {
  5. // ···
  6. }
  7. export { each as forEach };

上面代码的最后一行的意思是,暴露出forEach接口,默认指向each接口,即forEacheach指向同一个方法。

export default也可以用来输出类。

  1. // MyClass.js
  2. export default class { ... }
  3. // main.js
  4. import MyClass from 'MyClass';
  5. let o = new MyClass();

export 与 import 的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

  1. export { foo, bar } from 'my_module';
  2. // 可以简单理解为
  3. import { foo, bar } from 'my_module';
  4. export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

模块的接口改名和整体输出,也可以采用这种写法。

  1. // 接口改名
  2. export { foo as myFoo } from 'my_module';
  3. // 整体输出
  4. export * from 'my_module';

默认接口的写法如下。

  1. export { default } from 'foo';

具名接口改为默认接口的写法如下。

  1. export { es6 as default } from './someModule';
  2. // 等同于
  3. import { es6 } from './someModule';
  4. export default es6;

同样地,默认接口也可以改名为具名接口。

  1. export { default as es6 } from './someModule';

ES2020 之前,有一种import语句,没有对应的复合写法。

  1. import * as someIdentifier from "someModule";

ES2020补上了这个写法。

  1. export * as ns from "mod";
  2. // 等同于
  3. import * as ns from "mod";
  4. export {ns};

模块的继承

模块之间也可以继承。

假设有一个circleplus模块,继承了circle模块。

  1. // circleplus.js
  2. export * from 'circle';
  3. export var e = 2.71828182846;
  4. export default function(x) {
  5. return Math.exp(x);
  6. }

上面代码中的export *,表示再输出circle模块的所有属性和方法。注意,export *命令会忽略circle模块的default方法。然后,上面代码又输出了自定义的e变量和默认方法。

这时,也可以将circle的属性或方法,改名后再输出。

  1. // circleplus.js
  2. export { area as circleArea } from 'circle';

上面代码表示,只输出circle模块的area方法,且将其改名为circleArea

加载上面模块的写法如下。

  1. // main.js
  2. import * as math from 'circleplus';
  3. import exp from 'circleplus';
  4. console.log(exp(math.e));

上面代码中的import exp表示,将circleplus模块的默认方法加载为exp方法。

跨模块常量

const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。

  1. // constants.js 模块
  2. export const A = 1;
  3. export const B = 3;
  4. export const C = 4;
  5. // test1.js 模块
  6. import * as constants from './constants';
  7. console.log(constants.A); // 1
  8. console.log(constants.B); // 3
  9. // test2.js 模块
  10. import {A, B} from './constants';
  11. console.log(A); // 1
  12. console.log(B); // 3

如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。

  1. // constants/db.js
  2. export const db = {
  3. url: 'http://my.couchdbserver.local:5984',
  4. admin_username: 'admin',
  5. admin_password: 'admin password'
  6. };
  7. // constants/user.js
  8. export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

然后,将这些文件输出的常量,合并在index.js里面。

0

Copyright (C) 2021-2026 98社区 All Rights Reserved 蜀ICP备20012692号-3