一份沉甸甸的大厂面经

前端人

共 18977字,需浏览 38分钟

 · 2021-03-02


关注公众号 前端人,回复“加群

添加无广告优质学习群

js 基础

手写 reduce

Array.prototype.myReduce = function (fn,initVal{
    let result = initVal,
        i = 0;
    if (typeof initVal === 'undefined') {
        result = this[i]
        i++;
    }

    while(i < this.length) {
       result =  fn(result, this[i])
    }
    return result
}

手写 Promise.all

Promise.prototype.myAll = function (promiseArray{
    return new Promise((resolve,reject) => {
        if (promiseArray.length === 0) {
            return resolve([])
        } else {
            let res = [],count=0
             for (let i = 0; i < arr.length; i++) {
             // 同时也能处理arr数组中非Promise对象
                if (!(promiseArray[i] instanceof Promise)) {
                    res[i] = promiseArray[i]
                    if (++count === promiseArray.length)
                        resolve(res)
                } else {
                    promiseArray[i].then(data => {
                        res[i] = data
                        if (++count === promiseArray.length)
                            resolve(res)
                    }, err => {
                        reject(err)
                    })
                }
            }
        }
    })
}

requestAnimationFrame

优势:目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。使用这个API就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。

  • requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中完成,并且重绘或回流的时间间隔是跟随浏览器的刷新频率
  • 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这意味着更少的cpu,gpu和内存的使用量。

promise

是异步编程的一种解决方案,解决了回调地狱的问题。

特点:

  • 对象的状态状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending、fulfilled、rejected。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果

Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

async/await

async 函数就是Generator函数的语法糖。

优点:

  1. 内置执行器
  2. 更好的语义
  3. 更广是适用性
  4. 返回值是 Promise await命令就是内部 then 命令的语法糖

arguments 对象

arguments对象是函数中的局部变量,他拥有 lenth属性和索引元素,但没有数组的其他属性

创建对象的7种方法

  • 通过Object构造函数或对象字面量创建单个对象
  • 工厂模式
  • 构造函数模式
  • 原型模式
  • 组合使用构造函数模式和原型模式
  • 动态原型模式
  • Object.create()

工厂模式

function Person({
  var o = new Object();
  o.name = 'hanmeimei';
  o.say = function({
    alert(this.name);
  }
  return o;
}
var person1 = Person();
  • 优点:完成了返回一个对象的要求
  • 缺点:
    1. 无法通过constructor识别对象,以为都是来自Object,无法得知来自Person
    2. 每次通过Person创建对象的时候,所有的say方法都是一样的,但是却存储了多次,浪费资源

装饰器

装饰器是一种特殊的声明,可附加在类、方法、访问器、属性、参数声明上。

作用:它起到了以声明式方法将元信息添加至已有代码的作用。

函数柯里化

是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

function curry(fn, args = []{
    return function ({
        let rest = [...args, ...arguments]
        if (rest.length < fn.length) {
            return curry.call(this, fn, rest)
        } else {
            return fn.apply(this, rest)
        }
    }
}

DOM

node.nodeType 节点类型常量

常量描述
Node.ELEMENT_NODE1一个元素节点 < p>/< div>
Node.TEXT_NODE3Element或Attr中实际的文字
Node.CDATA_SECTION_NODE4一个 CDATASection,例如 <!CDATA[[ … ]]>
Node.PROCESSING_INSTRUCTION_NODE一个用于XML文档的 ProcessingInstruction ,例如 < ?xml-stylesheet ... ?> 声明。
Node.COMMENT_NODE8一个 comment 节点。
Node.DOCUMENT_NODE9一个 Document 节点。
Node.DOCUMENT_TYPE_NODE10描述文档类型的DocumentType节点。
Node.DOCUMENT_FRAGMENT_NODE11一个DocumentFragment节点

typescript

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:

  1. 类的实例成员
  2. 类的方法
  3. 函数参数
  4. 函数返回值

Hybrid

h5 与 Native 通信方式?

  1. jsBridge

    • WebViewJavaScriptBridge的基本原理简单来说就是,建立一个桥梁,然后注册自己,调用他人
  2. Scheme

    • 通过与app端约定协议名称,app端定义协议对应的方法,web端通过 window.location = 协议名称的方式 通信
  3. websocket

ios 与 android 兼容性问题

line-height 显示效果不一致问题

在部分安卓机下设置line-height与高度相等时显示不居中

input输入框聚焦

input 输入框在聚焦的时候,ios有时候会出现 outline或者阴影,安卓则显示正常 解决方案:input:focus{outline:none} input:{-webkit-appearance: none;}

ios系统会将数字当成电话号码,导致变色

<meta name="format-detection" content="telephone=no"> <meta http-equiv="x-rim-auto-match" content="none">

安卓手机可以点击图片

部分的安卓手机可以点击页面的图片,解决方案就是通过css3给img标签设置不可点击属性。

img{ pointer-events: none; }

weex 原理

  1. 将vue单文件打包成 js bundle
  2. 客户端打开某一个页面,执行 js bundle
  3. 客户端提供了 js的执行引擎 用于执行加载到的 jsbundle
  4. js执行引擎执行 js Bundle,生成VNode树进行patch,找出最小操作DOM节点的操作,把对DOM的操作转变为Native DOM API,调用 WXBridge进行通信
  5. WXBridge将渲染指令分发到 Native(Android、iOS)渲染引擎,由native 渲染引擎完成最终的页面渲染

看完上述整体流程后,可以大致理解为何WEEX可以达到媲美原生的体验,因为其页面渲染并不是像H5方案一样使用浏览器的渲染能力,而是原生渲染,所以本质上渲染出来的页面就是一个native页面。

flutter 原理

flutter 采用自绘 ui + 原生的方式,通过在不同平台实现一个统一的接口的渲染引擎来绘制UI,而不依赖系统原色控件,所以可以做到不同平台UI的一致性。注意,自绘引擎解决的是UI的跨平台问题,如果涉及其他系统能力调用,依然要涉及原生开发。

优点:

  • 性能高:由于自绘引擎是直接调用系统API来绘制UI,所以性能和原生控件接近。
  • 灵活、组件库易维护、UI外观保真度和一致性高:由于UI渲染不依赖原生控件,也就不需要根据不同平台的控件单独维护一套组件库,所以代码容易维护。由于组件库是同一套代码、同一个渲染引擎,所以在不同平台,组件显示外观可以做到高保真和高一致性;另外,由于不依赖原生控件,也就不会受原生布局系统的限制,这样布局系统会非常灵活。不足:

动态性不足:为了保证UI绘制性能,自绘UI系统一般会采用AOT模式编译其发布包,所以应用发布后,不能像Hybrid和RN那些使用JavaScript作为开发语言的框架那样动态下发代码。

浏览器

同源策略

同源策略是一个重要的安全策略,它用于限制一个origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助组个恶意文档,减少可能被攻击的媒介。

同源的定义

如果两个url的 协议,域名,端口号都相同,则两个url同源。

源的修改

脚本可以将 document.domain 的值设置为其当前域或其当前域的父域。

跨源网络访问

同源策略控制不同源之间的交互,例如在使用XMLHttpRequest 或 < img> 标签时则会受到同源策略的约束

跨源数据存储访问

  • 访问存储在浏览器中的数据,如 localStorage 和 IndexedDB,是以源进行分割。每个源都拥有自己单独的存储空间,一个源中的 JavaScript 脚本不能对属于其它源的数据进行读写操作。如果一个页面包含多个iframe且他们属于同源页面,那么他们之间是可以共享sessionStorage的。

  • 浏览器存储均不支持跨域

Vue

组件的 data 为何要用 function 返回一个对象

为了保证组件在复用时数据是相互隔离的,所以每个被服用的组件的 data 都是复制的一份新的对象仅供当前组件使用,当某一处的组件内的data数据被改变时,其他服用的

数组响应式监听

在数据初始化的时候 vue会判断属性值是否是数组 ,如果是数组会遍历数组的属性循环调用 observe 将数组中的所有属性转换成响应式

同时,对数组的原生API做劫持,当使用数组的原生APi修改数组时会手动执行dep.notify()

响应式原理

nextTick

可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM 同时在Vue内部更新render时也是调用了nextTick使得多次set的触发会再下次微任务中统一执行。

provide/inject

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效

提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

性能

如何做首屏优化

  1. 路由懒加载 :当打包构建应用时,JavaScript包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
  2. 框架和模块按需引入
  3. 开启gzip
  4. 框架和插件从cdn中引入:可以配置 webpack 的 externals

为什么 react和Vue 一定要用 vnode?

  1. vnode 是 DOM的描述对象,操作Vnode比操作DOM更方便
  2. 直接操作DOM会频繁的触发浏览器的重排和重绘,而操作Vnode则不会
  3. 大量的重排和重绘会导致浏览器频繁的更新DOM树和CSSOM以及生成RENDER树,对性能消耗很大

当需要大量数据渲染,在交互时点击其中一行需要通知所有的行,如何做性能优化?

页面性能监控指标

根据谷歌 RAIL模型分为4个部分

  1. Response
  2. Animation
  3. Idle
  4. Load 分别代表着web应用的生命周期的四个不同方面。最好的性能指标是:100ms内响应用户输入;动画或者滚动需在10ms内产生下一帧;最大化空闲时间;页面加载时长不超过5秒。

细分一下就是

  1. FP/FCP
  2. FMP
  3. LCP

FP/FCP

表示首次渲染、首次有内容的渲染

网络

从输入url到页面加载完成的过程

  1. 首先是dns查询
  2. 假设服务端会响应一个 HTML 文件
  3. 首先浏览器会判断状态码是什么,如果是200就继续解析
  4. 文件解码成功后开始渲染流程,先根据html创建dom,有css的话会去构建cssom树,如果遇到script标签,会判断是否存在async 或defer ,前者会并行进行下载并执行js,后者会先下载文件,然后等待html解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直至js执行完毕。遇到文件下载的会去下载文件
  5. 初始html被完全加载和解析后会触发 DOMContentLoaded 事件
  6. CSSOM 树和DOM树构建完成后会开始生成Render树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
  7. 在生成Render树的过程中,浏览器就开始调用GPU绘制,合成图层,将内容显示到屏幕上

https协议的过程

  1. 客户端向服务端发起HTTPS请求,连接到服务器的443端口
  2. 服务器端有一个密钥对,即公钥和私钥,服务端保存着私钥,公钥可以发送给任何人。
  3. 服务器将自己的公钥发给客户端。
  4. 验证服务端发送的数字证书的合法性,验证通过后客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,然后用用服务器的公钥对随机值进行非对称加密。这样客户端的密钥就变成了密文。
  5. 客户端再次发起http请求,将加密之后的客户端密钥发送给服务端
  6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥(随机值),然后用随机值对数据进行对称加密,这样数据就变成了密文
  7. 然后服务器将加密后的数据发送给客户端
  8. 客户端收到服务器发来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。传输完成

node

node异常处理方法

  1. 同步代码中的异常使用try{}catch结构即可捕获处理。
  2. process的uncaughtException事件

那异步错误该怎么处理呢?首先换个思维,因为异常并不是事先准备好的,不能控制其到底在哪儿发生,所以站更高的角度,如监听应用进程的错误异常,从而捕获不能预料的错误异常,保证应用不至于奔溃掉。

process.on('uncaughtException', (e)=>{  
  console.error('process error is:', e.message);
});

process.on('uncaughtException')的做法,很难去保证不造成内存的泄漏。所以当捕获到异常时,显式的手动杀掉进程,并开始重启node进程,即保证释放内存,又保证了保证服务后续正常可用。3. 使用domain模块

Domain 模块可分为隐式绑定和显式绑定:隐式绑定: 把在domain上下文中定义的变量,自动绑定到domain对象 显式绑定: 把不是在domain上下文中定义的变量,以代码的方式绑定到domain对象

domin明显的优点,能把出问题时的一些信息传递给错误处理函数,可以做一些打点上报等处理工作,最起码保证重启后的服务,程序猿们知道发生了什么,有线索可查,也可以选择传递上下文进去,做一些后续处理。比如当服务出错的时候,可以把用户请求栈信息传给下游,返回告知用户服务异常,而不是用户一直等到请求自动超时。

但是它和process.on('uncaughtException')的做法一样,很难去保证不造成内存的泄漏。

一种比较好的方案是,以多进程(cluster)的模式去部署应用,当某一个进程被异常捕获后,可以做一下打点上报后,开始重启释放内存,此时其他请求被接受后,其他进程依旧可以对外提供服务,当然前提是你的应用不能异常多的数都数不清。

将cluster和domain结合起来使用,以多进程的方式保证服务可用,同时可以将错误信息传递下去进行上报,并且保留错误出现的上下文环境,给用户返回请求,不让用户请求超时,然后在手动杀死异常进程,然后重启。

express 与 koa的区别

用法的区别

  • express是基于回调,也是node中最常见的error-first模式
  • Koa使用Async/Await实现异步方式

中间件的区别

  • express 中间件是线性模型
  • koa 中间件是洋葱圈模型

集成度

  • express自带了Router和static中间件
  • Koa需要自行安装Router和Static的中间件

数据结构与算法

数组去重

实现千位加逗号

var a = 222333444.12
function addDou(num{
     var numArr = num.split('.')
     num = numArr[0]
     var result = ''
     while(num.length>3){
         result = ','+num.slice(-3)+result
         num = num.slice(0,num.length-3)
     }
     if(num){
         result = num+result
     }
     result = result+'.'+numArr[1]
     return result
 }
 console.log(addDou(a.toString()))

正则版

function addDou (num{
    num = num + ''
    let numArr = num.split('.')

    return numArr[0].replace(/(\d)(?=(\d{3})+$)/g'$1,')
}

字符串排列

输入'abc'输出['abc','acb','bac','bca','cab','cba']

思路

  • 本文章只研究全排列的情况,比如,还是 abc 字符串,3个字符,则总共的排列组合方式应该有 n! 种,此处的 n 为3,则计算出来应该是6种。本文采用递归方式实现,基本思路是通过双循环来实现递归的主逻辑部分,外层循环 str,内层循环 n - 1 时的返回数组 preResult 部分,算法逻辑如下方流程图。
  • 首先,考虑递归如何产生,假如当 n = 1,即传参 str 的长度只有1时,则直接返回只有 str 的数组;当 n > 1时,则考虑 n 和 n - 1 方法返回数组的关系。可以想到,n 时多出的一个字符,来添加到 n - 1 返回数组中每个字符串的头部。
var perm = function(s{
    var result = [];
    if (s.length <= 1) {
      return [s];
    } else {
      for (var i = 0; i < s.length; i++) {
        var c = s[i];
        var newStr = s.slice(0, i) + s.slice(i + 1, s.length);
        var l = perm(newStr);
           
        for (var j = 0; j < l.length; j++) {
          var tmp = c + l[j];
          result.push(tmp);
        }
      }
    }
    return result;
  };

数组随机排序

// 方法1
    function method1(arr){
        for(var i=0,len=arr.length;i<len;i++){
            var a=parseInt(Math.random()*len);
            var temp=arr[a];
            arr[a]=arr[i];
            arr[i]=temp;
        }
        return arr;
    }
    // 方法2
    function method2(arr){
        var newarr=[];
        while(arr.length>0){
            var len=parseInt(Math.random()*arr.length);
            newarr.push(arr[len]);
            arr.splice(len,1)  //splice(index,num,x,x)函数,index删除元素的位置(必须),num删除的个数(必须),x向数组添加的新元素(可选)。该函数返回被删除元素组成的新数组,同时原始数组也被改变;
        }
        return newarr;
    }
    // 方法3
    function method3(arr){
        arr.sort(function(){
            return Math.random()-0.5;
        });
        console.log(arr);
    }

题目1

[
123 ],
456 ],
789 ]
]
输出: [1,2,4,7,5,3,6,8,9]

// 对角线遍历

var findDiagonalOrder = function(matrix{
  // 遍历数组,角标之和递增输出
  if(matrix.length === 0){
    return []
  }
  let res = []
  let r = 0
  let c = 0
  let col = matrix[0].length
  let row = matrix.length
  
  for (let i = 0; i < col * row; i ++) {
    //偶数向上遍历
    res[i] = matrix[r][c]
    if((r + c) % 2 === 0) {
      if (c === col -1) { // 当遍历到左后一列时,本次遍历结束,下移一行为下次遍历做准备
        
        r++
      } else if (r === 0) { // 当遍历到第一行时,本次遍历结束,向右移动一格为下次遍历做准备
        // 往移动一格准备向下遍历
        c++
      } else { // 一般向上对角线遍历
        r--
        c++
      }
    } else { // 奇数向下遍历
      if (r === row - 1){ // 向下遍历到最后一行时本次遍历结束,向右移动一格,为下一行遍历做准备
        c++
      } else if (c === 0) { // 当向下遍历到第一列时本次遍历结束,向下移动一行为下一行遍历做准备
        r++
      } else { // 一般向下对角线遍历
        r++
        c--
      }
    }
  }

  return res
};

题目2

给定有序数组[1,2,3,4,6,8,9,10] 输出连续的值 结果为 ['1-4',6,'8-10']

  function merge(arr{
    let start = 0 
    let res = []
    arr.reduce((prev,cur,index) => {
      if (cur - prev !== 1) {
          let v = index - start > 1 ? `${ arr[start] }-${ arr[index - 1] }` : arr[index - 1
          res.push(v)
          start = index
      }
      return cur
  },0)
  // 余下的数字就都是连续的了
  if (start < arr.length - 1) res.push(`${arr[start]}-${arr[arr.length -1]}`)
  else if (start === arr.length -1) res.push(arr[arr.length - 1])
  return res
}


来源:https://mp.weixin.qq.com/s/JRv4QxgIJX9Uny8pnACgrA


  • 回复资料包领取我整理的进阶资料包
  • 回复加群,加入前端进阶群
  • console.log("点赞===看===你我都快乐"
  • Bug离我更远了,快乐离我更近了
浏览 19
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报