前端面试,那些我回答不上来的问题

877 阅读17分钟

前两天电话面试了一个公司的前端,差不多问题都能回答出一点,但是一旦向下深挖,就不会了,还是自身基础打得不够啊,怕以后面试还是会遇到这些个问题,所以就觉得把面试官问我的,我回答不上来的,且现在还记得问题记录一下,方便以后巩固复习,顺便分享给面试的小伙伴

1. 深度拷贝和浅度拷贝了解过吗,是否可以说一下

当时回答:了解过,浅度拷贝的话,拷贝的如果是基本数据类型的话,他就会原样拷贝,如果拷贝的时引用数据类型,比如说对象,数组,函数这类的,他只能拷贝他的应用地址,所以拷贝完成,修改引用数据类型里面的数据时,原来的也会发生修改(当时有点紧张所以说的不是很清楚,再解释一下,a对象里面又有基本数据类型和引用数据类型,拷贝a得到b,修改b里面的引用数据类型的数据时,a也会发生变化),深度拷贝的话,他会拷贝的更加彻底,当他拷贝的时引用数据类型的数据,他会递归向下继续拷贝,直到拷贝的数据都是基本数据类型。 面试官继续问:你刚才提到了递归,那你能说一下,深度拷贝的递归出口是什么? 问道这个问题时,心里暗喜,我可是敲过深度拷贝的代码的,但是还是因为紧张,想不起来了,哎,果然面试总归会紧张,想了一会后终于想起来了,判断这个数据是不是引用数据类型,如果不是,结束递归,✌✌✌,面试官继续问:当深度拷贝中出现两个对象相互引用,该如何处理? 你懂的,我傻了,相互引用????????我咋从来没有想过这个问题呢,想了一会,告诉面试官,我们是不是可以判断这两个数据的地址是不是相等的是不是就好了,面试官:可是我不知道,哪两个对象相互引用,你怎么去判断地址是否相等呢 ?,完了完了,我已经彻底傻了...

面试完查的资料:可以使用一个WeakMap结构存储已经被拷贝的对象,每一次进行拷贝的时候就先向WeakMap查询该对象是否已经被拷贝,如果已经被拷贝则取出该对象并返回。

//判断是数组还是对象
function getType(target){
    // console.log(Object.prototype.toString.call(target).slice(8,-1))
    return Object.prototype.toString.call(target).slice(8,-1)
}

function cloneUtil(target,hasObj = new WeakMap()){
    let result;

    // 判断数据类型
    if(getType(target) === 'Array'){
        result = []
    } else if(getType(target) === 'Object'){
        result = {}
    } else {
        return result
    }

    for (let i in target) {
        let item = target[i]
        
        //当遍历的数据中仍然有数组/对象时,重新按照之前的步骤 进行拷贝,直到所有拷贝数据的类型都是基本数据类型
        if(getType(item) === 'Object' || getType(item) === 'Array'){
            //判断hasObj中是否已经拷贝过该对象,如果已经被拷贝则取出该对象并返回。
            if(hasObj.has(item)) return hasObj.get(item)
            hasObj.set(item,{})
            let cloneitem = cloneUtil(item,hasObj)
            result[i] = cloneitem
        } else{
            result[i] = item
        }
    }

    return result
}

let obj = {
    name:'Bob',
    age:22,
    sex:{
        option1:'男',
        option2:'女'
    }
}

let cloneObj = cloneUtil(obj)
cloneObj.sex.option1 = 'xxx'
obj.obj = obj
console.log(obj)
console.log(cloneObj)

2. vue项目中你用了ui框架,比方说element-ui,你要修改ui框架提供的组件样式的时候,你会怎么去操作

当时回答:vue的style标签中,会添加一个scoped属性,防止样式的污染,但是你要修改element-ui组件的话,再带有scoped属性的style标签中去修改,是不会达到想要的结果,vue组件是可以有多个style标签的,所以当你要修改element-ui组件的样式时,可以添加一个没有scoped的style,就可以修改组件样式。

面试官继续追问:那我就是想要在有scoped属性的style标签里面去修改,那要怎么处理?

