174道JavaScript 面试知识点总结(下)

174道JavaScript 面试知识点总结(下)_第1张图片

来源 | https://github.com/CavsZhouyou/

接上昨天的《174道JavaScript 面试知识点总结(上)》,昨天分享了前面100道题,今天分享后面的74道。正文如下:

101、toPrecision 和 toFixed 和 Math.round 的区别?

toPrecision 用于处理精度,精度是从左至右第一个不为 0 的数开始数起。
toFixed 是对小数点后指定位数取整,从小数点开始数起。
Math.round 是将一个数字四舍五入到一个整数。

102、什么是 XSS 攻击?如何防范 XSS 攻击?

XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。


XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。


XSS 一般分为存储型、反射型和 DOM 型。


存储型指的是恶意代码提交到了网站的数据库中,当用户请求数据的时候,服务器将其拼接为 HTML 后返回给了用户,从而导致了恶意代码的执行。


反射型指的是攻击者构建了特殊的 URL,当服务器接收到请求后,从 URL 中获取数据,拼接到 HTML 后返回,从而导致了恶意代码的执行。


DOM 型指的是攻击者构建了特殊的 URL,用户打开网站后,js 脚本从 URL 中获取数据,从而导致了恶意代码的执行。


XSS 攻击的预防可以从两个方面入手,一个是恶意代码提交的时候,一个是浏览器执行恶意代码的时候。


对于第一个方面,如果我们对存入数据库的数据都进行的转义处理,但是一个数据可能在多个地方使用,有的地方可能不需要转义,由于我们没有办法判断数据最后的使用场景,所以直接在输入端进行恶意代码的处理,其实是不太可靠的。


因此我们可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回。另一种是对需要插入到 HTML 中的代码做好充分的转义。对于 DOM 型的攻击,主要是前端脚本的不可靠而造成的,我们对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断。


还有一些方式,比如使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。


还可以对一些敏感信息进行保护,比如 cookie 使用 http-only ,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。

详细资料可以参考: 《前端安全系列(一):如何防止 XSS 攻击?》

103、 什么是 CSP?

CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。


通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式 

详细资料可以参考: 《内容安全策略(CSP)》 《前端面试之道》

104、什么是 CSRF 攻击?如何防范 CSRF 攻击?

CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被
攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。


CSRF 攻击的本质是利用了 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。


一般的 CSRF 攻击类型有三种:


第一种是 GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提
交。


第二种是 POST 类型的 CSRF 攻击,比如说构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。


第三种是链接类型的 CSRF 攻击,比如说在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。


CSRF 可以用下面几种方法来防护:


第一种是同源检测的方法,服务器根据 http 请求头中 origin 或者 referer 信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当 origin 或者 referer 信息都不存在的时候,直接阻止。这种方式的缺点是有些情况下 referer 可以被伪造。还有就是我们这种方法同时把搜索引擎的链接也给屏蔽了,所以一般网站会允许搜索引擎的页面请求,但是相应的页面请求这种请求方式也可能被攻击者给利用。


第二种方法是使用 CSRF Token 来进行验证,服务器向用户返回一个随机数 Token ,当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,然后服务器对这个 token 进行验证。这种方法解决了使用 cookie 单一验证方式时,可能会被冒用的问题,但是这种方法存在一个缺点就是,我们需要给网站中的所有请求都添加上这个 token,操作比较繁琐。还有一个问题是一般不会只有一台网站服务器,如果我们的请求经过负载平衡转移到了其他的服务器,但是这个服务器的 session 中没有保留这个 token 的话,就没有办法验证了。这种情况我们可以通过改变 token 的构建方式来解决。


第三种方式使用双重 Cookie 验证的办法,服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较,来进行验证。使用这种方式是利用了攻击者只能利用 cookie,但是不能访问获取 cookie 的特点。并且这种方法比 CSRF Token 的方法更加方便,并且不涉及到分布式访问的问题。这种方法的缺点是如果网站存在 XSS 漏洞的,那么这种方式会失效。同时这种方式不能做到子域名的隔离。


第四种方式是使用在设置 cookie 属性的时候设置 Samesite ,限制 cookie 不能作为被第三方使用,从而可以避免被攻击者利用。Samesite 一共有两种模式,一种是严格模式,在严格模式下 cookie 在任何情况下都不可能作为第三方 Cookie 使用,在宽松模式下,cookie 可以被请求是 GET 请求,且会发生页面跳转的请求所使用。

详细资料可以参考: 《前端安全系列之二:如何防止 CSRF 攻击?》 《[ HTTP 趣谈] origin, referer 和 host 区别》

105、 什么是 Samesite Cookie 属性?

Samesite Cookie 表示同站 cookie,避免 cookie 被第三方所利用。


将 Samesite 设为 strict ,这种称为严格模式,表示这个 cookie 在任何情况下都不可能作为第三方 cookie。


将 Samesite 设为 Lax ,这种模式称为宽松模式,如果这个请求是个 GET 请求,并且这个请求改变了当前页面或者打开了新的页面,那么这个 cookie 可以作为第三方 cookie,其余情况下都不能作为第三方 cookie。


使用这种方法的缺点是,因为它不支持子域,所以子域没有办法与主域共享登录信息,每次转入子域的网站,都回重新登录。还有一个问题就是它的兼容性不够好。

106、什么是点击劫持?如何防范点击劫持?

点击劫持是一种视觉欺骗的攻击手段,攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。


