likes
comments
collection
share

2024金三银四前端面试划重点了,带你盘一下面试官喜欢问的那些常用看似很像但是核心原理有区别的知识点,讲清楚了就是逢面必过

作者站长头像
站长
· 阅读数 2

程序员的金三银四求职宝典

又到了一年一度的金三银四,前端的孩子们太难了,大环境不好,不好好学习,生产队的驴也会下岗,别闲着了,这篇文章给你面试划重点了!!!!!

1.必问一:const,var,let的区别

  • 只有var可以垮块访问,其他都不能垮块访问。 var也可以重新声明和更新变量,也可以提升到其作用域的顶部。 三个都不能垮函数访问 let声明变量更安全,在声明之前尝试使用 let 变量,会得到一个 Reference Error。

  • var 声明是全局作用域或函数作用域,而 let 和 const 是块作用域。var 变量可以在其作用域内更新和重新声明; let变量可以更新但不能重新声明;const 变量既不能更新也不能重新声明。 它们都被提升到了作用域的顶部。但是,var 变量是用 undefined 初始化的,而 let 和 const 变量不会被初始化。 var 和 let 可以在不初始化的情况下声明,而const 必须在声明时初始化。

1.var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。 2.let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。 3.const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。 4.同一个变量只能使用一种方式声明,不然会报错。

  • 代码实例,块作用域{}中的区别,加深理解
    //原先js作用域有全局作用域,函数作用域,es6新增了块级作用域{},if和for的{}也是块级作用域

    {
        var a = 1;
        console.log(a); // 1
    }
    console.log(a); // 1
    // 可见,通过var定义的变量可以跨块作用域访问到。

    (function A() {
        var b = 2;
        console.log(b); // 2
    })();
    // console.log(b); // 报错,
    // 可见,通过var定义的变量不能跨函数作用域访问到

    if(true) {
        var c = 3;
    }
    console.log(c); // 3
    for(var i = 0; i < 4; i++) {
        var d = 5;
    };
    console.log(i); // 4   (循环结束i已经是4,所以此处i为4)
    console.log(d); // 5
    // if语句和for语句中用var定义的变量可以在外面访问到,
    // 可见,if语句和for语句属于块作用域,不属于函数作用域。

    {
        var a = 1;
        let b = 2;
        const c = 3;    

        {
            console.log(a);     // 1    子作用域可以访问到父作用域的变量
            console.log(b);     // 2    子作用域可以访问到父作用域的变量
            console.log(c);     // 3    子作用域可以访问到父作用域的变量

            var aa = 11;
            let bb = 22;
            const cc = 33;
        }

        console.log(aa);    // 11   // 可以跨块访问到子 块作用域 的变量
        // console.log(bb); // 报错   bb is not defined
        // console.log(cc); // 报错   cc is not defined
    }


    console.log (greeter);
    var greeter = "say hello"
    //undefined

   //var变量的提升到作用域的顶部,并使用undefined值进行初始化
    var greeter;
    console.log(greeter); // greeter is undefined
    greeter = "say hello"
    //'say hello'

2.必问二:bind、call、apply的区别

先看一下bind、call、apply的代码用法,

var a = {
    user:"良人",
    fn: function(arg1, arg2) {
        console.log(this.user)  // 良人
        console.log(arg1+ arg2) // 2
    }
}
var b = a.fn

1、bind的用法

var c = b.bind(a) // 返回一个已经切换this指向的新函数
c(1,1)

2、apply的用法

b.apply(a, [1, 1])  // 将b添加到a环境中

第一个参数是this指向的新环境 第二个参数是要传递给新环境的参数 注意: 第一个参数为null时表示指向window

3、call的用法

b.call(a, 1, 1) // 将b添加到a环境中

第一个参数是this指向的新环境 第二、三...个参数是传递给新环境的参数 注意: 第一个参数为null时表示指向window

总结只有apply参数是数组,bind方法可以让函数想什么时候调用就什么时候调用。apply、call方法只是临时改变了this指向

下面来一道笔试题深入理解一下:

console.log.call.apply(a => a, [1,2])输出结果是2

3. 必问三:谈谈ajax,axios和fetch的区别

  • ajax基于xmlHttpRequest的请求,
  • Axios是对XMLHttpRequest的封装,
  • Fetch是一种新的获取资源的接口方式,并不是对XMLHttpRequest的封装。

总结:最大的不同点在于Fetch是浏览器原生支持,而Axios需要引入Axios库。 一般用的较多的是axios,比如拦截取消请求等,功能丰富一些。

