ECMASCPIT 新特性

550 阅读25分钟

ECMAScript 概述

什么是ECMAScript

ECMAScript 是一门脚本语言,缩写为es,通常把它看作是Javascript 的标准规范。但实际上Javascript是ECMAScript的扩展语言,ECMAScript 只提供了基本的语法。而JavaScript实现了ECMAScript的标准,并且在这个基础之上做了一些扩展,例如在浏览器环境中用到的bom 和 dom对象,node 环境中读写文件的操作。总的来说,在浏览器环境中的JavaScript = ECMAScript + web api,如下图所示:

浏览器中的javascript的构成
在node环境中的 JavaScript = ECMAScript + node api,如下图所示:

JavaScript语言的本身就是ECMAScript

ECMAScript版本迭代

从 2015年开始保持每年迭代一次,也引入了许多的新特性,因此这门语言也变得越来越高级。es版本发型概览如下图所示:

从es5到es6经过6年的时间才被标准化,这6年也是web发展的黄金时期,因此es6的出现也带来了比较多的东西值得我们去一一比较学习,在此之后es 就每一年发布一个版本,发布的更加频繁,符合当下互联网小步快跑的精神 。

ES2015 概述

是最新ECMAScript 标准的代表版本,简称es6,泛指从ES2015 之后的版本

  • 相比于ES5.1,变化比较大
  • 命名规则发生变化,以发行年份命名

有4大类的变化

1.解决原有语法的一些问题和不足。比如 let 和 const 块级作用域

2.对原有语法进行增强。例如:解构,展开,模板字符串等

3.全新的对象,全新的语法,全新的功能。例如 promise,proxy,Object.assign ...

4.全新的数据类型和数据结构。例如 symble,set,map...

ES2015 运行环境

最新的浏览器和node环境都可以支持,学习中使用node 环境 + nodemon 工具 ,可以修改完之后马上执行

nodemon 安装方法

1、在终端中输入 yarn add nodemon --dev (ps:没有 yarn 的自行安装)

2、在终端中输入 yarn nodemon 文件路径 即可运行代码

let 和 const 块级作用域

作用域: 1.全局作用域 (ES5) 2.函数作用域 (ES5) 3.块级(ES6)

什么是块级作用域

if 或 for 中的花括号中都会产生块级作用域,以前块是没有单独的作用域,在块里面定义的变量,在外面也可以访问的到。 示例:

if (true){
    var name= "linhuan";
}
console.log(name);

结果可以看到在块中定义的变量在外面也能被访问到,这样是不安全的

var 改为 let

    if (true){
       // var name= "linhuan";
       let name= "linhuan"
    }
    console.log(name);

结果中可知道块级作用域是不能被外部访问的

问题剖析及解决

场景1、for 循环嵌套计数器同名的问题

    for (var i=0;i<3;i++){
      for (var i=0;i<3;i++){
          console.log(i);
      }
      console.log("内层循环结束:"+i);
  }

结果

原因:内层循环的计数器i 覆盖了外层的i

解决办法:使用let 声明 计数器 i

for (let i=0;i<3;i++){
    for (let i=0;i<3;i++){
        console.log(i);
    }
    console.log("内层循环结束:"+i);
}

结果一共运行了9次,达到了预期效果

最佳实践:不要使用同名计数器

场景2、循环注册事件,在处理事件中要循环的访问计数器

//定义一个数组,数组中定义三个空的对象,用来表示页面的元素
var elements = [{},{},{}];
// 模拟为每一个对象添加onclick事件
 for(var i =0;i<elements.length;i++){
    //为每一个元素定义一个事件
    elements[i].onclick=function(){
        console.log(i);
    }
}
elements[0].onclick();
elements[1].onclick();
elements[2].onclick(); 

结果: 输出打印了三个 3 ,与预期的不符合

原因: 打印的i都是全局作用域的 i,待循环结束后 调用onclick方法时,i被累加到了3

解决办法: 1.借助闭包,消除了全局作用域产生的影响

for(var i =0;i<elements.length;i++){
    //为每一个元素定义一个事件
    elements[i].onclick=(function(i){
       return function() {
         console.log(i);
       }
    })(i)
}
elements[0].onclick();
elements[1].onclick();
elements[2].onclick();
//结果:0 1 2

  1. let 让这个只能在块级作用域内被使用,实现机制也是利用了闭包。调用onclick的时候for循环已经结束了,i早就销毁了
for(let i =0;i<elements.length;i++){
    //为每一个元素定义一个事件
    elements[i].onclick=function(){
        console.log(i);
    }
}
elements[0].onclick();
elements[1].onclick();
elements[2].onclick();
//结果:0 1 2

场景3、for 循环 有两层嵌套的作用域,内层是for循环体的作用域,外层是for的作用域

// 使用let 的计数器 i 和内部定义的i 作用域不同,互相之间不会发生影响
 for(let i =0;i<3;i++){
    let i="foo";
    console.log(i);
} 
//结果 打印 foo foo foo

场景4、var 变量声明的提升,let 没有

console.log(foo);//undefined
var foo="foo";
console.log(name);// let 抛出异常 ReferenceError: Cannot access 'name' before initialization
let name="lh"; 

const 常量/恒量

  1. 在let的基础上多了一个只读的特性,声明过后就不能被修改
const name="lh";
name="hz"; //结果 TypeError: Assignment to constant variable.
  1. 不能先声明变量,再赋值
