JS18 - ES6 解构赋值、对象简写、展开/剩余运算、Promise 语法、async await 语法、模块化语法、class 类

391 阅读18分钟

ES6 - 解构赋值

基本内涵

ES6 中的解构赋值,主要针对于对象数组,左侧定义和右侧值类似的结构,这样声明几个变量,快速获取到每一个部分的信息。

数组解构赋值

应用 1 - 转换字符串格式

//"2020-9-8 9:55:12" 转换为 "2020年09月08日 09点55分12秒"
let timeStr = "2020-9-8 9:55:12";
let timeArr = timeStr.split(/\s|-|:/g);
let [year, month, day, hour, minute, second] = timeArr;      //数组解构赋值的语法
let timeTarget = [year, month, day, hour, minute, second];   //将解构赋值的新数组赋给一个变量以便调用
function supplyZero(value) {
    return value < 10 ? "0" + value : value;
}
let result = `${year}${supplyZero(month)}${supplyZero(day)}${supplyZero(hour)}${supplyZero(minute)}${supplyZero(second)}秒`;
console.log(result);    //2020年09月08日 09时55分12秒

应用 2 - 交换变量

//一般方法,交换变量
let num_1 = 10;
let num_2 = 20;
let temp;
temp = num_1;
num_1 = num_2;
num_2 = temp;
console.log(num_1,num_2);   //20 10
//通过解构赋值交换变量时,变量不能用 let 赋值,否则无法使用
var a = 10;
var b = 20;
var [b,a] = [a,b];
console.log(a,b);   //20 10

应用 3 - 扁平化多维数组

let multi = [1,2,[3,4,[5]]];    //多维数组
let [a,b,[c,d,[e]]] = multi;
console.log([a,b,c,d,e]);   //(5) [1, 2, 3, 4, 5]

对象解构赋值

应用 1 - 对象复制(key对应)

//对象解构赋值:只需要key对应即可,也可以把key-value写完整,并且完整写法可以避免与window属性重名
//原始对象
let obj = {
    sname:"James",
    age:18,
    location:{
        province:"Hongkong",
        city:"Jiulong"
    },
    hobby:["tennis","basketball","exercise"]
}

/* 使用对象属性 */
let {
    sname,
    age,
    location:{
        province,
        city
    },
    hobby:[hobby_1,hobby_2,bobby_3]
} = obj;
console.log(`${sname}, who likes ${hobby_1}${hobby_2}${bobby_3} in ${province} ${city}, is ${age}`);
//James, who likes tennis、basketball、exercise in Hongkong Jiulong, is 18

/* 复制对象 */
let newObj = {
    sname,
    age,
    location:{
        province,
        city
    },
    hobby:[hobby_1,hobby_2,bobby_3]
};
console.log(newObj);    //{sname: 'James', age: 18, location: {…}, hobby: Array(3)}

/* 对象解构赋值,可以采用组合的形式,如果第二个对象会覆盖第一个对象的值 */
let a = {
    aa:false,
    bb:{},
    cc:null,
    dd:4,
    ee:5
};
let b = {
    aa:1,
    bb:2,
    cc:3,
    dd:undefined,
    ee:4
};
let {aa,bb,cc,dd,ee} = {...a,...b};
console.log(aa,bb,cc,dd,ee);           //1 2 3 undefined 4

应用 2 - 接受参数

//在函数中使用对象简写,就是相当于使用了对象的解构赋值
function ob({name,age}){ //此处直接使用对象简写,就是使用的对象解构赋值
    console.log(name,age);
};
ob({name:"James", age:20}); // James 20 -> 相当于是 let {name, age} = {name:"James", age:20}

//后端数据
var obj = {
    sname:"James",
    age:18,
    location:"China"
}
/**问题分析:
 *  var {sname,age,location} = obj; //报错,因为location更改了window地址
 *  let {sname,age,location:myLocation} = obj;  //因为location是window属性,let声明就重复了
 *  以上代码运行后均报错,因为 location 默认属于window的属性
 *  但如果后端传递的数据就是location,那么解构赋值的时候,
 *  就需要在location后面加上value的变量名myLocation,
 *  这样可以避免与window的location重复 */
var {sname,age:myAge,location:myLocation} = obj;
console.log(sname);        
//James
console.log(age);           
/**报错ReferenceError: age is not defined,
 * 因为使用了myAge,这里的age就是obj对象的key,
 * 因为不是window对象的属性,直接调用就是undefined状态 */
console.log(obj.age);
//18
console.log(myAge);
//18
console.log(location);      
//window 属性
console.log(myLocation);    
/**China -> 这里是把后端数据的location通过myLocation传递给了当前的location,
 * 如果不使用myLocation,就相当于直接把China赋值给了window的location属性,
 * 这样就把网页地址给更改了,因此,加上一个中间变量可以在不更改后端key的前提下,
 * 通过解构赋值得到后端数据 */

