什么是ES6以及ES6的模块化

1.ES6入门

1.1 介绍

        es6是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等,ES6 的第一个版本,在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。

        es6在es5的基础上拓展了很多新特性。 ​

        Node.js是JavaScript的服务器运行环境。它对 ES6 的支持度更高。

1.2 ES6入门

1.2.1 Babel

        babel是一个广泛使用的es6转换器,可以将ES6代码转为ES5代码,从而在老版本的浏览器执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。

// 转码前 
input.map(item => item + 1); 
// 转码后
 input.map(function (item) { return item + 1; });

1.2.2 Babel-cli

        很多时候我们需要将es6的代码转换为es5的代码,这时候就需要babel-cli

        安装

npm install --global babel-cli

        安装预设并且添加配置文件配置.babelrc

npm install --save-dev babel-preset-es2015
{ “presets”: [ "es2015"]} 

        使用 ​ 转码结果输出到标准输出

babel example.js

        转码结果写入一个文件,--out-file 或 -o 参数指定输出文件(转码并直接输出到控制台)

babel example.js --out-file compiled.js

        整个目录转码 --out-dir 或 -d 参数指定输出目录 (转码并直接输出到新文件)

babel src --out-dir lib
如果全局不能使用
cnpm install --save-dev babel-cli babel-parest-latest(局部使用)
babel工具在项目的开发阶段使用
jQuery在项目的运行阶段使用

1.2.3 Babel-polyfill

        Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。举例来说,ES6 在Array对象上新增了Array.from方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片 ​

        安装

 npm install --save babel-polyfill

        在js文件中引用并且使用

import 'babel-polyfill'; // 或者 require('babel-polyfill'); 

1.3 ES6模块化

模块化机制:CommonJS 和 ES6

包管理机制:npm,cnpm,yarn

1.3 ES6模块化

模块化机制:CommonJS 和 ES6

包管理机制:npm,cnpm,yarn

