腾讯二面,打针哥的大厂集邮之路

发布于:2024-05-10 ⋅ 阅读:(18) ⋅ 点赞:(0)

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

本文为旅梦开发团原创文章,禁止转载,违者必究.

全目录

打针哥的腾讯面试系列文章链接整理如下:

前言

五一过后,还在掘金读面经的"苦孩子"们,想必和我一样慌的一批!

三月面百度,小破站,吃的挂面,反正时间还早无所谓. 4月战快手,撕滴滴,追小红书,感觉都答的挺好的.梦里都在庆祝了,醒来却是一天一封感谢信...

中小厂offer确实拿了不少,皆拒之.哥们手里握着的是三少爷的剑啊.再拼一把,努力抓住五月大厂补录.

继续打开打针哥给我的腾讯面试题,学习.真羡慕这位机房扫地僧啊,大家可以看,看打针哥的腾讯一面面试题.

大家给五月还在奋斗的孩子点个赞吧,一起加油去大厂.

腾讯一面回顾

大厂非常重视前端基础,在记录的一面面试题中,我们看到腾讯面试官考查了作用域,闭包,原型,new 手写, es6, promise, event loop 等js 核心知识点.我对照打针哥的回答,确实发现自己一些理解不够细,不够深入的地方.面试官从基础必考题出发,却突然有闭包内存泄露这种比较深入,十分考验语言底层能力与实战的问题出现.打针哥面试说他酣畅淋漓,如果面试的是我,怕是早已被面试官口水喷成了落汤鸡. 所以,建议大家再review一遍这些必考题,我们几乎拿下大厂offer, 即差一点点, 就在此处.

除了八股文外,一面面试题还考查了vue相关知识点,问题也是蛮刁钻的.只有扎实的开发经验,才能对单向数据流,状态管理等理解的更清楚.

最后,开放题看我们对于优化的认识,算法题也还行.

继续前行,看二面面试题.

二面面试题

1. 自我介绍

每次面面试官都不一样嘛,上文提到其实自我介绍很重要.5W + 怎么学习前端的 + AI兴趣. 可以迅速的破冰,并让面试官对我们感兴趣.AI这块面试官也在学,可能会与我们有共同话题.一聊,可能就high了.

5W: 我是来自XX大学XX学院大三的李XX,目前在找前端实习工作.

怎么学习的: 我在X年前,决定走前端方向,看你不知道的JavaScript,学习js 语法和底层,后在极客时间接触到大圣老师的vue课程,深入学习了vue全家桶vue底层和源码.在做项目的时候,用koa开发了api服务,了解node的相关知识.喜欢写作,经常在掘金发表文章,目前已有70篇文章,Lv5.

AI兴趣: 最近对AI挺感兴趣的,基于LLM的开发将是未来的主流开发方式.了解huggingface transformers 等库实现nlp任务,了解 OpenAI的相关接口开发AIGC应用,也在学习python,AI应用开发框架LangChain等. AI应用已带来全新用户开发体验,前端拥抱之,未来会不断的学习AI相关知识,并应用到开发中去.

2.问项目做了有关后端的东西吗

一面主要以前端知识点为主,二面果然就考察后端了.

  • MVC 分层

前端设计模式为MVVM, 后端是更为经典的MVC.

用户从路由进来, 参数校验/业务逻辑交给controller. controller 实例化相应数据表模型,完成数据操作(细分的话,会交给service层).最后,因为项目是为前端提供提供JSON API,所以这里不会渲染模板,而是返回JSON.

image.png

建议大家可以去学习,代码写的超级优秀.

  • 中间件

node 开发的核心思想就是中间件思想, 通过添加相应中间件即可为用户提供各种服务.

项目使用了cors,json,router等中间件,也定制了currentUser 用户鉴权,defaultErrorHandler错误处理中间件.

除了入口文件外,在业务流程中也大量使用中间件概念,将业务拆分成一个个的中间件,便于复用. 比如:

// 发表文章前 使用authGuard 中间件鉴权, 如果 未登录, 就会提前返回401
router.post('/posts', authGuard, postController.store);
// 文章列表页  sort中间件负责排序  filter中间件设置where,  paginate 中间件专门分页,太牛逼了
router.get(
  '/posts',
  sort,
  filter,
  paginate(POSTS_PER_PAGE),
  postController.index,
);
  • 大文件上传(难点)

欢迎通读 , 把代码加到自己的项目中, 然后开始装... 这里就不展开描述了.

  • 精准的状态码(亮点)

在错误处理中间件中, 详细且准确的使用状态码与messages, 告知用户.

