面试学习记录

619 阅读16分钟

写在最前

本篇博客仅为个人复习记录,其中部分代码及内容为阅读他人博客后所理解

防抖和节流

防抖就是让他无论触发多少次,都要以最后一次的一定时间后再触发,节流就是一定时间内只能执行一次

防抖实现

function dedounce(fn, time = 500) {
    var fnSet
    return function () {
        const self = this
        const args = [...arguments]
        clearTimeout(fnSet)
        fnSet = setTimeout(() => {
            fn.apply(self, this)
        }, time)
    }
}

节流实现

function throttle(fn, time = 500) {
    let flag = false
    return function () {
        const self = this
        const args = [...arguments]
        if (!flag) {
            flag = true
            fn.apply(self, args)
            setTimeout(() => {
                flag = false
            }, time)
        }
    }
}

柯里化

柯里化就是允许提前返回参数还没传完的函数并在参数传完时执行函数 柯里化好处: 参数复用,提前确定,延迟执行

const curry = (fn, ...args) => args.length < fn.length ? 
(...arguments) => curry(fn, ...args, ...arguments) : fn(...args)

前端模块化

前端模块化主要有amd和cmd,amd是在require.js的使用中形成的模块规范,主要是依赖前置,一开始就要在头部写好,编译也是在一开始就引进模块;cmd是在sea.js的使用中形成的规范,只要是依赖就近,要用的时候再书写引进,编译时也会在使用到的时候再引进模块;这两个都是非同步加载方案,运行异步加载;common.js主要是后端的模块方案,是同步加载方案;
es6的import export 在script上表示是相当于加了type='module',表现上是和defer一样的,内部是默认开启了严格模式的 它是编译时加载,(引入哪些就只引入哪些,运行时加载指的是整体引入后再使用那些引用的) 通过export 和 import 语法使用,它的变量是在模块内部活动的,外部调用只是引入一个符号链接只读的

new的实现

function newFunc(fn, ...args){
    // 新建一个对象
    let obj = new Object()
    // 将对象的原型链指向fn构造函数的原型,这是原型链的知识
    obj._proto_ = fn.prototype
    // 执行函数,利用apply将this指向为obj
    const result = fn.apply(obj, args)
    // 返回结果,判定方式是因为真实的new过程如果返回了对象也是返回该对象
    return typeof result === 'object' ? result : obj
}

关于上下文的问题

var foo = 1;
console.log(foo);
function foo(){
    console.log("foo");
}; // 打印出1

console.log(foo);
function foo(){
    console.log("foo");
}
var foo = 1; // 打印函数

为什么? 因为进入执行上下文时,分为两个阶段,创建阶段和执行阶段。 创建阶段是创建变量对象,建立作用域链,确打this的指向,执行阶段是变量赋值,函数引用,执行其他代码。 第一个他们的创建阶段扫描完成后vo为一个foo指向函数,因为创建变量对象时先声明函数再声明变量(var 中直接声明为undefined,let 和const 中声明为 未初始化 ),如果声明的变量名称和执行参数或者函数声明一致,则变量声明不会干扰类似的声明。

进入执行过程后,第一个,执行一遍,foo = 1,变量赋值,然后再执行console.log所以打印1,第二个进入后直接打印,所以打印的是foo

call的实现

Function.prototype.call = function() {
    // 利用解构赋值取出this指向对象和参数
    const [obj, args] = [...arguments]
    if(!obj) {
        // 这个指的是当没有传入对象时应该在没有window时指向undefined ,有window时指向 window
        obj = window === undefined ? undefined : window
    }
    // 利用函数被谁调用this救指向谁来实现
    obj.func = this
    // 指向函数返回结果
    const result = obj.func(...args)
    // 由于对象实际上是没有这个函数属性的,需要删掉
    delete obj.func
    return result
}

bind实现

bind返回的是函数

