0%

经过了昨天还算是友好的前菜,今天突然猛地来了一顿硬菜,让人有点措手不及(或者应该说是消化不良?)。不过想想这不就是我们来青训营的目的吗?见识以前可能只是听说过甚至完全没有听说过的知识,学习更先进的编程思维。总之,今天完全被月影老师圈粉了,甚至萌生了去极客买买买的想法(x

开篇,谨以此图表达我对 JavaScript 的热爱

如何写好 JavaScript

各司其职

举个例子,写一段 js,控制一个网页支持浅色和深色两种浏览模式,如何实现?
很容易想到的一种实现方法是,加一个切换按钮,然后给该按钮添加事件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', e => {
const body = document.body;
if (e.target.innerHTML === 'light') {
body.style.backgroundColor = 'black';
body.style.color = 'white';
e.target.innerHTML = 'dark';
} else {
body.style.backgroundColor = 'white';
body.style.color = 'black';
e.target.innerHTML = 'light';
}
})

这段代码的问题所在:直接使用 js 操作了样式,没有做到各司其职

修改版本一:

1
2
3
4
5
6
7
8
9
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', e => {
const body = document.body;
if (body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
})

通过添加或删除类名,实现状态切换,做到各司其职,使得代码结构得到优化。

修改版本二:完全使用 CSS 实现
这个主要是利用 CSS 的 checkbox 的 checked 伪类实现的。

1
2
3
4
#modeCheckBox:checked + .content {
background-color: black;
color: white;
}

推荐一个利用了这个原理实现明暗主题变换的 React 项目:戳这

组件封装

组件设计的原则:

  • 封装性
  • 正确性
  • 拓展性
  • 复用性

如何使用原生 JavaScript 实现一个电商网站的轮播图?

组件设计比较复杂的点往往在于控制的地方和组件本身有一定的耦合。 ——月影

比如说当前轮播图的第几个小圆点标红,就是显示第几张图片,它们之间的状态是耦合的。我们不希望它们之间关系绑的太死,所以我们一般会使用自定义事件。

详细的实现见青训营社区

划重点,原文中最后说到:
这个轮播组件实现了封装性和正确性,但是缺少了可扩展性。这个组件只能满足自身的使用,它的实现代码很难扩展到其他的组件,当有功能变化时,也需要修改其自身内部的代码。
比如产品经理因为某种原因,希望将图片下方的小圆点暂时去掉,只保留左右箭头。那么在这个版本中,就需要这么做:

  • 注释掉 html 中 .slider__control 相关的代码
  • 修改 Slider 组件,注释掉与小圆点控制相关的代码
  • 又或者,将来需要为这个组件添加新的用户控制,都需要对这个组件进行再修改

这样的修改常常涉及了核心代码的更改。那么,如何可以避免这样的修改,让组件具备可扩展性和复用性呢?

在 JavaScript 代码中,一个方法一般来说最多只能有 15 行代码,超过了就需要重构。 ——月影

解耦

如何解耦

  • 将控制元素(如小圆点)抽取成插件
  • 插件与组件之间通过依赖注入的方式建立联系

插件化

然后月影老师给了一个无敌的使用依赖注入实现的轮播图。核心要点是通过一个 registerPlugins 方法来完成插件的注册,然后分别实现三个插件(Controller、Next、Previous)。通过这样的重构,如果哪天 PM 要我们把小圆点删除掉,我们直接把小圆点对应的代码删掉就行了,不需要修改组件的核心代码。

依赖注入

为什么需要依赖注入?
插件的初始化依赖于组件。我们不希望组件知道插件的存在,但是插件需要知道组件的存在。

模板化

现在插件独立了,如果要删除小圆点,我们现在可以直接把对应插件的 JavaScript 代码删除掉,但是问题是我们还需要删掉 html 的对应代码,这样也是需要修改多个地方。因为我们还需要把 html 模板化。方法就是在 js 代码中实现一个 render 方法,来进行模板化的渲染。渲染方法里面可以加入一些参数,比如说有多少张图片,然后组件需要根据传入的参数进行渲染。另外插件也需要实现自己的 render 方法,来实现插件的 html 模板化;还有一个 action 函数,作用是根据组件依赖注入的实例来初始化插件数据。

抽象化

进一步修改实现的思想为:
实现一个 Component 类,里面有一个 render 方法。render 方法是一个抽象的方法,然后轮播图组件和各插件继承于它,再实现各自的 render 函数。也就是说,所有的元素都是组件(没有插件),小圆点也是组件、图片展示也是组件,最后整个的轮播图组件则是由多个小的组件组合而成。

过程抽象


什么是过程抽象呢?举个例子,比如你有间小房子,房子有门、有窗,这里门和窗也就是数据。那么你可以开门,也可以开窗,开门和开窗就属于过程。我们不仅仅可以抽象数据,还可以抽象这个过程。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<ul>
<li>任务一:学习 html</li>
<li>任务二:学习 css</li>
<li>任务三:学习 JavaScript</li>
</ul>

<script>
const ulElem = document.querySelector('ul');
const liElems = ulElem.querySelectorAll('li');

