前端八股文(持续更新)

·  阅读 1767

写在前面

自己在最近面试的过程中以及同事面试给的建议整理。
--2022年2月26日更新:又要跳槽了,重新整理一波 精致版

  1. 说话成体系,能自圆其说,逻辑有体系。在回答问题前,可以先停顿几秒。(先思考,再回答,控制好语速)
  2. 说说你在项目中碰到的最大困难是什么,怎么解决的。(实践中对项目的理解、总结)
  3. 职业规划是怎样的(短期?长期?)
  4. 这段时间有什么可以总结的?(方法论?技术沉淀?业务思考?)
  5. 你如何在团队中发挥自己的影响力?(技术团队中的作用,项目中的作用,横向部门间的作用,比如主动帮同事review代码、调研新技术并推进实践、主动发现问题解决问题,作为项目PM推进项目进度)
  6. 大原则:实事求是。(知之为知之,是知也)
  7. 为什么选择将要应聘的这家公司?
  8. 你为什么从上家离职?(不要说公司的坏话,表达要委婉)
  9. 三个关键词形容自己(能突出自己的品质特点)
  10. 你有什么要问的?(不要不问(比如:我没有什么问题了),能提问最好。比如贵公司使用什么技术栈?业务模式?问些实际的问题,显得自己的意愿比较强)

自我思考

  1. 工作中或者项目中遇到的问题和解决方案
  2. 介绍一下自己
  3. 未来的发展规划

基础概念

浏览器缓存机制

juejin.cn/post/684490…

get和post请求区别

1.url 体现区别,get请求能在URL上体现,post不可以。

2.长度限制,get请求有长度限制,但并非HTTP协议的限制,是浏览器和web服务器的限制,因此不同浏览器和web服务器限制成都不一样。

3.缓存区别, get请求一般用于查询,可以不用每次都与数据库交互,利用缓存机制,post请求一般用于增删改操作,必须与数据库就行操作,所以不可用于缓存。

跨域和解决方法

指浏览器不能执行其他网站的脚本,源于浏览器的同源策略,同源指的是协议相同,主机和端口均相同,更详细解答

解决:

Jsonp: 利用<scirpt>没有跨域限制,达到网页可以从其他来源动态产生json数据,需要服务器支持。

webpack 配置proxy,原理是webpack内部起了node服务正向代理,服务器 之间不需要代理

nginx 反向代理, nginx 配置跳板机, 前端请求跳板机,nginx 转发请求目标服务器。

Cors: 后端需要在目标服务器http相应头 配置允许跨域: Access-control-Allow-origin: '允许跨域的域名或者*'开启Cors;

前端发起请求时会分为“简单请求”和“复杂请求”,复杂请求时会在正式请求前发一个option请求来知道服务端是否允许跨域。

模块化发展历程

作用:主要用于抽离公共代码,隔离作用域,避免变量冲突等。

IIFE:立即执行函数,在一个独立的作用域中执行代码,不会污染全局作用域和避免变量冲突。

AMD: 用requireJS 来编写模块化,特点:依赖必须提前声明。

CMD: 使用seaJS 来编写模块化,特点:支持动态引入依赖文件。

CommonJS: nodeJS 内置的模块化。

ES6的modules: es6引入的模块化。

TCP三次握手

第一次:client -> server 服务端知道客户端有发送能力

第二次:server -> client 客户端知道服务端游发送能力

第三次:client -> server 让服务端知道客户端接受能力没问题

其中,为了保证后续的握手是为了应答上一个握手,每次握手都会带一个标识 seq,后续的ACK都会对这个seq进行加一来进行确认。

null 和 undefined 区别

简单点理解: undefined表示值不存在,null表示值存在但是为空,没有意义;

null == undefined // true
null === undefined // fasle
// typeof null 为 Object, typeof undefined 为 undefined

null: 作为对象原型链的终点

以下几种情况均为undefined

1.当定义一个变量没赋值。2.当调用一个对象还没添加的属性时候。3.当调用一个没有返回值的函数时。

let a; // undefined
let b = {};
b.name; // undefined
function c() {};
let d = c(); // undefined

函数柯里化

将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数且返回结果的新函数的技术。

const add = function add(x) {
    return function (y) {
        return x + y
    }
}
const add1 = add(1)
add1(2) === 3

节流防抖

场景:1.向服务端频繁发请求,增加服务器压力。2.频繁dom操作影响浏览器性能

解释: 防抖: 每次有请求是继续延长时间,如:输入框输入请求,延迟1秒钟,一秒内还有请求继续延迟 节流:固定个时间请求。

