JavaScript、Nodejs、TypeScripy 导包:module、export、import、require

CommonJS规范:https://javascript.ruanyifeng.com/nodejs/module.html

浅谈 JavaScript、ES5、ES6:https://www.cnblogs.com/lovesong/p/4908871.html

1、为什么有模块概念

理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。但是,Javascript 不是一种模块化编程语言,在 es6 以前,它是不支持 ""(class),所以也就没有 "模块"(module)了。

 Javascript 社区做了很多努力,在现有的运行环境中,实现 "模块" 的效果。

原始写法

模块就是实现特定功能的一组方法。只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。

function m1(){
  //...
}
function m2(){
  //...  
}

上面的函数 m1() 和 m2(),组成一个模块。使用的时候,直接调用就行了。

这种做法的缺点很明显:"污染" 了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。

对象写法

为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面

var module1 = new Object({
    _count: 0,
    m1: function () {
        //...
    },
    m2: function () {
        //...
    }
});

上面的函数 m1()m2(),都封装在 module1 对象里。使用的时候,就是调用这个对象的属性:module1.m1();

这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值:module1._ count = 1;

立即执行函数写法

使用 "立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的

var module = (function () {
    var _count = 0;
    var m1 = function () {
        alert(_count)
    }
    var m2 = function () {
        alert(_count + 1)
    }

    return {
        m1: m1,
        m2: m2
    }
})()

使用上面的写法,外部代码无法读取内部的 _count 变量:console.info(module._count);  // undefined

基于以上,就出现了 "模块系统",来实现 "模块的 导入、导出" 功能

CommonJS 模块系统

  • 使用require()函数进行模块引入,使用module.exportsexports进行模块导出。
  • 模块加载是同步的,当遇到require()时,会立即加载并执行所需的模块。
  • 模块路径可以是相对路径或绝对路径。
  • 可以在任何地方动态引入模块,无需在顶层使用。
  • 适用于服务器端开发和旧版浏览器环境。

Node.js 默认 CommonJS

在 Node.js 中,默认使用的是 CommonJS 模块系统,语法为module.exports和exports。

但是正在 Node.js 版本中(12.20.0及以上),可以使用 "type": "module" 配置启用 ES模块系统。

  • 注意:由于语法和特性上的差异,CommonJS模块和ES模块之间并不完全兼容,所以在使用时需要注意相应的规范和限制。
  • 总结:CommonJS适用于服务器端开发和旧版浏览器环境,而ES模块则适用于现代浏览器环境和某些模块打包工具。

nodejs 启用 ES 模块系统 后,一些 CommonJS 的特性在Node.js中将不再可用,例如,require()函数和 module.exports 语法无法直接使用。你需要更新你的代码以使用ES模块的语法。要在Node.js中使用ES模块系统(ESM),可以按照以下步骤进行操作:

  • 确保 Node.js 版本在12.20.0及以上。ESM 在 Node.js 12.20.0版本中开始支持。
  • 在项目根目录下创建一个 package.json 文件,并添加以下内容并保存:

    {
      "type": "module"
    }

  • 现在就可以使用 ES模块的语法和特性来编写代码。例如:可以使用 import 和 export 关键字进行模块引入和导出。
    import { foo } from './module.js';
    console.log(foo);

  • 运行ES模块脚本。命令:node script.mjs

启用 ES模块系统后,模块的文件扩展名必须是 .mjs,否则会被视为CommonJS模块。如果仍然希望使用 .js 作为文件扩展名,可以在文件中的 ES模块代码之前使用 .js 作为文件类型指示符,例如:

// script.js
import { foo } from './module.js';

console.log(foo);

注意,使用 ESM 可能需要一些注意事项:

  • ES 模块中的顶级importexport语句只能在模块的顶层使用,不能在条件语句或函数内部使用。
  • 文件路径必须包含文件扩展名,并且相对路径必须以./../开头。
  • Node.js的全局对象不再被默认注入到每个模块中,你需要显式引用它们。例如,使用import process from 'process'引入process对象。

通过上述步骤,就可以在Node.js中成功使用ES模块系统。记得在文件名、package.json配置和语法上进行相应的调整。

nodejs 导入包的 2 种 方式

  • 使用 require 函数:const package = require('package-name');
  • 使用 ES 模块的 import 语句:import * as package from 'package-name';

使用 ES 模块的 import 语句导入第三方包时,要求文件的扩展名为.mjs,或者在文件所在目录的package.json中设置"type": "module"。此外,使用 import 语句导入的模块是异步加载的,而不是像 require 函数一样是同步加载的。

无论是使用 require 函数还是 import 语句,导入的第三方包需要先通过 Node.js 的包管理工具(如npm或yarn)进行安装,然后才能在代码中导入和使用。例如,通过以下命令安装 package-name包:npm install package-name

ES 模块系统

"CommonJS模块系统" 和 "ES模块系统" 都是用于在 JavaScript 中组织和管理代码的模块系统,但它们在语法和一些特性上存在一些差异。

  • 使用import语句进行模块引入,使用export关键字进行模块导出。
  • 模块加载是异步的,在解析阶段只会创建一个模块的引用,真正需要使用时才会加载执行。
  • 默认情况下,模块路径是相对路径,但也可以使用绝对路径。
  • 模块引入必须在模块的顶层进行,不能放在条件语句或循环中。
  • 适用于现代浏览器环境(支持ES6模块)和某些模块打包工具(如Webpack、Rollup等)。

2、require 详解

Nodejs 中提供了 exports require 两个对象,

  • exports 模块公开的接口
  • require 用于 从外部获取一个模块的接口,即 获取模块的 exports 对象

exports 可以理解为 module.exports 的一个 快捷方式。

模块的加载机制

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个例子。

下面是一个模块文件lib.js

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代码输出内部变量counter和改写这个变量的内部方法incCounter

然后,加载上面的模块。

// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

上面代码说明,counter输出以后,lib.js模块内部的变化就影响不到counter了。

require 的内部处理流程

require命令是CommonJS规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的module.require命令,而后者又调用Node的内部命令Module._load

