一大堆搜刮来的前端面试题

最近又开始找工作之旅,通常说面试造火箭,入职拧螺丝,其实对于前端来说,并不是这样,前端当中大部分问到的题都是你工作中会遇到和使用到的,不过很多事情答不上的原因是可能有时候会用,但是没有去理解它的原理,也不排除面试官专门问那些自己可能都答不上来的题。

参照了很多文章,将自己平时没有注意的一些知识点记录下来,方便自己查阅的同时,也分享给大家。

注意:有一部分是我个人理解和观点,我会用【】将它标记出来,不一定正确,看看就好。

1. html

1.1 语义化

1、为什么需要语义化

  • 易修改、易维护。
  • 无障碍阅读支持。
  • 搜索引擎友好,利于 SEO。
  • 面向未来的 HTML,浏览器在未来可能提供更丰富的支持。

语义元素均有一个共同特点——他们均不做任何事情。换句话说,语义元素仅仅是页面结构的规范化,并不会对内容有本质的影响。

image-20200417132554712

1.1.1 头部<header>

  • 有两种用法,第一是标注内容的标题,第二是标注网页的页眉。
  • 除非必要(内容标题附带其它信息的情况下:发布时间、作者等),一般不在内容中使用。
  • 网页中可以包含多个<header>元素。按照 HTML5 的规定,<header>都应包含某个级别的标题,所以应隐式或显式地包含标题,通常将不希望显示的标题设置为display: none;,一方面遵守规范,另一方面则提供了无障碍阅读而不至于影响到页面设计。

1.1.2 导航栏<nav>

导航栏使用<nav>看起来是理所当然的,进一步,它也用于一组文章的链接。一个页面可以包含多个<nav>元素,但通常仅仅在页面的主要导航部分使用它。

1.1.3 附注<aside>

<aside>元素并不仅仅是侧栏,它表示与它周围文本没有密切关系的内容。文章中同样可以使用<aside>元素,来说明文章的附加内容、解释说明某个观点、相关内容链接等等。

<aside>用于侧栏时,其表示整个网页的附加内容。通常的广告区域、搜索、分享链接则位于侧栏。侧栏中的<aside>元素规定了一个区域,通常是带有标题的内容。

<section>标签适合标记的内容区块:

  • 与页面主体并列显示的小内容块。
  • 独立性内容,清单、表单等。
  • 分组内容,如 CMS 系统中的文章分类区块。
  • 比较长文档的一部分,可能仅仅是为了正确规定页面大纲。

<div>标签依然是可用的,当你觉得使用其它标签都不太合适的时候。新的语义元素出现之前,我们总是这么干的!

同可“包罗万象”的<header>元素不同,标准规定<footer标签仅仅可以包含版权、来源信息、法律限制等等之类的文本或链接信息。如果想要在页脚中包含其它内容,可以使用熟悉的<div>来帮忙。

<div>
  <aside>
  <!-- 其它内容 -->
  </aside>
  
  <footer>
    <!-- 法律、版权、来源、联系信息等 -->
  </footer>
</div>

1.1.5 主要内容<main>

在早先的 HTML5 版本中并没有规定页面主体的标签,相关的书中经常会说:除去头部、尾部、侧栏等其它部分,剩下的自然是主体部分。

然而,HTML5.1 中规定了一个<main>标签来标识主体内容。<main>标签不能包含在页面其它区块元素中,通常是<body>的子标签,或者是全局<div>的子标签。<main>标签可以帮助屏幕阅读工具识别页面的主体部分,从而让访问者迅速得到有用的信息。

1.1.6 文章<article>

<article>表示一个完整的、自成一体的内容块。如文章或新闻报道。<article>应包含完整的标题、文章署名、发布时间、正文。当语义与表现发生冲突,例如有时需要将文章分多个页面显示,那么需要把每个页面的文章区域都用<article>标记。

文章中包含插图时,使用新的语义元素<figure>标签。

<article>
  <h1>标题</h1>
  <p>
    <!-- 内容 -->
  </p>
  <figure>
    <img src="#" alt="插图">
    <figcaption>这是一个插图</figcaption> <!--文章中插图图像的标题-->
  </figure>
</article>

上述情况下,<figcaption>包含了关于插图的详细解释,则<img>alt属性可以略去。

1.2 图片的懒加载和预加载

  • 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
  • 懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。

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

2. CSS

2.1 垂直居中

absolute: position: absolute 配合 top:50%;left:50%;transform:translate(-50%, -50%)

aotubot: display:flex 配合 margin:auto

flex: display:flex 配合 align-items:centerjustify-content:center

grid: display:grid 配合 place-content:center;

2.2 flex布局

【弹性布局在现代前端开发中是一个非常重要的技术,因为现在大多数前端开发都不再兼容老版本IE浏览器,所以在开发的过程中会大量使用到flex布局。】

  • 该布局模型的目的是提供一种更加高效的方式来对容器中的条目进行布局、对齐和分配空间。
  • 在传统的布局方式中,block 布局是把块在垂直方向从上到下依次排列的;而 inline 布局则是在水平方向来排列。
  • 弹性盒布局并没有这样内在的方向限制,可以由开发人员自由操作。

