本人大三最近投了字节的前端实习,面试是三轮技术面+一轮hr面,已成功拿到offer,分享以下面试过程中的问题和解答。
一面(50 min)
面试我的的是一个二十出头的小哥哥,第一次面试我还是比较紧张的,还好小哥哥待人和蔼可亲,针对不会的问题并没有刁难我,反而给我不少提示(给点个赞)
首先是自我介绍,问了下我学习前端的途径和方法啥的,算是暖了个开场吧,然后是具体的问题。
1. ES6新特性
我首先说的const
和let
,学长就直接让我细说以下let和const
- let、const声明的变量只在其所在的块代码内有效,隐式截取了作用域。
- 暂时性死区,变量声明提升但在不会被赋值为
undefined
,不能在声明之前使用。 - 不允许重复声明。
然后给了两个例子让说一下:
const a = 1
const a = 2
console.log(a)
这个会直接报错SyntaxError: Identifier 'a' has already been declared
,ES6中的let
和const
是不允许变量重复声明的。
const b = []
b.push(1)
console.log(b)
输出结果是[1]
,这个是关于基本类型和引用类型的区别,b
指向的只是这个数组的内存地址。
2. ES6之前的模块引入方式和区别
ES6之前模块引入主要是CommonJS和AMD两种。
然后学长让说明一下CommonJs和ES6模块引入的区别:
- 首先,CommonJS导出值是浅拷贝,一旦输出某个值,模块内部的变化就影响不到这个值。而ES6导出是采用实时绑定的方式,是将其内存地址导出,导入是动态地加载模块取值,并且变量总是绑定其所在的模块,不能重新赋值。
- ES6模块化导入是异步导入,CommonJS导入是同步导入。这跟ES6模块通常用于web端,而CommonJS用于服务器端有关。
- CommonJS导入支持动态导入
require(`${path}/xx.js`)
,ES6模块化导入不支持,目前已有草案。 - ES6模块化会编译成
require/exports
来执行的。
3. 一道数组遍历
实现[1,2,3] => [2,4,6]
,这个太简单了:
function fn(arr) {
return arr.map(item => item * 2)
}
4. 简单算法题
对输入的字符串,去除其中的字符'b'以及连续出现的'a'和'c',例如:
'aacbd' -> 'ad'
'aabcd' -> 'ad'
'aaabbccc' -> ''
这个连续的ac
需要注意以下,最开始想的是使用栈也是可以的比较麻烦,学长提醒了下正则,直接使用正则:
function fn(str) {
let res = str.replace(/b+/g, '');
while(res.match(/(ac)+/)) {
res = res.replace(/ac/, '')
}
return res;
}
5. CSS9宫格
问题:创建出CSS9宫格,3*3宫格,每个宫格的长宽未知,要求达到自适应并且精确分配。
这个精确分配,就注定使用width: 1/3
是达不到要求的,需要使用flex布局:
<!--HTML-->
<div class="container">
<div class="wrapper">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
<div class="wrapper">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
<div class="wrapper">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
</div>
<!--CSS-->
.container {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
}
.wrapper {
display: flex;
flex: 1;
flex-direction: row;
flex-wrap: nowrap;
}
.item {
flex: 1;
}
6. 简单说下React Virtual Dom (1)diff算法 (2)映射到真实DOM\
React虚拟DOM和diff算法,几乎问到React都会问这两个吧。。。
React将DOM抽象为虚拟DOM(VDOM), 然后通过新旧虚拟DOM 这两个对象的差异(Diff算法),最终只把变化的部分重新渲染,提高渲染效率。
VDOM
使用JS模拟DOM树的结构,DOM树的变化通过JS进行对比。
当我们需要创建或更新元素时,React首先会让这个VitrualDom对象进行创建和更改,然后再将VitrualDom对象渲染成真实DOM;
虚拟DOM的优势:
- 函数式编程。UI取决于数据。
- 打开了跨平台的大门。提供了统一的JS层对象,根据不同平台制定不同的渲染方式,带来了跨平台渲染的能力。
Diff算法
创建一个React元素树之后,在更新的时候将创建一个新的React元素树,React使用Diff算法对元素树进行比对,只更新发生了改变的部分,避免多余的性能消耗。
主要是两个要点:
- 两个不同类型的元素会产生不同的树,直接替换。
- 对于同一层级的一组子节点,它们可以通过唯一 key 进行区分。
- 逐层递归比较。
7. NodeJS中的任务队列
说下输出顺序:
process.nextTick(function() {
console.log('a')
})
process.nextTick(function() {
console.log('b')
})
setImmediate(function() {
console.log('c')
process.nextTick(function() {
console.log('d')
})
})
setImmediate(function() {
console.log('e')
})
console.log('f')
输出:f -> a -> b -> c -> d -> e
这个值得注意的是setImmediate只能占用一次,这块setImmediate当时就回答错了。
8. ES6字符串
这道题的清醒没有遇到过,也不会,现在也忘了这道题。。。
9. Promise实现网络超时判断
使用Promise实现网络请求超时判断,超过三秒视为超时。 借助的是Promise.race这个方法:
const uploadFile = (url, params) => {
return Promise.race([
uploadFilePromise(url, params),
uploadFileTimeout(3000)
])
}
function uploadFilePromise(url, params) {
return new Promise((resolve, reject) => {
axios.post(url, params, {
headers: {'Content-Type': 'multipart/form-data'}, // 以formData形式上传文件
withCredentials: true
}).then(res => {
if(res.status===200 && res.data.code===0) {
resolve(res.data.result)
}else {
reject(res.data)
}
})
})
}
function uploadFileTimeout(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject({timeoutMsg: '上传超时'})
}, time)
})
}
二面(50min)
一面过后让我稍等十分钟,二面就开始了。
1. 进程和线程
简要说下操作系统的进程和线程
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线。
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间。
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
2. 简要说下操作系统中的锁(死锁)
通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个进程进入临界区代码,从而保证临界区中操作数据的一致性。
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,两羊过独木桥。
死锁的解除与预防,资源的合理分配。。。
这一块答的不是很好。
3. 说一下链表的类型
单向链表、双向链表、循环链表
4. 栈和队列的区别?
栈是后进先出的数据结构,常用的比如调用栈。
队列是先进先出的数据结构,常用的就是NodeJS和浏览器中的任务队列了。
5. 算法题
遍历一个二叉树所有节点,返回它们的和
function numSum(root) {
if(!root) return 0;
return root.val + numSum(root.left) + numSum(root.right);
}
6. 算法题
给出一个数组,求出连续元素组成子串和的最大值。
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
双层循环的话时间复杂度是: O(n * n), 使用分治的话时间复杂度可以压缩到O(n * logn),问了下学长说直接用笨方法写一下,给出代码:
function numSum(arr) {
if(!arr.length) return null;
if(arr.length < 2) return arr[0];
let sumMax = arr[0];
let sumTemp;
for(let i = 0; i < arr.length; i++) {
sumTemp = arr[i];
if(sumTemp > sumMax) sumMax = sumTemp;
for(let j = i + 1; j < arr.length; j++) {
sumTemp += arr[j];
if(sumTemp > sumMax) sumMax = sumTemp;
}
}
return sumMax;
}
7. 介绍下你的项目,微信小程序的原理简要说一下?
略。。。
三面(1h)
三面的难度就比之前的高了不少
1. 说下ES6新特性
列举下ES6新特性并简要说明一波
- let、const
- 解构赋值
- 字符串、正则、数值、函数、数组、对象的扩展
- symbol
- Set、Map
- Promise
- Generator
- async
- class
- 修饰器
- module
2. Generator实现一个自执行
就是实现以下著名的co模块:
function run(gen) {
return new Promise(function(resolve, reject) {
if (typeof gen == 'function') gen = gen();
// 如果 gen 不是一个迭代器
if (!gen || typeof gen.next !== 'function') return resolve(gen)
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise(ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise ' +
'but the following object was passed: "' + String(ret.value) + '"'));
}
})
}
function isPromise(obj) {
return 'function' == typeof obj.then;
}
function toPromise(obj) {
if (isPromise(obj)) return obj;
if ('function' == typeof obj) return thunkToPromise(obj);
return obj;
}
function thunkToPromise(fn) {
return new Promise(function(resolve, reject) {
fn(function(err, res) {
if (err) return reject(err);
resolve(res);
});
});
}
module.exports = run;
3. 说下微信小程序的生命周期
这个我没温习到,没有回答完整哈~ 页面生命周期:
- onLoad // 页面创建时执行
- onShow // 页面出现在前台执行
- onReady // 页面首次渲染完毕时执行
- onHide // 页面从前台变为后台时执行
- onUnload // 页面销毁时执行
- onPullDownRefresh // 触发下拉刷新时执行
- onReachBottom // 页面触底时执行
- onPageScroll // 页面滚动时执行
- onResize // 页面尺寸变化时执行
- onTabItemTap // tab点击时执行
组件声明周期(在lifetimes字段内进行声明):
- created // 在组件实例刚刚被创建时执行
- attached // 在组件实例进入页面节点树时执行
- ready // 在组件在视图层布局完成后执行
- moved // 在组件实例被移动到节点树的另一个位置时执行
- detached // 在组件实例被从页面节点移除时执行
- error // 每当组件方法抛出错误时执行
4. 说下你会的React技术栈
Redux、React-Router、Antd。。。
5. React-Router实现格简单路由
import { BroserRouter, Route, Switch, Redirect } from 'react-router-dom'
<BroserRouter>
<Switch>
<Route path='/login' exact component={Login} />
<Route path='/' render={() => (
<Switch>
<Route paht='/home' component={Home} />
<Redirect to='/home' />
</Switch>
)} />
</Switch>
</BroserRouter>
6. 说下Redux原理
Redux就是一个数据状态管理,简要说下Redux数据流,store、action、reducer,搭配React的使用。
可以有空看下源码,内容不是很多。
7. 说下你的项目
略。。。
8. 说下Koa洋葱模型
Koa的洋葱模型:一个请求从外到里一层一层地经过中间件,响应时从里到外一层一层地经过中间件。。
观看Koa源码,你会发现:实现洋葱模型依赖于koa-compose,分析一波koa-compose的源码:
'use strict'
module.exports = compose
function compose (middleware) {
// 类型判断 middleware必需是一个函数数组
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!');
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!');
}
// 返回一个函数,接受context和next参数,koa在调用koa-compose时只传入context,所以此处next为undefined
return function (context, next) {
// last called middleware #
// 初始化index
let index = -1;
// 从第一个中间件开始执行
return dispatch(0);
function dispatch (i) {
// 在一个中间件出现两次next函数时,抛出异常
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
// 设置index,作用是判断在同一个中间件中是否调用多次next函数
index = i;
// 中间件函数
let fn = middleware[i]
// 跑完所有中间件时,fn=next,即fn=undefined,可以理解为终止条件
if (i === middleware.length) fn = next
// fn为空时,返回一个空值的promise对象
if (!fn) return Promise.resolve();
try {
// 返回一个定值的promise对象,值为下一个中间件的返回值
// 这里时最核心的逻辑,递归调用下一个中间件,并将返回值返回给上一个中间件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
9. 实现一个Koa中间件
中间件通常为以下形式:
async (ctx, next) => {...}
一般来说中间件有自己的配置,所以我们总结出来一种通用的中间件写法,我们通过传入配置的方式可以返回根据配置定制的中间件:
module.exports = function(options) {
// 配置处理
return async (ctx, next) => {
// 中间件逻辑...
}
}
使用方式:
app.use(middleware(options)) // middleware(options)返回一个中间件
上一个面试题提到的koa-compose,可以将多个中间件合并,也可以将有关联的中间件合成一个大的中间件:
app.use(compose([middleware1, middleware2, ...]))
OK~!现在我们自定义一个中间件! 问题背景:网站经常被某些ip攻击,影响业务正常运行,于是做了一个中间件进行ip的过滤,对于ip黑名单上的ip一律拒绝进一步处理请求。
/*
** iplimiter: ip过滤中间件,对于在ip黑名单上的ip直接返回,请求不再进行下去
** ip_blacklist: Array, example: ['192.123.12.11']
*/
module.exports = function(ip_blacklist) {
return async (ctx, next) => {
if(Array.isArray(ip_blacklist) && ip_blacklist.length) {
let ip = ctx.request.headers['x-real-ip'] || '' //获取客户端ip,由于使用nginx作为负载均衡,所以获取ip的方式可通过x-real-ip字段
if(ip && ip_blacklist.indexOf(ip) !== -1) {
await next()
} else {
return res.end('ip restricted')
}
} else {
await next()
}
}
}
10. 参与比赛的项目中,你自我感觉发挥作用比较重要的一个项目?
略。。。
四面(30min)
四面是hr面,聊了一下选择前端的原因和对于前端发展的看法,平时的学习途径和学习方法,大学校园生活、参加的一些比赛和活动,对于考研和就业的一些看法。随后也是顺利拿到了字节的实习offer。
反思
字节的面试官还是比较善解人意的,对于没有思路的问题会引导着你去思考而不是以难倒人为乐趣,算法题嘛整体也是比较简单的,多关注于实际实现和知识基础。
面试的节奏也是比较快的,三轮技术面基本都是连着面中间隔个15min。也可以看到字节对于基础知识是比较看重的,知识基础还是需要多看,还有一些框架的源码和原理看了的话也是加分很多的。
三面的效果并不是很理想,但最后还是成功拿到了实习offer,还是比较幸运的。也祝大家都能拿到理想的offer啦~
参考文献: