『面试的经历』—— 面试官是怎么问穿你的假简历

10,418 阅读19分钟

最近疫情好转了很多,大家的心也开始躁动不安了,都有跳槽的心思。最近每天都会面试好几个小伙伴,以防不急之需。看了太多的简历,感觉可以分为三种,真假简历、假简历、真简历。那我是怎么问穿这些简历,下面给大家做个分享。

首先发几句牢骚,在刚开始带团队的时候,好不容易带会一个小伙伴,结果小伙伴呆了一年就要走了。最初会抱怨同事不够忠诚,现在就很坦然了,所谓忠诚就是背叛的筹码不够。最近感觉筹码够了,也动了跳槽的心思,恰逢公司每年例行的纳新,所以现在就处于一个不断被面试和面试的状态。面试的人多了,有诸多感触,想和大家分享一下。

下面主要分享一下,我面试过程中遇到假简历,想造假的伙伴们可以看一下,免得踩坑。

一、工作经验方面

很多求职者比较喜欢在这方面造假,主要有以下两种:

1、延长工作经验

去年年底时候,公司想找个经验丰富的前端,准备再设立一个研发部,当时面试一个据称有9年工作经验的大佬。简历非常好看,一家公司待了4年,另一家在职已5年了,一看就感觉很稳定,技术栈很全面,我当时很是激动,心想公司终于肯花钱招大佬了,本人才4年工作经验。

面试中,

  • 我:您用Vue.js框架几年了?
  • 他:8年了。
  • 我当时就点懵,不相信得再问他:“那您是2012年就开始用Vue?”。
  • 他:嗯,没错。
  • 我仍不死心,再问,那你刚开始用的是Vue几呢?
  • 他:当然是Vue2了。

此时心里一万头草泥马奔腾而过,还能不能靠谱点,2012年Vue还没问世吧。面试刚开始就结束了。

在这里我想说:

要造假麻烦先去了解一下前端每个技术栈的发展史吧!!

Vue的简史:

  • 2013年7月28日,尤雨溪在GitHub上第一次为Vue.js提交代码,当时还不叫Vue.js。
  • 2013年12月8日,尤雨溪在GitHub上发布0.6.0版本,并将项目正式改名为Vue.js。
  • 2015年10月27日,Vue.js迎来1.0.0版本。
  • 2016年10月1日,Vue.js迎来2.0.0版本,进入2的时代。

其他技术栈的发展史,都可以去git提交日志里面查到的。如Vue的

2、合并工作经验

合并工作经验的小伙伴,跳槽很频繁吧,怕让人觉得很不稳定。想法是好的,的确在同等条件下,我会考虑每份工作经验比较长的。

但是合并的时候,可不可牢记一下你的项目经历要跟工作经验的时间结点对的上。

在4月份刚面试了一位3年工作经验的小姐姐,人非常漂亮,第一印象非常好。

简历上的工作经验:

  • 2016.05.10-2018.04.20 xx公司 web前端工程师
  • 2018.04.24-2019.03.29 xx公司 web前端工程师
  • 2019.4.8-至今 xx公司 web前端工程师

当时我对她早期的一个项目经历非常感兴趣,这个项目是用Vue开发,element做为组件库,主要功能是地图应用和数据大屏,跟公司的一个要启动的项目比较接近。简历上写是她独立完成的,问了她很多东西。感觉还不错,蛮靠谱的,最后想确认一下这个项目的工期。

  • 我:这个项目什么时候开始的,花了多少时间完成这个项目
  • 她:2018年3月份开始,花了4个多月。
  • 2018年3月份,简历上写的是2018年5月份,我把她的话重复一遍,2018年3月份开始,花了4个多月。
  • 她:嗯,这个项目功能很多,从开发到上线花了4个多月。接下来她给我描述这个项目的过程。
  • 我:3月份开始的,7月份上线的,是不是很赶,中途是不是很经常加班。
  • 她;是经常加班,以项目为主。

