Skip to content

3.1 ES6+:下一代语法标准

ES6+ 指的是从 ECMAScript 标准的第 6 个版本开始,到目前为止所有的更新。因为 ES6 是一个 JavaScript 现代语法的分水岭,从 ES6 之后,几乎每年会出一个新版,新增更多功能以应对风云变幻的前端局势。到今天 ES2022(ES13)已经出现,JavaScript 还在高速发展中。

当前 ES6 是最重要的,JavaScript 中创造性的更新都是来源于这个版本,比如 Promise,class,模块化等等,所以我们先从 ES6 开始学起。

3.1.1 变量与字符串的扩展

变量的解构赋值

ES6 新增了声明关键字 letconst,这两个关键字和 var 关键字的用法和区别已经在第二章介绍过了,总结就是 const 用于声明常量,let 用于声明局部变量。

除了声明方式有变化,变量/常量的读取方式也有了大大的简化,比如下面这个对象:

js
var foods = {
  best: "小龙虾",
  good: "火锅",
  normal: "快餐",
  bad: "方便面",
};

// 获取 best 和 bad
var best = foods.best;
var bad = foods.bad;

ES6 提供了 解构赋值 这种高效操作,可以用更少的代码来实现一样的效果:

js
var { best, bad } = foods;
console.log("best", best); // 小龙虾

解构赋值相当于批量声明并读取某一个对象的属性,写法明显更简洁。如果有属性重名的情况,还可以为属性设置别名,如:

js
var { best: best1, bad: bed1, hate } = foods;
console.log("best", best1); // 小龙虾
console.log("bed", bed1); // 方便面
console.log("hate", hate); // undefined

使用冒号 “:” 设置别名后,原本的属性名就不可再用了。如果结构的属性不存在,如上述代码的 hate,则默认值为 undefined。

解构赋值还可以对多层嵌套对象起作用,例子如下:

js
var address = {
  city: {
    name: "北京市",
    area: {
      name: "海淀区",
      school: {
        name: "北京大学",
      },
    },
  },
};

// 分别取出城市,区和学校
console.log(address.city.name); // 北京市
console.log(address.city.area.name); // 海淀区
console.log(address.city.area.school.name); // 北京大学

这个三层对象看起来比较复杂,实际上根据对象的层级结构,以相同的层级就能解构出内层的属性,代码如下:

js
let {
  city: {
    name: city_name,
    area: {
      name: area_name,
      school: { name: school_name },
    },
  },
} = address;

console.log(city_name); // 北京市
console.log(area_name); // 海淀区
console.log(school_name); // 北京大学

除了对象可以解构,数组解构也不在话下。区别是对象解构根据属性,数组解构则根据位置,看下面例子:

js
var foods = ["炸鸡", "啤酒", "烧烤"];
let [a, b, c] = foods;

console.log(a); // 炸鸡
console.log(b); // 啤酒
console.log(c); // 烧烤

数组结构要比对象简单了许多,因为数组结构不存在属性,因此也不需要指定别名。但是数组也存在层级,数组的层级解构时也是完全按照位置匹配,例子如下:

js
var foods = ["小龙虾", ["羊肉串", "板筋", ["烤鸡翅", "烤鸡爪"]]];

let [a, [b1, b2, [c1, c2]]] = foods;
console.log(a); // 小龙虾
console.log(b1, b2); // 羊肉串 板筋
console.log(c1, c2); // 烤鸡翅 烤鸡爪

字符串的扩展

字符串在项目开发中是使用最多的数据类型之一。字符串操作包括但不限于拼接、截取、获取某个位置的值等等,ES6 提供了许多字符串操作方法来实现这个功能。

比如,要想知道某个字符串当中是否包含某个字符片段,通常只能用 indexOf 方法来判断。

js
var str = "You are best engineer";
str.indexOf("best"); // 8
str.indexOf("bst"); // -1

ES6 提供了三种新方法可以更便捷的判断包含关系,它们都返回布尔值。

  • includes():字符串中是否包含某个字符
  • startsWith():字符串是否以某个字符开头
  • endsWith():字符串是否以某个字符结尾
js
var str = "You are best engineer";
str.includes("best"); // true
str.startsWith("You"); // true,注意这里区分大小写
str.endsWith("neer"); // true

还有一个 repeat() 方法可以将字符串重复 N 次,这个方法在前端测试元素内容过多时的滚动效果非常好用。代码如下:

js
var str = "测试内容";
str = str.repeat(100);
console.log(str); // 测试内容测试内容测试内容测试内...

另一个常见的场景是将字符串内的字符 A 全部替换为字符 B,而旧语法只提供的 replace() 方法只能替换第一个匹配的值。ES6 新增了 replaceAll() 方法可以快速替换所有内容,举例如下:

js
var str = "I love you, superstar is you";
str = str.replaceAll("you", "me");
console.log(str); // 'I love me, superstar is me'

ES6 提供的最强大的字符串功能当属模板字符串。模板字符串用反引号(`)标识,它大大简化了字符串与变量的拼接,同时提供了格式保留(如换行,缩进等),这使字符串的使用和展示都非常友好。

js
var title = "块级元素";
var divstr = `
<div>
  <span>${title}</span>
</div>
`;

如上代码,用字符串表示一个元素结构,换行和缩进都能保留,同时还可以指定变量。字符串模版中使用 ${} 符号嵌入变量,这使传统的加号(+)拼接字符串成为了过去式。

3.1.2 对象的扩展

在 JavaScript 中,对象无处不在。ES6 新增的属性、方法、特性不仅简化了数据操作的方式,还增强了数据操作的能力。

ES5 要求在对象中定义属性和方法时必须采用 key:value 的方式。ES6 则允许在 key == value 时只使用一个属性,这是一种简化用法。示例如下:

js
var city = "北京市";
function getCity() {
  return city;
}
var object = { city, getCity };
// 等同于 var object = { city: ciry, getCity: getCity }

console.log(object.city); // '北京市'
console.log(object.getCity()); // '北京市'

除了定义对象可以简化,读取对象的属性/方法同样可以简化,简化的方式就是我们前面介绍过的解构赋值了。

js
var { city, getCity } = object;

扩展运算符

ES6 为对象新增了一个好用且强大的符号,叫做扩展运算符(用 ... 表示),它可以将对象中的“剩余属性”另存到一个新的对象中。剩余属性是指原对象中未显式解构的属性/方法,例子如下:

js
var obj = { a: 1, b: 2, c: 3, d: 4, e: 5 };
let { a, b, ...other } = obj;
console.log(other); // {c: 3, d: 4, e: 5}

上面代码还是用解构赋值的方式取值,最后用到了 ... 符号表示扩展运算符,将 c、d、e 三个属性放到新对象 other 之内,other 对象内包含了除 a,b 之外的剩余属性。

注意:使用 ... 符号的要求是必须放在花括号的最后,否则 JavaScript 会解析错误。

既然扩展运算符可以取剩余参数,那么当然也可以取全部参数。当一个对象未显示解构任意属性,只提供了扩展运算符,那么新对象就会包含原对象的所有属性,这样即实现了对象的“复制”。代码如下:

js
var obj = { a: 1, b: 2 };
let { ...copy } = obj;
console.log(copy); // {a: 1, b: 2}

// 等同于
let copy = { ...obj };
console.log(copy); // {a: 1, b: 2}

上面的两种解构方式都可以实现对象复制,这种复制方式是一种浅拷贝。

描述对象

对象是由多个属性组成的结构,在项目开发中,会非常频繁的进行属性操作。JavaScript 的对象很灵活,属性可以任意的添加、删除、遍历。但有时候我们希望可以控制属性的操作,比如强制某个属性不可以删除。

实现这个需求的方式是设置描述对象 ——— 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。使用 Object.getOwnPropertyDescriptor() 方法可以获取描述对象:

js
let obj = { city: "北京" };
let desc = Object.getOwnPropertyDescriptor(obj, "city");
console.log(desc);
//  {
//    value: city,
//    writable: true,
//    enumerable: true,
//    configurable: true
//    get: undefined
//    set: undefined
//  }

描述对象中包含六个常用属性值,分别代表的意义是:

  • value:属性的值。
  • writable:属性值是否可以修改。
  • enumerable:属性是否可以遍历。
  • configurable:描述对象是否可以修改。
  • get:取值函数(getter)。
  • set:存值函数(setter)。

既然可以获取到属性的描述对象,那么必然可以修改描述对象。修改描述对象用 Object.defineProperty() 实现。代码如下:

js
let obj = { city: "北京" };
// 设置描述符,是否可修改值
Object.defineProperty(obj, "city", {
  writable: false,
});
obj.city = "上海";
console.log(obj); // { city: '北京' }

上面代码中修改属性 city 的描述对象,将其可修改性 writable 改为 false。接着修改 city 的属性值,显然修改不成功,说明设置 city 属性不可修改生效了。

描述对象中的 setter 和 getter 方法,设置之后会分别会在属性的赋值和读取时触发。值得一提的是,Vue 就是通过设置描述对象的 setter/getter,从而实现响应式系统。

对象的遍历

对象的一个比较重要的功能是对象遍历。遍历数组很好理解,分别读取数组的元素进行操作,但是对象该怎么遍历呢?其实也简单,只要将对象的属性和值分别转化为数组,不就可以遍历了吗?

所幸 ES6 提供了便捷的方法实现这些功能:

  • Object.keys():获取对象的属性数组
  • Object.values():获取对象的值数组
  • Object.entries():获取对象的属性和值数组

用法非常简单,看一个例子就明白:

js
var obj = {
  name: "李小龙",
  position: "香港",
  skill: "中国武术",
};

console.log(Object.keys(obj));
// ['name', 'position', 'skill']
console.log(Object.values(obj));
// ['李小龙', '香港', '中国武术']
console.log(Object.entries(obj));
// [['name','李小龙'], ['position','香港'], ['skill','中国武术']]

比如我们要判断对象的所有属性是否都不为空,此时这些方法就派上用场了。上述对象的属性遍历也可以通过描述对象来控制。属性描述对象的 enumerable 表示属性是否可遍历,为 true 时才能被以上三个方法遍历。

我们修改一下 enumerable 再看结果:

js
Object.defineProperty(obj, "skill", {
  enumerable: false,
});
console.log(Object.keys(obj));
// ['name', 'position']

此时 skill 属性已经不可遍历了。

对象拷贝

(1)浅拷贝

在第二章讲过,JavaScript 中的引用类型存在堆内存中,栈内存只存一个引用,复制一个对象时默认会复制它的引用。这会导致两个对象的值互相影响,举个例子:

js
var a = { name: "前端" };
var b = a;
b.name = "后端";
connsole.log(b); // { name: '后端' }
connsole.log(a); // { name: '后端' }

上面代码,变量 b 只复制了 a 的引用,因此修改 b 时 a 也会变化,这种复制方式就叫做浅拷贝。

上面我们介绍到,使用扩展运算符也可以实现对象复制,这种复制同样是浅拷贝。

js
var a = { name: "前端" };
var b = { ...a };

ES6 还有第 3 种实现对象浅拷贝的方法 —— 对象合并。对象合并是将多个目标对象的可枚举属性(enumerable 为 true 的属性)合并到一个新的对象中,通过 Object.assign() 方法来实现。

js
var obj = {};
var obj2 = { b: 2 };
var obj3 = { c: 3 };

Object.assign(obj, obj2, obj3);
console.log(obj); // { b: 2, c: 3 }

Object.assign() 方法与扩展运算符(...)都能实现对象的浅拷贝,区别是前者是对对象属性的扩增,后者是对对象属性的缩减,最终处理后的属性被赋值给一个新对象。

上面介绍了对象浅拷贝的 3 种方式,在很多场景下我们还需要深拷贝。

(2)深拷贝

深拷贝就是复制后的对象修改其属性/方法时不会影响到原对象。要实现所有数据类型的深拷贝很复杂,我们这里介绍最常用的 JSON 数据的深拷贝,实现方法很简单,看代码:

js
var obj = {
  name: "电影",
  category: {
    cartoon: "动漫",
    kungfu: "武侠",
    love: "爱情",
  },
  platform: ["腾讯视频", "爱奇艺", "优酷"],
};

var obj2 = JSON.parse(JSON.stringify(obj));
obj2.category.kungfu = "仙侠";
obj2.platform[2] = "哔哩哔哩";

console.log(obj2.category.kungfu, obj2.platform[2]); // 仙侠 哔哩哔哩
console.log(obj.category.kungfu, obj.platform[2]); // 武侠 优酷

如上代码,先将对象序列化(JSON.stringify())为字符串,然后再进行反序列化(JSON.parse())为对象,最终就能实现一个深拷贝后的对象了。

3.1.3 数组的扩展

数组常常与对象结伴使用,两者组成了复杂的 JSON 数据。数组的扩展主要表现在查询、过滤、遍历和转换 4 个方面。

数组查询

数组查询分为查询元素和查询索引两类,是指在一个数组中查询到某一个满足条件的数组或索引并返回。ES6 中的数组查询有四个方法:

  • find()
  • findLast()
  • findIndex()
  • findLastIndex()

find() 和 findLast() 方法的作用是从数组中查找元素,前者查找匹配的第一个元素,后者查找匹配的最后一个元素。函数执行后会返回查找到的元素。

js
var arrs = [
  { name: "赛罗", color: "红蓝" },
  { name: "捷德", color: "红黑" },
  { name: "维克特利", color: "红黑" },
  { name: "迪迦", color: "红蓝" },
];

var row = arrs.find((row) => row.color == "红蓝");
console.log(row.name); // 赛罗

var row2 = arrs.findLast((row) => row.color == "红蓝");
console.log(row2.name); // 迪迦

如上代码,查找到就会返回匹配的元素,否则会返回 null。

findIndex() 和 findLastIndex() 方法与前两个方法逻辑一致,只不过查找返回的是索引。

js
var index = arrs.findIndex((row) => row.color == "红黑");
console.log(index); // 1

var index2 = arrs.findLastIndex((row) => row.color == "红黑");
console.log(index2); // 2

var index3 = arrs.findIndex((row) => row.color == "红白");
console.log(index3); // -1

如上代码,如果查找到元素,就会返回匹配元素的索引,否则返回 -1。

数组过滤

数组过滤是指从数组中筛选出我们想要的元素并返回新数组。下面介绍常用到的数组过滤方法:

  • filter()
  • slice()

filter() 方法是按照条件筛选数组,筛选出的数组长度小于等于原数组。

js
var generals = [
  { id: 1, name: "吕布" },
  { id: 2, name: "关羽" },
  { id: 3, name: "马超" },
  { id: 4, name: "邢道荣" },
];

var flarr = generals.filter((row) => row.id >= 3);
console.log(flarr);
// [ { id: 3, name: '马超' }, { id: 4, name: '邢道荣' } ]

slice() 方法同样是过滤数组,只不过它的过滤方式并不是依据条件,而是依据下标。它有两个参数分别指定开始下标和结束下标,区间规则是左闭右开(包含左边不包含右边)。

js
var flarr = generals.slice(1, 3);
console.log(flarr);
// [{ id: 2, name: '关羽' }, { id: 3, name: '马超' }]

我们常常需要判断一个元素是否在数组之中,传统的方法是用 indexOf 方法来获取索引位置,如果大于 -1 则表示存在,否则不存在。

ES6 提供了更快捷的方式 —— includes() 方法,可以更简单直观的判断包含关系。它的第二个参数表示从数组的哪个位置开始判断,代码如下:

js
var arrs = ["张环", "李朗", "杨方", "任阔"];
arrs.includes("张环"); // true
arrs.includes("魔灵"); // false

arrs.includes("李朗"); // true
arrs.includes("李朗", 2); // false
// 等同于
arrs.slice(2).includes("李朗"); // false

数组遍历

数组遍历,即按照元素顺序依次执行函数。JavaScript 原始的遍历方式是 for 循环,ES6 为数组新增了有遍历功能的便捷函数,主要包括两个:

  • forEach()
  • map()

两个函数都能实现遍历,区别是 forEach() 函数单纯的执行遍历,无返回;map() 函数可以在回调函数内 return 一个值,函数执行最外层返回一个新数组。

js
var arrs = [1, 2, 3, 4, 5];
arrs.forEach((n) => {
  console.log(n); // 分别打印出 1,2,3,4,5
});

let res = arrs.map((n) => {
  return n * 2;
});
console.log(res); // [2,4,6,8,10]

数组转换

数组转换表示将原数组根据需要转换成另一种格式,一般是指修改数组的组织方式。数组转换包括以下方法:

  • from()
  • flat()
  • sort()

from() 方法的作用是将类数组转换为数组。什么是类数组?拥有数组的特性(包括数字下标和 length 属性)就算是类数组,我们用对象来模拟一下:

js
var like_arr = {
  0: "a",
  1: "b",
  length: 2,
};
var arr = Array.from(like_arr);
// arr:[a,b]

from() 更常用的还是将 Set 转换为数组,从而实现数组去重:

js
var arr = [1, 2, 3, 2, 1];
var set = new Set(arr);
Array.from(set); // [1,2,3]

flat() 方法是数组扁平化的快捷方法,常常在面试中见到。举例如下:

js
var arr = ["a", "b", ["c", "d", ["e"]]];
arr.flat(); // ['a', 'b', 'c', 'd', ['e']]
arr.flat(2); // ['a', 'b', 'c', 'd', 'e']

如上代码,flat() 方法将多层嵌套数组合并,且默认只会合并一层。如果需要合并多层,则需要显式传参,比如代码中参数为 2 表示合并两层。如果要合并所有层则参数为 Infinity 关键字。

sort() 方法用于排序,也是比较常用的功能。下面看一个首字母排序的例子:

js
var arrs = ["萧炎", "美杜莎", "云韵", "海波东"];
arrs.sort((row1, row2) => {
  return row1.localeCompare(row2) ? 1 : -1;
});

代码如上,数组元素 row1 和 row2 两两比较,当返回 1 时向后排,返回 -1 向前排。

3.1.4 函数的扩展

函数是 JavaScript 的一等公民,也是代码运行时,函数的重要性不言而喻。函数的扩展主要表现在格式,上下文,参数。

ES6 提供了函数的最新格式 —— 箭头函数,使函数的书写更简洁:

js
// ES5 写法
function getName(name) {
  return name;
}

// 箭头函数写法
const getName = (name) => name;

可以看到,箭头函数去掉 function 关键字,当函数体只有返回值时可简写。

箭头函数除了在语法上简化,最大的不同在于上下文的改变。上下文就是 this 的指向,我们看有什么变化:

js
var obj = {
  fun1() {
    console.log("fun1:", this);
  },
  fun2: () => {
    console.log("fun2:", this);
  },
};
obj.fun1(); // { fun1: xx, fun2: xx}
obj.fun2(); // Window

如上代码,将两个函数放到 obj 对象下,它们的 this 指向不同。前者指向 obj 对象,后者指向 Window 对象。

这是因为,普通函数的 this 指向规则是:谁调用函数,this 就指向谁;而箭头函数的 this 指向与谁调用无关,它永远指向父作用域的 this。

通过 obj.fun1() 调用方法,调用者是 obj,因此 fun1 函数的 this 指向 obj;fun2 函数的父作用域就是全局作用域,因此函数内的 this 指向 Window。

ES6 之前,函数的参数不能指定默认值,但是 ES6 支持了这个功能:

js
function eat(food = "苹果") {
  console.log(food);
}
eat(); // 苹果
eat("香蕉"); // 香蕉

函数参数不仅可以指定默认值,还支持 rest 参数。rest 参数与前面的扩展运算符基本一致。如果一个函数的参数是动态的,数量不固定,那么使用 rest 参数可以很方便的取到剩余参数。

js
const myLog = (tag, ...args) => {
  console.log(`${tag}:`, args);
};
myLog("水果", "火龙果"); // 水果:['火龙果']
myLog("零食", "坚果", "芒果干", "辣条"); // 零食:['坚果', '芒果干', '辣条']

代码如上,使用(...)声明 rest 参数,将剩余参数放在一个数组中。

3.1.5 异步编程方案

Promise 是一种广泛应用的现代异步方案,它比传统的回调函数更简洁。下面用代码演示 Promise 如何使用。

js
const promise = new Promise((resolve, reject) => {
  Request({
    url: "http://xxx",
    onSuccess(data) {
      resolve(data);
    },
    onError(err) {
      reject(err);
    },
  });
});

上面代码中,用 Promise 构造函数包裹了一个异步请求方法。当请求成功时执行 resolve() 方法,请求失败时执行 reject() 方法。

使用 Promise 实例时,根据异步任务的执行结果会触发不同的方法,如下:

  • then():Promise 内部的 resolve() 执行时触发。
  • catch():Promise 内部的 reject() 执行时触发。
  • finally():异步任务完成即触发,无论成败。
js
// 使用 Promise
promise
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.log(err);
  })
  .finally(() => {
    console.log("完成");
  });

这是 Promise 最基本的用法,在前端请求接口时常常能见到。

除此之外,你还可以让一组 Promise 并行请求,主要用到两个方法:

  • Promise.all():全部请求完成触发 then()。
  • Promise.race():最快的一个请求完成触发 then()。

Promise 并行请求代码如下:

js
var promise1, promise2 = new Promise(...)

Promise.all([
  promise1, promise2
]).then([res1, res2]=> {
  console.log(res1, res2)
})

Promise.race([
  promise1, promise2
]).then(res => {
  console.log(res)
})

Promise 方案的升级版是 async/await,它们是 Promise 的语法糖,但写起来是完全同步的感觉。

js
const getRes = async () => {
  try {
    let res = await fetch("http://xxxxx.json");
    console.log(res);
  } catch (error) {
    console.log(error);
  }
};

上述代码中,fetch 方法是一个 Promise,加了 async/await 关键字之后就可以像同步代码一样书写。返回值 res 就是 then() 方法的返回值,异常时会被 catch 捕获到。

因此,使用 async/await 替代 Promise 时,未必要包裹一个 try...catch。

3.1.6 Class 语法

ES5 通过构造函数来声明一个类,这种方式并不语义化。ES6 引入了一种简单直观的声明方式,语法如下:

js
class Book {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

上面代码用 class 关键字声明一个类,类内部声明 constructor 方法作为类实例化的调用函数。不过 ES6 的 class 只是构造函数的语法糖,转换为 ES5 是这样:

js
function Book(name) {
  this.name = name;
}
Book.prototype.getName = function () {
  return this.name;
};

使用类和实例化构造函数完全一样,代码如下:

js
var book = new Book("三国演义");
book.getName(); // 三国演义
var book = new Book("西游记");
book.getName(); // 西游记

类还可以定义静态方法。静态的意思是不需类实例化就可以直接使用,代码如下:

js
class Dog {
  static getName() {
    console.log("狗");
  }
}
Dog.getName(); // 狗

如上代码,静态方法用关键字 static 标识。静态方法因为不需要实例化,因而方法内部的 this 指向会有不同。普通方法内的 this 指向实例,静态方法内的 this 指向类本身。

js
class Cat {
  static getName() {
    console.log(this); // 指向 Cat 类
  }
  getName() {
    console.log(this); // 指向 cat 实例
  }
}

Cat.getName();
var cat = new Cat();
cat.getName();

值得一提的是,类通常会有私有属性的需求,表示这个属性只能在内部使用。而 ES6 并没有提供这个功能,于是开发者约定以 _ 开头的命名表示私有属性。

ES2022 正式为 class 添加了私有属性,在属性名之前加一个 # 符合即可。

js
class Dragon {
  _weight = "100吨"; // 约定写法
  #color = "黑色"; // 正式写法
}

私有方法与私有属性同理。

3.1.7 模块体系

早期的 JavaScript 并没有模块化的功能,代码难以分块隔离,更不能实现导入导出。最早大规模引入模块系统的是 Node.js,其代码如下:

js
const path = require("path");

var json = {
  path: path.resolve(__dirname),
};

module.export = json;

上面代码中,开头用 require 导入一个模块,结尾用 module.export 导出一个模块。这样的模块之间相互隔离,导入和导出支持了模块间的复用。这套模块方案称为 CommonJS

ES6 并没有沿用 CommonJS,而是创造了自己的模块方案 —— ESModule(简称 ESM),实现方式如下:

js
import util from "./util.js";

var json = {
  path: util.getPath(),
};

export default json;

从代码来看,ESModule 与 CommonJS 并无二致,只是关键字不一样了。实际上 ESModule 还有许多功能,比如导出模块的变量:

js
// a.js
export const name = "大闸蟹";
export const getAttr = () => {
  return name;
};

// b.js
import { name, getAttr } from "./a.js";
console.log(name); // 大闸蟹
console.log(getAttr()); // 大闸蟹

上述代码中,并没有直接导出模块,而是导出模块内的变量。使用时可以直接导入这些变量。

代码中的 a.js 还可以换一种写法实现完全相同的效果,而且 b.js 也可以为变量指定别名。代码如下:

js
// a.js
const name = "大闸蟹";
const getAttr = () => {
  return name;
};
export default { name, getAttr };

// b.js
import { name as my_name, getAttr as myFun } from "./a.js";
console.log(my_name); // 大闸蟹
console.log(myFun()); // 大闸蟹

ESM 已经成为 JavaScript 模块化主流方案,会逐渐取代 CommonJS,成为浏览器和服务器通用的模块解决方案。

上述的所有 import 关键字,都会在编译时确定模块的依赖关系,因此 import 模块导入必须放在顶层。

实际场景中,有时我们希望可以根据条件动态导入模块,比如点击按钮时动态导入一个 JSON 文件,此时 import 关键字就办不到了。

为了解决这个问题,ES2020 引入了 import() 函数,支持动态加载模块。

js
if (true) {
  import("./xx.json").then((json) => {
    console.log(json);
  });
}

后续在学习框架路由的时候,路由组件就是用 import() 函数动态导入的。