滴滴、美团、腾讯 打针哥的大厂集邮之路

发布于:2024-05-06 ⋅ 阅读:(25) ⋅ 点赞:(0)

假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

前言

打针哥为谁?怎么那么厉害!先是拒了滴滴, 再是美团收入囊中。五一节前,腾讯要它快乐,送上offer。美团二面后,如同杜丽娘盼情郎的我,羡慕不已。特此一文,要来打针哥的腾讯面经,向大佬学习学习。也与各位共勉,一起把握五一后大厂补录机会。美团把我收了吧,我想黄袍加身...

image.png

打针哥

集邮的offer

  • 滴滴 28bf4ef004f47cf714ca67788dcff84.jpg

  • 美团 image.png

  • 腾讯

image.png

打针哥本来还有网易有道等大厂也在流程中,腾讯offer一到手,被我们集体封电脑,约女神,订宾馆,给我们留点机会啊....不然怕是要集齐BATJTMD, 召唤神龙。

外号由来

我们虽然是一群双非大学的苦孩子,但是代码氛围还可以。社团里有两位老师特别有代码激情,喜欢讲代码梦想。往届就有很多学长在感召下去了大厂(去年9人去了快手,抖音、拼多多也有),我们春招身边也陆续有同学拿下百度、字节、美团等offer。

打针哥就是我们中最强的一位。我们在听社团老师分享的时候,他经常不来,默默无闻,从不显山露水。老师问起,身边同学就说去打针了,名号由此而来。原来是巨佬,简单的就不来,自我学习。后来又了解到打针哥是学院机房管理员,各种假期,皆在机房写代码...

打针哥,乃机房扫地僧也!拿下腾讯足见其实力。

腾讯面试

打针哥的腾讯面试由三轮技术面和1轮hr面组成。

腾讯一面

1. 自我介绍

大家可以看 ,有标准公式。

打针哥补充,在中国AI元年的春招,自我介绍可以加入点自己对AIGC的认识。

比如在自我介绍最后,聊到最近学习了一些AI方面的内容,果然面试官的兴致就提起来了。问了打针哥为何前端需要学习AI,AI是否会取代程序员等问题。

打针哥的回答如下:

开始通过HuggingFace社区了解到transformer,可以快速完成诸如情感分析、图片识别、总结等NLP任务,最近还推出了js版本,前端完成AI任务是个趋势。

之后学习了OpenAI 的一些接口,了解了基于LLM开发的方式、Prompt Enginnering 的概念。未来一定是AI应用的时代,拥抱就好

前端用户体验,也因AIGC应用在改变。 比如在使用coze 生成AI应用时,可以根据应用名和介绍,调用AIGC服务接口生成logo, 而不需要上传文件。未来会有更多的用户体验因AI而创新。 最近自己也在学习python语言,LangChain AI应用开发框架。有了各种AI Copilot, 学什么都快,什么都更好学,自己有信心适应AI时代,成为一名优秀的AI全栈工程师。 最后,表达个人观点。AI不会取代工程师,但是会AI的工程师会取代传统工程师。

各位,简历上我们的信息都有。自我介绍貌似多余,但聊一聊面试官现在也在学习,非常感兴趣的AI,也许是破冰、与其攀谈的绝好方式。我周围有挺多同学,聊AI让面试官另眼相看的。我们现在也确实来到了传统互联网与AI智能的交界处。

2. 演示下自己的项目

打针哥的项目是一个全栈的商城。vue全家桶、JWT、购物车、node mvc、组件化、hooks、性能优化都有亮点和难点。

最大的亮点,我觉得是完成了服务器端部署,建议大家也可以做下。

3. 作用域和作用域链

作用域作为JS基础的开篇考题,很正常。面试官希望我们清晰且深入的理解其原理,并有一定的实战经验。

作用域

作用域定义了变量、函数等标识符在何处以及如何可以被访问。它限定了变量的生命周期和可见性。JavaScript中的作用域主要有三种类型:

  • 全局作用域Scope

在代码的最外层定义的变量拥有全局作用域,它们可以在代码的任何地方被访问。在浏览器环境下,全局作用域通常绑定到window对象上。在node环境下,全局作用域通常绑定到global对象上