2.3 CSS3新属性

【在现代前端开发中,除非业务需要,否则不会再去兼容老版本IE浏览器。

兼容老版本IE浏览器是一个成本非常高的事情,一般只有中大企业才会考虑进行兼容。

现在很多框架都已经开始逐步放弃老版本的IE浏览器,所以几乎所有的CSS属性都可以放心的使用。】

  1. RGBA和透明度。
  2. background-image background-origin(content-box/padding-box/border-box) background-size background-repeat
  3. word-wrap(对长的不可分割单词换行)word-wrap:break-word
  4. 文字阴影:text-shadow: 5px 5px 5px #FF0000;(水平阴影,垂直阴影,模糊距离,阴影颜色)。
  5. font-face属性:定义自己的字体。
  6. 圆角(边框半径):border-radius 属性用于创建圆角。
  7. 边框图片:border-image: url(border.png) 30 30 round
  8. 盒阴影:box-shadow: 10px 10px 5px #888888
  9. 媒体查询:定义两套CSS,当浏览器的尺寸变化时会采用不同的属性

2.4 兼容性

  1. 不同浏览器的标签默认的marginpadding不一样。*{margin:0;padding:0;}

  2. IE6双边距bug:块属性标签float后,又有横行的margin情况下,在IE6显示margin比设置的大。hack:display:inline;将其转化为行内属性。

  3. 渐进识别的方式,从总体中逐渐排除局部。首先,巧妙的使用“9”这一标记,将IE浏览器从所有情况中分离出来。接着,再次使用“+”将IE8和IE7、IE6分离开来,这样IE8已经独立识别。

    {
        background-color:#f1ee18;/*所有识别*/
        .background-color:#00deff\9; /*IE6、7、8识别*/
        +background-color:#a200ff;/*IE6、7识别*/
        _background-color:#1e0bd1;/*IE6识别*/
    }
  4. 设置较小高度标签(一般小于10px),在IE6,IE7中高度超出自己设置高度。hack:给超出高度的标签设置overflow:hidden;或者设置行高line-height 小于你设置的高度。

  5. IE下,可以使用获取常规属性的方法来获取自定义属性,也可以使用getAttribute()获取自定义属性;Firefox下,只能使用getAttribute()获取自定义属性。解决方法:统一通过getAttribute()获取自定义属性。

  6. Chrome 中文界面下默认会将小于 12px 的文本强制按照 12px 显示。

  7. 超链接访问过后hover样式就不出现了,被点击访问过的超链接样式不再具有hover和active了。解决方法是改变CSS属性的排列顺序:L-V-H-A ( love hate ): a:link {} a:visited {} a:hover {} a:active {}

【现代很多UI框架和样式库都会引入项目初始化样式,将很多样式进行统一初始化,(比如Normalize.css库,就是将不同的浏览器样式统一进行初始化)从而让每个浏览器能够表现得一致,并且因为有PostCss这个强大的插件,会自动的处理很多样式上的兼容性问题,所以当代前端开发大量的时间还是花在像表单验证这种前端逻辑上面。除非是做一个类似于苹果官网的网站。

