ES6语法知识

let/const(经常使用)

let,const用于声明变量,用来替代老语法的var关键字,与var不一样的是,let/const会建立一个块级做用域(通俗讲就是一个花括号内是一个新的做用域)前端

这里外部的console.log(x)拿不到前面2个块级做用域声明的let:node

1 {
2 let s =0;
3 }
4 {
5 let s =2;
6 }
7 console.log(s)   //Uncaught ReferenceError: s is not defined

 

在平常开发中多存在于使用if/for关键字结合let/const建立的块级做用域,值得注意的是使用let/const关键字声明变量的for循环和var声明的有些不一样ios

for(var i = 0; i<5; I++){
    // do something
}
console.log(i) // 5

 

for循环分为3部分,第一部分包含一个变量声明,第二部分包含一个循环的退出条件,第三部分包含每次循环最后要执行的表达式,也就是说第一部分在这个for循环中只会执行一次var i = 0,然后面的两个部分在每次循环的时候都会执行一遍es6

for(let i = 0; i<5; i++){
    // do something
}
console.log(i) //Uncaught ReferenceError: i is not defined

 

而使用使用let/const关键字声明变量的for循环,除了会建立块级做用域,let/const还会将它绑定到每一个循环中,确保对上个循环结束时候的值进行从新赋值web

什么意思呢?简而言之就是每次循环都会声明一次(对比var声明的for循环只会声明一次),能够这么理解let/const中的for循环ajax

给每次循环建立一个块级做用域:chrome

{
    let i = 0
}
{
    let i = 1 // for循环内部会记住上次循环结果并赋值给下个循环
}
{
    let i = 2
}

 

暂时性死区

使用let/const声明的变量,从一开始就造成了封闭做用域,在声明变量以前是没法使用这个变量的,这个特色也是为了弥补var的缺陷(var声明的变量有变量提高)npm

if(true){
    name= 'a';
    console.log(name) //Uncaught ReferenceError: name is not defined
    let name
}

 

在预编译的阶段,JS编译器会先解析一遍判断是否有let/const声明的变量,若是在一个花括号中存在使用let/const声明的变量,则ES6规定这些变量在没声明前是没法使用的,随后再是进入执行阶段执行代码编程

这里当知足if的条件时,进入true的逻辑,这里由于使用了let声明了变量name,在一开始就"劫持了这个做用域",使得任何在let声明以前使用name的操做都会报错axios

使用var声明的变量,由于会有变量提高,一样也是发生在预编译阶段,var会提高到当前函数做用域的顶部而且默认赋值为undefined,若是这几行代码是在全局做用域下,则name变量会直接提高到全局做用域,随后进入执行阶段执行代码,name被赋值为"a",而且能够成功打印出字符串abc

至关于这样

var name
if(true){
    name= 'a';
    console.log(name) //a
}

 

暂时性死区实际上是为了防止ES5之前在变量声明前就使用这个变量,这是由于var的变量提高的特性致使一些不熟悉var原理的开发者习觉得常的觉得变量能够先使用在声明,从而埋下一些隐患

关于JS预编译和JS的3种做用域(全局,函数,块级)这里也不赘述了,不然又能写出几千字的博客,有兴趣的朋友自行了解一下,一样也有助于了解JavaScript这门语言

const

使用const关键字声明一个常量,常量的意思是不会改变的变量,const和let的一些区别是

一、const声明变量的时候必须赋值,不然会报错,一样使用const声明的变量被修改了也会报错

二、const声明变量不能改变,若是声明的是一个引用类型,则不能改变它的内存地址(这里牵扯到JS引用类型的特色,有兴趣能够看我另外一篇博客对象深拷贝和浅拷贝)

有些人会有疑问,为何平常开发中没有显式的声明块级做用域,let/const声明的变量却没有变为全局变量

这个其实也是let/const的特色,ES6规定它们不属于顶层全局变量的属性,这里用chrome调试一下

能够看到使用let声明的变量x是在一个叫script做用域下的,而var声明的变量由于变量提高因此提高到了全局变量window对象中,这使咱们能放心的使用新语法,不用担忧污染全局的window对象