注意: 使用letconst声明的变量不会挂载到window对象上。这一改变是ES6引入块级作用域的一个重要特性,旨在解决变量声明提升和变量污染问题。

  • 函数作用域 在函数内部定义的变量拥有函数作用域,它们只在该函数内部及嵌套的函数内部可见。

  • 块级作用域(ES6引入) 使用letconst声明的变量具有块级作用域,这意味着它们只在定义它们的代码块(如if语句或for循环)内部可见。

比如传统循环事件监听,不用闭包,用块级作用域就行。

for (var i = 0; i < 5; i++) {
    (function(i) {
        document.getElementById('button' + i).addEventListener('click', function() {
            console.log("Button " + i + " clicked");
        });
    })(i);
}
for (let i = 0; i < 5; i++) {
    document.getElementById('button' + i).addEventListener('click', function() {
        console.log("Button " + i + " clicked");
    });
}
  • 作用域链(Scope Chain) 当JavaScript引擎需要查找一个变量的值时,它会遵循一定的规则去查找,这个查找路径就是作用域链。作用域链是由当前执行环境的作用域与其所有外部作用域组成的链式结构。

  • 每个函数在创建时,都会生成一个新的执行上下文,这个执行上下文中会包含一个指向其外部作用域的引用,这样就形成了一个链式结构。

image.png

  • 查找变量时,引擎首先会在当前作用域查找,如果找不到,就会向上一级作用域继续查找,直到全局作用域。如果在全局作用域也未找到,则认为该变量未定义。

  • 作用域链确保了函数可以访问其自身作用域以及包含它的所有外部作用域中的变量,但外部作用域无法访问函数内部的变量,体现了JavaScript的词法作用域特性。

4. 块级作用域有哪些

从ES6开始,JavaScript通过letconst正式引入了块级作用域的概念,改变了之前只有全局作用域和函数作用域的状况。

包括但不限于if语句、for循环、while循环、switch语句以及任何被大括号包围的代码块。使用letconst这两个关键字声明的变量仅在声明它们的代码块内部有效。

暂时性死区(TDZ, Temporal Dead Zone) 在使用letconst声明变量之前,该变量在其声明之前的区域被称为暂时性死区,任何访问该变量的尝试都将导致错误。

5. try catch 是块级作用域吗

尽管try...catch提供了异常处理机制,它同时也定义了一个新的块级作用域上下文,这与使用大括号 {} 包围的其他代码块(如if语句、循环等)在作用域规则上是一致的。

6. if else 是块级作用域吗

当然是

在JavaScript中,class关键字用于定义一个类,它实际上也是一个特殊的函数,因此它定义了一个新的作用域。然而,直接说class本身形成了一个典型的“块级作用域”可能不太准确,因为它的行为和用途更贴近于函数作用域而非常规意义上的块级作用域。在js中,class只是语法糖。

7. 什么是闭包

闭包本质上是函数与其词法环境(包括其外部变量)的组合。 闭包是指一个函数能够访问并维持其外部作用域中的变量,即便外部函数已经执行完毕。这种能力得益于JavaScript的词法作用域和动态作用域特性。

构成要素

  • 函数嵌套:闭包的形成需要至少两个函数,其中一个函数在另一个函数内部定义。
  • 变量访问:内部函数能够访问其外部函数的变量,包括参数和局部变量。
  • 生命周期:即使外部函数执行完毕,由于内部函数对这些变量的引用,它们不会被垃圾回收,从而延长了这些变量的生命周期。

应用场景

闭包在实践中的一些典型用途,比如保持状态、实现私有变量、封装、实现函数记忆功能、控制异步操作中的状态、防抖节流、柯里化等

如果面试官让我们手写个防抖节流,那就爽了, 因为准备好了。

内存管理

闭包可能带来的内存泄露问题

8. 想用闭包又不想内存泄漏怎么办?清除闭包,怎么清除?

  • 手动解除引用

当不再需要闭包或其引用的外部变量时,显式地将它们设置为null。这有助于垃圾回收机制识别这些对象已不再被使用,从而回收内存。