const name;  // 结果 SyntaxError: Missing initializer in const declaration
name="hz";
  1. 声明变量后不能修改是指,不能修改变量的内存地址,并不是不能修改恒量中的属性成员
const obj ={}
obj.name='linh';
console.log(obj) // 输出 { name: 'linh' }
obj ={};// TypeError: Assignment to constant variable.  改变了内存地址指向了

总结,目前变量声明有 var,let,const.

最佳实践 不用 var ,主用 const,配合 let。原因是var使用的特性是陋习,默认使用const可以明确代码中的成员会不会被修改

数组解构

直接上代码

// 之前
const array=[1,2,3];
// const a =array[0]
// const b =array[1]
// const c =array[2]

//现在
//const [a,b,c] = array;

//console.log(a,b,c); // 1 2 3
//只保留其中一部分成员,形式还是要保持逗号隔开
const [ , b,  ] = array;
console.log(b);// 2
// 提取剩余参数的,只能在最后一个参数使用
const [a,...rest] =array;
console.log(rest); // [ 2, 3 ]
//解构数组的成员个数小于被解构数组,按照从左到右的顺序提取
const [aa] = array;
console.log(aa); // 1
//解构数组的成员个数大于被解构数组,输出undefined
const [a1,b1,c1,more] = array
console.log(more);//undefined
//设置默认值
const [a2,b2,c2,more2='default value'] = array
console.log(more2);//default value

const path ='/foo/bar/baz';
//const tmp = path.split('/');
//console.log(tmp[0],tmp[1],tmp[2]);
const [,rootDir] =path.split('/');
console.log(rootDir);  // foo

数组解构,获取数组的数据更方便。

对象解构

大部分和数组的一致,除了以下:

  • 通过属性名解构
  • 解构变量和作用域中其他的变量重名是,使用别名 具体使用见代码
//通过属性名解构
const obj ={name:'linh',age:29};
 //const {name} =obj;
 //console.log(name); //linh
//解构变量和作用域中其他的变量重名是,使用别名
let name ="obj";
//const {name} =obj; //SyntaxError: Identifier 'name' has already been declared
const {name:objName} =obj
console.log(objName);

// 解构console.log
const {log} = console;
log(1);
log(12);
log(122);
log(1222);

好处:简化代码,较少代码体积

模板字符串

使用反引号 ` ,有以下两个特性:

  1. 支持换行,非常适合输出HTML 字符串
  2. 支持变量,数学运算 的插值 ${} ,拼接到字符串中
  3. 模板字符串可以使用一个标签函数来处理,可以处理一些逻辑转换,文本的多语言化,检查字符串中是否存在,还可以开发一个模板引擎
//模板字符串
// 反引号 转义
//支持换行,非常适合输出HTML 字符串
const str =`hello \`string\` 

I am linhuan`;
console.log(str);
// 支持变量,数学运算 的插值 ${} ,拼接到字符串中
const name='lim';
const sayHi = `hello,${name} ,age:${10+8}`
console.log(sayHi);
//定义一个模板字符串标签处理函数
// strings 接收的模板字符串中的静态字符串的数组
//借助标签函数可以处理一些逻辑转换
function myTagFn(strings,userName,age){
    console.log(strings,userName,age);// [ 'hey,', ',age is ', '' ]  
    const isAdult=age>=18?'成年人':'未成年人';
    return strings[0]+userName+strings[1]+age+strings[2]+isAdult;
}
//声明变量
const userName='lili'
const age =18
const result = myTagFn`hey,${userName},age is ${age},我是一名 ` ; 
console.log(result);//hey,lili,age is 18,我是一名 成年人

字符串扩展方法

  • includes() ,是否包含xxx
  • startsWith(),是否以xxx开头
  • endsWith(),是否以xxx结尾 它们是一组方法,可以用来方便的判断字符串中是否有指定的内容,相比于使用indexOf 或正则 ,会更加便捷
//字符串扩展方法,相比于使用indexOf 或正则 ,会更加便捷
const message ="Error: foo is not defined."
console.log(
    message.startsWith("Error"), //是否以xxx开头 
    message.endsWith('-'),//是否以xxx结尾
    message.includes('foo')//是否包含xxx
)  // true false true

参数默认值

  • 默认值只有当调用时没有传递实参,或 实参为 undefined 时会被使用
  • 注意点:当有多个参数时,带默认值的参数要放在从左到右的最后一个

直接上代码:


//ES5 使用默认值会使用短路运算来判断
// function foo(enable){
//     //enable = enable ||  true;  //错误用法:当 foo(false) 得到 enable = true;
//     //正确用法
//     enable = enable === undefined || true;
//     console.log(enable);
// }
// foo(); // true 
//ES 6
//这里的默认值只有当调用时没有传递实参,或 实参为 undefined 时会被使用
//注意:当有多个参数时,带默认值的参数要放在从左到右的最后一个  
//例如:错误用法 function foo(enable=true,baz),正确用法:function foo(baz,enable=true)
function foo(enable=true){
    //enable = enable ||  true;  //错误用法:当 foo(false) 得到 enable = true;
    //正确用法
    enable = enable === undefined || true;
    console.log(enable);
}
foo(); // true 

