ES6+新增的常用知识点

注:由于ES6之后的标准变化不大,因此在此将ES6及之后标准发生的变化在此一起总结

1.变量声明——let和const

  1. (作用域) let和const是块级作用域,var是函数作用域
  2. (变量提升) var声明变量存在变量提升
  3. (暂时性死区) let和const声明变量存在暂时性死区
  4. (重复声明) var声明的变量允许重复声明,后声明的变量覆盖先声明的变量,let和const不允许
  5. (全局变量) var在浏览器全局环境下声明的变量会自动加入全局对象的属性,不可被delete。
  6. (初始值) var和let不需要设置初始值,const声明变量必须要有初始值
  7. (指针指向) const的指针不可变,var和let可变

2.操作符

2.1 扩展运算符

三点运算符可以作为“扩展运算符”或“其余运算符”或“Object.assign简写形式”

  • 其余运算符。例如:函数定义式参数带有扩展运算符,表示传入实参时可以传递多个,被收集成一个数组。

  • 扩展运算符。作为扩展运算符时,对可迭代对象使用三点运算符都会调用迭代器方法。例如:解构声明变量时配合扩展运算符可以声明数组;console.log中使用扩展运算符可以直接遍历可迭代对象;在中括号中对可迭代对象使用扩展运算符可以进行浅拷贝。

  • Object.assign简写形式。例如:用于对象浅拷贝,在{}中使用三点展开对象,效果同Object.assign且不会调用迭代器方法。

2.2 指数操作符

**操作符代替Math.pow()进行指数运算

2.3 属性访问操作符

?.操作符是用于对象属性访问。短路操作,如果对象通过?.访问属性为null或undefined,那么不论属性访问式有多长访问结果立即为undefined。

let a = {
    b: null
}
console.log(a.b?.c.d) // undefined
// ?.左边的a.b为null,那么这个属性访问表达式结果立即返回undefined

console.log(a.b?.().c.d()) // undefined
// 当然在调用方法时也是一样,在调用b()前发现a.b为null,那么访问表达式立即返回undefined

2.4 缺值合并操作符

??操作符是用于求值。如果??操作符前的值为null或undefined,那么求值表达式的结果将会为??操作符后的值,反之求值表达式的结果将会为??操作符前的值。短路操作,如果操作符前不是null或undefined,操作符后就不管了。

;(function test(arg) {
    arg = arg ?? 1
    console.log(arg) // 1
    // 由于执行时未给arg传值,arg为undefined,所以函数体第一行arg为1
})();

2.5 与操作符

&&操作符可用于求值。如果&&操作符前后都是真值,那么返回&&操作符后的真值。否则返回假值。短路操作,如果操作符前是假值,操作符后就不管了。

console.log(0 && 12) // 0

console.log("dog" && null) // null

// 只有都是真值才会返回操作符后的结果
console.log("Danny" && 27) // 27

2.6 或操作符

||操作符可用于求值。如果||操作符前后都是假值,那么返回||操作符后的假值。否则返回真值。短路操作,如果操作符前是真值,操作符后就不管了。

console.log(undefined || "Danny") // Danny

console.log("Danny" || undefined) // Danny

// 只有都是假值才会返回操作符后的结果
console.log(undefined || null) // null

3.赋值语句——解构

解构提供了一种更好地提取数据的方式

3.1 数组解构

let [a, b, c] = [1, 2, 3] // a = 1, b = 2, c = 3

数组解构是按照数组下标来提取值

3.2 对象解构

let obj = {
    name: "Danny",
    gender: "man"
}
// ES6增强型对象写法
let { name, gender } = obj
// 对象解构普通写法,属性值用于存储提取出来的数据,属性名要和原对象属性名相同
let { name: name2, gender: gender2 } = obj

对象解构是按照属性名来提取值

3.3 解构应用

  1. 从深层嵌套的对象中提取数据
    应用场景是对象属性中嵌套对象,想要提取深层嵌套中的某个数据,可以使用对象解构
let obj = {
    event: {
        target: {
            result: "Danny"
        }
    }
}
let { event: { target: { result } } } = obj
console.log(result)
  1. 当函数参数过多时进行函数参数解构
    应用场景是函数参数过多,记得住参数叫什么,但记不清参数的顺序,可以使用对象结构
