前端高频面试题目 -- 15k级别

1.简单描述一下 Babel 的编译过程?

首先,Babel的作用是 从一种源码到另一种源码,充当转换编译器的作用,可以简述为 解析(解析JS代码)->转换(解析和修改AST)->重建(将修改后的AST转换成另一种JS代码)

2.JavaScript 中的数组和函数在内存中是如何存储的?

  • 数组,JS里的数组主要就是 以连续内存形式存储的FixedArray、以哈希表形式存储的HashTable

  • 函数,函数属于引用数据类型,存储在堆中,在栈内存中只是存了一个地址来表示对堆内存中的引用。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

3.发布 / 订阅模式和观察者模式的区别是什么?

在观察者模式中,被观察者通常会维护一个观察者列表。当被观察者的状态发生改变时,就会通知观察者。

在发布订阅模式中,具体发布者会动态维护一个订阅者的列表:可在运行时根据程序需要开始或停止发布给对应订阅者的事件通知。

区别在于发布者本身并不维护订阅列表(它不会像观察者一样主动维护一个列表),它会将工作委派给具体发布者(相当于秘书,任何人想知道我的事情,直接问我的秘书就可以了);订阅者在接收到发布者的消息后,会委派具体的订阅者来进行相关的处理。

4.如何处理浏览器中表单项的密码自动填充问题?

表单中当input是password类型时,打开浏览器会自动填充浏览器存储的密码,在input中加入autocomplete="new-password"即可解决。之所以new-password能够解决off失效的原因是autocomplete属性的有效值只有on和off,默认值是on,如果autocomplete的属性是除on和off外的值,那么就是个无效值,那么浏览器就会放弃对该属性的执行。

5.Hash 和 History 路由的区别和优缺点?

hash 路由模式的实现主要是基于下面几个特性:

  • URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;
  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。

history 路由模式的实现主要基于存在下面几个特性:

  • pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;
  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
  • history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

6.JavaScript 中的 const 数组可以进行 push 操作吗?为什么?

可以,也可以进行splice()操作。

const声明创建一个值的只读引用。但这并不意味着它所持有的值是不可变的,只是变量标识符不能重新分配。例如,在引用内容是对象的情况下,这意味着可以改变对象的内容

7.JavaScript 中对象的属性描述符有哪些?分别有什么作用?

  • Configurable(可配置性)

可配置性决定是否可以使用delete删除属性,以及是否可以修改属性描述符的特性,默认值为true

  • Enumerable(可枚举性)

可枚举性决定属性是否出现在对象的属性枚举中,比如是否可以通过for-in循环返回该属性,默认值为true

  • Writable(可写性)

可写性决定是否可以修改属性的值,默认值为true

  • Value(属性值)

属性值包含这个属性的数据值,读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认值为undefined

  • getter

在读取属性时调用的函数。默认值为undefined

  • setter

在写入属性时调用的函数。默认值为undefined

8.JavaScript 中 console 有哪些 api ?

console.clear()

console.log()

console.info()

console.warn()

console.error()

console.time()

console.timeEnd()

9.简单对比一下 Callback、Promise、Generator、Async 几个异步 API 的优劣?

  • 首先callback不是异步API,它是早年JS异步编程实现的一种手段。
  • Promise是社区为了解决回调地狱的问题在ES6版本提出的一种解决方案;
  • enerator也是一种异步编程解决方案,它最大的特点就是可以交出函数的执行权,Generator 函数可以看出是异步任务的容器,需要暂停的地方,都用 yield 语法来标注;
  • Async/await是 ES7 中提出的新的异步解决方案,async 是 Generator 函数的语法糖,async/await 的优点是代码清晰(不像使用 Promise 的时候需要写很多 then 的方法链)。async/await 不仅仅是 JS 的异步编程的一种方式,其可读性也接近于同步代码,让人更容易理解。

10.Object.defineProperty 有哪几个参数?各自都有什么作用?

Object.defineProperty(object, propertyname, descriptor)

  • object 必需。要在其上添加或修改属性的对象。这可能是一个本机 JavaScript对象(即用户定义的对象或内置对象)或 DOM 对象。
  • propertyname 必需。一个包含属性名称的字符串。
  • descriptor 必需。属性描述符。它可以针对数据属性或访问器属性。

它内部的descriptor参数如下:

  • value

属性的值,默认为 undefined。

  • writable

该属性是否可写,如果设置成 false,则任何对该属性改写的操作都无效(但不会报错),对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。

  • configurable

如果为false,则任何尝试删除目标属性或修改属性以下特性(writable, configurable, enumerable)的行为将被无效化,对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。。

  • enumerable

是否能在for-in循环中遍历出来或在Object.keys中列举出来。对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。

  • get

一旦目标对象访问该属性,就会调用这个方法,并返回结果。默认为 undefined。

  • set

一旦目标对象设置该属性,就会调用这个方法。默认为 undefined。

11.Object.defineProperty 和 ES6 的 Proxy 有什么区别?

Proxy的优势如下

  • Proxy可以直接监听整个对象而非属性。
  • Proxy可以直接监听数组的变化。
  • Proxy有13中拦截方法,如ownKeys、deleteProperty、has 等是 Object.defineProperty 不具备的。
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;
  • Proxy做为新标准将受到浏览器产商重点持续的性能优化,也就是传说中的新标准的性能红利。

Object.defineProperty 的优势如下

  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平。

Object.defineProperty 不足在于:

  • Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
  • Object.defineProperty不能监听数组。是通过重写数据的那7个可以改变数据的方法来对数组进行监听的。
  • Object.defineProperty 也不能对 es6 新产生的 Map,Set 这些数据结构做出监听。
  • Object.defineProperty也不能监听新增和删除操作,通过 Vue.set()和 Vue.delete来实现响应式的。

12.为什么要使用 TypeScript ? TypeScript 相对于 JavaScript 的优势是什么?

首先,不一定非要用TS,大型业务产品、多人协作写大堆的业务代码不适合TS。

1.为JavaScript、IDE和实践(如静态检查)提供了高效的开发工具。(主要)

2.其他的比如强大的类型系统,泛型支持、模块支持等等(次要)

13.Vue 2.x 模板中的指令是如何解析实现的?

指令本质上就是一个 JavaScript 对象,对象上挂着一些钩子函数,无论是官方提供的指令,还是自定义指令,一个指令从第一次被绑定到元素上到最终与被绑定的元素解绑,它会经过以下几种状态:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

了每个状态的钩子函数,这样我们就可以让指令在不同状态下做不同的事情。当虚拟DOM渲染更新的时候会触发create、update、destory这三个钩子函数,从而就会执行updateDirectives函数来处理指令的相关逻辑,执行指令函数,让指令生效。

14.简要说明 Vue 2.x 的全链路运作机制?

  • 初始化以及挂载init, mount
  • 在进行模板编译compile,将template编译为渲染函数render function
  • 执行render function生成Virtual DOM, render function => VNode tree
  • 再进行响应式依赖收集,render function => getter, setter => Watcher.update => patch。以及使用队列进行异步更新的策略。
  • 最后通过diff算法后进行patch更新视图

15.如何理解 Vue 是一个渐进式框架?

渐进式代表的含义是:没有多做职责之外的事。

你可以使用jsx开发,你也可以写template;你可以使用vue全家桶,你也可以把它做为某个业务的轻量视图,随你,不强求不主张。

16.Vue 里实现跨组件通信的方式有哪些?

  • 父子通信:

父向子传递数据是通过 props,子向父是通过 events(emit);通过父链/子链也可以通信(emit);通过父链 / 子链也可以通信(emit);通过父链/子链也可以通信(parent / children);ref也可以访问组件实例;provide/injectAPI;children);ref 也可以访问组件实例;provide / inject API;children);ref也可以访问组件实例;provide/injectAPI;attrs/$listeners

  • 兄弟通信:

Bus;Vuex

  • 跨级通信:

Bus;Vuex;provide / inject API、attrs/attrs/attrs/listeners

17.Vue 中响应式数据是如何做到对某个对象的深层次属性的监听的?

使用watch并且搭配deep:true 就可以实现对对象的深度监听

18.MVVM、MVC 和 MVP 的区别是什么?各自有什么应用场景?

MVC 是一种使用 MVC(Model View Controller 模型-视图-控制器)设计创建 Web 应用程序的模式。

  • 耦合性低
  • 重用性高
  • 生命周期成本低

MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。

  • 模型与视图完全分离,我们可以修改视图而不影响模型
  • 可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部
  • 我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。
  • 如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)

MVVM 本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

  • 低耦合
  • 可重用性
  • 独立开发
  • 可测试

19.什么是 MVVM 框架?

MVVM,特点是采用双向绑定(data-binding): View的 变动,自动反映在View Model,反之亦然。这样开发者就不用处理接收事件和View更新的工作,框架已经帮你做好了。

20.Vue CLI 3.x 有哪些功能?Vue CLI 3.x 的插件系统了解?

插件系统是给vue项目提供可选功能的npm包,如:Babel/TypeScript 转译、ESLint 集成、unit和 e2e测试 等

21.Vue CLI 3.x 中的 Webpack 是如何组装处理的?

对比vue-cli2,cli3 最主要的就是生成的项目中,进行webpack配置的文件没有了。cli3的脚手架封装了webpack绝大部分配置,使得生成的项目更加清晰,但是在开发中免不了会有自己的个性需求,来添加一些自己的项目配置,此时只需在项目的根目录下新建一个vue.config.js文件即可。而webpack中是通过 resolve.alias 来实现此功能的。在vue.config.js中修改webpack的配置,可以通过configureWebpack方法。

22.Vue 2.x 如何支持 TypeScript 语法?

  • 配置ts-loader,tsconfig
  • 增加类型扩展,让ts识别vue文件
  • vue文件中script里面换成ts写法, 需要增加几个ts扩展的package, 比如vue-property-decorator

23.Webpack 中 Loader 和 Plugin 的区别是什么?

在webpack中 Loader 就是负责完成项目中各种各样资源模块的加载,从而实现整体项目的模块化,而 Plugin 则是用来解决项目中除了资源模块打包以外的其他自动化工作,对比 Loader 只是在模块的加载环节工作,而插件的作用范围几乎可以触及 Webpack 工作的每一个环节。

24.如何发布开发项目的特定文件夹为 Npm 包的根目录?

一般情况下,npm包的根目录时node_modules,可以使用package.json的directories属性里的directories.lib,更改 Npm 包的根目录。

25.Npm 包中 peerDependencies 的作用是什么?

peerDependencies的目的是提示宿主环境去安装满足插件peerDependencies所指定依赖的包,然后在插件import或者require所依赖的包的时候,永远都是引用宿主环境统一安装的npm包,最终解决插件与所依赖包不一致的问题。

26.如何优雅的调试需要发布的 Npm 包?

  • 在需要调试的npm包目录下结构下的控制台输入npm link 这个命令会把当前包映射到本地的一个全局的npm包里面;
  • 在引用的目录结构下的控制台输入 npm link 包名称 这个命令会把本地引用的这个npm包的路径定位到全局的npm包下;
  • 全局的npm包相当于一个中转站,在编辑区域与引用区域之间中转。

27.了解 Git (Submodule)子模块吗?简单介绍一下 Git 子模块的作用?

子模块是进行开发和需求进行对接将需求文档作为子模块项目,嵌入开发人员的项目中。子模块的使用既可以减少需求或设计人员的git操作,又可以及时的将doc文档发布到项目的目录文件下,而且不会对开发人员的项目产生任何影响。

28.Git 如何修改已经提交的 Commit 信息?

  • git rebase -i <commit id> 列出 commit 列表
  • 找到需要修改的 commit 记录,把 pick 修改为 edit 或 e,:wq 保存退出
  • 修改 commit 的具体信息git commit --amend,保存并继续下一条git rebase --continue,直到全部完成
  • 中间也可跳过或退出git rebase (–skip | --abort)

29.Git 如何撤销 Commit 并保存之前的修改?

  • 查看commit git log --pretty=oneline
  • 撤销到上一个commit,但是保存当前的修改。git reset --soft <commit>
  • 修改成功。重建分支,进行提交。

30.Git 如何 ignore 被 commit 过的文件?

  • 删除 track 的文件 (已经 commit 的文件)
  • 在 .gitignore 文件中添加忽略规则

在 .gitignore 文件中添加 ignore 条目, 如: .DS_Store 提交 .gitignore 文件: git commit -a -m “添加ignore规则”

  • 推送到远程仓库让 ignore 规则对于其他开发者也能生效

31.在使用 Git 的时候如何规范 Git 的提交说明(Commit 信息)?

用Commitizen,Commitizen 是一个撰写符合上面 Commit Message 标准的一款工具。在push操作时检查commit的信息,使用正则检查是否匹配(比如使用angular的git规范),不符合的不允许Push。

32.ESLint 和 Prettier 的区别是什么?两者在一起工作时会产生问题吗?

这俩解决的不是一个问题,ESLint 主要解决的是代码质量问题;Prettier主要解决的是代码风格问题。两者在一起会产生问题。

33.如何调试 Node.js 代码?如何调试 Node.js TypeScript 代码?在浏览器中如何调试 Node.js 代码?

从nodejs8开始,node去掉了_debugger , 内部集成了inspect , 以往使用node-inspect实现的在线调试不再可用.node8开始要用新方法了。

  • 在服务端用inspect模式运行nodejs
node --inspect-brk=0.0.0.0:8080 index.js
  • 打开chrome浏览器 地址栏输入chrome://inspect,在弹出的界面中输入ip:port即可调试。

34.CDN 服务如何实现网络加速?

CDN的工作原理就是将您源站的资源缓存到位于全球各地的CDN节点上,用户请求资源时,就近返回节点上缓存的资源,而不需要每个用户的请求都回您的源站获取,避免网络拥塞、缓解源站压力,保证用户访问资源的速度和体验。

35.WebSocket 使用的是 TCP 还是 UDP 协议?

是基于TCP的,websocket的协议是在TCP/IP协议簇的应用层,和http在同一层。

36.什么是单工、半双工和全双工通信?

  • 单工数据传输只支持数据在一个方向上传输;
  • 半双工数据传输允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;
  • 全双工数据通信允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。

37.简单描述 HTTP 协议发送一个带域名的 URL 请求的协议传输过程?(DNS、TCP、IP、链路)

38.输入地址回车后

  • 浏览器通过请求得到一个HTML文本
  • 渲染进程解析HTML文本,构建DOM树
  • 解析HTML的同时,如果遇到内联样式或者样式脚本,则下载并构建样式规则(stytle rules),若遇到JavaScript脚本,则会下载执行脚本。
  • DOM树和样式规则构建完成之后,渲染进程将两者合并成渲染树(render tree)
  • 渲染进程开始对渲染树进行布局,生成布局树(layout tree)
  • 渲染进程对布局树进行绘制,生成绘制记录
  • 渲染进程的对布局树进行分层,分别栅格化每一层,并得到合成帧
  • 渲染进程将合成帧信息发送给GPU进程显示到页面中

39.Cookie 可以在服务端生成吗?Cookie 在服务端生成后的工作流程是什么样的?

可以。HTTP 协议中的 Cookie 包括 Web Cookie 和浏览器 Cookie,它是服务器发送到 Web 浏览器的一小块数据。服务器发送到浏览器的 Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器。通常,它用于判断两个请求是否来自于同一个浏览器,例如用户保持登录状态。

40.Session、Cookie 的区别和关联?如何进行临时性和永久性的 Session 存储?

  • Session

客户端请求服务端,服务端会为这次请求开辟一块内存空间,这个对象便是 Session 对象,存储结构为 ConcurrentHashMap。Session 弥补了 HTTP 无状态特性,服务器可以利用 Session 存储客户端在同一个会话期间的一些操作记录。

  • Cookies

HTTP 协议中的 Cookie 包括 Web Cookie 和浏览器 Cookie,它是服务器发送到 Web 浏览器的一小块数据。服务器发送到浏览器的 Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器。通常,它用于判断两个请求是否来自于同一个浏览器,例如用户保持登录状态。