1.3.1 模块化

        Javascript一直没有模块体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。这对开发大型的、复杂的项目形成了巨大障碍。

        模块化的好处:

                防止命名冲突

                代码复用

                高维护性

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

        ES6之前的模块化规范有:

                CommonJS ====> NodeJS、Browserify(服务器端)

                AMD ====> requireJS(客户端--浏览器)

                CMD ====> seaJS

        注意:从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。在v12的版本里面,需要在package.json中设置{"type": "module”}

1.3.2 es6模块化

        ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

        ES6 模块化代码:import

        NodeJS内有自己的模块化机制,实现CommonJS模块化规范

// ES6模块 
import { stat, exists, readFile } from 'fs'; 

        ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。(区别) ​

       ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。

1.3.3 export命令

export命令用于规定模块的对外标签

        一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。

        如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系,也就是说外部接口需要用这个接口名来引用。

var firstName = 'Michael'; 
var lastName = 'vicky'; 

//统一暴露
export { firstName, lastName }; //列表导出(导出的是一个接口)
export { firstName as first, lastName as last}; //重命名导出

//声明并初始化
export var a = 3;	//导出单个属性
export function multiply(x, y) { return x * y; }; //导出单个属性

//默认导出,一个模块只能有一个默认导出,不能使用 var、let 或 const 用于导出默认值 export default。
export default {}
export default function foo(){}

//错误写法
var a = 1;
export a;	//报错,因为没有提供对外的接口。应该export var a = 1; 或者export {a},用花括号来导出,{}表示一个接口

1.3.4 import命令

        import命令用于输入其他模块提供的功能

        静态的import 语句用于导入由另一个模块导出的绑定。

//通用导入
import * as person from './person.js'	//导入整个模块内容
import {firstName,lastName} from './person.js'	//导入多个接口
import {firstName as name} from './person.js'	//重命名
import '/modules/my-module.js';			//运行整个模块而不导入任何值
import myDefault from './my-module.js';		// 导入使用export default导出的模块

//解构方式导入
import {school,teach} from "./src/js/m1.js"
import {school as guigu,findJob} from "./src/js/m2.js"
import {default as m3 } from "./src/js/m3.js"

//简便形式(只针对默认暴露)
import m3 from "./src/js/m3.js"

1.4 CommonJS

1.4.1 CommonJS模块化

        CommonJS 和 AMD 模块,都只能在运行时确定模块之间的依赖关系,以及输入输出的变量。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

// CommonJS模块 
let { stat, exists, readfile } = require('fs');
 // 等同于
let _fs = require('fs'); 
let stat = _fs.stat;
let exists = _fs.exists; 
let readfile = _fs.readfile; 

        上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

        Node内部提供一个Module构建函数。所有模块都是Module的实例。每个模块内部,都有一个module对象,代表当前模块。它有以下属性。 ​         

        module.id 模块的识别符,通常是带有绝对路径的模块文件名。 ​

        module.filename 模块的文件名,带有绝对路径。 ​

        module.loaded 返回一个布尔值,表示模块是否已经完成加载。 ​

         module.parent 返回一个对象,表示调用该模块的模块。 ​

        module.children 返回一个数组,表示该模块要用到的其他模块。 ​

        module.exports 表示模块对外输出的值。

        为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令

var exports = module.exports; 
let firstname = 'li'
let lastname = 'si'

//作为对象导出(暴露的是一个对象ES6暴露的是一个接口)
module.exports =  {
	firstname,
	lastname
}

1.4.3 require函数

        Require函数是nodejs提供的内置函数,用于加载指定路径的模块或者是指定名称的模块。将加载的模块进行返回。

        运行时加载(ES6是编译时加载)

//导入
var obj = require('fs');
var {firstname,lastname} = require('module')

1.5 模块化差异(CommonJS和ES6)

1.5.1 require函数

        ES6模块与CommonJS模块的差异:

         1、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 ES6 模块加载的不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。 ES6 模块输出的是值的引用。

let obj = {
	name : 'anna',
	age : 8
}
let obj1 = obj  //赋值的是引用地址
export {
	obj1,
	obj
}
setTimeout(()=>{
	obj.name = 'lily'  //判断obj1是否受到影响
},2000)
import {obj1,obj} form '/module'
console.log(obj,obj1)
setTimeout(()=>{
	console.log(obj1)
},2000)
//{name :'anna',age:8} {name :'anna',age:8}
//{name :'anna',age:8}  obj1的值没有受到影响
let obj = {
	name : 'anna',
	age : 8
}
let obj1 = obj  //赋值的是引用地址
module.exports {
	obj1,
	obj
}
setTimeout(()=>{
	obj{
		name : 'lily',
		age : 12
	}  //判断obj1是否受到影响
},2000)
let {obj1,obj} = require('module')
console.log(obj,obj1)
setTimeout(()=>{
	console.log(obj1)
},2000)
//{name :'anna',age:8} {name :'anna',age:8}
//{name :'anna',age:8}  obj1的值没有受到影响

        2、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 加载的是一个对象,即module.exports属性,该对象只有在脚本运行完才能生成。 CommonJS 模块输出的是值的拷贝

1.5.2 commonJS

对于基本数据类型,属于复制,即会被模块缓存,同时,在另一个模块可以对该模块的输出的变量重新赋值

  • 对于复杂数据类型,属于浅拷贝,由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块

  • 当使用require命令加载某个模块时,就会运行整个模块的代码

  • 当使用require命令加载同一个模块时,不会在执行该模块,而是渠道缓存中的值,也就是说,commonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就会返回第一次运行的结果,除非手动清楚系统缓存

  • 循环加载,属于加载时运行,即脚本代码在require的时候,就会全部执行,一旦出现某个模块“循环加载”,就只输出已经执行的部分,还未执行的部分不会输出

        1.对于基本数据类型,属于复制。即会被模块缓存,同时,在另一个模块可以对该模块输出的变量重新赋值

// b.js
let count = 1
let plusCount = () => {
  count++
}
setTimeout(() => {
  console.log('b.js-1', count)
}, 1000)
module.exports = {
  count,
  plusCount
}

// a.js
let mod = require('./b.js')
console.log('a.js-1', mod.count)
mod.plusCount()
console.log('a.js-2', mod.count)
setTimeout(() => {
    mod.count = 3
    console.log('a.js-3', mod.count)
}, 2000)

node a.js
a.js-1 1
a.js-2 1
b.js-1 2  // 1秒后
a.js-3 3  // 2秒后

        B模块export的count变量,是一个复制行为,在pluscount方法调用后,a模块中的count不收影响,同时,可以在b模块中更改a模块中的值,如果希望能够同步代码,可以export出去一个getter

// 其他代码相同
module.exports = {
  get count () {
    return count
  },
  plusCount
}
​
node a.js
a.js-1 1
a.js-2 2
b.js-1 2  // 1秒后
a.js-3 2  // 2秒后, 由于没有定义setter,因此无法对值进行设置。所以还是返回2

        2.对于复杂数据类型,属于浅拷贝,由于两个模块引用的对象指向一个内存空间,因此该模块的值做修改时会影响另一个模块

// b.js
let obj = {
  count: 1
}
let plusCount = () => {
  obj.count++
}
setTimeout(() => {
  console.log('b.js-1', obj.count)
}, 1000)
setTimeout(() => {
  console.log('b.js-2', obj.count)
}, 3000)
module.exports = {
  obj,
  plusCount
}

// a.js
var mod = require('./b.js')
console.log('a.js-1', mod.obj.count)
mod.plusCount()
console.log('a.js-2', mod.obj.count)
setTimeout(() => {
  mod.obj.count = 3
  console.log('a.js-3', mod.obj.count)
}, 2000)

node a.js
a.js-1 1
a.js-2 2
b.js-1 2
a.js-3 3
b.js-2 3

        对于对象来说属于浅拷贝,当执行a模块修改count的值为3.此时在b模块的值也为3

        3.当使用require命令加载某个模块时,就会运行整个模块的代码

        4.当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值,也就是说,commonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清楚系统缓存

        5.循环加载时,属于加载时执行,即脚本代码在require的时候,就会全部执行,一旦出现某个模块被“循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

1.5.3 ES6模块

- ES6模块中的值属于【动态只读引用】
- 对于只读来说,即不允许修改引入变量得到值,import的变量是只读的,不论是基本数据类型还是复杂数据类型
- 当模块遇到import命令时,就会生成一个只读引用,等到脚本真正执行时,再根据这个制度引用,到被加载的那个模块里面去取值
- 对于动态来说,原始值会发生改变,import加载的值也会发生改变,不论是基本数据类型还是引用数据类型
- 循环加载时,ES6模块是动态引用的,只要两个模块之间存在谋而引用,代码就能够执行

1.5.4 CommonJs和es6的Module的区别

  1. 两者的模块导入导出语法不同,commonjs是module.exports,exports导出,require导入;ES6则是export导出,import导入。

  2. commonjs是运行时加载模块,ES6是在静态编译期间就确定模块的依赖。

  3. ES6在编译期间会将所有import提升到顶部,commonjs不会提升require。

  4. commonjs导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部。ES6是导出的一个引用,内部修改可以同步到外部。

  5. 两者的循环导入的实现原理不同,commonjs是当模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

  6. commonjs中顶层的this指向这个模块本身,而ES6中顶层this指向undefined。

  7. 然后就是commonjs中的一些顶层变量在ES6中不再存在:

arguments
require
module
exports
__filename
__dirname

1.5.5 深拷贝和先拷贝的区别

1.5.5.1 什么是深浅拷贝

浅拷贝:比如我们定义一个变量为 obj 的对象,然后我们把他赋值给另一个变量,这个过程就会涉及到浅拷贝问题,另一个变量只是之前变量的一份拷贝,前后两个变量的存储地址是公用的。你可以这么认为,赋值实际上是将地址绑定在一起

这里有两个注意点:

  • 引用数据类型:对象,数组,函数等,这些拷贝的是地址

let obj = {name: {code: '人生代码'}}
let copyObj = obj

console.log(copyObj.name === obj.name) // true 说明使用同一个引用地址

- 基本数据类型:数字,字符串,布尔值这些是拷贝值

let str = "Ken"
let str1 = str

console.log(str === str1) // true 但是只是拷贝了值
  • 深拷贝

你可这么理解,就比如我们将衣服放在一个衣柜里面,后面你又买了一件一模一样的衣服,顺便也买了另一个衣柜给这件新衣服放起来,这样这两件衣服不就完全不相干了?

深拷贝其实就是这个意思:

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

深拷贝

你可这么理解,就比如我们将衣服放在一个衣柜里面,后面你又买了一件一模一样的衣服,顺便也买了另一个衣柜给这件新衣服放起来,这样这两件衣服不就完全不相干了?

深拷贝其实就是这个意思:

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

1.5.5.2 赋值和深/浅拷贝的区别

我们这里比较的是关于引用类型:

赋值

赋值其实赋的是该对象,数组的引用地址,也就是说两个对象是联动的,存在关系的,只要其中一个变量改变,另外一个变量就会跟着改变。

let obj = {
  name: '人生代码',
  arr: [1,2,3]
}

let obj1 = obj

obj1.name = "Ken"
obj1.arr = [4,5,6]

console.log(obj1, obj) // obj1:{name: 'Ken', arr: [4,5,6]} obj:{name: 'Ken', arr: [4,5,6]}

浅拷贝

重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。

// 浅拷贝
let obj1 = {
    name : '人生代码',
    arr : [1,2,3],
};
let obj2=shallowClone(obj1)
obj2.name = "Ken";
obj2.arr = [5,6,7] ; // 新旧对象还是共享同一块内存
// 这是个浅拷贝的方法
function shallowClone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }
    return target;
}
console.log('obj1',obj1) 
// obj1 { name: '人生代码', arr: [ 5,6,7 ] }
console.log('obj2',obj2) 
// obj2 { name: 'Ken', arr: [ 5, 6, 7 ] }