我们可以在 http 相应头中设置 X-FRAME-OPTIONS 来防御用 iframe 嵌套的点击劫持攻击。通过不同的值,可以规定页面在特
定的一些情况才能作为 iframe 来使用。

详细资料可以参考: 《web 安全之--点击劫持攻击与防御技术简介》

107、SQL 注入攻击?

SQL 注入攻击指的是攻击者在 HTTP 请求中注入恶意的 SQL 代码,服务器使用参数构建数据库 SQL 命令时,恶意 SQL 被一起构
造,破坏原有 SQL 结构,并在数据库中执行,达到编写程序时意料之外结果的攻击行为。

详细资料可以参考: 《Web 安全漏洞之 SQL 注入》 《如何防范常见的 Web 攻击》

108、什么是 MVVM?比之 MVC 有什么区别?什么又是 MVP ?

MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化我们的开发效率。


比如说我们实验室在以前项目开发的时候,使用单页应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,当时一旦项目变得复杂,那么整个文件就会变得冗长,混乱,这样对我们的项目开发和后期的项目维护是非常不利的。


MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Co
ntroller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。


MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中我们使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的
Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此我们可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。


MVVM 模式中的 VM,指的是 ViewModel,它和 MVP 的思想其实是相同的,不过它通过双向的数据绑定,将 View 和 Model 的同步更新给自动化了。当 Model 发生变化的时候,ViewModel 就会自动更新;ViewModel 变化了,View 也会更新。这样就将 Presenter 中的工作给自动化了。我了解过一点双向数据绑定的原理,比如 vue 是通过使用数据劫持和发布订阅者模式来实现的这一功
能。

详细资料可以参考: 《浅析前端开发中的 MVC/MVP/MVVM 模式》 《MVC,MVP 和 MVVM 的图示》 《MVVM》 《一篇文章了解架构模式:MVC/MVP/MVVM》

109、 vue 双向数据绑定原理?

vue 通过使用双向数据绑定,来实现了 View 和 Model 的同步更新。vue 的双向数据绑定主要是通过使用数据劫持和发布订阅者模式来实现的。


首先我们通过 Object.defineProperty() 方法来对 Model 数据各个属性添加访问器属性,以此来实现数据的劫持,因此当 Model 中的数据发生变化的时候,我们可以通过配置的 setter 和 getter 方法来实现对 View 层数据更新的通知。


数据在 html 模板中一共有两种绑定情况,一种是使用 v-model 来对 value 值进行绑定,一种是作为文本绑定,在对模板引擎进行解析的过程中。


如果遇到元素节点,并且属性值包含 v-model 的话,我们就从 Model 中去获取 v-model 所对应的属性的值,并赋值给元素的 value 值。然后给这个元素设置一个监听事件,当 View 中元素的数据发生变化的时候触发该事件,通知 Model 中的对应的属性的值进行更新。


如果遇到了绑定的文本节点,我们使用 Model 中对应的属性的值来替换这个文本。对于文本节点的更新,我们使用了发布订阅者模式,属性作为一个主题,我们为这个节点设置一个订阅者对象,将这个订阅者对象加入这个属性主题的订阅者列表中。当 Model 层数据发生改变的时候,Model 作为发布者向主题发出通知,主题收到通知再向它的所有订阅者推送,订阅者收到通知后更改自己的数
据。

详细资料可以参考: 《Vue.js 双向绑定的实现原理》

110、 Object.defineProperty 介绍?

Object.defineProperty 函数一共有三个参数,第一个参数是需要定义属性的对象,第二个参数是需要定义的属性,第三个是该属性描述符。


一个属性的描述符有四个属性,分别是 value 属性的值,writable 属性是否可写,enumerable 属性是否可枚举,configurable 属性是否可配置修改。

详细资料可以参考: 《Object.defineProperty()》

111、 使用 Object.defineProperty() 来进行数据劫持有什么缺点?

有一些对属性的操作,使用这种方法无法拦截,比如说通过下标方式修改数组数据或者给对象新增属性,vue 内部通过重写函数解决了这个问题。在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用 Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为这是 ES6 的语法。

112、 什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?

我对 Virtual DOM 的理解是,


首先对我们将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后我们将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。


当页面的状态发生改变,我们需要对页面的 DOM 的结构进行调整的时候,我们首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。


最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。


我认为 Virtual DOM 这种方法对于我们需要有大量的 DOM 操作的时候,能够很好的提高我们的操作效率,通过在操作前确定需要做的最小修改,尽可能的减少 DOM 操作带来的重流和重绘的影响。其实 Virtual DOM 并不一定比我们真实的操作 DOM 要快,这种方法的目的是为了提高我们开发时的可维护性,在任意的情况下,都能保证一个尽量小的性能消耗去进行操作。

详细资料可以参考: 《Virtual DOM》 《理解 Virtual DOM》 《深度剖析:如何实现一个 Virtual DOM 算法》 《网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?》

113、如何比较两个 DOM 树的差异?

两个树的完全 diff 算法的时间复杂度为 O(n^3) ,但是在前端中,我们很少会跨层级的移动元素,所以我们只需要比较同一层级的元素进行比较,这样就可以将算法的时间复杂度降低为 O(n)。


算法首先会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个序号。在深度遍历的时候,每遍历到一个节点,我们就将这个节点和新的树中的节点进行比较,如果有差异,则将这个差异记录到一个对象中。