面试官这个问的时候,我顿时emmm傻了,仔细想来了想,记得如果项目用来stylus,less这样的预处理器,可以用样式穿透来实现,我就这么回答了,但是本人没有实践过,所以还是有点慌慌的,面试完之后就去查了资料。资料显示,我的回答是对的,举个例子把,看一下到底怎么穿透。

<style lang="stylus" scoped>
.a >>> .b { 
    /* ... */ 
}
</style>

//或者

<style lang="scss" scoped>
.homePage /deep/ .el-main {
    padding: 0;
}

3. call,apply了解过吗,能自己实现一个call,apply吗,还有其他什么立即绑定的函数吗

当时回答:了解过,call,apply都是更改this指向的,然后其他的我都没回答出来,后来面试完之后想想其他立即绑定还有bind啊,哎,傻了傻了

补完知识:

let person ={
    name: 'bob',
    age: 18
}

function student (){
    console.log(this.name,this.age,sex,like)
}
student.call(person,'nan','eat')

//call,apply的原理相当于,把上面的两个合并成立这个下面一个

let person = {
    name: 'bob',
    age: 18,
    fn:function student (){
        console.log(this.name,this.age,sex,like)
    }
}

person.fn('nan','like')
Function.prototype.myCall = function (context = window){//如果传入context没有赋值,就赋默认值window
    if(Object.prototype.toString.call(context) !== '[object Object]'){
        console.log('不是对象')
        return
    }
    let fn = Symbol('fn')//产生一个唯一不重复的值,以免冲突
    context[fn] = this//这里的this,是调用了myCall的函数,比方说a.mayCall(),那a就是这里的this
    let args = [...arguments].slice(1)//这里是传值
    let result = context[fn](...args)//运行这个函数
    delete context[fn] //删除context上的方法
    return result
}

Function.prototype.myApply = function (context = window){
    if(Object.prototype.toString.call(context) !== '[object Object]'){
        console.log('不是对象')
        return
    }
    let fn = Symbol('fn')
    context[fn] = this
    let args = arguments[1]
    let result = context[fn](...args)
    delete context[fn]
    return result
}

let person = {
    name: 'bob',
    age: 18
}

function student(sex,like){
    console.log(this.name,this.age,sex,like)
}

student.myCall(person,'nan','eat')
student.myApply(person,['nan','eat'])

4.因为之前问了事件循环机制是怎么样子操作的,当时回答的时候,说了js单线程的,然后就有了这个问题,js如果不是单线程会怎么样?后面再回答es6里面除了let,const新增的,es6里面还有什么你记得的内容吗,当时中间有回答了async 和 promise,所以,js单线程,又出了一道问题,promise在js单线程是怎么运行过程(突然描述不清楚了,大概就是这个意思)?

当时回答:js如果不是单线程的话,对dom会有影响,但是具体什么影响我记不清楚了,然后面试官就把答案告诉我了:如果js被设计了多线程,多个线程都要操作一个dom元素,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,这时候就会产生问题。

下面的那个promise 在js上运行过程我又没说出来,哎,面试完之后躺在床上好好的想了下,promise里面不就是装异步函数的么,啥ajax请求不是都是放里面的么,那不就是js运行到这边的时候,会交给对应的管理模块管理,满足条件之后,进入回调队列,然后,js主线程上的同步代码执行完成之后,通过事件轮询机制,看回调队列里是否又东西,有的话就把他钩到主线程上去运行,嗯,当时人傻已经实锤了,换种问法,问事件循环机制就不知道怎么说了,所以啊,小伙伴们,记得面试的时候要冷静,冷静,冷静,虽然我面试的时候也这么跟我自己说的🙃🙃🙃 这两天在家好好的看了一下关于promise的内容,针对于这个问题,做出相应的补充 我们知道js运行代码时,都是从上到下执行,当遇到异步任务的操作就会交给相应的处理异步任务的模块,然后,异步任务符合结果才会进入回调队列,但是回调队列又分为宏队列和微队列(这是我之前不知道的),像ajax,定时器等满足要求会进入宏队列,但是像promise满足要求后是会进入到微队列当中,当要执行宏队列中的异步任务时,他会先询问微队列中的异步任务是否清空,如果清空,他就会执行宏队列中的异步任务,举个例子把,比如说宏队列中有1,2,3三个异步任务,微队列中此时只有4,5两个异步任务,当执行1时,会先把4,5两个执行掉,再执行1,执行1的过程中,又有6,7进入微队列,那么等1执行完想执行2时,他又会去问微队列中是否还有要执行的异步任务,此时6,7再微队列中,所以他会先把6,7执行掉,然后在执行2,2执行完之后,他又会继续去问微队列是否有任务,此时微队列中没有新的任务,所以他回去执行宏队列中的3

