前端面试题之三
1. 盒模型是什么?
盒模型规定了网页元素如何显示、元素间的相互关系。
盒子模型的组成分为以下几个部分:
content(内容区):元素的宽和高
border(边框区):盒子得分边缘
padding(填充区):为了使页面布局看起来美观大方,盒子里的内容会用padding来解决父元素和子元素的位置关系。
margin(外边界区):控制同辈元素之间的位置关系
padding和margin的作用都是用来改变元素的位置,使页面看起来舒适。
padding的特点:
(1)使用padding来控制位置时,要知道它的值会把元素原有的值撑大,若内容区设置了宽高,想要让元素的大小不变,则要在元素的宽高上减去后加的padding值。
(2)padding属性对背景图片是不起作用的。
(3)背景色会延展到padding区域。
margin的特点:
(1)margin增加元素所占区域,但不会影响元素的实际宽高。
margin可以写负值,padding不可以。
(2)上下两个元素之间的margin值会重叠显示,谁的值大显示谁。
(3)当父元素里的第一个子元素(块元素),添加margin-top时,会错误地把margin-top添加给父元素。(建立在当前元素没有添加边框和浮动的前提下)
解决方法:给父元素添加overflow:hidden;
给父元素和子元素添加浮动属性;
给父元素添加边框;
把margin改成padding。
2. 说一下box-sizing。border-box的意义是什么?
box-sizing有三个值:
content-box:宽度和高度分别应用到元素的内容框。在宽度和高度之外绘制元素的内边距和边框。
border-box:为元素设定的宽度和高度决定了元素的边框盒。
就是说,为元素指定的任何内边距和边框都将在已设定的宽度和高度内进行绘制。
通过从已设定的宽度和高度分别减去边框和内边距才能得到内容的宽度和高度。
inherit:规定应从父元素继承 box-sizing 属性的值。
boder-box
对元素设定固定尺寸之后js获取到的值是固定的。
3. 箭头函数的特点
(1)省略function换成了=>,一个参数的时候()可以省略,一个return的时候{}可以省略。
(2)箭头函数是静态的,始终指向声明函数时的作用域。
(3)箭头函数不能作为构造函数的实例化对象。
(4)箭头函数没有原型对象。
(5)箭头函数不能使用arguments变量。取而代之用rest...解决。
4. 0.1+0.2===0.3?怎么解决?
不等于。
JS 采用 IEEE 754 双精度版本(64位),六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.1 和 0.2 都是无限循环的二进制,所以在小数位末尾处需要判断是否进位(规则和十进制里的四舍五入一样)。所以 0.1的二进制表示(0.1 = 2^-4 * 1.10011(0011)
) 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010
,同理可得0.2的二进制表示 。把这两个二进制加起来得到 2^-2 * 1.0011(0011 * 11次)0100
, 这个值再换算成十进制就是 0.30000000000000004
。
解决方法:(1)
(0.1*10+0.2*10)/10 ===0.3
(3)
parseFloat((0.1+0.2).toFixed(10)) ===0.3
5. HTTP协议的各个版本的特性都是什么?
HTTP/0.9:这是第一个版本的HTTP协议,它的组成很简单,只允许客户端发送GET这一种请求,且不支持请求头。由于没有请求,所以只支持一种内容,即纯文本。
HTTP/1.0:在请求中加入了HTTP版本号,HTTP开始有header,增加了HTTP status code 相关状态码,增加了Content-type,可以传输其他文件。
缺点:每请求一个资源都要新建一个TCP连接,而且是串行请求。
HTTP/1.1:HTTP1.1是目前使用最广泛的协议版本。HTTP1.1是目前主流的HTTP协议版本。
HTTP 1.1引入了许多关键性能优化:keepalive连接,chunked编码传输,字节范围请求,Pipelining(请求流水线)等。
HTTP/1.1支持设置keepalive让HTTP重用TCP链接;支持pipeline网络传输,第一个请求发出去,不必等其回来,就可以发第二个请求出去;支持 Chunked Responses,也就是说,在 Response 的时候,不必说明 Content-Length ,这样,客户端就不能断连接,直到收到服务端的EOF标识。这种技术又叫 “服务端push模型”;增加 cache control 机制;协议头注增加 Language, Encoding, Type 等头,让客户端可以跟服务器端进行更多协商;header中增加 HOST 头,让服务器知道请求的是哪个网站。因为可以有多个域名解析到同一个IP上;加入 OPTIONS 方法,其主要用于 CORS - Cross Origin Resource Sharing 应用。
缺点:请求并行数量限制;传输数据时,是以文本的方式,借助耗CPU的zip压缩的方式减少网络带宽,但是耗了前端和后端的CPU,传输数据的成本比较大。
HTTP 2.0:多路复用(二进制分帧)。
HTTP 2.0最大的特点:不会改动HTTP 的语义,HTTP 方法、状态码、URI 及首部字段,等等这些核心概念上一如往常,却能致力于突破上一代标准的性能限制,改进传输性能,实现低延迟和高吞吐量。而之所以叫2.0,是在于新增的二进制分帧层。在二进制分帧层上, HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。
缺点:协议复杂度提升,比如内部维护一个"优先级树"来做一些资源和请求的调度和控制,降低协议的可维护性和可扩展性的权衡;若干个HTTP的请求在复用一个TCP的连接,底层的TCP协议是不知道上层有多少个HTTP请求的,所以一旦发生丢包,造成的问题就是所有的HTTP请求都必需等待这个丢了的包被重新传回来,哪怕丢的那个包不是我这个HTTP请求的 ---- Head-of-Line Blocking
HTTP/3.0:把HTTP底层的TCP协议改成UDP
6. HTTP3.0中为什么使用UDP?
谷歌选择UDP可以先从TCP协议的不足和UDP的优势看:
-
基于TCP开发的设备和协议非常多,兼容困难
-
TCP协议栈是Linux内部的重要部分,修改和升级成本很大
-
UDP本身是无连接的、没有建链和拆链成本
-
UDP的数据包无队头阻塞问题
-
UDP改造成本小
QUIC目前的优势:
(1)减少了 TCP 三次握手及 TLS 握手时间。
(2)改进的拥塞控制。
(3)避免队头阻塞的多路复用。
(4)连接迁移。
(5)前向冗余纠错。
7. es5中的继承和es6中有什么不一样?
es5继承:
(1)原型链继承
/* es5原型链继承 */ function person (name,age) { this.name = name this.age = age } person.prototype.sayholle = function () { console.log(this.name+' holle'+ this.age) } function child (sex) { this.sex = sex; } child.prototype = new person(); child.prototype.hh = 'ddd' let p = new child('man') console.log(p) // console.log(new person()); let p2 = new child('man') p2.__proto__.age = '36' /* 给p2原型上的age赋值,则导致p上的age也改变,父类构造函数上的属性被所有子类共享 */ console.log(p) // 36 /* 缺点,child 新增的属性只能在new person 以后,创建实列时无法向 父类的构造函数传送参数,因为直接是指定了原型,所有也不能实现多继承 父类构造函数上的属性被所有子类共享 */
(2)构造函数继承
/* es5构造函数继承 */ function person (name,age) { this.name = name this.age = age } person.prototype.sayholle = function () { console.log(this.name+' holle'+ this.age) } function child (sex,name,age) { this.sex = sex person.call(this,name,age) } let p = new child('man','czklove','13') console.log(p); /* 可以是先多继承,只要执行多个call 创建实列时能像父类构造函数船体参数 不会出现父类属性,所有子类构造函数共享 缺点, 不能继承父类原型链上的方法,如上面不能掉用sayholle方法 子类构造函数的实列,原型链上并不存在父类构造函数, 因为不能继承父类原型链上的函数,所有要继承函数只能定义在父类构造函数上, 不能达到函数复用 */
(3)组合继承
/* es5组合继承 */ function person (name,age) { this.name = name this.age = age } person.prototype.sayholle = function () { console.log(this.name+' holle'+ this.age) } function child (sex,name,age) { this.sex = sex person.call(this,name,age) } child.prototype = new person(); /* 重新设置一下constructor 不设置也没有影响,严谨的角度上来说还是设置一下*/ /* 不设置的话,__proto__ 上时没有 constructor */ /* 正常来讲constructor是指向自身的 */ child.prototype.constructor = child; let p = new child('man','czklove','13') let p1 = new child('man','czklove1','16') p.sayholle(); // czklove holle13 console.log(p); child {sex: "man", name: "czklove", age: "13"} age: "13" name: "czklove" sex: "man" __proto__: person age: undefined constructor: ƒ child(sex,name,age) name: undefined __proto__: Object /* 组合继承,既能达到对父类属性的继承,也能继承父类原型上的方法 父类属性继承也不会在所有子类的实列上共享 唯一缺点,子类原型上有父类构造函数的属性,也就是多了一份属性 */ console.log(p.__proto__ === child.prototype) //true
(4)原型式继承
function object(o){ function F(){} F.prototype = o; return new F(); }
从本质上讲,object()对传入其中的对象执行了一次浅复制
var person = { name:'Annika', friendes:['Alice','Joyce'] }; var anotherPerson = object(person); anotherPerson.name = 'Greg'; anotherPerson.friendes.push('Rob'); var yetAnotherPerson = object(person); yetAnotherPerson.name = 'Linda'; yetAnotherPerson.friendes.push('Sophia'); console.log(person.friends); //['Alice','Joyce','Rob','Sophia']
ES5新增Object.create规范了原型式继承,接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象,在传入一个参数的情况下,Object.create()和object()行为相同。
var person = { name:'Annika', friendes:['Alice','Joyce'] }; var anotherPerson = object.create(person,{ name:{ value:"Greg" } }); \\用这种方法指定的任何属性都会覆盖掉原型对象上的同名属性 console.log(anotherPerson.name); \\Greg
(5)寄生式继承
function content(obj){ function F(){} F.prototype = obj;//继承了传入的参数 return new F();//返回函数对象 } var sup = new Person(); //以上是原型式继承,给原型式继承再套个壳子传递参数 function subobject(obj){ var sub = content(obj); sub.name = "gar"; return sub; } var sup2 = subobject(sup); //这个函数经过声明之后就成了可增添属性的对象 console.log(typeof subobject);//function console.log(typeof sup2);//object console.log(sup2.name);/r"gar",返回了个sub对象,继承了sub的属性
基本思想:寄生式继承是与原型式继承紧密相关的一种思路,它创造一个仅用于封装继承过程的函数,在函数内部以某种方式增强对象,最后再返回对象。
function createAnother(original){ \\通过调用函数创建一个新对象 var clone = object(original); \\以某种方式来增强对象 clone.sayHi = fuuction(){ alert("Hi"); }; \\返回这个对象 return clone }
(6)寄生组合继承
/* es5寄生组合继承 */ function person (name,age) { this.name = name this.age = age } person.prototype.sayholle = function () { console.log(this.name+' holle'+ this.age) } function child (sex,name,age) { this.sex = sex person.call(this,name,age) } child.prototype = Object.create(person.prototype); child.prototype.constructor = child let p = new child('man','czklove','13') p.sayholle(); // czklove holle13 console.log(p); /* child {sex: "man", name: "czklove", age: "13"} age: "13" name: "czklove" sex: "man" __proto__: person constructor: ƒ child(sex,name,age) __proto__: sayholle: ƒ () constructor: ƒ person(name,age) __proto__: Object */
es6继承:
/* esl class */ class person { constructor (name,age) { this.name = name this.age = age } syaholle () { console.log(this.name+ ' holle '+this.age) } } class child extends person { constructor (name,age,sex) { /* 执行父类的构造函数 子类必须在构造函数中掉用super */ super(name,age) /* 使用this一定要在super 之后 */ this.sex = sex } } let p = new child('czklove','23','man') console.log(p) /* child {name: "czklove", age: "23", sex: "man"} age: "23" name: "czklove" sex: "man" __proto__: person constructor: class child __proto__: constructor: class person syaholle: ƒ syaholle() __proto__: Object */
总结:
es5中是不允许原生构造函数的继承的
es6对object构造函数的继承,不能传参,传参数无效
es6新增关键字class,代表类,其实相当于代替了es5的构造函数
8. es6为什么加入class
这个关键字?
ES6的class可以看作是一个语法糖,它的绝大部分功能ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法。
9. es6为什么引入Promise
对象?
有没有遇到过这种需求,一个请求完毕后才能发起另一个请求。这样的话另一个请求就要嵌套进前一个请求中了,一个两个还好,但想象下要是有四五个呢,那代码将会变得非常难看和难以维护(回调地狱)。而promise的出现正是要解决这个问题的。
10. Promise
与es5中的callback
编程有什么不同?
当我们调用多次函数的时候,并且每一个函数都嵌套起来,这样就会代码变得冗余,如下所示:
// 多次调用函数并嵌套 function calltest(call){ setTimeout(function(){ call() },1000) } //循环调用--每隔一秒调用一次韩式calltest打印出结果 calltest(function(){ console.log(111) calltest(function(){ console.log(222) calltest(function(){ console.log(333) calltest(function(){ console.log(444) }) }) }) })
这时候我们就可以使用es6的Promise对象,Promise是一种异步编程,比es5的回调函数和事件更加的便捷,Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Fulfilled(已成功)、Rejected(已失败)。以下使用代码示例表示这个过程:
// Promise对象 function callbast(){ return new Promise((resolve,reject) => { setTimeout(()=> { resolve() },1000) }) } callbast().then(() => { console.log(111) return callbast() }).then(() => { console.log(222) return callbast() }).then(() => { console.log(333) return callbast() }).then(() => { console.log(444) return callbast() })
11. 手撕Promise.all()
function all(iterable) { return new Promise((resolve, reject) => { let index = 0; for (const promise of iterable) { // Capture the current value of `index` const currentIndex = index; promise.then( (value) => { if (anErrorOccurred) return; result[currentIndex] = value; elementCount++; if (elementCount === result.length) { resolve(result); } }, (err) => { if (anErrorOccurred) return; anErrorOccurred = true; reject(err); }); index++; } if (index === 0) { resolve([]); return; } let elementCount = 0; let anErrorOccurred = false; const result = new Array(index); }); }
12. 前端的优化策略有哪些?
1. 减少HTTP请求次数
尽量合并图片、CSS、JS。比如加载一个页面,如果有5个css文件的话,那么会发出5次http请求,这样会让用户第一次访问你的页面的时候会长时间等待。而如果把这个5个文件合成一个的话,就只需要发出一次http请求,节省网络请求时间,加快页面的加载。
2. 使用CDN
网站上静态资源即css、js全都使用cdn分发,图片亦然。
3. 避免空的src和href
当link标签的href属性为空、script标签的src属性为空的时候,浏览器渲染的时候会把当前页面的URL作为它们的属性值,从而把页面的内容加载进来作为它们的值。所以要避免犯这样的疏忽。
4. 为文件头指定Expires
Exipres是用来设置文件的过期时间的,一般对css、js、图片资源有效。 他可以使内容具有缓存性,这样下回再访问同样的资源时就通过浏览器缓存区读取,不需要再发出http请求。如下例子:
新浪微博的这个css文件的Expires时间是2016-5-04 09:14:14.
5. 使用gzip压缩内容
gzip能够压缩任何一个文本类型的响应,包括html,xml,json。大大缩小请求返回的数据量。
6. 把CSS放到顶部
网页上的资源加载时从上网下顺序加载的,所以css放在页面的顶部能够优先渲染页面,让用户感觉页面加载很快。
7. 把JS放到底部
加载js时会对后续的资源造成阻塞,必须得等js加载完才去加载后续的文件 ,所以就把js放在页面底部最后加载。
8. 避免使用CSS表达式
举个css表达式的例子
font-color: expression( (new Date()).getHours()%3 ? “#FFFFFF" : “#AAAAAA" );
这个表达式会持续的在页面上计算样式,影响页面的性能。并且css表达式只被IE支持。
9. 将CSS和JS放到外部文件中
目的是缓存文件,可以参考原则4。 但有时候为了减少请求,也会直接写到页面里,需根据PV和IP的比例权衡。
10. 权衡DNS查找次数
减少主机名可以节省响应时间。但同时,需要注意,减少主机会减少页面中并行下载的数量。
IE浏览器在同一时刻只能从同一域名下载两个文件。当在一个页面显示多张图片时,IE 用户的图片下载速度就会受到影响。所以新浪会搞N个二级域名来放图片。
下面是新浪微博的图片域名,我们可以看到他有多个域名,这样可以保证这些不同域名能够同时去下载图片,而不用排队。不过如果当使用的域名过多时,响应时间就会慢,因为不用响应域名时间不一致。
11. 精简CSS和JS
这里就涉及到css和js的压缩了。比如下面的新浪的一个css文件,把空格回车全部去掉,减少文件的大小。现在的压缩工具有很多,基本主流的前端构建工具都能进行css和js文件的压缩,如grunt,glup等。
12. 避免跳转
有种现象会比较坑爹,看起来没什么差别,其实多次了一次页面跳转。比如当URL本该有斜杠(/)却被忽略掉时。例如,当我们要访问 baidu.com 时,实际上返回的是一个包含301代码的跳转,它指向的是 baidu.com/(注意末尾的斜杠)。在nginx服务器可以使用rewrite;Apache服务器中可以使用Alias 或者 mod_rewrite或者the DirectorySlash来避免。
另一种是不用域名之间的跳转, 比如访问 baidu.com/bbs 跳转到bbs.baidu.com/。那么可以通过使用Alias或者mod_rewirte建立CNAME(保存一个域名和另外一个域名之间关系的DNS记录)来替代。
13. 删除重复的JS和CSS
重复调用脚本,除了增加额外的HTTP请求外,多次运算也会浪费时间。在IE和Firefox中不管脚本是否可缓存,它们都存在重复运算JavaScript的问题。
14. 配置ETags
它用来判断浏览器缓存里的元素是否和原来服务器上的一致。比last-modified date更具有弹性,例如某个文件在1秒内修改了10次,Etag可以综合Inode(文件的索引节点(inode)数),MTime(修改时间)和Size来精准的进行判断,避开UNIX记录MTime只能精确到秒的问题。 服务器集群使用,可取后两个参数。使用ETags减少Web应用带宽和负载
15. 可缓存的AJAX
异步请求同样的造成用户等待,所以使用ajax请求时,要主动告诉浏览器如果该请求有缓存就去请求缓存内容。如下代码片段, cache:true就是显式的要求如果当前请求有缓存的话,直接使用缓存
$.ajax({ url : 'url', dataType : "json", cache: true, success : function(son, status){ }
16. 使用GET来完成AJAX请求
当使用XMLHttpRequest时,浏览器中的POST方法是一个“两步走”的过程:首先发送文件头,然后才发送数据。因此使用GET获取数据时更加有意义。
17. 减少DOM元素数量
这是一门大学问,这里可以引申出一堆优化的细节。想要具体研究的可以看后面推荐书籍。总之大原则减少DOM数量,就会减少浏览器的解析负担。
18. 避免404
比如外链的css、js文件出现问题返回404时,会破坏浏览器的并行加载。
19. 减少Cookie的大小
Cookie里面别塞那么多东西,因为每个请求都得带着他跑。
20. 使用无cookie的域
比如CSS、js、图片等,客户端请求静态文件的时候,减少了 Cookie 的反复传输对主域名的影响。
21. 不要使用滤镜
IE独有属性AlphaImageLoader用于修正7.0以下版本中显示PNG图片的半透明效果。这个滤镜的问题在于浏览器加载图片时它会终止内容的呈现并且冻结浏览器。在每一个元素(不仅仅是图片)它都会运算一次,增加了内存开支,因此它的问题是多方面的。
完全避免使用AlphaImageLoader的最好方法就是使用PNG8格式来代替,这种格式能在IE中很好地工作。如果你确实需要使用AlphaImageLoader,请使用下划线_filter又使之对IE7以上版本的用户无效。
22. 不要在HTML中缩放图片
比如你需要的图片尺寸是50* 50
那就不用用一张500*500的大尺寸图片,影响加载