「建议收藏」送你一份精心总结的3万字ES6实用指南(上)

5,212 阅读27分钟

写本篇文章目的是为了夯实基础,基于阮一峰老师的著作 ECMAScript 6 入门 以及 tc39-finished-proposals 这两个知识线路总结提炼出来的重点和要点,涉及到从 ES2015ES2021 的几乎所有知识,基本上都是按照一个知识点配上一段代码的形式来展示,所以篇幅较长,也正是因为篇幅过长,所以就没把 Stage 2Stage 3 阶段的提案写到这里,后续 ES2021 更新了我再同步更新。

有 5 个提案已经列入 Expected Publication Year in 2021 所以本篇中暂且把他们归为 ES2021。

由于篇幅限制,所以将文章分成了 2 篇,想要看完整版的可以直接看我的:github blog

  • 「建议收藏」送你一份精心总结的3万字ES6实用指南(上)
  • 「建议收藏」送你一份精心总结的3万字ES6实用指南(下)

ES6前言

发展史

能写好 JS 固然是重要的,但是作为一个前端,我们也要了解自己所使用语言的发展历程,这里强烈推荐看 《JavaScript 20年》,本书详细记载和解读了自 1995 年语言诞生到 2015 年 ES6 规范制定为止,共计 20 年的 JavaScript 语言演化历程。

版本说明

2011 年,发布了 ECMAScript 5.1 版,而 2015 年 6 月发布了 ES6 的第一个版本又叫 ES2015ES6 其实是一个泛指,指代 5.1 版本以后的下一代标准。TC39 规定将于每年的 6 月发布一次正式版本,版本号以当年的年份为准,比如当前已经发布了 ES2015ES2016ES2017ES2018ES2019ES2020 等版本。

提案发布流程

任何人都可以向 TC39 提案,要求修改语言标准。一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。

  • Stage 0 - Strawperson(展示阶段)
  • Stage 1 - Proposal(征求意见阶段)
  • Stage 2 - Draft(草案阶段)
  • Stage 3 - Candidate(候选人阶段)
  • Stage 4 - Finished(定案阶段)

一个提案只要能进入 Stage 2,就差不多肯定会包括在以后的正式标准里面。ECMAScript 当前的所有提案,可以在这里查看 ecma262。关于提案流程可以在这里 TC39_Process 看到更加详细的信息。

ES2015

声明

const:声明一个常量,let:声明一个变量;const/let 声明的常量/变量都只能作用于代码块(块级作用域或函数作用域)里;

if (true) {
    let name = '布兰'
}
console.log(name)  // undefined

const/let 不存在变量提升,所以在代码块里必须先声明然后才可以使用,这叫暂时性死区;

let name = 'bubuzou'
if (true) {
    name = '布兰'
    let name
}
console.log(name)

const/let 不允许在同一个作用域内,重复声明;

function setName(name) {
    let name = ''  // SyntaxError
}

const 声明时必须初始化,且后期不能被修改,但如果初始化的是一个对象,那么不能修改的是该对象的内存地址;

const person = {
    name: '布兰'
}
person.name = 'bubuzou'  
console.log(person.name)  // 'bubuzou'
person = ''  // TypeError

const/let 在全局作用域中声明的常量/变量不会挂到顶层对象(浏览器中是 window )的属性中;

var name = '布兰'
let age = 12
console.log(window.name)  // '布兰'
console.log(window.age)  // undefined

解构赋值

解构类型

  • 字符串解构

    let [a, b, c = 'c'] = '12'
    console.log(a, b, c)  // '1' '2' 'c'
    
  • 数值解构

    let {toFixed: tf} = 10
    console.log( tf.call(Math.PI, 2) )  // 3.14
    
  • 布尔值解构

    let {toString: ts} = true
    console.log( ts.call(false) )  // 'false'
    
  • 数组解构:等号右侧的数据具有 Iterator 接口可以进行数组形式的解构赋值;

    // 解构不成功的变量值为 undefined
    let [a, b, c] = [1, 2]
    console.log(a, b, c)  // 1, 2, undefined
    
    // 可以设置默认值
    let [x, y, z = 3] = [1, 2, null]
    console.log(x, y, z)  // 1, 2, null
    

    什么样的数据具有 Iterator 接口呢?如果一个对象能够通过 [Symbol.iterator] 访问,且能够返回一个符合迭代器协议的对象,那么该对象就是可迭代的。目前内置的可迭代对象有:String、Array、TypeArray、Map、Set、arguments 和 NodeList 等。

  • 对象解构:与数组按照索引位置进行解构不同,对象解构是按照属性名进行解构赋值,如果在当前对象属性匹配不成功则会去对象的原型属性上查找:

    // 默认写法
    let { name: name, age: age } = { name: '布兰', age: 12 }
    
    // 简写
    let { name, age } = { name: '布兰', age: 12 }
    
    // 改名且设置默认值
    let { name: name1, age: age1 = 12 } = { name: '布兰' }
    console.log(name1, age1)  // '布兰' 12
    
  • 函数参数解构:其实就是运用上面的对象解构和数组解构规则;

    function move({x = 0, y = 0} = {}) {
        console.log([x, y])
        return [x, y];
    }
    move({x: 3, y: 8})  // [3, 8]
    move({x: 3})        // [3, 0]
    move({})            // [0, 0]
    move()              // [0, 0]
    

解构要点

  • 只要等号两边的模式相同(同是对象或同是数组),则左边的变量会被赋予对应的值;
  • 解构不成功的变量值为 undefined
  • 默认值生效的前提是当等号右边对应的值全等于 undefined 的时候;
  • 只要等号右边的值不是对象或者数组,则会进行自动装箱将其转成对象;
  • nullundefined 都无法转成对象,所以无法解构。

解构应用

  • 交换变量的值;

    let x = 1, y = 2;
    [x, y] = [y, x]
    console.log(x, y)  // 2 1
    
  • 通过函数返回对象属性

    function getParams() {
        return {
            name: '布兰',
            age: 12,
        }
    }
    let {name, age} = getParams()
    
  • 通过定义函数参数来声明变量

    let person = {
        name: '布兰',
        age: 12
    }
    init(person)
    
    // 普通用法
    function init(person) {
        let {name, age} = person
    }
    
    // 更简洁用法
    function init({name, age}) {}
    
  • 指定函数参数默认值

    function initPerson({name = '布兰', age = 12} = {}) {
        console.log(name, age)
    }
    initPerson()  // '布兰' 12
    initPerson({age: 20})  // '布兰' 20
    
  • 提取 JSON 数据

    let responseData = {
        code: 1000,
        data: {},
        message: 'success'
    }
    
    let { code, data = {} } = responseData
    
  • 遍历 Map 结构

    let map = new Map()
    map.set('beijing', '北京')
    map.set('xiamen', '厦门')
    
    for (let [key, value] of map) {
        console.log(key, value)
    }
    
  • 输入模块的指定方法和属性

    const { readFile, writeFile } = require("fs")
    