liElems.forEach(liElem => {
liElem.addEventListener('click', e => {
e.target.className = 'completed';
setTimeout(() => {
ulElem.removeChild(e.target);
}, 2000);
});
});
</script>

假设我们现在有三个任务需要完成,我们完成了其中一个所以要点击删除它。现在点击其中一个,他将会在 2s 之后完成删除,但是假如我们在这过程中再次点击这一项,就会出现报错:

报错的原因也很明显,点击的时候这一项已经被删除了,当然也就没有了,也无法执行删除操作。

这时候我们可以封装一个高阶函数 once,这个函数的目标就是限制删除只能进行一次,确保操作的安全性。

1
2
3
4
5
6
7
8
9
function once(fn) {
return function (...args) {
if (fn) {
const result = fn.apply(this, args);
fn = null;
return result;
}
}
}

观察这个函数的特征,它就是一个高阶函数

高阶函数

  • 以函数作为参数
  • 以函数作为返回值
  • 经常用于作为函数装饰器

常见的高阶函数还有:

  • 防抖和节流
  • consumer
  • iterative

防抖与节流

  • 防抖:在某个时间段内,函数只执行一次;但如果在该时间段内再次触发了这个事件,就会重新计算函数的执行时间。
  • 节流:在某个时间段内每隔一定时间,函数就执行一次。节流会降低函数的执行频率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 防抖
// 先开启一个定时任务执行,定时任务完成后则清空
// 当再调用时,如果定时任务仍存在则清空原来任务,创建新的定时任务
function debounce(fn, delay = 200) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 节流
// 先开启一个定时任务执行,定时任务完成后则清空
// 当再调用时,如果定时任务仍存在则不执行任何操作,如果不存在就开启一个新的定时器
function throttle(fn, delay = 200) {
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
}
}
}

测试防抖的代码如下,节流同理。

1
2
3
4
5
const debounceFn = debounce(function () {
console.log('我 resize 了');
}, 1000);

window.addEventListener('resize', debounceFn);

iterative

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 检验是否可迭代
const isIterable = obj => obj !== null
&& typeof obj[Symbol.iterator] === 'function';

function iterative(fn) {
return function (subject, ...rest) {
if (isIterable(subject)) {
// 把所有的执行结果存储于 result 中并返回
const result = [];
for (const obj of subject) {
result.push(fn.apply(this, [obj, ...rest]));
}
return result;
}
return fn.apply(this, [subject, ...rest]);
}
}

纯函数 & 非纯函数

  • 纯函数:如果输入值确定,那么输出值确定,函数没有状态。示例:
    1
    2
    3
    function add(a, b) {
    return a + b;
    }
  • 非纯函数:依赖于外部的环境,比如网络请求。示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    </ul>

    <script>
    function setColor(elems, color) {
    for (const elem of elems) {
    elem.style.color = color;
    }
    }

    const els1 = document.querySelectorAll('li:nth-child(2n+1)');
    const els2 = document.querySelectorAll('li:nth-child(3n+1)');
    setColor(els2, 'blue');
    setColor(els1, 'red');
    </script>
    上面的代码中,两个 setColor 的调用顺序如果更换,执行的结果会不一样,所以它的结果会受外部环境的影响,是非纯函数。

使用高阶函数,可以减少系统里面非纯函数的数量,从而使得系统的稳定性和可靠性加强。比如说,我们现在需要实现两个函数,一个是 setColor(elem, color),一次改变一个元素的颜色,还有一个 setColors(elems, color),一次改变多个元素的颜色。显然这两个函数都是非纯函数,但是我们可以选择直接定义:

1
const setColors = iterative(setColor);

这样就减少了一个非纯函数,有利于我们进行单元测试。

命令式 & 声明式

命令式:强调过程是什么

1
2
3
4
5
6
7
const list = [1, 2, 3, 4];
const arr = [];

for (let i = 0; i < list.length; i++) {
// 过程是给每个元素 × 2
arr.push(list[i] * 2);
}

声明式:强调操作是什么

1
2
3
4
5
6
const list = [1, 2, 3, 4];

// 操作是每个元素 x 2
const double = x => x * 2;

const arr = list.map(double);

假设现在需要实现一个开关,打开的时候显示字体 on,背景为绿色;关闭的时候显示字体 off,背景为红色。我们可以分别采用命令式和声明式实现。

命令式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<button class="off">off</button>

<style>
button {
width: 100px;
height: 100px;
border-radius: 50%;
border: none;
outline: none;
}

.on {
background-color: green;
}

.off {
background-color: red;
}
</style>

<script>
const button = document.querySelector('button');
button.addEventListener('click', (e) => {
if (e.target.className === 'on') {
e.target.className = 'off';
e.target.innerHTML = 'off';
} else {
e.target.className = 'on';
e.target.innerHTML = 'on';
}
})
</script>

优点:实现简单,代码比较容易理解。

声明式:
定义一个高阶函数 toggle,来实现状态的切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function toggle(...actions) {
return function (...args) {
// 先将第一个操作取出,然后再将其 push 到列表的末尾,因此可以实现循环
const action = actions.shift();
actions.push(action);
return action.apply(this, args);
}
}