5. 隐式类型转换,2 == {a:2} 会出现什么结果,判断过程是怎么样的

当时回答:emmmmmmmmm了半天,还是没回答出来🤦🤦🤦 补完知识的答案:左边是数字右边是对象进行比较,会将不是数字的一方转化为数字然后进行比较,但是像对象,数组这样引用类型的数据的转换比较复杂,他会先将对象通过valueOf获取原始值,然后在对原始值进行toString转化成字符串,然后对字符串转化为Number,然后在与数组进行比较,所以{a:2}通过valueof之后就会变成[object Object],在经过toString的处理就变成"[object Object]",在Number("[object Object]") 就会变成NaN,就是不是数字的数字,最后就变成 2==NaN 的比较了,显然是false

blog.csdn.net/itcast_cn/a… 这篇博客写隐式类型转换的坑,写的很棒,大家感兴趣的话,可以去了解一下

6. 图片懒加载原理

这个之前没有去了解过,所以当时没有回答出来,面试完后补了知识之后的答案:将img标签中的src指向一个loading图片地址,然后定义一个data-src属性(他有个data-属性 - 后面可以随便取名字,我这取的是data-src)指向真实的图片地址,根据页面的可视高度和滑轮滚动的高度与图片的高度进行比较,如果图片快要进入可视区域,就将src中的地址换成data-src中的地址,从而达到图片懒加载的效果

//窗口可视大小兼容性方法
function getViewportSize(){
    if(window.innerHeight){
        return {
            height:window.innerHeight,
            width:window.innerWidth
        }
    }else{
        if(document.compatMode === 'BackCompat'){
            return {
                height: document.documentElement.clientHeight,
                width: document.documentElement.clientLeft,
            }
        }else{
            return {
                height: document.body.clientHeight,
                width: document.body.clientLeft
            }
        }
    }
}
//滚动大小兼容性方法
function getScrollOffset(){
    if(window.pageXOffset){
        return {
            top: window.pageYOffset,
            left: window.pageXOffset
        }
    }else{
        return {
            top: document.body.scrollTop + document.documentElement.scrollTop,
            left: document.body.scrollLeft + document.documentElement.scrollLeft
        }
    }
}

//获取当前所有的img
let imgs = document.getElementsByTagName('img')
let len = imgs.length
//获取窗口高度
let windowHeight = getViewportSize().height 

//滚动事件
function lazyload(){
    //获取滚动高度
    let scrollHeight = getScrollOffset().top
    for(let i = 0; i<len; i++){
        //判断照片是不是快要显示
        if(imgs[i].offsetTop < windowHeight + scrollHeight){
            //判断是不是默认图片
            if(imgs[i].getAttribute('src') == "./img/1.jpg"){
                //将真实图片地址替代默认图片地址
                imgs[i].setAttribute('src',imgs[1].getAttribute('data-src'))
            }
        }
    }
}
//页面刚打开的时候,没有滚动,所以不会执行lazyload方法,所以要提前执行一次
lazyload()

//绑定滚动事件
window.onscroll = lazyload

7. 用const定义常量是否可以修改

当时回答:应该不可以吧(虚虚的),面试完补了知识之后的答案:const定义的是基本类型修改会报错,但是定义的是引用数据类型就不会报错,比如说对象,数组之类的,原因是因为,基本数据类型他都是存在栈中,const不能修改栈上的内容,但是引用数据类型,栈上只是保存了指向对象或者数组的指针,其他的属性啊或者是数组里面的数据都是放在堆上的,所以const可以修改堆上的内容,也就是const是可以修改对象的属性、数组的内容,记得const定义的常量是不可以重复定义的哦