// 线性回归的核心迭代函数,参数比较多,可能记不清顺序
function linearRegression({theta, Y, X, alpha = 0.5, iter =10000}) {
    // 略
}

let theta = []
let X = []
let Y = []

// 调用时只需要直到有哪些参数即可,不需要考虑它们的顺序
linearRegression({theta: theta, X: X, Y: Y})

4.数值

4.1 数值方法

  1. 将parseInt()和parseFloat()和isNaN()方法除了可以全局调用也可以通过Number对象调用

4.2 BigInt类型

  1. (数据类型) BigInt也是JavaScript数据类型,至此共有8种基础数据类型。

  2. (数值范围) IEEE754 64位标准表示的最大精度整数是16位,使用BigInt可以表示更大的数,在chrome测试最大能到10** 10 ** 9(10的10亿次方),理论上是无限大的。

  3. (使用) 可以使用BigInt工厂函数或者数值后面加上一个n来表示BigInt类型

4.3 Math方法

增加的Math方法一般用不到

5.字符串

5.1 模板字符串

模板字符串允许在字符串中插入动态的值,可以是JavaScript语句。这属于模板语法,模板语法不在此过多介绍,面试题总结基本从未涉及。

5.2 字符串新增方法

  1. includes()方法:用于判断子串是否存在
let s = "123Danny"
console.log(s.includes("Danny")) // true
  1. startsWith()方法:用于判断字符串开头是否是目标子串
let s = "123Danny"
console.log(s.startsWith("Danny")) // false
  1. endsWidth()方法:用于判断字符串结尾是否是目标子串
let s = "123Danny"
console.log(s.endsWith("Danny")) // true
  1. padStart()方法:用于在字符串左侧填充符号,使字符串长度变成参数指定的长度
let s = "123Danny"
console.log(s.padStart(20, " ")) //             123Danny
  1. padEnd()方法:用于在字符串右侧填充符号,使字符串长度变成参数指定的长度
let s = "123Danny"
console.log(s.padEnd(20, " ") + 1) // 123Danny            1
  1. trimStart()方法:用于去除字符串左侧的空格
let s = "123Danny"
console.log(s = s.padStart(20, " ")) //             123Danny
console.log(s = s.trimStart()) // 123Danny
  1. trimEnd()方法:用于去除字符串右侧的空格
let s = "123Danny"
console.log(s = s.padEnd(20, " "), s.length) // 20
console.log(s = s.trimEnd(), s.length) // 8
  1. matchAll()方法:用于正则表达式分组

6.符号

6.1 创建符号

  1. 使用Symbol()工厂函数创建 (Symbol不像其它类型,Symbol只能作为工厂函数,不能作为构造函数),传入字符串作为参数,返回一个符号值,即使每次传入相同字符串,也会返回不同的符号。

  2. 使用Symbol.for()方法创建,传入字符串作为参数,如果传入相同字符串,那么返回相同的符号值。

6.2 迭代器协议

[Symbol.iterator]方法和[Symbol.asyncIterator]方法用于实现迭代器和异步迭代器

6.3 对象方法

  1. [Symbol.toStringTag]方法用于改变对象调用toString方法的返回值,必须是get方法
let stu = {
    name: "Danny"
}
console.log(stu.toString()) // [object Object]

class Test {
    constructor() {}
    get [Symbol.toStringTag]() {
        return "Test"
    }
}
let test = new Test()
console.log(test.toString()) // [object Test]
  1. [Symbol.hasInstance]方法用于扩展instanceof操作符(右侧必须是构造函数)使用范围,使得那些没有构造函数的对象也可以使用该操作符。
// 不要给函数添加这个方法,只适用于非函数对象
function Student(name, gender) {
    let object = new Object()
    object.name = name
    object.gender = gender
    return object
}
Student[Symbol.hasInstance] = function(obj) {
    console.log(obj)
    return "name" in obj && "gender" in obj ? true : false
}

let student = Student("Danny", "man")
console.log(student instanceof Student) // false,这个在工厂函数中使用不起作用

// 正确示范,在对象中使用
let grade = {
    [Symbol.hasInstance](grade) {
        return grade >= 0 && grade <= 100 ? true : false
    }
}
console.log(98 instanceof grade) //true
  1. 其它的符号方法更不常用了,这里就列举两个

7.对象

