ES6-12与JS模块化笔记

ES介绍

ES就是ECMAScript简称,JS就是ES的一个实现,这里ECMA(欧洲计算机制造商学会)每年发布一个ES新版本,2020年到了ES11,前置内容: JS,Ajax,Promise

为什么从ES6开始: 变动多,有里程碑意义,新增语法特性

ES兼容性查询网站,即使不兼容我们也可以通过编译器编译成ES5

ES6新特性

eval函数(ES3函数补充)

eval()函数可计算某个字符串,并执行其中的的JavaScript代码。

  • 语法: eval(string) string: 要计算的字符串,其中含有要计算的 JavaScript 表达式或要执行的语句。
  • 返回值 通过计算 string 得到的值(如果有的话)。
  • 说明 该方法只接受原始字符串作为参数,如果 string 参数不是原始字符串,那么该方法将不作任何改变地返回。因此请不要为 eval() 函数传递 String 对象来作为参数。
  • 如果试图覆盖 eval 属性或把 eval() 方法赋予另一个属性,并通过该属性调用它,则 ECMAScript 实现允许抛出一个 EvalError 异常。
  • 抛出 如果参数中没有合法的表达式和语句,则抛出 SyntaxError 异常。如果非法调用 eval(),则抛出 EvalError 异常。如果传递给 eval() 的 Javascript 代码生成了一个异常,eval() 将把该异常传递给调用者。
  • 虽然 eval() 的功能非常强大,但在实际使用中用到它的情况并不多。不同浏览器不同版本对eval处理有差异 实例
    eval("x=10;y=20;document.write(x*y)")
    console.log(eval("2+2"))
    var x=10
    console.log(eval(x+17))
    输出
    200
    4
    27
  • 作用域
    var x = 1;
    (function () {
        eval('var x = 123;');
        console.log("I",x)
    })();
    console.log(x);
    var x = 1;
    (function () {
        window.eval('var x = 123;');
        console.log("I",x)
    })();
    console.log(x);
    在ES<=8,得到的是
    I 123
    1
    I 123
    1
    在ES>8得到的是
    I 123
    1
    I 123
    123
    在ES8-下eval是函数作用域,在ES8+下eval的作用域取决于调用者

let关键字

  • var的级别用法一样
  • let不可以重复声明,但是var可以
    var varIns = "A";
    var varIns = "B"; // 合法
    let letIns = "A";
    let letIns = "B"; // 不合法
  • let具有块级作用域,var没有块作用域,JS的作用域有
    • 全局作用域(例如a=1,那么a就是window的成员)
    • 函数作用域(例如在函数中var a=1, 那么出了函数a就没了)
    • eval作用域
    • 块作用域(例如{a=1},if(){},while(){}...,那么在括号外面a就不可用)
  • let不存在变量提升,例如
    console.log(a);       // 输出undefined
    var a=1;
    console.log(a);       // 输出1
    
    console.log(b);       // 报错
    let b=1;
    console.log(b);
  • let不影响作用域链 虽然是块级作用域,但是不影响如果函数内部没有变量会自动往外找

实例

在以前写代码的时候我们遇到过这个问题

// 我有5个button
var btns = document.querySelectorAll("button");
for(var i = 0;i < btns.length;i++){
    btns[i].onclick = function(){
        console.log(i);
    }
}
这时候不管点哪个都显示5,这是因为onclick里面没有i,于是闭包传输了i,这个i是外面的i,i在变化,到点击的时候相当于是走完了
{var i=0} //第1个循环
{var i=1} //第2个循环
{var i=2} //第3个循环
{var i=3} //第4个循环
{var i=4} //第5个循环
这个时候i就是5,因为i是for的块级作用域内部变量,var没有块级作用域,第二个循环的修改会影响第一个循环的i

我们有两个修改方法

  • 增加参数
    var btns = document.querySelectorAll("button");
    for(var i = 0;i < btns.length;i++){
        btns[i].idx = i;
        btns[i].onclick = function(){
            console.log(this.idx);
        }
    }
  • 修改为let
    // 我有5个button
    var btns = document.querySelectorAll("button");
    for(let i = 0;i < btns.length;i++){
        btns[i].onclick = function(){
            console.log(i);
        }
    }

相当于执行了

{let i=0} //第1个循环
{let i=1} //第2个循环
{let i=2} //第3个循环
{let i=3} //第4个循环
{let i=4} //第5个循环
let有块级作用域,第二个循环的修改不会影响第一个循环的i=1

const关键字

  • 格式于let一样
  • 必须赋初值
  • 建议大写
  • 不能修改
  • 有块级作用域
  • 常量指向数组/对象的修改不算修改,因为const对象指向的地址没有变

变量解构赋值

可以按照一定模式从数组/对象提取值,对变量进行赋值

  • 对数组进行解构[...]=Array,就是找对应元素分别对应数组,例如

    let [a,b,c,d]=[111,222,333,"789"];
    console.log(a);
    console.log(b);
    console.log(c);
    console.log(d);
    不是很常用

  • 可以对对象进行解构{...}=Object,要求变量名一一对应,例如

    let t={
        a:111,
        b:222,
        d:333,
        c:function(){console.log("OK")}
    }
    let {a,b,c,d}=t;
    console.log(a);
    console.log(b);
    console.log(c);
    c();
    console.log(d);
    d();              // Error
    解构一般用于提取方法,例如我之前要写t.c()就可以简写成c()

  • 解构时候如果数目不同/不匹配时,会尽量匹配,例如

    let [a,b] = [1,2,3]  // a=1 b=2
    let [a,b,c,d] = [1,2,3]  // a=1 b=2 c=3 d=undefined
    let t={
        x:111,
        y:222,
        z:333,
        w:function(){console.log("OK")}
    }
    let {x,y} = t;      // x=111 y=222
    let {z,w,s} = t;    // z=333 w=[function] s=undefined

  • 解构支持默认值 JS确认某个参数要使用默认值是这个参数===undefined

    let [m1,m2="S"] = ["A"]   // m1="A" m2="S"
    let [n1,n2="S"] = ["A",undefined]   // n1="A" n2="S"
    let [p1,p2="S"] = ["A",null]   // p1="A" p2=null

  • 字符串的解构赋值 将字符串转化为数组

    let [a,b,c] = "Liu" // a="L" b="i" c="u"

  • 数值与布尔值解构

    let {toString: s}=123;
    console.log(s===Number.prototype.toString)
    let {toString: r}=true;
    console.log(r===Boolean.prototype.toString)
    都是true

  • 函数参数解构

    function foo([a,b]){
      console.log(a,b); // 1,2
    }
    foo([1,2])
    
    function foo2({a=0,b=0}={}){        // 无参时默认{} a,b也有默认值
    //function foo2({a,b}={a=0,b=0}){   // 注意,这两种方法出的结果不同
      console.log(a,b);
    }
    foo2({a:1,b:2})         // 1,2
    foo2({a:1})             // 1,0
    foo2({a:undefined,b:2}) // 0,2
    foo2()                  // 0,0

  • 用处

    • 交换值[a,b]=[b,a]
    • 接受函数返回对象/数组
    • 函数参数定义
    • 函数参数默认值
    • 加载模块
      const { SourceMapConsumer, SourceNode} = require("source-map")
    • 获取map值
      let map = new Map();
      map.set("a","A")
      map.set("b","B")
      
      // 获取键值
      for (let [key,value] of map) {
        // ...
      }
      
      // 获取键名
      for (let [key] of map) {
        // ...
      }
      
      // 获取键值
      for (let [value] of map) {
        // ...
      }
  • 补充for的遍历

    • for in 便历出来的是属性
    • for of 遍历的是value
    • 手动给对象添加属性后, for in 是可以将新添加的属性遍历出来 但是for of 不行
    • for in 的属性是使用[]不可以使用 "." eg: data['index'] instead of data.index

模板字符串

模板字符串使用``声明,支持

  • 在内容中出现换行 例如
    let a = '123\n  456\n   789';
    console.log(a);
    let b = `123
      456
       789`;
    console.log(b);
    let c = '123
      456
       789';
    console.log(c);
    输出分别是
    123
      456
       789
    123
      456
       789
    报错
  • 支持变量拼接,使用${}
    let name = "Liu";
    let spk1 = `I'm `+name+`, Hello`
    let spk2 = `I'm ${name}, Hello`
    console.log(spk1)
    console.log(spk2)

简化对象写法

可以在{}中直接写对象,例如

let aaa = "aaa"
let bbb = "bbb"
console.log({aaa,bbb})
console.log({aaa: "aaa", bbb: "bbb"})
console.log({aaa, bbb, foo(){console.log("OK")}})
console.log({aaa: "aaa", bbb: "bbb", foo:function(){console.log("OK")}})
结果是
{aaa: "aaa", bbb: "bbb"}
{aaa: "aaa", bbb: "bbb"}
{aaa: "aaa", bbb: "bbb", foo: ƒ}
{aaa: "aaa", bbb: "bbb", foo: ƒ}
这里要简写的是变量,不能是常量,例如我想构造{"A":"A"}不能写{"A"}

箭头函数