字符串扩展

  • 可以使用 Unicode 编码来表示一个字符:

    // 以下写法都可以用来表示字符 z
    '\z'      // 转义
    '\172'    // 十进制表示法
    '\x7A'    // 十六进制表示法
    '\u007A'  // Unicode 普通表示法
    '\u{7A}'  // Unicode 大括号表示法
    

    www.52unicode.com 这个网站可以查询到常见符号的 Unicode 编码。

  • 可以使用 for...of 正确遍历字符串:

    let str = '😀🤣😜😍🤗🤔'
    for (const emoji of str) {
        console.log(emoji) // 😀🤣😜😍🤗🤔
    }
    for(let i = 0, l = str.length; i < l; i++) {
        console.log(str[i])  // 不能正确输出表情
    }
    
  • 模板字符串使用两个反引号标识(``),可以用来定义多行字符串,或者使用它在字符串中插入变量:

    let name = 'hero'
    let tips = `Hello ${name}, 
        welcome to my world.`
    alert( tips )
    
  • 标签模板:在函数名后面接一个模板字符串相当于给函数传入了参数进行调用:

    let name = '布兰', age = 12;
    let tips = parse`Hello ${name}, are you ${age} years old this year?`
    function parse(stringArr, ...variable) {
        
    }
    
    // 相当于传递如下参数进行调用 parse 函数
    parse(["Hello ", ", are you ", " years old this year?"], name, age)
    
  • String.fromCodePoint() 用于从 Unicode 码点返回对应字符,可以支持 0xFFFF 的码点:

    String.fromCharCode(0x1f600)   // ""
    String.fromCodePoint(0x1f600)  // "😀"
    
  • String.raw() 返回把字符串所有变量替换且对斜杠进行转义的结果:

    String.raw`Hi\n${2+3}!`  // "Hi\n5!"
    
  • codePointAt() 返回字符的十进制码点,对于 Unicode 大于 0xFFFF 的字符,会被认为是2个字符,十进制码点转成十六进制可以使用 toString(16)

    let emoji = '🤣'
    emoji.length  // 2
    emoji.charCodeAt(0).toString(16)  // 'd83d'
    emoji.charCodeAt(1).toString(16)  // 'de00'
    
    String.fromCodePoint(0xd83d, 0xde00) === '🤣'  // true
    
  • normalize() 方法会按照指定的一种 Unicode 正规形式将当前字符串正规化。(如果该值不是字符串,则首先将其转换为一个字符串):

    let str1 = '\u00F1'
    let str2 = '\u006E\u0303'
    
    str1  //  ñ
    str2  //  ñ
    str1 === str2  // false
    str1.length === str2.length  // false
    str1.normalize() === str2.normalize()  // true
    
  • 字符串是否包含子串:

    • includes():返回布尔值,表示是否找到了参数字符串。
    • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
    • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
    let s = 'Hello world!'
    
    s.includes('o')        // true
    s.startsWith('Hello')  // true
    s.endsWith('!')        // true
    

    这三个方法都支持第二个参数,表示开始搜索的位置:

    let s = 'Hello world!'
    
    s.includes('Hello', 6)   // false
    s.startsWith('world', 6) // true
    s.endsWith('Hello', 5)   // true
    

    上面代码表示,使用第二个参数 n 时,endsWith 的行为与其他两个方法有所不同。它针对前 n 个字符,而其他两个方法针对从第 n 个位置直到字符串结束。

  • repeat(n) 将当前字符串重复 n 次后,返回一个新字符串:

    'x'.repeat(2)         // 'xx'
    'x'.repeat(1.9)       // 'x'
    'x'.repeat(NaN)       // ''
    'x'.repeat(undefined) // ''
    'x'.repeat('2a')      // ''
    'x'.repeat(-0.6)      // '',解释:0 ~ 1 之间的小数相当于 0
    'x'.repeat(-2)        // RangeError
    'x'.repeat(Infinity)  // RangeError
    

数值扩展

  • 二进制(0b)和八进制(0o)表示法:

    let num = 100
    let b = num.toString(2)  // 二进制的100:1100100
    let o = num.toString(8)  // 八进制的100:144
    0b1100100 === 100        // true
    0o144 === 100            // true
    
  • Number.isFinite() 判断一个数是否是有限的数,入参如果不是数值一律返回 false

    Number.isFinite(-2.9)      // true
    Number.isFinite(NaN)       // false
    Number.isFinite('')        // false
    Number.isFinite(false)     // true
    Number.isFinite(Infinity)  // false
    
  • Number.isNaN() 判断一个数值是否为 NaN,如果入参不是 NaN 那结果都是 false

    Number.isNaN(NaN)       // true
    Number.isFinite('a'/0)  // true
    Number.isFinite('NaN')  // false
    
  • 数值转化:Number.parseInt()Number.parseFloat(),非严格转化,从左到右解析字符串,遇到非数字就停止解析,并且把解析的数字返回:

    parseInt('12a')  // 12
    parseInt('a12')  // NaN
    parseInt('')  // NaN
    parseInt('0xA')  // 10,0x开头的将会被当成十六进制数
    

    parseInt() 默认是用十进制去解析字符串的,其实他是支持传入第二个参数的,表示要以多少进制的 基数去解析第一个参数:

    parseInt('1010', 2)  // 10
    parseInt('ff', 16)  // 255
    

    参考:parseInt

  • Number.isInteger() 判断一个数值是否为整数,入参为非数值则一定返回 false

    Number.isInteger(25)   // true
    Number.isInteger(25.0) // true
    Number.isInteger()     // false
    Number.isInteger(null) // false
    Number.isInteger('15') // false
    Number.isInteger(true) // false
    Number.isInteger(3.0000000000000002) // true
    

    如果对数据精度的要求较高,不建议使用 Number.isInteger() 判断一个数值是否为整数。

  • Number.EPSILON 表示一个可接受的最小误差范围,通常用于浮点数运算:

    0.1 + 0.2 === 0.3  // false
    Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON  // true
    
  • Number.isSafeInteger() 用来判断一个数是否在最大安全整数(Number.MAX_SAFE_INTEGER)和最小安全整数(Number.MIN_SAFE_INTEGER)之间:

    Number.MAX_SAFE_INTEGER === 2 ** 53 -1        // true
    Number.MAX_SAFE_INTEGER === 9007199254740991  // true
    Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER  // true
    Number.isSafeInteger(2)         // true
    Number.isSafeInteger('2')       // false
    Number.isSafeInteger(Infinity)  // false
    
  • Math.trunc():返回数值整数部分

  • Math.sign():返回数值类型(正数 1、负数 -1、零 0)

  • Math.cbrt():返回数值立方根

  • Math.clz32():返回数值的 32 位无符号整数形式

  • Math.imul():返回两个数值相乘

  • Math.fround():返回数值的 32 位单精度浮点数形式

  • Math.hypot():返回所有数值平方和的平方根

  • Math.expm1():返回 e^n - 1

  • Math.log1p():返回 1 + n 的自然对数(Math.log(1 + n))

  • Math.log10():返回以 10 为底的 n 的对数

  • Math.log2():返回以 2 为底的n的对数

  • Math.sinh():返回 n 的双曲正弦

  • Math.cosh():返回 n 的双曲余弦

  • Math.tanh():返回 n 的双曲正切

  • Math.asinh():返回 n 的反双曲正弦

  • Math.acosh():返回 n 的反双曲余弦

  • Math.atanh():返回 n 的反双曲正切