建议

在平常开发中,个人建议是全面拥抱let/const,通常的变量声明使用let关键字,而当声明一些配置项(相似接口地址,npm依赖包,分页器默认页数等一些一旦声明后就不会改变的变量)的时候可使用const,来显式的告诉项目其余开发者,这个变量是不能改变的(const声明的常量建议使用全大写字母标识,单词间用下划线),同时也建议了解var关键字的缺陷(变量提高,污染全局变量等),这样才能更好的使用新语法

箭头函数(经常使用)

ES6 容许使用箭头(=>)定义函数

箭头函数对于使用function关键字建立的函数有如下区别

一、箭头函数没有arguments(建议使用更好的语法,剩余运算符替代)

二、箭头函数没有prototype属性,没有constructor,即不能用做与构造函数(不能用new关键字调用)

三、箭头函数没有本身this,它的this是词法的,引用的是上下文的this,即在你写这行代码的时候就箭头函数的this就已经和外层执行上下文的this绑定了(这里我的认为并不表明彻底是静态的,由于外层的上下文还是动态的可使用call,apply,bind修改,这里只是说明了箭头函数的this始终等于它上层上下文中的this)

 

由于setTimeout会将一个匿名的回调函数推入异步队列,而回调函数是具备全局性的,即在非严格模式下this会指向window,就会存在丢失变量a的问题,而若是使用箭头函数,在书写的时候就已经肯定它的this等于它的上下文(这里是makeRequest的函数执行上下文,至关于讲箭头函数中的this绑定了makeRequest函数执行上下文中的this),因此this就指向了makeRequest中的a变量

箭头函数中的this即便使用call,apply,bind也没法改变指向(这里也验证了为何ECMAScript规定不能使用箭头函数做为构造函数,由于它的this已经肯定好了没法改变)

建议

箭头函数替代了之前须要显式的声明一个变量保存this的操做,使得代码更加的简洁

ES5写法不推荐:

ES6箭头函数:

值得注意的是makeRequest后面的function不能使用箭头函数,由于这样它就会再使用上层的this,而再上层是全局的执行上下文,它的this的值会指向window

setTimeout第一个参数使用了箭头函数,它会引用上下文的this,而它的外层也是一个箭头函数,又会引用再上层的this,最上层就是整个全局上下文,即this的值为window对象,因此没有变量a

在数组的迭代中使用箭头函数更加简洁,而且省略了return关键字

不要在可能改变this指向的函数中使用箭头函数,相似Vue中的methods,computed中的方法,生命周期函数,Vue将这些函数的this绑定了当前组件的vm实例,若是使用箭头函数会强行改变this,由于箭头函数优先级最高(没法再使用call,apply,bind改变指向)

在把箭头函数做为平常开发的语法以前,我的建议是去了解一下箭头函数的是如何绑定this的,而不仅是当作省略function这几个单词拼写,毕竟那才是ECMAScript真正但愿解决的问题

iterator迭代器

iterator迭代器是ES6很是重要的概念,可是不少人对它了解的很少,可是它倒是另外4个ES6经常使用特性的实现基础(解构赋值,剩余/扩展运算符,生成器,for of循环),了解迭代器的概念有助于了解另外4个核心语法的原理,另外ES6新增的Map,Set数据结构也有使用到它,因此我放到前面来说

对于可迭代的数据解构,ES6在内部部署了一个[Symbol.iterator]属性,它是一个函数,执行后会返回iterator对象(也叫迭代器对象,也叫iterator接口),拥有[Symbol.iterator]属性的对象即被视为可迭代的

数组中的Symbol.iterator方法默认部署在数组原型上:

默认具备iterator接口的数据结构有如下几个,注意普通对象默认是没有iterator接口的(能够本身建立iterator接口让普通对象也能够迭代)

  • Array

  • Map

  • Set

  • String

  • TypedArray(类数组)

  • 函数的 arguments 对象

  • NodeList 对象

 

iterator迭代器是一个对象,它具备一个next方法因此能够这么调用