ES6 - 对象简写

简写规则 1 - 省略 value

对象简写规则:如果对象的属性值是变量,并且变量的名字跟属性名相同,只需要写上key就可以。

//对象 - 不简写
function complete(sname,age){
    let obj = {
        sname:sname,
        age:age
    }
    return `name:${obj.sname} age:${obj.age}`;
}
//对象 - 简写
function simple(sname,age){
    let obj = {
        sname,
        age
    }
    return `name:${obj.sname} age:${obj.age}`
}
console.log(complete("James",25));  //name:James age:25
console.log(simple("Joker",28));    //name:Joker age:28

简写规则 2 - 简写方法

//一般写法
let obj = {
    sname:"James",
    getName:function (){
        return this.sname;
    }
}
console.log(obj.getName()); //James

//ES6 简写对象中的方法 - 即把key直接仿作方法名,value省略
let objSimp = {
    sname:"Joker",
    getName(){
        return this.sname;
    }
}
console.log(objSimp);
/* {sname: 'Joker', getName: ƒ}
     getName: ƒ getName()
     sname: "Joker"
 */ 
console.log(objSimp.getName()); //Joker

ES6 - 开展/剩余运算符

基本语法... 可以把数组和类数组结构拆分成以逗号分割的参数序列

  • 三个点末尾一般紧跟上一个数组/对象变量,这个变量一般就是数组/对象

数组应用展开运算符

应用 1 - 拼接数组

//展开运算符 - 拼接
let a = [1,2,3];
let b = [4,5,6];
let conbinate = [...a,...b];
console.log(conbinate);     //(6) [1, 2, 3, 4, 5, 6]

应用 2 - 复制数组

//展开运算符 - 复制
let a = [1,2,3];
let b = [...a];
b.shift();        
console.log(a); //(3) [1, 2, 3]
console.log(b); //(2) [2, 3]

应用 3 - 函数参数

//展开运算符 - 参数(必须放在最后一个形参的位置)
let a = [10,20,30,40];
//作为形参
let b = (x,y,...params) => {
    return params;
}
console.log(b(1,2,3,4,5));  //(3) [3, 4, 5]
//作为实参
console.log(b(...a));       //(2) [30, 40]

应用 4 - 求最大最小值

//展开运算符 - 参数(必须放在最后一个形参的位置)
let arr = [10,20,3,10,20,60,10,20,60,10,20,7,40];
//求最大值
console.log(Math.max(...arr));  //60
//求最小值
console.log(Math.min(...arr));  //3

应用 5 - 类数组转换

//展开运算符 - 类数组转换
//函数内置arguments参数的转换
function test(){
    let args = [...arguments];
    return args instanceof Array;
}
console.log(test());    //true
//元素节点集合的转换
let nodeArr = [...document.querySelectorAll("*")];
nodeArr.filter(item=>{
    item === document.querySelector("input") ? console.log(item.nodeType):null; //1
});

应用 6 - 拆分字符串为字符数组

let str = "string";
console.log(...str);    //s t r i n g
console.log([...str]);  //(6) ['s', 't', 'r', 'i', 'n', 'g']

对象应用展开运算符

应用 1 - 拼接对象

//展开运算符 - 对象拼接
let obj_1 = {
    sname:"James",
    age:28
}
let obj_2 = {
    sname:"Joker",
    work:"developer"
}
let objNew = {
    ...obj_1,
    ...obj_2
}
//拼接的对象,如果key是相同的,那么后面的会覆盖前面的
console.log(objNew);    //{sname: 'Joker', age: 28, work: 'developer'}

ES6 - Set 数据结构

ES6 - Map 数据结构

ES6 - Promise 语法

参见:es6.ruanyifeng.com/#docs/promi…