2.5 选择器

  • id选择器(#myid)。
  • 类选择器(.myclassname)。
  • 标签选择器(div, h1, p)。
  • 相邻选择器(h1 + p)。
  • 子选择器(ul > li)。
  • 后代选择器(li a)。
  • 通配符选择器(*)。
  • 属性选择器(a[rel=”external”])。
  • 伪类选择器(a:hover, li:nth-child)。
  • 可继承的属性:font-size, font-family, color。
  • 不可继承的样式:border, padding, margin, width, height。
  • 优先级(就近原则):!important > [ id > class > tag ]。
  • !important 比内联优先级高

2.5.1 选择器算法

  • 元素选择符: 1。
  • class选择符: 10。
  • id选择符:100。
  • 元素标签:1000。
  • !important声明的样式优先级最高,如果冲突再进行计算。
  • 如果优先级相同,则选择最后出现的样式。
  • 继承得到的样式的优先级最低。

【在实际的项目开发中,我们用的最多的就是class选择符和内联样式,因为现在前端框架中有非常多的样式隔离的方式,所以样式与样式之间产生冲突可能并不再像过去一样经常发生。】

2.6 position

  • static(默认):按照正常文档流进行排列;
  • relative(相对定位):不脱离文档流,参考自身静态位置通过 top, bottom, left, right 定位;
  • absolute(绝对定位):参考距其最近一个不为static的父级元素通过top, bottom, left, right 定位;
  • fixed(固定定位):所固定的参照对像是可视窗口。
  • 类似于优先级机制:position:absolute/fixed优先级最高,有他们在时,float不起作用,display值需要调整。float 或者absolute定位的元素,只能是块元素或表格。
  • 同时设置top和bottom会将盒子拉开

2.7 visibility

当一个元素的visibility属性被设置成collapse值后,对于一般的元素,它的表现跟hidden是一样的。

  1. chrome中,使用collapse值和使用hidden没有区别。
  2. firefox,opera和IE,使用collapse值和使用display:none没有什么区别。

display:none 不显示对应的元素,在文档布局中不再分配空间(回流+重绘)

visibility:hidden 隐藏对应元素,在文档布局中仍保留原来的空间(重绘)

【实际应用中大部分还是使用display,就算要保留位置也一般调整该元素的透明度,opacity属性。】

2.8 浮动

浮动元素碰到包含它的边框或者浮动元素的边框停留。由于浮动元素不在文档流中,所以文档流的块框表现得就像浮动框不存在一样。浮动元素会漂浮在文档流的块框上。

浮动带来的问题:

  1. 父元素的高度无法被撑开,影响与父元素同级的元素。
  2. 与浮动元素同级的非浮动元素(内联元素)会跟随其后。
  3. 若非第一个元素浮动,则该元素之前的元素也需要浮动,否则会影响页面显示的结构。

清除浮动的方式:

  1. 父级div定义height。
  2. 最后一个浮动元素后加空div标签 并添加样式clear:both
  3. 包含浮动元素的父标签添加样式overflow为hidden或auto。
  4. 父级div定义zoom。

浮动时display:block,即可以调整盒子的大小。

【自从flex和grid布局的出现,浮动的应用场景就非常少了,因为它并不是那么方便,往往还伴随着清除浮动。虽说用的少,但是现在很多面试官都喜欢考如何清除浮动,所以一定要背下来,虽然可能不会用到。

2.9 BFC*

10 分钟理解 BFC 原理

[布局概念] 关于CSS-BFC深入理解

【我自己对BFC的了解也不是很深,但根据很多人分享的前端面试题来看,BFC绝对也是一个易考点,所以一定要进行掌握,就算不掌握,也要了解。

2.10 优化,提高性能

  1. 避免过度约束。
  2. 避免后代选择符。
  3. 避免链式选择符。
  4. 使用紧凑的语法。
  5. 避免不必要的命名空间。
  6. 避免不必要的重复。
  7. 最好使用表示语义的名字。一个好的类名应该是描述他是什么而不是像什么。
  8. 避免!important,可以选择其他选择器。
  9. 尽可能的精简规则,你可以合并不同类里的重复规则。

【加粗的几条是重中之重,写代码有一个核心思想就是降低代码的耦合度,增加代码的可复用性。

如果一个差不多的方法或组件你需要写2次以上,那么你就必须考虑如何将它们整合成一个组件或方法,这样在后期维护、调试的时候会大大降低难度,试想一下如果你差不多的方法或者组件写了多个,那么一旦需要修改,你就必须将所有地方都进行修改,不仅繁琐,最重要的是容易漏掉某些地方。】

2.11 响应式布局

<meta name="viewport" content="width=device-width, initial-scale=1.0">

【了解即可,因为现代前端框架在创建项目的时候自动就将这条语句加上了。如果你去掉这条语句,那么界面就会失去响应式。

注:这条语句在每个项目的HTML文件中。】

2.12 font-smoothing

让字体更加平滑。

该特性是非标准的,请尽量不要在生产环境中使用它!也就是说忘记它的存在…

2.13 手写动画需要注意什么

多数显示器默认频率是60Hz,即1秒刷新60次,所以理论上最小间隔为1/60*1000ms = 16.7ms。

2.14 图片格式

  1. png是便携式网络图片(Portable Network Graphics)是一种无损数据压缩位图文件格式。优点是:压缩比高,色彩好。 大多数地方都可以用。
  2. jpg是一种针对相片使用的一种失真压缩方法,是一种破坏性的压缩,在色调及颜色平滑变化做的不错。在www上,被用来储存和传输照片的格式。
  3. gif是一种位图文件格式,以8位色重现真色彩的图像。可以实现动画效果。
  4. webp格式是谷歌在2010年推出的图片格式,压缩率只有jpg的2/3,大小比png小了45%。缺点是压缩的时间更久了,兼容性不好,目前谷歌和opera支持。

【了解一下即可,一般这些都是美术人员考虑的事情,美术丢过来是什么图片,我们就用什么图片。】

2.15 style标签

页面加载自上而下,当然是先加载样式。

写在body标签后由于浏览器以逐行方式对HTML文档进行解析,当解析到写在尾部的样式表(外联或写在style标签)会导致浏览器停止之前的渲染,等待加载且解析样式表完成之后重新渲染,在windows的IE下可能会出现FOUC现象(即样式失效导致的页面闪烁问题)

2.16 overflow*

  • 参数是scroll时候,必会出现滚动条。
  • 参数是auto时候,子元素内容大于父元素时出现滚动条。
  • 参数是visible时候,溢出的内容出现在父元素之外。
  • 参数是hidden时候,溢出隐藏。

【重点内容,需要记忆!在项目中使用的场景也非常多。】

2.17 CSS Sprites

优点:将一个页面涉及到的所有图片都包含到一张大图中去,然后利用CSS的 background-image,background- repeat,background-position 的组合进行背景定位。利用CSS Sprites能很好地减少网页的http请求,从而大大的提高页面的性能;CSS Sprites能减少图片的字节。

缺点:维护难度较高,想要添加一个张图片需要对整个图片进行修改。

【一般来说,前端项目很少会用到Sprites,除非是有大量的ui比较小,但是又比较多的情况下,才需要使用。】

3. JavaScript

3.1 数据类型

  • 7 种原始类型
    • Boolean
    • Null
    • Undefined
    • Number
    • BigInt
    • String
    • Symbol
  • Object

【注意!数组属于Object,所以对数组使用typeof时,最终显示的类型为Object。】

3.2 Symbol

Symbol 生成一个全局唯一的值,目前我很少用到,不过我在平时阅读文章时,发现它拥有很多高级用法。

3.3 map和set

3.3.1 map*

Map是一组键值对的结构,具有极快的查找速度。

一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉。

  • set(key, val): 向Map中添加新元素
  • get(key): 通过键值查找特定的数值并返回
  • has(key): 判断Map对象中是否有Key所对应的值,有返回true,否则返回false
  • delete(key): 通过键值从Map中移除对应的数据
  • clear(): 将这个Map中的所有元素删除
  • size:返回Map对象中所包含的键值对个数
  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员

非常常用,它和普通的JavaScript对象不同的一点就是,它的key可以使用任意值,而JavaScript对象的key只能使用字符串类型。

而且因为它拥有极快的查询速度,所以我们可以用它在某些场景下简化if…else…或者switch…这些条件判断,这属于一种高级用法,现在在编程中会大量使用。

3.3.2 set

Set对象允许你存储任何类型的值,无论是原始值或者是对象引用。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set 本身是一个构造函数,用来生成Set 数据结构。Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

  • +0 与 -0 在存储判断唯一性的时候是恒等的,所以不重复
  • undefined 与 undefined 是恒等的,所以不重复
  • NaN 与 NaN 是不恒等的,但是在 Set 中认为NaN与NaN相等,所有只能存在一个,不重复。
  • add(value):添加某个值,返回 Set 结构本身(可以链式调用)。
  • delete(value):删除某个值,删除成功返回true,否则返回false
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。
  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回键值对的遍历器。
  • forEach():使用回调函数遍历每个成员。

【在数组去重时就可以考虑使用set方法。】

3.4 原型对象*

与大部分面向对象语言不同,JavaScript中并没有引入类(class)的概念,但JavaScript仍然大量地使用了对象,为了保证对象之间的联系,JavaScript引入了原型与原型链的概念。

原型链

function Person() {

}

var person = new Person(); // 实例化对象
console.log(person.__proto__ === Person.prototype); // true

3.4.1 __proto__

实例的原型对象

对象.__proto__ === 函数.prototype

3.4.2 原型prototype

  1. 每一个构造函数都拥有一个prototype属性,这个属性指向一个对象,也就是原型对象。当使用这个构造函数创建实例的时候,prototype属性指向的原型对象就成为实例的原型对象。

    function Parsen() {}
    Parsen.prototype.run = function() {
      console.log("在运动");
    };
    let p = new Parsen();
    console.log(Parsen.prototype === p.__proto__); 	//true
  2. 原型对象默认拥有一个constructor属性,指向指向它的那个构造函数(也就是说构造函数和原型对象是互相指向的关系)。

    function Parsen() {}
    Parsen.prototype.run = function() {
      console.log("在运动");
    };
    let p = new Parsen();
    console.log(Parsen.prototype.constructor === Parsen); //true
  3. 每个对象都拥有一个隐藏的属性[[prototype]],指向它的原型对象,这个属性可以通过 Object.getPrototypeOf(obj)obj.__proto__ 来访问。

    function Parsen() {}
    Parsen.prototype.run = function() {
      console.log("在运动");
    };
    let p = new Parsen();
    console.log(Object.getPrototypeOf(p) === p.__proto__); //true
    console.log(Object.getPrototypeOf(p) === Parsen.prototype); //true
  4. 实际上,构造函数的prototype属性与它创建的实例对象的[[prototype]]属性指向的是同一个对象,即 对象.__proto__ === 函数.prototype

  5. 如上文所述,原型对象就是用来存放实例中共有的那部分属性

  6. 在JavaScript中,所有的对象都是由它的原型对象继承而来,反之,所有的对象都可以作为原型对象存在。

  7. 访问对象的属性时,JavaScript会首先在对象自身的属性内查找,若没有找到,则会跳转到该对象的原型对象中查找。

3.5 原型链

原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链(prototype chain)

原型链的终点都是Object函数的prototype属性,因为在JavaScript中的对象都默认由Object()构造。Objec.prototype指向的原型对象同样拥有原型,不过它的原型是null,而null则没有原型。

通过原型链实现继承

function Dog(name, color) {
    this.name = name
    this.color = color
}

Dog.prototype.bark = () => {
    console.log('wangwang~')
}

function Husky(name, color, weight) {
    Dog.call(this, name, color)
    this.weight = weight
}

原型对象指向Object.prototype,而Object.prototype.__proto__ 没有原型

function Parsen() {}
Parsen.prototype.run = function() {
  console.log("在运动");
};

console.log(Parsen.prototype.__proto__ === Object.prototype); //true

【在TypeScript中,有一项功能叫做装饰器,虽然JavaScript中也有,但是仅在TypeScript中大量使用。

其中Angular,Nestjs已经开始大量使用装饰器,使用装饰器进行编程的这种思想叫做面向切面编程(Aspect Oriented Programming)简称AOP

如果你想要使用装饰器来简化你的代码,你就必须牢牢掌握住原型和原型链之间的关系。】

3.6 promise*

面试几乎必考题之一,项目中也会大量使用,自从ES7的async await横空出世后,promise的各种状态之间的处理已经没有那么复杂了,但是有些面试官会往复杂的问题上面问,甚至我以前还遇到过技术不扎实的面试官不太会用async await,觉得async await影响性能而不去用。那时我太菜,居然没有反驳…】

  1. 了解 Promise 吗?

  2. Promise 解决的痛点是什么?

    Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。

  3. Promise 解决的痛点还有其他方法可以解决吗?如果有,请列举。

  4. Promise 如何使用?

  5. Promise 常用的方法有哪些?它们的作用是什么?

  6. Promise 在事件循环中的执行过程是怎样的?

  7. Promise 的业界实现都有哪些?

    Qbluebirdbluebird 甚至号称运行最快的类库。

  8. 能不能手写一个 Promise 的 polyfill。

3.6.1 then

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

3.6.2 catch

  • 指定发生错误时的回调函数。

  • Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。

  • 一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

  • 没有报错时,会跳过catch()

    const someAsyncThing = function() {
      return new Promise(function(resolve, reject) {
        var x = 2;
        resolve(x + 2);
      });
    };
    
    someAsyncThing()
      //没有报错时会跳过
      .catch(function(error) {
        console.log("oh no", error);
      })
      .then(function() {
        console.log("carry on");
      });
    

3.6.3 finally

ES9的标准,用于指定不管 Promise 对象最后状态如何,都会执行的操作。

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

finally本质上是then方法的特例。

promise.finally(() => {
  // 语句
});

// 等同于
promise.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

实现方法

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

3.6.4 Promise.all()

  • 将多个 Promise 实例,包装成一个新的 Promise 实例。
  • 只有每个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。
  • 如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。

3.6.5 Promise.race()

  • 将多个 Promise 实例,包装成一个新的 Promise 实例。
  • 率先改变的 Promise 实例的返回值就传给P的回调函数。

3.7 async,await

  • async就是将函数返回值使用Promise.resolve()包裹了下,和then中处理返回值一样,并且await只能配套async使用。
  • await就是generator加上Promise的语法糖,且内部实现了自动执行generator
  • await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

【几乎不会出现多个异步操作没有依赖性写在一个方法中的情况,如果真的那么写了…那还是检查一下方法的设计是否有问题,因为编程还有一个原则是:一个方法尽量只做一件事。】

3.8 Generator

  • ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
  • 一个状态机,封装了多个内部状态。

next方法返回一个对象,它的value属性就是当前yield表达式的值hellodone属性的值false,表示遍历还没有结束。

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

一般采用第三种写法function* foo(x, y) { ··· }

【使用vue框架的可能有一部分人根本不知道这个玩意,而使用react框架的肯定会了解这玩意,因为react-saga就需要使用Generator 。因为和平时的编程习惯不同,所以在react-saga之外我几乎没有使用过Generator 】。

3.8.1 next()

  • 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
  • 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
  • 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
  • 如果该函数没有return语句,则返回的对象的value属性值为undefined
  • 只有调用next方法时,函数f才会执行。
  • yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
  • 由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数

3.8.2 for…of循环

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

3.8.3 Generator.prototype.return()

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

3.8.4 yield*

在 Generator 函数内部,调用另一个 Generator 函数。

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"
let obj = {
  * myGeneratorMethod() {
    ···
  }
};
//等价于
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

3.8.5 异步编程

Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

Generator+Promise实现完美异步

function getCallSettings() {
  // utils.ajax方法支持返回promise对象,把得到的promise return出去
  return utils.ajax({
    url: "/dialer/dialerSetting",
    method: "GET"
  });
}
function* dealData() {
  try {
    let settingInfo = yield getCallSettings();
    // do something……
  } catch (err) {
    console.log(err); // 接收错误
  }
}

let it = dealData();
let promise = it.next().value; // 注意,这里拿到yield出来的promise
promise.then(
  info => {
    it.next(info); // 拿到info传给yield表达式
  },
  err => {
    it.throw(err); // 抛出错误
  }
);

3.8.6 特征

  • function关键字与函数名之间有一个星号
  • 函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

3.9 继承方式

【在ES6后,JavaScript已经拥有了class和extends关键字,所以几乎不用再考虑如何继承,不过了解其中的原理,对JavaScript的使用会更上一层楼,虽然说很久不用就会忘了。现在也几乎不会再去手动书写继承。

其实现在学习前端也生在一个好时代,在ES6之前,JavaScript拥有很多坑,比如困扰了很多人的this指向问题,回调地狱问题,面向对象编程不方便的问题等等,这些在ES6中,这些都得到了很好的解决。】

3.9.1 原型链

原型链

既可以继承构造函数中的属性和方法,也可以继承原型链上的属性和方法,

  • 实例化子类时无法给父类传参。
  • 多个实例对引用类型的操作会被篡改。
function Person() {
  this.name = "张三";
  this.old = 18;
}
function Student() {}

Student.prototype = new Person();
var s = new Student();
console.log(s.name);

3.9.2 借用构造函数

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能
function Person() {
  this.name = "张三";
  this.old = 18;
}
Person.prototype.run = function() {
  console.log(this.name + "在运动");
};
function Student() {
  Person.call(this);
}

var s = new Student();
console.log(s.name);

3.9.3 组合继承

缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

3.9.4 原型式继承

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数
function object(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);
//"Shelby,Court,Van,Rob,Barbie"
console.log(person.name);

3.9.5 寄生式继承

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数
function object(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}
function createAnother(original) {
  var clone = object(original); // 通过调用 object() 函数创建一个新对象
  clone.sayHi = function() {
    // 以某种方式来增强对象
    alert("hi");
  };
  return clone; // 返回这个对象
}
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

3.9.6 寄生组合式继承

这是最成熟的方法,也是现在库实现的方法

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}