function debounce(fn, interval, immediate) {
  //fn为要执行的函数
  //interval为等待的时间
  //immediate判断是否立即执行
  var timeout;  //定时器
  return function () { //返回一个闭包
    var context = this, args = arguments; //先把变量缓存
    var later = function () {  //把稍后要执行的代码封装起来
      timeout = null; //成功调用后清除定时器
      if (!immediate) fn.apply(context, args); //不立即执行时才可以调用
    };
    var callNow = immediate && !timeout;  //判断是否立即调用,并且如果定时器存在,则不立即调用
    clearTimeout(timeout);  //不管什么情况,先清除定时器,这是最稳妥的
    timeout = setTimeout(later, interval);  //延迟执行
    if (callNow) fn.apply(context, args);  //如果是第一次触发,并且immediate为true,则立即执行
  };
};
var throttle = function (fn, interval = 500) { //fn为要执行的函数,interval为延迟时间
  var _self = fn,  //保存需要被延迟执行的函数引用
    timer,  //定时器
    firstTime = true;  //是否第一次调用
  return function () { //返回一个函数,形成闭包,持久化变量
    var args = arguments, //缓存变量
      _me = this;
    if (firstTime) { //如果是第一次调用,不用延迟执行
      _self.apply(_me, args);
      return firstTime = false;
    }
    if (timer) { //如果定时器还在,说明上一次延迟执行还没有完成
      return false;
    }
    timer = setTimeout(function () { //延迟一段时间执行
      clearTimeout(timer);
      timer = null;
      _self.apply(_me, args);
    }, interval);
  };
};

讲讲Redux原理

三大原则: 单一数据源: 整个应用的state都被存储到一个状态树(store)里面,并且这个状态树,只存在于唯一的store中;

状态只读:state是只读的,唯一改变state的方法就是触发action,action是一个用于描述以发生时间的普通对象;

数据改变只能通过纯函数: 使用纯函数来执行修改,为了描述action如何改变state的,你需要编写reducers

store: 存放数据的仓库,通过redux提供的createStore生成stroe

state: 数据源一个store可以有多一个state

action: 一个对象,其中必须包括type属性,用来告诉store如何操作state

store.dispatch: 接受一个action, 告诉store改变state

reducer: reducer是一个纯函数,store 接受到action后要返回一个新的state,这个计算过程就叫做reducer.

箭头函数和普通函数区别

1.this指向, 箭头函数不绑定this, 会自动捕获上下文的this

2.箭头函数不能作为构造函数,也就不能new

3.箭头函数不绑定arguments,用...解决

4.箭头函数没有原型属性

5.箭头函数不能当作Generator函数,不能使用yield关键字

javascript 单线程

问:说一下javascript的单线程模型

答:js只在一个线程上运行,也就是说JavaScript同时只能执行一个任务。 好处就是执行起来相对简单,坏处就是会造成任务堵塞,和CPU资源浪费。

问:javascript为什么采用单线程

答:历史原因,js在语言设计之初就定下的规范,js的设计者在设计这门语言的就不想让js过于复杂,举个例子,如果js多线程的话可能会出现两个线程同时操作一个dom,这时候浏览器又要判断执行哪一个,是否又要引用锁的机制,所以避免过于复杂,JavaScript一开始就是单线程模型。

说一下JavaScript 的事件循环

JS引擎单线程,浏览器提供多个辅助线程来执行js主线程中遇到的异步任务。

先说一下js的单线程模型,事件循环是JS的一个机制,并不是JS内部的一个方法什么的,首先JS除了主线程之外,JS引擎提供了多个任务队列来执行主任务的一些异步任务,主线程在执行的过程中遇到遇到异步任务的话,会放到异步队列中去执行,然后继续执行主任务,直到当前的主任务执行完了,再去看异步队列中有没有完成的任务,有的话,继续拿到主任务中去执行,在这个过程中又遇到异步的话,继续放到异步队列中,知道任务执行完。这样一个过程就叫事件循环。

前面说到有多个任务队列,是根据异步任务产生的,异步任务也有优先级的,所有任务队列分为两种,一个是微任务队列,一个是宏任务队列,微任务的优先级要高于宏任务,所以当主任务执行完,先执行微任务,在执行宏任务,微任务主要是Promise,MutationObserver(监听dom变化),剩下的定时器,ajax等都是宏任务。

说一下JavaScript 的原型链

首先原型链是JS实现继承的一种方式。 一个实例对象要调用属性或方法时,JS会先从自身的属性和方法上找,然后在其构造函数的原型上找,在在原型的原型上,直至找到object.prototype为止。

比如:let a = [], 调用a.toString, 若a对象下没有toString方法则 -> a.proto.toString -> Object.toString为止,这样的链式调用就叫做原型链

es6新特性

let/const 、变量解构、Promise、Set和map数据类型 、 class、模块化、anysc/await、symbol、generator函数