同步、异步、回调地狱

  • 同步程序:按照代码的书写顺序逐行执行,浏览器会在上一行完成后才执行下一行,这样的代码逻辑,在于每一行新代码都是建立在前面代码基础之上的,但如果其中一个环节的程序需要运行较长的时间,那么使用同步程序就会导致因为等待时间过长,而无法有效利用等待时间做其它的事情,降低了效率,但这个也是浏览器默认的执行方案
  • 异步编程:为了解决同步程序的等待问题,就有了异步函数编程的思想,异步函数是指具有某种专门功能的函数,其核心是通过调用一个函数来启动一个长效运行的程序功能,如果后面代码运行的时,遇到满足该函数的条件或传入了参数,那么该函数就开始操作并立即返回,这样我们的程序就可以保持对其他事件做出反应的能力,整个过程不妨碍整体代码的持续运行,因而在执行步骤上成了异步的状态。体现在代码上,就是在执行一个指令后不是马上得到结果,而是继续执行后面的代码,等到特定的事件触发或传递了相应参数后,在得到结果。
  • 传统异步编程的问题 - 回调地狱/厄运金字塔:由于异步编程是调用一个函数来启动长效的程序,因此需要传递一个参数,方可有效执行异步程序,即需要传递一个参数或者是回调函数(事件处理程序就是一个特殊的回调函数),但如果传递的参数也是一个异步函数,就需要给回调函数再传递参数,形成了异步函数的嵌套结构,这样一来,一旦业务逻辑层次增加,这种回调函数中嵌套回调函数的结构就变得非常缺乏可读性和维护性,形成“回调地狱”问题,也称为“厄运金字塔”(因为缩进看起来像一个金字塔的侧面)
//同步
function synchronization1(){
    console.log("synchronization - 1");
};
function synchronization2(){
    console.log("synchronization - 2");
};
function synchronization3(){
    console.log("synchronization - 3");
}
synchronization1();
synchronization2();
synchronization3(); 
//结果 1 2 3

//异步
function asynchronous1(){
    setTimeout(()=>{
        console.log(1);
    },3000);
}
function asynchronous2(){
    setTimeout(()=>{
        console.log(2);
    },2000);
}
function asynchronous3(){
    setTimeout(()=>{
        console.log(3);
    },1000);
}
asynchronous1();
asynchronous2();
asynchronous3();
//结果顺序 3 2 1
//传统解决方案 - 回调函数
function asynchronous1(){
    //先执行
    setTimeout(()=>{
        console.log(1);
        //再执行    
        setTimeout(()=>{
            console.log(2);
            //最后执行
            setTimeout(()=>{
                console.log(3);
            },1000);
        },2000);
    },3000);
}
asynchronous1();
//结果顺序 1 2 3
//传统解决方案 - 回调函数
// -> 这种方式虽然能够实现异步编程的有序可控,但是形成了厄运金字塔问题
function asynchronous1() {
    //先执行
    setTimeout(() => {
        console.log(1);
        //再执行    
        setTimeout(() => {
            console.log(2);
            //最后执行
            setTimeout(() => {
                console.log(3);
            }, 1000);
        }, 2000);
    }, 3000);
}
asynchronous1();
//厄运金字塔问题,也常常体现在ajax调用上面
$.ajax({
    success() {
        $.ajax({
            succes() {
                $.ajax({
                    succes() {
                        //...
                    }
                })
            }
        })
    }
})

Promise 概念

  • 含义:Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大
  • 本质:Promise 是一个对象,可以获取异步操作的信息,因此可以专门用来处理回调地狱问题,因此,大多数现代异步 API 都不使用回调,并且事实上,JavaScript 中异步编程的基础就是 Promise 语法
  • 功能:Promise 对象,可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。

Promise 特点

Promise 的三个状态:pending、fullfilled、rejected

  • 对象的状态不受外界影响:Promise 对象代表一个异步操作,有三种状态:pending(进行中)fulfilled(已成功)rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
  • 缺点:(1)无法取消,一旦新建 Promise 就会立即执行,无法中途取消;(2)如果不设置回调函数,Promise内部抛出的错误,不会反应到外部;(3)当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

Promise 语法

let promise = new Promise(resolve, reject){
    // ... 异步操作的代码
    if(/* 异步操作成功 */){
        resolve(res);
    } else {
        reject(err);
    }
}
  • 参数/函数 resolve:resolve() 的作用是将 Promise 对象的状态从 pending 变为 fullfilled(resolved),在异步成功时调用,并将异步操作的结果,作为参数传递出去
  • 参数/函数 reject:reject() 的作用是将 Promise 对象的状态从 pending 变为 rejected,在异步失败时调用,并异步操作报出的错误,作为参数传递出去
  • 以上两个回调函数,逻辑上的功能是完全相同的,只是开发中约定的不同

then() 方法

  • 功能:then() 方法是 Promise 状态改变时调用的回调函数,不一定调用了resolve()
  • 参数:then() 方法可以接受两个回调函数作为参数,第一个回调函数是 Promise 对象的状态变为 fullfilled(resolved) 时调用,第二个回调函数是 Promise 对象的状态变为 rejected 时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。
  • 返回:then() 方法返回的是一个新的 Promise 实例(注意,不是原来那个Promise实例)。因此可以采用链式调用写法,即 then() 方法后面再调用另一个 then() 方法。
  • 链式调用注意事项:如果 Promise 要实现 then() 链式调用,需要在 then() 的回调函数中 return 一个重新实例化的 Promise 对象,否则 then() 返回的将会是一个空的Promise对象,就无法实现链式调用该有的功能