4.必问四:Cookie、LocalStorage、SessionStorage和IndexedDB四种存储方式的区别

其中只有cookie由服务器操作,其他都是在客户端操作,下面从4个方面展开叙述一下他们区别

  • 数据容量

cookie:4kb localstorage和sessionstorage:5-10MB indexedDB:50-100MB(谷歌),取决于浏览器和操作系统

  • 生命周期

cookie:可以设置过期时间,可以在浏览器会话之间持久保存数据 localstorage:长期存储 indexedDB:长期存储 sessionstorage:浏览器会话期间有效,关闭页面就没了

  • 数据访问和安全

cookie在每次http请求头中会被发送到服务器,增加网络流量,可以设置路径,域名和安全标志限制访问,但容易被垮站点脚本攻击。 localstorage,indexedB,sessionstorage:在同源策略是安全的,在客户端,不会发送到服务器

  • 功能

cookie:用于会话跟综和存储少量数据 localstorage和sessionstorage:提供了基本的键值对存储,适用于存储较大的数据。 indexDB:提供了强大的查询和搜索,支持事务操作,适合存储和操作大量结构化数据。可用于1.收集用户日志,2.缓存请求数据,3.大文件上传,文件分片存储到indexDB库,动态更新上传状态,异常状态可以定位问题再重新上传。

具体实例代码如下: -------- cookie ---------------

// 设置Cookie
document.cookie = 
"username=lisi; expires=Thu, 15 Dec 2023 16:00:00 UTC; path=/";

// 读取Cookie
let cookies = document.cookie;
console.log(cookies);

// 删除Cookie,设置Cookie的过期时间为过去的时间
document.cookie = 
"username=; expires=Thu, 01 Jan 1980 00:00:00 UTC; path=/;";

-------- LocalStorage -------------

// 设置LocalStorage 
localStorage.setItem("key", "value");
localStorage.setItem("username", "lisi");

// 读取LocalStorage 
const value = localStorage.getItem("key");
console.log(value);
let username = localStorage.getItem("username"); 
console.log(username);

// 删除LocalStorage 
localStorage.removeItem("username");

------- LocalStorage (非关系型数据库)-----------

// 设置SessionStorage
sessionStorage.setItem("key", "value");

// 读取SessionStorage
const value = sessionStorage.getItem("key");
console.log(value);

// 删除SessionStorage
sessionStorage.removeItem("key");

-------- indexedDB -------------------

// 打开或创建数据库
let request = indexedDB.open("myDatabase", 1);
// 创建对象存储空间
request.onupgradeneeded = function(event) {
  let db = event.target.result;
  let objectStore = db.createObjectStore("people", { keyPath: "id" });
};
// 添加数据
request.onsuccess = function(event) {
  let db = event.target.result;
  let transaction = db.transaction(["people"], "readwrite");
  let objectStore = transaction.objectStore("people");
  let people = { id: 1, name: "lisi" };
  objectStore.add(people);
};
// 检索数据
request.onsuccess = function(event) {
  let db = event.target.result;
  let transaction = db.transaction(["people"], "readonly");
  let objectStore = transaction.objectStore("people");
  let request = objectStore.get(1);
  request.onsuccess = function(event) {
    let people = event.target.result;
    console.log(people.name);
  };
};
// 删除数据
request.onsuccess = function(event) {
  let db = event.target.result;
  let transaction = db.transaction(["people"], "readwrite");
  let objectStore = transaction.objectStore("people");
  let request = objectStore.delete(1);
};

5.必问五:for in 和for of的区别

先举两个例子结合代码看,理解更深入

  • 例1:遍历Object{}对象时
const obj = {
    a: 1,
    b: 2,
    c: 3
}
for (let i in obj) {
    console.log(i)
    // a
    // b
    // c
}
for (let i of obj) {
    console.log(i)
    // Uncaught TypeError: obj is not iterable 报错了
    // 一个数据结构只要部署了 Symbol.iterator 属性, 就被视为具有 iterator接口, 就可以使用 for 	
    //of循环。没有 Symbol.iterator这个属性,所以使用 for of会报 obj is not iterable
    
}
  • 例2: 遍历数组时
 const arr = 'abc'//['a','b','c']
    // for in 循环
    for (let i in arr) {
        console.log(i)
        // 0
        // 1
        // 2
    }
    
    // for of
    for (let i of arr) {
        console.log(i)
        // a
        // b
        // c
    }

总结