button.addEventListener('click', toggle(
e => {
e.target.className = 'on';
e.target.innerHTML = 'on';
},
e => {
e.target.className = 'off';
e.target.innerHTML = 'off';
}
))

优点:很容易拓展状态,比如说我想加一个 warn 状态,非常简单,直接在 toggle 函数中加入新的一项即可。

所有的抽象都是为了提升可拓展性。 ——月影

弄了一晚上终于把上半节课的笔记做完了,呜呜太难了。

DOCTYPE 的作用

  • <!DOCTYPE> 声明位于文档中的最前面,处于 <html> 标签之前。告知浏览器的解析器用什么文档标准来解析这个文档。如果 DOCTYPE 不存在或格式不正确的话,会导致文档以兼容模式呈现。
  • 在标准模式(严格模式)下,浏览器的解析规则都是按照最新的标准进行解析的。而在兼容模式(怪异模式)下,浏览器会以向后兼容的方式来模拟老式浏览器的行为,以保证一些老的网站的正确访问。

标签语义化的意义

  • 便于浏览器的解析与渲染
  • 帮助阅读代码的人更容易理解代码的结构,利于代码的维护
  • 有利于 SEO
  • 实现网页的无障碍阅读

strong 和 em 的区别

  • em 用来局部强调,strong 则是全局强调
  • em 的强调是有顺序的,只有阅读到某处时用户才会注意到。strong 的强调则是一种随意无顺序的,看见的时候立刻就凸显出来的关键词句。斜体和粗体刚好满足了这两种视觉效果,因此也就成了 emstrong 的默认样式。

href 和 src 的区别

  • src 是 source 的缩写,指向外部资源的位置,指向的内容将会嵌入到文档中当前标签所在位置;在请求 src 资源时会将其指向的资源下载并应用到文档内,例如 js 脚本,img 图片和 iframe 等元素
  • href 是 Hypertext Reference 的缩写,指向网络资源所在位置。例如说我们在文档中添加 <link href="common.css" rel="stylesheet"/>,那么浏览器会识别该文档为 CSS 文件,然后并行下载资源并且不会停止对当前文档的处理

iframe 的应用和缺点

iframe 的应用
缺点:

  • iframe 会阻塞主页面的 onload 事件
  • 搜索引擎的检索程序无法解读这种页面,不利于 SEO

关于 line-height 继承的计算

line-height 有三种赋值方式,分别是单位、百分比、纯数字。

  • 带单位:px 是固定值,而 em 会参考父元素 font-size 值计算自身的行高
  • 纯数字:会把比例传递给后代。例如,父级行高为 1.5,子元素字体为 18px,则子元素行高为 1.5 * 18 = 27px
  • 百分比:会将计算后的值传递给后代。例如,父级行高为 150%,font-size 是 20px,那么行高就是 20px * 150% = 30px,子元素也是 30px

line-height 与 height 的区别

  • line-height 是每一行文字的高度,如果文字换行,整个盒子的实际高度会变大,高度 = 行数 * 行高
  • height 本身是一个已经写死的值,就代表盒子高度

link 和 @import 的区别

  • linkhtml 方式, @importcss 方式。@import 只有导入样式表的作用;link 不仅可以加载 CSS 文件,还可以定义其他属性,如 shortcut
  • 加载页面时,link 标签引入的 CSS 被同时并行加载;@import 引入的 CSS 将在页面加载完毕后被加载,可能会出现 FOUC
  • 浏览器对 link 支持早于 @import,兼容性更好

FOUC(Flash Of Unstyled Content):用户定义样式表加载之前浏览器使用默认样式显示文档,用户样式加载渲染之后再重新显示文档,造成页面闪烁。

CSS 什么属性可以继承,什么不能继承?

可以继承:

  • 字体相关的属性,font-sizefont-weight
  • 文本相关的属性,colorline-heighttext-align
  • 光标属性 cursor
  • 元素可见性 visibility

不可继承:几何属性,如 paddingmarginborder

当一个属性不是继承属性的时候,我们也可以通过将它的值设置为 inherit 来使它从父元素那获取同名的属性值来继承

padding 和 margin 赋值为百分比的时候是相对于什么计算的?

在默认的文档流方向下,marginpadding 的水平和垂直方向的百分比值都是相对于宽度计算的。

CSS 画三角形

1
2
3
4
5
6
7
8
9
10
11
12
<div class="triangle">
</div>

<style>
.triangle {
width: 0;
height: 0;
border-style: solid;
border-width: 100px;
border-color: black transparent transparent transparent;
}
</style>

margin 合并

蛋老师

盒子模型的类型

有 3 种盒子模型:IE 盒模型(border-box)、标准盒模型(content-box)、padding-box
盒模型一般都由四部分组成:内容(content)、填充(padding)、边界(margin)、边框(border)

盒子模型之间的区别:

  • 标准盒模型:width,height 只包含内容 content,不包含 border 和 padding
  • IE 盒模型:width,height 包含 content、border 和 padding
  • padding-box:width,height 包含 content、padding,不包含 border

BFC

BFC 解析

