高级前端开发工程师面试题(高级前端常见面试题合集)
常见的图片格式及使用场景
(1)BMP ,是无损的 、既支持索引色也支持直接色的点阵图 。这种图片格式几乎没有对数据进行压缩 ,所以BMP格式的图片通常是较大的文件 。
(2)GIF是无损的 、采用索引色的点阵图 。采用LZW压缩算法进行编码 。文件小 ,是GIF格式的优点 ,同时 ,GIF格式还具有支持动画以及透明的优点 。但是GIF格式仅支持8bit的索引色 ,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景 。
(3)JPEG是有损的 、采用直接色的点阵图 。JPEG的图片的优点是采用了直接色 ,得益于更丰富的色彩 ,JPEG非常适合用来存储照片 ,与GIF相比 ,JPEG不适合用来存储企业Logo 、线框类的图 。因为有损压缩会导致图片模糊 ,而直接色的选用 ,又会导致图片文件较GIF更大 。
(4)PNG-8是无损的 、使用索引色的点阵图 。PNG是一种比较新的图片格式,PNG-8是非常好的GIF格式替代者 ,在可能的情况下 ,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下 ,PNG-8具有更小的文件体积 。除此之外 ,PNG-8还支持透明度的调节 ,而GIF并不支持 。除非需要动画的支持 ,否则没有理由使用GIF而不是PNG-8。
(5)PNG-24是无损的 、使用直接色的点阵图 。PNG-24的优点在于它压缩了图片的数据 ,使得同样效果的图片 ,PNG-24格式的文件大小要比BMP小得多 。当然 ,PNG24的图片还是要比JPEG 、GIF 、PNG-8大得多。
(6)SVG是无损的矢量图 。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成 。当放大SVG图片时 ,看到的还是线和曲线 ,而不会出现像素点 。SVG图片在放大时 ,不会失真 ,所以它适合用来绘制Logo 、Icon等 。
(7)WebP是谷歌开发的一种新图片格式 ,WebP是同时支持有损和无损压缩的 、使用直接色的点阵图 。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢?就是说相同质量的图片 ,WebP具有更小的文件体积 。现在网站上充满了大量的图片 ,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量 ,进而降低访问延迟 ,提升访问体验 。目前只有Chrome浏览器和Opera浏览器支持WebP格式 ,兼容性不太好 。
在无损压缩的情况下 ,相同质量的WebP图片 ,文件大小要比PNG小26%; 在有损压缩的情况下 ,具有相同图片精度的WebP图片 ,文件大小要比JPEG小25%~34%; WebP图片格式支持图片透明度 ,一个无损压缩的WebP图片 ,如果要支持透明度只需要22%的格外文件大小 。对 sticky 定位的理解
sticky 英文字面意思是粘贴 ,所以可以把它称之为粘性定位 。语法:position: sticky; 基于用户的滚动位置来定位 。
粘性定位的元素是依赖于用户的滚动 ,在 position:relative 与 position:fixed 定位之间切换 。它的行为就像 position:relative; 而当页面滚动超出目标区域时 ,它的表现就像 position:fixed;,它会固定在目标位置。元素定位表现为在跨越特定阈值前为相对定位 ,之后为固定定位 。这个特定阈值指的是 top, right, bottom 或 left 之一 ,换言之,指定 top, right, bottom 或 left 四个阈值其中之一 ,才可使粘性定位生效 。否则其行为与相对定位相同。
渐进增强和优雅降级之间的区别
(1)渐进增强(progressive enhancement):主要是针对低版本的浏览器进行页面重构 ,保证基本的功能情况下 ,再针对高级浏览器进行效果 、交互等方面的改进和追加功能 ,以达到更好的用户体验 。 (2)优雅降级 graceful degradation: 一开始就构建完整的功能 ,然后再针对低版本的浏览器进行兼容 。
两者区别:
优雅降级是从复杂的现状开始的 ,并试图减少用户体验的供给;而渐进增强是从一个非常基础的 ,能够起作用的版本开始的 ,并在此基础上不断扩充 ,以适应未来环境的需要; 降级(功能衰竭)意味着往回看 ,而渐进增强则意味着往前看 ,同时保证其根基处于安全地带 。“优雅降级 ”观点认为应该针对那些最高级 、最完善的浏览器来设计网站 。而将那些被认为“过时 ”或有功能缺失的浏览器下的测试工作安排在开发周期的最后阶段 ,并把测试对象限定为主流浏览器(如 IE、Mozilla 等)的前一个版本 。 在这种设计范例下,旧版的浏览器被认为仅能提供“简陋却无妨 (poor, but passable) ” 的浏览体验 。可以做一些小的调整来适应某个特定的浏览器 。但由于它们并非我们所关注的焦点 ,因此除了修复较大的错误之外 ,其它的差异将被直接忽略 。
“渐进增强 ”观点则认为应关注于内容本身 。内容是建立网站的诱因,有的网站展示它 ,有的则收集它 ,有的寻求 ,有的操作 ,还有的网站甚至会包含以上的种种 ,但相同点是它们全都涉及到内容 。这使得“渐进增强 ”成为一种更为合理的设计范例 。这也是它立即被 Yahoo 所采纳并用以构建其“分级式浏览器支持 (Graded Browser Support) ”策略的原因所在 。
如何获取安全的 undefined 值?
因为 undefined 是一个标识符 ,所以可以被当作变量来使用和赋值 ,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值 ,因此返回结果是 undefined 。void 并不改变表达式的结果 ,只是让表达式不返回值 。因此可以用 void 0 来获得 undefined。
new 一个构造函数 ,如果函数返回 return {} 、 return null , return 1 , return true 会发生什么情况?
如果函数返回一个对象,那么new 这个函数调用返回这个函数的返回对象 ,否则返回 new 创建的新对象
渲染过程中遇到 JS 文件如何处理?
JavaScript 的加载 、解析与执行会阻塞文档的解析 ,也就是说,在构建 DOM 时 ,HTML 解析器若遇到了 JavaScript ,那么它会暂停文档的解析 ,将控制权移交给 JavaScript 引擎 ,等 JavaScript 引擎运行完毕 ,浏览器再从中断的地方恢复继续解析文档 。也就是说 ,如果想要首屏渲染的越快 ,就越不应该在首屏就加载 JS 文件 ,这也是都建议将 script 标签放在 body 标签底部的原因 。当然在当下 ,并不是说 script 标签必须放在底部 ,因为你可以给 script 标签添加 defer 或者 async 属性 。
Sass、Less 是什么?为什么要使用他们?
他们都是 CSS 预处理器 ,是 CSS 上的一种抽象层 。他们是一种特殊的语法/语言编译成 CSS 。 例如 Less 是一种动态样式语言 ,将 CSS 赋予了动态语言的特性,如变量 ,继承 ,运算, 函数 ,LESS 既可以在客户端上运行 (支持 IE 6+, Webkit, Firefox) ,也可以在服务端运行 (借助 Node.js) 。
为什么要使用它们?
结构清晰 ,便于扩展 。 可以方便地屏蔽浏览器私有语法差异 。封装对浏览器语法差异的重复处理 , 减少无意义的机械劳动 。 可以轻松实现多重继承 。 完全兼容 CSS 代码 ,可以方便地应用到老项目中 。LESS 只是在 CSS 语法上做了扩展 ,所以老的 CSS 代码也可以与 LESS 代码一同编译 。POST和PUT请求的区别
PUT请求是向服务器端发送数据 ,从而修改数据的内容 ,但是不会增加数据的种类等 ,也就是说无论进行多少次PUT操作 ,其结果并没有不同。(可以理解为时更新数据) POST请求是向服务器端发送数据 ,该请求会改变数据的种类等资源 ,它会创建新的内容 。(可以理解为是创建数据)IndexedDB有哪些特点?
IndexedDB 具有以下特点:
键值对储存:IndexedDB 内部采用对象仓库(object store)存放数据 。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中 ,数据以"键值对"的形式保存 ,每一个数据记录都有对应的主键,主键是独一无二的 ,不能有重复 ,否则会抛出一个错误 。 异步:IndexedDB 操作时不会锁死浏览器 ,用户依然可以进行其他操作 ,这与 LocalStorage 形成对比 ,后者的操作是同步的 。异步设计是为了防止大量数据的读写 ,拖慢网页的表现 。 支持事务:IndexedDB 支持事务(transaction) ,这意味着一系列操作步骤之中 ,只要有一步失败 ,整个事务就都取消 ,数据库回滚到事务发生之前的状态 ,不存在只改写一部分数据的情况 。 同源限制: IndexedDB 受到同源限制 ,每一个数据库对应创建它的域名 。网页只能访问自身域名下的数据库,而不能访问跨域的数据库 。 储存空间大:IndexedDB 的储存空间比 LocalStorage 大得多 ,一般来说不少于 250MB ,甚至没有上限 。 支持二进制储存:IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象) 。AJAX
const getJSON = function(url) { return new Promise((resolve, reject) => { const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject(Microsoft.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(); }) }实现数组原型方法
forEach
Array.prototype.forEach2 = 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) // this 就是当前的数组 const len = O.length >>> 0 // 后面有解释 let k = 0 while (k < len) { if (k in O) { callback.call(thisArg, O[k], k, O); } k++; } }O.length >>> 0 是什么操作?就是无符号右移 0 位 ,那有什么意义嘛?就是为了保证转换后的值为正整数 。其实底层做了 2 层转换 ,第一是非 number 转成 number 类型 ,第二是将 number 转成 Uint32 类型
map
基于 forEach 的实现能够很容易写出 map 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) { + Array.prototype.map2 = 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 + let k = 0, res = [] while (k < len) { if (k in O) { - callback.call(thisArg, O[k], k, O); + res[k] = callback.call(thisArg, O[k], k, O); } k++; } + return res }filter
同样 ,基于 forEach 的实现能够很容易写出 filter 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) { + Array.prototype.filter2 = 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 + let k = 0, res = [] while (k < len) { if (k in O) { - callback.call(thisArg, O[k], k, O); + if (callback.call(thisArg, O[k], k, O)) { + res.push(O[k]) + } } k++; } + return res }some
同样 ,基于 forEach 的实现能够很容易写出 some 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) { + Array.prototype.some2 = 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); + if (callback.call(thisArg, O[k], k, O)) { + return true + } } k++; } + return false }reduce
Array.prototype.reduce2 = function(callback, initialValue) { 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, acc if (arguments.length > 1) { acc = initialValue } else { // 没传入初始值的时候 ,取数组中第一个非 empty 的值为初始值 while (k < len && !(k in O)) { k++ } if (k > len) { throw new TypeError( Reduce of empty array with no initial value ); } acc = O[k++] } while (k < len) { if (k in O) { acc = callback(acc, O[k], k, O) } k++ } return acc }JavaScript 类数组对象的定义?
一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象 ,类数组对象和数组类似 ,但是不能调用数组的方法 。常见的类数组对象有 arguments 和 DOM 方法的返回结果 ,还有一个函数也可以被看作是类数组对象 ,因为它含有 length 属性值 ,代表可接收的参数个数 。
常见的类数组转换为数组的方法有这样几种:
(1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);(2)通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);(3)通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);(4)通过 Array.from 方法来实现转换
Array.from(arrayLike);如何判断元素是否到达可视区域
以图片显示为例:
window.innerHeight 是浏览器可视区的高度; document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离; imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离); 内容达到显示区域的:img.offsetTop < window.innerHeight + document.body.scrollTop;Virtual Dom 的优势在哪里?
Virtual Dom 的优势」其实这道题目面试官更想听到的答案不是上来就说「直接操作/频繁操作 DOM 的性能差」 ,如果 DOM 操作的性能如此不堪,那么 jQuery 也不至于活到今天 。所以面试官更想听到 VDOM 想解决的问题以及为什么频繁的 DOM 操作会性能差。
首先我们需要知道:
DOM 引擎 、JS 引擎 相互独立 ,但又工作在同一线程(主线程) JS 代码调用 DOM API 必须 挂起 JS 引擎 、转换传入参数数据 、激活 DOM 引擎 ,DOM 重绘后再转换可能有的返回值,最后激活 JS 引擎并继续执行若有频繁的 DOM API 调用 ,且浏览器厂商不做“批量处理 ”优化 , 引擎间切换的单位代价将迅速积累若其中有强制重绘的 DOM API 调用 ,重新计算布局 、重新绘制图像会引起更大的性能消耗 。
其次是 VDOM 和真实 DOM 的区别和优化:
虚拟 DOM 不会立马进行排版与重绘操作 虚拟 DOM 进行频繁修改 ,然后一次性比较并修改真实 DOM 中需要改的部分 ,最后在真实 DOM 中进行排版与重绘 ,减少过多DOM节点排版与重绘损耗 虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版 ,因为最终与真实 DOM 比较差异 ,可以只渲染局部说一下 web worker
在 HTML 页面中 ,如果在执行脚本时 ,页面的状态是不可相应的 ,直到脚本执行完成后 ,页面才变成可相应 。web worker 是运行在后台的 js,独立于其他脚本 ,不会影响页面的性能。 并且通过 postMessage 将结果回传到主线程 。这样在进行复杂操作的时候 ,就不会阻塞主线程了 。
如何创建 web worker:
检测浏览器对于 web worker 的支持性 创建 web worker 文件(js,回传函数等) 创建 web worker 对象对keep-alive的理解
HTTP1.0 中默认是在每次请求/应答 ,客户端和服务器都要新建一个连接 ,完成之后立即断开连接 ,这就是短连接 。当使用Keep-Alive模式时 ,Keep-Alive功能使客户端到服务器端的连接持续有效 ,当出现对服务器的后继请求时 ,Keep-Alive功能避免了建立或者重新建立连接 ,这就是长连接 。其使用方法如下:
HTTP1.0版本是默认没有Keep-alive的(也就是默认会发送keep-alive) ,所以要想连接得到保持 ,必须手动配置发送Connection: keep-alive字段 。若想断开keep-alive连接 ,需发送Connection:close字段; HTTP1.1规定了默认保持长连接 ,数据传输完成了保持TCP连接不断开 ,等待在同域名下继续用这个通道传输数据 。如果需要关闭,需要客户端发送Connection:close首部字段 。Keep-Alive的建立过程:
客户端向服务器在发送请求报文同时在首部添加发送Connection字段 服务器收到请求并处理 Connection字段 服务器回送Connection:Keep-Alive字段给客户端 客户端接收到Connection字段 Keep-Alive连接建立成功服务端自动断开过程(也就是没有keep-alive):
客户端向服务器只是发送内容报文(不包含Connection字段) 服务器收到请求并处理 服务器返回客户端请求的资源并关闭连接 客户端接收资源 ,发现没有Connection字段 ,断开连接客户端请求断开连接过程:
客户端向服务器发送Connection:close字段 服务器收到请求并处理connection字段 服务器回送响应资源并断开连接 客户端接收资源并断开连接开启Keep-Alive的优点:
较少的CPU和内存的使⽤(由于同时打开的连接的减少了); 允许请求和应答的HTTP管线化; 降低拥塞控制 (TCP连接减少了); 减少了后续请求的延迟(⽆需再进⾏握⼿); 报告错误⽆需关闭TCP连;开启Keep-Alive的缺点:
长时间的Tcp连接容易导致系统资源无效占用,浪费系统资源 。同样是重定向 ,307 ,303 ,302的区别?
302是http1.0的协议状态码 ,在http1.1版本的时候为了细化302状态码⼜出来了两个303和307 。 303明确表示客户端应当采⽤get⽅法获取资源 ,他会把POST请求变为GET请求进⾏重定向 。 307会遵照浏览器标准 ,不会从post变为get 。
插入排序–时间复杂度 n^2
题目描述:实现一个插入排序
实现代码如下:
function insertSort(arr) { for (let i = 1; i < arr.length; i++) { let j = i; let target = arr[j]; while (j > 0 && arr[j - 1] > target) { arr[j] = arr[j - 1]; j--; } arr[j] = target; } return arr; } // console.log(insertSort([3, 6, 2, 4, 1]));什么是中间人攻击?如何防范中间人攻击?
中间⼈ (Man-in-the-middle attack, MITM) 是指攻击者与通讯的两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对⽅直接对话, 但事实上整个会话都被攻击者完全控制 。在中间⼈攻击中 ,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。
攻击过程如下:
客户端发送请求到服务端 ,请求被中间⼈截获 服务器向客户端发送公钥 中间⼈截获公钥 ,保留在⾃⼰⼿上 。然后⾃⼰⽣成⼀个伪造的公钥 ,发给客户端 客户端收到伪造的公钥后 ,⽣成加密hash值发给服务器 中间⼈获得加密hash值 ,⽤⾃⼰的私钥解密获得真秘钥,同时⽣成假的加密hash值,发给服务器 服务器⽤私钥解密获得假密钥,然后加密数据传输给客户端对事件委托的理解
(1)事件委托的概念事件委托本质上是利用了浏览器事件冒泡的机制 。因为事件在冒泡过程中会上传到父节点 ,父节点可以通过事件对象获取到目标节点 ,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件 ,这种方式称为事件委托(事件代理)。
使用事件委托可以不必要为每一个子元素都绑定一个监听事件 ,这样减少了内存上的消耗 。并且使用事件代理还可以实现事件的动态绑定 ,比如说新增了一个子节点 ,并不需要单独地为它添加一个监听事件 ,它绑定的事件会交给父元素中的监听函数来处理 。
(2)事件委托的特点 减少内存消耗如果有一个列表 ,列表之中有大量的列表项 ,需要在点击列表项的时候响应一个事件:
<ul id="list"> <li>item 1</li> <li>item 2</li> <li>item 3</li> ...... <li>item n</li> </ul>如果给每个列表项一一都绑定一个函数 ,那对于内存消耗是非常大的 ,效率上需要消耗很多性能 。因此 ,比较好的方法就是把这个点击事件绑定到他的父层 ,也就是 ul 上 ,然后在执行事件时再去匹配判断目标元素,所以事件委托可以减少大量的内存消耗 ,节约效率 。
动态绑定事件给上述的例子中每个列表项都绑定事件 ,在很多时候,需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素 ,那么在每一次改变的时候都需要重新给新增的元素绑定事件 ,给即将删去的元素解绑事件;如果用了事件委托就没有这种麻烦了 ,因为事件是绑定在父层的 ,和目标元素的增减是没有关系的 ,执行到目标元素是在真正响应执行事件函数的过程中去匹配的 ,所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的 。
// 来实现把 #list 下的 li 元素的事件代理委托到它的父层元素也就是 #list 上: // 给父层元素绑定事件 document.getElementById(list).addEventListener(click, function (e) { // 兼容性处理 var event = e || window.event; var target = event.target || event.srcElement; // 判断是否匹配目标元素 if (target.nodeName.toLocaleLowerCase === li) { console.log(the content is: , target.innerHTML); } });在上述代码中 , target 元素则是在 #list 元素之下具体被点击的元素 ,然后通过判断 target 的一些属性(比如:nodeName ,id 等等)可以更精确地匹配到某一类 #list li 元素之上;
(3)局限性当然 ,事件委托也是有局限的 。比如 focus 、blur 之类的事件没有事件冒泡机制 ,所以无法实现事件委托;mousemove 、mouseout 这样的事件 ,虽然有事件冒泡,但是只能不断通过位置去计算定位 ,对性能消耗高 ,因此也是不适合于事件委托的 。
当然事件委托不是只有优点,它也是有缺点的 ,事件委托会影响页面性能 ,主要影响因素有:
元素中 ,绑定事件委托的次数; 点击的最底层元素 ,到绑定事件元素之间的DOM层数;在必须使用事件委托的地方 ,可以进行如下的处理:
只在必须的地方 ,使用事件委托 ,比如:ajax的局部刷新区域 尽量的减少绑定的层级 ,不在body元素上 ,进行绑定 减少绑定的次数 ,如果可以 ,那么把多个事件的绑定 ,合并到一次事件委托中去,由这个事件委托的回调 ,来进行分发 。创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!