for in

  • 对象就是key,数组就是下标,字符串就是引用地址。
  • for ... in 循环返回的值都是数据结构的 键值名。 遍历对象返回的对象的key值,遍历数组返回的数组的下标(key)。 for ... in 循环不仅可以遍历数字键名,还会遍历原型上的值和手动添加的其他键。

for of

  • 对象就是键值value,数组就是数组的值,字符串就是字符。
  • for of 循环用来获取一对键值对中的值,而 for in 获取的是 键名。

补充:for of 不同与 forEach, 它可以与 break、continue和return 配合使用,也就是说 for of 循环可以随时退出循环。


能读到这里,说明你是一个有耐心并且有毅力的人,看完下面的你肯定能逢面必过


6.必问六:http1和http2和http3的区别

HTTP(Hypertext Transfer Protocol)是一种用于在客户端和服务器之间传输数据的协议。以下是HTTP/1.1、HTTP/2和HTTP/3之间的主要区别:

  1. HTTP/1.1:

    • 基于文本协议:HTTP/1.1是基于文本的协议,每个请求和响应都是以纯文本的形式进行传输。
    • 无法多路复用:HTTP/1.1每次只能处理一个请求,即一个连接只能处理一个请求和响应,需要等待前一个请求完成后才能发送下一个请求。
    • 无法压缩头部信息:HTTP/1.1无法对请求和响应的头部信息进行压缩,导致每个请求和响应都包含较多的重复头部信息,占用了较多的带宽。
    • 顺序阻塞:在HTTP/1.1中,如果一个请求在响应之前阻塞了,那么后续的请求也会被阻塞。
  2. HTTP/2:

    • 二进制协议:HTTP/2使用二进制格式传输数据,提高了传输效率。
    • 多路复用:HTTP/2引入了多路复用的机制,可以在一个连接上同时发送多个请求和响应,减少了连接的数量,提高了性能。
    • 头部压缩:HTTP/2使用HPACK算法对请求和响应的头部信息进行压缩,减少了数据传输的大小。
    • 服务器推送:HTTP/2支持服务器主动推送资源给客户端,提高了页面加载速度和性能。
  3. HTTP/3:

    • 基于UDP:HTTP/3使用基于UDP的传输协议QUIC(Quick UDP Internet Connections),而不是基于TCP的传输协议。
    • 增强的安全性:HTTP/3通过TLS 1.3提供了更强的安全性。
    • 低延迟:HTTP/3通过使用QUIC协议的连接迁移能力,可以更快地建立和恢复连接,减少了延迟。
    • 抗丢包:HTTP/3使用QUIC协议提供了更好的丢包恢复和拥塞控制机制,可以更好地适应不稳定的网络环境。

总的来说,HTTP/1.1是传统的基于文本的协议,HTTP/2引入了二进制协议、多路复用和头部压缩等特性以提高性能,而HTTP/3则进一步改进了传输协议,提供了更低的延迟和更好的抗丢包能力。

详细了解请看我的另一篇文章 blog.csdn.net/weixin_4762…

7.必问七:async和defer的区别

script 是会阻碍 HTML 解析的,只有下载好并执行完脚本才会继续解析 HTML。 defer 和 async有一个共同点:下载此类脚本都不会阻止页面呈现(异步加载),区别在于:

  • async 执行与文档顺序无关,先加载哪个就先执行哪个;defer会按照文档中的顺序执行async 脚本加载完成后立即执行,可以在DOM尚未完全下载完成就加载和执行;
  • 而defer脚本需要等到文档所有元素解析完成之后才执行

8 必问八:history和hash路由的区别

  • hash有#,没有history看起来优雅
  • hash 路由:监听 url 中 hash 的变化,然后渲染不同的内容,这种路由不向服务器发送请求,不需要服务端的支持;
  • history 模式改变 url的方式会导致浏览器向服务器发送请求,我们需要在服务器端做处理:如果匹配不到任何静态资源,则应该始终返回同一个 html 页面。
  • history模式下pushState设置的URL可以与当前的URL一模一样,这样也会把记录添加到栈中;而hash模式下hash设置的新值必须与原来的不一样才会触发记录添加到栈中。
  • history模式需要后端配合将所有的访问都指向index.html,否则用户刷新页面,会导致404错误。
  • pushState通过stateObject可以添加任意类型数据到记录中,而hash只可添加短字符串。
  • 路由改变触发的事件不一样,history路由触发onpopstate事件,hash路由触发onhashchange事件