好吧,我已经跟她确认好几遍了,这个项目经历跟她的工作经验的时间节点对不上,虽然她非常适合,但是最终还是被我pass掉。我不敢打保票,她的工作经验是否真实,之前是否有频繁跳槽。

二、项目经历方面

很多小伙伴在写项目经历时,会习惯的把别人做过的东西据为己有,添加到自己的项目经历里面,自认为这样会使自己的项目经历非常丰富。

欲戴其冠,必承其重。 欲握玫瑰,必承其伤。

别人的东西,如果你没有把它理解透,就把它据为己有,到时候受伤的还是自己。下面用一个面试来说明。

4月中旬时候面试了一位有2年工作经验的小伙伴。项目经历很丰富,短短两年时间做了9个项目,而且很多项目都是负责人。

我就开始一一和他交流起来。

1、讲讲Vue路由是怎么配置

他想了一会儿才回答:“要先import Vue和Vue Router,然后再用Vue.use安装路由。”

我听了接着说那你先写一下吧。

import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);

我看了一下,说到:“这个是安装Vue Router,那怎么配置。你继续写一下。” 这次他想的有点久,我提醒:“再给你3分钟思考。” 他磕磕绊绊写出来

const router = new Router({
    mode: 'history',
    routes:[
        {
            path: '/a',
            component: () =>import('page/a'),
        }
    ]
});

好吧跟挤牙膏似的。我安慰他别紧张,“为什么要用() =>import('page/a')引入组件a。”

"这样写可以懒加载这个组件。"

“什么懒加载,有什么好处,在哪里体现?”

“懒加载就是在要展示这个组件时,才加载这个组件的资源,可以减少首屏加载时间。”

还可以,我本来还想继续问:“为什么这样写会实现,展示组件时才加载组件的资源?” 算了吧,这涉及到webpack了。

我继续问道,“如果我项目部署在xxx.com/test/下面。路由应该怎么配置?”

他支吾了半天,我提醒到:“base这个选项有没有用过,你平时怎么配置这个选项?”

“用Vue Cli搭建项目时自动生成的,一般都是默认的。”

好吧,我直接告诉他答案;“把base配置为/test/。” 反正下午也没事情,继续聊着吧。继续问:“那配置完路由,怎么引入到项目使用?”

他在纸上继续写

const router = new Router({
    mode: 'history',
    routes:[
        {
            path: '/a',
            component: () =>import('page/a'),
        }
    ]
});
const  vm = new Vue({
    el:"#app",
    router:router,
    render: h => h(App)
})

“你平时都在main.js里面配置路由吗?独立用一个文件配置路由,怎么写”

“把路由写在src/router文件夹下index.js文件中,再把它export导出”

export default router

然后再main.js中写

import router from 'src/router'
const  vm = new Vue({
    el:"#app",
    router:router,
    render: h => h(App)
})

“写在index.js文件,那么为啥是import router from 'src/router'而不是import router from 'src/router/index'”,我问道。

他又在支吾了,好吧我承认我有点刁难了。

我直接告诉他答案;“因为在webpack的resolve(解析)配置中有mainFiles这个选项,作用是配置解析目录时要使用的文件名,默认是 ["index"],所以才不要写index。”

题外话

这里说个题外话,那为什么import Vue from 'vue';import Router from 'vue-router';这样可以引入Vue和vue Router,它们不是在node_modules里面吗?

其实还是webpack的resolve(解析)配置中的modules这个选项起作用,告诉webpack 解析模块时应该搜索的目录,默认是["node_modules"],所以才会直接去node_modules里面寻找。

而resolve.mainFields这个配置是告诉我们当从node_modules中导入模块时,此选项将决定在 package.json 中使用哪个字段导入模块,默认["module", "main"]

在Vue源码中package.json,有这一段配置

"main": "dist/vue.runtime.common.js",
 "module": "dist/vue.runtime.esm.js",