//then 使用第一个回调
let promise = new Promise((resolve, reject) => {
    setTimeout(()=>{
        console.log("asynchronous");
        resolve("success");
    },1000);
}).then(res=>{
    console.log("then: " + res);
});
//asynchronous
//test.html:12 then: success
//then 使用两个回调
let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("asynchronous");
        // resolve("定时器成功的数据"); //-> 谁在前就执行谁,因此 resolve 和 reject 不能放在一起
        reject("异步失败");
    }, 1000);
}).then(res => {
    console.log(res);
}, err => {
    console.log(err);   //输出:异步失败 -> 可见reject逻辑功能与resolve相同,只是约定使用场景不同
});
//then 链式调用,需要在 then 的回调函数中 return 一个重新实例化的 Promise
let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("asynchronous");
        resolve("我是第一个定时器的数据");
    }, 1000);
}).then(res => {
    console.log(res);
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            resolve("我是第二个定时器的数据");
        },1000);
    });
}).then(res => {
    console.log(res);
});
//输出结果:
//  asynchronous
//  我是第一个定时器的数据
//  我是第二个定时器的数据

catch() 方法

  • 功能Promise.prototype.catch() 方法是 .then(null, rejection).then(undefifined, rejection) 的别名,用于指定发生错误时的回调函数
  • Promise 的错误冒泡:Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获
  • 区别then的第二个参数:主要却别是,如果在 then 的第一个函数里抛出了异常,后面的 catch 能捕获到,而 then 的第二函数捕获不到
new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("asynchronous");
        // resolve("定时器成功的数据");
        reject("异步失败");
    }, 1000);
}).then(res => {
    console.log(res);
}, err => {
    console.log(err);   //输出:异步失败
});
//----------- 等同于 ------------
new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("asynchronous");
        // resolve("定时器成功的数据");
        reject("异步失败");
    }, 1000);
}).then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);   //输出:异步失败
});
let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        throw new Error("抛出异步异常");    //catch 不能捕获,因为异步是成功的
    }, 1000);
}).then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);
});
//---------------- Promise 状态改变,就不会再变 --------------
let promise = new Promise((resolve, reject) => {
    throw new Error("抛出异步异常");    //直接抛出异常,相当于模拟异步失败,此处状态从pending变为rejected,状态一边就不会再变
}).then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);   //Error: 抛出异步异常 -> catch 能够捕获Promise中的异常
});

Promise.all() 方法

  • 功能Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
  • 语法const p = Promise.all([p1, p2, p3]);
  • 参数:要求数组,或具有 Iterator 接口的对象;p1p2p3需要都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
  • 状态 - fullfilled:p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数
  • 状态 - rejected:p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数
  • 执行:根据 p 的最终状态决定执行 then 还是 catch
let p1 = new Promise((resolve, reject) => {
    resolve('成功了')
});
let p2 = new Promise((resolve, reject) => {
    resolve('success')
});
let p3 = Promise.reject('失败')
//-------------- all() 方法 fullfilled ---------------
Promise.all([p1, p2]).then((result) => {
    console.log(result) //['成功了', 'success']
}).catch((error) => {
    console.log(error)
});
//-------------- all() 方法 rejected ---------------
Promise.all([p1, p3, p2]).then((result) => {
    console.log(result)
}).catch((error) => {
    console.log(error) // 失败了,打出 '失败'
})

特殊情况

/**下面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是
 * 一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成
 * resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定
 * 的回调函数,而不会调用catch方法指定的回调函数。
 * 如果p2没有自己的catch方法,就会调用Promise.all()的catch方法
 */
let p1 = new Promise((resolve, reject)=>{
    resolve("success");
}).then(res=> res).catch(err=>err);
let p2 = new Promise((resolve, reject )=>{
    reject("fail");
}).then(res=>res).catch(err=>err);
Promise.all([p1,p2])
.then(res=>{
    console.log(res);   //输出:(2) ['success', 'fail']
}).catch(err=>{
    console.log(err);
});

Promise.race() 方法

  • 功能Promise.race() 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
  • 语法const p = Promise.race([p1, p2, p3]);
  • 参数:要求数组,或具有 Iterator 接口的对象;p1p2p3需要都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
  • 状态:只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变,由于Promise的状态改变后就不会再变,因此,率先改变的 Promise 实例的返回值,就传递给p的回调函数。
  • 执行:根据 p 的最终状态决定执行 then 还是 catch