generator函数

  1. 一种异步编程解决方案
  2. 语法上,Generator 函数是一个状态机,封装了多个内部状态 - yield/return 返回内部状态
  3. 执行 Generator 会返回一个遍历器对象,是一个遍历器对象生成函数,可以遍历函数内的每一个状态

Promise

  1. 异步编程的一个解决方案,可以让异步操作以同步的流出表达出来。传统的方案就是回调函数一级DOM事件
  2. 语法上讲Promise是一个对象,提供了统一的API进行异步操作

特点:

  1. 对象的状态不受外界影响,只有通过异步操作改变其状态
  • pendding: 进行中
  • fulfilled:已成功(resolve)
  • reject:已失败
  1. 状态一旦发生(pendding -> fulfilled/reject)就不可改变。

关于微前端:

什么是微前端,解决了什么

目前开发的前端产品主要有两个方向:MPA和SPA。

MPA: 应用独立通过域名实现应用之间的跳转,优点天生具备了技术栈无关,独立部署,独立开发的优势。缺点:应用跳转会导致浏览器重刷,流程体验上就会造成断点。

SPA:天生体验上的优势,应用内的跳转,无需刷新。缺点就是技术栈强耦合,以及项目会越积越大。

微前端可以说是这两者优势的合集,微前端架构可以吧一个应用拆分成多个互相独立的子应用,保证了应用的独立性,同时也保证了产品的完整体验流程。

解决的问题:

  1. 业务不断扩大,业务模块之间耦合加剧
  2. 开发团队面临拆分、解耦才能达到并行开发
  3. 新的框架、方案如何适用现有的工程环境(构建工具)等
  4. 旧的框架如何平稳升级

微前端中应用的预加载

详细参考

qiankun提供了开启预加载的字段(prefetch),通过setDefaultMountApp方法设置需要默认加载的字应用。

预加载的思路: 在浏览器提供的requsetIdleCallback事件里面执行import-html-entry。 加载的资源会根据url缓存起来。

子应用的加载

详细参考

服务通信

详细参考

首先微前端不建议应用之间过多的的通信。

1.localStorage

2.qiankun内部提供了action通信(观察者模式)

qiankun提供initGlobalState方法来在全局注册一个实例用于通信。这个实例有三个方法:

  1. setGlobalState: 来修改全局的状态(globalState),内部会对globalState进行浅比较,如果发生改变,会通知所有的观察者。
  2. onGlobalStateChange: 注册观察者的函数,子组件通过这个方法注册成观察者,当全局的globalState发生变化,就会触发该函数。
  3. offGlobalStateChange: 取消成为观察者。

下图可以看出整个流程就是,子组件通过注册成为观察者到观察者池中,通过修改globalState通知观察者函数,达到组件之间通信的效果。

ps:自己画一下图会更好理解,哪怕看着别人的画)

3.shared通信 思路:主应用通过基于redux维护一个状态池,暴露一些方法给子应用。

微前端中菜单路由的管理

qiankun是基于singe-spa实现的:主的思路是主应用里面维护一个路由规则表,每个子应用对应一个唯一的路由(比如商品应用goods),qiankun会劫持window.histroy相关的路由跳转事件,每次发生路由跳转时根据路由规则匹配到对应的子应用。匹配到后的路由交给子应用自己的路由系统。

为什么选择qiankun而不是飞冰

1.之前是因为飞冰的文档没乾坤全,乾坤可以参考singe-spa(现在的飞冰文档比乾坤好很多)。

2.沙盒模型: 在应用加载前对window对象做一个快照(拷贝),在应用卸载的时候恢复这个快照。这样可以实现每次进入应用都有一个全新的window对象环境。其实大致原理就是记录window对象在子系统运行期间新增、修改和删除的属性和方法,然后会在子系统卸载的时候复原这些操作。

qiankun: 在应用的 bootstrap 及 mount 两个生命周期开始之前分别给全局状态打下快照,然后当应用切出/卸载时,将状态回滚至 bootstrap 开始之前的阶段,确保应用对全局状态的污染全部清零。而当应用二次进入时则再恢复至 mount 前的状态的,从而确保应用在 remount 时拥有跟第一次 mount 时一致的全局上下文。

ice: 可在主应用钩子函数中按需记录全局状态

  <AppRouter
        onAppEnter={(appConfig) => {
         // 按需记录全局状态
        }}
        onAppLeave={(appConfig) => {
      // 按需恢复全局状态
        }}
  >
    // {...}
  </AppRouter>

React

说一下React生命周期