export const defaultErrorHandler = (
  error: any,
  request: Request,
  response: Response,
  next: NextFunction,
) => {
  if (error.message) {
    console.log('🚧', error.message);
  }

  let statusCode: number, message: string;

  /**
   * 处理异常
   */
  switch (error.message) {
    case 'NAME_IS_REQUIRED':
      statusCode = 400;
      message = '请提供用户名';
      break;
    case 'PASSWORD_IS_REQUIRED':
      statusCode = 400;
      message = '请提供用户密码';
      break;
    case 'USER_ALREADY_EXIST':
      statusCode = 409;
      message = '用户名已被占用';
      break;
    case 'USER_DOES_NOT_EXIST':
      statusCode = 400;
      message = '用户不存在';
      break;
    case 'PASSWORD_DOES_NOT_MATCH':
      statusCode = 400;
      message = '密码不对';
      break;
    case 'UNAUTHORIZED':
      statusCode = 401;
      message = '请先登录';
      break;
    case 'USER_DOES_NOT_OWN_RESOURCE':
      statusCode = 403;
      message = '您不能处理这个内容';
      break;
    case 'FILE_NOT_FOUND':
      statusCode = 404;
      message = '文件不存在';
      break;
    case 'TAG_ALREADY_EXISTS':
      statusCode = 400;
      message = '标签已存在';
      break;
    case 'POST_ALREADY_HAS_THIS_TAG':
      statusCode = 400;
      message = '内容已经有这个标签了';
      break;
    case 'UNABLE_TO_REPLY_THIS_COMMENT':
      statusCode = 400;
      message = '无法回复这条评论';
      break;
    case 'FILE_TYPE_NOT_ACCEPT':
      statusCode = 400;
      message = '不能上传此类型文件';
      break;
    case 'NOT_FOUND':
      statusCode = 404;
      message = '没找到 ~~ 🦖';
      break;
    case 'USER_NOT_FOUND':
      statusCode = 404;
      message = '没找到这个用户 ~~';
      break;
    case 'PASSWORD_IS_THE_SAME':
      statusCode = 400;
      message = '要修改的密码不能与原密码一样';
      break;
    default:
      statusCode = 500;
      message = '服务暂时出了点问题 ~~ 🌴';
      break;
  }

  response.status(statusCode).send({ message });
};
  • 使用了ts

    typescript特别适合中大型项目开发, 后端对稳定性要求更高,所以ts更适合.

  • 布署到服务器

在腾讯云,完成了项目部署.使用pm2管理和守护node 进程. 各位可以努力 ,特别加分.

3. 闲聊了几分钟,问有没有offer

果然,面试官对刚才的回答比较满意.介绍了下岗位相关业务,工作氛围啥啥的.

大厂面试一般一面面试官是直接用人的,之后的面试官要不是一面的leader,或隔壁组帮判断下的. 一面面试官面试已非常认真考查, 所以二面在觉得OK后, 就会释放好意.

offer 当然有, 各位, 就算手里没啥拿的出手的,也要说一些, 最后补一句,如果腾讯能给offer, 立马来...

谁不想从马爸爸手里领个开门红包?

4. 问ES6的新特性

一面也问到了, 大家看

这个问题的回答逻辑可以是:

  • ES6 让JS 成为像JAVA一样大型企业级开发语言

    1. 块级作用域 let const
    2. class ES6 引入了基于原型的面向对象编程模型的类,提供了更接近传统面向对象语言的语法糖,使得定义和继承类变得更加直观。
    3. 模块(module) 通过importexport关键字支持原生模块系统,这改善了代码组织结构,促进了代码重用,并有助于解决命名冲突问题。 vite 基于原生的模块化,更快.
    4. Map和Set: 新的数据结构提供了更高效和灵活的方式来处理键值对集合和不重复值的集合,补充了原有的数组和对象。WeakMap,WeakSet 弱使用,更好地规避内存泄露.
    5. for...of循环: 提供了一种新的迭代数组、Set、Map、类数组对象或任何可迭代对象的方式,语法更简洁,使用更广泛。
  • ES6 让代码更简洁,优雅,提高可读性

    1. 箭头函数: 箭头函数提供了更简洁的函数表达式书写方式,自动绑定this上下文,简化了函数内部作用域的处理,对于异步编程和回调函数特别有用。
    2. 模板字符串: 使用反引号(``)定义的字符串可以包含嵌入的表达式,通过${expression}插入变量或表达式的值,使得字符串拼接更自然,更易于阅读。
    3. 解构赋值: 允许快速从数组或对象中提取值到变量中,这简化了数据处理和交换过程,使得代码更加简洁。
    4. 默认参数、展开运算符和rest参数: 提供了更灵活的函数参数处理方式,使得函数定义更加清晰,同时也方便了数组和对象的操作。
  • 解决回调地调,更好地处理异步

    1. promise 和 generator/yield
    2. Promise和async/await: 这些异步编程特性使得处理异步操作变得更加流畅和同步化,提高了代码的可读性和可维护性,尤其是在处理复杂的异步逻辑时。
  • proxy

    相比于defineProperty, 拥有更多拦截器,性能更好.

5. let,const是为了解决什么问题

