前端面试八股文

JavaScript 核心

经典概念

javascript 有哪些原始类型

Undefined,Null,Boolean,Number,String,Symbol,Bigint (一项新的提案让这个答案可能增加 Record 和 Tuple 这两个不可变数据类型)

执行上下文和作用域链

变量或者函数的执行上下文决定了他们可以访问那些数据,每个函数都有自己的执行上下文,当函数被执行的时候, 它的上下文会被推入 执行栈 ,并创建一个作用域链,执行时沿着作用域链查找变量, 函数执行完毕后,执行上下文也会被弹出。

闭包

闭包是指那些引用了另一个函数作用域中的变量的函数,通常在嵌套函数中实现。内部的函数作为参数传递或结果返回, 仍能访问到其所在的外部函数的变量。闭包形成的原因是当一个函数执行完毕时,将会销毁其执行上下文以及附带的活 动对象, 但由于外部函数的活动对象已被添加到内部函数的作用域链中,故无法被销毁,仍然保留在内存中,供内部函 数使用。 闭包的使用场景包括防抖、节流、单次调用函数等。

尾调用优化

尾调用优化是 ES6 规范新增的内存管理优化机制(严格模式下开启),当一个函数的返回值是是它内部返回值的时候, 通过重用执行栈栈帧的 方式优化内存管理(内部函数执行上下文执行完毕后不会直接弹出,而是先判断它的外部栈帧 是否有存在必要)。

构造函数

任何函数只要通过 new 操作符调用就是构造函数(一个函数也可以通过 new.target 来判断自己是否最为构造函数 被调用)。当使用 new 操作符执行构造函数时,js 解释器会在内存中新建一个对象,并将该对象的 __proto__ 属性 赋值为构造函数的原型对象 prototype ,然后构造函数内部的 this 被赋值为这个新对象,执行完内部代码后返回 这个对象(如果构造函数直接返回了一个对象,除非手动操作否则原型链会断掉)。

原型和原型链

每个函数(包括 ES6 类)都会创建一个 prototype 属性指向它的原型对象,原型对象的 constructor 属性也会 指向这个函数(或类),当这个函数作为构造函数使用,实例化出一个对象时,这个实例对象的 __proto__ 属性也会 指向构它的原型对象。当一个函数的原型对象是另一个构造函数的实例时,就会形成原型链。

为什么 0.1+0.2 不等用 0.3

任何使用了 IEEE754 浮点规范的语言都会存在这个问题,双精度浮点数的可靠位为 15 位,16 位之后的可能是对不上的。 0.1 和 0.2 储存值都比实际值要大一些,所以结果不等于 0.3,比较小数是否相当,应该使用两者的差值与 ES6 新增的 Number.EPSILON 属性比较:

if (0.1 + 0.2 - 0.3 < Number.EPSILON) {
  console.log(`0.1+0.2=0.3`);
}

javascript 如何实现继承

原型链、借用构造函数、组合继承、原型式继承、寄生式继承、寄生式组合继承。详见这里

奇技淫巧

自己实现 call 或者 apply

Function.prototype.myApplay = function (newThis, argArray) {
  const tempObj = newThis ?? window;
  const funcSymb = Symbol("tempFunc");
  // 当一个函数作为对象的属性调用时,函数内this指向这个对象
  // 利用这点来达到绑定传入的newThis的目的
  tempObj[funcSymb] = this;
  tempObj[funcSymb](...argArray);
};

自己实现 bind

Function.prototype.myBind = function (newThis) {
  const self = this;
  // 利用闭包,绑定this
  return function () {
    self.apply(newthis, arguments);
  };
};

浏览器与 javaScript