next方法返回又会返回一个对象,有value和done两个属性,value即每次迭代以后返回的值,而done表示是否还须要再次循环,能够看到当value为undefined时,done为true表示循环终止

梳理一下

  • 可迭代的数据结构会有一个[Symbol.iterator]方法

  • [Symbol.iterator]执行后返回一个iterator对象

  • iterator对象有一个next方法

  • next方法执行后返回一个有value,done属性的对象

 

这里简要概述了如下iterator的概念,有兴趣能够去看阮一峰老师的《ECMAScript 6 入门》

解构赋值(经常使用)

解构赋值能够直接使用对象的某个属性,而不须要经过属性访问的形式使用,对象解构原理我的认为是经过寻找相同的属性名,而后原对象的这个属性名的值赋值给新对象对应的属性

这里左边真正声明的实际上是titleOne,titleTwo这两个变量,而后会根据左边这2个变量的位置寻找右边对象中title和test[0]中的title对应的值,找到字符串abc和test赋值给titleOne,titleTwo(若是没有找到会返回undefined)

数组解构的原理实际上是消耗数组的迭代器,把生成对象的value属性的值赋值给对应的变量

数组解构的一个用途是交换变量,避免之前要声明一个临时变量值存储值

ES6交换变量:

建议

一样建议使用,由于解构赋值语意化更强,对于做为对象的函数参数来讲,能够减小形参的声明,直接使用对象的属性(若是嵌套层数过多我我的认为不适合用对象解构,不太优雅)

一个经常使用的例子是Vuex中actions中的方法会传入2个参数,第一个参数是个对象,你能够随意命名,而后使用<名字>.commit的方法调用commit函数,或者使用对象解构直接使用commit

不使用对象解构:

使用对象解构:

另外能够给使用axios的响应结果进行解构(axios默认会把真正的响应结果放在data属性中)

剩余/扩展运算符(经常使用)

剩余/扩展运算符一样也是ES6一个很是重要的语法,使用3个点(...),后面跟着一个数组,它使得能够"展开"这个数组,能够这么理解,数组是存放元素集合的一个容器,而使用剩余/扩展运算符能够将这个容器拆开,这样就只剩下元素集合,你能够把这些元素集合放到另一个数组里面

扩展运算符

只要含有iterator接口的数据结构均可以使用扩展运算符

扩展运算符能够和数组的解构赋值一块儿使用,可是必须放在最后一个,由于剩余/扩展运算符的原理实际上是利用了数组的迭代器,它会消耗3个点后面的数组的全部迭代器,读取全部迭代器的value属性,剩余/扩展运算符后不能在有解构赋值,由于剩余/扩展运算符已经消耗了全部迭代器,而数组的解构赋值也是消耗迭代器,可是这个时候已经没有迭代器了,因此会报错

这里first会消耗右边数组的一个迭代器,...arr会消耗剩余全部的迭代器,而第二个例子...arr直接消耗了全部迭代器,致使last没有迭代器可供消耗了,因此会报错,由于这是毫无心义的操做

剩余运算符

剩余运算符最重要的一个特色就是替代了之前的arguments

访问函数的arguments对象是一个很昂贵的操做,之前的arguments.callee也被废止了,建议在支持ES6语法的环境下不要在使用arguments,使用剩余运算符替代(箭头函数没有arguments,必须使用剩余运算符才能访问参数集合)

剩余运算符和扩展运算符的区别就是,剩余运算符会收集这些集合,放到右边的数组中,扩展运算符是将右边的数组拆分红元素的集合,它们是相反的

在对象中使用扩展运算符

这个是ES9的语法,ES9中支持在对象中使用扩展运算符,以前说过数组的扩展运算符原理是消耗全部迭代器,但对象中并无迭代器,我我的认为多是实现原理不一样,可是仍能够理解为将键值对从对象中拆开,它能够放到另一个普通对象中

其实它和另一个ES6新增的API类似,即Object.assign,它们均可以合并对象,可是仍是有一些不一样Object.assign会触发目标对象的setter函数,而对象扩展运算符不会,这个咱们放到后面讨论

