【面试题】 做了一份前端面试复习计划,保熟~

 给大家推荐一个实用面试题库

1、前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

一、简历

简历在找工作过程中是非常非常重要的,无论你是什么途径去面试的,面试你的人一定会看你的简历。

1、重点

  • 简历就像高考作文——阅卷时间非常短。
  • 内容要简洁。
  • 直击重点,表现出自己的优势(只要是符合招人单位要求的都是优势,不是别人不会的你会才叫优势)。

2、简历包含的内容

  • 个人信息。
  • 专业技能。
  • 工作经历。
  • 项目经历。
  • 社区贡献。

2.1 基本信息

  • 必备:姓名 电话 邮箱。
  • 年龄(最好写上,在这个行业年龄还是比较重要的),学历(写好是哪一届)。
  • 头像无所谓(好看就放上呗)。
  • 可以放 github 链接,前提是有内容。

2.2 专业技能

  • 表现出自己的核心竞争力(只要是符合招人单位要求的都是优势)。
  • 内容不要太多,3、5 条即可。
  • 太基础的不要写,例如会用 vscode、lodash。

2.3 工作经历

  • 如实写。
  • 写明公司,职位,入职离职时间即可,多写无益。
  • 如果有空窗期,如实写明即可。

2.4 项目经历

  • 写 2-4 个具有说服力的项目(不要什么项目都写,没用)。
  • 项目名称,项目描述,技术栈,个人角色。

2.5 社区贡献

  • 有博客或者开源作品,会让你更有竞争力。
  • 切记:需要真的有内容,不可临时抱佛脚。

3、注意事项

  • 界面不能太花哨,简洁明了即可。
  • 注意用词,“精通”“熟练”等慎用,可用“熟悉”。
  • 不可造假,会被拉入黑名单。

4、面试前准备

  • 看 JD,是否需要临时准备一下。
  • 打印纸质简历,带着纸和笔(增加好印象)。
  • 最好带着自己电脑,现场可能手写代码(带一个帆布包最适合,又优雅又方便)。
  • 要有时间观念,如果迟到或者推迟,要提前说。
  • 衣着适当,不用正装,也不要太随意。
  • 为何离职?—— 不要吐槽前东家,说自己的原因(想找一个更好的发展平台等)。
  • 能加班吗?—— 能!除非你特别自信,能找到其他机会。
  • 不要挑战面试官,即便他错了(面试一定要保证愉快)。
  • 遇到不会的问题,要表现出自己积极的一面(不好意思哈,确实是我的知识盲区,可以跟我说下 xxx 吗,我回去研究一下)。

二、HTML+CSS 面试题

HTML 和 CSS 面试题答不出来基本可以回去了。

1、HTML 面试题

以下是针对 HTML 相关的面试题,一般来说这地方不会出太多题,面试官也不愿意花太多时间在这上面。

1.1 如何理解 HTML 语义化?

  • 让人更容易读懂(增加代码可读性)。
  • 让搜索引擎更容易读懂,有助于爬虫抓取更多的有效信息,爬虫依赖于标签来确定上下文和各个关键字的权重(SEO)。
  • 在没有 CSS 样式下,页面也能呈现出很好地内容结构、代码结构。

1.2 script 标签中 defer 和 async 的区别?

  • script :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
  • async script :解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。
  • defer script:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本。

下图清晰地展示了三种 script 的过程: 

推荐文章:

1.3 从浏览器地址栏输入 url 到请求返回发生了什么