// 父类初始化实例属性和原型属性
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
  alert(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function() {
  alert(this.age);
};

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance2.colors.push("3"); // ["red", "blue", "green", "3"]

3.10 浅拷贝深拷贝

  • 浅拷贝:一般指的是把对象的第一层拷贝到一个新对象上去,比如

    var a = { count: 1, deep: { count: 2 } };
    // var b = Object.assign({}, a);
    // 或者
    var b = { ...a };
    var c = { ...a };
    b.deep.count = 3;
    b.count = 2;
    console.log(c.deep.count); //3
    console.log(b.count); //2
    console.log(c.count); //1
  • 深拷贝:一般需要借助递归实现,如果对象的值还是个对象,要进一步的深入拷贝,完全替换掉每一个复杂类型的引用。

    var deepCopy = (obj) => {
        var ret = {}
        for (var key in obj) {
            var value = obj[key]
            ret[key] = typeof value === 'object' ? deepCopy(value) : value
        }
        return ret
    }

JSON.sringifyJSON.parse *

  • 不能复制function、正则、Symbol
  • 循环引用报错
  • 相同的引用会被重复复制

【手动写一个深拷贝函数也是一道笔试和面试中经常容易遇到的问题,因为深拷贝涉及到很多知识。一般来说在工程中使用深拷贝方法都不自己写手,直接使用lodash提供的clonedeep方法,因为lodash库是经历了无数个使用者的验证,不容易出BUG,而你自己手写可能会遇到一些各种各样的BUG。

但是面试的时候就喜欢考这些。

3.11 this指向问题*

【面试的常客之一,this指向问题,先ES6之前,无数个有经验的JavaScript开发者都可能在this指向上面翻跟头。而且面试的时候也喜欢考这个。】

参考文章 老生常谈的this

3.12 new关键字

【JavaScript的开发者可能会很少用到面向对象编程,所以可能不是经常用到new关键字,不过有面试官会问到这个问题,最好了解一下。】

  • 创建一个临时对象
  • 给临时对象绑定原型
  • 给临时对象对应属性赋值
  • prototype对象的方法的this指向实例对象
  • 将临时对象return
  • 本质上是语法糖
function Fun() {
  //new做的事情
  var obj = {};
  obj.__proto__ = Fun.prototype;//Base为构造函数
  obj.name = 'Damonare';
  ...//一系列赋值以及更多的事
  return obj
}

//例子
function Fun1() {
  this.name = "Damonre";
  this.age = 21;
  this.sex = "man";
  this.run = function() {
    return this.name + this.age + this.sex + "正在跑步";
  };
}
//可以改写为
function Fun2() {
  var obj = {};
  obj.name = "Damonre";
  obj.age = 21;
  obj.sex = "man";
  obj.__proto__ = Fun2.prototype;
  return obj;
}
Fun2.prototype.run = function() {
  return this.name + this.age + this.sex + "正在跑步";
};
console.log(Fun2().run());

3.13 事件

事件

element.addEventListener(event, function, useCapture)
attachEvent(event,listener) //旧版本IE,在新版本IE中已经废除
参数 描述
event 必须。字符串,指定事件名。 注意: 不要使用 “on” 前缀。 例如,使用 “click” ,而不是使用 “onclick”。 提示: 所有 HTML DOM 事件,可以查看我们完整的 HTML DOM Event 对象参考手册
function 必须。指定要事件触发时执行的函数。 当事件对象会作为第一个参数传入函数。 事件对象的类型取决于特定的事件。例如, “click” 事件属于 MouseEvent(鼠标事件) 对象。
useCapture 可选。布尔值,指定事件是否在捕获或冒泡阶段执行。 可能值:true - 事件句柄在捕获阶段执行(即在事件捕获阶段调用处理函数)false- false- 默认。事件句柄在冒泡阶段执行(即表示在事件冒泡的阶段调用事件处理函数)

对于事件代理来说,在事件捕获或者事件冒泡阶段处理并没有明显的优劣之分,但是由于事件冒泡的事件流模型被所有主流的浏览器兼容,从兼容性角度来说还是建议大家使用事件冒泡模型。

3.13.1 阻止事件冒泡

  • 给子级加 event.stopPropagation( )
  • 在事件处理函数中返回 false
    • return false 不仅阻止了事件往上冒泡,而且阻止了事件本身(默认事件)。event.stopPropagation()则只阻止事件往上冒泡,不阻止事件本身。
  • event.target==event.currentTarget,让触发事件的元素等于绑定事件的元素,也可以阻止事件冒泡;

3.13.2 事件委托

var parent = document.getElementById("parent");
var child = document.getElementById("child");
parent.onclick = function(e){
    if(e.target.id == "child"){
        console.log("您点击了child元素")
    }
}

【使用addEventListener注册的事件为全局事件,如果在Vue和React这种单页面应用的组件上面注册使用,一定要在组件销毁的时候将它清除掉!不然可能会引发一些意外的BUG出现,严重的话还可能引发内存泄露。】

3.14 定时器

【因为JavaScript单线程的特性,js中的定时器并非是一个特别准确的定时器,意思就是你设置的延迟是200ms,可能200.32132456ms才触发,理解定时器对于理解JavaScript底层原理有一定的帮助。同时如果面试中考到定时器,可能会考到有一定深度的东西。】

推荐文章:js定时器,你所要了解的那点事

4. vue

4.1 生命周期

image

  1. 实例、组件通过new Vue() 创建出来之后会初始化事件和生命周期,然后就会执行beforeCreate钩子函数,这个时候,数据还没有挂载呢,只是一个空壳,无法访问到数据和真实的dom,一般不做操作。
  2. 挂载数据,绑定事件等等,然后执行created函数,这个时候已经可以使用到数据,也可以更改数据,在这里更改数据不会触发updated函数,在这里可以在渲染前倒数第二次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取
  3. 接下来开始找实例或者组件对应的模板,编译模板为虚拟dom放入到render函数中准备渲染,然后执行beforeMount钩子函数,在这个函数中虚拟dom已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发updated,在这里可以在渲染前最后一次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取。
  4. 接下来开始render,渲染出真实dom,然后执行mounted钩子函数,此时,组件已经出现在页面中,数据、真实dom都已经处理好了,事件都已经挂载好了,可以在这里操作真实dom等事情…
  5. 当组件或实例的数据更改之后,会立即执行beforeUpdate,然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染,一般不做什么事儿。
  6. 当更新完成后,执行updated,数据已经更改完成,dom也重新render完成,可以操作更新后的虚拟dom。
  7. 当经过某种途径调用$destroy方法后,立即执行beforeDestroy,一般在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等。
  8. 组件的数据绑定、监听…去掉后只剩下dom空壳,这个时候,执行destroyed,在这里做善后工作也可以。

【在实际的项目开发中,我们一般只用到created,mounted,destroyed这3个生命周期,其中我习惯将网络请求放在created,对DOM的操作一定要放在mounted之后,而销毁监听事件和定时器都放在destroyed中进行处理。】

4.2 双向数据绑定原理

双向数据绑定

  • vue数据双向绑定是通过数据劫持结合发布者订阅者模式的方式来实现的。
  • 利用了 Object.defineProperty() 这个方法重新定义了对象获取属性值(get)和设置属性值(set)。

参考文章:手动实现Vue双向绑定

4.3 diff算法

Vue原理系列,有些大公司就喜欢考这种题,这种题目也是成为架构师必不可少的一环,如果不希望有人能够轻易替代你,就必须了解这些底层的东西,因为现在会喷框架的不计其数,但是能写框架寥寥无几。

具体可以参考字节跳动团队的:详解vue的diff算法

4.4 异步更新,nextTick

有时候方法中更新了data中的某个值,但是页面并不一定会立即刷新,但是要在同一个函数中更新data的值并且要获取页面刷新后DOM的值的话,就需要使用到nextTick

Vue.nextTick 的原理和用途

4.5 vue路由

vue动态路由是一个难点也是一个重点,它通过全局路由守卫进行实现,原理是根据判断用户的身份,动态的添加路由信息,这样就实现了权限的控制,有些界面就无法进行访问。

但是全局路由守卫编写的时候一定要注意步骤,不然很容易出现死循环而导致页面一直在跳转。

4.6 vuex

状态管理神器,现在最常用的状态管理方法之一,完美的解决了兄弟之间的值传递,我个人非常喜欢使用vuex。

优点:不用再去思考什么子传父,父传子,兄弟之间的值如何传递,可以更放心随意的抽取组件,不用再担心如果组件抽取过多,状态传递会非常繁琐的问题,实现一个组件干一件事。项目维护起来也会比较方便。

缺陷:

  1. 额外的学习成本。
  2. Vuex的状态如果你不刷新浏览器会一直存在,造成某些意外的BUG。
  3. 如果一个组件需要在多个地方使用,它们都是用的一份状态,一旦修改某个组件的状态则其它组件的状态都会被修改。就例如data状态中没有return一样,当然解决的方法就是深拷贝这个状态再在界面上使用拷贝后的状态。

5. React

2019年17道高频React面试题及详解

【由于React Hook的出现,所以记住useEffect这个重要的钩子函数就可以应对大部分场景。】

6. 浏览器

6.1 缓存

缓存

6.1.1 Service Worker

必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的

6.1.2 Memory Cache

存在于内存中, 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了

6.1.3 Disk Cache

存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上

浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?

  • 对于大文件来说,大概率是不存储在内存中的,反之优先。
  • 当前系统内存使用率高的话,文件优先存储进硬盘。

6.1.4 Push Cache

  • 它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂
  • 在Chrome浏览器中只有5分钟左右。

通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的

6.1.5 缓存过程

浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的

img

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识。
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中。

Cookie(复数形态Cookies),类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。

在现在的新项目辨别用户身份几乎都使用token,不再使用cookie。存储信息一般使用localStorage或者sessionStorage或者indexDB。

6.2.1 SameSite

7. 网络

7.1 TCP三次握手四次挥手

7.2 http和https

7.2.1 https

用途

  • 建立一个信息安全通道,来保证数据传输的安全。
  • 确认网站的真实性,防止钓鱼网站。

7.2.2 区别

  • HTTP 是明文传输,HTTPS 通过 SSL\TLS 进行了加密。
  • HTTP 的端口号是 80,HTTPS 是 443。
  • HTTPS 需要到 CA 申请证书,现在提供免费的证书还是挺多的,所以大部分网站都建议使用HTTPS。
  • HTTPS 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。

7.3 HTTP状态码

  • 1xx: 表示目前是协议处理的中间状态,还需要后续操作。

  • 2xx: 表示成功状态。

    200 OK是见得最多的成功状态码。通常在响应体中放有数据。

  • 3xx: 重定向状态,资源位置发生变动,需要重新请求。

    301 Moved Permanently即永久重定向,对应着302 Found,即临时重定向。

  • 4xx: 请求报文有误。

    403 Forbidden: 这实际上并不是请求报文出错,而是服务器禁止访问,原因有很多,比如法律禁止、信息敏感。

    404 Not Found: 资源未找到,表示没在服务器上找到相应的资源。

  • 5xx: 服务器端发生错误。

【在新版的Chrome中,因为安全问题,HTTPS的网站只能向HTTPS发送请求,没法向HTTP网站发送请求。

一般证书申请,域名绑定这些都是由运维或者后端进行操作,但是还是推荐前端了解这些东西,不然你甚至都无法部署属于自己的个人网站。】

7.4 URL

7.5 XSS攻击

7.5.1 概念

跨站脚本(英语:Cross-site scripting,通常简称为:XSS)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。

  • 一类是反射型XSS,又称非持久型XSS,
  • 一类是储存型XSS,也就是持久型XSS。

7.5.2 防范手段

  • 首先是过滤。对诸如<script><img><a>等标签进行过滤。
  • 其次是编码。像一些常见的符号,如<>在输入的时候要对其进行转换编码,这样做浏览器是不会对该标签进行解释执行的,同时也不影响显示效果。
  • 最后是限制。通过以上的案例我们不难发现xss攻击要能达成往往需要较长的字符串,因此对于一些可以预期的输入可以通过限制长度强制截断来进行防御。

7.6 跨域

前端开发中的跨域解决方案

跨域资源共享,用于让网页的受限资源能够被其他域名的页面访问的一种机制。 通过该机制,页面能够自由地使用不同源的图片、样式、脚本、iframes以及视频。一些跨域的请求常常会被同源策略所禁止的。跨源资源共享定义了一种方式,为的是浏览器和服务器之间能互相确认是否足够安全以至于能使用跨源请求。

7.7 防抖节流

前端常见面试题之一。

防抖(debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

节流(throttle):规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

应用场景:其实函数的防抖和节流应用场景大部分都是在与ajax请求,如果某些请求不进行防抖节流,客户或者网站攻击者对同一个请求在一段时间内重复点击,则会对后端造成非常大的压力,当然一个人可能还好,但是如果面向的用户群庞大,或者机器点击,这种情况下通常就需要防抖和节流,大部分情况下防抖和节流都可以起到对服务器减压的作用,但是相对的用户体验不同。

文章:7分钟理解JS的节流、防抖及使用场景


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!