在对列表元素进行对比的时候,由于 TagName 是重复的,所以我们不能使用这个来对比。我们需要给每一个子节点加上一个 key,列表对比的时候使用 key 来进行比较,这样我们才能够复用老的 DOM 树上的节点。

114、什么是 requestAnimationFrame ?

详细资料可以参考: 《你需要知道的 requestAnimationFrame》 《CSS3 动画那么强,requestAnimationFrame 还有毛线用?》

115、谈谈你对 webpack 的看法

我当时使用 webpack 的一个最主要原因是为了简化页面依赖的管理,并且通过将其打包为一个文件来降低页面加载时请求的资源
数。


我认为 webpack 的主要原理是,它将所有的资源都看成是一个模块,并且把页面逻辑当作一个整体,通过一个给定的入口文件,webpack 从这个文件开始,找到所有的依赖文件,将各个依赖文件模块通过 loader 和 plugins 处理后,然后打包在一起,最后输出一个浏览器可识别的 JS 文件。


Webpack 具有四个核心的概念,分别是 Entry(入口)、Output(输出)、loader 和 Plugins(插件)。


Entry 是 webpack 的入口起点,它指示 webpack 应该从哪个模块开始着手,来作为其构建内部依赖图的开始。


Output 属性告诉 webpack 在哪里输出它所创建的打包文件,也可指定打包文件的名称,默认位置为 ./dist。


loader 可以理解为 webpack 的编译器,它使得 webpack 可以处理一些非 JavaScript 文件。在对 loader 进行配置的时候,test 属性,标志有哪些后缀的文件应该被处理,是一个正则表达式。use 属性,指定 test 类型的文件应该使用哪个 loader 进行预处理。常用的 loader 有 css-loader、style-loader 等。


插件可以用于执行范围更广的任务,包括打包、优化、压缩、搭建服务器等等,要使用一个插件,一般是先使用 npm 包管理器进行安装,然后在配置文件中引入,最后将其实例化后传递给 plugins 数组属性。


使用 webpack 的确能够提供我们对于项目的管理,但是它的缺点就是调试和配置起来太麻烦了。但现在 webpack4.0 的免配置一定程度上解决了这个问题。但是我感觉就是对我来说,就是一个黑盒,很多时候出现了问题,没有办法很好的定位。

详细资料可以参考: 《不聊 webpack 配置,来说说它的原理》 《前端工程化——构建工具选型:grunt、gulp、webpack》 《浅入浅出 webpack》 《前端构建工具发展及其比较》

116、offsetWidth/offsetHeight,clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别?

clientWidth/clientHeight 返回的是元素的内部宽度,它的值只包含 content + padding,如果有滚动条,不包含滚动条。
clientTop 返回的是上边框的宽度。
clientLeft 返回的左边框的宽度。


offsetWidth/offsetHeight 返回的是元素的布局宽度,它的值包含 content + padding + border 包含了滚动条。
offsetTop 返回的是当前元素相对于其 offsetParent 元素的顶部的距离。
offsetLeft 返回的是当前元素相对于其 offsetParent 元素的左部的距离。


scrollWidth/scrollHeight 返回值包含 content + padding + 溢出内容的尺寸。
scrollTop 属性返回的是一个元素的内容垂直滚动的像素数。
scrollLeft 属性返回的是元素滚动条到元素左边的距离。

详细资料可以参考: 《最全的获取元素宽高及位置的方法》 《用 Javascript 获取页面元素的位置》

117、谈一谈你理解的函数式编程?

简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。


它具有以下特性:闭包和高阶函数、惰性计算、递归、函数是"第一等公民"、只用"表达式"。

详细资料可以参考: 《函数式编程初探》

118、异步编程的实现方式?

相关资料:

回调函数
优点:简单、容易理解
缺点:不利于维护,代码耦合高


事件监听(采用时间驱动模式,取决于某个事件是否发生):
优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
缺点:事件驱动型,流程不够清晰


发布/订阅(观察者模式)
类似于事件监听,但是可以通过‘消息中心’,了解现在有多少发布者,多少订阅者


Promise 对象
优点:可以利用 then 方法,进行链式写法;可以书写错误时的回调函数;
缺点:编写和理解,相对比较难


Generator 函数
优点:函数体内外的数据交换、错误处理机制
缺点:流程管理不方便


async 函数
优点:内置执行器、更好的语义、更广的适用性、返回的是 Promise、结构清晰。
缺点:错误处理机制

回答:

js 中的异步机制可以分为以下几种:


第一种最常见的是使用回调函数的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。


第二种是 Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。


第三种是使用 generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部我们还可以将执行权转移回来。当我们遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕的时候我们再将执行权给转移回来。因此我们在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式我们需要考虑的问题是何时将函数的控制权转移回来,因此我们需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。


第四种是使用 async 函数的形式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此我们可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

119、Js 动画与 CSS 动画区别及相应实现

CSS3 的动画的优点


在性能上会稍微好一些,浏览器会对 CSS3 的动画做一些优化
代码相对简单


缺点


在动画控制上不够灵活
兼容性不好


JavaScript 的动画正好弥补了这两个缺点,控制能力很强,可以单帧的控制、变换,同时写得好完全可以兼容 IE6,并且功能强大。对于一些复杂控制的动画,使用 javascript 会比较靠谱。而在实现一些小的交互动效的时候,就多考虑考虑 CSS 吧

120、get 请求传参长度的误区