Module._load = function(request, parent, isMain) {
  // 1. 检查 Module._cache,是否缓存之中有指定模块
  // 2. 如果缓存之中没有,就创建一个新的Module实例
  // 3. 将它保存到缓存
  // 4. 使用 module.load() 加载指定的模块文件,
  //    读取文件内容之后,使用 module.compile() 执行文件代码
  // 5. 如果加载/解析过程报错,就从缓存删除该模块
  // 6. 返回该模块的 module.exports
};

上面的第4步,采用module.compile()执行指定模块的脚本,逻辑如下。

Module.prototype._compile = function(content, filename) {
  // 1. 生成一个require函数,指向module.require
  // 2. 加载其他辅助方法到require
  // 3. 将文件内容放到一个函数之中,该函数可调用 require
  // 4. 执行该函数
};

上面的第1步和第2步,require函数及其辅助方法主要如下。

  • require(): 加载外部模块
  • require.resolve():将模块名解析到一个绝对路径
  • require.main:指向主模块
  • require.cache:指向所有缓存的模块
  • require.extensions:根据文件的后缀名,调用不同的执行函数

一旦require函数准备完毕,整个所要加载的脚本内容,就被放到一个新的函数之中,这样可以避免污染全局环境。该函数的参数包括requiremoduleexports,以及其他一些参数。

(function (exports, require, module, __filename, __dirname) {
  // YOUR CODE INJECTED HERE!
});

Module._compile方法是同步执行的,所以Module._load要等它执行完成,才会向用户返回module.exports的值。

加载规则

require 命令用于加载文件,后缀名默认为.js。

var foo = require('foo');
//  等同于
var foo = require('foo.js');

根据参数的不同格式,require命令去不同路径寻找模块文件。

  • (1)如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js
  • (2)如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js
  • (3)如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
    示例:脚本 /home/user/projects/foo.js 执行 require('bar.js') 后,Node 会依次搜索以下文件。
        /usr/local/lib/node/bar.js
        /home/user/projects/node_modules/bar.js
        /home/user/node_modules/bar.js
        /home/node_modules/bar.js
        /node_modules/bar.js
        这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。
  • (4)如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径。
  • (5)如果指定的模块文件没有发现,Node会尝试为文件名添加.js.json.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
  • (6)如果想得到require命令加载的确切文件名,使用require.resolve()方法。

从文件加载

当文件模块缓存中不存在,而且不是原生模块的时候,Node.js 会解析 require 方法传入的参数,并从文件系统中加载实际的文件,加载过程中的包装和编译细节在前一节中已经介绍过,这里我们将详细描述查找文件模块的过程,其中,也有一些细节值得知晓。

require方法接受以下几种参数的传递:

  • http、fs、path等,原生模块。
  • ./mod或../mod,相对路径的文件模块。
  • /pathtomodule/mod,绝对路径的文件模块。
  • mod,非原生模块的文件模块。

目录的加载规则

通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。

在目录中放置一个package.json文件,并且将入口文件写入main字段。下面是一个例子。

// package.json
{ "name" : "some-library", "main" : "./lib/some-library.js" }

require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。

模块的缓存

第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。

require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"

上面代码中,连续三次使用require命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个message属性。但是第三次加载的时候,这个message属性依然存在,这就证明require命令并没有重新加载模块文件,而是输出了缓存。

如果想要多次执行某个模块,可以让该模块输出一个函数,然后每次require这个模块的时候,重新执行一下输出的函数。

所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写。

// 删除指定模块的缓存
delete require.cache[moduleName];

// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})

注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。

模块的循环加载

如果发生模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本。

// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';

// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';

// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);

上面代码是三个JavaScript文件。其中,a.js加载了b.js,而b.js又加载a.js。这时,Node返回a.js的不完整版本,所以执行结果如下。

$ node main.js
b.js  a1
a.js  b2
main.js  a2
main.js  b2

修改main.js,再次加载a.js和b.js。

// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);

执行上面代码,结果如下。

$ node main.js
b.js  a1
a.js  b2
main.js  a2
main.js  b2
main.js  a2
main.js  b2

上面代码中,第二次加载a.js和b.js时,会直接从缓存读取exports属性,所以a.js和b.js内部的console.log语句都不会执行了。

require.main

require方法有一个main属性,可以用来判断模块是直接执行,还是被调用执行。

直接执行的时候(node module.js),require.main属性指向模块本身。

if(require.main === module){
    console.log('本模块执行');
}

调用执行的时候(通过require加载该脚本执行),上面的表达式返回false。

3、主流模块规范

在 es6 以前,还没有提出一套官方的规范,从社区和框架推广程度而言,目前通行的 javascript 模块规范有两种:CommonJS 和 AMD

CommonJS 规范

CommonJS规范:https://javascript.ruanyifeng.com/nodejs/module.html

2009年美国程序员 Ryan Dahl 创造了node.js 项目,将 javascript 语言用于服务器端编程。这标志 "Javascript模块化编程" 正式诞生。前端的复杂程度有限,没有模块也是可以的,但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

node 编程中最重要的思想之一就是模块,而正是这个思想,让JavaScript 的大规模工程成为可能。模块化编程在js界流行,也是基于此,随后在浏览器端,requirejs 和 seajs 之类的工具包也出现了,可以说在对应规范下,require 统治了ES6之前的所有模块化编程,即使现在,在 ES6 module 被完全实现之前,还是这样。

一个 js 文件就是一个模块

Node 应用由模块组成,采用 CommonJS 模块规范。

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

// example.js
var x = 5;
var addX = function (value) {
    return value + x;
};
上面代码中,变量x和函数addX,是当前文件example.js私有的,其他文件不可见。
如果想在多个文件分享变量,必须定义为global对象的属性。
global.warning = true;
上面代码的warning变量,可以被所有文件读取。当然,这样写法是不推荐的。

CommonJS 规范规定:每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口加载某个模块,其实是加载该模块的module.exports属性。

下面的代码通过 module.exports 输出变量x和函数addX。

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