function createClosure() {
   let externalVar = 'some value';
    return function innerFunction() {
        console.log(externalVar);
    };
}
const myClosure = createClosure();
// 使用后清理
myClosure();
myClosure = null;
  • 使用WeakMap或WeakSet 对于需要在闭包中引用对象但又不想阻止它们被垃圾回收的情况,可以使用WeakMapWeakSet来存储这些引用。弱引用不会阻止垃圾回收,即使它们还被某个数据结构引用。
const weakMap = new WeakMap();
function cacheData(obj) {
    weakMap.set(obj, 'cached data');
    return function getData() {
        return weakMap.get(obj);
    };
}

如果你对这个知识点不太懂,可以看下面这个例子

let dog1 = { name: 'Snickers' }; 
let dog2 = { name: 'Sunny' };

const strong = new Map();
const weak = new WeakMap();

strong.set(dog1, 'Snickers is the best!');
weak.set(dog2, 'Sunny is the 2nd best!');

dog1 = null; 
dog2 = null;

在上述代码中,dog1dog2对象被分别存储进了strong(普通Map)和weakWeakMap)中。然后,dog1dog2被设置为null,这意味着这两个对象没有其他直接的引用了。

在这个例子中,dog2及其在weak中的条目可能会被垃圾回收,而dog1因为被strong以强引用的方式持有,不会被回收。

  • 避免循环引用:特别注意闭包与DOM元素或其他长生命周期对象之间的循环引用,及时断开这些联系。

在JavaScript中,特别是在处理DOM事件监听器时,很容易不小心创建闭包与DOM元素之间的循环引用,进而导致内存泄漏。

function setupClickListener() {
    const button = document.getElementById('myButton');
    
    button.addEventListener('click', function handleClick() {
        console.log('Button clicked!');
        // 这个匿名函数形成了一个闭包,因为它访问了button变量
        // button -> 匿名函数(handleClick) 形成循环引用
    });
    // button元素持有着事件处理器的引用(通过闭包)
    // 同时,匿名函数(闭包)又持有着button的引用
}
setupClickListener();

button元素通过其事件监听器间接持有着对匿名函数(handleClick)的引用,而这个匿名函数(作为闭包)又持有着对外部变量button的引用,这就形成了一个循环引用。

解除引用:

function setupClickListener() {
    const button = document.getElementById('myButton');
    function handleClick() {
        console.log('Button clicked!');
        // 清理工作,解除引用
        button.removeEventListener('click', handleClick);
    }
    button.addEventListener('click', handleClick);
    // 当点击事件发生后,立即解除事件处理器与button的引用关系
}
setupClickListener();
  • 使用立即执行函数表达式(IIFE)

通过立即执行函数,可以在执行完毕后自动清理局部变量,减少内存占用。

  • 清除定时器和事件监听器

如果闭包中使用了setTimeoutsetInterval或添加了事件监听器,确保在不再需要时使用clearTimeoutclearIntervalremoveEventListener来清理它们。

  • 限制闭包的作用域

尽量缩小闭包所需外部变量的范围,只保留必要的变量在闭包中,减少潜在的内存占用。

9. this是干啥用的,this的绑定规则,call, apply bind区别

this关键字在JavaScript中用于表示函数执行时的上下文,即函数内部的this值会根据函数的调用方式和位置动态绑定。它提供了对当前执行环境的引用,使得函数能够访问到调用它的对象的属性和方法。

this的绑定规则

    1. 默认绑定:在非严格模式下,如果函数独立调用(不作为对象的方法),this指向全局对象(在浏览器中是window,在Node.js中是global)。在严格模式下,this会是undefined
    1. 隐式绑定:当函数作为某个对象的方法被调用时,this指向该对象。
    1. 显式绑定:使用call, apply, 或 bind方法可以显式地指定函数内部this的值。
    1. new绑定:使用new关键字调用构造函数时,this会指向新创建的对象实例。
    1. 箭头函数:箭头函数不绑定自己的this,它会捕获其所在上下文的this值作为自己的this值,也就是词法绑定,不会因调用方式改变。