HTTP、HTTPS 和 HTTP2

  • HTTPS = HTTP + SSL/TLS,(TLS 是 SSL 标准化后的产物)
  • SSL 使用非对称加密,对称指的是加密解密使用同一密钥,非对称使用不同密钥
  • HTTPS 证书中包含了公钥,发送数据时会使用该公钥加密,接受端使用私钥解密,加大了破解成本,提高安全性
  • HTTPS 加密和解密过程会有一定程度的性能损耗
  • 目前广泛使用的时 HTTP 协议版本为 1.1
  • HTTP2 建立在 HTTPS 协议的基础上,安全
  • HTTP2 通过二进制分帧来进行数据传输,高效
  • HTTP2 一个域建立一次 TCP 链接,使用多路复用和连接共享,没有 HTTP1 中同个域的并发限制 (得益于分帧机制,帧可以乱序发送,不再依赖多个 TCP 实现并行)

HTTP 缓存机制

浏览器每次发起请求,都会现在浏览器缓存中查找该请求的结果以及缓存标识,且每次拿到结果,都会将该结果 和缓存标识存入浏览器缓存中,而这个缓存过程又分为强制缓存和协商缓存。服务端控制缓存规则的字段包括 Expires (HTTP1.0 的字段,客户端对比时间存在缺陷,已被后者取代)和 Cache-Control (优先级更高), Cache-Control 的取值有:

  • public 所有内容都被缓存
  • private 默认值,所有内容只有客户端可以缓存
  • no-cache 客户端缓存内容,但是否使用缓存要经过协商缓存验证
  • no-store 所有内容都不会被缓存
  • max-age=time 缓存内容在 time 秒后失效

当发送请求时,不存在缓存结果或上述标志,则直接向服务器发起请求,如果有这是缓存标志,且存在缓存结果, 则直接返回缓存结果(强制缓存生效,取缓存先内存后硬盘),如果缓存结果失效,则携带缓存标志向服务器请求, 服务器可能会返回 304(协商缓存生效), 浏览器直接使用缓存,也可能返回 200(协商了决定给新的), 浏览器使用新的结果。

协商缓存的过程中,请求会携带一些信息帮助浏览器判断:

  • last-modified-since 上一次请求的返回体中的 last-modified 值
  • if-none-match 上一次请求的返回体中的 etag 标识

跨域

协议、域名、端口任一不同即为跨域,浏览器的同源策略,不限制跨域请求的发送,但会拦截请求响应, 常用的跨域解决办法:

  • CORS,服务端设置跨域原资源共享
  • JSONP,浏览器告诉服务一个返回函数的名称,服务在返回的 script 里调用这个回调函数,同时传进客户端需要的数据, 这样返回的代码就能在浏览器上执行了
  • 子域名跨父域名时,通过更改 document.domain 为父域名即可
  • 嵌套 iframe 跨父窗口,父域无法访问不同源的 iframe 内容,但 iframe 可以向上调用 window.parent , 两者间也可以通过 postMessage API 来通信

cookie

默认情况下跨域 ajax 请求不会携带 cookie ,除非设置 withCredentials 属性为 true

返回报文的 Set-Cookie 可以设置浏览器 cookie :

  • name=value 普通键值对
  • Domain 指定 cookie 有效的域名,发送到这个域名的请求都会携带 cookie,可以指定是否包含子域名
  • Path 指定使用 cookie 的路径,请求 URL 包含这个路径才会携带 cookie
  • Expires 过期时间,即什么时间之后就不要携带 cookie 了
  • Secure 安全标志,设置之后只有使用了 SSL 安全连接的情况才会携带 cookie
  • SameSite 可设置三个值
    • Strict 完全禁止第三方 cookie
    • Lax 默认值,仅发送连接、预加载还有 GET 请求的 cookie
    • None 关闭该设置

事件循环