7.1 对象属性增强写法

  1. 可以在对象字面量中只使用属性名
  2. 可以通过[]来访问属性

7.2 支持类

详情见“面向对象编程”中继承中关于类的介绍

7.3 for in和 for of 和 for await

  1. for in 是JavaScript本来就有的枚举方法
    (1)用来枚举对象的可枚举属性
    (2)与Object.keys相比,同样不可以枚举Symbol属性,可以枚举继承属性
let stu = {
    name: "Danny",
    gender: "man",
    grade: 100,
    changeName(name) {
        this.name = name
    }
}
// 枚举出所有属性(上述属性都是可枚举的),枚举有特殊顺序
for(let el in stu)
    console.log(el)
// 该对象不是可迭代对象,枚举会报错
for(let el of stu)
    console.log(el)
  1. for of 是ES6新增的枚举方法
    (1)用来按照迭代器协议枚举可迭代对象
function* generator(n) {
    let a = 0, b = 1
    while(a < n) {
        yield a;
        [a, b] = [b, a + b]
    }
}
let fib = generator(100)
for(let el of fib)
    console.log(el)
  1. for await 是ES6以后新增的枚举方法
    (1)是按照迭代器协议枚举可枚举对象,在给循环变量赋值时都会加上一个await
// 实现一个异步迭代器
function asyncIterator(ar) {
    let index = 0, length = ar.length
    return {
       [Symbol.asyncIterator]() {
           return this
       },
       next() {
           if(index === length)
               return new Promise(resolve => {
                   resolve({done: true, value: undefined})
               })
           else {
               if (ar[index] instanceof Promise)
                   return new Promise((resolve, reject) => {
                       ar[index ++].then(res => {
                           resolve({done: false, value: res})
                       }, error => {
                           reject(error)
                       })
                   })
               else
                   return new Promise(resolve => {
                       resolve({done: false, value: ar[index ++]})
                   })
           }
       }
   }
}

// 创建测试数据
let p1 = new Promise(res => {
    setTimeout(() => {
        res("p1")
    }, 3000)
}), p2 = new Promise(res => {
    setTimeout(() => {
        res("p2")
    }, 2000)
});

(async function () {
    // 使用for await迭代异步迭代器
    for await(let p of asyncIterator([p1, p2]))
        console.log(p);

    // 手动迭代异步迭代器
    (function autoMaker(asyncIter) {
        let result = asyncIter.next();
        (function next(result) {
            result.then(res => {
                if(res.done !== true) {
                    console.log(res.value)
                    result = asyncIter.next()
                    next(result)
                }
            })
        })(result);
    })(asyncIterator([p1, p2]));
})();

7.4 对象方法

只在ES6的文档中就扩展了不少新的对象方法,这里列举一下常用的。
注:虽然像Object.freeze和Object.seal这些方法也很有用,但是这里先不总结。Object.freeze可以配合Vue使用提高性能,等用到的时候再做总结。

  1. Object.is(value1, value2) 用于判断两个值是否相等。

判断规则:

  1. 类型转换不同于=,不会对value1和value2做强制类型转换。
  2. null和undefined不同于=,都为null或都为undefined时为true
  3. 字符串判断同=,字面相同即为true
  4. 数值判断不同于=,NaN和NaN为true,+0和-0为true
  5. 对象判断不同于=,指向同一个堆内存的对象为true
  1. Object.assign(target, …args) 用于对象合并,效果和扩展操作符一样

合并规则:

  1. target是目标对象,args都是来源对象,将来源对象的可枚举属性都复制到target上面,如果出现覆盖现象,那么按顺序覆盖。
  2. "Object.assign"和"扩展操作符+对象字面量"来合并对象,都是浅复制,不会复制堆内存。

8.数组