下面的代码,通过 require方法用于加载模块。

var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6

require 方法的详细解释参见《Require命令》一节。

CommonJS 模块的特点如下。

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

module 对象

Node 内部提供一个 Module 构建函数。所有模块都是 Module的实例。

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  // ...

每个模块内部,都有一个module对象,代表当前模块。它有以下属性。

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 表示模块对外输出的值。

下面是一个示例文件,最后一行输出module变量。

// example.js
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);

执行这个文件,命令行会输出如下信息。

{ id: '.',
  exports: { '$': [Function] },
  parent: null,
  filename: '/path/to/example.js',
  loaded: false,
  children:
   [ { id: '/path/to/node_modules/jquery/dist/jquery.js',
       exports: [Function],
       parent: [Circular],
       filename: '/path/to/node_modules/jquery/dist/jquery.js',
       loaded: true,
       children: [],
       paths: [Object] } ],
  paths:
   [ '/home/user/deleted/node_modules',
     '/home/user/node_modules',
     '/home/node_modules',
     '/node_modules' ]
}

如果在命令行下调用某个模块,比如node something.js,那么module.parent就是null。如果是在脚本之中调用,比如require('./something.js'),那么module.parent就是调用它的模块。利用这一点,可以判断当前模块是否为入口脚本。

if (!module.parent) {
    // ran with `node something.js`
    app.listen(8088, function() {
        console.log('app listening on port 8088');
    })
} else {
    // used with `require('/.something.js')`
    module.exports = app;
}

module.exportsexports 

module.exports 属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports 变量。

var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();

setTimeout(function() {
  module.exports.emit('ready');
}, 1000);

上面模块会在加载后1秒后,发出ready事件。其他文件监听该事件,可以写成下面这样。

var a = require('./a');
a.on('ready', function() {
  console.log('module a is ready');
});

示例:

// module.js
function sayHello() {
  console.log("Hello!");
}

module.exports = {
  sayHello: sayHello
};

exports 变量:为了方便,Node为每个模块提供一个exports变量,指向 module.exports。这等同在每个模块头部,有一行这样的命令:var exports = module.exports; 这样造成的结果是,在对外输出模块接口时,可以向 exports 对象添加方法。

exports.sayHello = function() {
  console.log("Hello!");
};

exports.area = function (r) {
  return Math.PI * r * r;
};

exports.circumference = function (r) {
  return 2 * Math.PI * r;
};

注意:不能直接将 exports 变量指向一个值,因为这样等于切断了exports与module.exports 的联系。

// 错误示例
exports = {
  sayHello: function() {
    console.log("Hello!");
  }
};

// 下面的写法是无效的,因为 exports 不再指向 module.exports了。

        exports = function(x) {console.log(x)};
// 下面的写法也是无效的。
        exports.hello = function() {
          return 'hello';
        };
        module.exports = 'Hello world';
上面代码中,hello 函数是无法对外输出的,因为 module.exports 被重新赋值了。
这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用 exports 输出,只能使用 module.exports 输出。
module.exports = function (x){ console.log(x);};
如果觉得 exports 与 module.exports 之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。

使用 require 导入 模块

Node 使用 CommonJS 模块规范,内置的全局方法 require 命令用于加载模块文件。
require 命令的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。

// example.js
var invisible = function () {
  console.log("invisible");
}
exports.message = "hi";
exports.say = function () {
  console.log(message);
}

运行下面的命令,可以输出exports对象。
var example = require('./example.js');
example
// {
//   message: "hi",
//   say: [Function]
// }

如果模块输出的是一个函数,那就不能定义在exports对象上面,而要定义在 module.exports变量上面。
module.exports = function () {
  console.log("hello world")
}

require('./example2.js')()
上面代码中,require命令调用自身,等于是执行module.exports,因此会输出 hello world。

正是由于 CommonJS 使用的 require 方式的推动,才有了后面的 AMD、CMD 也采用的 require 方式来引用模块的风格

export 和 export default

 在 ECMAScript 6 中又新增了语法 export 和 export default:

export default function () { }
export const site = 'https://tasaid.com'
export const name = 'linkFly'

到这里画风还比较正常,而大名鼎鼎的 JavaScript 转码编译器 babel 针对 ECMAScript 6 新增的 export default 语法,搞了个 babel-plugin-transform-es2015-modules-commonjs 的转换插件,用于将 ECMAScript 6 转码为 CommonJs 规范的语法:

源码:export default 42;

编译后:

Object.defineProperty(exports, "__esModule", {
  value: true
});

exports.default = 42;

到这里,我们看到有三种 export 默认值的语法:

module.exports = function () {}    // commonjs
exports.default = function () {}   // babel 转码
export default function () {}      // es6

TypeScript 导出、导入

关于 TypeScript 中不同 import 的含义,最典型的就是下面的 import 语法:

import * as xx from 'xx'   // commonjs 模块
import xx from 'xx'        // es6 模块
import xx = require('xx')  // commonjs 模块,类型声明为 export = xx
const xx = require('xx')   // 没有类型声明,默认导入 any 类型

import xx = require('xx') 是用来导入 commonjs 模块的库,特殊的地方在于这个库的类型声明是 export = xxx 这种方式导出的:

语法

import defaultExport from "module-name";
import * as name from "module-name";
import { export } from "module-name";
import { export as alias } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";

var promise = import("module-name"); //这是一个处于第三阶段的提案。
  • defaultExport    导入模块的默认导出接口的引用名。
  • module-name    要导入的模块。通常是包含目标模块的.js文件的相对或绝对路径名,可以不包括.js扩展名。某些特定的打包工具可能允许或需要使用扩展或依赖文件,它会检查比对你的运行环境。只允许单引号和双引号的字符串。
  • name    导入模块对象整体的别名,在引用导入模块时,它将作为一个命名空间来使用。
  • export, exportN    被导入模块的导出接口的名称。
  • alias, aliasN    将引用指定的导入的名称。

导入整个模块的内容

import * as myModule from '/modules/my-module.js';