剩余参数 & 数组展开

  • 使用 ... 剩余运算符
  • 表示从当前位置开始直到结束,所有参数的数组集合
  • 只能出现在形参的最后一位,且只能使用一次 代码:
//剩余参数
// ES 5 arguments 接受所有的参数,是个伪数组
// function foo(){
//     console.log(arguments);// [Arguments] { '0': 1, '1': 2, '2': 3 }
// }
// foo(1,2,3);
// ES 6 ... 剩余运算符,表示从当前位置开始直到结束,所有参数的数组集合
//只能出现在形参的最后一位,且只能使用一次
function foo(...args){
    console.log(args);// [Arguments] { '0': 1, '1': 2, '2': 3 }
}
foo(11,22,33);
//展开数组
const arry =['foo','baz','bar']; 
//ES5
console.log(arry[0],arry[1],arry[2]);// foo baz bar
console.log.apply(console,arry);// foo baz bar
//ES 6
console.log(...arry);// foo baz bar

箭头函数

用法:使用 => 来定义函数,优点:简短易读

  • => 左边是形参一个参数,右边 是 return 的语句,一句的情况下可省略 return
  • 形参 多个参数 ,函数体是语句块的,return 不能省略 具体见代码:
//箭头函数
// 使用 => 来定义函数
// =>  左边是形参一个参数,右边 是 return 的语句,一句的情况下可省略 return
const fn = x=> x+1;
// 形参 多个参数 ,函数体是语句块的,return 不能省略
const add =(x,y) =>{
    console.log(x);
    return x+y;
}
console.log(fn(10));//11
console.log(add(1,2));//3
//普通函数
 let arry = [1,2,4,6,8,9,0];
// let rs=arry.filter(function (item){
//     return item %2==0
// })
//使用箭头函数的优点:代码简短易读
let rs = arry.filter(i => i%2==0)
console.log(rs);

箭头函数与this

  • 箭头函数里没有this的机制,因此不会改变this的指向
  • 通俗点说就是,在箭头函数的外面this 是什么,我们拿到的this就是什么

代码:

//箭头函数 和 this
//箭头函数里没有this的机制,因此不会改变this的指向
//通俗点说就是,在箭头函数的外面this 是什么,我们拿到的this就是什么
const person ={
    name:'jack',
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
        return fn();
    },
    sayHi:function (){
        console.log(`hi,I am ${this.name}`); //hi,I am jack
    },
    sayHalo:()=>{
        console.log(`halo,I am ${this.name}`);//halo,I am undefined
       
    },
    sayHiAsync:function(){
       /*  let _this = this;//利用闭包的机制,将 当前的this 存到闭包里面
        setTimeout(function(){
           // console.log(this.name);//undefined  此时this指向的是window 
           console.log(_this.name);
        },1000); */
        setTimeout(()=>console.log(this.name),1000);//this 指向当前的person作用域
        
    }
}
person.sayHi();
person.sayHalo()
console.log(person.getAge());// 30
person.sayHiAsync()

对象字面量增强

代码:

//对象字面量增加
const bar =123
const obj ={
    name:'jack',
    //bar:bar ,//es5
    bar,//属性名和属性值得变量名一样时
    //method1:function(){}
    method1(){
        console.log('method11');
        console.log(this) // { name: 'jack', bar: 123, method1: [Function: method1] }

    },
    //计算属性名,使用表达式的返回值作为属性名
    [Math.random()]:'random'

}
obj[Math.random()]=100; //ES5 添加一个随机的属性
console.log(obj);
obj.method1();

对象的扩展

  • 将多个源对象中的属性,复制到一个目标对象
  • 目标对象中的属性会被原对象中的属性覆盖

代码:

const source1={
    a:123,
    b:456
}
const source2={
    c:'foo',
    d:'baz'
}
const target={
    a:789,
    c:135
}
const result =Object.assign(target,source1,source2);
console.log(result);//{ a: 123, c: 'foo', b: 456, d: 'baz' }
console.log(result === target);// true 表示Object.assign返回的就是源目标

Proxy

ES5 使用Oject.defineProperty 进行数据的双向绑定,响应式。vue2.0 使用的就是这个方法。ES6 专门为对象设置访问代理器 Poxy,它可以轻松的监视到数据的读写。 两者相互比较:proxy 功能更为强大,用起来更方便

代码:

// proxy 对象

const person={
    name:'tom',
    age:28
}
//实例化Proxy 对象
//1参:目标代理对象
//2参:代理的处理对象
const personProxy = new Proxy(person,{
    //监视属性的访问
    //target 目标代理对象  , property  访问属性名
    //返回值是外部访问这个属性值
   get(target,property){
        // console.log(target,property);
        // return 100
        //目标代理对象中存在属性,则返回属性值,否则返回 一个默认值
        return property in target?target[property]:'default'
   },
   //监视属性的设值
   //target 目标代理对象  , property  要写入的属性名  ,value 属性值
   set(target,property,value){
       //数据校验
       if(property === 'age'){
            if (!Number.isInteger(value)){
                throw new TypeError(`${value} is not int` )
            }
           
       }
       target[property]=value;
       console.log(target,property,value); //{ name: 'tom', age: 28 } gender true
   }
})
console.log(personProxy.name);  //  get 的返回值  tom
console.log(personProxy.sex) // 访问一个不存在的属性,  返回默认值 default
personProxy.gender = true;
//personProxy.age='bb'  // TypeError: bb is not int
personProxy.age=18
console.log(person) //{ name: 'tom', age: 18, gender: true }