数组扩展

  • 数组扩展运算符(...)将数组展开成用逗号分隔的参数序列,只能展开一层数组:

    // 应用一:函数传参
    Math.max(...[1, 2, 3])  // 3
    
    // 应用二:数组合并
    let merge = [...[1, 2], ...[3, 4], 5, 6]  // 1, 2, 3, 4, 5, 6
    
    // 应用三:浅克隆
    let a = [1, 2, 3]
    let clone = [...a]
    a === clone  // false
    
    // 应用四:数组解构
    const [x, ...y] = [1, 2, 3]
    x  // 1
    y  // [2, 3]
    
  • Array.from() 可以将类数组对象( NodeListarguments)和可迭代对象转成数组:

    // 应用一:字符串转数组
    Array.from('foo')  // ['f', 'o', 'o']
    
    // 应用二:数组合并去重
    let merge = [...[1, 2], ...[2, 3]]
    Array.from(new Set(merge))  // ['1', '2', '3']
    
    // 应用三:arguments 转数组
    function f() {
        return Array.from(arguments)
    }
    f(1, 2, 3)  // [1, 2, 3]
    

    如果 Array.from() 带第二个参数 mapFn,将对生成的新数组执行一次 map 操作:

    Array.from([1, 2, 3], (x) => x * x )    // [1, 4, 9]
    Array.from({length: 3}, (v, i) => ++i)  // [1, 2, 3]
    
  • Array.of() 将一组参数转成数组:

    Array.of(1, 2, 3)  // [1, 2, 3]
    
    // 类似于
    function arrayOf(...params){
        return [].slice.call(params)
    }
    arrayOf(1, 2, 3)  // [1, 2, 3]
    
  • Array.copyWithin() 在当前数组内部,将制定位置的成员复制到其他位置(会覆盖原来位置的成员),最后返回一个新数组。接收 3 个参数,参数为负数表示右边开始计算:

    • target(必选):替换位置的索引;
    • start(可选):从该位置开始读取数据,默认为 0;
    • end(可选):从该位置结束读取数据(不包括该位置的数据),默认为原数组长度;
    [1, 2, 3, 4, 5].copyWithin(-1)         // [1, 2, 3, 4, 1]
    [1, 2, 3, 4, 5].copyWithin(1)          // [1, 1, 2, 3, 4]
    [1, 2, 3, 4, 5].copyWithin(0, 3, 4)    // [4, 2, 3, 4, 5]
    [1, 2, 3, 4, 5].copyWithin(0, -3, -1)  // [3, 4, 3, 4, 5]
    
  • 查找第一个出现的子成员:find()findIndex()

    // 找出第一个偶数
    [1, 6, 9].find((val, index, arr) => val % 2 === 0)       // 6
    
    // 找出第一个偶数的索引位置
    [1, 6, 9].findIndex((val, index, arr) => val % 2 === 0)  // 1
    
  • fill() 使用给定的值来填充数组,有 3 个参数:

    • value:填充值;
    • start(可选),开始索引,默认为 0;
    • end(可选):结束索引,默认为数组长度,不包括该索引位置的值;
    // 初始化空数组
    Array(3).fill(1)  // [1, 1, 1]
    
    [1, 2, 3, 4].fill('a', 2, 4)  // [1, 2, 'a', 'a']
    
  • 通过 keys()(键名)、entries()(键值)和 values()(键值对) 获取数组迭代器对象,可以被 for...of 迭代,

    let arr = ['a', 'b', 'c']
    for (let x of arr.keys()) {
        console.log(x)  // 1, 2, 3
    }
    for (let v of arr.values()) {
        console.log(v)  // 'a' 'b' 'c'
    }
    for (let e of arr.entries()) {
        console.log(e)  // [0, 'a'] [0, 'b'] [0, 'c']
    }
    
  • 数组空位,是指数组没有值,比如:[,,],而像这种 [undefined] 是不包含空位的。由于 ES6 之前的一些 API 对空位的处理规则很不一致,所以实际操作的时候应该尽量避免空位的出现,而为了改变这个现状,ES6API 会默认将空位处理成 undefined

    [...[1, , 3].values()]  // [1, undefined, 3]
    [1, , 3].findIndex(x => x === undefined)  // 1
    

对象扩展

  • 对象属性简写:

    let name = '布兰'
    let person = {
        name,
        getName() {
            return this.name
        }
    }
    // 等同于
    let person1 = {
        name: '布兰',
        getName: function() {
            return this.name
        }
    }
    
  • 属性名表达式:在用对象字面量定义对象的时候,允许通过属性名表达式来定义对象属性:

    let name = 'name', 
    let person = {
        [name]: '布兰',
        ['get'+ name](){
            return this.name
        }
    }
    
  • 方法的 name 属性,存在好几种情况,这里仅列出常见的几种:

    情况一:普通对象方法的 name 属性直接返回方法名,函数声明亦是如此,函数表达式返回变量名:

    
    let person = {
        hi(){}
    }
    person.hi.name  // 'hi'
    

    情况二:构造函数的 nameanonymous

    (new Function).name  // 'anonymous'
    

    情况三:绑定函数的 name 将会在函数名前加上 bound

    function foo() {} 
    foo.bind({}).name  // 'bound foo'
    

    情况四:如果对象的方法使用了取值函数(getter)和存值函数(setter),则 name 属性不是在该方法上面,而是该方法的属性的描述对象的 getset 属性上面:

    let o = { 
        get foo(){}, 
        set foo(x){} 
    }
    o.foo.name  // TypeError: Cannot read property 'name' of undefined
    let descriptor = Object.getOwnPropertyDescriptor(o, "foo") 
    descriptor.get.name  // "get foo" 
    descriptor.set.name  // "set foo"
    

    参考:function_name

  • 属性的可枚举性

    对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。可以通过 Object.getOwnPropertyDescriptor() 来获取对象某个属性的描述:

    let person = { name: '布兰', age: 12 }
    Object.getOwnPropertyDescriptor(person, 'name')
    // {
    //     configurable: true,
    //     enumerable: true,
    //     value: "布兰",
    //     writable: true,
    // }
    

    这里的 enumerable 就是对象某个属性的可枚举属性,如果某个属性的 enumerable 值为 false 则表示该属性不能被枚举,所以该属性会被如下 4 种操作忽略:

    • for...in :只遍历对象自身的和继承的可枚举的属性;
    • Object.keys():返回对象自身的所有可枚举的属性的键名;
    • JSON.stringify():只串行化对象自身的可枚举的属性;
    • Object.assign(): 只拷贝对象自身的可枚举的属性。
    let person = { name: '布兰' }
    Object.defineProperty(person, 'age', {
        configurable: true,
        enumerable: false,
        value: 12,
        writable: true
    })
    person  // { name: '布兰', age: 12 }
    
    // 以下操作都将忽略 person 对象的 age 属性
    for (let x in person) {
        console.log(x)         // 'name'
    }
    Object.keys(person)        // ['name']
    JSON.stringify(person)     // '{"name": "布兰"}'
    Object.assign({}, person)  // { name: '布兰' }
    

    Reflect.ownKeys(obj): 返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举:

    // 基于上面的代码
    Reflect.ownKeys(person)  // ['name', 'age']
    
  • super 关键字,指向对象的原型对象,只能用于对象的方法中,其他地方将报错:

    let person = {
        name: '布兰',
        getName() {
            return super.name
        }
    }
    Object.setPrototypeOf(person, {name: 'hello'})
    person.getName()  // 'hello'
    
    // 以下几种 super 的使用将报错
    const obj1 = {
        foo: super.foo
    }
    const obj2 = {
        foo: () => super.foo
    }
    const obj3 = {
        foo: function () {
            return super.foo
        }
    }
    
  • Object.is() 用来判断两个值是否相等,表现基本和 === 一样,除了以下两种情况:

    +0 === -0            //true
    NaN === NaN          // false
    Object.is(+0, -0)    // false
    Object.is(NaN, NaN)  // true
    
  • Object.assign() 用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),如果有同名属性,则后面的会直接替换前面的:

    let target = { a: 1 }
    let source1 = { a: 2, b: 3, d: {e: 1, f: 2} }
    let source2 = { a: 3, c: 4, d: {g: 3} }
    Object.assign(target, source1, source2)
    target  // { a: 3, b: 3, c: 4, d: {g: 3} }
    

    Object.assign() 实行的是浅拷贝,如果源对象某个属性是对象,那么拷贝的是这个对象的引用:

    let target = {a: {b: 1}}
    let source = {a: {b: 2}}
    Object.assign(target, source)
    target.a.b = 3
    source.a.b  // 3
    
  • __proto__ 属性是用来读取和设置当前对象的原型,而由于其下划线更多的是表面其是一个内部属性,所以建议不在正式场合使用它,而是用下面的 Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

  • Object.setPrototypeOf() 用于设置对象原型,Object.getPrototypeOf() 用于读取对象原型:

    let person = {name: '布兰'}
    Object.setPrototypeOf(person, {name: '动物'})
    Object.getPrototypeOf(person)  // {name: '动物'}
    