服务器端session,如果你不指定session的存储时间,在你打开的浏览器中存储的值,是可以在新打开的框口内得到的,关闭后就自动消失(消失的其实是session_id,因为session的机制是依赖于cookie的(还可以依赖其他的)。

41.设置 Cookie 时候如何防止 XSS 攻击?

在服务器端设置cookie的时候设置 http-only, 这样就可以防止用户通过JS获取cookie。对cookie的读写或发送一般有如下字段进行设置:

  • http-only: 只允许http或https请求读取cookie、JS代码是无法读取cookie的(document.cookie会显示http-only的cookie项被自动过滤掉)。发送请求时自动发送cookie.
  • secure-only: 只允许https请求读取,发送请求时自动发送cookie。
  • host-only: 只允许主机域名与domain设置完成一致的网站才能访问该cookie。

设置Cookie,可以防止攻击者拿到正常用户的Cookie冒充身份非法调用网站接口。

42.简单描述一下用户免登陆的实现过程?可能会出现哪些安全性问题?一般如何对用户登录的密码进行加密?

在用户第一次登录成功的时候,后端会返回一个 Token,这个值Token 主要的作用就是用于识别用户的身份。相当于账号密码。正常情况下,前端给后端发送请求的时候,后端都需要先判断用户的身份,来返回相应的数据给用户。获取到Token后,你需要把 Token 存在 Cookie中。接着向服务器发送请求时,你从 Cookie 中取出 Token,在请求头中携带上 Token 。Token过期时间设置足够长,只要token没过期,这段时间用户都是免登录。

安全问题:其他人使用本机,实现免登录,无法在每次使用应用时验证用户的身份。提供了便捷,失去了安全校验。

对用户登录的密码进行加密,密码MD5化,不使用明文传输。

43.HTTP 中提升传输速率的方式有哪些?常用的内容编码方式有哪些?

  • 使用压缩技术把实体主体压小,在客户端再把数据解析。
  • 使用分块传输编码,将实体主体分块传输,当浏览器解析到实体主体就能够显示了。

常用的内容编码方式:

  • 非归零码
  • 曼彻斯特编码
  • 差分曼彻斯特编码

44.传输图片的过程中如果突然中断,如何在恢复后从之前的中断中恢复传输?

文件的断点续传,

前端工作

  • 为每一个文件切割块添加不同的标识
  • 当上传成功的之后,记录上传成功的标识
  • 当我们暂停或者发送失败后,可以重新发送没有上传成功的切割文件

后端工作

  • 接收每一个切割文件,并在接收成功后,存到指定位置,并告诉前端接收成功
  • 收到合并信号,将所有的切割文件排序,合并,生成最终的大文件,然后删除切割小文件,并告知前端大文件的地址

45.什么是代理?什么是网关?代理和网关的作用是什么?

代理是中间人,使用代理的主机发出的IP报文的目的IP是代理的,但是会在应用层里明确告诉代理,自己真实需求是什么。网关即Gateway,它是连接基于不同通信协议的网络的设备,使文件可以在这些网络之间传输。

46.HTTPS 相比 HTTP 为什么更加安全可靠?

因为 HTTPS 保证了传输安全,防止传输过程被监听、防止数据被窃取,可以确认网站的真实性(具体细节二面再说)。不过需要注意的是,即便使用 HTTPS 仍可能会被抓包,因为HTTPS 只防止用户在不知情的情况下通信被监听,如果用户主动授信,是可以构建“中间人”网络,代理软件可以对传输内容进行解密。

47.什么是对称密钥(共享密钥)加密?什么是非对称密钥(公开密钥)加密?哪个更加安全?

传统的对称式加密需要通讯双方都保存同一份密钥,通过这份密钥进行加密和解密。所以非对称加密也称为单密钥加密。在非对称加密中,加密和解密使用的是不同的密钥。非对称加密中的密钥分为公钥和私钥。公钥顾名思义就是公开的,任何人都可以通过公钥进行信息加密,但是只有用户私钥的人才能完成信息解密。非对称加密带来了一个好处,避免了对称式加密需要传输和保存同一份密钥的痛苦。

非对称加密一定比对称加密机密性更高吗? 不一定, 因为机密性高低是根据秘钥长度而变化的。而且非对称加密最大的问题,就是性能较差,无法应用于长期的通信。

48.你觉得 HTTP 协议目前存在哪些缺点?

HTTP不具备必要的安全功能,与最初的设计相比,现今的Web网站应用的HTTP协议的使用方式已发生了翻天覆地的变化。几乎现今所有的Web网站都会使用会话(session)管理、加密处理等安全性方面的功能,而HTTP协议内并不具备这些功能。

从整体上看,HTTP就是一个通用的单纯协议机制。因此它具备较多优势,但是在安全性方面则呈劣势。就拿远程登录时会用到的SSH协议来说,SSH具备协议级别的认证及会话管理等功能,HTTP协议则没有。另外在架设SSH服务方面,任何人都可以轻易地创建安全等级高的服务,而HTTP即使已架设好服务器,但若想提供服务器基础上的Web应用,很多情况下都需要重新开发。

因此,开发者需要自行设计并开发认证及会话管理功能来满足Web应用的安全。而自行设计就意味着会出现各种形形色色的实现。结果,安全等级并不完备,可仍在运作的Web应用背后却隐藏着各种容易被攻击者滥用的安全漏洞的Bug。

49.Webpack 中的插件机制是如何设计的?

Webpack 插件机制的目的是为了增强 Webpack 在项目自动化构建方面的能力。在webpack中 Loader 就是负责完成项目中各种各样资源模块的加载,从而实现整体项目的模块化,而 Plugin 则是用来解决项目中除了资源模块打包以外的其他自动化工作,对比 Loader 只是在模块的加载环节工作,而插件的作用范围几乎可以触及 Webpack 工作的每一个环节。

Webpack 的插件机制就是我们在软件开发中最常见的钩子机制。钩子机制也特别容易理解,它有点类似于 Web 中的事件。在 Webpack 整个工作过程会有很多环节,为了便于插件的扩展,Webpack 几乎在每一个环节都埋下了一个钩子。这样我们在开发插件的时候,通过往这些不同节点上挂载不同的任务,就可以轻松扩展 Webpack 的能力。

50.如何提升 Node.js 代码的运行稳定性?

  • 保障进程安全

由于一个用户的异常访问或者数据异常,加上没有做好异常处理和安全保护,直接导致了整个 Node.js 服务重启了,从而中断了所有人的请求,用户体验非常差。①由于 Node.js 使用的是 JavaScript,而JavaScript 是一个弱类型语言,因此在现网经常会引发一些由代码逻辑的异常导致的进程异常退出。②其次在 Node.js 中也经常会因为内存的使用不当,导致内存泄漏,当在 64 位系统中达到 1.4 G(32 位系统 0.7 G)时,Node.js 就会异常崩溃。③再而由于Node.js 的 I/O 较多也较为频繁,当启用较多 I/O 句柄,但是没有及时释放,同样会引发进程问题。

  • parameters error

关于 JSON.parse 很多时候我们都比较自然地将其他接口或者第三方的数据拿来解析,但是这里往往会忽略其非 JSON 字符串的问题,在这里需要进行try catch 异常判断。

  • other errors

当前 Node.js 的 Promise 应用越来越广泛了,因此对于 Promise 的 catch 也应该多进行重视,对于每个 Promise 都应该要处理其异常 catch 逻辑,不然系统会提示 warning 信息。还有一些常见的长连接的服务,比如 Socket、Redis、Memcache 等等,我们需要在连接异常时进行处理,如果没有处理同样会导致异常,比如 Socket 提供了 Socket.on(‘error’) 的监听。

  • 注意服务异常方面的内存泄露

设置最大临时缓存数,超出则不使用缓存;设置最大缓存句柄数,超出则不使用缓存;定时清理当前的临时缓存和句柄缓存。

  • 避免全局变量

一般情况下不建议使用全局变量,全局变量必须要有一定的上限和清理规则才能保证服务的安全。

  • 避免单例模块中的变量内存泄漏

要注意一个点,有些模块我们使用单例的模式,就是在每次 require 后都返回这个对象,这种情况也比较容易引发内存泄漏的问题。因为单例模式会引发每个用户访问的数据的叠加。

  • 主动关闭打开文件后未关闭的情况

一般打开文件句柄后,我们都应该主动关闭,如果未主动关闭,就会导致文件句柄越来越多,从而引发句柄泄漏问题。

51.Vue SSR 的工作原理?Vuex 的数据如何同构渲染?

在Vue SSR中,创建Vue实例、创建store和创建router都是套了一层工厂函数的,目的就是避免数据的交叉污染。在服务端只能执行生命周期中的created和beforeCreate,原因是在服务端是无法操纵dom。服务端渲染和客户端渲染不同,需要创建两个entry分别跑在服务端和客户端,并且需要webpack对其分别打包;SSR服务端请求不带cookie,需要手动拿到浏览器的cookie传给服务端的请求。SSR要求dom结构规范,因为浏览器会自动给HTML添加一些结构比如tbody,但是客户端进行混淆服务端放回的HTML时,不会添加这些标签,导致混淆后的HTML和浏览器渲染的HTML不匹配。

对于同构应用来说,我们必须实现客户端与服务端的路由、模型组件、数据模型的共享。Vuex是实现我们客户端和服务端的状态共享的关键,我们可以不使用vuex,但是我们得去实现一套数据预取的逻辑;可以尝试封装一个可以给组件们共享的EventBus,在main.js中export出我们的EventBus以便两个entry使用,接下来是我们的两个entry了。server用来匹配我们的组件并调用组件的asyncData方法去获取数据,client用来将预渲染的数据存储到我们eventBus中的data中。这样就相当于实现类Vuex的功能。

52.SSR 技术和 SPA 技术的各自的优缺点是什么?

SSR:

  • 更利于SEO。
  • 更利于首屏渲染
  • 服务端压力较大

SPA:

  • 页面之间的切换非常快
  • 一定程度上减少了后端服务器的压力(不用管页面逻辑和渲染)
  • 后端程序只需要提供API,完全不用管客户端到底是Web界面还是手机等
  • 不利于SEO
  • 首屏加载压力大

53.什么是单点登录?如何做单点登录?

单点登录是指在同一帐号平台下的多个应用系统中,用户只需登录一次,就可访问所有相互信任的应用系统。比如你在网页中登录了百度云盘,随后你再去贴吧发帖 是不需要二次登录的。

单点登录的本质就是在多个应用系统中共享登录状态。如果用户的登录状态是记录在 Session 中的,要实现共享登录状态,就要先共享 Session,比如可以将 Session 序列化到 Redis 中,让多个应用系统共享同一个 Redis,直接读取 Redis 来获取 Session。

因为不同的应用系统有着不同的域名,尽管 Session 共享了,但是一个企业不同应用的域名不同,依然可能出现跨站or跨域。

前端方面的实现方式:

  • 父域 Cookie
  • 认证中心
  • LocalStorage 跨域

手写

01.数组扁平化

const arr = [1, [2, [3, [4, 5]]], 6];

方法一:使用flat()

const res1 = arr.flat(Infinity);

方法二:使用reduce

const flatten = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, [])
}
const res4 = flatten(arr);