所以import Vue from 'vue',实际上去加载dist/vue.runtime.esm.js这个文件,而这个文件最后有 export default Vue;,这样就把Vue引入我们的项目。

回到主题

“没关系,要求把router选项的内容独立配置在一个文件里面,你要怎么做?”

这次他很快就写出来,看来还是会点嘛。在src/router文件夹下建立routes文件,在里面写。

const routes=[
    {
        path: '/a',
        component: () =>import('page/a'),
    }
]
export default routes

然后在index.js文件中

import routes from './routes'
const router = new Router({
    mode: 'history',
    routes: routes
});

看来这位兄台路由的基本配置还是会的,再问深入一点:“怎么通过路由配置地址白名单和黑名单?”

他想了一下,“在beforeEach钩子里面配置”,我点点头示意他继续,“写一下吧”

router.beforeEach((to,from,next) =>{
    const passurl = [];
    const nopassurl = [];
    let ispass = true;
    if(passurl.indexOf(to.path) > -1){
        ispass = true;
    }
    if(nopassurl.indexOf(to.path) > -1){
        ispass = true;
    }
    if(ispass){
        next();
    }
})

还可以,马马虎虎,“那嵌套路由会不会用?”

“有用过。”

“给你个场景,菜单a下面有子菜单a1,子菜单a2,你写一下路由配置和菜单页面文件目录的搭建及 <router-view/>要写在哪里。”

 - a
    - layout.vue
    - a1
        -index.vue
    -a2
        -index.vue
//layout.vue
<template>
    <router-view/>
</template>
const routes:[
    {
        path:'/a',
        component:() =>import('page/a/layout'),
        redirect: '/a/a1',
        children:[
            {
                path:'/a/a1',
                component:() =>import('page/a/a1')
            },
            {
                path:'/a/a2',
                component:() =>import('page/a/a2')
            }
        ]
    }
]

嗯,基本配置都会,最后问个问题:“路由404怎么配置?”

这位兄台又愣住了,问我什么是:“什么是路由404?”

好吧,“就是输入一个地址,但是这个地址没有在路由中配置,找不到对应的页面,这时候怎么处理?”

他想了想,“不知道,没用过。” 这次我没说答案。

{
    path: '*',
    redirect: '/home'
}

*代表通配符,匹配任何路径,注意含有通配符的路由应该放在最后,不然任何地址都会跳到/home这个地址。

2、关于首页中头部菜单和侧边栏菜单的功能

通常系统菜单会有多级菜单,而菜单的渲染数据一般来自系统的菜单权限数据,其一般是个树结构数据,当然有些后端比较懒,直接返回数组结构的数据,你还要自己构造树结构数据,怎么构造可以戳这里。

这样会涉及到两方面,一是树结构数据的处理,涉及到递归函数,二是递归组件的使用。

我开始问:“递归函数使用过吧,写一个处理树结构数据构造出一个以节点id为key,value为节点中url值的json”

写了5分钟后,他放弃,直接说不会。

function getUrl(data,result={}){
    data.forEach(item =>{
        result[item.id] = item.url;
        if(item.children && Array.isArray(item.children) && item.children.length){
            getUrl(item.children,result)
        }
    })
}

我再问:“那写一个方法,获取树结构数据的第一项的第一子节点的url,如果该节点还有子节点继续往下获取,直到该节点没有子节点为止。”

写了10分钟,还是放弃。

function getUrl(data) {
    let item = data[0];
    if (item && Array.isArray(item.children) && item.children.length) {
        return getFirstUrl(item.children)
    } else {
        return item.url
    }
};

这也不会,我直接放弃问他递归函数方面的东西。

他强笑解释:“这个会,只是现在有点紧张?”

这跟紧张有毛关系,我安慰他一下,接着问:“递归组件有没有用过,怎么用的,有什么地方要注意的?”

他想了好一会儿,我看他手在抖,说到:“别紧张,慢慢想”