myModule.doAllTheAmazingThings();

导入单个接口

import {myExport} from 'xxx.js';

导入多个接口

import {foo, bar} from 'xxx.js';

例如:
// module.js
const firstName = 'John';
const lastName = 'Doe';
export { firstName, lastName };

// main.js
import { firstName, lastName } from './module.js';
console.log(firstName); // 输出: John
console.log(lastName); // 输出: Doe

导入带有别名的接口 ( import {x as y} from 'zzz.js'; )

import {reallyLongModuleExportName as shortName} from '/modules/my-module.js';

export as 通过export关键字进行重命名导出时,使用as关键字指定新的名称

例如:
// module.js
const name = 'module';
export { name as moduleName };

// main.js
import { moduleName } from './module.js';
console.log(moduleName); // 输出: module

export all 通过export *关键字导出所有模块时,可以将一个模块中的所有导出对象都导入到当前模块。

例如:
// module.js
export const firstName = 'John';
export const lastName = 'Doe';

// main.js
import * as module from './module.js';
console.log(module.firstName); // 输出: John
console.log(module.lastName); // 输出: Doe

导入时重命名多个接口

使用别名导入模块的多个接口。

import {
  reallyReallyLongModuleMemberName as shortName,
  anotherLongModuleName as short
} from '/modules/my-module.js';

只运行全局代码,而不导入任何值:import 'xxx.js';

不导入模块中的任何内容(接口)。 这将运行模块中的全局代码, 但实际上不导入任何值。

import '/modules/my-module.js';

export default

通过 export default 导出模块时,可以直接指定文件或模块的默认导出值。
例如:

// module.js
        const name = 'module';
        export default name;

// main.js
        import moduleName from './module.js';
        console.log(moduleName); // 输出: module

引入模块可能有一个 default export(无论它是对象,函数,类等)可用。然后可以使用 import 语句来导入这样的默认接口。

最简单的用法是直接导入默认值:import myDefault from '/modules/my-module.js';

default、命名空间导入、命名导入 联合使用

也可以同时将 default 语法与上述用法(命名空间导入或命名导入)一起使用。在这种情况下,default 导入必须首先声明。 例如:

        import myDefault, * as myModule from '/modules/my-module.js';
        // myModule used as a namespace
或者
        import myDefault, {foo, bar} from '/modules/my-module.js';
        // specific, named imports

当用动态导入的方式导入默认导出时,其工作方式有所不同。你需要从返回的对象中解构并重命名 "default" 键。

(async () => {
    if (somethingIsTrue) {
        const {default: myDefault, foo, bar} = await import('/modules/my-module.js');
    }
})();

nodejs 示例:

const module_parse = require("@babel/parser")
const module_generator = require("@babel/generator").default
const module_fs = require('fs')

const js_code = `
const a = 3;
let string = "hello";
for (let i = 0; i < a; i++) {
    string += "world";
}
console.log("string", string);`;

// const js_code = module_fs.readFileSync("code1.js","utf-8");
let ast = module_parse.parse(js_code);

// 返回值中的 code 属性赋值给变量 output。
const { code: output } = module_generator(ast);
console.log(output);

动态 import

标准用法的 import 导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。下面的是你可能会需要动态导入的场景:

  • 当静态导入的模块很明显的降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它。
  • 当静态导入的模块很明显的占用了大量系统内存且被使用的可能性很低。
  • 当被导入的模块,在加载时并不存在,需要异步获取
  • 当导入模块的说明符,需要动态构建。(静态导入只能使用静态说明符)
  • 当被导入的模块有副作用(这里说的副作用,可以理解为模块中会直接运行的代码),这些副作用只有在触发了某些条件才被需要时。(原则上来说,模块不能有副作用,但是很多时候,你无法控制你所依赖的模块的内容)

请不要滥用动态导入(只有在必要情况下采用)。静态框架能更好的初始化依赖,而且更有利于静态分析工具和 tree shaking 发挥作用

关键字 import 可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise

import('/modules/my-module.js')
  .then((module) => {
    // Do something with the module.
  });

这种使用方式也支持 await 关键字。

let module = await import('/modules/my-module.js');

使用 @符号 进行导入

在JavaScript中,使用@符号进行导入时,通常是用于指定模块的命名空间或别名。

一种常见的情况是,在使用一些JavaScript框架或库时,它们可能会使用@符号作为包的前缀来表示该包的特定命名空间。

例如,import { func } from '@mylibrary/module'

  • @mylibrary 表示从名为 mylibrary 的包中导入module模块,并且@mylibrary是该包的命名空间。

另外一种情况是,在一些构建工具(例如Webpack)或模块解析器中,@符号可以用于定义模块的别名或路径映射。通过配置,我们可以将@符号映射到特定的文件夹路径,以简化长路径的书写。

例如,import module from '@/path/to/module'

  • @表示某个指定的目录,例如项目的根目录或者其他约定的路径。

需要注意的是,@符号的具体含义和使用方式取决于项目的配置和约定,并没有固定的规定。因此,在具体的应用场景中,可以查看相关文档或配置文件以了解@符号的使用方式和含义。

以上不同的导出方式可以组合使用,以满足不同场景下的需求。

示例:标准导入

下面的代码将会演示如何从辅助模块导入以协助处理 AJAX JSON 请求。

模块:file.js

function getJSON(url, callback) {
  let xhr = new XMLHttpRequest();
  xhr.onload = function () {
    callback(this.responseText)
  };
  xhr.open('GET', url, true);
  xhr.send();
}

export function getUsefulContents(url, callback) {
  getJSON(url, data => callback(JSON.parse(data)));
}

主程序:main.js

import { getUsefulContents } from '/modules/file.js';

getUsefulContents('http://www.example.com',
    data => { doSomethingUseful(data); });

示例:动态导入

此示例展示了如何基于用户操作去加载功能模块到页面上,在例子中通过点击按钮,然后会调用模块内的函数。当然这不是能实现这个功能的唯一方式,import()函数也可以支持await