JavaScript 设计之初就是一门处理浏览器网页交互的脚本语言,这决定了它注定是一门单线程语言 (多个线程同时操作 ui 是很麻烦的事情)。浏览器式多线程的,当 js 主线程调用 setTimeout 和 addEventLisenter 之类的方法时,会触发其他线程(定时器触发线程、事件触发线程),以此来实现 异步非阻塞。其他线程执行完毕后会将对应的额回调函数较为任务队列维护,当 js 主线程执行栈为空时, 从这个队列中依次去除任务(回调函数)执行。新一轮的执行若有异步任务则重复以上步骤,这个过程称之为 事件循环。

宏任务与微任务

  • 宏任务:I/O、setTimeout、setInterval、setImmediate、requestAnimationFrame、 requestIdleCallback(浏览器空闲时段调用)、ajax
  • 微任务:process.nextTick(node 中)、promise、MutationObserver、MessageChannel

首屏性能优化


浏览器渲染过程

  • HTML->DOM 树,CSS->CSSOM 规则树
  • DOM 树+CSSOM 规则树=渲染树
  • 遍历渲染树,先开始布局
  • 绘制节点

浏览器加载 js

  • 默认情况下同步加载,阻塞渲染
  • defer 表示延迟执行脚本,解析到 script 标签时立即下载,等待渲染完成后再执行,并保证按出现顺序执行
  • async 表示异步执行脚本,解析到 script 标签时立即下载,下载完就执行(不管浏览器是否渲染完成,可能造成阻塞) 不保证执行顺序
  • type="module" 表示 ES6 模块,浏览器会异步加载,解析到 script 标签时立即下载,等待渲染完成后再执行, (它会受到 async 的影响,可能阻塞渲染`

框架

MVC MVP MVVM…

  • MVC
    • Model 业务数据,在更新时会通知相应的观察者(View)
    • View 视图,负责 UI 交互,是 Model 的可视化表示,它会观察相应的 Model
    • Controller 控制器,时 Model 和 View 的中介,用户操作 View 时,它通常负责更新 Model
  • MVP
    • View 和 Model 不再练习,通过 Presenter 传递
    • View 转换为被动形态,没有任何业务逻辑
    • Presenter 表示器,几乎所有的逻辑都在这里
  • MVVM
    • 与 MVP 基本一致,Presenter 变成了 ViewModel
    • ViewModel 视图模型,采用了双向绑定,View 的变动自动更新到了 ViewModel,反之亦然
    • Model 更多是由 ViewModel 负责

Vue2

Vue 源码结构

自底向上分析,以下是 Vue 导出的全过程

  1. Vue 是一个函数类,构造函数内只有一个 this._init(options) (开发模式下会判断是否作为构造函数调用,直接调用 Vue 方法会报错)
  2. 调用各种混入方法,传入 Vue ,增强(或者说组合) Vue.prototype 能力:
    1. initMixin ,该混入声明了 Vue.prototype._init ,该方法负责一系列的初始化操作 (以下过程发生在实例化中):
      initLifecycle(vm)
      initEvents(vm)
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)
      initProvide(vm) // resolve provide after data/props
      callHook(vm, 'created')
      
    2. stateMixin ,加入了数据响应相关的能力:
      1. Vue.prototype 上创建 data 和 props 选项的代理访问 $data 和 $props
      2. Vue.prototype 上增加 $set , $delete , $watch
    3. eventsMixin ,事件相关的能力:
      Vue.prototype.$on
      Vue.prototype.$once
      Vue.prototype.$off
      Vue.prototype.$emit
      
    4. lifecycleMixin ,生命周期:
      Vue.prototype._update
      Vue.prototype.$forceUpdate
      Vue.prototype.$destroy
      
    5. renderMixin ,渲染相关:
      1. Vue.prototype 上安装渲染辅助工具,(挂载了原型上的 _o , _n , _s 之类的属性上)
      2. Vue.prototype.$nextTick
      3. Vue.prototype._render
  3. 调用 initGlobalApi(Vue) 加入 Vue 全局方法或属性(函数类的静态方法或者属性):
    1. Vue.config ,全局配置
    2. Vue.util ,包含 warn , extend , mergeOptions , defineReactive 方法
    3. Vue.set , Vue.delete , Vue.nextTick ,与原型上的是一样的
    4. Vue.observeable ,2.6 版本的 API,时一个对象可响应
    5. Vue.options ,初始化了 components , directives , filters 空值选项,以及 _base=Vue , 并增加内置组件 KeepAlive
    6. 调用 initUse ,增加插件拓展能力 Vue.use (判断插件是否有 install 方法以及是否重复安装)
    7. 调用 initMixin ,增加混入能力 Vue.mixin (实际上就是将混传入的 options 与当前 options , 会影响所有 vm 实例,通常在插件中使用, Vue.use 和 Vue.mixin 方法都返回了 this ,支持链式调用)
    8. 调用 initExtend ,增加继承能力 Vue.extend (用于创建 Vue的子类 ,会在内部缓存)
    9. 调用 initAssetRegisters ,增加 Vue.component , Vue.directives , Vue.filters 这三个资源注册能力
  4. 增加 Vue.protoype 环境相关标记 $isServer , $ssrContext , 以及全局标记 Vue.FunctionalRenderContext 和 Vue.version

以上为核心 Vue 的导出过程,接下来是导出完整的运行时版本 Vue,从这一步开始,就会区分 web 和 weex 平台了 (各平台的视图相关操作不尽相同),以下以 web 为例:

  1. Vue.config 上继续添加一些相关标记
  2. 添加平台内置的指令 v-model , v-show 和组件 Transition , TransitionGroup
  3. 判断是否浏览器环境,绑定 patch 方法到 Vue.prototype.__patch__
  4. 增加 Vue.prototype.$mount 方法(其实就是调用 mountCommponent 方法)

如果是待编译器的版本,会在上述基础上继续:

  1. 将运行时 Vue.prototype.$mount 方法取出暂存,重写 $mount :
    1. 检查挂载的根元素,不能是 html 或者 body
    2. 检查渲染函数,没有渲染函数时检查 template 并转换为渲染函数,没有 template 则取挂载元素的 outerHTML 作为 template
    3. 继续调用原本的 $mount
  2. 增加 Vue.compile = compileToFunctions

Vue 的实例化过程

Vue 的构造函数中只调用了 this._init , new Vue() 这个过程详细展开为:

  1. 合并配置项,将默认配置与构造函数传入配置合并,赋值给 vm.$options
  2. initLifecycle(vm) 初始化生命周期:
    1. 挂载父节点和跟节点
    2. 初始化 vm.$children=[] 和 vm.$refs={}
    3. 初始化 vm 上生命周期的相关标记, _watcher , _inactive , _directInactive , _isMounted , _isDestroyed , _isBeingDestroyed
  3. initEvents(vm) 初始化事件:
    1. 初始化 vm._events 和 vm._hasHookEvent
    2. 判断父组件是否有事件监听,有的话进行相应的更新
  4. initRender(vm) 初始化渲染:
    1. 初始化 vm._vnode 和 vm._staticTrees 标记
    2. 初始化 vm.$slot 和 vm.$scopedSlots
    3. 挂 createElement 到 vm._c 和 vm.$createElement ( alwaysNormalize 参数不同)
    4. 调用 defineReactive 注册 vm 实例响应对象 vm.$attrs 和 vm.$listeners
  5. callHook(vm, 'beforeCreate') 触发 beforeCreate 钩子
  6. initInjections(vm) // resolve injections before data/props
    1. 读取 vm.$options 上的 injection 配置
    2. 关闭全局 shouldObserve 开关
    3. 遍历 injection ,对每个 key 使用 defineReactive
    4. 打开全局 shouldObserve 开关
  7. initState(vm) 初始化状态:
    1. 初始化 vm._watchers=[]
    2. initProps
      1. 判断是否根节点,不是的话关闭全局 shouldObserve 开关
      2. 遍历 propsOptions
        1. 判断响应的是否 propsData 是否满足条件(类型,验证等)
        2. defineReactive 注册响应对象
        3. 创建 vm 的 props 访问代理 vm._props.xxx
      3. 打开全局 shouldObserve 开关
    3. initMethods ,遍历 metheds 里的方法,对每个方法使用 bind ,绑定 vm 执行上下文 (这也是方法中 this 能访问到 vm 的原因)
    4. initData
      1. 创建 vm 的 data 访问代理 vm._data.xxx
      2. 观察数据, observe(data,true)
    5. initComputed
      1. 初始化 vm._computedWatcher
      2. 遍历计算属性,调用 defineComputed ,非 ssr 情况会生成新的 watcher 赋值给 vm._computedWatcher.xxx
    6. initWatch ,遍历 watch 创建相应的 watcher
  8. initProvide(vm) // resolve provide after data/props
  9. callHook(vm, 'created') 触发 created 钩子
  10. 判断 vm.$options.el 是否存在,是则调用 vm.$mount 进行挂载

Vue 的挂载过程

  1. 检查 vm.$options 是否有 render ,没有的话调用 createEmptyVNode 生成空节点
  2. 触发 beforeMount 钩子
  3. 声明 updateComponent 回调函数 updateComponent = () => { vm._update(vm._render(), hydrating)}
    1. vm._render() -> vm.$createdElement -> createElement
  4. 实例化 Watcher ,传入 updateComponent 回调,并注入 beforeUpdate 钩子(初始化时,默认会执行`updateComponent 钩子)
  5. 标记 vm._isMounted ,触发 mounted 钩子

