上个星期心血来潮,暑假带领一个小团队参加了字节跳动第六届前端青训营的进阶班并且在最后的大项目决赛当中取得了优秀奖,让我的信心有着前所未有的提升——因此趁着刚开学无事,自己向身边的前辈请教了简历的编写和排版之后也准备了一份看得过去的但是仍有很多漏洞的前端开发简历,希望能够找一下实习丰富自己的实际工作与项目经历。
上个星期准备完了之后,直接在Boss和字节的内推投了几份简历。字节的内推投的是广告前端(全栈)开发实习生-Ads Infra岗位,因为我认为我有着一定的Node.js实际开发经历。我以为至少一周多之后才会给我答复吧?我趁着等待的时间先好好准备一下面试——结果当天下午直接告诉我简历初筛过了(当时大脑直接一片空白),进了一面。然而我之前根本就一丁点的对于面试的准备都没有做过(真的是零经验零经验),然后约了四天后进行一面。可想而知,四天的准备怎么能过得了字节的一面?当然是直接被筛下来了,而且事后听录音觉得自己讲的很多地方都很粗浅、知识点掌握不到位等等。
闲话先这样吧,讲一下题目:
你有听过RESTful
的设计模式吗?以团队为例,会有增删改查四个接口,对于这四个接口的api
请求路径是怎么进行设计的呢?
RESTful(Representational State Transfer)是一种用于构建网络应用的架构风格。在RESTful设计中,每个资源通常都有一个与之对应的URI(统一资源标识符),并通过HTTP方法(如GET、POST、PUT、DELETE等)来进行操作。
以一个团队管理系统为例,你可能会有以下几个主要的API接口:
增(Create) - POST
/api/teams
删(Delete) - DELETE
/api/teams/{teamId}
改(Update) - PUT 或 PATCH
/api/teams/{teamId}
查(Read) - GET
/api/teams
或 /api/teams/{teamId}
问了我暑期大项目的问题,我的大项目里面包括一个解析Swagger
文档并进行递归解析$ref
的功能,问我这个功能的输入输出是怎么实现的。
经典八股:请说一下从浏览器输入url
到页面展示的整个流程。把输入之后对url
进行处理的部分答完了之后,又接着问我页面渲染的详细流程。
经典八股:请讲一下跨域是什么?为什么会出现跨域这种东西?当跨域请求被拦截了之后会在浏览调试界面出现报错,那么这个请求有真正的被发送到服务器进行执行吗?
什么是跨域?
跨域(Cross-Origin)是一个Web安全机制,用于限制Web页面中的脚本对不同源(origin)的资源的访问。在这里,“源”是由协议(如http或https)、域名和端口三者组成的。如果这三者中有任何一个不同,就被认为是不同的源。
为什么会出现跨域?
跨域机制主要是为了保护用户的安全。如果没有跨域限制,恶意网站可以轻易地通过脚本访问其他网站的数据,这可能会导致信息泄露或其他安全问题。
跨域请求是否真正发送到服务器?
当你尝试进行跨域请求时,浏览器会先发送一个预检请求(pre-flight request)到目标服务器,以检查服务器是否允许该跨域请求。这通常是一个OPTIONS
方法的HTTP请求。
因此,当跨域请求被拦截时,实际的请求通常没有被发送到服务器,除非服务器明确地允许了这种跨域请求。
经典八股:有了解过WebSocket
吗?有用过它写过一些东西吗?
WebSocket
是一种网络通信协议,提供了全双工(full-duplex)的通信渠道。与 HTTP 不同,WebSocket 一旦建立连接,就会保持连接状态,允许服务器和客户端之间进行双向数据传输。
WebSocket 的特点:
WebSocket 协议非常灵活,适用于多种实时应用场景。然而,由于它是一个持久连接,可能会消耗更多的服务器资源。
经典八股:讲一下CSS选择器的优先级。
CSS(层叠样式表)选择器的优先级是一个重要的概念,它决定了当多个样式规则应用于同一个元素时,哪一个规则会生效。优先级是通过一种称为**“特异性”(Specificity)**的机制来计算的。
特异性的计算
特异性是一个由四个组成部分的值:[inline, ID, class, element]
style
属性定义的),则这一部分的值为1。#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选择器的优先级最高。
经典八股: 列举一下你所知道的令一个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
操作符创建一个对象实例时,以下几个步骤会依次发生:
实例化过程
__proto__
属性会被设置为构造函数的 prototype
对象。this
: 在构造函数内部,this
关键字会被绑定到这个新创建的对象。new
表达式的结果被返回。实例与构造函数的联系
new
创建的实例对象的 __proto__
属性会指向构造函数的 prototype
对象。这就是实例与构造函数之间的主要联系。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)的区别
require()
来导入模块,使用 module.exports
或 exports
来导出。import
和 export
关键字。import
和 export
必须位于模块作用域。模块变量的改动
在 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 等)中非常重要,因为它有助于减小最终打包文件的大小,从而提高应用的加载速度和性能。
如何工作?
适用场景
tree-shaking
通常更适用于 ESM(ECMAScript Modules)格式的代码,因为 ESM 的静态结构使得构建工具更容易分析哪些代码是多余的。注意事项
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 的静态解析特性,循环依赖会被正确地解析,而不会导致不完全初始化的模块。
总结
经典八股: 为什么JS是单线程的?如果要进行异步操作,又是怎么去实现的?
经典八股: 浏览器事件循环知道吗?请解释一下。
如果我定义了一个setTimeout
,定时为1秒,那么里面的内容一定会在一秒之后执行吗?
如果我在执行微任务的时候又产生了一个微任务,那么这个新产生的微任务会在当前的事件循环直接执行,还是会放到下一个事件循环进行清理?
当你在执行一个微任务(例如,一个 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" 都会在同一个事件循环中打印出来。
然后给我出了一道async/await
、Promise
、setTimeout
和同步代码全包含的各种嵌套的一道题目给我做,让我在右侧的记事本上写一下最终输出出来的顺序。
我看你简历上说自己的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
这个全局对象,有什么用法吗?
单页面应用(SPA)的实现
单页面应用(SPA)是一种Web应用或网站的设计模式,其中所有必要的代码(HTML、JavaScript 和 CSS)都在一个页面中加载。随后的页面交互通过动态地重写当前页面的内容来实现,而不是默认的从服务器加载新页面。
常用技术:
URL与内容的关系
在传统的多页面应用(MPA)中,不同的URL通常对应服务器上不同的HTML页面。但在SPA中,所有的视图逻辑都在一个HTML页面中处理,通常是index.html
。
实现方式:
#
)部分来表示当前的应用状态。由于哈希变化不会触发页面重新加载,因此可以用JavaScript来捕获哈希变化并据此渲染不同的内容。
http://example.com/#/user/1
pushState
和replaceState
方法,可以在不重新加载页面的情况下修改浏览器的历史记录和URL。
http://example.com/user/1
index.html
文件。这样,无论用户输入什么URL,都会加载相同的HTML文件,然后由前端路由接管后续的视图渲染。部署到服务器
当你部署一个使用HTML5 History API的SPA时,你需要确保服务器对于所有的请求都返回同一个index.html
文件。这通常通过服务器的重写规则来实现。
.htaccess
文件。nginx.conf
文件。这样,无论用户输入什么URL,都会得到相同的index.html
文件,然后前端路由会根据URL渲染不同的内容。
接下来是算法题,给了我一个二叉树,求所有路径中总和最大的那一条路径(这真的是一面吗?之前对于二叉树这一块实在是有点没了解透,只会前中后序遍历,路经总和根本就没有刷过,连印象都没有)。坦白说自己不会,又给了我一道编程题,实现一个函数,可以往这个函数里面传入三个参数:1. 要执行的函数;2. 延迟执行的时间(以毫秒为单位);3. 需要重复执行的次数,这个函数的功能就是调用这个函数之后创建传入三个参数,返回一个新函数,之后往这个新函数里面传在第一次传进去的函数的参数,从而调用第一次传进去的这个函数,重复执行i次,每次执行间隔n毫秒。这个是一个比较经典的闭包问题,用函数柯里化一下基本直接解决了。
最后结束了,反问环节——其实已经觉得自己寄了,但还是厚颜无耻的说了一句这是我的第一次面试,有答的不好的地方请多多见谅——估计就是这一句直接把我pass了吧。
暂时就是这样。准备的实在是太不充分了,算是给自己的面试经历填了浓墨重彩的一笔吧,也希望能给希望进行前端面试的同学们一些比较好的参考吧!这次经历也提醒我基础还是要好好巩固的。日积月累,技术功底扎实才是硬道理呀!
#前端面试#