方法五:函数递归

const res5 = [];
const fn = arr => {
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      fn(arr[i]);
    } else {
      res5.push(arr[i]);
    }
  }
}
fn(arr);

02.数组去重

const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];

方法一:利用Set

const res1 = Array.from(**new** Set(arr));

方法二:两层for循环+splice

const unique1 = arr => {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1);
        // 每删除一个树,j--保证j的值经过自加后不变。同时,len--,减少循环次数提升性能
        len--;
        j--;
      }
    }
  }
  return arr;
}

方法三:利用indexOf

const unique2 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
  }
  return res;
}
复制代码

当然也可以用include、filter,思路大同小异。

方法四:利用include

const unique3 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!res.includes(arr[i])) res.push(arr[i]);
  }
  return res;
}
复制代码

方法五:利用filter

const unique4 = arr => {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}
复制代码

方法六:利用Map

const unique5 = arr => {
  const map = new Map();
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!map.has(arr[i])) {
      map.set(arr[i], true)
      res.push(arr[i]);
    }
  }
  return res;
}

03.类数组转化为数组

类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有arguments、DOM操作方法返回的结果。

方法一:Array.from

Array.from(document.querySelectorAll('div'))

方法二:Array.prototype.slice.call()

Array.prototype.slice.call(document.querySelectorAll('div'))