响应式原理

再实例挂载的完毕中,创建了一个 watcher ,他的回调 updateComponent 会立即执行依次,调用 vm._render() 的过程中会触发响应式数据的 getter ,进行一次依赖收集,当有更新时则会派发更新。

响应式的核心是 Watcher 、 Observer 、 Dep (经典的观察者模式),大致的流程为

Vue 的实例化过程中,对 props , data 等都进行了 defineReactive 操作,该操作就是给定义一个响应式对象, 给对象动态添加 getter 和 setter

在 defineReactive 创建响应式对象的过程中,会实例化一个 Dep , Dep 类实际上是对 Watcher 的一种管理, 同时 Dep 类上有一个静态属性 target ,指向一个全局唯一的 Watcher , Watcher 类也会维护需要更新的 Dep 队列,当响应式对象的 getter 触发时,会把自己的 dep 实例加入到目标 watcher 的 dep 实例队列, 同时也会把 watcher 实例加入到 dep 的 subs 队列,这个过程则是依赖收集。

实际上 Watcher 类维护了两个 dep 实例数组,一个表示新添加的,另一个表示上一次添加的

当修改响应式对象时,则会触发相应的 setter ,它会调用 dep 的 notify 方法,然后依次调用 def 实例中的 subs 也就是 watcher 实例数组的 update 方法,将需要更新的回调加到一个队列里,在 nextTick 时后执行

讲一讲 vue2 的响应式原理

Vue 是怎么实现组件化的

从底层代码来看,我觉得组件化的核心是 Vue.extend ,

vue-router 原理

vuex 原理

Vue3

React

打包&构建&工程化

聊一聊前端工程化

很开放的问题,以下是一些可展开的点:

  • git 版本控制
  • 规范约束
    • 分支规范
    • 版本规范
    • CSS 规范(BEM、ATOM)
  • lint 校验、prettier 风格统一、husky 钩子流程做预检
  • 文档
  • 单元测试
  • 模块化
  • 自动化流程:打包、构建、发布、部署、性能测试
  • 环境区分:开发、测试、预发、线上
  • 线上监控报警
  • bug 修复流程

webpack

esbuild

rollup

vite

你可能感兴趣的:(javascript,前端,开发语言)