8.1 数组新方法

  1. Array.from(iterator) 工厂方法,可以将一个可迭代对象展开,转换成一个数组

    // 斐波那契数列迭代器
    function generator(n) {
        let a = 0, b = 1
        return {
            [Symbol.iterator]() {
                return this
            },
            next() {
                let temp = a
                if(temp <= n ) {
                    [a, b] = [b, a + b]
                    return {done: false, value: temp}
                }
                else
                    return { done: true, value: undefined }
            }
        }
    }
    // 得到一个1000以内的斐波那契数列数组
    console.log(Array.from(generator(1000))) 
    
  2. Array.isArray(obj) 工厂方法,判断传入的参数是否是数组

  3. Array.of(…args) 工厂方法,传入任意多个元素,成为数组的成员。在Array构造函数中,如果只传入一个元素,那么会设定为数组长度,Array.of()解决了这个问题。

  4. array.findIndex() 新增了数组元素搜索方法,搜不到返回-1,否则返回满足条件的元素的位置。此方法比indexOf方法更加灵活,这种方法可以说是高阶函数,允许自己编程。

    // 制造100个位于-50~+50之间的随机数
    let ar = []
    for(let i = 0; i < 100; i ++)
        ar[i] = (Math.random() - 0.5) * 100
    
    // 寻找第一个大于60的数的下标
    let index = ar.findIndex((el, index, array) => {
        return el > 30
    },this)
    console.log(index)
    
  5. array.find() 新增了数组元素搜索方法,搜不到返回undefined,否则返回满足条件的元素的值。

    // 制造100个位于-50~+50之间的随机数
    let ar = []
    for(let i = 0; i < 100; i ++)
        ar[i] = (Math.random() - 0.5) * 100
    
    // 寻找第一个大于60的数的值
    let el = ar.find((el, index, array) => {
        return el > 30
    },this)
    console.log(el)
    
  6. array.flat(depth) 新增了数组扁平化方法。可以设置扁平的层数。这个方法返回一个新数组,不修改原数组。

    let ar = [1, [2, [3, [4, [5]]]]]
    console.log(ar.flat(1), ar) // [ 1, 2, [ 3, [ 4, [Array] ] ] ] [ 1, [ 2, [ 3, [Array] ] ] ]
    console.log(ar.flat(2), ar) // [ 1, 2, 3, [ 4, [ 5 ] ] ] [ 1, [ 2, [ 3, [Array] ] ] ]
    console.log(ar.flat(3), ar) // [ 1, 2, 3, 4, [ 5 ] ] [ 1, [ 2, [ 3, [Array] ] ] ]
    console.log(ar.flat(4), ar) // [ 1, 2, 3, 4, 5 ] [ 1, [ 2, [ 3, [Array] ] ] ]
    
  7. array.flatMap() 新增了数组扁平化映射方法。默认扁平化一层。这个方法会先进行map映射,最后再进行数组扁平化。这个方法返回一个新数组,不修改原数组。

    // 使用flatMap方法比map方法更高效,除了能扁平化,还能实现filter的效果。
    let ar = [1, , , 2, , 3, [], 4, , [], , , 5]
    console.log(ar.flatMap((el, index, array) => {
        return el ? el : []
    }, this)) // [ 1, 2, 3, 4, 5 ]
    
  8. array.fill(value, start, end) 新增了数组填充方法。从起始位置到终点位置填充value值(左闭右开的区间)。这个方法不返回一个新数组,会修改原数组。

    let ar = new Array(10)
    ar.fill(12, 0, 9)
    console.log(ar) // [ 12, 12, 12, 12, 12, 12, 12, 12, 12, <1 empty item> ]
    
  9. array.includes(el) 新增了数组的元素存在方法。这个方法相比下面“12”中介绍的集合的测试元素是否存在方法低效很多。

    let ar = new Array(10)
    ar.fill(12, 0, 9)
    console.log(ar.includes(11), ar.includes(12)) // false true
    

9.函数

9.1 箭头函数

详情见“JavaScript解释器和编译器”中JavaScript运行时执行上下文创建时关于this绑定的问题

9.2 函数支持形参赋默认值

注意一下形参赋默认值实际是在执行上下文的词法环境中赋的值,不是在变量环境中赋值。详情见“JavaScript解释器和编译器”中JavaScript运行时执行上下文创建时的词法环境和语法环境,以及提供的例题。

9.3 尾调用

详情见“函数式编程”中尾调用

10.异步编程

10.1 Promise

详情见“JavaScript异步编程”中的Promise

10.2 迭代器和生成器

详情见“JavaScript异步编程”中的迭代器和生成器

10.3 async和await和异步迭代器

详情见“JavaScript异步编程”中的async和await

11.代理和反射

11.1 理解反射对象