let p1 = new Promise((resolve, reject) => {
    setTimeout(_=>{
        resolve("p1 - success");
    },2000);
});
let p2 = new Promise((resolve, reject) => {
    setTimeout(_=>{
        resolve("p2 - success");
    },500);
});
let p3 = new Promise((resolve, reject) => {
    setTimeout(_=>{
        reject("p3 - fail");
    },1000);
});
Promise.race([p1, p2, p3])
    .then(res => {
        console.log(res);   //p2 - success -> 因为p2计时器最先执行
    })
    .catch(err => {
        console.log(err);
    });

ES6 - async await 语法(ES7)

  • 含义:async 是“异步”的简写,而 await 可以认为是 async wait 的简写。
  • 功能async 用于声明一个异步函数 ,而 await 用于等待一个异步函数执行完成。让异步函数的编写如同写同步函数。
  • async 返回值async 函数 会返回一个 promise 对象,如果在函数中 return 一个值,那么该值会通过Promise.resolve() 传递出去,因此,async 函数的返回值就是 promise,return 的默认值是 resolve 的值。
  • await 等待值:await 等待一个异步函数执行完成,相当于等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定非要等待哪个对象)。在代码体现上,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
  • 本质:async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。
//async 定义异步函数 -> sync await 是语法糖,因此不会额外增加功能,定义的异步函数,需要开发者手动定义
async function foo(){
    let a = await p1(); //阻塞
    let b = await p2(); //阻塞 -> await 等待的值可以是任何值,如果不是异步函数,就会失去意义,即函数体一般加异步操作,但也不排斥同步,但是不建议,会给其他开发者带来困扰
    //--- 要等到异步的阻塞结束之后,才会输出 ---
    console.log(a);
    console.log(b);  //因为await异步阻塞,a b 两个值要等 3s 后才一起输出
}
foo();
function p1(){
    return new Promise((resolve,reject)=>{
        setTimeout(_=>{
            resolve("aaa");
        },2000);
    });
};
function p2(){
    return new Promise((resolve,reject)=>{
        setTimeout(_=>{
            resolve("bbb");
        },1000)
    });
};
//因为 async 的返回值是 promise,并且处于是pending状态的promise,所以也可以使用 then 来处理
async function foo() {
    let a = await p1(); 
    let b = await p2(); 
    // console.log(a);
    // console.log(b);
    return {a,b};   //需要 return 语句
}
foo().then(res => {
    console.log(res);   //3s 后输出 {a: 'aaa', b: 'bbb'}
});

ES6 - Module 语法

参见:es6.ruanyifeng.com/#docs/modul…

  • 在使用模块化语法之前,常常会有诸多问题,例如,代码私密度不足、重名、依赖等问题,通过ES6的模块化语法,可以较好地解决这些问题,但ES6也存在者兼容性的问题,对于这一点,可以通过webpack将ES6降低为ES5,解决ES6的兼容性问题。
  • 特点:ES6 模块不是对象,而是一个独立的文件,通过 export 命令用于规定模块的对外接口,显式地指定输出的代码,再通过 import 命令输入其他模块提供的功能

export 语法

  • 基本用法 - export:导出的一定是个完整的变量声明语句,同时导出要使用大括号 export{变量名1, 变量名2}
//导出单个 -> export 变量/函数声明语句等,不可使用变量名 -> 相当于导出这个运算关系
export var firstName = "James";
export var salaries = [2000, 3500, 5000, 6000];
export function getName(name){
    return this.name;
}

//导出多个 -> export{ 函数名、变量等,大括号中可以直接使用变量 }
var firstName = "James";
var salaries = [2000, 3500, 5000, 6000];
function getName(name){
    return this.name;
}
export { firstName, salaries, getName };

//export 改名 - 需要使用 as 关键字 -> { 原名 as 改名 }
function v1() { /* function code...*/ }
function v2() { /* function code...*/ }
export {
    v1 as streamV1,
    v2 as streamV2,
    v2 as streamLatestVersion
}; //重名后,v2 函数可以使用不同地名字输出两次

//export 默认 - 导出的是一个值,不是一个声明语句
export default function(){ console.log("foo") };
//以上同 -> export { function(){ console.log("foo")} as default }
//相应的 -> import { default as anyName } from "./file.js"; 或 import anyName from "./file.js";
var name = "James";
export default name;
//以上同 -> export { name as default }; -> 如果是 export {var name = "James" as default } 会直接编译错误
//相应的 -> import { default as anyName } from "./file.js"; 或 import anyName from "./file.js";

import 语法

  • 基本用法 - import:引入的时候注意,路径名如果是当前目录需要加 ./ 不然引入的就是核心模块 注意:
  • 注意只读:import命令输入的变量都是只读的,因为它的本质是输入接口,也就是说,不允许在加载模块的脚本里面,改写接口
  • 语法提升:import命令具有提升效果,会提升到整个模块的头部,首先执行,这种行为的本质是,import命令是编译阶段执行的,在代码运行之前
