(立下flag)每日10道前端面试题-02

441 阅读14分钟

1.居中为什么要使用transform(为什么不使用marginLeft/Top)(阿里)


transform 属于合成属性(composite property),对合成属性进行 transition/animation 动画将会创建一个合成层(composite layer),这使得被动画元素在一个独立的层中进行动画。通常情况下,浏览器会将一个层的内容先绘制进一个位图中,然后再作为纹理(texture)上传到 GPU,只要该层的内容不发生改变,就没必要进行重绘(repaint),浏览器会通过重新复合(recomposite)来形成一个新的帧。

top/left属于布局属性,该属性的变化会导致重排(reflow/relayout),所谓重排即指对这些节点以及受这些节点影响的其它节点,进行CSS计算->布局->重绘过程,浏览器需要为整个层进行重绘并重新上传到 GPU,造成了极大的性能开销。

2.首屏加载优化

  • spa首屏加载慢,主要是打包后的js文件过大,阻塞加载所致。那么如何减小js的体积呢? 那就是把库文件单独拿出来加载,不要参与打包。

  • 图片懒加载方案,这是史前前端就开始用的技术,在 JQuery 或者各种框架都有成熟方案

  • 资源压缩,其中关闭sourcemap,包的体积减少将近一半,sourcemap是为了方便线上调试用的,因为线上代码都是压缩过的,导致调试极为不便,而有了sourcemap,就等于加了个索引字典,出了问题可以定位到源代码的位置。 但是,这个玩意是每个js都带一个sourcemap,有时sourcemap会很大,拖累了整个项目加载速度,为了节省加载时间,我们将其关闭掉。就这一句话就可以关闭sourcemap了,很简单。

  • 开启http2.0,http1.x时代的优化折磨好长一段时间,各种奇淫技巧为了弥补http1的短板,影响着我们的开发专注度,好在http2已经开始盛行,相信不久的将来可以完全替代http1。现在基本主流的浏览器都支持http2.0了

  • cdn,已经见不到几个web 产品不用 cdn 了,尤其是云计算厂商崛起后 cdn 很便宜了

  • 缓存,缓存有很多种方式,大部分是服务器端处理的,而客户端处理的则一般是把资源缓存在本地。比较有效的本地缓存一般是用application-cache或者service worker将网站的资源缓存到本地,再次访问时直接调用本地的缓存资源,几乎是本地打开的速度

  • 雪碧图,很古老的技术了,http2 使用后也是效果有限了

  • css 放头,js 放最后,这种方式适合工程化之前,现在基本都用打包工具代替了

3. 10w 条记录的数组,一次性渲染到页面上,如何处理可以不冻结UI?

页面上有个空的无序列表节点ul,其idlist-with-big-data,现需要往列表插入 10w 个li,每个列表项的文本内容可自行定义,且要求当每个li被单击时,通过alert显示列表项内的文本内容。

分析

可能在看到这个问题的第一眼,我们可能会想到这样的解决办法:获取ul元素,然后新建li元素,并设置好li的文本内容和监听器绑定,然后在循环里对ul进行append操作,即可能想到的是以下代码实现。

实践上述代码,我们发现界面体验很不友好,卡顿感严重。出现卡顿感的主要原因是,在每次循环中,都会修改 DOM 结构,并且由于数据量大,导致循环执行时间过长,浏览器的渲染帧率过低。

事实上,包含 100000 个 li 的长列表,用户不会立即看到全部,只会看到少部分。因此,对于大部分的 li 的渲染工作,我们可以延时完成。

我们可以从 减少 DOM 操作次数 和 缩短循环时间 两个方面减少主线程阻塞的时间。

DocumentFragment
The DocumentFragment interface represents a minimal document object that has no parent. It is used as a lightweight version of Document that stores a segment of a document structure comprised of nodes just like a standard document. The key difference is that because the document fragment isn't part of the active document tree structure, changes made to the fragment don't affect the document, cause reflow, or incur any performance impact that can occur when changes are made.

在 MDN 的介绍中,我们知道可以通过 DocumentFragment 的使用,减少 DOM 操作次数,降低回流对性能的影响。

requestAniminationFrame
The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.

在缩短循环时间方面,我们可以通过 分治 的思想,将 100000 个 li 分批插入到页面中,并且我们通过 requestAniminationFrame 在页面重绘前插入新节点。

事件绑定

如果我们想监听海量元素,推荐方法是使用 JavaScript 的事件机制,实现事件委托,这样可以显著减少 DOM 事件注册 的数量。

4. 考虑到性能问题,如何快速从一个巨大的数组中随机获取部分元素

5. addEventListener第三个参数是什么,还可以是别的吗(淘系前端)

从官方文档看,addEventListener 方法使用如下:

target.addEventListener(type, listener[, options]);target.addEventListener(type, listener[, useCapture]);

还有一个兼容性不好的使用方法就不提了,也不太常用。