正则扩展

  • RegExp 构造函数,允许首参为正则表达式,第二个参数为修饰符,如果有第二个参数,则修饰符以第二个为准:

    let reg = new RegExp(/xYz\d+/gi, i)
    reg.flags  // 'i'
    
  • 正则方法调用变更:字符串对象的 match()replace()search()split() 内部调用转为调用 RegExp 实例对应的 RegExp.prototype[Symbol.方法]

  • u 修饰符:含义为 Unicode 模式,用来正确处理大于 \uFFFFUnicode 字符。也就是说,如果待匹配的字符串中可能包含有大于 \uFFFF 的字符,就必须加上 u 修饰符,才能正确处理。

    // 加上 u 修饰符才能让 . 字符正确识别大于 \uFFFF 的字符
    /^.$/.test('🤣')   // false
    /^.$/u.test('🤣')  // true
    
    // 大括号 Unicode 字符表示法必须加上 u 修饰符
    /\u{61}/.test('a')   // false
    /\u{61}/u.test('a')  // true
    
    // 有 u 修饰符,量词才能正确匹配大于 \uFFFF 的字符 
    /🤣{2}/.test('🤣🤣')  // false
    /🤣{2}/u.test('🤣🤣') // true
    

    RegExp.prototype.unicode 属性表示正则是否设置了 u 修饰符:

    /🤣/.unicode   // false
    /🤣/u.unicode  // true
    
  • y 修饰符,与 g 修饰符类似也是全局匹配;不同的是 g 是剩余字符中匹配即可,而 y 则是必须在剩余的第一个字符开始匹配才行,所以 y 修饰符也叫黏连修饰符:

    let s = 'aaa_aa_a'
    let r1 = /a+/g
    let r2 = /a+/y
    
    r1.exec(s)  // ["aaa"]
    r2.exec(s)  // ["aaa"]
    
    r1.exec(s)  // ["aa"]
    r2.exec(s)  // null
    

    RegExp.prototype.sticky 属性表示是否设置了 y 修饰符:

    /abc/y.sticky  // true
    
  • RegExp.prototype.flags 属性会返回当前正则的所有修饰符:

    /abc🤣/iuy.flags  // 'iuy'
    

函数扩展

  • 函数参数默认值。参数不能有同名的,函数体内不能用 letconst 声明同参数名的变量:

    function printInfo(name = '布兰', age = 12) {}
    

    使用参数默认值的时候,参数不能有同名的:

    function f(x, x, y) {}      // 不报错
    function f(x, x, y = 1) {}  // 报错
    

    函数体内不能用 letconst 声明同参数名的变量:

    // 报错
    function f(x, y) {
        let x = 0
    }
    

    函数的 length 属性会返回没有指定默认值的参数个数,且如果设置默认值的参数不是尾参数,则 length 不再计入后面的参数:

    (function f(x, y){}).length      // 2
    (function f(x, y = 1){}).length  // 1
    (function f(x = 1, y){}).length  // 0
    
  • 剩余(rest) 参数(...变量名)的形式,用于获取函数的剩余参数,注意 rest 参数必须放在最后一个位置,可以很好的代替 arguments 对象:

    function f(x, ...y) {
        console.log(x)  // 1
        for (let val of y) {
            coonsole.log(val)  // 2 3
        }
    }
    f(1, 2, 3)
    
  • 严格模式:只要函数参数使用了默认值、解构赋值或者扩展运算符,那么函数体内就不能显示的设定为严格模式,因为严格模式的作用范围包含了函数参数,而函数执行的顺序是先执行参数,然后再执行函数体,执行到函数体里的 use strict 的时候,那么此时因为函数参数已经执行完成了,那函数参数还要不要受到严格模式的限制呢?这就出现矛盾了。规避限制的办法有两个:设置全局的严格模式或者在函数体外在包一个立即执行函数并且声明严格模式:

    // 解法一
    'use strict'
    function f(x, y = 2) {
        
    }
    
    // 解法二
    let f = (function(){
        'use strict'
        return function(x, y = 2) {}
    })()
    
  • 箭头函数语法比函数表达式更简洁,并且没有自己的 thisarguments,不能用作构造函数和用作生成器。

    几种箭头函数写法:

    let f1 = () => {}               // 没有参数
    let f2 = (x) => {}              // 1个参数
    let f3 = x => {}                // 1个参数可以省略圆括号
    let f4 = (x, y) => {}           // 2个参数以上必须加上圆括号
    let f5 = (x = 1, y = 2) => {}   // 支持参数默认值
    let f6 = (x, ...y) => {}        // 支持 rest 参数
    let f7 = ({x = 1, y = 2} = {})  // 支持参数解构
    

    箭头函数没有自己的 this

    function Person(){
        this.age = 0
        setInterval(() => {
            this.age++
        }, 1000)
    }
    var p = new Person()  // 1 秒后 Person {age: 1}
    

    通过 call/apply 调用箭头函数的时候将不会绑定第一个参数的作用域:

    let adder = {
        base: 1,
        add: function(a) {
            let f = v => v + this.base
            return f(a)
        }, 
        addThruCall: function(a) {
            let f = v => v + this.base
            let b = {
                base: 2
            } 
            return f.call(b, a)
        }
    }
    adder.add(1)          // 输出 2
    adder.addThruCall(1)  // 仍然输出 2
    

    箭头函数没有自己的 arguments 对象,不过可以使用 rest 参数代替:

    let log = () => {
        console.log(arguments)  // ReferenceError
    }
    log(2, 3)
    
    // 剩余参数代替写法
    let restLog = (...arr) => {
        console.log(arr)  // [2, 3]
    }
    restLog(2, 3)
    

    箭头函数不能用作构造器,和 new 一起用会抛出错误:

    let Foo = () => {}
    let foo = new Foo()
    // TypeError: Foo is not a constructor
    

    箭头函数返回对象字面量,需要用圆括号包起来:

    let func2 = () => ({foo: 1})
    

    参考:Arrow_functions

  • 尾调用和尾递归

    首先得知道什么是尾调用:函数的最后一步调用另外一个函数:

    // 是尾调用
    function f(x) {
        return g(x)
    }
    
    // 以下都不是尾调用
    function f(x) {
        let y = g(x)
        return y
    }
    function f(x) {
        let y = g(x)
        return g(x) + 1
    }
    function f(x) {
        g(x)
        // 因为最后一步是 return: undefined
    }
    

    尾调用有啥用?我们知道函数的相互调用是会生成“调用帧”的,而“调用帧”里存了各种信息比如函数的内部变量和调用函数的位置等,所有的“调用帧”组成了一个“调用栈”。如果在函数的最后一步操作调用了另外一个函数,因为外层函数里调用位置、内部变量等信息都不会再用到了,所有就无需保留外层函数的“调用帧”了,只要直接用内层函数的“调用帧”取代外层函数的“调用帧”即可:

    function f() {
        let m = 1
        let n = 2
        return g(m + n)
    }
    f()
    
    // 等同于
    function f() {
        return g(3)
    }
    f()
    
    // 等同于
    g(3)
    

    这样一来就很明显的减少了调用栈中的帧数,内存占用就少了,所以这就是尾调用的优化作用。尾递归也是如此,递归如果次数多那就需要保留非常多的“调用帧”,所以经常会出现栈溢出错误,而使用了尾递归优化后就不会发生栈溢出的错误了:

    // 常规递归的斐波那契函数
    function Fibonacci(n) {
        if ( n <= 1 ) {return 1}
        return Fibonacci(n - 1) + Fibonacci(n - 2)
    }
    Fibonacci(100) // 超时
    
    // 尾递归优化后的斐波那契函数
    function Fibonacci2(n, ac1 = 1, ac2 = 1) {
        if( n <= 1 ) {return ac2}
        return Fibonacci2(n - 1, ac2, ac1 + ac2)
    }s
    Fibonacci2(100)  // 573147844013817200000
    

