搞清楚import和export| 8月更文挑战

308 阅读8分钟

问题背景

某angular4项目接入神策sdk失败,控制台报错undefined

引入方法

import sa from 'sa-sdk-javascript';
sa.init({
    server_url: 'https://xxx.com';
});   

最终解决方案

import * as sa from 'sa-sdk-javascript';
sa.init({
    server_url: 'https://xxx.com';
});   

这两种引入方式有什么区别呢,来看一个例子

exporter.js

export default {
  one: "One",
  two: "Two",
  three: "Three",
};

export var a = 'a';

importer.js

// Has 3 props ['one', 'two', 'three']
import anyname from './exporter';

// Has 2 props ['default', 'a'].
import * as allExports from './exporter';
import {one} from './exporter';
import two from './exporter';

console.log(anyname);
console.log(allExports);
console.log(one);
console.log(two);

输出如下

{one: "One", two: "Two", three: "Three"}
one: "One"
three: "Three"
two: "Two"
__proto__: Object


a: (...)
default: {one: "One", two: "Two", three: "Three"}
Symbol(Symbol.toStringTag): "Module"
__esModule: true
get a: ƒ ()
__proto__: Object


undefined


{one: "One", two: "Two", three: "Three"}
one: "One"
three: "Three"
two: "Two"
__proto__: Object

这是成功的例子,还有在此之前失败的例子

importer.js

// Has 3 props ['one', 'two', 'three']
import defaultExport from './exporter';

// Has 2 props ['default', 'a'].
import * as allExports from 'exporter';

console.log(defaultExport)
console.log(allExports)

直接运行node importer.js

import defaultExport from './exporter';
       ^^^^^^^^^^^^^

SyntaxError: Unexpected identifier
    at new Script (vm.js:74:7)
    at createScript (vm.js:246:10)
    at Object.runInThisContext (vm.js:298:10)
    at Module._compile (internal/modules/cjs/loader.js:646:28)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:689:10)
    at Module.load (internal/modules/cjs/loader.js:589:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:528:12)
    at Function.Module._load (internal/modules/cjs/loader.js:520:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:719:10)
    at startup (internal/bootstrap/node.js:228:19)

成功运行是因为什么呢,是放在angular项目中运行的,关键在于babel

至于importexport的理解呢,也不深,学习一下es6中Module 的语法

以下是关键词笔记

一、历史上js没有模块(module)体系

也就是js最初的设计根本没有模块这个概念

缺陷导致:无法将大的程序拆分成相互依赖的小文件(模块),再用简单的方法拼装起来

其实这是对于工程上的缺陷,之所以要把大的程序拆分成相互依赖的小模块,是为了不同的功能可以分开进行开发和维护,就像一个个小系统一样,而最终要用简单的方式拼装起来是因为程序运行并不关注程序究竟有多大有多复杂,只要程序运行的通就能跑起来,分治策略只是人类帮组自己理解和处理庞大程序的手段。

没有对比就没有伤害:同为脚本语言的ruby有自己的require,python有import,连样式语言css都有@import,es6之前的js被比的无地自容

二、CommonJSAMD原来是在es6出现前的社区解决方案

CommonJS用于服务器,AMD用于浏览器

三、es6从语言标准层面上实现了模块功能,而且实现得相当简单

nice,从此昂起头颅

es6的模块功能可以取代CommonJSAMD,成为通用的模块解决方案

四、es6模块设计思想是尽量的静态化(又一个新概念)

静态化是指编译时就能确定模块的依赖关系以及输入输出的变量

五、对比CommonJS的优势比如CommonJS的模块是对象,输入时必须查找对象属性

就很僵硬

看到这里我觉得有必要再去深入了解一下历史,也就是CommonJSAMD是怎么一回事

六、关于静态化的CommonJS和es6模块的对比

CommonJS

// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

