什么是ES,什么是ES6
ES(ECMAScript)是JavaScript的标准化脚本语言,用于Web浏览器和服务器端编程。ES有许多版本,每个版本都引入了一些新功能和改进。目前,最新版本的ES是ES2023,也被称为ES11,引入了一些新功能,如私有类字段、CSP支持等。
ES规范定义了JavaScript的核心语言特性,包括语法、数据类型、运算符、控制结构、对象、数组、函数等。ES规范是Web浏览器和服务器端开发人员的基础,它提供了编写JavaScript代码所需的全部信息。
除了核心语言特性外,ES还提供了一些额外的语言特性,如模块、异步函数、生成器等。这些特性可以增强JavaScript的功能,使其更加易于使用和维护。
总之,ES是JavaScript的核心组成部分,它提供了编写高质量、可维护的JavaScript代码所需的基本工具和框架。
ES6即指在2015年发布ES的第六版本,同时也泛指在这之后发布的ES6及之后版本的统称
ES6常用新特性
1. 块级作用域
为什么引入块级作用域?
是为了解决之前js的问题,之前js在声明变量后会导致变量提升,即使用var声明的变量在js加载的时候会将变量的声明提升到作用域的顶部,在<script>标签内声明的变量会被提升到代码最前面,在function内声明的变量也会被提升到方法内的最前面,这是很奇怪的一种现象
//全局的变量提升
<script>
console.log(a); // undefined,等价于var a; console.log(a); a = 10;
var a = 10;
</script>
//方法内的变量提升
function test() {
console.log(num); // undefined
var num = 3;
}
这会导致我们的程序在运行时出现一些意外的结果,命名冲突等。且代码阅读起来会比较混乱,比如如下两个例子
- 内层变量可能会覆盖外层变量
<script>
function init() {
console.log(a);
if(!a) {
a = 11;
console.log(`方法内初始化a:${a}`); // a=11
}
}
init();
var a = 10;
console.log(`方法外初始化a:${a}`); // a=10
</script>
这个例子中我们想在init方法中初始化一些全局变量,但是在运行完初始化函数之后,又有其他人的代码对a变量又进行了重新初始化,这就会导致我们代码逻辑的混乱。现实写代码中不会出现这么傻的操作,但是在实际开发中两个人开发的代码可能就会因这种变量提升引起不必要的bug。
- 用来计数的循环遍历泄露为全局变量
<script>
var array = [];
for(var i = 0;i < 10; i++) {
array[i] = function() {
return i;
}
}
console.log(array[5]()); // 输出10
</script>
这是由于计数变量i被提升为了全局变量导致的。我们本意是想将i作为计数使用的变量,使array数组中的每个函数都返回当时计数时的i指。但是由于i被提升为了全局变量,js在循环结束后并不会释放i,因此array中的每个函数还都能访问到全局i变量,所以array中的每个函数都会输出10(这个问题可以用闭包的方式解决)。
为了解决上面提到的两类问题,在ES6中引入了块级作用域的概念,块级作用域的变量不会被变量提升,只能在声明变量后才能够使用该变量,变量的作用域也不会被泄漏到外面。
声明块级作用域的关键字为let和const。
let
let是用来声明变量的,即let的值在初始化后可以被修改,其用法与var类似。
let声明的变量特性如下:
- 不允许变量提升
{
var a = 20;
let b = 20;
}
console.log(a); // 20
console.log(b); // Uncaught ReferenceError: b is not defined
- 不允许重复声明
var a = 10;
var a = 20; //运行通过
let a = 10;
let a = 20; //Uncaught SyntaxError: Identifier 'a' has already been declared
const
const用来声明常量,被const修饰的常量初始化后不允许被修改。
let a = 10;
a = 20; //运行通过
const b = 10;
b = 20; //Uncaught TypeError: Assignment to constant variable.
暂时性死区
在let和const声明的值在被声明前是不可用的,这一段不可用的区间被成为暂时性死区(temporal dead zone,简称 TDZ)
2. 模板字符串
当我们想生成一段字符串或生成一段dom元素,之前会使用字符串拼接的方式。
<script>
let pMsg = 'p标签文本';
let pDom = '<p>' + pMsg + '</p>';
document.body.innerHTML += pDom;
</script>
这种方式在生成长字符串或大dom时会非常费力,也容易出现错误,使代码不易阅读。在ES6中我们使用模板字符串的方式生成
<script>
let templateString = `<p>${pMsg}</p>`;
document.body.innerHTML += templateString;
</script>
这省去了字符串拼接带来的+号,减少了工作量,且在书写时清晰易读
3. 解构赋值
解构赋值可以根据对象或数组的结构来解析对象或数组的元素。
对象解构
let person = {
name: 'zhangsan',
age: 22,
gender: 'male'
}
let {name, age, gender} = person;
console.log(name); // 'zhangsan'
console.log(age); // 22
console.log(gender); // 'male'
对象结构的key名必须与对象中的字段名一致,否则会导致解构失败,解构映射不上时,key对应的值会被赋为undefined
let person = {
name: 'zhangsan',
age: 22,
gender: 'male'
}
let {name1} = person;
console.log(name1); // undefined
解构时我们可以给与默认值。当对象中没有我们需要的属性时,key会被赋予默认值。我们还可以使用剩余运算符(...)来将剩余字段赋值给最后一个值(...剩余/拓展运算符只能在最后一个参数上使用,否则会报错)。
let person = {
name: 'zhangsan',
age: 22,
gender: 'male'
}
let {hobby=['足球','篮球'],...otherContext} = person;
console.log(hobby); // ['足球', '篮球']
console.log(otherContext); //{name: 'zhangsan', age: 22, gender: 'male'}
数组解构
数组解构与对象结构类似,区别在于对象结构使用{},数组解构使用[],且数组解构时对key名没有要求,数组解构是根据key的位置来映射的。数组解构可以和对象结构组合使用
let arr = [1,'balabala', { name: 'kong' }, [2,3]];
let [number, context, obj, [arrObj1, arrObj2]] = arr;
console.log(number); // 1
console.log(context); // 'balabala'
console.log(obj); // {name: 'kong'}
console.log(arrObj1); // 2
console.log(arrObj2); // 3
方法传参解构
可以对方法的形参使用解构赋值
let person = {
name: 'zhangsan',
age: 22,
gender: 'male'
}
function printObj({age, name, gender}) {
console.log(`${name}-${age}-${gender}`); // zhangsan-22-male
}
printObj(person);
function printArr([num1, num2, num3]) {
console.log(num1);
console.log(num2);
console.log(num3);
}
printArr([1, 2, 3]); // 1, 2, 3
printArr([1, 3, 2]); // 1, 3, 2
方法返回值解构
可以对方法的返回值进行解构处理
function createNews() {
return {
title: 'XXX活动顺利举办',
context: 'XXXXXXXXXXXXXXXXXXXXXX'
data: '20xx-xx-xx'
}
}
let {title, context, data} = createNews();
console.log(title); // XXX活动顺利举办
console.log(context); // XXXXXXXXXXXXXXXXXXXXXX
console.log(data); // 20xx-xx-xx
4. 函数拓展
参数默认值
函数参数可以添加默认值,在不传值得时候,参数将被赋予默认值执行。ES6前赋予函数默认值的方法比较复杂。
// ES6前写法
function add(x,y) {
x = x || 10;
y = y || 20;
console.log(x + y); // 30
}
add();
// ES6后写法
function sum(x=10,y=20) {
console.log(`x+y=${x + y}`); // 40
}
sum(20);
// 默认值也可以是表达式
function getDefault() {
return ' world';
}
function printMsg(msg1, msg2 = getDefault()) {
console.log(msg1 + msg2);
}
printMsg('hello');
ES6后的写法可以很直观的标识出哪些参数是有默认值的,而不用查看文档或阅读代码就能知道哪些参数可以不传。
参数默认值与解构赋值结合
请思考print1与print2相同点与不同点
function print1({x, y} = {x: 10,y: 10}) {
console.log(x);
console.log(y);
}
function print2({x=10, y=10} = {}) {
console.log(x);
console.log(y);
}
相同点是print1与print2都需要接收一个对象,且对象为字段为x和y
不同点是print1在不传递对象时赋予了默认对象{x:10,y:10},真正形参的x与y是通过解构赋值赋予的10。int2在不传递对象时赋予了默认对象{},真正形参的x与y是通过函数默认值的方式赋予的10。
两个方法结果输出如下:
print1(); // 10, 10
print2(); // 10, 10
print1({}); // undefined undefined
print2({}); // 10, 10
print1({x: 20}); // 20 undefined
print2({x: 20}); // 20, 10
print1({x: 20, y: 20}); // 20, 20
print2({x: 20, y: 20}); // 20, 20
通过输出结果可以看出,我们传入的对象会替换函数参数的默认对象,然后再通过解构赋值映射到参数x和y上
rest参数
当我们的函数需要不定长的参数时可以使用rest参数来接收全部参数
// ES6之前写法,使用arguments对象来获取所有参数
function sum2() {
let sum = 0;
for(var i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
}
console.log(sum(1,2,3)); // 6
// ES6之后写法
function sum(...args) {
let sum = 0;
for(let arg of args) {
sum += arg;
}
return sum;
}
console.log(sum(1,2,3)); // 6
console.log(sum(1,2,3,4,5,6)); // 21
箭头函数(lambda表达式)
箭头函数可以使函数的声明变得简洁,箭头函数使用=>表示
let sayHello = () => {
console.log('hello world');
}
sayHello(); // hello world
let getValue = (x) => {
return x;
}
console.log(getValue(10)); // 10
let sum = (...args) => {
let sum = 0;
for(let arg of args) {
sum += arg;
}
return sum;
}
console.log(sum(1,2,3,4,5)); // 15
// 简化回调函数传入方式
console.log([1,2,3].map(x => x * 2)); // [2, 4, 6]
箭头函数的局限性
- 箭头函数没有自己的this指向,只能通过作用域链向上查找
let dog = {
name: '小花',
bark: () => {
console.log(this); //this指向了window
console.log('wang wang!');
}
}
dog.bark();
let cat = {
name: '小白',
bark: function() {
let bark = () => {
console.log(this); //this指向了cat
console.log('miao miao!');
}
bark();
}
}
cat.bark();
总结:如果箭头函数被一个非箭头函数所包括,那么this的值与该函数的所属对象相等,否则 则是全局的window对象
- 箭头函数不能作为构造函数使用
let Person = function() {
return {
name: 'zhangsan',
age: 25
}
}
let person = new Person();
console.log(person); // {name: 'zhangsan', age: 25}
let Person2 = () => {
return {
name: 'zhangsan',
age: 25
}
}
let person2 = new Person2();
console.log(person2); // Uncaught TypeError: Person2 is not a constructor
- 箭头函数内部没有arguments对象
let lambdaFunc = () => {
console.log(arguments); // Uncaught ReferenceError: arguments is not defined
}
lambdaFunc();
5. 对象拓展
属性简洁表示
当对象的属性key与变量的key相同时可以直接声明,无需使用:赋值
let name = 'zhangsan';
let age = 25;
let obj = {
name,
age
}
//等价于
let obj2 = {
name: name,
age: age
}
console.log(obj); // {name: 'zhangsan', age: 25}
console.log(obj2); // {name: 'zhangsan', age: 25}
拓展运算符也可以再对象上使用
// 解构赋值
let dog = {
dogName: '小花',
color: '白',
type: 'dog'
}
let {dogName, ...other} = dog;
console.log(dogName); // 小花
console.log(other); // {color: '白', type: 'dog'}
// 对象合并/拷贝,对象拷贝是深拷贝
let animal = {
legNum: 4,
hasTail: true
}
let fullDog = {...dog, ...animal};
console.log(fullDog); // {dogName: '小花', color: '白', type: 'dog', legNum: 4, hasTail: true}
6. 异步处理
迭代器(Iterator)
迭代器是一种新的遍历机制,可以使我们方便的访问数组或生成器的元素。可迭代对象中会包含一个Symbol.iterator方法,调用后返回一个迭代器对象,调用迭代器对象的next方法后即可将迭代指针向后移动一个。调用next对象后会返回一个值对象,包含两个字段,一个字段为value,一个done,value表示值,done表示迭代器是否走到最后。
//数组迭代器
let arr = [1,2,3,4,5];
let arrIter = arr[Symbol.iterator]();
console.log(arrIter.next()); // {value: 1, done: false}
console.log(arrIter.next()); // {value: 1, done: false}
console.log(arrIter.next()); // {value: 1, done: false}
console.log(arrIter.next()); // {value: 1, done: false}
console.log(arrIter.next()); // {value: 1, done: false}
console.log(arrIter.next()); // {value: undefined, done: true}
//map迭代器
let map = new Map();
map.set('name', 'zhangsan');
map.set('age', 25);
map.set('gender', 'male');
let mapIter = map[Symbol.iterator]();
console.log(mapIter.next()); //{value: Array(2), done: false}
console.log(mapIter.next()); //{value: Array(2), done: false}
console.log(mapIter.next()); //{value: Array(2), done: false}
console.log(mapIter.next()); //{value: undefined, done: true}
//set迭代器
let set = new Set();
set.add('zhangsan');
set.add('lisi');
set.add('wangwu');
let setIter = set[Symbol.iterator]();
console.log(setIter.next()); // {value: 'zhangsan', done: false}
console.log(setIter.next()); // {value: 'lisi', done: false}
console.log(setIter.next()); // {value: 'wangwu', done: false}
console.log(setIter.next()); // {value: undefined, done: true}
生成器(Generator)
生成器是一种新的函数,可以通过yield关键字将函数挂起,为改变函数执行流和异步转同步编程提供了一种方式。生成器声明方式为使用function*来表示。
// 创建生成器
function* customGenerator() {
console.log('one step'); //在迭代器第一次调用next后才会打印
yield 1;
console.log('two step');
yield 2;
console.log('three step');
yield 3;
}
let cIter = customGenerator(); //通过生成器创建迭代器
console.log(cIter.next()); // 打印one step + {value: 1, done: false}
console.log(cIter.next()); // 打印two step + {value: 2, done: false}
console.log(cIter.next()); // 打印three step + {value: 3, done: false}
console.log(cIter.next()); // 打印{value: undefined, done: true}
// 迭代中传入值
function* customGenerator2() {
let a = yield 1;
console.log('a = ' + a);
let b = yield 2;
console.log('b = ' + b);
return a + b;
}
let cIter2 = customGenerator2();
console.log(cIter2.next()); // 代码走到yield 1后停止执行。打印{value: 1, done: false}
console.log(cIter2.next(10)); // 将10赋值给变量a,然后继续执行。打印a = 10 + {value: 2, done: false}
console.log(cIter2.next(29)); // 将29赋值给变量b,然后继续执行,将retrun值返回。打印b = 29 + {value: 39, done: true}
为不可迭代对象创建自定义生成器
//声明一个生成器
function* customObjGenerator() {
let keys = Object.keys(this);
for(let key of keys) {
yield [key, this[key]];
}
}
//创建一个自定义对象
let obj = {
name: 'zhangsan',
age: 25,
gender: 'male'
}
//将生成器绑定到对象
obj[Symbol.iterator] = customObjGenerator;
//创建迭代器
let objIter = obj[Symbol.iterator]();
console.log(objIter.next()); // {value: Array(2), done: false}
console.log(objIter.next()); // {value: Array(2), done: false}
console.log(objIter.next()); // {value: Array(2), done: false}
console.log(objIter.next()); // {value: undefined, done: true}
利用生成器同步化执行代码
在前端开发中我们会遇到异步编程,当我们的请求具体接口时会遇到接口需要顺序请求的情况。比如请求购物车列表,然后再获取列表中每份商品的详细信息。我们可能会写成如下代码
$.ajax({
url: 'http://www.xxx.com/cart/list',
method: 'GET',
success(result) {
var list = result.data; //获取到列表数据
for(var item of list) {
var goodsId = item.goodsId;
$.ajax({
url: 'http://www.xxx.com/goods/' + goodsId,
method: 'GET',
success(goodsDetail) {
//回调中套着回调
...
}
})
}
}
})
当我们的请求依赖关系越来越多时,我们回调的会越来越深,相应的我们的代码会越来越复杂,缩进也会层层深入,这种回调嵌套我们称之为回调地狱。
为了解决回调地狱问题,我们有时会将异步代码同步化执行。此时我们可以选择使用生成器来实现
// 不使用生成器
function requestData() {
setTimeout(()=> {
console.log('加载到数据!');
let result = '加载结果';
console.log('页面结束加载。。。')
},1000);
}
console.log('页面开始加载。。。');
requestData(); // 输出1.页面开始加载。。。 2.加载到数据!3.页面结束加载。。。
//使用生成器
function* getData() {
console.log('页面开始加载。。。');
let result = yield requestData();
console.log('页面加载完毕。。。');
return result;
}
let iter = getData();
iter.next(); // 输出1.页面开始加载。。。 2.加载到数据!3.页面加载完毕。。。
function requestData() {
setTimeout(()=> {
console.log('加载到数据!');
iter.next('加载结果');
},1000);
}
Promise
基本用法
Promise是es6为我们提供了一种异步编程的实现方式。Promise是一个对象,在构造时传入我们的异步任务,并提供两个方法resolve和reject用来改变Promise的状态。Promise有三个状态pedding、resolved、rejected分别代表处理中、成功、失败。当我们调用Promise的then方法时传入seccuess和failed处理成功和失败状态的两个方法,就可以得到异步编程返回的结果和异常。
let promise = new Promise((resolve,reject) => {
//模拟任务异步执行
setTimeout(() => {
// resolve("成功获取到数据"); // 返回成功结果,将Prmise状态变为成功
reject("获取数据发生异常"); // 返回失败结果,将Promise状态变为失败
}, 1000);
})
promise.then((data) => {
console.log(data); // 打印:成功获取到数据
},(error) => {
console.error(error); // 打印:获取数据发生异常
});
//另一种异常处理写法
promise.then((data) => {
console.log(data); // 打印:成功获取到数据
}).catch((error) => {
console.error(error); // 打印:获取数据发生异常
});
同一个pormise对象可以被多次订阅,上述例子中两种异常处理都会被调用。通过Promise我们就解决了部分异步编程回调地狱的问题。当我们需要在一个异步任务执行完后执行下一个异步任务时,我们只需在第一个异步任务的promise的then方法创建新的Promise然后在代码外面继续。
//promise解决回调地狱问题
let promise = new Promise((resolve,reject) => {
//模拟任务异步执行
setTimeout(() => {
resolve("成功获取到数据");
}, 1000);
})
let promise2 = new Promise((resolve,reject) => {
//模拟任务异步执行
setTimeout(() => {
resolve("promise2成功获取到数据");
}, 1000);
})
promise.then((data) => {
console.log(data); // 打印:成功获取到数据
return promise2; //在 prmise的处理中返回promise2,后续可以一直then进行链式调用
}).then((data) => {
console.log(data); // 打印:promise2成功获取到数据
})
}
注意:当我们创建一个Promise对象而不调用其then方法时,在Promise中写的代码并不会执行。当我们真正调用了其then方法后才会真正执行异步任务
Promise的其他API
- all
当我们需要多个相互无关的异步任务同时执行,等所有异步任务结束后一起拿到结果时,可以使用all方法一起拿到返回结果
let getGoodsInfoPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{
"id": 1,
"name": "戴森吹风机",
"price": 2000
},
{
"id": 1,
"name": "苹果15",
"price": 8999
}
])
}, 1000);
})
let getUserInfoPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"id": 1,
"username": "zhangsan"
})
}, 2000);
})
Promise.all([getGoodsInfoPromise, getUserInfoPromise]).then((result) => {
console.log(result); //两秒后打印。result为一个数组,0号位是货物信息,1号位是用户信息
}).catch((error) => {
console.error(error);
});
- race
当我们需要多个相互无关的异步任务同时执行,只要有一个异步任务执行结束就拿到结果,可以使用race方法,race方法会返回第一个执行结束的异步任务的结果。
let getGoodsInfoPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{
"id": 1,
"name": "戴森吹风机",
"price": 2000
},
{
"id": 1,
"name": "苹果15",
"price": 8999
}
])
}, 1000);
})
let getUserInfoPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"id": 1,
"username": "zhangsan"
})
}, 2000);
})
Promise.race([getGoodsInfoPromise, getUserInfoPromise]).then((result) => {
console.log(result); //一秒后打印。result中只有货物信息
}).catch((error) => {
console.error(error);
});
await和async
await与async是两个关键字,是es6为我们提供处理Promise的语法糖,使用他们我们可以像声明变量一样处理Promise对象。async用在方法上,表明一个方法是一个异步方法,方法的返回值是一个promise对象。await用来处理一个promise对象,可以直接获取成功状态promise的数据,await必须用在async声明的方法内部。
let async1 = async () => {
return "success";
} //创建异步方法async1
async1().then((data) => {
console.log(data); //打印success
}) //调用异步方法,用then获取结果
// 如果在async方法中没有明确返回,则调用后返回的Promise对象的成功处理会得到一个undefined,感兴趣可以自己试一试
let async2 = async () => {
let result = await async1();
console.log(result); //打印success
}
async2().then((data) => {
console.log(data); //打印undefined
});
await只能用来处理成功状态的promise,而不能处理失败状态的,失败状态的promise会用异常的方式抛出,如需处理要用try catch的方式进行处理
let async1 = async () => {
return "success";
}
let async3 = async () => {
let result = await async1();
throw new Error("抛出异常");
}
let async4 = async () => {
try {
let async3Result = await async3();
console.log(async3Result);
} catch (error) {
console.log("异常处理",error) //打印在async3中抛出的异常
}
}
async4();
可见我们可以很方便的使用await来获取我们异步任务的执行结果,像处理同步任务一样。async和awit的本质还是使用Promise的机制实现的。我们使用这两个关键字也可以解决回调地狱的问题,解决方式也更加优雅,代码更加易读。