误区:我们经常说 get 请求参数的大小存在限制,而 post 请求的参数大小是无限制的。
实际上 HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对 get 请求参数的限制是来源与浏览器或web 服务器,浏览器或 web 服务器限制了 url 的长度。为了明确这个概念,我们必须再次强调下面几点:
  • 1.HTTP 协议未规定 GET 和 POST 的长度限制

  • 2.GET 的最大长度显示是因为浏览器和 web 服务器限制了 URI 的长度

  • 3.不同的浏览器和 WEB 服务器,限制的最大长度不一样

  • 4.要支持 IE,则最大长度为 2083byte,若只支持 Chrome,则最大长度 8182byte

121、URL 和 URI 的区别?

URI: Uniform Resource Identifier      指的是统一资源标识符
URL: Uniform Resource Location        指的是统一资源定位符
URN: Universal Resource Name          指的是统一资源名称


URI 指的是统一资源标识符,用唯一的标识来确定一个资源,它是一种抽象的定义,也就是说,不管使用什么方法来定义,只要能唯一的标识一个资源,就可以称为 URI。


URL 指的是统一资源定位符,URN 指的是统一资源名称。URL 和 URN 是 URI 的子集,URL 可以理解为使用地址来标识资源,URN 可以理解为使用名称来标识资源。

详细资料可以参考: 《HTTP 协议中 URI 和 URL 有什么区别?》 《你知道 URL、URI 和 URN 三者之间的区别吗?》 《URI、URL 和 URN 的区别》

122、 get 和 post 请求在缓存方面的区别

相关知识点:

get 请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存。


post 不同,post 做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。因此 get 请求适合于请求缓存。

回答:

缓存一般只适用于那些不会更新服务端数据的请求。一般 get 请求都是查找请求,不会对服务器资源数据造成修改,而 post 请求一般都会对服务器数据造成修改,所以,一般会对 get 请求进行缓存,很少会对 post 请求进行缓存。

详细资料可以参考: 《HTML 关于 post 和 get 的区别以及缓存问题的理解》

123、图片的懒加载和预加载

相关知识点:

预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。


懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。


两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

回答:

懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。


预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。


这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

详细资料可以参考: 《懒加载和预加载》 《网页图片加载优化方案》 《基于用户行为的图片等资源预加载》

124、 mouseover 和 mouseenter 的区别?

当鼠标移动到元素上时就会触发 mouseenter 事件,类似 mouseover,它们两者之间的差别是 mouseenter 不会冒泡。


由于 mouseenter 不支持事件冒泡,导致在一个元素的子元素上进入或离开的时候会触发其 mouseover 和 mouseout 事件,但是却不会触发 mouseenter 和 mouseleave 事件。

详细资料可以参考: 《mouseenter 与 mouseover 为何这般纠缠不清?》

125、 js 拖拽功能的实现

相关知识点:

首先是三个事件,分别是 mousedown,mousemove,mouseup
当鼠标点击按下的时候,需要一个 tag 标识此时已经按下,可以执行 mousemove 里面的具体方法。
clientX,clientY 标识的是鼠标的坐标,分别标识横坐标和纵坐标,并且我们用 offsetX 和 offsetY 来表示
元素的元素的初始坐标,移动的举例应该是:
鼠标移动时候的坐标-鼠标按下去时候的坐标。
也就是说定位信息为:
鼠标移动时候的坐标-鼠标按下去时候的坐标+元素初始情况下的 offetLeft.

回答:

一个元素的拖拽过程,我们可以分为三个步骤,第一步是鼠标按下目标元素,第二步是鼠标保持按下的状态移动鼠标,第三步是鼠
标抬起,拖拽过程结束。


这三步分别对应了三个事件,mousedown 事件,mousemove 事件和 mouseup 事件。只有在鼠标按下的状态移动鼠标我们才会
执行拖拽事件,因此我们需要在 mousedown 事件中设置一个状态来标识鼠标已经按下,然后在 mouseup 事件中再取消这个状
态。在 mousedown 事件中我们首先应该判断,目标元素是否为拖拽元素,如果是拖拽元素,我们就设置状态并且保存这个时候鼠
标的位置。然后在 mousemove 事件中,我们通过判断鼠标现在的位置和以前位置的相对移动,来确定拖拽元素在移动中的坐标。
最后 mouseup 事件触发后,清除状态,结束拖拽事件。

详细资料可以参考: 《原生 js 实现拖拽功能基本思路》

126、为什么使用 setTimeout 实现 setInterval?怎么模拟?

相关知识点:

// 思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果


function mySetInterval(fn, timeout) {
  // 控制器,控制定时器是否继续执行
  var timer = {
    flag: true
  };


  // 设置递归函数,模拟定时器执行。
  function interval() {
    if (timer.flag) {
      fn();
      setTimeout(interval, timeout);
    }
  }


  // 启动定时器
  setTimeout(interval, timeout);


  // 返回控制器
  return timer;
}

回答:

setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。


针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。

详细资料可以参考: 《用 setTimeout 实现 setInterval》 《setInterval 有什么缺点?》

127、let 和 const 的注意点?

  • 1.声明的变量只在声明时的代码块内有效

  • 2.不存在声明提升

  • 3.存在暂时性死区,如果在变量声明前使用,会报错

  • 4.不允许重复声明,重复声明会报错

128、什么是 rest 参数?

rest 参数(形式为...变量名),用于获取函数的多余参数。

129、什么是尾调用,使用尾调用有什么好处?