先阅读这篇科普性质的:从 URL 输入到页面展现到底发生什么? 先阅读篇文章:从输入 URL 开始建立前端知识体系

  1. 输入 URL 后解析出协议、主机、端口、路径等信息,并构造一个 HTTP 请求。
  • 强缓存。
  • 协商缓存。
  1. DNS 域名解析。(字节面试被虐后,是时候搞懂 DNS 了

  2. TCP 连接。

    总是要问:为什么需要三次握手,两次不行吗?其实这是由 TCP 的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。第一次握手可以确认客服端的发送能力,第二次握手,确认了服务端的发送能力和接收能力,所以第三次握手才可以确认客户端的接收能力。不然容易出现丢包的现象。

  3. http 请求。

  4. 服务器处理请求并返回 HTTP 报文。

  5. 浏览器渲染页面。

  1. 断开 TCP 连接。

2、CSS 面试题

以下是针对 CSS 相关的面试题,这些题答不出来会给人非常不好的技术印象。

2.1 盒模型介绍

CSS3 中的盒模型有以下两种:标准盒模型IE(替代)盒模型

两种盒子模型都是由 content + padding + border + margin 构成,其大小都是由 content + padding + border 决定的,但是盒子内容宽/高度(即 width/height)的计算范围根据盒模型的不同会有所不同:

  • 标准盒模型:只包含 content 。
  • IE(替代)盒模型:content + padding + border 。

可以通过 box-sizing 来改变元素的盒模型:

  • box-sizing: content-box :标准盒模型(默认值)。
  • box-sizing: border-box :IE(替代)盒模型。

2.2 css 选择器和优先级

首先我们要知道有哪些选择器:选择器参考表

常规来说,大家都知道样式的优先级一般为 !important > style > id > class ,但是涉及多类选择器作用于同一个元素时候怎么判断优先级呢?相信我,你在改一些第三方库(比如 antd 😂)样式时,理解这个会帮助很大!

这篇文章写的非常清晰易懂,强烈推荐,看完之后就没啥问题了:深入理解 CSS 选择器优先级

上述文章中核心内容: 优先级是由 A 、B、C、D 的值来决定的,其中它们的值计算规则如下:

  • 如果存在内联样式,那么 A = 1,否则 A = 0 ;
  • B 的值等于 ID选择器(#id) 出现的次数;
  • C 的值等于 类选择器(.class) 和 属性选择器(a[href="https://example.org"]) 和 伪类(:first-child) 出现的总次数;
  • D 的值等于 标签选择器(h1,a,div) 和 伪元素(::before,::after) 出现的总次数。

从左至右比较,如果是样式优先级相等,取后面出现的样式。

2.3 重排(reflow)和重绘(repaint)的理解

简单地总结下两者的概念:

  • 重排:无论通过什么方式影响了元素的几何信息(元素在视口内的位置和尺寸大小),浏览器需要重新计算元素在视口内的几何属性,这个过程叫做重排。
  • 重绘:通过构造渲染树和重排(回流)阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(元素在视口内的位置和尺寸大小),接下来就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘。

如何减少重排和重绘?

  • 最小化重绘和重排,比如样式集中改变,使用添加新样式类名 .class 或 cssText 。
  • 批量操作 DOM,比如读取某元素 offsetWidth 属性存到一个临时变量,再去使用,而不是频繁使用这个计算属性;又比如利用 document.createDocumentFragment() 来添加要被添加的节点,处理完之后再插入到实际 DOM 中。
  • 使用 **absolute** 或 **fixed** 使元素脱离文档流,这在制作复杂的动画时对性能的影响比较明显。
  • 开启 GPU 加速,利用 css 属性 transform 、will-change 等,比如改变元素位置,我们使用 translate 会比使用绝对定位改变其 left 、top 等来的高效,因为它不会触发重排或重绘,transform 使浏览器为元素创建⼀个 GPU 图层,这使得动画元素在一个独立的层中进行渲染。当元素的内容没有发生改变,就没有必要进行重绘。

这里推荐腾讯 IVWEB 团队的这篇文章:你真的了解回流和重绘吗,好好认真看完,面试应该没问题的。

2.4 对 BFC 的理解

BFC 即块级格式上下文,根据盒模型可知,每个元素都被定义为一个矩形盒子,然而盒子的布局会受到尺寸,定位,盒子的子元素或兄弟元素,视口的尺寸等因素决定,所以这里有一个浏览器计算的过程,计算的规则就是由一个叫做视觉格式化模型的东西所定义的,BFC 就是来自这个概念,它是 CSS 视觉渲染的一部分,用于决定块级盒的布局及浮动相互影响范围的一个区域

BFC 具有一些特性:

  1. 块级元素会在垂直方向一个接一个的排列,和文档流的排列方式一致。
  2. 在 BFC 中上下相邻的两个容器的 margin  会重叠,创建新的 BFC 可以避免外边距重叠。
  3. 计算 BFC 的高度时,需要计算浮动元素的高度。
  4. BFC 区域不会与浮动的容器发生重叠。
  5. BFC 是独立的容器,容器内部元素不会影响外部元素。
  6. 每个元素的左 margin  值和容器的左 border  相接触。

利用这些特性,我们可以解决以下问题:

  • 利用 4  和 6 ,我们可以实现三栏(或两栏)自适应布局。
  • 利用 2 ,我们可以避免 margin  重叠问题。
  • 利用 3 ,我们可以避免高度塌陷。

创建 BFC 的方式:

  • 绝对定位元素(position 为 absolute 或 fixed )。
  • 行内块元素,即 display 为 inline-block 。
  • overflow 的值不为 visible 。

推荐文章:可能是最好的 BFC 解析了...

2.5 实现两栏布局(左侧固定 + 右侧自适应布局)

现在有以下 DOM 结构:

<div class="outer">
  <div class="left">左侧</div>
  <div class="right">右侧</div>
</div>
复制代码
  1. 利用浮动,左边元素宽度固定 ,设置向左浮动。将右边元素的 margin-left 设为固定宽度 。注意,因为右边元素的 width 默认为 auto ,所以会自动撑满父元素。
.outer {
  height: 100px;
}
.left {
  float: left;
  width: 200px;
  height: 100%;
  background: lightcoral;
}
.right {
  margin-left: 200px;
  height: 100%;
  background: lightseagreen;
}
复制代码
  1. 同样利用浮动,左边元素宽度固定 ,设置向左浮动。右侧元素设置 overflow: hidden; 这样右边就触发了 BFC ,BFC 的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠。
.outer {
  height: 100px;
}
.left {
  float: left;
  width: 200px;
  height: 100%;
  background: lightcoral;
}
.right {
  overflow: auto;
  height: 100%;
  background: lightseagreen;
}
复制代码
  1. 利用 flex 布局,左边元素固定宽度,右边的元素设置 flex: 1 。
.outer {
  display: flex;
  height: 100px;
}
.left {
  width: 200px;
  height: 100%;
  background: lightcoral;
}
.right {
  flex: 1;
  height: 100%;
  background: lightseagreen;
}
复制代码
  1. 利用绝对定位,父级元素设为相对定位。左边元素 absolute  定位,宽度固定。右边元素的 margin-left  的值设为左边元素的宽度值。
.outer {
  position: relative;
  height: 100px;
}
.left {
  position: absolute;
  width: 200px;
  height: 100%;
  background: lightcoral;
}
.right {
  margin-left: 200px;
  height: 100%;
  background: lightseagreen;
}
复制代码
  1. 利用绝对定位,父级元素设为相对定位。左边元素宽度固定,右边元素 absolute  定位, left  为宽度大小,其余方向定位为 0 。
.outer {
  position: relative;
  height: 100px;
}
.left {
  width: 200px;
  height: 100%;
  background: lightcoral;
}
.right {
  position: absolute;
  left: 200px;
  top: 0;
  right: 0;
  bottom: 0;
  height: 100%;
  background: lightseagreen;
}
复制代码

2.6 实现圣杯布局和双飞翼布局(经典三分栏布局)

圣杯布局和双飞翼布局的目的:

  • 三栏布局,中间一栏最先加载和渲染(内容最重要,这就是为什么还需要了解这种布局的原因)。
  • 两侧内容固定,中间内容随着宽度自适应。
  • 一般用于 PC 网页。

圣杯布局和双飞翼布局的技术总结:

  • 使用 float  布局。
  • 两侧使用 margin 负值,以便和中间内容横向重叠。
  • 防止中间内容被两侧覆盖,圣杯布局用 padding ,双飞翼布局用 margin 。

圣杯布局: HTML 结构:

<div id="container" class="clearfix">
  <p class="center">我是中间</p>
  <p class="left">我是左边</p>
  <p class="right">我是右边</p>
</div>
复制代码

CSS 样式:

#container {
  padding-left: 200px;
  padding-right: 150px;
  overflow: auto;
}
#container p {
  float: left;
}
.center {
  width: 100%;
  background-color: lightcoral;
}
.left {
  width: 200px;
  position: relative;
  left: -200px;
  margin-left: -100%;
  background-color: lightcyan;
}
.right {
  width: 150px;
  margin-right: -150px;
  background-color: lightgreen;
}
.clearfix:after {
  content: "";
  display: table;
  clear: both;
}
复制代码

