前端基础面试题总结---网路/CSS/JS篇

好记性不如烂笔头,面试题看的再多不如自己总结,结合看过的面试题和 js 红宝书,根据自己的理解记录成章,方便后续面试前使用。因为是自学,内容有错或者不全面欢迎指正,感谢。

网络相关

浏览器地址栏输入文字或url后,都发生了什么

地址栏输入文字后会直接调用浏览器默认搜索引擎进行搜索,通过 TCP 握手建立连接,服务器响应返回资源,浏览器解析并渲染返回的资源并展示到页面。

地址栏输入 url 解析后会进行 DNS 查找服务器地址,通过 TCP 握手建立连接,服务器响应返回资源,浏览器解析并渲染返回的资源并展示到页面。

实际整个过程设计到的知识点很多,下面以输入 url 为例进行简要说明,可以参考扩展进行阅读:

  1. url 编码传递给浏览器:通过 encodeURIComponent()/encodeURI() 对地址进行编码,传递给浏览器

  2. DNS 域名解析得到 ip 地址:域名查找过程 本地host 文件 → 本地 DNS 解析器缓存 → 计算机设置的 DNS 服务器查询 → 根服务器查找

  3. TCP 握手和服务器建立连接:TCP的”三次握手“技术通过 “SYN, SYN-ACK, ACK” 消息协商完成身份认证

  4. 发起 http 请求,服务器处理后返回资源和 http 报文,浏览器缓存策略如下:

    强缓存:响应头中设置 Cache-Control/Expires 浏览器将信息缓存下来,在下一次请求会进行强缓存处理。

    协商缓存:在强缓存失效后会进行协商缓存,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程

    通过设置缓存达到优化加载速度的目的(关于强缓存和协议缓存具体的流程可以看参考一和二)

  5. 页面渲染:渲染机制

  6. TCP 挥手和服务器断开连接:TCP 的四次“挥手”

参考:url输入到返回请求的过程 从输入URL开始建立前端知识体系 渲染页面:浏览器的工作原理 mdn 《JavaScript高级程序设计(第四版) 》5.4 章节

谈谈浏览器渲染机制,重绘,重排

浏览器的渲染机制说明的是浏览器怎么解析 HTML / CSS / JavaScript 文件,并将解析后得到的内容绘制成页面呈现出来。总的来说有5步:

  1. 构建 DOM Tree:解析 HTML 文件,读取页面的标签并生成 DOM Tree,因为有预加载扫描器的存在,对于如 CSS / JavaScript / web字体的请求,不必等到主 HTML 解析器到达请求的资源时,就已经发起请求。对于 <script> 标签(特别是没有 async 或者 defer 属性)会阻塞渲染并停止HTML的解析。
  2. 构建 CSS Tree:浏览器将CSS规则转换为可以理解和使用的样式映射,遍历 CSS 中的每一个规则集,根据 CSS 选择器创建具有父、子和兄弟关系的节点树。
  3. 构建 Render Tree:将 DOM Tree 和 CSS Tree 组合成 Render Tree。Render Tree 保存所有具有内容和计算样式的可见节点——将所有相关样式匹配到 DOM Tree 中的每个可见节点,并根据 CSS 级联确定每个节点的计算样式,但不标识每个节点的尺寸或位置。
  4. 布局(layout):根据 Render Tree 中的每个结点得到的计算样式,确定节点的尺寸和位置信息。
  5. 绘制(paint):将各个节点绘制到屏幕上,包括文本、颜色、边框、阴影和替换的元素(如按钮和图像)等操作

