前端面试总结

187 阅读5分钟

1. 防抖和节流

防抖则延迟,节流则无视。

防抖

防抖(debounce),频繁触发某事件则为抖动,防抖则字面理解,防止频繁触发某事件。即创建一个函数,将要执行的函数(callback函数)延迟到某间隔时间之后

假设你的输入框有模糊查询功能,每输入一个字符就会向服务器发送请求,频繁快速的请求再加之回调可能有复杂的逻辑处理,则会给浏览器和服务器则会造成巨大负担。

面对这样的问题,显然,我们不能去掉对用户输入事件的监听。然我们可以通过防抖函数,来调节方法触发的时间。

手写防抖函数:

先分析想如何调用这个防抖函数?

 const onSearch = debounce((param)=>{
     // 函数体,输入事件的监听,输入字符的后续操作
     console.log(`执行防抖...,param为${param}`)
 }, 2000)
  1. 首先,这个函数必要两个参数,一个回调函数,另一个为时间间隔。

    function debounce(cb, timeout = 0){
    
    }
    
  2. 这时候,需要让回调函数(cb)每隔timeout触发一次

    function debounce(cb, timeout = 0){
        setTimeout(() => {
            cb()
        }, timeout)
    }
    

    但这样写由于函数无返回值,我们的onSearch拿到的是 undefined ,后续再怎么输入字符,都不会执行后续操作。那就需要,防抖函数有个返回值且是个函数,可以让onSearch在输入字符之后执行。

    function debounce(cb, timeout = 0){
        return ()=>{
            setTimeout(() => {
                cb()
            }, timeout)
        }
    }
    
  3. 代码写到这个时候,键盘快速输入几个字符,还是会执行多次。防抖函数内部需要把 timeout 清除

    function debounce(cb, timeout = 0){
        let timer = null
        return ()=>{
            clearTimeout(timer)
            timer = setTimeout(() => {
                cb()
            }, timeout)
        }
    }
    

    let timer = null 能否被写进 return 的函数体里呢?答案为不能。我们知道,通过return 返回的timer变量没有被销毁, timeout 时间内函数执行则会被 clearTimeout,以达到 timeout 时间内多次执行,只触发一次目的。若将 timer 变量定义在 return 函数内部,则一执行回调就会重新创建变量timer,还是会执行多次。

  4. 这时候,还有一个很大的不足:不能传参数。 onSearch执行的时候,不管参数传几,打印出来都是undefined,接下来兼容传参

    首先,debounce方法内部return的方法要接收参数,再把参数传给回调

    function debounce(cb, timeout = 0){
        let timer = null
        return (arg)=>{
            clearTimeout(timer)
            timer = setTimeout(() => {
                cb(arg)
            }, timeout)
        }
    }
    

    假设 cb 回调有多个参数,则用扩展运算符接收全部参数,再将全部参数给到 cb 回调

    function debounce(cb, timeout = 0){
        let timer = null
        return (...arg)=>{
            clearTimeout(timer)
            timer = setTimeout(() => {
                cb(...arg)
            }, timeout)
        }
    }
    

节流

节流(throttle),字面理解, 每隔一定时间触发一次,节约资源。在一定时间内, 无视 后续的触发。

试想,假设有个提交表单的功能,用户短时间内重复点击提交按钮,则会重复调用提交接口,这时给服务器带来压力不说,可能也会触发接口的错误提示一类。那解决这个问题,可以用到节流函数,提交点击完,再一定时间内再次点击提交则 无视 不做处理。

手写节流函数:

节流函数对调用和防抖函数是一样的?

 const onSearch = throttle((param)=>{
     // 函数体,输入事件的监听,输入字符的后续操作
     console.log(`执行节流...,param为${param}`)
 }, 2000)
  1. 和防抖函数一样,节流函数也是需要两个参数,且需要返回函数。

    function throttle(cb, timeout = 0){
        return ()=>{
            cb()
        }
    }
    
  2. 内部需要一个变量控制 timeout 时间内,是否可执行cb回调。处理参数与防抖函数一致

    function throttle(cb, timeout = 0){
        let flag = true
        return (...arg)=>{
            if(flag){
                flag = false
                cb(...arg)
                setTimeout(() => {
                    flag = true
                }, timeout)
            }
        }
    }
    

当然防抖/节流函数还有很多优化空间,比如内部对参数 cb 进行判断处理,timeout进行类型判断或类型转换等。