window.onpopstate = function(e){
    console.log(e)
}
window.onhashchange = function(e){
    console.log(e)
}

9 必问九:import和require的区别

两者区别

  • require在运行时调用,理论上可以放在代码的任何地方,例如react中可以用require去图片引入的方式
  • import在编译时调用,所有只能在文件开头调用
  • require 是赋值过程,通过require引入基础数据类型时,属于复制该变量。通过require引入复杂数据类型时,属于浅拷贝该对象。
  • import是解构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require

10.必问十:Map与WeakMap的区别或者WeakSet与Set的区别?

JavaScript垃圾回收是一种内存管理技术。在这种技术中,不再被引用的对象会被自动删除,而与其相关的资源也会被一同回收。Set中对象的引用都是强类型化的,并不会允许垃圾回收。这样一来,如果Set中引用了不再需要的大型对象,如已经从DOM树中删除的DOM元素,那么其回收代价是昂贵的。为了解决这个问题,ES6还引入了WeakSet的弱集合。这些集合之所以是“弱的”,是因为它们允许从内存中清除不再需要的被这些集合所引用的对象。

以下三点是Map和WeakMap的主要区别:

  • Map对象的键可以是任何类型,但WeakMap对象中的键只能是对象引用
  • WeakMap不能包含无引用的对象,否则会被自动清除出集合(垃圾回收机制)。
  • WeakSet对象是不可枚举的,无法获取大小。

以下三点是WeakSet与Set不一样的地方:

  • Set可以存储值类型和对象引用类型,而WeakSet只能存储对象引用类型,否则会抛出TypeError。
  • WeakSet不能包含无引用的对象,否则会被自动清除出集合(垃圾回收机制)。
  • WeakSet对象是不可枚举的,也就是说无法获取大小,也无法获取其中包含的元素。

11.必问十一:react和vue的区别

  • 1、响应式原理不同;
  • 2、监听数据变化的实现原理不同;
  • 3、组件写法不同;
  • 4、Diff算法不同;
  • 5、核心思想不同;
  • 6、数据流不同;
  • 7、组合不同功能的方式不同;
  • 8、组件通信方法不同;
  • 9、模板渲染方式不同;
  • 10、渲染过程不同;
  • 11、框架本质不同。

Diff算法不同

vue和react的diff算法都是进行同层次的比较,主要有以下两点不同: vue对比节点,如果节点元素类型相同,但是className不同,认为是不同类型的元素,会进行删除重建,但是react则会认为是同类型的节点,只会修改节点属性。 vue的列表比对采用的是首尾指针法,而react采用的是从左到右依次比对的方式,当一个集合只是把最后一个节点移动到了第一个,react会把前面的节点依次移动, 而vue只会把最后一个节点移动到最后一个,从这点上来说vue的对比方式更加高效。

核心思想不同

vue:Vue的核心思想是尽可能的降低前端开发的门槛,是一个灵活易用的渐进式双向绑定的MVVM框架。 react:React的核心思想是声明式渲染和组件化、单向数据流,React既不属于MVC也不属于MVVM架构。

数据流不同

vue:Vue1.0中可以实现两种双向绑定:父子组件之间,props可以双向绑定;组件与DOM之间可以通过v-model双向绑定。Vue2.x中去掉了第一种,也就是父子组件之间不能双向绑定了(但是提供了一个语法糖自动帮你通过事件的方式修改),并且Vue2.x已经不鼓励组件对自己的 props进行任何修改了。

react:React一直不支持双向绑定,提倡的是单向数据流,称之为onChange/setState()模式。 不过由于我们一般都会用Vuex以及Redux等单向数据流的状态管理框架,因此很多时候我们感受不到这一点的区别了。

12.必问十二:箭头函数()=>{}和普通函数function(){}的区别

  • 1.作用域

箭头函数没有独立的作用域,没有自己的this,继承了外部函数的作用域,this指向上下文环境,也就是最近的一个函数作用域。 普通函数有自己独立的作用域,有自己的this指向和arguments。

  • 2.构造函数

箭头函数不能用作构造函数,没办法实例化,没有自己的prototype属性。 普通函数可以用作构造函数,可以实例化,有自己的prototype属性。

  • 3.递归

箭头函数没有自己的独立作用域,递归调用时容易导致堆栈的溢出。 普通函数有自己的独立作用域,可以递归调用,每次调用都会创建一个新的作用域。

  • 总结:

总的来说箭头函数更适合用于简单的表达式和继承上下文的情况,而普通函数适合需要有独立作用域和自己自定义的this和arguments的场景。