建议

使用扩展运算符能够快速的将类数组转为一个真正的数组

合并多个数组

函数柯里化

对象属性/方法简写(经常使用)

对象属性简写

es6容许当对象的属性和值相同时,省略属性名

须要注意的是

对象属性简写常常与解构赋值一块儿使用

结合上文的解构赋值,这里的代码会实际上是声明了x,y,z变量,由于bar函数会返回一个对象,这个对象有x,y,z这3个属性,解构赋值会寻找等号右边表达式的x,y,z属性,找到后赋值给声明的x,y,z变量

方法简写

es6容许当一个对象的属性的值是一个函数(便是一个方法),可使用简写的形式

在Vue中由于都是在vm对象中书写方法,彻底可使用方法简写的方式书写函数

for … of循环

for ... of是做为ES6新增的遍历方式,容许遍历一个含有iterator接口的数据结构而且返回各项的值,和ES3中的for ... in的区别以下

一、for ... of遍历获取的是对象的键值,for ... in 获取的是对象的键名

二、for ... in会遍历对象的整个原型链,性能很是差不推荐使用,而for ... of只遍历当前对象不会遍历原型链

三、对于数组的遍历,for ... in会返回数组中全部可枚举的属性(包括原型链),for ... of只返回数组的下标对于的属性值

 

for ... of循环的原理其实也是利用了遍历对象内部的iterator接口,将for ... of循环分解成最原始的for循环,内部实现的机制能够这么理解

能够看到只要知足第二个条件(iterator.next()存在且res.done为true)就能够一直循环下去,而且每次把迭代器的next方法生成的对象赋值给res,而后将res的value属性赋值给for ... of第一个条件中声明的变量便可,res的done属性控制是否继续遍历下去

for... of循环同时支持break,continue,return(在函数中调用的话)而且能够和对象解构赋值一块儿使用

arr数组每次使用for ... of循环都返回一对象({a:1},{a:2},{a:3}),而后会通过对象解构,寻找属性为a的值,赋值给obj.a,因此在每轮循环的时候obj.a会分别赋值为1,2,3

Promise(经常使用)

Promise做为ES6中推出的新的概念,改变了JS的异步编程,现代前端大部分的异步请求都是使用Promise实现,fetch这个web api也是基于Promise的,这里不得简述一下以前统治JS异步编程的回调函数,回调函数有什么缺点,Promise又是怎么改善这些缺点

回调函数

众所周知,JS是单线程的,由于多个线程改变DOM的话会致使页面紊乱,因此设计为一个单线程的语言,可是浏览器是多线程的,这使得JS同时具备异步的操做,即定时器,请求,事件监听等,而这个时候就须要一套事件的处理机制去决定这些事件的顺序,即Event Loop(事件循环),这里不会详细讲解事件循环,只须要知道,前端发出的请求,通常都是会进入浏览器的http请求线程,等到收到响应的时候会经过回调函数推入异步队列,等处理完主线程的任务会读取异步队列中任务,执行回调

在《你不知道的JavaScript》下卷中,这么介绍

使用回调函数处理异步请求至关于把你的回调函数置于了一个黑盒,使用第三方的请求库你可能会这么写

收到响应后,执行后面的回调打印字符串,可是若是这个第三方库有相似超时重试的功能,可能会执行屡次你的回调函数,若是是一个支付功能,你就会发现你扣的钱可能就不止1000元了-.-

另一个众所周知的问题就是,在回调函数中再嵌套回调函数会致使代码很是难以维护,这是人们常说的“回调地狱”

 

你使用的第三方ajax库还有可能并无提供一些错误的回调,请求失败的一些错误信息可能会被吞掉,而你确彻底不知情

总结一下回调函数的一些缺点

一、多重嵌套,致使回调地狱

二、代码跳跃,并不是人类习惯的思惟模式

三、信任问题,你不能把你的回调彻底寄托与第三方库,由于你不知道第三方库到底会怎么执行回调(屡次执行)

四、第三方库可能没有提供错误处理

五、不清楚回调是否都是异步调用的(能够同步调用ajax,在收到响应前会阻塞整个线程,会陷入假死状态,很是不推荐)

 

xhr.open("GET","/try/ajax/ajax_info.txt",false); //经过设置第三个async为false能够同步调用ajax

 

Promise

针对回调函数这么多缺点,ES6中引入了一个新的概念,Promise,Promise是一个构造函数,经过new关键字建立一个Promise的实例,来看看Promise是怎么解决回调函数的这些问题

Promise并非回调函数的衍生版本,而是2个概念,因此须要将以前的回调函数改成支持Promise的版本,这个过程成为"提高",或者"promisory",现代MVVM框架经常使用的第三方请求库axios就是一个典型的例子,另外nodejs中也有bluebird,Q等

一、多重嵌套,致使回调地狱

Promise在设计的时候引入了链式调用的概念,每一个then方法一样也是一个Promise,所以能够无限链式调用下去

配合箭头函数,明显的比以前回调函数的多层嵌套优雅不少

二、代码跳跃,并不是人类习惯的思惟模式

Promise使得可以同步思惟书写代码,上述的代码就是先请求3000端口,获得响应后再请求3001,再请求3002,再请求3003,而书写的格式也是符合人类的思惟,从先到后

三、信任问题,你不能把你的回调彻底寄托与第三方库,由于你不知道第三方库到底会怎么执行回调(屡次执行)

Promise自己是一个状态机,具备pending(等待),resolve(决议),reject(拒绝)这3个状态,当请求发送没有获得响应的时候会pending状态,而且一个Promise实例的状态只能从pending => resolve 或者从 pending => reject,即当一个Promise实例从pending状态改变后,就不会再改变了(不存在resolve => reject 或 reject => resolve)

而Promise实例必须主动调用then方法,才能将值从Promise实例中取出来(前提是Promise不是pending状态),这一个“主动”的操做就是解决这个问题的关键,即第三方库作的只是把改变Promise的状态,而响应的值怎么处理,这是开发者主动控制的,这里就实现了控制反转,将原来第三方库的控制权转移到了开发者上

四、第三方库可能没有提供错误处理

Promise的then方法会接受2个函数,第一个函数是这个Promise实例被resolve时执行的回调,第二个函数是这个Promise实例被reject时执行的回调,而这个也是开发者主动调用的

使用Promise在异步请求发送错误的时候,即便没有捕获错误,也不会阻塞主线程的代码

五、不清楚回调是否都是异步调用的

Promise在设计的时候保证全部响应的处理回调都是异步调用的,不会阻塞代码的执行,Promise将then方法的回调放入一个叫微任务的队列中(MicroTask),保证这些回调任务都在同步任务执行完再执行,这部分一样也是事件循环的知识点,有兴趣的朋友能够深刻研究一下

建议

在平常开发中,建议全面拥抱新的Promise语法,其实如今的异步编程基本也都使用的是Promise

建议使用ES7的async/await进一步的优化Promise的写法,async函数始终返回一个Promise,await能够实现一个"等待"的功能,async/await被成为异步编程的终极解决方案,即用同步的形式书写异步代码,而且可以更优雅的实现异步代码顺序执行,详情能够看阮老师的ES6标准入门

关于Promise还有不少不少须要讲的,包括它的静态方法all,race,resolve,reject,Promise的执行顺序,Promise嵌套Promise,thenable对象的处理等,碍于篇幅这里只介绍了一下为何须要使用Promise。但不少开发者在平常使用中只是了解这些API,殊不知道Promise内部具体是怎么实现的,遇到复杂的异步代码就无从下手,很是建议去了解一下Promise A+的规范,本身实现一个Promise

ES6 Module(经常使用)

在ES6 Module出现以前,模块化一直是前端开发者讨论的重点,面对日益增加的需求和代码,须要一种方案来将臃肿的代码拆分红一个个小模块,从而推出了AMD,CMD和CommonJs这3种模块化方案,前者用在浏览器端,后面2种用在服务端,直到ES6 Module出现

ES6 Module默认目前尚未被浏览器支持,须要使用babel,在平常写demo的时候常常会显示这个错误

能够在script标签中使用tpye="module"在同域的状况下能够解决(非同域状况会被同源策略拦截,webstorm会开启一个同域的服务器没有这个问题,vscode貌似不行)

ES6 Module使用import关键字导入模块,export关键字导出模块,它还有如下特色

一、ES6 Module是静态的,也就是说它是在编译阶段运行,和var以及function同样具备提高效果(这个特色使得它支持tree shaking)

二、自动采用严格模式(顶层的this返回undefined)

三、ES6 Module支持使用export {<变量>}导出具名的接口,或者export default导出匿名的接口

module.js导出:

a.js导入:

这二者的区别是,export {<变量>}导出的是一个变量的引用,export default导出的是一个值

什么意思呢,就是说在a.js中使用import导入这2个变量的后,在module.js中由于某些缘由x变量被改变了,那么会马上反映到a.js,而module.js中的y变量改变后,a.js中的y仍是原来的值

module.js:

a.js:

能够看到给module.js设置了一个一秒后改变x,y变量的定时器,在一秒后同时观察导入时候变量的值,能够发现x被改变了,但y的值还是20,由于y是经过export default导出的,在导入的时候的值至关于只是导入数字20,而x是经过export {<变量>}导出的,它导出的是一个变量的引用,即a.js导入的是当前x的值,只关心当前x变量的值是什么,能够理解为一个"活连接"

export default这种导出的语法其实只是指定了一个命名导出,而它的名字叫default,换句话说,将模块的导出的名字重命名为default,也可使用import <变量> from <路径> 这种语法导入

module.js导出:

a.js导入:

可是因为是使用export {<变量>}这种形式导出的模块,即便被重命名为default,仍然导出的是一个变量的引用

这里再来讲一下目前为止主流的模块化方案ES6 Module和CommonJs的一些区别

一、CommonJs输出的是一个值的拷贝,ES6 Module经过export {<变量>}输出的是一个变量的引用,export default输出的是一个值的拷贝

二、CommonJs运行在服务器上,被设计为运行时加载,即代码执行到那一行才回去加载模块,而ES6 Module是静态的输出一个接口,发生在编译的阶段

三、CommonJs在第一次加载的时候运行一次,以后加载返回的都是第一次的结果,具备缓存的效果,ES6 Module则没有

import( )

关于ES6 Module静态编译的特色,致使了没法动态加载,可是老是会有一些须要动态加载模块的需求,因此如今有一个提案,使用把import做为一个函数能够实现动态加载模块,它返回一个Promise,Promise被resolve时的值为输出的模块

 

使用import方法改写上面的a.js使得它能够动态加载(使用静态编译的ES6 Module放在条件语句会报错,由于会有提高的效果,而且也是不容许的),能够看到输出了module.js的一个变量x和一个默认输出

Vue中路由的懒加载的ES6写法就是使用了这个技术,使得在路由切换的时候可以动态的加载组件渲染视图

函数默认值

ES6容许在函数的参数中设置默认值

ES5写法:

ES6写法:

相比ES5,ES6函数默认值直接写在参数上,更加的直观

若是使用了函数默认参数,在函数的参数的区域(括号里面),它会做为一个单独的做用域,而且拥有let/const方法的一些特性,好比暂时性死区,块级做用域,没有变量提高等,而这个做用域在函数内部代码执行前

这里当运行func的时候,由于没有传参数,使用函数默认参数,y就会去寻找x的值,在沿着词法做用域在外层找到了值为1的变量x

再来看一个例子

这里一样没有传参数,使用函数的默认赋值,x经过词法做用域找到了变量w,因此x默认值为2,y一样经过词法做用域找到了刚刚定义的x变量,y的默认值为3,可是在解析到z = z + 1这一行的时候,JS解释器先会去解析z+1找到相应的值后再赋给变量z,可是由于暂时性死区的缘由(let/const"劫持"了这个块级做用域,没法在声明以前使用这个变量,上文有解释),致使在let声明以前就使用了变量z,因此会报错

