ES6新特性
let关键字
控制作用域在块级作用域下的定义关键字,并且不允许重复声明,举例:
for(var i = 0; i < 10; i++) {}
console.log(i);
// 输出10
for(let j = 0; j < 10; j++) {}
console.log(j);
// j未定义,报错(let定义的变量出了其所在代码块就销毁)
注:
在没有let
关键字之前,只用用var
定义变量,但由于其作用域在函数级别,因此存在很多问题,例如下面希望实现多个按钮,点击输出对应的索引:
结果发现不管点击哪个按钮,输出的都是3,原因就是var
定义的变量i
作用域在函数,因此这里的i
对于3个按钮来说是共用的,而这个i
经过循环变成了3,所以无论哪个按钮,获得到的i
都是3。为了解决上述问题,在ES6前提出了闭包的解决方案:
但是这样需要多定义一个函数,代码上也显得冗余,因此ES6以后块级作用域的let
关键字完美的解决了这个问题:
const关键字
定义常量,被定义的常量无法被更改,但是要注意定义常量的时候必须为其初始化,以及其作用域和let
一样只在自己的代码块内起作用,并且也不可重复声明。实际上const
相当于一个不能被更改指向的指针,但是如果指向的是对象这样的数据,那么对象的内容是允许更改的,比如下面的操作是允许的:
const a = {name:1}
// 常量a指向一个对象
a["name"] = 2
// 修改对象的值允许
// const a = {name:1}
// 常量重新指向一个对象,此时不允许
数组解构赋值
允许将数组的值分别赋值给多个变量,举例:
[x, y] = [1, 2]
// 此时x=1,y=2
[x, y] = [1, 2, 3, 4]
// 此时只有1和2被赋值给x和y,和上面的等价
[x, y, z] = [1, 2]
// z没有被赋值,为undefined
对于数组当中不希望赋值的数据,可以用逗号间空开,举例:
[a, , c] = [1, 2, 3]
// 此时a=1,c=3,而2没有赋值给任何一个数
如果希望把数组剩余的部分都赋值给某个变量,可以用...变量名
实现,举例:
[a, ...c] = [1, 2, 3]
// 此时a=1,c=[2, 3],要注意...开头的变量必须放在最后
如果担心数组的内容不够赋值给变量,还可以给变量设置默认值,举例:
[a, b, c, d] = [1, 2, 3]
// 此时d没有被赋值,结果为undefined
[a, b, c, d=4] = [1, 2, 3]
// d设置了默认值为4
对于字符串也可以当做数组来解构,举例:
[a, b, c] = 'xyz'
// a='x',...
这种解构在函数传参时也很实用,举例:
function test([a, b, ...c]){
console.log(a + b, c);
}
test([1,2,3,4,5])
// 3, [3, 4, 5]
对象解构赋值
对象的解构赋值基于属性名,举例:
{a, b, c} = {a:1, b:2}
// a为1,b为2,c没有被赋值,为undefined
如果希望属性赋值给变量名不同的变量,则看下面示例:
{a:A, b} = {a:1, b:2}
// 此时a的属性赋值给变量A,所以变量a为未定义,A为1,b为2
由于对象解构时是使用{}
包起来的,因此会被解析成代码块,此时就可能出现作用域冲突问题,可以在外面包一层括号解决,举例:
let x = 0;
({x} = {x:1})
对于多级对象的解构示例如下:
{people: [{name}, age]} = {people:[{name:"aaa"}, 18]}
// 此时name为aaa,age为18
对象解构的默认值设置和数组的同理,举例:
{x=0,y=0} = {x:1}
// 此时x为1,y为0
还有些用法如提取对象中的方法成变量调用,举例:
let {floor} = Math;
// 将Math下的floor方法提取成变量
floor(1.5)
// 调用floor方法
let {length:s_length} = 'hello';
// 将字符串长度属性提取出来
s_length
// 输出字符串长度
对象解构同样可以用于函数传参,且更方便,避免了传递顺序等问题,举例:
function test({a, b}){
console.log(a + b);
}
test({a:1, b:2})
注:
这里顺便介绍一些对象的基本用法,比如直接定义函数,举例:
x = {a:1, b(){return 2;}, c(){console.log(3);}}
// {a: 1, b: ƒ, c: ƒ}
x.b()
// 2
x.c()
// 3
直接传入已赋值的变量或常量,举例:
let x = 1
const y = 2
z = {x, y}
// 将变量或常量传入对象,此时z为:{x: 1, y: 2}
z.y = 3
// 但传入以后都会变成变量,所以z里的y可以修改
z
// {x: 2, y: 3}
数组展开
通过...
可以将数组展开传入,举例:
function test(a, b, c) {
console.log(a + b + c);
}
test(...[1, 2, 3])
// 将数组展开成1, 2, 3赋值给a, b, c
数组展开还可以用于数组拼接,举例:
a = [1,2,3]
b = [4,5,6]
[...a, ...b]
// [1, 2, 3, 4, 5, 6]
实际上数组展开就是把数组当中的一个个数据通过类似迭代器那样给分离出来,因此需要给一个数据类型来存放所有分离出来的数据,举例:
a = [1,2,3]
...a
// 报错:VM323:1 Uncaught SyntaxError: Unexpected token ...,因为分离出来的数据没有东西存放
[...a]
// 通过数组存放a中分离出来的数据,结果为[1, 2, 3]
对象展开
和数组类似,对象也能够通过...
展开,同时也需要数据类型来存放这个展开后的所有数据,举例:
a = {name: 111, pwd: 222}
{...a, b:1}
// 通过对象{b: 1}存放...a,结果为:{name: 111, pwd: 222, b: 1}
字符串新增方法
includes方法
判断字符串是否存在于其中,举例:
"abc".includes('a')
// true
"abc".includes('d')
// false
注:
该方法对数组同样适用,举例:
[1,2,3].includes(1)
// true
startsWith方法
判断字符串是否以其开头,举例:
"abc".startsWith('a')
// true
"abc".startsWith('b')
// false
endsWith方法
判断字符串是否以其结尾
repeat方法
将字符串复制成原来的几倍,举例:
"abc".repeat(3)
// "abcabcabc"
数组新增方法
map方法
对数组数据进行统一操作,举例:
a = [1,2,3,4,5]
a.map(function(item){return item>=3})
// 统一操作判断每个数据是否大于等于3,[false, false, true, true, true]
// 由于上面的函数符合一个参数,且只有一条return语句,因此可以简写如下:
// 简写后:a.map(item=>item>=3)
// 这里容易看混掉的是第一个=>是代表箭头函数,第二个=>代表大于等于运算符
注:
map
方法里其实还有第二个参数,为数据索引,举例:
a = [1,2,3,4,5]
a.map(function(item, index){return [item, index]})
// [[1,0],[2,1],[3,2],[4,3],[5,4]]
reduce方法
对整个数组进行循环某运算,其下有三个参数为:tmp
/item
/index
,分别代表上一次操作返回的结果(第一次默认为数组的第一个数)、下一个传入的值(即数据的下一个数)、循环的索引,举例:
a = [1,2,3,4,5]
a.reduce(function(tmp, item, index) {
console.log(tmp, item);
return tmp + item;
// 返回结果为当前数和下一个数的求和
})
// 最后的结果为15
filter方法
根据条件过滤数据,举例:
a = [1,2,3,4,5]
a.filter(item => item%2==0)
// 返回所有偶数,结果为[2, 4]
forEach方法
遍历数组,举例:
a = [1,2,3,4,5]
a.forEach((item, index) => {console.log(item, index)})
// 循环输出:1 0、2 1、...
some方法
循环判断数组中是否存在满足条件的数据,只要有一个满足就返回true
,举例:
a = [1,2,3,4,5]
a.some(item=>item>=3)
// 有大于等于3的数,返回true
every方法
和some
方法类似,循环判断数组中是否全部满足条件,是则返回true
,举例:
a = [1,2,3,4,5]
a.every(item=>item>=3)
// 有不大于等于3的数,返回false
对象新增方法
Object.keys()
会将对象的所有键以数组形式输出,举例:
o = {a:1, b:2, c:3}
Object.keys(o)
// ["a", "b", "c"]
Object.values()
会将对象的所有值以数组形式输出
Object.entries()
会将对象的所有键和值以数组形式输出,数组中的子元素是长为2的数字,分别为键和值,举例:
o = {a:1, b:2, c:3}
Object.entries(o)
// [Array(2), Array(2), Array(2)]
Object.entries(o)[0]
// ["a", 1]
模板字符串
原来的字符串要拼接值都是通过多个+
号实现,而通过反引号包起来的模板字符串则可以通过${value}
传值,举例:
x = 2
`1${x}3`
// 123
模板字符串还可以嵌套,举例:
`1${x+`${y}`}3`
// 1203
Symbol数据类型
生成一个每次都不同的随机变量,一般用于防止对象属性被修改,举例:
a = Symbol()
b = Symbol()
a == b
// false
c = {}
c[a] = 1
c[b] = 2
c
// {Symbol(): 1, Symbol(): 2},可以看出a的值没有被b给覆盖
Set类型
集合,举例:
s = new Set([1,2,3,3])
// Set(3) {1, 2, 3},可以看出结果为3个
s.size
// 3,相当于length
s.add(4)
// Set(4) {1, 2, 3, 4},添加了4进去
s.delete(5)
// false,没有5,删除失败
s.delete(3)
// true,删除成功
s.has(2)
// true,存在2
s.clear()
// 清空集合
函数参数默认值
函数支持给参数设置默认值,举例:
function test(x, y=1){
return x + y;
}
test(1);
// 2
test(1, 2);
// 3
常量函数命名
可以通过定义一个常量而不用function
关键字来命令一个函数,举例:
const FUN = "test";
// 定义一个常量FUN,值为test
funs = {
[FUN] (x, y) {
return x + y;
}
}
// 定义以后,可以发现funs为:{test: ƒ},即定义了一个名为test的函数
funs.test(1, 2)
// 3
不过本人尝试以后发现貌似不用常量,用字符串也可以这样定义(常量传进对象本身也都不是常量了)
箭头函数
能够实现对函数的简写,举例:
function(x, y) {
...
}
// 上面的原来函数的写法
(x, y) => { ... }
// 通过箭头函数可以简写并替代上面的写法
其中使用箭头函数有以下注意事项:
- 当参数只有一个时,可以省略
()
,举例:
function(data) {
console.log(data);
}
// 只有一个参数
data => {console.log(data);}
// 省略了括号
- 如果函数里只有一个
return
语句,那么可以不用加{}
,并且可以省略return
关键字,举例:
function(x, y) {
return x + y;
}
// 只有一个return语句
(x, y) => x + y;
// 省略了花括号和return关键字
注:
通过箭头函数还能够绑定this
,使得this
一直为当前的环境而不会被变更,举例:
a = {
name: "aaa",
fn: function() {
console.log(this, this.name + "bbb")
// 使用function定义的this会绑定到当前对象,因此输出为当前对象a和"aaabbb"
},
fn1: () => {
console.log(this, this.name + "bbb")
// 箭头函数使得this绑定在window对象,因此输出为window对象和"bbb"
}
}
a.fn()
a.fn1()
JSON操作
对象转json
通过JSON
对象的stringify
方法实现,举例:
JSON.stringify({a:1, b:2})
// "{"a":1,"b":2}"
json转对象
通过JSON
对象的parse
方法实现,举例:
JSON.parse('{"a":1,"b":2}')
// {a: 1, b: 2}
面向对象
ES6之前JS的面向对象是一个伪面向对象,存在着许多问题,比如用function
关键字定义类,以至于不知道是函数还是类、添加方法方式限制不严,导致代码编写可能不统一等等一系列问题。因此在ES6中提供了几个面向对象的关键字统一了JS的面向对象编程,包括:class
(定义类)、constructor
(构造器)、extends
(继承)、super
(父类),static
(定义静态属性/方法)举例:
class User {
constructor(name, pwd) {
// 构造方法
this.name = name;
this.pwd = pwd;
}
showName() {
// 直接输入方法名定义方法
console.log(this.name);
}
static showRule() {
// 定义静态方法,可以从类直接调用而无需实例化
console.log("this is User rule");
}
static rule = "this is rule";
// 定义静态属性,如果报错可能是浏览器版本不支持,建议换成最新chrome尝试
}
class Vip extends User {
// 继承父类
constructor(name, pwd, level) {
super(name, pwd);
// 调用父类构造方法
this.level = level;
}
showPwd() {
console.log(this.pwd);
}
showLevel() {
console.log(this.level);
}
}
console.log(User.rule);
User.showRule();
// 直接从类调用静态方法
let user = new Vip("aaa", "111", "1");
user.showName();
user.showPwd();
user.showLevel();
模块化
使用步骤
1.通过export
导出成员
2.在另一个文件当中通过import
接收成员
3.引用通过webpack
编译后生成的bundle.js
文件
举例:
- a.js
export let a = { name: "aaa" };
export const b = 111;
// 导出成员a和b
- b.js
import * as a from './a'
// 导入a.js里的所有成员,注意相对路径需要加`./`这样的定位符
console.log(a.a, a.b);
// 输出导入的成员
- webpack.config.js
module.exports = {
mode: 'development',
entry: './b.js',
output: {
filename: 'bundle.js',
path: __dirname + '/build'
}
}
导入webpack生成的文件后可以发现输出了a
和b
的值。
注:
注意相对路径需要加./
这样的定位符,因为模块化需要在node环境下使用,如果不加定位符,则默认去依赖模块当中寻找,而不会去相对路径下找
默认成员default
在模块化当中还可以导出默认成员default
,则通过导入模块成功语法时,只导入默认成员default
的值,举例:
- a.js
export default "aaa";
// 导出默认成员"aaa"
- b.js
import a from './a'
// 导入默认成员default,并命名为a,相当于下面这个意思
// import default as a from './a'
console.log(a);
这种语法在vue
的组件化工程当中很常见
导入成员语法
前面介绍了两种语法,一种是导入全部:
import * as xxx from './xxx'
还有导入默认默认成员语法:
import xxx from './xxx'
如果希望导入指定的成员也可以:
import {xxx, yyy as zzz} from './xxx'
// 导入./xxx文件里的xxx和yyy成员,并将yyy改名为zzz
还有导入如CSS文件、图片文件等模块时(在模块化开发下一些静态文件都称为模块),因为这些文件里可能不存在成员啥的,所以可以只导入,举例:
import './xxx'
还有异步引入,使用import
方法,举例:
let x = import('./xxx')
// 当需要用到的时候引入,简化代码体积
// 传回来的是个Promise对象,需要使用await关键字等待
模块化导入路径问题
-
'xxx'
:直接从npm依赖里寻找相关模块 -
'./xxx'
/../xxx
:从相对路径当中寻找相关模块 -
@/xxx
:从src目录下寻找相关模块
浏览器兼容
对于ES6以上的写法,在一个古老的浏览器上是不兼容的,因此需要对这些内容进行编译
babel.js编译
需要安装node环境
安装
npm install @babel/core @babel/cli @babel/preset-env -D
使用步骤
1.初始化项目环境:npm init -y
2.在安装好babel后,在项目下添加一个文件.babelrc
,添加内容如下:
{
presets: ["@babel/preset-env"]
}
3.在src
目录下编写js文件
4.输入命令:babel src -d dest
,即可在dest
目录发现文件被编译成兼容低版本浏览器的js文件了
注:
如果嫌命令麻烦,也可以打开package.json
文件,在script
属性下配置自定义命令:"自定义命令": "babel src -d dest"
,此时只需要输入命令:npm run 自定义命令
即可
ES7新特性
幂操作
通过**
可以实现幂运算,举例:
2**3
// 8
(感觉这越发展越像java和python的结合体了)
ES8新特性
异步操作控制
对于异步操作(如ajax)本身带来了很大的便利,但是由于其异步的关系,有些依赖性的异步代码则很可能陷入一种回调地狱,比如下面的代码:
$.ajax({
url: "https://xxx",
success(data) {
// 当访问xxx成功时访问yyy
$.ajax({
url: "https://yyy",
success(data) {
// 当访问yyy成功时访问zzz
$.ajax({
url: "https://zzz",
success(data) {
// 一直回调下去...
},
error() {}
})
},
error() {}
})
},
error() {}
})
上面的代码里对于yyy的访问是基于xxx访问成功的前提下,而zzz的访问是基于yyy访问成功的前提下,一直这样下去,代码会十分混乱,所以为了美化这种代码,提出了两种解决方案:
- Promise对象(其实是ES5.5的内容)
- async/await关键字
Promise对象
用于封装异步操作,使用步骤如下:
1.实例化一个Promise()
对象
2.将所有异步操作封装到Promise()
对象内部,里面传入两个参数,分别是执行成功和失败的回调函数
3.通过Promise()
对象的then()
方法调用异步操作时执行的成功和失败的回调函数
举例:
let p = new Promise(function(resolve, reject) {
// 封装异步操作,传入成功时执行的resolve函数和失败执行的reject函数
$.ajax({
url: "https://xxx",
success(data) {
resolve("success", data);
// 成功的话执行resolve函数,并传入一个状态和对应的数据,在后面then的时候调用
},
error(data) {
reject("error", data);
// 失败执行reject函数,在后面catch的时候调用
}
});
});
// 在这里进行异步操作的回调处理
p.then(function(status, data) {
// 成功时的回调函数,相当于此时执行resolve方法
console.log(status, ":", data);
}).catch(function(status, data) {
// 失败时的回调函数,相当于此时执行reject方法
console.log(status, ":", data);
});
从上面代码可以看出,此时整个异步操作被放在了一个对象p里,然后后续只需要调用p.then().catch()
来进行异步操作的回调即可,相比之前的来说,这个显然看起来更加直观
Promise统一封装
Promise对象下有一个all
方法,可以将所有的异步操作通过数组方式传入,然后当所有异步操作都执行成功或存在失败操作时通过then
方法调用后续操作,举例:
new Promise.all([
// 传入一个包含多个异步操作的数组
$.ajax({ url: "https://xxx", dataType: "json"}),
$.ajax({ url: "https://yyy", dataType: "json"}),
$.ajax({ url: "https://zzz", dataType: "json"}),
]).then(
// 通过then方法调用后续操作,分别传入一个成功和失败的处理方法
([data1, data2, data3]) => {
// 返回的数组分别是data1,data2,data3,通过结构方式接受
console.log(data1, data2, data3);
}, res => {
console.log("失败");
})
Promise异步择优
Promise下有race
方法和all
方法操作类似,也是传入一个异步操作数组,然后当最快的一个异步操作完成时则调用then
方法进行后续操作,并忽视其他的异步操作,若所有操作都失败才会执行错误方法,常用于类似CDN加速时选择最快的链接场景,举例:
Promise.race([
$.ajax({ url: "https://xxx", dataType: "json"}),
$.ajax({ url: "https://yyy", dataType: "json"}),
$.ajax({ url: "https://zzz", dataType: "json"}),
]).then(
(data) => {
// 只对第一个完成的操作进行回调
console.log(data);
}, res => {
console.log("失败");
})
async/await关键字
前面的Promise对象虽然让回调代码更加直观,但在封装时感觉还是挺不简洁的,而async
和await
可以让异步的代码更加直观,能使我们编写异步代码时更加贴近于同步代码的写法。使用这两个关键字可以控制异步操作是否暂停,async
关键字用来声明操作中存在异步,只需在操作前声明这个关键字即可,而在async
关键字下的异步操作如果存在需要等待的操作,可以使用await
关键字声明,举例:
async function aaa() {
console.log(0);
let x = await $.ajax({ url: "https://xxx", dataType: "json" }, function(data) { console.log(data); })
// x接收异步操作返回的数据
// 由于这里使用了await等待,所以在该步会等待异步操作执行
// 如果异步操作失败成功则执行后续步骤,否则不会进行后面的操作
console.log(1);
let y = await $.ajax({ url: "https://yyy", dataType: "json" }, function(data) { console.log(data); })
console.log(2);
let z = await $.ajax({ url: "https://zzz", dataType: "json" }, function(data) { console.log(data); })
console.log(3);
}
上面是添加了async
和await
关键字以后的代码,其和没添加的代码结果有什么区别呢?如果是没添加这两个关键字的的情况,那么结果异步操作不管是否执行成功,一般都是先依次输出0
/1
/2
,然后再输出三个异步请求的回调方法(成功或者失败);而添加了关键字以后,就会先去等待第一个await
的地方的执行,并且判断执行是否成功,如果成功则继续往后执行,否则就报错不继续执行,所以就会先输出0
,然后等待一段时间,当第一个await
地方执行成功则继续往后输出1
,否则报错后停止,以此类推...
注:
从上面可以看出代码相比使用Promise
对象要更加的简洁,而其返回的对象(上面变量x、y、z)接收的内容,实际上还是Promise
对象,所以可以理解成这两个关键字就是对Promise
对象的进一步封装。
注2:
async
和await
关键字组合操作的原理就是将函数拆分成一个个小的存在依赖关系的函数执行,比如上面的实际上就可以看成如下代码:
function aaa() {
console.log(0);
let x = $.ajax({
url: "https://xxx",
dataType: "json"
}, function(data) {
console.log(data);
// 第一个await执行成功则执行后续内容
console.log(1);
let y = $.ajax({
url: "https://yyy",
dataType: "json"
}, function(data) {
console.log(data);
// 第二个await执行成功则执行后续内容
console.log(2);
let z = $.ajax({
url: "https://zzz",
dataType: "json"
}, function(data) {
console.log(data);
// 第三个await执行成功则执行后续内容
console.log(3);
})
})
})
}
可以看出这个和最开始回调地狱的代码一样,但是通过两个关键字修饰了以后,达到了简化代码的目的
更多参考
http://es6.ruanyifeng.com/#docs/module