function(a,b)=>{//code}简写为(a,b)=>{//code}

  • this是静态的,始终指向函数声明时所在作用域下的this(call,apply修改对 他无效),而funcion是this指向调用者
      S.prototype.work1=function(){
        setTimeout(function(){console.log(this)},1000)
    }
    
    S.prototype.work2=function(){
        setTimeout(()=>{console.log(this)},1000)
    }
    
    S.prototype.work3=()=>{
        setTimeout(()=>{console.log(this)},1000)
    }
    
    let s=new S();
    s.age = 12;
    s.work1();
    s.work2();
    s.work3();
    输出
    window
    s对象
    window
  • 不能作为构造函数实例化对象,因为this不能指向对象,会报错XX不是一个构造函数
  • 不能使用arguments变量
    function work1(){console.log(arguments)}
    function work2(...arg){console.log(arg)}
    work3=()=>{console.log(arguments)}
    work4=(...arg)=>{console.log(arg)}
    work1(1,2,3,4)    // 输出Arguments变量
    work2(1,2,3,4)    // 输出数组
    work4(1,2,3,4)    // 输出数组
    work3(1,2,3,4)    // 报错arguments是undefined
  • 箭头函数还可以简写
    • 当只有一个形参的时候可以把(a)=>{// code}简写成a=>{// code}
    • 当代码体只有一条语句的时候()=>{// onecode}简写成()=>// onecode,同时不能写return,此时语句的返回结果就是返回值,例如(a)=>{return a*a}简写为a=>a*a
  • 特例,我想简写返回对象x => { foo: x }会报错,换成x => ({ foo: x })

实例 不适合与this有关的操作,例如dom的回调

let [btn1,btn2] = document.querySelectorAll("button");
btn1.myflag=true;
btn1.onclick=function(){
  setTimeout(function(){
    console.log(this.myflag);
  },1000)
}
btn2.myflag=true;
btn2.onclick=function(){
  setTimeout(()=>{
    console.log(this.myflag);
  },1000)
}
点击第一个按钮是undefined,第二个是true 获取所有奇数
let s = [1,4,5,6,7,8]
console.log(s.filter(i=>i%2))

函数默认参数

  • 与解构赋值结合
    function foo({url=127,port=22}={}){
      console.log(url,port)
    }
    foo({url:128})
    foo({port:23})
    foo({lalala:128})
    foo()

REST参数

对于function定义的函数可以使用arguments获取参数列表,arguments是一个伪数组,没有forEach等函数,我们可以使用REST参数解决,写法是...变量,

function foo(...args){
  console.log(args)
}
foo(1,2,3,4);   // 结果是[1,2,3,4]
与C语言的...一样,我们也可以指定一部分,但是...必须放末尾
function foo(x,y,...args){
  console.log(args)
}
foo(1,2,3,4);   // 结果是[3,4]

...Spread扩展运算符

与REST的...标识一样,作用不同,rest是放在形参的位置,扩展运算符是放在调用实参的位置,用来将数组分开(类似于解构数组)

function foo(){
    console.log(arguments)
}
foo(...[1,2,3])   //Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
扩展运算符只能用在函数调用时候,但是也包括如写用法
// 1. 数组的合并
let ary = [...[1,2,3],...[4,5,6],...[7,8,9]]
// [1,2,3,4,5,6,7,8,9]

// 2. 数组的克隆
let aty_cp = [...ary]
// 当然这是个浅拷贝

// 3. 伪数组的转换
function foo(){
  let agms = [...arguments]
  console.log(agms)
}
foo(1,2,3,4,5)

...实际上是个语法糖

Symbol 数据类型

  • Symbol是ES6引入的一种新的数据类型
  • 用来表示独一无二的值
  • 他是一种类似于字符串的值,保证值是唯一的
  • Symbol值不能参与任何一种运算,外部也看不到Symbol的值是多少, 只能知道分别定义两个Symbol一定是不同的

创建Symbol

let s = Symbol()    // 不用写new 因为是js的默认数据类型
console.log(s,typeof s)     // Symbol() symbol
我们无法知道Symbol的值,js保证内部实现独一无二

我们还可以为Symbol传入一个注释

let s1 = Symbol("Liu");
let s2 = Symbol("Liu");
console.log(s1===s2)     // false
但是相同的注释不是生成相同的结果,这个功能一般用于调试使用, 例如想为对象加入一个debug熟悉就可以Obj[Symbol(debug)]="OK",方便我们删除,在ES10中会有新方法对注释进行利用

我们可以像使用hash一样为相同的内容生成相同的Symbol

let s1 = Symbol.for("Liu");
let s2 = Symbol.for("Liu");
console.log(s1===s2)     // true
Symbol的作用原文

Symbol的作用非常的专一,换句话说其设计出来就只有一个目的——作为对象属性的唯一标识符,防止对象属性冲突发生。

举个例子,你看上了公司前来的前台妹纸,想了解关于她的更多信息,于是就询问Hr同事,扫地阿姨,于是得到类似这样信息:

let info1 = {
    name: '婷婷',
    age: 24,
    job: '公司前台',
    description: '平时喜欢做做瑜伽,人家有男朋友,你别指望了'
}
let info2 = {
    description: '这小姑娘挺好的,挺热情的,嘿嘿嘿……'
}
显然,你需要对这两个数据进行汇总,结果,就会发现,描述都用了同一个对象属性description,于是整合的时候,就容器冲突,覆盖,导致“人家有男朋友”这么重要的信息都没注意到。

但是,如果要是Symbol,则完全就不要担心这个问题了:

let info1 = {
    name: '婷婷',
    age: 24,
    job: '公司前台',
    [Symbol('description')]: '平时喜欢做做瑜伽,人家有男朋友,你别指望了'
}
let info2 = {
    [Symbol('description')]: '这小姑娘挺好的,挺热情的,嘿嘿嘿……'
}
此时,我们对info1, info2对象进行复制,如下:
let target = {};
Object.assign(target, info1, info2);
此时target对象如下所示:
{name: "婷婷", age: 24, job: "公司前台", Symbol(description): "平时喜欢做做瑜伽,人家有男朋友,你别指望了", Symbol(description): "这小姑娘挺好的,挺热情的,嘿嘿嘿……"}

Symbol内置值 作为数据类型,Symbol不能像String一样直接直接使用,而是像声明一个对象一样的使用,这是因为Symbol是一个函数对象,具有成员函数,有

  • Symbol.hasInstance(): 判断是否是Symbol
  • Symbol.isConcatSpreadable: 决定是否可以Spread,例如
    let a = [1,2,3];
    let b = [4,5,6];
    let c = [...a,...b];
    let d = a.concat(b)
    b[Symbol.isConcatSpreadable] = false;
    let e = [...a,...b];
    let f = a.concat(b)
    console.log(c,d,e,f)
    结果
    [ 1, 2, 3, 4, 5, 6 ]
    [ 1, 2, 3, 4, 5, 6 ]
    [ 1, 2, 3, 4, 5, 6 ]
    [ 1, 2, 3, [ 4, 5, 6, [Symbol(Symbol.isConcatSpreadable)]: false ] ]
  • 还有一些属性是当这个Symbol作为一个函数的参数自动执行的方法,见此文章

其他Symbol相关

  • Symbol与for…in迭代 Symbols在for...in迭代中不可枚举,如果想要达到效果,借助Object.getOwnPropertySymbols(obj)这个方法。

    var obj = {};
    
    obj[Symbol("a")] = "a";
    obj[Symbol.for("b")] = "b";
    obj["c"] = "c";
    obj.d = "d";
    
    for (var i in obj) {
      console.log(i);   // 输出 "c" 和 "d"
    }

  • Symbol与JSON.stringify() 当使用JSON.strIngify()时以symbol值作为键的属性会被完全忽略,例如:

    JSON.stringify({[Symbol("foo")]: "foo"});    // '{}'

  • Symbol包装器对象作为属性的键 围绕原始数据类型创建一个显式包装器对象从ECMAScript 6开始不再被支持,所以new Symbol()会报错,然而,现有的原始包装器对象,如 new Boolean、new String以及new Number因为遗留原因仍可被创建。

    此时,如果我们想创建一个Symbol包装器对象 (Symbol wrapper object),你可以使用Object()函数:

    var sym = Symbol("foo");
    typeof sym;     // "symbol"
    var symObj = Object(sym);
    typeof symObj;  // "object"
    当一个Symbol包装器对象作为一个属性的键时,这个对象将被强制转换为它包装过的symbol值:
    var sym = Symbol("foo");
    var obj = {[sym]: 1};
    obj[sym];            // 1
    obj[Object(sym)];    // 还是1

总结JS数据类型: UOSNB

  • U: undefiend
  • O: Object
  • S: String Symbol
  • N: Null Number
  • B: Boolean

迭代器

迭代器是一种接口,各种数据结构只要在定义的时候定义了Iterator接口(就是对象就具有Iterator成员变量)就可以完成迭代操作

  • ES6加入了新的遍历方式 for-of , 调用Iterator接口对对象进行遍历
  • ES6原生具备Iterator的对象有
    • Array
    • Arguments
    • Set
    • Map
    • String
    • TypedArray
    • NodeList
  • Iterator的使用方法与C++的迭代器相似,创建的时候迭代器变量获得对象,在迭代器.next()后,next函数返回一个指向数据结构的首地址值,使用next进行指针的移动获得值,不断调用next达到数据结构的

查看使用预定义接口

以Array为例,Iterator保存在Array.prototype.Symbol(Symbol.iterator),也就是实例的[].__proto__.Symbol(Symbol.iterator),他对应的值是一个对象

使用next()

let a = ['A','B','C'];
let iterator = a[Symbol.iterator]();
console.log(iterator)         // 这里的iterator是一个包含next()的对象
console.log(iterator.next())  // 这里的iterator不变,next()返回的是对象的0地址等内容
console.log(iterator.next())  // 这里的iterator不变,next()返回的是对象的1地址等内容
console.log(iterator.next())  // 这里的iterator不变,next()返回的是对象的2地址等内容
console.log(iterator.next())  // 这里的iterator不变,next()返回的是无内容

结果

{value: "A", done: false}     // 返回值是一个对象,包含value和done表示是否结束迭代
{value: "B", done: false}
{value: "C", done: false}
{value: undefined, done: true}    // 遍历结束后设置done=true

使用for-of

let a = ['A','B','C'];
for(let i of a)
    console.log(i)          // i 保存的是Value

for(let i in a)
    console.log(i)          // i 保存的是Key

在这里也见识到到Symbol的作用,他创建了Array的一个独一无二的属性

自定义迭代器

为类自定义一个迭代器,我们实现一个FakeArray类,他包含了姓名和一个数组,每次迭代返回姓名@数组元素

function FakeArray(name,...arg){
  this.name = name;
  this.data = [];
  arg.forEach((d,i)=>
      this.data.push(d)
  )
}

/**
 * let fa = new FakeArray("Liu","H","A","P","P","Y")
 * 我们的需求是for(d of fk)遍历fk.data的元素
 * 不直接使用`fk.data.forEach()`的原因是1. 不OOP思想了 2. data不安全了
 */

FakeArray.prototype[Symbol.iterator]=function(){  // 定义一个迭代器的构造函数
  let idx = 0;          // 一部分自定义代码
  return {              // 迭代器应该是一个有next函数的对象
    next: ()=>{         // 每次执行next函数 包含value done, 用箭头函数使得可以读到idx
      return {value:this.name+"@"+this.data[idx],done:idx++===this.data.length}
    }
  }
}

let fa = new FakeArray("Liu","H","A","P","P","Y");
for(d of fa){
  console.log(d)
}

为对象自定义一个迭代器,我们实现一个fa对象,他包含了姓名和一个数组,每次迭代返回姓名@数组元素

let fa = {
  name : "Liu",
  data : [..."happy"],
  // [Symbol.iterator]:function(){
  [Symbol.iterator](){        // 进行了简写,等同于上面注释的代码
    let idx = 0;
    return {
      next: ()=>{
        return {value:this.name+"@"+this.data[idx],done:idx++===this.data.length}
      }
    }
  }
}

for(d of fa){
    console.log(d)
}

生成器

生成器是一个函数,是一个ES6异步编程解决方案, 之前我们异步编程使用的是回调函数,但是容易形成回调地狱

定义: 生成器函数与就是在普通函数声明前面加入了*,例如

function * gen(){
  console.log("hi");
  return "GEN OK"
}
我们可以将函数的结果赋值给变量,函数不会立刻运行,变量的值是一个迭代器。函数运行,当且仅当变量执行了next(),这个时候变量的值还是迭代器,返回值要另行接受,例如
function * gen(){
  console.log("hi");
  return "GEN OK"
}

let p = gen();
console.log(p);     // p是gen {<suspended>} 是一个迭代器
let q = p.next()    // 执行next 输出hi
console.log(p,q);   // p是gen {<closed>} 迭代器 q是迭代器结果{value: "GEN OK", done: true}
注意,gen函数可以使用yield进行分割,

  • yield是ES6的新关键字,使生成器函数执行暂停,yield关键字后面的表达式的值返回给生成器的调用者。它可以被认为是一个基于生成器的版本的return关键字。
  • yield关键字实际返回一个IteratorResult(迭代器)对象,它有两个属性,value和done,分别代表返回值和是否完成。
  • yield无法单独工作,需要配合generator(生成器)的其他函数,如next,懒汉式操作,展现强大的主动控制特性。
    function * gen(){
      // ====函数第1部分====
      console.log("Part 1");
      yield "P1"                  // == 返回"P1" 不得用return否则后面代码都无效了
      // ====函数第2部分====
      console.log("Part 2");
      yield "P2"                  // == 返回"P2" 不得用return否则后面代码都无效了
      // ====函数第3部分====
      console.log("Part 3");
      yield "P3"                  // == 返回"P3" 不得用return否则后面代码都无效了
      // ====函数第4部分====
      console.log("Part 4");
      yield "P4"                  // == 返回"P4" 不得用return否则后面代码都无效了
      // ====函数第5部分====
      return "AllEnd"             // == 返回"AllEnd" 不得用yield会新开一个部分
    }
    
    let f = gen();
    
    console.log(f.next())         // { value: 'P1', done: false }
    console.log(f.next())         // { value: 'P2', done: false }
    console.log(f.next())         // { value: 'P3', done: false }
    console.log(f.next())         // { value: 'P4', done: false }
    console.log(f.next())         // { value: 'AllEnd', done: true }
    console.log(f.next())         // { value: undefined, done: true }

由于生成器是一个迭代器对象,我们也可以这么写

function * gen(){
    // ====函数第1部分====
    console.log("Part 1");
    yield "P1"                  // == 返回"P1" 不得用return否则后面代码都无效了
    // ====函数第2部分====
    console.log("Part 2");
    yield "P2"                  // == 返回"P2" 不得用return否则后面代码都无效了
    // ====函数第3部分====
    console.log("Part 3");
    yield "P3"                  // == 返回"P3" 不得用return否则后面代码都无效了
    // ====函数第4部分====
    console.log("Part 4");
    yield "P4"                  // == 返回"P4" 不得用return否则后面代码都无效了
    // ====函数第5部分====
    return "AllEnd"             // == 返回"AllEnd" 不得用yield会新开一个部分
};

for(let d of gen()){
    console.log(d)
}
// Part 1
// P1
// Part 2
// P2
// Part 3
// P3
// Part 4
// P4
// 没有AllEnd,因为最后一次没用yield

yield参数传递

如果想给某个部分的函数传递参数可以把参数写在next(),yield会获得返回值,例如

function * gen(a){
    console.log(a)
    let b = yield "===PART B==="
    console.log(b)
    let c = yield "===PART C==="
    console.log(c)
    let d = yield "===PART D==="
    console.log(d)
}

let it = gen(5);
console.log(it.next(4))
console.log(it.next(3))
console.log(it.next(2))
console.log(it.next(1))
console.log(it.next(0))     // over bound

使用生成器避免回调地狱,实现异步操作1

需求是1-5s,每1s输出一个数字,使用回调函数的写法是

setTimeout(()=>{
    console.log(1)
    setTimeout(()=>{
        console.log(2)
        setTimeout(()=>{
            console.log(3)
            setTimeout(()=>{
                console.log(4)
                setTimeout(()=>{
                    console.log(5)
                },1000)
            },1000)
        },1000)
    },1000)
},1000)
形成回调地狱,可以进行代码的扁平化

function * gen(){
    setTimeout(()=>{
        console.log(1);
        it.next()           // 输出结束之后再调用下一部分
    },1000)
    yield "P2"
    setTimeout(()=>{
        console.log(2);
        it.next()
    },1000)
    yield "P3"
    setTimeout(()=>{
        console.log(3);
        it.next()
    },1000)
    yield "P4"
    setTimeout(()=>{
        console.log(4);
        it.next()
    },1000)
    yield "P5"
    setTimeout(()=>{
        console.log(5);
        it.next()
    },1000)
}

let it = gen();
it.next();

我们的需求变成五秒每秒输出一个前一个数+5的数

function * gen(p1){
    setTimeout(()=>{
        console.log(p1+5);
        it.next(p1+5)
    },1000)
    let p2 = yield "P2"
    setTimeout(()=>{
        console.log(p2+5);
        it.next(p2+5)
    },1000)
    let p3 = yield "P3"
    setTimeout(()=>{
        console.log(p3+5);
        it.next(p3+5)
    },1000)
    let p4 = yield "P4"
    setTimeout(()=>{
        console.log(p4+5);
        it.next(p4+5)
    },1000)
    let p5 = yield "P5"
    setTimeout(()=>{
        console.log(p5+5);
        it.next(p5+5)
    },1000)
}

let it = gen(666);      // 注意在这里传参
it.next();

对于流式工作我们可以这么写

function * gen(p1){
    setTimeout(()=>{
        console.log("do work 1");
    },1000)
    yield "P1 OK"
    setTimeout(()=>{
        console.log("do work 2");
    },1000)
    yield "P2 ER"
    setTimeout(()=>{
        console.log("do work 3");
    },1000)
    yield "P3 OK"
    setTimeout(()=>{
        console.log("do work 4");
    },1000)
    yield "P4 OK"
    setTimeout(()=>{
        console.log("do work 5");
    },1000)
    yield "ALL END"
}

let v = [];
let it = gen();      // 注意在这里传参
for(i of it){        // 当然,我们可以手动next方便传参
    v.push(i)
}
console.log(v)

结果是

[ 'P1 OK', 'P2 ER', 'P3 OK', 'P4 OK', 'ALL END' ]
do work 1
do work 2
do work 3
do work 4
do work 5

  • 生成器可以与Promise结合大大简化代码
  • yield是参考python的语法,在前端中用处不大,在后端中,就显得比较重要了,因为其优越的可控性,可是极大的提升线程的效率。

Promise

略,见Promise专门内容

将生成器与Promise结合来源

结合生成器(以及生成器暂停和恢复执行的能力)和promise,来实现更加优雅的异步代码。

try{
  const ninjas = syncGetJSON('data/ninja.json');
  const missions = syncGetJSON(ninjas[0].missionsUrl);
  const missionDetails = syncGetJSON(missions[0].detailsUrl);
  //Study the mission description
} catch (e) {
  //Oh no,we were not able to get the mission details
}
尽管这段代码对于简化错误处理很方便,但UI被阻塞了,用户不希望看到这个结果。则可以使用生成器和promise结合。从生成器中让渡后会挂起执行而不会发生阻塞。而且仅需调用生成器迭代器的next方法就可以唤醒生成器并继续执行。而promise在未来触发某种条件的情况下让我们得到它事先允诺的值,而且当错误发生后也会执行相应的回调函数。

这个方法将要以如下方式结合生成器和promise。

把异步任务放入一个生成器中,然后执行生成器函数。所以生成器执行的时候,我们会将执行权让渡给生成器,从而不会导致阻塞。过一会儿,当承若被兑现,我们会继续通过迭代器的next函数执行生成器。只要有需要就可以重复这个过程。

console.log('-------------将promise和生成器结合---------');
//返回异步结果的函数在等待异步结果返回时应当能够暂停,注意function*,使用生成器
async(function* () {
  try{
    //对每个任务均执行yield
    const ninjas = yield getJSON('data/ninjas.json');
    const missions = yield getJSON(ninjas[0].missionsUrl);
    const missionDescription = yield getJSON(missions[0].detailsUrl);
    //Study the mission description
  } catch (e) {
    //依旧可以使用标准的语言结构,诸如try-catch语句或循环语句
    //Oh no,we were not able to get the mission details
  }
});

//定义一个辅助函数,用于对我们定义的生成器执行操作
function async(generator) {
  //创建 一个迭代器,进而我们可以控制生成器
  var iterator = generator();

  //定义函数handle,用于对生成器产生的每个值进行处理。
  function handle(iteratorResult) {
    //当生成器没有更多结果返回时停止执行。
    if (iteratorResult.done) {
      return;
    }

    const iteratorValue = iteratorResult.value;
    //如果生成的值是一个promise,则对其注册成功和失败的回调。这是异步处理的部分。如果promise成功返回,则恢复生成器的执行并传入promise的返回结果。如果遇到错误,则生成器抛出异常。
    if (iteratorValue instanceof Promise) {
      iteratorValue.then(res =>{
        handle(iterator.next(res));
      }).catch(err =>{
        iterator.throw(err);
      });
    }
  }

  //重启生成器的执行
  try{
    handle(iterator.next());
  } catch (e) {
    iterator.throw(e);
  }
}

async函数内,我们声明了一个处理函数用于处理从生成器中返回的值——迭代器的一次“迭代”。如果生成器的结果是一个被成功兑现的承若,我们就是用迭代器的next方法把承诺的值返回给生成器并恢复执行。如果出现错误,承若被违背,我们就使用迭代器的throw方法抛出一个异常。直到生成器完成前,一直重复这几个操作。

注意:

这只是一个粗略的草稿,一个最小化的代码应该把生成器和promise结合在一起。不推荐生产环境下出现这些代码。

现在仔细看看这个生成器,在第一次调用迭代器的next方法后,生成器执行第一次getJSON('data/ninjas.json')调用。此次调用创建了一个promise,该promise会包含需要的信息。但是这个值是异步获取的,所以我们完全不知道浏览器会花多少时间来获取它。但我们明白一件事,我们不希望在等待中阻塞应用的执行。所以对于这个原因,在执行的这一刻,生成器让渡了控制权,生成器暂停,并把控制流还给了回调函数的执行。由于让渡的值是一个promise对象getJSON,在这个回调函数中,通过使用promise的then和catch方法,我们注册了一个success和一个error回调函数,从而继续了函数的执行。当浏览器接收到了响应(可能是成功的响应,也可能是失败的响应),promise的两个回调函数之一则被调用了。如果promise被成功解决,则会执行success回调函数,随之而来的是迭代器next方法的调用,用于向生成器请求新的值,从而向生成器请求新值,从而生成器从挂起状态恢复,并把得到的值回传给回调函数。这意味着,程序又重新进入到生成器函数体内,当第一次执行yield表达式后,得到的值变成从服务器端获取的信息。

下一行代码的生成器函数中,我们使用获取到的数据ninjas[0].missionsUrl来发起新的geJSON请求,从而创建一个新的promise对象,最后返回最新的数据。我们依然不知道这个异步任务会执行多久,所以我们再一次让渡了这次执行,并重复这个过程。只要生成器中有异步任务,这个过程就会重新执行一次。

这个例子有一点不同,但它结合了许多知识点:

  • 函数是第一类对象——我们向async函数了一个参数,该参数也是函数。
  • 生成器函数——用它的特性来挂起和恢复执行。
  • pomise——帮我们处理异步代码。
  • 回调函数——在promise对象上注册成功和失败的回调函数。
  • 箭头函数——箭头函数的简洁适合用在回调函数上。
  • 闭包——在我们控制生成器的过程中,迭代器在async函数内被创建,随之我们在promise的回调函数内通过闭包来获取该迭代器。

逻辑代码的书写如下方式:

getJSON("data/ninjas.json", (err, ninjas) =>{
  if (err) {
    console.log('Error fetchig ninjas', err);
    return;
  }

  getJSON(ninjas[0].missingsUrl, (err, missions) =>{
    if (err) {
      console.log("Error locating ninja missions", err);
      return;
    }
    console.log(missions);
  })
});

不同于把错误处理和流程中控制混合在一起,我们使用类似以下写法结束了代码的混乱:

async(function* () {
  try{
    //对每个任务均执行yield
    const ninjas = yield getJSON('data/ninjas.json');
    const missions = yield getJSON(ninjas[0].missionsUrl);
    //Study the mission description
  } catch (e) {
    //依旧可以使用标准的语言结构,诸如try-catch语句或循环语句
    //Oh no,we were not able to get the mission details
  }
});

最终结果结合了同步代码和异步代码的优点。有了同步代码,我们能更容易地理解、使用标准控制流以及异常处理机制、try-catch语句能力。而对于异步代码来说,我们有着天生的阻塞:当等待长时间运行的异步任务时,应用的执行不会被阻塞。

Set

与数组相似,但是成员唯一,支持...扩展展开,定义了iterator接口支持for-of遍历

  • 声明的时候可以向里面传输一个可迭代对象,一般我们用数组

  • s.size可以获取大小

  • s.add(v)可以添加元素v

  • s.delete(v)可以删除元素v

  • s.has(v)返回s中有没有v

  • s.clear()清空s

    let s = new Set()
    console.log(s,typeof s);            // Set(0) {} object
    
    let s2 = new Set([1,2,3,4,5,3,1])
    console.log(s2,typeof s2);          // Set(5) { 1, 2, 3, 4, 5 } object
    
    s2.add(666);
    console.log(s2);
    s2.delete(3);
    console.log(s2);
    console.log(s2.has(9))
    console.log(s2.has(5))
    console.log(s2,s2.size);
    s2.clear();
    console.log(s2,s2.size);

  • 数组去重

    let arr = [1,2,3,4,5,6,4,3,2,1];
    console.log([...new Set(arr)])

  • 求交

    let arr1 = [1,2,3,4,5,6,4,3,2,1];
    let arr2 = [5,6,7,8,9,8,7,6,5,4];
    let s1 = new Set(arr1);
    let s2 = new Set(arr2);
    let res = [];
    for(d of s1)
      if(s2.has(d))res.push(d);
    console.log(res);
    简化
    let arr1 = [1,2,3,4,5,6,4,3,2,1];
    let arr2 = [5,6,7,8,9,8,7,6,5,4];
    let s2 = new Set(arr2)
    let res = [...new Set(arr1)].filter((d)=>{          // 进行arr1去重
        return s2.has(d)                                // 使用filter判断交集
        // return arr2.indexOf(d)+1                     // 当然可以直接indexOf
    })
    console.log(res)
    语法简化
    let arr1 = [1,2,3,4,5,6,4,3,2,1];
    let arr2 = [5,6,7,8,9,8,7,6,5,4];
    let s2 = new Set(arr2)
    let res = [...new Set(arr1)].filter(d => s2.has(d))
    console.log(res)

  • 求并

    let arr1 = [1,2,3,4,5,6,4,3,2,1];
    let arr2 = [5,6,7,8,9,8,7,6,5,4];
    console.log([...new Set([...arr1,...arr2])])

  • 求补

    let arr1 = [1, 2, 3, 4, 5, 6, 4, 3, 2, 1];
    let arr2 = [5, 6, 7, 8, 9, 8, 7, 6, 5, 4];
    let s2 = new Set(arr2)
    let res = [...new Set(arr1)].filter(d => !s2.has(d))
    console.log(res)

Map

与C++的Map类似,键支持各种不固定数据类型,支持...,实现了迭代器支持for-of

let m = new Map();        // 定义一个map
let K3 = {"Name":"Liu"}   // 定义一个key

m.set("Key","value")      // 插入一个映射,两个参数分别是key,value
m.set("Key2",{"A":"B"})   // key/value 可以是基本数据类型,也可以是对象,包括函数
m.set(K3,()=>{console.log("OK")})

console.log(m)
// Map(3) {
//   'Key' => 'value',
//   'Key2' => { A: 'B' },
//   { Name: 'Liu' } => [Function (anonymous)]
// }

console.log(m.size)       // 大小
// 3

m.delete("Key")           // 删除
console.log(m,m.get(K3))  // 获取value
// Map(2) {
//   'Key2' => { A: 'B' },
//   { Name: 'Liu' } => [Function (anonymous)]
// } [Function (anonymous)]


m.get(K3)()               // OK

for(let v of m){          // 遍历
  console.log(v);
}
// [ 'Key2', { A: 'B' } ]
// [ { Name: 'Liu' }, [Function (anonymous)] ]

m.clear();
console.log(m);
// Map(0) {}

Map构造函数可以传入参数[[key1,val1],[key2,val2],[key3,val3],[key4,val4]]

class 类

ES引入了传统语言中类的写法,实际上ES6的Class就是一个语法糖,让我们写起来看着像一个传统的class

最简单的类定义

class Phone{
  constructor(brand,price){       // 构造函数,名字固定,new的时候执行
    this.brand = brand;
    this.price = price;
  }
  getVal(){                       // 成员方法 不能写function/()=>{}
    return `${this.brand} is ${this.price}`
  }
}

let phone = new Phone("LL",123)
console.log(phone.getVal())

静态成员

可以和Java一样设置类的静态属性

class Phone{
  static Name = "手机"            // 使用static
  constructor(brand,price){
    this.brand = brand;
    this.price = price;
  }
  getVal(){                       // 成员方法 不能写function/()=>{}
    return `${this.brand} is ${this.price}`
  }
}

let phone1 = new Phone("LL",123)
let phone2 = new Phone("RR",567)
console.log(phone1.Name)  // undefined
console.log(phone2.Name)  // undefined
console.log(Phone.Name)   // "手机"
静态成员属于类而不属于实例

继承

在ES5中继承的写法是

// 父类
function Phone(brand,price){
  this.brand = brand;
  this.price = price;
}

Phone.prototype.getVal=function(){
  return `${this.brand} is ${this.price}`
}

// 子类
function SmtPhone(brand,price,color,size){
  Phone.call(this,brand,price);   // 用this调用Phone的构造
  this.color = color;             // 独有的
  this.size = size;
}

SmtPhone.prototype = new Phone;    // 绑定父级方法
SmtPhone.prototype.constructor = SmtPhone;

SmtPhone.prototype.photo = function(){
  return "a photo";
}

let smtphone = new SmtPhone("A",1,2,3);
console.log(smtphone.getVal())
console.log(smtphone.photo())

ES6类的继承

class Phone{
  constructor(brand,price){       // 构造函数,名字固定,new的时候执行
    this.brand = brand;
    this.price = price;
  }
  getVal(){                       // 成员方法 不能写function/()=>{}
    return `${this.brand} is ${this.price}`
  }
}

class SmtPhone extends Phone{
  constructor(brand,price,color,size){
    super(brand,price);       // 与Java一样调用父类
    this.color = color;
    this.size = size;
  }
  photo(){
    return "A Photo";
  }
}

let smtphone = new SmtPhone("Mi",200,1,12)
console.log(smtphone.getVal())
console.log(smtphone.photo())

方法的重写

在js中,我们只能完全重写父类的方法,不能在重写后调用父类的方法

class Phone{
  constructor(brand,price){       // 构造函数,名字固定,new的时候执行
    this.brand = brand;
    this.price = price;
  }
  getVal(){                       // 成员方法 不能写function/()=>{}
    return `${this.brand} is ${this.price}`
  }
}

class SmtPhone extends Phone{
  constructor(brand,price,color,size){
    super(brand,price);       // 与Java一样调用父类
    this.color = color;
    this.size = size;
  }
  photo(){
    return "A Photo";
  }
  getVal(){     // 重写
    return `${this.brand} smart phone is ${this.price}`
  }
}

let smtphone = new SmtPhone("Mi",200,1,12)
console.log(smtphone.getVal())

Get和Set

可以对属性进行方法绑定,当要获取属性的时候执行对应get方法,当要设置属性值时执行对应的set方法, 这些属性不能在构造函数的时候被构造,看起来像一个虚拟变量

class GradeList{
  constructor(...grade){
    this.glst = grade;
    // 注意这里不能出现sumn,否则没法读取数据,sumn只能从get sumn获取,不过可以写_sumn方便保存
    this._sumn = null;
  }

  get sumn(){                    // 用于对对象的动态属性进行封装
    let tmp = 0;                 // 注意这里不能用sumn,否则每次for的时候都会跳转到get sumn 直接堆栈溢出
    this.glst.forEach((d,i)=>tmp+=d)
    this._sumn = tmp;            // 顺手保存一手,非必要
    return tmp;                  // 适当时候维护了sumn
  }

  get avg(){
    return this.sumn/this.glst.length
  }

  set sumn(d){                   // 必须有一个参数
    ++d;
    this._sumn = d;              // 进行一些处理,非必要,但是没有的话函数也没什么意义了
  }
}

let gst = new GradeList(1,2,3,4,5,6,7,8,9)

gst.sumn = 123;
console.log(gst._sumn)    // 124
console.log(gst.sumn)     // 45
console.log(gst.avg)      // 5
gst.avg = 456;
console.log(gst.avg)      // 5

如果设置属性为公有属性,set,get是无效的

class GradeList {
    testVal;                // 尝试注释这行
    constructor(...grade) {
        this.glst = grade;
        this._sumn = null;
    }

    get testVal() {
        console.log("Im ready to get")
        return 1
    }

    set testVal(d) {
        console.log("Im ready to set")
        d+=100;
    }
}

let gst = new GradeList(1,2,3);
gst.testVal=100;
console.log(gst.testVal);
结果
100
注释testVal后
Im ready to set
Im ready to get
1

数值扩展

  • Number.EPSILON 表示最小精度,用于浮点数的比较

  • 0b/0o/0x表示二进制八进制十六进制, o/b/x大小写均可

    console.log(0b1111);//15
    console.log(0o10);//8
    console.log(10);
    console.log(0x10);//16

  • Number.isFinite()判断是不是有限

     console.log(Number.isFinite(100)); //true
    console.log(Number.isFinite(100 / 0)); //false
    console.log(Number.isFinite(Infinity));//false

  • Number.isNaN()判断是不是Nan

    console.log(Number.isNaN(NaN)); //true
    console.log(Number.isNaN(123)); //false

  • Number.parseInt()字符串转为Int与Number.parseFloat()字符串转为Float

    console.log(Number.parseInt('5211314love')); //5211314
    console.log(Number.parseFloat('3.1415926hh')); //3.1415926

  • Number.isInteger() 判断一个数是否为整数

    console.log(Number.isInteger(5));   //true
    console.log(Number.isInteger(5.5)); //false

  • Math.trunc()抹掉数字的小数部分

    console.log(Math.trunc(3.555));  //3
    console.log(Math.trunc(0.555));  //0

  • Math.sign 判断一个数是正数还是负数还是0,有三个值 +:1, 0:0, -:-1

    console.log(Math.sign(100));  //1
    console.log(Math.sign(0));     // 0
    console.log(Math.sign(-100)); //-1

对象方法的扩展

  • Object.is()判断两个值是否相等, 作用与===相似,区别是is(NaN,NaN)有区别
    console.log(Object.is(1,1))               // True
    console.log(Object.is(1,2))               // False
    console.log(Object.is([1],[1]))           // False
    console.log(Object.is({A:"1"},{A:"1"}))   // False
    console.log(Object.is({A:"1"},{A:1}))     // False
    // 关于NaN
    console.log(Object.is(NaN,NaN))           // True
    console.log(NaN === NaN)                  // False
    console.log(Number("Hi"));                // NaN
    console.log(Number("Hi")===NaN)           // False
    console.log(Object.is(Number("Hi"),NaN))  // True
  • Object.assign()对象合并,可以合并对象, 例如Axios的时候三个配置文件合并可以写成
    let defaultConfig = {
      "url": "127.0.0.1",
      "port": 9000,
      method: "GET"
    }
    let config = {
      "url": "127.9.9.9",
    }
    config = Object.assign(defaultConfig,config,{"A":1})
    console.log(config)
    后者会覆盖前者
  • Object.setPrototypeOf()/Object.setPrototypeOf() 可以为对象设置原型
    let chd = {
      pos: "child",
      value: 1
    }
    let prt = {
      pos: "parent",
      value: 2
    }
    // 设置chd的原型是prt
    Object.setPrototypeOf(chd,prt);
    console.log(chd.__proto__)                // prt
    console.log(Object.getPrototypeOf(chd))   // prt

模块化

模块化可以防止命名冲突,直线代码复用,提高维护性

在ES6之前,JS本身没有模块化规范,社区推出的规范有

  • CommonJS规范: 这种规范的代表有NodeJS, Browserify
  • AMD规范: 这种规范的代表有requireJS
  • CMD规范: 这种规范的代表有SeaJS

ES6模块化

由两个命令构成

  • export 规定导出接口,与Node不太一样,只是前面加一个标识就可以了
  • import 导入其他模块

mod.js文件

export let name = "Hi"
export let nowis=()=> Date.now()
index.html
<script type="module">              // 要写type
  import * as m1 from "../mod.js"   // 导入为m1
  console.log(m1.nowis());          // 调用方法
</script>

暴露数据的方法

  • 分别暴露
    export let name = "Hi"
    export let nowis=()=> Date.now()
  • 统一暴露
    let name = "Hi"
    let nowis=()=> Date.now()
    export {name,nowis}
  • 默认暴露
    export default{
      name : "Hi",
      nowis: function(){return Date.now()}
    }
    默认暴露需要修改html
    <script type="module">
      import * as m1 from "../mod.js"
      console.log(m1.default.nowis())    // 多个default
    </script>

浏览器引用模块的方法

  • 通用引用方法
    <script type="module">
      import * as m1 from "../mod.js"
    </script>
  • 解构的方式引用
    <script type="module">
      import {name,nowis} from "../mod.js"
    </script>
    如果两个模块有同名函数,解构后会出现变量名重复的问题,可以使用as进行变量名的替换,不影响解构
    <script type="module">
      import {name,nowis} from "../mod1.js" // 有name nowis
      import {name as name2,lstis} from "../mod2.js" // 有name lstis
    </script>
  • 针对默认暴露的简便模式
    <script type="module">
      import m1 from "../mod1.js" // 因为默认暴露只有一个参数,可以这么做
    </script>

文件统一引用

可以把所有模块引用都放在一起,然后直接引用这个文件

app.js

import * as m1 from "../mod1.js"
import * as m2 from "../mod2.js"
import * as m3 from "../mod3.js"
index.html
<script type="module" src="./app.js">

将ES6代码转化为ES5代码

在项目中考虑到项目兼容性,需要将项目的JS进行转化,需要的工具有

  • Babel: 将ES6代码转化为ES5代码,但是是CommonJS规范
  • browserify: 打包工具,把CommonJS规范的JS转化为浏览器可读的,这里是简易打包,项目可能需要webpack
npm install babel-cli babel-preset-env browserify -D  // 软件分别是 命令行 工具 打包工具 -D是开发依赖
npx babel ./src -d ./dist/js --preset=babel-preset-env  // -d前是原js目录 -d后是输出目录 最后是使用预设
npx browserify dist/js/mod.js -o ./bundle.js          // 打包

浏览器最后引用./bundle.js

将npm包引入浏览器,jQuery为例

bash

npm install jquery
./src/js/app.js
import $ from "jquery";
$("body").css("background","pink");
bash
npx babel ./src/js -d ./dist/js --preset=babel-preset-env
npx browserify ./dist/js/app.js -o ./dist/bundle.js

ES7新特性

Array的includes

用来判断元素在不在数组

const s = ["AA","BB","CC","DD"]
console.log(s.includes("BB"))   // true
console.log(s.includes("EE"))   // false

**运算符

与python的**一样,用来算指数,Math.pow()功能一样

console.log(Math.pow(5,3))    // 125
console.log(5**3)             // 125

ES8新特性

async & await

async/await是一种新的异步函数同步解决方案

async

  • async可以放在函数前,定义async函数,这种函数的返回值是会被转化为Promise
  • async函数的返回值如果是是非Promise对象,那么会将return val的结果转化为Promise.resolve(val), 即使val是undefined
  • async函数中如果抛出异常,那么会返回Promise.reject
  • async函数如果返回Promise对象,那么会直接返回
  • 不能在全局直接声明async函数,实在不行可以写成
    (async()=>{})()
    的立即执行函数

await - await必须写在async函数中 - await右侧的表达式一般是Promise对象,也可以是正常值 - await的结果是Promise成功的,如果Promise的结果是reject,那么会抛出异常

详见Promise内容

对象方法的扩展

  • Object.keys()获取对象所有的键名,返回值是数组
    let lower = {
      "A":"a",
      "B":"b",
      "C":"c",
      "D":"d",
    }
    
    Object.keys(lower).forEach(d=>console.log(d))  // A B C D
  • Object.values()获取对象所有的,返回值是数组
    let lower = {
      "A":"a",
      "B":"b",
      "C":"c",
      "D":"d",
    }
    
    console.log(Object.values(lower))
    // [ 'a', 'b', 'c', 'd' ]
  • Object.entries()获取对象所有的键值对,返回值是数组,元素是一个数组,包含键和值, 可以用来构造Map
    let lower = {
      "A":"a",
      "B":"b",
      "C":"c",
      "D":"d",
    }
    console.log(Object.entries(lower))
    // [ [ 'A', 'a' ], [ 'B', 'b' ], [ 'C', 'c' ], [ 'D', 'd' ] ]
    let m = new Map(Object.entries(lower))
    console.log(m)
    // Map(4) { 'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd' }
  • Object.getOwnPropertyDescriptors()获取对象属性的描述对象,这个对象的每一个属性都对应描述中的一个对象,包括值,可写,可删除,可枚举,方便我们进行深层次的对象克隆
    let lower = {
        "name": "a",
        "age": 12,
        "sex": "F",
        "note": "good",
    }
    
    console.log(Object.getOwnPropertyDescriptors(lower))
    /**
    {
        name: { value: 'a', writable: true, enumerable: true, configurable: true },
        age: { value: 12, writable: true, enumerable: true, configurable: true },
        sex: { value: 'F', writable: true, enumerable: true, configurable: true },
        note: {
            value: 'good',
            writable: true,
            enumerable: true,
            configurable: true
        }
    }
    */

ES9新特性

REST参数与Spread扩展

在ES6中只有对数组的REST/Spread, 在ES9中支持对对象进行REST/Spread

REST

function cnnt({host,port,...args}){
    console.log(host);
    console.log(port);
    console.log(args);          // 对象的rest就是一个对象
}

cnnt({
    host: "127.0.0.1",
    port: 80,
    pwd: 123,
    type: "A"
})
// 127.0.0.1
// 80
// { pwd: 123, type: 'A' }

Spread

let Obj1={
    "name":"Liu"
}

let Obj2={
    "Sex":"M"
}

let Obj3={
    "Age":20
}

let res = {...Obj1,...Obj2,...Obj3}
console.log(res)
// { name: 'Liu', Sex: 'M', Age: 20 }

正则扩展

命名捕获分组

我们可以对政策匹配到的分组$1,$2...赋名,方面我们的使用

在之前

let str = '<iframe class="notranslate">Inner</iframe>';
// 想要获取class和标签内容要写两个()用来分组
let reg = /class="(.*)\".*>(.*)<\/iframe/;
let res = reg.exec(str);
console.log(res);
结果是
[
  'class="notranslate">Inner</iframe',
  'notranslate',
  'Inner',
  index: 8,
  input: '<iframe class="notranslate">Inner</iframe>',
  groups: undefined       // 这里是undefined
]
也就是res[0]是匹配结构,res[1]是第一个分组,res[2]是第二个分组

将需要赋值的分组括号由(条件)改为(?<变量名>条件),使用捕获分组会存储着groups中

let str = '<iframe class="notranslate">Inner</iframe>';
// 想要获取class和标签内容要写两个()用来分组
let reg = /class="(?<cls>.*)\".*>(?<Inn>.*)<\/iframe/;
let res = reg.exec(str);
console.log(res);
console.log(res.groups.cls);
console.log(res.groups.Inn);

结果是

[
  'class="notranslate">Inner</iframe',      // [0-2]都没有变
  'notranslate',
  'Inner',
  index: 8,
  input: '<iframe class="notranslate">Inner</iframe>',
  groups: [Object: null prototype] { cls: 'notranslate', Inn: 'Inner' }
  // groups变了
]
notranslate       // 可以直接输出了
Inner
就可以直接使用对应变量了,在修改正则的时候也不用修改下标了

反向断言

正向断言是匹配某个串要求不仅要满足串的条件,原串后面的内容也要满足指定条件,例如: 对于子复查u年'aaa111bbb222'我想要最后一个连续的字母,那么应该匹配的是[a-zA-Z]+并且后面是2实现方法是在正则条件后加上(?=后面的内容),这里要匹配的内容不用分组,因为我们这个方法就是要一次性匹配到结果

let str = 'aaa111bbb222';
let reg = /[a-zA-Z]+(?=2)/
let res = reg.exec(str);
console.log(res)
// [ 'bbb', index: 6, input: 'aaa111bbb222', groups: undefined ]

反向断言是要匹配一个串,满足串前面的内容是指定条件.实现方法是(?<=前面的内容)条件,例如我想匹配第一个出现的数字串

let str = 'aaa111bbb222';
let reg = /(?<=a)[0-9]+/
let res = reg.exec(str);
console.log(res)
// [ '111', index: 3, input: 'aaa111bbb222', groups: undefined ]

dotAll模式

在正则中.代表任意除\n外的任意内容,在提取有\n的内容的时候就显得不方便,只需要设置/条件/s即可,就是在最后加入s属性

.*用来匹配任意字符串的时候经常出现贪婪匹配,可以设置.*?禁止贪婪

ES10新特性

Object.fromEntries方法

  • Object.entries()可以获取对象所有的键值对,返回值是数组,元素是一个数组,包含键和值, 可以用来构造Map
  • Object.fromEntries()可以将一个Map/二维数组转化为对象的形式
let ary = [
    ['A','a'],
    ['B','b'],
    ['C','c'],
    ['D','d'],
];

let res = Object.fromEntries(ary);
console.log(res)
// { A: 'a', B: 'b', C: 'c', D: 'd' }

trimStart/trimEnd方法

在ES5中字符串有trim方法用来清除字符串两边的空白, 现在有start/end指定清除哪一边了空白

let s = '   abc     ';
console.log(s.trim(),s.trim().length)   //abc 3
console.log(s.trimStart(),s.trimStart().length)   //abc      8
console.log(s.trimEnd(),s.trimEnd().length)   //   abc 6

Array.flat/flatMap方法

flat译为平面,也就是可以将数组内部数组的元素维度降低,例如

let arr1 = [1,2,[3,4]];
let arr2 = [1,2,[3,4,[5,6]]];

console.log(arr1.flat());         // [ 1, 2, 3, 4 ]
console.log(arr2.flat());         // [ 1, 2, 3, 4, [ 5, 6 ] ]
console.log(arr2.flat().flat());  // [ 1, 2, 3, 4, 5, 6 ]
console.log(arr2.flat(2));        // [ 1, 2, 3, 4, 5, 6 ] 可以在括号中指定深度,默认1
flatMap可以将map的结果进行降维,这里的map和Map不同,类似于python的map做批量操作
let arr = [1,2,3,4];

let res1 = arr.map(item => item*10)
console.log(res1)
// [ 10, 20, 30, 40 ]
let res2 = arr.map(item => [item*10,item*10+1])
console.log(res2)
// [ [ 10, 11 ], [ 20, 21 ], [ 30, 31 ], [ 40, 41 ] ]
let res3 = arr.flatMap(item => [item*10,item*10+1])
console.log(res3)
// [ 10, 11, 20, 21, 30, 31, 40, 41 ]

Symbol.description方法

可以用Symbol.description方法获取Symbol的注释

let s = Symbol("我是一个注释")
console.log(s.description)  // 我是一个注释

ES11新特性

私有属性

在传统OOP语言中的对象可以是私有的,ES11引入了这个特性, 定义私有属性只要在前面加入#就可以了

class Psn{
    name;       // 公有的
    #age;       // 私有的
    constructor(name,age){
        this.name = name;
        this.#age = age;
    }
    getIt(){
        return {"N":this.name,"A":this.#age};
    }
}

let psn = new Psn("Liu",12);
console.log(psn.getIt());  // { N: 'Liu', A: 12 }
console.log(psn.name)      // Liu
console.log(psn.#age)      // Error

Promise.allSettled方法

可以指定一个变量为Promise.allSettled(), 他会运行一系列Promise,当全部运行结束后,不论他包含的Promise的结果是什么,状态都变为resolved,并保存每个Promise的结果

let p1 = new Promise((res,rej)=>{
    setTimeout(()=>{
        console.log("OK");
        res("Im P1");
    },1000);
})

let p2 = new Promise((res,rej)=>{
    setTimeout(()=>{
        console.log("NK");
        rej("Im P2");
    },1000);
})

let p3 = new Promise((res,rej)=>{
    setTimeout(()=>{
        console.log("OK");
        res("Im P3");
    },1000);
})

let result = Promise.allSettled([p1,p2,p3]);
console.log(result)
setTimeout(()=>console.log(result),5000)            // 延时显示等待Promise结束
结果是
Promise { <pending> }
OK
NK
OK
Promise {
  [
    { status: 'fulfilled', value: 'Im P1' },
    { status: 'rejected', reason: 'Im P2' },
    { status: 'fulfilled', value: 'Im P3' }
  ]
}

与他很像的是Promise.all()方法,他的结果是所有结果取AND,他们都用来做批量异步任务, all一般是用来做前一个成功后一个运行,allSettled一般是要全部运行, 之间没有关联, 要求保存结果

String.matchAll方法

如果正则表达式有/g标志,那么多次调用.exec()就会得到所有匹配的结果。如果没有匹配的结果,.exec()就会返回null。在这之前会返回每个匹配的匹配对象。这个对象包含捕获的子字符串和更多信息.如果正则表达式没有/g标志,.exec()总是返回第一次匹配的结果。

const matchIterator = str.matchAll(regExp);

给定一个字符串和一个正则表达式,.matchAll()为所有匹配的匹配对象返回一个迭代器。也可以使用一个扩展运算符...把迭代器转换为数组。

[...'-a-a-a'.matchAll(/-(a)/ug)]
//[ [ '-a', 'a' ], [ '-a', 'a' ], [ '-a', 'a' ] ]

现在是否设置/g,都不会有问题了。

[...'-a-a-a'.matchAll(/-(a)/u)]
// [ [ '-a', 'a' ], [ '-a', 'a' ], [ '-a', 'a' ] ]

可选链操作符号

在以前我们想访问一个对象很深的属性要进行多次尝试防止出现访问undefined.XXX,例如

let d = {
  name:{
    fname:{
      pub: true,
      value: "Liu"
    },
    lname:{
      pub : false,
    }
  }
}

let fnm = d.name&&d.name.fname&&d.name.fname.value    // 要一直尝试
let lnm = d.name&&d.name.lname&&d.name.lname.value    // 要一直尝试
console.log(fnm,lnm)    // Liu undefined

可选链的操作符是?.,例如A?.B就是A存在才去读取B,例如

let d = {
  name:{
    fname:{
      pub: true,
      value: "Liu"
    },
    lname:{
      pub : false,
    }
  }
}

let fnm = d?.name?.fname?.value
let lnm = d?.name?.lname?.value
console.log(fnm,lnm)    // Liu undefined

动态Import

动态导入支持按需加载模块/懒加载,而不是一股脑的在开头加载,语法是import("path"),返回的是一个Promise对象,当正确加载就resolve, resolve的值是模块暴露的对象

  • 静态导入
    // 文件头
    import * as md1 from "./demo.js"
  • 动态导入
    if(something){
      import("./demo.js").then((d)=>{
        d.xx();     // d就是暴露的对象
      })
    }

BigInt类型

比int类范围大,用于大数运算 - 字面值写法: 123n - Int转BigInt:BigInt(123) - BigInt不能和int进行运算,必须把int转为BigInt,例如

let i = 123n
console.log(i)              // 123n
console.log(i+10)           // Error
console.log(i+BigInt(10))   // 133n
### 绝对全局对象

浏览器的全局对象是window, 但是NodeJS等没有window, 在新的NodeJS/浏览器中都可以使用golbalThis指向全局对象, 使得在浏览器/NodeJS中编程得到了一个统一

// @Node
console.log(globalThis)
// <ref *1> Object [global] {
//   global: [Circular *1],
//   clearInterval: [Function: clearInterval],
//   clearTimeout: [Function: clearTimeout],
//   setInterval: [Function: setInterval],
//   setTimeout: [Function: setTimeout] {
//     [Symbol(nodejs.util.promisify.custom)]: [Getter]
//   },
//   queueMicrotask: [Function: queueMicrotask],
//   performance: [Getter/Setter],
//   clearImmediate: [Function: clearImmediate],
//   setImmediate: [Function: setImmediate] {
//     [Symbol(nodejs.util.promisify.custom)]: [Getter]
//   }
// }

// @Chrome
console.log(globalThis)
// Window {0: Window, 1: Window, window: Window, self: Window, document: document, name: "", location: Location, …}

ES12新特性

转载自: 前端虾米公社

replaceAll

看到replaceAll这个词,相比很容易联想到replace。在JavaScript中,replace方法只能是替换字符串中匹配到的第一个实例字符,而不能进行全局多项匹配替换,唯一的办法是通过正则表达式进行相关规则匹配替换

而replaceAll则是返回一个全新的字符串,所有符合匹配规则的字符都将被替换掉,替换规则可以是字符串或者正则表达式。

let string = 'I like 前端,I like 前端公虾米'
//使用replace
let replaceStr = string.replace('like','love')
console.log(replaceStr)  // 'I love 前端,I like前端公虾米'
//replace使用正则匹配所有
console.log(string.replace(/like/g,'love')) // 'I love 前端,I love 前端公虾米'
//使用replaceAll
let replaceAllStr = string.replaceAll('like','love')
console.log(replaceAllStr) // 'I love 前端,I love 前端公虾米'

需要注意的是,replaceAll在使用正则表达式的时候,如果非全局匹配(/g),则replaceAll()会抛出一个异常

let string = 'I like 前端,I like 前端公虾米'
console.log(string.replaceAll(/like/,'love')) //TypeError

Promise.any

当Promise列表中的任意一个promise成功resolve则返回第一个resolve的结果状态

如果所有的promise均reject,则抛出异常表示所有请求失败

Promise.any([
  new Promise((resolve, reject) => setTimeout(reject, 500, '哎呀,我被拒绝了')),
  new Promise((resolve, reject) => setTimeout(resolve, 1000, '哎呀,她接受我了')),
  new Promise((resolve, reject) => setTimeout(resolve, 2000, '哎呀,她也接受我了')),
])
.then(value => console.log(`输出结果: ${value}`))
.catch (err => console.log(err))

//输出
//输出结果:哎呀,她接受我了

再来看下另一种情况

Promise.any([
  Promise.reject('Error 1'),
  Promise.reject('Error 2'),
  Promise.reject('Error 3')
])
.then(value => console.log(`请求结果: ${value}`))
.catch (err => console.log(err))

//输出
AggregateError: All promises were rejected
Promise.any与Promise.race十分容易混淆,务必注意区分,Promise.race 一旦某个promise触发了resolve或者reject,就直接返回了该状态结果,并不在乎其成功或者失败

WeakRefs

使用WeakRefs的Class类创建对对象的弱引用(对对象的弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为)

当我们通过(const、let、var)创建一个变量时,垃圾收集器GC将永远不会从内存中删除该变量,只要它的引用仍然存在可访问。WeakRef对象包含对对象的弱引用。对对象的弱引用是不会阻止垃圾收集器GC恢复该对象的引用,则GC可以在任何时候删除它。

WeakRefs在很多情况下都很有用,比如使用Map对象来实现具有很多需要大量内存的键值缓存,在这种情况下最方便的就是尽快释放键值对占用的内存。

目前,可以通过WeakMap()或者WeakSet()来使用WeakRefs

举个栗子

我想要跟踪特定的对象调用某一特定方法的次数,超过1000条则做对应提示

let map = new Map()
function doSomething(obj){
 ...
}
function useObject(obj){
 doSomething(obj)

  let called = map.get(obj) || 0
  called ++

  if(called>1000){
     console.log('当前调用次数已经超过1000次了,over')
  }

  map.set(obj, called)
}

如上虽然可以实现我们的功能,但是会发生内存溢出,因为传递给doSomething函数的每个对象都永久保存在map中,并且不会被GC回收,因此我们可以使用WeakMap

let wmap = new WeakMap()
function doSomething(obj){
 ...
}
function useObject(obj){
 doSomething(obj)

  let called = wmap.get(obj) || 0

  called ++

  if(called>1000){
     console.log('当前调用次数已经超过1000次了,over')
  }

  wmap.set(obj, called)
}
因为是弱引用,所以WeakMap、WeakSet的键值对是不可枚举的

WeakSet和WeakMap相似,但是每个对象在WeakSet中的每个对象只可能出现一次,WeakSet中所有对象都是唯一的

let ws = new WeakSet()
let foo = {}
let bar = {}

ws.add(foo)
ws.add(bar)

ws.has(foo) //true
ws.has(bar) //true

ws.delete(foo) //删除foo对象
ws.has(foo) //false 已删除
ws.has(bar) //仍存在
WeakSet与Set相比有以下两个区别

WeakSet只能是对象集合,而不能是任何类型的任意值 WeakSet弱引用,集合中对象引用为弱引用,如果没有其他对WeakSet对象的引用,则会被GC回收 最后,WeakRef实例有一个方法deref,返回引用的原始对象,如果原始对象被回收,则返回undefined

const cache = new Map();

const setValue =  (key, obj) => {
  cache.set(key, new WeakRef(obj));
};

const getValue = (key) => {
  const ref = cache.get(key);
  if (ref) {
    return ref.deref();
  }
};

const fibonacciCached = (number) => {
  const cached = getValue(number);
  if (cached) return cached;
  const sum = calculateFibonacci(number);
  setValue(number, sum);
  return sum;
};

对于缓存远程数据来说,这可能不是一个好主意,因为远程数据可能会不可预测地从内存中删除。在这种情况下,最好使用LRU之类的缓存。

逻辑运算符和赋值表达式

逻辑运算符和赋值表达式,新特性结合了逻辑运算符(&&,||,??)和赋值表达式而JavaScript已存在的 复合赋值运算符有:

操作运算符:+= -= *= /= %= **= 位操作运算符:&= ^= |= 按位运算符:<<= >>= >>>= 现有的的运算符,其工作方式都可以如此来理解

表达式:a op= b 等同于:a = a op b

逻辑运算符和其他的复合赋值运算符工作方式不同

表达式:a op= b 等同于:a = a op (a = b)

  • a ||= b 等价于 a = a || (a = b)
  • a &&= b 等价于 a = a && (a = b)
  • a ??= b 等价于 a = a ?? (a = b)

为什么不再是跟以前的运算公式a = a op b一样呢,而是采用a = a op (a = b)。因为后者当且仅当a的值为false的时候才计算赋值,只有在必要的时候才执行分配,而前者的表达式总是执行赋值操作

  • ??=可用来补充/初始化缺失的属性
const pages = [
  {
  	title:'主会场',
    path:'/'
  },
  {
    path:'/other'
  },
  ...
]

for (const page of pages){
 page.title ??= '默认标题'
}
console.table(pages)
//(index)  title       		path
//0        "主会场"   	  "/"
//1        "默认标题"  	 "/other"

小结: - &&=: 当LHS值存在时,将RHS变量赋值给LHS - ||=: 当LHS值不存在时,将RHS变量赋值给LHS - ??=: 当LHS值为null或者undefined时,将RHS变量赋值给LHS

数字分隔符

数字分隔符,可以在数字之间创建可视化分隔符,通过_下划线来分割数字,使数字更具可读性

const money = 1_000_000_000
//等价于
const money = 1000000000

const totalFee = 1000.12_34
//等价于
const totalFee = 1000.1234
该新特性同样支持在八进制数中使用
const number = 0o123_456
//等价于
const number = 0o123456

JS模块化

概述

模块化就是将一个复杂的程序根据一定的规则封装成多个文件, 并进行组合. 使得模块内部数据/实现私有, 向外部暴露一些接口(方法)与外部模块通信

  • 早期没有模块化时, 所有代码写在一起, 十分容易污染global, 还会造成命名冲突(比如在一个模块定义了foo(),被另一个模块完全不知情的调用)
  • 之后有了简单的Namespace模式, 也就是将变量分类封装到不同的对象中, 使用对象.属性的方法调用, 虽然减少了全局变量, 但是变量仍然不安全, 外界可以直接访问变量
    let obj = {
      msg: "mod1",
      foo(){
        console.log("foo()",this.msg)
      }
    }
    obj.foo();
  • 后来发展出了IIFE模式, 也就是将功能封装到一个个立即执行函数里面, 外部无法访问内部的变量
    (function () {
      const msg = "mod1";
      function foo() {
        console.log("foo() ", msg);
      }
      foo();
    })()
  • 最后发展为引入依赖, 也就是将模块需要附着的对象传入模块, 在模块中将变量绑在传入对象上, 通过这种方式实现了按需加载与高可用性
    (function (window) {
      const msg = "mod1";
      function foo() {
        console.log("foo() ", msg);
      }
      window.module5 = {
        foo
      };
    })(window)
    module5.foo();

但是模块化也带来一些问题

  • 在调用外来库的时候每个库都要进行一次请求, 这样需要发送多个请求, 占用网络资源
  • 模块之间存在依赖关系(例如必须先引用jQuery然后再引用jQuery的扩展)

为了解决问题需要有模块化规范, 常见的模块化规范有

  • CommonJS: 以Node.js为代表
  • AMD:
  • CMS: 阿里内部规范, 不常用
  • ES6:

CommonJS

每个文件都是一个模块, 模块在服务端加载是运行时同步加载的, 在浏览器端加载的时候需要提前编译打包

  • 模块暴露:
    • exports.xxx = value;暴露多个
    • module.exports = value;暴露一个
    暴露的本质就是暴露exports对象, exports本身是一个空对象, 可以使用.来增加对象, 也可以直接修改对象的值, 但是要使用module.exports, 否则到时候module.export与exports指向的位置不同s
  • 引用模块: require()
  • 文档结构
    ├── index.js      // 入口JS文件
    ├── modules       // JS模块
    │   ├── module1.js
    │   ├── module2.js
    │   └── module3.js
    └── package.json  // 包文件

在服务器端想要使用CommonJS规范可以直接使用NodeJS中的require, 但是服务器端没有require我们需要将代码进行打包编译, 工具叫Browserify

使用方法: - npm init创建包文件package.json(注意包名不得有中文, 大写) - 创建文档结构

├── index.html      // 浏览器端测试文件
├── js              // js文件夹
│   ├── dist        // 转译打包后文件
│   └── src         // CommonJS源文件
│       ├── app.js  // 入口文件
│       ├── module1.js
│       ├── module2.js
│       └── module3.js
├── package.json
└── package-lock.json
- 安装browserify
sudo npm install browserify -g
npm install browserify --include=dev
注意, 全局与局部都必须安装. dev表示开发依赖, 也就是这个npm模块只在开发的时候使用, 在生产环境无需安装, dev依赖包会在json文件中标注
"devDependencies": {
  "browserify": "^17.0.0"
}
- 转译输出
browserify ./js/src/app.js -o ./js/dist/bundle.js
输出名可以随便写

AMD

AMD模块规范专门用于浏览器端, 为了防止阻塞, 模块加载是异步的,

基本语法 - 定义没有依赖的模块

define(function(){
  return 想要暴露的api;
})
- 定义有依赖的模块, 将依赖使用字符串数组的形式引入, 函数列表中的m1, m2分别对应了之前引用的依赖模块
define(['dep-mod1','dep-mod2'],function(m1,m2){
  return 想要暴露的api;
})

使用方法:

  • amd在浏览器端需要依赖一个库require.js, RequireJS是"A JAVASCRIPT MODULE LOADER", 也就是用来解析加载JS依赖, 使用require.js的优势是网页只需要引入一个JS文件, 其余依赖require.js会自行处理
  • 文档结构
    ├── index.html
    └── js                // 存放所有的JS文件
        ├── app.js        // 入口JS文件, 配置所有的模块位置
        ├── libs          // 放第三方库
            ├── jQuery.js // 比如我有一个jQuery
        │   └── require.min.js
        └── modules       // 自己的模块文件
            ├── mod1.js
            └── mod2.js
  • 模块编写格式形如
    define(['mod1'], function(mod1) {
        let val = 'mod2';
        function showVal(){
            console.log(val, mod1.getVal());
        }
        return {showVal};
    });
    这里的可以不必写mod1, 最后这个模块名与路径的映射还会单独在app.js中规定
  • 配置app.js
    (function () {        // 这里配置每一个模块的位置
      requirejs.config({
        baseUrl:'/js', // 基本的路径 出发点正在根目录下 不配置时从main.js出发去找
        paths:{
          mod1:'./modules/mod1', // 不要加.js 默认会添加后缀
          mod2:'./modules/mod2',
          jquery:'./libs/jQuery' // 也可以用三方库, 但是注意jQuery已经兼容AMD规范, 这时引用jQuery模块名必须叫做jquery
        }
      })
      requirejs(['mod2'],function(mod2){      // 在app.js中使用require.js引入模块的时候函数是requirejs不是require
        mod2.showVal();
      })
    })()
  • HTML引入, 指定入口文件
    <script data-main="js/main.js" src="./js/libs/require.js"></script>

CMD

CMD与AMD类似, 但是CMD模块只有在使用的时候才会异步加载, CMD的定义方式有点像CommonJS

define(function(require, exports, module){
  let mod1 = require("./mod1.js");    // 同步引入
  let mod2 = require.async("./mod2.js",(mod2)=>{
    mod2.foo();
  });    // 异步引入, 引入的回调
  exports.xxx = value;                // 导出
})

使用方法:

<script type='text/javascript' src="./node_modules/sea.js/sea.js"></script>
<script type='text/javascript'>
  seajs.use('./modules/app.js')
</script>

ES6

ES6还有很多语法浏览器不支持, 所以需要在上线时对数据进行打包编译为ES5, 方便识别, 编译工具是Babel, 打包工具是Browserify

语法

  • export 规定导出接口,与Node不太一样,只是前面加一个标识就可以了
  • import 导入其他模块

mod.js文件

export let name = "Hi"
export let nowis=()=> Date.now()

index.html

<script type="module">              // 要写type
  import * as m1 from "../mod.js"   // 导入为m1
  console.log(m1.nowis());          // 调用方法
</script>

暴露数据的方法

  • 分别暴露
export let name = "Hi"
export let nowis=()=> Date.now()
  • 统一暴露
let name = "Hi"
let nowis=()=> Date.now()
export {name,nowis}
  • 默认暴露
export default{
  name : "Hi",
  nowis: function(){return Date.now()}
}

默认暴露需要修改html

<script type="module">
  import * as m1 from "../mod.js"
  console.log(m1.default.nowis())    // 多个default
</script>

浏览器引用模块的方法

  • 通用引用方法
<script type="module">
  import * as m1 from "../mod.js"
</script>
  • 解构的方式引用
<script type="module">
  import {name,nowis} from "../mod.js"
</script>

如果两个模块有同名函数,解构后会出现变量名重复的问题,可以使用as进行变量名的替换,不影响解构

<script type="module">
  import {name,nowis} from "../mod1.js" // 有name nowis
  import {name as name2,lstis} from "../mod2.js" // 有name lstis
</script>
  • 针对默认暴露的简便模式
<script type="module">
  import m1 from "../mod1.js" // 因为默认暴露只有一个参数,可以这么做
</script>

文件统一引用

可以把所有模块引用都放在一起,然后直接引用这个文件

app.js

import * as m1 from "../mod1.js"
import * as m2 from "../mod2.js"
import * as m3 from "../mod3.js"

index.html

<script type="module" src="./app.js">

将ES6代码转化为ES5代码

在项目中考虑到项目兼容性,需要将项目的JS进行转化,需要的工具有

  • Babel: 将ES6代码转化为ES5代码,但是是CommonJS规范

  • browserify: 打包工具,把CommonJS规范的JS转化为浏览器可读的,这里是简易打包,项目可能需要webpack

  • 安装babel

    sudo npm install babel-cli browserify -g
    npm install babel-preset-es2015 -dev

  • 定义配置文件.babelrc文件

    {
      "presets": ["es2015"]
    }

  • 文档结构

    ├── js
    │   └── src
    │       ├── main.js
    │       ├── mod1.js
    │       ├── mod2.js
    │       └── mod3.js
    ├── package.json
    └── package-lock.json

  • 编译

    babel js/src -d js/lib

  • 打包

    browserify js/lib/main.js -o js/lib/bundle.js

浏览器最后引用./bundle.js

将npm包引入浏览器,jQuery为例

bash

npm install jquery

./src/js/app.js

import $ from "jquery";
$("body").css("background","pink");

bash

npx babel ./src/js -d ./dist/js --preset=babel-preset-env
npx browserify ./dist/js/app.js -o ./dist/bundle.js