Proxy VS Object.definedProperty()

Proxy 优势:

  • Object.definedProperty 只能监听到监听属性的读写,Proxy 能够监视到更多对象操作,例如 delete 操作
  • Object.definedProperty 监视数组只能通过重新数组的操作方法,proxy对象可以直接监视
  • proxy 以非侵入的方式监管了对象的读写 代码:
const person={
    name:'tom',
    age:28
}
//实例化Proxy 对象
//1参:目标代理对象
//2参:代理的处理对象
const personProxy = new Proxy(person,{
    //监视属性的访问
    //target 目标代理对象  , property  访问属性名
    //返回值是外部访问这个属性值
   get(target,property){
        // console.log(target,property);
        // return 100
        //目标代理对象中存在属性,则返回属性值,否则返回 一个默认值
        return property in target?target[property]:'default'
   },
   //监视属性的设值
   //target 目标代理对象  , property  要写入的属性名  ,value 属性值
   set(target,property,value){
       //数据校验
       if(property === 'age'){
            if (!Number.isInteger(value)){
                throw new TypeError(`${value} is not int` )
            }
           
       }
       target[property]=value;
       console.log(target,property,value); //{ name: 'tom', age: 28 } gender true
   },
   deleteProperty(target,property){
    console.log('将被删除的属性',property);
    delete target[property];
   }
})
console.log(personProxy.name);  //  get 的返回值  tom
console.log(personProxy.sex) // 访问一个不存在的属性,  返回默认值 default
personProxy.gender = true;
//personProxy.age='bb'  // TypeError: bb is not int
personProxy.age=18 

console.log("修改age",person) //修改age { name: 'tom', age: 18, gender: true }
delete personProxy.age; 
console.log("删除age",person);//删除age { name: 'tom', gender: true }
//监听数组
let arry = [1,2,3,4]
const listProxy = new Proxy(arry,{
    set(target,property,value){
        console.log('set',property,value);
        target[property]=value
        return true;
    }
})
listProxy.push(5);  
console.log(arry);//[ 1, 2, 3, 4, 5 ]

Reflect

统一的对象操作API,是一个静态类,不能通过 new 关键字 来创建,只能通过Reflect.get() ,内部封装了13个对对象的底层操作,Reflect 方法就是proxy对象的默认实现。 存在的意义:

  • 统一了一套用于操作对象的API

代码:

const obj ={
    name:'linh',
    age:28
}
console.log('name' in obj);
console.log(Object.keys(obj));
console.log(delete obj['age']);
//上面的代码对对象的操作,使用了 in 、delete 的对象,还使用的对象的方法比较混乱
//因此引入了Reflect 统一了对象API的操作
 console.log(Reflect.has(obj,'name'));
 console.log(Reflect.deleteProperty(obj,'age'));
 console.log(Reflect.ownKeys(obj));

class

代码:

//es5
/* function Person (name){
    this.name=name;
}
//在原型上定义共享方法
Person.prototype.sayHi=function(){
    console.log(`hi,${this.name}`); // hi,Jane
} */
//es6
class Person{
    constructor(name){
        this.name=name;
    }
    sayHi(){
        console.log(`hi,${this.name}`);
    }
}
const p = new Person('Jane');
p.sayHi();

static

ES2015 中新增的静态成员的static 关键字。

注意点 因为静态方法是挂载到类型上面的,所以在静态方法内this 不会指向某一个实例对象,而是当前的类型

代码:

//es6
class Person{
    constructor(name){
        this.name=name;
    }
    sayHi(){
        console.log(`hi,${this.name}`);
    }
    //静态方法
    static create(name){
        return new Person(name);
    }
}
//通过new 创建实例
const p = new Person('Jane');
p.sayHi();
//通过static静态方法创建实例
const p2 = Person.create('Tom');
p2.sayHi();

extends 类的继承

能够抽象中相互的类型之间重复的特性。在ES5 中使用原型进行继承,ES6 使用 extends,子类拥有父类的所有属性和方法

代码:

// extends 类继承  

//es6
class Person{
    constructor(name){
        this.name=name;
    }
    sayHi(){
        console.log(`hi,my name is ${this.name}`);
    }
    //静态方法
    static create(name){
        //因为静态方法是挂载到类型上面的,所以在静态方法内this 不会指向某一个实例对象,而是当前的类型
        console.log(this);// [Function: Person]
        //console.log(sayHi());//TypeError: this.sayHi is not a function
        return new Person(name);
    }
}
class Student extends Person{
    constructor(name,number){
        super(name);//调用父类的构造方法
        this.number=number;
        
    }
   doSelfIntro(){
       super.sayHi();
       console.log(`my No is ${this.number}`);
   }

}
const student = new Student('Tom',100);

student.doSelfIntro();

Set 数据结构

和Map相比,set中的每一值都是唯一的 代码:

// Set
const set = new Set();
//可以链式调用添加数据
set.add(1).add(2).add(3).add(4)
.add(3);//重复的数据无法添加到集合中
console.log(set);// Set(4) { 1, 2, 3, 4 }
//可遍历
//set.forEach(s=> console.log(s))
for (let i of set){
    console.log(i);
}
//判断是否存在某一个值
console.log(set.has(100));//false
//删除某一个值,
console.log(set.delete(100));//删除成功后返回true
//数组去重
let array = [1,2,3,4,2,5,1];
const s = new Set(array);
console.log(Array.from(s));
console.log([...s]);// [ 1, 2, 3, 4, 5 ]