const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
  link.addEventListener("click", e => {
    e.preventDefault();

    import('/modules/my-module.js')
      .then(module => {
        module.loadPageInto(main);
      })
      .catch(err => {
        main.textContent = err.message;
      });
  });
}

es6 的 import 引入模块

import 语法声明用于从已导出的模块、脚本中,导入函数、对象、指定文件(或模块)的原始值。

import 模块导入与 export 模块导出功能相对应,也存在两种模块导入方式:

  • 命名式导入(名称导入)
  • 默认导入(定义式导入)。

import 的语法跟 require 不同,而且 import 必须放在文件的最开始,且前面不允许有其他逻辑代码,这和其他所有编程语言风格一致。

import defaultMember from "module-name";
import * as name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1 , member2 } from "module-name";
import { member1 , member2 as alias2 , [...] } from "module-name";
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
import "module-name";
  • name-从将要导入模块中收到的导出值的名称
  • member, memberN-从导出模块,导入指定名称的多个成员
  • defaultMember-从导出模块,导入默认导出成员
  • alias, aliasN-别名,对指定导入成员进行的重命名
  • module-name-要导入的模块。是一个文件名
  • as-重命名导入成员名称(“标识符”)
  • from-从已经存在的模块、脚本文件等导入

命名式导入

我们可以通过指定名称,就是将这些成员插入到当作用域中。导出时,可以导入单个成员或多个成员:

注意,花括号里面的变量与 export 后面的变量一一对应

import {myMember} from "my-module";
import {foo, bar} from "my-module";

通过 * 符号,我们可以导入模块中的全部属性和方法。当导入模块全部导出内容时,就是将导出模块("my-module.js")所有的导出绑定内容,插入到当前模块(’myModule’)的作用域中:

import * as myModule from "my-module";

导入模块对象时,也可以使用as对导入成员重命名,以方便在当前模块内使用:

import {reallyReallyLongModuleMemberName as shortName} from "my-module";

导入多个成员时,同样可以使用别名:

import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";

导入一个模块,但不进行任何绑定:import "my-module";

默认导入

在模块导出时,可能会存在默认导出。同样的,在导入时可以使用 import 指令导出这些默认值。

直接导入默认值:import myDefault from "my-module";

也可以在命名空间导入和名称导入中,同时使用默认导入:

import myDefault, * as myModule from "my-module"; // myModule 做为命名空间使用
//或 
import myDefault, {foo, bar} from "my-module";    // 指定成员导入

import 使用示例

// --file.js--
function getJSON(url, callback) {
    let xhr = new XMLHttpRequest();
    xhr.onload = function () {
        callback(this.responseText)
    };
    xhr.open("GET", url, true);
    xhr.send();
}

export function getUsefulContents(url, callback) {
    getJSON(url, data => callback(JSON.parse(data)));
}

// --main.js--
import {getUsefulContents} from "file";

getUsefulContents("http://itbilu.com", data => {
    doSomethingUseful(data);
});

default 关键字

// d.js
export default function () {
}

// 等效于:
function a() {
};
export {a as default};

在 import 的时候,可以这样用:

import a from './d';

// 等效于,或者说就是下面这种写法的简写,是同一个意思
import {default as a} from './d';

这个语法糖的好处就是 import 的时候,可以省去花括号{}。

简单的说,如果 import 的时候,你发现某个变量没有花括号括起来(没有*号),那么你在脑海中应该把它还原成有花括号的 as 语法。

所以,下面这种写法你也应该理解了吧:import $,{each,map} from 'jquery';

import 后面第一个 $ 是 {defalut as $} 的替代写法。

as 关键字

as 简单的说就是取一个别名,export 中可以用,import 中其实可以用:

// a.js
var a = function () {
};
export {a as fun};

// b.js
import {fun as a} from './a';

a();

上面这段代码,export 的时候,对外提供的接口是 fun,它是 a.js 内部 a 这个函数的别名,但是在模块外面,认不到 a,只能认到 fun。

import 中的 as 就很简单,就是你在使用模块里面的方法的时候,给这个方法取一个别名,好在当前的文件里面使用。之所以是这样,是因为有的时候不同的两个模块可能通过相同的接口,比如有一个 c.js 也通过了 fun 这个接口:

// c.js
export function  fun() {};

如果在b.js中同时使用a和c这两个模块,就必须想办法解决接口重名的问题,as就解决了。

AMD 规范

CommonJS、AMD 区别

  • CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS规范比较适用服务器后端编程。
  • AMD 规范则是非同步加载模块,允许指定回调函数。因为浏览器环境是要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。

CommonJS 规范不适用于浏览器环境。下面代码如果在浏览器中运行,会有一个很大的问题

var math = require('math');
math.add( 2,3); // 5

第二行 math.add(2, 3),在第一行 require("math") 之后运行,因此必须等 math.js 加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。因此,浏览器端的模块,不能采用 "同步加载"(synchronous),只能采用 "异步加载"(asynchronous)。这就是AMD规范诞生的背景。

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

AMD 使用 define方法 定义模块

示例:

define(['package/lib'], function(lib){
  function foo(){
    lib.log('hello world!');
  }

  return {
    foo: foo
  };
});

用法:define(id?, dependencies?, factory)

  • id:字符串,模块名称 (可选)
  • dependencies: 是我们要载入的依赖模块(可选),使用相对路径。,注意是数组格式
  • factory: 工厂方法,返回一个模块函数

如果一个模块不依赖其他模块,那么可以直接定义在 define() 函数之中。示例:

// math.js
define(function () {
    var add = function (x, y) {
        return x + y;
    };
    return {
        add: add
    };
});

如果这个模块还依赖其他模块,那么 define() 函数的第一个参数,必须是一个数组,指明该模块的依赖性。

define(['Lib'], function (Lib) {
    function foo() {
        Lib.doSomething();
    }

    return {
        foo: foo
    };
});

上面代码中,当 require() 函数加载上面这个模块的时候,就会先加载 Lib.js 文件。

AMD 也可以采用 require() 语句加载模块