第一行虽然看起来和我们现在用的es6模块用法相仿,但实际上有两点关键的不同

  1. 上述操作实际整体加载fs模块,而只是仅仅用到了里面的三个方法
  2. 上述动作是在程序运行时加载的,因为只有运行时才能得到这个对象(因此无法在编译阶段做静态优化

es6模块

// ES6模块
import { stat, exists, readFile } from 'fs';
  1. es6模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入
  2. 上述操作只会加载这3个方法,其他方法不加载,称为编译时加载(静态加载)

这就是从语言标准层面实现的优势(一雪前耻),简简单单增加两个内置关键词,通过命令就完成了编译前的模块传输声明,社区方案只能曲线救国。

七、关于export命令

  1. 一个模块就是一个独立的文件
  2. 文件内部所有变量外部无法获取
  3. 外部读取模块内变量必须使用export输出该变量

例如

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

另一种写法

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

输出方法或类

export function multiply(x, y) {
  return x * y;
};
export class multiply(x, y) {
  return x * y;
};

输出时重命名

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion // 同一个输出对象(变量、类、方法)可以以不同的重命名输出多次
};

注意事项

  1. export命令规定的是对外的接口,必须与模块内的变量建立一对一关系(接口名与模块内部变量一对一)
  2. 不能直接对外输出变量的值,而是应以接口的形式输出

反例

// 直接输出值
export 1;

var m = 1;
// 直接输出变量的值
export m;

正解

// 直接输出声明的变量
export var m = 1;

// 先声明变量,再输出变量
var m = 1;
export {m};

// 先声明变量,再将变量以别名输出
var n = 1;
export {n as m};

以上三种都是在对外输出接口m,其他脚本通过接口m获取到其内部的值

特点

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

例如

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

foo的值先是bar,0.5秒后变为baz

此特点对比CommonJSCommonJS中的模块输出的是值的缓存

注意事项

  1. export可以出现在模块的任意位置,但需要在模块顶层
  2. 如果处于块级作用域会报错,因为处于条件代码块中会阻碍静态优化,违背了es6模块设计的初衷

八、关于import命令

  1. import命令接受一对大括号,里面指定要从其他模块导入的变量名——见例1
  2. 大括号里的变量名必须与被导入模块对外接口的名称相同
  3. 如果想为引入的变量重命名,可以在引入的时候使用as为其重命名——见例2
  4. import命令输入的变量都是只读的,但变量的属性可以被改写,但这种操作很危险,会影响到其他引入该模块的地方——见例3(类似于const声明的对象,可以改变其内部的属性,模块export输出的接口相当于一个对象)

例1

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

例2

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

例3

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

引入方式

  1. 可以是相对路径,也可以是绝对路径
  2. .js后缀可以省略
  3. 直接引入模块名不带路径,需要有配置文件告诉js引擎该模块的位置

注意事项

  1. import具有提升效果,会提升到整个模块的头部首先执行(本质因为import命令是编译阶段执行,在代码运行之前)——见例1
  2. 由于import静态执行,所以不能用表达式变量,因为这些都是在运行时才能得到结果的语法接口——见例2

例1

foo();
// 虽然位置在下面,但执行早于 foo() 调用
import { foo } from 'my_module';

例2

// 报错
import { 'f' + 'oo' } from 'my_module';

// 报错
let module = 'my_module';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

注意事项

  1. import语句会执行所加载的模块
  2. 多次重复执行同一句import语句只会执行一次(单例模式)——见例1
  3. 目前,通过babel转码,CommonJS模块的require命令和es6模块的import命令可以共存,但不建议同时使用,因为import静态解析阶段执行,在一个模块之中最早执行,可能会使得require语句不按预期执行——见例2

例1

import 'lodash';
import 'lodash';
// 只会加载一次

import { foo } from 'my_module';
import { bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
// 只会加载一次

例2

require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
// 最先执行
import React from 'React';

九、模块整体加载

方式

*号指定一个对象,所有模块的输出值都加载在这个对象上

// circle.js

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

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

逐一加载

// main.js

import { area, circumference } from './circle';

console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

整体加载

import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

注意点

模块整体加载所在的那个对象是可以静态分析的,不允许运行时改变

import * as circle from './circle';

// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};