//导入单个
import { firstName } from "./profile.js";

//导入多个
import { firstName, lastName, year } from "./profile.js";

//导入默认 - 不加 {} 表示导入 import 的默认导出
import anyName, {firstName, lastName} from "./profile.js";

//import 改名 - 需要 as 关键字,可指定改哪个的名
import { firstName as surName, lastName as familyName, year } from "./profile.js";

//import 只读 - 输入的本质是输入接口,因此是只读的
import {a} from './xxx.js'
a = {};             // Syntax Error : 'a' is read-only;
a.foo = 'hello';    // 如果a是一个对象,改写a的属性是允许的,但不推荐,因为难以查错

//import 文件 - 导入文件不需要 export 前提,直接导入即可
import "./index.css";
import "../index.js"
//语法提升
foo();
import { foo } from 'my_module';
//上面的代码不会报错,因为import的执行早于foo的调用

js 私密性不足

问题还原:

//js 引入部分
function playA() {
    console.log("对外调用 - 功能 A " + _comInner());
}
function playB() {
    console.log("对外调用 - 功能 B " + _comInner());
}
function _comInner() {   //函数名前面加下划线,表示尽量不要在外部访问,只在本文件内部调用,但只是君子协定
    var command = "command";
    return command;
}

<!-- html部分 -->
<head>
    <!-- 引入外部js -->
    <script src="cont.js"></script>
</head>
<body></body>
<script>
    //对于 playA 和 playB 两个函数,允许直接调用
    playA();    //对外调用 - 功能 A command
    playB();    //对外调用 - 功能 B command
    //但对于 cont.js 的内部函数,不允许被调用,还是能够被调用,私密性就不足
    console.log(_comInner());    //command
</script>

关键代码:

//导出单个 -> 暴露的方法,待后面导入
export 变量/函数声明语句 //相当于导出这个运算关系

//导出多个
export{
    //要导出的函数方法、变量等;大括号中可以直接使用变量
}