CSS 选择器

  • id 选择器 #myid
  • 类选择器 .classname
  • 标签选择器 div
  • 后代选择器 div p
  • 子选择器 div > p
  • 兄弟选择器 div ~ p
  • 相邻兄弟选择器 div + p
  • 属性选择器 a[rel=”external”]
  • 伪类选择器 a:hover,li:first-child
  • 伪元素选择器 a::before,a:after
  • 通配符选择器 *

伪元素和伪类的区别

  • 伪类用于当已有的元素处于某个状态时,为其添加对应的样式,这个状态是根据用户行为而动态变化的。比如说,当用户悬停在指定的元素时,我们可以通过 :hover 来描述这个元素的状态
  • 伪元素用于创建一些不在文档树中的元素,并为其添加样式。它们允许我们为元素的某些部分设置样式。比如说,我们可以通过 ::before 来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中

width: 100% 与 width: auto 的区别

  • width:100% 并不包含 margin-leftmargin-right 的属性值,直接取其父容器的宽度加上含 margin-leftmargin-right 的值。如果设置了 margin 那新的 width 值是容器的宽度加上 margin 的值
  • width:auto 包含 margin-leftmargin-right 的属性值。width:auto 总是占据整行,margin 的值已经包含其中了
  • 一般 width:auto 使用的多,因为这样灵活,而 width: 100% 使用比较少,因为在增加 padding 或者 margin 的时候,容易使其突破父级框,破坏布局

举例来说,
当 child 没有加 margin,两者之间没什么区别。

以下面这段代码为例,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
.parent {
height: 200px;
width: 200px;
background-color: rgb(195, 229, 236);
color: #000;
}

.child {
height: 100px;
width: 100%;
background-color: red;
color: #fff;
}
</style>

我们将其改成

1
2
3
.child {
width: auto;
}

结果相同,都是

但是如果给 child 加上 margin 后,比如:

1
2
3
.child {
margin-left: 50px;
}

width: 100% 的情况会变为:

width: auto 会变为:

图片空隙问题 & 基线

vertical-align 戳这

图片空隙问题见下面的代码:

1
2
3
4
<div>
<img src="https://cdn.jsdelivr.net/gh/Flower-F/picture@main/img/ai.jpg" alt="">
我是哀酱
</div>

因为是对齐基线,所以会有一点点空隙,修改方法:

1
2
3
img {
vertical-align: bottom;
}

浏览器页面的渲染过程 & 关键渲染路径

玩转 CSS 的艺术之美

行内元素和块级元素的区别

  • 块级元素可以设置宽高,独占一行
  • 行内元素不可以设置宽高,不独占一行

flex 布局

MDN 文档

grid 布局

MDN 文档

float 图文环绕

根据老师的说法,float 现在的使用场景基本只有这个。
双飞翼 & 圣杯:???

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
.container {
width: 300px;
overflow: hidden;
}

img {
height: 100px;
float: left;
margin-right: 10px;
}
</style>

<div class="container">
<img src="https://cdn.jsdelivr.net/gh/Flower-F/picture@main/img/ai.jpg" alt="你的哀酱">
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</p>
</div>

Chrome 文本小于 12px 如何解决

Chrome 中文界面下默认会将小于 12px 的文本强制按照 12px 显示。
可以使用 transform: scale(xx); 解决。

dp 问题的思考过程主要包括 5 个方面:

  • dp 数组的含义
  • base case,即边界
  • 状态,即会变化的变量
  • 选择:导致状态变化的行为
  • 状态转移方程

本文总结常见的动态规划题目:

下降路径最小和

这题属于是直接给出状态转移方程的 dp,所以不需要进行多余分析,直接确定边界就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @param {number[][]} matrix
* @return {number}
*/
var minFallingPathSum = function(matrix) {
const m = matrix.length, n = matrix[0].length;
const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));

for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (i === 0) {
// 边界:第一行,最小路径和等于自己本身
dp[i][j] = matrix[i][j];
} else if (j === 0) {
// 每行的最左边
dp[i][j] = Math.min(dp[i - 1][j], dp[i - 1][j + 1]) + matrix[i][j];
} else if (j === n - 1) {
// 每行的最右边
dp[i][j] = Math.min(dp[i - 1][j], dp[i - 1][j - 1]) + matrix[i][j];
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i - 1][j - 1], dp[i - 1][j + 1]) + matrix[i][j];
}
}
}

return Math.min(...dp[m - 1]);
};

零钱兑换

  • dp 含义:状态只有一个,所以是一维数组。定义为输入目标金额 i,凑出该目标金额 i 的最少硬币数量为 dp[i]
  • 边界:目标金额为 0,显然此时返回 0
  • 状态:硬币数量无限,硬币的面额题目固定了,所以变量只有目标金额
  • 选择:目标金额为什么会变化,是因为你选择了硬币
  • 状态转移方程:dp[i] = min { dp[i - coins[i]] + 1, dp[i] }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