13.必问十三:includes和indexOf的区别

判断某一项是否在数组中几种方式indexOf,includes,some,findIndex,

  • includes和indexOf的最大的区别就是

indexOf是 === ,includes是 ==

indexOf要求严格,无法正确判断数组中是否有NaN,但是includes可以

let arr = [NaN, 1, undefined]
console.log(arr.includes(NaN)) // true
console.log(arr.indexOf(NaN)) // -1
// undefined不受影响
console.log(arr.includes(undefined)) // true
console.log(arr.indexOf(undefined)) // 2

能读到这里,说明你是一个有耐心并且有毅力的人,看完下面的你肯定能逢面必过


14.必问十四:强缓存和协商缓存的区别

先聊一下浏览器缓存,是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档。使用缓存有下面的优点: 1.减少冗余的数据传输 2.减少服务器负担 3.加快客户端加载网页的速度

浏览器缓存主要分为强缓存(也称本地缓存)和协商缓存(也称弱缓存)。

  • 强缓存

当请求资源的时,如果是之前请求过的并使用强缓存,那么在过期时间内将不会发送本次请求向服务器获取资源,而是直接从浏览器缓存中获取(不管资源是否改动)。过期了将重新从服务器获取,并再次强缓存。强缓存是利用http头中的ExpiresCache-Control两个字段来控制的,Expires是http1.0的规范,Cache-Control是在http1.1中出现的,我们这里使用Cache-Control示范。 Cache-Control有一些常设置的值

private:仅浏览器可以缓存(默认值); public:浏览器和代理服务器都可以缓存; max-age=xxx:过期时间单位秒; no-cache:不进行强缓存; no-store:不强缓存,也不协商缓存)

  • 协商缓存

当请求资源时,如果是之前请求过的并使用协商缓存,还是发送请求到服务器,服务器通过逻辑判断确认资源没有修改返回304状态码,那么本次的资源则是从缓存中获取;如果经过判断确认资源被修改过,则重新发送资源到客户端,并且客户端更新缓存。主要涉及到两组header字段:EtagIf-None-Match、Last-Modified、if-modified-since

Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。

强缓存就是浏览器本地根据服务器设置的过期时间来判断是否使用缓存,未过期则从本地缓存里拿资源,已过期则重新请求服务器获取最新资源。

协商缓存则是浏览器本地每次都向服务器发起请求,由服务器来告诉浏览器是从缓存里拿资源还是返回最新资源给浏览器使用。

判断资源是否修改有两种标准,一种是判断最后修改时间是否变了(确实是修改了,但资源的内容可以没有变),另一种是判断资源的内容是否修改。

相同点:如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据 不同点:强缓存不发请求到服务器,协商缓存会发请求到服务器

  • 浏览器在加载资源时,根据请求头的expires和cache-control判断是否命中强缓存,是则直接从缓存读取资源,不会发请求到服务器
  • 如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过last-modified和etag验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,依然是从缓存中读取资源
  • 如果前面两者都没有命中,直接从服务器加载资源

15.必问十五:浅拷贝和深拷贝的区别

浅拷贝(Shallow Copy)是指创建一个新对象或数据结构,新对象的属性值是原对象的引用。换句话说,浅拷贝只复制对象的第一层属性,而不会递归复制对象的内部引用类型属性。修改浅拷贝后的对象的属性值可能会影响到原对象的属性值。

深拷贝(Deep Copy)是指创建一个新对象或数据结构,新对象的属性值与原对象完全独立,互不影响。深拷贝会递归复制对象的所有属性,包括内部引用类型属性。修改深拷贝后的对象的属性值不会影响到原对象的属性值。

下面是一个简单的示例来说明浅拷贝和深拷贝的区别:

// 浅拷贝示例
const obj1 = { name: 'Alice', age: 20 }
const shallowCopy = Object.assign({}, obj1)
shallowCopy.age = 30

console.log(obj1.age) // 输出: 20

// 深拷贝示例
const obj2 = { name: 'Bob', address: { city: 'New York', country: 'USA' } }
const deepCopy = JSON.parse(JSON.stringify(obj2))
deepCopy.address.city = 'Los Angeles'

console.log(obj2.address.city) // 输出: New York

在上面的例子中,我们使用Object.assign进行浅拷贝,将原对象obj1的属性复制到了shallowCopy对象上。当我们修改shallowCopyage属性时,原对象obj1age属性并没有改变。