callapplybind的区别:

call() 允许你调用一个函数,并且指定函数内部的this值和单独列出的参数

apply() 类似于call(),但是接受一个数组或类数组对象作为参数列表。

bind() 不会立即调用函数,而是创建一个新的函数,其this值被永久绑定到传入的值。 可以传入额外的参数,这些参数会被预填充到新函数的参数列表中。

10. 原型和原型链

原型(Prototype)是JavaScript中的一种机制,每个函数都有一个prototype属性,该属性是一个对象,用于存放所有实例共享的属性和方法。当一个函数作为构造函数创建实例时,这个实例的内部将有一个隐式链接指向构造函数的prototype对象,这就是原型链的基础。

原型链(Prototype Chain)是JavaScript实现继承的核心机制。每个对象(除了null)都有一个内部属性[[Prototype]](可被__proto__访问,但在现代JavaScript中建议避免直接使用__proto__),它指向其原型对象。当试图访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript引擎会继续在其原型对象中查找,如果原型对象中也没有,则继续在原型的原型中查找,以此类推,直到找到该属性或方法或者到达原型链的末端(通常是Object.prototype__proto__null),这就是所谓的原型链。

简而言之,原型链机制允许对象继承其原型上的属性和方法,形成了一种链式的继承结构,使得对象可以访问到其祖先对象的属性和方法,实现了面向对象编程中的继承特性。

11. new 操作符,实际做了哪些事?

在JavaScript中,new操作符用于创建并初始化一个由构造函数定义的新对象。当使用new关键字调用一个函数时,大致遵循以下步骤:

  1. 创建新对象new操作符首先会在内存中创建一个新的空对象,这个对象将成为构造函数的实例。

  2. 设置原型链:新创建的对象的[[Prototype]](或可以使用__proto__访问)被设置为构造函数的prototype属性所指向的对象。这样,新对象就可以继承构造函数原型上的属性和方法。

  3. 绑定this:在构造函数内部,this关键字被绑定到新创建的对象上。这意味着通过this可以给新对象添加属性和方法。

  4. 执行构造函数:接下来,new操作符会调用构造函数,使用新创建的对象作为this上下文。任何在构造函数中定义的属性和方法都会被添加到这个新对象上。

  5. 返回值处理

    • 如果构造函数没有明确返回一个对象(或者返回nullundefined),那么new操作符会自动返回新创建的对象。
    • 如果构造函数返回了一个对象,那么这个返回的对象将会替代步骤1中创建的新对象成为最终的结果。如果返回的是非对象值(如数字、字符串等),则忽略返回值,仍然返回新创建的对象。

总结来说,new操作符通过上述过程完成了一个对象的实例化,包括实例的创建、原型链的建立、上下文的绑定以及构造函数的执行,从而实现了面向对象编程中的对象创建与初始化。

12.介绍些ES6的新特性

  • let const
    引入块级作用域

  • 解构 reset spread 字符串模板 让js 更简洁优雅

  • promise

    解决回调地域

期待面试官问我手写Promise 或红绿灯,那将是表演时刻。

  • proxy

可以聊到vue3 和vue2 的区别, 手写vue3 响应式

reflect 可以聊聊。

  • class

让js 成为大型企业级开发语言。

  • 新的数据结构 Map Set WeakMap WeakSet

  • generator

生成器。 async/await 属于es7 更好理解

普通函数和箭头函数的区别,我可以改变箭头函数this的指向吗?

普通函数和箭头函数在JavaScript中有几个关键区别,其中最显著的是this的绑定行为:

  1. this的绑定规则

    • 普通函数:普通函数的this值在运行时基于函数的调用方式决定。例如,作为对象方法调用时this指向该对象,直接作为函数调用(非方法形式)时通常指向全局对象(在浏览器中是window,Node.js中是global,严格模式下为undefined)。
    • 箭头函数:箭头函数不绑定自己的this,它会捕获其所在上下文的this值作为自己的this值。这意味着箭头函数内部的this是定义时所在作用域的this,并且不会随着调用方式的变化而改变。
  2. 语法差异:箭头函数使用更简洁的语法,省略了function关键字,并且当函数体只有一行表达式时甚至可以省略花括号。

  3. 没有自己的argumentssupernew.target:箭头函数没有自己的arguments对象,也不支持使用new作为构造函数调用,同时没有自己的supernew.target