Map

和 对象相似,都是用来存放键值对的数据结构,但是对象中的key只能是字符串类型,存放一些复杂的数据类型作为键就会出现问题。 Object 和 Map 最大的区别就是,Map 可以使用任意类型作为键

代码:

// Map
// Map
//问题导入
// 使用对象
const obj ={};
obj[true]='val';
obj[123]= 1;
obj[{a:1}]=100;
//打印obj 中的所有的键
console.log(Object.keys(obj));
//结果:[ '123', 'true', '[object Object]' ]
//从结果可见:Object 内部使用toString(),将所有的key都转换成字符串
//假定要使用对象作为键存储值,结果任意一个对象作为key都能取到 obj[{a:1}] 的值,没办法做到区分
console.log(obj[{}]);//  100
console.log(obj['[object Object]']); // 100  
 //Map 才能从严格意义上键值对集合,去映射两个任意类型的对应关系
const map = new Map();
const tom={name:'linh'};
map.set(tom,90);
map.set(10,'value');
// 遍历 map
map.forEach((value,key)=>{
    console.log(key,value);
    //{ name: 'linh' } 90
    // 10 value
})
console.log('是否有属性:',map.has(tom));//是否有属性: true
console.log('删除某个属性:',map.delete(10)); //删除某个属性: true
console.log('清空前',map);//清空前 Map(1) { { name: 'linh' } => 90 }
console.log('清空:' ,map.clear());// 清空: undefined
console.log('清空后',map);//清空后 Map(0) {}

Symbol

ES 2015之前,对象的属性名都是字符串,而字符串可能存在重复,引发冲突

问题导入

代码:

//问题导入
// 缓存对象
const cache ={};
//a.js==========================
//在一个a文件中对这个缓存对象定义了一个属性 foo
cache['foo']=Math.random();

//b.js==========================
//在另一文件中,不知道有foo 这个属性的情况下,修改了这个属性值
cache['foo']='123';
//此时产生了冲突,以前解决问题是采用约定,约定a 中的属性名使用前缀a_, b 中使用 b_
//但是约定的方式只是规避了问题,并不能彻底解决问题,若是有人不遵守这个约定,这个问题依然存在
cache['a_foo']=Math.random();
cache['b_foo']=123;
console.log(cache);//{ foo: '123', a_foo: 0.2117882168404599, b_foo: 123 }

为了彻底的解决问题,引入Symbol 符号,表示了一个独一无二的值,创建的每一个值都是唯一的不会重复

代码:

//Symbol
const s = Symbol();
console.log(typeof s); //  symbol ,表明了Symbol 是一个全新的类型 
console.log(Symbol()===Symbol()) // false ,表明每一个都是全新的值
//创建对象的属性除了使用字符串,以后还可以使用 Symbol
const obj ={
    //使用计算属性
    [Symbol()]:'789'
};
obj[Symbol()] ='123';
obj[Symbol()] ='456';
console.log('symbol',obj) // symbol { [Symbol()]: '789', [Symbol()]: '123', [Symbol()]: '456' }

// 模拟对象的私有成员
const name =Symbol();
const person={
    [name]:'linh',
    say(){
        console.log(`hi , ${this[name]}`)
    }
}
console.log(person[Symbol()]) // undefined,表明无法访问到私有成员
person.say(); // hi , linh

截止到ES2019 一共定义了6种基本数据类型(number,boolean,null,undefined,string,Symbol), Object对象,加上以后会被ES 标准化的 bigInt。一共是8中数据类型

for...of

遍历迭代的几种方式:

  • for 使用与遍历普通的数组
  • for ... in 适合遍历键值对
  • 函数式对象的遍历方法 。 forEach 这几种方式都有一定的局限性,所以引入了for...of ,以后会作为遍历所有数据结构的统一方式

代码:

//for...of
const array=[1,2,3,4]
for(let i in array){ // 获取的到数组的索引
    console.log(i);//0 1 2 3  
}
array.forEach(item => {
    console.log('forEach:',item);
    //forEach 无法使用break 终止循环  ,SyntaxError: Illegal break statement
   /*  if (item > 1){
        break;
    } */
});
for(const item of array){ // 能直接获取到数组元素
    if(item >1){
        break;
    }
    console.log('forof:',item);
}
//for of 遍历Set
const s = new Set(['linh','huan']);
for(let i of s){
    console.log('set',i);//
}
//遍历Map
const map = new Map();
map.set('firstName','lin');
map.set('lastName','huan');
for(const m of map){
    console.log(m);
   // [ 'firstName', 'lin' ]
    //[ 'lastName', 'huan' ]
}
for (const [key,value] of map){// 利用数组结构
    console.log(key,value);
    //firstName lin
    //lastName huan
}
//遍历对象
const obj ={foo:123,baz:456}
for(const o of obj){  // TypeError: obj is not iterable 
    console.log(o);
}

从代码中可以总结出使用for...of 的优势有:

  • 相比于使用for...in 遍历数组,需要通过数组的索引值来间接获取数据,for...of 能直接获取到数据,更为方便
  • 相比于forEach方法,无法有条件的终止跳出循环,for...of 可以使用break