反射对象是Reflect,它类似于Map定义了一些映射关系。Reflect的方法直接映射到操作对象的方法上。举例如下:

  1. Reflect.ownKeys(Object) 直接映射到对象属性访问,访问对象所有属性
  2. Reflect.has(Object, name) 直接映射到in操作符,相当于name in Object的操作

11.2 理解对象代理

let p = new Proxy(target, handler)

对象类型意义作用
代理对象Proxy构造函数的实例化对象,指的是p目标对象的代理人,用户发起的对目标对象的操作都交给代理对象处理。代理对象接收到操作请求后会交给处理器对象。
目标对象一个普通对象,指的是target目标对象不希望暴露出去,指派代理对象作为代理人。
处理器对象一个普通对象,指的是handler目标对象的操作人,处理器对象有自己的处理函数命名规范,用来处理对象操作,例如set用于设置对象属性,get用于获取对象属性,也可以调用反射对象方法将操作返回给目标对象执行。

11.3 保护引用

对象代理中可以创建一个可撤销代理,当需要把对象交给第三方库时,可以给对象创建一个透明包装(处理器对象为空对象),并且这个包装是可撤销的,一使用完第三方库就立即撤销代理,第三方库无法访问目标对象。

// 一个涉及到学生成绩隐私的对象
let stu = {
    name: "Danny",
    grade: [98, 96, 96, 91, 91, 97, 94]
}

// 一个不受信赖的第三方函数
function unSafeFunc(stuInfo) {
    console.log(stuInfo)
}

// 创建可撤销代理
let { proxy, revoke } = Proxy.revocable(stu, {})

// 第三方函数操作,此时第三方函数中可以操作该对象
unSafeFunc(proxy)

// 撤销代理,proxy代理对象不能再代理目标对象stu,就是proxy不能再操作stu
revoke()

// 第三方函数再次尝试操作,发现不能操作该对象,直接报错
unSafeFunc(proxy)

11.4 隐藏属性

在3.5的示例中如果想保护学生隐私,不想让成绩被别发现,那么可以在处理器对象中设置拦截,在has函数和get函数中判断如果出现grade属性相关操作,那么一律拦截。

has(target, name) {
    if(name === "grade")
        return false
    return Reflect.has(target, name)
}

get(target, name, receiver) {
    if(name === "grade")
        return undefined
    return Reflect.get(target, name, receiver)
}

11.5 记录操作

对代理对象的操作会先交付给处理器对象,如果在处理器对象中设置监听操作,那么就可以记录这些针对目标对象的操作。

let stu = {
    name: "Danny",
    grade: [98, 96, 96, 91, 91, 97, 94]
}

let handler = {
    get(target, name, receiver) {
        console.log(`监听到访问对象${name}属性的操作`)
        return Reflect.get(target, name, receiver)
    },
    set(target, name, value, receiver) {
        console.log(`监听到修改对象${name}属性值为${value}的操作`)
        return Reflect.set(target, name, value, receiver)
    },
    has(target, name) {
        console.log(`监听到查询对象是否存在${name}属性的操作`)
        return Reflect.has(target, name)
    }
}

let proxy = new Proxy(stu, handler)

proxy.name = "Lucy"
console.log(proxy.name)
console.log("grade" in proxy)

11.6 类型验证

可以利用3.4中提到的拦截机制来提供JavaScript中不具备的自动类型检查功能。类型检查不用写在类中,提高了代码的简洁性。

class Student {
    constructor(name, gender) {
        this.name = name
        this.gender = gender
    }
}

let StudentProxy = new Proxy(Student, {
    construct(target, args, newTarget) {
        if(typeof args[0] !== "string")
            throw new TypeError("姓名必须是字符串")
        if(typeof args[1] !== "string" || (args[1] !== "男" && args[1] !== "女"))
            throw new TypeError("性别必须是男或女")
        return Reflect.construct(target, args, newTarget)
    }
})

// 由于man不符合处理器对象第二条规范,所以会抛出错误
let stu = new StudentProxy("Danny", "man")

11.7 数据绑定

可以利用3.4中提到的拦截机制来进行数据绑定,把两个本来不相关的数据绑定在一起。下面实现了每创建一个学生就会自动被加入学生列表的操作。

let stuList = []

class Student {
    constructor(name, gender) {
        this.name = name
        this.gender = gender
    }
}