关于能否改变箭头函数的this指向,答案是不可以。箭头函数的this是静态绑定的,也就是说在定义时就已经确定,并且不会受到call(), apply(), 或 bind()方法的影响。这些方法可以改变普通函数的this指向,但对于箭头函数无效,因为箭头函数的this是词法绑定(lexical binding),不可更改。这也是箭头函数设计之初的一个重要特性,旨在简化this的使用,减少因this绑定导致的常见错误。

13.对null和undefine结果解构会怎么样?

在JavaScript中,当你尝试对nullundefined进行解构赋值时,会遇到不同的行为,具体取决于解构的具体上下文。下面是两种情况的概述:

解构null

尝试对null进行解构赋值会导致运行时错误(TypeError)。这是因为解构赋值期望一个可以迭代或具有属性的对象,而null既不是可迭代的,也不是一个拥有属性的对象。例如:

1let { prop } = null; // 抛出 TypeError: Cannot destructure property 'prop' of 'null' as it is null or undefined.

解构undefined

null类似,尝试对undefined进行解构也会抛出错误,因为undefined同样不能被解构以获取属性或迭代。例如:

1let { prop } = undefined; // 抛出 TypeError: Cannot destructure property 'prop' of 'undefined' as it is null or undefined.

总结

在ES6及以后版本的JavaScript中,直接对nullundefined进行解构操作是不允许的,因为它们既不是对象也不是可迭代的数据结构,这会导致抛出TypeError异常。如果你预见到可能遇到这样的值,通常需要先进行检查,确保解构的对象不为nullundefined,或者提供一个默认值以避免错误。例如,可以使用逻辑OR (||) 提供一个默认对象:

let obj = null;
let { prop = 'defaultValue' } = obj || {}; // 这里不会抛错,prop会得到'defaultValue'

这样,如果objnullundefined,则会使用一个空对象作为解构的目标,从而避免了解构失败的情况。

14. promise

这里略过

15.setTimeout准不准?

setTimeout函数在JavaScript中用于在指定的时间(以毫秒为单位)之后调用一个函数或者执行某段代码。尽管它是一个非常有用的工具,用于实现定时操作,但它并不保证完全准确无误地在指定时间执行。

有几个因素会影响setTimeout的准确性:

  1. 最小延迟: 大部分浏览器对setTimeout的最短延迟时间有限制,通常是4ms左右,这是为了防止恶意脚本造成浏览器无响应。这意味着即使你设置延迟时间为0或小于4ms,实际执行也至少会有这个最小延迟。
  2. 任务队列: JavaScript是单线程且采用事件循环机制。当setTimeout指定的时间到达时,回调函数并不是立即执行,而是被加入到任务队列等待当前执行栈为空时才执行。如果当前有其他脚本或任务正在执行,那么setTimeout的回调将继续等待。
  3. 页面生命周期: 如果页面处于后台标签页或浏览器最小化状态,某些浏览器为了节省资源,可能会暂停或减慢setTimeout的计时器,导致延迟更久。
  4. 系统性能: 系统繁忙或CPU占用率高也可能影响到setTimeout的执行时机,尤其是在计算密集型任务或者大量JavaScript执行时。

综上所述,setTimeout提供的是“至少在指定时间之后”的执行保证,而不是精确的定时。对于需要更高精度的定时操作,Web Workers中的setTimeout(在某些环境中可能更稳定)或者Web APIs如requestAnimationFrame(适用于动画相关的定时需求,与显示器刷新率同步)可能是更好的选择。

16. 讲一下任务队列

在JavaScript中,任务队列(也常被称为消息队列或事件队列)是其异步编程模型的核心组成部分。由于JavaScript是单线程的,这意味着同一时间只能执行一个任务,为了处理异步操作(如定时器、网络请求、用户交互等),JavaScript引入了任务队列的概念,以便在主线程空闲时执行这些异步完成的任务。