而对于深拷贝,我们使用JSON.parse(JSON.stringify(obj2))来实现。通过将对象序列化为JSON字符串,再解析为新的对象,从而进行深拷贝。当我们修改deepCopyaddress.city属性时,并没有影响到原对象obj2address.city属性。

需要注意的是,深拷贝也有一些限制。它可能无法正确处理循环引用的对象或函数、特殊类型的对象(如Date、RegExp等)以及无法复制对象的原型链等情况。在实际应用中,需要根据具体情况选择适当的拷贝方式。

面试官常考的手撕代码:

  • 手写一个instanceof
function instanceofFun(left,right){
    let l = left.prototype
    let r = right._proto_
    while(true){
        if(l === r){
            return true
        }
        return false
    }
}
function A () {

}
const a = new A()
console.log(instanceofFun({},Object),{} instanceof Object)
console.log(instanceofFun([],Array),{} instanceof Object)
  • 手写一个浅拷贝
function shallowCopy (source) {
    if( Object(source) !== source ) return source;
    const _tmp = Array.isArray(source) ? [] : {};
    for (let key in source) {
        if (source.hasOwnProperty(key)) _tmp[key] = source[key]; 
    }
    return _tmp;
}

手写一个深拷贝

function deepClone(target){
    if(target !== null && typeof target === 'object'){
        let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {};
        for (let k in target){
            if (target.hasOwnProperty(k)) {
                result[k] = deepClone(target[k])
            }
        }
        return result;
    }else{
        return target;
    }
}

能读到这里,说明你是一个有耐心并且有毅力的人,看完下面的你肯定能逢面必过


16.必问十六:node和浏览器中的eventLoop事件机制的主要区别

Node.js和浏览器中的事件循环(Event Loop)机制有一些主要区别。以下是其中一些区别:

  1. 实现方式:Node.js和浏览器中的事件循环机制是不同的。浏览器使用Web APIs(如setTimeout、setInterval)和浏览器自身的事件循环机制来处理异步操作。而Node.js使用libuv库来实现事件循环,该库提供了一种跨平台的异步I/O模型。

  2. 宏任务和微任务:Node.js和浏览器中都有宏任务(Macro Task)和微任务(Micro Task)的概念,但它们的执行顺序有所不同。在浏览器中,微任务(如Promise的回调函数、MutationObserver的回调函数)会在当前宏任务执行结束后立即执行。而在Node.js中,微任务会在每个阶段结束后(如timers、I/O、check阶段)执行,而不是在当前宏任务执行结束后。

  3. 定时器精度:在浏览器中,定时器的精度通常是由浏览器实现决定的,可能会有一定的误差。而在Node.js中,定时器的精度通常是由操作系统的定时器决定的,通常能够提供更高的精度。

  4. 默认最大并发数:在浏览器中,对于同一域名下的请求,默认情况下会有一定数量的并发请求限制(通常是6个),超过限制的请求会被排队等待执行。而在Node.js中,默认情况下没有并发请求的限制,可以同时处理大量的并发请求。

  5. 全局对象:在浏览器中,全局对象是window,而在Node.js中,全局对象是global

需要注意的是,尽管Node.js和浏览器中的事件循环机制有一些区别,但它们都遵循了事件驱动的异步编程模型,通过事件循环来处理异步操作,使得编写高效、非阻塞的代码成为可能。

  • 浏览器中的事件循环机制是如何处理宏任务和微任务的执行顺序的

在浏览器中,事件循环(Event Loop)机制用于处理宏任务(Macro Task)和微任务(Micro Task)。它们的执行顺序如下:

  1. 执行当前宏任务:从宏任务队列中取出一个宏任务,并执行它。常见的宏任务包括脚本(整体代码)、setTimeout、setInterval、I/O等。

  2. 执行当前宏任务产生的微任务:在执行当前宏任务的过程中,如果产生了微任务,会将微任务添加到微任务队列中。常见的微任务包括Promise的回调函数、MutationObserver的回调函数等。

  3. 渲染页面:如果浏览器需要渲染页面,会执行渲染操作。

  4. 执行下一个宏任务:当前宏任务执行完毕后,会从宏任务队列中取出下一个宏任务,然后重复上述步骤。

需要注意的是,微任务的执行时机是在宏任务执行结束后、渲染页面之前。这意味着微任务可以在下一个宏任务之前执行,从而使得微任务的响应更为及时。