深拷贝

从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

// 深拷贝
let obj1 = {
    name : '人生代码',
    arr : [1,2,3],
};
let obj2=deepClone(obj1)
obj2.name = "Ken";
obj2.arr = [5,6,7] ; // 新对象跟原对象不共享内存
// 这是个深拷贝的方法
function deepClone(obj) {
    if (obj === null) return obj; 
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    if (typeof obj !== "object") return obj;
    let cloneObj = new obj.constructor();
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        // 实现一个递归拷贝
        cloneObj[key] = deepClone(obj[key]);
      }
    }
    return cloneObj;
}
console.log('obj1',obj1) 
// obj1 { name: '人生代码', arr: [ 1, 2, 3 ] }
console.log('obj2',obj2) 
// obj2 { name: 'Ken', arr: [ 5, 6, 7 ] }

1.5.5.3 浅拷贝实现的方式

Object.assign()

1.5.5.3 浅拷贝实现的方式

Object.assign()

let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); 
// { person: { name: 'wade', age: 41 }, sports: 'basketball' }

函数库lodash的_.clone方法

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);
// true

展开运算符...

let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) 
// obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

Array.prototype.concat()

let arr = [1, 3, {
    username: 'kobe'
}];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
console.log(arr); 
//[ 1, 3, { username: 'wade' } ]