任务队列的基本概念

  1. 执行栈:所有的同步代码都会形成一个执行栈,即当前正在执行的函数调用序列。JavaScript引擎会从上至下依次执行这些函数。
  2. 任务队列:当遇到异步操作时,比如setTimeoutfetchPromise等,它们不会立即执行,而是当条件满足(如延时结束、网络响应到达)后,将对应的回调函数添加到任务队列中排队等待执行。
  3. 事件循环:JavaScript引擎通过事件循环机制不断地检查执行栈是否为空,以及任务队列中是否有待处理的任务。当执行栈为空时,事件循环会从任务队列中取出下一个任务(先进先出FIFO原则),将其推入执行栈并执行。

宏任务与微任务

在ES6及以后的标准中,任务队列被进一步细分为两类:

  • 宏任务(Macrotask) :包括script(整体代码)、setTimeoutsetIntervalsetImmediate(Node.js环境)、I/O操作、UI渲染等。每次执行栈执行完毕,检查宏任务队列,取出一个任务执行。
  • 微任务(Microtask) :包括Promise的回调(.then.catch.finally)、MutationObserverprocess.nextTick(Node.js环境)等。每个宏任务执行结束后,会立即清空当前的所有微任务队列,然后再检查是否有新的宏任务。

执行流程示例

  1. 执行全局脚本,将同步代码压入执行栈。
  2. 遇到异步操作,将其回调函数放入相应的任务队列(宏任务或微任务)。
  3. 当执行栈为空,事件循环检查微任务队列,如果有则全部执行完。
  4. 清空微任务队列后,检查宏任务队列,取出一个任务执行。
  5. 重复步骤3和4,直到宏任务和微任务队列均为空,此时事件循环检查是否还有未处理的宏任务(例如定时器触发),然后继续循环。

通过这种方式,JavaScript能够在保持单线程执行的同时,高效地处理异步逻辑。

17. 代码输出题

第一题:
console.log('1');  
setTimeout(function() {     
  console.log('2');   
  process.nextTick(function() {         
    console.log('3');     
  })     
  new Promise(function(resolve) {        
    console.log('4');        
    resolve();     
  }).then(function() {         
    console.log('5')     
  }) 
}) 
process.nextTick(function() {     
  console.log('6'); 
}) 
new Promise(function(resolve) {     
  console.log('7');     
  resolve(); 
}).then(function() {     
  console.log('8') 
})  
setTimeout(function() {     
  console.log('9');     
  process.nextTick(function() {         
    console.log('10');     
  })     
  new Promise(function(resolve) {         
    console.log('11');         
    resolve();     
  }).then(function() {         
    console.log('12')     
  }) 
})

输出结果 1 7 6 8 2 4 3 5 9 11 10 12

第二题:
console.log(1);
const p = new Promise(r=>setTimeout(r,1000))
setTimeout(()=>{
	console.log(2);
})
await p
console.log(3);
// 1  2  3
第三题:将计时器时间改成0
console.log(1);
const p = new Promise(r=>setTimeout(r,0))
setTimeout(()=>{
	console.log(2);
})
await p
console.log(3);

//1 3  2   

18.为什么要用vue?原生也能做,为什么用vue?

现代mvvm, 让我们远离api, 关注业务,开发速度和性能都更优。 有以下好处:

vue 有以下好处:

  • 易学易用
  • 数据绑定:Vue提供了响应式的数据绑定,当数据变化时,视图会自动更新,反之亦然。这大大减少了手动操作DOM的需要,降低了出错概率,提高了开发效率。
  • 组件化开发:Vue支持组件化开发,允许开发者将UI划分为可复用的组件。这不仅有助于代码的模块化,还能提高代码的重用率,使得复杂应用的管理变得更加简单。
  • 虚拟DOM:Vue使用虚拟DOM(Virtual DOM)来提高性能,通过计算实际DOM与虚拟DOM之间的最小差异来减少不必要的DOM操作,进而提升页面渲染速度。
  • 强大的生态系统:Vue拥有一个庞大的生态系统,包括Vue Router(路由管理)、Vuex(状态管理)、Vue CLI(脚手架工具)等,这些工具和库极大地丰富了Vue的功能,支持开发者进行大型应用的开发。
  • 社区支持:Vue有一个活跃的社区,提供了丰富的插件、教程、文档和问答平台,使得开发者在遇到问题时能够快速找到解决方案。
  • 体积小,性能高:Vue的核心库非常轻量,这使得它特别适合用于构建轻量级的Web应用或是作为移动应用的前端框架(如与Weex结合)。