这里还会涉及到两个概念:重绘(repaint)和重排(reflow),由于元素的大小位置的改变会触发重排(display: none),仅是如颜色,边框等改变仅触发重绘(visibility: hidden

这里还有一个问题,如果 js 文件(位置可能在 body 中后)中有改动到 DOM ,渲染过程又是怎么样的,是触发 reflow/repaint 还是等 js 处理完将影响加入到 DOM Tree 中,才进行后面4步操作,又或者有其他的方式,如果有相关资料可以在文章后留言告知,感谢

参考:渲染页面:浏览器的工作原理 mdn 重排(reflow)和重绘(repaint)

跨域的原因及解决方法

浏览器出于安全的考虑存在同源策略,对于不同域的资源禁止解析。访问不同域名下的资源称为跨域。

同源:协议,主机,端口号三者相同,如果有任何一个或以上不同,则是不同源。

跨域的解决方式:

  • jsonp:原理是 script 标签不受同源策略影响,可以嵌入跨域脚本。实现方式:函数定义在前端,通过 script 标签,将函数作为 query 的 value 值传递给后端, 函数在后端执行,参数为需要传递的数据。只适用 get 请求。

  • CORS:CORS 标准新增了一组 HTTP 首部字段,允许服务器声明哪些源有权限访问服务器资源。对于不同的请求方式,CORS 处理方式也不同

    • 简单请求:不会触发 CORS 预检请求。满足的条件可见 MDN 参考文档。使用 OriginAccess-Control-Allow-Origin 就能完成最简单的访问控制。

      例如:请求头携带 Origin: https://foo.example,服务端响应头返回 Access-Control-Allow-Origin:* ,该资源可以被 任意外域访问

    • 非简单请求:会触发 CORS 预检请求,预检请求通过后,才会发起主请求。预检响应没有检验通过,CORS 会阻止跨域访问,实际的请求永远不会被发送。

      客户端会先使用 OPTIONS 方法发起一个预检请求,预检请求的请求头中同时携带了两个首部字段 Access-Control-Request-Method Access-Control-Request-Headers 告知服务器请求方法和自定义请求首部字段。服务器返回一个没有 body 的 HTTP 响应,标记允许的请求方法和HTTP Header 字段

      预检请求示例

      <!-- 请求头 -->
      OPTIONS https://api.mywebsite.com/user/1 HTTP/1.1
      Origin: https://www.mywebsite.com
      Access-Control-Request-Method: PUT
      Access-Control-Request-Headers: Content-Type
      
      <!-- 响应头 -->
      HTTP/1.1 204 No Content
      Access-Control-Allow-Origin: https://www.mywebsite.com
      Access-Control-Request-Method: GET POST PUT
      Access-Control-Request-Headers: Content-Type
      
  • 反向代理:服务器没有同源策略的限制,可由服务器向目标服务器发送请求,得到结果后再转发给客户端。

参考:浏览器的同源策略 mdn 跨源资源共享(CORS)MDN jsonp实现 动图讲解 CORS

前端常见的网络攻击及防护

  • XSS 攻击:跨站脚本攻击,指的是恶意脚本执行在被攻击的网站上。通过恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。XSS 攻击可以分为以下三种:

    • 存储型 XSS:通过在评论,论坛发帖,用户私信等带有输入性质的元素上提交恶意代码,并保存在数据库内。
    • 反射型 XSS:诱导用户点击恶意的 URL ,通过 URL 传递参数的功能,恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
    • DOM 型 XSS:恶意脚本只发生在客户端,修改页面 DOM 结构
    • 防御策略:可从两方面做防御。防止恶意脚本注入,如输入转义方式(把 & < > " ' / 这几个字符转义掉),控制输入内容长度;阻止恶意脚本执行,如输出转义,纯前端渲染,谨慎适用 .innerHTML .outerHTML document.write(),HTTP-only Cookie
  • CSRF 攻击:跨站请求伪造,指的是冒用被攻击者的登陆凭证 cookie ,向服务器发起非法请求或者攻击。CSRF 攻击形式主要如下:

    攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。常见的 CSRF 攻击包括如下三种:

    • GET 类型的 CSRF:利用图片 URL 发送非法的 HTTP请求
    • POST类型的CSRF:利用一个自动提交的表单发送非法的 HTTP请求
    • 链接类型的CSRF:图片或者广告中嵌入恶意链接发送非法的 HTTP请求
    • 防御策略:可以从两方面做防御。阻止不明外域的访问,如同源检测;提交时要求附加本域才能获取的信息,如CSRF token 和双重Cookies认证

参考:XSS 攻击详解 CSRF 攻击详解 浅说 XSS 和 CSRF HTTP cookies MDN

CSS 相关

CSS 盒模型

CSS 盒模型由 margin/border/padding/context 4部分组成,作用在 block/inline-block 元素。盒模型下有2种,标准盒模型content-box 和 IE 盒模型(怪异盒模型)border-box,这两种盒模型可以通过 box-sizing 设置。两者的区别在于计算盒子的宽高,标准盒模型 width 和 height 为 context 的大小, IE 盒模型 width 和 height 需要加上 padding 和 border。

参考:CSS盒模型 mdn box-sizing mdn

谈谈对 BFC 的理解

BFC,全称 Block Formatting Context,块级格式化上下文,是Web页面的可视CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区 。官方解释太抽象,参考多份资料,总结如下:

  1. BFC 是一种 CSS 的布局概念,触发了 BFC 的元素会提供了一个环境,其内部的元素布局不会影响到外部的元素布局

  2. BFC 的触发条件

    • body 元素

    • 开启浮动,不包括 float:none

    • 设置定位,position:absolute/fixed

    • 设置 overflow,除了 visible(默认值)

    • display:flex/table-cell/inline-block

  3. BFC 带来了什么问题,又解决了什么问题

    带来的问题:

    • margin 垂直塌陷(垂直方向的距离由margin决定, 属于同一个 BFC 的两个相邻的标签外边距会发生重叠)

    • margin 包含塌陷,子元素设置 margin-top带着父元素一并下移

    可以解决的问题:

    • 使用Float脱离文档流,高度塌陷(计算 BFC 的高度时,浮动元素也参与计算)
    • 两栏布局(左侧元素 float,右侧元素触发 BFC , BFC可以阻止标准流元素被浮动元素覆盖)

参考:BFC是什么?10 分钟讲透BFC 原理 面试官:请说说什么是BFC?大白话讲清楚 BFC mdn

CSS 样式的优先级

  1. 标签上存在单个选择器时,各选择器的优先级顺序:!important > 内联样式 > id 选择器 > 类选择器 = 属性选择器 = 伪类选择器 > 标签选择器 = 伪元素选择器 > 通用选择器(*),选择器在文件中的书写顺序也会影响优先级,同一选择器顺序靠后的优先级越大。

  2. 当一个标签同时被多个选择符选中,需要计算选择符中类选择器、属性选择器以及伪类选择器的个数之和,方便理解可以按照权重相加(但是这种方式不是很严谨,在不同的浏览器中,这个权重不同,CSS 权重进制在 IE6 为 256,后来扩大到了 65536,现代浏览器则采用更大的数量)

参考:CSS 选择器 mdn CSS 优先级 mdn CSS 样式优先级

三栏布局怎么实现

常用的有浮动/定位,推荐的是flex,冷门的有 table 和 grid 布局

  1. 浮动的思路是有三种,使用时需要解决float脱离文档流导致的高度塌陷

    • HTML结构:左右中,左右两个元素分布设置左右浮动,中间元素,可以控制 margin-top 或者触发 BFC 不会左浮动元素重叠

    • 双飞翼布局:(中)左右,三者浮动,中间元素包裹在浮动元素内外围浮动元素 width:100% 中间元素 margin-left/right 等于左右元素的宽用来空出显示位置,左右元素设置 margin-left为负值

    • 圣杯布局:中左右,思路和双飞翼差不多,重点是通过定位去除中间元素的包裹层

  2. 定位的思路:通过定位确定左中右的位置,但是绝对定位是脱离文档流,需要设置高度

  3. flex 记住使用规则,应用起来就比较简单, IE8 以下不支持

参考:详解 CSS 七种三栏布局技巧 浅谈 margin 负值 Flex 布局教程

水平垂直居中

常用的有定位,推荐的是 flex ,冷门的有 table 和 grid

  1. 定位的思路:通过 top/left:50% 使得元素左上角顶点居中,然后使用顶点分布左移一半的宽/上移一半的高实现,实现方式有以下s三种

    • 负 margin margin-left: -50px;margin-top: -50px;
    • transform transform: translate(-50%, -50%);
    • auto margin left: 0;top: 0;right: 0;bottom: 0;margin: auto;

    还可以直接使用 calc 实现

    • calc left: calc(50% - 50px);top: calc(50% - 50px);
  2. flex justify-content: center;align-items: center;

参考:面试指导

如何用 css 实现不同的形状:如三角形,圆形,椭圆,平行四边形

三角形利用 border 实现;

.triangle {
    width: 0px;
    height: 0px;
    /* 需要哪条边的三角形就将另外三边设为 transparent */
    border-top: 100px solid #111;
    border-left: 100px solid #333;
    border-right: 100px solid #666;
    border-bottom: 100px solid transparent;
}

圆形,椭圆利用 border-radius 实现

.round{
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background-color: #000;
}
/* 1/4 圆 */
.round-1of4{
    width: 100px;
    height: 100px;
    border-top-left-radius: 0% 0%;
    border-top-right-radius: 100% 100%;
    border-bottom-right-radius:0% 0%;
    border-bottom-left-radius: 0% 0%;
    /* 上面4行等价于 border-radius:0 100% 0 0 */
    background-color: #000;
}
.ellipse{
    width: 100px;
    height: 50px;
    border-radius: 50% 50%;
    background-color: #999;
}
/* 1/4 椭圆 */
.ellipse-1of4{
    width: 100px;
    height: 50px;
    border-top-left-radius: 0% 0%;
    border-top-right-radius: 100% 100%;
    border-bottom-right-radius:0% 0%;
    border-bottom-left-radius: 0% 0%;
    /* 上面4行等价于 border-radius:0 100% 0 0 */
    background-color: #000;
}

平行四边形可以通过 transform:skewX(45deg) 实现,为了不让字体倾斜,使用伪元素进行倾斜

.parallelogram{
    width: 100px;
    height: 50px;
    position: relative;
}
.parallelogram::before{
    content: '';
    display: block;
    position: absolute;
    z-index: -1;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #666;
    transform: skewX(45deg);
}

参考:《css揭秘》 第三章

JavaScript 相关

var let const 声明变量有什么区别

  1. var 声明的变量会提升,变量可以在声明前就使用(值为 undefined)。var 声明变量的范围是函数作用域,在全局作用域声明的变量会成为 window 对象的属性;
  2. let 声明的变量不会提升,因为存在“暂时性死区”,变量未声明就使用会抛出 RefernceError 错误。let 声明的范围是块作用域,在全局作用域声明的变量不会成为 window 对象的属性;
  3. const 的行为和 let 相当,唯一的区别在于声明变量的同时必须赋值,且无法修改。因为对象是按照引用传递的,const 声明的对象修改对象内的属性是不会报错的。

注意:在函数中变量未经声明直接使用,该变量会被提升到全局作用域中

参考:《JavaScript 高级程序设计(第四版)》3.3 章节 var 声明 mdn let 声明 mdn const 声明 mdn

JavaScript 的数据类型有哪些,如何判断

JavaScript 数据类型有两大类:简单数据类型 和 Object 类型

  1. 简单数据类型 6 种:null undefined String Number Boolean Symbol
  2. Object 类型:Function Array Date RegExp Map Set …
  3. 简单数据类型判断 :typeof,需要注意的是typeof null = object typeof Function = function,其他的返回的是其数据类型
  4. 数组的判断:Array.isArray() [] instanceof Array [].constructor === Array
  5. 对象的判断:对象 instanceof 构造函数 对象.constructor === 构造函数
a instanceof A
// 表示 A 的原型是否出现在 a 的原型链上,可以理解为如下(因为通过原型链可以一直向 Object 延长,以下方式并不严谨,实际实现需要向上递归)
a.__proto__ === A.prototype

注意:Number 类型有一个特别的值 NaN,NaN !== NaN ,判断值是否为 NaN 需使用 isNaN()

参考:《JavaScript 高级程序设计(第四版)》3.4 章节 数据类型判断 typeof mdn instanceof mdn

在 JavaScript 中,0.1 + 0.2 为什么不是 0.3

JavaScript 使用IEEE 754 格式表示整数和浮点值。该标准在浮点数计算时,由于这种微小的舍入错误,导致很难测试特定的浮点值。

简单的解决办法可以使用 toFixed() 对结果进行舍入,或者先将小数转成整数来运算,之后再转回小数。

参考:IEEE754详解 0.1+0.2!==0.3 各语言0.1+0.2结果

谈谈对作用域(执行上下文)的理解

  1. 定义:作用域(执行上下文)指是代码当前的运行环境。任何变量(不管包含的是原始值还是引用值)都存在于某个作用域(执行上下文)。这个作用域(执行上下文)决定了变量的生命周期,以及它们可以访问代码的哪些部分。

  2. JavaScript作用域分为三类:全局作用域、函数作用域和块级作用域。

  3. 作用域链:代码执行流每进入一个新的作用域,都会创建一个作用域链。

    • 函数或块级作用域通过作用域链访问全局作用域内的变量
    • 全局作用域只能访问全局作用域中的变量

参考:《你不知道的JavaScript(上卷)》第一部分 《JavaScript 高级程序设计(第四版)》4.2章节 执行上下文 mdn 作用域 mdn 作用域讲解

数据类型转换(显式/隐式转换)

数据类型转换主要讨论的是其他的数据类型转换成 Number/String/Boolean 类型的过程。类型转换可以通过两种方式完成,一种是显式转换:调用专有的转换函数,一种是隐式转换:通常是和操作符相关的操作。

  1. 显示转换:

    • 转换成数字:Number() parseInt() parseFloat()

      // Number()方法转换规则
      /*
      1. 布尔值,true 转换为1,false 转换为0。
      2. 数值,直接返回。
      3. null,返回0。
      4. undefined,返回NaN。
      5.字符串,应用以下规则。
      	如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。
      	如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
      	如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
      	如果是空字符串(不包含字符),则返回0。
      	如果字符串包含除上述情况之外的其他字符,则返回NaN。
      5.对象,调用valueOf()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照转换字符串的规则转换。
      */
      Number('123')     // 123
      Number('12.3')    // 12.3
      Number('12.00')   // 12
      Number('123e-1')  // 12.3
      Number('')        // 0
      Number(null)      // 0
      Number('0x11')    // 17
      Number('0b11')    // 3
      Number('0o11')    // 9
      Number('foo')     // NaN
      Number('100a')    // NaN
      Number('-Infinity') //-Infinity
      Number([1])       // 1
      Number([1,2])     // NaN
      Number({a:1})	  // NaN
      
    • 转换成字符串:toString() 和模板字符串

      // 模板字符串写法
      `字符串${变量}`
      
    • 转换成 Boolean:Boolean(),对于非空的字符串,非0,数组(包括空数组),对象(包括空对象)结果为 true。”“,0,NaN,undefined,null 结果为 false

  2. 隐式转换

    • 一元/乘法/除法/取模/减法/加法操作符:设计到计算的操作符,会讲符号两边的变量隐式调用 Number() 方法转化为数字再进行计算

      /*
      加法操作符针对字符串和其他的略有不同
      1. 如果有任一操作数是NaN,则返回NaN;
      2. 如果是Infinity 加Infinity,则返回Infinity;
      3. 如果是-Infinity 加-Infinity,则返回-Infinity;
      4. 如果是Infinity 加-Infinity,则返回NaN;
      5. 如果是+0 加+0,则返回+0;
      6. 如果是-0 加+0,则返回+0;
      7. 如果是-0 加-0,则返回-0。
      8. 有一个操作数是字符串,则要应用如下规则:
      	如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
      	如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。
      9. 如果有任一操作数是对象、数值或布尔值,则调用它们的toString()方法以获取字符串,然后再应用前面的关于字符串的规则
      */
      
    • 布尔操作符:会讲符号两边的变量隐式调用 Boolean() 方法转化成 Boolean 类型, && || 后接的是表达式,这里是将其作为短路操作符使用

    • 等于操作符():根据符号两边的数据类型,调用不同的方式进行比较,全等(=)不会进行类型转化

      /*
      1. 如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为0,true 转换
      为1。
      2. 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否
      相等。
      3. 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法取得其原始值,再
      根据前面的规则进行比较。
      4. null 和 undefined 的规则。
      	null 和undefined 相等。
      	null 和undefined 不能转换为其他类型的值再进行比较。
      	如果有任一操作数是NaN,则相等操作符返回false,不相等操作符返回true。记住:即使两个操作数都是NaN,相等操作符也返回	false,因为按照规则,NaN 不等于NaN。
      5. 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true。否则,两者不相等。
      */
      

参考:《JavaScript 高级程序设计(第四版)》3.4和3.5 章节 valueof() mdn 数据类型转化一 数据类型转化二

数组去重

  1. 原理简单的双层 for 循环:数组中每一项和其他项逐一比较,去除相同项

  2. 数组的方法:利用数组的方法找出重复性并去除减去一轮循环,只循环一轮调用数组的方法进行重复性确认

    • indexOf:构建新数组 newArr,newArr.indexof(arr[i]) === -1,则将 arr[i] push 到 newArr

    • includes:思路和 indexof 相同,newArr.includes(arr[i]) 为假,则将 arr[i] push 到 newArr

    • sort:先排序,相邻项两两比较,去除相同项

    • reduce + includes/indexOf:思路和 indexOf/includes 相同,只是将 for 循环用 reduce 替代

      //数组去重
      function unique(ary) {
          // reduce : 第一个是函数,第二个参数会传给第一次回调的prev;
          return ary.reduce((prev,next)=>{
              // 该函数返回值是下一次执行的prev;
              return prev.includes(next)?prev:[...prev,next];
          },[])
      }
      //数组对象去重
      function uniqueFunc2(arr, uniId){
        let hash = {}
        return arr.reduce((accum,item) => {
          hash[item[uniId]] ? '' : hash[item[uniId]] = true && accum.push(item)
          return accum
        },[])
      }
      
      
    • filter + map/set :

      //数组去重
      function distinct(a, b) {
          let arr = a.concat(b);
          return arr.filter((item, index)=> {
              return arr.indexOf(item) === index
          })
      }
      //数组对象去重
      function uniqueFunc(arr, uniId){
        const res = new Map();
        return arr.filter((item) => !res.has(item[uniId]) && res.set(item[uniId], 1));
      }
      
  3. 对象的属性名唯一性:利用空的 object 对象,把数组的值存成 Object 的 key 值,数组的 value 为 true,如果值再次出现,object[arr[i]]的值为 true ,不再加入对象

  4. Set 对象:利用 Set 对象值唯一性,[…new Set(arr)]

参考:数组去重1 数组去重2

原型对象和原型链

  1. 仅函数(除箭头函数)上存在 prototype 属性(这是一个显式原型属性),其指向原型对象,原型对象上存在两个属性:constructor 和 [[prototype]] (无法访问,部分浏览器可通过 __proto__ 访问,这是一个隐性的原型属性)
    • constructor :指向构造函数,即是函数自身
    • [[prototype]](__proto__):指向构造函数的原型对象,即 Object ,Object.__proto__ = null
  2. 当通过 new 调用这个函数时,得到的对象会存在 [[prototype]](__proto__),指向的是函数的 prototype 属性
  3. 所有的对象上都存在 [[prototype]](__proto__)属性
  4. 这种通过 __proto__ 在各对象间建立链接的工具就是原型链,通过原型链可以访问到其他对象的属性和方法

参考:《JavaScript 高级程序设计(第四版)》8.2.4章节 原型链

new 操作符创建对象的过程及模拟 new 实现

new 操作符创建对象会执行以下5步:

  1. 在内存中创建一个新对象
  2. 这个新对象内部的 [[Prototype]] 特性被赋值为构造函数的 prototype 属性(obj.__proto__ = fn.prototype
  3. 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
// 模拟 new 实现
function newFn(fn,...args) {
    const obj = Object.create(fn.prototype);
    // 等价于如下两句
    //const obj = new Object();
    //obj.__proto__ = fn.prototype;    
    that = obj;
    fn.apply(that,args)
    return that;
}

function Fns(name){
    this.name=name;
}

const fns = newFn(Fns,'fns');
const fns1 = newFn(Fns,'fns');
console.log("🚀 ~ file: 新建 文本文档.html ~ line 262 ~ fns", 
            fns,fns.__proto__,
            fns.name === fns1.name,
            fns.constructor === fns1.constructor,
            fns.prototype === fns1.prototype)

参考:《JavaScript 高级程序设计(第四版)》8.2 章节 new 的模拟实现

JS中继承实现的方式有哪些

继承指一个对象直接使用另一对象的属性和方法。JavaScript 的继承是通过原型链实现的。实现属性继承的核心代码是SuperFn.call(this),实现方法继承的核心代码是 SubFn.prototype.__proto__ = SuperFn.prototype,具体实现方式有以下6种:

  1. ES6 class类 extends 继承 :通过ES6 新增的语法糖实现继承
  2. 基于原型继承:原型赋值SubFn.prototype = new SuperFn();
  3. 借用构造函数:构造函数内部执行 SuperFn.call(this),只能继承属性,不能继承原型链上的方法
  4. 组合继承:构造函数内部执行 SuperFn.call(this),原型赋值 SubFn.prototype.__proto__ = SuperFn.prototype;
  5. 原型式继承:此种方式和 subObj = Object.create(superObj)实现的功能相同
  6. 寄生式组合继承:构造函数内部执行 SuperFn.call(this),原型赋值 SubFn.prototype = Object.create(SuperFn.prototype);

参考:《JavaScript 高级程序设计(第四版)》8.3 章节 继承 继承的8种方式

箭头函数和普通函数区别

箭头函数是 ES6 之后新增的语法糖,方便函数的简写,形式如下:

  1. 省略函数表达式:(a,b)=>{...} === function(a,b){...}
  2. 仅一个参数省略():a=>{...} === function(a){...}
  3. 函数只执行 returna=>a+1 === function(a){return a+1}

除了简写函数外,箭头函数不能使用arguments、super 和 new.target,也不能用作构造函数,也没有 prototype 属性 。箭头函数中的 this 的指向方面也和常规函数处理不同:在箭头函数中,this 绑定的是函数声明时所在的作用域的全局变量对象(保存全局上下文参数的对象)或活动对象(保存函数局部上下文参数的对象)

参考:《JavaScript 高级程序设计(第四版)》10.1 章节 ES6 系列之箭头函数

谈谈对闭包的理解

闭包指的是那些引用了另一个函数作用域中变量的函数(通常是在嵌套函数中实现的)。闭包的产生需满足以下两个条件:

  1. 在代码中引用了自由变量(其他作用域中的变量)
  2. 创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回,函数作为参数传递)

参考:《JavaScript 高级程序设计(第四版)》10.14 章节 闭包

this 的值

判断一个运行中函数的this 的值,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断this 的绑定对象。

  1. 由new 调用:绑定到新创建的对象。
  2. 由call 或者apply(或者bind)调用:绑定到指定的对象。
  3. 由上下文对象调用:绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到undefined,否则绑定到全局对象。

参考:《你不知道的JavaScript(上卷)》第2章 从 ECMAScript 规范解读 this this 的绑定规则

宏任务,微任务,事件循环

JavaScript 是通常是运行在浏览器环境下的,浏览器宿主环境中包含5个线程:

  1. JS引擎:负责执行执行栈的最顶部代码
  2. GUI线程:负责渲染页面
  3. 事件监听线程:负责监听各种事件
  4. 计时线程:负责计时
  5. 网络线程:负责网络通信

因为 JavaScript 引擎是单线程运行的,当其他线程发起事件和任务请求,该事件和任务请求会以异步的形式加入到事件队列中。

在浏览器中,事件队列分为两种:微任务和宏任务

  • 常见微任务:MutationObserver,Promise产生的回调

  • 常见宏任务 :macroTask,计时器结束的回调、事件回调、http回调

当执行栈清空时,JS 引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。

事件循环是 JavaScript 用来处理并发的一种模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。

参考:事件队列 并发模型与事件循环 mdn

谈谈对 Promise 的理解,模拟 Promise 实现

在Promise 还未提出之前,异步操作都是通过回调函数来执行的,使用回调函数往往会带来回调地狱和较差的可维护性两个缺点,使用 promise 能完美的解决。

对于异步处理,Promises/A+ 规范将异步操作分为两个阶段:unsettled 和 settled,事情总是从 unsettled 逐步发展到 settled

  • unsettled: 未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
  • settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转

事件存在 3 个状态:

  • pending:挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
  • fulfilled(resolved):已处理,已决阶段的一种状态,表示整件事情已经出现结果,并是一个可以按照正常逻辑进行下去的结果
  • rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法按照正常逻辑进行下去的结果,通常用于表示有一个错误

无论是阶段,还是状态,都是不可逆的,且状态改变如果是 resolved 和 rejected ,则无法再修改状态。

Promise 是一种异步处理的机制,符号Promises/A+ 规范,通过new 操作符来实例化。创建时需要传入执行器(executor,一个函数)函数作为参数。使用 Promise 的代码会执行异步操作,并且不会阻塞进程。Promise 的实例方法包括 Promise.prototype.then Promise.prototype.catch Promise.prototype.finally,静态方法包括 Promise.resolve Promise.reject Promise.race 返回的都是 Promise 对象可完成链式调用。

参考:Promise/A+ 规范 Promise mdn 45道 Promise 面试题 手写Promise 1 手写 Promise 2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值