三个阶段:

  1. 挂载时: 首次实例化组件被挂载时,主要的生命周期有:constructor, render,componentDidMount
  2. 更新时: 当组件的props或者state 更新时执行, componentDidUpdate和一个静态方法,返回最新的state
  3. 卸载时: 当组件卸载或销毁时,componentsWillUnmount

React Hooks

useState: 接受一个初始值返回一个新的state和一个更新state的函数。

useEffect: 接受两个参数,一个包含命令式和有可能有副作用代码的函数以及一个依赖值数组。 执行时机:在浏览器绘制之后并且保证在任何新的渲染之前执行。如果想在绘制之前执行可以使用useLayoutEffect代替

useContext: 接受一个context对象(Racet.creatContext返回值)返回该context的当前值。

额外的Hooks(如下):大多数是useState和useEffect的变体,在某些特殊情况下用起来更好。

useReducer: 类似于redux机制,接受一个reducer和一个initialState,返回一个state和dispatch方法。 在某些场景下代替uesState,比如:state逻辑复杂并且包含多个子值,或者下一个state依赖于上一个state.

useMemo和uesCallback: 作用:用法类似都是用于减少渲染次数,达到优化性能的作用。 useMemo(() => fn, [deps]) = useCallback(fn, [deps]) 他们唯一的区别是:useCallback是根据依赖项(deps)缓存第一个参数callback。useMemo是根据依赖项(deps)缓存第一个参数callback执行后的值。

说一下虚拟DOM

虚拟dom本质就是一个JS对象,虚拟dom存在一的意义在于最小化dom改变,然后批处理dom变化,在必要的时候在重新渲染页面。

diff 分为三类比较:

1.相同类型节点比较:只会对属性进行重设,从而实现节点的转变

2.不同类型节点比较:会直接删除旧的节点,创建新的节点代替。

3.列表节点比较:开发手动添加key,帮助react定位到正确的节点,减少dom操作次数。

单项数据流

我所理解的单项数据流是:数据是不可逆的,父组件向子组件通过props传递数据,子组件不可修改props, 只能通过触发事件通知父组件修改数据,在派发给子组件。

保证了数据了单一流向性,以及高维护性。

react的fiber

详细参考

背景: react 是通过虚拟 (Virtual) dom 进行UI的描述, 通过对比新旧两个dom来最小化一个dom 的更新。 这个对比的过程最核心的就是diff算法, diff算法的计算并不是免费的,当遇到大量的dom更新时会,造成动画丢帧以及用户操作不流畅等问题。

造成以上问题的原因: 在react 16之前进行UI渲染核心实现是采用递归的方式,我们知道,递归是无法被打断的,当需要更新的时候,会从需要更新的地方一直进行diff。当diff 时间过长,js 的主线程就会造成堵塞,影响UI响应。

react fiber 如何解决这个问题: 核心是基于浏览器提供的一个原生api -> requestIdlecallback: 让浏览器在空闲时候执行脚步。

那fiber要做的事就是,让diff的过程可中断/恢复,其次要有一个优先级的调度策略。这样就可以实现在浏览器空闲时候按优先级顺序去执行操作。

常问的一些总结

React和Vue的区别

详细参考

1.监听数据变化的实现不同

vue:通过劫持getter/setter等方法,准确的知道数据的变化。

react: 通过浅比较引用的方式,可能会造成大量不必要的渲染,当然也可以通过(pureComponet/shouldComponentUpdate/useMemo/useCallback)等进行优化

2.数据流的不同

vue: 双向数据绑定。

1.x版本中父子组件props可双向绑定,组件和dom通过v-model也可双向绑定。

2.x版本后不建议对props进行改变,只保留了组件和dom的双向绑定

react: 提倡单项数据流,自顶向下。 日常项目中一般使用vuex/redux等单项数据流状态管理框架,所以这一块可能感受不是很大。

3.hoc和mixins

vue 一直采用mixins, react最早也采用mixins,后面改成hoc。

原因:react组件和vue组件的区别。react组件本身是一个纯粹的函数,高阶函数对react来说 会简单很多。

vue组件就本身是一个被包装过的组件,比如模版是如何被编译,以及props如何被传递,这些都是vue在创建实例的时候隐式做的。如果使用hoc包装后可能就无法执行了。

4.组件通信

和react是父组件通过props给子组件传递数据。

vue最大的特点就是子组件可以通过自定义事件给父组件传递数据

5.模版渲染的区别

写法上,react通过jsx渲染模版,vue通过一种拓展的HTML语法渲染。

本质的区别: react 在写法上通过原声的JS实现一些插值、if、循环等操作。 vue通过v-if,v-for,等指令去操作。 更喜欢react做法,贴近原生

react的render是支持闭包的,所以在引用其他组件时可以直接拿来用,vue的话还需要在components下声明一下。

为什么使用hooks

描述一下观察者模式

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改