//导入
<script type="module">
    import { //要导入的函数方法} from "./code1.js" //如果路径直接写名字,默认是从核心模块(node_modules)引入,vue中常见 
    import { //要导入的函数方法} from "./code2.js"
    ...
</script>

改进后代码:

//cont.js 部分
var number = 20
let playA = _ => console.log("对外调用 - 功能 A " + comInner() + number);
let playB = _ => console.log("对外调用 - 功能 B " + comInner() + number);
let comInner = _ => {
    var command = "command";
    return command;
}
//导出
export { playA,playB }

//dis.js 部分
var number = 30;
let playC = _ => console.log("对外调用 - 功能 C " + comInner() + number);
let playD = _ => console.log("对外调用 - 功能 D " + comInner() + number);
function comInner() {
    var command = "command other";
    return command;
}
//导出
export { playC,playD }

<!-- html部分 -->
<head>
    <!-- 引入外部js -->
    <!-- 导出/导入的时候,都需要加 type="module" 否则报错
         SyntaxError: Unexpected token 'export' 
         或
         SyntaxError: Cannot use import statement outside
    -->
    <script src="cont.js" type="module"></script>
    <script src="dis.js" type="module"></script>
</head>
<body></body>
<script type="module">
    //导入
    import { playA,playB } from "./cont.js";
    import { playC } from "./dis.js";
    //开始调用,可见未导出(暴露)或导入(引用)的的代码就不可访问了
    playA();
    playB();
    playC();
    playD();  //Uncaught ReferenceError: playD is not defined
    console.log(comInner());    //现在就不行访问了 -> Uncaught ReferenceError: comInner is not defined
</script>

js 重名被覆盖

内部调用函数的重名被覆盖 - export/import

//cont.js 部分
var number = 20
function playA() {
    console.log("对外调用 - 功能 A " + comInner() + number);
}
function playB() {
    console.log("对外调用 - 功能 B " + comInner() + number);
}
function comInner() {
    var command = "command";
    return command;
}

//dis.js 部分
var number = 30;
function playC() {
    console.log("对外调用 - 功能 C " + comInner() + number);
}
function playD() {
    console.log("对外调用 - 功能 D " + comInner() + number);
}
function comInner() {
    var command = "command other";
    return command;
}

<!-- html部分 -->
<head>
    <!-- 引入外部js -->
    <script src="cont.js"></script>
    <script src="dis.js"></script>
</head>
<body></body>
<script>
    /**重名问题:
     * 在引入的 cont.js 和 dis.js 文件中,
     * 如果有两个变量或函数分别在各自的文件中,命名相同,
     * 那么就会按照引入js的先后顺序,后者覆盖前者的变量或函数
     */ 
    playA();    //对外调用 - 功能 A command other30
    playB();    //对外调用 - 功能 B command other30
    playC();    //对外调用 - 功能 C command other30
    playD();    //对外调用 - 功能 D command other30
</script>

解决办法:只需要加上模块化引用即可,使两个js文件的导出和导入独立开始,即可解决js内部调用的重名问题,这种办法也可解决外部调用函数重名的问题,只是一般情况下外部调用函数,都在会 import 部分引入要调用的函数,而此时,如果import两个文件的相同名称的函数,会报错,因此外部调用函数需要使用as关键字

//cont.js 部分
var number = 20
let playA = _ => console.log("对外调用 - 功能 A " + comInner() + number);
let playB = _ => console.log("对外调用 - 功能 B " + comInner() + number);
//仅在本js文件内部调用
function comInner() {
    var command = "command";
    return command;
}
//导出
export { playA, playB }

//dis.js 部分
var number = 30;
let playC = _ => console.log("对外调用 - 功能 C " + comInner() + number);
let playD = _ => console.log("对外调用 - 功能 D " + comInner() + number);
//仅在本js文件内部调用
function comInner() {
    var command = "command other";
    return command;
}
//导出
export { playC, playD }

<!-- html部分 -->
<head>
    <!-- 引入外部js -->
    <script src="cont.js" type="module"></script>
    <script src="dis.js" type="module"></script>
</head>
<body></body>
<script type="module">
    //导入
    import { playA, playB } from "./cont.js"
    import { playC, playD } from "./dis.js"
    //解决重名问题:关键字 as
    playA();    //对外调用 - 功能 A command20
    playB();    //对外调用 - 功能 B command20
    playC();    //对外调用 - 功能 C command other30
    playD();    //对外调用 - 功能 D command other30
</script>

外部调用函数的重名被覆盖 - as

关键代码:(结合export和import)

//导出
export{
    //要导出的函数方法
}

//导入
<script type="module">
    //例如,重名的方法是test(),可以通过 as 将重名的方法命名成不同的名称,在调用的时候只需要调用不同名称即可,但调用test就会报错了
    import { otherFn, test as A_test} from "./code1.js"
    import { otherFn, test as B_test} from "./code2.js"
    //...
    
    A_test();  //返回 test() 方法,在 A.js 中的代码
    B_test();  //返回 test() 方法,在 b.js 中的代码
</script>

改进后代码:

//cont.js 部分
var number = 20
let playA = _ => console.log("对外调用 - 功能 A " + comInner() + number);
let playB = _ => console.log("对外调用 - 功能 B " + comInner() + number);
//仅在本js文件内部调用
function comInner() {
    var command = "command";
    return command;
}
//供外部调用的函数
var playCom = _ => console.log("This is cont.js");
//导出
export { playA, playB, playCom }

//dis.js 部分
var number = 30;
let playC = _ => console.log("对外调用 - 功能 C " + comInner() + number);
let playD = _ => console.log("对外调用 - 功能 D " + comInner() + number);
//仅在本js文件内部调用
function comInner() {
    var command = "command other";
    return command;
}
//供外部调用的函数
var playCom = _ => console.log("This is dis.js");
//导出
export { playC, playD, playCom }

<!-- html部分 -->
<head>
    <!-- 引入外部js -->
    <script src="cont.js" type="module"></script>
    <script src="dis.js" type="module"></script>
</head>
<body></body>
<script type="module">
    //导入
    import { playA, playB, playCom as playCom_cont } from "./cont.js"
    import { playC, playD, playCom as playCom_dis } from "./dis.js"
    //解决重名问题:关键字 as,调用as关键字后面的名字
    playCom_cont(); //This is cont.js
    playCom_dis();  //This is dis.js
    playCom();      //Uncaught ReferenceError: playCom is not defined
</script>

js 依赖易混乱

//cont.js 部分
function playA(num) {
    num += 10;
    return num;
}

//dis.js 部分
function playC(num) {
    num *= 100;
    return num;
}

//depend.js 部分
var number = 5;
function calc(num){
    return playA(playC(num));
}

<!-- html 部分 -->
<head>
    <!-- 引入外部js -->
    <script src="dipend.js"></script>
    <script src="cont.js"></script>
    <script src="dis.js"></script>
</head>
<body></body>
<script>
    /**依赖易混乱问题:
     * 在引入的 cont.js 和 dis.js 文件中,如果还有第三个js文件 depend.js,
     * 而这个depend.js 想直接调用 cont.js 和 dis.js 的方法时(例如,封装好的冒泡排序),
     * 就是 depend.js 依赖于另外两个,此时,如果引入js的顺序,是另外两个在前,
     * depend.js 在后,就能够正常加载,而一旦引入顺序改变,那么就会报错变量还未定义,
     * 如果引入的js文件,特别多,那么就很容易导致依赖混乱问题
     */ 
     //在depend.js部分就报错了 --> Uncaught ReferenceError: playA is not defined
</script>

关键代码:在第三方js中导入要依赖的js文件

//depend.js 部分
// 导入
import { playA } from "./cont.js";
import { playC } from "./dis.js";

改进后代码:

//cont.js 部分
function playA(num) {
    num += 10;
    return num;
}
//导出
export { playA }

//dis.js 部分
function playC(num) {
    num *= 100;
    return num;
}
//导出
export { playC }

//depend.js 部分
// 导入
import { playA } from "./cont.js";
import { playC } from "./dis.js";
var number = 5;
function calc(num){
    return playA(playC(num));
}
console.log(calc(number));  //直接输出510

<!-- html 部分 -->
<head>
    <!-- 引入外部js -->
    <script src="depend.js" type="module"></script>
    <script src="cont.js" type="module"></script>
    <script src="dis.js" type="module"></script>
</head>
<body></body>
<script>
    //输出结果在deepend.js文件中就已经有了
</script>

使用 export default 默认导出,优化代码

//depend.js 部分
// 导入
import { playA } from "./cont.js";
import { playC } from "./dis.js";

var number = 5;
function calc(num){
    return playA(playC(num));
}
// console.log(calc(number));  //直接输出510

//默认导出 -> 在没有指定导出其它方法的时候,就默认导出这个方法
export default calc;

<!-- html 部分 -->
<head>
    <!-- 引入外部js -->
    <script src="depend.js" type="module"></script>
    <script src="cont.js" type="module"></script>
    <script src="dis.js" type="module"></script>
</head>
<body></body>
<script type="module">
    //输出结果在deepend.js文件中就已经有了
    import any from "./depend.js"
    console.log(any(2));    //由于使用了默认导出,所以导入的时候可以书写任意内容,使用的都是同一方法
</script>

ES6 - class 类

参见:es6.ruanyifeng.com/#docs/class…

基本语法

  • 本质:class(类)是构造函数的语法糖,因此,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
  • 等同构造函数:类的数据类型就是函数,类本身就指向构造函数。使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
  • 方法定义在原型:构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。
//class 中的构造方法
class Students{
    constructor(x,y){  //构造方法
        this.x = x;    //实例对象
        this.y = y;
    }
}
console.log(typeof Students);   //function
console.log(Students === Students.prototype.constructor); //true
//方法定义在原型上(包括构造方法)
    //在类中定义的所有方法默认都是定义在原型上的,因此等同于 Students.prototype.toString();
    //这也说明了为什么 Students === Students.prototype.constructor 为 true
    //同时也说明了,为什么每个原型对象中有个属性constructor指向构造函数
class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

constructor 方法

  • constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。
class Point {

}
// ----------------- 等同于 -----------------
class Point {
    constructor() {}
}

extends 和 super - class 继承

  • class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。
class Car{
    constructor(name, price){
        this.name = name;
        this.price = price;
    };
    drive(){
        return "can drive";
    };
}
//继承
class Benz extends Car{
    constructor(name, price, luxury){
        super(name, price);     //构造方法中 super 当作函数使用
        this.luxury = luxury;  
    }
    drive(){
        return super.drive();   //动态方法中 super 当作对象使用
    }
}
class Maserati extends Car{
    constructor(name, price, great){
        super(name, price);
        this.great = great;
    }
}
//方法是定义在原型上的,因此可以直接调用
let carBenz = new Benz("宝马", 200000, "luxury");
let carMaserati = new Maserati("玛莎拉蒂", 800000, "great");
console.log(carBenz.name,carBenz.price,carBenz.luxury,carBenz.drive());
//宝马 200000 luxury can drive
console.log(carMaserati.name,carMaserati.price,carMaserati.great,carMaserati.drive());
//玛莎拉蒂 800000 great can drive

ES6 规定,子类必须在constructor()方法中调用super(),否则就会报错。

为什么子类的构造函数,一定要调用super()? 原因:ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类,如果无法继承父类,也就不能正确构建子类实例,this关键字也不能用了。这意味着,新建子类实例时,父类的构造函数必定会先运行一次,并且只有调用super()之后,才可以使用this关键字

class Point { /* ... */ }
class ColorPoint extends Point {
    constructor() {
        //... 未调用 super() 
    }
}
let cp = new ColorPoint(); // ReferenceError
class Foo {
    constructor() {
        console.log("father");
    };
};
class Bar extends Foo {
    constructor() {
        super();
        console.log("son")
    }
}
let bar = new Bar();    // farther son