19.问原生js怎么实现数据双向绑定

Object.defineProperty(vue2)或Proxy(vue3 reactive)或class getter setter(vue3 ref)

20.为什么要用数据状态管理工具

数据状态管理工具(如Redux、Vuex、MobX等)在现代前端开发中扮演着至关重要的角色,特别是在构建中大型应用程序时。以下是使用数据状态管理工具的一些主要原因:

  1. 状态集中管理:数据状态管理工具提供了一个集中存储应用程序状态的地方,使得状态变更变得可预测且易于跟踪。这有助于避免因状态散落在各个组件中而导致的难以调试和维护的问题。
  2. 状态变更可追溯:通过定义明确的状态变更流程(如Redux的actions和reducers),每一项状态变更都有迹可循,有利于团队协作和问题排查,同时也便于实现时光旅行调试等功能。
  3. 提升应用性能:状态管理工具通常会实现状态的高效更新机制,如Redux的reducer纯函数确保每次状态更新都是全新的状态树,有利于虚拟DOM的diff算法高效运行,从而提升应用性能。
  4. 便于状态共享:在大型应用中,多个组件可能需要访问相同的数据。状态管理工具让这些数据成为全局可访问的,无需通过复杂的props drilling(层层传递props)或使用Context API,简化了组件间的通信。
  5. 增强可测试性:集中管理的状态和明确的变更逻辑使得编写单元测试和集成测试变得更加容易。可以独立测试状态逻辑,而不必关心UI组件的细节。
  6. 开发工具支持:大多数状态管理库都配套有强大的开发工具,例如Redux DevTools,它提供了状态变更的详细记录、时间旅行调试、状态快照比较等功能,极大提升了开发效率。
  7. 易于维护和扩展:随着应用规模的增长,良好的状态管理设计可以降低复杂度,使得新成员更容易理解和加入项目,同时为未来的需求变化和功能扩展打下坚实的基础。
  8. 响应式更新:一些状态管理库(如Vue的Vuex)提供了响应式数据绑定,当状态变更时,相关组件自动重新渲染,无需手动处理数据更新到视图的过程。

总之,数据状态管理工具通过提供统一的状态管理方案,增强了应用的可维护性、可测试性和团队协作效率,尤其在复杂应用的开发中,这些优势更为显著。

21.单向数据流是什么?

单向数据流(Unidirectional Data Flow)是一种软件架构设计模式,尤其是在前端应用开发领域,如React、Vue等框架中广泛采用。此模式定义了数据在应用中流动的方向是单一且不可逆的,通常是从父组件流向子组件,以此来管理应用的状态和UI更新。下面是单向数据流的几个关键特征和优势:

  1. 数据流向:在单向数据流中,数据流动遵循一个明确的方向,最常见的模式是从父组件到子组件。父组件通过属性(props)将数据传递给子组件,子组件负责展示这些数据,但不直接修改它们。
  2. 状态提升:当子组件需要改变数据时,它不是直接修改接收到的props,而是通过触发一个事件(如在React中的回调函数,或Vue中的$emit)通知父组件。父组件接收到事件后,会更新自己的状态,并将新的状态值通过props再次传递给子组件,从而引发UI的更新。
  3. 可预测性和可维护性:由于数据流动方向单一,单向数据流使得应用状态的变更路径更加清晰可预测,有助于开发者理解和维护代码。状态变更的原因和过程更容易追踪,降低了bug出现的几率。
  4. 易于调试:状态的变更集中控制,减少了潜在的副作用,使得在调试过程中更容易定位问题,因为数据的源头和流向是明确的。
  5. 组件解耦:子组件不依赖于外部状态,只关注自身的展示逻辑,这使得组件更加独立、可复用。父组件决定何时以及如何更新子组件的数据,增强了组件之间的边界清晰度。