但是不同于 CommonJS,它要求两个参数:require([module], callback);

  • 第一个参数 [module],是一个数组,里面的成员就是要加载的模块;
  • 第二个参数 callback,则是加载成功之后的回调函数。

如果将前面的代码改写成AMD形式,就是下面这样:

require(['math'], function (math) {
    math.add(2, 3);
});

math.add() 与 math 模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD 比较适合浏览器环境。

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

AMD 兼容 CommonJS 写法

AMD 规范允许输出的模块兼容CommonJS规范,这时define方法需要写成下面这样:

define(function (require, exports, module){
  var someModule = require("someModule");
  var anotherModule = require("anotherModule");

  someModule.doTehAwesome();
  anotherModule.doMoarAwesome();

  exports.asplode = function (){
    someModule.doTehAwesome();
    anotherModule.doMoarAwesome();
  };
});

CMD 规范

CMD (Common Module Definition), 是 seajs 推崇的规范,CMD 则是依赖就近,用的时候再 require。它写起来是这样的:

define(function (require, exports, module) {
    var clock = require('clock');
    clock.start();
});

CMD 与 AMD一样,也是采用特定的 define() 函数来定义,用 require 方式来引用模块:define(id?, dependencies?, factory)

  • id:字符串,模块名称(可选)
  • dependencies: 是我们要载入的依赖模块(可选),使用相对路径。注意是数组格式
  • factory: 工厂方法,返回一个模块函数
define('hello', ['jquery'], function (require, exports, module) {
    // 模块代码
});

如果一个模块不依赖其他模块,那么可以直接定义在 define() 函数之中。

define(function (require, exports, module) {
    // 模块代码
});

注意:带 id 和 dependencies 参数的 define 用法不属于 CMD 规范,而属于 Modules/Transport 规范。

CMD 与 AMD 区别

AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。

  • AMD 依赖前置,js可以方便知道依赖模块是谁,立即加载;
  • CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。

4、现阶段的标准

  • ES6 标准发布后,module 成为标准,标准使用是以 export 指令导出接口,以 import 引入模块
  • 但是在 node 模块中,依然采用的是 CommonJS 规范,使用 require 引入模块,使用 module.exports 导出接口

export 导出模块

export 语法声明用于导出函数、对象、指定文件(或模块)的原始值。

注意:

  • node 中使用的是 exports 不是 export。不要混淆了
  • ES6 标准发布后,module 成为标准,标准使用是以 export 导出接口,以 import 引入模块

export 有两种模块导出方式:

  • 命名式导出(名称导出),命名式导出每个模块可以多个
  • 默认导出(定义式导出),默认导出每个模块仅一个。
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN;         // also var
export let name1 = …, name2 = …, …, nameN; // also var, const
 
export default expression;
export default function (…) { … }      // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
 
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
  • name1… nameN-导出的“标识符”。导出后,可以通过这个“标识符”在另一个模块中使用import引用
  • default-设置模块的默认导出。设置后import不通过“标识符”而直接引用默认导入
  • -继承模块并导出继承模块所有的方法和属性
  • as-重命名导出“标识符”
  • from-从已经存在的模块、脚本文件…导出

命名式导出

模块可以通过 export 前缀关键词声明导出对象,导出对象可以是多个。这些导出对象用名称进行区分,称之为命名式导出。

export { myFunction };           // 导出一个已定义的函数
export const foo = Math.sqrt(2); // 导出一个常量

还可以使用 * 和 from 关键字来实现的模块的继承:

export * from 'article';

模块导出时,可以指定模块的导出成员。导出成员可以认为是类中的公有对象,而非导出成员可以认为是类中的私有对象:

var name = 'IT笔录';
var domain = 'http://itbilu.com';
 
export {name, domain}; // 相当于导出 {name:name,domain:domain}

模块导出时,我们可以使用 as 关键字对导出成员进行重命名:

var name = 'IT笔录';
var domain = 'http://itbilu.com';

export {name as siteName, domain};

注意,下面的语法有严重错误的情况:

// 错误演示
export 1; // 绝对不可以
 
var a = 100;
export a;

export 在导出接口的时候,必须与模块内部的变量具有一一对应的关系。直接导出1没有任何意义,也不可能在 import 的时候有一个变量与之对应

export a 虽然看上去成立,但是 a 的值是一个数字,根本无法完成解构,因此必须写成 export {a} 的形式。即使 a 被赋值为一个function,也是不允许的。而且,大部分风格都建议,模块中最好在末尾用一个 export 导出所有的接口,例如:

export { fun as default,a,b,c};

默认导出

默认导出也被称做定义式导出。命名式导出可以导出多个值,但在在 import 引用时,也要使用相同的名称来引用相应的值。而默认导出每个导出只有一个单一值,这个输出可以是一个函数、类或其它类型的值,这样在模块 import 导入时也会很容易引用。

export default function() {}; // 可以导出一个函数
export default class(){};     // 也可以出一个类

命名式导出默认导出

默认导出可以理解为另一种形式的命名导出,默认导出可以认为是使用了 default 名称的命名导出。

下面两种导出方式是等价的:

const D = 123;
 
export default D;
export { D as default };

export 使用示例

使用名称导出一个模块时:

// "my-module.js" 模块
export function cube(x) {
    return x * x * x;
}

const foo = Math.PI + Math.SQRT2;
export {foo};

在另一个模块(脚本文件)中,我们可以像下面这样引用:

import {cube, foo} from 'my-module';

console.log(cube(3)); // 27
console.log(foo);     // 4.555806215962888

使用默认导出一个模块时:

// "my-module.js"模块
export default function (x) {
    return x * x * x;
}

在另一个模块(脚本文件)中,我们可以像下面这样引用,相对名称导出来说使用更为简单:

// 引用 "my-module.js"模块
import cube from 'my-module';

console.log(cube(3)); // 27

TypeScript 中不同 import 的含义

5、JavaScript、ES5、ES6

什么是 JavaScript

