什么是: 支持换行,单双引号,以及支持动态生成内容的字符串。
何时: 今后只要拼接字符串,都用模板字符串代替+拼接
为什么: +
拼接非常用和算数计算+混淆
如何 :
① 整个字符串都要包裹在一对反引号中
② 在``内,支持单双引号,支持换行!
③ 如果在字符串中部分内容需要用js程序动态生成,则必须放在${}
内
总结 ${}
中能放那些东西,不能放哪些东西:
if(){ ... } else if(){ ... } else{ ... }
while(){ ...} do{...}while();
switch(){case ; case ; ...}
for(){ ... }
var uname="dingding",
price=12.5, count=5,
sex=1,
orderTime=1578384907673;
console.log(`用户名:${uname}`);
console.log(`小计:¥${(price*count).toFixed(2)}`);
console.log(`性别: ${sex===1?"男":"女"}`)
console.log(`下单时间: ${new Date(orderTime).toLocaleString()}`)
什么是: 专门代替var用来声明变量的关键词
何时: 今后,只要声明变量都用let,声明常量依然使用const。但是ES6中的const也已具有了let的优点!
为什么:
var的缺陷:
① 声明提前: 打乱程序正常的执行顺序,造成歧义
② Js中没有块级作用域,块内的变量可能跑到块外影响外部的代码!
let的优势:
①阻止声明提前
②为js添加块级作用域
var t=0;//保存两个函数一共执行的时间
function fun1(){
console.log(`fun1()耗时0.3s`);
t+=0.3;
}
function fun2(){
//var t;undefined fun2内已经有了局部变量t
console.log(`fun1()耗时0.8s`);
t+=0.8;
if(false){//不是作用域,拦不住var t被提前
var t=new Date();
console.log(`出错啦,at:${t.toLocaleString()}`)
}
}
fun1();
fun2();
console.log(t)//0.3s 少加了fun2()中的0.8s
原理: 2个:
①let等效于自动添加匿名函数自调!
范围: 从let位置开始,到当前作用域结束位置
②let还会自动给变量改名: let t -> 翻译为 var _t
示例:
var t=0;//保存两个函数一共执行的时间
function fun1(){
console.log(`fun1()耗时0.3s`);
t+=0.3;
}
function fun2(){
console.log(`fun1()耗时0.8s`);
t+=0.8;
if(false){//墙: 变成块级作用域,能拦住let t影响外部!
//(function(){
let t=new Date();
//var _t=new Date();
console.log(`出错啦,at:${t.toLocaleString()}`)
//})();
}
}
fun1();
fun2();
console.log(t)//1.1s
① 禁止在let前提前使用该变量
② 同一范围内,同一个变量,只能let一次,不能重复let
③ 在全局let的变量,在window中找不到!
示例:
//不能在let声明变量前提前使用该变量
console.log(a); //报错: a不能再声明前访问
// (function(){
// var _a=10;
let a=10;
// })();
//在全局的let的变量,在window中找不到
//(function(){
let a=10;
console.log(window.a);//undefined
//})()
什么是: 专门对函数定义的简写语法!
何时: 今后在程序中几乎就看不到"function",都用箭头函数代替
如何:
①去掉functio
n,在()
和{}
之间加=>
② 如果形参列表只有一个形参,可省略()
③ 如果函数体只有一句话,可省略{}
如果仅有的这一句话还是return,则必须省略return
小心仅有的一句话结尾的 ;
去分号
箭头函数特点: 内外this相同
① 好处: 代替晦涩的bind,来永久替换函数内的this,与函数外的this保持一致!
②坏处: 不是所有地方都希望内外this相同!比如: 对象的方法就不希望内外this相同!
强调: 箭头函数只影响this,不影响函数内的局部变量!
示例:
var lilei={
sname:"Li Lei",
friends:["qq","ww","ee"],
//intr:function(){
intr(){//ES6中对对象方法的简写!与箭头函数无关!
//intr()内的this指向lilei,是正确的。因为lilei.intr()
//遍历当前李磊对象内的friends数组中每个朋友
this.friends.forEach(
//大多数回调函数中的this->window
//因为大多数回调函数都是自动调用
//调用时前边什么都没有!
//function(elem){
elem=> //箭头函数可保证内部的this与外部的this保持一致,都指向lilei
//LiLei 认识 qq
console.log(`${this.sname} 认识 ${elem}`)
//}
)
}
}
lilei.intr();
总结:
①如果希望内外this相同时,或函数本身与this无关时,可改为箭头函数
② 如果反而不希望内外this相同时,就不能改成箭头函数,得想别的办法去掉function
什么是: 专门简化普通for循环的
何时: 几乎所有能用普通for循环的地方,都可用for of
比如: 变量所有数字下标的对象时都可用普通for循环,自然也能用for of代替
包括: 索引数组, 类数组对象(arguments), 字符串
如何:
for(var i=0;i<arr.length;i++){
//位置: i
//元素值: arr[i]
}
Vs
for(var elem of 要遍历的数组/类数组对象/字符串){
//of会依次取出每个元素的值!
//elem直接取出元素值!
//无法获得正在遍历的位置i
}
问题:
① 默认情况下,无法获得遍历位置!
所以,如果循环时,需要获得当前位置,不能用for of,而应该退一步用普通for循环
② for of无法控制遍历的步调和顺序
所以,如果循环时,需要控制循环的步调或顺序,也不能用for of,而退一步用普通for循环
③ 如果想修改原数组中的每个元素值,而原数组中保存的是原始类型的值,也不能用for of遍历修改!因为of取出的元素值,都是副本!修改副本,不影响原数组中的内容
解决: 退一步用for循环,或用forEach
兼容性问题或错误: ? is not iterable
看到在这个错误,都说明,这个东西无法被for of遍历
2种情况:
① 对象错了,比如for of本来就不能遍历对象和关联数组
② 如果对象是数组/类数组对象/字符串,但是还出这个错,就是浏览器兼容性问题,说明浏览器版本太旧了!升级浏览器!
示例: 证明for of可以遍历什么,不能遍历什么
//for of遍历索引数组
var arr=[1,2,3,4,5];
//for(var i=0;i
for(var elem of arr){
console.log(elem)
//想对原数组中每个元素值*2
//错: elem*=2
}
//for of遍历类数组对象
function add(){
var sum=0;
//for(var i=0;i
for(var elem of arguments){
sum+=elem;
}
return sum;
}
console.log(add(1,2,3,4,5))
//for of遍历字符串
var str="hello";
//for(var i=0;i
for(var char of str){
console.log(char);
}
//for of能遍历对象吗?
var lilei={
sname:"Li Lei",
sage:11
}
// for(var key in lilei){
// console.log(`${key}:${lilei[key]}`)
// }
for(var prop of lilei){
console.log(prop)
}
参数默认值(default):
什么是: 即使调用函数时,没有指定实参值,形参变量也有预设的默认值可用
何时: 如果函数只有最后一个形参变量的值不确定时,使用默认值最有效!
如何: 定义函数时:
function 函数名(形参1, 形参2,..., 形参n=默认值){
如果调用函数时,没有给最后一个形参变量传值,则最后一个形参变量使用预设的=后的默认值执行后续操作
}
其实每个形参变量都可以用=指定默认值,但是,通常只有最后1个形参指定默认值才有意义!因为调用函数传参时,只可能末尾的参数变量不给值,不可能中间跳过个别参数不给值。
原理: ||的短路逻辑:
// function order(zs,xc,yl){
// yl=yl||"可乐";
function order(zs,xc,yl="可乐"){
console.log(`您点的餐是:
主食: ${zs}
小吃: ${xc}
饮料: ${yl}`)
}
order("香辣鸡腿堡","薯条")
order("香辣鸡腿堡","薯条","豆浆")
什么是: 专门用来代替arguments,处理不确定参数个数的情况
为什么: arguments的问题:
① 不是纯正的数组!数组家的函数一个都用不了!
② 箭头函数中禁止使用arguments
③ arguments只能获得所有实参值列表,不能有所选择!
何时: 只要使用arguments的地方,都可用剩余参数语法代替
如何:定义函数时:
function 函数名(形参1,形参2,...数组名){
调用时, 形参1接住实参值1,形参2接住实参值2,形参2之后的所有剩余参数,不管有多少,全部装进数组中保存!
其中: ...表示收集剩余参数值的意思!
}
剩余参数语法优势:
①剩余参数语法的"...数组
"得到的是一个纯正的数组,可以随便使用数组家函数
②箭头函数也支持剩余参数语法
③可以有选择的获得实参值列表中部分参数值,不一定非要获得所有参数值。
示例: 计算一个员工的总工资:
function calc(ename,...arr){
console.log(arr);
var total=arr.reduce(
(prev,elem)=>prev+elem,
0
);
console.log(`${ename}的总工资是:${total}`)
}
calc("Li Lei",10000,1000,2000)
calc("Han Meimei",4000,1000,2000,3000,4000);
//箭头函数不支持arguments
//换成var add=()=>{ 报错: arguments未定义
function add(){
var total=0;
for(var num of arguments){
total+=num;
}
return total;
}
console.log(add(1,2,3))//6
原理: (兼容旧写法)其实类数组对象可转为数组:
arr.slice(starti, endi+1)
含头不含尾arr.slice(starti)
var arr2=arr.slice()
var arr=Array.prototype.slice .call(arguments)
var arr=[].slice.call(arguments)
示例: 类数组对象完整转为数组,并求和:
//用slice复制整个数组的内容
var arr=[1,2,3,4,5];
var arr2=arr.slice(
//创建一个新数组
//var newArr=[]
//遍历原数组中每个元素
//for(var i=0;i
//每遍历一个元素值,就将当前元素值放入新数组中相同位置
//newArr[i]=this[i]
//}
//返回新数组
//return newArr
);
console.log(arr2);
console.log(arr==arr2);//false
function add(){
//希望用arguments代替slice中的this,骗slice帮我们把arguments的内容复制到一个数组中返回!
//var arr=Array.prototype.slice.call(arguments)
var arr=[].slice.call(arguments);
//argumenents.slice()
return arr.reduce((prev,elem)=>prev+elem);
}
console.log(
add(1,2,3),//6
add(1,2,3,4,5)//15
)
有选择的将类数组对象中部分元素转为数组:
var arr=[].slice.call(arguments,开始位置);
//arguments.slice(开始位置)
比如: [].slice.call(arguments,1);
//["Li Lei",10000,1000,2000].slice(1)
// 0 1 2 3
//[10000,1000,2000]
示例: 有选择的将类数组对象中部分元素转为数组
//用arguments实现计算薪资
function calc(ename){
//从arguments中1位置开始复制到结尾的剩余元素为新数组返回
var arr=[].slice.call(arguments,1);
//arguments.slice(1)
//["Li Lei",10000,1000,2000].slice(1)
// 0 1 2 3
//[10000,1000,2000]
var total=arr.reduce(
(prev,elem)=>prev+elem,
0
)
console.log(`${ename}的总工资是:${total}`)
}
calc("Li Lei",10000,1000,2000)
什么是: 专门将一个数组打散为单个元素,再传入后续操作
何时: 如果想要的值,是放在一个数组中给的,但是我们却希望单独使用,就可以先打散数组再单独使用!
如何: 调用函数时: fun(...arr)
原理: 先将数组arr打散为单个元素,再传入fun中作为多个实参值。
兼容写法: 用apply() fun.apply(任意对象, arr)
apply,可将arr数组打散为单个值,再传给函数fun。
如果fun执行过程与this无关,则apply()第一个实参值"替换this的对象",可写任意值。
示例: 想获得一个数组中的最大值:
console.log(Math.max(1,7,2,5));//7
//想获得数组中的最大值:
var arr=[1,7,2,5];
//console.log(Math.max(...arr));
console.log(
Math.max.apply( null ,arr)
) /*替换this的对象*/
//因为本例与this无关,单纯只想打散
数组arr,所以,apply的第一个参数可以写任意对象或值都行。
时髦的语法糖:
示例: 合并两个数组或对象
//拼接数组:
var arr1=[1,2,3];
var arr2=[4,5,6];
//var arr3=arr1.concat(arr2,7,8,9);
var arr3=[...arr1,...arr2,7,8,9];
console.log(arr3);
//拼接对象:
var obj1={x:1,y:2};
var obj2={a:3,b:4};
//var obj3=Object.assign({i:5,j:6},obj1,obj2);
var obj3={...obj1, ...obj2, i:5, j:6 }
console.log(obj3);
二维数组降维 arr=[].concat(...arr)
多维数组降维
function fun(arr){
//先降一次维
arr=[].concat(...arr);
//再检查降维后的数组中是否还包含子数组
var hasArray=arr.some(function(elem){
return Array.isArray(elem);
})
if(hasArray){ //如果包含子数组
arr=fun(arr);//就只能再降维一次,直到检查不再包含子数组为止
}
return arr
解构(destruct):
什么是: 从一个大的对象或数组中,仅提取出想要的一小部分成员单独使用!
为什么: 因为将来项目中有很多对象或数组特别大!内容特别多!但是,我们一次可能只用其中很少的几个功能或数据。如果总是带着巨大个的对象或数组使用,非常不方便
何时: 希望从一个大的对象或数组中,仅提取出想要的一小部分成员单独使用,就用解构。
包含三种: 数组解构,对象解构,函数参数解构
什么是: 从一个大的数组中提取个别元素值出来,单独使用
何时: 只要希望从一个大的数组中提取个别元素值出来,单独使用,就要用数组解构
如何: 下标对下标: 2步
①先将要接收数据的变量,装扮为一个数组的样子
②再使用赋值语法,将大的数组,赋值给装扮好的数组格式
var [变量1, 变量2, 变量3]=数组
结果: 等号左边数组格式中的每个变量,会自动获得等号右边原数组中相同下标位置的元素值, 比如:
变量1=数组[0]
变量2=数组[1]
… …
var arr=[2020,1,8,14,28,43];
// 0 1 2 3 4 5
// 年 月日 时 分 秒
// var y=arr[0];
// var m=arr[1];
// var d=arr[2];
//1. 将y,m,d装扮成数组的样子——并不是创建数组
//2. 将原数组赋值给装扮好的数组的样子
var [y ,m,d]=arr;
//结果: 等号左边数组格式中,对应下标的变量会自动收到原数组中相同位置的元素值!
//0:y = 0:2020
//1:m = 1:1
//2:d = 2:8
console.log(y,m,d)
//从此,在这个功能的代码中,y、m、d三个变量可脱离数组单独使用!
鄙视题: 不声明第三个变量交换两个变量的值:
var a=3, b=5;
[a,b]=[b,a]
[5,3]
console.log(a,b) //5,3
什么是: 从大的对象中提取出个别成员,单独使用
何时: 只要希望从大的对象中提取出个别成员,单独使用时,都用对象解构:
如何: 属性名对属性名,2步:
① 先将准备接受对象中属性值的变量装扮成对象的样子。
强调: 装扮成的对象中的属性名必须和原对象中的属性名匹配才行!
②再通过赋值的方式,将原对象赋值给装扮好的对象结构
var {属性名1:变量1, 属性名2:变量2,…}=原对象
结果: 等号左边对象结构中指定属性名的变量,会自动获得等号右边原对象中同名属性的属性值或方法!
简写: 如果属性名,刚好和要使用的变量名相同,可只写一个名字就够了。但是一个名字两用(既当属性名配对用,又当变量名今后单独使用)
var {属性名1, 属性名2, …} = 原对象
示例: 从user对象中解构出想要的属性和方法:
var user={
id:1,
uname:"dingding",
sex:1,
age:25,
login:function(){
console.log(`登录...`);
},
logout:function(){
console.log(`注销...`);
},
register:function(){
console.log(`注册...`);
},
changePwd:function(){
console.log(`修改密码...`)
}
}
<script src="9_user.js">
var user={
//...
}
</script>
</head>
<body>
<script>
//只想使用uname属性值和logout方法以及changePwd方法
// var {
// //配对 变量名
// //不可改 可改
// uname : uname,
// logout: logout,
// changePwd: changePwd
// }=user;
var {
//配对 变量名
//不可改 可改
uname, // : uname,
logout, //: logout,
changePwd //: changePwd
}=user;
//uname=user.uname="dingding"
//logout=user.logout=function(){...}
//changePwd=user.changePwd=function(){... }
console.log(`Welcome ${uname}`);
logout();
changePwd();
什么是: 其实就是在向函数传递参数时,也以对象解构的方式传递:
何时: 如果一个函数的多个形参都不确定有没有值,但是又要求实参值与形参变量一一对应时,就只能用参数解构。
如何: 2步:
定义函数时: 将形参变量列表装扮成对象的格式!
function fun({
// 属性名 形参变量
// 配对用 获得实参值
属性名1: 形参1,
属性名2: 形参2,
... : ...
}){
... ...
}
强调: 通常因为属性名和形参名一致,所以只写一个名字即可。一个名字两用(既当做配对的属性名,又当做接受实参值的形参变量名)。
function fun({
// 属性名也是形参变量
// 既配对用又获得实参值
属性名1,
属性名2,
...
}){
... ...
}
说明: 如果形参还有默认值:也可简写为属性名1=默认值
function fun({
// 属性名也是形参变量
// 既配对用又获得实参值
属性名1=默认值1,
属性名2=默认值2,
...
}){
... ...
}
调用函数传参时: 实参值列表也必须以相同的对象结构传递
fun({
属性名1: 实参值1,
属性名2: 实参值2,
... : ...
})
结果:
形参对象结构中指定属性名的形参变量,可自动去调用函数时传入的实参对象解构中相同属性名对应的实参值。
形参1=实参值1
形参2=实参值2
要求:
① 一旦函数要求采用参数解构的方式传值,则不允许再使用普通方式传值,必须采用对象结构传参。
② 调用函数传参时,实参对象结构中属性名必须和定义函数时形参对象结构中属性名保持一致!
③ 形参变量与实参值之间没有必然的顺序、个数要求。唯一要求,只要有形参变量的属性名能和实参值对象中的属性名对应即可,形参变量即可获得实参值。
特殊情况: 如果实参值对象结构中,缺少形参对象结构中所需的属性值,则不会报错,而是形参变量获得undefined而已。
示例: 实现一个点餐函数,可直接点套餐,也可更换套餐中任意一款商品
function order(
{//属性名 形参变量
// 配对 可单独使用的变量
zs="香辣鸡腿堡",//:zs,
xc="薯条",//:xc,
yl="可乐",//:yl
}
){
// zs=zs||"香辣鸡腿堡";
// xc=xc||"薯条";
// yl=yl||"可乐";
console.log(`您的订单:
主食: ${zs},
小吃: ${xc},
饮料: ${yl}`)
}
//都要换
order(
{
zs:"巨无霸",
xc:"土豆泥",
yl:"九珍果汁"
}
)
//都不换,都采用默认值
order({});
//只换饮料,其余两个保持默认值
order({
yl:"雪碧"
})
//想换小吃为红豆派,yl为奶茶
order({
xc:"红豆派",
yl:"奶茶"
})
什么是: 集中保存构造函数和原型对象方法的程序结构
为什么: 因为在旧js中,构造函数和原型对象方法本属于一家人,但是,却是分开写的!
示例:
//想创建一种类型描述所有学生的属性结构和功能
//包含2部分:
//1. 构造函数: 用于描述所有学生统一的属性结构,并负责反复创建学生类型的子对象
function Student(sname, sage){
this.sname=sname;
this.sage=sage;
}
//2. 原型对象: 集中保存所有学生类型子对象共有的方法
Student.prototype.intr=function(){
console.log(`I'm ${this.sname}, I'm ${this.sage}`)
}
//用Student类型创建一个学生子对象
var lilei=new Student("Li Lei",11)
console.log(lilei);
lilei.intr();
何时: 今后只要打算创建一种类型(构造函数+原型对象)时,都要改成创建class
如何: 3步:
① 用class{}
包裹原来的构造函数和原型对象方法
② 将构造函数名提升为整个class的名字,所有构造函数去掉function,改为constructor.
③ 所有原型对象方法不要再加构造函数.prototype前缀!
直接放入class中的方法,自动默认就是存在原型对象中。
Class中的方法,都无需加=function
坑: 但是,直接放在class中的属性,不会成为共有属性,而会复制到每个子对象中,成为子对象的自有属性!
示例:
class Student{
className="初一2班"
constructor(sname, sage){
this.sname=sname;
this.sage=sage;
}
intr(){
console.log(`I'm ${this.sname}, I'm ${this.sage}`)
}
}
//用Student类型创建一个学生子对象
var lilei=new Student("Li Lei",11)
console.log(lilei);
lilei.intr();
问题: 要求多个异步任务必须顺序执行!
错误的解决: 单纯先后调用两个函数,绝对无法保证两个异步函数顺序执行,因为异步函数只要调用就是不可控的。
解决: 用回调函数方式可以保证两个异步函数顺序执行
//定义一个形参,准备暂时保存住要执行的下一项任务!
function liang(next){
console.log(`亮起跑...`);
setTimeout(function(){//异步
console.log(`亮到达终点!`)
//在亮到达终点之后,才自动执行下一项任务
next();
},Math.random()*2*1000+1000)
// 1~3s之间取随机
}
function ran(){
console.log(`然起跑...`);
setTimeout(function(){//异步
console.log(`然到达终点!`)
},Math.random()*2*1000+1000)
// 1~3s之间取随机
}
//单纯调用两个异步函数,无法保证一定顺序执行
//liang();
//ran();
//正确: 让liang()先保住ran(),但是暂不执行ran()
liang(
/*next:*/
function (){
ran();
}
);
问题: 如果多个异步函数必须顺序执行,则会造成深层嵌套结构!——回调地狱现象
//定义一个形参,准备暂时保存住要执行的下一项任务!
function liang(next){
console.log(`亮起跑...`);
setTimeout(function(){//异步
console.log(`亮到达终点!`)
//在亮到达终点之后,才自动执行下一项任务
next();
},Math.random()*2*1000+1000)
// 1~3s之间取随机
}
function ran(next){
console.log(`然起跑...`);
setTimeout(function(){//异步
console.log(`然到达终点!`)
next();
},Math.random()*2*1000+1000)
// 1~3s之间取随机
}
function dong(next){
console.log(`东起跑...`);
setTimeout(function(){//异步
console.log(`东到达终点!`)
next();
},Math.random()*2*1000+1000)
// 1~3s之间取随机
}
function tao(next){
console.log(`涛起跑...`);
setTimeout(function(){//异步
console.log(`涛到达终点!`)
next();
},Math.random()*2*1000+1000)
// 1~3s之间取随机
}
liang(
/*next:*/
function (){
ran(
function(){
dong(
function(){
tao(
function(){
console.log(`比赛结束`)
}
)
}
)
}
);
}
);
什么是: 专门解决回调地狱问题,保证多个异步任何可以顺序执行的技术
何时: 只要多个异步任务需要顺序执行时,都用Promise
优势: 既可保证顺序执行,又不会产生嵌套结构!
如何:
①用new Promise(function(door){ 原异步函数内容 })包裹原异步函数的代码。
强调: 决定用Promise技术,就不要再给异步函数添加next()参数。
②在当前异步函数最后一句执行的语句之后,调用new Promise附赠我们的door()函数,开门,用于通知下一项任务可以开始执行。
③ 将new Promise()对象return返回到当前函数外部
④ 调用当前异步函数时,可用.then()接住下一项函数的对象,但是,暂不执行。
结果:
①调用当前异步函数时,做两件事:
//定义一个形参,准备暂时保存住要执行的下一项任务!
function liang(){
return new Promise(
function(door){
console.log(`亮起跑...`);
setTimeout(function(){//异步
console.log(`亮到达终点! 亮开门!`)
//在亮到达终点后开门!
door();
},Math.random()*2*1000+1000)
// 1~3s之间取随机
}
)
}
function ran(){
console.log(`然起跑...`);
setTimeout(function(){//异步
console.log(`然到达终点!`)
},Math.random()*2*1000+1000)
// 1~3s之间取随机
}
liang() .then(ran)
//return new Promise()
//同时已经在执行亮的任务了
// .........door()->自动调用ran()
示例: 多个异步任务顺序执行,也不会形成回调地狱
function liang(){
return new Promise(function(door){
console.log(`亮起跑...`);
setTimeout(function(){//异步
console.log(`亮到达终点! 亮开门!`)
//在亮到达终点之后,才自动执行下一项任务
door();
},Math.random()*2*1000+1000)
// 1~3s之间取随机
})
}
function ran(){
return new Promise(function(door){
console.log(`然起跑...`);
setTimeout(function(){//异步
console.log(`然到达终点! 然开门!`)
door();
},Math.random()*2*1000+1000)
// 1~3s之间取随机
})
}
function dong(){
return new Promise(function(door){
console.log(`东起跑...`);
setTimeout(function(){//异步
console.log(`东到达终点!东开门!`)
door();
},Math.random()*2*1000+1000)
// 1~3s之间取随机
})
}
function tao(){
return new Promise(function(door){
console.log(`涛起跑...`);
setTimeout(function(){//异步
console.log(`涛到达终点!涛开门!`)
door();
},Math.random()*2*1000+1000)
// 1~3s之间取随机
})
}
liang().then(ran).then(dong).then(tao).then(()=>console.log("比赛结束"))