主要是为了解决 JavaScript 之前在变量声明和作用域管理方面存在的几个关键问题:

  • 变量提升(Hoisting)问题 var 声明的变量会自动提升到其所在作用域的顶部,这可能导致在变量声明之前就访问到该变量,默认初始化为 undefined,这往往会导致意料之外的行为。let 和 const 不会发生变量提升,它们要求变量在使用前必须声明,增加了代码的可预测性(可读性)。暂时性死区.

  • 函数作用域与全局污染:

    使用 var 声明的变量在函数作用域内有效,但若在任何函数外部声明,则自动成为全局变量(在非严格模式下),这容易导致全局命名空间的污染。let 和 const 支持块级作用域,这意味着它们声明的变量仅在其所在的代码块(例如if语句、for循环或一对大括号内)内有效,避免了不必要的全局污染。

  • 循环变量的封闭性:

    在循环中使用 var 声明的计数器变量会在每次循环迭代中保持相同的内存位置,导致所有循环迭代共享同一变量实例,可能引发意外行为。而使用 let 可以为每次迭代创建一个新的变量副本,解决了这一问题。

  • 常量的定义:

    在ES6之前,没有直接的方式声明不可变的变量。const 的引入填补了这一空白,允许开发者声明一个其值在初始化后不能被重新赋值的变量,这对于定义程序中不应更改的配置项、API密钥等非常有用。

  • 暂时性死区(Temporal Dead Zone, TDZ)

    let 和 const 创建的变量在声明它们的代码块或之前的部分被称为“暂时性死区”,在这个区域里访问这些变量会抛出引用错误(ReferenceError)。这一机制进一步强化了变量声明的严谨性,减少了潜在的错误。

6. promise主要是解决什么问题

由于javascript单线程,代码中充斥着大量异步任务,导致回调地狱,代码编写顺序和执行顺序不一致的可读性问题.

  • 多重回调函数带来的回调地狱问题可以由promise.then 转成顺序链式执行,方便安排多重复杂异步任务
  • promise + async/await 比then 更优雅
  • 使用.catch可以统一处理整个Promise链中的错误,不必在每个异步步骤中单独处理错误。
  • 可以将复杂的异步逻辑封装成Promise对象,便于在不同地方复用。
  • Promise有三种状态(pending, fulfilled, rejected),便于跟踪异步操作的完成情况及结果传递

7. await相比promise有啥不一样

  • 语法风格

    promise基于链式调用,通过 .then() 和 .catch() 处理结果或错误。 async/await更像同步代码,可以直接使用 await 关键字等待 Promise 完成,看起来更像是直接处理结果值。

  • 使用条件

    promise直接与异步操作的结果处理关联,需要手动管理成功和失败的回调。

    必须在 async 函数内部使用,使该函数自动返回一个 Promise,并能够暂停函数的执行直到 await 的 Promise 完成。

  • 错误处理

    promise通过 .catch() 或在 .then() 的第二个参数中捕获错误。

    async/await可以直接使用 try...catch 语句块来捕获错误,使得错误处理更贴近同步代码的风格。

  • 代码可读性

    promise相比于回调函数有所提高,但仍可能造成嵌套过深的问题。

    async/await大幅提高代码的可读性和简洁性,几乎与同步代码无异,易于理解和维护。

  • 控制流程

    promise需要手动链式调用来控制多个异步操作的顺序。

    async/await自然地支持序列化多个异步操作,无需显式链式调用,通过 await 直接等待前一个操作完成。

总结来说,await 是基于 Promise 的一种更高级的异步处理方式,它通过简化异步操作的编写和错误处理,使得异步代码的可读性和可维护性得到了显著提升。然而,await 必须在 async 函数中使用,这限制了它的使用场景,但同时也明确了这部分代码是进行异步操作的意图。

8. 为什么用await后,promise不用.then了

当使用 await 关键字时,你不再直接需要调用 .then() 方法来处理 Promise 的解决结果,这是因为 await 本身的设计就是用来“等待”Promise 完成(无论是resolve还是reject)并直接返回Promise的结果值或抛出错误。这种方式让异步代码看起来更像是同步代码,提高了可读性和易维护性。

  • 为什么可以不用 .then()
  1. 自动等待await 会使代码执行暂停在那一行,直到等待的 Promise 完成(解析或拒绝)。这意味着你不需要通过 .then() 来指定一个回调来处理结果。
  2. 更直接的值处理:一旦 Promise 解决,await 后续的表达式将直接获得Promise解决的值,你可以像处理普通同步返回值那样处理它。
  3. 错误处理简化:在 await 的使用中,如果 Promise 被拒绝,可以自然地使用 try...catch 语句来捕获错误,而不是在链式调用中添加 .catch()
// 使用 `.then()` 的 Promise 风格:
function fetchUserData(userId) {
  return getUserById(userId)
    .then(user => getUserProfile(user.profileId))
    .then(profile => ({
      user: user.name,
      profile: profile.bio
    }))
    .catch(error => console.error('Error fetching data:', error));
}

