春招 通过若川大佬的前端群认识了一个本科优秀小伙子 白熊C,实力加运气爆棚的,拥有诸多offer,最终选择了实力和前景远阔的字节。
【🌼 字节一面:41min】
1.介绍公司项目
2.讲述一下React中hooks和class的区别
最主要区别在于:心智模型(mental model)
hooks更加函数式和声明式。
从过去的命令式转变成了声明式的写法,Hooks 提供各种声明式的副作用 API (useEffect, useCallback),使得“生命周期”变成了一个“底层概念”,对开发者是无感知的,因此开发者更能够将精力聚焦在在更高的抽象层次上。
hooks组件其实是降低了react开发的使用难度的,让新手可以在不使用class组件的情况下依然可以进行项目开发,可以不需要考虑this问题,可以不用使用高阶组件依然可以进行功能复用了。
3.说一下输出
alert(a);
a();
a = 3;
function a (){
alert(10)
}
alert(a);
a = 6;
a()
答案:
//第一个输出
function(){
alert(10)
}
//第二个输出
10
//第三个输出
3
//第四个输出
Uncaught TypeError: a is not a function
at <anonymous>:9:5
4.如何学习前端
5.如何同时获取两个api的结果,手写Promise.all()
首先,我们得知道Promise.all() 方法接收一个promise的iterable类型,接收的是Promise对象的数组作为参数。当数组中所有的Promise对象全部变成Resovled状态,则整个Promise.all()也是Resolved状态;当数组中只要有一个Promise对象处于Rejected状态,则真哥哥Promise.all()处于Rejected状态。
即:Promise.all 需要等到所有的 promise 的状态都变成 fulfilled 之后才 resolve, 但只要有一个 promise 失败即返回失败的结果。
Promise.all的特点在于:
- 接收一个Promise实例的数组或具有Iterator接口的对象
- 如果元素不是Promise对象,则使用Promise.resolve转成Promise对象
- 如果全部成功,状态则变成resolved,返回值将组成一个数组传给回调
- 只要有一个失败,状态就变成rejected,返回值将直接传递给回调
- Promise.all()的返回值也是一个新的Promise对象
Promise.all = function(arr){
return new Promise((resolve,reject) => {
if(!Array.isArray(arr)){
return reject(new TypeError('arguments must be an array'));
}
let length = arr.length;
let resolveNum = 0;
let resolveResult = [];
for(let i = 0; i < length; i++){
arr[i].then(data => {
resolveNum++;
resolveResult.push(data)
if(resolveNum == length){
return resolve(resolveResult)
}
}).catch(data => {
return reject(data)
})
}
})
}
6.说一下进程和线程
官方说法:
- 进程:进程是CPU资源分配的最小单位。
- 线程:线程是CPU资源调度的最小单位。
个人理解:
- 进程:在操作系统中,会根据每一个独立运行的程序分配资源,这些程序之间互不干扰,都只负责运行自己的代码。
- 线程:在操作系统进行频繁创建销毁进程时,需要给进程频繁分配和收回资源,而时间和资源也便在此过程中消耗。但是随着计算机硬件的升级,内存大的多核心CPU的出现,能够对计算机内存资源根据需求进行合理调度,线程也逐渐取代了进程。
操作系统的进程图:
操作系统的线程图:
7.浏览器的缓存机制
通常浏览器有两种缓存策略:强缓存(Expires,cache-control)和协商缓存(Last-modified ,Etag),并且缓存策略都是通过设置 HTTP Header 来实现的。
浏览器的缓存过程是:
- 浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;
- 下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求;
- 服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;
- 如果服务器收到的请求没有Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;
8.cookie与localStorage、sessionStorage的区别
| cookie | sessionStorage | localStorage | |
|---|---|---|---|
| 生命周期 | 可设置失效时间,没有设置的话,默认是关闭浏览器后失效 | 仅在当前网页会话下有效,关闭页面或浏览器后就会被清除。 | 除非被手动清除,否则将会永久保存。 |
| 存放数据 | 4KB左右 | 可以保存5MB的信息。 | 可以保存5MB的信息。 |
| HTTP请求 | 每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题 | 仅在客户端(即浏览器)中保存,不参与和服务器的通信 | 仅在客户端(即浏览器)中保存,不参与和服务器的通信 |
| 易用性 | 需要程序员自己封装,源生的Cookie接口不友好 | 源生接口可以接受,亦可再次封装来对Object和Array有更好的支持 | 源生接口可以接受,亦可再次封装来对Object和Array有更好的支持 |
9.如何实现单点登录
单点登录:单点登录全称Single Sign On(以下简称SSO),是指多系统应用群中登录一个系统,便可以在其他所有系统中得到授权而无在此登录。简单来说,单点登录就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录。
具体步骤是:
- (1)用户访问系统A的受保护资源时,发现用户在系统A上并未登录,此时会携带系统A的地址信息,将登录请求跳转到SSO认证中心;
- (2)SSO认证中心发现用户未登录,将指引用户到系统A的登录页面;
- (3)用户此时应该输入用户名、密码进行登录提交申请;
- (4)SSO认证中心对用户信息的用户名和密码进行校验,创建用户与SSO认证中心之间的全局会话,同时创建授权令牌Token;
- (5)SSO认证中心携带Token令牌跳转到最初的请求地址(系统A的地址);
- (6)当用于在此发起请求时,系统A拿到令牌Token去SSO认证中心校验令牌是否有效;
- (7)SSO认证中心拿到Token后进行校验,如有效则返回,并注册系统A的地址;
- (8)系统A使用该令牌Token创建与用户的会话,成为局部会话,返回受保护资源;
- (9)当用户访问系统B的受保护资源时,发现用户未登录,则携带系统B的地址信息,将登录强求跳转到SSO认证中心;
- (10)SSO认证中心发现用户已经登录,发现用于已经登录,则携带令牌Token跳转到系统B的地址;
- (11)当向系统B在此发起请求时,系统B拿到Token令牌,去SSO认证中心校验令牌是否有效;
- (12)SSO认证中心校验Token有效,则注册系统B的地址信息;
- (13)系统B使用该Token信息与用户的局部会话,且向浏览器返回受保护资源。
在用户登录成功之后,会与SSO认证中心及各个子系统建立会话,用户与SSO认证中心建立的会话成为全局会话,用户与各个子系统建立的会话叫做局部会话。局部会话建立后,用户访问子系统受保护资源时将不再通过SSO认证中心。
11.说一下你在项目中遇到的难点
12.判断是否为回文数字
12.1 使用高阶函数
思路: 先将数字转成字符串A,再经过变成数组,数组反转,数组变成字符串B三步操作之后,比较字符串A和B,得出结论。
/**
* @param {number} x
* @return {boolean}
*/
function isPalindrome(x) {
if ( x < 0 ) return false
let str = '' + x
return Array.from(str).reverse().join('') === str
};
12.2 从后往前循环字符串数组
思路:将数字转换成字符串A,从后往前循环字符串A,将循环出来的字符拼接成新的字符串B,比较字符串A和B,得出结论。
/**
* @param {number} x
* @return {boolean}
*/
function isPalindrome(x) {
let str = x + ''
let newStr = ''
for(let len = str.length, i = len - 1; i >= 0 ; i--) {
newStr += str[i]
}}
return newStr === str
};
12.3 以中间数为节点,判断左右两边首尾是否相等
思路:以中间数为节点,逐一判断左右两边首尾是否相等。
function isPalindrome(x) {
if ( x < 0 || (x !== 0 && x % 10 === 0)) {
return false
} else if ( 0 <= x && x < 10) {
return true
}
x = '' + x
for(let i = 0 ; i < x.length/2; i++) {
if (x[i] !== x[x.length - i - 1]) {
return false
}
}
return true
};
【🌴 字节二面 60 min】
1.如何学习前端,看过哪些书
2. 进程和线程,进程如何通信
同一面
3.实现点击table里单元格拿到里面的内容
//1.获取单元格内容
let table = document.getElementById("myGrid");
let content = table.rows[i].cells[i].innerHTML;
//2.获取单元格内输入框中的内容
table.rows[1].cells[2].childNodes[0].value;
4.实现发布订阅,on,emit,off,清除其中一个方法
思路:
- 创建一个对象
- 在该对象上创建一个缓存列表(调度中心)
- on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心)
- emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
- off 方法可以根据 event 值取消订阅(取消订阅)
- once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)
代码:
实现了 on 和 emit 方法
// 创建对象
let eventEmitter = {};
// 缓存列表,存放 event 及 fn
eventEmitter.list = {};
// 订阅
eventEmitter.on = function (event, fn) {
let _this = this;
// 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表
// 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里
(_this.list[event] || (_this.list[event] = [])).push(fn);
return _this;
};
// 发布
eventEmitter.emit = function () {
let _this = this;
// 第一个参数是对应的 event 值,直接用数组的 shift 方法取出
let event = [].shift.call(arguments),
fns = [..._this.list[event]];
// 如果缓存列表里没有 fn 就返回 false
if (!fns || fns.length === 0) {
return false;
}
// 遍历 event 值对应的缓存列表,依次执行 fn
fns.forEach(fn => {
fn.apply(_this, arguments);
});
return _this;
};
function user1 (content) {
console.log('用户1订阅了:', content);
};
function user2 (content) {
console.log('用户2订阅了:', content);
};
// 订阅
eventEmitter.on('article', user1);
eventEmitter.on('article', user2);
// 发布
eventEmitter.emit('article', 'Javascript 发布-订阅模式');
/*
用户1订阅了: Javascript 发布-订阅模式
用户2订阅了: Javascript 发布-订阅模式
*/
5.class 的protected、 pravila和public
- public 修饰的属性可以在任意位置进行访问修改,默认值,作用范围是全局
- private 修饰的属性只能在类的内部进行访问修改,私有属性,作用范围是类的内部。通过在类中添加set get存取器可以在类的外部访问私有属性
- protected 修饰的属性只能在当前类和当前类的子类中进行访问修改,受保护的属性,作用范围是当前类和子类的范围内
6.跨域
- JSONP:在html中使用
- CORS(跨域资源共享):在服务端设置 Access-Control-Allow-Origin 就可以开启 CORS
- document.domain:该⽅式只能⽤于⼆级域名相同的情况下
- postMessage:这种⽅式通常⽤于获取嵌⼊⻚⾯中的第三⽅⻚⾯数据。⼀个⻚⾯发送消息,另⼀个⻚⾯判断来源并接收消息
- Nginx反向代理实现跨域
- window.name + iframe
7.cors原理
CORS 跨域的原理实际上是浏览器与服务器通过一些 HTTP 协议头来做一些约定和限制。
与跨域相关的协议头
| 请求头 | 说明 |
|---|---|
| Origin | 表明预检请求或实际请求的源站 URI,不管是否跨域ORIGIN 字段总是被发送 |
| Access-Control-Request-Method | 将实际请求所使用的 HTTP 方法告诉服务器 |
| Access-Control-Request-Headers | 将实际请求所携带的首部字段告诉服务器 |
| 响应头 | 说明 |
|---|---|
| Access-Control-Allow-Origin | 指定允许访问该资源的外域 URI,对于携带身份凭证的请求不可使用通配符* |
| Access-Control-Expose-Headers | 指定 XMLHttpRequest的getResponseHeader 可以访问的响应头 |
| Access-Control-Max-Age | 指定 preflight 请求的结果能够被缓存多久 |
| Access-Control-Allow-Credentials | 是否允许浏览器读取 response 的内容; 当用在 preflight 预检请求的响应中时,指定实际的请求是否可使用 credentials |
| Access-Control-Allow-Methods | 指明实际请求所允许使用的 HTTP 方法 |
| Access-Control-Allow-Headers | 指明实际请求中允许携带的首部字段 |
8.正向代理和反向代理
正向代理:是一个位于客户端和原始服务器(origin server)之间的服务器, 为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器), 然后代理向原始服务器转交请求并将获得的内容返回给客户端。 客户端必须要进行一些特别的设置才能使用正向代理。
反向代理:反向代理(Reverse Proxy)实际运行方式是指以代理服务器来接受internet上的连接请求, 然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端, 此时代理服务器对外就表现为一个服务器。
9.登录流程,状态持久化
同一面
10.token如何使用
- 1.客户端使用用户名跟密码请求登录
- 2.服务端收到请求,去验证用户名与密码。验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 3.客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 LocalStorage 里。客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 4.服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
11.判断object和array的方法
判断Array的方法:
//最稳妥方法
Object.prototype.toString.call([1,2,3]) // "[object Array]"
//instanceof方法
[1,2,3] instanceof Array
//构造函数法 注:如果 a = null ,还要报错,这种方式坚决不推荐!!!
[1,2,3].constructor === Array;//true
//检查原型的构造方法
[1,2,3].__proto__.constructor === Array
判断Object的方法:
//最稳妥方法
Object.prototype.toString.call({a: 123}) // "[object Object]"
//构造函数法 注:如果 a = null ,还要报错,这种方式坚决不推荐!!!
var o = {a: 123};
o.constructor === Object; // true 对象
//instanceof方法
var n2 = new Number(123) // 现在 n2 是一个 object 元素!!!
typeof n2; // "object"
n2 instanceof Number; // true
12.算法
无重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合,字符串每个字符均不相同。
示例:
输入:S = "qwe"
输出:["qwe", "qew", "wqe", "weq", "ewq", "eqw"]
时间复杂度
【👋 字节三面 35min】
1.react、vue设计思想
2.常见状态码
| 状态码 | 状态码英文名称 | 中文描述 |
|---|---|---|
| 200 | OK | 请求成功。一般用于GET与POST请求 |
| 204 | No Content | 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 |
| 206 | Partial Content | 是对资源某一部分的请求,服务器成功处理了部分GET请求,响应报文中包含由Content-Range指定范围的实体内容。 |
| 301 | Moved Permanently | 永久性重定向。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替 |
| 302 | Found | 临时性重定向。与301类似。但资源只是临时被移动。客户端应继续使用原有URI |
| 303 | See Other | 查看其它地址。与302类似。使用GET请求查看 |
| 304 | Not Modified | 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 |
| 307 | Temporary Redirect | 临时重定向。与302类似。使用GET请求重定向,会按照浏览器标准,不会从POST变成GET。 |
| 400 | Bad Request | 客户端请求报文中存在语法错误,服务器无法理解。浏览器会像200 OK一样对待该状态吗 |
| 401 | Unauthorized | 请求要求用户的身份认证,通过HTTP认证(BASIC认证,DIGEST认证)的认证信息,若之前已进行过一次请求,则表示用户认证失败 |
| 402 | Payment Required | 保留,将来使用 |
| 403 | Forbidden | 服务器理解请求客户端的请求,但是拒绝执行此请求 |
| 404 | Not Found | 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面。也可以在服务器拒绝请求且不想说明理由时使用 |
| 500 | Internal Server Error | 服务器内部错误,无法完成请求,也可能是web应用存在bug或某些临时故障 |
| 501 | Not Implemented | 服务器不支持请求的功能,无法完成请求 |
| 503 | Service Unavailable | 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中 |
3.协商缓存
同一面
4.发布订阅和观察者模式区别
观察者模式: 观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。
发布订阅模式: 订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
5. webpack设计理念
它是一个模块打包器,也可以引用官网的一幅图解释,我们可以看到webpack,可以分析各个模块的依赖关系,最终打包成我们常见的静态文件,.js 、 .css 、 .jpg 、.png。webpack是前端用来构建文件的管理工具,可以将各种插件创建的文件、各种版本资源的文件,打包成各种版本浏览器兼容的代码、资源文件。webpack是依赖于npm创建的插件工具。
- 具体流程就是:
- 首先各种模块文件导入到webpack打包配置文件中
- 将模块文件解析成chunk代码块,对各个模块间的关系进行静态分析
- 根据模块的依赖关系进行各种文件打包成各种版本浏览器可识别的文件
- 配置浏览器入口文件bundle
6. webpack的plugin和loader区别
- 加载器loader: webpack默认处理js和json文件,loader配置webpack去处理其他类型的文件,将其转为有效模块给应用程序使用,并添加到依赖图中。
- 插件plugin: loader用于转换某些类型的模块,而插件用于执行范围更广的任务。比如:打包优化、资源管理、注入环境变量等。
7. 原型链
8.项目难点
9.事件循环机制
见一面
10.实现deepClone(考虑循环引用)
-
浅拷贝:指的是将一个对象的属性值赋值到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址赋值给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用Object.assign和展开运算符来实现。
-
深拷贝:如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值赋值给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用JSON的两个函数来实现,但是由于json的对象格式比js的对象格式更加严格,所以如果属性值里面出现函数或者Symbol类型的值时,会转换失败。
function deepCopy(obj){ //先要判断当前输入的对象是数组还是object let newObj = obj instanceof Array ? [] : {}; for(const [key,value] of Object.entries(obj)){ newObj[key] = typeof value === "string" ? deepCopy : value; }; return newObj; };
【🔥 参考文章】
- 浏览器缓存机制:juejin.cn/post/684490…
- 单点登录:mp.weixin.qq.com/s/WgRujrI9W…
- 浏览器缓存机制:www.cnblogs.com/suihang/p/1…
- 正向代理和反向代理:juejin.cn/post/684490…
- cors的原理:juejin.cn/post/684490…
- 判断Array的方法:juejin.cn/post/684490…
大家对于本篇博文出现的不足之处有什么建议或者问题的话可以在评论区讨论鸭~ ✌️
整理不易,记得关注、点赞 👍 支持一下哦~ 😘