摘要:那之前的例子来使用一下的话,你会发现浏览器报错了,如图定义的变量不允许二次修改。如图箭头函数没有它自己的值,箭头函数内的值继承自外围作用域。如图这里两边的结构没有一致,如果是的话,是可以正常解构的。
前言
国庆假期已过一半,来篇干货压压惊。
ES6,并不是一个新鲜的东西,ES7、ES8已经赶脚了。但是,东西不在于新,而在于总结。每个学前端的人,身边也必定有本阮老师的《ES6标准入门》或者翻译的《深入理解ECMAScript6》。本篇主要是对ES6的一些常用知识点进行一个总结。如果你喜欢我的文章,欢迎评论,欢迎Star~。欢迎关注我的github博客
正文我们会更具之前的罗列的内容进行一个深入的分析。
变量的新定义——let和const在ES6没有被普及时,我们会用的变量定义的方法是var。其实,var对于一个刚刚接触js的人说,或许并不觉得怪异。但是,对于一个开发者而言,或许会在内心抨击它。因为它就是javascript的败笔之一,在其他语言看来的一个怪胎。那我们就来看看怪在何处呢?
可以重复定义。不知道你的代码里面会不会出现这样子的代码,举例:
var a = 10; var a = 11;
或许,你会看到这样子的写法觉得没啥,那么你很厉(wei)害(xian)。其实,这样子的坏处不言而喻。在大型的工程化开发中,你定义一个a,我定义一个a,还有千千万万个你和我,这时,技术总监就该着急了。所以,这是var的第一点令人讨厌的地方,但是如果你会说不是有严格模式嘛。的确,严格模式做了一定的规范,但是我们不加以讨论。毕竟,这时ES6的地盘(^-^)。
可随意修改。何为可随意修改?并不是指变量,而是指常量。举例:
var PI = 3.1415926 PI = 4.1415926
从例子中,我们可以看到,PI是一个我们经常会使用的常量,是公认的不可变动的。但在javascript中并不是如此。那天,如果你的PI被你们公司新晋的实习生改了,可能你找错误都得找半天,但这可不是实习生的锅,也许,他并不知道这里是个常量。不过,这种情况也是玩笑话(^_^)。
没有块级作用域。如果你连块级作用域都不知道的话,赶紧收藏一下^_^,回头再来看哈~,举例:
if(true){ var i = 10; } console.log(i); //10
相信,这变量不存在块级作用域给我们带来过不少麻烦吧。不知道啥时候,又得在循环中套一层闭包呢。而且,在非js开发者来说,可能觉得是个特(xiao)点(hua)。
所以,let和const就来拯救var了,如何一个拯救法呢?
在同一个块级作用域中,不允许重复定义。那之前的例子来使用一下的话,你会发现浏览器报错了,如图:
![let](https://user-gold-cdn.xitu.io/2017/10/6/2f0fb2e7243d5f6f19c2eb8ead022655)
const定义的变量不允许二次修改。还原一下之前的例子,如图:
![const](https://user-gold-cdn.xitu.io/2017/10/6/4665266d3b082f0275c989d96bf8af51) 是不是再也不用担心之前的实习生啦,呦!!!
let和const定义的变量会形成块级作用域。直接上图,看看:
![块级作用域](https://user-gold-cdn.xitu.io/2017/10/6/a670be66e059e8656b71237da148894c)
它们定义的变量不存在变量提升,以及存在暂时性死区
这个问题,我想举个例子可以更加方便的说明。首先,我们来看一题简单的笔试题:
var a = 10; function hello(){ console.log(a); var a = 11; console.log(a); } hello();
我想这个答案不言而喻,是undefined和11。原因:就是第一个console时,下面定义的变量a被提升了,所以a变成了undefined,第二个的话,就比较好理解。这个例子,我想会给初学者带来不小的麻烦,和当初的我一样哈。
使用let和const就会不一样,它们并不存在变量提升,如图:
函数的变化——箭头函数,剩余参数,参数默认值何为箭头函数,我们先上例子:
export const addToCart = productId => (dispatch, getState) => { if (getState().products.byId[productId].inventory > 0) { dispatch(addToCartUnsafe(productId)) } }
这是,我从redux例子中摘取的一个片段,第一感觉就是『代码风格简洁』,整体代码规范很好,毕竟是示例代码么。但是会让人难以理解。因此,为了避免以后看不懂的尴尬,还是来好好聊聊这个神奇的东西吧。
其实,这个东西类似于python的lambda。但是,它的确特别适合js这门语言,就一个字「酷」。它的几个规则:
变量如果只有一个的时候,可以省略()
如果是只有一句返回语句时,可以直接省略{return }这一部分
因为它本身叫做arrow,所以每次都必须带上=>符号
如果你一开始不会写,那就必须得多练习,这样才能在以后的工作中真正谋求便利。
当然咯,它有好处,但是在使用的时候,也得注意它的禁区。注意事项:
箭头函数不能作为构造函数。如图:
![arrow](https://user-gold-cdn.xitu.io/2017/10/6/80cab1e89e69343aaa393b871f947d23)
箭头函数没有它自己的this值,箭头函数内的this值继承自外围作用域。如图:
![arrow](https://user-gold-cdn.xitu.io/2017/10/6/2f4ee91258a2802f53abb504c75a4faf)
箭头函数没有arguments。这个我们直接测试一下就可以了,如图:
![arrow](https://user-gold-cdn.xitu.io/2017/10/6/95b20121716122fc48bdbec90ac9f6c4)
啥?没有arguments,那我如果正好要用到呢?这可咋办呢?下面再来说个有意思的改动——剩余参数。
什么叫剩余参数?别着急,看个例子就懂了。
const restParam = function(a, ...args){ console.log(args); }; restParam(1, 2, 3, 4); //[2, 3, 4]
这里你会发现这个args变量似乎包含了之后输入的所有参数,除了a以外。所以,这就是所谓的剩余参数。其中,运用到了一个…这个符号。其实这个符号的用处非常的多,ES6中可以将它称为扩展符。那么,我们来看看在箭头函数中的运用。
当然,在使用剩余参数时,需要注意一个问题,就是剩余参数设置的位置。我们先来看张图:
所以,在使用剩余参数时,需要注意的是,将这部分放在所有参数的最后位置。其实,ES6还带来了另一个参数的变动——默认参数。或许,我们可以先看一下默认参数这个问题,我们之前是怎么处理的。场景:一般在设置延迟的时候,都会有一个时间的默认值,防止用户在没有设置的情况下使用,看看下面的例子:
function defaultParam(time){ let wait = time || 1000; setTimeout(() => { //... }, wait); }
这种写法应该非常的常见,使用的也比较广泛。但是,使用ES6的语法的话,就会变成这样子,例子:
function defaultParam(time = 1000){ setTimeout(() => { //... }, time); }
看上去这样子的写法,会使得函数更加的简洁明了。
数组——解构赋值、二进制数组说到解构赋值呢?大家千万别误解为这是数组的特性。不是的,对象也能够满足。只是觉得放在这边来写会比较好而已
解构赋值这个新特性,说实话是真的好用。我们可以先来看一个复杂一点的例子:
let [a, b , {name, age}, ...args ] = [1, 2, {name: "zimo", age: 24}, 3, 4]; console.log(a, b, name, age, args); //1, 2, "zimo", 24, [3, 4]
你会发现例子中,有一个特点——对仗工整。
这是解构赋值时,必须要去满足的条件——想要解构的部分,内容保持一致。这样才能保证完美解构。对于解构而言,左右两边的内容长度不一致,不会出问题。比如,当你右边内容多一点的时候,其实没啥事,你只需要保证你左边的结构有一部分是你想要的,举例:
let [a, b, c] = [1, 2, 3, 4, 5]; console.log(a, b, c); //1, 2, 3
这种叫做部分解构,左边也是一样的,对于多处来的部分,会变成undefined。举例:
let [a,b,c] = [1, 2]; console.log(a, b, c); //1 2 undefined
解构赋值在使用过程中,也是有需要注意的部分:
必须保证有赋值的过程。看个例子:
![解构赋值](https://user-gold-cdn.xitu.io/2017/10/6/dd437a39aa760a86119524bfa7de36a2) 你可以看到图中的例子,多带带先声明了a和b,但是没有赋值的过程,会报错。
左边内容部分的结构必须与右边保持一致。如图:
这里两边的结构没有一致,如果是foo,bar的话,是可以正常解构的。但是这个例子的意图可能是想去解构foo中的值,但是写法上有一定的问题。
其实,解构也有没多种玩法:
默认值的使用。由于之前说过的部分解构的情况出现,所以我们在解构时,可以使用默认值的形式。
let [a, b = 10] = [1]; console.log(a, b); //1, 10
在这个例子中b原先是undefined,但是设置了默认值的情况下,undefined的变量会被赋上默认值
函数变量中使用解构。对于一个函数而言,它的参数也可能会是数组或对象,这是我们就可以使用解构赋值的方式
function destructuring({name, age}){ console.log(name, age); } destructuring({name: "zimo", age: 21}); // zimo 21
解构赋值现在被使用的频率也是非常之大,好好掌握一下也是有必要的。
之后的话,我们可以聊一下二进制数组的概念。
何为二进制数组?其实,我们可以先来了解一下javascript的数组。熟悉js的人都知道,其实js的数组的性能并不高,它的本质是一个对象。之所以现在你看到数组在使用时速度还可以,是因为js的引擎在处理时,做了不同的优化。拿v8引擎举例的话,对于内部元素类型相同的数组在编译运行的时候,会使用c编译器。如果对于内部元素类型不同的时候,它会先将数组分离开来,然后再进行编译。具体可以查看深入 JavaScript 数组:进化与性能
所以,我们可以直接了解一下二进制数组的使用。二进制数组可以由Int8Array、Int16Array、Int32Array等形式组成,在整数方面,可用性较强。
const buffer = new Buffer(100000000); const arr = new Int8Array(buffer); console.time("test time"); for(let i = 0; i < arr.length; i++){ arr[i] = i; } console.timeEnd("test time");
其实,现在二进制数组使用的频率并不多,ES6也仅仅是提出,后续还会对数组这一块进行一个更加详细的完善。
字符串——模版字符串、startsWith、endsWidth在ES6中,对字符串也做了一定的改进。先来聊聊我们的新朋友——模版字符串。其实,在语言中,字符串有多种表示方式:单引号、双引号和倒引号。在javascript中,双引号和单引号都是一样的,这点与一些静态语言不一样。但是,往往有时候,对于字符串的拼接会使得开发者厌烦。如何解决呢?
ES6带来了解决方案——模版字符串。何为模版字符串呢?由倒引号包裹``,然后使用${}来包裹变量。我们可以来实践一下
const name="zimo"; const str = `My name is ${name}`; console.log(str); //My name is zimo
这样,我们就可以非常方便的在其中添加变量了。或许,你会觉得这样的拼接,使用普通的方式也可以非常好的完成。但是,在开发过程中,我们或许会碰到更佳复杂的情况,比如说,我们现在要去创建一个DOM元素,以及它的内部元素。这种情况,通常还会带有表达式。
const width = 100; const height = 200; const src = "http://www.example.com/example.png"; const html = ``;
往往这样子的元素在手动拼接的过程中,总是会出错,因此,使用模版字符串是一种既「高效」又「简洁」的方式。
有了模版字符串,你可以解决非常棘手的问题。那么,标题中带有的startsWith和endsWith又是起到什么作用呢?可能你会使用正则表达式,那么你就有可能不会使用到这两个API。
按照惯例,还是需要来介绍一下这两个API的。
startsWith:返回值为boolean型,然后去匹配字符串开头的部分,举个例子:
const str = "start in the head"; console.log(str.startsWith("start")); //true console.log(str.startsWith("head")); //false
其实,这也是可以使用正则表达式来达到这一目的。还原上例:
const str = "start in the head"; console.log(/^start/.test(str)); //true console.log(/^head/.test(str)); //false
其实,两者方式的区别基本上没有,但是正则表达式的功能更佳的完善。这个API仅仅在一些场景下起到一定的便捷。比方说,我们需要去匹配一个URL的协议头是什么时,我们往往需要用到这种方式。例子:
const url = "http://www.example.com"; if(url.startsWith("http")){ console.log("this is http"); }else if(url.startsWith("https")){ console.log("this is https"); }else if(url.startsWith("ws")){ console.log("this is websocket"); } //this is http
同理,endWith也是一样的效果。
endsWith:返回值是boolean类型,然后去匹配字符串的结尾。举个例子:
const str = "something in the end"; console.log(str.endsWith("end")); //true console.log(str.endsWith("something")); //false
同样的,它也可以使用正则表达式来实现:
const str = "something in the end"; console.log(/end$/.test(str)); //true console.log(/something$/.test(str)); //false
这种情况的使用场景是,往往我们需要为上传的文件准备图标,那么我们就可以根据后缀来确定图标。
const filename = "upload.jpg"; if(filename.endsWith(".jpg")){ console.log("this is jpg file"); }else if(filename.endsWith(".png")){ console.log("this is png file"); }else if(filename.endsWith(".webp")){ console.log("this is webp file"); } //this is jpg file
同时,字符串还增加了许许多多的东西,有兴趣的,可以自己去翻书本详细的了解
Iterator和for...ofIterator的概念是迭代器。在ES6中,终于正式的添加了这个属性。迭代器,主要是一个集合类元素的遍历机制。何为集合类元素?最常见的就是数组,还有对象。迭代器可以帮助开发者完成遍历集合的过程。最开始javascript并没有设置接口,来自定义迭代器,但是从ES6开始,我们可以自定义迭代器了。在自定义迭代器之前,我们要清楚迭代器的作用有哪些:
为各种数据结构提供一个统一的、简便的访问接口
使得数据结构的成员能够按某种次序排列
在ES6中,迭代器主要供我们之后要讲述的for...of服务
迭代器,往往就是一个指针对象,不断调用,然后不断地指向下一个对象的过程,直到结束。ES6中,我们可以创建一个指针对象,然后调用next的函数,使得指针对象向下移动。同时,next函数会返回value和done,确定是否到达末尾。
同时,ES6还提供了Iterator接口——Symbol.iterator。首先,我们来看一下具备原生接口的集合类——数组,类数组对象、Set和Map。这样我们就可以直接调用它的接口来进行循环:
let arr = ["my", "name", "is", "iterator"]; let iter = arr[Symbol.iterator](); console.log(iter.next()); //{ value: "my", done: false} console.log(iter.next()); //{ value: "name", done: false} console.log(iter.next()); //{ value: "is", done: false}
同时,定义iterator接口的数据结构可以轻松的使用for...of进行值的遍历
let arr = ["I", "has", "iterator"]; for(let item of arr){ console.log(item); } //"I", "has", "iterator"
但是,如果没有定义iterator接口的数据结构就没有办法使用这种方式进行遍历,如图:
这时,我们又该如何呢?其实,针对一些可迭代的数据结构,我们是可以自定义迭代器的,例如:
let iteratorObj = { 0: "a", 1: "b", 2: "c", length: 3, [Symbol.iterator]: Array.prototype[Symbol.iterator] } for(let item of iteratorObj){ console.log(item); } // "a", "b", "c"
迭代器是一个非常实用的东西,不妨你也可以试试,同时去改善你的代码。
Generator和Promise其实,这两个是比较难以理解的东西。如果只是粗浅的了解一下,还是有许多的新东西的。在ES6中,引入了generator和promise两个概念。可能在这之前,你已经使用过了,通过其他的类库实现的。那么,其实ES6中的新概念也是差不多的,只是标准化了而已。
generator,叫做生成器。可以说与iterator有点相似,同样是通过next函数,来一步步往下执行的。同时,它的定义时,所使用的是function*的标识符。还具备yield这个操作符,可以实现逐步逐步向下执行。我们来看个例子:
function* generator(){ yield 1; yield 2; yield 3; }; let generate = generator(); console.log(generate.next()); //{value: 1, done: false} console.log(generate.next()); //{value: 2, done: false} console.log(generate.next()); //{value: 3, done: true} console.log(generate.next()); //{value: undefined, done: true}
这样子看起来,似乎就是迭代器的步骤。其实,iterator的接口,可以定义成这样子的形式。但是,generator的作用不仅仅如此。它就像一个状态机,可以在上一个状态到下一个状态之间进行切换。而一旦遇到yield部分,则可以表示当前是可以步骤的暂停。需要等到调用next方法才能进行下一步骤。同时,我们还可以使用上一步的结果值,进行下一步的运算。示例:
function* generator(){ yield 1; let value = yield 2; yield 3 + value; }; let generate = generator(); let value1 = generate.next(); let value2 = generate.next(); let value3 = generate.next(value2.value); console.log(value1); //{value: 1, done: false} console.log(value2); //{value: 2, done: false} console.log(value3); //{value: 5, done: true}
这样的话,就可以将value作为你第三步的参数值,进行使用。
之前说过,generator的next是需要自己调用的。但是,我们如何使它自己自动调用呢。我们可以使用for...of来自动调用next,就像迭代器一样。示例:
function* generator(){ yield 1; yield 2; yield 3; }; for(let value of generator()){ console.log(value); } //1, 2, 3
其实,之前所讲的只是generator的基本使用。generator主要被使用在异步编程领域。因为我们之前所讲的特性,非常适合在异步编程中使用。当然了,我们也需要去提一下promise这个异步编程的功臣。
Promise,翻译过来叫做承诺。我们可以理解为一种约定。大家都知道异步编程的时候,我们一般会使用到回调函数这个东西。但是,回调函数会导致的问题,也非常的明显。示例:
callback1(function(data){ //... callback2(function(data1){ const prevData = data; //... callback3(function(){ //... callback4(function(){ //... }); }); }); });
回调函数,写多了之后我们会发现,这个倒金字塔会越来越深,而我们会越来越难以管理。
这时,或许promise会起到一定的作用。试想一下,为什么这几个回调函数都能在另一个回调函数之外进行?主要原因:
每个回调函数,都会无法确定另一个回调函数会在何时会被调用,因为这个控制权不在当前这个程序之中。
每个回调函数,都或多或少的依赖于上一个回调函数执行的时间和数据
基于这两点,我们就会发现,一旦你需要这样去编写代码,就必须保证你的上一个回调函数在下一个回调函数之前进行。我们还可以发现,它们之间缺乏一种约定,就是一旦上一个发生了,无论是正确还是错误,都会通知对应的回调函数的约定。
Promise,或许就是起到了这样的一种作用。它具备三种状态:pending、resolved、rejected。它们之间分别对应:正在进行、已解决、已拒绝等三种结果。一个回调函数会开始从pending状态,它会向resolved和rejected的两者之一进行转换。而且这种转换是不可变的,即一旦你从pending状态转变到resolved状态,就不可以再变到rejected状态去了。
然后,promise会有一个then函数,可以向下传递之前回调函数返回的结果值。我们可以写个promise示例:
new Promise((resolved, rejected) => { resolved(1); }).then(data => { console.log(data); }, err => { console.log(err); }).catch(err => { console.log(err); }); // 1
其实,只需要记住这样子的一种形式,就可以写好promise。Promise是一个比较容易书写的东西。因为它的形式比较单一,而且现在有许多封装的比较好的异步请求库,都带有Promise的属性,例如axios。
Promise,还带有其他的一些API,上面我们也使用到了一个。
catch:用于指定发生错误时的回调函数。主要是我们之前说过Promise有个不可变的特性,所以,一旦这一个过程中发生错误,但是状态无法转变,只能在下一个流程中去捕获这个错误。因此,为了预防最后一个流程发生错误,需要在最后使用catch去捕获最后一个流程中的错误。
all:用于将多个Promise实例包装成一个新的Promise实例。
这个函数需要其中所有的Promise实例都变成Fulfilled时,才会将结果包装成一个数组传递给下一个Promise。
如果其中有一个Promise实例变成Rejected时,就会将第一个Rejected的结果传递给下一个Promise
race:也是用于将多个Promise实例包装成一个新的Promise实例。但是这个函数有所不同
如果这个函数中,有一个Promise变成Fulfilled时,它就会将结果传递给下一个Promise
resolve:它会将一个当前对象转化为Promise对象
reject:返回一个出错的Promise对象
Promise可以和之前所讲的Generator一起使用,我们可以看一下使用场景:
通过Generator函数来管理流程,遇到异步操作,就使用Promise进行处理。
function usePromise(){ return new Promise(resolve => { resolve("my name is promise"); }); } function* generator(){ try{ let item = yield usePromise(); console.log(item); }catch(err){ console.log(err); } } let generate = generator(); generate.next().value.then(data => { console.log(data); }, err => { console.log(err); }).catch(err => { console.log(err); }); //my name is promise
或许,你还可以写出更加复杂的程序。
Class和extends最后要聊的一个主题就是class。相信抱怨javascript没有类的特性数不胜数。同时,还需要去了解js的类继承式概念。那么,ES6也带来了我们最欢迎的class module部分。我们就不介绍之前我们是如果去构建对象的了(好像是构造函数)。
那么,我们可以来看一下,ES6给我带来的新变化:
class Animal{ constructor(name){ this.name = name; } sayName(){ return this.name; } } const animal = new Animal("dog"); console.log(animal.sayName()); // "dog"
似乎这样子的形式比之前的构造函数的方式强对了。我们可以理解一下这个结构:
其内部的constructor:指向的就是整个类的constructor
其内部的函数:这些函数的定义在类的原型上面
因此,上面那个其实可以写成原先的:
function Animal(name){ this.name = name; } Animal.prototype.sayName = function(){ return this.name; }
其实,就是class在ES6中得到了封装,可以使得现在的方式更加的优美。
之后,我们简单了解一下继承这个概念吧。
任何的东西,都是需要继承的。因为我们不可能都是从头去写这个类。往往是在原有类的基础之上,对它进行完善。在ES6之前,我们可能对构造函数完成的是组合式继承。示例:
function Animal(name){ this.name = name; } Animal.prototype.sayName = function(){ return this.name; } function Dog(name, barking){ Animal.call(this, name); this.barking = barking; } Dog.prototype = new Animal(); Dog.prototype.constructor = Dog; Dog.prototype.makeBarking = function(){ return this.barking; } const dog = new Dog("zimo", "汪汪汪"); console.log(dog.makeBarking()); //汪汪汪 console.log(dog.sayName()); //zimo
这样子的组合式继承书写起来,比较麻烦,需要重新去对每个元素设置,然后还要重新定义新类的原型。那么,我们可以来看一下ES6对于继承的封装:
class Animal{ constructor(name){ this.name = name; } sayName(){ return this.name; } } class Dog extends Animal{ constructor(name, barking){ super(name); this.barking = barking; } makeBarking(){ return this.barking; } }
这样子,就可以轻松的完成之前的组合式继承步骤了。如果你对extends的封装感兴趣的话,不妨看一下这篇文章javascript之模拟类继承
总结在这里ES6的内容只是总结了部分,大致可以分为这么几个部分:
变量定义——let和const
函数的变化——箭头函数、剩余参数
数组的变动——解构,展开符
字符串——模版字符串、startsWith、endsWith
Iterator和for...of
Generator和Promise
Class和extends
希望,你可以从这些内容中对ES6多一些了解,同时,如果你还想深入ES6进行了解的话,最直接的方式就是看书。希望你的代码写的越来越优雅。
如果你对我写的有疑问,可以评论,如我写的有错误,欢迎指正。你喜欢我的博客,请给我关注Star~呦。大家一起总结一起进步。欢迎关注我的github博客
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/88868.html
摘要:前言这里筑梦师是一名正在努力学习的开发工程师目前致力于全栈方向的学习希望可以和大家一起交流技术共同进步用简书记录下自己的学习历程个人学习方法分享本文目录更新说明目录学习方法学习态度全栈开发学习路线很长知识拓展很长在这里收取很多人的建议以后决 前言 这里筑梦师,是一名正在努力学习的iOS开发工程师,目前致力于全栈方向的学习,希望可以和大家一起交流技术,共同进步,用简书记录下自己的学习历程...
摘要:的翻译文档由的维护很多人说,阮老师已经有一本关于的书了入门,觉得看看这本书就足够了。前端的异步解决方案之和异步编程模式在前端开发过程中,显得越来越重要。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。 JavaScript Promise 迷你书(中文版) 超详细介绍promise的gitbook,看完再不会promise...... 本书的目的是以目前还在制定中的ECMASc...
阅读 2434·2021-10-09 09:44
阅读 3792·2021-09-22 15:43
阅读 2923·2021-09-02 09:47
阅读 2537·2021-08-12 13:29
阅读 3869·2019-08-30 15:43
阅读 1679·2019-08-30 13:06
阅读 2187·2019-08-29 16:07
阅读 2745·2019-08-29 15:23