这样理解函数的默认值会相对容易一些

当传入的参数为undefined时才使用函数的默认值(显式传入undefined也会触发使用函数默认值,传入null则不会触发)

在举个例子:

这里借用阮一峰老师书中的一个例子,func的默认值为一个函数,执行后返回foo变量,而在函数内部执行的时候,至关于对foo变量的一次变量查询(LHS查询),而查询的起点是在这个单独的做用域中,即JS解释器不会去查询去函数内部查询变量foo,而是沿着词法做用域先查看同一做用域(前面的函数参数)中有没有foo变量,再往函数的外部寻找foo变量,最终找不到因此报错了,这个也是函数默认值的一个特色

函数默认值配合解构赋值

第一行给func函数传入了2个空对象,因此函数的第一第二个参数都不会使用函数默认值,而后函数的第一个参数会尝试解构对象,提取变量x,由于第一个参数传入了一个空对象,因此解构不出变量x,可是这里又在内层设置了一个默认值,因此x的值为10,而第二个参数一样传了一个空对象,不会使用函数默认值,而后会尝试解构出变量y,发现空对象中也没有变量y,可是y没有设置默认值因此解构后y的值为undefined

第二行第一个参数显式的传入了一个undefined,因此会使用函数默认值为一个空对象,随后和第一行同样尝试解构x发现x为undefined,可是设置了默认值因此x的值为10,而y和上文同样为undefined

第三行2个参数都会undefined,第一个参数和上文同样,第二个参数会调用函数默认值,赋值为{y:10},而后尝试解构出变量y,即y为10

第四行和第三行相同,一个是显式传入undefined,一个是隐式不传参数

第五行直接使用传入的参数,不会使用函数默认值,而且可以顺利的解构出变量x,y

Proxy

Proxy做为一个"拦截器",能够在目标对象前架设一个拦截器,他人访问对象,必须先通过这层拦截器,Proxy一样是一个构造函数,使用new关键字生成一个拦截对象的实例,ES6提供了很是多对象拦截的操做,几乎覆盖了全部可能修改目标对象的状况(Proxy通常和Reflect配套使用,前者拦截对象,后者返回拦截的结果,Proxy上有的的拦截方法Reflect都有)

Object.definePropery

提到Proxy就不得不提一下ES5中的Object.defineProperty,这个api能够给一个对象添加属性以及这个属性的属性描述符/访问器(这2个不能共存,同一属性只能有其中一个),属性描述符有configurable,writable,enumerable,value这4个属性,分别表明是否可配置,是否只读,是否可枚举和属性的值,访问器有configurable,enumerable,get,set,前2个和属性描述符功能相同,后2个都是函数,定义了get,set后对元素的读写操做都会执行这个函数,而且覆盖默认的读写行为

 

定义了obj中a属性的表示为只读,且不可枚举,obj2定义了get,但没有定义set表示只读,而且读取obj2的b属性返回的值是get函数的返回值

ES5中的Object.defineProperty这和Proxy有什么关系呢?我的理解Proxy是Object.defineProperty的加强版,ES5只规定可以定义属性的属性描述符或访问器.而Proxy加强到了13种,具体太多了我就不一一放出来了,这里我举几个比较有意思的例子

handler.apply

apply可让咱们拦截一个函数(JS中函数也是对象,Proxy也能够拦截函数)的执行,咱们能够把它用在函数节流中

调用拦截后的函数:

handler.contruct

contruct能够拦截经过new关键字调用这个函数的操做,咱们能够把它用在单例模式中

这里经过一个闭包保存了instance变量,每次使用new关键字调用被拦截的函数后都会查看这个instance变量,若是存在就返回闭包中保存的instance变量,不然就新建一个实例,这样能够实现全局只有一个实例

handler.defineProperty

defineProperty能够拦截对这个对象的Object.defineProerty操做

注意对象内部的默认的[[SET]]函数(即对这个对象的属性赋值)会间接触发defineProperty和getOwnPropertyDescriptor这2个拦截方法