“递归组件应该是组件调用自身?”

“那怎么调用自身?”

他吱唔了半天,“我忘记了”

好吧,服了。

递归组件是通过组件的name选项来调用自身,使用时要注意确保递归调用是条件性的 (例如使用一个最终会得到 false 的 v-if),不然会陷入死循环。

问道这里我就可以判断出来,他根本就没做过头部菜单,侧边栏菜单这些功能。这些项目经历应该有部分是假的。

下午也没什么事情,就继续往下问,看他这些项目经历有多假。

3、关于配置封装axios的提问

“axios是什么,和ajax有什么区别。”我直接问道。

“axios是一个基于promise的HTTP库,ajax不支持promise,如果多个请求之间有先后关系的话,就会出现回调地狱”

“怎么安装axios,怎么引入axios?”

“执行命令npm instal axios --save安装,用import axios from 'axios'引入”

“那你是怎么封装axios,大概写一下”

import axios from 'axios'
const service = axios.create({
    timeout:3*60*1000,
    baseURL:'xxx',
})
export default service
import service from './axios'
export function xxx(data){
    return service.post('xxxx',data)
}
export function xxx(data){
    retruan service.get('xxxx',{params:data})
}

我看了一下,这么简单。问道:"还有吗?"

他看了我一下。想了想,然后摇摇头。

“你平时登录后,是怎么告诉后端已经登录了”

“有用token和cookie”

“那用token时候,你怎么配置的”

他想了许久,“放在请求头上。”

“怎么放写一下。”

import axios from 'axios'
const service = axios.create({
    timeout:3*60*1000,
    baseURL:'xxx',
    headers:{
        token:xxx
    }
})
export default service

“那登录接口不用在请求头上设置token,你怎么处理。”

等一会他摇头,那你“axios的拦截器用过吗?”

“有用过,request和response拦截器”

“讲一下这拦截器分别拦截了什么”

“request是在请求前拦截,response是在请求后拦截。”

“response是在请求后被then或catch之前拦截。”我补充了一下。“那用request拦截器实现给请求头加上token,写一下”

他写了几下,放下笔,说:“忘记怎么写了。”

好吧,看来他的项目经历是有多么水。我把答案告诉他。

service.interceptors.request.use(function(config){
    const token = getToken();//去获取token,如果有设置在请求头上。
    if (token) {
        config.headers.token = token;
    }
    return config;
})

他看了,指了interceptors这个单词,说:“我就是这个单词不会写,所以才写不出来。”

“噢,那你写一下,怎么对请求回来的数据预处理。”

service.interceptors.response.use(function(res){
    const data = res.data;
    if (res.status == 200) {
        return data
    }
})

难道真是那个单词忘记了,才不会写。我在问道:“那写一下怎么对请求出错进行拦截处理?”

service.interceptors.response.use(
    function(res){
        const data = res.data;
        if (res.status == 200) {
            return data
        }
    },
    function(err){
        this.$message({
            message: err.data.msg,
            type: 'error',
            showClose: true,
        })
        return err
    }
)

我看了他写的代码,已经很肯定他的项目经历是假的。“this指的是什么?”我问道。

“this是Vue实例。”

听到这句话,我已经不想继续问一下,客气跟他说:“回去等消息吧,2天内给你答复。”

关于axios配置的提问,真的只有这么多吗?当然不止,下面继续看我对另一位面试者的面试过程

这位面试者简历上写:熟悉axios,能进一步封装axios,比如请求前拦截处理请求头部,请求返回拦截处理返回数据,设置代理等。

关于之前的问题,我也一一对他进行提问了,答还不错,很快就到问他:“你是怎么对请求返回时做拦截处理的?写一下。” (ps:我公司面试时,都会要求上机面试)

