字节跳动前端+Node.js全栈一面面试记录
一、前言
上个星期心血来潮,暑假带领一个小团队参加了字节跳动第六届前端青训营的进阶班并且在最后的大项目决赛当中取得了优秀奖,让我的信心有着前所未有的提升——因此趁着刚开学无事,自己向身边的前辈请教了简历的编写和排版之后也准备了一份看得过去的但是仍有很多漏洞的前端开发简历,希望能够找一下实习丰富自己的实际工作与项目经历。
上个星期准备完了之后,直接在Boss和字节的内推投了几份简历。字节的内推投的是:广告前端(全栈)开发实习生-Ads Infra岗位,因为我认为我有着一定的Node.js实际开发经历。我以为至少一周多之后才会给我答复吧?我趁着等待的时间先好好准备一下面试——结果当天下午直接告诉我简历初筛过了(当时大脑直接一片空白),进了一面。然而我之前根本就一丁点的对于面试的准备都没有做过(真的是零经验零经验),然后约了四天后进行一面。可想而知,四天的准备怎么能过得了字节的一面?当然是直接被筛下来了,而且事后听录音觉得自己讲的很多地方都很粗浅、知识点掌握不到位等等。
闲话先这样吧,讲一下题目:
二、题目列表
-
你有听过
RESTful
的设计模式吗?以团队为例,会有增删改查四个接口,对于这四个接口的api
请求路径是怎么进行设计的呢?RESTful(Representational State Transfer)是一种用于构建网络应用的架构风格。在RESTful设计中,每个资源通常都有一个与之对应的URI(统一资源标识符),并通过HTTP方法(如GET、POST、PUT、DELETE等)来进行操作。
以一个团队管理系统为例,你可能会有以下几个主要的API接口:
增(Create) - POST
- 路径:
/api/teams
- 操作: 创建一个新的团队
- HTTP 方法: POST
删(Delete) - DELETE
- 路径:
/api/teams/{teamId}
- 操作: 删除一个指定ID的团队
- HTTP 方法: DELETE
改(Update) - PUT 或 PATCH
- 路径:
/api/teams/{teamId}
- 操作: 更新一个指定ID的团队的信息
- HTTP 方法: PUT 或 PATCH(PUT通常用于更新所有信息,PATCH用于部分更新)
查(Read) - GET
- 路径:
/api/teams
或/api/teams/{teamId}
- 操作: 获取所有团队或获取一个指定ID的团队的信息
- HTTP 方法: GET
- 路径:
-
问了我暑期大项目的问题,我的大项目里面包括一个解析
Swagger
文档并进行递归解析$ref
的功能,问我这个功能的输入输出是怎么实现的。 -
经典八股:请说一下从浏览器输入
url
到页面展示的整个流程。把输入之后对url
进行处理的部分答完了之后,又接着问我页面渲染的详细流程。 -
经典八股:请讲一下跨域是什么?为什么会出现跨域这种东西?当跨域请求被拦截了之后会在浏览调试界面出现报错,那么这个请求有真正的被发送到服务器进行执行吗?
什么是跨域?
跨域(Cross-Origin)是一个Web安全机制,用于限制Web页面中的脚本对不同源(origin)的资源的访问。在这里,“源”是由协议(如http或https)、域名和端口三者组成的。如果这三者中有任何一个不同,就被认为是不同的源。
为什么会出现跨域?
跨域机制主要是为了保护用户的安全。如果没有跨域限制,恶意网站可以轻易地通过脚本访问其他网站的数据,这可能会导致信息泄露或其他安全问题。
跨域请求是否真正发送到服务器?
这里很容易搞混,既然跨域了,那发送的请求应该会被浏览器给砍掉,不会到达服务器端,实则不然。跨域是可以正常发起请求的,服务器端能够收到请求并且正确返回结果,只是被浏览器拦截了。
要永远记住一个原则:同源策略只存在浏览器端,服务器是没有跨域问题的,甚至用
postman
等工具也不会出现跨域问题。 -
经典八股:有了解过
WebSocket
吗?有用过它写过一些东西吗?WebSocket
是一种网络通信协议,提供了全双工(full-duplex)的通信渠道。与 HTTP 不同,WebSocket 一旦建立连接,就会保持连接状态,允许服务器和客户端之间进行双向数据传输。WebSocket 的特点:
- 全双工通信: 服务器和客户端都可以在任何时候发送数据。
- 低延迟: 由于连接是持久的,不需要像 HTTP 那样每次请求都进行握手。
- 实时性: 适用于需要实时数据传输的应用,如聊天应用、在线游戏等。
WebSocket 协议非常灵活,适用于多种实时应用场景。然而,由于它是一个持久连接,可能会消耗更多的服务器资源。
-
经典八股:讲一下CSS选择器的优先级。
CSS(层叠样式表)选择器的优先级是一个重要的概念,它决定了当多个样式规则应用于同一个元素时,哪一个规则会生效。优先级是通过一种称为**特异性(Specificity)**的机制来计算的。
特异性的计算
特异性是一个由四个组成部分的值:
[inline, ID, class, element]
- 内联样式: 如果样式是内联的(即在HTML元素中使用
style
属性定义的),则这一部分的值为1。 - ID 选择器: 计算页面中所有ID选择器(如
#myId
)的数量。 - 类、属性和伪类选择器: 计算页面中所有类选择器(如
.myClass
)、属性选择器(如[type="text"]
)和伪类(如:hover
)的数量。当一个元素同时使用类、属性和伪类选择器时,它们的特异性是相同的,并且会累加。最终哪个规则生效取决于规则的出现顺序。 - 元素和伪元素选择器: 计算页面中所有元素选择器(如
div
,p
)和伪元素(如::before
)的数量。
优先级规则
- 越具体的选择器优先级越高: 例如,
#myId
(ID选择器)的优先级会高于.myClass
(类选择器)。 - 相同特异性下,后出现的规则优先: 如果两个选择器具有相同的特异性,那么后出现的规则会覆盖先出现的规则。
!important
优先级最高: 在样式声明后添加!important
会使该声明具有最高优先级,但如果多个!important
规则冲突,还是会回到特异性和源顺序来解决。
/* 特异性: [0, 1, 0, 0] */ #myId { color: blue; } /* 特异性: [0, 0, 1, 0] */ .myClass { color: red; } /* 特异性: [0, 0, 0, 1] */ p { color: green; }
在这个例子中,如果一个元素同时具有
id="myId"
和class="myClass"
,那么它的颜色将是蓝色,因为ID选择器的优先级最高。 - 内联样式: 如果样式是内联的(即在HTML元素中使用
-
经典八股: 列举一下你所知道的令一个
div
水平垂直居中的写法。使用 Flexbox
.parent { display: flex; justify-content: center; align-items: center; }
使用 Grid Layout
.parent { display: grid; place-items: center; }
使用绝对定位和
transform
.parent { position: relative; } .child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
使用绝对定位和
margin:auto
.parent { position: relative; } .child { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; }
使用
text-align
和line-height
(仅适用于单行文本).parent { text-align: center; line-height: [parent's height]; }
使用
vertical-align
(仅适用于inline
或inline-block
元素).parent { text-align: center; } .child { display: inline-block; vertical-align: middle; }
-
经典八股: 在使用new操作符创建一个对象的实例的时候,发生了什么?实例化之后的这个实例和原来的类的构造函数之间有什么联系?比如实例化出来一个Object对象,那么这个Object的原型指向什么?
使用
new
操作符创建一个对象实例时,以下几个步骤会依次发生:实例化过程
- 创建一个空对象: 首先,JavaScript 会创建一个新的空对象。
- 设置原型链: 这个新对象的
__proto__
属性会被设置为构造函数的prototype
对象。 - 绑定
this
: 在构造函数内部,this
关键字会被绑定到这个新创建的对象。 - 执行构造函数: 构造函数内的代码会被执行,通常用于初始化新对象的属性。
- 返回新对象: 除非构造函数返回一个非原始值(即一个对象或者一个函数),否则新创建的对象会作为
new
表达式的结果被返回。
实例与构造函数的联系
- 原型链: 通过
new
创建的实例对象的__proto__
属性会指向构造函数的prototype
对象。这就是实例与构造函数之间的主要联系。 - 访问属性和方法: 当你访问实例对象的属性或方法时,如果实例对象自身没有这个属性或方法,JavaScript 会沿着原型链查找,也就是去构造函数的
prototype
对象上查找。
Object 对象的原型
以
Object
为例,当你通过new Object()
创建一个新对象时,这个新对象的__proto__
属性会指向Object.prototype
。const newObj = new Object(); console.log(newObj.__proto__ === Object.prototype); // 输出 true
这意味着
newObj
继承了Object.prototype
上的所有属性和方法,例如hasOwnProperty
、toString
等。 -
经典八股:
let
、const
、var
这几个声明变量的关键词有什么区别? -
变量提升、函数提升有了解吗?然后给了我一个具体的例子,涉及到对一个
function
变量重新用var
赋值,问我最后输出啥。变量提升和函数提升
在 JavaScript 中,变量和函数声明会在代码执行前被“提升”到它们所在作用域的顶部。
- 变量提升: 使用
var
声明的变量会被提升,但只是声明会被提升,初始化(赋值)不会。这意味着变量会被声明为undefined
。 - 函数提升: 函数声明(不是函数表达式)会被整体提升,包括函数名和函数体。
具体例子
考虑以下代码:
console.log(foo); // 输出:[Function: foo] foo(); // 输出:"Hello from foo" var foo = "bar"; console.log(foo); // 输出:"bar" function foo() { console.log("Hello from foo"); }
在这个例子中,函数
foo
和变量foo
都会被提升,但函数提升的优先级更高。所以,第一个console.log(foo);
输出的是函数foo
,而不是undefined
。当执行到
var foo = "bar";
时,变量foo
会被重新赋值为"bar"
,覆盖了原来的函数。最后一个
console.log(foo);
输出的是字符串"bar"
,因为此时的foo
已经被重新赋值。 - 变量提升: 使用
-
经典八股: CJS和ESM有了解吗?说说他俩的区别。我在导入一个自己定义的模块并且使用的时候,将这个模块里面的一些东西改了,这个改动可以生效吗?比如我自己写了一个A模块,我在B模块中引入A模块,然后对A模块中的某个变量进行重新赋值,那么这个变量可以重新改掉吗?
CJS(CommonJS)和 ESM(ECMAScript Modules)的区别
- 语法:
- CJS: 使用
require()
来导入模块,使用module.exports
或exports
来导出。 - ESM: 使用
import
和export
关键字。
- CJS: 使用
- 运行时与编译时:
- CJS: 在运行时解析依赖。
- ESM: 在编译时解析依赖。
- 异步与同步:
- CJS: 同步加载模块。
- ESM: 支持异步加载。
- 作用域:
- CJS: 每个模块有自己的作用域。
- ESM:
import
和export
必须位于模块作用域。
- 互操作性:
- CJS: 在 Node.js 中广泛使用,但不是原生浏览器支持。
- ESM: 既可以在现代浏览器中使用,也逐渐在 Node.js 中得到支持。
模块变量的改动
在 CommonJS 中,当你导入一个模块,你实际上得到的是该模块导出对象的一个拷贝。这意味着,如果你在一个模块中改变了一个导入变量的值,这个改变不会反映到被导入模块中。
在 ESM 中,
import
得到的是一个只读引用。对于原始数据类型(如数字、字符串等),你不能改变它们的值。但如果你导入的是一个对象或数组,你可以改变其属性或元素。示例:
假设有一个模块 A:
// A模块(CommonJS) exports.someVar = 42;
在 B 模块中:
// B模块(CommonJS) const A = require('./A'); console.log(A.someVar); // 输出 42 A.someVar = 100; console.log(A.someVar); // 输出 100
在这个例子中,
someVar
的值在 B 模块中被改变了,但这个改变不会影响到 A 模块中someVar
的值。 - 语法:
-
treeShaking
有了解吗?(直接两眼一黑想死了,连这个名字本身都只听过一两遍,更不用说这是什么了)tree-shaking
是一个在前端工程中常用的术语,主要用于描述去除 JavaScript 文件中未使用的代码的过程。这个概念在现代前端构建工具(如 Webpack、Rollup 等)中非常重要,因为它有助于减小最终打包文件的大小,从而提高应用的加载速度和性能。如何工作?
- 静态分析: 构建工具会静态分析代码,找出哪些模块和函数没有被使用或引用。
- 去除代码: 在最终的打包文件中,未被使用的代码会被去除。
适用场景
- ESM:
tree-shaking
通常更适用于 ESM(ECMAScript Modules)格式的代码,因为 ESM 的静态结构使得构建工具更容易分析哪些代码是多余的。
注意事项
- 副作用: 如果代码有副作用(side-effects),那么
tree-shaking
可能会导致问题。例如,如果一个模块在被导入时执行了某些全局操作,即使没有直接使用这个模块,它也不能被安全地移除。 - 配置: 在某些情况下,你可能需要在构建工具的配置文件中明确指定哪些代码是“纯净的”(没有副作用),以便进行
tree-shaking
。
示例
假设你有如下的代码:
// math.js export const add = (a, b) => a + b; export const multiply = (a, b) => a * b; // app.js import { add } from './math'; console.log(add(1, 2));
在这个例子中,
multiply
函数没有在app.js
中被使用,因此通过tree-shaking
,这个函数会被从最终的打包文件中移除。 -
你知道CJS和ESM分别是怎么解决循环依赖的吗?(不知道)
循环依赖(或称为循环引用)是一个在模块系统中常见的问题,不同的模块系统有不同的方式来处理这个问题。
CommonJS(CJS)
在 CommonJS 中,当发生循环依赖时,模块系统会返回到目前为止已经解析(并执行)的部分。这意味着,在循环依赖的情况下,你可能得到一个不完全初始化的模块。
示例:
假设有两个模块 A 和 B,它们相互依赖:
// A.js const B = require('./B'); exports.name = 'Module A'; // B.js const A = require('./A'); exports.name = 'Module B';
在这种情况下,当你尝试
require('./A')
或require('./B')
,模块系统会尝试解析两者,但由于循环依赖,它会返回一个不完全初始化的模块。ECMAScript Modules(ESM)
ESM 采用了一种不同的方法来处理循环依赖。由于 ESM 在编译时解析依赖,它能更好地处理这种情况。在 ESM 中,导入的值是只读引用,而不是值的拷贝。这意味着,即使存在循环依赖,你也会得到预期的结果。
示例:
// A.js import { name as BName } from './B.js'; export const name = 'Module A'; // B.js import { name as AName } from './A.js'; export const name = 'Module B';
在这个例子中,由于 ESM 的静态解析特性,循环依赖会被正确地解析,而不会导致不完全初始化的模块。
总结
- CommonJS 在运行时解析模块,可能导致不完全初始化的模块。
- ESM 在编译时解析模块,能更好地处理循环依赖。
-
经典八股: 为什么JS是单线程的?如果要进行异步操作,又是怎么去实现的?
-
经典八股: 浏览器事件循环知道吗?请解释一下。如果我定义了一个
setTimeout
,定时为1秒,那么里面的内容一定会在一秒之后执行吗?如果我在执行微任务的时候又产生了一个微任务,那么这个新产生的微任务会在当前的事件循环直接执行,还是会放到下一个事件循环进行清理?然后给我出了一道async/await
、Promise
、setTimeout
和同步代码全包含的各种嵌套的一道题目给我做,让我在右侧的记事本上写一下最终输出出来的顺序。当你在执行一个微任务(例如,一个
Promise.then
回调)时,如果该微任务产生了一个新的微任务,那么这个新产生的微任务会被立即添加到微任务队列中,并会在当前的事件循环中执行。换句话说,浏览器会在当前事件循环中清空微任务队列,直到队列为空。这意味着,如果一个微任务产生了另一个微任务,新的微任务也会在当前事件循环中执行,而不会等到下一个事件循环。
示例:
Promise.resolve().then(() => { console.log('First micro-task'); return Promise.resolve(); }).then(() => { console.log('Second micro-task'); });
在这个例子中,"First micro-task" 和 "Second micro-task" 都会在同一个事件循环中打印出来。
-
我看你简历上说自己的
Vue
挺熟练的,React
有了解过吗? -
我看你自己组件二次封装的也比较多,你知道受控和非受控这两个概念吗?(???我完全这两个词都没听过)
**受控(Controlled)和非受控(Uncontrolled)**是两种常见的组件设计模式,特别是在 React 等前端框架中。这两种模式主要用于处理组件的状态和数据流。
受控组件(Controlled Components)
在受控组件中,组件的状态由父组件(或外部)完全控制。这通常是通过 props 传递状态和改变状态的回调函数来实现的。
优点:
- 更高的灵活性:因为状态是外部控制的,所以更容易实现复杂的功能和逻辑。
- 更容易测试和调试:状态管理在一个地方,更容易进行测试和调试。
示例:
// React 示例 function ControlledInput({ value, onChange }) { return <input value={value} onChange={onChange} />; }
非受控组件(Uncontrolled Components)
在非受控组件中,组件自己管理自己的状态,通常通过内部的 state 来实现。这种组件通常更容易使用和理解,但可能缺乏灵活性。
优点:
- 简单易用:因为组件自己管理状态,所以使用起来更简单。
- 减少渲染:由于状态仅在组件内部管理,可能会减少不必要的渲染。
示例:
// React 示例 class UncontrolledInput extends React.Component { state = { value: '' }; handleChange = (e) => { this.setState({ value: e.target.value }); }; render() { return <input value={this.state.value} onChange={this.handleChange} />; } }
总结
- 受控组件:更灵活,但可能更复杂。
- 非受控组件:更简单,但可能缺乏灵活性。
选择哪一种取决于你的具体需求和应用的复杂性。
-
你一般在开发过程中,组件间通信是怎么进行处理的?比如父子组件之间、兄弟组件之间、跨层级比较多的组件之间?
-
对于
Vue
的数据响应式这一块,详细说明一下。 -
对于单页面应用(
SPA
),有了解过是怎么实现的吗?你说你自己部署过应用到服务器,输入不同的url
会返回页面不同的内容,这个对于单页面应用是怎么实现的?history API
在实现SPA
的过程中有什么体现? -
接下来是算法题,给了我一个二叉树,求所有路径中总和最大的那一条路径(这真的是一面吗?之前对于二叉树这一块实在是有点没了解透,只会前中后序遍历,路经总和根本就没有刷过,连印象都没有)。坦白说自己不会,又给了我一道编程题,实现一个函数,可以往这个函数里面传入三个参数:1. 要执行的函数;2. 延迟执行的时间(以毫秒为单位);3. 需要重复执行的次数,这个函数的功能就是调用这个函数之后创建传入三个参数,返回一个新函数,之后往这个新函数里面传在第一次传进去的函数的参数,从而调用第一次传进去的这个函数,重复执行i次,每次执行间隔n毫秒。这个是一个比较经典的闭包问题,用函数柯里化一下基本直接解决了。
-
最后结束了,反问环节——其实已经觉得自己寄了,但还是厚颜无耻的说了一句这是我的第一次面试,有答的不好的地方请多多见谅——估计就是这一句直接把我pass了吧。
三、后记
暂时就是这样。准备的实在是太不充分了,算是给自己的面试经历填了浓墨重彩的一笔吧,也希望能给希望进行前端面试的同学们一些比较好的参考吧!这次经历也提醒我基础还是要好好巩固的。日积月累,技术功底扎实才是硬道理呀!
#前端面试#