然而在遍历对象的时候,却抛出了TypeError: obj is not iterable 的错误,和上文提到的for...of ,以后会作为遍历所有数据结构的统一方式有冲突了。看官别捉急,继续往下文看吧

for...of 遍历对象

为了统一数组、Set、 Map 、Object 等不同数据结构类型,ES2015 提供了Iterable接口来统一遍历方式,实现iterable 接口是for...of 的前提,需要实现统一的规格标准 接下来我们透过表象,来看看能被for...of 遍历的数据结构是如何实现Iterable的

为了避免使用谷歌浏览器的开发工具,来回切换窗口,我们通过使用VSCODE ,帮助-切换开发人员工具 打开和chrome类似的工具,来进行调试。 首先,我们来看看数组的原型结构,如下图:

接着是 Set :

紧接着 Map:

最后来看Object:

聪明的你一定知道共同点了吧!没错,Array,Set,Map 它们三个的内部原型对象上都实现了 Symbol.iterator 接口,而 Object 对象却没有,因此在上一节中,使用 for...of 直接遍历 对象 会报TypeError: obj is not iterable。 我们来看一下数组是如何实现这个Symbol.iterator接口的。 我们定义一个数组array[1,2,3],通过调用arraySymbol.iterator,可以获得一个Array Iterator 对象,如下图:

由图中可知该对象中有个方法next(),我们接着调用这个next

从上图中我们可以看到,每调用一次next,我们就得到了一个对象{value:'',done:false},其中value 是数组中的元素,done 表示这个迭代器是否执行了所有的迭代。

总结:所有能够使用for...of 遍历的数据结构,都要实现Iterable 的接口,那就是在内部都要挂载一个Iterator 的方法,这个方法要挂载一个next 方法,我们不断调用这个next 方法就可以实现对这个数组的遍历了。

我们编写代码来验证一下