JavaScript一种 动态类型、弱类型、基于原型 的客户端脚本语言,用来给HTML网页增加动态功能。

  • 动态:在运行时确定数据类型。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。
  • 弱类型:计算时可以不同类型之间对使用者透明地隐式转换,即使类型不正确,也能通过隐式转换来得到正确的类型。
  • 原型:新对象继承对象(作为模版),将自身的属性共享给新对象,模版对象称为原型。这样新对象实例化后不但可以享有自己创建时和运行时定义的属性,而且可以享有原型对象的属性。

PS:新对象指函数,模版对象是实例对象,实例对象是不能继承原型的,函数才可以的。

JavaScript 是基于原型的客户端脚本语言,用来给 HTML 网页增加动态功能。JavaScript 由三部分组成:

  • ECMAScript(核心):它规定了语言的组成部分:语法、类型、语句、关键字、保留字、操作符、对象
  • DOM(文档对象模型):DOM 把整个页面映射为一个多层节点结果,开发人员可借助 DOM提供的 API,轻松地删除、添加、替换或修改任何节点。
  • BOM(浏览器对象模型):支持可以访问和操作浏览器窗口的浏览器对象模型,开发人员可以控制浏览器显示的页面以外的部分。

ES 全称就是 ECMAScript,作为 JavaScript 语言的核心,规定了语言的组成部分:语法、类型、语句、关键字、保留字、操作符、对象。

什么是 ES5

作为 ECMAScript第五个版本(第四版因为过于复杂废弃了),浏览器支持情况可看第一副图,增加特性如下。

  • 1. strict 模式。严格模式,限制一些用法,'use strict';
  • 2. Array 增加方法。增加了every、some 、forEach、filter 、indexOf、lastIndexOf、isArray、map、reduce、reduceRight 方法。( PS: 还有其他方法 Function.prototype.bind、String.prototype.trim、Date.now )
  • 3. Object 方法。
            Object.getPrototypeOf
            Object.create
            Object.getOwnPropertyNames
            Object.defineProperty
            Object.getOwnPropertyDescriptor
            Object.defineProperties
            Object.keys
            Object.preventExtensions / Object.isExtensible
            Object.seal / Object.isSealed
            Object.freeze / Object.isFrozen

什么是 ES6

ECMAScript6 在保证向下兼容的前提下,提供大量新特性。

ES5 教程

ES6 教程

ES6 教程:https://wangdoc.com/es6/
https://www.bookstack.cn/read/es6-3rd/sidebar.md
https://es6.ruanyifeng.com/
https://www.runoob.com/w3cnote/es6-tutorial.html
ES6从入门到精通系列:https://www.bilibili.com/video/BV1ay4y1r78B/

ES6 特性如下:

1. 块级作用域 关键字 let,常量 const

2. 对象字面量的属性赋值简写(property value shorthand)

var obj = {
    // __proto__
    __proto__: theProtoObj,
    // Shorthand for ‘handler: handler’
    handler,
    // Method definitions
    toString() {
    // Super calls
    return "d " + super.toString();
    },
    // Computed (dynamic) property names
    [ 'prop_' + (() => 42)() ]: 42
};

3. 赋值解构

let singer = { first: "Bob", last: "Dylan" };
let { first: f, last: l } = singer; // 相当于 f = "Bob", l = "Dylan"
let [all, year, month, day] =  /^(\d\d\d\d)-(\d\d)-(\d\d)$/.exec("2015-10-25");
let [x, y] = [1, 2, 3]; // x = 1, y = 2

4. 函数参数 ( 默认值参数打包 数组展开(Default 、Rest 、Spread) )

//Default
function findArtist(name='lu', age='26') {
    ...
}

//Rest
function f(x, ...y) {
  // y is an Array
  return x * y.length;
}
f(3, "hello", true) == 6

//Spread
function f(x, y, z) {
  return x + y + z;
}
// Pass each elem of array as argument
f(...[1,2,3]) == 6

5. 箭头函数 Arrow functions

  • (1). 简化了代码形式,默认 return 表达式结果。
  • (2). 自动绑定语义 this,即定义函数时的 this。如上面例子中,forEach 的匿名函数参数中用到的 this。

6. 字符串模板 Template strings

var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
// return "Hello Bob, how are you today?"

7. Iterators(迭代器)+ for..of

迭代器有个 next 方法,调用会返回:

  • (1). 返回迭代对象的一个元素:{ done: false, value: elem }
  • (2). 如果已到迭代对象的末端:{ done: true, value: retVal }
for (var n of ['a','b','c']) {
  console.log(n);
}
// 打印a、b、c

8. 生成器 (Generators)

9. Class

Class,有 constructor、extends、super,但本质上是语法糖(对语言的功能并没有影响,但是更方便程序员使用)。

class Artist {
    constructor(name) {
        this.name = name;
    }

    perform() {
        return this.name + " performs ";
    }
}

class Singer extends Artist {

    constructor(name, song) {
        super.constructor(name);
        this.song = song;
    }

    perform() {
        return super.perform() + "[" + this.song + "]";
    }
}

let james = new Singer("Etta James", "At last");
james instanceof Artist; // true
james instanceof Singer; // true

james.perform(); // "Etta James performs [At last]"

10. Modules

ES6的内置模块功能借鉴了CommonJS和AMD各自的优点:

  • (1). 具有 CommonJS 的精简语法、唯一导出出口 (single exports) 和 循环依赖 (cyclic dependencies) 的特点。
  • (2). 类似 AMD,支持异步加载和可配置的模块加载。
// lib/math.js
export function sum(x, y) {
  return x + y;
}
export var pi = 3.141593;

// app.js
import * as math from "lib/math";
alert("2π = " + math.sum(math.pi, math.pi));

// otherApp.js
import {sum, pi} from "lib/math";
alert("2π = " + sum(pi, pi));

Module Loaders:
// Dynamic loading – ‘System’ is default loader
System.import('lib/math').then(function(m) {
  alert("2π = " + m.sum(m.pi, m.pi));
});

// Directly manipulate module cache
System.get('jquery');
System.set('jquery', Module({$: $})); // WARNING: not yet finalized

