es 简介
es 全称EcmaScript,是脚本语言的规范,JavaScript就是EcmaScript的一种实现,所以 es 新特性就是JavaScript的新特性
let 声明
使用let申明变量具备以下特性
- 不能重复申明,如果同一作用域存在同名的变量,则控制台会出现错误提示:
Uncaught SyntaxError: Identifier 'xxx' has already been declared,代表语法错误,变量已被申明 - 块级作用域,
let申明的变量只在{}代码块中生效,如果尝试在代码块以外使用代码块中被let申明的变量,会出现错误提示:ReferenceError: xxx is not defined,代表该变量未定义 - 不存在变量提升,使用
var申明的变量会被提示到当前作用域的最前面且不会被赋值,而let申明的变量不存在变量提升;如果在let申明变量之前去访问,则控制台会出现错误提示:Uncaught ReferenceError: Cannot access 'xxx' before initialization,代表xxx变量在初始化前被访问了 - 不影响作用域链(当前作用域中没有,则向上查找)
let 案例
有如下代码
<style>
div {
width: 100px;
height: 100px;
border: 1px solid black;
}
</style>
<body>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<script>
let divArr = document.querySelectorAll('div')
for (var i = 0; i < divArr.length; i++) {
divArr[i].onclick = function () {
divArr[i].style.background = 'pink'
}
}
</script>
</body>
当我们点击页面元素时,按照正常逻辑,对应的
div元素的背景会被修改,但实际控制台却打印错误:Cannot read properties of undefined (reading 'style'),这说明当前从divArr数组中拿到的元素是一个undefined,那么原因很明显,divArr[i]中的i超过了divArr中的有效索引,这正是因为使用var来定义i导致的使用
var申明的变量没有块级作用域的限制,当上述 for 循环执行完毕后,i的值已经为3了,后续点击事件触发时,依然使用的是i在for循环这个块级作用域中的变量值,索引发生越界如果将上述的
var变量申明的i改为let申明就不会出现索引越界的问题,因为let申明的变量有块级作用域的限制,在回调函数内部使用i实际上可以看作以下形式let i = 0 // 之后的循环依次类推:let i = 1 , let i = 2 divArr[i].onclick = function () { console.log(divArr[i]) divArr[i].style.background = 'pink' }因为
let块级作用域的存在,meigei都只会在当前遍历的{}代码块内生效,下一次遍历的i不会影响其他i变量,这就不会出现索引越界的问题为了更规范的书写代码和代码结构的严谨,实际开发中应该使用
let来申明变量
const 申明
es6 中使用const来定义常量,常量的值不能被修改且常量在申明时必须被赋值,const与let一样,也具备块级作用域
对常量修饰的数组 对象中的属性的修改,不算做对常量的修改
解构
数组的解构
const arr = ['西游记', '三国演义', '水浒传', '红楼梦']
/*
数组解构,let 申明的变量集合中,各个位置上的变量会被数组对应索引的元素赋值
*/
let [xiyou, sanguo, shuihu, honglou] = arr;
console.log(xiyou, sanguo, shuihu, honglou)
对象的解构
const sun_wu_kong = {
name: '孙悟空',
age: 1000,
skill: () => {
console.log('一个筋斗!十万八千里☁~')
}
}
/*
对象解构,let 申明的变量名,对应了 sun_wu_kong 对象中的属性名(必须一一对应,否则调用时会出现 undefined)
*/
let {name, age, skill} = sun_wu_kong
let obj = {name, age, skill} = sun_wu_kong // 也可以再次赋值为一个对象
console.log(name)
console.log(age)
skill()
模板字符串
模板字符串是一种新的字符串申明方式,使用 `` 进行申明,模板字符串中可以直接出现换行符以及变量拼接,代码如下
const name = '孙悟空'
const templateStr = `${name}`
console.log(templateStr)
箭头函数以及申明特点
es6 中允许使用=>来申明一个函数,如下
let fun = () => {
}
使用=>申明的函数,函数中调用的this 始终指向函数申明时所在作用域下的 this,即当前=>所在的{}块级作用域中,如下代码
window.username = 'aaa'
let fn1 = () => {
// => 申明的函数始终指向当前所在作用域的 this,也就是 window
console.log(this.username)
}
let fn2 = function () {
console.log(this.username)
}
fn1()
fn2()
如果修改两个函数的this引用,如下代码
window.username = 'aaa'
const refObj = {
username: 'bbb'
}
let fn1 = () => {
// => 申明的函数始终指向当前所在作用域的 this,也就是 window
console.log(this.username)
}
let fn2 = function () {
console.log(this.username)
}
// call() 修改函数的 this 指向并调用
fn1.call(refObj) // 打印 aaa
fn2.call(refObj) // 打印 bbb
可以看到,使用=>定义的函数其this引用不会被修改
但是如果直接修改=>所在当前作用域内的this,那么=>的this引用就可以被修改,如下代码
<script>
// 当前 fun 函数指向的 this 为 window
let fun = function () {
// fun2 在 fun 函数的 {} 块级作用域内 , 因此 fun2 的 this 此时也是 window
let fun2 = () => {
// 当前 window 中没有被赋值 username 属性 , 直接打印会出现 undefined
console.log(this.username)
}
fun2()
}
// 此时 fun 的 this 引用被修改为了 {username:'aaa'} , 那么上面 fun2 的打印结果会变为 aaa
fun.call({username: 'aaa'})
</script>
=>不能作为构造函数来实例化对象,如下写法会打印错误提示Uncaught TypeError: xxx is not a constructor
let Person = (username, age) => {
this.username = username
this.age = age
}
let per = new Person('aa', 10)
console.log(per)
=>申明的函数不能使用arguments变量,如下代码,当fn2调用时,控制台会打印错误信息ReferenceError: arguments is not defined
let fn1 = function fn(a, b) {
// 使用 funcation 申明的原生函数可以使用 arguments 来获取到当前实参
console.log(arguments)
}
let fn2 = (a, b) => {
console.log(arguments)
}
fn1(1, 2)
fn2(1, 2)
箭头函数适合用于this无关的回调,如定时器,数组方法的回调等;不适合与this相关的回调,包括dom事件源对象的回调,因为dom事件源对象回调中的this指向的是事件源对象本身,可以直接使用this来操作事件源对象,使用=>申明的函数则不然,如下代码
let divEle = document.getElementById('box')
divEle.addEventListener('click', () => {
/*
在该事件函数中,this 指向的是当前函数申明时所处作用域下的 this,也就是 window 对象
使用该 this 调用 style 属性会报错
*/
this.style.background = 'pink'
})
函数形参的默认值设置
es6 中允许为函数的参数赋初始值,如下代码
// 在申明函数时,可以指定形参的默认值
function connect(a, b, c = 10) {
return a + b + c
}
// 后续调用时,没有传入的形参会使用默认值替代
console.log(connect(1, 2))
这一特性也可以与解构相结合,如下代码
// 参数可以传入一个对象,会自动将对象中的属性解构给参数的形参
function connect({host = '127.0.0.1', port = 3306, username, password}) {
console.log(host, port, username, password)
}
connect({localhost: 'localhost', port: 3306, username: 'root', password: 'admin'})
rest 参数
es6 中引入了rest参数用于获取函数的实参,以代替arguments,如下代码
let fn = (...args) => {
console.log(args)
}
fn(1, 2, 3)
在函数形参的最后使用
...xxx的形式,可以将所有参数整合到xxx中,注意:如果还有其他的非rest参数,则...xxx必须放在最后,例如(a,b,...xxx)
/*
es9 中,支持对象的 ...rest 参数,可以将多个参数配合解构运算符 {} 整合到一个对象中
*/
function connect({host, port, ...user}) {
console.log(host)
console.log(port)
console.log(user)
}
connect({host: 'localhost', port: 3308, username: 'zhang san', password: '111111'})
扩展运行算符
基本使用
扩展运算符能够将数组转换为,分隔的参数序列,如下
// 使用 rest 特性接收多个参数
function fn(...params) {
console.log(params)
}
// 使用扩展运算符转换数组为参数序列
fn(...[1,2,3])
需要注意区分
rest和扩展运算符的区别,rest只能在参数定义时使用的,而扩展运算符可以在函数调用时使用
实际应用
数组合并,如下
let arr1 = [1, 2, 3]
let arr2 = [4, 5, 6]
// 使用扩展运算符可以将快捷的将多个数组进行合并或者是克隆
let arr3 = [...arr1, ...arr2]
console.log(arr3)
将伪数组转为真正的数组,如下代码
// querySelectorAll 默认返回的是一个伪数组,使用 ... 可以直接进行转换
let divArr = [...document.querySelectorAll('div')]
console.log(divArr)
对象的扩展运算符
es9 中新增支持了对象的扩展运算符
let phoneBaseInfo = {cpu: '高通骁龙888', brand: '摩托罗拉'}
let phoneDetailInfo = {color: '#ffffff', size: 6.1}
/*
使用对象的扩展运算符,能够将对象中的属性拆分为键值的形式,可以用于如下形式的
快速合并对象
*/
let phoneInfo = {...phoneBaseInfo, ...phoneDetailInfo}
console.log(phoneInfo)
更多用法可以参考博客:ES6对象的扩展运算符_大斧滴熊的博客-CSDN博客_es6对象扩展运算符
Symbol
es6 引入了一种新的原始数据类型Symbol,表示独一无二的值;它是JavaScript语言的第七种数据类型,是一种 类似 于字符串的数据类型
Symbol的值是唯一的,用来解决命名冲突的问题Symbol值不能与其他数据进行运算Symbol定义的对象属性不能使用for-in循环遍 历,但是可以使用Reflect.ownKeys来获取对象的所有键名
创建Symbol,如下代码
// 创建 Symbol 方式一
let s1 = Symbol()
// 创建 Symbol 方式二
let s2 = Symbol('aaa')
let s3 = Symbol('aaa')
console.log(s2 === s3) // false
// 创建 Symbol 方式三
let s4 = Symbol.for('a')
let s5 = Symbol.for('a')
console.log(s4 === s5) // true
注意:Symbol不能与其他数据进行运算,包括Symbol类型本身也不行
补充:js 中的数据类型有undefined string Symbol object null number boolean
Symbol 使用
根据Symbol的特性,有如下应用方式
-
作为对象的属性名,如下代码
const KEY = Symbol() // 申明一个 Symbol 作为对象的属性名 let o = { username: 'aa', age: 18, // 定义属性时,传入 KEY,[] 会计算出对应的值来作为对象的属性名,这能防止对象中属性重复的情况出现 [KEY]: 111 } console.log(o[KEY]) // 调用同时,使用
Symbol定义的属性名,在遍历该对象的属性时,不会被包含在遍历出的属性名集合中,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义;就算使用
JSON.stringify()将对象转换成JSON字符串的时候,Symbol属性也会被排除在输出内容之外如果需要遍历出对象的属性,需要通过
Reflect.ownKeys(obj)或Object.getOwnPropertySymbols(obj) -
使用
Symbol来替代常量,首先是一般的常量申明,如下代码const TYPE_ONE = 'ONE' const TYPE_TWO = 'TWO' const TYPE_THREE = 'THREE'每次申明一个常量,都需要给定一个不同的值
如果使用
Symbol则方便很多,如下代码const TYPE_ONE = Symbol() const TYPE_TWO = Symbol() const TYPE_THREE = Symbol() -
注册和获取全局
Symbol,如下代码let g1 = Symbol.for('global_symbol') //注册一个全局 Symbol let g2 = Symbol.for('global_symbol') //获取全局 Symbol console.log(g1 === g3) // trueSymbol.for()使用给定的key搜索现有的Symbol,如果找到则返回该Symbol,否则将使用给定的key在全局Symbol注册表中创建一个新的Symbol
参考博客:javascript中symbol类型的应用场景(意义)和使用方法 - 前端开发博客 (nblogs.com)
在 es10 中,还可以直接获取Symbol创建时传入的描述符,如下代码
let sym = Symbol('zhang san') // 描述符为 zhang san
console.log(sym.description);
迭代器
遍历器(Iterator)就是一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作,Iterator实际上就是一个属性,在Array中,使用Symbol(Symbol.iterator)表示,可以使用arr[Symbol.iterator]()来得某个数组对象的迭代器
同时也因为 es6 创造了一种新的遍历命令for-of循环,Iterator接口主要供for-of消费;原生具备iterator接口的数据类型有Array Arguments Set Map String TypedArray TypedArray,而for-of与for-in的区别在于,for-of可以遍历出集合的value,而for-in则是将集合的key给遍历出来(对于数组来说,则是遍历出其对应的index)
自定义迭代器
一般的自定义对象是不支持迭代器遍历其中的属性的,需要我们自定义迭代器来实现(这也是迭代器的实现原理),如下代码
let classRoom = {
className: '19-2 class',
stus: [
'zhang san',
'li si',
'wang wu',
'zhao liu'
],
// 申明一个迭代器 api,[Symbol.iterator] 这里相当于是作为方法名来使用
[Symbol.iterator]() {
let index = 0
// return 一个指针对象,指向当前数据结构的起始位置
return {
// 指针对象中提供一个 next 方法,第一次调用对象的 next 方法,指针自动指向数据结构的第一个成员,后续依次递增
next: () => {
if (index < this.stus.length) {
// 返回的结果中,value 代表当前迭代出的值,done 代表迭代是否结束
const res = {value: this.stus[index], done: false}
index++
return res
} else {
return {value: this.stus[index - 1], done: true}
}
}
}
}
}
for (let classRoomElement of classRoom) {
console.log(classRoomElement)
}
也可以结合Object.keys()使用for-of来单独遍历出对象中所有属性的值,如下
let classRoom = {
username: '19-2班',
stus: [
'zhang san',
'li si',
'wang wu',
'zhao liu'
],
[Symbol.iterator]: function () {
let index = 0
let keys = Object.keys(this) // 拿到当前对象中所有属性的 key
return {
next: () => {
if (index < keys.length) {
const res = {value: this[keys[index]], done: false}
index++
return res
} else {
return {value: this[keys[index - 1]], done: true}
}
}
}
}
}
for (let classRoomElement of classRoom) {
console.log(classRoomElement)
}
生成器
生成器实际上就是一个特殊的函数,可以用作异步编程,如下代码
/*
使用 function* xxx(){} 的形式申明一个生成器,使用函数的返回值来调用生成器中的被分割的代码段
生成器的调用和迭代器类似,生成器中使用 yield 来将代码的执行进行分割,每调用一次 next,则按照
分隔符的顺序,某个分隔符之前的代码会被执行,当执行到 yield 时,yield 的表达式会作为 next 的
返回值返回,当前分隔符执行完毕;下一次调用 next 时继续执行下一个分隔符之前的代码
这种形式将一个函数中的代码,分成多段,每次执行一段
*/
let fun = function* fun() {
console.log('step1')
yield 1 + 1
console.log('step2')
yield 2 + 2
console.log('step3')
yield 3 + 3
}
// 拿到生成器函数返回的迭代对象
let iterator = fun()
/*
使用迭代对象执行生成器中的若干个代码段
每次 next 的返回值形式为 {value:xxx, done:false}
value 为当次执行的 yield 的表达式值,而 done 代表生成器中的代码段是否迭代完毕
*/
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
生成器函数的参数传递
let fun = function* (args) {
console.log(args) // 生成器函数也可以正常传入参数
let one = yield
console.log(one)
let two = yield
console.log(two)
let three = yield
console.log(three)
}
let iter = fun('paramValue')
console.log(iter.next());
/*
next 方法也能够传入参数,且除了第一个 next 之外,下一个 next 的参数
会被作为当前 netx 的上一个 next 对应的 yield 的返回值
例如:
当前 next 为第二次调用,则该 next() 中的参数 BBB 会被作为第一个 next 对应的 yield 的返回值
则 one 的值为 BBB
*/
console.log(iter.next('BBB'));
console.log(iter.next('CCC'));
生成器函数应用一
在控制台间隔1秒打印111,间隔2秒,打印222,间隔3秒,打印333,使用生成器形式实现如下
// 单独申明任务函数
function one() {
setTimeout(() => {
console.log(111)
iter.next()
}, 1000)
}
function two() {
setTimeout(() => {
console.log(222)
iter.next()
}, 2000)
}
function three() {
setTimeout(() => {
console.log(333)
}, 3000)
}
// 在生成器中,分步调用不同的任务函数,达到回调的目的
let fun = function* () {
yield one()
yield two()
yield three()
}
let iter = fun()
iter.next() // 触发第一个函数的调用,后续函数的调用会在当前函数完成后,通过 netx 自动触发
生成器的形式避免了回调地狱的出现,如果使用一般的嵌套方式完成上述效果,会出现以下代码
setTimeout(() => {
console.log(111)
setTimeout(() => {
console.log(222)
setTimeout(() => {
console.log(333)
}, 3000)
}, 2000)
}, 1000)
上述写法一旦回调次数过多,代码的嵌套层级会影响代码的阅读且难以维护
Promise
Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值,语法上来说,Promise是一个构造函数,用于封装异步操作并获取其成功或失败的结果,代码如下
/*
实例化一个 Promise 对象,构造函数中传入一个处理函数
而处理函数的参数中分别为 resolve reject,这也是两个函数变量
*/
const p = new Promise((resolve, reject) => {
setTimeout(() => {
let data = {userId: 10001, userName: 'zhang san'}
// resolve(data) // resolve 的调用代表此次回调成功
reject({code: 500, msg: '系统错误!'}) // reject 调用代表此次回调失败
}, 1000)
}).then( // 如果 then 方法被调用,说明前一个回调执行完成
(value) => { // 如果调用了第一个回调函数参数,那么说明前一个回调执行成功
console.log(value)
},
(reason) => { // 否则会调用第二个回调函数参数,说明前一个回调执行失败
console.error(reason)
})
一个Promise必然会处于以下三种状态
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled):意味着操作成功完成。
- 已拒绝(rejected):意味着操作失败。
Promise 读取文件
const fs = require('fs')
new Promise((resolve, reject) => {
fs.readFile('../resource/hello.txt', (err, res) => {
err ? reject(err) : resolve(res)
})
}).then((res) => {
console.log(res.toString())
}, (reason) => {
console.error(reason)
})
Promise.prototype.then()
Promise 中的then方法能够继续返回一个Promise实例,通过该实例可以实现链式调用,如下代码
new Promise((resolve, reject) => {
resolve({userId: 10001, userName: 'zhang san'})
}).then(
res => {
/*
1、then 方法可以选择返回 new Promise() 对象,返回的 Promise 的状态,可以决定下一次链式调用的 then 方法是执行 res 参数的回调还是 err 参数的回调
2、如果直接返回一个非 Promise 类型的数据,则默认 Promise 的状态为成功,下一次 then,方法中,将会执行 res 参数的回调
3、如果直接抛出异常,则认为 Promise 的状态为拒绝,下一次 then 方法中,将会再执行 err 的回调
*/
return res
}, err => {
throw err
}
).then(
res => {
console.log(res)
},
err => {
console.error(err)
})
Catch
当Promise状态为拒绝时,在then中需要指定两个不同参数的回调,参数二的回调用于接收拒绝状态的,而使用catche可以直接接收拒绝状态的回调,如下
new Promise((resolve, reject) => {
reject({errMsg:'err respsonse'})
}).catch(err => {
console.error(err)
})
集合
Set 集合
let set = new Set() // 创建一个 set 集合,不允许存储重复元素
let set2 = new Set([1, 2, 3, 4, 5, 6, 6]) // 也可以直接传入一个可迭代的集合
set2.add(10) // 添加元素
set2.has(10) // 判断是否有该元素,返回布尔类型
set2.size // 获取集合的大小
set2.clear() // 清空集合
// 使用 for-of 迭代集合
for (let number of set2) {
console.log(number)
}
let arr = [1, 1, 1, 1, 1, 2, 23, 43, 435, 6, 667, 7, 8, 85, 3, 4, 5, 5]
// 使用 set 集合快速对数组去重
let deduplicationArr = [...new Set(arr)]
console.log(deduplicationArr)
// 获取两个集合的交集
let beMixedArr1 = [1, 2, 5, 7, 8]
let beMixedArr2 = [2, 3, 1, 6, 9]
let beMixedResArr = [...new Set(beMixedArr1)].filter(item => {
let set2 = new Set(beMixedArr2) // 两个集合中都存在的元素,为交集元素
return set2.has(item)
})
console.log(beMixedResArr)
// 求两个集合的差集
let diffArr1 = [1, 2, 5, 7, 9]
let diffArr2 = [10, 1, 2, 5, 9]
let diffResArr = [...new Set(diffArr1)].filter(item => {
/*
对于 diffArr1 数组中的元素,在 diffArr2 不存在,则该元素为 diffArr1 相较于 diffArr2 的差集元素
反之也成立
*/
return !new Set(diffArr2).has(item)
})
console.log(diffResArr)
Map 集合
let map = new Map() // 创建 Map
map.set('key', {username: 'zhang san'}) // 往 Map 中存放 key - value 键值对
map.size // 获取 Map 中的元素个数
map.get('key') // 获取指定 key 对应的元素
map.clear() // 清空 Map 中的所有元素
map.has('key') // 判断 Map 中是否有指定的 key
// Map 支持 for-of 迭代遍历
for (let mapElement of map) {
console.log(mapElement)
}
Class 类
es6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类
es6 的class可以看作只是一个语法糖,它的绝大部分功能,es5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已,代码如下
// 使用 class 定义一个类
class Phone {
// 使用 constructor 定义类的构造方法,该方法会在使用 new 关键字时,自动执行
constructor(phoneName, phonePrice) {
this.phoneName = phoneName
this.phonePrice = phonePrice
}
// 使用如下语法声明类中的方法
call(phoneName) {
console.log(`${phoneName}-打电话`)
}
}
let phone = new Phone('iphone13', 7999)
console.log(phone)
phone.call('魅族19')
class 中的静态属性
在 es5 中,使用如下方式来声明静态成员
function Phone() {
}
Phone.phoneName = '手机'
Phone.change = (name) => {
console.log(`${name} 改变世界!'`)
}
console.log(Phone.phoneName);
Phone.change('nokia')
声明的静态成员是属于类的而不属于对象,只能通过
Phone.xxx的形式来获取或调用类成员,使用new关键字创建出的对象不能调用到静态成员
在 es6 中,申明静态成员有新的语法,如下
class Phone {
static phoneName = 'nokia'
static change = (name) => {
console.log(`${name} 改变世界!`)
}
}
Phone.change(Phone.phoneName)
继承
在 es6 中,类之间的继承使用如下写法
class Phone {
constructor(phoneName, phonePrice) {
this.phoneName = phoneName
this.phonePrice = phonePrice
}
call() {
console.log('call up')
}
}
// 使用 extends 关键字继承 Phone
class SmartPhone extends Phone {
constructor(phoneName, phonePrice, color, size) {
super(phoneName, phonePrice); // 构造函数中需要先调用父类的构造函数,使用 super 关键字进行调用
this.color = color
this.size = size
}
playGame() {
console.log('play game')
}
}
// 创建出的子类对象,除了具备父类中的属性和方法外,还可以定义自己的属性和方法
let smartPhone = new SmartPhone('nokia', 1999, 'white', 6.1)
smartPhone.call()
smartPhone.playGame()
子类重写父类方法
子类如果想要重写父类中的方法,直接在子类中声明一个和父类同名的方法即可
getter-setter
es6 还可以在类中设置get set关键字申明的方法,此类方法会绑定类中的属性,当需要对类中的属性进行一个动态的修改、获取是,通过这两种方法可以方便的实现一些其他的逻辑,如下代码,将手机的折扣力度和折扣后价格的获取分别设置为set get类型,方便调用,简化了代码
class Phone {
constructor(phoneName, phonePrice) {
this.phoneName = phoneName
this.phonePrice = phonePrice
this.discountVal = 10
}
// 设置价格的折扣
set discount(dis) {
this.discountVal = dis
}
// 获取折扣后的价格
get discountPrice() {
return this.phonePrice * (this.discountVal / 10)
}
}
let nokia = new Phone('nokia', 1999)
nokia.discount = 8 // 设置折扣
console.log(nokia.discountPrice); // 获取折扣
数值扩展
es6 中对数值类型也做了一些扩展,代码演示如下
function equl(n1, n2) {
/*
EPSILON 是 JavaScript 中两个可表示 (representable) 数之间的最小间隔。
也就是说 EPSILON 是 JavaScript 中能够表示的数值的最小单位,利用该数值
可以进行一些最小精度单位的比较或计算
如下,如果两个数值之间的差的绝对值小于 EPSILON,那么就认为这两个数是相等的
*/
return Math.abs(n1 - n2) < Number.EPSILON
}
console.log(equl(1, 1.000000000000000000000000000000000000000011)) // true
console.log(equl(1, 1.001)) // false
// 二进制、八进制、十六进制表示
let binaryNum = 0b111 // 二进制以 0b 开头进行标识
let octalNum = 0o7777 // 八进制以 0o 开头进行标识
let hexNum = 0xfff // 十六进制以 0x 开头进行标识
console.log(binaryNum, octalNum, hexNum)
// 检测一个数是否为有限数
console.log(Number.isFinite(100)); // 有限数
console.log(Number.isFinite(100 / 0)); // 100 / 0 等于 Infinity,是一个无限数
// 检测数值是否为 isNaN
console.log(isNaN(NaN)) // true
console.log(isNaN(111)) // true
// 将字符串转为整数或浮点数
console.log(Number.parseInt('1131.6')); // 转换为整数时,会截断小数位
console.log(Number.parseFloat('1131.6')); // 转换为浮点数时,保留小数位
// 判断一个数是否为整数
console.log(Number.isFinite(5.2)) // false
// 去除数字的小数部分
console.log(Math.trunc(1 / 3)) // 0
// 判断一个数是正数、负数还是 0
console.log(Math.sign(0)) // 正数返回 1,负数返回 -1,0 返回 0
对象方法扩展
/*
判断两个值是否相等
*/
console.log(Object.is(1, 1));
/*
对象的合并或覆盖
1、第一个参数为给定的模板对象
2、后续的任意个参数都是需要被合并的对象
3、如果合并的对象中有模板对象不存在的属性,那么会直接合并到模板对象,或则是模板对象中的属性在合并对象中不存在,那么该属性会保持不变
4、如果合并对象中的属性与模板对象属性重合,那么以合并对象为准
5、多个合并对象中有重合的属性,则最终的属性值以最后一个合并对象为准
*/
Object.assign({host: 'localhost', port: 3306}, {username: 'root', password: '111111'})
/*
设置、获取原型对象
*/
const smartPhone = {
phoneName: 'nokia'
}
const phone = {
phone: '手机'
}
Object.setPrototypeOf(smartPhone, phone) // 给 smartPhone 设置原型对象
console.log(Object.getPrototypeOf(smartPhone)); // 获取 smartPhone 的原型对象
/*
es8 对象扩展方法
*/
Object.keys(user) // 获取对象中所有属性的名称
Object.values(user) // 获取对象中所有属性的值
let userEntrys = Object.entries(user) // 获取对象中所有属性的键值构成的元素数组
/*
Object.entries 获取到的 entrys 可以直接用于创建一个 Map
*/
let userMap = new Map(userEntrys)
console.log(userMap.get('username'));
// 返回对象属性的描述对象
Object.getOwnPropertyDescriptors(user)
/*
es10 对象扩展方法
*/
const map = new Map()
map.set('username', 'zhang san')
map.set('age', 18)
// Object.fromEntries 将一个 entry 集合转换为对象,entry 的 k-v 将自动映射为对象的属性和属性值
let resObj = Object.fromEntries(map)
// Object.fromEntries 和 Object.entries 的返回结果可以相互转化
let entrys = Object.entries(resObj)
console.log(resObj)
console.log(entrys)
模块化
模块化能够将一个大的程序,拆分为若干小的模块并且这些小的模块可以组合起来,模块化能够防止命名冲突,提高代码复用以及便于维护,简单使用如下
/*
模块化主要由 export 和 import 构成
export 用于规定模块的对外接口
import 用于输入其他模块提供的功能
*/
export let username = 'zhang san' // 导出一个变量
export function hello() { // 导出一个函数
console.log('hello')
}
<body>
<script type="module">
import * as model from './module.js' // 导入模块
model.hello()
console.log(model.username);
</script>
</body>
也可以使用统一暴露来简化写法
let username = 'zhang san'
function hello() {
console.log('hello')
}
// 统一暴露
export {
username, hello
}
还有一种暴露方式为默认暴露,如下代码
export default {
username: 'zhang san',
hello() {
console.log('hello')
}
}
<script type="module">
import * as model from './module.js'
model.default.hello() // 使用时需要加上 default 来调用
console.log(model.default.username);
</script>
如果是分别导出的模块,使用解构赋值导入的形式如下
export let username = 'zhang san'
export function hello() {
console.log('hello')
}
import {hello, username} from './module.js' // 对应的解构导入形式
统一导出的模块,使用解构导入的形式与上述相同
如果是默认导出的形式,使用解构赋值导入的形式如下
export default {
username: 'zhang san',
hello() {
console.log('hello')
}
}
import {default as m} from './module.js' // 对应的解构导入形式
还有一种简便形式的导入,如下
import m from './module.js'
这种形式的导入只能针对默认暴露,如果对其他暴露形式使用以上简便导入,会打印错误信息
The requested module 'xxxx' does not provide an export named 'default',代表导入的模块中,没有提供default暴露
如果有很多的模块需要引入,那么单个script标签中的import语句将会大量的重复出现,可以使用如下写法进行优化
-
使用一个统一的
js文件来导入模块,如app.jsimport M1 from "./m1.js"; // 在当前 js 文件中统一导入其他模块 import M2 from "./m2.js"; import M3 from "./m3.js"; export default { ...M1, ...M2, ...M3 // 统一导出模块中的每个属性 }也可以不导出,直接在
app.js中使用导入的模块进行开发,后续在页面中只引入app.js而不需要额外编写js代码 -
使用时,只需要引入
app.js即可import app from "./app.js"; console.log(app.username); console.log(app.compute(1, 3)); app.hello() app.greet()
babel 转换 es6 模块化代码
在实际开发中,可能有的浏览器不支持上述模块化的一系列新特性,此时就需要使用babel来转换模块化的js代码,在使用babel转换代码之前,需要先按照一些工具:babel-cli(babel命令行工具) babel-perset-env(用于转换 es 新特性的依赖包) browserify(打包工具,实际项目中通常使用 webpack)
使用npm按照上述依赖
npm i browserify
npm i babel-cli
npm i babel-preset-env
然后在对应的需要转换的js文件目录下,命令行执行如下指令
# ./src 代表需要转换的目录; ./dist/js 代表转换后的js文件的输出目录
npx babel ./src -d ./dist/js --presets=babel-preset-env
转换后的js也不能直接引入到页面中使用,需要先完成打包,执行如下指令
# browserify 指定打包工具; ./dist/app.js 指定入口文件; ./dist/bundel.js 指定打包后的入口文件
npx browserify ./dist/app.js -o ./dist/bundel.js
之后在页面中直接引入bundel.js即可
includes
使用数组的includes方法可以检查数组中是否包含某个元素,如下代码
/*
检查数组中是否包含某个元素
参数1 需要检查的目标元素
参数2 从指定索引开始
*/
console.log([1, 2, 3].includes(1, 0));
指数操作符
使用指数操作符能够快捷的实现幂运算,如下
console.log(99**2) // 计算 99 的二次方
async & await
async 函数
async 函数的返回值为promise对象,promise对象的状态由async函数的返回值决定
- 如果返回的是一个
promise类型,那么promise的状态由返回的promise对象中的状态决定 - 如果返回的是一个非
promise类型,那么最终的promise状态为resolve - 如果直接抛出异常,那么最终的
promise状态为rejected
await 表达式
await必须写在async函数中;await右侧的表达式返回值一般为promise对象;await返回的是promise成功的值,如果promise失败了就会抛出异常,需要通过try-catch处理,演示代码如下
// 统一登录函数
function doLogin() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({username: 'zhang san', token: 'MTMxM2VBRHdkd2Q'})
}, 1000)
})
}
// token 登录函数
async function tokenLogin() {
/*
等待登录完成后,拿到 token 值,执行到此处时
会等待 await 后面的 promise 返回结果才会继续往下执行
await 表达式的返回值直接就是 promise resolve 状态的返回值,而不是继续返回 promise
如果 promise 的状态为 reject,那么将直接抛出异常
*/
try {
let {token} = await doLogin()
console.log(token)
return token
} catch (e) {
console.error(e)
}
}
tokenLogin()
通过以上例子可以得出一种
async和await编程的思路:
- 将多个操作分别封装为一个函数且都在
promise中执行和返回结果- 申明一个
async函数,在其中根据业务需求,按照一定的顺序分别使用await关键字来调用上面封装的多个promise函数伪代码如下
function step1(){ return new Promise((resolve,reject) => { // 业务代码:成功调用 resolve(),失败调用 reject() 或抛出异常 }) } function step2(){ return new Promise((resolve,reject) => { // 业务代码 }) } function step3(){ return new Promise((resolve,reject) => { // 业务代码 }) } async function exec(){ let res1 = await step1() let {username} = await step2() // 也可以使用解构的形式获取指定的返回结果 let res3 = await step3() }
正则
命名捕获分组
现在需要提取一个a标签中的url以及a标签之间的文本信息,使用正则的写法如下
let lableStr = '<a href="https://www.baidu.com">百度 link</a>'
/*
使用正则进行匹配,其中 () 代表一个捕获项
. 代表除换行符外的任意字符
* 代表匹配前一个表达式 0 次或多次
*/
let reg = /<a href="(.*)">(.*)<\/a>/
let res = reg.exec(lableStr)
console.log(res)
最终匹配的结果为
[
"<a href=\"https://www.baidu.com\">百度 link</a>",
"https://www.baidu.com",
"百度 link"
]
将捕获到的内容以数组下标的形式进行区分,这种方式一旦匹配项发生变化,对应的数组下标也需要进行更改
在es9中引入了捕获分组的特性,只需要将上述的正则表达式改为如下形式
let reg = /<a href="(?<url>.*)">(?<text>.*)<\/a>/
其中,诸如?<url> ?<text>这类的表达式,都代表对当前捕获的内容进行分组,<>里面的内容为该组的组名,最终的返回结果中,除了数组形式的匹配结果,还会多一个groups属性,通过该属性可以拿到捕获分组的结果,代码如下
let lableStr = '<a href="https://www.baidu.com">百度 link</a>'
let reg = /<a href="(?<url>.*)">(?<text>.*)<\/a>/
let res = reg.exec(lableStr)
console.log(res.groups.url) // 通过捕获分组的 groups 属性获取结果
console.log(res.groups.text)
正向断言&反向断言
正向断言:(?=xxx)匹配右边是xxx的,例如使用\d+(?=aaa)来匹配右边为aaa的任意数字
反向断言:(?<=xxx)匹配左边是xxx的,例如使用(?<=aaa)\d+来匹配左边为aaa的任意数字
字符串扩展方法
let str = ' Java '
let trimStart = str.trimStart() // 清除字符串左侧空白
let trimEnd = str.trimEnd() // 清除字符串右侧空白
数组扩展方法
let arr = [[1, 2], [3, 4], [5, 6], [7, 8, [9, 10]]]
/*
flat 将多维数组转化为低维数组
每次调用,都会将目标数组降 1 维,n 次调用将会降 n 维
也可以直接传入参数,代表降低几维
*/
console.log(arr.flat(2));
/*
flatMap 相当于 map() 与 flat() 操作的结合
flatMap 可以对每个数组中的元素应用自定义的函数,同时将返回的结果进行降维(降 1 维)
对 flatMap 返回的结果,还可以继续执行 flat() 降维操作
*/
let arr2 = [10, 20, 30, 40]
console.log(arr2.flatMap(item => [item, [item * 10]]).flat());
私有属性
class Girl {
name;
#age; // 使用 # 申明私有属性
#weight
#height
constructor(name, age, weight, height) {
this.name = name
this.#age = age
this.#weight = weight
this.#height = height
}
// 对于私有属性,在类的内部可以直接得到,这里通过 get 方法来控制私有属性对外暴露的逻辑
get weight() {
return this.#weight + 'kg'
}
}
let girl = new Girl('杨玉环', 18, 55, 167)
console.log(girl.name)
/*
私有属性无法直接在类外部通过 . 运算得到
以下语句控制台会打印错误信息:Private field '#xxx' must be declared in an enclosing class
*/
console.log(girl.#weight)
可选链操作符
当需要对对象中的一些深层次属性进行判断时,通常会通过&&运算不断调用对象属性进行判断,如config && config.db && config.db.host,这种写法十分繁琐,es11中支持可选链操作符,官方文档定义如下
允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。
?.操作符的功能类似于.链式操作符,不同之处在于,在引用为空 (nullish ) (null或者undefined) 的情况下不会引起错误,该表达式短路返回值是undefined。与函数调用一起使用时,如果给定的函数不存在,则返回undefined。
代码如下
function connect(config) {
// .? 可选链接操作符
let host = config?.db?.host
console.log(host)
}
connect({
db: {
host: 'localhost',
port: 3306
},
user: {
username: 'root',
password: '111111'
}
})
动态 import
在使用模块化时,可能需要导入大量的模块,而这些模块可能有些并没有被使用,但依然被import所申明,es11 则支持动态导入模块,在需要时才导入,如下代码
// import() 的返回值是一个 Promise 对象,接收的参数为对应模块的变量
import('./hello.js').then(model => {
model.hello()
}).catch(err => { // 如果模块导入失败,则可以通过 catch 捕获到错误信息
console.log(err)
})
BigInt 类型
BigInt主要用于大整数的运算(不包含浮点数),如下代码
let bigInt = BigInt(2 ** 512)
console.log(bigInt)