Symbol

  • Symbol 是一个新的原始类型,用来表示一个独一无二的值,可以通过 Symbol() 函数来创建一个 Symbol 类型的值,为了加以区分,可以传入一个字符串作为其描述:

    let s1 = Symbol('foo') 
    let s2 = Symbol('foo')
    s1 === s2  // false
    
  • Symbol 类型无法通过数学运算符进行隐式类型转换,但是可以通过 String() 显示转成字符串或者通过 Boolean() 显示转成布尔值:

    let s = Symbol('foo')
    String(s)     // "Symbol('foo')"
    s.toString()  // "Symbol('foo')"
    Boolean(s)    // true
    
  • 引入 Symbol 最大的初衷其实就是为了让它作为对象的属性名而使用,这样就可以有效避免属性名的冲突了:

    let foo = Symbol('foo')
    let obj = {
        [foo]: 'foo1',
        foo: 'foo2'
    }
    obj[foo]  // 'foo1'
    obj.foo   // 'foo2'
    
  • Symbol 属性的不可枚举性,不会被 for...infor...ofObject.keys()Object.getOwnPropertyNames()JSON.stringify() 等枚举:

    let person = {
        name: '布兰',
        [Symbol('age')]: 12,
    }
    for (let x in person) {
        console.log(x)  // 'name'
    }
    Object.keys(person)  // ['name']
    Object.getOwnPropertyNames(person)  // ['name']
    JSON.stringify(person)  // '{"name":"布兰"}'
    

    但是可以通过 Object.getOwnPropertySymbols() 获取到对象的所有 Symbol 属性名,返回一个数组:

    // 基于上面的代码
    Object.getOwnPropertySymbols(person)  // [Symbol(age)]
    

静态方法

  • Symbol.for() 按照描述去全局查找 Symbol,找不到则在全局登记一个:

    let s1 = Symbol.for('foo')
    let s2 = Symbol.for('foo')
    s1 === s2 // true
    

    Symbol.for() 的这个全局登记特性,可以用在不同的 iframeservice worker 中取到同一个值。

  • Symbol.keyFor() 根据已经在全局登记的 Symbol 查找其描述:

    let s = Symbol.for('foo')
    Symbol.keyFor(s)  // 'foo'
    

Symbol 的内置值

  • Symbol.hasInstance:指向一个内部方法,当其他对象使用 instanceof 运算符判断是否为此对象的实例时会调用此方法;
  • Symbol.isConcatSpreadable:指向一个布尔,定义对象用于 Array.prototype.concat() 时是否可展开;
  • Symbol.species:指向一个构造函数,当实例对象使用自身构造函数时会调用指定的构造函数;
  • Symbol.match:指向一个函数,当实例对象被 String.prototype.match() 调用时会重新定义match()的行为;
  • Symbol.replace:指向一个函数,当实例对象被 String.prototype.replace() 调用时会重新定义 replace() 的行为;
  • Symbol.search:指向一个函数,当实例对象被 String.prototype.search() 调用时会重新定义 search() 的行为;s
  • Symbol.split:指向一个函数,当实例对象被 String.prototype.split() 调用时会重新定义 split() 的行为;
  • Symbol.iterator:指向一个默认遍历器方法,当实例对象执行 for...of 时会调用指定的默认遍历器;
  • Symbol.toPrimitive:指向一个函数,当实例对象被转为原始类型的值时会返回此对象对应的原始类型值;
  • Symbol.toStringTag:指向一个函数,当实例对象被 Object.prototype.toString() 调用时其返回值会出现在 toString() 返回的字符串之中表示对象的类型;
  • Symbol.unscopables:指向一个对象,指定使用 with 时哪些属性会被 with 环境排除;

Set

  • Set 是一种新的数据结构,类似数组,但是它没有键只有值,且值都是唯一的。可以通过构造函数生成一个新实例,接收一个数组或者可迭代数据结构作为参数:

    new Set([1, 2, 3])  // Set {1, 2, 3}
    new Set('abc')      // Set {'a', 'b', 'c'}
    
  • Set 判断两个值是不是相等用的是 sameValueZero 算法,类似于 ===,唯一的区别是,在 SetNaN 之间被认为是相等的:

    let set = new Set()
    let a = NaN
    let b = NaN
    set.add(a)
    set.add(b)
    set.size  // 1
    
  • 相同对象的不同实例也被 Set 认为是不相等的:

    let set = new Set()
    let a = {a: 1}
    let b = {a: 1}
    set.add(a)
    set.add(b)
    set.size  // 2
    
  • Set 是有顺序的,将按照插入的顺序进行迭代,可以使用 for...of 迭代:

    let set = new Set([1, 3])
    set.add(5)
    set.add(7)
    for(let x of set) {
        console.log(x)
    }
    

Set 实例属性和方法

  • Set.prototype.constructor:构造函数,默认就是 Set 函数;
  • Set.prototype.size:返回 Set 实例的成员总数;
  • Set.prototype.add(value):添加某个值,返回 Set 结构本身;
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功;
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员;
  • Set.prototype.clear():清除所有成员,没有返回值;
  • Set.prototype.keys():返回键名的遍历器;
  • Set.prototype.values():返回键值的遍历器;
  • Set.prototype.entries():返回键值对的遍历器;
  • Set.prototype.forEach():使用回调函数遍历每个成员;
let set = new Set([1, 3])
set.add(5)     // Set {1, 3, 5}
set.size       // 3
set.delete(1)  // true,1 已被删除
set.has(1)     // false
set.keys()     // SetIterator {3, 5}
set.clear()
set.size       // 0

Set 应用场景

  • 数组去重:

    [...new Set([1, 3, 6, 3, 1])]         // [1, 3, 6]
    Array.from(new Set([1, 3, 6, 3, 1]))  // [1, 3, 6]
    
  • 字符串去重:

    [...new Set('abcbacd')].join('')  // 'abcd'
    
  • 求两个集合的交集/并集/差集:

    let a = new Set([1, 2, 3])
    let b = new Set([4, 3, 2])
    
    // 并集
    let union = new Set([...a, ...b])  // Set {1, 2, 3, 4}
    
    // 交集
    let intersect = new Set([...a].filter(x => b.has(x)))  // set {2, 3}
    
    // (a 相对于 b 的)差集
    let difference = new Set([...a].filter(x => !b.has(x)))  // Set {1}
    
  • 遍历修改集合成员的值:

    let set = new Set([1, 2, 3])
    
    // 方法一
    let set1 = new Set([...set].map(val => val * 2))     // Set {2, 3, 6}
    
    // 方法二
    let set2 = new Set(Array.from(set, val => val * 2))  // Set {2, 4, 6}
    

WeakSet

  • WeakSet 对象允许将弱保持对象存储在一个集合中:

    let ws = new WeakSet()
    let foo = {}
    ws.add(foo)  // WeakSet {{}}
    ws.has(foo)  // true
    ws.delete(foo)  // WeakSet {}
    

Set 的区别

  • WeakSet 只能是对象的集合,而不能是任何类型的任意值;
  • WeakSet 持弱引用:集合中对象的引用为弱引用。如果没有其他的对 WeakSet 中对象的引用,那么这些对象会被当成垃圾回收掉。这也意味着 WeakSet 中没有存储当前对象的列表。正因为这样,WeakSet 是不可枚举的,也就没有 size 属性,没有 clear 和遍历的方法。

实例方法

  • WeakSet.prototype.add(value):添加一个新元素 value
  • WeakSet.prototype.delete(value):从该 WeakSet 对象中删除 value 这个元素;
  • WeakSet.prototype.has(value):返回一个布尔值, 表示给定的值 value 是否存在于这个 WeakSet 中;

