写之前
想说说自己为何写这么一篇。笔者学历渣渣,虽然三年经验,不过都是小厂,虽然一心向往,不过此前基本没什么机会,也是赶着正好三年这个节骨眼上,试着去冲下大厂,相信很多小伙伴应该是跟我一样的想法。然后就接到了某大厂面试机会,嗯,最后失败了,不过觉得收获还是挺多的,也算是走出了迈向大厂的第一步吧。具体流程就不说了,就聊聊面试题,感受一下和之前面试的区别。
面试的一些题
HTML5的新标签section和article的语义是什么
这里回答的并不好,就说了section表示某块功能区域,article表示具体的内容,就是从单词字面意思解释了一下,后来翻了一下whatwg文档,上面是这么描述section和article的。
The section element represents a generic section of a document or application. A section, in this context, is a thematic grouping of content, typically with a heading.
The article element represents a complete, or self-contained, composition in a document, page, application, or site and that is, in principle, independently distributable or reusable, e.g. in syndication. This could be a forum post, a magazine or newspaper article, a blog entry, a user-submitted comment, an interactive widget or gadget, or any other independent item of content.
When article elements are nested, the inner article elements represent articles that are in principle related to the contents of the outer article. For instance, a blog entry on a site that accepts user-submitted comments could represent the comments as article elements nested within the article element for the blog entry.
Author information associated with an article element (q.v. the address element) does not apply to nested article elements.
简单翻译一下就是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上。
手写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实现代码整理好放在了这里
上面这些只是面试的一部分,并且是自己回答的不够好或者是没有回答出来的,总的感受就是:并不是你会就可以了,还需要真正理解,所谓真正理解就是无论题目怎么变,原理还是那些,只有真正搞明白原理,才不会被问倒。
关于算法,也是参考了很多社区大佬的学习方法,分类刷题什么的,但是还是忽略了一些东西,比如思考的过程,如何推导,是否还有其他方法等等,而不是看答案对不对。所以其实刷题并不是让我们能有足够的题目数量的经验,而是培养解决问题的思考过程,如何思考,如果思路不正确的话,能否换个思路去解决,这才是该去关心的。
最后希望此篇能给各位正在准备面试的小伙伴一些经验和思考。