理解好 this 的处理机制可以设计出更加完善的 JavaScript 应用程序。this 在 ES6 的箭头函数中的表现也有所不同,可以查阅 ES6 中有关箭头函数的内容。
在 ES6 中可以使用反引号来声明一个字符变量的值。let name = `imooc`;上面例子中我们知道在 ES5 中拼接字符串需要用 + 运算符,但在 ES6 中把字符串写在 ```` 中,如果字符串中有变量可以使用 ${} 把变量放在大括号中。如下实例:var name = '慕课网';var lang = 'ES6';console.log(`这是${name}的${lang}教程!`);// 这是慕课网的ES6教程!上面的代码中把变量写在 ${} 的大括号中,在编译的过程会直接替换对应的变量。这种方式很明了地表达了字符串的完整性,更加直观地表达字符串的含义。
在讲解剩余参数前,我们先来看看,剩余参数在函数参数中都解决了哪些问题?为什么会引入剩余参数的概念?在 ES5 中,函数经常会传入不定参数,在传入不定参数时,ES5 的给出的解决方案是通过 arguments 对象来获取函数调用时传递的参数。 arguments 对象不是一个数组,它是一个类数组对象,所谓类数组对象,就是指可以通过索引属性访问元素并且拥有 length 属性的对象。一个简单的类数组对象是长这样的:var arrLike = { 0: 'name', 1: 'age', 2: 'job', length: 3}而它所对应的数组应该是这样子的:var arr = ['name', 'age', 'job'];这里我们说类数组对象与数组的性质相似,是因为类数组对象在访问、赋值、获取长度上的操作与数组是一致的,具体内容可查阅相关的类数组使用。在函数体中定义了 Arguments 对象,其包含函数的参数和其它属性,以 arguments 变量来指代。下面我们看个实例:function fn() { console.log(arguments);}fn('imooc', 7, 'ES6')在控制台中打印出上面的代码结果,如下图所示:在定义函数的时候没有给定参数,但是通过 arguments 对象可以拿到传入的参数。可以看到 arguments 中包含了函数传递的参数、length 等属性,length 属性表示的是实参的长度,即调用函数的时候传入的参数个数。这样我们就对 arguments 对象有了一定的了解。在 ES5 的开发模式下,想要使用传递的参数,则需要按位置把对应的参数取出来。尽管 arguments 是一个类数组且可遍历的变量,但它终究不是数组,它不支持数组方法,因此我们不能调用 arguments.forEeach (…) 等数组的方法。需要使用一些特殊的方法转换成数组使用,如:function fn() { var arr = [].slice.call(arguments); console.log(arr)}fn('ES6');// ["ES6"]fn('imooc', 7, 'ES6');// ["imooc", 7, "ES6"]终于借助 call 方法把 arguments 转化成一个真正的数组了。但是这样无疑是一个繁琐的过程,而且不容易理解。这时 ES6 给出了它的完美解决方案 —— 剩余参数,那剩余参数是如何在函数传参中使用的呢?下面我们来看看实例:function fn(...args) { console.log(args)}fn('ES6');// ["ES6"]fn('imooc', 7, 'ES6');// ["imooc", 7, "ES6"]使用方式很简单在函数定义时使用 ... 紧接着跟一个收集的参数,这个收集的参数就是我们所传入不定参数的集合 —— 也就是数组。这样就很简单地摆脱了 arguments 的束缚。另外,还可以指定一个默认的参数,如下示例:function fn(name, ...args) { console.log(name); // 基础参数 console.log(args); // 剩下的参数组成的数组}fn('ES6');// 'ES6'// []fn('imooc', 7, 'ES6');// "imooc"// [7, "ES6"]上面的代码中给函数第一个参数,声明一个变量 name,剩余的参数会被 ... 收集成一个数组,这就是剩余参数。引入剩余参数就是为了能替代函数内部的 arguments,由于 arguments 对象不具备数组的方法,所以很多时候在使用之前要先转换成一个数组。而剩余参数本来就是一个数组,避免了这多余的一步,使用起来既优雅又自然。
要深入前端学习时绕不开的是 Node 的学习,而 Node 中自带了模块化系统,Node 中的模块化是基于 CommonJS 规范实现的。而 ES6 中的模块化与之还有很多的不同的地方。现阶段 Node 还依然使用的是 CommonJS 规范,而前端正在逐渐使用 ES6 module 规范。两个规定统一是一个漫长的过程,两者都存在历史遗留问题和兼容问题需要浏览器和 Node 核心的支持。有必要搞清楚两个规范的区别和注意事项,有助于我们深入地学习前端。上一节我们学习 ES6 Module 的环境搭建和基本用法,本节将继续学习模块化的相关知识,本节主要是学习 CommonJS 规范,还有对比 ES6 module 规范的一些区别和注意事项。
本节主要介绍了ES5、ES6 的内容,需要注意以下几点:JavaScript 不算是一门纯正的编程语言,它是一个集合,是由 ECMAScript、DOM 和 BOM 组合而成的,ECMAScript 才是 JavaScript 在 Web 编程中的核心。ES6 是一个分水岭,提供了很多超前的特性,不能直接在浏览器中使用,需要借助转换器,把 ES6 语法转换成 ES5 或者更低的版本才能在浏览器中运行。
使用 ES6 的模版字符串时,不需要添加换行符,反引号里的内容就是最后结果的直观表达,下面我们看一下上面插入 DOM 的例子使用 ES6 是如何写的。<div id="imooc"></div><script type="text/javascript"> let lang = 'ES6'; document.getElementById('imooc').innerHTML = `<h1>慕课网${lang}Wiki</h1> <p>这里是慕课网Wiki,未经许可不能转载</p> <div>Wiki主要内容</div>`;</script>对上述 DOM 进行插入操作,在有变量的地方把变量直接放入 ${} 大括号中即可。
对于静态字符串一律使用单引号,不使用双引号。在拼接字符串的时候需要使用反引号的方式,更加易于阅读。// badconst name = "imooc";const info = name + ' ES6 Wiki';// goodconst name = 'imooc';const info = `${name} ES6 Wiki`;
本节主要讲解了 ES6 对数值的扩展主要总结一下几点:增加了二进制和八进制表示法;直接使用 Number 进行类型转换;移植了原有的方法到 Number 对象上;对新增的 Number 对象上的属性做了简单的解释。ES6 的 Number 对象还提供了很多方法,下面的小节会讲到,还有 ES6 对数字的处理在 Math 小节做详细的讲解。
上面我们看到了二维数组转为 Map 数据结构,那么 Map 数据结构怎么转回数组呢?其实很简单,和前面已经提过的 Set 转数组的方式一样,Map 也可以使用扩展运算符 (…) 进行转换。const map = new Map()map.set('name', 'imooc')map.set({name: 'imooc'}, ['JavaScript', 'ES6 wiki']);[...myMap]// [['name', 'imooc'], [{name: 'imooc'}, ['JavaScript', 'ES6 wiki']]]
本节学习了 Number 对象下的方法,讲解了为什么把全局的方法移植到 Number 对象下,以及对比没有 ES6 时是怎么判断数值为整数的情况,通过对 Number 对象下的方法的学习,可以看到 ES6 在收敛全局的方法,使语言逐步模块化,更加符合语言的规范。
indexOf() 在查询数组中元素时存在一些问题,下面我们就来看看为什么 ES6 要引入 includes() 方法。在 ES5 中使用 indexOf() 方法在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1。但是查找数组时存在一定缺陷,indexOf 不能判断数组中是否有 NaN,对于数组中的空项也不能判断。var arr1 = [,,,,,];var arr2 = [null, undefined, NaN];console.log(arr1[0], arr1[1]) // undefined undefinedarr1.indexOf(undefined) // -1arr2.indexOf(NaN); // -1上面的代码可以看到,在第 1 行中数组的每一项都是空的, 使用 indexOf() 查找返回的结果为 -1,没有查到 undefined 值,但从第 3 行打印的结果可以看到其实空数组的每一项都是 undefined。另外,还有个问题 indexOf() 不能解决,数组中有 NaN 时查询不了,返回的结果也是 -1。ES6 的includes() 可以完美解决上面的问题,看如下示例:[,,,,,].includes(undefined) // true[null, undefined, NaN].includes(NaN)] // true从上面的代码可以看出,使用 includes() 查询可以得到正确的结果。indexOf 返回的是数值型的,而 includes 返回的是布尔型的,方便逻辑判断。如下实例:var arr = ['imooc', 'ES6', 'wiki'];if (arr.includes('ES6')) { // todo}if (arr.indexOf('ES6') !== -1) { // todo}
ES6 对数值进行了大的改版,在向下兼容的同时,把所有的数值方法由全局移植到了 Number 对象下,在数组和字符串中也做了类似的操作,这样的主要目的是精简全局方法,更加明确的表达方法的含义。另外,ES6 还增加了二进制和八进制的表示方法,并增加了很多数值处理的方法,本节就来看看 ES6 对数值的扩展。
除了属性可以简写,对象中的方法也是可以简写的,而且更加简洁明了。我们来看下面的例子:const name = '张三'// ES5var person = { name: name, getName: function() { console.log('imooc') }};// ES6var person = { name, getName() { console.log(this.name) }};console.log(person) // {name: "imooc", getName: ƒ}上面的代码中,ES5 中定义一个对象上的方法时需要使用 function 关键字来定义,而 ES6 则直接省略了 冒号和 function 关键字。可以看出使用 ES6 这种简洁的方式更具表达力。在 Node 中进行模块导出时,这种方式更加方便。我们看下面的例子:var person = {};function getName () { return person.name}function setName () { person.name = '李四'}function clear () { person = {};}// ES5 写法module.exports = { getName: getName setName: setName, clear: clear};// ES6写法module.exports = { getName, setName, clear };上面的代码中,我们定义了一个 person 对象,并向外暴露了若干方法用来操作 person 对象,在导出的时候可以看出,ES6 不需要重复地去写变量名,从而更简洁地表达了模块所提供的方法。
在 Map 转 Object 时需要注意的是,因为 Map 实例上的键可以是任意类型,而 Object 上的键只能是字符串类型。所有,如果 Map 的键都是字符串,它可以转为对象,如果键是一个对象,在转为对象时会被进行 toString 操作。function mapToObj(map){ const obj = {} for (let [key, value] of map){ obj[key] = value; } return obj;}const map1 = new Map()map1.set('name', 'imooc')map1.set('lesson', 'ES6 Wiki');mapToObj(map1) // {name: "imooc", lesson: "ES6 Wiki"}const map2 = new Map()map2.set('name', 'imooc')map2.set({name: 'lesson'}, ['JavaScript', 'ES6 wiki']);mapToObj(map2) // {name: "lesson", [object Object]: ["JavaScript", "ES6 wiki"]}上面的代码中需要注意的是 map2,它的第二个元素的键是一个对象,在转换对象的键时进行了 toString 操作,变成字符串 [object Object]。
在 ES6 中和提升相关的内容又有些许不同,let 和 const 这两个新关键字对提升的表现也和 var 不同,具体可以参阅 ES6 中的相关内容。现在和提升相关的内容更多的出现在面试题里,由于代码检查工具的介入,一些由于提升特性造成的 BUG 出现的越来越少。
ES6 提供了二进制和八进制数值表示的新写法,分别用前缀 0b(或 0B)和 0o(或 0O)表示。对应的十六进制我们知道用 0x 作为前缀来表示的。下面我们来看看二进制和八进制是怎么表示的。// es6 2进制 0B 开头console.log('B',0B111110111); // 503console.log('B',0b111110111); // 相同,0B 和 0b 都可以// es6 8进制 0o 开头console.log(0o767); // 503console.log(0O767); // 相同,0o 和 0O 都可以上面的代码分别使用了二进制和八进制对数字 503 进行了表示,那下面我们来验证一下:0b111110111 === 503 // true0o767 === 503 // true上面的代码中使用了全等的方式进行判断,很明显,ES6 是支持这种方式的表示的。从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀 0 表示,ES6 进一步明确,要使用前缀 0o 表示。// 非严格模式(function(){ console.log(0o11 === 011); // true})()// 严格模式(function(){ 'use strict'; console.log(0o11 === 011);})()// Uncaught SyntaxError: Octal literals are not allowed in strict mode.将 0b 和 0o 前缀的字符串数值转为十进制,可以把 Number 对象直接作为方法使用。Number('0b111') // 7Number('0o10') // 8
虽然目前还在维护的最新版的浏览器几乎都支持了大部分 ES6 特性,但国内生态还不允许直接将 ES6 代码运行于线上,所以就需要一定的解决方案,使开发者开发过程中全面使用 ES6,但是线上又是运行 ES5、ES3 的代码。特性上会采用 shim 的方式,大部分情况下概念会将它与 polyfill 混用,可以理解成给浏览器打补丁,让旧版的浏览器支持新版的特性,如 ES6 提供的 Object.assign 方法,旧版的浏览器是没有的。通过 polyfill,使用 ES5 将刚该方法实现后,在放到 Object 对象下,变相的让浏览器支持新特性。但还有一些特性是 polyfill 很难解决的,特比是语法特性,如 let 关键字。这些特性就会采用 编译 的方式来解决,如将 let 替换成 var,这一块最常用的工具目前是 babel。所以如果项目是需要上线运行,并且目标用户群体范围非常广,尽量不要上线 ES6 代码,上线前也做好各个平台的浏览器测试。
本节开始我们将进入 ES6 实战课程,首先会花费两节的时间来学习 Vue3 响应式原理,并实现一个基础版的 Vue3 响应式系统;然后通过 Promise 来封装一个真实业务场景中的 ajax 请求;最后我们会聊聊前端开发过程中的编程风格。本实战主要通过对前面 ES6 的学习应用到实际开发中来,Vue3 的响应式系统涵盖了大部分 ES6 新增的核心 API,如:Proxy、Reflect、Set/Map、WeakMap、Symbol 等 ES6 新特性的应用。更加深入地学习 ES6 新增 API 的应用场景。由于篇幅内容有限,本实战不会完全实现 Vue3 响应式系统的所有 API,主要实现 reactive 、 effect 这四个核心 API,其他 API 可以参考 vue-next源码。本节的目录结构和命名和 Vue3 源码基本一致,在阅读源码的时候我们能看到作者的思考,和功能细颗粒度的拆分,使得代码更易于扩展和复用。
Object 转 Map 没有一步到位的方法,需要去遍历 Object 然后逐个添加。function objToMap(obj){ let map = new Map(); for (let [key, value] of Object.entries(obj)){ map.set(key, value); } return map;}objToMap({name:'imooc', lesson: 'ES6 Wiki'})// Map(2) {"name" => "imooc", "lesson" => "ES6 Wiki"}上面的代码中,我们创建了一个方法用于 Object 转 Map 使用,函数内部先构造一个 Map 实例,然后对 Object 进行遍历,逐个添加到 Map 实例上。