• 周四. 12月 1st, 2022

5G编程聚合网

5G时代下一个聚合的编程学习网

热门标签

ES6 入门教程(未完)

admin

11月 28, 2021

https://es6.ruanyifeng.com/

24.Module 的语法

  • CommonJS 模块就是对象,输入时必须查找对象属性(注释:require加载时实际把一个文件作为一个对象完整加载,然后再获取对象上面各种属性)

  • ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入(注释:所以可以实现树摇)

    • ES6 模块是编译时加载,使得静态分析成为可能
  • ES6 的模块自动采用严格模式,不管你有没有在模块头部加上”use strict”;严格模式主要有以下限制。

    • 变量必须声明后再使用
    • 函数的参数不能有同名属性,否则报错
    • 不能使用with语句
    • 不能对只读属性赋值,否则报错
    • 不能使用前缀 0 表示八进制数,否则报错
    • 不能删除不可删除的属性,否则报错
    • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
    • eval不会在它的外层作用域引入变量
    • eval和arguments不能被重新赋值
    • arguments不会自动反映函数参数的变化(疑问:引用类型内部改变,arguments内的对应值不会改变?)
    • 不能使用arguments.callee(注释:包含当前正在执行的函数)
    • 不能使用arguments.caller(注释:当前正在执行函数,已失效)
    • 禁止this指向全局对象(注释:顶层this指向undefined)
    • 不能使用fn.caller(注释:返回调用指定函数的函数,非标准)和fn.arguments获取函数调用的堆栈
    • 增加了保留字(比如protected(注释:受保护的)、static(注释:静态的)和interface(注释:私有的)
  • export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。、

// 报错
export 1;

// 报错
var m = 1;
export m;

// 应当在接口名与模块内部变量之间,建立了一一对应的关系
// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};
  • export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新
  • export命令处于块级作用域内,就会报错,因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
  • import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
  • 建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。(注释:引用类型可以改变内部属性,但是会导致很难查错)
  • import命令具有提升效果,会提升到整个模块的头部,首先执行。
    • 这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。
    • 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
    • 通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。
  • 如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。
  • 模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}

// other.js
import * as circle from './circle';

// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};
  • 本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字
  • 正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。
// 错误
export default var a = 1;
  • 写成一行以后,foo和bar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo和bar。
export { foo, bar } from 'my_module';
  • 默认接口的写法如下。
export { default } from 'foo';
  • 具名接口改为默认接口的写法如下。
export { es6 as default } from './someModule';
  • ES2020补(注释:全部接口改为具名接口)
export * as ns from "mod";

// 等同于
import * as ns from "mod";
export {ns};
  • 模块之间也可以继承
  • export * 命令会忽略circle模块的default方法
export * from 'circle';
  • require是运行时加载模块
  • import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。
  • import()允许模块路径动态生成。
  • import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。
    • 如果模块有default输出接口,可以用参数直接获得

25.Module 的加载实现

  • 浏览器允许脚本异步加载,下面就是两种异步加载的语法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
  • defer与async的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
  • 浏览器加载 ES6 模块,也使用<script>标签,但是要加入type=”module”属性。
  • 浏览器对于带有type=”module”的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。
  • 如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。
  • 利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。(注释:在模块中,全局变量并非绑定在window上)
import utils from 'https://example.com/js/utils.js';

const x = 1;

console.log(x === window.x); //false
console.log(this === undefined); // true
  • JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
  • ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo); // bar
setTimeout(() => console.log(foo), 500); // baz
  • 由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。
  • CommonJS 模块使用require()和module.exports
  • 从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
  • Node.js 要求 ES6 模块采用.mjs后缀文件名。
  • Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定”use strict”。
  • 如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。
{
   "type": "module"
}
  • 如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。
  • package.json文件有两个字段可以指定模块的入口文件:main和exports
  • exports字段的优先级高于main字段
    • exports字段可以指定脚本或子目录的别名
    • 如果没有指定别名,就不能用“模块+脚本名”这种形式加载脚本。
  • exports字段的别名如果是.,就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段的值。
{
  "exports": {
    ".": "./main.js"
  }
}

// 等同于
{
  "exports": "./main.js"
}
  • 利用.这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。目前,这个功能需要在 Node.js 运行的时候,打开–experimental-conditional-exports标志。
    • 别名.的require条件指定require()命令的入口文件(即 CommonJS 的入口),default条件指定其他情况的入口(即 ES6 的入口)。
{
  "type": "module",
  "exports": {
    ".": {
      "require": "./main.cjs",
      "default": "./main.js"
    }
  }
}
  • 上面的写法可以简写如下
    • 如果同时还有其他别名,就不能采用简写,否则或报错。
{
  "exports": {
    "require": "./main.cjs",
    "default": "./main.js"
  }
}
  • CommonJS 的require()命令不能加载 ES6 模块,会报错,只能使用import()这个方法加载。
(async () => {
  await import('./my-app.mjs');
})();
  • ES6 模块的import命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。
// 正确
import packageMain from 'commonjs-package';

// 报错
import { method } from 'commonjs-package';

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注