var coinChange = function(coins, amount) {
const dp = new Array(amount + 1).fill(amount + 1);

// base case
dp[0] = 0;

for (let i = 0; i <= amount; i++) {
for (const coin of coins) {
if (i - coin < 0) {
continue;
}
// 状态转移
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}

return dp[amount] === amount + 1 ? -1 : dp[amount];
};

最长递增子序列

  • dp 含义:dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度
    以 1 4 3 4 2 为例:
    • dp[0] 指 1 为结尾的最长递增子序列 1 的长度,即 1
    • dp[1] 指 4 为结尾的最长递增子序列 1 4 的长度,即 2
    • 同理,dp[2] 为 1 3 的长度 2,dp[3] 为 1 3 4 的长度 3,dp[4] 为 1 2 的长度 2
  • 边界:如上例所示,显然 dp[i] 初始值为 1,因为以 nums[i] 结尾的最长递增子序列至少也要包含它自己
  • 状态:最长递增子序列的长度
  • 选择:更换了一个新的子序列结尾,就会导致最长递增子序列的长度变化
  • 状态转移方程:举个例子,对于 1 4 3 4 2 3 来说,dp[0] = 1,dp[1] = 2,dp[2] = 2,dp[3] = 3,dp[4] = 2。现在要求的是 dp[5],因为序列必须递增,所以我们从前面的子序列选取时,要满足的第一个条件是:结尾的字符比 nums[5] 要小;在此基础上,我们需要选取前面最长的一个子序列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
const len = nums.length;
// 边界,要全部初始化为 1
const dp = new Array(len).fill(1);

for (let i = 0; i < len; i++) {
for (let j = 0; j < i; j++) {
// 判断是否满足递增要求
if (nums[j] < nums[i]) {
// 状态转移
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}

// 最终结果是 dp 中的最大值
return Math.max(...dp);
};

变式一

如何输出这个子序列(若有多个满足条件,输出其中一个即可)?
使用一个 sequence 数组记录一下所有的子序列即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function (nums) {
const len = nums.length;
// 记录所有的子序列
const sequence = [];
// 边界,要全部初始化为 1
const dp = new Array(len).fill(1);
// 边界,所有的 sequence[i] 设置为对应的 nums[i]
for (let i = 0; i < len; i++) {
sequence[i] = nums[i];
}

for (let i = 0; i < len; i++) {
for (let j = 0; j < i; j++) {
// 判断是否满足递增要求
if (nums[j] < nums[i]) {
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
sequence[i] = sequence[j] + ' ' + nums[i];
}
}
}
}

// 找到序列对应的下标
let maxLen = 0, maxIndex = 0;
for (let i = 0; i < len; i++) {
if (maxLen < dp[i]) {
maxIndex = i;
}
}

return sequence[maxIndex];
};

变式二

如何把时间复杂度优化到 O(NlogN)?
使用贪心算法。

  • 新建数组 ans,用于保存最长上升子序列(其实是假的)。
  • 对原序列进行遍历,将每位元素二分插入 ans 中。
    • 如果 ans 中元素都比它小,将它插到最后
    • 否则,用它覆盖掉比它大的元素中最小的那个
      总之,思想就是让 ans 中存储比较小的元素。这样 ans 未必是真实的最长上升子序列,但长度是对的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
const len = nums.length;

const ans = [];
ans.push(nums[0]);

for (let i = 1; i < len; i++) {
// ans 中元素都比它小,将它插到最后
if (nums[i] > ans[ans.length - 1]) {
ans.push(nums[i]);
} else {
// 二分,查找最左边满足条件的值的下标
let left = 0, right = ans.length - 1;
let targetIndex;
while (left <= right) {
const mid = (left + right) >> 1;
if (ans[mid] >= nums[i]) {
// 满足条件
// 因为查找的是最左的满足条件的元素,所以是收缩右边界
right = mid - 1;
targetIndex = mid;
} else {
left = mid + 1;
}
}
// 用它覆盖掉比它大的元素中最小的那个
ans[targetIndex] = nums[i];
}
}

return ans.length;
};

参考资料:

  1. https://labuladong.gitee.io/algo/3/23/70/
  2. https://leetcode-cn.com/problems/minimum-falling-path-sum/solution/nickxue-xi-ji-hua-xi-lie-dong-tai-gui-hu-fbsb/
  3. https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-dong-tai-gui-hua-e/
  4. https://blog.csdn.net/weixin_30360497/article/details/94884825

题目链接:
https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/
解法分析:

解法一:前序遍历


由图示可知,树扯直以后特征是:每个左孩子为空,右孩子为前序遍历的下一位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {void} Do not return anything, modify root in-place instead.
*/
var flatten = function(root) {
const list = [];
preorderTraversal(root);

for (let i = 1; i < list.length; i++) {
const pre = list[i - 1], cur = list[i];
pre.left = null;
pre.right = cur;
}

function preorderTraversal(root) {
if (!root) return;

list.push(root);
preorderTraversal(root.left);
preorderTraversal(root.right);
}
};

解法二:递归

把树拉直的步骤可归为以下三步:

  • 将左子树和右子树扯直
  • 将右子树换成左子树
  • 将原来的右子树接到当前右子树的后面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {void} Do not return anything, modify root in-place instead.
*/
var flatten = function(root) {
if (!root) {
return;
}

flatten(root.left);
flatten(root.right);

// 后序遍历,所以此时二叉树已经被拉平
const leftTree = root.left, rightTree = root.right;

// 将右子树换成左子树
root.right = leftTree;
root.left = null;

//将原来的右子树接到当前右子树的后面
let p = root;
while (p.right) { // 查找最右节点
p = p.right;
}

p.right = rightTree;
};