const showErrorMessage = function(message){
    vm.$message.close();
    vm.$message({
        message: message,
        type: 'error',
        showClose: true,
    })
}
const response = function(res){
    if(res.status == 200){
        const data = res.data;
        const message = data.msg ? data.msg : '未知错误';
        if(data.code == 518 || data.code == 519){//登录失效
            if(vm.$route.query.redirect){
                vm.$router.push({
                    path:'/login',
                    query:{
                        redirect:vm.$route.query.redirect
                    }
                })
            }else{
                vm.$router.push({
                    path:'/login',
                    query:{
                        redirect:encodeURIComponent(vm.$route.fullPath)
                    }
                })
            }
        }else if(data.code && data.code != 200){
            showErrorMessage(message)
        }
        return data
    }
}
const response_err = function(){
    if(err.response){
        const response = err.response;
        const status = response.status;
        const message = response.data.msg ? response.data.msg : '未知错误';
        switch (status) {
            case 400:
                showErrorMessage('请求出错');
                break;
            case 403:
                showErrorMessage('拒绝访问');
                break;
            case 404:
                showErrorMessage('资源不存在');
                break;
            case 405:
                showErrorMessage('请求方法未允许');
                break;
            case 500:
                showErrorMessage('服务器内部出错');
                break;
            case 503:
                showErrorMessage('访问服务器失败');
                break;
            default:
                showErrorMessage(message);
        }
    }
    return Promise.reject(err);
}
service.interceptors.response.use(response,response_err);

我看了他的答案,拦截处理这方面应该都会。问问请求头配置方面。

“请求头除了配置token,之外还要配置那些东西,写一下?”

“还要配置Content-Type类型,告诉服务器我是用那种格式将参数传给你,这一般由后端接口定义需要那种类型。”

“那一般由哪几种类型?”

“在项目中常用的有三种类型。”

  • application/x-www-form-urlencoded表单默认提交的格式,需要用new URLSearchParams处理数据后传给后端。
  • application/jsonJSON数据格式,需要用new Object处理数据后传给后端。
  • multipart/form-data需要在表单中进行文件上传时的格式,需要用new FormData处理数据后传给后端。

“那么在配置Content-Type类型时,要求后端应该怎么配合”

他想了一会儿,说:“好像不要吧。”

我继续问道,“如果在头部配置token,后端需要做什么。”

“这个不太清楚,不知道。”,我就纳闷,如果是自己配置的,应该会遇到这些问题,结果会导致跨域。

我继续问道:“知道什么是CORS跨域吗?”

“这个不太清楚。”

“那你平时遇到跨域怎么处理的?”

“都是后端帮忙处理的。”

这后端真体贴。后端的Access-Control-Allow-Headers要配置"content-type,token"

“那么如果后端要通过Cookie来验证,应该怎么配置。”

axios.defaults.withCredentials=true;

其实我挺想问他对应的后端要怎么配置,如果有实际配置过,有很大概率会遇到问题的。后端要配置两点

  • Access-Control-Allow-Credentials配置为true,表明服务器同意接受Cookie。
  • Access-Control-Allow-Origin不能设置为*,必须指定明确的、与请求网页一致的域名。如Access-Control-Allow-Origin: "localhost:8037"

“讲一下baseURL的作用和合并规则?”

“baseURL是请求地址的基础路径,将自动加在url前面,除非url是一个绝对 URL,就是有带http://或者https://的。”

“如果后端返回二进制流类型的数据,你要怎么配置,才能接收。写一下”

service.post('xxx',data,{responseType:'blob'});
service.get('xxx',{params:data, responseType:'blob'})

“如果遇到后端返回的id是Number类型,出现精度失真的问题,怎么解决?”

“精度失真,这个后端应该会处理好吧,没遇到过?”

好吧?其实这个问题在项目功能模块越来越多之后,肯定会遇到过。不过这位面试者在这axios这方面比之前那位面试者要好点。