主要关注下第三个参数,可以设置为bool类型(useCapture)或者object类型(options)。

主要关注下第三个参数,可以设置为bool类型(useCapture)或者object类型(options)。

  • options包括三个布尔值选项:

    • capture: 默认值为false(即 使用事件冒泡). 是否使用事件捕获;

    • once: 默认值为false. 是否只调用一次,if true,会在调用后自动销毁listener

    • passive: if true, 意味着listener永远不远调用preventDefault方法,如果又确实调用了的话,浏览器只会console一个warning,而不会真的去执行preventDefault方法。根据规范,默认值为false. 但是chrome, Firefox等浏览器为了保证滚动时的性能,在document-level nodes(Window, Document, Document.body)上针对touchstart和touchmove事件将passive默认值改为了true, 保证了在页面滚动时不会因为自定义事件中调用了preventDefault而阻塞页面渲染。

  • useCapture: 默认值为false(即 使用事件冒泡)。

如何与removeEventListener 方法合作?

removeEventListener的参数与addEventListener类似:

target.removeEventListener(type, listener[, options]);target.removeEventListener(type, listener[, useCapture]);

在移除之前添加的监听事件时,很显然需要传入同样的type和listener,那第三个参数options和useCapture呢?

只会检查addEventListener的useCapture或options中的capture值。

例如:

是否一定要与removeEventlister成对儿出现?

当DOM元素与事件拥有不同的生命周期时,倘若不remove掉eventListener就有可能导致内存泄漏(保留或增加了不必要的内存占用)。比如在单页应用中,切换了页面后,原组件已经卸载,但其注册在document上的事件却保留了下来,白白占用了内存空间。所以removeEventlister与addEventListener成对儿出现是best practice,可以避免可能出现的内存泄漏问题。

6. src与href属性的区别

src和href之间存在区别,能混淆使用。src用于替换当前元素,href用于在当前文档和引用资源之间确立联系。

src是source的缩写,指向外部资源的位置,指向的内容将会嵌入到文档中当前标签所在位置;在请求src资源时会将其指向的资源下载并应用到文档内,例如js脚本,img图片和frame等元素。

<script src ="js.js"></script>

当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执行完毕,图片和框架等元素也如此,类似于将所指向资源嵌入当前标签内。这也是为什么将js脚本放在底部而不是头部。

href是Hypertext Reference的缩写,指向网络资源所在位置,建立和当前元素(锚点)或当前文档(链接)之间的链接,如果我们在文档中添加

<link href="common.css" rel="stylesheet"/>

那么浏览器会识别该文档为css文件,就会并行下载资源并且不会停止对当前文档的处理。这也是为什么建议使用link方式来加载css,而不是使用@import方式。

又想到了一个比喻,我们处理穿衣服,src相当于换内裤,换内裤必须停下其他动作,来换内裤;href相当于刷牙,我可以一遍换一边刷牙一边看手机

