写之前
想说说自己为何写这么一篇。笔者学历渣渣,虽然三年经验,不过都是小厂,虽然一心向往,不过此前基本没什么机会,也是赶着正好三年这个节骨眼上,试着去冲下大厂,相信很多小伙伴应该是跟我一样的想法。然后就接到了某大厂面试机会,嗯,最后失败了,不过觉得收获还是挺多的,也算是走出了迈向大厂的第一步吧。具体流程就不说了,就聊聊面试题,感受一下和之前面试的区别。
面试的一些题
HTML5的新标签section和article的语义是什么
这里回答的并不好,就说了section表示某块功能区域,article表示具体的内容,就是从单词字面意思解释了一下,后来翻了一下whatwg文档,上面是这么描述section和article的。
简单翻译一下就是section元素表示文档的通用节,也就是按照主题进行划分的一块区域,通常会带上一个标题。而article则表示文档、页面、应用程序或站点中的完整或自包含的组合,是可独立分发或可重用的。举例来说article可以表示论坛帖子、杂志或报纸文章、博客条目、用户提交的评论、交互式小部件或小工具,或者任何其他独立的内容,内部嵌套的article也需要和外部的article内容上是相关联的,但是作者相关的信息则不适合使用article,而可以使用address元素。
CSS选择符的优先级以及如果元素带有一个id属性和10个class属性,那么声明CSS的时候,哪个优先级更高
CSS选择符的优先级我们都知道,id选择符 > class选择符、属性选择符、伪类选择符 > 标签选择符、伪元素选择符 > 通用选择符。那么如上描述一个id和10个class的选择符,哪个优先级更高呢?
当时就有点懵了,因为我回答的是id代表100优先级,class代表10优先级,那10个class理论上是不是应该能代替id呢。觉得这么一算好像又不太合理。后来查阅了《CSS权威指南》,上面所谓100、10的优先级称为特指度,特指度的比较是从左往右的,也就是说一个id选择符的特指度是100,写成0,1,0,0,而10个class选择器的特指度是100,但是写成0,0,100,0,从左往右比较的话,0,1,0,0永远是大于0,0,100,0的,这里class选择符叠加的再多,也不会有进位,因此1个id选择符的优先级大于任意数量的class叠加的优先级。
使用bind返回的函数作为构造函数执行,this是指向了什么
这里就是考察队bind实现的一个理解,所以先看下bind方法是如何实现的:
Function.prototype.myBind = function(context, args) {
const self = this;
const params = Array.prototype.slice.call(arguments, 1);
function FNO() {}
function Fn() {
const bindParams = Array.prototype.slice.call(arguments);
// 如果当前this是函数的实例,则此函数作为构造函数使用,因此其上下文指向了实例,否则的话其上下文就是指定的context
return self.call(this instanceof Fn ? this : context, params.concat(bindParams));
}
FNO.prototype = this.prototype;
Fn.prototype = new FNO();
return Fn;
}
通过这个实现问题就很明显了,如果bind返回的函数作为构造函数来调用的话,那么this并不会指向设置的那个this,而是指向了构造函数的实例。我们可以用一个例子来证明一下:
var obj = {
name: 'qiugu'
};
function Person(age) {
this.age = age;
}
// 注意这里new后面要加括号,不然会报错,因为Person.bind不能作为构造函数调用
const p = new (Person.bind(obj, 20));
console.log(p);
好了,清晰明了,根据bind的实现,也可以看到作为构造函数调用时,也就是this instanceof Fn
这里,this就指向了构造函数的实例了。
new调用的构造函数如果有返回值的话,返回什么,如果返回值是一个对象或者是基础类型的话又返回什么
这里也是和上面一样,关键还是要知道new的一个实现,来复习下new的实现:
function objFactory(fn, ...args) {
// 生成一个空对象
const obj = new Object();
// 将对象的原型链接到构造函数的原型上,这么做就能使对象访问到构造函数原型上的属性方法
obj.__proto__ = fn.prototype;
// 执行构造函数,利用call将上下文指向刚刚创建的对象,这样就能访问this上的属性方法
const res = fn.call(obj, ...args);
// 如果构造函数有返回值的话,需要判断返回值的类型是否是对象,如果是对象就返回这个对象
// 如果是基础类型,则还是返回创建的对象
return typeof res === 'object' ? res : obj;
}
那么答案也很明显了,所以有的时候,面试官可能并不会直接去问你new怎么实现,你也许背过,也许正好看到过,换个其他的方式更能证明是否真正理解了其中的原理。
数组去重的方法
这个也比较简单,但是如果想要面试官更加满意的话,那么以下方法都是要理解的(小声bb:我当时就记得三种了)。
- 双重for循环
function unique(arr) {
if (!Array.isArray(arr)) return;
let res = arr[0];
for(let i = 1; i < arr.length; i++) {
let flag = true;
for(let j = 0; j < res.length; j++) {
flag = true;
if (arr[i] === res[j]) break;
}
if (flag) res.push(arr[i]);
}
return res;
}
- indexOf
function unique(arr) {
if (!Array.isArray(arr)) return;
let res = [];
for(let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) !== -1) {
res.push(arr[i]);
}
}
return res;
}
- filter
function unique(arr) {
if (!Array.isArray(arr)) return;
return arr.filter((item, index) => arr.indexOf(item) === index);
}
- sort
function unique(arr) {
if (!Array.isArray(arr)) return;
arr.sort();
let res = [];
for(let i = 0; i < arr.length; i++) {
if (arr[i] !== arr[i-1]) res.push(arr[i]);
}
return res;
}
- reduce
function unique(arr) {
if (!Array.isArray(arr)) return;
return arr.reduce((prev, cur) => {
return prev.includes(cur) : prev : [...prev, cur];
}, []);
}
- Set
function unique(arr) {
if (!Array.isArray(arr)) return;
return [...new Set(arr)];
}
// 也可以使用Array.from来把Set转成数组
function unique(arr) {
if (!Array.isArray(arr)) return;
return Array.from(new Set(arr));
}
- 使用对象或者Map去重
function unique(arr) {
if (!Array.isArray(arr)) return;
let obj = {}, res = [];
for(let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
res.push(arr[i]);
obj[arr[i]] = 1;
} else {
obj[arr[i]]++;
}
}
return res;
}
参考来源
TypeScript中的any和unknown的区别
我用过ts,但是没用过unknown,所以没回答上来,回去翻了资料。
简单说下,any可是是任何类型的父类或者是子类,是类型不安全的,什么是类型不安全?很好理解,就是平常我们懒得写定义,直接any,让编译器忽略检查any类型的值,这么做就会产生一些意想不到的情况,导致代码出错,而且很难去排查。而unknown则是类型安全的,unknown也是可以赋值任何值,但是当我们进行使用它进行一些操作的时候,比如把unknown类型的值作为一种方法来调用,编译器就会报错,因为你不确定这个变量是不是个方法,因此是不能调用的,需要在调用前确保它可以被调用。
let fn:unknown = () => {};
if(typeof fn === 'function') {
fn();
}
这个时候再去调用,编译器就不会报错了,所以是类型安全的。
Hooks的实现,以及为什么采用这种方式实现,useState的状态怎么存储的
关于React中的源码实现,自己只是看过一些分析的文章,实际还是缺少深入的了解。
这里自己只回答了Hooks是用链表去实现的,所以如果把Hooks放入判断条件中,会破坏链表的结构。采用链表结构,也是从链表本身出发,链表对空间要求低,不需要连续的空间,链表添加删除操作效率高。关于useState如何存储状态,我们知道React中使用Fiber作为一种结构来存储组件的状态,所以useState中的状态也存储在节点对应的Fiber上。
Hooks的极简实现
手写EventEmit的实现
这道题也没有做出来,虽然有了解过,但是并没有注意其实现,后面就认真的看了其实现,然后自己再写了一遍,去理解其实现原理。
function EventEmit() {
this.listeners = {};
}
EventEmit.prototype.on = function(eventName, cb) {
// 因为事件是可以重复注册的,所以需要用一个数组来存储事件回调的队列
if (!this.listeners[eventName]) {
this.listeners[eventName] = [cb];
} else {
this.listeners[eventName].push(cb);
}
}
EventEmit.prototype.once = function(eventName, cb) {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [cb];
} else {
this.listeners[eventName].push(cb);
}
// 使用一个标记来标明这是一个一次性的事件回调
this.listeners[eventName].once = true;
}
EventEmit.prototype.off = function(eventName) {
if (this.listeners[eventName]) {
this.listeners[eventName] = null;
}
}
EventEmit.prototype.emit = function(eventName, args) {
if (this.listeners[eventName]) {
this.listeners[eventName].forEach(fn => fn.apply(this, args));
// 如果这个是一次性的事件的话,执行完成后销毁该事件
if (this.listeners[eventName].once) this.off(eventName);
}
}
算法题
/**
* 把一日划分为48段,每段是半个小时,用一个48位的位图表示一日中的多个工作时间段
* 按如下输入输出设计一个方案,计算一日中的有效工作时间段
* 输入'110011100000000000000000111100000000000000000000'
* 输出['00:00-01:00', '02:00-03:30', '12:00-02:00']
*/
function solution(bitmap) {
let p = 0, res = [], ans = [];
for(let i = 0; i < bitmap.length; i++) {
if (bitmap[i] === '0') p++;
else if (bitmap[i] === '1' && (bitmap[i+1] === '0' || !bitmap[i+1])) {
res.push([p, i]);
p = i+1;
}
}
const format = (left, right) => {
const timeZone = new Date().getTimezoneOffset() * 60 * 1000;
const leftTime = new Date(left / 2 * 60 * 60 * 1000 + timeZone);
const leftHours = leftTime.getHours() < 10 ? '0' + leftTime.getHours() : leftTime.getHours() + '';
const leftMinus = leftTime.getMinutes() < 10 ? '0' + leftTime.getMinutes(): leftTime.getMinutes() + '';
const leftStr = `${leftHours}:${leftMinus}`;
const rightTime = new Date((right / 2 + 0.5) * 60 * 60 * 1000 + timeZone);
const rightHours = rightTime.getHours() < 10 ? '0' + rightTime.getHours() : rightTime.getHours() + '';
const rightMinus = rightTime.getMinutes() < 10 ? '0' + rightTime.getMinutes(): rightTime.getMinutes() + '';
const rightStr = `${rightHours}:${rightMinus}`;
return [leftStr, rightStr];
}
for(let i = 0; i < res.length; i++) {
const item = format(res[i][0], res[i][1]);
ans.push(item);
}
return ans;
}
console.log(solution('110011100000000000000000111100000000000000000011'));
这题是后面自己才做出来的,实现思路就是将上面48位的位图,转换成这样一种结构
// 每个子数组存储对应的1的索引区间
[[0, 1],[4,6],[24,27]]
所以如何去转成这样一种结构才是这题的关键,虽然自己力扣也写了有200题,但是还是忽略了思考的过程,这也是此次感受最深的地方。
写到最后
自己将一些API实现代码整理好放在了这里
上面这些只是面试的一部分,并且是自己回答的不够好或者是没有回答出来的,总的感受就是:并不是你会就可以了,还需要真正理解,所谓真正理解就是无论题目怎么变,原理还是那些,只有真正搞明白原理,才不会被问倒。
关于算法,也是参考了很多社区大佬的学习方法,分类刷题什么的,但是还是忽略了一些东西,比如思考的过程,如何推导,是否还有其他方法等等,而不是看答案对不对。所以其实刷题并不是让我们能有足够的题目数量的经验,而是培养解决问题的思考过程,如何思考,如果思路不正确的话,能否换个思路去解决,这才是该去关心的。
最后希望此篇能给各位正在准备面试的小伙伴一些经验和思考。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!