这个问题考核了很多个知识点。

  • 什么是精度失真?js的Number类型有个最大值(安全值)。即2的53次方,为9007199254740992。如果超过这个值,那么js会出现不精确的问题。这个值为16位。
  • 怎么产生精度失真的? 其实后端返回给前端的值是一个JSON字符串。axios会默认用JSON.parse将器转为json对象,当JSON.parse解析超过16位的数值就会产生精度失真。
  • 那怎么解决呢,就要利用到axios这个transformResponse配置选项,这个选项的作用是在传递给 then/catch 前,允许修改响应数据。我们可以先利用正则和replace()来将返回数据中超过16位的数值全部替换成字符串,然后再用JSON.parse()转成JSON对象。
transformResponse: function (data) {
    data = data.replace(/"\w+":\s*\d{16,}/g, function (longVal) {
        let split = longVal.split(":");
        return split[0] + ':' + '"' + split[1].trim() + '"';
    });
    return JSON.parse(data)
}

4、关于项目业务方面的提问

项目业务这方面内容很泛,我一般会按下面三种情况提问。

  • 如果简历中的项目经历跟公司业务是契合的,我就会直接以项目经历中的业务为场景来考核
  • 如果简历中的项目经历跟公司业务是不是很契合的,我会先问你在这些项目中遇到过什么困难,后面是怎么解决这些困难,然后根据你的回答,来找到突破点来提问考核,看你是不是符合公司的需求。
  • 如果简历中的项目经历描述的很简单,不能从里面获取有效信息,我就会根据公司的业务模拟几个场景来考核。

我就第三种情况来举个例子。面试者自述已经独立用Vue全家桶做完了几个复杂的后台系统类的项目。

既然是复杂的后台系统项目,那肯定有有复杂表单。那么就考核一下他对数据来驱动页面展示的能力是咋样的。

基础套餐类型由套餐组合类型决定的。套餐组合类型为常规时,基础套餐类型为基础包。套餐组合类型为一次性时,基础套餐类型为可选包。套餐类型由续费包、加油包、可选包、附加包。

套餐名称的下拉选项是由运营商和套餐类型决定的。套餐类型和套餐名称可以添加多个。

后端接收的参数格式为

data:{
    type:'',//套餐组合类型
    operators:'',//运营商
    packagelist:[
        {
            packageType:'',//套餐类型
            packageName:'',//套餐名称
            isMain:'',//是否是基础套餐,1是,0不是.
        }
    ]
    
}

写一下这个怎么实现。这个业务场景是在表单中非常场景的,如果真是做过多个后台系统项目,这个应该非常简单。答案我就不写了,你们可以自己想一下怎么实现。

三、技能方面

我个人认为技能的熟练度可以分为4个等级。

  • 了解:听过这个技能,知道这个技术的概念,是用来实现哪些功能。
  • 熟悉:可以凭借技能文档来使用它。
  • 熟练:可以脱离文档开发、可以进一步封装它。
  • 精通:知道这个技能的原理,可以在去改造和优化它。

这个四个等级,很多文章都会介绍到,没什么好说的。

我这样要强调的是,

  • 没有在大量得在项目中使用这个技能,不要去用熟练来描述你对这个技能的熟练度。
  • 谨慎使用精通来描述你对这个技能的熟练度,一旦用,一定会问你,为什么这么使用(原理),而不是只问你怎么使用。

四、写在最后

虽然说简历是一块敲门砖,简历写的厉害一点,这块敲门砖就会重一点,就会砸开更多公司的大门。但是你有没有想过,如果这块敲门砖过重,你在面试过程中能举多久呢?到最后还是砸了自己的脚。

个人认为,简历除了是一块敲门砖,还是一份你和面试官之间交锋的计划书,在简历上面每一句话,都要对其负责,想明白自己能不能驾驭这个句话。驾驭不了,相当你自暴缺点给面试官看,得不偿失。

个人是不排斥假简历,只要有心去作假,对每个假项目经历、假技能都了如指掌,假得都会变成真的。

当然工作经历最好还是不要造假,因为在医社保系统的缴费记录可以看到你的工作经历。