双飞翼布局: HTML 结构:

<div id="main" class="float">
  <div id="main-wrap">main</div>
</div>
<div id="left" class="float">left</div>
<div id="right" class="float">right</div>
复制代码

CSS 样式:

.float {
  float: left;
}
#main {
  width: 100%;
  height: 200px;
  background-color: lightpink;
}
#main-wrap {
  margin: 0 190px 0 190px;
}
#left {
  width: 190px;
  height: 200px;
  background-color: lightsalmon;
  margin-left: -100%;
}
#right {
  width: 190px;
  height: 200px;
  background-color: lightskyblue;
  margin-left: -190px;
}
复制代码

tips:上述代码中 margin-left: -100%  相对的是父元素的 content  宽度,即不包含 paddig 、 border  的宽度。

其实以上问题需要掌握 margin 负值问题 即可很好理解。

2.7 水平垂直居中多种实现方式

  1. 利用绝对定位,设置 left: 50%  和 top: 50%  现将子元素左上角移到父元素中心位置,然后再通过 translate  来调整子元素的中心点到父元素的中心。该方法可以不定宽高
.father {
  position: relative;
}
.son {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
复制代码
  1. 利用绝对定位,子元素所有方向都为 0 ,将 margin  设置为 auto ,由于宽高固定,对应方向实现平分,该方法必须盒子有宽高
.father {
  position: relative;
}
.son {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0px;
  margin: auto;
  height: 100px;
  width: 100px;
}
复制代码
  1. 利用绝对定位,设置 left: 50% 和 top: 50% 现将子元素左上角移到父元素中心位置,然后再通过 margin-left  和 margin-top  以子元素自己的一半宽高进行负值赋值。该方法必须定宽高
.father {
  position: relative;
}
.son {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 200px;
  height: 200px;
  margin-left: -100px;
  margin-top: -100px;
}
复制代码
  1. 利用 flex ,最经典最方便的一种了,不用解释,定不定宽高无所谓的。
.father {
  display: flex;
  justify-content: center;
  align-items: center;
}
复制代码

其实还有很多方法,比如 display: grid  或 display: table-cell  来做,有兴趣点击下面这篇文章可以了解下:
面试官:你能实现多少种水平垂直居中的布局(定宽高和不定宽高)

2.8 flex 布局

这一块内容看 Flex 布局教程 就够了。

这里有个小问题,很多时候我们会用到 flex: 1 ,它具体包含了以下的意思:

  • flex-grow: 1 :该属性默认为 0 ,如果存在剩余空间,元素也不放大。设置为 1  代表会放大。
  • flex-shrink: 1 :该属性默认为 1 ,如果空间不足,元素缩小。
  • flex-basis: 0% :该属性定义在分配多余空间之前,元素占据的主轴空间。浏览器就是根据这个属性来计算是否有多余空间的。默认值为 auto ,即项目本身大小。设置为 0%  之后,因为有 flex-grow  和 flex-shrink  的设置会自动放大或缩小。在做两栏布局时,如果右边的自适应元素 flex-basis  设为 auto  的话,其本身大小将会是 0 。​

2.9 line-height 如何继承?

  • 父元素的 line-height 写了具体数值,比如 30px,则子元素 line-height 继承该值。
  • 父元素的 line-height 写了比例,比如 1.5 或 2,则子元素 line-height 也是继承该比例。
  • 父元素的 line-height 写了百分比,比如 200%,则子元素 line-height 继承的是父元素 font-size * 200% 计算出来的值。

三、js 基础

js 的考察其实来回就那些东西,不过就我自己而已学习的时候理解是真的理解了,但是忘也确实会忘(大家都说理解了一定不会忘,但是要答全的话还是需要理解+背)。

1、数据类型

以下是比较重要的几个 js 变量要掌握的点。

1.1 基本的数据类型介绍,及值类型和引用类型的理解

在 JS 中共有 8  种基础的数据类型,分别为: Undefined 、 Null 、 Boolean 、 Number 、 String 、 Object 、 Symbol 、 BigInt 。

其中 Symbol  和 BigInt  是 ES6 新增的数据类型,可能会被单独问:

  • Symbol 代表独一无二的值,最大的用法是用来定义对象的唯一属性名。
  • BigInt 可以表示任意大小的整数。

值类型的赋值变动过程如下:

let a = 100;
let b = a;
a = 200;
console.log(b); // 100
复制代码

 值类型是直接存储在**栈(stack)**中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;

引用类型的赋值变动过程如下:

let a = { age: 20 };
let b = a;
b.age = 30;
console.log(a.age); // 30
复制代码

 引用类型存储在**堆(heap)**中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;

1.2 数据类型的判断

  • typeof:能判断所有值类型,函数。不可对 null、对象、数组进行精确判断,因为都返回 object 。
console.log(typeof undefined); // undefined
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof "str"); // string
console.log(typeof Symbol("foo")); // symbol
console.log(typeof 2172141653n); // bigint
console.log(typeof function () {}); // function
// 不能判别
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
复制代码
  • instanceof:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。比如考虑以下代码:
class People {}
class Student extends People {}

const vortesnail = new Student();

console.log(vortesnail instanceof People); // true
console.log(vortesnail instanceof Student); // true
复制代码

其实现就是顺着原型链去找,如果能找到对应的 Xxxxx.prototype  即为 true 。比如这里的 vortesnail  作为实例,顺着原型链能找到 Student.prototype  及 People.prototype ,所以都为 true 。

  • Object.prototype.toString.call():所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等。
Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function () {}); // "[object Function]"
复制代码

在面试中有一个经常被问的问题就是:如何判断变量是否为数组?

Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"
复制代码

1.3 手写深拷贝

这个题一定要会啊!笔者面试过程中疯狂被问到!

文章推荐:如何写出一个惊艳面试官的深拷贝?

/**
 * 深拷贝
 * @param {Object} obj 要拷贝的对象
 * @param {Map} map 用于存储循环引用对象的地址
 */

function deepClone(obj = {}, map = new Map()) {
  if (typeof obj !== "object") {
    return obj;
  }
  if (map.get(obj)) {
    return map.get(obj);
  }

  let result = {};
  // 初始化返回结果
  if (
    obj instanceof Array ||
    // 加 || 的原因是为了防止 Array 的 prototype 被重写,Array.isArray 也是如此
    Object.prototype.toString(obj) === "[object Array]"
  ) {
    result = [];
  }
  // 防止循环引用
  map.set(obj, result);
  for (const key in obj) {
    // 保证 key 不是原型属性
    if (obj.hasOwnProperty(key)) {
      // 递归调用
      result[key] = deepClone(obj[key], map);
    }
  }

  // 返回结果
  return result;
}
复制代码

