<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Asuhe</title><description>Asuhe的个人博客 - 分享技术、生活与思考的空间</description><link>https://asuhe.org/</link><language>zh-CN</language><item><title>Boostrap初探</title><link>https://asuhe.org/blog/cb5afcc/</link><guid isPermaLink="true">https://asuhe.org/blog/cb5afcc/</guid><pubDate>Sat, 09 Oct 2021 19:35:43 GMT</pubDate><content:encoded>## Boostrap简介

Boostrap是一套基于 HTML、CSS 和 Javascript的的响应式前端框架。简单来说就是一套预先定义好的css文件和js文件，当我们需要使用时在html页面中用`link`标签引入我们需要的文件，然后使用即可。

## Boostrap的使用

Boostrap的使用极其简单，我们只需要根据它定义好的类名，在我们要引入样式的标签上添加它给的类名就可以使用了。若默认样式不满足我们的需求，我们可以自己使用选择器更改对应属性。

Bootstrap 需要为页面内容和栅格系统包裹一个 .container 容器，它提供了两个作此用处的类：container类和container-fluid类，container-fluid类一般用于移动端页面开发。这个.container容器被预定好响应式样式，当我们定义这个类时，引入的Boostrap文件会对其进行一系列初始化操作。

### container和container-fluid的区别

#### container类

* 响应式布局的容器 固定宽度
* 大屏 ( &gt;=1200px)  宽度定为 1170px 
* 中屏 ( &gt;=992px)   宽度定为 970px 
* 小屏 ( &gt;=768px)   宽度定为 750px 
* 超小屏 (100%)

#### container-fluid类

* 流式布局容器百分百宽度
* 占据全部视口（viewport）的容器

### 栅格系统

Bootstrap 提供了一套响应式、移动设备优先的流式栅格系统，随着屏幕或视口（viewport）尺寸的增加， 系统会自动分为最多12列。

它是指将页面布局划分为等宽的列，然后通 过列数的定义来模块化页面布局。

当我们使用栅格系统时都是通过使用一系列`row`和`column`来进行布局操作。例如携程的移动端页面![image-20211009195752403](https://i.loli.net/2021/10/09/s1eycKh4ALHdiav.png)

这种布局我们可以用flex布局，也可用栅格系统快速制作。

container里页面已经被分为十二等份，而这个页面中我们可以很容易知道，先将页面份为三列，再在第二列与第三列中分为上下两行。栅格系统中我们 html 可以这样写

```html
&lt;div class=&quot;row&quot;&gt;
    //col-sm-4 即让我们三个盒子各自分到了四份空间
	&lt;div class=&quot;col-sm-4 column1&quot;&gt;&lt;/div&gt;
	&lt;div class=&quot;col-sm-4 column2&quot;&gt;&lt;/div&gt;
	&lt;div class=&quot;col-sm-4 column3&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
```

当我们需要进行行切分时，html 中可以这样写

```html
//row-cols-2 让我们的盒子再次按行切分为两行
&lt;div class=&quot;row&quot;&gt;
    //col-sm-4 即让我们三个盒子各自分到了四份空间
	&lt;div class=&quot;col-sm-4 column1&quot;&gt;&lt;/div&gt;
	&lt;div class=&quot;col-sm-4 row-cols-2&quot;&gt;
        &lt;div class=&quot;col&quot;&gt;Column&lt;/div&gt;
    	&lt;div class=&quot;col&quot;&gt;Column&lt;/div&gt;
    &lt;/div&gt;
	&lt;div class=&quot;col-sm-4 row-cols-2&quot;&gt;
		&lt;div class=&quot;col&quot;&gt;Column&lt;/div&gt;
        &lt;div class=&quot;col&quot;&gt;Column&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
```

如上我们在row盒子里可以得到一个 3 * 2 布局</content:encoded><author>Asuhe</author></item><item><title>事件对象</title><link>https://asuhe.org/blog/131e447c/</link><guid isPermaLink="true">https://asuhe.org/blog/131e447c/</guid><pubDate>Wed, 13 Oct 2021 17:53:03 GMT</pubDate><content:encoded>## 什么是事件对象

在浏览器中我们通常都是通过事件触发来实现网页与用户的交互效果的，在这个过程中js会产生一个事件对象。例如我们用常规方法给按钮绑定一个点击事件

```html
&lt;button&gt;按钮&lt;/button&gt;

&lt;script&gt;
    var btn = document.querySelector(&apos;btn&apos;);
    btn.addEventListiner(&apos;click&apos;,function( e ){ //这里我们给匿名函数的形参 e 就可以接收到 click 产生的事件对象
        alert(&apos;哈喽&apos;);
    })
&lt;/script&gt;
```

## 事件对象可以干什么

在返回的事件对象中包含了很多属性与方法，这些属性可以让我们准确的获取到用户目前的状态，例如用户按下了什么键、鼠标移动坐标、鼠标点击等等。然后我们可以根据用户的状态去设计我们的交互。我们可以打印一下这个对象看看里面都有什么

![部分事件对象的内容](https://i.loli.net/2021/10/14/91XMzcK2inRrb4k.png)

## 事件触发的三个阶段

一个事件从产生到被触发可以分为三个阶段：捕获阶段、停留阶段、冒泡阶段。在捕获阶段，浏览器查找触发的事件顺序是从顶级DOM依次查找，直到找到源头。如果查找过程中有DOM元素绑定了和事件源头一样的事件，则会被触发。举个例子

```html
&lt;div class=&apos;father&apos;&gt;
	&lt;div class=&apos;son&apos;&gt;Asuhe&lt;/div&gt;
&lt;/div&gt;

&lt;script&gt;
    var father = document.querySelector(&apos;.father&apos;);
    father.addEventListiner(&apos;click&apos;,function(){
        alert(&apos;我是你爸爸&apos;);
    },true) //开启捕获阶段
    
    var son = document.querySelector(&apos;.son&apos;);
    son.addEventListiner(&apos;click&apos;,function(){
        alert(&apos;我是你儿子&apos;);
    })
&lt;/script&gt;
```

这种情况下会先弹出父亲的话，然后在弹出儿子的话。</content:encoded><author>Asuhe</author></item><item><title>Windows terminal 免密码登录Linux服务器</title><link>https://asuhe.org/blog/94c7f9ab/</link><guid isPermaLink="true">https://asuhe.org/blog/94c7f9ab/</guid><pubDate>Fri, 08 Oct 2021 20:12:16 GMT</pubDate><content:encoded>## Linxu 服务器配置

### 登录 Linux 并生成密钥对

首先我们使用 putty 登录 Linux 服务器

![putty登录服务器](https://i.loli.net/2021/10/08/KNjAHIwMhWoTBU2.png)

再让 Linux 服务器先生成 rsa 密钥对供我们认证。使用如下命令

```bash
ssh-keygen
```

命令执行后对于弹出的选项我们一路回车就行

![生成密钥对](https://i.loli.net/2021/10/08/IRbGlNqvMUCSgrA.png)

密钥对生成后我们要将其中的公钥文件`id_rsa.pub`更改为` authorized_keys`，然后将私钥文件`id_rsa`下载至本地 Windows

### 更改 Linux 公钥文件名称

我们找到生成密钥的地址` /home/unbuntu/.ssh`，因为我们现在就是在当前用户目录下生成的密钥，所以只需要输入`cd .ssh`即可跳转至密钥存放的文件夹，`ls`查看一下。确实存在我们刚刚生成的密钥，最后更改公钥文件名称 Linux 端就大功告成了。再查看一下，文件是否更改成功

```bash
//跳转到密钥文件所在目录
cd .ssh

//更改公钥文件名
mv id_rsa.pub authorized_keys
```

![更改公钥文件名称](https://i.loli.net/2021/10/08/p7y5bLd8Y4vwNso.png)

## Windows 端配置

### 从 Linux 服务器下载私钥

Linux 服务器已经生成了密钥对，并且我们也成功将 Linux 中的公钥文件修改成功。接下来我们只要将 Linux 生成的私钥下载至本地即可。

首先我们需要确定 Linux 上私钥的存放地址，然后再确定 Windows 中我们将私钥存放的地址。比如 Windows 中我就将私钥存放在`D:\ssh_keys\tencent`

找到 Linux 中文件存放的地址，其实地址之前我们生成密钥对的时候就已经显示过了，我们只需要复制下来即可。私钥文件路径`/home/ubuntu/.ssh/id_rsa`

![获取私钥](https://i.loli.net/2021/10/08/HM9rK5IytaZNpeo.png)

使用 scp 将文件下载至我们指定的位置:` scp ubuntu@Linux的IP:/home/ubuntu/.ssh/id_rsa D:\ssh_keys\tencent`

```bash
//scp命令格式
scp Linux用户名@Linxu的IP地址:密钥的文件路径 Windows存放密钥的文件路径
```

根据提示输入 Linxu 用户密码

![下载私钥](https://i.loli.net/2021/10/08/Iva5uWUGbtTC6Yc.png)

下载完成后我们可以看见我们的密钥文件

![找到私钥](https://i.loli.net/2021/10/08/FtJqo8wEWRHfG45.png)

到了这一步我们就可以使用密钥登录啦，在命令行中输入:`ssh -i D:\ssh_keys\tencent\id_rsa ubuntu@Linux的IP地址`

```bash
//登录命令格式
ssh -i Windows中私钥的文件路径 Linux用户名@Linxu的IP地址
```

登录成功

![密钥登录](https://i.loli.net/2021/10/08/z2efD4Nit97U5xg.png)

### 设置私钥文件权限

走到这一步我们已经基本完成了，但是还有一个问题就是当我们执行密钥登录时 Windows 可能会提示我们私钥文件过权限大，忽视这个私钥，导致我们无法使用密钥登录

![私钥文件权限过于开放](https://i.loli.net/2021/10/08/EUnf5Lp2QgGZqDx.png)

这时我们需要将私钥文件`id_rsa`的权限设置为仅**所有者**控制

在 Windows 资源管理器中找到`id_rsa`文件，右键单击该文件，然后选择“属性”。导航到“安全性”选项卡，然后单击“高级”。

将所有者更改为你，禁用继承并删除所有权限，把除了自己以外的权限条目全部删除。然后授予自己“完全控制权”并保存权限。

![删除其它用户对私钥文件的权限](https://i.loli.net/2021/10/08/sbrithHIYfGSwlU.png)

现在 SSH 不再抱怨文件权限太开放了。

若原本权限条目中没有拥有者，则去添加中寻找并加入

![寻找本机用户组](https://i.loli.net/2021/10/08/xfqCWE7l2KDMJnt.png)

## 设置 Windows terminal

只需要去设置中将我们刚才登录 Linux 的命令复制一份并保存就可以了

![Windows terminal设置](https://i.loli.net/2021/10/08/URK3hpTQWH569cX.png)

这样以后我们打开 Windows terminal 选择选项卡就可以登录服务器了，打完收工！</content:encoded><author>Asuhe</author></item><item><title>JavaScript中的继承</title><link>https://asuhe.org/blog/925e68ec/</link><guid isPermaLink="true">https://asuhe.org/blog/925e68ec/</guid><pubDate>Sat, 23 Oct 2021 08:33:54 GMT</pubDate><content:encoded>## ES6以前

在es6以前 js 并没有专门的关键字可以实现继承特性，但是那时候我们又需要使用继承。于是就产生了一些方法可以实现继承的效果

### 构造函数继承

call调用实现继承父属性。我们都知道`call`可以改变函数内`this`的指向，利用这个特性我们可以在子构造函数里面调用父构造函数，并利用`call`让父构造函数内的`this`指向子构造函数。这样就完成了继承操作

```javascript
function Father(uname,age){
	this.name = uname;
	this.age = age;
};
function Son(uname,age){
    Father.call(this,uname,age); //调用父构造函数，利用call传入自身的this实现继承
}
```

重点：让新实例的原型等于父类的实例。
　　　　特点：1、实例可继承的属性有：实例的构造函数的属性，父类构造函数属性，父类原型的属性。（新实例不会继承父类实例的属性！）
　　　　缺点：1、新实例无法向父类构造函数传参。
　　　　　　　2、继承单一。
　　　　　　　3、所有新实例都会共享父类实例的属性。（原型上的属性是共享的，一个实例修改了原型属性，另一个实例的原型属性也会被修改！）

### 原型链继承

我们还可以通过让子构造函数的`prototype`指向一个父构造函数的实例的形式实现继承，不过需要注意的是在赋值完成后我们需要让`constructor`属性重新指回子构造函数

```javascript
function Father(uname,age){
	this.name = uname;
	this.age = age;
};
Father.prototype.say = ()=&gt; console.log(&apos;hello!&apos;);
function Son(uname,age){
    this.name = uname;
    this.age = age;
}

// Son.prototype = Father.prototype 若我们直接赋值，会导致Son的原型对象和Father的原型对象是同一个，在Son中修改原型对象同时也会修改Father的。所以这样赋值是错误的

Son.prototype = new Father();  //通过一个Father实例化对象可以间接访问到Father的prototype
Son.prototype.constructor = Son; //让constructor指回Son构造函数，不然其会指向Father构造函数

let son = new Son(&apos;Asuka&apos;,16);
son.say(); //hello！
```

### 组合继承

```js
function Father(uname,age){
	this.name = uname;
	this.age = age;
};
function Son(uname,age){
	Father.call(this,uname,age); // 借用构造函数继承
}
Son.prototype = new Father(); // 原型链继承
const s11 = new Son(&apos;zhangsan&apos;,18)
```







## ES6以后

es6中引入了`class`关键字同时也引入了`extends`关键字，利用这些关键字我们可以很轻易的实现继承。同时若我们想要调用父类的方法和属性只需要使用`super`关键字。

```javascript
class Father{
	constructor(uname,age){
		this.name = uname;
		this.age = age;
	}
	say(){
		console.log(&apos;hello!&apos;);
	}
}
class Son extends Father{
    constructor(uname,age,sex){
        super(uname,age); //直接使用，调用父类constructor.一定要在this之前，不然SyntaxError
        this.sex = sex;
    }
    say(){
        super.say();
        console.log(&apos;hi!&apos;);
    }
}
let son = new Son(&apos;Asuhe&apos;,16,&apos;male&apos;);
console.log(son); //Son { name: &apos;Asuhe&apos;, age: 16, sex: &apos;male&apos; }
son.say();  // hello! hi!
```</content:encoded><author>Asuhe</author></item><item><title>flex布局</title><link>https://asuhe.org/blog/e43580c1/</link><guid isPermaLink="true">https://asuhe.org/blog/e43580c1/</guid><pubDate>Sat, 09 Oct 2021 17:42:16 GMT</pubDate><content:encoded>## 什么是flex布局

传统布局解决方案主要是依赖`display` 属性 + `float`属性 + `position`属性来控制我们盒模型的位置，用这三个属性相互配合达到将盒子摆放至预期位置的目的。而flex布局主要是利用在父元素中添加`display:flex`属性，使用与其相关的属性配合达到布局的目的。

在传统布局中有些特殊布局并不好实现，而在flex布局中我们可以轻松实现。例如将三个盒子同时水平对齐和垂直居中对齐。

## flex布局原理

flex布局里我们可以指定任意一个元素为父元素，即使是行内元素我们也可以设置flex布局。

将父盒子作为一个容器，父盒子的儿子元素作为容器的成员。使用父元素控制属性可以控制儿子元素的排列位置。同时flex布局也提供了儿子元素控制属性，令我们可以单独控制特定子元素的位置。父元素控制属性可以理解为游戏里的群体控制效果，子元素控制属性可以理解单体控制效果。

在flex布局中，我们将页面看作一个二维坐标系，主轴正方向为从左至右，侧轴正方向为从上至下。

![flex坐标轴](https://i.loli.net/2021/10/09/lxCpiurQ5AbY4eE.png)

利用父元素控制属性我们可以进行切换主轴、设置换行等系列操作。接下来我们可以看看主要的父元素控制元素有哪些。

## 常见父元素属性

```css
 flex-direction：设置主轴的方向 
 justify-content：设置主轴上的子元素排列方式 
 flex-wrap：设置子元素是否换行
 align-content：设置侧轴上的子元素的排列方式（多行） 
 align-items：设置侧轴上的子元素排列方式（单行） 
 flex-flow：复合属性，相当于同时设置了 flex-direction 和 flex-wrap
```

### flex-direction

`flex-direction`用于设置主轴为哪个，默认情况下flex主轴为x轴，正方向是从左至右的。

![flex-direction属性值](https://i.loli.net/2021/10/09/YNQTgrkiF8eSwqP.png)

`flex-direction:row `效果（默认方向），**主轴方向**为从左至右

![flex-direction:row](https://i.loli.net/2021/10/09/765eRbhkcVWaiqQ.png)

`flex-direction:row-reverse `效果，**主轴方向**为从右至左

![image-20211009181827790](https://i.loli.net/2021/10/09/5I3k2TsWvpAu4oP.png)

`flex-direction:column `效果，**主轴方向**为从上至下

![flex-direction:column](https://i.loli.net/2021/10/09/tRj8GibZuxDHkFP.png)

`flex-direction:column-reverse `效果，**主轴方向**为从下至上

![flex-direction:column-recerse](https://i.loli.net/2021/10/09/3Ty9nv5pOoxl24V.png)



### justify-content

`justify-content`的作用是设置**主轴**上子元素的排列方式

![justify-content属性值](https://i.loli.net/2021/10/09/rkaxRVUWpyLocHF.png)

下列中的示例效果，主轴皆为默认的从左至右

`justify-content:flex-start`效果（默认）

![justify-content:flex-start](https://i.loli.net/2021/10/09/S5789RBDwpvmtQh.png)

`justify-content:flex-end`效果（注意和`flex-direction:row-reverse`的区别）

![justify-content:flex-end](https://i.loli.net/2021/10/09/epTGnWflVFsqKkm.png)

`justify-content:center`效果

![justify-content:center](https://i.loli.net/2021/10/09/cbAU9a7YCRLXIV8.png)

`justify-content:space-around`效果（平分剩余空间）

![justify-content:space-around](https://i.loli.net/2021/10/09/YjNS3qDA8FaUpMh.png)

`justify-content:space-between`效果（头尾元素贴紧边缘）

![justify-content:space-between](https://i.loli.net/2021/10/09/HBqDgMu2Fw8VRkL.png)

### flex-wrap

`flex-wrap`设置当子元素一行排不下时，是否换行。默认为不换行，将每个子元素宽度压缩至能一行排列。

当`flex-wrap:nowrap`时，flex子元素都会挤压在一行而**不会**像`float`一样空间不够时自动换行

当`flex-wrap:wrap`时，flex子元素保持原有大小，一行排不下时**会**像`float`一样空间不够时自动换行

### align-items

`align-items`设置**侧轴**上子元素排列方式（单行），**当子元素不止一行时此属性不生效**

![align-items属性值](https://i.loli.net/2021/10/09/brXIQGHRkuntC64.png)

`align-items:flex-start`效果（默认）

![align-items:flex-start](https://i.loli.net/2021/10/09/6ZrzaxDKd1UknLV.png)

`align-items:flex-end`效果，在侧轴顶部排列

![align-items:flex-end](https://i.loli.net/2021/10/09/Gq9C5svxzOyPoDI.png)

`align-items:center`效果，在侧轴中部排列

![align-items:center](https://i.loli.net/2021/10/09/pJUaHMNGVLPhIOR.png)

`align-items:stretch`效果（当子元素的`height`属性不设置时才显示效果），将子元素拉伸到和父元素一样高

![align-items:stretch](https://i.loli.net/2021/10/09/gxXsAWk3Nz9rb4H.png)

### align-content

align-content设置侧轴上的子元素的排列方式（多行）

设置子项在侧轴上的排列方式并且只能用于子项出现换行的情况（多行），在单行下是没有效果的。

![align-content属性值](https://i.loli.net/2021/10/09/PmbyIid5qYpx9uJ.png)

`align-content:flex-end`效果，在侧轴尾部排列

![align-content:flex-end](https://i.loli.net/2021/10/09/fORD7b9Ul1oJWZN.png)

`align-content:center`效果，在侧轴中间排列

![align-content:center](https://i.loli.net/2021/10/09/l9O3rR1XNFcfDZ4.png)

`align-content: space-around` 效果，平分剩余空闲空间并在侧轴两侧排列

![align-content: space-around;](https://i.loli.net/2021/10/09/xKrhMCI1XmOAw7Q.png)

`align-content: space-between `效果，首行和尾行紧贴侧轴两端，中间行平分剩余空间

![align-content: space-between](https://i.loli.net/2021/10/09/KtlQ17b3WqAYmis.png)

### flex-flow

`flex-flow`是`flex-direction`和`flex-wrap`的复合写法

```css
flex-flow:flex-direction-value flex-wrap-value 
```



## 常见子元素属性

### flex

`flex `属性定义子项目分配剩余空间，用flex来表示占多少**份数** 

设置`中`的属性为`flex:2`,空白空间被分为四份，中间的占两份

```css
//父容器空间被分为四等份，中间元素占2份，两边元素占1份
.zuo {
	flex:1;
}
.zhong {
	flex:2;
}
.you {
	flex:1;
}
```

![flex:2](https://i.loli.net/2021/10/09/l6uzL1tUy9bw5nJ.png)

#### align-self

`align-self `控制子项**自己**在**侧轴**上的排列方式，可以单独控制一个子元素的位置


align-self 属性允许单个项目有与其他项目不一样的对齐方式，可覆盖 align-items 属性。 默认值为 auto，表示继承父元素的 align-items 属性，如果没有父元素，则等同于 stretch。

```css
//如设置 右 元素，在底部
.you {
	align-self:flex-end;
}
```

![align-self](https://i.loli.net/2021/10/09/eXoJMHfayB6qAY4.png)

#### order

order 属性定义项目的排列顺序,数值越小，排列越靠前，**默认值为0**。


设置`中`元素在第一个位置

```css
.zhong {
	order:-1;  //0 &gt; -1 
}
```

![order](https://i.loli.net/2021/10/09/xzeqv2lV69jBwmr.png)</content:encoded><author>Asuhe</author></item><item><title>原型与原型链</title><link>https://asuhe.org/blog/e8f85541/</link><guid isPermaLink="true">https://asuhe.org/blog/e8f85541/</guid><pubDate>Wed, 20 Oct 2021 08:04:04 GMT</pubDate><content:encoded>## 什么是原型

在搞明白什么是原型之前我们首先要明白我们为什么要原型，毕竟语言的设计者不会无缘无故搞出一个完全没有用的东西，它肯定是为了解决某个问题而诞生的。那么原型它解决了什么问题呢？

我们都知道在es6之前，js是没有关键字`class`创建类的。那时候我们要使用类就用构造函数这种形式来实现它。在C++这种原生支持类的语言中，同一个类的多个实例里的方法只有一份，也就是说不管我们实例化了多少个对象，在计算机的内存中这个类的方法在内存中只有一份。只有每个实例的属性才会在内存中产生多个副本。例如

```c++
class foo{
	private:
	string name;
	int age;
	public:
	string getName(){ return this.name; };
	int getAge(){ return this.age; };
}

int main(){
    foo a = new foo();
    foo b = new foo();
    return 0;
}
```

在计算机的内存中，a、b这两个实例化对象的内存分布仅有private里的属性才有独立的内存副本，而public里的方法a、b是共享同一片内存的，这样设计就节约了内存空间。

![C++的类方法只占一份内存](https://i.loli.net/2021/10/20/2PxV6KqCkLseEaQ.png)



而在es6以前没有`class`时候，我们通过构造函数实例化的类就会出现上图的第一种情况。每个实例的方法都有独有的内存空间。为了解决这个问题，所以就出现了原型。构造函数通过原型分配的函数是所有对象所共享的，也就实现了第二种内存分布。

## 如何使用原型

JavaScript 规定，每一个构造函数都有一个prototype 属性，指向prototype对象。注意这个prototype就是一个对象，这个对象的所有属性和方法，都会被构造函数所拥有。

我们可以把那些不变的方法，直接定义在 prototype 对象上，这样所有对象的实例就可以共享这些方法。

```javascript
function Person(uname,age){
    this.name = uname;
    this.age = age;
    //定义类共享的方法时我们将方法定义在原型上，而不是构造函数内部
}
Person.prototype.say = function(){ console.log(&apos;哈喽&apos;); };  //这样定义方法，多个实例对象就可以共享
var man = new Person(&apos;Asuhe&apos;,18);
var woman = new Person(&apos;Asuka&apos;,16);
man.say();  //输出 哈喽
woman.say(); //输出 哈喽
```

## 对象原型

在构造函数拥有一个原型对象叫prototype，**这个原型对象由构造函数内的prototype属性指明**，每个构造函数都有一个这样的原型对象。经过上述学习我们知道共享的方法是定义在构造函数的原型prototype中的，但是我们的实例化对象却能使用定义在构造函数里prototype上的方法。这是如何实现的呢？

实现这个机制的就是我们即将要讲的**对象原型**。在每个实例化的对象中，都会包含一个**属性**`__proto__`。这个`__protto__`属性就是令我们的实例化对象能够调用构造函数里`prototype`对象里定义的方法的原因，我们同时也称这个属性为对象原型。`__proto__`属性指向的是我们的构造函数的`prototype`原型对象，如上面的`man`当我们使用实例化的对象调用`prototype`里的方法时，我们的调用链是：`man` -&gt; `__proto__`  -&gt; `prototype` -&gt; `say()`。

经过这一层调用，我们的构造函数与实例化对象之间就形成了如下三角关系

![image-20211020094410424](https://i.loli.net/2021/10/20/We5yfcVqx2bsZGk.png)

## constructor构造函数

不管我们构造函数里的原型对象`Prototype`，还是我们实例化的对象里的对象原型`__proto__`，它们都包含了一个**属性**`constructor`。`constructor`属性的作用就是指明我们引用的构造函数是哪个。例如我们的`man、woman`它们都是通过构造函数`Person`创建出来的，所以它们的对象原型里的`construtctor`属性指向的就应该是`Person`这个构造函数。按这个指向顺序我们属性指向的关系应该是如下

![直觉上对象原型里constructor的指向](https://i.loli.net/2021/10/20/TKE9jX5aDgdJfsW.png)

但事实情况是不是这样的呢？实际上对象原型里的constructor指向是通过构造函数里的`prototype.constructor`间接指回构造函数的。

![20211020100310.png](https://i.loli.net/2021/10/20/2CUPlsKLuvbJtGp.png)

一般情况下，对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法，我们可以给原型对象采取对象形式赋值，但是这样就会覆盖构造函数原型对象原来的内容，这样修改后的原型对象 constructor  就不再指向当前构造函数了。此时，我们可以在修改后的原型对象中，添加一个 constructor 指向原来的构造函数。

```javascript
Person.prototype = {
	say:function(){ console.log(&apos;哈喽&apos;); }
}
//如果我们采用上述赋值的形式给原型对象添加新的方法，这意味着原本的那个原型对象被我们覆盖了。此时constructor指向的并不是Person

//所以我们需要手动指定constructor的指向，让它重新指向Person
Person.prototype = {
    constructor:Person,
	say:function(){ console.log(&apos;哈喽&apos;); }
}
```



## 原型链

当我们继续打印出`prototype`这个对象的时候，我们可以看到`prototype`里面和已经实例化的对象一样里面也有一个`__proto__`对象原型。那么这个`__proto__`又指向哪里呢。实际上这个`__proto__`指向的是js内置的空对象`Object`的`prototype`。这个`Objerct`构造函数的对象原型里的`__proto__`还会继续往下指向最后的`null`。所以当我们查找方法时就会延着这条路径链式查找下去返回最先查找到的方法，若最后没找到则返回null。

```javascript
function Person(uname,age){
    this.name = uname;
    this.age = age;
}
//Person.prototype.__proto__指向的是Object的prototype，Object.prototype.__proto__指向null
console.log(Person.prototype.__proto__);
console.log(Object.prototype.__proto__);
```



![输出结果](https://i.loli.net/2021/10/20/Q4hrRYLdgxqDAGP.png)



![实例对象查找共享方法的链式结构](https://i.loli.net/2021/10/20/D1tKv4GVaNwAUu8.png)

```javascript
function Person(uname,age){
    this.name = uname;
    this.age = age;
    
}
Person.prototype.say = function(){ console.log(&apos;哈喽&apos;); };  
var man = new Person(&apos;Asuhe&apos;,18);
var woman = new Person(&apos;Asuka&apos;,16);
man.say();  //输出 哈喽  //在Person.prototype中找到了say方法，停止查找
woman.sleep(); //输出 TypeError //Person.prototype -&gt; Object.prototype中均未找到sleep方法，返回null
```

像上面这种`__proto__`层层查找构成的链式结构就是我们常说的原型链。

还有一点需要我们注意的是，不管是构造函数里的`this` 还是构造函数的prototype里的`this`都是指向我们实例化出来的对象

```javascript

function Person(uname,age){
    this.name = uname;
    this.age = age;
}
var ptr = null;
Person.prototype.say = function(){
    console.log(&apos;哈喽&apos;);
    ptr = this;
}
var man = new Person(&apos;Asuhe&apos;,18);
console.log(man === ptr) //输出 true
```</content:encoded><author>Asuhe</author></item><item><title>cookie和sessionStorage &amp; localStorage</title><link>https://asuhe.org/blog/a4cf5750/</link><guid isPermaLink="true">https://asuhe.org/blog/a4cf5750/</guid><pubDate>Tue, 19 Oct 2021 08:42:28 GMT</pubDate><content:encoded>## 背景

本文主要围绕What、Why、How这三个方面谈谈sessionStorage 和 localStorage的联系与区别。

## What

sessionStorage 和 localStorage 都是BOM提供给我们在浏览器上进行本地存储数据的API，利用它我们可以实现在本地浏览器存储一些数据。这两个方法都是隶属于 Window这个顶级BOM下的，在需要的时候我们直接调用即可。

## Why

在某些场合下我们希望能够在用户本地的浏览器上存储一些数据以提高用户体验，所以本地存储技术就应运而生了。典型的应用场景就是我们在登录网站时希望下次登录网站能够记住我们的账号，以免再重新输入。这种时候就可以使用localStorage进行一个本地存储用户名，在用户加载完页面的时候就检测用户浏览器上有无存储相应账号数据，若有则自动填入。

## How

在使用方式上sessionStorage和localStorage是类似的

### window.sessionStorage

存储数据：

```javascript
sessionStorage.setItem(key, value)
```

获取数据：

```javascript
sessionStorage.getItem(key)
```

删除数据：

```javascript
sessionStorage.removeItem(key)
```

清空数据：(所有都清除掉)

```javascript
sessionStorage.clear()
```

### window.localStorage

存储数据：

```javascript
localStorage.setItem(key, value)
```

获取数据：

```javascript
localStorage.getItem(key)
```

删除数据：

```javascript
localStorage.removeItem(key)
```

清空数据：(所有都清除掉)

```javascript
localStorage.clear()
```



## cookie和sessionStorage &amp; localStorage的特点

### cookie

1. 存储容量约为4KB
2. 同源页面可以共享
3. 有`path`的概念，可以将cookie限制在某个路径下
4. 会随着http头发送给服务端

### sessionStorage

1. 存储容量约为5MB
2. 同一个页面下可以共享数据
3. 生命周期到页面关闭。当页面关闭后存储在浏览器上数据就会被清除，不会保留下来下次打开页面就没有数据了
4. 存在本地端，不会发送给服务端

### localStorage

1. 存储容量约为5MB
2. 同源页面共享数据，可以跨页面共享数据
3. 生命周期为永久，仅当我们手动删除时才会清除数据，否则下次打开页面或者浏览器数据依然存在
4. 存在本地端，不会发送给服务端

### sessionStorage &amp; localStorage 共同点

1. 数据都存储在用户的浏览器中
2. 数据都以键值对的形式存储
3. 仅能存取字符串，可以将对象JSON.stringify() 编码后存储
4. 读取方便，刷新页面也不会丢失数据

### localStorage &amp; cookie共同点

1. 同源页面可以共享数据</content:encoded><author>Asuhe</author></item><item><title>关于Javascript的作用域</title><link>https://asuhe.org/blog/b3d97852/</link><guid isPermaLink="true">https://asuhe.org/blog/b3d97852/</guid><pubDate>Wed, 13 Oct 2021 17:53:03 GMT</pubDate><content:encoded>## 函数作用域

js里若我们包装函数的声明，则该函数会被当做一个**函数表达式**，而不是一个函数声明。如何理解?

```javascript
var a = 2;
(function foo(){
	var a = 3;
	console.log(a); 
})(); //输出3
console.log(a); // 输出2

//当我们继续想要调用foo
foo(); //ReferenceError

---------
//foo函数的书写还有一个改进的形式
(function foo(){
	var a = 3;
	console.log(a); //输出3
}())  //即将()调用也包含进外层的()中，书写形式变了但其功能含义与上面的写法是一致的
```

上述例子中，foo函数的作用域仅限于`{...}`即第五行，foo函数不能再在后续代码被调用。这种情况就是被包装函数声明foo，被编译器当作一个函数表达式而不是一个标准的函数声明。

`(function foo(){...})`作为函数表达式意味着，foo只能在`{...}`所代表的位置中被访问，外部作用域无法访问。但是若是有一个变量去接收该函数表达式的返回值，则该函数依然可以被继续保留调用

```javascript
//最常见的函数表达式

var b = function foo(){
    console.log(20);
}

//
var n = (function foo(){
	var a = 3;
	console.log(a); 
});
n();//输出3
```





## 变量作用域

在js中只有全局作用域和函数作用域这两个基本的单位，**块级作用域在js中是不存在的**。如何理解呢，请看如下代码

```javascript
if(true){
	var i = 10;
	console.log(i);  //此时输出 10
}
console.log(i) //输出 ?     并不是输出 ReferenceError 而是输出 10
```

在C++或Java中，i 这个变量其作用域仅在 {...} 中，在 {...} 外调用变量 i 是无法通过编译的，会产生一个报错。而在 js 中，如果你用 `var`关键字声明的变量不在一个函数中，则其作用域会为上级外部作用域。这就导致了如果我们在后续代码中同样声明一样的变量，使用该变量时就会产生预料外的结果。

很多时候我们希望变量仅在块级作用域里生效，用完以后就被GC回收。例如

```javascript
for(var i = 0;i&lt;10;i++)  //在这个循环里我们通常希望 i 在for循环执行完之后就失效，这一我们就可以在后续代码中重复定义使用 i
{						//但是实际上 变量 i for循环完以后并不会失效，而是继续作用在全局作用域中
	console.log(i);
}
console.log(i) //输出 10
--------
上述代码等效于
var i;  // i的作用域不是仅限于 for 循环内
for(i=0;i&lt;10;i++)
{
	console.log(i);
}
```

为了解决这个问题，es6中引入了 `let`关键字进行解决。我们使用`let`关键字对其进行改进，让 i 的作用域限定在for循环内

```javascript
for(let i = 0;i&lt;10;i++)  
{						
	console.log(i);
}
console.log(i) //此时输出 ReferenceError
```



### let

`let`的作用域会与其最近的{...}绑定，也就是说js在编译`let`关键字声明的变量时，会将该变量的作用域限定在包含该变量的{...}内。举个例子

```javascript
if(true){
	let i = 10;
	console.log(i);  //输出 10
}
console.log(i)  //输出ReferenceError

变量 i 的作用域被限定在if结构的代码块内

------
如果我们用var声明
if(true){
	var i = 10;
	console.log(i);  //输出 10
}
console.log(i);  //依然输出 10

上述代码等效于
var i;
if(true){
	i = 10;
	console.log(i);
}
console.log(i);
```

这就是`var`关键字与`let`关键字的区别。`var`关键字定义的变量如果不是被包裹在函数体中，那么它就可以被外部作用域所访问。而`let`关键字声明的变量会将作用域绑定在最近的{...}包裹的块级作用域中。做个小测验

```javascript
if(true){
	{
		let i = 10;
		console.log(i);
	}
	console.log(i);
}
该程序会输出啥？
会是 10 10 吗？


----------
if(true){
	{
		var i = 10;
		console.log(i);
	}
	console.log(i);
}
console.log(i);
该程序的输出是?


```

第一题的答案为: 10  ReferenceError，第二题的答案为：10 10 10。这个例子可以很形象的说明`let`关键字的特性。

### const

`const`关键字的特性与`let`一样，都是将变量的作用域绑定在最近{...}中，唯一的不同就是当我们在其作用域中不允许修改其变量值，被`const`定义的变量都为常量，若想修改则会产生 TypeError.

```javascript
{
	const i = 10;
	i = 20; //TypeError
}
console.log(i) //ReferenceError
```



## 提升

我们都知道函数和变量若都声明在调用后面，js依然会通过编译。因为函数和变量的声明会被提升至所在作用域顶部，而赋值却留在原地。**若代码中出现多个重复的声明，函数提升会先于变量提升。而且若有多个重复的函数声明，则以最后一个为准**

```javascript
foo(); // 30

var foo;

function foo(){
	console.log(10);
}

foo = function(){
	console.log(20);
}

function foo(){
    console.log(30);
}

为什么foo();输出的是30?
//若再调用一次foo()
foo(); // 输出 ?            输出20
    
----------
//在编译器眼中上述代码的执行顺序

function foo(){
    console.log(30);
}
foo();

foo = function(){
    console.log(20);
}
```

**普通块内的函数声明通常会被提升至顶级作用域的顶部，且这个过程不可被条件判断控制**

```javascript
foo(); //&apos;asuhe&apos;

if(true){
	function foo(){
		console.log(&apos;ashitahe&apos;);
	}
}else{
	function foo(){
		console.log(&apos;asuhe&apos;);
	}
}
```</content:encoded><author>Asuhe</author></item><item><title>ES6新增特性</title><link>https://asuhe.org/blog/e408df6c/</link><guid isPermaLink="true">https://asuhe.org/blog/e408df6c/</guid><pubDate>Fri, 22 Oct 2021 08:14:57 GMT</pubDate><content:encoded>## let关键字

### 作用域绑定在最近的{...}块中

`let`在使用时会绑定离它最近的{...}作为作用域。原本在ES6以前是没有块级作用域的概念，在ES6引入了`let、const`关键字之后就有了块级作用域。**实际上这个块级作用域并不是我们平常理解的块级作用域，仅当变量是由`let、const`关键字声明时，这些关键字声明的变量其作用域会绑定在这个块上，若是使用`var`关键字声明，其变量依然会成为全局变量。**

使用这个特性我们可以很好的解决 for 循环全局变量污染的问题

```javascript
if(true){
	{
		let a = 10;
		var b = 20;
		console.log(a); // 10
	}
	console.log(a); // ReferenceError
}
console.log(b); // 20
```

### 无变量提升

`let`关键字在使用的时候，不像`var`那样存在变量提升的情况。它必须先声明再使用。

```javascript
function fn(){
	console.log(a); // undefined
	console.log(b); //ReferenceError
	var a = 10;
	let b = 20;
}
fn();
//上述代码等价于
function fn(){
	var a;  //var变量提升至所在作用域顶部
	console.log(a); // undefined
	console.log(b); //ReferenceError
	a = 10;
	let b = 20;
}
fn();
```

### 暂时性死区

所谓暂时性死区就是，你在块级作用域使用`let`声明了一个变量`i`，同时你在外部作用域也声明的一个同名变量`i`。若你在`let`声明这个变量前使用它，它并不会像以前的那样去上级作用域寻找这个变量，仅会在该作用域内寻找变量，又因为变量使用`let`声明，并且声明在其使用之后，就会抛出ReferenceError。**因为使用`let`在作用域内声明了同名变量，它会屏蔽外界变量，这个块级区域就被称为暂时性死区**。

```javascript
var a = 20;
//if{}内形成暂时性死区
if(true){
	console.log(a); // ReferenceError
	let a = 10;
}
-------------
var b = 20;
//不是同名变量，没有暂时性死区
if(true){
	console.log(b); // 20
	let a = 10;
}
```



## const

### 声明时必须赋值

`const`关键字的特性和`let`相同，`let`有的特性`const`都具有。不同点是**它在声明的同时必须赋予一个值**，否则抛出SyntaxError。

```javascript
const a;  // SyntaxError
```

### 赋值后不可改变

这里`const`关键字声明的变量能否更改还需要看具体的情况。

若`const`声明的是一个基本数据类型，则不能更改其值

```javascript
const a = 10;
a = 20; // TypeError
```

但是如果`const`声明的是一个复杂数据类型，则可以更改复杂数据类型变量内部的值，不可更改该复杂数据类型变量本身

```javascript
const b = [1,2,3];
b[1] = 100; 
console.log(b); // [1,100,3]
b = [3,4,5]; //TypeError
```



## 箭头函数

最初使用箭头函数的目的就是为了简化函数声明的操作，但它的作用不仅如此。我们都知道`this`指针的指向在不同地方其指向不同，例如在构造函数中其指向就是它的实例化对象，在`class`中的`constructor`中也是同样的指向自身的实例化对象，而在普通函数中其指向的是函数的调用者。

### 箭头函数中的`this`是根据上下文环境确定的

箭头函数`this`指向的是**被声明的作用域里面**

```javascript
let btn = document.querSelector(&apos;.btn&apos;);
btn.onclick = function(){
	setTimeout(function(){
		console.log(this); // window
	},1000);
}
------------
//在定时器的回调函数中使用箭头函数则其this会根据上下文指向btn对象，而不是指向window
btn.onclick = function(){
	setTimeout(()=&gt;{
		console.log(this); // btn
	},1000);
}
```

### 多种简写形式

当形参只有一个且函数体内仅有一句返回值时

```javascript
let sum = n =&gt; n*3;

//等价一
let sum = (n) =&gt; {
    return n*3;
}
//等价二
let sum = function(n){
    return n*3;
}
```

函数体中只有一句代码，且代码的执行结果就是返回值，可以省略大括号

```javascript
let sum = (a,b) =&gt; a+b;

//等价一
let sum = (a,b) =&gt; {
	return a + b;
}
//等价二
let sum = function(a,b){
    return a + b;
}
```

## 解构

### 数组的解构

在es6中我们可以使用新的方式来命名多个变量，这种新的声明赋值方式就叫解构

```javascript
//传统命名多个变量并赋值的方式
let a = 10,b = 20,c = 30;

//采用数组解构的方式
let arr = [10,20,30];
let [a,b,c] = arr;  //a = 10,b = 20,c = 30

//当变量数量大于数组长度
let arr = [1,2,3];
let [a,b,c,d] = arr; //a = 1,b = 2, c = 3,d = undefined //多余的变量为未定义

//当变量数量少于数组长度
let arr = [1,2,3];
let [a,b] = arr; //a = 1,b = 2  //多余的直接被截断

--------------
//对象解构
let obj = {
    name:&apos;Asuhe&apos;,
    age: 18,
    sex: &apos;男&apos;
}
let {name:myname,age:myage,sex:mysex} = obj; //myname = &apos;Asuhe&apos;,myage = 18,mysex = &apos;男&apos; //按匹配相同名称，即使乱序也会匹配上
//等价于
let myname = &apos;Asuhe&apos;;
let myage = 18;
let sex = &apos;男&apos;;

----------------
let {name,age,sex} = obj;

等价于
let name = &apos;Asuhe&apos;;
let age = 18;
let sex = &apos;男&apos;;
```

## 剩余参数

在es6中新扩展了一种方法可以使函数的形参接收不定数量的实参，那就是使用**剩余参数**。我们知道在以前js同样支持函数形参和实参数量不匹配，即使定义函数时形参列表为空我们依然可以使用`arguments`对象获取到实参。**但在es6新增的箭头函数中并不支持`arguments`，所以es6扩展了`...args`剩余参数来解决这个问题。**

```javascript
//传统获取所有形参
function foo(){
	console.log(arguments[0]); //1
    console.log(arguments[1]); //2
    console.log(arguments[2]); //3
}
foo(1,2,3);

//使用剩余参数
function foo(a,...args){ //剩余参数必须放在最后一个形参的位置
    console.log(a); //1
    console.log(args); //[2,3,4,5] //以数组的形式存储值
}
foo(1,2,3,4,5);
```</content:encoded><author>Asuhe</author></item><item><title>Javascript的坑</title><link>https://asuhe.org/blog/3e90b646/</link><guid isPermaLink="true">https://asuhe.org/blog/3e90b646/</guid><pubDate>Sun, 10 Oct 2021 20:31:38 GMT</pubDate><content:encoded>## 坑一：变量提升

js 在解析时，会将所有的变量与函数先声明再进行赋值操作。例如

```javascript
//先输出再声明并赋值变量
console.log(a); //输出结果为undefined
var a = 10;
```

像这类代码，在执行时等于如下效果

```javascript
var a;
console.log(a);
a = 10;
```

**这说明 js 在执行时，会将变量提至最前面先声明，再进行赋值操作。**同理，我们进行函数调用时也是如此进行函数提升

```javascript
fu1();  //在未定义fu1()时就调用它，此时我们执行时是可以输出xxx的
function  fu1(){
	console.log(&apos;xxx&apos;);
}

//但我们使用变量保存函数时，函数无法执行
fun();  //此时控制台报错

var fun = function (){
    console.log(&apos;asuhe&apos;);
}
```

## 坑二：逻辑中断

当出现`表达式1 &amp;&amp; 表达式2`时，前面的**表达式 1**返回 true 则返回**表达式 2**值，若**表达式 1**返回 false 则返回**表达式 1**的值

```javascript
var a = 5 &gt; 3 &amp;&amp; 10;
console.log(a); //输出 10

var b = 1 &gt; 3 &amp;&amp; 10;
console.log(b); //输出 false
```

当出现`表达式1 || 表达式2`时，**表达式 1**返回 true 则返回**表达式 1**的值，若**表达式 1**返回 false 则返回**表达式 2**的值

```javascript
var a = 5 &gt; 3 || 10;
console.log(a); //输出 true

var b = 1 &gt; 3 || 10;
console.log(b); //输出 10
```

## 坑三：变量声明

在 js 中我们可以不使用`var`关键字直接对一个变量名进行赋值，而且该变量还会成为一个全局变量

```javascript
function foo(){
	a = 10;
	console.log(a);
}

foo(); //输出 10
console.log(a); //输出 10
```

## 坑四：函数的形参与实参可以不匹配

js 的函数在定义时可以不书写形参，但是在调用该函数时，我们却可以传入实参。传入的实参会被一个`argument`对象保存，在函数内部可以使用该对象获取传入实参的值

```javascript
function foo(){
	console.log(arguments[0]); //输出 10
    console.log(arguments[1]); //输出 20
    console.log(arguments[2]); //输出 undefined
}
foo(10,20);

//形参多于实参时，未匹配的形参值为undefined
function foo(a,b){
	console.log(a); //输出 10
    console.log(b); //输出 undefined
}
foo(10);
//形参少于实参时，多余的实参值不会赋值给形参，但仍然保存在arguments中
function foo(a){
	console.log(a); //输出 10
    console.log(arguments[1]); //输出 20
}
foo(10,20);
```

`arugments`的内部结构

```javascript
function foo(){
	console.log(arguments); //输出 arguments结构
}
foo(10,20);
```

![arguments中保存的东西](https://i.loli.net/2021/10/10/ET3LHMgmd8rVUD2.png)

## 坑五：内置 Array.sort 方法里的比较默认是按第一位大小比较的

在 js 的内置 Array.sort 方法里，大小默认是为按第一位的大小比较的

```javascript
var a = [1,8,31,40,9,10,2,16];
a.sort();
for (let index = 0; index &lt; a.length; index++) {
    console.log(a[index]);
}
```

![数组按第一位的大小比较](https://i.loli.net/2021/10/10/YJzjXpPocntudgw.png)

若要变成正确的升序办法要传入特定的函数

```javascript
var a = [1,8,31,40,9,10,2,16];
a.sort(function(a,b){ return a - b;}); //a - b是升序，固定写法
for (let index = 0; index &lt; a.length; index++) {
    console.log(a[index]);
}
```

![正确的升序](https://i.loli.net/2021/10/10/OTUrlYLp5KMRs1x.png)

降序

```javascript
var a = [1,8,31,40,9,10,2,16];
a.sort(function(a,b){ return b - a;}); //b - a是降序，固定写法
for (let index = 0; index &lt; a.length; index++) {
    console.log(a[index]);
}
```

![降序](https://i.loli.net/2021/10/10/qXFoMdr1c72ipgk.png)

## 坑六：所谓的构造函数

js 里的所谓的构造函数**实际上就是一个自定义的用来创建对象的普通函数**，只是因为它的功能是用来创建对象所以才叫构造函数。本质上与普通函数并没有区别，不是 C++或 Java 里构造函数的概念。**这也就是说，js 本质上不是面向对象的，只是实现了类似面向对象的功能**

```javascript
function Person(name,age,sex){
    this.name = name; //this指针写出成员赋值
    this.age = age;
    this.sex = sex;
    this.getName = function(){ return this.name;}
}
var obj = new Person(&apos;Asuhe&apos;,18,&apos;保密&apos;); //创建对象时还是要使用 new 关键字
console.log(obj.getName()); //输出 Asuhe
console.log(obj); //输出obj的结构
```

![构造函数](https://i.loli.net/2021/10/10/OhNTdpIZF31sinP.png)

## 坑七：作用域链

因为变量提升导致我们在调用一个在后面定义并赋值的变量时，会先输出这个变量未赋值的状态。作用域链就是，当我们发现在当前作用域寻找不到变量声明时，就会往上一级作用域去寻找，**找到为止**。若在全局作用域中都未寻找到该变量声明，则报错。

```javascript
function foo(){
    console.log(a);
    var a = 10; //变量提升
}
foo();  //此时输出undefined


----------

function foo(){
    console.log(a);
}
foo();  //此时依然输出undefined
var a = 20; //变量提升

----------
var a = 20;
function foo(){
    console.log(a);
    var a = 10; //变量提升
}
foo(); //依然输出undefined , 因为在foo函数内部作用域中已经找到a的声明，所以不会继续往外查找

```

## 坑八：类没有变量提升

前面我们讲到对于函数有函数提升，变量有变量提升，我们可以先使用后声明不会报错。而 es6 中引入的 class 关键字就没有类似的提升机制。我们只能先把 class 的定义放在使用它之前。

```javascript
var obj = new Man();  //此行报错,不能先于Man类的定义使用
class Man{
	constructor(){	};
	say(){
		console.log(&apos;hello world!&apos;);
	}
}
```

## 坑九：类里面的 this 指向

在 es6 中引入了 class 关键字，用于实现类似面向对象的功能。但是这个 class 里的 this 指针的指向并不是总是指向实例化对象本身。在 constructor 函数中，this 指针是一直指向实例化对象本身的。而在这个**class 的方法中，this 指针指向的是调用该方法的对象**，有时候调用该方法的并不是该实例化对象所以 this 指向会发生改变。

```html
&lt;button&gt;点击&lt;/button&gt; //点击该按钮输出 undefined
&lt;script&gt;
	class Person {
		constructor(name,word){
            this.name = name;
            this.word = word;
            this.btn = document.querySelector(&apos;button&apos;); //绑定至btn
            this.btn.onclick = this.Say; //button 调用Say方法
        }
        getName(){
            console.log(this.name);
        }
        Say(){
            console.log(this.word);
        }
	}
	var asuhe = new Person(&apos;Asuhe&apos;,&apos;up&apos;);
	asuhe.getName(); //输出 Asuhe
	asuhe.Say(); //输出 up
&lt;/script&gt;
```

## 坑十：supre 关键字在子类 constructor 中的使用

我们都知道 supre 不仅可以调用父类的 constructor ，还可以用于调用父类的普通成员函数。

子类**在 constructor 中使用 super 关键字, 必须放到 this 指针 前面** ，但在普通成员函数中无此规则 (必须先调用父类的构造方法,在使用子类构造方法)

```javascript
class Father {
	constructor(x,y){
		this.x = x;
		this.y = y;
	}
	sum(){
		console.log(this.x + this.y);
	}
}
class Son extends Father {
	constructor(x,y){
		this.x = x;
		this.y = y;
		super(x,y); //此行报错，super在constructor中必须先于this。super.sum();同样报错
	}
	subtract(){
		console.log(this.x - this.y);
	}
}
var obj = new Son(5,3);
```</content:encoded><author>Asuhe</author></item><item><title>http协议</title><link>https://asuhe.org/blog/54593785/</link><guid isPermaLink="true">https://asuhe.org/blog/54593785/</guid><pubDate>Thu, 28 Oct 2021 08:43:22 GMT</pubDate><content:encoded>## 什么是HTTP协议

**HTTP 协议**即超文本传送协议 (HyperText Transfer Protocol) ，使用经典的[C/S模式](https://en.wikipedia.org/wiki/Client%E2%80%93server_model)交互，它规定了客户端与服务器之间进行网页内容传输时，所必须遵守的传输格式。

在[TCP/IP网络模型](https://en.wikipedia.org/wiki/Internet_protocol_suite)中，它位于[应用层](https://en.wikipedia.org/wiki/Application_layer)采用传输层采用的是[TCP](https://en.wikipedia.org/wiki/Transmission_Control_Protocol)协议，端口号默认是**80**。虽然它采用的是TCP协议，但它实际上是一个**无连接**协议，这里的无连接是指服务器每次处理完客户端请求并收到客户端的响应回答后就会马上断开连接，并不会一直维持会话连接状态。同时它是一个[无状态协议](https://en.wikipedia.org/wiki/Stateless_protocol)，即它并不会记录前后报文之间的关系。每一个请求报文在它看来都是独立的，没有上下文联系。这就给我们实际运用的时候带来了一些麻烦，例如在购物网站购买商品时，我们商品加入自己账号的购物车这就需要上下文联系。为了解决这个问题于是就有了{% post_link 2021-11-02-身份认证 &apos;cookie-session机制&apos; %}来帮助我们建立上下文联系。

## 为什么要HTTP协议

它是为 Web 浏览器与 Web 服务器之间的通信而设计的，但也可以用于其他目的，例如现在的电子邮箱流行使用http协议去传输内容。

## HTTP报文格式

http的报文可以大致分为请求报文和响应报文两种，请求报文即为客户端向服务器发送的请求消息，而响应报文则是服务器根据客户端的请求响应回去的消息。

### 请求报文

HTTP 请求消息由请求行（request line）、请求头部（ header ） 、空行 和 请求体 4 个部分组成。

![请求报文基本格式](https://i.loli.net/2021/10/28/MtukAIinQ2JCNvs.png)

### 常见的请求头部

| **头部字段**        | **说明**                                       |
| ------------------- | ---------------------------------------------- |
| Host                | 要请求的服务器域名                             |
| Connection          | 客户端与服务器的连接方式(close  或  keepalive) |
| Content-Length      | 用来描述请求体的大小                           |
| **Accept**          | 客户端可识别的响应内容类型列表                 |
| **User-Agent**      | 产生请求的浏览器类型                           |
| **Content-Type**    | 客户端告诉服务器实际发送的数据类型             |
| **Accept-Encoding** | 客户端可接收的内容压缩编码形式                 |
| **Accept-Language** | 用户期望获得的自然语言的优先顺序               |

我们可以在浏览器里具体抓一个请求报头来看看

![GET请求头](https://i.loli.net/2021/10/28/u236vDXOYrzJGZA.png)

![POST请求头](https://i.loli.net/2021/10/28/dF9bL3phfgrlaRK.png)

从上面请求头的对比中里我们可以得知一个结论。GET请求和POST请求的一个重要区别就是**GET请求方式中，我们要发送的数据会携带在请求报头中，实际的请求体是为空的。而在POST请求方式中，我们要提交的数据会携带在请求体中，并不会夹带在请求头中。**



### 响应报头

HTTP响应消息由状态行、响应头部、空行 和 响应体 4 个部分组成，如下图所示：

![响应报文基本格式](https://i.loli.net/2021/10/28/tq61CS3xo5hWckb.png)

实际浏览器中的响应报头

![GET响应报头](https://i.loli.net/2021/10/28/rk643O5sEK9ljdp.png)

![POST响应报头](https://i.loli.net/2021/10/28/2MERcynikH4O8T3.png)

### 常见响应报头

| **头部字段**         | **说明**                     |
| -------------------- | ---------------------------- |
| **Connect-Encoding** | 服务器返回数据的压缩编码格式 |
| **Content-Length**   | 用来描述响应体的大小         |
| **Content-Type**     | 服务器实际发送的数据类型     |
| Set-Cookie           | 服务端给客户端设置的cookie   |
| Date                 | 数据返回时间                 |



## http请求方法

| **序号** |  **方法**  | **描述**                                                     |
| -------- | :--------: | ------------------------------------------------------------ |
| 1        |  **GET**   | (查询)发送请求来获得服务器上的资源，请求体中不会包含请求数据，请求数据放在协议头中。 |
| 2        |  **POST**  | (新增)向服务器提交资源（例如提交表单或上传文件）。数据被包含在请求体中提交给服务器。 |
| 3        |  **PUT**   | (修改)向服务器提交资源，并使用提交的新资源，替换掉服务器对应的旧资源。 |
| 4        | **DELETE** | (删除)请求服务器删除指定的资源。                             |
| 5        |    HEAD    | HEAD  方法请求一个与 GET 请求的响应相同的响应，但没有响应体。 |
| 6        |  OPTIONS   | 获取http服务器支持的http请求方法，允许客户端查看服务器的性能，比如ajax跨域时的预检等。 |
| 7        |  CONNECT   | 建立一个到由目标资源标识的服务器的隧道。                     |
| 8        |   TRACE    | 沿着到目标资源的路径执行一个消息环回测试，主要用于测试或诊断。 |
| 9        |   PATCH    | 是对  PUT 方法的补充，用来对已知资源进行局部更新 。          |



## http响应状态码

http的状态码可以分为5大类型

| 分类 | **分类描述**                                                 |
| :--: | :----------------------------------------------------------- |
| 1**  | 信息，服务器收到请求，需要请求者继续执行操作（实际开发中很少遇到  1**  类型的状态码） |
| 2**  | 成功，操作被成功接收并处理                                   |
| 3**  | 重定向，需要进一步的操作以完成请求                           |
| 4**  | 客户端错误，请求包含语法错误或无法完成请求                   |
| 5**  | 服务器错误，服务器在处理请求的过程中发生了错误               |

### 服务器处理相关的响应状态码

| **状态码** | **状态码英文名称**  | **中文描述**                                                 |
| :--------: | :-----------------: | ------------------------------------------------------------ |
|    100     |      Continue       | 继续。客户端应继续其请求                                     |
|    101     | Switching Protocols | 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议，例如，切换到HTTP新版本 |

### 成功相关的响应状态码

**2xx 范围的状态码，表示服务器已成功接收到请求并进行处理。**常见的 2** 类型的状态码如下：

| **状态码** | **状态码英文名称** | **中文描述**                                                 |
| :--------: | :----------------: | ------------------------------------------------------------ |
|    200     |         OK         | 请求成功。一般用于  GET 与 POST  请求                        |
|    201     |      Created       | 已创建。成功请求并创建了新的资源，通常用于  POST 或 PUT  请求 |
|    206     |  Partial Content   | 返回部分内容。服务器成功处理了部分GET请求                    |

### 重定向相关的响应状态码

**3xx范围的状态码，表示表示服务器要求客户端重定向，需要客户端进一步的操作以完成资源的请求。**常见的 3** 类型的状态码如下：

| 状态码 | **状态码英文名称** | **中文描述**                                                 |
| :----: | :----------------: | ------------------------------------------------------------ |
|  301   | Moved  Permanently | 永久重定向。请求的资源已被永久的移动到新URI，返回信息会包括新的URI，浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替 |
|  302   |       Found        | 临时重定向。与301类似。但资源只是临时被移动。客户端应继续使用原有URI |
|  304   |   Not  Modified    | 未修改。所请求的资源未修改，服务器返回此状态码时，不会返回任何资源（响应消息中不包含响应体）。用于协商缓存 |

### 客户端错误相关的响应状态码

4** 范围的状态码，表示客户端的请求有非法内容，从而导致这次请求失败。常见的 4** 类型的状态码如下：

| **状态码** | **状态码英文名称** | **中文描述**                                                 |
| :--------: | :----------------: | ------------------------------------------------------------ |
|    400     |    Bad  Request    | 语义有误，当前请求无法被服务器理解。除非进行修改，否则客户端不应该重复提交这个请求。 |
|    401     |    Unauthorized    | 当前请求需要用户验证。                                       |
|    403     |     Forbidden      | 用户无权限，服务器拒绝执行它。                               |
|    404     |     Not Found      | 服务器无法根据客户端的请求找到资源（网页）。                 |
|    408     |  Request  Timeout  | 请求超时。服务器等待客户端发送的请求时间过长，超时。         |

### 服务端错误相关的响应状态码

5** 范围的状态码，表示服务器未能正常处理客户端的请求而出现意外错误。前端甩锅码，只要出现5开头的响应码全部甩锅后端哈哈。常见的 5** 类型的状态码如下：

| **状态码** |   **状态码英文名称**   | **中文描述**                                                 |
| :--------: | :--------------------: | ------------------------------------------------------------ |
|    500     | Internal  Server Error | 服务器内部错误，无法完成请求。                               |
|    501     |    Not  Implemented    | 服务器不支持该请求方法，无法完成请求。只有  GET 和 HEAD  请求方法是要求每个服务器必须支持的，其它请求方法在不支持的服务器上会返回501 |
|    503     |  Service  Unavailable  | 由于超载或系统维护，服务器暂时的无法处理客户端的请求。       |
|    504     |    Gateway Timeout     | 网关超时                                                     |

[更多状态码信息](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81)

## http缓存机制

第一次访问网站时服务端必须把所有资源都给浏览器，此时`http`会把一些资源缓存到本地浏览器这样下次访问该网站时就可以少传输一些数据能够有效地加快网站打开速度。一般来说静态资源如`js`、`css`、图谱等资源都可以被缓存，但`html`、业务数据等资源我们一般不会去缓存因为这些资源变化比较频繁我们需要时刻去获取最新的以保证正确性。

### http缓存策略

#### 强制缓存

强制缓存是指当我们在首次访问网页时，服务器返回给我们资源会在响应体里加上`Cache-Control`响应头来表示该资源可以被缓存，此时我们本地就会将该资源缓存下来。当我们第二次访问该网页时，浏览器就会根据`Cache-control`的记录尝试去本地缓存中寻找资源，若找到该资源则缓存命中不会再发送网络请求向服务器要资源。

查找缓存根据`Cache-control`的值来查找，当值为`max-age`时就会根据其数值来计算出该缓存是否过期，若资源过期则向服务器请求资源；当值为`no-cache`时说明我们不用本地缓存，浏览器直接向服务器请求资源，服务端如何处理浏览器不管；当值为`no-store`时表明我们既不用本地缓存也不理会服务器协商缓存的策略，直接要求服务器返回新资源；当值为`private`时表示我们只在终端用户缓存资源，不允许中间路由缓存资源；当值为`public`时我们允许中间路由缓存资源。

![image-20220224213049496](https://s2.loli.net/2022/02/24/pnEywNutRfIeFcD.png)



#### 协商缓存

协商缓存是服务器的一种缓存策略。当服务器接收到客户端请求资源的消息时，服务器可以做三种选择一是直接满足客户端的要求发送资源；二是比对客户端携带的资源标识根据标识来告诉客户端它的本地缓存有没有过期，若发现客户端的资源和服务器一致则告诉客户端可以直接使用本地缓存而服务器本身不会再次发送资源；三是服务器拒绝客户端的请求。

上面第二种情况就是典型的协商缓存。在协商缓存的过程中，客户端第一次访问资源服务器会在响应报文的响应头中加入`Last-Modified`字段来作为该资源的资源标识，该字段表示资源最后的修改时间。当客户端第二次请求资源时，在请求报文的请求头中加入`If-Modified-Since`字段来提供给服务器进行比对，该字段的值就是上次请求该资源时服务器设置的`Last-Modified`字段值。若服务端比对发现客户端的资源和服务器资源一致则返回`304`状态码告诉客户端它的本地缓存是可用的，若不一致则返回新的资源和`Last-Modified`。

![image-20220224213010137](https://s2.loli.net/2022/02/24/FOlY4HiK6bREPeA.png)

除了可以将`Last-Modified`作为资源标识，服务器还可以使用`Etag`作为资源的唯一标识。`Last-Modified`作为资源标识只能精确到秒级，而`Etag`可以更加精确一些因为它是根据文件内容来生成的唯一标识。若一个文件内容没有被修改，但是它一直被重复创建，这种情况下如果使用`Last-Modified`做资源标识就会导致服务器不断地发送资源给客户端即便资源本身并没有变化。如果使用`Etag`就可以避免这中情况的发生。`Last-Modified`和`Etag`并不是互斥关系，它们可以同时使用，但它们同时出现时`Etag`的优先级会更高，服务器会一`Etag`的比较结果为准。

![image-20220224213414054](https://s2.loli.net/2022/02/24/zJ2Ay5aGpWjlmZE.png)



### http缓存相关的报头

|   头部字段    | **说明**                                                     |
| :-----------: | ------------------------------------------------------------ |
| Cache-Control | 当该资源能够被缓存时，响应头里就会加上该字段以控制强制缓存的逻辑 |
| Last-Modified | 资源最后修改时间，用作资源标识。在协商缓存中判断资源是否更新 |
|     Etag      | 资源唯一标识。作用同Last-Modified                            |

### 刷新操作对缓存的影响

页面的刷新可以分为正常刷新（页面的前进后退、地址栏输入URL、链接跳转等）、手动刷新（F5、点击刷新按钮、右键菜单刷新等）、强制刷新（Ctrl + F5等）。当我们正常刷新时强制缓存和协商缓存都会正常生效，但当我们使用手动刷新时强制缓存会失效，仅协商缓存会生效。若我们使用强制刷新则强制缓存和协商缓存都会失效。

[更多http缓存信息](https://zh.wikipedia.org/wiki/Web%E7%BC%93%E5%AD%98)

## http协议演进历史

### http 0.9

- HTTP 0.9 是最早发布出来的一个版本，于1991年发布。

- 它只接受 GET 一种请求方法，没有在通讯中指定版本号，且不支持请求头。由于该版本不支持 POST 方法，因此客户端无法向服务器传递太多信息。

- HTTP 0.9 具有典型的无状态性，每个事务独立进行处理，事务结束时就释放这个连接。HTTP 协议的无状态特点在其第一个版本中已经成型。

### http 1.0

HTTP 1.0是HTTP协议的第二个版本，于1996年发布，如今仍然被广泛使用，尤其是在代理服务器中。

这是第一个在通讯中指定版本号的HTTP协议版本，具有以下特点：

- 不仅仅支持 GET 命令，还支持 POST 和 HEAD 等请求方法。

- HTTP 的请求和回应格式也发生了变化，除了要传输的数据之外，每次通信都包含头信息，用来描述一些信息。

- 不再局限于 0.9 版本的纯文本格式

  根据头信息中的 Content-Type 属性，可以支持多种数据格式，这使得互联网不仅仅可以用来传输文字，还可以传输图像、音频、视频等二进制文件。

- 开始支持cache，就是当客户端在规定时间内访问同一网站，直接访问cache即可。

- 其他的新增功能还包括状态码（status code）、多字符集支持、多部分发送（multi-part type）、权限（authorization）、缓存（cache）、内容编码（content encoding）等。

1.0 版本的工作方式是**每次 TCP 连接只能发送一个请求，当服务器响应后就会关闭这次连接，下一个请求需要再次建立 TCP 连接。** TCP 连接的建立成本很高，因为需要客户端和服务器三次握手，并且开始时发送速率较慢（slow start）。

HTTP 1.0 版本的性能比较差。随着网页加载的外部资源越来越多，这个问题就愈发突出了。为了解决这个问题，有些浏览器在请求时，即在请求头部加上 Connection 字段：

![image-20220302164936722](https://s2.loli.net/2022/03/02/sCrbSE1p8w2fFlX.png)

这个字段要求服务器不要关闭TCP连接，以便其他请求复用。服务器同样回应这个字段。

一个可以复用的TCP连接就建立了，直到客户端或服务器主动关闭连接。但是，这不是一个标准字段，不同实现的行为可能不一致，因此不是根本的解决办法。

### http 1.1

- 默认采用持续连接（Connection: keep-alive）
- **还支持以管道方式在同时发送多个请求**，以便降低线路负载，提高传输速度。**但是服务器响应请求并不能并发，只能串行发出，等上一个请求响应结束以后才能发出第二个请求的响应**

- 分块传输编码。使用 `Content-Length` 字段的前提条件是，服务器发送回应之前，必须知道回应的数据长度。对于一些很耗时的动态操作来说，这意味着，服务器要等到所有操作完成，才能发送数据，显然这样的效率不高。更好的处理方法是，产生一块数据，就发送一块，采用&quot;流模式&quot;（stream）取代&quot;缓存模式&quot;（buffer）。

因此，HTTP 1.1 版本规定可以不使用 `Content-Length` 字段，而使用&quot;分块传输编码&quot;（chunked transfer encoding）。只要请求或回应的头信息有 `Transfer-Encoding` 字段，就表明回应将由数量未定的数据块组成。

- 新增了请求方式 PUT、PATCH、OPTIONS、DELETE 等。

- 客户端请求的头信息新增了 Host 字段，用来指定服务器的域名。

HTTP 1.1 支持文件断点续传，RANGE:bytes，HTTP 1.0 每次传送文件都是从文件头开始，即 0 字节处开始。RANGE:bytes=XXXX 表示要求服务器从文件 XXXX 字节处开始传送，**断点续传。即返回码是 206（Partial Content）**

在Chrome中最多支持同时发出6个持久连接的http请求，这就限制了资源加载的速度，所以为了解决这个问题又推出了http 2.0

### http 2.0

- 二进制协议：HTTP 1.1 版的头信息肯定是文本（ASCII 编码），数据体可以是文本，也可以是二进制。HTTP 2.0 ：则是一个彻底的二进制协议，头信息和数据体都是二进制，并且统帧（frame）：头信息帧和数据帧。

- 多工：HTTP 2.0 复用 TCP 连接，在一个连接里，客户端和浏览器都可以同时发送多个请求或回应，而且不用按照顺序一一对应，这样就避免了&quot;队头堵塞&quot;（**HTTP 2.0 使用了多路复用的技术，做到同一个连接并发处理多个请求**，而且并发请求的数量比 HTTP 1.1大了好几个数量级）。

举例来说，在一个 TCP 连接里面，服务器同时收到了 A 请求和 B 请求，于是先回应 A 请求，结果发现处理过程非常耗时，于是就发送 A 请求已经处理好的部分， 接着回应 B 请求，完成后，再发送 A 请求剩下的部分。

- 头信息压缩：HTTP 协议不带有状态，每次请求都必须附上所有信息。所以，请求的很多字段都是重复的，比如 `Cookie` 和 `User Agent`，一模一样的内容，每次请求都必须附带，这会浪费很多带宽，也影响速度。HTTP 2.0 对这一点做了优化，引入了头信息压缩机制（header compression）。一方面，头信息使用 gzip 或c ompress 压缩后再发送；另一方面，客户端和服务器同时维护一张头信息表，所有字段都会存入这个表，生成一个索引号，以后就不发送同样字段了，只发送索引号，这样就提高速度了。

- 服务器主动推送：HTTP 2.0 允许服务器未经请求，主动向客户端发送资源，这叫做服务器推送（server push）。意思是说，当我们对支持 HTTP 2.0 的 web server 请求数据的时候，服务器会顺便把一些客户端需要的资源一起推送到客户端，免得客户端再次创建连接发送请求到服务器端获取。这种方式非常合适加载静态资源。 服务器端推送的这些资源其实存在客户端的某处地方，客户端直接从本地加载这些资源就可以了，不用走网络，速度自然是快很多的。</content:encoded><author>Asuhe</author></item><item><title>同源与跨域</title><link>https://asuhe.org/blog/bf54b5a0/</link><guid isPermaLink="true">https://asuhe.org/blog/bf54b5a0/</guid><pubDate>Fri, 29 Oct 2021 16:05:28 GMT</pubDate><content:encoded>## 域

在了解什么是同源和跨域之前我们首先要明确在浏览器中什么是域。浏览器中由协议名、服务器地址、端口号组成一个域。![默认网站的端口为80可以省略](https://i.loli.net/2021/10/29/lLjnOkM3rZ6Q4m2.png)

**同源**指的是两个 URL 的协议、域名、端口一致，反之，则是**跨域**。

出现跨域的根本原因：**浏览器的同源策略**不允许非同源的 URL 之间进行资源的交互。

网页访问资源时，只要 url 的协议名、服务器地址、端口号中任何一个发生了改变就会产生跨域问题。

## 同源策略

为了确保用户的上网安全，浏览器采用了同源策略来防止入侵。如果两个页面的协议，域名和端口都相同，则两个页面具有**相同的源**。。同源策略就是浏览器规定网页请求数据只能在同源站点中请求，不能跨域请求资源。浏览器规定，A 网站的 JavaScript，不允许和非同源的网站 C 之间，进行资源的交互。虽然我们的网络请求可以正常发送，服务器也会正常响应资源，但到浏览器这层资源就会被拦截下来，我们的 js 代码无法得到数据。例如：

①无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB

②无法接触非同源网页的 DOM

③无法向非同源地址发送 Ajax 请求

![数据被拦截](https://i.loli.net/2021/10/29/nFq7L2iQuZzObN8.png)



## 跨域解决方法

现如今，实现跨域数据请求，最主要的两种解决方案，分别是 JSONP 和 CORS。

JSONP：出现的早，兼容性好（兼容低版本IE）。是前端程序员为了解决跨域问题，被迫想出来的一种临时解决方案。缺点是只支持 GET 请求，不支持 POST 请求。

CORS：出现的较晚，它是 W3C 标准，属于跨域 Ajax 请求的根本解决方案。支持 GET 和 POST 请求。缺点是不兼容某些低版本的浏览器。

### JSONP

JSONP (JSON with Padding) 是 JSON 的一种“使用模式”，可用于解决主流浏览器的跨域数据访问的问题。

由于浏览器同源策略的限制，网页中无法通过 Ajax 请求非同源的接口数据。但是 \&lt;script\&gt;标签不受浏览器同源策略的影响，可以通过 src 属性，请求非同源的 js 脚本。

因此，JSONP 的实现原理，就是通过 \&lt;script\&gt; 标签的 src 属性，请求跨域的数据接口，并通过**函数调用**的形式，接收跨域接口响应回来的数据。

```html
//在这个标签页面定义函数
&lt;script&gt;
    function foo(a,b){
        return a + b;
    }
&lt;/script&gt;
```

```html
//在另一个标签页调用函数
&lt;script src=&quot;http://函数定义地址?callback=foo&amp;a=10s&amp;b=20&quot;&gt;&lt;/script&gt;
```

由于 JSONP 是通过 \&lt;script\&gt; 标签的 src 属性，来实现跨域数据获取的，所以，**JSONP 只支持 GET 数据请求**，不支持 POST 请求。

**JSONP** **和** **Ajax** **之间没有任何关系**，不能把 JSONP 请求数据的方式叫做 Ajax，因为 JSONP 没有用到 XMLHttpRequest 这个对象。



### CORS

CORS （Cross-Origin Resource Sharing，跨域资源共享）由一系列 HTTP 响应头组成，**这些** **HTTP** **响应头决定浏览器是否阻止前端** **JS** **代码跨域获取资源**。

①CORS 主要在服务器端进行配置。客户端浏览器**无须做任何额外的配置**，即可请求开启了 CORS 的接口。

②CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器，才能正常访问开启了 CORS 的服务端接口（例如：IE10+、Chrome4+、FireFox3.5+）。

### CORS响应头部- Access-Control-Allow-Origin

响应头部中可以携带一个 **Access-Control-Allow-Origin** 字段，其语法如下:

```bash
Access-Control-Allow-Origin:&lt;origin&gt; | * //*为通配符
```

其中，origin 参数的值指定了允许访问该资源的外域 URL。

例如，下面的字段值将**只允许**来自 http://asuhe.fun 的请求：

```javascript
res.setHeader(&apos;Access-Control-Allow-Origin&apos;,&apos;http://asuhe.fun&apos;)
```

### CORS响应头部- Access-Control-Allow-Headers

默认情况下，CORS **仅**支持客户端向服务器发送如下的 9 个请求头：

**Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type （值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一）**

如果客户端向服务器发送了额外的请求头信息，则需要在服务器端，通过 Access-Control-Allow-Headers 对额外的请求头进行声明，否则这次请求会失败！

```js
//允许客户端额外向服务器发送Content-Type请求头和X-Custom-Header请求头
res.setHeader(&apos;Access-Control-Allow-Headers&apos;,&apos;Content-Type,X-Custon-Header&apos;)
```

### CORS响应头部- Access-Control-Allow-Methods

默认情况下，CORS 仅支持客户端发起 **GET、POST、HEAD** 请求。

如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源，则需要在服务器端，通过 Access-Control-Alow-Methods来指明实际请求所允许使用的 HTTP 方法。

```js
//仅允许POST,GET,DELETE,HEAD方法
res.setHeader(&apos;Access-Control-Allow-Methods&apos;,&apos;POST,GET,DELETE,HEAD&apos;)

//允许所有HTTP方法
res.setHeader(&apos;Access-Control-Allow-Methods&apos;,&apos;*&apos;)
```

### CORS请求的分类

#### 简单请求

同时满足以下两大条件的请求，就属于简单请求：

① 请求方式：GET、POST、HEAD 三者之一

② HTTP 头部信息不超过以下几种字段：无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type（只有三个值application/x-www-form-urlencoded、multipart/form-data、text/plain）



#### 预检请求

只要符合以下任何一个条件的请求，都需要进行预检请求：

① 请求方式为 GET、POST、HEAD 之外的请求 Method 类型

② 请求头中包含自定义头部字段

③ 向服务器发送了 application/json 格式的数据

在浏览器与服务器正式通信之前，浏览器会先发送 OPTION 请求进行预检，以获知服务器是否允许该实际请求，所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后，才会发送真正的请求，并且携带真实数据。

**其中OPTION预检请求的请求体为空，若预检成功则服务器返回的响应体也为空**



**简单请求的特点**：客户端与服务器之间只会发生一次请求。

**预检请求的特点**：客户端与服务器之间会发生两次请求，OPTION 预检请求成功之后，才会发起真正的请求。</content:encoded><author>Asuhe</author></item><item><title>Git快速上手指北</title><link>https://asuhe.org/blog/eb420bee/</link><guid isPermaLink="true">https://asuhe.org/blog/eb420bee/</guid><pubDate>Fri, 29 Oct 2021 19:36:37 GMT</pubDate><content:encoded>## Git基础使用

### 用户信息配置

```bash
# 设置自己的用户名和邮件地址 将用于记录谁操作了仓库
git config --global user.name &quot;Asuhe&quot;
git config --global user.email &quot;sphinx@asuhe.fun&quot;
```



### 配置信息检查

通过 git config --global user.name 和 git config --global user.email 配置的用户名和邮箱地址，会被写
入到 C:/Users/用户名文件夹/.gitconfig文件中。这个文件是Git 的全局配置文件，配置一次即可永久生效。

```bash
# 查看所有的全局配置
git config --list --global

# 查看指定的全局配置项
git config user.name
git config user.email
```

### 获取帮助

```bash
# 获取 config 命令的帮助手册，将会打开网页
git help config

# 获取 config 命令的快速参考手册
git config -h
```



### 创建仓库

在想要创建仓库的目录下启动 git 然后将文件跟踪提交即可在本地创建 git 仓库

```bash
# 初始化仓库
git init
# 简要的显示文件状态
git status -s | --short


# 跟踪会将文件放到暂存区
# 跟踪单个文件
git add 文件名
# 跟踪目录下所有文件
git add .

# 提交文件,将会提交所有在暂存区的文件
git commit -m &quot;更新描述&quot;

# 回滚，即将git仓库中文件版覆盖现有的文件版本
git checkout -- 文件名

# 将文件从暂存区移出
git reset HEAD 文件名

# 强制提交所有已跟踪文件，跳过暂存阶段
git commit -a -m &quot;更新描述&quot;

# 同时从git仓库和工作区中删除文件
git rm -f 文件名

# 仅删除git仓库中的文件
git rm --cached 文件名
```

### 忽略文件

可以将不想提交的文件设置为忽略，这样文件就不会被 git 跟踪

创建一个名为.gitignore的配置文件，列出要忽略的文件的匹配模式。 文件 .gitignore 的格式规范如下： 

① 以# 开头的是注释
② 以/结尾的是目录
③ 以/开头防止递归
④ 以!开头表示取反
⑤ 可以使用glob 模式进行文件和文件夹的匹配（glob 指简化了的正则表达式）

所谓的glob 模式是指简化了的正则表达式：
① 星号* 匹配零个或多个任意字符
② [abc] 匹配任何一个列在方括号中的字符（此案例匹配一个a或匹配一个b 或匹配一个c）
③ 问号?只匹配一个任意字符
④ 在方括号中使用短划线分隔两个字符，表示所有在这两个字符范围内的都可以匹配（比如[0-9] 表示匹配
所有0 到9 的数字）
⑤ 两个星号 ** 表示匹配任意中间目录（比如a/**/z 可以匹配 a/z 、 a/b/z 或 a/b/c/z 等）

```bash
# 忽略所有的.a文件
*.a

# 跟踪所有的lib.a，即使前面设置了忽略.a文件
!lib.a

# 忽略当前目录下的TODO文件，而不忽略子目录下的TODO
/TODO

# 忽略任何目录下名为build的文件夹
build/

# 忽略doc/下的txt文件,但不忽略其子目录下的txt文件
doc/*.txt

# 忽略doc/目录及其所有子目录下的.pdf文件
doc/**/*.pdf
```

### 查看操作日志

```bash
# 按时间降序列出提交历史
git log

# 显示最近两条提交历史
git log -2

# 一行展示最近两条提交历史
git log -2 --pretty=oneline

# 自定义提交历史输出格式
# %h 提交的简写哈希值 %an作者名字 %ar作者修改日期 %s更新描述
git log -2 ---pretty=format:&quot;%h | %an | %ar | %s&quot;
```

### 回滚至指定版本

```bash
# 回滚至指定ID的版本
git reset --hard &lt;CommitID&gt;

# 
```



### 将本地仓库推送至GitHub仓库

**基于HTTPS**

```bash
# 关联远程仓库
git remote add origin 远程仓库的地址
# 首次推送初始化
git push -u origin master
```

**基于SSH**

```bash
# 生成密钥对
ssh-keygen -t rsa -b 4096 -C &quot;github账号的邮箱&quot;
# 连续敲击 3 次回车，即可在 C:\Users\用户名文件夹\.ssh目录中生成id_rsa 和 id_rsa.pub 两个文件
# 复制id_rsa.pub的内容
# 在GitHub账号中添加公钥 Settings -&gt; SSH and GPG Keys -&gt; New SSH key
# 测试是否添加成功
ssh -T git@github.com

# 初始化操作类似HTTPS
```



### 分支操作

```bash
# 查看当前git仓库中的所有分支
git branch

# 创建分支
git branch 分支名

# 切换分支
git checkout 分支名

# 创建分支并切换到上面
git checkout -b 分支名

# 合并分支
git merge 分支名

# 删除分支
git branch -d 分支名

# 推送分支至远程仓库,仅第一次推送时要带-u参数
git push -u 远程仓库别名(默认origin) 本地分支名称:远程分支名称

# 实例
git push -u origin payment:pay # 若不填写远程分支名称，远程分支名称将与本地分支一致

# 查看远程仓库分支信息
git remote show 远程仓库名

# 下载远程分支,本地分支名称默认与远程分支一致
git checkout 远程分支名称

# 从远程仓库把远程分支下载至本地，并对下载至本地的分支重命名
git checkout -b 本地分支名称 远程仓库名:远程分支名称
# 实例
git checkout -b payment origin/pay

# 拉取当前分支中的远程分支代码
git pull

# 删除指定远程分支
git push 远程仓库名称 --delete 远程分支名称

# 实例
git push origin --delete pay
```</content:encoded><author>Asuhe</author></item><item><title>防抖与节流</title><link>https://asuhe.org/blog/100fa4c9/</link><guid isPermaLink="true">https://asuhe.org/blog/100fa4c9/</guid><pubDate>Fri, 29 Oct 2021 15:11:24 GMT</pubDate><content:encoded>## 防抖

什么是防抖，在操作系统中如果一块数据在短时间内频繁地由内存换出到外存，再由外存换入内存。这种内存频繁进行换入换出的现象我们称之为内存抖动。类似的在前端中，如果一个页面过于地进行频繁地触发事件我们也将这种行为称之为抖动。

**防抖策略**（debounce）是当事件被触发后，延迟 n 秒后再执行回调，如果在这 n 秒内事件又被触发，则重新计时。

![防抖](https://i.loli.net/2021/10/29/EYTveZJrRH21wV8.png)

防抖最典型的应用例子就是我们在淘宝上进行商品搜索的时候，每当我们输入一个关键字，搜索框下面就会弹出相应关键字的搜索结果。

![淘宝搜索匹配](https://i.loli.net/2021/10/29/YRlbuQnFfctiPZ6.png)

这里如果我们设置的是每当用户弹起键盘我们就发送一次ajax请求去刷新数据，那么在输入过程中就会导致太多的ajax请求被发送，造成抖动。所以我们需要减少一些不必要的网络请求来减轻服务器压力。

**原理演示代码**

```javascript
//设置一个定时器记录
let timer = null

ipt.addEventListener(&apos;keyup&apos;,function(){
    //每次键盘按下就清除定时器,延迟触发
    if(timer) clearTimeout(timer);
    //设置200ms后触发事件
    timer = setTimeout(function(){
        /*进行相应操作*/
    },200)
})
```

**代码实现及其使用**

```js
const target = document.getElementById(id);
function debouch(callback,delay = 100){
    let timer = null;
    return function(){
        if(timer){
            clearTimeout(timer);
        }
        timer = setTimeout(()=&gt;{
            callback.apply(this,arguments);
            timer = null;
        },delay);
    }
}
function add(a,b){
    return a + b;
}
target.addEventListiner(&apos;click&apos;,debouch(add,300))
```



## 节流

和防抖类似减少事件触发的概念就是节流，节流的目的是让事件在规定的时间间隔内只能触发一次。和操作系统里同步问题使用互斥锁的思想类似。我们可以在触发某类事件时，先让它判断目前是否可以进入，若可以进入就让它进入后立即将flag置为false使后续事件不能继续进入，退出前在将flag置为true。

**节流策略**（throttle），顾名思义，可以减少一段时间内事件的触发频率。

![节流](https://i.loli.net/2021/10/29/iedsbl8ULn5Qyrq.png)

节流最典型的应用就是懒加载时要监听计算滚动条的位置，但不必每次滑动都触发，可以降低计算的频率，而不必去浪费 CPU 资源

**原理演示代码**

```javascript
//定义一个节流阀
let timer = null;
btn.addEventListener(&apos;click&apos;,function(){
    if(timer){
        return 0;
    }
    timer = setTimeout(function(){
        /*进行相应操作*/
        timer = null;
    },1000)
})
```

**代码实现及使用**

```js
const target = document.getElementById(id);
function throttle(callback,delay = 100){
    let timer = null;
    return function(){
        if(timer){
            return 0;
        }
        timer = setTimeout(()=&gt;{
            callback.apply(this,arguments);
            timer = null;
        },delay)
    }
}
function add(a,b){
    return a + b;
}
target.addEventListiner(&apos;click&apos;,thrrotle(add,300))
```



## 总结

防抖：如果事件被频繁触发，防抖能保证只有最后一次触发生效，前面 N 多次的触发都会被忽略！

节流：如果事件被频繁触发，节流能够减少事件触发的频率，因此，节流是有选择性地执行一部分事件！</content:encoded><author>Asuhe</author></item><item><title>重绘与回流</title><link>https://asuhe.org/blog/9779b982/</link><guid isPermaLink="true">https://asuhe.org/blog/9779b982/</guid><pubDate>Wed, 03 Nov 2021 21:15:11 GMT</pubDate><content:encoded># 回流

## 什么是回流

大部分情况下页面生成一个新的帧都是`js`通过修改`DOM`或`CSSOM`来触发的，还有一部分是`css`触发的。如果在计算样式的阶段发现页面的布局结构也就是`DOM`树有被修改，那么就会触发回流（重排），然后触发后续渲染流水线的一系列操作。这个代价是非常大的。

## 为什么会回流

## 回流的触发条件

上面提到当`DOM`树被修改时，就会触发回流(reflow)。在实际操作中我们使用`display:none`来隐藏元素，元素会从`DOM`树上删除，这就会导致回流的产生而且`display:none`元素的所有后代元素都会跟随着一起被移除

# 重绘

## 什么是重绘

在计算样式的阶段发现页面的结构布局未被改变，仅仅是修改了颜色一类的信息也就是`css`的样式，那么就会跳过布局阶段，直接进入绘制阶段，这个过程就叫重绘。相对于回流，重绘的代价要小许多。**注意无论是重绘还是回流，都是基于`js`代码操作的情况下才会触发。当我们使用`css`动画实现渐变、变形等操作时是不会触发重绘与回流的。**因为`css`动画是在合成线程上执行的，和`js`完成的动画效果并不同，这个过程称为合成。

## 为什么会重绘

## 重绘的触发条件

使用类似`visibility:hidden`进行隐藏元素时，其原理就是修改该元素的颜色信息。此时会触发重绘，但应用了`visibility:hidden`的元素仅对该元素本身生效，其后代元素并没有影响还是会显示出来，而且在文档流中元素依然会占据其本身的位置

# 分层合成机制

通常我们的页面组成十分复杂，实现动画的效果时绘制的帧此时要输出的帧如果全都在主线程里生成的话，会导致页面性能急剧下降。为了解决这个问题，所以`Chrome`引入的分层合成机制，合成操作是在合成线程上执行的不会影响主线程的执行

## 分层合成

一个页面通常会被划分成多个图层来进行渲染，类似于`ps`里的项目那样进行分层处理最后显示时合并成一个图层。页面元素被分层后，当我们需要对上一帧的某些元素进行几何变换如平移、旋转、缩放等或者阴影、Alpha渐变、opacity变化时，合成器只需要将需要变化的图层进行操作并且此时调用的是显卡进行计算处理，所以它的性能会十分高所需合成时间非常短。

### 分块

大多数场景下我们只需要对小部分特定元素做动画效果处理，若我们因为这一小部分元素的动画而进行整个页面的图层合成的话那么明显有许多重复性的无效果的计算。**为了减小这部分开销`Chrome`里合成线程会将每个图层分割成固定大小的图开，然后优先绘制靠近视口的图块，这样就可以大大加速页面的显示速度。**

但即便如此合成也需要耗费不少时间，于是`Chrome`又会在首次合成图块的时候使用一个低分辨率的图片显示出来，合成器继续绘制正常分辨率的图片绘制完替换掉低分辨率的图片，尽管用户刚刚开始看到的图片分辨率低的内容但也比白屏要好上很多

**当我们需要对某个元素做几何变换或者阴影、不透明度变化时，如果使用`js`实现就会牵涉到渲染流水线，而渲染流水线是在主线程里执行的，所以效率低下。但使用`css`实现这些动画效果则是在合成线程上实现的，不会阻塞主线程所以效率会大大提高**

我们可以使用`will-change`来明确告诉渲染引擎你会对该元素做一些特效变换

```css
.box {
    will-change:transform opacity;
}
```

这段代码就是提前告诉渲染引擎`box`元素将要做几何变换和不透明度操作，渲染引擎会将该元素单独实现一帧，当变换发生时渲染引擎会通过合成线程直接处理变换。

**当图层内容发生变化时(如文字内容改变)一定会触发重绘或回流，合成只是基于图层内容不变的情况下才产生！**</content:encoded><author>Asuhe</author></item><item><title>身份认证技术</title><link>https://asuhe.org/blog/e9e5edd8/</link><guid isPermaLink="true">https://asuhe.org/blog/e9e5edd8/</guid><pubDate>Tue, 02 Nov 2021 08:34:25 GMT</pubDate><content:encoded>## 身份认证的由来

平时我们在网页上进行交互时，例如登入自己的账号进行信息修改，往往需要服务器对我们进行身份的辨别，好让服务器能够拿到专属于我们自己账号的数据并返回给我们。由于http协议是无状态的，它并不会对会话进行控制。任何一个http请求对服务器来说都是独立的，毫不相干的请求。{% post_link &apos;2021-10-28-http协议&apos; &apos;http协议&apos;%}的这种特性就导致了我们需要自己想办法对用户进行识别，于是便有了身份认证技术。

现在流行的身份认证认证手段主要有三种，分别是：cookie、session和 JWT。这三种身份认证技术都有各自的适用场景，所以在前端中均有大量使用。下面就让我们深入了解一下这三种技术

## Cookie认证

首先我们要明确什么是cookie。cookie实际上就是**一份服务器给浏览器客户端设置的文本信息**。当用户第一次登录进系统时，服务器会给用户设置一个cookie，后续用户每次发送http请求中都会携带cookie用于身份识别。

cookie信息都携带在请求头中，而且一个cookie的大小一般不超过4KB。

![请求头中的cookie](https://i.loli.net/2021/11/02/uvhOaopbYzlVLrm.png)

它的内容由**名称、值和一些用于控制有效期、安全性、使用范围的可选属性组成**。不同域名下的cookie时各自独立的，不能拿一个网站的cookie去另一个网站做身份认证。每当客户端发起请求时，会**自动**把**当前域名下**所有**未过期的** **Cookie** 一同发送到服务器。

我们可以在浏览器的设置中查看cookie的内容

![某度的一个cookie](https://i.loli.net/2021/11/02/vXBxOJRNS9u4Pqn.png)

因为cookie认证是用户单方面认证，用户完全可以自己伪造一个cookie发送给服务器来欺骗服务器，这使得cookie安全性较低。为了解决cookie的这个缺陷，进一步提高系统的安全性，所以又有了session机制来辅助身份认证。

## Session认证

session认证即是在服务器上设置一个标识用户的信息，当用户拿cookie来认证时，服务器这边根据cookie拿出相应的session来必对认证。若服务器这边没有对应的session则说明该cookie是伪造的，拒绝响应。

![session认证原理](https://i.loli.net/2021/11/02/K75ZGYXQ6RJMjOC.png)

有了session就可以防止用户伪造cookie登录进系统。进一步提高了安全性。

Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持 {% post_link &apos;2021-10-29-同源与跨域&apos; &apos;跨域访问&apos;  %}，所以，当涉及到前端跨域请求后端接口的时候，**需要做很多额外的配置**，才能实现跨域 Session 认证。

为了解决这个跨域问题，我们又升级了身份认证技术，采用JWT技术可以解决跨域认证的问题。

## JWT

JWT（英文全称：JSON Web Token）是目前**最流行**的**跨域认证解决方案**。针对跨域问题，JWT采用Token加密字符串来实现身份认证。用户的信息通过 Token 字符串的形式，保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。

![JWT工作原理](https://i.loli.net/2021/11/02/ZVlX2zeU5kWOug8.png)

JWT 通常由三部分组成，分别是 Header（头部）、Payload（有效荷载）、Signature（签名）。

三者之间使用英文的“.”分隔，格式如下：

```sh
Header.Payload.Signature
```

其中：

* **Payload** 部分**才是真正的用户信息**，它是用户信息经过加密之后生成的字符串。
* Header 和 Signature 是**安全性相关**的部分，只是为了保证 Token 的安全性。

![Payload部分](https://i.loli.net/2021/11/02/HCj5Ra2xt4MTiDz.png)

客户端收到服务器返回的 JWT 之后，通常会将它储存在 **localStorage 或 sessionStorage** 中。

此后，客户端每次与服务器通信，都要带上这个 JWT 的字符串，从而进行身份认证。推荐的做法是**把** **JWT** **放在** **HTTP** **请求头的** **Authorization** **字段中**，格式如下：

```
Authorization: Bearer &lt;token字符串&gt;
```

## 三种认证技术的区别与特点

| **认证技术** | **工作原理** |               **生命周期**               |          适用场景          |
| :----------: | :----------: | :--------------------------------------: | :------------------------: |
|    cookie    |   单向认证   | 未设置expire属性的cookie关闭浏览器即失效 | 服务器端渲染数据、同源认证 |
|   session    |   双向认证   |          服务器未清除即一直生效          | 服务器端渲染数据、同源认证 |
|     JWT      |   密钥认证   |       服务器未更换secret就一直生效       |  前后端分离架构、跨域认证  |</content:encoded><author>Asuhe</author></item><item><title>深入浅出Promise</title><link>https://asuhe.org/blog/9ff31ae0/</link><guid isPermaLink="true">https://asuhe.org/blog/9ff31ae0/</guid><pubDate>Fri, 05 Nov 2021 08:26:46 GMT</pubDate><content:encoded>## 同步与异步

在学习Promise之前我们需要先明白一些基础知识，首先我们要知道什么叫实例对象，什么叫函数对象。所谓实例对象就是我们使用 new 关键字创建出来的对象，称为实例对象，一般简称对象。而函数对象是指当我们把一个函数当作对象使用时，此时我们称这个函数为函数对象

```js
//函数的两种身份：函数、函数对象
function foo(){
    console.log(&apos;ok&apos;);
}

foo() // ok 当我们用()调用函数时，此时函数称为一个函数
foo.a = 20 // 当我们像这样给函数添加属性或者方法时，此时函数称为一个函数对象
```

按函数的调用者分类我们可以将某些函数称为回调函数，**回调函数就是我们定义的，我们没调用，最终执行了。**最典型的例子就是定时器里我们传入的函数。根据回调函数执行时机的不同，我们又可以将回调函数分为**同步回调函数和异步回调函数**。

同步回调函数的特点就是**立即执行**，完全执行完了才结束，不会放入回调队列中。**换言之就是它是在主线程上执行的**。例如数组遍历相关的回调函数、Promise的excutor函数

```js
let arr = [10,20,30]
arr.forEach(value =&gt; console.log(value))  
console.log(&apos;主线程&apos;)
// 输出顺序为: 10 20 30 主线程
```

上述例子中可以明显看到，数组forEach方法里的回调函数先于主线程上的 console.log 输出，这表明这个回调函数就是一个同步回调

与同步回调函数相反，异步回调函数的特点就是**延迟执行**，它会被放入回调队列里，等主线程上的函数都执行完以后将来再根据条件执行。例如定时器上的回调函数、ajax回调、Promise的成功 | 失败回调

```js
setTimeout(()=&gt;(console.log(&apos;hello!&apos;)),0)
console.log(&apos;主线程&apos;)
//输出顺序为: 主线程 hello!
```

即便定时器的延迟设为 0 ，它里面的回调函数依然要等待主线程执行完毕才能执行。这就是一个典型的异步回调

通过理解同步回调和异步回调的例子，我们可以明白。所谓同步就是绝对的串行执行，只有上一步执行完了下一步才能继续执行。想象这样的一个场景，我们去做饭，电饭煲在煮饭的同时我们可以继续处理我们的菜，我们不必等饭完全煮好了才开始做菜，这样太浪费时间了。这种就是异步执行。如果我们必须等到饭煮好了才开始炒菜之类的，那这种就是同步执行。

## Promise

有了上面同步异步的概念以后，接下来我们就可以开始学习Promise了。我们先来看看它是什么，官方给的定义是啥。

* 抽象表达: 
  * Promise是JS中进行**异步编程**的**新方案**(旧的是谁?---纯回调)

*  具体表达:
  * 从语法上来说: Promise是一个构造函数
  *  从功能上来说: promise对象用来封装一个异步操作并可以获取其结果

其实看了上面的表达我们还是不懂什么是Promise。从异步这个词入手，我们都知道ajax就是一个典型的异步操作。那么我们就可以用Promise来封装ajax请求。那我们为什么非要用Promise来封装异步操作，我们用普通的回调函数形式不一样可以吗？这就涉及到回调地狱这个问题，关于[Promise的优越性](# 回调地狱 )我们后面再谈。首先来明确一些基础的Promise知识

* Promise不是回调，是一个内置的构造函数，是程序员自己new调用的。
* new Promise的时候，要传入一个回调函数，它是同步的回调，会立即在主线程上执行，它被称为executor函数
* 每一个Promise实例都有3种状态：初始化(pending)、成功(fulfilled)、失败(rejected)
* 每一个Promise实例在刚被new出来的那一刻，状态都是初始化(pending)
* executor函数会接收到2个参数，它们都是函数，分别用形参：resolve、reject接收
  * 调用resolve函数会：
    * 让Promise实例状态变为成功(fulfilled)
    * 可以指定成功的value。
  * 调用reject函数会：
    * 让Promise实例状态变为失败(rejected)
    * 可以指定失败的reason。

```js
//new 一个实例
const p  = new Promise((resolve,reject)=&gt;{})

//根据上面描述这个Promise必须传入一个excutor函数，而这个excutor函数又两个形参也是函数,我们不必定义该两个形参函数，可以直接调用
(resolve,reject)=&gt;{}  //excutor
```

当我们使用一个Promise管理异步操作的时候，我们要在excutor函数内启动异步任务然后再用它的 then 方法来指定异步任务结束后根据Promise实例的状态来调用相应的回调函数。

```js
//用Promise封装一个自己的get请求ajax
function myAjax(url,datas){
    return new Promise(
        (resolve,reject)=&gt;{
            const xhr = new XHLHttpRequest();
            xhr.onreadystatuschange = ()=&gt;{
                if(xhr.readyState === 4){
                    if(xhr.status === 200) resolve(&apos;ok&apos;) //请求成功将Promise状态置为fulfilled
                    else reject(&apos;falure&apos;)  //请求失败将Promise状态置为rejected
                }
            }
            xhr.opne(&apos;GET&apos;,url);
            xhr.send();
        }
    );
}

const p = myAjax(&apos;http://127.0.0.1/get&apos;,{test:&apos;test&apos;});

p.then( //为Promise实例指定成功与失败的回调函数
    (value)=&gt;{ console.log(&apos;请求成功1&apos;,value); }, //fulfilled状态调用
    (reason)=&gt;{ console.log(&apos;请求失败1&apos;,reason); } //rejected状态调用
)

//一个promise指定多个成功/失败回调函数, 则会依次调用并不会覆盖
p.then( //为Promise实例指定成功与失败的回调函数
    (value)=&gt;{ console.log(&apos;请求成功2&apos;,value); }, //fulfilled状态调用
    (reason)=&gt;{ console.log(&apos;请求失败2&apos;,reason); } //rejected状态调用
)

//若该次请求失败则依次输出:请求失败1 请求失败2
```

* 关于状态的注意点：
  * 三个状态:
    * pending: 未确定的------初始状态
    * fulfilled: 成功的------调用resolve()后的状态
    * rejected: 失败的-------调用reject()后的状态
  * 两种状态改变
    * pending ==&gt; fulfilled
    * pending ==&gt; rejected
  * 状态只能改变一次！

## Promise基本方法

### Promise构造函数: new Promise (executor) {}

* executor函数: 是同步执行的，(resolve, reject) =&gt; {}
* resolve函数: 调用resolve将Promise实例内部状态改为成功(fulfilled)。
* reject函数: 调用reject将Promise实例内部状态改为失败(rejected)。
* 说明: excutor函数会在Promise内部立即同步调用,异步代码放在excutor函数中。

### Promise.prototype.then方法: Promise实例.then(onFulfilled,onRejected)

* onFulfilled: 成功的回调函数 (value) =&gt; {}
* onRejected: 失败的回调函数 (reason) =&gt; {}
* 特别注意(难点)：then方法会返回一个新的Promise实例对象
* 如果上一个回调返回的是一个非promise对象，则这个新的Promise实例状态为fulfilled
* 当上一个回调返回一个Promise对象则该新返回的Promise对象的状态与回调返回的Promise对象一致

```js
const p = new Promise((resolve,reject) =&gt;{ resolve(&apos;ok&apos;); }) 
const x = p.then(
	value =&gt; {return 1}, //返回非Promise值
    reason =&gt; {return new Promise.reject(996)}
)
x.then(
	value =&gt; console.log(&apos;成功了&apos;), //x状态为fulfilled,输出成功了
    reason =&gt; console.log(&apos;失败了&apos;)
)

const z = p.then(
	value =&gt; {return new Promise((resolve,reject)=&gt;{})}, //返回Promise值
    reason =&gt; {return new Promise.reject(996)}
)
z.then( //z状态为 pending ,不调用回调函数
	value =&gt; console.log(&apos;成功了&apos;), 
    reason =&gt; console.log(&apos;失败了&apos;) 
)

```

### Promise.prototype.catch方法: Promise实例.catch(onRejected)

* onRejected: 失败的回调函数 (reason) =&gt; {}
* 说明: catch方法是then方法的语法糖, 相当于: then(undefined, onRejected)

### Promise.resolve方法: Promise.resolve(value)

* 说明: 用于快速返回一个状态为fulfilled或rejected的Promise实例对象
* 备注：value的值可能是：(1)非Promise值  (2)Promise值
* 当传入的值为非Promise值时或空值时，直接返回一个 fulfilled 状态的 Promise实例
* 当传入的值为 Promise 时，返回的Promise状态跟随传入的Promise

### Promise.reject方法: Promise.reject方法(reason)

* 说明: 用于快速返回一个状态必为rejected的Promise实例对象

```js
const x = Promise.reject(996);
x.then(
	value =&gt; console.log(&apos;成功了&apos;,value),
    reason =&gt; console.log(&apos;失败了&apos;,reason) //调用该函数 输出:失败了 996
)
```



### Promise.all方法: Promise.all(promiseArr)

* promiseArr: 包含n个Promise实例的数组
* 说明: 返回一个新的Promise实例, 只有所有的promise都成功才成功, 只要有一个失败了就直接失败。
* 若没有失败的值，且没有pending的值即全都成功返回的新Promise状态为fulfilled
* 若没有失败的值，但存在pending的值即数组内仅有pending和fulfilled两种值，则返回的新Promise状态为pending

### Promise.race方法: Promise.race(promiseArr)

* promiseArr: 包含n个Promise实例的数组
* 说明: 返回一个新的Promise实例, 成功还是很失败？以最先出结果的promise为准。
* 若最先出结果的promise为pending则跳过该promise
* 这也就意味着race返回的Promise实例仅有fulfilled和rejected两种状态，不存在pending状态的值

## 回调地狱

上面我们学习了基本Promise使用，但是我们依然没有看出来Promise的优越性在哪里。现在我们有这么一个需求，连发三次Ajax请求，仅当上次请求成功时才发送下一次请求，若请求失败则中断以后的所有请求。

```js
//用纯回调的方式封装ajax
function sendAjax(url,data,success,error){
    const xhr = new XMLHttpRequest()
    xhr.onreadystatechange = ()=&gt;{
        if(xhr.readyState === 4){
            if(xhr.status &gt;= 200 &amp;&amp; xhr.status &lt; 300) success(xhr.response);
            else error(&apos;请求出了点问题&apos;);
        }
    }
    //整理参数
    let str = &apos;&apos;
    for (let key in data){
        str += `${key}=${data[key]}&amp;`
    }
    str = str.slice(0,-1)
    xhr.open(&apos;GET&apos;,url+&apos;?&apos;+str)
    xhr.responseType = &apos;json&apos;
    xhr.send()
}
//传统解决方案，链式连发三个请求
sendAjax(
    &apos;https://api.apiopen.top/getJoke&apos;,
    {page:1,count:2,type:&apos;video&apos;},
    response =&gt;{
        console.log(&apos;第1次成功了&apos;,response);
        sendAjax(
            &apos;https://api.apiopen.top/getJoke&apos;,
            {page:1,count:2,type:&apos;video&apos;},
            response =&gt;{
                console.log(&apos;第2次成功了&apos;,response);
                sendAjax(
                    &apos;https://api.apiopen.top/getJoke&apos;,
                    {page:1,count:2,type:&apos;video&apos;},
                    response =&gt;{
                        console.log(&apos;第3次成功了&apos;,response);   //回调地狱
                    }, 
                    err =&gt;{console.log(&apos;第3次失败了&apos;,err);}
                )
            }, 
            err =&gt;{console.log(&apos;第2次失败了&apos;,err);}
        )
    }, 
    err =&gt;{console.log(&apos;第1次失败了&apos;,err);}
)
```

可以看到当我们要进行三次链式的异步请求时，采用纯回调的方式来处理就导致了回调地狱的问题。要对代码进行维护十分困难。而如果我们采用Promise去封装异步请求，则可以解决回调地狱的问题

```js
//Promsie封装ajax
function sendAjax(url,data){
    return new Promise((resolve,reject)=&gt;{
        const xhr = new XMLHttpRequest()
        xhr.onreadystatechange = ()=&gt;{
            if(xhr.readyState === 4){
                if(xhr.status &gt;= 200 &amp;&amp; xhr.status &lt; 300) resolve(xhr.response);
                else reject(&apos;请求出了点问题&apos;);
            }
        }
        //整理参数
        let str = &apos;&apos;
        for (let key in data){
            str += `${key}=${data[key]}&amp;`
        }
        str = str.slice(0,-1)
        xhr.open(&apos;GET&apos;,url+&apos;?&apos;+str)
        xhr.responseType = &apos;json&apos;
        xhr.send()
    })
}

//利用Promise进行三次链式ajax请求
sendAjax(&apos;https://api.apiopen.top/getJoke&apos;,{page:1})
    .then(
        value =&gt; {
            console.log(&apos;第1次请求成功了&apos;,value);
            //发送第2次请求
            return sendAjax(&apos;https://api.apiopen.top/getJoke&apos;,{page:1})
    	}
)
    .then(
        value =&gt; {
            console.log(&apos;第2次请求成功了&apos;,value);
            //发送第3次请求
            return sendAjax(&apos;https://api.apiopen.top/getJoke&apos;,{page:1})
        }
)
    .then(
        value =&gt; {console.log(&apos;第3次请求成功了&apos;,value);}
)
	.cathe(
		err =&gt; console.log(err);  //利用错误穿透进行兜底
)
```

可以明显地对比出来，利用Promise进行链式异步操作能清晰地看到调用结构，维护起来相比纯回调方便了很多。这就解决了回调地狱的问题。

## Promise的优势

* 指定回调函数的方式更加灵活: 
  * 旧的: 必须在启动异步任务前指定
  * Promise: 启动异步任务 =&gt; 返回promie对象 =&gt; 给promise对象绑定回调函数(甚至可以在异步任务结束后指定)

* 支持链式调用, 可以解决回调地狱问题
  * 什么是回调地狱：回调函数嵌套调用, 外部回调函数异步执行的结果是嵌套的回调函数执行的条件
  * 回调地狱的弊病：代码不便于阅读、不便于异常的处理
  * 一个不是很优秀的解决方案：then的链式调用
  * 终极解决方案：async/await（底层实际上依然使用then的链式调用）

## Promise关键问题

### 如何改变一个Promise实例的状态?

* 执行resolve(value): 如果当前是pending就会变为fulfilled
* 执行reject(reason): 如果当前是pending就会变为rejected
* 执行器函数(executor)抛出异常: 如果当前是pending就会变为rejected

### 改变Promise实例的状态和指定回调函数谁先谁后?

* 都有可能, 正常情况下是先指定回调再改变状态, 但也可以先改状态再指定回调
* 如何先改状态再指定回调? 
  * 延迟一会再调用then()
* Promise实例什么时候才能得到数据?
  * 如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据
  * 如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据



### Promise实例.then()返回的是一个【新的Promise实例】，它的值和状态由什么决定?

* 简单表达: 由then()所指定的回调函数执行的结果决定
* 详细表达:
  * 如果then所指定的回调返回的是非Promise值a:
    * 那么【新Promise实例】状态为：成功(fulfilled), 成功的value为a
  * 如果then所指定的回调返回的是一个Promise实例p:
    * 那么【新Promise实例】的状态、值，都与p一致
  * 如果then所指定的回调抛出异常:
    * 那么【新Promise实例】状态为rejected, reason为抛出的那个异常

### 如何中断promise链：

* 当使用promise的then链式调用时, 在中间中断, 不再调用后面的回调函数。
* 办法: 在失败的回调函数中返回一个pendding状态的Promise实例。

### Promise错误穿透的原理：

* 当使用promise的then链式调用时, 可以在最后用catch指定一个失败的回调,
* 前面任何操作出了错误, 都会传到最后失败的回调中处理了
* 备注：如果不存在then的链式调用，就不需要考虑then的错误穿透。

```js
const p = new Promise((resolve,reject)=&gt;{
    setTimeout(()=&gt;{
        reject(-100)
    },1000)
})
p.then(
    value =&gt; {console.log(&apos;成功了1&apos;,value);return &apos;b&apos;},
    reason =&gt; {throw reason}//底层帮我们补上的这个失败的回调
)
    .then(
    value =&gt; {console.log(&apos;成功了2&apos;,value);return Promise.reject(-108)},
    reason =&gt; {throw reason}//底层帮我们补上的这个失败的回调
)
    .catch(
    reason =&gt; {throw reason}
)
```





## async &amp; await

* async修饰的函数

  函数的返回值为promise对象

  Promise实例的结果由async函数执行的返回值决定，返回非Promise值则返回的Promise对象状态为fulfilled，返回Promise则状态跟随返回的Promise，但是不能返回一个 rejected的Promise，否则报错

```js
const p = (async () =&gt;{return 0;})();
console.log(p) //输出fulfilled
p.then(
	value =&gt; console.log(&apos;成功了&apos;,value), //执行成功回调
    reason =&gt; console.log(&apos;失败了&apos;,reason)
)
----------
const p = (async () =&gt;{return Promise.reject(996);})();
console.log(p) //输出pending ,同时也说明了Promise.reject()是个异步函数
p.then(
	value =&gt; console.log(&apos;成功了&apos;,value), 
    reason =&gt; console.log(&apos;失败了&apos;,reason)//执行失败回调
)
----------
const p = (async () =&gt;{return new Promise(()=&gt;{});})();
console.log(p) 
p.then(
	value =&gt; console.log(&apos;成功了&apos;,value), 
    reason =&gt; console.log(&apos;失败了&apos;,reason)
)//不调用任何一个,说明最后状态为pending
----------
const p = (async () =&gt;{return Promise.resolve(200);})();
console.log(p) //输出pending ,同时也说明了Promise.resolve()是个异步函数
p.then(
	value =&gt; console.log(&apos;成功了&apos;,value), //执行成功回调
    reason =&gt; console.log(&apos;失败了&apos;,reason)
)
```

* await表达式

  await右侧的表达式一般为Promise实例对象, 但也可以是其它的值

  (1).如果表达式是Promise实例对象, await后的返回值是promise成功的值

  (2).如果表达式是其它值, 直接将此值作为await的返回值

* 注意:

  await必须写在async函数中, 但async函数中可以没有await

  如果await的Promise实例对象失败了, 就会抛出异常, 需要通过try...catch来捕获处理

```js
const p1 = new Promise((resolve,reason)=&gt;{
    setTimeout(function(){
        resolve(&apos;ok了&apos;)
    },2000)
})
const p2 = Promise.reject(&apos;中断&apos;)

const p3 = new Promise((resolve,reason)=&gt;{
    setTimeout(function(){
        resolve(&apos;Ok啊&apos;)
    },4000)
})

;(async()=&gt;{
    try{
        const x1 = await p1;
        console.log(&apos;1&apos;,x1); //输出:1 ok了
        const x2 = await p2; //该点直接失败转入失败回调 throw error
        console.log(&apos;2&apos;,x2);
        const x3 = await p3;
        console.log(&apos;3&apos;,x3);
    }catch(err){
        console.log(err); //catch接收上面throw 的 error 输出:中断
    }
})()
------
//上面try里面的代码被浏览器翻译为
p1.then(
	value =&gt; {console.log(&apos;1&apos;,value) return p2;} //输出:1 ok了
    // 不写失败回调底层补上了 reason =&gt; throw reason
).then(
    value =&gt; {console.log(&apos;2&apos;,value); return p3;}
).then(
	value =&gt; {console.log(&apos;3&apos;,value);}
)
```

### 宏任务与微任务

目前为止，除了Promise里的成功和失败回调是微任务，其它异步回调都是宏任务

   宏队列:[宏任务1，宏任务2.....]

   微队列:[微任务1，微任务2.....]

   规则：每次要执行宏队列里的一个任务之前，先看微队列里是否有待执行的微任务

​      1.如果有，先执行微任务

​      2.如果没有，按照宏队列里任务的顺序，依次执行

## 自我检测

### 初级版

```js
// //代码一
setTimeout(()=&gt;{
    console.log(&apos;timeout&apos;)
},0)

Promise.resolve(1).then(
    value =&gt; console.log(&apos;成功1&apos;,value)
)
Promise.resolve(2).then(
    value =&gt; console.log(&apos;成功2&apos;,value)
)
console.log(&apos;主线程&apos;)


//代码二
setTimeout(()=&gt;{
    console.log(&apos;timeout1&apos;)
})
setTimeout(()=&gt;{
    console.log(&apos;timeout2&apos;)
})

Promise.resolve(1).then(
    value =&gt; console.log(&apos;成功1&apos;,value)
)
Promise.resolve(2).then(
    value =&gt; console.log(&apos;失败2&apos;,value)
) 

//代码三
setTimeout(()=&gt;{
    console.log(&apos;timeout1&apos;)
    Promise.resolve(5).then(
        value =&gt; console.log(&apos;成功了5&apos;)
    )
})
setTimeout(()=&gt;{
    console.log(&apos;timeout2&apos;)
})

Promise.resolve(3).then(
    value =&gt; console.log(&apos;成功了3&apos;)
)
Promise.resolve(4).then(
    value =&gt; console.log(&apos;失败了4&apos;)
)
```

### 高级版

```js
setTimeout(() =&gt; {
    console.log(&apos;0&apos;);
},0);
new Promise((resolve,reject) =&gt; {
    console.log(&apos;1&apos;);
    resolve()
}).then(()=&gt;{
    console.log(&apos;2&apos;);
    new Promise((resolve,reject) =&gt; {
        console.log(&apos;3&apos;);
        resolve();
    }).then(()=&gt;{
        console.log(&apos;4&apos;);
    }).then(()=&gt;{
        console.log(&apos;5&apos;);
    })
}).then(()=&gt;{
    console.log(&apos;6&apos;);
})

new Promise((resolve,reject) =&gt;{
    console.log(&apos;7&apos;);
    resolve();
}).then(()=&gt;{
    console.log(&apos;8&apos;);
})
```</content:encoded><author>Asuhe</author></item><item><title>TypeError:this.getOptions is not a function 报错解决</title><link>https://asuhe.org/blog/6ace2ef/</link><guid isPermaLink="true">https://asuhe.org/blog/6ace2ef/</guid><pubDate>Sun, 07 Nov 2021 11:29:37 GMT</pubDate><content:encoded>## webpack 中打包 css、less、sass文件的坑

vscode用import语法引入css文件不显示css路径：安装一个路径显示插件即可，推荐使用[Path Autocomplete](https://marketplace.visualstudio.com/items?itemName=ionutvmi.path-autocomplete)即可解决

原本安装好`css-loader`准备用`webpack`打包css文件时出现一堆底层报错说`TypeError: this.getOptions is not a function`

```sh
ERROR in ./src/css/index.css
Module build failed (from ./node_modules/style-loader/dist/cjs.js):
TypeError: this.getOptions is not a function
    at Object.loader (C:\Users\12071\Desktop\练习\webpack-test\node_modules\style-loader\dist\index.js:19:24)
 @ ./src/js/app.js 8:0-25
```

原因：loader版本不匹配

解决方案：安装配套版本的`style-loader`和`css-loader`

```shell
//安装2.0版本的style-loader
npm i style-loader@2.0.0

//安装5.2.7版本的css-loader
npm i css-loader@5.2.7
```

[stackoverflow YYDS](https://stackoverflow.com/questions/68580273/type-error-this-getoptions-is-not-a-function-for-style-loader)

less文件无法打包

原因：less-loader版本过高

解决方案：安装低于5.0版本的`less-loader`

```sh
npm install less-loader@5.0 -D
```

[stackoverflow YYDS](https://stackoverflow.com/questions/66792368/less-files-not-getting-picked-up-by-less-loader/66793018#66793018)</content:encoded><author>Asuhe</author></item><item><title>VUE基础(一)</title><link>https://asuhe.org/blog/6536d6b3/</link><guid isPermaLink="true">https://asuhe.org/blog/6536d6b3/</guid><pubDate>Mon, 08 Nov 2021 07:54:23 GMT</pubDate><content:encoded>## VUE的数据代理原理

```js
&lt;div id=&quot;app&quot;&gt;
    {{msg}}
&lt;/div&gt;
//最简单的Vue对象
const vm = new Vue({
    el:&apos;#app&apos;,
    data:{
        msg:&apos;I love you，asuka&apos;
    }
})
```

我们都知道当我们使用模板语法去输出msg时并不是直接输出data的msg，而是经过 vm 这层代理输出的。 

我们在模板中输出的所有数据、方法都是经过 vm 代理的，而不是直接使用配置对象中东西。同样地，当我们修改 msg或者方法时，我们修改的也是 data 、methods中的东西。而不是 vm 上挂载的，修改结果将直接影响到配置对象中的值。

那么这种数据代理是如何做到的呢，答案就是 `Object.defineProperty()`方法。

### Object.defineProperty()

这个方法可以添加或修改一个属性，将其变为**响应式属性**。什么叫响应式属性呢，就是随着你源属性值的变化。它的属性值也跟随着变化。举个简单的例子

```js
let obj = {
    firstName:&apos;zhang&apos;,
    lastName:&apos;san&apos;,
    fullName:&apos;zhang-san&apos;
};
console.log(obj.fullName) //zhang-san

obj.firstName = &apos;li&apos;
console.log(obj.fullName) //zhang-san
//我们改变firstName,合理的是输出是 li-san。但是输出的依然是zhang-san，也就是说fullName没有自动跟随firstName变化
--------------
//如果我们将fullName变成响应式属性，那么上述需求就额能实现
Object.defineProperty(obj,&apos;fullName&apos;,{
    get(){ //每次输出fullName属性时，get函数就会被调用
        return this.firstName + &apos;-&apos; + this.lastName; 
    },
    set(newVal){ //每次修改fullName属性时，set函数就会传入修改后的新值，然后被调用
        let arr = newVal.split(&apos;-&apos;);
        this.firstName = arr[0];
        this.lastName = arr[1];
    }
});
console.log(obj.fullName); //zhang-san

obj.firstName = &apos;li&apos;;
console.log(obj.fullName); //li-san

obj.fullName = &apos;xiao-xiangxiang&apos;;
console.log(obj.firstName); //xiao
console.log(obj.lastName); //xiangxiang
```

实际上我们可以 set 和 get 函数上做任何我们想做的事情。根据这个特性我们可以想到 vm 是如何做数据代理的

```js
//模拟数据代理
let vm = {};
let data = {msg:&apos;asuka&apos;};
Object.defineProperty(vm,&apos;msg&apos;,{
    get(){
        return data.msg;
    },
    set(newVal){
        data.msg = newVal;
    }
})
```

我们可以看到 vm 原本就是个空对象，但是我们使用了`Object.defineProperty()`方法后，它就增添了一个属性`msg`，每当这个`msg`被输出时就会自动调用`get`函数，被修改时就会自动调用`set`函数

当我们只设置 get 函数进行操作时在 vue 中实际上就是单向数据绑定基础，而我们同时设置好 get 和 set 函数时在 vue 中就变成了双向数据绑定的基础

## computed和watch

### computed

我们把上例中`obj`变量拿过来，在这个变量中我们定义了`fullName`属性。实际上这个属性是由`firstName`和`lastName`这两个源头属性组合而成的。既然它是由其它属性组合而成的，那么我们就可以在配置对象的 data 中省略它的定义，直接在 vue 的模板中动态生成。

```html

&lt;div id=&apos;app&apos;&gt;
    &lt;!-- 第一种方法 直接使用字符串拼接它 --&gt;
    &lt;p&gt;{{firstName}} + &apos;-&apos; + {{lastName}}&lt;/p&gt;
    &lt;!-- 第二种方法 用函数调用拼接,本质上和拼接字符串是一样的 --&gt;
	&lt;p&gt;getFullName()&lt;/p&gt;
&lt;/div&gt;

&lt;script&gt;
const vm = new Vue({
    el:&apos;#app&apos;,
    data(){
        return {
            firstName:&apos;zhang&apos;,
            lastName:&apos;san&apos;
        }
    },
    methods:{
        getFullName(){
            return this.firsName + &apos;-&apos; + this.lastName
        }
    }
})
&lt;/script&gt;
```

当我们使用字符串拼接它有一个很大的缺点就是，一旦数据多了起来。拼接字符串这种方法维护起来十分麻烦。而在 vue 它提供了一种计算属性的方式让我们可以更加高效地得到这个由其它属性组合而来的属性。

```html

&lt;div id=&apos;app&apos;&gt;
    &lt;!-- 第三种方法 计算属性 --&gt;
    &lt;p&gt;{{fullName}}&lt;/p&gt;
&lt;/div&gt;


&lt;script&gt;
const vm = new Vue({
    el:&apos;#app&apos;,
    data(){
        return {
            firstName:&apos;zhang&apos;,
            lastName:&apos;san&apos;
        }
    },
    //计算出fullName，这个fullName可以不预先定义，会被动态创建
    computed:{
        fullName:{
            get(){ //类似Object.defineProperty()
                return this.firstName + &apos;-&apos; + this.lastName;
            },
            set(newVal){
                /* code */
            }
        }
    }
    //当computed只使用 get 函数时，可以使用简写形式
    fullName(){
    	return this.firstName + &apos;-&apos; + this.lastName
	}
})
&lt;/script&gt;
```

表面上看起来使用**computed**去计算一个属性和第一种、第二种方法比起来貌似并没有什么区别，但是实际上用计算属性的方式去获取一个全新的响应式属性效率更高，因为它会被缓存起来。当`fullName`被多次使用时，它将会直接从缓存中拿出来使用不会多次使用`this.firstName + &apos;-&apos; + this.lastName`去拼接字符串。

由上我们可以总结出computed的特性：

* 可以动态创建出一个属性，不用预先定义属性
* 创建出来的这个属性会被缓存，当属性的值不改变时使用该属性将会直接从缓存中拿出，提高效率

### watch

除了上面三种方法来获得`fullName`属性我还有第四种方法得到`fullName`。那就是使用`watch`监视属性。**使用监视的前提是这个属性必须预先存在，也就是说我们必须在data里预先就有需要监视的属性，然后才能去对它使用`watch`监视。**这是和`computed`的一个重要区别。继续使用上面的例子，我们只需要在data后面在跟一个watch属性

```js
data(){
    return {
        firstName:&apos;zhang&apos;,
        lastName:&apos;san&apos;,
        fullName:&apos;&apos; //使用watch时fullName必须预先存在
    }
},
watch:{//对象内放置被监视的属性
    firstName:{ 
		handler(newVal,oldVal){  //这里我们使用handler函数回调，handler是固定写法不可更改,hander会传入被监视属性的新值和旧值两个参数
            //只有当firstName这个属性发生变化时，handler回调才会执行
            this.fullName = newVal + &apos;-&apos; + this.lastName;
        },
        immediat:true //配置了immediate 无论被监视属性是否变化都强制执行一次handler回调
    }
}

//第二种watch写法  在Vue()的外部使用$watch方法监视lastName属性
vm.$watch(&apos;lastName&apos;,function(newVal,oldVal){
    fullName = this.firstName + &apos;-&apos; + newVal;
})
```

通过上面例子我们可以体会到watch的特点：

* 被监视的属性必须预先存在
* 当被监视的属性不发生变化时，handler函数其实不会被调用
* 可以使用 immediat:true 强制执行 handler 回调

### computed 和 watch 最重要的区别

**实际上 computed 和 watch 有一个最重要的区别就是，computed里面只能获取到同步的数据，而不能获取到异步的数据。而 watch 里面同步异步的数据都可以获取到。**

```js
&lt;p&gt;{{fullName}}&lt;/p&gt;  // 1s后这个fullName可以被渲染出来 输出:嘿嘿

watch:{
    firstName:{
        //这个对象是一个配置对象
        //当数据发生改变的时候会自动调用handler回调
        handler(newVal,oldVal){
            
            setTimeout(() =&gt; {
                // 异步修改数据
                this.fullName = &apos;嘿嘿&apos;;
            }, 1000);
        }
    }
```

```js
&lt;p&gt;{{fullName}}&lt;/p&gt;  // 1s后这个fullName不能被渲染出来 fullName的值为null,无法得到字符串 哈哈

computed:{
    //计算属性的完整写法
    fullName:{
        get(){
            let n = null;
            //异步修改数据
            setTimeout(()=&gt;{
                n = &apos;哈哈&apos;;
            })
            return n;
        },
         // 当计算属性的数据能被修改时候使用（表单类元素在双向绑定计算属性值）
        set(val){
			/* code */
        }
}
```

### 深度监视

当我们使用watch的时候如果不指定为深度监视那么它就为一般监视。

一般监视可以用`const`理解，它只监视本身这个变量的引用，并不关心引用内部如何变化。如果我们使用一般监视去监视一个对象，那么它就会像浅拷贝一样，只关键浅层键值的变化，如果浅层键值又是一个对象，那么这个对象里面如何变化它是不会监视到的。

当我们使用深度监视的时候，就类似于深拷贝。它里面再套一层对象，它也能监测到。

```js
data(){
    return {
        comment:[
            {id:1,name:&apos;asuhe&apos;,content:&apos;666&apos;},
            {id:2,name:&apos;asuka&apos;,content:&apos;2333&apos;}
        ]
    }
},
watch:{
    comment:{
        //不开启深度监视时，只有comment数组里面的对象整个改变才能被监视到
        deep:true //开启深度监视，comment里面的对象内的数据(id、)改变也能监视到
        handler(newVal,oldVal){
            /*....*/
        }
    }
}
```</content:encoded><author>Asuhe</author></item><item><title>const 和 Object.freeze()</title><link>https://asuhe.org/blog/bc875182/</link><guid isPermaLink="true">https://asuhe.org/blog/bc875182/</guid><pubDate>Mon, 08 Nov 2021 20:12:48 GMT</pubDate><content:encoded>const 关键字是用来声明一个常量的，它可以使一个**变量引用**不能被改变，而且在定义之初就要赋予初值，不然会报语法错误。这个变量如果是数组或者对象，则它里面的内容其实是可以修改的。

而`Object.freeze()`方法是冻结了一个对象里面**属性的引用**，对象的属性值不能改变，但如果对象的属性值是另一个数组或对象，则这个属性值的内部依然可以修改。所以`Object.freeze()`实际上是一个**浅冻结**

```js
const obj = {
    name:&apos;zhangsan&apos;,
    age:18,
    address:[
        &apos;asuhe&apos;,
        &apos;asuka&apos;
    ]
}
console.log(obj.age); // 18
obj.age = 20;
console.log(obj.age); // 20
obj = {}; // TypeError


Object.freeze(obj)
obj.age = 30; 
console.log(obj.age); //20
obj.address = []; // 修改失败，但不报错
obj.address[0] = &apos;sphinx&apos;;
console.log(...obj.address); //&apos;sphinx&apos; &apos;asuhka&apos;
```

`Object.freeze()`内部还会调用`Object.seal()`方法，让被冻结的对象不能添加或删除属性

参考资料：

[[Object.freeze() vs const](https://stackoverflow.com/questions/33124058/object-freeze-vs-const)](https://stackoverflow.com/questions/33124058/object-freeze-vs-const)

[const vs Object.freeze() in JavaScript](https://alligator.io/js/const-vs-obj-freeze/)</content:encoded><author>Asuhe</author></item><item><title>VUE基础(二)</title><link>https://asuhe.org/blog/ca074dd1/</link><guid isPermaLink="true">https://asuhe.org/blog/ca074dd1/</guid><pubDate>Tue, 09 Nov 2021 22:53:35 GMT</pubDate><content:encoded>## Vue的生命周期

Vue总共有11个生命周期，下文只介绍8个生命周期

根据官网给的图可以大致将Vue的生命周期分为四个阶段：初始化阶段、挂载阶段、更新阶段、销毁阶段。而每个阶段又可以分为前、后两个阶段。

![vue生命周期](https://i.loli.net/2021/11/09/VJlqy2KYobWscwC.png)

### 初始化阶段

beforeCreate() 钩子可以让我们在初始化之前进行一些操作。在这个阶段，**数据还没有被代理到我们创建的实例中**，所以此时我们无法获取data里的数据。

created() 钩子可以让我们在初始化完成时进行一些操作。在这个阶段，**数据完成了代理**，此时我们可以获取 data中的数据。

```js
const vm = new Vue({
	el:&apos;#app&apos;,
    data(){
        return {
            msg:&apos;asuhe&apos;
        }
    },
    beforeCreate(){
        console.log(msg); //此时数据还未被代理无法获取msg属性的值，输出undefined
    },
    created(){
        console.log(msg); //数据已经被代理,输出asuhe
    }
})
```



### 挂载阶段

beforeMount()钩子可以让我们在DOM实际渲染到页面前进行一些操作，**这时挂载点的数据已经被渲染好了，但还没有实际插入页面，我们无法获取到DOM节点。**

mounted()钩子可以让我们的在DOM实际渲染到页面后进行一些操作，**此时DOM节点已经挂载，我们可以获取到DOM节点。**通常这时我们会向服务器发送请求获得数据、挂载事件等等

```js
&lt;div id=&quot;app&quot;&gt;
    &lt;p ref=&quot;pp&quot;&gt;&lt;/p&gt;
&lt;/div&gt;

const vm = new Vue({
	el:&apos;#app&apos;,
    data(){
        return {}
    },
    beforMount(){
        console.log(this.$refs.pp) //undefined
    },
    mounted(){
        console.log(this.$refs.pp) //&lt;p ref=&quot;pp&quot;&gt;&lt;/p&gt;
    }
})
```



### 更新阶段

当数据更新的时候，data配置项里的数据是马上更新的，但页面上的数据并不是能马上更新。所以有beforeUpdate()和updated()这两个钩子供我们在页面数据更新前后进行操作。

```js
const vm = new Vue({
	el:&apos;#app&apos;,
    data(){
        return {
            isOk:true
        }
    },
    
    beforUpdate(){
        console.log(this.$refs.pp) //输出空
    },
    updated(){
        console.log(this.$refs.pp) //&lt;p ref=&quot;pp&quot;&gt;&lt;/p&gt;
    }
})
```



### 销毁阶段

```js
&lt;div id=&quot;app&quot;&gt;
    &lt;p ref=&quot;pp&quot; v-show=&quot;isShow&quot;&gt;{{isShow?&apos;小香香&apos;:&apos;明日香&apos;}}&lt;/p&gt;
	&lt;button @click=&quot;destroy&quot;&gt;点击销毁实例&lt;/button&gt;
&lt;/div&gt;
const vm = new Vue({
    el:&apos;#app&apos;,
    data(){
        return {
            isShow:true
        }
    },
    methods: {
        destroy(){
            this.$destroy(); //这里触发两个销毁阶段的钩子，同步回调
            //最后执行
            console.log(&apos;正在清理&apos;);
        }
    },
    mounted(){
        this.timer = setInterval(()=&gt;{
            this.isShow = !this.isShow;
        },2000)
    },            
    //销毁阶段的两个钩子
    beforeDestroy() {
        //销毁阶段进行清理工作
        clearInterval(this.timer);
        this.timer = null;
        console.log(&apos;销毁了定时器&apos;);  
    },
    destroyed(){
        console.log(&apos;销毁完毕&apos;);
    }
})
//点击销毁按钮后输出顺序是: 销毁了定时器 销毁完毕 正在清理
```



## 自定义指令

自定义指令分为定义全局指令和局部指令，全局自定义指令在任何一个Vue实例中都可以使用，而局部自定义指令只能在特定的Vue实例中使用

### 全局自定义指令

  定义全局指令

  参数： 1、指令名称（不包含v- 只能是全小写） 2、回调函数

  （参数 1使用这个指令的那个节点,2这个指令使用的表达式的值以及表达式的集合）

```js
Vue.directive(&apos;upper&apos;,function(node,bindings){
    // console.log(node,bindings)
    node.textContent = bindings.value.toUpperCase();
})
```

### 局部自定义指令

```js
const vm = new Vue({
    el:&apos;#app&apos;,
    directives:{
        upper(node,bindings){
            
        }
    }
})
```

## 自定义过滤器

```js
Vue.filter(&apos;timeFormat&apos;,function(value,format=&apos;YYYY-MM-DD hh:mm:ss&apos;){
    return moment(value).format(format)
})
const vm = new Vue({
    el:&apos;#app&apos;,
    data(){
        return {
            timeNow: Date.now()
        }
    }
})
```</content:encoded><author>Asuhe</author></item><item><title>VUE基础(三)</title><link>https://asuhe.org/blog/b4f46dfa/</link><guid isPermaLink="true">https://asuhe.org/blog/b4f46dfa/</guid><pubDate>Thu, 11 Nov 2021 20:45:49 GMT</pubDate><content:encoded>## 组件间通信

### props

props组件是组件通信最常用最简单的方式。它**适用于父子组件之间进行通信**。父组件可以给子组件传递函数数据和非函数数据，当我们给子组件传递一个**非函数数据通常给子组件传递的是一个父组件配置对象里的一个属性，而不是具体的属性值**。子组件拿到了这个属性就是拿到了它的引用，从而可以开始操作属性值。当我们传递非函数数据时，本质上就是父组件给子组件传递数据，以供子组件使用。

当我们给子组件传递的是一个函数数据而时，本质上就是父组件想要得到子组件中收集来的数据，子组件通过给父组件传递过来的函数传递实参的方式，将收集到的数据给父组件。

使用props方法通信主要是运用在父子（嵌套）关系里，若想给兄弟组件传递数据就必须经过组件的共同祖先来中转再传递给兄弟组件，十分麻烦。它能够让父组件数据传递给子组件的原因是因为，站在父组件这个层级，父组件能够看到子组件，而子组件看上层组件是看不见的。从我们拆分的语法也可以很容易理解，当我们引入一个文件时，使用`import `引入文件的那个文件能够清楚地知道自己引用了哪些文件，而对于被引用的文件它其实并不知道自己被谁引用了。

```js
//子组件配置对象
{
    //第一种写法
    props:[&apos;传递过来的属性名&apos;]
    //第二种写法
    props:{
        传递过来的属性名:Function  //可以指定传递过来的数据类型，这里指定为Function
    }
    //第三种写法
    props:{
        传递过来的属性名:{
            type:Function //指定数据类型
            required:true //指定必须传递
            defaulte:value //指定如果没传数据，默认数据的value
        }
        
    }
}
```



### 自定义事件

自定义事件通信类似于`props`中父组件给子组件传递函数数据。自己定义事件和自定义回调函数，和系统事件触发相比。我们自己可以定义无数个事件类型，而系统事件类型是固定的；在系统事件的回调函数中，回调函数是由系统调用的，在自定义事件中回调函数是我们自己使用`$emit`函数触发的。

自定义事件**专门用于子组件向父组件通信，但不能用于父组件向子组件通信**。在组件的层级结构中，父组件是最顶层的可以看见下层的子组件，所以可以用`$on`来给子组件绑定事件，而子组件位于下层无法使用`$on`来给父组件绑定事件。这就导致了自定义事件这种通信类型无法用于父组件向子组件通信传递数据。总结起来就是：

* 接受数据的组件必须能看到预绑定事件的组件对象，才能绑定
* 发送数据的组件必须能看到绑定了事件的组件对象，才能触发事件

```js
//父组件 App.vue
&lt;Add ref=&quot;add&quot; &gt;&lt;/Add&gt;

{
    //挂载后才给子组件绑定事件
    monted(){
        this.$refs.add.$on(&apos;addUser&apos;,function(){  /*....*/ }); //给子组件绑定addUser事件
    }
}
------
//绑定事件简便写法
&lt;Add @addUser=&quot;addU&quot;&gt;&lt;/Add&gt;

//子组件 Add.vue
&lt;button @click=&quot;addSome&quot;&gt;&lt;/button&gt;

addSome(){
    let obj = {
        name:&apos;Asuhe&apos;,
        age:18
    }
    this.$emit(&apos;addUser&apos;,obj); //触发addUser事件
}

```

```js
//解绑事件
$off(&apos;EventName&apos;);
//绑定只能触发一次的事件
$once(&apos;EventName&apos;,callback);
```

子组件和父组件能够使用`$on`、`$emit`等函数是因为，子组件的实例化对象的`__proto__`会顺着原型链往上找到`Vue`方法的`prototype`，在`Vue`这个函数对象的`prototype`中有`$on`这些方法。

![原型链查找$on](https://i.loli.net/2021/11/13/bWnaiXxmFSDBozg.png)



### 全局事件总线通信

全局总线通信就是自定义事件通信的升级版，它也是利用原型链查找的特性来构建通信，它可以**适用任何通信场合**。使用全局事件总线通信首先我们要定义一个总线，这个总线是全部组件都可以通过原型链去查找到它，其次这个总线也要有能够使用`$on`和`$emit`函数的权利。只有符合上述两点的对象才能当作总线。根据上述自定义事件原型链，我们可以找到对象`vm`来充当总线。

```js
//在main.js中给vm的prototype添加一个$bus属性让其指向vm自身
beforeCreate(){
    vm.prototype.$bus = this; // 数据代理前就挂载总线
}
//App组件中让$bus绑定事件
mounted(){
    this.$bus.$on(&apos;Event_name&apos;,callback);
}

//子组件中触发$bus上的事件
this.$bus.$emit(&apos;Event_name&apos;,data);
```

通过原型链的查找，`$bus` → `vm` 和 `vm.__proto__` → `Vue.prototype`的环形查找，就可以使全局事件总线达到所有的组件对象都能找到它和可以调用`$on`和`$emit`的条件。

![全局事件总线](https://i.loli.net/2021/11/15/sCBtA2KWqNIYGy1.png)

完成全局事件总线的设置后，我们只需要将自定义事件绑定在全局事件总线中。在接收数据的组件中，获取到总线然后绑定事件，传入自己定义的回调函数。在发送数据的组件中，获取到总线然后触发事件，传入需要发送的数据。

![兄弟组件通信](https://i.loli.net/2021/11/15/kfdGEg4oN5ev7Cc.png)



### 消息订阅和发布

这种通信方式是通过使用第三方插件[`PubSubJS`](https://github.com/mroderick/PubSubJS)的方式来达到通信目的。其使用方法类似于全局事件总线。适用于任何组件之间进行通信，**但是互相通信的组件之间都需要分别引入该插件**

**接收数据的组件订阅事件源(类似`$emit`)，发送数据的组件绑定事件源(类似`$on`)。**

### xxxxxxxxxx11 1Vue.filter(&apos;timeFormat&apos;,function(value,format=&apos;YYYY-MM-DD hh:mm:ss&apos;){2    return moment(value).format(format)3})4const vm = new Vue({5    el:&apos;#app&apos;,6    data(){7        return {8            timeNow: Date.now()9        }10    }11})js

当一个组件会被多次使用，且里面大部分内容不变仅有非常少部分的结构改变时，可以使用slot插槽，将其理解为占位符。该种通信方式适用于父组件给子组件传递数据，但它与其它通信不同的是，它可以传递结构给子组件，子组件中的`slot`标签本质上就是一个占位符。若父组件给其传递`template`则使用父组件传递过来的`template`，否则使用默认定义的`template`。插槽又分为默认插槽、具名插槽和作用域插槽。

**默认插槽约定成俗只能有一个，具名插槽就是在默认插槽的基础上加上`name`属性唯一标识这个插槽**，这样父组件在传递数据的时候可以根据名字精准传递到指定的插槽中。

作用域插槽中子组件的slot可以通过 属性传递值给父组件，然后父组件可以根据不同需求改变这个slot内部的显示结构，把子组件的值，传给父组件固定的区域进行操作。父组件的数据是给子组件展示的。子组件展示过程当中，数据的结构由父组件决定的。

默认插槽和具名插槽

```js
//子组件放置插槽
&lt;slot&gt;&lt;/slot&gt;
&lt;slot name=&quot;asu&quot;&gt;&lt;/slot&gt;


//父组件传递数据
&lt;template&gt;
    &lt;button&gt;点击&lt;button&gt;
&lt;/template&gt;
&lt;template slot=&quot;asu&quot;&gt;
    &lt;a href=&quot;https://asuhe.fun&quot;&gt;&lt;/a&gt;
&lt;/template&gt;
```

作用域插槽

```js
//父组件
&lt;template slot-scope=&quot;{todo,index}&quot;&gt;
	&lt;span v-if=&quot;todo.isOver&quot; style=&quot;color:hotpink&quot;&gt;√ {{todo.content}}&lt;/span&gt;  //父组件控制子组件的样式
&lt;/template&gt;

//子组件
&lt;slot :todo=&quot;todo&quot; :index=&quot;index&quot;&gt;
    {{todo.content}}
&lt;/slot&gt;
```



## Vuex通信

vuex是一个vue官方推出的状态管理插件。它采用集中式存储管理应用中所有组件的状态，并以相应的规则保证状态可以以一种可预测的方式发生变化。我们可以将它认为是一种组件间通信的方式，**它适用于任何组件间通信。**它能够对应用中多个给组件共享的状态（数据）进行集中式的管理（读写）。

在以往当有多个视图（组件）依赖统一个状态时，或者说来自不同视图的行为需要变更同一状态，从前我们都是将数据以及操作定义在父组件，将数据以及操作数据的行为传递给各个需要使用这些数据和行为的子组件（有时可能需要多级传递）。这种方式去管理状态，当应用比较复杂时传递逻辑就会逐渐变得复杂，且代码后期维护也会变得更加困难，为了解决这个问题所以就出现了Vuex。Vuex就类似于一个数据管理的仓库，它存储着数据所有关于数据的操作都定义在它内部，当需要使用数据时仅需调用它提供的接口即可，这就是集中式管理状态。

#### Vuex的五个核心概念

* state：代表数据的初始状态，本质上就是包含多个属性的对象，属性的值只能是数据，不能是函数这样的东西
* getters：代表计算属性数据，是一个包含n个计算属性方法的对象，**处理同步任务**
* actions：代表用户行为，是一个包含多个方法的对象，方法里面可以进行for、if等等判断操作。**作为对用户提供操作数据的接口，处理异步任务**
* mutations：代表数据操作，是一个包含多个方法的对象，方法里面只能进行纯粹的数据操作不能进行if等判断操作，**作为actions操作数据的接口**
* Modules：代表事件处理，用于多模块时使用

#### 基本使用步骤

* 引入vuex并声明
* 向外暴露一个Store对象
* 将暴露出去的Store对象引入到实例化Vue的配置对象当中使用，Vue.use全局注册
* 在store对象中定义好五个核心概念

![Vuex作用原理](https://i.loli.net/2021/11/18/hzjoT9EBHpgZ2V4.png)

**所有从state里取出的数据都应该放在computed中，根据state中的数据变化而变化。若放在data中则会导致只能拿到初始的数据。**</content:encoded><author>Asuhe</author></item><item><title>Javascript执行上下文</title><link>https://asuhe.org/blog/698e82d4/</link><guid isPermaLink="true">https://asuhe.org/blog/698e82d4/</guid><pubDate>Fri, 12 Nov 2021 17:24:27 GMT</pubDate><content:encoded>## 执行上下文

在了解什么叫执行上下文之前我们需要区分一个小概念那就是程序执行和代码执行。我们的代码在执行之前是需要有一个环境的，就像我们做饭一样，做饭之前需要有做饭的环境就是厨房。代码执行就相当于做饭，做饭需要各种烹饪工具，而程序执行就是给代码执行创建一个可以让代码执行环境，相当于给一个厨房。程序执行的时候会在内存里面开辟空间存放各种代码执行需要的东西。**而程序在解析和运行的时候也需要依赖一些环境，这些程序执行所依赖和使用的环境就叫做执行上下文。**

执行上下文根据范围的不同分为全局执行上下文和函数执行上下文。它是根据js的作用域来区分的，因为一般来说js就分为两种作用域，一种是全局作用域也就是最外层的作用域，另一种就是函数作用域。而上面两种执行上下文就是和这些作用域一一对应的。

程序执行时主要做三件事：

* 收集变量，生成**变量对象**（预解析是其中的一个步骤）
* 确定 this 指向
* 确定作用域链

有了这三步以后，我们的代码才能执行。其中第一步的生成变量对象实际上就是把我们声明的那些 var 变量 和函数都打包装进一个对象里面。在全局执行上下文运行的时候那些在全局作用域下的 var变量和声明的函数都会收集进 global 对象里，**global就是一个全局变量对象**。

```js
var a = 10;
console.log(window);
```

![var a在全局作用域内](https://i.loli.net/2021/11/12/JgDQXyECYvZdj7w.png)

## 执行上下文调用栈

程序为了管理执行上下文（确保程序的执行顺序）所创建的一个栈数据结构，被称作执行上下文栈。本质上为了确保程序按预定的调用顺序执行，就使用栈这种结构来保存管理执行上下文。

```js
function foo(){
    var a = 10;
    function fn(){
        console.log(a);
    }
    fn();
}
foo();
```

![调用foo函数时的执行上下文调用栈](https://i.loli.net/2021/11/12/ntSeWVB36G7DRm1.png)



程序开始执行：（全局环境和函数环境）

全局执行上下文（分为创建阶段和执行阶段）代码开始执行之前和之后

​	1、全局执行上下文压入执行上下文栈）
​	创建上下文阶段：
​		1、收集变量形成变量对象 （函数 var的变量会收集）
​		预解析（其实在创建变量对象的时候已经做了预解析）
​		2、确定this指向（可以认为确定执行者）
​		3、创建自身执行上下文的作用域链
​		注意：同时确定函数在调用时候的上级作用域链。（根据ECMA词法去确定，看内部是否引用外部变量确定）

​	2、执行全局执行上下文
​		执行全局上下文阶段
​		为变量真正赋值
​		顺着作用域链查找要使用的变量或者函数执行

函数执行上下文
	1、函数执行上下文压栈
		1、收集变量  （var 形参  arguments  函数）
		2、确定this指向（可以认为确定执行者）
		3、创建自身执行上下文的作用域链
		注意：同时确定函数在调用时候的上级作用域链。（根据ECMA词法去确定，看内部是否引用外部变量确定）
		函数的作用域链：  自己定义的时候已经确定了函数在调用时候的上级作用域链，因此，在函数调用的时候，只需要将自己的变量对象添加到上级作用域链的顶端；就形成自己的作用域链
		
	2、执行函数执行上下文
		为变量真正赋值
		顺着作用域链查找要使用的变量或者函数执行

## 预解析

预解析就是我们平常所说的变量提升。它会先解析函数，在解析变量。如果有函数重名，则函数覆盖。如果有变量重名则忽略。

### 两个函数重名

函数重名，函数覆盖

```js
foo();  // sphinx
function foo(){
    console.log(&apos;asuhe&apos;);
}

function foo(){
    console.log(&apos;sphinx&apos;);
}
```

### 变量与变量重名

重名变量的值被覆盖

```js
var a = 10;
var a = 20;
console.log(a); // 20
```

### 函数与变量重名

函数和变量重名，则忽略变量

```js
function a(){
    console.log(&apos;asuhe&apos;);
}
var a;
a(); // asuhe
```

但是如果同名的变量在函数调用之前被赋值了，则会覆盖原来的函数

```js
function a(){
    console.log(&apos;asuhe&apos;);
}
a(); // asuhe
var a = 10;
a(); // TypeError 此时a已经不是函数了，被覆盖成了10
```

```js
var c = 1;
function c(c){
    console.log(c);
    var c = 3;
}
c(2);  //TypeError

--------
//浏览器执行时，上述代码被翻译为
function c(c){
    var c;
    console.log(c);
    c = 3;
}
var c;
c = 1;
c(2);

--------
//变形 1
function c(c){
    console.log(c);
    var c = 3;
}
c(2);  // 2

-------
//变形 2
function c(c){
    var c = 3;
    console.log(c);
}
c(2);  // 3

------
//变形 3
function c(c){
    console.log(c);
    let c = 3;
}
c(2);  // SyntaxError
```

#### 小测验

```js
var a;
function a() {}

console.log(typeof a) // function
```

```js
var fn = function () {
    console.log(fn)
}
fn() // fn函数体
--------
function fn(){console.log(fn)};
fn(); // fn函数体
```





## 作用域

作用域就是用来确定确定变量起作用的范围的。**作用域在代码定义的时候确定死了，而不是在执行的时候确定。**作用域的主要作用就是隔离变量，防止变量命名污染。

```js
var a = 10;
function fn(){
    console.log(a)
};
function foo(){
    var a = 20;
    fn();
}
foo(); // 10
```

而且在 js 中只有全局作用域和函数作用域这两种作用域。像对象和循环等等带有 { } 的都不存在作用域，不能限定变量范围

```js
var obj = {
    fn:function(){
        console.log(fn);
    }
}
obj.fn(); // ReferenceError
-------
if(true){
    function foo(){
        console.log(&apos;asuhe&apos;);
    }
}else{
    function fn(){
        console.log(&apos;sphinx&apos;);
    }
};
foo(); // asuhe
console.log(fn); // undefined fn会被当做变量提升，但是不会获得定义
```



## 作用域链

作用域链是使用执行上下文当中变量对象所组成的链条结构（数组结构）是真实存在的，查找的时候其实真正是先去自身的变量对象当中查找，如果没有，去上级执行上下文的变量对象当中去查找，直到找到全局执行上下文的变量对象；  函数调用的时候上一级的变量对象其实是在函数定义的时候都已经确定好的。

当一个内部作用域引用外部作用域的变量时，外部作用域会被加入内部作用域变量对象的作用域链数组中，否则不加入。

```js
var b = 20;
var c = 30;
function fn(){
    var a = 10;
    function fn1(){
        console.log(a,b,c);
    }
    fn1();
}
fn(); // 10 20 30
//此时因为fn1内引用了fn内的变量，fn的变量对象会被加入fn1的作用域链以便查找变量
```

![fn变量对象被加入fn1的作用域链数组](https://i.loli.net/2021/11/12/2HTfuOckJGvqobn.png)



```js
var b = 20;
var c = 30;
function fn(){
    var a = 10;
    function fn1(){
        console.log(b,c);
    }
    fn1();
}
fn(); //20 30
```

![fn变量对象被跳过](https://i.loli.net/2021/11/12/pOb7qZ4CwWiex8n.png)</content:encoded><author>Asuhe</author></item><item><title>数组花式遍历</title><link>https://asuhe.org/blog/ddee7727/</link><guid isPermaLink="true">https://asuhe.org/blog/ddee7727/</guid><pubDate>Thu, 11 Nov 2021 21:07:47 GMT</pubDate><content:encoded>## for遍历

for遍历是最基础最简单的一种遍历数组的方式，在js中的数组其实并不是其它语言通常意义上的数组。它实际上只是一个带有length属性的对象，当我们使用for循环加上index去遍历数组时，本质上是使用`obj[属性名]`的方式去遍历的，数组里它的属性名默认是从 0 开始增长的

```js
let arr = [1,2,3,44,5];
for(let index = 0; index &lt; arr.length;index++){
    console.log(arr[index]);
}
```

当我我们使用`Object.keys()`方法去获取一个数组的值时，实际上它只会返回从 0 开始的属性名组成的新数组。

```js
let arr = [1,2,3,4,5];
arr[&apos;5&apos;] = 6;
console.log(arr.length) // 6
arr[7] = 20;
console.log(arr.length) // 8 
console.log(arr) // [ 1, 2, 3, 4, 5, 6, &lt;1 empty item&gt;, 20 ]

let keys = Object.keys(arr);
console.log(keys); // [&apos;0&apos;, &apos;1&apos;, &apos;2&apos;, &apos;3&apos;, &apos;4&apos;, &apos;5&apos;, &apos;7&apos;]
```

当我们使用 [ ] 去获取对象属性的值时，实际上 [ ] 上的内容都会被强制转换成字符串的形式。也就是说 [ 7 ] 等价于 [ &apos;7&apos; ]。

## for in 遍历

使用 for in 适合用于遍历一个对象，但它效率极低。因为它不仅会遍历对象里的属性，而且还会延着原型链一直遍历所有原型的属性。但是由于原型上属性一般都是不可枚举的，所以通常不会被它枚举出来。但是若你给原型对象配置了一个可以枚举的属性，它就会将它枚举出来。遍历对象的属性都是随机的，根据引擎的不同其输出的顺序也不同

```js
let obj = {
    name:&apos;zhangsan&apos;,
    age:18
}
for(const key in obj){
    console.log(key);
} // name age

//给原型对象添加一个可枚举属性
Object.prototype.sex = &apos;male&apos;;
for(const key in obj){
    console.log(key);
} // name age sex   它将sex也遍历出来了
```



## for of

for of 用于遍历一个带有**迭代器**的对象。而通常内置的对象都会有一个迭代器供其使用。而我们自定的对象除非我们给它手动添加迭代器，否则是无法使用for of 遍历的。

![数组的迭代器](https://i.loli.net/2021/11/11/hYeG67q84KaZWAr.png)

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 对象。

## forEach

`forEach`是数组内置的一个遍历方法，它的是个高效的数组内置的遍历方法。`forEach` 不会直接改变调用它的对象，但是那个对象可能会被 `callback` 函数改变</content:encoded><author>Asuhe</author></item><item><title>Diff算法运行原理</title><link>https://asuhe.org/blog/6c66e211/</link><guid isPermaLink="true">https://asuhe.org/blog/6c66e211/</guid><pubDate>Mon, 15 Nov 2021 09:00:52 GMT</pubDate><content:encoded>## 虚拟DOM和Diff算法

虚拟DOM产生是因为真实DOM的体积通常非常大，当需要进行页面更新时若使用真实DOM进行比较会消耗大量时间和内存，效率十分低下。而如果使用虚拟DOM，仅需要保留必要的属性，进行比较时速度会快上很多而且所占资源也非常少。

### Diff算法运行原理

Diff算法进行比较时最小的比较粒度是一个标签，当一个标签发生变化时Diff就会运行进行新旧虚拟DOM的比较。

### key的作用

key值是作为虚拟DOM对象的标识，在更新显示时key值作为索引去拿去和旧DOM对象比较。

当状态中的数据发生变化时，vue会根据新数据的生成新的DOM，随后会拿新DOM和旧DOM进行Diff比较。

* 虚拟DOM中找到了与新虚拟DOM相同的key：
  * 若虚拟DOM中内容没有变化，直接使用之前的真实DOM
  * 若虚拟DOM中的内容发生变化，则生成新的真实DOM
* 旧虚拟DOM中没有找到与新虚拟DOM相同的key，直接根据数据创建新的真实DOM渲染到页面。只会创建新key值虚拟DOM的真实DOM

### 用index做key可能引发的问题

* 若对数据进行逆序添加、逆序删除等会破坏数据顺序的操作，会产生不必要的真实DOM更新，虽然界面显示没有问题，但是效率会很低
* 如果结构中还包含输入类的DOM则会产生错误的DOM更新，最常见的就是导致了页面输入DOM中的数据错乱</content:encoded><author>Asuhe</author></item><item><title>手撸一个Promise</title><link>https://asuhe.org/blog/3453bdfe/</link><guid isPermaLink="true">https://asuhe.org/blog/3453bdfe/</guid><pubDate>Fri, 19 Nov 2021 08:21:45 GMT</pubDate><content:encoded>## Promise构造函数

Promise构造函数需要传的参数就是一个executor构造器函数。executor里需要传resolve和reject函数，用于改变Promise实例的状态

* 功能：立即执行executor代码，并且传递出两个函数用于改变Promise实例状态
* 参数：resolve函数和reject函数
* 返回值：无

```js
//定义状态
const PENDING = &apos;pending&apos;;
const FULFILLED = &apos;fulfilled&apos;;
const REJECTED = &apos;rejected&apos;;

function Promise(executor){
    const that = this; //保证this指向实例
    this.status = PENDING; //初始化状态
    this.data = undefined; //保存成功或失败回调的值
    this.callbacks = []; //保存回调队列，用{onResolve,onReject}对象存储指定的回调函数队列
 	//更改实例状态为成功的函数
    function resolve(value){
        //判定当前状态是否为pending，若不是则不允许第二次更改状态
        if(that.status !== PENDING) return ;
        //将实例状态更改为fulfilled并保存传入的value
        that.status = FULFILLED;
        that.data = value;
        //检查回调队列有没有回调函数需要调用
        if (that.callbacks.length &gt; 0) {
            //将成功回调推入微队列异步执行
            queueMicrotask(() =&gt; {
                that.callbacks.forEach(cb =&gt; cb.onResolve(that.data));
            });
        }
    }
 	//更改实例状态为失败的函数
    function reject(reason){
        //判定当前状态是否为pending，若不是则不允许第二次更改状态
        if(that.status !== PENDING) return ;
        //将实例状态更改为rejected并保存传入的reason
        that.status = REJECTED;
        that.data = reason;
        //检查回调队列有没有回调函数需要调用
        if (that.callbacks.length &gt; 0) {
            //将失败回调推入微队列异步执行
            queueMicrotask(() =&gt; {
                that.callbacks.forEach(cb =&gt; cb.onResolve(that.data));
            });
        }
    }
    //同步执行executor，并向外传递出改变实例状态的函数
    executor(resolve,reject);
}
```



## Promise.prototype.then()

* 功能：给实例对象指定成功和失败的回调
* 参数：成功的回调函数和失败的回调函数
* 返回值：return一个新的Promise实例
* 特性：return的Promise实例状态根据传入回调函数的执行结果决定
* return的Promise状态影响因素
  1. 回调函数执行抛出异常，return的新Promise状态为rejected，reason为该异常
  2. 回调函数返回非Promise值，return的新Promise状态为fulfilled，value为回调函数的返回值
  3. 回调函数返回Promise值，return的新Promise状态和值都跟随返回的Promise

```js
Promise.prototype.then = function(onResolve,onReject){
    const that = this;
    //判定传入的成功回调是否为函数，若不是则强制改为函数
    onResolve = typeof onResolve === &apos;function&apos; ? onResolve : value =&gt; value ;
    //判定传入的失败回调是否为函数，若不是则原样抛出异常，实现异常穿透
    onReject = typeof onReject === &apos;function&apos; ? onReject : reason =&gt; {throw reason};
    return new Promise((resolve,reject)=&gt;{
        //定义handle函数处理所有能影响return的Promise状态的情况
        function handle(callback){
            	//1.若回调函数执行抛出异常则捕获异常
                try{
                    const res = callback(that.data); //接收成功回调的返回值
                    //判断返回值类型并处理
                    if(res instanceof Promise){
                        //3.根据res的状态决定return的Promise状态
                        res.then(resolve,reject);
                    }else{ //2.非Promise返回值直接返回
                        resolve(res);
                    }
                }catch(error){
                    reject(error);
                }
        }
        //若此时是先指定了回调函数，后面改变的状态则将回调放入回调队列
        if(that.status === PENDING){
            that.callbacks.push(
                {
                    onResolve(value){
                        handle(onResolve);
                    },
                    onReject(reason){
					  handle(onReject);
                    }
                }
            )
        }else if(that.status === FULFILLED){ //若先改变了状态，则直接将回调函数推入微队列执行
            queueMicrotask(()=&gt;{
                handle(onResolve);   
            })
        }else{
            queueMicrotask(()=&gt;{
                handle(onReject); 
            })
				
        }  
    })
}
```



## Promise.prototype.catch()

* 功能：给实例对象指定失败的回调
* 参数：失败的回调函数
* 返回值：return一个新的Promise实例
* 特性：return的Promise实例状态根据传入回调函数的执行结果决定
* return的Promise状态影响因素
  1. 回调函数执行抛出异常，return的新Promise状态为rejected，reason为该异常
  2. 回调函数返回非Promise值，return的新Promise状态为fulfilled，value为回调函数的返回值
  3. 回调函数返回Promise值，return的新Promise状态和值都跟随返回的Promise

```js
Promise.prototype.catch = function(onReject){
    return this.then(undefined,onReject);
}
```



## Promise.resolve()

* 功能：根据传入的value返回一个已经改变了状态的Promise
* 参数：value
* 返回值：return一个新的Promise实例
* 特性：return的Promise实例状态根据传入value决定
* return的Promise状态影响因素
  2. value是一个非Promise，return的新Promise状态为fulfilled，value为回调函数的返回值
  3. value是一个Promise，return的新Promise状态和值都跟随返回的Promise

```js
//仅需根据then中handle函数的逻辑重写一遍即可
Promise.resolve = function(value){
    return new Promise((resolve,reject)=&gt;{
        if(value instanceof Promise){
            value.then(resolve,reject);
        }else{
            resolve(value);
        }
    })
}
```



## Promise.reject()

* 功能：根据传入的value返回一个已经改变了状态的Promise
* 参数：reason
* 返回值：return一个新的rejected状态的Promise实例
* 特性：return的Promise实例状态一定是rejected

```js
Promise.reject = function(reason){
    return new Promise((resolve,reject)=&gt;{
        if(reason instanceof Error){
            reject(reason.message);
        }else if(reason instanceof Promise){
            reject(reason.data);
        }else{
           	reject(reason);
        }
    })
}
```



## Promise.all()

* 功能：根据传入的Promise数组，若该数组的Promise最后全都成功则返回一个fulfilled的Promise，否则返回一个rejected的Promise
* 参数：Promise数组
* 返回值：return一个Promise实例
* 特性：return的Promise实例状态根据传入的Promise数组决定

```js
Promise.all = function(promiseArr){
    const values = [];//定义一个values保存成功Promise的value的值
    let count = 0; //记录有几个Promise成功
    return new Promise((resolve,reject)=&gt;{
        //遍历数组所有元素
        promiseArr.forEach((p,index)=&gt;{
           Promise.resolve(p).then( //用Promise.resolve包裹，处理数组中有非Promise值的情况
                value =&gt; {
                    ++count;
                    values[index] = value;
                    if(count === promiseArr.length) resolve(values);
                },
                reason =&gt; reject(reason))
        });
    })
}
```



## Promise.race()

* 功能：根据传入的Promise数组，return的Promise状态跟随第一个完成的Promise
* 参数：Promise数组
* 返回值：return一个Promise实例
* 特性：return的Promise实例状态跟随第一个完成的Promise

```js
Promise.race = function(promiseArr){
    return new Promise((resolve,reject)=&gt;{
        promiseArr.forEach((p)=&gt;{
            Promise.resolve(p).then( //用Promise.resolve包裹，处理数组中有非Promise值的情况
                value =&gt; resolve(value),
                reason =&gt; reject(reason)
            )
        })
    })
}
```



## 完整代码

### ES5版

```js
(function (w) {
    const PENDING = &apos;pending&apos;;
    const FULFILLED = &apos;fulfilled&apos;;
    const REJECTED = &apos;rejected&apos;;
    function Promise(executor) {
        const that = this;
        that.status = PENDING;
        that.data = undefined;
        that.callbacks = [];
        function resolve(value) {
            if (that.status !== PENDING) return;
            that.status = FULFILLED;
            that.data = value;
            if (that.callbacks.length &gt; 0) {
                queueMicrotask(() =&gt; {
                    that.callbacks.forEach(cb =&gt; cb.onResolve(that.data));
                });
            }
        };
        function reject(reason) {
            if (that.status !== PENDING) return;
            that.status = REJECTED;
            that.data = reason;
            if (that.callbacks.length &gt; 0) {
                queueMicrotask(() =&gt; {
                    that.callbacks.forEach(cb =&gt; cb.onReject(that.data));
                });
            }
        };
        executor(resolve, reject);
    };

    Promise.prototype.then = function (onResolve, onReject) {
        const that = this;
        onResolve = typeof onResolve === &apos;function&apos; ? onResolve : value =&gt; value;
        onReject = typeof onReject === &apos;function&apos; ? onReject : reason =&gt; { throw reason };
        return new Promise((resolve, reject) =&gt; {
            function handle(callback) {
                try {
                    const res = callback(that.data);
                    if (res instanceof Promise) {
                        res.then(resolve, reject);
                    } else { 
                        resolve(res);
                    }
                } catch (error) {
                    reject(error);
                }
            }
            if (that.status === PENDING) {
                that.callbacks.push(
                    {
                        onResolve(value) {
                            handle(onResolve);
                        },
                        onReject(reason) {
                            handle(onReject);
                        }
                    }
                )
            } else if (that.status === FULFILLED) {
                queueMicrotask(() =&gt; {
                    handle(onResolve);
                })
            } else {
                queueMicrotask(() =&gt; {
                    handle(onReject);
                })
            };
        });
    };

    Promise.prototype.catch = function (onReject) {
        return this.then(undefined, onReject);
    };

    Promise.resolve = function (value) {
        return new Promise((resolve, reject) =&gt; {
            if (value instanceof Promise) {
                value.then(resolve, reject);
            }else {
                resolve(value);
            }
        })
    };

    Promise.reject = function (reason) {
        return new Promise((resolve, reject) =&gt; {
            if (reason instanceof Error) {
                reject(reason.message);
            } else if (reason instanceof Promise) {
                reject(reason.data);
            } else {
                reject(reason);
            }
        })
    };

    Promise.all = function (promiseArr) {
        const values = [];
        let count = 0;
        return new Promise((resolve, reject) =&gt; {
            promiseArr.forEach((p, index) =&gt; {
                Promise.resolve(p).then(
                    value =&gt; {
                        ++count;
                        values[index] = value;
                        if (count === promiseArr.length) resolve(values);
                    },
                    reason =&gt; reject(reason))
            });
        })
    };

    Promise.race = function (promiseArr) {
        return new Promise((resolve, reject) =&gt; {
            promiseArr.forEach((p) =&gt; {
                Promise.resolve(p).then(
                    value =&gt; resolve(value),
                    reason =&gt; reject(reason)
                )
            })
        })
    };

    w.Promise = Promise;
})(window)
```

### ES6版

```js
(function (w) {
    const PENDING = &apos;pending&apos;;
    const FULFILLED = &apos;fulfilled&apos;;
    const REJECTED = &apos;rejected&apos;;
    class Promise {
        constructor(executor) {
            const that = this;
            that.status = PENDING;
            that.data = undefined;
            that.callbacks = [];
            function resolve(value) {
                if (that.status !== PENDING) return;
                that.status = FULFILLED;
                that.data = value;
                if (that.callbacks.length &gt; 0) {
                    queueMicrotask(() =&gt; {
                        that.callbacks.forEach(cb =&gt; cb.onResolve(that.data));
                    });
                }
            }
            function reject(reason) {
                if (that.status !== PENDING) return;
                that.status = REJECTED;
                that.data = reason;
                if (that.callbacks.length &gt; 0) {
                    queueMicrotask(() =&gt; {
                        that.callbacks.forEach(cb =&gt; cb.onReject(that.data));
                    });
                }
            }
            executor(resolve, reject);
        };
        then(onResolve, onReject) {
            const that = this;
            onResolve = typeof onResolve === &apos;function&apos; ? onResolve : value =&gt; value;
            onReject = typeof onReject === &apos;function&apos; ? onReject : reason =&gt; { throw reason };
            return new Promise((resolve, reject) =&gt; {
                function handle(callback) {
                    try {
                        const res = callback(that.data);
                        if (res instanceof Promise) {
                            res.then(resolve, reject);
                        } else {
                            resolve(res);
                        }
                    } catch (error) {
                        reject(error);
                    }
                }
                if (that.status === PENDING) {
                    that.callbacks.push(
                        {
                            onResolve(value) {
                                handle(onResolve);
                            },
                            onReject(reason) {
                                handle(onReject);
                            }
                        }
                    )
                } else if (that.status === FULFILLED) {
                    queueMicrotask(() =&gt; {
                        handle(onResolve);
                    })
                } else {
                    queueMicrotask(() =&gt; {
                        handle(onReject);
                    })
                };
            });
        };
        catch(onReject) {
            return this.then(undefined, onReject);
        };
        static resolve(value) {
            return new Promise((resolve, reject) =&gt; {
                if (value instanceof Promise) {
                    value.then(resolve, reject);
                }else {
                    resolve(value);
                }
            })
        };
        static reject(reason) {
            return new Promise((resolve, reject) =&gt; {
                if (reason instanceof Error) {
                    reject(reason.message);
                } else if (reason instanceof Promise) {
                    reject(reason.data);
                } else {
                    reject(reason);
                }
            })
        };
        static all(promiseArr) {
            const values = [];
            let count = 0;
            return new Promise((resolve, reject) =&gt; {
                promiseArr.forEach((p, index) =&gt; {
                    Promise.resolve(p).then(
                        value =&gt; {
                            ++count;
                            values[index] = value;
                            if (count === promiseArr.length) resolve(values);
                        },
                        reason =&gt; reject(reason))
                });
            })
        };
        static race(promiseArr) {
            return new Promise((resolve, reject) =&gt; {
                promiseArr.forEach((p) =&gt; {
                    Promise.resolve(p).then(
                        value =&gt; resolve(value),
                        reason =&gt; reject(reason)
                    )
                })
            })
        };
    }
    w.Promise = Promise;
})(window)
```</content:encoded><author>Asuhe</author></item><item><title>一文搞定闭包</title><link>https://asuhe.org/blog/9f7b1b1d/</link><guid isPermaLink="true">https://asuhe.org/blog/9f7b1b1d/</guid><pubDate>Fri, 12 Nov 2021 22:40:08 GMT</pubDate><content:encoded>## 什么是闭包

闭包就是在一个变量对象里持有另一个变量对象里内容的引用时，就会产生闭包。常见的表现形式就是，内部函数持有外部函数的变量，我们可以通过返回内部函数去让更外层的作用域能够访问到内部函数的父函数里的变量。

```js
function foo(){
    var a = 10;
    function fn(){
        a++; // fn持有外层作用域foo函数里的变量a
        console.log(a);
    }
    return fn;
}
var f = foo();  // 最外层作用域获得fn的引用
f(); // 11
f(); // 12
f(); // 13

var f2 = foo();
f2(); // 11
f2(); // 12

```

![调用fn后产生的闭包](https://i.loli.net/2021/11/13/CEaLXeArRsUD5nh.png)

原本执行完foo()，Stack里foo的执行上下文的变量对象就要销毁。但是由于全局作用域的变量 f 持有其内部函数 fn 的引用而 fn 又持有变量 a 的引用。所以foo的执行上下文的变量对象一直不能释放回收，当我们调用 f() 时依然可以访问到变量 a。

![闭包内存示意图](https://i.loli.net/2021/11/13/Smqr31wDnj9QzVa.png)

 **本质上所谓的闭包是一个引用关系，该引用关系存在于内部函数中，内部引用的是外部函数(外部作用域)的变量的对象**

## 为什么会形成闭包

闭包的形成需要三个条件：

* 函数嵌套，本质上就是函数作用域的嵌套
* 内部函数持有外部函数的局部变量
* 外部函数被使用

函数嵌套我们很好理解，因为函数的作用域在函数声明的时候就已经确定，所以函数要嵌套声明。内部函数要持有外部函数变量是因为如果内部函数没有持有外部函数变量，则内部函数在形成变量对象的时候并不会把外部函数的变量对象加入作用域链中，会直接越过它。这一点我们在{% post_link &apos;2021-11-12-js执行上下文&apos; &apos;js执行上下文机制&apos;%}一文中已经详细说明。上面两条我们只是声明了函数并没有调用它，所以第三条外部函数被调用也就很好理解。

当出现了符合上述三个条件的情况时，就会产生闭包。但在实际运行环境中，内部函数也要调用或者引用才会产生Closure。这是因为部分浏览器会对内部函数做优化，当内部函数不使用或者不引用时我们去调试它并不会产生一个Closure对象。因为我们不去使用内部函数就相当于在代码执行过程中没有具体地体现出这种引用关系，尽管我们在定义的时候形成了这种引用关系，所以为了节省内存开销浏览器就不会生成Closure。

```js
function foo(){
    var a = 10;
    function fn(){
        a++; // fn持有外层作用域foo函数里的变量a
        console.log(a);
    }
    fn();
}
foo(); // 11
```

上面代码中同样会产生闭包，和第一个例子的代码相比只是我们不能多次访问而已。每调用一次外部函数就会产生一个Closure

![调用foo依然产生闭包](https://i.loli.net/2021/11/13/CU8XPdmNkKxH1aF.png)

以函数`fn`的视角来看，我们将函数`foo`里面的变量`a`称为**自由变量**，因为它并未在`fn`中定义但它却在`fn`中使用了。所谓**自由变量**即在**当前作用域里未被定义但却使用了的变量，它在查找时会向上级作用域逐级查找直到找到为止，若最终在全局作用域中都未找到则报错*xxx is not defined***

一般闭包的产生必定伴随着**自由变量**的产生，所以当我们确定闭包里变量的值时我们可以套用**自由变量**的查找规则：**所有自由变量的值在查找时都是按照在定义阶段的上级作用域里查找，而不是执行阶段查找。**

## 闭包的生命周期

产生: **在嵌套内部函数定义完时就产生了(不是在调用)，因为本质上闭包就是一种引用关系。**当外部函数调用的时候，浏览器就会具体产生一个Closure

死亡: 在嵌套的内部函数成为垃圾对象时，也就是引用关系断裂的时候闭包就会死亡

## 闭包的作用

闭包可以延长外部函数变量对象的生命周期，而且它也让函数外部也可以间接操作到函数内部的变量。虽然闭包可以将外部函数的变量对象保留下来，但是浏览器为了性能后期将外部函数中不被内部函数使用的变量清除了。

```js
function foo(){
    var a = 10;
    var b = 20;
    function fn(){
        console.log(a);  //只使用变量a
    }
    return fn;
}
var f = foo();
f(); // 10
```

![不被内部函数引用的变量被清除](https://i.loli.net/2021/11/13/ORX4QSE72Zultqx.png)



## 闭包的缺点

因为闭包的关系，外部函数的变量对象不会在执行完后就马上被销毁。这就导致了内存泄漏的问题，如果我们不去手动释放那么这个变量对象就会一直存在于内存中。当内存泄漏过多就会让页面越来越卡，最后导致内存溢出，程序崩溃。但是要解决这个问题的方式也很简单，那就是让持有引用的变量置为 null，把引用关系断了就可以让GC去正常回收内存。

## 应用闭包

js的模块化就用到了闭包，使用它可以防止全局变量污染。多个模块引入的同时不会产生因为变量重名而覆盖的问题。

此外还有一种经典用法就是让闭包去保存索引号

```html
&lt;!-- 使让点击每个li都会输出其数组下标的位置 --&gt;
&lt;body&gt;
    &lt;ul&gt;
        &lt;li&gt;输出0&lt;/li&gt;
        &lt;li&gt;输出1&lt;/li&gt;
        &lt;li&gt;输出2&lt;/li&gt;
        &lt;li&gt;输出3&lt;/li&gt;
    &lt;/ul&gt;
&lt;script&gt;
    var btns = document.querySelectorAll(&apos;li&apos;);
    for(var index = 0;index&lt;btns.length;index++){
        //利用闭包保存外部传入的 index值
        (function(i){
            btns[i].onclick = function(){
                console.log(i);
            }
        })(index);
    }
&lt;/script&gt;
```

对于上面的例题还有另外一种解法，就是利用`let`关键字绑定块级作用域也可以达到闭包的效果

```js
//只需将for循环里的var index 改成 let index
var btns = document.querySelectorAll(&apos;li&apos;);
for(let index = 0;index&lt;btns.length;index++){
    btns[index].onclick = function(){ console.log(index); };
}
```

![Block](https://i.loli.net/2021/11/13/DTpyBPHVohqCNmj.png)

Block会保存下index的值，当点击事件触发时就会输出当时保存下index值

当外部函数调用多少次，内部就回产生多少个闭包。但当外部函数被连续调用，则它一直持有该变量

## 自测题

```js
//代码片段一
var name = &quot;The Window&quot;;
var object = {
    name: &quot;My Object&quot;,
    getNameFunc: function () {
        return function () {
            return this.name;
        };
    }
};
console.log(object.getNameFunc()());  

//代码片段二
var name2 = &quot;The Window&quot;;
var object2 = {
    name2: &quot;My Object&quot;,
    getNameFunc: function () {
        var that = this;
        return function () {
            return that.name2;
        };
    }
};
console.log(object2.getNameFunc()());	


//代码片段三
function fun(n, o) {
    console.log(o)
    return {
        fun: function (m) {
            return fun(m, n)
        }
    }
}
var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
var b = fun(0).fun(1).fun(2).fun(3)  
```</content:encoded><author>Asuhe</author></item><item><title>Vue路由</title><link>https://asuhe.org/blog/92619e7d/</link><guid isPermaLink="true">https://asuhe.org/blog/92619e7d/</guid><pubDate>Mon, 22 Nov 2021 21:49:01 GMT</pubDate><content:encoded>## 基本使用

```js
// ../router/index.js

import Vue from &apos;vue&apos;;
import VueRouter from &apos;vue-router&apos;;
import routes from &apos;routes&apos;

// 注册路由
Vue.use(VueRouter);

export default new VueRouter({
    mode:&apos;history&apos;,
    routes
})

--------------
// routes.js 路由表文件
import Home from &apos;../pages/Home&apos;

export default [
    {
        path:&apos;/&apos;,
        component:Home
    },
    {
        path:&apos;/&apos;,
        redirect:Home // 默认选中Home路由组件
    }
]

-----------
// main.js 在全局文件中使用路由器
import Vue from &apos;vue&apos;;
import router from &apos;../Router&apos;;

new Vue({
    name:&apos;app&apos;,
    eq:&apos;#app&apos;,
    components:{
        router
    }
})
    
```

当我们在`main.js`中注册了`router`，我们就可以在任何一个组件（包括非路由组件）中使用`$route`和`$router`对象了。

`$route`对象是局部路由信息对象，它包含了当前路由组件里的信息，包括`params`、`query`参数等等，我们可以用它来获取路由组件的参数

`$router`对象是全局的路由器对象，全局就一个。它包含了很多属性和方法（push、replace、go等等），可以让整个应用都可以拥有路由功能。编程式导航就利用了这个对象来实现动态跳转。

`&lt;router-view&gt;`标签标明了切换路由组件时，路由组件的显示位置。路由导航有两种模式：一种是声明式路由导航，另一种是编程式路由导航。使用`&lt;router-link to=&quot;path&quot;&gt;`显示声明路由导航位置就是声明式导航，这种方式适合固定部位导航如顶部的注册登录按钮等，一旦页面要生成的声明式导航过多那么页面就会变的十分卡顿。当链接过多时推荐使用编程式导航来生成导航链接，编程式导航还可以利用事件委托进一步优化页面性能。使用编程式导航时我们只需要给DOM元素添加`click`事件，在这个事件触发的函数里我们可以用`$router.push`或`$router.replace`这两个函数完成路由跳转。这两种方式的唯一的区别就是`push`方法生成的链接没有历史浏览记录，无法回退。当需要回退功能时可以使用`replace`。

## 路由模式

`vue-router`有两种路由方式，分别是`hash`模式和`history`模式。一般默认使用 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL，于是当 URL 改变时，页面不会重新加载。在`hash`模式中，`URL`路径上会有一个`#`标识符，`#`标识符和后面的`URL`片段被称为`hash`。它有以下一些特点：

* 在第一个`#`后面出现的任何字符，都会被浏览器解读为位置标识符。这意味着，这些字符都不会被发送到服务器端。
* 单单改变#后的部分，浏览器只会滚动到相应位置，不会重新加载网页。
* 每一次改变#后的部分，都会在浏览器的访问历史中增加一个记录，使用&quot;后退&quot;按钮，就可以回到上一个位置。
* 可通过window.location.hash属性读取 hash 值，并且 window.location.hash 这个属性可读可写。
* 使用 window.addEventListener(&quot;hashchange&quot;, fun) 可以监听 hash 的变化

而`history`模式是采用`HTML5`标准的History。当你使用` history `模式时，URL 就像正常的 url，例如 `http://yoursite.com/user/id`，路径中不会出现`#`。这种模式需要后台的支持，若后台没有做相应处理，则该种模式直接使用`URL`路径访问会无法访问到对应资源。

两种模式的区别：

* `hash`模式相对`history`模式兼容性更好点
* `hash`模式中`#`以后的东西都不会被当作路径发送给后端服务器，`history`模式里整个`url`都会被当作请求路径发送给后端服务器，这可能导致刷新页面时，如果没有专门配置404页面会导致请求了没有的路径时页面空白
  * `history`模式需要后端服务器的支持

## 缓存路由组件

我们都知道当切换路由时原本的路由会被销毁，有时候我们不希望切换路由时原组件被销毁。这时就可以使用路由缓存技术。我们需要在放置`&lt;router-view&gt;`的组件里使用`&lt;keep-alive&gt;`包裹，这样所有在该视图里展示的路由组件都会被缓存。若我们不想所有路由组件被缓存我们可以使用`&lt;keep-alive include=&quot;组件名&quot;&gt;`来指定缓存的路由组件，需要缓存多个时可以使用数组的形式`&lt;keep-alive :include=&quot;[&apos;组件名1&apos;,&apos;组件名2&apos;]&quot;&gt;`。让不展示的路由组件保持挂载，不被销毁

## 两个路由组件独有的生命周期钩子

* activeated：路由组件被激活（展示）时触发
* deactivated：路由组件失活（隐藏）时触发

当路由组件被`keep-live`但又不被展示时，我们可以使用这两个钩子来做一些事情。例如停用定时器等等

## 路由守卫

路由守卫可以分为三大类：全局路由守卫、独享路由守卫和组件路由守卫。路由守卫使用的场景通常是需要鉴权的时候，例如有些页面需要登录才能查看，有些页面只能从规定的页面去跳转。

全局路由守卫和独享路由守卫的概念是类似的，只不过全局路由守卫是作用于全局，任何一个组件跳转都要通过全局路由守卫。`beforeEach`和`afterEach`就是提供给我们的两个用于操作组件路由匹配动作前后的函数，其中在所有的守卫中仅`afterEach`没有`next`参数，其它守卫都有`to、from、next`这三个参数。**这两个函数我理解为类似于`beforeMonted`和`mounted`这两个生命周期钩子，它们分别在路由规则匹配前后工作。**而独享路由守卫仅有一个匹配前的函数`beforeEnter`供我们使用。

组件路由守卫有三个函数`beforeRouterEnter`、` beforeRouteUpdate`和` beforeRouteLeave`。**组件路由守卫都是在组件通过路由规则匹配才能生效的，若不是通过路由规则跳转的则守卫不会被执行。**

* `beforeRouterEnter`在渲染该组件的对应路由被 confirm 前调用，并且不能获取组件实例 `this`因为当守卫执行前，组件实例还没被创建。

* `beforRouterUpdate`在当前路由改变，但是该组件被复用时调用。举例来说，对于一个带有动态参数的路径 /foo/:id，在 /foo/1 和 /foo/2 之间跳转的时候，由于会渲染同样的 Foo 组件，因此组件实例会被复用。而这个钩子就会在这个情况下被调用。它里面可以访问组件实例 `this`。

*  `beforeRouteLeave`导航离开该组件的对应路由时调用，这里的导航离开指的是不显示该组件要去往其它组件时的场景， 可以访问组件实例 `this`</content:encoded><author>Asuhe</author></item><item><title>手撸bind、apply、call</title><link>https://asuhe.org/blog/6a400b12/</link><guid isPermaLink="true">https://asuhe.org/blog/6a400b12/</guid><pubDate>Sun, 21 Nov 2021 19:55:16 GMT</pubDate><content:encoded>## call

* 功能：将函数this指向更改为第一个传入的形参对象，并调用函数本身
* 参数：
  * **thisArg**：可选的。在 *`function`* 函数运行时使用的 `this` 值。请注意，`this`可能不是该方法看到的实际值：如果这个函数处于[非严格模式](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode)下，则指定为 `null` 或 `undefined` 时会自动替换为指向全局对象，原始值会被包装。
  * **arg1, arg2, ...**：可选的。指定的参数列表。
* 返回值：使用调用者提供的 `this` 值和参数调用该函数的返回值。若该方法没有返回值，则返回 `undefined`。

### ES5写法

```js
Function.prototype.myCall = function(){
    //拿到传入的第一个参数
    var thisArg = [].shift.apply(arguments);
    //转换arguments为数组
    var args = [].slice.apply(arguments);
    var fn = &apos;Symbol&apos;;
    thisArg[fn] = this;
    var res = thisArg[fn].apply(args);
    delete thisArg[fn];
    return res;
};
```



### ES6写法

```js
// 将方法定义在Fuction的prototype上，这样任何一个函数对象都可以使用该方法
Function.prototype.myCall = function(thisArg,...args){
    // 判断第一个传入的形参是否为空
    if(thisArg === null || thisArg === undefined){
        thisArg = thisArg || window;
    };
    // 保存调用该方法的this，该this指向的也就是调用myCall方法的那个函数对象本身
    const fn = Symbol(0); // 用Symbol做唯一标识符，防止与源函数对象上的属性名冲突而产生覆盖
    thisArg[fn] = this;
    // 调用源函数
    const res = thisArg[fn](...args);
    // 清理属性名，使原来传入的对象其恢复原样
    delete thisArg[fn];
    // 返回改变this后源函数调用结果
    return res;
};
```



## apply

* 功能：同call，将函数this指向更改为第一个传入的形参对象，并调用函数本身

* 参数：
  * **thisArg**：可选的。在 *`func`* 函数运行时使用的 `this` 值。
  * **argsArray**：可选的。一个数组或者类数组对象，其中的数组元素将作为单独的参数传给 `func` 函数。如果该参数的值为 [`null`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/null) 或 [`undefined`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/undefined)，则表示不需要传入任何参数。
* 返回值：使用调用者提供的 `this` 值和参数调用该函数的返回值。若该方法没有返回值，则返回 `undefined`。

### ES5写法

```js
Function.prototype.myApply = function(){
    //拿到传入的第一个参数
    var thisArg = [].shift.call(arguments);
    //转换arguments为数组
    var args = [].slice.call(arguments);
    var fn = &apos;Symbol&apos;;
    thisArg[fn] = this;
    var res = thisArg[fn].apply(args);
    delete thisArg[fn];
    return res;
};
```



### ES6写法

```js
// 步骤基本和call一样，唯一的区别就是第二个参数是给源函数使用的参数数组
Function.prototype.myApply = function(thisArg,argsArray){
    if(thisArg === null || thisArg === undefined){
        thisArg = thisArg || window;
    }
    const fn = Symbol(0);
    thisArg[fn] = this;
    // 调用源函数,将参数组数展开
    const res = thisArg[fn](...argsArray);
    delete thisArg[fn];
    return res;
}
```



## bind

* 功能：将函数this指向更改为第一个传入的形参对象，而其余参数将作为返回的新函数的参数，供调用时使用。
* 参数：
  * **thisArg**：调用绑定函数时作为 `this` 参数传递给目标函数的值。
  * **arg1, arg2, ...**：当目标函数被调用时，被预置入绑定函数的参数列表中的参数。
* 返回值：返回一个原函数的拷贝，并拥有指定的 `this` 值和初始参数。

### ES5写法

```js
Function.prototype.myBind = function () {
    const arr = [].slice.apply(this,arguments)
    const thisArg = arr.shift();
    const fn = this;
    return function(){
        const arr2 = [].slice.apply(this,arguments)
        return fn.apply(thisArg,arr.concat(arr2))
    }
};
```



### ES6写法

```js
Function.prototype.myBind = function(thisArg,...args1){
    if(thisArg === null || thisArg === undefined){
        thisArg = thisArg || window;
    };
    const fn = this;
    return function(...args2) {
        return fn.apply(thisArg,args1.concat(args2));
    };
}
```</content:encoded><author>Asuhe</author></item><item><title>Mockjs基本使用</title><link>https://asuhe.org/blog/bdd668ce/</link><guid isPermaLink="true">https://asuhe.org/blog/bdd668ce/</guid><pubDate>Tue, 30 Nov 2021 14:04:59 GMT</pubDate><content:encoded>## node

```bash
# npm安装
npm install mockjs
# yarn安装 推荐
yarn add mockjs
```

### 使用

```js
// 使用 Mock
var Mock = require(&apos;mockjs&apos;)
var data = Mock.mock({
    // 属性 list 的值是一个数组，其中含有 1 到 10 个元素
    &apos;list|1-10&apos;: [{
        // 属性 id 是一个自增数，起始值为 1，每次增 1
        &apos;id|+1&apos;: 1
    }]
})
// 输出结果
console.log(JSON.stringify(data, null, 4))
```



## vue

使用步骤

* 定义数据结构
* 设置mock服务器
* 封装 axios
* 使用

### 定义数据结构

定义好要模拟数据的基本`json`结构，结构中可以使用mock提供的[语法](http://mockjs.com/examples.html)随机输出数据

```json
# banners.json
[
    {
        &quot;id&quot;:&quot;1&quot;,
        &quot;imgUrl&quot;:&quot;/images/banner1.jpg&quot;
    },
    {
        &quot;id&quot;:&quot;2&quot;,
        &quot;imgUrl&quot;:&quot;/images/banner2.jpg&quot;
    },
    {
        &quot;id&quot;:&quot;3&quot;,
        &quot;imgUrl&quot;:&quot;/images/banner3.jpg&quot;
    },
    {
        &quot;id&quot;:&quot;4&quot;,
        &quot;imgUrl&quot;:&quot;/images/banner4.jpg&quot;
    }
]
```



### 设置mock服务器

让mock模拟出接口，axios请求往模拟出的接口里发

```js
// mockServer
import Mock from &apos;mockjs&apos;;
import banners from  &apos;./banners.json&apos;; // 接口返回的数据

// 设置接口请求路径
Mock.mock(&apos;/mock/banners&apos;,{ 
    code:200,
    data:banners
});
```



### 封装 axios

封装好发往mock服务器的请求拦截器和响应拦截器

```js
// mockAjax.js
import axios from &quot;axios&quot;;
import NProgress from &quot;nprogress&quot;;
import &quot;nprogress/nprogress.css&quot;;

// 配置进度条
NProgress.configure({ showSpinner: false });

// 创建一个Axios实例
const service = axios.create({
  baseURL: &apos;/mock&apos;, // 设置基础url
  timeout: 30000,
});

// 设置请求拦截器，添加基础路径
service.interceptors.request.use((config) =&gt; {
  // 请求进度条
  NProgress.start();
  return config;
});
// 设置响应拦截器，处理返回数据
service.interceptors.response.use(
  (response) =&gt; {
    // 隐藏进度条
    NProgress.done();
    if (response.data.code === 200) {
      return response.data;
    } else {
      return Promise.reject(response.message);
    }
  },
  (error) =&gt; {
    NProgress.done();
    // console.log(error);
    alert(`请求出错${error.message}`||`未知错误`);
    return Promise.reject(error);
  }
);
// 将实例暴露出去
export default service;
```



### 使用

```js
import mockAjax from &apos;./mockAjax&apos;;

export const reqBanners = () =&gt; mockAjax(&apos;/banners&apos;);

requBannners().then(
	value =&gt; value,
    error =&gt; error
);
```</content:encoded><author>Asuhe</author></item><item><title>Swiper基本使用</title><link>https://asuhe.org/blog/9e3e1e52/</link><guid isPermaLink="true">https://asuhe.org/blog/9e3e1e52/</guid><pubDate>Tue, 30 Nov 2021 14:04:59 GMT</pubDate><content:encoded>## Vue

### 旧版

#### 下载依赖包

```bash
npm install swiper
```

#### 在轮播组件中引入swiper和其css

```js
import Swiper from &quot;swiper&quot;;
import &apos;swiper/css/swiper.css&apos;;
```

#### 设置轮播的位置以及容器

```html
&lt;div class=&quot;swiper-container&quot; ref=&quot;swiper&quot; id=&quot;mySwiper&quot;&gt;
    &lt;div class=&quot;swiper-wrapper&quot;&gt;
        &lt;div class=&quot;swiper-slide&quot; v-for=&quot;item in banners&quot; :key=&quot;item.id&quot;&gt;
            &lt;img :src=&quot;item.imageUrl&quot; style=&quot;width: 100%; height: 435px&quot; /&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- 如果需要分页器 --&gt;
    &lt;div class=&quot;swiper-pagination&quot;&gt;&lt;/div&gt;

    &lt;!-- 如果需要导航按钮 --&gt;
    &lt;div class=&quot;swiper-button-prev&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;swiper-button-next&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
```

#### 创建Swiper实例

原则上一个Swiper实例只控制一个轮播图，**而且Swiper实例一定要在界面显示后再创建，不然无法控制图片轮播**

```js
// 数据更新后，先同步调用watch的回调，最后异步更新界面
watch: { // watch监视轮播图片数据是否异步请求回来了
    banners() {
        this.$nextTick(() =&gt; { // nextTick会在界面完成更新之后才执行指定的回调
            var mySwiper = new Swiper(this.$refs.swiper, { // 用vue的ref来隔离各个轮播图，防止一个swiper实例控制多个轮播图
                // direction: &quot;vertical&quot;, // 垂直切换选项
                loop: true, // 循环模式选项

                autoplay: {
                    delay: 2000,
                    disableOnInteraction: false,
                },
                // 如果需要分页器
                pagination: {
                    el: &quot;.swiper-pagination&quot;,
                },

                // 如果需要前进后退按钮
                navigation: {
                    nextEl: &quot;.swiper-button-next&quot;,
                    prevEl: &quot;.swiper-button-prev&quot;,
                },
                // 如果需要滚动条
                scrollbar: {
                    el: &quot;.swiper-scrollbar&quot;,
                },
            });
        });
    },
},
```



### 新版

使用`vue-awesome-swiper`制作轮播图。

#### 安装

```bash
npm install swiper
npm install vue-awesome-swiper
```

#### 定义swiper.js文件并注册该组件

定义好`swiper.js`文件方便模块化管理插件

```js
// swiper.js
import Vue from &apos;vue&apos;;
import VueAwesomeSwiper from &apos;vue-awesome-swiper&apos;;
import &apos;swiper/css/swiper.css&apos;;
Vue.use(VueAwesomeSwiper);
```

#### 在vue工程的main.js中引入

```js
// main.js
import &apos;./swiper.js&apos;;
```

#### 使用

```html
// 轮播组件 tempalte部分
&lt;swiper :options=&quot;{
          loop: true, // 循环模式选项
          autoplay: {
            delay: 2000,
            disableOnInteraction: false,
          },
          // 如果需要分页器
          pagination: {
            el: &quot;.swiper-pagination&quot;,
            clickable:true
          },
          // 如果需要前进后退按钮
          navigation: {
            nextEl: &quot;.swiper-button-next&quot;,
            prevEl: &quot;.swiper-button-prev&quot;,
          },
      	}&quot; 
       class=&quot;swiper&quot;
       ref=&quot;swiper&quot;
       id=&quot;mySwiper&quot;&gt;
    &lt;swiper-slide v-for=&quot;item in banners&quot; :key=&quot;item.id&quot;&gt;
        &lt;img :src=&quot;item.imageUrl&quot; style=&quot;width:100%;heigth:435px&quot; /&gt;
    &lt;/swiper-slide&gt;
    &lt;div class=&quot;swiper-button-prev&quot; slot=&quot;button-prev&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;swiper-button-next&quot; slot=&quot;button-next&quot;&gt;&lt;/div&gt;
    &lt;div calss=&quot;swiper-pagination&quot; slot=&quot;pagination&quot;&gt;&lt;/div&gt;
&lt;/swiper&gt;
```</content:encoded><author>Asuhe</author></item><item><title>VUE基础(四)</title><link>https://asuhe.org/blog/4eb782f5/</link><guid isPermaLink="true">https://asuhe.org/blog/4eb782f5/</guid><pubDate>Tue, 30 Nov 2021 20:55:35 GMT</pubDate><content:encoded>## v-for和v-if的优先级

**`v-for`和`v-if`一起使用时，vue会先渲染`v-for`的元素，然后再对渲染出的元素进行`v-if`的判断。**例如有如下数据结构，我们要将它用`v-for`进行渲染，然后再根据 id 用`v-if`判断 id 是否奇数，然后再决定是否进行显示

```js
data(){
    return {
        human = [
            {
                id:1,
                name:&apos;asuhe&apos;
            },
            {
                id:2,
                name:&apos;sphinx&apos;
            },
            {
                id:3,
                name:&apos;asuka&apos;
            },
            {
                id:4,
                name:&apos;awesome&apos;
            },
        ]  
    }
}
```
### 情况1：v-if判断与v-for中的数据有关

```html
&lt;div v-for=&quot;item in human&quot; :key=&quot;item.id&quot; v-if=&quot;item.id%2 === 1&quot;&gt;
    {{item.name}}
&lt;/div&gt;
```

当我们使用上述方式进行判断时，vue会将四个`div`全部渲染出来，再进行`v-if`的判断。这就造成了性能损失，进行了一些不必要`div`的渲染和判断

### 解决方案

在这种情况下我们可以使用计算属性，先将符合条件的数据过滤出来，再去使用`v-for`渲染。这样就可以大幅降低性能损耗

```js
computed:{
    person(){
        return this.human.filter(item =&gt; item.id%2 === 1 );
    }
}
```

```html
&lt;div v-for=&quot;item in person&quot; :key=&quot;item.id&quot;&gt;
    {{item.name}}
&lt;/div&gt;
```

这样`v-for`就不会渲染不必要的数据

### 情况2：v-if判断与v-for中的数据无关

若`v-for`的执行是由其它数据判断则，则可以在`v-for`的上层添加一个`v-if`先进行`v-for`的判断。然后再执行`v-for`

### 解决方案

```js
data(){
	return{
        isShow:true,
        human = [
            {
                id:1,
                name:&apos;asuhe&apos;
            },
            {
                id:2,
                name:&apos;sphinx&apos;
            },
        ]
    }
}
```

```html
&lt;div v-if=&quot;isShow&quot;&gt;
    &lt;div v-for=&quot;item in person&quot; :key=&quot;item.id&quot;&gt;
        {{item.name}}
    &lt;/div&gt;
&lt;/div&gt;
```</content:encoded><author>Asuhe</author></item><item><title>http请求方式详解</title><link>https://asuhe.org/blog/95b6c6cd/</link><guid isPermaLink="true">https://asuhe.org/blog/95b6c6cd/</guid><pubDate>Wed, 08 Dec 2021 21:41:29 GMT</pubDate><content:encoded>## 请求方式与请求参数

常用的请求方式无非就是增删改查，在http请求中增删改查对应的请求方式分别为：

* post：增，提交数据给服务端
* delete：删，删除指定的数据
* put：改，更改服务端已有的数据
* get：查，从服务端获取数据

与这些相对的还有这些请求方式携带参数的形式，总共有三种携带数据的方式：

* query参数：

  &gt; 即查询字符串，就是我们最最常见的在地址栏上携带参数，get请求常用这种方式携带参数，例如`/addUser?userName=asuhe&amp;userId=1`。这种请求方式用`?`来与请求路径分割，当携带多个query参数时就用`&amp;`来分割参数
  * query参数的数据是显示暴露在地址栏上的
  * 编码方式为`urlencoded`

* params参数

  &gt; params参数和query参数一样是显示地暴露在地址栏上的，例如server:`/addUser/:userId`，client:`/addUser/1`，服务端就收到`userId=1`。

* 请求体参数

  &gt; 通常post请求发送的参数就是请求体参数，用这种方法携带参数会将参数包含在http请求的请求体中。该种参数有两种格式一种是`urlencoded`，另一种是`json`

  * `urlencoded`格式：在请求体中参数是以类似`query`参数的形式存放，例如：`name=asuhe&amp;age=18`。请求头参数的格式为：`Content-Type: application/x-www-form-urlencoded`
  * `json`格式：参数在请求体中以`json`格式存放，例如：`{&quot;name&quot;:&quot;tom&quot;,&quot;age&quot;:18}`。其请求头中的参数为：`Content-Type: application/json`

### 请求方式与请求参数之间的联系

* 通常情况下，**请求方式与请求参数可以任意搭配**。也就是说，即使我们使用`post`的方式提交参数，我们也可以使用`query`参数的形式来携带提交的数据
* 理论上一次请求可以使用上述三种类型参数中的任何一种，也可以同时使用不同类型的参数来完成一次请求
* 但`get`请求方式有些特殊，`get`方式提交请求不能使用请求体参数来携带数据，因为`get`请求是没有请求体的

## API接口风格

根据后端接口处理请求的方式，我们可以将API接口分为`restful`风格和非`restful`风格的API。

### REST API

`rest API`即`restful`风格的API有如下特点：

* `server`后端CURD操作是根据`client`发送请求的方式来决定的
* 同一个请求路径可以进行多个操作
* 请求方式会用到`get`、`post`、`put`、`delete`

通常`rest API`对同一数据进行操作时，只有一个路径。例如对用户信息进行CURD，`rest API`的形式为`/user`。客户端要对用户信息进行增就使用`post`请求提交数据，删就用`delete`请求，依次类推实现数据操作。这一个接口就实现了CURD四种功能

### 非REST API

`非rest API`即`restless`风格的API，它有如下特点：

* 后端CURD操作不根据`client`的请求方式决定，而是根据请求路径
* 一个请求路径只对应一个操作
* 一般只使用`get`、`post`请求就够了

开发中最常见的一个操作对应一个请求路径的就是`restless API`了。</content:encoded><author>Asuhe</author></item><item><title>终极原型链</title><link>https://asuhe.org/blog/15601a9f/</link><guid isPermaLink="true">https://asuhe.org/blog/15601a9f/</guid><pubDate>Thu, 23 Dec 2021 20:24:48 GMT</pubDate><content:encoded>接{% post_link &apos;2021-10-20-原型与原型链&apos; &apos;上文&apos;%}原型链，今天我们来说说终极原型链

## 终极原型链

只要对`js`深入了解一些，你肯定听说过一句话就是函数本质上也是一个对象，当函数被当作做对象去使用时函数就被称为函数对象，当函数被`()`调用是一个函数。既然函数也是对象，那它肯定也有一个原型。本文讲解将一直以下面代码为例

```js
function Person(name, age) { // 创建一个Person构造函数
    this.name = name;
    this.age = age;
}
const obj = new Person(&quot;asuhe&quot;, 18); // new 一个Person实例
console.log(obj);
```

所有的函数对象都会有一个原型（ES6箭头函数除外），且会有一个`prototype`属性指向该原型。当用一个函数对象当作构造函数使用`new`创建一个实例对象时，该构造函数所有实例对象的隐式原型即`__proto__`都会指向构造函数的原型对象。在上述代码中我们可以得到一个最基本的原型链结构

![原型链(1)](https://i0.hdslb.com/bfs/album/62c7f1c36da40209459be4ff80cb91fbd5e4856d.png)

函数对象的原型本质上也是一个对象，只要是对象那么必定会有一个构造函数。**实际上除函数对象和自身指定构造函数的对象外，所有的对象都是由`Object`这个构造函数`new`出来的。**也就是说函数对象的原型是由`Object`构造出来的。根据原型链查找规则，我们可以用如下代码证明

```js
let res = {};
console.log(res);
console.log(res.__proto__ === Person.prototype.__proto__); // true 说明普通对象都是由Object作为构造函数new出来的
```

![原型链(1)](https://s2.loli.net/2021/12/24/iq6Te7YVv1pBAcl.png)

`Object`作为构造函数那么它必然也有一个原型并且由`prototype`属性指向。上面我们说过除函数对象和自己指定构造的对象外，所有的对象都是由`Object`作为构造函数`new`出来的。所以我们可以画出更加完善一点的原型链

![原型链(2)](https://i0.hdslb.com/bfs/album/4e1203d7ceb497b592d178fd7f4588399a3f34df.png)

`Object`的原型的隐式原型`__proto__`应该指向其构造函数的原型`prototype`，而`Object`的原型是由它自身`new`出来的。这样它原型链就形成了一个环：$Object.prototype$ ▶` (prototype of Object).__proto__ `▶$Object.prototype$。为了阻止原型链在这个环里无限循环查找下去，所以在底层` (prototype of Object).__proto__ `被设置成了`null`。我们可以用代码证明上述的原型链

```js
console.log(Person.prototye.__proto__ === Object.prototype); //true 说明函数对象的原型是由Object new出来的
console.log(Object.prototype.__proto__); //null 说明Object的原型的隐式原型值为null
```

![输出](https://i0.hdslb.com/bfs/album/5263cabaa84f7a4254ee7a51bfead99fd1950f3e.png)

### 函数对象的原型链

到目前为止，我们基本搞定了普通对象的原型链结构。但是还有一个问题我们没有搞定就是，既然函数也是一个对象那么它肯定也有自己的隐式原型`__proto__`指向它的构造函数的原型。**在js的底层所有的函数都是由`Function`作为构造函数`new`出来的，也就是说任何一个函数都是`Function`的实例**

**当我们使用`function`关键字时本质上就是`Fuction`作为构造函数`new`了一个对象。在js的底层最终都是调用`Function`函数去创建一个函数的。**我们可以使用如下代码证明

```js
// 证明所有的函数对象都是由Function构造的
console.log(Person.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Person.__proto__ === Object.__proto__); // true
// 箭头函数没有prototype
const arrFn = ()=&gt; console.log(&apos;ash&apos;);
console.log(arrFn.prototype); // undefined 说明箭头函数没有原型
console.log(arrFn.__proto__ === Function.prototype); // true 说明箭头函数同样是Function new出来的
```

![输出](https://i0.hdslb.com/bfs/album/c7b8b125be3627b2d2c51d1bc0ef6015c3555138.png)

而`Function`函数本身也是一个函数对象，但是`Function`它是由自己new出来的。所以它的`__proto__`和`prototype`都指向同一个原型。

```js
console.log(Function.prototype === Function.__proto__); // true 说明Function自己new出的自己
console.log(Function.prototype.__proto__ === Object.prototype); // true 说明Function的原型还是Object new出来的
```

有了以上基础我们就可以继续完善一下原型链

![原型链(3)](https://i0.hdslb.com/bfs/album/ea5af04e95c07086eb47e2ffc44612301544cccc.png)

上面这个图就是我们常说的终极原型链了

# 自测题

```javascript
// 思考下列输出并说明原因
console.log(Object instanceof Object)
console.log(String instanceof Function)
console.log(Function instanceof Object)
console.log(Object instanceof Function)
console.log(String instanceof Object)
```</content:encoded><author>Asuhe</author></item><item><title>DOM树工作原理</title><link>https://asuhe.org/blog/5c9ce8df/</link><guid isPermaLink="true">https://asuhe.org/blog/5c9ce8df/</guid><pubDate>Wed, 05 Jan 2022 16:56:45 GMT</pubDate><content:encoded># 什么是DOM

平时我们写的`html`标签本质上就是一堆字符串，`html`文件组成的字节流实际上是无法被浏览器渲染引擎理解的。为了让渲染引擎能够解析这些字符串，并且让`JavaScript`能够动态操纵网页元素而不是直接操作一堆字符串，于是就有了`DOM`这个概念。`DOM`让`html`文档能够有结构化的表述。在渲染引擎中，`DOM`主要有三个层面的作用：

* 从页面的角度来看，`DOM`就是生成页面的基本数据结构
* 从`JavaScript`的角度来看，`DOM`提供了接口让`JavaScript`有能力操作页面的元素，改变页面的结构、样式和内容
* 从安全的角度来看，`DOM`提供了一个安全的容器，让一些不安全的内容直接在`DOM`解析的阶段就被排除了

# DOM树的生成

上面我们提到渲染引擎无法直接识别`html`文档字节流，所以在渲染引擎渲染页面之前`html`文档会被交给`HTML`解析器，让它先把`html`文档转换为`DOM`结构，再供渲染引擎使用。

`HTML`解析器在解析`html`文档时是一边加载一边解析的，也就是说`html`文档加载了多少内容它就解析多少内容，而不是等`html`文档全部加载完才开始解析内容的。这就像编译型语言和解释型语言，显然`HTML`解析器的工作模式是同解释型语言一样的。

在加载页面时，浏览器网络进程接收到响应头后会根据响应头中`content-type`字段来判断文件类型，接着启动相应进程去处理接收到的文件。如`html`文件的`content-type`字段是`text/html`，浏览器就会启动一个渲染进程去处理它。渲染进程启动完，网络进程和渲染进程之间会建立一个共享数据的管道，网络进程接收到多少内容就同时往管道里添加多少内容，而渲染进程就一直读取管道里的数据进行解析渲染。

![html响应头类型](https://i0.hdslb.com/bfs/album/fa20779d57737cf9ccfb482b89dfdea5766a7e34.png)

## DOM生成

将`html`字节流转换为`DOM`的过程大致分为三个阶段：

1. 通过分词器将字节流转换为`Token`，这一点类似`JavaScript`解析
2. 生成`Node`节点
3. 生成`DOM`

![DOM生成](https://i0.hdslb.com/bfs/album/5022e9fe0d2cb27440b97e3e1b52ced0b4bd14bb.png)

在分词器生成`Token`阶段，字节流一般会被转换成两种`Token`：`Tag Token`和`文本 Token`。经过分词器处理后`Tag Token`会被划分成`StartTag` 和`EndTag`。如：

```html
&lt;html&gt;
    &lt;body&gt;
        &lt;div&gt;
            Asuhe
        &lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;
```

![Tokens](https://i0.hdslb.com/bfs/album/7dc0875e58654646f90e83f5c2045968e44e87b6.png)

后续的生成`Node`节点和`DOM`是同步进行的，将`Token`变成`Node`节点再将`DOM`插入`DOM`树中，到这里文档的`DOM`树就基本生成完毕了。

利用上面生成的`Tokens`,`HTML`解析器维护了一个`Token`栈结构。利用栈来进行标签匹配完成`TagT Token`的闭合，其和括号匹配是一样的。以上面的代码为例，`HTML`解析器首先会将`html、body、div`的`StartTag`入栈，`文本Token`会直接拿去生成`DOM`加入`DOM树`，在遇到`EndTag`就弹出栈顶的`StartTag`，将其插入`DOM`树。`HTML`解析器开始工作时，会默认创建一个根为`document`的空`DOM`结构，同时将一个`StartTag document`的`Token`压入栈底，后面再装入分词器分类出的`token`，文本`Token`会被插入在其上一个`Tag Token`的后面作为其子节点

![Token栈与DOM树](https://i0.hdslb.com/bfs/album/9593dfafede6b5c9c2989066cc4490303d44d691.png)

每当`Token`栈里出栈一个元素的时候`DOM`树就会生成相应节点并插入，所以最后文档渲染完毕时`Token`栈为空。

分词器解析出`Token`后，渲染引擎的`XSSAuditor`模块会启动，检查词法安全。例如是否引用了外部脚本、是否符合[`CSP`规范](https://developer.mozilla.org/zh-CN/docs/Glossary/CSP)、是否跨域请求等等，若出现不规范的内容`XSSAuditor`会对该脚本或者下载任务进行拦截

## JavaScript对dom生成的影响

当`HTML`解析器遇到`&lt;script&gt;`标签时，渲染引擎判断出这是一段脚本，此时`HTML`解析器会停止对`DOM`的解析，因为段脚本里的代码可能会对已经生成的`DOM树`进行操作。所以渲染引擎会先执行完脚本代码再继续启动`HTML`解析器进行`DOM`解析。也就是说当有`JavaScript`在文档中时，`DOM`生成会被阻塞。同时若一个`JavaScript`脚本代码中对`DOM`进行了操作，但它操作的`DOM`位于该段代码的`&lt;script&gt;`标签之后那么这行代码就会执行失败，因为此时需要被操作的`DOM`并没有渲染出来。这就是为什么通常我们将`JavaScript`代码放在`html`文档最后的原因。`&lt;script&gt;`标签放在`html`文档的头部，当`&lt;script&gt;`中代码较多所需执行时间很长时我们的页面就会出现白屏。

**当我们使用外部链接来加载`&lt;script&gt;`代码时，浏览器需要先下载这段代码，而下载过程同样会阻塞`DOM`解析，此时如果源`js`文件站点网络较差就会导致长时间白屏。**

为了解决这个问题`Chrome`浏览器做了许多优化，主要的就是预解析操作。当渲染引擎接收到字节流以后会开启一个预解析线程用于分析`html`文件中包含的`JavaScript、Css`等相关文件，解析到了会提前下载这些文件以防止阻塞

上面我们知道`JavaScript`脚本会阻塞`DOM`的生成，对此我们也有可以采用一些方法来规避，例如当`javascript`代码中没有`DOM`操作相关的代码时，就可以将该`JavaScript`脚本设置为异步加载，或者使用`CDN`加速、代码压缩等方法。

```html
使用async异步加载代码
&lt;script async type=&quot;text/javascript&quot; src=&quot;foo.js&quot;&gt;&lt;/script&gt;
```

```html 
使用defer异步加载代码
&lt;script defer type=&quot;text/javascript&quot; src=&quot;foo.js&quot;&gt;&lt;/script&gt;
```

虽然`async`和`defer`都是异步加载`javascript`文件，但是`async`加载完`js`文件后会立即执行里面的代码而`defer`则会在[`DOMContentLoaded`事件](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/DOMContentLoaded_event)前执行

在页面的`JavaScript`代码中我们可能并不会增删`DOM`但会修改`DOM`的样式，操作[`CSSOM`](https://developer.mozilla.org/zh-CN/docs/Glossary/CSSOM)。如果`js`代码里操作了外部的`CSS`那么浏览器同样要等待外部的`CSS`文件下载完成并解析生成`CSSOM`对象之后才能执行`JavaScript`脚本。也就是说单纯的外部`css`文件并不会阻塞`DOM`渲染，但若是`js`代码中操作了外部`css`文件则该`css`文件就会间接导致`DOM`渲染被阻塞

![DOM渲染流程图](https://s2.loli.net/2022/01/07/jbye6CJrHR2xLuI.png)

当`HTML`解析器发现需要`css、js`外部文件时，浏览器会同时发起请求进程，也就是说请求`css`和`js`文件是并行的，所以在我们计算加载时间时仅需计算最大的那个文件所需传输时长即可

![浏览器渲染进程](https://s2.loli.net/2022/01/07/rB93eZ1EayhqolH.png)

# 首页白屏优化

通过上面的分析我们知道一般情况下网页性能瓶颈主要体现在`css`下载和`js`文件下载和代码执行中，所以想要缩短白屏时长我们可以采取以下策略：

* 通过内联`css`和`js`来消除文件下载时导致的进程阻塞
* 在不适合内联`css、js`的情况下尽量减小文件大小，例如`webpack`的`Tree Shaking`
* 对于未操作`DOM`的`js`文件用`async`或`defer`异步加载
* 对于大的`css`文件使用媒体查询将其拆分为多个`css`文件，需要用的时候再加载相关`css`文件

# 页面渲染全过程

* 渲染进程将`html`文档转换为渲染引擎能够识别的`DOM树结构`
* 渲染引擎将`css`样式表转换为可以理解的`styleSheets`，计算出`DOM`节点的样式生成`CSSOM`
* 创建布局树，并计算元素的分布信息
* 对布局树进行分层并生成分层树
* 为每个图层生成绘制列表，并将其提交到合成线程
* 合成线程将图层分成图块，并在光栅化线程池中将图块转换成位图
* 合成线程发送绘制图块命令`DrawQuad`给浏览器进程
* 浏览器进程根据`DrawQuad`消息生成页面，并显示到屏幕上</content:encoded><author>Asuhe</author></item><item><title>重新理解html</title><link>https://asuhe.org/blog/d775eff9/</link><guid isPermaLink="true">https://asuhe.org/blog/d775eff9/</guid><pubDate>Fri, 21 Jan 2022 09:48:51 GMT</pubDate><content:encoded>## HTML语义化

所谓`html`语义化就是用语义化标签去搭建网页结构。我们可以看如下两段代码体会语义化：

```html
// 代码一
&lt;div&gt;
    &lt;div&gt;标题&lt;/div&gt;
    &lt;div&gt;文字内容&lt;/div&gt;
    &lt;div&gt;列表
    	&lt;div&gt;列表项1&lt;/div&gt;
    	&lt;div&gt;列表项2&lt;/div&gt;
    &lt;/div&gt;    
&lt;/div&gt;

------
// 代码二
&lt;div&gt;
    &lt;h2&gt;标题&lt;/h2&gt;
    &lt;p&gt;文字内容&lt;/p&gt;
    &lt;ul&gt;列表
    	&lt;li&gt;列表项1&lt;/li&gt;
    	&lt;li&gt;列表项2&lt;/li&gt;
    &lt;/ul&gt;   
&lt;/div&gt;
```

通过css的控制，上面两种代码我们可以实现同一效果。对比这两种代码我们可以明显感觉出**代码二**更加符合人的阅读习惯，利于后期维护。同时语义化的代码可以更好的让搜索引擎抓取，有利于让搜索引擎分清网页内容主次，利于SEO。由此我们可以总结出`HTML`语义化的优点：

* 增加代码可读性，方便开发人员阅读
* 利于SEO，方便搜索引擎匹配

## HTML常用标签

`HTML`标签按照其特性我们可以将其大致划分为两类，分别是块级元素和内联元素。块级元素的特点是无论其内容的多少都会独占一行，块级元素内部可以继续嵌套其它行内元素和块级元素。而内联元素的大小通常根据其内容来决定，一般情况下内联元素内部只能包含数据以及其它内联元素。

常见块级元素：h1(所有标题标签)、div、p、form、header、footer、ul、ol...

常见内联元素：img、input、button、label、span、textarea...

## src 和 href 的区别

src 和 href 都是**用来引用外部的资源**，它们的区别如下：

- **src：** 表示对资源的引用，它指向的内容会嵌入到当前标签所在的位置。src 会将其指向的资源下载并应⽤到⽂档内，如请求 js 脚本。当浏览器解析到该元素时，会暂停其他资源的下载和处理，直到将该资源加载、编译、执⾏完毕，所以⼀般 js 脚本会放在页面底部。
- **href：** 表示超文本引用，它指向一些网络资源，建立和当前元素或本文档的链接关系。当浏览器识别到它他指向的⽂件时，就会并⾏下载资源，不会停⽌对当前⽂档的处理。 常用在 a、link 等标签上。

## script 标签中 defer 和 async 的区别

如果没有 defer 或 async 属性，浏览器会立即加载并执行相应的脚本。它不会等待后续加载的文档元素，读取到就会开始加载和执行，这样就阻塞了后续文档的加载。

下图可以直观的看出三者之间的区别:

![image.png](https://s2.loli.net/2022/02/28/CmN8oBEu4HzWgn6.png)

其中蓝色代表 js 脚本网络加载时间，红色代表 js 脚本执行时间，绿色代表 html 解析。

**defer 和 async 属性都是去异步加载外部的 JS 脚本文件，它们都不会阻塞页面的解析**，其区别如下：

- **执行顺序：**多个带 async 属性的标签，不能保证加载的顺序；多个带 defer 属性的标签，按照加载顺序执行；
- **脚本是否并行执行：**async 属性，表示**后续文档的加载和执行与 js 脚本的加载和执行是并行进行的**，即异步执行；defer 属性，加载后续文档的过程和 js 脚本的加载(此时仅加载不执行)是并行进行的(异步)，js 脚本需要等到文档所有元素解析完成之后才执行，DOMContentLoaded 事件触发执行之前。</content:encoded><author>Asuhe</author></item><item><title>this指向机制</title><link>https://asuhe.org/blog/5fafc9b8/</link><guid isPermaLink="true">https://asuhe.org/blog/5fafc9b8/</guid><pubDate>Tue, 22 Feb 2022 11:36:05 GMT</pubDate><content:encoded># This在不同情况下的指向

## This所处的场景

使用`this`的场景可大致分为下面几类：

- 普通函数调用：`this`指向全局对象
- class调用：`this`指向`class`实例
- bind、call、apply调用：`this`指向传入的对象
- 作为对象方法：`this`指向对象实例
- 箭头函数

不同的场景`this`的指向会发生变化，但有它们都有一个共同点，那就是**`this`是在执行时才确定其最终指向，而不是在定义时确定**。这一点和**自由变量**的查找有很大不同。

## 箭头函数的this

- 箭头函数没有自己的`this`，它的`this`在**定义时**继承于上级作用域
- 箭头函数的`this`不能被`call、apply、bind`等函数改变
- 箭头函数没有`prototype`。综上它不能用作构造函数用`new`操作符
- 箭头函数没有`arguments`对象
- 箭头函数不能用作`generator`函数</content:encoded><author>Asuhe</author></item><item><title>模块化与组件化</title><link>https://asuhe.org/blog/1a059597/</link><guid isPermaLink="true">https://asuhe.org/blog/1a059597/</guid><pubDate>Mon, 13 Dec 2021 20:08:29 GMT</pubDate><content:encoded>## ES6模块化

在ES6模块化规范中，每个文件都是一个模块。它的语法只能在浏览器端运行，还要借助`Babel`和`Browserify`依次编译代码才能在浏览器端运行。

### 默认暴露

默认暴露暴露出去的是一个对象。默认暴露必须用一个变量去统一接收

```js
// request.js
const a = 100;
const log = ()=&gt;{
    console.log(&apos;asuhe&apos;);
};

export default {
	a,
	log
};
/* 本质上暴露出去的是一个对象，以default为属性
{
    default:{
		a,
         log
    }
}
*/
-------
// index.js
import obj from &apos;requset.js&apos;; // 这种写法为简便写法
import {default as obj} from &apos;requset.js&apos;; // 完整写法 本质
exprot { default as obj } from &apos;request.js&apos;; // 引入并同时使用部分暴露，暴露出去
```

默认暴露只能暴露一次

### 分别暴露

分别暴露也叫部分暴露，同样是暴露出去一个对象。它在最终暴露的时候，把所有暴露的变量自动封装到对象当中

```js
// test.js
export const a = 200;
export const b = 300;
/* 实际暴露出去的对象
{
    a:100,
	b:200
}
*/
------
// index.js
import {a} from &apos;test.js&apos;;
// 部分暴露可以在引入的时候帮它改名
import { a as A } from &apos;test.js&apos;;
// 打包引入 将所有暴露的东西全部引入并封装在同一个对象里，作为属性
import * as All from &apos;test.js&apos;;
```



### 统一暴露

**统一暴露就是部分暴露的一个简便写法**，方便程序员少写一些`export`关键字。它暴露出去的也是一个对象。但它暴露出去的对象并没有被重新包装，暴露出去的对象就是它原本的那个对象

```js
// test.js
const a = 100;
const sum = (a,b) =&gt; a+b;
export {
	a,
    sum
};
/* 实际暴露出去的对象
{
    a,
	sum
}
*/
// 统一暴露可以在暴露前帮其改名，而不能在引入时帮他改名
export {
	a as A,
    sum as SUM
};
/* 改名后暴露出去的对象
{
	A,
	SUM
}
*/
-----
// index.js
import {a,sum} from &apos;test.js&apos;; // 解构更名
```

### 总结

部分暴露和默认暴露方式可以混合使用。

```js
// test.js
export const a = 100;
export default {
    a:200,
    b:300
}
// 最终暴露出去的对象
/*
{
    a:100,
    default:{
        a:200,
        b:300
    }
}
*/
```

```javascript
// 引入
import * as TEST from &quot;test.js&quot; // 将最终暴露出来的对象打包改名为TEST 类似于 TEST = 最终暴露对象
import { a as A,default as D } from &quot;test.js&quot; // 解构最终暴露对象，并将属性重新命名
```



## CommonJS模块化

在`CommonJS`模块化的规范里，每个文件都是一个模块。用它模块化的代码既可以在服务端运行也可以在浏览器端运行。但在浏览器端运行前需要用`Browserify`编译一遍。

### 基本语法

```js
// 暴露模块
module.exports = value; // 第一种方式
exports.xxx = value; // 第二种方式

// 引入模块
require(&apos;模块名&apos;); // 引入内置模块和第三方模块
requier(&apos;./xxx.js&apos;); // 引入自定义模块
```

在暴露语法中若`module.exports`和`exports`指向的是不同的对象，则引入模块的得到的是最终`module.exports`所指向的那个对象，而`exports`只是一个封装起来的语法糖。

```js
// test.js
module.exports = {
    a:100,
    b:200
}
exprots.c = 300;
exports.d = 400;
-----
const test = require(&apos;test.js&apos;);
/* test 得到的是module.exports指向的对象
{
	a:100,
	b:200
}
*/
```

`exports`和`module.exports`的关系图

![exports和module.exports的关系](https://i0.hdslb.com/bfs/album/dd021b3f8b6905f88e8bb1613f47951a3bc5f09b.png)

## 组件化

组件就是一个`html、css、js`的代码集合。它可以复用代码，简化项目的编码，提高运行效率。在组件注册的语法中实际上是把一个组件文件暴露出去的变成了一个构造组件实例对象的构造函数。每当我们使用一个组件标签的时候，在内存里就用构造函数new出了一个实例对象。</content:encoded><author>Asuhe</author></item><item><title>性能优化(二)</title><link>https://asuhe.org/blog/e5291a98/</link><guid isPermaLink="true">https://asuhe.org/blog/e5291a98/</guid><pubDate>Wed, 23 Feb 2022 13:47:57 GMT</pubDate><content:encoded># 页面优化

一个页面从输入`url`到最终呈现整个流程可以分为资源加载过程和页面渲染过程。优化页面时我们可以从这两个角度去思考得到一些性能调优的思路

## 加载过程

- DNS预取：首先从地址栏输入一个`url`我们需要将`url`地址解析，获取其中的主机地址，然后再对主机地址发送请求获取资源。这里我们先需要进行`DNS`查询，而`DNS`查询这个过程我们可以让浏览器进行`DNS预取`加快查询，电脑主机会先从本机DNS缓存中查找域名映射，若没有则会默认进行`DNS递归查询`。
- TCP preconnect：tcp调优、HTTP/2、keep-alive
- http缓存：让本地客户端缓存一些常用资源、from cache、from disk
- CND加速：多地分布服务器资源
- 合并请求：如雪碧图、nignx模块、SSR服务端渲染
- 减小资源体积：减小cookie体积（为什么cookie才4KB容量）、图片压缩、gzip压缩
- 文档加载：css样式放在html文档头部，js脚本放在html文档末尾、异步加载js

## 渲染过程

- 合并DOM操作，减少DOM操作
- 尽早开始执行js，用DOMContentLoad
- 懒加载
- 对DOM查询进行缓存
- SSR服务端渲染
- 使用防抖和节流

# 检测页面的内存泄漏

之前面试的时候被问到过这个问题，如何快速检测出页面存在内存泄露情况。当时完全懵了，不知道怎么样调试才能检测出。后来去网上搜找到了一篇文章讲的十分好，在此记录一下。[How to find memory leaks in JavaScript](https://medium.datadriveninvestor.com/how-to-find-memory-leaks-in-javascript-b898c85995a3)</content:encoded><author>Asuhe</author></item><item><title>数组去重、扁平化、函数柯里化、new、instanceof</title><link>https://asuhe.org/blog/d8ea1883/</link><guid isPermaLink="true">https://asuhe.org/blog/d8ea1883/</guid><pubDate>Sat, 05 Mar 2022 12:42:11 GMT</pubDate><content:encoded># 数组去重

```js
let arr = [1,21,3,4,5,1,3,1];
```

## 利用Set数据结构

```js
function unique(arr){
    // 利用es6的set数据结构
    return [...new Set(arr)];
}
```

## 利用Array.prototype.filter函数

```js
function unique(arr){
    return arr.filter((item,index) =&gt; {
        // 判断是否有出现索引号不一致情况，若有则说明有重复
        return arr.indexOf(item) === index;
    });
}
```

## 利用Array.prototype.includes函数

```js
function unique(arr){
    const newArr = [];
    
    arr.forEach(item =&gt; {
    // 判断新数组里是否有该item
        if(newArr.includes(item)) return;
        newArr.push(item);
    })
    
    return newArr;
}
```

## 利用Array.prototype.indexOf函数

```js
function unique(arr){
    const newArr = [];
    
    arr.forEach(item =&gt; {
        // 若item从未出现在新数组里，将item加入新数组
        if(newArr.indexOf(item) === -1) newArr.push(item)
        // if(!newArr.includes(item)) newArr.push(item)
    })
    
    return newArr;
}
```

# 数组扁平化

```js
let arr = [[1], [2, 3], [4, 5, 6, [7, 8, [9, 10, [11]]]], 12];
// 原生
arr.flat(Infinity);
```

## 利用字符串转换

```js
function flat(arr){
    let arrStr = arr.toString();
    let newArr = arrStr.split(&apos;,&apos;);
    return newArr.map(item =&gt; Number(item));
}
```

## 完美版本，不改变变量类型

```js
function flat(arr,maxDepth = 1){    
    let counter = 0;    
	// 利用迭代
    while(arr.some(item =&gt; Array.isArray(item))){
        if(counter &lt; maxDepth){
            ++counter;
            arr = [].concat(...arr);
        }        
    }
    return arr;
}
```

# 柯里化

```js
function curring(fn,...argsOne){
    // 若参数不够则继续返回函数，继续接收参数
    if(argsOne.length&lt;fn.length){
        return (...argsTwo)=&gt;{
            return curring(fn,...argsOne.concat(argsTwo));
        }
    }else{
        // 若参数足够则直接返回函数调用结果
        return fn.call(this,...argsOne);
    }
}
function add(a,b,c,d,e){
    return a + b + c + d + e;
}
let fn = curring(add);

console.log(fn(1,2,3,4,5))
console.log(fn(1,2,3,4)(5))
console.log(fn(1,2)(3,4,5))
console.log(fn(1)(2)(3)(4)(5))
```

# new

new操作符做的事情

- 判断操作对象是否为函数，若是则创建一个空对象。若不是则抛出类型错误
- 将该空对象的原型指向构造函数的原型
- 执行构造函数，将this指向该空对象
- 判断构造函数执行结果，若为对象则返回对象，若不是则返回开始时创建的对象

```js
function myNew(Constructor,...args){
   if(Constructor instanceof Function){
       const obj = Object.create(Constructor.prototype);
       const res = Constructor.call(obj,...args);
       return res instanceof Object ? res : obj;
   }else{
       // 值不是函数类型，抛出错误
       throw new Error(`TypeError:${Constructor} is not a Constructor`);
   }
}
```

# instanceof

`instanceof`就是根据构造函数的原型链网上查找，找到即返回`true`，找不到则返回`false`

- 判断右操作数是否为函数类型，是则继续。否则返回类型错误
- 左操作数的原型不是则继续获取该原型的原型
- 若最终找到`Object.prototype`的`__proto`则返回false，查找失败。否则返回true

```js
function myInstanceof(left,right){
    if(typeof right === &apos;function&apos;){
        let Lproto = Object.getPrototypeOf(left);
        while(true){
            // 找到终点null时，直接返回false
            if(!Lproto) return false;
            if(Lproto === right.prototype) return true;
            Lproto = Object.getPrototypeOf(Lproto);
        }
    }else{
        throw new Error(`Right-hand side of &apos;instanceof&apos; is not callable`)
    }
}
```</content:encoded><author>Asuhe</author></item><item><title>TypeScript基础(一)</title><link>https://asuhe.org/blog/6fb5089e/</link><guid isPermaLink="true">https://asuhe.org/blog/6fb5089e/</guid><pubDate>Sat, 30 Apr 2022 09:59:45 GMT</pubDate><content:encoded># 环境搭建

因为`TypeScript`是无法直接在浏览器上运行的，所以需要先将`Typescript`编译成`Javascript`代码后才能使用

## 安装TS

全局安装`Typescript`：

```sh
npm install -g typescript
```

检查安装情况

```sh
tsc -v
```

## 手动编译

手动编译仅需在目录下`tsc 文件名.ts`，例如：

```sh
tsc index.ts
```

## vscode自动编译

每次都要自行编译很麻烦，在`vscode`情况下我们可以使用监视任务让他自动编译。首先生成`ts`配置文件：

```sh
// 生成配置文件tsconfig.json
tsc --init
```

然后根据需要修改配置文件，常见的修改有设置输出js文件的目录、开启严格模式等。**根据每项参数后面的注解按需修改配置**。

```json
// tsconfig.json内设置编译好的js文件输出的目录
&quot;outDir&quot;:&quot;./js&quot; // 输出到当前文件夹下的js文件夹里面
```

最后启动监视任务，监视`tsconfig.json`文件

```sh
终端 -&gt; 运行任务 -&gt; 监视tsconfig.json
```

![image-20220430102519183](https://s2.loli.net/2022/04/30/5bTuPGsoLtiM3Sc.png)

## webpack打包编译

下载依赖并配置使用即可

```sh
yarn add -D typescript  // 下载ts编译包
yarn add -D ts-loader   // 下载ts的loader
```

# 基础类型

**TS的基本数据类型和JS是一致的。**

除了基本数据类型，TS新增了`统一类型的数组`、`元组`、`枚举`、`any`和`void`类型。

## 变量类型指定

我们可以像强类型语言一样指定变量的类型，仅需在后面`:指定类型`就可以完成类型检查

```typescript
let num1:number = 10 // ok
let num2:number = &apos;a&apos; // error
// 自动类型推断
let num3 = 10 // 根据初始赋值推断出变量类型为number
num3 = 20 // ok
num3 = &apos;a&apos; // error
```

## 函数返回值指定

指定函数的返回值类型

```typescript
function fn(x:number,y:number):number{ // 指定函数返回值类型为number
	return x + y;
}
```



## 数组

TS的数组定义像`C/C++`那样的强类型语言一样，指定数组内只能存同种类型的数据。可以在元素类型后面接上`[]`，表示由此类型元素组成的一个数组：

```typescript
let numArr:number[] = [1,2,3] // 指定只能存数字类型的数据
let numArr:number[] = [1,&apos;a&apos;,&apos;c&apos;] // 能强行通过编译，但是会报错
```

或者使用泛型声明：

```typescript
let numArr:Array&lt;number&gt; = [1,2,3]
```

## 元组

元组类型允许表示一个已知元素数量和类型的数组，`各元素的类型不必相同`。听起来像`javascript`原生的数组，但是并不一样。元组的限制很大，会固定数据的排列方式和数组大小。

```typescript
let arr:[string,number] // 和 let arr = [] 不一样
arr = [&apos;hello&apos;,10] // ok
arr = [10,&apos;hello&apos;] // error 报错

// 当访问一个已知索引的元素，能够得到正确的类型，会有智能提示.js原生数组不具有
console.log(arr[0].substring(1)) // ok
console.log(arr[1].substring(1)) // error
```

## 枚举

枚举类型`enum`类似`C/C++`里的枚举，能够修改枚举的值。默认枚举值是1，后续枚举值依次递增+1的

```typescript
enum Color{
    red,
    green,
    yellow,
    blue
}
// 枚举值默认从0递增
console.log(Color.red) // 0
console.log(Color.green) // 1
console.log(Color.yellow) // 2
console.log(Color.blue) // 3
```

可以手动指定枚举值

```typescript
enum Color{
    red = 100,
    green,
    yellow = 200,
    blue
}
// 手动指定枚举值后，后续枚举值会在此基础上+1递增
console.log(Color.red) // 100
console.log(Color.green) // 101
console.log(Color.yellow) // 200
console.log(Color.blue) // 201
```

还可以利用类似索引的方式反过来取值

```typescript
enum Color{
    red = 100,
    green,
    yellow = 200,
    blue
}
// 可以根据对应的枚举值反向取值
console.log(Color[100]) // red
console.log(Color[101]) // green
console.log(Color[200]) // yellow
console.log(Color[201]) // blue
```

## any

有时候，我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容，比如来自用户输入或第三方代码库。 这种情况下，我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 `any` 类型来标记这些变量：

```typescript
let a:any = 10
a = 20 // ok
a = &apos;abc&apos; // ok
```

## void

某种程度上来说，`void` 类型像是与 `any` 类型相反，它`表示没有任何类型`。 当一个函数没有返回值时，你通常会见到其返回值类型是 `void`：

```typescript
/* 表示没有任何类型, 一般用来说明函数的返回值不能是undefined和null之外的值 */
function fn(): void {
  console.log(&apos;fn()&apos;)
  // return undefined
  // return null
  // return 1 // error
}
```

## 联合类型

联合类型（Union Types）表示取值可以为多种类型中的一种

```typescript
// 需求1: 定义一个一个函数得到一个数字或字符串值的字符串形式值
function toString2(x: number | string) : string {
  return x.toString()
}

// 需求2: 定义一个一个函数得到一个数字或字符串值的长度
function getLength(x: number | string) {
  // return x.length // error
  if (x.length) { // error
    return x.length
  } else {
    return x.toString().length
  }
}
```

## 类型断言

通过类型断言这种方式可以告诉编译器，“相信我，我知道自己在干什么”。 类型断言好比其它语言里的类型转换，但是不进行特殊的数据检查和解构。 它没有运行时的影响，只是在编译阶段起作用。 TypeScript 会假设你，程序员，已经进行了必须的检查。

类型断言有两种形式。 其一是`&lt;&gt;`语法, 另一个为 `as` 语法

```typescript
/* 
类型断言(Type Assertion): 可以用来手动指定一个值的类型
语法:
    方式一: &lt;类型&gt;值
    方式二: 值 as 类型  tsx中只能用这种方式
*/

/* 需求: 定义一个函数得到一个字符串或者数值数据的长度 */
function getLength(x: number | string) {
  if ((&lt;string&gt;x).length) {
    return (x as string).length
  } else {
    return x.toString().length
  }
}
console.log(getLength(&apos;abcd&apos;), getLength(1234))
```</content:encoded><author>Asuhe</author></item><item><title>TypeScript基础(二)</title><link>https://asuhe.org/blog/c08493fc/</link><guid isPermaLink="true">https://asuhe.org/blog/c08493fc/</guid><pubDate>Sat, 30 Apr 2022 18:56:33 GMT</pubDate><content:encoded># 接口

接口是对象的状态(属性)和行为(方法)的抽象(描述)，**本质上是一种对对象的约束**。例如我们要求一个对象必须要有`id`这个属性，且属性值必须为`number`。这在`javascript`上是做不到的，但在`typescript`中我们可以利用接口来实现。

```typescript
// 需求：对象里必须要有id这个属性,且属性值为number类型
// 声明一个接口
interface IObj{ // 用I开头表示这是一个接口
	id:number
}
let obj1:IObj = {
    id:1, // ok
    name:&apos;asuhe&apos; // error 接口中未声明name属性
}
let obj2:IObj = {
    id:&apos;a&apos;, // error 数据类型错误
}
```

## 可选属性 | 参数

在接口里约定的属性或参数都是必选的，若我们要求在接口中约定一些属性为可选属性那么只需要在后面加上`?`

```typescript
// 需求：要求一个人的信息必须有名字，身高体重可选填
interface IPerson {
	name:string,
    height?:number,
    weight?:number
}
let obj:IPerson = {
	name:&apos;asuhe&apos; // ok
}
```

## 只读属性 | 参数

若我们想对象里某个参数只能读取不能修改，那么仅需在前面加上`readonly`

```typescript
// 需求：货物编号只能读取不能修改
interface IGoods {
	readonly id:number,
    name:string,
    weight?:number
}
let obj:IGoods = {
    id:1,
    name:&apos;asuhe&apos;
}
console.log(obj.id);
obj.id = 1 // error 不能赋值，只读属性 
```

##  readonly vs const

最简单判断该用 `readonly` 还是 `const` 的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 `const`，若做为属性则使用 `readonly`。

## 函数类型

接口能够描述 `JavaScript `中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外，接口也可以描述函数类型。

为了使用接口表示函数类型，我们需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

```typescript
/* 
接口可以描述函数类型(参数的类型与返回的类型)
*/

interface ISum {
  (a:number,b:number):number
}
let sum:ISum = function(x:number,y:number):number{
    return x + y
}
console.log(sum(1,2)) // 3
```

# 类中使用接口

与 C# 或 Java 里接口的基本作用一样，TypeScript 也能够用它来明确的强制一个类去符合某种契约。

```typescript
  /* 
  类类型: 实现接口
  1. 一个类可以实现多个接口
  2. 一个接口可以继承多个接口
  */
  interface IPerson {
    name: string,
    age: number
  }
  interface IStudent {
    course: string[]
  }
  // 类实现接口
  class Person implements IPerson {
    name: string
    age: number
    constructor(name: string, age: number) {
      this.name = name,
        this.age = age
    }
  }
  // 一个类实现多个接口
  class Student implements IPerson, IStudent {
    name: string
    age: number
    course: string[]
    constructor(name: string, age: number, course: string[]) {
      this.name = name
      this.age = age
      this.course = Array.from(course)
    }
  }
  // 一个接口继承多个接口
  interface IStudentAndPerson extends IPerson,IStudent{
    
  }
```</content:encoded><author>Asuhe</author></item><item><title>TypeScript基础(三)</title><link>https://asuhe.org/blog/be77b3d7/</link><guid isPermaLink="true">https://asuhe.org/blog/be77b3d7/</guid><pubDate>Sun, 01 May 2022 22:08:50 GMT</pubDate><content:encoded># 类

`typescript`里的类和`javascript`里的类基本一样。`typerscript`扩展了类，它可以通过先声明的方式限制实例属性的类型，并且还引入了类似`C++`、`Java`的继承权限设定，拥有`public`、`protect`、`private`三种修饰符。**若不声明权限则默认为`public`**

| 修饰符    | 外部访问 | 派生类访问 | 自身访问 |
| --------- | -------- | ---------- | -------- |
| public    | √        | √          | √        |
| protected | ×        | √          | √        |
| private   | ×        | ×          | √        |

## public

`public`修饰的属性可以在外部访问，也可以在派生类和自身实例中访问

```typescript
class Person {
    name: string
    age: number
    constructor(name:string, age:number) {
        this.name = name
        this.age = age
    }
    say(): void {
        console.log(`My name is ${this.name},I&apos;m ${this.age} years old`)
    }
}

class Student extends Person {
    course: string
    constructor(name:string, age:number, course:string) {
        super(name, age)
        this.course = course
    }
    say() {
        super.say()
        console.log(`My course is ${this.course}`)
    }
}

let man1: Person = new Person(&apos;sphinx&apos;, 16)
console.log(man1.name) // sphinx
man1.say() // My name is sphinx,I&apos;m 16 years old
let man2: Student = new Student(&apos;asuhe&apos;, 16, &apos;math&apos;)
man2.say() // My name is asuhe,I&apos;m 16 years old 
           // My course is math
```

## protected

`protected`修饰的属性不可以在外部访问，但可以在派生类和自身实例中访问

```typescript
class Person {
    protected name: string
    protected age: number
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
    say(): void {
        console.log(`My name is ${this.name},I&apos;m ${this.age} years old`)
    }
}

class Student extends Person {
    course: string
    constructor(name: string, age: number, course: string) {
        super(name, age)
        this.course = course
    }
    say() {
        super.say()
        console.log(`My course is ${this.course}`)
    }
}

let man1: Person = new Person(&apos;sphinx&apos;, 16)
console.log(man1.name) // error
man1.say() // My name is sphinx,I&apos;m 16 years old
let man2: Student = new Student(&apos;asuhe&apos;, 16, &apos;math&apos;)
man2.say() // My name is asuhe,I&apos;m 16 years old 
           // My course is math
```

## private

`protected`修饰的属性仅能自身实例中访问

```typescript
class Person {
    private name: string
    private age: number
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
    say(): void {
        console.log(`My name is ${this.name},I&apos;m ${this.age} years old`)
    }
}

let man1: Person = new Person(&apos;sphinx&apos;, 16)
console.log(man1.name) // error
console.log(man1.age) // error
man1.say() // My name is sphinx,I&apos;m 16 years old
```

## readonly vs 权限修饰符

当我们希望一个属性仅可自身访问且仅可读取时，可以和`readonly`修饰符一起使用

```typescript
class Goods {
	public readonly id:number
    constructor(id:number){
        this.id = id
    }
}
```

## 存取器

`TypeScript` 支持通过 `getters/setters` 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

下面来看如何把一个简单的类改写成使用 `get` 和 `set`。 首先，我们从一个没有使用存取器的例子开始。

```typescript
class Person {
  firstName: string = &apos;A&apos;
  lastName: string = &apos;B&apos;
  get fullName () {
    return this.firstName + &apos;-&apos; + this.lastName
  }
  set fullName (value) {
    const names = value.split(&apos;-&apos;)
    this.firstName = names[0]
    this.lastName = names[1]
  }
}

const p = new Person()
console.log(p.fullName)

p.firstName = &apos;C&apos;
p.lastName =  &apos;D&apos;
console.log(p.fullName)

p.fullName = &apos;E-F&apos;
console.log(p.firstName, p.lastName)
```



## 抽象类

抽象类做为其它派生类的基类使用。 **它们不能被实例化。**不同于接口，抽象类可以包含成员的实现细节。 `abstract` 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

```typescript
/* 
抽象类
  不能创建实例对象, 只有实现类才能创建实例
  可以包含未实现的抽象方法
*/
abstract class Animal {
  abstract cry ()
  run () {
    console.log(&apos;run()&apos;)
  }
}

class Dog extends Animal {
  cry () {
    console.log(&apos;Dog cry()&apos;)
  }
}

const dog = new Dog()
dog.cry()  // Dog cry()
dog.run() // run()
```

## 抽象类 vs 接口

接口可以理解为里面所有属性都是`abstract`，而且使用了接口的类必须按接口的规格实现。接口的函数只能是定义，不能有函数体不能实现。

抽象类里的函数方法可以为`abstract`也可以不为`abstract`，而且抽象类的函数方法可以有函数体能够去实现。若抽象类的函数方法被实现类重写了，那么在实现类调用该方法时会调用实现类重写的那个方法。</content:encoded><author>Asuhe</author></item><item><title>TypeScript基础(四)</title><link>https://asuhe.org/blog/44345cd8/</link><guid isPermaLink="true">https://asuhe.org/blog/44345cd8/</guid><pubDate>Mon, 02 May 2022 16:26:03 GMT</pubDate><content:encoded># 函数

和 JavaScript 一样，TypeScript 函数可以创建有名字的函数和匿名函数，但是在此基础上**Typescript支持函数重载**。

**TypeScript 里的每个函数参数都是必须的。** 这不是指不能传递 `null` 或 `undefined` 作为参数，而是说编译器检查用户是否为每个参数都传入了值。编译器还会假设只有这些参数会被传递进函数。 **简短地说，传递给一个函数的参数个数必须与函数期望的参数个数一致。**

JavaScript 里，每个参数都是可选的，可传可不传。 没传参的时候，它的值就是 `undefined`。 **在TypeScript 里我们可以在参数名旁使用 `?` 实现可选参数的功能。** 比如，我们想让 `name` 是可选的：

```typescript
function fn(name?:string):void{
	console.log(name ? name : &apos;asuhe&apos;)
}
```

## 剩余参数

必要参数，默认参数和可选参数有个共同点：它们表示某一个参数。 有时，你想同时操作多个参数，或者你并不知道会有多少参数传递进来。 在 JavaScript 里，你可以使用 `arguments` 来访问所有传入的参数。

在 TypeScript 里，你可以把所有参数收集到一个变量里：
剩余参数会被当做个数不限的可选参数。 可以一个都没有，同样也可以有任意个。 编译器创建参数数组，名字是你在省略号（ `...`）后面给定的名字，你可以在函数体内使用这个数组。

```typescript
function info(x: string, ...args: string[]) {
  console.log(x, args)
}
info(&apos;abc&apos;, &apos;c&apos;, &apos;b&apos;, &apos;a&apos;)
```

## 函数重载

函数重载: 函数名相同, 而形参不同的多个函数
在JS中, 由于弱类型的特点和形参与实参可以不匹配, 是没有函数重载这一说的 但在TS中, 与其它面向对象的语言(如Java)就存在此语法

```typescript
/* 
函数重载: 函数名相同, 而形参不同的多个函数
需求: 我们有一个add函数，它可以接收2个string类型的参数进行拼接，也可以接收2个number类型的参数进行相加 
*/

// 重载函数声明
function add (x: string, y: string): string
function add (x: number, y: number): number

// 定义函数实现
function add(x: string | number, y: string | number): string | number {
  // 在实现上我们要注意严格判断两个参数的类型是否相等，而不能简单的写一个 x + y
  if (typeof x === &apos;string&apos; &amp;&amp; typeof y === &apos;string&apos;) {
    return x + y
  } else if (typeof x === &apos;number&apos; &amp;&amp; typeof y === &apos;number&apos;) {
    return x + y
  }
}

console.log(add(1, 2))
console.log(add(&apos;a&apos;, &apos;b&apos;))
// console.log(add(1, &apos;a&apos;)) // error
```

## 命名空间

`Typescript`还支持类似于`C++`的命名空间，**它最主要的作用就是防止变量冲突**。每个命名空间都必须处于顶级作用域内，命名空间内的变量或函数需要对外暴露时仅需要在前面加上`export`关键字。

```typescript
// index.ts
namespace K {
    export const name = &quot;aushe&quot;
}
namespace N {
    export const name  = &quot;sphinx&quot;
}
console.log(K.name) // asuhe
consoe.log(N.name) // sphinx
```



# 泛型

**泛型**指在定义函数、接口或类的时候，不预先指定具体的类型，而在使用的时候再指定具体类型的一种特性。

下面创建一个函数, 实现功能：根据指定的数量 `count` 和数据 `value` , 创建一个包含 `count` 个 `value` 的数组 不用泛型的话，这个函数可能是下面这样：

```typescript
function createArray(value: any, count: number): any[] {
  const arr: any[] = [];
  for (let i = 0; i &lt; count; i++) {
    arr.push(value);
  }
  return arr;
}
const arr = createArray(&apos;asuhe&apos;,5);
console.log(arr[0].split(&apos;&apos;)) // 不会提示split函数，编辑器无法推断arr[0]就是string
```

但我们如果使用泛型，那么就可以解决这个问题。代码如下：

```typescript
function createArray&lt;T&gt;(value: T, count: number){
  const arr: Array&lt;T&gt; = [];
  for (let i = 0; i &lt; count; i++) {
    arr.push(value);
  }
  return arr;
}
const arr = createArray(&apos;asuhe&apos;,5);
console.log(arr[0].split(&apos;&apos;)) // 能够提示split函数可以使用
```

一个函数还可以定义多个泛型参数，**泛型参数的命名同其它变量一样，但是惯例来说需要大写且仅一个字母**：

```typescript
function swap &lt;K, V&gt; (a: K, b: V): [K, V] {
  return [a, b]
}
const result = swap&lt;string, number&gt;(&apos;abc&apos;, 123)
console.log(result[0].length, result[1].toFixed())
```

## 泛型接口

在定义接口时，为接口中的属性或方法定义泛型类型

在使用接口时，再指定具体的泛型类型

```typescript
interface IbaseCRUD &lt;T&gt; {
  data: T[]
  add: (t: T) =&gt; void
  getById: (id: number) =&gt; T
}

class User {
  id?: number; //id主键自增
  name: string; //姓名
  age: number; //年龄

  constructor (name, age) {
    this.name = name
    this.age = age
  }
}

class UserCRUD implements IbaseCRUD &lt;User&gt; {
  data: User[] = []
  
  add(user: User): void {
    user = {...user, id: Date.now()}
    this.data.push(user)
    console.log(&apos;保存user&apos;, user.id)
  }

  getById(id: number): User {
    return this.data.find(item =&gt; item.id===id)
  }
}

const userCRUD = new UserCRUD()
userCRUD.add(new User(&apos;tom&apos;, 12))
userCRUD.add(new User(&apos;tom2&apos;, 13))
console.log(userCRUD.data)
```

## 泛型类

在定义类时, 为类中的属性或方法定义泛型类型 在创建类的实例时, 再指定特定的泛型类型

```typescript
class GenericNumber&lt;T&gt; {
  zeroValue: T
  add: (x: T, y: T) =&gt; T
}

let myGenericNumber = new GenericNumber&lt;number&gt;()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function(x, y) {
  return x + y 
}

let myGenericString = new GenericNumber&lt;string&gt;()
myGenericString.zeroValue = &apos;abc&apos;
myGenericString.add = function(x, y) { 
  return x + y
}

console.log(myGenericString.add(myGenericString.zeroValue, &apos;test&apos;))
console.log(myGenericNumber.add(myGenericNumber.zeroValue, 12))
```

## 泛型约束

如果我们直接对一个泛型参数取 `length` 属性, 会报错, 因为这个泛型根本就不知道它有这个属性

```typescript
// 没有泛型约束
function fn &lt;T&gt;(x: T): void {
  console.log(x.length)  // error
}	
```

我们可以使用泛型约束来实现

```typescript
interface Lengthwise {
  length: number;
}

// 指定泛型约束
function fn2 &lt;T extends Lengthwise&gt;(x: T): void {
  console.log(x.length)
}
// 我们需要传入符合约束类型的值，必须包含必须 `length` 属性：
fn2(&apos;abc&apos;)
fn2(123) // error  number没有length属性
```</content:encoded><author>Asuhe</author></item><item><title>Webpack基础(一)</title><link>https://asuhe.org/blog/1e75b8f/</link><guid isPermaLink="true">https://asuhe.org/blog/1e75b8f/</guid><pubDate>Mon, 02 May 2022 16:25:28 GMT</pubDate><content:encoded># webpack简介

官方说，webpack is a static module bundler for modern JavaScript applications.

翻译过来：webpack是一个静态的模块化打包工具，为现代的JavaScript应用程序；

-  我们来对上面的解释进行拆解： p打包bundler：webpack可以将帮助我们进行打包，所以它是一个打包工具
- 静态的static：这样表述的原因是我们最终可以将代码打包成最终的静态资源（部署到静态服务器）； 
- 模块化module：webpack默认支持各种模块化开发，ES Module、CommonJS、AMD等； 
- 现代的modern：我们前端说过，正是因为现代前端开发面临各种各样的问题，才催生了webpack的出现和发展；

# 基础安装

## webpack和webpack-cli

`webpack`是执行打包的本体，它原生只支持打包`Javascript`和`Json`文件，若需要打包如`html`、`css`、`image`等资源则要安装相应的`loader`。

`webpack-cli`是调用`webpack`的一种手段。通常我们调用`webpack`都是直接在命令行里调用的，而这正是`webpack-cli`的功劳。如果没有安装`webpack-cli`那么我们将不能直接从命令行里读取参数。它起到的是一个传递命令行参数到`webpack`中的功能。实际上，当我们使用Vue或React的脚手架时，我们并没有安装`webpack-cli`这个东西。Vue有`vue-cli`而React有`creat-react-app`，它们替代了`webpack-cli`传递命令行参数到`webpack`的功能。

## 安装

```sh
# 全局安装 webpack5 和 webpack-cli4
npm install -g webpack@5 webpack-cli@4

# 局部安装
npm install -D webpack@5 webpack-cli@4
```

# 基础使用

**当我们直接使用`webpack`命令时，调用的是我们全局安装的`webpack`。**

如果我们需要使用当前项目中的`webpack`版本打包项目，有三种方式分别是：

第一种直接找到本地安装路径，调用

```sh
# 直接找到node_modules/.bin下的webpack文件打包
node_modules\.bin\webpack
```
第二种使用`npx`

```sh
# 使用npx，npx可以自动去node_modules文件夹下寻找文件
npx webpack
```

第三种在配置文件`package.json`中配置命令，该方法最常用

```json
// 在配置文件package.json的&quot;script&quot;里的配置命令
&quot;scripts&quot;: {
    &quot;build&quot;: &quot;webpack&quot; // package.json会优先在本地node_modules中寻找，若找不到则会往上查找全局环境下的webpack
}
```

```sh
# 调用scripts里的命令
npm run build
```

## 配置package.json

当我们使用`webpack`打包文件时，它会默认从当前目录下找到`src`文件夹下的`index.js`。所有引入或关联到`index.js`里的资源都会被`webpack`打包，否则不会被打包。 

默认打包的文件会生成在当前目录的`dist`文件夹下，名称为`main.js`。

![image-20220503175000507](https://s2.loli.net/2022/05/03/9EKh7lmZjntfxGq.png)

在通常情况下，webpack需要打包的项目是非常复杂的，并且我们需要一系列的配置来满足要求，默认配置必然是不可以的。

我们可以在根目录下创建一个`webpack.config.js`文件，来作为`webpack`的配置文件：

```javascript
// 需求：将入口文件改为 main.js ，输出文件改为 bundle.js
// 方法一 常用方法

// webpack.config.js
const path = require(&apos;path&apos;);
// 导出配置信息
module.exports = {
	entry: &quot;./scr/main.js&quot;, // 入口文件更改为 main.js
    output: {
		filename: &quot;bundle.js&quot;, // 输出文件的名称更改为bundle.js
         path: path.resolve(__dirname,&quot;./dist&quot;)
    }
}
```

```sh
# 方法二
npx webpack --entry ./src/main.js --output-path ./build
```

**默认情况下`webpack`从`webpack.config.js`里读取配置**，当我需要更改`webpack`读取配置的文件时，可以通过`--config`来指定配置文件

```sh
# 指定 wk.config.js 为 webpack 的配置文件
webpack --config wk.config.js
```

或者通过`package.json`指定

```json
// package.json
&quot;scripts&quot;: {
    &quot;build&quot;: &quot;webpack --config wk.config.js&quot;
}
```

## 配置Loader

开头我们说过，`webpack`原生只支持打包`javascript`文件和`json`文件。当我们需要打包其它资源时，就要配置相应的`loader`来支持，否则会报错。

### css-loader

使用`loader`前我们需要安装好`loader`。安装`css-loader`：

```sh
npm install css-loader -D
```

使用`loader`通常有三种方式，分别是：

- 内联方式
- cli方式
- 配置文件方式

#### 内联方式

内联方式就是在引入的样式前加上使用的loader，并且使用`!`分割每个`loader`。这种方式只能作用在当前文件下，若其它文件下也引入了css文件则需要再次配置。所以不常使用。

```javascript
// index.js中引入css文件并使用css-loader和style-loader
import &quot;style-loader!css-loader../css/index.css&quot; // 在路径前面加上loader名称
```

#### cli方式

在`webpack5`的文档中已经没有了`--module-bind`命令，所以`webpack5`不支持该模式。具体配置方式可查找[webpack4官方文档](https://v4.webpack.docschina.org/concepts/)

#### 配置文件方式

该方式是最常见的`loader`使用方式。其意思是在我们的`webpack.config.js`文件中写明配置信息。在该文件暴露的对象中，使用`module.rules`来配置`loader`

```javascript
// webpack.config.js
const path = require(&apos;path&apos;);
// 导出配置信息
module.exports = {
	entry: &quot;./scr/main.js&quot;,
    output: {
		filename: &quot;bundle.js&quot;,
         path: path.resolve(__dirname,&quot;./dist&quot;)
    }
    module: {
    	rules: [ // rules属性对应的值是一个数组：[Rule] 。数组中存放的是一个个的Rule，Rule是一个对象，对象中可以设置多个属性:
    		{
    			test: /\.css$/, // 用于对 resource（资源）进行匹配的，通常会设置成正则表达式
    			// loader:&quot;css-loader&quot; // 写法一
    			// use: [&quot;css-loader&quot;] // 写法二
    			// 写法三
    			use: [ // 对应的值时一个数组：[UseEntry] 。UseEntry是一个对象，可以通过对象的属性来设置一些其他属性:

    				{loader:&quot;css-loader&quot;,options:&apos;xxx&apos;} // 完整写法，options可以对css-loader传入参数
					// 若无需传参则可简写为写法二
					// 若处理该文件仅使用一个loader，可简写为写法一
    			]
                // loader:&apos;css-loader&apos; 写法四。 loader属性： Rule.use: [ { loader } ] 的简写。
			}
		]
	}
}
```

### style-loader

当我们把css文件打包好后，css文件并不会生效。此时我们还需要使用`style-loader`将打包好的css插入页面。安装`style-loader`：

```sh
npm install style-loader -D
```

配置`style-loader`。这里需要注意的是，使用多个`loader`处理同一个资源时，`loader`的调用顺序是从后往前的。所以`loader`的顺序需要将`style-loader`放在最后。配置书写如下：

```javascript
// webpack.config.js
module: {
	rules: [
        {
            test: /\.css$/,
            use: [
                {loader: &quot;style-loader&quot;}, // 然后再插入页面
                {loader: &quot;css-loader&quot;} // 先将css文件解析打包
            ]
        }
    ]
}
```

### less-loader

打包`less`文件的步骤也和上面的文件同理，但是`less-loader`本身并不能处理`less`文件，所以安装时还需要另外安装`less`处理工具，`less-loader`仅仅只是调用了`less`处理工具而已。安装`less`处理工具和`less-loader`：

```sh
npm install less -D  # 安装less处理工具
npm install less-loader -D # 安装less-loader
```

配置`less-loader`：

```javascript
// webpack.config.js
module: {
    rules: [
        {
            test: /\.css$/,
            use: [
                {loader: &quot;style-loader&quot;},
                {loader: &quot;css-loader&quot;},
                {loader: &quot;less-loader&quot;}
            ]
        }
    ]
}
```

### browserslist

开发中我们经常需要去处理浏览器兼容性问题，我们不可能针对每个浏览器一个一个去配置兼容性。通常的做法是根据浏览器的市场占有率去选择性兼容。在开发中有专门的工具如`autoprofixer`、`PostCss`、`postcss-preset-env`去帮助我们自动生成兼容代码，而这里我们首先要解决的是关于浏览器的市场占有率等信息如何自动获取。**`browserslist`就是帮助我们获取浏览器占有率，指定要配置哪些浏览器的工具。它可以让我们在不同的前端工具之间，共享目标浏览器和Node.js版本的配置。**

安装`browserslist`：

```sh
npm install browserslist -D
```

配置`browserslist`。配置它有两种方式，一是通过`package.json`，二是单独编写`.browserslistrc`文件配置当有该文件时会默认读取该文件中的配置。

`package.json`配置：

```json
&quot;browserslist&quot;: [
    &quot;last 2 version&quot;,
    &quot;not dead&quot;,
    &quot;&gt; 1%&quot;
]
```

我们还可以通过编辑`.browserslistrc`文件去配置：

```
last 2 version
not dead
&gt; 1%
```

Browserslist编写规则：

- defaults：Browserslist的默认浏览器（&gt; 0.5%, last 2 versions, Firefox ESR, not dead）
- 5%：通过全局使用情况统计信息选择的浏览器版本。 &gt;=，&lt;和&lt;=工作过
  - 5% in US：使用美国使用情况统计信息。它接受两个字母的国家/地区代
  - 5% in alt-AS：使用亚洲地区使用情况统计信息。有关所有区域代码的列表，请参见caniuse-lite/data/regions
  - 5% in my stats：使用自定义用法数据
  - 5% in browserslist-config-mycompany stats：使用 来自的自定义使用情况数据browserslist-config-mycompany/browserslist-stats.json
  - cover 99.5%：提供覆盖率的最受欢迎的浏览器
  - cover 99.5% in US：与上述相同，但国家/地区代码由两个字母组成
  - cover 99.5% in my stats：使用自定义用法数据。 
- dead：24个月内没有官方支持或更新的浏览器
- last 2 versions：每个浏览器的最后2个大版本
  - last 2 Chrome versions：最近2个版本的Chrome浏览器
  - last 2 major versions或last 2 iOS major versions：最近2个主要版本的所有次要/补丁版本
-  node 10和node 10.4：选择最新的Node.js10.x.x 或10.4.x版本
  - current node：Browserslist现在使用的Node.js版本
  - maintained node versions：所有Node.js版本，仍由 Node.js Foundation维护
- iOS 7：直接使用iOS浏览器版本7
  - Firefox &gt; 20：Firefox的版本高于20 &gt;=，&lt;并且&lt;=也可以使用。它也可以与Node.js一起使用
  - ie 6-8：选择一个包含范围的版本
  - Firefox ESR：最新的[Firefox ESR]版本
  - PhantomJS 2.1和PhantomJS 1.9：选择类似于PhantomJS运行时的Safari版本
- extends browserslist-config-mycompany：从browserslist-config-mycompanynpm包中查询
- supports es6-module：支持特定功能的浏览器。 es6-module这是“我可以使用” 页面feat的URL上的参数。有关所有可用功能的列表，请参见 。caniuse- lite/data/features
- browserslist config：在Browserslist配置中定义的浏览器。在差异服务中很有用，可用于修改用户的配置，例如 browserslist config and supports es6-module
- since 2015或last 2 years：自2015年以来发布的所有版本（since 2015-03以及since 2015-03-10)
- unreleased versions或unreleased Chrome versions：Alpha和Beta版本
- not ie &lt;= 8：排除先前查询选择的浏览器

根据上述编写规则，我们编写了多个条件之后，多个条件之间是什么关系呢？

![image-20220507105656473](https://s2.loli.net/2022/05/07/2rFqXUIKdk5T1NY.png)

刚刚我们在`.browserslistrc`中书写的规则没有添加关键字，默认为`or`的关系</content:encoded><author>Asuhe</author></item><item><title>Webpack基础(三)</title><link>https://asuhe.org/blog/d025e0c6/</link><guid isPermaLink="true">https://asuhe.org/blog/d025e0c6/</guid><pubDate>Sat, 07 May 2022 20:44:02 GMT</pubDate><content:encoded># webpack中的plugin

`Webpack`的另一个核心是`Plugin`，`Loader`是用于特定的模块类型进行转换用于处理资源文件，而`Plugin`可以用于执行更加广泛的任务，比如打包优化、资源管理、环境变量注入等。例如：生成html文件、将js/css文件插入html文件中。

## CleanWebpackPlugin

`CleanWebpackPlugin`的作用就是每次修改了一些配置，重新打包时，自动删除dist文件夹。

安装`CleanWebpackPlugin`：

```sh
npm install clean-webpack-plugin -D
```

配置`CleanWebpackPlugin`：

```javascript
// webpack.conifg.js
const { CleanWebpackPlugin } = require(&quot;clean-webpack-plugin&quot;);

module.exports = {
    plugins: [
        new CleanWebpackPlugin()
    ]
}
```

## HtmlWebpackPlugin

`HtmlWebpackPlugin`的作用就是用于生成`index.html`入口文件。

安装`HtmlWebpackPlugin`：

```sh
npm install html-webpack-plugin -D
```

配置`HtmlWebpackPlugin`：

```javascript
const HtmlWebpackPlugin = require(&quot;html-webpack-plugin&quot;);

module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            title: &quot;Asuhe&quot; // 给index.html文件添加title标签内容
            // 指定我们要使用的模块所在的路径
            /*
            template: &quot;./public/index.html&quot;
            */
        })
    ]
}
```

有时候我们想使用自己的模板而不用默认的`html`模板，这时我们就需要修改模板`index.html`文件。**在`html-webpack-plugin`的源码中，有一个default_index.ejs模块**，我们若要定义自己的`html`模板只需要用`ejs`的语法修改该文件即可。

## DefinePlugin

`DefinePlugin`是用于定义一些项目中的全局变量的，项目中所有文件都可以读取这些全局变量。它是一个`webpack`内置的插件，不需要单独安装

配置`DefinePlugin`：

```javascript
const { DefinePlugin } = require(&quot;webpack&quot;);
module.exports = {
    plugins: [
        new DefinePlugin({
            BASE_URL: &apos;&quot;./&quot;&apos;
        })
    ]
}
```

## CopyWebpackPlugin

`CopyWebpackPlugin`能够实现文件复制功能。使用`CopyWebpackPlugin`可以将指定目录下的文件复制到指定位置

安装`CopyWebpackPlugin`：

```sh
npm install copy-webpack-plugin -D
```

配置`CopyWebpackPlugin`：

```javascript
// webpack.config.js -&gt; plugin
module.exports = {
    plugins: [
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: &quot;public&quot;, // 设置从哪一个源中开始复制
                    // to: &quot;xxx&quot; 复制到的位置，可以省略，会默认复制到打包的目录下
                    globOptions: { // 设置一些额外的选项，其中可以编写需要忽略的文件
                        ignore: [
                            &apos;**/.DS_Store&apos;,
                            &apos;**/index.html&apos;
                        ]
                    }
                }
            ]
        })
    ]
}
```

# webpack中的mode</content:encoded><author>Asuhe</author></item><item><title>Webpack基础(二)</title><link>https://asuhe.org/blog/aed6c0ed/</link><guid isPermaLink="true">https://asuhe.org/blog/aed6c0ed/</guid><pubDate>Sat, 07 May 2022 10:28:26 GMT</pubDate><content:encoded># PostCSS工具

## 什么是PostCSS 

`PostCSS`是一个**通过JavaScript来转换样式的工具**，这个工具可以帮助我们进行一些CSS的转换和适配，比如自动添加浏览器前缀、css样式的重置。但是实现这些工具，我们需要借助于PostCSS对应的插件。**主要用来处理浏览器兼容性问题。**

安装PostCss：

```shell
npm install postcss postcss-cli -D
```

使用：在`webpack`的配置文件中编写相应的`loader`规则，或者直接在命令行中使用。

因为`postcss`是独立于`webpack`的，所以在`webpack.config.js`中配置`postcss`前需要安装相应`loader`：

```shell
npm install postcss-loader -D
```

配置：

```javascript
// webpack.config.js -&gt; modules -&gt; rules -&gt; use
{
    loader: &quot;postcss-loader&quot;,
    options: {
        postcssOptions: {
            plugins: [
                require(&apos;autoprefixer&apos;)
            ]
        }
    }
}
```



## autoprefixer

`autoprefixer`可以帮助我们生成兼容各种浏览器的`css`代码，它的主要作用就是给`css`代码添加上相应的浏览器前缀。如：

```css
/* autoprefixer处理前 */
:fullscreen {
    color: red;
}
.content {
    user-select: none;
}
```

```css
/* autoprefixer处理后 */
:-ms-fullscreen {
}
:fullscreen {
}
.content {
    -webkit-user-select: none;
    	-moz-user-select: none;
    		-ms-user-select: none;
    			user-select: none;
}
```

安装`autoprefixer`：

```sh
npm install autoprefixer -D
```

单独配置`autoprefixer`在`postcss.config.js`中配置：

```javascript
// postcss.config.js
module.exports = {
    plugins: [
        require(&quot;autoprefixer&quot;)
    ]
}
```

## postcss-preset-env

postcss-preset-env也是一个postcss的插件。它可以帮助我们将一些现代的CSS特性，转成大多数浏览器认识的CSS，并且会根据目标浏览器或者运行时环境添加所需的polyfill。也包括会自动帮助我们添加autoprefixer（所以相当于已经内置了autoprefixer）。

安装`postcss-preset-env`：

```sh
npm install postcss-preset-env -D
```

配置`postcss-preset-env`：

```javascript
// postcss.config.js
module.exports = {
    plugins: [
        require(&quot;postcss-preset-env&quot;)
    ]
}
```

# 加载和处理其它资源

## file-loader

`file-loader`的作用就是帮助我们处理`import/require()`方式引入的一个文件资源，并且会将它放到我们输出的文件夹中。

安装`file-loader`：

```sh
npm install file-loader -D
```

配置`file-loader`：

```javascript
// webpack.config.js -&gt; modules -&gt; rules
{
    test: /\.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: &quot;file-loader&quot;
    }
}
```

有时候我们处理后的文件名称按照一定的规则进行显示： 比如保留原来的文件名、扩展名，同时为了防止重复，包含一个hash值等。 这个时候我们可以使用`PlaceHolders`来完成，webpack给我们提供了大量的`PlaceHolders`来显示不同的内容。

常用的placeholder： 

- [ext]： 处理文件的扩展名
- [name]：处理文件的名称
- [hash]：文件的内容，使用MD4的散列函数处理，生成的一个128位的hash值（32个十六进制）
- [contentHash]：在file-loader中和[hash]结果是一致的（在webpack的一些其他地方不一样，后面会讲到）
- [hash:&lt;length&gt;]：截图hash的长度，默认32个字符太长了
- [path]：文件相对于webpack配置文件的路径

例如将图片文件输出到`/img`下，并且用`文件原名 + hash值前8位 + 扩展名`命名打包好的图片文件。

```javascript
// webpack.config.js -&gt; modules -&gt; rules
{
    test: /\.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: &quot;file-loader&quot;,
        // 写法一
        options: {
            name: &quot;img/[name].[hash:8].[ext]&quot;
        }
        // 写法二
        /*
        options: {
            name: &quot;img/[name].[hash:8].[ext]&quot;,
            outputPath: &quot;img&quot;
        }
        */
    }
}
```

## url-loader

`url-loader`和`file-loader`的工作方式是相似的，但是可以将较小的文件，转成base64的URI。 

安装`url-loader`：

```sh
npm install url-loader -D
```

配置`url-loader`：

```javascript
// webpack.config.js -&gt; modules -&gt; rules
{
    test: /\.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: &quot;url-loader&quot;,
        options: {
            limit: 100 * 1024, // 仅将100Kb一下的图片转换为base64
            name: &quot;[name].[hash:8].[ext]&quot;,
            outputPath: &quot;img&quot;
        }
    }
}
```

# asset module type

## 介绍

在webpack5之前，加载非`javascript`或`json`资源我们需要使用一些loader，比如`raw-loader 、url-loader、file-loader`。
在webpack5之后，我们可以直接使用资源模块类型（asset module type），来替代上面的这些loader。

资源模块类型(asset module type)，通过添加 4 种新的模块类型，来替换所有这些 loader： 

- passet/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现
- passet/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现
- passet/source 导出资源的源代码。之前通过使用 raw-loader 实现
- passet 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader，并且配置资源 体积限制实现

## 使用

使用`asset module type`我们可以在`rules`中配置：

```javascript
// webpack.config.js -&gt; module -&gt; rules
{
    test: /\.(png|jpe?g|svg|gif)$/i,
    type: &quot;asset/resource&quot;
}
```

自定义文件输出路径和文件名：

```javascript
// webpack.config.js -&gt; output

// 方法一
output: {
    filename: &quot;js/bundle.js&quot;,
    path:path.resolve(__dirname,&quot;./dist&quot;),
    assetModuleFilename: &quot;img/[name].[hash:6][ext]&quot;
}
```

```javascript
// webpack.config.js -&gt; module -&gt; rules

// 方法二 在rule中添加一个generator属性并设置filename
{
    test: /\.(png|jpe?g|svg|gif)$/i,
    type: &quot;asset/resource&quot;,
    generator: {
        filename: &quot;img/[name].[hash:6][ext]&quot;
    }
    // 实现 limit效果
    /*
    parser: {
        dataUrlCondition: {
            maxSize: 100 * 1024
        }
    }
    */
}
```

处理字体文件，用`webpack5`中的`asset/resource`来替代`file-loader`：

```javascript
// // webpack.config.js -&gt; module -&gt; rules
{
    test: /\.(woff2?|eot|ttf)$/i,
    type: &quot;asset/resource&quot;,
    generator: {
        filename: &quot;font/[name].[hash:6][ext]&quot;
    }
}
```</content:encoded><author>Asuhe</author></item><item><title>Python之Selenium库</title><link>https://asuhe.org/blog/34a3b1fc/</link><guid isPermaLink="true">https://asuhe.org/blog/34a3b1fc/</guid><pubDate>Sun, 05 Jun 2022 17:43:44 GMT</pubDate><content:encoded># Selenium基本使用

## 简介

`Selenium`是一种驱动浏览器的库，它通过调用[Webdriver ](https://w3c.github.io/webdriver/webdriver-spec.html)来驱动电脑中的浏览器从而使我们可以获得一个真实的浏览器环境而不是模拟浏览器环境获取网页数据。它提供了一系列API来帮助我们获取浏览器中的内容。

使用它之前需要下载对应浏览器版本的`Webdriver`：

- [Chrome Webdriver](https://sites.google.com/a/chromium.org/chromedriver/downloads)
- [Edge Webdriver](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/)
- [FireFox Webdriver](https://github.com/mozilla/geckodriver/releases)
- [Safari Webdriver](https://webkit.org/blog/6900/webdriver-support-in-safari-10/)

在合适的目录中放置好`Webdriver`后我们就可以通过`Selenium`调用它，从而获取网页内容。

安装`Selenium`：

```bash
pip install selenium
```

前置工作做好后，我们就可以开始抓取网页内容。

## Chromedriver 使用

```python
# 载入需要的库
from selenium import webdriver

# 开启浏览器视窗(Chrome)
# 方法一：执行前需要启动chromedriver.exe且与该代码文件在相同的工作目录
driver = webdriver.Chrome()
# 方法二：或是直接指定exe文件路径
driver = webdriver.Chrome(&quot;Desktop\chromedriver&quot;)
```

若是想要使用以上其他的浏览器，那只要在浏览器的名称上做一些更改。

```python
# 调用FireFox
driver = webdriver.Firefox()
# 调用Safari
driver = webdriver.Safari()
```

获得`driver`对象后调用`get`函数传入想要访问网页的url即可

```python
# 访问本站
driver.get(&quot;https://asuhe.fun&quot;)
# 关闭浏览器视窗
driver.close()
```

## 使用Selenium控制页面

当我们操作页面时，例如点击页面、提交表单，我们需要预先获取到该事件触发的`DOM元素`。类似于使用javascript去操纵页面，只是这里的语言换成了python。

```html
&lt;input class=&quot;usernameBox&quot; /&gt;
```

```python
# 获得输入框
element = driver.find_element_by_class_name(&quot;usernameBox&quot;)
# 填充内容
element.send_keys(&quot;hello selenium&quot;)
# 清除内容
element.clear()
# 点击元素
element.click()
```

其中`send_keys`函数不仅可以填充内容，还可以模拟所有的键盘操作。当使用非功能性按键如26个字母时，直接使用即可。

```python
# 载入对应库
from selenium.webdriver.common.keys import Keys
# 模拟ctrl + c
element.send_keys(Keys.CONTROL, &quot;c&quot;)
# 模拟ctrl + v
element.send_keys(Keys.CONTROL, &quot;v&quot;)
```

功能按键字符对照表

```python
NULL = &apos;\ue000&apos;
CANCEL = &apos;\ue001&apos; # ^break
HELP = &apos;\ue002&apos;
BACKSPACE = &apos;\ue003&apos;
BACK_SPACE = BACKSPACE
TAB = &apos;\ue004&apos;
CLEAR = &apos;\ue005&apos;
RETURN = &apos;\ue006&apos;
ENTER = &apos;\ue007&apos;
SHIFT = &apos;\ue008&apos;
LEFT_SHIFT = SHIFT
CONTROL = &quot;\ue009&quot;
LEFT_CONTROL = CONTROL
ALT = &quot;\ue00a&quot;
LEFT_ALT = ALT
PAUSE = &quot;\ue00b&quot;
ESCAPE = &quot;\ue00c&quot;
SPACE = &quot;\ue00d&quot;
PAGE_UP = &quot;\ue00e&quot;
PAGE_DOWN = &quot;\ue00f&quot;
END = &quot;\ue010&quot;
HOME = &quot;\ue011&quot;
LEFT = &quot;\ue012&quot;
ARROW_LEFT = LEFT
UP = &quot;\ue013&quot;
ARROW_UP = UP
RIGHT = &quot;\ue014&quot;
ARROW_RIGHT = RIGHT
DOWN = &quot;\ue015&quot;
ARROW_DOWN = DOWN
INSERT = &quot;\ue016&quot;
DELETE = &quot;\ue017&quot;
SEMICOLON = &quot;\ue018&quot;
EQUALS = &quot;\ue019&quot;
NUMPAD0 = &quot;\ue01a&quot; # number pad keys
NUMPAD1 = &quot;\ue01b&quot;
NUMPAD2 = &quot;\ue01c&quot;
NUMPAD3 = &quot;\ue01d&quot;
NUMPAD4 = &quot;\ue01e&quot;
NUMPAD5 = &quot;\ue01f&quot;
NUMPAD6 = &quot;\ue020&quot;
NUMPAD7 = &quot;\ue021&quot;
NUMPAD8 = &quot;\ue022&quot;
NUMPAD9 = &quot;\ue023&quot;
MULTIPLY = &quot;\ue024&quot;
ADD = &quot;\ue025&quot;
SEPARATOR = &quot;\ue026&quot;
SUBTRACT = &quot;\ue027&quot;
DECIMAL = &quot;\ue028&quot;
DIVIDE = &quot;\ue029&quot;
F1 = &quot;\ue031&quot;
F2 = &quot;\ue032&quot;
F3 = &quot;\ue033&quot;
F4 = &quot;\ue034&quot;
F5 = &quot;\ue035&quot;
F6 = &quot;\ue036&quot;
F7 = &quot;\ue037&quot;
F8 = &quot;\ue038&quot;
F9 = &quot;\ue039&quot;
F10 = &quot;\ue03a&quot;
F11 = &quot;\ue03b&quot;
F12 = &quot;\ue03c&quot;
META = &quot;\ue03d&quot;
COMMAND = &quot;\ue03d&quot; 
```

## 页面跳转

```python
# 前进
driver.forward()
# 后退
driver.back()
```</content:encoded><author>Asuhe</author></item><item><title>性能调优(一)</title><link>https://asuhe.org/blog/2a68eba3/</link><guid isPermaLink="true">https://asuhe.org/blog/2a68eba3/</guid><pubDate>Thu, 09 Dec 2021 21:33:50 GMT</pubDate><content:encoded>## 图片懒加载

使用图片懒加载功能，在目标图片还没加载过来时可以先显示loading图片。同时在`&lt;img&gt;`进入可视范围才加载请求目标图片

### 基本使用

* 安装

```sh
yarn add vue-lazyload
```

* 注册组件

```js
import Vue from &quot;vue&quot;;
import VueLazyload from &quot;vue-lazyload&quot;;
// 设置图片为加载过来时显示的loading图片
import loading from &quot;@/assets/images/loading.gif&quot;;
Vue.use(VueLazyload, {
  loading,
});
```

* 使用

```vue
// 注册图片懒加载组件后会多一个 v-lazy 指令，使用该指令替换 src就可以实现图片懒加载
&lt;img v-lazy=&quot;imgUrl&quot;&gt;
```



## 路由懒加载

路由懒加载使用`import`函数，可以让路由组件单独打包。当用户访问该路由时，才会从服务器请求路由资源，也就是动态引入。

### 特点

打包会打包成一个单独的文件
访问哪一个再去加载哪一个

* 当打包构建应用时，JS包会变得非常大，影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块，然后当路由被访问的时候才加载对应组件，这样就更加高效了
* 本质就是Vue 的异步组件在路由组件上的应用	

* 需要使用动态import语法, 也就是import()函数
* import(&apos;模块路径&apos;): webpack会对被引入的模块单独打包一个小文件
* 当第一次访问某个路径对应的组件时，此时才会调用import函数去加载对应的js打包文件

### 基本使用

```js
// 路由懒加载 
// 当路由被访问时，才会调用路由函数
// import Search from &quot;../pages/Search&quot;;
const Search = () =&gt; import(&apos;../pages/Search&apos;);
```</content:encoded><author>Asuhe</author></item><item><title>Vue组件间通信总结</title><link>https://asuhe.org/blog/a5af1acd/</link><guid isPermaLink="true">https://asuhe.org/blog/a5af1acd/</guid><pubDate>Fri, 10 Dec 2021 21:17:29 GMT</pubDate><content:encoded>## props通信

`props`通信是最基础最简单的一种通信方式，使用`props`进行组件间通信既可以用来父向子组件传递数据，也可以把子组件的数据传递给父组件。**实际上这种通信方式没有任何的限制，它也能够实现爷孙、兄弟通信，只需要层层传递参数即可**。但是若关系层级超过2层以上，就不推荐使用`props`通信了，面对这种场景我们可以使用全局事件总线更加方便地传递数据。

`props`通信通常都是由父组件给子组件传递数据，父组件传递的参数数据大致可以分为函数和非函数两种。当父组件给子组件传递非函数数据即纯数据时，子组件只能被动接收。而当父组件给子组件传递一个函数时，这时我们可以在子组件中调用这个函数，利用给这个定义在父组件里的函数传递形参，从而达到子组件给父组件传参的目的。

子组件在接收父组件传过来的参数时，也有三种方法接收。每种方法都有不同的特点，可以根据不同需求来选择使用

```vue
// 父组件
&lt;template&gt;
&lt;div&gt;
    &lt;Son :money=&quot;money&quot; :getmoney=&quot;getMoney&quot; /&gt; //父给子传参
&lt;/div&gt;
&lt;/template&gt;
&lt;javascript&gt;
export default {
    data(){
    	money:1000
    },
    methods:{
    	getMoney(money){
    		this.money +=money;
    	}
    }
}
&lt;/javascript&gt;
---------
// 子组件 Son
&lt;template&gt;
	&lt;ul&gt;
        &lt;li&gt;{{money}}&lt;/li&gt;
        &lt;li&gt;
            &lt;button @click=&apos;getmoney(100)&apos;&gt;&lt;/button&gt;  //子给父传参
    	&lt;/li&gt;
    &lt;/ul&gt;
&lt;/template&gt;
&lt;javascript&gt;
export default {
    //第一种props接收参数的形式，用数组形式接收数据
    props:[&apos;money&apos;],
    props:{
    	getmoney:Fuction //利用getmoney这个函数参数，更新父组件的money
    }
    /* 第二种props接收参数的形式，限定props参数类型
    props:{
    	money:Number
    }
    第三种props接收参数的形式，限定参数类型同时指定默认值
    props:{
    	money:{
    		type:Number,
    		default:1
		}
	}
    */
}
&lt;/javascript&gt;
```

### 路由传参里的props

在路由条目对象里配置`props`本质上就是将`$route.params`映射到`props`中。**使用对象传递`params`参数时，路由条目的对象必须使用`name`属性。**

```js
{
	name: &quot;search&quot;, //使用对象传params参数必须用name
	path: &quot;/search/:keyword?&quot;, // ?表示keyword参数可传可不传
	component: Search,
	props: true // 默认为false,true时会将所有的params参数映射到props
	// props:(route)=&gt;({keyword3:route.params.keyword,keyword4:route.query.keyword2})
},
```



## 自定义事件

在了解自定义事件之前我们需要明确什么是自定义事件。所谓自定义事件其实就是我们自己定义的事件，它和原生DOM事件有很大不同。

原生DOM事件的特点是：

* 系统定义的，数量是固定的。就那些事件，事件名是固定的
* 由系统（即浏览器）管理、触发
* 回调函数是我们定义的，系统调用的
* 回调函数的第一个参数是事件对象，是系统自动传入的

Vue自定义事件的特点是：

* 自己定义的，数量是无限个，自己想定义多少定义多少，事件名随意取
* 由我们自己管理、触发
* 回调函数是我们定义的，系统调用的
* 回调函数的参数是我们手动传入的，没传就没有

除了以上特点，判断一个事件是否是自定义事件的绝对准则是：**所有绑定在组件标签上的事件都是自定义事件**。若我们想将组件标签上的自定义事件转换为同名的原生DOM事件则需要在自定义事件后加上`.native`，例如`@click.native`。这样就把一个组件标签上的自定义事件`click`转换为同名的原生DOM事件`click`。

在我们使用vue的过程中，绑定原生DOM事件监听会有两种情况：一是在html标签上绑定事件监听，二是在组件标签上绑定事件监听。当我们在组件标签上绑定原生DOM事件时，实际上是将事件绑定在了组件的根标签上，利用事件委托来监听所有子组件发生的事件。

```vue
// 父组件
&lt;template&gt;
&lt;div&gt;
    &lt;Son @click.native=&quot;alert(&apos;呵呵呵&apos;)&quot;&gt;
&lt;/div&gt;
&lt;/template&gt;
--------
// 子组件 Son
&lt;template&gt;
&lt;div&gt; // 父组件的原生DOM事件绑定在div上
	&lt;h2&gt;哈哈哈&lt;/h2&gt;
    &lt;h3&gt;嘿嘿嘿&lt;/h3&gt;
&lt;/div&gt;
&lt;/template&gt;
```

同样地我们也可以将自定义事件绑定在html标签上，但是这样并没有意义。因为自定义事件要在标签内部使用`$emit`触发，而html标签无法进入内部触发`$emit`。

利用自定义事件的特性，我们可以实现**子向父通信传递参数**。

```vue
// 父组件
&lt;template&gt;
&lt;div&gt;
    &lt;Son @click=&quot;test1&quot; /&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;javascript&gt;
export default {
	data(){
    	
    },
    methods:{
    	test1(event){
    		alert(event);
    	}
    }
}
&lt;/javascript&gt;
--------
// 子组件
&lt;template&gt;
	&lt;div&gt;
        &lt;button @click=&quot;$emit(&apos;click&apos;,&apos;嘿嘿&apos;)&quot;&gt;嘿嘿&lt;/button&gt;
        &lt;button @click=&quot;$emit(&apos;click&apos;,&apos;哈哈&apos;)&quot;&gt;哈哈&lt;/button&gt;
    &lt;/div&gt;
&lt;/template&gt;
```

## PubSubJS

pubsub.js方式通信是让需要通信的两个组件分别引入该插件，然后再使用。

```js
import pubSub from &apos;pubsub-js&apos;;
// 发布消息的组件中传递数据，就给数据的组件
pubSub.publish(&apos;消息名&apos;,数据对象);
//订阅消息的组件，即接收数据的组件
pubSub.subscribe(&apos;消息名&apos;,(接收数据的参数)=&gt;{});
```



## 全局事件总线

全局事件总线本质上就是利用作用域链和自定义事件这两个机制来实现组件间通信。**它可以用于任意组件之间进行数据通信。**使用全局事件总线有三个步骤，分别是：

1. 定义总线
2. 在需要接收参数的组件里，让总线上绑定自定义事件
3. 在传出数据的组件里，触发总线上的自定义事件

随便用一个对象去定义总线是无效的，成为事件总线的对象要满足两个条件：

* 该对象能够被所有的组件实例找到
* 该对象要能够使用`$emit`和`$on`方法

```vue
// main.js 
Vue.prototype.$bus = new Vue() //定义事件总线

// Father.vue
this.$bus.$on(&apos;addUser&apos;,(data)=&gt;{console.log(data)};) // 给事件总线绑定事件

// Son.vue
this.$bus.$emit(&apos;addUser&apos;,data) // 触发事件总线
```

全局事件总线的原理

![全局事件总线原理](https://i0.hdslb.com/bfs/album/b512b4118e872b6f997c57049ca9cd9a1fd5c60a.png)



## 作用域插槽

根据作用域插槽的机制，我们可以实现子组件向父组件中传参。因为在作用域插槽中，数据的结构或样式是根据父组件决定的，而此时我们需要将在子组件中遍历过的数据重新传递给父组件，将改变的数据部分用`slot`包裹。在`slot`标签中将数据传递给父组件。父组件再根据传递过来的数据判断需要返回给子组件的样式结构，在`template`标签中使用`slot-scope=子组件属性对象`获取子组件数据。

```vue
// 父组件
&lt;template&gt;
&lt;div&gt;
    &lt;templat slot-scope=&quot;scope&quot;&gt; // scope对象 = {value:100,index:0}
        
    &lt;/templat&gt;
&lt;/div&gt;
&lt;/template&gt;

//子组件
&lt;template&gt;
	&lt;slot :value=&quot;100&quot; :index=&quot;0&quot;&gt;&lt;/slot&gt; // slot的所有属性都会自动传递给父组件
&lt;/template&gt;
```

## Vuex

利用`Vuex`这个插件我们也可以实现组件间通信。当`Vuex`注册为全局组件时，在任一组件里我们都可以用`this.$store`这个变量来从`Vuex`中取得数据并且能够操作数据。`Vuex`相当于充当了一个传话筒的角色，让我们能够在各个组件里自由通信。

对于以上六种组件间通信方式我以前的文章也有总结，{% post_link &apos;2021-11-11-VUE基础(三)&apos; &apos;戳这里查看更多&apos;%}

## v-model

通常`v-model`是使用在html中的表单标签里用于收集数据的。在html标签上的`v-model`本质上是用`:value`单向绑定一个数据，然后再用`input`事件触发改变绑定在`:value`上的值来实现的。

```vue
&lt;input type=&apos;text&apos; v-model=&apos;msg&apos; /&gt;
// 等价于
&lt;input type=&apos;text&apos; :value=&apos;msg&apos; @input=&apos;msg = $event.target.value&apos; /&gt;
```

当我们在组件标签上使用`v-model`时，本质上也是先用`:value`单向绑定一个值，然后再用自定义的`input`事件监听（将子组件分发数据保存父组件的属性上）。

```vue
&lt;myInput v-model=&apos;msg&apos;&gt;&lt;/myInput&gt;
// 等价于
&lt;myInput :value=&apos;msg&apos; @input=&apos;msg = $event.target.value&apos;&gt;&lt;/myInput&gt;
--------------
// myInput内部
&lt;template&gt;
	&lt;div&gt;
        &lt;h2&gt;input包装&lt;/h2&gt;
        &lt;input type=&apos;text&apos; @input=&apos;$emit(&quot;input&quot;,$event.target.value)&apos; /&gt;
    &lt;/div&gt;
&lt;/template&gt;
```

若组件标签里没有主动设置去使用`$emit`触发自定义事件则`v-model`并不会生效。

组件标签使用`v-model`本质上还是自定义事件和props的组合，它实现了父子组件双向数据同步的问题

## Sync属性

`sync`属性修饰符也是用来实现父子组件双向数据同步的问题和`v-model`实现的效果几乎一样。`v-model`一般用于带表单项的组件而`sync`一般用于不带表单项的组件。使用时它们并没有严格的界限，只是我们约定成俗的地会让带有表单项的组件使用`v-model`，而普通组件则使用`sync`属性修饰符。

```vue
// 父组件
&lt;h2&gt;不使用sync修改符&lt;/h2&gt;
&lt;Child :money=&quot;total&quot; @update:money=&quot;total=$event&quot;/&gt; // 事件命名格式一定要为update:传过去的数据名称
// 子组件 Child
&lt;button @click=&apos;$emit(&quot;update:money&quot;,100)&apos;&gt;&lt;/button&gt;

------
// 父组件
&lt;h2&gt;使用sync修改符&lt;/h2&gt;
&lt;Child :money.sync=&quot;total&quot;/&gt; // 其实就是不使用sync时的语法糖
```



## `$attrs`与`$lintener`

当我们需要封装一些组件时，我们可以利用`$attrs`与`$lintener`达到组件复用最大化。其实`$attrs`与`$lintener`就是一个对象，`$attrs`可以接收父组件传递过来的全部属性，而`$lintener`可以接收父组件传递过来的全部事件。

```vue
// 父组件
&lt;HintButton title=&quot;双击添加用户&quot; type=&quot;primary&quot; icon=&quot;el-icon-plus&quot; @dblclick.native=&quot;add2&quot;/&gt;

// 子组件 HintButton
&lt;el-button v-bind=&quot;$attrs&quot; v-on=&quot;$listeners&quot;&gt;&lt;/el-button&gt; // 必须用全写v-bind和v-on
-----
// $attrs和$linstener的结构
$attrs = {
	title:&quot;双击添加用户&quot;,
	type=&quot;primary&quot;,
	icon=&quot;el-icon-plus&quot;
}

$linstener = {
	@dblclick.native=&quot;add2&quot;
}
```

当我们需要单独去除某个父组件传递过来的属性值时，可以用`props`去单独接收。用`props`接收了的属性不再会出现在`$attrs`对象中



## `$Parent`与`$Children`、`$refs`

`$children`：所有子组件对象的**数组**，因为它返回的是一个数组所以可以使用数组方法对其遍历，但不能用数组下标去访问某个子对象

`$parent`：代表父组件对象

`$refs`：ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用，引用指向的就是 DOM 元素; 如果用在子组件上，引用就指向组件实例 当 v-for 用于元素或组件的时候，引用信息将是包含 DOM 节点或组件实实例的数组

* 给子组件标签使用ref标识` &lt;Son ref=&quot;son&quot; /&gt;`  `$refs`可以直接操作子组件内部的数据及方法通过`this.$refs.son`可以拿到组件对象本身 如果需要修改data数据 可以直接修改`this.$refs.son.msg=XXX`

* 给html标签使用ref  `&lt;p ref=&apos;pp&apos;&gt;&lt;/p&gt;`  拿到的是html标签本身的dom元素`this.$refs.pp`

一般慎用以下方法:  

**找子组件时`$children` 是将子组件对象放入数组中不能通过索引操作因为位置不固定，`$children`访问子组件顺序是随机的，所以无法使用下标索引操作**

**找父组件时`$parent`存在组件共用，此时可能不是一个父组件 会存在多个父组件**

父组件当中可以通过`$children`找到所有的子组件去操作子组件的数据（当然可以找孙子组件）

子组件当中可以通过`$parent`找到父组件（当然可以继续找爷爷组件）操作父组件的数据

## Mixin混入技术

html、js、css相同时我们会封装组件。单个组件里js代码重复我们会封装函数。当不同的组件js代码重复  封装混合时，我们就可以使用`minx`混入技术，重用js代码。新建一个myminxi.js文件 在js文件中暴露一个对象 对象内部可以有data methods computed...  会将js文件中暴露出的数据 方法等混入到组件内部。

### 使用

```js
//在组件内部引入 import myminxi from &apos;./myminxi.js&apos;
//使用mixins:[mymixin]  例如:
import {mixin} from &apos;./mymixin&apos;
export default {
    name: &apos;Daughter&apos;,
    mixins:[mixin],
    data(){
        money:1000
    }
},
-------
// myminx.js
export const mixin = {
  methods: {
    borrowMoney (count) {
      this.money -= count
    },
    gaveMoney (count) {
      this.money -= count
      // 给父组件增加count
      this.$parent.money += count
    }
  }
}
```

- 可以提高代码复用，优化性能
- 变量来源不明确，不利于阅读
- 多mixin可能会造成命名冲突
- mixin的组件可能会出现多对多的关系，复杂度较高</content:encoded><author>Asuhe</author></item><item><title>VUE基础(五)</title><link>https://asuhe.org/blog/481cd588/</link><guid isPermaLink="true">https://asuhe.org/blog/481cd588/</guid><pubDate>Fri, 04 Mar 2022 14:16:34 GMT</pubDate><content:encoded># Vue高级特性

## 自定义v-model

当我们需要在自定义的组件上使用`v-model`属性时，就需要自己去实现父子组件里的`v-model`通信了。用`props`传值，子组件将需要改变的值接收，然后使用`model`添加自定义事件将其绑定在标签上。

```vue
// 父组件
&lt;template&gt;
	&lt;p&gt;
        {{name}}
    &lt;/p&gt;
	&lt;Son v-model=&quot;name&quot; /&gt;
&lt;/template&gt;
&lt;scirpt&gt;
import Son from &apos;./Son&apos;
export default {
    components:{
    	Son
    },
    data(){
        return {
        	name:&apos;asuhe&apos;
        }
    }
}
&lt;/scirpt&gt;
```

```vue
// 子组件
&lt;template&gt;
	&lt;input type=&quot;text&quot; :value=&quot;text&quot; @input=&quot;$emit(&apos;change&apos;,$event.target.value)&quot; &gt;
&lt;/template&gt;
&lt;scirpt&gt;
export default {
    model:{
    	prop:&apos;name&apos;, // 对应 props的name
    	event:&apos;change&apos;
    }
    props:{
    	name:{
            type:String,
            default:1
        }
    }
}
&lt;/scirpt&gt;
```



## $nextTick

因为vue是异步渲染的。在其运作过程中，data数据改变后不会立马渲染DOM，而是用`Document.createDocumentFragment`创建文档碎片将多次data数据修改后的所有DOM操作整合成一次再去插入DOM中进行渲染，这也是vue性能优秀的原因之一。

在DOM渲染之后，vue提供了一个`$nextTick`函数，它会在DOM渲染之后触发，我们可以利用它来获取最新的DOM节点。

```vue
&lt;template&gt;
	&lt;p&gt;
        {{name}}
    &lt;/p&gt;
&lt;/template&gt;
&lt;script&gt;
	export default {
        data(){
            return {
                name:&apos;zhangsan&apos;
            }
        },
        mounted(){
			this.$nextTick(()=&gt;{
                console.log(&apos;调用了$nextTick&apos;);
            })
        }
    }
&lt;/script&gt;
```



## slot

当一个组件会被多次使用，且里面大部分内容不变仅有非常少部分的结构改变时，可以使用slot插槽，将其理解为占位符。该种通信方式适用于父组件给子组件传递数据，但它与其它通信不同的是，它可以传递结构给子组件，子组件中的`slot`标签本质上就是一个占位符。若父组件给其传递`template`则使用父组件传递过来的`template`，否则使用默认定义的`template`。插槽又分为默认插槽、具名插槽和作用域插槽。

**默认插槽约定成俗只能有一个，具名插槽就是在默认插槽的基础上加上`name`属性唯一标识这个插槽**，这样父组件在传递数据的时候可以根据名字精准传递到指定的插槽中。

作用域插槽中子组件的slot可以通过 属性传递值给父组件，然后父组件可以根据不同需求改变这个slot内部的显示结构，把子组件的值，传给父组件固定的区域进行操作。父组件的数据是给子组件展示的。子组件展示过程当中，数据的结构由父组件决定的。

### 默认插槽

```js
//子组件放置插槽
&lt;slot&gt;&lt;/slot&gt;
&lt;slot name=&quot;asu&quot;&gt;&lt;/slot&gt;


//父组件传递数据
&lt;template&gt;
    &lt;button&gt;点击&lt;button&gt;
&lt;/template&gt;
&lt;template slot=&quot;asu&quot;&gt;
    &lt;a href=&quot;http://asuhe.fun&quot;&gt;&lt;/a&gt;
&lt;/template&gt;
```

作用域插槽

```js
//父组件
&lt;template slot-scope=&quot;{todo,index}&quot;&gt;
	&lt;span v-if=&quot;todo.isOver&quot; style=&quot;color:hotpink&quot;&gt;√ {{todo.content}}&lt;/span&gt;  //父组件控制子组件的样式
&lt;/template&gt;

//子组件
&lt;slot :todo=&quot;todo&quot; :index=&quot;index&quot;&gt;
    {{todo.content}}
&lt;/slot&gt;
```

作用域插槽接收子组件数据时的指令：

- `slot`属性弃用，具名插槽通过指令参数`v-slot:插槽名` 的形式传入，可以简化为 `#插槽名`

- `slot-scope`属性弃用，作用域插槽通过`v-slot:xxx=&quot;slotProps&quot;`的slotProps来获取子组件传出的属性

- `v-slot`属性只能在`&lt;template&gt;`上使用，但在【**只有默认插槽时**】可以在组件标签上使用

#### 注意事项

1. 默认插槽名为`default`，可以省略`default`直接写`v-slot`，
   缩写为#时不能不写参数，写成`#default`（这点所有指令都一样，v-bind、v-on）
2. 多个插槽混用时，`v-slot`不能省略`default`
3. 同样可以通过解构获取`v-slot={user}`,还可以重命名`v-slot=&quot;{user: newName}&quot;`和定义默认值`v-slot=&quot;{user = &apos;默认值&apos;}&quot;`
4. 插槽名可以是动态变化的 `v-slot:[slotName]`

## 动态组件

所谓动态组件就是在一个页面中，各个组件的组成是不一样的。但是在同一个路由下，里面的二级路由组合可能经常变动。有些组件我们选择展示，有些组件我们选择不展示。这种时候就可以使用动态组件。

```vue
// 动态组件 Son
&lt;template&gt;
	&lt;p&gt;asuhe&lt;/p&gt;
&lt;/template&gt;
```

```vue
// 父组件
&lt;template&gt;
	// 用 Component 标签表示动态组件 is为动态组件的名称
	&lt;Component :is=&quot;NextTickName&quot;&gt;&lt;/Component&gt;
	/* 不能直接让 is 为具体的组件名称，必须为变量
	 错误用法 报错 &lt;Component is=&quot;Son&quot;&gt;&lt;/Component&gt; */
&lt;/template&gt;
&lt;script&gt;
import Son from &apos;./Son&apos;
export default {
    components:{
        Son
    },
    data(){
        return {
            NextTickName:&apos;Son&apos;
        }
    }
}
&lt;/script&gt;

```



## 异步组件

在某些场景下不需要加载全部的组件，我们只需要加载部分必要组件，当某些功能被使用到了我们就可以再去加载那些组件。这时我们就可以利用异步加载组件的技术，使用`import()`函数来实现这个功能，可以对我们的页面有非常大的性能提升。路由懒加载就是利用这个原理

```vue
// 异步的组件
&lt;template&gt;
	&lt;p&gt;AsyncComponent&lt;/p&gt;
&lt;/template&gt;
```

```vue
// 父组件
&lt;template&gt;
	&lt;Son v-if=&quot;show&quot;&gt;&lt;/Son&gt;
	&lt;button @click=&quot;load&quot;&gt;点击加载子组件&lt;/button&gt;
&lt;/template&gt;
&lt;script&gt;
// 同步引入组件 import Son from &apos;./Son&apos;
export default {
    components:{
       // 异步加载组件  
        Son:()=&gt; import(&apos;./Son&apos;)
    },
    data(){
      return {
          show:false
      }  
    },
    methods:{
        load(){
            this.show = true;
        }
    }
}
&lt;/script&gt;
```



## keep-alive

正常情况下当我们切换组件时，组件对象会被销毁。有时我们不希望如此，使用`keep-alive`可以将组件缓存下来。当我们在组件里来回切换的时候组件实例会被保存，不会被销毁所以也不用重复渲染，能够极大提升页面性能。

```vue
// 保活组件
&lt;template&gt;
	&lt;p&gt;AsyncComponent&lt;/p&gt;
&lt;/template&gt;
```

```vue
&lt;template&gt;
	&lt;p&gt;&lt;/p&gt;
	&lt;keep-alive&gt;
    	
    &lt;/keep-alive&gt;
&lt;/template&gt;
&lt;script&gt;
import Son from &apos;./Son&apos;
export default {
    components:{
        Son
    },
    data(){
        return {
            
        }
    }
}
&lt;/script&gt;
```



## mixin

html、js、css相同时我们会封装组件。单个组件里js代码重复我们会封装函数。当不同的组件js代码重复  封装混合时，我们就可以使用`minx`混入技术，重用js代码。新建一个myminxi.js文件 在js文件中暴露一个对象 对象内部可以有data methods computed...  会将js文件中暴露出的数据 方法等混入到组件内部。

### 使用

```js
//在组件内部引入 import myminxi from &apos;./myminxi.js&apos;
//使用mixins:[mymixin]  例如:
import {mixin} from &apos;./mymixin&apos;
export default {
    name: &apos;Daughter&apos;,
    mixins:[mixin],
    data(){
        money:1000
    }
},
-------
// myminx.js
export const mixin = {
  methods: {
    borrowMoney (count) {
      this.money -= count
    },
    gaveMoney (count) {
      this.money -= count
      // 给父组件增加count
      this.$parent.money += count
    }
  }
}
```

### mixin技术的特点

- 可以提高代码复用，优化性能
- 变量来源不明确，不利于阅读
- 多mixin可能会造成命名冲突
- mixin的组件可能会出现多对多的关系，复杂度较高</content:encoded><author>Asuhe</author></item><item><title>Python之urllib库</title><link>https://asuhe.org/blog/9552fdc3/</link><guid isPermaLink="true">https://asuhe.org/blog/9552fdc3/</guid><pubDate>Sun, 05 Jun 2022 15:33:03 GMT</pubDate><content:encoded># urllib基本使用

## 简介

`urllib`是python内置的一个网络库，我们可以直接导入该库，然后使用它去模拟浏览器向服务器发送请求以获取数据。

```python
# 导入请求对象
import urllib.request
baseUrl = &apos;http://www.baidu.com&apos;
# 通过baseUrl发送请求
respone = urllib.request.urlopen(url = baseUrl)
# 获取回的respone为一个对象，调用read方法逐字节读取响应数据
content = respone.read()
# respone.readline() 仅读取一行内容
# respone.readlines() 一行一行读取所有内容，返回一个字节流形式的数据的list
# content是字节流的形式存储的数据，所以要转换为utf-8字符串
result = content.decode(&apos;utf-8&apos;)
# 输出最终的html文档
print(result)
```

## 构造http请求头

有时候网站会要求识别请求的`UA`来反爬，或者需要爬取登录后才能查看的数据时要携带`cookie`。我们可以使用`urllib`库里带的函数去构造出`http`请求头来绕过。

![image-20220605155834239](https://s2.loli.net/2022/06/05/8NesfuQbFAcpWdC.png)

```python
import urllib.request
baseUrl = &apos;https://asuhe.fun&apos;
# 构造http请求头,携带UA
headers = {
    &apos;User-Agent&apos;: &apos;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36&apos;
}
# 将构造好的请求头封装进Request对象
request = urllib.request.Request(url = baseUrl,headers = headers)
# 发送请求
respone = urllib.request.urlopen(request)
```

### handler构造请求头

当我们需要使用`动态cookie`或者`ip代理`等高级功能时，上面构造的请求头已经无法满足。`handler`则可以实现这个功能。

```python
# 在上面代码的基础上，使用handler对象即可
# 1.创建handler对象
handler = urllib.request.HTTPHandler()
# 2.构建opener对象
opener = urllib.request.build_opener(handler)
# 3.调用opener对象的open方法请求，传入request对象
respone = opener.open(request)
```

### 使用代理

使用代理ip访问网站可以提高访问速度，突破网站访问的限制。

```python
import urllib.request
baseUrl = &apos;http://www.baidu.com&apos;
headers = {
    &apos;User-Agent&apos;: &apos;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36&apos;
}
# 1.创建request对象
request = urllib.request.Request(url=baseUrl,headers=headers)
# 2.创建ProxyHandler对象
proxies = {
    # 设置代理ip和端口
    &apos;http&apos;:&apos;110.110.110.110:996&apos;
}
proxyHandler = urllib.requers.ProxyHandler(proxies = proxies)
# 3.创建opener对象
opener = urllib.request.build_opener(proxyHandler)
# 4.使用open方法访问
respone = opener.open(request)
```

使用代理池

```python
proxiesPool = [
    {&apos;http&apos;:&apos;110.110.110.110:996&apos;},
    {&apos;http&apos;:&apos;111.110.110.110:996&apos;},
    {&apos;http&apos;:&apos;112.110.110.110:996&apos;}
]
import random
proxies = random.choice(proxiesPool)
```

## get和post请求的编码处理

无论是`get`还是`post`，在发送前如果携带参数中有中文或者非`utf-8`字符，则都需要先编码，然后才能发送出去。

```python
import urllib.request
import urllib.parse
# 处理单个参数
baseUrl1 = &apos;http://www.baidu.com/s?wd=&apos;
query = &apos;明日&apos;
# 编码单个参数
encoded = urllib.parse.quote(query) 
print(encoded) # %E6%98%8E%E6%97%A5
respone1 = urllib.request.urlopen(url = baseUrl1 + encoded)

# 处理多个参数
baseUrl2 = &apos;http://www.baidu.com/s?&apos;
querys = {
    &apos;wd&apos;:&apos;明日&apos;,
    &apos;sex&apos;:&apos;男&apos;
}
data = urllib.parse.urlencode(querys)
respone = urllib.request.urlopen(url = baseUrl2 + data)
```

### 发送post请求

```python
import urllib.request
import urllib.parse
baseUrl = &apos;https://fanyi.baidu.com/v2transapi?from=en&amp;to=zh&apos;
data = {
    &apos;from&apos;: &apos;en&apos;
    &apos;to&apos;: &apos;zh&apos;
    &apos;query&apos;: &apos;spider&apos;
    &apos;transtype&apos;: &apos;realtime&apos;
    &apos;simple_means_flag&apos;: &apos;3&apos;
    &apos;sign&apos;: &apos;63766.268839&apos;
    &apos;token&apos;: &apos;122223c8769458b6eb3613362ad8693e&apos;
    &apos;domain&apos;: &apos;common&apos;
}
params = urllib.parse.urlencode(data)
# post请求的参数必须编码为字节流的形式
params = params.encode(&apos;utf-8&apos;)
# urllib.request.Request的data参数即为post请求携带的参数
request = urllib.request.Request(url = baseUrl,data = params)
respone = urllib.request.urlopen(request)
```

### json处理

有时候我们请求网页返回来的是`json`格式的数据，这时我们要使用`json`模块去解析数据

```python
import json
jsonData = json.loads(data) # data为响应的json数据
```

## 异常处理

若通过`urllib`发送请求产生异常，通常为两种：一种是`URLError`，另一种是`HTTPError`。`URLError`当请求的主机地址错误时会发生，`HTTPError`例如当无法连接到主机时，请求携带的参数错误时，都会产生该错误。`HTTPError`是`URLError`的子类。

```python
import urllib.error
try:
    # code
except urllib.error.HTTPError:
    # something
except urllib.error.URLError:
    # something
```</content:encoded><author>Asuhe</author></item><item><title>React基础(一)</title><link>https://asuhe.org/blog/807d54a3/</link><guid isPermaLink="true">https://asuhe.org/blog/807d54a3/</guid><pubDate>Tue, 14 Jun 2022 09:49:23 GMT</pubDate><content:encoded># 组件声明

`React`中声明组件有两种方式，一是函数式组件，就是把一个函数作为组件的构造器；二是`class`组件，即用`ES6`的`class`语法声明一个类作为组件的构造器。在**16.8版本**以前函数式组件不能使用组件的`state`，`refs`属性等诸多特性，这使得函数式组件只能用来定义一些简单组件，对于复杂一些的组件只能使用`class`组件。而此版本以后`React`加入了`Hook`，使得函数式组件也能够使用组件的其它特性。[官方文档](https://reactjs.org/docs/hooks-intro.html)

## 函数式组件

函数式组件中我们只能用`props`属性，定义一个函数式组件只需要创建一个开头为大写字母的函数，然后再将函数最后`return`回一个`jsx`标签即可。代码如下：

```jsx
function Person(props) {
    return (
    	&lt;div&gt;
            &lt;h2&gt;{props.name}&lt;/h2&gt;
        &lt;/div&gt;
    )
}
ReactDOM.render(&lt;Person name=&quot;Asuhe&quot; /&gt;,document.querySelector(&apos;#root&apos;))
```

## class组件

`class`组件要比函数式组件强大的多，它可以完全使用组件三大属性：`state`、`props`、`refs`。它还有生命周期函数可供使用，在生命周期函数中我们可以进行许多操作。但创建一个`class`组件需要从`React对象`中继承一个类`React.Component`。代码如下：

```jsx
class MyComponent extends React.Component {
    // render函数必须要有
    render(){
        return (
            &lt;div&gt;
                &lt;h2&gt;Asuhe&lt;/h2&gt;
            &lt;/div&gt;
        )
    }
}
ReactDOM.render(&lt;MyComponent /&gt;,document.querySelector(&apos;#root&apos;))
```

当我们需要在`class`组件上挂载事件处理函数时有两种方式，一是在`constructor`中重写该事件处理函数，二是利用箭头函数和class语法声明函数。

```jsx
class Weather extends React.Component {
    /* 第一种方法 在jsx中调用的this调用weather和设置state
    constructor(props) {
        super(props)
        this.state = { isHot: true, breeze: &apos;微风&apos; }
        // 关键步骤：将原型上的changeWeather挂载载到组件实例对象的changeWeather里并更改this
        this.changeWeather = this.changeWeather.bind(this)
    }
    changeWeather() {
        let {isHot} = this.state
        this.setState({
            isHot: !isHot
        })
    }
    // 未重写changeWeather前，onClick绑定的this.changeWeather相当于
    // let a = new Weather()
    // let x = a.changeWeather
    // x()
    */
    // 第二种方法 不在原型上挂载changeWeather
    state = { isHot: true, breeze: &apos;微风&apos; }
    changeWeather = () =&gt; {
        let { isHot } = this.state
        this.setState({
            isHot: !isHot
        })
    }
    render() {
        return (
            &lt;h2 onClick={this.changeWeather}&gt;
                今天天气很{this.state.isHot ? &apos;炎热&apos; : &apos;凉爽&apos;},{this.state.breeze}
            &lt;/h2&gt;
        )
    }
}
ReactDOM.render(&lt;Weather /&gt;, el)
```



# 组件的三大属性

组件实例中有很多属性，但比较常用的就三个分别是：state、props、refs。

![image-20220614101741901](C:/Users/12071/AppData/Roaming/Typora/typora-user-images/image-20220614101741901.png)

## state

`state`属性是用来存储该组件实例的状态的。当我们使用`this.setState`函数去修改`state`时，页面会因为组件状态改变而同步改变。以上面的`MyComponent`组件为例，我们可以给组件定义一个初始状态：

```jsx
class MyComponent extends React.Component {
    /* 第一种方式在构造器里初始化状态
    constructor(props){
    	// 使用了构造器就一定要调用super 否则React报错
        super(props)
        this.state = {name:&apos;Asuhe&apos;}
        // 若不给super传props，则在constructor中使用this.props可能会出错
    }
    */
    // 第二种方式
    state = {name:&apos;Asuhe&apos;}
    // render函数必须要有
    render(){
        return (
            &lt;div&gt;
                &lt;h2&gt;{this.state.name}&lt;/h2&gt;
            &lt;/div&gt;
        )
    }
}
```



## props

`props`属性是用来接收外部传给组件的数据的，如果我们直接在标签上写数据，`props`会自动接收该数据。

```jsx
ReactDOM.render(&lt;MyComponent age={18} /&gt;,document.querySelector(&apos;#root&apos;))
```

![image-20220614103444298](https://img.outsider404.com/asuhe-blog-img/2024/04/44840d8849af3e7b793e947144f0f5cb.png)

### 限制props的数据类型

有时候我们希望限制传入数据的类型，此时我们需要额外加载一个`prop-types.js`包，里面有`PropTyps`对象以供我们使用。示例代码如下：

```jsx
// 需求： 
// 1.name属性必须为string，age属性必须为number，sex属性必须为string
// 2.sex属性必须传入
// 3.sex若未传入则默认值为male
class Person extends React.Component {
    static propTypes = {
        name: PropTypes.string,
        age: PropTypes.number,
        // sex为必须
        sex: PropTypes.string.isRequired
    }
    // 设置默认props值
    static defaultProps = {
        sex: &apos;male&apos;
    }
    render() {
        return (
            &lt;ul&gt;
                &lt;li&gt;name:{this.props.name}&lt;/li&gt;
                &lt;li&gt;age:{this.props.age}&lt;/li&gt;
                &lt;li&gt;sex:{this.props.sex}&lt;/li&gt;
            &lt;/ul&gt;
        )
    }
}
let data = {
    name: &apos;asuhe&apos;,
    age: 22,
    // sex: &apos;male&apos;
}
ReactDOM.render(&lt;Person {...data} /&gt;, el)
```



## refs

当我们需要获取标签or组件实例时，可以使用`ref`标记。然后就可以在函数中利用`this.refs`找到该组件or标签。`ref`有三种形式分别是：字符串类型、回调函数类型和`refs`对象类型。

### 字符串类型的ref

字符串形式的ref 已经不推荐使用， ref会被自动收录进组件的refs里，如同props。

```jsx
class Person extends React.Component {
    showData = (event) =&gt; {
        event.preventDefault()
        let { input2: { value } } = this.refs
        console.log(value)
    }
    render() {
        return (
            &lt;form&gt;
                &lt;input ref=&quot;input1&quot; type=&quot;text&quot; placehoder=&quot;username&quot; /&gt;
                &lt;button onClick={this.showData}&gt;提交&lt;/button&gt;
                &lt;input ref=&quot;input2&quot; type=&quot;password&quot; placehoder=&quot;password&quot; /&gt;
            &lt;/form&gt;
        )
    }
}
ReactDOM.render(&lt;Person /&gt;, el)
```

![image-20220614104753532](https://img.outsider404.com/asuhe-blog-img/2024/04/b160dc0fe2233eb7b3b93f7f0e27abd3.png)

### 回调函数类型的ref

 回调函数形式的ref 会将该标签DOM传给回调函数的形参，用this接住挂载在组件实例上。

```jsx
class Person extends React.Component {
    showData = (event) =&gt; {
        event.preventDefault()
        let { input2: { value } } = this
        console.log(value)
    }
    render() {
        return (
            &lt;form&gt;
                &lt;input ref={a =&gt; this.input1 = a} type=&quot;text&quot; placehoder=&quot;username&quot; /&gt;
                &lt;button onClick={this.showData}&gt;提交&lt;/button&gt;
                &lt;input ref={b =&gt; this.input2 = b} type=&quot;password&quot; placehoder=&quot;password&quot; /&gt;
            &lt;/form&gt;
        )
    }
}
ReactDOM.render(&lt;Person /&gt;, el)
```

![image-20220614104935057](https://img.outsider404.com/asuhe-blog-img/2024/04/bb2d659a4492fb9c16690182aae9576a.png)

### refs对象类型的ref

使用自定义ref对象 ref对象内含{ current:被ref标记的标签实例 } 当多个标签使用同一个ref对象标记时，仅保留最后一个。

```jsx
class Person extends React.Component {
    // 若要ref标记多个标签实例，则需要声明多个属性创建多个React.createRef
    myRefs = React.createRef()
    showData = (event) =&gt; {
        event.preventDefault()
        let { current: { value } } = this.myRefs
        console.log(value)
    }
    render() {
        return (
            &lt;form&gt;
                &lt;input ref={this.myRefs} type=&quot;text&quot; placehoder=&quot;username&quot; /&gt;
                &lt;button onClick={this.showData}&gt;提交&lt;/button&gt;
                &lt;input ref={this.myRefs} type=&quot;password&quot; placehoder=&quot;password&quot; /&gt;
            &lt;/form&gt;
        )
    }
}
ReactDOM.render(&lt;Person /&gt;, el)
```

![image-20220614105049968](https://img.outsider404.com/asuhe-blog-img/2024/04/a89ef197f37448cd8034bfb0ecfd77dc.png)</content:encoded><author>Asuhe</author></item><item><title>React基础(二)</title><link>https://asuhe.org/blog/2f4ccfc1/</link><guid isPermaLink="true">https://asuhe.org/blog/2f4ccfc1/</guid><pubDate>Mon, 20 Jun 2022 21:07:05 GMT</pubDate><content:encoded># 生命周期(旧)

`React`的生命周期可以为两条线，一是初始挂载阶段的生命周期，二是组件状态更新阶段的生命周期。在组件挂载阶段的生命周期只会触发一次，而组件状态更新阶段的生命周期可以多次触发。

在下图中，左边为挂载阶段的生命周期，右边为状态更新阶段的生命周期。

![Untitled Diagram (1)](https://img.outsider404.com/asuhe-blog-img/2024/04/2bef8adab67b899e5db5154aaf06d305.png)

特别说明`componentWillRecevieProps`函数只有在父组件给子组件传递`props`时才会被调用。而`shouldComponentUpdate`则一定会被调用，若我们不自己指定则会默认`return true`。

# 生命周期(新)

在`React 16.3` 中引入了新的生命周期，在旧版生命周期中删除了`componentWillReceiveProps`、`componentWillMount`、`componentWillUpdate`这三个生命周期。引入了两个新的生命周期`getDerivedStateFromProps`和`getSnapshotBeforeUpdate`。

![React生命周期(新)](https://img.outsider404.com/asuhe-blog-img/2024/04/4fef9a0865890d87261b6c5d3356fac3.png)</content:encoded><author>Asuhe</author></item><item><title>React基础(三)</title><link>https://asuhe.org/blog/51bfefea/</link><guid isPermaLink="true">https://asuhe.org/blog/51bfefea/</guid><pubDate>Tue, 28 Jun 2022 21:33:48 GMT</pubDate><content:encoded># 路由

在`React`中可以使用`react-router-dom`来实现前端路由功能。它使用分别暴露暴露出一些组件以供我们使用。

常用的内置路由组件有：

- &lt;BrowserRouter&gt;

  &gt; 用 BrowserRouter 标签包裹整个页面，使页面有一个全局的管理路由关系的路由器。并将路由的模式设置为常规路由模式，类似于 Vue 中 history 模式

- &lt;HashRouter&gt;

  &gt; HashRouter 标签和 BrowserRouter 一样，只是路由模式变更为 hash 模式

- &lt;Route&gt;

  &gt; Route 标签是控制路由组件的显示和隐藏，并且可以使用`exact={true}`将路由匹配模式设置为精准匹配，而不是默认的模糊匹配。若该路由下还有子路由，则不能使用精准匹配模式

- &lt;Redirect&gt;

  &gt; Redirect 标签是设置一个默认的重定向路由，我们可以使用它设定默认路由。当所有路由匹配失败时，也会重定向到该路由

- &lt;Link&gt;

  &gt; Link 标签是用于更改地址栏的路径，当我们点击该标签时地址栏的路径就会变成该标签的路径

- &lt;NavLink&gt;

  &gt; NavLink 标签能够设定导航的默认样式，利用`activeClassName=&quot;指定样式类&quot;`可以将导航标签的样式设置为指定的类的样式

- &lt;Switch&gt;

  &gt; Switch 标签的功能是将原本路由的全匹配模式改为匹配完即终止，默认当匹配到多个 Route 标签时会将所有的匹配到的路由组件展示，使用 Switch 包裹则当匹配到第一 Route 时即停止往下匹配

例如我们要实现一个页面的路由切换，那你能利用`&lt;Link&gt;`和`&lt;Route&gt;`组件将路由组件和跳转标签关联起来。

```jsx
// index.jsx
// 使用history模式路由
import React from &apos;react&apos;
import ReactDOM from &apos;react-dom&apos;
import App from &apos;./App&apos;
import {BrowserRouter} from &apos;react-router-dom&apos;

ReactDOM.render(
	&lt;BrowserRouter&gt;
		&lt;App/&gt;
	&lt;/BrowserRouter&gt;,
document.getElementById(&apos;root&apos;))
```

```jsx
// app.jsx
// 需求：点击标签跳转到about页面,并启用replace模式和精准匹配
// 		设置默认页面为404
//		设置初次匹配到即停止
import React, { Component } from &apos;react&apos;
import Home from &apos;./components/Home&apos;
import About from &apos;./components/About&apos;
import DefaultPage from &apos;./components/404&apos;
import {NavLink,Route,Redirect,Switch} from &apos;react-router-dom&apos;

export default class App extends Component {
	render(){
        return (
            &lt;div&gt;
                &lt;ul&gt;
                    &lt;li&gt;
                        &lt;NavLink to=&quot;/about&quot; activeClassName=&quot;demo&quot; replace={true}&gt;AboutPage&lt;/NavLink&gt;
                        &lt;NavLink to=&quot;/home&quot; activeClassName=&quot;demo&quot;&gt;HomePage&lt;/NavLink&gt;
                    &lt;/li&gt;
                &lt;/ul&gt;
                &lt;div&gt;
                    &lt;Switch&gt;
                        &lt;Route path=&quot;/about&quot; exact={true}&gt;&lt;/Route&gt;
                        &lt;Route path=&quot;/home&quot;&gt;&lt;/Route&gt;
                        &lt;Redirect to=&quot;/about&quot;&gt;
                    &lt;/Switch&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        )
    }
}

```

## 路由传参

有时候我们想在路由跳转的时候携带一些参数给对应的路由组件，路由组件可以根据传递过来的参数发送网络请求。在`React`当中有总共有三种路由传参的方式，分别是：

- params 传参
- search 传参
- state 传参

### params 传参

`params`传参就是利用地址栏里的`params`参数进行参数的传递，例如组件 A 给组件 B 传递两个 params 参数`id`和`name`

```jsx
// A.jsx
import React,{Component} from &apos;react&apos;
import {Link,Route,Router} from &apos;react-router-dom&apos;
import B from &quot;./B.jsx&quot;

export default class A extends Component {
    state = {
        id:&quot;001&quot;,
        name:&quot;asuhe&quot;
    }
    render(){
        return (
            &lt;div&gt;
                &lt;div&gt;
                    &lt;Link to={`/b/${this.state.id}/${this.state.name}`}&gt;click&lt;/Link&gt;
                &lt;/div&gt;
                &lt;div&gt;
                    {/* react-router-dom v6版本以下写法 */}
                    {/* &lt;Route path=&quot;/b/:id/:name&quot; component={B} /&gt; */}
                    &lt;Routes&gt;
                        &lt;Route path=&quot;/b/:id/:name&quot; element={&lt;B&gt;&lt;/B&gt;} /&gt;
                    &lt;/Routes&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        )
    }
}
```

而在组件 B 中，我们可以在 props 中接收到多个属性对象，传入的 params 参数我们可以在组件 B 的 props 对象中的 match 里的 params 中获取。

```jsx
// B.jsx
import React,{Component} from &quot;react&quot;
export default class B extends Component {
    render(){
        console.log(this.props)
    }
}
```

#### react-router-dom v5.x

```jsx
//路由链接(携带参数)：
&lt;Link to=&apos;/demo/test/tom/18&apos;}&gt;详情&lt;/Link&gt;
//或 &lt;Link to={{ pathname:&apos;/demo/test/tom/18&apos; }}&gt;详情&lt;/Link&gt;

//注册路由(声明接收)：
&lt;Route path=&quot;/demo/test/:name/:age&quot; component={Test}/&gt;

//接收参数：
this.props.match.params
```

#### react-router-dom v6.x

```jsx
//路由链接(携带参数)：
&lt;Link to={{ pathname:`/b/child1/${id}/${title}` }}&gt;Child1&lt;/Link&gt;
//或 &lt;Link  to={`/b/child1/${id}/${title}`}&gt;Child1&lt;/Link&gt;

//注册路由(声明接收)：
&lt;Route path=&quot;/b/child1/:id/:title&quot; component={Test}/&gt;

//接收参数：
import { useParams } from &quot;react-router-dom&quot;;
const params = useParams();
//params参数 =&gt; {id: &quot;01&quot;, title: &quot;消息1&quot;}
```

### search 传参

`React`中的`search`传参实际上就是我们传统请求参数中的`query`传参。只是写法稍微有些差异。

```jsx
// A.jsx
render(){
    return(
        &lt;div&gt;
            &lt;div&gt;
                &lt;Link to={`/b?id=${this.state.id}&amp;$name={this.state.name}`}&gt;点击跳转至组件B&lt;/Link&gt;
            &lt;/div&gt;
            &lt;div&gt;
                &lt;Router&gt;
                    &lt;Route path=&quot;/b&quot; component={B}&gt;&lt;/Route&gt;
                &lt;/Router&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    )
}
```

传入的 search 参数我们可以在组件 B 的 props 对象中的 location 里的 search 中获取。

#### react-router-dom v5.x

```jsx
//路由链接(携带参数)：
&lt;Link to=&apos;/demo/test?name=tom&amp;age=18&apos;}&gt;详情&lt;/Link&gt;

//注册路由(无需声明，正常注册即可)：
&lt;Route path=&quot;/demo/test&quot; component={Test}/&gt;

//接收参数：
this.props.location.search

//备注：获取到的search是urlencoded编码字符串(例如: ?id=10&amp;name=zhangsan)，需要借助query-string解析参数成对象
```

#### react-router-dom v6.x

```jsx
//路由链接(携带参数)：
 &lt;Link className=&quot;nav&quot; to={`/b/child2?age=20&amp;name=zhangsan`}&gt;Child2&lt;/Link&gt;

//注册路由(无需声明，正常注册即可)：
&lt;Route path=&quot;/b/child2&quot; component={Test}/&gt;

//接收参数方法1：
import { useLocation } from &quot;react-router-dom&quot;;
import qs from &quot;query-string&quot;;
const { search } = useLocation();
//search参数 =&gt; {age: &quot;20&quot;, name: &quot;zhangsan&quot;}

//接收参数方法2：
import { useSearchParams } from &quot;react-router-dom&quot;;
const [searchParams, setSearchParams] = useSearchParams();
// console.log( searchParams.get(&quot;id&quot;)); // 12

//备注：获取到的search是urlencoded编码字符串(例如: ?age=20&amp;name=zhangsan)，需要借助query-string解析参数成对象
```

### state 传参

`state`传参就是把`&lt;Link&gt;`里面的`to`属性换成一个对象，再将路径和参数信息填入该对象。

```jsx
// A.jsx
render(){
    &lt;div&gt;
        &lt;div&gt;
            &lt;Link to={{pathname:&quot;/b&quot;,state:{id:this.state.id,name:this.state.name}}}&gt;点击跳转至组件B&lt;/Link&gt;
        &lt;/div&gt;
        &lt;div&gt;
            &lt;Router&gt;
                &lt;Route path=&quot;/b&quot; component={B}&gt;&lt;/Route&gt;
            &lt;/Router&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    )
}
```

传入的 state 参数我们可以在组件 B 的 props 对象中的 location 里的 state 中获取。

#### react-router-dom v5.x

```jsx
//路由链接(携带参数)：
&lt;Link to={{pathname:&apos;/demo/test&apos;,state:{name:&apos;tom&apos;,age:18}}}&gt;详情&lt;/Link&gt;

//注册路由(无需声明，正常注册即可)：
 &lt;Route path=&quot;/demo/test&quot; component={Test}/&gt;

//接收参数：
this.props.location.state

//备注：刷新也可以保留住参数
```

#### react-router-dom v6.x

```jsx
//通过Link的state属性传递参数
 &lt;Link
     className=&quot;nav&quot;
     to={`/b/child2`}
     state={{ id: 999, name: &quot;i love merlin&quot; }}
 &gt;
    Child2
&lt;/Link&gt;

//注册路由(无需声明，正常注册即可)：
&lt;Route path=&quot;/b/child2&quot; component={Test}/&gt;

//接收参数：
import { useLocation } from &quot;react-router-dom&quot;;
const { state } = useLocation();
//state参数 =&gt; {id: 999, name: &quot;我是asuhe&quot;}

//备注：刷新也可以保留住参数
```

## 编程式路由

编程式路由可以允许我们用`javascript`动态生成路由链接

#### react-router-dom v5.x

```jsx
// A.jsx
class A extends Component {
    pushRoute = ()=&gt;{
        this.props.history.push(&quot;/b&quot;)
    }
    render(){
        return (
            &lt;div&gt;
                &lt;button onClick={this.pushRoute}&gt;&lt;/button&gt;
                &lt;Route path=&quot;/b&quot; component={B}/&gt;
            &lt;/div&gt;

        )
    }
}

```

#### react-router-dom v6.x

```jsx
function A {
    const navigate = useNavigate()
    navigate(&apos;b&apos;, {
        state: {
            id: item.id,
            content: item.content,
            title: item.title
        }
    })
    return (
    	&lt;div&gt;
            &lt;button onClick={() =&gt; navigate(-1)}&gt;back&lt;/button&gt;
            &lt;button onClick={() =&gt; navigate(1)}&gt;go&lt;/button&gt;
        &lt;/div&gt;
    )
}
```

### withRouter

当我想要在非路由组件中使用路由组件中的`push`、`replace`等编程式路由的函数时，就可以使用`withRouter`来获取这些函数。`withRouter`作为一个高阶组件，其作用是将一个组件包裹进`Route`里面, 然后`react-router`的三个对象`history,、location、match`就会被放进这个组件的 props 属性中。

```jsx
import React, { Component } from &apos;react&apos;
import {withRouter} from &apos;react-router-dom&apos;
class C extends Component {
	back = ()=&gt;{
		this.props.history.goBack()
	}
  	forward =  ()=&gt;{
		this.props.history.goForward()
	}
	render() {
		return (
			&lt;div &gt;
                &lt;button onClick={this.back}&gt;回退&lt;/button&gt;
                &lt;button onClick={this.forward}&gt;前进&lt;/button&gt;
			&lt;/div&gt;
		)
	}
}
export default withRouter(C)
```</content:encoded><author>Asuhe</author></item><item><title>Web Workers简介</title><link>https://asuhe.org/blog/b9660a40/</link><guid isPermaLink="true">https://asuhe.org/blog/b9660a40/</guid><pubDate>Sun, 11 Sep 2022 10:48:35 GMT</pubDate><content:encoded># Web Workers

因为Javascript从设计之初就是单线程的，所以它没有原生支持多线程操作。它没有办法在浏览器UI线程之外运行代码。Web Workers API改变了这种状况，它引入了一个接口，能够使代码运行且不占用浏览器UI线程的时间。作为HTML5最初的一部分，Web Workers API已经被分离出去称为独立的规范。

Web Workers给Web应用带来的潜在的巨大性能提升，因为每个Worker都在自己的线程中运行代码。这意味着Worker运行代码不仅不会影响浏览器UI，也不会影响其它Worker中运行的代码。

## Worker运行环境

由于Web Workers没有绑定UI线程，这也意味着它们不能访问浏览器的许多资源。Javascript和UI共享同一线程的部分原因是它们之间互相访问频繁，因此这些任务失控回导致糟糕的用户体验。Web Workers从外部线程中修改DOM会导致用户界面出现错误。但是每个Web Worker都有自己的全局运行环境，其功能只是javascript特性的一个子集。Web Worker运行环境如下部分组成：

- navigator对象，只包含四个属性：appName、appVersion、userAgent和platform
- location对象，与window.location相同只是所有属性都是只读的
- self对象，指向全局worker对象
- importScript方法，用来加载Worker所用到的外部Javascript文件
- 所有ECMAScript对象，如Object、Array、Date、Math等
- XMLHttpRequest构造器
- setTimeout和setInterval方法
- close方法，它能够立即停止Worker运行

Web Worker有着不同的全局运行环境，因此你无法从Javascript的代码中创建它。使用它需要一个完全独立的js文件，里面包含Worker中运行代码。

```javascript
var worker = new Worker(&quot;test.js&quot;);
```

这个代码一但执行，将为这个文件创建一个新的线程和一个新的Worker运行环境。该文件会被异步下载，直到文件被下载并执行完成后才会启动此Worker

## 与Worker通信

Worker与网页代码通过事件接口通信。网页代码可以使用`postMessage()`方法给Worker传递数据，它接受一个参数。此外Worker中有用于接收信息的`onmessage`事件处理器。

```javascript
var worker = new Worker(&quot;test.js&quot;);
worker.onmessage = function(event){
  console.log(event.data); // hello,world
};
worker.postMessage(&quot;Asuhe&quot;);
```

Worker内通过触发`message`事件来接收数据。Worker可以通过自己的`postMessage()`方法来把数据传递回页面。

```javascript
// test.js
self.onmessage = function(event){
  console.log(event.data); // Asuhe
  self.postMessage(&quot;hello,world&quot;);
}
```

这种方式是它们之间通信的唯一方式。

允许传递基本类型的数据和对象。其它类型的数据不允许被传递。

## Worker加载外部文件

Worker通过`importScript()`方法加载外部js文件，该方法可以接受一个或多个js文件的url作为参数。它的调用过程是阻塞式的，直到所有文件成功加载并执行后才会继续运行后续其它代码。</content:encoded><author>Asuhe</author></item><item><title>正则表达式急速上手</title><link>https://asuhe.org/blog/5a90a05f/</link><guid isPermaLink="true">https://asuhe.org/blog/5a90a05f/</guid><pubDate>Sun, 06 Nov 2022 00:56:43 GMT</pubDate><content:encoded># 正则表达式急速上手

正则表达式，即 Regular Expressions，缩写为 Regex 或 Regexp，是在正则表达式语法框架内创建的字符串。您可以用正则表达式轻松管理数据，执行查找、匹配或编辑等命令。正则表达式适用于 Python、SQL、JavaScript、R、Google Analytics、Google Data Studio 等编程语言和整个编程过程。

## 基本匹配

我们想要查找的字符或单词可以直接输入，就像搜索一样。例如匹配文本中的curious:

```javascript
const str = &quot;I have no special talents. I am only passionately curious.&quot;;
const reg = /curious/g;
console.log(str.match(reg)); // [&apos;curious&apos;]
```

## 基础元字符

### .

`.`：`.`允许匹配任何字符，包括特殊字符和空格。它可以匹配任何一个单个的字符。

&gt; 在同一个正则表达式里允许使用多个`.`字符，它们既可以连续出现（一个接着一个如：`..`将匹配任意两个字符），它也可以间隔着出现在模式的不同位置。
&gt;
&gt; 如果我们需要匹配`.`本身，则需要在.前面加上一个转义字符`\`

```js
const str = &quot;OK. !&quot;;
const reg = /./g;
console.log(str.match(reg)); // [&apos;O&apos;,&apos;K&apos;,&apos; &apos;,&apos;!&apos;]
```

### []

`[]`：`[]`表示一个字符集。如果一个词中的字符可以是各种字符，我们就将所有的可选字符写进中括号` [] `中。

例如，为了查找文本中的所有单词，我们需要编写表达式，在 `[]` 中相邻地输入字符 a、e、i、o、u。

```js
const str = &quot;bar ber bir bor bur&quot;;
const reg = /b[aeiou]r/g;
console.log(str.match(reg)); // [ &apos;bar&apos;, &apos;ber&apos;, &apos;bir&apos;, &apos;bor&apos;, &apos;bur&apos; ]
```

为了查找指定范围的字母，我们需要将起始字母和结束字母写进` [] `中，中间用连字符` - `分隔。它区分大小写。例如编写一个表达式，匹配 e 和 h 之间所有的小写字母，包括它们本身。

```js
const str = &quot;abcdefghijklmnopqrstuvwxyz&quot;;
const reg = /[e-h]/g;
console.log(str.match(reg)); // [ &apos;e&apos;, &apos;f&apos;, &apos;g&apos;, &apos;h&apos; ]
```

为了查找指定范围的数字，我们需要在` [] `中输入起始和结束数字，中间用连字符 `- `分隔。例如编写一个表达式，匹配 3 到 6 之间的所有数字，包括它们本身。

```js
const str = &quot;0123456789&quot;;
const reg = /[3-6]/g;
console.log(str.match(reg)); // [ &apos;3&apos;, &apos;4&apos;, &apos;5&apos;, &apos;6&apos; ]
```

### ^

`^`：`^`表示取反。正常的匹配模式可以理解为白名单，`^`就类似于将该字符设为黑名单模式。

为了查找下方文本的所有单词（ber 和 bor 除外），请在 [] 中的 ^ 后面并排输入 e 和 o。

```js
const str = &quot;bar ber bir bor bur&quot;;
const reg = /b[^eo]r/g;
console.log(str.match(reg)); // [&apos;bar&apos;,&apos;bir&apos;,&apos;bur&apos;]
```

## 重复匹配

### +

`+`：`+`表示一个字符可以出现一次或多次。

例如，表示 e 在下方文本中出现一次或多次。

```js
const str = &quot;br ber beer&quot;;
const reg = /be+r/g;
console.log(str.match(reg)); // [ &apos;ber&apos;, &apos;beer&apos; ]
```

### *

`*`：`*`表示一个字符完全不匹配或可以匹配多次。

例如，表示字母 e 在下方文本中不出现，只出现 1 次或者并排出现多次。

```js
const str = &quot;br ber beer&quot;;
const reg = /be*r/g;
console.log(str.match(reg)); // [ &apos;br&apos;, &apos;ber&apos;, &apos;beer&apos; ]
```

### ?

`?`：`?`表示一个字符是可选的。

例如，表示下方文本中的字母 u 是可选的。

```js
const str = &quot;color, colour&quot;;
const reg = /colo?r/g;
console.log(str.match(reg)); // [ &apos;color&apos; ]
```

### {}

`{}`：`{}`表示一个字符出现的次数，我们在该字符的末尾，将它出现的次数写进大括号` {} `中。

如 {n}。例如，表示下方文本中的字母 e 只能出现 2 次。

```js
const str = &quot;ber beer beeer beeeer&quot;;
const reg1 = /be{2}r/g;
console.log(str.match(reg1)); // [ &apos;beer&apos; ]

// 利用{}限定字符可以出现次数的范围
const reg2 = /be{2,4}/g;
console.log(str.match(reg2)); // [ &apos;beer&apos;, &apos;beeer&apos;, &apos;beeeer&apos; ]

// 可以利用{}限定该字符至少应该出现几次，只需要在区间数字的后一位留空
const reg3 = /be{3,}r/g;
console.log(str.match(reg3));// [ &apos;beeer&apos;, &apos;beeeer&apos; ]
```



## 修饰符



## 分组匹配

### ()

`()`：`()`可以对一个表达式进行分组，并用这些分组来引用或执行一些规则。本质上它是扩大修饰符的作用域，因为通常情况下修饰字符只会作用于它前一个字符中。

为了给表达式分组，我们需要将文本包裹在 () 中。现在为下方文本中的 haa 构造分组。

```js
const str = &quot;ha-ha,haa-haa&quot;;
const reg1 = /(haa)/g;
console.log(str.match(reg1)); // [ &apos;haa&apos;, &apos;haa&apos; ]

// 除此我们还可以通过引用分组的形式来避免重复书写相同的分组
const reg2 = /(ha)-\1,(haa)-\2/g;
console.log(str.match(reg2)); // [ &apos;ha-ha,haa-haa&apos; ]
```

### (?:)

`(?: )`： `(?: )`表示非捕获分组。它对可以表达式进行分组，并确保它不被引用捕获。

例如，下面有两个分组，但我们用 \1 引用的第一个组实际上是指向第二个组，因为第一个是未被捕获的分组。

```js
const str = &quot;ha-ha,haa-haa&quot;;
const reg = /(?:ha)-ha,(haa)-\1/g;
console.log(str.match(reg)); // [ &apos;ha-ha,haa-haa&apos; ]
```

### |

`|`：`|`竖线允许一个表达式包含多个不同的分支。所有分支用` | `分隔。和在字符层面上运作的字符集 [abc] 不同，分支在表达式层面上运作。

例如，下面的表达式同时匹配 cat 和 Cat。在末尾添加另一个 |，并输入 rat 以匹配所有单词。

```js
const str = &quot;cat Cat rat&quot;;
const reg = /(C|c)at|rat/g;
console.log(str.match(reg)); // [ &apos;cat&apos;, &apos;Cat&apos;, &apos;rat&apos; ]
```



## 位置匹配

### \b

`\b`：`\b`表示这个位置的字符是一个能够用于构成单词的字符（字母、数字、下划线，也就是在`\w`范围内的字符）。

例如，匹配 cat 单词，但是不匹配包含 cat 这三个字符的内容。

```js
const str = &quot;The cat scattered his food all over the room&quot;;
const reg = /\bcat\b/g;
console.log(str.match(reg)); // [ &apos;cat&apos; ]
```

 ### \B

`\B`：`\B`表示这个位置的字符是并非为`\w`范围内的字符，和`\b`取反。

### ^

`^`：`^`放在行首可以定义字符串的开头。

例如：我们用 [0-9] 查找数字，若仅查找行首的数字，可以在表达式前面加上 ^。

```js
const str = `1. 3 eggs, beaten
2. 1 tsp sunflower oil
3. 1 tsp butter`;
const reg = /^[0-9]/gm;
console.log(str.match(reg)); // [ &apos;1&apos;, &apos;2&apos;, &apos;3&apos; ]
```

### $

`$`：`$`用于定义字符串的结尾。

我们可以在 html 的后面添加 `$`，来查找仅在行末出现的 html。

```js
const str = `https://domain.com/what-is-html.html
https://otherdomain.com/html-elements
https://website.com/html5-features.html`;
const reg = /html$/gm;
console.log(str.match(reg)); // [ &apos;html&apos;, &apos;html&apos; ]
```



最后推荐一个练习网站[RegexLearn](https://regexlearn.com/zh-cn/learn)。</content:encoded><author>Asuhe</author></item><item><title>设计模式概述</title><link>https://asuhe.org/blog/bb7571ed/</link><guid isPermaLink="true">https://asuhe.org/blog/bb7571ed/</guid><pubDate>Sun, 06 Nov 2022 22:05:21 GMT</pubDate><content:encoded>## 何为设计模式

说起设计模式很多人就以为设计模式就是那23种设计模板，设计思路。其实不是的。在我看来设计模式应该把设计与模式拆分开来看待。设计是设计，指用按某种思路、原则或标准去实现功能达到目的。我将其称之为方法。而模式，指实现功能、达到目的的手段，是在设计原则指导下用以具体实操的手段。设计是方法、原则，模式是手段、实现。就如ECMAScript和Javascript的关系。**23种设计模式本质上就是在编程语言抽象能力不足的情况下，应对某些场景的最佳实践。**

## 设计原则

设计原则有很多，有不同分类。设计原则并不是不可违背的，就像有时候为了性能不得不牺牲代码可读性一样，是否遵循某条设计原则取决于我们对目标收益的衡量。

现在大多数的设计原则都是基于面向对象的思想提出的。所以其对面向对象有非常强的指导性，但这并不表示只有面向对象才适用这些设计原则。

既然讲到了设计原则，那么就必须讲讲最流行的SOLID设计的原则。SOLID意思为固体，但是它其实是五个原则缩写而成。

### Single Responsibility Principle

S：单一职责原则（Single Responsibility Principle）。单一职责原则简称 SRP ，顾名思义，就是一个类只负责一个职责。一个程序或者一个类就只负责做好一件事，把这件事情做到极致。功能过于复杂的就将其拆分开，各个部分保持独立，不依赖外部，以便后面可以组合。这种思路就类似于当今的流水线、各国的全球化分工、前后端分离。

### Open / Closed Principle

O：开放封闭原则（Open / Closed Principle）。开放封闭原则简称 OCP，它的开放指对扩展保持开放，封闭指对修改保持封闭，即可扩展(extension)，不可修改(modification)。所谓对扩展开放就是若想要扩展一个类的功能，应该是开放的、容易的。而想要修改原有功能是封闭的、困难的。该规则的“封闭”部分规定，一旦模块被开发和测试完成，代码被修改的原因应该只有修复bug这一种情况。 “开放”部分说，您应该能够扩展现有代码（而不是修改之前的代码）以引入新功能。与SRP一样，该原理通过限制对现有代码的更改来降低引入新错误的风险。

### Liskov Substitution Principle

L：里氏替换原则（Liskov Substitution Principle）。里氏替换原则简称LSP，它强调的是一个对象在其出现的任何地方，都要可以用子类的实例做替换，而程序不出错。即子类可以覆盖父类的所有功能。父类能干的，子类要都能干。子类能干父类却不一定能干。这个原则是与面向对象语言的 继承 特性密切相关的。LSP 原则最重要的一点就是：避免子类重写父类中已经实现的方法。这就是 LSP 原则的本质。子类应该避免对父类方法进行重写，如果需要增加个性化，就应该对父类进行扩展，而不是重写，否则也会违背OCP原则。

### Interface Segregation Principle

I：接口隔离原则（Interface Segregation Principle）。ISP表示接口应该保持单一独立，维持单一职责。这一点和SRP类似，但其不同之处是ISP强调一个类对另外一个类的依赖性应当是建立在最小的接口上的，达到最小依赖性。这一点是以SRP为基础。它还强调客户端不应该依赖它不需要的接口。如果一个类实现一个接口，但这个接口中有它不需要的方法，那么就需要把这个接口拆分，把它需要的方法提取出来，组成一个新的接口让这个类去实现，这就是接口隔离原则。简而言之，就是说，接口中的所有方法对其实现的子类都是有用的。否则，就将接口继续细分。可以把ISP理解为SRP的强化版。

### Dependency Inversion Principle

 D：依赖倒置原则（Dependency Inversion Principle）。DIP强调高级模块不应该依赖于低级模块，两者都应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。什么意思呢？简而言之就是要多用接口、针对接口编程，而不是针对具体实现细节编程。类似于学习中我们要针对概念进行学习、理解，最终的目的是理解抽象的概念。而不是去背诵对概念的文字描述、定义。

{% youtube  R-WnUpl49zg %}</content:encoded><author>Asuhe</author></item><item><title>Charles基础教程</title><link>https://asuhe.org/blog/7ac30d69/</link><guid isPermaLink="true">https://asuhe.org/blog/7ac30d69/</guid><pubDate>Mon, 14 Nov 2022 21:28:41 GMT</pubDate><content:encoded>因为工作上开发移动端的应用，而移动端并没有像Chrome那样好用的调试工具去查看网络请求的信息，所以需要利用抓包来进行网络请求的监控调试。在此介绍一些Charles抓包工具的基础使用，以作备份。

## 基础抓包

本文以抓ios手机上的流量为例。安卓手机的基本步骤也是同理。

### 基本流程

抓包基本流程：

- 确保手机与抓包的电脑连接在同一局域网下
- 抓包电脑开启http代理，并设置好本机的端口号
- 手机设计好代理服务器为抓包电脑的ip和端口

### 图例

- 设置电脑代理的端口，以`192.168.6.238:8888`为例
  - 打开Charles。
  - Proxy -&gt; Proxy Setting。设置端口号

![setProxy](https://img.outsider404.com/asuhe-blog-img/2024/04/5b04be7e3459d3465fdabb4917b9038d.png)

- 手机设置代理服务器的地址与端口号
  - 找到代理服务器IP以及端口
  - 设置代理

![lookIp](https://img.outsider404.com/asuhe-blog-img/2024/04/0a4e2a2a5f2f8b8aeccc7e9e6b892e9b.png)

## Https抓包

- 根据提示在手机上安装好证书ssl

- charles开启http抓包

  ![image-20230115104052948](https://img.outsider404.com/blogImg%2Fimage-20230115104052948.png)

![image-20230115104402665](https://img.outsider404.com/blogImg%2Fimage-20230115104402665.png)

## Mock数据

- 选中要mock的接口右键
- 映射为本地接口或者远程接口

![image-20230115104730170](https://img.outsider404.com/blogImg%2Fimage-20230115104730170.png)

![image-20230115105028625](https://img.outsider404.com/blogImg%2Fimage-20230115105028625.png)

![image-20230115105358136](https://img.outsider404.com/blogImg%2Fimage-20230115105358136.png)

## http拦截并修改后再发送数据

- 选中要拦截的接口右键
- 设置断点

![image-20230115104852689](https://img.outsider404.com/blogImg%2Fimage-20230115104852689.png)

![image-20230115104934941](https://img.outsider404.com/blogImg%2Fimage-20230115104934941.png)</content:encoded><author>Asuhe</author></item><item><title>文件上传原理</title><link>https://asuhe.org/blog/e71b1b82/</link><guid isPermaLink="true">https://asuhe.org/blog/e71b1b82/</guid><pubDate>Thu, 15 Dec 2022 13:03:14 GMT</pubDate><content:encoded># 浏览器文件上传原理

 现代网站的前端上传文件的流程主要是利用`input`标签读取到文件，再利用`ajax`将文件的数据发送到后端服务器。

因为web程序的运行环境是在浏览器里，在这个流程中web应用无法像直接运行在操作系统里的程序那样直接操作用户计算机上的文件。以前的做法是利用`&lt;input type=&quot;file&quot;&gt;`把文件放到一个表单中提交。而以前提交表单通常需要刷新页面，现代web应用发送数据基本都是利用`ajax`异步提交、获取数据。所以以前利用表单提交数据的方法做法就不太合适了。

为了解决`js`在浏览器环境中运行无法操作用户计算机文件的痛点，浏览器增加了File API和Blob API以增强`js`访问文件的能力。

## File API

HTML5在DOM的事件对象上添加了files数组，这个集合里保存了一组File对象，用来标明用户选中的文件。File对象中的属性保存着选中文件的相关信息。通过`Event`对象的`target`对象里的`files`属性我们可以访问到File对象。

```html
&lt;input id=&quot;inputBox&quot; type=&quot;file&quot; /&gt; 
```

```javascript
const inputBox = document.getElemetById(&apos;inputBox&apos;);
inputBox.onChange = function(event){
	const selectedFile = event.target.files[0];
    console.log(selectedFile);
}
```

![image-20230115133040378](https://img.outsider404.com/blogImg%2Fimage-20230115133040378.png)

File对象部分属性说明

| 属性             | 作用                       |
| ---------------- | -------------------------- |
| **name**         | 文件名称                   |
| **size**         | 以字节为单位计算的文件大小 |
| **type**         | 文件的MIME类型             |
| lastModified     | 文件最后修改时间的时间戳   |
| lastModifiedDate | 文件最后修改的时间         |

## FileReader类型

FileReader通常用于读取文件，它是一种异步读取文件的API。它类似于`XMLHttpRequest`，只不过它是从本地计算机上读取数据，而不是通过网络读取远程计算机上的数据。

**`FileReader`** 对象允许 Web 应用程序异步读取存储在用户计算机上的文件（或原始数据缓冲区）的内容，使用 [`File`](https://developer.mozilla.org/zh-CN/docs/Web/API/File) 或 [`Blob`](https://developer.mozilla.org/zh-CN/docs/Web/API/Blob) 对象指定要读取的文件或数据。

```javascript
const reader = new FileReader();
console.log(reader);
```

![image-20230115134232538](https://img.outsider404.com/blogImg%2Fimage-20230115134232538.png)

FileReader部分属性说明

| 属性          | 说明                                                         |
| ------------- | ------------------------------------------------------------ |
| error         | 返回读取文件时的错误信息。                                   |
| readyState    | 表示文件读取的状态。0：reader已创建；1：读取中；2：读取方法被调用。 |
| **result**    | 返回读取的文件内容。                                         |
| **onload**    | 事件。当文件读取成功时，触发该事件。若error事件被触发，则该事件不会被触发。 |
| onloadstart   | 事件。开始读取文件时，触发该事件。                           |
| onprogress    | 事件。文件读取时，每过50ms触发一次该事件。                   |
| **onloadend** | 事件。当文件读取完成时，触发该事件，无论文件读取成功与否。   |
| **onerror**   | 事件。当文件读取失败时，触发该事件。                         |
| onabort       | 事件。取消文件读取时触发该事件。                             |

FileReader类型提供了几个读取文件数据的方法分别用于不同场景。

### readAsText()

`readAsText` 方法可以将 [Blob](https://developer.mozilla.org/zh-CN/docs/Web/API/Blob) 或者 [File](https://developer.mozilla.org/zh-CN/docs/Web/API/File) 对象转根据特殊的编码格式转化为内容 (字符串形式)，这个方法是异步的，

`readAsText`执行完以后会将`readyState`属性改为2，表示文件读取完成。

函数签名

```javascript
 instance of FileReader.readAsText(blob[, encoding]);
```

使用实例

```javascript
const reader = new FileReader();
reader.readAsText(BlobObj, &apos;utf-8&apos;);
reader.onload = (res) =&gt; {
    console.log(&apos;result&apos;, reader.result);
}
```



### readAsDataURL()

`readAsDataURL` 方法可以将 [Blob](https://developer.mozilla.org/zh-CN/docs/Web/API/Blob) 或者 [File](https://developer.mozilla.org/zh-CN/docs/Web/API/File) 对象转化为base64编码的字符串，这个方法也是异步的。

`readAsDataURL`执行完以后会将`readyState`属性改为2，表示文件读取完成。

函数签名

```javascript
 instance of FileReader.readAsDataURL(blob);
```

使用实例

```javascript
const reader = new FileReader();
reader.readAsDataURL(BlobObj);
reader.onload = (res) =&gt; {
    console.log(&apos;result&apos;, reader.result);
}
```



### readAsBinaryString()（废除）

`readAsBinaryString` 方法可以将 [Blob](https://developer.mozilla.org/zh-CN/docs/Web/API/Blob) 或者 [File](https://developer.mozilla.org/zh-CN/docs/Web/API/File) 对象转化为原始文件的二进制格式。

`readAsBinaryString`执行完以后会将`readyState`属性改为2，表示文件读取完成。

函数签名

```javascript
 instance of FileReader.readAsBinaryString(blob);
```

使用实例

```javascript
const reader = new FileReader();
reader.readAsBinaryString(BlobObj);
reader.onload = (res) =&gt; {
    console.log(&apos;result&apos;, reader.result);
}
```

### readAsArrayBuffer()

用于替代`readAsBinaryString`。

### abort()

该方法可以取消 FileReader 的读取操作，触发之后 [`readyState`](https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readyState) 为已完成（DONE），值为2。

函数签名

```javascript
instanceOfFileReader.abort();
```

使用实例

```javascript
const reader = new FileReader();
setTimeout(() =&gt; reader.abort(), 100);
```

## Blob类型

`Blob` 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取，也可以转换成 [`ReadableStream`](https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStream) 来用于数据操作。

Blob 表示的不一定是 JavaScript 原生格式的数据。**[`File`](https://developer.mozilla.org/zh-CN/docs/Web/API/File) 接口基于 [`Blob`](https://developer.mozilla.org/zh-CN/docs/Web/API/Blob)，继承了 blob 的功能并将其扩展以支持用户系统上的文件。**



## 实现一个简单的图片上传

- 实现多张图片上传并限制图片大小小于2MB

```html
&lt;input id=&quot;inputBox&quot; type=&quot;file&quot; accept=&quot;image/*&quot; multiple=&quot;true&quot; /&gt;
```

```javascript
const inputBox = document.getElementById(&quot;inputBox&quot;);
function checkImgSize(file){
    const limitSize = 2 * 2 ** 20;
    return file.size &lt; limitSize;
}
inputBox.onchange = function(event){
    const selectedFiles = this.files;
    const isPass = selectedFiles.every(checkImgSize);
    if (!isPass) return alert(&quot;请确保每张图片的大小都小于2MB&quot;);
    // do something
}
```</content:encoded><author>Asuhe</author></item><item><title>V8的GC机制</title><link>https://asuhe.org/blog/d9ee3f42/</link><guid isPermaLink="true">https://asuhe.org/blog/d9ee3f42/</guid><pubDate>Thu, 01 Sep 2022 10:48:35 GMT</pubDate><content:encoded># V8的GC机制

## V8对JS使用内存的限制

V8引擎在执行JS的过程中限制了JS可以使用内存的大小。通常在32位系统下，JS可以使用的内存大小约为0.7GB而64位系统下约为1.4GB。当我们一直申请内存，JS所使用内存超过这个限制时就会报错。造成这个问题的主要原因在于JavaScript对象基本上都是通过V8自己的方式来进行分配和管理的。

**在V8中，所有的JavaScript对象都是通过堆来进行分配的。** 当我们在代码中声明变量并赋值时，所使用对象的内存就分配在堆中。至于V8为何要限制堆的大小，表层原因为V8最初为浏览器而设计，不太可能遇到用大量内存的场景。对于网页来说，V8的限制值已经绰绰有余。

深层原因是V8的垃圾回收机制的限制。**按官方的说法，以1.5 GB的垃圾回收堆内存为例，V8做一次小的垃圾回收需要50毫秒以上，做一次非增量式的垃圾回收甚至要1秒以上。这是垃圾回收中引起JavaScript线程暂停执行的时间，在这样的时间花销下，应用的性能和响应能力都会直线下降。 **这样的情况不仅仅后端服务无法接受，前端浏览器也无法接受。因此，在当时的考虑下直接限制堆内存大小是一个好的选择。

当然，这个限制也不是不能打开，V8依然提供了选项让我们使用更多的内存。Node在启动时可以传递`--max-old-space-size`或`--max-new-space-size`来调整内存限制的大小，示例如下：

```bash
node --max-old-space-size=1700 test.js # 单位为MB
node --max-new-space-size=1024 test.js # 单位为KB上述参数在V8初始化时生效，一旦生效就不能再动态改变。
```

如果遇到Node无法分配足够内存给JavaScript对象的情况，可以用这个办法来放宽V8默认的内存限制，避免在执行过程中稍微多用了一些内存就轻易崩溃。

## GC策略

**V8的垃圾回收策略主要基于分代式垃圾回收机制。**在自动垃圾回收的演变过程中，人们发现没有一种垃圾回收算法能够胜任所有的场景。因为在实际的应用中，对象的生存周期长短不一，不同的算法只能针对特定情况具有最好的效果。为此，统计学在垃圾回收算法的发展中产生了较大的作用，现代的垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不同的分代，然后分别对不同分代的内存施以更高效的算法。 

### 分代式垃圾回收

**在V8中JavaScript对象被分为新生代和老生代。新生代通常是一些生命周期较短的对象，而老生代则通常是新生代经过一轮GC后仍然存活的对象晋升而来。**

前面我们提及的`--max-old-space-size`命令行参数可以用于设置老生代内存空间的最大值，`--max-new-space-size`命令行参数则用于设置新生代内存空间的大小的。

![V8中GC管理的内存](https://img.outsider404.com/blogImg%2F202211052310591.jpg)

#### 新生代GC

在分代的基础上，V8新生代中的对象主要通过Scavenge算法进行垃圾回收。在Scavenge的具体实现中，主要采用了Cheney算法，该算法由C. J. Cheney于1970年首次发表在ACM论文上。 

Scavange算法将新生代堆分为两部分，分别叫from-space和to-space，工作方式也很简单，就是将from-space中存活的活动对象复制到to-space中，并将这些对象的内存有序的排列起来，然后将from-space中的非活动对象的内存进行释放，完成之后，将from space 和to space进行互换，这样可以使得新生代中的这两块区域可以重复利用。

![新生代内存空间](https://img.outsider404.com/blogImg%2F202211052313142.jpg)

Scavenge的缺点是只能使用堆内存中的一半，这是由划分空间和复制机制所决定的。但Scavenge由于只复制存活的对象，并且对于生命周期短的场景存活对象只占少部分，所以它在时间效率上有优异的表现。它是典型的以空间换时间的算法。

当一个对象经过多次复制依然存活时，它将会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中，采用新的算法进行管理。对象从新生代中移动到老生代中的过程称为晋升。

对象晋升的条件主要有两个，一个是对象是否经历过Scavenge回收，一个是To空间的内存占用比超过限制（25%）。 

在默认情况下，V8的对象分配主要集中在From空间中。对象从From空间复制到To空间时，会检查它的内存地址。以判断这个对象是否已经经历了一次Scavenge回收。若已经历过了，则将该对象晋升至老生代内存空间。

![uplevel](https://img.outsider404.com/asuhe-blog-img/2024/04/d30de0573fdc0cbd8dcc70023c30cba3.jpg)

设置25%这个限制值的原因是当这次Scavenge回收完成后，这个To空间将变成From空间，接下来的内存分配将在这个空间中进行。如果占比过高，会影响后续的内存分配。

对象晋升后，将会在老生代的内存空间中存活，由老生代的GC算法进行处理。

由于Scavenge是典型的牺牲空间换取时间的算法，所以无法大规模地应用到所有的垃圾回收中。但可以发现，Scavenge非常适合应用在新生代中，因为新生代中对象的生命周期较短，恰恰适合这个算法。

#### 老生代GC

对象晋升后，将会在老生代空间中作为存活周期较长的对象来对待，接受新的回收算法处理。对于老生代中的对象，由于存活对象占较大比重，再采用Scavenge的方式会有两个问题：一个是存活对象较多，复制存活对象的效率将会很低；另一个问题依然是浪费一半空间的问题。这两个问题导致应对生命周期较长的对象时Scavenge会显得捉襟见肘。为此，**V8在老生代中主要采用了Mark-Sweep和Mark-Compact相结合的方式进行垃圾回收。**

##### Mark-Sweep

Mark-Sweep是标记清除的意思，它分为标记和清除两个阶段。与Scavenge复制活着的对象不同，Mark-Sweep在标记阶段遍历堆中的所有对象，并标记活着的对象，在随后的清除阶段中，只清除没有被标记的对象。可以看出，Scavenge中只复制活着的对象，而Mark-Sweep只清理死亡对象。活对象在新生代中只占较小部分，死对象在老生代中只占较小部分，这是两种回收方式能高效处理的原因。

Mark-Sweep算法最大的问题就是在进行一次标记清理回收后，会产生内存碎片，内存空间会出现不连续的状态。这种内存碎片会对后续内存分配造成影响。例如要分配一个100MB大小的对象，此时总体空闲的内存空间大小为150MB但是并没有一个连续的100MB大小的内存空间，这就会导致内存分配失败。这种情况会导致提前触发GC是程序停顿，而此次的GC是不必要的。为了解决Mark-Sweep的问题，所以Mark-Compact被提出。

##### Mark-Compact

 Mark-Compact是标记整理的意思，是在Mark-Sweep的基础上演变而来的。它们的差别在于对象在标记为死亡后，在整理的过程中，将活着的对象往一端移动，移动完成后，直接清理掉边界外的内存。这样就可以减少内存碎片。总体上的思路就是将小碎片移动、合并成一个大块的内存空间。

这里将Mark-Sweep和Mark-Compact结合着介绍不仅仅是因为两种策略是递进关系，在V8的回收策略中两者是结合使用的。

#### GC停顿处理

为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致的情况，垃圾回收的3种基本算法都需要将应用逻辑暂停下来，待执行完垃圾回收后再恢复执行应用逻辑，这种行为被称为“全停顿”（stop-the-world）。

在V8的分代式垃圾回收中，一次小垃圾回收只收集新生代，由于新生代默认配置得较小，且其中存活对象通常较少，所以即便它是全停顿的影响也不大。但V8的老生代通常配置得较大，且存活对象较多，全堆垃圾回收（full垃圾回收）的标记、清理、整理等动作造成的停顿就会比较可怕，需要设法改善。

为了降低全堆垃圾回收带来的停顿时间，V8先从标记阶段入手，将原本要一口气停顿完成的动作改为增量标记（incremental marking），也就是拆分为许多小“步进”，每做完一“步进”就让JavaScript应用逻辑执行一小会儿，垃圾回收与应用逻辑交替执行直到标记阶段完成。

V8后续还引入了延迟清理（lazy sweeping）与增量式整理（incremental compaction），让清理与整理动作也变成增量式的。</content:encoded><author>Asuhe</author></item><item><title>如何在Node.js中使用ES6模块化语法</title><link>https://asuhe.org/blog/5f30bb4e/</link><guid isPermaLink="true">https://asuhe.org/blog/5f30bb4e/</guid><pubDate>Sat, 25 Mar 2023 23:46:00 GMT</pubDate><content:encoded>众所周知，在 Node 中不能直接使用 ES6 模块。通常情况下是这样的，但是如果我们非要用 ES6 模块那也是可以滴。下面我就介绍两种在 Node 环境下使用 ES6 模块的方法。不过在此之前，你必须保证项目中 Node 的版本在 14 以上。

## 方法一：修改文件后缀名

在 Node 项目中使用 ES6 模块最简单的方法就是将`.js`文件的扩展名替换为`.mjs`。Node 在编译`.mjs`文件时，将会使用`ES6模块化规范`来处理代码，而不是使用默认的`CommonJS模块化规范`。同时，你也可以用`.cjs`文件扩展名告诉 Node 使用`CommonJS模块化规范`处理代码。简而言之，你可以通过文件扩展名来告诉 Node 使用哪种模块化规范来处理代码，默认使用的是`CommonJS`。

### 在 ES6 模块化的文件中导入 CommonJS 模块

Node 支持在`ES6模块`中导入`CommonJS模块`。例如：

```javascript
// a.mjs
import test from &apos;b&apos;;
test.helloworld(); // hello world!
```

```javascript
// b.cjs
module.exports = {
    helloworld: function(){
        console.log(&apos;hello world!&apos;);
    }
}
```

但是反过来，在`CommonJS模块`中导入`ES6模块`在 Node 中不支持。如果非要在`CommonJS模块`中使用`ES6模块`，我们可以借助一些编译器来帮助我们把`ES6模块`翻译成`CommonJS模块`，从而实现导入。

## 方法二：配置 Package.json 文件

如果想要在整个项目中都使用`ES6模块化规范`，一个一个更改文件扩展名太费劲了。我们可以通过在项目的`package.json`文件中添加`&quot;type&quot;: &quot;module&quot;`。例如：

```json
// package.json
{
  &quot;name&quot;: &quot;ESM_Module&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;This is a test project&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;type&quot;: &quot;module&quot;, // 添加类型说明
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;node index.js&quot;
  },
}
```

当我们在`package.json`中添加`&quot;type&quot;: &quot;module&quot;`时，所有的文件都会被 Node 当作 ES6 模块文件来处理，除非该文件的扩展名为`.cjs`。`node_modules`文件夹中的依赖文件还是使用`CommonJS`模块化规范处理，不会受到影响。</content:encoded><author>Asuhe</author></item><item><title>防盗链原理</title><link>https://asuhe.org/blog/6a18b1b9/</link><guid isPermaLink="true">https://asuhe.org/blog/6a18b1b9/</guid><pubDate>Sat, 25 Mar 2023 21:35:52 GMT</pubDate><content:encoded>## 何为防盗链

防盗链是一种用于保护站点内容的技术，通常用于防止其它网站在未经授权的情况下使用本站链接或者嵌入本站内容。图片、视频、音频等资源是最常见的会做防盗链处理的资源。因为这些资源的传输会占用比较大的带宽资源，一是会拖慢源站速度，二是增加源站的带宽负担却不给源站带来任何访问或点击。为了避免这种资源盗用的行为，许多网站都会做防盗链处理，阻止薅羊毛保护自己资源的安全。

例如本站的文章图床就是B站，白嫖B站嘿嘿。右击鼠标，审查文章图片元素，我们可以看见图片链接来自B站。代码大概如下

```html
&lt;img src=&quot;http://article.biliimg.com/bfs/article/cab8d12175592664b2e18008c81b0e47681f46fd.jpg&quot;&gt;
```

![image-20230325220138555](https://img.outsider404.com/blogImg%2Fimage-20230325220138555.png)

## 防盗链的技术手段

防盗链基本都是在服务端做处理的。**当客户端请求资源时，服务器根据客户端请求中携带的某种约定好的标识来确定请求来自我们授权过资源的站点。**目前较为普遍采用的就是HTTP请求头中的`referrer`来判断请求来源。也有其它的方案来进行防盗链处理如：

### Token 防盗链

通过设置 Token 密钥，配合签名过期时间来控制资源内容的访问时限。Token 防盗链采用 md5 算法，将密钥、过期时间、文件路径等信息所计算的 md5 值加入到 URL 中，当客户端在验证请求时，除了验证过期时间，同时还会验证该 md5 值是否匹配，对于不匹配的 md5，说明 URL 被篡改，即使请求未过期也会禁止服务。

### User-Agent 防盗链

每一个客户端都拥有自己的专属 User-Agent，我们可以抓包请求头查询到 User-Agent ，然后将自己的 User-Agent 加入白名单，或者将其他想要禁止的客户端 User-Agent 加入黑名单，从而保证用户只从自己允许的客户端下载内容。

防盗链的方法有很多，但是基本逻辑都是一样的。**其关键点在于如何鉴别请求资源的客户端的身份，鉴权成功就返回资源。**

## 破解防盗链

有了上面明确的基本思考方向，那么破解防盗链的方法也就有头绪。**既然防盗链的关键是身份识别，那么我们想办法绕过身份识别不就可以了。**下面介绍一种现在最常用的`referrer`防盗链方法的破解。

 `referrer`防盗链是利用HTTP请求头里的`referrer`参数来进行客户端身份识别的，服务器可以设置校验这个字段信息。通常服务器的设置是有这个字段信息则进行校验，没有`referrer`字段则默认通过校验。即不强制校验`referrer`字段。利用这个特性我们可以将我们请求中的`referrer`去掉以达到破解防盗链的效果。

### Referrer-Policy

Referrer-Policy是HTTP请求头部的一个字段，它用于表示该次请求使用的`referrer`策略。

![image-20230325223458311](https://img.outsider404.com/blogImg%2Fimage-20230325223458311.png)

它有以下几种值：

```
Referrer-Policy: no-referrer
Referrer-Policy: no-referrer-when-downgrade
Referrer-Policy: origin
Referrer-Policy: origin-when-cross-origin
Referrer-Policy: same-origin
Referrer-Policy: strict-origin
Referrer-Policy: strict-origin-when-cross-origin
Referrer-Policy: unsafe-url
```

关于字段的具体解释，点击[这里](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referrer-Policy)查看。

#### 使用meta标签设定文档的Referrer-Policy

在网页的头部设置meta标签可以指定整个文档的资源请求`referrer`策略。例如设置资源请求不携带`referrer`。

```html
&lt;meta name=&quot;referrer&quot; content=&quot;no-referrer&quot;&gt;
```

关于meta标签的更多用法，点击[这里](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/meta)查看。

需要注意的是使用`meta`标签设定文档的 referrer 策略，在`css`样式中并不生效。什么意思，看下面代码：

```html
&lt;html&gt;
    &lt;head&gt;
        &lt;meta name=&quot;referrer&quot; content=&quot;no-referrer&quot;&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;div
            style=&quot;background-image: url(&apos;http://article.biliimg.com/bfs/article/cab8d12175592664b2e18008c81b0e47681f46fd.jpg&apos;);&quot;&gt;
        &lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;
```

按理来说我们给文档设置了`referrer`策略为`no-referrer`，图片请求的HTTP头不应携带`referrer`头部，但是实际上它还是会携带`referrer`。也就是说如果我们在`css`样式中引用了有防盗链保护的资源，那么它将不会被`meta`标签设定的`referrer`策略影响。

#### 单独给某个元素标签的请求设定Referrer-Policy

在元素上将 `rel` 属性设置为` noreferrer`。例如：

```html
&lt;a href=&quot;http://example.com&quot; rel=&quot;noreferrer&quot;&gt;
```

不过该方法只适用于特定的几个标签，其它的标签不支持`rel`属性。支持`rel`属性的标签有：`&lt;a&gt;`、`&lt;area&gt;`、`&lt;link&gt;`。

还有一些元素标签支持使用`referrerpolicy`属性，为其单独设置Referrer-Policy策略。例如：

```html
&lt;a href=&quot;http://example.com&quot; referrerpolicy=&quot;origin&quot;&gt;
```

支持`referrerpolicy`属性的标签有：`&lt;a&gt;`、`&lt;area&gt;`、`&lt;link&gt;`、`&lt;script&gt;`、`&lt;iframe&gt;`、`&lt;img&gt;`。</content:encoded><author>Asuhe</author></item><item><title>Linux环境下安装MySQL &amp; PHP环境</title><link>https://asuhe.org/blog/9fa73a0/</link><guid isPermaLink="true">https://asuhe.org/blog/9fa73a0/</guid><pubDate>Wed, 05 Apr 2023 16:23:44 GMT</pubDate><content:encoded>## 背景

最近朋友叫我帮忙起一个Nginx + PHP的项目，因为一直做的前端工作。对于后端服务怎么起来没太多了解。今天就从安装MySQL数据库开始，摸索一下后端搭建。

## 先决条件

- 系统环境：Ubuntu
- 用户权限：root

理论上用apt包管理器安装东西的都可以直接套用该教程，不是`apt`的理论上就把`apt`安装命令换一下就行。

## 目标

- 安装5.6版本的MySQL
- 安装7.2版本的PHP

## 安装MySQL

### 更新apt仓库

使用`apt`命令更新包仓库。

```shell
apt update &amp;&amp; apt upgrade
```

尝试直接下载，看你系统里的下载源有没有包括你需要的版本。

```shell
apt install mysql-server-5-6
```

若成功，则直接结束该步骤。若为安装成功，则添加下载源步骤。

### 添加下载源

#### 使用wget手动安装

使用`wget`添加apt的MySQL仓库。

```shell
wget https://dev.mysql.com/get/mysql-apt-config_0.8.12-1_all.deb
```

注意`wget`是一个`Linux`系统上（window版本正在支持ing）的网络下载软件，部分`Linux`系统会自带这个软件，若提示`wget`命令找不到，我们则需要再安装一下`wget`。更多关于[`wget`]((https://www.gnu.org/software/wget/))的介绍。

```shell
apt install wget # 安装wget
```

添加成功后你会见到提示如图：

![image-20230405165215341](https://img.outsider404.com/blogImg%2Fimage-20230405165215341.png)

该种方式的为下载好包进行手动安装（网上大多数资料都是用这个方法），后续请看[这里](https://computingforgeeks.com/how-to-install-mysql-on-ubuntu-focal/)。我觉得这个方式比较麻烦就不进行过多介绍了。

#### 使用apt自动安装

**除了以上方法，我们还可以使用自带的命令添加源。推荐使用自带命令添加源。**

```shell
add-apt-repository &apos;deb http://archive.ubuntu.com/ubuntu trusty universe&apos;
```

添加源完成后我们可以去源的配置文件`/etc/apt/sources.list`里查看。如果你的`add-apt-repository`命令失败了或者有问题，那么也可以直接去编辑`/etc/apt/sources.list`文件添加源。

![image-20230405170728080](https://img.outsider404.com/blogImg%2Fimage-20230405170728080.png)

添加好了以后我们需要更新一下。

```shell
apt update
```

由于系统的安全策略，我们可能会因为没有公钥认证，导致不能使用这个源来更新。因为我的系统已经添加过公钥了，此处用`monodb`演示操作都是一样的。

![image-20230405172133678](https://img.outsider404.com/blogImg%2Fimage-20230405172133678.png)

此时我们需要添加公钥，将上面`NO_PUBKEY`后面的字符串复制下来。执行以下命令添加公钥签名：

```shell
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 68818C72E52529D4
```

添加签名后再执行`apt update`更新一下。

该步骤参考资料点击[这里](https://askubuntu.com/questions/1393381/unable-to-add-ondrej-repository-apt-in-ubuntu-20-04)。

#### 查看apt源里，有哪些MySQL5.6版本的东西可供安装

```shell
apt search mysql | grep 5.6
```

![image-20230405172857651](https://img.outsider404.com/blogImg%2Fimage-20230405172857651.png)

安装5.6版本的服务器和客户端

```shell
apt install mysql-server-5.6 mysql-client-5.6
```

到这里就大功告成了！可以开始愉快地使用了。

在安装过程中会弹出界面叫你设置数据库root用户的用户密码，记得设置就好了。

### 使用命令行连接数据库

连接数据库前我们需要启动`MySQL`服务器。

```shell
service mysql start # 启动
```

检查一下起来没有。

```shell
service mysql status # 查看mysql服务器的状态
```

![image-20230405173928103](https://img.outsider404.com/blogImg%2Fimage-20230405173928103.png)

#### 创建数据库

使用用户账号密码直接连接，因为我们刚刚安装好数据库，里面没有创建任何一个数据。我们直接进入就可以了。这里推荐查看官方教程。[连接数据库教程](https://dev.mysql.com/doc/refman/8.0/en/connecting.html)、[创建数据教程](https://dev.mysql.com/doc/refman/8.0/en/database-use.html)。

```shell
mysql --host=localhost（数据库地址，我们本地起的也没改端口所以默认localhost） --user=用户名（刚刚安装好都是有root用户） --password=设置的密码
```

#### 使用sql脚本创建表 

**前置条件：**

- 数据库里面有一个已经存在的数据库，本例中为`asuhe`
- 可用的SQL脚本，本例中为`test.sql`

这里介绍如何使用现成的数据库脚本来创建数据库的表。其实也很简单，具体命令如下：

```shell
mysql -u username(数据库用户名) -p password（用户密码） asuhe（目标数据库） &lt; test.sql（sql脚本路径）
```

该步骤参考资料点击[这里](https://stackoverflow.com/questions/10769344/create-mysql-database-with-sql-file)。

### 卸载MySQL

```shell
apt remove mysql-client mysql-server -y
```

该步骤参考资料，点击[这里](https://phoenixnap.com/kb/uninstall-mysql)。



## 安装PHP

安装PHP的步骤和MySQL类似啦，都是先更新一下`apt`仓库的软件列表。这里不再过多赘述。

### 安装前置依赖

安装PHP我们需要一些前置依赖来帮我们更好地使用命令行自动安装。执行一下命令安装依赖：

```shell
apt install software-properties-common ca-certificates lsb-release apt-transport-https 
```

### 添加PHP下载源

这里其实和安装MySQL类似，都是使用命令来自动添加仓库源。

```shell
add-apt-repository ppa:ondrej/php
```

与MySQL相同，若你的`add-apt-repository`没法正常用或者报错。可以手动去`sources.list`添加`apt`的源。

```shell
vim /etc/apt/sources.list
```

添加该行。

```shell
deb https://ppa.launchpadcontent.net/ondrej/php/ubuntu focal main
```

该步骤[参考资料](https://askubuntu.com/questions/1393381/unable-to-add-ondrej-repository-apt-in-ubuntu-20-04)。

### 安装需要的PHP版本

安装7.2版本。

```shell
apt install php7.2
```

检查有没有安装好。

```shell
php -v
```

OK，到这里基本都完成了。

## 参考资料

[安装MySQL](https://www.osetc.com/archives/20436.html#ubuntu-install-mysql56)

[安装PHP](https://tecadmin.net/how-to-install-php-on-ubuntu-22-04/)</content:encoded><author>Asuhe</author></item><item><title>Git快速上手指北（二）</title><link>https://asuhe.org/blog/3728f9d8/</link><guid isPermaLink="true">https://asuhe.org/blog/3728f9d8/</guid><pubDate>Fri, 07 Apr 2023 21:02:54 GMT</pubDate><content:encoded>## 前言

工作里习惯用`git`命令行进行代码提交，但是用来用去一直都是只会`push`、`pull`、`checkout`等最基本的操作。还是有一些常用的命令操作不太记得，在此记录一下留一个备份以便日后查看。

## 目标

本文目标是介绍几个经典场景下`git`命令行的运用，通过本文的你将可以学会如下操作：

- 修改历史`commit`的描述信息
- 合并多个`commit`
- 对分支进行重命名
- 推荐的`commit`规范

## 修改历史`commit`

### 修改最近一次的`commit`

#### 方法一

有时我们想对刚刚提交的`commit`信息进行修改，那么此时我们可以将版本退回到上一次`commit`的状态，然后再重新提交一次`commit`。

先使用`git log`命令查看`commit`记录，复制想要回退版本的`HEAD值`。在进行`git reset HEAD值`就可以进行版本的回退。回退以后重新`commit`就OK啦。

```shell
git log # 查看commit记录
git reset HEAD值 # 回退版本
```



需要注意的是`git reset` 命令是取开区间的。什么意思呢，就是`git`会把状态回退到你所取的`HEAD值`的那个版本，在该`HEAD值`以后的`commit`都会被回退，**但并不包括该`HEAD值`的版本。**

![image-20230407213022783](https://img.outsider404.com/blogImg%2Fimage-20230407213022783.png)

#### 方法二

有时候我们有些改动提交到了暂存区，而此时我们又想在不动暂存区的东西的前提下，对最近一次提交的commit进行修改。那么此时我们可以使用`git commit --amend`命令。

```shell
git commit --amend # 修改最近的一条commit
```

这种方法的好处是不会修改`commit`的`HEAD值`，只是单纯修改`commit`信息。适用于我们上面所说的场景。**推荐使用。**

执行该条命令后我们会直接进入`vim`的界面，想要在`vim`的进行编辑我们只需要将光标移动到想要插入字符的位置然后按`i`进入`vim`的插入模式。

`commit`修改完成后，按`ESC键`退出插入模式，进入命令模式。命令模式下按`:`可进行命令的输入。

`:q`，直接退出不保持修改。

`:wq`，保存修改并退出。此时我们`:wq`保存退出即可。

![image-20230407214416961](https://img.outsider404.com/blogImg%2Fimage-20230407214416961.png)



### 修改多条`commit`

如果需要修改多条`commit`的描述信息，那么上面的方法就不太合适了。

我们可以使用`git rebase -i HEAD`命令来进行修改多条`commit`。

```shell
git rebase -i HEAD值
```

![image-20230415120304113](https://img.outsider404.com/blogImg%2Fimage-20230415120304113.png)

再次进入到熟悉的`vim`操作界面，根据提示我们可以将需要修改的`commit`前面的`pick`改成`reword`或者`r`即可完成`commit`的修改。

`git rebase -i` 不止可以这样使用。它还有几种修改选择：

- **pick**：保留该 commit
- **reword**：保留该 commit，但我需要修改该commit的 Message
- **edit**：保留该 commit, 但我要停下来修改该提交(包括修改文件)
- **squash**：将该 commit 和前一个 commit 合并
- **fixup**：将该 commit 和前一个 commit 合并，但我不要保留该提交的注释信息
- **exec**：执行 shell 命令
- **drop**：丢弃这个 commit

## 重命名`git`分支

有时我们想要重命名我们所在的分支，为了实现这个需求，下面我将介绍两种方法来重命名分支。

### 方法一

切换到我们需要重命名的分支上，然后使用`-m`参数重命名分支

```shell
git branch -m new-branch-name
```



### 方法二

在任意分支上，直接使用`-m`参数重命名分支

```shell
git branch -m old-branch-name new-branch-name
```



## 推荐的`commit` 规范

参考比较流行的`AngularJS`的规范，这里我推荐一些`commit`信息的填写规范。

- **feat:** 新增页面或功能
- **fix:** bug修复
- **docs:** 只改动了文档相关的内容
- **style:** 不影响代码含义的改动，例如去掉空格、改变缩进、增删分号
- **build:** 构 造 工 具的 或 者 外 部 依 赖 的 改 动 ， 例 如`webpack`，`npm`，`pom`
- **refactor:** 代码重构时使用,重构（既不是新增功能，也不是修改 bug 的代码变动）
- **revert:** 执行 `git revert` 回退类型的提交信息
- **test:** 添加测试或者修改现有测试
- **perf:** 性能提升改动
- **ci:** 对CI 配置文件和脚本的更改
- **choreL:** 不修改 `src `或者 `test `的其余修改，例如构建过程或辅助工具的变动
- **hotfix:** 紧急修复，</content:encoded><author>Asuhe</author></item><item><title>隐藏元素的几种方式</title><link>https://asuhe.org/blog/94c7f9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/94c7f9ab1/</guid><pubDate>Sat, 14 Dec 2024 23:12:16 GMT</pubDate><content:encoded># CSS 中隐藏元素的不同方式及其应用

在前端开发中，我们经常需要控制元素的显示和隐藏。虽然有多种方式可以实现这一目的，但每种方式都有其特定的使用场景和注意事项。本文将详细探讨四种主要的隐藏方式。

## 四种隐藏方式的对比

### 直接删除对应元素

最直接的也是最彻底的隐藏方式。因为从源码中删除了元素，所以审查元素时，元素会直接消失，不会占用任何空间。下面的三种方式都不会删除源码，只是隐藏了元素。

- **空间占用**：完全移除元素,不占用任何空间
- **动画效果**：无法添加过渡动画
- **性能影响**：会触发页面重排(reflow),性能消耗较大
- **子元素影响**：子元素会被一并删除
- **额外特性**：
  - 需要重新添加到 DOM 才能恢复显示
  - 适合一次性隐藏的场景
  - 适用于页面加载时或特定事件触发时的隐藏需求

### display: none

同第一种隐藏方法一样，隐藏的很彻底。和第一种方法相比不同的是，元素还存在源代码中，审查元素时，元素不存在，只是被隐藏了。但其也有局限性：

- **空间占用**：完全移除元素，不占用任何空间
- **动画效果**：无法添加过渡动画
- **性能影响**：会触发页面[重排(reflow)](https://developer.mozilla.org/zh-CN/docs/Glossary/Reflow)
- **子元素影响**：子元素也会完全隐藏，无法单独控制

### visibility: hidden

相对温和的隐藏方式：

- **空间占用**：元素不可见，但保留原有空间
- **动画效果**：可以配合 opacity 实现平滑过渡
- **性能影响**：只触发[重绘(repaint)](https://developer.mozilla.org/zh-CN/docs/Glossary/Repaint)
- **子元素影响**：子元素可以单独设置为 visible
- **额外特性**：
  - 阻止键盘访问
  - 对屏幕阅读器隐藏内容
  - 提升无障碍访问体验

### pointer-events: none

最轻量级的交互阻止方式：

- **空间占用**：元素保持原有空间和位置
- **动画效果**：可以自由添加各种动画效果
- **性能影响**：性能影响最小
- **子元素影响**：子元素可以单独启用事件
- **局限性**：
  - 只阻止鼠标交互
  - 不影响键盘访问
  - 不影响屏幕阅读器

## 实际应用示例

以下是一个搜索结果框的实现示例：

```css
#search-results.search-results-hidden {
  opacity: 0;
  transform: translate(-50%, 10px);
  pointer-events: none;
}
```

这个实现综合考虑了：

- 视觉隐藏（通过 opacity）
- 交互阻止（通过 pointer-events）
- 平滑过渡（通过 transform 和 transition）

## 选择建议

选择合适的隐藏方式时，需要考虑以下因素：

1. **是否需要动画过渡**

   - 需要动画效果，避免使用 `display: none`
   - 可以选择 `opacity` 配合 `pointer-events` 或 `visibility`

如果需要监听过渡完成事件，可以使用 `visibility: hidden` 和 `opacity` 的组合。监听方式也很简单，使用 [`transitionend事件`](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/transitionend_event) 即可。

```javascript
element.addEventListener(&apos;transitionend&apos;, () =&gt; {
  // 过渡完成后的操作
});
```

2. **性能考虑**

   - 如果频繁切换，优先使用 `pointer-events` 和 `opacity`
   - 如果是一次性隐藏，`display: none` 也是可行的

3. **无障碍性要求**

   - 需要考虑屏幕阅读器，使用 `visibility: hidden`
   - 仅需要视觉隐藏，使用 `opacity` 和 `pointer-events`

4. **空间占用**
   - 需要保留空间，使用 `visibility` 或 `pointer-events`
   - 完全移除空间，使用 `display: none`

## 结论

在实际开发中，这些方法往往需要组合使用以达到最佳效果。理解每种方式的特点和应用场景，才能在适当的地方使用适当的方案。对于大多数简单的交互场景，`pointer-events: none` 配合 `opacity` 的方案已经足够使用，而在需要考虑无障碍性的场景下，可以考虑添加 `visibility: hidden` 作为补充。</content:encoded><author>Asuhe</author></item><item><title>如何将docker容器加入指定网络</title><link>https://asuhe.org/blog/94acf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/94acf9ab1/</guid><pubDate>Sun, 15 Dec 2024 16:12:16 GMT</pubDate><content:encoded>要在启动 Docker 容器时将其加入指定的网络，可以使用 `docker run` 命令的 `--network` 选项。以下是详细步骤和示例：

### 1. 创建一个自定义网络（如果尚未创建）

首先，你可以创建一个自定义网络，以便更好地管理容器之间的通信。Docker 支持多种网络驱动，如 `bridge`、`overlay` 等。这里以创建一个 `bridge` 类型的网络为例：

```bash
docker network create --driver bridge my_custom_network
```

- `my_custom_network` 是你自定义的网络名称，可以根据需要更改。
- `--driver bridge` 指定了网络驱动类型为 `bridge`，这是默认的本地网络驱动。

### 2. 启动容器并加入指定网络

使用 `docker run` 命令时，通过 `--network` 选项指定要加入的网络：

```bash
docker run -d --name my_container --network my_custom_network my_image
```

- `-d`：后台运行容器。
- `--name my_container`：为容器指定一个名称，方便管理。
- `--network my_custom_network`：将容器加入到 `my_custom_network` 网络。
- `my_image`：你要运行的镜像名称，可以是官方镜像或自定义镜像。

### 3. 验证容器是否已加入指定网络

你可以使用以下命令查看容器所属的网络：

```bash
docker inspect my_container
```

在输出信息中，找到 `Networks` 部分，确认容器已连接到 `my_custom_network`。

或者，使用更简洁的命令查看网络中的所有容器：

```bash
docker network inspect my_custom_network
```

这将显示该网络下所有连接的容器。

### 4. 其他相关操作

- **连接已有容器到网络**：

  如果容器已经在运行，但未加入指定网络，可以使用 `docker network connect` 命令：

  ```bash
  docker network connect my_custom_network existing_container
  ```

- **断开容器与网络的连接**：

  使用 `docker network disconnect` 命令：

  ```bash
  docker network disconnect my_custom_network existing_container
  ```

- **查看所有网络**：

  ```bash
  docker network ls
  ```

- **删除网络**：

  注意，只有当没有容器连接到该网络时，才能删除网络。

  ```bash
  docker network rm my_custom_network
  ```

### 示例综合

假设你要运行一个基于 `nginx` 的容器，并将其加入到名为 `web_network` 的网络：

1. 创建网络：

   ```bash
   docker network create --driver bridge web_network
   ```

2. 运行容器并加入网络：

   ```bash
   docker run -d --name my_nginx --network web_network nginx
   ```

3. 验证连接：

   ```bash
   docker network inspect web_network
   ```

### 注意事项

- **网络类型**：选择合适的网络驱动非常重要。例如，`bridge` 适用于单主机上的容器通信，`overlay` 适用于跨主机的容器通信（如在 Docker Swarm 中）。
- **权限和安全**：确保网络配置符合你的安全策略，避免不必要的网络暴露。
- **名称唯一性**：网络名称在同一个 Docker 环境中应保持唯一，以避免冲突。

通过以上步骤，你可以在启动 Docker 容器时将其加入指定的网络，从而实现容器之间的高效通信和管理。</content:encoded><author>Asuhe</author></item><item><title>docker常用网络操作</title><link>https://asuhe.org/blog/21ccf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/21ccf9ab1/</guid><pubDate>Tue, 17 Dec 2024 16:12:16 GMT</pubDate><content:encoded>Docker 作为现代应用部署和管理的重要工具，其网络管理功能在容器化应用中扮演着关键角色。本文将深入介绍 Docker 操作中的 `network` 指令，涵盖网络的增删改查（CRUD 操作）、容器如何加入或退出网络、如何查看网络状态及其包含的容器，以及容器如何查看自身的网络状态和所属网络。

## Docker 网络基础

Docker 网络允许容器之间以及容器与外部世界之间进行通信。Docker 提供了多种网络驱动，包括：

- **bridge（桥接网络）**: 默认的网络驱动，适用于单主机容器通信。
- **host（主机网络）**: 容器共享宿主机的网络栈。
- **overlay（覆盖网络）**: 跨主机的容器通信，常用于 Docker Swarm。
- **macvlan**: 允许容器拥有独立的 MAC 地址，适用于需要直接在物理网络上通信的场景。
- **none**: 不为容器分配网络。

本文主要聚焦于 `bridge` 网络及其管理操作。

## Docker 网络指令概述

Docker 提供了一组命令用于管理网络，这些命令主要通过 `docker network` 子命令实现。常用的网络管理命令包括：

- `docker network create`: 创建新的网络。
- `docker network ls`: 列出所有网络。
- `docker network inspect`: 查看网络的详细信息。
- `docker network rm`: 删除网络。
- `docker network connect`: 将容器连接到网络。
- `docker network disconnect`: 将容器从网络断开。

## 创建网络

创建自定义网络可以更好地控制容器之间的通信。以下是创建不同类型网络的示例：

### 创建桥接网络

```bash
docker network create --driver bridge my_bridge_network
```

- `--driver bridge`: 指定使用桥接网络驱动。
- `my_bridge_network`: 自定义网络名称。

### 创建覆盖网络

```bash
docker network create --driver overlay my_overlay_network
```

- `--driver overlay`: 指定使用覆盖网络驱动。

### 指定子网和网关

```bash
docker network create \
  --driver bridge \
  --subnet 192.168.10.0/24 \
  --gateway 192.168.10.1 \
  my_custom_network
```

- `--subnet`: 指定子网范围。
- `--gateway`: 指定网关地址。

## 删除网络

删除不再需要的网络，以释放资源和避免配置混乱。

```bash
docker network rm my_bridge_network
```

**注意**: 只有在没有容器连接到该网络时，才能成功删除网络。若有容器连接，需要先断开连接或停止相关容器。

## 修改网络

Docker 不支持直接修改已存在网络的配置（如子网、驱动等）。若需修改，需删除现有网络并重新创建。

**步骤**：

1. **断开所有连接到网络的容器**:

   ```bash
   docker network disconnect my_network container_name
   ```

2. **删除网络**:

   ```bash
   docker network rm my_network
   ```

3. **重新创建网络**（根据需要指定新的配置）:

   ```bash
   docker network create --driver bridge --subnet 192.168.20.0/24 my_network
   ```

4. **重新连接容器**:

   ```bash
   docker network connect my_network container_name
   ```

## 查询网络

查看现有网络及其详细信息。

### 列出所有网络

```bash
docker network ls
```

**示例输出**:

```
NETWORK ID          NAME                DRIVER              SCOPE
d1f9f1b0c1a2        bridge              bridge              local
a2b3c4d5e6f7        host                host                local
c3d4e5f6a7b8        my_bridge_network   bridge              local
```

### 查看网络详细信息

```bash
docker network inspect my_bridge_network
```

**示例输出**:

```json
[
    {
        &quot;Name&quot;: &quot;my_bridge_network&quot;,
        &quot;Id&quot;: &quot;c3d4e5f6a7b8...&quot;,
        &quot;Created&quot;: &quot;2024-04-27T12:34:56.789Z&quot;,
        &quot;Scope&quot;: &quot;local&quot;,
        &quot;Driver&quot;: &quot;bridge&quot;,
        &quot;IPAM&quot;: {
            &quot;Driver&quot;: &quot;default&quot;,
            &quot;Options&quot;: null,
            &quot;Config&quot;: [
                {
                    &quot;Subnet&quot;: &quot;192.168.10.0/24&quot;,
                    &quot;Gateway&quot;: &quot;192.168.10.1&quot;
                }
            ]
        },
        &quot;Containers&quot;: {
            &quot;container_id1&quot;: {
                &quot;Name&quot;: &quot;my_container&quot;,
                &quot;EndpointID&quot;: &quot;endpoint_id1&quot;,
                &quot;MacAddress&quot;: &quot;02:42:c0:a8:0a:01&quot;,
                &quot;IPv4Address&quot;: &quot;192.168.10.2/24&quot;,
                &quot;IPv6Address&quot;: &quot;&quot;
            }
        },
        &quot;Options&quot;: {},
        &quot;Labels&quot;: {}
    }
]
```

## 容器加入网络

当启动一个新容器时，可以指定其连接到特定网络。

```bash
docker run -d --name my_container --network my_bridge_network nginx
```

对于已运行的容器，可以使用 `docker network connect` 命令将其连接到网络。

```bash
docker network connect my_bridge_network existing_container
```

**示例**:

```bash
docker network connect my_bridge_network web_app
```

## 容器退出网络

要将容器从网络中断开，可以使用 `docker network disconnect` 命令。

```bash
docker network disconnect my_bridge_network my_container
```

**注意**: 不能断开默认的 `bridge`、`host` 和 `none` 网络，除非明确指定。

## 查看网络状态及包含的容器

### 查看所有网络及其状态

```bash
docker network ls
```

### 查看特定网络的详细信息

```bash
docker network inspect my_bridge_network
```

在 `inspect` 的输出中，`Containers` 字段列出了所有连接到该网络的容器，包括容器名称、IP 地址等信息。

### 示例：列出网络中的容器

假设有一个网络 `my_bridge_network`，执行 `docker network inspect my_bridge_network` 后，可以看到如下部分：

```json
&quot;Containers&quot;: {
    &quot;d1e2f3g4h5i6&quot;: {
        &quot;Name&quot;: &quot;web_app&quot;,
        &quot;EndpointID&quot;: &quot;e7f8g9h0i1j2&quot;,
        &quot;MacAddress&quot;: &quot;02:42:c0:a8:0a:02&quot;,
        &quot;IPv4Address&quot;: &quot;192.168.10.3/24&quot;,
        &quot;IPv6Address&quot;: &quot;&quot;
    },
    &quot;j3k4l5m6n7o8&quot;: {
        &quot;Name&quot;: &quot;db_service&quot;,
        &quot;EndpointID&quot;: &quot;p9q0r1s2t3u4&quot;,
        &quot;MacAddress&quot;: &quot;02:42:c0:a8:0a:03&quot;,
        &quot;IPv4Address&quot;: &quot;192.168.10.4/24&quot;,
        &quot;IPv6Address&quot;: &quot;&quot;
    }
}
```

以上显示了两个容器 `web_app` 和 `db_service` 正连接在 `my_bridge_network` 网络中。

## 容器查看自身网络状态

容器内部可以通过多种方式查看自身的网络状态和所属网络：

### 使用 `ip` 或 `ifconfig` 命令

进入容器内部：

```bash
docker exec -it my_container /bin/bash
```

然后运行：

```bash
ip addr
```

或

```bash
ifconfig
```

这些命令将显示容器的网络接口及其 IP 地址。

### 查看 `/etc/hosts` 和 `/etc/resolv.conf`

容器的 `/etc/hosts` 文件包含主机名和 IP 地址的映射关系，而 `/etc/resolv.conf` 包含 DNS 配置。

```bash
cat /etc/hosts
cat /etc/resolv.conf
```

### 使用 Docker API 或环境变量

某些情况下，可以通过 Docker 提供的 API 或环境变量获取网络信息。不过，这需要额外的配置和权限。

## 容器所属网络的详细查看

除了容器内部的查看方法，宿主机上也可以通过 Docker 命令查看容器所属的网络。

### 查看单个容器的网络

```bash
docker inspect -f &apos;{{json .NetworkSettings.Networks}}&apos; my_container
```

**示例输出**:

```json
{
    &quot;my_bridge_network&quot;: {
        &quot;IPAMConfig&quot;: null,
        &quot;Links&quot;: null,
        &quot;Aliases&quot;: [
            &quot;my_container&quot;
        ],
        &quot;NetworkID&quot;: &quot;c3d4e5f6a7b8...&quot;,
        &quot;EndpointID&quot;: &quot;e7f8g9h0i1j2...&quot;,
        &quot;Gateway&quot;: &quot;192.168.10.1&quot;,
        &quot;IPAddress&quot;: &quot;192.168.10.2&quot;,
        &quot;IPPrefixLen&quot;: 24,
        &quot;IPv6Gateway&quot;: &quot;&quot;,
        &quot;GlobalIPv6Address&quot;: &quot;&quot;,
        &quot;GlobalIPv6PrefixLen&quot;: 0,
        &quot;MacAddress&quot;: &quot;02:42:c0:a8:0a:01&quot;,
        &quot;DriverOpts&quot;: null
    }
}
```

以上信息显示 `my_container` 连接到了 `my_bridge_network` 网络，拥有 IP 地址 `192.168.10.2`。

## 常见网络操作示例

### 创建一个自定义桥接网络

```bash
docker network create --driver bridge --subnet 172.25.0.0/16 my_custom_bridge
```

### 启动容器并连接到自定义网络

```bash
docker run -d --name app1 --network my_custom_bridge nginx
docker run -d --name app2 --network my_custom_bridge nginx
```

### 查看网络中的容器

```bash
docker network inspect my_custom_bridge
```

### 将现有容器连接到另一个网络

```bash
docker network connect another_network app1
```

### 将容器从网络中断开

```bash
docker network disconnect my_custom_bridge app2
```

## 注意事项

- **默认网络**: Docker 默认创建了几个网络，如 `bridge`、`host` 和 `none`。自定义网络更推荐使用，以避免与默认网络配置冲突。

- **网络驱动选择**: 根据应用需求选择合适的网络驱动。例如，单主机应用使用桥接网络，跨主机服务使用覆盖网络。

- **安全性**: 利用 Docker 网络隔离不同服务，提升应用安全性。不同网络中的容器默认无法互相通信，除非显式连接到同一网络。

- **资源管理**: 定期清理不再使用的网络，避免网络资源浪费和配置混乱。

## 总结

Docker 的网络管理功能强大且灵活，通过 `docker network` 命令，用户可以轻松地进行网络的创建、删除、修改和查询操作。同时，容器的网络连接和断开也变得简便。了解和掌握这些网络操作，不仅有助于构建高效、可扩展的容器化应用，也提升了应用的安全性和可维护性。随着容器化技术的不断发展，深入理解 Docker 网络管理将为开发和运维人员带来巨大的优势。</content:encoded><author>Asuhe</author></item><item><title>textContent与innerText和innerHTML的区别</title><link>https://asuhe.org/blog/22ccf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/22ccf9ab1/</guid><pubDate>Tue, 17 Dec 2024 16:12:16 GMT</pubDate><content:encoded>在 JavaScript 中，`textContent`、`innerText` 和 `innerHTML` 是操作 HTML 元素内容的三个常用属性。它们各自有不同的用途和行为，理解它们之间的区别对于有效地操作 DOM（文档对象模型）非常重要。以下是对这三个属性的详细解释及其区别：

## `textContent`

**定义：**
- `textContent` 属性用于获取或设置指定节点及其所有后代节点的文本内容。

**特性：**
- **获取文本内容**：返回元素内部的所有文本，包括隐藏的元素和脚本、样式中的文本。
- **设置文本内容**：会清除元素内部的所有子节点，并将新的文本作为纯文本插入，不会解析为 HTML。
- **性能**：通常比 `innerText` 更高效，因为它不需要考虑样式和渲染。

**示例：**

```javascript
&lt;div id=&quot;example&quot;&gt;
  &lt;p&gt;Hello &lt;span style=&quot;display:none;&quot;&gt;World&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;

const elem = document.getElementById(&apos;example&apos;);
console.log(elem.textContent); // 输出: &quot;Hello World&quot;

elem.textContent = &apos;New Text&apos;;
console.log(elem.innerHTML); // 输出: &quot;New Text&quot;
```

## `innerText`

**定义：**
- `innerText` 属性用于获取或设置元素的“渲染”文本内容，即用户实际看到的文本。

**特性：**
- **获取文本内容**：只返回对用户可见的文本，不包括隐藏的元素内容（例如 `display: none` 或 `visibility: hidden` 的元素）。
- **设置文本内容**：类似于 `textContent`，会清除所有子节点并插入新的文本，但会考虑 CSS 样式，如换行和空格。
- **性能**：因为需要考虑样式和渲染，通常比 `textContent` 性能稍低。

**示例：**

```javascript
&lt;div id=&quot;example&quot;&gt;
  &lt;p&gt;Hello &lt;span style=&quot;display:none;&quot;&gt;World&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;

const elem = document.getElementById(&apos;example&apos;);
console.log(elem.innerText); // 输出: &quot;Hello&quot;

elem.innerText = &apos;New Text&apos;;
console.log(elem.innerHTML); // 输出: &quot;New Text&quot;
```

## `innerHTML`

**定义：**
- `innerHTML` 属性用于获取或设置元素的 HTML 内容，包括所有的子元素和 HTML 标签。

**特性：**
- **获取 HTML 内容**：返回元素内部的完整 HTML 结构。
- **设置 HTML 内容**：会解析字符串中的 HTML 标签，动态生成对应的 DOM 结构。
- **安全性**：直接插入用户提供的内容可能导致 XSS（跨站脚本）攻击，需要谨慎处理。

**示例：**

```javascript
&lt;div id=&quot;example&quot;&gt;
  &lt;p&gt;Hello &lt;span&gt;World&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;

const elem = document.getElementById(&apos;example&apos;);
console.log(elem.innerHTML); // 输出: &quot;&lt;p&gt;Hello &lt;span&gt;World&lt;/span&gt;&lt;/p&gt;&quot;

elem.innerHTML = &apos;&lt;strong&gt;New HTML Content&lt;/strong&gt;&apos;;
console.log(elem.innerHTML); // 输出: &quot;&lt;strong&gt;New HTML Content&lt;/strong&gt;&quot;
```

## 主要区别总结

| 属性       | 获取内容的方式                     | 设置内容的方式                            | 性能          | 适用场景                                       |
|------------|------------------------------------|------------------------------------------|---------------|------------------------------------------------|
| `textContent` | 获取所有文本，包括隐藏的             | 设置纯文本，清除所有子节点                   | 高            | 需要快速获取或设置纯文本内容，不关心样式和隐藏元素 |
| `innerText`   | 获取用户可见的文本                   | 设置文本，考虑样式和隐藏元素                   | 较低          | 需要获取或设置用户实际看到的文本内容             |
| `innerHTML`   | 获取包含 HTML 标签的完整内容         | 设置 HTML 字符串，解析为 DOM 结构               | 较低          | 需要动态插入或修改 HTML 结构时使用               |

### 使用建议

- **使用 `textContent`**：
  - 当你只需要操作纯文本内容，不涉及 HTML 结构时。
  - 需要更好的性能，尤其是在处理大量文本时。

- **使用 `innerText`**：
  - 当你需要获取或设置用户实际看到的文本内容，并且需要考虑 CSS 样式的影响时。
  - 适用于需要与用户界面直接交互的场景。

- **使用 `innerHTML`**：
  - 当你需要动态插入或修改包含 HTML 标签的内容时。
  - 注意防范 XSS 攻击，避免直接插入用户提供的未经消毒的内容。

### 注意事项

- **安全性**：使用 `innerHTML` 时要特别注意安全问题，避免插入恶意脚本。推荐在插入用户生成的内容前进行适当的消毒和验证。
- **浏览器兼容性**：现代浏览器对这三个属性都有良好的支持，但在极少数旧浏览器中可能存在差异。通常无需担心，但在特殊情况下需进行测试。
- **性能优化**：在需要频繁更新 DOM 时，尽量减少直接操作 `innerHTML`，因为频繁解析和渲染 HTML 可能影响性能。可以考虑使用文档片段或其他高效的 DOM 操作方法。

通过理解和合理使用 `textContent`、`innerText` 和 `innerHTML`，可以更高效、安全地操作和管理网页内容。</content:encoded><author>Asuhe</author></item><item><title>meta标签的name属性是干什么的</title><link>https://asuhe.org/blog/25ccf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/25ccf9ab1/</guid><pubDate>Tue, 24 Dec 2024 16:12:16 GMT</pubDate><content:encoded>移动端开发中经常使用视口单位，为此会在全局设置视口单位配置。例如这行 `&lt;meta&gt;` 标签用于设置移动设备上网页的视口（viewport）属性，以确保网页在不同设备上的正确显示。具体来说，这个标签的内容如下：

```html
&lt;meta
  name=&quot;viewport&quot;
  content=&quot;width=device-width,user-scalable=no,initial-scale=0.5,maximum-scale=0.5,minimum-scale=0.5&quot;
/&gt;
```

下面是对每个属性的解释：

## **属性解析**

- **name=&quot;viewport&quot;**
  这个属性指定了该 `&lt;meta&gt;` 标签是关于视口的设置。它告诉浏览器这是一个视口配置，主要用于移动设备。

- **content**
  这个属性定义了视口的具体设置，包括多个子属性，分别控制视口的宽度、缩放行为等。

### **子属性解析**

- **width=device-width**
  这部分设置视口的宽度等于设备的宽度。`device-width` 是一个特殊值，表示设备屏幕的实际宽度（以像素为单位）。这样可以确保网页在不同屏幕尺寸的设备上都能适配。

- **user-scalable=no**
  此设置禁止用户手动缩放页面。即用户无法通过手势或其他方式放大或缩小网页。这在某些情况下有助于保持页面布局的一致性，但可能会影响用户体验。

- **initial-scale=0.5**
  这个属性设置页面首次加载时的缩放比例为 0.5，即页面会被缩小到原始大小的一半。这意味着用户看到的内容会比实际内容小 50%。

- **maximum-scale=0.5**
  此属性限制用户能够放大的最大比例为 0.5。由于 `initial-scale` 和 `maximum-scale` 的值相同，这意味着用户无法放大页面到原始大小以上。

- **minimum-scale=0.5**
  此属性限制用户能够缩小到的最小比例为 0.5。这与 `maximum-scale` 一样，确保了用户在任何情况下都无法将页面缩放到超过 50%的大小。

### **总结**

通过这种配置，开发者可以确保网页在移动设备上以预期的方式显示，同时控制用户交互中的缩放行为。尽管这种设置可以有效防止页面布局混乱，但也可能导致某些用户在阅读或交互时感到不便。因此，在使用这些设置时需要考虑到用户体验与设计需求之间的平衡。

## viewport-fit=cover 的作用是什么

`viewport-fit=cover` 是一个用于网页视口设置的属性，主要用于处理现代设备（尤其是 iPhone X 及其后续型号）上的安全区域问题。它的作用是控制网页内容如何填充整个屏幕，包括设备的“刘海”区域和圆角部分。

### **作用**

- **填充整个屏幕**
  当设置为 `cover` 时，网页内容会扩展到设备的整个显示区域，包括刘海和圆角。这意味着网页会覆盖所有的可视区域，而不留出额外的边距或空白区域。

- **解决遮挡问题**
  在一些设备上，尤其是 iOS 设备，网页内容可能会被地址栏、工具栏或刘海遮挡。使用 `viewport-fit=cover` 可以使网页内容在这些元素之下显示，从而避免被遮挡的问题。

### **取值说明**

`viewport-fit` 属性有三个取值：

- **auto**
  默认值，不影响初始布局视口，整个网页都是可视的。

- **contain**
  页面内容会被限制在安全区域内，确保重要内容不会被遮挡，但可能会在屏幕两侧留出空白。

- **cover**
  页面内容充满整个屏幕，包括刘海和圆角区域。适合需要全屏展示的应用场景。

### **使用示例**

在 HTML 中，可以通过以下方式设置 `viewport-fit=cover`：

```html
&lt;meta
  name=&quot;viewport&quot;
  content=&quot;width=device-width, initial-scale=1.0, viewport-fit=cover&quot;
/&gt;
```

### **总结**

使用 `viewport-fit=cover` 可以有效地解决现代手机（特别是带有刘海和圆角的设备）上网页展示的问题，使得页面能够充分利用屏幕空间，提升用户体验。在开发移动端网页时，合理使用这个属性可以确保页面在各种设备上的一致性和美观性。

## viewport-fit=cover 和 iOS 的安全区域配合使用

`viewport-fit=cover` 是一个用于网页视口配置的属性，特别设计用于适配 iOS 设备（尤其是 iPhone X 及其后续型号）。它的主要作用是确保网页内容能够覆盖整个屏幕，包括设备的安全区域（如刘海和底部黑条）。以下是该属性与 iOS 的安全区域配合使用的详细说明：

### **1. 安全区域概念**

在 iOS 11 及以上版本中，Apple 引入了安全区域的概念，以确保网页内容不会被设备的状态栏、刘海或底部的 Home Indicator 遮挡。安全区域定义了网页内容应该显示的区域，避免重要内容被遮挡。

### **2. viewport-fit 属性**

- **取值**
  `viewport-fit` 有三个取值：
  - **contain**：网页内容会被限制在安全区域内，不会超出这个区域。
  - **cover**：网页内容会扩展到整个屏幕，包括刘海和底部黑条。这意味着可能会有部分内容被遮挡。
  - **auto**：默认行为，与 `contain` 相同。

### **3. 使用场景**

**适配 iPhone X 及后续型号**

- 当开发者希望网页能够充分利用屏幕空间时，使用 `viewport-fit=cover` 是合适的选择。这样设置后，网页可以填满整个显示区域，但需要注意的是，某些页面元素（如固定定位的导航栏）可能会被刘海或底部黑条遮挡。

**CSS 中的安全区域处理**

为了避免内容被遮挡，我们通常可以结合使用 CSS 的 `env()` 和 `constant()` 函数来动态调整页面元素的位置。例如，可以通过以下方式设置 padding：

```css
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
```

这种方法确保了即使在不同设备或不同 iOS 版本上，页面元素也能正确显示在安全区域内。

### **4. 实际应用示例**

在 HTML 中，可以通过以下方式设置视口：

```html
&lt;meta
  name=&quot;viewport&quot;
  content=&quot;width=device-width, initial-scale=1.0, viewport-fit=cover&quot;
/&gt;
```

结合 CSS，确保页面元素不会被遮挡：

```css
body {
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}
```

### **5. 总结**

使用 `viewport-fit=cover` 可以让网页内容充分利用 iOS 设备的屏幕空间，但同时也需要开发者注意到可能出现的遮挡问题。通过合理使用 CSS 的安全区域函数，可以有效提升用户体验，使得网页在各种设备上都能正常显示。

## 在非 iOS 设备上是否需要使用 viewport-fit=cover

在非 iOS 设备上，使用 `viewport-fit=cover` 的必要性取决于具体的设计需求和目标设备的特性。以下是一些关键点，帮助理解在不同平台上使用该属性的情况：

### **1. 主要用途**

- **适用于有刘海或圆角的设备**
  `viewport-fit=cover` 主要是为了解决现代智能手机（尤其是带刘海和圆角的 iPhone）在显示网页时可能出现的内容遮挡问题。在这些设备上，网页内容可以扩展到整个屏幕，包括刘海和圆角区域。

### **2. 非 iOS 设备的情况**

- **Android 设备**
  在 Android 设备上，`viewport-fit=cover` 的效果可能不如在 iOS 设备上明显。虽然 Android 也有一些带刘海的设备，但大多数情况下，Android 浏览器不会自动应用安全区域的边距。因此，在这些设备上，使用 `viewport-fit=cover` 可能不会带来显著变化。

- **其他平台**
  对于其他操作系统或设备（如桌面浏览器），使用 `viewport-fit=cover` 通常没有必要，因为这些平台通常不会有刘海或特殊的屏幕形状。

### **3. 用户体验**

- **影响用户缩放功能**
  在某些情况下，使用 `viewport-fit=cover` 可能会影响用户的缩放体验。例如，在 Android 上，如果同时设置了 `maximum-scale=1` 和 `viewport-fit=cover`，可能会完全禁用用户缩放功能，这可能会对可访问性产生负面影响[3]。

### **4. 总结**

虽然在非 iOS 设备上使用 `viewport-fit=cover` 不一定是必要的，但在设计响应式网站时，考虑到不同设备和用户体验的差异是重要的。如果你的网页需要在各种设备上都能良好展示，并且特别关注那些带有特殊屏幕形状的设备，那么可以考虑使用该属性。否则，对于大多数传统屏幕，使用标准的视口设置就足够了。

## 参考文章

- [通过 viewport-fit=cover 解决 H5 页面在 IOS 下安全区域的问题](https://juejin.cn/post/7270146769685577765)
- [How to use “viewport-fit=cover” and “maximum-scale=1” without disabling zoom on Android](https://stackoverflow.com/questions/60303424/how-to-use-viewport-fit-cover-and-maximum-scale-1-without-disabling-zoom-on)
- [Is viewport-fit=cover no longer working on the iOS Safari?](https://stackoverflow.com/questions/60154236/is-viewport-fit-cover-no-longer-working-on-the-ios-safari/70760857)
- [viewport-fit=cover does not work on the iOS Safari?](https://stackoverflow.com/questions/76475354/viewport-fit-cover-does-not-work-on-the-ios-safari/76507424)</content:encoded><author>Asuhe</author></item><item><title>React真曝光Hook设计思路</title><link>https://asuhe.org/blog/23ccf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/23ccf9ab1/</guid><pubDate>Wed, 18 Dec 2024 16:12:16 GMT</pubDate><content:encoded>## 需求背景

在前端开发中，我们经常需要统计页面元素的曝光数据，比如广告展示、商品曝光等。一个完善的曝光统计需要考虑以下场景：

1. 元素是否在视口内可见
2. 资源是否加载完成
3. 页面切换状态时的重新曝光
4. 防止重复曝光

## 核心设计思路

### 曝光判定机制

```typescript
new IntersectionObserver((entries) =&gt; {
  for (const entry of entries) {
    exposedRef.current = entry.intersectionRatio &gt;= 0.9;
    // ...
  }
}, {
  threshold: 0.9
});
```

设计要点：

- 使用 IntersectionObserver API 替代传统的 scroll+getBoundingClientRect 方案
- 设置 90%曝光阈值，确保元素充分展示
- 使用 ref 记录曝光状态，避免重复触发

### 资源加载控制

```typescript

const resourceLoaded = useRef(false);
const onResourceLoad = useCallback(() =&gt; {
  resourceLoaded.current = true;
  setTimeout(() =&gt; {
    if (!document.hidden &amp;&amp; exposedRef.current) {
      invokeExposeCallback(onExposeRef.current);
    }
  }, 1);
}, []);

```

设计要点：
提供资源加载状态控制
使用 setTimeout 确保 DOM 布局稳定
同时判断页面可见性和曝光状态

### 页面可见性管理

```typescript

const callbacks: Array&lt;() =&gt; void&gt; = [];
const onPageVisible = (cb: () =&gt; void) =&gt; {
  callbacks.push(cb);
};

document.addEventListener(&apos;visibilitychange&apos;, () =&gt; {
  if (!document.hidden) {
    triggerExpose();
  }
});

```

设计要点：
实现简单的发布订阅模式
统一管理页面可见性回调
支持多个曝光实例同时工作 4. 重渲染处理

```typescript
useEffect(() =&gt; {
  if (elRef.current === preElRef.current) {
    return;
  }

  if (preElRef.current) {
    exposedRef.current = false;
    intersectionObserverRef.current!.unobserve(preElRef.current);
  }

  // ... 重新观察新元素
  preElRef.current = elRef.current;
});
```

设计要点：
比较新旧 ref 避免重复处理
清理旧元素的观察者
重置曝光状态

### 生命周期管理

```typescript
useEffect(() =&gt; {
  onPageVisible(cb);
  return () =&gt; {
    exposedRef.current = false;
    intersectionObserverRef.current?.disconnect();
    offPageVisible(cb);
  };
}, []);
```

设计要点：
组件卸载时清理所有监听器
重置曝光状态
移除页面可见性回调

## 性能优化考虑

防抖处理：

```typescript

setTimeout(() =&gt; {
  // 延迟处理资源加载后的曝光
}, 1);

```

缓存优化：

```typescript

const onExposeRef = useRef(onExpose);
onExposeRef.current = onExpose;

```

条件判断优化：

```typescript

if (elRef.current === preElRef.current) {
  return;
}

```

## 使用示例

```typescript
function Banner() {
  const { ref, onResourceLoad } = useExpose({
    needLoadResource: true,
    onExpose: () =&gt; {
      // 上报曝光数据
      trackEvent(&apos;banner_expose&apos;);
    }
  });

  return (
    &lt;div ref={ref}&gt;
      &lt;img
        src=&quot;banner.jpg&quot;
        onLoad={onResourceLoad}
      /&gt;
    &lt;/div&gt;
  );
}

```

## 扩展性考虑

- 支持自定义曝光阈值
- 支持多个资源加载完成的场景
- 支持自定义曝光条件判断

## 总结

这个曝光 Hook 的设计充分考虑了：

- 准确性：通过 IntersectionObserver 保证曝光判定准确
- 性能：避免频繁 DOM 操作和重复计算
- 可靠性：完善的生命周期管理和清理机制
- 易用性：简单的 API 设计，开箱即用
- 扩展性：预留配置项，支持自定义场景
  这样的设计使得埋点代码与业务逻辑解耦，提高了代码的可维护性和复用性。</content:encoded><author>Asuhe</author></item><item><title>2024年終總結</title><link>https://asuhe.org/blog/26ccf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/26ccf9ab1/</guid><pubDate>Sun, 29 Dec 2024 16:12:16 GMT</pubDate><content:encoded>### 年終回顧

二〇二四歲月既逝，回首此年，發覺當初所訂目標，多未達成。此年間接觸新事繁多，尤自財務狀況較去年改善後，對新事物更敢嘗試。昔經濟艱困之時，唯念生存儲蓄，以應失業之危，對新識之事物畏懼嘗試。蓋新嘗試多有時金之費用也。經此一年，亦學會心腸堅硬，愈發認同「心軟不好意思，唯有自毀；理性薄情無情，乃生存之利器」此言。

### 借貸事宜

年初借予H三萬，後續陸續借出一萬餘。約定還款之期過後，H無解釋，如無事發生。初借H，蓋因昔讀書時，面癱生病，H借錢助我治病。雖此錢間接自H借，幾月後醫保報銷即還，感激不盡，亦算還人情。終最後一次借，H言次月還，然次月未還，亦不復聯絡。吾不欲化身討債鬼，故H借錢之事告一段落。

去年亦陸續借予W一萬餘，年初時言今年過年還款。依現況，無望矣。此年間，W亦數次求借，吾以種種理由拒之。最近十月，W求借數百，吾不予，後爭執一時，W自覺過失，致歉於吾。自此W不再提及，吾亦不復談及錢財。

### S輟學之事

去年過年，S言欲輟學，因覺讀書難以繼續，上課常發呆，不如打工。加上班主任對其不滿，吾不允許，特致電責罵F一頓。其欲輟學之因，吾大略能解。

其一，F獨享餘裕，致使家中學費與生活費常缺。每當老師要求繳納學費或資料費，S常無力繳納，需延期，唯有向Q求助。此對處於青少年之S，自尊心受創，實為重大打擊。

其二，經濟困難致使其無心學業，故上課常發呆。

其三，班主任常不滿，屬刺頭學生。雖不擾亂課堂，亦不霸凌同儕，然愛玩好動，影響學風。日常管理費心，費用繳納不齊，令老師困擾，班主任不喜其為常情。

S欲輟學，吾問其故，答曰無心讀書。吾知其實因經濟困難及諸多問題，遂提議全解其讀書費，令其無憂，然S拒之。吾告之，望其完成學業，非為考取高名校，僅願其事業完成，不欲半途而廢。勸導良久，S未應允。

新學期始，S仍往校讀書，然終決意不讀，離校打工。工廠期間，F亦勸其復學，無果。學校有欣賞其之老師，致電勸之，亦未果。上半年，吾反覆與S溝通，終勸其返校。然不久，S復決輟學。其後，S輾轉各地求職。國慶吾返，薦其往M處，薪資尚可，並有照應，然未遂。

### 生活變遷

去年年中，吾本欲今年晉升，然未果，僅薪水稍增，聊勝於無。工作中更深度使用AI輔助，效率增四成，摸魚時間亦大增。於工作中，學會借力打力，主導項目推進，協調資源，使項目順利上線，此為今年工作之大收穫。工作複雜，編程僅一部分，職場軟技能與技術提升須並行，不可偏廢。與領導談及此，領導謂技術無虞，唯軟技能待增。

因去年購置iPhone，連帶今年購全家桶，耗資數萬。使用海外產品增多，接觸Apple低價區及各類雲土耳其、雲尼日利亞居民。學會於X紀錄己思，無人識己，念即掏手機記錄發出。

對加密貨幣應用亦有新認識，以往僅見新聞報導，今年初步接觸，尚未深入。

如開頭所述，對新鮮事物更願嘗試。今年於各種亂七八糟之物上花費約兩千，純為體驗其玩意。明年將有意識控制此方面，因多已體驗，祛魅最佳方式乃擁有。

### 總結

總而言之，今年尚可過去，心態較去年之恐慌為佳。</content:encoded><author>Asuhe</author></item><item><title>如何设置docker容器的环境变量</title><link>https://asuhe.org/blog/14acf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/14acf9ab1/</guid><pubDate>Mon, 16 Dec 2024 16:12:16 GMT</pubDate><content:encoded>## 在 Docker 中设置环境变量：全面指南

在当今快速发展的软件开发领域，容器化技术已成为构建、部署和管理应用程序的核心工具。Docker 作为领先的容器化平台，为开发者提供了强大的功能，其中设置和管理环境变量尤为重要。本文将以具体命令为基础，深入探讨如何在 Docker 中设置环境变量，及其在实际应用中的重要性和最佳实践。

## 环境变量在 Docker 中的重要性

环境变量是一种在运行时向应用程序传递配置信息的简便方式。它们允许开发者在不同的环境（开发、测试、生产等）中灵活地配置应用，而无需修改代码。这种方法不仅提高了配置的灵活性，还增强了应用的可移植性和安全性。

在 Docker 中，环境变量的使用尤为广泛，涵盖了数据库配置、API 密钥、服务端口等多个方面。通过合理设置环境变量，开发者可以轻松管理容器内的应用配置，实现高度自动化和可重复的部署流程。

## 设置环境变量的方法

Docker 提供了多种方式来设置环境变量，满足不同的使用场景和需求。

### 使用 `-e` 或 `--env` 标志

这是最直接的方法，适用于需要在运行容器时传递少量环境变量的情况。

**示例：**

```bash
docker run -e ENV_VAR_NAME=value image_name
```

**优点：**

- 简单直接，适用于快速测试和单个变量设置。

**缺点：**

- 当环境变量较多时，命令行会变得冗长且难以管理。
- 可能在命令历史中暴露敏感信息。

### 使用 `--env-file` 文件

当需要设置多个环境变量时，使用环境变量文件（`.env` 文件）更加高效和安全。

**步骤：**

1. 创建一个 `.env` 文件，定义所需的环境变量：

   ```bash
   MYSQL_ROOT_PASSWORD=996007
   MYSQL_DATABASE=example_db
   MYSQL_USER=example_user
   MYSQL_PASSWORD=example_pass
   ```

2. 使用 `--env-file` 选项运行容器：

   ```bash
   docker run --env-file ./path/to/.env image_name
   ```

**优点：**

- 管理多个环境变量更加简洁。
- 易于版本控制和环境切换。

**缺点：**

- 需要妥善管理 `.env` 文件，避免泄露敏感信息。

### 在 Dockerfile 中使用 `ENV` 指令

通过在 Dockerfile 中定义环境变量，可以在构建镜像时设定默认值。

**示例：**

```dockerfile
FROM mysql:5.6
ENV MYSQL_ROOT_PASSWORD=996007
ENV MYSQL_DATABASE=example_db
ENV MYSQL_USER=example_user
ENV MYSQL_PASSWORD=example_pass
```

**优点：**

- 环境变量与镜像紧密结合，确保一致性。
- 适用于镜像的默认配置。

**缺点：**

- 不适合需要在不同环境中动态配置的变量。
- 可能在镜像中暴露敏感信息。

## 实例解析：理解命令 `sudo docker run --name asuhe -e MYSQL_ROOT_PASSWORD=996007 -p 3306:3306 -d mysql:5.6`

让我们详细解析以下 Docker 命令，以理解其中的环境变量设置及其作用：

```bash
sudo docker run --name asuhe -e MYSQL_ROOT_PASSWORD=996007 -p 3306:3306 -d mysql:5.6
```

### 命令分解

1. **`sudo`**

   - **作用**：以超级用户权限执行命令。
   - **原因**：某些系统配置要求使用超级用户权限才能运行 Docker 命令。

2. **`docker run`**

   - **作用**：创建并启动一个新的容器实例。

3. **`--name asuhe`**

   - **作用**：为容器指定名称 `asuhe`。
   - **好处**：便于后续管理和引用容器。

4. **`-e MYSQL_ROOT_PASSWORD=996007`**

   - **作用**：设置环境变量 `MYSQL_ROOT_PASSWORD`，值为 `996007`。
   - **用途**：为 MySQL `root` 用户设置密码，确保数据库安全。

5. **`-p 3306:3306`**

   - **作用**：端口映射，将宿主机的 3306 端口映射到容器的 3306 端口。
   - **用途**：允许外部通过宿主机的 3306 端口访问容器内的 MySQL 服务。

6. **`-d`**

   - **作用**：以后台（detached）模式运行容器。
   - **好处**：命令执行后，容器在后台运行，终端不会被占用。

7. **`mysql:5.6`**
   - **作用**：指定使用的 Docker 镜像，这里为 MySQL 5.6 版本。
   - **详细说明**：`mysql` 是镜像名称，`5.6` 是标签，表示具体版本。

### 环境变量的作用

在此命令中，环境变量 `MYSQL_ROOT_PASSWORD` 是必需的，它为 MySQL 数据库的 `root` 用户设置了初始密码。这确保了数据库实例在启动时具备必要的安全配置，防止未授权访问。

通过设置环境变量，开发者无需手动配置数据库密码，提高了部署效率，同时也增强了安全性。

## 最佳实践

在设置 Docker 环境变量时，遵循一些最佳实践可以提升应用的安全性、可维护性和灵活性。

### 环境变量的命名规范

- **使用大写字母和下划线**：例如 `DATABASE_URL`、`API_KEY`。
- **具备描述性**：变量名应能清晰描述其用途，避免使用模糊或通用的名称。
- **统一前缀**：对于相关的环境变量，可以使用统一的前缀以便于管理，例如 `MYSQL_`、`APP_`。

### 避免在命令行中暴露敏感信息

在命令行中直接传递敏感信息（如密码、API 密钥）存在被系统进程或历史记录捕获的风险。建议使用以下方法：

- **环境变量文件**：将敏感信息存储在 `.env` 文件中，并确保该文件不被版本控制系统跟踪。
- **Docker Secrets**：对于更高的安全需求，可以使用 Docker Secrets 来管理敏感数据，尤其是在 Docker Swarm 集群中。

### 使用 Docker Secrets 管理敏感数据

Docker Secrets 提供了一种安全存储和管理敏感信息的方法。通过 Docker Secrets，可以确保敏感数据在传输和存储过程中得到加密和保护。

**示例步骤：**

1. **创建一个 secret：**

   ```bash
   echo &quot;996007&quot; | docker secret create mysql_root_password -
   ```

2. **在服务中使用该 secret：**

   ```bash
   docker service create --name mysql_service \
       --secret mysql_root_password \
       -p 3306:3306 \
       mysql:5.6
   ```

3. **在容器内访问 secret：**

   Docker 会将 secret 以只读文件的形式挂载到 `/run/secrets/` 目录中，应用程序可以通过读取该文件来获取敏感信息。

## 安全性考量

在设置和管理 Docker 环境变量时，安全性是不可忽视的重要因素。以下是一些关键的安全性考量：

1. **避免在公共仓库中存储敏感信息**：确保 `.env` 文件和 Dockerfile 不包含明文的敏感数据，尤其是在公开的代码仓库中。

2. **限制容器的权限**：使用最小权限原则，确保容器仅拥有运行所需的最少权限，防止潜在的安全漏洞被利用。

3. **定期更新和审查**：定期更新 Docker 镜像，应用安全补丁，并审查环境变量配置，确保其符合最新的安全标准。

4. **使用加密存储**：对于高度敏感的数据，考虑使用加密存储或外部秘密管理服务，以增强数据保护。

## 总结

环境变量在 Docker 容器化部署中扮演着至关重要的角色。它们不仅提供了一种灵活的配置方式，还增强了应用的可移植性和安全性。通过合理设置和管理环境变量，开发者可以实现高效、可维护且安全的应用部署。

本文通过解析具体的 Docker 命令，详细阐述了在 Docker 中设置环境变量的方法及其重要性。同时，提供了最佳实践和安全性考量，帮助开发者在实际应用中更好地利用环境变量，构建健壮的容器化应用。

无论是初学者还是有经验的开发者，理解并掌握 Docker 环境变量的设置和管理，都将为高效的容器化部署打下坚实的基础。</content:encoded><author>Asuhe</author></item><item><title>TCP为什么要三次握手和四次挥手</title><link>https://asuhe.org/blog/24ccf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/24ccf9ab1/</guid><pubDate>Tue, 24 Dec 2024 16:12:16 GMT</pubDate><content:encoded>## TCP 为什么要三次握手和四次挥手

TCP（传输控制协议）使用三次握手和四次挥手来建立和关闭连接，这种设计确保了数据传输的可靠性和完整性。

### 三次握手的过程及其必要性

三次握手（Three-way Handshake）是建立 TCP 连接的过程，具体步骤如下：

```mermaid

sequenceDiagram
    participant Client
    participant Server

    %% 初始状态
    Client-&gt;&gt;Server: SYN (Seq=x)
    Note right of Server: Client 发送 SYN 包&lt;br/&gt;用于初始化连接，Seq=x

    Server--&gt;&gt;Client: SYN-ACK (Seq=y, Ack=x+1)
    Note left of Client: Server 接收到 SYN 后&lt;br/&gt;发送 SYN-ACK 响应，Seq=y，Ack=x+1

    Client-&gt;&gt;Server: ACK (Seq=x+1, Ack=y+1)
    Note right of Server: Client 确认收到 SYN-ACK&lt;br/&gt;发送 ACK，Seq=x+1，Ack=y+1

    %% 连接建立完成
    Note over Client, Server: TCP 连接建立完成

```

1. **第一次握手**：客户端发送一个带有 SYN 标志的数据包，表示请求建立连接，并随机生成一个初始序列号（ISN）。

2. **第二次握手**：服务器接收到请求后，回复一个带有 SYN 和 ACK 标志的数据包，确认客户端的请求，并生成自己的初始序列号。

3. **第三次握手**：客户端收到服务器的响应后，再发送一个 ACK 标志的数据包以确认连接的建立。

通过三次握手，双方可以确认彼此的发送和接收能力是否正常，并同步各自的初始序列号。这一过程能够有效防止因网络延迟或丢包导致的错误连接。例如，如果只进行两次握手，服务器可能会误认为连接已建立，而客户端并未发送请求，从而造成资源浪费和潜在的连接问题[1][2][4]。

### 四次挥手的过程及其必要性

四次挥手（Four-way Handshake）是关闭 TCP 连接的过程，步骤如下：

```mermaid

sequenceDiagram
    participant Client
    participant Server

    %% 初始状态
    Client-&gt;&gt;Server: FIN (Seq=x)
    Note right of Server: 客户端发送 FIN 包&lt;br/&gt;用于请求终止连接，Seq=x

    Server--&gt;&gt;Client: ACK (Ack=x+1)
    Note left of Client: 服务器确认收到 FIN&lt;br/&gt;发送 ACK，Ack=x+1

    Server-&gt;&gt;Client: FIN (Seq=y)
    Note left of Client: 服务器发送 FIN 包&lt;br/&gt;用于终止连接，Seq=y

    Client--&gt;&gt;Server: ACK (Ack=y+1)
    Note right of Server: 客户端确认收到 FIN&lt;br/&gt;发送 ACK，Ack=y+1

    %% 连接关闭
    Note over Client, Server: TCP 连接正式关闭

```

1. **第一次挥手**：客户端发送一个带有 FIN 标志的数据包，请求关闭连接。

2. **第二次挥手**：服务器接收到 FIN 后，回复一个 ACK 标志的数据包，确认接收关闭请求。

3. **第三次挥手**：服务器发送自己的 FIN 标志的数据包，请求关闭连接。

4. **第四次挥手**：客户端接收到服务器的 FIN 后，再发送一个 ACK 标志的数据包以确认。

四次挥手确保双方都能完成当前的数据传输并正确关闭连接。由于 TCP 是全双工协议，每一方都可以独立地发送和接收数据，因此需要单独处理每个方向的关闭请求。如果只进行三次挥手，可能会导致未完成的数据丢失或连接状态不一致的问题。例如，在服务器尚未完成数据传输时就关闭连接，会导致数据丢失[1][3][5]。

### 总结

TCP 协议通过三次握手和四次挥手有效地管理连接的建立与关闭。这种机制不仅保证了双方在通信中的可靠性，还确保了数据传输的完整性，是网络通信中至关重要的一部分。

## 如果只用两次握手会出现什么问题

如果只用两次握手来建立 TCP 连接，会出现以下几个主要问题：

### 1. 连接状态不一致

在两次握手中，客户端发送 SYN 请求后，服务器只需回复 ACK 确认，而不需要发送自己的 SYN。这意味着服务器无法确认客户端是否成功接收了它的 ACK。这样一来，客户端可能会在没有确认的情况下开始发送数据，从而导致连接状态的不一致。

### 2. 重复连接请求的问题

如果网络中存在延迟或丢包，服务器的 ACK 可能会被延迟到达客户端。如果客户端在超时后再次发送 SYN 请求（可能带有一个新的序列号），而服务器已经发送了 ACK，但由于网络延迟未能及时到达客户端，那么客户端将误认为之前的连接仍然有效。这将导致客户端和服务器之间的序列号不同步，造成数据混乱。

### 3. 安全性问题

使用两次握手还可能引入安全隐患。例如，攻击者可以伪装成合法用户向服务器发送 SYN 请求，并在收到 ACK 后开始发送数据。如果没有第三次握手的确认，服务器无法验证发起连接的真正身份，从而可能导致未授权的数据传输。

### 4. 无法支持双向通信

TCP 是一个全双工协议，允许双方同时发送和接收数据。两次握手只能确保一方（通常是发起方）建立连接，而另一方无法确认其是否准备好接收数据。这限制了 TCP 的功能，使得它无法实现真正的双向通信。

综上所述，三次握手的设计不仅确保了双方能够同步初始序列号，还提供了对连接建立过程的确认，从而避免了上述问题的发生。

## 为什么四次挥手不能合并成三次

四次挥手是 TCP 协议中用于安全关闭连接的标准过程，涉及到四个步骤。这一过程之所以设计为四次，而不是三次，主要是为了确保双方都能正确地释放连接并处理未发送或未接收的数据。

### 四次挥手的过程

四次挥手的步骤如下：

1. **第一次挥手**：客户端发送一个带有 FIN（结束）标志的数据包，表示它已经没有数据要发送了，请求关闭连接。

2. **第二次挥手**：服务器接收到客户端的 FIN 报文后，发送一个带有 ACK（确认）标志的数据包作为响应，确认已收到客户端的关闭请求。

3. **第三次挥手**：服务器在确认客户端的关闭请求后，如果还有未发送完的数据，会先发送这些数据，然后再发送一个带有 FIN 标志的报文，表示自己也没有数据要发送了，请求关闭连接。

4. **第四次挥手**：客户端接收到服务器的 FIN 报文后，发送一个 ACK 报文作为确认，表示已收到服务器的结束请求。此时，连接正式关闭。

### 为什么不能合并成三次？

虽然在某些特定情况下（如服务端没有数据要发送且开启了 TCP 延迟确认机制），第二和第三次挥手可以合并为一次，但从协议设计的角度来看，四次挥手是必要的。原因包括：

- **全双工通信**：TCP 是全双工的协议，这意味着数据可以在两个方向上同时传输。在关闭连接时，需要确保每一方都能独立地完成自己的数据传输任务。四次挥手确保每一方都能确认对方已经完成了数据传输。

- **防止数据丢失**：如果只进行三次挥手，可能会导致一方在未完成数据传输时就关闭了连接，这样会造成数据丢失或混淆。例如，假设服务端在接收到客户端的关闭请求后立即关闭了连接，而此时它还有未发送的数据，这些数据将无法到达客户端。

- **ACK 确认的重要性**：第四次挥手中的 ACK 确认是非常重要的，它确保了客户端已经收到服务器的 FIN 请求。如果没有这一确认，服务器无法知道客户端是否已经成功处理了所有数据，从而可能导致资源浪费和潜在的数据丢失问题。

综上所述，虽然在某些情况下可以观察到三次挥手的现象，但从协议设计和可靠性考虑来看，四次挥手是确保 TCP 连接安全、完整关闭的重要步骤。

## 在什么情况下四次挥手会变成三次

在 TCP 协议中，正常情况下关闭连接的过程是通过**四次挥手**来完成的。然而，在某些特定情况下，这一过程可以简化为**三次挥手**。以下是详细的解释和条件。

### 四次挥手的标准流程

四次挥手的过程如下：

1. **第一次挥手**：客户端发送一个 FIN 报文，表示它不再发送数据，进入 FIN_WAIT_1 状态。

2. **第二次挥手**：服务端收到 FIN 报文后，发送一个 ACK 报文，确认收到客户端的关闭请求，并进入 CLOSE_WAIT 状态。

3. **第三次挥手**：服务端准备好关闭连接后，发送自己的 FIN 报文，表示它也不再发送数据。

4. **第四次挥手**：客户端收到服务端的 FIN 报文后，发送 ACK 报文确认，从而完成连接的关闭。

### 三次挥手的情况

在某些情况下，四次挥手可以变为三次挥手。这种情况通常发生在以下条件下：

- **被动关闭方没有数据要发送**：如果服务端在收到客户端的 FIN 报文时，没有任何待发送的数据，并且已经准备好关闭连接，那么它可以将第二次和第三次挥手合并为一次。也就是说，服务端在确认收到 FIN 报文（第二次挥手）的同时，可以直接发送自己的 FIN 报文（第三次挥手），这样就只需要三次交互。

- **TCP 延迟确认机制开启**：当 TCP 延迟确认机制处于开启状态时，服务端可能会将 ACK 和 FIN 合并在同一个报文中发送。这种机制旨在提高网络效率，因为单独发送一个 ACK 报文会浪费带宽。

### 具体示例

假设客户端首先发送了 FIN 报文。服务端收到后，如果此时没有数据需要发送，它就可以直接回复一个包含 ACK 和 FIN 的报文，这样就省去了单独发送 ACK 和 FIN 的步骤，从而实现了三次挥手。

### 总结

因此，在 TCP 连接关闭过程中，如果满足“被动关闭方没有数据要发送”且“开启了 TCP 延迟确认机制”这两个条件，那么四次挥手可以简化为三次。这种情况虽然不常见，但确实存在，并且是 TCP 协议灵活性的一部分。

## 如果网络延迟会对三次握手和四次挥手产生什么影响

网络延迟对 TCP 的三次握手和四次挥手过程有显著影响，主要体现在连接的建立和断开效率以及可靠性上。

### 三次握手的影响

在 TCP 协议中，三次握手的主要目的是建立一个可靠的连接，确保双方能够正常发送和接收数据。其过程如下：

- **第一次握手**：客户端发送一个 SYN 包以请求建立连接。
- **第二次握手**：服务器响应一个 SYN-ACK 包，确认接收到客户端的请求。
- **第三次握手**：客户端发送 ACK 包以确认收到服务器的响应。

#### 网络延迟的影响

1. **延迟增加连接建立时间**：如果网络延迟较高，三次握手所需的时间会增加，导致连接建立变慢。例如，每个包的往返时间（RTT）会直接影响到建立连接所需的总时间。

2. **重传机制**：在高延迟环境中，如果某个包丢失或超时未被确认，可能导致客户端或服务器重传 SYN 或 ACK 包，这会进一步增加连接建立所需的时间。

3. **历史连接问题**：网络延迟可能导致旧的 SYN 包在新的连接请求中被误认为是有效请求，这种情况下，服务器可能会错误地尝试建立连接，从而浪费资源。三次握手通过确认双方的接收能力来防止这种情况发生，但如果延迟过大，可能导致不必要的重传和资源占用。

### 四次挥手的影响

四次挥手用于安全地关闭 TCP 连接，其过程包括：

- **第一次挥手**：一方（如客户端）发送 FIN 包，请求关闭连接。
- **第二次挥手**：另一方（如服务器）确认收到 FIN 包，并发送 ACK。
- **第三次挥手**：服务器发送自己的 FIN 包，请求关闭连接。
- **第四次挥手**：客户端确认收到服务器的 FIN 包，并发送 ACK。

#### 网络延迟的影响

1. **延迟增加关闭时间**：类似于三次握手，高延迟会使得四次挥手所需的时间增加，从而导致连接关闭变慢。

2. **未完成数据传输的问题**：在四次挥手过程中，如果一方在发送 FIN 之前还有未发送的数据，网络延迟可能导致这些数据未能及时到达另一方。如果 FIN 包在未完成数据传输后到达，另一方可能会误认为可以立即关闭连接，从而导致数据丢失。

3. **TCP 延迟确认机制**：在某些情况下，例如当被动关闭方没有数据要发送时，网络延迟可能使得第二和第三次挥手合并为一次，这样就只需三次挥手即可完成关闭。这种情况通常发生在启用了 TCP 延迟确认机制时，这样可以减少不必要的数据包传输，但也可能导致不一致性。

综上所述，网络延迟对 TCP 协议中的三次握手和四次挥手过程有着重要影响，它不仅影响了连接建立和断开的效率，还可能引发数据丢失和资源浪费的问题。因此，在设计网络应用时，需要考虑这些因素，以优化性能和可靠性。

## 三次握手和四次挥手在高延迟网络中的性能差异

在高延迟网络环境中，TCP 的三次握手和四次挥手机制表现出不同的性能特征，这主要体现在连接建立和关闭的效率上。

### 三次握手

**建立连接的过程**

TCP 使用三次握手（Three-way Handshake）来建立连接，具体步骤如下：

1. **第一次握手**：客户端发送一个 SYN 报文，表示请求建立连接，并附带一个初始序列号。

2. **第二次握手**：服务器收到 SYN 后，回复一个 SYN-ACK 报文，确认收到客户端的请求，并发送自己的初始序列号。

3. **第三次握手**：客户端收到 SYN-ACK 后，发送 ACK 报文以确认连接建立。

在高延迟网络中，三次握手可能导致显著的延迟。由于每一步都需要一个往返时间（RTT），在网络延迟较高时，建立连接的总时间可能会增加。例如，如果 RTT 为 100ms，则三次握手的延迟可能达到 300ms（即 3 个 RTT）。此外，在丢包或网络不稳定的情况下，重传机制可能导致进一步的延迟和性能下降。

## 四次挥手

**关闭连接的过程**

TCP 使用四次挥手（Four-way Handshake）来安全地关闭连接，其步骤如下：

1. **第一次挥手**：一方（如客户端）发送 FIN 报文，表示不再发送数据。

2. **第二次挥手**：另一方（服务器）回复 ACK 报文，确认已收到 FIN。

3. **第三次挥手**：服务器发送 FIN 报文，表示也不再发送数据。

4. **第四次挥手**：客户端回复 ACK 报文，确认关闭请求。

在高延迟网络中，四次挥手同样会受到影响，因为每个步骤都需要等待对方的响应。虽然关闭连接所需的时间是四个 RTT，但由于 TCP 设计允许一方在接收到 FIN 后继续发送未完成的数据，这种机制有助于确保数据完整性并减少数据丢失风险。因此，在高延迟环境中，虽然四次挥手可能看似增加了延迟，但它实际上提供了更可靠的数据传输和资源管理。

### 性能差异总结

| 特性           | 三次握手             | 四次挥手                         |
| -------------- | -------------------- | -------------------------------- |
| 目的           | 建立可靠连接         | 安全地关闭连接                   |
| 延迟影响       | 高延迟环境下显著增加 | 每一步均需等待响应，整体延迟增加 |
| 数据完整性保障 | 无法保证             | 确保所有数据传输完成             |
| 资源管理       | 可能导致资源浪费     | 有效释放双方资源                 |

在高延迟网络中，三次握手和四次挥手都面临挑战，但四次挥手通过其设计能够更好地处理数据完整性和资源管理问题，而三次握手则可能因重传和延迟而引发性能瓶颈。因此，在设计网络应用时，应考虑这些因素以优化性能。</content:encoded><author>Asuhe</author></item><item><title>为什么有些第三方客户端可以直连pixiv？</title><link>https://asuhe.org/blog/a0221f19/</link><guid isPermaLink="true">https://asuhe.org/blog/a0221f19/</guid><pubDate>Sat, 19 Aug 2023 23:14:31 GMT</pubDate><content:encoded>##  引言

众所周知P站主站是被屏蔽的，需要科学上网才能正常访问。前阵子发现有些P站第三方客户端可以做到直连P站，便心血来潮想要研究一下他们是如何实现的。同时也做一个可以直连P站的图片下载器。

##  知己知彼

常言道知己知彼才能百战不殆。想要知道P站直连的原理就要先知道P站是如何被拦截的。通过查阅网上资料知道P站是被`DNS污染` + `SNI阻断`的方式被墙。接下来就可以从这两个方面入手。

##  DNS污染

所谓`DNS污染`就是污染你的DNS让你得到的DNS解析是一个错误的结果，令你最终的请求发送到一个不正确的IP以达到拦截的目的。当你想要请求某个网站时，通常会经过一下流程：

```mermaid
sequenceDiagram
    your PC-&gt;&gt;your PC: Request local DNS cache. Not found, request DNS server.
    your PC-&gt;&gt;DNS server: Queries: What is the IP address of www.pixiv.net?
    DNS server--&gt;&gt;your PC: Answers: The IP address of www.pixiv.net is 162.125.32.5!
    your PC -&gt;&gt;pixiv server: Use this IP address to request data.
    pixiv server--&gt;&gt;your PC: Response data
```

抓包实例：

![DNS](https://img.outsider404.com/blogImg%2FDNS%E8%BF%87%E7%A8%8B.png)

`DNS污染`就是在DNS服务器应答前，经过的中间路由检测到你发送的DNS请求是在GFW黑名单里的网站，中间路由器抢先给你返回一个DNS应答给你错误的IP地址。由于DNS协议设计之初没有身份鉴别机制且底层通常使用UDP协议是无连接的，它采用的策略是优先接收第一个DNS响应报文，后续到达的DNS响应报文将被丢弃，当上面的情况发生时，实际DNS服务器的报文会在后面到达，而它携带正确信息的DNS响应报文将会被你的主机丢弃。

所以真实网络环境下的请求是下面的的流程，而不是上面的简化版。

```mermaid
sequenceDiagram
    your PC -&gt;&gt; your PC: Request local DNS cache. Not found, request DNS server.
    your PC -&gt;&gt; Intermediate Router: Queries: What is the IP address of www.pixiv.net?
    Intermediate Router -&gt;&gt; DNS server: Queries: What is the IP address of www.pixiv.net?
    Intermediate Router --&gt;&gt; your PC: Answers: The IP address of www.pixiv.net is 100.100.100.1!
    DNS server --&gt;&gt; Intermediate Router:  Answers: The IP address of www.pixiv.net is 162.125.32.5!
    Intermediate Router --&gt;&gt; your PC:  Answers: The IP address of www.pixiv.net is 162.125.32.5!
    your PC -&gt;&gt; your PC: accept 100.100.100.1, drop 162.125.32.5 
    your PC -&gt;&gt; Intermediate Router:Use 100.100.100.1 to request data.
    Intermediate Router -&gt;&gt; 100.100.100.1: Use 100.100.100.1 to request data.
```

明白了上面这个流程，就可以针对性地去解决了。

解决DNS污染问题可以采用下面几个方法：

- 使用HTTPS。启用HTTPS后，浏览器与网站之间的通信是加密的，中间人无法读取和修改。这可以有效抵御DNS污染。
- 使用DNS加密协议。如DNS over HTTPS、DNS over TLS等协议，可以加密DNS查询，避免DNS查询结果被劫持或污染。
- 使用可信任的DNS服务器。选择知名的可信任DNS服务商,如Google DNS、OpenDNS等,而不使用可能被污染的本地ISP DNS。
- 启用DNSSEC。DNSSEC通过数字签名防止DNS响应被修改,可以验证DNS数据的完整性和真实性。
- 本地Hosts文件。在本地Hosts文件指定域名与IP地址映射,来绕过DNS查询。
- 智能选择DNS服务器。智能DNS系统可以检测异常情况,自动切换到其他正常DNS服务器。
- 使用反向代理服务。通过中间代理服务来进行访问,避免直接暴露在污染的网络环境中。



## SNI阻断

### 什么是SNI

`SNI`全称是`Server Name Indication`，中文可以翻译为“服务器名称指示”。它是SSL/TLS协议的一个扩展，允许在建立安全连接时,客户端告诉服务器它正在访问的是哪个 hostname。

以前在一个IP地址上只能部署一个SSL证书，这样就无法在一台服务器上托管多个需要SSL证书的网站。

SNI的出现改变了这个限制。客户端在连接服务器时发送想要访问的hostname，这样服务器就可以根据不同的hostname选择正确的SSL证书来建立安全连接。

SNI扩展于2003年被提出，2005年正式成为SSL标准的一部分。SNI是在TLS 1.0版本中提出的扩展，但直到TLS 1.2版本才被正式采纳为标准。主流浏览器对SNI的支持也是从2010年开始。

TLS 1.3版本对SNI做了优化，不再允许客户端`Anonymous模式`，必须发送SNI来建立连接。所以综上，SNI主要是在TLS 1.2/1.3版本中使用，一般来说如果不发送SNI，服务器会根据配置返回默认证书。

在TLS 1.3版本中,客户端必须发送SNI主要有以下两个原因:

1. 提高安全性

早期的TLS版本允许客户端匿名连接服务器,这带来了一些安全隐患。不发送SNI,服务器无法验证客户端请求的域名,存在被攻击的风险。TLS 1.3去掉了这种客户端匿名模式,要求必须发送SNI,以提高安全性。服务器可以验证连接请求是否对应合法域名。

2. 优化性能

TLS 1.3改进了握手流程,服务器在收到SNI后,可以立即准备对应的证书,从而省去了额外的往返延迟。不发送SNI,服务器只能使用默认证书,需要重新协商一遍才能切换到正确的证书,降低了性能。

综合考虑安全性和性能,TLS 1.3做出了需要强制SNI的设计。这尽量减少了匿名连接可能带来的风险,也优化了连接流程的效率。这项变化让SNI成为建立安全TLS连接的必备组件之一。

### 具体细节

SNI作为TLS扩展出现在`ClientHello`消息中。`ClientHello`是TLS握手过程的第一步,客户端首先发送这条消息给服务器。

1. 在`ClientHello消息`中,客户端可以发送一个`Server Name Indication extension`(类型是0号扩展)。这个扩展包含了客户端请求访问的主机名。 `Server Name`是可选的，如果客户端请求的是IP直接访问,就不需要带主机名。
2. 当服务器收到`ClientHello`时，会检查是否有SNI扩展，如果有就取出`Server Name`。服务器会根据这个`Server Name`选择合适的证书去回复客户端。如果没有接收到SNI扩展,通常会返回默认证书。
3. 之后就是标准的TLS握手流程，协商加密算法、交换证书等,来建立安全连接。如果握手成功,客户端就可以通过这个安全通道与服务器通信了。

其中第二步时，服务器会做出何种反应完全是由服务器配置规则决定的，通常都会采用以下几种策略：

- 返回一个通配符证书，适用于服务器上的所有域名。
- 返回一个不存在的域名的证书，使客户端出现证书错误,强制要求使用SNI。
- 返回一个第三方通用证书，如Let&apos;s Encrypt的DST Root CA X3证书。
- 甚至可以返回一个自签名证书作为默认证书。

```mermaid
sequenceDiagram
    Client -&gt;&gt; Server: Client Hello
    Server --&gt;&gt; Client: Server Hello
    Server --&gt;&gt; Client: Certificate
    Server --&gt;&gt; Client: Server Key Exchange
    Client -&gt;&gt; Server: Client Key Exchange
    Server --&gt;&gt; Client: New Session Ticket
```

我们可以抓包看看实际p站点的TLS流程，因为我这里没有清除电脑上原有的证书所以没有证书返回和密钥交换的过程。

![SNI发送](https://img.outsider404.com/blogImg%2FSNI2.png)

### 绕过思路

基于以上抓包信息，我们可以得到一个直接思路就是删除SNI扩展即直接不发送SNI，让服务器返回默认的证书供我们使用。恰巧P站返回的默认证书是`*.pixiv.net`证书，一个泛解析的证书，这样我们就可以直接利用啦！

## 第三方客户端是怎么做的

以[PixEz Flutter](https://github.com/Notsfsssf/pixez-flutter/tree/master)这个客户端为例，它是支持直连P站的。而且他们的代码是开源的，我们可以随意查看。通过翻[他们的代码](https://github.com/Notsfsssf/pixez-flutter/blob/master/lib/er/hoster.dart)和问群里的大佬得知。

针对DNS污染问题，他们是用修改本地host的思路，用一些技术手段获取到了p站一个服务器的真实IP，然后直接向这个地址发请求从而绕过了DNS污染。而对于SNI阻断他们采用的方案是直接在请求过程中删除SNI，刚刚好P站采用的是TLS 1.2版本，这样就实现了直连P站。</content:encoded><author>Asuhe</author></item><item><title>检测元素进入视口的几种方式</title><link>https://asuhe.org/blog/94ccf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/94ccf9ab1/</guid><pubDate>Sun, 15 Dec 2024 16:12:16 GMT</pubDate><content:encoded>在前端开发中,我们经常需要检测某个元素是否进入了浏览器的可视区域(视口)。本文将介绍几种浏览器原生提供的检测方法。

## Intersection Observer API

这是最现代和推荐的检测方式:

```js
const options = {
  root: null, // 用作视口的元素,默认为浏览器视口
  rootMargin: &apos;0px&apos;, // 视口的扩展或缩小范围
  threshold: 0.5 // 目标元素可见比例阈值
};

const observer = new IntersectionObserver((entries) =&gt; {
  entries.forEach(entry =&gt; {
    if (entry.isIntersecting) {
      console.log(&apos;元素进入视口&apos;);
    }
  });
}, options);

// 开始观察目标元素
observer.observe(targetElement);
```

**优点**:

- 性能好,不会造成主线程阻塞
- 可以异步检测
- 支持设置触发阈值
- 可以同时观察多个元素

**缺点**:

- 旧版浏览器可能需要 polyfill

## 通过获取元素的位置信息来判断

### getBoundingClientRect()

通过 `getBoundingClientRect()` 方法可以获取元素的边界信息,包括元素的 top, left, bottom, right 等属性,来计算元素是否在视口内:

```js
function isInViewport(element) {
  const rect = element.getBoundingClientRect();
  return (
    rect.top &gt;= 0 &amp;&amp;
    rect.left &gt;= 0 &amp;&amp;
    rect.bottom &lt;= window.innerHeight &amp;&amp;
    rect.right &lt;= window.innerWidth
  );
}
```

### elementFromPoint()

通过 `elementFromPoint()` 方法可以获取指定坐标点的元素:

```js
function isElementVisible(element) {
  const rect = element.getBoundingClientRect();
  const elementAtPoint = document.elementFromPoint(
    rect.left + rect.width/2,
    rect.top + rect.height/2
  );
  return element.contains(elementAtPoint);
}
```

### getClientRects()

`getClientRects()` 方法返回一个包含元素所有 CSS 边框的矩形集合,通过计算可以用来判断元素是否在视口内:

```js
function isElementInView(element) {
  const clientRects = element.getClientRects();
  if (!clientRects.length) return false;

  return Array.from(clientRects).some(rect =&gt;
    rect.top &gt;= 0 &amp;&amp;
    rect.bottom &lt;= window.innerHeight
  );
}
```

**优点**:

- 实现简单直观
- 浏览器兼容性好

**缺点**:

- 需要手动监听滚动事件
- 频繁调用可能影响性能

## 视口检测原理示意图

```mermaid
graph TB
    subgraph 视口区域
        A[可视区域]
    end

    subgraph 滚动区域
        B[元素未进入视口]
        C[元素部分进入]
        D[元素完全进入]
    end

    B --&gt;|向上滚动| C
    C --&gt;|继续滚动| D

    style A fill:#e1f5fe
    style B fill:#fff3e0
    style C fill:#fff3e0
    style D fill:#fff3e0
```

## 实际应用示例

### 懒加载图片

```js
const observer = new IntersectionObserver((entries) =&gt; {
  entries.forEach(entry =&gt; {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

// 观察所有懒加载图片
document.querySelectorAll(&apos;img[data-src]&apos;).forEach(img =&gt; observer.observe(img));
```

### 无限滚动

```js
const observer = new IntersectionObserver((entries) =&gt; {
  if (entries[0].isIntersecting) {
    loadMoreContent();
  }
});

observer.observe(document.querySelector(&apos;#loadMore&apos;));
```

## 选择建议

1. 优先使用 Intersection Observer API
   - 性能最好
   - API 设计合理
   - 使用方便
2. 降级使用 getBoundingClientRect()
   - 需要考虑性能优化
   - 建议使用节流/防抖
3. 特殊场景使用 scrollIntoView()
   - 主要用于滚动控制
   - 可配合其他方法使用

## 结论

在现代前端开发中,Intersection Observer API 是检测元素进入视口的最佳选择。它提供了优秀的性能和便捷的 API。对于需要兼容旧版浏览器的项目,可以使用 getBoundingClientRect() 作为备选方案。</content:encoded><author>Asuhe</author></item><item><title>Promise为什么没有取消?</title><link>https://asuhe.org/blog/27ccf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/27ccf9ab1/</guid><pubDate>Thu, 02 Jan 2025 16:12:16 GMT</pubDate><content:encoded># 为什么 JavaScript 的 Promise 没有取消机制？

在现代前端开发中，JavaScript 的 Promise 已经成为处理异步操作的核心工具。然而，在日常开发中我们会注意到，Promise 本身并不提供内建的取消机制。这引发了一个重要的问题：**为什么 Promise 没有取消功能？**

## 1. Promise 的设计初衷是什么？

### 简化异步编程

在 Promise 出现之前，JavaScript 主要依赖回调函数处理异步操作，如 AJAX 请求、定时器等。然而，回调函数容易导致“回调地狱”，代码难以维护和调试。Promise 的出现旨在通过链式调用和错误捕捉机制，简化异步编程，提高代码的可读性和可维护性。

### 明确异步操作的状态

Promise 引入了三个状态——待定（Pending）、已兑现（Fulfilled）和已拒绝（Rejected），使得异步操作的状态更加明确。这种状态管理机制有助于开发者更好地理解和控制异步流程。

### 不涉及取消的设计考量

在设计 Promise 时，初衷是处理异步操作的结果和错误，而非控制异步操作的生命周期。取消操作涉及到对异步任务的中断和资源的释放，这在设计初期并未被纳入 Promise 的核心功能范畴。因此，Promise 没有内建取消机制。

## 2. 在哪些场景下可能需要取消 Promise？

尽管 Promise 本身不支持取消，但在实际开发中，有许多场景需要对异步操作进行取消，以提高应用的性能和用户体验。以下是一些常见的场景：

### 用户取消操作

例如，用户在发起搜索请求后，可能会快速输入新的查询条件，导致之前的搜索请求变得无效。此时，取消之前的请求可以节省资源并避免不必要的数据处理。

### 组件卸载时清理异步任务

在单页应用（SPA）中，当组件卸载时，仍在进行的异步操作可能会导致内存泄漏或尝试更新已卸载的组件。取消这些异步任务可以防止潜在的问题。

### 限制资源使用

在一些高频率的异步操作中，取消不必要的任务有助于限制资源使用，提升应用的整体性能。

## 3. 如何实现取消效果，有哪些解决方案？

尽管 Promise 本身不支持取消，但开发者可以通过多种方法实现类似的效果。以下是几种常见的解决方案：

### 1. 使用标志变量

通过引入一个标志变量，控制异步操作的执行逻辑。当需要取消时，修改标志变量的值，阻止后续操作。

```javascript
let isCancelled = false;

const promise = new Promise((resolve, reject) =&gt; {
  fetch(&apos;https://api.example.com/data&apos;)
    .then(response =&gt; {
      if (isCancelled) {
        // 取消操作
        return;
      }
      return response.json();
    })
    .then(data =&gt; {
      if (!isCancelled) {
        resolve(data);
      }
    })
    .catch(error =&gt; {
      if (!isCancelled) {
        reject(error);
      }
    });
});

// 取消操作
isCancelled = true;
```

### 2. 使用 AbortController

`AbortController` 是 ES6 引入的一个标准接口，专门用于控制和中断可中止的操作，如 `fetch` 请求。通过结合 `AbortController` 和 Promise，可以实现取消异步操作的效果。

```javascript
const controller = new AbortController();
const signal = controller.signal;

const fetchPromise = fetch(&apos;https://api.example.com/data&apos;, { signal })
  .then(response =&gt; response.json());

// 取消请求
controller.abort();

fetchPromise.catch(error =&gt; {
  if (error.name === &apos;AbortError&apos;) {
    console.log(&apos;Fetch aborted&apos;);
  } else {
    console.error(&apos;Fetch error:&apos;, error);
  }
});
```

### 3. 使用第三方库

一些第三方库为 Promise 提供了取消功能。例如，Bluebird 库扩展了 Promise，添加了取消方法。

```javascript
const Promise = require(&apos;bluebird&apos;);

const promise = new Promise((resolve, reject, onCancel) =&gt; {
  const timeout = setTimeout(() =&gt; {
    resolve(&apos;Done&apos;);
  }, 5000);

  onCancel(() =&gt; {
    clearTimeout(timeout);
    console.log(&apos;Promise cancelled&apos;);
  });
});

promise.then(result =&gt; {
  console.log(result);
});

// 取消 Promise
promise.cancel();
```

### 4. 使用 Async/Await 与取消

在使用 `async/await` 语法时，可以结合 `AbortController` 或其他取消机制，实现更简洁的取消逻辑。

```javascript
const controller = new AbortController();
const signal = controller.signal;

async function fetchData() {
  try {
    const response = await fetch(&apos;https://api.example.com/data&apos;, { signal });
    const data = await response.json();
    console.log(data);
  } catch (error) {
    if (error.name === &apos;AbortError&apos;) {
      console.log(&apos;Fetch aborted&apos;);
    } else {
      console.error(&apos;Fetch error:&apos;, error);
    }
  }
}

fetchData();

// 取消请求
controller.abort();
```

## 4. 结论

JavaScript 的 Promise 在设计时主要关注于简化异步编程和管理异步操作的状态，而取消机制并未成为其核心功能的一部分。然而，在实际开发中，取消异步操作的需求是普遍存在的。开发者可以通过多种方法，如使用 `AbortController`、标志变量或第三方库，实现 Promise 的取消效果。随着 JavaScript 生态的不断发展，未来可能会有更多原生或标准化的解决方案，进一步完善异步操作的控制机制。</content:encoded><author>Asuhe</author></item><item><title>为什么let、const要有暂时性死区，它的设计初衷是什么？</title><link>https://asuhe.org/blog/28ccf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/28ccf9ab1/</guid><pubDate>Sun, 05 Jan 2025 16:12:16 GMT</pubDate><content:encoded># 为什么 let、const 要有暂时性死区，它的设计初衷？

在 JavaScript 中，`let`和`const`引入了一个重要的概念——**暂时性死区**（Temporal Dead Zone, TDZ）。这个概念的设计初衷主要是为了增强代码的安全性和可预测性，避免在变量声明之前使用这些变量所导致的错误。

## 暂时性死区的定义

暂时性死区指的是在执行代码时，变量被创建但尚未初始化的状态。在这个状态下，如果尝试访问这些变量，将会抛出 `ReferenceError`。具体来说，当进入一个块级作用域（如函数、条件语句或循环）时，所有用 `let` 或 `const` 声明的变量会在作用域内被创建，但只有在执行到变量声明语句时，变量才会被初始化并可以访问。

### 例子

考虑以下代码：

```javascript
console.log(a); // ReferenceError: Cannot access &apos;a&apos; before initialization
let a = 10;
```

在这段代码中，尽管变量 `a` 在块级作用域内被声明了，但在其声明之前访问它会导致错误。这与使用 `var` 声明的变量不同，后者会返回 `undefined`：

```javascript
console.log(b); // undefined
var b = 20;
```

## 设计初衷

1. **减少运行时错误**：通过引入 TDZ，JavaScript 能够防止开发者在变量声明之前使用它们，从而减少因未初始化或错误使用变量而导致的运行时错误。

2. **增强可读性和可维护性**：TDZ 使得代码更加清晰，因为开发者可以明确知道何时可以使用某个变量。这种机制促使开发者养成良好的编程习惯，即总是在声明之后使用变量。

3. **支持块级作用域**：`let`和`const`引入了块级作用域，这使得 JavaScript 的作用域管理更加灵活。TDZ 确保了在块级作用域内，只有在变量声明后才能访问该变量，从而避免了全局或函数作用域中可能出现的命名冲突和意外覆盖。

4. **提升代码安全性**：由于 TDZ 会抛出错误，而不是返回 `undefined`，这使得潜在的问题更容易被发现。开发者需要显式地处理这些变量，从而提高了代码的安全性。

## 小结

暂时性死区是 ES6 中引入的一项重要特性，其设计目的是为了增强 JavaScript 语言的严谨性和可维护性。通过确保在变量声明之前无法访问这些变量，TDZ 有效地减少了运行时错误，并促进了更好的编程实践。

## TDZ 在实际开发中会出现哪些问题

在实际开发中，**暂时性死区**（TDZ）可能会导致多种问题，特别是在使用 `let` 和 `const` 声明变量时。以下是一些常见的 TDZ 相关问题及其影响：

### 常见问题

1. **引用错误（ReferenceError）**：

   - 这是最常见的问题。当开发者尝试在变量被声明和初始化之前访问它时，JavaScript 会抛出 `ReferenceError`。例如：
     ```javascript
     console.log(a); // 抛出错误: ReferenceError: Cannot access &apos;a&apos; before initialization
     let a = 5;
     ```
   - 这种错误通常在代码重构或修改变量声明时发生，尤其是从 `var` 转换为 `let` 或 `const` 时。

2. **作用域管理混乱**：

   - 在复杂函数或条件语句中，开发者可能误解 `let` 和 `const` 的作用域，导致难以追踪的错误。例如，在一个块内声明的变量在块外是不可访问的：
     ```javascript
     function example() {
         if (true) {
             let x = &quot;hello&quot;;
         }
         console.log(x); // 抛出错误: ReferenceError: x is not defined
     }
     ```

3. **重构引入的新错误**：

   - 在将变量声明从 `var` 更改为 `let` 或 `const` 的过程中，开发者可能会无意中引入新的错误。例如，在循环中使用 `let` 会导致与使用 `var` 时不同的行为：
     ```javascript
     for (var i = 0; i &lt; 5; i++) {
         console.log(i); // 输出: 0, 1, 2, 3, 4
     }
     for (let j = 0; j &lt; 5; j++) {
         console.log(j); // 抛出错误: ReferenceError: j is not defined
     }
     ```

4. **默认参数中的 TDZ**：
   - 在函数的默认参数中，如果一个参数依赖于另一个尚未声明的参数，也会导致 TDZ 问题：
     ```javascript
     function test(a = b, b) {
         console.log(a); // 抛出错误: ReferenceError: Cannot access &apos;b&apos; before initialization
     }
     test(undefined, 1);
     ```

### 避免 TDZ 问题的最佳实践

- **提前声明变量**：确保在使用变量之前进行声明和初始化，以避免进入 TDZ。
- **了解作用域规则**：熟悉 `let`、`const` 和 `var` 的作用域规则，以便正确使用它们。

- **使用代码检查工具**：利用 ESLint 等工具来检测和防止在声明之前访问变量，从而减少 TDZ 相关的问题。

- **进行充分测试**：使用调试工具和测试框架确保代码在存在 TDZ 时表现如预期。

通过理解和管理 TDZ，开发者可以提高 JavaScript 代码的可靠性和可维护性，从而减少潜在的运行时错误。</content:encoded><author>Asuhe</author></item><item><title>暂时性死区和变量提升是什么关系？</title><link>https://asuhe.org/blog/29ccf9ab1/</link><guid isPermaLink="true">https://asuhe.org/blog/29ccf9ab1/</guid><pubDate>Mon, 06 Jan 2025 16:12:16 GMT</pubDate><content:encoded>在 JavaScript 中，**暂时性死区**（Temporal Dead Zone, TDZ）和**变量提升**（Hoisting）是两个关键概念，它们在处理变量声明和作用域时具有不同的行为和影响。

## 变量提升

**变量提升**是 JavaScript 的一种机制，在执行代码之前，变量和函数的声明会被移动到其所在作用域的顶部。这意味着你可以在声明之前使用这些变量。例如：

```javascript
console.log(x); // 输出: undefined
var x = 5;
console.log(x); // 输出: 5
```

在这个例子中，虽然 `x` 的赋值在第二行，但由于变量提升的机制，`x` 的声明被提升到顶部，因此第一次 `console.log(x)` 返回 `undefined` 而不是抛出错误。

### 关键点

- 只有声明被提升，而赋值不被提升。
- 使用 `var` 声明的变量会初始化为 `undefined`。

## 暂时性死区（TDZ）

**暂时性死区**是指在块级作用域内，变量存在但不可访问的状态。对于使用 `let` 和 `const` 声明的变量，在它们被初始化之前，任何访问都会导致 `ReferenceError`。例如：

```javascript
console.log(y); // 抛出错误: ReferenceError: Cannot access &apos;y&apos; before initialization
let y = 10;
```

在这个例子中，尝试在 `y` 被声明之前访问它会抛出错误，因为它处于暂时性死区。

### 关键点

- TDZ 从块级作用域的开始到变量被初始化之间的时间段。
- 在 TDZ 中，无法访问使用 `let` 和 `const` 声明的变量。
- TDZ 确保开发者只能在变量声明后使用它们，从而提高代码的可预测性和安全性。

## TDZ 与变量提升的区别

| 特性     | 变量提升（Hoisting）                      | 暂时性死区（TDZ）                              |
| -------- | ----------------------------------------- | ---------------------------------------------- |
| 定义     | 声明被移动到作用域顶部，允许提前使用。    | 在初始化之前，变量存在但不可访问。             |
| 行为     | 使用 `var` 声明的变量会返回 `undefined`。 | 使用 `let` 和 `const` 声明的变量会抛出错误。   |
| 适用情况 | 适用于所有类型的声明（包括函数）。        | 仅适用于块级作用域中的 `let` 和 `const` 声明。 |
| 初始化   | 声明被提升，但赋值不被提升。              | 在进入作用域后直到初始化前，处于不可访问状态。 |

## 小结

理解 **TDZ** 和 **变量提升** 是编写可靠和可维护 JavaScript 代码的重要基础。变量提升允许在声明之前使用某些类型的变量，而暂时性死区则确保了对未初始化变量的严格控制，从而减少了潜在错误并促进了更好的编码实践。在现代 JavaScript 开发中，推荐尽量使用 `let` 和 `const` 来避免因提升带来的意外行为。

## 为什么变量提升只有声明被提升，而赋值不被提升？

上面提到只有声明被提升，而赋值不被提升，那么为什么呢？

在 JavaScript 中，**变量提升**（Hoisting）是一个重要的概念，它指的是变量和函数的声明在代码执行之前被提升到其作用域的顶部。然而，只有变量的声明被提升，而赋值不被提升，主要是为了代码的逻辑清晰性、一致性、可预测性。

### 变量提升机制

1. **解析阶段与执行阶段**：

   - JavaScript 引擎在执行代码之前，会经历一个解析阶段。在这个阶段，所有的变量和函数声明都会被识别并存储在执行上下文中。
   - 在解析阶段，变量会被初始化为 `undefined`，而函数声明则会被完整地提升（包括其赋值）。

2. **声明与赋值的区别**：

   - 在解析阶段，`var` 声明的变量会被添加到当前作用域的词法环境中，但赋值操作只会在执行阶段进行。这意味着即使在代码中先使用了变量，JavaScript 也会将其视为已声明，但未赋值。
   - 例如：
     ```javascript
     console.log(x); // 输出: undefined
     var x = 5;
     console.log(x); // 输出: 5
     ```
     在这个例子中，`var x` 的声明被提升，而 `x = 5` 的赋值则是在执行时进行。

3. **提高性能与容错性**：
   - 这种设计允许 JavaScript 在运行时更高效，因为它只需在解析阶段处理一次变量和函数，而不是每次执行时都重新解析。
   - 此外，它也提高了代码的容错性。开发者可以在变量声明之前使用它们，而不会导致运行时错误。例如：
     ```javascript
     a = 1; // 不会报错
     var a; // 声明提升
     console.log(a); // 输出: 1
     ```

### 为什么赋值不被提升？

- **逻辑清晰性**：如果赋值也被提升，这将导致代码逻辑变得更加混乱。例如，如果你尝试访问一个未赋值的变量，它可能会返回意想不到的结果，而不是 `undefined`。
- **避免潜在错误**：如果赋值操作被提升，开发者可能会误以为某个变量在某个时间点已经有了有效值，从而导致难以追踪的错误。通过只提升声明而不提升赋值，可以强制开发者在使用变量之前先进行明确的初始化。

- **一致性与可预测性**：这种设计确保了无论何时访问变量，都会得到一致且可预测的结果。只有在实际执行到赋值语句时，变量才会获得新的值。

### 总结

JavaScript 中的变量提升机制通过只提升声明而不提升赋值，确保了代码的逻辑清晰性和可预测性。这种设计不仅提高了性能，还增强了容错性，使得开发者能够更灵活地编写代码，同时减少了潜在错误的风险。理解这一机制对于编写高质量 JavaScript 代码至关重要。</content:encoded><author>Asuhe</author></item><item><title>HTML页面中script标签的错误处理机制分析</title><link>https://asuhe.org/blog/19ccf9ab4/</link><guid isPermaLink="true">https://asuhe.org/blog/19ccf9ab4/</guid><pubDate>Thu, 27 Feb 2025 16:12:16 GMT</pubDate><content:encoded># 一个 html 页面有两个 script 标签 A 和 B。A 里面的代码报错了，会不会阻塞 B 里面的代码执行？

现代 Web 开发中，JavaScript 的执行机制与页面渲染流程的交互关系始终是前端工程师需要深入理解的核心知识点。本文针对 HTML 文档中多个`&lt;script&gt;`标签间的错误传播机制进行系统性分析，结合浏览器渲染引擎的工作原理和 ECMAScript 规范要求，全面阐释脚本块之间的执行隔离机制及其对页面性能的影响。通过实验验证和理论推演，本文得出以下核心结论：**同一 HTML 文档中的不同`&lt;script&gt;`标签具有执行环境隔离特性，单个脚本块内的运行时错误仅会终止当前代码块的后续执行，不会影响其他独立`&lt;script&gt;`块的解析与执行**。

## 一、浏览器渲染引擎的脚本处理模型

### 1.1 文档解析与脚本执行关系

浏览器采用渐进式解析策略处理 HTML 文档，当解析器遇到`&lt;script&gt;`标签时会触发同步执行机制。这个过程包含三个关键阶段：脚本下载（对外部资源）、预处理（编译）和运行时执行。对于内联脚本，解析器会立即暂停文档解析，进入 JavaScript 执行阶段，直到脚本完全执行完毕后才继续后续内容解析。

这种设计源于历史原因，早期 JavaScript 常包含`document.write()`等直接操作 DOM 结构的语句，同步执行机制可确保 DOM 修改的原子性。现代浏览器虽对异步加载脚本进行了优化，但默认的同步执行特性仍被保留以维持向后兼容性。

### 1.2 脚本块划分原则

每个独立的`&lt;script&gt;`标签构成一个**执行单元块**，浏览器引擎将其视为独立的编译上下文。实验数据显示，不同`&lt;script&gt;`块间的变量声明和函数定义共享全局作用域，但是执行流程完全隔离。例如：

```html
&lt;script&gt;
  console.log(a); // ReferenceError
  let a = 10; // 此语句不会执行
&lt;/script&gt;

&lt;script&gt;
  console.log(&quot;独立块正常执行&quot;); // 成功输出
&lt;/script&gt;
```

在此案例中，第一个脚本块因变量提升问题导致运行时错误，但第二个脚本块仍能完整执行。这种现象验证了执行流程隔离的机制特性。

## 二、脚本错误传播机制分析

### 2.1 错误作用域限定规则

JavaScript 引擎采用分块处理策略，每个`&lt;script&gt;`标签的代码在预处理阶段会生成独立的语法树。当某个代码块出现语法错误时，整个块将被标记为无效，浏览器会跳过该块的执行并抛出相应错误信息。而运行时错误（如未定义变量引用）则表现出不同的传播特性：

```html
&lt;script&gt;
  function blockA() {
    console.log(unefinedVar); // 运行时错误
  }
  blockA(); // 触发错误
  console.log(&quot;此语句不会执行&quot;);
&lt;/script&gt;

&lt;script&gt;
  function blockB() {
    console.log(&quot;块B正常执行&quot;);
  }
  blockB(); // 成功执行
&lt;/script&gt;
```

此例中，第一个脚本块的错误终止了该块内后续语句的执行，但对第二个脚本块没有产生任何影响。这种错误隔离机制确保了页面核心功能的渐进增强。

### 2.2 错误类型与影响范围

不同错误类型对脚本执行的影响存在显著差异：

| 错误类型                   | 作用范围       | 跨块传播 | 典型示例             |
| :------------------------- | :------------- | :------- | :------------------- |
| 语法错误(SyntaxError)      | 当前脚本块     | 否       | 缺少括号、错误关键字 |
| 运行时错误(ReferenceError) | 当前执行上下文 | 否       | 未定义变量引用       |
| 网络加载错误               | 当前外部脚本   | 否       | 404 资源未找到       |
| 类型错误(TypeError)        | 当前执行栈     | 否       | 对 null 进行属性访问 |

实验数据显示，所有类型的错误均不会跨`&lt;script&gt;`块传播，但可能通过共享的全局状态间接影响后续脚本。例如前序脚本未能正确初始化全局变量，可能导致后续脚本出现逻辑错误，但这属于应用程序设计缺陷而非引擎执行机制问题。

## 三、多脚本块执行时序验证

### 3.1 同步加载脚本的执行顺序

对于常规的同步脚本加载，浏览器严格遵循文档顺序执行原则：

```html
&lt;script src=&quot;a.js&quot;&gt;&lt;/script&gt;
&lt;!-- 先下载执行 --&gt;
&lt;script src=&quot;b.js&quot;&gt;&lt;/script&gt;
&lt;!-- 后下载执行 --&gt;
```

即使 b.js 先于 a.js 完成下载，浏览器仍会等待 a.js 执行完毕后再执行 b.js。这种顺序保证对存在依赖关系的脚本至关重要，但同时也带来了性能瓶颈问题。

### 3.2 异步加载模式对比

现代浏览器提供了`async`和`defer`属性来优化脚本加载：

```html
&lt;script async src=&quot;a.js&quot;&gt;&lt;/script&gt;
&lt;!-- 异步下载，立即执行 --&gt;
&lt;script defer src=&quot;b.js&quot;&gt;&lt;/script&gt;
&lt;!-- 异步下载，延迟执行 --&gt;
```

实验数据表明：

- `async`脚本在下载完成后立即执行，可能中断文档解析
- `defer`脚本在 DOMContentLoaded 事件前顺序执行
- 内联脚本始终具有最高执行优先级

值得注意的是，异步脚本之间若存在执行顺序依赖，可能引发不可预知的结果，此时应优先使用`defer`属性。

## 四、错误处理最佳实践

### 4.1 结构化异常处理

建议在每个脚本块起始处添加全局错误监听：

```javascript
window.addEventListener(&quot;error&quot;, function (e) {
  console.error(&quot;Script error:&quot;, e.message);
  return true; // 阻止默认处理
});
```

此方案可有效捕获未处理的异常，同时避免错误信息污染控制台输出。

### 4.2 脚本加载优化策略

为提升页面加载性能并降低错误影响范围，建议采用以下方案：

1. **非关键脚本异步化**：对非渲染阻塞脚本添加`async`属性

```html
&lt;script async src=&quot;analytics.js&quot;&gt;&lt;/script&gt;
```

2. **依赖管理模块化**：使用 ES6 模块或 AMD/CMD 规范管理依赖

```javascript
import { utils } from &quot;./core.js&quot;;
```

3. **错误重试机制**：对关键资源实现自动重试逻辑

```javascript
function loadScript(src, retries = 3) {
  const script = document.createElement(&quot;script&quot;);
  script.src = src;
  script.onerror = () =&gt; retries-- &gt; 0 &amp;&amp; loadScript(src, retries);
  document.head.appendChild(script);
}
```

此方案在保证功能可靠性的同时，显著提升错误恢复能力。

## 五、特殊场景影响分析

### 5.1 预解析优化机制

现代浏览器采用预解析器(pre-parser)加速资源加载，该机制可能提前触发外部脚本的下载，但不会改变执行顺序。实验显示，即使预加载了后续脚本，引擎仍严格遵循文档顺序执行原则。

### 5.2 Web Worker 的影响

通过 Web Worker 执行的脚本完全独立于主线程：

```javascript
// 主线程
const worker = new Worker(&quot;task.js&quot;);
worker.onerror = handleError;

// task.js
self.addEventListener(&quot;error&quot;, () =&gt; {
  // Worker内部错误处理
});
```

Worker 线程中的错误不会影响主线程脚本执行，这为构建容错系统提供了新范式。

## 结论

本文通过理论分析与实验验证，系统阐述了 HTML 文档中多个`&lt;script&gt;`标签间的错误传播机制。研究证实，单个脚本块的执行错误不会阻塞其他独立脚本块的执行，这种隔离机制为现代 Web 应用的健壮性提供了基础保障。开发者应充分理解该特性，采用模块化设计、异步加载和结构化错误处理等最佳实践，以构建高性能、高可用的前端应用系统。未来研究可进一步探索 Service Worker 等新技术对脚本错误处理机制的影响，以及跨 iframe 脚本错误传播的边界条件。</content:encoded><author>Asuhe</author></item><item><title>React 中渲染与状态的关系：设计理念深度解析</title><link>https://asuhe.org/blog/29ccf9a11/</link><guid isPermaLink="true">https://asuhe.org/blog/29ccf9a11/</guid><pubDate>Fri, 04 Apr 2025 16:12:16 GMT</pubDate><content:encoded># React 中渲染与状态的关系：设计理念深度解析

## 渲染与状态的概念区别

**组件渲染**：指的是 React 调用组件函数，计算并生成新的虚拟 DOM (VDOM) 的过程。

**状态 (state)**：是组件记忆的数据，在组件的多次渲染之间保持不变，除非明确通过 `setState` 或 hook 的状态更新函数修改。

## 为什么重新渲染不会重置状态？

### 1. 组件一致性原则

React 的核心设计理念是保持用户界面与状态的一致性。如果每次渲染都重置状态，那么用户交互后的数据就会丢失，UI 将无法正确反映应用的实际状态。

### 2. 声明式编程范式

React 采用声明式编程，组件函数本质上描述&quot;给定当前状态，UI 应该是什么样子&quot;，而不是命令式地定义&quot;如何从一个 UI 状态转换到另一个&quot;。这要求状态在渲染之间保持稳定。

```javascript
// 声明式：描述基于 count 的 UI 应该是什么样子
function Counter() {
  const [count, setCount] = useState(0);
  return &lt;button onClick={() =&gt; setCount(count + 1)}&gt;{count}&lt;/button&gt;;
}
```

### 3. 组件标识和生命周期

React 通过组件的位置和类型（有时还有 key）来识别组件实例。只要组件保持相同的&quot;身份&quot;，React 就会保留其状态，即使该组件重新渲染多次。

## React 的状态管理设计理念

### 1. 单向数据流

React 采用单向数据流模型，状态变化触发渲染，而非渲染影响状态。这种单向流动使应用的数据流更加可预测。

### 2. 组件的封装与隔离

每个组件维护自己的状态，形成封装单元。父组件的重新渲染不应该&quot;侵入&quot;子组件的内部状态，这保证了组件的独立性和可复用性。

### 3. 性能与优化

如果每次渲染都重置状态，那么：

- 用户交互体验会极差（如输入框内容会不断消失）
- 需要不断从外部数据源重新获取数据
- 优化技术（如记忆化）将无法发挥作用

### 4. 内部实现机制

React 在内部将状态存储在与组件实例相关联的&quot;fiber&quot;节点上，而不是组件函数内部。即使函数组件在每次渲染时重新执行，其关联的 fiber 节点（包含状态）也会被保留。

```javascript
function Example() {
  // useState 调用并不在每次重新创建状态，而是从 fiber 节点检索现有状态
  const [state, setState] = useState(initialValue);
  // ...
}
```

## 类比理解

可以将 React 组件类比为具有状态的函数：

- **传统函数**：每次调用都是全新的执行，没有&quot;记忆&quot;
- **React 组件**：虽然函数体每次渲染都重新执行，但 React 框架在后台维护了组件的&quot;记忆&quot;（状态）

## 实际意义

这种设计使开发者能够：

1. 构建持久化的用户界面，保持用户交互状态
2. 将状态逻辑与渲染逻辑清晰分离
3. 编写可预测和可测试的组件
4. 实现复杂的组件树，而不必担心状态同步问题

## 总结

状态在重新渲染间的持久性是 React 的核心设计选择，使其能够同时提供良好的开发者体验和用户体验。这种分离使开发者可以专注于声明式地描述 UI，而将状态管理的复杂性交给 React 框架处理。</content:encoded><author>Asuhe</author></item><item><title>Function Components 重新执行与子元素渲染</title><link>https://asuhe.org/blog/29ccf9ab2/</link><guid isPermaLink="true">https://asuhe.org/blog/29ccf9ab2/</guid><pubDate>Sat, 05 Apr 2025 16:12:16 GMT</pubDate><content:encoded># Function Components 重新执行与子元素渲染

在 React 中，当一个 function component 重新执行时，它的子元素是否重新渲染取决于几个因素：

## 默认行为

默认情况下，当一个 function component 重新渲染（重新执行）时，它的所有子组件也会重新渲染，无论这些子组件的 props 是否发生了变化。这是 React 的默认渲染机制。

```jsx
function Parent() {
  const [count, setCount] = useState(0);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
        增加计数: {count}
      &lt;/button&gt;
      &lt;Child /&gt; {/* Parent 每次重新渲染，Child 也会重新渲染 */}
    &lt;/div&gt;
  );
}

function Child() {
  console.log(&quot;Child 渲染了&quot;);
  return &lt;div&gt;Child 组件&lt;/div&gt;;
}
```

## 优化方法

React 提供了几种方式来避免不必要的子组件重新渲染：

### 1. React.memo

使用 `React.memo` 高阶组件可以记忆化一个组件，只有当组件的 props 发生变化时才会重新渲染：

```jsx
const MemoizedChild = React.memo(function Child() {
  console.log(&quot;Child 渲染了&quot;);
  return &lt;div&gt;Child 组件&lt;/div&gt;;
});
```

### 2. useMemo

对于复杂的子元素，可以使用 `useMemo` 来记忆化：

```jsx
function Parent() {
  const [count, setCount] = useState(0);

  // 只有依赖项变化时才会重新创建子元素
  const memoizedChild = useMemo(() =&gt; &lt;Child /&gt;, []);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
        增加计数: {count}
      &lt;/button&gt;
      {memoizedChild}
    &lt;/div&gt;
  );
}
```

### 3. 使用 children props

将子组件作为 props 传入可以避免重新创建子组件：

```jsx
function Container({ children }) {
  const [count, setCount] = useState(0);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
        增加计数: {count}
      &lt;/button&gt;
      {children} {/* children 不会因为 Container 重新渲染而重新渲染 */}
    &lt;/div&gt;
  );
}

// 使用
&lt;Container&gt;
  &lt;Child /&gt;
&lt;/Container&gt;
```

## 总结

- 默认情况下，父组件重新渲染会导致所有子组件重新渲染
- 使用 `React.memo`、`useMemo` 和 `children props` 等方式可以避免不必要的子组件重新渲染
- 合理使用这些优化方法对于提高应用性能尤为重要，特别是在子组件渲染开销较大的情况下</content:encoded><author>Asuhe</author></item><item><title>HTTP OPTIONS请求详解</title><link>https://asuhe.org/blog/29ccf9ab4/</link><guid isPermaLink="true">https://asuhe.org/blog/29ccf9ab4/</guid><pubDate>Thu, 10 Apr 2025 16:12:16 GMT</pubDate><content:encoded># HTTP OPTIONS 请求详解

OPTIONS 是 HTTP 协议中一种重要但不太常用的请求方法，它主要用于探测服务器支持的通信选项和能力。本文将详细介绍 OPTIONS 请求的细节、应用场景以及如何处理其返回值，帮助读者全面理解这一&quot;隐形侦探&quot;的工作机制。

## OPTIONS 方法的基本概念

HTTP 协议定义了多种请求方法，除了常见的 GET 和 POST 外，还有 OPTIONS、HEAD、PUT、DELETE、TRACE 和 CONNECT 等方法。根据 RFC2616 标准（HTTP/1.1），OPTIONS 方法是一种用于发送&quot;探测&quot;请求的方法，目的是确定针对某个目标地址的请求必须具有怎样的约束。

OPTIONS 方法的官方定义是：用于请求获得由 Request-URI 标识的资源在请求/响应的通信过程中可以使用的功能选项。通过这个方法，客户端可以在采取具体资源请求之前，决定对该资源采取何种必要措施，或者了解服务器的性能。

在语法上，OPTIONS 请求可以针对特定资源或整个服务器：

```bash
OPTIONS /index.html HTTP/1.1  // 针对特定资源
OPTIONS * HTTP/1.1           // 针对整个服务器
```

这两种形式在使用场景上有所不同，星号（\*）形式主要用于测试服务器性能或作为&quot;ping&quot;操作。

## OPTIONS 请求的具体细节

### 请求格式

一个标准的 OPTIONS 请求格式如下：

```bash
OPTIONS /resource HTTP/1.1
Host: example.com
Accept: */*
```

这个请求中，OPTIONS 表明请求方法，/resource 指定了客户端想要了解的资源路径，Host 指定请求的目标主机名和端口号，而 Accept 表明客户端能够接受的响应内容类型。

在跨域资源共享（CORS）场景中，OPTIONS 请求还会包含特殊的头部信息：

```bash
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.example
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
```

这里的 Access-Control-Request-Method 告知服务器实际请求将使用 POST 方法，Access-Control-Request-Headers 则说明实际请求会携带 X-PINGOTHER 和 Content-Type 头部。

### 特殊处理要点

OPTIONS 请求有一些特殊的处理要点：

1. 该请求方法的响应不能被缓存。
2. 如果 OPTIONS 请求包含一个正文，则必须有 Content-Type 来指定媒体类型。
3. 请求头中的 Max-Forwards 用来请求特定代理的处理。

## OPTIONS 响应详解

### 响应格式

服务器对 OPTIONS 请求的响应通常如下所示：

```bash
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 0
Content-Type: text/html; charset=UTF-8
Date: Tue, 4 May 2021 9:56:29 GMT
Server: Apache/2.4.46 (Unix)
```

这个响应中，状态码 200 OK 表示请求成功，Allow 头部列出了服务器允许的 HTTP 方法，这是 OPTIONS 请求的主要目的。由于 OPTIONS 请求通常不返回实体内容，Content-Length 通常为 0。

在 CORS 场景中，响应还会包含额外的头部：

```bash
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
```

这些额外的头部信息告知浏览器：允许来自`https://foo.example`的跨域请求，允许的方法有 POST、GET 和 OPTIONS，允许的请求头部有 X-PINGOTHER 和 Content-Type，预检响应可以缓存 86400 秒（1 天）。

### 响应状态码

OPTIONS 请求通常会返回以下状态码：

- 200 OK：表示请求成功，响应中包含了请求的信息。
- 204 No Content：请求成功但没有返回任何内容。
- 403 Forbidden：表示服务器理解请求但拒绝执行。
- 405 Method Not Allowed：表示请求方法不被允许。

## OPTIONS 方法的主要应用场景

### 1. 探测服务器功能

客户端可以通过 OPTIONS 请求来了解服务器支持哪些 HTTP 方法，从而避免发送服务器无法处理的请求。例如，使用 curl 命令发送 OPTIONS 请求：

```bash
curl -X OPTIONS https://example.org -i
```

返回的响应会包含 Allow 头部，表明服务器支持的方法。

### 2. CORS 预检请求

在现代 Web 开发中，OPTIONS 最常见的应用是作为跨源资源共享（CORS）的预检请求。当浏览器需要向不同源的服务器发送&quot;非简单请求&quot;时，会先自动发送一个 OPTIONS 请求，询问服务器是否允许实际的跨域请求。

非简单请求包括：

- 使用 PUT、DELETE 等方法
- 发送自定义头部
- Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain 之一

### 3. RESTful API 元信息发现

在 RESTful API 设计中，OPTIONS 方法可用于提供资源的元信息发现功能。客户端可以通过 OPTIONS 请求获取资源支持的操作列表，甚至包括每个方法的简要说明、参数要求等附加信息。

## OPTIONS 响应的处理

### 处理 Allow 头部

客户端收到 OPTIONS 响应后，首先需要检查 Allow 头部，了解服务器支持的方法列表：

```javascript
fetch(&quot;https://example.com/resource&quot;, {
  method: &quot;OPTIONS&quot;,
})
  .then((response) =&gt; {
    const allowedMethods = response.headers.get(&quot;Allow&quot;);
    console.log(&quot;服务器支持的方法:&quot;, allowedMethods);
    // 根据支持的方法决定下一步操作
  })
  .catch((error) =&gt; console.error(&quot;错误:&quot;, error));
```

### 处理 CORS 预检响应

浏览器会自动处理 CORS 预检请求的响应。开发者需要关注的是确保服务器正确返回所需的 CORS 头部。服务器端的响应应当包含：

- Access-Control-Allow-Origin：允许请求的来源
- Access-Control-Allow-Methods：允许的 HTTP 方法
- Access-Control-Allow-Headers：允许的请求头部
- Access-Control-Max-Age：预检请求的有效期（秒）

如果预检请求失败（如服务器没有返回正确的 CORS 头部），浏览器将阻止后续的实际请求，并在控制台显示错误。

### OPTIONS 响应缓存

虽然 OPTIONS 响应本身不可缓存，但 CORS 预检响应可以通过 Access-Control-Max-Age 头部指定缓存时间，减少预检请求的频率。例如，Access-Control-Max-Age: 86400 表示该预检响应可以缓存一天，在此期间对同一资源的类似请求不会再触发预检请求。

## 结论

OPTIONS 请求是 HTTP 协议中一个非常实用但容易被忽视的方法。它在服务器功能探测、CORS 预检和 API 元信息发现等场景中扮演着重要角色。通过本文的详细介绍，相信读者已经对 HTTP OPTIONS 请求的细节、用途以及如何处理其响应有了全面的理解。

在实际开发中，尤其是涉及跨域请求的场景，合理配置 OPTIONS 请求的响应头部对于确保应用的正常运行至关重要。同时，了解 OPTIONS 方法也有助于我们设计更加符合 RESTful 风格的 API，提供更好的服务发现能力。</content:encoded><author>Asuhe</author></item><item><title>iOS (WebKit) 环境下轮播图等滚动容器 border-radius 失效问题</title><link>https://asuhe.org/blog/adbd5443/</link><guid isPermaLink="true">https://asuhe.org/blog/adbd5443/</guid><pubDate>Thu, 29 May 2025 23:14:31 GMT</pubDate><content:encoded>## 关于 iOS (WebKit) 环境下轮播图等滚动容器 `border-radius` 失效问题的解决方案

### 问题描述

在 Web 开发中，我们经常会给一个容器（如 `div`）设置 `border-radius` 和 `overflow: hidden` 来实现圆角裁剪效果。当这个容器的子元素是静态的时，这个效果在所有平台上都能完美展现。

然而，当容器内包含可滚动或自动轮播的内容时（例如使用 Swiper.js 或其他库实现的轮播图），在 iOS 设备（iPhone/iPad）上的 Safari 或其他基于 WebKit 内核的浏览器（包括 App 内的 WebView）中，会出现一个常见的渲染问题：**在滚动或动画切换的瞬间，子元素（如图片）的直角会短暂地溢出父容器的圆角边界，破坏了圆角效果。**

这个问题在 Android、Windows 和 macOS 的 Chrome/Firefox 上通常不会出现，是 iOS WebKit 特有的渲染行为。

[video:demo](https://img.asuhe.org/asuhe-blog-img/ios14%E8%BD%AE%E6%92%AD%E5%9B%BE%E5%85%BC%E5%AE%B9%E6%80%A7%E9%97%AE%E9%A2%98%E8%A7%86%E9%A2%91.mp4)

#### 演示代码

```html
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;iOS border-radius Bug Demo&lt;/title&gt;
    &lt;style&gt;
      /* 基础样式，用于页面居中展示 */
      body {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
        font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,
          &quot;Helvetica Neue&quot;, Arial, sans-serif;
      }

      .info {
        position: absolute;
        top: 20px;
        padding: 10px 20px;
        background: white;
        border-radius: 8px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        text-align: center;
      }

      /* 轮播图容器 - 问题所在 */
      .swiper-container {
        width: 300px;
        height: 200px;
        border-radius: 20px; /* 设置一个明显的圆角 */
        overflow: hidden; /* 必须有 overflow: hidden 来裁剪内容 */
        border: 3px solid #333;

        /*
        ========= 解决方案 (默认注释掉) =========
        取消下面的注释，刷新页面，bug 就会消失。
      */
        /*
      -webkit-transform: translateZ(0);
      transform: translateZ(0);
      */
      }

      /* 轮播内容的总包裹器 */
      .swiper-wrapper {
        display: flex;
        width: 300%; /* 因为有3个slide，所以是300% */
        height: 100%;
        /* 关键：使用 transform 动画来触发硬件加速，从而复现问题 */
        animation: scroll 4s infinite linear;
      }

      /* 单个轮播页 */
      .swiper-slide {
        width: 300px; /* 和容器宽度一致 */
        height: 100%;
        flex-shrink: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 48px;
        color: white;
        font-weight: bold;
      }

      /* 为每个slide设置不同背景色，方便观察 */
      .slide-1 {
        background-color: #e91e63;
      }
      .slide-2 {
        background-color: #2196f3;
      }
      .slide-3 {
        background-color: #4caf50;
      }

      /* 定义滚动的动画 */
      @keyframes scroll {
        0% {
          transform: translateX(0);
        }
        30% {
          transform: translateX(0); /* 第1张停留 */
        }
        33% {
          transform: translateX(-300px); /* 切换到第2张 */
        }
        63% {
          transform: translateX(-300px); /* 第2张停留 */
        }
        66% {
          transform: translateX(-600px); /* 切换到第3张 */
        }
        97% {
          transform: translateX(-600px); /* 第3张停留 */
        }
        100% {
          transform: translateX(0); /* 回到开头 */
        }
      }
    &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div class=&quot;info&quot;&gt;
      &lt;p&gt;在有问题的 iOS 版本上查看&lt;/p&gt;
      &lt;p&gt;&lt;b&gt;观察点：&lt;/b&gt;色块的直角会溢出圆角容器。&lt;/p&gt;
    &lt;/div&gt;

    &lt;div class=&quot;swiper-container&quot;&gt;
      &lt;div class=&quot;swiper-wrapper&quot;&gt;
        &lt;div class=&quot;swiper-slide slide-1&quot;&gt;1&lt;/div&gt;
        &lt;div class=&quot;swiper-slide slide-2&quot;&gt;2&lt;/div&gt;
        &lt;div class=&quot;swiper-slide slide-3&quot;&gt;3&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;
```

### 问题原因

这个问题的根源在于 **WebKit 的渲染和硬件加速机制**。

1.  **渲染层 (Compositing Layers)**: 为了优化性能，现代浏览器会将页面上的某些元素提升到单独的“渲染层”中。当一个元素被提升后，它的绘制（painting）会独立于页面的其他部分，并且可以由 GPU 直接进行处理（硬件加速），这使得像 `transform`, `opacity` 这类动画变得非常流畅。

2.  **触发条件**: CSS 的 `transform` 属性（尤其是 3D 变换，如 `translate3d` 或 `translateZ`）是触发硬件加速和创建新渲染层的常见方式。轮播图插件为了实现平滑的滚动效果，内部大量使用了 `transform: translateX(...)` 动画。

3.  **问题发生**: 在 iOS 的 WebKit 内核中，当轮播图的子元素（slide）因为 `transform` 动画被提升到一个新的渲染层时，这个渲染层有时会“忘记”或忽略其父容器通过 `border-radius` 和 `overflow: hidden` 设置的裁剪路径（clipping path）。结果就是，动画过程中的子元素直接在其矩形渲染层上绘制，短暂地覆盖了父容器的圆角，看起来就像是 “溢出” 了。

### 解决方案

针对这个问题，有多种有效的 CSS 解决方案，核心思想都是通过一些技巧来“提示”或强制 WebKit 浏览器正确应用裁剪。

---

#### 方案一：为容器开启硬件加速 (最推荐)

这是最常用且最可靠的解决方案。通过给设置了 `border-radius` 和 `overflow: hidden` 的父容器也添加一个能触发硬件加速的 CSS 属性，让父容器和子元素在同一个渲染上下文中处理，从而解决裁剪问题。

**代码示例：**

假设你的轮播图容器 class 是 `.swiper-container`。

```css
.swiper-container {
  border-radius: 12px; /* 你的圆角值 */
  overflow: hidden;

  /* 关键修复代码 */
  /*
   * 这会为容器创建一个独立的渲染层，确保其裁剪效果（border-radius）
   * 会被应用到所有子元素上，即使子元素也在自己的渲染层中。
   * translateZ(0) 是一个不会产生任何视觉变化的 &quot;hack&quot;，但能有效触发硬件加速。
  */
  -webkit-transform: translateZ(0);
  transform: translateZ(0);
}
```

---

#### 方案二：使用 `webkit-mask-image` (效果稳定)

这个方案利用 WebKit 的遮罩属性来强制实现裁剪，效果同样非常稳定。它相当于创建了一个与 `border-radius` 形状一致的蒙版。

**代码示例：**

```css
.swiper-container {
  border-radius: 12px;
  overflow: hidden; /* 建议保留，作为不支持 mask 属性的浏览器的降级方案 */

  /* 关键修复代码 */
  /*
   * 使用一个径向渐变作为遮罩，模拟圆角裁剪。
   * 这个属性专门针对 WebKit 内核的浏览器。
  */
  -webkit-mask-image: -webkit-radial-gradient(white, black);
}
```

**注意**: 这个方法只对 WebKit 有效，但对于解决这个问题来说已经足够了，因为问题本身就主要出现在 WebKit 中。

---

#### 方案三：创建新的层叠上下文 (备选方案)

在某些情况下，为容器创建一个新的层叠上下文（Stacking Context）也能解决此问题。

**代码示例：**

```css
.swiper-container {
  border-radius: 12px;
  overflow: hidden;

  /* 关键修复代码 */
  /*
   * `position: relative` 和 `z-index` 的组合会创建一个新的层叠上下文。
   * 这有时可以影响渲染顺序和层级，从而间接修复裁剪问题。
  */
  position: relative;
  z-index: 1; /* z-index 值只要不是 auto 即可 */
}
```

这个方案不总是奏效，其效果取决于具体的布局和周围元素的层级关系，但作为备选方案值得一试。

### 总结与推荐

遇到 iOS 上 `border-radius` 在滚动/动画时失效的问题，**首选方案一 (`transform: translateZ(0)`)**。它利用了浏览器的原生机制，副作用最小，兼容性好，且逻辑上最接近问题的根源。如果方案一无效，再尝试方案二 (`-webkit-mask-image`)。</content:encoded><author>Asuhe</author></item><item><title>JavaScript中稀疏数组的空槽</title><link>https://asuhe.org/blog/29ccf9ab6/</link><guid isPermaLink="true">https://asuhe.org/blog/29ccf9ab6/</guid><pubDate>Sat, 12 Apr 2025 16:12:16 GMT</pubDate><content:encoded>上文提到数组里因为有空槽，所以这种数组是稀疏数组。而稀疏数组会导致一些问题，比如遍历、计算长度等。本文接续上文，深入解析空槽的本质、与 `undefined` 的区别、不同浏览器表现、以及开发者如何应对。

# JavaScript 数组中的空槽（Holes）完全解析

在 JavaScript 中，数组是一种非常灵活的数据结构，但也因此带来了一些难以直观理解的概念，其中就包括“空槽（hole）”。本文将深入解析空槽的本质、与 `undefined` 的区别、不同浏览器表现、以及开发者如何应对。

---

## 什么是空槽？

空槽是指 **数组中某个索引位置根本没有值、也没有被初始化**。它和 `undefined` 不同，`undefined` 是一个明确存在的值，而空槽则是“索引缺失”。

JavaScript 数组本质上是对象，索引是字符串形式的键。稀疏数组中未赋值的索引**压根没有被定义**，就像这个对象：

```js
const obj = {};
obj[2] = &quot;foo&quot;;
console.log(obj[1]); // undefined
console.log(1 in obj); // false
```

数组是对象的特殊形式，空槽就是没有这个 key 的对象属性。

```js
const arr = [1, , 3];
// arr[1] 是一个空槽，不是 undefined
```

你可以通过 `in` 操作符检查某个索引是否存在：

```js
console.log(1 in arr); // false
console.log(arr[1]); // undefined
```

---

## 空槽 vs undefined

| 比较项           | 空槽（hole）  | `undefined`   |
| ---------------- | ------------- | ------------- |
| 值是否存在       | ❌ 不存在     | ✅ 存在       |
| `in` 操作符结果  | `false`       | `true`        |
| 是否能遍历到     | ❌ 一般跳过   | ✅ 可以遍历到 |
| 是否占用数组空间 | ❌ 理论上不占 | ✅ 占用       |

示例：

```js
const arr1 = [undefined];
const arr2 = [,];

console.log(0 in arr1); // true
console.log(0 in arr2); // false
```

---

## 图示对比：稠密数组 vs 稀疏数组

### 稠密数组（Dense Array）

```js
const dense = [&quot;a&quot;, undefined, &quot;c&quot;];
```

```bash
索引:   0        1        2
值:    &apos;a&apos;    undefined  &apos;c&apos;
状态:   ✔️       ✔️        ✔️
```

### 稀疏数组（Sparse Array，含空槽）

```js
const sparse = [&quot;a&quot;, , &quot;c&quot;];
```

```bash
索引:   0      1        2
值:    &apos;a&apos;   [空槽]     &apos;c&apos;
状态:   ✔️      ❌        ✔️
```

---

## 空槽的运行时表现差异

ECMAScript 明确定义了空槽不参与大多数数组方法的处理行为，比如 map, forEach, filter, reduce 等。行为在规范中是一致的。

虽然空槽的语义在 ECMAScript 规范中是统一的，但各大浏览器或 JS 引擎的行为略有不同：

### 控制台显示差异

```js
const arr = [1, , 3];
console.log(arr);
```

| 引擎         | 控制台输出                 |
| ------------ | -------------------------- |
| Chrome       | `[1, &lt;1 empty item&gt;, 3]`   |
| Firefox      | `[1, &lt;empty&gt;, 3]`          |
| Safari       | `[1, &lt;empty&gt;, 3]`          |
| Node.js (V8) | `[ 1, &lt;1 empty item&gt;, 3 ]` |

### 运行时差异

主要体现在以下几个方面：

| 行为                   | Chrome (V8)           | Firefox (SpiderMonkey) | Node.js        | Safari         |
| ---------------------- | --------------------- | ---------------------- | -------------- | -------------- |
| `console.log([,1,2])`  | 显示 `&lt;1 empty item&gt;` | 显示 `&lt;empty&gt;`         | 和 Chrome 一致 | 显示 `&lt;empty&gt;` |
| 性能优化策略           | 可能放弃稠密优化      | 同样放弃优化           | 同 V8          | 自有策略       |
| `JSON.stringify([,1])` | `&quot;[null,1]&quot;`          | 一致                   | 一致           | 一致           |

💡**注意**：空槽在所有主流浏览器中的序列化（如 `JSON.stringify`）行为一致，会被转换为 `null`。

### JSON 序列化行为

```js
JSON.stringify([1, , 3]);
// 结果: &quot;[1,null,3]&quot;
```

空槽会被序列化为 `null`，这是因为 JSON 无法表示“无属性的索引”。

### 数组方法行为

| 方法                     | 是否跳过空槽        |
| ------------------------ | ------------------- |
| `forEach`                | ✅ 跳过             |
| `map`                    | ✅ 跳过             |
| `filter`                 | ✅ 跳过             |
| `for...in`               | ❌ 不跳过           |
| `for...of` + `entries()` | ✅ 跳过             |
| `Object.keys()`          | ✅ 只列出有值的索引 |

示例：

```js
[1, , 3].forEach((v, i) =&gt; console.log(i, v));
// 输出: 0 1 和 2 3，中间不会有 1 undefined
```

---

## 如何避免空槽带来的问题

- ✅ 使用 `.fill()` 初始化数组：
  ```js
  const arr = new Array(5).fill(undefined);
  ```
- ❌ 避免使用 `Array(n)` 创建数组然后直接使用：
  ```js
  const bad = new Array(5); // 空槽数组
  ```
- ❌ 避免使用 `delete` 删除数组元素：

  ```js
  delete arr[2]; // 会留下空槽
  ```

  ✅ 推荐使用 `splice()`：

  ```js
  arr.splice(2, 1); // 删除索引2并保持稠密
  ```

- ✅ 判断空槽的方法：
  ```js
  function hasHole(arr) {
    return arr.some((_, i) =&gt; !(i in arr));
  }
  ```

---

## 小结

- 空槽是一种“未定义的存在”，在数组中表现为索引缺失。空槽不是值，而是“值的缺席”，该索引在数组内部压根不存在。
- 它不同于 `undefined`，不应该混淆。`undefined` 是一种值，空槽根本没有值。
- 空槽影响数组方法的行为、调试可读性和运行时性能。大多数数组方法会忽略空槽，但标准行为是统一的。
- 写代码时建议避免空槽，保持数组的稠密性，有助于提高可读性、性能和逻辑一致性。不同引擎对空槽的显示方式、优化策略可能不同，但语义是一致的。
- 不建议依赖空槽进行逻辑编程，推荐用 `undefined` 表示空值更安全。</content:encoded><author>Asuhe</author></item><item><title>Chromium 内核浏览器中不同类型的网络请求执行的优先级时如何的？</title><link>https://asuhe.org/blog/adbd5445/</link><guid isPermaLink="true">https://asuhe.org/blog/adbd5445/</guid><pubDate>Thu, 12 Jun 2025 23:14:31 GMT</pubDate><content:encoded>## Chromium 内核浏览器中不同类型的网络请求执行的优先级时如何的？

## 背景

书接上文，我从发起者视角把浏览器的网络请求分为了声明式和命令式。同时知道了所有网络请求最终都汇入同一条“河流”，即网络服务(Network Service)由同一发起线程发起和同一处理线程进行处理。

说到这里，我又有问题了。既然大家都殊途同归，那它们的执行优先级又是如何的呢？

### 资源优先级与调度

我们都知道浏览器如何决定先加载哪个资源，对页面性能至关重要因为资源的加载顺序大大影响着页面最终呈现给用户的速度。但这里并没有统一标准规范，这是一个“约定优于配置”和“开发者控制”并存的领域。基于这两块领域，目前有两种手段影响资源的优先级和调度:

#### 浏览器的默认启发式策略: 利用浏览器内部复杂的启发式算法来为不同资源分配优先级。

- **关键资源优先**: 用于渲染页面的关键 `CSS(&lt;link rel=&quot;stylesheet&quot;&gt;)`和阻塞性的 `JavaScript(&lt;script&gt;无 async/defer)` 通常拥有 Highest(最高)或 High(高)的优先级，因为它们会阻塞页面的渲染。

- **fetch 请求**: 通过 `JavaScript` 发起的 `fetch` 调用通常也被赋予 `High` 优先级，因为它们往往是应用逻辑获取关键数据所必需的。

- **图片的复杂情况**: `&lt;img&gt;`标签是一个有趣的特例。默认情况下，它们的优先级是 Low(低)。但浏览器会动态调整: 例如，Chrome 117 及更高版本会将页面中前 5 个大尺寸图片的优先级提升至 Medium(中等)，而一旦某个图片进入了用户的可视区域(viewport)，其优先级会被立刻提升至 High。这些动态变化都可以在 Chrome 开发者工具的“网络”(Network)面板中观察到。

#### 开发者的控制手段: 当浏览器的默认行为不符合预期时，开发者可以通过一些手段进行干预。

- **`&lt;link rel=&quot;preload&quot;&gt;`**: 这是一个发现提示，而非严格意义上的优先级提示。它告诉浏览器: “这个资源未来肯定会用到，请尽早开始获取它”，从而让浏览器可以提前发现那些隐藏在 CSS 或 JS 深处的资源。但是，被预加载的资源仍然以其类型的默认优先级进行获取(例如，预加载的图片默认还是 Low 优先级)。

- **fetchpriority 属性**: 这是一个显式的调度提示，是开发者干预资源优先级的最强力工具。它允许开发者为`&lt;img&gt;`、`&lt;link&gt;`和`&lt;script&gt;`标签设置 `fetchpriority=&quot;high&quot;`或 `fetchpriority=&quot;low&quot;`，从而直接覆盖浏览器的默认优先级。这对于优化核心 Web 指标(Core Web Vitals)中的最大内容绘制(LCP)至关重要。通过为 LCP 图片设置 `fetchpriority=&quot;high&quot;`，开发者可以确保该图片从一开始就以高优先级下载，而无需等待浏览器启发式地提升其优先级。

### 安全层面的考虑

当我们使用 XHR 或 Fetch 进行网络请求时，需要特别注意跨域安全问题。针对跨域对于声明式请求我们好像从来没考虑过，但实际上服务端网关层会处理这种问题，前端确实是不管的。

上面说了一大通好像都只是在说声明式和命令式请求之间的差异只是发起端不一样而已，但是声明式请求与命令式请求之间最重要、最能体现设计意图的差异之一就是安全层面的设计考虑。下面我就来详细讨论一下两种请求的安全层面的差异:

- **声明式标签(`&lt;img&gt;`、` &lt;script&gt;`等)的默认行为**: 当这些标签请求一个跨域资源时，它们默认发起的是一种`no-cors`请求。这是一种高度受限的模式，其安全原则是“仅供使用，不可读取”。浏览器会正常接收并使用资源(如渲染图片、执行脚本)，但会严格禁止页面中的 JavaScript 代码访问响应的任何内容(包括响应体和大部分响应头)。对于图片，这意味着你可以展示它，但无法在`&lt;canvas&gt;`中使用它而不“污染”(taint)画布。对于脚本，这意味着如果脚本执行出错，全局的`onerror事件处理器`无法捕获到详细的错误信息，只能得到一个模糊的`Script error.`。这是一种为简单嵌入场景设计的“默认安全”模型。

- **命令式 API(fetch, XHR)的默认行为**: `fetch API `默认发起的是`cors`请求。这种模式是为 API 通信而设计的。对于跨域请求，浏览器会遵循完整的跨源资源共享(CORS)协议。对于可能产生副作用的“非简单请求”(如 PUT、DELETE 或带有自定义头的 POST)，浏览器会自动发送一个 OPTIONS 预检(preflight)请求，询问服务器是否允许即将到来的实际请求。只有当服务器在响应中返回了正确的 `Access-Control-Allow-Origin` 等 CORS 头部时，浏览器才会将响应暴露给 JavaScript 代码。`XMLHttpRequest` 也遵循同样的行为。

- **crossorigin 属性**: 权限提升的开关: `&lt;img&gt;` 和 `&lt;script&gt;` 等标签上的 `crossorigin` 属性，是连接上述两种安全模型的桥梁。它允许开发者显式地请求提升请求的权限。设置 `crossorigin=&quot;anonymous&quot;`会告诉浏览器将请求模式从默认的 `no-cors` 切换到 `cors`，并发起一个不携带身份凭证(如 Cookie、客户端证书)的 CORS 请求。设置 `crossorigin=&quot;use-credentials&quot;`则会发起一个携带身份凭证的 CORS 请求。如果服务器端也配置了允许 CORS 请求，那么这次“权限提升”就成功了。成功后，跨域图片就可以被`&lt;canvas&gt;`安全地读取，跨域脚本的详细错误信息也可以被捕获。

### 总结

最后用一个表格总结一下 Chromium 中部分资源的默认优先级以及 fetchpriority 属性的影响:

| 特性                   | 发起上下文          | 治理标准     | 设计意图                                 | 默认 CORS 模式 | CORS 模式切换           | 默认优先级                          | 优先级控制                         | 缓存控制                        | 响应访问                             |
| :--------------------- | :------------------ | :----------- | :--------------------------------------- | :------------- | :---------------------- | :---------------------------------- | :--------------------------------- | :------------------------------ | :----------------------------------- |
| `&lt;img&gt;` 、 `&lt;video&gt;`   | HTML 解析器         | WHATWG HTML  | 简单、声明式地嵌入媒体内容               | `no-cors`      | `crossorigin`           | Low (常被启发式策略提升)            | `fetchpriority`、 `loading=&quot;lazy&quot;` | 仅 HTTP 响应头                  | 脚本不可访问 (对脚本透明)            |
| `&lt;link&gt;` 、 `&lt;script&gt;` | HTML 解析器         | WHATWG HTML  | 链接关键的渲染或逻辑资源                 | `no-cors`      | `crossorigin`           | Highest (CSS), High/Medium (Script) | `fetchpriority`、 `async`、`defer` | 仅 HTTP 响应头                  | 脚本不可访问 (对脚本透明)            |
| `XMLHttpRequest`       | JavaScript API 调用 | WHATWG XHR   | 用于动态页面的异步数据交换(AJAX)         | `same-origin`  | 浏览器根据 URL 自动处理 | High                                | 不可用                             | HTTP 响应头, URL 参数“缓存破坏” | `responseText`, `responseXML` 等属性 |
| `fetch`                | JavaScript API 调用 | WHATWG Fetch | 一个现代、强大、统一的底层网络交互构建块 | `cors`         | `mode` 配置项           | High                                | `priority` 配置项                  | HTTP 响应头 + `cache` 配置项    | 返回解析为 `Response` 对象的 Promise |

### 参考阅读

- https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Attributes/rel/preload

- https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLLinkElement/fetchPriority

- https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

- https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Attributes/crossorigin</content:encoded><author>Asuhe</author></item><item><title>全面解读 JavaScript 中的稀疏数组</title><link>https://asuhe.org/blog/29ccf9ab5/</link><guid isPermaLink="true">https://asuhe.org/blog/29ccf9ab5/</guid><pubDate>Sat, 12 Apr 2025 12:12:16 GMT</pubDate><content:encoded># 全面解读 JavaScript 中的稀疏数组

JavaScript 中的数组是一种非常灵活的数据结构，但这种灵活性也带来了很多“奇特”的现象，其中之一就是**稀疏数组（Sparse Array）**。本文将系统讲解稀疏数组的概念、用途、优缺点、构造方式及其在前端开发中的实际场景。

---

## 1. 什么是稀疏数组？

稀疏数组指的是**索引不连续、元素不完整的数组**，也就是说数组中某些索引上没有值（即“空槽”）。例如：

```js
const arr = [1, , 3]; // arr[1] 是一个空槽
```

或者通过直接赋值创建：

```js
const arr = [];
arr[100] = &quot;hello&quot;;
console.log(arr.length); // 101
```

或者通过构造函数创建：

```js
const arr = new Array(100);
arr[100] = &quot;hello&quot;;
console.log(arr.length); // 101
```

在这个例子中，`arr` 的长度是 101，但只有一个元素（索引为 100），其他索引处都是空槽。

---

## 2. 稀疏数组有什么用？

虽然稀疏数组很少被显式使用，它们在某些特定场景下可以带来一定的优势：

- **节省内存空间**：不连续存储数据时，可避免为每个索引都分配内存（视引擎实现）。
- **快速跳跃访问**：可用于模拟大空间但数据稀疏的结构，例如：
  - 稀疏矩阵
  - 图数据结构
  - 缓存映射（例如以用户 ID 作为数组索引）

---

## 3. 稀疏数组的优缺点

### ✅ 优点：

- **性能节省**（理论上）：避免分配大量连续内存。
- **灵活表达**：可以表示某些不连续、稀疏的数据模型。

### ❌ 缺点：

- **遍历行为异常**：大部分数组方法（如 `forEach`, `map`）**跳过空槽**，但有些（如 `for..in`, `Object.keys`）则不会。
- **调试困难**：控制台显示类似 `[ &lt;1 empty item&gt;, 2, 3 ]`，容易混淆。
- **与密集数组行为不一致**：可能导致程序错误或逻辑混乱。
- **性能不可预期**：一些 JS 引擎对稀疏数组的优化不足，性能反而下降。

---

## 4. 稀疏数组的影响

稀疏数组影响多个方面：

- **遍历行为**

  ```js
  const arr = [1, , 3];
  arr.forEach((v) =&gt; console.log(v)); // 输出 1 和 3，不包括 undefined
  ```

- **数组方法**

  - `map`, `filter`, `forEach` 会跳过空槽
  - `in` 操作符可以判断空槽
  - `Object.keys`, `Array.keys()` 只返回已赋值的索引
  - `JSON.stringify()` 会把空槽转为 `null`

- **性能问题**
  - 在一些浏览器中，稀疏数组无法享受快速路径（Fast Path），执行效率不如稠密数组。

---

## 5. 如何避免或处理稀疏数组？

- **避免用数组作为稀疏映射结构**，更推荐使用对象或 `Map`。
- **初始化数组时填充默认值**：创建数组后使用 fill 或其他方式初始化所有索引。
  ```javascript
  const arr = new Array(5).fill(null);
  ```
- **避免使用 `delete arr[i]`**，会留下空槽；应使用 `arr.splice(i, 1)` 删除元素
- **遍历时使用索引检查**：如果你明确知道数组存在空洞而需要遍历所有索引，推荐使用传统 for 循环或者 for...in 循环（注意 for...in 循环会遍历所有可枚举属性，不仅限于数字索引）。
  ```javascript
  const arr = new Array(3);
  for (let i = 0; i &lt; arr.length; i++) {
    // 即使没有赋值，依然会执行
    console.log(`Index ${i}:`, arr[i]);
  }
  ```
- **避免无意中创建稀疏数组**：在需要一个固定长度的数组并希望进行遍历前，务必初始化每个元素，而不要直接用 new Array(length) 后再进行遍历操作。

---

## 6. 如何判断稀疏数组？

判断数组是否稀疏的关键在于检测是否存在“空槽”。
空槽的检测可以使用`for...in`循环。也可以使用`hasOwnProperty`方法。

```javascript
// for in 检测
function isSparseArray(arr) {
  if (!Array.isArray(arr)) return false;
  for (let i = 0; i &lt; arr.length; i++) {
    if (!(i in arr)) {
      return true;
    }
  }
  return false;
}

// hasOwnProperty 检测
function isSparseArray(arr) {
  if (!Array.isArray(arr)) return false;
  for (let i = 0; i &lt; arr.length; i++) {
    if (!arr.hasOwnProperty(i)) {
      return true;
    }
  }
  return false;
}
```

这个方法会检查数组中是否存在未定义的索引。

---

## 7. 如何构造稀疏数组，及其构造原理

### 常见构造方式：

- 使用 `Array` 构造函数但不初始化：
  ```js
  const arr = new Array(10); // 全是空槽
  ```
- 在某些索引上赋值跳过中间值：
  ```js
  const arr = [];
  arr[3] = &quot;hello&quot;;
  ```

### 构造原理：

稀疏数组的实现依赖于 JS 引擎底层的优化。在某些引擎中，如果数组索引是连续的，会使用线性内存存储；一旦变为稀疏，就退化为类似哈希表的结构。也就是说，**数组一旦稀疏，可能永远不能回到快速模式**。

---

## 8. 日常前端开发中哪些场景会遇到稀疏数组？

- **不当使用 `Array(length)` 创建数组**：

  ```js
  const arr = Array(5); // 不是 [undefined, undefined, undefined, undefined, undefined]
  ```

- **动态设置数组的远端索引**：

  ```js
  const arr = [];
  arr[100] = &quot;foo&quot;; // 0~99 都是空槽
  ```

- **误用 `delete` 删除数组元素**：

  ```js
  delete arr[1]; // 不会改变 length，但会留下空槽
  ```

- **接收第三方接口或老旧代码生成的数组**：某些库可能会返回稀疏数组，导致不可预测行为。

---

## 总结

稀疏数组在 JavaScript 中是一种值得了解但需谨慎使用的特性。它本质上揭示了 JS 数组的“类对象”本质，提醒开发者数组并非总是连续、密集的结构。

如果你希望代码更健壮、更可维护，建议：

- 尽量避免稀疏数组
- 明确初始化数组内容
- 谨慎使用数组方法，结合 `in` 或 `hasOwnProperty` 判断索引存在性</content:encoded><author>Asuhe</author></item><item><title>Chromium 内核浏览器中不同类型的网络请求的执行是否为同一线程？</title><link>https://asuhe.org/blog/adbd5444/</link><guid isPermaLink="true">https://asuhe.org/blog/adbd5444/</guid><pubDate>Wed, 11 Jun 2025 23:14:31 GMT</pubDate><content:encoded>## Chromium 内核浏览器中不同类型的网络请求的执行是否为同一线程？

### 背景

现代 web 前端框架中资源加载的方式通常分为两种，一种是声明式资源加载，另一种是程序式资源（XHR、Fetch 等）加载。XHR、Fetch 这种请求方式我们已经非常熟悉，另外一种其实我们也非常熟悉，比如使用 `&lt;img&gt;` 标签加载图片，使用 `&lt;link&gt;` 标签加载 CSS 文件。这种在 HTML 中使用标签加载资源的方式，我称之为声明式资源加载。

在我们的浏览器调试工具中我们可看到 XHR、Fetch 加载和图片加载等方式都有不同的筛选标签。看到这个调试面板，我不由想到这些不同类型的网络请求，在底层的执行机制上，尤其是线程模型上，是相同的吗？为了探索这个问题，于是便有的本文，本文将用作我的思考记录。

### 浏览器架构

要探讨这个问题我们首先需要回到浏览器本身的架构方式，必须首先理解现代浏览器赖以运行的宏观架构。我们都知道目前最强最先进的 Chromium 浏览器的架构方式是基于多进程模型的，它有三个主要的进程：

- **浏览器进程（Browser Process）**: 这是整个浏览器的主控中心和协调者，也被称为“代理进程”（Broker Process）。它负责管理所有其他进程、绘制浏览器窗口的非网页部分（如地址栏、标签页、按钮）、维护用户配置、并执行核心的安全策略。它是权限最高的进程。

- **渲染器进程（Renderer Process）**: 这些是处理网页内容的“工人”进程。得益于“站点隔离”（Site Isolation）特性，现代 Chrome 会为每个网站实例（包括跨站 iframe）分配一个独立的渲染器进程。每个渲染器进程都运行在严格的沙箱（Sandbox）环境中，其权限受到极大限制，无法直接访问用户的磁盘文件或直接与网络通信。它的核心职责是使用 Blink 渲染引擎解析 HTML、执行 JavaScript、计算布局并绘制页面。

- **网络服务（Network Service）**: 作为 Chromium“服务化”（Servicification）架构重构的一部分，所有网络相关的代码被从特权较高的浏览器进程中剥离出来，迁移到一个独立的、同样被沙箱化的网络服务进程中。这一举措进一步强化了浏览器的安全壁垒。现在，沙箱化的渲染器进程唯一的网络访问途径，就是通过这个受到严格管控的网络服务。

对于一个网络请求的发起可以划分为两个阶段“请求的发起”和“请求的执行”。在这个架构下我们可以清晰的看到，请求过程中，渲染器进程负责“请求的发起”，而网络服务进程负责“请求的执行”。当渲染器进程中的 HTML 解析器遇到一个&lt;img&gt;标签，或者 JavaScript 引擎执行一个 fetch 调用时，它并不能自己创建网络连接。相反，它必须通过 IPC（进程间通信）向浏览器进程发出请求，浏览器进程在进行安全检查后，再将请求转发给网络服务进程去执行。

### 当打开一个 tab 窗口时发生了什么

基于以上进程的流转，实际工作负载又被进一步细分到不同的线程上，以确保关键任务的响应性。当我们开一个新 tab 页时，Chromium 就会创建一个独立的进程来负责这个 tab 窗口的工作。在这个 tab 窗口的进程中，会有三个线程来负责主要工作：

- **主线程/UI 线程（Main/UI Thread）**: 这是每个进程中最重要的线程。在浏览器进程中，它被称为 BrowserThread::UI，负责响应用户的所有界面操作，如点击、输入等。在渲染器进程中，它就是 Blink 的主线程，负责运行 HTML 解析器、执行几乎所有的 JavaScript 代码、进行样式计算、布局和绘制。**这个线程的绝对天条是：永不阻塞。** 任何耗时操作都会导致界面卡顿，严重影响用户体验。

- **IO 线程（IO Thread）**: 这是为处理异步输入/输出（I/O）而生的专用线程。在浏览器进程和网络服务进程中都存在一个 IO 线程（在浏览器进程中称为 BrowserThread::IO）。它的核心职责是处理所有的 IPC 消息和网络通信。整个网络堆栈被设计为主要运行在这个单一的、非阻塞的 IO 线程上，通过异步回调机制来处理网络事件。

- **工作线程池（Worker Thread Pool）**: 这是一组通用的后台线程，用于执行那些可能阻塞或计算量大的任务，从而将它们从关键的 UI 线程和 IO 线程上卸载下来。

至此，我们可以对开头问题的核心——“不同网络请求是否使用相同线程”——给出一个基于架构的、更为精确的回答。`&lt;img&gt;`标签的解析和 `fetch()`的调用，都发生在渲染器进程的主线程上。这是它们生命周期的起点。然而，由于渲染器进程的沙箱限制，这些请求的实际网络 I/O 操作，都必须被代理到网络服务的 IO 线程上执行。这是它们生命周期的终点。因此，答案是：它们的发起线程相同，但执行线程也相同，只是发起和执行发生在不同的进程和线程上下文中。这种“发起与执行的分离”是现代浏览器为了兼顾安全与性能而做出的根本性架构决策。

### 结论

综上所述，一旦一个网络请求，无论它源自 `HTML` 解析器（声明式资源加载）还是 `JavaScript API`（程序式资源加载），跨越了进程边界抵达网络服务，它就会进入一个通用且高度标准化的处理流水线。

### 参考阅读

- https://developer.chrome.com/blog/inside-browser-part1?hl=zh-cn#browser-architecture</content:encoded><author>Asuhe</author></item><item><title>CSS基础</title><link>https://asuhe.org/blog/599b70c0/</link><guid isPermaLink="true">https://asuhe.org/blog/599b70c0/</guid><pubDate>Sun, 24 Jul 2022 10:35:55 GMT</pubDate><content:encoded># CSS基础

## 布局单位

常用的布局单位包括像素（`px`），百分比（`%`），`em`，`rem`，`vw/vh`。

**（1）像素**（`px`）是页面布局的基础，一个像素表示终端（电脑、手机、平板等）屏幕所能显示的最小的区域，像素分为两种类型：CSS 像素和物理像素：

- **CSS 像素**：为 web 开发者提供，在 CSS 中使用的一个抽象单位；
- **物理像素**：只与设备的硬件密度有关，任何设备的物理像素都是固定的。

**（2）百分比**（`%`），当浏览器的宽度或者高度发生变化时，通过百分比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化，从而实现响应式的效果。一般认为子元素的百分比相对于直接父元素。

**（3）em 和 rem**相对于 px 更具灵活性，它们都是相对长度单位，它们之间的区别：**em 相对于父元素，rem 相对于根元素。**

- **em：** 文本相对长度单位。相对于当前对象内文本的字体尺寸。如果当前行内文本的字体尺寸未被人为设置，则相对于浏览器的默认字体尺寸(默认 16px)。(相对父元素的字体大小倍数)。
- **rem：** rem 是 CSS3 新增的一个相对单位，相对于根元素（html 元素）的 font-size 的倍数。**作用**：利用 rem 可以实现简单的响应式布局，可以利用 html 元素中字体的大小与屏幕间的比值来设置 font-size 的值，以此实现当屏幕分辨率变化时让元素也随之变化。

**（4）vw/vh**是与视图窗口有关的单位，vw 表示相对于视图窗口的宽度，vh 表示相对于视图窗口高度，除了 vw 和 vh 外，还有 vmin 和 vmax 两个相关的单位。

- vw：相对于视窗的宽度，视窗宽度是 100vw；
- vh：相对于视窗的高度，视窗高度是 100vh；
- vmin：vw 和 vh 中的较小值；
- vmax：vw 和 vh 中的较大值；

**vw/vh** 和百分比很类似，两者的区别：

- 百分比（`%`）：大部分相对于祖先元素，也有相对于自身的情况比如（border-radius、translate 等)
- vw/vm：相对于视窗的尺寸

## 盒模型

判断一个元素的大小时有两种盒模型，一种是IE盒模型，另一种是标准盒模型。盒子的大小由`margin`、`border`、`padding`、`content`四部分组成。

在IE盒模型中如果我们给一个元素设置了`height`和`width`时，这些值会包括`content`、`padding`、`border`。如：

```css
.box{
  height:100px;
  width:100px;
  backgroundcolor:blue;
}
```

![img](https://cdn.nlark.com/yuque/0/2020/png/1500604/1603600820555-dc6ed390-d47e-412b-942a-857bbe5f280d.png?x-oss-process=image%2Fresize%2Cw_746#align=left&amp;display=inline&amp;height=368&amp;margin=%5Bobject%20Object%5D&amp;originHeight=462&amp;originWidth=791&amp;size=0&amp;status=done&amp;style=none&amp;width=630)

在标准盒模型中，如果我们给一个元素设置了宽高属性，这些值的只会包括`content`。

![image](https://cdn.nlark.com/yuque/0/2020/png/1500604/1603600820746-e10daafa-451a-454e-9705-f8c358769d5b.png#align=left&amp;display=inline&amp;height=366&amp;margin=%5Bobject%20Object%5D&amp;originHeight=455&amp;originWidth=746&amp;size=0&amp;status=done&amp;style=none&amp;width=600)

- 标准盒模型的 width 和 height 属性的范围只包含了 content，
- IE 盒模型的 width 和 height 属性的范围包含了 border、padding 和 content。

可以通过修改元素的 box-sizing 属性来改变元素的盒模型：

- `box-sizing: content-box`表示标准盒模型（默认值）
- `box-sizing: border-box`表示 IE 盒模型（怪异盒模型）



在书写html进行页面布局的过程中，我们经常使用`float`、`postion`属性进行布局。float只能在块级元素中使用，若在内联元素里使用时会失效。使用float时，对应的标签会脱离标准文档流，一个盒子

## 选择器

| **选择器**     | **格式**      | **优先级权重** |
| -------------- | ------------- | -------------- |
| id 选择器      | #id           | 100            |
| 类选择器       | .classname    | 10             |
| 属性选择器     | a[ref=“eee”]  | 10             |
| 伪类选择器     | li:last-child | 10             |
| 标签选择器     | div           | 1              |
| 伪元素选择器   | li::after     | 1              |
| 相邻兄弟选择器 | h1+p          | 0              |
| 子选择器       | ul&gt;li         | 0              |
| 后代选择器     | li a          | 0              |
| 通配符选择器   | \*            | 0              |

对于选择器的**优先级**：

- 标签选择器、伪元素选择器：1
- 类选择器、伪类选择器、属性选择器：10
- id 选择器：100
- 内联样式：1000

**注意事项：**

- `!important` 声明的样式的优先级最高；
- 如果优先级相同，则最后出现的样式生效；
- **继承得到的样式的优先级最低**；
- 通用选择器（\*）、子选择器（&gt;）和相邻同胞选择器（+）并不在这四个等级中，所以它们的权值都为 0 ；
- **样式表的来源不同时，优先级顺序为：内联样式 &gt; 内部样式 &gt; 外部样式 &gt; 浏览器用户自定义样式 &gt; 浏览器默认样式。**

## 文本溢出处理

- 单行文本溢出

```css
overflow: hidden;            // 溢出隐藏
text-overflow: ellipsis;      // 溢出用省略号显示
white-space: nowrap;         // 规定段落中的文本不进行换行
```

- 多行文本溢出

```css
overflow: hidden;            // 溢出隐藏
text-overflow: ellipsis;     // 溢出用省略号显示
display:-webkit-box;         // 作为弹性伸缩盒子模型显示。
-webkit-box-orient:vertical; // 设置伸缩盒子的子元素排列方式：从上到下垂直排列
-webkit-line-clamp:3;        // 显示的行数
```

注意：由于上面的三个属性都是 CSS3 的属性，不是所有浏览器都可以兼容，所以要在前面加一个`-webkit-` 来兼容一部分浏览器。

## 定位&amp;布局

### 浮动

**浮动的定义：** 非 IE 浏览器下，容器不设高度且子元素浮动时，容器高度不能被内容撑开。 此时，内容会溢出到容器外面而影响布局。这种现象被称为浮动（溢出）。

**浮动的特点:**

- 脱离标准文档流

  &gt; 脱离文档流的特点：
  &gt;
  &gt; - 块元素：
  &gt;   - 块元素不再独占一行
  &gt;   - 盒模型的宽高都默认由内容撑开
  &gt; - 行内元素：
  &gt;   - 行内元素浮动后特点变成块元素，可以设置宽高

- 当水平空间不够时，浮动元素会自动挤向下一行

- 浮动元素的高度不会超过其兄弟浮动浮动元素的高度

- 浮动不会遮盖文字，文字会环绕在浮动元素旁边

- 浮动元素不会脱离父元素

- 若浮动元素上一个元素未设置浮动，则浮动元素无法上移

**浮动的工作原理：**

- 浮动元素脱离文档流，不占据空间（引起“高度塌陷”现象）
- 浮动元素碰到包含它的边框或者其他浮动元素的边框停留

浮动元素可以左右移动，直到遇到另一个浮动元素或者遇到它外边缘的包含框。浮动框不属于文档流中的普通流，当元素浮动之后，不会影响块级元素的布局，只会影响内联元素布局。此时文档流中的普通流就会表现得该浮动框不存在一样的布局模式。当包含框的高度小于浮动框的时候，此时就会出现“高度塌陷”。

**浮动元素引起的问题？**

- 父元素的高度无法被撑开，影响与父元素同级的元素
- 与浮动元素同级的非浮动元素会跟随其后
- 若浮动的元素不是第一个元素，则该元素之前的元素也要浮动，否则会影响页面的显示结构

**清除浮动的方式如下：**

- 给父级 div 定义`height`属性
- 最后一个浮动元素之后添加一个空的 div 标签，并添加`clear:both`样式
- 包含浮动元素的父级标签添加`overflow:hidden`或者`overflow:auto`
- 使用 :after 伪元素。由于 IE6-7 不支持 :after，使用 zoom:1 触发 hasLayout\*\*

```css
.clearfix::after{
    content: &quot;&quot;;
    display: block;
    height: 0;
    clear: both;
  }
  .clearfix{
    *zoom: 1;
  }
```

### BFC

先来看两个相关的概念：

- Box: Box 是 CSS 布局的对象和基本单位，⼀个⻚⾯是由很多个 Box 组成的，这个 Box 就是我们所说的盒模型。
- Formatting context：块级上下⽂格式化，它是⻚⾯中的⼀块渲染区域，并且有⼀套渲染规则，它决定了其⼦元素将如何定位，以及和其他元素的关系和相互作⽤。

[块格式化上下文（Block Formatting Context，BFC）](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flow_Layout/Intro_to_formatting_contexts)是 Web 页面的可视化 CSS 渲染的一部分，是布局过程中生成块级盒子的区域，也是浮动元素与其他元素的交互限定区域。

通俗来讲：BFC 是一个独立的布局环境，可以理解为一个容器，在这个容器中按照一定规则进行物品摆放，并且不会影响其它环境中的物品。如果一个元素符合触发 BFC 的条件，则 BFC 中的元素布局不受外部影响。

**创建 BFC 的条件：**

- 根元素：body；
- 元素设置浮动：float 除 none 以外的值；
- 元素设置绝对定位：position (absolute、fixed)；
- display 值为：inline-block、table-cell、table-caption、flex 等；
- overflow 值为非visible；

**BFC 的特点：**

- 垂直方向上，自上而下排列，和文档流的排列方式一致。
- 在 BFC 中上下相邻的两个容器的 margin 会重叠
- 计算 BFC 的高度时，需要计算浮动元素的高度
- BFC 区域不会与浮动的容器发生重叠
- BFC 是独立的容器，容器内部元素不会影响外部元素
- 每个元素的左 margin 值和容器的左 border 相接触

**BFC 的作用：**

- **解决 margin 的重叠问题**：由于 BFC 是一个独立的区域，内部的元素和外部的元素互不影响，将两个元素变为两个 BFC，就解决了 margin 重叠的问题。
- **解决高度塌陷的问题**：在对子元素设置浮动后，父元素会发生高度塌陷，也就是父元素的高度变为 0。解决这个问题，只需要把父元素变成一个 BFC。常用的办法是给父元素设置`overflow:hidden`。
- **创建自适应两栏布局**：可以用来创建自适应两栏布局：左边的宽度固定，右边的宽度自适应。

```css
.left{
     width: 100px;
     height: 200px;
     background: red;
     float: left;
 }
 .right{
     height: 300px;
     background: blue;
     overflow: hidden;
 }

&lt;div class=&quot;left&quot;&gt;&lt;/div&gt;
&lt;div class=&quot;right&quot;&gt;&lt;/div&gt;
```

### margin重叠问题

**问题描述：**

两个块级元素的上外边距和下外边距可能会合并（折叠）为一个外边距，其大小会取其中外边距值大的那个，这种行为就是外边距折叠。需要注意的是，**浮动的元素和绝对定位**这种脱离文档流的元素的外边距不会折叠。重叠只会出现在**垂直方向**。

**计算原则：**

折叠合并后外边距的计算原则如下：

**兄弟元素**

- 如果两者都是正数，那么就去最大者
- 如果是一正一负，就会取两者之和
- 两个都是负值时，取绝对值大的那个

**父子元素**

- 父子元素相邻外边距，子元素的会传递给父元素（上外边距）

**解决办法：**

对于折叠的情况，主要有两种：**兄弟之间重叠**和**父子之间重叠**

（1）兄弟之间重叠

- 底部元素变为行内盒子：`display: inline-block`
- 底部元素设置浮动：`float`
- 底部元素的 position 的值为`absolute/fixed`

（2）父子之间重叠

- 父元素加入：`overflow: hidden`
- 父元素添加透明边框：`border:1px solid transparent`
- 子元素变为行内盒子：`display: inline-block`
- 子元素加入浮动属性或定位

### position属性

position 有以下属性值：

| 属性值   | 概述                                                         |
| -------- | ------------------------------------------------------------ |
| absolute | 生成绝对定位的元素，相对于 static 定位以外的一个父元素进行定位。元素的位置通过 left、top、right、bottom 属性进行规定。 |
| relative | 生成相对定位的元素，相对于其原来的位置进行定位。元素的位置通过 left、top、right、bottom 属性进行规定。 |
| fixed    | 生成绝对定位的元素，指定元素相对于屏幕视⼝（viewport）的位置来指定元素位置。元素的位置在屏幕滚动时不会改变，⽐如回到顶部的按钮⼀般都是⽤此定位⽅式。 |
| static   | 默认值，没有定位，元素出现在正常的文档流中，会忽略 top, bottom, left, right 或者 z-index 声明，块级元素从上往下纵向排布，⾏级元素从左向右排列。 |
| inherit  | 规定从父元素继承 position 属性的值                           |

前面三者的定位方式如下：

- **relative：**元素的定位永远是相对于元素自身位置的，和其他元素没关系，也不会影响其他元素。

![img](https://cdn.nlark.com/yuque/0/2020/png/1500604/1603554694939-58dfe7f7-2fc9-45e5-9961-a953f95496a7.png#align=left&amp;display=inline&amp;height=105&amp;margin=%5Bobject%20Object%5D&amp;originHeight=105&amp;originWidth=448&amp;size=0&amp;status=done&amp;style=stroke&amp;width=447)

**fixed：**元素的定位是相对于 window （或者 iframe）边界的，和其他元素没有关系。但是它具有破坏性，会导致其他元素位置的变化。

![img](https://cdn.nlark.com/yuque/0/2020/png/1500604/1603554694841-89472ba9-b236-4098-802f-c3c26ff49466.png#align=left&amp;display=inline&amp;height=117&amp;margin=%5Bobject%20Object%5D&amp;originHeight=135&amp;originWidth=516&amp;size=0&amp;status=done&amp;style=stroke&amp;width=446)

**absolute：**元素的定位相对于前两者要复杂许多。如果为 absolute 设置了 top、left，浏览器会根据什么去确定它的纵向和横向的偏移量呢？答案是浏览器会递归查找该元素的所有父元素，如果找到一个设置了`position:relative/absolute/fixed`的元素，就以该元素为基准定位，如果没找到，就以浏览器边界定位。如下两个图所示：

![img](https://cdn.nlark.com/yuque/0/2020/png/1500604/1603554694882-589670e0-cd52-41d4-a3ed-4ebbdfc88f32.png#align=left&amp;display=inline&amp;height=142&amp;margin=%5Bobject%20Object%5D&amp;originHeight=183&amp;originWidth=576&amp;size=0&amp;status=done&amp;style=stroke&amp;width=446)

![img](https://cdn.nlark.com/yuque/0/2020/png/1500604/1603554694842-2764d9ed-d5fe-45f4-8ede-34a73d237f94.png#align=left&amp;display=inline&amp;height=118&amp;margin=%5Bobject%20Object%5D&amp;originHeight=137&amp;originWidth=516&amp;size=0&amp;status=done&amp;style=stroke&amp;width=446)

### position &amp; float &amp; display的关系

（1）首先判断 display 属性是否为 none，如果为 none，则 position 和 float 属性的值不影响元素最后的表现。

（2）然后判断 position 的值是否为 absolute 或者 fixed，如果是，则 float 属性失效，并且 display 的值应该被设置为 table 或者 block，具体转换需要看初始转换值。

（3）如果 position 的值不为 absolute 或者 fixed，则判断 float 属性的值是否为 none，如果不是，则 display 的值则按上面的规则转换。注意，如果 position 的值为 relative 并且 float 属性的值存在，则 relative 相对于浮动后的最终位置定位。

（4）如果 float 的值为 none，则判断元素是否为根元素，如果是根元素则 display 属性按照上面的规则转换，如果不是，则保持指定的 display 属性值不变。

总的来说，可以把它看作是一个类似优先级的机制，&quot;position:absolute&quot;和&quot;position:fixed&quot;优先级最高，有它存在的时候，浮动不起作用，&apos;display&apos;的值也需要调整；其次，元素的&apos;float&apos;特性的值不是&quot;none&quot;的时候或者它是根元素的时候，调整&apos;display&apos;的值；最后，非根元素，并且非浮动元素，并且非绝对定位的元素，&apos;display&apos;特性值同设置值。

### flex布局

Flex 是 FlexibleBox 的缩写，意为&quot;弹性布局&quot;，用来为盒状模型提供最大的灵活性。任何一个容器都可以指定为 Flex 布局。行内元素也可以使用 Flex 布局。注意，设为 Flex 布局以后，**子元素的 float、clear 和 vertical-align 属性将失效**。采用 Flex 布局的元素，称为 Flex 容器（flex container），简称&quot;容器&quot;。它的所有子元素自动成为容器成员，称为 Flex 项目（flex item），简称&quot;项目&quot;。容器默认存在两根轴：水平的主轴（main axis）和垂直的交叉轴（cross axis），项目默认沿水平主轴排列。

以下 6 个属性设置在**容器上**：

- flex-direction 属性决定主轴的方向（即项目的排列方向）。
- flex-wrap 属性定义，如果一条轴线排不下，如何换行。
- flex-flow 属性是 flex-direction 属性和 flex-wrap 属性的简写形式，默认值为 row nowrap。
- justify-content 属性定义了项目在主轴上的对齐方式。
- align-items 属性定义项目在交叉轴上如何对齐。
- align-content 属性定义了多根轴线的对齐方式。如果项目只有一根轴线，该属性不起作用。

以下 6 个属性设置在**项目上**：

- order 属性定义项目的排列顺序。数值越小，排列越靠前，默认为 0。
- flex-grow 属性定义项目的放大比例，默认为 0，即如果存在剩余空间，也不放大。
- flex-shrink 属性定义了项目的缩小比例，默认为 1，即如果空间不足，该项目将缩小。
- flex-basis 属性定义了在分配多余空间之前，项目占据的主轴空间。浏览器根据这个属性，计算主轴是否有多余空间。它的默认值为 auto，即项目的本来大小。
- flex 属性是 flex-grow，flex-shrink 和 flex-basis 的简写，默认值为 0 1 auto。
- align-self 属性允许单个项目有与其他项目不一样的对齐方式，可覆盖 align-items 属性。默认值为 auto，表示继承父元素的 align-items 属性，如果没有父元素，则等同于 stretch。</content:encoded><author>Asuhe</author></item><item><title>深入理解同源策略</title><link>https://asuhe.org/blog/adbd5446/</link><guid isPermaLink="true">https://asuhe.org/blog/adbd5446/</guid><pubDate>Fri, 13 Jun 2025 23:14:31 GMT</pubDate><content:encoded>## 深入理解同源策略

## 背景

浏览器作为用户通往网络的主要入口,其设计的核心目标之一便是保护用户数据和系统安全。

同源策略是浏览器最核心、最基本的安全功能 。其核心原则可以概括为:

一个源(Origin)加载的文档或脚本,如何与来自另一个源的资源进行交互,应当受到严格限制 。这项策略旨在隔离潜在的恶意文档,防止恶意网站通过在用户浏览器中执行脚本,读取用户在其他网站(如已登录的邮箱、公司内网)上的敏感数据 。

### &quot;源&quot;的定义

&quot;源&quot;的定义是理解同源策略的关键。一个源由 **协议(Scheme)、主机(Host)和端口(Port)** 三者组成的元组(tuple)唯一确定 。只要这三个部分中任何一个不完全匹配,浏览器就会视其为不同的源。

### 同源策略的限制与许可

同源策略并非一刀切地禁止所有跨源交互。它的限制主要集中在防止数据被窃取上。通常,跨源交互可以分为三类 :

- **跨源写入(Cross-origin writes)**: 通常被允许。例如,链接(`&lt;a&gt;` 标签)、重定向以及表单提交(`&lt;form&gt;`)。一个页面的表单可以向另一个域名的服务器提交数据。

- **跨源嵌入(Cross-origin embedding)**: 通常被允许。这是现代网页功能的基础。例如,使用 `&lt;img&gt;` 标签嵌入来自其他域的图片,使用 `&lt;script&gt;` 标签加载第三方脚本(如 Google Analytics),或使用 `&lt;iframe&gt;` 嵌入其他网站的内容。

- **跨源读取(Cross-origin reads)**: 通常被禁止。这是同源策略的核心保护机制。一个源的脚本不能直接读取另一个源返回的响应数据。例如,通过 fetch 或 XMLHttpRequest 发起的跨源请求,其响应体默认无法被 JavaScript 代码访问。

### 跨源资源共享 (CORS)

同源策略在保障安全的同时,也给现代 Web 应用的开发带来了限制。许多合法的应用场景需要跨源获取数据,例如前端应用从独立的 API 服务器请求数据。为了在不破坏安全的前提下满足这种需求 **跨源资源共享
(Cross-Origin Resource Sharing, CORS)** 机制应运而生。

CORS 并非绕过安全限制的后门,而是一种基于 HTTP 头的机制,它允许服务器明确声明哪些源有权限读取其资源。这是一个服务器端的&quot;选择性加入&quot;(opt-in)策略,将跨源访问的控制权交还给了资源提供方。

#### CORS 将跨源请求分为两类,它们的处理流程截然不同

**简单请求 (Simple Requests)**:
简单请求不会触发一个额外的“预检”请求。其设计初衷是为了兼容历史,因为在 `XMLHttpRequest` 和 `CORS` 出现之前,HTML 的 `&lt;form&gt;` 元素就已经可以发起跨源的 POST 请求。因此,标准制定者认为服务器开发者理应已经部署了针对这类请求的安全防护(如 CSRF 防护) 。

一个请求要成为“简单请求”,必须同时满足以下所有条件 :

- 请求方法是 GET、HEAD 或 POST 之一。

- HTTP 头中,除了由浏览器自动设置的头(如 Connection, User-Agent 等)之外,开发者手动设置的头只能是 CORS 安全列表中的几个,包括 `Accept`、`Accept-Language`、`Content-Language`、`Content-Type`。

- 当请求方法是 POST 时,Content-Type 的值必须是 `application/x-www-form-urlencoded`、`multipart/form-data` 或 `text/plain` 中的一种。

- 请求中没有使用 ReadableStream 对象。

- 如果使用 XMLHttpRequest,其 upload 属性上没有注册任何事件监听器。

对于简单请求,浏览器会直接发送请求,并在请求头中附加一个 Origin 字段,表明请求的来源。服务器根据这个 Origin 值判断是否允许该请求。如果允许,服务器需要在响应头中包含 `Access-Control-Allow-Origin` 字段,其值可以是请求的 Origin 或者是 \*(表示允许任何源)。如果响应头中没有这个字段或字段值不匹配,浏览器会拦截该响应,不允许 JavaScript 代码读取,并会在控制台抛出 CORS 错误。

**预检请求 (Preflighted Requests)**:
任何不满足“简单请求”条件的跨源请求,都会在发送实际请求之前,触发一次预检请求 。预检请求使用 OPTIONS 方法,其目的是向服务器“询问”实际请求是否安全,是否被允许。

常见的触发预检请求的场景包括:

- 使用了 PUT、DELETE、PATCH 等非简单请求方法。

- Content-Type 的值为 `application/json`。

- 请求中包含了自定义的 HTTP 头,例如 `Authorization`。

预检请求的流程如下 :

浏览器发送一个 OPTIONS 请求到目标 URL。该请求包含 Origin 头,以及两个特殊的头:

1. Access-Control-Request-Method: 告知服务器实际请求将使用的方法(如 PUT)。

2. Access-Control-Request-Headers: 告知服务器实际请求将携带的自定义头(如 Content-Type, Authorization)。

服务器收到预检请求后,检查这些信息,并决定是否同意即将到来的实际请求。如果同意,服务器返回一个成功的响应(如 204 No Content),并在响应头中包含相应的 CORS 许可头:

1. Access-Control-Allow-Origin: 必须包含请求的源。

2. Access-Control-Allow-Methods: 必须包含实际请求将使用的方法。

3. Access-Control-Allow-Headers: 必须包含实际请求将携带的自定义头。

浏览器收到肯定的预检响应后,才会发送实际的跨源请求。如果预检失败,浏览器将不会发送实际请求,并在控制台报错。

#### 预检请求的性能影响与缓存

预检请求虽然保证了安全,但也带来了性能开销。每一次需要预检的跨源请求都会产生两次网络往返(一次 OPTIONS,一次实际请求),这会显著增加请求的延迟,对 API 密集型应用的影响尤为明显 。

为了缓解这个问题,CORS 规范提供了 `Access-Control-Max-Age` 响应头。服务器可以在预检响应中包含此头,以告知浏览器可以将该预检结果缓存多长时间(单位为秒) 。在缓存有效期内,对同一 URL 的同类请求将不再发送预检请求,直接发送实际请求,从而显著降低延迟 。

值得注意的是,浏览器自身会对 `Access-Control-Max-Age` 的值设置上限,这体现了不同浏览器厂商在性能与安全之间的权衡。

Firefox 的上限是 24 小时 (86400 秒) 。

Chromium (v76 及以后版本) 的上限是 2 小时 (7200 秒) 。

如果服务器未提供此头,Fetch 规范定义的默认值为 5 秒 。

### Cookie 与跨源状态: SameSite 属性

在跨源交互中,Cookie 的发送行为是一个核心问题,直接关系到身份认证和会话保持。SameSite 属性是 `Set-Cookie` HTTP 响应头的一个关键指令,它允许服务器定义 Cookie 在跨站(Cross-site)请求中应如何被发送,是防御跨站请求伪造(CSRF)攻击的有力武器 。

`SameSite` 属性有三个主要值 :

- **Strict**: 最严格的模式。Cookie 只会在同站请求中被发送。即使用户从一个外部网站点击链接导航到当前网站,该 Cookie 也不会被发送。

- **Lax**: 现代浏览器的默认值 。在跨站请求中,Cookie 不会随着子请求(如加载图片、`&lt;iframe&gt;`)发送,但会在用户通过顶层导航(如点击链接)跳转到目标网站时发送。这在安全性和可用性之间取得了很好的平衡。

- **None**: 允许 Cookie 在所有跨站请求中发送。这对于需要跨站传递状态的场景(如嵌入式第三方登录组件、广告跟踪)是必需的。然而,为了防止滥用,规范要求设置 `SameSite=None` 的 Cookie 必须同时设置 Secure 属性,意味着该 Cookie 只能通过安全的 HTTPS 连接传输 。

这些安全策略并非孤立存在,而是构成了一个层级分明的决策体系。同源策略是基础的“默认拒绝”规则。CORS 是服务器端的一种“明确授权”机制,用于放宽同源策略对“读”操作的限制。而 SameSite 则是服务器对单个 Cookie 设置的“发送策略”,它在浏览器端执行,其优先级非常高。

一个典型的混淆场景是: 开发者在服务器端配置了 `Access-Control-Allow-Origin: * ` 和 `Access-Control-Allow-Credentials: true`, 并在客户端的 fetch 请求中设置了 `credentials: &apos;include&apos;`, 意图是允许跨源请求携带凭证。然而,如果服务器设置会话 Cookie 时没有指定 SameSite 属性(或使用了现代浏览器的默认值 Lax),浏览器在发起这个跨源 fetch 请求时,仍然会拒绝发送该 Cookie。这是因为 Cookie 自身的 SameSite 策略禁止了它在跨站子请求中被发送。在这个决策链中,Cookie 的策略成为了最终的守门人,覆盖了服务器的 CORS 许可和开发者的请求意图。理解这种策略的层级和优先级对于调试复杂的跨源认证问题至关重要。

### 声明式请求和命令式请求的同源策略

之前我们提到,浏览器的网络请求根据其发起方式,可以分为两大类: 声明式请求和命令式请求。前者由浏览器解析 HTML 文档时自动发起,后者则由开发者通过 JavaScript 代码访问码主动调用。这两种请求方式在处理跨源资源时,其默认行为和安全模型存在根本性的差异,这也是许多人感到困惑的核心所在。

- 对于同源的声明式请求,浏览器没有任何限制。资源会被正常获取,并且其内容对于页面是完全可用的。

- 对于跨源的声明式请求,浏览器默认采用的是 `no-cors` 请求模式 。`no-cors` 模式的特点是: 请求可以成功发送,资源也可以被下载和使用(例如,图片被显示,脚本被执行),但其响应对于发起页面的 JavaScript 代码来说是不透明的(Opaque)。

这意味着:

画布污染 (Canvas Tainting): 如果你尝试将一个通过 `&lt;img&gt;` 标签加载的跨源图片绘制到 `&lt;canvas&gt;` 元素上,然后试图通过 `getImageData()` 或 `toDataURL()` 等方法读取画布的像素数据,浏览器会阻止出安全错误。此时,该画布被认为是“已污染”的 。

脚本错误信息屏蔽: 如果一个通过 `&lt;script&gt;` 标签加载的跨源脚本在执行时发生错误,`window.onerror` 事件处理器无法捕获到详细的错误信息(如错误消息、文件名、行号和列号)。你只会得到一个非常笼统的 `&quot;Script error.&quot;` 提示 。这使得跨域脚本的调试变得异常困难。

- 对于同源的命令式请求,浏览器没有限制,资源会被正常获取。

- 对于跨源的命令式请求,浏览器默认采用的是 `cors` 请求模式 。`cors` 模式必须遵守 CORS 规则。如果目标服务器没有返回正确的 CORS 响应头,浏览器会彻底阻止响应数据到达 JavaScript 代码。

声明式元素的 `no-cors` 默认行为遵循的是&quot;最小意外原则&quot;(Principle of Least Surprise),为了保持对海量存量网页的向后兼容。而 fetch 等较新的命令式 API 则遵循&quot;默认安全原则&quot;(Secure by Default)。

#### 命令式请求在跨源请求中管理凭证

Fetch API: 通过 `credentials` 选项来精确控制凭证的发送 。

- &apos;same-origin&apos; (默认值): 只在同源请求中发送凭证。

- &apos;include&apos;: 在同源和跨源请求中都发送凭证。

- &apos;omit&apos;: 任何情况下都不发送凭证。

XMLHttpRequest: 通过布尔类型的 `withCredentials` 属性实现。`xhr.withCredentials = true; `的效果等同于 fetch 中的 `credentials: &apos;include&apos;` 。

需要再次强调的是,Cookie 自身的 SameSite 属性拥有更高的优先级。即便一个 fetch 请求设置了 `credentials: &apos;include&apos;`,如果目标 Cookie 的 SameSite 属性为 Lax 或 Strict,浏览器依然会阻止该 Cookie 在这次跨源请求中被发送 。请求本身会发出（不带此 Cookie）,但服务器将无法收到预期的会话信息。

### 总结

为了直观地总结声明式请求与命令式请求在 CORS 行为上的差异,下表进行了详细对比:

---

| 场景                  | 请求类型  | 客户端设置                                                      | 服务器要求                                                                         | 结果分析                                                 |
| :-------------------- | :-------- | :-------------------------------------------------------------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------- |
| **跨源,无特殊设置**   | `&lt;img&gt;`   | (无 `crossorigin` 属性)                                         | 无                                                                                 | 请求成功。图片正常显示。但响应对 JS 不透明（画布污染）。 |
|                       | `fetch()` | `mode: &apos;cors&apos;` (默认)                                           | 必须返回 `Access-Control-Allow-Origin`                                             | 请求失败。fetch Promise 因网络错误被拒绝。               |
| **跨源,请求匿名访问** | `&lt;img&gt;`   | `crossorigin=&quot;anonymous&quot;`                                       | 必须返回 `Access-Control-Allow-Origin`                                             | 请求成功。响应对 JS 透明。                               |
|                       | `fetch()` | `mode: &apos;cors&apos;` (默认),\&lt;br\&gt;`credentials: &apos;same-origin&apos;` (默认) | 必须返回 `Access-Control-Allow-Origin`                                             | 请求成功。响应对 JS 透明。                               |
| **跨源,请求凭证访问** | `&lt;img&gt;`   | `crossorigin=&quot;use-credentials&quot;`                                 | 必须返回 `Access-Control-Allow-Origin` 和 `Access-Control-Allow-Credentials: true` | 请求成功 (若凭证有效)。响应对 JS 透明。                  |
|                       | `fetch()` | `mode: &apos;cors&apos;` (默认),\&lt;br\&gt;`credentials: &apos;include&apos;`            | 必须返回 `Access-Control-Allow-Origin` 和 `Access-Control-Allow-Credentials: true` | 请求成功 (若凭证有效)。响应对 JS 透明。                  |

---

### 参考阅读

- https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Access-Control-Max-Age

- https://developer.mozilla.org/zh-CN/docs/Web/Privacy/Guides/Third-party_cookies

- https://developer.mozilla.org/zh-CN/docs/Web/API/Request/credentials</content:encoded><author>Asuhe</author></item><item><title>HTML元素点击事件解析</title><link>https://asuhe.org/blog/29ccf9ab3/</link><guid isPermaLink="true">https://asuhe.org/blog/29ccf9ab3/</guid><pubDate>Wed, 09 Apr 2025 16:12:16 GMT</pubDate><content:encoded># HTML元素点击事件解析

## 引言

在Web开发中，理解事件的触发顺序对于创建响应式和用户友好的界面至关重要。本文将分析常见DOM事件的触发顺序，特别是鼠标和触摸事件的交互流程，并且解析fastclick的原理。

## 鼠标事件触发顺序

当用户与输入元素交互时，鼠标事件通常按以下顺序触发：

1. **mousedown**: 当鼠标按钮被按下时触发
2. **mouseup**: 当鼠标按钮被释放时触发
3. **click**: 当完成一次完整的点击（mousedown后跟mouseup）时触发

## 触摸事件触发顺序

在移动设备上，触摸事件的顺序如下：

1. **touchstart**: 当手指触摸屏幕时触发
2. **touchmove**: 当手指在屏幕上移动时触发（可能多次触发）
3. **touchend**: 当手指从屏幕上离开时触发

## 事件流程详解

### 点击元素事件触发的完整流程

在PC端，当用户点击元素时时，事件通常按以下顺序触发：

```bash
mousedown → mouseup → click
```

### 触摸输入框的完整流程

当用户在移动设备上点击时：

```bash
touchstart → touchend → mousedown → mouseup → click
```

## 实际应用

理解这些事件的触发顺序有助于：

1. 创建更精确的用户交互响应
2. 避免事件冲突和意外行为
3. 实现更复杂的交互模式，如拖放、手势识别等

## FastClick原理解析

### 移动端点击延迟问题

在移动设备上，浏览器默认会在用户触摸屏幕后等待约300ms，然后才会触发click事件。这个延迟存在的原因是浏览器需要判断用户是否要执行双击操作（用于缩放页面）。这种延迟会导致移动页面响应迟缓，影响用户体验。

### FastClick的工作原理

FastClick库通过以下步骤解决了这个延迟问题：

1. **监听touchend事件**：FastClick在文档加载完成后，会给目标元素（通常是document.body）添加touchstart和touchend事件监听器。

2. **阻止原生click事件**：当捕获到touchend事件后，FastClick会阻止浏览器默认的click事件（这个事件会在300ms后触发）。

3. **立即触发合成click事件**：FastClick立即在touchend事件发生的元素上通过JavaScript创建并分发一个新的鼠标事件（通过`document.createEvent`和`element.dispatchEvent`），从而实现&quot;无延迟&quot;的点击效果。

4. **处理边缘情况**：FastClick还处理了各种特殊情况，如表单元素的焦点问题、阻止重复点击等。

### 修改后的事件流程

使用FastClick后，移动设备上的事件触发顺序变为：

```bash
touchstart → touchend → [FastClick合成的click事件] → ...
```

原生的mousedown、mouseup和延迟的click事件被跳过，从而实现了更快的响应速度。

### 关键实现细节

1. **合成点击事件**：

   ```javascript
   // 通过 MouseEvents 创建合成点击事件
   clickEvent = document.createEvent(&quot;MouseEvents&quot;);
   clickEvent.initMouseEvent(
     this.determineEventType(targetElement),
     true,
     true,
     window,
     1,
     touch.screenX,
     touch.screenY,
     touch.clientX,
     touch.clientY,
     false,
     false,
     false,
     false,
     0,
     null,
   );
   clickEvent.forwardedTouchEvent = true; // 标记为转发的触摸事件
   targetElement.dispatchEvent(clickEvent);
   ```

2. **阻止重复点击**：

   - 记录上一次点击的时间，防止快速连续点击
   - 使用 `tapDelay` 和 `tapTimeout` 控制点击的时间窗口

3. **浏览器和设备检测**：

   - 检测iOS、Android、Windows Phone等不同平台
   - 针对不同平台提供特定的处理逻辑

4. **特殊元素处理**：

   - 通过 `needsClick` 和 `needsFocus` 方法判断元素是否需要特殊处理
   - 为表单元素、标签等提供特殊处理逻辑

5. **自动判断是否需要FastClick**：
   ```javascript
   FastClick.notNeeded = function (layer) {
     // 检测设备、浏览器版本和CSS属性等判断是否需要FastClick
   };
   ```

### 使用场景

FastClick特别适用于：

- 需要快速响应的移动网页应用
- 游戏和其他对响应时间敏感的应用
- 改善移动设备上表单提交和按钮点击的体验

### 现代Web开发中的替代方案

随着移动浏览器的发展，许多现代浏览器已经解决了这个300ms延迟问题：

- 设置viewport meta标签：`&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width&quot;&gt;`
- 使用CSS的touch-action属性：`touch-action: manipulation;`

在 Android 系统上的 Chrome 32+ 版本，如果在 viewport meta 标签中设置了 width=device-width，则不会有 300 毫秒的延迟。

```html
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot; /&gt;
```

同样，Android 上的 Chrome（所有版本）如果在 viewport meta 标签中设置了 user-scalable=no，也不会有延迟。但请注意，user-scalable=no 同时也会禁用捏合缩放功能，这可能会引起无障碍性问题。

对于 IE11+ 浏览器，你可以在特定元素（如链接和按钮）上使用 `touch-action: manipulation;` 来禁用双击缩放功能。对于 IE10，使用 `-ms-touch-action: manipulation`。

这些方法在许多现代浏览器中可以达到与FastClick类似的效果，且不需要额外的JavaScript库。

## 测试代码

```html
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;app&quot;&gt;点击测试&lt;/div&gt;
    &lt;script&gt;
      const app = document.getElementById(&quot;app&quot;);
      app.addEventListener(&quot;click&quot;, () =&gt; {
        console.log(&quot;click&quot;);
      });
      app.addEventListener(&quot;mousedown&quot;, () =&gt; {
        console.log(&quot;mousedown&quot;);
      });
      app.addEventListener(&quot;mouseup&quot;, () =&gt; {
        console.log(&quot;mouseup&quot;);
      });
      app.addEventListener(&quot;touchstart&quot;, () =&gt; {
        console.log(&quot;touchstart&quot;);
      });
      app.addEventListener(&quot;touchmove&quot;, () =&gt; {
        console.log(&quot;touchmove&quot;);
      });
      app.addEventListener(&quot;touchend&quot;, () =&gt; {
        console.log(&quot;touchend&quot;);
      });
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
```</content:encoded><author>Asuhe</author></item><item><title>Cache设计</title><link>https://asuhe.org/blog/5b6fc6d1/</link><guid isPermaLink="true">https://asuhe.org/blog/5b6fc6d1/</guid><pubDate>Fri, 07 May 2021 23:06:08 GMT</pubDate><content:encoded># Cache设计

## Cache原理

&gt; 利用程序的局部性原理，缩减CPU的访存时间，让CPU能够更好的发挥性能

* 空间局部性：最近被访问的块邻近的块很有可能被访问
* 时间局部性：最近被的访问的块很有可能被再次访问

## Cache设计的两大原则

* 高命中率，要求高命中率减少块的置换操作
* 对CPU透明，即CPU访问内存和访问Cache为同一种方式，无需改变

## Cache设计的四个问题

* 从主存取得的块如何在Cache中存放
* 如何访问主存放入Cache中的块
* 当Cache未命中时置换数据块的策略
* 怎样保持Cache块中数据与主存块的数据同步

# 三种Cache与主存地址映射策略

&gt; 通常Cache相对于主存都十分小，所以主存块的哪些部分放入有限的Cache空间中是一个很重要的问题。下面有三种策略用于解决主存块在Cache中的存放位置问题


## 全相联映射

&gt; Cache数据块称为行，主存数据块称为块，其中行大小==块大小。全相联映射中，主存一个块地址、块号、块内容都被一起存于Cache的行中

主存中的块被复制到Cache的任意行中

![全相联](https://i.loli.net/2021/02/09/rsdg2fLWOGitSzp.png)

CPU访问一个主存地址时，为了快速检索，指令中的块号与Cache所有行号的标记同时在比较器中比较，若命中则从Cache中读取一个字，否则在主存地址中读取

全相联映射其主要缺点是高速比较器电路难于设计和实现，因此适合小容量Cache

## 直接映射

主存被划分为n个和Cache大小相同的区，区内的块只能存入对应Cache行内

Cache行号 i 和主存块号 j 关系为 i = j mode m，其中m为Cache的大小

![全相联映射](https://i.loli.net/2021/02/09/1zF6SnI5QXZMYqV.png)



直接映射其优点是硬件简单，成本低，地址变换速度快，但其缺点是每个主存块有一个固定行号可以存放。

当连续两个访存指令要求访问块号相距m整数倍的两个块时，因两个块同时映射到同一个Cache行内，所以会产生冲突，需要置换行块，导致置换操作频繁，降低Cache效率

## 组相联

将Cache划分为若干个大小相同的组，而主存不同区内，区内的对应块可以换入Cache对应的组中，在组中可以存放在任意行

当Cache分组的大小为1时，组内只有一个块，此时为直接映射；当Caceh分组大小为整个Cache大小时，只有一个分组，此时为全相联

![组相联](https://i.loli.net/2021/02/09/ujlnd1BE5Q2NTZw.png)

组相联模式结合了适度兼顾了全相联和直接映射的优点，被普遍采用

# Cache替换策略

&gt; 当Cache存满需要换入新块时，有三种主流的替换算法分别是

## LFU算法

LFU，最不经常使用算法，将一段时间内被访问次数最少的行换出。每行设置一个计数器，当新行调入Cache，该新行计数器为0。当某个行被读取一次，该行的计数器+1，需要换出行时寻找到计数器最低的行将其换出。

## LRU算法

LRU，近期最少使用算法，将近期内最长久未被访问过的行换出。同样如LFU为每行设置一个计数器，但是Cache行每次被命中其计数器清零，其它各行计数器+1，与LFU算法相反，需要置换出行时将计数器值最高的行换出

## 随机替换

随机换出行，无固定规则。研究表明随机替换的性能只是稍稍逊于前两种算法

# Cache写操作策略

&gt; 因为Cache中的数据时主存块中的副本，在任何系统中只要存在副本就存在各副本之间数据同步的问题。当前Cache中普遍采用的同步策略有以下三种：

## 写回法

&gt; write back,copy back：CPU命中Cache时，只修改Cache行中的数据内容，仅当Cache该块被换出时，将数据写回主存；CPU未命中Cache时，将该块从主存复制到Cache行中，再对其修改。Cache中每行都置一个修改位，记录该块是否被修改，若该行被换出是修改位为0，则直接抛弃

写回法优点是减少访问主存的次数，但是写回法会导致Cache行中的数据与对应主存块中的数据不一致，产生隐患

## 全写法

&gt; write through：CPU命中Cache时，同时修改Cache行和主存块中的内容，主存和Cache同步；当CPU未命中Cache时，有两种策略，一是WTWA法，将主存块置换如Cache行中，对Cache行修改，另一种是WTNWA法，直接修改主存块，不置换入Cache中

全写法的优点是简单，Cache和主存同步，不会发生数据不一致，但是其频繁的访问主存操作导致Cache性能降低

## 写一次法

&gt; write once：CPU第一次命中Cache时，同时修改Cache行中和主存块中的内容，后面再命中时采用写回法的策略；CPU未命中时采用写回法策略

写一次法结合了写回法和全写法</content:encoded><author>Asuhe</author></item><item><title>PSW寄存器分析</title><link>https://asuhe.org/blog/2970ae2d/</link><guid isPermaLink="true">https://asuhe.org/blog/2970ae2d/</guid><pubDate>Tue, 01 Jun 2021 07:51:22 GMT</pubDate><content:encoded># FLAG 寄存器

&gt; 存储**程序状态字（PSW，program status word）**，存储相关指令的执行结果，为相关指令提供行为依据，用来控制 CPU 的相关工作方式。**flag 寄存器是按位起作用的**，**每一位都有专门的含义记录特定信息**。

&gt; 在 32 位 CPU 中称为为**EFLAGS 寄存器**，64 为 CPU 中称为**RFLAGS 寄存器**，它们扩展出的高位地址都不使用。现代计算机中一般也叫做**PSW 寄存器**。下面以 8086 为基础处理器分析该寄存器的功能

![flag寄存器](https://i.loli.net/2021/06/01/UlwYT5fQjx3gNsM.png)

#### ZF（Zero Flag）

&gt; 第 6 位标志位，零标志位。计算指令执行后，若结果为 0，则 zf=1，否则 zf=0

#### PF（Parity Flag）

&gt; 第 2 位标志位，奇偶标志位。指令执行后，若所有 bit 位中 1 的个数为偶数，则 pf=1，否则 pf=0

```asm
; 示例1
mov al, 1
add al, al
; al=00000010   1的个数为1，所以pf=0

; 示例2
add al, 10H
; al=00000011  1的个数为2，所以pf=1
```

#### SF（Symbol Flag）

&gt; 第 7 位标志位，符号标志位。指令执行后，若结果为负，则 sf=1，否则 sf=0

#### CF（Carry Flag）

&gt; 第 0 位标志位，进位标志位。**无符号运算中指令执行后，若产生进位**，则 cf=1，否则 cf=0，同时也可以用作表示借位。若其值被 add 设置则表示进位，被 sub 设置则表示借位

#### OF（Overflow Flag）

&gt; 第 11 位标志位，溢出标志位。**有符号运算**时，若产生溢出，则 of=1，否则 of=0

#### AF（Auxiliary Flag）

&gt; 第 4 位标志位，辅助进位标志位。若操作中发生了进位或借位，则 af=1，否则 af=0

```asm
; 辅助进位标志位示例
mov al, 0Fh
add al, 1
; 计算过程
; 00001111
; 00000001
; ——————
; 00010000 位3中发生了进位，af=1
```

#### IF（Interrupt-Enable Flag）

&gt; 第 9 位标志位，中断允许标志位。决定 CPU 是否能够响应外部可屏蔽中断请求，若响应则 if=1，否则 if=0

#### DF（Direction Flag）

&gt; 第 10 位标志位，方向标志位。控制 SI 和 DI 自增还是自减。若自减，则 df=1，否则 df=0

#### TF（Trap Flag）

&gt; 第 8 位标志位，追踪标志位。若 CPU 进入单步调试，则 tf=1，否则 tf=0</content:encoded><author>Asuhe</author></item><item><title>Javascript基础</title><link>https://asuhe.org/blog/b18b9acc/</link><guid isPermaLink="true">https://asuhe.org/blog/b18b9acc/</guid><pubDate>Sun, 24 Jul 2022 10:36:46 GMT</pubDate><content:encoded># Javascript基础

## 数据类型

在`javascript`中数据值的类型可以分为两大类，分别是基本类型和引用类型。基本类型都存储在栈内存上，而引用类型通常都存储在堆内存上。

### 基本类型

基本数据类型有七大类：

- Number：number类型包括了整数类型和浮点数类型，所有的数字类型都可以用number表示

- Boolean：boolean类型就只有两个值，true和false。通常用于条件判断

- String：string类型表示字符串

- Null：null类型一般用于给一个准备设置为引用类型的数据赋初始值，如`var a = null`，表示变量`a`在后面可能会被赋予一个引用类型的值

- Undefined：undefined类型用于给变量当默认值，如`var a;console.log(a)`，此时会输出`undefined`，我们没有在声明变量后给一个初始值时js引擎会给它默认添加`undefined`作为默认值

  &gt; undefined 在 JavaScript 中不是一个保留字，它作为一个属性挂载在Windows对象上，这意味着可以使用 undefined 来作为一个变量名，但是这样的做法是非常危险的，它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值，比如说 void 0。

- Symbol：symbol类型被设计之初是用于解决命名冲突问题，用它作为标识符永远不会与其他变量冲突。如`console.log(Symbol(1)===Symbol(1))`，输出false。即便传入的变量都为1，获取的结果也是不同的

- BigInt：BigInt是一种数字类型的数据，它可以表示任意精度格式的整数，使用 BigInt 可以安全地存储和操作大整数，即使这个数已经超出了 Number 能够表示的安全整数范围。

基本数据类型直接存储在栈（stack）中的简单数据段，占据空间小、大小固定，属于被频繁使用数据，所以放入栈中存储

### 引用类型

除了基本类型以外的类型都是引用类型，如Array、Object、Function。

引用数据类型存储在堆（heap）中的对象，占据空间大、大小不固定。如果存储在栈中，将会影响程序运行的性能；引用数据类型在栈中存储了指针，该指针指向堆中该实体的起始地址。当解释器寻找引用值时，会首先检索其在栈中的地址，取得地址后从堆中获得实体

## 类型判断

### typeof

`typeof`一般用于判断基本数据类型，但是它在判断`null`时会被判断为object

```javascript
console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof &apos;str&apos;);           // string
console.log(typeof []);              // object    
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object
```

### instanceof

`instanceof`一般用于判断引用数据类型，它的运行原理是判断其在原型链中是否能够找到该类型的原型

```javascript
console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log(&apos;str&apos; instanceof String);                // false 
 
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true
```

`instanceof`简单实现

```javascript
function myInstanceof(left, right) {
  if (left === null) return false
  if (typeof left === &quot;object&quot; || typeof left === &quot;function&quot;) {
    let leftProto = Object.getPrototypeOf(left)
    const rightProto = right?.prototype
    while (leftProto) {
      if (leftProto === rightProto) {
        return true
      }
      leftProto = Object.getPrototypeOf(leftProto)
    }
    return false
  }
  return false
}
```

### constructor

`constructor`有两个作用，一是判断数据的类型，二是对象实例通过 `constructor` 对象访问它的构造函数。需要注意，如果创建一个对象来改变它的原型，`constructor`就不能用来判断数据类型了

```javascript
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log((&apos;str&apos;).constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
```

```javascript
function Fn(){};

Fn.prototype = new Array();

var f = new Fn();

console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true
```



### Object.prototype.toString.call()

`Object.prototype.toString.call()` 使用 Object 对象的原型方法 toString 来判断数据类型

```javascript
var a = Object.prototype.toString;
 
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call(&apos;str&apos;));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));
```

同样是检测对象obj调用toString方法，obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样，这是为什么？

这是因为toString是Object的原型方法，而Array、function等**类型作为Object的实例，都重写了toString方法**。不同的对象类型调用toString方法时，根据原型链的知识，调用的是对应的重写之后的toString方法（function类型返回内容为函数体的字符串，Array类型返回元素组成的字符串…），而不会去调用Object上原型toString方法（返回对象的具体类型），所以采用obj.toString()不能得到其对象类型，只能将obj转换为字符串类型；因此，在想要得到对象的具体类型时，应该调用Object原型上的toString方法。

### 判断数组

- 通过Object.prototype.toString.call()做判断

```javascript
Object.prototype.toString.call(obj).slice(8,-1) === &apos;Array&apos;;
```

- 通过原型链做判断

```javascript
obj.__proto__ === Array.prototype;
```

- 通过ES6的Array.isArray()做判断

```
Array.isArray(obj);
```

- 通过instanceof做判断

```javascript
obj instanceof Array
```

- 通过Array.prototype.isPrototypeOf

```javascript
Array.prototype.isPrototypeOf(obj)
```



## 类型转换

### == 和 ===

对于 `==` 来说，如果对比双方的类型**不一样**，就会进行**类型转换**。假如对比 `x` 和 `y` 是否相同，就会进行如下判断流程：

1. 首先会判断两者类型是否**相同，**相同的话就比较两者的大小；
2. 类型不相同的话，就会进行类型转换；
3. 会先判断是否在对比 `null` 和 `undefined`，是的话就会返回 `true`
4. 判断两者类型是否为 `string` 和 `number`，是的话就会将字符串转换为 `number`

```javascript
1 == &apos;1&apos;
      ↓
1 ==  1
```

6. 判断其中一方是否为 `boolean`，是的话就会把 `boolean` 转为 `number` 再进行判断

```
&apos;1&apos; == true
        ↓
&apos;1&apos; ==  1
        ↓
 1  ==  1
```

7. 判断其中一方是否为 `object` 且另一方为 `string`、`number` 或者 `symbol`，是的话就会把 `object` 转为原始类型再进行判断

```
&apos;1&apos; == { name: &apos;js&apos; }
        ↓
&apos;1&apos; == &apos;[object Object]&apos;
```

其流程图如下：

![image](https://cdn.nlark.com/yuque/0/2021/png/1500604/1615475217180-eabe8060-a66a-425d-ad4c-37c3ca638a68.png)

### 其他类型到数值类型的转换规则

- Undefined 类型的值转换为 NaN。
- Null 类型的值转换为 0。
- Boolean 类型的值，true 转换为 1，false 转换为 0。
- String 类型的值转换如同使用 Number() 函数进行转换，如果包含非数字值则转换为 NaN，空字符串为 0。
- Symbol 类型的值不能转换为数字，会报错。
- 对象（包括数组）会首先被转换为相应的基本类型值，如果返回的是非数字的基本类型值，则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值，抽象操作 ToPrimitive 会首先（通过内部操作 DefaultValue）检查该值是否有valueOf()方法。如果有并且返回基本类型值，就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值（如果存在）来进行强制类型转换。

如果 valueOf() 和 toString() 均不返回基本类型值，会产生 TypeError 错误。

### 其他类型到布尔类型的转换规则

以下这些是假值：

- undefined
- null
- false
- +0、-0 和 NaN
- &quot;&quot;

假值的布尔强制类型转换结果为 false。从逻辑上说，假值列表以外的都应该是真值。

## 变量类型

### var

在全局作用域下声明的`var`变量会有变量提升，同时它会被挂载到`window`对象上作为一个属性

```javascript
console.log(asuhe)
var asuhe = 10
console.log(window)
```

![image-20220708145618892](/Users/admin/Library/Application Support/typora-user-images/image-20220708145618892.png)

### let

`let` 声明的变量其作用域会绑定在最近的`{}`花括号里面，而且不存在变量提升。

```javascript
// 作用域绑定
{
  let a = 10
  console.log(&quot;inner&quot;,a) // inner 10
}
console.log(&quot;outer&quot;,a) // ReferenceError
```

同时对于函数作用域内的同名变量会有`暂时性死区`的效果，`暂时性死区`和没有变量提升配合起来会屏蔽变量作用域的查找

```javascript
var a = 10
function foo(){
  console.log(a)
  let a = 20
  }
foo() // ReferenceError
```

### const

`const` 变量基本特点和`let` 一致，但是`const` 所指向的那个值不允许被修改，因此`const`在声明之初一定要赋予一个初始值

```javascript
const a // SyntaxError

const b = 10
b = 20 // TypeError

const c = { n:30 }
c.n = 40
console.log(c.n) // 40
```



## 函数

### 函数声明

函数声明有函数提升的效果，而函数声明表达式则没有

```javascript
// 因为函数提升，所以函数可以在声明前被调用
foo(1,2) // 3 
function foo(a,b){
	console.log(a + b)
}
```



### 函数声明表达式

函数声明表达式没有函数提升效果，所以必须在赋值完成后才能调用

```javascript
// 无函数提升
foo(1,2) // TypeError 此时foo为undefined
var foo = function(a,b){
  console.log(a*b)
}

foo(1,2) // 2
```



```javascript
foo(1, 2) // 3
function foo(a, b) {
  console.log(a + b)
}
foo(1, 2) // 3
var foo = function foo(a,b){
  console.log(a * b)
}
foo(1, 2) // 2
```



### this指向

在使用`function`关键字声明的函数中，`this`指向的是其调用者

```javascript
function foo(){
  console.log(this.a)
}
var a = 10
let obj = {
	f:foo,
  a:20
}
obj.f() // 20
foo() // 10
```

匿名函数没有自己的`this`，匿名函数中的`this`是匿名函数定义时的上层作用域的`this`

```javascript
let obj = {
  f:()=&gt; console.log(this.a),
  a:10
}
var a = 20
obj.f() // 20
```

### 匿名函数

1. 匿名函数只有一个形参时，可以省略（），且在函数体只有一句时可以省略{}并将该语句结果返回

   ```javascript
   let f = n =&gt; n+1
   console.log(f(2)) // 3
   ```

2. 匿名函数没有自己的`this`，因此也不能使用`call`、`apply`、`bind`等方法来改变`this`指向

3. 由于箭头函数时没有自己的this的，且this指向外层的执行环境，且不能改变指向，所以不能当做构造函数使用

   ```javascript
   let Student = (a, b) =&gt; a + b
   let s1 = new Student() // TypeError
   ```

4. 箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值

5. 箭头函数没有prototype

6. 箭头函数不能用作Generator函数，不能使用yeild关键字

## 作用域 &amp;&amp; 作用域链

在ES6以前javascript并没有像其他语言一样拥有`块级作用域`的概念，仅有函数作用域和全局作用域这两种作用域。例如：

```javascript
{
  var a = 20
}
console.log(a) // 20
```

而在ES6新增了`let`、`const`关键字后，由这两种关键字声明的变量其作用域会绑定在其最近的`{}`内。例如：

```javascript
{
  let a = 20
}
console.log(a) // ReferenceError
```

在 JavaScript 里面，函数、块、模块都可以形成作用域（一个存放变量的独立空间），他们之间可以相互嵌套，作用域之间会形成引用关系，这条链叫做作用域链。

当我们去查找一个变量a时，若这个变量a不在本级作用域中那么它会继续往它的上级作用域中去寻找该变量的声明。若最后找不到就报错。

### 闭包

**闭包就是在一个变量对象里持有另一个变量对象里内容的引用**，这时就会产生闭包。常见的表现形式就是，内部函数持有外部函数的变量，我们可以通过返回内部函数去让更外层的作用域能够访问到内部函数的父函数里的变量。

```javascript
function fun(n, o) {
  console.log(o)
  return {
    fun: function (m) {
      return fun(m, n)
    }
  }
}
var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
var b = fun(0).fun(1).fun(2).fun(3) 
```

## 常用对象&amp;&amp;对象方法

### Array对象

#### 静态方法

##### Array.from

`Array.from()`从一个可迭代对象或类数组对象中，创建一个新的数组实例。**它实现的是浅拷贝。**

参数：

arrayLike：一个可迭代对象或类数组对象

mapFn（可选）：遍历函数

thisArg（可选）：执行遍历函数时的this指针

```javascript
console.log(Array.from(&quot;asuhe&quot;)) // [&quot;a&quot;, &quot;s&quot;, &quot;u&quot;, &quot;h&quot;, &quot;e&quot;]

console.log(Array.from([2,4,5,6])) // [2,4,5,6]

// 传入遍历函数
function map(x){
	return x + 1
}
console.log(Array.from([2,4,5,6],map)) // [3,5,6,7]

// 传入this参数
function Map(x){
  console.log(this.a)
  return x + 2
}
const obj = { a:666 }
console.log(Array.from([1,2,3],Map,obj)) // 666 666 666 [3,4,5]
```

##### Array.isArray

判断一个变量是否为数组

参数：

value：需要判断的变量

```javascript
console.log(Array.isArray([1,2,3])) // true
console.log(Array.isArray(1)) // false
console.log(Array.isArray({a:666})) // false
console.log(Array.isArray(&quot;asuhe&quot;)) // false
```

##### Array.of

根据传入的参数创建一个数组，浅拷贝

参数：

elementN：传入的参数可以为可变数量，传多少个就创建多长的数组

```javascript
const obj = {a:666}
const test = Array.of(1,2,&apos;asuhe&apos;,obj)
console.log(test) // [1, 2, &quot;asuhe&quot;, Object { a: 666 }]
obj.a = 777
console.log(test) // [1, 2, &quot;asuhe&quot;, Object { a: 777 }]
```

#### 实例方法

##### concat

合并多个参数，返回一个新数组。浅拷贝

参数：

valueN：参数可以为数组，也可以为普通参数。

```javascript
const array1 = [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]
const array2 = [&apos;d&apos;, &apos;e&apos;, &apos;f&apos;]
const array3 = array1.concat(array2,666)

console.log(array3) // [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;, &quot;f&quot;,666]
```

##### filter

过滤出回调函数返回值为true的元素，返回一个符合条件的新数组。

参数：

callbackFn：遍历的回调

thisArg（可选）：回调执行时的this

```javascript
const arr = [1,2,3,4,5,6,7]
const res = arr.filter((element,index,arr)=&gt;{
  return element%2 === 1
})
console.log(res) // [1, 3, 5, 7]
```

##### every

当所有的元素都通过了回调函数的条件时，返回一个true的布尔值，否则返回false。

参数：

callbackFn：遍历的回调

thisArg（可选）：回调执行时的this
```javascript
function isOdd(e){
  return e%2===1
}
const arr = [1,3,5,7]
console.log(arr.every(isOdd)) // true
```

#### some

当数组中有一个元素通过了回调函数的条件时，返回一个true的布尔值，否则返回false。

参数：

callbackFn：遍历的回调

thisArg（可选）：回调执行时的this

```javascript
function isOdd(e){
  return e%2===1
}
const arr = [2,3,4,6,8]
console.log(arr.some(isOdd)) // true
```

##### reduce

根据遍历函数的返回值，一直传递给下次遍历函数。一般用于计算总和

参数：

callbackFn：遍历的回调

initialValue（可选）：回调执行时的this

```javascript
const initialValue = 0
const arr = [1,2,3,4,5]
const res = arr.reduce((preValue,e) =&gt; preValue+e,initialValue )

console.log(res) // 15
```

### Object对象

#### 静态方法

##### Object.keys

返回一个数组，数组内容为对象中所有的可枚举属性的key值

```javascript
const flag = Symbol()
const obj = {
  a:1,
  b:&quot;asuhe&quot;,
  hhh:666,
  [flag]:&quot;sphinx&quot;
}
console.log(Object.keys(obj)) // [&quot;a&quot;, &quot;b&quot;, &quot;hhh&quot;]
```

##### Object.getPrototypeOf

获取一个对象的隐式原型

```javascript
const obj = {
  name:&quot;asuhe&quot;,
  age:16
}
const objProto = Object.getPrototypeOf(obj)

console.log(objProto === Object.prototype) // true
```



##### Object.create

使用传入的对象作为`prototype`，创建一个新的对象

```javascript
const obj = {
	name:&quot;asuhe&quot;,
  age:16
}
const res = Object.create(obj)

console.log(Object.getPrototypeOf(res) === obj) // true
```

##### Object.assign

从源对象中，合并源对象的可枚举属性进目标对象，并返回修改后的对象

参数：

target：需要修改的目标对象

source：提供可枚举属性来源的源对象

```javascript
const target = {
  name:&quot;asuhe&quot;,
  age:16
}
const source = {
  gender:&quot;male&quot;
}
const res = Object.assign(target,source)
console.log(res) // { name: &quot;asuhe&quot;, age: 16, gender: &quot;male&quot; }
console.log(res === target) // true
```



## 逻辑中断

### &amp;&amp;

`&amp;&amp;`逻辑与，当前一个条件判断为真时，才会继续执行后一个条件，并返回表达式的值

```javascript
console.log(1+2 &amp;&amp; 3+4) // 7
console.log(0 &amp;&amp; 2 + 4) // 0
```



### ||

`||`逻辑或，当前一个条件判断为假时，才会继续执行后一个条件，并返回表达式的值

```javascript
console.log( 0 || 3 + 3) // 6
console.log( 1 + 4 || 4 + 5) // 5
```

### ？ ：

`? :`三目运算符，当条件值为true时，返回第一个表达式的值，否则返回第二个表达式的值

```javascript
console.log( 1 ? 1+2 : 3+4 ) // 3
console.log( 0 ? 1+2 : 3+4) // 7
```

## 原型链

- 所有对象都有一个隐式原型`__proto__`
- 所有函数对象都有一个原型`prototype`
- 以某函数为构造函数，`new`出实例的隐式原型`__proto__`都会指向该函数的原型`prototype`
- 所有函数对象都是`Function`的实例，包括`Function`自身
- 所有函数对象的`prototype`都是`Object`的实例

```javascript
function Foo(a,b){
  return a+b
}
const obj = new Foo()

console.log(obj instanceof Object) // true
console.log(obj instanceof Foo) // true
console.log(Foo instanceof Object) // true
console.log(Foo instanceof Function) // true
```

![终极原型链](https://s2.loli.net/2022/07/12/snmKAgt8XkfTbSM.png)

## 异步编程

### event-loop

事件循环模型示意图

![事件循环模型](https://s2.loli.net/2022/07/12/yUMJtAuW38Fv7x9.png)

### Promise



### Task &amp;&amp; Microtask 



[Microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide)

MDN中Task（宏任务）的描述：

&gt; A **task** is any JavaScript code which is scheduled to be run by the standard mechanisms such as initially starting to run a program, an event callback being run, or an interval or timeout being fired. These all get scheduled on the **task queue**.

MDN中Microtask（微任务）的描述：

&gt; At first the difference between microtasks and tasks seems minor. And they are similar; both are made up of JavaScript code which gets placed on a queue and run at an appropriate time. However, whereas the event loop runs only the tasks present on the queue when the iteration began, one after another, it handles the microtask queue very differently.

两者不同之处的MDN描述：

&gt; There are two key differences.
&gt;
&gt; First, each time a task exits, the event loop checks to see if the task is returning control to other JavaScript code. If not, it runs all of the microtasks in the microtask queue. The microtask queue is, then, processed multiple times per iteration of the event loop, including after handling events and other callbacks.
&gt;
&gt; Second, if a microtask adds more microtasks to the queue by calling [`queueMicrotask()`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask), those newly-added microtasks *execute before the next task is run*. That&apos;s because the event loop will keep calling microtasks until there are none left in the queue, even if more keep getting added.</content:encoded><author>Asuhe</author></item><item><title>react脚手架配置代理</title><link>https://asuhe.org/blog/1db0eb3/</link><guid isPermaLink="true">https://asuhe.org/blog/1db0eb3/</guid><pubDate>Fri, 01 Jul 2022 22:53:12 GMT</pubDate><content:encoded># react脚手架配置代理总结



## 方法一

&gt; 在package.json中追加如下配置

```json
&quot;proxy&quot;:&quot;http://localhost:5000&quot;
```

说明：

1. 优点：配置简单，前端请求资源时可以不加任何前缀。
2. 缺点：不能配置多个代理。
3. 工作方式：上述方式配置代理，当请求了3000不存在的资源时，那么该请求会转发给5000 （优先匹配前端资源）



## 方法二

1. 第一步：创建代理配置文件

   ```
   在src下创建配置文件：src/setupProxy.js
   ```

2. 编写setupProxy.js配置具体代理规则：

   ```js
   const proxy = require(&apos;http-proxy-middleware&apos;)
   
   module.exports = function(app) {
     app.use(
       proxy(&apos;/api1&apos;, {  //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
         target: &apos;http://localhost:5000&apos;, //配置转发目标地址(能返回数据的服务器地址)
         changeOrigin: true, //控制服务器接收到的请求头中host字段的值
         /*
         	changeOrigin设置为true时，服务器收到的请求头中的host为：localhost:5000
         	changeOrigin设置为false时，服务器收到的请求头中的host为：localhost:3000
         	changeOrigin默认值为false，但我们一般将changeOrigin值设为true
         */
         pathRewrite: {&apos;^/api1&apos;: &apos;&apos;} //去除请求前缀，保证交给后台服务器的是正常请求地址(必须配置)
       }),
       proxy(&apos;/api2&apos;, { 
         target: &apos;http://localhost:5001&apos;,
         changeOrigin: true,
         pathRewrite: {&apos;^/api2&apos;: &apos;&apos;}
       })
     )
   }
   ```

说明：

1. 优点：可以配置多个代理，可以灵活的控制请求是否走代理。
2. 缺点：配置繁琐，前端请求资源时必须加前缀。</content:encoded><author>Asuhe</author></item><item><title>如何设计一个监听http请求的埋点</title><link>https://asuhe.org/blog/86b1c5112/</link><guid isPermaLink="true">https://asuhe.org/blog/86b1c5112/</guid><pubDate>Sun, 05 Nov 2023 10:14:23 GMT</pubDate><content:encoded>## 前言

为了更好地进行数据统计，更加深入地获取应用运行的状态，监控线上异常。

我们想做一个针对 http 数据请求的埋点监听。通过监听应用的中的 http 数据请求，和往常业务数据进行对比，从而实现对线上业务的异常监控。

## 目标

为了实现这个线上监控的目的，我们要先制定需要采集的数据。

目前主流采集的数据有两个思考方向，一是从响应速度，二是响应质量。响应速度方面，我们经常采集的数据有，http 请求时间、结束时间、持续时间；响应质量这块，我们常采集的数据有 http 状态码、请求的接口、当前请求发出的页面。

## 方案设计

无论你是用当前流行的`SWR`或者`axios`请求库，亦或是用例如`next.js`、`umi.js`自带的请求库。**其底层异步请求都是[xhr](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/XMLHttpRequest)、[fetch](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API)支撑的。**

所以我们仅需要从`xhr`和`fetch`入手就能实现的我们的目的。

`xhr`全称是`XMLHttpRequest`，它利用这个对象进行 ajax 请求。一个`XMLHttpRequest`对象在发送请求时，必定要经过创建请求实例、发送请求这两个阶段。而这两个阶段又分别对应它实例里的`open`和`send`方法，在这里我们可以选择其中一个阶段进行我们的埋点操作。这个例子中我选择在`open`阶段进行计时。

到这一步，思路就很清晰了。我们可以直接在`XMLHttpRequest`对象的原型上重写`open`方法，来实现我们的目的。

`fetch`是基于`Promise`的异步网络请求，它的底层并不使用`XMLHttpRequest`对象，所以对它我们需要额外进行处理。

根据上面封装`xhr`的思路，我们可以对`fetch`进行重写，把它重新包装成一个`Promise`来达到我们的目的。

具体代码如下：

```javascript
let isCalled = false;
const sendData = data =&gt; data;

const httpMonitor = () =&gt; {
  if (isCalled) return;
  isCalled = true;

  // 处理XHR请求
  if (XMLHttpRequest?.prototype?.open) {
    const originOpen = XMLHttpRequest.prototype.open;

    const start = new Date().getTime();

    XMLHttpRequest.prototype.open = function (...args) {
      this.addEventListener(&apos;loadend&apos;, () =&gt; {
        const end = new Date().getTime();
        const data = {
          start,
          end,
          duration: end - start,
          httpStatus: this.status,
          reqUrl: args[1],
          reqPage: window?.location?.href
        };
        sendData(data);
      });

      this.addEventListener(&apos;timeout&apos;, () =&gt; console.log(&apos;xhr timeout&apos;));

      originOpen.apply(this, args);
    };
  }

  // 处理fetch请求
  if (window?.fetch) {
    const originFetch = window.fetch;

    window.fetch = function (...args) {
      const start = new Date().getTime();
      let end = start;

      return new Promise((resolve, reject) =&gt; {
        originFetch.apply(this, args).then(
          response =&gt; {
            end = new Date().getTime();
            resolve(response);
          },
          error =&gt; {
            end = new Date().getTime();
            reject(error);
          }
        );
      }).then(
        res =&gt; {
          const { code = 200, url = &apos;&apos; } = res || {};
          const data = {
            start,
            end,
            duration: end - start,
            httpStatus: code,
            reqUrl: url,
            reqPage: window?.location?.href
          };

          sendData(data);
          return res;
        },
        err =&gt; err
      );
    };
  }
};

httpMonitor();

export { httpMonitor };

```</content:encoded><author>Asuhe</author></item><item><title>计算机总线系统概述</title><link>https://asuhe.org/blog/691bd858/</link><guid isPermaLink="true">https://asuhe.org/blog/691bd858/</guid><pubDate>Mon, 14 Jun 2021 21:34:31 GMT</pubDate><content:encoded># 总线系统

## 基础概念

总线用于将计算机各个部件连接起来，成为一个整体，是多个系统功能部件之间进行数据传输的公共通道

单处理器系统中总线分为三大类

* 内部总线：CPU内部连接寄存器、运算部件
* 系统总线：计算机系统用于连接其它高速设备，存储器、通道等
* I/O总线：用于连接中、低速 I/O 设备

## 总线的特性

* 物理特性：指总线的物理连接方式，包括总线的根数，总线插头、引脚数等
* 功能特性：描述总线中每一根线的功能
* 电气特性：定义每根线上信号的传递方向以及有效的电平范围
* 时间特性：定义了每根线在何时有效

## 总线连接方式

通常采用适配器部件来完成

* 单总线系统：要总线上的所有逻辑部件都高速运行，以便某些设备需要使用总线时能迅速获得总线控制权
* 多总线系统：将高速设备（北桥）和低速设备（南桥）分开，用不同类型的桥扩展出不同层次的总线

## 总线内部结构

* 地址线：单向，用于传送主存与设备的地址
* 数据线：双向，用来传送数据
* 控制线：对每根线是单向（CPU发向接口，或接口发向CPU），用来指明数据传送的方向（存储器读写、I/O读写）、中断控制和定时控制等

## 总体总线分类

### 数据传送总线

由地址线、数据线、控制线组成

### 仲裁总线

包括总线请求线和总线授权线

### 中断和同步总线

用于处理带优先级的中断操作，包括中断请求线和中断认可线

### 公用线

包括时钟信号线、电源线、地线、系统复位线以及加电或断电的时序信号线

## 总线仲裁

### 集中式仲裁

* 链式查询

  菊花链式查询，授权信号串行地从一个接口发送到下一个接口，若中间有设备请求总线则获得授权，信号不再传递。离CPU越近的设备优先级越高

  该种方式优点是简单、节省线路；其缺点是优先级不能改变，不灵活，可能存在优先级低的设备饿死，对电路故障十分敏感，若中间设备故障则后面的设备都无法工作

* 计数器定时查询

  设备通过BR线发出请求，总线仲裁器接受到请求后，在BS线为0的情况下计数，计数值通过地址线发往各设备。每个设备都有设备地址判别电路，当地址线计数值与设备地址一致时，该设备获得总线使用权，BS置1，终止计数

  该种方式优点是可以通过决定是否从上次终止点开始继续计数还是从0计数来改变各个设备的优先级；其缺点是要增加线的数量

* 独立请求

  每个设备都有一对独立的总线请求线和授权线，请求的信号在排队电路中被总线仲裁器以优先级次序处理授权

  该种方式优点是响应快，优先级次序控制灵活；其缺点是线数大量增加

**现代计算机基本采用独立请求方式**

### 分布式仲裁

不需要集中的总线仲裁器，每个设备都有自己的总线仲裁器。请求总线时，将它们唯一的仲裁号发送到共享的仲裁总线上，每个仲裁器将从仲裁总线上得到的号与自己比较。若仲裁总线上的号大，则不予响应并撤销其仲裁号，最后获胜者的仲裁号保留在仲裁总线上获得授权</content:encoded><author>Asuhe</author></item><item><title>CSMA/CD中重传时机的确定</title><link>https://asuhe.org/blog/adbd544a/</link><guid isPermaLink="true">https://asuhe.org/blog/adbd544a/</guid><pubDate>Sat, 29 May 2021 23:14:31 GMT</pubDate><content:encoded># 截断二进制指数规避算法

## 什么是截断二进制指数规避算法

在[CSMA/CD](https://baike.baidu.com/item/CSMA%2FCD%E5%8D%8F%E8%AE%AE)协议中，检测到碰撞我们要重传数据。那么如何选择碰撞后数据的重传的时机呢？该算法就是为了解决这个问题而提出的

## 如何使用该算法计算重传时机

首先，发生碰撞后我们要确定一个基本的退避时间来延迟发送。假设从A发送到B的[传播时延](https://baike.baidu.com/item/%E4%BC%A0%E6%92%AD%E6%97%B6%E5%BB%B6)是 t ，我们将基本退避时间设置为 2t 。

其次，定义参数 k ，用于记录重传的次数。 k = min[ 重传次数，10 ]。当检测到数据碰撞时，在 0 ~ 2&lt;sup&gt;k&lt;/sup&gt; - 1 的范围中随机取出一个数 r ，重传时间 R 即为 r 倍的基本退避时间 ， R = 2tr 

最后，若重传 **16** 次仍然不能成功发送数据则向网络层发出错误信息表示网络拥挤并抛弃此帧</content:encoded><author>Asuhe</author></item><item><title>数据链路层中最小帧长问题</title><link>https://asuhe.org/blog/af7555b9/</link><guid isPermaLink="true">https://asuhe.org/blog/af7555b9/</guid><pubDate>Thu, 20 May 2021 23:12:19 GMT</pubDate><content:encoded># 数据链路层中的最小帧长是如何计算出来的

众所周知，[Ethernet](https://zh.wikipedia.org/wiki/%E4%BB%A5%E5%A4%AA%E7%BD%91#:~:text=%E4%BB%A5%E5%A4%AA%E7%BD%91%EF%BC%88%E8%8B%B1%E8%AA%9E%EF%BC%9AEthernet%EF%BC%89,%E7%89%8C%E7%8E%AF%E3%80%81FDDI%E5%92%8CARCNET%E3%80%82)的最小帧长是64 Byte。那么这个最小帧长是如何得到的呢？

## 数据碰撞问题

当多个主机挂载在bus型半双工信道上时我们都会存在一个信道争用问题，为解决这个问题我们发明了CSMA/CD协议。

当两个主机同时在bus型半双工信道上发送数据就会产生数据碰撞。数据发送过程如图所示

![碰撞检测](https://i.loli.net/2021/02/02/4SZ3mcQAJtBNjoM.png)

我们假设单向传播时延为 t ，图中发生数据碰撞时，A知道碰撞检测的时间为 2 t&lt;sub&gt;1&lt;/sub&gt; ，而B检测到碰撞的时间为 2 t&lt;sub&gt;2&lt;/sub&gt;。

t = t&lt;sub&gt;1&lt;/sub&gt; + t&lt;sub&gt;2&lt;/sub&gt;

最差的情况下，当A即将到达B时，B才发送数据，此时发生碰撞。所以最迟我们检测到数据碰撞的时间是 2t

## 如何确定最小帧长

因为如果我们自身数据全部发完后才检测到数据则数据会被误认为是正常的数据而被接收，所以我们要确保我们还没有发完数据时就检测到碰撞。

因此根据上面的时延计算，我们可以得出 **帧长 / 数据传输速率 &gt;= 2 t**

**最小帧长 = 数据传输速率 x 传播时延 x 2**</content:encoded><author>Asuhe</author></item><item><title>浅谈rem布局</title><link>https://asuhe.org/blog/86b1c518/</link><guid isPermaLink="true">https://asuhe.org/blog/86b1c518/</guid><pubDate>Thu, 07 Oct 2021 23:14:23 GMT</pubDate><content:encoded># rem + less + 媒体查询布局

## rem

### 初识rem

什么是rem，rem的全称为root em。em我们都知道，1em代表一个字符的大小。而root em顾名思义，我们可以大致猜测出是以某个源为标准的单位。实际上rem代表的是以&lt;html&gt;标签里font-size属性的单位，举个例子

```css
html {
	设置html文件的默认字体大小为16px
	font-size:16px;
}
此时 1 rem = 16 px;
```

由此可以看出，html标签里设置的字体像素值为多少，其每单位rem就是多少像素的大小。

### 为什么要rem

我们都知道当我们缩放浏览器窗口时，网页的内容会因为我们的而改变缩放。如果我们进行开发时，使用的是rem则采用rem进行设置大小的内容都会进行等比缩放，从而实现自适应效果。但如果我们使用px规定了大小，此时我们进行缩放或者使用不同分辨率的设备查看网页内容时，里面的字体的内容并不会进行一个等比缩放而是依然使用规定的像素值。

rem的出现很好的解决了不同分辨率下查看同一网页字体不会自适应的问题。

相比于em仅依靠父元素来设置文字大小，在实际开发中父元素文字大小可能不一致，但是整个页面只有一个html，使用rem可以很好来控制整个页面的元素大小。

## less

### 什么是less

为解决css代码冗余，无法使用变量、函数等概念的问题。引入less简化css代码的开发。

Less （Leaner Style Sheets 的缩写） 是一门 CSS 扩展语言，也称为CSS预处理器。

### 典型场景

```css
当我们需要在不同选择器重复使用同一属性并对其修改时，需要一个一个找到相应属性然后修改十分麻烦，如下情形

div {
    backgroud-color:green;
}

body {
    background-color:green;
}

我们要同时修改背景颜色时，需要一个一个去修改很麻烦。若我们使用less开发，则可以同时修改背景颜色，只需如下

@Color:black;

div {
    backgroud-color:@Color;
}

body {
    background-color:@Color;
}

改变变量Color的值即可同时修改body 和 div 的背景颜色为black

可以将其理解为一个全局变量
```

#### 基础语法

##### 定义变量

```less
@varname:value; 
```

注意事项

* 必须有@为前缀
* 不能包含特殊字符
*  不能以数字开头
*  大小写敏感

##### 嵌套

直接选中div里的子元素span

```less
div {
    CSS-Code;
    span {
        CSS-Code;
    }
}
编译出的css代码为
div {
    CSS-Code;
}
div span {
    CSS-Code;
}
```

注意事项

* 如果遇见（交集|伪类|伪元素选择器）
* 内层选择器的前面没有&amp; 符号，则它被解析为父选择器的后代；
* 如果有&amp; 符号，它就被解析为父元素自身或父元素的伪类。

如，使用伪类选择器

```less
div {
    CSS-Code
    &amp;:hover {
        CSS-Code
    }
}
编译出来的CSS代码为
div {
    CSS-Code
}
div:hover {
    CSS-Code
}
```



##### 运算

可使用的运算符有 + - * / ，运算符前后左右都要空格

运算优先级为先乘除后加减，从左往右

```less
width:50px - 5; //最终值为 45px 

若改变运算优先级，则使用()即可，如下

witdth:(50px + 10px) * 2; //最终值为 120px
```

注意事项

* 对于两个不同的单位的值之间的运算，运算结果的值取第一个值的单位

  ```less
  width: 10rem / 5px; //最终值为 2rem
  ```

* 如果两个值之间只有一个值有单位，则运算结果就取该单位

  

## 媒体查询

### 什么是媒体查询

媒体查询（Media Query）是CSS3新语法。 使用媒体查询可以令我们针对不同的设备开发不同的css样式用以适配，当查看网页的设备不同，就可以根据媒体查询的结果去使用不同的css样式以达到最佳的显示效果。

### 媒体查询使用

#### 基础范式

```css
检测设备分辨率
@media -mediatype- -and | not | only -(media feature)- {
	CSS-Code
}

根据设备分辨率引入文件
&lt;link rel=&quot;stylesheet&quot; media=&quot;mediatype and|not|only (media feature)&quot; href=&quot;mystylesheet.css&quot;&gt;
```

#### 注意事项

* 用 @media 开头 注意@符号 
* mediatype  媒体类型 
* 关键字 and  not   only
*  media feature 媒体特性 必须有小括号包含

#### 属性详解

mediatype 查询类型，将不同的终端设备划分成不同的类型，称为媒体类型，**一般使用 screen**

![image-20211007222508126](https://i.loli.net/2021/10/07/D8iPs1ZbQXMKA7I.png)

关键字 

* and：可以将多个媒体特性连接到一起，相当于“且”的意思。 
* not：排除某个媒体类型，相当于“非”的意思，可以省略。 
* only：指定某个特定的媒体类型，可以省略。

媒体特性

每种媒体类型都具体各自不同的特性，根据不同媒体类型的媒体特性设置不同的展示风格。我们暂且了解三个。注意他们要加小括号包含

![image-20211007222823943](https://i.loli.net/2021/10/07/E1rsOG5hjDNeFqB.png)

#### 使用实例

```css
当分辨率小于540px的设备查看时.使html字体大小为16px，且body变蓝
@media screen and (max-width:540px) {
	html {
		font-size:16px;
	}
	body {
		background-color:blue;
	}
}

当分辨率 540px &lt; X &lt; 970px 的设备查看时，使html字体大小为25px，且body变黑
@media screen and (min-width:540px) {
	html {
		font-size:25px;
	}
	body {
		background-color:black;
	}
}

当分辨率大于970px的设备查看时，使html字体大小为50px，且body变红
@media screen and (min-width:970px) {
	html {
		font-size:50px;
	}
	body {
		background-color:red;
	}
}

使用媒体查询引入不同css文件，当设备分辨率大于400px时引入styleA.css
&lt;link rel=&quot;stylesheet&quot; href=&quot;styleA.css&quot; media=&quot;screen and (min-width: 400px)&quot;&gt;
```

#### 实例分析

分析媒体查询的里的css层叠特性

![image-20211007224005323](https://i.loli.net/2021/10/07/GwktRdAie3QYoZM.png)</content:encoded><author>Asuhe</author></item></channel></rss>