8. 可以描述一下浏览器渲染网页吗

当时回答:浏览器渲染html,会先将html渲染成dom树,css被渲染成css树,dom树和css树合并到一起之后就会变成一个render树,之后就会计算dom的大小和位置称之为回流,之后会将计算后的dom位置大小样式渲染到屏幕上,这称之为重绘。 **面试官追问:怎么样会导致回流与重绘?**emmm之前看过,所以就只有简单的回答出来改变样式,dom节点会导致,但是时间有点久远详细的就说不出来了,查完资料之后的回答:回流一定会造成重绘,但是重绘不一定回流。页面的首次渲染,浏览器窗口大小的改变,dom元素的变化,就是操作会导致文档流发生改变的,比如说新增加dom元素,删除dom元素,或者某个dom元素的大小发生改变,文本节点发生改变,字体大小发生改变等等都会导致回流。当你修改的样式不会对文档流发生改变,就不会发生回流,比如说,颜色改变,visibility,圆角,阴影什么的都是不会造成回流只会重绘。

www.imooc.com/article/459…这个对于回流重绘写的挺好的,大家感兴趣的化可以看一下

9.NaN知道是什么吗,是否可以说一下Number.isNaN和isNaN的区别

当时回答:NaN是判断是不是个数字,NaN === NaN返回false 资料补充:NaN是判断是不是数字,但是因为NaN不等于NaN,所以需要有一个判断这个东西是不是NaN,所以出现了一个isNaN()来判断,isNaN(NaN)返回为true,但是isNaN()会将字符串,或者数组,对象都会都认为是不是数字,所以返回true,Number.isNaN()是用来判断是不是真的为NaN的,其他的字符串,数组,数字都会返回为false只有Number.isNaN(NaN)才会返回true

10.base64与md5的区别以及应用场景

当时回答:不知道具体区别,应用场景:base64可以用于将图片转化为base64字符串进行上传,md5主要用于登录,对密码按照一定的规则进行加密 资料补充:base64主要是通过encode进行加密,所以他是可以用decode解密,一般用于当图片过大时,可以采用base64加密,从而压缩图片的大小,进行上传,md5是一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致,一旦加密后不能解开,所以一般用于密码,或者向服务器进行通讯时,双方可以按照相同的规则,对一个东西进行加密,对比两者是否一致,从而防止内容篡改

11.什么是闭包以及他的应用场景

js中内部函数通过作用域链可以获取外部函数的变量,例如:

function f1() {
    var a  = 1
    console.log('f1:',a)
}
f1()
console.log(a) //报错,因为a是f1的私有变量,外部不能获取

但是外部函数想访问内部函数的变量,是会报错的,所以闭包相当于是外部函数和内部函数的一个桥梁

function f1() {
    var a = 1

    return function (){
        console.log(a) //f2通过作用域链可以访问f1中的变量
    }

}
var b = f1()
b() //外部访问到了变量a

形成闭包的条件:1.函数嵌套 2.内部函数使用外部函数的局部变量 3.执行外部函数 主要作用:延长外部局部变量的生命周期,使得从外部能访问到函数内部的变量

应用场景:

  1. 返回值:外部需要访问函数内部的变量
  2. 函数参数
function f2(num){
    console.log(num)
}
function f1(){
    var a = 1
    function f3(){
        return a
    }
    var b = f3()
    f2(b)
}

f1()
  1. 函数赋值
var num;
function f1(){
    var a = 1
    function f2(){
        return a
    }

    num = f2()
}
f1()
console.log(num)
  1. 立即执行函数(IIFE)
  2. 循环赋值
function f(){
    var arr = []
    for(var i = 0; i < 5; i++){
        (function(i){
            arr[i] = function(){
                return i
            }
        })(i);
    }
    return arr
}
var a = f()
console.log(a[1]())
  1. 对私有变量进行修改