1.4 根据 0.1+0.2 ! == 0.3,讲讲 IEEE 754 ,如何让其相等?

建议先阅读这篇文章了解 IEEE 754 :硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准。 再阅读这篇文章了解如何运算:0.1 + 0.2 不等于 0.3?为什么 JavaScript 有这种“骚”操作?。​

原因总结:

  • 进制转换 :js 在做数字计算的时候,0.1 和 0.2 都会被转成二进制后无限循环 ,但是 js 采用的 IEEE 754 二进制浮点运算,最大可以存储 53 位有效数字,于是大于 53 位后面的会全部截掉,将导致精度丢失。
  • 对阶运算 :由于指数位数不相同,运算时需要对阶运算,阶小的尾数要根据阶差来右移(0舍1入),尾数位移时可能会发生数丢失的情况,影响精度。

解决办法:

  1. 转为整数(大数)运算。
function add(a, b) {
  const maxLen = Math.max(
    a.toString().split(".")[1].length,
    b.toString().split(".")[1].length
  );
  const base = 10 ** maxLen;
  const bigA = BigInt(base * a);
  const bigB = BigInt(base * b);
  const bigRes = (bigA + bigB) / BigInt(base); // 如果是 (1n + 2n) / 10n 是等于 0n的。。。
  return Number(bigRes);
}
复制代码

这里代码是有问题的,因为最后计算 bigRes 的大数相除(即 /)是会把小数部分截掉的,所以我很疑惑为什么网络上很多文章都说可以通过先转为整数运算再除回去,为了防止转为的整数超出 js 表示范围,还可以运用到 ES6 新增的大数类型,我真的很疑惑,希望有好心人能解答下。

  1. 使用 Number.EPSILON 误差范围。
function isEqual(a, b) {
  return Math.abs(a - b) < Number.EPSILON;
}

console.log(isEqual(0.1 + 0.2, 0.3)); // true
复制代码

Number.EPSILON 的实质是一个可以接受的最小误差范围,一般来说为 Math.pow(2, -52) 。​

  1. 转成字符串,对字符串做加法运算。