[(立下flag)每日10道前端面试题-01] 上篇题集里谈到了import和link 的区别(mp.weixin.qq.com/s?__...

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

此题是我综合之前的开发经验以及遇到的JS各种坑汇集而成。此题涉及的知识点众多,包括变量定义提升、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级等等。

此题包含7小问,分别说下。

第一问

===

先看此题的上半部分做了什么,首先定义了一个叫Foo的函数,之后为Foo创建了一个叫getName的静态属性存储了一个匿名函数,之后为Foo的原型对象新创建了一个叫getName的匿名函数。之后又通过函数变量表达式创建了一个getName的函数,最后再声明一个叫getName函数。

第一问的 Foo.getName 自然是访问Foo函数上存储的静态属性,自然是2,没什么可说的。

第二问

===

第二问,直接调用 getName 函数。既然是直接调用那么就是访问当前上文作用域内的叫getName的函数,所以跟1 2 3都没什么关系。此题有无数面试者回答为5。此处有两个坑,一是变量声明提升,二是函数表达式。

第三问

===

第三问的 Foo().getName(); 先执行了Foo函数,然后调用Foo函数的返回值对象的getName属性函数。

Foo函数的第一句 getName = function () { alert (1); }; 是一句函数赋值语句,注意它没有var声明,所以先向当前Foo函数作用域内寻找getName变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有getName变量,找到了,也就是第二问中的alert(4)函数,将此变量的值赋值为 function(){alert(1)}。

此处实际上是将外层作用域内的getName函数修改了。

注意:此处若依然没有找到会一直向上查找到window对象,若window对象中也没有getName属性,就在window对象中创建一个getName变量。

之后Foo函数的返回值是this,而JS的this问题博客园中已经有非常多的文章介绍,这里不再多说。

简单的讲,this的指向是由所在函数的调用方式决定的。而此处的直接调用方式,this指向window对象。

遂Foo函数返回的是window对象,相当于执行 window.getName() ,而window中的getName已经被修改为alert(1),所以最终会输出1

此处考察了两个知识点,一个是变量作用域问题,一个是this指向问题。

第四问

===

直接调用getName函数,相当于 window.getName() ,因为这个变量已经被Foo函数执行时修改了,遂结果与第三问相同,为1

第五问

===

第五问 newFoo.getName(); ,此处考察的是js的运算符优先级问题。

通过查上表可以得知点(.)的优先级高于new操作,遂相当于是:

new (Foo.getName)();

所以实际上将getName函数作为了构造函数来执行,遂弹出2。

第六问

===

第六问 newFoo().getName() ,首先看运算符优先级括号高于new,实际执行为

(new Foo()).getName()

遂先执行Foo函数,而Foo此时作为构造函数却有返回值,所以这里需要说明下js中的构造函数返回值问题。

第七问

===

第七问, newnewFoo().getName(); 同样是运算符优先级问题。

最终实际执行为:

new ((new Foo()).getName)();

先初始化Foo的实例化对象,然后将其原型上的getName函数作为构造函数再次new。

遂最终结果为3

===2016年03月23日更新===

这里引用 @于明昊 的评论,更详细的解释了第7问:

这里确实是(new Foo()).getName(),但是跟括号优先级高于成员访问没关系,实际上这里成员访问的优先级是最高的,因此先执行了 .getName,但是在进行左侧取值的时候, new Foo() 可以理解为两种运算:new 带参数(即 new Foo())和函数调用(即 先 Foo() 取值之后再 new),而 new 带参数的优先级是高于函数调用的,因此先执行了 new Foo(),或得 Foo 类的实例对象,再进行了成员访问 .getName。

最后

==

就答题情况而言,第一问100%都可以回答正确,第二问大概只有50%正确率,第三问能回答正确的就不多了,第四问再正确就非常非常少了。其实此题并没有太多刁钻匪夷所思的用法,都是一些可能会遇到的场景,而大多数人但凡有1年到2年的工作经验都应该完全正确才对。

只能说有一些人太急躁太轻视了,希望大家通过此文了解js一些特性。

8 浏览器如何解析css选择器?

浏览器会『从右往左』解析CSS选择器。

我们知道DOM Tree与Style Rules合成为 Render Tree,实际上是需要将Style Rules附着到DOM Tree上,因此需要根据选择器提供的信息对DOM Tree进行遍历,才能将样式附着到对应的DOM元素上。
以下这段css为例
.mod-nav h3 span {font-size: 16px;}
我们对应的DOM Tree 如下

若从左向右的匹配,过程是:

  1. 从 .mod-nav 开始,遍历子节点 header 和子节点 div

  2. 然后各自向子节点遍历。在右侧 div 的分支中

  3. 最后遍历到叶子节点 a ,发现不符合规则,需要回溯到 ul 节点,再遍历下一个 li-a,一颗DOM树的节点动不动上千,这种效率很低。

如果从右至左的匹配:

  1. 先找到所有的最右节点 span,对于每一个 span,向上寻找节点 h3

  2. 由 h3再向上寻找 class=mod-nav 的节点

  3. 最后找到根元素 html 则结束这个分支的遍历。

后者匹配性能更好,是因为从右向左的匹配在第一步就筛选掉了大量的不符合条件的最右节点(叶子节点);而从左向右的匹配规则的性能都浪费在了失败的查找上面。

9 DOM Tree是如何构建的?

  1. 转码: 浏览器将接收到的二进制数据按照指定编码格式转化为HTML字符串

  2. 生成Tokens: 之后开始parser,浏览器会将HTML字符串解析成Tokens

  3. 构建Nodes: 对Node添加特定的属性,通过指针确定 Node 的父、子、兄弟关系和所属 treeScope

  4. 生成DOM Tree: 通过node包含的指针确定的关系构建出DOM Tree

10 PUT和POST都是给服务器发送新增资源,有什么区别?

PUT 和POST方法的区别是,PUT方法是幂等的:连续调用一次或者多次的效果相同(无副作用),而POST方法是非幂等的。

除此之外还有一个区别,通常情况下,PUT的URI指向是具体单一资源,而POST可以指向资源集合。

举个例子,我们在开发一个博客系统,当我们要创建一篇文章的时候往往用POST https://www.jianshu.com/articles,这个请求的语义是,在articles的资源集合下创建一篇新的文章,如果我们多次提交这个请求会创建多个文章,这是非幂等的。

PUT https://www.jianshu.com/articles/820357430的语义是更新对应文章下的资源(比如修改作者名称等),这个URI指向的就是单一资源,而且是幂等的,比如你把『刘德华』修改成『蔡徐坤』,提交多少次都是修改成『蔡徐坤』

ps: 『POST表示创建资源,PUT表示更新资源』这种说法是错误的,两个都能创建资源,根本区别就在于幂等性