Map

  • Map 是一种类似于 Object 的这种键值对的数据结构,区别是对象的键只能是字符串或者 Symbol,而 Map 的键可以是任何类型(原始类型、对象或者函数),可以通过 Map 构造函数创建一个实例,入参是具有 Iterator 接口且每个成员都是一个双元素数组 [key, value] 的数据结构:

    let map1 = new Map()
    map1.set({}, 'foo')
    
    let arr = [['name', '布兰'], ['age', 12]]
    let map2 = new Map(arr)
    
  • Map 中的键和 Set 里的值一样也必须是唯一的,遵循 sameValueZero 算法,对于同一个键后面插入的会覆盖前面的,

    let map = new Map()
    let foo = {foo: 'foo'}
    map.set(foo, 'foo1')
    map.set(foo, 'foo2')
    map.get(foo)  // 'foo2' 
    
  • 对于键名同为 NaN 以及相同对象而不同实例的处理同 Set 的值一样:

    let a = NaN
    let b = NaN
    let map = new Map()
    map.set(a, 'a')
    map.set(b, 'b')
    map.size    // 1
    map.get(a)  // 'b'
    
    let c = {c: 'c'}
    let d = {c: 'c'}
    map.set(c, 'c')
    map.set(d, 'd')
    map.size    // 3
    map.get(c)  // 'c'
    

实例属性和方法

  • Map.prototype.size:返回 Map 对象的键值对数量;
  • Map.prototype.set(key, value):设置 Map 对象中键的值。返回该 Map 对象;
  • Map.prototype.get(key): 返回键对应的值,如果不存在,则返回 undefined
  • Map.prototype.has(key):返回一个布尔值,表示 Map 实例是否包含键对应的值;
  • Map.prototype.delete(key): 如果 Map 对象中存在该元素,则移除它并返回 true
  • Map.prototype.clear(): 移除 Map 对象的所有键/值对;
  • Map.prototype.keys():返回一个新的 Iterator 对象, 它按插入顺序包含了 Map 对象中每个元素的键;
  • Map.prototype.values():返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的值;
  • Map.prototype.entries():返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的 [key, value] 数组;
  • Map.prototype.forEach(callbackFn[, thisArg]):按插入顺序遍历 Map
let map = new Map()
map.set({a: 1}, 'a')
map.set({a: 2}, 'b')

for (let [key, value] of map) {
    console.log(key, value)
}
// {a: 1} 'a'
// {a: 2} 'b'

for (let key of map.keys()) {
    console.log(key)
}
// {a: 1}
// {a: 2}

WeakMap

  • 类似于 Map 的结构,但是键必须是对象的弱引用,注意弱引用的是键名而不是键值,因而 WeakMap 是不能被迭代的;

    let wm = new WeakMap()
    let foo = {name: 'foo'}
    wm.set(foo, 'a')  // Weak
    wm.get(foo)       // 'a'
    wm.has(foo)       // true
    

    虽然 wm 的键对 foo 对象有引用,但是丝毫不会阻止 foo 对象被 GC 回收。当引用对象 foo 被垃圾回收之后,wmfoo 键值对也会自动移除,所以不用手动删除引用。

实例方法

  • WeakMap.prototype.delete(key):移除 key 的关联对象;
  • WeakMap.prototype.get(key):返回key关联对象, 或者 undefined(没有key关联对象时);
  • WeakMap.prototype.has(key):根据是否有 key 关联对象返回一个 Boolean 值;
  • WeakMap.prototype.set(key, value):在 WeakMap 中设置一组 key 关联对象,返回这个 WeakMap 对象;

Proxy

  • Proxy 用来定义基本操作的的自定义行为,可以理解为当对目标对象 target 进行某个操作之前会先进行拦截(执行 handler 里定义的方法),必须要对 Proxy 实例进行操作才能触发拦截,对目标对象操作是不会拦截的,可以通过如下方式定义一个代理实例

    let proxy = new Proxy(target, handler)
    
    let instance = new Proxy({name: '布兰'}, {
        get(target, propKey, receiver) {
            return `hello, ${target.name}`
        },
    })
    instance.name  // 'hello, 布兰'
    
  • 如果 handle 没有设置任何拦截,那么对实例的操作就会转发到目标对象身上:

    let target = {}
    let proxy = new Proxy(target, {})
    proxy.name = '布兰'
    target.name  // '布兰'
    
  • 目标对象被 Proxy 代理的时候,内部的 this 会指向代理的实例:

    const target = {
        m: function () {
            console.log(this === proxy)
        }
    }
    const handler = {}
    const proxy = new Proxy(target, handler)
    target.m()  // false
    proxy.m()   // true
    

静态方法

  • Proxy.revocable() 用以定义一个可撤销的 Proxy

    let target = {}
    let handler = {}
    let {proxy, revoke} = Proxy.revocable(target, handler)
    
    proxy.foo = 123
    proxy.foo  // 123
    revoke()
    proxy.foo  // TypeError
    

handle 对象的方法

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。 deleteProperty(target, propKey):拦截 delete proxy[propKey] 的操作,返回一个布尔值。
  • ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(...args)

Reflect

  • Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handlers 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。

  • 设计的目的:

    • Object 属于语言内部的方法放到 Reflect 上;
    • 修改某些 Object 方法的返回结果,让其变得更合理;
    • Object 操作变成函数行为;
    • Proxy handlesReflect 方法一一对应,前者用于定义自定义行为,而后者用于恢复默认行为;

静态方法

  • Reflect.apply(target, thisArgument, argumentsList) 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似;
  • Reflect.construct(target, argumentsList[, newTarget]) 对构造函数进行 new 操作,相当于执行 new target(...args)
  • Reflect.defineProperty(target, propertyKey, attributes)Object.defineProperty() 类似。如果设置成功就会返回 true
  • Reflect.deleteProperty(target, propertyKey) 作为函数的 delete 操作符,相当于执行 delete target[name]
  • Reflect.get(target, propertyKey[, receiver]) 获取对象身上某个属性的值,类似于 target[name]
  • Reflect.getOwnPropertyDescriptor(target, propertyKey) 类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined
  • Reflect.getPrototypeOf(target) 类似于 Object.getPrototypeOf()
  • Reflect.has(target, propertyKey) 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同;
  • Reflect.isExtensible(target) 类似于 Object.isExtensible()
  • Reflect.ownKeys(target) 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受 enumerable 影响);
  • Reflect.preventExtensions(target) 类似于 Object.preventExtensions()。返回一个 Boolean
  • Reflect.set(target, propertyKey, value[, receiver]) 将值分配给属性的函数。返回一个 Boolean,如果更新成功,则返回 true
  • Reflect.setPrototypeOf(target, prototype) 设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回 true

Class

  • 可以用 class 关键字来定义一个类,类是对一类具有共同特征的事物的抽象,就比如可以把小狗定义为一个类,小狗有名字会叫也会跳;类是特殊的函数,就像函数定义的时候有函数声明和函数表达式一样,类的定义也有类声明和类表达式,不过类声明不同于函数声明,它是无法提升的;类也有 name 属性

    // 类声明
    class Dog {
        constructor(name) {
            this.name = name
        }
        bark() {}
        jump() {}
    }
    Dog.name  // 'Dog'
    
    // 类表达式:可以命名(类的 name 属性取类名),也可以不命名(类的 name 属性取变量名)
    let Animal2 = class {
        // xxx
    }
    Animal2.name  // 'Animal2'
    
  • JS 中的类建立在原型的基础上(通过函数来模拟类,其实类就是构造函数的语法糖),和 ES5 中构造函数类似,但是也有区别,比如类的内部方法是不可被迭代的:

    class Dog {
        constructor() {}
        bark() {}
        jump() {}
    }
    Object.keys(Dog.prototype)  // []
    
    // 类似于
    function Dog2(){
    }
    Dog2.prototype = {
        constructor() {},
        bark() {},
        jump() {},
    }
    Object.keys(Dog2.prototype)  // ['constructor', 'bark', 'jump']
    
  • 基于原型给类添加新方法:

    Object.assign(Dog.prototype, {
        wag() {}  // 摇尾巴
    })
    
  • 类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,gettersetter 都在严格模式下执行。

  • 类内部的 this 默认指向类实例,所以如果直接调用原型方法或者静态方法会导致 this 指向运行时的环境,而类内部是严格模式,所以此时的 this 会是 undefined

    class Dog {
        constructor(name) {
            this.name = name
        }
        bark() {
            console.log( `${this.name} is bark.` )
        }
        static jump() {
            console.log( `${this.name} is jump.` )
        }
    }
    let dog = new Dog('大黄')
    let { bark } = dog
    let { jump } = Dog
    bark()  // TypeError: Cannot read property 'name' of undefined
    jump()  // TypeError: Cannot read property 'name' of undefined
    