方法三:扩展运算符

[...document.querySelectorAll('div')]

方法四:利用concat

Array.prototype.concat.apply([], document.querySelectorAll('div'));

04.Array.prototype.filter()

img

Array.prototype.filter = function(callback, thisArg) {
  if (this == undefined) {
    throw new TypeError('this is null or not undefined');
  }
  if (typeof callback !== 'function') {
    throw new TypeError(callback + 'is not a function');
  }
  const res = [];
  // 让O成为回调函数的对象传递(强制转换对象)
  const O = Object(this);
  // >>>0 保证len为number,且为正整数
  const len = O.length >>> 0;
  for (let i = 0; i < len; i++) {
    // 检查i是否在O的属性(会检查原型链)
    if (i in O) {
      // 回调函数调用传参
      if (callback.call(thisArg, O[i], i, O)) {
        res.push(O[i]);
      }
    }
  }
  return res;
}

05.Array.prototype.map()

img

Array.prototype.map = function(callback, thisArg) {
  if (this == undefined) {
    throw new TypeError('this is null or not defined');
  }
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  const res = [];
  // 同理
  const O = Object(this);
  const len = O.length >>> 0;
  for (let i = 0; i < len; i++) {
    if (i in O) {
      // 调用回调函数并传入新数组
      res[i] = callback.call(thisArg, O[i], i, this);
    }
  }
  return res;
}

06.Array.prototype.forEach()

img

Array.prototype.forEach = function(callback, thisArg) {
  if (this == null) {
    throw new TypeError('this is null or not defined');
  }
  if (typeof callback !== "function") {
    throw new TypeError(callback + ' is not a function');
  }
  const O = Object(this);
  const len = O.length >>> 0;
  let k = 0;
  while (k < len) {
    if (k in O) {
      callback.call(thisArg, O[k], k, O);
    }
    k++;
  }
}

07.Array.prototype.reduce()

img

Array.prototype.reduce = function(callback, initialValue) {
  if (this == undefined) {
    throw new TypeError('this is null or not defined');
  }
  if (typeof callback !== 'function') {
    throw new TypeError(callbackfn + ' is not a function');
  }
  const O = Object(this);
  const len = this.length >>> 0;
  let accumulator = initialValue;
  let k = 0;
  // 如果第二个参数为undefined的情况下
  // 则数组的第一个有效值作为累加器的初始值
  if (accumulator === undefined) {
    while (k < len && !(k in O)) {
      k++;
    }
    // 如果超出数组界限还没有找到累加器的初始值,则TypeError
    if (k >= len) {
      throw new TypeError('Reduce of empty array with no initial value');
    }
    accumulator = O[k++];
  }
  while (k < len) {
    if (k in O) {
      accumulator = callback.call(undefined, accumulator, O[k], k, O);
    }
    k++;
  }
  return accumulator;
}

08.Function.prototype.apply()

第一个参数是绑定的this,默认为window,第二个参数是数组或类数组

Function.prototype.apply = function(context = window, args) {
  if (typeof this !== 'function') {
    throw new TypeError('Type Error');
  }
  const fn = Symbol('fn');
  context[fn] = this;

  const res = context[fn](...args);
  delete context[fn];
  return res;
}

09.Function.prototype.call

call唯一不同的是,call()方法接受的是一个参数列表

Function.prototype.call = function(context = window, ...args) {
  if (typeof this !== 'function') {
    throw new TypeError('Type Error');
  }
  const fn = Symbol('fn');
  context[fn] = this;

  const res = context[fn](...args);
  delete context[fn];
  return res;
}

10.Function.prototype.bind