Array.prototype.slice()

let arr = [1, 3, {
    username: ' kobe'
    }];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); 
// [ 1, 3, { username: 'wade' } ]

1.5.5.4 深拷贝的实现方式

JSON.parse(JSON.stringify())

函数库lodash的_.cloneDeep方法

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false

jQuery.extend()方法

var $ = require('jquery');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); 
// false

手写递归方法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。

1.6 npm(包管理机制)

1.6.1 创建模块

        npm init -y 初始化nodejs项目

        生成一个package.json文件,该文件中保存了项目所有相关信息

        全局依赖

                cnpm install xxx --global

                简写 :cnpm i xxx --g

        局部依赖

                产品依赖

                        cnpm install --save

                        简写:cnpm i xxx -s

                开发依赖

                        cnpm install --save-dev

                        简写:cnpm i xxx -D

        npm是的Javascript开发者能够更方便的分享和复用以及更新代码,被复用的代码被称为包或者模块,一个模块中包含了一到多个js文件。在模块中一般还会包含一个package.json的文件,该文件中包含了该模块的配置信息。该文件是个json文件,其配置信息如下: ​

        name 模块名称 ​

        version 模块版本 ​

        description 描述信息 ​

        main 指定模块入口文件 ​

        type 当type值为module的时候,支持es模块化 ​

        scripts 脚本,使用' npm run 脚本名'可以调用 ​

        dependencies 依赖关系 ​

        devDependencies 环境依赖或测试依赖

1.6.2 常见命令

        通过npm可以为当前模块安装依赖模块,更新依赖模块,删除依赖的模块

npm init 
	-y
npm install 
	--save、-S
	--save-dev、-D
	--global、-g
npm update 
npm uninstall 

1.6.3 淘宝npm镜像cnpm

        使用过程中,我们会发现,npm安装依赖的速度比较慢,我们可以使用淘宝的npm镜像cnpm,cnpm的使用与npm使用非常类似。不过在使用之前要先安装cnpm

npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm init 
	-y
cnpm install 
	--save、-S
	--save-dev、-D
	--global、-g
cnpm update 
cnpm uninstall 

你可能感兴趣的