从2017年12月底,有了换一个平台的想法,陆陆续续收到了一些面试机会,又失败,也有获得offer的。
在每一次面试之后,我的看法都比较负面,因为确实短板很多,不了解不会的东西太多。每次面试之后我都会花费很长时间将面试中遇到的问题复盘总结。这样做确实在短期对自己的提高很大。
我会在这里将每次面试的总结分次发出来,可能不会有其他前端同学看到,就算看到也会觉得“啊你水平好low”。没关系,我就是这个水平,但是如果能够帮到谁,那我就很高兴。
过了九个月之后,又踏上了面试的旅途
本来对爱XX这个公司兴趣不太,本来不太想去,后来又去了的原因主要有两个:
- 觉得在现在的公司业务上能学到的东西不多了,而且公司技术研发水平、产品实力确实都很一般,也没有人指导一下
- 昨天得知自己做的项目组可能要被合并到另外一个项目组当中,确实非常失望非常沮丧
所以抱着增加些经验的想法就去参加了面试。
这个公司也是做互联网教育的,不过侧重点不是在教育产品上,而是为从事教育的老师、学校、培训公司做工具类产品,比如制作在线PPT的工具
根据面试官说的,这个在线制作PPT的工具还是很复杂的,其他产品线也很多,所以程序研发人员有100多人,前端30多人
说实在的,我对他们的产品不感兴趣,也不太看好,但是没经过深入了解,只是直觉上受众面太窄,成功的可能性不大
公司就是这个情况,而我自己的发挥也很不好,虽然比起九个月之前懂的东西多了很多,但是要想在职业生涯上再进一步,现在的水平是觉得不够的
通过这次面试主要暴露出来的问题是:
- 已经会的、用过的东西,细节了解不到位、不深入
- 新的技术栈比如VUE等接触太少
- 算法、数据结构等基础太薄弱
果然,半路转行的程序员,再加上脑子又不那么灵光,职业生涯的道路太艰难了
下面复盘面试题目,先是笔试题部分:
1、给原生对象String添加名称为trim
的原型方法,用于截取空白字符串,要求alert(' ok'.trim())
输出'ok'
分析:本来以为这道题答的差不多,结果发现大错特错:
- 向原型添加方法之前要判断一下已有的方法是否存在:
if(String.prototype.trim)
如果不存在再添加,存在的话还添加个屁
不幸的是,String对象是存在trim的原生方法的,trim()方法会返回一个新的字符串,删除字符串前或者后存在的空格:
console.log(' 123'.trim()); // 返回结果'123'
console.log('123 '.trim()); // 返回结果'123'
console.log('12 3'.trim()); // 返回结果'12 3'
如果不存在话则可以添加,现在假设要添加的是trim2()方法
- 第二个问题就是没有理解题目的意思,题目的意思是删除前面或者结尾的空格,我理解成为删除所有空格了
- 即使是删除所有空格,我的方法也是失败的,因为对于空字符串
''
判断是false,但是对于空格判断' '
,是true
console.log(!!""); // false
console.log(!!" "); // true
正确的方法是应该是利用这则表达式,\s
匹配任何空白字符,利用replace
方法来替换空白字符串
String.prototype.trim2 = function () {
return this.replace(/(^\s*)|(\s*$)/g, '');
};
- 不能获得输入值,
this
指的运行时此函数在什么对象上被调用,String原型上的方法的this
指代的就是调用这个方法的字符串
所以正确的答案应该是:
if (!String.prototype.trim2) {
String.prototype.trim2 = function () {
return this.replace(/(^\s*)|(\s*$)/g, '');
}
}
==丢人啊!!!!!!!!!!!!!!!!!!!!!!!!!!!==
2、考察逻辑&&运算
var a = 0, b =1;
a && b
对这里的理解太不到位了~根本就不知道原理, 竟然写了false
,我去
几乎所有语言中||
和&&
都遵循短路原理:
- 如
&&
中第一个表达式为假就不会去处理第二个表达式,而||
正好相反。 - 当
||
时,找到为true
的分项就停止处理,并返回该分项的值,否则执行完,并返回最后分项的值。 - 当
&&
时,找到为false
的分项就停止处理,并返回该分项的值。 &&
优先级高于||
,先运算&&
,再用&&
运算的结果去||
运算
==&&
找 false
, ||
找 true
==
所以上面题目的结果应该是0
var a = 0 && -1; //0
var b = 3 && 4; //4
var c = 0 && 1 && 2; //0
var d = 1 && 0 && 2; //0
var e = 1 && 2 && 3; //3
var f = 0 && false && '' && 1; //0
var g = 0 || 1; //1
var h = 1 || 0; //1
var i = 1 || 2 && 3; //1
var i = 1 || 2 || 3; //1
var x = false || 0 || ''; //''
var j = 1 && 2 || 3; //2
3、考察类型转换和运算顺序
'1' + 2 + 3
第一道做对的题,运算顺序是从左至右,所以先进行了类型转换,转换为了字符串类型,所以结果是'123'
4、考察作用域和函数声明提前
var a = 1;
function test() {
a = 3;
return ;
function a() {}
}
test();
console.log(a)
当时考虑的完全不在点子上,看到return
了根本没多想,直接认为是a = 3
的赋值操作了,实际上存在着函数声明提前的情况,代码相当于:
var a = 1;
function test() {
var a = function (){};
a = 3;
return ;
}
test();
console.log(a) // 1
这样就相当于a
是test
函数的内部变量,改变不了外部的a
的值,所以输出值是1
效果与下面的代码是一样的:
var a = 1;
function test() {
var a = 3;
}
test();
console.log(a) // 1
还有就是如果在函数内部没有用var
声明,则相当于赋值操作,如果在上面所有级别的作用域链都没有对应变量,则相当于创建了一个全局变量:(在严格模式下会报错)
function test() {
a = 3;
}
test();
console.log(a) //3
还有两个类似的例子:
function bar() {
return foo;
foo = 10;
function foo() {};
var foo = 11;
}
console.log(typeof bar());//'function'
var scope = "global";
function test() {
console.log(scope); //输出undefined,而不是global
var scope = "local"; //变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的。
console.log(scope); //输出local
}
test()
排序问题
考了两个问题,一个是根据绝对值差进行排序,还有一个就是直接写出冒泡排序的算法
我竟然都写错了
冒泡排序是进行了两轮排序,都是从0开始,然后进行两两排序,完成之后再从外轮再来一次
题目就是给定一个数组,根据传入的参数n对数组排序,排序的规则就是数字与n的差值的绝对值
var arr = [7, 28, -1, 0, 7, 33];
function sort(n) {
for (var i = 0; i < arr.length; i++) {
for (var j = 0; j < arr.length; j++) {
if (Math.abs(arr[j] - n) > Math.abs(arr[j + 1] - n)) {
var temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
console.log(sort(0))
position的属性和区别
写的不够全面和准确:
- static:默认值,没有定位,元素出现在正常的文档流中
- relative:相对定位,是相对于其正常位置进行定位
- absolute:绝对定位,是相对于static定位之外的第一个父元素进行定位
- fixed:绝对定位,相对于浏览器窗口进行定位
- inherit:从父元素继承position属性的值
考察CSS三角形的写法
实心的答对了:
.left {
position: relative;
flex-shrink: 0;
width: 0;
height: 0;
border: 50px solid transparent;
border-top: 50px solid red;
border-bottom: 0;
margin-right: 20px;
}
空心的怎么都想不起来,当时就考虑怎么一下子用一个类实现,结果忘了after伪元素遮挡一下的事情,脑子转的太慢
.left:after {
position: absolute;
left: -50px;
bottom: 5px;
width: 0;
height: 0;
content: '';
border: 50px solid transparent;
border-top: 50px solid #fff;
border-bottom: 0;
}
注意,将border-bottom置为0的原因是防止透明的border-bottom影响其他元素和after元素的定位
下面是面试的问题:
异步函数的执行顺序
主要异步函数队列中macrotask 和 macrotask的执行顺序
之前根本不了解这些概念,只知道同步执行完了执行异步,现在才知道,在异步函数中还分为两个队列
setTimeout(function() {
console.log(4)
}, 0);
new Promise(function(resolve) {
console.log(1)
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve()
}
console.log(2)
}).then(function() {
console.log(5)
});
console.log(3);
// 1 2 3 5 4
装饰器
知道这么个东西,是ES6的新的语法,但是当时只是过了一眼,根本没往心里去,所以根本回答不出来
现在大概理解装饰器就是为了修改类的行为的函数,其实在mobx中就用过,只是不知道为什么这么用
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
exam中用到的:
// 点击显示更改学生自判答案的菜单
@action showSelect(index) {
for (let i = 0; i < this.selectShow.length; i++) {
this.selectShow[i] = false
}
this.selectShow[index] = true;
}
现在还是不是很了解,把阮一峰ES6的那本书看一遍再说吧
模块化
啥东西,不了解
所谓模块化,就是防止变量之间的相互交叉,具有独立性的对象:
var module1 = (function() {
var _count = 0;
var m1 = function() {
//...
};
var m2 = function() {
//...
};
return {
m1: m1,
m2: m2
};
})();
有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。
通行的Javascript模块规范共有两种:CommonJS和AMD
node.js的模块系统,就是参照CommonJS规范实现的,在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。
var math = require('math');
然后,就可以调用模块提供的方法:
var math = require('math');
math.add(2, 3); // 5
由于一个重大的局限,使得CommonJS规范不适用于浏览器环境,上一节的代码,如果在浏览器中运行,会有一个很大的问题,第二行math.add(2, 3),在第一行require(‘math’)之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。
这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。
因此,浏览器端的模块,不能采用”同步加载”(synchronous),只能采用”异步加载”(asynchronous)。这就是AMD规范诞生的背景。
AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD也采用require()
语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module]
,是一个数组,里面的成员就是要加载的模块;第二个参数callback
,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function(math) {
math.add(2, 3);
});
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。
大改先这么理解吧,具体的再学习
Async Await
对于这里会用,但是始终不太理解原理
async 函数就是 Generator 函数的语法糖。
所以,想要彻底理解async还是要彻底理解generator和promise对象
加入ES6学习序列吧
react生命周期
老毛病,了解,但不深入,不清晰,用这张图很好:
react虚拟DOM的理解
虚拟DOM是用JS的对象结构模拟出html中DOM的结构,批量的增删改查,由于直接操作的是JS对象,所以速度要比操作真实DOM要快,最后更新到真正的额DOM中
虚拟DOM构建的对象,除了dom相关属性,还包括了React自身需要的属性,比如ref,key等,大概如下结构:
{
type: 'div',
props: {
className: 'xxx',
children: [{
type: 'span',
props: {
children: ['CLICK ME']
},
ref: key:
}, {
type: Form,
props: {
children: []
},
ref: key:
}] | Element
}
ref: 'xxx',
key: 'xxx'
}
react何时将虚拟DOM渲染为真实DOM
render这个步骤就是react组件挂载的步骤
react组件挂载:将组件渲染,并构建DOM元素然后插入页面的过程
render的步骤是创建虚拟DOM,挂载组件,
在render之后,将这个虚拟 dom 树真正渲染成一个 dom 树,插入了页面,可以认为是在componentDidMount这个步骤完成的,该方法被调用时,已经渲染出真实的 DOM
在组件存在期,componentDidUpdate与componentDidMount类似,在组件被重新渲染后,此方法被调用,真实DOM已经渲染完成
react不能setState的步骤
shouldComponentUpdate和componentWillUpdate就会造成循环调用,使得浏览器内存占满后崩溃
render里面不能调用,我脑子吃屎了吧~
时间复杂度为n的去重
时间复杂度: 算法的时间复杂度是一个函数,它定性描述了该算法的运行时间。这是一个关于代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述
空间复杂度(Space Complexity): 对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n)
比如直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量。
面试的题目是从数组中找出一个重复的数字,要求时间复杂度为O(n),当时整个都蒙了,现在想来不过是用哈希结构来判断即可
let arr = [1, 2, 3, 5, 34, 44, 3, 100];
function sort(arr) {
let obj = {};
let result;
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
obj[arr[i]] = true
}
else {
result = arr[i]
}
}
return result;
}
console.log(sort(arr)) //3
不过不知道当时面试官说的看成纯数学问题,利用求和来判断是什么方法
webpack中插件和loader的区别
loader是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程其作用。