let StudentProxy = new Proxy(Student, {
    construct(target, argArray, newTarget) {
        let stu = Reflect.construct(target, argArray, newTarget)
        stuList.push(stu)
        return stu
    }
})

let stu = new StudentProxy("Danny", "man")
console.log(stuList)
运用方向应用场景
保护目标对象3.3 保护引用
保护目标对象3.4 隐藏属性
提高代码封装性3.5 记录操作
提高代码封装性3.6 类型验证
提高代码封装性3.7 数据绑定

12.集合和映射

12.1 Map

12.1.1 Map和Object的对比

情况MapObject
意外的键Map默认情况下不包含键默认情况下Object包含原型链上的键,可能会被自身的键覆盖
键的顺序枚举时Map的键的顺序是插入顺序枚举时Object的键有特殊顺序
键的类型Map的键可以是任意类型Object的键只能是Symbol或者String
迭代性Map可迭代Object只有写迭代器才可以迭代
性能优化1.可以快速通过size属性获取键值数量 2.对频繁增删查改键值做出优化未作出优化

12.1.2 如果没有Map如何模拟一个Map

根据“12.1”中提到的Map和Object的对比,可以考虑用Object来模拟一个Map。实际上就是指定一个特殊的映射,可以使用hash算法,也可以借助Symbol.for()来完成,将键进行序列化后传入Symbol.for()得到一个符号,一定和传入其它字符串产生的Symbol不同。

12.1.3 Map方法介绍

基础方法:

  1. set(key, value) 设置键和值
  2. get(key) 通过键获取值
  3. delete(key) 根据键删除某个键值
  4. has(key ) 判断是否有键
  5. clear() 清空所有键值

迭代器方法:

  1. keys() 返回键构成的数组
  2. values() 返回值构成的数组
  3. entries() 返回键和值构成的数组
  4. forEach() 类似数组的遍历方法
  5. size 返回Map键值对的个数

12.1.4 WeakMap和Map的对比

情况MapWeakMap
键的要求原始类型或者引用类型必须是引用类型
垃圾回收Map中的值如果是引用类型,相当于对该堆内存又添加了一个引用,不管是标记清理还是引用计数都无法释放该堆内存WeakMap中的值是引用类型,但是不会影响该堆内存被垃圾回收
迭代性可迭代由于值随时会被垃圾回收,所以不可迭代

12.1.5 WeakMap方法介绍

  1. set(key, value) 添加一个键值对
  2. has(key) 判断是否有键
  3. delete(key) 删除一个键值对
  4. get(key) 获取一个键值

12.2 Set

12.2.1 Set和Array的对比

情况SetArray
顺序性元素插入Set的顺序有数值索引顺序
重复性不允许元素重复允许元素重复
迭代性可迭代可迭代
索引无索引有索引
性能优化1.对检查是否存在某一个元素做了优化相比Set没什么优化

12.2.2 如果没有Set如何模拟一个Array

上述提到的Array来模拟,实际上可以对Array增加一些检查机制,可以通过对象代理来实现,来实现Set对于元素的严格要求。之后对元素检查做一个O(lgn)的优化。

12.2.3 Set方法介绍

基础方法:

  1. add(el) 给集合添加一个元素
  2. has(el) 判断集合中是否存在一个元素
  3. delete(el) 删除集合中的一个元素
  4. clear() 清空集合

迭代器方法:

  1. forEach 类似数组的遍历方法
  2. size 返回集合元素个数

13.模块化

13.1 CommonJS规范

13.1.1 CommonJS的实现机制

1. 模块加载器机制: 为了实现CommonJS,NodeJS或webpack会实现一个模块加载器,用于实现下列两种机制。模块加载器的部分代码如下举例。

2. 包裹机制: 实现CommonJS模块化需要在模块被创建时在外面包裹一个闭包

// 下面是模拟实现,Module类控制模块,调用Module.wrap方法,传入你的代码后就会自动对你的代码进行包装
Module.wrapper = [
    // 可以看到CommonJS要求为每个模块额外提供5个全局变量
   '(function (exports, require, module, __dirname, __filename) {', '})'
]

Module.wrap = script => {
   return `${Module.wrapper[0]}${script}${Module.wrapper[1]}`
}