参考题解:

  1. https://labuladong.gitee.io/algo/2/17/21/
  2. https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/solution/er-cha-shu-zhan-kai-wei-lian-biao-by-leetcode-solu/

合并两个有序链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
var mergeTwoLists = function(list1, list2) {
let dummyHead = new ListNode(-1);
let p1 = list1, p2 = list2;

let p = dummyHead;
while (p1 && p2) {
if (p1.val > p2.val) {
p.next = p2;
p2 = p2.next;
} else {
p.next = p1;
p1 = p1.next;
}
p = p.next;
}

p.next = p1 || p2;

return dummyHead.next;
};

链表中倒数第 K 个节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var getKthFromEnd = function(head, k) {
let slow, fast;
slow = fast = head;

for (let i = 0; i < k; i++) {
fast = fast.next;
}

while (fast) {
fast = fast.next;
slow = slow.next;
}

return slow;
};

删除链表的倒数第 N 个结点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
var removeNthFromEnd = function(head, n) {
const dummyHead = new ListNode(-1, head);
// 要删除倒数第 n 个节点,则需要找到倒数第 n + 1 个节点
const temp = getKthFromEnd(dummyHead, n + 1);
temp.next = temp.next.next;

return dummyHead.next;

// 返回链表的倒数第 k 个节点
function getKthFromEnd(head, k) {
let slow, fast;
slow = fast = head;
for (let i = 0; i < k; i++) {
fast = fast.next;
}
while (fast) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
};

链表的中间结点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var middleNode = function(head) {
let slow, fast;
slow = fast = head;
while (fast && fast.next) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
};

环形链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/

/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
let slow, fast;
slow = fast = head;

while (fast && fast.next) {
fast = fast.next.next;
slow = slow.next;

if (fast === slow) {
return true;
}
}

return false;
};

环形链表 II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/

/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function(head) {
let slow, fast;
slow = fast = head;

let flag = false; // 标记是否有环
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;

if (slow === fast) {
flag = true;
break;
}
}

if (!flag) {
return null;
}

slow = head;
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}

return slow;
};

相交链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/

/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
let p1 = headA, p2 = headB;
while (p1 !== p2) {
p1 = p1 ? p1.next : headB;
p2 = p2 ? p2.next : headA;
}
return p1;
};

反转链表

迭代版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let prev = null, cur = head;
while (cur) {
const tmp = cur.next; // 存储 cur 的下一个节点
cur.next = prev;
// 移动指针,继续前进
prev = cur;
cur = tmp;
}
return prev; // prev 最后指向的就是头节点
};

递归版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
// 若链表只有一个节点或根本没有节点,直接返回即可
if (head === null || head.next === null) {
return head;
}

const newList = reverseList(head.next);
// 假设节点 head 为节点 1,节点 head 的下一个节点为节点 2
const p2 = head.next; // 获取节点 2
p2.next = head; // 节点 2 的 next 指向节点 1
head.next = null; // 节点 1 的 next 设为空

return newList;
};

如果对 2->3->4 进行递归反转,则得到:

接下来只要把节点 2 的 next 指向节点 1,然后节点 1 的 next 指向 null 即可,如图:

反转链表 II

  • 定义两个指针,分别称之为 g(guard 守卫) 和 p(point)。我们首先根据方法的参数 left 确定 g 和 p 的位置。将 g 移动到第一个要反转的节点的前面,将 p 移动到第一个要反转的节点的位置上。我们以 left = 2,right = 4 为例。
  • 头插法:将 p 后面的元素删除,然后添加到 g 的后面。
  • 重复上面头插法的步骤,一共需要操作 right - left 次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} left
* @param {number} right
* @return {ListNode}
*/
var reverseBetween = function(head, left, right) {
let dummyHead = new ListNode(-1, head);

let g = dummyHead, p = dummyHead.next;

for (let i = 0; i < left - 1; i++) {
g = g.next;
p = p.next;
}

for (let i = 0; i < right - left; i++) {
// 暂存删除节点
let removed = p.next;
// 删除节点
p.next = p.next.next;
// 插入刚才删除的节点
removed.next = g.next;
g.next = removed;
}

return dummyHead.next;
};

参考资料:

  1. https://www.pianshen.com/article/1931399442/
  2. https://labuladong.gitee.io/algo/2/16/15/
  3. https://leetcode-cn.com/problems/reverse-linked-list-ii/solution/java-shuang-zhi-zhen-tou-cha-fa-by-mu-yi-cheng-zho/

该类题目包含以下的四道题:

删除有序数组中的重复项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @param {number[]} nums
* @return {number}
*/
var removeDuplicates = function (nums) {
if (nums.length === 0) {
return;
}

let slow, fast;
slow = fast = 0;
const len = nums.length;
while (fast < len) {
if (nums[fast] !== nums[slow]) {
// 每次找到一个不重复的元素就告诉 slow 并让 slow 前进一步
// 然后给 slow 赋值 fast 所在位置的值
// 因为第一个元素必定不是重复元素,所以先移动 slow
slow++;
nums[slow] = nums[fast];
}
fast++;
}

return slow + 1;
};

