前言
在日常开发中,有些语法稀里糊涂的就用着了,用着还挺方便好用的,但是如果不深入探索就不知道它出自哪里,原理是什么,下面就是整理的一些比较常用的ES6语法
let 和 const
ES6 新增了 let 和 const,它们的用法类似于 var,但是它们所声明的变量和常量只能在块级作用域有效,也会就是大括号{}内,那么它们三者之间的有什么区别呢?请看下面解释:
var 声明的是全局变量,存在变量提升
let 声明的是局部变量,不允许重复声明,
存在暂时性死区:
ES6 明确规定,如果区块中存在
let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
var temp = 123;
if(true){
temp = 'abc'; //ReferenceError
let temp;
console.log(temp1) //ReferenceError
const temp1 =234
}
const 声明的是常量,一旦声明就必须赋值,声明的值不能修改
{
console.log(temp2) //undefined
var temp2 = 123;
temp = 'abc'; //ReferenceError
let temp;
console.log(temp1); //ReferenceError
const temp1 =234;
const temp3; //Uncaught SyntaxError: Missing initializer in const declaration
}
解构赋值
解构赋值语法是一种 Javascript 表达式。可以将数组中的值或对象的属性取出,赋值给其他变量。
let a,b,rest;
[a,b] = [10,20];
console.log(a) //10
console.log(b) //20
[a,b,...rest] = [10,20,30,40,50];
console.log(rest) //[30,40,50]
数组解构:在赋值的左侧定义了要从原变量中取出哪些值,赋值对应着相应的下标
const x = [1, 2, 3, 4, 5];
const [y, z] = x;
console.log(y); // 1 y的下标为0,右侧数组1的下标为0
console.log(z); // 2
对象解构:与数组解构不同的是,它不是根据次序(下标)进行匹配,而是只需要变量名属性名相同即可
const obj = {a:1,b:2}
const {a,b} = obj
console.log(a,b) //1,2
const {b} = obj
console.log(b) //2
此外我们还可以设置默认值
每个解构属性都可以有一个默认值。当属性不存在或值为
undefined时,将使用默认值。如果属性的值为null,则不使用它、
const [a = 1] = [] //a=1
const {b = 2} = {b:undefined} // b = 2
const {c = 3} = {c:null} //c = null
const {a} = {b:2} //a = undefined
const {a,b = 2} = {a:1} //a = 1 ,b = 2
扩展运算符
对象的扩展运算符
对象中的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中
const bar = {a:1,b:2}
const bab = {...bar} //{a:1,b:2}
实际上等价于
const bar = {a:1,b:2};
const bab = object.assign({},bar) // {a:1,b:2}
有的同学不是很了解Object.assign的作用或者概念。
Object.assign()方法将所有可枚举(Object.propertyIsEnumerable()返回 true)的自有(Object.hasOwnProperty()返回 true)属性从一个或多个源对象复制到目标对象,返回修改后的对象。如果目标对象与源对象具有相同的 key,则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的属性。
const target = {a:1,b:2}
const source = {b:4,c:5}
const returnTarget = Object.assign(target,source)
console.log(target) //{a:1,b:4,c:5}
数组的扩展运算符
扩展运算符可以将数组转化为参数序列
function add(x,y){
return x+y;
}
const arr1 = [4,5]
add(...arr1) //9
console.log(...arr1) //4,5
需要注意一点的就是扩展运算符对对象实例的拷贝属于一种浅拷贝,对于基本数据类型的拷贝会完整的复制一份,复制后修改不会改变原来的对象,对于引用数据类型,在拷贝时拷贝的是对象的引用,当原对象发生变化时,拷贝对象也跟着变化
const obj1 = {a:1,b:2}
const obj2 = {...obj1}
obj2.a = 3
console.log(obj1) //{a:1,b:2}
console.log(obj2) //{a:3,b:2}
const obj1 = {a:1,b:2,c:{d:"haha"}}
const obj2 = {...obj1}
obj2.c.d = "xixi"
console.log(obj1) //{a:1,b:2,c:{d:"xixi"}}
console.log(obj2) //{a:1,b:2,c:{d:"xixi"}}
若扩展运算符用于数组赋值,只能放在参数的最后一位,否则报错
const [...rest,last] = [1,2,3,4,5] //报错
const [frist,...rest,last] = [1,2,3,4,5] //报错
const [frist,...rest] = [1,2,3,4,5] //不报错
Set 和 Map
Set 是什么呢
Set对象是值的集合,你可以按照插入的顺序迭代它的元素。Set 中的元素只会出现一次,即 Set 中的元素是唯一的。
学习Set需要掌握它的属性和方法
Set 实例的属性和方法
Set本身是一个构造函数,用于生成Set数据结构
Set().size :返回Set实例的成员总数。
Set().add()如果没有相同的元素,则将在Set对象尾部添加一个元素
Set().clear 从对象中移除所有元素
Set().delete(value) 删除与之关联的元素value并返回一个布尔值来表示是否移除成功
Set().has(value) 判断对象中是否存在具有给定值得元素,返回一个布尔值
const set = new Set()
set.add(1).add(2).add(2).add(4);//2被添加了两次
set.size //3
set.has(1) //true
set.has(3) //false
set.delete(2) //true
set.has(2) //false
set.clear
set.has(1) //false
由于Set中的元素是唯一的特性,常被用于数组的去重
const arr1 = [1,1,2,2,3,3,5,5,5,5,5]
console.log([... new Set(arr1)]) //[1,2,3,5]
然而Map又是什么呢
它类似于对象,也是键值对的集合,但是“键”的范围可以是字符串,数字,布尔值,甚至是对象都可以当做键。它跟对象的区别是,Object结构提供了“字符串--值”的对应,Map结构提供了“值--值”的对应
Map对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值。
Map对象是键值对的集合。Map中的一个键只能出现一次;它在Map的集合中是独一无二的。Map对象按键值对迭代——一个for...of循环在每次迭代后会返回一个形式为[key,value]的数组。迭代按插入顺序进行,即键值对按set()方法首次插入到集合中的顺序(也就是说,当调用set()时,map 中没有具有相同值的键)进行迭代。
const map = new Map()
map.set('a',1)
map.set('b',2)
map.set('a',3)
console.log(map) // {'a' => 3, 'b' => 2}
Map 的属性和方法
1、Map().size 返回 Map结构的成员总数。
const map = new Map()
map.set('foo',true)
map.set('bar',false)
map.size //2
2、Map().set(key,value) 设置键名 key 对应的键值为value,然后返回整个Map结构,如果Key已经有值,则键值会被更新,否则就新生成该键。
const map = new Map()
map.set('a',1)
map.set('b',2)
map.set('a',3)
console.log(map) // {'a' => 3, 'b' => 2}
3、Map().get 方法读取key对应的键值,如果找不到key,则返回undefined
const map = new Map()
map.set('a',1)
map.set('b',2)
console.log(map.get('a')) // 1
4、Map().has(key) 返回一个布尔值,表示某个键是否在当前Map中
const map = new Map()
map.set('a',1)
map.set('b',2)
map.has('a') // true
map.has('c') //false
5、Map().delete(key) 删除某个键,返回true。如果删除失败,返回false
const map = new Map()
map.set('a',1)
map.set('b',2)
map.has('a') // true
map.delete('a') //true
map.has('a') // false
6、Map().clear() 清除所有成员,没有返回值
const map = new Map()
map.set('a',1)
map.set('b',2)
map.clear()
map.has('a') //false
Promise对象
Promise的概念:
Promise 是异步编程的一种解决方案,比传统的解决方案--回调函数和事件--更加合理和更加强大。有了Promise对象,就可以把异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数(回调地狱)。
Promise是什么呢?
Promise对象简单来说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。并且Promise提供统一的API,各种异步操作都可以以同样的方法处理
Promise 对象有以下两个特点:
-
对象的状态不受外界影响:
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。Promise的英文意思是“承诺”,就是答应了就不会反悔的意思 -
一旦状态改变就不会再次发生改变:任何时候都可以得到这个结果。
Promise对象的状态只有两种可能:从pending变为fulfilled或者从pending变为rejected。只要这两种情况发生改变,状态就会凝固,不会再变了,会一直保持这个结果,如果改变已经发生,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件完全不同(事件的特点是,如果你错过了它,再去监听,是得不到结果的)。
Promise构造函数
ES6规定,Promise对象是一个构造函数,用于生成 Promise实例
const promise = new Promise((resolve,reject) =>{
if('异步操作成功'){
resolve(value)
}else{
reject(error) //失败执行reject
}
})
我举个例子吧,就用 技术蛋老师 的那个例子会比较有趣:
一个女人问男人,我怀孕了你会跟我结婚吗,男人说会的,这时候就是给了女人一个承诺(
Promise),但是女人到底有没有怀孕男人现在是不知道的,如果怀孕了就是叫女人为孩子他妈(fulfilled),如果没有怀孕就叫女人为老婆(rejected),男人还是要兑现承诺跟女人结婚,十个月之后,如果孩子生出来了就叫女人为“孩子他妈”(then),女人没有生出孩子来就只能叫女人为“老婆”了(catch)
Promise在一开始都是 pending状态,之后执行完逻辑后就会变成 setted(fulfilled或rejected),
当 Promise 进入到 fulfilled 状态是会调用 then 函数
当 Promise 进入到 rejected 状态是会调用 catch 函数
当 Promise 进入到 setted 状态是会调用 finally 函数
const isMaternity = true
function Marry(){
return new Promise((resolve,reject) =>{
setTimeout (() =>{
if(isMaternity){
resolve()
}else{
reject()
}
},1000)
})
}
Marry().then(res =>{
console.log('孩子他妈')
})
.catch(()=>{
console.log('老婆')
})
.finally(() =>{
console.log('结婚')
})
箭头函数
箭头函数表达式的语法比较普通函数表达式更加简洁,并且没有自己的 this , arguments , super 或 new.target 。箭头函数表达式更加适用于那些本来需要匿名函数的地方,并且它不能用作构造函数
引入箭头函数有两个主要的作用:更加简短的函数并且不绑定 this
- 更短的函数
const arr1 = ['HelloWord' ,'javascript']
arr1.map(function (item) {
return item.length
})
//返回数组[9,10]
//把上面的普通函数换成箭头
arr1.map((item)=> {
return item.length
}) //[9,10]
//如果只有一个参数可以把括号去掉
arr1.map(item => {
return item.length
}) //[9,10]
//当箭头函数的函数体只有一个 return 语句时,可以省略 `return` 关键词和方法体的花括号
arr1.map(item => item.length) //[9,10]
- 没有单独的
this
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义函数的 this值:
- 如果该函数是一个构造函数,this 指针指向一个新的对象
- 在严格模式下的函数调用下,this 指向 undefined
- 如果该函数是一个对象的方法,则它的 this 指针指向这个对象
function Person(){
//Person()构造函数定义 `this` 作为它自己的实例
this.age = 0
setInterval(function growUp() {
//在非严格模式,growUp() 函数定义 `this` 作为全局对象,与在 Person() 构造函数中定义的 `this`并不同。
this.age++
},1000);
}
var p = new Person();
箭头函数不会创建自己的 this 它只会从自己的作用域的上一层继承 this 。因此,在下面的代码中,传递给 setInterval 的函数内的 this 与封闭函数的this值相同:
function Person(){
this.age = 0;
setInterval(() =>{
this.age++; //|this| 正确地指向 p 实例
},1000);
}
var p = new Person()
现在我们大概了解了箭头函数的this,那么它跟普通函数有什么区别呢?
普通函数中的 this
- 总是代表着它的直接使用者,如 obj.fn ,fn 里的最外层this就是指向 obj
- 默认情况下,没有直接调用者,this 指向 window
- 严格模式下,this 为 undefined
- 当使用 call 、apply 、bind绑定的,this指向绑定对象 :
call 方法的第一个参数是 This 的指向,后面传入的是一个参数列表。当第一个参数为 null 、undefined 的时候,默认指向 windown 。如 getColor(obj,'green','red','blue')
apply方法接收两个参数,第一个参数是 this 的指向,第二个参数是一个参数数组。当第一个参数为 null 、undefined 的时候,默认指向 windown 。 getColor(obj,['green','red','blue])
bind 方法和 call 方法很相似,第一个参数是 this 的指向,从第二个开始接收的是参数列表。区别在于 bind 方法返回值是函数以及 bind 接收的参数列表的使用
var id = 'global'
let fun1 = () =>{
console.log(this.id)
}
fun1()
fun1.call({id:'obj'}) //global
fun1.apply({id:'obj'}) //global
fun1.bind({id:'obj'}) //global
箭头函数中的 this
- 默认指向定义它时,所处上下文的对象的 this 指向。即ES6 箭头函数里 this 的指向就是上下文里对象 this 指向,偶尔没有上下文对象,this就指向 windown
- 即使是 call 、 apply 和 bind 等方法也不能改变箭头函数 this 的指向
//fn 是全局函数,没有直接调用它的对象,也没有使用严格模式,this 指向 windown
function fn(){
console.log(this) //windown
}
fn()
//fn 是全局函数,没有直接调用它的对象,但是指定了严格模式,this 指向 undefined
function fn(){
'use strict'
console.log(this)
}
fn()
//fn 直接调用者是 obj ,第一个 this 指向 obj,setTimeout 里匿名函数没有直接调用者, this 指向 windown
const obj = {
num:10
fn:function() {
console.log(this) //obj
setTimeout(function () {
console.log(this) //windown
})
}
}
obj.hello();
//fn 直接调用者是 obj ,第一个 this 指向 obj,setTimeout箭头函数,this 指向最近的函数的 this 指向,也就是 obj
const obj = {
num: 10
fn:function (){
console.log(this); //obj
setTimeout(() =>{
console.log(this) //obj
});
}
}
obj.hello()
//diameter是普通函数,里面的this指向直接调用它的对象obj。perimeter是箭头函数,this应该指向上下文函数this的指向,这里上下文没有函数对象,就默认为window,而window里面没有radius这个属性,就返回为NaN
const obj = { radius: 10, diameter() { return this.radius * 2 }, perimeter: () => 2 * Math.PI * this.radius } console.log(obj.diameter()) // 20 console.log(obj.perimeter()) // NaN
再温习一个箭头函数的概念:
箭头函数没有自己的 this ,它会捕获自己在定义时(注意,是定义时,而不是调用时)所处的外层执行环境的 this ,并继承这个 this 值。所以箭头函数中 this 的指向在它被定义的时候就已经确定了,之后永远不会改变
var id = 'golba'
function fun1(){
setTimeout(function(){
console.log(this.id)
},2000)
}
function fun2(){
setTimeout(()=>{
console.log(this.id);
},2000)
}
fun1.call({id:'obj'}) //golbal
fun2.call({id:'obj'}) //obj
上面这个例子,函数fun1中的setTimeout中使用普通函数,2秒后函数执行时,这时函数其实是在全局作用域执行的,所以this指向Window对象,this.id就指向全局变量id,所以输出'Global'。 但是函数fun2中的setTimeout中使用的是箭头函数,这个箭头函数的this在定义时就确定了,它继承了它外层fun2的执行环境中的this,而fun2调用时this被call方法改变到了对象{id: 'Obj'}中,所以输出'Obj' (注意上面的定义时而不是调用时)
var id = 'global'
var obj = {
id:'obj',
a:function(){
console.log(this.id)
},
b:() =>{
console.log(this.id)
}
}
obj.a() //obj
obj.b() //global
对象obj的方法a使用普通函数定义的,普通函数作为对象的方法调用时,this指向它所属的对象。所以,this.id就是obj.id,所以输出'OBJ'。 但是方法b是使用箭头函数定义的,箭头函数中的this实际是继承的它定义时所处的全局执行环境中的this,所以指向Window对象,所以输出'GLOBAL'。(这里要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中!! )
箭头函数继承而来的this指向永远不变,上面的例子可以看出箭头函数继承来的 this 指向永远不变。对象 obj 的方法 b 是使用箭头函数定义的,这个函数的 this 就永远指向它定义时所处的全局执行环境的 this ,即便这个函数是作为对象 obj 的方法调用,this依旧指向 `windown·
类
我们可能听说过类,在实际开发中一般比较少会用到类。那么类是什么呢?
实际上,类是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类语法有两部分组成:类表达式和类声明
类声明
定义类的一种方法是类声明。要声明一个类,你可以使用带有 class 关键字的类名
class Person{
constructor (name,age){
this.name = name
this.age = age
}
}
函数声明和类声明之间的一个重要区别是:函数声明会提升,而类不会。你要先声明你的类,然后再访问它,否则类似以下的代码将抛出 ReferenceError
let p = new Person() //ReferenceError
class Person{}
//...................
let p1 = new Person1() //不报错
function Person1(){}
使用 extends 扩展子类
exends 关键字在 类声明和类表达式中用于创建一个类作为另一个类的子类
class Animal {
constructor(name){
this.name = name;
}
speak(){
console.log(`${this.name} makes a noise`);
}
}
class Dog extends Animal {
constructor(name){
//如果子类定义了构造函数,那么它必须先调用`super` 才能使用 `this`
super(name)
}
speak(){
console.log(`${this.name} barks.`)
}
}
var d = new Dog('Mitzie')
d.speak() //Mitzie barks.
模块化(Module)
在日常开发中我们常用到导入(import)、导出(export),其实这个ES6新增的模块加载
在 ES6 之前社区制定了一些模块加载方案,主要 Common JS 和 AMD ,前者用于服务器,后者用于浏览器
CommonJS主要用在node开发上,每个文件就是一个模块,每个文件就是一个模块,每个文件都有自己的一个作用域。通过module.exports暴露public成员。通过require引入模块依赖
let x = 1;
let addx = function (value) {
retuen value + x
}
module.export.x = x
module.export.addx = addx
//使用 `require` 引入
var example = require('暴露文件路径')
ES6 module 和 Common JS 以及 AMD的比较
CommonJS 和 AMD 模块 , 采用的是 “运行时加载” , 是将暴露的变量、方法等构成一个对象,然后再从这个而对象上读取对应的方法。也就是说会将所有的方法都进行加载。
ES6模块 ,采用的是 “编译时加载”或者静态加载。即只加载需要的方法,其他的方法不加载。
ES6的模块自动采取严格模式,不管你有没有在模块头部加上‘use strict’
严格模式主要有以下限制 :
- 变量必须声明后再使用
- 函数参数不能有同名属性,否则报错
- 不能使用with 语句
- 不能对只读属性赋值,否则报错
- 不能使用 arguments.callee
- 不能使用 arguments.calles
- 禁止this 指向全局对象
- 不能使用前缀0表示八进制数,否则报错
- 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
- eval不会在它的外层作用域引入变量
- eval和arguments不能被重新赋值
- arguments不会自动反映函数参数的变化
const lastName = "Jone"
export {lastName}
import {lastName} from '路径'
Proxy
proxy :用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
var proxy = new Proxy(target,hander)
target表示所要拦截的目标对象(任何类型的对象,包括原生数组、函数、甚至另一个代理)
handler通常以函数作为属性的对象,各属性的函数分别定义了在执行各种操作时的代理行为
参数 handler 属性:
- get(target,propKey,receiver):拦截对象属性的读取
- set(target,propKey,value,receiver):拦截对象属性的设置
- has(target,propKey) : 拦截
propKey in proxy的操作,返回一个布尔值 - deleteProperty(target,propKey) :拦截
delete proxy[propKey]的操作,返回一个布尔值 - ownKeys(target):拦截
Object.keys(proxy) 、 for...in等循环,返回一个数组 - getOwnProertyDescriptor(target,proKey):拦截
Object.getOwnProertyDescriptor(target,proKey),返回属性的描述对象 - defineProperty(target,propKey,propDesc):拦截
Object.defineProperty(target,propKey,propDesc)返回一个布尔值 - preventExtensions(target):拦截
Object.preventExtensions(proxy),返回一个布尔值 - getPrototypeOf(target) :拦截
Object.getPrototypeOf(proxy),返回一个对象 - isExtensible(traget):拦截
Object.isExtensible(proxy),返回一个布尔值 - setPrototypeOf(target,proto):拦截
Object.setPrototypeOf(target,proto),返回一个布尔值 - apply(target,object,args):拦截proxy实例作为函数回调的操作
- construct(target,args):拦截proxy实例作为函数回调的操作
看到proxy ,有没有跟 Object.defineProperty 非常相似呀,其中 get 和 set 属性都是一样的
Object.defineProperty的作用:直接在一个对象上新增一个属性、修改和删除已有的属性值,并返回这个对象 ,同时proxy也具备这些作用,而且性能更好。
它们最大的区别就是,拦截修改的对象不同,先看Object.defineProperty的参数(object,propName,descriptor)
- object对象 =>对哪个对象进行操作
- propName属性名 =>要操作对象里的哪个属性
- descriptor属性描述 => 加的这属性有什么特性(属性的值,可以是数字、字符串和方法等)
var user ={ name:"嘿嘿" }
var count = 12; //定义一个age 获取值时返回定义好的变量count
Object.defineProperty(user,"age",{
//value:18
get:function(){ return count;
}
})
console.log(user.age);//12
而 proxy 拦截的是整个对象,它的参数只有两个(target,handler)
在使用 proxy之前先介绍一下 relect
ES6 中操作对象而提供的新 API ,若需要在 Proxy内部调用对象的默认行为,建议使用 reflect,它的特性:
- 只要
proxy对象具有的代理方法(handler),reflect对象全部都有,以静态方法的形式存在 - 修改某些
object方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false) - 让
objet操作都变成函数行为
**get()**方法
let person = {
name:"huahua"
}
let proxy = new Proxy(person,{
get:function(target,propKey,receiver){
return Reflect.get(target,propKey,receiver)
}
})
console.log(proxy.name) //huahua
get还能对数组的增删改查进行拦截
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
}
let arr = createArray('a', 'b', 'c');
arr[-1] // c
set()
const obj = {name:"张三",age:18};
const proxy = new Proxy (obj,{
get(target,prop){
if(prop in target){
return Reflect.get(target,prop);
}else{
console.error("字段不存在")
return undefined
}
},
set(target,propKey,value,receiver){
if(propKey === "age"){
if(typeof value === "number"){
return Reflect.set(target,propKey,value,receiver);
}else{
console.log("年龄只能输入正整数");
return false;
}
}else{
return false
}
}
})
proxy.age = 20
console.log(proxy.age)
proxy.age = "20"
console.log(proxy.age)
console.log(proxy.tar)
// 20
//年龄只能输入正整数
//20
//字段不存在
//undefined
使用场景:
- 拦截和监视外部对象的访问
- 降低函数或类的复杂度
- 在复杂操作前对操作进行校验或者对所需资源进行管理
使用proxy 保障数据类型的准确性
let data = {num:0}
data = new Proxy(data,{
set(target,key,value,proxy){
if(typeof value !== 'number'){
console.error('属性只能是number类型')
}
return Reflect.set(target,key,value,proxy)
}
})
data.num = 'foo' //属性只能是number类型