//Iterator 迭代器
const s = new Set([100,200,300]);
const iterator = s[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
//打印结果
// { value: 100, done: false }
// { value: 200, done: false }     
// { value: 300, done: false }     
// { value: undefined, done: true }
// { value: undefined, done: true }

实现可迭代接口Iterable

定义一个对象实现Iterable,具体实现如下:

const obj ={//实现Iterable接口,约定了内部必须要有一个实现迭代的iterable 方法
    [Symbol.iterator]:function(){
        return { //实现了迭代器方法 iterator,约定了内部必须要有一个用于迭代的next方法
            next:function(){
                return {//实现iterationResult,约定了内部必须要有一个value用于存放值,done 用来判断迭代是否结束
                    value:'linh',
                    done:true
                }
            }
        }
    }

}


使用for...of 遍历 对象

for(const i of obj){
    console.log(i);
}

结果不再报错,目前还没有执行循环体,原因是iterationResult返回对象中的done 我们写死了,我们接下继续实现:

const obj ={//实现Iterable接口,约定了内部必须要有一个实现迭代的iterable 方法
    store:['foo','bar','baz'], //用于存放数据
    [Symbol.iterator]:function(){
        let index=0;
        const self = this;
        return { //实现了迭代器方法 iterator,约定了内部必须要有一个用于迭代的next方法
            next:function(){
                const result ={//实现iterationResult,约定了内部必须要有一个value用于存放值,done 用来判断迭代是否结束
                    value:self.store[index],
                    done:index >=self.store.length
                }
                index ++;
                return result;
            }
        }
    }

}
//for...of 遍历obj
for(const i of obj){
    console.log(i);
}

结果:

由此可见我们已经实现了for...of 迭代对象结构的数据了,那这个看起来实现起来那么麻烦,有什么作用呢? 我们接着分析。。。。

迭代器设计模式

我们现在假定一个场景,你我协同开发一个任务清单应用,代码如下

//假定场景:你我协同开发一个任务清单应用
//我提供数据结构
const task= {
    life:['吃饭','睡觉','打豆豆'],
    learn:['语文','数学','外语'],
}

//你来获取我的数据
for(const i of task.life){
    console.log(i);
}
for(const i of task.learn){
    console.log(i);
}

此时,若是我这边数据新增了一项任务,按照正常的处理方式,你那边也要跟着增加一个任务迭代的循环,如下:

//我提供数据结构
const task= {
    life:['吃饭','睡觉','打豆豆'],
    learn:['语文','数学','外语'],
    work :['喝茶']
}

//你来获取我的数据
for(const i of task.life){
    console.log(i);
}
for(const i of task.learn){
    console.log(i);
}
for(const i of task.work){
    console.log(i);
}

可见常规的做法,你我两个人之间的耦合比较高,因此,我们做了如下的优化,在task 任务列表中增加了一个each方法,用来遍历内部的所有的任务清单,执行回调方法,代码具体实现:

//Iterator 迭代器模式
//假定场景:你我协同开发一个任务清单应用
//我提供数据结构
const task= {
    life:['吃饭','睡觉','打豆豆'],
    learn:['语文','数学','外语'],
    work :['喝茶'],
    each(callback){
        const all =[].concat(this.life,this.learn,this.work);
        for (const item of all){
            callback(item)
        }
    }
}

//你来获取我的数据
/* for(const i of task.life){
    console.log(i);
}
for(const i of task.learn){
    console.log(i);
}
for(const i of task.work){
    console.log(i);
} */
task.each(item=>{console.log(item)})

接下来我们使用迭代器的接口实现一下

//Iterator 迭代器模式
//假定场景:你我协同开发一个任务清单应用
//我提供数据结构
const task= {
    life:['吃饭','睡觉','打豆豆'],
    learn:['语文','数学','外语'],
    work :['喝茶'],
    each(callback){
        const all =[].concat(this.life,this.learn,this.work);
        for (const item of all){
            callback(item)
        }
    },
    [Symbol.iterator]:function (){
        let all =[...this.life,...this.learn,...this.work];
        let index =0;
        return {
            next(){
                return {
                    value:all[index],
                    done:index++>=all.length
                }
            }
        }
    }
}

//你来获取我的数据

//task.each(item=>{console.log(item)})
for(const i of task){
    console.log(i);
}


迭代器的核心:对外提供统一的迭代接口,不用关心内部的数据结构。这里的each 实现方式只能实现特定的结构,而使用迭代器可以适用于任何数据结构

Generator 生成器函数

避免异步编程中回调嵌套过深的问题,从而提供更好的异步编程解决方案 定义:

//生成器函数
function  * fun(){
    console.log('gernerator func')
    return 100;
}
console.log(fun()); // Object [Generator] {}

生成器函数不能马上调用,定义之后是一个生成器对象。该函数会被挂起,原型对象上有一个next方法

调用next() 之后,

//生成器函数
function  * fun(){
    console.log('gernerator func')
    return 100;
}
console.log(fun()); // Object [Generator] {}
const r =fun();
console.log(r.next());  

结果我们发现函数被执行了,并且以{value:100,done:true}对象形式返回了原本函数体里对应的返回值。因此我们可以得出结论生成器函数内部也实现了迭代器Iterable的协议

然而生成器函数在实际使用中需要配合yield 一起使用

function * fn(){
    console.log('1');
    yield 100;
    console.log('2');
    yield 200;
    console.log('3');
    yield 300;
    console.log('4');
    yield 400;
}

const result =fn();
console.log(result.next());

结果:从结果发现只返回了一个yield 的值

我们继续调用一次result.next(),如下图,从图中我们发现,此时被执行到了第二个yield 的位置了

接着再调用一次一次result.next(),如下图

最终代码和结果如下:

我们得到如下结论:生成器函数会自动返回一个生成器对象,调用这个对象的next方法才会让这个函数体开始执行,执行过程中一旦遇到yield ,执行会被暂停,yield 后面的值将作为next 的结果返回。再调用next ,函数又从暂停的位置开始执行,周而复始,会一直执行到 next 的返回对象的done 的值为 true 为止。生成器函数的最大特点是:惰性执行

应用场景:

1.发号器

//生成器函数 实现发号器
function * idMaker(){
    let id =1;
    while(true){//此处无需担心死循环的问题,因为yiled 会暂停函数的执行
        yield id++;
    }
}
const idMakerRs= idMaker();
console.log('NO',idMakerRs.next().value);
console.log('NO',idMakerRs.next().value);
console.log('NO',idMakerRs.next().value);
console.log('NO',idMakerRs.next().value);

2.使用generator 函数实现 iterator 方法

const task= {
    life:['吃饭','睡觉','打豆豆'],
    learn:['语文','数学','外语'],
    work :['喝茶','撸代码'],
 
    [Symbol.iterator]:function *(){
        let all =[...this.life,...this.learn,...this.work];
       for(const item of all){
           yield item 

       }
    }
}
for (const i of task){
    console.log(i);
}
//结果:
// 吃饭
// 睡觉
// 打豆豆
// 语文
// 数学
// 外语
// 喝茶
// 撸代码

ESCAMSCRIPT 2016

新增的两个特性: 1.数组的 indeludes,检查数组中是否有指定元素。includes 和 indexOf 的区别是:indexOf 无法判断数组中是否存在NaN 的数据,而 includes 可以 2.指数运算 。使用 ** 运算符,进行指数计算
例如 2**10 =1024

代码:

//es 2016 新增的特性
//之前使用 indeof 查找 数组中是否存在某个值

const arry = [1,'foo',NaN,true]
console.log(arry.indexOf(true)>-1); // true
console.log(arry.indexOf(NaN));  // -1

// 1.includes
console.log(arry.includes(NaN));// true

//2. 指数运算
console.log(Math.pow(2,10));  //1024
//现在
console.log(2**10);//1024

ECAMSCRIPT 2017

新增的特性: 1.Object.values() 返回对象所有属性值得数组 2.Object.entries() 将对象中的key:value转换成数组[key,value] 3.Object.getOwnPropertyDescriptors() 获取对象中的属性描述器 4.String.prototype.padStart / String.prototype.padStart 。用给定的字符串填充目标的开始 或结束位置,直到指定字符串达到指定长度为止 5.在函数参数中添加尾逗号 6.Async/Wait,解决异步编程中回调嵌套过深的问题

const obj ={
    name:'foo',
    age:18
}
//Object.values 获取到所有的属性值数组
console.log(Object.values(obj));// [ 'foo', 18 ]
//Object.entries
console.log(Object.entries(obj)); // [ [ 'name', 'foo' ], [ 'age', 18 ] ]
for (const [key,value] of Object.entries(obj)){
    console.log(key,value);
    // name foo
    // age 18
}
console.log(new Map(Object.entries(obj)));
//Object.getOwnPropertyDescriptors
const p1={
    firstName:'lin',
    lastName:'huan',
    get fullName(){
        return this.firstName+' '+this.lastName
    }
}
console.log('全名:',p1.fullName);
//复制一个对象
const p2 =Object.assign({},p1);
 p2.lastName = 'han';
console.log('p2全名',p2.lastName);//han
console.log('p2全名',p2.fullName);//p2全名 lin huan 
//我们可以看到,lastName 已经被修改,可访问器get fullName 属性还是没变
const descriptors = Object.getOwnPropertyDescriptors(p1);
const p3 = Object.defineProperties({},descriptors)
p3.lastName='ge';
console.log('p3 全名', p3.fullName); // p3 全名 lin ge
//String.prototype.padStart / String.prototype.padStart 
//用给定的字符串填充目标的开始 或结束位置,直到指定字符串达到指定长度为止
const books={
    html:5,
    css:6,
    ecamScript: 2017
}
for (const [name,value] of Object.entries(books)){
    console.log(name,value);
    //打印结果
    // html 5
    // css 6
    // ecamScript 2017
}
//为了使结果格式对齐
for (const [name,value] of Object.entries(books)){
    console.log(`${name.padEnd(16,'-')}|${value.toString().padStart(3,0)}`);
    //打印结果
    // html------------|005
    // css-------------|006
    // ecamScript------|2017
}
//在函数参数中添加尾逗号
function fn(arg1,arg2,){

}

async/await的特点

1.async/await更加语义化,async 是“异步”的简写,async function 用于申明一个 function 是异步的; await,可以认为是async wait的简写, 用于等待一个异步方法执行完成;

2.async/await是一个用同步思维解决异步问题的方案(等结果出来之后,代码才会继续往下执行)

3.可以通过多层 async function 的同步写法代替传统的callback嵌套

async function add(x){
    const a=1;
    return a+x;
}
console.log(add(10)); //Promise { 11 }

结果:

可以看出add 返回的是一个状态是resolved 的Promise 对象

await 语法

  • await 放置在Promise调用之前,await 强制后面点代码等待,直到Promise对象resolve,得到resolve的值作为await表达式的运算结果
  • await只能在async函数内部使用,用在普通函数里就会报错
function pm(){
    return new Promise((resolve,reject)=>{
    	//setTimeout(()=>{resolve('promise value 1')},1000)
        resolve('promise value 1')
    })
}
async function test(){
    const p1= await pm();
    const p2 = await 'not promise value 2';
    console.log(p1)
    console.log(p2)
    return p1 + p2;
}
const result =test();
console.log(result);

结果:

可以看出调用async 函数先返回了一个等待状态的Promise对象,然后执行函数体的逻辑,带执行结束后 Promise 对象的状态变成了 resolved

错误处理

在async函数里,无论是Promise reject的数据还是逻辑报错,都会被默默吞掉,所以最好把await放入try{}catch{}中,catch能够捕捉到Promise对象rejected的数据或者抛出的异常

function timeout(ms) {

  return new Promise((resolve, reject) => {

    setTimeout(() => {reject('error')}, ms);  //reject模拟出错,返回error

  });

}

async function asyncPrint(ms) {

  try {

     console.log('start');

     await timeout(ms);  //这里返回了错误

     console.log('end');  //所以这句代码不会被执行了

  } catch(err) {

     console.log(err); //这里捕捉到错误error

  }

}

asyncPrint(1000);

如果不用try/catch的话,也可以像下面这样处理错误(因为async函数执行后返回一个promise)

function timeout(ms) {

  return new Promise((resolve, reject) => {

    setTimeout(() => {reject('error')}, ms);  //reject模拟出错,返回error

  });

}

async function asyncPrint(ms) {

  console.log('start');

  await timeout(ms)

  console.log('end');  //这句代码不会被执行了

}

asyncPrint(1000).catch(err => {

    console.log(err); // 从这里捕捉到错误

});

如果你不想让错误中断后面代码的执行,可以提前截留住错误,像下面

function timeout(ms) {

  return new Promise((resolve, reject) => {

    setTimeout(() => {

        reject('error')

    }, ms);  //reject模拟出错,返回error

  });

}

async function asyncPrint(ms) {

  console.log('start');

  await timeout(ms).catch(err => {  // 注意要用catch

console.log(err) 

  })

  console.log('end');  //这句代码会被执行

}

asyncPrint(1000);

使用场景

多个await命令的异步操作,如果不存在依赖关系(后面的await不依赖前一个await返回的结果),用Promise.all()让它们同时触发

function test1 () {
    return new Promise((resolve, reject) => {

        setTimeout(() => {

            resolve(1)

        }, 1000)

    })

}

function test2 () {

    return new Promise((resolve, reject) => {

        setTimeout(() => {

            resolve(2)

        }, 2000)

    })

}

async function exc1 () {

    console.log('exc1 start:',Date.now())

    let res1 = await test1();

    let res2 = await test2(); // 不依赖 res1 的值

    console.log('exc1 end:', Date.now())

}

async function exc2 () {

    console.log('exc2 start:',Date.now())

    let [res1, res2] = await Promise.all([test1(), test2()])

    console.log('exc2 end:', Date.now())

}

exc1();

exc2();

结果

结果可以发现,exc1 的两个并列await的写法,比较耗时,只有test1执行完了才会执行test2