3. 同步加载机制: 实现CommonJS模块化需要实现重要的模块加载机制。加载机制可以简述为以下几个步骤:
(1) 解析路径
(2) 调用底层的C++提供的IO模块同步读取文件
(3) 创建模块对象,创建时会实现上面的包裹机制
(4) 将模块对象加入自定义的缓存对象存储,提高多次调用时的效率
(5) 返回该模块暴露出来的module.exports对象

// 模拟实现的核心代码
// 实现模块加载的函数
function req (filename) {
    //1.我们需要一个绝对路径来,缓存是根据绝对路径的来的
    filename =  Module.resolvePathName(filename)
    if(!filename) return new Error('not find file')
    
    //判断是否有缓存,有的话返回缓存对象
    let cacheModule = Module._cache[filename]
    if(cacheModule) return cacheModule

    // 2,没有模块 创建模块
    let module = new Module(filename) //创建模块

    //3.加载这个模块{filename: 'c:xx', exports: 'hello world'}
    module.load(filename) // 这里不过多列举load方法,感兴趣可以再到网上了解
    
    //4.把加载好的模块加入缓存
    Module._cache[filename] = module
    return module.exports
}

// 实现路径解析的函数
Module.resolvePathName = filename => {
   // 1.拿到路径,进行解析绝对路径
   let p = path.resolve(__dirname, filename)
   //2.判断路径里是路径还是文件名,如果是文件名的话查找文件
   if(!path.extname(p)) {
      //4.如果有文件名,则确定是一个文件,开始
      for(var i =0, arr = Module._extentions, len = arr.length; i < len; i++) {
         let newPath = `${p}${arr[i]}`
         //如果访问的文件不存在, 就会发生异常
         try {
            fs.accessSync(newPath)
            return newPath
         } catch(e) {}
      }
   } else {
      // 3.如果没有文件名,则进行模块化加载:查找同名文件夹下的package.json || index.js
      // 这里没有做处理,只是做了防止报错
      try {
         fs.accessSync(p)
         return p
      } catch(e) {}
   }
}

13.1.2 CommonJS全局对象介绍

  1. require(filename) 表示加载某一个模块,加载规则如下

    • 路径规则:自定义模块必须使用相对路径或者绝对路径。如果只写文件名,表示是内置模块
    • 文件名规则:文件名的js可以省略
  2. exports 表示一个对module.exports的引用,即module.exports === exports为true。模块在导出时导出的是module.exports的栈内存地址,而不是exports的栈内存地址。这就让人想到了在“原型链”中总结到的原型的动态性,实例化对象的__proto__指针和构造函数的prototype指针与这里的module.exports指针和exports指针的情况有些相似的地方。 这里也可以举出很多例子来。


    举例一: 修改module.exports的堆内存

    // A.js
    console.log(require("./B.js")) // msg,name,gender都会输出
    
    // B.js
    module.exports.msg = "hello"
    exports.name = "Danny"
    exports.gender = "man"
    

    举例二:重写module.exports的堆内存

    // A.js
    console.log(require("./B.js")) // 只会输出msg
    
    // B.js
    // module.exports的堆内存被重写了,即新开辟了堆内存。下面exports仍然在旧的堆内存上修改,这些修改不会作用到最后的结果上去。
    module.exports = {
        msg: "hello"
    }
    exports.name = "Danny"
    exports.gender = "man"
    
  3. module 该对象主要使用module.exports进行模块内容导出,使用module.exports导出的内容会被其它引用它的模块接收(可以参考13.1.1中提到的模块加载机制)

  4. __dirname 表示当前文件所在的文件夹的绝对路径

  5. __filename 表示当前文件所在的绝对路径,包含文件名

13.1.3 CommonJS的使用

NodeJS实现了CommonJS规范,可以直接使用上述提到的所有机制。在客户端中不能直接使用CommonJS规范,需要借助webpack等打包工具实现上述机制。

13.2 AMD规范

13.2.1 AMD的实现机制

AMD的实现机制和CommonJS的实现机制大同小异,也分为以下三点。

1. 模块加载器机制: 与CommonJS类似,可以在github中找到对应的实现(require.js)

2. 包裹机制: moduleName表示当前模块的名称,不写moduleName会成为匿名模块,文件名为模块名。requireModules表示要异步引入的模块的名称,这些模块的工厂函数返回值将会作为当前模块的全局变量。function是一个工厂函数,工厂函数是需要返回内容,这个返回的内容将会被引用该模块的模块接收到,在下面例子中提到工厂函数也可以模仿CommonJS的动态加载机制。