具体来说,事件循环的执行过程如下:

  1. 执行当前宏任务,期间产生的微任务会被添加到微任务队列中。

  2. 当前宏任务执行完毕后,检查微任务队列是否为空。

  3. 如果微任务队列不为空,则按照先进先出的顺序依次执行微任务,直到微任务队列为空。

  4. 执行渲染操作,更新页面的显示。

  5. 从宏任务队列中取出下一个宏任务,执行它,然后重复上述步骤。

这样,浏览器通过事件循环机制来处理宏任务和微任务的执行顺序,使得异步操作可以以非阻塞的方式进行,提高了程序的响应能力和用户体验。

  • node中的事件循环机制是如何处理宏任务和微任务的执行顺序的? Node.js中的事件循环(Event Loop)机制也会处理宏任务(Macro Task)和微任务(Micro Task),它们的执行顺序如下:
  1. 执行当前宏任务:从宏任务队列中取出一个宏任务,并执行它。常见的宏任务包括定时器(setTimeout、setInterval)、I/O操作、网络请求等。

  2. 执行当前宏任务产生的微任务:在执行当前宏任务的过程中,如果产生了微任务,会将微任务添加到微任务队列中。常见的微任务包括Promise的回调函数、process.nextTick等。

  3. 执行下一个宏任务或者检查是否有I/O等待:当前宏任务执行完毕后,会检查是否有I/O等待的操作,如果有,则会先处理它们。如果没有I/O等待的操作,会继续从宏任务队列中取出下一个宏任务,然后重复上述步骤。

需要注意的是,Node.js中的事件循环机制与浏览器中的略有不同。在Node.js中,微任务的执行时机是在每个阶段结束后执行,而不是在当前宏任务执行结束后。

具体来说,事件循环的执行过程如下:

  1. 执行当前宏任务,期间产生的微任务会被添加到微任务队列中。

  2. 当前宏任务执行完毕后,检查是否有I/O等待的操作,如果有,则先处理它们。如果没有,则继续下一步。

  3. 执行所有微任务,按照先进先出的顺序依次执行微任务队列中的微任务。

  4. 执行下一个宏任务或者检查是否有I/O等待的操作,如果有,则先处理它们。如果没有,则继续下一步。

  5. 重复上述步骤,直到事件循环结束。

通过事件循环机制,Node.js能够处理宏任务和微任务,实现异步非阻塞的编程模型,提高程序的性能和可伸缩性。

17.必问十七:防抖和节流的区别

  • 防抖,场景:搜索输入框,多次触发,只在最后一次触发时执行

原理为设置一个定时器,每隔一段时间触发事件,下次执行时则重新计时

function debounce(fuc,number){
    let timeout = null
    let self = this
    return function(...args){
        if(timeout){
            clearTimeout(timeout)
        }
        setTimeout(()=>{
            fuc.apply(self,args)
        },number)
    }
}
  • 节流函数,场景:轮播图,限制目标调用频率,以固定的时间触发函数
 function throttle(fuc,number){
    let self = this
    let pre = 0
    return function(...args){
        let now = +new Date()
        if(now-pre>number){
            pre = now
            fuc.apply(self,args)
        }
    }
}

18.必问十八:状态码301 和 302 的区别。

301和302状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取(用户看到的效果就是他输入的地址A瞬间变成了另一个地址B)——这是它们的共同点。他们的不同在于:

  • 301表示旧地址A的资源已经被 永久地 移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;
  • 302表示旧地址A的资源还在(仍然可以访问),这个重定向只是 临时地 从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址。SEO302好于301

19.必问十九: MVVM与MVP区别,MVVM与MVC区别

  • MVVM与MVP区别 mvvm模式将Presener改名为View Model,基本上与MVP模式完全一致,唯一的区别是,它采用双向绑定(data-binding): View的 变动,自动反映在View Model,反之亦然。 这样开发者就不用处理接收事件和View更新的工作,框架已经帮你做好了。

  • MVVM与MVC区别 MVVM与MVC最大的区别就是:它实现了View和Model的自动同步,也就是当Model的属性改变时,我们不用再自己手动操作Dom元素, 来改变View的显示,而是改变属性后该属性对应View层显示会自动改变。

20.for的用法

for(var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i)
    }, 1000)
}
console.log(i)
//5 -> 5,5,5,5,5 
  1. 接触let的暂时性死区,只在本循环内有效,块级作用域
for(let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i)
    }, 1000)
}
console.log(i)
//输出结果:5 -> 0,1,2,3,4
  1. 接触setTimeout的第三个参数
 for(var i = 0; i < 5; i++) {
    setTimeout((j) => {
        console.log(j)
    }, 1000, i)
}
console.log(i)
//输出结果:5 -> 0,1,2,3,4
  1. 借助立即执行函数,也就是闭包的引用