尾调用指的是函数的最后一步调用另一个函数。我们代码执行是基于执行栈的,所以当我们在一个函数里调用另一个函数时,我们会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这个时候我们可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

130、Symbol 类型的注意点?

  • 1.Symbol 函数前不能使用 new 命令,否则会报错。

  • 2.Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

  • 3.Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。

  • 4.Object.getOwnPropertySymbols 方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

  • 5.Symbol.for 接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

  • 6.Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。

131、Set 和 WeakSet 结构?

  • 1.ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

  • 2.WeakSet 结构与 Set 类似,也是不重复的值的集合。但是 WeakSet 的成员只能是对象,而不能是其他类型的值。WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,

132、 Map 和 WeakMap 结构?

  • 1.Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

  • 2.WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。

133、什么是 Proxy ?

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。


Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

134、Reflect 对象创建目的?

  • 1.将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty,放到 Reflect 对象上。

  • 2.修改某些 Object 方法的返回结果,让其变得更合理。

  • 3.让 Object 操作都变成函数行为。

  • 4.Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

135、 require 模块引入的查找方式?

当 Node 遇到 require(X) 时,按下面的顺序处理。


(1)如果 X 是内置模块(比如 require('http'))
  a. 返回该模块。
  b. 不再继续执行。


(2)如果 X 以 "./" 或者 "/" 或者 "../" 开头
  a. 根据 X 所在的父模块,确定 X 的绝对路径。
  b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
    X
    X.js
    X.json
    X.node


  c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
    X/package.json(main字段)
    X/index.js
    X/index.json
    X/index.node


(3)如果 X 不带路径
  a. 根据 X 所在的父模块,确定 X 可能的安装目录。
  b. 依次在每个目录中,将 X 当成文件名或目录名加载。


(4)抛出 "not found"

详细资料可以参考: 《require() 源码解读》

136、 什么是 Promise 对象,什么是 Promises/A+ 规范?

Promise 对象是异步编程的一种解决方案,最早由社区提出。Promises/A+ 规范是 JavaScript Promise 的标准,规定了一个 Promise 所必须具有的特性。


Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是 pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者 rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。状态的改变是通过 resolve() 和 reject() 函数来实现的,我们
可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。

详细资料可以参考: 《Promises/A+ 规范》 《Promise》

137、 手写一个 Promise

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";