方法和关键字

  • constructor 方法是类的默认方法,通过 new 关键字生成实例的时候,会自动调用;一个类必须有constructor 方法,如果没有显示定义,则会自动添加一个空的;constructor 默认会返回实例对象:

    class Point {}
    
    // 等同于
    class Point {
        constructor() {}
    }
    
  • 通过 getset 关键字拦截某个属性的读写操作:

    class Dog {
        get age(){
            return 1
        }
        set age(val){
            this.age = val
        }
    }
    
  • static 关键字给类定义静态方法,静态方法不会存在类的原型上,所以不能通过类实例调用,只能通过类名来调用,静态方法和原型方法可以同名:

    class Dog {
        bark() {}
        jump() {
            console.log('原型方法')
        }
        static jump() {
            console.log('静态方法')
        }
    }
    Object.getOwnPropertyNames(Dog.prototype)  // ['constructor', 'bark', 'jump']
    Dog.jump()  // '静态方法'
    let dog = new Dog()
    dog.jump()  // '原型方法'
    
  • 公有字段和私有字段:

    静态公有字段和静态方法一样只能通过类名调用;私有属性和私有方法只能在类的内部调用,外部调用将报错:

    class Dog {
        age = 12                   // 公有字段
        static sex = 'male'        // 静态公有字段
        #secret = '我是人类的好朋友'  // 私有字段
        #getSecret() {              // 私有方法
            return this.#secret
        }
    }
    Dog.sex  // 'male'
    let dog = new Dog()
    dog.#getSecret()  // SyntaxError
    

    公共和私有字段声明是 JavaScript 标准委员会 TC39 提出的实验性功能(第 3 阶段)。浏览器中的支持是有限的,但是可以通过 Babel 等系统构建后使用此功能。

  • new.target 属性允许你检测函数、构造方法或者类是否是通过 new 运算符被调用的。在通过 new 运算符被初始化的函数或构造方法中,new.target 返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是 undefined,子类继承父类的时候会返回子类:

    class Dog {
        constructor() {
            console.log(new.target.name)
        }
    }
    function fn(){
        if (!new.target) return 'new target is undefined'
        console.log('fn is called by new')
    }
    let dog = new Dog()  // 'Dog'
    fn()                 // 'new target is undefined'
    new fn()             // 'fn is called by new'
    

类的继承

  • 类可以通过 extends 关键字实现继承,如果子类显示的定义了 constructor 则必须在内部调用 super() 方法,内部的 this 指向当前子类:

    class Animal {
        constructor(name) {
            this.name = name
        }
        run() {
            console.log(`${this.name} is running.`)
        }
    }   
    class Dog extends Animal{
        constructor(name){
            super(name)  // 必须调用
            this.name = name
        }
        bark() {
            console.log(`${this.name} is barking.`)
        }
    
    }
    let dog = new Dog('大黄')
    dog.run()  // '大黄 is running.'
    
  • 通过 super() 调用父类的构造函数或者通过 super 调用父类的原型方法;另外也可以在子类的静态方法里通过 super 调用父类的静态方法:

    // 基于上面的代码改造
    class Dog extends Animal{
        constructor(name){
            super(name)  // 调用父类构造函数
            this.name = name
        }
        bark() {
            super.run()  // 调用父类原型方法
            console.log(`${this.name} is barking.`)
        }
    }
    let dog = new Dog()
    dog.bark()s
    // '大黄 is running.'
    // '大黄 is barking.'
    
  • 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类;子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的prototype属性:

    class Animal {}
    class Dog extends Animal {}
    
    Dog.__proto__ === Animal  // true
    Dog.prototype.__proto__ === Animal.prototype  // true
    

    子类原型的原型指向父类的原型:

    // 基于上面的代码
    let animal = new Animal()
    let dog = new Dog()
    dog.__proto__.__proto__  === animal.__proto__  // true
    
  • 使用 extends 还可以实现继承原生的构造函数,如下这些构造函数都可以被继承:

    • String()
    • Number()
    • Boolean()
    • Array()
    • Object()
    • Function()
    • Date()
    • RegExp()
    • Error()
    class MyString extends String {
        constructor(name){
            super(name)
            this.name = name
        }
        welcome() {
            return `hello ${this.name}`
        }
    }
    let ms = new MyString('布兰')
    ms.welcome()      // 'hello 布兰'
    ms.length         // 2
    ms.indexOf('兰')  // 1
    

Module

  • 浏览器传统加载模块方式:

    // 同步加载
    <script src="test.js"></script>
    
    // defer异步加载:顺序执行,文档解析完成后执行;
    <script src="test.js" defer></script>
    
    // async异步加载:乱序加载,下载完就执行。
    <script src="test.js" async></script>
    
  • 浏览器现在可以按照模块(加上 type="module")来加载脚本,默认将按照 defer 的方式异步加载;ES6 的模块加载依赖于 importexport 这 2 个命令;模块内部自动采用严格模式:

    // 模块加载
    <script type="module" src="test.js"></script>
    
  • export 用于输出模块的对外接口,一个模块内只能允许一个 export default 存在,以下是几种输出模块接口的写法:

    // person.js
    // 写法一:单独导出
    export const name = '布兰'
    export const age = 12
    
    // 写法二:按需导出
    const name = '布兰', age = 12
    export { name, age }
    
    // 写法三:重命名后导出
    const name = '布兰', age = 12
    export { name as name1, age as age1 }
    
    // 写法四:默认导出
    const name = '布兰'
    export default name
    
  • import 用于输入其他模块的接口:

    // 按需导入
    import { name, age } './person.js'
    
    // 导入后重命名
    import { name1 as name, age1 as age } from './person.js'
    
    // 默认导入
    import person from './person.js'
    
    // 整体导入
    import * as person from './person.js'
    
    // 混合导入
    import _, { each } from 'lodash'
    

    import 导入的细节:

    • 导入的变量名必须与导出模块的名字一致,可以使用 as 进行重命名;
    • 导入的变量都是只读的,不能改写;
    • import 命令具有提升效果,会提升到整个模块的头部,首先执行;
    • import 是编译时导入,所以不能将其写到代码块(比如 if 判断块里)或者函数内部;
    • import 会执行所加载的模块的代码,如果重复导入同一个模块则只会执行一次模块;
  • importexport 的复合写法:exportimport 语句可以结合在一起写成一行,相当于是在当前模块直接转发外部模块的接口,复合写法也支持用 as 重命名。以下例子中需要在 hub.js 模块中转发 person.js 的接口:

    // person.js
    const name = '布兰', age = 12
    export { name, age }
    // 按需转发接口(中转模块:hub.js)
    export { name, age } from './person.js'
    // 相当于
    import { name, age } from './person.js'
    export { name, age }
    
    // person.js
    const name = '布兰', age = 12
    export default { name, age }
    // 转发默认接口(中转模块:hub.js)
    export { default } from './person.js'
    // 相当于
    import person from './person.js'
    export default person
    
  • ES6 模块和 CommonJS 模块的差异:

    • CommonJS 模块输出的是一个值的拷贝(一旦输出一个值,模块内部的变化就影响不到这个值),ES6 模块输出的是值的引用(是动态引用且不会缓存值,模块里的变量绑定其所在的模块,等到脚本真正执行时,再根据这个只读引用到被加载的那个模块里去取值);
    • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口;
    • CommonJS 模块的 require() 是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段;