11. Map + Set + WeakMap + WeakSet

四种集合类型,WeakMap、WeakSet 作为属性键的对象如果没有别的变量在引用它们,则会被回收释放掉。

// Sets
var s = new Set();
s.add("hello").add("goodbye").add("hello");
s.size === 2;
s.has("hello") === true;

// Maps
var m = new Map();
m.set("hello", 42);
m.set(s, 34);
m.get(s) == 34;

//WeakMap
var wm = new WeakMap();
wm.set(s, { extra: 42 });
wm.size === undefined

// Weak Sets
var ws = new WeakSet();
ws.add({ data: 42 });//Because the added object has no other references, it will not be held in the set

12. Math + Number + String + Array + Object APIs

一些新的 API

Number.EPSILON
Number.isInteger(Infinity) // false
Number.isNaN("NaN") // false

Math.acosh(3) // 1.762747174039086
Math.hypot(3, 4) // 5
Math.imul(Math.pow(2, 32) - 1, Math.pow(2, 32) - 2) // 2

"abcde".includes("cd") // true
"abc".repeat(3) // "abcabcabc"

Array.from(document.querySelectorAll('*')) // Returns a real Array
Array.of(1, 2, 3) // Similar to new Array(...), but without special one-arg behavior

[0, 0, 0].fill(7, 1) // [0,7,7]
[1, 2, 3].find(x => x == 3) // 3
[1, 2, 3].findIndex(x => x == 2) // 1
[1, 2, 3, 4, 5].copyWithin(3, 0) // [1, 2, 3, 1, 2]
["a", "b", "c"].entries() // iterator [0, "a"], [1,"b"], [2,"c"]
["a", "b", "c"].keys() // iterator 0, 1, 2
["a", "b", "c"].values() // iterator "a", "b", "c"

Object.assign(Point, { origin: new Point(0,0) })

13. Proxies

使用代理(Proxy)监听对象的操作,然后可以做一些相应事情。

var target = {};
var handler = {
  get: function (receiver, name) {
    return `Hello, ${name}!`;
  }
};

var p = new Proxy(target, handler);
p.world === 'Hello, world!';

可监听的操作: get、set、has、deleteProperty、apply、construct、getOwnPropertyDescriptor、defineProperty、getPrototypeOf、setPrototypeOf、enumerate、ownKeys、preventExtensions、isExtensible。

14. Symbols

Symbol 是一种基本类型。Symbol 通过调用symbol函数产生,它接收一个可选的名字参数,该函数返回的 symbol 是唯一的。

var key = Symbol("key");
var key2 = Symbol("key");
key == key2  //false

15. Promises

Promises 是处理异步操作的对象,使用了 Promise 对象之后可以用一种链式调用的方式来组织代码,让代码更加直观(类似 jQuery 的 deferred 对象)。

function fakeAjax(url) {
  return new Promise(function (resolve, reject) {
    // setTimeouts are for effect, typically we would handle XHR
    if (!url) {
      return setTimeout(reject, 1000);
    }
    return setTimeout(resolve, 1000);
  });
}

// no url, promise rejected
fakeAjax().then(function () {
  console.log('success');
},function () {
  console.log('fail');
});

ES5、ES6 的区别

初学者在 ES6 和 ES5 之间的取舍?:https://www.zhihu.com/question/30608934

1.  es5 使用 require 导包,es6 使用 import from

ES5 中的引用需要先使用 require 导入包,成为对象,再去进行真正引用

// ES5
var React = require("react");
var { Component,PropTypes } = React;  //引用React抽象组件
 
var ReactNative = require("react-native");
var { Image,Text } = ReactNative;  //引用具体的React Native组件

在 ES6 里,可以使用 import 直接实现系统库引用,不需要额外制作一个类库对象:

//ES6
import { Component,PropTypes } from 'react';
import { Image,Text } from 'react-native'

2.  导出及引用单个类

ES5 中,要导出一个类给别的模块使用,一般通过 module.exports 来实现,引用时通过 require方法来获取:

//ES5导出
var MyComponent = React.createClass({
    ...
});
module.exports = MyComponent;
 
//ES5导入
var MyComponent = require('./MyComponent');

ES6 中,使用 export default 实现同样的功能,但要使用 import 方法来实现导入:

//ES6导出(一般都继承于Component类)
export default class MyComponent extends Component{
    ...
}
 
//ES6导入
import MyComponent from './MyComponent';

3.  ES5 和 ES6 继承的区别

// ES5的继承
function FatherClass(name) {
    this.family = ['father', 'mother', 'daughter'];
    this.name = name
};
FatherClass.prototype.getName = function () {
    console.log(this.name);
};
function ChilderClass(name, age) {
    // 子类继承父类(没涉及到父类原型)
    FatherClass.call(this, name)
    this.age = age;
};
function F() { };
//过渡函数的原型继承父对象
F.prototype = FatherClass.prototype;
ChilderClass.prototype = new F();
var c = new ChilderClass('lily', 18);

这个就是 es5 的类继承,构造函数继承和原型继承是分开的(两种不同的方式)。子类继承父类的时候,先创造子类的实例对象this,然后再将父类的属性和方法添加到this上面(FatherClass.call(this,name)),然后再去继承原型链。

// ES6的继承
class FatherClass {
    family = ['father', 'mother', 'daughter'];
    constructor(name) {
        this.name = name
    }
    getName() {
        console.log(this.name);
    }
};

class ChilderClass extends FatherClass {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
};
var c = new ChilderClass('lily', 18);

这里类继承机制完全和 es5 不一样,是调用 super 方法将父类的属性和方法加到 this 上面。在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类的构建基于父类,只有 super 方法才能调用父类构造函数。

  • ES5 的继承实质上是先创建子类的实例对象 this,然后再将父类的方法添加到 this 上(Parent.apply(this)),然后继承原型。
  • ES6 的继承机制完全不同,它是在子类的构造器中先调用 super 方法,创建出父类实例对象 this,然后再去修改子类中的 this 完善子类。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值