// 使用 `await` 的风格

async function fetchUserData(userId) {
  try {
    const user = await getUserById(userId);
    const profile = await getUserProfile(user.profileId);
    return {
      user: user.name,
      profile: profile.bio
    };
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

在使用 await 的版本中,可以看到代码结构更扁平,逻辑更直观,类似于同步代码的读写方式,减少了回调的嵌套,使得错误处理也更加集中和直接。

9. 微任务解决了什么问题

微任务有: Promise的回调,Async/Await,MutationObserver,Object.observe(已废弃),process.nextTick

  1. 确保任务的及时执行:微任务在当前执行栈结束之后、下一个宏任务开始之前执行。这意味着微任务中的代码可以立即得到执行,非常适合需要在当前事件循环周期内快速响应的操作,如DOM更新、数据绑定等。
  2. 提高异步操作的效率:相比于宏任务(如setTimeoutsetInterval等),微任务的执行时机更早,减少了等待时间,提高了执行效率,特别是在连续的异步操作中,能够更快地得到结果并继续后续处理。
  3. 简化异步代码流程:微任务使得异步代码可以按照更自然的顺序书写,无需为了等待某个操作完成而嵌套回调函数,尤其是在使用Promiseasync/await时,代码更加清晰和易于理解。
  4. 保证数据一致性:在处理需要立即反映到界面或状态中的操作时(如表单验证后的即时反馈),通过微任务可以在DOM更新之前完成数据处理,确保用户看到的是最新的状态。
  5. 控制异步流程的精细度:微任务提供了比宏任务更细粒度的控制,有助于开发者精确控制哪些操作需要立即执行,哪些可以稍后处理,从而优化应用性能和用户体验。

10. ES6的class在new 的时候相比ES5有什么不一样的地方

ES6的class在使用new关键字实例化时,相比ES5的传统构造函数,有几个主要的不同点:

  1. 语法糖

    • ES6的class是基于ES5的构造函数和原型链的一个语法糖,使得代码结构更接近面向对象的编程风格,更易于阅读和理解。
  2. 类的方法是不可枚举的

    • 在ES6的class中,定义的方法默认不会出现在实例的[[Enumerable]]属性描述符中,也就是说,它们默认是不可枚举的,而ES5中通过原型添加的方法默认是可枚举的。
  3. 严格模式

    • ES6的class体默认是运行在严格模式(use strict)下的,无论是否在全局文件中声明了严格模式,这有助于消除一些不安全的用法和提升代码质量。
  4. 构造函数

    • ES6的class中通过constructor方法定义构造函数,这是实例化时自动调用的。而在ES5中,构造函数通常直接定义在函数体中,名称与类名相同。
  5. 继承机制

    • ES6引入了extends关键字来实现继承,简化了继承的语法。子类通过super关键字调用父类的构造函数和方法,这比ES5中通过修改原型链(prototype)和使用callapply方法来继承更为直观和简洁。
  6. 静态方法和属性

    • ES6的class支持静态方法和属性,使用static关键字定义,这些方法和属性属于类本身而非实例。在ES5中,虽然可以通过在构造函数上直接定义属性和方法达到类似效果,但ES6的静态特性更明确且易于区分。
  7. this的绑定规则

    • 在ES6的class中,constructor以及其他非静态方法内的this自动绑定到实例对象,这减少了在ES5中因忘记绑定this而导致的常见错误。
  8. 实例和原型的生成方式

    • ES6的new操作在内部处理了原型链的建立,使得开发者无需手动设置__proto__或修改prototype,降低了出错的可能性。

综上,ES6的class通过提供更简洁、更面向对象的语法,以及内置的继承机制和严格模式,使得对象的创建和继承更加规范和高效。

11. class在new的时候做了什么事情

当使用new关键字实例化一个ES6的class时,大致经历了以下几个步骤:

  1. 创建一个新的空对象

    • JavaScript引擎首先会创建一个新的空对象,这个对象将作为新实例的基础。
  2. 设置原型链

    • 新创建的对象的内部[[Prototype]]属性(可访问为__proto__)被设置为构造函数的prototype属性所指向的对象。这意味着新对象可以从构造函数的原型上继承方法和属性。
  3. 绑定this并执行构造函数

    • new操作会改变构造函数内部的this绑定,使其指向新创建的对象。然后,它调用构造函数,使用传递给new的操作数作为参数(如果有)。在这个过程中,构造函数可以初始化新对象的状态,比如设置属性值等。
  4. 返回新对象

    • 如果构造函数没有显式返回一个对象(或者返回的是非对象类型,如基本类型的值),那么new操作会自动返回第一步中创建的新对象。如果构造函数显式返回了一个新对象(且该对象不是null或基本类型值),则返回该对象,而不是第一步创建的原始对象。

简而言之,new关键字执行的过程确保了对象的正确创建、原型链的正确设置、this上下文的绑定以及可能的属性初始化,从而实例化一个类的全新对象。

12. 我看你用到了token,这个token是个什么东西

JWT(JSON Web Token)是一种紧凑、安全的 token 格式,用于在各方之间安全地传输信息,作为身份验证和授权的机制。

13. token包含了哪些信息

JWT Token包含三部分信息:

  1. 头部(Header) :包含令牌的类型(通常是JWT)和使用的签名算法(如HS256)。
  2. 有效载荷(Payload) :存储实际信息,如用户ID、角色、过期时间等,不敏感信息且可选加密。
  3. 签名(Signature) :结合头部与载荷,使用密钥经过算法计算得出,确保Token未被篡改。

一个JWT Token的实例通常长这样(已简化示例):

1eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

这里分为三部分:

  1. 头部(Header) :

    1eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    

    解码后(Base64解码)为:

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    

    表明该JWT使用HS256算法加密,类型为JWT。

  2. 有效载荷(Payload) :

    1eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
    

    解码后(Base64解码)为:

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    

    包含了用户ID(sub字段)、用户名(name字段)及签发时间(iat字段)等信息。

  3. 签名(Signature) :

    1SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    

    这部分是基于头部和载荷以及一个密钥(不在Token中显示)通过HS256算法计算出来的,确保Token的完整性和真实性。

14. 为什么加盐,不加会有什么问题

JWT加盐主要是为了增强安全性,尽管“加盐”这一术语在JWT的标准讨论中并不常用,但可以理解为你提到的是在JWT签发时增加安全性的措施。实际上,JWT的安全性增强通常涉及到以下几个方面,这些可以类比于“加盐”的概念:

  1. 签名密钥(Secret Key) :JWT通过使用密钥对头部和载荷进行签名,生成签名部分。这个密钥就像是“盐”,使得外部无法轻易伪造JWT。不使用密钥签名或使用弱密钥将使JWT容易被篡改或伪造,从而可能导致安全漏洞。
  2. 使用HTTPS:虽然不是直接对JWT“加盐”,但通过HTTPS传输JWT可以防止中间人攻击,确保传输过程中的安全性。
  3. 过期时间(Expiration Time) :在JWT的Payload中设置一个短的过期时间,可以减少令牌被盗用后的影响范围。这虽不直接涉及“加盐”,但有助于提升整体安全性。
  4. 刷新令牌(Refresh Tokens) :不直接关联到JWT本身的“加盐”,但使用刷新令牌机制可以进一步安全地管理JWT的有效期,即使JWT泄露也不至于长期暴露用户凭证。

如果不采取这些安全措施,特别是不正确地签署JWT或使用弱密钥,会面临如下问题:

  • 易被篡改:攻击者可以修改JWT的内容,比如改变用户权限等级,进而非法访问受限资源。
  • 重放攻击:攻击者可以捕获JWT后无限期地重复使用,尤其是在JWT没有过期时间或过期时间很长的情况下。
  • 信息泄露:JWT通常包含用户标识信息,如果未加密或签名不当,这些信息可能会被轻易读取。

因此,“加盐”或者说采取适当的签名和传输安全措施,对于维护JWT的安全性至关重要。

15. token存在哪里,存在cookie有什么问题

Local Storage/Session Storage: 允许在客户端存储较大的数据量,且不受同源策略限制,且不会随HTTP请求自动发送,需要手动管理(axios 请求拦截 Authorization)。

存在cookie中的缺点

  • 安全性风险:由于cookies可以受到跨站脚本(XSS)攻击,如果cookie没有设置HttpOnly标志,恶意脚本可能能够读取到token,从而盗取用户身份。
  • CSRF攻击:默认情况下,浏览器会自动在跨站请求中发送cookie,除非设置了SameSite属性,这可能使应用程序容易受到跨站请求伪造(CSRF)攻击。
  • 大小限制:cookie有大小限制,通常不超过4KB,而JWT可能因为包含较多信息而超过这个限制。
  • 控制难度:服务器端难以直接控制cookie的有效期和清除策略,尤其是在多域名或子域名环境下。

16. cookie和session有什么区别,session你知道吗

当然,Cookie和Session都是用于用户身份验证和会话管理的技术,但它们在存储位置、数据大小、安全性、生命周期以及对服务器资源的影响等方面有所不同:

  1. 存储位置

    • Cookie:数据存储在客户端的浏览器上。
    • Session:数据存储在服务器端。
  2. 数据大小

    • Cookie:单个Cookie的大小通常限制在4KB左右,并且每个域名下的Cookie数量也有限制(不同浏览器限制不同,例如IE6及以下每个域名最多20个,现代浏览器更多)。
    • Session:存储大小理论上只受服务器内存限制,没有Cookie那样的硬性大小约束。
  3. 安全性

    • Cookie:较不安全,因为数据存储在客户端,可能被用户查看、修改或盗窃(尽管可以使用HttpOnly标记来防止JavaScript访问,降低XSS攻击风险)。
    • Session:相对安全,因为数据存储在服务器端,不易被直接访问,但需要防范Session劫持和重放攻击。
  4. 生命周期

    • Cookie:可以通过设置到期时间来控制其生命周期,可长期保持。
    • Session:通常依赖于用户的会话周期,用户关闭浏览器或长时间不活动后会失效,但也可以通过服务器配置来延长。
  5. 服务器资源

    • Cookie:不占用服务器资源,减轻了服务器负担。
    • Session:需要服务器存储会话数据,当用户量大时,会占用内存资源,影响服务器性能。
  6. 实现机制

    • Session通常依赖Cookie来传递会话ID(session id),即首次建立会话时服务器生成一个唯一标识并发送给客户端,客户端之后的每次请求都会带着这个标识回到服务器,服务器根据此标识找到对应的会话数据。

17. 我怎么知道localstorage还有多少空间可用,程序能读到吗

面试官是要整死打针哥吗?就是不按套路出牌,为他捏把汗。

这时候有两种做法:一是表明不太清楚,但请面试官教教自己,好学的同时,快速理解并举一反三也挺管用的。

另外,就是死磕。背背八股,刷刷面经是不太可能拿下腾讯这样的顶级offer的。在大厂,会有太大可能临时指派有挑战性的任务。有比较好的抗压能力,以及在压力下能保证清晰思考,拿出解决方案,才是王道。静下来思考,虽然js 没有给出localStorage剩余空间的API, 我们将总空间减去已用空间(字符串的长度)就好了。

function calculateLocalStorageUsage() {
    var storage = window.localStorage;
    var totalSize = 0;
    
    for (var key in storage) {
        if (storage.hasOwnProperty(key)) {
            // 计算每个键值对的字节长度,包括key和value
            totalSize += key.length + storage[key].length;
        }
    }
    
    // 假设localStorage的最大容量为5MB (5 * 1024 * 1024 字节)
    var maxStorageSize = 5 * 1024 * 1024;
    var remainingSpace = maxStorageSize - totalSize;
    
    console.log("已使用空间: " + (totalSize / 1024).toFixed(2) + " KB");
    console.log("剩余空间: " + (remainingSpace / 1024).toFixed(2) + " KB");
}

// 调用函数检查localStorage使用情况
calculateLocalStorageUsage();

18. localstorage支持跨域吗

不支持

19. 什么是跨域

跨域是指一个网页尝试访问与自身不在同一域名、协议或端口下的资源时,浏览器基于同源策略阻止这种访问的行为。

20. 预检请求是干嘛的

预检请求就是浏览器在正式跨域请求前进行的一次“试探性”请求。

先向浏览器发送一个HTTP OPTIONS请求, 以确认请求是否安全可接受。服务器通过响应告知浏览器是否允许实际请求携带指定的HTTP方法、头信息等进行跨域访问,从而防止跨域请求对服务器造成未预期的影响。

比如:

浏览器向服务器发送的请求报文如下:

OPTIONS /api/data HTTP/1.1
Host: example.com
Origin: http://client.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

客户端(origin为http://client.example.com)想要向example.com/api/data路径发起一个POST请求,并且希望携带Content-Type头部。在实际发送POST请求之前,它先发送了一个OPTIONS请求来询问服务器是否允许这样的跨域操作

服务器Options预检请求:

const express = require('express'); 

const app = express(); // 处理OPTIONS请求,主要是为了CORS预检请求 

app.options('*', 
    (req, res) => { // 允许所有源进行跨域请求 
        res.header('Access-Control-Allow-Origin', '*'); // 允许的HTTP方法 
        res.header('Access-Control-Allow-Methods', 
        'GET,PUT,POST,DELETE,OPTIONS'); // 允许的请求头 
        
        res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); // 如果是预检请求,直接返回200状态码,不进行实际操作 
        if (req.method === 'OPTIONS') { 
            res.sendStatus(200); 
        } else { // 处理实际请求逻辑... } 
     }
);         

21. http3了解不,解决了什么问题

http协议是必问的,但问http3就有点不厚道了。准备了一堆的缓存、多路复用,面试官问http3...

考查的真广,需要了解的事无巨细。

HTTP/3主要旨在解决HTTP/2在某些网络条件下遇到的问题,特别是与TCP协议相关的局限性。以下是HTTP/3解决的关键问题:

  1. TCP的队头阻塞问题: 虽然HTTP/2通过多路复用缓解了队头阻塞,但在TCP协议层面,一旦有数据包丢失,TCP会暂停后续所有数据包的发送直到丢失的数据包被重传并确认,这可能导致已到达的数据也无法被应用层处理,影响性能。HTTP/3基于QUIC(Quick UDP Internet Connections)协议,使用UDP而非TCP,QUIC内置了自己的重传和流量控制机制,能更高效地处理丢包和乱序问题,几乎消除了队头阻塞现象。
  2. 连接建立的延迟: TCP连接需要经历三次握手,加上TLS握手,这会增加初始连接的延迟。QUIC利用了0-RTT(Zero Round-Trip Time)特性,在之前建立过连接的客户端上,可以近乎即时地开始发送加密数据,显著减少了首次请求的延迟。
  3. 更好的拥塞控制和恢复机制: QUIC具有更先进的拥塞控制算法,能够更快地适应网络状况的变化,减少网络拥塞对数据传输的影响,提高传输效率。
  4. 改进的安全性: QUIC将TLS加密直接集成到协议中,确保从第一个数据包开始就是加密的,增强了安全性。此外,每个QUIC连接都有自己的加密密钥,即使在同一主机上的不同连接也互不影响,提升了安全性隔离。

考虑到腾讯的核心是实时通讯,对协议(TCP/IP/HTTP/Socket/UDP)考察的比较深一点,忍了吧。

22.说下https

HTTPS是超文本传输安全协议,它在HTTP基础上加入了SSL/TLS层,通过对称加密/非对称加密和身份认证确保数据传输的安全性、完整性和防篡改性,保护用户隐私和数据安全。

23.你知道他大概的握手或者证书验证的流程吗

  • 三次握手

TCP三次握手确保可靠连接建立, 即确保双方都能发送消息和接收消息。

  1. 客户端发送SYN包到服务器,请求连接;
  2. 服务器回复SYN+ACK包,确认收到并同意连接;
  3. 客户端再发ACK包,确认准备完毕。 至此,双方确认连接建立,可开始数据传输。
  • 证书验证的流程

客户端请求HTTPS网站,服务器发送证书;客户端验证证书有效性,包括来源、有效期、域名匹配等;通过后,客户端生成会话密钥并用证书公钥加密发送给服务器;服务器用私钥解密得到密钥,之后双方用此密钥进行安全通信。

24. 那CA证书可以伪造吗

CA证书可以伪造,但难度极高,因为需要全球受信的根证书信任链,且伪造行为易被检测,会导致信任体系崩溃。

25. 我怎么知道这个证书是合法有效的

查看证书是否由受信任CA签发,检查有效期、域名匹配,验证签名未被篡改,并查询证书未被吊销,多数浏览器会自动完成这些验证。

26. http缓存怎么工作的

HTTP缓存通过强制缓存和协商缓存工作。强制缓存直接使用本地副本(若未过期)。协商缓存则先询问服务器资源是否更新,未更新则用缓存版本。

协商缓存主要通过以下HTTP头部信息实现,这里以表格形式展示:

客户端请求头部 作用
If-Modified-Since 携带上次资源的最后修改时间,询问服务器资源是否有更新
If-None-Match 携带上次资源的ETag(实体标签),用来标识资源唯一性
服务器响应头部 作用
Last-Modified 响应中包含资源最后修改的时间戳
ETag 响应中提供资源的唯一标识符,用于变更检测

当客户端再次请求资源时,会发送上述请求头之一或两者皆有,服务器根据这些头信息判断资源是否已更改,未更改则返回304 Not Modified状态码,告知客户端使用本地缓存。

27. eTag是干什么用的,这个值是怎么生成的

ETag(Entity Tag)是HTTP协议中用于确定请求资源当前版本或状态的一个响应头。它的主要用途是在于实现高效的网页缓存和减少网络传输,具体功能如下:

  1. 缓存验证:当客户端再次请求一个资源时,会将之前收到的ETag值放在请求头的If-None-Match字段中发送给服务器。服务器比较当前资源的ETag与请求中的ETag,如果两者相同,说明资源没有改变,服务器就会返回一个304 Not Modified状态码,指示客户端直接使用本地缓存的副本,而不需要重新下载完整的资源内容。
  2. 避免重复提交:在某些场景下,ETag也可以帮助防止因网络延迟或重试机制导致的重复POST或PUT请求带来的副作用,即如果服务器已经处理了一个请求,后续带有相同ETag的请求到达时,可以判断为重复请求并忽略。

ETag的生成没有统一的标准,取决于服务器的实现,通常基于以下几个因素或它们的组合来生成:

  • 内容哈希:最常见的方式是计算资源内容的哈希值(如MD5、SHA-1或SHA-256等),这样资源内容的任何变化都会导致哈希值的不同。
  • 修改时间:使用资源的最后修改时间戳作为基础来生成ETag。
  • 文件属性:包括文件大小、权限等元数据的组合。
  • 自定义逻辑:服务器可以根据特定的业务需求,结合上述一种或多种方式,甚至其他信息来生成独特的ETag。

由于ETag的灵活性,服务器可以根据资源的具体情况选择最适合的生成策略,以达到最佳的缓存管理和性能优化效果。

28. 根据你的项目,具体到某个场景你觉得哪些地方可以适用协商缓存

  • 商品列表页: 商品数据可能频繁更新但不需即时刷新,每次加载时可通过ETag或Last-Modified检查商品列表是否有所变动,未变则直接使用缓存
  • 商品详情页: 类似商品列表,商品详情虽然会更新(如价格、库存),但非每次浏览都需要最新数据,可实施协商缓存策略减少请求。

但说实话,项目没有写的这么牛逼....

29. 什么场景适合用强缓存,什么时候适用协商缓存

强缓存适用场景示例:

  1. 网站Logo图片:因为Logo很少发生变化,可以设置一个较长的有效期,让浏览器直接从本地缓存加载,减少对服务器的请求,加速页面渲染。
  2. 常用CSS样式表:例如,一个网站的基础样式表文件,这些文件内容相对固定,通过设置强缓存可以让用户在多次访问网站时直接使用本地缓存,提升页面加载速度。

协商缓存适用场景示例:

  1. 新闻详情页面:新闻内容可能每天或每小时更新,使用协商缓存,首次访问时从服务器获取,之后每次访问前,浏览器发送If-Modified-Since或If-None-Match请求头,询问服务器内容是否更新,未更新则直接使用缓存,既确保用户看到最新内容,又减少不必要的数据传输。
  2. 应用的API数据:假设一个天气查询API,虽然天气数据实时变化,但短时间内可能不会大幅变动,客户端可以存储上次获取的数据,并在下次请求时携带上次的ETag或Last-Modified时间,服务器根据这些信息判断是否返回新数据,这样既能保证用户获取到较新的天气信息,又能避免频繁的全量数据请求。

30. 如果你网站的logo资源变更了,如何让他之前的强缓存失效,重新获得新的资源

改变资源URL或添加查询参数变更版本号,例如logo.png?ver=2,或者设置新的Cache-Control指令和ETag,确保浏览器不再使用旧缓存。

31. 如何知道你这个请求和之前的请求是同一个请求

通过对比URL、ETag或Last-Modified时间戳,可识别请求是否针对相同资源。

这一路网络相关的考题,着实有点难顶,还好结束了。

32. 求10000的阶乘

计算10000的阶乘是一个非常大的数字计算,普通的递归或循环很容易导致栈溢出或者计算时间过长。尾递归是一种优化手段,它能让函数在递归调用时不增加新的栈帧,理论上可以处理更深层次的递归。但在JavaScript中,默认情况下并没有开启尾递归优化。尽管如此,我们仍然可以尝试编写一个尾递归风格的阶乘函数,只是需要注意JavaScript引擎可能并不会对其进行优化。下面是一个尝试体现尾递归思想的阶乘实现,但请注意,实际运行此代码去计算10000的阶乘可能会导致浏览器或环境崩溃,因为结果数字极大超出了JavaScript Number类型的表示范围。

function factorial(n, accumulator = 1) { 
    if (n <= 1) return accumulator; // 尾递归风格,虽然JavaScript引擎不一定优化,但保持这种写法习惯 
    return factorial(n - 1, n * accumulator); 
} // 注意:计算10000的阶乘会导致数值过大,超出JavaScript Number类型的最大安全整数范围 // 因此这里不直接调用factorial(10000),实际应用中应考虑使用BigInt或其他方法处理大数运算

function factorialBigInt(n) { 
    let result = BigInt(1); 
    for (let i = 2; i <= n; i++) { 
        result *= BigInt(i); 
    } 
    return result; 
} 
console.log(factorialBigInt(10000).toString()); // 使用BigInt计算10000的阶乘

这段代码使用了循环而非递归,这是因为BigInt可以安全处理任意大小的整数,避免了递归调用的栈溢出风险,同时也更直接地实现了大数阶乘的计算。

33. 写一个二叉树的层序遍历

层序遍历二叉树,也称为广度优先遍历,可以使用队列来实现。

function levelOrder(root) { 
    if (!root) return []; // 如果根节点为空,直接返回空数组 
    const result = []; // 存储每一层节点值的数组 
    const queue = [root]; // 初始化队列,放入根节点 
    while (queue.length > 0) { 
        const levelSize = queue.length; // 当前层的节点数量 
        const currentLevel = []; // 存储当前层节点值的数组 
        for (let i = 0; i < levelSize; i++) { 
            const node = queue.shift(); // 取出队列首部节点进行处理 
            currentLevel.push(node.val); // 将节点值加入当前层结果 // 将左右子节点(如果存在)加入队列,以便后续处理 
            if (node.left) queue.push(node.left); 
            if (node.right) queue.push(node.right); 
        } 
        result.push(currentLevel); // 将当前层的结果加入最终结果数组 
    } 
    return result; 
}

总结

腾讯面试之难,难于上青天。打针哥可上可上九天揽月!

  • 扎实的基础
  • 底层知识的灵活应用
  • 抗压,面试中灵活应对