删除排序链表中的重复元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var deleteDuplicates = function(head) {
if (!head) {
return head;
}
let slow, fast;
slow = fast = head;
while (fast) {
if (fast.val !== slow.val) {
slow.next = fast;
slow = slow.next;
}
fast = fast.next;
}
// 与后面断开连接
slow.next = null;
return head;
};

移除元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @param {number[]} nums
* @param {number} val
* @return {number}
*/
var removeElement = function(nums, val) {
let slow, fast;
slow = fast = 0;
while (fast < nums.length) {
if (nums[fast] !== val) {
// 第一个元素也可能等于 val,所以先赋值再移动 slow
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
};

移动零

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
let slow, fast;
slow = fast = 0;
while (fast < nums.length) {
if (nums[fast] !== 0) {
[nums[slow], nums[fast]] = [nums[fast], nums[slow]];
slow++;
}
fast++;
}
};

参考题解:

  1. https://labuladong.gitee.io/algo/4/29/120/
  2. 代码随想录

创建应用

首先使用脚手架 create-react-app 创建应用

1
yarn create react-app project-name --template typescript

添加 baseUrl

打开文件 tsconfig.json,添加 baseUrl

添加 prettier

prettier 官方文档

1
yarn add --dev --exact prettier

在根目录创建空文件 .prettierrc.json,里面写入内容

1
{}

根目录创建新文件 .prettierignore,里面写入内容

1
2
build
coverage

接下来配置 pre-commit hooks

1
npx mrm@2 lint-staged

配置后,每次代码提交之前都会自动格式化

为了避免 prettier 与 eslint 冲突,需要安装 eslint-config-prettier

1
yarn add eslint-config-prettier -D

修改 package.json,在 lint-staged 配置项中增加 ts 和 tsx 选项(可自行选择其余拓展名)

修改 package.json,添加 prettier 拓展。

添加 commitlint

commitlint github 链接

设置提交规范

1
yarn add @commitlint/config-conventional @commitlint/cli -D

根目录新建文件 commitlint.config.js,添加如下内容

1
module.exports = { extends: ['@commitlint/config-conventional'] }

运行命令

1
yarn husky add .husky/commit-msg 'yarn commitlint --edit $1'

提交类型总结

  • build 编译相关的修改,例如发布版本、对项目构建或者依赖的改动
  • chore 其他修改,比如改变构建流程、或者增加依赖库、工具等
  • ci 持续集成修改
  • docs 文档修改
  • feat 新特性、新功能
  • fix 修改bug
  • perf 优化相关,比如提升性能、体验
  • refactor 代码重构
  • revert 回滚到上一个版本
  • style 代码格式修改,注意不是 css 修改
  • test 测试用例修改

参考资料:
链接:https://www.jianshu.com/p/9edce0b60f83

Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class Promise {
constructor(executor) {
this.state = PENDING;
this.value = null;
this.reason = null;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];

const resolve = (value) => {
// 避免多次 resolve 或 reject
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
// 一旦 resolve 执行,调用成功数组的函数
this.onResolvedCallbacks.forEach(fn => fn());
}
}

const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
// 一旦 reject 执行,调用失败数组的函数
this.onRejectedCallbacks.forEach(fn => fn());
}
}

try {
// executor 同步执行,用户可以选择调用 resolve 和 reject
executor(resolve, reject);
} catch (error) {
reject(error);
}
}

then(onFulfilled, onRejected) {
if (typeof onFulfilled !== 'function') {
// 当不是函数时,onFulfilled 返回一个普通的值,成功时直接等于 value
onFulfilled = value => value;
}
if (typeof onRejected !== 'function') {
// 当不是函数时,onRejected 直接抛出错误
onRejected = reason => {
throw new Error(reason instanceof Error ? reason.message : reason);
}
}

// onFulfilled 和 onRejected 都必须异步调用,而且严格意义上应该是微任务
// 此处只是使用 setTimeout 来模拟异步

// 若 onFulfilled 或 onRejected 报错,则直接 reject
const promise2 = new Promise((resolve, reject) => {
if (this.state === FULFILLED) {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
// resolvePromise 函数,处理自己 return 的 promise 和默认的 promise2 的关系
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.state === REJECTED) {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.state === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
}
})

return promise2;
}

// 处理不同的 promise 套用
resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject('Chaining cycle detected for promise')
}

let called = false; // 成功和失败只能调用一个,此处防止多次调用
if (x instanceof Object) {
try {
const then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if (called) return;
called = true;
// resolve 的结果依旧是 promise 那就继续解析
this.resolvePromise(promise2, y, resolve, reject);
}, error => {
if (called) return;
called = true;
reject(error);
})
} else {
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
reject(error);
}
} else {
resolve(x);
}
}
};

完成书写以后,可以使用 npm 包 promises-aplus-tests 进行测试。

测试前需要加上这几行代码:

1
2
3
4
5
6
7
8
9
Promise.defer = Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
module.exports = Promise;

Promise.resolve

Promise.resolve 会将任何值转成值为 value 状态是 fulfilled 的 Promise,但如果传入的值本身是 Promise 则会原样返回它。

