Es6_Es11

115 阅读15分钟

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了,后续点击事件触发时,依然使用的是ifor循环这个块级作用域中的变量值,索引发生越界

如果将上述的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来定义常量,常量的值不能被修改且常量在申明时必须被赋值constlet一样,也具备块级作用域

对常量修饰的数组 对象中的属性的修改,不算做对常量的修改

解构

数组的解构

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) // true
    

    Symbol.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-offor-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.js

    import 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()

通过以上例子可以得出一种asyncawait编程的思路:

  • 将多个操作分别封装为一个函数且都在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)

绝对全局对象

参考 globalThis - JavaScript | MDN (mozilla.org)