for(var i = 0; i < 5; i++) {
    ((j) => {
        setTimeout(() => {
            console.log(j)
        }, 1000)
    })(i)
}
console.log(i)
//输出结果:5 -> 0,1,2,3,4
  1. 借助形参的特性
var sleepConsole = (i) => {
    setTimeout(() => {
        console.log(i)
    }, 1000)
}
for(var i = 0; i < 5; i++) {
    sleepConsole(i)  // i会被复制后传递
}
console.log(i)
//输出结果:5 -> 0,1,2,3,4
  1. 借助 Promise 1.建立数组存储 Promise
const task = []

2.抽取方法生成异步操作

const output = (i) => new Promise(resolve => {
    setTimeout(() => {
        console.log(i)
        resolve()
    }, 1000 * i)
})

3.循环执行异步操作

for(var i = 0; i < 5; i++) {
    task.push(output(i))
}

4.异步操作执行完成后输出最后的i

Promise.all(task).then(() => {
    setTimeout(() => {
        console.log(i)
    }, 1000)
})
//输出结果:0 -> 1 -> 2 -> 3 -> 4 -> 5
  1. 借助 async/await 生成休眠函数
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
(async () => {
    for(var i = 0; i < 5; i++) {
        if (i > 0) {
            await sleep(1000)
        }
        console.log(i)
    }
    await sleep(1000)
    console.log(i)
})()
//输出结果:0 -> 1 -> 2 -> 3 -> 4 -> 5

21. babel的原理

  • 解析阶段:Babel 默认使用 @babel/parser 将代码转换为 AST。解析一般分为两个阶段:词法分析和语法分析。 词法分析:对输入的字符序列做标记化(tokenization)操作。 语法分析:处理标记与标记之间的关系,最终形成一颗完整的 AST 结构。

  • 转换阶段:Babel 使用 @babel/traverse 提供的方法对 AST 进行深度优先遍历,调用插件对关注节点的处理函数,按需对 AST 节点进行增删改操作。

  • 生成阶段:Babel 默认使用 @babel/generator 将上一阶段处理后的 AST 转换为代码字符串。

Babel 的核心模块 @babel/core,@babel/parser,@babel/traverse 和 @babel/generator 提供了完整的编译流程。而具体的转换逻辑需要插件来完成。在使用 Babel 时,我们可通过配置文件指定 plugin 和 preset。而 preset 可以是 plugin 和 preset 以及其他配置的集合。Babel 会递归读取 preset,最终获取一个大的 plugins 数组,用于后续使用。

22.手写实现一个迭代器

// 创建一个简单的迭代器对象
const iterableObject = {
    data: [1, 2, 3, 4],
    currentIndex: 0,
    [Symbol.iterator]: function() {
      const self = this;
      return {
        next: function() {
          if (self.currentIndex < self.data.length) {
            return { value: self.data[self.currentIndex++], done: false };
          } else {
            return { done: true };
          }
        }
      };
    }
  };
  
  // 使用 for...of 循环遍历迭代器
  for (const item of iterableObject) {
    console.log(item);
  }

23.flat笔试题

  • 手写flat
Array.prototype.flat = function(dep=1){
    let array = []
    this.forEach((item)=>{
        if(Array.isArray(item) && dep > 0){
            dep--
            array =  array.concat(item.flat(dep))
        }else{
            array.push(item)
        }
    })
    return array
}
const a= [1,2,3,[4,5,6,[8,9,[10,11]]]]
console.log(a.flat(3))
  • 实现一个函数,对输入的数组进行拍平、排序、去重,递归实现拍平
const input = [1, [2, [3, 4], 5], 6, [7, 8, [9, 10]]];
const flatArray = function(array){
        var result = []
        //递归去平
        for(var i in array){
            if(array[i] instanceof Array){
               result = result.concat(flatArray(array[i]))
            }else{
                result = result.concat(array[i])
            }
        }
        return result
}
const flatFuc = function(params){
    //实现拍平
    var result = flatArray(params)
    //实现去重
    result = Array.from(new Set(result))
    //实现排序
    result = result.sort((a,b)=>a-b)
    console.log(result)
}
flatFuc(input)

最后但也是全文最重要的,码字不易,欢迎点赞关注加收藏,你们的鼓励是我码字的动力! 感谢感谢感谢!!!!!!!!