// requireModules也可以是require和modules等等,这样就可以在AMD规范中实现一个CommonJS规范的风格
define(moduleName, [...requireModules], function(...requireModules) {
    return {
        msg: "hello"
    }
})

3. 异步加载机制: 与CommonJS类似,只不过换成了异步加载

13.2.2 AMD与CommonJS的相同与不同

1. 不同点: AMD是“异步加载机制”更关注于客户端JavaScript,CommonJS在上面提到是“同步加载机制”更关注于服务端JavaScript。

2. 相同点: CommonJS是NodeJS的默认模块机制,如果想在NodeJS使用AMD需要通过npm或yarn安装。CommonJS和AMD都是使用了“模块加载器”机制。

13.3 CMD规范

13.3.1 CMD规范的实现机制

CMD的实现机制和AMD和CommonJS实现大同小异,也分为以下三点。

1. 模块加载器机制: 与CommonJS类似,可以在github中找到对应的实现(sea.js)

2. 包裹机制: 包裹机制和AMD太相似了,但是CMD默认为你提供了一个CommonJS风格的模块。CMD虽然可以像AMD一样在模块创建时在第二个参数写依赖模块,但是提供的CommonJS风格的模块还是希望你使用动态模块加载机制,使用require在用到模块时再加载。

define(moduleName, [...requireModules], function(require, exports, module) {
    return {
        msg: "hello"
    }
})

3. 加载机制: 可以在第二个参数中写依赖的模块,就像模仿AMD规范一样,也可以使用CommonJS风格,动态加载模块。

13.4 UMD规范

13.4.1 UMD规范的实现

UMD规范主要是为了整合上述规范。实现思路是,模块创建时会自动检测define,module的类型情况,以此来判断该使用哪一种模块。

13.5 ES6模块规范

13.5.1 ES6模块机制的使用

注:起始ES6的模块机制的export和import有很多种有意思的用法,这里仅列举出常用的,不涉及一些组合用法。

  1. 导出export:

    • export 后跟{},大括号中写要一次性导出的多个已声明的变量。这很像ES6增强的对象写法,实际上这不是对象。
    • export 后跟变量声明语句,表示一次导出一个变量。
    • export default 后跟变量。export default一个模块中只能有一个,export可以有多个。
  2. 导入import:

    • import {} from ‘filename’。大括号中填写export导出的变量名,要保持一致。filename必须带上js,必须使用绝对路径或者相对路径,否则会被当做内置模块。
    • import 自定义名称 from ‘filename’。如果是接收export default的导出,需要自定义一个名称。
    • import ‘filename’。仅将文件执行一遍。
    • import(‘filename’).then(module => {})。异步加载模块,加载完成后模块将作为promise的兑现值。可以通过module.default或者module.xxx访问模块导出的内容。

13.5.2 ES6模块机制和CommonJS模块机制的区别

  1. CommonJS模块输出的是值的拷贝,ES6模块输出的是值的引用,是只读的。 当被引用的模块中的变量发生变化时,CommonJS规范下导出的该变量不会变化,ES6规范下导出的该变量会变化。因为只读的特性,ES6模块输出值的栈内存不可更改,堆内存可以更改,CommonJS则是都可以修改。

CommonJS模块输出测试

// test.js
let {a} = require("./server.js")
console.log(a)      // 1
setTimeout(() => {
    console.log(a)  // 1
}, 3000)

// server.js
let a = 1
setTimeout(() => {
   a = 3
}, 2000)
exports.a = a

ES6模块测试

// test.js
import {a} from "./server.js"
console.log(a)
setTimeout(() => {
    console.log(a)
}, 3000)

// server.js
let a = 1
setTimeout(() => {
   a = 3
}, 2000)
export { a }
  1. CommonJS是运行时加载,ES6是编译时加载。

CommonJS: CommonJS模块是对象。在遇到require时,会加载整个模块。模块导出值集中到module.exports暴露出去。

ES6: ES6模块不是对象。在遇到import时会去找导出的变量,而不是加载整个模块。export+{}表示显示输出代码,而不是将变量集中到某个变量中去。

  • 14
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vanghua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值