// 字符串数字相加
var addStrings = function (num1, num2) {
  let i = num1.length - 1;
  let j = num2.length - 1;
  const res = [];
  let carry = 0;
  while (i >= 0 || j >= 0) {
    const n1 = i >= 0 ? Number(num1[i]) : 0;
    const n2 = j >= 0 ? Number(num2[j]) : 0;
    const sum = n1 + n2 + carry;
    res.unshift(sum % 10);
    carry = Math.floor(sum / 10);
    i--;
    j--;
  }
  if (carry) {
    res.unshift(carry);
  }
  return res.join("");
};

function isEqual(a, b, sum) {
  const [intStr1, deciStr1] = a.toString().split(".");
  const [intStr2, deciStr2] = b.toString().split(".");
  const inteSum = addStrings(intStr1, intStr2); // 获取整数相加部分
  const deciSum = addStrings(deciStr1, deciStr2); // 获取小数相加部分
  return inteSum + "." + deciSum === String(sum);
}

console.log(isEqual(0.1, 0.2, 0.3)); // true
复制代码

这是 leetcode 上一道原题:415. 字符串相加。区别在于原题没有考虑小数,但是也是很简单的,我们分为两个部分计算就行。

2、 原型和原型链

可以说这部分每家面试官都会问了。。首先理解的话,其实一张图即可,一段代码即可。

function Foo() {}

let f1 = new Foo();
let f2 = new Foo();
复制代码

千万别畏惧下面这张图,特别有用,一定要搞懂,熟到提笔就能默画出来。 

总结:

  • 原型:每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性,其实就是 prototype 对象。
  • 原型链:由相互关联的原型组成的链状结构就是原型链。

先说出总结的话,再举例子说明如何顺着原型链找到某个属性。

推荐的阅读:JavaScript 深入之从原型到原型链 掌握基本概念,再阅读这篇文章轻松理解 JS 原型原型链加深上图的印象。

3、 作用域与作用域链

  • 作用域:规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。(全局作用域、函数作用域、块级作用域)
  • 作用域链:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找 。这种层级关系就是作用域链。(由多个执行上下文的变量对象构成的链表就叫做作用域链,学习下面的内容之后再考虑这句话)

需要注意的是,js 采用的是静态作用域,所以函数的作用域在函数定义时就确定了。

推荐阅读:先阅读JavaScript 深入之词法作用域和动态作用域,再阅读深入理解 JavaScript 作用域和作用域链。​

4、 执行上下文

这部分一定要按顺序连续读这几篇文章,必须多读几遍:

总结:当 JavaScript 代码执行一段可执行代码时,会创建对应的执行上下文。对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO);
  • 作用域链(Scope chain);
  • this。(关于 this 指向问题,在上面推荐的深入系列也有讲从 ES 规范讲的,但是实在是难懂,对于应付面试来说以下这篇阮一峰的文章应该就可以了:JavaScript 的 this 原理

5、 闭包

根据 MDN 中文的定义,闭包的定义如下:

在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。可以在一个内层函数中访问到其外层函数的作用域。

也可以这样说:

闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量变量。 闭包 = 函数 + 函数能够访问的自由变量。

在经过上一小节“执行上下文”的学习,再来阅读这篇文章:JavaScript 深入之闭包,你会对闭包的实质有一定的了解。在回答时,我们这样答:

在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。

闭包应用: 函数作为参数被传递:

function print(fn) {
  const a = 200;
  fn();
}

const a = 100;
function fn() {
  console.log(a);
}

print(fn); // 100
复制代码

函数作为返回值被返回:

function create() {
  const a = 100;

  return function () {
    console.log(a);
  };
}

const fn = create();
const a = 200;
fn(); // 100
复制代码

闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。

应用实例:比如缓存工具,隐藏数据,只提供 API 。

function createCache() {
  const data = {}; // 闭包中被隐藏的数据,不被外界访问
  return {
    set: function (key, val) {
      data[key] = val;
    },
    get: function (key) {
      return data[key];
    },
  };
}

const c = createCache();
c.set("a", 100);
console.log(c.get("a")); // 100
复制代码

6、 call、apply、bind 实现

这部分实现还是要知道的,就算工作中不会自己手写,但是说不准面试官就是要问,知道点原理也好,可以扩宽我们写代码的思路。

call

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

举个例子:

var obj = {
  value: "vortesnail",
};

function fn() {
  console.log(this.value);
}

fn.call(obj); // vortesnail
复制代码

通过 call 方法我们做到了以下两点:

  • call 改变了 this 的指向,指向到 obj 。
  • fn 函数执行了。

那么如果我们自己写 call 方法的话,可以怎么做呢?我们先考虑改造 obj 。

var obj = {
  value: "vortesnail",
  fn: function () {
    console.log(this.value);
  },
};

obj.fn(); // vortesnail
复制代码

这时候 this 就指向了 obj ,但是这样做我们手动给 obj 增加了一个 fn 属性,这显然是不行的,不用担心,我们执行完再使用对象属性的删除方法(delete)不就行了?

obj.fn = fn;
obj.fn();
delete obj.fn;
复制代码

根据这个思路,我们就可以写出来了:

Function.prototype.myCall = function (context) {
  // 判断调用对象
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  // 首先获取参数
  let args = [...arguments].slice(1);
  let result = null;
  // 判断 context 是否传入,如果没有传就设置为 window
  context = context || window;
  // 将被调用的方法设置为 context 的属性
  // this 即为我们要调用的方法
  context.fn = this;
  // 执行要被调用的方法
  result = context.fn(...args);
  // 删除手动增加的属性方法
  delete context.fn;
  // 将执行结果返回
  return result;
};
复制代码

apply

我们会了 call 的实现之后,apply 就变得很简单了,他们没有任何区别,除了传参方式。

Function.prototype.myApply = function (context) {
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  let result = null;
  context = context || window;
  // 与上面代码相比,我们使用 Symbol 来保证属性唯一
  // 也就是保证不会重写用户自己原来定义在 context 中的同名属性
  const fnSymbol = Symbol();
  context[fnSymbol] = this;
  // 执行要被调用的方法
  if (arguments[1]) {
    result = context[fnSymbol](...arguments[1]);
  } else {
    result = context[fnSymbol]();
  }
  delete context[fnSymbol];
  return result;
};
复制代码

bind

bind 返回的是一个函数,这个地方可以详细阅读这篇文章,讲的非常清楚:解析 bind 原理,并手写 bind 实现

Function.prototype.myBind = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  // 获取参数
  const args = [...arguments].slice(1),
  const fn = this;
  return function Fn() {
    return fn.apply(
      this instanceof Fn ? this : context,
      // 当前的这个 arguments 是指 Fn 的参数
      args.concat(...arguments)
    );
  };
};
复制代码

7、 new 实现

  1. 首先创一个新的空对象。
  2. 根据原型链,设置空对象的 __proto__ 为构造函数的 prototype 。
  3. 构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
  4. 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象。
function myNew(context) {
  const obj = new Object();
  obj.__proto__ = context.prototype;
  const res = context.apply(obj, [...arguments].slice(1));
  return typeof res === "object" ? res : obj;
}
复制代码

8、 异步

这部分着重要理解 Promise、async awiat、event loop 等。

8.1 event loop、宏任务和微任务

首先推荐一个可以在线看代码流程的网站:loupe。 然后看下这个视频学习下:到底什么是 Event Loop 呢?

简单的例子:

console.log("Hi");

setTimeout(function cb() {
  console.log("cb"); // cb 即 callback
}, 5000);

console.log("Bye");
复制代码

它的执行过程是这样的: 

 Web APIs 会创建对应的线程,比如 setTimeout 会创建定时器线程,ajax 请求会创建 http 线程。。。这是由 js 的运行环境决定的,比如浏览器。

看完上面的视频之后,至少大家画 Event Loop 的图讲解不是啥问题了,但是涉及到宏任务微任务,我们还得拜读一下这篇文章:这一次,彻底弄懂 JavaScript 执行机制。如果意犹未尽,不如再读下这篇非常详细带有大量动图的文章:做一些动图,学习一下 EventLoop。想了解事件循环和页面渲染之间关系的又可以再阅读这篇文章:深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)

注意:1.Call Stack 调用栈空闲 -> 2.尝试 DOM 渲染 -> 触发 Event loop

  • 每次 Call Stack 清空(即每次轮询结束),即同步任务执行完。
  • 都是 DOM 重新渲染的机会,DOM 结构有改变则重新渲染。
  • 然后再去触发下一次 Event loop。

宏任务:setTimeout,setInterval,Ajax,DOM 事件。 微任务:Promise async/await。