var getName, setName;

function fn(){
    var name = 'aaa'
    setName = function(newName){
        name = newName
    }

    getName = function (){
        return name
    }
}
fn()
setName('bbb')
console.log(getName())
  1. 计数器
//计数器
var add = function (){
    var num = 0 
    return function (){
        return num++
    }
}()
console.log(add())
console.log(add())

12.webpack的原理

webpack的配置文件我自己写过,但是让我去说他的原理还真是说不太出来,所以我这边找了一个写的挺好的文章,可供大家阅读 www.cnblogs.com/chengxs/p/1…

13. scoped的原理

Vue中的scoped属性的效果主要通过PostCSS转译实现,转译前的Vue代码:

<div class="box">
    hello world!!!
</div>
<style scoped>
.box{
    height: 100px;
    width: 100px;
    background-color: rgb(35, 85, 85);
}
</style>

转译后的代码:

<div data-v-431182d6 class="box">
    hello world!!!
</div>
.box[data-v-431182d6] {
    height: 100px;
    width: 100px;
    background-color: rgb(35, 85, 85);
}

说明:从上面的代码,我们不难看出postcss给所有的都没添加了一个随机且不重复的data-属性,css选择器添加了一个属性选择器来选择这个dom,从而达到每个dom节点的样式私有化不会相互污染

14.数组扁平化(除了用flat方法)

方法1

let arr = [1,[2,3,[4,5,[6],7,8],9],0]
let newArr = []

//可以将多层数组嵌套数组的数组扁平化
function flatOne(array){
    array.map(item => {
        //判断遍历的每一项是否是数组,是的话递归,不是的话就把数组push进入newArr
        Array.isArray(item) ? flatOne(item) : newArr.push(item)
    })
    return newArr
}

console.log(flatOne(arr))

方法2

function flatTwo(array){
    return [].concat(
        ...array.map(item => {
            return (Array.isArray(item) ? flatTwo(item) : item)
        })
    )
}
console.log(flatTwo(arr))

方法三

function flatThree(array){
    let newArr = []
    //将数组转化成字符串
    //于数组对象,toString 方法连接数组并返回一个字符串,其中包含用逗号分隔的每个数组元素
    array.toString().split(',').forEach(element => {
        //将字符串形式的数字转化为数字类型的数字
        newArr.push(parseInt(element))
    });
    return newArr
}
console.log(flatThree(arr))

15.restful标准特点

资源、统一接口、URI和无状态 详细的介绍可以看这里www.jianshu.com/p/65ab865a5…

16.用es6标准实现将999999999转变成999,999,999

我也没理解什么叫es6标准转化成9,999,999,999所以我就按照我自己的想法来写了这题

思路:把数字变成字符串,然后每三个加一个逗号

let a = 9999999999,
    b = '';
a = a.toString()
//9,999,999,999 算出第一个分号前有几个数组
let flag = a.length % 3
//a是伪数组所以不能通过forEach遍历
//需要通过slice传一个数组/集合,回这个集合的原理,拿到的也是数组,这样就可以使用数组的所有方法了
Array.prototype.slice.call(a).forEach((element,index) => {
    b = b + element
    //(a.length-1) != index 如果数字的位数凑巧会导致数字最后也加上一个逗号,所以我们要把这种情况考虑到
    if((index+1-flag) % 3 == 0 && (a.length-1) != index){
        b = `${b},`
    }
});

console.log(b)

17. 数据结构,链表出现环要怎么处理

还有17这个问题先放在这怕忘记,知识点还没补上,所以暂时就不写答案了,或者大家有什么好的方法也可以告诉我,理解之后会写上去的哦

所有的面试都已经结束了,大大小小加上电话面试下来也有十几家公司,其中有自己喜欢的公司,自己很幸运也被喜欢的公司发了offer,上面的一些问题都是一些面试中我回答不出来的一些问题,还有些记不住了,就没有写上去,到时候想起来的话,还会写上去的,上面如果写的有问题的,欢迎大家来指正✌✌✌🎉🎉🎉🤞🤞🤞🌹🌹🌹