1
2
3
4
5
6
Promise.resolve = function(value) {
if (value instanceof Promise) {
return value;
}
return new Promise(resolve => resolve(value));
};

Promise.reject

Promise.resolve 类似,Promise.reject 会实例化一个 rejected 状态的 Promise。但与 Promise.resolve 不同的是,如果给 Promise.reject 传递一个 Promise 对象,则这个对象会成为新 Promise 的值。

1
2
3
Promise.reject = function(reason) {
return new Promise((resolve, reject) => reject(reason));
};

Promise.all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Promise.all = function(promiseArr) {
let resolveCount = 0;
const result = [];
return new Promise((resolve, reject) => {
promiseArr.forEach((promise, index) => {
Promise.resolve(promise).then(val => {
resolveCount++;
result[index] = val;
if (resolveCount === promiseArr.length) {
resolve(result);
}
}, error => {
reject(error);
})
})
})
};

Promise.allSettled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Promise.allSettled = function(promiseArr) {
const result = [];

return new Promise((resolve, reject) => {
promiseArr.forEach((promise) => {
Promise.resolve(promise).then(val => {
result.push({
state: FULFILLED,
value: val
})
}, err => {
result.push({
state: REJECTED,
value: err
})
})
if (result.length === promiseArr.length) {
resolve(result);
}
})
})

}

Promise.race

1
2
3
4
5
6
7
8
9
10
11
Promise.race = function(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(promise => {
Promise.resolve(promise).then(val => {
resolve(val);
}, err => {
reject(err);
})
})
})
}

Promise.any

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Promise.any = function(promiseArr) {
let rejectCount = 0;
return new Promise((resolve, reject) => {
promiseArr.forEach((promise) => {
Promise.resolve(promise).then(val => {
resolve(val);
}, err => {
rejectCount++;
if (rejectCount === promiseArr.length) {
throw new Error('All promises were rejected')
}
})
})
})
}

参考资料:
https://juejin.cn/post/6844903625769091079
https://juejin.cn/post/6946022649768181774

事件捕获

当鼠标点击或者触发 DOM 事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件

事件冒泡

与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<style>
#div1 {
display: flex;
justify-content: center;
align-items: center;
width: 200px;
height: 200px;
background-color: red;
}
#div2 {
width: 100px;
height: 100px;
background-color: green;
}
</style>

<div id="div1">
<div id="div2"></div>
</div>

<script>
const div1 = document.getElementById('div1');
const div2 = document.getElementById('div2');

div1.onclick = function(){
alert('1');
}

div2.onclick = function(){
alert('2');
}
</script>

当点击 div2 时,会弹出两个弹出框。在 Chrome 浏览器,会先弹出 2 再弹出 1,这就是事件冒泡:事件从最底层的节点向上冒泡传播。事件捕获则跟事件冒泡相反。

W3C 的标准是先捕获再冒泡, addEventListener 函数的第三个参数决定把事件注册在捕获(true)还是冒泡(false)。

事件流阻止

event.preventDefault():取消事件对象的默认动作以及继续传播

事件委托

事件冒泡还允许我们利用事件委托这个概念依赖于这样一个事实,如果你想要在大量子元素中单击任何一个都可以运行一段代码,您可以将事件监听器设置在其父节点上,并让子节点上发生的事件冒泡到父节点上,而不是每个子节点单独设置事件监听器。

一个很好的例子是一系列列表项,如果你想让每个列表项被点击时弹出一条信息,您可以将click单击事件监听器设置在父元素 <ul> 上,这样事件就会从列表项冒泡到其父元素 <ul> 上。

例如,假设现在有 100 个 li,每个 li 有相同的点击事件。如果为每个 li 都添加事件,则会造成 DOM 访问次数过多,引起浏览器重绘与重排的次数过多,因此性能会降低。使用事件委托则可以解决这样的问题。

实现事件委托是利用了事件的冒泡原理实现的。当我们为最外层的节点添加点击事件,那么里面的 ul、li、a 的点击事件都会冒泡到最外层节点上,委托它代为执行事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

<script>
window.onload = function () {
const ul = document.getElementById('ul');
ul.onclick = function (ev) {
const target = ev.target;
if (target.nodeName.toLowerCase() === 'li') {
alert(target.innerHTML);
}
}
}
</script>

参考资料:
https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Building_blocks/Events

new 关键字做了什么

  • 创建一个新对象
  • 将新对象原型设置为构造函数原型
  • 使用 apply 绑定 this
  • 返回结果,需要特判上一步的返回值是否是 Object 类型

手写 new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function myNew(fn, ...args) {
// 创建一个新的对象
const obj = {}
// 把该对象的原型设置为 fn 的原型
if (fn.prototype !== null) {
Object.setPrototypeOf(obj, fn.prototype);
}
// 使用 apply,改变构造函数 this 的指向到新建的对象
// 这样 obj 就可以访问到构造函数中的属性
const result = fn.apply(obj, args);

return result instanceof Object ? result : obj;
}

function Person(name, age) {
this.name = name;
this.age = age;
}

// const person = new Person('Jack', 12);
const person = myNew(Person, 'Jack', 12);
console.log(person);