两者区别:

  • 宏任务:DOM 渲染后触发,如 setTimeout 、setInterval 、DOM 事件 、script 。
  • 微任务:DOM 渲染前触发,如 Promise.then 、MutationObserver 、Node 环境下的 process.nextTick 。

从 event loop 解释,为何微任务执行更早?

  • 微任务是 ES6 语法规定的(被压入 micro task queue)。
  • 宏任务是由浏览器规定的(通过 Web APIs 压入 Callback queue)。
  • 宏任务执行时间一般比较长。
  • 每一次宏任务开始之前一定是伴随着一次 event loop 结束的,而微任务是在一次 event loop 结束前执行的。

8.2 Promise

关于这一块儿没什么好说的,最好是实现一遍 Promise A+ 规范,多少有点印象,当然面试官也不会叫你默写一个完整的出来,但是你起码要知道实现原理。

关于 Promise 的所有使用方式,可参照这篇文章:ECMAScript 6 入门 - Promise 对象。 手写 Promise 源码的解析文章,可阅读此篇文章:从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节。 关于 Promise 的面试题,可参考这篇文章:要就来 45 道 Promise 面试题一次爽到底

实现一个 Promise.all:

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    // 参数可以不是数组,但必须具有 Iterator 接口
    if (typeof promises[Symbol.iterator] !== "function") {
      reject("Type error");
    }
    if (promises.length === 0) {
      resolve([]);
    } else {
      const res = [];
      let count = 0;
      const len = promises.length;
      for (let i = 0; i < len; i++) {
        //考虑到 promises[i] 可能是 thenable 对象也可能是普通值
        Promise.resolve(promises[i])
          .then((data) => {
            res[i] = data;
            if (++count === len) {
              resolve(res);
            }
          })
          .catch((err) => {
            reject(err);
          });
      }
    }
  });
};
复制代码

8.3 async/await 和 Promise 的关系

  • async/await 是消灭异步回调的终极武器。
  • 但和 Promise 并不互斥,反而,两者相辅相成。
  • 执行 async 函数,返回的一定是 Promise 对象。
  • await 相当于 Promise 的 then。
  • tru...catch 可捕获异常,代替了 Promise 的 catch。

9、 浏览器的垃圾回收机制

这里看这篇文章即可:「硬核 JS」你真的了解垃圾回收机制吗

总结一下:

有两种垃圾回收策略:

  • 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
  • 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。

标记清除的缺点:

  • 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
  • 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。

解决以上的缺点可以使用 **标记整理(Mark-Compact)算法 **,标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图) 

引用计数的缺点:

  • 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
  • 解决不了循环引用导致的无法回收问题。

V8 的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。

  • 针对新生区采用并行回收。
  • 针对老生区采用增量标记与惰性回收。

10、 实现一个 EventMitter 类

EventMitter 就是发布订阅模式的典型应用:

export class EventEmitter {
  private _events: Record<string, Array<Function>>;

  constructor() {
    this._events = Object.create(null);
  }

  emit(evt: string, ...args: any[]) {
    if (!this._events[evt]) return false;

    const fns = [...this._events[evt]];
    fns.forEach((fn) => {
      fn.apply(this, args);
    });

    return true;
  }

  on(evt: string, fn: Function) {
    if (typeof fn !== "function") {
      throw new TypeError("The evet-triggered callback must be a function");
    }
    if (!this._events[evt]) {
      this._events[evt] = [fn];
    } else {
      this._events[evt].push(fn);
    }
  }

  once(evt: string, fn: Function) {
    const execFn = () => {
      fn.apply(this);
      this.off(evt, execFn);
    };
    this.on(evt, execFn);
  }

  off(evt: string, fn?: Function) {
    if (!this._events[evt]) return;
    if (!fn) {
      this._events[evt] && (this._events[evt].length = 0);
    }

    let cb;
    const cbLen = this._events[evt].length;
    for (let i = 0; i < cbLen; i++) {
      cb = this._events[evt][i];
      if (cb === fn) {
        this._events[evt].splice(i, 1);
        break;
      }
    }
  }

  removeAllListeners(evt?: string) {
    if (evt) {
      this._events[evt] && (this._events[evt].length = 0);
    } else {
      this._events = Object.create(null);
    }
  }
}
复制代码

 

 给大家推荐一个实用面试题库

1、前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端技术栈

支持鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值