Function.prototype.bind = function(context, ...args) {
  if (typeof this !== 'function') {
    throw new Error("Type Error");
  }
  // 保存this的值
  var self = this;

  return function F() {
    // 考虑new的情况
    if(this instanceof F) {
      return new self(...args, ...arguments)
    }
    return self.apply(context, [...args, ...arguments])
  }
}

11.debounce(防抖)

触发高频时间后n秒内函数只会执行一次,如果n秒内高频时间再次触发,则重新计算时间。

const debounce = (fn, time) => {
  let timeout = null;
  return function() {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      fn.apply(this, arguments);
    }, time);
  }
};

防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次。

12.throttle(节流)

高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。

const throttle = (fn, time) => {
  let flag = true;
  return function() {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, arguments);
      flag = true;
    }, time);
  }
}
复制代码

节流常应用于鼠标不断点击触发、监听滚动事件。

13.Object.assign

Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象(请注意这个操作是浅拷贝)

Object.defineProperty(Object, 'assign', {
  value: function(target, ...args) {
    if (target == null) {
      return new TypeError('Cannot convert undefined or null to object');
    }
    
    // 目标对象需要统一是引用数据类型,若不是会自动转换
    const to = Object(target);

    for (let i = 0; i < args.length; i++) {
      // 每一个源对象
      const nextSource = args[i];
      if (nextSource !== null) {
        // 使用for...in和hasOwnProperty双重判断,确保只拿到本身的属性、方法(不包含继承的)
        for (const nextKey in nextSource) {
          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
    }
    return to;
  },
  // 不可枚举
  enumerable: false,
  writable: true,
  configurable: true,
})

14.深拷贝

const cloneDeep1 = (target, hash = new WeakMap()) => {
  // 对于传入参数处理
  if (typeof target !== 'object' || target === null) {
    return target;
  }
  // 哈希表中存在直接返回
  if (hash.has(target)) return hash.get(target);

  const cloneTarget = Array.isArray(target) ? [] : {};
  hash.set(target, cloneTarget);

  // 针对Symbol属性
  const symKeys = Object.getOwnPropertySymbols(target);
  if (symKeys.length) {
    symKeys.forEach(symKey => {
      if (typeof target[symKey] === 'object' && target[symKey] !== null) {
        cloneTarget[symKey] = cloneDeep1(target[symKey]);
      } else {
        cloneTarget[symKey] = target[symKey];
      }
    })
  }

  for (const i in target) {
    if (Object.prototype.hasOwnProperty.call(target, i)) {
      cloneTarget[i] =
        typeof target[i] === 'object' && target[i] !== null
        ? cloneDeep1(target[i], hash)
        : target[i];
    }
  }
  return cloneTarget;
}

15.Promise

// 模拟实现Promise
// Promise利用三大手段解决回调地狱:
// 1. 回调函数延迟绑定
// 2. 返回值穿透
// 3. 错误冒泡

// 定义三种状态
const PENDING = 'PENDING';      // 进行中
const FULFILLED = 'FULFILLED';  // 已成功
const REJECTED = 'REJECTED';    // 已失败

class Promise {
  constructor(exector) {
    // 初始化状态
    this.status = PENDING;
    // 将成功、失败结果放在this上,便于then、catch访问
    this.value = undefined;
    this.reason = undefined;
    // 成功态回调函数队列
    this.onFulfilledCallbacks = [];
    // 失败态回调函数队列
    this.onRejectedCallbacks = [];

    const resolve = value => {
      // 只有进行中状态才能更改状态
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        // 成功态函数依次执行
        this.onFulfilledCallbacks.forEach(fn => fn(this.value));
      }
    }
    const reject = reason => {
      // 只有进行中状态才能更改状态
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        // 失败态函数依次执行
        this.onRejectedCallbacks.forEach(fn => fn(this.reason))
      }
    }
    try {
      // 立即执行executor
      // 把内部的resolve和reject传入executor,用户可调用resolve和reject
      exector(resolve, reject);
    } catch(e) {
      // executor执行出错,将错误内容reject抛出去
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function'? onRejected :
      reason => { throw new Error(reason instanceof Error ? reason.message : reason) }
    // 保存this
    const self = this;
    return new Promise((resolve, reject) => {
      if (self.status === PENDING) {
        self.onFulfilledCallbacks.push(() => {
          // try捕获错误
          try {
            // 模拟微任务
            setTimeout(() => {
              const result = onFulfilled(self.value);
              // 分两种情况:
              // 1. 回调函数返回值是Promise,执行then操作
              // 2. 如果不是Promise,调用新Promise的resolve函数
              result instanceof Promise ? result.then(resolve, reject) : resolve(result);
            })
          } catch(e) {
            reject(e);
          }
        });
        self.onRejectedCallbacks.push(() => {
          // 以下同理
          try {
            setTimeout(() => {
              const result = onRejected(self.reason);
              // 不同点:此时是reject
              result instanceof Promise ? result.then(resolve, reject) : resolve(result);
            })
          } catch(e) {
            reject(e);
          }
        })
      } else if (self.status === FULFILLED) {
        try {
          setTimeout(() => {
            const result = onFulfilled(self.value);
            result instanceof Promise ? result.then(resolve, reject) : resolve(result);
          });
        } catch(e) {
          reject(e);
        }
      } else if (self.status === REJECTED) {
        try {
          setTimeout(() => {
            const result = onRejected(self.reason);
            result instanceof Promise ? result.then(resolve, reject) : resolve(result);
          })
        } catch(e) {
          reject(e);
        }
      }
    });
  }
  catch(onRejected) {
    return this.then(null, onRejected);
  }
  static resolve(value) {
    if (value instanceof Promise) {
      // 如果是Promise实例,直接返回
      return value;
    } else {
      // 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLED
      return new Promise((resolve, reject) => resolve(value));
    }
  }
  static reject(reason) {
    return new Promise((resolve, reject) => {
      reject(reason);
    })
  }
  static all(promiseArr) {
    const len = promiseArr.length;
    const values = new Array(len);
    // 记录已经成功执行的promise个数
    let count = 0;
    return new Promise((resolve, reject) => {
      for (let i = 0; i < len; i++) {
        // Promise.resolve()处理,确保每一个都是promise实例
        Promise.resolve(promiseArr[i]).then(
          val => {
            values[i] = val;
            count++;
            // 如果全部执行完,返回promise的状态就可以改变了
            if (count === len) resolve(values);
          },
          err => reject(err),
        );
      }
    })
  }
  static race(promiseArr) {
    return new Promise((resolve, reject) => {
      promiseArr.forEach(p => {
        Promise.resolve(p).then(
          val => resolve(val),
          err => reject(err),
        )
      })
    })
  }
}

16.Promise.all

Promise.all是支持链式调用的,本质上就是返回了一个Promise实例,通过resolvereject来改变实例状态。

Promise.myAll = function(promiseArr) {
  return new Promise((resolve, reject) => {
    const ans = [];
    let index = 0;
    for (let i = 0; i < promiseArr.length; i++) {
      promiseArr[i]
      .then(res => {
        ans[i] = res;
        index++;
        if (index === promiseArr.length) {
          resolve(ans);
        }
      })
      .catch(err => reject(err));
    }
  })
}

17.Promise.race

Promise.race = function(promiseArr) {
  return new Promise((resolve, reject) => {
    promiseArr.forEach(p => {
      // 如果不是Promise实例需要转化为Promise实例
      Promise.resolve(p).then(
        val => resolve(val),
        err => reject(err),
      )
    })
  })
}

18.JSONP

const jsonp = ({ url, params, callbackName }) => {
  const generateUrl = () => {
    let dataSrc = '';
    for (let key in params) {
      if (Object.prototype.hasOwnProperty.call(params, key)) {
        dataSrc += `${key}=${params[key]}&`;
      }
    }
    dataSrc += `callback=${callbackName}`;
    return `${url}?${dataSrc}`;
  }
  return new Promise((resolve, reject) => {
    const scriptEle = document.createElement('script');
    scriptEle.src = generateUrl();
    document.body.appendChild(scriptEle);
    window[callbackName] = data => {
      resolve(data);
      document.removeChild(scriptEle);
    }
  })
}

19.AJAX

const getJSON = function(url) {
  return new Promise((resolve, reject) => {
    const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
    xhr.open('GET', url, false);
    xhr.setRequestHeader('Accept', 'application/json');
    xhr.onreadystatechange = function() {
      if (xhr.readyState !== 4) return;
      if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    }
    xhr.send();
  })
}

20.图片懒加载

function lazyload() {
  const imgs = document.getElementsByTagName('img');
  const len = imgs.length;
  // 视口的高度
  const viewHeight = document.documentElement.clientHeight;
  // 滚动条高度
  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;
  for (let i = 0; i < len; i++) {
    const offsetHeight = imgs[i].offsetTop;
    if (offsetHeight < viewHeight + scrollHeight) {
      const src = imgs[i].dataset.src;
      imgs[i].src = src;
    }
  }
}

// 可以使用节流优化一下
window.addEventListener('scroll', lazyload);

21.滚动加载

原理就是监听页面滚动事件,分析clientHeightscrollTopscrollHeight三者的属性关系。

window.addEventListener('scroll', function() {
  const clientHeight = document.documentElement.clientHeight;
  const scrollTop = document.documentElement.scrollTop;
  const scrollHeight = document.documentElement.scrollHeight;
  if (clientHeight + scrollTop >= scrollHeight) {
    // 检测到滚动至页面底部,进行后续操作
    // ...
  }
}, false);

22.渲染几万条数据不卡住页面

渲染大数据时,合理使用createDocumentFragmentrequestAnimationFrame,将操作切分为一小段一小段执行。

setTimeout(() => {
  // 插入十万条数据
  const total = 100000;
  // 一次插入的数据
  const once = 20;
  // 插入数据需要的次数
  const loopCount = Math.ceil(total / once);
  let countOfRender = 0;
  const ul = document.querySelector('ul');
  // 添加数据的方法
  function add() {
    const fragment = document.createDocumentFragment();
    for(let i = 0; i < once; i++) {
      const li = document.createElement('li');
      li.innerText = Math.floor(Math.random() * total);
      fragment.appendChild(li);
    }
    ul.appendChild(fragment);
    countOfRender += 1;
    loop();
  }
  function loop() {
    if(countOfRender < loopCount) {
      window.requestAnimationFrame(add);
    }
  }
  loop();
}, 0)

23.模拟new

new操作符做了这些事:

  • 它创建了一个全新的对象
  • 它会被执行[[Prototype]](也就是__proto__)链接
  • 它使this指向新创建的对象
  • 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上
  • 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用
// objectFactory(name, 'cxk', '18')
function objectFactory() {
  const obj = new Object();
  const Constructor = [].shift.call(arguments);

  obj.__proto__ = Constructor.prototype;

  const ret = Constructor.apply(obj, arguments);

  return typeof ret === "object" ? ret : obj;
}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值