ECMAScript 概述
什么是ECMAScript
ECMAScript 是一门脚本语言,缩写为es,通常把它看作是Javascript 的标准规范。但实际上Javascript是ECMAScript的扩展语言,ECMAScript 只提供了基本的语法。而JavaScript实现了ECMAScript的标准,并且在这个基础之上做了一些扩展,例如在浏览器环境中用到的bom 和 dom对象,node 环境中读写文件的操作。总的来说,在浏览器环境中的JavaScript = ECMAScript + web api,如下图所示:
ECMAScript版本迭代
从 2015年开始保持每年迭代一次,也引入了许多的新特性,因此这门语言也变得越来越高级。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);
结果可以看到在块中定义的变量在外面也能被访问到,这样是不安全的
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);
}
结果:
解决办法:使用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
- 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 常量/恒量
- 在let的基础上多了一个只读的特性,声明过后就不能被修改
const name="lh";
name="hz"; //结果 TypeError: Assignment to constant variable.
- 不能先声明变量,再赋值
const name; // 结果 SyntaxError: Missing initializer in const declaration
name="hz";
- 声明变量后不能修改是指,不能修改变量的内存地址,并不是不能修改恒量中的属性成员
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);
好处:简化代码,较少代码体积
模板字符串
使用反引号 ` ,有以下两个特性:
- 支持换行,非常适合输出HTML 字符串
- 支持变量,数学运算 的插值 ${} ,拼接到字符串中
- 模板字符串可以使用一个标签函数来处理,可以处理一些逻辑转换,文本的多语言化,检查字符串中是否存在,还可以开发一个模板引擎
//模板字符串
// 反引号 转义
//支持换行,非常适合输出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 的位置了
最终代码和结果如下:
我们得到如下结论:生成器函数会自动返回一个生成器对象,调用这个对象的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 }
结果:
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 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