飞道的博客

(已更新3.1w字)《大前端吊打面试官系列》 之 ES6 精华篇

184人阅读  评论(0)

📚更新日志

文中所有修改或补充内容,会在日志中实时更新。

  • 2020/01/7 开始决定写近十几万字前端面试系列,规划整个系列目录提纲。
  • 2020/01/8 写完部分“面试官到底考察你什么”内容。
  • 2020/01/9 继续完善”面试官到底考察你什么“内容。
  • 2020/01/11 开始写准备简历部分。
  • 2020/01/13 完善面试前的准备内容。
  • 2020/01/14 对面试准备内容做一次整体的优化。
  • 2020/01/15 开始写 JS 系列部分。
  • 2020/01/16 写数据类型中的七大模块部分 。
  • 2020/01/17 写 this、闭包等 JS 重点部分。
  • 2020/01/30 写消息循环机制
  • 2020/02/03 添加配图以及动画演示
  • 2020/02/02 补充 new 的实现原理
  • 2020/02/03 补充继承等知识内容,以及一些参考文献
  • 2020/02/04 补充垃圾回收机制等知识内容
  • 2020/02/05 补充深浅拷贝等知识内容
  • 2020/02/07 开始整理 ES6 的面试内容
  • 2020/02/08 补充 var、let、const 等内容
  • 2020/02/09 完善 proxy 代理知识点
  • 2020/02/10 整理 ES6/7 中的异步编程
  • 2020/02/11 更新 generator 生成器
  • 2020/02/12 更新 Promise 内容
  • 2020/02/14 更新模块化内容
  • 2020/02/16 更新 HTML5/CSS3 常见面试知识点
  • 持续更新中…

已更新完的前两篇内容传送门:

🔥 《大前端吊打面试官系列》 之面试备战篇(近几十万字持续更新中,建议收藏~)
🔥 动画:《大前端吊打面试官系列》 之原生 JavaScript 精华篇

目录

ES6 基础知识点

变量提升

面试官:什么是变量提升?为什么存在变量提升?

变量提升就是该变量还未被声明,但是却可以使用未声明的变量。

虽然描述为提升,但不是真正的将代码提升到顶部,而是在代码执行前,先在词法环境中进行了注册。

如果没有变量提升,下方代码就无法执行:

function fn1(){
    fn2();
}

function fn2(){
    fn1();
}

fn1();

变量提升的根本原因就是为了解决函数之间互相调用的情况。

面试官:变量和函数怎么进行提升的?优先级是怎么样的?

  • 第一阶段:对所有函数声明进行提升(忽略表达式和箭头函数),引用类型的赋值(函数的声明,函数的提升代表着可执行,因为提升之后保持着引用)分为三步:
    • 开辟堆空间
    • 存储内容
    • 将地址赋值给变量
  • 第二阶段:对所有的变量进行提升,全部赋值为 undefined(如果已经存在,不赋值为undefined)。然后依次顺序执行代码。

PS:使用 let 和 const 时,我们不能在声明之前使用变量,这叫做暂时性死区。

var、let、const

面试官:var、let、const 三者的区别什么?

  • var 存在变量提升,而 let、const 则不会。
  • var 声明的变量会挂载到 window 上,而其他两者不会。
  • letconst 的作用基本一致,后者声明的变量不能再次赋值(注意:但是能改变值)

map、filter、reduce

这三个的区别经常出现于面试中,同时在实际项目中也是会频繁的使用,从如何使用到什么业务逻辑下使用是非常重要滴~

面试官:说说 map、filter、reduce三者的使用以及区别是什么?

map

map 的作用是 map 中传入一个函数,该函数会遍历该数组,对每一个元素做变换之后返回新数组。

  • element : 对应数组的每个元素。
  • index: 数组元素的下标。
  • arr : 原数组。
let arr = [2,3,4]
arr = arr.map(function(element,index,arr){
    return arr[index]+1;
}) // [3,4,5]

filter

filter 的作用是也是生成一个数组,传入的函数返回值确实布尔类型,返回值为 true 的元素放入新数组,通常来筛选删除不需要的元素。

  • element : 对应数组的每个元素。
  • index : 数组元素的下标。
  • arr : 原数组。
