ES6+
ES6 的环境配置
前言
ECMAScript 简介
ES 的全称是 ECMAScript,它是由 ECMA 国际标准化组织 制定的一套脚本语言的标准化规范。
详细来说,ES 是由 ECMA 的第 39 号技术专家委员会(Technical Committee 39,简称 TC39)负责制订 ECMAScript 标准,成员包括 Microsoft、Mozilla、Google 等公司。
PS:简单来说,ECMAScript 是 JS 的语言标准。当然,ECMAScript 还包括其他脚本语言的语言标准。
ECMAScript 版本发布记录
1995 年:ECMAScript 诞生。
1997 年:ECMAScript 标准确立。ECMA 发布 ECMA-262 标准,推出浏览器标准语言 ECMAScript 1.0。
1999 年:发布 ES3;与此同时,IE5 风靡一时。
2009 年:发布 ECMAScript 5.0(简称 ES5)。例如 foreach、Object.keys、Object.create 和 json 标准。
2011 年:发布 ECMAScript5.1,成为 ISO 国际标准,从而推动所有浏览器都支持。
2015 年 6 月:发布 ECMAScript 6(简称 ES6),即 ECMAScript 2015。(注意,前者是按版本号区分,后者是按年份区分。ES 的后续的版本,请尽量用年份来命名。)
2016 年 6 月:发布 ECMAScript 7,即 ECMAScript 2016。
2017 年 6 月:发布 ECMAScript 8,即 ECMAScript 2017。
2018 年 6 月:发布 ECMAScript 9,即 ECMAScript 2018。
2019 年 6 月:发布 ECMAScript 10,即 ECMAScript 2019。
2020 年 6 月:发布 ECMAScript 11,即 ECMAScript 2020。
……
- 此后,每年更新一版。
ES6 简介
从上面的 ES 的版本记录可以看出:2015 年 6 月,ES6 正式发布。如果用年份来命名版本号,也可以称之为 ES2015。
ES6 是新的 JS 语法标准。ES6 实际上是一个泛指,泛指 ES 2015 及后续的版本。
很多人在做业务选型的时候,会倾向于选 jQuery。其实 jQuery 的语法是偏向于 ES3 的。而现在主流的框架 Vue.js 和 React.js 的默认语法,都是用的 ES6。
ES6 的改进如下:
ES6 之前的变量提升,会导致程序在运行时有一些不可预测性。而 ES6 中通过 let、const 变量优化了这一点。
ES6 增加了很多功能,比如:常量、作用域、对象代理、异步处理、类、继承等。这些在 ES5 中想实现,比较复杂,但是 ES6 对它们进行了封装。
ES6 之前的语法过于松散,实现相同的功能,不同的人可能会写出不同的代码。
ES6 的目标是:让 JS 语言可以编写复杂的大型应用程序,成为企业级开发语言。
ES6 的环境配置(为了兼容 ES5)
掌握 ES6 之后,如果要考虑 ES5 的兼容性,可以这样做:写 ES6 语法的 js 代码,然后通过 Babel
将 ES6 转换为 ES5。
babel 的作用是将 ES6 语法转为 ES5 语法,支持低端浏览器。
但是,在这之前,我们需要配置一下相关的环境。
建立工程目录
(1)先建立一个空的工程目录 es6
,并在目录下建立两个文件夹 src
和 dist
:
src
:书写 ES6 代码,我们写的 js 程序都放在这里。dist
:利用 Babel 编译生成的 ES5 代码。我们在 HTML 页面需要引入 dist 里的 js 文件。
(2)在 工程根目录里新建文件 index.html
:
1 |
|
注意,上方代码中,我们引入的是dist
目录下的 js 文件。
然后我们新建文件 src/index.js
:
1 | let a = 'smyhvae'; |
这个文件是一个 ES6 语法 的 js 文件,稍后,我们尝试把这个 ES6 语法的 js 文件转化为 ES5 的 js 文件。
PS:我们在写代码时,能用单引号尽量用单引号,而不是双引号,前者在压缩之后,程序执行会更快。
全局安装 Babel-cli
(1)初始化项目:
在安装 Babel 之前,需要先用 npm init 先初始化我们的项目。打开终端或者通过 cmd 打开命令行工具,进入项目目录,输入如下命令:
1 | npm init -y |
上方代码中,-y
代表全部默认同意,就不用一次次按回车了(稍后再根据需要,在文件中手动修改)。命令执行完成后,会在项目的根目录下生成 package.json 文件:
1 | { |
PS:VS Code 里打开终端的快捷键是:Contol + ~
。
(2)全局安装 Babel-cli:
在终端中输入以下命令:
1 | npm install -g babel-cli |
如果安装比较慢的话,Mac 下可以使用cnpm
进行安装 ,windows 下可以使用npm
切换到 taobao 的镜像。
(3)本地安装 babel-preset-es2015 和 babel-cli:
1 | npm install --save-dev babel-preset-es2015 babel-cli |
安装完成后,会发现package.json
文件,已经多了 devDependencies 选项:
(4)新建.babelrc:
在根目录下新建文件.babelrc
,输入如下内容:
1 | { |
(5)开始转换:
现在,我们应该可以将 ES6 的文件转化为 ES5 的文件了,命令如下:(此命令略显复杂)
1 | babel src/index.js -o dist/index.js |
我们可以将上面这个命令进行简化一下。操作如下:
在文件 package.json
中修改键 scripts
中的内容:
1 | "scripts": { |
目前为止,环境配置好了。以后,我们执行如下命令,即可将src/index.js
这个 ES6 文件转化为 dist/index.js
这个 ES5 文件:
1 | npm run build |
我们执行上面的命令之后,会发现, dist 目录下会生成 ES5 的 js 文件:
index.js:
1 | ; |
当我们打开网页后,就可以在浏览器的控制台,看到代码的输出结果。
ES5中的严格模式
严格模式的理解
概念
理解:除了正常运行模式(混杂模式),ES5添加了第二种运行模式:”严格模式”(strict mode)。
顾名思义,这种模式使得Javascript在更严格的语法条件下运行。
目的:
消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为
消除代码运行的一些不安全之处,为代码的安全运行保驾护航
为未来新版本的Javascript做好铺垫
使用
针对整个脚本文件:将
use strict
放在脚本文件的第一行,则整个脚本文件将以严格模式运行。针对单个函数:将
use strict
放在函数体的第一行,则整个函数以严格模式运行。
PS:如果浏览器不支持,则这句话只解析为一条简单的语句, 没有任何副作用。
脚本文件的变通写法:因为第一种调用方法不利于文件合并,所以更好的做法是,借用第二种方法,将整个脚本文件放在一个立即执行的匿名函数之中。
严格模式和普通模式的区别
1.全局变量必须显式声明
在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明。
示例:
1 | ; |
在严格模式下,变量都必须先用var
、let
或cons
t声明,然后再使用。
2.禁止动态绑定
好处:在编译时就确定属性与方法到底归属哪个对象,有利于编译效率的提高,也有助于代码的阅读。
动态绑定:即某些属性和方法到底属于哪个对象,不是在编译时确定,而是在运行时确定。
哪些使用了动态绑定?
1.with语句
1 | ; |
为什么使用with语句,不确定属性的归属呢?
很简单,在正常模式下:with绑定的是obj对象,name属性在obj中,而sex不在obj中,则最终会将sex声明为为全局变量。
2.eval作用域
正常模式下:eval语句的作用域取决于它处于全局作用域,还是函数作用域。
严格模式下:eval语句本身就是一个作用域,它生成的变量只能在eval内部使用。
1 | ; |
3.禁止this指向全局对象,而是指向undefined
1 | var name="xiaoming"; |
4.禁止删除变量
严格模式下,声明的变量无法被删除,但对象中的属性,若设置了configurable:true
,即可配置,那么这个对象的属性是可以被删除的。
1 | ; |
5.函数声明必须在顶层
所谓的顶层是在全局中声明,即在块作用域中声明是错误的。
1 | ; |
6.禁止变量或函数参数重名
重名:在同一作用域,两个或两个以上变量名相同。
正常模式下,如果对象有多个重名属性,那么最后一个会覆盖前面的值。
但在严格模式下这是会报错的。
1 | ; |
正常模式下:在函数参数中,如果参数有重名情况,我们可以使用arguments[i]
读取参数,以便区分参数
1 | function foo(a,b,b,c){ |
上面重复参数名,在严格模式下报错。
7.限制了arguments对象
1.不允许对arguments赋值
正常模式下:
1 | function foo(a,b){ |
严格模式下会报错。
2.arguments不再跟踪参数的变化
1 | function foo(a,b){ |
严格模式下,a,b已经重新赋值,但是arguments没有记录它们的变化。
ES5中的一些扩展
JSON 对象
1、js对象(数组) –> json对象(数组):
1 | JSON.stringify(obj/arr) |
2、json对象(数组) –> js对象(数组):
1 | JSON.parse(json) |
上面这两个方法是ES5中提供的。
我们要记住,我们通常说的“json字符串”,只有两种:json对象、json数组。
typeof json字符串
的返回结果是string。
Object的扩展
ES5给Object扩展了一些静态方法,常用的有2个,我们接下来讲解。
方法一
1 | Object.create(prototype, [descriptors]) |
作用: 以指定对象为原型,创建新的对象。同时,第二个参数可以为为新的对象添加新的属性,并对此属性进行描述。
举例1:(没有第二个参数时)
1 | var obj1 = {username: 'smyhvae', age: 26}; |
obj1成为了obj2的原型。
举例2:(有第二个参数时)
第二个参数可以给新的对象添加新的属性。我们修改上面的代码,尝试给obj2添加新属性sex
:
1 | var obj1 = {username: 'smyhvae', age: 26}; |
上方代码中,我们通过第5行的sex给obj2设置了一个新的属性sex
,但是要通过value
来设置属性值(第6行)。
设置完属性值后,这个属性值默认是不可修改的,要通过writable
来设置。总而言之,这几个关键字的解释如下:
value
:设置属性值。writable
:标识当前属性值是否可修改。如果不写的话,默认为false,不可修改。configurable
:标识当前属性是否可以被删除。默认为false,不可删除。enumerable
:标识当前属性是否能用 for in 枚举。 默认为false,不可。
方法二
这个方法有点难理解。
1 | Object.defineProperties(object, descriptors) |
作用:为指定对象定义扩展多个属性。
代码举例:
1 | var obj2 = { |
get :用来获取当前属性值的回调函数
set :修改当前属性值得触发的回调函数,并且实参即为修改后的值
存取器属性:setter,getter一个用来存值,一个用来取值。
Object的扩展(二)
obj对象本身就自带了两个方法。格式如下:
1 | get 属性名(){} 用来得到当前属性值的回调函数 |
举例如下:
1 | var obj = { |
数组的扩展
下面讲的这几个方法,都是给数组的实例用的。
方法1:
1 | Array.prototype.indexOf(value) |
作用:获取 value 在数组中的第一个下标。
方法2:
1 | Array.prototype.lastIndexOf(value) |
作用:获取 value 在数组中的最后一个下标。
方法3:遍历数组
1 | Array.prototype.forEach(function(item, index){}) |
方法4:
1 | Array.prototype.map(function(item, index){}) |
作用:遍历数组返回一个新的数组,返回的是加工之后的新数组。
方法5:
1 | Array.prototype.filter(function(item, index){}) |
作用:遍历过滤出一个新的子数组,返回条件为true的值。
函数function的扩展:bind()
ES5中新增了
bind()
函数来改变this的指向。
1 | Function.prototype.bind(obj) |
作用:将函数内的this绑定为obj, 并将函数返回。
面试题: call()、apply()和bind()的区别:
都能改变this的指向
call()/apply()是立即调用函数
bind():绑定完this后,不会立即调用当前函数,而是将函数返回,因此后面还需要再加
()
才能调用。
PS:bind()传参的方式和call()一样。
分析:
为什么ES5中要加入bind()方法来改变this的指向呢?因为bind()不会立即调用当前函数。
bind()通常使用在回调函数中,因为回调函数并不会立即调用。如果你希望在回调函数中改变this,不妨使用bind()。
ES6 的变量声明
ES5 中,使用 var
定义全局变量( var 是 variable 的简写)。
ES6 中,新增了 let 和 const 来定义变量:
let
:定义局部变量,替代 var。const
:定义常量(定义后,不可修改)。
var:定义全局变量
看下面的代码:
1 | { |
上方代码是可以输出结果的,输出结果为 1。因为 var 是全局声明的,所以,即使是在区块里声明,但仍然在全局起作用。
也就是说:使用 var 声明的变量不具备块级作用域特性。
再来看下面这段代码:
1 | var a = 1; |
上方代码的输出结果为 2 ,因为 var 是全局声明的。
总结:
用 var 定义的全部变量,有时候会污染整个 js 的作用域。我们在如今的实战中,基本都是用的 ES6 语法,所以请尽量避免使用 var 定义变量。
let:定义局部变量
举例 1:
1 | { |
上方代码,打印报错。
举例 2:
1 | var a = 2; |
通过上面两个例子可以看出,用 let 声明的变量,只在局部(块级作用域内)起作用。
经典面试题:
let 可以防止数据污染,我们来看下面这个 for 循环的经典面试题。
1、用 var 声明变量:
1 | for (var i = 0; i < 10; i++) { |
上方代码可以正常打印结果,且最后一行的打印结果是 10。说明循环体外定义的变量 i,是全局作用域下的 i。
2、用 let 声明变量:
1 | for (let i = 0; i < 10; i++) { |
上方代码的关键在于:每次循环都会产生一个块级作用域,每个块级作用域中会重新定义一个新的变量 i。
另外,上方代码的最后一行无法打印结果,也就是说打印会报错。因为用 let 定义的变量 i,只在{ }
这个块级作用域里生效。
总结:我们要习惯用 let 声明,减少 var 声明带来的污染全局空间。
为了进一步说明 let 不会带来污染,需要说明的是:当我们定义了let a = 1
时,如果我们在同一个作用域内继续定义let a = 2
,是会报错的。
const:定义常量
在程序开发中,有些变量是希望声明后,在业务层就不再发生变化,此时可以用 const 来定义常量。
常量就是值(内存地址)不能变化的量。
如果用 const 声明基本数据类型,则无法被修改;
如果用 const 声明引用数据类型(即“对象”),这里的“无法被修改”指的是不能改变内存地址的引用;
但对象里的内容是可以被修改的。
举例:
1 | const name = 'smyhvae'; //定义常量 |
用 const 声明的常量,只在局部(块级作用域内)起作用;而且,用 const 声明常量时,必须赋值,否则报错。
let 和 const 的特点【重要】
不属于顶层对象 Window
var 声明的变量会挂载在 window 对象上,而 let 和 const 声明的变量不会
不允许重复声明
支持块级作用域
不存在变量提升 暂时性死区
var可以先使用,再声明(undefined)如果区块中存在 let 和 const 命令,则这个区块对这些命令声明的变量从一开始就形成封闭作用域。只要在声明之前使用这些变量,就会报错。这种语法称为“暂时性死区”(temporal dead zone,简称TDZ)。
综上:var 声明的变量,很容易造成全局污染。
for 循环举例(经典案例)
代码 1、我们先来看看如下代码:(用 var 定义变量 i)
1 |
|
上方代码中的运行效果如下:
你可能会感到诧异,为何点击任何一个按钮,弹出的内容都是 4 呢?这是因为,我们用 var 定义的变量 i,是在全局作用域声明的。整个代码中,自始至终只有一个变量。
for 循环是同步代码,而 onclick点击事件是异步代码。当我们还没点击按钮之前,同步代码已经执行完了,变量 i 已经循环到 4 了。
也就是说,上面的 for 循环,相当于如下代码:
1 | var i = 0; |
代码 2、上面的代码中,如果我们改为用 let 定义变量 i:
1 |
|
上方代码中的运行效果如下:
上面这个运行结果,才是我们预期的效果。我们用 let 定义变量 i,在循环的过程中,每执行一次循环体,就会诞生一个新的 i。循环体执行 4 次,就会有四个 i。
补充知识
宏任务与微任务
原理图:
说明:
- JS中用来存储待执行回调函数的队列包含2个不同特定的列队
宏队列
:用来保存待执行的宏任务(回调),比如:定时器
回调/ajax回调/dom事件回调微队列
:用来保存待执行的微任务(回调),比如:Promise
的回调/muntation回调
- JS执行时会区别这2个队列:
- JS执行引擎首先必须执行所有的
初始化同步任务
代码 - 每次准备取出第一个
宏任务执行前
,都要将所有的微任务
一个一个取出来执行
- JS执行引擎首先必须执行所有的
ES5 中如何定义常量
ES5中有Object.defineProperty
这样一个api,可以定义常量。这个API中接收三个参数。
代码举例:
1 | // 定义常量 PI |
解构赋值
解构赋值的概念
解构赋值:ES6 允许我们,按照一一对应的方式,从数组或者对象中提取值,再将提取出来的值赋值给变量。
解构:分解数据结构;赋值:给变量赋值。
解构赋值在实际开发中可以大量减少我们的代码量,并且让程序结构更清晰。
数组的解构赋值
数组的结构赋值:将数组中的值按照位置提取出来,然后赋值给变量。
语法
在 ES6 之前,当我们在为一组变量赋值时,一般是这样写:
1 | var a = 1; |
或者是这样写:
1 | var arr = [1, 2, 3]; |
现在有了 ES6 之后,我们可以通过数组解构的方式进行赋值:(根据位置进行一一对应)
1 | let [a, b, c] = [1, 2, 3]; |
二者的效果是一样的,但明显后者的代码更简洁优雅。
未匹配到的情况
数据的结构赋值,是根据位置进行一一对应来赋值的。可如果左边的数量大于右边的数量时(也就是变量的数量大于值的数量时),多余的变量要怎么处理呢?
答案是:如果变量在一一对应时,没有找到对应的值,那么,多余的变量会被赋值为 undefined。
解构时,左边允许有默认值
在解构赋值时,是允许使用默认值的。举例如下:
1 | { |
将右边的 undefined
和null
赋值给变量
如果我们在赋值时,采用的是 undefined
或者null
,那会有什么区别呢?
1 | { |
上方代码分析:
undefined:相当于什么都没有,此时 b 采用默认值。
null:相当于有值,但值为 null。
对象的解构赋值
对象的结构赋值:将对象中的值按照属性匹配的方式提取出来,然后赋值给变量。
语法
在 ES6 之前,我们从接口拿到 json 数据后,一般这么赋值:
1 | var name = json.name; |
上面这种写法,过于麻烦了。
现在,有了 ES6 之后,我们可以使用对象解构的方式进行赋值。举例如下:
1 | const person = { name: 'leslie', age: 28, sex: '男' }; |
上方代码可以看出,对象的解构与数组的结构,有一个重要的区别:数组的元素是按次序排列的,变量的取值由它的位置决定;
而对象的属性没有次序,是根据键来取值的。
未匹配到的情况
对象的结构赋值,是根据属性名进行一一对应来赋值的。可如果左边的数量大于右边的数量时(也就是变量的数量大于值的数量时),多余的变量要怎么处理呢?
答案是:如果变量在一一对应时,没有找到对应的值,那么,多余的变量会被赋值为 undefined。
给左边的变量自定义命名
对象的结构赋值里,左边的变量名一定要跟右边的属性名保持一致么?答案是不一定。我们可以单独给左边的变量自定义命名。
举例如下:
1 | const person = { name: 'leslie', age: 28 }; |
上方的第 2 行代码中:(请牢记)
等号左边的属性名 name、age 是对应等号右边的属性名。
等号左边的 myName、myAge 是左边自定义的变量名。
圆括号的使用
如果变量 foo 在解构之前就已经定义了,此时你再去解构,就会出现问题。下面是错误的代码,编译会报错:
1 | let foo = 'haha'; |
要解决报错,只要在解构的语句外边,加一个圆括号即可:
1 | let foo = 'haha'; |
字符串解构
字符串也可以解构,这是因为,此时字符串被转换成了一个类似数组的对象。举例如下:
1 | const [a, b, c, d] = 'hello'; |
打印结果:
1 | h |
函数扩展
箭头函数
定义箭头函数的语法
语法:
1 | (参数1, 参数2 ...) => { 函数体 } |
解释:
如果有且仅有 1 个形参,则
()
可以省略如果函数体内有且仅有 1 条语句,则
{}
可以省略,但前提是,这条语句必须是 return 语句。
需要强调的是,箭头函数是没有函数名的,既然如此,那要怎么调用箭头函数呢?你可以将箭头函数赋值给一个变量,通过变量名调用函数;也可以直接使用箭头函数。我们来看看下面的例子。
举例
写法 1、定义和调用函数:(传统写法)
1 | function fn1(a, b) { |
写法 2、定义和调用函数:(ES6中的写法)
1 | const fn2 = (a, b) => { |
上面的两种写法,效果是一样的。
从上面的箭头函数中,我们可以很清晰地看到变量名、参数名、函数体。
另外,箭头函数的写法还可以精简一下,继续往下看。
在箭头函数中,如果方法体内只有一句话,且这句话是 return 语句,那就可以把 {}
省略。写法如下:
1 | const fn2 = (a, b) => a + b; |
在箭头函数中,如果形参只有一个参数,则可以把()
省略。写法如下:
1 | const fn2 = a => { |
箭头函数的 this 的指向
箭头函数只是为了让函数写起来更简洁优雅吗?当然不只是这个原因,还有一个很大的作用是与 this 的指向有关。
ES6 之前的普通函数中:this 指向的是函数被调用的对象(也就是说,谁调用了函数,this 就指向谁)。
而 ES6 的箭头函数中:箭头函数本身不绑定 this,this 指向的是箭头函数定义位置的 this(也就是说,箭头函数在哪个位置定义的,this 就跟这个位置的 this 指向相同)。
代码举例:
1 | const obj = { name: 'leslie' }; |
打印结果:
1 | obj |
代码解释:(一定要好好理解下面这句话)
上面的代码中,箭头函数是在 fn1()函数里面定义的,所以第二个 this 跟 第一个 this 指向的是同一个位置。又因为,在执行 fn1.call(obj)
之后,第一个 this 就指向了 obj,所以第二个 this 也是指向 了 obj。
面试题:箭头函数的 this 指向
代码举例:
1 | var name = '许嵩'; |
上方代码的打印结果是什么?你可能很难想到。
正确答案的打印结果是许嵩
。因为 obj
这个对象并不产生作用域, sayHello()
这个箭头函数实际仍然是定义在 window 当中的,所以 这里的 this 指向是 window。
补充
作用域
- 全局作用域
- 函数作用域:
function() {}
- 块级作用域:
{}
参数默认值
传统写法:
1 | function fn(param) { |
上方代码中,函数体内的写法是:如果 param 不存在,就用 hello
字符串做兜底。这样写比较啰嗦。
ES6 写法:(参数默认值的写法,很简洁)
1 | function fn(param = 'hello') { |
在 ES6 中定义方法时,我们可以给方法里的参数加一个默认值(缺省值):
方法被调用时,如果没有给参数赋值,那就是用默认值;
方法被调用时,如果给参数赋值了新的值,那就用新的值。
如下:
1 | var fn2 = (a, b = 5) => { |
提醒 1:默认值的后面,不能再有没有默认值的变量。比如(a,b,c)
这三个参数,如果我给 b 设置了默认值,那么就一定要给 c 设置默认值。
提醒 2:
我们来看下面这段代码:
1 | let x = 'smyh'; |
注意第二行代码,我们给 y 赋值为x
,这里的x
是括号里的第一个参数,并不是第一行代码里定义的x
。打印结果:vae vae
。
如果我把第一个参数改一下,改成:
1 | let x = 'smyh'; |
此时打印结果是:vae smyh
。
剩余参数
剩余参数允许我们将不确定数量的剩余的元素放到一个数组中。
比如说,当函数的实参个数大于形参个数时,我们可以将剩余的实参放到一个数组中。
传统写法:
ES5 中,在定义方法时,参数要确定个数,如下:(程序会报错)
1 | function fn(a, b, c) { |
上方代码中,因为方法的参数是三个,但使用时是用到了四个参数,所以会报错:
ES6 写法:
ES6 中,我们有了剩余参数,就不用担心报错的问题了。代码可以这样写:
1 | const fn = (...args) => { |
打印结果:
1 | 1 |
上方代码中注意,args 参数之后,不能再加别的参数,否则编译报错。
下面这段代码,也是利用到了剩余参数:
1 | function fn1(first, ...args) { |
剩余参数的举例:参数求和
代码举例:
1 | const sum = (...args) => { |
打印结果:60
剩余参数和解构赋值配合使用
代码举例:
1 | const students = ['张三', '李四', '王五']; |
扩展运算符(展开语法)
扩展运算符和剩余参数是相反的。
剩余参数是将剩余的元素放到一个数组中;
而扩展运算符是将数组或者可迭代对象拆分成逗号分隔的参数序列。
代码举例:
1 | const arr = [10, 20, 30]; |
上面的代码要仔细看:
arr
是一个数组,而...arr
则表示10, 20, 30
这样的序列。
我们把...arr
打印出来,发现打印结果竟然是 10 20 30
,为啥逗号不见了呢?因为逗号被当作了 console.log 的参数分隔符。如果你不信,可以直接打印 console.log(10, 20, 30)
看看。
接下来,我们看一下扩展运算符的应用。
举例1:数组赋值
数组赋值的代码举例:
1 | let arr2 = [...arr1]; // 将 arr1 赋值给 arr2 |
为了理解上面这行代码,我们先来分析一段代码:(将数组 arr1 赋值给 arr2)
1 | let arr1 = ['www', 'smyhvae', 'com']; |
上方代码中,我们往往 arr2 里添加了你懂的
,却发现,arr1 里也有这个内容。原因是:let arr2 = arr1;
其实是让 arr2 指向 arr1 的地址。也就是说,二者指向的是同一个内存地址。
如果不想让 arr1 和 arr2 指向同一个内存地址,我们可以借助扩展运算符来做:
1 | let arr1 = ['www', 'smyhvae', 'com']; |
运行结果:
1 | arr1:www,smyhvae,com |
我们明白了这个例子,就可以避免开发中的很多业务逻辑上的 bug。
举例2:合并数组
代码举例:
1 | let arr1 = ['王一', '王二', '王三']; |
举例3:将伪数组或者可遍历对象转换为真正的数组
代码举例:
1 | const myDivs = document.getElementsByClassName('div'); |
补充:
我们在《JavaScript基础/数组的常见方法》中也学过,还有一种方式,可以将伪数组(或者可遍历对象)转换为真正的数组。语法格式如下:
1 | let arr2 = Array.from(arrayLike); |
字符串的扩展
模板字符串
ES6 引入新的声明字符串的方式 『``』
之前 ''
""
1 | //1. 声明 |
其他扩展
ES6 中的字符串扩展如下:
includes(str)
:判断是否包含指定的字符串startsWith(str)
:判断是否以指定字符串开头endsWith(str)
:判断是否以指定字符串结尾repeat(count)
:重复指定次数trimStart()
:清除左侧空白trimEnd()
:清除右侧空白
举例如下:
1 | let str = 'abcdefg'; |
String.prototype.matchAll
1 | let str = `<ul> |
Number的扩展
- 二进制与八进制数值表示法: 二进制用
0b/0B
, 八进制用0o/0O
。 - 指数运算符:
ES2016
新增的 ,指数运算符(**
)。 Number.EPSILON
:数值最小精度Number.MIN_SAFE_INTEGER
:最小安全数值(-2^53
)Number.MAX_SAFE_INTEGER
:最大安全数值(2^53
)Number.parseInt()
:返回转换值的整数部分Number.parseFloat()
:返回转换值的浮点数部分Number.isFinite()
:是否为有限数值Number.isNaN()
:是否为NaNNumber.isInteger()
:是否为整数Number.isSafeInteger()
:是否在数值安全范围内Math.trunc()
:返回数值整数部分Math.sign()
:返回数值类型(正数1
、负数-1
、零0
)Math.cbrt()
:返回数值立方根Math.clz32()
:返回数值的32位无符号整数形式Math.imul()
:返回两个数值相乘Math.fround()
:返回数值的32位单精度浮点数形式Math.hypot()
:返回所有数值平方和的平方根Math.expm1(n)
:返回`e^n - 1``- ``Math.log1p(n)
:返回
1 + n的自然对数(
Math.log(1 + n)`) Math.log10(n)
:返回以10为底的n的对数Math.log2(n)
:返回以2为底的n的对数Math.sinh(n)
:返回n的双曲正弦Math.cosh(n)
:返回n的双曲余弦Math.tanh(n)
:返回n的双曲正切Math.asinh(n)
:返回n的反双曲正弦Math.acosh(n)
:返回n的反双曲余弦Math.atanh(n)
:返回n的反双曲正切
1 | //0. Number.EPSILON 是 JavaScript 表示的最小精度 |
Symbol
概述
背景:ES5中对象的属性名都是字符串,容易造成重名,污染环境。
概念:ES6 引入了一种新的原始数据类型Symbol
,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined
、null
、布尔值(Boolean)
、字符串(String)
、数值(Number)
、对象(Object)
。
特点:
Symbol属性对应的值是唯一的,解决命名冲突问题
Symbol值不能与其他数据进行计算,包括同字符串拼串
for in、for of 遍历时不会遍历Symbol属性。
创建Symbol属性值
Symbol是函数,但并不是构造函数。创建一个Symbol数据类型:
1 | let mySymbol = Symbol(); |
下面来讲一下Symbol的使用。
创建Symbol属性值时,传参作为标识
如果我通过 Symbol()函数创建了两个值,这两个值是不一样的:
1 | let mySymbol1 = Symbol(); |
上面代码中,倒数第三行的打印结果也就表明了,二者的值确实是不相等的。
最后两行的打印结果却发现,二者的打印输出,肉眼看到的却相同。那该怎么区分它们呢?
既然Symbol()是函数,函数就可以传入参数,我们可以通过参数的不同来作为标识。比如:
1 | //在括号里加入参数,来标识不同的Symbol |
将Symbol作为对象的属性值
1 | let mySymbol = Symbol(); |
上面的代码中,我们尝试给obj添加一个Symbol类型的属性值,但是添加的时候,不能采用.
这个符号,而是应该用属性选择器
的方式。
现在我们用for in尝试对上面的obj进行遍历:
1 | let mySymbol = Symbol(); |
从打印结果中可以看到:for in、for of 遍历时不会遍历Symbol属性。
1 | //向对象中添加方法 up down |
定义常量
Symbol 可以用来定义常量:
1 | const MY_NAME = Symbol('my_name'); |
内置的 Symbol 值
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
1 | class Person{ |
迭代器
迭代器(Itertor)是一种接口,为各种不同的数据结构提供统一的访问机制。
任何数据结构只要部署了Itertor接口(其实就是对象中的一个属性),就可以完成遍历操作。
ES6创造了一种新的遍历命令
for...of
循环,Itertor接口主要供for...of
消费原生具备Itertor接口的数据
- Array
- Arguments
- Set
- Map
- String
- TypedArray
- NodeList
工作原理
- 创建一个指针对象,指向当前数据结构的起始位置
- 第一次调用对象的next方法,指针自动指向数据结构的第一个成员
- 接下来不断调用next方法,指针一直往后移动,直到指向最后一个成员
- 每调用next方法返回一个包含value和done属性的对象
注;需要自定义遍历数据的时候,要想到迭代器
1 | //声明一个数组 |
自定义遍历数据
1 | //声明一个对象 |
生成器
生成器其实就是一个特殊的函数 异步编程
异步编程 纯回调函数 node fs ajax mongodb
1 | //函数代码的分隔符 yield |
生成器函数的参数
1 | function * gen(arg){ |
生成器函数实例
1 | // 异步编程 文件操作 网络操作(ajax, request) 数据库操作 |
1 | //模拟获取 用户数据 订单数据 商品数据 |
数组的扩展
- Array.from()
- find()
- findIndex()
1 | //flat 平 |
对象的扩展
简化对象写法
1 | //ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。 |
class类
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。
通过 class 关键字,可以定义类。基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到。
新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
知识点:
- class 声明类;
- constructor 定义构造函数初始化;
- extends 继承父类;
- super 调用父级构造方法;
- static 定义静态方法和属性;
- 父类方法可以重写;
1 | //手机 |
静态成员
属于类,但不属于实例对象
1 | // function Phone(){ |
继承
对象继承
1 | //手机 |
类继承
1 | class Phone{ |
get 和 set
1 | class Phone{ |
私有属性
1 | class Person{ |
对象方法的扩展
ES6
1 | //1. Object.is 判断两个值是否完全相等 |
ES8
1 | //声明对象 |
ES10
1 | //二维数组 |
可选链操作符
1 | // ?. |
模块化
模块化是指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来。
模块化的好处:
- 防止命名冲突;
- 代码复用;
- 高维护性;
模块化规范产品:
ES6 之前的模块化规范有:
CommonJS => NodeJS、Browserify;
AMD => requireJS;
CMD => seaJS;
ES6 模块化语法:
模块功能主要由两个命令构成:export 和 import;
export
命令用于规定模块的对外接口(导出模块);
import
命令用于输入其他模块提供的功能(导入模块);
暴露
分别暴露
1 | export let school = '尚硅谷'; |
统一暴露
1 | let school = '尚硅谷'; |
默认暴露
1 | export default { |
引入
通用的导入方式
1 | //引入 m1.js 模块内容 |
解构赋值形式
1 | import {school, teach} from "./src/js/m1.js"; |
简便形式 针对默认暴露
1 | import m3 from "./src/js/m3.js"; |
入口文件
app.js
1 | //模块引入 |
index,html
1 | <script src="./src/js/app.js" type="module"></script> |
引入NPM包
1 | //修改背景颜色为粉色 |
动态import
1 | // import * as m1 from "./hello.js"; |
数据结构
Set
ES6 提供了新的数据结构 Set(集合)。它类似于数组,但成员的值都是唯一的,集合实现了 iterator接口,所以可以使用扩展运算符
和for…of…
进行遍历,集合的属性和方法:
- size 返回集合的元素个数;
- add 增加一个新元素,返回当前集合;
- delete 删除元素,返回 boolean 值;
- has 检测集合中是否包含某个元素,返回 boolean 值;
- clear 清空集合,返回 undefined;
1 | //声明一个 set |
Set实践
1 | //1. 数组去重 |
Map
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合。
但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
Map 也实现了iterator 接口,所以可以使用扩展运算符
和for…of…
进行遍历;
Map 的属性和方法:
- size 返回 Map 的元素个数;
- set 增加一个新元素,返回当前 Map;
- get 返回键名对象的键值;
- has 检测 Map 中是否包含某个元素,返回 boolean 值;
- clear 清空集合,返回 undefined;
1 | //声明 Map |
正则扩展
命名捕获分组
ES9 允许命名捕获组使用符号?
,这样获取捕获结果可读性更强;
1 | //声明一个字符串 |
反向断言
ES9 支持反向断言,通过对匹配结果前面的内容进行判断,对匹配进行筛选;
1 | //声明字符串 |
dotAll 模式
正则表达式中点.匹配除回车外的任何单字符,标记s
改变这种行为,允许行终止符出现;
1 | //dot . 元字符 除换行符以外的任意单个字符 |
BigInt
BigInt
数据类型的目的是比Number
数据类型支持的范围更大的整数值。在对大整数执行数学运算时,以任意精度表示整数的能力尤为重要。使用BigInt
,整数溢出将不再是问题。
1 | //大整形 |
globalThis
全局属性 globalThis
包含全局的 this
值,类似于全局对象(global object)。