Iterator和for...of

  • Iterator 迭代器协议,为各种数据结构提供了一种统一按照某种顺序进行访问的机制。通常部署在一个可迭代数据结构内部或其原型上。一个对象要能够成为迭代器,它必须有一个 next() 方法,每次执行 next() 方法会返回一个对象,这个对象包含了一个 done 属性(是个布尔值,true 表示可以继续下次迭代)和一个 value 属性(每次迭代的值):

    // 生成一个迭代器
    let makeIterator = (arr) => {
        let index = 0
        return {
            next(){
                return index < arr.length ? {
                    value: arr[index++],
                    done: false
                } : { done: true }
            }
        }
    }
    
  • iterable 可迭代数据结构:内部或者原型上必须有一个 Symbol.iterator 属性(如果是异步的则是 Symbol.asyncIterator),这个属性是一个函数,执行后会生成一个迭代器:

    let obj = {
        [Symbol.iterator]() {
            return {
                index: 0,
                next() {
                    if (this.index < 3) {
                        return {value: this.index++, done: false}
                    } else {
                        return {done: true}
                    }
                } 
            }
        }
    }
    for (let x of obj) {
        console.log(x)  // 0 1 2
    }
    

    内置的一些可迭代数据结构有:StringArrayTypedArrayMapSetargumentsNodeList

    let si = 'hi'[Symbol.iterator]()
    si         // StringIterator
    si.next()  // {value: 'h', done: false}
    si.next()  // {value: 'i', done: false}
    si.next()  // {value: undefined, done: true}
    
  • for...of:用于遍历可迭代数据结构:

    • 遍历字符串:for...in 获取索引,for...of 获取值;
    • 遍历数组:for...in 获取索引,for...of 获取值;
    • 遍历对象:for...in 获取键,for...of 需自行部署 [Symbol.iterator] 接口;
    • 遍历 Setfor...of 获取值, for (const v of set)
    • 遍历 Mapfor...of 获取键值对,for (const [k, v] of map)
    • 遍历类数组:包含 length 的对象、arguments 对象、NodeList对象(无 Iterator 接口的类数组可用 Array.from() 转换);
    // 迭代字符串
    for (let x of 'abc') {
        console.log(x)  // 'a' 'b' 'c'
    }
    
    // 迭代数组
    for (let x of ['a', 'b', 'c']) {
        console.log(x)  //  'a' 'b' 'c'
    }
    
    // 遍历 Set
    let set = new Set(['a', 'b', 'c'])
    for (let x of set) {
        console.log(x)  // 'a' 'b' 'c'
    }
    
    // 遍历 Map
    let map = new Map([['name', '布兰'], ['age', 12]])
    for (let [key, value] of map) {
        console.log(key + ': ' + value)  // 'name: 布兰' 'age: 12'  
    }
    
  • for...offor...in 对比

    共同点:能够通过 breakcontinuereturn 跳出循环; 不同点: - for...in 的特点:只能遍历键,会遍历原型上属性,遍历无顺序,适合于对象的遍历; - for...of 的特点:能够遍历值(某些数据结构能遍历键和值,比如 Map),不会遍历原型上的键值,遍历顺序为数据的添加顺序,适用于遍历可迭代数据结构;

Promise

Promise 这块知识可以直接看我之前写的一篇文章:深入理解Promise 非常完整。

Generator

  • function* 会定义一个生成器函数,调用生成器函数不会立即执行,而是会返回一个 Generator 对象,这个对象是符合可迭代协议和迭代器协议的,换句话说这个 Generator 是可以被迭代的。

  • 生成器函数内部通过 yield 来控制暂停,而 next() 将把它恢复执行,它的运行逻辑是如下这样的:

    • 遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值作为返回的对象的 value 属性值;
    • 下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式;
    • 如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值;
    • 如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined
    function* gen() {
        yield 'hello'
        yield 'world'
        return 'end'
    }
    let g = gen()
    g.next()  // {value: 'hello', done: false}
    g.next()  // {value: 'world', done: false}
    g.next()  // {value: 'end', done: true}
    
  • 在生成器函数内部可以使用 yield* 表达式委托给另一个 Generator 或可迭代对象,比如数组、字符串等;yield* 表达式本身的值是当迭代器关闭时返回的值(即 donetrue 时):

    function* inner() {
        yield* [1, 2]
        return 'foo'
    }
    function* gen() {
        let result = yield* inner()
        console.log(result)
    }
    let g = gen()
    g.next()
    g.next()
    g.next()
    

实例方法

  • Generator.prototype.next():返回一个包含属性 donevalue 的对象。该方法也可以通过接受一个参数用以向生成器传值。如果传入了参数,那么这个参数会传给上一条执行的 yield 语句左边的变量:

    function* f() {
        let a = yield 12
        console.log(a)
        let b = yield a
        console.log(b)
    }
    let g = f()
    console.log(g.next('a'))
    console.log(g.next('b'))
    console.log(g.next('c'))
    // {value: 12, done: false}
    // 'b'
    // {value: 'b', done: false}
    // 'c'
    // {value: undefined, done: true}
    
  • Generator.prototype.throw():用来向生成器抛出异常,如果内部捕获了则会恢复生成器的执行(即执行下一条 yield 表达式),并且返回带有 donevalue 两个属性的对象:

    function* gen() {
        try {
            yield 'a'
        } catch(e) {
            console.log(e)
        }
        yiele 'b'
        yield 'c'
    }
    let g = gen()
    g.next()
    g.throw('error a')
    g.next()
    // {value: "a", done: false}
    // 'error a' 
    // {value: "b", done: false}
    // {value: "c", done: false}
    

    如果内部没有捕获异常,将中断内部代码的继续执行(类似 throw 抛出的异常,如果没有捕获,则后面的代码将不会执行),此时异常会抛到外部,可以被外部的 try...catch 块捕获,此时如果再执行一次 next(),会返回一个值为 done 属性为 true 的对象:

    function* gen() {
        yield 'a'
        yield 'b'
        yield 'c'
    }
    let g = gen()
    g.next()
    try {
        g.throw('error a')
    } catch(e) {
        console.log(e)
    }
    g.next()
    // {value: "a", done: false}
    // 'error a'
    // {value: undefined, done: true}
    
  • Generator.prototype.return():返回给定的值并结束生成器:

    function* gen() {
        yield 1
        yield 2
        yield 3
    }  
    let g = gen()
    g.next()         // { value: 1, done: false }
    g.return('foo')  // { value: "foo", done: true }
    g.next()         // { value: undefined, done: true }
    

应用

  • 将异步操作同步化,比如同时有多个请求,多个请求之间是有顺序的,只能等前面的请求完成了才请求后面的:

    function* main() {
        let res1 = yield request('a')
        console.log(res1)
        let res2 = yield request('b')
        console.log(res2)
        let res3 = yield request('c')
        console.log(res3)
    }
    function request(url) {
        setTimeout(function(){  // 模拟异步请求
            it.next(url)
        }, 300)
    }
    let it = main()
    it.next()
    // 'a' 'b' 'c'
    
  • 给对象部署 Iterator 接口:

    function* iterEntries(obj) {
        let keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            let key = keys[i]
            yield [key, obj[key]]
        }
    }
    let obj = { foo: 3, bar: 7 }
    for (let [key, value] of iterEntries(myObj)) {
        console.log(key, value)
    }
    // 'foo' 3
    // 'bar' 7
    

参考文章