let array = [1, 2, 4, 6]
let arr = array.filter(function(element){
	return element != 6;
})// [1,2,4]

reduce

reduce 可以将数组中的元素通过回调函数最终转换为一个值。

  • acc:累计值(第一次的值代表初始化的值)。
  • element: 当前元素。
  • index: 当前索引。
  • arr: 原数组。
let arr = [1,2,3]
let sum = arr.reduce(function(acc,element){
    return acc + element;
},0) // 6

Proxy

对于 Proxy 代理我们从最基本的 gettersetter 说起~

面试官:通过 getter 与 setter 访问属性值有什么好处?代理与 getter 和 setter 的主要区别是什么?

我们为什么会使用 gettersetter 来进行控制对值的访问呢,有以下是三个作用:

  • 避免意外的错误发生。
  • 需要记录属性的变化:比如属性值的访问日志记录。
  • 数据绑定:在 vue 中使用的数据的双向绑定。

对于有哪几种方式来定义 gettersetter 呢?

  • 字面量定义;
  • ES6 中的 Class 定义;
  • 使用 Object.definedProperty 方法;

字面量定义

对象访问属性通常隐式调用 gettersetter 方法,属性自动关联 gettersetter 方法。这些都是标准方法,在访问属性的时候都是立即执行的。

const collection = {
	name='xiaolu',
	
	// 读取属性
	get name(){
		return this.name;
	}
	// 设置属性
	set name(value){
		this.name = value;
	}
}
collection.name 			// 隐士调用 getter 方法
collection.name = "xiaolu1" // 隐士调用 setter 方法

ES6 中的 Class 定义

// calss 定义 setter 和 getter
class Xiaolu {
    constructor(){
        this.name = 'xiaolu'
    }
	
    get firstXiaolu(){
        console.log('属性已访问')
        return this.name;
    }
	
    set firstXiaolu(value){
        this.name = value;
    }
}

const x = new Xiaolu();
x.firstXiaolu

Object.definedProperty()

对象的字面量与类、gettersetter 方法不是在一同一作用域定义的,因此那些希望作为私有变量属性的标量是无法实现的。

// Object.definedProperty
function xiaolu(){
    let count = 0;
	
    Object.defineProperty(this,'skillLevel',{
        get:() => {
            return count;
        },
        set:value => {
            count = value;
        }
    })
}
// 隐士的调用 get 方法
console.log(xiaolu.count)

通过 Object.definedProperty() 创建的 getset 方法,与私有的变量处于相同的作用域中,getset方法分别创建了含有私有变量的闭包。

Proxy 代理

面试官:说说 ES6 中的 Proxy 和 getter、setter 的区别?

代理 proxyES6 新提出的。代理(proxy)使我们通过代理控制对另一个对象的访问。proxygetter 以及 setter 的区别是,gettersetter 仅仅控制的是单个对象属性,而 proxy 代理的是对象交互的通用处理,包括对象的方法。

用法:

  • **target:**要进行代理的对象。
  • **handler:**是一个特定的对象,定义了特定行为时触发的函数。
var proxy = new Proxy(target, handler);

getset 的参数:

  • target: 传入的对象。
  • key: 属性。
  • value: 要赋的值。
const obj = {name:'xiaolu'}
const representtive = new Proxy(obj, {
    get: (target, key) =>{
        return key in target ? target[key]:"不存在该值“
    },
    set: (target, key, value)=>{
        target[key] = value;
    }
})

我们试图想使用代理访问对象时,此时会触发 get 方法。或者试图将代理的对象进行赋值时,会触发调用 set方法。

面试官:说说 Proxy 的基本应用?

