前端经典面试题(有答案)
对 this 对象的理解
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
说一下 HTTP 3.0
HTTP/3 基于 UDP 协议实现了类似于 TCP 的多路复用数据流、传输可靠性等功能,这套功能被称为 QUIC 协议。
流量控制、传输可靠性功能:QUIC 在 UDP 的基础上增加了一层来保证数据传输可靠性,它提供了数据包重传、拥塞控制、以及其他一些 TCP 中的特性。
集成 TLS 加密功能:目前 QUIC 使用 TLS1.3,减少了握手所花费的 RTT 数。
多路复用:同一物理连接上可以有多个独立的逻辑数据流,实现了数据流的单独传输,解决了 TCP 的队头阻塞问题。
快速握手:由于基于 UDP,可以实现使用 0 ~ 1 个 RTT 来建立连接。
原型/原型链
__proto__
和 prototype 关系 :__proto__
和constructor
是对象独有的。2️⃣prototype
属性是函数独有的
在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说我们是不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来让我们访问这个属性,但是我们最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个
Object.getPrototypeOf()
方法,我们可以通过这个方法来获取对象的原型。
当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype
所以这就是我们新建的对象为什么能够使用 toString()
等方法的原因。
特点:JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与 之相关的对象也会继承这一改变
原型(
prototype
): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在Firefox
和Chrome
中,每个JavaScript
对象中都包含一个__proto__
(非标准)的属性指向它爹(该对象的原型),可obj.__proto__
进行访问。构造函数: 可以通过
new
来 新建一个对象 的函数。实例: 通过构造函数和
new
创建出来的对象,便是实例。 实例通过__proto__
指向原型,通过constructor
指向构造函数。
以
Object
为例,我们常用的Object
便是一个构造函数,因此我们可以通过它构建实例。
则此时, 实例为
instance
, 构造函数为Object
,我们知道,构造函数拥有一个prototype
的属性指向原型,因此原型为:
这里我们可以来看出三者的关系:
实例.__proto__ === 原型
原型.constructor === 构造函数
构造函数.prototype === 原型
原型链
原型链是由原型对象组成,每个对象都有
__proto__
属性,指向了创建该对象的构造函数的原型,__proto__
将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链
属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象
Object.prototype
,如还是没找到,则输出undefined
;属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用:
b.prototype.x = 2
;但是这样会造成所有继承于该对象的实例的属性发生改变。
js 获取原型的方法
p.proto
p.constructor.prototype
Object.getPrototypeOf(p)
总结
每个函数都有
prototype
属性,除了Function.prototype.bind()
,该属性指向原型。每个对象都有
__proto__
属性,指向了创建该对象的构造函数的原型。其实这个属性指向了[[prototype]]
,但是[[prototype]]
是内部属性,我们并不能访问到,所以使用_proto_
来访问。对象可以通过
__proto__
来寻找不属于该对象的属性,__proto__
将对象连接起来组成了原型链。
为什么 0.1+0.2 ! == 0.3,如何让其相等
在开发过程中遇到类似这样的问题:
这里得到的不是想要的结果,要想等于 0.3,就要把它进行转化:
toFixed(num)
方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?
计算机是通过二进制的方式存储数据的,所以计算机计算 0.1+0.2 的时候,实际上是计算的两个数的二进制的和。0.1 的二进制是0.0001100110011001100...
(1100 循环),0.2 的二进制是:0.00110011001100...
(1100 循环),这两个数的二进制都是无限循环的数。那 JavaScript 是如何处理无限循环的二进制小数呢?
一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留 52 位,再加上前面的 1,其实就是保留 53 位有效数字,剩余的需要舍去,遵从“0 舍 1 入”的原则。
根据这个原则,0.1 和 0.2 的二进制数相加,再转化为十进制数就是:0.30000000000000004
。
下面看一下双精度数是如何保存的:
第一部分(蓝色):用来存储符号位(sign),用来区分正负数,0 表示正数,占用 1 位
第二部分(绿色):用来存储指数(exponent),占用 11 位
第三部分(红色):用来存储小数(fraction),占用 52 位
对于 0.1,它的二进制为:
转为科学计数法(科学计数法的结果就是浮点数):
可以看出 0.1 的符号位为 0,指数位为-4,小数位为:
那么问题又来了,指数位是负数,该如何保存呢?
IEEE 标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于 JavaScript 的数字是双精度数,这里就以双精度数为例,它的指数部分为 11 位,能表示的范围就是 0~2047,IEEE 固定双精度数的偏移量为 1023。
当指数位不全是 0 也不全是 1 时(规格化的数值),IEEE 规定,阶码计算公式为 e-Bias。 此时 e 最小值是 1,则 1-1023= -1022,e 最大值是 2046,则 2046-1023=1023,可以看到,这种情况下取值范围是
-1022~1013
。当指数位全部是 0 的时候(非规格化的数值),IEEE 规定,阶码的计算公式为 1-Bias,即 1-1023= -1022。
当指数位全部是 1 的时候(特殊值),IEEE 规定这个浮点数可用来表示 3 个特殊值,分别是正无穷,负无穷,NaN。 具体的,小数位不为 0 的时候表示 NaN;小数位为 0 时,当符号位 s=0 时表示正无穷,s=1 时候表示负无穷。
对于上面的 0.1 的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011
.
所以,0.1 表示为:
说了这么多,是时候该最开始的问题了,如何实现 0.1+0.2=0.3 呢?
对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对 JavaScript 来说,这个值通常为 2-52,在 ES6 中,提供了Number.EPSILON
属性,而它的值就是 2-52,只要判断0.1+0.2-0.3
是否小于Number.EPSILON
,如果小于,就可以判断为 0.1+0.2 ===0.3
escape、encodeURI、encodeURIComponent 的区别
encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义。
encodeURIComponent 是对 URI 的组成部分进行转义,所以一些特殊字符也会得到转义。
escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。
什么是物理像素,逻辑像素和像素密度,为什么在移动端开发时需要用到 @3x, @2x 这种图片?
以 iPhone XS 为例,当写 CSS 代码时,针对于单位 px,其宽度为 414px & 896px,也就是说当赋予一个 DIV 元素宽度为 414px,这个 DIV 就会填满手机的宽度;
而如果有一把尺子来实际测量这部手机的物理像素,实际为 1242*2688 物理像素;经过计算可知,1242/414=3,也就是说,在单边上,一个逻辑像素=3 个物理像素,就说这个屏幕的像素密度为 3,也就是常说的 3 倍屏。
对于图片来说,为了保证其不失真,1 个图片像素至少要对应一个物理像素,假如原始图片是 500300 像素,那么在 3 倍屏上就要放一个 1500900 像素的图片才能保证 1 个物理像素至少对应一个图片像素,才能不失真。 当然,也可以针对所有屏幕,都只提供最高清图片。虽然低密度屏幕用不到那么多图片像素,而且会因为下载多余的像素造成带宽浪费和下载延迟,但从结果上说能保证图片在所有屏幕上都不会失真。
还可以使用 CSS 媒体查询来判断不同的像素密度,从而选择不同的图片:
参考 前端进阶面试题详细解答
对 line-height 的理解及其赋值方式
(1)line-height 的概念:
line-height 指一行文本的高度,包含了字间距,实际上是下一行基线到上一行基线距离;
如果一个标签没有定义 height 属性,那么其最终表现的高度由 line-height 决定;
一个容器没有设置高度,那么撑开容器高度的是 line-height,而不是容器内的文本内容;
把 line-height 值设置为 height 一样大小的值可以实现单行文字的垂直居中;
line-height 和 height 都能撑开一个高度;
(2)line-height 的赋值方式:
带单位:px 是固定值,而 em 会参考父元素 font-size 值计算自身的行高
纯数字:会把比例传递给后代。例如,父级行高为 1.5,子元素字体为 18px,则子元素行高为 1.5 * 18 = 27px
百分比:将计算后的值传递给后代
display:inline-block 什么时候会显示间隙?
有空格时会有间隙,可以删除空格解决;
margin
正值时,可以让margin
使用负值解决;使用
font-size
时,可通过设置font-size:0
、letter-spacing
、word-spacing
解决;
for...in 和 for...of 的区别
for…of 是 ES6 新增的遍历方式,允许遍历一个含有 iterator 接口的数据结构(数组、对象等)并且返回各项的值,和 ES3 中的 for…in 的区别如下
for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;
总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
call() 和 apply() 的区别?
它们的作用一模一样,区别仅在于传入参数的形式的不同。
apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。
Promise 的基本用法
(1)创建 Promise 对象
Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。
一般情况下都会使用new Promise()
来创建 promise 对象,但是也可以使用promise.resolve
和promise.reject
这两个方法:
Promise.resolve
Promise.resolve(value)
的返回值也是一个 promise 对象,可以对返回值进行.then 调用,代码如下:
resolve(11)
代码中,会让 promise 对象进入确定(resolve
状态),并将参数11
传递给后面的then
所指定的onFulfilled
函数;
创建 promise 对象可以使用new Promise
的形式创建对象,也可以使用Promise.resolve(value)
的形式创建 promise 对象;
Promise.reject
Promise.reject
也是new Promise
的快捷形式,也创建一个 promise 对象。代码如下:
就是下面的代码 new Promise 的简单形式:
下面是使用 resolve 方法和 reject 方法:
上面的代码的含义是给testPromise
方法传递一个参数,返回一个 promise 对象,如果为true
的话,那么调用 promise 对象中的resolve()
方法,并且把其中的参数传递给后面的then
第一个函数内,因此打印出 “hello world
”, 如果为false
的话,会调用 promise 对象中的reject()
方法,则会进入then
的第二个函数内,会打印No thanks
;
(2)Promise 方法
Promise 有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。
then()
当 Promise 执行的内容符合成功条件时,调用resolve
函数,失败就调用reject
函数。Promise 创建完了,那该如何调用呢?
then
方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为resolved
时调用,第二个回调函数是 Promise 对象的状态变为rejected
时调用。其中第二个参数可以省略。 then
方法返回的是一个新的 Promise 实例(不是原来那个 Promise 实例)。因此可以采用链式写法,即then
方法后面再调用另一个 then 方法。
当要写有顺序的异步事件时,需要串行时,可以这样写:
那当要写的事件没有顺序或者关系时,还如何写呢?可以使用all
方法来解决。
2. catch()
Promise 对象除了有 then 方法,还有一个 catch 方法,该方法相当于then
方法的第二个参数,指向reject
的回调函数。不过catch
方法还有一个作用,就是在执行resolve
回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch
方法中。
3. all()
all
方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise
对象。当数组中所有的promise
的状态都达到resolved
的时候,all
方法的状态就会变成resolved
,如果有一个状态变成了rejected
,那么all
方法的状态就会变成rejected
。
调用all
方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个 promise 对象resolve
执行时的值。
(4)race()
race
方法和all
一样,接受的参数是一个每项都是promise
的数组,但是与all
不同的是,当最先执行完的事件执行完之后,就直接返回该promise
对象的值。如果第一个promise
对象状态变成resolved
,那自身的状态变成了resolved
;反之第一个promise
变成rejected
,那自身状态就会变成rejected
。
那么race
方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:
5. finally()
finally
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
上面代码中,不管promise
最后的状态,在执行完then
或catch
指定的回调函数以后,都会执行finally
方法指定的回调函数。
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally
方法关掉服务器。
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally
本质上是then
方法的特例:
上面代码中,如果不使用finally
方法,同样的语句需要为成功和失败两种情况各写一次。有了finally
方法,则只需要写一次。
margin 和 padding 的使用场景
需要在 border 外侧添加空白,且空白处不需要背景(色)时,使用 margin;
需要在 border 内测添加空白,且空白处需要背景(色)时,使用 padding。
伪元素和伪类的区别和作用?
伪元素:在内容元素的前后插入额外的元素或样式,但是这些元素实际上并不在文档中生成。它们只在外部显示可见,但不会在文档的源代码中找到它们,因此,称为“伪”元素。例如:
伪类:将特殊的效果添加到特定选择器上。它是已有元素上添加类别的,不会产生新的元素。例如:
总结: 伪类是通过在元素选择器上加⼊伪类改变元素状态,⽽伪元素通过对元素的操作进⾏对元素的改变。
new 操作符的实现原理
new 操作符的执行过程:
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
具体实现:
隐藏元素的方法有哪些
display: none:渲染树不会包含该渲染对象,因此该元素不会在页面中占据位置,也不会响应绑定的监听事件。
visibility: hidden:元素在页面中仍占据空间,但是不会响应绑定的监听事件。
opacity: 0:将元素的透明度设置为 0,以此来实现元素的隐藏。元素在页面中仍然占据空间,并且能够响应元素绑定的监听事件。
position: absolute:通过使用绝对定位将元素移除可视区域内,以此来实现元素的隐藏。
z-index: 负值:来使其他元素遮盖住该元素,以此来实现隐藏。
clip/clip-path :使用元素裁剪的方法来实现元素的隐藏,这种方法下,元素仍在页面中占据位置,但是不会响应绑定的监听事件。
**transform: scale(0,0)**:将元素缩放为 0,来实现元素的隐藏。这种方法下,元素仍在页面中占据位置,但是不会响应绑定的监听事件。
transition 和 animation 的区别
transition 是过度属性,强调过度,它的实现需要触发一个事件(比如鼠标移动上去,焦点,点击等)才执行动画。它类似于 flash 的补间动画,设置一个开始关键帧,一个结束关键帧。
animation 是动画属性,它的实现不需要触发事件,设定好时间之后可以自己执行,且可以循环一个动画。它也类似于 flash 的补间动画,但是它可以设置多个关键帧(用 @keyframe 定义)完成动画。
iframe 有那些优点和缺点?
iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。
优点:
用来加载速度较慢的内容(如广告)
可以使脚本可以并行下载
可以实现跨子域通信
缺点:
iframe 会阻塞主页面的 onload 事件
无法被一些搜索引擎索识别
会产生很多页面,不容易管理
CSS 预处理器/后处理器是什么?为什么要使用它们?
预处理器, 如:less
,sass
,stylus
,用来预编译sass
或者less
,增加了css
代码的复用性。层级,mixin
, 变量,循环, 函数等对编写以及开发 UI 组件都极为方便。
后处理器, 如: postCss
,通常是在完成的样式表中根据css
规范处理css
,让其更加有效。目前最常做的是给css
属性添加浏览器私有前缀,实现跨浏览器兼容性的问题。
css
预处理器为css
增加一些编程特性,无需考虑浏览器的兼容问题,可以在CSS
中使用变量,简单的逻辑程序,函数等在编程语言中的一些基本的性能,可以让css
更加的简洁,增加适应性以及可读性,可维护性等。
其它css
预处理器语言:Sass(Scss)
, Less
, Stylus
, Turbine
, Swithch css
, CSS Cacheer
, DT Css
。
使用原因:
结构清晰, 便于扩展
可以很方便的屏蔽浏览器私有语法的差异
可以轻松实现多重继承
完美的兼容了
CSS
代码,可以应用到老项目中
实现一个宽高自适应的正方形
利用 vw 来实现:
利用元素的 margin/padding 百分比是相对父元素 width 的性质来实现:
利用子元素的 margin-top 的值来实现:
为什么需要浏览器缓存?
对于浏览器的缓存,主要针对的是前端的静态资源,最好的效果就是,在发起请求之后,拉取相应的静态资源,并保存在本地。如果服务器的静态资源没有更新,那么在下次请求的时候,就直接从本地读取即可,如果服务器的静态资源已经更新,那么我们再次请求的时候,就到服务器拉取新的资源,并保存在本地。这样就大大的减少了请求的次数,提高了网站的性能。这就要用到浏览器的缓存策略了。
所谓的浏览器缓存指的是浏览器将用户请求过的静态资源,存储到电脑本地磁盘中,当浏览器再次访问时,就可以直接从本地加载,不需要再去服务端请求了。
使用浏览器缓存,有以下优点:
减少了服务器的负担,提高了网站的性能
加快了客户端网页的加载速度
减少了多余网络数据传输
评论