这里有几个知识点

一、这里使用了递归的操做,当须要访问对象的属性时候,会判断代理的对象属性的值还是一个能够代理的对象就递归的进行代理,不然经过错误捕获执行默认的get函数

二、定义了defineProperty的拦截方法,当对这个代理对象的某个属性进行赋值的时候会执行对象内部的[[SET]]函数进行赋值,这个操做会间接触发defineProperty这个方法,随后会执行定义的callback函数

 

这样就实现了不管对象嵌套多少层,只要有属性进行赋值就会触发get方法,对这层对象进行代理,随后触发defineProperty执行callback回调函数

其余的使用场景

Proxy另外还有不少功能,好比在实现验证器的时候,能够将业务逻辑和验证器分离达到解耦,经过defineProperty设置一些私有变量,拦截对象作日志记录等

 

Proxy就没有这个问题,而且还提供了更多的拦截方法,彻底能够替代Object.defineProperty,惟一不足的也就是浏览器的支持程度了(IE:谁在说我?)

因此要想深刻了解Vue3.0实现机制,学会Proxy是必不可少的

Object.assign

这个ES6新增的Object静态方法容许咱们进行多个对象的合并

 

能够这么理解,Object.assign遍历须要合并给target的对象(即sourece对象的集合)的属性,用等号进行赋值,这里遍历{a:1}将属性a和值数字1赋值给target对象,而后再遍历{b:2}将属性b和值数字2赋值给target对象

这里罗列了一些这个API的须要注意的知识点

一、Object.assign是浅拷贝,对于值是引用类型的属性拷贝扔的是它的引用

二、对于Symbol属性一样能够拷贝

三、不可枚举的属性没法拷贝

四、target必须是一个对象,若是传入一个基本类型,会变成基本包装类型,null/undefined没有基本包装类型,因此传入会报错

五、source参数若是是不可枚举的会忽略合并(字符串类型被认为是可枚举的,由于内部有iterator接口)

六、由于是用等号进行赋值,若是被赋值的对象的属性有setter函数会触发setter函数,同理若是有getter函数,也会调用赋值对象的属性的getter(这就是为何Object.assign没法合并对象属性的访问器,由于它会直接执行对应的getter/setter函数而不是合并它们,在ES7中可使用Object.defineOwnPropertyDescriptors实现复制属性访问器的操做)

 

这里为了加深了解我本身模拟了Object.assign的实现,可供参考

和ES9的对象扩展运算符对比

ES9支持在对象上使用扩展运算符,实现的功能和Object.assign类似,惟一的区别就是在含有getter/setter函数的对象有所区别

能够看到,ES9在合并2个对象的时候触发了合并对象的getter,而ES6中触发了target对象的setter而不会触发getter,除此以外,Object.assgin和对象扩展运算符功能是相同的,二者均可以使用,二者都是浅拷贝,使用ES9的方法相对简洁一点

建议

一、Vue中重置data中的数据

这个是我最经常使用的小技巧,使用Object.assign能够将你目前组件中的data对象和组件默认初始化状态的data对象中的数据合并,这样能够达到初始化data对象的效果

在当前组件的实例中$data属性保存了当前组件的data对象,而$options是当前组件实例初始化时候的对象,其中有个data方法,即在组件中写的data函数,执行后会返回一个初始化的data对象,而后将这个初始化的data对象合并到当前的data来初始化全部数据

二、给对象合并须要的默认属性

能够封装一个函数,外层声明一个DEFAULTS常量,options为每次传入的动态配置,这样每次执行后会合并一些默认的配置项

三、在传参的时候能够多个数据合并成一个对象传给后端

 

原文连接:https://mp.weixin.qq.com/s?__biz=MzAwNDcyNjI3OA==&mid=2650842343&idx=1&sn=fbfec54835d25b29a572046d04146e28&chksm=80d38f8eb7a40698a2bee5ab30fd2eb6edaefbd6cac1a80dd1474159611e4c03a6a4e0998d90&scene=0#rd

相关文章
相关标签/搜索