Funtion.prototype.bind = function() {
    // 获取this指向对象obj和入参
    const [obj, args] = [...arguments]
    const fn = this // 此时this指向的是执行函数
    const result =  function() {
        // 因为绑定函数也可以入参,和curry函数同理
        const argu = [...arguments]
        // 利用apply修改this指向,并讲参数合并
        // 因为bind函数在new时丢失一开始绑定的this指向,由于new的时候this指向实例,所以利用this instanof result ? this修改指向,否则则说明只是普通调用,继续绑定为obj
        return fn.apply(this instanof result ? this : obj, args.concat(argu))
    }
    // 将result原型指向this的原型,使之new出来的实例可以获得最初执行函数的属性
    result.prototype = fn.prototype
    return result
}

事件循环理解

JS Event Loop(事件循环) 众所周知,Js是单线程。Js主线程(Web Worker的是子线程)。Js引擎在执行栈中依次执行任务,遇到异步任务就会挂起(交给相对应的模块处理),执行接下来的任务。 当异步任务完成后,对应模块会将异步任务的回调扔进任务队列中 等到当前执行栈任务执行完以后,就会将任务队列中的任务扔进执行栈中执行。事实上,任务队列存在micro task 和 macro task两种,其中promise扔进micro,settimeout扔进macro中。 事实上,js引擎在执行栈清空后,就会先检查micro里的任务,如果有就执行,没有就去取macro里的任务进执行栈,当执行栈清空后又循环往复,形成事件循环机制。 有时候then太多也会让人教难理解,所以引入async await; 有的then的例子和解释:

html5新特性

  1. 语义化标签 article header footer aside nav等
  2. 增加表单组件 date email 等
  3. 视频和音频
  4. canvas
  5. 拖拽api
  6. svg绘图
  7. 地理定位(不好使
  8. web存储

http1.0 1.1 2.0

1.1的新特性: 长连接,运行提交部分资源及对应状态码206,更多的错误状态码,http缓存机制,host头的加入
长连接:需要注意这不是真的长连接,真的只有websocket实现了;它只是在同一个tcp链接上发送多次http请求,有头阻塞的问题,懒加载也解决不了
2.0的新特性: 二进制帧,多路复用,头部压缩,服务器推送

https的理解

https是在tcp和http中间加了一层ssl/tsl的协议,使用非对称加密协议传输对称协议的秘钥,对称加密协议传输。 首先客户端向服务器发起安全请求,服务器返回自己的数字证书,客服端收到后用让ca验证数字证书的真实性,如果真的就获得了服务器的公钥,然后用服务器公钥加密此次对称协议加密的秘钥,服务器收到后用自己的秘钥解析获得 传输秘钥,开始传输。

继承

function SuperClass(a) {
    this.a = a
}
1. 原型链继承
  function SubClass(b) {
      this.b = b
  }
  SubClass.prototype = SuperClass.prototype
2. 借用构造函数继承
  function SubClass(b) {
      SuperClass.call(this, b)
  }
3. 组合继承
  function SubClass(b) {
      SuperClass.call(this, b)
  }
  SubClass.prototype = SuperClass.prototype
4. 原型式继承
  function create(obj) {
      function f() {}
      f.prototype = obj
      return new f()
  }
5. 寄生式继承
  function js(b) {
      var obj = create(SuperClass)
      obj.b = b
      return obj
  }
6. 寄生组合继承
  function jszh(SubClass, SuperClass) {
      var prototype = create(SuperClass.prototype)
      prototype.constructor = SubClass
      SubClass.prototype = prototype
  }
7. ES6继承
    CLass B extend A {
        constructor() {
            super()
        }
    }

http缓存

浏览器的缓存策略分为强缓存和协商缓存。强缓存主要看的是cache-control 和 expires两个,其中cache-control的优先级更高一些,主要是通过s-maxage、maxage + 响应报文生成时间 来判定是否过期,expires是绝对时间,如果不过期,浏览器直接返回200状态码,完成一次强缓存。

cache-control: no-cache(不使用绝对缓存)

no-store 不缓存

如果强缓存过期了,会向服务器发起协商缓存,主要是认etag(优先级高一点)和last-modified,这时候服务器会根据这两个字段识别资源是否过期,没有过期返回304浏览器读取缓存,过期了就返回其他状态码,并返回资源

实际策略时,对于带有hash值设置一个超长的expire值,让他不过期;不带hash值的则设置cache-control: no-cache让它协商缓存,让服务器识别

set、weakset 、map、weakmap

weakset和set的区别就是weakset只能存放对象,而且是弱应用,受标记清除的影响,对象被清除后,就没了,不会存在内存泄露的危险

可以用来存放对象,主要用处就是当需要使用set的时候而且都是元素都是对象

weakMap和map之间的区别就是weakmap的key只能是对象,而且是弱引用,

可以用来存放对象的特性,不需要关心内存泄露问题

响应式原理理解

响应式原理需要三个模块,一个是收集到数据的变化即数据劫持, 一个是知道视图依赖了哪些数据, 即收集依赖 最后就是将收集到的变化通知到需要更新的视图部分,即发布订阅模式 首先是vue实例会用observer对data属性的数据进行数据劫持, 现在是用Objtct.defineProperty来改造访问器属性, getter中实现收集依赖,因为只有视图使用到了该属性才会触发getter,向发布者订阅一个通知, setter中触发通知告诉发布者, 发布者才可以通知遍历通知订阅者,订阅者收到通知后,触发渲染当前组件,生成新的虚拟dom树,vue框架会比对当前dom树的区别并局部更新

diff算法理解

diff只会在同级节点间比较。在比较时,会先比较新旧节点的key值和class属性等属性,如果值得比较进入下一步,不值得就直接替换了。替换是寻找父节点并在父节点添加新节点,删除旧节点

下一步比较时会比较两个虚拟节点对真实节点的引用是否一样,是的话直接返回,不是的话,继续比较是不是文本节点,如果是就比较文本是否不一样,用新的文本替换老的文本,不是的话开始比较新旧节点子节点的比较,两个都没有子节点返回,新的有子节点旧的没有就增加,新的没有旧的有就删除,都有子节点就下一步比较子节点

比较子节点时先用key值比较,如果没有key值会用在新旧节点设置起始点,分别开始比较头头节点,尾尾节点,新头旧尾节点,新尾旧头节点直到新旧节点的头节点位置小于尾节点的位置了。如果新的有多,则对应插入,如果旧的有剩,就对应删除。如果四种比较都找不到,就会讲新节点的头节点插入并向后移,继续下一轮循环。

旧尾新头意味着旧尾应该要插在旧头前了,既oldStartIdx目前位置的前一位;旧头新尾意味着旧头应该要插在旧尾了,既oldEndIdx目前位置后一位

实现''.indexOf

String.protptype.indexOf = function(b) {
    const reg = new RegExp(`${b}`, 'gi')
    const c = reg.exec(this)
    return c ? c.index : -1
}

用gennerator实现fibo

function *fibo() {
    let a = 0, b = 0
    yield a
    yield b
    while(true) {
        let c = a + b
        a = b
        b = c
        yield c
    }
}

优化

js优化既编码优化

  1. 在作用域上读取变量会变慢,越深越慢,尽量缓存为局部变量
  2. 同理对象结果太深也是同理,最好将结构扁平化,避免嵌套太深,如果做不到就缓存局部变量
  3. 循环上的优化,应该严格控制循环次数,尽量不用for in,因为会遍历原型链上的属性,递归上也可以利用尾帧优化
  4. cookie上的使用,会一直携带
  5. 要用switch不如用object/map

dom上的优化

  1. 注意dom操作是十分耗时的,避免边读边写,避免一直读取(使用缓存),一次性写入
  2. 避免重排,或者使用documentflagment等,或者将其脱离文档流,完成后再移回
  3. 使用事件委托,避免绑定大量事件,新增的节点这样也能使用

css上的优化

  1. 避免多层级嵌套,这个是很需要注意的,特别是大家使用sass等css工具库时很容易一直嵌套,我之前就是这样
  2. 使用css动画,开启硬件加速(translatez)
  3. 使用link不用@import,没有好处,link还可以同步渲染,看起来更快
  4. 不使用css表达式
  5. 避免无意义的属性,例如有些是继承的,如font-size text-align 就不需要重复写

html上的优化

  1. css放头部,js放尾部,这是因为js会阻塞渲染,而且阻塞时会等到css渲染完毕
  2. 避免太重的dom数
  3. 语义化,有利于seo
  4. 尽量避免使用table布局(因为会多次重排 重绘)
  5. 避免空的src,部分浏览器仍会发起一次空的请求
  6. 压缩文档,g-zip(后端启用

网络上的优化

  1. 使用cdn
  2. 使用http缓存,实践上有hash的永久缓存,没有hash的协商缓存
  3. 使用http2.0 ,至少是http1.1
  4. 资源可放在多个域名下,但是不要太多,最好四个,会有dns查询代价
  5. 减少请求,如雪碧图,base64直接在html中
  6. 避免重定向,300重定向,301永久重定向,302临时重定向,304协商缓存命中,直接读取缓存
  7. 对静态资源文件使用dns-prefetch

首屏优化

  1. 预下载和懒下载
  2. 首页只加载首页的js和css
  3. 使用骨架屏和动画欺骗用户,这样用户就会觉得是自己网络不行,不是我们不行

vue数组劫持实现

let oldProtoMehtods = Array.prototype;
let proto = Object.create(oldProtoMehtods);
['push','pop','shift','unshift'].forEach(method=>{
    Object.defineProperty(proto,method,{
        get(){
            update();
            oldProtoMehtods[method].call(this,...arguments)
        }
    })
})

在这里我们可以看到,依然是使用Object.defineProperty来劫持对象的属性,只不过这次劫持的是数组的原型

promise的实现

class _Promise {
    constructor(func) {
        // 记录promise内部的状态
        this.status = 'pending'
        this.value = ''
        this.reason = ''
        //执行函数,并将函数传入,改变this指向为内部
        func(this.resolve.bind(this), this.reject.bind(this))
    }
    resolve(res) {
        this.value = res ? res : undefined
        this.status = 'fullfill'
    }
    reject(res) {
        this.reason = res ? res : undefined
        this.status = 'rejected'
    }
    then(onFulfilled, onRejected) {
        if(this.status === 'fullfill') {
            // 执行第一个函数
            const _isPromise = onFulfilled(this.value)
            // 如果执行完的结果是_Promise实例,既链式调用
            if(_isPromise instanceof _Promise) {
                return _isPromise
            }
            不然返回this
            return this
        } else if(this.status === 'rejected' && arguments[1]) { // 这个主要是允许不写第二个函数的
            const _isPromise = onRejected(this.reason)
            if(_isPromise instanceof _Promise) {
                return _isPromise
            }
            return this
        }
        // 如果状态为reject又没写第二个函数说明要调用catch函数
        return this
    }
}

展平数组,且实现Array.prototype.flat

const arrForFlat = [1, 2, 3, [4, 5, [5, 6], 7, 8], 9, [10, 11, [12]]]
// 第一种方案:用现成api
const arr1 = arrForFlat.flat(infinity)
// 第二种方案:利用join展平
const arr2 = arrForFlat.join().spilt(",").map(Number)
// 第三种方案:利用toString直接展平
const arr3 = arrForFlat.toString().spilt(",").map(Number)

// 递归实现:
Array.prototype.flat = function(deep = 1) {
    let result = []
    const arr = this
    const realDeep = deep
    arr.forEach(e => {
        if(Array.isArray(e) && deep > 0) {
            result.push(...e.flat(--deep))
        } else {
            result.push(e)
            deep = realDeep
        }
    })
    return result
}

this指向总结

  1. 全局环境中this指向全局对象 浏览器环境指向 window , node环境中指向global
  2. 构造函数中只要没有返回新的函数或对象,指向实例
  3. 隐式绑定,普通函数谁调用指向谁
  4. 显式绑定中,call、 apply、bind 会指向 传入的对象 ,如果传入的是null/undefined,严格模式下会指向传入对象,非严格模式下指向全局对象
  5. 普通函数内直接调用时,非严格模式 指向 全局对象, 严格模式指向 undefined
  6. 箭头函数没有自己的this,继承外层上下文的this

BFC总结

bfc是一块渲染区域,并有自己的布局规则,主要用处是清除浮动,消除重叠,两栏布局等,

布局规则:

bfc内的box依次垂直排列;
margin会重叠,最后取值大的;
内部box永远向左边贴着,浮动元素也如此 ;
bfc区域不会和float box重叠
bfc是独立,内部不会和外部发生影响;
计算高度时,浮动元素也参与其中

产生条件:

根元素
position: absolute fixed
float不为none
display 为 inline-block table-cell
overflow 不为 visible

跨域

同源策略是指只能访问同域名、同协议、同端口的资源。

  1. jsonp古老的实现,兼容老环境,缺点是只能实现get请求, cxrf、xss 利用的是script的src没有同源策略,然后返回内容直接执行callback函数及带入参数数据
  2. cors 只要在后端设置允许即可,如需带上cookie,前端也需设置一个字段withCredentals 分为简单请求和复杂请求,简单请求为 请求方式为head、get、push 请求头字段不能过多,主要注意的是json传输格式为复杂请求 复杂请求就是多请求了一次option请求,知道哪些方式是可以的
  3. ngix反向代理,前端vue常用 同源策略限制的是浏览器,不是http协议,所以利用多起一个服务器反向代理请求过去即可

vue生命周期

  1. beforeCreate、created 这个阶段主要是初始化,初始化前el和data都为undefined, 初始化后 完成data
  2. beforeMount, mounted 这个阶段主要是挂载实例, 挂载前el已经初始化好,页面中为占坑数据 挂载后替换html中的el
  3. beforeUpdate, updated 这个阶段是更新数据触发的,主要是明确在updated中更新数据可能会引起死循环
  4. beforeDestroy, destroyed 这个阶段是销毁触发的
  5. activated deactivated ,前者是第一次进入被keep-alive包裹的组件触发,后者是缓存后进入触发的

判断变量的类型

如果是基本类型则用typeof, 如果是引用类型则用Object.prototype.toString或者instanceof

vue组件化的好处

重复利用,便于调试,多人协作开发,便于维护

async的原理

async本质上是一个generator的语法糖,只不过自带了执行器,返回的是promise

项目难点

  1. promise.Allsettled的实现
  2. 复合组件-既木偶组件和智能组件

vue3新特性

  1. v-dom的更新
    这个优化主要是针对以前的动态节点很少时依然要遍历整一个组件v-dom,现在切割成以动态节点为单元的块嵌套的,更新时各自更新自己那一块
  2. typeScript重写底层
  3. Object.defineProporty -> proxy
    这个主要是针对老的api对于对象需要循环定义,对于数组的下标读取设置无法识别

vuex在mutation中提交异步代码会怎样

vuex中mutation中代码在commit时是用forEach的方法执行的,异步的代码不会执行。

自动识别姓名地址电话

  1. 先利用地址数据库识别地址信息,再从原文剔除这些内容
  2. 在匹配出字符串中的手机号或者电话号码。先匹配11位数字,匹配不上继续匹配电话
  3. 剩下利用姓氏数据匹配名字,遵从一个字的姓氏最多三个字,两个字的姓氏最多四个字。
  4. 如果匹配不上,则去除,。 空格 等符号后看下是不是小于三个字的则视同为姓名