2. 从输入url到页面渲染发生了什么

  1. DNS解析,将域名地址解析为IP地址

    在浏览器中输入www.baidu.com, 操作系统会先检查本地hosts文件是否有该域名与IP的映射,如果没有,则查找本地DNS解析器缓存

    若还没查到,则去本地DNS服务器查询,若查询的域名包含在本地配置区域资源中,则完成域名解析。若不由本地DNS服务器区域解析,但该服务器已缓存了网址映射关系,则直接用这个映射。

    若以上都没拿到IP地址,则由DNS服务器查询,若DNS服务器的设置为转发模式,则向上一级DNS服务器转发,若上一级没能解析,则转发至上上一级,以此循环;若DNS服务器的设置未用转发模式,则本地DNS服务器则把请求发至根DNS,根DNS回根据.com是谁来授权管理,并返回一个负责该顶级域名服务器的IP。本地DNS服务器收到这个IP后,会联系这台服务器,若这台服务器不能解析则找一个管理.com域的下一台DNS服务器地址(baidu.com)给本地服务器,本地服务器拿到这个IP重复上面动作,直至找到。

    客户端和本地DNS服务器之间查询是递归查询,DNS服务器之间的交互查询是迭代查询

  2. TCP链接,TCP三次握手

    (1)第一次握手,由浏览器发起,告知服务器要发送请求
    (2)第二次握手,由服务器发起,告知浏览器准备接收了
    (3)第三次握手,由浏览器发送,告知服务器,我马上就发送请求了,准备接收(确认要发送请求,避免浏览器不发送请求情况下,服务器干等着的情况)

  3. 发送请求报文

  4. 接收响应

  5. 浏览器渲染页面

    遇见HTML标记,则调用HTML解析器解析成Token并构建dom树
    遇见style/link标记,则调用css解析器,构建cssom树
    遇见script标记,则调用javascript解析器处理script代码
    将dom树和cssom树合并成一个渲染树。
    根据渲染树计算布局
    渲染

  6. 断开链接,TCP四次挥手

    (1)第一次挥手,由浏览器发起,告知服务器,请求报文发送完了
    (2)第二次挥手,由服务器发起,告知浏览器,请求报文接收完毕
    (3)第三次挥手,由服务器发起,告知浏览器,响应报文发送完了
    (4)第四次挥手,由浏览器发起,告知服务器,响应报文接收完毕

3. 原型链

每个函数都有一个 prototype 属性,默认指向一个object空对象,也就是原型对象

假设如下代码:

function Fn(){
  this.a = 1
}
Fn.prototype.test = function(){
  console.log('test')
}
var fn = new Fn()
console.log(fn.tostring())

代码里创建了构造函数 Fn,函数内部添加了属性a,并在Fn的 prototype 上添加了方法 test。创建了Fn的实例对象fn,这时候a就是fn的自有属性。

当通过fn查找属性 a 的时候,先在自身找,找到了属性a。
当通过fn查找属性 test 的时候,先在自身找,没有找到,这时候通过 __proto__ 找test。
当通过fn查找属性 tostring 的时候,先在自身找,没有找到,这时候通过 __proto__ 找,还没有找到则通过 __proto__ 再往上找,直至一个对象的原型对象为 null 找到该属性

每个实例对象(object)都有一个私有属性(称之为 __ proto__ )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__ proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

4. Promise

Promise 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。

实例对象中有一个属性[[PromiseState]] 来代表 Promise的状态。 它有三个状态:pending(进行中),fulfilled(成功),rejected(失败)。而这些状态只能通过异步操作的结果来决定,其他手段无法改变。

Promise是一个构造函数。实例化参数为函数类型,该函数接收两个函数类型的参数,通常命名为resolverejectresolve 函数在成功时调用,reject在失败时调用。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。then方法可以链式调用

const promise = new Promise((resolve, reject)=>{
  if(Math.random() * 10 > 5){
    resolve()
  }else {
    reject()
  }
})
promise.then((value)=>{
  // 这里为成功的回调
},(reason)=>{
  //这里为失败的回调
}).then((value)=>{
  // 这里为成功的回调,链式调用
})

5. 用reduce实现map

Array.prototype.mymap = function(cb,initval){
    return this.reduce((acc,cur,index,ary)=>{
        acc[index] = cb.call(initval,cur,index,ary)
        return acc
    },[])
}