与之相对的是双向数据绑定(Two-Way Data Binding),如AngularJS早期版本中常见,它允许数据在组件间直接双向同步更新,简化了某些交互逻辑,但也可能导致状态管理复杂化,增加维护难度。单向数据流是为了解决这类复杂性而设计的一种模式,尤其适合于构建大型和可维护的应用程序。

22.开放题

有个图片网站,要让用户的用户体验尽可能的好,加载尽可能快,任何手段任何措施都可以,有哪些方法

为了确保图片网站的用户体验尽可能好,加载速度尽可能快,可以采取以下一系列措施:

  1. 图片优化

    • 压缩图片:使用工具(如TinyPNG、Squoosh或ImageOptim)压缩图片,减少文件大小而不牺牲太多画质。
    • 选择合适格式:根据图片类型选择最佳格式,如JPEG适用于照片,PNG用于透明背景,WebP提供更好的压缩比率。
    • 适当尺寸:确保图片尺寸与展示容器匹配,避免上传过大的图片并在HTML中使用正确的width和height属性。
  2. 懒加载(Lazy Loading)

    • 只在图片即将进入可视区域时才加载图片,减少初始页面加载时间。
  3. 使用CDN(内容分发网络)

    • 将图片托管在CDN上,利用全球节点加速图片传输,减少用户访问延迟。
  4. 缓存策略

    • 设置浏览器缓存策略,如Cache-Control和Expires头,使浏览器缓存已加载过的图片。
  5. 服务器端优化

    • 升级服务器硬件或迁移到响应更快的服务提供商,如使用SiteGround这样的高性能主机。
    • 优化服务器配置,比如使用HTTP/2协议,启用Gzip压缩。
  6. 代码层面的优化

    • 减少HTTP请求数,合并CSS和JavaScript文件。
    • 使用异步加载脚本,避免阻塞页面渲染。
  7. 预加载和预读取

    • 对于关键图片,可以使用预加载。
    • 预读取技术可以预测用户行为,提前加载可能需要的图片资源。
  8. 图片分片和渐进式加载

    • 对于大图,可以使用图像分片技术,先加载低质量版本,然后逐步加载高质量版本。
    • 渐进式JPEG格式先加载图片轮廓,逐渐填充细节,提高用户体验。
  9. 无损和有损压缩结合

    • 对于视觉上要求不高的图片,可以采用有损压缩,而对于重要图片使用无损压缩。
  10. 监控和分析

    • 使用Google PageSpeed Insights、Lighthouse等工具定期检测页面性能,并根据建议进行优化。
    • 监控网站性能,及时发现并解决加载速度问题。
  11. 代码分割和路由懒加载

    • 如果是单页应用,确保代码按需加载,减少首屏加载时间。
  12. 减少图片数量

    • 在不影响用户体验的前提下,合理减少页面中的图片数量,尤其是首页。

综合运用这些策略,可以显著提升图片网站的加载速度和用户体验。

23.算法题

用一个产生1-7的随机整数的随机数产生器实现产生1-10的随机整数的随机数发生器,要求概率均等 const random7 = ()=>Math.floor(Math.random() * 7 + 1),只能调用random7随机数产生器

function random10() {
    // 通过两次调用random7()来生成一个1到49之间的数字(7*7=49)
    let num = (random7() - 1) * 7 + (random7() - 1);
    
    // 当生成的数字落在1到34范围内时,直接映射到1到10
    // 因为 34/49 接近于期望的 10/10+1 的比例,这样可以保证大致的等概率
    if (num <= 34) {
        return num % 10 + 1;
    } else { // 如果数字大于34,则重新调用random10()直到得到合适的值
        return random10();
    }
}

总结

羡慕我打针哥,腾讯面试果然JS基础考的很深入,面试当场必然大汗淋漓,畅快畅快。

如果您也喜欢我的面经,欢迎点赞收藏,评论区讨论。 您的支持,是我写二面,三面面经的动力,谢谢!