最基本的应用如下一个:

  • 日志记录 —— 当访问属性时,可以在 get 和 `set 中记录访问日志。
  • 校验值 —— 有效的避免指定属性类型错误的发生。
  • 定义如何计算属性值 —— 每次访问属性值,都会进行计算属性值。
  • 数据的双向绑定(Vue)—— 在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。
// 校验值
function xiaolu(){
    let count = 0;
	
    Object.defineProperty(this,'skillLevel',{
        get:() => {
            return count;
        },
        set:value => {
            if(!Number.isInteger(value)){
                throw new TypeError("抛出错误")
            }
            count = value;
        }
    })
}
// 定义如何计算属性值 
const collection = {
    name:'xiaolu',
    age:'2',
    // 读取属性
    get getName(){
        return this.name + " "+this.age;
    },
    // 设置属性
    set setName(value){
        this.name = value;
    }
}

console.log(collection.getName)

ES6/7 的异步编程

在之前的 js 基础面试知识一篇中,我们说到了异步编程,留一下一个传送门,直接传送到这里了。上回说到,要想解决函数回调地狱问题,我们要用到 ES6/ES7中的一些内容,在这一节,小鹿会详细的分享~

Generator 生成器

面试官:说说 Generator 是如何使用的?以及各个阶段的状态是如何变化的?

使用生成器函数可以生成一组值的序列,每个值的生成是基于每次请求的,并不同于标准函数立即生成。

调用生成器不会直接执行,而是通过叫做迭代器的对象控制生成器执行。

function* WeaponGenerator(){
    yield "1";
    yield "2";
    yield "3";
}

for(let item of WeaponGenerator()){
    console.log(item);
}
//1
//2
//3

使用迭代器控制生成器。

  • 通过调用生成器返回一个迭代器对象,用来控制生成器的执行。
  • 调用迭代器的 next 方法向生成器请求一个值。
  • 请求的结果返回一个对象,对象中包含一个value值和 done布尔值,告诉我们生成器是否还会生成值。
  • 如果没有可执行的代码,生成器就会返回一个 undefined 值,表示整个生成器已经完成。
function* WeaponGenerator(){
    yield "1";
    yield "2";
    yield "3";
}

let weapon = WeaponGenerator();
console.log(weapon.next());
console.log(weapon.next());
console.log(weapon.next());

状态变化如下:

  • 每当代码执行到 yield 属性,就会生成一个中间值,返回一个对象。
  • 每当生成一个值后,生成器就会非阻塞的挂起执行,等待下一次值的请求。
  • 再次调用 next 方法,将生成器从挂起状态唤醒,中断执行的生成器从上次离开的位置继续执行。
  • 直到遇到下一个 yield ,生成器挂起。
  • 当执行到没有可执行代码了,就会返回一个结果对象,value 的值为 undefined, done 的值为 true,生成器执行完成。

面试官:说说 Generator 内部结构实现?

生成器更像是一个状态运动的状态机。

  • 挂起开始状态——创建一个生成器处于未执行状态。
  • 执行状态——生成器的执行状态。
  • 挂起让渡状态——生成器执行遇到第一个 yield 表达式。
  • 完成状态——代码执行到 return 全部代码就会进入全部状态。

执行上下文跟踪生成器函数。

function* WeaponGenerator(action){
    yield "1"+action;
    yield "2";
    yield "3";
}

let Iterator = WeaponGenerator("xiaolu");
let result1 = Iterator.next()
let result2 = Iterator.next()
let result3 = Iterator.next()
  • 在调用生成器之前的状态——只有全局执行上下文,全局环境中除了生成器变量的引用,其他的变量都为 undefined
  • 调用生成器并没有执行函数,而是返回一个 Iterator迭代器对象并指向当前生成器的上下文。
  • 一般函数调用完成上下文弹出栈,然后被摧毁。当生成器的函数调用完成之后,当前生成器的上下文出栈,但是在全局的迭代器对象还与保持着与生成器执行上下文引用,且生成器的词法环境还存在。
  • 执行 next 方法,一般的函数会重新创建执行上下文。而生成器会重新激活对应的上下文并推入栈中(这也是为什么标准函数重复调用时,重新从头执行的原因所在。与标准函数相比较,生成器暂时会挂起并将来恢复)。
  • 当遇到 yield关键字的时候,生成器上下文出栈,但是迭代器还是保持引用,处于非阻塞暂时挂起的状态。
  • 如果遇到 next 指向方法继续在原位置继续 执行,直到遇到 return 语句,并返回值结束生成器的执行,生成器进入结束状态。

Promise

面试官:说说 Promise 的原理?你是如何理解 Promise 的?

在深入 Promise 之前,我们先想想 Why(为什么)会有 PromisePromise 的诞生解决了哪些问题呢?

小鹿总结到的原因有两方面,第一,由于 JS 的运行时单线程的,所以当执行耗时的任务时,就会造成 UI 渲染的阻塞。当前的解决方法是使用回调函数来解决这个问题,当任务执行完毕会,会调用回调方法。

第二就是回调函数存在以下几个缺点:

  • 不能捕捉异常(错误处理困难)——回调函数的代码和开始任务代码不在同一事件循环中;
  • 回调地域问题(嵌套回调);
  • 处理并行任务棘手(请求之间互不依赖);

实现一个简单的 Promise:

let promise = new Promise(function(resolve, reject) {
    resolve();
});

promise.then((res)=>{
    console.log("回调成功!")
},(err) =>{
    console.log("回调失败!")
});
  • 通过内置的 Promise 构造函数可以创建一个 Promise 对象,构造函数中传入两个函数参数:resolvereject。两个参数的作用是,在函数内手动调用 resolve 的时候,就说明回调成功了;调用 reject 说明调用失败。通常在 promise 中进行耗时的异步操作,响应是否成功,我们根据判断就可以调用对应的函数。
  • 调用 Promise 对象内置的方法 then,传入两个函数,一个是成功回调的函数,一个失败回调的函数。当再 promise 内部调用 resolve 函数时,之后就会回调 then 方法里的第一个函数。当调用了 reject 方法时,就会调用then 方法的第二个函数。
  • promise 相当于是一个承诺,当承诺兑现的时候(调用了 resolve 函数),就会调用 then 中的第一个回调函数,在回调函数中做处理。当承诺出现未知的错误或异常的时候(调用了 reject 函数),就会调用 then 方法的第二个回调函数,提示开发者出现错误。

Promise 的状态:

其实 Promise 对象用作异步任务的一个占位符,代表暂时还没有获得但在未来获得的值。

Promise 共有三种状态,完成状态和拒绝状态状态都是由等待状态转变的。一旦 Promise进入了拒绝或完成状态,它的状态就不能切换了。

  • 等待状态(pending)
  • 完成状态(resolve)
  • 拒绝状态(reject)

Promise 共有两种拒绝状态:显示拒绝(直接调用 reject)和隐式拒绝(抛出异常)。

一个 Promise 实例:

客户端请求服务器是最常用的异步任务,下面实现一下。

function getJson(url){
    // 返回一个 promise 对象
    return new Promise((resolve,reject)=>{
        const request = new XMLHttpRequest();
        request.open("GET",url);

        // 服务器响应成功
        request.onload = function(){
            //try-catch 考虑到 JSON 可能解析会出现错误
            try{
                // 响应码正确且 JSON 解析正确
                if(this.status == 200){
                    // 调用成功承诺
                    resolve(JSON.parse(this.response))
                }else{
                    // 调用失败承诺
                    reject(this.status + " " +this.statusText);
                }
            }catch(e){
                // 调用失败承诺
                reject(e.message)
            }
        }

        // 服务器响应失败
        request.onerror = function(){
            // 调用失败承诺
            reject(this.status + " " +this.statusText);
        }

        // 发送请求
        request.send();

    })
}

// 调用上述异步请求
getJson("url").then((res)=>{
    console.log("解析的JSON数据为:"+res)
},(res)=>{
    console.log("错误信息:"+res)
})

面试官:什么是 Promise 链?说说 Promise 如何使用的?

promise可以实现链式调用,每次then 之后返回的都是一个promise对象,可以紧接着使用 then继续处理接下来的任务,这样就实现了链式调用。如果在 then 中使用了return,那么 return 的值也会被Promise.resove()包装。

Promise.resolve(1)
  .then(res => {
    console.log(res) // => 1
    return 2 // 包装成 Promise.resolve(2)
  })
  .then(res => {
    console.log(res) // => 2
  })

Promise 最常用的使用方式有以下几个方式:

嵌套任务处理:

// 链式回调
getJson("url")
    .then(n => {getJson(n[0].url)})
    .then(m => {getJson(m[0].url)})
    .then(w => {getJson(w[0].url)})
    .catch((error => {console.log("异常错误!")}))
  • 因为 then方法会返回一个 promise 对象,所以连续调用 then 方法可以进行链式调用 promise
  • 多个异步任务中可能出现错误,只需要调用一个 catch 方法并向其传入错误处理的回调函数。

并行处理任务:

上述的链式调用主要处理的是多个异步任务之间存在依赖性的,如果同时执行多个异步任务,就是用 promise 中的 all 方法。

 // 并行处理多个异步任务
Promise.all([getJson(url),
             getJson(url),
             getJson(url)]).then(resule => {
                 // 如果三个请求都响应成功
                 if(result[0] == 1 && result[1] == 1 && result[2] == 1){
                     console.log("请求成功!")
                 }
             }).catch(error => {console.log("异常错误!")})
  • 使用 Promise.all()方法进行异步请求,将多个请求任务封装数组进行同步请求。
  • 返回的结果值会打包成一个数组,可以通过数组的下标获取值对返回的结果进行判断。
  • 只有全部请求成功才会进入成功的方法,否则就会调用 catch 抛出异常。
  • Promise.race()方法不同的是,race方法只要其中一个返回成功,就会调用成功的方法。

面试官:说说怎么实现一个 Promise?

我们根据 Promise 的执行顺序,手动实现一个 Promise

  • 先执行 MyPromise 构造函数;
  • 注册 then 函数;
  • 此时的 promise 挂起,UI 非堵塞,执行其他的同步代码;
  • 执行回调函数。
// 三种状态
const PENDING = "pending";
const RESOLVE = "resolve";
const REJECT = "reject";

// promise 函数
function MyPromise(fn){
    const that = this;           // 回调时用于保存正确的 this 对象
    that.state = PENDING;        // 初始化状态
    that.value = null;           // value 用于保存回调函数(resolve/reject 传递的参数值)
    that.resolvedCallbacks = []; // 用于保存 then 中的回调
    that.rejectedCallbacks = [];

    // resolve 和 reject 函数 
    function resolve(value) {
        if(that.state === PENDING){
            that.state = 'resolve';
            that.value = value;
            that.resolvedCallbacks.map(cb => cb(that.value));
        }
    }

    function reject(value) {
        if (that.state === PENDING) {
            that.state = 'reject'
            that.value = value
            that.rejectedCallbacks.map(cb => cb(that.value))
        }
    }

    // 实现如何执行 Promise 中传入的函数
    try {
        fn(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

// 实现 then 函数
MyPromise.prototype.then = function(onResolved, onRejected) {
    const that = this;
    // 判断两个参数是否为函数类型(如果不是函数,就创建一个函数赋值给对应的参数)
    onResolved = typeof onResolved === 'function' ? onResolved : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : r => {throw r}

    // 判断当前的状态
    if (that.state === 'pending') {
        that.resolvedCallbacks.push(onResolved)
        that.rejectedCallbacks.push(onRejected)
    }
    if (that.state === 'resolve') {
        onResolved(that.value)
    }
    if (that.state === 'reject') {
        onRejected(that.value)
    }
}

new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(1)
    }, 0)
}).then(value => {
    console.log(value)
})

async 及 await

面试官:async 及 await 和 Generator 以及 Promise 什么区别?它们的优点和缺点分别是什么?await 原理是什么?

其实 ES7 中的 asyncawait就是 Generator 以及 Promise 的语法糖,内部的实现原理还是原来的,只不过是在写法上有所改变,这些实现一些异步任务写起来更像是执行同步任务。

async 及 await 的特点:

一个函数前加上 async关键字,就将该函数返回一个 Promiseasync 直接将返回值使用 Promise.resolve() 进行包裹(与 then 处理效果相同)。

await 只能配套 async 使用,await 内部实现了 generatorawait 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator

async 和 await 的优缺点?

  • 内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
  • 更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
  • 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

缺点:

因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。

await 原理是什么?

async 函数的实现,就是将 Generator函数和自动执行器,包装在一个函数里。

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    var gen = genF();
    function step(nextF) {
      try {
        var next = nextF();
      } catch(e) {
        return reject(e); 
      }
      if(next.done) {
        return resolve(next.value);
      } 
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });      
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

模块化

面试官:为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?

模块化解决了命名冲突问题,可以提高代码的复用率,提高代码的可维护性。

模块化的好处:

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

方式一:函数

最起初,实现模块化的方式使用函数进行封装。将不同功能的代码实现封装到不同的函数中。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。

function a(){
  // 功能二
}
function b(){
  // 功能一
}

**缺陷:**容易发生命名冲突或者数据的不安全性。

方式二:立即执行函数

立即执行函数中的匿名函数中有独立的 词法作用域,避免了外界访问此作用域的变量。通过函数作用域解决了命名冲突、污染全局作用域的问题

// module.js文件
(function(window) {
  let name = 'xiaolu'
  // 暴露的接口来访问数据
  function a() {
    console.log(`name:${name}`)
  }
  //暴露接口
  window.myModule = { a } 
})(window)
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.name = 'xixi' // 无法访问
    myModule.foo()  // name:xiaolu
</script>

方式三:CommonJS

  • CommonJS 的规范主要用在 Node.js 中,为模块提供了四个接口:module、exports、require、global
  • CommonJS 用同步的方式加载模块(服务器端),在浏览器端使用的是异步加载模块。
// lib.js
var counter = 3;
function incCounter() {
    counter++;
}
// 对外暴露接口
module.exports = {
    counter: counter,
    incCounter: incCounter,
};
// 加载外部模块
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
// 原始类型的值被缓存,所以就没有被改变(commonJS 不会随着执行而去模块随时调用)
console.log(mod.counter); // 3

加载机制:

CommonJS 模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

方式四:AMD 和 CMD

有关 AMD 和 CMD 就不多说了,详细请看这篇文章。

AMD 和 CMD 的区别有哪些?

面试官:说说 ES6 中的模块化?CommonJS 和 ES6 模块化的区别?

方式五:ES6 Moudle

ES6 实现的模块非常简单,用于浏览器和服务器端。mport命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码 .主要有两个命令组成:exportimport:

  • export命令用于规定模块的对外接口
  • import命令引入其他模块的功能。
// 指定指定的值暴露对外的接口
export let counter = 3;
export function incCounter() {
  counter++;
}

// 加载模块中的某个值
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
// ES6 模块不同的是,静态加载完毕之后,每执行到模块中的方法,就去模块内调用(外部的变量总是与模块进行绑定的),而且值不会被缓存。
console.log(counter); // 4

ES6 模块与 CommonJS 模块的区别:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    • 所谓值的拷贝,原始类型的值被缓存,不随模块内部的改变而改变。
    • ES6 模块是动态引用不缓存值,模块内外是绑定的,而且是只读引用,不能修改值。ES6 的 js 引擎对脚本静态分析的时候,遇到加载命令模块 import ,就会生成一个只读引用,当真正用到模块里边的值的时候,就会去模块内部去取。
  • CommonJS 模块是运行时加载,ES6 模块是编译时加载输出接口。
    • 运行时加载CommonJS 模块就是对象;是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
    • **编译时加载:**ES6 模块不是对象,而是通过 export 命令「显式指定输出的代码」。import 时采用静态命令的形式,即在import指定「加载某个输出值」,而「不是加载整个模块」,这种加载称为“编译时加载”。

小结

  • CommonJS 规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • AMD 规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD 规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD 规范与 AMD 规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在 Node.js 中运行。不过,依赖 SPM 打包,模块的加载逻辑偏重。
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

本系列持续更新中…

❤️ 最后不要忘记三连哦~ [点赞 + 收藏 + 评论]!

如果觉得文章不错,希望你能给小鹿的文章轻轻的点个赞,希望能够更多的面试者带来帮助,谢谢你!

1、也可以扫描下方二维码,后台回复「进群」,即可加入小鹿的技术交流群哦~

2、获取一份小鹿自学的资料,后台回复:「资料」,精心整理的分享给你~

3、两年来,小鹿在公众号每天都会写原创和分享一些互联网的编程随想!

欢迎关注,写的不好再取关哦~

参考文献:

1、https://www.zhihu.com/question/20351507/answer/14859415

2、https://zhuanlan.zhihu.com/p/26625636

3、https://github.com/seajs/seajs/issues/588


转载:https://blog.csdn.net/qq_36903042/article/details/104353908
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场