function MyPromise(fn) {
  // 保存初始化状态
  var self = this;


  // 初始化状态
  this.state = PENDING;


  // 用于保存 resolve 或者 rejected 传入的值
  this.value = null;


  // 用于保存 resolve 的回调函数
  this.resolvedCallbacks = [];


  // 用于保存 reject 的回调函数
  this.rejectedCallbacks = [];


  // 状态转变为 resolved 方法
  function resolve(value) {
    // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
    if (value instanceof MyPromise) {
      return value.then(resolve, reject);
    }


    // 保证代码的执行顺序为本轮事件循环的末尾
    setTimeout(() => {
      // 只有状态为 pending 时才能转变,
      if (self.state === PENDING) {
        // 修改状态
        self.state = RESOLVED;


        // 设置传入的值
        self.value = value;


        // 执行回调函数
        self.resolvedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }


  // 状态转变为 rejected 方法
  function reject(value) {
    // 保证代码的执行顺序为本轮事件循环的末尾
    setTimeout(() => {
      // 只有状态为 pending 时才能转变
      if (self.state === PENDING) {
        // 修改状态
        self.state = REJECTED;


        // 设置传入的值
        self.value = value;


        // 执行回调函数
        self.rejectedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }


  // 将两个方法传入函数执行
  try {
    fn(resolve, reject);
  } catch (e) {
    // 遇到错误时,捕获错误,执行 reject 函数
    reject(e);
  }
}


MyPromise.prototype.then = function(onResolved, onRejected) {
  // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
  onResolved =
    typeof onResolved === "function"
      ? onResolved
      : function(value) {
          return value;
        };


  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function(error) {
          throw error;
        };


  // 如果是等待状态,则将函数加入对应列表中
  if (this.state === PENDING) {
    this.resolvedCallbacks.push(onResolved);
    this.rejectedCallbacks.push(onRejected);
  }


  // 如果状态已经凝固,则直接执行对应状态的函数


  if (this.state === RESOLVED) {
    onResolved(this.value);
  }


  if (this.state === REJECTED) {
    onRejected(this.value);
  }
};

138、 如何检测浏览器所支持的最小字体大小?

用 JS 设置 DOM 的字体为某一个值,然后再取出来,如果值设置成功,就说明支持。

139、怎么做 JS 代码 Error 统计?

error 统计使用浏览器的 window.error 事件。

140、单例模式模式是什么?

单例模式保证了全局只有一个实例来被访问。比如说常用的如弹框组件的实现和全局状态的实现。

141、策略模式是什么?

策略模式主要是用来将方法的实现和方法的调用分离开,外部通过不同的参数可以调用不同的策略。我主要在 MVP 模式解耦的时候
用来将视图层的方法定义和方法调用分离。

142、代理模式是什么?

 代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。比如说常见的事件代理。

 143、 中介者模式是什么?

中介者模式指的是,多个对象通过一个中介者进行交流,而不是直接进行交流,这样能够将通信的各个对象解耦。

144、适配器模式是什么?

适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。假如我们需要一种
新的接口返回方式,但是老的接口由于在太多地方已经使用了,不能随意更改,这个时候就可以使用适配器模式。比如我们需要一种
自定义的时间返回格式,但是我们又不能对 js 时间格式化的接口进行修改,这个时候就可以使用适配器模式。

更多关于设计模式的资料可以参考: 《前端面试之道》 《JavaScript 设计模式》 《JavaScript 中常见设计模式整理》

145、观察者模式和发布订阅模式有什么不同?

发布订阅模式其实属于广义上的观察者模式


在观察者模式中,观察者需要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并作出响应。


而在发布订阅模式中,发布者和订阅者之间多了一个调度中心。调度中心一方面从发布者接收事件,另一方面向订阅者发布事件,订阅者需要在调度中心中订阅事件。通过调度中心实现了发布者和订阅者关系的解耦。使用发布订阅者模式更利于我们代码的可维护性。

详细资料可以参考: 《观察者模式和发布订阅模式有什么不同?》

146、 Vue 的生命周期是什么?

Vue 的生命周期指的是组件从创建到销毁的一系列的过程,被称为 Vue 的生命周期。通过提供的 Vue 在生命周期各个阶段的钩子函数,我们可以很好的在 Vue 的各个生命阶段实现一些操作。

147、 Vue 的各个生命阶段是什么?

Vue 一共有8个生命阶段,分别是创建前、创建后、加载前、加载后、更新前、更新后、销毁前和销毁后,每个阶段对应了一个生命周期的钩子函数。


(1)beforeCreate 钩子函数,在实例初始化之后,在数据监听和事件配置之前触发。因此在这个事件中我们是获取不到 data 数据的。


(2)created 钩子函数,在实例创建完成后触发,此时可以访问 data、methods 等属性。但这个时候组件还没有被挂载到页面中去,所以这个时候访问不到 $el 属性。一般我们可以在这个函数中进行一些页面初始化的工作,比如通过 ajax 请求数据来对页面进行初始化。


(3)beforeMount 钩子函数,在组件被挂载到页面之前触发。在 beforeMount 之前,会找到对应的 template,并编译成 render 函数。


(4)mounted 钩子函数,在组件挂载到页面之后触发。此时可以通过 DOM API 获取到页面中的 DOM 元素。


(5)beforeUpdate 钩子函数,在响应式数据更新时触发,发生在虚拟 DOM 重新渲染和打补丁之前,这个时候我们可以对可能会被移除的元素做一些操作,比如移除事件监听器。


(6)updated 钩子函数,虚拟 DOM 重新渲染和打补丁之后调用。


(7)beforeDestroy 钩子函数,在实例销毁之前调用。一般在这一步我们可以销毁定时器、解绑全局事件等。


(8)destroyed 钩子函数,在实例销毁之后调用,调用后,Vue 实例中的所有东西都会解除绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。


当我们使用 keep-alive 的时候,还有两个钩子函数,分别是 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

详细资料可以参考: 《vue 生命周期深入》 《Vue 实例》

148、Vue 组件间的参数传递方式?

(1)父子组件间通信


第一种方法是子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事
件来向父组件发送数据。


第二种是通过 ref 属性给子组件设置一个名字。父组件通过 $refs 组件名来获得子组件,子组件通过 $parent 获得父组
件,这样也可以实现通信。


第三种是使用 provider/inject,在父组件中通过 provider 提供变量,在子组件中通过 inject 来将变量注入到组件
中。不论子组件有多深,只要调用了 inject 那么就可以注入 provider 中的数据。


(2)兄弟组件间通信


第一种是使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实
例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。


第二种是通过 $parent.$refs 来获取到兄弟组件,也可以进行通信。


(3)任意组件之间


使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。




如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候
可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个
公共数据进行读写操作,这样达到了解耦的目的。

详细资料可以参考: 《VUE 组件之间数据传递全集》

149、computed 和 watch 的差异?

(1)computed 是计算一个新的属性,并将该属性挂载到 Vue 实例上,而 watch 是监听已经存在且已挂载到 Vue 实例上的数据,所以用 watch 同样可以监听 computed 计算属性的变化。


(2)computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值。而 watch 则是当数据发生变化便会调用执行函数。


(3)从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据。

详细资料可以参考: 《做面试的不倒翁:浅谈 Vue 中 computed 实现原理》 《深入理解 Vue 的 watch 实现原理及其实现方式》

150、vue-router 中的导航钩子函数

(1)全局的钩子函数 beforeEach 和 afterEach


beforeEach 有三个参数,to 代表要进入的路由对象,from 代表离开的路由对象。next 是一个必须要执行的函数,如果不传参数,那就执行下一个钩子函数,如果传入 false,则终止跳转,如果传入一个路径,则导航到对应的路由,如果传入 error ,则导航终止,error 传入错误的监听函数。


(2)单个路由独享的钩子函数 beforeEnter,它是在路由配置上直接进行定义的。


(3)组件内的导航钩子主要有这三种:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。它们是直接在路由组
件内部直接进行定义的。

详细资料可以参考: 《导航守卫》

151、$route 和 $router 的区别?

$route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。而 $router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。

152、vue 常用的修饰符?

.prevent: 提交事件不再重载页面;.stop: 阻止单击事件冒泡;.self: 当事件发生在该元素本身而不是子元素的时候会触发;

153、 vue 中 key 值的作用?

vue 中 key 值的作用可以分为两种情况来考虑。


第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当我们使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此我们可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。


第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。

详细资料可以参考: 《Vue 面试中,经常会被问到的面试题 Vue 知识点整理》 《Vue2.0 v-for 中 :key 到底有什么用?》 《vue 中 key 的作用》

154、computed 和 watch 区别?

computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。


watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。

155、keep-alive 组件有什么作用?

如果你需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。

156、vue 中 mixin 和 mixins 区别?

mixin 用于全局混入,会影响到每个组件实例。
mixins 应该是我们最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并

详细资料可以参考: 《前端面试之道》 《混入》

157、开发中常用的几种 Content-Type ?

(1)application/x-www-form-urlencoded


浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。该种方式提交的数据放在 body 里面,数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL
转码。


(2)multipart/form-data


该种方式也是一个常见的 POST 提交方式,通常表单上传文件时使用该种方式。


(3)application/json


告诉服务器消息主体是序列化后的 JSON 字符串。


(4)text/xml


该种方式主要用来提交 XML 格式的数据。

详细资料可以参考: 《常用的几种 Content-Type》

158、如何封装一个 javascript 的类型判断函数?

function getType(value) {
  // 判断数据是 null 的情况
  if (value === null) {
    return value + "";
  }


  // 判断数据是引用类型的情况
  if (typeof value === "object") {
    let valueClass = Object.prototype.toString.call(value),
      type = valueClass.split(" ")[1].split("");


    type.pop();


    return type.join("").toLowerCase();
  } else {
    // 判断数据是基本数据类型的情况和函数的情况
    return typeof value;
  }
}

详细资料可以参考: 《JavaScript 专题之类型判断(上)》

159、如何判断一个对象是否为空对象?

function checkNullObj(obj) {
  return Object.keys(obj).length === 0;
}

详细资料可以参考: 《js 判断一个 object 对象是否为空》

160、使用闭包实现每隔一秒打印 1,2,3,4

// 使用闭包实现
for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}


// 使用 let 块级作用域


for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}

161、手写一个 jsonp

function jsonp(url, params, callback) {
  // 判断是否含有参数
  let queryString = url.indexOf("?") === "-1" ? "?" : "&";


  // 添加参数
  for (var k in params) {
    if (params.hasOwnProperty(k)) {
      queryString += k + "=" + params[k] + "&";
    }
  }


  // 处理回调函数名
  let random = Math.random()
      .toString()
      .replace(".", ""),
    callbackName = "myJsonp" + random;


  // 添加回调函数
  queryString += "callback=" + callbackName;


  // 构建请求
  let scriptNode = document.createElement("script");
  scriptNode.src = url + queryString;


  window[callbackName] = function() {
    // 调用回调函数
    callback(...arguments);


    // 删除这个引入的脚本
    document.getElementsByTagName("head")[0].removeChild(scriptNode);
  };


  // 发起请求
  document.getElementsByTagName("head")[0].appendChild(scriptNode);
}

详细资料可以参考: 《原生 jsonp 具体实现》 《jsonp 的原理与实现》

162、 手写一个观察者模式?

var events = (function() {
  var topics = {};


  return {
    // 注册监听函数
    subscribe: function(topic, handler) {
      if (!topics.hasOwnProperty(topic)) {
        topics[topic] = [];
      }
      topics[topic].push(handler);
    },


    // 发布事件,触发观察者回调事件
    publish: function(topic, info) {
      if (topics.hasOwnProperty(topic)) {
        topics[topic].forEach(function(handler) {
          handler(info);
        });
      }
    },


    // 移除主题的一个观察者的回调事件
    remove: function(topic, handler) {
      if (!topics.hasOwnProperty(topic)) return;


      var handlerIndex = -1;
      topics[topic].forEach(function(item, index) {
        if (item === handler) {
          handlerIndex = index;
        }
      });


      if (handlerIndex >= 0) {
        topics[topic].splice(handlerIndex, 1);
      }
    },


    // 移除主题的所有观察者的回调事件
    removeAll: function(topic) {
      if (topics.hasOwnProperty(topic)) {
        topics[topic] = [];
      }
    }
  };
})();

详细资料可以参考: 《JS 事件模型》

163、 EventEmitter 实现

class EventEmitter {
  constructor() {
    this.events = {};
  }


  on(event, callback) {
    let callbacks = this.events[event] || [];
    callbacks.push(callback);
    this.events[event] = callbacks;


    return this;
  }


  off(event, callback) {
    let callbacks = this.events[event];
    this.events[event] = callbacks && callbacks.filter(fn => fn !== callback);


    return this;
  }


  emit(event, ...args) {
    let callbacks = this.events[event];
    callbacks.forEach(fn => {
      fn(...args);
    });


    return this;
  }


  once(event, callback) {
    let wrapFun = function(...args) {
      callback(...args);


      this.off(event, wrapFun);
    };
    this.on(event, wrapFun);


    return this;
  }
}

164、一道常被人轻视的前端 JS 面试题

function Foo() {
  getName = function() {
    alert(1);
  };
  return this;
}
Foo.getName = function() {
  alert(2);
};
Foo.prototype.getName = function() {
  alert(3);
};
var getName = function() {
  alert(4);
};
function getName() {
  alert(5);
}


//请写出以下输出结果:
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); // 3

详细资料可以参考: 《前端程序员经常忽视的一个 JavaScript 面试题》 《一道考察运算符优先级的 JavaScript 面试题》 《一道常被人轻视的前端 JS 面试题》

165、 如何确定页面的可用性时间,什么是 Performance API?

Performance API 用于精确度量、控制、增强浏览器的性能表现。这个 API 为测量网站性能,提供以前没有办法做到的精度。


使用 getTime 来计算脚本耗时的缺点,首先,getTime方法(以及 Date 对象的其他方法)都只能精确到毫秒级别(一秒的千分之一),想要得到更小的时间差别就无能为力了。其次,这种写法只能获取代码运行过程中的时间进度,无法知道一些后台事件的时间进度,比如浏览器用了多少时间从服务器加载网页。


为了解决这两个不足之处,ECMAScript 5引入“高精度时间戳”这个 API,部署在 performance 对象上。它的精度可以达到1毫秒
的千分之一(1秒的百万分之一)。


navigationStart:当前浏览器窗口的前一个网页关闭,发生 unload 事件时的 Unix 毫秒时间戳。如果没有前一个网页,则等于 fetchStart 属性。


loadEventEnd:返回当前网页 load 事件的回调函数运行结束时的 Unix 毫秒时间戳。如果该事件还没有发生,返回 0。

根据上面这些属性,可以计算出网页加载各个阶段的耗时。比如,网页加载整个过程的耗时的计算方法如下:

var t = performance.timing;
var pageLoadTime = t.loadEventEnd - t.navigationStart;

详细资料可以参考: 《Performance API》

166、js 中的命名规则

(1)第一个字符必须是字母、下划线(_)或美元符号($)
(2)余下的字符可以是下划线、美元符号或任何字母或数字字符


一般我们推荐使用驼峰法来对变量名进行命名,因为这样可以与 ECMAScript 内置的函数和对象命名格式保持一致。

详细资料可以参考: 《ECMAScript 变量》

167、js 语句末尾分号是否可以省略?

在 ECMAScript 规范中,语句结尾的分号并不是必需的。但是我们一般最好不要省略分号,因为加上分号一方面有
利于我们代码的可维护性,另一方面也可以避免我们在对代码进行压缩时出现错误。

168、Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

169、 Math.ceil 和 Math.floor

Math.ceil() === 向上取整,函数返回一个大于或等于给定数字的最小整数。


Math.floor() === 向下取整,函数返回一个小于或等于给定数字的最大整数。

170、js for 循环注意点

for (var i = 0, j = 0; i < 5, j < 9; i++, j++) {
  console.log(i, j);
}


// 当判断语句含有多个语句时,以最后一个判断语句的值为准,因此上面的代码会执行 10 次。
// 当判断语句为空时,循环会一直进行。

171、一个列表,假设有 100000 个数据,这个该怎么办?

我们需要思考的问题:该处理是否必须同步完成?数据是否必须按顺序完成?


解决办法:


(1)将数据分页,利用分页的原理,每次服务器端只返回一定数目的数据,浏览器每次只对一部分进行加载。


(2)使用懒加载的方法,每次加载一部分数据,其余数据当需要使用时再去加载。


(3)使用数组分块技术,基本思路是为要处理的项目创建一个队列,然后设置定时器每过一段时间取出一部分数据,然后再使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。

172、js 中倒计时的纠偏实现?

在前端实现中我们一般通过 setTimeout 和 setInterval 方法来实现一个倒计时效果。但是使用这些方法会存在时间偏差的问题,这是由于 js 的程序执行机制造成的,setTimeout 和 setInterval 的作用是隔一段时间将回调事件加入到事件队列中,因此事件并不是立即执行的,它会等到当前执行栈为空的时候再取出事件执行,因此事件等待执行的时间就是造成误差的原因。


一般解决倒计时中的误差的有这样两种办法:


(1)第一种是通过前端定时向服务器发送请求获取最新的时间差,以此来校准倒计时时间。


(2)第二种方法是前端根据偏差时间来自动调整间隔时间的方式来实现的。这一种方式首先是以 setTimeout 递归的方式来实现倒计时,然后通过一个变量来记录已经倒计时的秒数。每一次函数调用的时候,首先将变量加一,然后根据这个变量和每次的间隔时间,我们就可以计算出此时无偏差时应该显示的时间。然后将当前的真实时间与这个时间相减,这样我们就可以得到时间的偏差大小,因此我们在设置下一个定时器的间隔大小的时候,我们就从间隔时间中减去这个偏差大小,以此来实现由于程序执行所造成的时间误差的纠正。

详细资料可以参考: 《JavaScript 前端倒计时纠偏实现》

173、进程间通信的方式?

  • 1.管道通信

  • 2.消息队列通信

  • 3.信号量通信

  • 4.信号通信

  • 5.共享内存通信

  • 6.套接字通信

详细资料可以参考: 《进程间 8 种通信方式详解》 《进程与线程的一个简单解释》

174、 如何查找一篇英文文章中出现频率最高的单词?

function findMostWord(article) {
  // 合法性判断
  if (!article) return;


  // 参数处理
  article = article.trim().toLowerCase();


  let wordList = article.match(/[a-z]+/g),
    visited = [],
    maxNum = 0,
    maxWord = "";


  article = " " + wordList.join("  ") + " ";


  // 遍历判断单词出现次数
  wordList.forEach(function(item) {
    if (visited.indexOf(item) < 0) {


      // 加入 visited 
      visited.push(item);


      let word = new RegExp(" " + item + " ", "g"),
        num = article.match(word).length;


      if (num > maxNum) {
        maxNum = num;
        maxWord = item;
      }
    }
  });


  return maxWord + "  " + maxNum;
}

本文完〜

你可能感兴趣的:(ipad,relativelayout,redirect,weex,hashtable)