<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Clloz&apos;s Blog</title><description>人生如逆旅，我亦是行人</description><link>https://clloz.com</link><item><title>Mihomo Mac 配置</title><link>https://clloz.com/blog/mihomo</link><guid isPermaLink="true">https://clloz.com/blog/mihomo</guid><description>Mihomo 在 Mac 的命令行工具配置</description><pubDate>Sun, 09 Nov 2025 22:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;Mac 上不想要使用 Clash 客户端，大部分都不维护，直接使用 launchd 配置 mihomo 的客户端启动代理，可以自己完全控制代理工具。&lt;/p&gt;
&lt;h2&gt;创建用户&lt;/h2&gt;
&lt;p&gt;mihomo 毕竟也是一个第三方程序，为了保证安全性还是创建一个独立的用户，用户名称带前置下划线是 mac 默认的系统级用户的通用做法&lt;/p&gt;
&lt;p&gt;| 类型              | UID/GID 范围 | 说明                                                                     |
| ----------------- | ------------ | ------------------------------------------------------------------------ |
| 系统保留用户      | 0~399        | macOS 自带系统用户，如 root (0)、_www、_spotlight 等；不要使用，会冲突 |
| 服务/守护进程用户 | 400~499      | 推荐范围，用于自建服务用户（隐藏）                                       |
| 普通用户          | 501~         | 正常登录用户，显示在“用户与群组”界面                                     |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 判断 UID 是否被占用
dscl . -list /Users UniqueID | grep 450

# 判断 GID 是否被占用
dscl . -list /Groups PrimaryGroupID | grep 450

# 列出所有 UID
dscl . -list /Users UniqueID

# 列出所有 GID
dscl . -list /Groups PrimaryGroupID

# 查看 UID 属于那个分组
id UID

# 创建 group
sudo dscl . -create /Groups/_clash
sudo dscl . -create /Groups/_clash RealName &quot;Clash Service Group&quot;
sudo dscl . -create /Groups/_clash gid 450

# 创建 user
sudo dscl . -create /Users/_clash
sudo dscl . -create /Users/_clash UserShell /usr/bin/false # 禁止该用户登录，使用 shell
sudo dscl . -create /Users/_clash RealName &quot;Clash Service&quot;
sudo dscl . -create /Users/_clash UniqueID 450
sudo dscl . -create /Users/_clash PrimaryGroupID 450
sudo dscl . -create /Users/_clash NFSHomeDirectory /var/empty # 设置家目录路径
sudo dscl . -create /Users/_clash IsHidden 1 # 不在系统设置，用户和分组中展示该分组

# 删除用户
sudo dscl . -delete /Users/_clash

# 可选：删除 home 目录或其他文件
sudo rm -rf /var/empty  # 如果 home 是 /var/empty

# 删除分组 确保没有其他用户使用该分组
sudo dscl . -delete /Groups/_clash
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;管理 clash&lt;/h2&gt;
&lt;p&gt;创建 &lt;code&gt;/opt/clash&lt;/code&gt; 文件夹用来管理所有 clash 相关的可执行文件，配置和日志，内部创建三个文件夹分别用来管理可执行文件，配置和日志&lt;/p&gt;
&lt;p&gt;最新的 mihomo &lt;a href=&quot;https://github.com/MetaCubeX/mihomo/releases/tag/v1.19.16&quot;&gt;下载地址&lt;/a&gt;，根据 &lt;a href=&quot;https://wiki.metacubex.one/startup/faq/&quot;&gt;官方文档&lt;/a&gt; MacOS 15 选择 go122 版本&lt;/p&gt;
&lt;p&gt;由于 mihomo 是未经第三方验证的程序，mac 默认的 Gatekeeper 默认会禁止我们运行该程序&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Apple could not verify “mihomo-darwin-arm64-go122-v1.19.15” is free of malware that may harm your Mac or compromise your privacy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;遇到这种情况到 System Settings -&gt; Privacy &amp;#x26; Security 中滑到最下面点击 run anyway&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 创建文件夹
sudo mkdir -p /opt/clash/{bin,config,logs}
sudo mkdir /opt/clash/config/ui

# 下载最新版本的 mihomo
curl -L \
  -o mihomo-darwin-arm64-go122-v1.19.16.gz \
  https://github.com/MetaCubeX/mihomo/releases/download/v1.19.16/mihomo-darwin-arm64-go122-v1.19.16.gz

# 解压
gunzip mihomo-darwin-arm64-go122-v1.19.16.gz

# 创建软链接
sudo ln -sf /opt/clash/bin/mihomo-darwin-arm64-latest /opt/clash/bin/clash-target

# 验证软链接
ls -la /opt/clash/bin/clash-target

# 验证新版本
sudo -u _clash /opt/clash/bin/clash-target -v

# 设置正确的权限，确保二进制文件可执行
sudo chmod +x /opt/clash/bin/mihomo
sudo chmod +x /opt/clash/bin/clash-target

# 配置用户和分组
sudo chown root:wheel /opt/clash/bin/
sudo chown -R _clash:_clash /opt/clash/config/ /opt/clash/logs/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终的目录结构如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;/opt/clash/
├── bin/
│ ├── mihomo-darwin-arm64-go122-v1.19.16 # Mihomo 可执行文件
│ ├── bin # 自定义脚本
│ └── clash-target -&gt; /opt/clash/bin/mihomo-darwin-arm64-go122-v1.19.16 # 软链接
├── config/
│ └── config.yaml # Clash 配置文件
└── logs/
├── clash.log # 标准输出日志
└── clash-error.log # 标准错误日
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更新 mihomo 版本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 下载最新版本的 mihomo
curl -L \
  -o mihomo-darwin-arm64-go122-v1.19.16.gz \
  https://github.com/MetaCubeX/mihomo/releases/download/v1.19.16/mihomo-darwin-arm64-go122-v1.19.16.gz

# 解压
gunzip mihomo-darwin-arm64-go122-v1.19.16.gz

# 创建软链接
sudo ln -sf /opt/clash/bin/mihomo-darwin-arm64-latest /opt/clash/bin/clash-target

# 验证软链接
ls -la /opt/clash/bin/clash-target

# 验证新版本
sudo -u _clash /opt/clash/bin/clash-target -v

# 重启 mihomo
clash restart
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;mihomo 配置&lt;/h2&gt;
&lt;p&gt;参考官方的配置文件 &lt;a href=&quot;https://github.com/MetaCubeX/mihomo/blob/Meta/docs/config.yaml&quot;&gt;config.yaml&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 检查配置是否合法
sudo -u nobody /opt/clash/bin/clash-target -t -d /opt/clash/config/

# 配置加载过程中可能由于网络问题导致 geoip.dat geoip.metadb geosite.dat 下载失败，可以手动下载然后复制到 /opt/clash/config 目录下
# 或者尝试 设置 config.yaml 中的 ipv6: false 看是否能下载成功
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自定义 clash 脚本&lt;/h2&gt;
&lt;p&gt;创建一个 clash 脚本能够快速执行一些命令管理 clash&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;touch /opt/clash/bin/clash
sudo chmod +x /opt/clash/bin/clash

# 添加到系统环境变量
export PATH=&quot;/opt/clash/bin:$PATH&quot;

# 也可以使用短链接
sudo ln -s /opt/clash/bin/clash /usr/local/bin/clash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的脚本实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/zsh
set -euo pipefail

# Clash Management Script
# Location: /opt/clash/bin/clash
# This script provides comprehensive control for Mihomo/Clash running via launchd

CLASH_BIN=&quot;/opt/clash/bin/clash-target&quot;
CONFIG_DIR=&quot;/opt/clash/config&quot;
CONFIG_FILE=&quot;$CONFIG_DIR/config.yaml&quot;
LOG_DIR=&quot;/opt/clash/logs&quot;
LAUNCHD_PLIST=&quot;/Library/LaunchDaemons/com.user.clash.plist&quot;

# 定义颜色输出
RED=&apos;\033[0;31m&apos;
GREEN=&apos;\033[0;32m&apos;
YELLOW=&apos;\033[1;33m&apos;
BLUE=&apos;\033[0;34m&apos;
NC=&apos;\033[0m&apos; # No Color

# 基础函数
print_status() { echo -e &quot;${BLUE}[INFO]${NC} $1&quot;; }
print_success() { echo -e &quot;${GREEN}[SUCCESS]${NC} $1&quot;; }
print_warning() { echo -e &quot;${YELLOW}[WARNING]${NC} $1&quot;; }
print_error() { echo -e &quot;${RED}[ERROR]${NC} $1&quot;; }

# 检查服务是否运行
is_service_running() {
    # 方法1（推荐）：直接检查是否有 clash-target 进程在运行
    if pgrep -f &quot;clash-target&quot; &gt; /dev/null; then
        return 0
    else
        return 1
    fi
}

# 检查启动项是否启用
is_launchd_enabled() {
    launchctl print system/com.user.clash &amp;#x26;&gt;/dev/null &amp;#x26;&amp;#x26; return 0 || return 1
}

# 验证配置文件
validate_config() {
    local validation_result

    if [[ ! -f &quot;$CLASH_BIN&quot; ]]; then
        print_error &quot;Clash binary not found: $CLASH_BIN&quot;
        return 1
    fi

    if [[ ! -f &quot;$CONFIG_FILE&quot; ]]; then
        print_error &quot;Config file not found: $CONFIG_FILE&quot;
        return 1
    fi

    # 使用更明确的错误处理
    if validation_result=$(sudo -u nobody &quot;$CLASH_BIN&quot; -t -d &quot;$CONFIG_DIR&quot; 2&gt;&amp;#x26;1); then
        print_success &quot;Configuration validation passed&quot;
        return 0
    else
        print_error &quot;Configuration validation failed: $validation_result&quot;
        return 1
    fi
}

# 检查系统代理状态
is_system_proxy_enabled() {
    local http_proxy=$(networksetup -getwebproxy &quot;Wi-Fi&quot; | grep &quot;Enabled: Yes&quot;)
    local https_proxy=$(networksetup -getsecurewebproxy &quot;Wi-Fi&quot; | grep &quot;Enabled: Yes&quot;)

    if [[ -n &quot;$http_proxy&quot; &amp;#x26;&amp;#x26; -n &quot;$https_proxy&quot; ]]; then
        return
    else
        return 1
    fi
}

# 主功能函数
clash_enable() {
    print_status &quot;Enabling clash auto-start on boot...&quot;
    if sudo launchctl load -w &quot;$LAUNCHD_PLIST&quot; 2&gt;/dev/null; then
        print_success &quot;Clash auto-start enabled&quot;
    else
        print_error &quot;Failed to enable auto-start&quot;
    fi
}

clash_disable() {
    print_status &quot;Disabling clash auto-start...&quot;
    if sudo launchctl unload -w &quot;$LAUNCHD_PLIST&quot; 2&gt;/dev/null; then
        print_success &quot;Clash auto-start disabled&quot;
    else
        print_error &quot;Failed to disable auto-start&quot;
    fi
}

clash_status() {
    echo &quot;=== Clash Service Status ===&quot;

    # 检查启动项状态
    if is_launchd_enabled; then
        echo -e &quot;Auto-start: ${GREEN}Enabled${NC}&quot;
    else
        echo -e &quot;Auto-start: ${RED}Disabled${NC}&quot;
    fi

    # 检查服务运行状态
    if is_service_running; then
        echo -e &quot;Service: ${GREEN}Running${NC}&quot;
    else
        echo -e &quot;Service: ${RED}Stopped${NC}&quot;
    fi

    # 检查系统代理状态
    if is_system_proxy_enabled; then
        echo -e &quot;System Proxy: ${GREEN}Enabled${NC}&quot;
    else
        echo -e &quot;System Proxy: ${RED}Disabled${NC}&quot;
    fi

    # 显示文件路径
    echo &quot;Config Path: $CONFIG_FILE&quot;
    echo &quot;Log Path: $LOG_DIR&quot;
    echo &quot;Binary Path: $CLASH_BIN&quot;

    # 显示最新日志
    if [[ -f &quot;$LOG_DIR/clash.log&quot; ]]; then
        echo -e &quot;\n=== Recent Logs ===&quot;
        tail -5 &quot;$LOG_DIR/clash.log&quot; 2&gt;/dev/null || echo &quot;No recent logs found&quot;
    fi
}

clash_start() {
    if is_service_running; then
        print_warning &quot;Clash is already running&quot;
        return
    fi

    print_status &quot;Starting clash service...&quot;
    validate_config || return 1

    sudo launchctl start com.user.clash
    sleep 2

    if is_service_running; then
        print_success &quot;Clash started successfully&quot;
    else
        print_error &quot;Failed to start clash&quot;
    fi
}

clash_stop() {
    if ! is_service_running; then
        print_warning &quot;Clash is not running&quot;
        return
    fi

    print_status &quot;Stopping clash service...&quot;
    sudo launchctl stop com.user.clash

    sleep 2
    if ! is_service_running; then
        print_success &quot;Clash stopped successfully&quot;
    else
        print_error &quot;Failed to stop clash&quot;
    fi
}

clash_restart() {
    clash_stop
    clash_start
}

# 需要设置所有网络的代理才能让代理生效，实测仅设置 Wifi 的代理，虽然命令行能正常走代理但是浏览器还是无法使用
clash_on() {
    print_status &quot;Enabling system proxy for all network services...&quot;

    # 获取当前网络端口(从配置文件中提取)
    local proxy_port=$(grep -E &quot;^mixed-port:&quot; &quot;$CONFIG_FILE&quot; 2&gt;/dev/null | awk &apos;{print $2}&apos;)
    if [[ -z &quot;$proxy_port&quot; ]]; then
        proxy_port=7890  # 默认端口
    fi

    # 1. 获取所有网络服务列表
    # 使用 networksetup 命令列出所有服务，并过滤掉已禁用的服务(名称前带*号)
    local network_services
    network_services=$(networksetup -listallnetworkservices | tail -n +2 | grep -v &apos;^\*&apos;)

    # 2. 遍历每个网络服务并设置代理
    echo &quot;$network_services&quot; | while read -r service; do
        # 跳过空行
        if [[ -z &quot;$service&quot; ]]; then
            continue
        fi

        print_status &quot;Setting proxy for: $service&quot;

        # 设置HTTP/HTTPS/SOCKS代理
        sudo networksetup -setwebproxy &quot;$service&quot; 127.0.0.1 &quot;$proxy_port&quot;
        sudo networksetup -setsecurewebproxy &quot;$service&quot; 127.0.0.1 &quot;$proxy_port&quot;
        sudo networksetup -setsocksfirewallproxy &quot;$service&quot; 127.0.0.1 &quot;$proxy_port&quot;

        # 启用代理
        sudo networksetup -setwebproxystate &quot;$service&quot; on
        sudo networksetup -setsecurewebproxystate &quot;$service&quot; on
        sudo networksetup -setsocksfirewallproxystate &quot;$service&quot; on
    done

    print_success &quot;System proxy enabled on port $proxy_port for all network services&quot;
}

clash_off() {
    print_status &quot;Disabling system proxy for all network services...&quot;

    # 获取所有网络服务列表
    local network_services
    network_services=$(networksetup -listallnetworkservices | tail -n +2 | grep -v &apos;^\*&apos;)

    # 遍历每个网络服务并关闭代理
    echo &quot;$network_services&quot; | while read -r service; do
        # 跳过空行
        if [[ -z &quot;$service&quot; ]]; then
            continue
        fi

        print_status &quot;Disabling proxy for: $service&quot;

        sudo networksetup -setwebproxystate &quot;$service&quot; off
        sudo networksetup -setsecurewebproxystate &quot;$service&quot; off
        sudo networksetup -setsocksfirewallproxystate &quot;$service&quot; off
    done

    print_success &quot;System proxy disabled for all network services&quot;
}

clash_config_edit() {
    if [[ ! -f &quot;$CONFIG_FILE&quot; ]]; then
        print_error &quot;Config file not found: $CONFIG_FILE&quot;
        return 1
    fi

    # 备份原配置
    cp &quot;$CONFIG_FILE&quot; &quot;$CONFIG_FILE.bak&quot;
    print_status &quot;Backup created: $CONFIG_FILE.bak&quot;

    # 使用vim编辑
    vim &quot;$CONFIG_FILE&quot;

    # 验证编辑后的配置
    if validate_config; then
        print_success &quot;Configuration updated and validated&quot;
        # 询问是否重启服务
        read -q &quot;REPLY?Restart clash to apply changes? (y/n): &quot;
        echo
        if [[ $REPLY =~ ^[Yy]$ ]]; then
            clash_restart
        fi
    else
        print_error &quot;Configuration validation failed, restoring backup...&quot;
        cp &quot;$CONFIG_FILE.bak&quot; &quot;$CONFIG_FILE&quot;
        return 1
    fi
}

clash_config_set() {
    local field=&quot;$1&quot;
    local value=&quot;$2&quot;

    if [[ -z &quot;$field&quot; || -z &quot;$value&quot; ]]; then
        print_error &quot;Usage: clash config-set &amp;#x3C;field&gt; &amp;#x3C;value&gt;&quot;
        return 1
    fi

    if [[ ! -f &quot;$CONFIG_FILE&quot; ]]; then
        print_error &quot;Config file not found&quot;
        return 1
    fi

    # 备份原配置
    cp &quot;$CONFIG_FILE&quot; &quot;$CONFIG_FILE.bak&quot;

    # 使用yq工具修改配置(需要先安装yq)
    if command -v yq &gt;/dev/null 2&gt;&amp;#x26;1; then
        yq e &quot;.$field = \&quot;$value\&quot;&quot; -i &quot;$CONFIG_FILE&quot;
    else
        # 如果没有yq，使用sed简单替换（适用于简单字段）
        if grep -q &quot;^$field:&quot; &quot;$CONFIG_FILE&quot;; then
            sed -i.bak &quot;s/^$field:.*/$field: $value/&quot; &quot;$CONFIG_FILE&quot;
        else
            # 如果字段不存在，添加到文件末尾
            echo &quot;$field: $value&quot; &gt;&gt; &quot;$CONFIG_FILE&quot;
        fi
    fi

    # 验证配置
    if validate_config; then
        print_success &quot;Configuration updated: $field = $value&quot;
    else
        print_error &quot;Configuration validation failed, restoring backup...&quot;
        cp &quot;$CONFIG_FILE.bak&quot; &quot;$CONFIG_FILE&quot;
        return 1
    fi
}

# 模式设置函数
clash_mode_rule() { clash_config_set &quot;mode&quot; &quot;rule&quot; &amp;#x26;&amp;#x26; clash_restart; }
clash_mode_global() { clash_config_set &quot;mode&quot; &quot;global&quot; &amp;#x26;&amp;#x26; clash_restart; }
clash_mode_direct() { clash_config_set &quot;mode&quot; &quot;direct&quot; &amp;#x26;&amp;#x26; clash_restart; }

# TUN模式设置函数
clash_tun_on() {
    clash_config_set &quot;tun.enable&quot; &quot;true&quot; &amp;#x26;&amp;#x26; clash_restart;
}

clash_tun_off() {
    clash_config_set &quot;tun.enable&quot; &quot;false&quot; &amp;#x26;&amp;#x26; clash_restart;
}

clash_validate() {
    print_status &quot;Validating clash configuration...&quot;

    # 检查二进制文件是否存在
    if [[ ! -f &quot;$CLASH_BIN&quot; ]]; then
        print_error &quot;Clash binary not found: $CLASH_BIN&quot;
        return 1
    fi

    # 检查配置文件是否存在
    if [[ ! -f &quot;$CONFIG_FILE&quot; ]]; then
        print_error &quot;Config file not found: $CONFIG_FILE&quot;
        return 1
    fi

    # 检查配置文件语法
    echo &quot;=== Configuration Syntax Check ===&quot;
    if validation_result=$(sudo -u nobody &quot;$CLASH_BIN&quot; -t -d &quot;$CONFIG_DIR&quot; 2&gt;&amp;#x26;1); then
        print_success &quot;✓ Configuration syntax is valid&quot;
    else
        print_error &quot;✗ Configuration syntax error:&quot;
        echo &quot;$validation_result&quot;
        return 1
    fi

    # 检查关键配置项
    echo -e &quot;\n=== Key Configuration Items ===&quot;

    # 检查端口配置
    local mixed_port=$(grep -E &quot;^mixed-port:&quot; &quot;$CONFIG_FILE&quot; 2&gt;/dev/null | awk &apos;{print $2}&apos;)
    local socks_port=$(grep -E &quot;^socks-port:&quot; &quot;$CONFIG_FILE&quot; 2&gt;/dev/null | awk &apos;{print $2}&apos;)
    local external_port=$(grep -E &quot;^external-controller:&quot; &quot;$CONFIG_FILE&quot; 2&gt;/dev/null | awk -F: &apos;{print $3}&apos;)

    echo -e &quot;Mixed Port: ${mixed_port:-7890 (default)}&quot;
    echo -e &quot;SOCKS Port: ${socks_port:-Not set}&quot;
    echo -e &quot;External Controller: ${external_port:-9090 (default)}&quot;

    # 检查模式设置
    local mode=$(grep -E &quot;^mode:&quot; &quot;$CONFIG_FILE&quot; 2&gt;/dev/null | awk &apos;{print $2}&apos;)
    echo -e &quot;Mode: ${mode:-rule (default)}&quot;

    # 检查代理组和规则
    echo -e &quot;\n=== Proxies and Rules ===&quot;
    # 统计 proxies 下的节点数量
    local proxy_count=$(yq e &apos;.proxies | length&apos; &quot;$CONFIG_FILE&quot; 2&gt;/dev/null || echo &quot;0&quot;)

    # 统计 rules 下的规则数量
    local rule_count=$(yq e &apos;.rules | length&apos; &quot;$CONFIG_FILE&quot; 2&gt;/dev/null || echo &quot;0&quot;)

    echo -e &quot;Proxy count: $proxy_count&quot;
    echo -e &quot;Rule count: $rule_count&quot;

    # 检查DNS配置
    local dns_enabled=$(grep -A5 &quot;^dns:&quot; &quot;$CONFIG_FILE&quot; 2&gt;/dev/null | grep -q &quot;enable: true&quot; &amp;#x26;&amp;#x26; echo &quot;Enabled&quot; || echo &quot;Disabled&quot;)
    echo -e &quot;DNS: $dns_enabled&quot;

    # 检查TUN配置
    local tun_enabled=$(grep -A5 &quot;^tun:&quot; &quot;$CONFIG_FILE&quot; 2&gt;/dev/null | grep -q &quot;enable: true&quot; &amp;#x26;&amp;#x26; echo &quot;Enabled&quot; || echo &quot;Disabled&quot;)
    echo -e &quot;TUN: $tun_enabled&quot;

    # 检查文件权限
    echo -e &quot;\n=== File Permissions ===&quot;
    if [[ -r &quot;$CONFIG_FILE&quot; ]]; then
        print_success &quot;✓ Config file is readable&quot;
    else
        print_error &quot;✗ Config file is not readable&quot;
    fi

    if [[ -x &quot;$CLASH_BIN&quot; ]]; then
        print_success &quot;✓ Binary file is executable&quot;
    else
        print_error &quot;✗ Binary file is not executable&quot;
    fi

    # 检查端口占用情况
    echo -e &quot;\n=== Port Availability ===&quot;
    if [[ -n &quot;$mixed_port&quot; ]]; then
        if sudo lsof -i :$mixed_port &gt;/dev/null 2&gt;&amp;#x26;1; then
            print_warning &quot;⚠ Port $mixed_port is already in use&quot;
        else
            print_success &quot;✓ Port $mixed_port is available&quot;
        fi
    fi

    if [[ -n &quot;$external_port&quot; ]]; then
        if sudo lsof -i :$external_port &gt;/dev/null 2&gt;&amp;#x26;1; then
            print_warning &quot;⚠ External port $external_port is already in use&quot;
        else
            print_success &quot;✓ External port $external_port is available&quot;
        fi
    fi

    # 最终验证结果 - 修复后的逻辑
    echo -e &quot;\n=== Final Validation Result ===&quot;
    if sudo -u nobody &quot;$CLASH_BIN&quot; -t -d &quot;$CONFIG_DIR&quot; &gt;/dev/null 2&gt;&amp;#x26;1; then
        print_success &quot;🎉 Configuration validation passed! All checks are good.&quot;
        return 0
    else
        print_error &quot;❌ Configuration validation failed. Please check the errors above.&quot;
        return 1
    fi
}

# 主命令解析
case &quot;$1&quot; in
    &quot;enable&quot;)
        clash_enable
        ;;
    &quot;disable&quot;)
        clash_disable
        ;;
    &quot;status&quot;)
        clash_status
        ;;
    &quot;start&quot;)
        clash_start
        ;;
    &quot;stop&quot;)
        clash_stop
        ;;
    &quot;restart&quot;)
        clash_restart
        ;;
    &quot;on&quot;)
        clash_on
        ;;
    &quot;off&quot;)
        clash_off
        ;;
    &quot;valid&quot;)
        clash_validate
        ;;
    &quot;config-edit&quot;)
        clash_config_edit
        ;;
    &quot;config-set&quot;)
        clash_config_set &quot;$2&quot; &quot;$3&quot;
        ;;
    &quot;mode-rule&quot;)
        clash_mode_rule
        ;;
    &quot;mode-global&quot;)
        clash_mode_global
        ;;
    &quot;mode-direct&quot;)
        clash_mode_direct
        ;;
    &quot;tun-on&quot;)
        clash_tun_on
        ;;
    &quot;tun-off&quot;)
        clash_tun_off
        ;;
    *)
        echo &quot;Clash Management Script&quot;
        echo &quot;Usage: clash {enable|disable|status|start|stop|restart|on|off|config-edit|config-set|mode-rule|mode-global|mode-direct|tun-on|tun-off}&quot;
        echo &quot;&quot;
        echo &quot;Commands:&quot;
        echo &quot;  enable        Enable auto-start on boot&quot;
        echo &quot;  disable       Disable auto-start&quot;
        echo &quot;  status        Show service status and information&quot;
        echo &quot;  start         Start clash service&quot;
        echo &quot;  stop          Stop clash service&quot;
        echo &quot;  restart       Restart clash service&quot;
        echo &quot;  on            Enable system proxy&quot;
        echo &quot;  off           Disable system proxy&quot;
        echo &quot;  valid      Validate configuration file and check for issues&quot;
        echo &quot;  config-edit   Edit configuration with vim&quot;
        echo &quot;  config-set    &amp;#x3C;field&gt; &amp;#x3C;value&gt;  Set specific configuration field&quot;
        echo &quot;  mode-rule     Set mode to Rule and restart&quot;
        echo &quot;  mode-global   Set mode to Global and restart&quot;
        echo &quot;  mode-direct   Set mode to Direct and restart&quot;
        echo &quot;  tun-on        Enable TUN mode and restart&quot;
        echo &quot;  tun-off       Disable TUN mode and restart&quot;
        exit 1
        ;;
esac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以使用 clash -h 命令查看用法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Clash Management Script
Usage: clash {enable|disable|status|start|stop|restart|on|off|config-edit|config-set|mode-rule|mode-global|mode-direct|tun-on|tun-off}

Commands:
enable Enable auto-start on boot
disable Disable auto-start
status Show service status and information
start Start clash service
stop Stop clash service
restart Restart clash service
on Enable system proxy
off Disable system proxy
valid Validate configuration file and check for issues
config-edit Edit configuration with vim
config-set &amp;#x3C;field&gt; &amp;#x3C;value&gt; Set specific configuration field
mode-rule Set mode to Rule and restart
mode-global Set mode to Global and restart
mode-direct Set mode to Direct and restart
tun-on Enable TUN mode and restart
tun-off Disable TUN mode and restart
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;launchd 配置&lt;/h2&gt;
&lt;p&gt;需要创建一个系统级的 plist 来管理 clash 的开机启动&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo touch com.user.clash.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;plist 具体内容如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&amp;#x3C;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&amp;#x3C;plist version=&quot;1.0&quot;&gt;
&amp;#x3C;dict&gt;
    &amp;#x3C;key&gt;Label&amp;#x3C;/key&gt;
    &amp;#x3C;string&gt;com.user.clash&amp;#x3C;/string&gt;

    &amp;#x3C;!-- 传递给程序的参数：-d 指定配置目录 --&gt;
    &amp;#x3C;key&gt;ProgramArguments&amp;#x3C;/key&gt;
    &amp;#x3C;array&gt;
        &amp;#x3C;string&gt;/opt/clash/bin/clash-target&amp;#x3C;/string&gt;
        &amp;#x3C;string&gt;-d&amp;#x3C;/string&gt;
        &amp;#x3C;string&gt;/opt/clash/config&amp;#x3C;/string&gt;
    &amp;#x3C;/array&gt;

    &amp;#x3C;!-- 核心修改：指定运行身份为 _clash 用户和组 --&gt;
    &amp;#x3C;key&gt;UserName&amp;#x3C;/key&gt;
    &amp;#x3C;string&gt;_clash&amp;#x3C;/string&gt;
    &amp;#x3C;key&gt;GroupName&amp;#x3C;/key&gt;
    &amp;#x3C;string&gt;_clash&amp;#x3C;/string&gt;

    &amp;#x3C;!-- 设置工作目录 --&gt;
    &amp;#x3C;key&gt;WorkingDirectory&amp;#x3C;/key&gt;
    &amp;#x3C;string&gt;/opt/clash&amp;#x3C;/string&gt;

    &amp;#x3C;!-- 在加载服务时立即启动 --&gt;
    &amp;#x3C;key&gt;RunAtLoad&amp;#x3C;/key&gt;
    &amp;#x3C;true/&gt;

    &amp;#x3C;!-- 配置基于网络状态的保活 --&gt;
    &amp;#x3C;key&gt;KeepAlive&amp;#x3C;/key&gt;
    &amp;#x3C;dict&gt;
        &amp;#x3C;key&gt;NetworkState&amp;#x3C;/key&gt;
        &amp;#x3C;true/&gt;
    &amp;#x3C;/dict&gt;

    &amp;#x3C;!-- 标准输出和错误输出到日志文件 --&gt;
    &amp;#x3C;key&gt;StandardOutPath&amp;#x3C;/key&gt;
    &amp;#x3C;string&gt;/opt/clash/logs/clash.log&amp;#x3C;/string&gt;
    &amp;#x3C;key&gt;StandardErrorPath&amp;#x3C;/key&gt;
    &amp;#x3C;string&gt;/opt/clash/logs/clash-error.log&amp;#x3C;/string&gt;

    &amp;#x3C;!-- 关于 LimitLoadToSessionType 的解释见下文 --&gt;
    &amp;#x3C;key&gt;LimitLoadToSessionType&amp;#x3C;/key&gt;
    &amp;#x3C;array&gt;
        &amp;#x3C;string&gt;System&amp;#x3C;/string&gt;
        &amp;#x3C;!-- 对于需要与任意用户图形会话交互的图形界面程序，可考虑添加 &quot;Aqua&quot; --&gt;
        &amp;#x3C;!-- 但对于 Mihomo 这种网络代理守护进程，通常只使用 &quot;System&quot; --&gt;
    &amp;#x3C;/array&gt;
&amp;#x3C;/dict&gt;
&amp;#x3C;/plist&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 将 plist 文件复制到系统守护进程目录
sudo cp com.user.clash.plist /Library/LaunchDaemons/

# 2. 确保文件权限正确（root 用户和 wheel 组拥有所有权）
sudo chown root:wheel /Library/LaunchDaemons/com.user.mihomo.plist
sudo chmod 644 /Library/LaunchDaemons/com.user.mihomo.plist # 通常plist文件权限为644

# 3. 确保 /opt/clash 目录及其下文件对运行用户（nobody）有适当的访问权限
# 一种简单的方式是将配置目录的读权限赋予其他用户，但更安全的方法是更改文件所有者或设置更精细的ACL
sudo chmod o+r /opt/clash/config/config.yaml # 让其他用户能读配置（如果配置不含敏感信息）

# 4. 加载服务配置使其生效
sudo launchctl load -w /Library/LaunchDaemons/com.user.mihomo.plist

# 5. 如果服务没有因 RunAtLoad 而自动启动，或者您想立即启动它，可以使用：
sudo launchctl start com.user.mihomo

# 如果使用上面的 /bin/clash 命令
clash enable # 设置开机启动并立即启动
clash on # 设为系统代理
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还需要用一个用户级的 plist 自动启动设置系统代理，否则每次重启后都需要重新运行 &lt;code&gt;clash on&lt;/code&gt; 来开启系统代理&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&amp;#x3C;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&amp;#x3C;plist version=&quot;1.0&quot;&gt;
&amp;#x3C;dict&gt;
    &amp;#x3C;key&gt;Label&amp;#x3C;/key&gt;
    &amp;#x3C;string&gt;com.user.clash-on&amp;#x3C;/string&gt;

    &amp;#x3C;key&gt;ProgramArguments&amp;#x3C;/key&gt;
    &amp;#x3C;array&gt;
        &amp;#x3C;string&gt;/opt/clash/bin/clash&amp;#x3C;/string&gt;  &amp;#x3C;!-- 使用绝对路径 --&gt;
        &amp;#x3C;string&gt;on&amp;#x3C;/string&gt;
    &amp;#x3C;/array&gt;

    &amp;#x3C;!-- 登录时自动执行 --&gt;
    &amp;#x3C;key&gt;RunAtLoad&amp;#x3C;/key&gt;
    &amp;#x3C;true/&gt;

    &amp;#x3C;!-- 登录 15 秒后执行 clash on --&gt;
    &amp;#x3C;key&gt;StartDelay&amp;#x3C;/key&gt;
    &amp;#x3C;integer&gt;5&amp;#x3C;/integer&gt;

    &amp;#x3C;!-- 只在图形界面会话中运行 --&gt;
    &amp;#x3C;key&gt;LimitLoadToSessionType&amp;#x3C;/key&gt;
    &amp;#x3C;array&gt;
        &amp;#x3C;string&gt;Aqua&amp;#x3C;/string&gt;
    &amp;#x3C;/array&gt;

    &amp;#x3C;!-- 可选：日志输出 --&gt;
    &amp;#x3C;key&gt;StandardOutPath&amp;#x3C;/key&gt;
    &amp;#x3C;string&gt;/tmp/clash-on.log&amp;#x3C;/string&gt;
    &amp;#x3C;key&gt;StandardErrorPath&amp;#x3C;/key&gt;
    &amp;#x3C;string&gt;/tmp/clash-on-error.log&amp;#x3C;/string&gt;
&amp;#x3C;/dict&gt;
&amp;#x3C;/plist&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo touch com.user.clash-on.plist

sudo cp com.user.clash-on.plist ~/Library/LaunchAgents/

chown $(whoami):staff ~/Library/LaunchAgents/com.user.clash-on.plist

# 运行，不要使用 sudo 会切换到 root
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.user.clash-on.plist
launchctl enable gui/$UID/com.user.clash-on

# 卸载 禁用
launchctl bootout gui/$UID ~/Library/LaunchAgents/com.user.clash-on.plist
launchctl disable gui/$UID/com.user.clash-on

&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.HAXFr_hw.jpg"/><enclosure url="/_astro/thumbnail.HAXFr_hw.jpg"/></item><item><title>Tips to improve concentration</title><link>https://clloz.com/blog/improve-concentration</link><guid isPermaLink="true">https://clloz.com/blog/improve-concentration</guid><description>Mindfulness, cognitive training, and a healthy lifestyle may help sharpen your focus.</description><pubDate>Sat, 10 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure-clloz/user&apos;&lt;/p&gt;
&lt;p&gt;You&apos;re trying to concentrate, but your mind is wandering or you&apos;re easily distracted. What happened to the laser-sharp focus you once enjoyed? As we age, we tend to have more difficulty filtering out stimuli that are not relevant to the task at hand.&lt;/p&gt;
&lt;h2&gt;What&apos;s fogging up focus?&lt;/h2&gt;
&lt;p&gt;Like a computer that slows with use, the brain accumulates wear and tear that affects processing. This can be caused by a number of physiological stressors such as inflammation, injury to blood vessels (especially if you have high blood pressure), the buildup of abnormal proteins, and naturally occurring brain shrinkage.&lt;/p&gt;
&lt;p&gt;The following factors can also affect your concentration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Underlying conditions.&lt;/strong&gt; Depression or sleep disorders (such as sleep apnea) can undermine your ability to concentrate. So can the effects of vision or hearing loss. You waste precious cognitive resources when you spend too much time trying to make out what&apos;s written on a page or just hear what someone is saying.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Medication side effects.&lt;/strong&gt; Some drugs, especially anticholinergics (such as treatments for incontinence, depression, or allergies), can slow processing speed and your ability to think clearly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Excessive drinking.&lt;/strong&gt; Having too much alcohol impairs thinking and causes interrupted sleep, which affects concentration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Information overload.&lt;/strong&gt; We are bombarded with information from TVs, computers, and messages such as texts or emails. When there&apos;s too much material, it burdens our filtering system and it&apos;s easy to get distracted.&lt;/p&gt;
&lt;h2&gt;Strategies to stay focused&lt;/h2&gt;
&lt;p&gt;To improve attention, consider the following strategies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mindfulness.&lt;/strong&gt; &quot;Mindfulness is about focusing attention on the present moment, and practicing mindfulness has been shown to rewire the brain so that attention is stronger in everyday life,&quot; says Kim Willment, a neuropsychologist with Brigham and Women&apos;s Hospital. She recommends sitting still for a few minutes each day, closing your eyes, and focusing on your breathing as well as the sounds and sensations around you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cognitive training.&lt;/strong&gt; Computerized cognitive training games aim to improve your response times and attention. Evidence that this works has been mixed. &quot;The goal of playing these games is not to get better at them, but to get better in the cognitive activities of everyday life,&quot; Willment says. &quot;But there is evidence that a person&apos;s ability to pay attention can be improved by progressively pushing the person to higher levels of performance. So if you reach a certain level of sustained attention, pushing it to the next level can help improve it, and this may translate to everyday life.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A healthier lifestyle.&lt;/strong&gt; Many aspects of a healthy lifestyle can help attention, starting with sleep and exercise. There is a direct link between exercise and cognitive ability, especially attention. When you exercise, you increase the availability of brain chemicals that promote new brain connections, reduce stress, and improve sleep. And when we sleep, we reduce stress hormones that can be harmful to the brain, and we clear out proteins that injure it.&lt;/p&gt;
&lt;p&gt;Aim for seven to eight hours of sleep each night, and 150 minutes per week of aerobic exercise, such as brisk walking.&lt;/p&gt;
&lt;p&gt;Other healthy steps to improve focus: eat a Mediterranean-style diet, which has been shown to support brain health; treat underlying conditions; and change medications that may be affecting your ability to focus.&lt;/p&gt;
&lt;p&gt;Getting older is out of your control, but healthier living is something you determine, and it may improve concentration.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Article from: &lt;a href=&quot;https://www.health.harvard.edu/mind-and-mood/tips-to-improve-concentration&quot;&gt;Harvard Health Publishing&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="/_astro/thumbnail.1GZ294Dz.jpg"/><enclosure url="/_astro/thumbnail.1GZ294Dz.jpg"/></item><item><title>Using MDX</title><link>https://clloz.com/blog/using-mdx</link><guid isPermaLink="true">https://clloz.com/blog/using-mdx</guid><description>Learning how to use MDX in Astro</description><pubDate>Sun, 01 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This theme comes with the &lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/mdx/&quot;&gt;@astrojs/mdx&lt;/a&gt; integration installed and configured in your &lt;code&gt;astro.config.mjs&lt;/code&gt; config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.&lt;/p&gt;
&lt;h2&gt;Why MDX?&lt;/h2&gt;
&lt;p&gt;MDX is a special flavor of Markdown that supports embedded JavaScript &amp;#x26; JSX syntax. This unlocks the ability to &lt;a href=&quot;https://docs.astro.build/en/guides/markdown-content/#mdx-features&quot;&gt;mix JavaScript and UI Components into your Markdown content&lt;/a&gt; for things like interactive charts or alerts.&lt;/p&gt;
&lt;p&gt;If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.&lt;/p&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;p&gt;Here is how you import and use a UI component inside of MDX.&lt;br&gt;
When you open this page in the browser, you should see the clickable button below.&lt;/p&gt;
&lt;p&gt;import { Button } from &apos;astro-pure-clloz/user&apos;&lt;/p&gt;
&lt;p&gt;Click Me&lt;/p&gt;
&lt;h2&gt;More Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mdxjs.com/docs/what-is-mdx&quot;&gt;MDX Syntax Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages&quot;&gt;Astro Usage Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;a href=&quot;https://docs.astro.build/en/reference/directives-reference/#client-directives&quot;&gt;Client Directives&lt;/a&gt; are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Markdown Syntax Support</title><link>https://clloz.com/blog/markdown</link><guid isPermaLink="true">https://clloz.com/blog/markdown</guid><description>Markdown is a lightweight markup language.</description><pubDate>Wed, 26 Jul 2023 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Basic Syntax&lt;/h2&gt;
&lt;p&gt;Markdown is a lightweight and easy-to-use syntax for styling your writing.&lt;/p&gt;
&lt;h3&gt;Headers&lt;/h3&gt;
&lt;p&gt;When the content of the article is extensive, you can use headers to segment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# Header 1

## Header 2

## Large Header

### Small Header
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Header previews would disrupt the structure of the article, so they are not displayed here.&lt;/p&gt;
&lt;h3&gt;Bold and Italics&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;_Italic text_ and **Bold text**, together will be **_Bold Italic text_**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Italic text&lt;/em&gt; and &lt;strong&gt;Bold text&lt;/strong&gt;, together will be &lt;strong&gt;&lt;em&gt;Bold Italic text&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Links&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Text link [Link Name](http://link-url)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;Text link &lt;a href=&quot;http://link-url&quot;&gt;Link Name&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Inline Code&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;This is an `inline code`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;This is an &lt;code&gt;inline code&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Code Blocks&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;```js
// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  const result = fibonacci(n - 1) + fibonacci(n - 2) // [\!code --]
  return fibonacci(n - 1) + fibonacci(n - 2) // [\!code ++]
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  const result = fibonacci(n - 1) + fibonacci(n - 2) // [!code --]
  return fibonacci(n - 1) + fibonacci(n - 2) // [!code ++]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Currently using shiki as the code highlighting plugin. For supported languages, refer to &lt;a href=&quot;https://shiki.matsu.io/languages.html&quot;&gt;Shiki: Languages&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Inline Formula&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;This is an inline formula $e^{i\pi} + 1 = 0$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;This is an inline formula $e^{i\pi} + 1 = 0$&lt;/p&gt;
&lt;h3&gt;Formula Blocks&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} \, dx
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} , dx
$$&lt;/p&gt;
&lt;p&gt;Currently using KaTeX as the math formula plugin. For supported syntax, refer to &lt;a href=&quot;https://katex.org/docs/supported.html&quot;&gt;KaTeX Supported Functions&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Images&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;![CWorld](https://cravatar.cn/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cravatar.cn/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200&quot; alt=&quot;CWorld&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Strikethrough&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;~~Strikethrough~~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;~~Strikethrough~~&lt;/p&gt;
&lt;h3&gt;Lists&lt;/h3&gt;
&lt;p&gt;Regular unordered list&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- 1
- 2
- 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;ul&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;p&gt;Regular ordered list&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;1. GPT-4
2. Claude Opus
3. LLaMa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GPT-4&lt;/li&gt;
&lt;li&gt;Claude Opus&lt;/li&gt;
&lt;li&gt;LLaMa&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can continue to nest syntax within lists.&lt;/p&gt;
&lt;h3&gt;Blockquotes&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&gt; Gunshot, thunder, sword rise. A scene of flowers and blood.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gunshot, thunder, sword rise. A scene of flowers and blood.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You can continue to nest syntax within blockquotes.&lt;/p&gt;
&lt;h3&gt;Line Breaks&lt;/h3&gt;
&lt;p&gt;Markdown needs a blank line to separate paragraphs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;If you don&apos;t leave a blank line
it will be in one paragraph

First paragraph

Second paragraph
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;If you don&apos;t leave a blank line
it will be in one paragraph&lt;/p&gt;
&lt;p&gt;First paragraph&lt;/p&gt;
&lt;p&gt;Second paragraph&lt;/p&gt;
&lt;h3&gt;Separators&lt;/h3&gt;
&lt;p&gt;If you have the habit of writing separators, you can start a new line and enter three dashes &lt;code&gt;---&lt;/code&gt; or asterisks &lt;code&gt;***&lt;/code&gt;. Leave a blank line before and after when there are paragraphs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Advanced Techniques&lt;/h2&gt;
&lt;h3&gt;Inline HTML Elements&lt;/h3&gt;
&lt;p&gt;Currently, only some inline HTML elements are supported, including &lt;code&gt;&amp;#x3C;kdb&gt; &amp;#x3C;b&gt; &amp;#x3C;i&gt; &amp;#x3C;em&gt; &amp;#x3C;sup&gt; &amp;#x3C;sub&gt; &amp;#x3C;br&gt;&lt;/code&gt;, such as&lt;/p&gt;
&lt;h4&gt;Key Display&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Use &amp;#x3C;kbd&gt;Ctrl&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Alt&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Del&amp;#x3C;/kbd&gt; to reboot the computer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;Use Ctrl + Alt + Del to reboot the computer&lt;/p&gt;
&lt;h4&gt;Bold Italics&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;b&gt; Markdown also applies here, such as _bold_ &amp;#x3C;/b&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt; Markdown also applies here, such as &lt;em&gt;bold&lt;/em&gt; &lt;/p&gt;
&lt;h3&gt;Other HTML Writing&lt;/h3&gt;
&lt;h4&gt;Foldable Blocks&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;details&gt;&amp;#x3C;summary&gt;Click to expand&amp;#x3C;/summary&gt;It is hidden&amp;#x3C;/details&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;h3&gt;Tables&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;| Header1  | Header2  |
| -------- | -------- |
| Content1 | Content2 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;| Header1  | Header2  |
| -------- | -------- |
| Content1 | Content2 |&lt;/p&gt;
&lt;h3&gt;Footnotes&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Use [^footnote] to add a footnote at the point of reference.

Then, at the end of the document, add the content of the footnote (it will be rendered at the end of the article by default).

[^footnote]: Here is the content of the footnote
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;Use [^footnote] to add a footnote at the point of reference.&lt;/p&gt;
&lt;p&gt;Then, at the end of the document, add the content of the footnote (it will be rendered at the end of the article by default).&lt;/p&gt;
&lt;p&gt;[^footnote]: Here is the content of the footnote&lt;/p&gt;
&lt;h3&gt;To-Do Lists&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- [ ] Incomplete task
- [x] Completed task
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Incomplete task&lt;/li&gt;
&lt;li&gt;[x] Completed task&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Symbol Escaping&lt;/h3&gt;
&lt;p&gt;If you need to use markdown symbols like _ # * in your description but don&apos;t want them to be escaped, you can add a backslash before these symbols, such as &lt;code&gt;\_&lt;/code&gt; &lt;code&gt;\#&lt;/code&gt; &lt;code&gt;\*&lt;/code&gt; to avoid it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;\_Don&apos;t want the text here to be italic\_

\*\*Don&apos;t want the text here to be bold\*\*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview:&lt;/p&gt;
&lt;p&gt;_Don&apos;t want the text here to be italic_&lt;/p&gt;
&lt;p&gt;**Don&apos;t want the text here to be bold**&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Embedding Astro Components&lt;/h2&gt;
&lt;p&gt;See &lt;a href=&quot;/docs/integrations/components&quot;&gt;User Components&lt;/a&gt; and &lt;a href=&quot;/docs/integrations/advanced&quot;&gt;Advanced Components&lt;/a&gt; for details.&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.HAXFr_hw.jpg"/><enclosure url="/_astro/thumbnail.HAXFr_hw.jpg"/></item><item><title>Markdown 语法支持</title><link>https://clloz.com/blog/markdown-zh</link><guid isPermaLink="true">https://clloz.com/blog/markdown-zh</guid><description>Markdown 是一种轻量级的「标记语言」。</description><pubDate>Wed, 26 Jul 2023 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;基本语法&lt;/h2&gt;
&lt;p&gt;Markdown 是一种轻量级且易于使用的语法，用于为您的写作设计风格。&lt;/p&gt;
&lt;h3&gt;标题&lt;/h3&gt;
&lt;p&gt;文章内容较多时，可以用标题分段：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# 标题 1

## 标题 2

## 大标题

### 小标题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;标题预览会打乱文章的结构，所以在此不展示。&lt;/p&gt;
&lt;h3&gt;粗斜体&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;_斜体文本_

**粗体文本**

**_粗斜体文本_**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;斜体文本&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;粗体文本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;粗斜体文本&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;链接&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;文字链接 [链接名称](http://链接网址)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;文字链接 &lt;a href=&quot;http://%E9%93%BE%E6%8E%A5%E7%BD%91%E5%9D%80&quot;&gt;链接名称&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;行内代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;这是一条 `单行代码`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;这是一条 &lt;code&gt;行内代码&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;```js
// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  return fibonacci(n - 1) + fibonacci(n - 2)
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  return fibonacci(n - 1) + fibonacci(n - 2)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前使用 shiki 作为代码高亮插件，支持的语言请参考 &lt;a href=&quot;https://shiki.matsu.io/languages.html&quot;&gt;shiki / languages&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;行内公式&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;这是一条行内公式 $e^{i\pi} + 1 = 0$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;这是一条行内公式 $e^{i\pi} + 1 = 0$&lt;/p&gt;
&lt;h3&gt;公式块&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} \, dx
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} , dx
$$&lt;/p&gt;
&lt;p&gt;当前使用 KaTeX 作为数学公式插件，支持的语法请参考 &lt;a href=&quot;https://katex.org/docs/supported.html&quot;&gt;KaTeX Supported Functions&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;图片&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;![CWorld](https://cravatar.cn/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cravatar.cn/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200&quot; alt=&quot;CWorld&quot;&gt;&lt;/p&gt;
&lt;h4&gt;删除线&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;~~删除线~~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;~~删除线~~&lt;/p&gt;
&lt;h3&gt;列表&lt;/h3&gt;
&lt;p&gt;普通无序列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- 1
- 2
- 3
&lt;/code&gt;&lt;/pre&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;li&gt;3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;普通有序列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;1. GPT-4
2. Claude Opus
3. LLaMa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GPT-4&lt;/li&gt;
&lt;li&gt;Claude Opus&lt;/li&gt;
&lt;li&gt;LLaMa&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;列表里可以继续嵌套语法&lt;/p&gt;
&lt;h3&gt;引用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&gt; 枪响，雷鸣，剑起。繁花血景。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;枪响，雷鸣，剑起。繁花血景。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;引用里也可以继续嵌套语法。&lt;/p&gt;
&lt;h3&gt;换行&lt;/h3&gt;
&lt;p&gt;markdown 分段落是需要空一行的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;如果不空行
就会在一段

第一段

第二段
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;如果不空行
就会在一段&lt;/p&gt;
&lt;p&gt;第一段&lt;/p&gt;
&lt;p&gt;第二段&lt;/p&gt;
&lt;h3&gt;分隔符&lt;/h3&gt;
&lt;p&gt;如果你有写分割线的习惯，可以新起一行输入三个减号&lt;code&gt;---&lt;/code&gt; 或者星号 &lt;code&gt;***&lt;/code&gt;。当前后都有段落时，请空出一行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;高级技巧&lt;/h2&gt;
&lt;h3&gt;行内 HTML 元素&lt;/h3&gt;
&lt;p&gt;目前只支持部分段内 HTML 元素效果，包括 &lt;code&gt;&amp;#x3C;kdb&gt; &amp;#x3C;b&gt; &amp;#x3C;i&gt; &amp;#x3C;em&gt; &amp;#x3C;sup&gt; &amp;#x3C;sub&gt; &amp;#x3C;br&gt;&lt;/code&gt; ，如&lt;/p&gt;
&lt;h4&gt;键位显示&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;使用 &amp;#x3C;kbd&gt;Ctrl&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Alt&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Del&amp;#x3C;/kbd&gt; 重启电脑
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;使用 Ctrl + Alt + Del 重启电脑&lt;/p&gt;
&lt;h4&gt;粗斜体&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;b&gt; Markdown 在此处同样适用，如 _加粗_ &amp;#x3C;/b&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt; Markdown 在此处同样适用，如 &lt;em&gt;加粗&lt;/em&gt; &lt;/p&gt;
&lt;h3&gt;其他 HTML 写法&lt;/h3&gt;
&lt;h4&gt;折叠块&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;details&gt;&amp;#x3C;summary&gt;点击展开&amp;#x3C;/summary&gt;它被隐藏了&amp;#x3C;/details&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;h3&gt;表格&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;| 表头1 | 表头2 |
| ----- | ----- |
| 内容1 | 内容2 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;| 表头1 | 表头2 |
| ----- | ----- |
| 内容1 | 内容2 |&lt;/p&gt;
&lt;h3&gt;注释&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;在引用的地方使用 [^注释] 来添加注释。

然后在文档的结尾，添加注释的内容（会默认于文章结尾渲染之）。

[^注释]: 这里是注释的内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;在引用的地方使用 &lt;a href=&quot;%E8%BF%99%E9%87%8C%E6%98%AF%E6%B3%A8%E9%87%8A%E7%9A%84%E5%86%85%E5%AE%B9&quot;&gt;^注释&lt;/a&gt; 来添加注释。&lt;/p&gt;
&lt;p&gt;然后在文档的结尾，添加注释的内容（会默认于文章结尾渲染之）。&lt;/p&gt;
&lt;h3&gt;To-Do 列表&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- [ ] 未完成的任务
- [x] 已完成的任务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 未完成的任务&lt;/li&gt;
&lt;li&gt;[x] 已完成的任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;符号转义&lt;/h3&gt;
&lt;p&gt;如果你的描述中需要用到 markdown 的符号，比如 _ # * 等，但又不想它被转义，这时候可以在这些符号前加反斜杠，如 &lt;code&gt;\_&lt;/code&gt; &lt;code&gt;\#&lt;/code&gt; &lt;code&gt;\*&lt;/code&gt; 进行避免。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;\_不想这里的文本变斜体\_

\*\*不想这里的文本被加粗\*\*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;_不想这里的文本变斜体_&lt;/p&gt;
&lt;p&gt;**不想这里的文本被加粗**&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;内嵌 Astro 组件&lt;/h2&gt;
&lt;p&gt;See &lt;a href=&quot;/docs/integrations/components&quot;&gt;User Components&lt;/a&gt; and &lt;a href=&quot;/docs/integrations/advanced&quot;&gt;Advanced Components&lt;/a&gt; for details.&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.HAXFr_hw.jpg"/><enclosure url="/_astro/thumbnail.HAXFr_hw.jpg"/></item><item><title>实现一个 lazyman</title><link>https://clloz.com/blog/lazyman</link><guid isPermaLink="true">https://clloz.com/blog/lazyman</guid><pubDate>Sun, 01 May 2022 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;开门见山，今天朋友问了我一道面试题。&lt;/p&gt;
&lt;p&gt;实现一个 LazyMan，可以按照以下方式调用：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LazyMan(&apos;Hank&apos;)&lt;/code&gt;，输出：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hi, This is Hank!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;LazyMan(&apos;Hank&apos;).sleep(5).eat(&apos;dinner&apos;)&lt;/code&gt;，输出：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hi, This is Hank!
// 等待5秒
Weak up after 10
Eat dinner ~&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;LazyMan(&apos;Hank&apos;).eat(&apos;dinner&apos;).eat(&apos;supper&apos;)&lt;/code&gt;，输出&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hi, this is Hank!
Eat dinner ~
Eat supper ~&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;LazyMan(&apos;Hank&apos;).sleepFirst(5).eat(&apos;supper&apos;)&lt;/code&gt;，输出&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;// 等待5秒
Wake up after 5
Hi, this is Hank!
Eat supper&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;看到这个题目首先看到链式调用，那么自然想到的是构造函数，原型方法，每个方法调用后再返回调用对象，这样可以实现链式调用，LazyMan 可以直接调用那么我们可以检测一下是否是 new 调用，不是则返回 new 调用。基本结构如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function LazyMan(name) {
  if (!(this instanceof LazyMan)) {
    return new LazyMan(name)
  }
}

LazyMan.prototype.sleep = function (time) {
  return this
}

LazyMan.prototype.eat = function (food) {
  return this
}

LazyMan.prototype.sleepFirst = function (time) {
  return this
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现链式调用之后我们继续看题目，可以看到需要有等待，自然想到 setTimeout，但是这里 setTimeout 无法满足我们的要求，因为 setTimeout 本身也是同步执行的，其回调函数是异步，并且 setTimeout 没法返回 this 对象。那么我们自然想到了 Promise，async/await。&lt;/p&gt;
&lt;p&gt;使用 Promise 或 async/await 有一个和 setTimeout 一样的问题，就是他们的返回值都是 Promise，没法返回对象。同时我们注意题目中的最后一个调用，sleepFirst 方法最后调用但是却先执行，这给了一些提示。链式调用中肯定是前面的执行完了才执行后面，sleepFirst 看上去好像先执行，只有一种可能就是前面的函数执行没有执行 console，只是将 console 放到了某个地方等待执行。&lt;/p&gt;
&lt;p&gt;所以我们需要在对象上增加一个 tasks 队列来存放任务，每个函数执行只是将要执行的任务放到 tasks 队列中，sleepFirst 的任务则放到队列最前面。&lt;/p&gt;
&lt;p&gt;那么这个队列怎么执行呢，我们需要一个执行函数，这里我第一时间想到的是防抖函数，在 eat，sleep 和 sleepFirst 里面都执行这个执行函数，利用防抖的机制在链式调用结束才会真正执行这个执行函数。&lt;/p&gt;
&lt;p&gt;根据上面的思路我写了下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function LazyMan(name) {
  if (!(this instanceof LazyMan)) {
    return new LazyMan(name)
  }
  this.name = name
  console.log(`this is ${this.name}`)
  this.firstSleepTime = 0
  this.tasks = []
  // this.execute();
}

function sleep(time) {
  return new Promise((resolve) =&gt; setTimeout(resolve, time))
}

function debounce(fn, interval) {
  let timeout = null
  return function () {
    clearTimeout(timeout)
    timeout = setTimeout(() =&gt; {
      fn.apply(this, arguments)
    }, interval)
  }
}

LazyMan.prototype.execute = debounce(function () {
  sleep(this.firstSleepTime).then(async () =&gt; {
    for (let i = 0; i &amp;#x3C; this.tasks.length; i++) {
      if (typeof this.tasks[i] === &apos;string&apos;) {
        console.log(`Eat ${this.tasks[i]}`)
      } else {
        await sleep(this.tasks[i])
      }
    }
  })
}, 0)

LazyMan.prototype.sleep = function (time) {
  this.tasks.push(time)
  this.execute()
  return this
}

LazyMan.prototype.eat = function (food) {
  this.tasks.push(food)
  this.execute()
  return this
}

LazyMan.prototype.sleepFirst = function (time) {
  this.firstSleepTime += time
  this.execute()
  return this
}

LazyMan(&apos;clloz&apos;).eat(&apos;orange&apos;).sleep(5000).eat(&apos;apple&apos;).sleepFirst(3000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果是达到了，不过有点投机取巧，我这里 tasks 存的不是任务，并且 sleepFirst 也是特殊处理。后来我去网上看了看别人的实现，将所有任务逻辑统一。修改后的实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function LazyMan(name) {
  if (!(this instanceof LazyMan)) {
    return new LazyMan(name)
  }
  this.name = name
  this.sayName()
  this.tasks = []
}

function sleep(time) {
  return new Promise((resolve) =&gt; setTimeout(resolve, time))
}

function debounce(fn, interval = 0) {
  let timeout = null
  return function () {
    clearTimeout(timeout)
    timeout = setTimeout(() =&gt; {
      fn.apply(this, arguments)
    }, interval)
  }
}

LazyMan.prototype.sayName = function () {
  console.log(`this is ${this.name}`)
}

LazyMan.prototype.execute = debounce(async function () {
  for (const task of this.tasks) {
    await task()
  }
})

LazyMan.prototype.sleep = function (time) {
  this.tasks.push(async () =&gt; {
    console.log(`sleep ${time}ms`)
    await sleep(time)
  })
  this.execute()
  return this
}

LazyMan.prototype.eat = function (food) {
  this.tasks.push(async () =&gt; {
    console.log(`Eat ${food}`)
  })
  this.execute()
  return this
}

LazyMan.prototype.sleepFirst = function (time) {
  this.tasks.unshift(async () =&gt; {
    console.log(`first sleep ${time}ms`)
    await sleep(time)
  })
  this.execute()
  return this
}

LazyMan(&apos;clloz&apos;).eat(&apos;orange&apos;).sleep(5000).eat(&apos;apple&apos;).sleepFirst(3000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这个题目考察了构造函数，原型，Promise，防抖等知识点，是一道不错的题目，希望本文对你有所帮助&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/ensnail/p/9866130.html&quot;&gt;如何实现一个 LazyMan&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>拖动时禁止选中文字和元素</title><link>https://clloz.com/blog/drag-prevent-selection</link><guid isPermaLink="true">https://clloz.com/blog/drag-prevent-selection</guid><pubDate>Mon, 10 Jan 2022 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在做一些和拖动相关的功能的时候，比如用到 &lt;code&gt;mousedown&lt;/code&gt; 和 &lt;code&gt;mousemove&lt;/code&gt; 的时候，会发现拖动会选中页面中的不相关的文字和图片，文字还好，就是难看一点，但是图片的选中可能会影响到我们的拖动功能。本文分享一下对于拖动时禁止选中文字和元素的方法。&lt;/p&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
&lt;p&gt;可以看一下我写的这个 &lt;a href=&quot;https://cdn.clloz.com/study/drag-selection.html&quot;&gt;Demo&lt;/a&gt;，红色方块是用来拖动的，你可以多触发几次 &lt;code&gt;mousedown&lt;/code&gt;，&lt;code&gt;mousemove&lt;/code&gt; 和 &lt;code&gt;mouseup&lt;/code&gt;，拖动几次方块穿过右边的图片，有时候会发现右侧的图片被你拖走然后红色方块就一直跟着鼠标移动，即使你已经松开鼠标。&lt;/p&gt;
&lt;p&gt;如果页面上有文本的话你会发现拖动的时候会随着鼠标的移动选择文本，这在显示效果上也不是很好，虽然不会像上面的图片一样影响到我们的功能。&lt;/p&gt;
&lt;h2&gt;preventDefault&lt;/h2&gt;
&lt;p&gt;一个比较简单的办法就是在 &lt;code&gt;mousedown&lt;/code&gt; 的回调函数中调用 &lt;code&gt;e.preventDefault()&lt;/code&gt;，这样能够禁用 &lt;code&gt;mousedown&lt;/code&gt; 的默认行为，我们的拖动不再会选中文本或者页面上的元素。&lt;/p&gt;
&lt;p&gt;但是这个方法有一个问题，就是会导致我们的目标元素中的 &lt;code&gt;input&lt;/code&gt; 或者 &lt;code&gt;textarea&lt;/code&gt; 无法触发 &lt;code&gt;focus&lt;/code&gt;，你点击输入框输入框中也不会出现光标，也就无法输入。因为输入框的 &lt;code&gt;focus&lt;/code&gt; 就是通过 &lt;code&gt;mousedown&lt;/code&gt; 触发的，而 &lt;code&gt;mousedown&lt;/code&gt; 的默认行为被我们禁用了。&lt;/p&gt;
&lt;p&gt;这当然不是一个常见的需求，一般我们拖动的元素中不太会出现输入框（不过我今天刚好就遇到了 :joy: ）。如果你需要在拖动元素中使用输入框，那么你就没法使用这个比较方便的方法了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里我没有找到合适的解决方案，如果你有好的解决方案，欢迎指教。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;CSS&lt;/h2&gt;
&lt;p&gt;还有一种实现方式就是使用 &lt;code&gt;CSS&lt;/code&gt; 的 &lt;code&gt;user-select&lt;/code&gt; 属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;* {
  -webkit-touch-callout: none; /*系统默认菜单被禁用*/
  -webkit-user-select: none; /*webkit浏览器*/
  -khtml-user-select: none; /*早期浏览器*/
  -moz-user-select: none; /*火狐*/
  -ms-user-select: none; /*IE10*/
  user-select: none;
  -webkit-user-select: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样会导致文本始终不可选，我们比较想要的效果应该是拖动的时候不会选中，但在其他时候应该是可以选中的。我们可在 &lt;code&gt;mousedown&lt;/code&gt; 触发的时候给 &lt;code&gt;body&lt;/code&gt; 加上对应的样式，在 &lt;code&gt;mouseup&lt;/code&gt; 触发的时候再去掉这个样式。&lt;/p&gt;
&lt;p&gt;还有一个比较麻烦的点在于图片的可拖拽（点击图片并拖动会出现一张透明的图片）。有时我们的拖动会触发到图片的拖拽，会导致我们绑定的 &lt;code&gt;mouseup&lt;/code&gt; 没有执行，在松开鼠标后拖动的元素还是会跟着我们的鼠标移动。&lt;code&gt;-webkit-user-select&lt;/code&gt; 这个 &lt;code&gt;css&lt;/code&gt; 属性可以帮我们禁止图片拖拽，但是这个属性的兼容性很不好，移动端是都不支持的。如果有兼容性要求可以使用 &lt;code&gt;img&lt;/code&gt; 的 &lt;code&gt;draggable&lt;/code&gt; 属性。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;目前测试能够使用的就这两种方法，都不够完美，如果你有好的解决方案欢迎讨论。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/6854573217021952008&quot;&gt;H5拖拽禁止选中文字&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/5429827/how-can-i-prevent-text-element-selection-with-cursor-drag&quot;&gt;How can I prevent text/element selection with cursor drag&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>动手实现一个 WebSocket 服务器</title><link>https://clloz.com/blog/in-depth-websocket</link><guid isPermaLink="true">https://clloz.com/blog/in-depth-websocket</guid><description>实现一个 websocket 服务理解协议的封装</description><pubDate>Tue, 21 Dec 2021 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;现在在开发一些前后端频繁需要通信，或者需要后端主动推送消息到前端的功能时，我们一般会使用 &lt;code&gt;WebSocket&lt;/code&gt;，基本每次项目搭建的时候都会要重新封装一个 &lt;code&gt;Socket&lt;/code&gt; 类来创建前后端通信用的 &lt;code&gt;websocket&lt;/code&gt; 对象。不过每次使用的时候我都要到 &lt;code&gt;MDN&lt;/code&gt; 上看一看 &lt;code&gt;WebSocket&lt;/code&gt; 对象的属性和方法，到网上找一找别人的封装赋值粘贴一下，可见对 &lt;code&gt;websocket&lt;/code&gt; 协议的理解和浏览器的 &lt;code&gt;WebSocket&lt;/code&gt; 对象的缺乏理解。并且由于前端的 &lt;code&gt;WebSocket&lt;/code&gt; 对象比较简单，隐藏了非常多的细节，所以我决定用 &lt;code&gt;NodeJS&lt;/code&gt; 的 &lt;code&gt;Net&lt;/code&gt; 模块提供的 &lt;code&gt;TCP&lt;/code&gt; 来实现一个 &lt;code&gt;WebSocket&lt;/code&gt; 服务端来加深对 &lt;code&gt;WebSocket&lt;/code&gt; 的理解。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实现代码在 &lt;a href=&quot;https://github.com/Clloz/network-note/tree/master/WebSocket&quot; title=&quot;Github&quot;&gt;Github&lt;/a&gt;，打开 &lt;code&gt;index.html&lt;/code&gt;, 并且用 &lt;code&gt;node websocket.js&lt;/code&gt; 启动服务端，即可进行通信。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;WebSocket 简介&lt;/h2&gt;
&lt;p&gt;首先来说一说 &lt;code&gt;WebSocket&lt;/code&gt; 协议，&lt;code&gt;WebSocket&lt;/code&gt; 是一个在 &lt;code&gt;TCP&lt;/code&gt; 连接上提供的一个全双工通信通道。&lt;code&gt;WebSocket&lt;/code&gt; 协议于 &lt;code&gt;2011&lt;/code&gt; 年被 &lt;code&gt;IETF&lt;/code&gt; 标准化为 &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6455&quot; title=&quot;RFC 6455&quot;&gt;RFC 6455&lt;/a&gt;，后由 &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7936&quot; title=&quot;RFC 7936&quot;&gt;RFC 7936&lt;/a&gt; 补充。&lt;code&gt;Web IDL&lt;/code&gt; 中的 &lt;code&gt;WebSocket API&lt;/code&gt; 由 &lt;code&gt;W3C&lt;/code&gt; 进行标准化。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;RFC&lt;/code&gt; 指的是 &lt;code&gt;Request for Comments&lt;/code&gt; 是一系列出版物，来自互联网的主要技术开发和标准制定机构，其中最著名的是互联网工程任务组 (&lt;code&gt;IETF&lt;/code&gt;)。可以理解为网络相关的标准文档。 &lt;code&gt;Web IDL&lt;/code&gt; 是指 &lt;code&gt;Web interface description language&lt;/code&gt;，即 &lt;code&gt;Web&lt;/code&gt; 接口描述语言，用来描述将要在浏览器中实现的 &lt;code&gt;API&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;WebSocket&lt;/code&gt; 是一个全新的协议，&lt;code&gt;WebSocket&lt;/code&gt; 和 &lt;code&gt;HTTP&lt;/code&gt; 是不同的，虽说他们是有交集的。他们都在网络 &lt;code&gt;OSI model&lt;/code&gt; 的第七层应用层，都依赖于第四层传输层的 &lt;code&gt;TCP&lt;/code&gt;。虽然两者是不同的协议，但是 &lt;code&gt;RFC 6455&lt;/code&gt; 指出 &lt;code&gt;WebSocket&lt;/code&gt; 被设计为工作在 &lt;code&gt;HTTP&lt;/code&gt; 的 &lt;code&gt;443&lt;/code&gt; 和 &lt;code&gt;80&lt;/code&gt; 端口上，并支持 &lt;code&gt;HTTP&lt;/code&gt; 代理和中介，使其与 &lt;code&gt;HTTP&lt;/code&gt; 兼容。为了实现兼容性，&lt;code&gt;WebSocket&lt;/code&gt; 的握手使用 &lt;code&gt;HTTP Upgrade header&lt;/code&gt; 改变 &lt;code&gt;HTTP&lt;/code&gt; 协议为 &lt;code&gt;WebSocket&lt;/code&gt; 协议。一般我们在使用 &lt;code&gt;WebSocket&lt;/code&gt; 的时候我们可以在 &lt;code&gt;Request Header&lt;/code&gt; 中看到 &lt;code&gt;Connection: upgrade&lt;/code&gt;，在 &lt;code&gt;Response Header&lt;/code&gt; 中看到 &lt;code&gt;Connection: upgrade&lt;/code&gt; 和 &lt;code&gt;Upgrade: websocket&lt;/code&gt;。一般我们在 &lt;code&gt;nginx&lt;/code&gt; 中要配置 &lt;code&gt;WebSocket&lt;/code&gt; 代理有如下配置（字段解释可以参考 &lt;a href=&quot;https://segmentfault.com/a/1190000039977023&quot; title=&quot;Nginx如何配置Http、Https、WS、WSS&quot;&gt;Nginx如何配置Http、Https、WS、WSS&lt;/a&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;map $http_upgrade $connection_upgrade {
  default upgrade;
  &apos;&apos; close;
}
server {
  listen 8000;
  location / {
    proxy_pass http://localhost:4000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WebSocket&lt;/code&gt; 协议让我们能够以比 &lt;code&gt;HTTP&lt;/code&gt; 轮询等方式更低的开销在 &lt;code&gt;web client&lt;/code&gt; 和 &lt;code&gt;web server&lt;/code&gt; 进行实时数据传输。&lt;code&gt;WebSocket&lt;/code&gt; 使得我们能以一种标准化的方式实现服务端主动向客户端发送内容，并且允许在保持同一个连接打开的状态下来回传递数据。&lt;/p&gt;
&lt;p&gt;在没有 &lt;code&gt;WebSocket&lt;/code&gt; 之前，我们都是用 &lt;code&gt;HTTP&lt;/code&gt; 进行客户端和服务端的通信，这种通信方式只能由服务端发起请求，简历连接，客户端收到请求后进行处理，将数据返回给客户端然后连接即被关闭。在很多场景下，这种通信方式并不科学，比如实时通信，我们的请求建立非常频繁，由于 &lt;code&gt;HTTP&lt;/code&gt; 请求的建立和销毁开销是挺大的，并且请求和响应都包含比较长的头部，真正有效的数据只是整个请求中很小的一部分，会多消耗很多资源。再比如有时候我们想要实时刷新一些数据，但是客户端是不知道数据什么时候发生了更新，我们就只能以轮询的方式定时向服务器发送请求。&lt;/p&gt;
&lt;h2&gt;轮询 pooling&lt;/h2&gt;
&lt;p&gt;这里我们详细说一说轮询的方式，即轮询和长轮询。网络通信有两种方式，一种叫 &lt;code&gt;push technology&lt;/code&gt; 或者叫 &lt;code&gt;server push&lt;/code&gt;，就是请求由发布者或者服务器发起。与之相对的就称为 &lt;code&gt;pull coding&lt;/code&gt; 或者 &lt;code&gt;client pull&lt;/code&gt;，即请求由客户端发起，然后由服务端进行响应，我们的 &lt;code&gt;HTTP&lt;/code&gt; 即是一种广泛应用的 &lt;code&gt;pull technology&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在我们实际开发中，绝大多数时候我们都是用的 &lt;code&gt;pull&lt;/code&gt; 模式的 &lt;code&gt;HTTP&lt;/code&gt;。&lt;code&gt;Push&lt;/code&gt; 的使用场景一般就是即时信息，因为一般信息的产生是在服务端，如果我们想在客户端第一时间显示信息的变更就需要服务端在信息发生变化的时候推送到客户端。这也是发布订阅模型的一种实现，客户端订阅了一系列服务端提供的信息频道，当某个频道有新的内容产生的时候，服务器就将这个新消息推送到客户端。&lt;/p&gt;
&lt;p&gt;我们的轮询就是一种用 &lt;code&gt;pull&lt;/code&gt; 来模拟 &lt;code&gt;push&lt;/code&gt; 的技术，分为常规轮询 &lt;code&gt;pooling&lt;/code&gt; 和 长轮询 &lt;code&gt;long pooling&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;常规轮询非常简单，即客户端定时向服务端发出请求，时间间隔由客户端决定。常规轮询的问题在于，消息存在延迟（延迟取决于你的请求发送频率），并且我们发送了很多无用的信息，且每个客户端都在定时发送，对于服务器和网路来说这都是一个很大的负担。它的优点就是不需要额外进行支持，我们用 &lt;code&gt;HTTP&lt;/code&gt; 即可实现。只有在我们的服务非常小，用户很少的情况这才是一个勉强可行的办法。&lt;/p&gt;
&lt;p&gt;长轮询其实也很简单，就是服务器收到请求后在没有产生新的消息前不会关闭连接而是将连接挂起 &lt;code&gt;pending&lt;/code&gt;，等新的连接产生时才返回给前端并关闭连接。前端收到新消息后立即发出一个新的请求，如此循环，如下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.clloz.com/blog/writing/long-pooling.svg&quot; alt=&quot;long-pooling&quot; title=&quot;long-pooling&quot;&gt;&lt;/p&gt;
&lt;p&gt;长轮询的缺点就是需要挂起很多连接，有些服务器架构是一个连接对应一个进程，连接数多了会消耗相当多的内存。长轮询的 &lt;code&gt;demo&lt;/code&gt; 可以参考 &lt;a href=&quot;https://zh.javascript.info/long-polling#shi-li-liao-tian&quot; title=&quot;长轮询聊天 - JAVASCRIPT.INFO&quot;&gt;长轮询聊天 - JAVASCRIPT.INFO&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;服务端的 &lt;code&gt;TCP&lt;/code&gt; 连接数不是问题，客户端的 &lt;code&gt;TCP&lt;/code&gt; 连接数受限于端口最多只能有 &lt;code&gt;2 ^ 16 - 1&lt;/code&gt; 个，但是服务端监听在固定端口，所以不会有这个限制。参考 &lt;a href=&quot;https://blog.csdn.net/OiteBody/article/details/111640298&quot; title=&quot;服务器最大TCP连接数及调优汇总&quot;&gt;服务器最大TCP连接数及调优汇总&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这也就是所谓的 &lt;code&gt;comet&lt;/code&gt; 技术，在没有 &lt;code&gt;WebSocket&lt;/code&gt; 之前基本都是通过这两种方式来服务器消息的推送，一般都是为了即时消息的展示。&lt;a href=&quot;https://en.wikipedia.org/wiki/Comet_(programming)#Script_tag_long_polling&quot; title=&quot;维基百科&quot;&gt;维基百科&lt;/a&gt;上还介绍了一种 &lt;code&gt;script tag long pooling&lt;/code&gt; 来解决跨域情况下的长轮询，其实就是将 &lt;code&gt;ajax&lt;/code&gt; 替换成 &lt;code&gt;JSONP&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;comet&lt;/code&gt; 还有 &lt;code&gt;iframe&lt;/code&gt; 流和 &lt;code&gt;flash socket&lt;/code&gt; 等实现方式，参考 &lt;a href=&quot;https://en.wikipedia.org/wiki/Comet_(programming)&quot; title=&quot;维基百科&quot;&gt;维基百科&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本着尽量搞懂每一个遇到的知识点，我这里写了个例子试了试 &lt;code&gt;iframe&lt;/code&gt; 流。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;iframe src=&quot;http://localhost:8080/iframe&quot;&gt;&amp;#x3C;/iframe&gt;
&amp;#x3C;script&gt;
  const xhr = new XMLHttpRequest()
  xhr.open(&apos;GET&apos;, &apos;http://localhost:8080/normal&apos;)
  xhr.send()
  xhr.onreadystatechange = () =&gt; {
    if (xhr.readyState === 4) {
      console.log(xhr.responseText)
    }
  }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const http = require(&apos;http&apos;)
const url = require(&apos;url&apos;)

http
  .createServer((req, res) =&gt; {
    const { pathname } = url.parse(req.url)
    if (pathname === &apos;/favicon.ico&apos;) return false
    console.log(`visit: ${pathname}`)

    if (pathname === &apos;/iframe&apos;) {
      res.setHeader(&apos;Content-Type&apos;, &apos;text/html;charset=utf-8&apos;)
      setInterval(() =&gt; {
        res.write(`&amp;#x3C;script&gt;console.log(&apos;${new Date()}&apos;)&amp;#x3C;/script&gt;`)
      }, 1000)
    }

    if (pathname === &apos;/normal&apos;) {
      res.setHeader(&apos;Access-Control-Allow-Origin&apos;, &apos;*&apos;)
      res.setHeader(&apos;Access-Control-Allow-Methods&apos;, &apos;GET, POST, PUT, DELETE, OPTIONS&apos;)
      res.writeHead(200, { &apos;Content-Type&apos;: &apos;text/plain&apos; })
      res.write(&apos;hello world&apos;)
      res.end()
    }
  })
  .listen(8080)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实说简单点就是 &lt;code&gt;iframe&lt;/code&gt; 可以不用 &lt;code&gt;response.end()&lt;/code&gt; 来完成响应。所以我们可以一直持续发送消息，并且这个处理也不是那么灵活，基本上也只能是一个定时器持续执行一个函数（比如查询数据并返回）。（这里如有理解错误，欢迎指出）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;还有个以前经常出现的概念是&lt;strong&gt;长连接&lt;/strong&gt;，我一直没找到一个具体的定义，只有在维基百科上找到 &lt;code&gt;HTTP keep-alive&lt;/code&gt;，其实本质就是传输层的 &lt;code&gt;TCP&lt;/code&gt; 不断开，在一个 &lt;code&gt;TCP&lt;/code&gt; 连接上进行持续的通信。&lt;a href=&quot;https://zh.wikipedia.org/wiki/HTTP%E6%8C%81%E4%B9%85%E8%BF%9E%E6%8E%A5&quot; title=&quot;HTTP持久连接&quot;&gt;HTTP持久连接&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于轮询的缺点，所以最终有了 &lt;code&gt;WebSocket&lt;/code&gt; 和 &lt;code&gt;EventSource&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在进入 &lt;code&gt;WebSocket&lt;/code&gt; 协议细节和实现之前，我们先看一看比较简单的浏览器提供的 &lt;code&gt;WebSocket API&lt;/code&gt;，也是我们平常接触比较多比较熟悉的对象。&lt;/p&gt;
&lt;h2&gt;浏览器端的 &lt;code&gt;WebSocket&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;浏览器端的 &lt;code&gt;WebSocket&lt;/code&gt; 对象是非常简单的，基本我们只要 &lt;code&gt;new&lt;/code&gt; 一个 &lt;code&gt;WebSocket&lt;/code&gt; 对象然后调用一些方法即可使用，在实现之前我们先介绍一下浏览器提供的 &lt;code&gt;WebSocket API&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;构造函数&lt;/h5&gt;
&lt;p&gt;一般我们使用 &lt;code&gt;WebSocket&lt;/code&gt; 就直接 &lt;code&gt;new WebSocket(url)&lt;/code&gt; 就可以构造出 &lt;code&gt;WebSocket&lt;/code&gt; 对象了，不过 &lt;code&gt;WebSocket&lt;/code&gt; 还接受一个可选参数 &lt;code&gt;protocols&lt;/code&gt;，可以是一个字符串，或者字符串数组。所谓子协议其实就是在建立连接的时候多加了一个 &lt;code&gt;header&lt;/code&gt; 叫做 &lt;code&gt;sec-websocket-protocol&lt;/code&gt;，实际上就是我们可以自己商量一些解析内容的方式，看到对应的头就用对应的解析方式，一般是不需要使用的。&lt;/p&gt;
&lt;h2&gt;WebSocket.prototype.binaryType&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WebSocket&lt;/code&gt; 可以发送两种数据，一种是 &lt;code&gt;USVString&lt;/code&gt;，另一种是二进制数据（包括 &lt;code&gt;ArrayBuffer&lt;/code&gt;，&lt;code&gt;TypedArray&lt;/code&gt;，&lt;code&gt;DataView&lt;/code&gt; 和 &lt;code&gt;Blob&lt;/code&gt;）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;USVString&lt;/code&gt; 概念参考 &lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/26/character-encoding/#USVStringDOMStringCSSOMString&quot; title=&quot;搞懂字符编码&quot;&gt;搞懂字符编码&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个属性是用来控制浏览器收到二进制数据如何处理的，该属性可以设置为 &lt;code&gt;blob&lt;/code&gt; 和 &lt;code&gt;arraybuffer&lt;/code&gt;，&lt;code&gt;blob&lt;/code&gt; 是默认值。这个值不会传递给服务端，它的功能只是告诉浏览器在拿到服务器的二进制数据之后处理为哪个对象。如果你需要 &lt;code&gt;blob&lt;/code&gt; 就默认就可以，如果你希望要读写拿到的二进制数据那么就应该使用 &lt;code&gt;arraybuffer&lt;/code&gt;。如果对于前端的二进制处理不清楚，欢迎移步到我的另一篇博客 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2021/12/06/js-binary-data-manipulate/#&quot; title=&quot;JS 中的二进制数据的操作&quot;&gt;JS 中的二进制数据的操作&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;WebSocket.prototype.bufferedAmount&lt;/h2&gt;
&lt;p&gt;这个&lt;strong&gt;只读&lt;/strong&gt;属性返回已经调用 &lt;code&gt;send()&lt;/code&gt; 方法发送但还没有实际发送到网络上去的字节数，发送出去后这个值会清零。如果连接已经关闭，继续调用 &lt;code&gt;send&lt;/code&gt; 方法，这个值会一直增长。一般来说不会使用。&lt;/p&gt;
&lt;h2&gt;WebSocket.prototype.extensions&lt;/h2&gt;
&lt;p&gt;这个&lt;strong&gt;只读&lt;/strong&gt;属性返回服务器选择的 &lt;code&gt;WebSocket&lt;/code&gt; 扩展。所谓的扩展就是对 &lt;code&gt;WebSocket&lt;/code&gt; 协议的一种增强，比如 &lt;code&gt;deflate-frame&lt;/code&gt; 就是告诉服务器浏览器支持数据压缩。一般来说这个值是空字符串，前端可以通过建立连接时用 &lt;code&gt;Sec-Websocket-Extensions&lt;/code&gt; 来告诉服务器哪些扩展是被支持的，这个 &lt;code&gt;header&lt;/code&gt; 是每次连接建立时浏览器自动生成的，告诉服务器该浏览器支持哪些扩展，服务器会选择它需要使用的扩展在 &lt;code&gt;Response&lt;/code&gt; 的 &lt;code&gt;Sec-Websocket-Extensions&lt;/code&gt; 头中，我们的这个只读属性就是获取的服务端返回的这个 &lt;code&gt;header&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;WebSocket.prototype.protocol&lt;/h2&gt;
&lt;p&gt;这个&lt;strong&gt;只读&lt;/strong&gt;属性返回服务器选择的子协议，比如 &lt;code&gt;soap&lt;/code&gt; 和 &lt;code&gt;wamp&lt;/code&gt;。前端在建立请求的时候可以用 &lt;code&gt;WebSocket&lt;/code&gt; 构造函数的第二个参数（本质就是添加 &lt;code&gt;Sec-Websocket-Protocol&lt;/code&gt; 头） 来告诉服务器前端支持哪些子协议，服务器会返回它决定使用的子协议并放在 &lt;code&gt;Response&lt;/code&gt; 的 &lt;code&gt;Sec-Websocket-Protocol&lt;/code&gt; 中来告诉客户端，我们的这个只读属性就是获取的服务端返回的这个 &lt;code&gt;header&lt;/code&gt;。可以使用通用的&lt;a href=&quot;https://www.iana.org/assignments/websocket/websocket.xml&quot; title=&quot;子协议&quot;&gt;子协议&lt;/a&gt;，也可以自定义子协议。所谓的子协议其实就是客户端和服务端商量一种通信使用的数据结构。&lt;/p&gt;
&lt;p&gt;关于扩展和子协议我们可以举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Request
GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascript.info
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp

# Response
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WebSocket.prototype.url&lt;/h2&gt;
&lt;p&gt;这个&lt;strong&gt;只读&lt;/strong&gt;属性返回 &lt;code&gt;WebSocket&lt;/code&gt; 连接的绝对地址。&lt;/p&gt;
&lt;h2&gt;WebSocket.prototype.readyState&lt;/h2&gt;
&lt;p&gt;这个&lt;strong&gt;只读&lt;/strong&gt;属性返回 &lt;code&gt;WebSocket&lt;/code&gt; 连接的当前状态，返回值是一个&lt;code&gt;unsigned short&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;| Value | State      | Description                                              |
| ----- | ---------- | -------------------------------------------------------- |
| 0     | CONNECTING | Socket has been created. The connection is not yet open. |
| 1     | OPEN       | The connection is open and ready to communicate.         |
| 2     | CLOSING    | The connection is in the process of closing.             |
| 3     | CLOSED     | The connection is closed or couldn&apos;t be opened.          |&lt;/p&gt;
&lt;h2&gt;WebSocket.prototype.send(data)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;send&lt;/code&gt; 是我们用来发送消息的方法。&lt;code&gt;send&lt;/code&gt; 方法将要发送给 &lt;code&gt;server&lt;/code&gt; 的数据排入队列，同时增大 &lt;code&gt;bufferedAmount&lt;/code&gt;。如果数据无法被发送（比如缓冲区已满），&lt;code&gt;socket&lt;/code&gt; 会自动关闭。如果 &lt;code&gt;readyState&lt;/code&gt; 是 &lt;code&gt;CONNECTING&lt;/code&gt; 时调用 &lt;code&gt;send&lt;/code&gt; 方法，浏览器会抛出一个 &lt;code&gt;INVALID_STATE_ERR&lt;/code&gt; 异常。如果在 &lt;code&gt;readyState&lt;/code&gt; 为 &lt;code&gt;CLOSING&lt;/code&gt; 或者 &lt;code&gt;CLOSED&lt;/code&gt;，浏览器会静默丢弃要发送的数据。&lt;/p&gt;
&lt;p&gt;我们可发送的数据类型有 &lt;code&gt;USVString&lt;/code&gt;，&lt;code&gt;ArrayBuffer&lt;/code&gt;，&lt;code&gt;Blob&lt;/code&gt;，&lt;code&gt;TypedArray&lt;/code&gt; 和 &lt;code&gt;DataView&lt;/code&gt;。简单点说我们可以发送字符串和二进制数据。&lt;code&gt;USVString&lt;/code&gt; 会被转为 &lt;code&gt;UTF-8&lt;/code&gt; 放入缓冲，&lt;code&gt;bufferedAmount&lt;/code&gt; 也会按照 &lt;code&gt;UTF-8&lt;/code&gt; 的大小增长。需要注意的是如果我们使用单个的代理平面的码点，或者错误顺序的代理平面码点都会被替换成 &lt;code&gt;�&lt;/code&gt;，比如 &lt;code&gt;ws.send(&apos;\uDC00\uD800&apos;)&lt;/code&gt; 我们会在后端接收到 &lt;code&gt;��&lt;/code&gt;，&lt;code&gt;ws.send(&apos;\uDC00&apos;)&lt;/code&gt; 或者 &lt;code&gt;ws.send(&apos;\uD800&apos;)&lt;/code&gt; 则都会收到 &lt;code&gt;�&lt;/code&gt;。具体就是因为 &lt;code&gt;USVString&lt;/code&gt; 的概念即浏览器的处理，参考 &lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/26/character-encoding/#USVStringDOMStringCSSOMString&quot; title=&quot;搞懂字符编码&quot;&gt;搞懂字符编码&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;WebSocket.prototype.close&lt;/h2&gt;
&lt;p&gt;这个方法是关闭 &lt;code&gt;WebSocket&lt;/code&gt; 连接的，可以传递两个可选参数，一个 &lt;code&gt;code&lt;/code&gt;，表示连接关闭的状态码，默认值是 &lt;code&gt;1005&lt;/code&gt;，其他状态码参考 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code&quot; title=&quot;CloseEvent.code&quot;&gt;CloseEvent.code&lt;/a&gt; 。另一个是 &lt;code&gt;reason&lt;/code&gt;，是一个人类刻度的表示连接关闭原因的字符串，长度不能超过 &lt;code&gt;123&lt;/code&gt; 字节的 &lt;code&gt;UTF-8&lt;/code&gt; 文本。&lt;/p&gt;
&lt;h2&gt;Event&lt;/h2&gt;
&lt;p&gt;剩下的就是四个事件和事件监听函数，分别是 &lt;code&gt;close&lt;/code&gt;，&lt;code&gt;error&lt;/code&gt;，&lt;code&gt;open&lt;/code&gt; 和 &lt;code&gt;message&lt;/code&gt;。分别对应连接的关闭，连接报错，连接打开和信息的接受。事件监听可以用 &lt;code&gt;addEventListener&lt;/code&gt; 或者 &lt;code&gt;onEvent&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;进本上前端的 &lt;code&gt;WebSocket&lt;/code&gt; 内容就这么多，并不是很复杂。有了这个为基础我们可以开始进入 &lt;code&gt;WebSocket server&lt;/code&gt; 实现。我们可以利用前端建立连接，发送不同的数据类型，以及用浏览器的开发者工具查看请求的 &lt;code&gt;header&lt;/code&gt; 和 &lt;code&gt;message&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;实现 &lt;code&gt;WebSocket&lt;/code&gt; 服务器&lt;/h2&gt;
&lt;p&gt;要用 &lt;code&gt;TCP&lt;/code&gt; 处理连接，我们必须知道 &lt;code&gt;WebSocket&lt;/code&gt; 连接是如何建立，以及发送的帧格式，如果你有耐心，可以去阅读 &lt;a href=&quot;https://tools.ietf.org/html/rfc6455&quot; title=&quot;RFC6455&quot;&gt;RFC6455&lt;/a&gt;，我在这里做一个简单的梳理。&lt;/p&gt;
&lt;h2&gt;握手&lt;/h2&gt;
&lt;p&gt;建立 &lt;code&gt;WebSocket&lt;/code&gt; 连接客户端需要发送一个握手请求，服务端返回握手请求后连接即被建立，之后客户端和服务端就可以进行全双工通信了。&lt;code&gt;WebSocket&lt;/code&gt; 是通过 &lt;code&gt;HTTP/1.1&lt;/code&gt; 协议的 &lt;code&gt;101&lt;/code&gt; 状态码进行握手。我们的请求头如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;GET ws://localhost:3000/ HTTP/1.1
Host: localhost:3000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Upgrade: websocket
Origin: http://localhost:63342
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: en,ja;q=0.9,zh;q=0.8,zh-CN;q=0.7
Sec-WebSocket-Key: hFP3hhcWJNLBqU+rCnHPng==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这其中比较关键的就是： 1. &lt;code&gt;Connection: Upgrade&lt;/code&gt; 和 &lt;code&gt;Upgrade: websocket&lt;/code&gt;：噶偶速服务端升级到 &lt;code&gt;websocket&lt;/code&gt; 协议 2. &lt;code&gt;Sec-WebSocket-Version&lt;/code&gt;： 表示支持的 &lt;code&gt;WebSocket&lt;/code&gt; 版本。&lt;code&gt;RFC6455&lt;/code&gt; 要求使用的版本是 &lt;code&gt;13&lt;/code&gt;，之前草案的版本均应当弃用。 3. &lt;code&gt;Sec-WebSocket-Key&lt;/code&gt; 是随机 &lt;code&gt;base64&lt;/code&gt; 编码的字符串，&lt;code&gt;RFC6455&lt;/code&gt; 要其这个值必须是用一个临时创建的 &lt;code&gt;16&lt;/code&gt; 字节的值经过 &lt;code&gt;base64&lt;/code&gt; 编码后生成的 &lt;code&gt;24&lt;/code&gt; 字节的值（最后两位为 &lt;code&gt;==&lt;/code&gt;）如果不符合这个要求，绝大多数现代 &lt;code&gt;HTTP server&lt;/code&gt; 会拒绝请求并返回一个错误 &lt;code&gt;invalid Sec-WebSocket-Key header&lt;/code&gt;。服务器端会用这些数据来构造出一个 &lt;code&gt;SHA-1&lt;/code&gt; 的信息摘要。把 &lt;code&gt;Sec-WebSocket-Key&lt;/code&gt; 加上一个特殊字符串 &lt;code&gt;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&lt;/code&gt;，然后计算 &lt;code&gt;SHA-1&lt;/code&gt; 摘要，之后进行 &lt;code&gt;Base64&lt;/code&gt; 编码，将结果做为 &lt;code&gt;Sec-WebSocket-Accept&lt;/code&gt; 头的值，返回给客户端。如此操作，可以尽量避免普通 &lt;code&gt;HTTP&lt;/code&gt; 请求被误认为 &lt;code&gt;WebSocket&lt;/code&gt; 协议，所以这个 &lt;code&gt;header&lt;/code&gt; 不是为了安全性考虑的，只是防止非 &lt;code&gt;websocket&lt;/code&gt; 的客户端误发 &lt;code&gt;WebSocket&lt;/code&gt; 请求。&lt;strong&gt;注意这个头是由客户端（一般是浏览器）自动添加的，我们无法通过 XMLHttpRequest.setRequestHeader() 添加&lt;/strong&gt; 4. Sec-WebSocket-Extensions 和 Sec-Websocket-Protocol 参考上文的浏览器中的 &lt;code&gt;WebSocket&lt;/code&gt; 对象&lt;/p&gt;
&lt;p&gt;服务端的返回如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 8XodYgYF1B62DPQYUIi5VJu/vCI=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Sec-WebSocket-Accept&lt;/code&gt; 就是根据客户端的 &lt;code&gt;Sec-WebSocket-Key&lt;/code&gt; 计算得到的值。服务端也会带上 &lt;code&gt;Upgrade&lt;/code&gt; 和 &lt;code&gt;Connection&lt;/code&gt;，客户端收到服务端的返回，即完成握手，连接建立成功，我们可以通过这个连接进行全双工通信，直至连接关闭。&lt;/p&gt;
&lt;p&gt;握手中我们要处理的就是生成 &lt;code&gt;Sec-WebSocket-Accept&lt;/code&gt;，监听 &lt;code&gt;socket&lt;/code&gt; 的 &lt;code&gt;data&lt;/code&gt; 事件，解析请求头，判断是否是合法的 &lt;code&gt;WebSocket&lt;/code&gt; 请求，然后闹到 &lt;code&gt;Sec-WebSocket-Key&lt;/code&gt;，利用 &lt;code&gt;NodeJS&lt;/code&gt; 的 &lt;code&gt;Crypto&lt;/code&gt; 模块可以很轻松计算出 &lt;code&gt;Sec-WebSocket-Accept&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const net = require(&apos;net&apos;) // net 模块提供一个异步的网络 API 用来创建基于流的 TCP 或者 IPC(Inter Process Communication) 的服务端或者客户端 https://blog.csdn.net/manhua253/article/details/4219655
const crypto = require(&apos;crypto&apos;) // crypto 模块提供了一些加密和解密的方法
const { Buffer } = require(&apos;buffer&apos;)

// 解析请求头
function parseHeader(data) {
  const header = {}
  const lines = data.split(&apos;\r\n&apos;).filter((line) =&gt; line)
  lines.shift() // 去除第一行请求行
  lines.forEach((line) =&gt; {
    const [key, value] = line.split(&apos;: &apos;)
    header[key.toLowerCase()] = value
  })
  return header
}

const server = net.createServer((socket) =&gt; {
  // console.log(socket);
  socket.once(&apos;data&apos;, (buffer) =&gt; {
    console.log(Object.prototype.toString.call(buffer)) // 确定 buffer 的类型 Uint8Array
    const str = buffer.toString()
    const headers = parseHeader(str) // 解析请求头
    console.log(headers) // 观察一下请求头

    if (headers.upgrade !== &apos;websocket&apos;) {
      console.log(&apos;不是 websocket 请求&apos;)
      socket.end(&apos;HTTP/1.1 400 Bad Request\r\n\r\n&apos;)
    } else if (headers[&apos;sec-websocket-version&apos;] !== &apos;13&apos;) {
      console.log(&apos;不支持的 websocket 版本&apos;)
      socket.end(&apos;HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\n\r\n&apos;)
    } else {
      const GUID = &apos;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&apos;
      const key = headers[&apos;sec-websocket-key&apos;]
      const acceptKey = crypto
        .createHash(&apos;sha1&apos;) // 创建 sha1 hash 对象
        .update(key + GUID) // 更新 hash 对象内容
        .digest(&apos;base64&apos;) // 生成摘要
      const response = `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${acceptKey}\r\n\r\n`
      socket.write(response)
      console.log(response)

      let maskingKey = []

      socket.on(&apos;data&apos;, (msgBuffer) =&gt; {
        // 监听前端发来的数据
      })

      console.log(maskingKey)
    }
  })
})
server.listen(3000) // 监听端口
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解析消息帧&lt;/h2&gt;
&lt;p&gt;连接建立完成后我们就要处理消息的接收和发送了，这其中最重要的就是要理解 &lt;code&gt;WebSocket&lt;/code&gt; 的消息帧了，对照&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6455#section-5.2&quot; title=&quot;Base Framing Protocol - RFC 6455&quot;&gt;Base Framing Protocol - RFC 6455&lt;/a&gt; 可以看到详细说明，我这里做一个简单的说明，如果要了解细节还是参考规范文档。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一条 &lt;code&gt;WebSocket message&lt;/code&gt; 是可以分多帧发送的，可能我们在客户端调用的 &lt;code&gt;websocket.send()&lt;/code&gt; 被浏览器分成了多帧发送，所以我们在实现 &lt;code&gt;WebSocket&lt;/code&gt; 服务器的时候需要考虑多帧的问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/websocket-frame.Dt9X91NL_ZKezSv.webp&quot; alt=&quot;websocket-frame&quot; title=&quot;websocket-frame&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个帧格式最上面的数字表示 &lt;code&gt;bit&lt;/code&gt;，一行一共是 &lt;code&gt;32 bits&lt;/code&gt;，也就是 &lt;code&gt;4&lt;/code&gt; 个字节。下面我们分别介绍各个位的含义。 1. &lt;code&gt;FIN&lt;/code&gt;：占 &lt;code&gt;1 bit&lt;/code&gt;，表示这是不是一条 &lt;code&gt;message&lt;/code&gt; 的最后一个 &lt;code&gt;fragment&lt;/code&gt;，&lt;code&gt;1&lt;/code&gt; 就表示是最后一个片段。如果一条消息只有一个片段，那么第一个片段就是最后一个片段。 2. 连续的三个 &lt;code&gt;RSV&lt;/code&gt;，每个都是 &lt;code&gt;1 bit&lt;/code&gt;，除非我们使用的扩展对 &lt;code&gt;RSV&lt;/code&gt; 的非 &lt;code&gt;0&lt;/code&gt; 值进行了定义，否则这三个值都是 &lt;code&gt;0&lt;/code&gt;。一般来说这三个值都是 &lt;code&gt;0&lt;/code&gt;，如果没有使用上述扩展，并且发送了任意 &lt;code&gt;RSV&lt;/code&gt; 非 &lt;code&gt;0&lt;/code&gt; 的帧，那么接收端会关闭连接并且会报错。 3. &lt;code&gt;Opcode&lt;/code&gt;： &lt;code&gt;4 bits&lt;/code&gt;，定义帧类型 &lt;code&gt;%x0&lt;/code&gt; 表示一个连续帧（接续上一个帧）， &lt;code&gt;%x1&lt;/code&gt; 为文本帧， &lt;code&gt;%x2&lt;/code&gt; 为二进制帧， &lt;code&gt;%x3-7&lt;/code&gt; 为未来的帧类型保留， &lt;code&gt;%x8&lt;/code&gt; 表示连接关闭， &lt;code&gt;%x9&lt;/code&gt; 为 &lt;code&gt;ping&lt;/code&gt; 帧， &lt;code&gt;%xA&lt;/code&gt; 为 &lt;code&gt;pong&lt;/code&gt; 帧， &lt;code&gt;%xB-F&lt;/code&gt; 保留。如果任意端（客户端或者服务端）发送了一个未知的 &lt;code&gt;opcode&lt;/code&gt;，另一端会关闭连接。 4. &lt;code&gt;MASK&lt;/code&gt;：&lt;code&gt;1 bit&lt;/code&gt;，表示是否对 &lt;code&gt;Payload data&lt;/code&gt; 使用了掩码，为 &lt;code&gt;1&lt;/code&gt; 则表示了使用掩码，并且后面会有四个字节的掩码。 6. &lt;code&gt;Payload length&lt;/code&gt;：可以是 &lt;code&gt;7 bits&lt;/code&gt;，&lt;code&gt;7 + 16 bits&lt;/code&gt; 或者是 &lt;code&gt;7 + 64 bits&lt;/code&gt;。表示的是 &lt;code&gt;payload data&lt;/code&gt; 的字节数，如果前 &lt;code&gt;7 bit&lt;/code&gt; 为 &lt;code&gt;0 - 125&lt;/code&gt; 之间，则这 &lt;code&gt;7 bits&lt;/code&gt; 表示的数字就是 &lt;code&gt;payload data&lt;/code&gt; 的字节数。如果前 &lt;code&gt;7 bits&lt;/code&gt; 为 &lt;code&gt;126&lt;/code&gt;，那么后面 &lt;code&gt;2&lt;/code&gt; 个字节表示的 &lt;code&gt;Extended payload length&lt;/code&gt; 表示的数作为 &lt;code&gt;payload&lt;/code&gt; 的字节数。如果前 &lt;code&gt;7 bits&lt;/code&gt; 的值为 &lt;code&gt;127&lt;/code&gt;，那么后面 &lt;code&gt;8&lt;/code&gt; 个字节表示的 &lt;code&gt;Extended payload length&lt;/code&gt; 表示的数作为 &lt;code&gt;payload&lt;/code&gt; 的字节数。所以 &lt;code&gt;payload length&lt;/code&gt; 这里具体有多少 &lt;code&gt;bits&lt;/code&gt; 取决于我们的 &lt;code&gt;payload data&lt;/code&gt; 的长度。 7. &lt;code&gt;Masking-key&lt;/code&gt;：如果前面我们的 &lt;code&gt;MASK&lt;/code&gt; 那一位为 &lt;code&gt;1&lt;/code&gt;，那么这里会有四个字节用来表示掩码，如果&lt;code&gt;MASK&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;，则这四个字节不存在。 8. &lt;code&gt;Payload data&lt;/code&gt;：最后就是我们的实际数据，如果有 &lt;code&gt;MASK&lt;/code&gt;，那么这个数据是经过掩码处理后的。&lt;code&gt;Payload data&lt;/code&gt; 包含两个部分，&lt;code&gt;Extension data&lt;/code&gt; 和 &lt;code&gt;Application data&lt;/code&gt;，前者只有在我们 &lt;code&gt;Extension&lt;/code&gt; 的时候会有，后者就是我们实际要发送的数据。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;所谓 &lt;code&gt;payload&lt;/code&gt; 英文翻译为有效荷载，其实就是指我们发送的内容中实际要发送的数据，或者我们程序中实际有意义的数据，因为一个帧里面除了我们要发的数据还要带很多其他信息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;socket&lt;/code&gt; 收到的所有消息都是 &lt;code&gt;Uint8Array&lt;/code&gt; 类型的，如果是字符串就是 &lt;code&gt;UTF-8&lt;/code&gt; 的，所以我们要先写一些工具函数，&lt;code&gt;UTF-8&lt;/code&gt; 和 &lt;code&gt;String&lt;/code&gt; 的相互转换，以及 &lt;code&gt;mask&lt;/code&gt; 的算法。&lt;/p&gt;
&lt;p&gt;这里单独说一下 &lt;code&gt;mask&lt;/code&gt; 的算法，&lt;code&gt;mask&lt;/code&gt; 的加解密算法是一样的，就是遍历 &lt;code&gt;payload&lt;/code&gt; 的每个字节，然后依次和 &lt;code&gt;Masking-Key&lt;/code&gt; 数组中的每一个进行按位异或就得到了掩码后或掩码前的值。&lt;strong&gt;特别需要注意的一点是，客户端给服务端发的消息是强制要掩码的，服务端给客户端发消息虽然没有强制规定，但很多客户端不支持，比如 chrome，所以我们给客户端发的时候不要加掩码（我在代码中的发送分别处理加掩码和不加掩码两种情况）&lt;/strong&gt; 标准中给出的说明&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6455#section-5.3&quot; title=&quot;如下&quot;&gt;如下&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UTF-8&lt;/code&gt; 和 &lt;code&gt;String&lt;/code&gt; 的互转就不另外说明了，大家直接看代码和注释。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// utf-8 转字符串
function utf8ToString(buffer) {
  console.log(buffer)
  let result = &apos;&apos;
  for (let i = 0; i &amp;#x3C; buffer.length; i += 1) {
    if (buffer[i] &gt;&gt; 7 === 0) {
      result += String.fromCodePoint(buffer[i])
    }
    if (buffer[i] &gt;&gt; 5 === 0b110) {
      const codePoint = ((buffer[i] &amp;#x26; 0x1f) &amp;#x3C;&amp;#x3C; 6) | (buffer[i + 1] &amp;#x26; 0x3f)
      result += String.fromCodePoint(codePoint)
      i += 1
    }
    if (buffer[i] &gt;&gt; 4 === 0b1110) {
      const codePoint =
        ((buffer[i] &amp;#x26; 0xf) &amp;#x3C;&amp;#x3C; 12) | ((buffer[i + 1] &amp;#x26; 0x3f) &amp;#x3C;&amp;#x3C; 6) | (buffer[i + 2] &amp;#x26; 0x3f)
      result += String.fromCodePoint(codePoint)
      i += 2
    }
    if (buffer[i] &gt;&gt; 3 === 0b11110) {
      const codePoint =
        ((buffer[i] &amp;#x26; 0xf) &amp;#x3C;&amp;#x3C; 18) |
        ((buffer[i + 1] &amp;#x26; 0x3f) &amp;#x3C;&amp;#x3C; 12) |
        ((buffer[i + 2] &amp;#x26; 0x3f) &amp;#x3C;&amp;#x3C; 6) |
        (buffer[i + 3] &amp;#x26; 0x3f)
      try {
        result += String.fromCodePoint(codePoint)
      } catch (e) {
        console.log(buffer[i], buffer[i + 1], buffer[i + 2], buffer[i + 3])
      }
      i += 3
    }
  }
  return result
}

// 字符串转 utf-8
function stringToUtf8(str) {
  const { length } = str
  const result = []
  for (let i = 0; i &amp;#x3C; length; i += 1) {
    const codePoint = str.codePointAt(i)
    if (codePoint &amp;#x3C;= 0x7f) {
      result.push(codePoint &amp;#x26; 0x7f)
    } else if (codePoint &gt;= 0x80 &amp;#x26;&amp;#x26; codePoint &amp;#x3C;= 0x7ff) {
      result.push(((codePoint &gt;&gt; 6) &amp;#x26; 0x1f) | 0xc0)
      result.push((codePoint &amp;#x26; 0x3f) | 0x80)
    } else if (codePoint &gt;= 0x800 &amp;#x26;&amp;#x26; codePoint &amp;#x3C;= 0xffff) {
      result.push(((codePoint &gt;&gt; 12) &amp;#x26; 0xf) | 0xe0)
      result.push(((codePoint &gt;&gt; 6) &amp;#x26; 0x3f) | 0x80)
      result.push((codePoint &amp;#x26; 0x3f) | 0x80)
    } else if (codePoint &gt;= 0x10000 &amp;#x26;&amp;#x26; codePoint &amp;#x3C;= 0x10ffff) {
      result.push(((codePoint &gt;&gt; 18) &amp;#x26; 0x7) | 0xf0)
      result.push(((codePoint &gt;&gt; 12) &amp;#x26; 0x3f) | 0x80)
      result.push(((codePoint &gt;&gt; 6) &amp;#x26; 0x3f) | 0x80)
      result.push((codePoint &amp;#x26; 0x3f) | 0x80)
      i += 1
    }
  }
  return Uint8Array.from(result)
}

// 带掩码的数据的编解码，编解码的方式都一样的
function maskCodec(data, mask) {
  if (mask.length !== 4) return data
  const { length } = data
  const result = new Uint8Array(length)
  for (let i = 0; i &amp;#x3C; length; i += 1) {
    result[i] = data[i] ^ mask[i % 4]
  }
  return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了这些基础的方法之后就是按照帧的格式进行封装和解封了，直接上代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;/**
 * @description: 解封数据帧，主要是根据 payloadLength 和 maskingKey 将 payload 解码为对应的 utf-8 编码或者二进制数据 TODO: 此处只是一个简单的实现，没有处理连续帧的情况
 * @param data (数据帧)
 * @return: utf-8 对应的字符串或者二进制数据
 */
function decodeWebSocketFrame(data) {
  const frame = {
    isFinal: (data[0] &gt;&gt; 7) &amp;#x26; 1, // 是否为最后一帧
    rsv1: (data[0] &gt;&gt; 6) &amp;#x26; 1, // 必须为0 除非扩展了非 0 值的含义的扩展
    rsv2: (data[0] &gt;&gt; 5) &amp;#x26; 1, // 同上
    rsv3: (data[0] &gt;&gt; 4) &amp;#x26; 1, // 同上
    opcode: data[0] &amp;#x26; 0xf, // 帧类型 %x0 表示一个连续帧（接续上一个帧） %x1 为文本帧 %x2 为二进制帧 %x3-7 保留 %x8 表示连接关闭 %x9 为ping帧 %xA 为pong帧 %xB-F 保留
    mask: (data[1] &gt;&gt; 7) &amp;#x26; 1, // 是否有掩码
    payloadLength: data[1] &amp;#x26; 0x7f, // 帧长度 0-125 则为精确长度，如果为126 则后面两个字节为长度 如果为127 则后面8个字节为长度
    extendedPayloadLength:
      // eslint-disable-next-line no-nested-ternary
      data[1] === 0x7f ? data.readUIntBE(2, 2) : data[1] === 0xff ? data.readUIntBE(2, 8) : 0, // 扩展长度
    maskingKey: [data[2], data[3], data[4], data[5]], // 掩码
    maskedPayload: data.slice(6) // 掩码后的数据
  }

  // payloadLength 为 126 则后面 2 字节的 16 位无符号整数为 payloadLength
  if (frame.payloadLength === 0x7e) {
    frame.payloadLength = data.readUIntBE(2, 2)
    frame.maskingKey = [data[4], data[5], data[6], data[7]]
    frame.maskedPayload = data.slice(8)
  }

  // payloadLength 为 127 则后面 8 字节的 64 位无符号整数(最高位必须为 0)为 payloadLength
  if (frame.payloadLength === 0x7f) {
    frame.payloadLength = data.readUIntBE(2, 8)
    frame.maskingKey = [data[10], data[11], data[12], data[13]]
    frame.maskedPayload = data.slice(14)
  }

  frame.unMaskedPayload = maskCodec(frame.maskedPayload, frame.maskingKey) // 解码
  console.log(frame)
  return frame
}

/**
 * @description: 这里的分别测试了发送单帧和连续帧的两种情况
 * 这里我设置了封装帧的时候可以设置掩码，实际服务端向客服端发送的数据的时候浏览器不一定支持用掩码
 * 比如 chrome，如果你用掩码就会报错 `A server must not mask any frames that it sends to the client.` 参考 https://stackoverflow.com/a/16935108/8854649
 * @param maskingKey: 掩码，如果不需要用掩码则传入 [] 即可
 * @param data1: 第一帧的数据
 * @param data2: 第二帧的数据（optional）
 * @return result 封装好的帧数据
 */
function encodeWebsocketFrame(maskingKey, data1, data2) {
  let result
  const mask = maskingKey &amp;#x26;&amp;#x26; maskingKey.length === 4 ? maskingKey : []
  if (data2) {
    const dataBuf1 = stringToUtf8(data1)
    const dataBuf2 = stringToUtf8(data2)
    const frame1 = Buffer.concat(
      [
        Buffer.from([0b00000001, dataBuf1.length + (mask.length ? 0b10000000 : 0), ...mask]),
        maskCodec(dataBuf1, mask)
      ],
      2 + mask.length + dataBuf1.length
    )
    const frame2 = Buffer.concat(
      [
        Buffer.from([0b10000000, dataBuf2.length + (mask.length ? 0b10000000 : 0), ...mask]),
        maskCodec(dataBuf2, mask)
      ],
      2 + mask.length + dataBuf2.length
    )
    result = [frame1, frame2]
  } else {
    const dataBuf = stringToUtf8(data1)
    result = Buffer.concat(
      [
        Buffer.from([0b10000001, dataBuf.length + (mask.length ? 0b10000000 : 0), ...mask]),
        maskCodec(dataBuf, mask)
      ],
      2 + mask.length + dataBuf.length
    )
  }
  console.log(result)
  return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的代码在 &lt;a href=&quot;https://github.com/Clloz/network-note/tree/master/WebSocket&quot; title=&quot;github&quot;&gt;Github&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;WebSocket 的安全&lt;/h2&gt;
&lt;p&gt;由于 &lt;code&gt;WebSocket&lt;/code&gt; 客户端并不是局限在浏览端，所以和常规的 &lt;code&gt;HTTP&lt;/code&gt; 请求不一样，&lt;code&gt;WebSocket&lt;/code&gt; 没有同源策略的限制。因此，&lt;code&gt;WebSocket&lt;/code&gt; 服务器在建立连接时必须根据预期的来源验证 &lt;code&gt;Origin&lt;/code&gt; 头，以避免跨站点 &lt;code&gt;WebSocket&lt;/code&gt; 劫持攻击（类似于跨站点请求伪造），当连接使用 &lt;code&gt;cookie&lt;/code&gt; 或 &lt;code&gt;HTTP&lt;/code&gt; 进行身份验证时可能会发生这种情况验证。当敏感（私人）数据通过 &lt;code&gt;WebSocket&lt;/code&gt; 传输时，最好使用令牌或类似的保护机制来验证 &lt;code&gt;WebSocket&lt;/code&gt; 连接。&lt;/p&gt;
&lt;h2&gt;参考文档&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/WebSocket&quot; title=&quot;WebSocket - wikipedia&quot;&gt;WebSocket - wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000023402628&quot; title=&quot;你不知道的 WebSocket&quot;&gt;你不知道的 WebSocket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.javascript.info/long-polling#chang-gui-lun-xun&quot; title=&quot;长轮询 - JAVASCRIPT.INFO&quot;&gt;长轮询 - JAVASCRIPT.INFO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/chencl1986/article/details/88411056&quot; title=&quot;原生实现 WebSocket 应用&quot;&gt;原生实现 WebSocket 应用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://javascript.info/websocket&quot; title=&quot;websocket - JAVASCRIPT-INFO&quot;&gt;websocket - JAVASCRIPT-INFO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://security.tencent.com/index.php/blog/msg/119&quot; title=&quot;WebSocket 安全问题分析&quot;&gt;WebSocket 安全问题分析&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>JS 中的二进制数据的操作</title><link>https://clloz.com/blog/js-binary-data-manipulate</link><guid isPermaLink="true">https://clloz.com/blog/js-binary-data-manipulate</guid><pubDate>Mon, 06 Dec 2021 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 设计的初期，没有预计到这个语言会得到如此广泛的应用和发展，也没有想过会处理如此复杂的业务，所以也没有添加对二进制数据处理的支持。但是现在很多场景需要我们处理二进制数据，比如 &lt;code&gt;canvas&lt;/code&gt; 的图像处理，&lt;code&gt;WebGL&lt;/code&gt; 与显卡的通信，一些音视频文件的处理（比如语音对讲的功能实现），&lt;code&gt;ajax&lt;/code&gt; 的二进制输出传输，文件处理（创建，上传，下载）等。需求的改变必然推动 &lt;code&gt;JavaScript&lt;/code&gt; 对二进制数据处理的支持，目前我们在 &lt;code&gt;JavaScript&lt;/code&gt; 中处理二进制主要依赖几个对象 &lt;code&gt;ArrayBuffer&lt;/code&gt;，&lt;code&gt;TypedArray&lt;/code&gt;， &lt;code&gt;DataView&lt;/code&gt;， &lt;code&gt;Blob&lt;/code&gt; 和 &lt;code&gt;File&lt;/code&gt;等。本文详细探索一下前端二进制处理相关的内容。&lt;/p&gt;
&lt;h2&gt;为什么用二进制&lt;/h2&gt;
&lt;p&gt;首先说两个计算机中常见的概念 &lt;code&gt;stream&lt;/code&gt; 流和 &lt;code&gt;buffer&lt;/code&gt; 缓冲。&lt;/p&gt;
&lt;h2&gt;stream 流&lt;/h2&gt;
&lt;p&gt;我们经常听到的字节流，视频流，文件流，这个流要怎么理解。首先这个 &lt;code&gt;stream&lt;/code&gt; 肯定就是借用我们现实世界的流的概念来形象化地表示计算机中的抽象概念，比如世界的流入水流气流，所以抽象到计算机中其表示的就是一段连续的数据。比如水龙头，当我们打开开关（开始产生数据），流就产生了，这些数据没有绝对位置，也没有确定的开头和结尾，只是不断产生，并随着时间向前流动，你可以随时截取流中的一段数据进行处理。&lt;/p&gt;
&lt;h2&gt;buffer 缓冲&lt;/h2&gt;
&lt;p&gt;比如我们做音视频处理一般会涉及到 &lt;code&gt;buffer&lt;/code&gt; 的设置，其实就是我们开辟了一块空间叫做 &lt;code&gt;buffer&lt;/code&gt;，当数据流装满这个空间的时候我们在一次处理整个 &lt;code&gt;buffer&lt;/code&gt; 中的数据。因为很多时候我们的 &lt;code&gt;stream&lt;/code&gt; 的速度不是确定的，为了保证数据的生产和消费的速度相匹配，保证一个稳定的输出。&lt;/p&gt;
&lt;p&gt;说完了两个概念我们来说一说为什么要使用二进制数据，可以确定的是这肯定是为了性能。因为二进制就是我们的数据在内存中的形式，直接操作内存的效率肯定是最高的。平时我们开发 &lt;code&gt;web&lt;/code&gt; 应用基本不需要考虑性能问题，因为 &lt;code&gt;Web&lt;/code&gt; 应用基本都是 &lt;code&gt;IO&lt;/code&gt; 密集型的，以如今的个人电脑和移动设备的性能，绝大多数的 &lt;code&gt;web&lt;/code&gt; 应用的数据量和数据处理都不是瓶颈，即使我们用了比较糟糕的数据结构和算法基本也不存在问题 :laughing:。但是在涉及到像是 &lt;code&gt;canvas&lt;/code&gt; 和 &lt;code&gt;webgl&lt;/code&gt; 这样每一帧都需要渲染大量像素的场景下，性能就非常重要，比如 &lt;code&gt;webgl&lt;/code&gt; 我们就需要连续的内存交给底层的 &lt;code&gt;C&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt; 去处理。再比如像是前端录制音频和传输，本身就是连续采样的大量的模拟转数字的数据，自然用二进制来处理是最合适的。你可以想象一下上面的这些场景如果我们把数据都存放到数组中我们需要遍历数组额外做很多操作，这样性能肯定大受影响，在这样的 &lt;code&gt;CPU&lt;/code&gt; 密集型工作显然要优先以性能为第一优先级。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于 &lt;code&gt;JS&lt;/code&gt; 中为什么使用二进制和引擎的一些细节可以参考 &lt;a href=&quot;https://stackoverflow.com/questions/13328658/are-the-advantages-of-typed-arrays-in-javascript-is-that-they-work-the-same-or-s&quot; title=&quot;Are the advantages of Typed Arrays in JavaScript is that they work the same or similar in C?&quot;&gt;Are the advantages of Typed Arrays in JavaScript is that they work the same or similar in C?&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;JS 中的二进制相关对象&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中的二进制相关对象主要是 &lt;code&gt;ArrayBuffer&lt;/code&gt;，&lt;code&gt;TypedArray&lt;/code&gt;（只是一个统称，没有一个叫 &lt;code&gt;TypedArray&lt;/code&gt; 的对象），&lt;code&gt;DataView&lt;/code&gt;，&lt;code&gt;Blob&lt;/code&gt; 和 &lt;code&gt;File&lt;/code&gt;。其中前三个可以算是真正的二进制操作，后面两个是二进制大对象的操作，不能进行内部的修改。下面我们详细说一说这些对象。&lt;/p&gt;
&lt;h2&gt;ArrayBuffer&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ArrayBuffer&lt;/code&gt; 是 &lt;code&gt;JavaScript&lt;/code&gt; 中基础的二进制对象，是一个固定长度连续内存区域的引用，我们可以用 &lt;code&gt;ArrayBuffer&lt;/code&gt; 构造函数来创建一个新的 &lt;code&gt;ArrayBuffer&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const buffer = new ArrayBuffer(16) // 开辟了一个 16 字节的内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;code&gt;ArrayBuffer&lt;/code&gt; 虽然名字里面有 &lt;code&gt;Array&lt;/code&gt; ，但是和 &lt;code&gt;Array&lt;/code&gt; 没有任何关系。该构造函数只是创建一个通用的固定长度的原始二进制数据缓冲区，&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ArrayBuffer&lt;/code&gt; 并没有暴露太多方法和属性，构造函数本身有一个静态方法，&lt;code&gt;isView&lt;/code&gt; 用来判断所给参数是否是一个 &lt;code&gt;ArrayBuffer&lt;/code&gt; 的视图，其实就是判断是否是一个 &lt;code&gt;TypedArray&lt;/code&gt; 或者 &lt;code&gt;DataView&lt;/code&gt; 的实例。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ArrayBuffer.prototype&lt;/code&gt; 上暴露了一个属性 &lt;code&gt;byteLength&lt;/code&gt; 和一个方法 &lt;code&gt;slice&lt;/code&gt;，前者很简单就是二进制缓冲区的字节数，后者是用来复制这个缓冲区的内容到一个新的 &lt;code&gt;ArrayBuffer&lt;/code&gt; 中，接受两个参数，一个是开始的字节索引，另一个是结束的字节索引，不包括结束的这个字节。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;数据的处理速度当然是越高越好的，但是此消彼长，直接操作内存当然效率很高，但是开发过程就要复杂的多，如果只是处理比较简单或者少量的数据就完全没必要，所以实际写程序实际是根据具体的场景选择合适的工具和技术。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;TypedArray&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ArrayBuffer&lt;/code&gt; 帮我们开辟了一块缓冲区，我们是不能直接对这块缓冲区进行操作，需要借助视图，也就是 &lt;code&gt;TypedArray&lt;/code&gt; 或者 &lt;code&gt;DataView&lt;/code&gt;，先来介绍以下 &lt;code&gt;TypedArray&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;首先要说的是并没有一个叫 &lt;code&gt;TypedArray&lt;/code&gt; 的可访问对象，他是对类型化数组的一个统称，实际上标准中定义了 &lt;code&gt;TypedArray&lt;/code&gt; 的构造函数，不过这个构造函数并没有暴露出来，不过可以通 &lt;code&gt;Object.getPrototypeOf(Int8Array)&lt;/code&gt; 来访问到。所有我们能访问的十一种 &lt;code&gt;TypedArray&lt;/code&gt; 构造函数比如 &lt;code&gt;Int8Array&lt;/code&gt; 的 &lt;code&gt;[[prototype]]&lt;/code&gt; 都指向标准中的 &lt;code&gt;TypedArray&lt;/code&gt; 构造函数，而 &lt;code&gt;Int8Array.prototype&lt;/code&gt; 的 &lt;code&gt;[[prototype]]&lt;/code&gt; 则又指向标准中的 &lt;code&gt;TypedArray.prototype&lt;/code&gt;，所以表达式 &lt;code&gt;Object.getPrototypeOf(Int8Array).prototype === Int8Array.prototype.__proto__&lt;/code&gt; 的结果为 &lt;code&gt;true&lt;/code&gt;，它们的关系如下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/typedarray2.DmJJ-mw6_Z1hYFdL.webp&quot; alt=&quot;typedarray2&quot; title=&quot;typedarray2&quot;&gt;&lt;/p&gt;
&lt;p&gt;一共有 &lt;code&gt;11&lt;/code&gt; 种不同的 &lt;code&gt;TypedArray&lt;/code&gt;，见下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/typedarray.DruYjP0r_Z11eiXW.webp&quot; alt=&quot;typedarray&quot; title=&quot;typedarray&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;two&apos;s complement 是补码的意思，octet 是八位字节&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这 &lt;code&gt;11&lt;/code&gt; 种 &lt;code&gt;TypedArray&lt;/code&gt; 就是用来创建操作底层二进制缓冲区的视图的，为了处理不同数据类型的数据而被区分成了不同 &lt;code&gt;size&lt;/code&gt; 不同含义的类型。**&lt;code&gt;TypedeArray&lt;/code&gt; 必须使用 &lt;code&gt;new&lt;/code&gt; 来创建，直接调用会报错！**每个 &lt;code&gt;TypedArray&lt;/code&gt; 构造函数有多种参数搭配： 1. 无参数，相当于传入 &lt;code&gt;0&lt;/code&gt; 作为 &lt;code&gt;length&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const initNoArg = new Int8Array()
console.log(initNoArg.byteLength) // 0
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;只传入一个 &lt;code&gt;length&lt;/code&gt; 的时候就会创建一个 &lt;code&gt;length&lt;/code&gt; 字节的 &lt;code&gt;array buffer&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const initWithLength = new Int8Array(3)
for (const byte of initWithLength) {
  console.log(byte)
}
/*
 * 0
 * 0
 * 0
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;可以传入一个 &lt;code&gt;TypedArray&lt;/code&gt; 的实例作为参数，新创建的 &lt;code&gt;array buffer&lt;/code&gt; 的长度（&lt;strong&gt;注意是 length ，不是 byteLength&lt;/strong&gt;）和参数是相同的，会先把参数 &lt;code&gt;TypedArray&lt;/code&gt; 中的每一个值转为新的 &lt;code&gt;TypedArray&lt;/code&gt; 所对应的类型然后再赋值。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const typedArray = new Int8Array(3)
for (let i = 0; i &amp;#x3C; typedArray.length; i += 1) {
  typedArray[i] = i
}
const initWithTypedArray = new Int16Array(typedArray)
for (let i = 0; i &amp;#x3C; initWithTypedArray.length; i += 1) {
  console.log(initWithTypedArray[i])
}
/*
 * 0
 * 1
 * 2
 */
console.log(typedArray.length, initWithTypedArray.length) // 3 3
console.log(typedArray.byteLength, initWithTypedArray.byteLength) // 3 6
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;可以传入一个对象作为参数，该对象必须为类数组对象或者可迭代对象，和 &lt;code&gt;TypedArray.from&lt;/code&gt; 相似的结果。注意这个作为参数的对象中的迭代值如果是非数字和 &lt;code&gt;BigInt&lt;/code&gt; 请参考下面的&lt;strong&gt;注意&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const obj = { length: 3 }
const initWithObj = new Int8Array(obj)
for (let i = 0; i &amp;#x3C; initWithObj.length; i += 1) {
  console.log(initWithObj[i])
}
/*
 * 0
 * 0
 * 0
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;上面的几个都是创建新的 &lt;code&gt;array buffer&lt;/code&gt;，我们也可以传入一个 &lt;code&gt;array buffer&lt;/code&gt;，这样就直接生成了这个指定 &lt;code&gt;array buffer&lt;/code&gt; 视图，还有两个可选的参数 &lt;code&gt;byteOffset&lt;/code&gt; 和 &lt;code&gt;length&lt;/code&gt;，我们可以用这两个参数选择 &lt;code&gt;ArrayBuffer&lt;/code&gt; 的指定区域建立视图。这样我们就可以在一段 &lt;code&gt;ArrayBuffer&lt;/code&gt; 上使用不同类型的 &lt;code&gt;TypedArray&lt;/code&gt;，因为很多时候我们的 &lt;code&gt;ArrayBuffer&lt;/code&gt; 中不都是相同类型的数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;**注意：**如果我们给 &lt;code&gt;TypedArray&lt;/code&gt; 赋值的时候使用的不是数字，比如构造函数传入类数组对象，比如 &lt;code&gt;TypedArray.from&lt;/code&gt;，根据 &lt;a href=&quot;https://tc39.es/ecma262/multipage/indexed-collections.html#sec-settypedarrayfromarraylike&quot; title=&quot;标准&quot;&gt;标准&lt;/a&gt;，这些值会先转为数字或者 &lt;code&gt;BigInt&lt;/code&gt;，然后用 &lt;a href=&quot;https://tc39.es/ecma262/multipage/structured-data.html#sec-numerictorawbytes&quot; title=&quot; NumericToRawBytes&quot;&gt;NumericToRawBytes&lt;/a&gt; 方法转为 &lt;code&gt;RowBypte&lt;/code&gt;，这里不同的 &lt;code&gt;RowByte&lt;/code&gt; 类型的处理可能略有不同，比如 &lt;code&gt;Int8&lt;/code&gt; 类型将 &lt;code&gt;NaN, +0, -0, +∞, or -∞&lt;/code&gt; 全部处理为 &lt;code&gt;+0&lt;/code&gt;，具体细节参考 &lt;a href=&quot;https://tc39.es/ecma262/multipage/indexed-collections.html#table-the-typedarray-constructors&quot; title=&quot;标准&quot;&gt;标准&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以像数组一样直接用 &lt;code&gt;index&lt;/code&gt; 索引访问 &lt;code&gt;TypedArray&lt;/code&gt;，也可以对其进行赋值。正常情况下我们使用 &lt;code&gt;Number&lt;/code&gt; 和 &lt;code&gt;BigInt&lt;/code&gt; 来赋值，&lt;code&gt;BigInt&lt;/code&gt; 主要是为了处理 &lt;code&gt;BigInt64Array&lt;/code&gt; 和 &lt;code&gt;BigUnit64Array&lt;/code&gt;，他们都是 &lt;code&gt;8&lt;/code&gt; 字节 &lt;code&gt;64&lt;/code&gt; 位的，超出了 &lt;code&gt;JavaScript&lt;/code&gt; 能表示的最大安全整数 &lt;code&gt;2 ^ 53 - 1&lt;/code&gt;，所以要使用 &lt;code&gt;BigInt&lt;/code&gt; 类型。使用除了这两个类型的其他类型并没有实际意义，如果你对其他类型的行为感兴趣，还是参考上面的 &lt;code&gt;注意&lt;/code&gt; 中给出的标准中的链接。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TypedArray&lt;/code&gt; 有两个静态属性，&lt;code&gt;BYTES_PER_ELEMENT&lt;/code&gt; 和 &lt;code&gt;name&lt;/code&gt;，前者就是每个索引对应的字节数，参考上面的那张表，第二个就是实例对应的构造函数的名字。还有两个静态方法，&lt;code&gt;TypedArray.from&lt;/code&gt; 和 &lt;code&gt;TypedArray.of&lt;/code&gt; 可以类比 &lt;code&gt;Array.from&lt;/code&gt; 和 &lt;code&gt;Array.of&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;关于挂载在 &lt;code&gt;TypedArray&lt;/code&gt; 上的属性和方法我就不一一介绍了，很多都可以和数组进行类比，详细内容查阅 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt;，我这里主要介绍两个普通数组中没有的方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;其实 &lt;code&gt;TypedArray&lt;/code&gt; 就是给了我们一个在 &lt;code&gt;JavaScript&lt;/code&gt; 中以类似数组的方式来查看和操作二进制数据（也可以理解为我们在操作 &lt;code&gt;C&lt;/code&gt; 语言中的数据类型），所以才被称为类型化数组 &lt;code&gt;TypedArray&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;TypedArray.prototype.set()&lt;/h5&gt;
&lt;p&gt;这个方法是用数组或者 &lt;code&gt;TypedArray&lt;/code&gt; 作为参数来设置 &lt;code&gt;TypedArray&lt;/code&gt; 的值，语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;set(array)
set(array, offset)

set(typedarray)
set(typedarray, offset)

const buffer = new ArrayBuffer(8)
const uint8 = new Uint8Array(buffer)
uint8.set([1, 2, 3], 3)
console.log(uint8)
// Uint8Array(8) [
//     0, 0, 0, 1,
//    2, 3, 0, 0
// ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个参数就是我们的复制源，第二个参数表示复制的目标 &lt;code&gt;TypedArray&lt;/code&gt; 的起始位置。如果复制源（参数）的长度加上 &lt;code&gt;offset&lt;/code&gt; 已经超出了复制目标的长度，则会抛出错误。&lt;/p&gt;
&lt;p&gt;浏览器没有提供像 &lt;code&gt;NodeJS&lt;/code&gt; 的 &lt;code&gt;Buffer&lt;/code&gt; 类一样的对 &lt;code&gt;TypedArray&lt;/code&gt; 类的扩展，所以操作起来比较麻烦的，主要是前端一般使用 &lt;code&gt;TypedArray&lt;/code&gt; 的场景并不多，比如 &lt;code&gt;concat&lt;/code&gt; 这样的操作都得要借助 &lt;code&gt;set&lt;/code&gt; 方法。&lt;/p&gt;
&lt;h5&gt;TypedArray.prototype.subarray()&lt;/h5&gt;
&lt;p&gt;这个方法是赋值 &lt;code&gt;TypedArray&lt;/code&gt; 的一部分，和被复制的 &lt;code&gt;TypedArray&lt;/code&gt; 所对应的 &lt;code&gt;ArrayBuffer&lt;/code&gt; 是同一个，所以我们通过任意一个视图修改 &lt;code&gt;ArrayBuffer&lt;/code&gt; 也会影响到另一个视图，因为我们修改的是同一个 &lt;code&gt;ArrayBuffer&lt;/code&gt;。&lt;strong&gt;需要特别注意的是&lt;/strong&gt;和 &lt;code&gt;TypedArray.prototype.slice&lt;/code&gt; 的区别，他们都是通过 &lt;code&gt;begin&lt;/code&gt; 和 &lt;code&gt;end&lt;/code&gt; 索引（左闭右开）生成新视图，但是 &lt;code&gt;slice&lt;/code&gt; 生成的视图会同时生成一个新的 &lt;code&gt;ArrayBuffer&lt;/code&gt;，而 &lt;code&gt;subarray&lt;/code&gt; 不会。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const uint8 = new Uint8Array([10, 20, 30, 40, 50])
const array1 = uint8.slice(1)
const array2 = uint8.subarray(1)
uint8[1] = 100
console.log(array1)
console.log(array2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;字节序的影响&lt;/h5&gt;
&lt;p&gt;平时我们编写 &lt;code&gt;JavaScript&lt;/code&gt; 代码不会关心 &lt;code&gt;endianness&lt;/code&gt; &lt;strong&gt;字节&lt;/strong&gt;顺序，但是在进行操作二进制数据的时候字节顺序就非常重要了。字节顺序分为两种: 1. &lt;code&gt;Big-Endian&lt;/code&gt; 大端序：数据的低位字节存放在内存的高位地址，高位字节存放在内存的低位地址。 2. &lt;code&gt;Little-Endian&lt;/code&gt; 小端序：数据的低位存放在内存的低位地址处，高位存放在内存的高位地址。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意是字节顺序，而不是 &lt;code&gt;bit&lt;/code&gt; 顺序，也就是对于字节内的位的排列还是按照正常的右侧为低位，左侧为高位，因为字节才是计算机寻址的最小单位。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大端序比较符合我们人类的阅读习惯，简单来说就是把数据按照我们的书写方式依次存入内存中，但是这不符合计算机的读取方式，因为计算机的计算都是从低位开始的，而内存的读取肯定是从低到高的，所以把低位存放到内存的低位肯定是更高效的，所以现在的计算机 &lt;code&gt;CPU&lt;/code&gt; 一般都采取的小端序存入内存，但是在网络传输则使用大端序。关于字节顺序参考 &lt;a href=&quot;https://cloud.tencent.com/developer/article/1802637&quot; title=&quot;什么是大端序和小端序，为什么要有字节序&quot;&gt;什么是大端序和小端序，为什么要有字节序&lt;/a&gt; 和 &lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E5%BA%8F&quot; title=&quot;字节序 - wikipedia&quot;&gt;字节序 - wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们现在使用的 &lt;code&gt;CPU&lt;/code&gt; 基本都是 &lt;code&gt;x86&lt;/code&gt; 或者 &lt;code&gt;ARM&lt;/code&gt; 架构的，&lt;code&gt;x86&lt;/code&gt; 使用的小端序，&lt;code&gt;ARM&lt;/code&gt; 是可以配置的，所以我们一般认为 &lt;code&gt;TypedArray&lt;/code&gt; 工作在小端序下即可，看下面这段代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const buffer = new ArrayBuffer(8)
const int8Array = new Int8Array(buffer)
int8Array[0] = 30
int8Array[1] = 41
const int16Array = new Int16Array(buffer)
console.log(int16Array[0])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们创建了一个 &lt;code&gt;8&lt;/code&gt; 字节的 &lt;code&gt;ArrayBuffer&lt;/code&gt;，然后用一个 &lt;code&gt;Int8Array&lt;/code&gt; 的视图将 &lt;code&gt;ArrayBuffer&lt;/code&gt; 的第一和第二字节分别写入了 &lt;code&gt;30&lt;/code&gt; 和 &lt;code&gt;41&lt;/code&gt;，也就是分别是二进制的 &lt;code&gt;00011110&lt;/code&gt; 和 &lt;code&gt;00101001&lt;/code&gt;，如果按照我们人类的思维模式，也就是大端序，此时 &lt;code&gt;ArrayBuffer&lt;/code&gt; 的前两个字节也就是 &lt;code&gt;0001111000101001&lt;/code&gt;，也就是 &lt;code&gt;7721&lt;/code&gt;。但是我们用一个 &lt;code&gt;Int16Array&lt;/code&gt; 一次读取两个字节，可以发现得到的结果是 &lt;code&gt;10526&lt;/code&gt;。其实这就是因为计算机是用小端序来进行处理的，也就是实际上 &lt;code&gt;ArrayBuffer&lt;/code&gt; 中的前两字节是 &lt;code&gt;01111000&lt;/code&gt; 和 &lt;code&gt;10010100&lt;/code&gt;，合并为 &lt;code&gt;0111100010010100&lt;/code&gt;，然后我们读取的时候最右侧才是最高位，也就是取反一下，得到 &lt;code&gt;0010100100011110&lt;/code&gt;，结果正好就是 &lt;code&gt;10526&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里顺便说一下 &lt;code&gt;NodeJS&lt;/code&gt; 中的 &lt;code&gt;Buffer&lt;/code&gt; 类就是继承自 &lt;code&gt;Uint8Array&lt;/code&gt;，提供了一些浏览器端没有的 &lt;code&gt;API&lt;/code&gt;，比如读写多字节 &lt;code&gt;readUIntBE&lt;/code&gt; &lt;code&gt;writeUIntBe&lt;/code&gt; 等，&lt;code&gt;NodeJS&lt;/code&gt; 虽然也支持 &lt;code&gt;Uint8Array&lt;/code&gt;，但是可以使用更好用的 &lt;code&gt;Buffer&lt;/code&gt; 子类。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;DataView&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;DataView&lt;/code&gt; 是另一种形式的视图，它相当于把我们的 &lt;code&gt;TypedArray&lt;/code&gt; 中的各个类型变成方法，我们可以在一个视图中读取和写入各种类型的二进制数据。并且在 &lt;code&gt;DataView&lt;/code&gt; 中我们可以自己配置字节序。&lt;/p&gt;
&lt;p&gt;和 &lt;code&gt;TypedArray&lt;/code&gt; 一样，&lt;code&gt;DataView&lt;/code&gt; 必须通过 &lt;code&gt;new&lt;/code&gt; 操作符作为构造函数来调用，直接调用会报错。和 &lt;code&gt;TypedArray&lt;/code&gt; 不同的是，&lt;code&gt;new DataVIew&lt;/code&gt; 必须有一个 &lt;code&gt;ArrayBuffer&lt;/code&gt; 作为参数，如果第一个参数不是 &lt;code&gt;ArrayBuffer&lt;/code&gt; 同样会报错。另外还有两个可选参数，&lt;code&gt;byteOffset&lt;/code&gt; 表示从 &lt;code&gt;ArrayBuffer&lt;/code&gt; 的第几个字节开始建立 &lt;code&gt;DataView&lt;/code&gt; 视图（如果没传则从第一个字节开始），&lt;code&gt;ByteLength&lt;/code&gt; 表示 &lt;code&gt;DataView&lt;/code&gt; 的字节数（如果没有传则和 &lt;code&gt;ArrayBuffer&lt;/code&gt; 相同，&lt;code&gt;byteOffset&lt;/code&gt; 和 &lt;code&gt;byteLength&lt;/code&gt; 之和不能超过 &lt;code&gt;ArrayBuffer&lt;/code&gt; 的 &lt;code&gt;byteLength&lt;/code&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const buffer = new ArrayBuffer(16)
const view1 = new DataView(buffer)
const view2 = new DataView(buffer, 5)
const view3 = new DataView(buffer, 8, 2)
const view4 = new DataView(buffer, 8, 9) // 报错
console.log(view1)
console.log(view2)
console.log(view3)
// DataView {
//     byteLength: 16,
//     byteOffset: 0,
//     buffer: ArrayBuffer {
//     [Uint8Contents]: &amp;#x3C;00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00&gt;,
//         byteLength: 16
//     }
// }
// DataView {
//     byteLength: 11,
//     byteOffset: 5,
//     buffer: ArrayBuffer {
//     [Uint8Contents]: &amp;#x3C;00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00&gt;,
//         byteLength: 16
//     }
// }
// DataView {
//     byteLength: 2,
//     byteOffset: 8,
//     buffer: ArrayBuffer {
//     [Uint8Contents]: &amp;#x3C;00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00&gt;,
//         byteLength: 16
//     }
// }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的输出我们也可以看到 &lt;code&gt;DataView&lt;/code&gt; 有三个属性 &lt;code&gt;buffer&lt;/code&gt;，&lt;code&gt;byteLength&lt;/code&gt; 和 &lt;code&gt;byteOffset&lt;/code&gt;，对应的就是我们构造函数中的三个参数，这三个属性都是在 &lt;code&gt;DataView.prototype&lt;/code&gt; 上。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DataView.prototype&lt;/code&gt; 上共有十对用来读写的 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 方法，&lt;code&gt;get&lt;/code&gt; 方法接受一个 &lt;code&gt;byteOffset&lt;/code&gt; 作为参数，&lt;code&gt;set&lt;/code&gt; 方法接受 &lt;code&gt;byteOffset&lt;/code&gt; 和 &lt;code&gt;value&lt;/code&gt; 作为参数，&lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 都能传一个可选的 &lt;code&gt;littleEndian&lt;/code&gt; 参数，如果为 &lt;code&gt;true&lt;/code&gt; 则为小端序，如果为 &lt;code&gt;false&lt;/code&gt; 或者 &lt;code&gt;undefined&lt;/code&gt; 则为大端序，见下表。&lt;/p&gt;
&lt;p&gt;| 方法名       | 功能                                                                                       |
| ------------ | ------------------------------------------------------------------------------------------ |
| getInt8      | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 读取一个字节，返回该字节表示的 &lt;code&gt;8&lt;/code&gt; 位有符号整数                      |
| getUint8     | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 读取一个字节，返回该字节表示的 &lt;code&gt;8&lt;/code&gt; 为无符号整数                      |
| getInt16     | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 读取两个字节，返回该字节表示的 &lt;code&gt;16&lt;/code&gt; 位有符号整数                     |
| getUint16    | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 读取两个字节，返回该字节表示的 &lt;code&gt;16&lt;/code&gt; 位无符号整数                     |
| getInt32     | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 读取四个字节，返回该字节表示的 &lt;code&gt;32&lt;/code&gt; 位有符号整数                     |
| getInt32     | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 读取四个字节，返回该字节表示的 &lt;code&gt;32&lt;/code&gt; 位无符号整数                     |
| getFloat32   | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 读取四个字节，返回该字节表示的 &lt;code&gt;32&lt;/code&gt; 位有符号&lt;code&gt;IEEE-754&lt;/code&gt; 浮点数        |
| getFloat64   | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 读取八个字节，返回该字节表示的 &lt;code&gt;64&lt;/code&gt; 位有符号&lt;code&gt;IEEE-754&lt;/code&gt;浮点数         |
| getBigInt64  | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 读取八个字节，返回该字节表示的 &lt;code&gt;64&lt;/code&gt; 位有符号整数                     |
| getBigUint64 | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 读取八个字节，返回该字节表示的 &lt;code&gt;64&lt;/code&gt; 位无符号整数                     |
| setInt8      | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 写入一个字节，写入值为 &lt;code&gt;value&lt;/code&gt; 转换的 &lt;code&gt;8&lt;/code&gt; 位有符号整数               |
| setUint8     | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 写入一个字节，写入值为 &lt;code&gt;value&lt;/code&gt; 转换的 &lt;code&gt;8&lt;/code&gt; 为无符号整数               |
| setInt16     | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 写入两个字节，写入值为 &lt;code&gt;value&lt;/code&gt; 转换的 &lt;code&gt;16&lt;/code&gt; 位有符号整数              |
| setUint16    | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 写入两个字节，写入值为 &lt;code&gt;value&lt;/code&gt; 转换的 &lt;code&gt;16&lt;/code&gt; 位无符号整数              |
| setInt32     | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 写入四个字节，写入值为 &lt;code&gt;value&lt;/code&gt; 转换的 &lt;code&gt;32&lt;/code&gt; 位有符号整数              |
| setInt32     | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 写入四个字节，写入值为 &lt;code&gt;value&lt;/code&gt; 转换的 &lt;code&gt;32&lt;/code&gt; 位无符号整数              |
| setFloat32   | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 写入四个字节，写入值为 &lt;code&gt;value&lt;/code&gt; 转换的 &lt;code&gt;32&lt;/code&gt; 位有符号&lt;code&gt;IEEE-754&lt;/code&gt; 浮点数 |
| setFloat64   | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 写入八个字节，写入值为 &lt;code&gt;value&lt;/code&gt; 转换的 &lt;code&gt;64&lt;/code&gt; 位有符号&lt;code&gt;IEEE-754&lt;/code&gt;浮点数  |
| setBigInt64  | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 写入八个字节，写入值为 &lt;code&gt;value&lt;/code&gt; 转换的 &lt;code&gt;64&lt;/code&gt; 位有符号整数              |
| setBigUint64 | 在指定的 &lt;code&gt;byteOffset&lt;/code&gt; 写入八个字节，写入值为 &lt;code&gt;value&lt;/code&gt; 转换的 &lt;code&gt;64&lt;/code&gt; 位无符号整数              |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在读取或者写入超过一个字节的时候，字节序是非常重要的，如果字节序搞错了很可能导致非常离谱的结果，例子可以看上面的 &lt;code&gt;TypedArray&lt;/code&gt; 讲字节序时举的例子。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于不支持 &lt;code&gt;BigInt&lt;/code&gt; 的&lt;a href=&quot;https://caniuse.com/?search=bigint&quot; title=&quot;浏览器&quot;&gt;浏览器&lt;/a&gt;，&lt;code&gt;getBigInt64&lt;/code&gt;, &lt;code&gt;getBigUint64&lt;/code&gt;，&lt;code&gt;setBigInt64&lt;/code&gt; 和 &lt;code&gt;setBigUint64&lt;/code&gt; 无法使用，具体的兼容办法参考 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView#64-bit_integer_values&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Blob&lt;/h2&gt;
&lt;p&gt;上面介绍的操作二进制的对象你可能没有接触过，但是只要你处理过和文件相关的 &lt;code&gt;API&lt;/code&gt;，比如上传下载文件，你肯定接触过 &lt;code&gt;Blob&lt;/code&gt; 对象。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意 &lt;code&gt;Blob&lt;/code&gt; 是只有浏览器中才有的对象，在 &lt;code&gt;NodeJS&lt;/code&gt; 中是没有这个对象的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;blob&lt;/code&gt; 的全称是 &lt;code&gt;Binary Large Object&lt;/code&gt;，也就是二进制大对象的意思（作为单个实体存储的二进制数据的集合），最早是用在数据库管理系统中，一般是用来存储音频视频和其他多媒体文件，也有一些二进制可执行代码被存储为一个 &lt;code&gt;blob&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中，&lt;code&gt;Blob&lt;/code&gt; 对象代表一个 &lt;code&gt;blob&lt;/code&gt;, 它是一个类似文件的不可变原始数据对象；它们可以作为文本或二进制数据读取，或转换为 &lt;code&gt;ReadableStream&lt;/code&gt; 以便其方法可用于处理数据。&lt;code&gt;Blobs&lt;/code&gt; 可以用来表示没有采用 &lt;code&gt;JavaScript&lt;/code&gt; 原生格式的数据，&lt;code&gt;File&lt;/code&gt; 接口就是继承自 &lt;code&gt;Blob&lt;/code&gt; 并进行了扩展来支持用户系统中的文件。&lt;/p&gt;
&lt;p&gt;从上面的描述我们可以看出，&lt;code&gt;Blob&lt;/code&gt; 对象也是二进制数据的集合，但是它是一个不可变的对象，我们不能像 &lt;code&gt;ArrayBuffer&lt;/code&gt; 一样利用视图对其进行读写。&lt;/p&gt;
&lt;p&gt;我们可以用 &lt;code&gt;Blob&lt;/code&gt; 构造函数创建一个新的 &lt;code&gt;Blob&lt;/code&gt; 对象，这个构造函数接受一个 &lt;code&gt;array&lt;/code&gt; 作为必选参数，可以理解为构建 &lt;code&gt;Blob&lt;/code&gt; 的数据源，这个数据源的格式必须是数组，数组中的元素必须是 &lt;code&gt;ArrayBuffer&lt;/code&gt;，&lt;code&gt;TypedArray&lt;/code&gt;，&lt;code&gt;Blob&lt;/code&gt;，&lt;code&gt;USVString&lt;/code&gt; 对象。另一个可选参数是 &lt;code&gt;options&lt;/code&gt;，是一个独享，其中比较重要的一个属性就是 &lt;code&gt;type&lt;/code&gt;，默认值是 &lt;code&gt;&quot;&quot;&lt;/code&gt;，这个值是表名这个 &lt;code&gt;blob&lt;/code&gt; 数据的 &lt;code&gt;MIME type&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;USVString&lt;/code&gt; 的概念可以参考 &lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/26/character-encoding/#USVStringDOMStringCSSOMString&quot; title=&quot;搞懂字符编码&quot;&gt;搞懂字符编码&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const text = new Blob([&apos;clloz&apos;], { type: &apos;text/plain&apos; })
console.log(text)
//Blob {size: 5, type: &apos;text/plain&apos;}
//size: 5
//type: &quot;text/plain&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到 &lt;code&gt;Blob&lt;/code&gt; 有两个属性 &lt;code&gt;size&lt;/code&gt; 和 &lt;code&gt;type&lt;/code&gt;，这两个属性都是在 &lt;code&gt;Blob.prototype&lt;/code&gt; 上，分别表示 &lt;code&gt;Blob&lt;/code&gt; 对象中的数据的字节数和 &lt;code&gt;Blob&lt;/code&gt; 对象的 &lt;code&gt;MIME type&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;原型上还有几个方法&lt;/p&gt;
&lt;h3&gt;Blob.prototype.arrayBuffer()&lt;/h3&gt;
&lt;p&gt;该方法返回一个 &lt;code&gt;Promise&lt;/code&gt;，这个 &lt;code&gt;Promise&lt;/code&gt; 的状态是 &lt;code&gt;resolve&lt;/code&gt;，&lt;code&gt;resolve&lt;/code&gt; 的值是一个包含 &lt;code&gt;blob&lt;/code&gt; 中的二进制数据的 &lt;code&gt;ArrayBuffer&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const blob = new Blob([&apos;123&apos;])
const bufferPromise = blob.arrayBuffer()
const buffer = await blob.arrayBuffer()
const view = new Int8Array(buffer)
for (const item of view) {
  console.log(item)
}
// 49
// 50
// 51
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字符串中的 &lt;code&gt;123&lt;/code&gt; 被保存为 &lt;code&gt;unicode&lt;/code&gt; 中的 &lt;code&gt;codepoint&lt;/code&gt;，对应的就是 &lt;code&gt;49， 50，51&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;Blob.prototype.slice()&lt;/h5&gt;
&lt;p&gt;看到 &lt;code&gt;slice&lt;/code&gt; 自然明白就是切片，接受三个可选参数 &lt;code&gt;start&lt;/code&gt;，&lt;code&gt;end&lt;/code&gt; 和 &lt;code&gt;contentType&lt;/code&gt;。&lt;code&gt;start&lt;/code&gt; 和 &lt;code&gt;end&lt;/code&gt; 是左闭右开的截取范围，默认截取全部，如果 &lt;code&gt;start&lt;/code&gt; 大于 &lt;code&gt;blob&lt;/code&gt; 的 &lt;code&gt;size&lt;/code&gt; 则返回一个 &lt;code&gt;size&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt; 的 &lt;code&gt;blob&lt;/code&gt;。&lt;code&gt;start&lt;/code&gt; 或者 &lt;code&gt;end&lt;/code&gt; 如果为负值则从最后一个字节向前计算。&lt;code&gt;contentType&lt;/code&gt; 默认为 &lt;code&gt;&quot;&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const blob = new Blob([&apos;123&apos;])
const newBlob = blob.slice(1)
const bufferPromise = newBlob.arrayBuffer()
const buffer = await newBlob.arrayBuffer()
const view = new Int8Array(buffer)
for (const item of view) {
  console.log(item)
}
// 50
// 51
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Blob.prototype.stream()&lt;/h5&gt;
&lt;p&gt;这个方法返回一个 &lt;code&gt;ReadableStream&lt;/code&gt; 来读取 &lt;code&gt;Blob&lt;/code&gt; 对象中的内容，&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStream&quot; title=&quot;ReadableStream&quot;&gt;ReadableStream&lt;/a&gt; 的相关内容可以参考文档，是 &lt;code&gt;Fetch API&lt;/code&gt; 在 &lt;code&gt;Response&lt;/code&gt; 中提供的一个对象，这里给大家一个例子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const blob = new Blob([&apos;123&apos;])
blob
  .stream()
  .getReader()
  .read()
  .then(({ done, value }) =&gt; {
    console.log(done)
    console.log(value)
  })
// Uint8Array(3) [49, 50, 51, buffer: ArrayBuffer(3), byteLength: 3, byteOffset: 0, length: 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Blob.prototype.text()&lt;/h5&gt;
&lt;p&gt;这个方法返回一个 &lt;code&gt;Promise&lt;/code&gt;，将 &lt;code&gt;Blob&lt;/code&gt; 对象中的内容转为 &lt;code&gt;UTF-8&lt;/code&gt; 返回。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const view = new Int8Array([67, 108, 108, 111, 122])
const blob = new Blob([view])
blob.text().then((val) =&gt; console.log(val))
// Clloz
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;h2&gt;字符串和 ArrayBuffer 的转换&lt;/h2&gt;
&lt;p&gt;在能够确定字符的编码的时候（确定了编码我们就知道字符串在内存中的存储形式），我们可以在字符串和 &lt;code&gt;ArrayBuffer&lt;/code&gt; 之间进行互相转换，比如 &lt;code&gt;UTF-16&lt;/code&gt; 可以用如下代码转换：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ArrayBuffer转为字符串，参数为ArrayBuffer对象
function ab2str(buf) {
  return String.fromCharCode.apply(null, new Uint16Array(buf))
}

// 字符串转为ArrayBuffer对象，参数为字符串
function str2ab(str) {
  var buf = new ArrayBuffer(str.length * 2) // 每个字符占用2个字节
  var bufView = new Uint16Array(buf)
  for (var i = 0, strLen = str.length; i &amp;#x3C; strLen; i++) {
    bufView[i] = str.charCodeAt(i)
  }
  return buf
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;播放 PCM&lt;/h2&gt;
&lt;p&gt;之前做过一个语音对讲的需求，一般来说录音设备直接录制的音频是未经压缩的音频采样数据裸流 &lt;code&gt;PCM（Pulse Code Modulation）&lt;/code&gt;，这个 &lt;code&gt;PCM&lt;/code&gt; 格式是不能直接在前端播放的，我采用的方式是添加一个 &lt;code&gt;wav&lt;/code&gt; 的 &lt;code&gt;header&lt;/code&gt; 转成 &lt;code&gt;wav&lt;/code&gt; 后进行播放，大致的做法如下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 将后端传递的 PCM 字符串转为 buffer 并添加 wav header UTF-16格式字符串
export function addWavHeader(str) {
  const WAV_HEAD_SIZE = 44
  const buffer = new ArrayBuffer(str.length * 2 + WAV_HEAD_SIZE)
  const view = new DataView(buffer)

  // 为 PCM 添加 wav header 转为 wav
  // RIFF chunk descriptor/identifier
  writeUTFBytes(view, 0, &apos;RIFF&apos;)
  // RIFF chunk length
  view.setUint32(4, 44 + str.length * 2, true)
  // RIFF type
  writeUTFBytes(view, 8, &apos;WAVE&apos;)
  // format chunk identifier
  // FMT sub-chunk
  writeUTFBytes(view, 12, &apos;fmt &apos;)
  // format chunk length
  view.setUint32(16, 16, true)
  // sample format (raw)
  view.setUint16(20, 1, true)
  // stereo (2 channels)
  view.setUint16(22, 1, true)
  // sample rate
  view.setUint32(24, 8000, true)
  // byte rate (sample rate * block align)
  view.setUint32(28, 8000 * 2, true)
  // block align (channel count * bytes per sample)
  view.setUint16(32, 2, true)
  // bits per sample
  view.setUint16(34, 16, true)
  // data sub-chunk
  // data chunk identifier
  writeUTFBytes(view, 36, &apos;data&apos;)
  // data chunk length
  view.setUint32(40, str.length * 2, true)

  const length = str.length
  let index = 44
  for (let i = 0; i &amp;#x3C; length; i++) {
    view.setInt16(index, str.charCodeAt(i), true)
    index += 2
  }
  return buffer
}

// 生成 wav 并用 audio 播放
export function genWavAndPlay(buffer) {
  const blob = new Blob([new Uint8Array(buffer)])
  const blobUrl = URL.createObjectURL(blob)
  if (!blobUrlList.length) {
    audioEl.src = blobUrl
    audioEl.play()
  } else {
    blobUrlList.push(blobUrl)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;发送接收二进制数据&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data&quot; title=&quot;JavaScript 接收发送二进制数据&quot;&gt;JavaScript 接收发送二进制数据&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ecma-international.org/publications-and-standards/standards/ecma-262/&quot; title=&quot;ecma-262&quot;&gt;ecma-262&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/27996269&quot; title=&quot;如何理解编程语言中流的概念&quot;&gt;如何理解编程语言中流的概念&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/26190832&quot; title=&quot;cache 和 buffer 都是缓存，主要区别是什么&quot;&gt;cache 和 buffer 都是缓存，主要区别是什么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://javascript.info/arraybuffer-binary-arrays&quot; title=&quot;ArrayBuffer, binary arrays&quot;&gt;ArrayBuffer, binary arrays&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer&quot; title=&quot;ArrayBuffer - MDN&quot;&gt;ArrayBuffer - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray&quot; title=&quot;TypedArray - MDN&quot;&gt;TypedArray - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView&quot; title=&quot;DataView - MDN&quot;&gt;DataView - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jianshu.com/p/b9a77b1891a7&quot; title=&quot;jsmpeg系列一 基础知识 字符处理 ArrayBuffer TypedArray&quot;&gt;jsmpeg系列一 基础知识 字符处理 ArrayBuffer TypedArray&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>设置 Apple Music 全局快捷键</title><link>https://clloz.com/blog/apple-music-global-shortcuts</link><guid isPermaLink="true">https://clloz.com/blog/apple-music-global-shortcuts</guid><pubDate>Wed, 01 Dec 2021 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;以前写过 &lt;a href=&quot;https://www.clloz.com/programming/assorted/2018/10/23/terminal-shortcut/&quot; title=&quot;Mac设置terminal快捷键&quot;&gt;Mac设置terminal快捷键&lt;/a&gt; 讲了一下用 &lt;code&gt;automator&lt;/code&gt; 来添加 &lt;code&gt;quick action&lt;/code&gt; 来用快捷键启动终端，今天在讲一下如何设置 &lt;code&gt;Apple Music&lt;/code&gt; 的全局快捷键来执行上一曲，下一曲和播放暂停。&lt;/p&gt;
&lt;h2&gt;添加 Quick Action&lt;/h2&gt;
&lt;p&gt;整个操作过程和添加终端的快捷键差不多，不过现在 &lt;code&gt;automator&lt;/code&gt; 的界面和原来有些不同，原来齿轮图标是叫 &lt;code&gt;Service&lt;/code&gt;，现在叫 &lt;code&gt;Quick Action&lt;/code&gt;。我们打开 &lt;code&gt;automator&lt;/code&gt;，然后在顶部任务栏中选择 &lt;code&gt;File -&gt; New&lt;/code&gt; 或者直接点解弹出界面的 &lt;code&gt;New Docment&lt;/code&gt;，然后选择其中的 &lt;code&gt;Quick Action&lt;/code&gt;，如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/music-shortcut2.BkBL1Dtw_1Qd9Au.webp&quot; alt=&quot;music-shortcut-2&quot; title=&quot;music-shortcut-2&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后将右侧的 &lt;code&gt;Workflow Recieve&lt;/code&gt; 选择为 &lt;code&gt;no input&lt;/code&gt;，在左侧的 &lt;code&gt;Actions&lt;/code&gt; 中找到 &lt;code&gt;Run AppleScript&lt;/code&gt;，然后双击，在右侧弹出的脚本输入框中输入脚本即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/music-shortcut.ClYAyf2O_Z1iUhqb.webp&quot; alt=&quot;music-shortcut&quot; title=&quot;music-shortcut&quot;&gt;&lt;/p&gt;
&lt;p&gt;脚本如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;# 下一首
tell application &quot;Music&quot;
    if it is running then
        play (next track)
    end if
end tell

# 上一首
tell application &quot;Music&quot;
    if it is running then
    play (previous track)
    end if
end tell

# 播放暂停
tell application &quot;Music&quot;
    if it is running then
        set isPlaying to the player state
        if (isPlaying = playing) then
            pause
        else
            play
        end if
    end if
end tell
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，这里我把三个脚本写在一起了，请根据注释分别创建三个 &lt;code&gt;Quick Action&lt;/code&gt;，注意不要把注释复制进去&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;粘贴好脚本后可以点击一下 &lt;code&gt;▶️&lt;/code&gt; 按钮看看脚本能不能生效，如果没问题就可以保存脚本了，保存一个自己喜欢的名字。&lt;code&gt;Quick Action&lt;/code&gt; 的保存路径在 &lt;code&gt;/Users/Clloz/Library/Services&lt;/code&gt;，可以进去重命名。&lt;/p&gt;
&lt;p&gt;之后就是设置快捷键了，和设置 &lt;code&gt;terminal&lt;/code&gt; 快捷键一样，我们可以进入 &lt;code&gt;System Preferences-&gt;Keyboard-&gt;Shortcuts-&gt;Services-&gt;General&lt;/code&gt;，在里面找到刚刚创建的三个 &lt;code&gt;Quick Action&lt;/code&gt; 的名字，设置快捷键即可。需要注意的是，由于这里设置的是全局快捷键，建议设置得复杂一点，如果设置的太简单很可能和其他应用的快捷键冲突导致不生效。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://sspai.com/post/58907&quot; title=&quot;不使用第三方程序在 mac 上用快捷键全局控制 Spotify&quot;&gt;不使用第三方程序在 mac 上用快捷键全局控制 Spotify&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/macos.RfaH1x-K.jpg"/><enclosure url="/_astro/macos.RfaH1x-K.jpg"/></item><item><title>执行上下文和词法环境</title><link>https://clloz.com/blog/execution-context-and-lexical-environment</link><guid isPermaLink="true">https://clloz.com/blog/execution-context-and-lexical-environment</guid><pubDate>Sat, 27 Nov 2021 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我之前曾经写过 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/03/24/execution-contextscope-chain/&quot; title=&quot;JS 中的执行环境和作用域链&quot;&gt;JS 中的执行环境和作用域链&lt;/a&gt; 和 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/01/variable-hoist/&quot; title=&quot;var，let，const和变量提升（hoist）&quot;&gt;var，let，const和变量提升（hoist）&lt;/a&gt; 两篇文章来总结 &lt;code&gt;JavaScript&lt;/code&gt; 中代码执行和作用域的一些知识点，最近在读 &lt;a href=&quot;https://book.douban.com/subject/30143702/&quot; title=&quot;JavaScript 忍者秘籍（第二版）&quot;&gt;JavaScript 忍者秘籍（第二版）&lt;/a&gt; 的第五章的时候结合 &lt;a href=&quot;https://262.ecma-international.org/12.0/#sec-environment-records&quot; title=&quot;最新版的 Spec&quot;&gt;最新版的 Spec&lt;/a&gt; 将这部分知识又复习梳理了一遍，进行一个新的总结。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ECMAScript 的标准只是一份文档，具体的引擎实现并不完全参照标准，我们只是借助标准来理解引擎的一些行为，除非翻引擎的源码，否则我们很难确定底层的实现细节，首要的目的还是理解行为和机制。对绝大多数前端程序员来说，并没有了解底层实现的必要，如果确有需要并且有实力可以去阅读源码，否则只要大致理解机制即可，不必花费过多时间钻牛角尖。本文所讨论的 ECMAScript 规范的版本范围从 2011 年的 5.1 版本到最新的 2021版本，其他版本不在讨论范围内&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;环境记录 Environment Record 和 词法环境 Lexical Environment&lt;/h2&gt;
&lt;p&gt;这两个概念联系很紧密，它们都是标准中核心章节 &lt;code&gt;Executable Code and Execution Contexts&lt;/code&gt; 中的重要概念。这两个概念一直都是存在的，从 &lt;a href=&quot;https://262.ecma-international.org/5.1/#sec-10.2.1&quot; title=&quot;ES 5.1&quot;&gt;ES 5.1&lt;/a&gt; 的 &lt;code&gt;10.2&lt;/code&gt; 小节可以看出此时的 &lt;code&gt;Environment Records&lt;/code&gt; 只是 &lt;code&gt;Lexical Environment&lt;/code&gt; 下的一个概念，或者说是 &lt;code&gt;Lexical Environment&lt;/code&gt; 中的一个组成部分，基本到 &lt;a href=&quot;https://262.ecma-international.org/10.0/#sec-executable-code-and-execution-contexts&quot; title=&quot;ES2020&quot;&gt;ES2020&lt;/a&gt;之前，这两个概念在标准中都没什么变化，最新的 &lt;a href=&quot;https://262.ecma-international.org/5.1/#sec-10.2.1&quot; title=&quot;ES 2021&quot;&gt;ES 2021&lt;/a&gt; 我们会发现 &lt;code&gt;Lexical Environment&lt;/code&gt; 已经放到了 &lt;code&gt;Execution Contexts&lt;/code&gt; 中，成了执行上下文中的一个概念，而 &lt;code&gt;Environment Records&lt;/code&gt; 则是和执行上下文同级的一个小节，不过它们两个概念依然有很紧密的联系。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：规范中也明确说了 &lt;code&gt;Lexical Environments&lt;/code&gt; 和 &lt;code&gt;Environment Record&lt;/code&gt; 是纯粹的规范内的机制并没有对应某个特定的实现，我们无法在程序中对这些值进行访问。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 &lt;code&gt;2020&lt;/code&gt; 之前的版本中对 &lt;code&gt;Environment Record&lt;/code&gt; 和 &lt;code&gt;Lexical Environment&lt;/code&gt; 的描述就是一个 &lt;code&gt;Specification Tpyes&lt;/code&gt; 就是规范中的类型，规范类型对应于在算法中使用的元值，用于描述 &lt;code&gt;ECMAScript&lt;/code&gt; 语言构造和 &lt;code&gt;ECMAScript&lt;/code&gt; 语言类型的语义。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Lexical Environment&lt;/code&gt; 和 &lt;code&gt;Environment Record&lt;/code&gt; 类型是用来解释嵌套的方法和代码块的标识符解析的行为的。&lt;code&gt;Lexical Environment&lt;/code&gt; 的作用是根据代码的词法嵌套结构来定义标识符和特定的变量或方法的关联。&lt;code&gt;Lexical Environment&lt;/code&gt; 由 &lt;code&gt;Environment Record&lt;/code&gt; 和一个可能为空的外层 &lt;code&gt;Lexical Environment&lt;/code&gt; 组成。通常 &lt;code&gt;Lexical Environment&lt;/code&gt; 和特定的语法结构相关联，比如函数声明，块语句，&lt;code&gt;Try&lt;/code&gt; 语句的 &lt;code&gt;Catch&lt;/code&gt; 子句，每次这些代码执行都会创建一个新的 &lt;code&gt;Lexical Environment&lt;/code&gt;。&lt;code&gt;Lexical Environment&lt;/code&gt; 由该环境的 &lt;code&gt;Environment Record&lt;/code&gt; 和一个可能为 &lt;code&gt;null&lt;/code&gt; 的指向其外部 &lt;code&gt;Lexical Environment&lt;/code&gt; 的引用组成（我们下面称这个引用为 &lt;code&gt;OuterEnv&lt;/code&gt;）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里有两个比较疑惑的点，一个是这里说的是 &lt;code&gt;FunctionDeclaration&lt;/code&gt;，在规范中 &lt;code&gt;FunctionDeclaration&lt;/code&gt; 和 &lt;code&gt;FunctionExpression&lt;/code&gt; 也是不同的，按理说函数声明和函数表达式的执行应该是没本质区别的（虽然有提升的区别）但对于代码的执行应该是一样的，不过标准中在说特定的语法结构用的是 &lt;code&gt;such as&lt;/code&gt; 姑且认为只是列出了部分。另一个是为什么要单独说 &lt;code&gt;Catch&lt;/code&gt; 子句，根据规范的定义，&lt;code&gt;Catch&lt;/code&gt; 子句已经是 &lt;code&gt;Block&lt;/code&gt; 了，有没有必要单独列出来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而一个 &lt;code&gt;Environment Record&lt;/code&gt; 用来记录其所在的 &lt;code&gt;Lexical Environment&lt;/code&gt; 的作用域中创建的标识符绑定。根据规范，&lt;code&gt;Environment Records&lt;/code&gt; 有两种主要的类型 &lt;code&gt;declarative Environment Records&lt;/code&gt; 和 &lt;code&gt;object Environment Records&lt;/code&gt;，前者就是我们常见的函数声明，便令声明等，包括了 &lt;code&gt;variable&lt;/code&gt;，&lt;code&gt;constant&lt;/code&gt;，&lt;code&gt;let&lt;/code&gt;，&lt;code&gt;class&lt;/code&gt;，&lt;code&gt;module&lt;/code&gt;，&lt;code&gt;import&lt;/code&gt; 和 &lt;code&gt;function declarations&lt;/code&gt;，后者则是主要用来关联 &lt;code&gt;WithStatement&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于 &lt;code&gt;Environment Records&lt;/code&gt; 在规范中还介绍了抽象类和实体子类的细节，这里就不展开了，不影响我们理解，有兴趣可以阅读规范。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;Lexical Environment&lt;/code&gt; 的嵌套自然就和我们上面说道的会产生 &lt;code&gt;Lexical Environment&lt;/code&gt; 的结构的嵌套紧密相关，当我们定义了嵌套的函数或者 &lt;code&gt;BlockStatement&lt;/code&gt;，自然会发生 &lt;code&gt;Lexical Environment&lt;/code&gt; 的嵌套，所以 &lt;code&gt;Lexical Environment&lt;/code&gt; 就有一个 &lt;code&gt;Outer Environment&lt;/code&gt; 引用指向其&lt;strong&gt;定义&lt;/strong&gt;时所在的 &lt;code&gt;Lexical Environment&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下面的这部分内容都是我根据标准还有一些资料对引擎实现的一些猜想和推测，我对编译原理是一点不了解，只是把自己的一些想法记录一下&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里有一个点需要注意，&lt;code&gt;Lexical Environment&lt;/code&gt; 是由代码的执行产生的，但是产生的 &lt;code&gt;Lexical Records&lt;/code&gt; 其内部的 &lt;code&gt;Outer Environment&lt;/code&gt; 确是由定义的位置确定的。至于引擎是如何实现这种定位位置的跟踪，我是比较好奇内部的实现的，我猜想应该是预编译的时候再任何函数对象产生的时候，比如说声明提升或者是函数表达式赋值的时候，给内存中产生的函数对象加上一个内部属性 &lt;code&gt;[[OuterEnv]]&lt;/code&gt; 指向当前所在的 &lt;code&gt;Lexical Environment&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在学习这部分内容的时候我还有一个疑问就是闭包和 &lt;code&gt;with&lt;/code&gt;的问题，&lt;code&gt;Lexical Environment&lt;/code&gt; 是在函数执行的时候创建的，那如果我们的函数是从某个内部函数返回的，当这个函数执行的时候，其定义时所在的环境的闭包以及其 &lt;code&gt;Lexical Environment&lt;/code&gt; 是如何保存的，是所有的标识符解析都保存着还是说只保留了这个函数引用的部分，我个人倾向于是值保留了还需要使用的那部分，毕竟内存的消耗肯定是越小越好，当然这就需要预编译的时候引擎做更多事情。我们可以找一段简单的闭包代码来分析一下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;outerVar = 1
function outerFn() {
  const uselessVar = &apos;useless&apos;
  const innerVar = 10
  return function inner() {
    console.log(innerVar)
  }
}
const innerFn = outerFn()
innerFn()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在分析之前我还是先说一下我对闭包 &lt;code&gt;closure&lt;/code&gt; 的理解，其实并不是一个复杂的概念，主要是用在函数式编程语言中的一个概念，由于函数式编程中的函数是 &lt;code&gt;first-class object&lt;/code&gt;，可以作为一个普通对象使用（作为参数，返回值等），所以其&lt;strong&gt;作用域&lt;/strong&gt;的工作方式产生了闭包。在 &lt;code&gt;JavaScript&lt;/code&gt; 中我们定义的函数可以在任何地方调用，如果按照调用的位置确定作用域，那么其作用域会变得不确定，所以函数式编程语言的作用域采用的是函数内定义的参数，变量，方法在函数外不可访问，只有内部定义的方法可以访问（隐藏状态是实现 &lt;code&gt;OOP&lt;/code&gt; 的一个重要特性），一个函数能访问的作用域由其定义的位置确定，所以我们可以说闭包就是函数和其能访问的作用域构成的。我们可以看出无论是 &lt;code&gt;Lexical Environment&lt;/code&gt;，&lt;code&gt;Environment Record&lt;/code&gt; 还是 &lt;code&gt;Closure&lt;/code&gt; 的概念都是在解释函数式编程语言的标识符绑定也就是作用域的问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;维基百科的解释：闭包在实现上是一个结构体，它存储了一个函数（通常是其入口地址）和一个关联的环境（相当于一个符号查找表）。环境里是若干对符号和值的对应关系，它既要包括约束变量（该函数内部绑定的符号），也要包括自由变量（在函数外部定义但在函数内被引用），有些函数也可能没有自由变量。闭包跟函数最大的不同在于，当捕捉闭包的时候，它的自由变量会在捕捉时被确定，这样即便脱离了捕捉时的上下文，它也能照常运行。捕捉时对于值的处理可以是值拷贝，也可以是名称引用，这通常由语言设计者决定，也可能由用户自行指定（如C++）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;继续分析上面的代码，我们假设 &lt;code&gt;outer&lt;/code&gt; 函数定义在全局中，并且全局环境中只有这段代码，我们分三个时间点来分析代码，第一个时间点是全局环境装载的时候，第二个时间点是 &lt;code&gt;outerFn&lt;/code&gt; 执行的时候，第三个时间点是 &lt;code&gt;innerFn&lt;/code&gt; 执行的时候。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/lexical-environment.CK7EPF8G_3DsRu.webp&quot; alt=&quot;lexical-environment-1&quot; title=&quot;lexical-environment-1&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们先来看全局环境装载，也就是我们代码开始执行的时候，此时的执行上下文栈只有一个全局执行上下文，全局代码的执行也会创建全局的 &lt;code&gt;Lexical Environment&lt;/code&gt;，&lt;code&gt;Global Lexical Environment&lt;/code&gt; 的 &lt;code&gt;OuterEnv&lt;/code&gt; 是 &lt;code&gt;null&lt;/code&gt;，&lt;code&gt;Environment Record&lt;/code&gt; 则记录了全局环境中的标识符绑定，在这段代码中有两个 &lt;code&gt;constant&lt;/code&gt;：&lt;code&gt;outerVar&lt;/code&gt; 和 &lt;code&gt;innerFn&lt;/code&gt;，还有一个 &lt;code&gt;FunctionDeclaration&lt;/code&gt;：&lt;code&gt;OuterFn&lt;/code&gt;。从上面我作的这个图可以看出全局的 &lt;code&gt;Lexcial Environment&lt;/code&gt; 和 &lt;code&gt;Environment Record&lt;/code&gt; 的关系。这里我有个推测就是当全局环境或者函数进行预编译的时候，检测到函数声明的时候会给函数加上一个 &lt;code&gt;[[Environment]]&lt;/code&gt; 字段，后面函数执行的时候就是用这个字段作为新生成的 &lt;code&gt;Lexical Environment&lt;/code&gt; 的 &lt;code&gt;OuterEnv&lt;/code&gt;。如果是函数表达式，这个操作应该是在执行到函数表达式赋值的时候进行的。有了这个 &lt;code&gt;[[Environment]]&lt;/code&gt; 属性之后，无论之后的函数在哪里执行，我们都知道其 &lt;code&gt;OunterEnv&lt;/code&gt; 是哪个，逻辑上没什么问题。&lt;code&gt;这里也只是个人的想法，引擎的具体实现不一定是这样，但整个思路应该是没有问题的&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/lexical-environment2.C94Rmm2S_Mu1VB.webp&quot; alt=&quot;lexical-environment-2&quot; title=&quot;lexical-environment-2&quot;&gt;&lt;/p&gt;
&lt;p&gt;下面我们进入第二个时间点，就是 &lt;code&gt;outerFn&lt;/code&gt; 执行的时候。如上图所示，&lt;code&gt;outerFn&lt;/code&gt; 的执行创建了一个新的 &lt;code&gt;Lexical Environment&lt;/code&gt;，这个新的 &lt;code&gt;Lexical Environment&lt;/code&gt; 的 &lt;code&gt;outerEnv&lt;/code&gt; 就会拿我们放在函数上的 &lt;code&gt;[[Environemnt]]&lt;/code&gt; 属性，也就是 &lt;code&gt;GlobalLexicalEnvironment&lt;/code&gt;。&lt;code&gt;outerFn Lexical Environmet&lt;/code&gt; 的 &lt;code&gt;Environment Record&lt;/code&gt; 中就放着函数中的标识符绑定，&lt;code&gt;uselessVar&lt;/code&gt;，&lt;code&gt;innerVar&lt;/code&gt; 和一个函数声明 &lt;code&gt;innerFn&lt;/code&gt;。和全局环境装在时一样，我们会在 &lt;code&gt;outerFn&lt;/code&gt; 预编译的时候给 &lt;code&gt;innerFn&lt;/code&gt; 上加一个 &lt;code&gt;[[Environment]]&lt;/code&gt; 属性指向 &lt;code&gt;outerFn Lexical Envrionment&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/lexical-environment3.BfFTIyeJ_ofBQO.webp&quot; alt=&quot;lexical-environment-3&quot; title=&quot;lexical-environment-3&quot;&gt;&lt;/p&gt;
&lt;p&gt;最后一个时间点是全局的 &lt;code&gt;innerFn&lt;/code&gt; 的执行，此时的 &lt;code&gt;outerFn Execution Context&lt;/code&gt; 已经执行完毕出栈销毁了，但是由于其内部定义的 &lt;code&gt;innerFn&lt;/code&gt; 仍然在全局环境中有引用，该 &lt;code&gt;innerFn&lt;/code&gt; 可以通过闭包访问到 &lt;code&gt;outerFn&lt;/code&gt; 中定义的变量，现在这些内部变量只有 &lt;code&gt;innerFn&lt;/code&gt; 能够访问到了。所以我们可以看到 &lt;code&gt;outerFn Lexical Environment&lt;/code&gt; 并不会销毁，当 &lt;code&gt;innerFn&lt;/code&gt; 执行过程中进行标识符检索的时候会现在当前的 &lt;code&gt;innerFn Lexical Environment&lt;/code&gt; 中的 &lt;code&gt;Environment Record&lt;/code&gt; 中进行检索，找不到会到 &lt;code&gt;innerFn Lexical Environment&lt;/code&gt; 中的 &lt;code&gt;OuterEnv&lt;/code&gt; 指向的 &lt;code&gt;outerFn Lexical Environment&lt;/code&gt; 中进行检索，如果还找不到就继续按相同的规则向上找，直到到达 &lt;code&gt;Global Lexical Environment&lt;/code&gt;，这也就是以前一个比较流行的概念&lt;strong&gt;作用域链&lt;/strong&gt;的本质。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里我又一个疑问就是 &lt;code&gt;outerFn Lexical Environment&lt;/code&gt; 中的之后不会用到的变量比如图中我用虚线标出来的 &lt;code&gt;uselessVar&lt;/code&gt; 是否会在执行上下文销毁的时候被销毁，毕竟这能节省不必要浪费的内存，也就是闭包中是否只保存会被再次用到的变量，我个人认为不会再次被使用的变量是会销毁的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;ES2021&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;ES 2021&lt;/code&gt; 中，标准有了一个较大的变动，&lt;code&gt;Environment Record&lt;/code&gt; 的概念被升级，而 &lt;code&gt;LexicalEnvironment&lt;/code&gt; 则放到了 &lt;code&gt;Execution Contexts&lt;/code&gt; 中和 &lt;code&gt;VariableEnvironment&lt;/code&gt; 同级，并且标准中说了 &lt;code&gt;The LexicalEnvironment and VariableEnvironment components of an execution context are always Environment Records.&lt;/code&gt;，我个人认为相当于把 &lt;code&gt;Lexical Environment&lt;/code&gt; 和 &lt;code&gt;Environment Record&lt;/code&gt; 两个概念合并了，相当于把我们上面图中的 &lt;code&gt;Lexical Environment&lt;/code&gt; 换成 &lt;code&gt;Environment Record&lt;/code&gt;，然后 &lt;code&gt;Environment Record&lt;/code&gt; 引用的内容直接放到当前的 &lt;code&gt;Environment Record&lt;/code&gt; 下，&lt;code&gt;OuterEnv&lt;/code&gt; 不变。其实他们的本质没什么变化，只是表述上有些不同，毕竟 &lt;code&gt;JavaScript&lt;/code&gt; 引擎已经比较稳定，像作用域这种底层核心设施应该不会有什么颠覆性的变化。&lt;/p&gt;
&lt;h2&gt;忍者秘籍&lt;/h2&gt;
&lt;p&gt;虽然忍者秘籍也是 &lt;code&gt;18&lt;/code&gt; 年的书了，可能有些人觉得有些过时，但我觉得 &lt;code&gt;JavaScript&lt;/code&gt; 的基础内容都还是值得一看的，特别是对有一定基础，想继续夯实的并且能够自己区分哪些内容是不需要看的同学，看一看没什么坏处。第五章的内容我个人觉得讲的还是挺不错的，一些例子（比如那个定时器的例子）和图都能很好的帮助理解。不过我强烈建议有英文阅读能力的去读英文，中文版我真不知道说啥，看到 &lt;code&gt;in a nutshell&lt;/code&gt; 被翻译成 &lt;code&gt;在果壳之下&lt;/code&gt; 我也是醉了。&lt;/p&gt;
&lt;p&gt;对于书籍的阅读我一直建议的是自己去看一看再说有没有用，这东西很主观，有些人说一些东西过时了，那可能是他掌握的很好，未必对你没用，更何况还有很多跟风的。具体到自己的情况，还是自己去读一下才能确定，如果读个几页发现确实没用，也浪费不了什么时间，而且很多书温故而知新，在不同的阶段再去读一读也能有不少收获。总之我还是建议大家多自己看，少听人说。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文就是根据标准对 &lt;code&gt;Lexical Environment&lt;/code&gt; 和 &lt;code&gt;Environment Record&lt;/code&gt; 以及闭包的一些解读，如有错漏，欢迎指正和交流。&lt;/p&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>m1 芯片的 node 版本问题</title><link>https://clloz.com/blog/node-version-apple-silicon</link><guid isPermaLink="true">https://clloz.com/blog/node-version-apple-silicon</guid><pubDate>Wed, 27 Oct 2021 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;昨天早上新的 &lt;code&gt;MBP&lt;/code&gt; 到了，花了一天的时间差不多把开发环境部署的差不多了，今天开始把公司的项目拉下来跑了跑，结果还是遇到了一些问题，总结一下。&lt;/p&gt;
&lt;h2&gt;node 版本&lt;/h2&gt;
&lt;p&gt;关于 &lt;code&gt;M1&lt;/code&gt; 芯片的支持情况可以参考这个网站 &lt;a href=&quot;https://doesitarm.com/app/nodejs/&quot; title=&quot;doesitarm&quot;&gt;doesitarm&lt;/a&gt;，从网站给出的 &lt;a href=&quot;https://github.com/nodejs/TSC/issues/886&quot; title=&quot;Github issue&quot;&gt;Github issue&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/ThatGuySam/doesitarm/issues/299#issuecomment-733210648&quot; title=&quot;version support&quot;&gt;version support&lt;/a&gt;，我们可以知道 &lt;code&gt;v15.3.0&lt;/code&gt; 之后都是原生支持 &lt;code&gt;M1&lt;/code&gt; 的，之前的版本则需要通过 &lt;code&gt;Rosetta 2&lt;/code&gt; 将 &lt;code&gt;x86&lt;/code&gt; 指令翻译成 &lt;code&gt;ARM&lt;/code&gt; 指令。&lt;/p&gt;
&lt;p&gt;我是用的 &lt;code&gt;NVM&lt;/code&gt; 来进行 &lt;code&gt;node&lt;/code&gt; 的版本管理，&lt;code&gt;NVM&lt;/code&gt; 的安装和之前并没有什么太大的差别。我第一个安装的版本是一个 &lt;code&gt;v15&lt;/code&gt; 的版本，装了非常长的时间，并且 &lt;code&gt;CPU&lt;/code&gt; 满负荷在运行，不过最后还是装好了。但是安装 &lt;code&gt;v16&lt;/code&gt; 之后的版本就和原来非 &lt;code&gt;M1&lt;/code&gt; 的安装差不多，非常迅速。根据 &lt;a href=&quot;https://www.infoworld.com/article/3615673/nodejs-16-introduces-apple-silicon-support.html&quot; title=&quot;Node.js 16 introduces Apple Silicon support&quot;&gt;Node.js 16 introduces Apple Silicon support&lt;/a&gt; 和上面的兼容性网站，我估计是 &lt;code&gt;v15&lt;/code&gt; 的版本还是兼容性不太好的，&lt;code&gt;v16&lt;/code&gt; 是第一个为 &lt;code&gt;M1&lt;/code&gt; 提供预构建的二进制的版本。&lt;/p&gt;
&lt;p&gt;我目前安装了三个版本： 1. 14.16.0：这是我之前一直使用的版本，原来的项目都是在这个版本下构建和运行的 2. 16.13.0：该版本是目前原生支持 &lt;code&gt;M1&lt;/code&gt; 的版本中唯一的 &lt;code&gt;LTS&lt;/code&gt; 版本 3. 17.0.1: 最新版本，我用 &lt;code&gt;brew&lt;/code&gt; 安装 &lt;code&gt;yarn&lt;/code&gt; 的时候 &lt;code&gt;brew&lt;/code&gt; 还安装了一个最新版的 &lt;code&gt;node&lt;/code&gt;，导致我的 &lt;code&gt;NVM&lt;/code&gt; 版本切换无效了，之后我卸载了 &lt;code&gt;brew&lt;/code&gt; 的，用 &lt;code&gt;NVM&lt;/code&gt; 重装了一个&lt;/p&gt;
&lt;p&gt;如果你原本使用的就是 &lt;code&gt;v16&lt;/code&gt; 版本，那么就建议无缝切换到 &lt;code&gt;v16.13.0&lt;/code&gt;。如果和我一样原来使用的是比较低的版本，就需要在 &lt;code&gt;Rosetta 2&lt;/code&gt; 的终端中安装低版本 &lt;code&gt;node&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;安装低版本&lt;/h2&gt;
&lt;p&gt;参考 &lt;a href=&quot;https://github.com/nvm-sh/nvm/issues/2350#issuecomment-734132550&quot; title=&quot;install node under v15&quot;&gt;install node under v15&lt;/a&gt;，简单来说就是在终端执行 &lt;code&gt;arch -x86_64 zsh&lt;/code&gt; 让当前终端运行在 &lt;code&gt;Rosetta 2&lt;/code&gt; 下，然后就可以用 &lt;code&gt;nvm&lt;/code&gt; 安装低版本 &lt;code&gt;node&lt;/code&gt; 了，之后我们可以在非 &lt;code&gt;Rosetta 2&lt;/code&gt; 的终端下任意切换和使用任意版本的 &lt;code&gt;nodejs&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你使用 &lt;code&gt;homebrew&lt;/code&gt; 安装了最新的 &lt;code&gt;zsh&lt;/code&gt;，那么这个 &lt;code&gt;zsh&lt;/code&gt; 的版本是 &lt;code&gt;zsh 5.8 (arm-apple-darwin21.1.0)&lt;/code&gt;，而 &lt;code&gt;mac&lt;/code&gt; 自带的 &lt;code&gt;zsh&lt;/code&gt; 版本是 &lt;code&gt;zsh 5.8 (x86_64-apple-darwin21.1.0)&lt;/code&gt;。这是如果我们执行 &lt;code&gt;arch x86_64 zsh&lt;/code&gt; 会报错 &lt;code&gt;arch: posix_spawnp: zsh: Bad CPU type in executable&lt;/code&gt;，因为我们的 &lt;code&gt;zsh&lt;/code&gt; 此时已经是 &lt;code&gt;arm&lt;/code&gt; 架构了，不能切换。如果是这种情况要安装老版本的 &lt;code&gt;nodejs&lt;/code&gt; 就需要在启动 &lt;code&gt;iTerm&lt;/code&gt; 或者 &lt;code&gt;terminal&lt;/code&gt; 之前在 &lt;code&gt;get info&lt;/code&gt;（⌘ + I）勾选上 &lt;code&gt;open using Rosetta&lt;/code&gt;，之后启动终端就能正常安装老版本的 &lt;code&gt;node&lt;/code&gt; 了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;高版本遇到的问题&lt;/h2&gt;
&lt;p&gt;我一开始不想安装低版本的需要 &lt;code&gt;Rosetta 2&lt;/code&gt; 的 &lt;code&gt;node&lt;/code&gt;， 但是在给几个项目安装依赖的时候发现出了很多问题，还是装了原来的 &lt;code&gt;14.16.0&lt;/code&gt; 的版本。&lt;/p&gt;
&lt;h2&gt;canvas 包安装不上&lt;/h2&gt;
&lt;p&gt;首先遇到的是 &lt;code&gt;canvas&lt;/code&gt; 包装不上，最后在 &lt;code&gt;Google&lt;/code&gt; 上找到的解决方案就是 &lt;code&gt;brew install pkg-config cairo pango libpng jpeg giflib librsvg&lt;/code&gt;， 参考 &lt;a href=&quot;https://jialidun.vip/2021/08/21/54.html&quot; title=&quot;解决m1的mac 无法安装 canvas 包&quot;&gt;解决m1的mac 无法安装 canvas 包&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;node-sass 问题&lt;/h2&gt;
&lt;p&gt;接下来就是 &lt;code&gt;node-sass&lt;/code&gt; 的问题，首先我们要明确自己的 &lt;code&gt;node&lt;/code&gt; 版本和所需的 &lt;code&gt;node-sass&lt;/code&gt; 的版本，参考 &lt;a href=&quot;https://github.com/sass/node-sass&quot; title=&quot;node-sass - Github&quot;&gt;node-sass - Github&lt;/a&gt;，这里我使用的是 &lt;code&gt;v16&lt;/code&gt;，所以我需要的 &lt;code&gt;node-sass&lt;/code&gt; 版本是 &lt;code&gt;6.0+&lt;/code&gt;，但是 &lt;code&gt;6.0+&lt;/code&gt; 的 &lt;code&gt;node-sass&lt;/code&gt; 又需要 &lt;code&gt;11.0+&lt;/code&gt; 的 &lt;code&gt;sass-loader&lt;/code&gt;，而 &lt;code&gt;vue 2.6&lt;/code&gt; 又不支持这么高版本的 &lt;code&gt;sass-loader&lt;/code&gt;，这变成一个无解的问题了，所以最后我只能选择切换成一个低版本。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于 &lt;code&gt;node-sass&lt;/code&gt; 的讨论可以参考 &lt;a href=&quot;https://github.com/sass/node-sass/issues/3103&quot; title=&quot;node-sass - issue&quot;&gt;node-sass - issue&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不过在我用 &lt;code&gt;v15&lt;/code&gt; 版本的时候似乎 &lt;code&gt;npm i&lt;/code&gt; 成功并且项目成功跑起来过，在我切换到 &lt;code&gt;v16&lt;/code&gt; 安装好依赖 &lt;code&gt;npm run serve&lt;/code&gt; 后一直提示 &lt;code&gt;Error: vue-loader requires @vue/compiler-sfc to be present in the dependency tree.&lt;/code&gt;，目前我还没搞清楚什么问题。安装 &lt;code&gt;@vue/compiler-sfc&lt;/code&gt; 能够把项目跑起来，不过 &lt;code&gt;package.json&lt;/code&gt; 和 &lt;code&gt;package-lock.json&lt;/code&gt; 都发生变化了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;目前遇到的问题就这些，后面会持续更新遇到的问题和解决方法。&lt;/p&gt;</content:encoded><h:img src="/_astro/nodejs-logo.D-3tcjWe.jpeg"/><enclosure url="/_astro/nodejs-logo.D-3tcjWe.jpeg"/></item><item><title>网站的免费 https 证书申请和更新</title><link>https://clloz.com/blog/free-ssl-certificate</link><guid isPermaLink="true">https://clloz.com/blog/free-ssl-certificate</guid><pubDate>Sun, 21 Mar 2021 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;网站的 &lt;code&gt;HTTPS&lt;/code&gt; 现在基本已经是必须的了，不过对于个人网站而言，一年几百块的 &lt;code&gt;SSL&lt;/code&gt; 证书显然是没有必要的，我们需要的是比较方便的免费证书。我之前一直是申请的阿里云的免费证书，不过阿里云的免费证书的有效期为一年，并且每次快过期了都要手动进行部署，修改 &lt;code&gt;nginx&lt;/code&gt; 配置，比较麻烦。之前在 &lt;code&gt;V2Ray&lt;/code&gt; 的配置中用了 &lt;code&gt;certbot&lt;/code&gt; 来免费申请 &lt;code&gt;Let&apos;s Encrypt&lt;/code&gt; 的证书，虽然有效期只要三个月，但是可以配置自动续签，所以还是比较方便的。本文就介绍以下配置过程。&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Let&apos;s Encrypt&lt;/code&gt; 是一家免费，开放，自动化的证书颁发机构，官方文档参考 &lt;a href=&quot;https://letsencrypt.org/zh-cn/getting-started/&quot; title=&quot;Let&amp;#x27;s Encrypt 快速入门&quot;&gt;Let&apos;s Encrypt 快速入门&lt;/a&gt;。&lt;code&gt;Let&apos;s Encrypt&lt;/code&gt; 官方建议使用 &lt;code&gt;certbot&lt;/code&gt; 来进行证书的获取。安装 &lt;code&gt;certbot&lt;/code&gt; 需要先安装 &lt;code&gt;epel&lt;/code&gt; 仓库，命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo yum install epel-release

sudo yum install certbot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后我们就可以运行 &lt;code&gt;certbot&lt;/code&gt; 命令来获取证书了，&lt;code&gt;certbot&lt;/code&gt; 命令的详细参数说明可以参考 &lt;a href=&quot;https://blog.ibaoger.com/2017/03/07/certbot-command-line-tool-usage-document/&quot; title=&quot;Certbot命令行工具使用说明&quot;&gt;Certbot命令行工具使用说明&lt;/a&gt;。我们这里为我们的网站进行证书的申请主要是下面的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;certbot certonly --webroot -m youremail@email.com -w your-root-path -d your-domain.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;certonly&lt;/code&gt; 表示获取证书，但是不安装，我们手动进行配置。 &lt;code&gt;--webroot&lt;/code&gt; 表示把身份认证文件放置在服务器的网页根目录下，这样可以让我们在更新证书的时候不用关闭 &lt;code&gt;nginx&lt;/code&gt;。 &lt;code&gt;-m&lt;/code&gt; 输入自己的邮箱用来接收 &lt;code&gt;Let&apos;s Encrypt&lt;/code&gt; 的邮件，比如证书要过期了等。 &lt;code&gt;-d&lt;/code&gt; 输入自己要申请帧数的域名，如果不输入会在命令执行后要你手动填，也可以选择跳过 &lt;code&gt;-w&lt;/code&gt; 网站的根目录&lt;/p&gt;
&lt;p&gt;生成好的证书文件路径如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在我们的 &lt;code&gt;nginx&lt;/code&gt; 配置文件中配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssl_certificate       /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key   /etc/letsencrypt/live/example.com/privkey.pem;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重启 &lt;code&gt;nginx&lt;/code&gt; 就完成配置了。&lt;/p&gt;
&lt;p&gt;你可以使用 &lt;code&gt;certbot certificates&lt;/code&gt; 来查看你已经申请的证书信息，你也可以多次运行上面的命令为多个域名创建证书，如果要更新单个证书就重新运行创建证书的命令，如果要一次更新所有域名的证书可以使用 &lt;code&gt;certbot renew&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;自动更新证书&lt;/h2&gt;
&lt;p&gt;证书的自动更新需要安装 &lt;code&gt;crontabs&lt;/code&gt;，安装命令如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yum -y install vixie-cron

yum -y install crontabs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后 &lt;code&gt;service crond start&lt;/code&gt; 启动 &lt;code&gt;crond&lt;/code&gt; 服务，然后用 &lt;code&gt;crontab -e&lt;/code&gt; 来创建自动更新的命令 &lt;code&gt;30 0 15 * * /usr/bin/certbot renew --post-hook &quot;systemctl restart nginx&quot; &gt;&gt; /var/log/le-renew.log&lt;/code&gt;，该命令表示每个月 &lt;code&gt;15&lt;/code&gt; 号的 &lt;code&gt;0:30&lt;/code&gt; 执行 &lt;code&gt;certbot renew&lt;/code&gt; 进行证书的更新，你也可以根据自己的需求写 &lt;code&gt;cron&lt;/code&gt; 表达式，&lt;code&gt;--pre-hook&lt;/code&gt; 和 &lt;code&gt;--post-hook&lt;/code&gt; 则是证书更新前后的两个命令钩子，这里我们不需要像 &lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/11/24/v2ray-install-configuration/#_SSL&quot; title=&quot;配置 v2ray 证书&quot;&gt;配置 v2ray 证书&lt;/a&gt; 里面那样先停止自己的 &lt;code&gt;nginx&lt;/code&gt;，因为那里我们没有一个真的 &lt;code&gt;web&lt;/code&gt; 服务供 &lt;code&gt;Let&apos;s Encrypt&lt;/code&gt; 进行检测，而是用 &lt;code&gt;certbot&lt;/code&gt; &lt;code&gt;--standalone&lt;/code&gt; 启动了一个独立的 &lt;code&gt;80&lt;/code&gt; 端口服务来进行验证，这里我们有自己的 &lt;code&gt;nginx&lt;/code&gt; 可以提供验证，所以不需要用 &lt;code&gt;--pre-hook&lt;/code&gt; 先关闭 &lt;code&gt;nginx&lt;/code&gt;。最好自己先执行以下 &lt;code&gt;certbot renew --dry-run&lt;/code&gt; 来测试一下命令是否能正常运行。&lt;/p&gt;
&lt;p&gt;还有一点需要注意的是，证书的默认有效期是 &lt;code&gt;90&lt;/code&gt; 天，默认情况下只有有效期小于 &lt;code&gt;30&lt;/code&gt; 天的时候才可以进行证书的更新，否则更新会失败。&lt;/p&gt;
&lt;h2&gt;遇到的问题&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;2021-12-15&lt;/code&gt; 收到 &lt;code&gt;Let&apos;s Encrypt&lt;/code&gt; 的邮件说是证书还有一天就过期了，上服务器上执行 &lt;code&gt;certbot renew&lt;/code&gt; 发现报错 &lt;code&gt;Failed to renew certificate with error: __str__ returned non-string&lt;/code&gt;，在 &lt;code&gt;Let&apos;s Encrypt&lt;/code&gt; &lt;a href=&quot;https://community.letsencrypt.org/t/connection-error-on-acme-v02-api-letsencrypt-org/162393&quot; title=&quot;社区&quot;&gt;社区&lt;/a&gt; 里找到类似的问题用 &lt;code&gt;pip&lt;/code&gt; 安装 &lt;code&gt;urllib3&lt;/code&gt; 和 &lt;code&gt;requests&lt;/code&gt; 后由报了一个新的错误 &lt;code&gt;Failed to renew certificate with error: (&quot;bad handshake: Error([(&apos;SSL routines&apos;, &apos;ssl3_get_server_certificate&apos;, &apos;certificate verify failed&apos;)],)&quot;,)&lt;/code&gt;，没找到答案，最后只能按 &lt;a href=&quot;https://community.letsencrypt.org/t/sslerror-ssl-certificate-verify-failed-certificate-verify-failed/162777/10&quot; title=&quot;社区&quot;&gt;社区&lt;/a&gt; 里说的加上 &lt;code&gt;--no-verify-ssl&lt;/code&gt; 参数，更新成功了。看情况应该是 &lt;code&gt;Let&apos;s Encrypt&lt;/code&gt; 的什么证书过期了，暂时就先这么解决了。&lt;/p&gt;
&lt;p&gt;尝试了如下这些方法，不过都没有效果： 1. &lt;a href=&quot;https://www.openssl.org/blog/blog/2021/09/13/LetsEncryptRootCertExpire/&quot; title=&quot;Old Let’s Encrypt Root Certificate Expiration and OpenSSL 1.0.2&quot;&gt;Old Let’s Encrypt Root Certificate Expiration and OpenSSL 1.0.2&lt;/a&gt; 2. &lt;a href=&quot;https://serverfault.com/questions/791205/centos7-dont-trust-certificate-issued-by-lets-encrypt&quot; title=&quot;Centos7 don&amp;#x27;t trust certificate issued by lets encrypt&quot;&gt;Centos7 don&apos;t trust certificate issued by lets encrypt&lt;/a&gt; 3. &lt;a href=&quot;https://community.letsencrypt.org/t/certbot-0-31-0-renew-failed-peers-certificate-issuer-not-recognized/161771&quot; title=&quot;Certbot 0.31.0 renew failed, peer’s certificate issuer not recognized&quot;&gt;Certbot 0.31.0 renew failed, peer’s certificate issuer not recognized&lt;/a&gt; 4. &lt;a href=&quot;https://letsencrypt.org/certificates/&quot; title=&quot;Chain of Trust&quot;&gt;Chain of Trust&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是配置 &lt;code&gt;Let&apos;s Encrypt&lt;/code&gt; 证书并设置自动更新的方法了，如果有问题，欢迎留言讨论。&lt;/p&gt;</content:encoded><h:img src="/_astro/nginx.WNyebmBm.png"/><enclosure url="/_astro/nginx.WNyebmBm.png"/></item><item><title>对 Promise 的一些新的思考</title><link>https://clloz.com/blog/new-thoughts-on-promise</link><guid isPermaLink="true">https://clloz.com/blog/new-thoughts-on-promise</guid><pubDate>Thu, 18 Mar 2021 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我之前在阅读 &lt;code&gt;《ECMAScript 6 入门》&lt;/code&gt; 的时候曾经写过一篇 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/28/deep-into-promise/&quot; title=&quot;深入 Promise&quot;&gt;深入 Promise&lt;/a&gt;，从 &lt;code&gt;Promise&lt;/code&gt; 实现的角度对 &lt;code&gt;Promise&lt;/code&gt; 的机制进行了一点探究，我自己认为理解的还可以，不过昨天同事给了我一道 &lt;code&gt;Promise&lt;/code&gt; 的执行顺序题，让我久违的复习了一下 &lt;code&gt;Promise&lt;/code&gt;，发现之前的理解还是存在一些问题。&lt;/p&gt;
&lt;h2&gt;题目分析&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Promise.resolve()
  .then(() =&gt; {
    console.log(&apos;0&apos;)
    return Promise.resolve(&apos;4&apos;)
  })
  .then((res) =&gt; {
    console.log(res)
  })
Promise.resolve()
  .then(() =&gt; {
    console.log(&apos;1&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;2&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;3&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;5&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;6&apos;)
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是这样一个 &lt;code&gt;Promise&lt;/code&gt; 的执行顺序题，我当时的第一反应是 &lt;code&gt;0 1 4 2 3 5 6&lt;/code&gt;，我在 &lt;code&gt;code runner&lt;/code&gt; 里面跑了一下也没问题，但是后来到浏览器控制台以及 &lt;code&gt;node&lt;/code&gt; 中跑了一下代码，发现输出都是 &lt;code&gt;0 1 2 3 4 5 6&lt;/code&gt;，就让我非常迷茫，后面我又把我那篇 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/28/deep-into-promise/&quot; title=&quot;深入 Promise&quot;&gt;深入 Promise&lt;/a&gt; 看了一遍，发现了一些问题。&lt;/p&gt;
&lt;p&gt;首先是对 &lt;code&gt;then&lt;/code&gt; 的理解，我在之前的文章中也写了 &lt;code&gt;new Promise&lt;/code&gt; 和 &lt;code&gt;then&lt;/code&gt; 本身都是同步执行的，但我还是认为 &lt;code&gt;Promise&lt;/code&gt; 变为 &lt;code&gt;settled&lt;/code&gt; 状态以后才会执行 &lt;code&gt;then&lt;/code&gt;，但这显然是不太可能，也不符合 &lt;code&gt;JS&lt;/code&gt; 引擎的执行规律。在 &lt;code&gt;Promise&lt;/code&gt; 的实现中 &lt;code&gt;then&lt;/code&gt; 本身就是 &lt;code&gt;Promise&lt;/code&gt; 类的一个方法，其执行会返回一个 &lt;code&gt;Promise&lt;/code&gt; 对象，既然是方法并且是同步代码，&lt;code&gt;then&lt;/code&gt; 肯定会同步执行。只不过如果上级 &lt;code&gt;Promise&lt;/code&gt; 如果是 &lt;code&gt;pending&lt;/code&gt; 状态，&lt;code&gt;then&lt;/code&gt; 的参数方法会被缓存到上级 &lt;code&gt;Promise&lt;/code&gt; 的回调函数队列中而已，上级 &lt;code&gt;Promise&lt;/code&gt; 已经 &lt;code&gt;resolved&lt;/code&gt; 进入 &lt;code&gt;fulfilled&lt;/code&gt; 状态，则 &lt;code&gt;then&lt;/code&gt; 的参数方法直接进入微任务队列。&lt;/p&gt;
&lt;p&gt;但是 &lt;code&gt;then&lt;/code&gt; 还有一个细节就是如果我们在 &lt;code&gt;then&lt;/code&gt; 中返回的是一个 &lt;code&gt;Promise&lt;/code&gt;，会调用这个 &lt;code&gt;Promise&lt;/code&gt; 的 &lt;code&gt;then&lt;/code&gt; 方法，并且在这个 &lt;code&gt;then&lt;/code&gt; 当中进行父级 &lt;code&gt;Promise&lt;/code&gt; 的 &lt;code&gt;resolve&lt;/code&gt; 或者 &lt;code&gt;reject&lt;/code&gt;，细节参考 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/28/deep-into-promise/&quot; title=&quot;深入 Promise&quot;&gt;深入 Promise&lt;/a&gt; 的源码实现中的 &lt;code&gt;resolvePromise&lt;/code&gt;方法。其实解释这个也很简单，当我们返回的是一个 &lt;code&gt;Promise&lt;/code&gt; 的时候，我们需要在这个 &lt;code&gt;Promise&lt;/code&gt; 的状态 &lt;code&gt;settled&lt;/code&gt; 之后才能对父级的这个 &lt;code&gt;then&lt;/code&gt; 进行 &lt;code&gt;resolve&lt;/code&gt;，所以当 &lt;code&gt;then&lt;/code&gt; 中返回的是一个 &lt;code&gt;Promise&lt;/code&gt; 的时候会存在一次隐式的 &lt;code&gt;then&lt;/code&gt; 调用。我们来看下面四段代码的输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 第一段代码
Promise.resolve()
  .then(() =&gt; {
    console.log(&apos;0&apos;)
    return new Promise((resolve) =&gt; {
      return resolve(10)
    })
  })
  .then((res) =&gt; {
    console.log(res)
  })
Promise.resolve()
  .then(() =&gt; {
    console.log(&apos;1&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;2&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;3&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;5&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;6&apos;)
  })

// 0
// 1
// 2
// 3
// 10
// 5
// 6
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 第二段代码
Promise.resolve()
  .then(() =&gt; {
    console.log(&apos;0&apos;)
    return new Promise((resolve) =&gt; {
      return resolve(4)
    }).then(() =&gt; 11)
  })
  .then((res) =&gt; {
    console.log(res)
  })
Promise.resolve()
  .then(() =&gt; {
    console.log(&apos;1&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;2&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;3&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;5&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;6&apos;)
  })

// 0
// 1
// 2
// 3
// 11
// 5
// 6
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 第三段代码
Promise.resolve()
  .then(() =&gt; {
    console.log(&apos;0&apos;)
    return new Promise((resolve) =&gt; {
      return resolve(4)
    })
      .then(() =&gt; 11)
      .then(() =&gt; 12)
  })
  .then((res) =&gt; {
    console.log(res)
  })
Promise.resolve()
  .then(() =&gt; {
    console.log(&apos;1&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;2&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;3&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;5&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;6&apos;)
  })

// 0
// 1
// 2
// 3
// 5
// 12
// 6
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 第四段代码
Promise.resolve()
  .then(() =&gt; {
    console.log(&apos;0&apos;)
    return new Promise((resolve) =&gt; {
      return resolve(4)
    })
      .then(() =&gt; 11)
      .then(() =&gt; 12)
      .then(() =&gt; 13)
  })
  .then((res) =&gt; {
    console.log(res)
  })
Promise.resolve()
  .then(() =&gt; {
    console.log(&apos;1&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;2&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;3&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;5&apos;)
  })
  .then(() =&gt; {
    console.log(&apos;6&apos;)
  })

// 0
// 1
// 2
// 3
// 5
// 6
// 13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一段代码中我们返回了一个直接 &lt;code&gt;resolve&lt;/code&gt; 的 &lt;code&gt;Promise&lt;/code&gt;，我们发现和 &lt;code&gt;Promise.reslove()&lt;/code&gt; 效果是一样的。我们可以看到返回的这个 &lt;code&gt;Promise&lt;/code&gt; 它只是 &lt;code&gt;resolve&lt;/code&gt; 了它自己，而父级的 &lt;code&gt;then&lt;/code&gt; 状态改变必然要在这个返回的 &lt;code&gt;Promise&lt;/code&gt; 状态确定之后才能确定是 &lt;code&gt;resolve&lt;/code&gt; 还是 &lt;code&gt;reject&lt;/code&gt;，所以在 &lt;code&gt;Promise&lt;/code&gt; 实现的源码中我们可以发现，如果 &lt;code&gt;then&lt;/code&gt; 的返回是一个 &lt;code&gt;Promise&lt;/code&gt;，会在这个 &lt;code&gt;Promise&lt;/code&gt; 的 &lt;code&gt;then&lt;/code&gt; 中进行 &lt;code&gt;resolve&lt;/code&gt; 和 &lt;code&gt;reject&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;第二段代码中我们对返回的 &lt;code&gt;Promise&lt;/code&gt; 进行了 &lt;code&gt;then&lt;/code&gt;，结果和第一段代码的执行结果是相同的。这段代码的过程比较好分析（以打印的值来标志代码段，比如 &lt;code&gt;console.log(0)&lt;/code&gt; 这段代码我们就称为 &lt;code&gt;0&lt;/code&gt;）： 1. 执行所有同步代码，两个 &lt;code&gt;Promise.resolve()&lt;/code&gt; 直接 &lt;code&gt;resolved&lt;/code&gt;，所以 &lt;code&gt;0&lt;/code&gt; 和 &lt;code&gt;1&lt;/code&gt; 都被加入微任务队列。 2. 同步代码执行完成，开始执行微任务，先执行 &lt;code&gt;0&lt;/code&gt;，打印 &lt;code&gt;0&lt;/code&gt;，然后执行 &lt;code&gt;new Promise&lt;/code&gt; 并直接 &lt;code&gt;resolve&lt;/code&gt;，将这个新的 &lt;code&gt;Promise&lt;/code&gt; 的 &lt;code&gt;then&lt;/code&gt; 中的 &lt;code&gt;11&lt;/code&gt; 加入微任务队列。 3. 执行下一个 &lt;code&gt;1&lt;/code&gt; 微任务，打印 &lt;code&gt;1&lt;/code&gt;，并把 &lt;code&gt;2&lt;/code&gt; 加入微任务队列。 4. 执行 &lt;code&gt;11&lt;/code&gt; 微任务，并隐式调用返回的 &lt;code&gt;Promise&lt;/code&gt; 的 &lt;code&gt;then&lt;/code&gt;，隐式调用的 &lt;code&gt;then&lt;/code&gt; 的回调函数加入微任务队列 5. 执行 &lt;code&gt;2&lt;/code&gt;，打印 &lt;code&gt;2&lt;/code&gt;，将 &lt;code&gt;3&lt;/code&gt; 加入微任务队列， 6. 执行隐式调用的 &lt;code&gt;then&lt;/code&gt; 微任务，在这个 &lt;code&gt;then&lt;/code&gt; 中 &lt;code&gt;resolve&lt;/code&gt; 父级的 &lt;code&gt;then&lt;/code&gt;，同时将 &lt;code&gt;console.log(res)&lt;/code&gt; 这段代码加入微任务队列 7. 执行 &lt;code&gt;3&lt;/code&gt;，打印 &lt;code&gt;3&lt;/code&gt;，将 &lt;code&gt;5&lt;/code&gt; 加入微任务队列 8. 执行 &lt;code&gt;console.log(res)&lt;/code&gt;，打印 &lt;code&gt;11&lt;/code&gt; 9. 执行 &lt;code&gt;5&lt;/code&gt;，打印 &lt;code&gt;5&lt;/code&gt;，将 &lt;code&gt;6&lt;/code&gt; 加入微任务队列 10. 执行 &lt;code&gt;6&lt;/code&gt;，打印 &lt;code&gt;6&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个过程逻辑没有什么问题，但是第一段代码比第二段少一个 &lt;code&gt;then&lt;/code&gt; 执行结果依然是一样的，感觉就像是隐式调用了两次 &lt;code&gt;then&lt;/code&gt;，这里我没有找到具体的原因，去翻了翻 &lt;code&gt;v8&lt;/code&gt; 的 &lt;code&gt;Promise&lt;/code&gt; 源码也没有找到合适的解释（源码参考 &lt;a href=&quot;https://segmentfault.com/a/1190000019258738&quot; title=&quot;从Google V8引擎剖析Promise实现&quot;&gt;从Google V8引擎剖析Promise实现&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;后面的两段代码用上面的理论也比较好解释，每多一个 &lt;code&gt;then&lt;/code&gt; 就推迟一次打印结果。有了这几个例子，上面的 &lt;code&gt;Promise.resolve&lt;/code&gt; 那一个问题也是一样的思考逻辑。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文对 &lt;code&gt;Promise&lt;/code&gt; 特别是 &lt;code&gt;then&lt;/code&gt; 的执行细节进行了更深入的探究，但还是有一个问题没有解决，如果你知道答案欢迎回复评论。这里在分享几个跟 &lt;code&gt;Promise&lt;/code&gt; 相关的题目 &lt;a href=&quot;https://segmentfault.com/a/1190000016848192&quot; title=&quot;关于 ES6 中 Promise 的面试题&quot;&gt;关于 ES6 中 Promise 的面试题&lt;/a&gt;，第五题和第七题还是有点意思的。&lt;/p&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>JS 强制转 Number 的方法研究</title><link>https://clloz.com/blog/js-convert-number-ways</link><guid isPermaLink="true">https://clloz.com/blog/js-convert-number-ways</guid><pubDate>Wed, 17 Mar 2021 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/13/type-conversion/&quot; title=&quot;深入Javascript类型转换&quot;&gt;深入Javascript类型转换&lt;/a&gt; 一文中从标准的角度对 &lt;code&gt;JavaScript&lt;/code&gt; 中的类型转换的部分内容进行解读，主要是 &lt;code&gt;Number&lt;/code&gt;，&lt;code&gt;String&lt;/code&gt; 和 &lt;code&gt;Boolean&lt;/code&gt; 这三个日常经常遇到的类型。昨天在阅读 &lt;code&gt;lodash&lt;/code&gt; 的 &lt;code&gt;slice&lt;/code&gt; 源码看到用无符号右移 &lt;code&gt;start &gt;&gt;&gt; 0&lt;/code&gt; 来确保 &lt;code&gt;start&lt;/code&gt; 是一个 &lt;code&gt;Number&lt;/code&gt; 类型的正整数。在 &lt;code&gt;JavaScript设计模式与开发实践&lt;/code&gt; 中也看到了 &lt;code&gt;+new Date&lt;/code&gt; 这样转换日期毫秒数的。我日常转换日期毫秒数都是用的 &lt;code&gt;Date.prototype.getTime()&lt;/code&gt; 方法，强制转换数字我经常使用 &lt;code&gt;Number()&lt;/code&gt; 构造器。在经过一番搜索之后发现很多文章说 &lt;code&gt;Number()&lt;/code&gt; 是效率最低的一种方式，于是我决定自己对强制转换为 &lt;code&gt;Number&lt;/code&gt; 类型的多种方法进行一下探究。&lt;/p&gt;
&lt;h2&gt;转换方法&lt;/h2&gt;
&lt;h2&gt;一元操作符&lt;/h2&gt;
&lt;p&gt;一元操作符有 &lt;code&gt;++, --, +, -, ~, !&lt;/code&gt;，我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/13/type-conversion/&quot; title=&quot;深入Javascript类型转换&quot;&gt;深入Javascript类型转换&lt;/a&gt; 中已经写过，除了 &lt;code&gt;!&lt;/code&gt;，其他一元操作符都是会将类型转换为 &lt;code&gt;Number&lt;/code&gt; 类型，&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/conflicting/Web/JavaScript/Reference/Operators#Unary_plus&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt; 中也推荐用一元 &lt;code&gt;+&lt;/code&gt; 来讲其他类型转为 &lt;code&gt;Number&lt;/code&gt;：一元正号是转换其他对象到数值的最快方法，也是最推荐的做法，因为它不会对数值执行任何多余操作。它可以将字符串转换成整数和浮点数形式，也可以转换非字符串值 &lt;code&gt;true&lt;/code&gt;，&lt;code&gt;false&lt;/code&gt; 和 &lt;code&gt;null&lt;/code&gt;。小数和十六进制格式字符串也可以转换成数值。负数形式字符串也可以转换成数值（对于十六进制不适用）。如果它不能解析一个值，则计算结果为 &lt;code&gt;NaN&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;位运算&lt;/h2&gt;
&lt;p&gt;对于 &lt;code&gt;JavaScript&lt;/code&gt; 中的位运算不熟悉的同学可以先去看一看 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/04/bitwise-operator/&quot; title=&quot;JavaScript 中的按位操作符&quot;&gt;JavaScript 中的按位操作符&lt;/a&gt;。&lt;code&gt;JavaScript&lt;/code&gt; 中的按位操作符会将操作数转换成 &lt;code&gt;32&lt;/code&gt; 位的二进制整数，所以也可以用来进行 &lt;code&gt;Number&lt;/code&gt; 转换。但是需要注意的是 &lt;code&gt;32&lt;/code&gt; 位有符号二进制整数所能表示的最大值为 $2^32 - 1$，即 &lt;code&gt;4294967295&lt;/code&gt;，如果数字超过这个范围则会出错，超出 &lt;code&gt;32&lt;/code&gt; 位的部分会直接被丢弃。所以我们使用连续的按位非 &lt;code&gt;~~&lt;/code&gt;，或者无符号右移 &lt;code&gt;&gt;&gt;&gt;&lt;/code&gt;，以及 &lt;code&gt;|0&lt;/code&gt; 都要注意操作数的范围。比如上面说的用 &lt;code&gt;+new Date&lt;/code&gt; 可以获得毫秒数，如果使用 &lt;code&gt;&gt;&gt;&gt;&lt;/code&gt; 将会得到错误结果。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const time = +new Date()
console.log(+new Date()) // 1615946620311
const time_binary = time.toString(2)
console.log(time_binary, time_binary.length) // 10111100000111101111011001100010001011011 41
const time_bit = time &gt;&gt;&gt; 0
console.log(time_bit) // 1038988502
console.log(parseInt(`${time_binary}`.slice(8), 2)) // 1038988502
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到上面的代码 &lt;code&gt;time&lt;/code&gt; 为当前时间毫秒数，是用 &lt;code&gt;+&lt;/code&gt; 进行转换的，&lt;code&gt;time_binary&lt;/code&gt; 是用 &lt;code&gt;Number.prototype.toString&lt;/code&gt; 转换的二进制字符串，&lt;code&gt;time_bit&lt;/code&gt; 则是用无符号右移获得的 &lt;code&gt;Number&lt;/code&gt;。我们可以看到无符号右移只能保留低位的 &lt;code&gt;32&lt;/code&gt; 位二进制，超出的高位都被丢弃。&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;JavaScript&lt;/code&gt; 中的进制的一些细节参考我的另一篇文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/06/11/javascript-number/#JS&quot; title=&quot;JS 中的数字进制&quot;&gt;JS 中的数字进制&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;parseInt&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;parseInt&lt;/code&gt; 是一个内置的全局函数，也是我们使用频率比较高的一个方法，但是 &lt;code&gt;parseInt&lt;/code&gt; 的机制还是比较复杂的，这里我来详细总结一下。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;parseInt&lt;/code&gt; 的语法是 &lt;code&gt;parseInt(string, radix)&lt;/code&gt;，接受两个参数，第一个参数是一个字符串，如果不是字符串会强制转换为 &lt;code&gt;String&lt;/code&gt;，字符串开头的空白符会被忽略。第二个参数为可选参数，表示字符串的基数，范围为 &lt;code&gt;2-36&lt;/code&gt;。虽然看起来很简单，但是由于 &lt;code&gt;JavaScript&lt;/code&gt; 中的类型转换规则比较复杂，所以第一个参数的 &lt;code&gt;toString&lt;/code&gt; 会产生很多奇怪的结果。比如下面：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(parseInt(&apos;012&apos;)) // 12
console.log(parseInt(012)) // 10
console.log(parseInt(&apos;012&apos;, 8)) // 10
console.log(parseInt(012, 8)) // 8

console.log(parseInt(&apos;0o12&apos;)) // 0
console.log(parseInt(0o12)) // 10
console.log(parseInt(&apos;0o12&apos;, 8)) // 0
console.log(parseInt(0o12, 8)) // 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我选了八进制作为示例，注意八进制的两种表示方式 &lt;code&gt;0&lt;/code&gt; 和 &lt;code&gt;0o&lt;/code&gt;，前者是不推荐使用了，&lt;code&gt;ES5&lt;/code&gt; 本身也是不支持八进制的，我们使用的 &lt;code&gt;0&lt;/code&gt; 开头的八进制是浏览器厂商自己支持的，&lt;code&gt;ES6&lt;/code&gt; 中则使用 &lt;code&gt;0o&lt;/code&gt; 对八进制进行了支持。&lt;code&gt;ECMAScript 5&lt;/code&gt; 规范也不允许 &lt;code&gt;parseInt&lt;/code&gt; 函数的实现环境把以 &lt;code&gt;0&lt;/code&gt; 字符开始的字符串作为八进制数值。所以我们在编码中尽量用复合标准的 &lt;code&gt;0o&lt;/code&gt;，虽然 &lt;code&gt;0&lt;/code&gt; 也能得到大部分浏览器的支持。&lt;/p&gt;
&lt;p&gt;这里我们在说一下 &lt;code&gt;radix&lt;/code&gt; 的规定，&lt;strong&gt;注意，radix 的默认值不是 10&lt;/strong&gt;，如果 &lt;code&gt;radix&lt;/code&gt; 是 &lt;code&gt;undefined&lt;/code&gt;、&lt;code&gt;0&lt;/code&gt; 或未指定的，&lt;code&gt;JavaScript&lt;/code&gt; 会假定以下情况：&lt;/p&gt;
&lt;p&gt;如果输入的 &lt;code&gt;string&lt;/code&gt; 以 &lt;code&gt;0x&lt;/code&gt; 或 &lt;code&gt;0x&lt;/code&gt; 开头，那么 &lt;code&gt;radix&lt;/code&gt; 被假定为 &lt;code&gt;16&lt;/code&gt;，字符串的其余部分被当做十六进制数去解析。 如果输入的 &lt;code&gt;string&lt;/code&gt; 以任何其他值开头， &lt;code&gt;radix&lt;/code&gt; 是 &lt;code&gt;10&lt;/code&gt;。 如果 &lt;code&gt;radix&lt;/code&gt; 的范围超出 &lt;code&gt;2-36&lt;/code&gt;，那么 &lt;code&gt;parseInt&lt;/code&gt; 表达式直接返回 &lt;code&gt;NaN&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果输入的 &lt;code&gt;string&lt;/code&gt; 以 &lt;code&gt;0&lt;/code&gt; 开头， &lt;code&gt;radix&lt;/code&gt; 被假定为八进制或十进制。具体选择哪一个 &lt;code&gt;radix&lt;/code&gt; 取决于实现。&lt;code&gt;ECMAScript 5&lt;/code&gt; 规定了应该使用十进制，但不是所有的浏览器都支持。因此，在使用 &lt;code&gt;parseInt&lt;/code&gt; 时，一定要指定一个 &lt;code&gt;radix&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;讲完了规则，我们一条一条来分析： &lt;code&gt;parseInt(&apos;012&apos;)&lt;/code&gt; 的结果是 &lt;code&gt;12&lt;/code&gt;，这是因为 &lt;code&gt;chrome&lt;/code&gt; 对于 &lt;code&gt;0&lt;/code&gt; 开头的字符串的 &lt;code&gt;parseInt&lt;/code&gt; 实现遵循了 &lt;code&gt;ES5&lt;/code&gt; 标准，即没有传入 &lt;code&gt;radix&lt;/code&gt; 的时候，&lt;code&gt;0&lt;/code&gt; 开头的字符串会被作为一个十进制数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;parseInt(012)&lt;/code&gt;，这里我们直接传入了一个八进制数 &lt;code&gt;012&lt;/code&gt;，即十进制的 &lt;code&gt;10&lt;/code&gt;，我们看到结果成功返回了 &lt;code&gt;10&lt;/code&gt;。我们分析一下过程，我们传入的是一个 &lt;code&gt;Number&lt;/code&gt;，所以会先调用 &lt;code&gt;012 .toString()&lt;/code&gt;(注意数字和小数点之间有空格，这是 &lt;code&gt;JS&lt;/code&gt; 的一个 &lt;code&gt;bug&lt;/code&gt;，否则 &lt;code&gt;.&lt;/code&gt; 会被解释为一个小数点，而不是成员访问)，&lt;code&gt;012&lt;/code&gt; 被 &lt;code&gt;toString&lt;/code&gt; 转为十进制的字符串 &lt;code&gt;&apos;10&apos;&lt;/code&gt; (&lt;code&gt;toString&lt;/code&gt; d的默认参数是 &lt;code&gt;10&lt;/code&gt;)，所以我们的该条表达式最后实际被转化为 &lt;code&gt;parseInt(&apos;10&apos;)&lt;/code&gt;，自然能够得到 &lt;code&gt;10&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;parseInt(&apos;012&apos;, 8)&lt;/code&gt; 是一个推荐的标准写法，即我们传入一个个指定进制的字符串，同时传入该进制的基数，能够得到一个确定的正确结果，日常编码中我们应该采用这种写法，以免出现意想不到的结果。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;parseInt(012, 8)&lt;/code&gt; 得到了一个让我们以外的结果 &lt;code&gt;8&lt;/code&gt;，但其实结合 &lt;code&gt;parseInt(012)&lt;/code&gt; 的分析过程我们可以发现这个语句被转换成了 &lt;code&gt;parseInt(&apos;10&apos;, 8)&lt;/code&gt;，这样得到 &lt;code&gt;8&lt;/code&gt; 就不足为奇了。&lt;/p&gt;
&lt;p&gt;后面四条语句我是使用的 &lt;code&gt;ES6&lt;/code&gt; 标准的 &lt;code&gt;0o&lt;/code&gt; 表示的八进制数，我们主要来看第一条和第三条表达式。 &lt;code&gt;parseInt(&apos;0o12&apos;)&lt;/code&gt; 和 &lt;code&gt;parseInt(&apos;0o12&apos;, 8)&lt;/code&gt; 得到了两个相同的结果 &lt;code&gt;0&lt;/code&gt;，这主要是因为这条规则&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 &lt;code&gt;parseInt&lt;/code&gt; 遇到的字符不是指定 &lt;code&gt;radix&lt;/code&gt; 参数中的数字，它将忽略该字符以及所有后续字符，并返回到该点为止已解析的整数值。 &lt;code&gt;parseInt&lt;/code&gt; 将数字截断为整数值。 允许前导和尾随空格。如果第一个空白字符不能转换为数字则 &lt;code&gt;parseInt&lt;/code&gt; 表达式返回 &lt;code&gt;NaN&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;结合上面的 &lt;code&gt;radix&lt;/code&gt; 的规则，我们可以得出结论，除了十六进制字符串能够被准确识别出 &lt;code&gt;radix&lt;/code&gt;，其他字符串的 &lt;code&gt;radix&lt;/code&gt; 都默认是 &lt;code&gt;10&lt;/code&gt;，而且 &lt;code&gt;parseInt&lt;/code&gt; 对字符串是否是数字不是做一个整体的判断，而是从低位开始解析，解析到不是数字的位置停止，并返回已经解析的结果。这就导致了像 &lt;code&gt;123sdfds&lt;/code&gt; 这样的数字字符混合的字符串我们不能得到 &lt;code&gt;NaN&lt;/code&gt;，这是我们使用 &lt;code&gt;parseInt&lt;/code&gt; 需要注意的一个问题。&lt;code&gt;MDN&lt;/code&gt; 上给出了一个严格解析函数来应对这个问题：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function filterInt(value) {
  // 正则表达式判断是否只有正负号数字以及 Infinity，否则返回 NaN
  if (/^[-+]?(\d+|Infinity)$/.test(value)) {
    return Number(value)
  } else {
    return NaN
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;parseInt&lt;/code&gt; 可以识别 &lt;code&gt;+&lt;/code&gt; 和 &lt;code&gt;-&lt;/code&gt;。 &lt;code&gt;parseInt&lt;/code&gt; 不应用于科学计数法，类似 &lt;code&gt;6.022e23&lt;/code&gt; 这样的数字，对于科学计数法应当使用 &lt;code&gt;Math.floor&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;parseFloat&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;parseFloat&lt;/code&gt; 和 &lt;code&gt;parseInt&lt;/code&gt; 一样是个全局函数,不属于任何对象。它只接受一个 &lt;code&gt;String&lt;/code&gt; 作为参数，如果参数不是 &lt;code&gt;String&lt;/code&gt;，会强制转换为 &lt;code&gt;String&lt;/code&gt;，开头的空白符会被忽略，解析规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;parseFloat&lt;/code&gt; 在解析过程中遇到了正号 &lt;code&gt;+&lt;/code&gt;、负号 &lt;code&gt;-&lt;/code&gt;、数字、小数点、或者科学记数法中的指数（&lt;code&gt;e&lt;/code&gt; 或 &lt;code&gt;E&lt;/code&gt;）以外的字符，则它会忽略该字符以及之后的所有字符，返回当前已经解析到的浮点数。&lt;/li&gt;
&lt;li&gt;第二个小数点的出现也会使解析停止（在这之前的字符都会被解析）。&lt;/li&gt;
&lt;li&gt;参数首位和末位的空白符会被忽略。&lt;/li&gt;
&lt;li&gt;如果参数字符串的第一个字符不能被解析成为数字,则 &lt;code&gt;parseFloat&lt;/code&gt; 返回 &lt;code&gt;NaN&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parseFloat&lt;/code&gt; 也可以解析并返回 &lt;code&gt;Infinity&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parseFloat&lt;/code&gt; 解析 &lt;code&gt;BigInt&lt;/code&gt; 为 &lt;code&gt;Numbers&lt;/code&gt;, 丢失精度。因为末位 &lt;code&gt;n&lt;/code&gt; 字符被丢弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;考虑使用 &lt;code&gt;Number(value)&lt;/code&gt; 进行更严谨的解析，只要参数带有无效字符就会被转换为 &lt;code&gt;NaN&lt;/code&gt; 。&lt;/p&gt;
&lt;h2&gt;Number&lt;/h2&gt;
&lt;p&gt;作为原始包装对象，使用 &lt;code&gt;Number()&lt;/code&gt; 可以将参数转换为 &lt;code&gt;Number&lt;/code&gt;，转换规则参考 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/13/type-conversion/#Number&quot; title=&quot;类型转换细节&quot;&gt;类型转换细节&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;性能&lt;/h2&gt;
&lt;p&gt;上面分析了这些方法的细节，那么这些方法的性能到底如何呢，是不是真的如有些文章说的 &lt;code&gt;Number&lt;/code&gt; 的性能很差呢？我分别用 &lt;code&gt;benchmarkjs&lt;/code&gt; 在本地的 &lt;code&gt;node&lt;/code&gt; 环境中和 &lt;a href=&quot;https://jsbench.me/&quot; title=&quot;jsbench&quot;&gt;jsbench&lt;/a&gt; 上各跑了几次。&lt;code&gt;benchmarkjs&lt;/code&gt; 的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const Benchmark = require(&apos;benchmark&apos;)

const suite = new Benchmark.Suite()

suite
  .add(&apos;parseInt&apos;, () =&gt; {
    parseInt(&apos;4294967295&apos;)
  })
  .add(&apos;parseFloat&apos;, () =&gt; {
    parseFloat(&apos;4294967295&apos;)
  })
  .add(&apos;unary&apos;, () =&gt; {
    ;+&apos;4294967295&apos;
  })
  .add(&apos;bit&apos;, () =&gt; {
    &apos;4294967295&apos; &gt;&gt;&gt; 0
  })
  .add(&apos;Number&apos;, () =&gt; {
    Number(&apos;4294967295&apos;)
  })
  // add listeners
  .on(&apos;cycle&apos;, function (event) {
    console.log(String(event.target))
  })
  .on(&apos;complete&apos;, function () {
    console.log(&apos;Fastest is &apos; + this.filter(&apos;fastest&apos;).map(&apos;name&apos;))
  })
  // run async
  .run({ async: true })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跑的结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;parseInt x 8,844,909 ops/sec ±3.67% (86 runs sampled)
parseFloat x 12,166,845 ops/sec ±1.11% (91 runs sampled)
unary x 838,130,806 ops/sec ±0.92% (86 runs sampled)
bit x 839,113,877 ops/sec ±0.88% (89 runs sampled)
Number x 838,017,789 ops/sec ±0.96% (87 runs sampled)
Fastest is bit,unary,Number
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;jsbench&lt;/code&gt; 网站上的结果如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/number-jsbench.CXMB2-uW_dkF1B.webp&quot; alt=&quot;number-jsbench&quot; title=&quot;number-jsbench&quot;&gt;&lt;/p&gt;
&lt;p&gt;从两个结果上看我们可以看出一元操作符，位运算符和 &lt;code&gt;Number&lt;/code&gt; 的性能是很接近的，而 &lt;code&gt;parseInt&lt;/code&gt; 的性能则要差很多。而位操作符又有范围限制。所以最后的结论解释我们可以使用一元 &lt;code&gt;+&lt;/code&gt; 或者 &lt;code&gt;Number&lt;/code&gt; 都可以。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文我们详细分析了几种将值转为 &lt;code&gt;Number&lt;/code&gt; 的方法的细节，可能有错漏之处，欢迎指出。&lt;/p&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>token 机制和实现方式</title><link>https://clloz.com/blog/token-refresh</link><guid isPermaLink="true">https://clloz.com/blog/token-refresh</guid><pubDate>Mon, 14 Dec 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;之前在面试的时候被问到过刷新 &lt;code&gt;token&lt;/code&gt; 的问题，其实我对 &lt;code&gt;token&lt;/code&gt; 验证机制的细节一直不清楚。新项目和后端的同学商量后使用刷新 &lt;code&gt;token&lt;/code&gt; 来实现。本文主要分享一下对 &lt;code&gt;token&lt;/code&gt; 机制的理解和实现方式。&lt;/p&gt;
&lt;h2&gt;登录验证的方式&lt;/h2&gt;
&lt;p&gt;登录验证一般来说有两个目的，一个是为了安全，一个是为了用户方便。因为 &lt;code&gt;HTTP&lt;/code&gt; 是无状态的，所以后端在接受到请求之后并不能知道请求是从哪里来的，但是很多时候我们有验证用户身份的需求，同时前端又有保存用户登录状态的需求。而如果将用户信息保存在前端，必然是非常危险的，很容易被获取，所以就有了在后端进行非对称加密的方式来实现登录的验证和保存。&lt;/p&gt;
&lt;p&gt;目前主要的登录验证方式有 &lt;code&gt;cookie + session&lt;/code&gt;，&lt;code&gt;token&lt;/code&gt;，单点登录和 &lt;code&gt;OAuth 第三方登录&lt;/code&gt;。本文我们主要讲一讲 &lt;code&gt;token&lt;/code&gt; 登录验证。&lt;/p&gt;
&lt;h2&gt;什么是 token&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;token&lt;/code&gt; 直译就是令牌的意思，其实就是后端将用户信息进行非对称加密，然后将加密后的内容保存在前端，当发送请求的时候带上这个令牌来实现身份验证。大致的过程是第一次登录用户输入用户名和密码，服务器验证无误后会对用户的信息进行非对称加密生成一个令牌返回给前端，前端可以存入 &lt;code&gt;cookie&lt;/code&gt; 或者 &lt;code&gt;localStorage&lt;/code&gt; 等，以后每次发送请求带上这个令牌，后端通过对令牌的验证来识别用户的身份以及请求的合法性。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;token&lt;/code&gt; 的优点是服务端不需要保存 &lt;code&gt;token&lt;/code&gt;，只需要验证前端传过来的 &lt;code&gt;token&lt;/code&gt; 即可，所以几遍是分布式部署也可以使用这种方式。&lt;code&gt;token&lt;/code&gt; 的缺点就是，由于服务器不保存 &lt;code&gt;session&lt;/code&gt; 状态，因此无法在使用过程中废止某个 &lt;code&gt;token&lt;/code&gt;，或者更改 &lt;code&gt;token&lt;/code&gt; 的权限。也就是说，一旦 &lt;code&gt;token&lt;/code&gt; 签发了，在到期之前就会始终有效，除非服务器部署额外的逻辑。&lt;/p&gt;
&lt;p&gt;目前比较常用的 &lt;code&gt;token&lt;/code&gt; 加密方式是 &lt;code&gt;JWT JSON Web Token&lt;/code&gt;，关于 &lt;code&gt;JWT&lt;/code&gt; 可以参考阮一峰老师的 &lt;a href=&quot;https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html&quot; title=&quot;JSON Web Token 入门教程&quot;&gt;JSON Web Token 入门教程&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;token 刷新&lt;/h2&gt;
&lt;p&gt;按照上面的 &lt;code&gt;token&lt;/code&gt; 逻辑，前端只要保存一个后端传过来的 &lt;code&gt;token&lt;/code&gt;，每次请求附上即可。当令牌过期有两种选择，我们可以让用户冲洗你登录，或者后端生成一个新的令牌，前端保存新的令牌并重新发送请求。但是这两种方式都有问题，如果让用户重新登录，用户体验不是很好，频繁的重新登录并不是一种比较好的交互方式。而如果自动生成新的令牌则会出现安全问题，比如黑客获取了一个过期的令牌并向后端发送请求，则也可以获得一个更新的令牌。&lt;/p&gt;
&lt;p&gt;为了权衡上面的问题，产生了一种刷新 &lt;code&gt;token&lt;/code&gt; 的机制，当用户第一次登录成功，后端会返回两个 &lt;code&gt;token&lt;/code&gt;，一个 &lt;code&gt;accessToken&lt;/code&gt; 用来进行请求，也就是我们每次请求都附上 &lt;code&gt;accessToken&lt;/code&gt;，而 &lt;code&gt;refreshToken&lt;/code&gt; 则是用来在 &lt;code&gt;accessToken&lt;/code&gt; 过期的时候进行 &lt;code&gt;accessToken&lt;/code&gt; 的刷新。一般来说，&lt;code&gt;accessToken&lt;/code&gt; 由于每次请求都会附上，所以安全风险比较高，所以过期时间较短，而 &lt;code&gt;refreshToken&lt;/code&gt; 则只有在 &lt;code&gt;accessToken&lt;/code&gt; 过期的时候才会发送到后端，所以安全风险相对较低，所以过期时间可以长一点。&lt;/p&gt;
&lt;p&gt;当我们的 &lt;code&gt;accessToken&lt;/code&gt; 过期之后，我们会向后端的 &lt;code&gt;token&lt;/code&gt; 刷新接口请求并传入 &lt;code&gt;refreshToken&lt;/code&gt;，后端验证梅雨问题之后会给我们一个新的 &lt;code&gt;accessToken&lt;/code&gt;，我们保存后就可以保证访问的连续性。当然，这也并非绝对安全的，只是一种相对安全一点的做法。一般我们将两个 &lt;code&gt;token&lt;/code&gt; 保存在 &lt;code&gt;localStorage&lt;/code&gt; 中。&lt;/p&gt;
&lt;h2&gt;刷新 token 的实现&lt;/h2&gt;
&lt;p&gt;在项目中我主要使用的是 &lt;code&gt;axios&lt;/code&gt;，所以 &lt;code&gt;token&lt;/code&gt; 的刷新以及请求附带 &lt;code&gt;token&lt;/code&gt; 都是使用的 &lt;code&gt;axios&lt;/code&gt; 的拦截器完成的。这其中需要注意的地方有三点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不要重复刷新 &lt;code&gt;token&lt;/code&gt;，即一个请求已经刷新 &lt;code&gt;token&lt;/code&gt; 了，此时可能新的 &lt;code&gt;token&lt;/code&gt; 还没有回来，其他请求不应该重复刷新。&lt;/li&gt;
&lt;li&gt;当新的 &lt;code&gt;token&lt;/code&gt; 还没有回来的时候，其他的请求应该进行暂存，等新的 &lt;code&gt;token&lt;/code&gt; 回来以后再一次进行请求。&lt;/li&gt;
&lt;li&gt;如果请求是由登录页面或者请求本身就是刷新 &lt;code&gt;token&lt;/code&gt; 的请求则不需要拦截，否则会陷入死循环。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一个问题用一个 &lt;code&gt;Boolean&lt;/code&gt; 字段加锁即可，第二个问题将请求新 &lt;code&gt;token&lt;/code&gt; 过程中发起的请求用状态为 &lt;code&gt;pendding&lt;/code&gt; 的 &lt;code&gt;Promise&lt;/code&gt; 进行暂存，放到一个数组中，当新的 &lt;code&gt;token&lt;/code&gt; 回来的时候依次 &lt;code&gt;resolve&lt;/code&gt; 每一个 &lt;code&gt;pendding&lt;/code&gt; 的 &lt;code&gt;Promise&lt;/code&gt; 即可。具体的代码细节我直接贴上项目上的源码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import axios, * as AxiosInterface from &apos;axios&apos;;

// Token 接口，访问 token，刷新 token 和过期时
const instance = axios.create({
    // baseURL: &apos;&apos;
    timeout: 300000,
    headers: {
        &apos;Content-Type&apos;: &apos;application/json&apos;,
        &apos;X-Requested-With&apos;: &apos;XMLHttpRequest&apos;,
    },
});

async function refreshAccessToken(): Promise&amp;#x3C;AxiosInterface.AxiosResponse&amp;#x3C;AxiosData&gt;&gt; {
    return await instance.post(&apos;api/refreshtoken&apos;);
}

let isRefreshing = false;
let requests: Array&amp;#x3C;Function&gt; = []; // 若在 token 刷新过程中进来多个请求则存入 requests 中
// axios.defaults.baseURL = &apos;http://127.0.0.1:8888/api/private/v1/&apos;;
// 设置请求拦截器，若 token 过期则刷新 token
axios.interceptors.request.use(config =&gt; {
    const tokenObj = JSON.parse(window.localStorage.getItem(&apos;token&apos;) as string);
    if (config.url === &apos;api/login&apos; || config.url === &apos;api/refreshtoken&apos;) return config;
    let accessToken = tokenObj.accessToken;
    let expireTime = tokenObj.expireTime;
    const refreshToken = tokenObj.refreshToken;

    config.headers.Authorization = accessToken;

    let time = Date.now();
    console.log(time, expireTime);
    if (time &gt; expireTime) {
        if (!isRefreshing) {
            isRefreshing = true;
            refreshAccessToken()
                .then(res =&gt; {
                    ({ accessToken, expireTime } = res.data.data);
                    time = Date.now();
                    const tokenStorage = {
                        accessToken,
                        refreshToken,
                        expireTime: Number(time) + Number(expireTime),
                    };
                    window.localStorage.setItem(&apos;token&apos;, JSON.stringify(tokenStorage));
                    isRefreshing = false;
                    return accessToken;
                })
                .then((accessToken: string) =&gt; {
                    requests.forEach(cb =&gt; {
                        cb(accessToken);
                    });
                    requests = [];
                })
                .catch((err: string) =&gt; {
                    throw new Error(`refresh token error: ${err}`);
                });
        }

        // 如果是在刷新 token 时进行的请求则暂存在 requests 数组中，这里需要使用一个 pendding 的 Promise 来确保拦截的成功
        const parallelRequest: Promise&amp;#x3C;AxiosInterface.AxiosRequestConfig&gt; = new Promise(resolve =&gt; {
            requests.push((accessToken: string) =&gt; {
                config.headers.Authorization = accessToken;
                console.log(accessToken + Math.random() * 1000);
                resolve(config);
            });
        });

        return parallelRequest;
    }

    return config;
});

export default (vue: Function) =&gt; {
    vue.prototype.$http = axios;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是我对刷新 &lt;code&gt;token&lt;/code&gt; 的实现，如果有什么错误之处欢迎指正交流。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html&quot; title=&quot;JSON Web Token 入门教程&quot;&gt;JSON Web Token 入门教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000013151506&quot; title=&quot;解决使用jwt刷新token带来的问题&quot;&gt;解决使用jwt刷新token带来的问题&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/6844903993274007565#comment&quot; title=&quot;axios如何利用promise无痛刷新token（二）&quot;&gt;axios如何利用promise无痛刷新token（二）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/6844903925078818829#heading-13&quot; title=&quot;axios如何利用promise无痛刷新token&quot;&gt;axios如何利用promise无痛刷新token&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>Vue 响应式原理</title><link>https://clloz.com/blog/vue-reactive</link><guid isPermaLink="true">https://clloz.com/blog/vue-reactive</guid><pubDate>Sun, 22 Nov 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Vue3&lt;/code&gt; 将双向数据绑定的实现由 &lt;code&gt;Object.defineProperty()&lt;/code&gt; 换成了 &lt;code&gt;Proxy&lt;/code&gt;。本文我们来深入研究一下这两种方法分别如何实现双向数据绑定，并且他们的区别和优劣在哪里。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文所有代码均在 &lt;a href=&quot;https://github.com/Clloz/clloz-vue/tree/dev&quot; title=&quot;clloz-vue - Github&quot;&gt;clloz-vue - Github&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;双向数据绑定原理&lt;/h2&gt;
&lt;p&gt;双向数据绑定的概念其实很简单，比如你在页面上有一个 &lt;code&gt;input&lt;/code&gt;，你将这个 &lt;code&gt;input&lt;/code&gt; 的 &lt;code&gt;value&lt;/code&gt; 和你在 &lt;code&gt;JavaScript&lt;/code&gt; 中的一个值打个比方，你声明了一个对象 &lt;code&gt;obj&lt;/code&gt;，用其中的 &lt;code&gt;getVal&lt;/code&gt; 属性来保存这个 &lt;code&gt;input&lt;/code&gt; 的 &lt;code&gt;value&lt;/code&gt;，通过一段代码进行了绑定。达到的效果就是，当你在页面上的 &lt;code&gt;input&lt;/code&gt; 进行输入的时候（即改变了 &lt;code&gt;input&lt;/code&gt; 的 &lt;code&gt;value&lt;/code&gt;），此时你 &lt;code&gt;JavaScript&lt;/code&gt; 中的 &lt;code&gt;getVal&lt;/code&gt; 这个属性就自动发生变化了。同样当你在 &lt;code&gt;JavaScript&lt;/code&gt; 中修改了 &lt;code&gt;getVal&lt;/code&gt; 的值的时候，页面上的 &lt;code&gt;input&lt;/code&gt; 中的 &lt;code&gt;value&lt;/code&gt; 也会自动改变。&lt;/p&gt;
&lt;p&gt;想一想如何用最简单的方法实现这个功能，用 &lt;code&gt;Object.defineProperty&lt;/code&gt; 将 &lt;code&gt;obj&lt;/code&gt; 的 &lt;code&gt;getVal&lt;/code&gt; 属性的 &lt;code&gt;set&lt;/code&gt; 和 &lt;code&gt;get&lt;/code&gt; 进行重写，在 &lt;code&gt;set&lt;/code&gt; 中我们将 &lt;code&gt;set&lt;/code&gt; 的值也设置到 &lt;code&gt;input&lt;/code&gt; 的 &lt;code&gt;value&lt;/code&gt; 中去，这样就打到了修改数据，页面上的元素自动改变的效果。而修改元素改变数据，只要给 &lt;code&gt;input&lt;/code&gt; 添加一个事件即可。实现效果如下：&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;body&gt;
  &amp;#x3C;input id=&quot;be-watch&quot; type=&quot;text&quot; /&gt;
  &amp;#x3C;p id=&quot;show&quot;&gt;&amp;#x3C;/p&gt;
&amp;#x3C;/body&gt;
&amp;#x3C;script&gt;
  let obj = {
    getVal: 0
  }

  let cache = obj.getVal

  let input = document.getElementById(&apos;be-watch&apos;)
  let p = document.getElementById(&apos;show&apos;)
  input.addEventListener(&apos;input&apos;, (e) =&gt; {
    obj.getVal = e.target.value
  })

  Object.defineProperty(obj, &apos;getVal&apos;, {
    enumerable: true,
    configurable: true,
    get() {
      return cache
    },
    set(val) {
      cache = val
      input.value = val
      p.innerHTML = val
    }
  })
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们可以发现 &lt;code&gt;Object.defineProperty&lt;/code&gt; 的一个问题，由于我们使用这个方法监听属性，相当于把属性从数据属性变成了访问器属性，此时我们就无法用这个访问器属性来保存 &lt;code&gt;value&lt;/code&gt; 了。为了保证对象的正常访问，我们需要找另一个地方将变量保存起来，当执行 &lt;code&gt;get&lt;/code&gt; 的时候我们会返回那个保存的变量，也就是上面代码中的 &lt;code&gt;cache&lt;/code&gt;。注意，这里的 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 方法中不要使用 &lt;code&gt;obj.getValue&lt;/code&gt; 保存 &lt;code&gt;value&lt;/code&gt;，此时他们是访问器属性，如果你这么做会报栈溢出，因为你 &lt;code&gt;set&lt;/code&gt; 的时候执行 &lt;code&gt;obj.getValue = val&lt;/code&gt;，此时相当于执行了 &lt;code&gt;getter&lt;/code&gt;，而 &lt;code&gt;getter&lt;/code&gt; 返回的又是 &lt;code&gt;obj.getValue&lt;/code&gt;，然后又会执行 &lt;code&gt;getter&lt;/code&gt;，一直递归下去，自然就栈溢出了。&lt;/p&gt;
&lt;h2&gt;优化和应用&lt;/h2&gt;
&lt;p&gt;上面的代码是最简单的双向数据绑定，也是其核心原理。但是在实际的项目中我们不可能这么使用，因为实际的项目中要监听的对象和属性非常多，元素也各不相同，事件也各不相同，如果用上面的这种方法实现每一个属性和对应的元素事件我们都要单独绑定太麻烦了，而且所有的代码都耦合在一起，肯定是非常难以维护的。所以要把这个技术真的应用的到工程中去我们必须抽象出它的一套可行的逻辑。这其中最主要的就是发布订阅模式，不太清楚发布订阅模式的同学可以参考我的文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/18/observer-pub-sub-pattern/&quot; title=&quot;深入发布订阅模式&quot;&gt;深入发布订阅模式&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;我们来重新理一下需求，我们希望我们声明的对象和某些方法关联起来。在上面的例子中，我们是希望对象属性变化的的时候能够改变 &lt;code&gt;input&lt;/code&gt; 的 &lt;code&gt;value&lt;/code&gt;，并显示到一个 &lt;code&gt;p&lt;/code&gt; 标签中，其实就是执行了一个简单的方法，虽然这个方法只有两行代码。即我希望对象的属性变化的时候，让用到这个属性的方法能够执行。最简单的做法就是像上面的例子中，将这个方法的代码直接放到 &lt;code&gt;setter&lt;/code&gt; 中，那么在属性改变的时候自然就会执行对应的代码。&lt;/p&gt;
&lt;p&gt;但是上面这种实现在项目中肯定是无法实现的，我们每有一个地方用到了某个属性就要去它对应的 &lt;code&gt;Object.defineProperty&lt;/code&gt; 的位置加上变更的代码，而且每个属性都要有一个不一样的 &lt;code&gt;defineProperty&lt;/code&gt; 的代码，肯定是没法实现的。&lt;/p&gt;
&lt;p&gt;这个时候就要用到发布订阅模式了，我们把调用属性的方法放一边，单独抽象出对象属性的逻辑。我们对对象的包装就两件事，设置 &lt;code&gt;setter&lt;/code&gt; 和 &lt;code&gt;getter&lt;/code&gt;，在 &lt;code&gt;setter&lt;/code&gt; 中通知变化，至于通知给谁，这里我们就要通知给发布订阅模式的事件中心，这里我们称为依赖中心 &lt;code&gt;Dep&lt;/code&gt;，我们为每一个属性单独建立一个 &lt;code&gt;Dep&lt;/code&gt;，当属性变化我们就告诉 &lt;code&gt;Dep&lt;/code&gt; 就可以了，至于 &lt;code&gt;Dep&lt;/code&gt; 要怎么做我们后面再讨论。这样我们就将把普通 &lt;code&gt;object&lt;/code&gt; 变为一个可侦测监听的 &lt;code&gt;object&lt;/code&gt; 这个逻辑分离成一个可以复用的逻辑，我们将这个逻辑抽象出一个 &lt;code&gt;Observer&lt;/code&gt; 类，它通过 &lt;code&gt;setter&lt;/code&gt; 发布变化给事件中心 &lt;code&gt;Dep&lt;/code&gt;，也就是发布订阅模式中 &lt;strong&gt;发布者&lt;/strong&gt;。下面给出一个 &lt;code&gt;Observer&lt;/code&gt; 类的简单骨架：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import Dep from &apos;./dep.mjs&apos;

export class Observer {
  constructor(data) {
    this.data = data
    if (!Array.isArray(data)) {
      this.walk(data)
    }
  }

  /* eslint-disable */
  walk(obj) {
    Object.keys(obj).forEach((key) =&gt; {
      let dep = new Dep()
      let val = obj[key]

      observe(val)
      Object.defineProperty(obj, key, {
        enumreable: true,
        configurable: true,
        get() {
          if (Dep.target) {
            dep.depend()
          }
          return val
        },
        set(newValue) {
          if (val === newValue) return
          val = newValue
          dep.notify()
        }
      })
    })
  }
}

export function observe(val) {
  if (!val || typeof val !== &apos;object&apos;) {
    return
  }
  return new Observer(val)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再去思考调用到属性的方法那边，我们要解决的一个主要问题就是知道方法用了哪些属性，这些属性变化后我们要做些什么。这一步我们自然是要对代码进行分析，我们这里暂时不讨论模版解析的问题，我们就以一个简单的场景来看这个问题，比如我们在 &lt;code&gt;vue&lt;/code&gt; 中常用的 &lt;code&gt;$watch&lt;/code&gt;，我们需要在属性 &lt;code&gt;a&lt;/code&gt; 变化的时候执行某个方法就是 &lt;code&gt;vm.$watch(&apos;a&apos;, function(newVal, oldVal){ ... }&lt;/code&gt;，这是最简单的监听需求。其实无论什么样复杂的代码，简化到最后的逻辑都是这样，就是上面的例子是修改一个 &lt;code&gt;input&lt;/code&gt; 的 &lt;code&gt;value&lt;/code&gt;，或者改变一个元素的样式，它也只是想在某个属性变化的时候执行一个回调函数，它就是我们发布订阅模式中的 &lt;strong&gt;订阅者&lt;/strong&gt;。那我们可以将这个逻辑抽象成一个 &lt;code&gt;Watcher&lt;/code&gt; 类，每一个监听和回调都是一个 &lt;code&gt;Watcher&lt;/code&gt; 实例。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Watcher&lt;/code&gt; 实例主要做什么呢？分析要访问的属性（比如上面的 &lt;code&gt;a&lt;/code&gt;，这里路径可能有嵌套表达式等情况），访问该属性触发 &lt;code&gt;getter&lt;/code&gt;，我们将会在 &lt;code&gt;getter&lt;/code&gt; 中进行依赖的收集。提供一个 &lt;code&gt;update&lt;/code&gt; 函数，当依赖的属性发出变化通知的时候（&lt;code&gt;setter&lt;/code&gt; 中的逻辑），外部会调用实例内部的 &lt;code&gt;update&lt;/code&gt; 方法，执行回调函数。下面给出一个简单的 &lt;code&gt;Watcher&lt;/code&gt; 骨架：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import Dep from &apos;./dep.mjs&apos;

export default class Watcher {
  constructor(gb, exp, cb) {
    this.gb = gb
    this.data = this.gb.data
    this.exp = exp
    this.cb = cb
    this.value = this.get()
  }

  // 访问属性，添加订阅
  get() {
    Dep.target = this // 设置 Dep.target 为当前 watcher，让 Dep 知道添加谁
    let value = this.data[this.exp]
    Dep.target = null // getter 触发结束成功添加注册后设置 Dep.target 为 null，防止非 watcher 的属性访问也添加订阅
    return value
  }

  addDep(dep) {
    dep.addSub(this) // 进行订阅
  }

  // 给 Dep 调用的更新方法
  update() {
    const oldVal = this.value
    const newVal = this.get()
    if (oldVal !== newVal) {
      this.cb.call(this.gb, this.newVal, oldVal)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结一下 &lt;code&gt;Observer&lt;/code&gt; 和 &lt;code&gt;Wathcer&lt;/code&gt; 两个类，&lt;code&gt;Observer&lt;/code&gt; 是用来将属性用 &lt;code&gt;Object.defineProperty&lt;/code&gt; 进行重写的，让每一个属性都有 &lt;code&gt;getter&lt;/code&gt; 和 &lt;code&gt;setter&lt;/code&gt;，在 &lt;code&gt;getter&lt;/code&gt; 和 &lt;code&gt;setter&lt;/code&gt; 中分别完成依赖的收集和属性变更的发布，即通过 &lt;code&gt;Observer&lt;/code&gt; 我们将一个普通的 &lt;code&gt;object&lt;/code&gt; 变成了一个可以被侦测监听的 &lt;code&gt;object&lt;/code&gt; 了。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;Watcher&lt;/code&gt; 就是我们将想要订阅属性变更的一些方法的抽象，即每一个方法都是一个 &lt;code&gt;Watcher&lt;/code&gt; 实例，它通过访问对应的属性触发 &lt;code&gt;getter&lt;/code&gt; 来实现依赖的注入（注入到 &lt;code&gt;Dep&lt;/code&gt; 中），同时提供一个 &lt;code&gt;update()&lt;/code&gt;，让 &lt;code&gt;Dep&lt;/code&gt; 能在属性变化的时候调用从而执行回调函数。在实际的 &lt;code&gt;Vue&lt;/code&gt; 代码中，&lt;code&gt;Watcher&lt;/code&gt; 是在模版的解析过程中生成的，这个我们在后面讨论。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;剩下的就是依赖调度中心 &lt;code&gt;Dep&lt;/code&gt; 了，每一个被侦测的属性都会有一个 &lt;code&gt;Dep&lt;/code&gt; 实例，&lt;code&gt;Dep&lt;/code&gt; 的主要任务管理该属性依赖，即将收集到的依赖保存，当该属性变化的时候调用对应 &lt;code&gt;Watcher&lt;/code&gt; 的 &lt;code&gt;update&lt;/code&gt; 执行回调函数。&lt;/p&gt;
&lt;p&gt;依赖的收集是依靠 &lt;code&gt;Watcher&lt;/code&gt; 访问属性触发 &lt;code&gt;getter&lt;/code&gt; 来实现的，也就是我们在 &lt;code&gt;getter&lt;/code&gt; 中要将对应的 &lt;code&gt;Watcher&lt;/code&gt; 实例写入 &lt;code&gt;Dep&lt;/code&gt; 实例中去，这里就有一个问题了，&lt;code&gt;getter&lt;/code&gt; 只知道属性被调用的，但是无法确定是谁调用了属性，我们怎么将 &lt;code&gt;Watcher&lt;/code&gt; 传递进去呢？这个当然只能依赖一个 &lt;code&gt;Observer&lt;/code&gt; 类和 &lt;code&gt;Watcher&lt;/code&gt; 类都能访问到的外部属性了，&lt;code&gt;Vue&lt;/code&gt; 的做法是用一个 &lt;code&gt;Dep&lt;/code&gt; 上的静态属性 &lt;code&gt;target&lt;/code&gt;。也就是当 &lt;code&gt;Watcher&lt;/code&gt; 进行属性访问之前会先设置这个 &lt;code&gt;Dep.target&lt;/code&gt; 为自己这个 &lt;code&gt;Watcher&lt;/code&gt; 实例，然后出发 &lt;code&gt;getter&lt;/code&gt; 的时候 &lt;code&gt;getter&lt;/code&gt; 内部会告诉 &lt;code&gt;Dep&lt;/code&gt; 实例需要添加依赖的。最后当然 &lt;code&gt;Dep&lt;/code&gt; 当中还有有一个 &lt;code&gt;notify&lt;/code&gt; 方法通知保存的每个依赖 &lt;code&gt;Watcher&lt;/code&gt; 实例执行实例中的 &lt;code&gt;update&lt;/code&gt; 方法。下面是一个 &lt;code&gt;Dep&lt;/code&gt; 的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function remove(arr, item) {
  if (arr.length) {
    let index = arr.indexOf(item)
    if (index &gt; -1) {
      return arr.splice(index, 1)
    }
  }
  return false
}

export default class Dep {
  constructor() {
    this.subs = []
  }

  // 调用订阅者 Watcher 的 addDep 方法添加订阅，实际内部调用的就是下面的 addSub
  depend() {
    Dep.target.addDep(this)
  }

  // 添加订阅
  addSub(sub) {
    this.subs.push(sub)
  }

  // 移除订阅
  removeSub(sub) {
    remove(this.subs, sub)
  }

  // 接收到来自 setter 的属性变化的消息则执行所有订阅者的 update 方法
  notify() {
    this.subs.forEach((sub) =&gt; sub.update())
  }
}

Dep.target = null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这张图来自 《深入浅出 vue.js》，可以帮助大家理解这里的逻辑：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/vue-two-ways-binding1.DTe36g2y_ZNoVjC.webp&quot; alt=&quot;binding1&quot; title=&quot;binding1&quot;&gt;&lt;/p&gt;
&lt;p&gt;最后我们写一个例子来测试上面的逻辑是否能够运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { observe } from &apos;./observer.mjs&apos;
import Watcher from &apos;./watcher.mjs&apos;

let obj = {
  data: {
    name: &apos;clloz&apos;,
    age: &apos;28&apos;
  }
}

observe(obj)

/* eslint-disable */
new Watcher(obj, &apos;name&apos;, function () {
  console.log(&apos;reactive successful&apos;)
})

obj.data.name = &apos;clloz1992&apos; // reactive successful
console.log(obj.data.name) // clloz1992
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在例子中我们用 &lt;code&gt;observe&lt;/code&gt; 方法来对 &lt;code&gt;obj&lt;/code&gt; 对象进行改造，然后用一个 &lt;code&gt;watcher&lt;/code&gt; 示例来注册 &lt;code&gt;name&lt;/code&gt; 属性，当我们给 &lt;code&gt;name&lt;/code&gt; 属性赋值的时候，我们发现 &lt;code&gt;reactive successful&lt;/code&gt; 成功输出了。&lt;/p&gt;
&lt;h2&gt;Vue 的双向绑定&lt;/h2&gt;
&lt;p&gt;上面我们已经基本实现了一个还算不错的双向绑定，但是在 &lt;code&gt;Vue&lt;/code&gt; 中要还需要处理的更加细致，比如 &lt;code&gt;Watcher&lt;/code&gt; 中的 &lt;code&gt;exp&lt;/code&gt;，它可能是一个函数，函数不需要特别处理，执行就可以了。也有可能是 &lt;code&gt;a.b.c&lt;/code&gt; 这样的嵌套形式，我们必须得对这个字符串进行解析，将它的访问层级区分开来。&lt;code&gt;Vue&lt;/code&gt; 的处理如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split(&apos;.&apos;)
  return function (obj) {
    for (let i = 0; i &amp;#x3C; segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;ParsePath&lt;/code&gt; 就是用来处理 &lt;code&gt;exp&lt;/code&gt; 的，在我上面写的例子中，我是直接访问 &lt;code&gt;this.data[exp]&lt;/code&gt;，如果是有嵌套的对象这样肯定行不通。通过上面的函数。用 &lt;code&gt;.&lt;/code&gt; 将嵌套的访问进行分割，然后沿着 &lt;code&gt;obj&lt;/code&gt; 一层一层向下访问即可。添加了这个逻辑之后我们就可以访问嵌套的逻辑了。当然了我们的属性访问还可以通过 &lt;code&gt;[]&lt;/code&gt; 运算符，如果使用了 &lt;code&gt;[]&lt;/code&gt; 就必须要转换成 &lt;code&gt;.&lt;/code&gt; 的访问形式。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Vue&lt;/code&gt; 还为 &lt;code&gt;Watcher&lt;/code&gt; 和 &lt;code&gt;Dep&lt;/code&gt; 实例添加了 &lt;code&gt;id&lt;/code&gt; 和 &lt;code&gt;hash&lt;/code&gt;，防止重复的订阅添加。以及在 &lt;code&gt;Observer&lt;/code&gt; 的 &lt;code&gt;setter&lt;/code&gt; 中对新添加的属性执行了 &lt;code&gt;observe&lt;/code&gt;，应该是为了处理设置的属性是一个对象的情况。想要详细了解可以去看 &lt;code&gt;Vue&lt;/code&gt; 的源码 &lt;a href=&quot;https://github.com/vuejs/vue/tree/70f497e2e1651f78c5189a05144ed8b1659a1826/src/core/observer&quot; title=&quot;Vue Souce Code&quot;&gt;Vue Souce Code&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vue&lt;/code&gt; 的双向绑定原理图（来自 &lt;a href=&quot;https://segmentfault.com/a/1190000022600105&quot; title=&quot;vue双向数据绑定原理图(简易) - zhangjinpei&quot;&gt;vue双向数据绑定原理图(简易) - zhangjinpei&lt;/a&gt;，可以帮助大家理解整个执行过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/vue-two-ways-binding2.CawZjY6p_1sFg8A.webp&quot; alt=&quot;binding2&quot; title=&quot;binding2&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Object.defineProperty 的局限&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;Object.defineProperty&lt;/code&gt; 实现双向绑定大致就这些内容，但是 &lt;code&gt;Object.defineProperty&lt;/code&gt; 有一些局限。比如我们是通过改写对象的属性，添加 &lt;code&gt;getter&lt;/code&gt; 和 &lt;code&gt;setter&lt;/code&gt; 实现属性监听的。如果我们是在对象已经用 &lt;code&gt;observe&lt;/code&gt; 包装完成后再向其中添加属性，这些属性由于没有进行 &lt;code&gt;getter&lt;/code&gt; 和 &lt;code&gt;setter&lt;/code&gt; 的设置，自然是无法追踪的。而且 &lt;code&gt;Object.defineProperty&lt;/code&gt; 也无法侦测到对象的删除。&lt;/p&gt;
&lt;p&gt;总的来说 &lt;code&gt;Object.defineProperty&lt;/code&gt; 只能通过将数据属性转换为访问器属性来实现数据变化的追踪，它只能监听数据的变化，而不是对对象进行监听，属性的增加删除它是不知道的。&lt;code&gt;Vue2&lt;/code&gt; 的解决方式主要是通过 &lt;code&gt;vm.$set&lt;/code&gt; 和 &lt;code&gt;vm.$delete&lt;/code&gt; 解决的，这里不讨论。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object.defineProperty&lt;/code&gt; 的另一个缺陷就是无法监听数组的变化，这里的不能监听主要是不能监听 &lt;code&gt;Array.prototype&lt;/code&gt; 上的方法，数组元素的变化还是能监听的，比如一个数组 &lt;code&gt;[1,2,3]&lt;/code&gt;，我们还是能通过 &lt;code&gt;arr.0&lt;/code&gt; 监听数组元素的变化。但是像 &lt;code&gt;push&lt;/code&gt;，&lt;code&gt;pop&lt;/code&gt; 等方法就无能为了。因为这些方法不需要经过 &lt;code&gt;getter&lt;/code&gt; 和 &lt;code&gt;setter&lt;/code&gt;，而是数组原型上的方法。&lt;/p&gt;
&lt;p&gt;虽然数组的元素是能够实现监听的，但是实际上 &lt;code&gt;vue2&lt;/code&gt; 中并没有对数组的元素进行监听，即我们直接对数组元素修改是不会有任何响应式的行为，因为在 &lt;code&gt;JavaScript&lt;/code&gt; 中数组的操作非常频繁，不像对象一样只有 &lt;code&gt;key-value&lt;/code&gt; 的变化，如果对每一个元素都进行响应式的绑定在数组非常大并且操作非常频繁的的时候是很可能影响性能，所以 &lt;code&gt;vue2&lt;/code&gt; 采取的是比较折中的方法，只是对数组的 &lt;code&gt;7&lt;/code&gt; 中操作进行了监听：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shift()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unshift()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;splice()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sort()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reverse()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主要的方法就是在 &lt;code&gt;Array&lt;/code&gt; 实例和 &lt;code&gt;Array.prototype&lt;/code&gt; 之间添加了一个拦截器，当我们访问这些方法的时候，实际上访问的是拦截器上的方法。依赖的收集和 &lt;code&gt;Object&lt;/code&gt; 一样，都是在 &lt;code&gt;Observer&lt;/code&gt; 中的 &lt;code&gt;getter&lt;/code&gt; 中进行收集，在拦截器中触发依赖（这部分等以后有时间展开）&lt;/p&gt;
&lt;h2&gt;Vue 3.0 的 Proxy 实现&lt;/h2&gt;
&lt;p&gt;关于 &lt;code&gt;Proxy&lt;/code&gt; 的响应式，可以先看一个 &lt;code&gt;Proxy&lt;/code&gt; 实现的调色器的例子：&lt;/p&gt;
&lt;p&gt;代码在 &lt;a href=&quot;https://github.com/Clloz/clloz-vue/tree/dev/src/reactive/simplify&quot; title=&quot;调色盘 - Proxy&quot;&gt;调色盘 - Proxy&lt;/a&gt;，具体如何进行绑定的参考代码。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Vue 3&lt;/code&gt; 的源码阅读中，有时间补上。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/6844903601416978439#heading-14&quot; title=&quot;面试官: 实现双向绑定Proxy比defineproperty优劣如何?&quot;&gt;面试官: 实现双向绑定Proxy比defineproperty优劣如何?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;《深入浅出 vue.js》&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000022600105&quot; title=&quot;vue双向数据绑定原理图(简易)&quot;&gt;vue双向数据绑定原理图(简易)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/entry/6844903479044112391&quot; title=&quot;vue 的双向绑定原理及实现&quot;&gt;vue 的双向绑定原理及实现&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ustbhuangyi.github.io/vue-analysis/v2/prepare/&quot; title=&quot;Vue.js 技术揭秘&quot;&gt;Vue.js 技术揭秘&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/vuelogo.SKsyftqi.png"/><enclosure url="/_astro/vuelogo.SKsyftqi.png"/></item><item><title>githubusercontent curl 无法连接的解决办法</title><link>https://clloz.com/blog/curl-dns-pollution</link><guid isPermaLink="true">https://clloz.com/blog/curl-dns-pollution</guid><pubDate>Fri, 20 Nov 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;今天在安装 &lt;code&gt;wakatime&lt;/code&gt; 的 &lt;code&gt;Xcode&lt;/code&gt; 插件的时候发现 &lt;code&gt;curl&lt;/code&gt; 返回 &lt;code&gt;Failed to connect to raw.githubusercontent.com port 443: Connection refused&lt;/code&gt;。相信也有小伙伴遇到过这种情况，并且开启了代理也依然无法访问。这主要原因是 &lt;code&gt;raw.githubusercontent.com&lt;/code&gt; 的 &lt;code&gt;DNS&lt;/code&gt; 被污染了，也就是 &lt;code&gt;DNS&lt;/code&gt; 服务器没有解析正确的 &lt;code&gt;IP&lt;/code&gt; 给我们，所以结果自然是访问失败 😏 。&lt;/p&gt;
&lt;h2&gt;解决办法&lt;/h2&gt;
&lt;p&gt;解决办法就是修改 &lt;code&gt;hosts&lt;/code&gt; 文件，直接在本地完成 &lt;code&gt;DNS&lt;/code&gt; 的解析就不需要去访问上层的 &lt;code&gt;DNS&lt;/code&gt; 服务器了。&lt;code&gt;DNS&lt;/code&gt; 解析一般是先查看本机的 &lt;code&gt;hosts&lt;/code&gt;，然后是查看路由器，如果都没查到就要访问 &lt;code&gt;DNS&lt;/code&gt; 服务器了。&lt;/p&gt;
&lt;p&gt;我们先去 &lt;a href=&quot;https://www.ipaddress.com/&quot; title=&quot;IP Address&quot;&gt;IP Address&lt;/a&gt; 查询一下域名定义的 &lt;code&gt;IP&lt;/code&gt;，然后在 &lt;code&gt;hosts&lt;/code&gt; 中添加一条解析记录即可。最后查询到的 &lt;code&gt;IP&lt;/code&gt; 是 &lt;code&gt;199.232.96.133&lt;/code&gt;，所以我们用 &lt;code&gt;sudo vim /etc/hosts&lt;/code&gt; 编辑 &lt;code&gt;hosts&lt;/code&gt; 文件，然后添加一条 &lt;code&gt;199.232.68.133 raw.githubusercontent.com&lt;/code&gt; 解析记录即可。&lt;/p&gt;</content:encoded><h:img src="/_astro/macos.RfaH1x-K.jpg"/><enclosure url="/_astro/macos.RfaH1x-K.jpg"/></item><item><title>JavaScript 面向对象</title><link>https://clloz.com/blog/javascript-oop</link><guid isPermaLink="true">https://clloz.com/blog/javascript-oop</guid><pubDate>Sun, 08 Nov 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;想写这篇文章很久了，虽然现在都 &lt;code&gt;ES6&lt;/code&gt; 已经相当普及了，大家都已经开始用 &lt;code&gt;class&lt;/code&gt; 来定义类了。不过 &lt;code&gt;class&lt;/code&gt; 关键字也可以看做是一个语法糖（&lt;code&gt;Syntactic sugar&lt;/code&gt;），它的绝大部分功能，&lt;code&gt;ES5&lt;/code&gt; 都可以做到，新的 &lt;code&gt;class&lt;/code&gt; 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。不过我认为对于整个 &lt;code&gt;javascript&lt;/code&gt; 的继承机制还是应该更加深入了解比较好。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;语法糖（英语：&lt;code&gt;Syntactic sugar&lt;/code&gt;）是由英国计算机科学家彼得·兰丁发明的一个术语，指计算机语言中添加的某种语法，这种语法对语言的功能没有影响，但是更方便程序员使用。语法糖让程序更加简洁，有更高的可读性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;什么是面向对象&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.zhihu.com/question/305042684&quot;&gt;https://www.zhihu.com/question/305042684&lt;/a&gt; 面向对象这个名词从大学开始就一直接触，它的核心似乎很简单，但又模模糊糊。我个人觉得产生这种感觉的原因就是面向对象不是一个数学或物理概念，不是一个定理。它是一种设计模式，没有一个确定的描述，它是从实践中总结出的一种比较优秀的设计程序的模式，所以要学会它，只能不断的从实践中去验证，采坑。&lt;/p&gt;
&lt;p&gt;我个人的理解面向对象就是对问题的抽象，将问题抽象成一个个小的模块分而治之，模块之间的关系尽量单一清晰，这样代码在扩展和维护的时候所花费的精力最小，也就是所谓的高内聚，低耦合。之所以它并不容易掌握，就是这个抽象能力是需要通过各种复杂的问题不断训练的。很多时候我们只是想着将写出来的代码进行复用，不要写出重复的代码，但其实最重要的是对问题的抽象，划分出合理的模块和设计模块之间的耦合关系，代码复用只是这个过程中自然而然产生的现象，我们应该明白问题的本质。&lt;/p&gt;
&lt;h2&gt;面向对象设计原则&lt;/h2&gt;
&lt;p&gt;| 缩写  | 英文名称                                        | 中文名称                                                       | 概念                                                                     |
| ----- | ----------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ |
| &lt;code&gt;SRP&lt;/code&gt; | &lt;code&gt;Single Responsibility Principle&lt;/code&gt;               | 单一职责原则                                                   | 对象应该仅具有一种单一功能                                               |
| &lt;code&gt;OCP&lt;/code&gt; | &lt;code&gt;Open Close Principle&lt;/code&gt;                          | 开闭原则                                                       | 软件体应该是对于扩展开放的，但是对于修改封闭的                           |
| &lt;code&gt;LSP&lt;/code&gt; | &lt;code&gt;Liskov Substitution Principle&lt;/code&gt;                 | 里氏替换原则                                                   | 程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的       |
| &lt;code&gt;LoD&lt;/code&gt; | &lt;code&gt;Law of Demeter （ Least Knowledge Principle）&lt;/code&gt; | 迪米特法则（最少知道原则）                                     | 一个对象应该对尽可能少的对象有接触，也就是只接触那些真正需要接触的对象。 |
| &lt;code&gt;ISP&lt;/code&gt; | &lt;code&gt;Interface Segregation Principle&lt;/code&gt;               | 接口分离原则                                                   | 多个特定客户端接口要好于一个宽泛用途的接口                               |
| &lt;code&gt;DIP&lt;/code&gt; | &lt;code&gt;Dependency Inversion Principle&lt;/code&gt;依赖倒置原则    | 一个方法应该遵从依赖于抽象而不是一个实例依赖注入是一种实现方式 |                                                                          |&lt;/p&gt;
&lt;p&gt;对于面向对象设计原则的解读可以参考 &lt;a href=&quot;https://juejin.im/post/6844903673672237063#heading-42&quot; title=&quot;面向对象设计的六大设计原则&quot;&gt;面向对象设计的六大设计原则&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;创建对象&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;ES6&lt;/code&gt; 之前，语言的标准中没有一个正式的面向对象的构造和继承的支持，我们的对象继承机制都是基于原型的。在 &lt;code&gt;ES6&lt;/code&gt; 之后，标准中引进了更类似传统面向对象于洋的 &lt;code&gt;class&lt;/code&gt; 和继承方式，如今已经得到广泛的支持。也许你觉得只要学会 &lt;code&gt;class&lt;/code&gt; 的用法就可以了，但其实 &lt;code&gt;class&lt;/code&gt; 也只是对原型继承的一个语法层面的包装，搞清楚 &lt;code&gt;class&lt;/code&gt; 背后的原型继承的整个机制，我们才能写出更好的代码。&lt;/p&gt;
&lt;p&gt;我们知道在 &lt;code&gt;JavaScript&lt;/code&gt; 创建一个普通的对象可以使用 &lt;code&gt;Object&lt;/code&gt; 构造函数，对象字面量 &lt;code&gt;{}&lt;/code&gt; 。但是如果我们想要批量创建一些具有相同属性的对象，这些方法就不太适合了，我们会写出许多重复的代码。这当然是面向对象中一个很重要的问题，即如何封装对象属性方法，用一个比较高效的方式创建对象。在 &lt;code&gt;JavaScript&lt;/code&gt; 的发展过程中，有很多种创建对象的方式，有些可能现在不会使用了，但还是了解一下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;由于 JavaScript 的面向对象是基于原型的，所以如果你对 JavaScript 的原型还不是很了解，建议你先看一下 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/11/javascript-prototype/&quot; title=&quot;JavaScript 原型&quot;&gt;JavaScript 原型机制&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;工厂模式&lt;/h2&gt;
&lt;p&gt;工厂模式是在软件工程中非常有名的设计模式，它将创建对象的具体过程抽象了出来。将创建对象，添加属性的过程封装为一个函数，属性值作为函数的参数传入，当我们想要一个对象的时候，就执行这个函数即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function createPerson(name, age, job) {
  let o = new Object()
  o.name = name
  o.age = age
  o.job = job
  o.sayName = function () {
    console.log(this.name)
  }
  return o
}

let person1 = createPerson(&apos;Nicholas&apos;, 29, &apos;Software Engineer&apos;)
let person2 = createPerson(&apos;Greg&apos;, 27, &apos;Doctor&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工厂模式创建的对象有很多问题，一个最基本的就是不同的工厂模式函数创建出来的对象都是 &lt;code&gt;Object&lt;/code&gt; 构造的，我们没有办法区分它们。&lt;/p&gt;
&lt;h2&gt;构造函数模式&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中，构造函数用来创建特定类型的对象。&lt;code&gt;JavaScript&lt;/code&gt; 引擎给我们提供了非常多的内置构造函数，比如 &lt;code&gt;Array&lt;/code&gt; 和 &lt;code&gt;Object&lt;/code&gt; 等等。我们也可以通过 &lt;code&gt;function&lt;/code&gt; 定义自己的构造函数，在构造函数中我们可以定义我们的对象需要的属性和方法。&lt;/p&gt;
&lt;p&gt;比如上面的那个 &lt;code&gt;createPerson&lt;/code&gt; 的例子我们可以用构造函数进行如下改写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = function () {
    console.log(this.name)
  }
}

let person1 = new Person(&apos;Nicholas&apos;, 29, &apos;Software Engineer&apos;)
let person2 = new Person(&apos;Greg&apos;, 27, &apos;Doctor&apos;)

person1.sayName() // Nicholas
person2.sayName() // Greg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用构造函数和上面的工厂模式的区别是，我们不在需要手动创建一个对象，所有的属性和对象都赋值到了 &lt;code&gt;this&lt;/code&gt; 对象上，我们也不需要返回语句。还有一个小区别就是，我们创建构造函数一般首字母会大写，这是一个大多数面向对象语言的一个规定，用来区分普通函数和构造函数，特别是在 &lt;code&gt;JavaScript&lt;/code&gt; 中构造函数其实就是一个普通的函数。&lt;/p&gt;
&lt;p&gt;之所以有上面这些区别，是因为 &lt;code&gt;new&lt;/code&gt; 操作符帮我们做了很多事情，关于 &lt;code&gt;new&lt;/code&gt; 操作符我在另一片文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/29/new-operator/&quot; title=&quot;JavaScript中new操作符的解析和实现&quot;&gt;JavaScript中new操作符的解析和实现&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;构造函数可以是函数声明，也可以是函数表达式。相比于工厂模式，构造函数模式让我们能够以一个固定的类型创建对象，并且所有对象都会是 &lt;code&gt;Object&lt;/code&gt; 构造函数的实例。实例化的时候，&lt;code&gt;new&lt;/code&gt; 后面的函数名可以带括号也可以不带括号，如果你不需要传入参数，可以省略括号。&lt;/p&gt;
&lt;p&gt;需要注意的是，构造函数和其他的函数没有本质的不同，唯一的区别就是调用方式。任何函数通过 &lt;code&gt;new&lt;/code&gt; 操作符调用就是作为一个构造函数，没有用 &lt;code&gt;new&lt;/code&gt; 进行调用就是作为一个普通的函数。当然还有一个区别就是我们会将构造函数的首字母大写，但这不是强制性的，只是一种约定。&lt;/p&gt;
&lt;p&gt;构造函数模式创建对象也有自己的缺点，那就是构造函数中的方法会在每个实例上都创建一遍，比如例子中的 &lt;code&gt;sayName&lt;/code&gt; 方法，但是实际上我们并不需要在每个实例上都创建这个方法，特别是在我们有 &lt;code&gt;this&lt;/code&gt; 对象的情况下。我们需要有一种方式让我们的实例能够共用某些方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里提一点，我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/30/js-this/&quot; title=&quot;JavaScript中的this指向&quot;&gt;JavaScript中的this指向&lt;/a&gt; 一文中曾经说过不要将箭头函数使用在对象的方法或者构造函数原型的方法上，因为它没有自己的 &lt;code&gt;this&lt;/code&gt;。但是在构造函数中定义的实例的方法（绑定在 &lt;code&gt;this&lt;/code&gt; 上的）可以使用箭头函数，&lt;code&gt;this&lt;/code&gt; 能正确返回。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;原型模式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;如果你对 JavaScript 的原型还不是很了解，请先看 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/11/javascript-prototype/&quot; title=&quot;JavaScript 原型&quot;&gt;JavaScript 原型机制&lt;/a&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原型机制是整个 &lt;code&gt;JavaScript&lt;/code&gt; 继承的核心。每一个函数创建的时候就会有一个 &lt;code&gt;prototype&lt;/code&gt; 属性指向一个对象，用该构造函数创建的每一个对象都可以共享这个 &lt;code&gt;prototype&lt;/code&gt; 上的属性和方法，一般我们就称 &lt;code&gt;prototype&lt;/code&gt; 对应的对象为实例的原型对象。&lt;/p&gt;
&lt;p&gt;有了原型机制，我们可以把那些每个实例都通用的属性和方法放到原型对象中去，而不必在每个实例上都创建。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Person() {}

Person.prototype.name = &apos;Nicholas&apos;
Person.prototype.age = 29
Person.prototype.job = &apos;Software Engineer&apos;
Person.prototype.sayName = function () {
  console.log(this.name)
}

let person1 = new Person()
person1.sayName() // &quot;Nicholas&quot;

let person2 = new Person()
person2.sayName() // &quot;Nicholas&quot;

console.log(person1.sayName == person2.sayName) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原型机制是贯穿整个 &lt;code&gt;JavaScript&lt;/code&gt; 核心的一个机制，所有内置对象都基于这个机制创建，我们之所以能在自己创建的对象，数组字符串上使用内置的 &lt;code&gt;API&lt;/code&gt;，就是因为原型机制的存在，所以一定要搞清楚这个机制的细节。当然，不建议直接修改内置对象的原型，除非你确定不会引发命名冲突和兼容性问题。&lt;/p&gt;
&lt;p&gt;尽管原型模式非常有用，但是如果只是单纯使用原型模式还是存在问题。在构造函数模式中，我们可以通过向构造函数传入参数，让最后生成的对象的属性值是由我们自定义的，每个实例都可以通过参数直接设定属性值。而在原型模式中，访问每个实例的同一个属性返回值都是相同的。&lt;/p&gt;
&lt;p&gt;但是这还是个小问题，最关键的问题是所有实例共享属性。对于函数的共享来说，这当然是一个理想的解决方案。对于一个原型类型的值也还说得过去。但是如果对象的属性是一个引用类型，那么问题就出现了，我在 &lt;code&gt;A&lt;/code&gt; 实例中修改了这个引用类型的值，当 &lt;code&gt;B&lt;/code&gt; 实例去取这个引用类型中的属性的时候，这个引用类型已经发生了变化。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Person() {}

Person.prototype = {
  constructor: Person,
  name: &apos;Nicholas&apos;,
  age: 29,
  job: &apos;Software Engineer&apos;,
  friends: [&apos;Shelby&apos;, &apos;Court&apos;],
  sayName() {
    console.log(this.name)
  }
}

let person1 = new Person()
let person2 = new Person()

person1.friends.push(&apos;Van&apos;)

console.log(person1.friends) // &quot;Shelby,Court,Van&quot;
console.log(person2.friends) // &quot;Shelby,Court,Van&quot;
console.log(person1.friends === person2.friends) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的例子我们可以看到，实例 &lt;code&gt;person1&lt;/code&gt; 修改了原型上的 &lt;code&gt;friends&lt;/code&gt; 属性，该属性是个引用类型。当 &lt;code&gt;person2&lt;/code&gt; 去取这个属性的时候，这个变化也出现了。这是因为引用类型保存的只是一个指向对象的指针。如果我们确实希望 &lt;code&gt;person1&lt;/code&gt; 的修改反映到 &lt;code&gt;person2&lt;/code&gt; 中，这样做没有问题，但是大部分时候我们希望实例化对象的时候，它们的属性都是独立的。所以我们很少单独使用原型模式创建对象。&lt;/p&gt;
&lt;h2&gt;继承&lt;/h2&gt;
&lt;p&gt;继承是面向语言中的一个最为人津津乐道的概念。许多面向对象语言都支持两种继承方式: 接口继承和 实现继承。接口继承只继承方法签名，而实现继承则继承实际的方法。如前所述，由于函数没有签名， 在 &lt;code&gt;ECMAScript&lt;/code&gt; 中无法实现接口继承。&lt;code&gt;ECMAScript&lt;/code&gt; 只支持实现继承，而且其实现继承主要是依靠原型链 来实现的。&lt;/p&gt;
&lt;p&gt;一个函数签名 (或类型签名，或方法签名) 定义了 函数 或 方法 的输入与输出。一个签名可以包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数 及参数的 类型&lt;/li&gt;
&lt;li&gt;一个返回值及其类型&lt;/li&gt;
&lt;li&gt;可能会抛出或传回的 异常&lt;/li&gt;
&lt;li&gt;有关 面向对象 程序中方法可用性的信息 (例如关键字 public、static 或 prototype)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原型链的内容这里不重复讲了，还是看上面说的那篇文章，讲的很详细了。这里说一说原型链继承方式的问题。上面的小结已经介绍了一个问题，就是原型中共享的引用类型的修改会反映到每个实例中，这也就是为什么大部分属性我们都是在构造函数中定义。并且在实际的编码中，我们的构造函数的原型常常是另一个构造函数的实例，即使我们原先是用构造函数创建的实例属性，也成了另一个实例的原型属性。可以看一看下面的原型继承的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function SuperType() {
  this.colors = [&apos;red&apos;, &apos;blue&apos;, &apos;green&apos;]
}

function SubType() {}

// inherit from SuperType
SubType.prototype = new SuperType()

let instance1 = new SubType()
instance1.colors.push(&apos;black&apos;)
console.log(instance1.colors) // &quot;red,blue,green,black&quot;

let instance2 = new SubType()
console.log(instance2.colors) // &quot;red,blue,green,black&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SubType.prototype&lt;/code&gt; 是 &lt;code&gt;SuperType&lt;/code&gt; 的一个实例，&lt;code&gt;SuperType&lt;/code&gt; 构造函数中定义的 &lt;code&gt;colors&lt;/code&gt; 属性也就成了 &lt;code&gt;SubType&lt;/code&gt; 实例的原型属性而被每个实例共享，最后的问题就还是和上面一样，每个实例对该属性的修改都会反映在其他实例上。原型继承还有个问题就是我们在实例化 &lt;code&gt;SubType&lt;/code&gt; 的时候没法向 &lt;code&gt;SuperType&lt;/code&gt; 构造函数传递参数。&lt;/p&gt;
&lt;p&gt;原型继承的所有问题的根源就是所有实例共享属性，属性的变化会反映到每个实例中，这在实践中是非常糟糕的，所以我们很少单独使用原型链来实现继承。为了解决这个问题，实现一个比较好用又稳定的继承，有很多种继承方式被提出来。&lt;/p&gt;
&lt;h2&gt;借用构造函数&lt;/h2&gt;
&lt;p&gt;借用构造函数 &lt;code&gt;constructor stealing&lt;/code&gt;，也被称为对象伪造 &lt;code&gt;object masquerading&lt;/code&gt; 或者经典继承 &lt;code&gt;classical inheritance&lt;/code&gt;。它的实现原理很简单，在子类型 &lt;code&gt;SubType&lt;/code&gt; 的构造函数中调用超类型 &lt;code&gt;SuperType&lt;/code&gt; 的构造函数。这种思想的本质就是实例化的过程即 &lt;code&gt;new&lt;/code&gt; 的过程还是指定 &lt;code&gt;this&lt;/code&gt; 执行构造函数，如果我们希望超类型的属性也被实例创建，在子类型构造函数中通过 &lt;code&gt;apply&lt;/code&gt; 或者 &lt;code&gt;call&lt;/code&gt; 指定 &lt;code&gt;this&lt;/code&gt;执行超类型即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function SuperType() {
  this.colors = [&apos;red&apos;, &apos;blue&apos;, &apos;green&apos;]
}

function SubType() {
  // inherit from SuperType
  SuperType.call(this)
}

let instance1 = new SubType()
instance1.colors.push(&apos;black&apos;)
console.log(instance1.colors) // &quot;red,blue,green,black&quot;

let instance2 = new SubType()
console.log(instance2.colors) // &quot;red,blue,green&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码就是接用构造函数的一种实现，你甚至可以简单的理解为，把 &lt;code&gt;SuperType&lt;/code&gt; 中的代码移到 &lt;code&gt;SubType&lt;/code&gt; 中执行，就像把它的代码 &lt;strong&gt;“借”&lt;/strong&gt; 过来一样，所以取名叫借用构造函数，本质上相当于将构造函数中可以复用的属性定义提取成一个函数，方便复用。比如你的需求中有很多构造函数都需要 &lt;code&gt;name&lt;/code&gt;，&lt;code&gt;age&lt;/code&gt; 和 &lt;code&gt;sex&lt;/code&gt; 属性，你不需要在每个构造函数中都写上 &lt;code&gt;this.name = xxx&lt;/code&gt;，&lt;code&gt;this.age = xxx&lt;/code&gt; 等，把他们提取出来写成一个函数，然后在构造函数中调用即可。&lt;/p&gt;
&lt;p&gt;这样 &lt;code&gt;SuperType&lt;/code&gt; 中定义的属性，&lt;code&gt;SubType&lt;/code&gt; 的每一个实例都有自己的副本而不是指向同一个对象。并且我们可以直接在 &lt;code&gt;SubType&lt;/code&gt; 的构造过程中传递参数给 &lt;code&gt;SuperType&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;借用构造函数本质还是构造函数，只是优化了构造函数的结构而已，并没有什么革新。所以构造函数的问题它自然也没有解决，那就是定义的属性和方法没法复用。而且我们在子类构造的时候虽然调用了 &lt;code&gt;SuperType&lt;/code&gt;，但是 &lt;code&gt;SuperType&lt;/code&gt; 中的属性和方法对 &lt;code&gt;SubType&lt;/code&gt; 完全是不可见的，我们也没法操作这些方法和属性，这样灵活性就大打折扣。&lt;/p&gt;
&lt;h2&gt;组合继承&lt;/h2&gt;
&lt;p&gt;组合继承 &lt;code&gt;combination inheritance&lt;/code&gt; 有时也称为伪经典继承 &lt;code&gt;pseudoclassical inheritance&lt;/code&gt;，它的思路就是将借用构造函数和原型链结合起来（由于借用构造函数本质就是构造函数，可以理解为构造函数和原型链的结合），使用原型链实现对原型属性和方法的继承，而通过借用构造函数来实现对实例属性的继承。这样，既通过在原型上定义方法实现了函数复用，又能够保证每个实例都有它自己的属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function SuperType(name) {
  this.name = name
  this.colors = [&apos;red&apos;, &apos;blue&apos;, &apos;green&apos;]
}

SuperType.prototype.sayName = function () {
  console.log(this.name)
}

function SubType(name, age) {
  // inherit properties
  SuperType.call(this, name)

  this.age = age
}

// inherit methods
SubType.prototype = new SuperType()

SubType.prototype.sayAge = function () {
  console.log(this.age)
}

let instance1 = new SubType(&apos;Nicholas&apos;, 29)
instance1.colors.push(&apos;black&apos;)
console.log(instance1.colors) // &quot;red,blue,green,black&quot;
instance1.sayName() // &quot;Nicholas&quot;;
instance1.sayAge() // 29

let instance2 = new SubType(&apos;Greg&apos;, 27)
console.log(instance2.colors) // &quot;red,blue,green&quot;
instance2.sayName() // &quot;Greg&quot;;
instance2.sayAge() // 27
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的例子中我们将共用的方法写在 &lt;code&gt;SuperType.prototype&lt;/code&gt; 上通过原型链实现继承，不需要共用的属性如 &lt;code&gt;name&lt;/code&gt; 和 &lt;code&gt;colors&lt;/code&gt; 则放到构造函数中，让每一个实例都有独立的副本。组合模式结合了借用构造函数和原型链的优点而避开了它们的缺点，是比较常用的实现继承的方式。这种实现方式也能够使用 &lt;code&gt;instanceof&lt;/code&gt; 和 &lt;code&gt;isPrototypeOf()&lt;/code&gt; 来进行继承关系的判断。&lt;/p&gt;
&lt;p&gt;组合式继承的一个缺点就是它调用了两次构造函数，一次在子类的构造函数中借用父类的构造函数。一次是在创建子类的 &lt;code&gt;prototype&lt;/code&gt; 的时候是用实例化父类来实现的，这样创建的 &lt;code&gt;prototype&lt;/code&gt; 里面可能会有一些没用的实例属性，而且如果构造函数很复杂，也会影响一些性能，这完全是可以规避的，用我们下面介绍的两种继承方式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function SuperType(name) {
  this.name = name
  this.colors = [&apos;red&apos;, &apos;blue&apos;, &apos;green&apos;]
}

SuperType.prototype.sayName = function () {
  console.log(this.name)
}

function SubType(name, age) {
  SuperType.call(this, name)

  this.age = age
}
SubType.prototype = new SuperType()

SubType.prototype.sayAge = function () {
  console.log(this.age)
}

let instance1 = new SubType(&apos;Nicholas&apos;, 29)
console.log(instance1.__proto__) // SuperType {name: undefined, colors: [ &apos;red&apos;, &apos;blue&apos;, &apos;green&apos; ], sayAge: [Function]}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;原型式继承&lt;/h2&gt;
&lt;p&gt;原型式继承 &lt;code&gt;prototypal inheritance&lt;/code&gt; 是 &lt;code&gt;Douglas Crockford&lt;/code&gt;（《JavaScript语言精粹》作者，&lt;code&gt;JSON&lt;/code&gt; 的创建者）提出的一种继承方式，他的思路是通过一个中间函数来实现继承：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;object&lt;/code&gt; 函数中塔创建了一个临时的函数，将这个函数的 &lt;code&gt;prototype&lt;/code&gt; 指向要继承的对象，然后用这个函数作为构造函数生成一个新的实例对象并返回。如果熟悉 &lt;code&gt;Object.create()&lt;/code&gt; 的话，你会发现这就是 &lt;code&gt;Object.create()&lt;/code&gt; 方法的 &lt;code&gt;polyfill&lt;/code&gt; 实现中的核心代码，它相当于对传入的对象实现了一个浅复制。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let person = {
  name: &apos;Nicholas&apos;,
  friends: [&apos;Shelby&apos;, &apos;Court&apos;, &apos;Van&apos;]
}

let anotherPerson = object(person)
anotherPerson.name = &apos;Greg&apos;
anotherPerson.friends.push(&apos;Rob&apos;)

let yetAnotherPerson = object(person)
yetAnotherPerson.name = &apos;Linda&apos;
yetAnotherPerson.friends.push(&apos;Barbie&apos;)

console.log(person.friends) // &quot;Shelby,Court,Van,Rob,Barbie&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以发现，原型式继承从语法上相当于直接指定某个对象作为新对象的原型，但是本质上只是它内部帮我们把构造函数这一步做了（只不过这个构造函数是个空函数），我需要我们再创建对象。当我们只是想实现对某个对象的属性方法继承的时候，这是一个非常实用并且方便的方法，所以 &lt;code&gt;ES5&lt;/code&gt; 中也提供了 &lt;code&gt;Object.create()&lt;/code&gt; 方法，其核心就是原型式继承，只不过 &lt;code&gt;Object.create()&lt;/code&gt; 方法还能传递第二个参数，为新对象添加独立的属性。&lt;/p&gt;
&lt;p&gt;一般来说，我们在实际编码中想要继承的只是一个对象的属性和方法，而该对象并没有对应的构造函数，这个时候原型式继承就是最合适的方式。&lt;/p&gt;
&lt;p&gt;需要注意的是一个原型创建的多个对象，原型上的引用类型属性还是共享的，一个实例的对引用类型的修改会反映到另一个实例中（他们本质就是同一个对象），不过实际编码中不太会出现这样的情况，一般一个原型进行 &lt;code&gt;create&lt;/code&gt; 只会使用一次。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Object.create()&lt;/code&gt; 方法参考 &lt;code&gt;MDN&lt;/code&gt; 和 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/10/object-create-null/&quot; title=&quot;Object.create(null) 和 {…}&quot;&gt;Object.create(null) 和 {…}&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;寄生式继承&lt;/h2&gt;
&lt;p&gt;寄生式继承 &lt;code&gt;parasitic inheritance&lt;/code&gt; 其实就是对原型式继承的一个包装，因为我们用原型式继承生成对象以后可能还要为这个新对象添加一些新的属性或者方法，寄生式继承就是将这些过程进行一个封装而已，没有什么特别的内容，是原型式继承和工厂模式的一种结合。之所以叫寄生式继承，可以理解为它将继承的过程寄生在了一个函数里面。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function createAnother(original) {
  let clone = Object.create(original) // create a new object by calling a function
  clone.sayHi = function () {
    // augment the object in some way
    console.log(&apos;hi&apos;)
  }
  return clone // return the object
}
let person = {
  name: &apos;Nicholas&apos;,
  friends: [&apos;Shelby&apos;, &apos;Court&apos;, &apos;Van&apos;]
}

let anotherPerson = createAnother(person)
anotherPerson.sayHi() // &quot;hi&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;寄生组合式继承&lt;/h2&gt;
&lt;p&gt;组合式继承已经是一种比较不错的继承实现方式，但是它有一个问题就是父类 &lt;code&gt;SuperType&lt;/code&gt; 的构造函数调用了两次，一次是创建子类 &lt;code&gt;SubType&lt;/code&gt; 的原型 &lt;code&gt;prototype&lt;/code&gt; 时，一次是在子类的构造函数中借用构造时。为了优化这个过程，就有了寄生组合式继承 &lt;code&gt;parasitic combination inheritance&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;寄生组合式继承其实就是把创建子类原型 &lt;code&gt;SubType.prototype&lt;/code&gt; 从 &lt;code&gt;new SuperType&lt;/code&gt; 换成了原型式继承的方式，现在我们一般就是使用 &lt;code&gt;Object.create()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function SuperType(name) {
  this.name = name
  this.colors = [&apos;red&apos;, &apos;blue&apos;, &apos;green&apos;]
}

SuperType.prototype.sayName = function () {
  console.log(this.name)
}

function SubType(name, age) {
  SuperType.call(this, name)

  this.age = age
}

SubType.prototype = Object.create(SuperType.prototype)
SubType.prototype.constructor = SubType

SubType.prototype.sayAge = function () {
  console.log(this.age)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ES5 面向对象总结&lt;/h2&gt;
&lt;p&gt;上面我们介绍了几乎关于 &lt;code&gt;ES5&lt;/code&gt; 的全部面向对象内容，主要是基于 《JavaScript 高级程序设计》第四版来写的。说实话，我个人认为用这样的方式区分继承的模式是非常不合理的，既不符合使用习惯，也很难记忆，特别是对于初学者（如果不是初学者，也不会看这些内容，瞄一下就行了）。之所以我还是按照书上的内容来介绍，是因为你可能在面试的时候被问到，比如如何实现寄生组合式继承。&lt;/p&gt;
&lt;p&gt;我这里说一下我自己的理解，关于 &lt;code&gt;ES5&lt;/code&gt; 的对象创建和继承方式，我们只要记住三个，构造函数，原型链和 &lt;code&gt;Object.create()&lt;/code&gt;（它的本质就是构造函数和原型链的结合）。上面提到的所有创建对象和继承对象的方式都是这三种的方法的排列组合以及包装。比如组合继承，就是构造函数和原型链的组合，原型式继承已经通过 &lt;code&gt;Object.create()&lt;/code&gt; 写入标准。寄生组合式继承则是构造函数，原型链和 &lt;code&gt;Object.create()&lt;/code&gt; 三者的继承。借用构造函数则只是构造函数的一个变体而已，本质没有区别。&lt;/p&gt;
&lt;p&gt;我们在实际的工作中根据我们的需要进行三者的组合，完全没有必要用一个个模式进行束缚。它们三者的功能我进行一个简单的归纳：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造函数：为实例创建独立的属性和方法。&lt;/li&gt;
&lt;li&gt;原型链：为实例创建共享的属性和方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.create()&lt;/code&gt;：本质就是构造函数和原型链的一个封装，当我们不需要构造函数希望直接继承某个对象的属性和方法时，使用这个方法最便捷。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;弄懂原型机制是搞懂 &lt;code&gt;JavaScript&lt;/code&gt; 面向对象的关键。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;ES6 面向对象&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;ES6&lt;/code&gt; 之前，我们用原型链和构造函数来模拟 &lt;code&gt;class&lt;/code&gt; 风格的面向对象。但是显而易见，这样的实现方式有很多问题，也必须进行一些折衷，因为没有语法层面的支持，我们要实现面向对象只能这么做。最重要的是，这样的实现是非常不优雅的，逻辑混乱，代码冗长。特别是如果你使用过其他面向对象语言。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，&lt;code&gt;ES6&lt;/code&gt; 标准正式引入了 &lt;code&gt;class&lt;/code&gt; 关键字让我们能够定义 &lt;strong&gt;类&lt;/strong&gt;。&lt;code&gt;class&lt;/code&gt; 的是 &lt;code&gt;ECMAScript&lt;/code&gt; 引入的一个全新语法，目的就是为了拥有和传统面向对象语言相似的使用方式。虽然它看上去和传统的面向对象编程的语法很相似，但是其实在底层，依然是使用的构造函数和原型，&lt;code&gt;class&lt;/code&gt; 只是一个语法层面的包装，也就是一个语法糖（当然还是有一些不一样的东西），它的绝大部分功能，用 &lt;code&gt;ES5&lt;/code&gt; 完全可以做到，新的 &lt;code&gt;class&lt;/code&gt; 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。&lt;/p&gt;
&lt;h2&gt;class 基础定义&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt; 的定义非常简单，和函数类似，可以使用声明也可以使用表达式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// class declaration
class Person {}

// class expression
const Animal = class {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实 &lt;code&gt;class&lt;/code&gt; 本质就是一个构造函数，看下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Point {}
console.log(typeof Point) // &quot;function&quot;
console.log(Object.prototype.toString.call(Point)) //[object Function]
console.log(Point === Point.prototype.constructor) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是它又和函数有些不同，普通的函数无论是函数声明还是函数表达式，都会在所在执行环境进行提升 &lt;code&gt;hoist&lt;/code&gt;，而类不存在提升行为，必须在声明之后调用。函数声明会在提升时同时用函数体赋值，函数表达式则和普通的变量提升表现一致（只提升不赋值，如果在赋值之前使用该变量，则返回 &lt;code&gt;undefined&lt;/code&gt;。关于变量和函数的提升，参考我的另一篇文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/01/variable-hoist/&quot; title=&quot;var，let，const和变量提升（hoist）&quot;&gt;var，let，const和变量提升（hoist）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt; 和函数的首先一点不同就是类声明没有提升行为，不能在声明前使用，会报错。当然类表达式的提升和变量一样。另一个不同就是类只能作为构造函数进行 &lt;code&gt;new&lt;/code&gt; 调用，直接调用将会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(FunctionExpression) // undefined
var FunctionExpression = function () {}
console.log(FunctionExpression) // function() {}

console.log(FunctionDeclaration) // FunctionDeclaration() {}
function FunctionDeclaration() {}
console.log(FunctionDeclaration) // FunctionDeclaration() {}

console.log(ClassExpression) // undefined
var ClassExpression = class {}
console.log(ClassExpression) // class {}

// console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
class ClassDeclaration {}
console.log(ClassDeclaration) // class ClassDeclaration {}

ClassDeclaration() //TypeError: Class constructor ClassDeclaration cannot be invoked without &apos;new&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然了，普通函数也可以处理成只只能用 &lt;code&gt;new&lt;/code&gt; 进行调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Constructor() {
  if (!(this instanceof Constructor)) {
    throw new TypeError(&apos;Constructor cannot be invoked without &quot;new&quot;&apos;)
  }
}
Constructor() //TypeError: Constructor cannot be invoked without &quot;new&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一个不同就是函数声明和类声明在块级作用域中的表现不同，非严格模式下，函数声明在块级作用域中会被同时提升到当前块的顶部和当前执行环境的顶部，在当前执行环境中使用，其值是 &lt;code&gt;undefined&lt;/code&gt;，在当前块级作用域中则整个函数声明会被提升到顶部。 &lt;code&gt;class&lt;/code&gt; 则遵循块级作用域。具体提升行为参考 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/01/variable-hoist/#block&quot; title=&quot;var，let，const和变量提升（hoist）&quot;&gt;var，let，const和变量提升（hoist）&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  function FunctionDeclaration() {}
  class ClassDeclaration {}
}

console.log(FunctionDeclaration) // FunctionDeclaration() {}
console.log(ClassDeclaration) // ReferenceError: ClassDeclaration is not defined

//函数声明在块级作用域中的奇特行为，类似于函数声明变成了用 `var` 定义的函数表达式
function test() {
  console.log(a) //undefined
  {
    console.log(a) //undefined
    function a() {
      console.log(&apos;a&apos;)
    }
  }
  console.log(a) //[Function a]
  a() //a
}
test()
console.log(a) //ReferenceError: a is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;class 的组成&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt; 中可以包含构造函数，实例继承的方法，&lt;code&gt;getter&lt;/code&gt; 和 &lt;code&gt;setter&lt;/code&gt; 以及静态方法。这些组成部分都不是必须的，即使是一个空的 &lt;code&gt;class&lt;/code&gt; 声明在语法上也是没有错误的。&lt;code&gt;class&lt;/code&gt; 中的所有方法都是在严格模式下执行。和构造函数一样，一般我们将类的首字母大写，用来和实例作区分（比如一个 &lt;code&gt;class Foo{}&lt;/code&gt; 可能有一个实例 &lt;code&gt;foo&lt;/code&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Valid empty class definition
class Foo {}

// Valid class definition with constructor
class Bar {
  constructor() {}
}

// Valid class definition with getter
class Baz {
  get myBaz() {}
}

// Valid class definition with static method
class Qux {
  static myQux() {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和函数表达是一样，类表达式可以匿名也可以命名，如果命名，该标识符的仅在类的作用域内可访问。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let Person = class PersonName {
  identify() {
    console.log(Person.name, PersonName.name)
  }
}

let p = new Person()

p.identify() // PersonName, PersonName

console.log(Person.name) // PersonName
console.log(PersonName) // ReferenceError: PersonName is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;constructor&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;constructor&lt;/code&gt; 关键字在 &lt;code&gt;class&lt;/code&gt; 定义块中使用，用来表示 &lt;code&gt;class&lt;/code&gt; 的构造函数。当使用 &lt;code&gt;new&lt;/code&gt; 进行实例化的时候，就会调用这个 &lt;code&gt;constructor&lt;/code&gt; 方法。一个类必须有 &lt;code&gt;constructor&lt;/code&gt; 方法，如果没有显式定义，一个空的 &lt;code&gt;constructor&lt;/code&gt; 方法会被默认添加。实例化一个 &lt;code&gt;class&lt;/code&gt; 和一个普通的构造函数没有区别，只是把构造函数中的语句挪到 &lt;code&gt;class&lt;/code&gt; 中的 &lt;code&gt;constructor&lt;/code&gt; 方法中而已，&lt;code&gt;new&lt;/code&gt; 的过程还是一样。如果你不了解 &lt;code&gt;new&lt;/code&gt; 的行为，请看我的另一篇文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/29/new-operator/&quot; title=&quot;JavaScript中new操作符的解析和实现&quot;&gt;JavaScript中new操作符的解析和实现&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;类的 &lt;code&gt;constructor&lt;/code&gt; 和普通构造函数不同的是，在实例化之后，它可以作为实例的一个属性访问，我们可以用它来创建新的对象。实际上这是通过类的原型 &lt;code&gt;prototype&lt;/code&gt; 进行访问的，因为类的 &lt;code&gt;prototype&lt;/code&gt; 上有 &lt;code&gt;constructor&lt;/code&gt; 指向类。这与 &lt;code&gt;ES5&lt;/code&gt; 的行为是一致的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Person {}

// Create a new instance using the class
let p1 = new Person()

p1.constructor()
// TypeError: Class constructor Person cannot be invoked without &apos;new&apos;

// Create a new instance using the reference to the class constructor
let p2 = new p1.constructor()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;constructor&lt;/code&gt; 中用 &lt;code&gt;this&lt;/code&gt; 定义的实例属性还有另一种写法，就是写到 &lt;code&gt;class&lt;/code&gt; 的顶部。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class foo {
  bar = &apos;hello&apos;
  baz = &apos;world&apos;
}

let a = new foo()
console.log(a) //foo { bar: &apos;hello&apos;, baz: &apos;world&apos; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;理解 class 是一个特殊函数&lt;/h3&gt;
&lt;p&gt;我上面已经说过 &lt;code&gt;class&lt;/code&gt; 本质是一个构造函数，那么它自然也有 &lt;code&gt;prototype&lt;/code&gt; 属性指向一个对象，该 &lt;code&gt;prototype&lt;/code&gt; 对象的 &lt;code&gt;constructor&lt;/code&gt; 属性指向 &lt;code&gt;class&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Person {}

console.log(Person.prototype) // { constructor: f() }
console.log(Person === Person.prototype.constructor) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和普通的构造函数一样，我们可以用 &lt;code&gt;instanceof&lt;/code&gt; 操作符来检测对象是否是某个 &lt;code&gt;class&lt;/code&gt; 的实例。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Person {}

let p = new Person()

console.log(p instanceof Person) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt; 内部的 &lt;code&gt;constructor&lt;/code&gt; 方法可以被当做实例的一个方法调用，但是如果你用这种方式创建对象，这个对象的构造函数是 &lt;code&gt;class&lt;/code&gt; 内部的那个 &lt;code&gt;constructor&lt;/code&gt; 方法，而不是 &lt;code&gt;class&lt;/code&gt;，&lt;code&gt;class&lt;/code&gt; 的 &lt;code&gt;prototype&lt;/code&gt; 不会在这个实例的原型链上。也就是如果你使用 &lt;code&gt;class&lt;/code&gt;，那么 &lt;code&gt;class&lt;/code&gt; 才是构造函数，而不是内部的 &lt;code&gt;constructor&lt;/code&gt; 方法（虽然 &lt;code&gt;new&lt;/code&gt; 的时候确实是执行的 &lt;code&gt;constructor&lt;/code&gt; 方法内的内容），这一点非常重要。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Person {}

let p1 = new Person()

console.log(p1.constructor === Person) // true
console.log(p1 instanceof Person) // true
console.log(p1 instanceof Person.constructor) // false

let p2 = new Person.constructor()

console.log(p2.constructor === Person) // false
console.log(p2 instanceof Person) // false
console.log(p2 instanceof Person.constructor) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt; 和对象一样是 &lt;code&gt;JavaScript&lt;/code&gt; 中的一等公民，意味着它可以作为对象的属性和函数的参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Classes may be defined anywhere a function would, such as inside an array:
let classList = [
  class {
    constructor(id) {
      this.id_ = id
      console.log(&apos;instance ${this.id_}&apos;)
    }
  }
]

function createInstance(classDefinition, id) {
  return new classDefinition(id)
}

let foo = createInstance(classList[0], 3141) // instance 3141
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和函数表达式一样我们可以对类表达式进行立即调用来生成对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = new (function (name) {
  this.name = name
})(&apos;clloz&apos;)
console.log(a) //{ name: &apos;clloz&apos; }

// Because it is a class expression, the class name is optional
let p = new (class Foo {
  constructor(x) {
    console.log(x)
  }
})(&apos;bar&apos;) // bar

console.log(p) // Foo {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类是一个特殊的函数还体现在它也有 &lt;code&gt;name&lt;/code&gt; 属性，和函数一样，&lt;code&gt;name&lt;/code&gt; 属性返回的是紧跟在 &lt;code&gt;class&lt;/code&gt; 后面的标识符的名称。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = class B {}
console.log(a.name) //B
new B() //ReferenceError: B is not defined

let fn = function func() {}
console.log(fn.name)
func() //func is no defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;类的 prototype&lt;/h3&gt;
&lt;p&gt;为了能让实例间共享属性和方法，&lt;code&gt;class&lt;/code&gt; 自然也提供了在 &lt;code&gt;prototype&lt;/code&gt; 上添加方法和属性的功能。我们可以在 &lt;code&gt;class&lt;/code&gt; 中定义方法和 &lt;code&gt;getter&lt;/code&gt;，&lt;code&gt;setter&lt;/code&gt; 属性访问器，注意，不能定义对象或者原始数据类型。也就是 &lt;code&gt;class&lt;/code&gt; 中定义的方法除了 &lt;code&gt;constructor&lt;/code&gt;，其他非静态方法都相当于定义在了 &lt;code&gt;prototype&lt;/code&gt; 上(实际上 &lt;code&gt;constructor&lt;/code&gt; 也能从 &lt;code&gt;prototype&lt;/code&gt; 上访问到)，并且所有方法都可以使用 &lt;code&gt;String&lt;/code&gt;，&lt;code&gt;Symbol&lt;/code&gt; 和计算属性作为方法名。虽然不能直接定义原始数据类型或者对象作为属性，但是我们可以用 &lt;code&gt;getter&lt;/code&gt; 和 &lt;code&gt;setter&lt;/code&gt; 进行属性设定。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;**注意：**和 &lt;code&gt;ES5&lt;/code&gt; 不同的是，类内部定义的所有方法，默认都是不可枚举的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Person {
    constructor() {
        // Everything added to &apos;this&apos; will exist on each individual instance
        this.locate = () =&gt; console.log(&apos;instance&apos;);
    }

    // Everything defined in the class body is defined on the class prototype object
    locate() {
        console.log(&apos;prototype&apos;);
    }
}

let p = new Person();

p.locate(); // instance
Person.prototype.locate(); // prototype

//不能定义原始数据类型和对象
class Person {
    // name: &apos;Jake&apos;, //SyntaxError: Unexpected identifier
    test: {}; //SyntaxError: Unexpected identifier
    test: {};
}

//方法名可以使用 String Symbol 和 计算属性
const symbolKey = Symbol(&apos;symbolKey&apos;);
class Person {
 stringKey() {
  console.log(&apos;invoked stringKey&apos;);
 }
 [symbolKey]() {
  console.log(&apos;invoked symbolKey&apos;);
 }
 [&apos;computed&apos; + &apos;Key&apos;]() {
  console.log(&apos;invoked computedKey&apos;);
 }
}
let p = new Person();
p.stringKey();   // invoked stringKey
p[symbolKey]();  // invoked symbolKey
p.computedKey(); // invoked computedKey

//访问器属性
class Person {
    set name(newName) {
        this.name_ = newName;
    }
    get name() {
        return this.name_;
    }
}
let p = new Person();
p.name = &apos;Jake&apos;;
console.log(p.name); // Jake
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，&lt;code&gt;class&lt;/code&gt; 中的方法间不能有逗号，否则会报错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们也可以在 &lt;code&gt;class&lt;/code&gt; 之外为 &lt;code&gt;class&lt;/code&gt; 或者 &lt;code&gt;prototype&lt;/code&gt; 添加属性，但这是不推荐的，实例的属性应该统一由 &lt;code&gt;this&lt;/code&gt; 引用，而 &lt;code&gt;prototype&lt;/code&gt; 上的共享方法也统一在 &lt;code&gt;class&lt;/code&gt; 中定义时比较好的实践。这是一种反模式，在类外进行变量或者方法的声明很容易被忽略，也不易维护。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Person {
  sayName() {
    console.log(&apos;${Person.greeting} ${this.name}&apos;)
  }
}

// Define data member on class
Person.greeting = &apos;My name is&apos;

// Define data member on prototype
Person.prototype.name = &apos;Jake&apos;

let p = new Person()
p.sayName() // My name is Jake
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类中的方法也可以使用简写，和对象的方法一样，简写的方法不能作为构造函数调用，简写方法没有内部 &lt;code&gt;[[construct]]&lt;/code&gt; 方法。当然我们一般不需要对静态方法或者原型上的方法使用 &lt;code&gt;new&lt;/code&gt;，所以直接使用简写即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Parent {
  constructor(a, b) {
    this.a = a
    this.b = b
  }
  pFn1 = function () {} //能够作为构造函数
  pFn2() {} //TypeError: p.pFn2 is not a constructor
}

let p = new Parent()
new p.pFn1()
new p.pFn2()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;静态方法和访问器属性&lt;/h3&gt;
&lt;p&gt;静态方法是所有面向对象都提供的一种设施。为什么需要这种设施，是有些方法并不需要创建实例使用，比如我们熟悉的 &lt;code&gt;Math&lt;/code&gt; 对象的方法，我们希望能够直接使用，于是就有了静态方法这种机制，它们不属于实例，而是属于类，我们不需要实例化就能直接使用。静态方法与非静态方法的本质区别：静态方法在程序初始化后会一直贮存在内存中，不会被垃圾回收器回收，非静态方法只在该类初始化后贮存在内存中，当该类调用完毕后会被垃圾回收器收集释放。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;class&lt;/code&gt; 中定义静态方法和其他面向对象语言类似，使用 &lt;code&gt;static&lt;/code&gt; 关键字，和 &lt;code&gt;prototype&lt;/code&gt; 上的方法一样，他们只会创建一次。看看下面的例子可以看到构造函数中的方法，&lt;code&gt;prototype&lt;/code&gt; 上的方法和静态方法的区别。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Person {
  constructor() {
    // Everything added to &apos;this&apos; will exist on each individual instance
    this.locate = () =&gt; console.log(&apos;instance&apos;, this)
  }

  // Defined on the class prototype object
  locate() {
    console.log(&apos;prototype&apos;, this)
  }

  // Defined on the class
  static locate() {
    console.log(&apos;class&apos;, this)
  }
}

let p = new Person()

p.locate() // instance, Person {}
Person.prototype.locate() // prototype, {constructor: … }
Person.locate() // class, class Person {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;静态属性&lt;/h3&gt;
&lt;p&gt;静态属性指的是 &lt;code&gt;Class&lt;/code&gt; 本身的属性，即 &lt;code&gt;Class.propName&lt;/code&gt;，而不是定义在实例对象（&lt;code&gt;this&lt;/code&gt;）上的属性。使用方法和静态方法一样，就是用 &lt;code&gt;static&lt;/code&gt; 关键字。它不需要像实例属性一样写在顶部，可以在类里面的任何位置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class foo {
  static bar = &apos;hello&apos;
  baz = &apos;world&apos;
  test() {
    console.log(&apos;haha&apos;)
  }
  static m = &apos;m&apos;
}

let a = new foo()
console.log(a) //foo { bar: &apos;hello&apos;, baz: &apos;world&apos; }
console.log(foo.bar, foo.m)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;私有方法和属性&lt;/h3&gt;
&lt;p&gt;私有方法和私有属性，是只能在类的内部访问的方法和属性，外部不能访问。这是常见需求，有利于代码的封装，但 &lt;code&gt;ES6&lt;/code&gt; 目前还不支持，只能通过变通方法模拟实现。&lt;/p&gt;
&lt;p&gt;一种实现方式是用命名区分，在私有方法和属性前面加上下划线，当然这只是一个约定，在类的外部依然可以调用对应的方法和属性。&lt;/p&gt;
&lt;p&gt;另一种是将方法移出类外，然后在类内部用 &lt;code&gt;call&lt;/code&gt; 或者 &lt;code&gt;apply&lt;/code&gt; 进行调用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Widget {
  foo(baz) {
    bar.call(this, baz)
  }
}

function bar(baz) {
  return (this.snaf = baz)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后还有一种就是利用 &lt;code&gt;Symbol&lt;/code&gt; 的唯一性来做变量名，降低属性或者方法被外部访问的可能，但是要注意 &lt;code&gt;Reflect.ownKeys()&lt;/code&gt; 方法或者 &lt;code&gt;Object.getOwnPropertySymbols&lt;/code&gt; 方法都可以遍历到 &lt;code&gt;Symbol&lt;/code&gt; 属性。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ES2020&lt;/code&gt; 的草案已经正式提出了私有属性和方法的支持，使用方式就是在属性或者方法之前加上 &lt;code&gt;#&lt;/code&gt;，当然目前支持的只有高版本的 &lt;code&gt;edge&lt;/code&gt;，&lt;code&gt;chrome&lt;/code&gt; 和 &lt;code&gt;opera&lt;/code&gt; 浏览器。所以在得到普遍的支持之前，我们还是要使用不安全的模拟私有属性。&lt;/p&gt;
&lt;h3&gt;new.target&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;new.target&lt;/code&gt; 属性允许你检测函数或构造方法是否是通过 &lt;code&gt;new&lt;/code&gt; 运算符被调用的。在通过 &lt;code&gt;new&lt;/code&gt; 运算符被初始化的函数或构造方法中，&lt;code&gt;new.target&lt;/code&gt; 返回一个指向构造方法或函数的引用。在普通的函数调用中，&lt;code&gt;new.target&lt;/code&gt; 的值是 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;new.target&lt;/code&gt; 语法由一个关键字 &lt;code&gt;new&lt;/code&gt;，一个点，和一个属性名 &lt;code&gt;target&lt;/code&gt; 组成。通常 &lt;code&gt;new.&lt;/code&gt; 的作用是提供属性访问的上下文，但这里 &lt;code&gt;new.&lt;/code&gt; 其实不是一个真正的对象。不过在构造方法调用中，&lt;code&gt;new.target&lt;/code&gt; 指向被 &lt;code&gt;new&lt;/code&gt; 调用的构造函数，所以 &lt;code&gt;new.&lt;/code&gt; 成为了一个虚拟上下文。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;arrow functions&lt;/code&gt; 中，&lt;code&gt;new.target&lt;/code&gt; 指向最近的外层函数的 &lt;code&gt;new.target&lt;/code&gt;，箭头函数没有自己的 &lt;code&gt;new.target&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;new.target&lt;/code&gt; 可以用在任何函数中，函数不是通过 &lt;code&gt;new&lt;/code&gt; 或者 &lt;code&gt;Reflect.construct()&lt;/code&gt; 调用的则返回 &lt;code&gt;undefined&lt;/code&gt;。如果是通过 &lt;code&gt;new&lt;/code&gt; 调用的则返回该函数，注意函数表达式的如果不是匿名函数则返回的函数名称是函数表达式定义的函数名。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Target() {
  console.log(new.target)
}
Target() //undefined
new Target() //[Function: Target]

//函数表达式
let Target = function targetName() {
  console.log(new.target)
}
Target() //undefined
new Target() //[Function: targetName]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在类的 &lt;code&gt;constructor&lt;/code&gt; 中使用则返回该类（类只能通过 &lt;code&gt;new&lt;/code&gt; 调用），子类继承父类的时候，&lt;code&gt;super()&lt;/code&gt; 会执行父类的 &lt;code&gt;constructor&lt;/code&gt;，如果内部有 &lt;code&gt;new.target&lt;/code&gt; 返回的是子类而不是父类。如果是在类的其他函数中使用（如果不是通过 &lt;code&gt;new&lt;/code&gt; 调用的话，一般不是）则和普通函数一样返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Rectangle {
  constructor() {
    console.log(new.target === Rectangle)
  }
}
class Square extends Rectangle {}
var obj = new Square() // 输出 false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以利用 &lt;code&gt;new.target&lt;/code&gt; 让类必须继承后才能创建实例。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error(&apos;本类不能实例化&apos;)
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super()
  }
}

var x = new Shape() // Error: 本类不能实例化
var y = new Rectangle(3, 4) // 正确
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ES6 继承&lt;/h2&gt;
&lt;p&gt;上面我们介绍了 &lt;code&gt;ES5&lt;/code&gt; 的继承，基本都是需要些很长的原型和构造函数逻辑，并不是很方便。在 &lt;code&gt;ES6&lt;/code&gt; 有了 &lt;code&gt;class&lt;/code&gt; 之后，也原生提供了继承的语法，当然这个继承与法的背后还是原型继承。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 的继承是使用 &lt;code&gt;extends&lt;/code&gt; 关键词，我们可以用 &lt;code&gt;extends&lt;/code&gt; 关键词继承任何有 &lt;code&gt;[[constructor]]&lt;/code&gt;（这是一个内部属性，表示可以作为一个构造函数） 属性和 &lt;code&gt;prototype&lt;/code&gt; 的对象，这种形式保证了向后兼容，让 &lt;code&gt;ES5&lt;/code&gt; 的构造函数也能使用这种语法继承。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Vehicle {}

// Inherit from class
class Bus extends Vehicle {}

let b = new Bus()
console.log(b instanceof Bus) // true
console.log(b instanceof Vehicle) // true

function Person() {}

// Inherit from function constructor
class Engineer extends Person {}

let e = new Engineer()
console.log(e instanceof Engineer) // true
console.log(e instanceof Person) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;派生类可以继承父类的所有方法，包括类本身的静态方法和 &lt;code&gt;prototype&lt;/code&gt; 上的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Vehicle {
  identifyPrototype(id) {
    console.log(id, this)
  }

  static identifyClass(id) {
    console.log(id, this)
  }
}

class Bus extends Vehicle {}

let v = new Vehicle()
let b = new Bus()

b.identifyPrototype(&apos;bus&apos;) // bus, Bus {}
v.identifyPrototype(&apos;vehicle&apos;) // vehicle, Vehicle {}

Bus.identifyClass(&apos;bus&apos;) // bus, class Bus {}
Vehicle.identifyClass(&apos;vehicle&apos;) // vehicle, class Vehicle {}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;extends&lt;/code&gt; 也可以用在类表达式中，&lt;code&gt;let Bar = class extends Foo {}&lt;/code&gt; 是一个合法的语句。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实我们仔细分析一下 &lt;code&gt;ES6&lt;/code&gt; 的继承，和寄生组合式继承只有一些细节不同，只是对寄生组合式继承的一种包装，把原来我们要写的逻辑帮我们封装好了而已。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Parent {
  constructor(a, b) {
    this.a = a
    this.b = b
  }
  pFn() {}
}
class Child extends Parent {
  constructor(a, b) {
    super(a, b)
  }
  cFn() {}
}

let c = new Child(1, 2)
console.log(c) //Child { a: 1, b: 2 }
console.log(Reflect.ownKeys(c.__proto__)) //[ &apos;constructor&apos;, &apos;cFn&apos; ]
console.log(Reflect.ownKeys(c.__proto__.__proto__)) //[ &apos;constructor&apos;, &apos;pFn&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到，子类的构造函数中调用 &lt;code&gt;super()&lt;/code&gt; 就有点借用构造函数的感觉，只不过这里细节有所不同。在 &lt;code&gt;ES5&lt;/code&gt; 中我们是直接在调用子类构造函数的时候创建一个新的对象作为 &lt;code&gt;this&lt;/code&gt;，而 &lt;code&gt;ES6&lt;/code&gt; 中是在 &lt;code&gt;super()&lt;/code&gt; 中创建 &lt;code&gt;this&lt;/code&gt;，然后用这个 &lt;code&gt;this&lt;/code&gt; 执行 &lt;code&gt;super()&lt;/code&gt;，所以我们在子类中只有执行完了 &lt;code&gt;super()&lt;/code&gt; 才有 &lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;prototype&lt;/code&gt; 的机制和 &lt;code&gt;ES5&lt;/code&gt; 的原型式继承没什么不同，看下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Parent {
  constructor(a, b) {
    this.a = a
    this.b = b
  }
  pFn() {}
}
class Child extends Parent {
  constructor(a, b) {
    super(a, b)
  }
  cFn() {}
}

let c = new Child(1, 2)
console.log(c) //Child { a: 1, b: 2 }
console.log(Reflect.ownKeys(c.__proto__)) //[ &apos;constructor&apos;, &apos;cFn&apos; ]
console.log(Reflect.ownKeys(c.__proto__.__proto__)) //[ &apos;constructor&apos;, &apos;pFn&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到子类实例的 &lt;code&gt;[[prototype]]&lt;/code&gt; 有 &lt;code&gt;constructor&lt;/code&gt; 和 &lt;code&gt;cFn&lt;/code&gt; 两个属性，也就是 &lt;code&gt;Child.prototype&lt;/code&gt;。而我们看到原型的原型，有两个属性 &lt;code&gt;constructor&lt;/code&gt; 和 &lt;code&gt;pFn&lt;/code&gt;，也就是 &lt;code&gt;Parent.prototype&lt;/code&gt;，这就跟我们上面原型式继承 &lt;code&gt;Object.create(Parent.prototype)&lt;/code&gt; 效果相同。所以我们通过访问子类实例的原型的原型是可以修改父类实例的原型的。&lt;/p&gt;
&lt;p&gt;上面的例子是说的 &lt;code&gt;class&lt;/code&gt; 的实例的原型链，其实 &lt;code&gt;class&lt;/code&gt; 本身的 &lt;code&gt;[[prototype]]&lt;/code&gt; 也和 &lt;code&gt;ES5&lt;/code&gt; 中的构造函数有些不同。在 &lt;code&gt;ES5&lt;/code&gt; 中，每个函数都是 &lt;code&gt;Function&lt;/code&gt; 的实例，所以自然其 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向的是 &lt;code&gt;Function.prototype&lt;/code&gt;。在 &lt;code&gt;ES6&lt;/code&gt; 中，如果 &lt;code&gt;class&lt;/code&gt; 没有继承其他类，那么他的 &lt;code&gt;[[prototype]]&lt;/code&gt; 也是指向 &lt;code&gt;Function.prototype&lt;/code&gt;，但是如果 &lt;code&gt;class&lt;/code&gt; 是一个子类，那么它的 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向的是其父类。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Parent {
  constructor(a, b) {
    this.a = a
    this.b = b
  }
  pFn() {}
}
console.log(Parent.__proto__ === Function.prototype) //true
console.log(Parent.prototype) //{constructor: ƒ, pFn: ƒ}

class Child extends Parent {
  constructor(a, b) {
    super(a, b)
  }
  cFn() {}
}
console.log(Child.__proto__) //class Parent {}
console.log(Child.prototype) //Parent {constructor: ƒ, cFn: ƒ}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合类和实例的继承关系，我们可以将 &lt;code&gt;class&lt;/code&gt; 的继承进行如下理解：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class A {}

class B {}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype)

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A)

const b = new B()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以被继承的基类不一定是一个类，只要是一个有 &lt;code&gt;prototype&lt;/code&gt; 的函数就可以被继承，由于函数都有 &lt;code&gt;prototype&lt;/code&gt; 属性（除了 &lt;code&gt;Function.prototype&lt;/code&gt;函数），因此任意函数都可以作为基类被继承。&lt;/p&gt;
&lt;h3&gt;super&lt;/h3&gt;
&lt;p&gt;想要在派生类中访问父类，&lt;code&gt;ES6&lt;/code&gt; 提供了 &lt;code&gt;super&lt;/code&gt; 关键字。&lt;code&gt;super&lt;/code&gt; 关键字有多种不同的用法，还是比较复杂的，总结一下如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只有派生类中能使用 &lt;code&gt;super&lt;/code&gt;（即使用了 &lt;code&gt;extends&lt;/code&gt; 的类或者设置了 &lt;code&gt;[[prototype]]&lt;/code&gt; 的对象的方法中）。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;super&lt;/code&gt; 作为方法调用时，只能是在派生类的构造函数中。&lt;/li&gt;
&lt;li&gt;在派生类的构造函数中，如果没有定义 &lt;code&gt;constructor&lt;/code&gt; 方法，会有一个默认的 &lt;code&gt;constructor&lt;/code&gt; 方法被创建，内部会执行 &lt;code&gt;super()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;派生类的构造函数中的 &lt;code&gt;super()&lt;/code&gt; 会创建子类构造函数的 &lt;code&gt;this&lt;/code&gt;，然后用这个 &lt;code&gt;this&lt;/code&gt; 执行 &lt;code&gt;super()&lt;/code&gt;。所以在执行 &lt;code&gt;super()&lt;/code&gt; 之前派生类的构造函数中没有 &lt;code&gt;this&lt;/code&gt;，所有用到 &lt;code&gt;this&lt;/code&gt; 的语句都要写在 &lt;code&gt;super()&lt;/code&gt; 之后。&lt;/li&gt;
&lt;li&gt;派生类如果显示的定义了一个 &lt;code&gt;constructor&lt;/code&gt; 则必须在其中调用 &lt;code&gt;super&lt;/code&gt; 或者返回一个对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;super&lt;/code&gt; 还可以作为对象，在普通的方法中（包括 &lt;code&gt;constructor&lt;/code&gt;），这个对象指向父类的 &lt;code&gt;prototype&lt;/code&gt;，在静态方法中指向父类。注意，父类 &lt;code&gt;constructor&lt;/code&gt; 中定义的实例的属性和方法是无法访问的，只能访问在 &lt;code&gt;constructor&lt;/code&gt; 之外定义的静态方法或者 &lt;code&gt;prototype&lt;/code&gt; 上的方法和访问器属性。&lt;/li&gt;
&lt;li&gt;在派生类的普通方法中使用 &lt;code&gt;super&lt;/code&gt; 对象中的方法时，其中的 &lt;code&gt;this&lt;/code&gt; 指向子类实例。&lt;/li&gt;
&lt;li&gt;在派生类的静态方法中使用 &lt;code&gt;super&lt;/code&gt; 对象中的方法时，方法内部的 &lt;code&gt;this&lt;/code&gt; 指向当前的派生类而不是父类。&lt;/li&gt;
&lt;li&gt;在派生类的普通方法或静态方法中使用 &lt;code&gt;super&lt;/code&gt; 时，必须调用 &lt;code&gt;super&lt;/code&gt; 的某个属性或者方法，不能直接使用 &lt;code&gt;super&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;你不能使用 &lt;code&gt;delete&lt;/code&gt; 操作符 加 &lt;code&gt;super.prop&lt;/code&gt; 或者 &lt;code&gt;super[expr]&lt;/code&gt; 去删除父类的属性，这样做会抛出 &lt;code&gt;ReferenceError&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;super&lt;/code&gt; 作为对象使用时我们只能对其进行读操作，当对 &lt;code&gt;super&lt;/code&gt; 进行写操作时，所有的操作都发生在 &lt;code&gt;this&lt;/code&gt; 上。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class A {
  fn() {
    console.log(&apos;A&apos;)
  }
}
A.prototype.x = 10

class B extends A {
  constructor() {
    super()
    //super 在赋值的时候相当于 this，在读取的时候则是指向父类的 prototype
    this.x = 2
    console.log(this.x)
    console.log(super.x)
    super.x = 3
    console.log(this.x)
    console.log(super.x)

    //父类 prototype 上的方法和属性的行为一样，读操作时指向是父类的 prototype，写操作则是写在 this 上
    this.fn() //A
    super.fn() //A
    console.log(this.hasOwnProperty(&apos;fn&apos;)) //false
    super.fn = function () {
      console.log(&apos;B&apos;)
    }
    this.fn() //B
    super.fn() //A
    console.log(this.hasOwnProperty(&apos;fn&apos;)) //true
  }
}

let b = new B()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;super&lt;/code&gt; 的规则非常复杂，网络上的相关内容也不多，这里的主要规则来源于 《JavaScript 高级程序设计 第四版》和 《ES6 标准入门》。当然你也可以看 &lt;a href=&quot;https://tc39.es/ecma262/#sec-super-keyword&quot; title=&quot;标准&quot;&gt;标准&lt;/a&gt;，看起来比较吃力，而且我没有找到 &lt;code&gt;static&lt;/code&gt; 相关的规定。不过记住这些总结的规则基本上就没有问题了。&lt;/p&gt;
&lt;h3&gt;原生构造函数的继承&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;ES5&lt;/code&gt; 中我们无法继承原生构造函数，因为原生构造函数无法像普通函数一样用借用构造函数的方法传入 &lt;code&gt;this&lt;/code&gt; 执行，原生构造函数会忽略 &lt;code&gt;apply&lt;/code&gt; 或者 &lt;code&gt;call&lt;/code&gt; 设置的 &lt;code&gt;this&lt;/code&gt;。比如下面的 &lt;code&gt;ES5&lt;/code&gt; 尝试继承 &lt;code&gt;Array&lt;/code&gt; 的代码，它的表现和数组并不相同。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function MyArray() {
  Array.apply(this, arguments)
}

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
})

var colors = new MyArray()
colors[0] = &apos;red&apos;
colors.length // 0

colors.length = 0
colors[0] // &quot;red&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ES5&lt;/code&gt; 是先新建子类的实例对象 &lt;code&gt;this&lt;/code&gt;，再将父类的属性添加到子类上，由于父类的内部属性无法获取，导致无法继承原生的构造函数。比如，&lt;code&gt;Array&lt;/code&gt; 构造函数有一个内部属性 &lt;code&gt;[[DefineOwnProperty]]&lt;/code&gt;，用来定义新属性时，更新 &lt;code&gt;length&lt;/code&gt; 属性，这个内部属性无法在子类获取，导致子类的 &lt;code&gt;length&lt;/code&gt; 属性行为不正常。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 的 &lt;code&gt;class&lt;/code&gt; 让我们能够实现对原生构造函数的继承，主要原因就是 &lt;code&gt;class&lt;/code&gt; 的 &lt;code&gt;super()&lt;/code&gt; 让子类能够用自己的的 &lt;code&gt;this&lt;/code&gt; 执行构造函数，也支持原生构造函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class MyArray extends Array {
  constructor(...args) {
    super(...args)
  }
}

var arr = new MyArray()
arr[0] = 12
arr.length // 1

arr.length = 0
arr[0] // undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了这个功能之后，&lt;code&gt;JavaScript&lt;/code&gt; 变得更灵活，我们可以在原生数据结构的基础上定义自己的数据结构。比如下面实现了一个带版本功能的数组：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class VersionedArray extends Array {
  constructor() {
    super()
    this.history = [[]]
  }
  commit() {
    this.history.push(this.slice())
  }
  revert() {
    this.splice(0, this.length, ...this.history[this.history.length - 1])
  }
}

var x = new VersionedArray()

x.push(1)
x.push(2)
x // [1, 2]
x.history // [[]]

x.commit()
x.history // [[], [1, 2]]

x.push(3)
x // [1, 2, 3]
x.history // [[], [1, 2]]

x.revert()
x // [1, 2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再来看一自定义 &lt;code&gt;Error&lt;/code&gt; 的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class ExtendableError extends Error {
  constructor(message) {
    super()
    this.message = message
    this.stack = new Error().stack
    this.name = this.constructor.name
  }
}

class MyError extends ExtendableError {
  constructor(m) {
    super(m)
  }
}

var myerror = new MyError(&apos;ll&apos;)
console.log(myerror.message) // &quot;ll&quot;
console.log(myerror instanceof Error) // true
console.log(myerror.name) // &quot;MyError&quot;
console.log(myerror.stack)
// Error
//     at MyError.ExtendableError
//     ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是 &lt;code&gt;Object&lt;/code&gt; 构造函数和其他内置构造函数的行为不同：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class NewObj extends Object {
  constructor() {
    super(...arguments)
  }
}
var o = new NewObj({ attr: true })
o.attr === true // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，&lt;code&gt;NewObj&lt;/code&gt; 继承了 &lt;code&gt;Object&lt;/code&gt;，但是无法通过 &lt;code&gt;super&lt;/code&gt; 方法向父类 &lt;code&gt;Object&lt;/code&gt; 传参。这是因为 &lt;code&gt;ES6&lt;/code&gt; 改变了 &lt;code&gt;Object&lt;/code&gt; 构造函数的行为，一旦发现 &lt;code&gt;Object&lt;/code&gt; 方法不是通过 &lt;code&gt;new Object()&lt;/code&gt; 这种形式调用，&lt;code&gt;ES6&lt;/code&gt; 规定 &lt;code&gt;Object&lt;/code&gt; 构造函数会忽略参数。&lt;/p&gt;
&lt;p&gt;有些内置类型的构造函数（比如 &lt;code&gt;Array&lt;/code&gt;）的方法会返回一个新的实例（比如 &lt;code&gt;Array&lt;/code&gt; 的 &lt;code&gt;map&lt;/code&gt; 和 &lt;code&gt;filter&lt;/code&gt; 等方法），如果我们用的是 &lt;code&gt;Array&lt;/code&gt; 的派生类创建的实例再调用 &lt;code&gt;map&lt;/code&gt; 等方法返回的新实例其原型是派生类而不是 &lt;code&gt;Array&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class SuperArray extends Array {}

let a1 = new SuperArray(1, 2, 3, 4, 5)
let a2 = a1.filter((x) =&gt; !!(x % 2))

console.log(a1) // [1, 2, 3, 4, 5]
console.log(a2) // [1, 3, 5]
console.log(a1 instanceof SuperArray) // true
console.log(a2 instanceof SuperArray) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你不希望方法调用返回的对象是派生类的实例，希望是 &lt;code&gt;Array&lt;/code&gt; 的实例，你需要重写内置的 &lt;code&gt;Symbol.species&lt;/code&gt; 访问器，这个内置 &lt;code&gt;Symbol&lt;/code&gt; 就是用来确定返回实例的构造函数的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class SuperArray extends Array {
  get [Symbol.species]() {
    return Array
  }
}

let a1 = new SuperArray(1, 2, 3, 4, 5)
let a2 = a1.filter((x) =&gt; !!(x % 2))

console.log(a1) // [1, 2, 3, 4, 5]
console.log(a2) // [1, 3, 5]
console.log(a1 instanceof SuperArray) // true
console.log(a2 instanceof SuperArray) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文主要是讲了 &lt;code&gt;ES5&lt;/code&gt; 和 &lt;code&gt;ES6&lt;/code&gt; 的对象和继承，以及其中的一些细节，面向对象的学习需要在实践中不断的探索和练习。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;《ES6 标准入门》 —— 阮一峰&lt;/li&gt;
&lt;li&gt;《JavaScript 高级程序司机 4th Edition》&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>深入扩展运算符</title><link>https://clloz.com/blog/deep-into-spread-syntax</link><guid isPermaLink="true">https://clloz.com/blog/deep-into-spread-syntax</guid><pubDate>Thu, 05 Nov 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;扩展运算符 (&lt;code&gt;spread syntax&lt;/code&gt;) 是 &lt;code&gt;ES6&lt;/code&gt; 提供的一种非常便捷的新语法，给我们操作数组和对象带来了非常大的便利，我在很多文章中也提到了这个语法。但是其实扩展运算符的用法还是比较多比较杂的，我用一篇文章来做一下总结，梳理一下扩展运算的语法和使用细节。&lt;/p&gt;
&lt;h2&gt;基础用法&lt;/h2&gt;
&lt;p&gt;扩展运算符 &lt;code&gt;spread syntax&lt;/code&gt; 又叫展开语法，写法是 &lt;code&gt;...&lt;/code&gt;，顾名思义，其实是用来展开字符串，数组和对象的一种语法，可以在函数调用/数组构造时, 将数组表达式或者 &lt;code&gt;string&lt;/code&gt; 在语法层面展开；还可以在构造字面量对象时, 将对象表达式按 &lt;code&gt;key-value&lt;/code&gt; 的方式展开。常用的语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//函数调用：
myFunction(...iterableObj)

//字面量数组构造或字符串：
;[...iterableObj, &apos;4&apos;, ...&apos;hello&apos;, 6]

// 构造字面量对象时,进行克隆或者属性拷贝（ECMAScript 2018规范新增特性）：
let objClone = { ...obj }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在函数调用时使用扩展运算符相当于使用 &lt;code&gt;Function.prototype.apply&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function myFunction(x, y, z) {}
var args = [0, 1, 2]
myFunction(...args)

//相当于
function myFunction(x, y, z) {}
var args = [0, 1, 2]
myFunction.apply(null, args)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和 &lt;code&gt;apply&lt;/code&gt; 不同的是，我们不仅可以将全部参数放到一个数组中，还可以只将其中几个参数用扩展运算符展开，并且可以再一次调用中多次使用扩展运算符。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function myFunction(a, b, c, d, e) {
  console.log(a, b, c, d, e) //-1 0 1 2 3
  console.log(arguments) //[Arguments] { &apos;0&apos;: -1, &apos;1&apos;: 0, &apos;2&apos;: 1, &apos;3&apos;: 2, &apos;4&apos;: 3 }
}
var args = [0, 1]
myFunction(-1, ...args, 2, ...[3])
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;使用 &lt;code&gt;new&lt;/code&gt; 关键字来调用构造函数时，不能直接使用数组加上 &lt;code&gt;apply&lt;/code&gt; 的方式（&lt;code&gt;apply&lt;/code&gt; 执行的是调用 &lt;code&gt;[[Call]]&lt;/code&gt; , 而不是构造 &lt;code&gt;[[Construct]]&lt;/code&gt;）。有了展开语法, 将数组展开为构造函数的参数就很简单了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var dateFields = [1970, 0, 1] // 1970年1月1日
var d = new Date(...dateFields)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想要不使用扩展运算符实现同样的效果，我们必须用一个函数包装构造函数，将这个新的构造函数的 &lt;code&gt;prototype&lt;/code&gt; 设为原构造函数的实例，用 &lt;code&gt;Object.create(constructor.prototype)&lt;/code&gt;（这里主要是为了新构造函数原型的修改不影响原构造函数的原型，直接用 &lt;code&gt;constructor.prototype&lt;/code&gt; 作为新构造函数的原型也可以实现）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function applyAndNew(constructor, args) {
  function partial() {
    return constructor.apply(this, args)
  }
  if (typeof constructor.prototype === &apos;object&apos;) {
    partial.prototype = Object.create(constructor.prototype)
  }
  return partial
}

function myConstructor() {
  console.log(&apos;arguments.length: &apos; + arguments.length)
  console.log(arguments)
  this.prop1 = &apos;val1&apos;
  this.prop2 = &apos;val2&apos;
}

var myArguments = [&apos;hi&apos;, &apos;how&apos;, &apos;are&apos;, &apos;you&apos;, &apos;mr&apos;, null]
var myConstructorWithArguments = applyAndNew(myConstructor, myArguments)

console.log(new myConstructorWithArguments())
// (myConstructor构造函数中):           arguments.length: 6
// (myConstructor构造函数中):           [&quot;hi&quot;, &quot;how&quot;, &quot;are&quot;, &quot;you&quot;, &quot;mr&quot;, null]
// (&quot;new myConstructorWithArguments&quot;中): {prop1: &quot;val1&quot;, prop2: &quot;val2&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;当然用的最多的还是在字面量数组上，没有展开语法的时候，只能组合使用 &lt;code&gt;push, splice, concat&lt;/code&gt; 等方法，来将已有数组元素变成新数组的一部分。有了展开语法, 通过字面量方式, 构造新数组会变得更简单、更优雅：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var parts = [&apos;shoulders&apos;, &apos;knees&apos;]
var lyrics = [&apos;head&apos;, ...parts, &apos;and&apos;, &apos;toes&apos;]
// [&quot;head&quot;, &quot;shoulders&quot;, &quot;knees&quot;, &quot;and&quot;, &quot;toes&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以用来实现数组浅拷贝：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var arr = [1, 2, 3]
var arr2 = [...arr] // like arr.slice()
arr2.push(4)

// arr2 此时变成 [1, 2, 3, 4]
// arr 不受影响
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连接多个数组：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var arr1 = [0, 1, 2]
var arr2 = [3, 4, 5]
var arr3 = [...arr1, ...arr2]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;扩展运算符还可以将已有对象的所有可枚举(&lt;code&gt;enumerable&lt;/code&gt;)属性拷贝到新构造的对象中。该方法为浅拷贝，可以拷贝 &lt;code&gt;Symbol&lt;/code&gt; 属性，但不包含原型上的属性和方法。如果同时拷贝多个对象，后面的对象会覆盖前面对象的同名属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var obj1 = { foo: &apos;bar&apos;, x: 42, [Symbol(&apos;a&apos;)]: 123 }
var obj2 = { foo: &apos;baz&apos;, x: 100, y: 13 }

var clonedObj = { ...obj1 }
console.log(clonedObj) //{ foo: &apos;bar&apos;, x: 42, [Symbol(a)]: 123 }

var mergedObj = { ...obj1, ...obj2 }
console.log(mergedObj) //{ foo: &apos;baz&apos;, x: 100, y: 13, [Symbol(a)]: 123 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方法的性质和 &lt;code&gt;Object.assign&lt;/code&gt; 类似，但是 &lt;code&gt;Object.assign()&lt;/code&gt; 函数会触发 &lt;code&gt;setters&lt;/code&gt;，而展开语法则不会。&lt;/p&gt;
&lt;p&gt;扩展运算符用于展开对象的时候可以发生覆盖，后面展开的或声明的属性会覆盖前面的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const test = {
    name: &apos;clloz&apos;,
    age: 29
}
{...test, age: 30} // {name: &quot;clloz&quot;, age: 30}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在数组或函数参数中使用展开语法时, 扩展运算符只能用于可迭代对象。&lt;/li&gt;
&lt;li&gt;只有函数调用时，扩展运算符才可以放在圆括号中，否则会报错。&lt;/li&gt;
&lt;li&gt;只能用在函数调用，字面量数组（可以在数组中展开字符串），字面量对象中。&lt;/li&gt;
&lt;li&gt;用于数组的解构赋值的时候，扩展运算符只能处于最后一个。&lt;/li&gt;
&lt;li&gt;展开对象可以是任意可迭代对象。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;剩余参数&lt;/h2&gt;
&lt;p&gt;剩余参数语法允许我们将一个不定数量的参数表示为一个数组。如果函数的最后一个命名参数以 &lt;code&gt;...&lt;/code&gt; 为前缀，则它将成为一个由剩余参数组成的真数组，其中从 &lt;code&gt;0&lt;/code&gt;（包括）到 &lt;code&gt;theArgs.length&lt;/code&gt;（排除）的元素由传递给函数的实际参数提供。&lt;/p&gt;
&lt;p&gt;剩余语法(&lt;code&gt;Rest syntax&lt;/code&gt;) 看起来和展开语法完全相同，不同点在于, 剩余参数用于解构数组和对象。从某种意义上说，剩余语法与展开语法是相反的：展开语法将数组展开为其中的各个元素，而剩余语法则是将多个元素收集起来并“凝聚”为单个元素。扩展运算符是用在函数调用，而剩余参数是用在函数声明。&lt;/p&gt;
&lt;p&gt;剩余参数和 &lt;code&gt;arguments&lt;/code&gt; 对象之间的区别主要有三个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;剩余参数只包含那些没有对应形参的实参，而 &lt;code&gt;arguments&lt;/code&gt; 对象包含了传给函数的所有实参。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arguments&lt;/code&gt; 对象不是一个真正的数组，而剩余参数是真正的 &lt;code&gt;Array&lt;/code&gt; 实例，也就是说你能够在它上面直接使用所有的数组方法，比如 &lt;code&gt;sort&lt;/code&gt;，&lt;code&gt;map&lt;/code&gt;，&lt;code&gt;forEach&lt;/code&gt; 或 &lt;code&gt;pop&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arguments&lt;/code&gt; 对象还有一些附加的属性 （如 &lt;code&gt;callee&lt;/code&gt; 属性）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果剩余参数（包括在解构赋值中）右侧有逗号，会抛出 &lt;code&gt;SyntaxError&lt;/code&gt;，因为剩余元素必须是函数的最后一个参数或者数组的最后一个元素。&lt;/p&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>深入 aync/await</title><link>https://clloz.com/blog/deep-into-async-await</link><guid isPermaLink="true">https://clloz.com/blog/deep-into-async-await</guid><pubDate>Mon, 02 Nov 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES 2017&lt;/code&gt; 引入了 &lt;code&gt;async&lt;/code&gt; 函数，它使得异步操作更加便捷。&lt;code&gt;async&lt;/code&gt; 是生成器函数的语法糖，我们可以理解成一个结合了 &lt;code&gt;Promise&lt;/code&gt; 和 &lt;code&gt;Generator&lt;/code&gt; 的自动执行的生成器。它让我们可以用一种更简洁的方式写出基于 &lt;code&gt;Promise&lt;/code&gt; 的异步行为，而无需刻意地链式调用 &lt;code&gt;promise&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;p&gt;回忆一下 &lt;code&gt;co&lt;/code&gt; 自动执行生成器函数的方式（&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/11/01/generator-async/#co&quot; title=&quot;生成器 Generator 的异步应用&quot;&gt;生成器 Generator 的异步应用&lt;/a&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var gen = function* () {
  var f1 = yield readFile(&apos;/etc/fstab&apos;)
  var f2 = yield readFile(&apos;/etc/shells&apos;)
  console.log(f1.toString())
  console.log(f2.toString())
}
co(gen)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 函数的使用方法类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var asyncReadFile = async function () {
  var f1 = await readFile(&apos;/etc/fstab&apos;)
  var f2 = await readFile(&apos;/etc/shells&apos;)
  console.log(f1.toString())
  console.log(f2.toString())
}
asyncReadFile()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出 &lt;code&gt;async&lt;/code&gt; 函数其实就是把 &lt;code&gt;*&lt;/code&gt; 和 &lt;code&gt;yield&lt;/code&gt; 换成了 &lt;code&gt;async&lt;/code&gt; 和 &lt;code&gt;await&lt;/code&gt;。&lt;code&gt;async&lt;/code&gt; 对 &lt;code&gt;Generator&lt;/code&gt; 函数的改进有以下几点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内置执行器：&lt;code&gt;Generator&lt;/code&gt; 函数的执行必须依赖执行器（比如 &lt;code&gt;co&lt;/code&gt;），否则需要手动调用 &lt;code&gt;next&lt;/code&gt; 才能执行。&lt;code&gt;async&lt;/code&gt; 函数则像一个普通函数一样执行即可。&lt;/li&gt;
&lt;li&gt;更好的语义：&lt;code&gt;async&lt;/code&gt; 和 &lt;code&gt;await&lt;/code&gt; 比 &lt;code&gt;*&lt;/code&gt; 和 &lt;code&gt;yield&lt;/code&gt; 语义更清楚。&lt;/li&gt;
&lt;li&gt;适用性更广：我们编写 &lt;code&gt;Generator&lt;/code&gt; 的自动执行器需要将 &lt;code&gt;yield&lt;/code&gt; 后的值包装成一个 &lt;code&gt;Thunk&lt;/code&gt; 函数或者 &lt;code&gt;Promise&lt;/code&gt;，&lt;code&gt;co&lt;/code&gt; 模块就是包装成 &lt;code&gt;Promise&lt;/code&gt;。&lt;code&gt;async&lt;/code&gt; 则支持 &lt;code&gt;Promise&lt;/code&gt; 和原始数据类型，不需要我们手动转换，开箱即用。&lt;/li&gt;
&lt;li&gt;和 &lt;code&gt;co&lt;/code&gt; 模块一样，&lt;code&gt;async&lt;/code&gt; 函数的返回值是一个 &lt;code&gt;Promise&lt;/code&gt;，我们可以用 &lt;code&gt;then&lt;/code&gt; 指定下一步操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 函数完全可以看作多个异步操作，包装成的一个 &lt;code&gt;Promise&lt;/code&gt; 对象，而 &lt;code&gt;await&lt;/code&gt; 命令就是内部 &lt;code&gt;then&lt;/code&gt; 命令的语法糖。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 函数是 &lt;code&gt;AsyncFunction&lt;/code&gt; 构造函数的实例，我们无法直接访问 &lt;code&gt;AsyncFunction&lt;/code&gt;，因为它不是一个全局对象，但是我们可以通过 &lt;code&gt;Object.getPrototypeOf(async function(){}).constructor&lt;/code&gt; 得到它，并且可以用它来构造 &lt;code&gt;async&lt;/code&gt; 函数 &lt;code&gt;new AsyncFunction([arg1[, arg2[, ...argN]],] functionBody)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 有很多使用形式，既可以用作函数声明，也可以用作函数表达式，还可以作为对象的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {}

// 对象的方法
let obj = { async foo() {} }
obj.foo().then((val) =&gt; val)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open(&apos;avatars&apos;)
  }
  async getAvatar(name) {
    const cache = await this.cachePromise
    return cache.match(`/avatars/${name}.jpg`)
  }
}
storage.getAvatar(&apos;jake&apos;).then((val) =&gt; val)

// 箭头函数
const foo = async () =&gt; {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 函数返回一个 &lt;code&gt;Promise&lt;/code&gt; 对象（如果一个 &lt;code&gt;async&lt;/code&gt; 函数的返回值看起来不是 &lt;code&gt;promise&lt;/code&gt;，那么它将会被隐式地包装在一个 &lt;code&gt;promise&lt;/code&gt; 中)，可以使用 &lt;code&gt;then&lt;/code&gt; 方法添加回调函数。当函数执行的时候，一旦遇到 &lt;code&gt;await&lt;/code&gt; 就会先返回，等到异步操作完成，再接着执行函数体内后面的语句。&lt;code&gt;async&lt;/code&gt; 函数返回一个 &lt;code&gt;Promise&lt;/code&gt; 对象。&lt;code&gt;async&lt;/code&gt; 函数内部 &lt;code&gt;return&lt;/code&gt; 语句返回的值，会成为 &lt;code&gt;then&lt;/code&gt; 方法回调函数的参数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; 操作符用于等待一个 &lt;code&gt;Promise&lt;/code&gt; 对象。它只能在异步函数 &lt;code&gt;async function&lt;/code&gt; 中使用。&lt;code&gt;await&lt;/code&gt; 表达式会暂停当前 &lt;code&gt;async function&lt;/code&gt; 的执行，等待 &lt;code&gt;Promise&lt;/code&gt; 处理完成。若 &lt;code&gt;Promise&lt;/code&gt; 正常处理(&lt;code&gt;fulfilled&lt;/code&gt;)，其回调的 &lt;code&gt;resolve&lt;/code&gt; 函数参数作为 &lt;code&gt;await&lt;/code&gt; 表达式的值，继续执行 &lt;code&gt;async function&lt;/code&gt;。若 &lt;code&gt;Promise&lt;/code&gt; 处理异常(&lt;code&gt;rejected&lt;/code&gt;)，&lt;code&gt;await&lt;/code&gt; 表达式会把 &lt;code&gt;Promise&lt;/code&gt; 的异常原因抛出。另外，如果 &lt;code&gt;await&lt;/code&gt; 操作符后的表达式的值不是一个 &lt;code&gt;Promise&lt;/code&gt;，&lt;code&gt;await&lt;/code&gt; 会把该值转换为已 &lt;code&gt;resolve&lt;/code&gt; 的 &lt;code&gt;Promise&lt;/code&gt;，然后等待其处理结果。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;async / await&lt;/code&gt; 关键字就可以在异步代码中使用普通的 &lt;code&gt;try / catch&lt;/code&gt; 代码块。因为同步或者异步代码出错都会将 &lt;code&gt;Promise&lt;/code&gt; 进行 &lt;code&gt;reject&lt;/code&gt; 返回。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;async/await&lt;/code&gt; 的目的为了简化使用基于 &lt;code&gt;promise&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt; 时所需的语法。&lt;code&gt;async/await&lt;/code&gt; 的行为就好像搭配使用了生成器和 &lt;code&gt;promise&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 函数的函数体可以被看作是由 &lt;code&gt;0&lt;/code&gt; 个或者多个 &lt;code&gt;await&lt;/code&gt; 表达式分割开来的。从第一行代码直到（并包括）第一个 &lt;code&gt;await&lt;/code&gt; 表达式（如果有的话）都是同步运行的。这样的话，一个不含 &lt;code&gt;await&lt;/code&gt; 表达式的 &lt;code&gt;async&lt;/code&gt; 函数是会同步运行的。然而，如果函数体内有一个 &lt;code&gt;await&lt;/code&gt; 表达式，&lt;code&gt;async&lt;/code&gt; 函数就一定会异步执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function k() {
  console.log(123)
  let m = await 3
  return m
}

k().then((val) =&gt; console.log(val))

console.log(&apos;end&apos;)
// 123
// end
// 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 函数内部抛出错误，会导致返回的 &lt;code&gt;Promise&lt;/code&gt; 对象变为 &lt;code&gt;reject&lt;/code&gt; 状态。抛出的错误对象会被 &lt;code&gt;catch&lt;/code&gt; 方法回调函数接收到。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function f() {
  throw new Error(&apos;出错了&apos;)
}
f().then(
  (v) =&gt; console.log(v),
  (e) =&gt; console.log(e)
)
// Error: 出错了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 函数返回的 &lt;code&gt;Promise&lt;/code&gt; 对象，必须等到内部所有 &lt;code&gt;await&lt;/code&gt; 命令后面的 &lt;code&gt;Promise&lt;/code&gt; 对象执行完，才会发生状态改变，除非遇到 &lt;code&gt;return&lt;/code&gt; 语句或者抛出错误。 也就是说，只有 &lt;code&gt;async&lt;/code&gt; 函数内部的异步操作执行完，才会执行 &lt;code&gt;then&lt;/code&gt; 方法指定的回调函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function getTitle(url) {
  let response = await fetch(url)
  let html = await response.text()
  return html.match(/&amp;#x3C;title&gt;([\s\S]+)&amp;#x3C;\/title&gt;/i)[1]
}
getTitle(&apos;https://tc39.github.io/ecma262/&apos;).then(console.log)
// &quot;ECMAScript 2017 Language Specification&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，函数 &lt;code&gt;getTitle&lt;/code&gt; 内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成，才会执行 &lt;code&gt;then&lt;/code&gt; 方法里面的 &lt;code&gt;console.log&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; 命令后面是一个 &lt;code&gt;Promise&lt;/code&gt; 对象。如果不是，会被转成一个立即 &lt;code&gt;resolve&lt;/code&gt; 的 &lt;code&gt;Promise&lt;/code&gt; 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function f() {
  return await 123
}
f().then((v) =&gt; console.log(v)) // 123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; 命令后面的 &lt;code&gt;Promise&lt;/code&gt; 对象如果变为 &lt;code&gt;reject&lt;/code&gt; 状态，则 &lt;code&gt;reject&lt;/code&gt; 的参数会 被外部的 &lt;code&gt;catch&lt;/code&gt; 方法的回调函数接收到。&lt;code&gt;reject&lt;/code&gt; 的 &lt;code&gt;Promise&lt;/code&gt; 不需要 &lt;code&gt;return&lt;/code&gt;，&lt;code&gt;catch&lt;/code&gt; 也能接收到 &lt;code&gt;reject&lt;/code&gt; 的参数，在 &lt;code&gt;await&lt;/code&gt; 前加上 &lt;code&gt;return&lt;/code&gt; 效果一样。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function f() {
  await Promise.reject(&apos;出错了&apos;)
}
f()
  .then((v) =&gt; console.log(v))
  .catch((e) =&gt; console.log(e)) // 出错了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要一个 &lt;code&gt;await&lt;/code&gt; 语句后面的 &lt;code&gt;Promise&lt;/code&gt; 变为 &lt;code&gt;reject&lt;/code&gt; ，那么整个 &lt;code&gt;async&lt;/code&gt; 函数都会中断执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function f() {
  await Promise.reject(&apos;出错了&apos;)
  await Promise.resolve(&apos;hello world&apos;) // 不会执行
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们希望一个 &lt;code&gt;await&lt;/code&gt; 即使 &lt;code&gt;reject&lt;/code&gt; 了也不要中断执行，那么我们有两种解决办法，一种是将这个 &lt;code&gt;await&lt;/code&gt; 包裹早一个 &lt;code&gt;try ... catch&lt;/code&gt; 中。另一种方法是将 &lt;code&gt;await&lt;/code&gt; 后面的 &lt;code&gt;Promise&lt;/code&gt; 用一个链式的 &lt;code&gt;catch&lt;/code&gt; 捕捉 &lt;code&gt;reject&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function f() {
  try {
    await Promise.reject(&apos;出错了&apos;)
  } catch (e) {}
  return await Promise.resolve(&apos;hello world&apos;)
}
f().then((v) =&gt; console.log(v)) // hello world

async function f() {
  await Promise.reject(&apos;出错了&apos;).catch((e) =&gt; console.log(e))
  return await Promise.resolve(&apos;hello world&apos;)
}
f().then((v) =&gt; console.log(v)) // 出错了
// hello world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;await&lt;/code&gt; 后面的异步操作抛错，那么等同于 &lt;code&gt;async&lt;/code&gt; 函数返回的 &lt;code&gt;Promise&lt;/code&gt; 对象被 &lt;code&gt;reject&lt;/code&gt;。要防止 &lt;code&gt;async&lt;/code&gt; 因为抛错而中断执行，解决方法依然和上面一样用 &lt;code&gt;try ... catch&lt;/code&gt; 包裹，或者对 &lt;code&gt;await&lt;/code&gt; 后面的 &lt;code&gt;Promise&lt;/code&gt; 加上 &lt;code&gt;catch&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error(&apos;出错了&apos;)
  })
}
f()
  .then((v) =&gt; console.log(v))
  .catch((e) =&gt; console.log(e)) // Error:出错了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一个注意要点就是 &lt;code&gt;await&lt;/code&gt; 只能在 &lt;code&gt;async&lt;/code&gt; 函数中使用，这里要注意嵌套关系。比如下面这种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function dbFuc(db) {
    let docs = [{}, {}, {}];
    // 报错
    docs.forEach(function (doc) {
        await db.post(doc);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; 实际上是写在 &lt;code&gt;forEach&lt;/code&gt; 的回调函数中，虽然外层的函数是 &lt;code&gt;async&lt;/code&gt;，但是 &lt;code&gt;forEach&lt;/code&gt; 的回调函数并不是 &lt;code&gt;async&lt;/code&gt; 函数。同时我们还要注意，上面的这种 &lt;code&gt;forEach&lt;/code&gt; 写法最后所有的任务是并发执行的，而不是继发。因为 &lt;code&gt;forEach&lt;/code&gt; 的每个回调函数都是立即执行的，是同步的，如果我们想要实现继发的效果，需要使用 &lt;code&gt;for&lt;/code&gt; 循环。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function dbFuc(db) {
  let docs = [{}, {}, {}]
  for (let doc of docs) {
    await db.post(doc)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想要实现并发，还是像上面一样同步执行异步任务，&lt;code&gt;await&lt;/code&gt; 异步任务返回的 &lt;code&gt;Promise&lt;/code&gt; 或者直接使用 &lt;code&gt;Promise.all&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;async/await 原理&lt;/h2&gt;
&lt;p&gt;我们已经可以很明显的看出，&lt;code&gt;async&lt;/code&gt; 是结合了 &lt;code&gt;Promise&lt;/code&gt; 和生成器的一个语法糖，它的目的就是为了优化 &lt;code&gt;Promise&lt;/code&gt; 的使用方法。&lt;code&gt;async&lt;/code&gt; 函数的实现原理，就是将 &lt;code&gt;Generator&lt;/code&gt; 函数和自动执行器，包装在一个函数里。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function fn(args) {
  // ...
}
// 等同于
function fn(args) {
  return spawn(function* () {
    // ...
  })
}

function spawn(genF) {
  return new Promise(function (resolve, reject) {
    var gen = genF()
    function step(nextF) {
      try {
        var next = nextF()
      } catch (e) {
        return reject(e)
      }
      if (next.done) {
        return resolve(next.value)
      }
      Promise.resolve(next.value).then(
        function (v) {
          step(function () {
            return gen.next(v)
          })
        },
        function (e) {
          step(function () {
            return gen.throw(e)
          })
        }
      )
    }
    step(function () {
      return gen.next(undefined)
    })
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;spawn&lt;/code&gt; 就是一个自动执行器，它是对 &lt;code&gt;co&lt;/code&gt; 的一个简化，关于 &lt;code&gt;co&lt;/code&gt; 的源码分析可以看 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/11/01/generator-async/#co&quot; title=&quot;生成器 Generator 的异步应用&quot;&gt;生成器 Generator 的异步应用&lt;/a&gt;。所有的 &lt;code&gt;async&lt;/code&gt; 函数都可以写成上面的第二种形式，其中的 &lt;code&gt;spawn&lt;/code&gt; 函数就是自动执行器。&lt;/p&gt;
&lt;h2&gt;继发和并发问题&lt;/h2&gt;
&lt;p&gt;我在上文已经说了一些继发和并发的例子和处理方式，这里在详细说一下这个比较重要的问题。&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;await&lt;/code&gt; 是有先后执行顺序的，也就是继发的。但我们的业务逻辑并不会是简单的继发或者并发，很可能是比较混杂的，我们需要自己分析好依赖关系然后再用 &lt;code&gt;async&lt;/code&gt; 函数处理。比如如下的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;;(async () =&gt; {
  const listData = await getList()
  const anotherListData = await getAnotherList()

  // do something

  await submit(listData)
  await submit(anotherListData)
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;getList&lt;/code&gt; 和 &lt;code&gt;getAnotherList&lt;/code&gt; 是没有依赖关系的，但是 &lt;code&gt;submit&lt;/code&gt; 需要依赖两者返回的数据，这种情况我们需要像如下处理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function handleList() {
  const listPromise = await getList()
  // ...
  await submit(listData)
}

async function handleAnotherList() {
  const anotherListPromise = await getAnotherList()
  // ...
  await submit(anotherListData)
}

// 方法一
;(async () =&gt; {
  const handleListPromise = handleList()
  const handleAnotherListPromise = handleAnotherList()
  await handleListPromise
  await handleAnotherListPromise
})()(
  // 方法二
  async () =&gt; {
    Promise.all([handleList(), handleAnotherList()]).then()
  }
)()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是分析清除依赖关系，把不相关的逻辑分开，并发执行。只有互相依赖的异步任务采用 &lt;code&gt;await&lt;/code&gt; 进行继发执行。&lt;/p&gt;
&lt;p&gt;一把来说，实现继发我们就依次写 &lt;code&gt;await&lt;/code&gt; 就行了。并发则是直接执行或者用 &lt;code&gt;Promise.all&lt;/code&gt;。比如给定一个 &lt;code&gt;url&lt;/code&gt; 数组如何实现继发和并发的请求。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 继发一
async function loadData() {
  var res1 = await fetch(url1)
  var res2 = await fetch(url2)
  var res3 = await fetch(url3)
  return &apos;whew all done&apos;
}

// 继发二
async function loadData(urls) {
  for (const url of urls) {
    const response = await fetch(url)
    console.log(await response.text())
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 并发一
async function loadData() {
  var res = await Promise.all([fetch(url1), fetch(url2), fetch(url3)])
  return &apos;whew all done&apos;
}
// 并发二
async function loadData(urls) {
  // 并发读取 url
  const promises = urls.map((url) =&gt; {
    return fetch(url)
  })

  // 按次序输出
  for (const promise of promises) {
    console.log(await promise)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;异步遍历器&lt;/h2&gt;
&lt;p&gt;未完待续...&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mqyqingfeng/Blog/issues/100&quot; title=&quot;我们来聊聊 async&quot;&gt;我们来聊聊 async&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;《ES6 标准入门》—— 阮一峰&lt;/li&gt;
&lt;li&gt;MDN&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>JavaScript 各异步方式比较</title><link>https://clloz.com/blog/javascript-async-implementation-compare</link><guid isPermaLink="true">https://clloz.com/blog/javascript-async-implementation-compare</guid><pubDate>Mon, 02 Nov 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;学习完了 &lt;code&gt;Promise&lt;/code&gt;，&lt;code&gt;Generator&lt;/code&gt; 和 &lt;code&gt;async&lt;/code&gt; 之后，用法基本都掌握了，也都理解了他们是如何实现的，但是对于为什么要有它们，在什么时候使用等问题似乎还是云里雾里，本文写一写我对几个异步处理方法的理解。&lt;/p&gt;
&lt;p&gt;如果你对 &lt;code&gt;ES6&lt;/code&gt; 的异步解决方案还不熟悉，请看之前的几篇文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/28/deep-into-promise/&quot; title=&quot;深入 Promise&quot;&gt;深入 Promise&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/31/es6-iterator/&quot; title=&quot;ES6 迭代器 Iterator&quot;&gt;ES6 迭代器 Iterator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/31/es6-generator/&quot; title=&quot;ES6 生成器 Generator&quot;&gt;ES6 生成器 Generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/11/01/generator-async/&quot; title=&quot;生成器 Generator 的异步应用&quot;&gt;生成器 Generator 的异步应用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/11/02/deep-into-aync-await/&quot; title=&quot;深入 aync/await&quot;&gt;深入 aync/await&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;异步处理的方式&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;JS&lt;/code&gt; 中异步是非常重要的概念，因为 &lt;code&gt;JS&lt;/code&gt; 的执行时单线程的，所以有些需要等待的任务必须要异步。比如我们请求一个文件，如果是同步的情况下，我们在等待请求这段时间线程是卡死的，我们什么也不能做，并且我们不确定什么时候请求成功。这就必须要异步，异步就是将执行分为两个阶段，第一个阶段同步执行，然后等第一个阶段的代码出结果了，在执行第二个阶段。在等待的这段时间，我们可以先处理其他任务。&lt;/p&gt;
&lt;h2&gt;回调函数&lt;/h2&gt;
&lt;p&gt;最早的异步处理方式自然是回调函数，我们将一个函数作为参数，当某个事件触发了，再执行这个函数。这是最简单的异步方式。包括浏览器的时间，定时器，&lt;code&gt;AJAX&lt;/code&gt; 请求都是这个思路。&lt;/p&gt;
&lt;p&gt;回调函数有很多问题，比较有名的就是回调地狱。当多个异步任务存在依赖关系的时候，我们只能等一个异步任务完成了，然后在回调函数中发起下一个异步任务。如果嵌套的任务非常多的话就会变成如下情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;ajax(&apos;XXX1&apos;, () =&gt; {
  // callback 函数体
  ajax(&apos;XXX2&apos;, () =&gt; {
    // callback 函数体
    ajax(&apos;XXX3&apos;, () =&gt; {
      // callback 函数体
    })
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于我们写代码的缩进关系，代码变成了横向发展，而不是纵向发展。最关键的是，这段代码可读性很差，非常难以维护，耦合度非常高，一旦这段逻辑要进行修改，就会牵一发而动全身。&lt;/p&gt;
&lt;p&gt;但是在我看来回调函数还有一些其他问题，就是我们必须在回调函数内处理逻辑。使用回调函数的异步任务没有返回值，异步任务的处理结果是作为回调函数的参数传入的，而回调函数也是异步任务的一个参数。我们想要到外部处理异步任务的结果，只能在外部申请变量，然后将异步任务的结果赋值给变量。换言之，我们是“配合”回调函数来写自己的业务逻辑。&lt;/p&gt;
&lt;p&gt;并且使用回调函数的异步任务，无法使用 &lt;code&gt;try ... catch&lt;/code&gt; 来进行错误处理，回调函数不会将错误向外层抛出，我们只能将错误处理写到回调函数中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// try catch 无法捕获到内部的错误
try {
  setTimeout(() =&gt; {
    console.log(aa)
  }, 1000)
} catch (error) {
  console.log(error)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Promise&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Promise&lt;/code&gt; 是 &lt;code&gt;ES6&lt;/code&gt; 引入的一个新的内置对象，它本质是对回调函数的一个封装。&lt;code&gt;Promise&lt;/code&gt; 的每一个方法返回的都还是一个 &lt;code&gt;Promise&lt;/code&gt; 对象，所以它能实现链式调用，这样就解决了回调地狱的问题。&lt;/p&gt;
&lt;p&gt;对于错误处理，&lt;code&gt;Promise&lt;/code&gt; 中测错误必须要用 &lt;code&gt;.catch&lt;/code&gt; 来捕获，它也不会抛到外层。&lt;/p&gt;
&lt;p&gt;我认为 &lt;code&gt;Promise&lt;/code&gt; 还有一点对回调函数的优化就是它将异步任务的状态和结果保存在了 &lt;code&gt;Promise&lt;/code&gt; 对象中，我们不再像回调函数一样，异步任务已完成就要执行对应的逻辑。只要有 &lt;code&gt;Promise&lt;/code&gt; 对象，我们随时都可以用 &lt;code&gt;.then&lt;/code&gt;，&lt;code&gt;.catch&lt;/code&gt; 来执行后面的逻辑。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Promise&lt;/code&gt; 也有自己的缺点，我们无法中断一个 &lt;code&gt;Promise&lt;/code&gt; 的执行，一个 &lt;code&gt;Promise&lt;/code&gt; 一旦开就无法取消。换言之我们无法介入一个已经开始执行的 &lt;code&gt;Promise&lt;/code&gt; 进行交互。还有一个问题就是上面说道的错误处理，和回调函数一样，&lt;code&gt;Promise&lt;/code&gt; 也不能通过外层的 &lt;code&gt;try ... catch&lt;/code&gt; 捕获错误，必须用 &lt;code&gt;.catch&lt;/code&gt; 来捕获内部的 &lt;code&gt;reject&lt;/code&gt;（抛错在 &lt;code&gt;Promise&lt;/code&gt; 中和 &lt;code&gt;reject&lt;/code&gt; 是一样的）。&lt;/p&gt;
&lt;p&gt;还有一个小问题就是 &lt;code&gt;Promise&lt;/code&gt; 的语义并不是很明确，我们的代码逻辑被隐藏在了 &lt;code&gt;Promise&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt; 之后，并不是很容易阅读。&lt;/p&gt;
&lt;h2&gt;Generator&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Generator&lt;/code&gt; 函数是 &lt;code&gt;ES6&lt;/code&gt; 对协程的实现，但属于不完全实现。&lt;code&gt;Generator&lt;/code&gt; 函数被称 为“半协程”(&lt;code&gt;semi-coroutine&lt;/code&gt;)，意思是只有 &lt;code&gt;Generator&lt;/code&gt; 函数的调用者，才能将程序的执行权还给 &lt;code&gt;Generator&lt;/code&gt; 函数。如果是完全执行的协程，任何函数都可以让暂停的 协程继续执行。&lt;/p&gt;
&lt;p&gt;如果将 &lt;code&gt;Generator&lt;/code&gt; 函数当作协程，完全可以将多个需要互相协作的任务写成 &lt;code&gt;Generator&lt;/code&gt; 函数，它们之间使用 &lt;code&gt;yield&lt;/code&gt; 表示式交换控制权。协程的过程可以模拟如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一步，协程 &lt;code&gt;A&lt;/code&gt; 开始执行。&lt;/li&gt;
&lt;li&gt;第二步，协程 &lt;code&gt;A&lt;/code&gt; 执行到一半，进入暂停，执行权转移到协程 &lt;code&gt;B&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;第三步，(一段时间后)协程 &lt;code&gt;B&lt;/code&gt; 交还执行权。&lt;/li&gt;
&lt;li&gt;第四步，协程 &lt;code&gt;A&lt;/code&gt; 恢复执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在有生成器之前，我们处理异步主要依靠的就是回调函数和 &lt;code&gt;Promise&lt;/code&gt;。不同的环境提供了不同的异步功能，比如 &lt;code&gt;node&lt;/code&gt; 的文件操作，浏览器的 &lt;code&gt;DOM&lt;/code&gt; 事件，以及网络请求等，都提供了异步的支持。我们在执行代码的时候声明一个 &lt;code&gt;callback&lt;/code&gt;，引擎会在事件完成的时候调用我们的回调函数。&lt;/p&gt;
&lt;p&gt;当然也有不依赖系统 &lt;code&gt;API&lt;/code&gt; 的异步方式，比如发布订阅模式，我们将我们要执行代码交给一个事件管理中心（可能是存在一个数组中），这个过程称为订阅。当我们想要执行某段回电函数的时候就通知事件管理中心，让它执行对应的函数，这叫做发布。实际上 &lt;code&gt;Promise&lt;/code&gt; 的实现也用到了这个模式。&lt;/p&gt;
&lt;p&gt;生成器可以说完全是一种全新的机制，它让我们能够让一个函数分段执行，并且函数内外能够在暂停和恢复的时候进行数据交换。内部的错误可以被外部捕获，也提供了从外部向内部跑错的 &lt;code&gt;throw&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;虽然 &lt;code&gt;Generator&lt;/code&gt; 不是只能用来进行异步处理，但是它在异步处理上是进行了革新的，提供了一种全新的形式，也就是协程的概念，我们可以堆函数的执行流程进行控制。&lt;/p&gt;
&lt;p&gt;生成器的缺点就是需要手动执行，我们必须手动调用 &lt;code&gt;next&lt;/code&gt;。所以需要我们自己写自动执行器，也就是 &lt;code&gt;co&lt;/code&gt; 模块，并且需要 &lt;code&gt;yield&lt;/code&gt; 后面的表达式返回一个 &lt;code&gt;Promise&lt;/code&gt; 对象（&lt;code&gt;co&lt;/code&gt; 模块还能够接受函数，生成器，数组和对象，数组和对象是为了并发执行）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;生成器还有一些其他作用，比如将普通对象变为可迭代对象，惰性求值等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;async/await&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 函数是生成器的语法糖，其实它就是自动执行的生成器和 &lt;code&gt;Promise&lt;/code&gt; 的结合。不过它比 &lt;code&gt;co&lt;/code&gt; 更好的地方是 &lt;code&gt;await&lt;/code&gt; 后面可以跟原始数据类型。&lt;/p&gt;
&lt;p&gt;其实 &lt;code&gt;async&lt;/code&gt; 函数和用 &lt;code&gt;co&lt;/code&gt; 包装过的生成器没有什么区别，不过它的语义化更好，&lt;code&gt;async&lt;/code&gt; 和 &lt;code&gt;await&lt;/code&gt; 非常容易理解，我们可以把我们一步任务像写同步任务一样用 &lt;code&gt;await&lt;/code&gt; 继发执行。&lt;code&gt;Async&lt;/code&gt; 函数的实现最简洁，最符合语义，几乎没有语义不相关的代码。它将 &lt;code&gt;Generator&lt;/code&gt; 写法中的自动执行器，改在语言层面提供，不暴露给用户，因此代码量最少。如果使用 &lt;code&gt;Generator&lt;/code&gt; 写法，自动执行器需要用户自己提供。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 有一个问题就是我们需要理清楚自己异步任务之间的依赖，如果你将两个不互相依赖的异步任务写在同一个 &lt;code&gt;async&lt;/code&gt; 的连续 &lt;code&gt;await&lt;/code&gt; 中，他们将会继发执行，降低效率。我们需要的是并发执行，可以用 &lt;code&gt;Promise.all&lt;/code&gt; 实现。&lt;/p&gt;
&lt;h2&gt;错误处理机制&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Promise&lt;/code&gt; 的错误只能用 &lt;code&gt;.catch&lt;/code&gt; 捕获，如果没有 &lt;code&gt;.catch&lt;/code&gt;，那么错误不会冒泡到外层，代码也不会中断执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var someAsyncThing = function () {
  return new Promise(function (resolve, reject) {
    // 下面一行会报错，因为x没有声明
    console.log(&apos;throw error&apos;)
    resolve(x + 2)
  })
}
try {
  someAsyncThing()
    .then(function () {
      console.log(&apos;everything is great&apos;)
    })
    .catch((e) =&gt; {
      console.log(&apos;promise catch: &apos; + e) //promise catch: ReferenceError: x is not defined
    })

  console.log(123123) //123123
} catch (e) {
  console.log(&apos;outer: &apos; + e)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Generator&lt;/code&gt; 内层抛错可以被外层接受（如果内层没有写 &lt;code&gt;catch&lt;/code&gt;），外层抛错可以用 &lt;code&gt;Generator.prototype.throw&lt;/code&gt; 方法传递给内层。一旦抛错被外层接收处理，则生成器的执行立即结束，&lt;code&gt;done&lt;/code&gt; 变为 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function* gen() {
  yield 1
  try {
    throw new Error(&apos;error1&apos;)
  } catch (e) {
    console.log(&apos;inner: &apos; + e)
  }
  yield 2
  throw new Error(&apos;error2&apos;)
}
let g = gen()
try {
  console.log(g.next())
  console.log(g.next())
  console.log(g.next())
} catch (e) {
  console.log(&apos;outer: &apos; + e)
}
// { value: 1, done: false }
// inner: Error: error1
// { value: 2, done: false }
// outer: Error: error2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 函数返回的是一个 &lt;code&gt;Promise&lt;/code&gt;，所以内部一旦抛错，这个 &lt;code&gt;Promise&lt;/code&gt; 的状态就立即变为 &lt;code&gt;rejected&lt;/code&gt;，错误信息会被传递给 &lt;code&gt;.catch&lt;/code&gt;。这就让我们能够在 &lt;code&gt;async&lt;/code&gt; 中使用普通的 &lt;code&gt;try ... catch&lt;/code&gt;。因为同步和异步的代码出错都会 &lt;code&gt;rejected&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function test() {
  console.log(x)
}
test()
  .then((val) =&gt; console.log(val))
  .catch((reason) =&gt; {
    console.log(&apos;reason: &apos; + reason) //reason: ReferenceError: x is not defined
  })

async function test() {
  console.log(1)
  await new Promise((resolve) =&gt; {
    throw new Error(&apos;promise error&apos;)
  })
}
test()
  .then((val) =&gt; console.log(val))
  .catch((reason) =&gt; {
    console.log(&apos;reason: &apos; + reason) //reason: Error: promise error
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;读取最大文件实现&lt;/h2&gt;
&lt;p&gt;有两个异步操作 &lt;code&gt;fs.readdir&lt;/code&gt; 和 &lt;code&gt;fs.stat&lt;/code&gt;，看看四种方式最后实现的结构如何。&lt;/p&gt;
&lt;h2&gt;回调函数实现&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var fs = require(&apos;fs&apos;)
var path = require(&apos;path&apos;)

function findLargest(dir, cb) {
  // 读取目录下的所有文件
  fs.readdir(dir, function (er, files) {
    if (er) return cb(er)

    var counter = files.length
    var errored = false
    var stats = []

    files.forEach(function (file, index) {
      // 读取文件信息
      fs.stat(path.join(dir, file), function (er, stat) {
        if (errored) return

        if (er) {
          errored = true
          return cb(er)
        }

        stats[index] = stat

        // 事先算好有多少个文件，读完 1 个文件信息，计数减 1，当为 0 时，说明读取完毕，此时执行最终的比较操作
        if (--counter == 0) {
          var largest = stats
            .filter(function (stat) {
              return stat.isFile()
            })
            .reduce(function (prev, next) {
              if (prev.size &gt; next.size) return prev
              return next
            })

          cb(null, files[stats.indexOf(largest)])
        }
      })
    })
  })
}
// 查找当前目录最大的文件
findLargest(&apos;./&apos;, function (er, filename) {
  if (er) return console.error(er)
  console.log(&apos;largest file was:&apos;, filename)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Promise 实现&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var fs = require(&apos;fs&apos;)
var path = require(&apos;path&apos;)

var readDir = function (dir) {
  return new Promise(function (resolve, reject) {
    fs.readdir(dir, function (err, files) {
      if (err) reject(err)
      resolve(files)
    })
  })
}

var stat = function (path) {
  return new Promise(function (resolve, reject) {
    fs.stat(path, function (err, stat) {
      if (err) reject(err)
      resolve(stat)
    })
  })
}

function findLargest(dir) {
  return readDir(dir)
    .then(function (files) {
      let promises = files.map((file) =&gt; stat(path.join(dir, file)))
      return Promise.all(promises).then(function (stats) {
        return { stats, files }
      })
    })
    .then((data) =&gt; {
      let largest = data.stats
        .filter(function (stat) {
          return stat.isFile()
        })
        .reduce((prev, next) =&gt; {
          if (prev.size &gt; next.size) return prev
          return next
        })

      return data.files[data.stats.indexOf(largest)]
    })
}

findLargest(&apos;./&apos;)
  .then(function (filename) {
    console.log(&apos;largest file was:&apos;, filename)
  })
  .catch(function (error) {
    console.log(error)
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Generator 实现&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var fs = require(&apos;fs&apos;)
var path = require(&apos;path&apos;)

var co = require(&apos;./co&apos;)

var readDir = function (dir) {
  return new Promise(function (resolve, reject) {
    fs.readdir(dir, function (err, files) {
      if (err) reject(err)
      resolve(files)
    })
  })
}

var stat = function (path) {
  return new Promise(function (resolve, reject) {
    fs.stat(path, function (err, stat) {
      if (err) reject(err)
      resolve(stat)
    })
  })
}

function* findLargest(dir) {
  var files = yield readDir(dir)
  var stats = yield files.map(function (file) {
    return stat(path.join(dir, file))
  })

  let largest = stats
    .filter(function (stat) {
      return stat.isFile()
    })
    .reduce((prev, next) =&gt; {
      if (prev.size &gt; next.size) return prev
      return next
    })

  return files[stats.indexOf(largest)]
}

co(findLargest, &apos;./&apos;)
  .then(function (filename) {
    console.log(&apos;largest file was:&apos;, filename)
  })
  .catch(function (error) {
    console.log(error)
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var fs = require(&apos;fs&apos;)
var path = require(&apos;path&apos;)

var readDir = function (dir) {
  return new Promise(function (resolve, reject) {
    fs.readdir(dir, function (err, files) {
      if (err) reject(err)
      resolve(files)
    })
  })
}

var stat = function (path) {
  return new Promise(function (resolve, reject) {
    fs.stat(path, function (err, stat) {
      if (err) reject(err)
      resolve(stat)
    })
  })
}

async function findLargest(dir) {
  var files = await readDir(dir)

  let promises = files.map((file) =&gt; stat(path.join(dir, file)))
  var stats = await Promise.all(promises)

  let largest = stats
    .filter(function (stat) {
      return stat.isFile()
    })
    .reduce((prev, next) =&gt; {
      if (prev.size &gt; next.size) return prev
      return next
    })

  return files[stats.indexOf(largest)]
}

findLargest(&apos;./&apos;)
  .then(function (filename) {
    console.log(&apos;largest file was:&apos;, filename)
  })
  .catch(function (error) {
    console.log(error)
  })
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>事件循环 Event Loop</title><link>https://clloz.com/blog/event-loop</link><guid isPermaLink="true">https://clloz.com/blog/event-loop</guid><pubDate>Sun, 01 Nov 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文分析 &lt;code&gt;JavaScript&lt;/code&gt; 的事件循环 &lt;code&gt;Event Loop&lt;/code&gt; 的执行机制和细节。&lt;code&gt;Event Loop&lt;/code&gt; 标准文档见 &lt;a href=&quot;https://html.spec.whatwg.org/multipage/webappapis.html#event-loops&quot; title=&quot;HTML5&quot;&gt;Event Loop - whatwg&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;基础概念&lt;/h2&gt;
&lt;p&gt;为了协调事件、用户交互、脚本、&lt;code&gt;UI&lt;/code&gt; 渲染、网络请求，用户代理必须使用 &lt;code&gt;Event Loop&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Event Loop&lt;/code&gt; 是 &lt;code&gt;JS&lt;/code&gt; 宿主环境的一个设施（浏览器和 &lt;code&gt;nodejs&lt;/code&gt; 都实现了），它不是 &lt;code&gt;JS&lt;/code&gt; 引擎的一部分。&lt;code&gt;JS&lt;/code&gt; 引擎负责 &lt;code&gt;JS&lt;/code&gt; 代码的执行，主要包括了一个内存堆和一个调用栈。而 &lt;code&gt;Event Loop&lt;/code&gt; 的主要作用就是对异步任务何时进入引擎执行进行管理。&lt;/p&gt;
&lt;p&gt;先了解三种数据结构&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;栈( &lt;code&gt;stack&lt;/code&gt; )：栈在计算机科学中是限定仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构，它按照后进先出的原则存储数据，先进入的数据被压入栈底，最后的数据在栈顶，需要读数据的时候从栈顶开始弹出数据。栈是只能在某一端插入和删除的特殊线性表。&lt;/li&gt;
&lt;li&gt;堆( &lt;code&gt;heap&lt;/code&gt; )：堆是一种数据结构，是利用完全二叉树维护的一组数据，堆分为两种，一种为最大堆，一种为最小堆，将根节点最大的堆叫做最大堆或大根堆，根节点最小的堆叫做最小堆或小根堆。堆是线性数据结构，相当于一维数组，有唯一后继。&lt;/li&gt;
&lt;li&gt;队列( &lt;code&gt;queue&lt;/code&gt; )：特殊之处在于它只允许在表的前端（ &lt;code&gt;front&lt;/code&gt; ）进行删除操作，而在表的后端（ &lt;code&gt;rear&lt;/code&gt; ）进行插入操作，和栈一样，队列是一种操作受限制的线性表。进行插入操作的端称为队尾，进行删除操作的端称为队头。 队列中没有元素时，称为空队列。队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队，从队列中删除一个队列元素称为出队。因为队列只允许在一端插入，在另一端删除，所以只有最早进入队列的元素才能最先从队列中删除，故队列又称为先进先出（ &lt;code&gt;FIFO—first in first out&lt;/code&gt; ）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;javaScript&lt;/code&gt; 是单线程，也就是说只有一个主线程，主线程有一个栈，每一个函数执行的时候，都会生成新的&lt;code&gt;execution context&lt;/code&gt; （执行上下文），执行上下文会包含一些当前函数的参数、局部变量之类的信息，它会被推入栈中， &lt;code&gt;running execution context&lt;/code&gt;（正在执行的上下文）始终处于栈的顶部。当函数执行完后，它的执行上下文会从栈弹出。把 &lt;code&gt;JS&lt;/code&gt; 执行设施再细分有三个部分（&lt;code&gt;1&lt;/code&gt; 和 &lt;code&gt;2&lt;/code&gt; 为 &lt;code&gt;JS&lt;/code&gt; 引擎中的）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Stack&lt;/code&gt; ：主线程的函数执行都压在这个栈中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Heap&lt;/code&gt; ：存放对象，数据。没有引用的对象会被垃圾回收。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Task Queue&lt;/code&gt; ：执行栈为空的时候从任务队列中取一个任务执行，再次为空时再次到任务队列中取任务执行，如此循环，所以称为 &lt;code&gt;Event Loop&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://img.clloz.com/blog/writing/js-engine.svg&quot; alt=&quot;js-engine&quot; title=&quot;js-engine&quot;&gt;&lt;/p&gt;
&lt;h2&gt;理解事件循环&lt;/h2&gt;
&lt;p&gt;理解事件循环机制首先要理解浏览器的组成，可以参考我的另一片文章：&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/04/25/how-browser-work-2/&quot; title=&quot;浏览器渲染过程&quot;&gt;浏览器渲染过程&lt;/a&gt;，这里我就做一个简短的说明。&lt;/p&gt;
&lt;p&gt;以 &lt;code&gt;Chorme&lt;/code&gt; 为例，每一个页面（一个 &lt;code&gt;tab&lt;/code&gt;）都是一个独立的进程，我们的浏览器内核也就是渲染引擎就在其中工作。渲染引擎包括了如下一些部分：&lt;code&gt;GUI&lt;/code&gt; 渲染线程，&lt;code&gt;JavaScript&lt;/code&gt; 引擎，事件触发线程，定时器触发线程，异步请求线程等。我们的 &lt;code&gt;JavaScript&lt;/code&gt; 引擎是工作在渲染引擎之下的。事件循环就可以理解为渲染引擎和 &lt;code&gt;JavaScript&lt;/code&gt; 引擎之间为了协调工作而创造的一种模式。&lt;/p&gt;
&lt;p&gt;首先在浏览器中，我们的 &lt;code&gt;JavaScript&lt;/code&gt; 脚本最终运行的目标都是改变页面，无论是改变页面的效果还是改变页面上的数据，所有的改动（不管是复杂的还是简单的）都会直接或间接的为页面上的某个或某些元素服务的。&lt;code&gt;JavaScript&lt;/code&gt; 和渲染引擎的交互主要是依靠浏览器提供的一系列 &lt;code&gt;Web APIs&lt;/code&gt;（可以参考 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API&quot; title=&quot;Web API - MDN&quot;&gt;Web API 接口参考 - MDN&lt;/a&gt;），而渲染引擎和 &lt;code&gt;JavaScript&lt;/code&gt; 引擎的交互就是依靠事件循环。&lt;/p&gt;
&lt;p&gt;用实际的例子来说明，我们打开一个 &lt;code&gt;tab&lt;/code&gt;，输入一个网址回车。网络进程像服务器发起请求。获得请求的内容之后开一个新的渲染进程，渲染引擎开始工作。&lt;code&gt;GUI&lt;/code&gt; 渲染线程进行 &lt;code&gt;HTML&lt;/code&gt;，&lt;code&gt;CSS&lt;/code&gt; 的解析，解析到 &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; 交给 &lt;code&gt;JavaScript&lt;/code&gt; 引擎处理。&lt;code&gt;JavaScript&lt;/code&gt; 创建全局环境，全局环境压入执行栈开始执行代码，当遇到 &lt;code&gt;setTimeout&lt;/code&gt; 等定时器代码，交给定时器触发线程，遇到 &lt;code&gt;ajax&lt;/code&gt; 请求交给异步请求线程，遇到事件绑定交给事件触发线程。按照这个逻辑，一直执行下去。我们可以看到，我们写的 &lt;code&gt;JavaScript&lt;/code&gt; 代码利用浏览器提供的 &lt;code&gt;Web API&lt;/code&gt; 把很多异步任务交给渲染引擎的其他线程进行处理。&lt;/p&gt;
&lt;p&gt;当这些异步任务完成之后，渲染引擎的其他线程需要通知 &lt;code&gt;JavaScript&lt;/code&gt; 引擎执行回调函数。异步任务的完成事件是不确定的，可能同时有很多任务完成了，而 &lt;code&gt;JavaScript&lt;/code&gt; 引擎是单线程的，所以必然要有一个机制让引擎能够依次执行异步任务的回调函数。这就是要有任务队列的原因。&lt;/p&gt;
&lt;p&gt;所以事件循环本质是渲染引擎和 &lt;code&gt;JavaScript&lt;/code&gt; 引擎为了处理异步任务而设计的一种有效率的模式。&lt;/p&gt;
&lt;p&gt;可以借助下图理解：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/event-loop.Nf0zc0WA_Y8p9q.webp&quot; alt=&quot;event-loop&quot; title=&quot;event-loop&quot;&gt;&lt;/p&gt;
&lt;h2&gt;规范&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://html.spec.whatwg.org/multipage/webappapis.html#event-loops&quot; title=&quot;HTML5&quot;&gt;Event Loop - whatwg&lt;/a&gt; 规范对 &lt;code&gt;Event Loop&lt;/code&gt; 进行了严格的定义。我们从规范来解读 &lt;code&gt;Event Loop&lt;/code&gt; 的细节。这里我们直接把最关键的几步列出来（翻译来自&lt;a href=&quot;https://juejin.im/entry/6844903487700992007&quot; title=&quot;深入探究 eventloop 与浏览器渲染的时序问题&quot;&gt;深入探究 eventloop 与浏览器渲染的时序问题&lt;/a&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1-5&lt;/code&gt; 条：从 &lt;code&gt;task&lt;/code&gt; 队列（一个或多个）中选出最老的一个 &lt;code&gt;task&lt;/code&gt;，执行它。&lt;/li&gt;
&lt;li&gt;第 &lt;code&gt;6&lt;/code&gt; 条：执行 &lt;code&gt;microtask&lt;/code&gt; 检查点。简单说，会执行 &lt;code&gt;microtask&lt;/code&gt; 队列中的所有 &lt;code&gt;microtask&lt;/code&gt;，直到队列为空。如果 &lt;code&gt;microtask&lt;/code&gt; 中又添加了新的 &lt;code&gt;microtask&lt;/code&gt;，直接放进本队列末尾。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7&lt;/code&gt;： 执行 &lt;code&gt;UI render&lt;/code&gt; 操作：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;7.1-7.4&lt;/code&gt;：判断 &lt;code&gt;document&lt;/code&gt; 在此时间点渲染是否会『获益』。浏览器只需保证 &lt;code&gt;60Hz&lt;/code&gt; 的刷新率即可（在机器负荷重时还会降低刷新率），若 &lt;code&gt;eventloop&lt;/code&gt; 频率过高，即使渲染了浏览器也无法及时展示。所以并不是每轮 &lt;code&gt;eventloop&lt;/code&gt; 都会执行 &lt;code&gt;UI Render&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7.5-7.9&lt;/code&gt;： 执行各种渲染所需工作，如 触发 &lt;code&gt;resize、scroll&lt;/code&gt; 事件、建立媒体查询、运行 &lt;code&gt;CSS&lt;/code&gt; 动画等等&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7.10&lt;/code&gt;： 执行 &lt;code&gt;animation frame callbacks&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7.11&lt;/code&gt;： 执行 &lt;code&gt;IntersectionObserver callback&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7.12&lt;/code&gt;： 渲染 &lt;code&gt;UI&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;任务队列&lt;/h2&gt;
&lt;p&gt;从标准中我们看出，一共有两种任务 &lt;code&gt;task&lt;/code&gt; 和 &lt;code&gt;microtask&lt;/code&gt;，&lt;code&gt;task&lt;/code&gt; 很多时候也被称为 &lt;code&gt;macrotask&lt;/code&gt;。关于 &lt;code&gt;task&lt;/code&gt; 标准中有详细 &lt;a href=&quot;https://html.spec.whatwg.org/multipage/webappapis.html#concept-task&quot; title=&quot;定义&quot;&gt;定义&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;An event loop has one or more task queues. For example, a user agent could have one task queue for mouse and key events (to which the user interaction task source is associated), and another to which all other task sources are associated. Then, using the freedom granted in the initial step of the event loop processing model, it could give keyboard and mouse events preference over other tasks three-quarters of the time, keeping the interface responsive but not starving other task queues. Note that in this setup, the processing model still enforces that the user agent would never process events from any one task source out of order.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个 &lt;code&gt;eventloop&lt;/code&gt; 有一或多个 &lt;code&gt;task&lt;/code&gt; 队列。每个 &lt;code&gt;task&lt;/code&gt; 由一个确定的 &lt;code&gt;task&lt;/code&gt; 源提供。从不同 &lt;code&gt;task&lt;/code&gt; 源而来的 &lt;code&gt;task&lt;/code&gt; 可能会放到不同的 &lt;code&gt;task&lt;/code&gt; 队列中。例如，浏览器可能单独为鼠标键盘事件维护一个 &lt;code&gt;task&lt;/code&gt; 队列，所有其他 &lt;code&gt;task&lt;/code&gt; 都放到另一个 &lt;code&gt;task&lt;/code&gt; 队列。通过区分 &lt;code&gt;task&lt;/code&gt; 队列的优先级，使高优先级的 &lt;code&gt;task&lt;/code&gt; 优先执行，保证更好的交互体验。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;task&lt;/code&gt; 源包括（ &lt;a href=&quot;https://html.spec.whatwg.org/multipage/webappapis.html#generic-task-sources&quot; title=&quot;generic-task-sources&quot;&gt;generic-task-sources&lt;/a&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DOM&lt;/code&gt; 操作任务源：如元素以非阻塞方式插入文档&lt;/li&gt;
&lt;li&gt;用户交互任务源：如鼠标键盘事件。用户输入事件（如 &lt;code&gt;click&lt;/code&gt;） 必须使用 &lt;code&gt;task&lt;/code&gt; 队列&lt;/li&gt;
&lt;li&gt;网络任务源：如 &lt;code&gt;XHR&lt;/code&gt; 回调&lt;/li&gt;
&lt;li&gt;&lt;code&gt;history&lt;/code&gt; 回溯任务源：使用 &lt;code&gt;history.back()&lt;/code&gt; 或者类似 &lt;code&gt;API&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setTimeout&lt;/code&gt;、&lt;code&gt;setInterval&lt;/code&gt;、&lt;code&gt;IndexDB&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以常见的 &lt;code&gt;task&lt;/code&gt; 任务包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;script&lt;/code&gt; 代码&lt;/li&gt;
&lt;li&gt;事件回调&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XHR&lt;/code&gt; 回调&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IndexDB&lt;/code&gt; 数据库操作等 &lt;code&gt;I/O&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setTimeout&lt;/code&gt; / &lt;code&gt;setInterval&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;history.back&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;postMessage&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面讲一讲微任务 &lt;code&gt;microtask&lt;/code&gt;，每一个 &lt;code&gt;eventloop&lt;/code&gt; 都有一个 &lt;code&gt;microtask&lt;/code&gt; 队列。&lt;code&gt;microtask&lt;/code&gt; 会排在 &lt;code&gt;microtask&lt;/code&gt; 队列而非 &lt;code&gt;task&lt;/code&gt; 队列中。一般微任务包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Promise.then&lt;/code&gt;，&lt;code&gt;Promise.catch&lt;/code&gt;，&lt;code&gt;Promise.finally&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MutationObserver&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;process.nextTick&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前浏览器暴露了一个 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope/queueMicrotask&quot; title=&quot;WindowOrWorkerGlobalScope.queueMicrotask()&quot;&gt;WindowOrWorkerGlobalScope.queueMicrotask()&lt;/a&gt; 让我们能够用微任务执行我们的代码。&lt;/p&gt;
&lt;h3&gt;为什么有宏任务和微任务&lt;/h3&gt;
&lt;p&gt;其实网上关于宏任务微任务的讲解非常多，基本都是讲一讲哪些属于宏任务，哪些属于微任务，然后执行顺序。但是几乎没有人说为什么要设计成这两种模式。&lt;/p&gt;
&lt;p&gt;其实如果我们仔细想一想，为什么需要宏任务和微任务？任何设计都是有原因的，浏览器内核发展了这么多年，有这种设计必然就有需求，比如我上面讲的事件循环解决的问题，那么微任务也必然有其解决的问题。&lt;/p&gt;
&lt;p&gt;我这里先说我的结论，宏任务和微任务的区分主要还是为了给任务一个优先级的区分，让一些有优先级要求或者连续执行需求的任务优先执行。&lt;/p&gt;
&lt;p&gt;我们来看一看 &lt;code&gt;MDN&lt;/code&gt; 上对于宏任务和微任务的说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个微任务（&lt;code&gt;microtask&lt;/code&gt;）就是一个简短的函数，当创建该函数的函数执行之后，并且只有当 &lt;code&gt;Javascript&lt;/code&gt; 调用栈为空，而控制权尚未返还给被 &lt;code&gt;user agent&lt;/code&gt; 用来驱动脚本执行环境的事件循环之前，该微任务才会被执行。 事件循环既可能是浏览器的主事件循环也可能是被一个 &lt;code&gt;web worker&lt;/code&gt; 所驱动的事件循环。这使得给定的函数在没有其他脚本执行干扰的情况下运行，也保证了微任务能在用户代理有机会对该微任务带来的行为做出反应之前运行。&lt;code&gt;JavaScript&lt;/code&gt; 中的 &lt;code&gt;promises&lt;/code&gt; 和 &lt;code&gt;Mutation Observer API&lt;/code&gt; 都使用微任务队列去运行它们的回调函数，但当能够推迟工作直到当前事件循环过程完结时，也是可以执行微任务的时机。&lt;/li&gt;
&lt;li&gt;一个任务就是由执行诸如从头执行一段程序、执行一个事件回调或一个 &lt;code&gt;interval/timeout&lt;/code&gt; 被触发之类的标准机制而被调度的任意 &lt;code&gt;JavaScript&lt;/code&gt; 代码。这些都在任务队列（&lt;code&gt;task queue&lt;/code&gt;）上被调度。在以下时机，任务会被添加到任务队列：
&lt;ul&gt;
&lt;li&gt;一段新程序或子程序被直接执行时（比如从一个控制台，或在一个 &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; 元素中运行代码）。&lt;/li&gt;
&lt;li&gt;触发了一个事件，将其回调函数添加到任务队列时。&lt;/li&gt;
&lt;li&gt;执行到一个由 &lt;code&gt;setTimeout()&lt;/code&gt; 或 &lt;code&gt;setInterval()&lt;/code&gt; 创建的 &lt;code&gt;timeout&lt;/code&gt; 或 &lt;code&gt;interval&lt;/code&gt;，以致相应的回调函数被添加到任务队列时。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;MDN&lt;/code&gt; 也给出了使用微任务的建议：因为微任务自身可以入列更多的微任务，且事件循环会持续处理微任务直至队列为空，那么就存在一种使得事件循环无尽处理微任务的真实风险。如何处理递归增加微任务是要谨慎而行的。如果可能的话，大部分开发者并不应该过多的使用微任务。在基于现代浏览器的 &lt;code&gt;JavaScript&lt;/code&gt; 开发中有一个高度专业化的特性，那就是允许你调度代码跳转到其他事情之前，而那些事情原本是处于用户计算机中一大堆等待发生的事情集合之中的。滥用这种能力将带来性能问题。&lt;/p&gt;
&lt;p&gt;所以微任务的处理方式本质还是提供了一个优先执行任务的通道，让一些有需要的任务优先执行，不过我们需要正确地使用。&lt;/p&gt;
&lt;p&gt;这里属于我个人的理解，如果有错误或者意见，欢迎讨论。&lt;/p&gt;
&lt;h2&gt;事件循环过程&lt;/h2&gt;
&lt;p&gt;简化标准中的执行流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;取一个宏任务来执行。执行完毕或没有宏任务，进入下一步。执行过程中触发的微任务会直接放入微任务队列，会在本轮执行。&lt;/li&gt;
&lt;li&gt;取一个微任务来执行，执行完毕后，再取一个微任务来执行。直到微任务队列为空，执行下一步。执行过程中遇到的微任务也会放到队列后在本轮执行。&lt;/li&gt;
&lt;li&gt;更新UI渲染。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;Event Loop&lt;/code&gt; 会无限循环执行上面3步，这就是 &lt;code&gt;Event Loop&lt;/code&gt; 的主要控制逻辑。其中第三部 &lt;code&gt;UI&lt;/code&gt; 渲染不是每次事件循环都进行的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个&lt;a href=&quot;http://latentflip.com/loupe/?code=ZnVuY3Rpb24gYygpIHt9CmZ1bmN0aW9uIGIoKSB7CgljKCk7Cn0KZnVuY3Rpb24gYSgpIHsKCXNldFRpbWVvdXQoYiwgMjAwMCkKfQphKCk7!!!&quot; title=&quot;可视化 JS 执行过程&quot;&gt;可视化 JS 执行过程&lt;/a&gt;可以帮助你理解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从逻辑上来看，浏览器倾向于尽可能快地执行完微任务，当全局任务（其实是全局函数中的同步任务）执行完之后，会立即执行微任务队列，即使微任务队列执行完了，在每次执行完一个宏任务之后都会检查微任务队列，如果就微任务就一直执行到微任务队列为空才会执行宏任务。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&apos;script start&apos;);

// 微任务
Promise.resolve().then(() =&gt; {
    console.log(&apos;p 1&apos;);
});

// 宏任务
setTimeout(() =&gt; {
    console.log(&apos;setTimeout&apos;);
}, 0);

var s = new Date();
while(new Date() - s &amp;#x3C; 50); // 阻塞50ms

// 微任务
Promise.resolve().then(() =&gt; {
    console.log(&apos;p 2&apos;);
});

console.log(&apos;script ent&apos;);

/*** output ***/
// one macro task
script start
script ent

// all micro tasks
p 1
p 2

// one macro task again
setTimeout
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;什么时候触发 UI render&lt;/h2&gt;
&lt;p&gt;按照标准中的说明：如果浏览器试图实现 &lt;code&gt;60Hz&lt;/code&gt; 的刷新率，那么 &lt;code&gt;UI Render&lt;/code&gt; 只需要每秒执行 &lt;code&gt;60&lt;/code&gt; 次（每 &lt;code&gt;16.7 ms&lt;/code&gt;）。如果浏览器发现『顶层浏览器上下文』无法维持住这个频率，可能会下调到可维持的 &lt;code&gt;30Hz&lt;/code&gt;，而不是掉帧。（本规范并不对何时进行 &lt;code&gt;render&lt;/code&gt; 做任何规定。）类似的，如果一个顶层浏览器上下文在后台运行，用户代理可能决定将该页面的刷新率降到 &lt;code&gt;4Hz&lt;/code&gt;，甚至更低。&lt;/p&gt;
&lt;p&gt;如果满足以下条件，也会跳过渲染：浏览器判断更新渲染不会带来视觉上的改变。&lt;code&gt;map of animation frame callbacks&lt;/code&gt; 为空，也就是帧动画回调为空，可以通过 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 来请求帧动画。&lt;/p&gt;
&lt;p&gt;这里要说一说 &lt;code&gt;requestAnimationFrame&lt;/code&gt;，&lt;code&gt;window.requestAnimationFrame()&lt;/code&gt; 告诉浏览器——你希望执行一个动画，并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数，该回调函数会在浏览器下一次重绘之前执行。根据标准中的定义 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 也是 &lt;code&gt;UI Render&lt;/code&gt; 的其中一步，所以 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 的回调函数的执行时跟 &lt;code&gt;UI render&lt;/code&gt; 是同步的。但是我们上面说了，&lt;code&gt;UI render&lt;/code&gt; 的频率是不确定的，所以我们不能明确知道 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 会在哪一次 &lt;code&gt;Event Loop&lt;/code&gt; 执行。既有可能出现每一轮 &lt;code&gt;eventloop&lt;/code&gt; 后都 &lt;code&gt;render&lt;/code&gt; 的现象，也有可能出现几十轮 &lt;code&gt;eventloop&lt;/code&gt; 都不 &lt;code&gt;render&lt;/code&gt; 的情况，根据浏览器的实现不同和使用的电脑状况不同都有可能出现差异。比如下面这个题目：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;setTimeout(function () {
  console.log(1)
}, 1)
setTimeout(function () {
  console.log(2)
}, 2)
setTimeout(function () {
  console.log(3)
}, 3)
requestAnimationFrame(function () {
  console.log(4)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这道题的输出可能是 &lt;code&gt;4123&lt;/code&gt;，&lt;code&gt;1423&lt;/code&gt;，&lt;code&gt;1243&lt;/code&gt; 或者 &lt;code&gt;1234&lt;/code&gt;，主要愿意就是我们不知道浏览器在哪一次 &lt;code&gt;Event Loop&lt;/code&gt; 进行渲染，所以 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 可能在任意一个 &lt;code&gt;setTimeout&lt;/code&gt; 后面执行。&lt;/p&gt;
&lt;p&gt;再比如下面这题求输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&apos;1&apos;)

setTimeout(function () {
  console.log(&apos;2&apos;)
  process.nextTick(function () {
    console.log(&apos;3&apos;)
  })
  new Promise(function (resolve) {
    console.log(&apos;4&apos;)
    resolve()
  }).then(function () {
    console.log(&apos;5&apos;)
  })
})
process.nextTick(function () {
  console.log(&apos;6&apos;)
})
new Promise(function (resolve) {
  console.log(&apos;7&apos;)
  resolve()
}).then(function () {
  console.log(&apos;8&apos;)
})

setTimeout(function () {
  console.log(&apos;9&apos;)
  process.nextTick(function () {
    console.log(&apos;10&apos;)
  })
  new Promise(function (resolve) {
    console.log(&apos;11&apos;)
    resolve()
  }).then(function () {
    console.log(&apos;12&apos;)
  })
})
//1，7，6，8，2，4，3，5，9，11，10，12
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;NodeJs 的 Event Loop&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Node&lt;/code&gt; 端事件循环中的异步队列也是这两种：&lt;code&gt;macro&lt;/code&gt;（宏任务）队列和 &lt;code&gt;micro&lt;/code&gt;（微任务）队列。&lt;/p&gt;
&lt;p&gt;常见的 &lt;code&gt;macro-task&lt;/code&gt; 比如：&lt;code&gt;setTimeout&lt;/code&gt;、&lt;code&gt;setInterval&lt;/code&gt;、 &lt;code&gt;setImmediate&lt;/code&gt;、&lt;code&gt;script&lt;/code&gt;（整体代码）、 &lt;code&gt;I/O&lt;/code&gt; 操作等。 常见的 &lt;code&gt;micro-task&lt;/code&gt; 比如: &lt;code&gt;process.nextTick&lt;/code&gt;、&lt;code&gt;new Promise().then&lt;/code&gt;(回调)等。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Event Loop&lt;/code&gt; 之前会先做这些工作： 1. 初始化 &lt;code&gt;Event Loop&lt;/code&gt; 2. 执行主代码。这里同样，遇到异步处理，就会分配给对应的队列。直到主代码执行完毕。 3. 执行主代码中出现的所有微任务：先执行完所有nextTick()，然后在执行其它所有微任务。 4. 开始 &lt;code&gt;Event Loop&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Event Loop&lt;/code&gt;分为&lt;code&gt;6&lt;/code&gt;个阶段： 1. &lt;code&gt;timers&lt;/code&gt; : 这个阶段执行 &lt;code&gt;setTimeout()&lt;/code&gt; 和 &lt;code&gt;setInterval()&lt;/code&gt; 设定的回调。 2. &lt;code&gt;pending callbacks&lt;/code&gt; : 上一轮循环中有少数的 &lt;code&gt;I/O callback&lt;/code&gt; 会被延迟到这一轮的这一阶段执行。 3. &lt;code&gt;idle&lt;/code&gt; , &lt;code&gt;prepare&lt;/code&gt; : 仅内部使用。 4. &lt;code&gt;poll&lt;/code&gt; : 执行 &lt;code&gt;I/O callback&lt;/code&gt;，在适当的条件下会阻塞在这个阶段 5. &lt;code&gt;check&lt;/code&gt; : 执行 &lt;code&gt;setImmediate()&lt;/code&gt;设定的回调。 6. &lt;code&gt;close callbacks&lt;/code&gt; : 执行比如 &lt;code&gt;socket.on(&apos;close&apos;, ...)&lt;/code&gt; 的回调。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Node 11&lt;/code&gt; 之前每个阶段执行完毕后，才会执行所有微任务（先 &lt;code&gt;nextTick&lt;/code&gt;，后其它），然后再进入下一个阶段。在 &lt;code&gt;Node 11&lt;/code&gt; 之后，就和浏览器一样，在每一个宏任务执行之后，执行所有微任务队列中的微任务。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;关于事件循环，网络上的文章都差不多，不过今天看了两篇文章：&lt;a href=&quot;https://juejin.im/entry/6844903487700992007&quot; title=&quot;深入探究 eventloop 与浏览器渲染的时序问题&quot;&gt;深入探究 eventloop 与浏览器渲染的时序问题&lt;/a&gt; 和 &lt;a href=&quot;https://zhuanlan.zhihu.com/p/142742003&quot; title=&quot;深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系&quot;&gt;深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系&lt;/a&gt; 发现自己懂得并不是很透彻。从标准的角度看，事件循环的逻辑也还是挺复杂的，其中还有很多细节没有掌握。当然，这也可能是事件循环还没有一个确定的标准导致的，各个浏览器的实现还不完全一致，包括 &lt;code&gt;ECMAScript&lt;/code&gt; 关于 &lt;code&gt;Job&lt;/code&gt; 的一些规定和 &lt;code&gt;HTML5&lt;/code&gt; 标准对于时间循环的定义都是有冲突的，&lt;code&gt;Node&lt;/code&gt; 的实现也和浏览器不同，还是要继续摸索。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000011198232&quot; title=&quot;https://segmentfault.com/a/1190000011198232&quot;&gt;JavaScript 异步、栈、事件循环、任务队列&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/5c3d8956e51d4511dc72c200#heading-15&quot; title=&quot;一次弄懂Event Loop&quot;&gt;一次弄懂Event Loop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/142742003&quot; title=&quot;深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系&quot;&gt;深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/entry/6844903487700992007&quot; title=&quot;深入探究 eventloop 与浏览器渲染的时序问题&quot;&gt;深入探究 eventloop 与浏览器渲染的时序问题&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ginobilee.github.io/blog/2019/02/01/requestAnimationFrame%E6%98%AF%E4%B8%80%E4%B8%AA%E5%AE%8F%E4%BB%BB%E5%8A%A1%E4%B9%88/&quot; title=&quot;requestAnimationFrame是一个宏任务么&quot;&gt;requestAnimationFrame是一个宏任务么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844903761949753352&quot; title=&quot;浏览器与Node的事件循环(Event Loop)有何区别?&quot;&gt;浏览器与Node的事件循环(Event Loop)有何区别?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/26&quot; title=&quot;浏览器和Node 事件循环的区别&quot;&gt;浏览器和Node 事件循环的区别&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>生成器 Generator 的异步应用</title><link>https://clloz.com/blog/generator-async</link><guid isPermaLink="true">https://clloz.com/blog/generator-async</guid><pubDate>Sun, 01 Nov 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;生成器有一个问题就是需要我们手动调用 &lt;code&gt;next&lt;/code&gt; 来执行，这就表示我们需要单独写一段代码来管理生成器的执行与暂停，这是比较麻烦的。如何让我们写的生成器能够自动执行，本文就来讨论实现方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你对生成器 &lt;code&gt;Generator&lt;/code&gt; 还不了解，请先看另一篇详细介绍生成器的文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/31/es6-generator/&quot; title=&quot;ES6 生成器 Generator&quot;&gt;ES6 生成器 Generator&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;生成器的关键&lt;/h2&gt;
&lt;p&gt;生成器之所以能够比 &lt;code&gt;Promise&lt;/code&gt; 更好的封装异步任务，首先是它可以利用 &lt;code&gt;yield&lt;/code&gt; 和 &lt;code&gt;next&lt;/code&gt; 来暂停和回复函数的执行。但是还有两个重要的特性让它能够更好的完成这个任务，那就是 &lt;code&gt;yield&lt;/code&gt; 和 &lt;code&gt;next&lt;/code&gt; 能够实现函数体内外的数据交换，以及函数体内外能够互相进行错误捕获。&lt;/p&gt;
&lt;p&gt;封装异步任务的一个关键就是，当函数暂停执行之后，我们如何知道异步任务完成了，然后调用 &lt;code&gt;next&lt;/code&gt; 执行下一步？对于异步任务，只有通过回调函数才能知道任务是否完成。所以要封装异步任务，我们就要保证回调函数是在生成器外部执行的，我们可以在回调函数中执行 &lt;code&gt;next&lt;/code&gt;。所以我们要封装异步任务最主要的一个任务就是让回调函数在生成器外声明和执行。这就是核心思想。&lt;/p&gt;
&lt;h2&gt;Thunk 函数&lt;/h2&gt;
&lt;p&gt;我们先来看看一般异步任务的形式，比如异步读取文件 &lt;code&gt;fs.readFile(fileName, callback);&lt;/code&gt;，我们执行这个函数后悔立即根据第一个文件名参数读取文件，读取成功后执行 &lt;code&gt;callback&lt;/code&gt;，根据 &lt;code&gt;API&lt;/code&gt; 的不同，&lt;code&gt;callback&lt;/code&gt; 会被传入相应的参数。如果我们直接把这么一个函数放到生成器的 &lt;code&gt;yield&lt;/code&gt; 后面我们是无法处理的。因为我们肯定是希望 &lt;code&gt;callback&lt;/code&gt; 执行以后再执行下一步，但是这样的调用方式，我们不可能知道 &lt;code&gt;callback&lt;/code&gt; 何时执行，此时我们在生成器外部实际已经丢失了对异步任务的控制。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var gen = function* () {
  yield readFile(&apos;/etc/filea&apos;)
}

let g = gen()
let filea = g.next() //此时我们实际已经丢失了异步任务的控制
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们要对异步任务进行封装。封装的目标是什么？我们希望在生成器内部传入文件名，在生成器外部传入 &lt;code&gt;callback&lt;/code&gt;。形式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var gen = function* () {
  yield readFileThunk(&apos;/etc/filea&apos;)
}

let g = gen()
let filea = g.next()
filea.value(function (err, data) {
  //your code
  g.next(data)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实我们可以看出，内部的封装函数执行只不过是传入了一个参数，真正的查询文件操作并没有执行，只是返回了一个带参数的函数而已，真正的执行还是在外部我们传入了 &lt;code&gt;callback&lt;/code&gt; 之后，所以我们能够在 &lt;code&gt;callback&lt;/code&gt; 中将控制权传回生成器内。这就是 &lt;code&gt;Thunk&lt;/code&gt; 的目标，逻辑也非常简单。&lt;/p&gt;
&lt;p&gt;一个 &lt;code&gt;Thunk&lt;/code&gt; 函数我们大概要封装三层，第一层传入那个待执行的异步函数，然后传入参数，最后在生成器外部传入 &lt;code&gt;callback&lt;/code&gt;。封装的实现我们可以参考 &lt;code&gt;tj&lt;/code&gt; 实现的 &lt;a href=&quot;https://github.com/tj/node-thunkify&quot; title=&quot;thunkify&quot;&gt;thunkify&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function thunkify(fn) {
  return function () {
    var args = new Array(arguments.length)
    var ctx = this
    for (var i = 0; i &amp;#x3C; args.length; ++i) {
      args[i] = arguments[i]
    }
    return function (done) {
      var called
      args.push(function () {
        if (called) return
        called = true
        done.apply(null, arguments)
      })
      try {
        fn.apply(ctx, args)
      } catch (err) {
        done(err)
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到整个代码非常短，内部有两个 &lt;code&gt;return&lt;/code&gt;，连上最外层的函数调用，一共三层，分别传入异步函数，参数，和回调函数。&lt;/p&gt;
&lt;p&gt;简要分析一下流程，传入异步函数（比如 &lt;code&gt;fs.readFile&lt;/code&gt;）执行 &lt;code&gt;thunkify&lt;/code&gt; 会返回一个函数。返回的函数是一个中间层，供我们传入参数，比如 &lt;code&gt;fs.readFile&lt;/code&gt; 的文件名，这个函数也就是我们写在 &lt;code&gt;yield&lt;/code&gt; 后面的函数。实际这一层的主要操作就是对 &lt;code&gt;arguments&lt;/code&gt; 的操作，它将我们传入的参数保存到了一个数组中，同时保存了 &lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个函数执行后返回一个函数，也就是被保存在 &lt;code&gt;yield&lt;/code&gt; 返回的对象的 &lt;code&gt;value&lt;/code&gt; 属性中。于是在生成器外，我们可以获得这个函数，然后就可以传入回调函数执行了。最后返回的这个函数内部主要的处理就是把新传入的回调函数放到刚刚保存的参数的数组的最后，然后用保存的 &lt;code&gt;this&lt;/code&gt; 和这个参数数组执行异步函数 &lt;code&gt;fn&lt;/code&gt;。这里它赌回调函数进行了一个封装，就是确保回调函数值执行一次。&lt;/p&gt;
&lt;p&gt;有了这个 &lt;code&gt;thunkify&lt;/code&gt; 函数对异步函数进行封装以后我们就可以向如下使用 &lt;code&gt;Generator&lt;/code&gt; 封装异步任务了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var fs = require(&apos;fs&apos;)
var thunkify = require(&apos;thunkify&apos;)
var readFileThunk = thunkify(fs.readFile)
var gen = function* () {
  var r1 = yield readFileThunk(&apos;/etc/fstab&apos;)
  console.log(r1.toString())
  var r2 = yield readFileThunk(&apos;/etc/shells&apos;)
  console.log(r2.toString())
}

var g = gen()
var r1 = g.next()
r1.value(function (err, data) {
  if (err) throw err
  var r2 = g.next(data)
  r2.value(function (err, data) {
    if (err) throw err
    g.next(data)
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过仔细看这个代码，好像比我们直接执行 &lt;code&gt;fs.readFile()&lt;/code&gt; 复杂多了，而且最后的执行还是个回调嵌套，既不方便，也不优雅，兜了个大圈子还是跟原来一样。但其实我们仔细想一想，这里的回调函数不用像我们平时的嵌套回调一样，把所有的逻辑都写上，我们要做的就两件事，用 &lt;code&gt;next&lt;/code&gt; 将回调结果传递到生成器内（数据处理的逻辑完全可以放到生成器内），同时将执行权交给生成器，当生成器返回一个新的异步任务后我们再重复这个步骤。也就是说每个回调的逻辑都是相同的。&lt;/p&gt;
&lt;p&gt;既然每个回调的逻辑都相同，我们完全可以用递归来实现。比如实现如下这个 &lt;code&gt;run&lt;/code&gt; 函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function run(fn) {
  var gen = fn()
  function next(err, data) {
    var result = gen.next(data)
    if (result.done) return
    result.value(next)
  }
  next()
}
run(gen)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;run&lt;/code&gt; 函数就能实现生成器的自动执行。&lt;code&gt;run&lt;/code&gt; 的参数 &lt;code&gt;fn&lt;/code&gt; 就是生成器函数，内部的逻辑就是用一个 &lt;code&gt;next&lt;/code&gt; 函数进行递归。在 &lt;code&gt;next&lt;/code&gt; 函数中，我们调用生成器的 &lt;code&gt;next&lt;/code&gt; 的方法恢复生成器执行，同时传入上次异步的结果 &lt;code&gt;data&lt;/code&gt;。当生成器返回的时候，我们对返回的对象进行分析，如果 &lt;code&gt;done&lt;/code&gt; 是 &lt;code&gt;true&lt;/code&gt; 就直接 &lt;code&gt;return&lt;/code&gt;。如果 &lt;code&gt;done&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt; 就将 &lt;code&gt;next&lt;/code&gt; 作为回调函数执行返回对象的 &lt;code&gt;value&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以这里的 &lt;code&gt;next&lt;/code&gt; 函数就是我们上面说的，每个回调的逻辑都相同。它只做了两件事，传入 &lt;code&gt;data&lt;/code&gt; 执行生成器的 &lt;code&gt;next&lt;/code&gt;，将执行权交给生成器，在生成器返回新的函数后重复上述步骤，一直到所有异步任务完成，从而实现自动执行。当然这样的自动执行器有一个前提就是，每一个 &lt;code&gt;yield&lt;/code&gt; 后面都是用 &lt;code&gt;thunkify&lt;/code&gt; 封装好的 &lt;code&gt;Thunk&lt;/code&gt; 函数。&lt;/p&gt;
&lt;p&gt;现在我们的异步任务的写法已经和同步任务完全一样了，比如我们要异步读取 &lt;code&gt;n&lt;/code&gt; 个文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var g = function* () {
  var f1 = yield readFileThunk(&apos;fileA&apos;)
  var f2 = yield readFileThunk(&apos;fileB&apos;)
  // ...
  var fn = yield readFileThunk(&apos;fileN&apos;)
}
run(g)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码逻辑非常清晰，看上去就和同步代码一样。&lt;/p&gt;
&lt;p&gt;其实总结一下，要让生成器自动执行最重要的一点就是我们要在异步任务完成之后将执行权还给生成器，同时把异步任务的结果传递到生成器中。&lt;/p&gt;
&lt;h2&gt;co 源码解读&lt;/h2&gt;
&lt;p&gt;其实包装 &lt;code&gt;Thunk&lt;/code&gt; 函数还是有一点麻烦的，有没有其他更好的方法呢？当然可以，除了利用回调函数（&lt;code&gt;thunkify&lt;/code&gt; 本身就是利用回调函数，在回调函数中将执行权交给生成器并且传递异步操作的记过），另一种就是 &lt;code&gt;Promise&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Promise&lt;/code&gt; 的详细介绍请看我的另一篇文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/28/deep-into-promise/&quot; title=&quot;深入 Promise&quot;&gt;深入 Promise&lt;/a&gt;，&lt;code&gt;Promise&lt;/code&gt; 本身就是对回调函数形式的异步任务的一种封装，它让嵌套的异步操作不再是回调地狱，而是以一种接近于同步的形式来编写。所以可以使用回调函数自然能够使用 &lt;code&gt;Promise&lt;/code&gt; 来实现。而且 &lt;code&gt;Promise&lt;/code&gt; 比回调函数更好的是，我们可以直接在生成器内部执行异步任务，只要返回 &lt;code&gt;Promise&lt;/code&gt; 对象即可，因为 &lt;code&gt;Promise&lt;/code&gt; 对象会保存异步任务的状态和结果，我们随时可以取到，而不是像回调函数一样，你错过了就无法再获得这个回调的结果。&lt;/p&gt;
&lt;p&gt;这里我们主要通过研究 &lt;code&gt;co&lt;/code&gt; 模块的源码来理解其中的实现细节。&lt;code&gt;co&lt;/code&gt; 模块是著名程序员 &lt;code&gt;TJ Holowaychuk&lt;/code&gt; 在 &lt;code&gt;2013&lt;/code&gt; 年编写的一个用于 &lt;code&gt;Generator&lt;/code&gt; 自动执行的小工具。最初 &lt;code&gt;co&lt;/code&gt; 是同时支持 &lt;code&gt;Thunk&lt;/code&gt; 函数和 &lt;code&gt;Promise&lt;/code&gt; 的，&lt;code&gt;4.0&lt;/code&gt; 之后只支持 &lt;code&gt;Promise&lt;/code&gt;了，我们这里主要从源码的角度看看它是如何基于 &lt;code&gt;Promise&lt;/code&gt; 实现生成器的自动执行的。&lt;/p&gt;
&lt;p&gt;先看下面这个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const co = require(&apos;./co&apos;)
const fs = require(&apos;fs&apos;)
var gen = function* () {
  var f1 = yield readFile(&apos;/Users/clloz/code/testing/.eslintrc.js&apos;)
  var f2 = yield readFile(&apos;/Users/clloz/code/testing/package.json&apos;)
  console.log(f1.toString()) //正常输出
  console.log(f2.toString()) //正常输出
}
co(gen).then(function () {
  console.log(&apos;Generator 函数执行完成&apos;)
})

function readFile(filename) {
  return new Promise(function (resolve, reject) {
    fs.readFile(filename, function (err, data) {
      resolve()
    })
  })
}
//Generator 函数执行完成
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到，我们直接用 &lt;code&gt;co&lt;/code&gt; 调用生成器函数，就直接自动执行了， 最后返回了一个 &lt;code&gt;Promise&lt;/code&gt; 对象。我们也可以模仿上面的 &lt;code&gt;run&lt;/code&gt; 写一个自动执行的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function run(gen) {
  var g = gen()
  function next(data) {
    var result = g.next(data)
    if (result.done) return result.value
    result.value.then(function (data) {
      next(data)
    })
  }
  next()
}
run(gen)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;co&lt;/code&gt; 其实就是对这个 &lt;code&gt;run&lt;/code&gt; 的扩展，我们可以看一看 &lt;a href=&quot;https://github.com/tj/co/blob/master/index.js&quot; title=&quot;源码&quot;&gt;源码&lt;/a&gt;，源码去掉注释一共不到 &lt;code&gt;130&lt;/code&gt; 行。其中大部分都是一些判断和转 &lt;code&gt;Promise&lt;/code&gt; 代码，&lt;code&gt;co&lt;/code&gt; 函数大概就 &lt;code&gt;40&lt;/code&gt; 行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;co&lt;/code&gt; 函数内的结构是获取除了生成器函数以后的参数，然后返回一个 &lt;code&gt;Promise&lt;/code&gt;，主要的逻辑都在 &lt;code&gt;Promise&lt;/code&gt; 中完成。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function co(gen) {
  var ctx = this
  var args = slice.call(arguments, 1)

  return new Promise(function (resolve, reject) {})
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在返回的 &lt;code&gt;Promise&lt;/code&gt; 中，首先是判断传入的参数是不是一个 &lt;code&gt;Generator&lt;/code&gt; 函数。它的逻辑是，如果第一个参数是一个函数，先用保存的参数执行这个传入的函数，并保存执行结果。然后判断这个执行结果有没有 &lt;code&gt;next&lt;/code&gt; 方法，如果没有直接 &lt;code&gt;resolve&lt;/code&gt; 执行结果。如果有 &lt;code&gt;next&lt;/code&gt; 的方法，那么我们已经获得了生成器，就是 &lt;code&gt;gen&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (typeof gen === &apos;function&apos;) gen = gen.apply(ctx, args)
if (!gen || typeof gen.next !== &apos;function&apos;) return resolve(gen)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意这里 &lt;code&gt;resolve&lt;/code&gt; 前面加了 &lt;code&gt;return&lt;/code&gt;，表示后面的代码都不会执行了.这是一个小技巧，&lt;code&gt;Promise&lt;/code&gt; 默认会执行到 &lt;code&gt;return&lt;/code&gt; 或者到函数结束（如果没有 &lt;code&gt;return&lt;/code&gt;），如果我们不想执行 &lt;code&gt;resolve&lt;/code&gt; 或者 &lt;code&gt;reject&lt;/code&gt; 之后的代码可以在他们之前加上 &lt;code&gt;return&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来是一个 &lt;code&gt;onFulfilled&lt;/code&gt; 函数，这个函数内部用 &lt;code&gt;try ... catch&lt;/code&gt; 来执行 &lt;code&gt;gen.next(res)&lt;/code&gt;，目的就是为了捕获错误，生成器内部的错误也能捕获（如果生成器内部没有定义 &lt;code&gt;catch&lt;/code&gt;，错误会抛到外面），如果抛错就直接 &lt;code&gt;reject(e)&lt;/code&gt;。当生成器抛出的错误被外部的 &lt;code&gt;catch&lt;/code&gt; 捕获，生成器就不会在执行了，其返回的对象的 &lt;code&gt;done&lt;/code&gt; 会变成 &lt;code&gt;true&lt;/code&gt;，相当于生成器执行完毕了，这个我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/31/es6-generator/&quot; title=&quot;ES6 生成器 Generator&quot;&gt;ES6 生成器 Generator&lt;/a&gt; 有详细说明。这里在执行 &lt;code&gt;gen.next&lt;/code&gt; 的时候像生成器内部传入了数据 &lt;code&gt;res&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function onFulfilled(res) {
  var ret
  try {
    ret = gen.next(res)
  } catch (e) {
    return reject(e)
  }
  next(ret)
  return null
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 &lt;code&gt;try ... catch&lt;/code&gt; 执行完 &lt;code&gt;gen.next&lt;/code&gt; 之后是用 &lt;code&gt;yield&lt;/code&gt; 返回的结果作为参数执行了一个 &lt;code&gt;next&lt;/code&gt; 函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function next(ret) {
  if (ret.done) return resolve(ret.value)
  var value = toPromise.call(ctx, ret.value)
  if (value &amp;#x26;&amp;#x26; isPromise(value)) return value.then(onFulfilled, onRejected)
  return onRejected(
    new TypeError(
      &apos;You may only yield a function, promise, generator, array, or object, &apos; +
        &apos;but the following object was passed: &quot;&apos; +
        String(ret.value) +
        &apos;&quot;&apos;
    )
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;next&lt;/code&gt; 函数的 &lt;code&gt;ret&lt;/code&gt; 就是 &lt;code&gt;yield&lt;/code&gt; 返回的结果。&lt;code&gt;next&lt;/code&gt; 方法主要做了以下几件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;判断返回对象的 &lt;code&gt;done&lt;/code&gt; 是不是 &lt;code&gt;true&lt;/code&gt;，如果是 &lt;code&gt;true&lt;/code&gt; 直接 &lt;code&gt;resolve&lt;/code&gt; 返回对象的 &lt;code&gt;value&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果返回对象的 &lt;code&gt;done&lt;/code&gt; 不是 &lt;code&gt;true&lt;/code&gt;，也就是生成器还没有执行完，就将返回对象的 &lt;code&gt;value&lt;/code&gt; 包装成一个 &lt;code&gt;Promise&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;value&lt;/code&gt; 成功包装成一个 &lt;code&gt;Promise&lt;/code&gt;，那么就执行 &lt;code&gt;value.then(onFulfilled, onRejected)&lt;/code&gt;。这里就相当于递归调用 &lt;code&gt;onFulfilled&lt;/code&gt; 方法，实现自动执行。&lt;/li&gt;
&lt;li&gt;如果包装 &lt;code&gt;Promise&lt;/code&gt; 失败，则用 &lt;code&gt;onRejected&lt;/code&gt; 方法进行抛错。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们可以看到，&lt;code&gt;co&lt;/code&gt; 的做法就是在我们上面实现的 &lt;code&gt;run&lt;/code&gt; 函数外面多加了一层，目的就是为了实现错误的抛出和捕获。这里我们也可以看出 &lt;code&gt;Promise&lt;/code&gt; 相比于回调函数的优势，我们不需要思考回调函数什么时候触发，直接丢进 &lt;code&gt;Promise&lt;/code&gt; 就可以了。&lt;code&gt;Promise&lt;/code&gt; 会帮我们保存回调函数的状态和结果，我们用 &lt;code&gt;then&lt;/code&gt; 来获取异步任务的状态和结果。&lt;/p&gt;
&lt;p&gt;这里顺便说一下 &lt;code&gt;onRejected&lt;/code&gt; 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function onRejected(err) {
  var ret
  try {
    ret = gen.throw(err)
  } catch (e) {
    return reject(e)
  }
  next(ret)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它其实是用 &lt;code&gt;gen.throw&lt;/code&gt; 进行抛错，这个方法我们也在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/31/es6-generator/&quot; title=&quot;ES6 生成器 Generator&quot;&gt;ES6 生成器 Generator&lt;/a&gt; 进行了详细说明。简单的来说就是错误如果被生成器内部 &lt;code&gt;catch&lt;/code&gt; 了，那么生成器还能继续执行，生成器会执行到下一个 &lt;code&gt;yield&lt;/code&gt; 然后暂停。如果是被外部的 &lt;code&gt;catch&lt;/code&gt; 捕获了，那么生成器就执行结束了。&lt;/p&gt;
&lt;p&gt;这就是 &lt;code&gt;co&lt;/code&gt; 的主要执行逻辑，当然后面还有一些判断和转换 &lt;code&gt;Promise&lt;/code&gt; 的方法，这里我们说一说比较重要的并发实现。&lt;/p&gt;
&lt;p&gt;对象转 &lt;code&gt;Promise&lt;/code&gt; 方法 &lt;code&gt;objectToPromise(obj)&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function objectToPromise(obj) {
  var results = new obj.constructor()
  var keys = Object.keys(obj)
  var promises = []
  for (var i = 0; i &amp;#x3C; keys.length; i++) {
    var key = keys[i]
    var promise = toPromise.call(this, obj[key])
    if (promise &amp;#x26;&amp;#x26; isPromise(promise)) defer(promise, key)
    else results[key] = obj[key]
  }
  return Promise.all(promises).then(function () {
    return results
  })

  function defer(promise, key) {
    // predefine the key in the result
    results[key] = undefined
    promises.push(
      promise.then(function (res) {
        results[key] = res
      })
    )
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的处理逻辑是，把每一个键值包装成一个 &lt;code&gt;Promise&lt;/code&gt;，这个 &lt;code&gt;Promise&lt;/code&gt; 的 &lt;code&gt;then&lt;/code&gt; 就是将 &lt;code&gt;Promise&lt;/code&gt; 执行成功的结果放入一个新的对象中（这个对象是用传入对象的构造函数构造的一个空对象）。然后把这么多 &lt;code&gt;Promise.then&lt;/code&gt; 放入一个数组然后执行 &lt;code&gt;Promise.all()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这么做的目的是啥呢？就是为了实现并发操作，有时候我们的异步不一定是一个等一个，可能有些任务可以同步执行，这些同步执行的任务如果一个等一个就太浪费时间了，&lt;code&gt;co&lt;/code&gt; 就是通过上面对象的这种处理实现并发的。传入的 &lt;code&gt;obj&lt;/code&gt; 的每一个键值都是一个异步任务，我们创建一个和这个 &lt;code&gt;obj&lt;/code&gt; 同构造函数的空对象 &lt;code&gt;result&lt;/code&gt;，然后把每个键值都包装成一个 &lt;code&gt;Promise&lt;/code&gt;，只有这个 &lt;code&gt;Promise&lt;/code&gt; 完成 &lt;code&gt;resolve(res)&lt;/code&gt; 之后，我们才将 &lt;code&gt;Promise&lt;/code&gt; 的结果 &lt;code&gt;res&lt;/code&gt; 保存到 &lt;code&gt;result&lt;/code&gt; 对象中，键名还是和 &lt;code&gt;obj&lt;/code&gt; 的键名一样。将所有的键值都这么包装完成后，用 &lt;code&gt;Promise.all&lt;/code&gt; 执行这个 &lt;code&gt;Promise&lt;/code&gt; 数组，就实现了并发。&lt;/p&gt;
&lt;p&gt;数组也是同样的逻辑，用 &lt;code&gt;Promise.all&lt;/code&gt; 实现并发。&lt;/p&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>拖动旋转的 3D 骰子效果</title><link>https://clloz.com/blog/spin-dice</link><guid isPermaLink="true">https://clloz.com/blog/spin-dice</guid><pubDate>Wed, 28 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;CSS&lt;/code&gt; 实现一个 &lt;code&gt;3d&lt;/code&gt; 的骰子，然后实现用鼠标拖动旋转的效果。实现的效果如下，可以拖动这个骰子进行旋转。&lt;/p&gt;
&lt;h2&gt;CSS 实现 3D 骰子&lt;/h2&gt;
&lt;p&gt;想要实现一个 &lt;code&gt;3d&lt;/code&gt; 的骰子，肯定是要使用 &lt;code&gt;transform&lt;/code&gt;。关于 &lt;code&gt;transform&lt;/code&gt; 的细节本文就不多讲了，可以参考 &lt;code&gt;MDN&lt;/code&gt; 和 &lt;a href=&quot;https://www.cnblogs.com/xiaohuochai/p/5351477.html&quot; title=&quot;深入理解CSS变形transform(3d)&quot;&gt;深入理解CSS变形transform(3d)&lt;/a&gt;。我们主要讲讲如何实现效果。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTML&lt;/code&gt; 的结构很简单，我们需要一个包含块（最后我们旋转的就是这个包含块），和 &lt;code&gt;6&lt;/code&gt; 个子元素作为骰子的六个面。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;ul id=&quot;dice&quot;&gt;
  &amp;#x3C;li class=&quot;front&quot;&gt;1&amp;#x3C;/li&gt;
  &amp;#x3C;li class=&quot;back&quot;&gt;2&amp;#x3C;/li&gt;
  &amp;#x3C;li class=&quot;right&quot;&gt;3&amp;#x3C;/li&gt;
  &amp;#x3C;li class=&quot;left&quot;&gt;4&amp;#x3C;/li&gt;
  &amp;#x3C;li class=&quot;top&quot;&gt;5&amp;#x3C;/li&gt;
  &amp;#x3C;li class=&quot;bottom&quot;&gt;6&amp;#x3C;/li&gt;
&amp;#x3C;/ul&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;父元素的处理非常简单，主要的属性就是 &lt;code&gt;transform-style: preserve-3d&lt;/code&gt;，因为我们的子元素是在 &lt;code&gt;3d&lt;/code&gt; 空间中的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;ul {
  display: block;
  width: 100px;
  height: 100px;
  margin: 100px auto;
  padding: 0;
  list-style: none;
  transform-style: preserve-3d;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;六个面的 transform&lt;/h2&gt;
&lt;p&gt;在处理 &lt;code&gt;transform&lt;/code&gt; 之前我们用绝对定位把六个面的元素都固定到父元素的 &lt;code&gt;top left&lt;/code&gt; 位置，这样六个面的 &lt;code&gt;transform&lt;/code&gt; 的坐标就都相同了。每个面的 &lt;code&gt;transform&lt;/code&gt; 都不相同，我们需要在脑海中模拟一下从当前位置到目标位置的移动过程。这里需要注意两点，第一点是坐标轴的方向，&lt;code&gt;z&lt;/code&gt; 轴是垂直屏幕向外的的，也就是向外移动是正，向内移动是负，同理 &lt;code&gt;x&lt;/code&gt; 轴是左为正，&lt;code&gt;y&lt;/code&gt; 轴是下为正；第二点就是元素拥有独立的坐标系，而不是共用同一个坐标自，当一个元素发生了旋转，他的坐标系也在旋转。比如我将一个元素以 &lt;code&gt;x&lt;/code&gt; 轴为旋转轴旋转了 &lt;code&gt;180deg&lt;/code&gt;，那么此时他的 &lt;code&gt;z&lt;/code&gt; 轴就不在是&lt;strong&gt;垂直屏幕向外&lt;/strong&gt;，而是&lt;strong&gt;垂直屏幕向内&lt;/strong&gt;的，这一点要注意一下。根据这些规则我们来总结一下各个面需要如何移动，我们以骰子的边长为 &lt;code&gt;100px&lt;/code&gt; 为例。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;front&lt;/code&gt;：沿着 &lt;code&gt;z&lt;/code&gt; 轴向外移动 &lt;code&gt;50px&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;back&lt;/code&gt;：沿着 &lt;code&gt;z&lt;/code&gt; 轴向内移动 &lt;code&gt;50px&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;right&lt;/code&gt;：以 &lt;code&gt;y&lt;/code&gt; 轴为旋转轴顺时针旋转 &lt;code&gt;90deg&lt;/code&gt;，然后向右移动 &lt;code&gt;50px&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;left&lt;/code&gt;：以 &lt;code&gt;y&lt;/code&gt; 轴为旋转轴逆时针旋转 &lt;code&gt;90deg&lt;/code&gt;，然后向左移动 &lt;code&gt;50px&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top&lt;/code&gt;：以 &lt;code&gt;x&lt;/code&gt; 轴为旋转轴顺时针旋转 &lt;code&gt;90deg&lt;/code&gt;，然后向上移动 &lt;code&gt;50px&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bottom&lt;/code&gt;： 以 &lt;code&gt;x&lt;/code&gt; 轴为旋转轴逆时针旋转 &lt;code&gt;90deg&lt;/code&gt;，然后向下移动 &lt;code&gt;50px&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里的顺时针逆时针我个人总结就是从旋转轴的正方向向负方向看，比如 &lt;code&gt;x&lt;/code&gt; 轴是从右往左看，&lt;code&gt;y&lt;/code&gt; 轴是从下网上看，如果方向看反的话，顺时针逆时针也会搞反。这里的旋转需要一点空间想象力，特别是刚刚接触 &lt;code&gt;3d&lt;/code&gt; 的 &lt;code&gt;transform&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;根据上面总结的各个面的移动方式，我们就可以写出我们的代码了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;ul li {
  position: absolute;
  display: block;
  width: 100%;
  height: 100%;
  font-size: 30px;
  color: white;
  line-height: 100px;
  text-align: center;
  backface-visibility: visible;
}
.front {
  background-color: rgba(90, 90, 90, 0.7);
  transform: translateZ(50px);
}
.back {
  background-color: rgba(0, 210, 0, 0.7);
  transform: rotateY(180deg) translateZ(50px);
}
.right {
  background-color: rgba(210, 0, 0, 0.7);
  transform: rotateY(90deg) translateZ(50px);
}
.left {
  background-color: rgba(0, 0, 210, 0.7);
  transform: rotateY(-90deg) translateZ(50px);
}
.top {
  background-color: rgba(210, 210, 0, 0.7);
  transform: rotateX(90deg) translateZ(50px);
}
.bottom {
  background-color: rgba(210, 0, 210, 0.7);
  transform: rotateX(-90deg) translateZ(50px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们就已经得到一个 &lt;code&gt;3d&lt;/code&gt; 的骰子了。我们可以给它一个初始的角度或者加上透视，就能够看到 &lt;code&gt;3d&lt;/code&gt; 的效果。&lt;/p&gt;
&lt;h2&gt;旋转动画&lt;/h2&gt;
&lt;p&gt;在实现拖动旋转之前，我们先做一个旋转动画来了解 &lt;code&gt;3d&lt;/code&gt; 旋转。我们实现将这个骰子立起来，然后进行旋转，效果如下。&lt;/p&gt;
&lt;p&gt;如何实现这样的效果呢，我们要做的就是先将筛子立起来。其实就是以 &lt;code&gt;z&lt;/code&gt; 轴顺时针旋转 &lt;code&gt;45deg&lt;/code&gt;，然后以 &lt;code&gt;x&lt;/code&gt; 轴逆时针旋转 &lt;code&gt;45deg&lt;/code&gt;。最后的旋转方向我们使用 &lt;code&gt;rotate3d(1, 1, 1, ndeg)&lt;/code&gt; 来实现，这里的三个 &lt;code&gt;1&lt;/code&gt; 可以理解成向量，我们的旋转轴就是原点到这个向量的连线，原点默认在中心，而 &lt;code&gt;1，1，1&lt;/code&gt; 的位置就相当于在 &lt;code&gt;xyz&lt;/code&gt; 的坐标系中取点 &lt;code&gt;(1, 1, 1)&lt;/code&gt;（这里注意坐标轴的方向和我们平时数学题中的方向不同），他们的连线就是一个垂直穿过的对角的轴。最后的效果就是一个立起来的骰子沿着垂直方向旋转。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;safari&lt;/code&gt;，&lt;code&gt;firefox&lt;/code&gt; 以及 &lt;code&gt;iOS&lt;/code&gt; 上的 &lt;code&gt;chrome&lt;/code&gt; 都不支持 &lt;code&gt;keyframe&lt;/code&gt; 只写两帧（也就是 &lt;code&gt;from - to&lt;/code&gt; 和 &lt;code&gt;0% - 100%&lt;/code&gt; 的形式）我最终尝试只有 &lt;code&gt;0% 25% 50% 75% 100%&lt;/code&gt; 这种形式能正常工作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;拖动旋转&lt;/h2&gt;
&lt;p&gt;把旋转的原理搞清楚了，实现拖动旋转就非常简单了。我们要做的就是触发 &lt;code&gt;mousemove&lt;/code&gt; 的时候就重新计算我们的 &lt;code&gt;transform&lt;/code&gt; 的值，这个值的计算就根据 &lt;code&gt;mousedown&lt;/code&gt; 时候的 &lt;code&gt;clientX clientY&lt;/code&gt; 和 &lt;code&gt;mousemove&lt;/code&gt; 时候的 &lt;code&gt;clientX clientY&lt;/code&gt; 的差值进行计算，比如移动 &lt;code&gt;10&lt;/code&gt; 个像素就转动一度。这里需要注意的一点是，我们鼠标在垂直方向上移动的距离影响的是 &lt;code&gt;rotateX&lt;/code&gt; 而不是 &lt;code&gt;rotateY&lt;/code&gt;，因为初始方向移动相当于绕着 &lt;code&gt;X&lt;/code&gt; 轴旋转。&lt;/p&gt;
&lt;p&gt;最后就是当 &lt;code&gt;mouseup&lt;/code&gt; 的时候记录当前的 &lt;code&gt;rotateX&lt;/code&gt; 和 &lt;code&gt;rotateY&lt;/code&gt; 的值，让下次点击事件发生的时候从上次结束的状态开始旋转而不是回到初始状态。最后的代码如下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &amp;#x3C;title&gt;Spin Dice&amp;#x3C;/title&gt;
    &amp;#x3C;style&gt;
      ul {
        display: block;
        width: 100px;
        height: 100px;
        margin: 100px auto;
        padding: 0;
        list-style: none;
        /* perspective: 550px; */
        transform-style: preserve-3d;
        /* transition: all 0.3s ease-in; */
        /* animation: spin 5s infinite linear; */
        transform: rotateX(13deg) rotateY(13deg);
      }
      ul li {
        position: absolute;
        display: block;
        width: 100%;
        height: 100%;
        font-size: 30px;
        color: white;
        line-height: 100px;
        text-align: center;
        backface-visibility: visible;
      }
      .front {
        background-color: rgba(90, 90, 90, 0.7);
        transform: translateZ(50px);
      }
      .back {
        background-color: rgba(0, 210, 0, 0.7);
        transform: rotateY(180deg) translateZ(50px);
      }
      .right {
        background-color: rgba(210, 0, 0, 0.7);
        transform: rotateY(90deg) translateZ(50px);
      }
      .left {
        background-color: rgba(0, 0, 210, 0.7);
        transform: rotateY(-90deg) translateZ(50px);
      }
      .top {
        background-color: rgba(210, 210, 0, 0.7);
        transform: rotateX(90deg) translateZ(50px);
      }
      .bottom {
        background-color: rgba(210, 0, 210, 0.7);
        transform: rotateX(-90deg) translateZ(50px);
      }
    &amp;#x3C;/style&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;ul id=&quot;dice&quot;&gt;
      &amp;#x3C;li class=&quot;front&quot;&gt;1&amp;#x3C;/li&gt;
      &amp;#x3C;li class=&quot;back&quot;&gt;2&amp;#x3C;/li&gt;
      &amp;#x3C;li class=&quot;right&quot;&gt;3&amp;#x3C;/li&gt;
      &amp;#x3C;li class=&quot;left&quot;&gt;4&amp;#x3C;/li&gt;
      &amp;#x3C;li class=&quot;top&quot;&gt;5&amp;#x3C;/li&gt;
      &amp;#x3C;li class=&quot;bottom&quot;&gt;6&amp;#x3C;/li&gt;
    &amp;#x3C;/ul&gt;
    &amp;#x3C;script&gt;
      let dice = document.getElementById(&apos;dice&apos;)
      let baseX = 13
      let baseY = 13
      dice.addEventListener(&apos;mousedown&apos;, (e) =&gt; {
        let rotateX = e.clientX
        let rotateY = e.clientY

        let move = (e) =&gt; {
          // console.log(baseX, rotateX, e.clientX);
          // console.log(baseY, rotateY, e.clientY);
          dice.style.transform = `rotateX(${baseX - (((e.clientY - rotateY) / 10) % 360)}deg) rotateY(${
            baseY + (((e.clientX - rotateX) / 10) % 360)
          }deg)`
          // console.log(dice.style.transform);
        }
        let up = (e) =&gt; {
          baseX = baseX - (((e.clientY - rotateY) / 10) % 360)
          baseY = baseY + (((e.clientX - rotateX) / 10) % 360)
          // console.log(baseX, baseY);
          document.removeEventListener(&apos;mousemove&apos;, move)
          document.removeEventListener(&apos;mouseup&apos;, up)
        }
        document.addEventListener(&apos;mousemove&apos;, move)
        document.addEventListener(&apos;mouseup&apos;, up)
      })
      document.addEventListener(&apos;selectstart&apos;, (e) =&gt; e.preventDefault())
    &amp;#x3C;/script&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意要把 &lt;code&gt;CSS&lt;/code&gt; 代码中的 &lt;code&gt;transition&lt;/code&gt; 注释掉，否则影响旋转效果。&lt;/p&gt;</content:encoded><h:img src="/_astro/dice.BlJFPTtz.png"/><enclosure url="/_astro/dice.BlJFPTtz.png"/></item><item><title>前端模块化</title><link>https://clloz.com/blog/js-module</link><guid isPermaLink="true">https://clloz.com/blog/js-module</guid><pubDate>Fri, 23 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文主要讲一讲和前端模块化相关的一些历史和知识&lt;/p&gt;
&lt;h2&gt;为什么要模块化&lt;/h2&gt;
&lt;p&gt;可以看一看这个 &lt;a href=&quot;https://huangxuan.me/2015/07/09/js-module-7day/&quot;&gt;JavaScript 模块化七日谈&lt;/a&gt;，了解一下模块化的发展历史。&lt;/p&gt;
&lt;p&gt;一切工程相关的概念都是为了效率诞生的。如何在多人开发的较大的项目中保持开发的效率以及降低维护成本，就是问题的核心。在前端中 &lt;code&gt;js&lt;/code&gt; 的依赖关系和代码复用就是其中的两个核心问题。如果你只是写一个几十行的小 &lt;code&gt;demo&lt;/code&gt;，并且确定不会复用，你完全不需要任何的模块化和设计模式的概念，就结构化的代码就可以了。比如让你写一个斐波那契数列，你不会想什么模块化，面向对象，一个递归或者循环就可以了。所以一切都是需求决定的，我们也要根据需求选择开发方式，效率是第一位的。 有几个原因：&lt;/p&gt;
&lt;p&gt;语言特性：&lt;code&gt;html/css/js&lt;/code&gt; 都是比较高“容错”的，高自由度的，没有非常明确的规范。比如 &lt;code&gt;html&lt;/code&gt; 的标签是否闭合，大小写都是可兼容的，相同的功能可以用各种形式的标签组合来实现。&lt;code&gt;css&lt;/code&gt; 是全局的并且属性之间非正交，&lt;code&gt;css&lt;/code&gt; 的行为想要搞清楚并不容易。&lt;code&gt;javascript&lt;/code&gt; 作为一个弱类型动态语言，也是相当自由的，你可以定义一堆全局变量结构化地实现你的功能；也可以进行模块化编写易于维护和复用的代码。自由度和容错率高，并且没有一个明确的 &lt;code&gt;好&lt;/code&gt; 的标准，导致前端的代码参差不齐。&lt;/p&gt;
&lt;p&gt;浏览器渲染机制，&lt;code&gt;js&lt;/code&gt; 引擎是单线程的，这主要是因为它主要是用在浏览器上和 &lt;code&gt;UI&lt;/code&gt; 交互而不得不做的选择。这种单线程也意味着，&lt;code&gt;js&lt;/code&gt; 的依赖是线性的，早期的模式其实就可以理解为只有一个很大的 &lt;code&gt;js&lt;/code&gt; 文件，我们的所写的一段段代码在其中依次排开。当代码量上升的时候，几十个 &lt;code&gt;js&lt;/code&gt; 文件之间的依赖关系很难搞清楚，并且大家共享一个作用域，很容易造成全局变量的污染。如何对 &lt;code&gt;js&lt;/code&gt; 进行按需的加载，以及保持各模块独立的作用域就是模块化不断发展的方向。&lt;/p&gt;
&lt;p&gt;模块化需求是随着前端不断发展而产生的，前端应用变得越来越庞大和复杂，自然就因为效率的问题而产生了模块化的需求。&lt;/p&gt;
&lt;h2&gt;模块化的历史&lt;/h2&gt;
&lt;p&gt;在我们刚学习 &lt;code&gt;JavaScript&lt;/code&gt; 的时候，我们都是用一个个函数拼凑出我们的功能，代码大概都是下面这个样子（其实无论学什么语言都是一样，人的思维就是结构化的）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function foo() {
  //...
}
function bar() {
  //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做的缺陷就是所有的函数都是定义在全局中的，如果是多人开发很可能发生冲突。当然这个问题有很多种解决办法，比如将函数都写成一个对象的方法，这样就大大减少了全局中的变量名，减小了冲突的可能性。但是我们定义的方法依然是能在全局访问的，并不足够安全。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var MYAPP = {
  foo: function () {},
  bar: function () {}
}

MYAPP.foo()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面就发展到了以前经常被问到的一个问题 &lt;code&gt;IIEF&lt;/code&gt;，立即执行函数。在 &lt;code&gt;ES6&lt;/code&gt; 之前，&lt;code&gt;JS&lt;/code&gt; 没有块级作用域，函数很多时候就被当成了块级作用域的替代品。在函数外部只能访问我们返回的值，而我们在函数内部定义的变量方法是无法被访问到的。函数的作用相当于产生一个局部作用域。同时我们可以通过 &lt;code&gt;IIEF&lt;/code&gt; 向函数内部传递参数，相当于一种引入依赖的方式，这样做除了保证模块的独立性，还使得模块之间的依赖关系变得明显。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var Module = (function ($) {
  var _$body = $(&apos;body&apos;) // we can use jQuery now!
  var foo = function () {
    console.log(_$body) // 特权方法
  }

  // Revelation Pattern
  return {
    foo: foo
  }
})(jQuery)

Module.foo()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发展到这里的时候我们实际上已经实现了模块化的主要目的，就是将对应的功能封装到一个局部作用于中，我们可以向其中注入依赖，我们也只能使用它返回给我们的内容，而不关心它内部如何实现的。模块就相当于一个黑盒，我们只关心它的输入输出，而不关心它如何实现。&lt;/p&gt;
&lt;p&gt;后面出现的问题就是当项目庞大以后，可能有几十个 &lt;code&gt;js&lt;/code&gt; 文件需要加载，而 &lt;code&gt;DOM&lt;/code&gt; 文档中加载顺序就是执行顺序，这么多文件之间的依赖关系会非常复杂。比如一个文件的执行可能都要依赖好几个文件，那么这些依赖的文件必须在这个文件执行之前加载完毕，这在文件比较少的时候还好处理，但是一旦文件增多依赖关系将错综复杂，很容易出错。如果要对其中的功能进行修改，处理依赖的关系都会花很长时间，而且每一个文件都是一个 &lt;code&gt;HTTP&lt;/code&gt; 请求，文件过多也会造成请求过多。&lt;/p&gt;
&lt;p&gt;为了解决依赖问题产生了很多工具，比如 &lt;code&gt;Yahoo&lt;/code&gt; 的 &lt;code&gt;YUI&lt;/code&gt;，关于 &lt;code&gt;YUI&lt;/code&gt; 的原理可以参考 &lt;a href=&quot;https://www.jb51.net/article/43336.htm&quot; title=&quot;YUI模块开发原理详解&quot;&gt;YUI模块开发原理详解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;进行一个总结，其实模块化的目标就是把页面的功能进行分割，不同的功能之间解耦独立，互不干扰。并且模块能够按需加载，根据模块的依赖进行整合让请求数量变少。但是由于浏览器的特性，很多问题并不能得到根本性的解决，所以后面的模块化思路就跳出浏览器了。这也是现在还在流行的模块化方案。&lt;/p&gt;
&lt;h2&gt;模块化规范&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/module.egrZ0HWD_Z2pGD5K.webp&quot; alt=&quot;module&quot; title=&quot;module&quot;&gt;&lt;/p&gt;
&lt;h2&gt;CommonJS&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CommonJS&lt;/code&gt; 就是如今 &lt;code&gt;NodeJS&lt;/code&gt; 在使用的模块化标准，它的大致使用方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// math.js
exports.add = function (a, b) {
  return a + b
}

// main.js
var math = require(&apos;math&apos;) // ./math in node
console.log(math.add(1, 2)) // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个文件就是一个模块，有自己的作用域。在一个文件里面定义的变量、函数、类，都是私有的，对其他文件不可见。在服务器端，模块的加载是运行时同步加载的；在浏览器端，模块需要提前编译打包处理。所有代码都运行在模块作用域，不会污染全局作用域。模块可以多次加载，但是只会在第一次加载时运行一次，然后运行结果就被缓存了，以后再加载，就直接读取缓存结果。要想让模块再次运行，必须清除缓存。模块加载的顺序，按照其在代码中出现的顺序。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;require&lt;/code&gt; 有一个 &lt;code&gt;main&lt;/code&gt; 属性，其实就是入口文件的意思，和我们在 &lt;code&gt;package.json&lt;/code&gt; 中使用的 &lt;code&gt;main&lt;/code&gt; 字段含义类似，如果该文件是我们直接用 &lt;code&gt;node&lt;/code&gt; 命令运行的，那么 &lt;code&gt;request.main&lt;/code&gt; 的值就是文件对应的 &lt;code&gt;module&lt;/code&gt;。我们可以用 &lt;code&gt;require.main === module&lt;/code&gt; 来判断当前文件是不是直接运行的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;模块导入规则： 假设以下目录为 &lt;code&gt;src/app/index.js&lt;/code&gt; 的文件调用 &lt;code&gt;require()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;以 &lt;code&gt;./moduleA&lt;/code&gt; 相对路径开头，在没有指定后缀名的情况下先去寻找同级目录同级目录：&lt;code&gt;src/app/&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/app/moduleA&lt;/code&gt; 无后缀名文件，按照 &lt;code&gt;javascript&lt;/code&gt; 解析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/app/moduleA.js&lt;/code&gt; js文件，按照 &lt;code&gt;javascript&lt;/code&gt; 解析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/app/moduleA.json&lt;/code&gt; &lt;code&gt;json&lt;/code&gt; 文件，按照 &lt;code&gt;json&lt;/code&gt; 解析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/app/moduleA.node&lt;/code&gt; &lt;code&gt;node&lt;/code&gt; 文件，按照加载的编译插件模块 &lt;code&gt;dlopen&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同级目录没有 &lt;code&gt;moduleA&lt;/code&gt; 文件会去找同级的 &lt;code&gt;moduleA&lt;/code&gt; 目录：&lt;code&gt;src/app/moduleA&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/app/moduleA/package.json&lt;/code&gt; 判断该目录是否有 &lt;code&gt;package.json&lt;/code&gt; 文件， 如果有找到 &lt;code&gt;main&lt;/code&gt; 字段定义的文件返回， 如果 &lt;code&gt;main&lt;/code&gt; 字段指向文件不存在或 &lt;code&gt;main&lt;/code&gt; 字段不存在或 &lt;code&gt;package.json&lt;/code&gt; 文件不存在向下执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/app/moduleA/index.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/app/moduleA/index.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/app/moduleA/index.node&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里讲一讲 &lt;code&gt;module.exports&lt;/code&gt; 和 &lt;code&gt;exports&lt;/code&gt;，在 &lt;code&gt;CommonJS&lt;/code&gt; 中有 &lt;code&gt;module&lt;/code&gt; 模块标识，&lt;code&gt;exports&lt;/code&gt; 模块定义，&lt;code&gt;require&lt;/code&gt; 模块引用。其中 &lt;code&gt;module&lt;/code&gt; 有一个 &lt;code&gt;exports&lt;/code&gt; 属性，该属性最初是一个空对象。而 &lt;code&gt;exports&lt;/code&gt; 是 &lt;code&gt;module.exports&lt;/code&gt; 的一个引用。&lt;code&gt;require&lt;/code&gt; 能看到的只有 &lt;code&gt;module.exports&lt;/code&gt;，它并不能看到 &lt;code&gt;exports&lt;/code&gt;，所以尽量使用 &lt;code&gt;module.exports&lt;/code&gt;，不要使用 &lt;code&gt;exports&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;require&lt;/code&gt; 命令的基本功能是，读入并执行一个 &lt;code&gt;JavaScript&lt;/code&gt; 文件，然后返回该模块的 &lt;code&gt;exports&lt;/code&gt; 对象（这里指的就是 &lt;code&gt;module&lt;/code&gt; 的 &lt;code&gt;exports&lt;/code&gt; 对象）。如果没有发现指定模块，会报错。&lt;/p&gt;
&lt;p&gt;其实 &lt;code&gt;node&lt;/code&gt; 中的模块原理还是和我们前面的立即执行函数一样的，我们的模块中的代码也是被放在一个函数中，而 &lt;code&gt;module&lt;/code&gt; 对象就是传入这个函数的一个参数，然后再把我们的每个模块的输出统一进行保存。当我们 &lt;code&gt;require&lt;/code&gt; 的时候他就到保存的输出中找对应模块的输出即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 准备module对象:
var module = {
  id: &apos;hello&apos;,
  exports: {}
}
var load = function (module) {
  // 读取的模块代码并执行

  module.exports = obj
  // 模块代码结束 返回module.exports
  return module.exports
}
var exported = load(module)
// 保存module:
save(module, exported)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以在模块中我们打印 &lt;code&gt;module.exports&lt;/code&gt; 是一个空对象，其 &lt;code&gt;[[prototype]]&lt;/code&gt; 是 &lt;code&gt;Object.prototype&lt;/code&gt;。我们也可以直接将 &lt;code&gt;module.exports&lt;/code&gt; 赋值为一个基本类型，因为最终保存的知识这个返回值，是不是对象都可以。&lt;/p&gt;
&lt;h2&gt;AMD Async Module Definition&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CommonJS&lt;/code&gt; 规范加载模块是同步的，只有加载完成，才能执行后面的操作。&lt;code&gt;AMD&lt;/code&gt; 规范则是非同步加载模块，允许指定回调函数。由于 &lt;code&gt;Node.js&lt;/code&gt; 主要用于服务器编程，模块文件一般都已经存在于本地硬盘，所以加载起来比较快也很稳定，不用考虑非同步加载的方式，所以 &lt;code&gt;CommonJS&lt;/code&gt; 规范比较适用。但是，如果是浏览器环境，我们需要引用的脚本一般都是直接写到 &lt;code&gt;HTML&lt;/code&gt; 中，而想要实现模块化只能在 &lt;code&gt;JS&lt;/code&gt; 中异步加载脚本。&lt;code&gt;AMD&lt;/code&gt; 就是用来实现这个效果的一种规范。此外 &lt;code&gt;AMD&lt;/code&gt; 规范比 &lt;code&gt;CommonJS&lt;/code&gt; 规范在浏览器端实现更早。&lt;code&gt;AMD&lt;/code&gt; 的实现方案主要是 &lt;code&gt;require.js&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AMD&lt;/code&gt; 的诞生，就是为了解决这两个问题： 1. 实现 &lt;code&gt;js&lt;/code&gt; 文件的异步加载，避免网页失去响应 2. 管理模块之间的依赖性，便于代码的编写和维护&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AMD&lt;/code&gt; (异步模块定义)主要为前端 &lt;code&gt;JS&lt;/code&gt; 的表现指定规范。它采用异步方式加载模块，模块的加载不影响它后面语句的运行。所有依赖这个模块的语句，都定义在一个回调函数中，等到加载完成之后，这个回调函数才会运行。那么这是如何实现的呢，其实就是创建 &lt;code&gt;script&lt;/code&gt; 标签，然后用 &lt;code&gt;onload&lt;/code&gt; 事件来处理回调函数（所以需要一个模块一个文件）。也就是说当我们执行到一个有依赖的函数时，&lt;code&gt;require.js&lt;/code&gt; 会创建依赖的标签，然后浏览器会请求标签，当触发 &lt;code&gt;onload&lt;/code&gt; 之后再利用回调函数执行之前的函数。那么多个依赖如何解决呢，多个依赖是无法确定哪个先加载完哪个后加载完的，虽然不知道 &lt;code&gt;require.js&lt;/code&gt; 是怎么执行，但是想一想可以用一个属性做标记，每触发一个 &lt;code&gt;onload&lt;/code&gt; 标记加一，在每个 &lt;code&gt;onload&lt;/code&gt; 中判断是否全部请求回来了即可。&lt;/p&gt;
&lt;p&gt;下面给出一个 &lt;code&gt;require.js&lt;/code&gt; 的小例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//index.html
&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
    &amp;#x3C;head&gt;
        &amp;#x3C;title&gt;Modular Demo&amp;#x3C;/title&gt;
    &amp;#x3C;/head&gt;
    &amp;#x3C;body&gt;
        &amp;#x3C;!-- 引入require.js并指定js主文件的入口 --&gt;
        &amp;#x3C;script data-main=&quot;./index&quot; src=&quot;require.js&quot;&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;

//index.js
(function () {
    require.config({
        baseUrl: &apos;./&apos;, //基本路径 出发点在根目录下
        paths: {
            //映射: 模块标识名: 路径
            alerter: &apos;./alerter&apos;, //此处不能写成alerter.js,会报错
            dataService: &apos;./dataService&apos;,
        },
    });
    require([&apos;alerter&apos;], function (alerter) {
        alerter.showMsg();
    });
})();

//dataService.js
define(function () {
    let msg = &apos;https://www.clloz.com&apos;;
    function getMsg() {
        return msg.toUpperCase();
    }
    return { getMsg }; // 暴露模块
});

//alerter.js
define([&apos;dataService&apos;], function (dataService) {
    let name = &apos;Tom&apos;;
    function showMsg() {
        alert(dataService.getMsg() + &apos;, &apos; + name);
    }
    // 暴露模块
    return { showMsg };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器打开 &lt;code&gt;index.html&lt;/code&gt; 将能看到打印信息，文件全在同一级目录。我们可以看出 &lt;code&gt;require.js&lt;/code&gt; 在 &lt;code&gt;script&lt;/code&gt; 标签中用一个 &lt;code&gt;data-main&lt;/code&gt; 属性设置了程序的入口，然后用 &lt;code&gt;define&lt;/code&gt; 来创建模块，&lt;code&gt;define&lt;/code&gt; 的第一个参数可以接受一个数组，传入依赖的模块。也有语法糖可以让它换成类似 &lt;code&gt;CommonJS&lt;/code&gt; 的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;define(function (require) {
  var dependency1 = require(&apos;dependency1&apos;),
    dependency2 = require(&apos;dependency2&apos;)

  return function () {}
})

// parse out require...
define([&apos;require&apos;, &apos;dependency1&apos;, &apos;dependency2&apos;], function (require) {
  var dependency1 = require(&apos;dependency1&apos;),
    dependency2 = require(&apos;dependency2&apos;)

  return function () {}
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比较 &lt;code&gt;require.js&lt;/code&gt; 和 &lt;code&gt;CommonJS&lt;/code&gt;，两者一个是用于浏览器，一个是用于 &lt;code&gt;Node&lt;/code&gt;，写法上 &lt;code&gt;CommonJS&lt;/code&gt; 是依赖就近（需要时再写也可以），&lt;code&gt;require.js&lt;/code&gt; 是依赖前置。并且前者是同步加载，而后者是异步加载，在 &lt;code&gt;CommonJS&lt;/code&gt; 中，当执行到 &lt;code&gt;require&lt;/code&gt; 语句的时候才会去加载执行模块，而在 &lt;code&gt;require.js&lt;/code&gt; 中，在执行到 &lt;code&gt;require&lt;/code&gt; 之前依赖的模块就已经加载并执行完毕了，这是浏览器特性所致。&lt;/p&gt;
&lt;p&gt;尽管 &lt;code&gt;AMD&lt;/code&gt; 的设计理念很好，但是与同步加载的模块标准相比其语法更冗长，另外加载的方式不如同步显得清晰，容易造成回调地狱。所以在目前的实际应用中已经越来越少，大多数的开发者还是会选择 &lt;code&gt;CommonJS&lt;/code&gt; 或者 &lt;code&gt;ES6 Module&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;CMD Common Module Definition&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CMD&lt;/code&gt; 规范专门用于浏览器端，模块的加载是异步的，模块使用时才会加载执行。&lt;code&gt;CMD&lt;/code&gt; 规范整合了 &lt;code&gt;CommonJS&lt;/code&gt; 和 &lt;code&gt;AMD&lt;/code&gt; 规范的特点，主要的实现就是 &lt;code&gt;Sea.js&lt;/code&gt;。在 &lt;code&gt;Sea.js&lt;/code&gt; 中，所有 &lt;code&gt;JavaScript&lt;/code&gt; 模块都遵循 &lt;code&gt;CMD&lt;/code&gt; 模块定义规范。看一个 &lt;code&gt;Sea.js&lt;/code&gt; 的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//index.html
;&amp;#x3C;body&gt;
  &amp;#x3C;script type=&apos;text/javascript&apos; src=&apos;sea.js&apos;&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;script type=&apos;text/javascript&apos;&gt;seajs.use(&apos;./index&apos;);&amp;#x3C;/script&gt;
&amp;#x3C;/body&gt;

//index.js
define(function (require) {
  var m1 = require(&apos;./module1&apos;)
  var m4 = require(&apos;./module4&apos;)
  m1.show()
  m4.show()
})

//module1.js
define(function (require, exports, module) {
  //内部变量数据
  var data = &apos;clloz.com&apos;
  //内部函数
  function show() {
    console.log(&apos;module1 show() &apos; + data)
  }
  //向外暴露
  exports.show = show
})

//module2.js
define(function (require, exports, module) {
  module.exports = {
    msg: &apos;I Will Back&apos;
  }
})

//module3.js
define(function (require, exports, module) {
  const API_KEY = &apos;clloz1992&apos;
  exports.API_KEY = API_KEY
})

//module4.js
define(function (require, exports, module) {
  //引入依赖模块(同步)
  var module2 = require(&apos;./module2&apos;)
  function show() {
    console.log(&apos;module4 show() &apos; + module2.msg)
  }
  exports.show = show
  //引入依赖模块(异步)
  require.async(&apos;./module3&apos;, function (m3) {
    console.log(&apos;异步引入依赖模块3  &apos; + m3.API_KEY)
  })
})

//输出
//module1 show() clloz.com
//module4 show() I Will Back
//异步引入依赖模块3  clloz1992
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看出 &lt;code&gt;sea.js&lt;/code&gt; 的语法更接近 &lt;code&gt;CommonJS&lt;/code&gt;，它的设计原则也是就近依赖，在用到某个模块的时候再去 &lt;code&gt;require&lt;/code&gt;。对 &lt;code&gt;CMD&lt;/code&gt; 来说代码在运行时，首先是不知道依赖的，需要遍历所有的 &lt;code&gt;require&lt;/code&gt; 关键字，找出后面的依赖。具体做法是将 &lt;code&gt;function toString&lt;/code&gt; 后，用正则匹配出 &lt;code&gt;require&lt;/code&gt; 关键字后面的依赖。显然，这是一种牺牲性能来换取更多开发便利的方法。所以对比一下两者的实现方式，在模块的加载上并没有什么太大的区别，但是回调函数执行时机不一样的。打个比方有一个模块 &lt;code&gt;a.js&lt;/code&gt; 依赖 &lt;code&gt;require1.js&lt;/code&gt;, &lt;code&gt;require([require1], callback() {})&lt;/code&gt;，代码必然是先执行 &lt;code&gt;a.js&lt;/code&gt;，然后生成一个 &lt;code&gt;src&lt;/code&gt; 为 &lt;code&gt;require1.js&lt;/code&gt; 的 &lt;code&gt;script&lt;/code&gt; 标签 &lt;code&gt;append&lt;/code&gt; 到 &lt;code&gt;body&lt;/code&gt; 上，同时把 &lt;code&gt;a.js&lt;/code&gt; 的 &lt;code&gt;callback&lt;/code&gt; 函数放到全局的一个 &lt;code&gt;module&lt;/code&gt; 对象的对应属性上，这个 &lt;code&gt;module&lt;/code&gt; 对象给每个模块一个 &lt;code&gt;id&lt;/code&gt;，也就是说 &lt;code&gt;a.js&lt;/code&gt; 的 &lt;code&gt;callback&lt;/code&gt; 就在 &lt;code&gt;modules[id].callback&lt;/code&gt;，当 &lt;code&gt;require1.js&lt;/code&gt; 的 &lt;code&gt;onload&lt;/code&gt; 触发之后，就会执行 &lt;code&gt;modules[id].callback()&lt;/code&gt;。大概原理就是这样，&lt;code&gt;AMD&lt;/code&gt; 和 &lt;code&gt;CMD&lt;/code&gt; 的区别就在于，在 &lt;code&gt;AMD&lt;/code&gt; 中，当 &lt;code&gt;onload&lt;/code&gt; 一触发立即执行 &lt;code&gt;modules[id].callback()&lt;/code&gt;，而在 &lt;code&gt;CMD&lt;/code&gt; 中，执行到 &lt;code&gt;require&lt;/code&gt; 才会执行 &lt;code&gt;modules[id].callback()&lt;/code&gt;。这就是他们本质的区别，其他都只是一些语法上的差别，在模块的加载和运行上是没有本质区别的。&lt;/p&gt;
&lt;h2&gt;打包工具&lt;/h2&gt;
&lt;p&gt;在讲 &lt;code&gt;ES6&lt;/code&gt; 的模块之前先说一说打包工具。打包工具是解决模块化的另一个方案，打包工具有很多种，目前最流行的就是 &lt;code&gt;webpack&lt;/code&gt;。打包工具在服务端帮我们做好依赖分析， 然后根据依赖把所有的 &lt;code&gt;js&lt;/code&gt; （&lt;code&gt;webpack&lt;/code&gt; 可以利用 &lt;code&gt;loader&lt;/code&gt; 打包其他类型文件）都打包到一个文件中，同时还能够扩展非常多的功能，包括代码的压缩混淆，代码分割，基础库分离，服务端渲染等等，对于开发有 &lt;code&gt;webpack-dev-server&lt;/code&gt; 这样的热更新机制，压缩混淆后的文件也能够用 &lt;code&gt;source map&lt;/code&gt; 进行调试。有了打包工具，配合 &lt;code&gt;babel&lt;/code&gt;，&lt;code&gt;eslint&lt;/code&gt; 还有各种 &lt;code&gt;loader&lt;/code&gt;，&lt;code&gt;plugin&lt;/code&gt; 就已经能够实现我们之前的模块化目标了。&lt;/p&gt;
&lt;h2&gt;ES6&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 模块化早期叫做 &lt;code&gt;ES Harmony&lt;/code&gt;，现在一般就叫做 &lt;code&gt;ES6&lt;/code&gt; 模块。这很可能成为浏览器和服务端的最终模块化方案，目前的支持已经越来越好，不过还是要借助一些工具和特殊语法。兼容性可以查看 &lt;a href=&quot;https://caniuse.com/mdn-javascript_statements_import&quot; title=&quot;Can I use&quot;&gt;Can I use&lt;/a&gt;。我们平时在 &lt;code&gt;Webpack&lt;/code&gt; 中使用的 &lt;code&gt;export&lt;/code&gt; 和 &lt;code&gt;import&lt;/code&gt;，会经过 &lt;code&gt;Babel&lt;/code&gt; 转换为 &lt;code&gt;CommonJS&lt;/code&gt; 规范。在使用上的差别主要有：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 的模块主要是用 &lt;code&gt;import&lt;/code&gt; 和 &lt;code&gt;export&lt;/code&gt; 两个命令来导入和导出模块。表面上看和 &lt;code&gt;CommonJS&lt;/code&gt; 并没有什么不同，但是实际上它们的原理并不相同。它们的不同点主要有以下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;CommonJS&lt;/code&gt; 是运行时才能确定依赖，进行加载；ES6 模块是编译时输出接口。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommonJs&lt;/code&gt; 是动态语法可以写在判断里（运行时加载），&lt;code&gt;ES6 Module&lt;/code&gt; 静态语法只能写在顶层（编译时加载，有提升机制）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommonJS&lt;/code&gt; 模块输出的是一个值的拷贝（模块就是对象，输入时必须查找对象属性），&lt;code&gt;ES6&lt;/code&gt; 模块不是对象，而是通过 &lt;code&gt;export&lt;/code&gt; 命令显式指定输出的代码，再通过 &lt;code&gt;import&lt;/code&gt; 命令输入。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommonJs&lt;/code&gt; 是单个值导出，&lt;code&gt;ES6&lt;/code&gt; 模块可以导出多个。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommonJs&lt;/code&gt; 的 &lt;code&gt;this&lt;/code&gt; 是当前模块，&lt;code&gt;ES6&lt;/code&gt; 模块的 &lt;code&gt;this&lt;/code&gt; 是 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ES6&lt;/code&gt; 的模块自动采用严格模式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;require()&lt;/code&gt; 是同步加载，后面的代码必须等待这个命令执行完，才会执行。&lt;code&gt;import&lt;/code&gt; 命令则是异步加载，或者更准确地说，&lt;code&gt;ES6&lt;/code&gt; 模块有一个独立的静态解析阶段，依赖关系的分析是在那个阶段完成的，最底层的模块第一个执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;import&lt;/code&gt; 和 &lt;code&gt;export&lt;/code&gt; 的变量或方法名要对应，&lt;code&gt;CommonJS&lt;/code&gt; 的 &lt;code&gt;require&lt;/code&gt; 和 &lt;code&gt;module.exports&lt;/code&gt; 则不必。主要原因就是 &lt;code&gt;ES6&lt;/code&gt; 是根据对应的标识符去模块中找，而 &lt;code&gt;require&lt;/code&gt; 只是到保存模块返回值的对象中找对应的模块。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 模块的设计思想，是尽量的静态化，使得编译时就能确定模块的依赖关系，以及输入和输出的变量。这样当然效率更高，但是相应的就是所有的依赖就要在模块顶部声明，且不能按需加载（将依赖写进判断中），&lt;code&gt;CommonJS&lt;/code&gt; 的 &lt;code&gt;require&lt;/code&gt; 则可以按需加载。为了解决这个问题，也提出了动态 &lt;code&gt;import()&lt;/code&gt; 的概念，不过目前还没有很好的兼容性，兼容性查看 &lt;a href=&quot;https://caniuse.com/es6-module-dynamic-import&quot; title=&quot;Can I use&quot;&gt;Can I use&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;还有一点很重要的区分就是 &lt;code&gt;CommonJS&lt;/code&gt; 中我们输出的其实就是 &lt;code&gt;module.exports&lt;/code&gt; 这个对象，而 &lt;code&gt;ES6&lt;/code&gt; 模块不是对象，而是通过 &lt;code&gt;export&lt;/code&gt; 命令显式指定输出的代码，再通过 &lt;code&gt;import&lt;/code&gt; 命令输入。&lt;/p&gt;
&lt;h3&gt;export&lt;/h3&gt;
&lt;p&gt;一个模块就是一个独立的文件。该文件内部的所有变量，外部无法获取。如果你希望外部能够读取模块内部的某个变量，就必须使用 &lt;code&gt;export&lt;/code&gt; 关键字输出该变量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// profile.js
export var firstName = &apos;Michael&apos;
export var lastName = &apos;Jackson&apos;
export var year = 1958

export { firstName, lastName, year }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码就是模块的输出接口，我们可以看到有两种写法，一种是单独 &lt;code&gt;export&lt;/code&gt; 每一个变量，还有一种是一次 &lt;code&gt;export&lt;/code&gt; 多个变量。尽量使用后一种，这样我们能在脚本中很清晰的了解到输出了哪些变量和方法。&lt;/p&gt;
&lt;p&gt;我们还可以利用 &lt;code&gt;as&lt;/code&gt; 关键字来给输出的变量或者方法重命名，同一个变量可以重命名为不同的分别输出。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function v1() { ... } function v2() { ... }
export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;export&lt;/code&gt; 和 &lt;code&gt;CommonJS&lt;/code&gt; &lt;code&gt;module.exports&lt;/code&gt; 以及 &lt;code&gt;exports&lt;/code&gt; 是完全不同的。它输出的不是一个对象，而只是模块的一个接口，这个接口必须和导出的变量名有一一对应的关系，所以一般我们只有两种写法，一种是在声明前加 &lt;code&gt;export&lt;/code&gt;，另一种就是用大括号将要输出的接口包裹起来。下面的代码中报错的两种写法相当于直接输出常量，不是一个合法的接口。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 报错
export 1;
// 报错
var m = 1; export m;

// 写法一
export var m = 1;
// 写法二
var m = 1; export {m};
// 写法三
var n = 1; export {n as m};

// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实所谓的输出接口不是很好理解，下面一个例子可以帮助大家理解。在 &lt;code&gt;CommonJS&lt;/code&gt; 中，我们引入一个模块的输出的非引用类型的值，这个值就和原来的模块没有关系了，&lt;code&gt;CommonJS&lt;/code&gt; 模块输出的是值的缓存。但是在 &lt;code&gt;ES6 Module&lt;/code&gt; 中我们取到的就是原模块中的值，所以想修改依赖模块中的 &lt;code&gt;const&lt;/code&gt; 变量将会报错。可以看下面的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { foo } from &apos;./export.js&apos;

setTimeout(() =&gt; {
  console.log(foo)
}, 500)
//123
//baz

//export.js
console.log(123)
export let foo = &apos;bar&apos;
setTimeout(() =&gt; (foo = &apos;baz&apos;), 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在依赖的模块 &lt;code&gt;export.js&lt;/code&gt; 中异步修改 &lt;code&gt;foo&lt;/code&gt; 的值，最后我们在执行输出的时候，发现这个值也随着元模块的改变而改变了。使用 &lt;code&gt;CommonJS&lt;/code&gt; 则依然会输出 &lt;code&gt;bar&lt;/code&gt;。当然如果输出的本身是一个引用类型，但让指向的都是同一个对象。&lt;/p&gt;
&lt;p&gt;最后， &lt;code&gt;export&lt;/code&gt; 命令可以出现在模块的任何位置，只要处于模块顶层就可以。如果处于块级作用域内，就会报错，下一节的 &lt;code&gt;import&lt;/code&gt; 命令也是如此。这是因为处于条件代码块之中，就没法做静态优化了，违背了 &lt;code&gt;ES6&lt;/code&gt; 模块的设计初衷。&lt;/p&gt;
&lt;h3&gt;import&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;export&lt;/code&gt; 命令定义了模块的对外接口以后，其他 &lt;code&gt;JS&lt;/code&gt; 文件就可以通过 &lt;code&gt;import&lt;/code&gt; 命令加载这个模块。&lt;code&gt;import&lt;/code&gt; 命令接受一对大括号，里面指定要从其他模块导入的变量名。大括号里面的变量名，必须与被导入模块对外接口的名称相同。和 &lt;code&gt;export&lt;/code&gt; 一样，&lt;code&gt;import&lt;/code&gt; 也支持 &lt;code&gt;as&lt;/code&gt; 关键字，如果你想要一个新的名字可以用 &lt;code&gt;as&lt;/code&gt; 重新命名。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;import&lt;/code&gt; 后面的 &lt;code&gt;from&lt;/code&gt; 指定模块文件的位置，可以是相对路径，也可以是绝对路径， &lt;code&gt;.js&lt;/code&gt; 后缀可以省略。如果只是模块名，不带有路径，那么必须有配置文件， 告诉 &lt;code&gt;JavaScript&lt;/code&gt; 引擎该模块的位置。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;import&lt;/code&gt; 命令具有提升效果，会提升到整个模块的头部，首先执行。比如下面的写法并不会报错，这种行为的本质是， &lt;code&gt;import&lt;/code&gt; 命令是编译阶段执行的，在代码运行之前。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { foo } from &apos;my_module&apos;

foo()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 &lt;code&gt;import&lt;/code&gt; 存在静态分析，所以不能使用表达式和变量，这些只有在运行时才能得到结果的语法结构。下面三种写法都会报错，因为它们用到了表达式、变量和 &lt;code&gt;if&lt;/code&gt; 结构。在静态分析阶段，这些语法都是没法得到值的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 报错
import { &apos;f&apos; + &apos;oo&apos; } from &apos;my_module&apos;;
// 报错
let module = &apos;my_module&apos;;
import { foo } from module;
// 报错
if (x === 1) {
  import { foo } from &apos;module1&apos;;
} else {
  import { foo } from &apos;module2&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;import&lt;/code&gt; 和 &lt;code&gt;require&lt;/code&gt; 都会执行加载的模块。多次执行同一句 &lt;code&gt;import&lt;/code&gt; 或者 &lt;code&gt;require&lt;/code&gt; 都只会执行一次对应的模块。对于同一个模块中输出的不同变量，可以在一条 &lt;code&gt;import&lt;/code&gt; 中引入，也可以分多条引入，他们是等价的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 等同于
import { bar, bar, foo, foo } from &apos;my_module&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;import&lt;/code&gt; 支持用 &lt;code&gt;*&lt;/code&gt; 指定一个对象，所有输出值都加载在这个对象上面。使用 &lt;code&gt;*&lt;/code&gt; 用 &lt;code&gt;as&lt;/code&gt; 指定一个生成的对象名。使用 &lt;code&gt;*&lt;/code&gt; 整体载入的对象，应该是可以静态分析 的，所以不允许运行时改变。下面的写法都是不允许的（一般会提示，&lt;code&gt;The members of xxx are read-only&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import * as circle from &apos;./circle&apos;

// 下面两行都是不允许的
circle.foo = &apos;hello&apos;
circle.area = function () {}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;关于 &lt;code&gt;import&lt;/code&gt; 的 &lt;code&gt;from&lt;/code&gt; 后面引入的模块是不是要加 &lt;code&gt;.js&lt;/code&gt; 后缀的问题，我在 &lt;code&gt;node&lt;/code&gt; 中测试是不加会报错。这一点我也感觉很奇怪。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;export default&lt;/h3&gt;
&lt;p&gt;前面的例子可以看出，和 &lt;code&gt;require&lt;/code&gt; 不一样，我们使用 &lt;code&gt;import&lt;/code&gt; 必须知道对应的模块输出的变量的名字，否则无法进行 &lt;code&gt;import&lt;/code&gt;。不过 &lt;code&gt;ES6 Module&lt;/code&gt; 也提供了一种让我们直接加载模块的方法，就是 &lt;code&gt;export default&lt;/code&gt;，指定模块的默认输出。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;export default&lt;/code&gt; 有几个特别的地方： 1. 我们在 &lt;code&gt;import&lt;/code&gt; 的时候可以自己定义引入的变量名，类似 &lt;code&gt;require&lt;/code&gt;，并且不需要使用大括号。 2. &lt;code&gt;export default&lt;/code&gt; 的变量不会出现在 &lt;code&gt;*&lt;/code&gt; 中。 3. 不能使用 &lt;code&gt;export default let a = 10&lt;/code&gt; 这样的形式，必须先声明变量，再导出 &lt;code&gt;let a; export default a = 10&lt;/code&gt;，函数和类的声明可以直接导出。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;export default&lt;/code&gt; 命令用于指定模块的默认输出。显然，一个模块只能有一个默认输出，因此命令只能使用一次。所以， &lt;code&gt;import&lt;/code&gt; 命令后面才不用加大括号，因为只可能对应一个方法。本质上，&lt;code&gt;export default&lt;/code&gt; 就是输出一个叫做 &lt;code&gt;default&lt;/code&gt; 的变量或方法，然后系统允许你为它取任意名字。所以，下面的写法是有效的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 等同于 export default add;

// app.js
import { default as xxx } from &apos;modules&apos;

// modules.js
function add(x, y) {
  return x * y
}
export { add as default }

// 等同于  import xxx from &apos;modules&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正是因为 &lt;code&gt;export default&lt;/code&gt; 命令其实只是输出一个叫做 &lt;code&gt;default&lt;/code&gt; 的变量，所以它后面不能跟变量声明语句。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样地，因为 &lt;code&gt;export default&lt;/code&gt; 本质是将该命令后面的值，赋给 &lt;code&gt;default&lt;/code&gt; 变量以 后再默认，所以直接将一个值写在 &lt;code&gt;export default&lt;/code&gt; 之后。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 正确
export default 42;
// 报错
export 42;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想在一条 &lt;code&gt;import&lt;/code&gt; 语句中同时引入默认方法可其它接口，可以写成这样：&lt;code&gt;import _, { each, each as forEach } from &apos;lodash&apos;;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果在一个模块之中，先输入后输出同一个模块， &lt;code&gt;import&lt;/code&gt; 语句可以与 &lt;code&gt;export&lt;/code&gt; 语句写在一起。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export { foo, bar } from &apos;my_module&apos;;
// 等同于
import { foo, bar } from &apos;my_module&apos;;
export { foo, bar };

//模块的接口改名和整体输出，也可以采用这种写法。
// 接口改名
export { foo as myFoo } from &apos;my_module&apos;;
// 整体输出
export * from &apos;my_module&apos;;

//默认输出也可以使用这种写法
export { default } from &apos;foo&apos;;

//具名接口输入改为默认接口输出
export { es6 as default } from &apos;./someModule&apos;;
// 等同于
import { es6 } from &apos;./someModule&apos;;
export default es6;

//默认接口输入改为具名接口输出
export { default as es6 } from &apos;./someModule&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有三种语句不具备复合写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import * as someIdentifier from &apos;someModule&apos;
import someIdentifier from &apos;someModule&apos;
import someIdentifier, { namedIdentifier } from &apos;someModule&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;import()&lt;/h3&gt;
&lt;p&gt;动态 &lt;code&gt;import&lt;/code&gt; 的语法是 &lt;code&gt;import()&lt;/code&gt;，静态的 &lt;code&gt;import&lt;/code&gt; 没法像 &lt;code&gt;require&lt;/code&gt; 那样按需加载，写在条件语句中。&lt;code&gt;ES6 Module&lt;/code&gt; 要取代 &lt;code&gt;CommonJS&lt;/code&gt; 这也是必须解决的一个问题，所以提出了动态 &lt;code&gt;import&lt;/code&gt;。比如下面的代码，&lt;code&gt;require&lt;/code&gt; 可以进行动态加载，要加载哪个模块只有运行时才知道。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const path = &apos;./&apos; + fileName
const myModual = require(path)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;import()&lt;/code&gt; 是一个函数，返回一个 &lt;code&gt;Promise&lt;/code&gt; 对象。下面是一个例子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const main = document.querySelector(&apos;main&apos;)
import(`./section-modules/${someVariable}.js`)
  .then((module) =&gt; {
    module.loadPageInto(main)
  })
  .catch((err) =&gt; {
    main.textContent = err.message
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;import()&lt;/code&gt; 函数可以用在任何地方，不仅仅是模块，非模块的脚本也可以使用。它是运行时执行，也就是说，什么时候运行到这一句，也会加载指定的模块。另外，&lt;code&gt;import()&lt;/code&gt; 函数与所加载的模块没有静态连接关系，这点也是与 &lt;code&gt;import&lt;/code&gt; 语句不相同。&lt;code&gt;import()&lt;/code&gt; 类似于 &lt;code&gt;Node&lt;/code&gt; 的 &lt;code&gt;require&lt;/code&gt; 方法，区别主要是前者是异步加载，后者是同步加载。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;import()&lt;/code&gt; 可以用于动态加载，条件加载和动态的模块路径。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;import()&lt;/code&gt; 加载模块成功以后，这个模块会作为一个对象，当作 &lt;code&gt;then&lt;/code&gt; 方法的参 数。因此，可以使用对象解构赋值的语法，获取输出接口。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import(&apos;./myModule.js&apos;).then(({ export1, export2 }) =&gt; {
  // ...·
})

//default
import(&apos;./myModule.js&apos;).then((myModule) =&gt; {
  console.log(myModule.default)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前在浏览器中使用 &lt;code&gt;ES6 Module&lt;/code&gt; 必须要增加一个标签属性 &lt;code&gt;type=&quot;module&quot;&lt;/code&gt;，并且要启动 &lt;code&gt;web&lt;/code&gt; 服务器进行访问，否则会有跨域报错。&lt;code&gt;ES6 Module&lt;/code&gt; 的加载机制和加了 &lt;code&gt;defer&lt;/code&gt; 的标签一样，不会堵塞 &lt;code&gt;DOM&lt;/code&gt; 的解析和渲染，在 &lt;code&gt;DOM&lt;/code&gt; 构建完成，触发 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件以后按照标签的顺序执行对应的模块。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&amp;#x3C;script type=&quot;module&quot; src=&quot;foo.js&quot;&gt;&amp;#x3C;/script&gt;
&amp;#x3C;!-- 等同于 --&gt;
&amp;#x3C;script type=&quot;module&quot; src=&quot;foo.js&quot; defer&gt;&amp;#x3C;/script&gt;
```html

`&amp;#x3C;script&gt;` 标签的 `async` 属性也可以打开，这时只要加载完成，渲染引擎就会中 断渲染立即执行。执行完成后，再恢复渲染。关于 `defer`，`async` 和资源加载的阻塞机制可以参考我的另一篇文章：[浏览器渲染过程及Event Loop浅析](https://www.clloz.com/programming/front-end/js/2019/04/25/how-browser-work-2/ &quot;浏览器渲染过程及Event Loop浅析&quot;)

`ES6` 模块也允许内嵌在网页中，语法行为与加载外部脚本完全一致。

```javascript
&amp;#x3C;script type=&quot;module&quot;&gt;
    import utils from &quot;./utils.js&quot;;
    // other code
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于外部加载的模块脚本，有几点需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码是在模块作用域之中运行，而不是在全局作用域运行。模块内部的顶层变 量，外部不可见。&lt;/li&gt;
&lt;li&gt;模块脚本自动采用严格模式，不管有没有声明&lt;/li&gt;
&lt;li&gt;模块之中，可以使用 &lt;code&gt;import&lt;/code&gt; 命令加载其他模块( &lt;code&gt;.js&lt;/code&gt; 后缀不能省略，需要提供绝对 &lt;code&gt;URL&lt;/code&gt; 或相对 &lt;code&gt;URL&lt;/code&gt;)，也可以使用 &lt;code&gt;export&lt;/code&gt; 命令输出对外接口。&lt;/li&gt;
&lt;li&gt;模块之中，顶层的 &lt;code&gt;this&lt;/code&gt; 关键字返回 ，而不是指向 &lt;code&gt;window&lt;/code&gt; 。也就是说，在模块顶层使用 &lt;code&gt;this&lt;/code&gt; 关键字，是无意义的。&lt;/li&gt;
&lt;li&gt;同一个模块如果加载多次，将只执行一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;严格模式限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;变量必须声明后再使用&lt;/li&gt;
&lt;li&gt;函数的参数不能有同名属性，否则报错&lt;/li&gt;
&lt;li&gt;不能使用 &lt;code&gt;with&lt;/code&gt; 语句&lt;/li&gt;
&lt;li&gt;不能对只读属性赋值，否则报错&lt;/li&gt;
&lt;li&gt;不能使用前缀 &lt;code&gt;0&lt;/code&gt; 表示八进制数，否则报错&lt;/li&gt;
&lt;li&gt;不能删除不可删除的属性，否则报错&lt;/li&gt;
&lt;li&gt;不能删除变量，只能删除对象属性&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eval&lt;/code&gt; 不会在它的外层作用域引入变量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eval&lt;/code&gt; 和 &lt;code&gt;arguments&lt;/code&gt; 不能被重新赋值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arguments&lt;/code&gt; 不会自动反映函数参数的变化&lt;/li&gt;
&lt;li&gt;不能使用 &lt;code&gt;a&apos;r&apos;guments.callee&lt;/code&gt; 和 &lt;code&gt;arguments.caller&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;禁止 &lt;code&gt;this&lt;/code&gt; 指向全局对象&lt;/li&gt;
&lt;li&gt;不能使用 &lt;code&gt;fn.caller&lt;/code&gt; 和 &lt;code&gt;fn.callee&lt;/code&gt; 获取函数的调用堆栈。&lt;/li&gt;
&lt;li&gt;增加了保留字（比如 &lt;code&gt;protected&lt;/code&gt;，&lt;code&gt;static&lt;/code&gt; 和 &lt;code&gt;interface&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;umd 模块（通用模块）&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;;(function (global, factory) {
  typeof exports === &apos;object&apos; &amp;#x26;&amp;#x26; typeof module !== &apos;undefined&apos;
    ? (module.exports = factory())
    : typeof define === &apos;function&apos; &amp;#x26;&amp;#x26; define.amd
      ? define(factory)
      : (global.libName = factory())
})(this, function () {
  &apos;use strict&apos;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你在 &lt;code&gt;js&lt;/code&gt; 文件头部看到这样的代码，那么这个文件使用的就是 &lt;code&gt;UMD Universal Module Definition&lt;/code&gt; 规范。实际上就是 &lt;code&gt;amd + commonjs + 全局变量&lt;/code&gt; 这三种风格的结合。&lt;/p&gt;
&lt;p&gt;这段代码就是对当前运行环境的判断，如果是 &lt;code&gt;Node&lt;/code&gt; 环境 就是使用 &lt;code&gt;CommonJs&lt;/code&gt; 规范， 如果不是就判断是否为 &lt;code&gt;AMD&lt;/code&gt; 环境， 最后导出全局变量。&lt;/p&gt;
&lt;p&gt;有了 &lt;code&gt;UMD&lt;/code&gt; 后我们的代码和同时运行在 &lt;code&gt;Node&lt;/code&gt; 和 浏览器上，所以现在前端大多数的库最后打包都使用的是 &lt;code&gt;UMD&lt;/code&gt; 规范。&lt;/p&gt;
&lt;p&gt;需要注意的问题是，&lt;code&gt;UMD&lt;/code&gt; 模块一般都先判断 &lt;code&gt;AMD&lt;/code&gt; 环境，也就是全局下是否有 &lt;code&gt;defined&lt;/code&gt; 函数，通过 &lt;code&gt;AMD&lt;/code&gt; 定义的模块是无法使用 &lt;code&gt;CommondJS&lt;/code&gt; 或 &lt;code&gt;ES6 Module&lt;/code&gt; 的形式正确引入的。在 &lt;code&gt;webpack&lt;/code&gt; 中由于它同时支持 &lt;code&gt;AMD&lt;/code&gt; 和 &lt;code&gt;CommonJS&lt;/code&gt;，如果你的工程中的所有模块都是 &lt;code&gt;CommonJS&lt;/code&gt;，但是通过了 &lt;code&gt;AMD&lt;/code&gt; 检测（全局有 &lt;code&gt;define&lt;/code&gt; 函数），那么就会使用 &lt;code&gt;AMD&lt;/code&gt; 的方式导出，遇到这样的情况我们可以更改 &lt;code&gt;UMD&lt;/code&gt; 的判断顺序即可。&lt;/p&gt;
&lt;h2&gt;ES6 Module 和 CommonJS 差异&lt;/h2&gt;
&lt;p&gt;上面已经说过一些不同，这里进行一下总结。最主要的两点不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CommonJS&lt;/code&gt; 模块输出的是一个值的拷贝，&lt;code&gt;ES6&lt;/code&gt; 模块输出的是值的引用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommonJS&lt;/code&gt; 模块是运行时加载，&lt;code&gt;ES6&lt;/code&gt; 模块是编译时输出接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先说一说第一个差异，这里主要说的是原始数据类型，对于输出的原始数据类型，&lt;code&gt;require&lt;/code&gt; 进一个新的模块后相当于复制了一个副本，对这个副本的操作不会影响到原来模块中的值，而 &lt;code&gt;import&lt;/code&gt; 则不同。对于引用类型，&lt;code&gt;require&lt;/code&gt; 过来的就是一个对象的引用，所以对对象的修改自然会对被引入模块有影响。&lt;code&gt;import&lt;/code&gt; 对引用类型的引入虽然表现和 &lt;code&gt;require&lt;/code&gt; 类型，但是他们底层的机制是完全不同的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//a.js
const { bar } = require(&apos;./b.js&apos;)

console.log(bar.count)
bar.count++

//b.js
exports.bar = {
  count: 0
}

setTimeout(() =&gt; {
  console.log(exports.bar.count)
}, 1000)

//运行 a.js 得到的结果是 1 2，可见 a.js 中的 bar.count++ 还是传递到了 b.js 中
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二个差异是因为 &lt;code&gt;CommonJS&lt;/code&gt; 加载的是一个对象(即 &lt;code&gt;module.exports&lt;/code&gt;)， 该对象只有在脚本运行完才会生成。而 ES6 模块不是对象，它的对外接口只是一种静态定义，在代码静态解析阶段就会生成。&lt;/p&gt;
&lt;p&gt;第一个差异是 &lt;code&gt;ES6&lt;/code&gt; 模块的运行机制与 &lt;code&gt;CommonJS&lt;/code&gt; 不一样。**&lt;code&gt;JS&lt;/code&gt; 引擎对脚本静态分析的时候，遇到模块加载命令 &lt;code&gt;import&lt;/code&gt; ，就会生成一个只读引用。等到脚本真正执行时，再根据这个只读引用，到被加载的那个模块里面去取值。**换句话说，&lt;code&gt;ES6&lt;/code&gt; 的 &lt;code&gt;import&lt;/code&gt; 有点 像 &lt;code&gt;Unix&lt;/code&gt; 系统的“符号连接”，原始值变了， &lt;code&gt;import&lt;/code&gt; 加载的值也会跟着变。因此， &lt;code&gt;ES6&lt;/code&gt; 模块是动态引用，并且不会缓存值，模块里面的变量绑定其所在的模块。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// main.js
import { counter, incCounter } from &apos;./lib&apos;

// lib.js
export let counter = 3
export function incCounter() {
  counter++
}

console.log(counter) // 3
incCounter()
console.log(counter) // 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 模块不会缓存运行结果，而是动态地去被加载的模块取值，并且变量总是绑定其所在的模块。由于 &lt;code&gt;ES6&lt;/code&gt; 输入的模块变量，只是一个“符号连接”，所以这个变量是只读的，对它进行重新赋值会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// main.js
import { obj } from &apos;./lib&apos;

// lib.js
export let obj = {}

obj.prop = 123 // OK
obj = {} // TypeError: Assignment to constant variable.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中， &lt;code&gt;main.js&lt;/code&gt; 从 &lt;code&gt;li&apos;b.js&lt;/code&gt; 输入变量 &lt;code&gt;obj&lt;/code&gt; ，可以对 &lt;code&gt;obj&lt;/code&gt; 添加属性，但是重新赋值就会报错。因为变量 &lt;code&gt;obj&lt;/code&gt; 指向的地址是只读的，不能重新赋值，这就好比 &lt;code&gt;main.js&lt;/code&gt; 创造了一个名为 &lt;code&gt;obj&lt;/code&gt; 的 &lt;code&gt;const&lt;/code&gt; 变量。&lt;/p&gt;
&lt;p&gt;最后， &lt;code&gt;export&lt;/code&gt; 通过接口，输出的是同一个值。不同的脚本加载这个接口，得到的都是同样的实例。&lt;/p&gt;
&lt;h2&gt;在 &lt;code&gt;node&lt;/code&gt; 中使用 &lt;code&gt;ES6&lt;/code&gt; 模块&lt;/h2&gt;
&lt;p&gt;目前在 &lt;code&gt;NodeJS&lt;/code&gt; 中直接使用 &lt;code&gt;ES6&lt;/code&gt; 有两种方式，一种是在 &lt;code&gt;package.json&lt;/code&gt; 中添加字段 &lt;code&gt;type=&quot;module&quot;&lt;/code&gt;，另一种是设置文件后缀为 &lt;code&gt;mjs&lt;/code&gt;。模块寻找顺序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import &apos;./foo&apos;
// 依次寻找
// ./foo.js
// ./foo/package.json
// ./foo/index.js

import &apos;baz&apos;

// 依次寻找
// ./node_modules/baz.js
// ./node_modules/baz/package.json
// ./node_modules/baz/index.js
// 寻找上一级目录
// ../node_modules/baz.js
// ../node_modules/baz/package.json
// ../node_modules/baz/index.js
// 再上一级目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 &lt;code&gt;import&lt;/code&gt; 加载 &lt;code&gt;CommonJS&lt;/code&gt; 模块的时候 &lt;code&gt;module.exports&lt;/code&gt; 会被当做 &lt;code&gt;export default&lt;/code&gt; 处理，如果使用 &lt;code&gt;*&lt;/code&gt; 则会在新的变量名上添加一个 &lt;code&gt;default&lt;/code&gt; 属性指向 &lt;code&gt;module.exports&lt;/code&gt;。比如 &lt;code&gt;import * as clloz from &apos;xxx&apos;&lt;/code&gt;，则 &lt;code&gt;clloz.default&lt;/code&gt; 即指向 &lt;code&gt;module.exports&lt;/code&gt;。&lt;code&gt;CommonJS&lt;/code&gt; 模块的输出缓存机制，在 &lt;code&gt;ES6&lt;/code&gt; 加载方式下依然有效。使用 &lt;code&gt;import&lt;/code&gt; 引入 &lt;code&gt;CommonJS&lt;/code&gt; 的模块的时候只能使用 &lt;code&gt;default&lt;/code&gt; 或者 &lt;code&gt;*&lt;/code&gt; 的形式，不能使用 &lt;code&gt;import {readfile} from &apos;fs&apos;;&lt;/code&gt; 这种形式。&lt;/p&gt;
&lt;p&gt;采用 &lt;code&gt;require&lt;/code&gt; 命令加载 &lt;code&gt;ES6&lt;/code&gt; 模块时，&lt;code&gt;ES6&lt;/code&gt; 模块的所有输出接口，会成为输入对象的属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// es.js
let foo = { bar: &apos;my-default&apos; }
export default foo
foo = null

// cjs.js
const es_namespace = require(&apos;./es&apos;)
console.log(es_namespace.default)
// {bar:&apos;my-default&apos;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中， 接口变成了 &lt;code&gt;es_namespace.default&lt;/code&gt; 属性。另外，由于存在缓存机制，&lt;code&gt;e&apos;s.js&lt;/code&gt; 对 &lt;code&gt;foo&lt;/code&gt; 的重新赋值没有在模块外部反映出来。&lt;/p&gt;
&lt;h2&gt;循环加载&lt;/h2&gt;
&lt;p&gt;循环加载(&lt;code&gt;circular dependency&lt;/code&gt;)指的是， &lt;code&gt;a&lt;/code&gt; 脚本的执行依赖 &lt;code&gt;b&lt;/code&gt; 脚本，而 &lt;code&gt;b&lt;/code&gt; 脚本的执行又依赖 &lt;code&gt;a&lt;/code&gt; 脚本。&lt;code&gt;CommonJS&lt;/code&gt; 和 &lt;code&gt;ES6&lt;/code&gt;，处理循环加载的方法是不一样的，返回的结果也不一样。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CommonJS&lt;/code&gt; 上面也已经讲过其原理，一个模块就是一个脚本文件。 &lt;code&gt;require&lt;/code&gt; 命令第一次加载该脚本， 就会执行整个脚本，然后在内存生成一个对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    id: &apos;...&apos;,
    exports: { ... },
    loaded: true,
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该对象的 &lt;code&gt;id&lt;/code&gt; 属性是模块名， &lt;code&gt;exports&lt;/code&gt; 属性是模块输出的各个接口， &lt;code&gt;loaded&lt;/code&gt; 属性是一个布尔值，表示该模块的脚本是否执行完毕。其他还有很多属性，这里都省略了。以后需要用到这个模块的时候，就会到 &lt;code&gt;exports&lt;/code&gt; 属性上面取值。即使再次执 行 &lt;code&gt;require&lt;/code&gt; 命令，也不会再次执行该模块，而是到缓存之中取值。也就是说， &lt;code&gt;CommonJS&lt;/code&gt; 模块无论加载多少次，都只会在第一次加载时运行一次，以后再加载， 就返回第一次运行的结果，除非手动清除系统缓存。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CommonJS&lt;/code&gt; 模块的重要特性是加载时执行，即脚本代码在 &lt;code&gt;require&lt;/code&gt; 的时候，就会全部执行。一旦出现某个模块被&quot;循环加载&quot;，就只输出已经执行的部分，还未执行的部分不会输出。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//a.js
exports.done = false
var b = require(&apos;./b.js&apos;)
console.log(&apos;在 a.js 之中，b.done = %s&apos;, b.done)
exports.done = true
console.log(&apos;a.js 执行完毕&apos;)

//b.js
exports.done = false
var a = require(&apos;./a.js&apos;)
console.log(&apos;在 b.js 之中，a.done = %s&apos;, a.done)
exports.done = true
console.log(&apos;b.js 执行完毕&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的例子是 &lt;code&gt;NodeJS&lt;/code&gt; 官方给出的例子，当执行 &lt;code&gt;a&lt;/code&gt; 时执行到 &lt;code&gt;require b&lt;/code&gt; 会立即执行 &lt;code&gt;b&lt;/code&gt;，在 &lt;code&gt;b&lt;/code&gt; 中执行到 &lt;code&gt;require a&lt;/code&gt; 的时候只会直接返回当前 &lt;code&gt;a&lt;/code&gt; 已经执行的部分，系统也只能取到第一个 &lt;code&gt;exports.done = false&lt;/code&gt; 交给 &lt;code&gt;b&lt;/code&gt;，然后 &lt;code&gt;b&lt;/code&gt; 继续执行，执行完之后再把执行权交给 &lt;code&gt;a&lt;/code&gt;。如果我们这里连 &lt;code&gt;exports.done&lt;/code&gt; 都没有，那么 &lt;code&gt;b&lt;/code&gt; 拿到的就是一个空对象，因为实际上取得就是 &lt;code&gt;module.exports&lt;/code&gt; 对象。&lt;/p&gt;
&lt;p&gt;执行下面的 &lt;code&gt;main.js&lt;/code&gt; 最后的输出如下 ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//main.js
var a = require(&apos;./a.js&apos;)
var b = require(&apos;./b.js&apos;)
console.log(&apos;在 main.js 之中, a.done=%s, b.done=%s&apos;, a.done, b.done)

//在 b.js 之中，a.done = false
//b.js 执行完毕
//在 a.js 之中，b.done = true
//a.js 执行完毕
//在 main.js 之中, a.done=true, b.done=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码证明了两件事。一是，在 &lt;code&gt;b.js&lt;/code&gt; 之中， &lt;code&gt;a.js&lt;/code&gt; 没有执行完毕，只执行了第一行。二是， &lt;code&gt;main.js&lt;/code&gt; 执行到第二行时，不会再次执行 &lt;code&gt;b.js&lt;/code&gt; ，而是输出缓存的 &lt;code&gt;b.js&lt;/code&gt; 的执行结果（每个模块只会执行一次），即它的第四行。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 处理“循环加载”与 &lt;code&gt;CommonJS&lt;/code&gt; 有本质的不同。&lt;code&gt;ES6&lt;/code&gt; 模块是动态引用，如果使用 &lt;code&gt;import&lt;/code&gt; 从一个模块加载变量(即 &lt;code&gt;import foo from &apos;foo&apos;&lt;/code&gt; )，那些变量不会被缓存，而是成为一个指向被加载模块的引用，需要开发者自己保证，真正取值的时候能够取到值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// a.js如下

// b.js
import { foo } from &apos;./a.js&apos;
import { bar } from &apos;./b.js&apos;

console.log(&apos;a.js&apos;)
console.log(bar)
export let foo = &apos;foo&apos;

console.log(&apos;b.js&apos;)
console.log(foo)
export let bar = &apos;bar&apos;

//执行a.js 后输出如下
//b.js
//undefined
//a.js
//bar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行 &lt;code&gt;a.js&lt;/code&gt;，第一行就是 &lt;code&gt;import&lt;/code&gt; 加载 &lt;code&gt;b.js&lt;/code&gt;，所以进入会执行 &lt;code&gt;b.js&lt;/code&gt;。&lt;code&gt;b.js&lt;/code&gt; 的第一行是加载 &lt;code&gt;a.js&lt;/code&gt;，这时由于 &lt;code&gt;a.js&lt;/code&gt; 已经执行，所以不会把执行权交给 &lt;code&gt;a&lt;/code&gt;，而是继续执行 &lt;code&gt;b&lt;/code&gt;，此时 &lt;code&gt;b&lt;/code&gt; 中的 &lt;code&gt;import&lt;/code&gt; 相当于记录了 &lt;code&gt;a&lt;/code&gt; 中的一个接口 &lt;code&gt;foo&lt;/code&gt;，等到代码调用到这个接口的时候，会到 &lt;code&gt;a&lt;/code&gt; 的执行环境中去找这个接口对应的值。当执行到 &lt;code&gt;console.log(foo)&lt;/code&gt; 的时候，引擎会去找 &lt;code&gt;a&lt;/code&gt; 中有没有 &lt;code&gt;export&lt;/code&gt; 的 &lt;code&gt;foo&lt;/code&gt;（注意，此时引擎肯定已经完成了对 &lt;code&gt;a&lt;/code&gt; 的静态分析和执行环境的生成，也就是变量提升都完成了），如果我们在 &lt;code&gt;a&lt;/code&gt; 中的 &lt;code&gt;foo&lt;/code&gt; 是用 &lt;code&gt;let&lt;/code&gt; 声明的，那么这里会报错 &lt;code&gt;ReferenceError: Cannot access &apos;foo&apos; before initialization&lt;/code&gt;，因为 &lt;code&gt;let&lt;/code&gt; 的暂时性死区。如果是用 &lt;code&gt;var&lt;/code&gt; 声明的，那么我们会获得 &lt;code&gt;undefined&lt;/code&gt;。执行完 &lt;code&gt;b&lt;/code&gt; 之后回到 &lt;code&gt;a&lt;/code&gt; 执行，一切恢复正常。&lt;/p&gt;
&lt;p&gt;这里我总结一下，当一个模块开始执行，那么它的静态分析和执行上下文（即环境记录 &lt;code&gt;environment records&lt;/code&gt; 已经生成，新标准中称为环境记录，你也可以用以前的变量对象活动对象理解，即变量声明提升已经完成）肯定都构建完成了，而当我们执行到 &lt;code&gt;import&lt;/code&gt; 的时候主要就是做两件事，一件就是去执行引入的模块（如果模块正在执行栈中就略过这一步），另一件事就是将引入的接口保存，比如 &lt;code&gt;a&lt;/code&gt; 中的 &lt;code&gt;bar&lt;/code&gt;，这一步不会去访问这个 &lt;code&gt;bar&lt;/code&gt; 是什么，只是记住有这个 &lt;code&gt;bar&lt;/code&gt;，下面调用这个 &lt;code&gt;bar&lt;/code&gt; 的时候才会去 &lt;code&gt;b&lt;/code&gt; 的环境记录中去取。如果你对变量提升的细节不清楚，可以看我的另一篇文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/01/variable-hoist/&quot; title=&quot;var，let，const和变量提升（hoist）&quot;&gt;var，let，const和变量提升（hoist）&lt;/a&gt;，主要要注意的就是每一个模块都是一个单独的环境，在浏览器中我们只有一个全局环境，和块级作用域，函数作用域，在 &lt;code&gt;node&lt;/code&gt; 中每个模块都会是一个单的的作用域，&lt;code&gt;var&lt;/code&gt; 声明的全局变量也只在这个模块下生效（注意 &lt;code&gt;es6 module&lt;/code&gt; 自动运行在严格模式下，模块环境下 &lt;code&gt;this&lt;/code&gt; 指向 &lt;code&gt;undefined&lt;/code&gt;）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里我之前对模块执行完之后垃圾回收有点疑问，不过我后来的理解就是模块也是一个单独的环境，就是比原来浏览器环境的 &lt;code&gt;js&lt;/code&gt; 多了一个模块级作用域，垃圾回收机制还是一样，只要存在引用就不会回收。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;再来一个复杂一点的例子，求执行 &lt;code&gt;a.js&lt;/code&gt; 的输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// a.js

// b.js
import { foo } from &apos;./a.js&apos;
import { bar } from &apos;./b.js&apos;

export function foo() {
  console.log(&apos;foo&apos;)
  bar()
  console.log(&apos;执行完毕&apos;)
}
foo()

export function bar() {
  console.log(&apos;bar&apos;)
  if (Math.random() &gt; 0.5) {
    foo()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这段代码使用的 &lt;code&gt;require&lt;/code&gt; 那么肯定会报错，因为 &lt;code&gt;b&lt;/code&gt; 中的 &lt;code&gt;foo&lt;/code&gt; 是 &lt;code&gt;undefined&lt;/code&gt;，执行 &lt;code&gt;foo&lt;/code&gt; 肯定会报错（当然，如果没有进入 &lt;code&gt;bar&lt;/code&gt; 中执行 &lt;code&gt;foo&lt;/code&gt; 的逻辑就不会报错，&lt;code&gt;50%&lt;/code&gt; 的概率。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//a.js
const { bar } = require(&apos;./b.js&apos;)
function foo() {
  console.log(&apos;foo&apos;)
  bar()
  console.log(&apos;执行完毕&apos;)
}
foo()
module.exports.foo = foo

//b.js
const { foo } = require(&apos;./a.js&apos;)
function bar() {
  console.log(&apos;bar&apos;)
  if (Math.random() &gt; 0.5) {
    console.log(foo)
    foo()
  }
}
module.exports.bar = bar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在 &lt;code&gt;ES6&lt;/code&gt; 的模块机制中这段代码使能执行的。为什么呢，其实我们就把 &lt;code&gt;ES6&lt;/code&gt; 中的 &lt;code&gt;import&lt;/code&gt; 看得简单一点，&lt;code&gt;import&lt;/code&gt; 语句的功能就是执行对应模块，然后留下一个接口。**最关键的地方就是当这个接口被使用时，才会去对应的模块 &lt;code&gt;export&lt;/code&gt; 的变量方法中找这个接口对应的变量或方法，记住这个接口只是对模块的一个引用。**这是我个人的理解。&lt;/p&gt;
&lt;p&gt;把这个理解带入到代码中就能得到，&lt;code&gt;a&lt;/code&gt; 中先执行 &lt;code&gt;import b&lt;/code&gt;，会立即执行 &lt;code&gt;b&lt;/code&gt; 同时添加一个 &lt;code&gt;b&lt;/code&gt; 中的引用 &lt;code&gt;bar&lt;/code&gt;（注意此时无所谓这个引用是什么，只有下面的语句执行用到 &lt;code&gt;bar&lt;/code&gt; 的时候才会去到 &lt;code&gt;b&lt;/code&gt; 里面找有没有 &lt;code&gt;export&lt;/code&gt; 的 &lt;code&gt;bar&lt;/code&gt;）。执行 &lt;code&gt;b&lt;/code&gt; 的过程也一样，第一句是 &lt;code&gt;import a&lt;/code&gt;，和上一个例子一样，此时 &lt;code&gt;a&lt;/code&gt; 已经在执行了，所以这一句只是添加一个叫 &lt;code&gt;foo&lt;/code&gt; 的指向 &lt;code&gt;a&lt;/code&gt; 的引用。&lt;code&gt;b&lt;/code&gt; 执行完之后回到执行权交回 &lt;code&gt;a&lt;/code&gt;，在 &lt;code&gt;a&lt;/code&gt; 中执行到 &lt;code&gt;foo()&lt;/code&gt; 会执行 &lt;code&gt;foo&lt;/code&gt; 方法，&lt;code&gt;foo&lt;/code&gt; 方法内部再调用 &lt;code&gt;bar&lt;/code&gt;，此时就会去 &lt;code&gt;b&lt;/code&gt; 中找有没有 &lt;code&gt;export&lt;/code&gt; 的 &lt;code&gt;bar&lt;/code&gt;，找到后执行 &lt;code&gt;bar&lt;/code&gt;。执行 &lt;code&gt;bar&lt;/code&gt; 的过程一样，，不过这里加了个随机数，如果执行到 &lt;code&gt;foo&lt;/code&gt; 就会去 &lt;code&gt;a&lt;/code&gt; 中找有没有 &lt;code&gt;export&lt;/code&gt; 的 &lt;code&gt;foo&lt;/code&gt;，所以这段代码的执行次数是不确定的，最少两次，多的话多少次都有可能。&lt;/p&gt;
&lt;p&gt;其实我们还可以再深入一点，如果在 &lt;code&gt;b&lt;/code&gt; 中直接加一句 &lt;code&gt;foo()&lt;/code&gt; 会怎么样呢？我们将代码变成如下情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//a.js

// b.js
import { foo } from &apos;./a.js&apos;
import { bar } from &apos;./b.js&apos;

export function foo() {
  console.log(&apos;foo&apos;)
  console.log(&apos;执行完毕&apos;)
}

export function bar() {
  console.log(&apos;bar&apos;)
}
foo()

//执行结果
//foo
//bar
//执行完毕
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以发现，在 &lt;code&gt;b&lt;/code&gt; 中直接执行 &lt;code&gt;foo()&lt;/code&gt; 依然能够正常执行，虽然此时 &lt;code&gt;a&lt;/code&gt; 中只执行到第一句 &lt;code&gt;import&lt;/code&gt;。我们再深入一点，我们在 &lt;code&gt;a&lt;/code&gt; 中 &lt;code&gt;export&lt;/code&gt; 出各种不同的数据类型看看会有什么现象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//a.js

// b.js
import { m, name, obj, str } from &apos;./a.js&apos;
import { bar } from &apos;./b.js&apos;

bar()

export function m() {
  return {
    name: &apos;clloz&apos;
  }
}
var name = m()
export let str = &apos;clloz&apos;
export let obj = {}
export { name }

export function bar() {
  console.log(&apos;bar&apos;)
}
console.log(m) //[Function: m]
console.log(str) //ReferenceError: Cannot access &apos;str&apos; before initialization
console.log(obj) //ReferenceError: Cannot access &apos;obj&apos; before initialization
console.log(name) //undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面 &lt;code&gt;m&lt;/code&gt; 是一个函数声明，&lt;code&gt;str&lt;/code&gt; 是一个 &lt;code&gt;let&lt;/code&gt; 声明的字符串，&lt;code&gt;obj&lt;/code&gt; 是一个 &lt;code&gt;let&lt;/code&gt; 声明的对象，&lt;code&gt;name&lt;/code&gt; 是一个 &lt;code&gt;var&lt;/code&gt; 声明的变量，它的值是执行 &lt;code&gt;m&lt;/code&gt; 后返回的对象。最后在 &lt;code&gt;b&lt;/code&gt; 中的执行结果看上面的代码，&lt;code&gt;m&lt;/code&gt; 成功输出 &lt;code&gt;a&lt;/code&gt; 中的 &lt;code&gt;m&lt;/code&gt; 函数；&lt;code&gt;str&lt;/code&gt; 和 &lt;code&gt;obj&lt;/code&gt; 都是 &lt;code&gt;ReferenceError&lt;/code&gt;，&lt;code&gt;name&lt;/code&gt; 是 &lt;code&gt;undefined&lt;/code&gt;。看出来了没有，他们都是根据变量提升的规则获得了对应的值。&lt;/p&gt;
&lt;p&gt;当执行 &lt;code&gt;b&lt;/code&gt; 的时候，模块 &lt;code&gt;a&lt;/code&gt; 中的代码其实只执行了一句，就是 &lt;code&gt;import&lt;/code&gt; 语句，但此时模块内的变量提升肯定已经完成了（变量提升在执行之前），关于变量提升的规则这里不细讲了，不清楚的同学看另一片文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/01/variable-hoist/&quot; title=&quot;var，let，const和变量提升（hoist）&quot;&gt;var，let，const和变量提升（hoist）&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;这里我补充一个我发现的循环依赖的问题，是关于 &lt;code&gt;export default&lt;/code&gt; 的。看下面这个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//a.mjs
import bar from &apos;./b.mjs&apos;;

function foo(invoker) {
    console.log(invoker + &apos; invokes foo.js&apos;);
    bar(&apos;foo.js&apos;);
}

foo(&apos;index.js&apos;);
export default foo;

//b.mjs
import foo from &apos;./a.mjs&apos;;
let invoked = false;
// console.log(foo);
function bar(invoker) {
    if (!invoked) {
        invoked = true;
        console.log(invoker + &apos; invokes bar.js&apos;);
        foo(&apos;bar.js&apos;);
    }
}
export default bar;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行 &lt;code&gt;a.mjs&lt;/code&gt;，在执行到 &lt;code&gt;bar&lt;/code&gt; 函数中调用 &lt;code&gt;foo&lt;/code&gt; 的地方报错：&lt;code&gt;ReferenceError: Cannot access &apos;foo&apos; before initialization&lt;/code&gt;。把 &lt;code&gt;a.mjs&lt;/code&gt; 中的 &lt;code&gt;foo()&lt;/code&gt; 调用移动到 &lt;code&gt;export default&lt;/code&gt; 之后能正常运行，或者把 &lt;code&gt;export default&lt;/code&gt; 改成普通的 &lt;code&gt;export {}&lt;/code&gt; 形式也可以。&lt;/p&gt;
&lt;p&gt;这里我猜想是不是 &lt;code&gt;export default&lt;/code&gt; 也只是把后面的值传给 &lt;code&gt;default&lt;/code&gt;（假想成一个变量之类的），然后这个 &lt;code&gt;default&lt;/code&gt; 类似于 &lt;code&gt;let&lt;/code&gt; 有暂时性死区，只有执行到 &lt;code&gt;export default&lt;/code&gt; 这一句时才给 &lt;code&gt;default&lt;/code&gt; 赋值，所以会造成上面的现象。我们在 &lt;code&gt;b&lt;/code&gt; 中加一句 &lt;code&gt;console.log(foo);&lt;/code&gt; 的话，即使把 &lt;code&gt;foo()&lt;/code&gt; 移动到 &lt;code&gt;export default&lt;/code&gt; 下面，也会报错，因为执行 &lt;code&gt;b.mjs&lt;/code&gt; 的时候，&lt;code&gt;export default&lt;/code&gt; 还没有执行，这也印证了我的想法。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;通过这几个例子，&lt;code&gt;ES6&lt;/code&gt; 的模块规则其实已经非常清晰了。当我们执行 &lt;code&gt;import&lt;/code&gt; 的时候，会做两件事：执行 &lt;code&gt;import&lt;/code&gt; 的模块，在当前模块添加一个指向依赖模块的引用，比如上面的例子中，&lt;code&gt;import {bar} from &apos;b.js&apos;&lt;/code&gt;，就是在 &lt;code&gt;a&lt;/code&gt; 中添加了一个 &lt;code&gt;b&lt;/code&gt; 的引用 &lt;code&gt;bar&lt;/code&gt;，当后面用到这个 &lt;code&gt;bar&lt;/code&gt; 的时候会去 &lt;code&gt;b&lt;/code&gt; 中找。注意一点，这个引用在没有使用之前，不必关心它是什么，&lt;code&gt;a&lt;/code&gt; 也不知道 &lt;code&gt;bar&lt;/code&gt; 是什么。当 &lt;code&gt;a&lt;/code&gt; 的语句执行用到 &lt;code&gt;bar&lt;/code&gt; 的时候，引擎就会去 &lt;code&gt;b&lt;/code&gt; 的&lt;strong&gt;当前环境&lt;/strong&gt;中去找有没有 &lt;code&gt;export&lt;/code&gt; 的 &lt;code&gt;bar&lt;/code&gt;，找到则返回，找不到则抛错。注意这个当前环境，这是循环加载的中点，上面的例子 &lt;code&gt;a&lt;/code&gt; 的 &lt;code&gt;import&lt;/code&gt; 后面的语句并没与执行，但是此时由于变量提升机制，我们已经能取到这些值了，所以成功返回。只是由于 &lt;code&gt;str&lt;/code&gt; 和 &lt;code&gt;obj&lt;/code&gt; 是用 &lt;code&gt;let&lt;/code&gt; 声明的，所以不能在声明语句前使用。其实我们完全可以把那几句 &lt;code&gt;console.log&lt;/code&gt; 语句放到 &lt;code&gt;a&lt;/code&gt; 中的 &lt;code&gt;import&lt;/code&gt; 语句下方执行，得到的结果是相同的。&lt;/p&gt;
&lt;p&gt;总之记住一点，&lt;code&gt;ES6&lt;/code&gt; 的模块机制，引入的别的模块的变量都是要到对应的模块中去找，并且是去对应模块的变量对象中去找，记住这个原则，一般的问题应该都能够理清楚，还有就是记住 &lt;code&gt;export default&lt;/code&gt; 的情况。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于模块的 &lt;code&gt;gc&lt;/code&gt; 我认为就是添加一个模块级作用域即可（和函数作用域类比），它是一个单独的环境，只有内部的变量或者方法被外部引用，就不会释放其环境记录，&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;非模块化文件的使用&lt;/h2&gt;
&lt;p&gt;非模块化文件指的是并不遵循任何一种模块化标准的文件。如果你维护的是一个几年前的项目，那么极有可能里面会与非模块化文件，最常见的就是在 &lt;code&gt;script&lt;/code&gt; 标签中引入的 &lt;code&gt;jQuery&lt;/code&gt; 及其各种插件。&lt;/p&gt;
&lt;p&gt;对于这类文件，我们如果想用打包工具进行处理，直接引入即可：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import &apos;./jquery.min.js&apos;

require(&apos;./jquery.min.js&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实不止这些非模块化文件，我们如果有什么不需要明确导出值，而只是要其执行的 &lt;code&gt;js&lt;/code&gt;，甚至是一些静态文件（比如图片），都可以这样引入。一般来说，&lt;code&gt;jQuery&lt;/code&gt; 这类库都会将其接口绑定在全局（我们可以通过 &lt;code&gt;window.jQuery&lt;/code&gt; 获取 &lt;code&gt;jQuery&lt;/code&gt;），因此无论从 &lt;code&gt;script&lt;/code&gt; 标签引入，还是使用 &lt;code&gt;webpack&lt;/code&gt; 打包引入，效果都是一样的，即只要在使用前执行即可。&lt;/p&gt;
&lt;p&gt;但是有一点需要注意，&lt;code&gt;webpack&lt;/code&gt; 会将模块都包裹在一层函数中，如果你的非模块化文件中的接口暴露是用的 &lt;code&gt;var&lt;/code&gt; 这种隐式全局变量声明的方式，则无法在全局访问到。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这篇文章从模块化的冬季和历史为切入点，讲了模块化发展历史上几个比较重要的模块化规范的使用方式和原理。其实我们现在主要掌握 &lt;code&gt;CommonJS&lt;/code&gt; 和 &lt;code&gt;ES6 Module&lt;/code&gt; 即可，它们两个是现在最主流的模块化方案，我们需要深入了解它们的设计思路和原理。童颜 &lt;code&gt;CMD&lt;/code&gt; 和 &lt;code&gt;AMD&lt;/code&gt; 的设计思路和思想还是值得看一看的。希望这篇文章对你有帮助，如果有错误，欢迎指正。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://neveryu.github.io/2017/03/20/amd-cmd/&quot; title=&quot;AMD，CMD 规范详解&quot;&gt;AMD，CMD 规范详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/dong-xu/p/7160919.html&quot; title=&quot;从 RequireJs 源码剖析脚本加载原理&quot;&gt;从 RequireJs 源码剖析脚本加载原理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844903759009595405&quot; title=&quot;模块化之AMD与CMD原理(附源码)&quot;&gt;模块化之AMD与CMD原理(附源码)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://justineo.github.io/singles/writing-modular-js/&quot; title=&quot;使用 AMD、CommonJS 及 ES Harmony 编写模块化的 JavaScript&quot;&gt;使用 AMD、CommonJS 及 ES Harmony 编写模块化的 JavaScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;《ES6 标准入门》 —— 阮一峰&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000017878394&quot; title=&quot;深入 CommonJs 与 ES6 Module&quot;&gt;深入 CommonJs 与 ES6 Module&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/js-module.CQUZgyQj.png"/><enclosure url="/_astro/js-module.CQUZgyQj.png"/></item><item><title>Proxy ，Reflect，代理和反射</title><link>https://clloz.com/blog/javascript-reflect-proxy</link><guid isPermaLink="true">https://clloz.com/blog/javascript-reflect-proxy</guid><pubDate>Mon, 19 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文主要说一说 &lt;code&gt;ES6&lt;/code&gt; 添加的新内置对象，&lt;code&gt;Proxy&lt;/code&gt; 和 &lt;code&gt;Reflect&lt;/code&gt;，一般中文翻译为代理和反射。在编程语言中普遍存在的一个概念，&lt;code&gt;JavaScript&lt;/code&gt; 在 &lt;code&gt;ES6&lt;/code&gt; 中也引入了。&lt;/p&gt;
&lt;h2&gt;Proxy&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Proxy&lt;/code&gt; 是代理的意思，简单一点说就是我们用 &lt;code&gt;Proxy&lt;/code&gt; 对对象进行一层包装，返回一个新的代理对象。通过这个代理对象，我们可以对原来我们操作 &lt;code&gt;Object&lt;/code&gt; 对象的很多属性方法进行定制。比如普通的 &lt;code&gt;Object&lt;/code&gt; 对象的属性访问，我们就用 &lt;code&gt;.&lt;/code&gt; 操作符或者 &lt;code&gt;[]&lt;/code&gt; 进行成员访问，但是我们没办法对这个访问进行定制，比如我希望访问对象的时候通知其他对象，这在没有 &lt;code&gt;Proxy&lt;/code&gt; 的时候是非常不方便的，我们可以通过 &lt;code&gt;Object.definedProperty()&lt;/code&gt; 设置访问器属性，但是每个属性都要单独设置，而 &lt;code&gt;Proxy&lt;/code&gt; 是对属性访问这个行为进行定制。&lt;/p&gt;
&lt;p&gt;从上面的描述我们可以看出，&lt;code&gt;Proxy&lt;/code&gt; 是对 &lt;code&gt;JavaScript&lt;/code&gt; 中的对象的一种增强，我们拥有了更强的对象定制功能。&lt;code&gt;Proxy&lt;/code&gt; 能够修改某些操作的默认行为，等同于在语言层面做出修改，所以属于一种“元编程”(&lt;code&gt;meta programming&lt;/code&gt;)，即对编程语言进行编程。看一个最简单的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let o = {
  name: &apos;clloz&apos;,
  age: &apos;28&apos;,
  site: &apos;clloz.com&apos;
}

let p = new Proxy(o, {
  get: function (target, property, receiver) {
    console.log(target)
    console.log(property)
    console.log(receiver)
    return target[property]
  }
})

console.log(o.name) //clloz
console.log(p.name)
//{ name: &apos;clloz&apos;, age: &apos;28&apos;, site: &apos;clloz.com&apos; }
//name
//{ name: &apos;clloz&apos;, age: &apos;28&apos;, site: &apos;clloz.com&apos; }
//clloz
console.log(p) //{ name: &apos;clloz&apos;, age: &apos;28&apos;, site: &apos;clloz.com&apos; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个用 &lt;code&gt;Proxy&lt;/code&gt; 代理 &lt;code&gt;Object&lt;/code&gt; 的属性访问行为的例子，我们可以看到 &lt;code&gt;Proxy&lt;/code&gt; 接受两个参数，一个是代理的目标对象，另一个是一个对象，其中放着我们定制的代理方法，&lt;code&gt;Proxy&lt;/code&gt; 提供了非常丰富的方法，这里我只是使用了我们比较熟悉的属性访问。我们可以看到 &lt;code&gt;get&lt;/code&gt; 方法传入了三个参数，分别对应目标对象，我们访问的属性，和 &lt;code&gt;Proxy&lt;/code&gt; 对象。例子中 &lt;code&gt;receiver&lt;/code&gt; 就是我们定义的 &lt;code&gt;p&lt;/code&gt; 对象，&lt;code&gt;receiver === p&lt;/code&gt; 将返回 &lt;code&gt;true&lt;/code&gt;，但是 &lt;code&gt;receiver&lt;/code&gt; 不一定是 &lt;code&gt;Proxy&lt;/code&gt; 下面介绍 &lt;code&gt;handler&lt;/code&gt; 会详细说明。&lt;/p&gt;
&lt;p&gt;只有通过代理进行访问才能出发我们定制的行为，比如例子中我们直接访问 &lt;code&gt;o.name&lt;/code&gt; 就只是访问对象属性。而我们打印 &lt;code&gt;Proxy&lt;/code&gt; 对象，和普通的对象看上去也没有区别。我试了 &lt;code&gt;Objec.prototype.toString.call(p)&lt;/code&gt;，得到的结果只是 &lt;code&gt;[object Object]&lt;/code&gt;，所以目前应该没有方法直接能够判断一个对象是代理还是普通对象。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Proxy&lt;/code&gt; 是一个构造函数，并且不能直接调用，直接调用会报错：&lt;code&gt;TypeError: Constructor Proxy requires &apos;new&apos;&lt;/code&gt;。&lt;code&gt;Proxy&lt;/code&gt; 没有 &lt;code&gt;prototype&lt;/code&gt;，&lt;code&gt;Proxy.prototype&lt;/code&gt; 将返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;code&gt;Proxy.[[prototype]]&lt;/code&gt; 为 &lt;code&gt;Function.protoyep&lt;/code&gt;，这一点和其他所有函数相同。&lt;/p&gt;
&lt;h2&gt;用法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Proxy&lt;/code&gt; 对象只有一种用法就是 &lt;code&gt;let proxy = new Proxy(target, handler)&lt;/code&gt;，以构造函数的形式创建目标对象的代理，两个参数都是必须的，缺少参数将抛错 &lt;code&gt;TypeError: Cannot create proxy with a non-object as target or handler&lt;/code&gt;。&lt;code&gt;target&lt;/code&gt; 是要使用 &lt;code&gt;Proxy&lt;/code&gt; 包装的目标对象（可以是任何类型的对象，包括原生数组，函数，甚至另一个代理）。我们主要控制的就是 &lt;code&gt;handler&lt;/code&gt;，所有的代理逻辑都在 &lt;code&gt;handler&lt;/code&gt; 中。如果 &lt;code&gt;hanlder&lt;/code&gt; 为空对象，那么就相当于我们没有任何代理逻辑，通过代理访问和访问源对象就没区别，不过这样做并没有什么实际意义。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let o = {
  name: &apos;clloz&apos;,
  age: &apos;28&apos;,
  site: &apos;clloz.com&apos;
}

let p = new Proxy(o, {})

console.log(p.name) //clloz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有一个小技巧是我们可以将代理对象设置为目标对象的一个属性，这样我们可以在目标对象上直接访问到代理对象。&lt;code&gt;let object = { proxy: new Proxy(target, handler) };&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;代理对象也可以作为其他对象的原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let o = {
  name: &apos;clloz&apos;,
  age: &apos;28&apos;,
  site: &apos;clloz.com&apos;
}

let p = new Proxy(o, {})

let m = Object.create(p)
console.log(m.name) //clloz

console.log(p.__proto__ === Object.prototype)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面我们说过，&lt;code&gt;Proxy.prototype&lt;/code&gt; 是 &lt;code&gt;undefined&lt;/code&gt;，按照通常的原型链规则推断，&lt;code&gt;p&lt;/code&gt; 的 &lt;code&gt;[[prototype]]&lt;/code&gt; 应该是指向其构造函数的 &lt;code&gt;prototype&lt;/code&gt; 属性，但是在 &lt;code&gt;Proxy&lt;/code&gt; 这里，&lt;code&gt;p&lt;/code&gt; 的 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向的是 &lt;code&gt;Object.prototype&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;综合上面的一些结论，其实我们完全可以把代理理解为一个功能更强大的**“普通对象”**，只是他的一些行为可能不完全和普通对象一致，比如 &lt;code&gt;this&lt;/code&gt; 的指向。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;Proxy&lt;/code&gt; 代理的核心就是 &lt;code&gt;handler&lt;/code&gt;，&lt;code&gt;handler&lt;/code&gt; 对象是一个容纳一批特定属性的占位符对象。它包含有 &lt;code&gt;Proxy&lt;/code&gt; 的各个捕获器（&lt;code&gt;trap&lt;/code&gt;）。所有的捕捉器是可选的。如果没有定义某个捕捉器，那么就会保留源对象的默认行为。所以下面我们介绍一下 &lt;code&gt;Proxy&lt;/code&gt; 都提供了哪些捕获器。&lt;/p&gt;
&lt;h3&gt;handler.get()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.get(target, key, receiver)&lt;/code&gt; 拦截属性读取操作。&lt;code&gt;target&lt;/code&gt; 为代理的目标对象，&lt;code&gt;property&lt;/code&gt; 为被读取的属性名。第三个参数 &lt;code&gt;receiver&lt;/code&gt; 为最初被调用的对象，即让我们在 &lt;code&gt;getter&lt;/code&gt; 知道是谁在访问。通常是 &lt;code&gt;proxy&lt;/code&gt; 本身，但 &lt;code&gt;handler&lt;/code&gt; 的 &lt;code&gt;get&lt;/code&gt; 方法也有可能在原型链上，或以其他方式被间接地调用（因此不一定是 &lt;code&gt;proxy&lt;/code&gt; 本身）看下面的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let o = {
  name: &apos;clloz&apos;,
  age: &apos;28&apos;,
  site: &apos;clloz.com&apos;
}

let p = new Proxy(o, {
  get: function (target, property, receiver) {
    console.log(receiver === m) //true
    return target[property]
  }
})

let m = Object.create(p)
console.log(m.name) //clloz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到，我们以 &lt;code&gt;Proxy&lt;/code&gt; 为原型创建了一个对象 &lt;code&gt;m&lt;/code&gt;，当访问 &lt;code&gt;m&lt;/code&gt; 上没有的属性的时候就会到原型链上查找，就触发了 &lt;code&gt;get&lt;/code&gt; 捕捉器，我们在其中的 &lt;code&gt;receiver === m&lt;/code&gt; 将返回 &lt;code&gt;true&lt;/code&gt;。这里要特别注意，不要直接执行 &lt;code&gt;console.log(receiver)&lt;/code&gt;，否则将会出现栈溢出的状况。其中原因就是，&lt;code&gt;receiver&lt;/code&gt; 是一个对象，我们输出这个对象，又会执行对象的 &lt;code&gt;get&lt;/code&gt; 操作，进入无限的循环。不过如果我们直接输出 &lt;code&gt;p.name&lt;/code&gt; 则不会有这个错误，可能是因为 &lt;code&gt;Proxy&lt;/code&gt; 本身没有 &lt;code&gt;get&lt;/code&gt; 操作。从这个例子中我们也可以看到，&lt;code&gt;get&lt;/code&gt; 方法是可以继承的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;get&lt;/code&gt; 方法会拦截目标对象的以下操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;访问属性: &lt;code&gt;proxy[foo]&lt;/code&gt;和 &lt;code&gt;proxy.bar&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;访问原型链上的属性: &lt;code&gt;Object.create(proxy)[foo]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.get()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违背了以下的约束，&lt;code&gt;proxy&lt;/code&gt; 会抛出 &lt;code&gt;TypeError&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果要访问的目标属性是不可写以及不可配置的，则返回的值必须与该目标属性的值相同。&lt;/li&gt;
&lt;li&gt;如果要访问的目标属性没有配置访问方法，即 &lt;code&gt;get&lt;/code&gt; 方法是 &lt;code&gt;undefined&lt;/code&gt; 的，则返回值必须为 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;利用代理的 &lt;code&gt;get&lt;/code&gt; 捕捉器我们可以实现很多有趣的功能，比如用负数索引读取数组：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey)
      if (index &amp;#x3C; 0) {
        propKey = String(target.length + index)
      }
      return Reflect.get(target, propKey, receiver)
    }
  }
  let target = []
  target.push(...elements)
  return new Proxy(target, handler)
}
let arr = createArray(&apos;a&apos;, &apos;b&apos;, &apos;c&apos;)
console.log(arr[-1]) // c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用 &lt;code&gt;Proxy&lt;/code&gt;，可以将读取属性的操作( &lt;code&gt;get&lt;/code&gt; )，转变为执行某个函数，从而实现属性的链式操作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var pipe = (function () {
  return function (value) {
    var funcStack = []
    var oproxy = new Proxy(
      {},
      {
        get: function (pipeObject, fnName) {
          if (fnName === &apos;get&apos;) {
            return funcStack.reduce(function (val, fn) {
              return fn(val)
            }, value)
          }
          funcStack.push(window[fnName])
          return oproxy
        }
      }
    )
    return oproxy
  }
})()
var double = (n) =&gt; n * 2
var pow = (n) =&gt; n * n
var reverseInt = (n) =&gt; n.toString().split(&apos;&apos;).reverse().join(&apos;&apos;) | 0
pipe(3).double.pow.reverseInt.get // 63
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一个属性不可配置(&lt;code&gt;configurable&lt;/code&gt;)和不可写(&lt;code&gt;writable&lt;/code&gt;)，则该属性不能被代理，通过 &lt;code&gt;Proxy&lt;/code&gt; 对象访问该属性会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const target = Object.defineProperties(
  {},
  {
    foo: {
      value: 123,
      writable: false,
      configurable: false
    }
  }
)
const handler = {
  get(target, propKey) {
    return &apos;abc&apos;
  }
}
const proxy = new Proxy(target, handler)
proxy.foo // TypeError: Invariant check failed
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;handler.set()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.set(target, key, value, receiver)&lt;/code&gt; 属性设置操作的捕捉器，返回一个布尔值。返回 &lt;code&gt;true&lt;/code&gt; 代表属性设置成功。在严格模式下，如果 &lt;code&gt;set()&lt;/code&gt; 方法返回 &lt;code&gt;false&lt;/code&gt;，那么会抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 异常。&lt;/p&gt;
&lt;p&gt;该方法会拦截目标对象的以下操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指定属性值：&lt;code&gt;proxy[foo] = bar&lt;/code&gt; 和 &lt;code&gt;proxy.foo = bar&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;指定继承者的属性值：&lt;code&gt;Object.create(proxy)[foo] = bar&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.set()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违背以下的约束条件，&lt;code&gt;proxy&lt;/code&gt; 会抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 异常：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若目标属性是一个不可写及不可配置的数据属性，则不能改变它的值。&lt;/li&gt;
&lt;li&gt;如果目标属性没有配置存储方法，即 &lt;code&gt;[[Set]]&lt;/code&gt; 属性的是 &lt;code&gt;undefined&lt;/code&gt;，则不能设置它的值。&lt;/li&gt;
&lt;li&gt;在严格模式下，如果 &lt;code&gt;set()&lt;/code&gt; 方法返回 &lt;code&gt;false&lt;/code&gt;，那么也会抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;利用 &lt;code&gt;set&lt;/code&gt; 可以进行数据验证，还可以进行数据绑定，即每当对象发生变化时，会自动更新 &lt;code&gt;DOM&lt;/code&gt;。有时，我们会在对象上面设置内部属性，属性名的第一个字符使用下划线开头，表示这些属性不应该被外部使用。结合 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 方法，就可以做到防止这些内部 属性被外部读写。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var handler = {
  get(target, key) {
    invariant(key, &apos;get&apos;)
    return target[key]
  },
  set(target, key, value) {
    invariant(key, &apos;set&apos;)
    target[key] = value
    return true
  }
}
function invariant(key, action) {
  if (key[0] === &apos;_&apos;) {
    throw new Error(`Invalid attempt to ${action} private &quot;${key} &quot; property`)
  }
}
var target = {}
var proxy = new Proxy(target, handler)
proxy._prop
// Error: Invalid attempt to get private &quot;_prop&quot; property
proxy._prop = &apos;c&apos;
// Error: Invalid attempt to set private &quot;_prop&quot; property
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;set&lt;/code&gt; 可以监听数组的变化，包括一些 &lt;code&gt;Array.prototype&lt;/code&gt; 上的方法，这也是 &lt;code&gt;vue3.0&lt;/code&gt; 要使用 &lt;code&gt;Proxy&lt;/code&gt; 来替换 &lt;code&gt;Object.defineProperty&lt;/code&gt; 来实现响应式的原因之一。看下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = [1, 2, 3]
let p = new Proxy(a, {
  get(target, key, receiver) {
    console.log(&apos;this is getter &apos; + key)
    return Reflect.get(target, key)
  },
  set(target, key, val, receiver) {
    console.log(&apos;this is setter &apos; + key)
    return Reflect.set(target, key, val, receiver)
  }
})
p.push(10)
//this is getter push
//this is getter length
//this is setter 3
//this is setter length

console.log(p)
//[ 1, 2, 3, 10 ]

console.log(p[0])
//this is getter 0
//1

console.log(p.length)
//this is getter length
//3

p.length = 0 // this is setter length
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到我们调用 &lt;code&gt;push&lt;/code&gt; 方法，访问了两次 &lt;code&gt;getter&lt;/code&gt;，第一次访问的 &lt;code&gt;key&lt;/code&gt; 是 &lt;code&gt;push&lt;/code&gt;，第二次则是 &lt;code&gt;length&lt;/code&gt;。我们访问 &lt;code&gt;p&lt;/code&gt; 从外部看到的效果和访问 &lt;code&gt;a&lt;/code&gt; 没有区别，甚至我们直接访问和操作 &lt;code&gt;length&lt;/code&gt; 也能够触发 &lt;code&gt;getter&lt;/code&gt; 和 &lt;code&gt;setter&lt;/code&gt;，因为 &lt;code&gt;push&lt;/code&gt; 方法和 &lt;code&gt;length&lt;/code&gt; 也是数组的属性和方法（虽然方法是在原型上的，但依然是通过该数组访问的），这就是 &lt;code&gt;Proxy&lt;/code&gt; 提供的元编程的强大能力。&lt;/p&gt;
&lt;h3&gt;handler.apply()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.apply(target, thisArg, argumentsList)&lt;/code&gt; 方法用于拦截函数的调用。该捕捉器接受三个参数：&lt;code&gt;target&lt;/code&gt; 目标对象，&lt;code&gt;thisArg&lt;/code&gt; 被调用时的 &lt;code&gt;this&lt;/code&gt; 上下文，&lt;code&gt;argumentsList&lt;/code&gt; 被调用时的参数数组。&lt;/p&gt;
&lt;p&gt;该方法会拦截目标对象的以下操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;proxy(...args)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Function.prototype.apply()&lt;/code&gt; 和 &lt;code&gt;Function.prototype.call()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.apply()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违反了以下约束，代理将抛出一个 &lt;code&gt;TypeError&lt;/code&gt;：&lt;code&gt;target&lt;/code&gt; 必须是可被调用的。也就是说，它必须是一个函数对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var p = new Proxy(function () {}, {
  apply: function (target, thisArg, argumentsList) {
    console.log(&apos;called: &apos; + argumentsList.join(&apos;, &apos;)) // &quot;called: 1, 2, 3&quot;
    return argumentsList[0] + argumentsList[1] + argumentsList[2]
  }
})

console.log(p(1, 2, 3)) // 6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;handler.has()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.has(target, prop)&lt;/code&gt; 该方法用来拦截 &lt;code&gt;HasProperty&lt;/code&gt; 操作，即判断对象是否具有某个属性时，这个方法会生效。典型的操作就是 &lt;code&gt;in&lt;/code&gt; 运算符。&lt;code&gt;handler.has&lt;/code&gt; 方法可以看作是针对 &lt;code&gt;in&lt;/code&gt; 操作的钩子。&lt;/p&gt;
&lt;p&gt;这个钩子可以拦截下面这些操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;属性查询: &lt;code&gt;foo in proxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;继承属性查询: &lt;code&gt;foo in Object.create(proxy)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;with&lt;/code&gt; 检查: &lt;code&gt;with(proxy) { (foo); }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.has()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违反了下面这些规则, &lt;code&gt;proxy&lt;/code&gt; 将会抛出 &lt;code&gt;TypeError&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果目标对象的某一属性本身不可被配置，则该属性不能够被代理隐藏.&lt;/li&gt;
&lt;li&gt;如果目标对象为不可扩展对象，则该对象的属性不能够被代理隐藏&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var p = new Proxy(
  {},
  {
    has: function (target, prop) {
      console.log(&apos;called: &apos; + prop) // &quot;called: a&quot;
      return true
    }
  }
)

console.log(&apos;a&apos; in p) // true
console.log(Reflect.has(p, &apos;a&apos;)) //和上一句代码一样被拦截
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对象不可扩展或属性不可配置拦截 &lt;code&gt;has&lt;/code&gt; 将抛错，所以如果我们不希望抛错可能需要对属性或者对象进行判断：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var obj = { a: 10 }
Object.preventExtensions(obj)
var p = new Proxy(obj, {
  has: function (target, prop) {
    return false
  }
})

&apos;a&apos; in p // TypeError: &apos;has&apos; on proxy: trap returned falsish for property &apos;a&apos; but the proxy target is not extensible
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意的是 &lt;code&gt;has&lt;/code&gt; 方法拦截的是 &lt;code&gt;hasProperty&lt;/code&gt;，而不是 &lt;code&gt;hasOwnProperty&lt;/code&gt;，也就是说原型上的属性访问也会拦截。还有一点就是虽然 &lt;code&gt;for ... in&lt;/code&gt; 虽然也用到了 &lt;code&gt;in&lt;/code&gt; 运算符，但是 &lt;code&gt;has&lt;/code&gt; 方法不会拦截 &lt;code&gt;for ... in&lt;/code&gt; 循环。&lt;/p&gt;
&lt;h3&gt;handler.construct()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.construct(target, argumentsList, newTarget)&lt;/code&gt; 方法用于拦截 &lt;code&gt;new&lt;/code&gt; 操作符. 为了使 &lt;code&gt;new&lt;/code&gt; 操作符在生成的 &lt;code&gt;Proxy&lt;/code&gt; 对象上生效，用于初始化代理的目标对象自身必须具有 &lt;code&gt;[[Construct]]&lt;/code&gt; 内部方法（即 &lt;code&gt;new target&lt;/code&gt; 必须是有效的）。&lt;/p&gt;
&lt;p&gt;三个参数将被传入 &lt;code&gt;construct&lt;/code&gt; 方法：&lt;code&gt;target&lt;/code&gt; 目标对象，&lt;code&gt;argumentsList&lt;/code&gt; 构造函数的参数列表，&lt;code&gt;newTarget&lt;/code&gt; 最初被调用的构造函数。&lt;/p&gt;
&lt;p&gt;该拦截器可以拦截以下操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new proxy(...args)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.construct()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违反以下约定，代理将会抛出错误 &lt;code&gt;TypeError&lt;/code&gt;: 必须返回一个对象。&lt;code&gt;target&lt;/code&gt; 必须由一个有效的 &lt;code&gt;constructor&lt;/code&gt; 供 &lt;code&gt;new&lt;/code&gt; 调用，一般情况下，目标对象要是一个函数。&lt;/p&gt;
&lt;p&gt;两个简单的示例，演示拦截 &lt;code&gt;new&lt;/code&gt; 操作以及违反约定的情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//拦截new操作
var p = new Proxy(function () {}, {
  construct: function (target, argumentsList, newTarget) {
    console.log(&apos;called: &apos; + argumentsList.join(&apos;, &apos;)) // &quot;called: 1&quot;
    return { value: argumentsList[0] * 10 }
  }
})

console.log(new p(1).value) // 10

//违反约定，没有返回对象将抛错
var p = new Proxy(function () {}, {
  construct: function (target, argumentsList, newTarget) {
    return 1
  }
})

new p() // TypeError is thrown

//目标对象不能new
var p = new Proxy(
  {},
  {
    construct: function (target, argumentsList, newTarget) {
      return {}
    }
  }
)

new p() // TypeError is thrown, &quot;p&quot; is not a constructor
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;handler.deleteProperty()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.deleteProperty(target, property)&lt;/code&gt; 方法用于拦截对对象属性的 &lt;code&gt;delete&lt;/code&gt; 操作。&lt;code&gt;deleteProperty&lt;/code&gt; 必须返回一个 &lt;code&gt;Boolean&lt;/code&gt; 类型的值，表示了该属性是否被成功删除。如果这个方法抛出错误或者返回 &lt;code&gt;false&lt;/code&gt; ，当前属性就无法被 &lt;code&gt;delete&lt;/code&gt; 命令删除。&lt;/p&gt;
&lt;p&gt;该方法会拦截以下操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除属性: &lt;code&gt;delete proxy[foo]&lt;/code&gt; 和 &lt;code&gt;delete proxy.foo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.deleteProperty()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违背了以下不变量，&lt;code&gt;proxy&lt;/code&gt; 将会抛出一个 &lt;code&gt;TypeError&lt;/code&gt;: 如果目标对象的属性是不可配置的，那么该属性不能被删除。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var p = new Proxy(
  {},
  {
    deleteProperty: function (target, prop) {
      console.log(&apos;called: &apos; + prop)
      return true
    }
  }
)

delete p.a // &quot;called: a&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;handler.defineProperty()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.defineProperty(target, property, descriptor)&lt;/code&gt; 该方法用于拦截对对象的 &lt;code&gt;Object.defineProperty()&lt;/code&gt; 操作。该方法接受三个参数：&lt;code&gt;target&lt;/code&gt; 目标对象，&lt;code&gt;prop&lt;/code&gt; 待检索其描述符的属性名，&lt;code&gt;descriptor&lt;/code&gt; 属性描述符。&lt;code&gt;defineProperty&lt;/code&gt; 方法必须以一个 &lt;code&gt;Boolean&lt;/code&gt; 返回，表示定义该属性的操作成功与否。&lt;/p&gt;
&lt;p&gt;该方法会拦截目标对象的以下操作 :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Object.defineProperty()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.defineProperty()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;proxy.property=&apos;value&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违背了以下的不变量，&lt;code&gt;proxy&lt;/code&gt; 会抛出 &lt;code&gt;TypeError&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果目标对象不可扩展， 将不能添加属性。&lt;/li&gt;
&lt;li&gt;不能添加或者修改一个属性为不可配置的，如果它不作为一个目标对象的不可配置的属性存在的话。&lt;/li&gt;
&lt;li&gt;如果目标对象存在一个对应的可配置属性，这个属性可能不会是不可配置的。&lt;/li&gt;
&lt;li&gt;如果一个属性在目标对象中存在对应的属性，那么 &lt;code&gt;Object.defineProperty(target, prop, descriptor)&lt;/code&gt; 将不会抛出异常。&lt;/li&gt;
&lt;li&gt;在严格模式下， &lt;code&gt;false&lt;/code&gt; 作为 &lt;code&gt;handler.defineProperty&lt;/code&gt; 方法的返回值的话将会抛出 &lt;code&gt;TypeError&lt;/code&gt; 异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var p = new Proxy(
  {},
  {
    defineProperty: function (target, prop, descriptor) {
      console.log(&apos;called: &apos; + prop)
      descriptor.value = &apos;clloz&apos; //修改数据属性值
      descriptor.configurable = true //如果该属性不可配置，则这一句将抛错
      return Reflect.defineProperty(target, prop, descriptor)
    }
  }
)

var desc = { configurable: true, enumerable: true, value: 10, writable: true }
Object.defineProperty(p, &apos;a&apos;, desc) // &quot;called: a&quot;

console.log(p.a)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当调用 &lt;code&gt;Object.defineProperty()&lt;/code&gt; 或者 &lt;code&gt;Reflect.defineProperty()&lt;/code&gt;，传递给 &lt;code&gt;defineProperty&lt;/code&gt; 的 &lt;code&gt;descriptor&lt;/code&gt; 有一个限制 - 只有以下属性才有用，非标准的属性将会被无视 :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enumerable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;configurable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;handler.getOwnPropertyDescriptor()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.getOwnPropertyDescriptor(target, prop)&lt;/code&gt; 方法是 &lt;code&gt;Object.getOwnPropertyDescriptor()&lt;/code&gt; 的钩子。&lt;code&gt;getOwnPropertyDescriptor&lt;/code&gt; 方法必须返回一个 &lt;code&gt;object&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个捕捉器可以拦截这些操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Object.getOwnPropertyDescriptor()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.getOwnPropertyDescriptor()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果下列不变量被违反，代理将抛出一个 TypeError：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getOwnPropertyDescriptor&lt;/code&gt; 必须返回一个 &lt;code&gt;object&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果属性作为目标对象的不可配置的属性存在，则该属性无法报告为不存在。&lt;/li&gt;
&lt;li&gt;如果属性作为目标对象的属性存在，并且目标对象不可扩展，则该属性无法报告为不存在。&lt;/li&gt;
&lt;li&gt;如果属性不存在作为目标对象的属性，并且目标对象不可扩展，则不能将其报告为存在。&lt;/li&gt;
&lt;li&gt;属性不能被报告为不可配置，如果它不作为目标对象的自身属性存在，或者作为目标对象的可配置的属性存在。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.getOwnPropertyDescriptor(target)&lt;/code&gt;的结果可以使用 &lt;code&gt;Object.defineProperty&lt;/code&gt; 应用于目标对象，也不会抛出异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var p = new Proxy(
  { a: 20 },
  {
    getOwnPropertyDescriptor: function (target, prop) {
      console.log(&apos;called: &apos; + prop) // &quot;called: a&quot;
      return { configurable: true, enumerable: true, value: 10 }
    }
  }
)

console.log(Object.getOwnPropertyDescriptor(p, &apos;a&apos;).value) // 10

//属性存在，不能返回 undefined
var obj = { a: 10 }
Object.preventExtensions(obj)
var p = new Proxy(obj, {
  getOwnPropertyDescriptor: function (target, prop) {
    return undefined
  }
})

Object.getOwnPropertyDescriptor(p, &apos;a&apos;) // TypeError is thrown
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;handler.getPrototypeOf()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.getPrototypeOf(target)&lt;/code&gt; 该方法当读取代理对象的原型时，该方法就会被调用。&lt;code&gt;getPrototypeOf&lt;/code&gt; 方法的返回值必须是一个对象或者 &lt;code&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中，下面这五种操作（方法/属性/运算符）可以触发 &lt;code&gt;JS&lt;/code&gt; 引擎读取一个对象的原型，也就是可以触发 &lt;code&gt;getPrototypeOf()&lt;/code&gt; 代理方法的运行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Object.getPrototypeOf()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.getPrototypeOf()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__proto__&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.prototype.isPrototypeOf()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;instanceof&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果遇到了下面两种情况，&lt;code&gt;JS&lt;/code&gt; 引擎会抛出 &lt;code&gt;TypeError&lt;/code&gt; 异常：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getPrototypeOf()&lt;/code&gt; 方法返回的不是对象也不是 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;目标对象是不可扩展的，且 &lt;code&gt;getPrototypeOf()&lt;/code&gt; 方法返回的原型不是目标对象本身的原型。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//五种触发 getPrototypeOf() 的方式
var obj = {}
var p = new Proxy(obj, {
  getPrototypeOf(target) {
    return Array.prototype
  }
})
console.log(
  Object.getPrototypeOf(p) === Array.prototype, // true
  Reflect.getPrototypeOf(p) === Array.prototype, // true
  p.__proto__ === Array.prototype, // true
  Array.prototype.isPrototypeOf(p), // true
  p instanceof Array // true
)

//异常情况
var obj = {}
var p = new Proxy(obj, {
  getPrototypeOf(target) {
    return &apos;foo&apos;
  }
})
Object.getPrototypeOf(p) // TypeError: &quot;foo&quot; is not an object or null

var obj = Object.preventExtensions({})
var p = new Proxy(obj, {
  getPrototypeOf(target) {
    return {} // 想要正确返回这里应该是 Object.prototype
  }
})
Object.getPrototypeOf(p) // TypeError: &apos;getPrototypeOf&apos; on proxy: proxy target is non-extensible but the trap did not return its actual prototype
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;handler.isExtensible()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.isExtensible(target)&lt;/code&gt; 该方法用于拦截对对象的 &lt;code&gt;Object.isExtensible()&lt;/code&gt;。&lt;code&gt;isExtensible&lt;/code&gt; 方法必须返回一个 &lt;code&gt;Boolean&lt;/code&gt; 值或可转换成 &lt;code&gt;Boolean&lt;/code&gt; 的值。&lt;/p&gt;
&lt;p&gt;该方法会拦截目标对象的以下操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Object.isExtensible()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.isExtensible()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违背了以下的约束，&lt;code&gt;proxy&lt;/code&gt; 会抛出 &lt;code&gt;TypeError&lt;/code&gt;: &lt;code&gt;Object.isExtensible(proxy)&lt;/code&gt; 必须同 &lt;code&gt;Object.isExtensible(target)&lt;/code&gt;返回相同值。也就是必须返回 &lt;code&gt;true&lt;/code&gt; 或者为 &lt;code&gt;true&lt;/code&gt; 的值,返回 &lt;code&gt;false&lt;/code&gt; 和为 &lt;code&gt;false&lt;/code&gt; 的值都会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var p = new Proxy(
  {},
  {
    isExtensible: function (target) {
      console.log(&apos;called&apos;) // &quot;called&quot;
      return true //也可以return 1;等表示为true的值
    }
  }
)

console.log(Object.isExtensible(p)) // true

//违反约束
var p = new Proxy(
  {},
  {
    isExtensible: function (target) {
      return false //return 0;return NaN等都会报错
    }
  }
)

Object.isExtensible(p) // TypeError is thrown
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;handler.ownKeys()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.ownKeys(target)&lt;/code&gt; 方法用来拦截对象自身属性的读取操作。&lt;code&gt;this&lt;/code&gt; 被绑定在 &lt;code&gt;handler&lt;/code&gt; 上。&lt;code&gt;ownKeys&lt;/code&gt; 方法必须返回一个可枚举对象。&lt;/p&gt;
&lt;p&gt;该拦截器可以拦截以下操作::&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Object.getOwnPropertyNames()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.getOwnPropertySymbols()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.keys()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.ownKeys()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违反了下面的约束，&lt;code&gt;proxy&lt;/code&gt; 将抛出错误 &lt;code&gt;TypeError&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ownKeys&lt;/code&gt; 的结果必须是一个数组.&lt;/li&gt;
&lt;li&gt;数组的元素类型要么是一个 &lt;code&gt;String&lt;/code&gt; ，要么是一个 &lt;code&gt;Symbol&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;结果列表必须包含目标对象的所有不可配置（&lt;code&gt;non-configurable&lt;/code&gt; ）、自有（&lt;code&gt;own&lt;/code&gt;）属性的 &lt;code&gt;key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;如果目标对象不可扩展，那么结果列表必须包含目标对象的所有自有（&lt;code&gt;own&lt;/code&gt;）属性的 &lt;code&gt;key&lt;/code&gt;，不能有其它值.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 &lt;code&gt;Object.keys&lt;/code&gt; 方法时，有三类属性会被 &lt;code&gt;ownKeys&lt;/code&gt; 方法自动过滤，不会返回。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标对象上不存在的属性&lt;/li&gt;
&lt;li&gt;属性名为 &lt;code&gt;Symbol&lt;/code&gt; 值&lt;/li&gt;
&lt;li&gt;不可遍历( &lt;code&gt;enumerable&lt;/code&gt; )的属性&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let target = {
  a: 1,
  b: 2,
  c: 3,
  [Symbol.for(&apos;secret&apos;)]: &apos;4&apos;
}
Object.defineProperty(target, &apos;key&apos;, {
  enumerable: false,
  configurable: true,
  writable: true,
  value: &apos;static&apos;
})
let handler = {
  ownKeys(target) {
    return [&apos;a&apos;, &apos;d&apos;, Symbol.for(&apos;secret&apos;), &apos;key&apos;]
  }
}
let proxy = new Proxy(target, handler)
console.log(Object.keys(proxy)) // [&apos;a&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，&lt;code&gt;ownKeys&lt;/code&gt; 方法之中，显式返回不存在的属性( &lt;code&gt;d&lt;/code&gt; )、&lt;code&gt;Symbol&lt;/code&gt; 值 (&lt;code&gt;Symbol.for(&apos;secret&apos;)&lt;/code&gt;)、不可遍历的属性( &lt;code&gt;key&lt;/code&gt; )，结果都被自动过滤掉。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//拦截 Object.getOwnPropertyNames()
var p = new Proxy(
  {},
  {
    ownKeys: function (target) {
      console.log(&apos;called&apos;) // &quot;called&quot;
      return [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]
    }
  }
)

console.log(Object.getOwnPropertyNames(p)) // [ &apos;a&apos;, &apos;b&apos;, &apos;c&apos; ]

//违反约定
var obj = {}
Object.defineProperty(obj, &apos;a&apos;, {
  configurable: false,
  enumerable: true,
  value: 10
})

var p = new Proxy(obj, {
  ownKeys: function (target) {
    return [123, 12.5, true, false, undefined, null, {}, []]
  }
})

console.log(Object.getOwnPropertyNames(p))

// TypeError: proxy [[OwnPropertyKeys]] 必须返回一个数组
// 数组元素类型只能是String或Symbol

//必须包含目标对象的所有不可配置属性
var obj = {}
Object.defineProperty(obj, &apos;a&apos;, {
  configurable: false,
  enumerable: true,
  value: 10
})
var p = new Proxy(obj, {
  ownKeys: function (target) {
    return [&apos;b&apos;]
  }
})
Object.getOwnPropertyNames(p)
// Uncaught TypeError: &apos;ownKeys&apos; on proxy: trap result did not i nclude &apos;a&apos;

//若对象不可配置，ownKeys 方法返回的数组之中，必须包含原对象的所有属性，且不能包含多余的属性，否则报错
var obj = { a: 1 }
Object.preventExtensions(obj)
var p = new Proxy(obj, {
  ownKeys: function (target) {
    return [&apos;a&apos;, &apos;b&apos;]
  }
})
Object.getOwnPropertyNames(p)
// Uncaught TypeError: &apos;ownKeys&apos; on proxy: trap returned extra k eys but proxy target is non-extensible
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;handler.preventExtensions()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.preventExtensions(target)&lt;/code&gt; 方法用于设置对 &lt;code&gt;Object.preventExtensions()&lt;/code&gt; 的拦截.该方法必须返 回一个布尔值，否则会被自动转为布尔值。&lt;/p&gt;
&lt;p&gt;这个捕捉器可以拦截这些操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Object.preventExtensions()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.preventExtensions()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违反了下列规则, &lt;code&gt;proxy&lt;/code&gt; 则会抛出一个 &lt;code&gt;TypeError&lt;/code&gt;: 如果目标对象是可扩展的，那么只能返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var p = new Proxy(
  {},
  {
    preventExtensions: function (target) {
      console.log(&apos;called&apos;) // &quot;called&quot;
      Object.preventExtensions(target)
      return true
    }
  }
)

console.log(Object.preventExtensions(p)) // false

//违反约定
var p = new Proxy(
  {},
  {
    preventExtensions: function (target) {
      return true
    }
  }
)

Object.preventExtensions(p) // 抛出类型错误
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;handler.setPrototypeOf()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;handler.setPrototypeOf(target, prototype)&lt;/code&gt; 方法主要用来拦截 &lt;code&gt;Object.setPrototypeOf()&lt;/code&gt;。如果成功修改了&lt;code&gt;[[Prototype]]&lt;/code&gt;, &lt;code&gt;setPrototypeOf&lt;/code&gt; 方法返回 &lt;code&gt;true&lt;/code&gt;,否则返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个方法可以拦截以下操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Object.setPrototypeOf()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.setPrototypeOf()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果违反了下列规则，则 &lt;code&gt;proxy&lt;/code&gt; 将抛出一个 &lt;code&gt;TypeError&lt;/code&gt;: 如果 &lt;code&gt;target&lt;/code&gt; 不可扩展, 原型参数必须与 &lt;code&gt;Object.getPrototypeOf(target)&lt;/code&gt; 的值相同.&lt;/p&gt;
&lt;h3&gt;Proxy.revocable()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Proxy.revocable()&lt;/code&gt; 方法可以用来创建一个可撤销的代理对象。该方法的返回值是一个对象，其结构为： &lt;code&gt;{&quot;proxy&quot;: proxy, &quot;revoke&quot;: revoke}&lt;/code&gt;，&lt;code&gt;proxy&lt;/code&gt; 表示新生成的代理对象本身，和用一般方式 &lt;code&gt;new Proxy(target, handler)&lt;/code&gt; 创建的代理对象没什么不同，只是它可以被撤销掉。&lt;code&gt;revoke&lt;/code&gt; 表示撤销方法，调用的时候不需要加任何参数，就可以撤销掉和它一起生成的那个代理对象。&lt;/p&gt;
&lt;p&gt;一旦某个代理对象被撤销，它将变得几乎完全不可调用，在它身上执行任何的可代理操作都会抛出 &lt;code&gt;TypeError&lt;/code&gt; 异常（注意，可代理操作一共有 &lt;code&gt;13&lt;/code&gt; 种，执行这 &lt;code&gt;13&lt;/code&gt; 种操作以外的操作不会抛出异常）。一旦被撤销，这个代理对象便不可能被直接恢复到原来的状态，同时和它关联的目标对象以及处理器对象都有可能被垃圾回收掉。再次调用撤销方法 &lt;code&gt;revoke()&lt;/code&gt; 则不会有任何效果，但也不会报错。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Proxy.revocable&lt;/code&gt; 的一个使用场景是，目标对象不允许直接访问，必须通过代理访问，一旦访问结束，就收回代理权，不允许再次访问。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var revocable = Proxy.revocable(
  {},
  {
    get(target, name) {
      return &apos;[[&apos; + name + &apos;]]&apos;
    }
  }
)
var proxy = revocable.proxy
proxy.foo // &quot;[[foo]]&quot;

revocable.revoke()

console.log(proxy.foo) // 抛出 TypeError
proxy.foo = 1 // 还是 TypeError
delete proxy.foo // 又是 TypeError
typeof proxy // &quot;object&quot;，因为 typeof 不属于可代理操作
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;this 问题&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;let p = new Proxy(target, handler)&lt;/code&gt; 中，我们上面介绍的 &lt;code&gt;handler&lt;/code&gt; 中的捕捉器中的 &lt;code&gt;this&lt;/code&gt; 都是指向 &lt;code&gt;handler&lt;/code&gt; 对象。而通过代理访问的 &lt;code&gt;target&lt;/code&gt; 中的的方法如果有 &lt;code&gt;this&lt;/code&gt;，指向的则是生成的 &lt;code&gt;Proxy&lt;/code&gt; 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const target = {
  m: function () {
    console.log(this === proxy)
  }
}
const handler = {}
const proxy = new Proxy(target, handler)
target.m() // false
proxy.m() // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以虽然 &lt;code&gt;Proxy&lt;/code&gt; 可以代理针对目标对象的访问，但它不是目标对象的透明代理，即不做任何拦截的情况下，也无法保证与目标对象的行为一致。主要原因就是在 &lt;code&gt;Proxy&lt;/code&gt; 代理的情况下，目标对象内部的 &lt;code&gt;this&lt;/code&gt; 关键字会指向 &lt;code&gt;Proxy&lt;/code&gt; 代理。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const _name = new WeakMap()
class Person {
  constructor(name) {
    _name.set(this, name)
  }
  get name() {
    return _name.get(this)
  }
}
const jane = new Person(&apos;Jane&apos;)
jane.name // &apos;Jane&apos;
const proxy = new Proxy(jane, {})
proxy.name // undefined

console.log(Object.getOwnPropertyDescriptor(jane.__proto__, &apos;name&apos;))
//{
//  get: [Function: get name],
//  set: undefined,
//  enumerable: false,
//  configurable: true
//}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，目标对象 &lt;code&gt;jane&lt;/code&gt; 的 &lt;code&gt;name&lt;/code&gt; 属性，实际保存在外部对象 &lt;code&gt;_name&lt;/code&gt; 上面，通过 &lt;code&gt;this&lt;/code&gt; 键区分。由于通过 &lt;code&gt;proxy.name&lt;/code&gt; 访问时，&lt;code&gt;this&lt;/code&gt; 指向 &lt;code&gt;proxy&lt;/code&gt;，导致无法取到值，所以返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以当我们使用代理的时候需要注意源对象方法中的 &lt;code&gt;this&lt;/code&gt;。此外，有些原生对象的内部属性，只有通过正确的 &lt;code&gt;this&lt;/code&gt; 才能拿到，所以 &lt;code&gt;Proxy&lt;/code&gt; 也 无法代理这些原生对象的属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const target = new Date()
const handler = {}
const proxy = new Proxy(target, handler)
proxy.getDate()
// TypeError: this is not a Date object.

const target = new Date(&apos;2015-01-01&apos;)
const handler = {
  get(target, prop) {
    if (prop === &apos;getDate&apos;) {
      return target.getDate.bind(target)
    }
    return Reflect.get(target, prop)
  }
}
const proxy = new Proxy(target, handler)
proxy.getDate() // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Reflect 反射&lt;/h2&gt;
&lt;p&gt;所谓的 &lt;code&gt;Reflect&lt;/code&gt; 反射，一般是用在静态语言中的概念。在计算机学中，反射（英语：&lt;code&gt;reflection&lt;/code&gt;）是指计算机程序在运行时（&lt;code&gt;runtime&lt;/code&gt;）可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说，反射就是程序在运行的时候能够“观察”并且修改自己的行为。这是维基百科给出的定义。&lt;/p&gt;
&lt;p&gt;比如我们经常使用的 &lt;code&gt;Object.getOwnPropertyDescriptor&lt;/code&gt;，&lt;code&gt;Object.keys()&lt;/code&gt; 在其他语言中都会被归类于反射的范畴。那么为什么 &lt;code&gt;ES6&lt;/code&gt; 要推出一个 &lt;code&gt;Reflect&lt;/code&gt; 对象呢，而且这个对象的方法也都是一些原本就存在的方法。我个人认为是早期的 &lt;code&gt;JavaScript&lt;/code&gt; 标准设计没有想到这个问题，在逐步推出一些比较重要的静态方法的过程中就直接通过 &lt;code&gt;Object&lt;/code&gt;，&lt;code&gt;Function&lt;/code&gt; 等内置对象暴露出来（为什么是它们，因为他们在所有对象和函数的原型链上，几乎所有对象都能通过原型链访问到他们的方法），在 &lt;code&gt;API&lt;/code&gt; 逐渐增多的过程中，让这些原本功能比较明确的对象越来越冗余，&lt;code&gt;API&lt;/code&gt; 结构也非常不清晰，甚至有些混乱。&lt;/p&gt;
&lt;p&gt;所以到了 &lt;code&gt;ES6&lt;/code&gt; 大概制定委员会决定解决这个问题，把一些应该归类于反射机制的方法单独用一个对象来暴露，于是就有了 &lt;code&gt;Reflect&lt;/code&gt;。以下是阮一峰老师的 《ES6标准入门》总结的 &lt;code&gt;Reflect&lt;/code&gt; 的设计目的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将 &lt;code&gt;Object&lt;/code&gt; 对象的一些明显属于语言内部的方法（比如 &lt;code&gt;Object.defineProperty&lt;/code&gt;），放到 &lt;code&gt;Reflect&lt;/code&gt; 对象上。目前，因为兼容性的问题，很多方法在 &lt;code&gt;Object&lt;/code&gt; 和 &lt;code&gt;Reflect&lt;/code&gt; 上同时存在，但是以后还会推出新的方法，到时候只会在 &lt;code&gt;Reflect&lt;/code&gt; 对象上添加。&lt;/li&gt;
&lt;li&gt;修改某些不合理的 &lt;code&gt;API&lt;/code&gt;，比如 &lt;code&gt;Object.defineProperty(obj, name, desc)&lt;/code&gt;，在无法定义属性时，会抛出一个错误，而 &lt;code&gt;Reflect.defineProperty(obj, name, desc)&lt;/code&gt; 则会返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;让 &lt;code&gt;Object&lt;/code&gt; 操作都变成函数行为。某些 &lt;code&gt;Object&lt;/code&gt; 操作是命令式，比如 &lt;code&gt;name in obj&lt;/code&gt; 和 &lt;code&gt;delete obj[name]&lt;/code&gt;，而 &lt;code&gt;Reflect.has(obj, prop)&lt;/code&gt; 和 &lt;code&gt;Reflect.deleteProperty(obj, prop)&lt;/code&gt; 是函数行为。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect&lt;/code&gt; 的方法和 &lt;code&gt;Proxy&lt;/code&gt; 的方法一一对应，它们将成为我们进行元编程修改默认行为的固定搭档。无论我在在 &lt;code&gt;Proxy&lt;/code&gt; 内部如何设计逻辑，我们总能通过 &lt;code&gt;Reflect&lt;/code&gt; 获取默认行为。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我个人进行一个总结就是，让 &lt;code&gt;JavaScript&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt; 设计更规范，保持风格的统一，让每个对象都更纯粹，各司其职，更好记更好用。&lt;/p&gt;
&lt;h2&gt;Reflect 用法&lt;/h2&gt;
&lt;p&gt;与大多数全局对象不同 &lt;code&gt;Reflect&lt;/code&gt; 并非一个构造函数，所以不能通过new运算符对其进行调用，或者将 &lt;code&gt;Reflect&lt;/code&gt; 对象作为一个函数来调用。&lt;code&gt;Reflect&lt;/code&gt; 的所有属性和方法都是静态的（就像 &lt;code&gt;Math&lt;/code&gt; 对象）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Reflect&lt;/code&gt; 对象提供了 &lt;code&gt;13&lt;/code&gt; 种静态方法，这些方法与 &lt;code&gt;proxy handler methods&lt;/code&gt; 的命名相同.其中的一些方法与 &lt;code&gt;Object&lt;/code&gt; 相同, 尽管二者之间存在某些细微上的差别 ，差别可以参考 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/%E6%AF%94%E8%BE%83_Reflect_%E5%92%8C_Object_%E6%96%B9%E6%B3%95&quot; title=&quot;比较 Reflect 和 Object 方法&quot;&gt;比较 Reflect 和 Object 方法&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Reflect.apply(target, thisArg, args)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.construct(target, args)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.get(target, name, receiver)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.set(target, name, value, receiver)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.defineProperty(target, name, desc)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.deleteProperty(target, name)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.has(target, name)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.ownKeys(target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.isExtensible(target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.preventExtensions(target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.getOwnPropertyDescriptor(target, name)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.getPrototypeOf(target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.setPrototypeOf(target, prototype)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Reflect.get()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Reflect.get(target, propertyKey[, receiver])&lt;/code&gt; 方法与从 对象 (&lt;code&gt;target[propertyKey]&lt;/code&gt;) 中读取属性类似，但它是通过一个函数执行来操作的。第三个可选参数表示如果 &lt;code&gt;target&lt;/code&gt; 对象中指定了 &lt;code&gt;getter&lt;/code&gt;，&lt;code&gt;receiver&lt;/code&gt; 则为 &lt;code&gt;getter&lt;/code&gt; 调用时的 &lt;code&gt;this&lt;/code&gt; 值。该方法查找并返回 &lt;code&gt;target&lt;/code&gt; 对象的 &lt;code&gt;name&lt;/code&gt; 属性，如果没有该属性，则返回 &lt;code&gt;undefined&lt;/code&gt; 。如果目标值类型不是 &lt;code&gt;Object&lt;/code&gt;，则抛出一个 &lt;code&gt;TypeError&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Object
var obj = { x: 1, y: 2 }
Reflect.get(obj, &apos;x&apos;) // 1

// Array
Reflect.get([&apos;zero&apos;, &apos;one&apos;], 1) // &quot;one&quot;

// Proxy with a get handler
var x = { p: 1 }
var obj = new Proxy(x, {
  get(t, k, r) {
    return k + &apos;bar&apos;
  }
})
Reflect.get(obj, &apos;foo&apos;) // &quot;foobar&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于第三个参数 &lt;code&gt;receiver&lt;/code&gt;，我们需要注意，如果 &lt;code&gt;target&lt;/code&gt; 是一个 &lt;code&gt;Proxy&lt;/code&gt; 并且拦截了 &lt;code&gt;get&lt;/code&gt; 方法，那么这里的 &lt;code&gt;receiver&lt;/code&gt; 是不会生效的，&lt;code&gt;Proxy&lt;/code&gt; 中的 &lt;code&gt;get&lt;/code&gt; 还是指向 &lt;code&gt;handler&lt;/code&gt; 对象。其实只要属性是通过 &lt;code&gt;Proxy&lt;/code&gt; 的 &lt;code&gt;get&lt;/code&gt; 访问到的都是一样的效果，&lt;code&gt;Proxy&lt;/code&gt; 是可以被继承的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let thisObj = { a: 1, b: 2 }
let a = {
  get name() {
    console.log(this)
  }
}
let p = new Proxy(a, {
  get(target, key, receiver) {
    console.log(this)
  }
})

let b = Object.create(p)

Reflect.get(a, &apos;name&apos;, thisObj) // { a: 1, b: 2 }
Reflect.get(p, &apos;name&apos;, thisObj) // { get: [Function: get] }
Reflect.get(b, &apos;name&apos;, thisObj) // { get: [Function: get] }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reflect.set()&lt;/h3&gt;
&lt;p&gt;静态方法 &lt;code&gt;Reflect.set(target, propertyKey, value[, receiver])&lt;/code&gt; 工作方式就像在一个对象上设置一个属性。最后一个可选参数表示如果遇到 &lt;code&gt;setter&lt;/code&gt;，&lt;code&gt;receiver&lt;/code&gt; 则为 &lt;code&gt;setter&lt;/code&gt; 调用时的 &lt;code&gt;this&lt;/code&gt; 值。返回一个 &lt;code&gt;Boolean&lt;/code&gt; 值表明是否成功设置属性。如果目标值类型不是 &lt;code&gt;Object&lt;/code&gt;，则抛出一个 &lt;code&gt;TypeError&lt;/code&gt;。它的作用和属性访问器形式的赋值一样，但是是以函数的形式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Object
var obj = {}
Reflect.set(obj, &apos;prop&apos;, &apos;value&apos;) // true
obj.prop // &quot;value&quot;

// Array
var arr = [&apos;duck&apos;, &apos;duck&apos;, &apos;duck&apos;]
Reflect.set(arr, 2, &apos;goose&apos;) // true
arr[2] // &quot;goose&quot;

// It can truncate an array.
Reflect.set(arr, &apos;length&apos;, 1) // true
arr // [&quot;duck&quot;];

// With just one argument, propertyKey and value are &quot;undefined&quot;.
var obj = {}
Reflect.set(obj) // true
Reflect.getOwnPropertyDescriptor(obj, &apos;undefined&apos;)
// { value: undefined, writable: true, enumerable: true, configurable: true }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;receiver&lt;/code&gt; 的处理和 &lt;code&gt;Reflect.get()&lt;/code&gt; 类似。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Reflect.has()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Reflect.has(target, propertyKey)&lt;/code&gt; 的功能和 &lt;code&gt;in&lt;/code&gt; 操作符完全相同，如果指定的属性在指定的对象或其原型链中，返回 &lt;code&gt;true&lt;/code&gt;。如果第一个参数不是对象， &lt;code&gt;Reflect.has&lt;/code&gt; 和 &lt;code&gt;in&lt;/code&gt; 运算符都会报错。返回值是一个 &lt;code&gt;Boolean&lt;/code&gt; 指示是否存在此属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Reflect.has({ x: 0 }, &apos;x&apos;) // true
Reflect.has({ x: 0 }, &apos;y&apos;) // false

// 如果该属性存在于原型链中，返回true
Reflect.has({ x: 0 }, &apos;toString&apos;)

// Proxy 对象的 .has() 句柄方法
obj = new Proxy(
  {},
  {
    has(t, k) {
      return k.startsWith(&apos;door&apos;)
    }
  }
)
Reflect.has(obj, &apos;doorbell&apos;) // true
Reflect.has(obj, &apos;dormitory&apos;) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reflect.deleteProperty()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Reflect.deleteProperty(target, propertyKey)&lt;/code&gt; 方法等同于 &lt;code&gt;delete obj[name]&lt;/code&gt; ，用于删除对象的属性，区别就是该方法是一个函数。返回值是一个 &lt;code&gt;Boolean&lt;/code&gt; 值表明该属性是否被成功删除，如果删除成功，或者被删除的属性不存在，返回 &lt;code&gt;true&lt;/code&gt;，删除失败，被删除的属性依然存在，返回 &lt;code&gt;false&lt;/code&gt;。如果目标值类型不是 &lt;code&gt;Object&lt;/code&gt;，则抛出一个 &lt;code&gt;TypeError&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Reflect.getPrototypeOf({}) // Object.prototype
Reflect.getPrototypeOf(Object.prototype) // null
Reflect.getPrototypeOf(Object.create(null)) // null

// 如果参数为 Object，返回结果相同
Object.getPrototypeOf({}) // Object.prototype
Reflect.getPrototypeOf({}) // Object.prototype

// 在 ES5 规范下，对于非 Object，抛异常
Object.getPrototypeOf(&apos;foo&apos;) // Throws TypeError
Reflect.getPrototypeOf(&apos;foo&apos;) // Throws TypeError

// 在 ES2015 规范下，Reflect 抛异常, Object 强制转换非 Object
Object.getPrototypeOf(&apos;foo&apos;) // String.prototype
Reflect.getPrototypeOf(&apos;foo&apos;) // Throws TypeError

// 如果想要模拟 Object 在 ES2015 规范下的表现，需要强制类型转换
Reflect.getPrototypeOf(Object(&apos;foo&apos;)) // String.prototype
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var obj = { x: 1, y: 2 }
Reflect.deleteProperty(obj, &apos;x&apos;) // true
obj // { y: 2 }

var arr = [1, 2, 3, 4, 5]
Reflect.deleteProperty(arr, &apos;3&apos;) // true
arr // [1, 2, 3, , 5]

// 如果属性不存在，返回 true
Reflect.deleteProperty({}, &apos;foo&apos;) // true

// 如果属性不可配置，返回 false
Reflect.deleteProperty(Object.freeze({ foo: 1 }), &apos;foo&apos;) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Reflect.construct()&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Reflect.construct(target, argumentsList[, newTarget])&lt;/code&gt; 方法的行为有点像 &lt;code&gt;new&lt;/code&gt; 操作符构造函数 ， 相当于运行 &lt;code&gt;new target(...args)&lt;/code&gt;，这提供了一种不使 用 ，来调用构造函数的方法。&lt;code&gt;target&lt;/code&gt; 是被运行的目标构造函数，&lt;code&gt;argumentsList&lt;/code&gt; 是目标构造函数调用时的参数，第三个可选参数 &lt;code&gt;newTarget&lt;/code&gt;，作为新创建对象的原型对象的 &lt;code&gt;constructor&lt;/code&gt; 属性， 参考 &lt;code&gt;new.target&lt;/code&gt; 操作符，默认值为 &lt;code&gt;target&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;返回值是以 &lt;code&gt;target&lt;/code&gt;（如果 &lt;code&gt;newTarget&lt;/code&gt; 存在，则为 &lt;code&gt;newTarget&lt;/code&gt;）函数为构造函数，&lt;code&gt;argumentList&lt;/code&gt; 为其初始化参数的对象实例。如果 &lt;code&gt;target&lt;/code&gt; 或者 &lt;code&gt;newTarget&lt;/code&gt; 不是构造函数，抛出 &lt;code&gt;TypeError&lt;/code&gt; 异常。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;Reflect.construct&lt;/code&gt; 和 &lt;code&gt;Object.create()&lt;/code&gt; 创建对象有如下不同：当使用 &lt;code&gt;Object.create()&lt;/code&gt; 和 &lt;code&gt;Function.prototype.apply()&lt;/code&gt; 时，如果不使用 &lt;code&gt;new&lt;/code&gt; 操作符调用构造函数，构造函数内部的&lt;code&gt;new.target&lt;/code&gt; 值会指向 &lt;code&gt;undefined&lt;/code&gt;。当调用 &lt;code&gt;Reflect.construct()&lt;/code&gt; 来创建对象，&lt;code&gt;new.target&lt;/code&gt; 值会自动指定到 &lt;code&gt;target&lt;/code&gt;（或者 &lt;code&gt;newTarget&lt;/code&gt;，前提是 &lt;code&gt;newTarget&lt;/code&gt; 指定了）。&lt;/p&gt;
&lt;h5&gt;Reflect.getPrototypeOf()&lt;/h5&gt;
&lt;p&gt;静态方法 &lt;code&gt;Reflect.getPrototypeOf(target)&lt;/code&gt; 与 &lt;code&gt;Object.getPrototypeOf()&lt;/code&gt; 方法几乎是一样的。都是返回指定对象的原型（即内部的 &lt;code&gt;[[Prototype]]&lt;/code&gt; 属性的值）。返回值为给定对象的原型。如果给定对象没有继承的属性，则返回 &lt;code&gt;null&lt;/code&gt;。如果目标值类型不是 &lt;code&gt;Object&lt;/code&gt;，则抛出一个 &lt;code&gt;TypeError&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Reflect.getPrototypeOf({}) // Object.prototype
Reflect.getPrototypeOf(Object.prototype) // null
Reflect.getPrototypeOf(Object.create(null)) // null

// 如果参数为 Object，返回结果相同
Object.getPrototypeOf({}) // Object.prototype
Reflect.getPrototypeOf({}) // Object.prototype

// 在 ES5 规范下，对于非 Object，抛异常
Object.getPrototypeOf(&apos;foo&apos;) // Throws TypeError
Reflect.getPrototypeOf(&apos;foo&apos;) // Throws TypeError

// 在 ES2015 规范下，Reflect 抛异常, Object 强制转换非 Object
Object.getPrototypeOf(&apos;foo&apos;) // String.prototype
Reflect.getPrototypeOf(&apos;foo&apos;) // Throws TypeError

// 如果想要模拟 Object 在 ES2015 规范下的表现，需要强制类型转换
Reflect.getPrototypeOf(Object(&apos;foo&apos;)) // String.prototype
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Reflect.setPrototypeOf()&lt;/h5&gt;
&lt;p&gt;除了返回类型以外，静态方法 &lt;code&gt;Reflect.setPrototypeOf()&lt;/code&gt; 与 &lt;code&gt;Object.setPrototypeOf()&lt;/code&gt; 方法是一样的。它可设置对象的原型（即内部的 &lt;code&gt;[[Prototype]]&lt;/code&gt; 属性）为另一个对象或 &lt;code&gt;null&lt;/code&gt;，如果操作成功返回 &lt;code&gt;true&lt;/code&gt;，否则返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;返回一个 &lt;code&gt;Boolean&lt;/code&gt; 值表明是否原型已经成功设置。如果 &lt;code&gt;target&lt;/code&gt; 不是 &lt;code&gt;Object&lt;/code&gt; ，或 &lt;code&gt;prototype&lt;/code&gt; 既不是对象也不是 &lt;code&gt;null&lt;/code&gt;，抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 异常。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Reflect.setPrototypeOf({}, Object.prototype) // true

// It can change an object&apos;s [[Prototype]] to null.
Reflect.setPrototypeOf({}, null) // true

// Returns false if target is not extensible.
Reflect.setPrototypeOf(Object.freeze({}), null) // false

// Returns false if it cause a prototype chain cycle.
var target = {}
var proto = Object.create(target)
Reflect.setPrototypeOf(target, proto) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Reflect.apply()&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Reflect.apply(target, thisArgument, argumentsList)&lt;/code&gt; 方法等同于 &lt;code&gt;Function.prototype.apply.call(func, thisArg, args)&lt;/code&gt;,用于绑定 &lt;code&gt;this&lt;/code&gt; 对象后执行给定函数。返回值是调用完带着指定参数和 &lt;code&gt;this&lt;/code&gt; 值的给定的函数后返回的结果。如果 &lt;code&gt;target&lt;/code&gt; 对象不可调用，抛出 &lt;code&gt;TypeError&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;原来我们要指定 &lt;code&gt;this&lt;/code&gt; 执行函数，需要 &lt;code&gt;fn.apply(thisArg, args)&lt;/code&gt;， 如果函数重写了 &lt;code&gt;apply&lt;/code&gt; 方法，就需要这样 &lt;code&gt;Function.prototype.apply.call(fn, thisArg, args)&lt;/code&gt;，使用 &lt;code&gt;Reflect.apply&lt;/code&gt; 方法会使代码更加简洁易懂。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Reflect.apply(Math.floor, undefined, [1.75])
// 1;

Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111])
// &quot;hello&quot;

Reflect.apply(RegExp.prototype.exec, /ab/, [&apos;confabulation&apos;]).index
// 4

Reflect.apply(&apos;&apos;.charAt, &apos;ponies&apos;, [3])
// &quot;i&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Reflect.defineProperty()&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Reflect.defineProperty(target, propertyKey, attributes)&lt;/code&gt; 基本等同于 &lt;code&gt;Object.defineProperty()&lt;/code&gt; 方法，唯一不同是返回 &lt;code&gt;Boolean&lt;/code&gt; 值。未来，后者会被逐渐废除，请从现在开始就使用 &lt;code&gt;Reflect.defineProperty&lt;/code&gt; 代替它。返回值是一个 &lt;code&gt;Boolean&lt;/code&gt; 值指示了属性是否被成功定义。如果 &lt;code&gt;target&lt;/code&gt; 不是 &lt;code&gt;Object&lt;/code&gt;，抛出一个 &lt;code&gt;TypeError&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;Reflect.getOwnPropertyDescriptor()&lt;/h5&gt;
&lt;p&gt;该方法与 &lt;code&gt;Object.getOwnPropertyDescriptor()&lt;/code&gt; 方法相似，用于得到指定属性的描述对象，将来 会替代掉后者。。如果属性在对象中存在，则返回给定的属性的属性描述符。否则返回 &lt;code&gt;undefined&lt;/code&gt;。该方法和 &lt;code&gt;Object.getOwnPropertyDescriptor()&lt;/code&gt; 的区别是如果第一个参数不是对象，&lt;code&gt;Object.getOwnPropertyDescriptor()&lt;/code&gt; 返回 &lt;code&gt;undefined&lt;/code&gt;，而 &lt;code&gt;Reflect.getOwnPropertyDescriptor&lt;/code&gt; 会抛出错误。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Reflect.getOwnPropertyDescriptor({ x: &apos;hello&apos; }, &apos;x&apos;)
// {value: &quot;hello&quot;, writable: true, enumerable: true, configurable: true}

Reflect.getOwnPropertyDescriptor({ x: &apos;hello&apos; }, &apos;y&apos;)
// undefined

Reflect.getOwnPropertyDescriptor([], &apos;length&apos;)
// {value: 0, writable: true, enumerable: false, configurable: false}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Reflect.isExtensible()&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Reflect.isExtensible(target)&lt;/code&gt; 判断一个对象是否可扩展 （即是否能够添加新的属性）。与它 &lt;code&gt;Object.isExtensible()&lt;/code&gt; 方法相似，但有一些不同，如果对象是可扩展的，则 &lt;code&gt;Object.isExtensible()&lt;/code&gt; 返回 &lt;code&gt;true&lt;/code&gt;，否则返回 &lt;code&gt;false&lt;/code&gt;。如果第一个参数不是对象（原始值），则在 &lt;code&gt;ES5&lt;/code&gt; 中抛出 &lt;code&gt;TypeError&lt;/code&gt;。在 &lt;code&gt;ES2015&lt;/code&gt; 中，它将被强制为不可扩展的普通对象并返回 &lt;code&gt;false&lt;/code&gt;。如果对象是可扩展的，则 &lt;code&gt;Reflect.isExtensible()&lt;/code&gt; 返回 &lt;code&gt;true&lt;/code&gt;，否则返回 &lt;code&gt;false&lt;/code&gt;。如果第一个参数不是对象（原始值），则抛出 &lt;code&gt;TypeError&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// New objects are extensible.
var empty = {}
Reflect.isExtensible(empty) // === true

// ...but that can be changed.
Reflect.preventExtensions(empty)
Reflect.isExtensible(empty) // === false

// Sealed objects are by definition non-extensible.
var sealed = Object.seal({})
Reflect.isExtensible(sealed) // === false

// Frozen objects are also by definition non-extensible.
var frozen = Object.freeze({})
Reflect.isExtensible(frozen) // === false
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Reflect.preventExtensions()&lt;/h5&gt;
&lt;p&gt;静态方法 &lt;code&gt;Reflect.preventExtensions(target)&lt;/code&gt; 方法阻止新属性添加到对象 (例如：防止将来对对象的扩展被添加到对象中)。该方法与 &lt;code&gt;Object.preventExtensions()&lt;/code&gt; 相似，但有一些不同点。返回一个 &lt;code&gt;Boolean&lt;/code&gt; 值表明目标对象是否成功被设置为不可扩展。抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 错误，如果 &lt;code&gt;target&lt;/code&gt; 不是 &lt;code&gt;Object&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Objects are extensible by default.
var empty = {}
Reflect.isExtensible(empty) // === true

// ...but that can be changed.
Reflect.preventExtensions(empty)
Reflect.isExtensible(empty) // === false
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Reflect.ownKeys()&lt;/h5&gt;
&lt;p&gt;方法用于返回对象的所有属性，基本等同于 &lt;code&gt;Object.getOwnPropertyNames()&lt;/code&gt; 与 &lt;code&gt;Object.getOwnPropertySymbols&lt;/code&gt; 之和。由目标对象的自身属性键组成的 &lt;code&gt;Array&lt;/code&gt;，它的返回值等同于&lt;code&gt;Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))&lt;/code&gt;。如果目标不是 &lt;code&gt;Object&lt;/code&gt;，抛出一个 &lt;code&gt;TypeError&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Reflect.ownKeys({ z: 3, y: 2, x: 1 }) // [ &quot;z&quot;, &quot;y&quot;, &quot;x&quot; ]
Reflect.ownKeys([]) // [&quot;length&quot;]

var sym = Symbol.for(&apos;comet&apos;)
var sym2 = Symbol.for(&apos;meteor&apos;)
var obj = { [sym]: 0, str: 0, 773: 0, 0: 0, [sym2]: 0, &apos;-1&apos;: 0, 8: 0, &apos;second str&apos;: 0 }
Reflect.ownKeys(obj)
// [ &quot;0&quot;, &quot;8&quot;, &quot;773&quot;, &quot;str&quot;, &quot;-1&quot;, &quot;second str&quot;, Symbol(comet), Symbol(meteor) ]
// Indexes in numeric order,
// strings in insertion order,
// symbols in insertion order
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Proxy&lt;/code&gt; 在我们的日常编码中使用不是很多，由于他提供的功能能够让我们对语言的行为进行定制，所以很多框架和底层库都会使用，比如 &lt;code&gt;Vue3&lt;/code&gt; 就把双向数据绑定从 &lt;code&gt;Object.defineProperty&lt;/code&gt; 改成了用 &lt;code&gt;Proxy&lt;/code&gt; 实现，理解代理行为有助于我们理解这些框架和库。而 &lt;code&gt;Reflect&lt;/code&gt; 中的很多方法将会逐步取代 &lt;code&gt;Object&lt;/code&gt;，所以我们应该尽量使用。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;《ES6标准入门》 —— 阮一峰&lt;/li&gt;
&lt;li&gt;MDN&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>JavaScript 大数相加相乘实现</title><link>https://clloz.com/blog/javascript-bignumber-add-multiply</link><guid isPermaLink="true">https://clloz.com/blog/javascript-bignumber-add-multiply</guid><pubDate>Sun, 18 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中的最大安全整数是 $2 ^{53} - 1$，即 &lt;code&gt;9007199254740991&lt;/code&gt;，当我们进行超出这个范围的数值计算的时候就无法得到精确的值，而是一个近似值，比如我们计算 &lt;code&gt;9007199254740991 + 10&lt;/code&gt; 得到的结果是 &lt;code&gt;9007199254741000&lt;/code&gt;。本文讲一下如何利用字符串在 &lt;code&gt;JavaScript&lt;/code&gt; 中实现大数相加相乘。&lt;/p&gt;
&lt;h2&gt;相加&lt;/h2&gt;
&lt;p&gt;用字符串实现相加相乘基本思路就是按照我们在纸上进行竖式运算一样。对于加法，我们需要将两个数 &lt;code&gt;num1&lt;/code&gt; 和 &lt;code&gt;num2&lt;/code&gt; 上下对齐，然后从个位开始计算两个数对应位的和，循环到最高位，将每一次运算的结果保存到一个数组 &lt;code&gt;result&lt;/code&gt; 中去，最终用 &lt;code&gt;Array.prototype.join()&lt;/code&gt; 方法还原成一个数组。&lt;/p&gt;
&lt;p&gt;这里为了保持循环的正常进行，我们需要保证两个字符串位数相等，所以我们要用 &lt;code&gt;String.prototpye.padStart()&lt;/code&gt; 方法将位数比较小的那一个字符串的前面用 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 补齐。&lt;/p&gt;
&lt;p&gt;按位相加有个问题就是进位如何保存，我的思路是这样的。当我们相加 &lt;code&gt;num1[i]&lt;/code&gt; 和 &lt;code&gt;num2[i]&lt;/code&gt; 的时候，得到的最多是一个两位数，它将影响 &lt;code&gt;result&lt;/code&gt; 的两位，即当前的 &lt;code&gt;result[0]&lt;/code&gt; 位置和即将 &lt;code&gt;unshift&lt;/code&gt; 到 &lt;code&gt;result&lt;/code&gt; 中的一位。当前的 &lt;code&gt;result[0]&lt;/code&gt; 位置的数就是计算 &lt;code&gt;[i -1]&lt;/code&gt; 是得到的数的高位（即进位），我们将我们计算的值加上进位，得到的数在分成两位分别放到 &lt;code&gt;result&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;所以总结一下就是我们计算 &lt;code&gt;num1[i] + num2[i]&lt;/code&gt; 得到一个两位数，这个两位数要先和 &lt;code&gt;num1[i-1] + num2[i-1]&lt;/code&gt; 的结果的进位（即 &lt;code&gt;result[0]&lt;/code&gt; 相加，然后在分成 &lt;code&gt;high&lt;/code&gt; 和 &lt;code&gt;low&lt;/code&gt; 两位，将 &lt;code&gt;result[0]&lt;/code&gt; 的值用 &lt;code&gt;low&lt;/code&gt; 位替换，然后将 &lt;code&gt;high&lt;/code&gt; 位 &lt;code&gt;unshift&lt;/code&gt; 到 &lt;code&gt;result&lt;/code&gt; 最前面。可以参考下图理解。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/bignumber1.CAShLZCd_Z13G1AD.webp&quot; alt=&quot;bignumber1&quot; title=&quot;bignumber1&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以我们每次计算都是确定一位和下一位的进位。最后代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let add = function (num1, num2) {
  if (isNaN(num1) || isNaN(num2)) return &apos;&apos;
  if (num1 === &apos;0&apos; || num2 === &apos;0&apos;) return num1 === &apos;0&apos; ? num2 : num1

  let len = Math.max(num1.length, num2.length)
  num1 = num1.padStart(len, &apos;0&apos;)
  num2 = num2.padStart(len, &apos;0&apos;)

  let result = []

  for (let i = len - 1; i &gt;= 0; i--) {
    let sum = Number(num1[i]) + Number(num2[i]) + (result[0] || 0)
    let low = sum % 10
    let high = Math.floor(sum / 10)

    result[0] = low
    result.unshift(high)
  }
  return result.join(&apos;&apos;)
}

console.log(add(&apos;10&apos;, &apos;9007199254740991&apos;)) //09007199254741001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中我们加了两个判断，判断两个参数是否是合法数字格式，以及如果一个数是 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 则直接返回另一个数。&lt;/p&gt;
&lt;h2&gt;相乘&lt;/h2&gt;
&lt;p&gt;相乘的逻辑要比相加复杂一点，但是总体思路还是根据竖式来实现算法，我画了一张图，我们借助图来说明。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/bignumber2.BUSYgHi7_vbBpt.webp&quot; alt=&quot;bignumber2&quot; title=&quot;bignumber2&quot;&gt;&lt;/p&gt;
&lt;p&gt;相乘是一个两层循环，我们要循环一个数的位，每一位再与另一个数循环的每一位相乘。我们每次相乘的结果最多是一个两位数。但是与相加不同的是，相加的 &lt;code&gt;high&lt;/code&gt; 每次都是 &lt;code&gt;unshift&lt;/code&gt; 进去即可，而相乘的高位也要与 &lt;code&gt;result&lt;/code&gt; 的位进行运算。&lt;/p&gt;
&lt;p&gt;我们来看一看相乘的规律，当我们用 &lt;code&gt;num1[i] * num2[j]&lt;/code&gt; 的时候，可能得到两位数，也可能得到一位数，我们都统一算作两位数，高位没有的就用 &lt;code&gt;0&lt;/code&gt; 补齐，那么最后我们得到的结果将是一个 &lt;code&gt;i + j&lt;/code&gt; 位的数（开头可能存在补齐的 &lt;code&gt;0&lt;/code&gt;）。而我们每次计算 &lt;code&gt;num1[i] * num2[j]&lt;/code&gt; 的结果影响到的都是 &lt;code&gt;result&lt;/code&gt; 中的 &lt;code&gt;i + j&lt;/code&gt; 和 &lt;code&gt;i + j + 1&lt;/code&gt; 位。&lt;/p&gt;
&lt;p&gt;和加法中逻辑一样，我们将 &lt;code&gt;num1[i] * num2[j]&lt;/code&gt; 的结果和 &lt;code&gt;result[i + j + 1]&lt;/code&gt; 相加，得到的结果分为 &lt;code&gt;low&lt;/code&gt; 和 &lt;code&gt;high&lt;/code&gt; 分别存入 &lt;code&gt;reslut&lt;/code&gt; 的 &lt;code&gt;[i + j +1]&lt;/code&gt; 和 &lt;code&gt;[i +j]&lt;/code&gt; 中。但是这里要注意，和加法不同，加法的高位直接存入就可以，我们这里的 &lt;code&gt;high&lt;/code&gt; 对应的 &lt;code&gt;result[i + j]&lt;/code&gt; 可能已经有值了，我们需要将已经存在的值加上。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;high&lt;/code&gt; 和 &lt;code&gt;result[i +j]&lt;/code&gt; 的相加可能存在进位怎么办呢，看上图中右边的当前 &lt;code&gt;result&lt;/code&gt; 值中我们可以看到有些位存了不止一位数，我们将 &lt;code&gt;high + result[i +j]&lt;/code&gt; 的值直接连进位一起保存到 &lt;code&gt;result[i + j]&lt;/code&gt; 中。为什么能这样做呢，因为下次计算 &lt;code&gt;num1[i] * num2[j - 1]&lt;/code&gt; 的时候（注意我们是从后往前遍历），会把 &lt;code&gt;result[i + j]&lt;/code&gt;和 &lt;code&gt;low&lt;/code&gt; 相加，进位自然能被处理，这也是这个算法比较重要的地方。&lt;/p&gt;
&lt;p&gt;最后的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let multiply = function (num1, num2) {
  if (isNaN(num1) || isNaN(num2)) return &apos;&apos;
  if (num1 === &apos;0&apos; || num2 === &apos;0&apos;) return &apos;0&apos;

  let l1 = num1.length,
    l2 = num2.length

  let result = []

  for (let i = l1 - 1; i &gt;= 0; i--) {
    for (let j = l2 - 1; j &gt;= 0; j--) {
      let index1 = i + j
      let index2 = i + j + 1

      let product = num1[i] * num2[j] + (result[index2] || 0)
      result[index2] = product % 10
      result[index1] = Math.floor(product / 10) + (result[index1] || 0)
    }
  }
  return result.join(&apos;&apos;).replace(/^0+/, &apos;&apos;)
}

console.log(multiply(&apos;123&apos;, &apos;234&apos;)) //28782
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中加了两个判断：是否是合法数字，如果有一个值为 &lt;code&gt;0&lt;/code&gt; 则直接返回 &lt;code&gt;0&lt;/code&gt;。注意最后要判断得到的结果是否开头有 &lt;code&gt;0&lt;/code&gt;，如果有则要去掉，这里用的正则表达式。&lt;/p&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>深入发布订阅模式</title><link>https://clloz.com/blog/observer-pub-sub-pattern</link><guid isPermaLink="true">https://clloz.com/blog/observer-pub-sub-pattern</guid><pubDate>Sun, 18 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;前端其实涉及到的设计模式不多，不过现在的主流框架都是用了发布订阅模式，所以这个设计模式也经常被提到，也是面试的时候的高频问题，这篇文章我们来深入讨论一下什么是发布订阅模式，为什么要使用发布订阅模式以及如何实现发布订阅模式。&lt;/p&gt;
&lt;h2&gt;观察者模式 Observer pattern&lt;/h2&gt;
&lt;p&gt;在讨论发布订阅模式之前，我们先来讨论一下观察者模式。这两者的思想是一致的，只是具体的实现不同，应用场景也不同，可以说发布订阅模式是观察者模式的一种更优化的实现。但是我觉的观察者模式更容易理解，我们也经常使用，所以作为前置。&lt;/p&gt;
&lt;p&gt;观察者模式说起来很简单，我们希望对一个目标进行 &lt;code&gt;observe&lt;/code&gt;，当这个目标发生变化的时候告诉我们。其实这听上去和回调函数很像，其实回调函数就是一种特殊的观察者模式。真正的观察者模式需要是一对多的，被观察者 &lt;code&gt;Subject&lt;/code&gt; 会管理一个观察者列表，当这个 &lt;code&gt;Subject&lt;/code&gt; 发生某个变化的时候会通知观察者列表中的所有观察者。大概关系如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/observer1.BaHX_Jix_10kPQ.webp&quot; alt=&quot;observer1&quot; title=&quot;observer1&quot;&gt;&lt;/p&gt;
&lt;p&gt;有没有感觉这个模式非常熟悉，这和我们经常使用的 &lt;code&gt;DOM&lt;/code&gt; 事件模式完全相同。我们的 &lt;code&gt;DOM&lt;/code&gt; 事件也就是一个观察者模式的实现。当我们为一个元素绑定事件的时候，我们就相当于进行了 &lt;code&gt;Subscribe&lt;/code&gt; 订阅，成为了一个观察者。当对应的事件触发的时候，被观察的元素会通知我们，执行我们的回调函数（所谓的通知其实就是执行观察者中的对应方法，并不是真的发个通知）。我们也可以绑定多个 &lt;code&gt;Observer&lt;/code&gt;，当事件触发的时候，所有的观察者都会被通知。&lt;code&gt;DOM&lt;/code&gt; 提供了我们订阅 &lt;code&gt;addEventListener&lt;/code&gt;，和取消订阅 &lt;code&gt;removeEventListener&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以观察者模式有两个部分；观察者和被观察对象。而主要的实现都是在被观察对象上，一个模拟的观察者模式的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 观察者
class Observer {
  constructor() {}
  update(val) {}
}
// 观察者列表
class ObserverList {
  constructor() {
    this.observerList = []
  }
  add(observer) {
    return this.observerList.push(observer)
  }
  remove(observer) {
    this.observerList = this.observerList.filter((ob) =&gt; ob !== observer)
  }
  count() {
    return this.observerList.length
  }
  get(index) {
    return this.observerList(index)
  }
}
// 目标
class Subject {
  constructor() {
    this.observers = new ObserverList()
  }
  addObserver(observer) {
    this.observers.add(observer)
  }
  removeObserver(observer) {
    this.observers.remove(observer)
  }
  notify(...args) {
    let obCount = this.observers.count()
    for (let i = 0; i &amp;#x3C; obCount; i++) {
      this.observers.get(i).update(...args)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到所以的逻辑都是在被观察者 &lt;code&gt;Subject&lt;/code&gt; 上，它提供了添加观察者，删除观察者和通知等方法，有一个管理观察者列表的机制，当事件触发的时候，会执行列表中所有观察者的对应方法。&lt;/p&gt;
&lt;p&gt;观察者模式的逻辑是非常简单的，特别是我们可以和经常使用的 &lt;code&gt;DOM&lt;/code&gt; 事件结合起来。它主要解决的问题就是我们希望在某种情况下执行我们的代码，但我们不确定这个情况什么时候发生，比如浏览器事件（我们不知道用户什么时候点击，但我们希望用户点击的时候执行某一段代码）。&lt;/p&gt;
&lt;p&gt;最后在放一个用 &lt;code&gt;Proxy&lt;/code&gt; 实现的简单的观察者模式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let fnArr = new Set()
function observable(obj) {
  return new Proxy(obj, {
    set: function (target, prop, value, receiver) {
      Reflect.set(target, prop, value)
      for (let fn of fnArr) {
        console.log(fn)
        fn()
      }
    }
  })
}

function observe(fn) {
  fnArr.add(fn)
}

const person = observable({
  name: &apos;张三&apos;,
  age: 20
})
function print() {
  console.log(`${person.name}, ${person.age}`)
}
observe(print)
person.name = &apos;李四&apos; // 李四, 20
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;发布订阅模式 Pub-sub pattern&lt;/h2&gt;
&lt;p&gt;有了观察者模式，为什么又要有发布订阅模式呢？观察者模式中，观察者和目标是依赖的，耦合性很强。&lt;/p&gt;
&lt;p&gt;我们可以想象一个场景，我们有多个目标要进行监听，但是这些目标事件触发后执行的逻辑其实是相同的，如果是使用观察者模式我们不得不对每一个目标都进行绑定，并且每一个目标都要事先一套上面 &lt;code&gt;Subject&lt;/code&gt; 的逻辑。这样处理在系统越来越复杂的情况下代码的逻辑会非常混乱，也非常难以管理，并且有非常多冗余的部分。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，就有了发布订阅模式。他们的本质都是一样的，都是对于未来会发生的事件进行一个监听，当事件发生了，通知监听的对象执行对应的方法。但是他们实现的逻辑不同。看下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/observer2.BqAdb97u_1XT9Wt.webp&quot; alt=&quot;observer2&quot; title=&quot;observer2&quot;&gt;&lt;/p&gt;
&lt;p&gt;在发布订阅模式中，添加了一个事件通道，发布者和订阅者不在直接进行交互。所有的注册，解绑，发布都是通过 &lt;code&gt;Event Channel&lt;/code&gt; 来实现的。也就是说我们把观察者模式中的逻辑抽象出来，形成了一个单独的模块。&lt;/p&gt;
&lt;p&gt;现在整个的逻辑大概是这样：我们不再是对某个对象进行监听，而是告诉 &lt;code&gt;Event Channel&lt;/code&gt;，我想要注册一个名叫 &lt;code&gt;type&lt;/code&gt; 的事件，当这个事件触发以后，请执行 &lt;code&gt;fn&lt;/code&gt; 函数。事件中心将我的注册信息进行保存。当有一个模块想要执行订阅者的对应方法的时候，只要告诉 &lt;code&gt;Event Channel&lt;/code&gt;，我想要触发 &lt;code&gt;type&lt;/code&gt; 事件，并且传入参数 &lt;code&gt;arg1, arg2 ...&lt;/code&gt;。&lt;code&gt;Event Channel&lt;/code&gt; 就会找到对应事件对应的 &lt;code&gt;fn&lt;/code&gt; 传入 &lt;code&gt;arg1, arg2 ...&lt;/code&gt; 并执行。&lt;/p&gt;
&lt;p&gt;我们可以看到 &lt;code&gt;Event Channel&lt;/code&gt; 就像一个消息中介，调度中心，它让发布者和订阅者之间完全解耦，它们甚至不知道对方的存在。我们将所有的订阅发布行为进行集中的管理，并且能够定制我们的订阅发布行为，让不同模块之间的通信业变得非常便捷。我们看下面的模拟实现发布订阅代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class PubSub {
  constructor() {
    this.subscribers = {}
  }
  subscribe(type, fn) {
    let listeners = this.subscribers[type] || []
    listeners.push(fn)
  }
  unsubscribe(type, fn) {
    let listeners = this.subscribers[type]
    if (!listeners || !listeners.length) return
    this.subscribers[type] = listeners.filter((v) =&gt; v !== fn)
  }
  publish(type, ...args) {
    let listeners = this.subscribers[type]
    if (!listeners || !listeners.length) return
    listeners.forEach((fn) =&gt; fn(...args))
  }
}

let ob = new PubSub()
ob.subscribe(&apos;add&apos;, (val) =&gt; console.log(val))
ob.publish(&apos;add&apos;, 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比较观察者模式和发布订阅模式的代码我们可以发现，观察者模式由具体目标调度，每个被订阅的目标里面都需要有对观察者的处理，会造成代码的冗余。而发布订阅模式则统一由调度中心处理，消除了发布者和订阅者之间的依赖。&lt;/p&gt;
&lt;p&gt;我们可以结合生活中的实例来帮助你理解，比如你周五想约朋友去吃饭，你不知道谁有空，按观察者模式的逻辑来说，你得给每个可能的朋友发条信息：&lt;code&gt;如果触发有空事件请给我打电话&lt;/code&gt;（注册），当朋友触发 &lt;code&gt;有空&lt;/code&gt; 事件的时候，将会对你进行通知（执行 &lt;code&gt;打电话&lt;/code&gt; 方法）。如果是订阅发布模式的话逻辑就是，你向 &lt;code&gt;朋友圈&lt;/code&gt;（这里相当于 &lt;code&gt;Event Channel&lt;/code&gt;） 注册了一个事件，名字叫 &lt;code&gt;有空&lt;/code&gt;，方法是 &lt;code&gt;打电话&lt;/code&gt;，当有朋友想要参与聚会的时候，会告诉 &lt;code&gt;朋友圈&lt;/code&gt; （&lt;code&gt;Event Channel&lt;/code&gt;），你要发布 &lt;code&gt;有空&lt;/code&gt; 事件，参数是 &lt;code&gt;185xxxxxxxx&lt;/code&gt;，朋友圈用这个参数执行了 &lt;code&gt;打电话&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;不知道上面这个例子有没有帮助你理解观察者模式和订阅发布模式的逻辑和区别。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;正因为观察订阅模式的这种机制，它成了很多框架和库用来实现模块之间通信的方式。比如 &lt;code&gt;Vue&lt;/code&gt; 的 &lt;code&gt;Event(bus)&lt;/code&gt;，&lt;code&gt;React&lt;/code&gt; 的 &lt;code&gt;Event&lt;/code&gt; 模块，他们都用来实现非父子组件的通信。实际上几乎所有的模块通信都是基于类似的模式,包括安卓开发中的&lt;code&gt;Event Bus&lt;/code&gt;，&lt;code&gt;Node.js&lt;/code&gt; 中的 &lt;code&gt;Event&lt;/code&gt; 模块( &lt;code&gt;Node&lt;/code&gt; 中几乎所有的模块都依赖于 &lt;code&gt;Event&lt;/code&gt;,包括不限于 &lt;code&gt;http&lt;/code&gt;、&lt;code&gt;stream&lt;/code&gt;、&lt;code&gt;buffer&lt;/code&gt;、&lt;code&gt;fs&lt;/code&gt; 等)。&lt;/p&gt;
&lt;p&gt;这一小节我们就仿照 &lt;code&gt;NodeJS&lt;/code&gt; 的 &lt;code&gt;Event API&lt;/code&gt; 实现一个简单的 &lt;code&gt;Event&lt;/code&gt; 库。&lt;/p&gt;
&lt;p&gt;我们的大致需求是：实现一个 &lt;code&gt;Emitter&lt;/code&gt; 类，该类能够实现事件的订阅，解绑，发布。事件的存储使用 &lt;code&gt;Map&lt;/code&gt;。对于同一个类型的事件，支持多次绑定（即传入多个方法），当事件发布的时候，这些方法都将执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Emitter {
  constructor() {
    this._event = this._event || new Map()
    this.maxListeners = 10
  }

  addEventListener(type, fn) {
    const handler = this._event.get(type)

    if (!handler) {
      this._event.set(type, fn)
    } else {
      if (handler &amp;#x26;&amp;#x26; typeof handler === &apos;function&apos;) {
        this._event.set(type, [handler, fn])
      } else {
        handler.push(fn)
      }
    }
  }

  removeEventListener(type, fn) {
    const handler = this._event.get(type)

    if (handler &amp;#x26;&amp;#x26; typeof handler === &apos;function&apos;) {
      if (handler === fn) this._event.delete(type)
    } else {
      let newHandler = handler.filter((v) =&gt; v !== fn)
      if (newHandler.length === 1) {
        this._event.set(type, newHandler[0])
      } else {
        this._event.set(type, newHandler)
      }
    }
  }

  emit(type, ...args) {
    const handler = this._event.get(type)

    if (Array.isArray(handler)) {
      handler.forEach((fn) =&gt; {
        fn.apply(this, args)
      })
    } else {
      handler.apply(this, args)
    }
    return true
  }
}

let emitter = new Emitter()

emitter.addEventListener(&apos;change&apos;, (obj) =&gt; {
  console.log(`name is ${obj.name}.`)
})

emitter.addEventListener(&apos;change&apos;, (obj) =&gt; {
  console.log(`age is ${obj.age}.`)
})

emitter.addEventListener(&apos;change&apos;, (obj) =&gt; {
  console.log(`sex is ${obj.sex}.`)
})

function site(obj) {
  console.log(`site is ${obj.site}`)
}

emitter.addEventListener(&apos;change&apos;, site)

emitter.emit(&apos;change&apos;, {
  name: &apos;clloz&apos;,
  age: 28,
  sex: &apos;male&apos;,
  site: &apos;clloz.com&apos;
})
//name is clloz.
//age is 28.
//sex is male.
//site is clloz.com

emitter.removeEventListener(&apos;change&apos;, site)

emitter.emit(&apos;change&apos;, {
  name: &apos;clloz&apos;,
  age: 28,
  sex: &apos;male&apos;,
  site: &apos;clloz.com&apos;
})
//name is clloz.
//age is 28.
//sex is male.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你有兴趣也可以研究一下 &lt;code&gt;browserify&lt;/code&gt; 的 &lt;code&gt;Event&lt;/code&gt; 模块实现：&lt;a href=&quot;https://github.com/browserify/events/blob/master/events.js&quot; title=&quot;event.js - browserify&quot;&gt;event.js - browserify&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文对观察者模式和发布订阅模式进行了比较深入的分析，也进行了简单的模拟实现，希望对你的理解有所帮助。如果有错漏之处欢迎指正。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844903587043082247&quot; title=&quot;面试官:既然React/Vue可以用Event Bus进行组件通信,你可以实现下吗?&quot;&gt;面试官:既然React/Vue可以用Event Bus进行组件通信,你可以实现下吗?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/23486749/answer/314072549&quot; title=&quot;观察者模式和发布订阅模式有什么不同？ - 无邪气的回答 - 知乎&quot;&gt;观察者模式和发布订阅模式有什么不同？ - 无邪气的回答 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/60324936&quot; title=&quot;面试题， 实现一个Event类（发布订阅模式）&quot;&gt;面试题， 实现一个Event类（发布订阅模式）&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>JS事件详解</title><link>https://clloz.com/blog/js-event</link><guid isPermaLink="true">https://clloz.com/blog/js-event</guid><pubDate>Wed, 14 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JS&lt;/code&gt; 和 &lt;code&gt;HTML&lt;/code&gt; 之间的交互是通过事件来实现的，在我们的页面加载完毕，所有的 &lt;code&gt;html，css，js&lt;/code&gt; 文件都已经 &lt;code&gt;load&lt;/code&gt; 的情况下，我们如何在文档或浏览器窗口发生变化时通过 &lt;code&gt;js&lt;/code&gt; 来进行交互，这就是事件产生的原因。我们在 &lt;code&gt;js&lt;/code&gt; 中预定一个事件的处理程序，然后浏览器的监听程序监听各种事件的发生，当事件发生的时候调用预定的事件处理程序来执行。这种观察员模式的模型让我们实现了 &lt;code&gt;js&lt;/code&gt; 和 &lt;code&gt;html，css&lt;/code&gt; 之间的松散耦合。&lt;/p&gt;
&lt;h2&gt;事件流&lt;/h2&gt;
&lt;p&gt;由于 &lt;code&gt;DOM&lt;/code&gt; 结构是层层嵌套的，所以监听程序会遇到一个问题，就是当我们点击一个位置的时候，这个位置有多个 &lt;code&gt;DOM&lt;/code&gt; 节点嵌套在一起，那么我们到底点击的谁呢？&lt;code&gt;Javascript高级程序设计&lt;/code&gt; 给出了一个更形象的比喻，在纸上画一组同心圆，然后把手指指向圆心，此时你指向的不仅仅是一个圆，而是一串同心圆。同理当你点击一个 &lt;code&gt;DOM&lt;/code&gt; 元素，你不仅点击了这个元素，也点击了所有父元素，包括页面本身。由于这个问题，才产生了事件流，也就是一个事件发生了，这个事件会在嵌套的 &lt;code&gt;DOM&lt;/code&gt; 结构里面传播，而事件流就描述了这个传播的具体规则，嵌套的 &lt;code&gt;DOM&lt;/code&gt; 元素是按照什么顺序接收事件的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;即使你没深入学习过事件，也应该听过事件捕获和事件冒泡，它们也只不过是浏览器大战中IE和网景对事件流的不同理解而产生的不同实现，IE选择了事件冒泡流，而网景选择了事件捕获流。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;事件冒泡流&lt;/h2&gt;
&lt;p&gt;先来说一说IE的事件冒泡流，事件冒泡流的设计思路是事件最开始是由事件的目标事件（当前点击的位置所嵌套的DOM结构的嵌套最深的那个节点）接收，然后沿着 &lt;code&gt;DOM&lt;/code&gt; 树逐级向上传播一直到根节点也就是 &lt;code&gt;document&lt;/code&gt; 节点。一下面的 &lt;code&gt;HTML&lt;/code&gt; 文档为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div class=&quot;btn&quot;&gt;Click me!&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们点击了 &lt;code&gt;Click&lt;/code&gt; 这个按钮以后按事件冒泡流的规则，事件会按照如下顺序传播 1. &lt;code&gt;&amp;#x3C;div&gt;&lt;/code&gt; 2. &lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt; 3. &lt;code&gt;&amp;#x3C;html&gt;&lt;/code&gt; 4. &lt;code&gt;document&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;也就是 &lt;code&gt;click&lt;/code&gt; 事件首先发生在 &lt;code&gt;div&lt;/code&gt; 上，也就是我们单击的元素，然后 &lt;code&gt;click&lt;/code&gt; 事件沿着 &lt;code&gt;DOM&lt;/code&gt; 树向上传播，在每一级的 &lt;code&gt;DOM&lt;/code&gt; 节点上都会发生，直到传播到 &lt;code&gt;DOM&lt;/code&gt; 树的根节点 &lt;code&gt;document&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/event-bubbling.CKNq-P4w_Z46gql.webp&quot; alt=&quot;event-bubbling&quot; title=&quot;event-bubbling&quot;&gt;&lt;/p&gt;
&lt;p&gt;所有现代浏览器都支持事件冒泡，但在具体实现上还是有一些差别。&lt;code&gt;IE5.5&lt;/code&gt; 及更早版本中的事件冒 泡会跳过&lt;code&gt;&amp;#x3C;html&gt;&lt;/code&gt;元素(从 &lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt; 直接跳到 &lt;code&gt;document&lt;/code&gt;)。&lt;code&gt;IE9&lt;/code&gt; 、 &lt;code&gt;Firefox&lt;/code&gt; 、 &lt;code&gt;Chrome&lt;/code&gt; 和 &lt;code&gt;Safari&lt;/code&gt; 则将事件一直 冒泡到 &lt;code&gt;window&lt;/code&gt; 对象。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;目标事件，也就是我们后面提到的 &lt;code&gt;DOM2&lt;/code&gt; 里面 &lt;code&gt;event&lt;/code&gt; 对象的 &lt;code&gt;target&lt;/code&gt; 属性，我看网上的很多解释都不清晰，其实直白的说就是事件触发的坐标所在位置 &lt;code&gt;DOM&lt;/code&gt; 结构嵌套最深的那个节点，也即是在 &lt;code&gt;DOM&lt;/code&gt; 树上最深的节点，这个节点也就是所谓的 &lt;code&gt;target&lt;/code&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;事件捕获&lt;/h2&gt;
&lt;p&gt;网景对于事件流的实现和 &lt;code&gt;IE&lt;/code&gt; 则是截然相反的，也就是目标节点最后接收事件， &lt;code&gt;DOM&lt;/code&gt; 树上的上层节点则更早地接收事件。还以上面的 &lt;code&gt;HTML&lt;/code&gt; 代码作为例子，在事件捕获流中，事件的传播顺序如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;document&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;html&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;div&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在事件捕获流中，&lt;code&gt;document&lt;/code&gt; 对象首先接收到事件，然后沿着 &lt;code&gt;DOM&lt;/code&gt; 树逐级向下传播，一直传播到目标节点 &lt;code&gt;div&lt;/code&gt; 元素，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/event-capturing.BAkdeyBa_Z1UjarY.webp&quot; alt=&quot;event-capturing&quot; title=&quot;event-capturing&quot;&gt;&lt;/p&gt;
&lt;h2&gt;DOM事件流&lt;/h2&gt;
&lt;p&gt;事件捕获只能在事件传播到目标元素之前截获，而事件冒泡只能在事件已经传播到目标元素之后进行截获，所以 &lt;code&gt;DOM二级事件&lt;/code&gt; 把两者结合了起来，&lt;code&gt;DOM二级事件&lt;/code&gt; 规定了事件传播的三个阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;事件捕获阶段&lt;/li&gt;
&lt;li&gt;处于目标事件阶段&lt;/li&gt;
&lt;li&gt;事件冒泡阶段&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;还用上面的 &lt;code&gt;HTML&lt;/code&gt; 例子来解释，在 &lt;code&gt;DOM&lt;/code&gt; 事件流中，实际的目标( &lt;code&gt;&amp;#x3C;div&gt;&lt;/code&gt; 元素)在捕获阶段不会接收到事件。这意味着在捕获阶段，事件从 &lt;code&gt;document&lt;/code&gt; 到 &lt;code&gt;&amp;#x3C;html&gt;&lt;/code&gt; 再到 &lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt; 后就停止了。下一个阶段是“处于目标”阶段，于是事件在 &lt;code&gt;&amp;#x3C;div&gt;&lt;/code&gt; 上发生，并在事件处理(后面将会讨论这个概念)中被看成冒泡阶段的一部分。然后，冒泡阶段发生， 事件又传播回文档。传播的顺序如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/dom-event.DpNZqIHn_Z21DDuI.webp&quot; alt=&quot;dom-event&quot; title=&quot;dom-event&quot;&gt;&lt;/p&gt;
&lt;p&gt;在&lt;a href=&quot;https://www.w3.org/TR/DOM-Level-3-Events/#ui-events-intro&quot; title=&quot;DOM Level 3 Events draft &quot;&gt;DOM Level 3 Events draft&lt;/a&gt;中有一个更清晰的图表示三个阶段：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/dom3.CQcHOqDk_Z1ybDb3.webp&quot; alt=&quot;dom3&quot; title=&quot;dom3&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里总结一下事件流的顺序 &lt;code&gt;window -&gt; document -&gt; documentElement -&gt; document.body -&gt; ... -&gt; target -&gt; ... -&gt; document.body -&gt; documentElement -&gt; document -&gt; window&lt;/code&gt;。一个元素上绑定的多个事件的执行顺序首先是看事件处理程序是定义在哪个阶段，定义在同一个阶段的多个事件处理程序的执行顺序由代码中的顺序决定，先绑定的就先执行。比如给目标元素同时绑定捕获阶段和冒泡阶段两个事件，如果冒泡阶段的事件处理程序先绑定，那么先执行的就是冒泡阶段的事件监听程序先执行。&lt;/p&gt;
&lt;p&gt;关于事件流写了两个简单的 &lt;code&gt;Demo&lt;/code&gt;，点击查看 &lt;a href=&quot;https://cdn.clloz.com/study/event/index.html&quot; title=&quot;Demo1&quot;&gt;Demo1&lt;/a&gt; 和 &lt;a href=&quot;https://cdn.clloz.com/study/event/event-flow.html&quot; title=&quot;Demo2&quot;&gt;Demo2&lt;/a&gt;，打开控制台查看事件流的触发机制。&lt;/p&gt;
&lt;h2&gt;事件处理程序&lt;/h2&gt;
&lt;p&gt;也可以叫做事件侦听器，事件就是用户或浏览器自身执行的某种动作。诸如 &lt;code&gt;click、load 和 mouseover&lt;/code&gt;，都是事件的名字。 而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以 &lt;code&gt;on&lt;/code&gt; 开头，因此 &lt;code&gt;click&lt;/code&gt; 事件的事件处理程序就是 &lt;code&gt;onclick&lt;/code&gt;，&lt;code&gt;load&lt;/code&gt;事件的事件处理程序就是 &lt;code&gt;onload&lt;/code&gt;。为事件指定处理程序的方式有好几种。&lt;/p&gt;
&lt;p&gt;事件处理程序主要有以下几种：&lt;code&gt;HTML&lt;/code&gt; 事件处理程序就是我们常见的用 &lt;code&gt;HTML&lt;/code&gt; 元素 &lt;code&gt;attribute&lt;/code&gt; 写在行内的事件，这种做法非常不推荐，效率低下难以维护。然后是 &lt;code&gt;DOM&lt;/code&gt; 事件处理程序，&lt;code&gt;DOM&lt;/code&gt; 处理程序有所谓的 &lt;code&gt;DOM0&lt;/code&gt;，&lt;code&gt;DOM2&lt;/code&gt; 和 &lt;code&gt;DOM3&lt;/code&gt; 等，这里的数字指的就是 &lt;code&gt;DOM&lt;/code&gt; 标准的版本，目前的主流浏览器基本都实现了 &lt;code&gt;DOM3&lt;/code&gt; 标准，正在推行的的是 &lt;code&gt;DOM4&lt;/code&gt;。至于为什么没有 &lt;code&gt;DOM1&lt;/code&gt; 事件处理程序，因为 &lt;code&gt;DOM1&lt;/code&gt; 中没有和事件相关的更新，所以在讨论事件处理程序的时候就没有 &lt;code&gt;DOM1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;还有一点就是 &lt;code&gt;HTML&lt;/code&gt; 的 &lt;code&gt;attribute&lt;/code&gt; 和 &lt;code&gt;DOM&lt;/code&gt; 的 &lt;code&gt;property&lt;/code&gt; 的区别，我们发现 &lt;code&gt;DOM0&lt;/code&gt; 级事件使用的也是 &lt;code&gt;onclick&lt;/code&gt; 这样的属性，而 &lt;code&gt;HTML&lt;/code&gt; 事件处理程序的属性名也是这个，但是不要混淆他们。可以简单的理解为多数情况 &lt;code&gt;attribute&lt;/code&gt; 值仅用作初始 &lt;code&gt;DOM&lt;/code&gt; 节点对象使用，而 &lt;code&gt;property&lt;/code&gt; 更多用于页面交互，很多框架都是在与元素和指令的 &lt;code&gt;property&lt;/code&gt; 和事件打交道。比如 &lt;code&gt;input&lt;/code&gt; 标签有 &lt;code&gt;value&lt;/code&gt; 属性，&lt;code&gt;input&lt;/code&gt; 对应的 &lt;code&gt;DOM&lt;/code&gt; 对象也有 &lt;code&gt;value&lt;/code&gt; 属性，当在用户未输入数据，或设置 &lt;code&gt;property&lt;/code&gt; 的值时，取的值是 &lt;code&gt;attribute&lt;/code&gt; 的值。当用户输入值或者设置了 &lt;code&gt;property&lt;/code&gt; 的值后，&lt;code&gt;property&lt;/code&gt; 的值就不受 &lt;code&gt;attribute&lt;/code&gt; 影响了。&lt;/p&gt;
&lt;p&gt;当然也不是所有的 &lt;code&gt;HTML attribute&lt;/code&gt; 都和 &lt;code&gt;DOM property&lt;/code&gt; 同名，比如 &lt;code&gt;HTML&lt;/code&gt; 的 &lt;code&gt;class&lt;/code&gt; 属性和 &lt;code&gt;DOM&lt;/code&gt; 的 &lt;code&gt;className&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;HTML事件处理程序&lt;/h2&gt;
&lt;p&gt;当一个HTML元素支持某种事件，我们可以通过该元素的属性来指定事件处理程序，比如&lt;code&gt;click&lt;/code&gt;事件就可以用 &lt;code&gt;onclick&lt;/code&gt; 属性来指定事件处理程序，属性值应该是可执行的 &lt;code&gt;javascript&lt;/code&gt; 代码，比如点击按钮弹出警告框：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;input type=&quot;button&quot; value=&quot;Click Me&quot; onclick=&quot;alert(&apos;Clicked&apos;)&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;html&lt;/code&gt;中定义的事件处理程序也可以调用别的地方定义的脚本，比如调用你在别的地方定义的函数，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;script type=&quot;text/javascript&quot;&gt;
  function showMessage() {
    alert(&apos;Hello world!&apos;)
  }
&amp;#x3C;/script&gt;
&amp;#x3C;input type=&quot;button&quot; value=&quot;Click Me&quot; onclick=&quot;showMessage()&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用的函数可以是在当前 &lt;code&gt;html&lt;/code&gt; 文件中的 &lt;code&gt;script&lt;/code&gt; 标签中，也可以是在页面引用的其他 &lt;code&gt;js&lt;/code&gt; 文件中，事件处理程序中的代码在执行时，有权访问全局作用域中的任何代码。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTML&lt;/code&gt; 事件处理程序的特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建了一个封装着元素属性值的函数，通过函数中的局部变量 &lt;code&gt;event&lt;/code&gt; 直接访问事件对象（后面会介绍），不需要定义这个参数，也不需要从参数列表中读取，可以直接使用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;script type=&quot;text/javascript&quot;&gt;
  function showMessage() {
    console.log(event.type)
    console.log(this.value)
  }
&amp;#x3C;/script&gt;
&amp;#x3C;input type=&quot;button&quot; value=&quot;Click Me&quot; onclick=&quot;showMessage()&quot; /&gt; /* 输出 click*/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt; 的指向：如果是直接在&lt;code&gt;onclick&lt;/code&gt;属性中执行的&lt;code&gt;javascript&lt;/code&gt;代码，那么 &lt;code&gt;this&lt;/code&gt; 指向当前的元素；如果是引用自其他标签或文件中的函数，那么 &lt;code&gt;this&lt;/code&gt; 指向 &lt;code&gt;window&lt;/code&gt; 对象。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;script type=&quot;text/javascript&quot;&gt;
  function showMessage() {
    console.log(event.type)
    console.log(this)
  }
&amp;#x3C;/script&gt;
&amp;#x3C;!-- 指向当前元素 --&gt;
&amp;#x3C;input type=&quot;button&quot; value=&quot;Click Me&quot; onclick=&quot;console.log(this)&quot; /&gt;
&amp;#x3C;!-- 指向window对象 --&gt;
&amp;#x3C;input type=&quot;button&quot; value=&quot;Click Me&quot; onclick=&quot;showMessage()&quot; /&gt;
```html 这里说一下内联的 `onclick` 引用其他地方的函数的理解方式，对于 `
&amp;#x3C;div onclick=&quot;fun()&quot;&gt;&amp;#x3C;/div&gt;
` 我们应该这么理解 ```javascript div.onclick = function (this, event) { fun(this, event); } function
fun() { //code.... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内联的 &lt;code&gt;onclick&lt;/code&gt; 触发的时候相当于执行的上面这段代码，我们写在 &lt;code&gt;onclick&lt;/code&gt; 属性中的代码其实是在一个匿名函数中执行的 &lt;code&gt;js&lt;/code&gt; 代码，这也是为什么写成 &lt;code&gt;onclick=&quot;fun()&quot;&lt;/code&gt; 而不是写成 &lt;code&gt;onclick=fun&lt;/code&gt; 的形式，这里不要理解成对 &lt;code&gt;fun&lt;/code&gt; 函数的引用，而是在 &lt;code&gt;onclick&lt;/code&gt; 触发的时候，执行双引号中的代码，此时我们的 &lt;code&gt;fun&lt;/code&gt; 函数只是在匿名函数中执行的一个在全局中定义的函数，如果匿名函数没有把 &lt;code&gt;this&lt;/code&gt; 传递给 &lt;code&gt;fun&lt;/code&gt; 函数，那么 &lt;code&gt;fun&lt;/code&gt; 的 &lt;code&gt;this&lt;/code&gt; 应该是指向 &lt;code&gt;window&lt;/code&gt; 对象的。看下面这段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div onclick=&quot;obj.fun()&quot;&gt;test&amp;#x3C;/div&gt;
&amp;#x3C;script&gt;
  var obj = {
    a: &apos;test&apos;,
    fun: function () {
      console.log(this) //{a: &quot;test&quot;, fun: ƒ}
      console.log(this === obj) //true
    }
  }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样是不是比较容易理解呢。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是 &lt;code&gt;body&lt;/code&gt; 中的 &lt;code&gt;onload=&quot;console.log(this);&quot;&lt;/code&gt; 会指向 &lt;code&gt;window&lt;/code&gt; 对象，但是 &lt;code&gt;img&lt;/code&gt; 中的 &lt;code&gt;onload=&quot;console.log(this);&lt;/code&gt; 则还是指向当前元素的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;在函数内部可以像访问局部变量一样访问 &lt;code&gt;document&lt;/code&gt; 以及该元素本身的成员，需要注意的是引用的函数同样不可以（函数的作用域链取决于函数定义的位置，而不是执行的位置），函数的内部实现类似如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function(){
    with(document){
        with(this){ //元素属性值
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果当前元素是一个表单输入元素，则作用域中还会包含访问表单元素(父元素)的入口，这样扩展作用域的方式，无非就是想让事件处理程序无需引用表单元素就能访问其他表单字段。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;form method=&quot;post&quot;&gt;
  &amp;#x3C;input type=&quot;text&quot; name=&quot;username&quot; value=&quot;&quot; /&gt;
  &amp;#x3C;!-- 可以直接访问username的value --&gt;
  &amp;#x3C;input type=&quot;button&quot; value=&quot;Echo Username&quot; onclick=&quot;alert(username.value)&quot; /&gt;
&amp;#x3C;/form&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;如果属性值采取的引用函数的方式，当元素已经渲染好，而 &lt;code&gt;js&lt;/code&gt; 还没有加载完成，可能会造成触发事件而事件处理程序并没有执行，这样会报错，防止报错可以使用 &lt;code&gt;try-catch&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;input type=&quot;button&quot; value=&quot;Click Me&quot; onclick=&quot;try{showMessage();}catch(ex){}&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;用 &lt;code&gt;HTML&lt;/code&gt; 指定的事件处理程序造成 &lt;code&gt;html&lt;/code&gt; 和 &lt;code&gt;javascript&lt;/code&gt; 耦合，我们也无法同时给多个元素绑定事件，也无法给同一个事件绑定多个函数等等，由于这种方式的缺点非常明显，所以几乎已经消失了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;再次强调，不要使用 &lt;code&gt;HTML&lt;/code&gt; 内联事件处理程序，它是非常低效和难以维护的！&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于属性值是 &lt;code&gt;javascript&lt;/code&gt; 代码，因此不能在语句中使用未经转义的 &lt;code&gt;HTML&lt;/code&gt; 语法字符，比如和号&lt;code&gt;&amp;#x26;&lt;/code&gt;，双引号&lt;code&gt;&quot;&lt;/code&gt;，大于号&lt;code&gt;&gt;&lt;/code&gt;，小于号&lt;code&gt;&amp;#x3C;&lt;/code&gt;等，并且在 &lt;code&gt;HTML&lt;/code&gt; 中转义不能使用反斜杠&lt;code&gt;\&lt;/code&gt;，而要使用 &lt;code&gt;html&lt;/code&gt; 实体（&lt;code&gt;entity&lt;/code&gt;），比如双引号是&lt;code&gt;&quot;&lt;/code&gt;，如果你要查询某个字符的实体，在&lt;a href=&quot;https://dev.w3.org/html5/html-author/charref&quot; title=&quot;w3.org&quot;&gt;w3.org&lt;/a&gt;查询。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;DOM0级事件处理程序&lt;/h2&gt;
&lt;p&gt;页面上的每一个元素都有一个事件处理程序属性（包括 &lt;code&gt;window&lt;/code&gt; 和&lt;code&gt;document&lt;/code&gt; 对象），这些属性都是小写，比如代表 &lt;code&gt;click&lt;/code&gt; 事件的&lt;code&gt;onclick&lt;/code&gt;，将需要监听事件的元素的该属性的值设置为一个函数，就可以指定事件处理程序，当事件在元素上触发的时候会调用事件处理程序。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var btn = document.getElementById(&apos;myBtn&apos;)
btn.onclick = function () {
  alert(&apos;Clicked&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;DOM0&lt;/code&gt; 级事件处理程序中的&lt;code&gt;this&lt;/code&gt;指向绑定事件的元素，在事件处理程序中可以访问元素的所有属性和方法。这种方法绑定的事件处理程序会在事件流的冒泡阶段被执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;想要删除绑定的 &lt;code&gt;DOM0&lt;/code&gt; 级事件处理程序，只要将元素的&lt;code&gt;onclick&lt;/code&gt;属性设置为&lt;code&gt;null&lt;/code&gt;即可，在 &lt;code&gt;HTML&lt;/code&gt; 标签中绑定的事件处理函数也可以用这个方法来删除绑定的事件处理程序，需要注意的是删除绑定的代码要在需要删除的标签之后。&lt;/p&gt;
&lt;h2&gt;DOM2级事件处理程序&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;DOM2&lt;/code&gt; 级事件处理程序是我们目前最多使用的绑定事件处理程序的方法，包含了两个主要的方法用来绑定和删除事件处理函数&lt;code&gt;addEventListener()&lt;/code&gt;和&lt;code&gt;removeEventListener()&lt;/code&gt;。所有 &lt;code&gt;DOM&lt;/code&gt; 节点都包含这两个方法。这两个方法都接受三个参数，第一个参数是要处理的事件类型（和 &lt;code&gt;DOM0&lt;/code&gt; 级事件中的对象属性不同，这里的事件类型不需要加&lt;code&gt;on&lt;/code&gt;），第二个参数是事件处理程序对应的函数，第三个参数是一个布尔值，如果是&lt;code&gt;true&lt;/code&gt;表示在捕获阶段调用事件处理程序，如果是&lt;code&gt;false&lt;/code&gt;表示在冒泡阶段调用事件处理程序，默认为&lt;code&gt;false&lt;/code&gt;。具体细节可以看&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt;。第三个参数还可以是一个对象，有如下三个属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useCapture&lt;/code&gt;: &lt;code&gt;Boolean&lt;/code&gt;，&lt;code&gt;true&lt;/code&gt; 表示 &lt;code&gt;listener&lt;/code&gt; 会在该类型的事件捕获阶段传播到该 &lt;code&gt;EventTarget&lt;/code&gt; 时触发。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;once&lt;/code&gt;: &lt;code&gt;Boolean&lt;/code&gt;，表示 &lt;code&gt;listener&lt;/code&gt; 在添加之后最多只调用一次。如果是 &lt;code&gt;true&lt;/code&gt;， &lt;code&gt;listener&lt;/code&gt; 会在其被调用之后自动移除。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;passive&lt;/code&gt;: &lt;code&gt;Boolean&lt;/code&gt;，设置为 &lt;code&gt;true&lt;/code&gt; 时，表示 &lt;code&gt;listener&lt;/code&gt; 永远不会调用 &lt;code&gt;preventDefault()&lt;/code&gt;。如果 &lt;code&gt;listener&lt;/code&gt; 仍然调用了这个函数，客户端将会忽略它并抛出一个控制台警告。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们要在一个元素上添加click事件的事件处理程序，就可以使用如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var btn = document.querySelector(&apos;.btn&apos;)
btn.addEventListener(
  &apos;click&apos;,
  function () {
    console.log(this)
  },
  false
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的&lt;code&gt;click&lt;/code&gt;就是要处理的事件类型，匿名函数就是我们指定的事件处理函数，最后的&lt;code&gt;false&lt;/code&gt;就是指定事件触发是在冒泡阶段。当事件监听程序监听到符合要求的事件发生时，就会调用事件处理程序来执行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DOM2&lt;/code&gt; 级事件处理程序的特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以为同一个元素的同一类型的事件绑定多个事件处理程序，他们会按照添加顺序执行。如：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var btn = document.getElementById(&apos;myBtn&apos;)
btn.addEventListener(
  &apos;click&apos;,
  function () {
    console.log(this.id)
  },
  false
)
btn.addEventListener(
  &apos;click&apos;,
  function () {
    console.log(&apos;Hello world!&apos;)
  },
  false
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码为 &lt;code&gt;btn&lt;/code&gt; 的 &lt;code&gt;click&lt;/code&gt; 类型的事件指定了两个事件处理程序，当我们触发 &lt;code&gt;btn&lt;/code&gt; 的 &lt;code&gt;click&lt;/code&gt; 事件的时候，这两个事件处理程序会按顺序执行，也就是先输出&lt;code&gt;this.id&lt;/code&gt;然后输出&lt;code&gt;Hello world!&lt;/code&gt;。&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;由于可以指定事件触发的阶段以及 &lt;code&gt;event&lt;/code&gt; 对象的存在我们可以用 &lt;code&gt;DOM2&lt;/code&gt; 级事件进行事件委托，达到对性能的提升和同类型元素绑定事件的简化，后面会详细讨论。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因为可以为同一元素和同一类型的事件绑定多个事件处理程序，所以要删除这些事件只能通过 &lt;code&gt;removeEventListener()&lt;/code&gt; 来删除，并且移除时传入的参数必须与绑定时传入的参数相同，如果在绑定的时候如果用的是匿名函数，那么这个事件处理程序将无法删除，因为两个不同的匿名函数指向的是不同的空间，如：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var btn = document.getElementById(&apos;myBtn&apos;)
btn.addEventListener(
  &apos;click&apos;,
  function () {
    alert(this.id)
  },
  false
)
btn.removeEventListener(
  &apos;click&apos;,
  function () {
    //无效
    alert(this.id)
  },
  false
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要实现事件处理函数的删除需要将第二个参数换成函数的引用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var btn = document.getElementById(&apos;myBtn&apos;)
var handler = function () {
  alert(this.id)
}
btn.addEventListener(&apos;click&apos;, handler, false)
//这里省略了其他代码
btn.removeEventListener(&apos;click&apos;, handler, false) //有效!
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;IE9&lt;/code&gt; 、&lt;code&gt;Firefox&lt;/code&gt; 、&lt;code&gt;Safari&lt;/code&gt; 、&lt;code&gt;Chrome&lt;/code&gt; 和 &lt;code&gt;Opera&lt;/code&gt; 支持 &lt;code&gt;DOM2&lt;/code&gt; 级事件处理程序。如果不是特别的事件如&lt;code&gt;mouseenter&lt;/code&gt;不支持事件冒泡，一般就默认在冒泡阶段触发即可，事件捕获可以当我们需要在事件传播到目标之前截获的时候使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;IE事件处理程序&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;IE9&lt;/code&gt; 之前的版本使用的是 &lt;code&gt;IE&lt;/code&gt; 独特的事件处理程序，它只支持冒泡，有两个方法 &lt;code&gt;attachEvent()&lt;/code&gt; 和 &lt;code&gt;detachEvent()&lt;/code&gt; 两个方法，接收两个参数：事件类型（和 &lt;code&gt;DOM0&lt;/code&gt; 一样需要加上 &lt;code&gt;on&lt;/code&gt;）和事件处理程序函数，由于现在很少需要兼容 &lt;code&gt;IE9&lt;/code&gt; 之前的版本，就不过多讨论了，放上一个跨浏览器的事件处理程序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;/* 若支持DOM2级事件处理程序则用DOM2级，若支持IE的事件处理程序则用IE，否则用DOM0级事件处理程序 */
var EventUtil = {
  addHandler: function (element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false)
    } else if (element.attachEvent) {
      element.attachEvent(&apos;on&apos; + type, handler)
    } else {
      element[&apos;on&apos; + type] = handler
    }
  },
  removeHandler: function (element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false)
    } else if (element.detachEvent) {
      element.detachEvent(&apos;on&apos; + type, handler)
    } else {
      element[&apos;on&apos; + type] = null
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;事件对象&lt;/h2&gt;
&lt;p&gt;在上面的事件处理程序中多次提到了 &lt;code&gt;event&lt;/code&gt; 对象，我们在上面的代码中输出过 &lt;code&gt;event&lt;/code&gt; 对象的 &lt;code&gt;type&lt;/code&gt; 属性，这个属性表示当前指定的事件处理成熟的事件类型。事实上，每当某个 &lt;code&gt;DOM&lt;/code&gt; 元素触发了某个事件，都会产生一个 &lt;code&gt;event&lt;/code&gt; 事件对象，这个对象中包含着所有与事件有关的信息。包括触发事件的元素、事件的类型以及其他与特定事件相关的信息。例如，鼠标操作导致的事件 对象中，会包含鼠标位置的信息，而键盘操作导致的事件对象中，会包含与按下的键有关的信息。所有 浏览器都支持 &lt;code&gt;event&lt;/code&gt; 对象，但支持方式不同。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DOM0&lt;/code&gt; 级和 &lt;code&gt;DOM2&lt;/code&gt; 级事件处理程序中，浏览器会将一个 &lt;code&gt;event&lt;/code&gt; 对象传入我们定义的事件处理程序的函数中，即使我们没有在函数的参数列表中加入 &lt;code&gt;event&lt;/code&gt; 形参，我们也可以在函数内部使用，应该是浏览器替我加上了参数 &lt;code&gt;event&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;button id=&quot;myBtn&quot;&gt;btn&amp;#x3C;/button&gt;
&amp;#x3C;script type=&quot;text/javascript&quot;&gt;
  var btn = document.getElementById(&apos;myBtn&apos;)
  btn.onclick = function () {
    console.log(event.type) //可以输出
  }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般为了代码便于理解，我们在事件处理程序的回调函数中给出参数 &lt;code&gt;event&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;即使我们通过HTML内联的方式执行事件处理程序，在其中我们也可以使用一个指向 &lt;code&gt;event&lt;/code&gt; 对象的变量 &lt;code&gt;event&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;input type=&quot;button&quot; value=&quot;Click Me&quot; onclick=&quot;alert(event.type)&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;event&lt;/code&gt; 对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样，可用的属性和方法也不一样。不过，所有事件都会有下表列出的成员。&lt;/p&gt;
&lt;p&gt;| 属性/方法           | 类型       | 读/写 | 说明                                                                                                                                                                                                            |
| ------------------- | ---------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| &lt;code&gt;Event()&lt;/code&gt;           | 构造函数   |       | &lt;code&gt;Event()&lt;/code&gt; 构造函数, 创建一个新的事件对象 &lt;code&gt;Event&lt;/code&gt;。语法 &lt;code&gt;event = new Event(typeArg, eventInit);&lt;/code&gt;，&lt;code&gt;typeArg&lt;/code&gt; 是必选参数，为一个字符串，表示创建的事件名称。第二个参数是一个对象，用来定义事件是否冒泡和可以取消。 |
| &lt;code&gt;bubbles&lt;/code&gt;           | &lt;code&gt;Boolean&lt;/code&gt;  | 只读  | 事件是否冒泡                                                                                                                                                                                                    |
| &lt;code&gt;cancelBubble&lt;/code&gt;      | &lt;code&gt;Boolean&lt;/code&gt;  | 可写  | &lt;code&gt;Event.stopPropagation()&lt;/code&gt; 的历史别名。在事件处理器函数返回之前，将此属性的值设置为 &lt;code&gt;true&lt;/code&gt;，亦可阻止事件继续冒泡。                                                                                               |
| &lt;code&gt;cancelable&lt;/code&gt;        | &lt;code&gt;Boolean&lt;/code&gt;  | 只读  | 是否可以取消事件的默认行为，如果为 &lt;code&gt;false&lt;/code&gt; 则事件发生时无法在事件监听回调中用 &lt;code&gt;preventDefault()&lt;/code&gt; 停止事件                                                                                                       |
| &lt;code&gt;composed&lt;/code&gt;          | &lt;code&gt;Boolean&lt;/code&gt;  | 只读  | 一个布尔值，表示事件是否可以穿过 &lt;code&gt;Shadow DOM&lt;/code&gt; 和常规 &lt;code&gt;DOM&lt;/code&gt; 之间的隔阂进行冒泡。                                                                                                                                 |
| &lt;code&gt;currentTarget&lt;/code&gt;     | &lt;code&gt;Element&lt;/code&gt;  | 只读  | 事件处理程序当前处理元素                                                                                                                                                                                        |
| &lt;code&gt;defaultPrevented&lt;/code&gt;  | &lt;code&gt;Boolean&lt;/code&gt;  | 只读  | 为 true表示已经调用了 &lt;code&gt;preventDefault()&lt;/code&gt; ( &lt;code&gt;DOM3&lt;/code&gt; 级事件中新增)                                                                                                                                                 |
| &lt;code&gt;detail&lt;/code&gt;            | &lt;code&gt;Integer&lt;/code&gt;  | 只读  | 与事件相关细节信息                                                                                                                                                                                              |
| &lt;code&gt;eventPhase&lt;/code&gt;        | &lt;code&gt;Integer&lt;/code&gt;  | 只读  | 事件处理程序阶段：&lt;code&gt;1&lt;/code&gt; 捕获阶段，&lt;code&gt;2&lt;/code&gt; 处于目标阶段，&lt;code&gt;3&lt;/code&gt; 冒泡阶段                                                                                                                                                  |
| &lt;code&gt;preventDefault()&lt;/code&gt;  | &lt;code&gt;Function&lt;/code&gt; |       | 取消事件默认行为                                                                                                                                                                                                |
| &lt;code&gt;stopPropagation()&lt;/code&gt; | &lt;code&gt;Function&lt;/code&gt; |       | 取消事件进一步捕获或冒泡                                                                                                                                                                                        |
| &lt;code&gt;target&lt;/code&gt;            | &lt;code&gt;Element&lt;/code&gt;  | 只读  | 事件的目标元素                                                                                                                                                                                                  |
| &lt;code&gt;timeStamp&lt;/code&gt;         |            | 只读  | 事件创建时的时间戳（精度为毫秒）。按照规范，这个时间戳是 &lt;code&gt;Unix&lt;/code&gt; 纪元起经过的毫秒数，但实际上，在不同的浏览器中，对此时间戳的定义也有所不同。另外，规范正在将其修改为 &lt;code&gt;DOMHighResTimeStamp&lt;/code&gt;                      |
| &lt;code&gt;isTrusted&lt;/code&gt;         | &lt;code&gt;Boolean&lt;/code&gt;  | 只读  | 为 &lt;code&gt;true&lt;/code&gt; 表示事件是浏览器生成的。为 &lt;code&gt;false&lt;/code&gt; 表 示事件是由开发人员通过 &lt;code&gt;JavaScript&lt;/code&gt; 创建的(&lt;code&gt;DOM3&lt;/code&gt; 级事件中新增)                                                                                                 |
| &lt;code&gt;type&lt;/code&gt;              | &lt;code&gt;String&lt;/code&gt;   | 只读  | 被触发的事件类型                                                                                                                                                                                                |
| &lt;code&gt;composedPath&lt;/code&gt;      | &lt;code&gt;Function&lt;/code&gt; |       | 返回事件的路径（将在该对象上调用监听器）。如果阴影根节点 (&lt;code&gt;shadow root&lt;/code&gt;) 创建时 &lt;code&gt;ShadowRoot.mode&lt;/code&gt; 值为 &lt;code&gt;closed&lt;/code&gt;，那么路径不会包括该根节点下阴影树 (&lt;code&gt;shadow tree&lt;/code&gt;) 的节点。                                      |&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;currentTarget&lt;/code&gt; 和 &lt;code&gt;target&lt;/code&gt; 只要记住一点，不管事件传播处于什么阶段，&lt;code&gt;target&lt;/code&gt; 都是不变的，指向目标元素，所以我们在实现事件委托的时候会用到 &lt;code&gt;target&lt;/code&gt;。而 &lt;code&gt;currentTarget&lt;/code&gt; 则是随着事件传播处于不同的阶段而指向不同的元素。具体细节点击&lt;a href=&quot;https://www.clloz.com/study/event-target.html&quot;&gt;实现页面&lt;/a&gt;，打开控制台点击不同的元素查看细节。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在事件处理程序内部，对象 &lt;code&gt;this&lt;/code&gt; 始终等于 &lt;code&gt;currentTarget&lt;/code&gt; 的值，而 &lt;code&gt;target&lt;/code&gt; 则只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素，则 &lt;code&gt;this&lt;/code&gt;、&lt;code&gt;currentTarget&lt;/code&gt; 和 &lt;code&gt;target&lt;/code&gt; 包含相同 的值。来看下面的例子。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以利用 &lt;code&gt;event&lt;/code&gt; 对象的 &lt;code&gt;type&lt;/code&gt; 属性来用一个函数处理多种事件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var handler = function (event) {
  switch (event.type) {
    case &apos;click&apos;:
      alert(&apos;Clicked&apos;)
      break
    case &apos;mouseover&apos;:
      event.target.style.backgroundColor = &apos;red&apos;
      break
    case &apos;mouseout&apos;:
      event.target.style.backgroundColor = &apos;&apos;
      break
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有时我们会需要阻止某些事件的默认行为，比如点击表单的 &lt;code&gt;submit&lt;/code&gt; 跳转，以及点击 &lt;code&gt;a标签&lt;/code&gt; 的跳转，如果我们不希望这些行为发生，那么我们可以利用 &lt;code&gt;event对象&lt;/code&gt; 的 &lt;code&gt;preventDefault()&lt;/code&gt; 方法，只有 &lt;code&gt;cancelable&lt;/code&gt; 属性设置为 &lt;code&gt;true&lt;/code&gt; 的事件，才可以使用 &lt;code&gt;preventDefault()&lt;/code&gt; 来取消其默认行为。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;event对象&lt;/code&gt; 还有一个重要的方法是 &lt;code&gt;stopPropagation()&lt;/code&gt;，这个方法用来停止事件在 &lt;code&gt;DOM&lt;/code&gt; 树上的传播，不管在哪个传播阶段，都会停止事件的传播。比如我们在按钮和 &lt;code&gt;body&lt;/code&gt; 上都注册了一个事件，当用户点击按钮，我们不希望注册在 &lt;code&gt;body&lt;/code&gt; 上的事件被触发，此时我们就需要用到这个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var btn = document.getElementById(&apos;myBtn&apos;)
btn.onclick = function (event) {
  alert(&apos;Clicked&apos;)
  event.stopPropagation()
}
document.body.onclick = function (event) {
  alert(&apos;Body clicked&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事件对象的 &lt;code&gt;eventPhase&lt;/code&gt; 属性，可以用来确定事件当前正位于事件流的哪个阶段。如果是在捕获阶 段调用的事件处理程序，那么 &lt;code&gt;eventPhase&lt;/code&gt; 等于 &lt;code&gt;1&lt;/code&gt;;如果事件处理程序处于目标对象上，则 &lt;code&gt;event- Phase&lt;/code&gt; 等于 &lt;code&gt;2&lt;/code&gt;;如果是在冒泡阶段调用的事件处理程序，&lt;code&gt;eventPhase&lt;/code&gt; 等于 &lt;code&gt;3&lt;/code&gt;。这里要注意的是，尽管“处于目标”发生在冒泡阶段，但 &lt;code&gt;eventPhase&lt;/code&gt; 仍然一直等于 2。来看下面的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var btn = document.getElementById(&apos;myBtn&apos;)
btn.onclick = function (event) {
  alert(event.eventPhase) //2
}
document.body.addEventListener(
  &apos;click&apos;,
  function (event) {
    alert(event.eventPhase) //1
  },
  true
)
document.body.onclick = function (event) {
  alert(event.eventPhase) //3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一个要注意的点是，很多同学学习事件传播顺序的知识会容易混淆到单个元素的事件处理程序的执行顺序上，对于单个元素，无论你是绑定在捕获阶段还是冒泡阶段，都是先绑定的事件处理程序先执行，事件传播只在嵌套中的不同 &lt;code&gt;DOM&lt;/code&gt; 元素之间有效，比如如下代码，就是先执行冒泡在执行捕获。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div id=&quot;el&quot;&gt;element&amp;#x3C;/div&gt;
&amp;#x3C;script type=&quot;text/javascript&quot;&gt;
  var el = document.getElementById(&apos;el&apos;)
  //冒泡
  el.addEventListener(
    &apos;click&apos;,
    function () {
      console.log(&apos;el冒泡&apos;)
    },
    false
  )
  //捕获
  el.addEventListener(
    &apos;click&apos;,
    function () {
      console.log(&apos;el捕获&apos;)
    },
    true
  )
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;event对象&lt;/code&gt; 在事件触发的时候生成，当事件处理程序执行结束后，&lt;code&gt;event对象&lt;/code&gt; 即被销毁，也就是说只有在事件处理程序执行期间，&lt;code&gt;event对象&lt;/code&gt; 才会存在。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;事件类型&lt;/h2&gt;
&lt;p&gt;浏览器中发生的事件类型很多，详情查询&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/Events&quot; title=&quot;MDN&quot;&gt;事件参考 - MDN&lt;/a&gt;，&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/GlobalEventHandlers&quot; title=&quot;GlobalEventHandlers - MDN&quot;&gt;GlobalEventHandlers - MDN&lt;/a&gt;，每种事件对应的元素，行为都不尽相同。&lt;/p&gt;
&lt;h2&gt;内存和性能&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;JS&lt;/code&gt; 中，添加到页面上的事件处理程序的数量会影响的页面的整体性能，因为每一个事件处理函数也都是对象，都保存在内存中，事件处理程序多了自然对内存的开销会增大。其次，在 &lt;code&gt;JS&lt;/code&gt; 的渲染过程中，指定事件处理程序需要访问 &lt;code&gt;DOM&lt;/code&gt; ，没绑定一个事件处理程序都需要访问一次DOM，如果事件处理程序过多，会影响页面渲染完成的时间。如果我们能够更好地处理事件，对提升页面的性能是有一定的帮助的。&lt;/p&gt;
&lt;h2&gt;事件委托&lt;/h2&gt;
&lt;p&gt;事件委托其实很好理解，利用事件流传播的特性，利用事件冒泡，我们可以对多个需要绑定事件的同类型元素的上级 &lt;code&gt;DOM&lt;/code&gt; 节点绑定一个事件处理程序，用这个上层节点的事件处理程序来同一管理那些同一类型的事件。举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;ul id=&quot;myLinks&quot;&gt;
  &amp;#x3C;li id=&quot;goSomewhere&quot;&gt;Go somewhere&amp;#x3C;/li&gt;
  &amp;#x3C;li id=&quot;doSomething&quot;&gt;Do something&amp;#x3C;/li&gt;
  &amp;#x3C;li id=&quot;sayHi&quot;&gt;Say hi&amp;#x3C;/li&gt;
&amp;#x3C;/ul&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们要实现点击每个 &lt;code&gt;li&lt;/code&gt; 都输出其中的文本，那么按照传统的做法，我们会为每个 &lt;code&gt;li&lt;/code&gt; 绑定一个事件，如果同样类型的元素特别多，那么我们一个一个绑定事件显然是不现实的，而且这样页面的性能也不佳。如果我们事件委托，我们就可以把事件处理程序绑定到 &lt;code&gt;ul&lt;/code&gt; 上，利用事件冒泡的特性来统一管理点击 &lt;code&gt;li&lt;/code&gt; 的事件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var list = document.querySelector(&apos;myLinks&apos;)
ul.addEventListener(&apos;click&apos;, function (e) {
  if (e.target.tagName.toLowerCase() === &apos;li&apos;) {
    console.log(e.target.innerText)
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们点击 &lt;code&gt;li&lt;/code&gt; 的时候，由于事件冒泡传播，所以当事件传播到 &lt;code&gt;ul&lt;/code&gt; 的时候，会被我们绑定在 &lt;code&gt;ul&lt;/code&gt; 上的事件处理程序捕获，然后在函数内部我们利用 &lt;code&gt;event.target&lt;/code&gt; 会指向目标元素的特点来判断用户点击的是否是 &lt;code&gt;li&lt;/code&gt;，然后在执行对应的逻辑需求。&lt;/p&gt;
&lt;p&gt;上面只是最简单的事件委托情况，有时候我们的 &lt;code&gt;DOM&lt;/code&gt; 结构嵌套更多，我们不仅要处理绑定元素内的元素的委托，还有可能要在触发内部元素的事件之后修改外部的 &lt;code&gt;DOM&lt;/code&gt; 节点，这时候我们就需要将 &lt;code&gt;currentTarget&lt;/code&gt; 和 &lt;code&gt;target&lt;/code&gt; 结合起来用，比如下面这种场景: 当我们点击上面的按钮的时候，下面的 &lt;code&gt;panel&lt;/code&gt; 要进行切换，一共有四个 &lt;code&gt;panel&lt;/code&gt; 点击第几个按钮，显示第一个 &lt;code&gt;panel&lt;/code&gt;， 用 &lt;code&gt;display: none&lt;/code&gt; 来设置。并且这样的模块有两个。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/tab-select.WMjY7Ohy_7iLIm.webp&quot; alt=&quot;tab-select&quot; title=&quot;tab-select&quot;&gt;&lt;/p&gt;
&lt;p&gt;DOM结构如下 ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;mod_tab&quot;&gt;
  &amp;#x3C;div class=&quot;header&quot;&gt;
    &amp;#x3C;div class=&quot;tab active&quot;&gt;1&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;tab&quot;&gt;2&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;tab&quot;&gt;3&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;tab&quot;&gt;4&amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;content&quot;&gt;
    &amp;#x3C;div class=&quot;panel active&quot;&gt;panel1&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;panel&quot;&gt;panel2&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;panel&quot;&gt;panel3&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;panel&quot;&gt;panel4&amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&amp;#x3C;div class=&quot;mod_tab&quot;&gt;
  &amp;#x3C;div class=&quot;header&quot;&gt;
    &amp;#x3C;div class=&quot;tab active&quot;&gt;1&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;tab&quot;&gt;2&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;tab&quot;&gt;3&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;tab&quot;&gt;4&amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;content&quot;&gt;
    &amp;#x3C;div class=&quot;panel active&quot;&gt;panel1&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;panel&quot;&gt;panel2&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;panel&quot;&gt;panel3&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;panel&quot;&gt;panel4&amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以把事件绑定到 &lt;code&gt;header&lt;/code&gt; 上，当点击按钮的时候用 &lt;code&gt;event.target&lt;/code&gt; 来操作按钮的样式，同时利用 &lt;code&gt;event.currentTarget&lt;/code&gt; 找到 &lt;code&gt;header&lt;/code&gt; 然后找到 &lt;code&gt;header&lt;/code&gt; 的兄弟元素 &lt;code&gt;content&lt;/code&gt; 来操作 &lt;code&gt;panel&lt;/code&gt; 的显示。具体代码查看&lt;a href=&quot;https://www.clloz.com/study/tab-select/jquery-select.html&quot;&gt;页面&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;使用事件委托还有一个优点就是当我们动态向我们绑定了事件处理程序的上册元素中添加新的元素时，事件处理程序对这个新的元素也会生效，而传统的绑定事件方法则不行。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;最适合采用事件委托技术的事件包括 &lt;code&gt;click&lt;/code&gt;、&lt;code&gt;mousedown&lt;/code&gt;、&lt;code&gt;mouseup&lt;/code&gt;、&lt;code&gt;keydown&lt;/code&gt;、&lt;code&gt;keyup&lt;/code&gt; 和 &lt;code&gt;keypress&lt;/code&gt;。 虽然 &lt;code&gt;mouseover&lt;/code&gt; 和 &lt;code&gt;mouseout&lt;/code&gt; 事件也冒泡，但要适当处理它们并不容易，而且经常需要计算元素的位置。(因为当鼠标从一个元素移到其子节点时，或者当鼠标移出该元素时，都会触发 &lt;code&gt;mouseout&lt;/code&gt; 事件。)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;移除事件处理程序&lt;/h2&gt;
&lt;p&gt;移除事件处理程序更像一个程序员来维护的垃圾回收方式。每当将事件处理程序指定给元素时，运行中的浏览器代码与支持页面交互的 &lt;code&gt;JavaScript&lt;/code&gt; 代码之间就 会建立一个连接。这种连接越多，页面执行起来就越慢。采用事件委托的方式能有效的减少连接的数量，但是一些残留在内存中的未被回收的空事件处理程序也是影响页面性能的一个原因。&lt;/p&gt;
&lt;p&gt;如果我们绑定了事件处理程序的元素被 &lt;code&gt;removeChild(), replaceChild()或者innerHTML&lt;/code&gt; 方法删除或替换的时候，原来添加到元素中的事件处理程序很可能没有被当作垃圾回收，这时候用 &lt;code&gt;removeEventListener()&lt;/code&gt; 或者 &lt;code&gt;element.onclick = null&lt;/code&gt; 来手动移除事件处理程序是个不错的选择。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在事件处理程序中删除按钮也能阻止事件冒泡。目标元素在文档中是事件冒泡的前提。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;自定义事件&lt;/h2&gt;
&lt;p&gt;自定义事件与浏览器定义的事件并没有什么不同，一样能够传播，能够指定事件处理程序。有了自定义事件以后，我们可以在任意时刻触发特定的事件。对于复杂页面不同功能模块之间的解耦有很大的帮助。同时自定义事件能够实现对全局的广播，这在复杂的应用中有很大的作用。&lt;/p&gt;
&lt;h2&gt;创建自定义事件&lt;/h2&gt;
&lt;p&gt;Events 可以使用 Event 构造函数创建如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var event = new Event(&apos;build&apos;);

// Listen for the event.
elem.addEventListener(&apos;build&apos;, function (e) { ... }, false);

// Dispatch the event.
elem.dispatchEvent(event);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意的是如果要模拟内置事件需要使用内置事件自己的对应接口，比如鼠标事件用 &lt;code&gt;new MouseEvent()&lt;/code&gt;，焦点相关事件用 &lt;code&gt;new FocusEvent()&lt;/code&gt;，和键盘事件相关的 &lt;code&gt;new KeyboardEvent()&lt;/code&gt;。如果是用 &lt;code&gt;new Event()&lt;/code&gt; 的话，很可能会出问题，比如你想模拟复选框的点击事件，如果用 &lt;code&gt;new Event(&apos;click&apos;)&lt;/code&gt;，虽然事件流都触发了，但是最后复选框的勾没打上，用 &lt;code&gt;MouseEvent&lt;/code&gt; 就没有问题。但是 &lt;code&gt;MouseEvent&lt;/code&gt; 无法配置 &lt;code&gt;bubbles&lt;/code&gt; 和 &lt;code&gt;cancelable&lt;/code&gt; 字段。内置事件的详情查看 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API&quot; title=&quot;Web API 接口参考 - MDN&quot;&gt;Web API 接口参考 - MDN&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;最早定义事件使用 &lt;code&gt;event = document.createEvent(MouseEvents)&lt;/code&gt;，&lt;code&gt;event.initEvent()&lt;/code&gt; 和 &lt;code&gt;event.initMouseEvent()&lt;/code&gt; 等方法，如今这些方法董聪标准中移除，不推荐使用。现在要模拟内置事件，就用内置事件的构造函数（不再支持 &lt;code&gt;bubbles&lt;/code&gt; 和 &lt;code&gt;cancelable&lt;/code&gt; 参数，内置事件的冒泡和取消默认行为都要在绑定事件时设定）创建内置事件，想要自定义事件则使用 &lt;code&gt;Event()&lt;/code&gt; 或者 &lt;code&gt;customEvent()&lt;/code&gt; 构造函数。&lt;/p&gt;
&lt;p&gt;自定义事件通常称为合成事件，而不是浏览器本身触发的事件。如果我们用 &lt;code&gt;new Event()&lt;/code&gt;，创建了和内置事件同名的事件，不会影响浏览器的默认行为。&lt;/p&gt;
&lt;h2&gt;派发事件&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;EventTarget.dispatchEvent&lt;/code&gt; 方法进行事件的派发，语法是 &lt;code&gt;cancelled = !target.dispatchEvent(event)&lt;/code&gt;，&lt;code&gt;event&lt;/code&gt; 是要被派发的 &lt;strong&gt;事件对象&lt;/strong&gt;，注意这里必须是事件对象，而不是事件类型的字符串。比如你想派发一个内置 &lt;code&gt;click&lt;/code&gt; 事件，需要 &lt;code&gt;element.dispatchEvent(new MouseEvent(&apos;click&apos;))&lt;/code&gt;，而不是 &lt;code&gt;e&apos;le&apos;ment.dispatchEvent(&apos;click&apos;)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;该方法的返回值是一个布尔值，当该事件是可取消的(&lt;code&gt;cancelable&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;)并且&lt;strong&gt;至少一个&lt;/strong&gt;该事件的事件处理方法调用了&lt;code&gt;Event.preventDefault()&lt;/code&gt;，则返回值为 &lt;code&gt;false&lt;/code&gt;；否则返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果该被派发的事件的事件类型(&lt;code&gt;event&apos;s type&lt;/code&gt;)在方法调用之前没有被经过初始化被指定，就会抛出一个 &lt;code&gt;UNSPECIFIED_EVENT_TYPE_ERR&lt;/code&gt; 异常，或者如果事件类型是 &lt;code&gt;null&lt;/code&gt; 或一个空字符串。 &lt;code&gt;event handler&lt;/code&gt; 就会抛出未捕获的异常； 这些 &lt;code&gt;event handlers&lt;/code&gt; 运行在一个嵌套的调用栈中： 他们会阻塞调用直到他们处理完毕，但是异常不会冒泡。&lt;/p&gt;
&lt;p&gt;最后一点需要注意的是，事件派发必须在事件绑定完成之后，否则不会有任何效果。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;关于 &lt;code&gt;cancelable&lt;/code&gt; 和 &lt;code&gt;preventDefault()&lt;/code&gt; 添加一点理解，我觉得 &lt;code&gt;cancelable&lt;/code&gt; 对自定义事件来说意义不是很大，因为自定义事件本来就没有任何默认行为，&lt;code&gt;cancelable&lt;/code&gt; 唯一的作用就是确定自定义事件中的 &lt;code&gt;event.preventDefault()&lt;/code&gt; 是否执行了。而这种状态的传递是完全可以依靠其他的方式来做，不是必须的。有默认行为的内置事件已经不支持设置 &lt;code&gt;cancelable&lt;/code&gt; 和 &lt;code&gt;bubbles&lt;/code&gt;。下面这个例子是 &lt;a href=&quot;https://zh.javascript.info/dispatch-events&quot; title=&quot;现代 JavaScript 教程&quot;&gt;现代 JavaScript 教程&lt;/a&gt; 中的一个例子，可以在 &lt;code&gt;dispatchEvent&lt;/code&gt; 的位置根据返回值进行逻辑判断，不过我依然觉得这个功能不是很有用，除非以后能让我们添加自定义函数的默认行为，这才会比较有用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;pre id=&quot;rabbit&quot;&gt;
  |\   /|
   \|_|/
   /. .\
  =\_Y_/=
   {&gt;o&amp;#x3C;}
&amp;#x3C;/pre&gt;
&amp;#x3C;button onclick=&quot;hide()&quot;&gt;Hide()&amp;#x3C;/button&gt;

&amp;#x3C;script&gt;
  // hide() 将在 2 秒后被自动调用
  function hide() {
    let event = new CustomEvent(&apos;hide&apos;, {
      cancelable: true // 没有这个标志，preventDefault 将不起作用
    })
    if (!rabbit.dispatchEvent(event)) {
      alert(&apos;The action was prevented by a handler&apos;)
    } else {
      rabbit.hidden = true
    }
  }

  rabbit.addEventListener(&apos;hide&apos;, function (event) {
    if (confirm(&apos;Call preventDefault?&apos;)) {
      event.preventDefault()
    }
  })
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;添加自定义数据&lt;/h2&gt;
&lt;p&gt;要向事件对象添加更多数据，可以使用 &lt;code&gt;CustomEvent&lt;/code&gt;，该接口继承自 &lt;code&gt;Event&lt;/code&gt; 所以 &lt;code&gt;Event&lt;/code&gt; 的方法和属性它都继承了。他自己本身只有一个静态属性，就是初始化时传入的自定义数据 &lt;code&gt;detail&lt;/code&gt;。它的第二个参数是一个对象接受三个参数：&lt;code&gt;bubbles&lt;/code&gt;，&lt;code&gt;cancelable&lt;/code&gt; 和 &lt;code&gt;detail&lt;/code&gt;。其中 &lt;code&gt;detail&lt;/code&gt; 就是我们初始化的时候传给事件的数据，当派发的时间出发的时候，我们可以在回调函数中取得该值。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CustomEvent&lt;/code&gt; 接口可以为 &lt;code&gt;event&lt;/code&gt; 对象添加更多的数据。例如，&lt;code&gt;event&lt;/code&gt; 可以创建如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var event = new CustomEvent(&apos;build&apos;, { detail: elem.dataset.time })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问自定义数据，在事件处理程序中的回调函数中使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function eventHandler(e) {
  log(&apos;The time is: &apos; + e.detail)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;元素可以侦听尚未创建的事件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;form&gt;
  &amp;#x3C;textarea&gt;&amp;#x3C;/textarea&gt;
&amp;#x3C;/form&gt;

&amp;#x3C;script&gt;
  const form = document.querySelector(&apos;form&apos;)
  const textarea = document.querySelector(&apos;textarea&apos;)

  form.addEventListener(&apos;awesome&apos;, (e) =&gt; console.log(e.detail.text()))

  textarea.addEventListener(&apos;input&apos;, function () {
    // Create and dispatch/trigger an event on the fly
    // Note: Optionally, we&apos;ve also leveraged the &quot;function expression&quot; (instead of the &quot;arrow function expression&quot;) so &quot;this&quot; will represent the element
    this.dispatchEvent(
      new CustomEvent(&apos;awesome&apos;, { bubbles: true, detail: { text: () =&gt; textarea.value } })
    )
  })
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;javascript&lt;/code&gt;高级程序设计中的模拟事件章节中的方法都已经废弃，想要了解自定义事件的查看&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/Guide/Events/Creating_and_triggering_events&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通常事件是在队列中处理的。也就是说：如果浏览器正在处理 &lt;code&gt;onclick&lt;/code&gt;，这时发生了一个新的事件，例如鼠标移动了，那么它会被排入队列，相应的 &lt;code&gt;mousemove&lt;/code&gt; 处理程序将在 &lt;code&gt;onclick&lt;/code&gt; 事件处理完成后被调用。&lt;/p&gt;
&lt;p&gt;值得注意的例外情况就是，一个事件是在另一个事件中发起的。例如使用 &lt;code&gt;dispatchEvent&lt;/code&gt;。这类事件将会被立即处理，即在新的事件处理程序被调用之后，恢复到当前的事件处理程序。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;《JavaScript 高级程序设计 3rd Edition》&lt;/li&gt;
&lt;li&gt;MDN&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>深入 JavaScript 类型转换</title><link>https://clloz.com/blog/type-conversion</link><guid isPermaLink="true">https://clloz.com/blog/type-conversion</guid><pubDate>Tue, 13 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中的类型转换是一个非常让人头大的内容，其实我们平时的编码一般会尽量避免让自己陷入不确定的类型转换中。但是很多时候面试会考查这方面的知识，并且搞清楚类型转换的机制能够让我们在遇到一些奇葩问题的时候知道发生了什么。我们不一定要记住所有的类型转换的可能性，只要记住一些常用的，以及如何进行查询即可。&lt;/p&gt;
&lt;h2&gt;装箱拆箱&lt;/h2&gt;
&lt;p&gt;在讨论具体的类型转换场景之前，我们先来说一下装箱拆箱操作。在这之前你应该复习一下 &lt;code&gt;JavaScript&lt;/code&gt; 中关于数据类型的知识，你可以看我的这一篇文章：&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/30/data-type-indicate/&quot; title=&quot;JS数据类型和判断方法&quot;&gt;JS数据类型和判断方法&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;装箱 wrapper&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中目前共有八种数据类型 &lt;code&gt;Undefined, Null, Number, String, Boolean, BigInt, Symbol, Object&lt;/code&gt;。除了 &lt;code&gt;Object&lt;/code&gt; 其他都是基本数据类型（&lt;code&gt;primitive values&lt;/code&gt;，也称原始值，原始类型）。所谓基本数据类型就是它们是一种即非对象也没有属性和方法的数据，基本类型直接代表了最底层的语言实现。&lt;/p&gt;
&lt;p&gt;所有基本类型的值都是不可改变的。但需要注意的是，基本类型本身和一个赋值为基本类型的变量的区别。变量会被赋予一个新值，而原值不能像数组、对象以及函数那样被改变。即基本类型值可以被替换，但不能被改变。比如，&lt;code&gt;JavaScript&lt;/code&gt; 中对字符串的操作一定返回了一个新字符串，原始字符串并没有被改变。&lt;/p&gt;
&lt;p&gt;既然如此，为什么我们还能在 &lt;code&gt;Number&lt;/code&gt; 或者 &lt;code&gt;String&lt;/code&gt; 上使用方法呢？这就引出了 &lt;code&gt;JavaScript&lt;/code&gt; 中的基本包装类型（&lt;code&gt;primitive wrapper types&lt;/code&gt;，也成为原始包装类型），因为我们有在基本类型上频繁操作的需求（比如 &lt;code&gt;String&lt;/code&gt; 的截取，&lt;code&gt;Number&lt;/code&gt; 的格式转换等），所以 &lt;code&gt;JavaScript&lt;/code&gt; 也为基本类型内置了一系列的 &lt;code&gt;API&lt;/code&gt;。但是只有对象才能使用方法，所以 &lt;code&gt;JavaScript&lt;/code&gt; 就用基本包装类型来让基本类型能够拥有属性和方法。除了 &lt;code&gt;null&lt;/code&gt; 和 &lt;code&gt;undefined&lt;/code&gt; 之外，所有基本类型都有其对应的包装对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt; 为字符串基本类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Number&lt;/code&gt; 为数值基本类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BigInt&lt;/code&gt; 为大整数基本类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Boolean&lt;/code&gt; 为布尔基本类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol&lt;/code&gt; 为字面量基本类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中最重要的就是 &lt;code&gt;String&lt;/code&gt;，&lt;code&gt;Number&lt;/code&gt; 和 &lt;code&gt;Boolean&lt;/code&gt; 三种原始包装类型，也是我们本文重点讨论的内容。&lt;/p&gt;
&lt;p&gt;这些类型与其他引用类型相似，但同时也具有与各自的基本类型相应的特殊行为。 实际上，每当读取一个基本类型值的时候，后台就会创建一个对应的基本包装类型的对象，从而让我们 能够调用一些方法来操作这些数据。看下面的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let str1 = &apos;clloz&apos;
let str2 = str1.substring(2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中我们创建了一个基本类型的字符串 &lt;code&gt;str1&lt;/code&gt;，然后我们调用了 &lt;code&gt;str1&lt;/code&gt; 的 &lt;code&gt;substring&lt;/code&gt; 方法，从逻辑上来讲基本类型不应该有方法的。实际上 &lt;code&gt;JavaScript&lt;/code&gt; 在背后为我们创建了一个基本包装类型，大致过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let temp = new String(&apos;clloz&apos;)
let str2 = temp.substring(2)
temp = null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;引用类型与基本包装类型的主要区别就是对象的生存期。使用 new 操作符创建的引用类型的实例， 在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象，则只存在于一 行代码的执行瞬间，然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let str1 = &apos;clloz&apos;
str1.color = &apos;red&apos;
console.log(str1.color) // undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般情况下，我们不需要手动进行装箱操作，因为装箱后的基本类型就变成了一个对象，&lt;code&gt;typeof&lt;/code&gt; 将返回 &lt;code&gt;object&lt;/code&gt;，在转换为 &lt;code&gt;Boolean&lt;/code&gt; 的时候也会转换成 &lt;code&gt;true&lt;/code&gt;，比如 &lt;code&gt;Boolean(new Boolean(false))&lt;/code&gt; 将返回 &lt;code&gt;true&lt;/code&gt;。我们只需要根据自己的需求来创建基本类型即可，将是否需要装箱的判断交给引擎，一般来说我们能在代码中优化的内容，引擎一定会帮我们进行优化。&lt;/p&gt;
&lt;p&gt;最后说一说进行装箱的几种方法，这些方法对除了 &lt;code&gt;null&lt;/code&gt; 和 &lt;code&gt;undefined&lt;/code&gt; 的基本类型都有效（&lt;code&gt;null&lt;/code&gt; 和 &lt;code&gt;undefined&lt;/code&gt; 没有原生构造函数，因为它们并不需要 &lt;code&gt;API&lt;/code&gt;）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;用 &lt;code&gt;new&lt;/code&gt; 操作符调用对应类型的构造函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;Object&lt;/code&gt; 函数，带不带 &lt;code&gt;new&lt;/code&gt; 都可以。&lt;code&gt;Object()&lt;/code&gt; 构造函数将会根据参数的不同做以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果给定值是 &lt;code&gt;null&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt;，将会创建并返回一个空对象&lt;/li&gt;
&lt;li&gt;如果传进去的是一个基本类型的值，则会构造其包装类型的对象&lt;/li&gt;
&lt;li&gt;如果传进去的是引用类型的值，仍然会返回这个值，经他们复制的变量保有和源对象相同的引用地址&lt;/li&gt;
&lt;li&gt;当以非构造函数形式被调用时，&lt;code&gt;Object&lt;/code&gt; 的行为等同于 &lt;code&gt;new Object()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;利用 &lt;code&gt;call&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = 2
console.log(typeof a) //number
let t = function () {
  return this
}.call(a)
console.log(typeof t) //object
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;拆箱 toPrimitive&lt;/h2&gt;
&lt;p&gt;装箱的操作是为了让我们能够使用一些为基本类型内置的 &lt;code&gt;API&lt;/code&gt;。但有时我们也需要对对象进行拆箱操作，比如当我们进行四则运算，进行比较等逻辑运算，等等。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 标准中，规定了 &lt;code&gt;ToPrimitive&lt;/code&gt; 函数，它是对象类型到基本类型的转换（即，拆箱转换）。拆箱转换会尝试调用 &lt;code&gt;valueOf&lt;/code&gt; 和 &lt;code&gt;toString&lt;/code&gt; 来获得拆箱后的基本类型。如果 &lt;code&gt;valueOf&lt;/code&gt; 和 &lt;code&gt;toString&lt;/code&gt; 都不存在，或者没有返回基本类型，则会产生类型错误 &lt;code&gt;TypeError&lt;/code&gt;。&lt;code&gt;String&lt;/code&gt; 的拆箱转换会优先调用 &lt;code&gt;toString&lt;/code&gt;。在 &lt;code&gt;ES6&lt;/code&gt; 之后，还允许对象通过显式指定 &lt;code&gt;@@toPrimitive&lt;/code&gt; &lt;code&gt;Symbol&lt;/code&gt; 来覆盖原有的行为。&lt;/p&gt;
&lt;p&gt;这里为了让大家彻底明白拆箱的机制，我们直接把 &lt;a href=&quot;https://tc39.es/ecma262/#sec-toprimitive&quot; title=&quot;ECMAScript2021&quot;&gt;ECMAScript2021&lt;/a&gt; 标准中的定义拿过来解读一下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion.CSQktPq5_stsSC.webp&quot; alt=&quot;type-conversion0&quot; title=&quot;type-conversion0&quot;&gt;&lt;/p&gt;
&lt;p&gt;我主要讲一下 &lt;code&gt;2&lt;/code&gt; 中的步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;a&lt;/code&gt;：获取 &lt;code&gt;input&lt;/code&gt; 的 &lt;code&gt;@@toPrimitive&lt;/code&gt; 方法，&lt;code&gt;input&lt;/code&gt; 是一个对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;b&lt;/code&gt;：如果 &lt;code&gt;@@toPrimitive&lt;/code&gt; 不是 &lt;code&gt;undefined&lt;/code&gt;，然后&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt;：如果 &lt;code&gt;@@toPrimitive&lt;/code&gt; 方法中没有指定第二个参数，那么 &lt;code&gt;hint&lt;/code&gt; 设为 &lt;code&gt;default&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ii&lt;/code&gt;：如果第二个参数是 &lt;code&gt;string&lt;/code&gt;，那么 &lt;code&gt;hint&lt;/code&gt; 设为 &lt;code&gt;string&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iii&lt;/code&gt;：如果第二个参数是 &lt;code&gt;number&lt;/code&gt;，那么 &lt;code&gt;hint&lt;/code&gt; 设为 &lt;code&gt;number&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iv&lt;/code&gt;：以 &lt;code&gt;input&lt;/code&gt; 和 &lt;code&gt;hint&lt;/code&gt; 为参数调用 &lt;code&gt;@@toPrimitive&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;v&lt;/code&gt;：如果执行结果不是一个对象，那么返回结果。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vi&lt;/code&gt;：如果执行结果是一个对象，抛出 &lt;code&gt;TypeError&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;c&lt;/code&gt;：如果没有定义 &lt;code&gt;@@toPrimitive&lt;/code&gt; 方法，并且没有指定 &lt;code&gt;preferredType&lt;/code&gt;，那么 &lt;code&gt;preferredType&lt;/code&gt; 设为 &lt;code&gt;number&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;d&lt;/code&gt;：返回 &lt;code&gt;OrdinaryToPrimitive(input, preferredType)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以当我们没有指定 &lt;code&gt;@@toPrimitive&lt;/code&gt; 方法的时候，就是执行 &lt;code&gt;OrdinaryToPrimitive(input, preferredType)&lt;/code&gt;，该函数定义如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion1.BnBQDxaM_Z2gaAHC.webp&quot; alt=&quot;type-conversion1&quot; title=&quot;type-conversion1&quot;&gt;&lt;/p&gt;
&lt;p&gt;它接受两个参数 &lt;code&gt;O&lt;/code&gt; 和 &lt;code&gt;hint&lt;/code&gt;，也就是我们上面 &lt;code&gt;d&lt;/code&gt; 步骤中的 &lt;code&gt;input&lt;/code&gt; 和 &lt;code&gt;preferredType&lt;/code&gt;。主要步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hint&lt;/code&gt; 必须是 &lt;code&gt;string&lt;/code&gt; 或者 &lt;code&gt;number&lt;/code&gt; 的一种。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;hint&lt;/code&gt; 是 &lt;code&gt;string&lt;/code&gt;，就按顺序调用对象的 &lt;code&gt;toString&lt;/code&gt; 和 &lt;code&gt;valueOf&lt;/code&gt; 方法，如果调用后结果不是对象则返回。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;hint&lt;/code&gt; 是 &lt;code&gt;number&lt;/code&gt;，就按顺序调用对象的 &lt;code&gt;valueOf&lt;/code&gt; 和 &lt;code&gt;toString&lt;/code&gt; 方法，如果调用后结果不是对象则返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实逻辑还是比较清晰，并没有很复杂，最后在说一说 &lt;code&gt;toPrimitive&lt;/code&gt; 中的 &lt;code&gt;b&lt;/code&gt; 情况。&lt;code&gt;@@toPrimitive&lt;/code&gt; 方法就是让我们自定义拆箱的规则，而不是根据标准的规则进行，我们可以根据自己的需求定制拆箱的规则。&lt;code&gt;@@&lt;/code&gt; 开头的名字是标准中的 &lt;a href=&quot;https://tc39.es/ecma262/#sec-well-known-symbols&quot; title=&quot;Well-Known Symbols&quot;&gt;Well-Known Symbols&lt;/a&gt;，他们是内置的 &lt;code&gt;Symbol&lt;/code&gt;，作为属性的 &lt;code&gt;key&lt;/code&gt;。在 &lt;code&gt;ES2016&lt;/code&gt; 引入 &lt;code&gt;Symbol&lt;/code&gt; 后我们已经可以访问这些 &lt;code&gt;Symbol&lt;/code&gt;，比如 &lt;code&gt;@@match&lt;/code&gt;，&lt;code&gt;@@matchAll&lt;/code&gt; 等等，我们在编码中可以直接使用 &lt;code&gt;String.prototype.match&lt;/code&gt; 和 &lt;code&gt;String.prototype.matchAll&lt;/code&gt; 来调用，他们在引擎内部即调用的 &lt;code&gt;Symbol&lt;/code&gt; 对应的方法。&lt;code&gt;@@&lt;/code&gt; 是在标准文档中的名字，我们在 &lt;code&gt;JavaScript&lt;/code&gt; 编码中使用的名字是将 &lt;code&gt;@@&lt;/code&gt; 替换为 &lt;code&gt;Symbol.&lt;/code&gt;，所以我们给对象添加 &lt;code&gt;@@toPrimitive&lt;/code&gt; 属性就是添加一个 &lt;code&gt;Symbol.toPrimitive&lt;/code&gt; 属性。当引擎调用 &lt;code&gt;@@toPrimitive&lt;/code&gt; 的时候就会找到我们定义的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 一个没有提供 Symbol.toPrimitive 属性的对象，参与运算时的输出结果
var obj1 = {}
console.log(+obj1) // NaN
console.log(`${obj1}`) // &quot;[object Object]&quot;
console.log(obj1 + &apos;&apos;) // &quot;[object Object]&quot;

// 接下面声明一个对象，手动赋予了 Symbol.toPrimitive 属性，再来查看输出结果
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if (hint == &apos;number&apos;) {
      return 10
    }
    if (hint == &apos;string&apos;) {
      return &apos;hello&apos;
    }
    return true
  }
}
console.log(+obj2) // 10      -- hint 参数值是 &quot;number&quot;
console.log(`${obj2}`) // &quot;hello&quot; -- hint 参数值是 &quot;string&quot;
console.log(obj2 + &apos;&apos;) // &quot;true&quot;  -- hint 参数值是 &quot;default&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内置 &lt;code&gt;Symbol&lt;/code&gt; 参考 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol&quot; title=&quot;Symbol - MDN&quot;&gt;Symbol - MDN&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;类型转换&lt;/h2&gt;
&lt;p&gt;现在我们已经知道装箱和拆箱的规则，也就是掌握了类型转换的工具，剩下的只要搞清楚哪个场景用哪个工具进行转换即可。&lt;/p&gt;
&lt;h2&gt;显式强制类型转换&lt;/h2&gt;
&lt;p&gt;在讨论隐式强制类型转换之前，我们先讨论一下显式强制类型转换。&lt;/p&gt;
&lt;p&gt;所谓 &lt;strong&gt;显式强制类型转换&lt;/strong&gt; 指的就是我们直接调用 &lt;code&gt;Number()&lt;/code&gt;， &lt;code&gt;String()&lt;/code&gt; 和 &lt;code&gt;Boolean()&lt;/code&gt; 构造函数（不带 &lt;code&gt;new&lt;/code&gt;）对一个值进行类型转换。我们还是来解读标准文档。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意一点，标准文档中的蓝色的方法前面的 &lt;code&gt;!&lt;/code&gt; 不是取反的意思，你可以无视掉，就当做执行后面的方法就可以。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Number&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion2.-q7gS05q_ZVJtVU.webp&quot; alt=&quot;type-conversion2&quot; title=&quot;type-conversion2&quot;&gt;&lt;/p&gt;
&lt;p&gt;上面的截图就是对 &lt;code&gt;Number&lt;/code&gt; 构造函数的定义，内容很简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不是用 &lt;code&gt;new&lt;/code&gt; 调用的，则返回 &lt;code&gt;ToNumeric(value)&lt;/code&gt; 的值，&lt;code&gt;value&lt;/code&gt; 是我们传入的值，如果没有传入 &lt;code&gt;value&lt;/code&gt;，那么就返回 &lt;code&gt;+0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果是用 &lt;code&gt;new&lt;/code&gt; 调用，则生成基本包装类型对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;ToNumeric&lt;/code&gt; 的定义如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion3.Bn7oLgVC_Z9LFeT.webp&quot; alt=&quot;type-conversion3&quot; title=&quot;type-conversion3&quot;&gt;&lt;/p&gt;
&lt;p&gt;表格十分清晰，我就不解读了。表格中没有说的是 &lt;code&gt;String&lt;/code&gt;，&lt;code&gt;String&lt;/code&gt; 转 &lt;code&gt;Number&lt;/code&gt; 在标准中定义了非常长的内容，我个人理解就是不符合 &lt;code&gt;JavaScript&lt;/code&gt; 格式的 &lt;code&gt;string&lt;/code&gt; 返回 &lt;code&gt;NaN&lt;/code&gt;，其他返回对应的数字。所谓的符合格式就包括 &lt;code&gt;0o&lt;/code&gt; 或 &lt;code&gt;0&lt;/code&gt; 开头的八进制，&lt;code&gt;0x&lt;/code&gt; 开头的十六进制，&lt;code&gt;0b&lt;/code&gt; 开头的二进制，科学计数法等。&lt;/p&gt;
&lt;h3&gt;String&lt;/h3&gt;
&lt;p&gt;还是从标准解读：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion4.Db5xoc9C_Z1Eza60.webp&quot; alt=&quot;type-conversion4&quot; title=&quot;type-conversion4&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不是 &lt;code&gt;new&lt;/code&gt; 调用 &lt;code&gt;String&lt;/code&gt; 构造函数，返回 &lt;code&gt;ToString(value)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果是以 &lt;code&gt;new&lt;/code&gt; 调用 &lt;code&gt;String&lt;/code&gt; 构造函数，返回基本包装类型对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;ToStrong&lt;/code&gt; 定义如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion5.DEJcCyMH_Zo1wHX.webp&quot; alt=&quot;type-conversion5&quot; title=&quot;type-conversion5&quot;&gt;&lt;/p&gt;
&lt;p&gt;这当中 &lt;code&gt;Number::String&lt;/code&gt; 在标准中定义比较复杂，应该是进行了严格的数学定义，我们按我们正常的理解就可以了。&lt;code&gt;-0，-0&lt;/code&gt; 都是 &lt;code&gt;0&lt;/code&gt;，&lt;code&gt;NaN&lt;/code&gt; 返回 &lt;code&gt;&quot;NaN&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Boolean&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion6.mSQKLmvS_1PjffM.webp&quot; alt=&quot;type-conversion6&quot; title=&quot;type-conversion6&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不是 &lt;code&gt;new&lt;/code&gt; 调用 &lt;code&gt;Boolean&lt;/code&gt; 构造函数，返回 &lt;code&gt;ToBoolean(value)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果是以 &lt;code&gt;new&lt;/code&gt; 调用 &lt;code&gt;Boolean&lt;/code&gt; 构造函数，返回基本包装类型对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion7.DP8hnV7j_Z1LpJho.webp&quot; alt=&quot;type-conversion7&quot; title=&quot;type-conversion7&quot;&gt;&lt;/p&gt;
&lt;h2&gt;隐式强制类型转换&lt;/h2&gt;
&lt;p&gt;隐式强制类型转换可能是更让人头疼的一部分，其实只要搞清楚标准，隐式的转换也是用的我们上面看到的那些方法进行转换的，我们也不必记清楚每一个规则，只要知道到哪里去查，还有编码中避免一些会出问题的转换。我这里就找出一些我们比较常见的隐式转换的场景对标准进行解读。&lt;/p&gt;
&lt;h3&gt;算数运算符&lt;/h3&gt;
&lt;p&gt;在标准中所有的算数运算符最后都是由下面这个方法执行的 &lt;code&gt;lval&lt;/code&gt; 即操作符左边的值，&lt;code&gt;opText&lt;/code&gt; 即操作符，&lt;code&gt;rval&lt;/code&gt; 即操作符右边的值：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion8.JpWw26DR_gC0Ho.webp&quot; alt=&quot;type-conversion8&quot; title=&quot;type-conversion8&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果操作符是 &lt;code&gt;+&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;计算 &lt;code&gt;ToPrimitive(lval)&lt;/code&gt; 赋值给 &lt;code&gt;lprim&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 &lt;code&gt;ToPrimitive(rval)&lt;/code&gt; 赋值给 &lt;code&gt;rprim&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;lprim&lt;/code&gt; 或 &lt;code&gt;rprim&lt;/code&gt; 中有一个类型是 &lt;code&gt;String&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算 &lt;code&gt;ToString(lprim)&lt;/code&gt; 赋值给 &lt;code&gt;lstr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;计算 &lt;code&gt;ToString(rprim)&lt;/code&gt; 赋值给 &lt;code&gt;rstr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;拼接 &lt;code&gt;lstr&lt;/code&gt; 和 &lt;code&gt;rstr&lt;/code&gt; 并返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将 &lt;code&gt;lprim&lt;/code&gt; 赋值给 &lt;code&gt;lval&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将 &lt;code&gt;rprim&lt;/code&gt; 赋值给 &lt;code&gt;rval&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 &lt;code&gt;ToNumeric(lval)&lt;/code&gt;，赋值给 &lt;code&gt;lnum&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 &lt;code&gt;ToNumeric(rval)&lt;/code&gt;，赋值给 &lt;code&gt;rnum&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;Type(lnum)&lt;/code&gt; 和 &lt;code&gt;Type(rnum)&lt;/code&gt; 不同，抛出一个 &lt;code&gt;TypeError&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行算数运算&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以看到这段定义中的方法都是我们在上面显示转换中介绍过的方法。在算数操作符中 &lt;code&gt;ToPrimitive()&lt;/code&gt; 是并没有传入 &lt;code&gt;hint&lt;/code&gt; 的，所以就用默认 &lt;code&gt;number&lt;/code&gt;，所以在算术运算的类型转换中，总是先调用 &lt;code&gt;valueOf&lt;/code&gt;，后调用 &lt;code&gt;toString()&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;一元操作符&lt;/h3&gt;
&lt;p&gt;一元操作符的定义都非常简单，这里就不贴图了，直接给一个总结，如果你想看相关定义点击&lt;a href=&quot;https://tc39.es/ecma262/#sec-unary-operators&quot; title=&quot;ECMAScript 2021 - Unary Operators&quot;&gt;ECMAScript 2021 - Unary Operators&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;++&lt;/code&gt; -&gt; &lt;code&gt;ToNumeric&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--&lt;/code&gt; -&gt; &lt;code&gt;ToNumeric&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt; -&gt; &lt;code&gt;ToNumeric&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-&lt;/code&gt; -&gt; &lt;code&gt;ToNumeric&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~&lt;/code&gt; -&gt; &lt;code&gt;ToNumeric&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!&lt;/code&gt; -&gt; &lt;code&gt;ToBoolean&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里再给大家举个例子 &lt;code&gt;&apos;a&apos; + + &apos;a&apos;&lt;/code&gt; （注意两个加号不能相连）得到的结果是 &lt;code&gt;aNaN&lt;/code&gt;，因为第二个 &lt;code&gt;+&lt;/code&gt; 作为一元操作符，调用 &lt;code&gt;ToNumber()&lt;/code&gt; 最后的结果是 &lt;code&gt;NaN&lt;/code&gt;。然后执行 &lt;code&gt;&apos;a&apos; + NaN&lt;/code&gt;，就是回到算术运算符的定义，有一个是 &lt;code&gt;String&lt;/code&gt; 两个都转成 &lt;code&gt;String&lt;/code&gt; 然后返回拼接的字符串，所以最后的结果是 &lt;code&gt;aNaN&lt;/code&gt;。你也可以找一些例子进行验证。&lt;/p&gt;
&lt;h3&gt;关系运算符&lt;/h3&gt;
&lt;p&gt;所有的关系运算符（&lt;code&gt;&amp;#x3C;, &gt;, &amp;#x3C;=, &gt;=&lt;/code&gt;）的结果都是根据 &lt;code&gt;Abstract Relational Comparison&lt;/code&gt; 的返回值计算，所以我们先着重分析这个方法，看下图。由于在标准中统一用小于号，所以用 &lt;code&gt;leftFirst&lt;/code&gt; 表示是大于操作符还是小于操作符，&lt;code&gt;true&lt;/code&gt; 则为小于关系符，&lt;code&gt;false&lt;/code&gt; 则为大于关系符。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion9.CGnL7OWw_1gzz01.webp&quot; alt=&quot;type-conversion9&quot; title=&quot;type-conversion9&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以看到第一部就是进行拆箱操作，&lt;code&gt;hint&lt;/code&gt; 为 &lt;code&gt;number&lt;/code&gt;，也就是先调用 &lt;code&gt;valueOf&lt;/code&gt;，在调用 &lt;code&gt;toString&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当两个操作数 &lt;code&gt;operand&lt;/code&gt; 都是字符串的时候，会调用一个方法 &lt;code&gt;IsStringPrefix(a, b)&lt;/code&gt; 来计算结果。这个方法的意思就是：比如判断 &lt;code&gt;a&amp;#x3C;b&lt;/code&gt; 的结果，就是判断 &lt;code&gt;a&lt;/code&gt; 是不是 &lt;code&gt;b&lt;/code&gt; 的一个前缀，就是 &lt;code&gt;a&lt;/code&gt; 加上另一个字符串能构成 &lt;code&gt;b&lt;/code&gt;，如果能，则返回 &lt;code&gt;true&lt;/code&gt; ；如果 &lt;code&gt;b&lt;/code&gt; 是 &lt;code&gt;a&lt;/code&gt; 的前缀，则返回 &lt;code&gt;false&lt;/code&gt;，所以 &lt;code&gt;&apos;cl&apos; &amp;#x3C; &apos;clloz&apos;&lt;/code&gt; 会返回 &lt;code&gt;true&lt;/code&gt;。如果不存在前缀关系，则进行 &lt;code&gt;code unit&lt;/code&gt; 比较，在 &lt;code&gt;JavaScript&lt;/code&gt; 中是 &lt;code&gt;UTF-16&lt;/code&gt; 编码，从最低位开始进行码点比较相同则进入下一位，如果能找到一位是 &lt;code&gt;a&lt;/code&gt; 的码点小于 &lt;code&gt;b&lt;/code&gt; 则返回 &lt;code&gt;true&lt;/code&gt; 否则返回 &lt;code&gt;false&lt;/code&gt;。一般的字符串我们只要根据扩展 &lt;code&gt;ASCII&lt;/code&gt; 进行比较即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&apos;cllob&apos; &amp;#x3C; &apos;clloc&apos;) //true
console.log(&apos;cllob&apos; &amp;#x3C; &apos;clloa&apos;) //false
console.log(&apos;clloba&apos; &amp;#x3C; &apos;cllob&apos;) / false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bigInt&lt;/code&gt; 我们就跳过，因为运用不是很多。我们直接进入下面的 &lt;code&gt;ToNumeric&lt;/code&gt;，将两个操作数都进行 &lt;code&gt;ToNumeric&lt;/code&gt;，如果得到的结果类型相同，则调用对应类型的 &lt;code&gt;T::lessThan&lt;/code&gt;. &lt;code&gt;ToNumeric&lt;/code&gt; 的结果要么是 &lt;code&gt;Number&lt;/code&gt; 要么是 &lt;code&gt;BigInt&lt;/code&gt;，要么抛错，所以我们只要看 &lt;code&gt;Number::lessThan(x, y)&lt;/code&gt; 的定义即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;x&lt;/code&gt; 是 &lt;code&gt;NaN&lt;/code&gt;，返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;y&lt;/code&gt; 是 &lt;code&gt;NaN&lt;/code&gt;，返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 是相同的数值，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 一个是 &lt;code&gt;+0&lt;/code&gt; 一个是 &lt;code&gt;-0&lt;/code&gt; 返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 任意一个为 $\pm \infty$，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;其他情况进行数值比较（非零并且不是无穷），&lt;code&gt;x &amp;#x3C; y&lt;/code&gt; 返回 &lt;code&gt;true&lt;/code&gt;，否则返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意，得到的 &lt;code&gt;Abstract Relational Comparison&lt;/code&gt; 的返回值不是最终的结果&lt;/strong&gt;。对于 &lt;code&gt;&amp;#x3C;, &gt;&lt;/code&gt; 来说，如果 &lt;code&gt;Abstract Relational Comparison&lt;/code&gt; 返回值是 &lt;code&gt;undefined&lt;/code&gt;，则则返回 &lt;code&gt;false&lt;/code&gt;，否则直接返回 &lt;code&gt;Abstract Relational Comparison&lt;/code&gt; 的返回值。对于 &lt;code&gt;&amp;#x3C;=, &gt;=&lt;/code&gt;，如果 &lt;code&gt;Abstract Relational Comparison&lt;/code&gt; 的返回值是 &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt;，则返回 &lt;code&gt;false&lt;/code&gt;，否则返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里可能有同学疑惑 &lt;code&gt;&amp;#x3C;=&lt;/code&gt; 和 &lt;code&gt;&gt;=&lt;/code&gt; 的逻辑是不是错了，&lt;code&gt;Abstract Relational Comparison&lt;/code&gt; 的返回值是 &lt;code&gt;true&lt;/code&gt; 应该返回 &lt;code&gt;true&lt;/code&gt;，这里标准里面是将 &lt;code&gt;&amp;#x3C;=, &gt;=&lt;/code&gt; 的 &lt;code&gt;leftFirst&lt;/code&gt; 相对于 &lt;code&gt;&amp;#x3C;, &gt;&lt;/code&gt; 去了一个相反，这样能保持 &lt;code&gt;lessThan&lt;/code&gt; 中的逻辑最简单，即 &lt;code&gt;&amp;#x3C;, &gt;&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt; 的时候 &lt;code&gt;&amp;#x3C;=, &gt;=&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;；&lt;code&gt;&amp;#x3C;, &gt;&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 的时候 &lt;code&gt;&amp;#x3C;=, &gt;=&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt;。否则因为有第三条相等规则在，逻辑会比较复杂。具体的定义看 &lt;a href=&quot;https://tc39.es/ecma262/#sec-relational-operators-runtime-semantics-evaluation&quot; title=&quot;Relation-Operators -ECMAScript&quot;&gt;Relation-Operators -ECMAScript&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下面来几个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(null &amp;#x3C; -0) //false null被转为 +0，和 -0 进行lessThan 返回false，所以最终结果为 false

console.log(NaN &amp;#x3C; 10) //false 只要有 NaN，lessThan的结果就是 undefined，对于 &amp;#x3C; 和 &gt; 来说 undefined最后返回 false

console.log(NaN &amp;#x3C;= 10) //false 对于 &amp;#x3C;= 和 &gt;= 来说，undefined就是 false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;相等操作符&lt;/h3&gt;
&lt;p&gt;相等操作符有四个 &lt;code&gt;==, !=, ===, !==&lt;/code&gt;，定义在 &lt;a href=&quot;https://tc39.es/ecma262/#sec-equality-operators&quot; title=&quot;Equality-Operators - ECMAScript&quot;&gt;Equality-Operators - ECMAScript&lt;/a&gt;，其中最关键的就是两个方法：&lt;code&gt;Abstract Equality Comparison&lt;/code&gt; 和 &lt;code&gt;Strict Equality Comparison&lt;/code&gt;，前者是双等号的方法，后者是全等号的方法。定义间下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/type-conversion10.DtouZDf4_JNU7r.webp&quot; alt=&quot;type-conversion10&quot; title=&quot;type-conversion10&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以看到两个方法的定义长度完全不同 :joy: 。双等号可以算作是 &lt;code&gt;JavaScript&lt;/code&gt; 中的一个设计失误，非常不建议使用。这里我就说一说全等好的定义。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果两个操作数的类型不同，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果两个操作数的类型都是 &lt;code&gt;Number&lt;/code&gt;，调用 &lt;code&gt;Number::equal(x, y)&lt;/code&gt; 方法，返回方法的返回值（&lt;code&gt;BigInt&lt;/code&gt; 不讨论）。该方法定义在 &lt;a href=&quot;https://tc39.es/ecma262/#sec-numeric-types-number-equal&quot; title=&quot;Number::equal - ECMAScript&quot;&gt;Number::equal - ECMAScript&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两个操作数有一个是 &lt;code&gt;NaN&lt;/code&gt;，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;两个操作数是同一个数值，返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;两个操作数分别是 &lt;code&gt;+0&lt;/code&gt; 和 &lt;code&gt;-0&lt;/code&gt;，返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;上面的条件都不满足，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果类型不是 &lt;code&gt;Number&lt;/code&gt; 或 &lt;code&gt;BigInt&lt;/code&gt;，则返回 &lt;code&gt;SameValueNonNumeric(x, y)&lt;/code&gt; 的返回值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;断言：两者不是 &lt;code&gt;Number&lt;/code&gt; 和 &lt;code&gt;BigInt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;断言：两者类型相同&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;x&lt;/code&gt; 类型是 &lt;code&gt;Undefined&lt;/code&gt;，返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;x&lt;/code&gt; 类型是 &lt;code&gt;Null&lt;/code&gt;，返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;x&lt;/code&gt; 类型是 &lt;code&gt;String&lt;/code&gt;，则必须 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 的所有码点序列完全一致才返回 &lt;code&gt;true&lt;/code&gt;，否则返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;x&lt;/code&gt; 类型是 &lt;code&gt;Boolean&lt;/code&gt;，则必须 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 同为 &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;false&lt;/code&gt; 才返回 &lt;code&gt;true&lt;/code&gt;，否则返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;x&lt;/code&gt; 类型是 &lt;code&gt;Symbol&lt;/code&gt;，则必须 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 是同一个 &lt;code&gt;Symbol&lt;/code&gt; 才返回 &lt;code&gt;true&lt;/code&gt;，否则返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 是同一个对象，返回 &lt;code&gt;true&lt;/code&gt;，否则返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相等操作符到这里就讲完了，双等号我没有仔细看，因为我从来不用，也不建议大家使用。如果你有兴趣可以仔细阅读一下图片中的标准定义。&lt;/p&gt;
&lt;h2&gt;valueOf 和 toString&lt;/h2&gt;
&lt;p&gt;上面从标准的角度讲了类型转换，&lt;code&gt;toPrimitive&lt;/code&gt; 最后会尝试调用 &lt;code&gt;valueOf&lt;/code&gt; 或者 &lt;code&gt;toString&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;valueOf&lt;/code&gt; 就是返回对象的原始值，这个方法是在 &lt;code&gt;Object.prototype&lt;/code&gt; 上。&lt;code&gt;JavaScript&lt;/code&gt; 调用 &lt;code&gt;valueOf&lt;/code&gt; 方法将对象转换为原始值。你很少需要自己调用 &lt;code&gt;valueOf&lt;/code&gt; 方法；当遇到要预期的原始值的对象时，&lt;code&gt;JavaScript&lt;/code&gt; 会自动调用它。&lt;/p&gt;
&lt;p&gt;默认情况下，&lt;code&gt;valueOf&lt;/code&gt; 方法由 &lt;code&gt;Object&lt;/code&gt; 后面的每个对象继承。 每个内置的核心对象都会覆盖此方法以返回适当的值。如果对象没有原始值，则 &lt;code&gt;valueOf&lt;/code&gt; 将返回对象本身。&lt;code&gt;JavaScript&lt;/code&gt; 的许多内置对象都重写了该函数，以实现更适合自身的功能需要。因此，不同类型对象的 &lt;code&gt;valueOf()&lt;/code&gt; 方法的返回值和返回值类型均可能不同。不同类型对象的 &lt;code&gt;valueOf()&lt;/code&gt; 方法的返回值如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Array&lt;/code&gt;：返回数组对象本身。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Boolean&lt;/code&gt;：返回布尔值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Date&lt;/code&gt;：存储的时间从 &lt;code&gt;1970 年 1 月 1 日&lt;/code&gt; 午夜开始计的毫秒数 &lt;code&gt;UTC&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Function&lt;/code&gt;：函数本身。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Number&lt;/code&gt;：数字值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object&lt;/code&gt;：对象本身。这是默认情况。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt;：字符串值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Math&lt;/code&gt; 和 &lt;code&gt;Error&lt;/code&gt; 对象没有 &lt;code&gt;valueOf&lt;/code&gt; 方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以在自己的代码中使用 &lt;code&gt;valueOf&lt;/code&gt; 将内置对象转换为原始值。 创建自定义对象时，可以覆盖 &lt;code&gt;Object.prototype.valueOf()&lt;/code&gt; 来调用自定义方法，而不是默认 &lt;code&gt;Object&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;每个对象都有一个 &lt;code&gt;toString()&lt;/code&gt; 方法，当该对象被表示为一个文本值时，或者一个对象以预期的字符串方式引用时自动调用。默认情况下，&lt;code&gt;toString()&lt;/code&gt; 方法被每个 &lt;code&gt;Object&lt;/code&gt; 对象继承。如果此方法在自定义对象中未被覆盖，&lt;code&gt;toString()&lt;/code&gt; 返回 &lt;code&gt;[object type]&lt;/code&gt;，其中 &lt;code&gt;type&lt;/code&gt; 是对象的类型。&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;valueOf&lt;/code&gt; 就是返回这个对象本来的“面目”，比如一个被包装过的 &lt;code&gt;Number&lt;/code&gt;；而 &lt;code&gt;toString&lt;/code&gt; 则是把对象转化成一个字符串。不同的对象有不同的处理方式，内置对象几乎都实现了自己的对应方法覆盖 &lt;code&gt;Object.prototype&lt;/code&gt; 上的方法。这两个方法主要就是为了应对不同的类型在进行某些操作是需要进行类型转换的情况。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这篇文章应该将双等号以外的绝大多数类型转换的情况都说清楚了，而且是根据标准来讲的，还是比较清晰的。其实整个思路理下来也不是非常的复杂，所以有时候就是 &lt;code&gt;Just Do It!&lt;/code&gt; :punch: 。希望这篇文章给你带来帮助，如果有错误的地方，欢迎指正。&lt;/p&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>函数柯里化</title><link>https://clloz.com/blog/curry</link><guid isPermaLink="true">https://clloz.com/blog/curry</guid><pubDate>Mon, 12 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文讲一讲面试经常出现但是实际编码中很少使用到 :laughing: 的函数柯里化的实现。&lt;/p&gt;
&lt;h2&gt;什么是函数柯里化&lt;/h2&gt;
&lt;p&gt;柯里化是一个函数式编程的概念，维基百科的定义时在数学和计算机科学中，柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。大致的效果是这样，比如一个函数需要三个参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function fu(a, b, c) {
  console.log(a, b, c)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般我们调用这样的函数是一次性传入三个参数 &lt;code&gt;fn(1, 2, 3)&lt;/code&gt;，但是经过柯里化之后，我们可以分次传入参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function curry(fn) {
  //content ....
  return function () {
    //content ...
  }
}

let newFn = curry(fn)

newFn(1)(2)(3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么函数柯里化有什么用呢？一个比较明显的作用就是返回一个已经设定好参数的函数，当我们的函数参数有多种可能情况的时候，比如 &lt;code&gt;ajax&lt;/code&gt; 我们可能需要传入 &lt;code&gt;type, url, data&lt;/code&gt; 等数据，那我们可以用 &lt;code&gt;curry&lt;/code&gt; 进行包装，得到预设好 &lt;code&gt;type&lt;/code&gt; 的 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;post&lt;/code&gt; 函数，这样我们使用的时候更加方便。但其实这个功能用 &lt;code&gt;bing&lt;/code&gt; 也能实现。&lt;/p&gt;
&lt;p&gt;所以我觉得 &lt;code&gt;bind&lt;/code&gt; 和 &lt;code&gt;curry&lt;/code&gt; 是有一些相同的逻辑的。比如我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/07/simulation-of-call-apply-bind/&quot; title=&quot;模拟实现call，apply 和 bind&quot;&gt;模拟实现call，apply 和 bind&lt;/a&gt; 一文中写的用 &lt;code&gt;bind&lt;/code&gt; 可以将 &lt;code&gt;Array.prototype.slice.apply(arguments)&lt;/code&gt; 简化成 &lt;code&gt;slice(arguments)&lt;/code&gt; 的调用方式。&lt;code&gt;curry&lt;/code&gt; 也有相同的作用。以我的理解就是函数柯里化是对函数的一种抽象和包装，让我们能够更好地定制函数，更简洁地调用函数。&lt;/p&gt;
&lt;p&gt;虽然可能在实际编码中我们不太会用到这个，但是之所以经常有面试问到这个问题我觉得是考查你对于函数的理解。函数既可以作为一个参数，也可以作为一个返回值。也可以赋值给变量，所谓的一等公民。只要是函数式编程就可以实现柯里化，他也是函数式编程语言自带的一个特性。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;实现函数柯里化的主要逻辑： 1. 实现一个 &lt;code&gt;curry&lt;/code&gt; 函数 &lt;code&gt;function curry() {}&lt;/code&gt;，该函数接受一个函数&lt;code&gt;beCurry&lt;/code&gt;作为参数，返回一个函数 &lt;code&gt;fn_judge&lt;/code&gt;。这个返回的函数 &lt;code&gt;fn_judge&lt;/code&gt; 才是我们后面分次接受参数执行的函数，也是我们的主要逻辑。 2. 给 &lt;code&gt;fn_judge&lt;/code&gt; 传入参数，执行 &lt;code&gt;fn_judge&lt;/code&gt;，按照柯里化的定义，每次只传入一个参数，实际可以根据自己的需求。 3. &lt;code&gt;fn_judge&lt;/code&gt; 的主要逻辑就是用一个变量 &lt;code&gt;args&lt;/code&gt; 保存传入的参数，判断传入的参数个数是否足够执行 &lt;code&gt;beCurry&lt;/code&gt;，如果足够执行了。直接执行 &lt;code&gt;beCurry&lt;/code&gt;；如果不够，则返回一个匿名函数 &lt;code&gt;fn_anonymous&lt;/code&gt;。 4. 后面传入参数就是执行 &lt;code&gt;fu_anonymous&lt;/code&gt;，&lt;code&gt;fn_anonymous&lt;/code&gt; 的逻辑很简单，将传入 &lt;code&gt;fn_anonymous&lt;/code&gt; 的参数拼接到 &lt;code&gt;args&lt;/code&gt; 上，递归调用 &lt;code&gt;fn_judge&lt;/code&gt;，然后回到第二步再依次执行。&lt;/p&gt;
&lt;p&gt;其实说简单一点，就是我们在函数外面进行了一层逻辑包装：当前是否有足够参数执行函数？没有则将参数保存起来，返回一个函数继续接受参数，下次执行要将新传入的参数和保存的参数合并；如果有，则立即执行函数。最后其实我们要解决的就是如何保存和合并参数的问题，解决这个问题的方法很多，也大同小异。我的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function curry(fn) {
  let length = fn.length

  return function judge() {
    let args = Array.prototype.slice.call(arguments)

    if (args.length &amp;#x3C; length) {
      return function () {
        args = Array.prototype.concat.apply(args, arguments)
        return judge.apply(null, args)
      }
    } else {
      return fn.apply(null, args)
    }
  }
}

var fn = curry(function (a, b, c) {
  console.log([a, b, c])
})

fn(&apos;a&apos;, &apos;b&apos;, &apos;c&apos;) // [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]
fn(&apos;a&apos;, &apos;b&apos;)(&apos;c&apos;) // [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]
fn(&apos;a&apos;)(&apos;b&apos;)(&apos;c&apos;) // [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]
fn(&apos;a&apos;)(&apos;b&apos;, &apos;c&apos;) // [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然如果你有需要可以设置 &lt;code&gt;judge&lt;/code&gt; 只可以传入一个参数，一般来说不需要。&lt;/p&gt;
&lt;p&gt;最后还有一个问题就是 &lt;code&gt;this&lt;/code&gt; 的问题，比如如下这样的形式我们按上面的方式进行柯里化会丢失 &lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let obj = {
  id: &apos;clloz&apos;,
  age: &apos;28&apos;,
  func: function (a, b, c) {
    console.log(this.id, this.age)
    console.log([a, b, c])
  }
}
obj.func(1, 2, 3)
//clloz 28
//[1, 2, 3]

var fn = curry(obj.func, obj)
fn(1)(2)(3)
//undefined undefined
//[1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我没想到特别好的解决办法，唯一就是在进行柯里化的时候传入需要的 &lt;code&gt;this&lt;/code&gt; 在后面 &lt;code&gt;apply&lt;/code&gt; 的时候传入这个 &lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function curry(fn, thisArg) {
  let length = fn.length

  return function judge() {
    let args = Array.prototype.slice.call(arguments)

    if (args.length &amp;#x3C; length) {
      return function () {
        args = Array.prototype.concat.apply(args, arguments)
        return judge.apply(thisArg, args)
      }
    } else {
      return fn.apply(thisArg, args)
    }
  }
}

let obj = {
  name: &apos;clloz&apos;,
  age: &apos;28&apos;,
  func: function (a, b, c) {
    console.log(this.name, this.age)
    console.log([a, b, c])
  }
}
var fn = curry(obj.func, obj)
fn(1)(2)(3)
//clloz 28
//[ 1, 2, 3 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其他实现&lt;/h2&gt;
&lt;p&gt;这里给大家贴上一些别人的实现，主要的逻辑是不变的，看能不能帮助理解。&lt;/p&gt;
&lt;h2&gt;冴羽的实现&lt;/h2&gt;
&lt;p&gt;冴羽大神的博客非常建议大家看，很多非常深入的知识，博客地址：&lt;a href=&quot;https://github.com/mqyqingfeng/Blog&quot; title=&quot;的博客&quot;&gt;冴羽的博客&lt;/a&gt;。他的 &lt;a href=&quot;https://github.com/mqyqingfeng/Blog/issues/42&quot; title=&quot;JavaScript专题之函数柯里化&quot;&gt;JavaScript专题之函数柯里化&lt;/a&gt; 这篇博客里也贴出了几种实现。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;逻辑比较 &lt;strong&gt;复杂&lt;/strong&gt; 的实现&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function sub_curry(fn) {
  var args = [].slice.call(arguments, 1)
  return function () {
    return fn.apply(this, args.concat([].slice.call(arguments)))
  }
}

function curry(fn, length) {
  length = length || fn.length

  var slice = Array.prototype.slice

  return function () {
    if (arguments.length &amp;#x3C; length) {
      var combined = [fn].concat(slice.call(arguments))
      return curry(sub_curry.apply(this, combined), length - arguments.length)
    } else {
      return fn.apply(this, arguments)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;和我上面的实现逻辑比较接近的实现&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function curry(fn, args) {
  var length = fn.length

  args = args || []

  return function () {
    var _args = args.slice(0),
      arg,
      i

    for (i = 0; i &amp;#x3C; arguments.length; i++) {
      arg = arguments[i]

      _args.push(arg)
    }
    if (_args.length &amp;#x3C; length) {
      return curry.call(this, fn, _args)
    } else {
      return fn.apply(this, _args)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以改变参数顺序的实现&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function curry(fn, args, holes) {
  length = fn.length

  args = args || []

  holes = holes || []

  return function () {
    var _args = args.slice(0),
      _holes = holes.slice(0),
      argsLen = args.length,
      holesLen = holes.length,
      arg,
      i,
      index = 0

    for (i = 0; i &amp;#x3C; arguments.length; i++) {
      arg = arguments[i]
      // 处理类似 fn(1, _, _, 4)(_, 3) 这种情况，index 需要指向 holes 正确的下标
      if (arg === _ &amp;#x26;&amp;#x26; holesLen) {
        index++
        if (index &gt; holesLen) {
          _args.push(arg)
          _holes.push(argsLen - 1 + index - holesLen)
        }
      }
      // 处理类似 fn(1)(_) 这种情况
      else if (arg === _) {
        _args.push(arg)
        _holes.push(argsLen + i)
      }
      // 处理类似 fn(_, 2)(1) 这种情况
      else if (holesLen) {
        // fn(_, 2)(_, 3)
        if (index &gt;= holesLen) {
          _args.push(arg)
        }
        // fn(_, 2)(1) 用参数 1 替换占位符
        else {
          _args.splice(_holes[index], 1, arg)
          _holes.splice(index, 1)
        }
      } else {
        _args.push(arg)
      }
    }
    if (_holes.length || _args.length &amp;#x3C; length) {
      return curry.call(this, fn, _args, _holes)
    } else {
      return fn.apply(this, _args)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;利用箭头函数一行实现&lt;/h2&gt;
&lt;p&gt;这个实现来自 &lt;code&gt;segmentfault&lt;/code&gt; 的大笑平。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var curry = (fn) =&gt;
  (judge = (...args) =&gt; (args.length === fn.length ? fn(...args) : (arg) =&gt; judge(...args, arg)))

//分行
var curry = (fn) =&gt;
  (judge = (...args) =&gt; (args.length === fn.length ? fn(...args) : (arg) =&gt; judge(...args, arg)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看着很美，但是可读性很差，我把它转化成了比较好理解的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function curry(fn) {
  return function judge(...args) {
    if (args.length === fn.length) {
      return fn(...args)
    } else {
      return function (arg) {
        return judge(...args, arg)
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mqyqingfeng/Blog/issues/42&quot; title=&quot;JavaScript专题之函数柯里化&quot;&gt;JavaScript专题之函数柯里化&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/yygmind/blog/issues/37&quot; title=&quot;【进阶 6-2 期】深入高阶函数应用之柯里化&quot;&gt;【进阶 6-2 期】深入高阶函数应用之柯里化&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>实现 B 站首页头部效果</title><link>https://clloz.com/blog/bilibili-header-effect</link><guid isPermaLink="true">https://clloz.com/blog/bilibili-header-effect</guid><pubDate>Sun, 11 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;今天偶然发现 &lt;code&gt;B&lt;/code&gt; 站首页的头部有一个移动鼠标，背景移动并且透明度变化，也带有一点视差滚动的效果。同时小人的眼睛隔一段时间眨一下。我觉得挺有趣就自己实现了一下。&lt;/p&gt;
&lt;p&gt;实现效果：点击查看 &lt;a href=&quot;https://clloz.com/study/bilibili-header-effect/dist/&quot; title=&quot;Demo&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;实现思路&lt;/h2&gt;
&lt;p&gt;分析一下这个效果大概就是两个部分。第一个部分是人物的眨眼，这个我用的嵌套的定时器实现。第二个部分就是鼠标移动改变图片的位置和透明度，这个用鼠标事件解决。&lt;/p&gt;
&lt;h2&gt;眨眼效果&lt;/h2&gt;
&lt;p&gt;眨眼效果的其实就是几张图片的连续播放。但是需要注意的是，眨眼的动作是非常快的，整个动作要三个状态，睁眼，半睁眼，闭眼，我们需要四张图片循环即可。&lt;/p&gt;
&lt;p&gt;由于眨眼的帧之间的间隔和两次眨眼之间的间隔是不同的，我们需要嵌套定时器。外部的就是控制多久眨眼一次，内部的就是眨眼的几个帧的切换。对于定时器的嵌套中的一些细节可以看我的另一篇文章：&lt;a href=&quot;https://clloz.com/blog/settimeout/&quot; title=&quot;定时器的一些思考&quot;&gt;定时器的一些思考&lt;/a&gt;。最后的代码大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let img = this.layers[anime.index].querySelector(&apos;img&apos;)

setInterval(() =&gt; {
  let index = 0

  let blink = setInterval(() =&gt; {
    img.src = `./images/${anime.path}/${index % anime.length}.png`
    index++
    if (index === anime.length + 1) clearInterval(blink)
  }, 100)
}, 5000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有一个我没有解决的问题，我的图片切换是用改变 &lt;code&gt;img&lt;/code&gt; 的 &lt;code&gt;src&lt;/code&gt; 来实现的。这样会出现一个问题，每次改变 &lt;code&gt;src&lt;/code&gt; 浏览器都会发送新的请求。而且由于我们的这种行为在浏览器看来很怪异，可能会被 &lt;code&gt;cancel&lt;/code&gt; 掉（关于浏览器请求的 &lt;code&gt;cancel&lt;/code&gt; 可以参考这篇文章 &lt;a href=&quot;https://segmentfault.com/a/1190000008492105&quot; title=&quot;浏览器你为什么要干掉我的请求？&quot;&gt;浏览器你为什么要干掉我的请求？&lt;/a&gt;。这就导致了我们本地运行可能没问题，但是到线上访问动画就时灵时不灵了。我看 &lt;code&gt;B&lt;/code&gt; 站的实现似乎是将 &lt;code&gt;img&lt;/code&gt; 这个元素替换了，源码混淆过，看着着实吃力，放弃了。这里应该是一个知识盲区，我尝试了替换元素，但是并没有效果 :dizzy_face: 。这里暂时只能留个坑，等以后知道怎么解决了再来填。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;在 &lt;code&gt;segmentfault&lt;/code&gt; 上提问后，&lt;a href=&quot;https://segmentfault.com/u/fractal_5a0bfbf5bc7e7&quot; title=&quot;Fractal&quot;&gt;Fractal&lt;/a&gt; 给出的回答是预先生成好 &lt;code&gt;img&lt;/code&gt; 元素，放到一个数组中，然后定时器内替换即可。我是用 &lt;code&gt;cloneNode&lt;/code&gt; 和 &lt;code&gt;replaceChild&lt;/code&gt; 两个方法进行创建和切换的。这样就基本实现了原先要的效果了。以后遇到这种加载切换问题应该也可以用同样的逻辑解决。&lt;/p&gt;
&lt;h2&gt;鼠标事件&lt;/h2&gt;
&lt;p&gt;第二个效果就是移动鼠标图片会跟着移动，然后透明度变化，最后的效果就是感觉随着鼠标的移动，我们的视角也发生变化的感觉。这部分的实现就是用三个鼠标事件，&lt;code&gt;mouseover, mousemove, mouseout&lt;/code&gt;。然后改变元素的 &lt;code&gt;style&lt;/code&gt; 对象中的属性即可，并没有什么复杂的逻辑。需要注意的是，&lt;code&gt;mouseout&lt;/code&gt; 之后我们要将元素的状态还原，同时解绑 &lt;code&gt;mousemove&lt;/code&gt; 和 &lt;code&gt;mouseout&lt;/code&gt; 事件。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;完整的代码在 &lt;a href=&quot;https://github.com/Clloz/bilibili-header-effect&quot; title=&quot;Github&quot;&gt;Github&lt;/a&gt;。运行方法为 &lt;code&gt;npm install&lt;/code&gt; 之后运行 &lt;code&gt;npx webpack-dev-server&lt;/code&gt; 然后打开 &lt;code&gt;index.html&lt;/code&gt; 即可。&lt;/p&gt;</content:encoded><h:img src="/_astro/project.BLrgMxA0.png"/><enclosure url="/_astro/project.BLrgMxA0.png"/></item><item><title>深入 JavaScript 数组</title><link>https://clloz.com/blog/javascript-array-skills</link><guid isPermaLink="true">https://clloz.com/blog/javascript-array-skills</guid><pubDate>Fri, 09 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文整理一些我觉得有用或者没用的（说不定哪天就有用了 :stuck_out_tongue: ） &lt;code&gt;JavaScript&lt;/code&gt; 数组技巧。本文前半部分是对 &lt;code&gt;Array&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt; 进行比较深入的解读，后半部分是对一些具体场景的解决方案。本文较长，请耐心阅读。&lt;/p&gt;
&lt;h2&gt;创建数组&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 的数组创建大致三种方式：字面量，&lt;code&gt;new Array(el1, el2, el3...)&lt;/code&gt;，&lt;code&gt;new Array(length)&lt;/code&gt;。&lt;code&gt;new&lt;/code&gt; 可以省略。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;new Array()&lt;/code&gt; 有两种初始化可能，即 &lt;code&gt;new Array(el1, el2, el3...)&lt;/code&gt;，&lt;code&gt;new Array(length)&lt;/code&gt;，出现第二种情况当且仅当只有一个参数，并且该参数是 $1 - (2^{32} - 1)$ 之间的整数，其他情况都会把传入的参数当做生成的数组的元素。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上， &lt;code&gt;JavaScript&lt;/code&gt; 并没有常规的数组，所有的数组其实就是个对象，只不过会自动管理一些&quot;数字&quot;属性和 &lt;code&gt;length&lt;/code&gt; 属性罢了。说的更直接一点, &lt;code&gt;JavaScript&lt;/code&gt; 中的数组根本没有索引，因为索引应该是数字，而 &lt;code&gt;JavaScript&lt;/code&gt; 中数组的索引其实是字符串。&lt;code&gt;arr[1]&lt;/code&gt;其实就是 &lt;code&gt;arr[&quot;1&quot;]&lt;/code&gt;，给&lt;code&gt;arr[&quot;1000&quot;] = 1&lt;/code&gt;，&lt;code&gt;arr.length&lt;/code&gt; 也会自动变为 &lt;code&gt;1001&lt;/code&gt;。这些表现的根本原因就是，&lt;code&gt;JavaScript&lt;/code&gt; 中的对象就是字符串到任意值的键值对。注意键只能是字符串。不过目前 &lt;code&gt;ES6&lt;/code&gt; 中已经有了类似于 &lt;code&gt;Java&lt;/code&gt; 等语言的 &lt;code&gt;Map&lt;/code&gt; 类型,键可以是任意类型的值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;稀疏数组和密集数组&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;Java&lt;/code&gt; 和C语言中，数组是一片连续的存储空间，有着固定的长度。即数组元素之间是紧密相连的，不存在空隙，这就是密集数组。在 &lt;code&gt;JavaScript&lt;/code&gt; 中是支持稀疏数组的，比如我们用 &lt;code&gt;new Array(length)&lt;/code&gt; 创建的就是一个稀疏数组。&lt;/p&gt;
&lt;p&gt;我们熟知的 &lt;code&gt;Array&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt; 都会对这些空位有自己的处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flat()，flatMap()&lt;/code&gt; 会移除空位。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.keys()&lt;/code&gt;，&lt;code&gt;Object.entires()&lt;/code&gt; 会跳过数组的空位&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forEach(), filter(), reduce(), every() 和some()&lt;/code&gt;都会跳过空位。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map()&lt;/code&gt; 会跳过空位，但会保留这个值，即保留这个空位。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;join()&lt;/code&gt;和 &lt;code&gt;toString()&lt;/code&gt; 会将空位视为 &lt;code&gt;undefined&lt;/code&gt;，而 &lt;code&gt;undefined&lt;/code&gt; 和 &lt;code&gt;null&lt;/code&gt; 会被处理成空字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.from&lt;/code&gt; 将数组的空位转为 &lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;…&lt;/code&gt; 将空位转换为 &lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;copyWithin&lt;/code&gt; 将空位一起拷贝&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fill()&lt;/code&gt; 将空位视为正常的数组位置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for…of&lt;/code&gt; 循环会遍历空位&lt;/li&gt;
&lt;li&gt;&lt;code&gt;entries()&lt;/code&gt;、&lt;code&gt;keys()&lt;/code&gt;、&lt;code&gt;includes()&lt;/code&gt;、&lt;code&gt;values()&lt;/code&gt;、&lt;code&gt;find()&lt;/code&gt; 和 &lt;code&gt;findIndex()&lt;/code&gt; 将空位处理成 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你遍历数组希望跳过数组中的未赋值空位，可以使用 &lt;code&gt;in&lt;/code&gt; 操作符，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = []
a[1] = &apos;clloz&apos;
console.log(a) //[ &amp;#x3C;1 empty item&gt;, &apos;clloz&apos; ]
console.log(0 in a) //false
console.log(1 in a) //true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;创建密集数组的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//利用 apply 将数组作为多个参数传入
Array.apply(null, Array(3)) //[undefined, undefined, undefined]

//生成一个元素值等于下标的数组
Array.apply(null, Array(3)).map(Function.prototype.call.bind(Number)) // [0, 1, 2]
//上面这行代码等同于下面的代码
;[undefined, undefined, undefined].map((value, index) =&gt; Number.call(value, index)) //index

Array.from({ length: 3 }) //Array.from() 可以作用于拥有一个 length 属性和若干索引属性的任意对象

Array.apply(null, { length: 3 }) //这种用法突出了JavaScript中的数组其实就是个对象，只要有 `length` 属性，并且都是数字索引属性即使数组

new Array(...Array(3)) //扩展运算符和 apply 类似的效果
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Array.apply&lt;/code&gt; 实际上是利用了 &lt;code&gt;apply&lt;/code&gt; 将数组作为多个参数传递的特性来生成密集数组，&lt;code&gt;Array.call(null, Array(3))&lt;/code&gt; 实际上等于 &lt;code&gt;Array(arr[0], arr[1], arr[2])&lt;/code&gt;（设 &lt;code&gt;Array(3)&lt;/code&gt; 生成的数组是 &lt;code&gt;arr&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;如果想让数组变为稀疏数组，可以用 &lt;code&gt;delete&lt;/code&gt; 操作符删除数组的项，比如 &lt;code&gt;delete arr[1]&lt;/code&gt; 则下标为 &lt;code&gt;1&lt;/code&gt; 的数组项会变为 &lt;code&gt;empty&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Array API 深入&lt;/h2&gt;
&lt;p&gt;我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/10/built-in-objects-api/&quot; title=&quot;JavaScript常用内置对象API&quot;&gt;JavaScript常用内置对象API&lt;/a&gt; 一文整理了一些 &lt;code&gt;JavaScript&lt;/code&gt; 内置对象的 &lt;code&gt;API&lt;/code&gt;，主要是当做工具表，在使用一些不熟悉的 &lt;code&gt;API&lt;/code&gt; 的时候有快速查询的地方（不常使用的知识很快就忘了 :sleeping: ，不过学习也就是不断遗忘和重复的过程）。本文主要是说 &lt;code&gt;Array&lt;/code&gt; 的技巧，必然要对 &lt;code&gt;API&lt;/code&gt; 深入一些，这一小节我们就对 &lt;code&gt;Array&lt;/code&gt; 相关的内容进行梳理。&lt;/p&gt;
&lt;p&gt;因为数组的 &lt;code&gt;API&lt;/code&gt; 很多，有些很类似，特别是有没有返回值，以及是否会改变原数组，很容易混淆，在这一小节的最前面进行一个整理。&lt;/p&gt;
&lt;h3&gt;不改变原数组&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Array.from()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.isArray()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.concat()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.every()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.filter()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.find()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.findIndex()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.flat()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.flatMap()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.forEach()&lt;/code&gt; &lt;strong&gt;forEach本身不改变原数组，但是 callback 可能会改变&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.includes()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.map()&lt;/code&gt; &lt;strong&gt;map，但是 callback 可能会改变&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.reduce()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.reduceRight()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.slice()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;改变原数组&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.copyWithin()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.fill()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.pop()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.push()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.shift()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.sort()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.splice()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.unshift()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;返回数组&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Array.from()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.concat()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.copyWithin()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.fill()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.filter()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.flat()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.flatMap()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.map()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.slice()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.sort()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.splice()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;不返回数组&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Array.isArray()&lt;/code&gt; 返回 &lt;code&gt;Boolean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.every()&lt;/code&gt; 返回 &lt;code&gt;Boolean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.find()&lt;/code&gt; 返回第一个满足测试函数的元素值或 &lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.findIndex()&lt;/code&gt; 返回第一个满足测试函数的元素索引或 &lt;code&gt;-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.forEach()&lt;/code&gt; 返回 &lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.includes()&lt;/code&gt; 返回 &lt;code&gt;Boolean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.pop()&lt;/code&gt; 返回元素的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.pop()&lt;/code&gt; 返回数组的新长度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.reduce()&lt;/code&gt; 返回遍历完成后累积的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.reduceRight()&lt;/code&gt;返回遍历完成后累积的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.shift()&lt;/code&gt; 返回删除的元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.unshift()&lt;/code&gt; 返回新数组的长度&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Array.length&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 规定了数组的 &lt;code&gt;length&lt;/code&gt; 是一个 &lt;code&gt;32bits&lt;/code&gt; 无符号整数，所以数组的最大长度是是 $2^{32} - 1$，所以 &lt;code&gt;Array.length&lt;/code&gt; 的范围应该是在 &lt;code&gt;0 - 4294967295&lt;/code&gt; 之间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var namelistA = new Array(4294967296) // 2的32次方 = 4294967296
var namelistC = new Array(-100) // 负号

console.log(namelistA.length) // RangeError: 无效数组长度
console.log(namelistC.length) // RangeError: 无效数组长度

var namelistB = []
namelistB.length = Math.pow(2, 32) - 1 //set array length less than 2 to the 32nd power
console.log(namelistB.length) // 4294967295
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改变 &lt;code&gt;length&lt;/code&gt; 的大小会改变数组。当我们设置 &lt;code&gt;length&lt;/code&gt; 小于数组长度的时候。超过的部分会被截断。当我们设置 &lt;code&gt;length&lt;/code&gt; 大于数组长度的时候，实际的元素数目会增加，新增的元素的值为 &lt;code&gt;undefined&lt;/code&gt;（实际上和 &lt;code&gt;Array(length)&lt;/code&gt; 一样，新增的位置此时并没有包含任何实际的元素，不能理所当然地认为它包含 &lt;code&gt;arrayLength&lt;/code&gt; 个值为 &lt;code&gt;undefined&lt;/code&gt; 的元素，但是如果输出）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let arr = [1, 2, 3]

arr.length = 5

console.log(arr, arr[3], arr[4]) //[ 1, 2, 3, &amp;#x3C;2 empty items&gt; ] undefined undefined

let b = arr.map((e) =&gt; e)
console.log(b) //[ 1, 2, 3, &amp;#x3C;2 empty items&gt; ]

arr.forEach((e) =&gt; console.log(e)) //forEach 会跳过空位
//1
//2
//3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Array.length&lt;/code&gt; 的属性特性为&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;writable：true&lt;/code&gt; 属性值可写。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enumerable：false&lt;/code&gt; 属性不可以通过迭代器 &lt;code&gt;for&lt;/code&gt; 或 &lt;code&gt;for...in&lt;/code&gt; 进行迭代。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;configurable：false&lt;/code&gt; 不可删除或更改属性特性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Array.from&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.from()&lt;/code&gt; 方法从一个类似数组或可迭代对象创建一个新的，浅拷贝的数组实例。稀疏数组的空位会被转为 &lt;code&gt;undefined&lt;/code&gt;。&lt;code&gt;Array.from.length === 1&lt;/code&gt;。当我们有需要将对象转为数组的时候（比如需要使用数组方法而不想使用 &lt;code&gt;apply&lt;/code&gt; 和 &lt;code&gt;call&lt;/code&gt;），&lt;code&gt;Array.from&lt;/code&gt; 是一个不错的选择。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Array.from()&lt;/code&gt; 的第一个参数可以类数组对象（拥有一个 &lt;code&gt;length&lt;/code&gt; 属性和若干索引属性的任意对象，包括字符串）可迭代对象（可以获取对象中的元素,如 &lt;code&gt;Map&lt;/code&gt; 和 &lt;code&gt;Set&lt;/code&gt; 等）包括字符串。我的理解是可以用迭代器 &lt;code&gt;for ... of&lt;/code&gt; 进行迭代的对象都可以。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Array.from()&lt;/code&gt; 可以接受第二，第三个参数，可以用这两个参数在新生成的数组上执行一次 &lt;code&gt;map&lt;/code&gt;，&lt;code&gt;Array.from(obj, mapFn, thisArg)&lt;/code&gt; 就相当于 &lt;code&gt;Array.from(obj).map(mapFn, thisArg)&lt;/code&gt;, 除非创建的不是可用的中间数组。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Array.from(&apos;clloz&apos;) //[&quot;c&quot;, &quot;l&quot;, &quot;l&quot;, &quot;o&quot;, &quot;z&quot;]

//从Set生成数组
const set = new Set([&apos;foo&apos;, &apos;bar&apos;, &apos;baz&apos;, &apos;foo&apos;])
Array.from(set) // [ &quot;foo&quot;, &quot;bar&quot;, &quot;baz&quot; ]

//从Map生成数组
const map = new Map([
  [1, 2],
  [2, 4],
  [4, 8]
])
Array.from(map) // [[1, 2], [2, 4], [4, 8]]

Array.from(map.keys()) //[1, 2, 4]
Array.from(map.values()) //[2, 4, 8]

//从类数组对象生成数组
function f() {
  return Array.from(arguments)
}
f(1, 2, 3) //[ 1, 2, 3 ]

//使用map生成元素值为下标的数组
Array.from({ length: 5 }, (v, i) =&gt; i) // [0, 1, 2, 3, 4]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不考虑 &lt;code&gt;Array.from&lt;/code&gt; 的 &lt;code&gt;map&lt;/code&gt;，&lt;code&gt;Array.from&lt;/code&gt; 做的事情类似如下函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Array.from = function (arrayLike) {
  let k = 0
  len = arrayLike.length
  let result = new Array(len)
  while (k &amp;#x3C; len) {
    result[k] = arrayLike[k]
    k++
  }
  return result
}
let c = Array.from(&apos;clloz&apos;)
console.log(c) // [ &apos;c&apos;, &apos;l&apos;, &apos;l&apos;, &apos;o&apos;, &apos;z&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我这个只是大致演示一下 &lt;code&gt;Array.from&lt;/code&gt; 做的事情，完整的 &lt;code&gt;polyfill&lt;/code&gt; 参考 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/from&quot; title=&quot;Array.from - MDN&quot;&gt;Array.from - MDN&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Array.isArray()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.isArray()&lt;/code&gt; 有几个容易忽略的点: 1. &lt;code&gt;Array.isArray(Array.prototype)&lt;/code&gt; 返回 &lt;code&gt;true&lt;/code&gt;。&lt;code&gt;Array.prototype&lt;/code&gt; 本身也是一个数组，&lt;code&gt;length&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;。 2. &lt;code&gt;Array.isArray()&lt;/code&gt; 能检测 &lt;code&gt;iframes&lt;/code&gt; 中的 &lt;code&gt;Array&lt;/code&gt;，&lt;code&gt;instanceof&lt;/code&gt; 则不能。 3. &lt;code&gt;Array.isArray()&lt;/code&gt; 的 &lt;code&gt;polyfill&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (!Array.isArray) {
  Array.isArray = function (arg) {
    return Object.prototype.toString.call(arg) === &apos;[object Array]&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.of()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.of()&lt;/code&gt; 就是没有 &lt;code&gt;Array(length)&lt;/code&gt; 形式的 &lt;code&gt;Array&lt;/code&gt;。也就是说他只支持 &lt;code&gt;Array.of(el1, el2, ...)&lt;/code&gt; 这种形式。它是 &lt;code&gt;ES6&lt;/code&gt; 中的新方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//polyfill
if (!Array.of) {
  Array.of = function () {
    return Array.prototype.slice.call(arguments)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.prototype.concat()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.concat()&lt;/code&gt; 需要注意的一点就是它是浅拷贝，当我们的数组中有对象或者嵌套数组的时候，是无法进行深拷贝的。细节看下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = [[1], 2]
let b = [[3], 4, { name: &apos;clloz&apos; }]

let c = a.concat(b)
console.log(c) //[ [ 1 ], 2, [ 3 ], 4, { name: &apos;clloz&apos; } ]
b[2].name = &apos;clloz1992&apos;
console.log(c)
a[0].push(100) //[ [ 1 ], 2, [ 3 ], 4, { name: &apos;clloz1992&apos; } ]
console.log(c) //[ [ 1, 100 ], 2, [ 3 ], 4, { name: &apos;clloz1992&apos; } ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;concat&lt;/code&gt; 除了接受数组作为参数，也可以接受其他值，如果值是引用类型则将引用添加到新的数组中，如果是值类型则将值添加到新数组中（&lt;code&gt;String&lt;/code&gt;，&lt;code&gt;Number&lt;/code&gt; 或者 &lt;code&gt;Boolean&lt;/code&gt;，非包装类型）。&lt;/p&gt;
&lt;p&gt;我自己实现的一个简单的 &lt;code&gt;concat&lt;/code&gt; 的 &lt;code&gt;polyfill&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Array.prototype._concat = function () {
  if (Object.prototype.toString.call(this) !== &apos;[object Array]&apos;) throw Error(&apos;this is not a Array!&apos;)
  let result = Array.prototype.slice.call(this)
  let args = Array.prototype.slice.call(arguments)

  for (let i = 0; i &amp;#x3C; args.length; i++) {
    if (Array.isArray(args[i])) {
      for (let j = 0; j &amp;#x3C; args[i].length; j++) {
        result.push(args[i][j])
      }
    }
  }
  return result
}
let a = [1, 2, 3, 4]
let b = [5, 6, 7]
let c = [8, 9, 10]
let d = a._concat(b, c)
console.log(d) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.prototype.every()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.every()&lt;/code&gt; 是 &lt;code&gt;ES5&lt;/code&gt; 添加的数组方法，下面列出 &lt;code&gt;every&lt;/code&gt; 的几个注意点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;every&lt;/code&gt; 和数学中的&quot;所有&quot;类似，当所有的元素都符合条件才会返回 &lt;code&gt;true&lt;/code&gt;。正因如此，若传入一个空数组，无论如何都会返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;没有传入 &lt;code&gt;this&lt;/code&gt; 值，&lt;code&gt;callback&lt;/code&gt; 中的 &lt;code&gt;this&lt;/code&gt; 在非严格模式下是全局对象，严格模式下为 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;只要任何一次 &lt;code&gt;callback&lt;/code&gt; 的执行返回 &lt;code&gt;false&lt;/code&gt;，将直接返回 &lt;code&gt;false&lt;/code&gt;（后面的元素讲不会再执行回调函数）。否则会执行到最后一个元素，返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;不改变原数组。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;every&lt;/code&gt; 遍历的元素范围在第一次调用 &lt;code&gt;callback&lt;/code&gt; 之前就已确定了。在调用 &lt;code&gt;every&lt;/code&gt; 之后添加到数组中的元素不会被 &lt;code&gt;callback&lt;/code&gt; 访问到。如果数组中存在的元素被更改，则他们传入 &lt;code&gt;callback&lt;/code&gt; 的值是 &lt;code&gt;every&lt;/code&gt; 访问到他们那一刻的值。那些被删除的元素或从来未被赋值的元素将不会被访问到（所以对于稀疏数组中的空位 &lt;code&gt;every&lt;/code&gt; 是不会处理的）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;polyfill&lt;/code&gt; 实现参考 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/every&quot; title=&quot;Array.prototype.every() - MDN&quot;&gt;Array.prototype.every() - MDN&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Array.prototype.fill()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.fill()&lt;/code&gt; 是 &lt;code&gt;ES6&lt;/code&gt; 添加的数组方法，接受三个参数 &lt;code&gt;value，start 和 end&lt;/code&gt;，&lt;code&gt;start&lt;/code&gt; 和 &lt;code&gt;end&lt;/code&gt; 的默认值分别是 &lt;code&gt;0，this.length&lt;/code&gt;，如果传入的值是个负数, 则开始索引会被自动计算成为 &lt;code&gt;length+start/end&lt;/code&gt;，即从后往前数第 &lt;code&gt;start/end&lt;/code&gt; 个，&lt;code&gt;-1&lt;/code&gt; 就是倒数第一个。几个注意点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;会填充稀疏数组的空位 &lt;code&gt;Array(3).fill(4)&lt;/code&gt; 返回 &lt;code&gt;[4,4,4]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;若填充的值是对象，则填充的是对象的引用。&lt;code&gt;let a = Array(3).fill({}); console.log(a[0] === a[1])&lt;/code&gt; 将返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;原数组会改变，返回改变后的数组。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Array.prototype.filter()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.filter()&lt;/code&gt; 是 &lt;code&gt;ES5&lt;/code&gt; 添加的数组方法。&lt;code&gt;filter&lt;/code&gt; 为数组中的每个元素调用一次 &lt;code&gt;callback&lt;/code&gt; 函数，并利用所有使得 &lt;code&gt;callback&lt;/code&gt; 返回 &lt;code&gt;true&lt;/code&gt; 或等价于 &lt;code&gt;true&lt;/code&gt; 的值的元素创建一个新数组。注意点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;filter&lt;/code&gt; 的一个参数 &lt;code&gt;callback&lt;/code&gt; 接受三个参数，&lt;strong&gt;元素值&lt;/strong&gt;，&lt;strong&gt;元素索引&lt;/strong&gt;，&lt;strong&gt;数组本身&lt;/strong&gt;；第二个参数是 &lt;code&gt;callback&lt;/code&gt; 执行时的 &lt;code&gt;this&lt;/code&gt;，若没有传入则在非严格模式下为全局对象，严格模式下为 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;callback&lt;/code&gt; 只会在已经赋值的索引上被调用，对于那些已经被删除或者从未被赋值的索引不会被调用。稀疏数组中的空位不会被处理。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter&lt;/code&gt; 遍历的元素范围在第一次调用 &lt;code&gt;callback&lt;/code&gt; 之前就已经确定了。在调用 &lt;code&gt;filter&lt;/code&gt; 之后被添加到数组中的元素不会被 &lt;code&gt;filter&lt;/code&gt; 遍历到（比如在遍历过程中）。如果已经存在的元素被改变了，则他们传入 &lt;code&gt;callback&lt;/code&gt; 的值是 &lt;code&gt;filter&lt;/code&gt; 遍历到它们那一刻的值。被删除或从来未被赋值的元素不会被遍历到。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Array.prototype.find()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.find()&lt;/code&gt; 是 &lt;code&gt;ES6&lt;/code&gt; 添加的数组方法，参数和注意点和 &lt;code&gt;filter&lt;/code&gt; 类似，不同的是不会跳过稀疏数组的空位，找到立即返回元素值（只找第一个），否则返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Array.prototype.findIndex()&lt;/h2&gt;
&lt;p&gt;和 &lt;code&gt;find&lt;/code&gt; 几乎相同，不过返回值是元素索引，若没有找到则返回 &lt;code&gt;-1&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Array.prototype.flat()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES2019&lt;/code&gt; 新增的数组扁平化 &lt;code&gt;API&lt;/code&gt;，支持设定深度，不改变原数组，返回扁平化后的新数组。&lt;strong&gt;该方法会移除稀疏数组中的空位&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Array.prototype.flatMap()&lt;/h2&gt;
&lt;p&gt;这个方法相当于先对数组执行 &lt;code&gt;map&lt;/code&gt;，然后对 &lt;code&gt;map&lt;/code&gt; 之后返回的数组执行 &lt;code&gt;flat(1)&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var arr1 = [1, 2, 3, 4]
JSON.stringify(arr1.map((x) =&gt; [[x * 2]])) // &quot;[[[2]],[[4]],[[6]],[[8]]]&quot;
JSON.stringify(arr1.map((x) =&gt; [[x * 2]]).flat(1)) //&quot;[[2],[4],[6],[8]]&quot;
JSON.stringify(arr1.map((x) =&gt; [[x * 2]]).flat(1)) ===
  JSON.stringify(arr1.flatMap((x) =&gt; [[x * 2]])) //true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.prototype.forEach()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;forEach()&lt;/code&gt; 方法按升序为数组中含有效值的每一项执行一次 &lt;code&gt;callback&lt;/code&gt; 函数，稀疏数组上的空位被跳过。&lt;code&gt;forEach&lt;/code&gt; 接受两个参数，第一个是 &lt;code&gt;callback&lt;/code&gt; 回调函数，接受三个参数：当前元素值，当前元素索引，数组对象本身（后两个可选）；第二个是回调函数执行时的 &lt;code&gt;this&lt;/code&gt;。该方法注意点总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;稀疏数组的空位会被跳过。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this&lt;/code&gt; 默认在严格模式下是 &lt;code&gt;undefined&lt;/code&gt;，非严格模式下是全局对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forEach()&lt;/code&gt; 遍历的范围在第一次调用 &lt;code&gt;callback&lt;/code&gt; 前就会确定。调用 &lt;code&gt;forEach&lt;/code&gt; 后添加到数组中的项不会被 &lt;code&gt;callback&lt;/code&gt; 访问到。如果已经存在的值被改变，则传递给 &lt;code&gt;callback&lt;/code&gt; 的值是 &lt;code&gt;forEach()&lt;/code&gt; 遍历到他们那一刻的值。已删除的项不会被遍历到。如果已访问的元素在迭代时被删除了（例如使用 &lt;code&gt;shift()&lt;/code&gt;），之后的元素将被跳过。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forEach()&lt;/code&gt; 为每个数组元素执行一次 &lt;code&gt;callback&lt;/code&gt; 函数，返回值始终是 &lt;code&gt;undefined&lt;/code&gt;，所以不可被链式调用，一般会放在链式调用的最后。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forEach()&lt;/code&gt; 本身不会改变原数组，但是在回调函数中由于我们可以传入原数组和当前元素索引，所以回调函数中可以修改原数组。&lt;/li&gt;
&lt;li&gt;除了抛出异常以外，没有办法中止或跳出 &lt;code&gt;forEach()&lt;/code&gt; 循环。如果你需要中止或跳出循环，&lt;code&gt;forEach()&lt;/code&gt; 方法不是应当使用的工具。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//回调函数改变原数组
let arr = [1, 2, 3]
arr.forEach((v, i, thisArr) =&gt; {
  thisArr[i] *= 2
})
console.log(arr) //[ 2, 4, 6 ]

//迭代过程中修改数组，有元素会被跳过，其他遍历方法类似
var words = [&apos;one&apos;, &apos;two&apos;, &apos;three&apos;, &apos;four&apos;]
words.forEach(function (word) {
  console.log(word)
  if (word === &apos;two&apos;) {
    words.shift()
  }
})
// one
// two
// four
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.prototype.includes()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.includes()&lt;/code&gt; 方法用来判断一个数组是否包含一个指定的值，根据情况，如果包含则返回 &lt;code&gt;true&lt;/code&gt;，否则返回 &lt;code&gt;false&lt;/code&gt;。该方法接受两个参数，一个是要查找的值，另一个可选参数是从哪个索引开始查找。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.includes()&lt;/code&gt; 的注意点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比较字符和字符串区分大小写。&lt;/li&gt;
&lt;li&gt;稀疏数组的空位被当做 &lt;code&gt;undefined&lt;/code&gt; 处理（当你想找的是 &lt;code&gt;undefined&lt;/code&gt; 的时候会返回 &lt;code&gt;true&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;如果第二个参数大于等于数组长度，会直接返回 &lt;code&gt;false&lt;/code&gt;，并且不会进行查找。&lt;/li&gt;
&lt;li&gt;如果第二个参数 &lt;code&gt;fromIndex&lt;/code&gt; 为负值，计算出的索引 &lt;code&gt;array.length + fromIndex&lt;/code&gt; 将作为开始搜索的位置。如果计算出的索引仍然小于 &lt;code&gt;0&lt;/code&gt;，则整个数组都会被搜索。&lt;/li&gt;
&lt;li&gt;不仅可以用于数组，也可以用于类数组对象，需利用 &lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Array.prototype.indexOf()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;indexOf()&lt;/code&gt; 方法返回在数组中可以找到一个给定元素的第一个索引，如果不存在，则返回 &lt;code&gt;-1&lt;/code&gt;。接受两个参数，一个是要查找的值，另一个可选参数是从哪个索引开始查找。该方法和 &lt;code&gt;Array.prototype.includes()&lt;/code&gt; 类似，只不过一个返回的是布尔值，一个返回的是索引。匹配是否和参数相等采用的是 &lt;code&gt;===&lt;/code&gt; 严格相等。&lt;/p&gt;
&lt;h2&gt;Array.prototype.join()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;join()&lt;/code&gt; 方法将一个数组（或一个类数组对象）的所有元素连接成一个字符串并返回这个字符串。如果数组只有一个项目，那么将返回该项目而不使用分隔符。接受一个可选参数作为连接数组元素时的分隔符。返回所有数组元素连接的字符串如果 &lt;code&gt;arr.length === 0&lt;/code&gt;，则返回空字符串。该方法的注意点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果一个元素为 &lt;code&gt;undefined&lt;/code&gt; 或 &lt;code&gt;null&lt;/code&gt;，它会被转换为空字符串。&lt;/li&gt;
&lt;li&gt;如果元素是 &lt;code&gt;Object&lt;/code&gt; 或 &lt;code&gt;Function&lt;/code&gt;，则会调用对应的 &lt;code&gt;toString()&lt;/code&gt; 方法转为字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Number(), String(), Boolean&lt;/code&gt; 包装的对象也都会被当做基本类型处理。&lt;/li&gt;
&lt;li&gt;利用 &lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 可以用在其他可迭代对象上，比如 &lt;code&gt;arguments&lt;/code&gt;，字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = { length: 10, name: &apos;clloz&apos; }
let str = Array.prototype.join.call(a, &apos;,&apos;)
console.log(str) //,,,,,,,,,
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function m() {}
let arr = [
  1,
  2,
  3,
  4,
  { name: &apos;clloz&apos; },
  [2, [3, 4, 5]],
  m,
  Number(10),
  true,
  String(&apos;clloz&apos;),
  Boolean(1)
]
console.log(arr.join(&apos;,&apos;))
//1,2,3,4,[object Object],2,3,4,5,function m() {},10,true,clloz,true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.prototype.keys()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 添加的数组方法.&lt;code&gt;keys()&lt;/code&gt; 方法返回一个包含数组中每个索引键的 &lt;code&gt;Array Iterator&lt;/code&gt; 对象。一个新的 &lt;code&gt;Array&lt;/code&gt; 迭代器对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var arr = [&apos;a&apos;, , &apos;c&apos;]
var sparseKeys = Object.keys(arr)
var denseKeys = [...arr.keys()]
console.log(sparseKeys) // [&apos;0&apos;, &apos;2&apos;]
console.log(denseKeys) // [0, 1, 2]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.prototype.lastIndexOf()&lt;/h2&gt;
&lt;p&gt;理解为反向的 &lt;code&gt;Array.prototype.indexOf()&lt;/code&gt; 即可。&lt;/p&gt;
&lt;h2&gt;Array.prototype.map()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.map()&lt;/code&gt; 是 &lt;code&gt;ES5&lt;/code&gt; 提供的数组方法。&lt;code&gt;map()&lt;/code&gt; 方法创建一个新数组，其结果是该数组中的每个元素是调用一次提供的函数后的返回值。&lt;/p&gt;
&lt;p&gt;和 &lt;code&gt;forEach()&lt;/code&gt; 相同，接受两个参数，第一个是 &lt;code&gt;callback&lt;/code&gt; 回调函数，接受三个参数：当前元素值，当前元素索引，数组对象本身（后两个可选）；第二个是回调函数执行时的 &lt;code&gt;this&lt;/code&gt;。注意点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;稀疏数组的空位会被跳过并被保留在返回的数组中（根据规范中定义的算法，如果被 &lt;code&gt;map&lt;/code&gt; 调用的数组是离散的，新数组将也是离散的保持相同的索引为空）。&lt;/li&gt;
&lt;li&gt;只有在你需要返回数组或需要回调函数返回值的时候再使用 &lt;code&gt;map&lt;/code&gt;，否则应该使用 &lt;code&gt;forEach&lt;/code&gt; 或 &lt;code&gt;for ... of&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;和所有遍历方法一样，在回调中修改数组可能会让一些元素无法执行回调。&lt;/li&gt;
&lt;li&gt;利用 &lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 可以用在其他可迭代对象上，比如 &lt;code&gt;arguments&lt;/code&gt;，字符串。&lt;/li&gt;
&lt;li&gt;注意回调函数的参数个数和顺序，一个经典的问题就是 &lt;code&gt;[&quot;1&quot;, &quot;2&quot;, &quot;3&quot;].map(parseInt);&lt;/code&gt; 返回 &lt;code&gt;[1, NaN, NaN]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;回调函数没有返回值，生成的元素是 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Array.prototype.pop()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES3&lt;/code&gt; 就存在的数组方法。&lt;code&gt;pop()&lt;/code&gt; 方法从数组中删除最后一个元素，并返回该元素的值(当数组为空时返回 &lt;code&gt;undefined&lt;/code&gt;)。此方法更改数组的长度。可应用在类似数组的对象上。&lt;code&gt;pop&lt;/code&gt; 方法根据 &lt;code&gt;length&lt;/code&gt; 属性来确定最后一个元素的位置。如果不包含 &lt;code&gt;length&lt;/code&gt; 属性或 &lt;code&gt;length&lt;/code&gt; 属性不能被转成一个数值，会将 &lt;code&gt;length&lt;/code&gt; 置为 &lt;code&gt;0&lt;/code&gt;，并返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//数组即对象
let a = { length: 10, name: &apos;clloz&apos; }
Array.prototype.pop.call(a)
console.log(a) //{ length: 9 }

//length 为 0，下标 0 处本来就没有元素，不会有任何变化
var obj = {
  2: 3,
  3: 4,
  length: 0,
  pop: Array.prototype.pop
}

obj.pop()
console.log(obj) //{ &apos;2&apos;: 3, &apos;3&apos;: 4, length: 0, pop: [Function: pop] }

//length 为 3，下标为 2 的元素是 `3`，该元素奖杯删除，`length` 减一
var obj = {
  2: 3,
  3: 4,
  length: 0,
  pop: Array.prototype.pop
}

obj.pop()
console.log(obj) //{ &apos;2&apos;: 3, &apos;3&apos;: 4, length: 0, pop: [Function: pop] }

//没有 length 或 length不能转为数字，会添加 length属性，属性值为 0
var obj = {
  2: 3,
  3: 4,
  pop: Array.prototype.pop
}

obj.pop()
console.log(obj) //{ &apos;2&apos;: 3, &apos;3&apos;: 4, pop: [Function: pop], length: 0 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.prototype.push()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES3&lt;/code&gt; 就存在的数组方法。&lt;code&gt;push()&lt;/code&gt; 方法将一个或多个元素添加到数组的末尾，并返回该数组的新长度。&lt;/p&gt;
&lt;p&gt;可以接受多个参数，作为添加到数组末尾的元素，返回值为新的 &lt;code&gt;length&lt;/code&gt; 属性。和 &lt;code&gt;pop&lt;/code&gt; 一样利用 &lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 可以应用到非数组对象上。&lt;/p&gt;
&lt;p&gt;push 方法根据 &lt;code&gt;length&lt;/code&gt; 属性来决定从哪里开始插入给定的值。如果 &lt;code&gt;length&lt;/code&gt; 不能被转成一个数值，则插入的元素索引为 &lt;code&gt;0&lt;/code&gt;，包括 length 不存在时。当 &lt;code&gt;length&lt;/code&gt; 不存在时，将会创建它。这一点非常重要，看下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var obj = {
  2: 3,
  3: 4,
  length: 2,
  splice: Array.prototype.splice,
  push: Array.prototype.push
}

obj.push(1)
obj.push(2)
console.log(obj)

//{
//  &apos;2&apos;: 1,
//  &apos;3&apos;: 2,
//  length: 4,
//  splice: [Function: splice],
//  push: [Function: push]
//}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为对象中的 &lt;code&gt;length&lt;/code&gt; 为 &lt;code&gt;2&lt;/code&gt;，所以从下标 &lt;code&gt;2&lt;/code&gt; 开始插入，并改变 &lt;code&gt;length&lt;/code&gt; 的值，所以最后 &lt;code&gt;1&lt;/code&gt; &lt;code&gt;2&lt;/code&gt; 分别插入了下标 &lt;code&gt;2&lt;/code&gt; 和 &lt;code&gt;3&lt;/code&gt; 的报位置，取代了原来的值 &lt;code&gt;3&lt;/code&gt; 和 &lt;code&gt;4&lt;/code&gt;。而如果没有 &lt;code&gt;length&lt;/code&gt; 则会创建 &lt;code&gt;length&lt;/code&gt;，初始值为 &lt;code&gt;0&lt;/code&gt;，并从 &lt;code&gt;0&lt;/code&gt; 开始插入，最后结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var obj = {
  2: 3,
  3: 4,
  splice: Array.prototype.splice,
  push: Array.prototype.push
}

obj.push(1)
obj.push(2)
console.log(obj)

//{
//  &apos;0&apos;: 1,
//  &apos;1&apos;: 2,
//  &apos;2&apos;: 3,
//  &apos;3&apos;: 4,
//  splice: [Function: splice],
//  push: [Function: push],
//  length: 2
//}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;唯一的原生类数组（&lt;code&gt;array-like&lt;/code&gt;）对象是 &lt;code&gt;String&lt;/code&gt;，它们并不适用该方法，因为字符串是不可改变的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = { length: 10, name: &apos;clloz&apos; }
Array.prototype.push.call(a, 1, 2, 3, 4)
console.log(a)
console.log(a[0])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.prototype.reduce()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.reduce()&lt;/code&gt; 是 &lt;code&gt;ES5&lt;/code&gt; 提供的数组方法。&lt;code&gt;reduce()&lt;/code&gt; 方法对数组中的每个元素执行一个由您提供的 &lt;code&gt;reducer&lt;/code&gt; 函数(升序执行)，将其结果汇总为单个返回值。&lt;/p&gt;
&lt;p&gt;该方法接受两个参数，回调函数 &lt;code&gt;reducer&lt;/code&gt; 和 初始值 &lt;code&gt;initialValue&lt;/code&gt;。&lt;code&gt;reducer&lt;/code&gt; 接受四个参数，&lt;code&gt;accumulator&lt;/code&gt; 为上一次回调函数执行累积的值或者 &lt;code&gt;initialValue&lt;/code&gt;，&lt;code&gt;currentValue&lt;/code&gt; 当前正在处理的元素，&lt;code&gt;index&lt;/code&gt; 当前处理元素的索引，&lt;code&gt;array&lt;/code&gt; 调用 &lt;code&gt;reducer&lt;/code&gt; 的数组，后两个参数可选。&lt;code&gt;initialValue&lt;/code&gt; 作为第一次调用 &lt;code&gt;callback&lt;/code&gt; 函数时的第一个参数的值。 如果没有提供初始值，则将使用数组中的第一个元素。 在没有初始值的空数组上调用 &lt;code&gt;reduce&lt;/code&gt; 将报错。返回值为累积处理的结果。&lt;/p&gt;
&lt;p&gt;回调函数第一次执行时，&lt;code&gt;accumulator&lt;/code&gt; 和 &lt;code&gt;currentValue&lt;/code&gt; 的取值有两种情况：如果调用 &lt;code&gt;reduce()&lt;/code&gt; 时提供了 &lt;code&gt;initialValue&lt;/code&gt;，&lt;code&gt;accumulator&lt;/code&gt; 取值为 &lt;code&gt;initialValue&lt;/code&gt;，&lt;code&gt;currentValue&lt;/code&gt; 取数组中的第一个值；如果没有提供 &lt;code&gt;initialValue&lt;/code&gt;，那么 &lt;code&gt;accumulator&lt;/code&gt; 取数组中的第一个值，&lt;code&gt;currentValue&lt;/code&gt; 取数组中的第二个值。如果没有提供 &lt;code&gt;initialValue&lt;/code&gt;，&lt;code&gt;reduce&lt;/code&gt; 会从索引1的地方开始执行 &lt;code&gt;callback&lt;/code&gt; 方法，跳过第一个索引。如果提供 &lt;code&gt;initialValue&lt;/code&gt;，从索引 &lt;code&gt;0&lt;/code&gt; 开始。&lt;/p&gt;
&lt;p&gt;如果数组为空且没有提供 &lt;code&gt;initialValue&lt;/code&gt;，会抛出 &lt;code&gt;TypeError&lt;/code&gt; 。如果数组仅有一个元素（无论位置如何）并且没有提供 &lt;code&gt;initialValue&lt;/code&gt;， 或者有提供 &lt;code&gt;initialValue&lt;/code&gt; 但是数组为空，那么此唯一值将被返回并且 &lt;code&gt;callback&lt;/code&gt; 不会被执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//reduce 实现 map
if (!Array.prototype.mapUsingReduce) {
  Array.prototype.mapUsingReduce = function (callback, thisArg) {
    return this.reduce(function (mappedArray, currentValue, index, array) {
      mappedArray[index] = callback.call(thisArg, currentValue, index, array)
      return mappedArray
    }, [])
  }
}

;[1, 2, , 3].mapUsingReduce((currentValue, index, array) =&gt; currentValue + index + array.length) // [5, 7, , 10]

//多函数管道 将多个函数当做数组进行 reduce实现特定功能
// Building-blocks to use for composition
const double = (x) =&gt; x + x
const triple = (x) =&gt; 3 * x
const quadruple = (x) =&gt; 4 * x

// Function composition enabling pipe functionality
const pipe =
  (...functions) =&gt;
  (input) =&gt;
    functions.reduce((acc, fn) =&gt; fn(acc), input)

// Composed functions for multiplication of specific values
const multiply6 = pipe(double, triple)
const multiply9 = pipe(triple, triple)
const multiply16 = pipe(quadruple, quadruple)
const multiply24 = pipe(double, triple, quadruple)

// Usage
multiply6(6) // 36
multiply9(9) // 81
multiply16(16) // 256
multiply24(10) // 240
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.prototype.reduceRight()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.reduceRight()&lt;/code&gt; 是 &lt;code&gt;ES5&lt;/code&gt; 提供的数组方法。从右向左进行遍历的 &lt;code&gt;reduce()&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Array.prototype.reverse()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES1&lt;/code&gt; 就存在的数组方法。&lt;code&gt;reverse()&lt;/code&gt; 方法将数组中元素的位置颠倒，并返回该数组。数组的第一个元素会变成最后一个，数组的最后一个元素变成第一个。该方法会改变原数组。利用 &lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 可以用在其他可迭代对象上，比如 &lt;code&gt;arguments&lt;/code&gt;。唯一的原生类数组（&lt;code&gt;array-like&lt;/code&gt;）对象是 &lt;code&gt;String&lt;/code&gt;，它们并不适用该方法，因为字符串是不可改变的。&lt;/p&gt;
&lt;h2&gt;Array.prototype.shift()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES3&lt;/code&gt; 增加的数组方法。&lt;code&gt;shift()&lt;/code&gt; 方法从数组中删除第一个元素，并返回该元素的值。此方法更改数组的长度。该方法移除索引为 &lt;code&gt;0&lt;/code&gt; 的元素(即第一个元素)，并返回被移除的元素，其他元素的索引值随之减 &lt;code&gt;1&lt;/code&gt;。如果 &lt;code&gt;length&lt;/code&gt; 属性的值为 &lt;code&gt;0&lt;/code&gt; (长度为 &lt;code&gt;0&lt;/code&gt;)，则返回 &lt;code&gt;undefined&lt;/code&gt;。该方法能够通过 &lt;code&gt;call&lt;/code&gt; 或 &lt;code&gt;apply&lt;/code&gt; 方法作用于类似数组的对象上，若对象没有 &lt;code&gt;length&lt;/code&gt; 属性，调用该方法不会有其他操作，但是会添加 &lt;code&gt;length&lt;/code&gt; 属性，属性值 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//没有length会给对象添加值为 0 的 length 属性，不执行其他操作
var obj = {
  2: 3,
  3: 4,
  shift: Array.prototype.pop
}

obj.shift()
console.log(obj) //{ &apos;2&apos;: 3, &apos;3&apos;: 4, shift: [Function: pop], length: 0 }

//length 为 0 不进行任何操作，返回undefined
var obj = {
  2: 3,
  3: 4,
  length: 0,
  shift: Array.prototype.pop
}

obj.shift()
console.log(obj) //{ &apos;2&apos;: 3, &apos;3&apos;: 4, length: 0, shift: [Function: pop] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实就是一个反向的 &lt;code&gt;Array.prototype.pop()&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Array.prototype.slice()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;slice()&lt;/code&gt; 方法返回一个新的数组对象，这一对象是一个由 &lt;code&gt;begin&lt;/code&gt; 和 &lt;code&gt;end&lt;/code&gt; 决定的原数组的浅拷贝（包括 &lt;code&gt;begin&lt;/code&gt;，&lt;code&gt;不包括end&lt;/code&gt;）。原始数组不会被改变。&lt;/p&gt;
&lt;p&gt;该方法接受两个可选参数 &lt;code&gt;begin&lt;/code&gt; 和 &lt;code&gt;end&lt;/code&gt;，表示提取元素的索引，&lt;code&gt;[begin, end)&lt;/code&gt;，包括 &lt;code&gt;begin&lt;/code&gt; 不包括 &lt;code&gt;end&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;若没有传入 &lt;code&gt;begin&lt;/code&gt; 则从 &lt;code&gt;0&lt;/code&gt; 开始提取；没有传入 &lt;code&gt;end&lt;/code&gt;，提取到原数组末尾。若 &lt;code&gt;begin&lt;/code&gt; 或 &lt;code&gt;end&lt;/code&gt; 为负，则计算 &lt;code&gt;length + begin&lt;/code&gt;，若计算值在数组的索引范围内则从计算值开始提取，若依然为负则从 &lt;code&gt;0&lt;/code&gt; 开始。如果 &lt;code&gt;begin&lt;/code&gt; 大于原数组的长度，则会返回空数组。如果 &lt;code&gt;end&lt;/code&gt; 大于数组的长度，&lt;code&gt;slice&lt;/code&gt; 也会一直提取到原数组末尾。&lt;/p&gt;
&lt;p&gt;需要注意 &lt;code&gt;slice&lt;/code&gt; 是浅拷贝，如果某个元素是对象引用，在返回的新数组中依然是相同的引用。对于字符串、数字及布尔值来说（不是 &lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Number&lt;/code&gt; 或者 &lt;code&gt;Boolean&lt;/code&gt; 对象），&lt;code&gt;slice&lt;/code&gt; 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值，将不会影响另一个数组。&lt;/p&gt;
&lt;p&gt;可以使用 &lt;code&gt;apply&lt;/code&gt; 或 &lt;code&gt;call&lt;/code&gt; 将一个类数组（&lt;code&gt;Array-like&lt;/code&gt;）对象/集合转换成一个新数组。&lt;/p&gt;
&lt;h2&gt;Array.prototype.some()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;some()&lt;/code&gt; 方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个 &lt;code&gt;Boolean&lt;/code&gt; 类型的值。&lt;/p&gt;
&lt;p&gt;该方法可以类比 &lt;code&gt;Array.prototype.every()&lt;/code&gt;，&lt;code&gt;every()&lt;/code&gt; 是找到一个 &lt;code&gt;callback&lt;/code&gt; 返回值可转换为为 &lt;code&gt;false&lt;/code&gt; 的就立即返回，否则遍历所有元素，最后返回 &lt;code&gt;true&lt;/code&gt;。&lt;code&gt;some()&lt;/code&gt; 是找到一个 &lt;code&gt;callback&lt;/code&gt; 返回值可转换为为 &lt;code&gt;true&lt;/code&gt; 的就立即返回，否则遍历所有元素，最后返回 &lt;code&gt;false&lt;/code&gt;。其他机制基本相同。&lt;/p&gt;
&lt;h2&gt;Array.prototype.sort()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES1&lt;/code&gt; 就提供的数组方法。&lt;code&gt;sort()&lt;/code&gt; 方法用&lt;strong&gt;原地算法&lt;/strong&gt;对数组的元素进行排序，并返回数组。由于它取决于具体实现，因此无法保证排序的时间和空间复杂性。&lt;/p&gt;
&lt;p&gt;该方法接受一个可选的 &lt;code&gt;compareFunction&lt;/code&gt; 函数，如果没有指明该函数，那么元素会按照转换为的字符串的诸个字符的 &lt;code&gt;Unicode&lt;/code&gt; 位点进行排序。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//默认按Unicode进行排序，我们记住 ACSII 的顺序即可
let arr = [3, 15, 8, 29, 102, 22]
console.log(arr.sort())
//[ 102, 15, 22, 29, 3, 8 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果指明了 &lt;code&gt;compareFunction&lt;/code&gt; ，那么数组会按照调用该函数的返回值排序。即 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 是两个将要被比较的元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;compareFunction(a, b)&lt;/code&gt; 小于 &lt;code&gt;0&lt;/code&gt; ，那么 &lt;code&gt;a&lt;/code&gt; 会被排列到 &lt;code&gt;b&lt;/code&gt; 之前；&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;compareFunction(a, b)&lt;/code&gt; 等于 &lt;code&gt;0&lt;/code&gt; ， &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 的相对位置不变。备注： &lt;code&gt;ECMAScript&lt;/code&gt; 标准并不保证这一行为，而且也不是所有浏览器都会遵守（例如 &lt;code&gt;Mozilla&lt;/code&gt; 在 &lt;code&gt;2003&lt;/code&gt; 年之前的版本）；&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;compareFunction(a, b)&lt;/code&gt; 大于 &lt;code&gt;0&lt;/code&gt; ， &lt;code&gt;b&lt;/code&gt; 会被排列到 &lt;code&gt;a&lt;/code&gt; 之前。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;compareFunction(a, b)&lt;/code&gt; 必须总是对相同的输入返回相同的比较结果，否则排序的结果将是不确定的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Array.prototype.splice()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.splice()&lt;/code&gt; 是 &lt;code&gt;ES3&lt;/code&gt; 提供的数组方法。 &lt;code&gt;splice()&lt;/code&gt; 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。&lt;/p&gt;
&lt;p&gt;该方法的前两个参数 &lt;code&gt;start&lt;/code&gt; 和 &lt;code&gt;deleteCount&lt;/code&gt; 表示从哪个索引开始删除多少个元素，删除的元素包含 &lt;code&gt;start&lt;/code&gt; 位置的元素。如果 &lt;code&gt;deleteCount&lt;/code&gt; 被省略了，或者它的值大于等于&lt;code&gt;array.length - start&lt;/code&gt;(也就是说，如果它大于或者等于 &lt;code&gt;start&lt;/code&gt; 之后的所有元素的数量)，那么 &lt;code&gt;start&lt;/code&gt; 之后数组的所有元素都会被删除。如果 &lt;code&gt;deleteCount&lt;/code&gt; 是 &lt;code&gt;0&lt;/code&gt; 或者负数，则不移除元素，这种情况下，至少应添加一个新元素，否则没有意义。&lt;/p&gt;
&lt;p&gt;从第三个参数开始就是要天剑的新元素，添加的位置就是从 &lt;code&gt;start&lt;/code&gt; 开始。如果添加进数组的元素个数不等于被删除的元素个数，数组的长度会发生相应的改变。&lt;/p&gt;
&lt;p&gt;返回值是由被删除的元素组成的一个数组。如果只删除了一个元素，则返回只包含一个元素的数组。如果没有删除元素，则返回空数组。&lt;/p&gt;
&lt;h2&gt;Array.prototype.unshift()&lt;/h2&gt;
&lt;p&gt;类比 &lt;code&gt;Array.prototype.push()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//没有length属性的对象调用会添加值为 0 的length属性
var obj = {
  0: 3,
  1: 4,
  unshift: Array.prototype.unshift
}

obj.unshift()
console.log(obj) //{ &apos;0&apos;: 3, &apos;1&apos;: 4, unshift: [Function: unshift], length: 0 }

//从 length 处插入
var obj = {
  0: 3,
  1: 4,
  length: 0,
  unshift: Array.prototype.unshift
}

obj.unshift(1, 2)
console.log(obj) //{ &apos;0&apos;: 1, &apos;1&apos;: 2, length: 2, unshift: [Function: unshift] }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.prototype.toString()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;toString()&lt;/code&gt; 返回一个字符串，表示指定的数组及其元素。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Array&lt;/code&gt; 对象覆盖了 &lt;code&gt;Object&lt;/code&gt; 的 &lt;code&gt;toString&lt;/code&gt; 方法。对于数组对象，&lt;code&gt;toString&lt;/code&gt; 方法连接数组并返回一个字符串，其中包含用逗号分隔的每个数组元素。&lt;/p&gt;
&lt;p&gt;当一个数组被作为文本值或者进行字符串连接操作时，将会自动调用其 &lt;code&gt;toString&lt;/code&gt; 方法。&lt;/p&gt;
&lt;h2&gt;API 的几点共通点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;很多方法存在对应，比如 &lt;code&gt;some&lt;/code&gt; 和 &lt;code&gt;every&lt;/code&gt;，&lt;code&gt;pop&lt;/code&gt; 和 &lt;code&gt;shift&lt;/code&gt;，&lt;code&gt;push&lt;/code&gt; 和 &lt;code&gt;unshift&lt;/code&gt; 等，他们的机制基本相同。&lt;/li&gt;
&lt;li&gt;几乎所有 &lt;code&gt;callback&lt;/code&gt; 的机制都相同，即在第一次 &lt;code&gt;callback&lt;/code&gt; 执行之前元素遍历范围就已经确定，在 &lt;code&gt;callback&lt;/code&gt; 执行过程中如果对原数组进行改动可能引起部分元素不会被遍历到。&lt;/li&gt;
&lt;li&gt;大多数 &lt;code&gt;API&lt;/code&gt; 都可以使用 &lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 拓展到任何带有 &lt;code&gt;length&lt;/code&gt; 属性的对象（&lt;code&gt;javascript&lt;/code&gt; 的数组本质就是这样一个对象）。部分方法不支持字符串调用，因为字符串不可被更改。&lt;/li&gt;
&lt;li&gt;大多数 &lt;code&gt;callback&lt;/code&gt; 需要传入索引作为参数的方法，机制都相同。当索引为负的时候，会计算 &lt;code&gt;length + index&lt;/code&gt;，若计算值是一个在数组范围内的值，则以这个值执行；若计算值依然是负值，则忽略该参数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pop&lt;/code&gt;, &lt;code&gt;shift&lt;/code&gt;, &lt;code&gt;unshift&lt;/code&gt; 这几个添加删除元素的方法都是以 &lt;code&gt;length&lt;/code&gt; 为基准进行操作，对于没有 &lt;code&gt;length&lt;/code&gt; 的对象会添加一个值为 &lt;code&gt;0&lt;/code&gt; 的 &lt;code&gt;length&lt;/code&gt; 属性。以 &lt;code&gt;length&lt;/code&gt; 为基准的意思举个例子就是，我们执行 &lt;code&gt;push(1,2)&lt;/code&gt;，如果我们的对象 &lt;code&gt;length&lt;/code&gt; 为 &lt;code&gt;2&lt;/code&gt;，那么即使对象中索引 &lt;code&gt;2&lt;/code&gt; 和 &lt;code&gt;3&lt;/code&gt; 已经有元素值，那么也会用 &lt;code&gt;1&lt;/code&gt; 和 &lt;code&gt;2&lt;/code&gt; 进行覆盖。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;查找数组&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.find()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.findIndex()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.includes()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.indexOf()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.lastIndexOf()&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;遍历数组&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.every()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.filter()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.forEach()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.map()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.reduce()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.reduceRgiht()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.some()&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Array Iterator&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.entries()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.keys()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.values&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;复制数组&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.slice.call()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Array.from()&lt;/code&gt;，和 &lt;code&gt;Array.prototype.slice.call()&lt;/code&gt; 的区别就是 &lt;code&gt;Array.from()&lt;/code&gt; 是 &lt;code&gt;ES6&lt;/code&gt; 才有的方法。他们的性能存在差异，我只用了一个简单的数组进行了测试：结果就是当数组长度比较小的时候，&lt;code&gt;Array.from()&lt;/code&gt; 速度更快一点，当数组长度越长，&lt;code&gt;Array.prototype.slice.call()&lt;/code&gt; 速度更快，我只是用的一个 &lt;code&gt;flat&lt;/code&gt; 的数组，没有测试嵌套数组和对象， &lt;code&gt;Set&lt;/code&gt; &lt;code&gt;Map&lt;/code&gt; 等情况。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = Array.from({ length: 1000000 }, (v, i) =&gt; i)
console.time(&apos;slice&apos;)
for (let i = 0; i &amp;#x3C; 1000; i++) {
  Array.prototype.slice.call(a)
}
console.timeEnd(&apos;slice&apos;) //slice: 5995.096ms
console.time(&apos;from&apos;)
for (let i = 0; i &amp;#x3C; 1000; i++) {
  Array.from(a)
}
console.timeEnd(&apos;from&apos;) //from: 6668.785ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建元素值等于下标的数组&lt;/h2&gt;
&lt;p&gt;这个源于知乎上的一道题目，解法有如下几种：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Object.keys(Array.apply(null, {length: 100}))

Array.from(Array(100).keys())

Array.from({length: 100}, (v, i) =&gt; i);

[...Array(100).keys()]

Array.apply(null, Array(100)).map(Function.prototype.call.bind(Number))

//自定义迭代器
Number.prototype[Symbol.iterator] = function() {
    return {
        v: 0,
        e: this,
        next() {
            return {
                value: this.v++,
                done: this.v &gt; this.e
            }
        }
    }
}

[...100]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;需要特别注意，在 &lt;code&gt;JavaScript&lt;/code&gt; 中参数的个数是有上限的，&lt;code&gt;JavaScriptCore&lt;/code&gt; 引擎中有被硬编码的 参数个数上限：&lt;code&gt;65536&lt;/code&gt;。但是实际能接受多少参数取决于当前的系统和浏览器，并不确定。比如我用上面的用 &lt;code&gt;apply&lt;/code&gt; 生成元素值为元素下标的数组，在 &lt;code&gt;safari&lt;/code&gt; 中的上限是 &lt;code&gt;65536&lt;/code&gt;，在 &lt;code&gt;chrome&lt;/code&gt; 中是 &lt;code&gt;125382&lt;/code&gt;。任何用到超大栈空间的行为都有可能出现这个现象，超出限制则会报错 &lt;code&gt;Uncaught RangeError: Maximum call stack size exceeded&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;去重&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;利用 &lt;code&gt;Array.from&lt;/code&gt; 和 &lt;code&gt;Set&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function combine() {
  let arr = [].concat.apply([], arguments) //没有去重复的新数组
  return Array.from(new Set(arr))
}

var m = [1, 2, 2],
  n = [2, 3, 3]
console.log(combine(m, n)) // [1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;reduce&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let myArray = [&apos;a&apos;, &apos;b&apos;, &apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;e&apos;, &apos;e&apos;, &apos;c&apos;, &apos;d&apos;, &apos;d&apos;, &apos;d&apos;, &apos;d&apos;]
let myOrderedArray = myArray.reduce(function (accumulator, currentValue) {
  if (accumulator.indexOf(currentValue) === -1) {
    accumulator.push(currentValue)
  }
  return accumulator
}, [])
console.log(myOrderedArray)

let arr = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4]
let result = arr.sort().reduce((init, current) =&gt; {
  if (init.length === 0 || init[init.length - 1] !== current) {
    init.push(current)
  }
  return init
}, [])
console.log(result) //[1,2,3,4,5]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两层循环比较&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function unique(arr) {
  for (var i = 0; i &amp;#x3C; arr.length; i++) {
    for (var j = i + 1; j &amp;#x3C; arr.length; j++) {
      if (arr[i] == arr[j]) {
        //第一个等同于第二个，splice方法删除第二个
        arr.splice(j, 1)
        j--
      }
    }
  }
  return arr
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;filter + indexOf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function unique(arr) {
  return arr.filter(function (item, index, arr) {
    //当前元素，在原始数组中的第一个索引==当前索引值，否则返回当前元素
    return arr.indexOf(item, 0) === index
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;forEach + includes&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function unique(arr) {
  let result = []
  arr.forEach((v) =&gt; {
    if (!result.includes(v)) result.push(v)
  })
  return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;去重还有很多种实现，不过本质都是一样的，遍历数组比较去除重复（根据利用的 &lt;code&gt;API&lt;/code&gt; 不同可能要创建一个中间数组）。或者利用像 &lt;code&gt;Set&lt;/code&gt; 或者 &lt;code&gt;Object&lt;/code&gt; 的键不可重复的特性。&lt;/p&gt;
&lt;h2&gt;扁平化&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.flat()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;reduce 与 concat&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var flattened = [
  [0, 1],
  [2, 3],
  [4, 5]
].reduce(function (a, b) {
  return a.concat(b)
}, [])
// flattened is [0, 1, 2, 3, 4, 5]

var flattened = [
  [0, 1],
  [2, 3],
  [4, 5]
].reduce((acc, cur) =&gt; acc.concat(cur), [])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;reduce + concat + isArray + recursivity&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 使用 reduce、concat 和递归展开无限多层嵌套的数组
var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]]

function flatDeep(arr, d = 1) {
  return d &gt; 0
    ? arr.reduce((acc, val) =&gt; acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), [])
    : arr.slice()
}

flatDeep(arr1, Infinity)
// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;forEach+isArray+push+recursivity&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// forEach 遍历数组会自动跳过空元素
const eachFlat = (arr = [], depth = 1) =&gt; {
  const result = [] // 缓存递归结果
  // 开始递归
  ;(function flat(arr, depth) {
    // forEach 会自动去除数组空位
    arr.forEach((item) =&gt; {
      // 控制递归深度
      if (Array.isArray(item) &amp;#x26;&amp;#x26; depth &gt; 0) {
        // 递归数组
        flat(item, depth - 1)
      } else {
        // 缓存元素
        result.push(item)
      }
    })
  })(arr, depth)
  // 返回递归结果
  return result
}

// for of 循环不能去除数组空位，需要手动去除
const forFlat = (arr = [], depth = 1) =&gt; {
  const result = []
  ;(function flat(arr, depth) {
    for (let item of arr) {
      if (Array.isArray(item) &amp;#x26;&amp;#x26; depth &gt; 0) {
        flat(item, depth - 1)
      } else {
        // 去除空元素，添加非undefined元素
        item !== void 0 &amp;#x26;&amp;#x26; result.push(item)
      }
    }
  })(arr, depth)
  return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用堆栈stack&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 无递归数组扁平化，使用堆栈
// 注意：深度的控制比较低效，因为需要检查每一个值的深度
// 也可能在 shift / unshift 上进行 w/o 反转，但是末端的数组 OPs 更快
var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]]
function flatten(input) {
  const stack = [...input]
  const res = []
  while (stack.length) {
    // 使用 pop 从 stack 中取出并移除值
    const next = stack.pop()
    if (Array.isArray(next)) {
      // 使用 push 送回内层数组中的元素，不会改动原始输入
      stack.push(...next)
    } else {
      res.push(next)
    }
  }
  // 反转恢复原数组的顺序
  return res.reverse()
}
flatten(arr1) // [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
// 递归版本的反嵌套
function flatten(array) {
  var flattend = []
  ;(function flat(array) {
    array.forEach(function (el) {
      if (Array.isArray(el)) flat(el)
      else flattend.push(el)
    })
  })(array)
  return flattend
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Generator&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function* flatten(array) {
  for (const item of array) {
    if (Array.isArray(item)) {
      yield* flatten(item)
    } else {
      yield item
    }
  }
}

var arr = [1, 2, [3, 4, [5, 6]]]
const flattened = [...flatten(arr)]
// [1, 2, 3, 4, 5, 6]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;计算元素出现的次数&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;reduce&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var names = [&apos;Alice&apos;, &apos;Bob&apos;, &apos;Tiff&apos;, &apos;Bruce&apos;, &apos;Alice&apos;]

var countedNames = names.reduce(function (allNames, name) {
  if (name in allNames) {
    allNames[name]++
  } else {
    allNames[name] = 1
  }
  return allNames
}, {})
// countedNames is:
// { &apos;Alice&apos;: 2, &apos;Bob&apos;: 1, &apos;Tiff&apos;: 1, &apos;Bruce&apos;: 1 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;清除&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;设置数组的 &lt;code&gt;length&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;按属性分类对象&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var people = [
  { name: &apos;Alice&apos;, age: 21 },
  { name: &apos;Max&apos;, age: 20 },
  { name: &apos;Jane&apos;, age: 20 }
]

function groupBy(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {
    var key = obj[property]
    if (!acc[key]) {
      acc[key] = []
    }
    acc[key].push(obj)
    return acc
  }, {})
}

var groupedPeople = groupBy(people, &apos;age&apos;)
// groupedPeople is:
// {
//   20: [
//     { name: &apos;Max&apos;, age: 20 },
//     { name: &apos;Jane&apos;, age: 20 }
//   ],
//   21: [{ name: &apos;Alice&apos;, age: 21 }]
// }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;求两数组交集&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let arr1 = [0, 1, 2, 3, 4, 5, 4, 5]
let arr2 = [4, 5, 6, 7, 8, 9, 0]
let result = Array.from(new Set(arr1.filter((v) =&gt; arr2.includes(v))))
console.log(result) //[ 0, 4, 5 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844904050152964109&quot; title=&quot;JS 中的稀疏数组和密集数组&quot;&gt;JS 中的稀疏数组和密集数组&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/41493194/answer/91224565&quot; title=&quot;如何不使用loop循环，创建一个长度为100的数组，并且每个元素的值等于它的下标？ - Gaubee的回答 - 知乎 &quot;&gt;如何不使用loop循环，创建一个长度为100的数组，并且每个元素的值等于它的下标？ - Gaubee的回答 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>模拟实现call，apply 和 bind</title><link>https://clloz.com/blog/simulation-of-call-apply-bind</link><guid isPermaLink="true">https://clloz.com/blog/simulation-of-call-apply-bind</guid><pubDate>Wed, 07 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;call&lt;/code&gt;，&lt;code&gt;apply&lt;/code&gt; 和 &lt;code&gt;bind&lt;/code&gt; 是 &lt;code&gt;Function.prototype&lt;/code&gt; 上的三个方法，他们能让我们指定函数执行的上下文和参数。关于他们的区别，可以参考我的另一篇文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/03/apply-call-bind/&quot; title=&quot;apply和call, bing方法的应用&quot;&gt;apply和call, bing方法的应用&lt;/a&gt;。为了加深对他们的理解，就动手实现一下模拟的 &lt;code&gt;call&lt;/code&gt;，&lt;code&gt;apply&lt;/code&gt; 和 &lt;code&gt;bind&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;模拟 call&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 的主要区别就是参数的形式，本质并没有不同，我们实现了其中一个另一个也就解决了，这里我们详细说一下 &lt;code&gt;call&lt;/code&gt; 实现的过程。&lt;/p&gt;
&lt;p&gt;我们先来看看 &lt;code&gt;ES5&lt;/code&gt; 标准中对 &lt;code&gt;call&lt;/code&gt; 的定义：当以 &lt;code&gt;thisArg&lt;/code&gt; 和可选的 &lt;code&gt;arg1, arg2&lt;/code&gt; 等等作为参数在一个 &lt;code&gt;func&lt;/code&gt; 对象上调用 &lt;code&gt;call&lt;/code&gt; 方法，采用如下步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;IsCallable(func)&lt;/code&gt; 是 &lt;code&gt;false&lt;/code&gt;, 则抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 异常。&lt;/li&gt;
&lt;li&gt;令 &lt;code&gt;argList&lt;/code&gt; 为一个空列表。&lt;/li&gt;
&lt;li&gt;如果调用这个方法的参数多于一个，则从 &lt;code&gt;arg1&lt;/code&gt; 开始以从左到右的顺序将每个参数插入为 &lt;code&gt;argList&lt;/code&gt; 的最后一个元素。&lt;/li&gt;
&lt;li&gt;提供 &lt;code&gt;thisArg&lt;/code&gt; 作为 &lt;code&gt;this&lt;/code&gt; 值并以 &lt;code&gt;argList&lt;/code&gt; 作为参数列表，调用 &lt;code&gt;func&lt;/code&gt; 的 &lt;code&gt;[[Call]]&lt;/code&gt;内部方法，返回结果。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;call&lt;/code&gt; 方法的 &lt;code&gt;length&lt;/code&gt; 属性是 &lt;code&gt;1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;在外面传入的 &lt;code&gt;thisArg&lt;/code&gt; 值会修改并成为 &lt;code&gt;this&lt;/code&gt; 值。&lt;code&gt;thisArg&lt;/code&gt; 是 &lt;code&gt;undefined&lt;/code&gt; 或 &lt;code&gt;null&lt;/code&gt; 时它会被替换成全局对象，所有其他值会被应用 &lt;code&gt;ToObject&lt;/code&gt; 并将结果作为 &lt;code&gt;this&lt;/code&gt; 值，这是第三版引入的更改。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以我们要做的事情很简单，就是将 &lt;code&gt;call&lt;/code&gt; 的第一个参数作为函数执行的 &lt;code&gt;this&lt;/code&gt;，&lt;code&gt;call&lt;/code&gt; 的后面其他参数作为函数执行的参数执行函数即可。&lt;/p&gt;
&lt;p&gt;改变函数的 &lt;code&gt;this&lt;/code&gt; 就需要改变函数的调用方式，直接调用的话 &lt;code&gt;this&lt;/code&gt; 指向的是全局对象。我们很容易想到的就是将函数作为一个方法添加到 &lt;code&gt;thisArg&lt;/code&gt; 上。但是这样 &lt;code&gt;thisArg&lt;/code&gt; 上就多了一个属性，改变了 &lt;code&gt;thisArg&lt;/code&gt;，所以我们要在函数调用完之后用 &lt;code&gt;delete&lt;/code&gt; 删除这个属性。这样处理虽然还是能在函数中的 &lt;code&gt;this&lt;/code&gt; 中看到我们添加的属性（因为我们删除是在函数调用之后），和 &lt;strong&gt;原版&lt;/strong&gt; 的 &lt;code&gt;call&lt;/code&gt; 不一样，不过目前我能想到的只有这么解决。我们可以用 &lt;code&gt;Symbol&lt;/code&gt; 来让这个新添加的方法不可访问，不过 &lt;code&gt;Symbol&lt;/code&gt; 是 &lt;code&gt;ES6&lt;/code&gt; 的新特性，而 &lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 都是在 &lt;code&gt;es3&lt;/code&gt; 就支持的方法，所以这里我们也可以用 &lt;code&gt;Math.random()&lt;/code&gt; 生成一串随机数作为键名，或者用 &lt;code&gt;new Date().getTime()&lt;/code&gt; 生成时间戳也可以，这样做的另一个原因就是防止和 &lt;code&gt;thisArg&lt;/code&gt; 中原有的属性名冲突 :joy:。下面的代码可以看出两者的区别。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//this is a testing javascript file

Function.prototype._call = function (thisArg) {
  thisArg.func = this
  thisArg.func()
  delete thisArg.func
}

let obj1 = {}
let obj2 = {}

function iscalled() {
  console.log(Object.getOwnPropertyNames(this))
}

iscalled._call(obj1) //[&apos;func&apos;]
iscalled.call(obj2) //[]
console.log(Object.getOwnPropertyNames(obj1)) //[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 &lt;code&gt;thisArg&lt;/code&gt; 后面的参数作为函数调用的参数我们可以用一个数组将所有的参数 &lt;code&gt;push&lt;/code&gt; 进去。但是执行的时候如何调用这个数组作为参数呢。我们比较容易想到用 &lt;code&gt;es6&lt;/code&gt; 的扩展运算符 &lt;code&gt;...&lt;/code&gt; 这样调用 &lt;code&gt;thisArg.func(...argList)&lt;/code&gt;，这样确实能解决问题，但和上面的 &lt;code&gt;Symbol&lt;/code&gt; 一样，扩展运算符是一个 &lt;code&gt;ES6&lt;/code&gt; 的特性，我们想要模拟实现一个 &lt;code&gt;es3&lt;/code&gt; 的方法。所以这里我们可以用拼接字符串然后用 &lt;code&gt;eval&lt;/code&gt; 调用的方式来执行代码。最后的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Function.prototype._call = function (thisArg) {
    //判断this是否是函数
    if (typeof this !== &apos;function&apos;) {
        throw new TypeError(this + &apos; is not a function&apos;);
    }

    //thisArg 为 undefined 或者 null 则转为全局对象
    if (thisArg === void(0) || thisArg === null) {
        thisArg = window;
    } else {
        //thisArg 不是对象为其包装
        thisArg = new Object(thisArg)
    }
    console.log(thisArg)

    let argList = [];
    const FUNC = Symbol(&apos;func&apos;);
    thisArg[FUNC] = this;

    for (let i = 1; i &amp;#x3C; arguments.length; i++) {
        //es6
        //argList.push(arguments[i])

        //es3
        argList.push(&apos;arguments[&apos; + i + &apos;]&apos;);
    }
    //es6
    //let result = thisArg[FUNC](...argList);

    //es3
    let result = eval(&apos;thisArg[FUNC](&apos; + argList + &apos;)&apos;); //这里会调用 `Array.prototype.toString()` 进行argList的类型转换
    delete thisArg[FUNC];
    return result;
}

// 测试
var value = 1;

var obj = {
    value: 2
}

function beCalled(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

beCalled._call(null); // 1 thisArg: window
beCalled._call(undefined); 1 thisArg: window
beCalled._call(1) // undefined thisArg: Number(1)

console.log(beCalled._call(obj, &apos;clloz&apos;, &apos;28&apos;)); //2 {value: 2, name: &quot;clloz&quot;, age: &quot;28&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对 &lt;code&gt;thisArg&lt;/code&gt; 进行了一些判断，如果是 &lt;code&gt;undefined null&lt;/code&gt; 就转为全局对象（判断 &lt;code&gt;undefined&lt;/code&gt; 最好是使用 &lt;code&gt;void(0)&lt;/code&gt;，因为在非全局作用域 &lt;code&gt;window&lt;/code&gt; 和 &lt;code&gt;undefined&lt;/code&gt; 都是能被修改的），如果不是对象则用 &lt;code&gt;Object()&lt;/code&gt; 进行包装。&lt;/p&gt;
&lt;h2&gt;模拟 apply&lt;/h2&gt;
&lt;p&gt;有了 &lt;code&gt;call&lt;/code&gt; 经验，实现 &lt;code&gt;apply&lt;/code&gt; 就比较简单了，我们只是取参数的方式变化一下即可。我们需要判断一下 &lt;code&gt;apply&lt;/code&gt; 的第二个参数是否是一个可用的数组。具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Function.prototype._apply = function (thisArg, args) {
  //判断this是否是函数
  if (typeof this !== &apos;function&apos;) {
    throw new TypeError(this + &apos; is not a function&apos;)
  }

  //thisArg 为 undefined 或者 null 则转为全局对象
  if (thisArg === void 0 || thisArg === null) {
    thisArg = window
  } else {
    //thisArg 不是对象为其包装
    thisArg = new Object(thisArg)
  }

  const FUNC = Symbol(&apos;func&apos;)
  thisArg[FUNC] = this
  let argList = []
  let result

  if (!argList) {
    result = thisArg[FUNC]()
  } else {
    for (let i = 0; i &amp;#x3C; args.length; i++) {
      argList.push(&apos;args[&apos; + i + &apos;]&apos;)
    }
    result = eval(&apos;thisArg[FUNC](&apos; + argList + &apos;)&apos;)
  }

  delete thisArg[FUNC]
  return result
}

// 测试一下
var value = 1

var obj = {
  value: 2
}

function beCalled(name, age) {
  console.log(this.value)
  return {
    value: this.value,
    name: name,
    age: age
  }
}

beCalled._apply(null) // 2
beCalled._apply(undefined)
beCalled._apply(1)

console.log(beCalled._apply(obj, [&apos;clloz&apos;, &apos;28&apos;]))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模拟 bind&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;bind&lt;/code&gt; 是返回一个指定了 &lt;code&gt;this&lt;/code&gt; 的函数，同时这个函数支持 &lt;code&gt;new&lt;/code&gt; 调用，使用 &lt;code&gt;new&lt;/code&gt; 调用则指定的 &lt;code&gt;this&lt;/code&gt; 不生效。&lt;/p&gt;
&lt;p&gt;在模拟 &lt;code&gt;bind&lt;/code&gt; 之前，我们先看一下 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind&quot; title=&quot;Function.prototype.bind&quot;&gt;Function.prototype.bind&lt;/a&gt; 上的一个例子，这个例子我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/03/apply-call-bind/#bind&quot; title=&quot;apply和call, bing方法的应用&quot;&gt;apply和call, bing方法的应用&lt;/a&gt; 里面也谈过，不过今天看了下自己还不是很透彻就再讲一遍，感觉还是有助于对于本文知识点，包括是函数的理解的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//给 Array.prototype.slice 一个别名，方便调用
var slice = Array.prototype.slice

slice.apply(arguments)

//也可以这样用bind实现
var unboundSlice = Array.prototype.slice
var slice = Function.prototype.apply.bind(unboundSlice)

slice(arguments)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的两段代码实现的都是实现 &lt;code&gt;Array.prototype.slice&lt;/code&gt; 的快捷调用，让我们不用每次都输入一长串字符，直接一个 &lt;code&gt;slice&lt;/code&gt; 就可以了。不过第一种实现，我们需要显示的使用 &lt;code&gt;slice.apply&lt;/code&gt;，第二种实现则直接使用 &lt;code&gt;slice&lt;/code&gt; 即可。这是如何实现的呢？&lt;/p&gt;
&lt;p&gt;首先我们要明白，&lt;code&gt;apply&lt;/code&gt; 本身就是一个函数，它是在 &lt;code&gt;Function.prototype&lt;/code&gt; 上定义的一个函数，所有函数都能调用它。当我们用 &lt;code&gt;func.apply()&lt;/code&gt; 调用 &lt;code&gt;apply&lt;/code&gt; 的时候，本质就是以 &lt;code&gt;func&lt;/code&gt; 作为 &lt;code&gt;this&lt;/code&gt; 调用 &lt;code&gt;apply&lt;/code&gt;。那么第二种实现就是用 &lt;code&gt;Array.prototype.slice&lt;/code&gt; 作为 &lt;code&gt;this&lt;/code&gt; 创建 &lt;code&gt;apply&lt;/code&gt; 的一个绑定函数。当我们调用这个绑定函数的时候就相当于调用 &lt;code&gt;Array.prototype.slice.apply()&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Function.prototype.call.bind(func)&lt;/code&gt; 或者 &lt;code&gt;Function.prototype.apply.bind(func)&lt;/code&gt; 就可以直接理解为返回的绑定函数是 &lt;code&gt;func.call&lt;/code&gt; 或者 &lt;code&gt;func.apply&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当我们需要频繁调用一个指定 &lt;code&gt;this&lt;/code&gt; 的函数，我们可以用 &lt;code&gt;bind&lt;/code&gt; 来实现快捷调用。举个例子子，我们相对类数组对象（比如 &lt;code&gt;arguments&lt;/code&gt;）执行数组方法（比如 &lt;code&gt;slice&lt;/code&gt;），我们一般是 &lt;code&gt;Array.prototype.slice.apply(arguments)&lt;/code&gt;，当我们需要频繁使用这个方法的时候，我们可能会这样 &lt;code&gt;let slice = Array.prototype.slice; slice.apply(arguments);&lt;/code&gt;。如果我们使用 &lt;code&gt;bind&lt;/code&gt;，我们可以直接 &lt;code&gt;slice(arguments)&lt;/code&gt; 这样调用，更方便，具体实现看下面的代码。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;实现 &lt;code&gt;bind&lt;/code&gt; 主要有三个点，返回一个函数，可以预设参数以及生成的绑定函数可以使用 &lt;code&gt;new&lt;/code&gt; 操作符。&lt;/p&gt;
&lt;p&gt;返回函数和预设参数我们可以用 &lt;code&gt;apply&lt;/code&gt; 来实现，大致的效果如下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Function.prototype._bind = function (thisArg) {
  let self = this
  let args = Array.prototype.slice.call(arguments, 1)
  let fBound = function () {
    let bindArgs = Array.prototype.slice.call(arguments)
    return self.apply(thisArg, args.concat(bindArgs))
  }
  return fBound
}

function sum(c, d) {
  return this.a + this.b + c + d
}

let obj = { a: 1, b: 2 }

let t = sum._bind(obj, 3)

console.log(t(4)) //10
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;下面就是要实现 &lt;code&gt;new&lt;/code&gt; 调用。如果你对 &lt;code&gt;new&lt;/code&gt; 操作符不熟悉，可以先看一下 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/29/new-operator/&quot; title=&quot;JavaScript 中的 new 操作符和实现&quot;&gt;JavaScript 中的 new 操作符和实现&lt;/a&gt;。当使用 &lt;code&gt;new&lt;/code&gt; 调用绑定函数，&lt;code&gt;this&lt;/code&gt; 将指向绑定函数的原型，我们要的效果是原型指向的是原函数的 &lt;code&gt;prototype&lt;/code&gt;，那么最直接的想法就是将绑定函数的 &lt;code&gt;prototype&lt;/code&gt; 指向原函数的 &lt;code&gt;prototype&lt;/code&gt; 即可。但是这样做有一个问题就是当我们后面改变绑定函数的 &lt;code&gt;prototype&lt;/code&gt;，原函数的 &lt;code&gt;prototype&lt;/code&gt; 也会被修改，他们指向的是同一个对象。基于这样的原因我们需要在中间加一层。最终的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Function.prototype._bind = function (thisArg) {
  if (typeof this !== &apos;function&apos;) {
    throw new TypeError(&apos;Function.prototype.bind - what is trying to be bound is not callable&apos;)
  }
  let self = this
  let args = Array.prototype.slice.call(arguments, 1)

  let fNOP = function () {}
  let fBound = function () {
    let bindArgs = Array.prototype.slice.call(arguments)
    return self.apply(this instanceof fNOP ? this : thisArg, args.concat(bindArgs))
  }

  fNOP.prototype = this.prototype
  fBound.prototype = new fNOP()

  return fBound
}

function sum(c, d) {
  console.log(this.a, this.b) //undefined undefined
  this.a = c
  this.b = d
}

let obj = { a: 1, b: 2 }

let t = sum._bind(obj, 3)

let m = new t(4, 5)

console.log(m) //{3, 4}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到最后结果使用的参数是 &lt;code&gt;bind&lt;/code&gt; 的时添加的一个参数和 &lt;code&gt;new&lt;/code&gt; 添加的第一个参数，&lt;code&gt;new&lt;/code&gt; 的多余参数被忽略。这也是 &lt;code&gt;bind&lt;/code&gt; 的另一个功能，可以预设参数。而我们也发现 &lt;code&gt;bind&lt;/code&gt; 绑定的 &lt;code&gt;obj&lt;/code&gt; 没有生效，这部分我们是用 &lt;code&gt;instanceof&lt;/code&gt; 判断调用绑定函数时的 &lt;code&gt;this&lt;/code&gt; 来判断的，如果是 &lt;code&gt;new&lt;/code&gt; 调用，那么这个 &lt;code&gt;this&lt;/code&gt; 是 &lt;code&gt;fNOP&lt;/code&gt; 的实例（如果是直接调用，那么这个 &lt;code&gt;this&lt;/code&gt; 会是全局对象，浏览器环境就是 &lt;code&gt;window&lt;/code&gt; 对象）。&lt;/p&gt;
&lt;p&gt;关于原生的 &lt;code&gt;bind&lt;/code&gt; 和我们这个 &lt;code&gt;bind&lt;/code&gt; 还有一个区别就是原生的 &lt;code&gt;bind&lt;/code&gt; 生成的绑定函数的 &lt;code&gt;prototype&lt;/code&gt; 是 &lt;code&gt;undefined&lt;/code&gt;，并且同时 &lt;code&gt;newObj instanceof 绑定函数&lt;/code&gt; 返回时 &lt;code&gt;true&lt;/code&gt;，这是违反我们对 &lt;code&gt;instanceof&lt;/code&gt; 的理解的，我在标准中也没有找到合理的解释。我们这里实现的绑定函数的 &lt;code&gt;prototype&lt;/code&gt; 就是 &lt;code&gt;new fNOP()&lt;/code&gt;，在我们的代码里，&lt;code&gt;t.prototype.__proto__ === sum.prototype&lt;/code&gt; 将返回 &lt;code&gt;true&lt;/code&gt;。关于这一点，在我的另一片文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/03/apply-call-bind/#bind&quot; title=&quot;apply和call, bing方法的应用&quot;&gt;apply和call, bing方法的应用&lt;/a&gt; 的 &lt;code&gt;bind&lt;/code&gt; 章节有更详细的说明&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mqyqingfeng/Blog/issues/11&quot; title=&quot;JavaScript深入之call和apply的模拟实现&quot;&gt;JavaScript深入之call和apply的模拟实现&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mqyqingfeng/Blog/issues/12&quot; title=&quot;JavaScript 深入之 bind 实现&quot;&gt;JavaScript 深入之 bind 实现&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind&quot; title=&quot;Function.prototype.bind - MDN&quot;&gt;Function.prototype.bind - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844903728147857415&quot; title=&quot;面试官问：能否模拟实现JS的call和apply方法&quot;&gt;面试官问：能否模拟实现JS的call和apply方法&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>水平垂直居中方案</title><link>https://clloz.com/blog/center-layout</link><guid isPermaLink="true">https://clloz.com/blog/center-layout</guid><pubDate>Mon, 05 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文总结一些水平和垂直居中的方案。&lt;/p&gt;
&lt;h2&gt;水平居中&lt;/h2&gt;
&lt;h2&gt;行内元素&lt;/h2&gt;
&lt;p&gt;行内元素的水平居中设置父元素的 &lt;code&gt;text-aling: center&lt;/code&gt; 即可。元素的 &lt;code&gt;display&lt;/code&gt; 可以是 &lt;code&gt;inline&lt;/code&gt;，&lt;code&gt;inline-block&lt;/code&gt;，&lt;code&gt;inline-table&lt;/code&gt; 和 &lt;code&gt;inline-flex&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .wrap {
    text-align: center;
    margin-bottom: 30px;
  }
  .wrap &gt; div {
    height: 200px;
    width: 200px;
    background-color: lightblue;
  }
&amp;#x3C;/style&gt;

&amp;#x3C;div class=&quot;wrap&quot;&gt;this is a text.&amp;#x3C;/div&gt;
&amp;#x3C;div class=&quot;wrap&quot;&gt;
  &amp;#x3C;div class=&quot;ib&quot; style=&quot;display: inline-block&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&amp;#x3C;div class=&quot;wrap&quot;&gt;
  &amp;#x3C;div class=&quot;it&quot; style=&quot;display: inline-table&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&amp;#x3C;div class=&quot;wrap&quot;&gt;
  &amp;#x3C;div class=&quot;if&quot; style=&quot;display: inline-flex&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;块元素&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;margin: 0 auto&lt;/code&gt;。&lt;code&gt;margin: 0 auto&lt;/code&gt; 之所以能够进行居中，是在 &lt;a href=&quot;https://www.w3.org/TR/CSS22/visudet.html#blockwidth&quot; title=&quot;CSS2.2&quot;&gt;CSS2.2&lt;/a&gt;中 进行了规定的。当元素的宽度不为 &lt;code&gt;auto&lt;/code&gt; 并且 &lt;code&gt;margin-left&lt;/code&gt; 和 &lt;code&gt;margin-right&lt;/code&gt; 都为 &lt;code&gt;auto&lt;/code&gt; 的时候，元素将在水平方向上居中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .center {
    height: 200px;
    width: 200px;
    margin: 0 auto;
    background-color: lightblue;
  }
&amp;#x3C;/style&gt;

&amp;#x3C;div class=&quot;center&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多个元素&lt;/h2&gt;
&lt;p&gt;实现多个元素的水平居中比较常用的方法就是 &lt;code&gt;display: inline-block&lt;/code&gt; 配合 &lt;code&gt;text-align: center&lt;/code&gt; 来实现。&lt;/p&gt;
&lt;h2&gt;水平垂直居中&lt;/h2&gt;
&lt;p&gt;这里介绍的几种垂直居中的方法都可以应用到水平方向上，所以就放到一起来说&lt;/p&gt;
&lt;h2&gt;绝对定位 + 负 margin&lt;/h2&gt;
&lt;p&gt;在知道元素宽高的情况下可以利用这一方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .center {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 200px;
    height: 200px;
    margin: -100px 0 0 -100px;
    background-color: lightblue;
  }
&amp;#x3C;/style&gt;

&amp;#x3C;div class=&quot;center&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;绝对定位 + calc&lt;/h2&gt;
&lt;p&gt;必须确定居中元素的宽高。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .center {
    position: absolute;
    top: calc(50% - 100px);
    left: calc(50% - 100px);
    height: 200px;
    width: 200px;
    background-color: lightblue;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;center&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;绝对定位 + margin&lt;/h2&gt;
&lt;p&gt;元素必须设置宽高，否则元素的宽高都会是 &lt;code&gt;100%&lt;/code&gt;，不过宽高可以设置百分比而不一定非要像素。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .center {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    width: 200px;
    height: 200px;
    margin: auto;
    background-color: lightblue;
  }
&amp;#x3C;/style&gt;

&amp;#x3C;div class=&quot;center&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;绝对定位 + transform&lt;/h2&gt;
&lt;p&gt;这个方法和 &lt;code&gt;绝对定位 + 负margin&lt;/code&gt; 类似，但是我们可以不用固定宽高，因为 &lt;code&gt;transform&lt;/code&gt; 支持百分比。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&amp;#x3C;style&gt;
    .center {
        position: absolute;
        top: 50%;
        left: 50%;
        width: 200px;
        height: 200px;
        transform: translate(-50%, -50%);
        background-color: lightblue;
    }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;center&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;table-cell&lt;/h2&gt;
&lt;p&gt;将父元素的 &lt;code&gt;display&lt;/code&gt; 设为 &lt;code&gt;table-cell&lt;/code&gt;，然后利用 &lt;code&gt;text-align&lt;/code&gt; 和 &lt;code&gt;vertical-align&lt;/code&gt; 来实现元素的居中。不过要居中的元素需要是行内级元素。如果想相对于视口居中，可以在外面再套一层 &lt;code&gt;display: table;&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .container {
    display: table;
    width: 100%;
    height: 500px;
  }
  .wrap {
    display: table-cell;
    height: 500px;
    width: 500px;
    border: 1px solid black;
    text-align: center;
    vertical-align: middle;
  }
  .center {
    display: inline-block;
    width: 200px;
    height: 200px;
    background-color: lightblue;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;container&quot;&gt;
  &amp;#x3C;div class=&quot;wrap&quot;&gt;
    &amp;#x3C;div class=&quot;center&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;flex布局&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 200px;
    border: 1px solid;
  }

  .center {
    height: 100px;
    width: 100px;
    background-color: lightblue;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;container&quot;&gt;
  &amp;#x3C;div class=&quot;center&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;::after 伪元素和 vertical-align&lt;/h2&gt;
&lt;p&gt;可以利用撑满父元素的 &lt;code&gt;::after&lt;/code&gt; 伪元素和 &lt;code&gt;vertical-align&lt;/code&gt; 实现行内元素的居中（不用伪元素，用 &lt;code&gt;line-height&lt;/code&gt; 同样也可以实现，根据需求选择）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .wrap {
    width: 100%;
    height: 600px;
    /* line-height: 600px; */
    text-align: center;
    border: 1px solid;
    font-size: 0;
  }
  .wrap::after {
    content: &apos;&apos;;
    display: inline-block;
    height: 100%;
    width: 1px;
    background-color: red;
    vertical-align: middle;
  }
  .center {
    display: inline-block;
    height: 200px;
    width: 200px;
    vertical-align: middle;
    background-color: lightblue;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;wrap&quot;&gt;
  &amp;#x3C;div class=&quot;center&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/coco1s/p/4444383.html&quot; title=&quot;最全面的水平垂直居中方案&quot;&gt;最全面的水平垂直居中方案&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://liuyib.github.io/2020/04/07/css-h-and-v-center/#table-cell-vertical-align&quot; title=&quot;CSS 拷问：水平垂直居中方法你会几种？&quot;&gt;CSS 拷问：水平垂直居中方法你会几种？&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>JavaScript 中的按位操作符</title><link>https://clloz.com/blog/bitwise-operator</link><guid isPermaLink="true">https://clloz.com/blog/bitwise-operator</guid><pubDate>Sun, 04 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 提供了多种按位操作符，由于不是很了解，我使用频率很低。不过经常看到别人的代码中利用按位操作符简化代码，在一些场景下能够更有效率。本文记录学习按位操作符的笔记。&lt;/p&gt;
&lt;h2&gt;按位操作符&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中的按位操作符见下表：&lt;/p&gt;
&lt;p&gt;| 运算符               | 用法      | 描述                                                                                    |
| -------------------- | --------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| 按位与（ &lt;code&gt;AND&lt;/code&gt;）     | &lt;code&gt;a &amp;#x26; b&lt;/code&gt;   | 对于每一个比特位，只有两个操作数相应的比特位都是 &lt;code&gt;1&lt;/code&gt; 时，结果才为 &lt;code&gt;1&lt;/code&gt;，否则为 &lt;code&gt;0&lt;/code&gt;。     |
| 按位或（&lt;code&gt;OR&lt;/code&gt;）       | &lt;code&gt;a        | b&lt;/code&gt;                                                                                      | 对于每一个比特位，当两个操作数相应的比特位至少有一个 &lt;code&gt;1&lt;/code&gt; 时，结果为 &lt;code&gt;1&lt;/code&gt;，否则为 &lt;code&gt;0&lt;/code&gt;。 |
| 按位异或（&lt;code&gt;XOR&lt;/code&gt;）    | &lt;code&gt;a ^&lt;/code&gt;     | 对于每一个比特位，当两个操作数相应的比特位有且只有一个 &lt;code&gt;1&lt;/code&gt; 时，结果为 &lt;code&gt;1&lt;/code&gt;，否则为 &lt;code&gt;0&lt;/code&gt;。 |
| 按位非（&lt;code&gt;NOT&lt;/code&gt;）      | &lt;code&gt;~ a&lt;/code&gt;     | 反转操作数的比特位，即 &lt;code&gt;0&lt;/code&gt; 变成 &lt;code&gt;1&lt;/code&gt;，&lt;code&gt;1&lt;/code&gt; 变成 &lt;code&gt;0&lt;/code&gt;。                                     |
| 左移（&lt;code&gt;Left shift&lt;/code&gt;） | &lt;code&gt;a &amp;#x3C;&amp;#x3C; b&lt;/code&gt;  | 将 &lt;code&gt;a&lt;/code&gt; 的二进制形式向左移 &lt;code&gt;b (&amp;#x3C; 32)&lt;/code&gt; 比特位，右边用 &lt;code&gt;0&lt;/code&gt; 填充。                          |
| 有符号右移           | &lt;code&gt;a &gt;&gt; b&lt;/code&gt;  | 将 &lt;code&gt;a&lt;/code&gt; 的二进制表示向右移 &lt;code&gt;b (&amp;#x3C; 32)&lt;/code&gt; 位，丢弃被移出的位。                               |
| 无符号右移           | &lt;code&gt;a &gt;&gt;&gt; b&lt;/code&gt; | 将 &lt;code&gt;a&lt;/code&gt; 的二进制表示向右移 &lt;code&gt;b (&amp;#x3C; 32)&lt;/code&gt; 位，丢弃被移出的位，并使用 &lt;code&gt;0&lt;/code&gt; 在左侧填充。        |&lt;/p&gt;
&lt;p&gt;所有的按位操作符的操作数都会被转成补码形式的有符号 &lt;code&gt;32&lt;/code&gt; 位二进制整数。关于补码的知识，可以参考我的另一片文章&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/06/05/true-form-ones-complement-twos-complement/&quot; title=&quot;原码，反码和补码&quot;&gt;原码，反码和补码&lt;/a&gt;。如果操作数是一个非数字，我们需要注意 &lt;code&gt;JavaScript&lt;/code&gt; 会将它转化为什么：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Number(null) //0
Number(undefined) //NaN
Number([]) //0
Number([1, 2, 3]) //NaN
Number(true) //1
Number(false) //0
Number(&apos;123&apos;) //123
Number(&apos;abc&apos;) //NaN
Number(&apos;&apos;) //0
Number(Symbol(123)) //Uncaught TypeError: Cannot convert a Symbol value to a number
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;按位逻辑操作符&lt;/h2&gt;
&lt;p&gt;按位逻辑操作符（按位与 &lt;code&gt;&amp;#x26;&lt;/code&gt;， 按位或 &lt;code&gt;|&lt;/code&gt;，按位异或 &lt;code&gt;^&lt;/code&gt;，按位非 &lt;code&gt;~&lt;/code&gt;）的规则：操作数被转换成 &lt;code&gt;32&lt;/code&gt; 位二进制整数，超过 &lt;code&gt;32&lt;/code&gt; 位的数字会被丢弃。第一个操作数的每个比特位与第二个操作数的相应比特位匹配（匹配规则见下方无序列表）：第一位对应第一位，第二位对应第二位，以此类推。位运算符应用到每对比特位，结果是新的比特值。&lt;/p&gt;
&lt;p&gt;位匹配规则： - 按位与 &lt;code&gt;&amp;#x26;&lt;/code&gt;：对每对比特位执行与（&lt;code&gt;AND&lt;/code&gt;）操作。只有 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 都是 &lt;code&gt;1&lt;/code&gt; 时，&lt;code&gt;a AND b&lt;/code&gt; 才是 &lt;code&gt;1&lt;/code&gt;。将任一数值 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;0&lt;/code&gt; 执行按位与操作，其结果都为 &lt;code&gt;0&lt;/code&gt;。将任一数值 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;-1&lt;/code&gt; 执行按位与操作，其结果都为 &lt;code&gt;x&lt;/code&gt;。 - 按位或 &lt;code&gt;|&lt;/code&gt;：对每一对比特位执行或（&lt;code&gt;OR&lt;/code&gt;）操作。如果 &lt;code&gt;a&lt;/code&gt; 或 &lt;code&gt;b&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt;，则 &lt;code&gt;a OR b&lt;/code&gt; 结果为 &lt;code&gt;1&lt;/code&gt;。将任一数值 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;0&lt;/code&gt; 进行按位或操作，其结果都是 &lt;code&gt;x&lt;/code&gt;。将任一数值 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;-1&lt;/code&gt; 进行按位或操作，其结果都为 &lt;code&gt;-1&lt;/code&gt;。 - 按位异或 &lt;code&gt;^&lt;/code&gt;: 对每一对比特位执行异或（&lt;code&gt;XOR&lt;/code&gt;）操作。当 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 不相同时，&lt;code&gt;a XOR b&lt;/code&gt; 的结果为 &lt;code&gt;1&lt;/code&gt;。将任一数值 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;0&lt;/code&gt; 进行异或操作，其结果为 &lt;code&gt;x&lt;/code&gt;。将任一数值 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;-1&lt;/code&gt; 进行异或操作，其结果为 &lt;code&gt;~x&lt;/code&gt;。 - 对每一个比特位执行非（&lt;code&gt;NOT&lt;/code&gt;）操作。&lt;code&gt;NOT a&lt;/code&gt; 结果为 &lt;code&gt;a&lt;/code&gt; 的反码。对任一数值 &lt;code&gt;x&lt;/code&gt; 进行按位非操作的结果为 &lt;code&gt;-(x + 1)&lt;/code&gt;。比如 &lt;code&gt;~str.indexOf(searchFor)&lt;/code&gt; 可以判断字符 &lt;code&gt;searchFor&lt;/code&gt; 是否在 &lt;code&gt;str&lt;/code&gt; 中出现，当 &lt;code&gt;indexOf&lt;/code&gt; 返回 &lt;code&gt;-1&lt;/code&gt; 时，&lt;code&gt;~str.indexOf(searchFor)&lt;/code&gt; 的结果为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里需要注意，所有的按位操作符都是用 &lt;code&gt;32&lt;/code&gt; 位&lt;strong&gt;有符号二进制补码&lt;/strong&gt;进行计算的，所以上面的位匹配规则中用 &lt;code&gt;-1&lt;/code&gt; 来说明，在补码中 &lt;code&gt;-1&lt;/code&gt; 就是全为 &lt;code&gt;1&lt;/code&gt;。&lt;code&gt;~x&lt;/code&gt; 表示 &lt;code&gt;x&lt;/code&gt; 取反。&lt;code&gt;NaN&lt;/code&gt; 作为操作数的表现和 &lt;code&gt;0&lt;/code&gt; 相同，比如 &lt;code&gt;({}) &amp;#x26; -1&lt;/code&gt; 的结果为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;按位移动操作符&lt;/h2&gt;
&lt;p&gt;按位移动操作符（按位左移 &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt;，有符号按位右移 &lt;code&gt;&gt;&gt;&lt;/code&gt;，无符号按位右移 &lt;code&gt;&gt;&gt;&gt;&lt;/code&gt;）有两个操作数：第一个是要被移动的数字，而第二个是要移动的长度。移动的方向根据操作符的不同而不同。&lt;/p&gt;
&lt;p&gt;按位移动会先将操作数转换为大端字节序顺序(&lt;code&gt;big-endian order&lt;/code&gt;)的 &lt;code&gt;32&lt;/code&gt; 位整数,并返回与左操作数相同类型的结果。右操作数应小于 &lt;code&gt;32&lt;/code&gt; 位，否则只有最低 &lt;code&gt;5&lt;/code&gt; 个字节会被使用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Big-Endian&lt;/code&gt;:高位字节排放在内存的低地址端，低位字节排放在内存的高地址端，又称为&quot;高位编址&quot;，是最直观的字节序。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;按位左移 &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt;：该操作符会将第一个操作数向左移动指定的位数。向左被移出的位被丢弃，右侧用 &lt;code&gt;0&lt;/code&gt; 补充。&lt;code&gt;9 &amp;#x3C;&amp;#x3C; 2&lt;/code&gt; 结果为 &lt;code&gt;36&lt;/code&gt;，即 &lt;code&gt;1001&lt;/code&gt; 变为 &lt;code&gt;100100&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;有符号按位右移 &lt;code&gt;&gt;&gt;&lt;/code&gt;：该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃，拷贝最左侧的位以填充左侧。由于新的最左侧的位总是和以前相同，符号位没有被改变。所以被称作“符号传播”。&lt;code&gt;9&gt;&gt;2&lt;/code&gt; 结果为 &lt;code&gt;2&lt;/code&gt;，即 &lt;code&gt;1001&lt;/code&gt; 变为 &lt;code&gt;0010&lt;/code&gt;；&lt;code&gt;-9&gt;&gt;2&lt;/code&gt; 结果为 &lt;code&gt;-3&lt;/code&gt;，即 &lt;code&gt;11111111111111111111111111110111&lt;/code&gt; 变为 &lt;code&gt;11111111111111111111111111111101&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;无符号按位右移 &lt;code&gt;&gt;&gt;&gt;&lt;/code&gt;：该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃，左侧用 &lt;code&gt;0&lt;/code&gt; 填充。因为符号位变成了 &lt;code&gt;0&lt;/code&gt;，所以结果总是非负的。对于非负数，有符号右移和无符号右移总是返回相同的结果。（即便右移 &lt;code&gt;0&lt;/code&gt; 个比特，结果也是非负的，可以理解为将补码直接当做正数，比如 &lt;code&gt;--1&lt;/code&gt; 的补码是 &lt;code&gt;32&lt;/code&gt; 个 &lt;code&gt;1&lt;/code&gt;，那么 &lt;code&gt;-1 &gt;&gt;&gt;0&lt;/code&gt; 的结果就是 &lt;code&gt;4294967295&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;p&gt;上面介绍了 &lt;code&gt;JavaScript&lt;/code&gt; 中的位运算概念，本节介绍一些比较常见的应用场景。&lt;/p&gt;
&lt;h2&gt;掩码 bitmask&lt;/h2&gt;
&lt;p&gt;所谓掩码其实就是一串二进制数，我们通过 &lt;strong&gt;与/或&lt;/strong&gt; 的操作来读取标志位。主要利用的特性就是任何数与 &lt;code&gt;0&lt;/code&gt; 进行&lt;strong&gt;与&lt;/strong&gt;操作都是 &lt;code&gt;0&lt;/code&gt;，任何数与 &lt;code&gt;1&lt;/code&gt; 进行&lt;strong&gt;或&lt;/strong&gt;操作都是 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;比如我们常见的子网掩码就是一种应用，为了区分我们 &lt;code&gt;IP&lt;/code&gt; 地址中的网络号与主机号，用子网掩码来做 &lt;strong&gt;与&lt;/strong&gt; 操作即可。我们的 &lt;code&gt;IP&lt;/code&gt; 的地址是 &lt;code&gt;4&lt;/code&gt; 字节，子网掩码也是 &lt;code&gt;4&lt;/code&gt; 字节，当我们的子网掩码是 &lt;code&gt;255.255.255.0&lt;/code&gt; 的时候，其实就是 &lt;code&gt;11111111.11111111.11111111.00000000&lt;/code&gt;，当我们的 &lt;code&gt;IP&lt;/code&gt; 地址和子网掩码进行&lt;strong&gt;与&lt;/strong&gt;操作之后，只会留下前面 &lt;code&gt;24&lt;/code&gt; 位，这 &lt;code&gt;24&lt;/code&gt; 位也就是我们的主机号。也就是说，子网掩码就是将网络号对应的位全部设为 &lt;code&gt;1&lt;/code&gt;，而主机号的位设为 &lt;code&gt;0&lt;/code&gt;，可以很方便的知道我们的网络号和主机号。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中也有应用，比如 &lt;code&gt;MouseEvent.buttons&lt;/code&gt; 就是用的掩码的形式来分辨当前按下了哪些鼠标上的键，用六位二进制数作为标志位，&lt;/p&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;4 : 鼠标滚轮或者是中键&lt;/li&gt;
&lt;li&gt;8 : 第四按键 (通常是“浏览器后退”按键)&lt;/li&gt;
&lt;li&gt;16 : 第五按键 (通常是“浏览器前进”)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以利用掩码来进行一些状态的保存和判断，用法主要是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用&lt;strong&gt;或&lt;/strong&gt;运算设置标志位，掩码中为 &lt;code&gt;1&lt;/code&gt; 的位可以设置对应的位，比如 &lt;code&gt;0011&lt;/code&gt; 就可以将最后两位都设置为 &lt;code&gt;1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用&lt;strong&gt;与&lt;/strong&gt;运算清除标志位，利用掩码中的 &lt;code&gt;0&lt;/code&gt; 将目标位置进行重置。比如 &lt;code&gt;0000&lt;/code&gt; 可以将目标的四位全部置为 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用&lt;strong&gt;与&lt;/strong&gt;运算获得目标位置的状态，比如 &lt;code&gt;0010&lt;/code&gt; 与目标进行&lt;strong&gt;与&lt;/strong&gt;运算可以获得目标第三位的状态。&lt;/li&gt;
&lt;li&gt;用&lt;strong&gt;异或&lt;/strong&gt;运算切换标志位状态，掩码中的 &lt;code&gt;1&lt;/code&gt; 能够让目标位置的状态切换。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以用左移运算符进行掩码的自动化创建，无符号右移将掩码转化为布尔值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//布尔值转掩码
function createMask() {
  var nMask = 0,
    nFlag = 0,
    nLen = arguments.length &gt; 32 ? 32 : arguments.length
  for (nFlag; nFlag &amp;#x3C; nLen; nMask |= arguments[nFlag] &amp;#x3C;&amp;#x3C; nFlag++);
  return nMask
}
var mask1 = createMask(true, true, false, true) // 11, 0b1011
var mask2 = createMask(false, false, true) // 4, 0b0100
var mask3 = createMask(true) // 1, 0b0001

//掩码转布尔值
function arrayFromMask(nMask) {
  // nMask 必须介于 -2147483648 和 2147483647 之间，去除符号位还有31位
  if (nMask &gt; 0x7fffffff || nMask &amp;#x3C; -0x80000000) {
    throw new TypeError(&apos;arrayFromMask - out of range&apos;)
  }
  for (
    var nShifted = nMask, aFromMask = [];
    nShifted;
    aFromMask.push(Boolean(nShifted &amp;#x26; 1)), nShifted &gt;&gt;&gt;= 1
  );
  return aFromMask
}

var array1 = arrayFromMask(11) //[true, true, false, true]
var array2 = arrayFromMask(4) //[false, false, true]
var array3 = arrayFromMask(1) //[true]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;乘除&lt;/h2&gt;
&lt;p&gt;我们的右移相当于&lt;strong&gt;除2&lt;/strong&gt;，左移相当于&lt;strong&gt;乘2&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = -10
a &gt;&gt; 1 // -5
a &amp;#x3C;&amp;#x3C; 1 // -20
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;交换两个数的值&lt;/h2&gt;
&lt;p&gt;利用异或的特点，我们可以不创建第三个变量进行两个变量值的交换，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = 100, b = -100;
a ^ = b; //a = a ^ b
b ^ = a; //b = b ^ b ^ a -&gt; a
a ^ = b; // a = a ^ a ^ b -&gt; b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这主要利用的是异或的结合律和交换律，即 &lt;code&gt;a ^ b&lt;/code&gt; 和 &lt;code&gt;b ^ a&lt;/code&gt; 是相等的；以及异或自身的结果为 &lt;code&gt;0&lt;/code&gt;，任何值和 &lt;code&gt;0&lt;/code&gt; 进行异或结果都为自身。&lt;/p&gt;
&lt;h2&gt;判断奇偶&lt;/h2&gt;
&lt;p&gt;二进制数只有最后一位是 &lt;code&gt;0&lt;/code&gt; 就是偶数，最后一位是 &lt;code&gt;1&lt;/code&gt; 就是奇数，我们可以利用这一点进行判断，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (0 === (a &amp;#x26; 1)) {
  console.log(&apos;an even number&apos;)
} else {
  console.log(&apos;an odd number&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;变更符号&lt;/h2&gt;
&lt;p&gt;对于二进制补码，有 &lt;code&gt;X + ~X + 1 = 0&lt;/code&gt;，变形可得 &lt;code&gt;-X = ~X + 1&lt;/code&gt;，所以一个数的相反数就是按位取反再加一。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = 10
console.log(~a + 1) //-10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用这一特性我们也可以求绝对值，只要先判断值的正负即可。利用 &lt;code&gt;a &gt;&gt; 31&lt;/code&gt; 即可判断正负，正数右移 &lt;code&gt;31&lt;/code&gt; 位后结果为 &lt;code&gt;0&lt;/code&gt;，负数右移 &lt;code&gt;31&lt;/code&gt; 位后结果为 &lt;code&gt;-1&lt;/code&gt;，如果是负数则求其相反数。求绝对值还有一个更简化一点的办法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = -10
let i = a &gt;&gt; 31
console.log((a ^ i) - i) //10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要就是利用任何数与 &lt;code&gt;-1&lt;/code&gt; 进行异或运算就是取反。&lt;/p&gt;
&lt;h2&gt;高低位交换&lt;/h2&gt;
&lt;p&gt;比如一个 &lt;code&gt;16位&lt;/code&gt; 无符号整数，我们可以利用位运算进行高八位第八位的交换。&lt;code&gt;a &amp;#x3C;&amp;#x3C; 8 | a &gt;&gt; 8&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;求最小的2的整数次幂约数&lt;/h2&gt;
&lt;p&gt;给定一个数，想求其最小的整数次幂约数可以用 &lt;code&gt;a &amp;#x26; -a&lt;/code&gt;。求最小的 &lt;code&gt;2&lt;/code&gt; 的整数次幂约数其实就是找其二进制表示中从右往左数第一个 &lt;code&gt;1&lt;/code&gt;。利用 &lt;code&gt;-a = ~a + 1&lt;/code&gt; 可以找到这个 &lt;code&gt;1&lt;/code&gt; 的位置。&lt;/p&gt;
&lt;h2&gt;计算二进制数中 &lt;code&gt;1&lt;/code&gt; 的个数&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let count = 0;
let a = 100;
while(a){
  a = a &amp;#x26; (a - 1);
  count++;
}
console.log(count) //3
console.log(100. toString(2)) //1100100
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;小数转整数&lt;/h2&gt;
&lt;p&gt;由于按位操作符的计算都会先将操作数转为 &lt;code&gt;32&lt;/code&gt; 位有符号二进制数，我们可以利用这个来将小数转为整数。比如我们需要随机数的时候 &lt;code&gt;Math.random() * 16 | 0&lt;/code&gt; 就能够直接得到整数。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/38206659/answer/736472332&quot; title=&quot;位运算有什么奇技淫巧？ - 力扣（LeetCode）的回答 - 知乎 &quot;&gt;位运算有什么奇技淫巧？ - 力扣（LeetCode）的回答 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators&quot; title=&quot;按位操作符 - MDN&quot;&gt;按位操作符 - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>实现一个 JS 动画模块</title><link>https://clloz.com/blog/js-animation-module</link><guid isPermaLink="true">https://clloz.com/blog/js-animation-module</guid><pubDate>Sun, 27 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;前端的动画可以用 &lt;code&gt;CSS&lt;/code&gt; 来实现，但是如果我们希望管理多个元素的动画进行，支持暂停和继续。那么我们可以用 &lt;code&gt;JS&lt;/code&gt; 来实现。&lt;/p&gt;
&lt;h2&gt;功能分析&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;CSS&lt;/code&gt; 实现动画是用 &lt;code&gt;keyframe&lt;/code&gt; 定义关键帧，然后用 &lt;code&gt;animation&lt;/code&gt; 属性对关键帧的过渡进行配置，其中比较常用的几个属性是 &lt;code&gt;animation-na&apos;me, animation-delay, animation-duration, animation-timing-function&lt;/code&gt;（详细内容参考&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Animations/Using_CSS_animations&quot; title=&quot;使用 CSS 动画 - MDN&quot;&gt;使用 CSS 动画 - MDN&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;JavaScript&lt;/code&gt; 来实现动画就是根据时间计算出对应时间点的元素样式。用 &lt;code&gt;JavaScript&lt;/code&gt; 的好处是我们能够将动画的逻辑抽象出来，能够同时管理各种需要进行动画元素，并且我们能够对元素的动画进行更精确的控制，精确到帧。&lt;/p&gt;
&lt;p&gt;从分析中我们可以得出，我们用 &lt;code&gt;JavaScript&lt;/code&gt; 实现动画的核心就是对时间的把控，我们要明确每一帧元素应该处于什么样的状态。浏览器中一帧是 &lt;code&gt;16ms&lt;/code&gt; （一秒钟 &lt;code&gt;60&lt;/code&gt; 帧），我们要实现对每一帧的控制可以使用的几个方法是 &lt;code&gt;setInterval&lt;/code&gt;，&lt;code&gt;setTimeout&lt;/code&gt; 和 &lt;code&gt;requestAnimationFrame&lt;/code&gt;。本文我们用 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 来实现。&lt;code&gt;requestAnimationFrame&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt; 参考 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame&quot; title=&quot;window.requestAnimationRequest&quot;&gt;window.requestAnimationRequest&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;动画类&lt;/h2&gt;
&lt;p&gt;首先要设计一个动画类，这个动画类主要是对元素和动画属性的对象进行初始化，同时根据时间计算当前元素的样式。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Animation {
  constructor(obj, prop, startVal, endVal, duration, delay, timeFunc, template) {
    this.obj = obj
    this.prop = prop
    this.startVal = startVal
    this.endVal = endVal
    this.duration = duration
    this.delay = delay
    this.timeFunc = timeFunc
    this.template = template
  }
  trans(time) {
    console.log(time)
    let range = this.endVal - this.startVal
    let progress = this.timeFunc(time / this.duration)
    this.obj[this.prop] = this.template(this.startVal + range * progress)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数中的 &lt;code&gt;obj&lt;/code&gt; 是表示要应用动画的元素，&lt;code&gt;prop&lt;/code&gt; 是要进行动画的属性，&lt;code&gt;timeFunc&lt;/code&gt; 是和 &lt;code&gt;css&lt;/code&gt; 中的 &lt;code&gt;animation-timing-function&lt;/code&gt; 类似，&lt;code&gt;template&lt;/code&gt; 是为了应对不同的 &lt;code&gt;CSS&lt;/code&gt; 属性的不同格式，比如 &lt;code&gt;transform&lt;/code&gt; 属性。&lt;/p&gt;
&lt;h2&gt;timeline类&lt;/h2&gt;
&lt;p&gt;我们要对动画实现 &lt;code&gt;start&lt;/code&gt;，&lt;code&gt;pause&lt;/code&gt;，&lt;code&gt;resume&lt;/code&gt; 等功能，需要一个 &lt;code&gt;timeline&lt;/code&gt; 对时间进行管理。我们的 &lt;code&gt;Animation&lt;/code&gt; 是根据时间计算样式的，这个时间是一个相对时间。比如 &lt;code&gt;pause&lt;/code&gt; 功能，我们可以在用户点击 &lt;code&gt;pause&lt;/code&gt; 按钮后记录时间，然后在用户点击 &lt;code&gt;resume&lt;/code&gt; 按钮后计算出暂停的时间，在返回给 &lt;code&gt;Animation.trans()&lt;/code&gt; 方法的时间中减去这个暂停的时间就能够让元素继续暂停之前的状态进行动画。&lt;/p&gt;
&lt;p&gt;对于不同的元素的动画我们用 &lt;code&gt;Set&lt;/code&gt; 进行管理，当有新的需要动画的元素加入时我们将 &lt;code&gt;new&lt;/code&gt; 的 &lt;code&gt;Animation&lt;/code&gt; 对象存入 &lt;code&gt;Set&lt;/code&gt;，当动画完成在从 &lt;code&gt;Set&lt;/code&gt; 中删除。&lt;/p&gt;
&lt;p&gt;整个的逻辑还是比较简单的，直接看代码吧.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const TICK = Symbol(&apos;tick&apos;)
const TICK_HANDLER = Symbol(&apos;tick handler&apos;)
const ANIMATIONS = Symbol(&apos;animations&apos;)
const MOVETIME = Symbol(&apos;movetime&apos;)
const PAUSE_START = Symbol(&apos;pause start&apos;)
const PAUSE_TIME = Symbol(&apos;pause time&apos;)

export class Timeline {
  constructor() {
    this.state = &apos;inited&apos;
    this[ANIMATIONS] = new Set()
    this[MOVETIME] = new Map()
  }
  start() {
    if (this.state !== &apos;inited&apos;) return
    this.state = &apos;started&apos;
    let startTime = Date.now()
    this[PAUSE_TIME] = 0
    console.log(startTime)
    this[TICK] = () =&gt; {
      let now = Date.now()
      for (let animation of this[ANIMATIONS]) {
        let t
        if (this[MOVETIME].get(animation) &amp;#x3C; startTime) {
          t = now - startTime - this[PAUSE_TIME] - animation.delay
        } else {
          t = now - this[MOVETIME].get(animation) - this[PAUSE_TIME] - animation.delay
        }
        if (t &gt; animation.duration) {
          this[ANIMATIONS].delete(animation)
          t = animation.duration
        }
        if (t &gt; 0) animation.trans(t)
      }
      this[TICK_HANDLER] = requestAnimationFrame(this[TICK])
    }
    this[TICK]()
  }
  pause() {
    if (this.state !== &apos;started&apos;) return
    this.state = &apos;paused&apos;
    this[PAUSE_START] = Date.now()
    cancelAnimationFrame(this[TICK_HANDLER])
  }
  resume() {
    if (this.state !== &apos;paused&apos;) return
    this.state = &apos;started&apos;
    this[PAUSE_TIME] += Date.now() - this[PAUSE_START]
    this[TICK]()
  }
  reset() {
    this.pause()
    this.state = &apos;inited&apos;
    // let startTime = Date.now();
    this[ANIMATIONS] = new Set()
    this[MOVETIME] = new Map()
    this[TICK_HANDLER] = null
    this[PAUSE_START] = 0
  }
  add(animation, startTime) {
    if (arguments.length &amp;#x3C; 2) {
      startTime = Date.now()
    }
    this[ANIMATIONS].add(animation)
    this[MOVETIME].set(animation, startTime)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一些模块中私有的属性，我用 &lt;code&gt;Symbol&lt;/code&gt; 来生成，这样在模块文件外这些属性是不会被访问到的（目前 &lt;code&gt;ES6&lt;/code&gt; 的静态属性支持还不好）。对于 &lt;code&gt;delay&lt;/code&gt; 的处理其实和 &lt;code&gt;pause&lt;/code&gt; 的逻辑也一样，我们记录 &lt;code&gt;start&lt;/code&gt; 开始的时间，只有等到时间超过 &lt;code&gt;delay&lt;/code&gt; 才会调用。注意我们的时间计算是从 &lt;code&gt;start&lt;/code&gt; 开始一直到结束的，所以每次的 &lt;code&gt;pause&lt;/code&gt; 的时间都需要累加到 &lt;code&gt;pause time&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;时间的前进我们利用 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 的回调函数来递归调用我们封装的函数实现。&lt;/p&gt;
&lt;p&gt;最后我们可以为动画加上贝塞尔曲线的支持。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export function cubicBezier(p1x, p1y, p2x, p2y) {
  const ZERO_LIMIT = 1e-6
  const ax = 3 * p1x - 3 * p2x + 1
  const bx = 3 * p2x - 6 * p1x
  const cx = 3 * p1x

  const ay = 3 * p1y - 3 * p2y + 1
  const by = 3 * p2y - 6 * p1y
  const cy = 3 * p1y
  function sampleCurveDerivativeX(t) {
    return (3 * ax * t + 2 * bx) * t + cx
  }
  function sampleCurveX(t) {
    return ((ax * t + bx) * t + cx) * t
  }
  function sampleCurveY(t) {
    return ((ay * t + by) * t + cy) * t
  }
  function solveCurveX(x) {
    var t2 = x
    var derivative
    var x2
    for (let i = 0; i &amp;#x3C; 8; i++) {
      x2 = sampleCurveX(t2) - x
      if (Math.abs(x2) &amp;#x3C; ZERO_LIMIT) {
        return t2
      }
      derivative = sampleCurveDerivativeX(t2)
      if (Math.abs(derivative) &amp;#x3C; ZERO_LIMIT) {
        break
      }
      t2 -= x2 / derivative
    }
    var t1 = 1
    var t0 = 0
    t2 = x
    while (t1 &gt; t0) {
      x2 = sampleCurveX(t2) - x
      if (Math.abs(x2) &amp;#x3C; ZERO_LIMIT) {
        return t2
      }
      if (x2 &gt; 0) {
        t1 = t2
      } else {
        t0 = t2
      }
      t2 = (t1 + t0) / 2
    }
    return t2
  }
  function solve(x) {
    return sampleCurveY(solveCurveX(x))
  }
  return solve
}

export let ease = cubicBezier(0.25, 0.1, 0.25, 1)
export let easeIn = cubicBezier(0.42, 0, 1, 1)
export let easeOut = cubicBezier(0, 0, 0.58, 1)
export let easeInOut = cubicBezier(0.42, 0, 0.58, 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果查看：&lt;a href=&quot;https://cdn.clloz.com/study/js-animation&quot; title=&quot;效果Demo&quot;&gt;效果Demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;代码地址：&lt;a href=&quot;https://github.com/Clloz/Frontend-02-Template/tree/master/week13/animation&quot; title=&quot;Github&quot;&gt;Github&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>一台设备添加多个 Github 账号</title><link>https://clloz.com/blog/ssh-multiple-github-account</link><guid isPermaLink="true">https://clloz.com/blog/ssh-multiple-github-account</guid><pubDate>Fri, 25 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;想要深入学习 &lt;code&gt;Git&lt;/code&gt; 肯定要模拟多人操作同一个仓库，最好的方法就是自己创建两个账号进行模拟。&lt;code&gt;Github&lt;/code&gt; 的账号注册很简单，但是一般我们在一台设备上只配置一个 &lt;code&gt;ssh&lt;/code&gt; 公私钥，多个账号的 &lt;code&gt;ssh&lt;/code&gt; 该如何配置呢。在谷歌上找的文章没有一个讲的特别清楚的，不过经过我的尝试，已经把配置和使用过程搞清楚了，本文和大家分享一下。&lt;/p&gt;
&lt;h2&gt;准备&lt;/h2&gt;
&lt;p&gt;准备工作就是两个 &lt;code&gt;Github&lt;/code&gt; 账号和两对 &lt;code&gt;ssh&lt;/code&gt; 公私钥。&lt;code&gt;Github&lt;/code&gt; 的账号注册就不说了，&lt;code&gt;ssh&lt;/code&gt; 的公私钥的创建可以参考&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/10/02/ssh-rsa/&quot; title=&quot;ssh的简介和使用&quot;&gt;ssh的简介和使用&lt;/a&gt;。这里特别提一下，&lt;code&gt;ssh&lt;/code&gt; 默认创建的公私钥文件名分别是 &lt;code&gt;id_rsa&lt;/code&gt; 和 &lt;code&gt;id_rsa.pub&lt;/code&gt;，为了清楚的区分我们是为 &lt;code&gt;Github&lt;/code&gt; 创建的公私钥我们可以加上 &lt;code&gt;-f location&lt;/code&gt; 参数来指定生成的文件的路径和名字，比如 &lt;code&gt;-f ~/.ssh/github1&lt;/code&gt; 就会生成 &lt;code&gt;github1&lt;/code&gt; 和 &lt;code&gt;github1.pub&lt;/code&gt; 这一对公私钥。&lt;/p&gt;
&lt;p&gt;然后将两个公钥分别放到自己注册的两个 &lt;code&gt;Gihub&lt;/code&gt; 账号的 &lt;code&gt;Settings -&gt; SSH and GPG keys&lt;/code&gt; 中。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注册账号的时候如果 &lt;code&gt;verify&lt;/code&gt; 页面报错 &lt;code&gt;Unable to verify your captcha response&lt;/code&gt;，很可能是连不上 &lt;code&gt;https://octocaptcha.com/&lt;/code&gt;，需要代理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;设置过程&lt;/h2&gt;
&lt;p&gt;现在我们已经生成两对公私钥，并且公钥也配置到对应的账户中去了，下面就是对机器的配置。&lt;/p&gt;
&lt;p&gt;我们通过 &lt;code&gt;ssh&lt;/code&gt; 访问 &lt;code&gt;Github&lt;/code&gt; 的项目。比如 &lt;code&gt;git clone&lt;/code&gt;，&lt;code&gt;Github&lt;/code&gt; 会给我们一个项目链接形如 &lt;code&gt;git@github.com:username/repository_name.git&lt;/code&gt;，这个 &lt;code&gt;git@github.com&lt;/code&gt; 就是我们连接 &lt;code&gt;Github&lt;/code&gt; 的关键。当我们的设备中只有一对默认公私钥 &lt;code&gt;id_rsa&lt;/code&gt; 的时候，&lt;code&gt;ssh&lt;/code&gt; 请求默认就会认为私钥是 &lt;code&gt;id_rsa&lt;/code&gt;，从而进行匹配。&lt;/p&gt;
&lt;p&gt;但是当我们的 &lt;code&gt;.ssh&lt;/code&gt; 文件夹中有两对甚至更多的公私钥的时候，并且我们进行了自定义的命名，此时我们就要对公私钥进行配置，告诉 &lt;code&gt;ssh&lt;/code&gt; 如何寻找对应的私钥。&lt;/p&gt;
&lt;p&gt;配置文件位于 &lt;code&gt;~/.ssh/config&lt;/code&gt;，如何配置看下面的例子。更多 &lt;code&gt;ssh config&lt;/code&gt; 的配置字段参考 &lt;a href=&quot;https://deepzz.com/post/how-to-setup-ssh-config.html&quot; title=&quot;SSH Config 那些你所知道和不知道的事&quot;&gt;SSH Config 那些你所知道和不知道的事&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#Github clloz@outlook.com
host github.com
    hostname github.com
    User Clloz
    IdentityFile /Users/clloz/.ssh/Clloz_Github

#Github clloz1992@gmail.com
host clloz1992
    hostname github.com
    User Clloz1992
    IdentityFile /Users/clloz/.ssh/Clloz1992_Github
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这几个字段意思如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Host&lt;/code&gt;: 我们上面说过 &lt;code&gt;Github&lt;/code&gt; 的链接是 &lt;code&gt;git@github.com&lt;/code&gt;，这个 &lt;code&gt;host&lt;/code&gt; 就是我们自定义的，下面的 &lt;code&gt;hostname&lt;/code&gt; 别名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hostname&lt;/code&gt;：&lt;code&gt;Github&lt;/code&gt; 域名，其实 &lt;code&gt;IP&lt;/code&gt; 也可以，我们和 &lt;code&gt;git&lt;/code&gt; 进行 &lt;code&gt;ssh&lt;/code&gt; 通信的时候，请求从这个地址来。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user&lt;/code&gt;：我们在 &lt;code&gt;Github&lt;/code&gt; 上注册的用户名（好像邮箱也可以）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IdentityFile&lt;/code&gt;：对应的&lt;strong&gt;私钥&lt;/strong&gt;的路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里特别提一下 &lt;code&gt;Host&lt;/code&gt; 这个字段，理论上这个字段可以自定义，但是我建议你常用的那个 &lt;code&gt;GIthub&lt;/code&gt; 账号这个字段就使用 &lt;code&gt;github.com&lt;/code&gt;。首先我们说一下配置生效的原理，所有的 &lt;code&gt;Github&lt;/code&gt; 的仓库的地址默认都是 &lt;code&gt;git@github.com&lt;/code&gt; 开头，无论是来自哪个用户，也就是说 &lt;code&gt;hostname&lt;/code&gt; 都是 &lt;code&gt;github.com&lt;/code&gt;。现在我们在 &lt;code&gt;config&lt;/code&gt; 文件中，为 &lt;code&gt;github.com&lt;/code&gt; 指定了两个别名 &lt;code&gt;allias1&lt;/code&gt; 和 &lt;code&gt;alias2&lt;/code&gt;，现在我们设置 &lt;code&gt;remote&lt;/code&gt; 或者 &lt;code&gt;git clone&lt;/code&gt; 的时候不再是使用 &lt;code&gt;git@github.com&lt;/code&gt;，而是换成 &lt;code&gt;git@alias1&lt;/code&gt; 和 &lt;code&gt;git@alias2&lt;/code&gt;。这样配置以后，每次用 &lt;code&gt;ssh&lt;/code&gt; 通信的时候我们用别名做 &lt;code&gt;host&lt;/code&gt;，而每个别名对应的私钥都在 &lt;code&gt;identityfile&lt;/code&gt; 字段中配置了，自然能够成功的按账号进行区分。简单的说，&lt;strong&gt;就是原本我们的公私钥是按照 &lt;code&gt;hostname&lt;/code&gt; 来匹配的，但是由于 &lt;code&gt;github&lt;/code&gt; 的所有仓库 &lt;code&gt;hostname&lt;/code&gt; 都一样，无法对用户进行区分，我们就用别名来设置 &lt;code&gt;host&lt;/code&gt; 进行区分，别名的作用就类似于用户名&lt;/strong&gt;，&lt;/p&gt;
&lt;p&gt;而由于 &lt;code&gt;Github&lt;/code&gt; 上复制地址的时候默认就是 &lt;code&gt;git@github.com&lt;/code&gt;，如果你将 &lt;code&gt;host&lt;/code&gt; 自定义成其他的，每次 &lt;code&gt;git clone&lt;/code&gt; 都要手动改一下 &lt;code&gt;host&lt;/code&gt;，非常麻烦。如果不改的话将无法对仓库进行任何操作，因为此时 &lt;code&gt;github.com&lt;/code&gt; 这个 &lt;code&gt;host&lt;/code&gt; 在我们本地的 &lt;code&gt;ssh&lt;/code&gt; 中是找不到对应的私钥的。而且如果你的本地本来就有很多原来 &lt;code&gt;clone&lt;/code&gt; 的项目，他们的 &lt;code&gt;host&lt;/code&gt; 都是 &lt;code&gt;github.com&lt;/code&gt;，此时你也不能对他们进行任何操作，&lt;code&gt;git pull&lt;/code&gt; 都不可以，你必须将 &lt;code&gt;remote&lt;/code&gt; 修改为你配置后的 &lt;code&gt;host&lt;/code&gt; 才能正常操作，如果本地仓库非常多的话，这将非常麻烦。关于报错可以参考下面的我将 &lt;code&gt;host&lt;/code&gt; 改为一个自定义的值之后，对原来的 &lt;code&gt;github.com&lt;/code&gt; 的仓库进行 &lt;code&gt;git pull&lt;/code&gt; 报错如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;都设置完成后我们可以用 &lt;code&gt;ssh -T host_alias&lt;/code&gt; 来进行测试，如果 &lt;code&gt;host&lt;/code&gt; 和返回的用户名匹配成功则说明我们的设置生效了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh -T git@clloz1992
#Hi Clloz1992! You&apos;ve successfully authenticated, but GitHub does not provide shell access.

ssh -T git@github.com 
#Hi Clloz! You&apos;ve successfully authenticated, but GitHub does not provide shell access.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;经过上面的配置以后，我们已经能够正常的进行两个 &lt;code&gt;github&lt;/code&gt; 账号的仓库管理了。需要注意的是分清楚当前仓库是属于哪个用户的。比如 &lt;code&gt;account1&lt;/code&gt; 对应的 &lt;code&gt;host&lt;/code&gt; 是 &lt;code&gt;allias1&lt;/code&gt;，&lt;code&gt;accout2&lt;/code&gt; 对应的 &lt;code&gt;host&lt;/code&gt; 是 &lt;code&gt;alias2&lt;/code&gt;，那么你 &lt;code&gt;clone&lt;/code&gt; 或者设置 &lt;code&gt;remote&lt;/code&gt; 的时候地址就要将 &lt;code&gt;github.com&lt;/code&gt; 改为对应的 &lt;code&gt;alias&lt;/code&gt;。如果你在 &lt;code&gt;accout2&lt;/code&gt; 下面创建了一个仓库，然后 &lt;code&gt;clone&lt;/code&gt; 的时候用的是 &lt;code&gt;git clone git@alias1:username/repository_name.git&lt;/code&gt;。那么你会发现，当你进行 &lt;code&gt;push&lt;/code&gt; 的时候会报如下的错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ERROR: Permission to Clloz/git_learning.git denied to Clloz1992.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为这是 &lt;code&gt;alias1&lt;/code&gt; 的仓库，我们自然没有权限对其进行操作。不过我们可以登录 &lt;code&gt;alias1&lt;/code&gt; 的 &lt;code&gt;Github&lt;/code&gt; 账户，在对应的 &lt;code&gt;repository&lt;/code&gt; 的 &lt;code&gt;Settings -&gt; Manage Access&lt;/code&gt; 中将 &lt;code&gt;allias2&lt;/code&gt; 对应的用户添加到 &lt;code&gt;Collaborator&lt;/code&gt; 中，我们就能对这个仓库进行操作了。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;还有一点需要注意的，在 &lt;code&gt;git config --global&lt;/code&gt; 中的 &lt;code&gt;user.name&lt;/code&gt; 和 &lt;code&gt;user.email&lt;/code&gt; 我们可以设置为常用账户的，在 &lt;code&gt;alias2&lt;/code&gt; 的本地仓库中我们可以用 &lt;code&gt;git config --local&lt;/code&gt; 进行单独的设置。我们在 &lt;code&gt;Github&lt;/code&gt; 中看到的 &lt;code&gt;commit&lt;/code&gt; 就是根据 &lt;code&gt;config&lt;/code&gt; 来确定是哪个 &lt;code&gt;github&lt;/code&gt; 用户提交的。比如我在 &lt;code&gt;alias2&lt;/code&gt; 的仓库中设置 &lt;code&gt;config&lt;/code&gt; 的 &lt;code&gt;user.name&lt;/code&gt; 和 &lt;code&gt;user.email&lt;/code&gt; 为 &lt;code&gt;alias1&lt;/code&gt; 对应的用户信息然后进行 &lt;code&gt;commit push&lt;/code&gt;，那么在 &lt;code&gt;github&lt;/code&gt; 上看到的提交就是由 &lt;code&gt;alias1&lt;/code&gt; 对用的用户完成的，&lt;code&gt;contributor&lt;/code&gt; 中也多了 &lt;code&gt;alias1&lt;/code&gt; 对应的用户，虽然我们并没有把他加入到 &lt;code&gt;Collaborator&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;所以如果我们只是要模拟多人提交，我们也不必在本地配置两个 &lt;code&gt;github&lt;/code&gt; 账户的 &lt;code&gt;ssh&lt;/code&gt;，我们可以用同一个账号 &lt;code&gt;clone&lt;/code&gt; 将 &lt;code&gt;repository&lt;/code&gt; 克隆到两个不同的文件夹，然后两个文件夹的 &lt;code&gt;.git/config&lt;/code&gt; 中的 &lt;code&gt;user.name&lt;/code&gt; 和 &lt;code&gt;user.email&lt;/code&gt; 配置成我们对应的 &lt;code&gt;github&lt;/code&gt; 账号的就可以。因为 &lt;code&gt;github&lt;/code&gt; 分辨 &lt;code&gt;commit&lt;/code&gt; 的来源是根据 &lt;code&gt;config&lt;/code&gt; 中的 &lt;code&gt;user&lt;/code&gt; 信息的。&lt;/p&gt;
&lt;p&gt;所以我们总结一下：&lt;code&gt;ssh&lt;/code&gt; 的公私钥只是根据 &lt;code&gt;host&lt;/code&gt; 确定了当前的设备是否有权限访问某个 &lt;code&gt;Github&lt;/code&gt; 下的仓库；而 &lt;code&gt;config&lt;/code&gt; 中的 &lt;code&gt;user&lt;/code&gt; 信息确定了当前在操作仓库的是谁。&lt;/p&gt;
&lt;h2&gt;ssh-agent&lt;/h2&gt;
&lt;p&gt;最后再说一下 &lt;code&gt;ssh-agent&lt;/code&gt;。关于 &lt;code&gt;ssh-agent&lt;/code&gt; 的详细内容还是看&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/10/02/ssh-rsa/&quot; title=&quot;ssh的简介和使用&quot;&gt;ssh的简介和使用&lt;/a&gt;，我这里主要要说的是，如果你为私钥设置了 &lt;code&gt;passphrase&lt;/code&gt;，想要使用 &lt;code&gt;ssh-agent&lt;/code&gt;，那么你&lt;strong&gt;必须把两个账号的私钥都交给 &lt;code&gt;ssh-agent&lt;/code&gt; 来代理&lt;/strong&gt;，否则会出错。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jawil/notes/issues/2&quot; title=&quot;同一台电脑配置多个git账号&quot;&gt;同一台电脑配置多个git账号&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/fanbi/p/7825746.html&quot; title=&quot;一台电脑，两个及多个git账号配置&quot;&gt;一台电脑，两个及多个git账号配置&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://me.chjiyun.com/2017/08/28/%E5%A4%9A%E4%B8%AA%E5%AF%86%E9%92%A5ssh-key%E7%9A%84%E7%94%9F%E6%88%90%E4%B8%8E%E7%AE%A1%E7%90%86/&quot; title=&quot;多个密钥ssh-key的生成与管理&quot;&gt;多个密钥ssh-key的生成与管理&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/github.CXd4_sPJ.png"/><enclosure url="/_astro/github.CXd4_sPJ.png"/></item><item><title>CSS实现视差滚动 Parallax Scrolling</title><link>https://clloz.com/blog/parallax-scrolling</link><guid isPermaLink="true">https://clloz.com/blog/parallax-scrolling</guid><pubDate>Thu, 24 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在博客首页的头部背景图本来是用 &lt;code&gt;JavaScript&lt;/code&gt; 实现的视差滚动，但是觉得性能不是很好。于是尝试用 &lt;code&gt;CSS&lt;/code&gt; 来实现视差滚动的效果。&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;CSS&lt;/code&gt; 实现视差滚动的原理就是利用 &lt;code&gt;3d&lt;/code&gt; 空间的 &lt;code&gt;z&lt;/code&gt; 轴距离产生的近大远小，让元素之间的滚动距离产生差距。&lt;code&gt;perspective&lt;/code&gt; 的属性值确定观察的 &lt;code&gt;z&lt;/code&gt; 轴坐标，比如我们设 &lt;code&gt;perspective: 1px&lt;/code&gt;，最后所有的透视效果都是以和 &lt;code&gt;z = 1px&lt;/code&gt; 的位置观察的效果是相同的。&lt;code&gt;z&lt;/code&gt; 轴的原点就是我们的屏幕，所有的最后显示效果，都是投影在屏幕上的效果。如果以人眼作为比喻的话，&lt;code&gt;perspective&lt;/code&gt; 就是我们的眼睛位置，而屏幕就是视网膜的位置。&lt;/p&gt;
&lt;p&gt;如果我们以 &lt;code&gt;z = -1px&lt;/code&gt; 为观察点，在z &lt;code&gt;z = 0&lt;/code&gt; 和 &lt;code&gt;z= -1px&lt;/code&gt; 的位置放两个相同长度的元素，那么实际的成像效果就后面的元素只有前面的元素的长度的一半。如果我们想要让位于 &lt;code&gt;z = -1px&lt;/code&gt; 位置的元素看上去和 &lt;code&gt;z = 0&lt;/code&gt; 的元素一样大，那么我们就需要将它的边长放大到两倍，可以用 &lt;code&gt;scale(2)&lt;/code&gt; 实现。放大后，从我们的 &lt;code&gt;perspective&lt;/code&gt; 位置看上去就和 &lt;code&gt;z = 0&lt;/code&gt; 的元素是一样的，但是如果我们对屏幕进行滚动，可以理解为我们将我们的观察点沿着 &lt;code&gt;y&lt;/code&gt; 轴上下移动，这个位移对于 &lt;code&gt;z = 0&lt;/code&gt; 和 &lt;code&gt;z = -1px&lt;/code&gt; 的元素是相同的，但是由于 &lt;code&gt;z = -1px&lt;/code&gt; 的元素的边长是 &lt;code&gt;z = 0&lt;/code&gt; 位置元素的两倍，所以视觉上，我们觉得 &lt;code&gt;z = -1px&lt;/code&gt; 的元素的位移好像更短，这就是视差效果。&lt;/p&gt;
&lt;p&gt;详细的研究和推理过程可以参考 &lt;a href=&quot;https://css-tricks.com/tour-performant-responsive-css-site/&quot; title=&quot;Tour of a Performant and Responsive CSS Only Site&quot;&gt;Tour of a Performant and Responsive CSS Only Site&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;有了原理之后，实现就简单了。我们将要进行视差移动的元素放到 &lt;code&gt;-1px&lt;/code&gt; 的位置，同时放大两倍，将 &lt;code&gt;perspective&lt;/code&gt; 设置到 &lt;code&gt;1px&lt;/code&gt; 即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &amp;#x3C;title&gt;parallax&amp;#x3C;/title&gt;
    &amp;#x3C;style&gt;
      html,
      body {
        height: 100%;
        margin: 0;
      }
      .container {
        position: relative;
        height: 100%;
        overflow: auto;
        perspective: 1px;
        perspective-origin: 0 0;
      }
      .bg {
        /* position: absolute; */
        width: 100%;
        height: 600px;
        background-image: url(&apos;bg.jpg&apos;);
        background-size: cover;
        transform-origin: 0 0;
        transform: translateZ(-1px) scale(2);
      }
      .content {
        height: 3000px;
        background: pink;
        margin: -60px 15px 0;
        position: relative;
      }
    &amp;#x3C;/style&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div class=&quot;container&quot;&gt;
      &amp;#x3C;div class=&quot;bg&quot;&gt;&amp;#x3C;/div&gt;
      &amp;#x3C;div class=&quot;content&quot;&gt;&amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Demo&lt;/code&gt; 效果可以查看：&lt;a href=&quot;https://cdn.clloz.com/study/parallax_scroll.html&quot; title=&quot;视差滚动效果demo&quot;&gt;视差滚动效果demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这种 &lt;code&gt;CSS&lt;/code&gt; 实现的视差滚动效果在手机上没法达到效果，手机上可以实现透视的效果，但是滚动的效果出不来。最终我也没有应用到博客上。几种视差滚动的实现方式，包括 &lt;code&gt;js&lt;/code&gt;，&lt;code&gt;background-attachment&lt;/code&gt; 和 透视，都没有一个十分满意的方法，因为性能都不是非常好，而且改动也非常多，最后我干脆就把把这个效果拿掉了，&lt;/p&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>图片轮播</title><link>https://clloz.com/blog/carousel-component</link><guid isPermaLink="true">https://clloz.com/blog/carousel-component</guid><pubDate>Tue, 22 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;用两种方式实现图片轮播，自动轮播和拖动轮播。&lt;/p&gt;
&lt;h2&gt;代码结构&lt;/h2&gt;
&lt;p&gt;由于 &lt;code&gt;img&lt;/code&gt; 是一个可选标签，可以被拖动，我们这里选择用 &lt;code&gt;background-image&lt;/code&gt; 来实现。基本的 &lt;code&gt;DOM&lt;/code&gt; 结构和 &lt;code&gt;CSS&lt;/code&gt; 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .carousel {
    width: 500px;
    height: 280px;
    margin: 30px auto;
    font-size: 0;
    white-space: nowrap;
    overflow: hidden;
  }
  .carousel &gt; div {
    display: inline-block;
    width: 500px;
    height: 280px;
    background-size: contain;
    transition: ease 0.5s;
  }
  .carousel &gt; div:nth-child(1) {
    background-image: url(&apos;https://img.clloz.com/blog/writing/cat1.jpg&apos;);
  }
  .carousel &gt; div:nth-child(2) {
    background-image: url(&apos;https://img.clloz.com/blog/writing/cat2.jpg&apos;);
  }
  .carousel &gt; div:nth-child(3) {
    background-image: url(&apos;https://img.clloz.com/blog/writing/cat3.jpg&apos;);
  }
  .carousel &gt; div:nth-child(4) {
    background-image: url(&apos;https://img.clloz.com/blog/writing/cat4.jpg&apos;);
  }
&amp;#x3C;/style&gt;

&amp;#x3C;div class=&quot;carousel cp1&quot;&gt;
  &amp;#x3C;div&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&amp;#x3C;div class=&quot;carousel cp2&quot;&gt;
  &amp;#x3C;div&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们用 &lt;code&gt;display: inline-block&lt;/code&gt; 和 &lt;code&gt;overflow: hidden&lt;/code&gt; 让图片横向排列并且一次只显示一张。这里需要注意 &lt;code&gt;inline-block&lt;/code&gt; 空格导致的间隙问题，我直接用 &lt;code&gt;font-size: 0&lt;/code&gt; 来解决。&lt;/p&gt;
&lt;h2&gt;自动轮播&lt;/h2&gt;
&lt;p&gt;自动轮播的逻辑是比较简单的，我们其实只要关注两张图片，即当前图片和下一张图片。设每一张图片的编号为 &lt;code&gt;index&lt;/code&gt; （从 &lt;code&gt;0&lt;/code&gt; 开始），每一张图片要显示所对应的 &lt;code&gt;translateX&lt;/code&gt; 的值就是 &lt;code&gt;-(index * 100)%&lt;/code&gt;。所以我们只需要每次移动时将当前图片和下一章图片左移一个单位（即一张图片的宽度），同时将下一次要显示的图片放到当前图片的右边一个单位。如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/carousel.B9KLeJAc_1qt0bC.webp&quot; alt=&quot;carousel&quot; title=&quot;carousel&quot;&gt;&lt;/p&gt;
&lt;p&gt;关键逻辑就是将下次要显示的图片移动到预备位置，并且这个过程要关掉动画效果。最后的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// transform loop settimeout
let el2 = document.querySelector(&apos;.carousel.cp2&apos;)
let currentIndex = 0
setInterval(() =&gt; {
  let children = el2.children
  let nextIndex = (currentIndex + 1) % children.length
  let current = children[currentIndex]
  let next = children[nextIndex]

  next.style.transition = &apos;none&apos;
  next.style.transform = `translateX(${100 - nextIndex * 100}%)`

  setTimeout(() =&gt; {
    next.style.transition = &apos;&apos;
    current.style.transform = `translateX(${-100 - currentIndex * 100}%)`
    next.style.transform = `translateX(${-nextIndex * 100}%)`
    currentIndex = nextIndex
  }, 16)
}, 3000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动画效果的开关我们可以用 &lt;code&gt;element.style.transition&lt;/code&gt; 来控制行内样式的值来达到效果。因为行内样式的优先级最高，当我们设置其值为 &lt;code&gt;none&lt;/code&gt; 会覆盖 &lt;code&gt;style&lt;/code&gt; 标签中的样式。当我们将 &lt;code&gt;element.style.transition&lt;/code&gt; 的值设为 &lt;code&gt;&apos;&apos;&lt;/code&gt;，&lt;code&gt;style&lt;/code&gt; 标签中的对应样式又生效了&lt;/p&gt;
&lt;p&gt;代码中间使用了一个小技巧。就是如果连续对同一个元素进行操作，浏览器会忽略前一个操作，这里我们用一个 &lt;code&gt;setTimeout&lt;/code&gt; 避免浏览器的这种行为，&lt;code&gt;16ms&lt;/code&gt; 为浏览器一帧的时间。&lt;/p&gt;
&lt;p&gt;还有一点就是最后一张图片的下一张应该是第一张，这里我们可以使用简单的模运算来达到效果，模就是元素的个数。&lt;/p&gt;
&lt;h2&gt;拖动轮播&lt;/h2&gt;
&lt;p&gt;拖动轮播的逻辑要比自动复杂一些，因为拖动的情况下我们既可以向左又可以向右进行拖动。并且当拖动结束的时候，显示窗口中会有两张图片，我们要根据面积来判断哪张图片显示，并且要把另一张图片也移动到窗口外。&lt;/p&gt;
&lt;p&gt;首先我们要处理拖动事件，这个逻辑和我在&lt;a href=&quot;https://www.clloz.com/programming/front-end/css/2020/09/18/spin-dice/&quot; title=&quot;拖动旋转 3D 的骰子效果&quot;&gt;拖动旋转 3D 的骰子效果&lt;/a&gt;一文中的逻辑是相同，我们对包含元素，也就是代码中的 &lt;code&gt;.carousel&lt;/code&gt; 绑定一个 &lt;code&gt;mousedown&lt;/code&gt; 事件，在其回调函数中绑定 &lt;code&gt;mousemove&lt;/code&gt; 和 &lt;code&gt;mouseup&lt;/code&gt; 事件。注意后两个事件要绑定到 &lt;code&gt;document&lt;/code&gt; 上，因为我们即使拖动到元素外也是一个完整的 &lt;code&gt;mousemove&lt;/code&gt; 行为，并且绑定到 &lt;code&gt;document&lt;/code&gt; 上即使我们拖出浏览器外也依然能保持触发 &lt;code&gt;mousemove&lt;/code&gt; 事件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mousedown&lt;/code&gt; 事件我们只要做一件事就是记录用户点击的初始位置坐标，用 &lt;code&gt;clientX&lt;/code&gt;(因为轮播是横向的，我们只需要判断 &lt;code&gt;x&lt;/code&gt; 方向的移动距离)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mousemove&lt;/code&gt; 我们要处理图片的移动，这里我们需要将动画效果关闭。以一张图片的的宽度为一个单位，我们只需要知道一共滚过了几个单位，就知道当前显示图片的 &lt;code&gt;index&lt;/code&gt;，然后在计算当前 &lt;code&gt;index&lt;/code&gt; 的前一张和后一张就能够得到连续的效果。&lt;code&gt;mousemove&lt;/code&gt; 回调函数如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let move = (e) =&gt; {
  let x = e.clientX - startX

  //拖动的整数屏
  let current = index - (x - (x % 500)) / 500

  for (let offset of [-1, 0, 1]) {
    //计算可能出现的图片下标
    let pos = current + offset
    pos = (pos + children.length) % children.length //要处理大屏幕拖动小于 -children.length 的情况

    children[pos].style.transition = &apos;none&apos;
    children[pos].style.transform = `translateX(${-pos * 500 + offset * 500 + (x % 500)}px)` //当前，前一个，后一个图片当前位置
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;mouseup&lt;/code&gt; 是这里面逻辑稍微复杂的一个，当我们拖动停止的时候，视口里面会有两张图片（一般情况下）（&lt;code&gt;x&lt;/code&gt; 为 &lt;code&gt;e.clientX - startX&lt;/code&gt;，即从触发 &lt;code&gt;mousedown&lt;/code&gt; 到 &lt;code&gt;mouseup&lt;/code&gt; 水平方向一共移动了多少像素），我们要判断哪张图片所占面积比较大，让这张图片用动画移动到整个视口，而另一张图片用动画移出视口。这里我讲两种实现方式。&lt;/p&gt;
&lt;p&gt;第一种是比较好理解，但是代码量比较大。我们可以用 &lt;code&gt;(x - (x % 500)) / 500&lt;/code&gt; 得出一共滚动了多少个单位，&lt;code&gt;index - (x - (x % 500)) / 500&lt;/code&gt; 这张图片在触发 &lt;code&gt;mouseup&lt;/code&gt; 时必然在视口内，只是我们不确定它是要移出的还是要显示的，我们只需要分情况，用 &lt;code&gt;if&lt;/code&gt; 判断一下 &lt;code&gt;x % 500&lt;/code&gt; 的各种情况。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;x % 500&lt;/code&gt; 的值就是页面滚动完整数个单位后多的部分，这部分如果超过一半的图片宽度，当前图片就要从视口移出；如果小于一半的图片宽度，当前图片就显示到视口中。同时我们还要判断视口中的另一张图片的移动方向，这里就不仅需要判断面积，同时需要判断是向左拖动还是向右拖动的。所以最后我们要 &lt;code&gt;2 x 2&lt;/code&gt; 共四种情况。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let up = (e) =&gt; {
  let x = e.clientX - startX

  //index不变，向下取整，需要分情况，可读性好
  index = (index - (x - (x % 500)) / 500) % children.length
  index = (index + children.length) % children.length
  let base = x % 500

  if (base &gt; 0) {
    if (base &gt; 250) {
      children[index].style.transition = &apos;&apos;
      children[index].style.transform = `translateX(${(-index + 1) * 500}px)`
      let pre = index === 0 ? children.length - 1 : index - 1
      children[pre].style.transition = &apos;&apos;
      children[pre].style.transform = `translateX(${-pre * 500}px)`
      index = pre
    } else {
      children[index].style.transition = &apos;&apos;
      children[index].style.transform = `translateX(${-index * 500}px)`
      let pre = index === 0 ? children.length - 1 : index - 1
      children[pre].style.transition = &apos;&apos;
      children[pre].style.transform = `translateX(${(-pre - 1) * 500}px)`
    }
  }
  if (base &amp;#x3C; 0) {
    if (base &amp;#x3C; -250) {
      children[index].style.transition = &apos;&apos;
      children[index].style.transform = `translateX(${(-index - 1) * 500}px)`
      let pre = index === 3 ? 0 : index + 1
      children[pre].style.transition = &apos;&apos;
      children[pre].style.transform = `translateX(${-pre * 500}px)`
      index = pre
    } else {
      children[index].style.transition = &apos;&apos;
      children[index].style.transform = `translateX(${-index * 500}px)`
      let pre = index === 3 ? 0 : index + 1
      children[pre].style.transition = &apos;&apos;
      children[pre].style.transform = `translateX(${(-pre + 1) * 500}px)`
    }
  }

  document.removeEventListener(&apos;mousemove&apos;, move)
  document.removeEventListener(&apos;mouseup&apos;, up)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;上面的代码比较繁琐，其实我们可以将情况简化，我们可以利用 &lt;code&gt;Math.round()&lt;/code&gt; 四舍五入求出 &lt;code&gt;mouseup&lt;/code&gt; 触发以后要显示的图片（四舍五入后就不需要在特别判断哪个占的面积大了），现在我们要判断的就是另一张图片是前一张还是后一张即可。这个判断也是有规律的，我们利用 &lt;code&gt;Math.abs()&lt;/code&gt; 和 &lt;code&gt;Math.sign()&lt;/code&gt; 可以得出其值，&lt;code&gt;Math.sign(base)&lt;/code&gt; 判断是向左还是向右拖动，&lt;code&gt;Math.abs(base)&lt;/code&gt; 判断 &lt;code&gt;base&lt;/code&gt; 是否超过一半，结合两者我们就能判断出是上一张还是下一张。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let up = (e) =&gt; {
  let x = e.clientX - startX

  //index 四舍五入，代码简洁，不易理解，主要是利用四舍五入，统一了要从可视范围移出的元素的下标
  index = (index - Math.round(x / 500)) % children.length
  index = (index + children.length) % children.length //四舍五入，得到的就是mouseup触发后应该显示的图片下标
  let base = x % 500
  for (let offset of [0, (Math.abs(base) &gt; 250 ? 1 : -1) * Math.sign(base)]) {
    let pos = (index + offset + children.length) % children.length //获得另一个要移动的图片的下标（要移除可视范围的图片）
    children[pos].style.transition = &apos;&apos;
    children[pos].style.transform = `translateX(${(offset - pos) * 500}px)` //一个下标为index图片要显示它的偏移量是 -index, 偏移量 -1 表示再向左移动一个图片单位，偏移量 1 表示向右移动一个图片单位，最后的总偏移量为 -index + offset
  }

  document.removeEventListener(&apos;mousemove&apos;, move)
  document.removeEventListener(&apos;mouseup&apos;, up)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样代码也简单多了，不过要比上面那个难理解一点。理解的关键就是，同一个方向的移动，&lt;code&gt;base&lt;/code&gt; 是否超过一半其 &lt;code&gt;index&lt;/code&gt; 是不同的。&lt;/p&gt;
&lt;p&gt;完整的代码查看：&lt;a href=&quot;https://cdn.clloz.com/study/carousel.html&quot; title=&quot;图片轮播效果&quot;&gt;图片轮播效果&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;轮播问题的主要逻辑就是我们不需要关注所有图片，不需要每次移动都要保持视口外的图片都在**“正确”**的位置，我们只需要关注几张与当前显示相关的图片即可。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>终端设置代理</title><link>https://clloz.com/blog/terminal-proxy-configure</link><guid isPermaLink="true">https://clloz.com/blog/terminal-proxy-configure</guid><pubDate>Tue, 15 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;配置了 &lt;code&gt;V2ray&lt;/code&gt; 或者 &lt;code&gt;Shadowsocks&lt;/code&gt; 以后，一般来说，只有浏览器（包括内嵌在各种软件中的浏览器，比如 &lt;code&gt;WeGame&lt;/code&gt;、优酷、迅雷等软件中的内嵌浏览器）会走代理，其他的应用默认是不走代理的，需要我们手动配置。当需要在终端中使用 &lt;code&gt;brew&lt;/code&gt;，&lt;code&gt;git&lt;/code&gt; 或者 &lt;code&gt;npm&lt;/code&gt; 等安装 &lt;code&gt;package&lt;/code&gt; 或应用的时候，如果连接比较吃力的时候启动代理是一个解决方案。本文介绍以下如何在终端中配置代理。&lt;/p&gt;
&lt;h2&gt;全局代理模式&lt;/h2&gt;
&lt;p&gt;我们使用 &lt;code&gt;V2ray&lt;/code&gt; 或者 &lt;code&gt;Shadowsocks&lt;/code&gt; 会进行全局代理模式的选择，一般来说有三种 &lt;code&gt;PAC&lt;/code&gt;，&lt;code&gt;Global&lt;/code&gt; 和 &lt;code&gt;Manual&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PAC&lt;/code&gt;：&lt;code&gt;Proxy auto-config&lt;/code&gt;，根据配置文件来确定当前的连接是否需要代理，一般来说这个配置文件是 &lt;code&gt;GFW List&lt;/code&gt; 加上我们自己配置的 &lt;code&gt;user rules&lt;/code&gt;。关于 &lt;code&gt;user rules&lt;/code&gt; 如何配置可以参考我的另一片文章：&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/11/24/v2ray-install-configuration/#user-rules&quot; title=&quot;V2ray安装配置教程&quot;&gt;V2ray安装配置教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Global&lt;/code&gt;: 全局模式，所有连接都走代理。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Manual&lt;/code&gt;：手动模式，不会设置系统级代理，需要使用代理的应用（比如浏览器）都需要手动配置代理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一般来说我们使用前两个模式比较多，大多数情况下 &lt;code&gt;PAC&lt;/code&gt; 模式都足够了，如果发现某个我们经常访问的网站不在 &lt;code&gt;PAC&lt;/code&gt; 的规则之中，则手动添加到 &lt;code&gt;user rules&lt;/code&gt; 里面即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/proxy-rules.CBY0nMta_2qoOo8.webp&quot; alt=&quot;proxy-rules&quot; title=&quot;proxy-rules&quot;&gt;&lt;/p&gt;
&lt;p&gt;虽然 &lt;code&gt;PAC&lt;/code&gt; 和 &lt;code&gt;Global&lt;/code&gt; 都设置了系统级代理，但是一般只有浏览器（包括内嵌在各种软件中的浏览器，比如 &lt;code&gt;WeGame&lt;/code&gt;、优酷、迅雷等软件中的内嵌浏览器）才会使用这个系统级代理，其他应用一般还是需要手动配置。大部分应用不太需要使用这个需求，但是对于经常使用的终端配置一下代理能让我们使用 &lt;code&gt;homebrew&lt;/code&gt;，&lt;code&gt;git&lt;/code&gt; 和 &lt;code&gt;npm&lt;/code&gt; 下载的时候效率高很多。&lt;/p&gt;
&lt;h2&gt;终端配置&lt;/h2&gt;
&lt;p&gt;终端中使用有两种方式，一种是临时配置，重启终端后就失效了；另一种是写入到配置文件中去，每次启动终端都可以使用。我们还需要知道 &lt;code&gt;V2ray&lt;/code&gt; 和 &lt;code&gt;Shadowsocks&lt;/code&gt; 的客户端给我们提供了三种代理配置类型，&lt;code&gt;HTTP&lt;/code&gt;，&lt;code&gt;socks5&lt;/code&gt; 和 &lt;code&gt;PAC&lt;/code&gt;，对应的端口都不同。&lt;/p&gt;
&lt;h2&gt;临时配置&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 默认一般address是127.0.0.1，http默认port是1087，socks5默认port是1086，PAC默认port是1089
export http_proxy=http://proxyAddress:port
export https_proxy=&quot;http://localhost:port&quot;
export all_proxy=socks5://127.0.0.1:1086
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;写入配置文件&lt;/h2&gt;
&lt;p&gt;在终端配置文件中（&lt;code&gt;.zshrc&lt;/code&gt; 或者 &lt;code&gt;.bash_profile&lt;/code&gt;）将配置写入。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;alias proxy=&apos;export all_proxy=socks5://127.0.0.1:1086&apos;
alias unproxy=&apos;unset all_proxy&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;proxy&lt;/code&gt; 命令开启代理，使用 &lt;code&gt;unproxy&lt;/code&gt; 关闭代理。如何检测我们的代理是否开启了，可以使用命令 &lt;code&gt;curl cip.cc&lt;/code&gt;，你可以看到你当前的 &lt;code&gt;IP&lt;/code&gt;，位置和运营商。如果代理成功开启，那么你可以看到你的 &lt;code&gt;IP&lt;/code&gt; 位置等信息都编程了你的代理服务器。&lt;/p&gt;
&lt;p&gt;这里补充一下，用 &lt;code&gt;curl cip.cc&lt;/code&gt; 进行测试，其实这个测试结果完全就是看你 &lt;code&gt;curl&lt;/code&gt; 这个地址是否走了代理，如果在代理工具中配置了这个地址直连，那么得到的结果肯定还是真实的 &lt;code&gt;IP&lt;/code&gt; 而不是代理服务器 &lt;code&gt;IP&lt;/code&gt;，可以参考 &lt;a href=&quot;https://github.com/Dreamacro/clash/issues/592&quot;&gt;请问clashx怎么设置终端代理呢？&lt;/a&gt; 这个 &lt;code&gt;issue&lt;/code&gt;，用 &lt;code&gt;curl -vv https://www.google.com&lt;/code&gt; 来进行测试。&lt;/p&gt;
&lt;p&gt;最后还需要注意的一点是，如果你设置了 &lt;code&gt;brew&lt;/code&gt; 或者 &lt;code&gt;npm&lt;/code&gt; 的国内镜像，那么开启了代理以后，这些镜像的访问也会走代理，可能影响访问速度。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://vimcaw.github.io/blog/2018/03/12/Shadowsocks(R)%E8%AE%BE%E7%BD%AE%EF%BC%9A%E7%B3%BB%E7%BB%9F%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E3%80%81PAC%E3%80%81%E4%BB%A3%E7%90%86%E8%A7%84%E5%88%99/&quot; title=&quot;Shadowsocks(R)设置：系统代理模式、PAC、代理规则&quot;&gt;Shadowsocks(R)设置：系统代理模式、PAC、代理规则&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/shadowsocks.COhsaZjH.jpg"/><enclosure url="/_astro/shadowsocks.COhsaZjH.jpg"/></item><item><title>JavaScript 原型机制</title><link>https://clloz.com/blog/javascript-prototype</link><guid isPermaLink="true">https://clloz.com/blog/javascript-prototype</guid><pubDate>Fri, 11 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;原型链的概念相信大家都知道，&lt;code&gt;ES6&lt;/code&gt; 出来以后可能关注度没有以前那么高的。虽然在 &lt;code&gt;ES2015/ES6&lt;/code&gt; 中引入了 &lt;code&gt;class&lt;/code&gt; 关键字，但那只是语法糖，&lt;code&gt;JavaScript&lt;/code&gt; 仍然是基于原型的，作为 &lt;code&gt;JavaScript&lt;/code&gt; 中的主要继承方式，我们有必要深入理解它。理解了原型之后，你对对象的理解也会更深入。&lt;/p&gt;
&lt;h2&gt;原型机制&lt;/h2&gt;
&lt;p&gt;原型机制说起来很简单，就是一个对象可以访问它原型对象上的属性和方法，从而实现属性和方法的复用。而原型对象又有自己的原型对象，这样原型就构成了一个链式结构，也就是我们说的原型链。一个对象可以访问自己原型链上的所有方法和属性。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中的继承只有一种结构：对象。每个实例对象（ &lt;code&gt;object&lt;/code&gt; ）都有一个私有属性（称之为 &lt;code&gt;__proto__&lt;/code&gt;，引擎内部是 &lt;code&gt;[[prototype]]&lt;/code&gt; ）指向它的构造函数的原型对象（&lt;code&gt;prototype&lt;/code&gt; ）。该原型对象也有一个自己的原型对象( &lt;code&gt;__proto__&lt;/code&gt; ) ，层层向上直到一个对象的原型对象为 &lt;code&gt;null&lt;/code&gt;。根据定义，&lt;code&gt;null&lt;/code&gt; 没有原型，并作为这个原型链中的最后一个环节。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中，我们知道的数据类型有 &lt;code&gt;Number, String, Undefined, Null, Boolean, BigInt, Symbol&lt;/code&gt; 七个基础类型，还有就是一个引用类型 &lt;code&gt;Object&lt;/code&gt;。在内置对象比如 &lt;code&gt;Function, Array, Date, RegExp&lt;/code&gt; 等中，&lt;code&gt;Function&lt;/code&gt; 是一个特殊的内置对象。&lt;/p&gt;
&lt;p&gt;我们将 &lt;code&gt;JavaScript&lt;/code&gt; 中的对象分成两大类，一类是 &lt;code&gt;Object&lt;/code&gt; ，一类就是 &lt;code&gt;Function&lt;/code&gt;。我们来说一下他们之间的关系。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我们创建对象有很多种方法，&lt;code&gt;Object.create()&lt;/code&gt;，&lt;code&gt;new Object()&lt;/code&gt;，&lt;code&gt;new function()&lt;/code&gt;，和对象字面量等。但其实他们的本质都是 &lt;code&gt;new Object()&lt;/code&gt; （关于 &lt;code&gt;new&lt;/code&gt; 和对象创建的内容参考另外两篇文章：&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/09/javascript-object-prop-assign/&quot; title=&quot;JavaScript对象属性类型和赋值细节&quot;&gt;JavaScript对象属性类型和赋值细节&lt;/a&gt; 和 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/29/new-operator/&quot; title=&quot;JavaScript中new操作符的解析和实现&quot;&gt;JavaScript中new操作符的解析和实现&lt;/a&gt;）。&lt;/p&gt;
&lt;h2&gt;Object.prototype.&lt;strong&gt;__proto__&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;我们用 &lt;code&gt;new Object()&lt;/code&gt; 创建一个空对象，它在 &lt;code&gt;Chrome&lt;/code&gt; 中打印出的结果如下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/proto1.DCy6MpEe_gItkr.webp&quot; alt=&quot;proto3&quot; title=&quot;proto1&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以看到所谓的 &lt;strong&gt;空对象&lt;/strong&gt;，并不是完全空的，它内部有一个 &lt;code&gt;__proto__&lt;/code&gt; 属性。但其实这个属性并不是它自身的，这个属性是 &lt;code&gt;Object.prototype.__proto__&lt;/code&gt;，一个访问器属性（一个 &lt;code&gt;getter&lt;/code&gt; 函数和一个 &lt;code&gt;setter&lt;/code&gt; 函数）, 暴露了通过它访问的对象的内部 &lt;code&gt;[[Prototype]]&lt;/code&gt; (一个对象或 &lt;code&gt;null&lt;/code&gt;)。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里要注意，&lt;code&gt;Object.prototype.__proto__&lt;/code&gt; 和内部的 &lt;code&gt;[[prototype]]&lt;/code&gt; 并不是同一个东西。我们的原型是靠内部的 &lt;code&gt;[[prototype]]&lt;/code&gt; 链接的，&lt;code&gt;Object.prototype.__proto__&lt;/code&gt; 只是浏览器提供的一个访问器属性向我们暴露 &lt;code&gt;[[prototype]]&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个属性是由浏览器厂商提供的，并且目前绝大多数的浏览器都支持这个属性，所以 &lt;code&gt;ECMAScript 2015&lt;/code&gt; 中也将其写入标准附录中，保持浏览器的兼容性。但是直接修改对象的 &lt;code&gt;[[prototype]]&lt;/code&gt; 在任何引擎和浏览器中都是非常慢并且影响性能的操作，使用这种方式来改变和继承属性是对性能影响非常严重的，并且性能消耗的时间也不是简单的花费在 &lt;code&gt;obj.__proto__ = ...&lt;/code&gt; 语句上, 它还会影响到所有继承来自该 &lt;code&gt;[[Prototype]]&lt;/code&gt; 的对象。标准中还提供了两组关于读写原型对象的方法 &lt;code&gt;Object.getPrototypeOf/Reflect.getPrototypeOf&lt;/code&gt; 和 &lt;code&gt;Object.setPrototypeOf/Reflect.setPrototypeOf&lt;/code&gt;。不过写对象和上面说的一样，依然是一个影响性能的操作，如果你关心性能，不应该用这些方法。比较好的实践是用 &lt;code&gt;Object.create()&lt;/code&gt; 来设置原型，用 &lt;code&gt;Object.getPrototypeOf()&lt;/code&gt; 来读取原型对象。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们同样可以用对象字面量来设置 &lt;code&gt;__proto__&lt;/code&gt;，也可以自定义 &lt;code&gt;__proto__&lt;/code&gt; 来覆盖 &lt;code&gt;Object.prototype.__proto__&lt;/code&gt;。参考文章：&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/09/javascript-object-prop-assign/&quot; title=&quot;JavaScript对象属性类型和赋值细节&quot;&gt;JavaScript对象属性类型和赋值细节&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不同类型的对象其 &lt;code&gt;[[prototype]]&lt;/code&gt; 是不同的，对于使用数组字面量创建的对象，这个值是 &lt;code&gt;Array.prototype&lt;/code&gt;。对于 &lt;code&gt;functions&lt;/code&gt;，这个值是 &lt;code&gt;Function.prototype&lt;/code&gt;。对于使用 &lt;code&gt;new fun&lt;/code&gt; 创建的对象，其中 &lt;code&gt;fun&lt;/code&gt; 是由 &lt;code&gt;js&lt;/code&gt; 提供的内建构造器函数之一(&lt;code&gt;Array, Boolean, Date, Number, Object, String&lt;/code&gt; 等等），这个值总是 &lt;code&gt;fun.prototype&lt;/code&gt;。对于用 &lt;code&gt;js&lt;/code&gt; 定义的其他 &lt;code&gt;js&lt;/code&gt; 构造器函数创建的对象，这个值就是该构造器函数的 &lt;code&gt;prototype&lt;/code&gt; 属性。关于内置对象之间的关系，我们后面会详细讨论。&lt;/p&gt;
&lt;h2&gt;Object 和 Function&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Object&lt;/code&gt; 和 &lt;code&gt;Function&lt;/code&gt; 是 &lt;code&gt;JavaScript&lt;/code&gt; 中最重要的两个对象，他们同时也是构造函数 &lt;code&gt;function Object(), function Function()&lt;/code&gt;。几乎所有对象都是 &lt;code&gt;function Object()&lt;/code&gt; 的实例，而所有函数都是 &lt;code&gt;function Function()&lt;/code&gt; 的实例，包括 &lt;code&gt;Object&lt;/code&gt; 也是由 &lt;code&gt;Function&lt;/code&gt; 构造的。&lt;/p&gt;
&lt;p&gt;我们上面说过对象内部有一个 &lt;code&gt;[[prototype]]&lt;/code&gt; 属性指向它的源性对象；而每一个函数都有一个 &lt;code&gt;prototype&lt;/code&gt; 属性，指向由这个函数构造出的对象的 &lt;code&gt;[[prototype]]&lt;/code&gt;。更准确的说，在函数被创建的时候，就有一个 &lt;code&gt;prototype&lt;/code&gt; 属性指向一个对象，这个对象本身只有一个 &lt;code&gt;constructor&lt;/code&gt; 属性指向这个函数。当用 &lt;code&gt;new func()&lt;/code&gt; 创建对象的时候，新对象的 &lt;code&gt;[[prototype]]&lt;/code&gt; 就指向构造函数的 &lt;code&gt;prototype&lt;/code&gt; 对应的对象。不过需要注意的是 &lt;code&gt;prototype&lt;/code&gt; 和 &lt;code&gt;constructor&lt;/code&gt; 都是可以 &lt;strong&gt;重写&lt;/strong&gt; 的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/proto2.BriB60KY_Z2wjTMR.webp&quot; alt=&quot;proto3&quot; title=&quot;proto2&quot;&gt;&lt;/p&gt;
&lt;p&gt;对于我们的自定义对象，这是很好理解的。那么内置对象之间的关系，特别是 &lt;code&gt;Object&lt;/code&gt; 和 &lt;code&gt;Function&lt;/code&gt; 之间的关系是怎么样的呢。先明确两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一切函数都是由 &lt;code&gt;function Function()&lt;/code&gt; 构造的，所以函数的 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向 &lt;code&gt;Function.prototype&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;所有由 &lt;code&gt;function Object()&lt;/code&gt; 构造的非函数对象的 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向 &lt;code&gt;Object.prototype&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;函数的创建的同时，会创建一个 &lt;code&gt;function.prototype&lt;/code&gt; 对象，该对象是一个由 &lt;code&gt;function Object()&lt;/code&gt; 构造的对象。&lt;code&gt;prototype&lt;/code&gt; 属性可以任意指定，指定的对象内可能没有 &lt;code&gt;constructor&lt;/code&gt; 属性或者是错误的 &lt;code&gt;constructor&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;所有非函数对象都是由构造函数通过 &lt;code&gt;new&lt;/code&gt; 运算符创建的（本质都是 &lt;code&gt;new Object()&lt;/code&gt;，很多内置对象可以省略 &lt;code&gt;new&lt;/code&gt;，比如 &lt;code&gt;Function&lt;/code&gt;， &lt;code&gt;Object&lt;/code&gt;，&lt;code&gt;Array&lt;/code&gt;，省略和不省略效果是一样的)。这个构造函数要么是自定义的（由 &lt;code&gt;function Function()&lt;/code&gt; 构造），要么是 &lt;code&gt;function Object()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;自定义函数手动指定 &lt;code&gt;prototype&lt;/code&gt; 为其它自定函数的实例， 可以让我们实现链式继承，这条链最终有一个节点会是有 &lt;code&gt;function Object()&lt;/code&gt; 构造的对象，它的 &lt;code&gt;[[protottype]]&lt;/code&gt; 指向 &lt;code&gt;Object.prototype&lt;/code&gt;。所以我们可以说，所有的非函数对象都是 &lt;code&gt;function Object()&lt;/code&gt; 的实例。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其实记住这几点就可以应对绝大部分问题，如果你还对内置对象的关系有兴趣，可以继续往下看。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;根据我们上面的两条规律我们可以知道 &lt;code&gt;Object&lt;/code&gt; 的 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向 &lt;code&gt;Function.prototype&lt;/code&gt;，那么 &lt;code&gt;function Function()&lt;/code&gt; ，&lt;code&gt;Function.prototype&lt;/code&gt; 和 &lt;code&gt;Object.prototype&lt;/code&gt; 的 &lt;code&gt;[[prototype]]&lt;/code&gt; 都分别是什么呢？&lt;/p&gt;
&lt;p&gt;先说 &lt;code&gt;Object.prototype&lt;/code&gt;，它是所有非函数对象的 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向，而它自己的 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向的就是 &lt;code&gt;null&lt;/code&gt;，也就是一切对象的原型链的终点。它的 &lt;code&gt;constructor&lt;/code&gt; 属性指向 &lt;code&gt;function Object()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;function Function()&lt;/code&gt; 的 &lt;code&gt;[[prototype]]&lt;/code&gt; 和其它的函数一样，指向 &lt;code&gt;Function.prototype&lt;/code&gt;，也就是说 &lt;code&gt;function Function()&lt;/code&gt; 的 &lt;code&gt;prototype&lt;/code&gt; 和 &lt;code&gt;[[prototype]]&lt;/code&gt;指向的是同一个对象 &lt;code&gt;Function.prototype&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Function.prototype&lt;/code&gt; 的 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向的是 &lt;code&gt;Object.prototype&lt;/code&gt;。&lt;code&gt;consctructor&lt;/code&gt; 指向的是 &lt;code&gt;function Function()&lt;/code&gt;。其实 &lt;code&gt;Function.prototype&lt;/code&gt; 本身就是函数，可以直接调用，接受任何参数并返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为什么要这样呢？我认为是确保每一个函数对象，非函数对象，他们的原型链上都有 &lt;code&gt;Object.prototype&lt;/code&gt;，都能够访问 &lt;code&gt;Object.prototype&lt;/code&gt; 上定义的一些公有方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想要更清晰的看清楚我上面说的关系，可以借助于这张来自网上的图，画的非常好。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/proto3.BFFjOmGn_Z1mYfHp.webp&quot; alt=&quot;proto3&quot; title=&quot;proto3&quot;&gt;&lt;/p&gt;
&lt;h2&gt;其他内置对象&lt;/h2&gt;
&lt;p&gt;最后在说一说其他的内置对象，绝大多数内置对象都是函数对象（&lt;code&gt;BigInt&lt;/code&gt;，&lt;code&gt;Math&lt;/code&gt;，&lt;code&gt;JSON&lt;/code&gt; 和 &lt;code&gt;Reflect&lt;/code&gt; 不是函数对象），虽然有些不能用 &lt;code&gt;new&lt;/code&gt; 操作符（比如 &lt;code&gt;Symbol&lt;/code&gt;，有些对象用不用 &lt;code&gt;new&lt;/code&gt; 表现一样，比如 &lt;code&gt;Object&lt;/code&gt;， &lt;code&gt;Function&lt;/code&gt;，&lt;code&gt;Array&lt;/code&gt; 等）。所以内置对象的 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向 &lt;code&gt;function.prototype&lt;/code&gt;。内置对象的 &lt;code&gt;prototype&lt;/code&gt; 一般来说就是一个普通的对象（用 &lt;code&gt;function Object()&lt;/code&gt; 构造的）。这个对象上挂载了很多该类型可以使用的方法，比如 &lt;code&gt;Array.prototype&lt;/code&gt; 有如下属性：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;concat: ƒ concat()
constructor: ƒ Array()
copyWithin: ƒ copyWithin()
entries: ƒ entries()
every: ƒ every()
fill: ƒ fill()
filter: ƒ filter()
find: ƒ find()
findIndex: ƒ findIndex()
flat: ƒ flat()
flatMap: ƒ flatMap()
forEach: ƒ forEach()
includes: ƒ includes()
indexOf: ƒ indexOf()
join: ƒ join()
keys: ƒ keys()
lastIndexOf: ƒ lastIndexOf()
length: 0
map: ƒ map()
pop: ƒ pop()
push: ƒ push()
reduce: ƒ reduce()
reduceRight: ƒ reduceRight()
reverse: ƒ reverse()
shift: ƒ shift()
slice: ƒ slice()
some: ƒ some()
sort: ƒ sort()
splice: ƒ splice()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
unshift: ƒ unshift()
values: ƒ values()
Symbol(Symbol.iterator): ƒ values()
Symbol(Symbol.unscopables): {copyWithin: true, entries: true, fill: true, find: true, findIndex: true, …}
__proto__: Object
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的一点是，几乎所有内置对象的属性都是不可枚举的，所以无论是 &lt;code&gt;for ... in&lt;/code&gt; 还是 &lt;code&gt;Object.keys()&lt;/code&gt; 都是无法枚举这些属性的。我们自己也可以在内置对象的 &lt;code&gt;prototype&lt;/code&gt; 上添加属性或者方法，让所有该类型的对象都能使用。&lt;/p&gt;
&lt;p&gt;基本包装类型 &lt;code&gt;String&lt;/code&gt;，&lt;code&gt;Number&lt;/code&gt; 和 &lt;code&gt;Boolean&lt;/code&gt; 在一般情况下不要使用创建对象的方式来初始化对应的类型。使用这种方式创建的值都是对象（使用 &lt;code&gt;typeof&lt;/code&gt; 返回 &lt;code&gt;object&lt;/code&gt;），而且所有基本包装类型的对象都会被转换为布尔值 &lt;code&gt;true&lt;/code&gt;。看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(typeof String(&apos;&apos;)) //string
console.log(typeof new String(&apos;&apos;)) //object
console.log(!!&apos;&apos;) //false
console.log(!!String(&apos;&apos;)) //false
console.log(!!new String(&apos;&apos;)) //true

console.log(typeof Number(0)) //number
console.log(typeof new Number(0)) //object
console.log(!!0) //false
console.log(!!Number(0)) //false
console.log(!!new Number(0)) //true

console.log(typeof Boolean(&apos;&apos;)) //string
console.log(typeof new Boolean(&apos;&apos;)) //object
console.log(!!Boolean(&apos;&apos;)) //false
console.log(!!new Boolean(&apos;&apos;)) //true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 &lt;code&gt;constructor&lt;/code&gt; 和 &lt;code&gt;prototype&lt;/code&gt; 有一个有趣的小题目，可以看一看：&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/05/31/prototype-constructor/&quot; title=&quot;关于constructor和prototype的思考&quot;&gt;关于constructor和prototype的思考&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain&quot; title=&quot;继承与原型链&quot;&gt;继承与原型链 - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/proto&quot; title=&quot;Object.prototype.__proto__ - MDN&quot;&gt;Object.prototype.&lt;strong&gt;proto&lt;/strong&gt; - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>更换 Apache 到 Nginx</title><link>https://clloz.com/blog/change-apache-to-nginx</link><guid isPermaLink="true">https://clloz.com/blog/change-apache-to-nginx</guid><pubDate>Thu, 10 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;将服务器上的 &lt;code&gt;Apache&lt;/code&gt; 升级到 &lt;code&gt;2.4.46&lt;/code&gt; 后，内充占用率飙涨，改了 &lt;code&gt;MaxConnectionPerChild&lt;/code&gt; 配置到 &lt;code&gt;50&lt;/code&gt; 也不见效。&lt;code&gt;2G&lt;/code&gt; 的内存占用率已经超过 &lt;code&gt;90&lt;/code&gt;，&lt;code&gt;Apache&lt;/code&gt; 吃掉了差不多 &lt;code&gt;1G&lt;/code&gt; 内存。&lt;code&gt;systemctl restart httpd&lt;/code&gt; 以后，很快又把内存吃回去。虽然一直都遇到内存占用的问题，但之前没有这么严重，也就凑活着用了。每次想换 &lt;code&gt;nginx&lt;/code&gt; 都觉得太折腾就作罢。现在这情况只能强行折腾了，服务器都卡的用不了了。下面分享一下更换 &lt;code&gt;web&lt;/code&gt; 服务器的过程。&lt;/p&gt;
&lt;h2&gt;过程&lt;/h2&gt;
&lt;p&gt;其实过程也比较简单，停了 &lt;code&gt;apache&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;systemctl stop httpd
systemctl disable httpd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装 &lt;code&gt;nginx&lt;/code&gt;，直接用 &lt;code&gt;yum&lt;/code&gt; 安装即可。启动并设置开机启动，同时确保 &lt;code&gt;php-fpm&lt;/code&gt; 也启动了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yum install nginx
systemctl start nginx
systemctl enable ninx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;剩下的就是配置了，配置文件路径 &lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;。&lt;code&gt;nginx&lt;/code&gt; 支持模块化的配置，你可以把不同功能的配置写到不同的文件里面，然后用 &lt;code&gt;include&lt;/code&gt; 引入。单独的 &lt;code&gt;conf&lt;/code&gt; 文件要放到 &lt;code&gt;/etc/nginx/conf.d&lt;/code&gt; 文件夹里。如果你不想创建单独的文件，就把配置写在 &lt;code&gt;nginx.conf&lt;/code&gt;文件夹里也可以，是一个 &lt;code&gt;server {}&lt;/code&gt;。需要特别注意的是你的配置要写到 &lt;code&gt;include /etc/nginx/conf.d/*.conf&lt;/code&gt; 这一句的 &lt;strong&gt;前面&lt;/strong&gt;。我一开始就是看错成后面，白白乱折腾了一阵子。&lt;/p&gt;
&lt;p&gt;然后 &lt;code&gt;nginx&lt;/code&gt; 的配置其实还是比较好理解的，但是不支持 &lt;code&gt;.htaccess&lt;/code&gt;。关于配置我这里就不细说了，我也就东拼西凑搞了个差不多的，目前看来基本能用了，有些问题可能后期使用中才能慢慢发现，这里就给大家贴一下我现在的配置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;server {
    #http重定向到https
    listen    80;
    listen [::]:80;
    server_name www.clloz.com clloz.com;
    return 301  https://$server_name$request_uri;
}
server {
    listen                  443 ssl http2;
    listen                  [::]:443 ssl http2;
    server_name             www.clloz.com clloz.com;

    #网站根目录
    root            /var/www/html;

    #https
    ssl_certificate         ssl/3793755_www.clloz.com.pem;
    ssl_certificate_key     ssl/3793755_www.clloz.com.key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; #使用此加密套件。
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #使用该协议进行配置。
    ssl_prefer_server_ciphers on;

    # 安全标头
    add_header Strict-Transport-Security &quot;max-age=31536000; includeSubDomains; preload&quot; always;
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-Xss-Protection 1;

    # 禁用目录列表
    autoindex off;

    # 限制请求次数
    #limit_req_zone $binary_remote_addr zone=WPRATELIMIT:10m rate=2r/s;
    #location ~ \wp-login.php$ {
    #    limit_req zone=WPRATELIMIT;
    #}

    #隐藏 nginx 版本.
    server_tokens off;

    #隐藏 PHP 版本
    fastcgi_hide_header X-Powered-By;
    proxy_hide_header X-Powered-By;

    # 禁止访问敏感文件
    location ~ /\.(svn|git)/* {
        deny all;
        access_log off;
        log_not_found off;
    }
    location ~ /\.ht {
        deny all;
        access_log off;
        log_not_found off;
    }
    location ~ /\.user.ini {
        deny all;
        access_log off;
        log_not_found off;
    }

    # 禁止直接访问php文件
#    location ~* /(?:uploads|files|wp-content|wp-includes|akismet)/.*.php$ {
#       deny all;
#       access_log off;
#       log_not_found off;
#    }

    #固定链接交给php-fpm处理
    location / {
        index index.php index.html index.htm;
        try_files $uri $uri/ /index.php?$args;
    }

    # 禁止访问指定类型文件
    location ~ \.(ini|conf)$ {
        deny all;
    }

    # 允许内部分  wp-includes 目录的 .php 文件
    location ~* ^/wp-includes/.*\.(php|phps)$ {
        internal;
    }

    #禁止访问 wp-config.php install.php 文件
    location = /wp-config.php {
        deny all;
    }
    location = /wp-admin/install.php {
        deny all;
    }


    # 禁止访问 /wp-content/ 目录的 php 格式文件 (包含子目录)
    location ~* ^/wp-content/.*.(php|phps)$ {
        deny all;
    }
    # 固定连接的php处理
    location ~* ^/s/.*.(php|phps)$ {
        #deny all;
    return 404;
    }
    location ~* ^/programming/.*.(php|phps)$ {
        #deny all;
    return 404;
    }
    location ~* ^/essay/.*.(php|phps)$ {
        #deny all;
    return 404;
    }
    location ~* ^/sweets/.*.(php|phps)$ {
        #deny all;
    return 404;
    }
    location ~* ^/links/.*.(php|phps)$ {
        #deny all;
    return 404;
    }
    location ~* ^/abouts/.*.(php|phps)$ {
        #deny all;
    return 404;
    }

    location ~* /(?:uploads|files|wp-content|wp-includes|akismet)/.*.php$ {
        deny all;
        access_log off;
        log_not_found off;
    }

    # 错误页面设置
    error_page 403 /403.html;
    location = /403.html {
        root /etc/nginx/error_pages;
        internal;
    }
    error_page 404 /404.html;
    location = /404.html {
        root /etc/nginx/error_pages;
        internal;
    }
    error_page 500 /500.html;
    location = /500.html {
        root /etc/nginx/error_pages;
        internal;
    }
    error_page 503 /503.html;
    location = /503.html {
        root /etc/nginx/error_pages;
        internal;
    }


    location ~ .php$ {
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一个没有解决的问题就是，&lt;code&gt;php&lt;/code&gt; 类型的 &lt;code&gt;url&lt;/code&gt; 都交给 &lt;code&gt;php-fpm&lt;/code&gt; 处理，当找不到 &lt;code&gt;url&lt;/code&gt; 的时候，&lt;code&gt;php-fpm&lt;/code&gt; 会直接返回一个 &lt;code&gt;file not found&lt;/code&gt;。我们在 &lt;code&gt;nginx&lt;/code&gt; 中设置的 &lt;code&gt;404&lt;/code&gt; 页面也不会显示。我本来想看看 &lt;code&gt;php-fpm&lt;/code&gt; 能不能设置默认 &lt;code&gt;404&lt;/code&gt; 页面的，不过没找到方法。&lt;code&gt;nginx&lt;/code&gt; 这边也没设么很好的处理方法，我最后的解决办法就是用 &lt;code&gt;location&lt;/code&gt; 来过滤固定链接，只要检测到是固定链接，同时路径是以 &lt;code&gt;php&lt;/code&gt; 结束的直接返回 &lt;code&gt;404&lt;/code&gt;，固定链接一共也就几种(取决于你有几个一级分类目录)。这个方法有一个瑕疵就是根目录下的以 &lt;code&gt;php&lt;/code&gt; 结尾的路径无法过滤，因为根目录下有些 &lt;code&gt;php&lt;/code&gt; 是要访问的，我们没法一刀切。不过目前也没有找到其他的好办法，就先这样吧，问题也不大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;更新&lt;/strong&gt;：根目录下路径下的 &lt;code&gt;php&lt;/code&gt; 结尾的路径我用正则表达式 &lt;code&gt;^/(?!(wp-|xmlrpc))[^/]*php$&lt;/code&gt; 进行了处理，把除了 &lt;code&gt;wp-&lt;/code&gt; 开头的和 &lt;code&gt;index.php&lt;/code&gt;，&lt;code&gt;xmlrpc.php&lt;/code&gt; 以外的全部过滤了。除了 &lt;code&gt;wp-content&lt;/code&gt;，&lt;code&gt;wp-includes&lt;/code&gt;，&lt;code&gt;wp-admin&lt;/code&gt; 路径下，其他的带 &lt;code&gt;/&lt;/code&gt; 的路径访问 &lt;code&gt;php&lt;/code&gt; 都直接返回 &lt;code&gt;404&lt;/code&gt;，正则表达式为 &lt;code&gt;^/(?!wp-content|wp-includes|wp-admin|editormd).*/.*php$&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里顺便说一下 &lt;code&gt;location&lt;/code&gt; 配置指令格式为：&lt;code&gt;location [ = | ~ | ~* | ^~ ] uri {...}&lt;/code&gt;。这里的 &lt;code&gt;uri&lt;/code&gt; 分为标准 &lt;code&gt;uri&lt;/code&gt; 和正则 &lt;code&gt;uri&lt;/code&gt;，两者的唯一区别是 &lt;code&gt;uri&lt;/code&gt; 中是否包含正则表达式。&lt;code&gt;uri&lt;/code&gt; 前面的方括号中的内容是可选项，解释如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;=&lt;/code&gt; ：用于标准 &lt;code&gt;uri&lt;/code&gt; 前，要求请求字符串与 &lt;code&gt;uri&lt;/code&gt; 严格匹配，一旦匹配成功则停止&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~&lt;/code&gt; ：用于正则 &lt;code&gt;uri&lt;/code&gt; 前，并且区分大小写&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~*&lt;/code&gt; ：用于正则 &lt;code&gt;uri&lt;/code&gt; 前，但不区分大小写&lt;/li&gt;
&lt;li&gt;&lt;code&gt;^~&lt;/code&gt; ：用于标准 &lt;code&gt;uri&lt;/code&gt; 前，要求 &lt;code&gt;Nginx&lt;/code&gt; 找到标识 &lt;code&gt;uri&lt;/code&gt; 和请求字符串匹配度最高的 &lt;code&gt;location&lt;/code&gt; 后，立即使用此 &lt;code&gt;location&lt;/code&gt; 处理请求，而不再使用 &lt;code&gt;location&lt;/code&gt; 块中的正则 &lt;code&gt;uri&lt;/code&gt; 和请求字符串做匹配&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于我只是更换 &lt;code&gt;web&lt;/code&gt; 服务器，所以还算比较简单，如果你是从头安装，那么你可以看我之前的文章，或者参考腾讯云的这篇教程：&lt;a href=&quot;https://cloud.tencent.com/document/product/213/38056&quot; title=&quot;手动搭建 LNMP 环境（CentOS 7）&quot;&gt;手动搭建 LNMP 环境（CentOS 7）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;更新&lt;/strong&gt;内存占用的主要原因是 &lt;code&gt;php-fpm&lt;/code&gt; 进程过多，配置一下 &lt;code&gt;/etc/php-fpm.d/www.conf&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pm = dynamic #指定进程管理方式，有3种可供选择：static、dynamic和ondemand。
pm.max_children = 16 #static模式下创建的子进程数或dynamic模式下同一时刻允许最大的php-fpm子进程数量。
pm.start_servers = 10 #动态方式下的起始php-fpm进程数量。
pm.min_spare_servers = 8 #动态方式下服务器空闲时最小php-fpm进程数量。
pm.max_spare_servers = 16 #动态方式下服务器空闲时最大php-fpm进程数量。
pm.max_requests = 2000 #php-fpm子进程能处理的最大请求数。
pm.process_idle_timeout = 10s
request_terminate_timeout = 120
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我是将 &lt;code&gt;max_spare_servers&lt;/code&gt; 设置为 &lt;code&gt;10&lt;/code&gt;，这样最多同时存在 &lt;code&gt;10&lt;/code&gt; 个 &lt;code&gt;php-fpm&lt;/code&gt;。&lt;/p&gt;</content:encoded><h:img src="/_astro/nginx.WNyebmBm.png"/><enclosure url="/_astro/nginx.WNyebmBm.png"/></item><item><title>Object.create(null) 和 {...}</title><link>https://clloz.com/blog/object-create-null</link><guid isPermaLink="true">https://clloz.com/blog/object-create-null</guid><pubDate>Thu, 10 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们会在一些框架的源码中看到作者用 &lt;code&gt;Object.create(null)&lt;/code&gt; 来初始化一个新的对象，和我们平常使用的对象字面量 &lt;code&gt;{}&lt;/code&gt; 不同。本文讲解一下这两者的区别。&lt;/p&gt;
&lt;h2&gt;对象初始化&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中初始化对象的方法有三种：&lt;code&gt;Object.create()&lt;/code&gt;，&lt;code&gt;new Object()&lt;/code&gt; 和字面量标记。我们分别来讲一讲三者的区别。&lt;/p&gt;
&lt;h2&gt;Object.create()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Object.create()&lt;/code&gt; 方法接受两个参数，一个是新对象的原型对象，一个是要添加到新对象的属性（可选，是一个对象，添加的属性默认不可枚举，属性的形式参考 &lt;code&gt;Object.defineProperty()&lt;/code&gt;）。返回值一个新对象，带着指定的原型对象和属性。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object.create()&lt;/code&gt; 内部的原理参考 &lt;code&gt;MDN&lt;/code&gt; 的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//请注意，尽管在 ES5 中 Object.create支持设置为[[Prototype]]为null，但因为那些ECMAScript5以前版本限制，此 polyfill 无法支持该特性。
if (typeof Object.create !== &apos;function&apos;) {
  Object.create = function (proto, propertiesObject) {
    if (typeof proto !== &apos;object&apos; &amp;#x26;&amp;#x26; typeof proto !== &apos;function&apos;) {
      throw new TypeError(&apos;Object prototype may only be an Object: &apos; + proto)
    } else if (proto === null) {
      throw new Error(
        &quot;This browser&apos;s implementation of Object.create is a shim and doesn&apos;t support &apos;null&apos; as the first argument.&quot;
      )
    }

    if (typeof propertiesObject !== &apos;undefined&apos;)
      throw new Error(
        &quot;This browser&apos;s implementation of Object.create is a shim and doesn&apos;t support a second argument.&quot;
      )

    function F() {}
    F.prototype = proto

    return new F()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Object.create()&lt;/code&gt; 可以帮我们实现继承，并且配置属性特性。也可以结合 &lt;code&gt;Object.assign()&lt;/code&gt; 来实现多继承。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function MyClass() {
  SuperClass.call(this)
  OtherSuperClass.call(this)
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype)
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype)
// 重新指定constructor
MyClass.prototype.constructor = MyClass

MyClass.prototype.myMethod = function () {
  // do a thing
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;new Object()&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中，几乎所有的对象都是 &lt;code&gt;Object&lt;/code&gt; 类型的实例，它们都会从 &lt;code&gt;Object.prototype&lt;/code&gt; 继承属性和方法。&lt;code&gt;Object&lt;/code&gt; 构造函数为给定值创建一个对象包装器。用 &lt;code&gt;Object&lt;/code&gt; 构造函数创建新的对象，参数接收任何值，但是根据值的不同会产生不同的对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果给定值是 &lt;code&gt;null&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt;，将会创建并返回一个空对象（没有给定值也可以认为是 &lt;code&gt;undefined&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;如果传进去的是一个基本类型的值，则会构造其包装类型的对象&lt;/li&gt;
&lt;li&gt;如果传进去的是引用类型的值，仍然会返回这个值，经他们复制的变量保有和源对象相同的引用地址&lt;/li&gt;
&lt;li&gt;当以非构造函数形式被调用时，&lt;code&gt;Object&lt;/code&gt; 的行为等同于 &lt;code&gt;new Object()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在实际编码中我们很少用到这种方式初始化对象。&lt;/p&gt;
&lt;h2&gt;对象字面量&lt;/h2&gt;
&lt;p&gt;一个对象初始化器，由花括号&lt;code&gt;{}&lt;/code&gt; 包含的一个由零个或多个对象属性名和其关联值组成的一个逗号分隔的列表构成。这是我们最常使用的一种初始化对象的方式。之所以使用最频繁，是因为他是最方便的一种创建对象的方式。我们可以在创建对象的时候同时创建属性，并且在 &lt;code&gt;ES6&lt;/code&gt; 之后，字面量方式创建对象添加了更多支持，包括扩展运算符，计算属性名等。&lt;/p&gt;
&lt;p&gt;定义属性为 &lt;code&gt;__proto__:&lt;/code&gt; 值 或 &lt;code&gt;&quot;__proto__&quot;:&lt;/code&gt; 值 时，不会创建名为 &lt;code&gt;__proto__&lt;/code&gt; 属性。如果给出的值是对象或者 &lt;code&gt;null&lt;/code&gt;，那么对象的 &lt;code&gt;[[Prototype]]&lt;/code&gt; 会被设置为给出的值。注意的是一定要使用冒号的方式定义，不使用冒号标记的属性定义，不会变更对象的原型；而是和其他具有不同名字的属性一样是普通属性定义。下面这些形式都不可以。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var __proto__ = &apos;variable&apos;

var obj1 = { __proto__ }
console.log(Object.getPrototypeOf(obj1) === Object.prototype) //true
console.log(obj1.hasOwnProperty(&apos;__proto__&apos;)) //true
console.log(obj1.__proto__ === &apos;variable&apos;) //true

var obj2 = {
  __proto__() {
    return &apos;hello&apos;
  }
}
console.log(obj2.__proto__() === &apos;hello&apos;) //true

var obj3 = { [&apos;__prot&apos; + &apos;o__&apos;]: 17 }
console.log(obj3.__proto__ === 17) //true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一点就是对象字面量虽然和 &lt;code&gt;JSON&lt;/code&gt; 很像，但他们不是同一个东西，主要不同点有以下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;JSON&lt;/code&gt; 只允许 &lt;code&gt;&quot;property&quot;: value syntax&lt;/code&gt; 形式的属性定义。属性名必须用双引号括起来。且属性定义不允许使用简便写法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JSON&lt;/code&gt; 中，属性的值仅允许字符串，数字，数组，&lt;code&gt;true&lt;/code&gt;，&lt;code&gt;false&lt;/code&gt;，&lt;code&gt;null&lt;/code&gt; 或其他（&lt;code&gt;JSON&lt;/code&gt;）对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JSON&lt;/code&gt; 中，不允许将值设置为函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Date&lt;/code&gt; 等对象，经 &lt;code&gt;JSON.parse()&lt;/code&gt; 处理后，会变成字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JSON.parse()&lt;/code&gt; 不会处理计算的属性名，会当做错误抛出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Object.create(null) 和 {...}&lt;/h2&gt;
&lt;p&gt;最后来说一说 &lt;code&gt;Object.create(null)&lt;/code&gt; 和 &lt;code&gt;{...}&lt;/code&gt; 的区别。&lt;/p&gt;
&lt;p&gt;其实很简单，&lt;code&gt;{}&lt;/code&gt; 是一个继承自 &lt;code&gt;Object.prototype&lt;/code&gt; 的空对象，它能够使用 &lt;code&gt;Object.prototype&lt;/code&gt; 上定义的一些方法，比如 &lt;code&gt;hasOwnProperty()&lt;/code&gt;，&lt;code&gt;toString()&lt;/code&gt; 等。他的结果和 &lt;code&gt;new Object()&lt;/code&gt;（可以传 &lt;code&gt;null&lt;/code&gt; 或者 &lt;code&gt;undefined&lt;/code&gt; 做参数） 或者 &lt;code&gt;Object.create(Object.prototype)&lt;/code&gt; 是一样的。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;Object.create(null)&lt;/code&gt; 是以继承自 &lt;code&gt;null&lt;/code&gt; 的对象，它是一个没有任何属性，包括 &lt;code&gt;[[prototype]]&lt;/code&gt;，包括原型的非常**“干净”** 的对象，和 &lt;code&gt;{__porot__: null}&lt;/code&gt; 是一样的（不建议用这种方式设置原型）。在 &lt;code&gt;chrome&lt;/code&gt; 中打印这个对象然后展开会显示 &lt;code&gt;No properties&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以他们唯一的区别就是一个有 &lt;code&gt;[[prototype]]&lt;/code&gt;，另一个没有。不过我个人认为这是没什么影响的，定义和原型上同名的方法会覆盖原型的方法，而且 &lt;code&gt;Object.prototype&lt;/code&gt; 上的所有属性和方法都是不可枚举的，所以 &lt;code&gt;for ... in&lt;/code&gt; 也是不会访问到多原型上的属性。两者之间应该也没有很大的性能差异。我也是在想不出有什么不得不用 &lt;code&gt;Object.create(null)&lt;/code&gt; 的场景，除非你不希望自己的对象上有除了自己定义以外的能访问的属性🤔。&lt;/p&gt;
&lt;p&gt;所以我觉得日常编码中使用哪个都可以，对象字面量肯定更方便。&lt;code&gt;Object.create(null)&lt;/code&gt; 可以创建一个更干净的对象，但是由于 &lt;code&gt;Object.prototpye&lt;/code&gt; 上的属性都不可枚举，其实也不会产生影响，可以放心使用。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object&quot; title=&quot;Object - MDN&quot;&gt;Object - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Object_initializer&quot; title=&quot;对象字面量&quot;&gt;对象字面量&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create&quot; title=&quot;Object.create()&quot;&gt;Object.create()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844903589815517192&quot; title=&quot;详解 Object.create(null)&quot;&gt;详解 Object.create(null)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>Symbol</title><link>https://clloz.com/blog/symbol</link><guid isPermaLink="true">https://clloz.com/blog/symbol</guid><pubDate>Thu, 10 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文主要讲一讲 &lt;code&gt;ES6&lt;/code&gt; 引入的原始数据类型 &lt;code&gt;Symbol&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;概念&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Symbol&lt;/code&gt; 本身的概念并不复杂，就一个独一无二的值。在 &lt;code&gt;ES6&lt;/code&gt; 之前，我们只能用字符串作为对象的属性名，这很容易造成属性名冲突。&lt;code&gt;Symbol&lt;/code&gt; 就是为了解决这种问题而产生的。&lt;/p&gt;
&lt;p&gt;注意 &lt;code&gt;Symbol&lt;/code&gt; 函数不是一个构造函数，不能用 &lt;code&gt;new&lt;/code&gt; 操作符。&lt;code&gt;Symbol()&lt;/code&gt; 函数会返回 &lt;code&gt;symbol&lt;/code&gt; 类型的值，该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象；它的静态方法会暴露全局的 &lt;code&gt;symbol&lt;/code&gt; 注册，且类似于内建对象类。&lt;/p&gt;
&lt;p&gt;创建一个 &lt;code&gt;Symbol&lt;/code&gt; 的语法是 &lt;code&gt;Symbol([description])&lt;/code&gt;，参数是可选的，字符串类型，是对 &lt;code&gt;symbol&lt;/code&gt; 的描述，可用于调试但不是访问 &lt;code&gt;symbol&lt;/code&gt; 本身。每个从 &lt;code&gt;Symbol()&lt;/code&gt; 返回的 &lt;code&gt;symbol&lt;/code&gt; 值都是唯一的。一个 &lt;code&gt;symbol&lt;/code&gt; 值能作为对象属性的标识符；这是该数据类型仅有的目的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，&lt;code&gt;Symbol&lt;/code&gt; 函数的参数只是表示对当前 &lt;code&gt;Symbol&lt;/code&gt; 值的描述，因此相同参数的 &lt;code&gt;Symbol&lt;/code&gt; 函数的返回值是不相等的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果 &lt;code&gt;Symbol&lt;/code&gt; 函数的参数是一个对象，就会调用该对象的 &lt;code&gt;toString&lt;/code&gt; 方法，将其转为字 符串，然后才生成一个 &lt;code&gt;Symbol&lt;/code&gt; 值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const obj = {
  toString() {
    return &apos;abc&apos;
  }
}
const sym = Symbol(obj)
sym // Symbol(abc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然不能用 &lt;code&gt;new&lt;/code&gt; 创建一个 &lt;code&gt;Symbol&lt;/code&gt; 对象，但是可以通过 &lt;code&gt;Object&lt;/code&gt; 方法获得一个包装对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var sym = Symbol(&apos;foo&apos;)
typeof sym // &quot;symbol&quot;
var symObj = Object(sym)
typeof symObj // &quot;object&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Symbol&lt;/code&gt; 作为属性名，该属性不会出现在 &lt;code&gt;for ... in&lt;/code&gt;、&lt;code&gt;for ... of&lt;/code&gt; 循环中，也不会被被 &lt;code&gt;Object.keys()&lt;/code&gt;、&lt;code&gt;Object.getOwnPropertyNames()&lt;/code&gt;、&lt;code&gt;JSON.toStringify()&lt;/code&gt; 返回。但是，它也不是私有属性，有一个 &lt;code&gt;Object.getOwnPropertySymbols()&lt;/code&gt; 放法，可以获取指定对象的所有 &lt;code&gt;Symbol&lt;/code&gt; 属性名。另一个新的 &lt;code&gt;API&lt;/code&gt;， &lt;code&gt;Reflect.ownKeys&lt;/code&gt; 方法可以返回所有类型的键名，包括常规键名和 &lt;code&gt;Symbol&lt;/code&gt; 键名。由于以 &lt;code&gt;Symbol&lt;/code&gt; 值作为名称的属性，不会被常规方法遍历得到。我们可以利用这个 特性，为对象定义一些非私有的、但又希望只用于内部的方法。&lt;/p&gt;
&lt;p&gt;对于用了 &lt;code&gt;Symbol&lt;/code&gt; 作为键名的对象，我们可以用 &lt;code&gt;Object.getOwnPropertySymbols()&lt;/code&gt; 方法查找对象的符号属性。该返回一个 &lt;code&gt;symbol&lt;/code&gt; 类型的数组。注意，每个初始化的对象都是没有自己的symbol属性的，因此这个数组可能为空，除非你已经在对象上设置了 &lt;code&gt;symbol&lt;/code&gt; 属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let obj = {
  [Symbol(&apos;clloz&apos;)]: &apos;clloz&apos;
}
console.log(Object.getOwnPropertySymbols(obj)) //[ Symbol(clloz) ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 &lt;code&gt;Symbol&lt;/code&gt; 的类型转换可以参考我的另一片文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/13/type-conversion/&quot; title=&quot;深入 JavaScript 类型转换&quot;&gt;深入 JavaScript 类型转换&lt;/a&gt;，这里做一个简单的总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尝试将一个 &lt;code&gt;symbol&lt;/code&gt; 值转换为一个 &lt;code&gt;number&lt;/code&gt; 值时，会抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 错误 (&lt;code&gt;e.g. +sym or sym | 0&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;使用宽松相等时， &lt;code&gt;Object(sym) == sym&lt;/code&gt; 返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol&lt;/code&gt; 值不能与其他类型的值进行运算，会报错，这会阻止你从一个 &lt;code&gt;symbol&lt;/code&gt; 值隐式地创建一个新的 &lt;code&gt;string&lt;/code&gt; 类型的属性名。例如，&lt;code&gt;Symbol(&quot;foo&quot;) + &quot;bar&quot;&lt;/code&gt; 将抛出一个&lt;code&gt;TypeError can&apos;t convert symbol to string&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol&lt;/code&gt; 值可以显式转为字符串，用 &lt;code&gt;String()&lt;/code&gt; 强制转换或者使用 &lt;code&gt;Symbol.prototype.toString()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol&lt;/code&gt; 值也可以转为布尔值，但是不能转为数值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;方法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Symbol&lt;/code&gt; 的静态方法有两个 &lt;code&gt;Symbol.for()&lt;/code&gt; 和 &lt;code&gt;Symbol.keyFor()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;Symbol()&lt;/code&gt; 函数创建的 &lt;code&gt;Symbol&lt;/code&gt;，不会在你的整个代码库中创建一个可用的全局的 &lt;code&gt;symbol&lt;/code&gt; 类型。 要创建跨文件可用的 &lt;code&gt;symbol&lt;/code&gt;，甚至跨域（每个都有它自己的全局作用域） , 使用 &lt;code&gt;Symbol.for()&lt;/code&gt; 方法和 &lt;code&gt;Symbol.keyFor()&lt;/code&gt; 方法从全局的 &lt;code&gt;symbol&lt;/code&gt; 注册表设置和取得 &lt;code&gt;symbol&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Symbol.for()&lt;/code&gt; 方法会根据给定的键 &lt;code&gt;key&lt;/code&gt;，来从运行时的 &lt;code&gt;symbol&lt;/code&gt; 注册表中找到对应的 &lt;code&gt;symbol&lt;/code&gt;，如果找到了，则返回它，否则，新建一个与该键关联的 &lt;code&gt;symbol&lt;/code&gt;，并放入全局 &lt;code&gt;symbol&lt;/code&gt; 注册表中。注意，&lt;code&gt;Symbol.for()&lt;/code&gt; 只能找到用 &lt;code&gt;Symbol.for()&lt;/code&gt; 创建的 &lt;code&gt;Symbol&lt;/code&gt;，不能找到用 &lt;code&gt;Symbol()&lt;/code&gt; 创建的 &lt;code&gt;Symbol&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Symbol.for()&lt;/code&gt; 和 &lt;code&gt;Symbol()&lt;/code&gt; 的区别是，&lt;code&gt;Symbol.for()&lt;/code&gt; 创建的 &lt;code&gt;Symbol&lt;/code&gt; 会被登记在全局环境中共搜索。&lt;code&gt;Symbol.fo()&lt;/code&gt; 不会每次都创建一个新的 &lt;code&gt;Symbol&lt;/code&gt;，只会在搜索不到的时候创建。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Symbol.for(&apos;foo&apos;) // 创建一个 symbol 并放入 symbol 注册表中，键为 &quot;foo&quot;
Symbol.for(&apos;foo&apos;) // 从 symbol 注册表中读取键为&quot;foo&quot;的 symbol

Symbol.for(&apos;bar&apos;) === Symbol.for(&apos;bar&apos;) // true，证明了上面说的
Symbol(&apos;bar&apos;) === Symbol(&apos;bar&apos;) // false，Symbol() 函数每次都会返回新的一个 symbol

var sym = Symbol.for(&apos;mario&apos;)
sym.toString()
// &quot;Symbol(mario)&quot;，mario 既是该 symbol 在 symbol 注册表中的键名，又是该 symbol 自身的描述字符串

//为了防止冲突，最好为键名设置前缀
Symbol.for(&apos;mdn.foo&apos;)
Symbol.for(&apos;mdn.bar&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Symbol.keyFor(sym)&lt;/code&gt; 方法用来获取全局 &lt;code&gt;symbol&lt;/code&gt; 注册表中与某个 &lt;code&gt;symbol&lt;/code&gt; 关联的键。即参数是一个 &lt;code&gt;Symbol&lt;/code&gt;，如果这个 &lt;code&gt;Symbol&lt;/code&gt; 是用 &lt;code&gt;Symbol.for()&lt;/code&gt; 在全局注册的，则返回这个 &lt;code&gt;Symbol&lt;/code&gt; 的描述符，一个字符串。若没找到则返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let symbol1 = Symbol(&apos;clloz&apos;)
let symbol2 = Symbol.for(&apos;clloz&apos;)
console.log(Symbol.keyFor(symbol1)) //undefined
console.log(Symbol.keyFor(symbol2)) //clloz
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;Symbol()&lt;/code&gt; 有原型 &lt;code&gt;Symbol.prototype&lt;/code&gt;，你可以使用构造函数的原型对象来给所有 &lt;code&gt;Symbol&lt;/code&gt; 实例添加属性或者方法。&lt;code&gt;Symbol.prototype&lt;/code&gt; 默认有一个数姓 &lt;code&gt;Symbol.prototype.description&lt;/code&gt;，返回对应 &lt;code&gt;Symbol&lt;/code&gt; 的描述符。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Symbol.prototype&lt;/code&gt; 有几个方法，&lt;code&gt;Symbol.prototype.valueOf()&lt;/code&gt;，&lt;code&gt;Symbol.Prototype.toString()&lt;/code&gt; 和 &lt;code&gt;Symbol.prototype[Symbol.toPrimitive](hint)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;toString()&lt;/code&gt; 方法返回当前 &lt;code&gt;symbol&lt;/code&gt; 对象的字符串表示。&lt;code&gt;symbol&lt;/code&gt; 原始值不能转换为字符串，所以只能先转换成它的包装对象，再调用 &lt;code&gt;toString()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Symbol(&apos;foo&apos;) + &apos;bar&apos;
// TypeError: Can&apos;t convert symbol to string
Symbol(&apos;foo&apos;).toString() + &apos;bar&apos;
// &quot;Symbol(foo)bar&quot;，就相当于下面的:
Object(Symbol(&apos;foo&apos;)).toString() + &apos;bar&apos;
// &quot;Symbol(foo)bar&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;valueOf()&lt;/code&gt; 方法返回当前 &lt;code&gt;symbol&lt;/code&gt; 对象所包含的 &lt;code&gt;symbol&lt;/code&gt; 原始值。在 &lt;code&gt;JavaScript&lt;/code&gt; 中，虽然大多数类型的对象在某些操作下都会自动的隐式调用自身的 &lt;code&gt;valueOf()&lt;/code&gt; 方法或者 &lt;code&gt;toString()&lt;/code&gt; 方法来将自己转换成一个原始值，但 &lt;code&gt;symbol&lt;/code&gt; 对象不会这么干，&lt;code&gt;symbol&lt;/code&gt; 对象无法隐式转换成对应的原始值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Object(Symbol(&apos;foo&apos;)) + &apos;bar&apos;
// TypeError: can&apos;t convert symbol object to primitive
// 无法隐式的调用 valueOf() 方法

Object(Symbol(&apos;foo&apos;)).valueOf() + &apos;bar&apos;
// TypeError:  can&apos;t convert symbol to string
// 手动调用 valueOf() 方法，虽然转换成了原始值，但 symbol 原始值不能转换为字符串

Object(Symbol(&apos;foo&apos;)).toString() + &apos;bar&apos;
// &quot;Symbol(foo)bar&quot;，需要手动调用 toString() 方法才行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 &lt;code&gt;Symbol.prototype[Symbol.toPrimitive](hint)&lt;/code&gt; 参考 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/13/type-conversion/&quot; title=&quot;深入 JavaScript 类型转换&quot;&gt;深入 JavaScript 类型转换&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;属性&lt;/h2&gt;
&lt;p&gt;除了我们自己创建的 &lt;code&gt;Symbol&lt;/code&gt;，&lt;code&gt;JavaScript&lt;/code&gt; 还内建了一些在 &lt;code&gt;ECMAScript 5&lt;/code&gt; 之前没有暴露给开发者的 &lt;code&gt;symbol&lt;/code&gt;，它们代表了内部语言行为。在标准中他们以 &lt;code&gt;@@&lt;/code&gt; 开头替代 &lt;code&gt;Symbol&lt;/code&gt;，被称为 &lt;a href=&quot;https://tc39.es/ecma262/#sec-well-known-symbols&quot; title=&quot;Well-Known Symbols&quot;&gt;Well-Known Symbols&lt;/a&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Symbol.iterator&lt;/code&gt;：一个返回一个对象默认迭代器的方法。被 &lt;code&gt;for...of&lt;/code&gt; 使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.asyncIterator&lt;/code&gt;: 一个返回对象默认的异步迭代器的方法。被 &lt;code&gt;for await of&lt;/code&gt; 使用。&lt;strong&gt;试验性API&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.match&lt;/code&gt;: 一个用于对字符串进行匹配的方法，也用于确定一个对象是否可以作为正则表达式使用。被 &lt;code&gt;String.prototype.match()&lt;/code&gt; 使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.replace&lt;/code&gt;: 一个替换匹配字符串的子串的方法. 被 &lt;code&gt;String.prototype.replace()&lt;/code&gt; 使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.search&lt;/code&gt;: 一个返回一个字符串中与正则表达式相匹配的索引的方法。被 &lt;code&gt;String.prototype.search()&lt;/code&gt; 使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.split&lt;/code&gt;: 一个在匹配正则表达式的索引处拆分一个字符串的方法.。被 &lt;code&gt;String.prototype.split()&lt;/code&gt; 使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.hasInstance&lt;/code&gt;: 一个确定一个构造器对象识别的对象是否为它的实例的方法。被 &lt;code&gt;instanceof&lt;/code&gt; 使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.isConcatSpreadable&lt;/code&gt;: 一个布尔值，表明一个对象是否应该 &lt;code&gt;flattened&lt;/code&gt; 为它的数组元素。被 &lt;code&gt;Array.prototype.concat()&lt;/code&gt; 使用。简单说就是表示该对象用于 &lt;code&gt;Array.prototype.concat()&lt;/code&gt; 时，是否可以展开。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.unscopables&lt;/code&gt;: 拥有和继承属性名的一个对象的值被排除在与环境绑定的相关对象外。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.species&lt;/code&gt;: 一个用于创建派生对象的构造器函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.toPrimitive&lt;/code&gt;: 一个将对象转化为基本数据类型的方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.toStringTag&lt;/code&gt;: 用于对象的默认描述的字符串值。被 &lt;code&gt;Object.prototype.toString()&lt;/code&gt; 使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以看到我们原来使用的一些方法其实就是调用的这些 &lt;code&gt;Symbol&lt;/code&gt; 对应的方法。现在 &lt;code&gt;JavaScript&lt;/code&gt; 将这些 &lt;code&gt;Symbol&lt;/code&gt; 暴露出来我们可以自己配置这些 &lt;code&gt;Symbol&lt;/code&gt;。在 &lt;code&gt;ES6&lt;/code&gt; 之前，当我们执行这些 &lt;code&gt;Symbol&lt;/code&gt; 对应的操作的时候，调用的是引擎内部的默认方法，现在这些 &lt;code&gt;Symbol&lt;/code&gt; 暴露出来以后，我们可以自己配置这些 &lt;code&gt;Symbol&lt;/code&gt; 对应的一些行为。&lt;/p&gt;
&lt;h2&gt;Symbol.hasInstance&lt;/h2&gt;
&lt;p&gt;当我们执行 &lt;code&gt;A instanceof B&lt;/code&gt; 的时候其实就是调用的 &lt;code&gt;B&lt;/code&gt; 内部的 &lt;code&gt;Symbol.hasInstance&lt;/code&gt; 方法。我们可以自己定义对象内部的这个方法，改变对象的行为。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array
  }
}
console.log([1, 2, 3] instanceof new MyClass()) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbol.isConcatSpreadable&lt;/h2&gt;
&lt;p&gt;对象的 &lt;code&gt;Symbol.isConcatSpreadable&lt;/code&gt; 属性等于一个布尔值，表示该对象用于 &lt;code&gt;Array.prototype.concat()&lt;/code&gt; 时，是否可以展开。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let arr1 = [&apos;c&apos;, &apos;d&apos;]
console.log([&apos;a&apos;, &apos;b&apos;].concat(arr1, &apos;e&apos;)) // [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;, &apos;e&apos;]
console.log(arr1[Symbol.isConcatSpreadable]) // undefined

let arr2 = [&apos;c&apos;, &apos;d&apos;]
arr2[Symbol.isConcatSpreadable] = false
console.log([&apos;a&apos;, &apos;b&apos;].concat(arr2, &apos;e&apos;))
//[&apos;a&apos;, &apos;b&apos;, [&apos;c&apos;, &apos;d&apos;, ([Symbol(Symbol.isConcatSpreadable)]: false)], &apos;e&apos;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码说明，数组的默认行为是可以展开，&lt;code&gt;Symbol.isConcatSpreadable&lt;/code&gt; 默认等于 &lt;code&gt;undefined&lt;/code&gt; 。该属性等于 &lt;code&gt;true&lt;/code&gt; 时，也有展开的效果。当我们把数组的 &lt;code&gt;Symbol.isConcatSpreadable&lt;/code&gt; 设置为 &lt;code&gt;false&lt;/code&gt;，在调用 &lt;code&gt;cancat&lt;/code&gt; 发现没有展开。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let obj = { length: 2, 0: &apos;c&apos;, 1: &apos;d&apos; }
console.log([&apos;a&apos;, &apos;b&apos;].concat(obj, &apos;e&apos;)) // [ &apos;a&apos;, &apos;b&apos;, { &apos;0&apos;: &apos;c&apos;, &apos;1&apos;: &apos;d&apos;, length: 2 }, &apos;e&apos; ]

obj[Symbol.isConcatSpreadable] = true
console.log([&apos;a&apos;, &apos;b&apos;].concat(obj, &apos;e&apos;)) // [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;, &apos;e&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似数组的对象正好相反，默认不展开。将它的 &lt;code&gt;Symbol.isConcatSpreadable&lt;/code&gt; 属性设为 &lt;code&gt;true&lt;/code&gt; ，就可以展开。&lt;/p&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class A1 extends Array {
  constructor(args) {
    super(args)
    this[Symbol.isConcatSpreadable] = true
  }
}
class A2 extends Array {
  constructor(args) {
    super(args)
  }
  get [Symbol.isConcatSpreadable]() {
    return false
  }
}
let a1 = new A1()
a1[0] = 3
a1[1] = 4
let a2 = new A2()
a2[0] = 5
a2[1] = 6
console.log([1, 2].concat(a1).concat(a2)) // [ 1, 2, 3, 4, A2(2) [ 5, 6 ] ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以在类中设置这个属性，可以在构造函数中设置，也可以直接作为原型的访问器属性。&lt;/p&gt;
&lt;h2&gt;Symbol.species&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 有个特性，当我们用一个 &lt;code&gt;MyArray&lt;/code&gt; 继承 &lt;code&gt;Array&lt;/code&gt; 的时候，用 &lt;code&gt;MyArray&lt;/code&gt; 创建一个数组，然后用 &lt;code&gt;map&lt;/code&gt; 生成一个新的数组。这个新数组的创建调用的是 &lt;code&gt;MyArray&lt;/code&gt; 构造函数，而不是 &lt;code&gt;Array&lt;/code&gt; 构造函数。这是非常有用的，因为新数组还可以用我们在 &lt;code&gt;MyArray&lt;/code&gt; 上定义的方法。但是如果我们希望新数组是以 &lt;code&gt;Array&lt;/code&gt; 为构造函数创建的话，就需要用到 &lt;code&gt;Symbol.species&lt;/code&gt; 这个访问器属性，它能够修改派生对象的构造函数。&lt;strong&gt;注意，使用 &lt;code&gt;new&lt;/code&gt; 操作符的时候依然是创建 &lt;code&gt;MyArray&lt;/code&gt; 的实例。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class MyArray extends Array {
  // 覆盖 species 到父级的 Array 构造函数上
  static get [Symbol.species]() {
    return Array
  }
}

const a = new MyArray(1, 2, 3)
console.log(a instanceof MyArray) //true
console.log(a instanceof Array) //true

const mapped = a.map((x) =&gt; x * x)

console.log(mapped instanceof MyArray)
// expected output: false

console.log(mapped instanceof Array)
// expected output: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，子类 &lt;code&gt;MyArray&lt;/code&gt; 继承了父类 &lt;code&gt;Array&lt;/code&gt; 。用 &lt;code&gt;new&lt;/code&gt; 操作符创建的实例 &lt;code&gt;a&lt;/code&gt; 是 &lt;code&gt;MyArray&lt;/code&gt; 的实例。但是用 &lt;code&gt;map&lt;/code&gt; 创建的实例却不是 &lt;code&gt;MyArray&lt;/code&gt; 的实例，而是 &lt;code&gt;Array&lt;/code&gt; 的实例。这个例子也说明，定义 &lt;code&gt;Symbol.species&lt;/code&gt; 属性要采用 &lt;code&gt;get&lt;/code&gt; 读取器。默认 的 &lt;code&gt;Symbol.species&lt;/code&gt; 属性等同于下面的写法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class MyArray extends Array {
  static get [Symbol.species]() {
    return this
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbol.match&lt;/h2&gt;
&lt;p&gt;对象的 &lt;code&gt;Symbol.match&lt;/code&gt; 属性，指向一个函数。当执行 &lt;code&gt;str.match(RegExp)&lt;/code&gt; 时，如果该属性存在，会调用它，返回该方法的返回值。这个方法用于确定一个对象是否可以作为正则表达式使用，&lt;code&gt;String.prototype.match()&lt;/code&gt; 默认就是找参数的这个方法。&lt;/p&gt;
&lt;p&gt;有了这个属性之后，即使不是正则对象，我们也可以让 &lt;code&gt;String.prototype.match()&lt;/code&gt; 正常执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)

class MyMatcher {
  [Symbol.match](string) {
    return &apos;hello world&apos;.indexOf(string)
  }
}
&apos;e&apos;.match(new MyMatcher()) // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbol.replace&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Symbol.replace&lt;/code&gt; 一个替换匹配字符串的子串的方法。当对象被 &lt;code&gt;String.prototype.replace()&lt;/code&gt; 方法调用时，会调用对象上的 &lt;code&gt;Symbol.replace&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue) // this 就是调用 replace 的字符串
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了这个属性，即使不是正则对象，&lt;code&gt;String.prototype.replace()&lt;/code&gt; 也能正常执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const x = {}
x[Symbol.replace] = (...s) =&gt; console.log(s)
&apos;Hello&apos;.replace(x, &apos;World&apos;) // [&quot;Hello&quot;, &quot;World&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbol.search&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Symbol.search&lt;/code&gt; 指向一个返回一个字符串中与正则表达式相匹配的索引的方法。被 &lt;code&gt;String.prototype.search()&lt;/code&gt; 使用。当对象被 &lt;code&gt;String.prototype.search()&lt;/code&gt; 方法调用时，会调用对象的这个方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;String.prototype.search(regexp) // 等同于 regexp[Symbol.search](this)
class MySearch {
  constructor(value) {
    this.value = value
  }
  [Symbol.search](string) {
    return string.indexOf(this.value)
  }
}
&apos;foobar&apos;.search(new MySearch(&apos;foo&apos;)) // 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbol.split&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Symbol.split&lt;/code&gt; 指向一个在匹配正则表达式的索引处拆分一个字符串的方法.。被 &lt;code&gt;String.prototype.split()&lt;/code&gt; 使用。当对象被 &lt;code&gt;String.prototype.split()&lt;/code&gt; 方法调用时，会调用对象的这个方法。&lt;code&gt;split&lt;/code&gt; 的第一个参数可以是字符串也可以是正则表达式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了这个属性，我们可以定制 &lt;code&gt;split&lt;/code&gt; 的行为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class MySplitter {
  constructor(value) {
    this.value = value
  }
  [Symbol.split](string) {
    var index = string.indexOf(this.value)
    if (index === -1) {
      return string
    }
    return [string.substr(0, index), string.substr(index + this.value.length)]
  }
}
&apos;foobar&apos;.split(new MySplitter(&apos;foo&apos;))
// [&apos;&apos;, &apos;bar&apos;]
&apos;foobar&apos;.split(new MySplitter(&apos;bar&apos;)) // [&apos;foo&apos;, &apos;&apos;]
&apos;foobar&apos;.split(new MySplitter(&apos;baz&apos;)) // &apos;foobar&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbol.Iterator&lt;/h2&gt;
&lt;p&gt;这应该是最重要的一个内置 &lt;code&gt;Symbol&lt;/code&gt;，我会在别的文章单独讨论，这里简单说一下。该属性指向一个返回一个对象默认迭代器的方法。被 &lt;code&gt;for...of&lt;/code&gt; 和扩展运算符等需要进行迭代是使用。很多内置类型都有默认的 &lt;code&gt;@@iterator&lt;/code&gt; 方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Array.prototype[@@iterator]()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TypedArray.prototype[@@iterator]()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String.prototype[@@iterator]()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Map.prototype[@@iterator]()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Set.prototype[@@iterator]()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自定义迭代器：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var myIterable = {}
myIterable[Symbol.iterator] = function* () {
  yield 1
  yield 2
  yield 3
}
;[...myIterable] // [1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbol.toPrimitive&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Symbol.toPrimitive&lt;/code&gt; 指向一个将对象转化为基本数据类型的方法，当一个对象转换为对应的原始值时，会调用此函数。这个方法我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/13/type-conversion/&quot; title=&quot;深入 JavaScript 类型转换&quot;&gt;深入 JavaScript 类型转换&lt;/a&gt; 中已经详细介绍过了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Symbol.toPrimitive&lt;/code&gt; 被调用时，一个对象可被转换为原始值。该函数被调用时，会被传递一个字符串参数 &lt;code&gt;hint&lt;/code&gt; ，表示要转换到的原始值的预期类型。 &lt;code&gt;hint&lt;/code&gt; 参数的取值是 &lt;code&gt;number&lt;/code&gt;、&lt;code&gt;string&lt;/code&gt; 和 &lt;code&gt;default&lt;/code&gt; 中的任意一个。详细的解析参考上面的那篇文章，这里放一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 一个没有提供 Symbol.toPrimitive 属性的对象，参与运算时的输出结果
var obj1 = {}
console.log(+obj1) // NaN
console.log(`{obj1}`) // &quot;[object Object]&quot;
console.log(obj1 + &apos;&apos;) // &quot;[object Object]&quot;

// 接下面声明一个对象，手动赋予了 Symbol.toPrimitive 属性，再来查看输出结果
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if (hint == &apos;number&apos;) {
      return 10
    }
    if (hint == &apos;string&apos;) {
      return &apos;hello&apos;
    }
    return true
  }
}
console.log(+obj2) // 10      -- hint 参数值是 &quot;number&quot;
console.log(`{obj2}`) // &quot;hello&quot; -- hint 参数值是 &quot;string&quot;
console.log(obj2 + &apos;&apos;) // &quot;true&quot;  -- hint 参数值是 &quot;default&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbol.toStringTag&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Symbol.toStringTag&lt;/code&gt; 是一个内置 &lt;code&gt;symbol&lt;/code&gt;，它通常作为对象的属性键使用，对应的属性值应该为字符串类型，这个字符串用来表示该对象的自定义类型标签，通常只有内置的 &lt;code&gt;Object.prototype.toString()&lt;/code&gt; 方法会去读取这个标签并把它包含在自己的返回值里。&lt;/p&gt;
&lt;p&gt;许多内置的 &lt;code&gt;JavaScript&lt;/code&gt; 对象类型即便没有 &lt;code&gt;toStringTag&lt;/code&gt; 属性，也能被 &lt;code&gt;toString()&lt;/code&gt; 方法识别并返回特定的类型标签，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Object.prototype.toString.call(&apos;foo&apos;) // &quot;[object String]&quot;
Object.prototype.toString.call([1, 2]) // &quot;[object Array]&quot;
Object.prototype.toString.call(3) // &quot;[object Number]&quot;
Object.prototype.toString.call(true) // &quot;[object Boolean]&quot;
Object.prototype.toString.call(undefined) // &quot;[object Undefined]&quot;
Object.prototype.toString.call(null) // &quot;[object Null]&quot;
// ... and more
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外一些对象类型则不然，&lt;code&gt;toString()&lt;/code&gt; 方法能识别它们是因为引擎为它们设置好了 &lt;code&gt;toStringTag&lt;/code&gt; 标签：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Object.prototype.toString.call(new Map()) // &quot;[object Map]&quot;
Object.prototype.toString.call(function* () {}) // &quot;[object GeneratorFunction]&quot;
Object.prototype.toString.call(Promise.resolve()) // &quot;[object Promise]&quot;
// ... and more
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但你自己创建的类不会有这份特殊待遇，&lt;code&gt;toString()&lt;/code&gt; 找不到 &lt;code&gt;toStringTag&lt;/code&gt; 属性时只好返回默认的 &lt;code&gt;Object&lt;/code&gt; 标签：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class ValidatorClass {}

Object.prototype.toString.call(new ValidatorClass()) // &quot;[object Object]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加上 &lt;code&gt;toStringTag&lt;/code&gt; 属性，你的类也会有自定义的类型标签了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class ValidatorClass {
  get [Symbol.toStringTag]() {
    return &apos;Validator&apos;
  }
}

Object.prototype.toString.call(new ValidatorClass()) // &quot;[object Validator]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbol.unscopables&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Symbol.unscopables&lt;/code&gt; 指向一个对象。该对象指定了使用 &lt;code&gt;with&lt;/code&gt; 关 键字时，哪些属性会被 &lt;code&gt;with&lt;/code&gt; 环境排除。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(Array.prototype[Symbol.unscopables])
// [Object: null prototype] {
//     copyWithin: true,
//     entries: true,
//     fill: true,
//     find: true,
//     findIndex: true,
//     flat: true,
//     flatMap: true,
//     includes: true,
//     keys: true,
//     values: true
//   }
console.log(Object.keys(Array.prototype[Symbol.unscopables]))
// [
//     &apos;copyWithin&apos;, &apos;entries&apos;,
//     &apos;fill&apos;,       &apos;find&apos;,
//     &apos;findIndex&apos;,  &apos;flat&apos;,
//     &apos;flatMap&apos;,    &apos;includes&apos;,
//     &apos;keys&apos;,       &apos;values&apos;
//   ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码说明，数组有 &lt;code&gt;7&lt;/code&gt; 个属性，会被 &lt;code&gt;with&lt;/code&gt; 命令排除。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 没有 unscopables 时
class MyClass {
  foo() {
    return 1
  }
}
var foo = function () {
  return 2
}
with (MyClass.prototype) {
  console.log(foo()) // 1
}

// 有 unscopables 时
class MyClass {
  foo() {
    return 1
  }
  get [Symbol.unscopables]() {
    return { foo: true }
  }
}
var foo = function () {
  return 2
}
with (MyClass.prototype) {
  console.log(foo()) // 2
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>JavaScript对象属性类型和赋值细节</title><link>https://clloz.com/blog/javascript-object-prop-assign</link><guid isPermaLink="true">https://clloz.com/blog/javascript-object-prop-assign</guid><pubDate>Wed, 09 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在研究 &lt;code&gt;JavaScript&lt;/code&gt; 中深浅拷贝的方式的时候遇到一个违反我直觉的内容，就是 &lt;code&gt;JavaScript&lt;/code&gt; 对象在和原型对象的情况下的赋值行为。本文介绍一下这部分的一些细节。&lt;/p&gt;
&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;在研究 &lt;code&gt;Object.create&lt;/code&gt; 方法的时候，发现用 &lt;code&gt;Object.create&lt;/code&gt; 创建的对象的时候，给访问到的原型中的属性赋值的时候会在新创建的对象中新建这个属性。看如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let Obj = {
  a: 1,
  b: 2
}

let obj = Object.create(Obj)

obj.a = 10

console.log(obj, Obj) //{ a: 10 } { a: 1, b: 2 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我也曾使用这个方法来复制对象的属性。但是今天仔细看里面的细节，发现这是违反我直觉的。我自己的思路是，&lt;code&gt;obj&lt;/code&gt; 对象没有 &lt;code&gt;a&lt;/code&gt; 属性，所以访问到的是原型 &lt;code&gt;Obj&lt;/code&gt; 上的 &lt;code&gt;a&lt;/code&gt; 属性，那么我修改属性的时候应该修改的也是原型上的属性。但是实际情况是一个 &lt;code&gt;a&lt;/code&gt; 属性在 &lt;code&gt;obj&lt;/code&gt; 对象上创建，原型上的 &lt;code&gt;a&lt;/code&gt; 属性还保持原来的状态。&lt;/p&gt;
&lt;p&gt;其实仔细想一下，这种处理才是合理的。原型存在的目的是为了继承，继承的目的本质也是为了复用。而用来复用的方法或者属性随便就被修改了，会影响到很多其他对象。所以 &lt;code&gt;JavaScript&lt;/code&gt; 的这种处理是合理的。&lt;/p&gt;
&lt;p&gt;我们可以把原型中的属性认为是一个&lt;strong&gt;默认值&lt;/strong&gt;，当我们的对象没有对应属性的时候，原型能够提供一个默认值给我们，而默认值是不应该随便被修改的。甚至当我们用 &lt;code&gt;delete&lt;/code&gt; 删除对象的属性的时候，原型上的同名属性依然是可以访问的，这也正是原型的意义。而且仔细想一想，非常频繁被使用的赋值操作都可以修改原型上的属性的话，将会是非常危险的。&lt;/p&gt;
&lt;h2&gt;深入&lt;/h2&gt;
&lt;p&gt;上面我们对属性的赋值的行为举了一个例子，在参考了网络上的其他文章后，我发现这个简单的赋值行为其实还有更多可以研究的行为。&lt;/p&gt;
&lt;h2&gt;属性类型&lt;/h2&gt;
&lt;p&gt;在分析具体的情况之前我们先说一下 &lt;code&gt;JavaScript&lt;/code&gt; 中对象的属性。&lt;code&gt;JavaScript&lt;/code&gt; 中属性分为两种类型，一种是数据属性 &lt;code&gt;data properties&lt;/code&gt;，一种是访问器属性 &lt;code&gt;accessor properties&lt;/code&gt;。&lt;code&gt;JavaScript&lt;/code&gt; 标准还定义了一些用来描述属性的 &lt;strong&gt;特性&lt;/strong&gt; &lt;code&gt;attributes&lt;/code&gt;。属性的精确描述方式称为属性描述符 &lt;code&gt;properties descriptor&lt;/code&gt;，也是 &lt;code&gt;Object.defineProperty()&lt;/code&gt; 方法的第三个参数，数据属性的描述符称为 &lt;code&gt;data descriptor&lt;/code&gt;，访问器属性的描述符称为 &lt;code&gt;accessor descriptor&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;属性描述符其实就是对属性的精确定义，数据描述符是一个具有值的属性，该值可以是可写的，也可以是不可写的。访问器描述符是由 &lt;code&gt;getter&lt;/code&gt; 函数和 &lt;code&gt;setter&lt;/code&gt; 函数所描述的属性。一个描述符只能是这两者其中之一，不能同时是两者。这两种描述符都是对象。&lt;/p&gt;
&lt;p&gt;数据描述符和访问器描述符都支持支持以下两个 &lt;code&gt;attribute&lt;/code&gt; 描述属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;configurable&lt;/code&gt;：特性表示对象的属性是否可以被删除，以及除 &lt;code&gt;value&lt;/code&gt; 和 &lt;code&gt;writable&lt;/code&gt; 特性外的其他特性是否可以被修改。。默认为 &lt;code&gt;false&lt;/code&gt;。&lt;code&gt;configurable&lt;/code&gt; 属性设置为 &lt;code&gt;false&lt;/code&gt;，则该属性被认为是 &lt;strong&gt;不可配置的&lt;/strong&gt;，并且没有属性可以被改变（除了单向改变 &lt;code&gt;writable&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt;）。当属性不可配置时，不能在数据和访问器属性类型之间切换。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enumerable&lt;/code&gt;：当且仅当该属性的 &lt;code&gt;enumerable&lt;/code&gt; 键值为 &lt;code&gt;true&lt;/code&gt; 时，该属性才会出现在对象的枚举属性中。默认为 &lt;code&gt;false&lt;/code&gt;。定义了对象的属性是否可以在 &lt;code&gt;for...in&lt;/code&gt; 循环和 &lt;code&gt;Object.keys()&lt;/code&gt; 中被枚举。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据描述符还支持两个独占的 &lt;code&gt;attribute&lt;/code&gt; 来描述属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt;：该属性对应的值。可以是任何有效的 &lt;code&gt;JavaScript&lt;/code&gt; 值（数值，对象，函数等）。默认为 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writable&lt;/code&gt;：当且仅当该属性的 &lt;code&gt;writable&lt;/code&gt; 键值为 &lt;code&gt;true&lt;/code&gt; 时，属性的值，也就是上面的 &lt;code&gt;value&lt;/code&gt;，才能被赋值运算符改变。默认为 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;访问器属性也有两个独占的 &lt;code&gt;attribute&lt;/code&gt; （两个都是函数）来描述属性：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;get&lt;/code&gt;：属性的 &lt;code&gt;getter&lt;/code&gt; 函数，如果没有 &lt;code&gt;getter&lt;/code&gt;，则为 &lt;code&gt;undefined&lt;/code&gt;。当访问该属性时，会调用此函数。执行时不传入任何参数，但是会传入 &lt;code&gt;this&lt;/code&gt; 对象（由于继承关系，这里的 &lt;code&gt;this&lt;/code&gt; 并不一定是定义该属性的对象，比如是从原型脸上访问到的 &lt;code&gt;get&lt;/code&gt;）。该函数的返回值会被用作属性的值。默认为 &lt;code&gt;undefined&lt;/code&gt;。 &lt;code&gt;set&lt;/code&gt;：属性的 &lt;code&gt;setter&lt;/code&gt; 函数，如果没有 &lt;code&gt;setter&lt;/code&gt;，则为 &lt;code&gt;undefined&lt;/code&gt;。当属性值被修改时，会调用此函数。该方法接受一个参数（也就是被赋予的新值），会传入赋值时的 &lt;code&gt;this&lt;/code&gt; 对象。默认为 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 并一定要成对出现，只指定 &lt;code&gt;getter&lt;/code&gt; 意味着属性是不能写，尝试写入属性会被忽略。 在严格模式下，尝试写入只指定了 &lt;code&gt;getter&lt;/code&gt; 函数的属性会抛出错误。类似地，只指定 &lt;code&gt;setter&lt;/code&gt; 函数的属性也不能读，否则在非严格模式下会返回 &lt;code&gt;undefined&lt;/code&gt;，而在严格模式下会抛出错误。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// this 指向
function myclass() {}

Object.defineProperty(myclass.prototype, &apos;x&apos;, {
  get() {
    return this.stored_x
  },
  set(x) {
    this.stored_x = x
  }
})

var a = new myclass()
var b = new myclass()
a.x = 1 //this 是 a
console.log(b.x) // undefined //this 是 b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拥有布尔值的特性 &lt;code&gt;configurable&lt;/code&gt;、&lt;code&gt;enumerable&lt;/code&gt; 和 &lt;code&gt;writable&lt;/code&gt; 的默认值都是 &lt;code&gt;false&lt;/code&gt;。属性值和函数的键 &lt;code&gt;value&lt;/code&gt;、&lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 字段的默认值为 &lt;code&gt;undefined&lt;/code&gt;。默认值在描述符省略某些字段时启用。对于直接用对象字面量或属性访问器（点运算符或者方括号运算符）赋值的方式（比如 &lt;code&gt;obj.a = 10&lt;/code&gt; ）创建的属性其数据描述符中的属性的默认值和 &lt;code&gt;Object.defineProperty()&lt;/code&gt; 方法是不同的，参考如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = {
  m: 1,
  set t(arg) {}
}
console.log(Object.getOwnPropertyDescriptors(a))

//{
//  m: { value: 1, writable: true, enumerable: true, configurable: true },
//  t: {
//      get: undefined,
//      set: [Function: set t],
//      enumerable: true,
//      configurable: true
//  }
//}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个 &lt;code&gt;configurable&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 的属性是可以在数据属性和访问器属性之间切换，方法就是用 &lt;code&gt;Object.defineProperty()&lt;/code&gt; 方法重新定义一个同名属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = {}
a.m = 10 //字面量定义，所有的布尔型 attribute 都为 true

Object.defineProperty(a, &apos;m&apos;, {
  get() {
    return 100
  }
})

console.log(a.m) //100

Object.defineProperty(a, &apos;m&apos;, {
  value: 20
})

console.log(a.m) //20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一个描述符不具有 &lt;code&gt;value&lt;/code&gt;、&lt;code&gt;writable&lt;/code&gt;、&lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 中的任意一个键，那么它将被认为是一个数据描述符。如果一个描述符同时拥有 &lt;code&gt;value&lt;/code&gt; 或 &lt;code&gt;writable&lt;/code&gt; 和 &lt;code&gt;get&lt;/code&gt; 或 &lt;code&gt;set&lt;/code&gt; 键，则会产生一个异常。&lt;/p&gt;
&lt;p&gt;无论是数据属性还是访问器属性，都是可以从原型上继承的。如果原型上是一些不希望被修改的默认值，可以用 &lt;code&gt;Object.freeze&lt;/code&gt; 冻结源性对象，防止后续代码添加或删除对象原型的属性。&lt;/p&gt;
&lt;p&gt;如果原型上有了同名的访问器属性，那么你无法用属性访问器（点运算符或者方括号运算符）赋值的方式，比如 &lt;code&gt;obj.a = 10&lt;/code&gt;，在子对象上创建同名属性（只能用 &lt;code&gt;Object.defineProperty() 方法&lt;/code&gt;），在子对象上访问或者修改这个属性都会调用原型上的 &lt;code&gt;get&lt;/code&gt; 或者 &lt;code&gt;set&lt;/code&gt; 方法（如果只指定了一个，那么行为参考上面的 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 部分）。和访问器属性不一样，数据属性始终在对象自身上设置，而不会影响到原型上的属性。但如果一个不可写的属性被继承，它仍然可以防止修改对象的属性。这也是我们这篇文章讨论的重点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//只设置set
let value = 10
let a = {
  set m(m) {
    value = m
  }
}
let b = Object.create(a)
console.log(a.m) //没有设置get 返回undefined 严格模式下报错
console.log(b.m) //b对象没有m属性，调用a的get方法。返回undefined，同上
Object.defineProperty(b, &apos;m&apos;, {
  value: 100,
  writable: true,
  configurable: true,
  enmerable: true
})
console.log(b) //{m: 100}
console.log(b.m, a.m) //100 undefined

//只设置get
let value = 10
let a = {
  get m() {
    return value
  }
}
let b = Object.create(a)
console.log(a.m) //10
a.m = 100 //没有设置set，赋值会被忽略，严格模式下报错
console.log(a.m) //10

console.log(b.m) //10, b上面没有m属性，返回a.m
b.m = 100 //属性访问器（点运算符或方括号运算符）无法创建同名属性
console.log(b.m) //10, 依然返回a.m
Object.defineProperty(b, &apos;m&apos;, {
  value: 100,
  writable: true,
  configurable: true,
  enmerable: true
})
console.log(b, b.m) //{m:100} 100
console.log(a.m) //10 a对象不受影响
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们平时可能使用数据属性比较多，但是其访问器属性也有很多应用场景。比如我们属性的 &lt;code&gt;Vue&lt;/code&gt; 的双向数据绑定就是用 &lt;code&gt;set&lt;/code&gt; 实现的。&lt;/p&gt;
&lt;h2&gt;赋值行为&lt;/h2&gt;
&lt;h3&gt;原型链上没有同名属性&lt;/h3&gt;
&lt;p&gt;这是最简单的情况，会直接在子对象上创建一个新的属性。&lt;code&gt;JavaScript&lt;/code&gt; 会现在子对象中检索该属性，如果没有找到则会沿着原型链寻找到原型链的终点 &lt;code&gt;null&lt;/code&gt;，在原型链的任何位置找到会立即返回找到的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = {}
let b = Object.create(a)
b.m = 10
console.log(b) //{m: 10}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;原型链上有同名可写属性&lt;/h5&gt;
&lt;p&gt;这种情况就是开头的问题中提到的情况，同样会在子对象上创建新的属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = {
  m: 2
}
let b = Object.create(a)
b.m = 10
console.log(a) //{m: 2} a对象不变
console.log(b) //{m: 10}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;原型链上有同名不可写属性&lt;/h5&gt;
&lt;p&gt;这种情况下不会在子对象上创建新的属性，赋值也不会执行，在严格模式下会报错。至于为什么这样设计，&lt;a href=&quot;https://www.zhihu.com/question/31934148/answer/53949560&quot; title=&quot;贺师俊&quot;&gt;贺师俊&lt;/a&gt; 认为是保持 &lt;code&gt;getter-only property&lt;/code&gt;（只定义了&lt;code&gt;get&lt;/code&gt; 方法的访问器属性，上面详细介绍了） 和 &lt;code&gt;non-writable property&lt;/code&gt; 行为的一致。&lt;code&gt;You Dont Know Js&lt;/code&gt; 则认为是为了保持和传统语言继承表现的一致。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//&apos;use strict&apos;
let a = {}
Object.defineProperty(a, &apos;m&apos;, {
  value: 10,
  configurable: true,
  enumerable: true,
  writable: false
})

console.log(a.m)
a.m = 100 //无效，严格模式下会报错 TypeError: Cannot assign to read only property &apos;m&apos; of object

let b = Object.create(a)
console.log(b.m)
b.m = 200 //无效，严格模式下报错
console.log(b) //{} 不会创建新的属性
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;访问器属性&lt;/h5&gt;
&lt;p&gt;关于访问器属性，我在上一节详细介绍过了。如果不考虑用 &lt;code&gt;Object.defineProperty&lt;/code&gt; 来定义属性描述符，我们是无法在子对象上创建新的同名属性的，我们对同名属性的操作都是在调用原型对象上对应属性的 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 方法，唯一不同的就是方法内的 &lt;code&gt;this&lt;/code&gt; 指向会发生变化。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;其实对属性类型和赋值行为的讨论，最终还是会回到继承机制的问题上。属性的类型和继承的机制在标准的发展过程中也不是一成不变的，&lt;code&gt;ES5&lt;/code&gt; 标准中属性描述里的特性都是没法直接在 &lt;code&gt;JS&lt;/code&gt; 中访问和操作的，它只是在实现引擎是使用的。而现在 &lt;code&gt;getter&lt;/code&gt; 和 &lt;code&gt;setter&lt;/code&gt; 也能够让我们实现一些高级特性。而继承机制需要在很多方面取得一个平衡，比如复用，灵活性和数据的安全性等。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty&quot; title=&quot;Object.defineProperty() - MDN&quot;&gt;Object.defineProperty() - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000016865771&quot; title=&quot;js细节剖析&quot;&gt;js细节剖析&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>JavaScript浅拷贝和深拷贝</title><link>https://clloz.com/blog/javascript-shallow-deep-copy</link><guid isPermaLink="true">https://clloz.com/blog/javascript-shallow-deep-copy</guid><pubDate>Wed, 09 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中最重要的就是对象，除了 &lt;code&gt;Number, String, Null, Undefined, Boolean, Symbol, BigInt&lt;/code&gt; 等基本数据类型，剩下的就 &lt;code&gt;Object&lt;/code&gt; 对象。&lt;code&gt;JavaScript&lt;/code&gt; 也给我们提供了一系列内置对象，比如 &lt;code&gt;Function，Array，Math，Date，RegExp&lt;/code&gt; 等等，他们都是用 &lt;code&gt;function Object()&lt;/code&gt; 构造的。我们使用 &lt;code&gt;JavaScript&lt;/code&gt; 大多是时候都是在操作对象。本文就讲一讲复制对象涉及到的浅拷贝和深拷贝。&lt;/p&gt;
&lt;h2&gt;内存堆栈&lt;/h2&gt;
&lt;p&gt;在讲对象的复制之前我们先来了解一下 &lt;code&gt;JavaScript&lt;/code&gt; 中的数据类型在内存中是如何存放的。&lt;/p&gt;
&lt;p&gt;每一个数据都需要分配一块内存空间，内存空间分为两种：栈 &lt;code&gt;stack&lt;/code&gt; 和 堆 &lt;code&gt;heap&lt;/code&gt;：&lt;code&gt;stack&lt;/code&gt; 为自动分配的内存空间，它由系统自动释放；而 &lt;code&gt;heap&lt;/code&gt; 则是动态分配的内存，大小不定也不会自动释。基本类型值是存储在栈中的简单数据段，也就是说，他们的值直接存储在变量访问的位置。堆是存放数据的基于散列算法的数据结构，在 &lt;code&gt;javascript&lt;/code&gt; 中，引用值是存放在堆中的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = 10
let b = 20
let obj = {
  name: &apos;clloz&apos;
}
let obj2 = obj
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如这段代码在内存中的结构应该为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/shallowcopy.DppfKbL7_2iD8c.webp&quot; alt=&quot;shallowcoppy&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以访问基本类型的变量时，是直接访问到栈内存中其真正的值；而访问引用类型的变量时，是通过栈内存中保存的引用地址去访问。&lt;/p&gt;
&lt;p&gt;栈的优势就是存取速度比堆要快，仅次于直接位于 &lt;code&gt;CPU&lt;/code&gt; 中的寄存器，但缺点是，存在栈中的数据大小与生存期必须是确定的，缺乏灵活性。堆的优势是可以动态地分配内存大小，生存期也不必事先告诉编译器，垃圾收集器会自动地收走这些不再使用的数据，但是缺点是由于在运行时动态分配内存，所以存取速度较慢。所以对于基本数据类型，他们占用内存比较小，如果放在堆中，查找会浪费很多时间，而把堆中的数据放入栈中也会影响栈的效率。比如对象和数组是可以无限拓展的，正好放在可以动态分配大小的堆中。&lt;/p&gt;
&lt;p&gt;从上面的例子中我们可以看到，我们将一个对象赋值给一个变量的时候，系统会在栈中为我们分配一块空间，里面存入对象在堆中的地址。&lt;code&gt;obj&lt;/code&gt; 和 &lt;code&gt;obj1&lt;/code&gt; 指向的是堆中的同一块内存，不管我们用哪个标识符来操作对象中的数据，都会影响到另一个，因为他们本质就是同一个对象的不同名字。而如果是基本数据类型的复制，则直接在栈中将值写入，新变量的改变不会影响到原来的变量。&lt;/p&gt;
&lt;p&gt;这就是值传递和地址传递的主要区别，也就是深拷贝和浅拷贝的产生的原因。当我们想要真正地复制一个对象，希望开辟一块新的内存空间，新对象的操作不会影响到原来的对象，就需要用深拷贝的方式。&lt;/p&gt;
&lt;h2&gt;概念&lt;/h2&gt;
&lt;p&gt;有了上面对数据类型和内存对战的概念，我们可以来说一说浅拷贝 &lt;code&gt;shallow copy&lt;/code&gt; 和深拷贝 &lt;code&gt;deep copy&lt;/code&gt; 的概念了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浅拷贝：只对原始数据类型进行复制，引用类似则是复制其地址引用。也就是说对对象属性中的对象，不会进行递归复制，只会拷贝对象第一层的值类型。&lt;/li&gt;
&lt;li&gt;深拷贝：进行递归复制，不仅复制值类型，还对引用类型进行完整的递归复制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;遍历对象的方法&lt;/h2&gt;
&lt;p&gt;不管是浅拷贝和深拷贝都会涉及到对象的遍历，&lt;code&gt;JavaScript&lt;/code&gt; 提供了很多遍历对象的方法，我们先来看看它们有什么不同，方便我们在对应的场景选择合适的遍历方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for ... in&lt;/code&gt;：遍历对象自身可其原型链上的可枚举属性，不包括 &lt;code&gt;Symbol&lt;/code&gt; 属性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.keys()&lt;/code&gt;：返回一个数组，包括对象自身的(不含圆形脸上的)所有可枚举属性 (不含 &lt;code&gt;Symbol&lt;/code&gt; 属性)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.getOwnPropertyNames()&lt;/code&gt;：返回一个数组，包含对象自身的所有属性(不含 &lt;code&gt;Symbol&lt;/code&gt; 属性，但是包括不可枚举属性)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.getOwnPropertySymbols()&lt;/code&gt;：返回一个数组，包含对象自身的所有 &lt;code&gt;Symbol&lt;/code&gt; 属性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.ownKeys()&lt;/code&gt;：返回一个数组，包含对象自身的所有属性，不管属性名是 &lt;code&gt;Symbol&lt;/code&gt; 或字符串，也不管是否可枚举。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.values()&lt;/code&gt;：返回一个给定对象自身的所有可枚举属性值的数组。(不含 &lt;code&gt;Symbol&lt;/code&gt; 属性)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.entries()&lt;/code&gt;：返回一个给定对象自身可枚举属性的键值对数组。(不含 &lt;code&gt;Symbol&lt;/code&gt; 属性)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;他们遍历属性的顺序都遵循以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先遍历所有属性名为数值的属性，按照数字排序。&lt;/li&gt;
&lt;li&gt;其次遍历所有属性名为字符串的属性，按照生成时间排序。&lt;/li&gt;
&lt;li&gt;最后遍历所有属性名为 &lt;code&gt;Symbol&lt;/code&gt; 值的属性，按照生成时间排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;浅拷贝实现&lt;/h2&gt;
&lt;h2&gt;Object.assingn()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Object.assign()&lt;/code&gt; 方法也是常用的浅拷贝方法，该方法只会拷贝源对象自身的并且可枚举的属性到目标对象，&lt;code&gt;String&lt;/code&gt; 类型和 &lt;code&gt;Symbol&lt;/code&gt; 类型的属性都会被拷贝。看下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = {
  p1: 10,
  p2: 20,
  p3: {
    m: 100,
    n: 200
  }
}
let b = Object.assign({}, a)
console.log(b) //{ p1: 10, p2: 20, p3: { m: 100, n: 200 } }
b.p1 = &apos;teste&apos;
console.log(a) //{ p1: 10, p2: 20, p3: { m: 100, n: 200 } } a中的p1没有改变
b.p3.m = &apos;test&apos;
console.log(a) //{ p1: 10, p2: 20, p3: { m: &apos;test&apos;, n: 200 } } a.m是一个嵌套对象，浅拷贝
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;关于 &lt;code&gt;Object.assign()&lt;/code&gt; 还有需要注意的点就是，该方法只能拷贝源对象的可枚举的自身属性，同时拷贝时无法拷贝属性的特性，而且访问器属性会被转换成数据属性（值为访问器属性的 &lt;code&gt;getter&lt;/code&gt; 的返回值，如果访问器属性没有设置 &lt;code&gt;getter&lt;/code&gt;，那么值为 &lt;code&gt;undefined&lt;/code&gt;）。看下面的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//&apos;use strict&apos;
let a_p = {
  fun: () =&gt; console.log(&apos;a.[[prototype]]&apos;)
}
let a = Object.create(a_p)
let out_var = &apos;out variable&apos;

Object.defineProperty(a, Symbol(&apos;symbol&apos;), {
  value: &apos;symbol&apos;,
  enumerable: true
})

Object.defineProperty(a, &apos;val&apos;, {
  //不可枚举属性
  value: 100,
  configurable: false,
  enumerable: false,
  writable: true
})

Object.defineProperty(a, &apos;enum&apos;, {
  //可枚举属性
  value: &apos;enumerable&apos;,
  configurable: false,
  enumerable: true,
  writable: false
})

Object.defineProperty(a, &apos;m&apos;, {
  //不可枚举的访问器属性
  enumerable: false,
  set(val) {
    this.val = val
  },
  get() {
    return this.val
  }
})

Object.defineProperty(a, &apos;n&apos;, {
  //可枚举的访问器属性
  enumerable: true,
  set(val) {
    a.val = val
  },
  get() {
    return a.val
  }
})

let b = Object.assign({}, a)
console.log(b) //{ enum: &apos;enumerable&apos;, n: 100, [Symbol(symbol)]: &apos;symbol&apos; } 只有可枚举的数据属性和访问器属性是会被复制的。访问器属性被转换成数据属性，值是调用访问器属性getter的返回值
console.log(Object.getOwnPropertyDescriptor(b, &apos;enum&apos;)) //属性描述符全部变为 true
//{
//  value: &apos;enumerable&apos;,
//  writable: true,
//  enumerable: true,
//  configurable: true
//}
console.log(Object.getPrototypeOf(b) === Object.prototype) //true 没有复制原型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想要实现复制属性的特性，访问器属性以及链接原型，可用如下的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let c = Object.create(Object.getPrototypeOf(a), Object.getOwnPropertyDescriptors(a))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原始数据类型作为 &lt;code&gt;source&lt;/code&gt; 的时候，字符串会以数组形式，拷贝入目标对象，数字和布尔值则没有效果，这是因为只有字 符串的包装对象，会产生可枚举属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var v1 = &apos;abc&apos;
var v2 = true
var v3 = 10
var obj = Object.assign({}, v1, v2, v3)
console.log(obj) // { &quot;0&quot;: &quot;a&quot;, &quot;1&quot;: &quot;b&quot;, &quot;2&quot;: &quot;c&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Object.create()&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;Object.create()&lt;/code&gt; 也可以实现对象的浅拷贝。主要是结合对象的属性类型和赋值特性，主要是配合 &lt;code&gt;Object.getOwnPropertyDescriptors()&lt;/code&gt; 方法获取要拷贝的对象的属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
// 或者
const shallowClone = (obj) =&gt;
  Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然其实直接用 &lt;code&gt;Object.create(source)&lt;/code&gt; 也可以实现 &lt;code&gt;拷贝&lt;/code&gt;，我们可以通过访问原型访问到对应的属性，并且如果我们如果给属性赋值，不会影响到原型，具体可以参考另一篇文章：&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/09/javascript-object-prop-assign/#i-8&quot; title=&quot;JavaScript对象属性类型和赋值细节&quot;&gt;JavaScript对象属性类型和赋值细节&lt;/a&gt;。当然这不算传统意义上的 &lt;strong&gt;拷贝&lt;/strong&gt;，并且对访问器属性无效。&lt;/p&gt;
&lt;h2&gt;遍历&lt;/h2&gt;
&lt;p&gt;还有一个最普遍的浅拷贝方法就是遍历：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function shallowClone(source) {
  var target = {}
  for (var i in source) {
    if (source.hasOwnProperty(i)) {
      target[i] = source[i]
    }
  }
  return target
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然也可以用我们上面提到的其他遍历方法。&lt;/p&gt;
&lt;h2&gt;深拷贝实现&lt;/h2&gt;
&lt;h2&gt;for...in 递归&lt;/h2&gt;
&lt;p&gt;这个方法是最好理解的，代码如下。注意 &lt;code&gt;for...in&lt;/code&gt; 会遍历所有能访问到的属性，包括原型链上的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let testObj = {
    num: 0,
    str: &apos;clloz&apos;,
    boolean: true,
    unf: undefined,
    nul: null,
    obj: {
        name: &apos;clloz&apos;,
        id: 1
    },
    arr: [0, 1, 2],
    func: function() {
        console.log(&apos;clloz&apos;)
    },
    date: new Date(0),
    reg: new RegExp(&apos;/clloz/ig&apos;),
    err: new Error(&apos;clloz&apos;)
}

function isObject(obj) {
    return (typeof obj === &apos;function&apos; || typeof obj === &apos;object&apos;) &amp;#x26;&amp;#x26; obj !== null;
}

function deepClone(obj) {
    if (!isObject(obj)) {
        throw new Error(obj + &apos;is not a Object!&apos;);
    }

    let newObj = Array.isArray(obj); ? [] : {};

    for (let prop in obj) {
        if (obj.hasOwnProperty(prop)) {
            newObj[prop] = isObject(obj[prop]) ? deepClone(obj[prop]) : obj[prop];
        }
    }
    return newObj;
}

let a = deepClone(testObj)
console.log(a)
//{
//  num: 0,
//  str: &apos;&apos;,
//  boolean: true,
//  unf: undefined,
//  nul: null,
//  obj: { name: &apos;我是一个对象&apos;, id: 1 },
//  arr: [ 0, 1, 2 ],
//  func: {},
//  date: {},
//  reg: {},
//  err: {}
//}

console.log(a.obj === testObj.obj) //false
console.log(a.arr === testObj.arr) //false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终成功拷贝了 &lt;code&gt;obj&lt;/code&gt; 和 &lt;code&gt;arr&lt;/code&gt;。&lt;code&gt;func&lt;/code&gt;，&lt;code&gt;date&lt;/code&gt;，&lt;code&gt;reg&lt;/code&gt; 和 &lt;code&gt;err&lt;/code&gt; 没有拷贝成功，因为他们不是普通的 &lt;code&gt;Object&lt;/code&gt; 结构。&lt;/p&gt;
&lt;p&gt;采用 &lt;code&gt;for ... in&lt;/code&gt; 递归我们有几个需要注意的细节：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;没有对传入参数进行校验，传入 &lt;code&gt;null&lt;/code&gt; 时应该返回 &lt;code&gt;null&lt;/code&gt; 而不是 &lt;code&gt;{}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;要对传入的参数进行判断是否是对象&lt;/li&gt;
&lt;li&gt;要对传入参数是数组的情况进行兼容&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一点很好解决，第二点判断对象我们一般可以使用 &lt;code&gt;Object.prototype.toString.call(x) === &apos;[object Object]&apos;&lt;/code&gt;，但是这压力需要兼容数组（在这题里面，数组也是一个合法参数），所以我们使用 &lt;code&gt;(typeof obj === &apos;function&apos; || typeof obj === &apos;object&apos;) &amp;#x26;&amp;#x26; obj !== null&lt;/code&gt;。然后创建新对象的时候用 &lt;code&gt;let newObj = isArray ? [] : {}&lt;/code&gt; 进行处理。&lt;/p&gt;
&lt;h2&gt;递归爆栈&lt;/h2&gt;
&lt;p&gt;如果我们要拷贝的对象层级非常深，有可能导致递归爆栈（一般来说不可能，这里只是讨论解决方案）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//创建一个指定深度和宽度的对象
function createData(deep, breadth) {
  var data = {}
  var temp = data

  for (var i = 0; i &amp;#x3C; deep; i++) {
    temp = temp[&apos;data&apos;] = {}
    for (var j = 0; j &amp;#x3C; breadth; j++) {
      temp[j] = j
    }
  }

  return data
}
function clone(source) {
  var target = {}
  for (var i in source) {
    if (source.hasOwnProperty(i)) {
      if (typeof source[i] === &apos;object&apos;) {
        target[i] = clone(source[i]) // 注意这里
      } else {
        target[i] = source[i]
      }
    }
  }

  return target
}
clone(createData(1000)) // ok
clone(createData(10000)) // Maximum call stack size exceeded
clone(createData(10, 100000)) // ok 广度不会溢出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决的方案就是用循环代替递归，把横向的嵌套对象想象成一个纵向的树，然后用栈和循环来处理。开始将跟元素放入栈中，每次从栈中取一个节点进行拷贝，如果拷贝过程中遇到对象则放入栈中，循环从栈中取元素，知道栈空，则拷贝完毕。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function clone(obj) {
  if (!isObject(obj)) return obj
  //创建一个新对象用来复制
  let root = {}

  //创建一个栈，栈中的第一个元素就是 root，不断从栈中取出元素，遍历元素的 data 中的属性复制到 parent 中
  let stack = [
    {
      parent: root, //这里栈中第一个元素设置 key 为 undefined，用来判断是否是跟节点
      key: undefined,
      data: obj
    }
  ]

  while (stack.length) {
    const node = stack.pop()
    const parent = node.parent
    const key = node.key
    const data = node.data

    let res = parent
    if (key !== undefined) res = parent[key] = {}

    for (let key in data) {
      if (Object.prototype.hasOwnProperty.call(data, key)) {
        if (isObject(data[key])) {
          stack.push({
            parent: res,
            key: key,
            data: data[key]
          })
        } else {
          res[key] = data[key]
        }
      }
    }
  }
}

function isObject(obj) {
  return (typeof obj === &apos;function&apos; || typeof obj === &apos;object&apos;) &amp;#x26;&amp;#x26; obj !== null
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;循环引用&lt;/h2&gt;
&lt;p&gt;我们在深拷贝的 &lt;code&gt;JSON&lt;/code&gt; 序列化部分说到了循环引用会引起序列化报错的问题。但时期我们的 &lt;code&gt;for...in&lt;/code&gt; 方法遇到循环引用一样会出问题。&lt;/p&gt;
&lt;p&gt;比如上面的例子，我们为 &lt;code&gt;testObj&lt;/code&gt; 添加一个属性 &lt;code&gt;loop: testObj&lt;/code&gt;，在执行会发现栈溢出了 &lt;code&gt;RangeError: Maximum call stack size exceeded&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果你用了 &lt;code&gt;lodash&lt;/code&gt; 的 &lt;code&gt;baseClone&lt;/code&gt; 会发现它的执行不会报错，因为它用栈来保存克隆的对象，用来检测循环引用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Check for circular references and return its corresponding clone.
stack || (stack = new Stack())
const stacked = stack.get(value)
if (stacked) {
  return stacked
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我这里写了一个简单的实现，用一个 &lt;code&gt;WeakMap&lt;/code&gt; 来保存嵌套的对象，每进入一层对象就在 &lt;code&gt;Set&lt;/code&gt; 中保存起来，每次要递归之前，检测该对象是否已在 &lt;code&gt;Set&lt;/code&gt; 中，如果不在才进行递归。每次递归完一个对象时，将该对象从 &lt;code&gt;set&lt;/code&gt; 中删除。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function cloneDeep3(source, hash = new WeakMap()) {
  if (!isObject(source)) return source
  if (hash.has(source)) return hash.get(source) //查询该对象是否在 WeakMap 中

  var target = Array.isArray(source) ? [] : {}
  hash.set(source, target) // 注意这里的键名是源对象中的对象，键值是新对象中的

  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (isObject(source[key])) {
        target[key] = cloneDeep3(source[key], hash) // 新增代码，传入哈希表
      } else {
        target[key] = source[key]
      }
    }
  }
  return target
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要兼容 &lt;code&gt;ES6&lt;/code&gt; 之前的代码，不能使用 &lt;code&gt;WeakMap&lt;/code&gt;，及使用数组来保存，需要增加一个遍历数组的逻辑，因为不想 &lt;code&gt;WeakMap&lt;/code&gt; 有 &lt;code&gt;has&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function cloneDeep3(source, uniqueList) {
  if (!isObject(source)) return source
  if (!uniqueList) uniqueList = [] // 新增代码，初始化数组

  var target = Array.isArray(source) ? [] : {}

  // ============= 新增代码
  // 数据已经存在，返回保存的数据
  var uniqueData = find(uniqueList, source)
  if (uniqueData) {
    return uniqueData.target
  }

  // 数据不存在，保存源数据，以及对应的引用
  uniqueList.push({
    source: source,
    target: target
  })
  // =============

  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (isObject(source[key])) {
        target[key] = cloneDeep3(source[key], uniqueList) // 新增代码，传入数组
      } else {
        target[key] = source[key]
      }
    }
  }
  return target
}

// 新增方法，用于查找
function find(arr, item) {
  for (var i = 0; i &amp;#x3C; arr.length; i++) {
    if (arr[i].source === item) {
      return arr[i]
    }
  }
  return null
}

// 用上面测试用例已测试通过
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JSON 序列化和反序列化&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;JSON.stringify()&lt;/code&gt; 和 &lt;code&gt;JSON.parse()&lt;/code&gt; 进行序列化反序列化。先将一个 &lt;code&gt;JavaScript&lt;/code&gt; 对象转为一个 &lt;code&gt;JSON&lt;/code&gt; 字符串，然后再将字符串转为对象。这种方法不能复制非枚举属性，也不能复制属性特性，也不能复制访问器属性。而且&lt;code&gt;JSON.parse()&lt;/code&gt;和 &lt;code&gt;JSON.stringify()&lt;/code&gt; 能正确处理的对象只有 &lt;code&gt;Number&lt;/code&gt;、&lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Array&lt;/code&gt; 和 &lt;code&gt;Boolean&lt;/code&gt; 等能够被 &lt;code&gt;json&lt;/code&gt; 表示的数据结构，因此函数，&lt;code&gt;RegExp&lt;/code&gt;这种不能被 &lt;code&gt;json&lt;/code&gt; 表示的类型将不能被正确处理。&lt;/p&gt;
&lt;p&gt;还有一点存在循环引用的对象，例如 &lt;code&gt;let a = {m:a}&lt;/code&gt; 这样的对象序列化会报错 &lt;code&gt;TypeError: Converting circular structure to JSON&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个方法根据自己的需求使用。&lt;/p&gt;
&lt;h2&gt;第三方库&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;jQuery.extend&lt;/code&gt;, &lt;a href=&quot;https://github.com/lodash/lodash/blob/master/.internal/baseClone.js&quot; title=&quot;baseClone -lodash&quot;&gt;baseClone -lodash&lt;/a&gt;，还有 &lt;code&gt;lodash&lt;/code&gt;自定义深拷贝方法 &lt;a href=&quot;https://lodash.com/docs/4.17.15#cloneDeepWith&quot; title=&quot;cloneDeepWith&quot;&gt;cloneDeepWith&lt;/a&gt;。有兴趣可以去阅读以下源码。&lt;/p&gt;
&lt;h2&gt;Symbol&lt;/h2&gt;
&lt;p&gt;如果属性的 &lt;code&gt;key&lt;/code&gt; 是一个 &lt;code&gt;Symbol&lt;/code&gt;，那么 &lt;code&gt;for...in&lt;/code&gt; 无法遍历到该属性。这里我们可以用 &lt;code&gt;Reflect.ownKeys()&lt;/code&gt; 方法。也可以使用 &lt;code&gt;Object.getOwnPropertySymbols()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function deepClone(obj) {
  if (!isObject(obj)) {
    throw new Error(&apos;obj 不是一个对象！&apos;)
  }

  let isArray = Array.isArray(obj)
  let newObj = isArray ? [...obj] : { ...obj }
  Reflect.ownKeys(newObj).forEach((key) =&gt; {
    newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
  })

  return newObj
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不可枚举属性&lt;/h2&gt;
&lt;p&gt;关于不可枚举属性的深拷贝，我想到的是用 &lt;code&gt;Object.getOwnPropertyNames&lt;/code&gt; 来遍历添加。如果遇到 &lt;code&gt;value&lt;/code&gt; 是对象则递归。不过很多内置对象需要单独处理，比如 &lt;code&gt;Function&lt;/code&gt;，&lt;code&gt;RegExp&lt;/code&gt;，&lt;code&gt;Date&lt;/code&gt; 等。这种需求几乎不存在，看一看就好。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let testObj = {
  num: 0,
  str: &apos;clloz&apos;,
  boolean: true,
  unf: undefined,
  nul: null,
  obj: {
    name: &apos;clloz&apos;,
    id: 1
  },
  arr: [0, 1, 2],
  func: function () {
    console.log(&apos;clloz&apos;)
  },
  date: new Date(0),
  reg: new RegExp(&apos;/clloz/ig&apos;),
  err: new Error(&apos;clloz&apos;)
}
Object.defineProperty(testObj, &apos;test&apos;, {
  value: {
    name: &apos;clloz&apos;,
    age: 28
  },
  writable: true,
  enumerable: false,
  configurable: true
})

function isObject(obj) {
  return (
    typeof obj === &apos;object&apos; &amp;#x26;&amp;#x26;
    obj !== null &amp;#x26;&amp;#x26;
    obj instanceof RegExp !== true &amp;#x26;&amp;#x26;
    obj instanceof Date !== true
  )
}

function deepClone(obj) {
  if (!isObject(obj)) {
    throw new Error(&apos;obj is not a Object!&apos;)
  }

  let isArray = Array.isArray(obj)

  let newObj = isArray ? [] : {}

  let keys = Object.getOwnPropertyNames(obj)

  for (let key of keys) {
    if (isObject(Object.getOwnPropertyDescriptor(obj, key).value)) {
      Object.defineProperty(newObj, key, {
        value: deepClone(obj[key]),
        configurable: Object.getOwnPropertyDescriptor(obj, key).configurable,
        enumerable: Object.getOwnPropertyDescriptor(obj, key).enumerable,
        writable: Object.getOwnPropertyDescriptor(obj, key).writable
      })
    } else {
      Object.defineProperty(newObj, key, Object.getOwnPropertyDescriptor(obj, key))
    }
  }
  return newObj
}

let a = deepClone(testObj)
console.log(Object.getOwnPropertyDescriptors(a))
console.log(a.obj === testObj.obj) //false
console.log(a.arr === testObj.arr) //false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个实现在加上 &lt;code&gt;Symbol&lt;/code&gt;的处理，循环引用的处理基本就可以应对大多数情况了。如果你还想增加对原型的支持，那么可以在创建对象的时候用 &lt;code&gt;Object.create()&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;如果要实现一个能应对各种对象各种情况的深拷贝函数还是非常不容易的，本文处理的情况包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Symbol&lt;/code&gt;：用 &lt;code&gt;Reflect.ownKeys()&lt;/code&gt; 或者 &lt;code&gt;Object.getOwnPropertySymbols()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;for-in&lt;/code&gt;：遍历原型链上的属性，如果不需要原型上的方法，可以添加判断。&lt;/li&gt;
&lt;li&gt;递归爆栈&lt;/li&gt;
&lt;li&gt;循环引用的处理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.getOwnPropertyNames&lt;/code&gt;: 实现非枚举属性的拷贝&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在日常的编码中我们不太会遇到这么复杂的情况，大多数情况下我们要深拷贝的对象用 &lt;code&gt;for...in&lt;/code&gt; 或者序列化就可以处理了。扩展这么多主要是为了加深对 &lt;code&gt;JavaScript&lt;/code&gt; 对象的理解。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000015830451&quot; title=&quot;JS数据类型和内存堆栈&quot;&gt;JS数据类型和内存堆栈&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/jingtian678/article/details/83902819&quot; title=&quot;JS的栈与堆的讲解&quot;&gt;JS的栈与堆的讲解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptors&quot; title=&quot;Object.getOwnPropertyDescriptors() - MDN&quot;&gt;Object.getOwnPropertyDescriptors() - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign&quot; title=&quot;Object.assign() - MDN&quot;&gt;Object.assign() - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844903592587952135#heading-6&quot; title=&quot;深入深入在深入JS深拷贝对象&quot;&gt;深入深入在深入JS深拷贝对象&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/yygmind/blog/issues/29&quot; title=&quot;面试题之如何实现一个深拷贝&quot;&gt;面试题之如何实现一个深拷贝&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000016672263&quot; title=&quot;深拷贝的终极探索（99%的人都不知道）&quot;&gt;深拷贝的终极探索（99%的人都不知道）&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>CentOS7 Apache 开启 HTTP/2 支持</title><link>https://clloz.com/blog/centos7-apache-http-2</link><guid isPermaLink="true">https://clloz.com/blog/centos7-apache-http-2</guid><pubDate>Mon, 07 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Apache Httpd&lt;/code&gt; 从 &lt;code&gt;2.4.17&lt;/code&gt; 开始支持 &lt;code&gt;mod_http2&lt;/code&gt;，不过 &lt;code&gt;CentOS 7&lt;/code&gt; 的 &lt;code&gt;httpd&lt;/code&gt; 版本一直停留在 &lt;code&gt;2.4.6&lt;/code&gt;。今天详细说明一下如何安装升级 &lt;code&gt;Apache&lt;/code&gt; 的最新版本和开启 &lt;code&gt;HTTP/2&lt;/code&gt; 的支持。&lt;/p&gt;
&lt;h2&gt;关于 HTTP/2&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTTP/2&lt;/code&gt; 简称为 &lt;code&gt;h2&lt;/code&gt;（基于 &lt;code&gt;TLS/1.2&lt;/code&gt; 或以上版本的加密连接）或 &lt;code&gt;h2c&lt;/code&gt;（非加密连接），是 &lt;code&gt;HTTP&lt;/code&gt; 协议的的第二个主要版本。是 &lt;code&gt;HTTP协&lt;/code&gt; 议自 &lt;code&gt;1999&lt;/code&gt; 年 &lt;code&gt;HTTP 1.1&lt;/code&gt; 发布后的首个更新，主要基于 &lt;code&gt;SPDY&lt;/code&gt; 协议。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTTP/2&lt;/code&gt; 标准于 &lt;code&gt;2015年5月&lt;/code&gt; 以 &lt;code&gt;RFC 7540&lt;/code&gt; 正式发表。&lt;code&gt;HTTP/2&lt;/code&gt; 的标准化工作由 &lt;code&gt;Chrome&lt;/code&gt;、&lt;code&gt;Opera&lt;/code&gt;、&lt;code&gt;Firefox&lt;/code&gt;、&lt;code&gt;Internet Explorer 11&lt;/code&gt;、&lt;code&gt;Safari&lt;/code&gt;、&lt;code&gt;Amazon Silk&lt;/code&gt;及 &lt;code&gt;Edge&lt;/code&gt; 等浏览器提供支持。&lt;/p&gt;
&lt;p&gt;多数主流浏览器已经在 &lt;code&gt;2015&lt;/code&gt; 年底支持了该协议。此外，根据 &lt;code&gt;W3Techs&lt;/code&gt; 的数据，截至 &lt;code&gt;2019年6月&lt;/code&gt;，全球有 &lt;code&gt;36.5%&lt;/code&gt; 的网站支持了 &lt;code&gt;HTTP/2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;HTTP/2&lt;/code&gt; 这里就不再详细介绍了，想要了解的可以参考 &lt;a href=&quot;https://zh.wikipedia.org/wiki/HTTP/2&quot; title=&quot;HTTP/2 - Wikipedia&quot;&gt;HTTP/2 - Wikipedia&lt;/a&gt; 和我的另一篇文章 &lt;a href=&quot;https://www.clloz.com/programming/network/2019/05/02/http/#HTTP2&quot; title=&quot;前端网络基础和HTTP&quot;&gt;前端网络基础和HTTP&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTTP/2&lt;/code&gt; 的优势可以看这篇知乎上的文章： &lt;a href=&quot;https://zhuanlan.zhihu.com/p/29609078&quot; title=&quot;怎样把网站升级到http/2&quot;&gt;怎样把网站升级到http/2&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;升级 Apache&lt;/h2&gt;
&lt;p&gt;首先是要升级 &lt;code&gt;Apache&lt;/code&gt; 的版本，你可以用 &lt;code&gt;yum info httpd&lt;/code&gt; 查看一下 &lt;code&gt;CeontOS 7&lt;/code&gt; 软件库中的 &lt;code&gt;Apache&lt;/code&gt; 版本，不出意外是 &lt;code&gt;2.4.6&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CodeIT&lt;/code&gt; 提供了一个很好的自定义库。这个库提供了最新版本的服务器软件(&lt;code&gt;Apache &amp;#x26; Nginx&lt;/code&gt;)。在安装 &lt;code&gt;CodeIT&lt;/code&gt; 库之前，你需要开启 &lt;code&gt;EPEL&lt;/code&gt; 。&lt;code&gt;EPEL&lt;/code&gt; 提供了 &lt;code&gt;CodeIT&lt;/code&gt; 库需要的依赖。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo yum install -y epel-release

cd /etc/yum.repos.d &amp;#x26;&amp;#x26; wget https://repo.codeit.guru/codeit.el`rpm -q --qf &quot;%{VERSION}&quot; $(rpm -q --whatprovides redhat-release)`.repo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们再用 &lt;code&gt;yum info httpd&lt;/code&gt; 查看 &lt;code&gt;Apache&lt;/code&gt; 版本会发现已经到最新的版本了(我今天安装的是 &lt;code&gt;2.4.46&lt;/code&gt;)。&lt;/p&gt;
&lt;h2&gt;安装依赖&lt;/h2&gt;
&lt;p&gt;需要安装的依赖主要是 &lt;code&gt;openssl&lt;/code&gt; 和 &lt;code&gt;nghttp2&lt;/code&gt;，其中 &lt;code&gt;openssl&lt;/code&gt; 版本要大于 &lt;code&gt;1.0.2&lt;/code&gt;，不过这两个依赖在 &lt;code&gt;CentOS 7&lt;/code&gt; 软件库中都是有的，直接用 &lt;code&gt;yum&lt;/code&gt; 安装即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yum install openssl nghttp2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置 httpd.conf&lt;/h2&gt;
&lt;p&gt;这里参考 &lt;code&gt;Apache&lt;/code&gt; 的 &lt;a href=&quot;https://httpd.apache.org/docs/2.4/howto/http2.html&quot; title=&quot;官方文档&quot;&gt;官方文档&lt;/a&gt; 即可，文档也有 &lt;a href=&quot;https://www.docs4dev.com/docs/zh/apache/2.4/reference/howto-http2.html&quot; title=&quot;中文版&quot;&gt;中文版&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;根据文档中的说明，我们需要加载 &lt;code&gt;mod_http2&lt;/code&gt; 模块，这个模块是依赖于上面安装的 &lt;code&gt;nghttp2&lt;/code&gt; 的。然后就是添加一行配置即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 加载 mod_http2
LoadModule http2_module modules/mod_http2.so

# 配置文件中添加指令，该指令允许h2协议，并让其成为服务器连接上的首选协议
Protocols h2 http/1.1

# 要启用所有 HTTP/2 变体时，使用如下指令
Protocols h2 h2c http/1.1

# 根据放置此指令的位置，它会影响所有连接或仅影响到某个虚拟主机的连接。比如下面的配置仅允许使用 HTTP/1 进行连接，但与提供 HTTP/2 的test.example.org的 SSL 连接除外。
Protocols http/1.1
&amp;#x3C;VirtualHost ...&gt;
    ServerName test.example.org
    Protocols h2 http/1.1
&amp;#x3C;/VirtualHost&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里就已经完成全部配置，&lt;code&gt;systemctl restart httpd&lt;/code&gt; 即可。此时打开浏览器的开发者工具访问自己的网站会发现 &lt;code&gt;HTTP/2&lt;/code&gt; 已经生效。除了通过开发者工具，这个 &lt;a href=&quot;https://tools.keycdn.com/http2-test&quot; title=&quot;http2 - test网站&quot;&gt;http2-test&lt;/a&gt; 网站也可以帮助你检测网站是否开启 &lt;code&gt;HTTP/2&lt;/code&gt; 的支持。$&lt;/p&gt;
&lt;h2&gt;内存占用&lt;/h2&gt;
&lt;p&gt;如果 &lt;code&gt;Apache&lt;/code&gt; 出现内存占用过高，那么修改 &lt;code&gt;httpd-mpm.conf&lt;/code&gt; 中的 &lt;code&gt;prefork&lt;/code&gt; 工作模式的参数，将 &lt;code&gt;MaxConnectionPerChild&lt;/code&gt; 改为 &lt;code&gt;50&lt;/code&gt;。文件位置用命令 &lt;code&gt;find / -name httpd-mpm.conf&lt;/code&gt; 查找。更多内容参考文章 &lt;a href=&quot;https://abc-ziv.github.io/2018/06/13/2018-06-27-OutOfMemory/&quot; title=&quot;Apache内存溢出的分析与解决&quot;&gt;Apache内存溢出的分析与解决&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/ihuangjianxin/p/9036646.html&quot; title=&quot;如何在Centos7下升级Apache至最新版本&quot;&gt;如何在Centos7下升级Apache至最新版本&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.mf8.biz/apache-httpd-%E5%BC%80%E5%90%AF-https-%E5%92%8C-http2/&quot; title=&quot;Apache Httpd 开启 HTTPS 和 HTTP/2&quot;&gt;Apache Httpd 开启 HTTPS 和 HTTP/2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jb51.net/article/76432.htm&quot; title=&quot;详解为新版Apache服务器开启HTTP/2支持的方法&quot;&gt;详解为新版Apache服务器开启HTTP/2支持的方法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://my.oschina.net/u/3495789/blog/4346276&quot; title=&quot;Apache如何开启HTTP/2&quot;&gt;Apache如何启用HTTP/2&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/apache.DHQksI7g.png"/><enclosure url="/_astro/apache.DHQksI7g.png"/></item><item><title>JavaScript逻辑运算符</title><link>https://clloz.com/blog/logical-operators</link><guid isPermaLink="true">https://clloz.com/blog/logical-operators</guid><pubDate>Thu, 03 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;关于 &lt;code&gt;JavaScript&lt;/code&gt; 中的逻辑运算符，我们经常使用却可能不知道它的一些机制和用法。&lt;/p&gt;
&lt;h2&gt;机制&lt;/h2&gt;
&lt;p&gt;首先我们需要知道几种逻辑运算符的优先级是不同的（关于完整的运算符优先级，看&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/04/05/operator-precedence/&quot; title=&quot;运算符优先级&quot;&gt;运算符优先级&lt;/a&gt;），&lt;strong&gt;逻辑非&gt;逻辑与&gt;逻辑或&gt;条件运算符（三目运算符）&lt;/strong&gt;。运算顺序条件运算符是从右向左，而逻辑与和逻辑或都是从左向右。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//表达式的优先级导致结果不同
;(false &amp;#x26;&amp;#x26; true) || true // 结果为 true
false &amp;#x26;&amp;#x26; (true || true) // 结果为 false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑运算表达式返回的是字表达式的值，而不是一个 &lt;code&gt;Boolean&lt;/code&gt;，只不过很多时候我们使用逻辑表达式的地方帮我们强制转换了，比如 &lt;code&gt;if&lt;/code&gt; 语句等。&lt;/p&gt;
&lt;p&gt;逻辑运算符通常用于布尔型（逻辑）值。这种情况下，它们返回一个布尔值。然而， &lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt; 和 &lt;code&gt;||&lt;/code&gt; 运算符会返回一个指定操作数的值，因此，这些运算符也用于非布尔值。这时，它们也就会返回一个非布尔型值。&lt;/p&gt;
&lt;p&gt;逻辑与 &lt;code&gt;expression1 &amp;#x26;&amp;#x26; expression2&lt;/code&gt; 的机制是，如果 &lt;code&gt;expression1&lt;/code&gt; 能够转换为 &lt;code&gt;true&lt;/code&gt; 那么返回 &lt;code&gt;expression2&lt;/code&gt; ，否则返回 &lt;code&gt;expression1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;逻辑或 &lt;code&gt;expression1 || expression2&lt;/code&gt; 的机制是，如果 &lt;code&gt;expression1&lt;/code&gt; 能够转化为 &lt;code&gt;true&lt;/code&gt; 那么返回 &lt;code&gt;expression1&lt;/code&gt;，否则返回 &lt;code&gt;expression2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;逻辑非 &lt;code&gt;!expression&lt;/code&gt; ，若 &lt;code&gt;expression&lt;/code&gt; 能够转化为 &lt;code&gt;true&lt;/code&gt; 则返回 &lt;code&gt;false&lt;/code&gt;，否则返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;expression&lt;/code&gt; 可能是任何一种类型, 不一定是布尔值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;会被转化为 &lt;code&gt;false&lt;/code&gt; 的表达式有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NaN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;空字符串（&lt;code&gt;&quot;&quot;&lt;/code&gt; or &lt;code&gt;&apos;&apos;&lt;/code&gt; or &lt;code&gt;``&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;需要特别注意的是 &lt;code&gt;undefined&lt;/code&gt;，有些表达式返回的是 &lt;code&gt;undefined&lt;/code&gt; ，比如没有设置 &lt;code&gt;return&lt;/code&gt; 的函数执行的返回值就是 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;尽管 &lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt; 和 &lt;code&gt;||&lt;/code&gt; 运算符能够使用非布尔值的操作数, 但它们依然可以被看作是布尔操作符，因为它们的返回值总是能够被转换为布尔值。如果要显式地将它们的返回值（或者表达式）转换为布尔值，请使用双重非运算符（即&lt;code&gt;!!&lt;/code&gt;）或者 &lt;code&gt;Boolean&lt;/code&gt; 构造函数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;双重非运算符 &lt;code&gt;!!&lt;/code&gt; 可以将任意值强制转换为布尔值，在需要条件判断的地方经常使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;短路计算&lt;/h2&gt;
&lt;p&gt;逻辑运算的机制还存在短路计算：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(some falsy expression1) &amp;#x26;&amp;#x26; (expression2)&lt;/code&gt; 短路计算的结果为假。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(some truthy expression1) || (expression2)&lt;/code&gt; 短路计算的结果为真。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;短路意味着上述表达式中的 &lt;code&gt;expression2&lt;/code&gt; 部分不会被执行，因此 &lt;code&gt;expression2&lt;/code&gt; 的任何副作用都不会生效（举个例子，如果 &lt;code&gt;expression&lt;/code&gt; 是一次函数调用，这次调用就不会发生）。造成这种现象的原因是，整个表达式的值在第一个操作数被计算后已经确定了。&lt;/p&gt;
&lt;h2&gt;用法&lt;/h2&gt;
&lt;p&gt;利用 &lt;code&gt;javascript&lt;/code&gt; 中逻辑运算符支持任意类型和短路计算的特性我们可以将逻辑运算符运用到一些特殊的地方。&lt;/p&gt;
&lt;h2&gt;逻辑与&lt;/h2&gt;
&lt;p&gt;逻辑与可以用来获得第一个假值，比如 &lt;code&gt;expr1 &amp;#x26;&amp;#x26; expr2 &amp;#x26;&amp;#x26; expr3&lt;/code&gt;，当其中存在假值的时候会被返回。也可以类推至前面的表达式都为真的时候执行最后一个表达式来简化判断逻辑的代码，比如 &lt;code&gt;x &gt; 0 &amp;#x26;&amp;#x26; a()&lt;/code&gt;，这可以代替 &lt;code&gt;if&lt;/code&gt; 语句。&lt;/p&gt;
&lt;h2&gt;逻辑或&lt;/h2&gt;
&lt;p&gt;逻辑或可以用来设置默认值，比如你的函数需要用户输入一个参数，如果用户没有输入则给定一个默认值。&lt;code&gt;this.a = param || {}&lt;/code&gt;。&lt;/p&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>npm常用命令</title><link>https://clloz.com/blog/npm-cmd</link><guid isPermaLink="true">https://clloz.com/blog/npm-cmd</guid><pubDate>Wed, 02 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;整理 &lt;code&gt;npm&lt;/code&gt; 常用的一些命令，方便查看。&lt;/p&gt;
&lt;h2&gt;通用命令&lt;/h2&gt;
&lt;h2&gt;帮助命令&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm &amp;#x3C;command&gt; -h  #快速查看某条命令的简单使用帮助，包括语法和别名。
npm -l            #显示所有可用命令和说明
npm help &amp;#x3C;term&gt;   #查看命令的详细帮助
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;初始化&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm init    #在命令行所在的文件夹初始化一个项目（创建 package.json 文件）
npm init --yes    #跳过配置，强制yes
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模块&lt;/h2&gt;
&lt;h2&gt;通用命令&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm root    #查看本地安装的目录

npm root -g    #查看全局安装的目录

npm info package    #查看包信息

npm info &quot;package@latest&quot; peerDependencies #查看包依赖

npm ls    #查看本地安装包

npm ls -g    #查看全局安装包，包含依赖

npm ls -g --depth 0    #查看全局安装包，不包含依赖

npm outdated    #列出所有不是最新版的包，可以带参数

npm cache clean    #清除本地缓存

npm config ls -l    #查看npm配置

npm view package versions    #查看包的所有版本

npm publish     #发布包

npm access    #设置发布包的访问级别

npm search    #搜索registry
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装模块&lt;/h2&gt;
&lt;p&gt;我们经常在 &lt;code&gt;package.json&lt;/code&gt; 看到包的版本号之前有 &lt;code&gt;~ ^ * x&lt;/code&gt; 等符号，这些符号是 &lt;code&gt;semantic-versioning&lt;/code&gt; 语义化版本控制。版本号形如 &lt;code&gt;major.minor.patch&lt;/code&gt;，分别表示 &lt;code&gt;主版本号.次版本号.修补版本号&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;没有任何修饰符的版本号表示必须匹配某个具体版本号。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C; &amp;#x3C;= &gt; &gt;=&lt;/code&gt; 即字面上的意思。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~&lt;/code&gt; 波浪线表示如果 &lt;code&gt;minor&lt;/code&gt; 版本号指定了，那么 &lt;code&gt;minor&lt;/code&gt; 版本号不变，而 &lt;code&gt;patch&lt;/code&gt; 版本号任意，如果 &lt;code&gt;minor&lt;/code&gt; 和 &lt;code&gt;patch&lt;/code&gt; 版本号未指定，那么 &lt;code&gt;minor&lt;/code&gt; 和 &lt;code&gt;patch&lt;/code&gt; 版本号任意，如果 &lt;code&gt;patch&lt;/code&gt; 版本号也制定了，则从指定的数字为范围的下限。下面是 &lt;code&gt;npm&lt;/code&gt; 官方文档的一些例子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;~1.2.3 := &gt;=1.2.3 &amp;#x3C;1.(2+1).0 := &gt;=1.2.3 &amp;#x3C;1.3.0
~1.2 := &gt;=1.2.0 &amp;#x3C;1.(2+1).0 := &gt;=1.2.0 &amp;#x3C;1.3.0 (Same as 1.2.x)
~1 := &gt;=1.0.0 &amp;#x3C;(1+1).0.0 := &gt;=1.0.0 &amp;#x3C;2.0.0 (Same as 1.x)
~0.2.3 := &gt;=0.2.3 &amp;#x3C;0.(2+1).0 := &gt;=0.2.3 &amp;#x3C;0.3.0
~0.2 := &gt;=0.2.0 &amp;#x3C;0.(2+1).0 := &gt;=0.2.0 &amp;#x3C;0.3.0 (Same as 0.2.x)
~0 := &gt;=0.0.0 &amp;#x3C;(0+1).0.0 := &gt;=0.0.0 &amp;#x3C;1.0.0 (Same as 0.x)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;^&lt;/code&gt; 尖括号表示版本号中最左边的非 &lt;code&gt;0&lt;/code&gt; 数字的右侧可以任意。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;^1.2.3 := &gt;=1.2.3 &amp;#x3C;2.0.0
^0.2.3 := &gt;=0.2.3 &amp;#x3C;0.3.0
^0.0.3 := &gt;=0.0.3 &amp;#x3C;0.0.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;x&lt;/code&gt; 表示该位置任意，&lt;code&gt;*&lt;/code&gt; 和 &lt;code&gt;&quot;&quot;&lt;/code&gt; 则表示任意版本。&lt;/p&gt;
&lt;p&gt;关于版本号的语法参考&lt;a href=&quot;https://docs.npmjs.com/cli/v6/using-npm/semver&quot; title=&quot;官方文档&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;可以用连字符表示版本号范围，比如 &lt;code&gt;1.2.3 - 2.3.4&lt;/code&gt; 等价于 &lt;code&gt;&gt;=1.2.3 &amp;#x3C;=2.3.4&lt;/code&gt;，可以用空格来表示逻辑与，&lt;code&gt;||&lt;/code&gt; 表示逻辑或，如：&lt;code&gt;&amp;#x3C;1.0.0 || &gt;=2.3.1 &amp;#x3C;2.4.5 || &gt;=2.5.2 &amp;#x3C;3.0.0&lt;/code&gt;，表示满足这 &lt;code&gt;3&lt;/code&gt; 个范围的版本都可以。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install package    #局部安装模块，安装在命令行所在的文件夹；并将模块依赖写入到 package.json 文件的 dependencies 中（生产环境）
#简写
npm i package

npx install-peerdeps  package #安装包的同时安装所有依赖

npm install --save-prod package    #局部安装时将模块依赖写入到 package.json 文件的 dependencies 中（生产环境），这是默认值，除非指定其他值，所以一般不需要输入这个参数
#简写
npm install -P package

npm install --save-dev package    #局部安装时将模块依赖写入到 package.json 文件的 devDependencies 中（开发环境）
#简写
npm install -D package

npm install --save-optional package #局部安装时将模块依赖写入到 package.json 文件的 optionalDependencies 中.
#简写
npm install -O package

npm install --no-save package #局部安装时阻止模块依赖写入到 package.json 文件的 devDependencies 中.

npm install -g package    #全局安装模块

#从github仓库安装
npm install git://github.com/package/path.git
npm install git://github.com/package/path.git#0.1.0

npm install &amp;#x3C;packageName&gt; --force    #强制重新安装

#安装指定版本
npm install sax@latest
npm install sax@0.1.1
npm install sax@&quot;&gt;=0.1.0 &amp;#x3C;0.2.0&quot;

#安装beta版
npm install &amp;#x3C;module-name&gt;@beta (latest beta)
npm install &amp;#x3C;module-name&gt;@1.3.1-beta.3

#只安装package.json中的dependencies字段的模块
npm install --production
NODE_ENV=production npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里讲一讲 &lt;code&gt;dependencies&lt;/code&gt;，&lt;code&gt;devDependencies&lt;/code&gt;，&lt;code&gt;optionalDependencies&lt;/code&gt; 的区别，在 &lt;code&gt;npm 5.0.0&lt;/code&gt; 之前，局部安装的包是默认不写入 &lt;code&gt;package.json&lt;/code&gt; 中的，那时候需要将模块依赖手动加入 &lt;code&gt;package.json&lt;/code&gt;，后来有个命令就是 &lt;code&gt;npm install --save package&lt;/code&gt; 或者 &lt;code&gt;npm install -S package&lt;/code&gt;，会将模块依赖写入到 &lt;code&gt;dependencies&lt;/code&gt; 中，不过在 &lt;code&gt;5.0.0&lt;/code&gt; 之后模块依赖默认写入到 &lt;code&gt;dependencies&lt;/code&gt; 中，我们已经不需要为 &lt;code&gt;npm install&lt;/code&gt; 添加额外的参数了，现在的缺省参数是 &lt;code&gt;npm install --save-prod package&lt;/code&gt; 或者 &lt;code&gt;npm install -P package&lt;/code&gt;。所以就我们现在的使用来说，生产环境的的包安装直接 &lt;code&gt;npm install&lt;/code&gt; 即可，开发环境的包 &lt;code&gt;npm install -D&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;那么这几种依赖有什么区别呢？大致总结如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dependencies&lt;/code&gt;：应用依赖，或者叫做业务依赖，它用于指定应用依赖的外部包，这些依赖是应用发布后正常执行时所需要的，但不包含测试时或者开发时所使用的包，比如打包工具之类。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;devDependencies&lt;/code&gt;：开发环境依赖，它的对象定义和 &lt;code&gt;dependencies&lt;/code&gt; 一样，只不过它里面的包只用于开发环境，不用于生产环境，这些包通常是单元测试或者打包工具等，例如 &lt;code&gt;gulp, grunt, webpack, moca, coffee&lt;/code&gt; 等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;optionalDependencies&lt;/code&gt;：可选依赖，如果有一些依赖包即使安装失败，项目仍然能够运行或者希望 &lt;code&gt;npm&lt;/code&gt; 继续运行，就可以使用 &lt;code&gt;optionalDependencies&lt;/code&gt;。另外 &lt;code&gt;optionalDependencies&lt;/code&gt; 会覆盖 &lt;code&gt;dependencies&lt;/code&gt; 中的同名依赖包，所以不要在两个地方都写。举个栗子，可选依赖包就像程序的插件一样，如果存在就执行存在的逻辑，不存在就执行另一个逻辑。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;peerDependencies&lt;/code&gt;：可以理解为子依赖，有时我们安装的包会依赖于其他的包。关于这一点可以参考&lt;a href=&quot;https://nodejs.org/es/blog/npm/peer-dependencies/&quot; title=&quot;官方文档&quot;&gt;官方文档&lt;/a&gt;以及&lt;a href=&quot;https://www.cnblogs.com/wonyun/p/9692476.html&quot; title=&quot;探讨npm依赖管理之peerDependencies&quot;&gt;探讨npm依赖管理之peerDependencies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;卸载模块&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm uninstall package    #卸载局部模块

npm uninstall -g package    #卸载全局模块
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;更新模块&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm update package    #更新局部模块

npm update -g package    #更新全局模块

npm update -g package@version   #更新全局模块 package-name 到 x.x.x 版本

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;npm 镜像&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install express --registry https://registry.npm.taobao.org    #临时使用

npm config set registry https://registry.npm.taobao.org    #永久更换 可以用 npm config get registry或npm info

npm config set registry https://registry.npmjs.org    #设置为默认镜像
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以安装 &lt;code&gt;nrm&lt;/code&gt; 管理 &lt;code&gt;npm&lt;/code&gt; 源，安装命令 &lt;code&gt;npm install -g nrm&lt;/code&gt;，它内置了如下源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm ---- https://registry.npmjs.org/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cnpm --- http://r.cnpmjs.org/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;taobao - https://registry.npm.taobao.org/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nj ----- https://registry.nodejitsu.com/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rednpm - http://registry.mirror.cqupt.edu.cn/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npmMirror https://skimdb.npmjs.com/registry/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;edunpm - http://registry.enpmjs.org/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主要有如下几个命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ls&lt;/code&gt;：列出所有可用 &lt;code&gt;registries&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;current&lt;/code&gt;：列出当前使用的 &lt;code&gt;registry&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;use &amp;#x3C;registry&gt;&lt;/code&gt;：切换源&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add &amp;#x3C;registry&gt; &amp;#x3C;url&gt; [home]&lt;/code&gt;：添加一个自定义的源&lt;/li&gt;
&lt;li&gt;&lt;code&gt;del &amp;#x3C;registry&gt;&lt;/code&gt;：删除一个自定义源&lt;/li&gt;
&lt;li&gt;&lt;code&gt;home &amp;#x3C;registry&gt; [browser]&lt;/code&gt;：打开指定源的主页&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test [registry]&lt;/code&gt;：查看指定源的连接情况（响应时间）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;help&lt;/code&gt;：查看帮助&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;npm的包管理机制&lt;/h2&gt;
&lt;p&gt;关于 &lt;code&gt;package.json&lt;/code&gt; 的详细剖析和实际工作中的包管理实践，参考抖音前端整理的&lt;a href=&quot;https://juejin.cn/post/6844904022080667661&quot; title=&quot;npm的包管理机制&quot;&gt;npm的包管理机制&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://javascript.ruanyifeng.com/nodejs/npm.html#toc2&quot; title=&quot;npm模块管理器&quot;&gt;npm模块管理器&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/npm.BF2BTeTu.png"/><enclosure url="/_astro/npm.BF2BTeTu.png"/></item><item><title>软件版本周期 - Wikipedia</title><link>https://clloz.com/blog/software-release-life-cycle</link><guid isPermaLink="true">https://clloz.com/blog/software-release-life-cycle</guid><pubDate>Wed, 02 Sep 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文转自维基百科。&lt;/p&gt;
&lt;h2&gt;开发期&lt;/h2&gt;
&lt;h2&gt;&lt;code&gt;Pre-alpha&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;有时候软件会在 &lt;code&gt;Alpha&lt;/code&gt; 或 &lt;code&gt;Beta&lt;/code&gt; 版本前先发布 &lt;code&gt;Pre-alpha&lt;/code&gt; 版本。一般而言相对于 &lt;code&gt;Alpha&lt;/code&gt; 或 &lt;code&gt;Beta&lt;/code&gt; 版本，&lt;code&gt;Pre-alpha&lt;/code&gt; 版本是一个功能不完整的版本。&lt;/p&gt;
&lt;h2&gt;Alpha&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Alpha&lt;/code&gt; 版本仍然需要测试，其功能亦未完善，因为它是整个软件发布周期中的第一个阶段，所以它的名称是 &lt;code&gt;Alpha&lt;/code&gt;，希腊字母中的第一个字母 &lt;code&gt;α&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Alpha&lt;/code&gt; 版本通常会送到开发软件的组织或某群体中的软件测试者作内部测试。在市场上，越来越多公司会邀请外部客户或合作伙伴参与其测试。这令软件在此阶段有更大的可用性测试。&lt;/p&gt;
&lt;p&gt;在测试的第一个阶段中，开发者通常会进行白盒测试。其他测试会在稍后时间由其他测试团体以黑盒或灰盒技术进行，不过有时会同时进行。&lt;/p&gt;
&lt;h2&gt;Beta&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Beta&lt;/code&gt; 版本是软件最早对外公开的软件版本，由公众（通常为公司外的第三方开发者和业余玩家）参与测试。 因为是 &lt;code&gt;Alpha&lt;/code&gt; 的下一个阶段，所以为希腊字母的第二个字 &lt;code&gt;Beta&lt;/code&gt; (&lt;code&gt;β&lt;/code&gt;)。 一般来说，Beta包含所有功能，但可能有一些已知问题和较轻微的程序错误（&lt;code&gt;BUG&lt;/code&gt;），要进行调试（debug）。Beta版本的测试者通常是开发软件的组织的客户，他们会以免费或优惠价钱得到软件。&lt;code&gt;Beta&lt;/code&gt; 版本亦作为测试产品的支持和市场反应等。&lt;/p&gt;
&lt;p&gt;其他情况不同企业有不同的称法，例如微软曾以 &lt;code&gt;Community Technology Preview&lt;/code&gt;（简称 &lt;code&gt;CTP&lt;/code&gt;，中文称为“社区技术预览”）为发布软件的测试版本之一，微软将这个阶段的软件散布给有需要先行试用的用户或厂商，并收集这些人的使用经验，以便作为进一步修正软件的参考。&lt;/p&gt;
&lt;h2&gt;Release Candidate&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Release Candidate&lt;/code&gt;（简称 &lt;code&gt;RC&lt;/code&gt;）指可能成为最终产品的候选版本，如果未出现问题则可发布成为正式版本。在此阶段的产品通常包含所有功能、或接近完整，亦不会出现严重问题。&lt;/p&gt;
&lt;p&gt;多数开源软件会推出两个 &lt;code&gt;RC&lt;/code&gt; 版本，最后的RC2则成为正式版本。闭源软件较少公开使用，微软公司在 &lt;code&gt;Windows 7&lt;/code&gt; 上应用此名称。苹果公司把在这阶段的产品称为 &lt;code&gt;Golden Master Candidate&lt;/code&gt;（简称&lt;code&gt;GM Candidate&lt;/code&gt;），而最后的 &lt;code&gt;GM&lt;/code&gt; 即成为正式版本。&lt;/p&gt;
&lt;h2&gt;完成期&lt;/h2&gt;
&lt;h2&gt;生产商发放（&lt;code&gt;Release to Manufacting&lt;/code&gt;，&lt;code&gt;RTM&lt;/code&gt;）&lt;/h2&gt;
&lt;p&gt;生产商发放（&lt;code&gt;Release to Manufacturing&lt;/code&gt;，缩写 &lt;code&gt;RTM&lt;/code&gt;）是软件产品准备交付时使用的术语，来自于以前还需要使用实体载具（光盘，硬盘等）来进行安装的时代。某些计算机程序以 &lt;code&gt;RTM&lt;/code&gt; 作为软件版本代号，例如微软 &lt;code&gt;Windows 7&lt;/code&gt; 发行零售版前的 &lt;code&gt;RTM&lt;/code&gt; 版本主要是发放给组装机生产商用，使制造商能够提早进行集成工作或解决软件与硬件设备可能遇到的错误。&lt;code&gt;RTM&lt;/code&gt; 版本并不一定意味着创作者解决了软件所有问题；仍有可能向公众发布前更新版本。以 &lt;code&gt;Windows 7&lt;/code&gt; 为例：&lt;code&gt;RTM&lt;/code&gt; 版与零售版的版本号是一样的。&lt;/p&gt;
&lt;h2&gt;一般可用 GA&lt;/h2&gt;
&lt;p&gt;一般可用（&lt;code&gt;General availability&lt;/code&gt;, 缩写 &lt;code&gt;GA&lt;/code&gt;）是所有必要的商业活动已经完成，该软件产品已经可以发售的阶段。然而，这取决于语言、地域和电子设备与媒体的可用性，有些地区之间可能会有上市时间的延迟。商业活动可能也包括安全性和合法测试，以及本地化和全球销售的可能性评估。&lt;code&gt;RTM&lt;/code&gt; 与 &lt;code&gt;GA&lt;/code&gt; 的间隔可能会是 &lt;code&gt;1&lt;/code&gt; 周或几个月，因为在此过程中需要进行许多商业活动。在这个阶段，可以说软件已经“上线”了。&lt;/p&gt;
&lt;h2&gt;网络分发 RTW&lt;/h2&gt;
&lt;p&gt;网络分发（&lt;code&gt;Release to Web&lt;/code&gt;，缩写 &lt;code&gt;RTW&lt;/code&gt;），或称 &lt;code&gt;Web&lt;/code&gt; 发布是一种利用互联网进行分发的软件交付方式。制造商在这种类型的发布中并不生产实体软件工具，而会借由 &lt;code&gt;OTA&lt;/code&gt; 来进行发放。随着互联网使用人数的增长，&lt;code&gt;RTW&lt;/code&gt; 变得越来越普遍。&lt;/p&gt;
&lt;h2&gt;稳定版 Stable&lt;/h2&gt;
&lt;p&gt;稳定版本来自预览版本释出使用与改善而修正完成，通常是初始版本进行几个小更新后的版本。为目前所使用的软件在符合需求规格的硬件与操作系统中运行不会造成严重的不兼容或是硬件冲突，其已受过某定量的测试无误后所释出者。&lt;/p&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;h2&gt;原始设备制造商（&lt;code&gt;Original Equipment Manufacturer&lt;/code&gt;，&lt;code&gt;OEM&lt;/code&gt;）&lt;/h2&gt;
&lt;p&gt;给计算机厂商随着计算机贩卖的，也就是随机版。只能随机器出货，不能零售。只能全新安装，不能从旧有操作系统升级。包装不像零售版精美，通常只有一张 &lt;code&gt;CD&lt;/code&gt; 和说明书(授权书)。&lt;/p&gt;
&lt;h2&gt;零售版 RTL&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Retail&lt;/code&gt;正式上架零售版。&lt;/p&gt;
&lt;h2&gt;组织团体批量许可 VOL&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Volume OR Volume License for Organizations&lt;/code&gt; 政府部门或大型商业机构批量购买的版本。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CSS 选择器 Selector</title><link>https://clloz.com/blog/css-selector</link><guid isPermaLink="true">https://clloz.com/blog/css-selector</guid><pubDate>Sun, 30 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;去年曾经仔细地学习了一下&lt;a href=&quot;https://www.w3.org/TR/CSS22/selector.html&quot;&gt;css2.2的规范&lt;/a&gt;。其中，选择器，属性的赋值、层叠和继承，盒模型，&lt;strong&gt;&lt;em&gt;视觉格式化模型&lt;/em&gt;&lt;/strong&gt; 几章是最重要的，其实去看这个规范的原因很简单，自己在使用 &lt;code&gt;css&lt;/code&gt; 的过程中，经常出现让自己很迷惑的问题，比如 &lt;code&gt;inline-block&lt;/code&gt; 布局为什么两个块之间出现空隙，&lt;code&gt;vertical-align&lt;/code&gt; 怎么有时有用有时没用，行内元素的高度怎么计算的等等，以往遇到这些问题只能依靠搜索引擎，但是依然不得要领，后面就到 &lt;code&gt;w3c&lt;/code&gt; 去找规范看了看，其实内容也不是非常多，但是要理解透彻还是要结合不断的实践，不然感觉看懂了，理解了，很快又会忘了。&lt;/p&gt;
&lt;h2&gt;什么是 CSS 选择器&lt;/h2&gt;
&lt;p&gt;CSS选择器是我们在前端写样式的时候每天都要用到的，刚接触 &lt;code&gt;CSS&lt;/code&gt; 的时候就会用到 &lt;code&gt;类选择器&lt;/code&gt;，&lt;code&gt;ID选择器&lt;/code&gt;，&lt;code&gt;子选择器&lt;/code&gt;，&lt;code&gt;后代选择器&lt;/code&gt;等等，说白了选择器的目的就是准确地定位到我们想要赋予样式的元素，我们只要掌握 &lt;code&gt;CSS&lt;/code&gt; 的语法和模式匹配的规则，那么我们在使用选择器的时候就能够得心应手了。&lt;/p&gt;
&lt;h2&gt;模式匹配 Pattern matching&lt;/h2&gt;
&lt;p&gt;模式匹配的规则看下图，不管你的选择器是简单的类型选择器，还是丰富的上下文选择器，如果模式中的所有条件对于某个元素都为真，那么选择器就会匹配该元素。这里面有我们熟悉的类型选择器（标签），后代选择器，子选择器，兄弟选择器，属性选择器，伪类选择器。其实我们平时用的最多的类选择器和ID选择器，在 &lt;code&gt;HTML&lt;/code&gt; 里面也属于属性选择器的范畴，比如途中的E#myid写成E[id=&quot;myid&quot;]也是可以匹配的。 &lt;a href=&quot;https://img.clloz.com/blog/cs/css-selector1.png&quot; title=&quot;CSS选择器模式匹配规则&quot;&gt;&lt;img src=&quot;https://clloz.com/_astro/css-selector1.DL6h9xXG_2jVnuN.webp&quot; alt=&quot;CSS选择器模式匹配规则&quot; title=&quot;CSS选择器模式匹配规则&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;选择器语法&lt;/h2&gt;
&lt;p&gt;在我刚开始写 &lt;code&gt;CSS&lt;/code&gt; 的时候，类型和属性，属性和属性之间，是否有空格，逗号，把我搞得很迷糊，踩了不少坑，晚上文章虽多却很少有写的透彻的。其实这也就是选择器语法的问题。复杂的选择器也是由简单选择器通过连接符来构成的，简单选择器包括：类型选择器（标签选择器），属性选择器（包括类选择器和ID选择器），伪类，伪元素，通配符选择器。而连接符一般就是空白符，&lt;code&gt;&gt;&lt;/code&gt; 和 &lt;code&gt;+&lt;/code&gt; ，不过要注意的是我们一般使用 &lt;code&gt;&gt;&lt;/code&gt; 和&lt;code&gt;+&lt;/code&gt; 也会在它们和选择器之间留空白符比如 &lt;code&gt;.a &gt; .b&lt;/code&gt; 而不会写成 &lt;code&gt;.a&gt;.b&lt;/code&gt;，主要是为了可读性。除了上面说到的三个连接符，还有一个逗号需要提一下，在规范中被称之为分组（ &lt;code&gt;grouping&lt;/code&gt; ），就是当几组选择器具有相同的声明的时候我们可以把他们当作一个分组来处理，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;h1 {
  font-family: sans-serif;
}

h2 {
  font-family: sans-serif;
}

h3 {
  font-family: sans-serif;
}

h1,
h2,
h3 {
  font-family: sans-serif;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;他们是等价的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;所有 &lt;code&gt;CSS&lt;/code&gt; 关键字都是 &lt;code&gt;ASCII case-insensitive&lt;/code&gt; 的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;选择器类型&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;简单选择器 &lt;code&gt;simple selector&lt;/code&gt;：单个元素的单个条件。包括 &lt;code&gt;type selector, universal selector, attribute selector, class selector, ID selector, pseudo-class&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;复合选择器 &lt;code&gt;compound selector&lt;/code&gt;：一组没有被关系符分隔的简单选择器，表示单个元素的多个同时发生的条件。&lt;code&gt;type selector&lt;/code&gt; 和 &lt;code&gt;universal selector&lt;/code&gt; 必须放在最前面，并且只能允许有一个 &lt;code&gt;universal selector&lt;/code&gt; 或 &lt;code&gt;type selector&lt;/code&gt;。一个元素要匹配复合选择器就要匹配其中的所有简单选择器。&lt;/li&gt;
&lt;li&gt;关系符 &lt;code&gt;combinator&lt;/code&gt;：关系符是处于两个复合选择器之间的一个表示关系的条件。有后代关系符 &lt;code&gt;descendant combinator&lt;/code&gt; （用空格表示），子代关系符 &lt;code&gt;child combinator&lt;/code&gt;（用 &lt;code&gt;&gt;&lt;/code&gt; 表示），相邻兄弟关系符 &lt;code&gt;next-sibling combinator&lt;/code&gt; （用 &lt;code&gt;+&lt;/code&gt; 表示）和 兄弟关系符 &lt;code&gt;subsequent-sibling combinator&lt;/code&gt;（用 &lt;code&gt;~&lt;/code&gt; 表示）。&lt;/li&gt;
&lt;li&gt;复杂选择器 &lt;code&gt;complex selector&lt;/code&gt;：即一个或多个复合选择器和关系符组成的选择器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关于相邻兄弟选择器和兄弟选择器这里说明一下区别：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 对于math + p这样的相邻兄弟选择器，只有当p紧跟在math后面的时候才会匹配 --&gt;
&amp;#x3C;div&gt;
  &amp;#x3C;math&gt;&amp;#x3C;/math&gt;
  &amp;#x3C;p&gt;&amp;#x3C;/p&gt;
&amp;#x3C;/div&gt;

&amp;#x3C;!-- 对于h1 ~ pre这样的兄弟选择器，pre不需要紧跟在h1后面，只要共享父元素即可匹配 --&gt;
&amp;#x3C;h1&gt;Definition of the function a&amp;#x3C;/h1&gt;
&amp;#x3C;p&gt;Function a(x) has to be applied to all figures in the table.&amp;#x3C;/p&gt;
&amp;#x3C;pre&gt;function a(x) = 12x/13.5&amp;#x3C;/pre&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通配符选择器，类型选择器&lt;/h2&gt;
&lt;p&gt;通配符选择器就是选择所有元素，一般是不建议使用的。类型选择器就是我们理解的标签选择器，他可以匹配文档树中所有该类型元素。&lt;/p&gt;
&lt;h2&gt;后代选择器&lt;/h2&gt;
&lt;p&gt;当我们想要选择某个元素的后代元素，比如匹配所有 &lt;code&gt;h1&lt;/code&gt; 元素包裹的 &lt;code&gt;em&lt;/code&gt; 元素，我们就会用 &lt;code&gt;h1 em { color: red;}&lt;/code&gt; 这种表达方式，后代元素可以有多层，一个后代选择器由两个或者多个选择器由用空白符隔开的选择器组成，注意，此处的多个选择器不一定非要是简单选择器，比如我们可以这样 &lt;code&gt;p .a.b.c { color: blue;}&lt;/code&gt;来选择被p包裹的同时具有 &lt;code&gt;a&lt;/code&gt;，&lt;code&gt;b&lt;/code&gt;，&lt;code&gt;c&lt;/code&gt; 三个类的元素。还有一个场景就是当我们想选择某个元素的所有非子元素的后代的时候我们可以这样写 &lt;code&gt;div * p {display: inline-block;}&lt;/code&gt;，需要注意，这里的 &lt;code&gt;*&lt;/code&gt; 不是连接符，而是通配符选择器。&lt;/p&gt;
&lt;h2&gt;子选择器&lt;/h2&gt;
&lt;p&gt;子选择器匹配某个元素的子集元素，子选择器可以和后代选择器类比，子选择器是后代选择器的子集，因为它只检索一层，而后代选择器则要检索对应元素的全部子元素。子选择器用连接符 &lt;code&gt;&gt;&lt;/code&gt; 连接两个或多个选择器，同样这里的选择器不一定是简单选择器。不同类型的选择器是可以一起使用的，比如 &lt;code&gt;div ul &gt; li p {font-size: 1.2em}&lt;/code&gt; 匹配了作为 &lt;code&gt;div&lt;/code&gt; 后代的无序列表 &lt;code&gt;ul&lt;/code&gt; 的子级&lt;code&gt;li&lt;/code&gt; 的后代 &lt;code&gt;p&lt;/code&gt; 元素。&lt;/p&gt;
&lt;h2&gt;一般兄弟选择器&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;~&lt;/code&gt; 组合器选择兄弟元素，也就是说，后一个节点在前一个节点后面的任意位置，并且共享同一个父节点。语法：&lt;code&gt;A ~ B&lt;/code&gt;，例子：&lt;code&gt;p ~ span&lt;/code&gt; 匹配同一父元素下，&lt;code&gt;&amp;#x3C;p&gt;&lt;/code&gt; 元素后的所有 &lt;code&gt;&amp;#x3C;span&gt;&lt;/code&gt; 元素。注意 &lt;code&gt;B&lt;/code&gt; 必须是在 &lt;code&gt;A&lt;/code&gt; 后面。&lt;/p&gt;
&lt;h2&gt;紧邻兄弟选择器&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;+&lt;/code&gt; 组合器选择相邻元素，即后一个元素紧跟在前一个之后，并且共享同一个父节点。语法：&lt;code&gt;A + B&lt;/code&gt;，例子：&lt;code&gt;h2 + p&lt;/code&gt; 会匹配所有紧邻在 &lt;code&gt;&amp;#x3C;h2&gt;&lt;/code&gt; 元素后的 &lt;code&gt;&amp;#x3C;p&gt;&lt;/code&gt; 元素。注意先后顺序， &lt;code&gt;B&lt;/code&gt; 必须在 &lt;code&gt;A&lt;/code&gt; 后面。&lt;/p&gt;
&lt;h2&gt;属性选择器&lt;/h2&gt;
&lt;p&gt;属性选择器有几种表达方式&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;[attr]&lt;/code&gt; : 匹配拥有属性 &lt;code&gt;attr&lt;/code&gt; 的元素，无论属性值是多少；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[attr=val]&lt;/code&gt;: 匹配拥有属性 &lt;code&gt;attr&lt;/code&gt; 且属性值为 &lt;code&gt;val&lt;/code&gt; 的元素；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[attr~=val]&lt;/code&gt; : 匹配拥有属性 &lt;code&gt;attr&lt;/code&gt; 且 &lt;code&gt;attr&lt;/code&gt; 属性值中包含 &lt;code&gt;val&lt;/code&gt; 的元素（ &lt;code&gt;DOM&lt;/code&gt; 元素的属性可以包含多个属性值，每一个属性值对应一个属性节点，我们常用的 &lt;code&gt;class&lt;/code&gt; 属性就可以有多个值，用逗号分隔开，该条规则同样适用于 &lt;code&gt;attr&lt;/code&gt; 属性值只有一项的元素）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[attr|=val]&lt;/code&gt; :匹配一个有 &lt;code&gt;attr&lt;/code&gt; 属性且值 以 &lt;code&gt;val&lt;/code&gt; 开头后面紧跟着 &lt;code&gt;-&lt;/code&gt; ( &lt;code&gt;U+002D&lt;/code&gt; )的元素。&lt;code&gt;*[lang|=&quot;en&quot;] {color: red;}&lt;/code&gt; 就会匹配下面代码的前三行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[attr^=&quot;val&quot;]&lt;/code&gt; : 匹配拥有属性 &lt;code&gt;attr&lt;/code&gt; 且属性值是以 &lt;code&gt;val&lt;/code&gt; 开头的元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[attr$=&quot;val&quot;]&lt;/code&gt; : 匹配拥有属性 &lt;code&gt;attr&lt;/code&gt; 且属性值是以 &lt;code&gt;val&lt;/code&gt; 结尾的元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[attr*=&quot;val&quot;]&lt;/code&gt;: 匹配拥有 &lt;code&gt;attr&lt;/code&gt; 属性，且属性值包含 &lt;code&gt;val&lt;/code&gt; 的元素。（只要 &lt;code&gt;attr&lt;/code&gt; 属性中的任意一项包含val字符串就匹配该模式）&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;5&lt;/code&gt;，&lt;code&gt;6&lt;/code&gt;，&lt;code&gt;7&lt;/code&gt; 三项都是在 &lt;code&gt;CSS2.1&lt;/code&gt; 规范之后发布的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;p lang=&quot;en&quot;&gt;Hello!&amp;#x3C;/p&gt;
&amp;#x3C;p lang=&quot;en-us&quot;&gt;Greetings!&amp;#x3C;/p&gt;
&amp;#x3C;p lang=&quot;en-au&quot;&gt;G&apos;day!&amp;#x3C;/p&gt;
&amp;#x3C;p lang=&quot;fr&quot;&gt;Bonjour!&amp;#x3C;/p&gt;
&amp;#x3C;p lang=&quot;cy-en&quot;&gt;Jrooana!&amp;#x3C;/p&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;类选择器&lt;/h3&gt;
&lt;p&gt;类选择器其实也是属性选择器的一种，我们平时使用的 &lt;code&gt;.&lt;/code&gt; 其实和刚刚属性选择器中的第三个表达方式 &lt;code&gt;[attr~=val]&lt;/code&gt; 是同样的作用。当我们需要同时匹配多个属性的时候，我们可以将多个属性写到一起，不适用空白符 &lt;code&gt;p.marine.pastoral { color: green }&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;ID 选择器&lt;/h3&gt;
&lt;p&gt;注意，一个元素只能拥有一个 &lt;code&gt;id&lt;/code&gt;，并且文档树中的元素不可以具有相同的 &lt;code&gt;ID&lt;/code&gt;，如果同一个元素两次设置 &lt;code&gt;ID&lt;/code&gt;，那么只有前面一个生效；如果两个元素设置了相同的 &lt;code&gt;ID&lt;/code&gt;，他们的样式都会生效，但是如果你用 &lt;code&gt;js&lt;/code&gt; 获取 &lt;code&gt;ID&lt;/code&gt; 对应的元素只有第一个会被返回。&lt;/p&gt;
&lt;h2&gt;伪元素和伪类&lt;/h2&gt;
&lt;p&gt;伪元素和伪类在&lt;code&gt;CSS2.2&lt;/code&gt; 中的第五章 &lt;code&gt;Selector&lt;/code&gt; 中进行了介绍。但也有一个独立的&lt;a href=&quot;https://drafts.csswg.org/selectors-3/#context&quot; title=&quot;Selectors Level 3&quot;&gt;Selectors Level 3&lt;/a&gt;标准，现在 &lt;code&gt;CSS&lt;/code&gt; 很多模块都是单独的标准。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CSS 2.2&lt;/code&gt; 中，样式通常是根据元素在文档树中的位置添加的。这种简单模型在大多数情况下足够了，但在有些常见的排版场景下可能无法根据文档树的结构来添加样式。例如，&lt;code&gt;HTML 4&lt;/code&gt; 中，没有元素对应的是一个段落的第一行。&lt;/p&gt;
&lt;p&gt;为了允许根据文档树之外的信息来格式化，&lt;code&gt;CSS&lt;/code&gt; 引入了伪元素和伪类的概念。伪元素建立了对超出文档语言指定的文档树的抽象。例如，文档语言不提供访问元素内容的第一个字母或者第一行的机制。CSS伪元素允许样式表设计者引用这个通过其它方式无法访问的信息。伪元素也给样式表设计者提供了一种给源文档中不存在的内容赋予样式的方法（例如，&lt;code&gt;::before&lt;/code&gt; 和 &lt;code&gt;::after&lt;/code&gt; 伪元素提供了访问生成的内容的方法）&lt;/p&gt;
&lt;p&gt;伪类根据元素的特征分类，而不是名字，属性或者内容。原则上，这些特征无法从文档树推断得出。伪类可以是动态的，用户与文档交互时，一个元素可能获得或者失去一个伪类。&lt;code&gt;:first-child&lt;/code&gt; 是个特例，可以根据文档树推断出来，而且 &lt;code&gt;:lang()&lt;/code&gt;在某些情况下也能根据文档树推断出来。伪元素和伪类都不会出现在源文档或者文档树中，伪类允许出现在选择器的任何位置，而一个伪元素只能跟在选择器的最后一个简单选择器后面。&lt;/p&gt;
&lt;p&gt;有些伪类是互斥的，而其它的可以同时用在一个元素上。在规则冲突的情况下，常规层叠顺序决定结果。需要注意的是伪类和伪元素同样参与优先级的计算，伪类和普通类相同，伪元素和普通元素相同。&lt;/p&gt;
&lt;h2&gt;伪元素&lt;/h2&gt;
&lt;p&gt;伪元素有单独的标准：&lt;a href=&quot;https://www.w3.org/TR/css-pseudo-4/#first-letter-pseudo&quot; title=&quot;CSS Pseudo-Elements Module Level 4&quot;&gt;CSS Pseudo-Elements Module Level 4&lt;/a&gt;。目前兼容性达到可用的伪元素一共有四个 &lt;code&gt;::first-letter&lt;/code&gt;，&lt;code&gt;::first-line&lt;/code&gt;，&lt;code&gt;::before&lt;/code&gt;，&lt;code&gt;::after&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;::first-line&lt;/code&gt;表示的是 &lt;code&gt;contents of the first formatted line of its originating element.&lt;/code&gt;。必须作用于包含块 &lt;code&gt;block container&lt;/code&gt; 才能生效。一个元素的 &lt;code&gt;first formatted line&lt;/code&gt; 一定是在它的流内块级后代内（这里的块级不是元素，而是块盒即可）。比如 &lt;code&gt;display&lt;/code&gt; 为 &lt;code&gt;table-cell&lt;/code&gt; 或者 &lt;code&gt;inline-block&lt;/code&gt; 的后代流内元素的第一行就不会匹配到 &lt;code&gt;::first-line&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- div::first-line{color: red;} 生效的是 etcetera --&gt;
&amp;#x3C;div&gt;
  &amp;#x3C;p style=&quot;display: inline-block&quot;&gt;hello&amp;#x3C;br /&gt;goodbye&amp;#x3C;/p&gt;
  etcetera
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码如果我们给 &lt;code&gt;div&lt;/code&gt; 设置了一个宽度，让 &lt;code&gt;etcetera&lt;/code&gt; 不在第一行出现，那么这个元素就没有能够匹配 &lt;code&gt;::first-line&lt;/code&gt; 的部分了。&lt;/p&gt;
&lt;p&gt;还有一点需要注意的是，直接出现在块级元素中的文本按视觉格式化模型来说应该是生成的 &lt;code&gt;anonymous inline box&lt;/code&gt;，内部也应该是行内格式上下文，不过依然能够匹配 &lt;code&gt;::first-line&lt;/code&gt; 的样式，这里是和标准有些出入的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- div::first-line{color: red;} 会对div内的文本生效 --&gt;
&amp;#x3C;div&gt;this is a test this is another test&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再来看下面两个例子的比较&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  div {
    width: 300px;
  }

  div::first-line {
    color: red;
  }
  div span {
    color: green;
  }

  div p {
    color: green;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div&gt;
  &amp;#x3C;span&gt;This is a somewhat long HTML paragraph that will be broken into several lines.&amp;#x3C;/span&gt; The
  first line will be identified by a fictional tag sequence. The other lines will be treated as
  ordinary lines in the paragraph.
&amp;#x3C;/div&gt;

&amp;#x3C;div&gt;
  &amp;#x3C;p&gt;This is a somewhat long HTML paragraph that will be broken into several lines.&amp;#x3C;/p&gt;
  The first line will be identified by a fictional tag sequence. The other lines will be treated as
  ordinary lines in the paragraph.
&amp;#x3C;/div&gt;
```html 最后的效果如下图： ![first-line1](./images/first-line1.png &quot;first-line1&quot;) 如果我们将第一个
`div span` 的 `color: green` 注释掉，得到的结果如下图： ![first-line1](./images/first-line2.png
&quot;first-line2&quot;) 如果我们在第二个 `div` 的 `p` 元素之前插入一个空的 `div` ，则 `div::first-line`
失效。 综合以上的例子，我自己对 `first-line` 做一个总结：`first-line` 只对 `block container`
生效，生效位置就是 `the first formatted line of its originating element`。`first formatted line`
的规则： 1.
如果内部是行内格式上下文，则表示渲染后的第一行，若第一行文本有行内级元素额外设置了样式，则会覆盖
`::first-line`。 2. 如果内部是块级格式上下文，则表示渲染后的第一行中非 `tabel-cell` 和
`inline-block` 的部分。 3. 注意所谓的渲染后的第一行内可能没有文本，比如一个空的 `span` 或者 `p`
元素甚至就元素内部开头的 `br` ，但它们对应的就是 `first formatted line`。如 `
&amp;#x3C;p&gt;
  &amp;#x3C;br /&gt;First...`，这里的第一行对应的就是 `p` 和 `br` 之间的内容，后面的 `First` 并不能匹配
  `::first-line`。 4. 如果第一行存在元素嵌套则递归调用者几条规则。 * * * 讲清楚了 `::first-line`
  以后 `::first-letter` 就简单了，`first-letter` 就是 `the first formatted line of originating
  element` 的第一个字母。但是需要注意的是，有些 `::first-line` 生效的情况 `::first-letter`
  也不存在。比如 `
&amp;#x3C;/p&gt;

&amp;#x3C;div&gt;
  &amp;#x3C;p style=&quot;display: inline-block&quot;&gt;hello&amp;#x3C;br /&gt;goodbye&amp;#x3C;/p&gt;
  etcetera
&amp;#x3C;/div&gt;
` 就没有 `::first-letter`。类似 `
&amp;#x3C;div&gt;
  &amp;#x3C;br /&gt;
  this is a test
&amp;#x3C;/div&gt;
` 同样也没有 `::first-letter`。 * * * `::before` 和 `::after` ：只要他们的 `content` 不为
`none`，他们的表现就像 `originating element`
的紧邻的子元素一样。他们也可以像普通的文档元素一样设置样式，虽然他们并不在文档树中。
需要特别注意的是 `::before` 和 `::after` 一样会参与 `::first-line` 和 `::first-letter`
的计算。比如下面的例子中的第一个 `N` 就会设为红色。 ```html
&amp;#x3C;style&gt;
  p.note::before {
    content: &apos;Note: &apos;;
  }
  p::first-letter {
    color: red;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;p class=&quot;note&quot;&gt;test&amp;#x3C;/p&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;伪类 Pseudo-classes&lt;/h2&gt;
&lt;p&gt;伪类是简单选择器，它允许基于文档树之外的信息进行选择，或者使用其他简单的选择器可能难以表达或无法表达这些信息。它们也可以是动态的，即在用户与文档交互时元素可以获取或丢失伪类的意义上，而无需更改文档本身。伪类不会出现在文档源或文档树中，也不会对其进行修改。&lt;/p&gt;
&lt;p&gt;伪类的语法为一个 &lt;code&gt;:&lt;/code&gt;，后跟伪类的名称作为 &lt;code&gt;CSS&lt;/code&gt; 标识符，如果是函数型伪类，则包含一对括号，其中包含它的参数（比如 &lt;code&gt;:lang()&lt;/code&gt; 就是一个 &lt;code&gt;functional pseudo-class&lt;/code&gt;，而 &lt;code&gt;:valid&lt;/code&gt; 就是一个 &lt;code&gt;regular pseudo-class&lt;/code&gt;）。我将目前标准中的所有伪类都整理出来，见下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/pseudo-classes.BkVSTO7K_Z2wUjAF.webp&quot; alt=&quot;pseudo-classes&quot; title=&quot;pseudo-classes&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意伪类和伪元素不一样，他不是依附于某个文档元素的。伪元素的 &lt;code&gt;::&lt;/code&gt; 前必须由一个文档元素，但是不是所有伪类伪类不是这样。比如 &lt;code&gt;:root&lt;/code&gt;，&lt;code&gt;div :first-child&lt;/code&gt; 都是合法的伪类使用方法，前者表示根元素，后者表示 &lt;code&gt;div&lt;/code&gt; 的第一个子元素。而当使用 &lt;code&gt;div p:first-child&lt;/code&gt; 则表示 &lt;code&gt;div&lt;/code&gt; 的第一个子元素要同时是一个 &lt;code&gt;p&lt;/code&gt; 元素才能匹配，即&lt;code&gt;p:first-child&lt;/code&gt; 组成了一个复合选择器，这里要把&lt;code&gt;:first-child&lt;/code&gt; 当做一个普通的&lt;strong&gt;类&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;不过直接使用伪类而不使用其他简单选择器复合的话（比如 &lt;code&gt;div :first-child&lt;/code&gt; 等），在嵌套情况比较复杂的时候是比较难分析的，比如下面的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  ul :last-child {
    color: red;
    font-weight: bold;
  }
  ul :nth-last-child(1) {
    color: blue;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;ul&gt;
  &amp;#x3C;li&gt;
    Item 1
    &amp;#x3C;ul&gt;
      &amp;#x3C;li&gt;Item 3.1&amp;#x3C;/li&gt;
      &amp;#x3C;li&gt;Item 3.2&amp;#x3C;/li&gt;
      &amp;#x3C;li&gt;Item 3.3&amp;#x3C;/li&gt;
    &amp;#x3C;/ul&gt;
  &amp;#x3C;/li&gt;
  &amp;#x3C;li&gt;
    Item 2
    &amp;#x3C;ul&gt;
      &amp;#x3C;li&gt;Item 3.1&amp;#x3C;/li&gt;
      &amp;#x3C;li&gt;Item 3.2&amp;#x3C;/li&gt;
      &amp;#x3C;li&gt;Item 3.3&amp;#x3C;/li&gt;
    &amp;#x3C;/ul&gt;
  &amp;#x3C;/li&gt;
  &amp;#x3C;li&gt;Item 3&amp;#x3C;/li&gt;
&amp;#x3C;/ul&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最外层的 &lt;code&gt;ul&lt;/code&gt; 选择了 &lt;code&gt;item3&lt;/code&gt; 没有问题，内层嵌套的 &lt;code&gt;ul&lt;/code&gt; 内部所有子元素全部都被选中。原因就是内部的 &lt;code&gt;ul&lt;/code&gt; 都是 &lt;code&gt;li&lt;/code&gt; 的 &lt;code&gt;last-child&lt;/code&gt;，所以样式就应用到了整个 &lt;code&gt;ul&lt;/code&gt; 上。如果我们把选择器改成 &lt;code&gt;ul &gt; *:last-child&lt;/code&gt; 就没有问题。&lt;/p&gt;
&lt;p&gt;所以还是建议大家使用伪类的时候用 &lt;code&gt;p:first-child&lt;/code&gt; 这样的复合选择器，如果确实有找不同类型的子元素需求，则可以像上面的例子中一样用后代关系符和通配符选择器 &lt;code&gt;ul &gt; *:last-child&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;结构伪类&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:root&lt;/code&gt;：匹配文档的根元素。一般的 html 文件的根元素是 html 元素，而 SVG 或 XML 文件的根元素则可能是其他元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:empty&lt;/code&gt;：匹配除空格外没有孩子（包括元素，&lt;code&gt;HTML entity&lt;/code&gt;，文本节点） 的元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:nth-child(an + b)&lt;/code&gt;: 根据位置匹配兄弟元素，&lt;code&gt;n&lt;/code&gt; 为自然数，&lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 都必须为整数，并且元素的第一个子元素的下标为 &lt;code&gt;1&lt;/code&gt;。标准中还有一种 &lt;code&gt;of&lt;/code&gt; 语法，不过目前只有 &lt;code&gt;safari&lt;/code&gt; 支持。一般使用都会添加父元素，否则会在全部文档生效。比如 &lt;code&gt;div p:nth-child(2n + 1)&lt;/code&gt;，匹配 &lt;code&gt;div&lt;/code&gt; 内处于奇数位的 &lt;code&gt;p&lt;/code&gt; 元素，注意的是要同时满足奇数和 &lt;code&gt;p&lt;/code&gt; 才能匹配。注意这里的计算都是以元素为单位，&lt;code&gt;&amp;#x3C;br&gt;&lt;/code&gt; 也是元素。&lt;code&gt;2n&lt;/code&gt; 与 &lt;code&gt;even&lt;/code&gt; 等价，&lt;code&gt;2n+1&lt;/code&gt; 与 &lt;code&gt;odd&lt;/code&gt; 等价。下面举了一个简单的例子，更多示例请查看&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/CSS/:nth-child&quot; title=&quot;:nth-child() - MDN&quot;&gt;:nth-child() - MDN&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 1, 5, 7 生效 --&gt;
&amp;#x3C;style&gt;
  div p:nth-child(2n + 1) {
    color: red;
  }
  div::before {
    content: &apos;test&apos;;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div&gt;
  &amp;#x3C;p&gt;1&amp;#x3C;/p&gt;
  &amp;#x3C;span&gt;2&amp;#x3C;/span&gt;
  &amp;#x3C;span&gt;3&amp;#x3C;/span&gt;
  &amp;#x3C;p&gt;4&amp;#x3C;/p&gt;
  &amp;#x3C;p&gt;5&amp;#x3C;/p&gt;
  &amp;#x3C;p&gt;6&amp;#x3C;/p&gt;
  &amp;#x3C;p&gt;7&amp;#x3C;/p&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:nth-last-child()&lt;/code&gt;: 和 &lt;code&gt;:nth-child()&lt;/code&gt; 类似，不同的是从末尾开始计数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:first-child&lt;/code&gt;: 匹配一组兄弟元素中的第一个，比如 &lt;code&gt;div &gt; p:first-child&lt;/code&gt; 表示匹配 &lt;code&gt;div&lt;/code&gt; 的 &lt;code&gt;p&lt;/code&gt; 子元素中的第一个，要同时满足 &lt;code&gt;p&lt;/code&gt; 和第一个。需要注意的是 &lt;code&gt;div &gt; p:first-child&lt;/code&gt; ， &lt;code&gt;div p:first-child&lt;/code&gt; 和 &lt;code&gt;div :first-child&lt;/code&gt;（有空格）的不同。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 1 和 2 都生效 --&gt;
&amp;#x3C;style&gt;
  div :first-child {
    color: red;
  }
&amp;#x3C;/style&gt;

&amp;#x3C;div&gt;
  &amp;#x3C;div&gt;1&amp;#x3C;/div&gt;
  &amp;#x3C;div&gt;
    &amp;#x3C;div&gt;2&amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:last-child&lt;/code&gt;: 匹配一组兄弟元素中的最后一个。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:only-child&lt;/code&gt;: 匹配一个没有兄弟元素的元素。比如 &lt;code&gt;div &gt; p:only-child&lt;/code&gt; 表示当 &lt;code&gt;div&lt;/code&gt; 中只有一个 &lt;code&gt;p&lt;/code&gt; 元素时匹配。和 &lt;code&gt;:first-child:last-child&lt;/code&gt; 或 &lt;code&gt;:nth-child(1):nth-last-child(1)&lt;/code&gt; 效果相同，不过前者的优先级更低。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:nth-of-type()&lt;/code&gt;：语法和 &lt;code&gt;:nth-child()&lt;/code&gt; 类似，但是它的选择范围不再是所有兄弟元素，而是根据给定的类型筛选出相同类型的标签作为范围。比如 &lt;code&gt;img:nth-of-type(2n + 1)&lt;/code&gt; 就表示匹配所有奇数位置的 &lt;code&gt;img&lt;/code&gt; 兄弟元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:nth-last-of-type()&lt;/code&gt;: 和 &lt;code&gt;:nth-of-type()&lt;/code&gt; 类型，不过是从末尾开始计数。比如 &lt;code&gt;body &gt; h2:nth-of-type(n+2):nth-last-of-type(n+2)&lt;/code&gt; 选中 &lt;code&gt;body&lt;/code&gt; 中除了第一个和最后一个的 &lt;code&gt;h2&lt;/code&gt; 元素，等价于 &lt;code&gt;body &gt; h2:not(:first-of-type):not(:last-of-type)&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:first-of-type&lt;/code&gt;: 等价于 &lt;code&gt;:nth-of-type(1)&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:last-of-type&lt;/code&gt;: 等价于 &lt;code&gt;:nth-last-of-type(1)&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:only-of type&lt;/code&gt;: 等价于 &lt;code&gt;:first-of-type:last-of-type&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;逻辑伪类&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;:is()&lt;/code&gt;: 原来的名字是 &lt;code&gt;:match()&lt;/code&gt;，一些旧的浏览器上用的是 &lt;code&gt;:any()&lt;/code&gt;。该伪类函数将选择器列表作为参数，并选择该列表中任意一个选择器可以选择的元素。这对于以更紧凑的形式编写大型选择器非常有用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:not()&lt;/code&gt;: 该伪类函数用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中，它也被称为反选伪类（&lt;code&gt;negation pseudo-class&lt;/code&gt;）。需要注意的是，内部参数不能有伪元素，也不能有 &lt;code&gt;:not()&lt;/code&gt; 本身。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:where()&lt;/code&gt;: 该伪类和 &lt;code&gt;:is()&lt;/code&gt; 的语法和功能完全按相同，不同的是 &lt;code&gt;:is()&lt;/code&gt; 的优先级是由参数列表中优先级最高的选择器决定的，而 &lt;code&gt;:where()&lt;/code&gt; 的优先级总是 &lt;code&gt;0&lt;/code&gt;。目前只有 &lt;code&gt;firefox&lt;/code&gt; 实现了这个伪类。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:has()&lt;/code&gt;: 对某个元素，参数对应的选择器至少匹配个子元素。比如 &lt;code&gt;a:has(&gt; img)&lt;/code&gt; 匹配含有子元素 &lt;code&gt;img&lt;/code&gt; 的 &lt;code&gt;a&lt;/code&gt; 元素。目前没有浏览器实现。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;状态伪类&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;:hover&lt;/code&gt;: 适用于用户使用指示设备虚指一个元素（鼠标指针虚指在某个元素但没有激活）的情况。这个样式会被任何与链接相关的伪类重写，像 &lt;code&gt;:link&lt;/code&gt;, &lt;code&gt;:visited&lt;/code&gt;, 和 &lt;code&gt;:active&lt;/code&gt; 等。为了确保生效，&lt;code&gt;:hover&lt;/code&gt;规则需要放在 &lt;code&gt;:link&lt;/code&gt; 和 &lt;code&gt;:visited&lt;/code&gt; 规则之后，但是在 &lt;code&gt;:active&lt;/code&gt; 规则之前，按照 &lt;code&gt;LVHA&lt;/code&gt; 的循顺序声明 &lt;code&gt;:link－:visited－:hover－:active&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:active&lt;/code&gt;: 匹配被用户激活的元素。它让页面能在浏览器监测到激活时给出反馈。当用鼠标交互时，它代表的是用户按下按键和松开按键之间的时间。一般被用在 &lt;code&gt;&amp;#x3C;a&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;button&gt;&lt;/code&gt; 元素中. 这个伪类的一些其他适用对象包括包含激活元素的元素，以及可以通过他们关联的 &lt;code&gt;&amp;#x3C;label&gt;&lt;/code&gt; 标签被激活的表格元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:focus&lt;/code&gt;: 示获得焦点的元素（如表单输入）。当用户点击或触摸元素或通过键盘的 &lt;code&gt;tab&lt;/code&gt; 键选择它时会被触发。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:focus-within&lt;/code&gt;: 表示一个元素获得焦点，或，该元素的后代元素获得焦点。换句话说，元素自身或者它的某个后代匹配 &lt;code&gt;:focus&lt;/code&gt; 伪类。该选择器非常实用。举个通俗的例子：表单中的某个 &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt; 字段获得焦点时，整个表单的 &lt;code&gt;&amp;#x3C;form&gt;&lt;/code&gt; 元素都可被高亮。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:focus-visible&lt;/code&gt;: 实验功能，当元素匹配 &lt;code&gt;:focus&lt;/code&gt; 伪类并且客户端( &lt;code&gt;UA&lt;/code&gt; )的启发式引擎决定焦点应当可见(在这种情况下很多浏览器默认显示“焦点框”。)时，&lt;code&gt;:focus-visible&lt;/code&gt; 伪类将生效。这个选择器可以有效地根据用户的输入方式(鼠标或键盘)展示不同形式的焦点。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;链接伪类&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;:link&lt;/code&gt;: 选中元素当中的链接.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:any-link&lt;/code&gt;: 匹配一个有链接锚点的元素，而不管它是否被访问过，也就是说，它会匹配每一个有 &lt;code&gt;href&lt;/code&gt; 属性的 &lt;code&gt;&amp;#x3C;a&gt;&lt;/code&gt;、&lt;code&gt;&amp;#x3C;area&gt;&lt;/code&gt; 或 &lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt; 元素。因此，它会匹配到所有的 &lt;code&gt;:link&lt;/code&gt; 或 &lt;code&gt;:visited&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:visited&lt;/code&gt;: 匹配用户已访问过的链接。出于隐私原因，可以使用此选择器修改的样式非常有限。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:target&lt;/code&gt;: 匹配一个唯一的页面元素(目标元素)，其 &lt;code&gt;id&lt;/code&gt; 与当前 &lt;code&gt;URL&lt;/code&gt; 片段匹配 。比如当前页面 &lt;code&gt;URL&lt;/code&gt; 为 &lt;code&gt;http://www.example.com/index.html#section2&lt;/code&gt;，则 &lt;code&gt;:target&lt;/code&gt; 将会匹配一个 &lt;code&gt;id&lt;/code&gt; 为 &lt;code&gt;section2&lt;/code&gt; 的元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:scope&lt;/code&gt;: 表示作为选择器要匹配的参考点元素。当前，在样式表中使用时, &lt;code&gt;:scope&lt;/code&gt; 等效于 &lt;code&gt;:root&lt;/code&gt;，因为目前尚无一种方法来显式建立作用域元素。当从 &lt;code&gt;DOM API&lt;/code&gt; 使用，如（&lt;code&gt;querySelector(), querySelectorAll(), matches(), 或 Element.closest()&lt;/code&gt;）, &lt;code&gt;:scope&lt;/code&gt; 匹配你调用 &lt;code&gt;API&lt;/code&gt; 的元素。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;表单伪类&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;:enabled&lt;/code&gt;: 表示任何被启用的（&lt;code&gt;enabled&lt;/code&gt;）元素。如果一个元素能够被激活（如选择、点击或接受文本输入），或者能够获取焦点，则该元素是启用的。元素也有一个禁用的状态（&lt;code&gt;disabled state&lt;/code&gt;），在被禁用时，元素不能被激活或获取焦点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:disabled&lt;/code&gt;: 表示任何被禁用的元素。如果一个元素不能被激活（如选择、点击或接受文本输入）或获取焦点，则该元素处于被禁用状态。元素还有一个启用状态（&lt;code&gt;enabled state&lt;/code&gt;），在启用状态下，元素可以被激活或获取焦点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:read-only&lt;/code&gt;: 表示元素不可被用户编辑的状态（如锁定的文本输入框）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:read-write&lt;/code&gt;: 匹配可以被用户编辑的元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:placeholder-shown&lt;/code&gt;: 在 &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt; 或 &lt;code&gt;&amp;#x3C;textarea&gt;&lt;/code&gt; 元素显示 &lt;code&gt;placeholder text&lt;/code&gt; 时生效。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:default&lt;/code&gt;: 表示一组相关元素中的默认表单元素，可以理解为给默认选项一个特殊状态，告诉用户哪个选项是默认的。该选择器可以在 &lt;code&gt;&amp;#x3C;button&gt;, &amp;#x3C;input type=&quot;checkbox&quot;&gt;, &amp;#x3C;input type=&quot;radio&quot;&gt;, 以及 &amp;#x3C;option&gt;&lt;/code&gt; 上使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:checked&lt;/code&gt;: 表示任何处于选中状态的&lt;code&gt;radio&lt;/code&gt;, &lt;code&gt;checkbox&lt;/code&gt; 或 &lt;code&gt;select&lt;/code&gt; 元素中的&lt;code&gt;option HTML&lt;/code&gt;元素。用户通过勾选/选中元素或取消勾选/取消选中，来改变该元素的 &lt;code&gt;:checked&lt;/code&gt; 状态。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:indeterminate&lt;/code&gt;: 表示状态不确定的表单元素。&lt;code&gt;indeterminate&lt;/code&gt; 属性可以用 &lt;code&gt;JavaScript&lt;/code&gt; 进行设置，布尔型。这个属性可以影响的元素包括 &lt;code&gt;indeterminate&lt;/code&gt; 属性为 &lt;code&gt;true&lt;/code&gt; 的 &lt;code&gt;checkbox&lt;/code&gt;；同 &lt;code&gt;name&lt;/code&gt; 的所有 &lt;code&gt;radio&lt;/code&gt; 都未被选中或没设置 &lt;code&gt;name&lt;/code&gt; 的 &lt;code&gt;radio&lt;/code&gt;；状态不确定的 &lt;code&gt;&amp;#x3C;progress&gt;&lt;/code&gt; 元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:blank&lt;/code&gt;: 匹配用户输入为空的输入框，如 &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;textarea&gt;&lt;/code&gt;。目前尚未有浏览器支持。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:valid&lt;/code&gt;: 内容验证正确的 &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt; 或其他 &lt;code&gt;&amp;#x3C;form&gt;&lt;/code&gt; 元素。这能简单地将校验字段展示为一种能让用户辨别出其输入数据的正确性的样式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:invalid&lt;/code&gt;: 任意内容未通过验证的 &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt; 或其他 &lt;code&gt;&amp;#x3C;form&gt;&lt;/code&gt; 元素。可以突出显示用户的错误输入。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:in-range/:out-range&lt;/code&gt;: 代表一个 &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt; 元素，其当前值处于属性 &lt;code&gt;min&lt;/code&gt; 和 &lt;code&gt;max&lt;/code&gt; 限定的范围之内/之外。该伪类仅适用于那些拥有（或可以接受）取值范围设定的元素，包括 &lt;code&gt;type&lt;/code&gt; 为 &lt;code&gt;date&lt;/code&gt;，&lt;code&gt;time&lt;/code&gt;，&lt;code&gt;week&lt;/code&gt;，&lt;code&gt;month&lt;/code&gt;，&lt;code&gt;datetime-local&lt;/code&gt;，&lt;code&gt;number&lt;/code&gt; 和 &lt;code&gt;range&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:required&lt;/code&gt;: 任意设置了 &lt;code&gt;required&lt;/code&gt; 属性的 &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt; ，&lt;code&gt;&amp;#x3C;select&gt;&lt;/code&gt; , 或 &lt;code&gt;&amp;#x3C;textarea&gt;&lt;/code&gt; 元素。 这个伪类对于高亮显示在提交表单之前必须具有有效数据的字段非常有用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:optional&lt;/code&gt;: 表示任意没有 &lt;code&gt;required&lt;/code&gt; 属性的 &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt;，&lt;code&gt;&amp;#x3C;select&gt;&lt;/code&gt; 或 &lt;code&gt;&amp;#x3C;textarea&gt;&lt;/code&gt; 元素使用它。该伪类可让表单展示可选字段并且渲染其外观。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;语言相关伪类&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;:dir()&lt;/code&gt;: 匹配特定文字书写方向的元素。试验性功能，只有 &lt;code&gt;firefox&lt;/code&gt; 实现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:lang()&lt;/code&gt;: 基于元素语言来匹配页面元素。可以用 &lt;code&gt;quotes&lt;/code&gt; 属性来根据语言设置引号。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CSS&lt;/code&gt; 的标准目前还是非常快的变化阶段，我们主要还是掌握一些核心用法，一些特别的用法可能会持续在标准中发生变化，如果&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;http://www.alloyteam.com/2016/05/summary-of-pseudo-classes-and-pseudo-elements/#prettyPhoto&quot; title=&quot;总结伪类与伪元素&quot;&gt;总结伪类与伪元素&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CSS 标准文档&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/CSS/Reference&quot; title=&quot;CSS 参考 - MDN&quot;&gt;CSS 参考 - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>几个有用的 Mac 命令</title><link>https://clloz.com/blog/mac-useful-cmd</link><guid isPermaLink="true">https://clloz.com/blog/mac-useful-cmd</guid><pubDate>Sun, 30 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 &lt;a href=&quot;https://www.clloz.com/programming/computer-science/operating-system/2020/08/18/linux-command/&quot; title=&quot;Linux常用命令&quot;&gt;Linux常用命令&lt;/a&gt; 中介绍了一些在 &lt;code&gt;Linux&lt;/code&gt; 中的常用命令。虽然 &lt;code&gt;Mac OS X&lt;/code&gt; 和 &lt;code&gt;Linux&lt;/code&gt; 都是基于 &lt;code&gt;Unix&lt;/code&gt; 的，但是有些系统命令并不是通用的，本文就介绍一些只有 &lt;code&gt;Mac&lt;/code&gt; 上可以使用的比较有用的命令。&lt;/p&gt;
&lt;h2&gt;pbcopy 和 pbpaste&lt;/h2&gt;
&lt;p&gt;原来我想复制一个文件中的内容，都是用 &lt;code&gt;cat&lt;/code&gt; 命令打印出来然后复制，或者直接在软件中打开文件后再全选复制。在 &lt;code&gt;Mac&lt;/code&gt; 中其实有命令可以直接实现这个功能，就是 &lt;code&gt;pbcopy&lt;/code&gt; 和 &lt;code&gt;pbpaste&lt;/code&gt;。这两个命令打通了终端和剪贴板。它们不仅可以复制文件，也可以结合其他命令进行使用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#将主目录的文件列表复制到剪贴板
ls ~ | pbcopy

#将任意文件的内容复制到剪贴板
pbcopy &amp;#x3C; filename.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;mdfind&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mdfind&lt;/code&gt; 是 &lt;code&gt;Mac&lt;/code&gt; 上更强大的文件搜索命令，可以理解为命令行版的 &lt;code&gt;spotlight&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#按文件名关键字查找文件：
mdfind -name keyword

#查找文件内容中包含关键字的文件
mdfind &quot;key string&quot;

#-onlyin 参数指定搜索范围
mdfind -onlyin ~/Library txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;launchctl&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 上没有 &lt;code&gt;systemctl&lt;/code&gt; 和 &lt;code&gt;services&lt;/code&gt; 命令。管理服务的命令是 &lt;code&gt;launchctl&lt;/code&gt;，我个人觉得不是很好用，如果你有使用 &lt;code&gt;homebrew&lt;/code&gt; 也可以使用 &lt;code&gt;homebrew&lt;/code&gt; 的 &lt;code&gt;services&lt;/code&gt; 管理命令 &lt;code&gt;brew services&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#加载一个服务到启动列表
sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist
#卸载一个服务
sudo launchctl unload  /System/Library/LaunchDaemons/ssh.plist
#查看服务
sudo launchctl list | grep &amp;#x3C;&amp;#x3C;Service Name&gt;&gt;
#停止
sudo launchctl stop &amp;#x3C;&amp;#x3C;Service Name&gt;&gt;
#开始
sudo launchctl start &amp;#x3C;&amp;#x3C;Service Name&gt;&gt;
#kill
sudo launchctl kill &amp;#x3C;&amp;#x3C;Service Name&gt;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;say&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;say&lt;/code&gt; 命令使用 &lt;code&gt;VoiceOver&lt;/code&gt; 给你朗读文本，比较特别的是你可以选择语言和发音（需要安装 &lt;code&gt;System Preferences -&gt; Accessibility -&gt; Speech&lt;/code&gt;）。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000000509514&quot; title=&quot;8个不可不知的Mac OS X专用命令行工具&quot;&gt;8个不可不知的Mac OS X专用命令行工具&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/macos.RfaH1x-K.jpg"/><enclosure url="/_astro/macos.RfaH1x-K.jpg"/></item><item><title>Alfred入门</title><link>https://clloz.com/blog/alfred</link><guid isPermaLink="true">https://clloz.com/blog/alfred</guid><pubDate>Fri, 28 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 系统本身提供了 &lt;code&gt;spotlight&lt;/code&gt; 来供用户查询应用、文稿及其他文件等信息，但是我个人觉得不是很好用，一直都没怎么用过。&lt;code&gt;Alfred&lt;/code&gt; 以前也听说过，但是没有详细了解，我一直也为很鸡肋。不过今天仔细研究了一下感觉确实是一个能提高 &lt;code&gt;Mac&lt;/code&gt; 使用效率的工具。本文讲一下该软件的一些基础用法。&lt;/p&gt;
&lt;h2&gt;购买安装&lt;/h2&gt;
&lt;p&gt;软件有两个版本，&lt;code&gt;single license&lt;/code&gt; 和 &lt;code&gt;mega supporter&lt;/code&gt;，前者可以在两台设备上上使用当前版本（即小版本可以更新，大版本不能更新，你买的 &lt;code&gt;4.x&lt;/code&gt; 版本就不能更新到 &lt;code&gt;5.x&lt;/code&gt;），后者就是在以太设备上永久更新。现在绝大多数买断制的软件都是这种模式。价格分别是 &lt;code&gt;29£&lt;/code&gt; 和 &lt;code&gt;49£&lt;/code&gt;，真的是有点贵，我是在淘宝上买的那种授权版。&lt;/p&gt;
&lt;p&gt;也可以使用免费版，那样的话你只能进行基础的文件查找。&lt;/p&gt;
&lt;h2&gt;功能&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Alfred&lt;/code&gt; 提供了非常丰富的功能，所有功能的调用方式都是以命令的形式，和终端的感觉类似，只不过它能在任何环境下直接启动，比较方便。&lt;/p&gt;
&lt;h2&gt;基础设置&lt;/h2&gt;
&lt;p&gt;基础设置中主要就是设置开机启动和调用快捷键。我的快捷键设置为双击 ⌘ 启动 &lt;code&gt;Alfred&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在默认的查找结果中（即没有给出任何执行的情况下），可以选择哪些文件类型或文件夹需要被检索。这里我就添加了两个文件夹到 &lt;code&gt;Search Scope&lt;/code&gt; 中，一个是 &lt;code&gt;~&lt;/code&gt; 文件夹，一个是我的 &lt;code&gt;SD&lt;/code&gt; 卡的对应的 &lt;code&gt;Volume&lt;/code&gt;。如果你查找某个文件夹没有找到，那么可以检查一下是否对应的文件夹不在软件的检索范围。&lt;/p&gt;
&lt;p&gt;如果没有检查到任何结果，默认情况下会显示三个查询选项：在谷歌，亚马逊，维基中查找。&lt;/p&gt;
&lt;p&gt;查询出的选项除了可以用 ⌘ 加上数字进行选择。&lt;/p&gt;
&lt;h2&gt;文件查找&lt;/h2&gt;
&lt;p&gt;文件查找分为两个部分，一个是基础的 &lt;code&gt;file search&lt;/code&gt;；另一个是 &lt;code&gt;action&lt;/code&gt;，即对查找到的文件夹进行一些操作。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;file search&lt;/code&gt; 的部分主要有四个指令 &lt;code&gt;open&lt;/code&gt;，&lt;code&gt;find&lt;/code&gt;，&lt;code&gt;in&lt;/code&gt; 和 &lt;code&gt;tags&lt;/code&gt;。&lt;code&gt;open&lt;/code&gt; 和 &lt;code&gt;find&lt;/code&gt; 都非常简单，&lt;code&gt;in&lt;/code&gt; 是用来查询文本内容是否包含某一串字符；&lt;code&gt;tags&lt;/code&gt; 则是查询文件夹标记的（估计大部分人很少使用，除了默认的几个颜色标记还可以在 &lt;code&gt;Finder&lt;/code&gt; 的 &lt;code&gt;Preferences&lt;/code&gt; 中添加），比如你有一个 &lt;code&gt;tags&lt;/code&gt; 是 &lt;code&gt;工作&lt;/code&gt;，那么你用 &lt;code&gt;tags 工作&lt;/code&gt; 就能找到打了这个 &lt;code&gt;tag&lt;/code&gt; 的文件夹。&lt;/p&gt;
&lt;p&gt;文件查询也支持导航，你可以用左右方向键进入或退出文件夹，&lt;code&gt;previous&lt;/code&gt; 执行可以打开上次打开的文件夹的父文件夹。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;action&lt;/code&gt; 则是和文件相关的一系列操作，我们用 &lt;code&gt;find&lt;/code&gt; 指令找到对应文件夹选项后，点击 ⌃ （可以自己录制快捷键）就可以打开操作面板，选择你想要的操作。操作包括在 &lt;code&gt;finder&lt;/code&gt; 中查看，复制文件，复制路径等。&lt;/p&gt;
&lt;h2&gt;Web Search 和 Web Bookmarks&lt;/h2&gt;
&lt;p&gt;这两个功能就比较简单，&lt;code&gt;Web Search&lt;/code&gt; 就是提供了一些常用网站的查询链接，直接可以输入关键词加查询内容直接在浏览器中打开查询页面，只是一个简化操作而已。软件自己提供了常用的一些网站，你也可以根据自己的需求添加，&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Web Bookmarks&lt;/code&gt; 也很好理解，就是检索内容包括了浏览器的书签（只支持 &lt;code&gt;safari&lt;/code&gt; 和 &lt;code&gt;chrome&lt;/code&gt;），也就是你只要保存了一个书签，直接调出 &lt;code&gt;Alfred&lt;/code&gt; 的查询框就可以进行模糊查询，这一功能还是非常方便的。&lt;/p&gt;
&lt;h2&gt;剪切板历史&lt;/h2&gt;
&lt;p&gt;这个功能也是比较好用的，它能够记录我们进行剪切复制操作的历史，包括了文本，图片和文件。只要输入关键词 &lt;code&gt;clipboard&lt;/code&gt; 就能够进行查询（也可以录制快捷键）。&lt;/p&gt;
&lt;h2&gt;计算器和字典&lt;/h2&gt;
&lt;p&gt;这两个功能都不用说太多，比较简单也很好用。&lt;code&gt;Mac&lt;/code&gt; 自带的字典不是很好用，也没有发音，建议使用下面插件部分介绍的有道词典。&lt;/p&gt;
&lt;h2&gt;snippets&lt;/h2&gt;
&lt;p&gt;这个功能可以称为模板，有时候我们需要在一些地方填写自己的地址邮件等比较长的文本，我们可以预先在软件中写好模板，然后只要在任意文本框内输入指令就会直接转化成我们模版中的内容。比如你的地址用关键词 &lt;code&gt;\\address&lt;/code&gt; 为名字记录成模版，然后在任意文本框中输入 &lt;code&gt;\\address&lt;/code&gt; 会自动替换为你的地址。&lt;/p&gt;
&lt;h2&gt;系统指令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Alfred&lt;/code&gt; 还提供了丰富的 &lt;code&gt;Mac&lt;/code&gt; 系统功能对应的指令，比如锁屏，重启，关机，清空回收站，音量调节，强制关闭等等。&lt;/p&gt;
&lt;h2&gt;终端&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Alfred&lt;/code&gt; 也支持直接执行终端命令，指令为 &lt;code&gt;&amp;#x3C;&lt;/code&gt;，默认调用的终端是 &lt;code&gt;terminal&lt;/code&gt;。如果你跟我一样使用的是 &lt;code&gt;iTerm&lt;/code&gt;，那么你就参考 &lt;a href=&quot;https://github.com/vitorgalvao/custom-alfred-iterm-scripts&quot; title=&quot;更换默认终端为iTerm2&quot;&gt;更换默认终端为iTerm2&lt;/a&gt; 进行更换。不过根据我的实际使用情况，这个功能比较鸡肋，因为每次都还是在终端打开一个新的 &lt;code&gt;tab&lt;/code&gt;，然后执行指令，不是很好用。&lt;/p&gt;
&lt;h2&gt;插件&lt;/h2&gt;
&lt;p&gt;目前没有一个专门管理插件的网站，如果你想寻找插件，可以到&lt;a href=&quot;https://www.alfredapp.com/workflows/&quot; title=&quot;官网&quot;&gt;官网&lt;/a&gt;以及&lt;a href=&quot;https://www.alfredforum.com/&quot; title=&quot;官方论坛&quot;&gt;官方论坛&lt;/a&gt;。其他的只能借助搜索引擎查询或者按照自己的需求写一个。下面放一下我尝试的插件的链接。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;http://www.packal.org/workflow/colors&quot; title=&quot;颜色格式转换&quot;&gt;颜色格式转换&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/deanishe/alfred-reddit&quot; title=&quot;Reddit浏览工具&quot;&gt;Reddit浏览工具&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/deanishe/alfred-stackexchange&quot; title=&quot;StackOverFlow问题查询工具&quot;&gt;StackOverFlow问题查询工具&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zqzten/alfred-web-search-suggest&quot; title=&quot;搜索工具（支持谷歌，百度，知乎，微博等）&quot;&gt;搜索工具（支持谷歌，百度，知乎，微博等）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/h3l/douban-workflow&quot; title=&quot;豆瓣书籍电影查询工具&quot;&gt;豆瓣书籍电影查询工具&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/gharlan/alfred-github-workflow&quot; title=&quot;github工具&quot;&gt;github工具&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/derimagia/awesome-alfred-workflows&quot; title=&quot;awesome-workflow&quot;&gt;awesome-workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/wensonsmith/YoudaoTranslate&quot; title=&quot;有道翻译&quot;&gt;有道翻译&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ajgon/alfred2-html-entity-lookup&quot; title=&quot;html字符实体查询&quot;&gt;html字符实体查询&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ruedap/alfred-font-awesome-workflow&quot; title=&quot;font awesome 图表查询&quot;&gt;font awesome 图标查询&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/willfarrell/alfred-pkgman-workflow&quot; title=&quot;包查询&quot;&gt;包查询&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/willfarrell/alfred-encode-decode-workflow&quot; title=&quot;对字符进行多种形式的编码解码&quot;&gt;对字符进行多种形式的编码解码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://raw.githubusercontent.com/willfarrell/alfred-workflows/master/IPAddress.alfredworkflow&quot; title=&quot;ip信息查询&quot;&gt;ip信息查询&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ngreenstein/alfred-process-killer&quot; title=&quot;强制结束进程&quot;&gt;强制结束进程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dash API&lt;/code&gt; 查询集成，由 &lt;code&gt;Dash&lt;/code&gt; 软件提供。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其实插件功能就类似于 &lt;code&gt;Mac&lt;/code&gt; 的 &lt;code&gt;automator&lt;/code&gt;，不过它提供了更多更人性化的支持。关于自己写 &lt;code&gt;workflow&lt;/code&gt; 我还在研究，，给几篇文章大家参考一下。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/66514693&quot; title=&quot;Alfred 3 天气预报workflow&quot;&gt;Alfred 3 天气预报workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://allenwu.itscoder.com/how-to-write-a-workflow-for-mac&quot; title=&quot;如何去写一个第三方的workflow&quot;&gt;如何去写一个第三方的workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844903490406318093&quot; title=&quot;用NodeJS把玩一番workflow&quot;&gt;用NodeJS把玩一番workflow&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;把自己日常的工作流形成 &lt;code&gt;workflow&lt;/code&gt;，能够极大的提高我们在 &lt;code&gt;Mac&lt;/code&gt; 上的使用效率，这也是 &lt;code&gt;Alfred&lt;/code&gt; 最强大的功能，不过需要一点学习成本。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://louiszhai.github.io/2018/05/31/alfred/#alfred-workflow&quot; title=&quot;Alfred神器使用手册&quot;&gt;Alfred神器使用手册&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://1991421.cn/2019/04/06/b908e228/&quot; title=&quot;Alfred打磨之路&quot;&gt;Alfred打磨之路&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/alfred.CB5hM49E.jpg"/><enclosure url="/_astro/alfred.CB5hM49E.jpg"/></item><item><title>CSS标准</title><link>https://clloz.com/blog/css-specification-choose</link><guid isPermaLink="true">https://clloz.com/blog/css-specification-choose</guid><pubDate>Fri, 28 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CSS&lt;/code&gt; 的知识本身就是比较庞杂，难以成体系，而且标准化也不像 &lt;code&gt;ECMAScript&lt;/code&gt; 和 &lt;code&gt;HTML&lt;/code&gt; 有一份完整的标准。一般我们查询 &lt;code&gt;CSS&lt;/code&gt; 的内容都是到 &lt;code&gt;MDN&lt;/code&gt; 上进行查询，如果想要查询 &lt;code&gt;W3C&lt;/code&gt; 的标准，除了一份&lt;a href=&quot;https://www.w3.org/TR/CSS22/Overview.html#minitoc&quot; title=&quot;CSS2.2&quot;&gt;CSS2.2&lt;/a&gt;，其他的标准一大片，不知道看哪个才好。在&lt;code&gt;W3C&lt;/code&gt; 的 &lt;code&gt;ALL STANDARDS AND DRAFTS&lt;/code&gt; 中以 &lt;code&gt;CSS&lt;/code&gt; 为 &lt;code&gt;title&lt;/code&gt; 搜索一共有 &lt;code&gt;94&lt;/code&gt; 份文档。本文来总结一下如何查看自己需要的标准。&lt;/p&gt;
&lt;h2&gt;MDN&lt;/h2&gt;
&lt;p&gt;对于我们日常的查询来说，&lt;code&gt;MDN&lt;/code&gt; 是一个更合适的选择，如果遇到问题都去查标准效率太低了，&lt;code&gt;MDN&lt;/code&gt; 的内容足够满足我们的大部分需求了。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;MDN&lt;/code&gt; 我们除了使用使用关键词查询之外，在 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/CSS/Reference&quot; title=&quot;CSS参考&quot;&gt;CSS参考&lt;/a&gt;页面给出了按字母索引 的所有标准 &lt;code&gt;CSS&lt;/code&gt; 属性、伪类、伪元素、数据类型、以及 &lt;code&gt;@&lt;/code&gt; 规则。 还按类型排列的 &lt;code&gt;CSS&lt;/code&gt; 选择器 列表和 &lt;code&gt;CSS&lt;/code&gt; 关键概念 列表。还有一份简短的 &lt;code&gt;DOM-CSS / CSSOM&lt;/code&gt; 参考。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MDN&lt;/code&gt; 的使用大家都不陌生，这里再给大家推荐两个软件，一个是 &lt;code&gt;Dash for Mac&lt;/code&gt;，他提供了大多数语言和工具的 &lt;code&gt;API&lt;/code&gt;，前端的包括 &lt;code&gt;HTML&lt;/code&gt;，&lt;code&gt;CSS&lt;/code&gt;，&lt;code&gt;Javascript&lt;/code&gt;，&lt;code&gt;NodeJS&lt;/code&gt;，&lt;code&gt;React&lt;/code&gt;，&lt;code&gt;Vue&lt;/code&gt;，&lt;code&gt;Angular&lt;/code&gt; 等的 &lt;code&gt;API&lt;/code&gt;，并且也能和 &lt;code&gt;Alfred&lt;/code&gt; 进行整合，非常方便的 &lt;code&gt;API&lt;/code&gt; 查询工具。&lt;/p&gt;
&lt;p&gt;另一个是 &lt;code&gt;CodeRunner&lt;/code&gt;，他右侧的工具栏直接可以进行 &lt;code&gt;Google&lt;/code&gt;，&lt;code&gt;StackOverflow&lt;/code&gt; 以及 &lt;code&gt;MDN&lt;/code&gt; 的检索，非常方便。如果你是写一些小的 &lt;code&gt;demo&lt;/code&gt; 非常建议使用这个软件。&lt;/p&gt;
&lt;h2&gt;标准&lt;/h2&gt;
&lt;p&gt;下面就是稍微有些杂乱的标准，如果我们想要系统地了解规范的细节，那么只能去看标准了。不过那么多的标准我们应该看哪些。这份&lt;a href=&quot;https://drafts.csswg.org/&quot; title=&quot;CSS Working Group Editor Drafts&quot;&gt;CSS Working Group Editor Drafts&lt;/a&gt;可以作为参考，它给出了各个 &lt;code&gt;CSS&lt;/code&gt; 细分区域有几份标准，哪一份是 &lt;code&gt;Current Work&lt;/code&gt; 的。&lt;/p&gt;
&lt;p&gt;在这些标准中最重要的就是 &lt;a href=&quot;https://www.w3.org/TR/CSS22/Overview.html#minitoc&quot; title=&quot;CSS2.2&quot;&gt;CSS2.2&lt;/a&gt;，它是所有 &lt;code&gt;CSS&lt;/code&gt; 的基础，其他的标准都要在学习了 &lt;code&gt;CSS2.2&lt;/code&gt; 之后，其中最重要的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;5 Selectors 选择器&lt;/li&gt;
&lt;li&gt;6 Assigning property values, Cascading, and Inheritance 属性值赋值，层叠和继承&lt;/li&gt;
&lt;li&gt;8 Box model 盒模型&lt;/li&gt;
&lt;li&gt;9 Visual formatting model 视觉格式化模型&lt;/li&gt;
&lt;li&gt;10 Visual formatting model details 视觉格式化模型细节&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CSS2.2&lt;/code&gt;之后的茫茫多的标准就是按具体内容细分成了独立的标准，比较重要的有如下这些：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Selectors Level 4&lt;/li&gt;
&lt;li&gt;CSS Box Model Module Level 3&lt;/li&gt;
&lt;li&gt;CSS Cascading and Inheritance Level 3&lt;/li&gt;
&lt;li&gt;CSS Values and Units Module Level 3&lt;/li&gt;
&lt;li&gt;CSS Pseudo-Elements Module Level 4&lt;/li&gt;
&lt;li&gt;CSS Animations Level 1&lt;/li&gt;
&lt;li&gt;CSS Transforms Module Level 1&lt;/li&gt;
&lt;li&gt;Media Queries Level 4&lt;/li&gt;
&lt;li&gt;CSS Flexible Box Layout Module Level 1&lt;/li&gt;
&lt;li&gt;CSS Regions Module Level 1&lt;/li&gt;
&lt;li&gt;CSS Multi-column Layout Module&lt;/li&gt;
&lt;li&gt;CSS Inline Layout Module Level 3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我还是建议在有确实需要的时候再阅读标准，因为标准的阅读需要花费大量的时间，并且有些非常新的标准不一定就会一直持续下去，把最重要的 &lt;code&gt;CSS2.2&lt;/code&gt; 掌握，其他的略读一下即可。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;关于 &lt;code&gt;CSS2.2&lt;/code&gt; 之后的标准还可以参考 &lt;code&gt;MDN&lt;/code&gt; 的&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Archive/CSS3&quot; title=&quot;CSS3&quot;&gt;CSS3&lt;/a&gt;。&lt;code&gt;CSS3&lt;/code&gt; 是层叠样式表（&lt;code&gt;Cascading Style Sheets&lt;/code&gt;）语言的最新版本，旨在扩展 &lt;code&gt;CSS2.1&lt;/code&gt;。它带来了许多期待已久的新特性， 例如圆角、阴影、&lt;code&gt;gradients&lt;/code&gt;(渐变) 、&lt;code&gt;transitions&lt;/code&gt;(过渡) 与 &lt;code&gt;animations&lt;/code&gt;(动画) 。以及新的布局方式，如 &lt;code&gt;multi-columns&lt;/code&gt; 、 &lt;code&gt;flexible box&lt;/code&gt; 与 &lt;code&gt;grid layouts&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CSS Level 2&lt;/code&gt; 经历了 &lt;code&gt;9&lt;/code&gt; 年的时间（从 2002 年 8 月到 2011 年 6 月）才达到 &lt;code&gt;Recommendation&lt;/code&gt;（推荐） 状态，主要原因是被一些次要特性拖了后腿。为了加快那些已经确认没有问题的特性的标准化速度，&lt;code&gt;W3C&lt;/code&gt; 的 &lt;code&gt;CSS Working Group&lt;/code&gt;(&lt;code&gt;CSS&lt;/code&gt; 工作组) 作出了一项被称为 &lt;code&gt;Beijing doctrine&lt;/code&gt; 的决定，将 &lt;code&gt;CSS&lt;/code&gt; 划分为许多小组件，称之为模块（也就是我们现在看到的非常多的不同标准）。这些模块彼此独立，按照各自的进度来进行标准化。其中一些已经是 &lt;code&gt;W3C Recommendation&lt;/code&gt; 状态，也有一些仍是 &lt;code&gt;early Working Drafts&lt;/code&gt;（早期工作草案）。当新的需求被肯定后， 新的模块也会同样地添加进来。&lt;/p&gt;
&lt;p&gt;从形式上来说，&lt;code&gt;CSS3&lt;/code&gt; 标准自身已经不存在了。每个模块都被独立的标准化，现在标准 &lt;code&gt;CSS&lt;/code&gt; 包括了修订后的 &lt;code&gt;CSS2.1&lt;/code&gt; 以及完整模块对它的扩充，模块的 &lt;code&gt;level&lt;/code&gt;（级别）数并不一致。可以在每个时间点上为 &lt;code&gt;CSS&lt;/code&gt; 标准定义一个 &lt;code&gt;snapshots&lt;/code&gt;（快照），列出 &lt;code&gt;CSS 2.1&lt;/code&gt; 和成熟的模块。&lt;code&gt;W3C&lt;/code&gt; 会定期的发布这些 &lt;code&gt;snapshots&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/41191048/answer/90058208&quot; title=&quot;有哪些CSS标准是前端工程师很有必要研读的？ - 貘吃馍香的回答 - 知乎 &quot;&gt;有哪些CSS标准是前端工程师很有必要研读的？ - 貘吃馍香的回答 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/41191048/answer/89996829&quot; title=&quot;有哪些CSS标准是前端工程师很有必要研读的？ - 贺师俊的回答 - 知乎&quot;&gt;有哪些CSS标准是前端工程师很有必要研读的？ - 贺师俊的回答 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>CSS 实现文字颜色渐变</title><link>https://clloz.com/blog/css-text-color-gradient</link><guid isPermaLink="true">https://clloz.com/blog/css-text-color-gradient</guid><pubDate>Tue, 25 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我经常将自己的一些突发奇想的设计到自己的博客上尝试。博客首页右侧的热门文章的小工具上的文本颜色一直没找到满意的，于是想试试渐变色的文本会不会有不错的效果。&lt;/p&gt;
&lt;h2&gt;文本渐变色实现&lt;/h2&gt;
&lt;h2&gt;background-clip&lt;/h2&gt;
&lt;p&gt;这种实现的主要思路就是对 &lt;code&gt;background&lt;/code&gt; 进行裁剪。&lt;code&gt;background-clip&lt;/code&gt; 提供了一个 &lt;code&gt;text&lt;/code&gt; 属性，可以将背景裁剪成文本的前景色（即只有文本覆盖的部分有背景色），然后我们再将文本颜色设置为 &lt;code&gt;transparent&lt;/code&gt; 即可以实现文本颜色的渐变。&lt;code&gt;background-clip&lt;/code&gt; 的 &lt;code&gt;text&lt;/code&gt; 属性目前仍然是实验功能，需要使用 &lt;code&gt;-webkit-background-clip&lt;/code&gt; 才能生效。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;text-color-gradient&amp;#x3C;/title&gt;
    &amp;#x3C;style&gt;
      .text-color-gradient {
        display: inline-block;
        font-size: 10em;
        font-weight: 700;
        background: linear-gradient(0.25turn, #c21500, #ffc500);
        background-clip: text;
        -webkit-background-clip: text;
        color: transparent;
      }
    &amp;#x3C;/style&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div style=&quot;text-align: center;&quot;&gt;
      &amp;#x3C;div class=&quot;text-color-gradient&quot;&gt;Clloz&amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;mask-image&lt;/h2&gt;
&lt;p&gt;第二种方法是使用 &lt;code&gt;mask-image&lt;/code&gt; 配合伪元素做一个渐变遮罩层。如果我们的渐变是 &lt;code&gt;color A&lt;/code&gt; 到 &lt;code&gt;color B&lt;/code&gt;，我们可以将文本颜色设为 &lt;code&gt;A&lt;/code&gt;，然后用伪元素（&lt;code&gt;content&lt;/code&gt; 与元素文本相同）配合 &lt;code&gt;mask-image&lt;/code&gt; 做一个渐变的遮罩层，渐变是从 &lt;code&gt;transparent&lt;/code&gt; 到 &lt;code&gt;color B&lt;/code&gt;，然后我们将伪元素覆盖到原来的文本上，就能得到想要的渐变。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;text-color-gradient&amp;#x3C;/title&gt;
    &amp;#x3C;style&gt;
      .text-color-gradient {
        display: inline-block;
        font-size: 10em;
        position: relative;
        color: #ffc500;
      }

      .text-color-gradient[data-text]::after {
        content: attr(data-text);
        color: #c21500;
        position: absolute;
        left: 0;
        z-index: 2;
        -webkit-mask-image: linear-gradient(0.25turn, #c21500, transparent);
      }
    &amp;#x3C;/style&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div style=&quot;text-align: center;&quot;&gt;
      &amp;#x3C;div class=&quot;text-color-gradient&quot; data-text=&quot;Clloz&quot;&gt;Clloz&amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关于渐变色&lt;/h2&gt;
&lt;p&gt;关于渐变色推荐一个网站：&lt;a href=&quot;https://uigradients.com/&quot; title=&quot;uigradients&quot;&gt;uigradients&lt;/a&gt;，该网站有很多不错的渐变色搭配。另外还有一个软件&lt;a href=&quot;https://www.macstories.net/reviews/aquarelo-a-beautifully-designed-mac-color-utility/&quot; title=&quot;aquarelo&quot;&gt;aquarelo&lt;/a&gt;，可以查看渐变色，也支持导出。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这就是我实现渐变的两种方法，总的来说还是第一种方法比较简单好用，并且可以设置两种以上颜色的渐变。&lt;code&gt;mask-image&lt;/code&gt; 的 &lt;code&gt;linear-gradient&lt;/code&gt; 取值似乎必须要一个 &lt;code&gt;transparent&lt;/code&gt; ，并且只能有两个颜色。综合而言还是使用第一个方式比较好。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhangxinxu.com/wordpress/2011/04/%E5%B0%8Ftipcss3%E4%B8%8B%E7%9A%84%E6%B8%90%E5%8F%98%E6%96%87%E5%AD%97%E6%95%88%E6%9E%9C%E5%AE%9E%E7%8E%B0/http://&quot; title=&quot;CSS3下的渐变文字效果实现&quot;&gt;CSS3下的渐变文字效果实现&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>HTML meta 标签</title><link>https://clloz.com/blog/html-meta-tag</link><guid isPermaLink="true">https://clloz.com/blog/html-meta-tag</guid><pubDate>Tue, 25 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文讲一讲 &lt;code&gt;HTML&lt;/code&gt; 的 &lt;code&gt;&amp;#x3C;meta&gt;&lt;/code&gt; 标签。&lt;/p&gt;
&lt;h2&gt;元数据&lt;/h2&gt;
&lt;p&gt;所谓元数据就是用来描述其他数据的数据 &lt;code&gt;data that provides information about other data&lt;/code&gt; 或者说 &lt;code&gt;data about data&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;meta&gt;&lt;/code&gt; 元素表示那些不能由其它HTML元相关元素 (&lt;code&gt;&amp;#x3C;base&gt;, &amp;#x3C;link&gt;, &amp;#x3C;script&gt;, &amp;#x3C;style&gt;, &amp;#x3C;title&gt;&lt;/code&gt;) 之一表示的任何元数据信息。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;meta&lt;/code&gt; 元素定义的元数据的类型包括以下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果设置了 &lt;code&gt;name&lt;/code&gt; 属性，&lt;code&gt;meta&lt;/code&gt; 元素提供的是文档级别（&lt;code&gt;document-level&lt;/code&gt;）的元数据，应用于整个页面。&lt;/li&gt;
&lt;li&gt;如果设置了 &lt;code&gt;http-equiv&lt;/code&gt; 属性，&lt;code&gt;meta&lt;/code&gt; 元素则是编译指令，提供的信息与类似命名的 &lt;code&gt;HTTP&lt;/code&gt; 头部相同。&lt;/li&gt;
&lt;li&gt;如果设置了 &lt;code&gt;charset&lt;/code&gt; 属性，&lt;code&gt;meta&lt;/code&gt; 元素是一个字符集声明，告诉文档使用哪种字符编码。&lt;/li&gt;
&lt;li&gt;如果设置了 &lt;code&gt;itemprop&lt;/code&gt; 属性，&lt;code&gt;meta&lt;/code&gt; 元素提供用户定义的元数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Referer 首部字段和 Referrer-policy 首部字段&lt;/h2&gt;
&lt;p&gt;关于 &lt;code&gt;HTTP&lt;/code&gt; 请求头中的 &lt;code&gt;Referer&lt;/code&gt; 字段，是可选的，客户端在发送请求的时候可以选择是否加上这个字段。这个字段的正确拼写是 &lt;code&gt;Referrer&lt;/code&gt;，也就是推荐人的意思，但是在写入标准的时候少了一个 &lt;code&gt;r&lt;/code&gt;，后来就将错就错沿用到现在。&lt;/p&gt;
&lt;p&gt;浏览器向服务器请求资源的时候，如果用户在地址栏输入网址，或者选中浏览器书签，就不发送 &lt;code&gt;Referer&lt;/code&gt; 字段。发送 &lt;code&gt;Referer&lt;/code&gt; 字段的情况主要有三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户点击网页上的链接。&lt;/li&gt;
&lt;li&gt;用户发送表单。&lt;/li&gt;
&lt;li&gt;网页加载静态资源，比如加载图片、脚本、样式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;浏览器都会将当前网址作为 &lt;code&gt;Referer&lt;/code&gt; 字段，放在 &lt;code&gt;HTTP&lt;/code&gt; 请求的头信息发送。浏览器的 &lt;code&gt;JavaScript&lt;/code&gt; 引擎提供 &lt;code&gt;document.referrer&lt;/code&gt; 属性，可以查看当前页面的引荐来源。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Referer&lt;/code&gt; 字段实际上告诉了服务器，用户在访问当前资源之前的位置。这往往可以用来用户跟踪。&lt;/p&gt;
&lt;p&gt;比如你在阿里云服务器的 &lt;code&gt;OSS&lt;/code&gt; 里面存着一些自己服务器上要用的图片之类的静态资源，你不希望别人随意访问这些资源（&lt;code&gt;cdn&lt;/code&gt; 流量要花你的钱），那么你就可以在后台设置允许访问的 &lt;code&gt;referrer&lt;/code&gt;。你甚至可以禁止空 &lt;code&gt;referrer&lt;/code&gt; 的访问，也就是直接在浏览器输入地址访问也不行。&lt;/p&gt;
&lt;p&gt;而且 &lt;code&gt;referrer&lt;/code&gt; 有可能暴露隐私，因此有些情况不能使用。比如功能 &lt;code&gt;URL&lt;/code&gt;，即有的 &lt;code&gt;URL&lt;/code&gt; 不要登录，可以访问，就能直接完成密码重置、邮件退订等功能。或者是内网 &lt;code&gt;URL&lt;/code&gt;，不希望外部用户知道内网有这样的地址。&lt;code&gt;Referer&lt;/code&gt; 字段很可能把这些 &lt;code&gt;URL&lt;/code&gt; 暴露出去。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;W3C&lt;/code&gt; 制定了一个&lt;a href=&quot;https://w3c.github.io/webappsec-referrer-policy/&quot; title=&quot;referrer-policy&quot;&gt;referrer-policy&lt;/a&gt;标准来控制文档的 &lt;code&gt;Referer&lt;/code&gt; 策略。&lt;code&gt;referrer-policy&lt;/code&gt; 一共有 &lt;code&gt;8&lt;/code&gt; 个值，和 &lt;code&gt;name&lt;/code&gt; 为 &lt;code&gt;referrer&lt;/code&gt; 的 &lt;code&gt;&amp;#x3C;meta&gt;&lt;/code&gt; 的 &lt;code&gt;content&lt;/code&gt; 取值是相同的（各个取值和含义参考下面的属性章节的 &lt;code&gt;name&lt;/code&gt; 部分）。改变浏览器默认 &lt;code&gt;referrer-policy&lt;/code&gt; 的方法大概有三种&lt;code&gt;referrer-policy&lt;/code&gt; 集成到 &lt;code&gt;HTML&lt;/code&gt; 的方法有三种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;用一个 &lt;code&gt;name&lt;/code&gt; 为 &lt;code&gt;referrer&lt;/code&gt; 的 &lt;code&gt;&amp;#x3C;meta&gt;&lt;/code&gt; 元素为整个文档设置 &lt;code&gt;referrer&lt;/code&gt; 策略。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;meta name=&quot;referrer&quot; content=&quot;origin&quot; /&gt;
```html
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用 &lt;code&gt;&amp;#x3C;a&gt;&lt;/code&gt;、&lt;code&gt;&amp;#x3C;area&gt;&lt;/code&gt;、&lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt;、&lt;code&gt;&amp;#x3C;iframe&gt;&lt;/code&gt;、&lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; 或者 &lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt; 元素上的 &lt;code&gt;referrerpolicy&lt;/code&gt; 属性为其设置独立的请求策略。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;a href=&quot;http://example.com&quot; referrerpolicy=&quot;origin&quot;&gt; ```html &amp;#x3C;/a&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;&amp;#x3C;a&gt;&lt;/code&gt;、&lt;code&gt;&amp;#x3C;area&gt;&lt;/code&gt; 或者 &lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt; 元素上将 &lt;code&gt;rel&lt;/code&gt; 属性设置为 &lt;code&gt;noreferrer&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;a href=&quot;http://example.com&quot; rel=&quot;noreferrer&quot;&gt;&amp;#x3C;/a&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;CSS&lt;/code&gt; 可以从样式表获取引用的资源，这些资源也可以遵从 &lt;code&gt;referrer&lt;/code&gt; 策略：外部 &lt;code&gt;CSS&lt;/code&gt; 样式表使用默认策略 (&lt;code&gt;no-referrer-when-downgrade&lt;/code&gt;)，除非 &lt;code&gt;CSS&lt;/code&gt; 样式表的响应消息通过 &lt;code&gt;Referrer-Policy&lt;/code&gt; 首部覆盖该策略。对于 &lt;code&gt;&amp;#x3C;style&gt;&lt;/code&gt; 元素或 &lt;code&gt;style&lt;/code&gt; 属性，则遵从文档的 &lt;code&gt;referrer&lt;/code&gt; 策略。&lt;/p&gt;
&lt;p&gt;| &lt;code&gt;Policy&lt;/code&gt;                          | &lt;code&gt;Document&lt;/code&gt;                      | &lt;code&gt;Navigation to&lt;/code&gt;                      | &lt;code&gt;Referrer&lt;/code&gt;                      |
| --------------------------------- | ------------------------------- | ------------------------------------ | ------------------------------- |
| &lt;code&gt;no-referrer&lt;/code&gt;                     | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;any domain or path&lt;/code&gt;                 | &lt;code&gt;no referrer&lt;/code&gt;                   |
| &lt;code&gt;no-referrer-when-downgrade&lt;/code&gt;      | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;https://example.com/otherpage.html&lt;/code&gt; | &lt;code&gt;https://example.com/page.html&lt;/code&gt; |
| &lt;code&gt;no-referrer-when-downgrade&lt;/code&gt;      | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;https://mozilla.org&lt;/code&gt;                | &lt;code&gt;https://example.com/page.html&lt;/code&gt; |
| &lt;code&gt;no-referrer-when-downgrade&lt;/code&gt;      | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;http://example.org&lt;/code&gt;                 | &lt;code&gt;no referrer&lt;/code&gt;                   |
| &lt;code&gt;origin&lt;/code&gt;                          | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;any domain or path&lt;/code&gt;                 | &lt;code&gt;https://example.com/&lt;/code&gt;          |
| &lt;code&gt;origin-when-cross-origin&lt;/code&gt;        | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;https://example.com/otherpage.html&lt;/code&gt; | &lt;code&gt;https://example.com/page.html&lt;/code&gt; |
| &lt;code&gt;origin-when-cross-origin&lt;/code&gt;        | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;https://mozilla.org&lt;/code&gt;                | &lt;code&gt;https://example.com/&lt;/code&gt;          |
| &lt;code&gt;origin-when-cross-origin&lt;/code&gt;        | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;http://example.com/page.html&lt;/code&gt;       | &lt;code&gt;https://example.com/&lt;/code&gt;          |
| &lt;code&gt;same-origin&lt;/code&gt;                     | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;https://example.com/otherpage.html&lt;/code&gt; | &lt;code&gt;https://example.com/page.html&lt;/code&gt; |
| &lt;code&gt;same-origin&lt;/code&gt;                     | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;https://mozilla.org&lt;/code&gt;                | &lt;code&gt;no referrer&lt;/code&gt;                   |
| &lt;code&gt;strict-origin&lt;/code&gt;                   | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;https://mozilla.org&lt;/code&gt;                | &lt;code&gt;https://example.com/&lt;/code&gt;          |
| &lt;code&gt;strict-origin&lt;/code&gt;                   | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;http://example.org&lt;/code&gt;                 | &lt;code&gt;no referrer&lt;/code&gt;                   |
| &lt;code&gt;strict-origin&lt;/code&gt;                   | &lt;code&gt;http://example.com/page.html&lt;/code&gt;  | &lt;code&gt;any domain or path&lt;/code&gt;                 | &lt;code&gt;http://example.com/&lt;/code&gt;           |
| &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;https://example.com/otherpage.html&lt;/code&gt; | &lt;code&gt;https://example.com/page.html&lt;/code&gt; |
| &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;https://mozilla.org&lt;/code&gt;                | &lt;code&gt;https://example.com/&lt;/code&gt;          |
| &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;http://example.org&lt;/code&gt;                 | &lt;code&gt;no referrer&lt;/code&gt;                   |
| &lt;code&gt;unsafe-url&lt;/code&gt;                      | &lt;code&gt;https://example.com/page.html&lt;/code&gt; | &lt;code&gt;any domain or path&lt;/code&gt;                 | &lt;code&gt;https://example.com/page.html&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;Referer&lt;/code&gt; 和 &lt;code&gt;Referrer-Policy&lt;/code&gt; 这里做一下总结。&lt;code&gt;Referer&lt;/code&gt; 就是我们在向服务器请求的时候，告诉服务器我们是来自哪里，服务器根据我们的这个位置决定是否要返回我们所请求的资源。而 &lt;code&gt;Referrer-Policy&lt;/code&gt; 则是一个策略，确定我们在当前文档中的发生的请求要如何发送 &lt;code&gt;Referer&lt;/code&gt; 首部字段。&lt;/p&gt;
&lt;h2&gt;属性&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;全局属性 &lt;code&gt;name&lt;/code&gt; 在 &lt;code&gt;&amp;#x3C;meta&gt;&lt;/code&gt; 元素中具有特殊的语义; 在同一个 &lt;code&gt;&amp;#x3C;meta&gt;&lt;/code&gt; 标签中，&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;http-equiv&lt;/code&gt; 或者 &lt;code&gt;charset&lt;/code&gt; 三者中任何一个属性存在时，&lt;code&gt;itemprop&lt;/code&gt; 属性不能被使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;charset&lt;/h2&gt;
&lt;p&gt;这个属性声明了文档的字符编码。如果使用了这个属性，其值必须是与 &lt;code&gt;ASCII&lt;/code&gt; 大小写无关（&lt;code&gt;ASCII case-insensitive&lt;/code&gt;）的 &lt;code&gt;utf-8&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;content&lt;/h2&gt;
&lt;p&gt;此属性包含 &lt;code&gt;http-equiv&lt;/code&gt; 或 &lt;code&gt;name&lt;/code&gt; 属性的值，具体取决于所使用的值。&lt;/p&gt;
&lt;h2&gt;http-equiv&lt;/h2&gt;
&lt;p&gt;此属性定义了一个编译指示指令。这个属性叫做 &lt;code&gt;http-equiv(alent)&lt;/code&gt;是因为所有允许的值都是特定 &lt;code&gt;HTTP&lt;/code&gt; 头部的名称，如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;content-security-policy&lt;/code&gt;：允许页面作者定义当前页的内容策略。内容策略主要指定允许的服务器源和脚本端点，这有助于防止跨站点脚本攻击。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content-type&lt;/code&gt;：如果使用这个属性，其值必须是 &lt;code&gt;text/html; charset=utf-8&lt;/code&gt;。该属性只能用于 &lt;code&gt;MIME type&lt;/code&gt; 为 &lt;code&gt;text/html&lt;/code&gt; 的文档，不能用于 &lt;code&gt;MIME&lt;/code&gt; 类型为 &lt;code&gt;XML&lt;/code&gt; 的文档。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;default-style&lt;/code&gt;：设置默认CSS样式表组的名称。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x-ua-compatible&lt;/code&gt;：如果使用了该属性，那么 &lt;code&gt;content&lt;/code&gt; 属性必须包含 &lt;code&gt;IE=edge&lt;/code&gt;，用户代理必须忽略此编译指示。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refresh&lt;/code&gt;：这个属性指定:
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;content&lt;/code&gt; 只包含一个正整数,则是重新载入页面的时间间隔(秒);&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;content&lt;/code&gt; 包含一个正整数并且跟着一个字符串 &lt;code&gt;;url=&lt;/code&gt; 和一个合法的 &lt;code&gt;URL&lt;/code&gt;，则是重定向到指定链接的时间间隔(秒)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;name&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;name&lt;/code&gt; 和 &lt;code&gt;content&lt;/code&gt; 属性可以一起使用，以&lt;code&gt;key - value&lt;/code&gt; 对的方式给文档提供元数据，其中 &lt;code&gt;name&lt;/code&gt; 作为元数据的名称，&lt;code&gt;content&lt;/code&gt; 作为元数据的值。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTML&lt;/code&gt; 标准中定义的 &lt;code&gt;name&lt;/code&gt; 取值如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;author&lt;/code&gt;：文档作者。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;：一个简短精确的页面内容概要，&lt;code&gt;firefox&lt;/code&gt; 和 &lt;code&gt;Opera&lt;/code&gt; 用这个元数据作为书签页面的默认描述。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;generator&lt;/code&gt;：生成页面的软件的标识符。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;keyword&lt;/code&gt;：用逗号分隔开的页面相关的关键词。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;referrer&lt;/code&gt;：控制文档发送的 &lt;code&gt;HTTP request&lt;/code&gt; 的请求首部字段 &lt;code&gt;Referer&lt;/code&gt; 的值。首部字段 &lt;code&gt;Referer&lt;/code&gt; 会告知服务器请求的原始资源的 &lt;code&gt;URI&lt;/code&gt;，通俗点说就是当前请求页面的来源页面的地址，即表示当前页面是通过此来源页面里的链接进入的。&lt;code&gt;Referer&lt;/code&gt; 的正确的拼写应该是 &lt;code&gt;Referrer 推荐人&lt;/code&gt;，但不知为何，大家一 直沿用这个错误的拼写。&lt;code&gt;referrer&lt;/code&gt; 对应的 &lt;code&gt;content&lt;/code&gt; 取值有如下这些（即&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referrer-Policy&quot; title=&quot;referrer-policy&quot;&gt;referrer-policy&lt;/a&gt;）：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;no-referrer&lt;/code&gt;：整个 &lt;code&gt;Referer&lt;/code&gt; 首部会被移除。访问来源信息不随着请求一起发送。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;origin&lt;/code&gt;：&lt;code&gt;Referer&lt;/code&gt; 字段一律只发送源信息（协议+域名+端口），不管是否跨域。&lt;code&gt;https://example.com/page.html&lt;/code&gt; 会将 &lt;code&gt;https://example.com/&lt;/code&gt; 作为引用地址。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no-referrer-when-downgrade&lt;/code&gt;(默认值): 如果从 &lt;code&gt;HTTPS&lt;/code&gt; 网址链接到 &lt;code&gt;HTTP&lt;/code&gt; 网址，不发送 &lt;code&gt;Referer&lt;/code&gt; 字段，其他情况发送（包括 &lt;code&gt;HTTP&lt;/code&gt; 网址链接到 &lt;code&gt;HTTP&lt;/code&gt; 网址）。这是浏览器的默认行为。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;origin-when-cross-origin&lt;/code&gt;：对于同源的请求，会发送完整的 &lt;code&gt;URL&lt;/code&gt; 作为引用地址，但是对于非同源请求仅发送文件的源。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;same-origin&lt;/code&gt;：对于同源（协议+域名+端口 都相同）的请求会发送引用地址，但是对于非同源请求则不发送引用地址信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;strict-origin&lt;/code&gt;：在同等安全级别的情况下，发送文件的源作为引用地址(&lt;code&gt;HTTPS-&gt;HTTPS&lt;/code&gt;)，但是在降级的情况下不会发送 (&lt;code&gt;HTTPS-&gt;HTTP&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt;：对于同源的请求，会发送完整的 &lt;code&gt;URL&lt;/code&gt; 作为引用地址；在同等安全级别的情况下，发送文件的源作为引用地址(&lt;code&gt;HTTPS-&gt;HTTPS&lt;/code&gt;)；在降级的情况下不发送此首部 (&lt;code&gt;HTTPS-&gt;HTTP&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unsafe-url&lt;/code&gt;：无论是同源请求还是非同源请求（包含源信息、路径和查询字符串，不包含锚点、用户名和密码），都发送完整的 URL（移除参数信息之后）作为引用地址。这项设置会将受 &lt;code&gt;TLS&lt;/code&gt; 安全协议保护的资源的源和路径信息泄露给非安全的源服务器。进行此项设置的时候要慎重考虑。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 实例 --&gt;
&amp;#x3C;meta name=&quot;keywords&quot; content=&quot;HTML, CSS, XML, XHTML, JavaScript&quot; /&gt;
&amp;#x3C;meta name=&quot;description&quot; content=&quot;Free Web tutorials on HTML and CSS&quot; /&gt;
&amp;#x3C;meta name=&quot;author&quot; content=&quot;Hege Refsnes&quot; /&gt;
&amp;#x3C;meta http-equiv=&quot;refresh&quot; content=&quot;30&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;非标准的 &lt;code&gt;name&lt;/code&gt; 只要记住一个 &lt;code&gt;viewport&lt;/code&gt; 即可，该 &lt;code&gt;name&lt;/code&gt; 提示移动设备如何初始化视口大小，只对移动设备有效。该 &lt;code&gt;name&lt;/code&gt; 对应多个 &lt;code&gt;content&lt;/code&gt;，并且 &lt;code&gt;content&lt;/code&gt; 还有取值，可以在一个 &lt;code&gt;meta&lt;/code&gt; 标签中同时设置多个 &lt;code&gt;content&lt;/code&gt;，用逗号隔开。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;meta name=&quot;viewport&quot;&gt;&lt;/code&gt; 的 &lt;code&gt;content&lt;/code&gt; 取值：&lt;/p&gt;
&lt;p&gt;| &lt;code&gt;Value&lt;/code&gt;         | &lt;code&gt;Description &amp;#x26; Subvalue&lt;/code&gt;                                                                                                                                                                                                                                                                                                               |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| &lt;code&gt;width&lt;/code&gt;         | 以 &lt;code&gt;pixels&lt;/code&gt;（像素）为单位， 定义 &lt;code&gt;viewport&lt;/code&gt;（视口）的宽度。。值为一个正整数，或字符串 &lt;code&gt;width-device&lt;/code&gt;                                                                                                                                                                                                                                   |
| &lt;code&gt;initial-scale&lt;/code&gt; | 设置页面的初始缩放值（&lt;code&gt;device-width&lt;/code&gt; 和 &lt;code&gt;viewport size&lt;/code&gt; 的比例）。值为一个正数，取值范围为 &lt;code&gt;0.0 ~ 10.0&lt;/code&gt;                                                                                                                                                                                                                                |
| &lt;code&gt;minimum-scale&lt;/code&gt; | 定义最小缩放值，值为一个正数，取值范围为 &lt;code&gt;0.0 ~ 10.0&lt;/code&gt;。取值必须小于等于 &lt;code&gt;maximum-scale&lt;/code&gt;，不然会导致不确定的行为发生。浏览器设定可以忽略这条规则，&lt;code&gt;iOS10+&lt;/code&gt; 默认忽略这条规则                                                                                                                                                             |
| &lt;code&gt;maximum-scale&lt;/code&gt; | 定义最大缩放值，值为一个正数，取值范围为 &lt;code&gt;0.0 ~ 10.0&lt;/code&gt;。取值必须大于等于 &lt;code&gt;minimum-scale&lt;/code&gt;，不然会导致不确定的行为发生。浏览器设定可以忽略这条规则，&lt;code&gt;iOS10+&lt;/code&gt; 默认忽略这条规则                                                                                                                                                             |
| &lt;code&gt;height&lt;/code&gt;        | 以 &lt;code&gt;pixels&lt;/code&gt;（像素）为单位， 定义 &lt;code&gt;viewport&lt;/code&gt;（视口）的高度，取值为一个正整数或者字符串 &lt;code&gt;device-height&lt;/code&gt;。很多浏览器不支持，很少使用                                                                                                                                                                                                      |
| &lt;code&gt;user-scalable&lt;/code&gt; | 是否允许用户进行缩放，值为 &lt;code&gt;no&lt;/code&gt; 或 &lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt; 代表不允许，&lt;code&gt;yes&lt;/code&gt; 代表允许，默认值为 &lt;code&gt;yes&lt;/code&gt;，浏览器设定可以忽略这条规则，&lt;code&gt;iOS10+&lt;/code&gt; 默认忽略这条规则                                                                                                                                                                                       |
| &lt;code&gt;viewport-fit&lt;/code&gt;  | 取值为 &lt;code&gt;auto&lt;/code&gt;、&lt;code&gt;contain&lt;/code&gt; 或 &lt;code&gt;cover&lt;/code&gt;。取值为 &lt;code&gt;auto&lt;/code&gt; 不会影响 &lt;code&gt;layout viewport&lt;/code&gt;，整个页面都会显示。&lt;code&gt;contain&lt;/code&gt; 值表示 &lt;code&gt;viewport&lt;/code&gt; 已缩放以适合显示在显示屏上的最大矩形。 &lt;code&gt;cover&lt;/code&gt; 表示视口已缩放至填充设备显示。强烈建议用 &lt;code&gt;CSS&lt;/code&gt; 的&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/CSS/env&quot; title=&quot;env()&quot;&gt;env()&lt;/a&gt;来确保重要内容没有被截断在屏幕之外。 |&lt;/p&gt;
&lt;p&gt;我们经常使用的 &lt;code&gt;viewport&lt;/code&gt; 取值为 &lt;code&gt;&amp;#x3C;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0&quot;&gt;。&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;viewport&lt;/code&gt; 的更多相关内容，请参考这篇文章&lt;a href=&quot;https://www.cnblogs.com/2050/p/3877280.html#!comments&quot; title=&quot;移动前端开发之viewport的深入理解&quot;&gt;移动前端开发之viewport的深入理解&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referrer-Policy&quot; title=&quot;Referrer-Policy - MDN&quot;&gt;Referrer-Policy - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2019/06/http-referer.html&quot; title=&quot;HTTP Referer 教程 - 阮一峰&quot;&gt;HTTP Referer 教程 - 阮一峰&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/meta&quot;&gt;meta标签 - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/html-logo.jHluxOew.png"/><enclosure url="/_astro/html-logo.jHluxOew.png"/></item><item><title>HTML标签语义化</title><link>https://clloz.com/blog/html-tag-semantic</link><guid isPermaLink="true">https://clloz.com/blog/html-tag-semantic</guid><pubDate>Tue, 25 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;HTML5&lt;/code&gt; 中推出了很多语义化的标签，所谓语义化就是标签能够对内容有所表达，比如 &lt;code&gt;p&lt;/code&gt; 标签就是段落 &lt;code&gt;paragraph&lt;/code&gt; 的意思。其实如果只是实现页面的效果，只使用 &lt;code&gt;div&lt;/code&gt; 和 &lt;code&gt;span&lt;/code&gt; 就可以做到。但是语义化的好处就是对开发者比较友好，文档的结构清晰，各部分功能一目了然，便于开发和维护。同时语义化的文档也能够对 &lt;code&gt;SEO&lt;/code&gt; 起到更好的效果。本文来介绍一下 &lt;code&gt;HTML5&lt;/code&gt; 中的标签。&lt;/p&gt;
&lt;h2&gt;HTML5 标签&lt;/h2&gt;
&lt;p&gt;本文列出了所有标准化的 &lt;code&gt;HTML5&lt;/code&gt; 元素，使用起始标签描述，按照功能分组。新网站应当只使用这里列出的元素。&lt;/p&gt;
&lt;p&gt;符号 这个元素在 HTML5 中加入 代表该元素是在 &lt;code&gt;HTML5&lt;/code&gt; 中新增的。另外注意，这里列出的其他元素可能在 &lt;code&gt;HTML5&lt;/code&gt; 标准中得到了扩充或经过修改。&lt;/p&gt;
&lt;h2&gt;根元素&lt;/h2&gt;
&lt;p&gt;| Element  | Description                                                           |
| -------- | --------------------------------------------------------------------- |
| &lt;code&gt;&amp;#x3C;html&gt;&lt;/code&gt; | 代表 &lt;code&gt;HTML&lt;/code&gt; 或 &lt;code&gt;XHTML&lt;/code&gt; 文档的根。其他所有元素必须是这个元素的子节点。 |&lt;/p&gt;
&lt;h2&gt;文档元数据&lt;/h2&gt;
&lt;p&gt;| Element   | Description                                                                                  |
| --------- | -------------------------------------------------------------------------------------------- |
| &lt;code&gt;&amp;#x3C;head&gt;&lt;/code&gt;  | 代表关于文档元数据的一个集合，包括脚本或样式表的链接或内容。                                 |
| &lt;code&gt;&amp;#x3C;title&gt;&lt;/code&gt; | 定义文档的标题，将显示在浏览器的标题栏或标签页上。该元素只能包含文本，包含的标签不会被解释。 |
| &lt;code&gt;&amp;#x3C;base&gt;&lt;/code&gt;  | 定义页面上相对 &lt;code&gt;URL&lt;/code&gt; 的基准 &lt;code&gt;URL&lt;/code&gt;。                                                          |
| &lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt;  | 用于链接外部资源到该文档。                                                                   |
| &lt;code&gt;&amp;#x3C;meta&gt;&lt;/code&gt;  | 定义其他 &lt;code&gt;HTML&lt;/code&gt; 元素无法描述的元数据。                                                       |
| &lt;code&gt;&amp;#x3C;style&gt;&lt;/code&gt; | 用于内联 &lt;code&gt;CSS&lt;/code&gt;。                                                                             |&lt;/p&gt;
&lt;h2&gt;脚本&lt;/h2&gt;
&lt;p&gt;| Element      | Description                                                             |
| ------------ | ----------------------------------------------------------------------- |
| &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt;   | 定义一个内联脚本或链接到外部脚本。脚本语言是 &lt;code&gt;JavaScript&lt;/code&gt;。             |
| &lt;code&gt;&amp;#x3C;noscript&gt;&lt;/code&gt; | 定义当浏览器不支持脚本时显示的替代文字。                                |
| &lt;code&gt;&amp;#x3C;template&gt;&lt;/code&gt; | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，通过 &lt;code&gt;JavaScript&lt;/code&gt; 在运行时实例化内容的容器。 |&lt;/p&gt;
&lt;h2&gt;章节&lt;/h2&gt;
&lt;p&gt;| Element                         | Description                                                                                           |
| ------------------------------- | ----------------------------------------------------------------------------------------------------- |
| &lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt;                        | 代表 &lt;code&gt;HTML&lt;/code&gt; 文档的内容。在文档中只能有一个 &lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt; 元素。                                            |
| &lt;code&gt;&amp;#x3C;section&gt;&lt;/code&gt;                     | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，定义文档中的一个章节。                                                     |
| &lt;code&gt;&amp;#x3C;nav&gt;&lt;/code&gt;                         | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，定义只包含导航链接的章节。                                                 |
| &lt;code&gt;&amp;#x3C;article&gt;&lt;/code&gt;                     | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，定义可以独立于内容其余部分的完整独立内容块。                               |
| &lt;code&gt;&amp;#x3C;aside&gt;&lt;/code&gt;                       | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，定义和页面内容关联度较低的内容——如果被删除，剩下的内容仍然很合理。         |
| &lt;code&gt;&amp;#x3C;h1&gt;,&amp;#x3C;h2&gt;,&amp;#x3C;h3&gt;,&amp;#x3C;h4&gt;,&amp;#x3C;h5&gt;,&amp;#x3C;h6&gt;&lt;/code&gt; | 标题元素实现了六层文档标题，&lt;code&gt;&amp;#x3C;h1&gt;&lt;/code&gt; 是最大的标题，&lt;code&gt;&amp;#x3C;h6&gt;&lt;/code&gt; 是最小的标题。标题元素简要地描述章节的主题。  |
| &lt;code&gt;&amp;#x3C;header&gt;&lt;/code&gt;                      | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，定义页面或章节的头部。它经常包含 logo、页面标题和导航性的目录。            |
| &lt;code&gt;&amp;#x3C;footer&gt;&lt;/code&gt;                      | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，定义页面或章节的尾部。它经常包含版权信息、法律信息链接和反馈建议用的地址。 |
| &lt;code&gt;&amp;#x3C;address&gt;&lt;/code&gt;                     | 定义包含联系信息的一个章节。                                                                          |
| &lt;code&gt;&amp;#x3C;main&gt;&lt;/code&gt;                        | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，定义文档中主要或重要的内容。                                               |&lt;/p&gt;
&lt;h2&gt;组织内容&lt;/h2&gt;
&lt;p&gt;| Element        | Description                                           |
| -------------- | ----------------------------------------------------- |
| &lt;code&gt;&amp;#x3C;p&gt;&lt;/code&gt;          | 定义一个段落。                                        |
| &lt;code&gt;&amp;#x3C;hr&gt;&lt;/code&gt;         | 代表章节、文章或其他长内容中段落之间的分隔符。        |
| &lt;code&gt;&amp;#x3C;pre&gt;&lt;/code&gt;        | 代表其内容已经预先排版过，格式应当保留 。             |
| &lt;code&gt;&amp;#x3C;blockquote&gt;&lt;/code&gt; | 代表引用自其他来源的内容。                            |
| &lt;code&gt;&amp;#x3C;ol&gt;&lt;/code&gt;         | 定义一个有序列表。                                    |
| &lt;code&gt;&amp;#x3C;ul&gt;&lt;/code&gt;         | 定义一个无序列表。                                    |
| &lt;code&gt;&amp;#x3C;li&gt;&lt;/code&gt;         | 定义列表中的一个列表项。                              |
| &lt;code&gt;&amp;#x3C;dl&gt;&lt;/code&gt;         | 定义一个定义列表（一系列术语和其定义）。              |
| &lt;code&gt;&amp;#x3C;dt&gt;&lt;/code&gt;         | 代表一个由下一个 &lt;code&gt;&amp;#x3C;dd&gt;&lt;/code&gt; 定义的术语。                  |
| &lt;code&gt;&amp;#x3C;dd&gt;&lt;/code&gt;         | 代表出现在它之前术语的定义。                          |
| &lt;code&gt;&amp;#x3C;figure&gt;&lt;/code&gt;     | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表一个和文档有关的图例。 |
| &lt;code&gt;&amp;#x3C;figcaption&gt;&lt;/code&gt; | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表一个图例的说明。       |
| &lt;code&gt;&amp;#x3C;div&gt;&lt;/code&gt;        | 代表一个通用的容器，没有特殊含义。                    |&lt;/p&gt;
&lt;h2&gt;文字形式&lt;/h2&gt;
&lt;p&gt;| Element       | Description                                                                                                                                  |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| &lt;code&gt;&amp;#x3C;a&gt;&lt;/code&gt;         | 代表一个链接到其他资源的超链接 。                                                                                                            |
| &lt;code&gt;&amp;#x3C;em&gt;&lt;/code&gt;        | 代表强调 文字。                                                                                                                              |
| &lt;code&gt;&amp;#x3C;strong&gt;&lt;/code&gt;    | 代表特别重要 文字。                                                                                                                          |
| &lt;code&gt;&amp;#x3C;small&gt;&lt;/code&gt;     | 代表注释 ，如免责声明、版权声明等，对理解文档不重要。                                                                                        |
| &lt;code&gt;&amp;#x3C;s&gt;&lt;/code&gt;         | 代表不准确或不相关 的内容。                                                                                                                  |
| &lt;code&gt;&amp;#x3C;cite&gt;&lt;/code&gt;      | 代表作品标题 。                                                                                                                              |
| &lt;code&gt;&amp;#x3C;q&gt;&lt;/code&gt;         | 代表内联的引用 。                                                                                                                            |
| &lt;code&gt;&amp;#x3C;dfn&gt;&lt;/code&gt;       | 代表一个术语包含在其最近祖先内容中的定义 。                                                                                                  |
| &lt;code&gt;&amp;#x3C;abbr&gt;&lt;/code&gt;      | 代表省略 或缩写 ，其完整内容在 title 属性中。                                                                                                |
| &lt;code&gt;&amp;#x3C;data&gt;&lt;/code&gt;      | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，关联一个内容的机器可读的等价形式 （该元素只在 &lt;code&gt;WHATWG&lt;/code&gt; 版本的 &lt;code&gt;HTML&lt;/code&gt; 标准中，不在 &lt;code&gt;W3C&lt;/code&gt; 版本的 &lt;code&gt;HTML5&lt;/code&gt; 标准中）。 |
| &lt;code&gt;&amp;#x3C;time&gt;&lt;/code&gt;      | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表日期 和时间 值；机器可读的等价形式通过 &lt;code&gt;datetime&lt;/code&gt; 属性指定。                                                  |
| &lt;code&gt;&amp;#x3C;code&gt;&lt;/code&gt;      | 代表计算机代码 。                                                                                                                            |
| &lt;code&gt;&amp;#x3C;var&gt;&lt;/code&gt;       | 代表代码中的变量 。                                                                                                                          |
| &lt;code&gt;&amp;#x3C;samp&gt;&lt;/code&gt;      | 代表程序或电脑的输出 。                                                                                                                      |
| &lt;code&gt;&amp;#x3C;kbd&gt;&lt;/code&gt;       | 代表用户输入 ，一般从键盘输出，但也可以代表其他输入，如语音输入。                                                                            |
| &lt;code&gt;&amp;#x3C;sub&gt;,&amp;#x3C;sup&gt;&lt;/code&gt; | 分别代表下标 和上标 。                                                                                                                       |
| &lt;code&gt;&amp;#x3C;i&gt;&lt;/code&gt;         | 代表一段不同性质 的文字，如技术术语、外文短语等。                                                                                            |
| &lt;code&gt;&amp;#x3C;b&gt;&lt;/code&gt;         | 代表一段需要被关注 的文字。                                                                                                                  |
| &lt;code&gt;&amp;#x3C;u&gt;&lt;/code&gt;         | 代表一段需要下划线呈现的文本注释，如标记出拼写错误的文字等。                                                                                 |
| &lt;code&gt;&amp;#x3C;mark&gt;&lt;/code&gt;      | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表一段需要被高亮的引用 文字。                                                                                   |
| &lt;code&gt;&amp;#x3C;ruby&gt;&lt;/code&gt;      | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表被ruby 注释 标记的文本，如中文汉字和它的拼音。                                                                |
| &lt;code&gt;&amp;#x3C;rt&gt;&lt;/code&gt;        | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表 &lt;code&gt;ruby&lt;/code&gt; 注释 ，如中文拼音。                                                                                   |
| &lt;code&gt;&amp;#x3C;rp&gt;&lt;/code&gt;        | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表 &lt;code&gt;ruby&lt;/code&gt; 注释两边的额外插入文本 ，用于在不支持 &lt;code&gt;ruby&lt;/code&gt; 注释显示的浏览器中提供友好的注释显示。                   |
| &lt;code&gt;&amp;#x3C;bdi&gt;&lt;/code&gt;       | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表需要脱离父元素文本方向的一段文本。它允许嵌入一段不同或未知文本方向格式的文本。                                |
| &lt;code&gt;&amp;#x3C;bdo&gt;&lt;/code&gt;       | 指定子元素的文本方向 ，显式地覆盖默认的文本方向。                                                                                            |
| &lt;code&gt;&amp;#x3C;span&gt;&lt;/code&gt;      | 代表一段没有特殊含义的文本，当其他语义元素都不适合文本时候可以使用该元素。                                                                   |
| &lt;code&gt;&amp;#x3C;br&gt;&lt;/code&gt;        | 代表换行 。                                                                                                                                  |
| &lt;code&gt;&amp;#x3C;wbr&gt;&lt;/code&gt;       | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表建议换行 (&lt;code&gt;Word Break Opportunity&lt;/code&gt;) ，当文本太长需要换行时将会在此处添加换行符。                              |&lt;/p&gt;
&lt;h2&gt;编辑&lt;/h2&gt;
&lt;p&gt;| Element | Description             |
| ------- | ----------------------- |
| &lt;code&gt;&amp;#x3C;ins&gt;&lt;/code&gt; | 定义增加 到文档的内容。 |
| &lt;code&gt;&amp;#x3C;del&gt;&lt;/code&gt; | 定义从文档移除 的内容。 |&lt;/p&gt;
&lt;h2&gt;嵌入内容&lt;/h2&gt;
&lt;p&gt;| Element    | Description                                                                                      |
| ---------- | ------------------------------------------------------------------------------------------------ |
| &lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt;    | 代表一张图片 。                                                                                  |
| &lt;code&gt;&amp;#x3C;iframe&gt;&lt;/code&gt; | 代表一个内联的框架 。                                                                            |
| &lt;code&gt;&amp;#x3C;embed&gt;&lt;/code&gt;  | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表一个嵌入 的外部资源，如应用程序或交互内容。&lt;strong&gt;不推荐使用&lt;/strong&gt;         |
| &lt;code&gt;&amp;#x3C;object&gt;&lt;/code&gt; | 代表一个外部资源 ，如图片、HTML 子文档、插件等。                                                 |
| &lt;code&gt;&amp;#x3C;param&gt;&lt;/code&gt;  | 代表 &lt;code&gt;&amp;#x3C;object&gt;&lt;/code&gt; 元素所指定的插件的参数 。                                                        |
| &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt;  | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入 ，表一段视频及其视频文件和字幕，并提供了播放视频的用户界面。           |
| &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt;  | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表一段声音 ，或音频流 。                                            |
| &lt;code&gt;&amp;#x3C;source&gt;&lt;/code&gt; | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，为 &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 或 &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 这类媒体元素指定媒体源 。                   |
| &lt;code&gt;&amp;#x3C;track&gt;&lt;/code&gt;  | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，为 &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 或 &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 这类媒体元素指定文本轨道（字幕） 。         |
| &lt;code&gt;&amp;#x3C;canvas&gt;&lt;/code&gt; | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表位图区域 ，可以通过脚本在它上面实时呈现图形，如图表、游戏绘图等。 |
| &lt;code&gt;&amp;#x3C;map&gt;&lt;/code&gt;    | 与 &lt;code&gt;&amp;#x3C;area&gt;&lt;/code&gt; 元素共同定义图像映射 区域。                                                          |
| &lt;code&gt;&amp;#x3C;area&gt;&lt;/code&gt;   | 与 &lt;code&gt;&amp;#x3C;map&gt;&lt;/code&gt; 元素共同定义图像映射 区域。                                                           |
| &lt;code&gt;&amp;#x3C;svg&gt;&lt;/code&gt;    | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，定义一个嵌入式矢量图 。                                               |
| &lt;code&gt;&amp;#x3C;math&gt;&lt;/code&gt;   | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，定义一段数学公式 。&lt;strong&gt;大多数浏览器暂不支持&lt;/strong&gt;                           |&lt;/p&gt;
&lt;h2&gt;表格&lt;/h2&gt;
&lt;p&gt;| Element      | Description                           |
| ------------ | ------------------------------------- | --- | --- |
| &lt;code&gt;&amp;#x3C;table&gt;&lt;/code&gt;    | 定义多维数据 。                       |
| &lt;code&gt;&amp;#x3C;caption&gt;&lt;/code&gt;  | 代表表格的标题 。                     |
| &lt;code&gt;&amp;#x3C;colgroup&gt;&lt;/code&gt; | 代表表格中一组单列或多列 。           |
| &lt;code&gt;&amp;#x3C;col&gt;&lt;/code&gt;      | 代表表格中的列 。                     |
| &lt;code&gt;&amp;#x3C;tbody&gt;&lt;/code&gt;    | 代表表格中一块具体数据 （表格主体）。 |
| &lt;code&gt;&amp;#x3C;thead&gt;&lt;/code&gt;    | 代表表格中一块列标签 （表头）。       |
| &lt;code&gt;&amp;#x3C;tfoot&gt;&lt;/code&gt;    | 代表表格中一块列摘要 （表尾）。       |
| &lt;code&gt;&amp;#x3C;tr&gt;&lt;/code&gt;       | 代表表格中的行 。                     |
| &lt;code&gt;&amp;#x3C;td&gt;&lt;/code&gt;       | 代表表格中的单元格                    |     | 。  |
| &lt;code&gt;&amp;#x3C;th&gt;&lt;/code&gt;       | 代表表格中的头部单元格 。             |&lt;/p&gt;
&lt;h2&gt;表单&lt;/h2&gt;
&lt;p&gt;| Element      | Description                                                             |
| ------------ | ----------------------------------------------------------------------- |
| &lt;code&gt;&amp;#x3C;form&gt;&lt;/code&gt;     | 代表一个表单 ，由控件组成。                                             |
| &lt;code&gt;&amp;#x3C;fieldset&gt;&lt;/code&gt; | 代表控件组 。                                                           |
| &lt;code&gt;&amp;#x3C;legend&gt;&lt;/code&gt;   | 代表 &lt;code&gt;&amp;#x3C;fieldset&gt;&lt;/code&gt; 控件组的标题 。                                       |
| &lt;code&gt;&amp;#x3C;label&gt;&lt;/code&gt;    | 代表表单控件的标题 。                                                   |
| &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt;    | 代表允许用户编辑数据的数据区 （文本框、单选框、复选框等）。             |
| &lt;code&gt;&amp;#x3C;button&gt;&lt;/code&gt;   | 代表按钮 。                                                             |
| &lt;code&gt;&amp;#x3C;select&gt;&lt;/code&gt;   | 代表下拉框 。                                                           |
| &lt;code&gt;&amp;#x3C;datalist&gt;&lt;/code&gt; | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表提供给其他控件的一组预定义选项 。        |
| &lt;code&gt;&amp;#x3C;optgroup&gt;&lt;/code&gt; | 代表一个选项分组 。                                                     |
| &lt;code&gt;&amp;#x3C;option&gt;&lt;/code&gt;   | 代表一个 &lt;code&gt;&amp;#x3C;select&gt;&lt;/code&gt; 元素或 &lt;code&gt;&amp;#x3C;datalist&gt;&lt;/code&gt; 元素中的一个选项                |
| &lt;code&gt;&amp;#x3C;textarea&gt;&lt;/code&gt; | 代表多行文本框 。                                                       |
| &lt;code&gt;&amp;#x3C;keygen&gt;&lt;/code&gt;   | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表一个密钥对生成器控件。&lt;strong&gt;已在标准中废弃&lt;/strong&gt; |
| &lt;code&gt;&amp;#x3C;output&gt;&lt;/code&gt;   | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表计算值 。                                |
| &lt;code&gt;&amp;#x3C;progress&gt;&lt;/code&gt; | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表进度条 。                                |
| &lt;code&gt;&amp;#x3C;meter&gt;&lt;/code&gt;    | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表滑动条 。                                |&lt;/p&gt;
&lt;h2&gt;交互元素&lt;/h2&gt;
&lt;p&gt;| Element      | Description                                                                    |
| ------------ | ------------------------------------------------------------------------------ |
| &lt;code&gt;&amp;#x3C;details&gt;&lt;/code&gt;  | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表一个用户可以(点击)获取额外信息或控件的小部件 。 |
| &lt;code&gt;&amp;#x3C;summary&gt;&lt;/code&gt;  | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表 &lt;code&gt;&amp;#x3C;details&gt;&lt;/code&gt; 元素的综述 或标题 。               |
| &lt;code&gt;&amp;#x3C;menuitem&gt;&lt;/code&gt; | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表一个用户可以点击的菜单项。&lt;strong&gt;已在标准中废弃&lt;/strong&gt;    |
| &lt;code&gt;&amp;#x3C;menu&gt;&lt;/code&gt;     | 这个元素在 &lt;code&gt;HTML5&lt;/code&gt; 中加入，代表菜单。                                          |&lt;/p&gt;
&lt;h2&gt;新标签的应用&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;HTML5&lt;/code&gt; 中提供的新标签中有些特别的可以拿出来特别说一下。&lt;/p&gt;
&lt;h2&gt;templete 内容模板元素&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTML&lt;/code&gt; 内容模板（&lt;code&gt;&amp;#x3C;template&gt;&lt;/code&gt;）元素是一种用于保存客户端内容机制，该内容在加载页面时不会呈现，但随后可以(原文为 &lt;code&gt;may be&lt;/code&gt;)在运行时使用 &lt;code&gt;JavaScript&lt;/code&gt; 实例化。将模板视为一个可存储在文档中以便后续使用的内容片段。虽然解析器在加载页面时确实会处理 &lt;code&gt;&amp;#x3C;template&gt;&lt;/code&gt; 元素的内容，但这样做只是为了确保这些内容有效；但元素内容不会被渲染。&lt;/p&gt;
&lt;h2&gt;figure 和 figurecaption&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTML &amp;#x3C;figure&gt;&lt;/code&gt; 元素代表一段独立的内容, 经常与说明（&lt;code&gt;caption&lt;/code&gt;） &lt;code&gt;&amp;#x3C;figcaption&gt;&lt;/code&gt; 配合使用, 并且作为一个独立的引用单元。当它属于主内容流（&lt;code&gt;main flow&lt;/code&gt;）时，它的位置独立于主体。这个标签经常是在主文中引用的图片，插图，表格，代码段等等，当这部分转移到附录中或者其他页面时不会影响到主体。&lt;/p&gt;
&lt;h2&gt;mark&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;mark&gt;&lt;/code&gt; 元素用来显示与用户当前活动相关的一部分文档内容。例如，它可能被用于显示匹配搜索结果中的单词。&lt;/p&gt;
&lt;h2&gt;ruby&gt;、rt 和 rp&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;ruby&gt;&lt;/code&gt; 元素被用来展示东亚文字注音或字符注释。 &lt;code&gt;&amp;#x3C;rt&gt;&lt;/code&gt; 代表 &lt;code&gt;ruby&lt;/code&gt; 注释 ，如中文拼音，日语罗马音等。 &lt;code&gt;&amp;#x3C;rp&gt;&lt;/code&gt; 元素用于不支持 &lt;code&gt;&amp;#x3C;ruby&gt;&lt;/code&gt; 元素的情况。 &lt;code&gt;&amp;#x3C;rp&gt;&lt;/code&gt; 的内容提供了应该展示的东西，通常是圆括号，以便表示 &lt;code&gt;ruby&lt;/code&gt; 注解的存在。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ruby&lt;/code&gt; 这个单词来源于日语 &lt;code&gt;ルビ&lt;/code&gt;，表示注音假名小铅字，即振り仮名。&lt;code&gt;ruby&lt;/code&gt; 语言的名字也来源于此。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;bdi 和 bdo&lt;/h2&gt;
&lt;p&gt;这两个元素在&lt;a href=&quot;https://www.clloz.com&quot; title=&quot;HTML全局属性dir&quot;&gt;HTML全局属性dir&lt;/a&gt;中进行详细介绍。&lt;/p&gt;
&lt;h2&gt;wbr&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;wbr&gt;&lt;/code&gt; 是指 &lt;code&gt;Word Break Opportunity&lt;/code&gt;，让浏览器在需要换行时从我们设定的位置进行换行。&lt;code&gt;&amp;#x3C;wbr&gt;&lt;/code&gt; 仅仅表示一个零宽的位置，不会对文本产生额外的影响。我们可以在我们期待浏览器换行的位置插入 &lt;code&gt;&amp;#x3C;wbr&gt;&lt;/code&gt; 标签，比如 &lt;code&gt;url&lt;/code&gt; 的换行推荐在各个标点之前。比如如下的代码，当我们不断减小视口或元素的宽度，浏览器会从我们设置 &lt;code&gt;&amp;#x3C;wbr&gt;&lt;/code&gt; 的位置进行换行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;p&gt;
  http://this&amp;#x3C;wbr /&gt;.is&amp;#x3C;wbr /&gt;.a&amp;#x3C;wbr /&gt;.really&amp;#x3C;wbr /&gt;.long&amp;#x3C;wbr /&gt;.example&amp;#x3C;wbr /&gt;.com/With&amp;#x3C;wbr /&gt;/deeper&amp;#x3C;wbr /&gt;/level&amp;#x3C;wbr /&gt;/pages&amp;#x3C;wbr /&gt;/deeper&amp;#x3C;wbr /&gt;/level&amp;#x3C;wbr /&gt;/pages&amp;#x3C;wbr /&gt;/deeper&amp;#x3C;wbr /&gt;/level&amp;#x3C;wbr /&gt;/pages&amp;#x3C;wbr /&gt;/deeper&amp;#x3C;wbr /&gt;/level&amp;#x3C;wbr /&gt;/pages&amp;#x3C;wbr /&gt;/deeper&amp;#x3C;wbr /&gt;/level&amp;#x3C;wbr /&gt;/pages
&amp;#x3C;/p&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;embed&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;embed&gt;&lt;/code&gt; 元素将外部内容嵌入文档中的指定位置。此内容由外部应用程序或其他交互式内容源（如浏览器插件）提供。&lt;/p&gt;
&lt;p&gt;大多数现代浏览器已经弃用并取消了对浏览器插件的支持，所以如果您希望您的网站可以在普通用户的浏览器上运行，那么依靠 &lt;code&gt;&amp;#x3C;embed&gt;&lt;/code&gt; 通常是不明智的。可以使用 &lt;code&gt;&amp;#x3C;img&gt;、&amp;#x3C;iframe&gt;、&amp;#x3C;video&gt;、&amp;#x3C;audio&gt;&lt;/code&gt; 等标签代替。&lt;/p&gt;
&lt;h2&gt;picture、video、audio、track 和 source&lt;/h2&gt;
&lt;p&gt;这四个标签都是用来在文档中嵌入媒体文件的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;picture&gt;&lt;/code&gt; 元素通过包含零或多个 &lt;code&gt;&amp;#x3C;source&gt;&lt;/code&gt; 元素和一个 &lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt; 元素来为不同的显示/设备场景提供图像版本。浏览器会选择最匹配的子 &lt;code&gt;&amp;#x3C;source&gt;&lt;/code&gt; 元素，如果没有匹配的，就选择 &lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt; 元素的 &lt;code&gt;src&lt;/code&gt; 属性中的 &lt;code&gt;URL&lt;/code&gt;。然后，所选图像呈现在 &lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt; 元素占据的空间中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 元素 用于在 &lt;code&gt;HTML&lt;/code&gt; 或者 &lt;code&gt;XHTML&lt;/code&gt; 文档中嵌入媒体播放器，用于支持文档内的视频播放。这些视频资源可以使用 &lt;code&gt;src&lt;/code&gt; 属性或者 &lt;code&gt;&amp;#x3C;source&gt;&lt;/code&gt; 元素来进行描述：浏览器将会选择最合适的一个来使用。也可以将 &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 标签用于音频内容，但是 &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 元素可能在用户体验上更合适。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt;元素用于在文档中嵌入音频内容。 &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 元素可以包含一个或多个音频资源， 这些音频资源可以使用 &lt;code&gt;src&lt;/code&gt; 属性或者 &lt;code&gt;&amp;#x3C;source&gt;&lt;/code&gt; 元素来进行描述：浏览器将会选择最合适的一个来使用。也可以使用 &lt;code&gt;MediaStream&lt;/code&gt; 将这个元素用于流式媒体。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;track&gt;&lt;/code&gt; 元素被当作媒体元素 &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 的子元素来使用。它允许指定时序文本字幕（或者基于时间的数据），例如自动处理字幕。字幕格式有 &lt;code&gt;WebVTT&lt;/code&gt; 格式（&lt;code&gt;.vtt&lt;/code&gt; 格式文件）— &lt;code&gt;Web&lt;/code&gt; 视频文本字幕格式，以及指时序文本标记语言（&lt;code&gt;TTML&lt;/code&gt;）格式。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;source&gt;&lt;/code&gt; 元素为 &lt;code&gt;&amp;#x3C;picture&gt;&lt;/code&gt;, &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 或者 &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 元素指定多个媒体资源。这是一个空元素。它通常用于以不同浏览器支持的多种格式提供相同的媒体内容。要决定加载哪个 &lt;code&gt;URL&lt;/code&gt;，&lt;code&gt;user agent&lt;/code&gt; 检查每个 &lt;code&gt;&amp;#x3C;source&gt;&lt;/code&gt; 的 &lt;code&gt;srcset&lt;/code&gt;、&lt;code&gt;media&lt;/code&gt; 和 &lt;code&gt;type&lt;/code&gt; 属性，来选择最匹配页面当前布局、显示设备特征等的兼容资源。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;picture&gt;
  &amp;#x3C;source media=&quot;(min-width: 650px)&quot; srcset=&quot;demo1.jpg&quot; /&gt;
  &amp;#x3C;source media=&quot;(min-width: 465px)&quot; srcset=&quot;demo2.jpg&quot; /&gt;
  &amp;#x3C;img src=&quot;img_girl.jpg&quot; /&gt;
&amp;#x3C;/picture&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 都有很多属性和用法，本文不作详细介绍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;canvas&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;canvas&gt;&lt;/code&gt;元素可被用来通过 &lt;code&gt;JavaScript&lt;/code&gt;（&lt;code&gt;Canvas API&lt;/code&gt; 或 &lt;code&gt;WebGL API&lt;/code&gt;）绘制图形及图形动画。&lt;code&gt;&amp;#x3C;canvas&gt;&lt;/code&gt; 的内容也是非常丰富的，不在本文做详细介绍。&lt;/p&gt;
&lt;h2&gt;svg&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SVG&lt;/code&gt; 是一种基于 &lt;code&gt;XML&lt;/code&gt; 语法的图像格式，全称是可缩放矢量图（&lt;code&gt;Scalable Vector Graphics&lt;/code&gt;）。其他图像格式都是基于像素处理的，&lt;code&gt;SVG&lt;/code&gt; 则是属于对图像的形状描述，所以它本质上是文本文件，体积较小，且不管放大多少倍都不会失真。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;svg&lt;/code&gt; 不是根元素，&lt;code&gt;svg&lt;/code&gt; 元素可以用于在当前文档（比如说，一个 &lt;code&gt;HTML&lt;/code&gt; 文档）内嵌套一个独立的 &lt;code&gt;svg&lt;/code&gt; 片段 。 这个独立片段拥有独立的视口和坐标系统。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;svg&lt;/code&gt; 的内容可以参考阮一峰老师的文章&lt;a href=&quot;https://www.ruanyifeng.com/blog/2018/08/svg.html&quot; title=&quot;SVG图像入门教程&quot;&gt;SVG图像入门教程&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;math&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mathematical Markup Language (MathML)&lt;/code&gt; 是一个用于描述数学公式、符号的一种 &lt;code&gt;XML&lt;/code&gt; 标记语言。&lt;code&gt;MathML&lt;/code&gt; 的顶级元素是 &lt;code&gt;&amp;#x3C;math&gt;&lt;/code&gt;。所有有效的 &lt;code&gt;MathML&lt;/code&gt; 实例必须被包括在 &lt;code&gt;&amp;#x3C;math&gt;&lt;/code&gt; 标记中。另外不可以在一个 &lt;code&gt;&amp;#x3C;math&gt;&lt;/code&gt; 元素中嵌套第二个 &lt;code&gt;&amp;#x3C;math&gt;&lt;/code&gt; 元素，但是 &lt;code&gt;&amp;#x3C;math&gt;&lt;/code&gt; 元素中可以有任意多的子元素 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MathML&lt;/code&gt; 的内容可以参考&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/MathML&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt;，目前大多数浏览器都不支持 &lt;code&gt;&amp;#x3C;math&gt;&lt;/code&gt;，如果想使用数学公式有几个方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;LaTex&lt;/code&gt; 编译数学公式，默认输出 &lt;code&gt;PDF&lt;/code&gt;，可以转为 &lt;code&gt;png&lt;/code&gt; 再到页面使用。&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&quot;https://github.com/mathjax/MathJax&quot; title=&quot;MathJax&quot;&gt;MathJax&lt;/a&gt; 渲染。&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&quot;https://github.com/KaTeX/KaTeX&quot; title=&quot;KaTeX&quot;&gt;KaTeX&lt;/a&gt; 渲染。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;datalist&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;datalist&gt;&lt;/code&gt; 元素包含了一组 &lt;code&gt;&amp;#x3C;option&gt;&lt;/code&gt; 元素，这些元素表示其它表单控件可选值。&lt;/p&gt;
&lt;h2&gt;output&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;output&gt;&lt;/code&gt; 标签表示计算或用户操作的结果。可以用标签的 &lt;code&gt;form&lt;/code&gt; 属性指定关联的 &lt;code&gt;form&lt;/code&gt; 的 &lt;code&gt;id&lt;/code&gt;，从而达到在文档任何位置使用 &lt;code&gt;&amp;#x3C;output&gt;&lt;/code&gt; 的目的。&lt;/p&gt;
&lt;h2&gt;progress&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;progress&gt;&lt;/code&gt; 元素用来显示一项任务的完成进度.虽然规范中没有规定该元素具体如何显示,浏览器开发商可以自己决定,但通常情况下,该元素都显示为一个进度条形式。&lt;/p&gt;
&lt;h2&gt;meter&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;meter&gt;&lt;/code&gt;元素用来显示已知范围的标量值或者分数值。&lt;/p&gt;
&lt;h2&gt;details&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;details&gt;&lt;/code&gt; 元素可创建一个挂件，仅在被切换成展开状态时，它才会显示内含的信息。&lt;code&gt;&amp;#x3C;summary&gt;&lt;/code&gt; 元素可为该部件提供概要或者标签。&lt;/p&gt;
&lt;h2&gt;已废弃的标签&lt;/h2&gt;
&lt;p&gt;以下标签已经从 &lt;code&gt;HTML5&lt;/code&gt; 中移除，应停止使用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;acronym&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;applet&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;basefont&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;big&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;center&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;dir&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;font&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;frame&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;frameset&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;noframes&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;strike&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;tt&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;语义化标签实例&lt;/h2&gt;
&lt;p&gt;上面我们已经介绍了所有的 &lt;code&gt;HTML5&lt;/code&gt; 标签，&lt;code&gt;HTML5&lt;/code&gt; 提供了非常丰富的语义化标签。我们如何用这些语义化的标签来重构我们的文档解构呢？我们就以 &lt;code&gt;MDN&lt;/code&gt; 的&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/Guide/HTML/HTML5/HTML5_element_list&quot; title=&quot;HTML5 标签列表 - MDN&quot;&gt;HTML5 标签列表&lt;/a&gt;页面为例子来看看它是怎么实现的。&lt;code&gt;MDN&lt;/code&gt; 的页面基本都是用语义化的标签编写 &lt;code&gt;HTML&lt;/code&gt; 文档的。&lt;/p&gt;
&lt;p&gt;首先我们按功能将页面分为几个主要结构，所有内容都是这些主要结构的子结构。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;body&gt;
  &amp;#x3C;div&gt;
    &amp;#x3C;header&gt;页面头部，通常包含 logo、页面标题和导航性的目录。&amp;#x3C;/header&gt;
    &amp;#x3C;main&gt;主内容：整个页面核心内容&amp;#x3C;/main&gt;
    &amp;#x3C;section&gt;一个章节：显示一些与页面主要内容无关的部分&amp;#x3C;/section&gt;
    &amp;#x3C;footer&gt;页面尾部，通常包含版权信息、法律信息链接和反馈建议用的地址。&amp;#x3C;/footer&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/body&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们就已经确定了页面的结构，当我们要写入一个内容的时候也知道应该往哪个部分填充。&lt;/p&gt;
&lt;h2&gt;header&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;header&lt;/code&gt; 部分的内容有页面的标题，搜索框，用户头像和一个导航。&lt;code&gt;MDN&lt;/code&gt; 采用的是 &lt;code&gt;grid&lt;/code&gt; 网格结构，导航用的是 &lt;code&gt;nav&lt;/code&gt; 标签，其他都是用 &lt;code&gt;div&lt;/code&gt; 实现。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;header&gt;
  页面头部，通常包含 logo、页面标题和导航性的目录。
  &amp;#x3C;div&gt;MDN标题&amp;#x3C;/div&gt;
  &amp;#x3C;nav&gt;
    导航
    &amp;#x3C;ul&gt;
      列表
      &amp;#x3C;li&gt;&amp;#x3C;/li&gt;
      &amp;#x3C;li&gt;&amp;#x3C;/li&gt;
      &amp;#x3C;li&gt;&amp;#x3C;/li&gt;
    &amp;#x3C;/ul&gt;
  &amp;#x3C;/nav&gt;
  &amp;#x3C;div&gt;搜索框&amp;#x3C;/div&gt;
  &amp;#x3C;div&gt;用户头像&amp;#x3C;/div&gt;
&amp;#x3C;/header&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;main&lt;/h2&gt;
&lt;p&gt;核心内容部分是用户浏览页面最关注的部分，我们来 &lt;code&gt;MDN&lt;/code&gt; 查阅资料主要就是看这部分内容，甚至可以说只看这部分内容。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;main&gt;
  主内容：整个页面核心内容
  &amp;#x3C;header&gt;主内容头部：包括标题导航等内容&amp;#x3C;/header&gt;
  &amp;#x3C;div&gt;
    &amp;#x3C;aside&gt;
      &amp;#x3C;section&gt;
        侧边导航栏
        &amp;#x3C;header&gt;侧边导航栏标题&amp;#x3C;/header&gt;
        &amp;#x3C;ul&gt;
          列表展示当前页面的导航目录，点击跳转到对应章节，此处也可以用一个nav嵌套
        &amp;#x3C;/ul&gt;
      &amp;#x3C;/section&gt;
    &amp;#x3C;/aside&gt;
    &amp;#x3C;div class=&quot;content&quot;&gt;
      核心内容
      &amp;#x3C;article&gt;content&amp;#x3C;/article&gt;
      &amp;#x3C;div&gt;
        一个独立于content的section：metadata 修改时间
        &amp;#x3C;section&gt;&amp;#x3C;/section&gt;
      &amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/main&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;section&lt;/h2&gt;
&lt;p&gt;一个用来填写邮箱，接收 &lt;code&gt;MDN&lt;/code&gt; 邮件的章节，内部为一个 &lt;code&gt;form&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;footer&lt;/h2&gt;
&lt;p&gt;最后是页面的尾部，包含了 &lt;code&gt;MDN&lt;/code&gt; 网站的一些重要链接导航和版权信息、法律信息链接等。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/Guide/HTML/HTML5/HTML5_element_list&quot; title=&quot;HTML5 标签列表 - MDN&quot;&gt;HTML5 标签列表 - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/html-logo.jHluxOew.png"/><enclosure url="/_astro/html-logo.jHluxOew.png"/></item><item><title>HTTP MIME 类型</title><link>https://clloz.com/blog/http-mime-type</link><guid isPermaLink="true">https://clloz.com/blog/http-mime-type</guid><pubDate>Tue, 25 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们经常听到 &lt;code&gt;MIME type&lt;/code&gt;，但是可能并不了解它是什么。我们如今的 &lt;code&gt;web&lt;/code&gt; 内容非常丰富，有各种媒体资源在 &lt;code&gt;web&lt;/code&gt; 上传播共享。那么浏览器如何分辨资源的类型而进行处理呢？就是通过 &lt;code&gt;HTTP&lt;/code&gt; 响应报文实体首部中的 &lt;code&gt;Content-Type&lt;/code&gt; 字段中的 &lt;code&gt;MIME type&lt;/code&gt; 来确定的，比如常见的的 &lt;code&gt;Content-Type: text/html&lt;/code&gt;，也就是描述报文实体主体内容的一些标准化名称。本文就来介绍以下 &lt;code&gt;MIME type&lt;/code&gt; 相关的内容。&lt;/p&gt;
&lt;h2&gt;MIME type 简介&lt;/h2&gt;
&lt;p&gt;因特网上有数千种不同的数据类型，&lt;code&gt;HTTP&lt;/code&gt; 仔细地给每种要通过 &lt;code&gt;Web&lt;/code&gt; 传输的对象都打上了名为 &lt;code&gt;MIME&lt;/code&gt; 类型(&lt;code&gt;MIME type&lt;/code&gt;)的数据格式标签。最初设计 &lt;code&gt;MIME&lt;/code&gt;(&lt;code&gt;Multipurpose Internet Mail Extension&lt;/code&gt;，多用途因特网邮件扩展)是为了解决在不同的电子邮件系统之间搬移报文时存在的问题。&lt;code&gt;MIME&lt;/code&gt; 在电子邮件系统中工作得非常好，因此 &lt;code&gt;HTTP&lt;/code&gt; 也采纳了它，用它来描述并标记多媒体内容。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Web&lt;/code&gt; 服务器会为所有 &lt;code&gt;HTTP&lt;/code&gt; 对象数据附加一个 &lt;code&gt;MIME&lt;/code&gt; 类型(见下图，&lt;code&gt;Content-Type&lt;/code&gt; 字段在 &lt;code&gt;HTTP&lt;/code&gt; 报文的实体首部中)。当 &lt;code&gt;Web&lt;/code&gt; 浏览器从服务器中取回一个对象时，会去查看相关的 &lt;code&gt;MIME&lt;/code&gt; 类型，看看它是否知道应该如何处理这个对象。大多数浏览器都可以处理数百种常见的对象类型:显示图片文件、解析并格式化 &lt;code&gt;HTML&lt;/code&gt; 文件、通过计算机声卡播放音频文件，或者运行外部插件软件来处理特殊格式的数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/mime1.CKuRNoy3_Z2scuy5.webp&quot; alt=&quot;mime1&quot; title=&quot;mime1&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MIME&lt;/code&gt; 类型是一种文本标记，表示一种主要的对象类型和一个特定的子类型，中间由一条斜杠来分隔。常见的 MIME 类型有数百个，实验性或用途有限的 MIME 类型则更多。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HTML&lt;/code&gt; 格式的文本文档由 &lt;code&gt;text/html&lt;/code&gt; 类型来标记。&lt;/li&gt;
&lt;li&gt;普通的 &lt;code&gt;ASCII&lt;/code&gt; 文本文档由 &lt;code&gt;text/plain&lt;/code&gt; 类型来标记。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JPEG&lt;/code&gt; 格式的图片为 &lt;code&gt;image/jpeg&lt;/code&gt; 类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GIF&lt;/code&gt; 格式的图片为 &lt;code&gt;image/gif&lt;/code&gt; 类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Apple&lt;/code&gt; 的 &lt;code&gt;QuickTime&lt;/code&gt; 电影为 &lt;code&gt;video/quicktime&lt;/code&gt; 类型。&lt;/li&gt;
&lt;li&gt;微软的 &lt;code&gt;PowerPoint&lt;/code&gt; 演示文件为 &lt;code&gt;application/vnd.ms-powerpoint&lt;/code&gt; 类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;web 服务器如何确定 MIME 类型&lt;/h2&gt;
&lt;p&gt;当浏览器想服务器请求某个资源的时候，服务器要确定相应主体的 &lt;code&gt;MIME&lt;/code&gt; 类型，并在响应报文首部的实体首部中。有很多配置服务器的方法可以将 &lt;code&gt;MIME&lt;/code&gt; 类型与资源关联起来。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MIME&lt;/code&gt; 类型(&lt;code&gt;mime.types&lt;/code&gt;) &lt;code&gt;Web&lt;/code&gt; 服务器可以用文件的扩展名来说明 &lt;code&gt;MIME&lt;/code&gt; 类型。&lt;code&gt;Web&lt;/code&gt; 服务器会为每个资源 扫描一个包含了所有扩展名的 &lt;code&gt;MIME&lt;/code&gt; 类型的文件，以确定其 &lt;code&gt;MIME&lt;/code&gt; 类型。这种基于扩展名的类型相关是最常见的，见下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/mime2.C0eO7YIa_XL7Np.webp&quot; alt=&quot;mime2&quot; title=&quot;mime2&quot;&gt;&lt;/p&gt;
&lt;p&gt;魔法分类(&lt;code&gt;Magic typing&lt;/code&gt;) &lt;code&gt;Apache Web&lt;/code&gt; 服务器可以扫描每个资源的内容，并将其与一个已知模式表(被称为魔法文件)进行匹配，以决定每个文件的 &lt;code&gt;MIME&lt;/code&gt; 类型。这样做可能比较慢， 但很方便，尤其是文件没有标准扩展名的时候。&lt;/p&gt;
&lt;p&gt;显式分类(&lt;code&gt;Explicit typing&lt;/code&gt;) 可以对 &lt;code&gt;Web&lt;/code&gt; 服务器进行配置，使其不考虑文件的扩展名或内容，强制特定文件 或目录内容拥有某个 &lt;code&gt;MIME&lt;/code&gt; 类型。&lt;/p&gt;
&lt;p&gt;类型协商 有些 &lt;code&gt;Web&lt;/code&gt; 服务器经过配置，可以以多种文档格式来存储资源。在这种情况下， 可以配置 &lt;code&gt;Web&lt;/code&gt; 服务器，使其可以通过与用户的协商来决定使用哪种格式(及相 关的 &lt;code&gt;MIME&lt;/code&gt; 类型)“最好”。还可以通过配置 &lt;code&gt;Web&lt;/code&gt; 服务器，将特定的文件与 &lt;code&gt;MIME&lt;/code&gt; 类型相关联。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;Apache Web&lt;/code&gt; 服务器 &lt;code&gt;httpd&lt;/code&gt; 的配置文件 &lt;code&gt;/etc/httpd/conf/httpd.conf&lt;/code&gt; 中就有两个配置是跟 &lt;code&gt;MIME type&lt;/code&gt; 相关的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://httpd.apache.org/docs/2.2/mod/mod_mime.html#addtype&quot; title=&quot;AddType&quot;&gt;AddType&lt;/a&gt;：用于返回 &lt;code&gt;HTTP&lt;/code&gt; 响应给浏览器，将给定的文件扩展名映射到指定的内容类型（设置 &lt;code&gt;Content-Type&lt;/code&gt;）。&lt;code&gt;AddType image/gif .gif&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://httpd.apache.org/docs/2.2/mod/mod_mime.html#addhandler&quot; title=&quot;AddHandler&quot;&gt;AddHandler&lt;/a&gt;: 用于处理接收到的浏览器请求，将文件扩展名映射到指定的处理程序（用指定的程序处理某种类型的文件）。&lt;code&gt;AddHandler cgi-script .cgi&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;浏览器通常使用 &lt;code&gt;MIME&lt;/code&gt; 类型（而不是文件扩展名）来确定如何处理 &lt;code&gt;URL&lt;/code&gt;，因此 &lt;code&gt;Web&lt;/code&gt; 服务器在响应头中添加正确的 &lt;code&gt;MIME&lt;/code&gt; 类型非常重要。如果配置不正确，浏览器可能会曲解文件内容，网站将无法正常工作，并且下载的文件也会被错误处理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;MIME type 语法&lt;/h2&gt;
&lt;p&gt;MIME 主要由下列 5 份文档定义。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RFC 2045&lt;/code&gt;，&lt;code&gt;MIME: Format of Internet Message Bodies&lt;/code&gt; (&lt;code&gt;MIME&lt;/code&gt;: 因特网报文主体的格式)：描述了 &lt;code&gt;MIME&lt;/code&gt; 报文结构的概况，并介绍了 &lt;code&gt;HTTP&lt;/code&gt; 借用的 &lt;code&gt;Content-Type&lt;/code&gt; 首部。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RFC 2046&lt;/code&gt;，&lt;code&gt;MIME: Media Types&lt;/code&gt; (&lt;code&gt;MIME&lt;/code&gt;:媒体类型)：介绍了 &lt;code&gt;MIME&lt;/code&gt; 类型及其结构。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RFC 2047&lt;/code&gt;，&lt;code&gt;MIME: Message Header Extensions for Non-ASCII Text&lt;/code&gt; (&lt;code&gt;MIME&lt;/code&gt;: 非 &lt;code&gt;ASCII&lt;/code&gt; 文本的报文首部扩展)：定义了一些在首部包含非 &lt;code&gt;ASCII&lt;/code&gt; 字符的方式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RFC 2048&lt;/code&gt;，&lt;code&gt;MIME: Registration Procedures&lt;/code&gt; (&lt;code&gt;MIME&lt;/code&gt;:注册过程)：定义了如何向因特网号码分配机构(&lt;code&gt;Internet Assigned Numbers Authority&lt;/code&gt;，&lt;code&gt;IA- NA&lt;/code&gt;)注册 &lt;code&gt;MIME&lt;/code&gt; 值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RFC 2049&lt;/code&gt;，&lt;code&gt;MIME: Conformance Criteria and Examples&lt;/code&gt;(&lt;code&gt;MIME&lt;/code&gt;:一致性标准及实例)：详细介绍了一致性规则，并提供了一些实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;IANA&lt;/code&gt; 是 &lt;code&gt;MIME&lt;/code&gt; 媒体类型的官方注册机构，并维护了 &lt;a href=&quot;https://www.iana.org/assignments/media-types/media-types.xhtml&quot; title=&quot;list of all the official MIME types&quot;&gt;list of all the official MIME types&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;MIME 类型结构&lt;/h2&gt;
&lt;p&gt;每种 &lt;code&gt;MIME&lt;/code&gt; 媒体类型都包含主类型、子类型和可选参数的列表。类型和子类型由一个 斜杠分隔，如果有可选参数的话，则以分号开始，&lt;code&gt;MIME&lt;/code&gt; 类型对大小写不敏感，但是传统写法都是小写。在 &lt;code&gt;HTTP&lt;/code&gt; 中，&lt;code&gt;MIME&lt;/code&gt; 媒体类型被 广泛用于 &lt;code&gt;Content-Type&lt;/code&gt; 和 &lt;code&gt;Accept&lt;/code&gt; 首部。下面是几个例子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Content-Type: video/quicktime
Content-Type: text/html; charset=&quot;iso-8859-6&quot;
Content-Type: multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p
Accept: image/gif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MIME type&lt;/code&gt; 可以分为离散类型、复合类型和多部分类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;离散类型：&lt;code&gt;MIME&lt;/code&gt; 类型可以直接用于描述对象类型，也可以用于描述其他对象类型的集合或类 型包。如果直接用 &lt;code&gt;MIME&lt;/code&gt; 类型来描述某个对象类型，它就是一种离散类型(&lt;code&gt;discrete type&lt;/code&gt;)。其中包括文本文件、视频和应用程序特有的文件格式。&lt;/li&gt;
&lt;li&gt;复合类型：如果 &lt;code&gt;MIME&lt;/code&gt; 类型描述的是其他内容的集合或封装包，这种 &lt;code&gt;MIME&lt;/code&gt; 类型就被称为复合 类型(&lt;code&gt;composite type&lt;/code&gt;)。复合类型描述的是封装包的格式。将封装包打开时，其中包含的每个对象都会有其各自的类型。&lt;/li&gt;
&lt;li&gt;多部分类型：多部分媒体类型是复合类型。多部分对象包含多个组件类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;MIME&lt;/code&gt; 类型由主类型、子类型和可选参数的列表组成。 主类型可以是预定义类型、&lt;a href=&quot;https://www.ietf.org/&quot; title=&quot;IETF &quot;&gt;IETF&lt;/a&gt;（互联网工程任务组 &lt;code&gt;Internet Engineering Task Forc&lt;/code&gt;）定义的扩展标记，或者(以&lt;code&gt;x-&lt;/code&gt;开头的)实验性标记。常见的主类型见下表：&lt;/p&gt;
&lt;p&gt;| 类型          | 描述                               | 典型示例                                                                                                                              |
| ------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| &lt;code&gt;application&lt;/code&gt; | 应用程序特有的内容格式(离散类型)   | &lt;code&gt;application/octet-stream, application/pkcs12, application/vnd.mspowerpoint, application/xhtml+xml, application/xml, application/pdf&lt;/code&gt; |
| &lt;code&gt;audio&lt;/code&gt;       | 音频格式(离散类型)                 | &lt;code&gt;audio/midi, audio/mpeg, audio/webm, audio/ogg, audio/wav&lt;/code&gt;                                                                            |
| &lt;code&gt;chemical&lt;/code&gt;    | 化学数据集(离散 &lt;code&gt;IETF&lt;/code&gt; 扩展类型)   |                                                                                                                                       |
| &lt;code&gt;image&lt;/code&gt;       | 图片格式(离散类型)                 | &lt;code&gt;image/gif, image/png, image/jpeg, image/bmp, image/webp, image/x-icon, image/vnd.microsoft.icon&lt;/code&gt;                                     |
| &lt;code&gt;message&lt;/code&gt;     | 报文格式(复合类型)                 |                                                                                                                                       |
| &lt;code&gt;model&lt;/code&gt;       | 三维模型格式(离散 &lt;code&gt;IETF&lt;/code&gt; 扩展类型) |                                                                                                                                       |
| &lt;code&gt;multipart&lt;/code&gt;   | 多部分对象集合(复合类型)           | &lt;code&gt;multipart/form-data,multipart/byteranges&lt;/code&gt;                                                                                            |
| &lt;code&gt;text&lt;/code&gt;        | 文本格式(离散类型)                 | &lt;code&gt;text/plain, text/html, text/css, text/javascript&lt;/code&gt;                                                                                    |
| &lt;code&gt;video&lt;/code&gt;       | 视频电影格式(离散类型)             | &lt;code&gt;video/webm, video/ogg&lt;/code&gt;                                                                                                               |&lt;/p&gt;
&lt;p&gt;子类型可以是主类型(比如，&lt;code&gt;text/text&lt;/code&gt;)、&lt;code&gt;IANA&lt;/code&gt; 注册的子类型，或者是(以 &lt;code&gt;x-&lt;/code&gt; 开头的)实验性扩展标记。类型和子类型都是由 &lt;code&gt;US-ASCII&lt;/code&gt; 字符的一个子集构成的。空格和某些保留分组以及标点符号称为 &lt;code&gt;tspecials&lt;/code&gt;，它们是控制字符，不能用于类型和子类型名。&lt;/p&gt;
&lt;h2&gt;重要的 MIME type&lt;/h2&gt;
&lt;p&gt;常用 &lt;code&gt;MIME type&lt;/code&gt; 可以查看&lt;code&gt;MDN&lt;/code&gt;：&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types&quot; title=&quot;常用MIME类型列表&quot;&gt;常用MIME类型列表&lt;/a&gt;，完整的 &lt;code&gt;MIME type&lt;/code&gt; 列表查看 &lt;code&gt;IANA&lt;/code&gt; 的&lt;a href=&quot;https://www.iana.org/assignments/media-types/media-types.xhtml&quot; title=&quot;list of all the official MIME types&quot;&gt;list of all the official MIME types&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;application/octet-stream&lt;/h2&gt;
&lt;p&gt;这是应用程序文件的默认值。意思是未知的应用程序文件 ，浏览器一般不会自动执行或询问执行。&lt;code&gt;浏览器会像对待设置了HTTP&lt;/code&gt; 头 &lt;code&gt;Content-Disposition&lt;/code&gt; 值为 &lt;code&gt;attachment&lt;/code&gt; 的文件一样来对待这类文件。&lt;/p&gt;
&lt;h2&gt;text/plain&lt;/h2&gt;
&lt;p&gt;文本文件默认值。即使它意味着未知的文本文件，但浏览器认为是可以直接展示的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;text/plain&lt;/code&gt; 并不是意味着某种文本数据。如果浏览器想要一个文本文件的明确类型，浏览器并不会考虑他们是否匹配。比如说，如果通过一个表明是下载 &lt;code&gt;CSS&lt;/code&gt; 文件的 &lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt; 链接下载了一个 &lt;code&gt;text/plain&lt;/code&gt; 文件。如果提供的信息是 &lt;code&gt;text/plain&lt;/code&gt;，浏览器并不会认出这是有效的 &lt;code&gt;CSS&lt;/code&gt; 文件。&lt;code&gt;CSS&lt;/code&gt; 类型需要使用 &lt;code&gt;text/css&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;text/css&lt;/h2&gt;
&lt;p&gt;在网页中要被解析为 &lt;code&gt;CSS&lt;/code&gt; 的任何 &lt;code&gt;CSS&lt;/code&gt; 文件必须指定 &lt;code&gt;MIME&lt;/code&gt; 为 &lt;code&gt;text/css&lt;/code&gt;。通常，服务器不识别以 &lt;code&gt;.css&lt;/code&gt; 为后缀的文件的 &lt;code&gt;MIME&lt;/code&gt; 类型，而是将其以 &lt;code&gt;MIME&lt;/code&gt; 为 &lt;code&gt;text/plain&lt;/code&gt; 或 &lt;code&gt;application/octet-stream&lt;/code&gt; 来发送给浏览器：在这种情况下，大多数浏览器不识别其为 &lt;code&gt;CSS&lt;/code&gt; 文件，直接忽略掉。特别要注意为 &lt;code&gt;CSS&lt;/code&gt; 文件提供正确的 &lt;code&gt;MIME&lt;/code&gt; 类型。我们在使用 &lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt; 标签的时候也会设置 &lt;code&gt;type&lt;/code&gt; 属性为 &lt;code&gt;text/css&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;text/html&lt;/h2&gt;
&lt;p&gt;所有的 &lt;code&gt;HTML&lt;/code&gt; 内容都应该使用这种类型。&lt;code&gt;XHTML&lt;/code&gt; 的其他 &lt;code&gt;MIME&lt;/code&gt; 类型（如 &lt;code&gt;application/xml+html&lt;/code&gt;）现在基本不再使用（&lt;code&gt;HTML5&lt;/code&gt; 统一了这些格式）。如果你要使用严格的 &lt;code&gt;XML&lt;/code&gt; 解析规则，你仍然要使用 &lt;code&gt;application/xml&lt;/code&gt; 或者 &lt;code&gt;application/xhtml+xml&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;JavaScript types&lt;/h2&gt;
&lt;p&gt;据 &lt;code&gt;MIME&lt;/code&gt; 嗅探标准，&lt;code&gt;application/javascript&lt;/code&gt;，&lt;code&gt;application/ecmascript&lt;/code&gt;是有效的 &lt;code&gt;JavaScript MIME&lt;/code&gt; 类型。所有的 &lt;code&gt;text JavaScript&lt;/code&gt; 类型已经被 &lt;code&gt;RFC 4329&lt;/code&gt; 废弃。&lt;/p&gt;
&lt;h2&gt;图片类型&lt;/h2&gt;
&lt;p&gt;只有一小部分图片类型是被广泛支持的，&lt;code&gt;Web&lt;/code&gt; 安全的，可随时在 &lt;code&gt;Web&lt;/code&gt; 页面中使用的：&lt;/p&gt;
&lt;p&gt;| &lt;code&gt;MIME&lt;/code&gt; 类型       | 图片类型                                   |
| ----------------- | ------------------------------------------ |
| &lt;code&gt;image/gif&lt;/code&gt;       | &lt;code&gt;GIF&lt;/code&gt; 图片 (无损耗压缩方面被 &lt;code&gt;PNG&lt;/code&gt; 所替代) |
| &lt;code&gt;image/jpeg&lt;/code&gt;      | &lt;code&gt;JPEG&lt;/code&gt; 图片                                |
| &lt;code&gt;image/png&lt;/code&gt;       | &lt;code&gt;PNG&lt;/code&gt; 图片                                 |
| &lt;code&gt;image&lt;/code&gt;/&lt;code&gt;svg+xml&lt;/code&gt; | &lt;code&gt;SVG&lt;/code&gt; 图片 (矢量图)                        |&lt;/p&gt;
&lt;p&gt;另外的一些图片种类可以在 &lt;code&gt;Web&lt;/code&gt; 文档中找到。比如很多浏览器支持 &lt;code&gt;icon&lt;/code&gt; 类型的图标作为 &lt;code&gt;favicons&lt;/code&gt; 或者类似的图标，并且浏览器在 &lt;code&gt;MIME&lt;/code&gt; 类型中的 &lt;code&gt;image/x-icon&lt;/code&gt; 支持 &lt;code&gt;ICO&lt;/code&gt; 图像。尽管 i&lt;code&gt;mage/vnd.microsoft.icon&lt;/code&gt; 在 &lt;code&gt;IANA&lt;/code&gt; 注册, 它仍然不被广泛支持，&lt;code&gt;image/x-icon&lt;/code&gt; 被作为替代品使用。&lt;/p&gt;
&lt;h2&gt;音频与视频&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTML&lt;/code&gt; 并没有明确定义被用于 &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 元素所支持的文件类型，所以在 &lt;code&gt;web&lt;/code&gt; 上使用的只有相对较小的一组类型。&lt;code&gt;Web&lt;/code&gt; 中最常见的音频视频格式见下表。&lt;/p&gt;
&lt;p&gt;| MIME 类型                                         | 音频或视频类型                                                                                                           |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| &lt;code&gt;audio/wave,audio/wav,audio/x-wav,audio/x-pn-wav&lt;/code&gt; | 音频流媒体文件。一般支持 &lt;code&gt;PCM&lt;/code&gt; 音频编码 (&lt;code&gt;WAVE codec 1&lt;/code&gt;) ，其他解码器有限支持（如果有的话）。                            |
| &lt;code&gt;audio/webm&lt;/code&gt;                                      | &lt;code&gt;WebM&lt;/code&gt; 音频文件格式。&lt;code&gt;Vorbis&lt;/code&gt; 和 &lt;code&gt;Opus&lt;/code&gt; 是其最常用的解码器。                                                             |
| &lt;code&gt;video/webm&lt;/code&gt;                                      | 采用 &lt;code&gt;WebM&lt;/code&gt; 视频文件格式的音视频文件。&lt;code&gt;VP8&lt;/code&gt; 和 &lt;code&gt;VP9&lt;/code&gt; 是其最常用的视频解码器。&lt;code&gt;Vorbis&lt;/code&gt; 和 &lt;code&gt;Opus&lt;/code&gt; 是其最常用的音频解码器。 |
| &lt;code&gt;audio/ogg&lt;/code&gt;                                       | 采用 &lt;code&gt;OGG&lt;/code&gt; 多媒体文件格式的音频文件。 &lt;code&gt;Vorbis&lt;/code&gt; 是这个多媒体文件格式最常用的音频解码器。                                  |
| &lt;code&gt;video/ogg&lt;/code&gt;                                       | 采用 &lt;code&gt;OGG&lt;/code&gt; 多媒体文件格式的音视频文件。常用的视频解码器是 &lt;code&gt;Theora&lt;/code&gt;；音频解码器为 &lt;code&gt;Vorbis&lt;/code&gt; 。                             |
| &lt;code&gt;application/ogg&lt;/code&gt;                                 | 采用 &lt;code&gt;OGG&lt;/code&gt; 多媒体文件格式的音视频文件。常用的视频解码器是 &lt;code&gt;Theora&lt;/code&gt;；音频解码器为 &lt;code&gt;Vorbis&lt;/code&gt; 。                             |&lt;/p&gt;
&lt;h2&gt;multipart/form-data&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;multipart/form-data&lt;/code&gt; 可用于 &lt;code&gt;HTML&lt;/code&gt; 表单从浏览器发送信息给服务器。作为多部分文档格式，它由边界线（一个由 &lt;code&gt;--&lt;/code&gt; 开始的字符串）划分出的不同部分组成。每一部分有自己的实体，以及自己的 &lt;code&gt;HTTP&lt;/code&gt; 请求头，&lt;code&gt;Content-Disposition&lt;/code&gt;和 &lt;code&gt;Content-Type&lt;/code&gt; 用于文件上传领域，最常用的 (&lt;code&gt;Content-Length&lt;/code&gt; 因为边界线作为分隔符而被忽略）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;form action=&quot;http://localhost:8000/&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&gt;
  &amp;#x3C;input type=&quot;text&quot; name=&quot;myTextField&quot;&gt;
  &amp;#x3C;input type=&quot;checkbox&quot; name=&quot;myCheckBox&quot;&gt;Check&amp;#x3C;/input&gt;
  &amp;#x3C;input type=&quot;file&quot; name=&quot;myFile&quot;&gt;
  &amp;#x3C;button&gt;Send the file&amp;#x3C;/button&gt;
&amp;#x3C;/form&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的表单会发送如下请求：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;POST / HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
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
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=---------------------------8721656041911415653955004498
Content-Length: 465

-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name=&quot;myTextField&quot;

Test
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name=&quot;myCheckBox&quot;

on
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name=&quot;myFile&quot;; filename=&quot;test.txt&quot;
Content-Type: text/plain

Simple file.
-----------------------------8721656041911415653955004498--

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;multipart/byteranges&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;multipart/byteranges&lt;/code&gt; 用于把部分的响应报文发送回浏览器。当发送状态码 &lt;code&gt;206 Partial Content&lt;/code&gt; 时，这个 &lt;code&gt;MIME&lt;/code&gt; 类型用于指出这个文件由若干部分组成，每一个都有其请求范围。就像其他很多类型 &lt;code&gt;Content-Type&lt;/code&gt; 使用分隔符来制定分界线。每一个不同的部分都有 &lt;code&gt;Content-Type&lt;/code&gt; 这样的HTTP头来说明文件的实际类型，以及 &lt;code&gt;Content-Range&lt;/code&gt; 来说明其范围。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
Content-Length: 385

--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 100-200/1270

eta http-equiv=&quot;Content-type&quot; content=&quot;text/html; charset=utf-8&quot; /&gt;
    &amp;#x3C;meta name=&quot;vieport&quot; content
--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 300-400/1270

-color: #f0f0f2;
        margin: 0;
        padding: 0;
        font-family: &quot;Open Sans&quot;, &quot;Helvetica
--3d6b6a416f9b5--
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;浏览器和 MIME type&lt;/h2&gt;
&lt;p&gt;很多 &lt;code&gt;web&lt;/code&gt; 服务器使用默认的 &lt;code&gt;application/octet-stream&lt;/code&gt; 来发送未知类型。出于一些安全原因，对于这些资源浏览器不允许设置一些自定义默认操作，导致用户必须存储到本地以使用。常见的导致服务器配置错误的文件类型如下所示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RAR&lt;/code&gt; 编码文件。在这种情况，理想状态是，设置真实的编码文件类型；但这通常不可能（可能是服务器所未知的类型或者这个文件包含许多其他的不同的文件类型）。这这种情况服务器将发送 &lt;code&gt;application/x-rar-compressed&lt;/code&gt; 作为 &lt;code&gt;MIME&lt;/code&gt; 类型，用户不会将其定义为有用的默认操作。&lt;/li&gt;
&lt;li&gt;音频或视频文件。只有正确设置了MIME类型的文件才能被 &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 或 &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 识别和播放。&lt;/li&gt;
&lt;li&gt;专有文件类型。是专有文件时需要特别注意。使用 &lt;code&gt;application/octet-stream&lt;/code&gt; 作为特殊处理是不被允许的：对于一般的 &lt;code&gt;MIME&lt;/code&gt; 类型浏览器不允许定义默认行为（比如 &lt;code&gt;在Word中打开&lt;/code&gt; ）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在缺失 &lt;code&gt;MIME&lt;/code&gt; 类型或客户端认为文件设置了错误的 &lt;code&gt;MIME&lt;/code&gt; 类型时，浏览器可能会通过查看资源来进行 &lt;code&gt;MIME&lt;/code&gt; 嗅探。每一个浏览器在不同的情况下会执行不同的操作。因为这个操作会有一些安全问题，有的 &lt;code&gt;MIME&lt;/code&gt; 类型表示可执行内容而有些是不可执行内容。浏览器可以通过请求头&lt;code&gt;Content-Type&lt;/code&gt; 来设置 &lt;code&gt;X-Content-Type-Options&lt;/code&gt; 以阻止 &lt;code&gt;MIME&lt;/code&gt; 嗅探。&lt;/p&gt;
&lt;h2&gt;参考文档&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;HTTP&lt;/code&gt; 权威指南&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types&quot; title=&quot;MIME类型 -MDN&quot;&gt;MIME类型 -MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/network.D8pkh64H.jpg"/><enclosure url="/_astro/network.D8pkh64H.jpg"/></item><item><title>HTML 发展</title><link>https://clloz.com/blog/html-history</link><guid isPermaLink="true">https://clloz.com/blog/html-history</guid><pubDate>Mon, 24 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTML&lt;/code&gt; 的全称是 &lt;code&gt;HyperText Markup Language&lt;/code&gt; 超文本标记语言，是一种用于创建网页的标准标记语言。所谓标记语言，就是一种将文本 &lt;code&gt;text&lt;/code&gt; 以及文本相关的其他信息结合起来，展现出关于文档结构和数据处理细节的计算机编码，用标记进行标识。常见的标记语言有 &lt;code&gt;SGML&lt;/code&gt; &lt;code&gt;HTML&lt;/code&gt;、&lt;code&gt;XML&lt;/code&gt; 和 &lt;code&gt;XHTML&lt;/code&gt; 等。&lt;/p&gt;
&lt;h2&gt;HTML 历史&lt;/h2&gt;
&lt;p&gt;其实关于早期的 &lt;code&gt;HTML&lt;/code&gt; 版本我们并没有太多的了解必要，我们主要关注的是，&lt;code&gt;HTML4.01&lt;/code&gt; 和 &lt;code&gt;HTML5&lt;/code&gt; 之间的差别，以及 &lt;code&gt;SGML&lt;/code&gt;、&lt;code&gt;XML&lt;/code&gt; 和 &lt;code&gt;XHTML&lt;/code&gt; 与 &lt;code&gt;HTML&lt;/code&gt; 之间的区别。这里先简要介绍以下 &lt;code&gt;HTML&lt;/code&gt; 的发展历史，作为了解即可。&lt;/p&gt;
&lt;p&gt;首先介绍以下 &lt;code&gt;SGML&lt;/code&gt;，标准通用标记语言（&lt;code&gt;Standard Generalized Markup Language&lt;/code&gt;）是现时常用的超文本格式的最高层次标准，是可以定义标记语言的元语言，甚至可以定义不必采用&lt;code&gt;&amp;#x3C; &gt;&lt;/code&gt;的常规方式。由于它的复杂，因而难以普及。&lt;code&gt;HTML&lt;/code&gt; 和 &lt;code&gt;XML&lt;/code&gt; 同样派生于它：&lt;code&gt;XML&lt;/code&gt; 可以被认为是它的一个子集，而 &lt;code&gt;HTML&lt;/code&gt; 是它的一个应用。&lt;code&gt;XML&lt;/code&gt; 的产生就是为了简化它，以便用于更加通用的目的，比如语义 &lt;code&gt;Web&lt;/code&gt;。它已经应用于大量的场合，比较著名的有 &lt;code&gt;XHTML&lt;/code&gt;、&lt;code&gt;RSS&lt;/code&gt;、&lt;code&gt;XML-RPC&lt;/code&gt; 和 &lt;code&gt;SOAP&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTML&lt;/code&gt; 是蒂姆·伯纳斯-李在发明万维网的过程中根据 &lt;code&gt;SGML&lt;/code&gt; 创造的，它描述 &lt;code&gt;18&lt;/code&gt; 个元素，包括 &lt;code&gt;HTML&lt;/code&gt; 初始的、相对简单的设计。除了超链接之外，其他标签都是以 &lt;code&gt;SGML&lt;/code&gt; 为基础的。这些标签中有 &lt;code&gt;11&lt;/code&gt; 个到 &lt;code&gt;HTML4&lt;/code&gt; 都仍然存在。&lt;/p&gt;
&lt;p&gt;不存在 &lt;code&gt;HTML 1.0&lt;/code&gt;，只在 &lt;code&gt;1993&lt;/code&gt; 年中期由 &lt;a href=&quot;https://zh.wikipedia.org/zh/%E4%BA%92%E8%81%94%E7%BD%91%E5%B7%A5%E7%A8%8B%E4%BB%BB%E5%8A%A1%E7%BB%84&quot; title=&quot;IETF互联网工程任务组&quot;&gt;IETF互联网工程任务组&lt;/a&gt;提出了一个&lt;a href=&quot;https://www.w3.org/MarkUp/draft-ietf-iiir-html-01.txt&quot; title=&quot;超文本标记语言（HTML）互联网草案&quot;&gt;超文本标记语言（HTML）互联网草案&lt;/a&gt;。&lt;code&gt;HTML&lt;/code&gt; 第一个正式的规范是由 &lt;code&gt;IETF&lt;/code&gt; 的 &lt;code&gt;HTML&lt;/code&gt; 工作小组创建的 &lt;code&gt;HTML 2.0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;IETF&lt;/code&gt; 的主持下，&lt;code&gt;HTML&lt;/code&gt; 标准的进一步发展因竞争利益而遭受停滞。自 &lt;code&gt;1996&lt;/code&gt; 年起，&lt;code&gt;HTML&lt;/code&gt; 规范一直由万维网联盟（&lt;code&gt;W3C&lt;/code&gt;）维护，并由商业软件厂商出资。不过在 &lt;code&gt;2000&lt;/code&gt; 年，&lt;code&gt;HTML&lt;/code&gt; 也成为国际标准（&lt;code&gt;ISO/ IEC15445：2000&lt;/code&gt;）。&lt;code&gt;HTML 4.01&lt;/code&gt; 于 &lt;code&gt;1999&lt;/code&gt; 年末发布，进一步的勘误版本于 &lt;code&gt;2001&lt;/code&gt; 年发布。&lt;code&gt;2004&lt;/code&gt; 年，网页超文本应用技术工作小组（&lt;code&gt;WHATWG&lt;/code&gt;）开始开发 &lt;code&gt;HTML5&lt;/code&gt;，并在 &lt;code&gt;2008&lt;/code&gt; 年与 &lt;code&gt;W3C&lt;/code&gt; 共同交付，&lt;code&gt;2014年10月28日&lt;/code&gt; 完成标准化。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;whatwg&lt;/code&gt; 是在 &lt;code&gt;W3C&lt;/code&gt; 强推 &lt;code&gt;XHTML2.0&lt;/code&gt; 后从 &lt;code&gt;W3C&lt;/code&gt; 分裂出去的组织，如今两边已经是高度合作推动标准化。&lt;code&gt;W3C&lt;/code&gt; 的 &lt;code&gt;HTML5&lt;/code&gt; 是 &lt;code&gt;whatwg&lt;/code&gt; 的一个 &lt;code&gt;snapshot&lt;/code&gt;。&lt;code&gt;whatwg&lt;/code&gt; 的标准是 &lt;code&gt;living standard&lt;/code&gt;，也就是只有一个最新版本，不像 &lt;code&gt;w3c&lt;/code&gt; 有各个阶段的版本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;HTML 语法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/html-syntax.CePQUlbl_Z6Maqx.webp&quot; alt=&quot;html-syntax&quot; title=&quot;html-syntax&quot;&gt;&lt;/p&gt;
&lt;h2&gt;标签&lt;/h2&gt;
&lt;p&gt;标签语法产生元素，我们从语法的角度讲，就用“标签”这个术语，我们从运行时的角度讲，就用“元素”这个术语。&lt;code&gt;HTML&lt;/code&gt; 中，用于描述一个元素的标签分为开始标签、结束标签和自闭合标签。开始标签和自闭合标签中，又可以有属性。开始标签的标签名称只能使用英文字母。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开始标签：&lt;code&gt;&amp;#x3C;tagname&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;带属性的开始标签：&lt;code&gt;&amp;#x3C;tagname attributename=&quot;attributevalue&quot;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;结束标签：&lt;code&gt;&amp;#x3C;/tagname&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;自闭合标签：&lt;code&gt;&amp;#x3C;tagname /&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;属性可以使用单引号、双引号或者完全不用引号，这三种情况下，需要转义的部分都不太一样。属性中可以使用字符实体（参考另一篇文章&lt;a href=&quot;https://www.clloz.com/programming/front-end/2019/11/19/html-character-entity/&quot; title=&quot;HTML character entity HTML字符实体&quot;&gt;HTML character entity HTML字符实体&lt;/a&gt;）来做转义，属性中，一定需要转义的有下面几种。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无引号属性：&lt;code&gt;&amp;#x3C;tab&gt; &amp;#x3C;LF&gt; &amp;#x3C;FF&gt; &amp;#x3C;SPACE&gt; &amp;#x26;&lt;/code&gt;五种字符。&lt;/li&gt;
&lt;li&gt;单引号属性：&lt;code&gt;&apos; &amp;#x26;&lt;/code&gt;两种字符。&lt;/li&gt;
&lt;li&gt;双引号属性：&lt;code&gt;&quot; &amp;#x26;&lt;/code&gt;两种字符。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一般来说，灵活运用属性的形式，是不太用到文本实体转义的。&lt;/p&gt;
&lt;h2&gt;文本&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;HTML&lt;/code&gt; 中，规定了两种文本语法，一种是普通的文本节点，另一种是 &lt;code&gt;CDATA&lt;/code&gt; 文本节点。注意，&lt;code&gt;CDATA&lt;/code&gt; 片段不应该在 &lt;code&gt;HTML&lt;/code&gt; 中被使用；它只在 &lt;code&gt;XML&lt;/code&gt; 中有效。&lt;/p&gt;
&lt;h2&gt;注释&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTML&lt;/code&gt; 注释语法以 &lt;code&gt;&amp;#x3C;!--开头，以--&gt;&lt;/code&gt; 结尾，注释的内容非常自由，除了 &lt;code&gt;--&gt;&lt;/code&gt; 都没有问题。如果注释的内容一定要出现 &lt;code&gt;--&gt;&lt;/code&gt;，我们可以拆成多个注释节点。&lt;/p&gt;
&lt;h2&gt;DTD 文档类型定义&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SGML&lt;/code&gt; 的 &lt;code&gt;DTD&lt;/code&gt; 语法十分复杂，但是对 &lt;code&gt;HTML&lt;/code&gt; 来说，其实 &lt;code&gt;DTD&lt;/code&gt; 的选项是有限的，浏览器在解析 &lt;code&gt;DTD&lt;/code&gt; 时，把它当做几种字符串之一，关于 &lt;code&gt;DTD&lt;/code&gt;，我在本篇文章的后面会详细讲解。&lt;/p&gt;
&lt;h2&gt;ProcessingInstruction 语法（处理信息）&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ProcessingInstruction&lt;/code&gt; 多数情况下，是给机器看的。&lt;code&gt;HTML&lt;/code&gt; 中规定了可以有 &lt;code&gt;ProcessingInstruction&lt;/code&gt;，但是并没有规定它的具体内容，所以可以把它视为一种保留的扩展机制。对浏览器而言，&lt;code&gt;ProcessingInstruction&lt;/code&gt; 的作用类似于注释。&lt;code&gt;ProcessingInstruction&lt;/code&gt; 包含两个部分，紧挨着第一个问号后，空格前的部分被称为“目标”，这个目标一般表示处理 &lt;code&gt;ProcessingInstruction&lt;/code&gt; 的程序名。剩余部分是它的文本信息，没有任何格式上的约定，完全由文档编写者和处理程序的编写者约定。&lt;/p&gt;
&lt;h2&gt;DTD&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;DTD&lt;/code&gt; 的全称是 &lt;code&gt;Document Type Defination&lt;/code&gt;，也就是文档类型定义。&lt;code&gt;DTD&lt;/code&gt; 是一套用来定义文档类型的标记声明，主要的对象是 &lt;code&gt;SGML&lt;/code&gt; 家族的标记语言：&lt;code&gt;GML, SGML, XML, HTML&lt;/code&gt;。在 &lt;code&gt;DTD&lt;/code&gt; 中可以定义文档中的元素、元素的属性、元素的排列方式、元素包含的内容等等。&lt;code&gt;DTD&lt;/code&gt; 有四个组成如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;元素（&lt;code&gt;Elements&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;属性（&lt;code&gt;Attribute&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;实体（&lt;code&gt;Entities&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;注释（&lt;code&gt;Comments&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!--元素声明语法--&gt;
&amp;#x3C;!ELEMENT 元素名称　元素內容&gt;
&amp;#x3C;!--属性声明语法--&gt;
&amp;#x3C;!ATTLIST 元素名称、属性名称、属性值数据类型、属性默认值&gt;
&amp;#x3C;!--实体声明语法--&gt;
&amp;#x3C;!ENTITY 是名称　实体内容&gt;
&amp;#x3C;!--注释语法--&gt;
&amp;#x3C;!-- 注释内容 --&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;在 &lt;code&gt;HTML5&lt;/code&gt; 之后，由于这些 &lt;code&gt;DTD&lt;/code&gt; 过于复杂的写法没啥实际用途，加之浏览器也不会使用 &lt;code&gt;SGML&lt;/code&gt; 去解析他们，干脆放弃了 &lt;code&gt;SGML&lt;/code&gt; 子集的的支持，规定了一个简单易记住的类似 &lt;code&gt;DTD&lt;/code&gt; 的声明 &lt;code&gt;&amp;#x3C;!DOCTYPE html&gt;&lt;/code&gt;。&lt;code&gt;XML&lt;/code&gt; 也逐渐用 &lt;code&gt;XML Schema&lt;/code&gt; 来代替 &lt;code&gt;DTD&lt;/code&gt;。但是因为我们的互联网上仍然存在大量的 &lt;code&gt;HTML4.01&lt;/code&gt; 和 &lt;code&gt;xhtml1.0&lt;/code&gt; 的网页，所以新的标准都是向前兼容的（比较激进的不向前兼容的 &lt;code&gt;xhtml2.0&lt;/code&gt; 已经废弃）。常用的 &lt;code&gt;DOCTYPE&lt;/code&gt; 声明有如下几种。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!--HTML 5--&gt;
&amp;#x3C;!DOCTYPE html&gt;

&amp;#x3C;!--HTML 4.01 Strict 这个 DTD 包含所有 HTML 元素和属性，但不包括表象或过时的元素（如 font ）。框架集是不允许的。--&gt;
&amp;#x3C;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01//EN&quot; &quot;http://www.w3.org/TR/html4/strict.dtd&quot;&gt;

&amp;#x3C;!--HTML 4.01 Transitional 这个 DTD 包含所有 HTML 元素和属性，包括表象或过时的元素（如 font ）。框架集是不允许的。--&gt;
&amp;#x3C;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&gt;

&amp;#x3C;!--HTML 4.01 Frameset 这个 DTD 与 HTML 4.01 Transitional 相同，但是允许使用框架集内容。--&gt;
&amp;#x3C;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Frameset//EN&quot; &quot;http://www.w3.org/TR/html4/frameset.dtd&quot;&gt;

&amp;#x3C;!--XHTML 1.0 Strict 这个 DTD 包含所有 HTML 元素和属性，但不包括表象或过时的元素（如 font ）。框架集是不允许的。结构必须按标准格式的 XML 进行书写。--&gt;
&amp;#x3C;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Strict//EN&quot; &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd&quot;&gt;

&amp;#x3C;!--XHTML 1.0 Transitional 这个 DTD 包含所有 HTML 元素和属性，包括表象或过时的元素（如 font ）。框架集是不允许的。结构必须按标准格式的 XML 进行书写。--&gt;
&amp;#x3C;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Transitional//EN&quot; &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;&gt;

&amp;#x3C;!--XHTML 1.0 Frameset 这个 DTD 与 XHTML 1.0 Transitional 相同，但是允许使用框架集内容。--&gt;
&amp;#x3C;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Frameset//EN&quot; &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd&quot;&gt;

&amp;#x3C;!--XHTML 1.1 这个 DTD 与 XHTML 1.0 Strict 相同，但是允许您添加模块（例如为东亚语言提供 ruby 支持）。--&gt;
&amp;#x3C;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.1//EN&quot; &quot;http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;doctype&lt;/code&gt; 声明开头的 &lt;code&gt;!DOCTYPE&lt;/code&gt; 大小写不敏感。文档开头必须声明 &lt;code&gt;doctype&lt;/code&gt; 否则浏览器无法判断文档类型，也无法正确解析。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;XML&lt;/h2&gt;
&lt;p&gt;可扩展标记语言（&lt;code&gt;Extensible Markup Language&lt;/code&gt;）是从标准通用标记语言（&lt;code&gt;SGML&lt;/code&gt;）中简化修改出来的。之所以成为可扩展是因为它的标记 &lt;code&gt;markup&lt;/code&gt; 是可以由开发者自由定义的，而不像 &lt;code&gt;HTML&lt;/code&gt; 是由标准制定者来定义一个通用的标准。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;XML&lt;/code&gt; 用于描绘封装数据，而 &lt;code&gt;HTML&lt;/code&gt; 超文本标记语言用于展示数据，&lt;code&gt;XHTML&lt;/code&gt; 就是用 &lt;code&gt;XML&lt;/code&gt; 规则规范的 &lt;code&gt;HTML&lt;/code&gt;。&lt;code&gt;XML&lt;/code&gt; 语法更加严格。&lt;code&gt;XML&lt;/code&gt; 与 &lt;code&gt;HTML&lt;/code&gt; 的区别主要有如下一些点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;XML&lt;/code&gt; 大小写敏感，&lt;code&gt;HTML&lt;/code&gt; 大小写不敏感。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XML&lt;/code&gt; 是严格的树状结构，不能省略掉结束标记。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;XML&lt;/code&gt; 中，拥有单个标记而没有匹配的结束标记的元素必须用一个&lt;code&gt;/&lt;/code&gt; 字符作为结尾。这样分析器就知道不用查找结束标记了。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;XML&lt;/code&gt; 中，属性值必须分装在引号中。在 &lt;code&gt;HTML&lt;/code&gt; 中，引号是可用可不用的。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;HTML&lt;/code&gt; 中，可以拥有不带值的属性名。在 &lt;code&gt;XML&lt;/code&gt; 中，所有的属性都必须带有相应的值。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;XML&lt;/code&gt; 文档中，空白部分不会被解析器自动删除；但是 &lt;code&gt;HTML&lt;/code&gt; 是过滤掉空格的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HTML&lt;/code&gt; 使用固有的标记；而 &lt;code&gt;XML&lt;/code&gt; 没有固有的标记。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HTML&lt;/code&gt; 标签是预定义的；&lt;code&gt;XML&lt;/code&gt; 标签是自定义的、可扩展的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HTML&lt;/code&gt; 是用来显示数据的；&lt;code&gt;XML&lt;/code&gt; 是用来描述数据、存放数据的，所以可以作为持久化的介质。&lt;code&gt;HTML&lt;/code&gt; 将数据和显示结合在一起，在页面中把这数据显示出来；&lt;code&gt;XML&lt;/code&gt; 则将数据和显示分开。 &lt;code&gt;XML&lt;/code&gt; 被设计用来描述数据，其焦点是数据的内容。&lt;code&gt;HTML&lt;/code&gt; 被设计用来显示数据，其焦点是数据的外观。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XML&lt;/code&gt; 不是 HTML 的替代品，&lt;code&gt;XML&lt;/code&gt; 和 &lt;code&gt;HTML&lt;/code&gt; 是两种不同用途的语言。 &lt;code&gt;XML&lt;/code&gt; 不是要替换 &lt;code&gt;HTML&lt;/code&gt;；&lt;code&gt;XML&lt;/code&gt; 和 &lt;code&gt;HTML&lt;/code&gt; 的目标不同 &lt;code&gt;HTML&lt;/code&gt; 的设计目标是显示数据并集中于数据外观，而 &lt;code&gt;XML&lt;/code&gt; 的设计目标是描述数据并集中于数据的内容。&lt;/li&gt;
&lt;li&gt;没有任何行为的 &lt;code&gt;XML&lt;/code&gt;。与 &lt;code&gt;HTML&lt;/code&gt; 相似，&lt;code&gt;XML&lt;/code&gt; 不进行任何操作。（共同点）&lt;/li&gt;
&lt;li&gt;对于 &lt;code&gt;XML&lt;/code&gt; 最好的形容可能是: &lt;code&gt;XML&lt;/code&gt; 是一种跨平台的，与软、硬件无关的，处理与传输信息的工具。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XML&lt;/code&gt; 未来将会无所不在。&lt;code&gt;XML&lt;/code&gt; 将成为最普遍的数据处理和数据传输的工具。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;XHTML&lt;/h2&gt;
&lt;p&gt;可扩展超文本标记语言（&lt;code&gt;eXtensible HyperText Markup Language&lt;/code&gt;），是一种标记语言，表现方式与超文本标记语言（&lt;code&gt;HTML&lt;/code&gt;）类似，不过语法上更加严格。从继承关系上讲，&lt;code&gt;HTML&lt;/code&gt; 是一种基于标准通用标记语言（&lt;code&gt;SGML&lt;/code&gt;）的应用（&lt;code&gt;HTML5&lt;/code&gt; 之前），是一种非常灵活的置标语言，而 &lt;code&gt;XHTML&lt;/code&gt; 则基于可扩展标记语言（&lt;code&gt;XML&lt;/code&gt;），&lt;code&gt;XML&lt;/code&gt; 是 &lt;code&gt;SGML&lt;/code&gt; 的一个子集。&lt;code&gt;XHTML 1.0&lt;/code&gt; 在 &lt;code&gt;2000年1月26日&lt;/code&gt; 成为 &lt;code&gt;W3C&lt;/code&gt; 的推荐标准。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;XHTML1.1&lt;/code&gt; 为 &lt;code&gt;XHTML&lt;/code&gt; 最后的独立标准，&lt;code&gt;2.0&lt;/code&gt; 止于草案阶段。&lt;code&gt;XHTML5&lt;/code&gt; 则是属于 &lt;code&gt;HTML5&lt;/code&gt; 标准的一部分，且名称已改为 &lt;code&gt;以XML序列化的HTML5 XML-serialized HTML5&lt;/code&gt;，而非 &lt;code&gt;可扩展的HTML eXtensible HyperText Markup Language&lt;/code&gt;。&lt;code&gt;XHTML5&lt;/code&gt; 是对 &lt;code&gt;HTML5&lt;/code&gt; 的 &lt;code&gt;XML&lt;/code&gt; 序列化。&lt;code&gt;XML&lt;/code&gt; 文档必须被设置为 &lt;code&gt;XML&lt;/code&gt; 互联网文件类型，像 &lt;code&gt;application/xhtml+xml&lt;/code&gt; 或者 &lt;code&gt;application/xml&lt;/code&gt;。&lt;code&gt;XHTML5&lt;/code&gt; 要求像 &lt;code&gt;XML&lt;/code&gt; 一样严格的格式化的语法。在 &lt;code&gt;XHTML5&lt;/code&gt; 中，&lt;code&gt;HTML5&lt;/code&gt; 的&lt;code&gt;&amp;#x3C;!DOCTYPE HTML&gt;&lt;/code&gt; 可有可无的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;Content-Type: text/html

&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;utf-8&quot; /&gt;
    &amp;#x3C;title&gt;HTML&amp;#x3C;/title&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;p&gt;I am a HTML document&amp;#x3C;/p&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;Content-Type: application/xhtml+xml &amp;#x3C;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&amp;#x3C;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; xml:lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;title&gt;XHTML&amp;#x3C;/title&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;p&gt;I am a XHTML document&amp;#x3C;/p&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;XHTML&lt;/code&gt; 曾经是作为 &lt;code&gt;HTML&lt;/code&gt; 的继承者推出的,由于 &lt;code&gt;HTML&lt;/code&gt; 的语法较为松散,因此就出现了由 &lt;code&gt;DTD&lt;/code&gt; 定义规则,语法要求更加严格的 &lt;code&gt;XHTML&lt;/code&gt;。&lt;code&gt;XHTML&lt;/code&gt; 与 &lt;code&gt;HTML&lt;/code&gt; 的主要区别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;XHTML&lt;/code&gt; 与 &lt;code&gt;HTML&lt;/code&gt; 的最大的变化在于所有标签必须闭合。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XHTML&lt;/code&gt; 中所有的标签必须小写。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XHTML&lt;/code&gt; 元素必须被正确地嵌套。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XHTML&lt;/code&gt; 文档必须拥有根元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTML、XHTML、XML 和 SGML 的关系&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SGML&lt;/code&gt; 定义电子文档和内容描述的标准。&lt;code&gt;DTD&lt;/code&gt; 标准是 &lt;code&gt;SGML&lt;/code&gt; 的一部分。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XML&lt;/code&gt; 是 &lt;code&gt;SGML&lt;/code&gt; 的子集，优化版。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HTML&lt;/code&gt; 是遵循了 &lt;code&gt;DTD&lt;/code&gt; 标准的 &lt;code&gt;SGML&lt;/code&gt; 的文档，也可以说是 &lt;code&gt;SGML&lt;/code&gt; 的一个实例。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XHTML&lt;/code&gt; 是遵循了 &lt;code&gt;XML&lt;/code&gt; 标准的 &lt;code&gt;HTML&lt;/code&gt; 文档。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HTML5&lt;/code&gt; 是最新的 &lt;code&gt;HTML&lt;/code&gt; 标准。但不基于 &lt;code&gt;SGML&lt;/code&gt;，所以不遵循 &lt;code&gt;DTD&lt;/code&gt; 标准。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTML5&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTML5&lt;/code&gt; 提供了一些新的元素和属性，反映典型的现代用法网站。其中有些是技术上类似 &lt;code&gt;&amp;#x3C;div&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;span&gt;&lt;/code&gt; 标签，但有一定含义，例如 &lt;code&gt;&amp;#x3C;nav&gt;&lt;/code&gt;（网站导航块）和 &lt;code&gt;&amp;#x3C;footer&gt;&lt;/code&gt;。这种标签将有利于搜索引擎的索引整理、小屏幕设备和视障人士使用。同时为其他浏览要素提供了新的功能，通过一个标准接口，如 &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 标记。一些过时的 &lt;code&gt;HTML 4.01&lt;/code&gt; 标记将取消，其中包括纯粹用作显示效果的标记，如 &lt;code&gt;&amp;#x3C;font&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;center&gt;&lt;/code&gt;，因为它们已经被 &lt;code&gt;CSS&lt;/code&gt; 取代。还有一些通过 &lt;code&gt;DOM&lt;/code&gt; 的网络行为。&lt;/p&gt;
&lt;p&gt;尽管和 &lt;code&gt;SGML&lt;/code&gt; 在标记上的相似性，&lt;code&gt;HTML5&lt;/code&gt; 的句法并不再基于它了，而是被设计成向后兼容对老版本的 &lt;code&gt;HTML&lt;/code&gt; 的解析。它有一个新的开始列看起来就像 &lt;code&gt;SGML&lt;/code&gt; 的文档类型声明，&lt;code&gt;&amp;#x3C;!DOCTYPE HTML&gt;&lt;/code&gt;，这会触发和标准兼容的渲染模式。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTML5&lt;/code&gt; 与 &lt;code&gt;HTML4.01&lt;/code&gt; 和 &lt;code&gt;XHTML1.x&lt;/code&gt; 的差异&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不再基于 &lt;code&gt;SGML&lt;/code&gt; 所以也不再需要 &lt;code&gt;DTD&lt;/code&gt;，文件类型声明仅有一型：&lt;code&gt;&amp;#x3C;!DOCTYPE HTML&gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;新的解析顺序：不再基于 &lt;code&gt;SGML&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;新的语义化标签：&lt;code&gt;section, video, progress, nav, meter, time, aside, canvas, command, datalist, details, embed, figcaption, figure, footer, header, hgroup, keygen, mark, output, rp, rt, ruby, source, summary, wbr&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;input&lt;/code&gt; 元素的新类型：&lt;code&gt;date&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt; 等等。&lt;/li&gt;
&lt;li&gt;新的属性：&lt;code&gt;ping&lt;/code&gt;（用于 &lt;code&gt;a&lt;/code&gt; 与 &lt;code&gt;area&lt;/code&gt;）, &lt;code&gt;charset&lt;/code&gt;（用于 &lt;code&gt;meta&lt;/code&gt;）, &lt;code&gt;async&lt;/code&gt;（用于 &lt;code&gt;script&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;全局属性：&lt;code&gt;id, tabindex, repeat&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;新的全局属性：&lt;code&gt;contenteditable, contextmenu, draggable, dropzone, hidden, spellcheck&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;移除标签：&lt;code&gt;acronym, applet, basefont, big, center, dir, font, frame, frameset, isindex, noframes, strike, tt&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;除了原先的 &lt;code&gt;DOM&lt;/code&gt; 接口，&lt;code&gt;HTML5&lt;/code&gt; 增加了更多样化的 &lt;code&gt;API&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Canvas&lt;/li&gt;
&lt;li&gt;Timed Media Playback&lt;/li&gt;
&lt;li&gt;Offline&lt;/li&gt;
&lt;li&gt;Editable content&lt;/li&gt;
&lt;li&gt;Drag and drop&lt;/li&gt;
&lt;li&gt;History&lt;/li&gt;
&lt;li&gt;MIME type and protocol handler registration&lt;/li&gt;
&lt;li&gt;Microdata&lt;/li&gt;
&lt;li&gt;Web Messaging&lt;/li&gt;
&lt;li&gt;Web Storage – a key-value pair storage framework that provides behaviour similar to cookies but with larger storage capacity and improved API.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于 &lt;code&gt;HTML5&lt;/code&gt; 的新特性，参考 &lt;code&gt;MDN&lt;/code&gt; 的&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/Guide/HTML/HTML5&quot; title=&quot;HTML5&quot;&gt;HTML5&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文介绍了一些和 &lt;code&gt;HTML&lt;/code&gt; 相关的概念，这些概念其实并不清晰甚至有些混乱，主要就是 &lt;code&gt;HTML&lt;/code&gt; 的标准化本身就走的比较磕磕绊绊，有些标准已经消失在历史中。不过现在我们只要专注于掌握 &lt;code&gt;HTML5&lt;/code&gt; 标准即可。用来描述数据的 &lt;code&gt;XML&lt;/code&gt; 也需要掌握。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_41796631/article/details/89371356&quot; title=&quot;HTML、XML、XHTML 有什么区别 (SGML、DTD 标准 )&quot;&gt;HTML、XML、XHTML 有什么区别 (SGML、DTD 标准 )&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/HTML5#XHTML5%EF%BC%88XML-serialized_HTML5%EF%BC%89&quot; title=&quot;HTML5 - 维基百科&quot;&gt;HTML5 - 维基百科&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/html-logo.jHluxOew.png"/><enclosure url="/_astro/html-logo.jHluxOew.png"/></item><item><title>CSS 视觉格式化模型（一）：盒模型和盒类型</title><link>https://clloz.com/blog/visual-formatting-model-1</link><guid isPermaLink="true">https://clloz.com/blog/visual-formatting-model-1</guid><pubDate>Wed, 19 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;CSS 视觉格式化模型系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/css/2020/08/19/visual-formatting-model-1/&quot; title=&quot;CSS 视觉格式化模型（一）：盒模型和盒类型&quot;&gt;CSS 视觉格式化模型（一）：盒模型和盒类型&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/css/2020/08/19/visual-formatting-model-2/&quot; title=&quot;CSS 视觉格式化模型（二）：格式上下文&quot;&gt;CSS 视觉格式化模型（二）：格式上下文&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们的浏览器说到底也就是一种信息的展示，无论是文字，图片还是其他媒体内容。内容的展示其实就是将内容按照一定的规则来对文档信息（&lt;code&gt;HTML&lt;/code&gt; 文档和 &lt;code&gt;CSS&lt;/code&gt; ）进行计算并渲染到屏幕上，这本质上上和现实中的印刷排版还是类似的，只不过载体不同。视觉格式化模型就是这套规则。我将分几篇文章来写一写我阅读标准的一些笔记。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;视觉格式化模型 &lt;code&gt;visual formatting model&lt;/code&gt; 对应&lt;a href=&quot;https://www.w3.org/TR/CSS22/&quot; title=&quot;CSS2.2&quot;&gt;CSS2.2&lt;/a&gt;的第九和第十章，我认为是 &lt;code&gt;CSS2.2&lt;/code&gt; 中最核心的部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;盒模型 box model&lt;/h2&gt;
&lt;p&gt;盒模型是我们了解视觉格式化模型的基础，我们的浏览器就是将文档中的元素转化为一个一个的盒子再来进行排版和渲染的，盒是浏览器排版和渲染的基本单位。盒模型描述了一个为文档树中的元素生成的并根据视觉格式化模型进行布局的矩形框。&lt;/p&gt;
&lt;p&gt;我们先来了解一个盒子的基本结构（见下图）。一个盒子必然有一个 &lt;code&gt;content area&lt;/code&gt; 和可选的 &lt;code&gt;margin&lt;/code&gt;，&lt;code&gt;padding&lt;/code&gt; 和 &lt;code&gt;border&lt;/code&gt;。&lt;code&gt;margin&lt;/code&gt; 表示外边距，&lt;code&gt;padding&lt;/code&gt; 表示内边距，&lt;code&gt;border&lt;/code&gt; 表示边框。&lt;code&gt;content area&lt;/code&gt; 的宽高由盒内的内容以及视觉格式化模型的规则来决定。外边距总是透明的，内边距和边框的背景样式由元素的 &lt;code&gt;background&lt;/code&gt; 决定。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/box-model.DCObkgyT_ZxiXVx.webp&quot; alt=&quot;box-model&quot; title=&quot;box-model&quot;&gt;&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;margin&lt;/code&gt; 和 &lt;code&gt;padding&lt;/code&gt; 的值是百分比时，注意的是这个百分比是根据该元素的包含块 &lt;code&gt;containing block&lt;/code&gt; 的 &lt;code&gt;width&lt;/code&gt; 来计算的。每一个元素生成的盒就是其后代元素的盒的包含块，这里说的该元素的包含块是指该元素所在的包含块，而不是该元素生成的包含块。如果包含块的宽度是由该元素决定的（即包含块没有设置宽度，由该元素撑开的情况），这种情况下产生的布局是 &lt;code&gt;CSS2.2&lt;/code&gt; 中未定义的（即由具体实现的浏览器决定）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以包含块的宽度为基准的原因就是为了在水平和垂直放下上有相同的留白，无论是外边距还是内边距都是为了分隔盒，让排版更加的清晰，容易阅读。而水平和垂直方向的边距用相同的基准也是符合直觉的。而为什么以宽度而不是高度为基准其实也很简单，我们的阅读习惯是从左向右的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;margin&lt;/code&gt; 和 &lt;code&gt;padding&lt;/code&gt; 区别主要有三个个，第一是 &lt;code&gt;margin&lt;/code&gt; 可以取值 &lt;code&gt;auto&lt;/code&gt; 而 &lt;code&gt;padding&lt;/code&gt; 不可以，第二是 &lt;code&gt;margin&lt;/code&gt; 值可以为负但是 &lt;code&gt;padding&lt;/code&gt; 不可以，第三是垂直方向上的 &lt;code&gt;margin&lt;/code&gt; 对非替换的行内元素是不会产生任何效果。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可替换元素指的是独立于 &lt;code&gt;CSS&lt;/code&gt; 的外部对象，它们的内容不受当前文档的样式的影响。CSS 可以影响可替换元素的位置，但不会影响到可替换元素自身的内容。某些可替换元素，例如 &lt;code&gt;&amp;#x3C;iframe&gt;&lt;/code&gt; 元素，可能具有自己的样式表，但它们不会继承父文档的样式。常见的可替换元素有 &lt;code&gt;iframe&lt;/code&gt;，&lt;code&gt;img&lt;/code&gt;，&lt;code&gt;ebmed&lt;/code&gt; 和 &lt;code&gt;video&lt;/code&gt; 等，有些元素只在特定情况下被当做可替换元素，比如 &lt;code&gt;&amp;#x3C;option&gt;&lt;/code&gt;，&lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt;，&lt;code&gt;&amp;#x3C;canvas&gt;&lt;/code&gt;，&lt;code&gt;&amp;#x3C;object&gt;&lt;/code&gt;，&lt;code&gt;&amp;#x3C;applet&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;margin&lt;/code&gt; 和 &lt;code&gt;padding&lt;/code&gt; 都是简写属性，他们都有上下左右四个分量。简写属性接受多个值，用逗号分隔开。如果只给一个值，那么该值作用于四个分量；如果给两个值，那么第一个值作用于上下分量，而第二个值作用于左右分量；如果给三个值，那么第一个值作用于上分量，第二个值作用于左右分量，第三个值作用于下分量；如果给四个值，那么分别作用域上下左右分量。这种规则也适用于 &lt;code&gt;border&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;border&lt;/code&gt; 是简写属性，他有上下左右四个分量 &lt;code&gt;border-top&lt;/code&gt;，&lt;code&gt;border-right&lt;/code&gt;，&lt;code&gt;border-bottome&lt;/code&gt; 和 &lt;code&gt;border-left&lt;/code&gt;，不过和 &lt;code&gt;margin/padding&lt;/code&gt; 不同，&lt;code&gt;border&lt;/code&gt; 简写属性不能对四个方向设置不同的值。&lt;code&gt;border&lt;/code&gt; 的四个分量依然是简写属性，他们还有 &lt;code&gt;color&lt;/code&gt;，&lt;code&gt;style&lt;/code&gt; 和 &lt;code&gt;width&lt;/code&gt; 三个分量。而 &lt;code&gt;border-color&lt;/code&gt;，&lt;code&gt;border-style&lt;/code&gt; 和 &lt;code&gt;border-width&lt;/code&gt; 也可以作为简写属性直接赋值，他们能够接受一到四个值，和 &lt;code&gt;margin/padding&lt;/code&gt; 的规则一样，可以为四个方向应用不同的值。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;border&lt;/code&gt; 相关的属性很多并且功能有所重叠，我们可以灵活运用，比如用 &lt;code&gt;border&lt;/code&gt; 设置好四个方向的值在用 &lt;code&gt;border-left&lt;/code&gt; 对 &lt;code&gt;border&lt;/code&gt; 进行覆盖实现独立的效果等。&lt;/p&gt;
&lt;p&gt;还有需要注意一点的是元素的 &lt;code&gt;background&lt;/code&gt; 是覆盖到 &lt;code&gt;border&lt;/code&gt; 的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div style=&quot;width: 100%; text-align:center;&quot;&gt;
  &amp;#x3C;div
    style=&quot;display: inline-block; height: 300px; width: 300px; background: pink; border: 3px dashed rgba(0, 0, 0, 0.3)&quot;
  &gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;盒模型还有一点要注意的是 &lt;code&gt;box-sizing&lt;/code&gt; 属性，该属性决定了如何计算元素的高度和宽度。默认设定下，宽度和高度只表示 &lt;code&gt;content area&lt;/code&gt; ，也就是 &lt;code&gt;content-box&lt;/code&gt;。而如果设置 &lt;code&gt;box-sizing&lt;/code&gt; 为 &lt;code&gt;border-box&lt;/code&gt; 的话，宽度和高度将包括 &lt;code&gt;padding&lt;/code&gt; 和 &lt;code&gt;border&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;盒的类型&lt;/h2&gt;
&lt;p&gt;上面已经介绍了盒的基本结构，想要了解视觉格式化模型的具体内容我们还需要了解盒子的类型。&lt;/p&gt;
&lt;p&gt;基本上我们的盒子可以分为两大类，一类是块级元素对应的盒子，一类是行内元素对应的盒子。但是在文档中将这两大类由进行了详细的说明，并引出了很多相似的概念，我们需要区分清楚。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;包含块 &lt;code&gt;containing block&lt;/code&gt;：一个元素生成的盒子是其后代盒子的包含块，通常情况下根元素所在的包含块就是初始包含块，他的大小取决于视口 &lt;code&gt;viewport&lt;/code&gt;（浏览器环境下就是浏览器窗口）。包含块在标准中的具体定义看下面的介绍。&lt;/li&gt;
&lt;li&gt;盒 &lt;code&gt;box&lt;/code&gt;：一个抽象的概念，由 &lt;code&gt;CSS&lt;/code&gt; 引擎根据文档中的内容所创建，主要用于文档元素的定位、布局和格式化等用途。盒子与元素并不是一一对应的，有时多个元素会合并生成一个盒子（列表元素），有时一个元素会生成多个盒子（如匿名盒子）。&lt;/li&gt;
&lt;li&gt;块级元素&lt;code&gt;block-level element&lt;/code&gt;：源文档中那些被格式化成视觉上的块的元素（例如，段落），元素的 &lt;code&gt;display&lt;/code&gt; 为 &lt;code&gt;block&lt;/code&gt;、&lt;code&gt;list-item&lt;/code&gt;、&lt;code&gt;table&lt;/code&gt; 时，该元素将成为块级元素。元素是否是块级元素仅是元素本身的属性，并不直接用于格式化上下文的创建或布局。&lt;/li&gt;
&lt;li&gt;块级盒 &lt;code&gt;block-level box&lt;/code&gt;：由块级元素生成，是参与块格式化上下文的盒。每个块级元素生成一个主块级盒（&lt;code&gt;principal block-level box&lt;/code&gt;），用来包含后代盒及生成的内容，并且任何定位方案都与该盒有关。有些块级元素可能会生成除主盒外的额外的盒，比如 `list-item&apos; 元素。这些额外的盒根据主盒来放置。&lt;/li&gt;
&lt;li&gt;块容器盒 &lt;code&gt;block container box&lt;/code&gt;：除了表格盒与替换元素外，一个块级盒也是块容器盒。一个块容器盒要么只包含块级盒，要么建立了行内格式化上下文并因此只包含行内盒。不是所有的块容器盒都是块级盒：非替换的行内块（比如 &lt;code&gt;inline-block&lt;/code&gt;）与非替换的表格单元是块级容器盒，但不是块级盒。&lt;/li&gt;
&lt;li&gt;块盒 &lt;code&gt;block box&lt;/code&gt;：作为块级容器的块级盒也叫块盒。&lt;/li&gt;
&lt;li&gt;块 block：块级盒（&lt;code&gt;block-level box&lt;/code&gt;），块容器盒（&lt;code&gt;block container box&lt;/code&gt;）和 块盒（&lt;code&gt;block box&lt;/code&gt;）这三个术语在没有歧义的时候就简称为“块（&lt;code&gt;block&lt;/code&gt;）”。&lt;/li&gt;
&lt;li&gt;行内级元素 &lt;code&gt;inline-level element&lt;/code&gt;：行内级元素是源文档中那些不会形成新内容块的元素，内容分布于多行（例如，强调段落中的一部分文本，行内图片等等）。&lt;code&gt;display&lt;/code&gt; 为 &lt;code&gt;inline&lt;/code&gt;、&lt;code&gt;inline-block&lt;/code&gt;、&lt;code&gt;inline-table&lt;/code&gt; 的元素称为行内级元素。与块级元素一样，元素是否是行内级元素仅是元素本身的属性，并不直接用于格式化上下文的创建或布局。&lt;/li&gt;
&lt;li&gt;行内级盒 &lt;code&gt;inline-level box&lt;/code&gt;：行内级元素生成行内级盒，即参与行内格式化上下文的盒。行内级盒子包括行内盒和原子行内级盒两种，区别在于该盒是否参与行内格式化上下文的创建。&lt;/li&gt;
&lt;li&gt;行内盒 &lt;code&gt;inline box&lt;/code&gt;：特殊的行内级盒，其内容参与包含行内格式化上下文创建，&lt;code&gt;display&lt;/code&gt; 值为 &lt;code&gt;inline&lt;/code&gt; 的非替换元素会生成一个行内盒。比如 &lt;code&gt;inline-block&lt;/code&gt; 虽然也是行内级盒，但是内部是块容器盒，对包含行内格式上下文来说它就像一个不透明的行内元素。与块盒类似，行内盒也分为具名行内盒和匿名行内盒（&lt;code&gt;anonymous inline box&lt;/code&gt;）两种。&lt;/li&gt;
&lt;li&gt;原子行内级盒 &lt;code&gt;atomic inline-level box&lt;/code&gt;：不创建行内格式化上下文的行内级盒子，（例如，行内级替换元素，&lt;code&gt;inline-block&lt;/code&gt; 元素和 &lt;code&gt;inline-table&lt;/code&gt; 元素），它们作为单一的不透明盒（&lt;code&gt;opaque box&lt;/code&gt;）参与包含行内格式化上下文。原子行内级盒子的内容不会拆分成多行显示。&lt;/li&gt;
&lt;li&gt;行盒（也有翻译为行框的） &lt;code&gt;line box&lt;/code&gt;：在行内格式化上下文中，盒是从包含块的顶部开始一个挨一个水平放置的。这些盒之间的水平外边距，边框和内边距都有效。盒可能会以不同的方式垂直对齐：以它们的底部或者顶部对齐，或者以它们里面的文本的基线对齐。包含来自同一行的盒的矩形区域叫做行盒。虽然标准中没有明确指出，但是行盒应该是块级的。行盒是我们无法看到也无法用语言接触到的一个概念上的盒。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一个盒子的类型不一定是唯一的，取决于你看盒子的角度，比如 &lt;code&gt;inline-block&lt;/code&gt; 从父元素的盒角度来看，它是一个原子行内级盒，但是相对于内部的元素他也是一个块容器盒。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;虽然概念看上去很多，但大部分都是不同的角度看同一个东西得出的不同概念，我们只需要对应起来理解。块级元素对应行内级元素，他们一般是由 &lt;code&gt;display&lt;/code&gt; 来决定的；块级盒和行内级盒，他们由对应的块级元素或行内及元素生成，参与对应各格式上下文；块容器盒没有对应的行内级概念（行内格式上下文中的行框概念可以类比），他的角度是包含，即当前盒和它的后代之间的关系，它虽然不参与内部的子元素的布局和定位，但是他是内部子元素的布局定位的基准，可以简单理解为边界。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们编写 &lt;code&gt;HTML&lt;/code&gt; 和 &lt;code&gt;CSS&lt;/code&gt; 都是以元素为单位，但是浏览器的工作过程却是把元素转换成一个一个的盒子，也就是手我们编写文档是以元素为单位，浏览器渲染文档是以盒为单位。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在标准中，块级元素块级盒和行内元素行内盒是并列的，但我认为块级元素块级盒才是排版的基本单元。所谓的行内元素行内盒都是在某个块内部的。一个块级盒内部要么全部是块级盒；要么建立了行内格式上下文，只有行内级盒。但其实行内格式上下文都是在行盒内 &lt;code&gt;line boxex&lt;/code&gt;，而行框其实是块级的，也就是说从根元素 &lt;code&gt;&amp;#x3C;html&gt;&lt;/code&gt; 开始，块级盒里面只有块级盒，我们所见到的全是行内级元素的块容器盒，其实内部也是一个个垂直排列的行盒，只不过行盒我们无法看到。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;CSS2.2&lt;/code&gt; 中没有明确说明行盒是块级的，我在 &lt;code&gt;google&lt;/code&gt; 中也没有找到相关的说明。唯一看到的资料就是 &lt;code&gt;winter&lt;/code&gt; 的文章，不过我认为这种逻辑是比较合理的，整个视觉格式化模型的结构在这种理解下变得非常有逻辑和清晰。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/block-inline-css22.DLF3eSCO_Z1krujv.webp&quot; alt=&quot;block-inline-css22&quot; title=&quot;block-inline-css22&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Some text 虽然没有被任何元素包裹，其实他显示包裹了一层匿名块盒，匿名块盒内部又包裹了一层匿名行盒 --&gt;
&amp;#x3C;div&gt;
  &amp;#x3C;div style=&quot;margin: 0 auto;border: 1px solid;width: 200px;font-size: 20px;&quot;&gt;
    Some text
    &amp;#x3C;p style=&quot;border: 2px solid red&quot;&gt;More text&amp;#x3C;/p&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some text&lt;/p&gt;
&lt;p&gt;More text&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;当一个行内盒包含流内块级元素（&lt;code&gt;in-flow&lt;/code&gt;，没有因浮动或定位而脱离文档流的块级元素）时，行内盒（以及属于同一个行框的行内祖先）会被破坏，分布到该块级元素（以及任何连续的或只被可合并的空白符和/或流外元素隔开的块级兄弟：多个块级元素的情况）周围。行内盒会被切成两个部分（这两个部分各有一边是空的，也就是只有三条边），分布于块级元素的两边。拆分前的行框和拆分后的行框都被包进匿名块盒，这些匿名块盒会作为分隔块级盒的兄弟。当这样一个行内盒受到相对定位的影响时，任何由此产生的位移也会影响行内盒里面的块级盒。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 该例中p为行内元素，其中包含了两个匿名的文本块和一个块级元素，
最终的结果就是最外层的div产生一个块盒，里面的两个文本块产生两个匿名块盒，作为span块级盒的兄弟。
并且我们可以看到被块级元素分隔开的两个文本块的边框是断开的，这部分细节我们在下一篇格式上下文里面讲 --&gt;
&amp;#x3C;div&gt;
  &amp;#x3C;p style=&quot;display: inline; border: 1px solid red&quot;&gt;
    This is anonymous text before the SPAN.
    &amp;#x3C;span style=&quot;display: block; border: 2px solid blue;&quot;&gt;This is the content of SPAN.&amp;#x3C;/span&gt;
    This is anonymous text after the SPAN.
  &amp;#x3C;/p&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is anonymous text before the SPAN.This is the content of SPAN.This is anonymous text after the SPAN.&lt;/p&gt;
&lt;h2&gt;包含块 containing block&lt;/h2&gt;
&lt;p&gt;元素（生成的）盒的位置和大小有时是根据一个特定矩形计算的，叫做该元素的包含块（&lt;code&gt;containing block&lt;/code&gt;）。元素包含块的定义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根元素所在的包含块是一个被称为初始包含块的矩形。对于连续媒体，尺寸取自视口的尺寸，并且被固定在画布开始的位置；对于分页媒体就是页区（&lt;code&gt;page area&lt;/code&gt;）。初始包含块的&apos;direction&apos;属性与根元素的相同&lt;/li&gt;
&lt;li&gt;对于其它元素，如果该元素的 &lt;code&gt;position&lt;/code&gt; 是 &lt;code&gt;relative&lt;/code&gt;或者 &lt;code&gt;static&lt;/code&gt;，包含块由其最近的块容器祖先盒的内容边界形成&lt;/li&gt;
&lt;li&gt;如果元素具有 &lt;code&gt;position: fixed&lt;/code&gt;，包含块由连续媒体的视口或者分页媒体的页区建立&lt;/li&gt;
&lt;li&gt;如果元素具有 &lt;code&gt;position: absolute&lt;/code&gt;，包含块由最近的 &lt;code&gt;position&lt;/code&gt; 为 &lt;code&gt;absolute&lt;/code&gt;，&lt;code&gt;relative&lt;/code&gt; 或者 &lt;code&gt;fixed&lt;/code&gt; 的祖先建立，按照如下方式：
&lt;ul&gt;
&lt;li&gt;如果该祖先是一个行内元素，包含块就是环绕着为该元素生成的第一个和最后一个行内盒的内边距框的边界框（&lt;code&gt;bounding box&lt;/code&gt;）。在 &lt;code&gt;CSS 2.2&lt;/code&gt; 中，如果该行内元素被跨行分割了，那么包含块是未定义的&lt;/li&gt;
&lt;li&gt;否则，包含块由该祖先的内边距边界形成&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;display 属性&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;display&lt;/code&gt; 属性确定元素的类型和生成的盒的类型，以及内部的元素应用何种布局。&lt;code&gt;display&lt;/code&gt; 属性会设置元素的内部和外部显示类型。外部类型确定了元素如何参与流 &lt;code&gt;flow&lt;/code&gt; 的布局（关于 &lt;code&gt;flow&lt;/code&gt; 的介绍在系列文章的&lt;a href=&quot;https://www.clloz.com/programming/front-end/css/2020/08/19/visual-formatting-model-2/&quot; title=&quot;第二篇&quot;&gt;第二篇&lt;/a&gt;），内部类型确定子元素的布局（这也就是我上面说的元素生成的盒不一定是唯一的，比如 &lt;code&gt;inline-block&lt;/code&gt;。有些 &lt;code&gt;display&lt;/code&gt; 属性的布局定义有一份完全单独的标准，比如 &lt;code&gt;flex&lt;/code&gt; 属性就有自己单独的标准 &lt;a href=&quot;https://www.w3.org/TR/css-flexbox-1/&quot; title=&quot;CSS Flexible Box Layout Module Level 1&quot;&gt;CSS Flexible Box Layout Module Level 1&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;display&lt;/code&gt; 的各种取值的含义如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;block&lt;/code&gt;：这个取值会让元素生成一个主块盒。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inline-block&lt;/code&gt;：这个取值会让元素生成一个行内块级容器 &lt;code&gt;inline-level block container&lt;/code&gt;。元素内部会被格式化为一个块盒；对于所在流，元素本身被格式化为一个原子行内级盒。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inline&lt;/code&gt;：这个取值会让元素生成一个或多个行内盒。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list-item&lt;/code&gt;：这个取值会让元素（比如 &lt;code&gt;li&lt;/code&gt; 元素）生成一个主块盒和一个标记盒（列表项前的标记）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;none&lt;/code&gt;：这个取值下元素不会出现在格式化结构 &lt;code&gt;formatting structure&lt;/code&gt; 中，对可视化媒体即不会生成盒也不会对布局有任何影响，对于最终的渲染结果，元素就好像不存在一样。元素的后代同样也不会生成盒，元素被完全移除格式化结构。后代元素的 &lt;code&gt;display&lt;/code&gt; 属性不会影响上述行为。取值为 &lt;code&gt;none&lt;/code&gt; 不会创建任何可见的盒；如果你想要元素在格式化结构中生成盒但是元素不可见，可以使用 &lt;code&gt;CSS&lt;/code&gt; 的另一个属性 &lt;code&gt;visibility&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table, inline-table, table-row-group, table-column, table-column-group, table-header-group, table-footer-group, table-row, table-cell, and table-caption&lt;/code&gt;：这些取值会让元素表现的像一个表格元素。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;除了定位 &lt;code&gt;positioning&lt;/code&gt;，浮动 &lt;code&gt;float&lt;/code&gt; 和根元素，其他元素的 &lt;code&gt;display&lt;/code&gt; 属性的计算值和指定值相同。关于例外的三者，我们会在下一篇文章进行讨论。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文主要是讲标准中的盒模型和盒的类型，盒是浏览器渲染文档的基本单元。我觉得这部分我们需要记住几点就行。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;盒的结构。&lt;/li&gt;
&lt;li&gt;文档被分成一个一个的块，根元素就是一个块盒。&lt;/li&gt;
&lt;li&gt;一个块盒的子盒（直接子元素，嵌套的后代不在此列）要么全是块盒，要么全是行内级盒（其实行内级盒也是包含在一个看不见的行框中，而行框是块级的）。&lt;/li&gt;
&lt;li&gt;块容器盒内的只要有一个块级元素，所有的行内元素都会被包裹上一层匿名块盒，为了保证 第&lt;code&gt;3&lt;/code&gt; 条中的内容。&lt;/li&gt;
&lt;li&gt;块容器盒中未被任何元素包裹的文本将被一个匿名行内盒包裹（匿名行内盒外层就是匿名行块盒）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/CSS22/box.html#margin-properties&quot; title=&quot;CSS2.2 - Box model&quot;&gt;CSS2.2 - Box model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-box-3/&quot; title=&quot;CSS Box Model Module Level 3&quot;&gt;CSS Box Model Module Level 3&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/css-spec.DPJ7VnGq.png"/><enclosure url="/_astro/css-spec.DPJ7VnGq.png"/></item><item><title>ES6 迭代器 Iterator</title><link>https://clloz.com/blog/es6-iterator</link><guid isPermaLink="true">https://clloz.com/blog/es6-iterator</guid><pubDate>Tue, 18 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;ES6&lt;/code&gt; 中我们可以用 &lt;code&gt;for ... of&lt;/code&gt; 对很多对象进行遍历操作，包括数组，&lt;code&gt;Map&lt;/code&gt;，&lt;code&gt;Set&lt;/code&gt;，甚至是类数组对象和字符串都可以。之所以能够进行这样的操作是因为 &lt;code&gt;ES6&lt;/code&gt; 引入了迭代器 &lt;code&gt;Iterator&lt;/code&gt; 的机制，来为不同的数据结构提供一种统一的访问机制。本文就来讨论一下迭代器的机制。&lt;/p&gt;
&lt;h2&gt;概念&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 之前，我们遍历数组一般是使用 &lt;code&gt;for&lt;/code&gt; 或者 &lt;code&gt;map&lt;/code&gt;，&lt;code&gt;forEach&lt;/code&gt; 等这样的 &lt;code&gt;API&lt;/code&gt;。&lt;code&gt;for&lt;/code&gt; 循环使用太麻烦，我们有时仅仅是需要数组中元素的值，但是我们需要提前知道数组的长度，并且声明一个索引变量，当出现嵌套的循环的时候，代码更复杂。而 &lt;code&gt;map&lt;/code&gt; 和 &lt;code&gt;forEach&lt;/code&gt; 等 &lt;code&gt;API&lt;/code&gt; 则是 &lt;code&gt;Array&lt;/code&gt; 对象特有的，使用起来也不够方便，比如无法中途跳出 &lt;code&gt;forEach&lt;/code&gt; 循环， &lt;code&gt;break&lt;/code&gt; 命令或 &lt;code&gt;return&lt;/code&gt; 命令都不能奏效。。&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;ES6&lt;/code&gt; 引入了迭代器和 &lt;code&gt;for ... of&lt;/code&gt; 来统一和简化我们对对象的遍历。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;要对对象进行遍历，首先要确定对象是否可遍历，以及如何进行遍历。&lt;code&gt;ES6&lt;/code&gt; 就有两个协议：&lt;strong&gt;可迭代协议&lt;/strong&gt; 和 &lt;strong&gt;迭代器协议&lt;/strong&gt;。前者用来确定一个对象是可遍历的，后者告诉 &lt;code&gt;for ... of&lt;/code&gt; 遍历对象的规则。&lt;/p&gt;
&lt;p&gt;要确定一个对象是不是可迭代的，只要看看它有没有实现 &lt;code&gt;Symbol.iterator&lt;/code&gt; 方法。&lt;code&gt;Symbol.iterator&lt;/code&gt; 是一个内置 &lt;code&gt;Symbol&lt;/code&gt;，它指向一个方法（即它是一个方法名），是专门供 &lt;code&gt;for ... of&lt;/code&gt; 遍历使用的，当用 &lt;code&gt;for ... of&lt;/code&gt; 对一个对象进行遍历的时候，就会首先寻找这个对象的 &lt;code&gt;Symbol.iterator&lt;/code&gt; 方法。一个对象可遍历，就表示它自身或者其原型链上的某个对象实现了 &lt;code&gt;Symbol.iterator&lt;/code&gt; 方法，这个方法是一个无参数的方法，其返回值为一个符合迭代器协议的对象。。这一些规则就是所谓的 &lt;strong&gt;可迭代协议&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Symbol.iterator&lt;/code&gt; 方法并不是没有要求的，这个方法必须要返回一个对象，这个对象必须实现了一个 &lt;code&gt;next&lt;/code&gt; 方法。&lt;code&gt;next()&lt;/code&gt; 方法必须返回一个对象，该对象应当有两个属性： &lt;code&gt;done&lt;/code&gt; 和 &lt;code&gt;value&lt;/code&gt;，如果返回了一个非对象值（比如 &lt;code&gt;false&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt;），则会抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 异常（&lt;code&gt;iterator.next() returned a non-object value&lt;/code&gt;）。这里的 &lt;code&gt;done&lt;/code&gt; 是一个布尔值，表示迭代器是否还有下一个值。当迭代器还有下一个值，&lt;code&gt;done&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt;，此时可省略；当迭代器已经遍历完毕，则此时 &lt;code&gt;done&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;。&lt;code&gt;value&lt;/code&gt; 则表示迭代器的返回值，可以是任意 &lt;code&gt;javascript&lt;/code&gt; 值，当 &lt;code&gt;done&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 时刻省略。这就是 &lt;strong&gt;迭代器协议&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当一个对象满足上面两个协议的时候，就表示这个对象是可迭代对象，可以用 &lt;code&gt;for ... of&lt;/code&gt; 对它进行遍历。当你将一个可迭代对象放入 &lt;code&gt;for ... of&lt;/code&gt; 进行遍历的时候，会先调用这个对象的 &lt;code&gt;Symbol.iterator&lt;/code&gt; 方法，然后用返回对象中的 &lt;code&gt;next&lt;/code&gt; 方法不断对对象进行迭代，直到 &lt;code&gt;done&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;一个可迭代对象的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let obj = {
  num: 0,
  [Symbol.iterator]() {
    let num = this.num //在 Symbol.iterator 中我们可以用 this 访问可迭代对象的属性，传递给 next，最后由 next 方法返回出去
    return {
      next() {
        if (num &amp;#x3C; 10) {
          return {
            value: num++,
            done: false
          }
        } else {
          return {
            value: undefined,
            done: true
          }
        }
      }
    }
  }
}

for (let c of obj) {
  console.log(c)
}
//输出：0 1 2 3 4 5 6 7 8 9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到只要满足了可迭代协议和迭代协议，任何对象都能够用 &lt;code&gt;for ... of&lt;/code&gt; 进行遍历，并且遍历行为是我们自定义的。其实 &lt;code&gt;for ... of&lt;/code&gt; 的实现也很简单，就是一个 &lt;code&gt;while&lt;/code&gt; 循环：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function forOf(obj, cb) {
  if (!obj[Symbol.iterator]) throw new TypeError(typeof obj + &apos; is not iterable&apos;)
  if (typeof obj[Symbol.iterator] !== &apos;function&apos;)
    throw new TypeError(&apos;Result of the Symbol.iterator method is not an object&apos;)
  if (typeof cb !== &apos;function&apos;) throw new TypeError(&apos;cb must be callable&apos;)

  let iterator = obj[Symbol.iterator]()
  let result = iterator.next()
  while (!result.done) {
    cb(result.value)
    result = iterator.next()
  }
}

forOf(obj, function (val) {
  console.log(val)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样将 &lt;code&gt;next&lt;/code&gt; 写在 &lt;code&gt;Symbol.iterator&lt;/code&gt; 方法的返回对象中我们没法直接用 &lt;code&gt;this&lt;/code&gt; 访问 &lt;code&gt;obj&lt;/code&gt; 的属性所以我们可以改成如下形式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let obj = {
  num: 0,
  [Symbol.iterator]() {
    return this
  },
  next() {
    if (this.num &amp;#x3C; 10) {
      return {
        value: this.num++,
        done: false
      }
    } else {
      return {
        value: undefined,
        done: true
      }
    }
  }
}

for (let c of obj) {
  console.log(c)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从这个修改我们可以看出来，可迭代对象也可以是一个迭代器，这在后面还会应用到，生成器对象 &lt;code&gt;Generator&lt;/code&gt; 其实是这种实现。&lt;/p&gt;
&lt;p&gt;其实不止 &lt;code&gt;for ... of&lt;/code&gt;，还有很多其他操作也是调用的迭代器进行遍历，包括对数组和 &lt;code&gt;Set&lt;/code&gt; 的解构赋值，扩展运算符，&lt;code&gt;yield*&lt;/code&gt;，以及任何接受数组作为参数的场景。而且很多内置的对象默认就是可迭代的，目前所有的内置可迭代对象如下：&lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Array&lt;/code&gt;、&lt;code&gt;TypedArray&lt;/code&gt;、&lt;code&gt;Map&lt;/code&gt; 和 &lt;code&gt;Set&lt;/code&gt;，它们的原型对象都实现了 &lt;code&gt;@@iterator&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(typeof &apos;clloz&apos;[Symbol.iterator]) //function

let iterator = &apos;clloz&apos;[Symbol.iterator]()

console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

// { value: &apos;c&apos;, done: false }
// { value: &apos;l&apos;, done: false }
// { value: &apos;l&apos;, done: false }
// { value: &apos;o&apos;, done: false }
// { value: &apos;z&apos;, done: false }
// { value: undefined, done: true }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;很多 &lt;code&gt;API&lt;/code&gt; 可以接受一个可迭代对象作为参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new Map([iterable])&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new WeakMap([iterable])&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new Set([iterable])&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new WeakSet([iterable])&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Promise.all(iterable)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Promise.race(iterable)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.from(iterable)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;内建迭代器&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;for ... of&lt;/code&gt; 调用的迭代器只能返回键值，但有时候我们希望能够返回键名，或者两者都需求。针对这种情况，&lt;code&gt;ES6&lt;/code&gt; 为数组，&lt;code&gt;Map&lt;/code&gt; 和 &lt;code&gt;Set&lt;/code&gt; （其实 &lt;code&gt;Object&lt;/code&gt; 也有这三个方法，不过不是返回迭代器）提供了三种不同的方法返回不同的迭代器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;entries()&lt;/code&gt; 返回一个遍历器对象，用来遍历 &lt;code&gt;[键名, 键值]&lt;/code&gt; 组成的数组。对于数组，键名就是索引值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;keys()&lt;/code&gt; 返回一个遍历器对象，用来遍历所有的键名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;values()&lt;/code&gt; 返回一个遍历器对象，用来遍历所有的键值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于数组，&lt;code&gt;for ... of&lt;/code&gt; 依次返回数组的元素值，&lt;code&gt;keys()&lt;/code&gt; 返回每个元素的索引，&lt;code&gt;values()&lt;/code&gt; 返回每个元素的值，&lt;code&gt;entries()&lt;/code&gt; 返回每个元素所以和值组成的数组。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;Map&lt;/code&gt;，&lt;code&gt;for ... of&lt;/code&gt; 按插入顺序返回每个键值对组成的数组，&lt;code&gt;keys()&lt;/code&gt; 按插入顺序返回每个键名，&lt;code&gt;values()&lt;/code&gt; 按插入顺序返回每个键值，&lt;code&gt;entries()&lt;/code&gt; 和 &lt;code&gt;for ... of&lt;/code&gt; 相同。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let m = new Map()
m.set(1, &apos;a&apos;)
m.set(2, &apos;b&apos;)
m.set(3, &apos;c&apos;)
m.set(4, &apos;d&apos;)

for (let c of m) {
  console.log(c)
}
// [ 1, &apos;a&apos; ]
// [ 2, &apos;b&apos; ]
// [ 3, &apos;c&apos; ]
// [ 4, &apos;d&apos; ]
for (let c of m.keys()) {
  console.log(c)
}
//1 2 3 4
for (let c of m.values()) {
  console.log(c)
}
//a b c d
for (let c of m.entries()) {
  console.log(c)
}
// [ 1, &apos;a&apos; ]
// [ 2, &apos;b&apos; ]
// [ 3, &apos;c&apos; ]
// [ 4, &apos;d&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 &lt;code&gt;Set&lt;/code&gt;，&lt;code&gt;for ... of&lt;/code&gt;，&lt;code&gt;keys()&lt;/code&gt; 和 &lt;code&gt;values()&lt;/code&gt; 返回相同，说明 &lt;code&gt;Set&lt;/code&gt; 结构的键名和键值是相同的， &lt;code&gt;entries()&lt;/code&gt; 则和 &lt;code&gt;Map&lt;/code&gt; 一样返回键名和键值组成的数组，只不过键名和键值相同。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let s = new Set()
s.add(&apos;a&apos;)
s.add(&apos;b&apos;)
s.add(&apos;c&apos;)

for (let c of s) {
  console.log(c)
}
// a b c
for (let c of s.keys()) {
  console.log(c)
}
// a b c
for (let c of s.values()) {
  console.log(c)
}
//a b c
for (let c of s.entries()) {
  console.log(c)
}
// [ &apos;a&apos;, &apos;a&apos; ]
// [ &apos;b&apos;, &apos;b&apos; ]
// [ &apos;c&apos;, &apos;c&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实我们可以看出，对于 &lt;code&gt;Set&lt;/code&gt; 来说，默认调用的是 &lt;code&gt;values()&lt;/code&gt; 来获得迭代器，而 &lt;code&gt;Map&lt;/code&gt; 默认是调用 &lt;code&gt;entries()&lt;/code&gt; 来获得默认迭代器。&lt;/p&gt;
&lt;p&gt;这里一定要注意数组，&lt;code&gt;Map&lt;/code&gt; 和 &lt;code&gt;Set&lt;/code&gt; 的 &lt;code&gt;keys&lt;/code&gt;，&lt;code&gt;values&lt;/code&gt; 和 &lt;code&gt;entries&lt;/code&gt; 方法返回的是一个迭代器，而 &lt;code&gt;Object&lt;/code&gt; 的对应方法返回的是一个数组。我们可以利用这些方法返回的迭代器来设置对象的 &lt;code&gt;Symbol.iterator&lt;/code&gt; 方法让对象变为可迭代对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let obj = {
  nicknames: [&apos;Jack&apos;, &apos;Jake&apos;, &apos;J-Dog&apos;],
  [Symbol.iterator]() {
    console.log(Array.isArray(this.nicknames.entries()))
    return this.nicknames.entries()
  }
}

for (let c of obj) {
  console.log(c)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;p&gt;迭代器的知识其实并不多也不复杂，上面的两个协议掌握即可，下面我来说一说一些应用。&lt;/p&gt;
&lt;h2&gt;实现链表&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function List(value) {
  this.value = value
  this.next = null
}

List.prototype[Symbol.iterator] = function () {
  let current = this
  return {
    next() {
      if (current.next) {
        let value = current.value
        current = current.next
        return {
          value: value,
          done: false
        }
      } else {
        return {
          value: current.value,
          done: true
        }
      }
    }
  }
}
let a = new List(&apos;a&apos;)
let b = new List(&apos;b&apos;)
let c = new List(&apos;c&apos;)

a.next = b
b.next = c

for (let val of a) {
  console.log(val)
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>Linux常用命令</title><link>https://clloz.com/blog/linux-command</link><guid isPermaLink="true">https://clloz.com/blog/linux-command</guid><pubDate>Tue, 18 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;整理 &lt;code&gt;Linux&lt;/code&gt; 中的常用命令，方便查看使用。提供一个查询命令的网站&lt;a href=&quot;https://wangchujiang.com/linux-command/&quot; title=&quot;linux-command&quot;&gt;linux-command&lt;/a&gt;。也可以用 &lt;code&gt;man&lt;/code&gt; 命令查看对应命令的说明。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文主要介绍 &lt;code&gt;Linux&lt;/code&gt; 的常用命令，对 &lt;code&gt;shell&lt;/code&gt; 的介绍和特殊符号的含义请查看另一篇文章&lt;a href=&quot;https://www.clloz.com/programming/backend/server/2019/11/01/shell-script-syntax/&quot; title=&quot;Shell脚本语法和常用命令&quot;&gt;Shell脚本语法和常用命令&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;命令&lt;/h2&gt;
&lt;h2&gt;man 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;man&lt;/code&gt; 命令 是 &lt;code&gt;Linux&lt;/code&gt; 下的帮助指令 (&lt;code&gt;manual)&lt;/code&gt;，通过 &lt;code&gt;man&lt;/code&gt; 指令可以查看 &lt;code&gt;Linux&lt;/code&gt; 中的指令帮助、配置文件帮助和编程帮助等信息。 &lt;code&gt;man(选项)(参数)&lt;/code&gt; 参数可以是数字或者关键字，数字是指定从哪本man手册中搜索帮助，关键字是指定要搜索帮助的关键字。数字有如下几种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户在 &lt;code&gt;shell&lt;/code&gt; 环境可操作的命令或执行文件；&lt;/li&gt;
&lt;li&gt;系统内核可调用的函数与工具等&lt;/li&gt;
&lt;li&gt;一些常用的函数(&lt;code&gt;function&lt;/code&gt;)与函数库(&lt;code&gt;library&lt;/code&gt;)，大部分为C的函数库(libc)&lt;/li&gt;
&lt;li&gt;设备文件说明，通常在/dev下的文件&lt;/li&gt;
&lt;li&gt;配置文件或某些文件格式&lt;/li&gt;
&lt;li&gt;游戏(games)&lt;/li&gt;
&lt;li&gt;惯例与协议等，如Linux文件系统，网络协议，ASCII code等说明&lt;/li&gt;
&lt;li&gt;系统管理员可用的管理命令&lt;/li&gt;
&lt;li&gt;跟kernel有关的文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
man -a #在所有的man帮助手册中搜索；
man -f # 等价于whatis指令，显示给定关键字的简短描述信息；
man -P # 指定内容时使用分页程序；
man -M # 指定man手册搜索的路径。
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;我们输入 &lt;code&gt;man ls&lt;/code&gt;，它会在最左上角显示 &lt;code&gt;LS（1）&lt;/code&gt;，在这里，&lt;code&gt;LS&lt;/code&gt; 表示手册名称，而 &lt;code&gt;（1）&lt;/code&gt; 表示该手册位于第一节章，同样，我们输 &lt;code&gt;man ifconfig&lt;/code&gt; 它会在最左上角显示 &lt;code&gt;IFCONFIG（8）&lt;/code&gt;。也可以这样输入命令：&lt;code&gt;man [章节号] 手册名称&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;ls 命令&lt;/h2&gt;
&lt;p&gt;就是 &lt;code&gt;list&lt;/code&gt; 的缩写，通过 &lt;code&gt;ls&lt;/code&gt; 命令不仅可以查看 &lt;code&gt;linux&lt;/code&gt; 文件夹包含的文件，而且可以查看文件权限(包括目录、文件夹、文件权限)查看目录信息等等。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
ls -a # 列出目录所有文件，包含以.开始的隐藏文件
ls -A # 列出除.及..的其它文件
ls -r # 反序排列
ls -t # 以文件修改时间排序
ls -S #以文件大小排序
ls -h #以易读大小显示
ls -l #除了文件名之外，还将文件的权限、所有者、文件大小等信息详细列出来
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;cd 命令&lt;/h2&gt;
&lt;p&gt;切换当前目录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
cd / # 进入根目录
cd ~ # 进入 &quot;home&quot; 目录
cd - # 进入上一次工作路径
cd !$ # 把上个命令的参数作为cd参数使用。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pwd 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pwd&lt;/code&gt; 命令用于查看当前工作目录路径。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
pwd # 查看当前路径
pwd -P # 查看软连接的实际路径

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;mkdir 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mkdir&lt;/code&gt; 命令用于创建文件夹。参数为目录，指定要创建的目录列表，多个目录之间用空格隔开。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
mkdir -Z # 设置安全上下文，当使用SELinux时有效；
mkdir -m || mkdir --mode # 建立目录的同时设置目录的权限；
mkdir -p || mkdir --parents # 若所要建立目录的上层目录目前尚未建立，则会一并建立上层目录；
mkdir --version # 显示版本信息。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;rm 命令&lt;/h2&gt;
&lt;p&gt;删除一个目录中的一个或多个文件或目录，如果没有使用 &lt;code&gt;-r&lt;/code&gt; 选项，则 &lt;code&gt;rm&lt;/code&gt; 不会删除目录。如果使用 &lt;code&gt;rm&lt;/code&gt; 来删除文件，通常仍可以将该文件恢复原状。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
rm -d # 直接把欲删除的目录的硬连接数据删除成0，删除该目录；
rm -f # 强制删除文件或目录；
rm -i # 删除已有文件或目录之前先询问用户；
rm -r || rm -R # 递归处理，将指定目录下的所有文件与子目录一并处理；
rm --preserve-root #不对根目录进行递归操作；
rm -v # 显示指令的详细执行过程。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;rmdir 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;rmdir dirname&lt;/code&gt;用来删除空目录。当目录不再被使用时，或者磁盘空间已到达使用限定值，就需要删除失去使用价值的目录。利用 &lt;code&gt;rmdir&lt;/code&gt; 命令可以从一个目录中删除一个或多个空的子目录。该命令从一个目录中删除一个或多个子目录，其中 &lt;code&gt;dirname&lt;/code&gt; 表示目录名。如果 &lt;code&gt;dirname&lt;/code&gt; 中没有指定路径，则删除当前目录下由 &lt;code&gt;dirname&lt;/code&gt; 指定的目录；如 &lt;code&gt;dirname&lt;/code&gt; 中包含路径，则删除指定位置的目录。删除目录时，必须具有对其父目录的写权限。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;子目录被删除之前应该是空目录。就是说，该目录中的所有文件必须用 &lt;code&gt;rm&lt;/code&gt; 命令全部删除，另外，当前工作目录必须在被删除目录之上，不能是被删除目录本身，也不能是被删除目录的子目录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
rmdir -p || rmdir --parents # 删除指定目录后，若该目录的上层目录已变成空目录，则将其一并删除；
rmdir --ignore-fail-on-non-empty # 此选项使rmdir命令忽略由于删除非空目录时导致的错误信息；
rmdir -v || rmdir -verboes # 显示命令的详细执行过程；
rmdir --help # 显示命令的帮助信息；
rmdir --version # 显示命令的版本信息。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;mv 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mv source target&lt;/code&gt;用来对文件或目录重新命名，或者将文件从一个目录移到另一个目录中。&lt;code&gt;source&lt;/code&gt; 表示源文件或目录(源文件可以是多个文件，用空格分开，也可以使用通配符 &lt;code&gt;mv * ../&lt;/code&gt;)，&lt;code&gt;target&lt;/code&gt; 表示目标文件或目录。如果将一个文件移到一个已经存在的目标文件中，则目标文件的内容将被覆盖。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mv&lt;/code&gt; 命令可以用来将源文件移至一个目标文件中，或将一组文件移至一个目标目录中。源文件被移至目标文件有两种不同的结果：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果目标文件是到某一目录文件的路径，源文件会被移到此目录下，且文件名不变。&lt;/li&gt;
&lt;li&gt;如果目标文件不是目录文件，则源文件名(只能有一个)会变为此目标文件名，并覆盖己存在的同名文件。如果源文件和目标文件在同一个目录下，mv的作用就是改文件名。当目标文件是目录文件时，源文件或目录参数可以有多个，则所有的源文件都会被移至目标文件中。所有移到该目录下的文件都将保留以前的文件名。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;注意事项：&lt;code&gt;mv&lt;/code&gt; 与 &lt;code&gt;cp&lt;/code&gt; 的结果不同，&lt;code&gt;mv&lt;/code&gt; 好像文件“搬家”，文件个数并未增加。而 &lt;code&gt;cp&lt;/code&gt; 对文件进行复制，文件个数增加了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
mv --backup=&amp;#x3C;备份模式&gt; # 若需覆盖文件，则覆盖前先行备份；
mv -b # 当文件存在时，覆盖前，为其创建一个备份；
mv -f # 若目标文件或目录与现有的文件或目录重复，则直接覆盖现有的文件或目录；
mv -i # 交互式操作，覆盖前先行询问用户，如果源文件与目标文件或目标目录中的文件同名，则询问用户是否覆盖目标文件。用户输入”y”，表示将覆盖目标文件；输入”n”，表示取消对源文件的移动。这样可以避免误将文件覆盖。
mv --strip-trailing-slashes # 删除源文件中的斜杠“/”；
mv -S&amp;#x3C;后缀&gt; # 为备份文件指定后缀，而不使用默认的后缀；
mv --target-directory=&amp;#x3C;目录&gt; # 指定源文件要移动到目标目录；
mv -u # 当源文件比目标文件新或者目标文件不存在时，才执行移动操作。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;cp 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cp source target&lt;/code&gt; 用来将一个或多个源文件或者目录复制到指定的目的文件或目录。它可以将单个源文件复制成一个指定文件名的具体的文件或一个已经存在的目录下。&lt;code&gt;cp&lt;/code&gt; 命令还支持同时复制多个文件，当一次复制多个文件时，目标文件参数必须是一个已经存在的目录，否则将出现错误。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
cp -a #此参数的效果和同时指定&quot;-dpR&quot;参数相同；
cp -d：当复制符号连接时，把目标文件或目录也建立为符号连接，并指向与源文件或目录连接的原始文件或目录；
cp -f # 强行复制文件或目录，不论目标文件或目录是否已存在；
cp -i # 覆盖既有文件之前先询问用户；
cp -l # 对源文件建立硬连接，而非复制文件；
cp -p # 保留源文件或目录的属性；
cp -R || cp r # 递归处理，将指定目录下的所有文件与子目录一并处理；
cp -s # 对源文件建立符号连接，而非复制文件；
cp -u # 使用这项参数后只会在源文件的更改时间较目标文件更新时或是名称相互对应的目标文件并不存在时，才复制文件；
cp -S # 在备份文件时，用指定的后缀“SUFFIX”代替文件的默认后缀；
cp -b # 覆盖已存在的文件目标前将目标文件备份；
cp -v # 详细显示命令执行的操作。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;cat 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cat&lt;/code&gt; 连接多个文件并打印到标准输出。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显示文件内容，如果没有文件或文件为-则读取标准输入。&lt;/li&gt;
&lt;li&gt;将多个文件的内容进行连接并打印到标准输出。&lt;/li&gt;
&lt;li&gt;显示文件内容中的不可见字符(控制字符、换行符、制表符等)。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
cat -A # --show-all 等价于&quot;-vET&quot;组合选项。
cat -b, # --number-nonblank 只对非空行编号，从1开始编号，覆盖&quot;-n&quot;选项。
cat -e  # 等价于&quot;-vE&quot;组合选项。
cat -E # --show-ends 在每行的结尾显示&apos;$&apos;字符。
cat -n # --number 对所有行编号，从1开始编号。
cat -s # --squeeze-blank 压缩连续的空行到一行。
cat -t # 等价于&quot;-vT&quot;组合选项。
cat -T # --show-tabs 使用&quot;^I&quot;表示TAB（制表符）。
cat -u  #  POSIX兼容性选项，无意义。
cat -v # --show-nonprinting   使用&quot;^&quot;和&quot;M-&quot;符号显示控制字符，除了LFD（line feed，即换行符&apos;\n&apos;）和TAB（制表符）。
cat --help                   显示帮助信息并退出。
cat --version                显示版本信息并退出。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;more 命令&lt;/h2&gt;
&lt;p&gt;是一个基于 &lt;code&gt;vi&lt;/code&gt; 编辑器文本过滤器，它以全屏幕的方式按页显示文本文件的内容，支持 &lt;code&gt;vi&lt;/code&gt; 中的关键字定位操作。&lt;code&gt;more&lt;/code&gt; 名单中内置了若干快捷键，常用的有 &lt;code&gt;H&lt;/code&gt;（获得帮助信息），&lt;code&gt;Enter&lt;/code&gt;（向下翻滚一行），空格（向下滚动一屏），&lt;code&gt;Q&lt;/code&gt;（退出命令）。语法 &lt;code&gt;more(选项)(参数)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;该命令一次显示一屏文本，满屏后停下来，并且在屏幕的底部出现一个提示信息，给出至今己显示的该文件的百分比：&lt;code&gt;--More--（XX%&lt;/code&gt;）可以用下列不同的方法对提示做出回答：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按 &lt;code&gt;Space&lt;/code&gt; 键：显示文本的下一屏内容。&lt;/li&gt;
&lt;li&gt;按 &lt;code&gt;Enter&lt;/code&gt; 键：只显示文本的下一行内容。&lt;/li&gt;
&lt;li&gt;按斜线符&lt;code&gt;|&lt;/code&gt;：接着输入一个模式，可以在文本中寻找下一个相匹配的模式。&lt;/li&gt;
&lt;li&gt;按 &lt;code&gt;H&lt;/code&gt; 键：显示帮助屏，该屏上有相关的帮助信息。&lt;/li&gt;
&lt;li&gt;按 &lt;code&gt;B&lt;/code&gt; 键：显示上一屏内容。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+F&lt;/code&gt;: ：向下滚动一屏&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+B&lt;/code&gt;：返回上一屏&lt;/li&gt;
&lt;li&gt;&lt;code&gt;=&lt;/code&gt;：输出当前行的行号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:f&lt;/code&gt;：输出文件名和当前行的行号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;V&lt;/code&gt;：调用vi编辑器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!命令&lt;/code&gt;：调用 &lt;code&gt;Shell&lt;/code&gt;，并执行命令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q&lt;/code&gt;：退出 &lt;code&gt;more&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
more -&amp;#x3C;数字&gt; # 指定每屏显示的行数；
more -d # 显示“[press space to continue,&apos;q&apos; to quit.]”和“[Press &apos;h&apos; for instructions]”；
more -c # 不进行滚屏操作。每次刷新这个屏幕；
more -s # 将多个空行压缩成一行显示；
more -u # 禁止下划线；
more &amp;#x3C;数字&gt; # 从指定数字的行开始显示。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;less 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;less&lt;/code&gt; 命令 的作用与 &lt;code&gt;more&lt;/code&gt; 十分相似，都可以用来浏览文字档案的内容，不同的是 &lt;code&gt;less&lt;/code&gt; 命令允许用户向前或向后浏览文件，而 &lt;code&gt;more&lt;/code&gt; 命令只能向前浏览，而且 less 在查看之前不会加载整个文件。用 &lt;code&gt;less&lt;/code&gt; 命令显示文件时，用 &lt;code&gt;PageUp&lt;/code&gt; 键向上翻页，用 &lt;code&gt;PageDown&lt;/code&gt; 键向下翻页。要退出 &lt;code&gt;less&lt;/code&gt; 程序，应按 &lt;code&gt;Q&lt;/code&gt; 键。操作和选项如下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; 字符串：向下搜索“字符串”的功能&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt; 字符串：向上搜索“字符串”的功能&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;：重复前一个搜索（与 / 或 ? 有关）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;N&lt;/code&gt;：反向重复前一个搜索（与 / 或 ? 有关）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-x&lt;/code&gt; &amp;#x3C;数字&gt; 将“tab”键显示为规定的数字空格&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b&lt;/code&gt; 向后翻一页&lt;/li&gt;
&lt;li&gt;&lt;code&gt;d&lt;/code&gt; 向后翻半页&lt;/li&gt;
&lt;li&gt;&lt;code&gt;h&lt;/code&gt; 显示帮助界面&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Q&lt;/code&gt; 退出less 命令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u&lt;/code&gt; 向前滚动半页&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt; 向前滚动一行&lt;/li&gt;
&lt;li&gt;空格键 滚动一行&lt;/li&gt;
&lt;li&gt;回车键 滚动一页&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[pagedown]&lt;/code&gt;： 向下翻动一页&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[pageup]&lt;/code&gt;： 向上翻动一页&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;语法 &lt;code&gt;less [选项] [参数]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
less -e # 文件内容显示完毕后，自动退出；
less -f # 强制显示文件；
less -g # 不加亮显示搜索到的所有关键词，仅显示当前显示的关键字，以提高显示速度；
less -l # 搜索时忽略大小写的差异；
less -N # 每一行行首显示行号；
less -s # 将连续多个空行压缩成一行显示；
less -S # 在单行显示较长的内容，而不换行显示；
less -x&amp;#x3C;数字&gt; # 将TAB字符显示为指定个数的空格字符。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;head 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;head&lt;/code&gt; 用来显示档案的开头至标准输出中，默认 &lt;code&gt;head&lt;/code&gt; 命令打印其相应文件的开头 &lt;code&gt;10&lt;/code&gt; 行。处理多个文件时会在各个文件之前附加含有文件名的行。当没有文件或文件为 &lt;code&gt;-&lt;/code&gt; 时，读取标准输入。&lt;/p&gt;
&lt;p&gt;语法 &lt;code&gt;head [选项] [参数]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
head -n&amp;#x3C;行数&gt; # 显示的行数（行数为负数表示从最后向前数）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;tail 命令&lt;/h2&gt;
&lt;p&gt;用于显示指定文件末尾内容，不指定文件时，作为输入信息进行处理。常用查看日志文件。&lt;/p&gt;
&lt;p&gt;语法 &lt;code&gt;tail [选项] [参数]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
tail -f # 循环读取（常用于查看递增的日志文件）
tail -n&amp;#x3C;行数&gt; # 显示行数（从后向前）
tail -c&amp;#x3C;N&gt; #——bytes=&amp;#x3C;N&gt;：输出文件尾部的N（N为整数）个字节内容；
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;env 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;env&lt;/code&gt; 命令用于显示系统中已存在的环境变量，以及在定义的环境中执行指令。该命令只使用 &lt;code&gt;-&lt;/code&gt; 作为参数选项时，隐藏了选项 &lt;code&gt;-i&lt;/code&gt; 的功能。若没有设置任何选项和参数时，则直接显示当前的环境变量。&lt;/p&gt;
&lt;p&gt;如果使用env命令在新环境中执行指令时，会因为没有定义环境变量&quot;PATH&quot;而提示错误信息 &lt;code&gt;such file or directory&lt;/code&gt;。此时，用户可以重新定义一个新的&quot;PATH&quot;或者使用绝对路径。&lt;/p&gt;
&lt;p&gt;语法 &lt;code&gt;env [选项] [参数]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
env -i # 开始一个新的空的环境；
env -u&amp;#x3C;变量名&gt; # 从当前环境中删除指定的变量。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数可以是定义在新的环境中变量，定义多个变量定义用空格隔开。格式为&lt;code&gt;变量名=值&lt;/code&gt;；也可以指定要执行的指令和参数。&lt;/p&gt;
&lt;h2&gt;echo 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;echo&lt;/code&gt; 命令用于在 &lt;code&gt;shell&lt;/code&gt; 中打印 &lt;code&gt;shell&lt;/code&gt; 变量的值，或者直接输出指定的字符串。&lt;code&gt;linux&lt;/code&gt; 的 &lt;code&gt;echo&lt;/code&gt; 命令，在 &lt;code&gt;shell&lt;/code&gt; 编程中极为常用, 在终端下打印变量 &lt;code&gt;value&lt;/code&gt; 的时候也是常常用到的，因此有必要了解下 &lt;code&gt;echo&lt;/code&gt; 的用法 &lt;code&gt;echo&lt;/code&gt; 命令的功能是在显示器上显示一段文字，一般起到一个提示的作用。&lt;/p&gt;
&lt;p&gt;语法 &lt;code&gt;echo [选项] [参数]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
echo -e：激活转义字符。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;-e&lt;/code&gt; 选项时，若字符串中出现以下字符，则特别加以处理，而不会将它当成一般文字输出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;\a&lt;/code&gt; 发出警告声；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\b&lt;/code&gt; 删除前一个字符；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\c&lt;/code&gt; 不产生进一步输出 (\c 后面的字符不会输出)；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\f&lt;/code&gt; 换行但光标仍旧停留在原来的位置；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\n&lt;/code&gt; 换行且光标移至行首；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\r&lt;/code&gt; 光标移至行首，但不换行；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\t&lt;/code&gt; 插入 &lt;code&gt;tab&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\v&lt;/code&gt; 与 &lt;code&gt;\f&lt;/code&gt; 相同；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\\&lt;/code&gt; 插入 &lt;code&gt;\&lt;/code&gt; 字符；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\nnn&lt;/code&gt; 插入 &lt;code&gt;nnn&lt;/code&gt;（八进制）所代表的 &lt;code&gt;ASCII&lt;/code&gt; 字符；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;echo&lt;/code&gt; 命令还可以设置输出的样式，格式如下：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;echo -e &quot;\033[字背景颜色；文字颜色m 字符串\033[0m&quot;&lt;/code&gt; &lt;code&gt;echo -e &quot;\033[1;36;41m Something here \033[0m&quot;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;\033&lt;/code&gt; 代表键盘的 &lt;code&gt;Ctl&lt;/code&gt; 键或 &lt;code&gt;Esc&lt;/code&gt; 键（即 &lt;code&gt;ASCII&lt;/code&gt; 码中的第 &lt;code&gt;27&lt;/code&gt; 位 &lt;code&gt;ESCAPE&lt;/code&gt;，八进制表示为 &lt;code&gt;33&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt; 代表字体行为(高亮，闪烁，下划线等)；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;36&lt;/code&gt; 代表字体的颜色，&lt;/li&gt;
&lt;li&gt;&lt;code&gt;41&lt;/code&gt; 的位置代表背景色&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：字背景颜色和文字颜色之间是英文的分号 &lt;code&gt;;&lt;/code&gt;，并且文字颜色后面有个 &lt;code&gt;m&lt;/code&gt; ，字符串前后可以没有空格，如果有的话，输出也是同样有空格。在 &lt;code&gt;Bash&lt;/code&gt; 中，&lt;code&gt;Esc&lt;/code&gt; 字符可以用以下的语法来表示：&lt;code&gt;\e&lt;/code&gt;，&lt;code&gt;\033&lt;/code&gt;，&lt;code&gt;\x1B&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;字体颜色范围是 &lt;code&gt;30 - 37&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt; 表示重置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo -e &quot;\033[30m 黑色字 \033[0m&quot;
echo -e &quot;\033[31m 红色字 \033[0m&quot;
echo -e &quot;\033[32m 绿色字 \033[0m&quot;
echo -e &quot;\033[33m 黄色字 \033[0m&quot;
echo -e &quot;\033[34m 蓝色字 \033[0m&quot;
echo -e &quot;\033[35m 紫色字 \033[0m&quot;
echo -e &quot;\033[36m 天蓝字 \033[0m&quot;
echo -e &quot;\033[37m 白色字 \033[0m&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;背景颜色范围是 &lt;code&gt;40 - 47&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo -e &quot;\033[40;37m 黑底白字 \033[0m&quot;
echo -e &quot;\033[41;37m 红底白字 \033[0m&quot;
echo -e &quot;\033[42;37m 绿底白字 \033[0m&quot;
echo -e &quot;\033[43;37m 黄底白字 \033[0m&quot;
echo -e &quot;\033[44;37m 蓝底白字 \033[0m&quot;
echo -e &quot;\033[45;37m 紫底白字 \033[0m&quot;
echo -e &quot;\033[46;37m 天蓝底白字 \033[0m&quot;
echo -e &quot;\033[47;30m 白底黑字 \033[0m&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;控制选项&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;033[0m # 关闭所有属性
\033[1m # 设置高亮度
\033[4m # 下划线
\033[5m # 闪烁
\033[7m # 反显
\033[8m # 消隐
\033[30m — \33[37m # 设置前景色
\033[40m — \33[47m # 设置背景色
\033[60A  # 光标上移60行
\033[60B  # 光标下移60行
\033[60C  # 光标右移60行
\033[60G  # 光标右移60行
\033[60D  # 光标左移60行
\033[y;xH # 设置光标位置
\033[2J   # 清屏
\033[K    # 清除从光标到行尾的内容
\033[s    # 保存光标位置
\033[u    # 恢复光标位置
\033[?25l # 隐藏光标
\033[?25h # 显示光标
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结束非常规字符序列的 &lt;code&gt;m&lt;/code&gt; 要紧跟前面的数字，不能有空格命令也可以写成&lt;code&gt;echo -e &quot;^[[44;37;5m ME \033[0m COOL&quot;&lt;/code&gt;，其中的 &lt;code&gt;^[&lt;/code&gt; 需要先按 &lt;code&gt;⌃ + V&lt;/code&gt; 键 ,然后再按 &lt;code&gt;ESC&lt;/code&gt; 键生成。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;在 &lt;code&gt;RedHat&lt;/code&gt; 系统的 &lt;code&gt;/etc/sysconfig/init&lt;/code&gt; 文件中有事先设置好的输出方式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# color =&gt; new RH6.0 bootup
# verbose =&gt; old-style bootup
# anything else =&gt; new style bootup without ANSI colors or positioning
BOOTUP=color
# column to start &quot;[  OK  ]&quot; label in
RES_COL=60
# terminal sequence to move to that column. You could change this
# to something like &quot;tput hpa ${RES_COL}&quot; if your terminal supports it
MOVE_TO_COL=&quot;echo -en \\033[${RES_COL}G&quot;
# terminal sequence to set color to a &apos;success&apos; color (currently: green)
SETCOLOR_SUCCESS=&quot;echo -en \\033[0;32m&quot;
# terminal sequence to set color to a &apos;failure&apos; color (currently: red)
SETCOLOR_FAILURE=&quot;echo -en \\033[0;31m&quot;
# terminal sequence to set color to a &apos;warning&apos; color (currently: yellow)
SETCOLOR_WARNING=&quot;echo -en \\033[0;33m&quot;
# terminal sequence to reset to the default color.
SETCOLOR_NORMAL=&quot;echo -en \\033[0;39m&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 实例
echo -e &quot;\e[1;31mThis is red text\e[0m&quot; #红色字体

echo -e &quot;\e[1;42mGreed Background\e[0m&quot; # 绿色背景

echo -e &quot;\033[37;31;5mMySQL Server Stop...\033[39;49;0m&quot; #文字闪烁
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;文字和背景也可以设置更多颜色，参考网站&lt;a href=&quot;https://misc.flogisoft.com/bash/tip_colors_and_formatting&quot; title=&quot;Bash tips: Colors and formatting (ANSI/VT100 Control sequences)&quot;&gt;Bash tips: Colors and formatting (ANSI/VT100 Control sequences)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;which 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;which&lt;/code&gt; 命令 用于查找并显示给定命令的绝对路径，环境变量 &lt;code&gt;PATH&lt;/code&gt; 中保存了查找命令时需要遍历的目录。&lt;code&gt;which&lt;/code&gt; 指令会在环境变量 &lt;code&gt;$PATH&lt;/code&gt; 设置的目录里查找符合条件的文件。也就是说，使用 &lt;code&gt;which&lt;/code&gt; 命令，就可以看到某个系统命令是否存在，以及执行的到底是哪一个位置的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
which -n&amp;#x3C;文件名长度&gt; # 制定文件名长度，指定的长度必须大于或等于所有文件中最长的文件名；
which -p&amp;#x3C;文件名长度&gt; # 与-n参数相同，但此处的&amp;#x3C;文件名长度&gt;包含了文件的路径；
which -w # 指定输出时栏位的宽度；
which -V # 显示版本信息。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;whereis 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;whereis&lt;/code&gt; 命令只能用于程序名的搜索，而且只搜索二进制文件(参数 &lt;code&gt;-b&lt;/code&gt;)、&lt;code&gt;man&lt;/code&gt; 说明文件(参数&lt;code&gt;-m&lt;/code&gt;)和源代码文件(参数&lt;code&gt;-s&lt;/code&gt;)。如果省略参数，则返回所有信息。&lt;/p&gt;
&lt;p&gt;和 &lt;code&gt;find&lt;/code&gt; 相比，&lt;code&gt;whereis&lt;/code&gt; 查找的速度非常快，这是因为 &lt;code&gt;linux&lt;/code&gt; 系统会将 系统内的所有文件都记录在一个数据库文件中，当使用 &lt;code&gt;whereis&lt;/code&gt; 和下面即将介绍的 &lt;code&gt;locate&lt;/code&gt; 时，会从数据库中查找数据，而不是像 &lt;code&gt;find&lt;/code&gt; 命令那样，通过遍历硬盘来查找，效率自然会很高。 但是该数据库文件并不是实时更新，默认情况下时一星期更新一次，因此，我们在用 &lt;code&gt;whereis&lt;/code&gt; 和 &lt;code&gt;locate&lt;/code&gt; 查找文件时，有时会找到已经被删除的数据，或者刚刚建立文件，却无法查找到，原因就是因为数据库文件没有被更新。&lt;/p&gt;
&lt;p&gt;语法 &lt;code&gt;whereis [选项] [参数]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
whereis -b # 只查找二进制文件(可执行文件)；
whereis -B&amp;#x3C;目录&gt; # 只在设置的目录下查找二进制文件；
whereis -f # 不显示文件名前的路径名称；
whereis -m # 只查找说明文件；
whereis -M&amp;#x3C;目录&gt; # 只在设置的目录下查找说明文件；
whereis -s # 只查找原始代码文件；
whereis -S&amp;#x3C;目录&gt; # 只在设置的目录下查找原始代码文件；
whereis -u # 搜索默认路径下除可执行文件、源代码文件、帮助文件以外的其它文件。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：指令名：要查找的二进制程序、源文件和 &lt;code&gt;man&lt;/code&gt; 手册页的指令名。&lt;/p&gt;
&lt;h2&gt;locate 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;locate&lt;/code&gt; 通过搜寻系统内建文档数据库达到快速找到档案，数据库由 &lt;code&gt;updatedb&lt;/code&gt; 程序来更新，&lt;code&gt;updatedb&lt;/code&gt; 是由 &lt;code&gt;cron daemon&lt;/code&gt; 周期性调用的。默认情况下 &lt;code&gt;locate&lt;/code&gt; 命令在搜寻数据库时比由整个由硬盘资料来搜寻资料来得快，但较差劲的是 &lt;code&gt;locate&lt;/code&gt; 所找到的档案若是最近才建立或 刚更名的，可能会找不到，在内定值中，&lt;code&gt;updatedb&lt;/code&gt; 每天会跑一次，可以由修改 &lt;code&gt;crontab&lt;/code&gt; 来更新设定值 (&lt;code&gt;etc/crontab&lt;/code&gt;)。&lt;code&gt;locate&lt;/code&gt; 与 &lt;code&gt;find&lt;/code&gt; 命令相似，可以使用如 &lt;code&gt;*、?&lt;/code&gt;等进行正则匹配查找。&lt;/p&gt;
&lt;p&gt;语法 &lt;code&gt;locate [选项] [模式]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-b, --basename  # 仅匹配路径名的基本名称
-c, --count     # 只输出找到的数量
-d, --database DBPATH # 使用DBPATH指定的数据库，而不是默认数据库 /var/lib/mlocate/mlocate.db
-e, --existing  # 仅打印当前现有文件的条目
-1 # 如果 是 1．则启动安全模式。在安全模式下，使用者不会看到权限无法看到 的档案。这会始速度减慢，因为 locate 必须至实际的档案系统中取得档案的  权限资料。
-0, --null            # 在输出上带有NUL的单独条目
-S, --statistics      # 不搜索条目，打印有关每个数据库的统计信息
-q                    # 安静模式，不会显示任何错误讯息。
-P, --nofollow, -H    # 检查文件存在时不要遵循尾随的符号链接
-l, --limit, -n LIMIT # 将输出(或计数)限制为LIMIT个条目
-n                    # 至多显示 n个输出。
-m, --mmap            # 被忽略，为了向后兼容
-r, --regexp REGEXP   # 使用基本正则表达式
    --regex           # 使用扩展正则表达式
-q, --quiet           # 安静模式，不会显示任何错误讯息
-s, --stdio           # 被忽略，为了向后兼容
-o                    # 指定资料库存的名称。
-h, --help            # 显示帮助
-i, --ignore-case     # 忽略大小写
-V, --version         # 显示版本信息

# 实例
locate -r &apos;^/var.*reason$&apos; # 查找 /var 目录下，以 reason 结尾的文件
locate pwd # 查找和 pwd 有关的文件
locate /etc/sh # 搜索 etc 目录下所有以 sh 开头的文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;find 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;find&lt;/code&gt; 命令用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为欲查找的目录名。如果使用该命令时，不设置任何参数，则 &lt;code&gt;find&lt;/code&gt; 命令将在当前目录下查找子目录与文件，并且将查找到的子目录和文件全部进行显示。&lt;/p&gt;
&lt;p&gt;语法 &lt;code&gt;find pathname -options [-print -exec -depth -ok ...]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-name #按照文件名查找文件
-perm #按文件权限查找文件
-user #按文件属主查找文件
-group  #按照文件所属的组来查找文件。
-type  #查找某一类型的文件，诸如：
# b - 块设备文件
# d - 目录
# c - 字符设备文件
# l - 符号链接文件
# p - 管道文件
# f - 普通文
-size n :[c] # 查找文件长度为n块文件，带有c时表文件字节大小
-amin n   # 查找系统中最后N分钟访问的文件
-atime n  # 查找系统中最后n*24小时访问的文件
-cmin n   # 查找系统中最后N分钟被改变文件状态的文件
-ctime n  # 查找系统中最后n*24小时被改变文件状态的文件
-mmin n   # 查找系统中最后N分钟被改变文件数据的文件
-mtime n  # 查找系统中最后n*24小时被改变文件数据的文件(用减号-来限定更改时间在距今n日以内的文件，而用加号+来限定更改时间在距今n日以前的文件。 )
-maxdepth n # 最大查找目录深度
-prune # 选项来指出需要忽略的目录。在使用-prune选项时要当心，因为如果你同时使用了-depth选项，那么-prune选项就会被find命令忽略
-newer # 如果希望查找更改时间比某个文件新但比另一个文件旧的所有文件，可以使用-newer选项
-print # find命令将匹配的文件输出到标准输出。
-exec # find命令对匹配的文件执行该参数所给出的shell命令。相应命令的形式为&apos;command&apos; {  } \;，注意{   }和\；之间的空格。
-ok # 和-exec的作用相同，只不过以一种更为安全的模式来执行该参数所给出的shell命令，在执行每一个命令之前，都会给出提示，让用户来确定是否执行。
-depth # 从指定目录下最深层的子目录开始查找；

# 实例
find -atime -2 # 查找48小时内修改过的文件
find ./ -name &apos;*.log&apos; # 在当前目录查找 以 .log 结尾的文件。 . 代表当前目录
find /opt -perm 777 #查找 /opt 目录下 权限为 777 的文件
find -size +1000c #查找大于 1K 的文件
find -size 1000c #查找等于 1000 字符的文件

find . -type f -mtime +10 -exec rm -f {} \; # 在当前目录中查找更改时间在10日以前的文件并删除它们(无提醒）
find . -name &apos;*.log&apos; mtime +5 -ok -exec rm {} \; # 当前目录中查找所有文件名以.log结尾、更改时间在5日以上的文件，并删除它们，只不过在删除之前先给出提示。 按y键删除文件，按n键不删除
find . -f -name &apos;passwd*&apos; -exec grep &quot;pkg&quot; {} \; # 当前目录下查找文件名以 passwd 开头，内容包含 &quot;pkg&quot; 字符的文件
find . -name &apos;*.log&apos; -exec cp {} test3 \; # 用 exec 选项执行 cp 命令
find . -type f -print | xargs file # 查找当前目录下每个普通文件，然后使用 xargs 来判断文件类型
find . -type f -name &quot;*.js&quot; -exec grep -lF &apos;editor&apos; {} \; # 查找当前目录下所有以 js 结尾的并且其中包含 &apos;editor&apos; 字符的普通文件
find -type f -name &apos;*.js&apos; | xargs grep -lF &apos;editor&apos; # 查找当前目录下所有以 js 结尾的并且其中包含 &apos;editor&apos; 字符的普通文件
find . -name &quot;*.log&quot; | xargs -i mv {} test4 # 利用 xargs 执行 mv 命令
find . -name \*(转义） -type f -print | xargs grep -n &apos;hostnames&apos; # 用 grep 命令在当前目录下的所有普通文件中搜索 hostnames 这个词，并标出所在行
find . -name &apos;[a-z]*[4-9].log&apos; -print # 查找当前目录中以一个小写字母开头，最后是 4 到 9 加上 .log 结束的文件
find test -path &apos;test/test4&apos; -prune -o -print #
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;find&lt;/code&gt; 命令的参数非常多， 用法爷非常多，这里只是列出了很小一部分，如果想详细了解可以用 &lt;code&gt;man&lt;/code&gt; 命令查看。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;chmod 命令&lt;/h2&gt;
&lt;p&gt;用于改变 &lt;code&gt;linux&lt;/code&gt; 系统文件或目录的访问权限。用它控制文件或目录的访问权限。该命令有两种用法。一种是包含字母和操作符表达式的文字设定法；另一种是包含数字的数字设定法。&lt;/p&gt;
&lt;p&gt;每一文件或目录的访问权限都有三组，每组用三位表示，分别为文件属主的读、写和执行权限；与属主同组的用户的读、写和执行权限；系统中其他用户的读、写和执行权限。可使用 &lt;code&gt;ls -l&lt;/code&gt; 查找。关于 &lt;code&gt;ls -l&lt;/code&gt; 的各个字段表示什么意思可以看我的另一篇文章&lt;a href=&quot;https://www.clloz.com/programming/assorted/wordpress/2020/07/28/wordpress-permissions/&quot; title=&quot;WordPress文件权限问题&quot;&gt;WordPress文件权限问题&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-c 当发生改变时，报告处理信息
-R 处理指定目录以及其子目录下所有文件
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;权限范围&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;u&lt;/code&gt; ：&lt;code&gt;user&lt;/code&gt; 目录或者文件的当前的用户&lt;/li&gt;
&lt;li&gt;&lt;code&gt;g&lt;/code&gt; ：&lt;code&gt;group&lt;/code&gt; 目录或者文件的当前的群组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;o&lt;/code&gt; ：&lt;code&gt;other&lt;/code&gt; 除了目录或者文件的当前用户或群组之外的用户或者群组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt; ：&lt;code&gt;all&lt;/code&gt; 所有的用户及群组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;权限代号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt; ：读权限，用数字 &lt;code&gt;4&lt;/code&gt; 表示&lt;/li&gt;
&lt;li&gt;&lt;code&gt;w&lt;/code&gt; ：写权限，用数字 &lt;code&gt;2&lt;/code&gt; 表示&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; ：执行权限，用数字 &lt;code&gt;1&lt;/code&gt; 表示&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-&lt;/code&gt; ：删除权限，用数字 &lt;code&gt;0&lt;/code&gt; 表示&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; ：特殊权限&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 实例
chmod a+x t.log # 增加文件 t.log 所有用户可执行权限
chmod u=r t.log -c # 撤销原来所有的权限，然后使拥有者具有可读权限,并输出处理信息
chmod 751 t.log -c（或者：chmod u=rwx,g=rx,o=x t.log -c) # 给 file 的属主分配读、写、执行(7)的权限，给file的所在组分配读、执行(5)的权限，给其他用户分配执行(1)的权限
chmod u+r,g+r,o+r -R text/ -c # 将 test 目录及其子目录所有文件添加可读权限
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;tar 命令&lt;/h2&gt;
&lt;p&gt;用来压缩和解压文件。&lt;code&gt;tar&lt;/code&gt; 本身不具有压缩功能，只具有打包功能，有关压缩及解压是调用其它的功能来完成（比如 &lt;code&gt;gzip&lt;/code&gt; 和 &lt;code&gt;bz2&lt;/code&gt;）。打包是指将一大堆文件或目录变成一个总的文件；压缩则是将一个大的文件通过一些压缩算法变成一个小文件。&lt;code&gt;Linux&lt;/code&gt; 中很多压缩程序只能针对一个文件进行压缩，这样当你想要压缩一大堆文件时，你得先将这一大堆文件先打成一个包（&lt;code&gt;tar&lt;/code&gt; 命令），然后再用压缩程序进行压缩（&lt;code&gt;gzip&lt;/code&gt; &lt;code&gt;bzip2&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;语法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-c #建立新的压缩文件
-f #指定压缩文件
-r #添加文件到已经压缩文件包中
-u #添加改了和现有的文件到压缩包中
-x #从压缩包中抽取文件
-t #显示压缩文件中的内容
-z #支持gzip压缩
-j #支持bzip2压缩
-Z #支持compress解压文件
-v #显示操作过程

#实例
tar -cvf log.tar 1.log,2.log 或tar -cvf log.* # 将文件全部打包成 tar 包
tar -zcvf /tmp/etc.tar.gz /etc # 将 /etc 下的所有文件及目录打包到指定目录，并使用 gz 压缩
tar -ztvf /tmp/etc.tar.gz # 查看刚打包的文件内容（一定加z，因为是使用 gzip 压缩的）
tar --exclude /home/dmtsai -zcvf myfile.tar.gz /home/* /etc # 要压缩打包 /home, /etc ，但不要 /home/dmtsai
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;chown 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;chown&lt;/code&gt; 将指定文件的拥有者改为指定的用户或组，用户可以是用户名或者用户 &lt;code&gt;ID&lt;/code&gt;；组可以是组名或者组 &lt;code&gt;ID&lt;/code&gt;；文件是以空格分开的要改变权限的文件列表，支持通配符。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-c #显示更改的部分的信息
-R #处理指定目录及子目录

# 实例
chown -c mail:mail log2012.log # 改变拥有者和群组 并显示改变信息
chown -c :mail t.log # 改变文件群组
chown -cR mail: test/ # 改变文件夹及子文件目录属主及属组为 mail
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;df 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;df&lt;/code&gt; 命令显示磁盘空间使用情况。获取硬盘被占用了多少空间，目前还剩下多少空间等信息，如果没有文件名被指定，则所有当前被挂载的文件系统的可用空间将被显示。默认情况下，磁盘空间将以 &lt;code&gt;1KB&lt;/code&gt; 为单位进行显示，除非环境变量 &lt;code&gt;POSIXLY_CORRECT&lt;/code&gt; 被指定，那样将以 &lt;code&gt;512&lt;/code&gt; 字节为单位进行显示。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-a #全部文件系统列表
-h #以方便阅读的方式显示信息
-i #显示inode信息
-k #区块为1024字节
-l #只显示本地磁盘
-T #列出文件系统类型

# 实例
df -l # 显示磁盘使用情况
df -haT # 以易读方式列出所有文件系统及其类型
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;du 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;du&lt;/code&gt; 命令也是查看使用空间的，但是与 &lt;code&gt;df&lt;/code&gt; 命令不同的是 &lt;code&gt;du&lt;/code&gt; 命令是对文件和目录磁盘使用的空间的查看。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-a #显示目录中所有文件大小
-k #以KB为单位显示文件大小
-m #以MB为单位显示文件大小
-g #以GB为单位显示文件大小
-h #以易读方式显示文件大小
-s #仅显示总计
-c #或--total  除了显示个别目录或文件的大小外，同时也显示所有目录或文件的总和

# 实例
du -h scf/ # 以易读方式显示文件夹内及子文件夹大小
du -ah scf/ # 以易读方式显示文件夹内所有文件大小
du -hc test/ scf/ # 显示几个文件或目录各自占用磁盘空间的大小，还统计它们的总和
du -hc --max-depth=1 scf/ # 输出当前目录下各个子目录所使用的空间
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ln 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ln&lt;/code&gt; 命令的功能是为文件在另外一个位置建立一个同步的链接，当在不同目录需要该问题时，就不需要为每一个目录创建同样的文件，通过 &lt;code&gt;ln&lt;/code&gt; 创建的链接（&lt;code&gt;link&lt;/code&gt;）减少磁盘占用量。链接分为硬链接和软链接，关于两者的区别请看我的另一篇文章&lt;a href=&quot;https://www.clloz.com/programming/assorted/wordpress/2020/07/28/wordpress-permissions/&quot; title=&quot;WordPress文件权限问题&quot;&gt;WordPress文件权限问题&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-b# 删除，覆盖以前建立的链接
-s #软链接（符号链接）
-v #显示详细处理过程

# 实例
ln -sv source.log link.log # 给文件创建软链接，并显示操作信息
ln -v source.log link1.log # 给文件创建硬链接，并显示操作信息
ln -sv /opt/soft/test/test3 /opt/soft/test/test5 # 给目录创建软链接
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;date 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Date&lt;/code&gt; 命令显示或设定系统的日期与时间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-d&amp;#x3C;字符串&gt; 　#显示字符串所指的日期与时间。字符串前后必须加上双引号。
-s&amp;#x3C;字符串&gt; 　#根据字符串来设置日期与时间。字符串前后必须加上双引号。
-u 　#显示GMT。
%H #小时(00-23)
%I #小时(00-12)
%M #分钟(以00-59来表示)
%s #总秒数。起算时间为1970-01-01 00:00:00 UTC。
%S #秒(以本地的惯用法来表示)
%a #星期的缩写。
%A #星期的完整名称。
%d #日期(以01-31来表示)。
%D #日期(含年月日)。
%m #月份(以01-12来表示)。
%y #年份(以00-99来表示)。
%Y #年份(以四位数来表示)。

# 实例
date +%Y%m%d --date=&quot;+1 day&quot;  #显示下一天的日期
date -d &quot;nov 22&quot;  #今年的 11 月 22 日是星期三
date -d &apos;2 weeks&apos; #2周后的日期
date -d &apos;next monday&apos; #下周一的日期
date -d next-day +%Y%m%d #明天的日期 或者：date -d tomorrow +%Y%m%d
date -d last-day +%Y%m%d #昨天的日期 或者：date -d yesterday +%Y%m%d
date -d last-month +%Y%m #上个月是几月
date -d next-month +%Y%m #下个月是几月
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;cal 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cal&lt;/code&gt; 命令可以用户显示公历（阳历）日历如只有一个参数，则表示年份(1-9999)，如有两个参数，则表示月份和年份。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#选项
-3 #显示前一月，当前月，后一月三个月的日历
-m #显示星期一为第一列
-j #显示在当前年第几天
-y #[year]显示当前年[year]份的日历

#实例
cal 9 2012 #显示指定年月日期
cal -y 2013 #显示2013年每个月日历
cal -3m #将星期一做为第一列,显示前中后三月
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;grep 命令&lt;/h2&gt;
&lt;p&gt;强大的文本搜索命令，&lt;code&gt;grep(Global Regular Expression Print)&lt;/code&gt; 全局正则表达式搜索。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;grep&lt;/code&gt; 的工作方式是这样的，它在一个或多个文件中搜索字符串模板。如果模板包括空格，则必须被引用，模板后的所有字符串被看作文件名。搜索的结果被送到标准输出，不影响原文件内容。&lt;/p&gt;
&lt;p&gt;关于正则表达式的应用参考我的另一篇文章&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/08/05/regex-javascript-apply/&quot; title=&quot;正则表达式入门以及JavaScript中的应用&quot;&gt;正则表达式入门以及JavaScript中的应用&lt;/a&gt;，需要注意一点的是在 &lt;code&gt;grep&lt;/code&gt; 中分组 &lt;code&gt;()&lt;/code&gt; 圆括号需要转义。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#选项
-a --text  # 不要忽略二进制数据。
-A &amp;#x3C;显示行数&gt;   --after-context=&amp;#x3C;显示行数&gt;   # 除了显示符合范本样式的那一行之外，并显示该行之后的内容。
-b --byte-offset                           # 在显示符合范本样式的那一行之外，并显示该行之前的内容。
-B&amp;#x3C;显示行数&gt;   --before-context=&amp;#x3C;显示行数&gt;   # 除了显示符合样式的那一行之外，并显示该行之前的内容。
-c --count    # 计算符合范本样式的列数。
-C&amp;#x3C;显示行数&gt; --context=&amp;#x3C;显示行数&gt;或-&amp;#x3C;显示行数&gt; # 除了显示符合范本样式的那一列之外，并显示该列之前后的内容。
-d&amp;#x3C;进行动作&gt; --directories=&amp;#x3C;动作&gt;  # 当指定要查找的是目录而非文件时，必须使用这项参数，否则grep命令将回报信息并停止动作。
-e&amp;#x3C;范本样式&gt; --regexp=&amp;#x3C;范本样式&gt;   # 指定字符串作为查找文件内容的范本样式。
-E --extended-regexp             # 将范本样式为延伸的普通表示法来使用，意味着使用能使用扩展正则表达式。
-f&amp;#x3C;范本文件&gt; --file=&amp;#x3C;规则文件&gt;     # 指定范本文件，其内容有一个或多个范本样式，让grep查找符合范本条件的文件内容，格式为每一列的范本样式。
-F --fixed-regexp   # 将范本样式视为固定字符串的列表。
-G --basic-regexp   # 将范本样式视为普通的表示法来使用。
-h --no-filename    # 在显示符合范本样式的那一列之前，不标示该列所属的文件名称。
-H --with-filename  # 在显示符合范本样式的那一列之前，标示该列的文件名称。
-i --ignore-case    # 忽略字符大小写的差别。
-l --file-with-matches   # 列出文件内容符合指定的范本样式的文件名称。
-L --files-without-match # 列出文件内容不符合指定的范本样式的文件名称。
-n --line-number         # 在显示符合范本样式的那一列之前，标示出该列的编号。
-P --perl-regexp         # PATTERN 是一个 Perl 正则表达式
-q --quiet或--silent     # 不显示任何信息。
-R/-r  --recursive       # 此参数的效果和指定“-d recurse”参数相同。
-s --no-messages  # 不显示错误信息。
-v --revert-match # 反转查找。
-V --version      # 显示版本信息。
-w --word-regexp  # 只显示全字符合的列。
-x --line-regexp  # 只显示全列符合的列。
-y # 此参数效果跟“-i”相同。
-o # 只输出文件中匹配到的部分。
-m &amp;#x3C;num&gt; --max-count=&amp;#x3C;num&gt; # 找到num行结果后停止查找，用来限制匹配行数

#实例
ps -ef | grep svn #查找指定进程
ps -ef | grep svn -c #查找指定进程个数
cat test1.txt | grep -f key.log #从文件中读取关键词
grep -lR &apos;^grep&apos; /tmp #从文件夹中递归查找以grep开头的行，并只列出文件
grep &apos;^[^x]&apos; test.txt #查找非x开头的行内容
grep -E &apos;ed|at&apos; test.txt #显示包含 ed 或者 at 字符的内容行
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;wc 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;wc(word count)&lt;/code&gt; 命令功能为统计指定的文件中字节数、字数、行数，并将统计结果输出。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#选项
-c #统计字节数
-l #统计行数
-m #统计字符数
-w #统计词数，一个字被定义为由空白、跳格或换行字符分隔的字符串

#实例
wc text.txt #查找文件的 行数 单词数 字节数 文件名，结果：7 8 70 test.txt
cat test.txt | wc -l #统计输出结果的行数

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ps 命令&lt;/h2&gt;
&lt;p&gt;ps(process status)，用来查看当前运行的进程状态，一次性查看，如果需要动态连续结果使用 top&lt;/p&gt;
&lt;p&gt;linux上进程有 &lt;code&gt;5&lt;/code&gt; 种状态:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;运行(正在运行或在运行队列中等待)&lt;/li&gt;
&lt;li&gt;中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号)&lt;/li&gt;
&lt;li&gt;不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生)&lt;/li&gt;
&lt;li&gt;僵死(进程已终止, 但进程描述符存在, 直到父进程调用 &lt;code&gt;wait4()&lt;/code&gt; 系统调用后释放)&lt;/li&gt;
&lt;li&gt;停止(进程收到 &lt;code&gt;SIGSTOP, SIGSTP, SIGTIN, SIGTOU&lt;/code&gt; 信号后停止运行运行)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;ps&lt;/code&gt; 工具标识进程的 &lt;code&gt;5&lt;/code&gt; 种状态码:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;D&lt;/code&gt; 不可中断 &lt;code&gt;uninterruptible sleep (usually IO)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;R&lt;/code&gt; 运行 &lt;code&gt;runnable (on run queue)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;S&lt;/code&gt; 中断 &lt;code&gt;sleeping&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt; 停止 &lt;code&gt;traced or stopped&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Z&lt;/code&gt; 僵死 &lt;code&gt;a defunct (”zombie”) process&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-A #显示所有进程
a #显示所有进程
-a #显示同一终端下所有进程
c #显示进程真实名称
e #显示环境变量
f #显示进程间的关系
r #显示当前终端运行的进程
-aux #显示所有包含其它使用的进程

# 实例
ps -ef #显示当前所有进程环境变量及进程间关系
ps -A #显示当前所有进程
ps -aux | grep apache #与grep联用查找某进程
ps aux | grep &apos;(cron|syslog)&apos; #找出与 cron 与 syslog 这两个服务有关的 PID 号码
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;top 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;top&lt;/code&gt; 命令显示当前系统正在执行的进程的相关信息，包括进程 &lt;code&gt;ID&lt;/code&gt;、内存占用率、&lt;code&gt;CPU&lt;/code&gt; 占用率等。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#选项
-c 显示完整的进程命令
-s 保密模式
-p &amp;#x3C;进程号&gt; 指定进程显示
-n &amp;#x3C;次数&gt;循环显示次数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果的前五行是当前系统情况整体的统计信息区。举例如下的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;top - 14:06:23 up 70 days, 16:44,  2 users,  load average: 1.25, 1.32, 1.35
Tasks: 206 total,   1 running, 205 sleeping,   0 stopped,   0 zombie
Cpu(s):  5.9%us,  3.4%sy,  0.0%ni, 90.4%id,  0.0%wa,  0.0%hi,  0.2%si,  0.0%st
Mem:  32949016k total, 14411180k used, 18537836k free,   169884k buffers
Swap: 32764556k total,        0k used, 32764556k free,  3612636k cached
PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND

28894 root      22   0 1501m 405m  10m S 52.2  1.3   2534:16 java
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;第一行，任务队列信息，同 uptime 命令的执行结果，具体参数说明情况如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;14:06:23&lt;/code&gt; — 当前系统时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;up 70 days, 16:44&lt;/code&gt; — 系统已经运行了 &lt;code&gt;70&lt;/code&gt; 天 &lt;code&gt;16&lt;/code&gt; 小时 &lt;code&gt;44&lt;/code&gt; 分钟&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2 users&lt;/code&gt; — 当前有 &lt;code&gt;2&lt;/code&gt; 个用户登录系统&lt;/li&gt;
&lt;li&gt;&lt;code&gt;load average: 1.15, 1.42, 1.44&lt;/code&gt; — &lt;code&gt;load average&lt;/code&gt; 后面的三个数分别是 &lt;code&gt;1&lt;/code&gt; 分钟、&lt;code&gt;5&lt;/code&gt; 分钟、&lt;code&gt;15&lt;/code&gt; 分钟的负载情况。load average数据是每隔5秒钟检查一次活跃的进程数，然后按特定算法计算出的数值。如果这个数除以逻辑CPU的数量，结果高于5的时候就表明系统在超负荷运转了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二行，&lt;code&gt;Tasks&lt;/code&gt; — 任务（进程），具体信息说明举例：系统现在共有 &lt;code&gt;206&lt;/code&gt; 个进程，其中处于运行中的有&lt;code&gt;1&lt;/code&gt;个，&lt;code&gt;205&lt;/code&gt;个在休眠（&lt;code&gt;sleep&lt;/code&gt;），&lt;code&gt;stoped&lt;/code&gt; 状态的有 &lt;code&gt;0&lt;/code&gt; 个，&lt;code&gt;zombie&lt;/code&gt; 状态（僵尸）的有 &lt;code&gt;0&lt;/code&gt; 个。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三行，cpu状态信息，具体属性说明如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;5.9%us&lt;/code&gt; — 用户空间占用 &lt;code&gt;CPU&lt;/code&gt; 的百分比。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3.4% sy&lt;/code&gt; — 内核空间占用 &lt;code&gt;CPU&lt;/code&gt; 的百分比。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.0% ni&lt;/code&gt; — 改变过优先级的进程占用 &lt;code&gt;CPU&lt;/code&gt; 的百分比&lt;/li&gt;
&lt;li&gt;&lt;code&gt;90.4% id&lt;/code&gt;— 空闲 &lt;code&gt;CPU&lt;/code&gt; 百分比&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.0% wa&lt;/code&gt; — &lt;code&gt;IO&lt;/code&gt; 等待占用 &lt;code&gt;CPU&lt;/code&gt; 的百分比&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.0% hi&lt;/code&gt; — 硬中断（&lt;code&gt;Hardware IRQ&lt;/code&gt;）占用 &lt;code&gt;CPU&lt;/code&gt; 的百分比&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.2% si&lt;/code&gt; — 软中断（&lt;code&gt;Software Interrupts&lt;/code&gt;）占用 &lt;code&gt;CPU&lt;/code&gt; 的百分比&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第四行，内存状态，具体信息如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;32949016k total&lt;/code&gt; — 物理内存总量（&lt;code&gt;32GB&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;14411180k used&lt;/code&gt; — 使用中的内存总量（&lt;code&gt;14GB&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;18537836k free&lt;/code&gt; — 空闲内存总量（&lt;code&gt;18GB&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;169884k buffers&lt;/code&gt; — 缓存的内存量 （&lt;code&gt;169M&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第五行，&lt;code&gt;swap&lt;/code&gt;交换分区信息，具体信息说明如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;32764556k total&lt;/code&gt; — 交换区总量（&lt;code&gt;32GB&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0k used&lt;/code&gt; — 使用的交换区总量（&lt;code&gt;0K&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;32764556k free&lt;/code&gt; — 空闲交换区总量（&lt;code&gt;32GB&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3612636k cached&lt;/code&gt; — 缓冲的交换区总量（&lt;code&gt;3.6GB&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第六行空行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第七行以下：各进程（任务）的状态监控，项目列信息说明如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PID&lt;/code&gt; — 进程 &lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;USER&lt;/code&gt; — 进程所有者&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PR&lt;/code&gt; — 进程优先级&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NI&lt;/code&gt; — &lt;code&gt;nice&lt;/code&gt; 值。负值表示高优先级，正值表示低优先级&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VIRT&lt;/code&gt; — 进程使用的虚拟内存总量，单位 &lt;code&gt;kb&lt;/code&gt;。&lt;code&gt;VIRT=SWAP+RES&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RES&lt;/code&gt; — 进程使用的、未被换出的物理内存大小，单位 &lt;code&gt;kb&lt;/code&gt;。&lt;code&gt;RES=CODE+DATA&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SHR&lt;/code&gt; — 共享内存大小，单位 &lt;code&gt;kb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;S&lt;/code&gt; — 进程状态。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%CPU&lt;/code&gt; — 上次更新到现在的CPU时间占用百分比&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%MEM&lt;/code&gt; — 进程使用的物理内存百分比&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIME+&lt;/code&gt; — 进程使用的 &lt;code&gt;CPU&lt;/code&gt; 时间总计，单位 &lt;code&gt;1/100&lt;/code&gt; 秒&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COMMAND&lt;/code&gt; — 进程名称（命令名/命令行）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;top&lt;/code&gt; 命令的交互指令&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;h&lt;/code&gt;：显示 &lt;code&gt;top&lt;/code&gt; 交互命令帮助信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c&lt;/code&gt;：切换显示命令名称和完整命令行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;m&lt;/code&gt;：以内存使用率排序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;P&lt;/code&gt; 根据 &lt;code&gt;CPU&lt;/code&gt; 使用百分比大小进行排序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt;：根据时间/累计时间进行排序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;W&lt;/code&gt;：将当前设置写入&lt;code&gt;~/.toprc&lt;/code&gt; 文件中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;o&lt;/code&gt;：或者 &lt;code&gt;O&lt;/code&gt; 改变显示项目的顺序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q&lt;/code&gt;：退出&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 下排序需要先输入 &lt;code&gt;o&lt;/code&gt; 然后输入列名，比如 &lt;code&gt;mem&lt;/code&gt;，&lt;code&gt;cpu&lt;/code&gt; 等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;kill 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;kill&lt;/code&gt; 命令发送指定的信号到相应进程。不指定型号将发送 &lt;code&gt;SIGTERM（15）&lt;/code&gt; 终止指定进程。如果仍无法终止该程序可用 &lt;code&gt;-KILL&lt;/code&gt; 参数，其发送的信号为&lt;code&gt;SIGKILL(9)&lt;/code&gt; ，将强制结束进程，使用 &lt;code&gt;ps&lt;/code&gt; 命令或者 &lt;code&gt;jobs&lt;/code&gt; 命令可以查看进程号。&lt;code&gt;root&lt;/code&gt; 用户将影响用户的进程，非 &lt;code&gt;root&lt;/code&gt; 用户只能影响自己的进程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-l  #信号，若果不加信号的编号参数，则使用“-l”参数会列出全部的信号名称
-a  #当处理当前进程时，不限制命令名和进程号的对应关系
-p  #指定kill 命令只打印相关进程的进程号，而不发送任何信号
-s  #指定发送信号
-u  #指定用户

#实例
kill -9 $(ps -ef | grep pro1) #先使用ps查找进程pro1，然后用kill杀掉
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;free 命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;free&lt;/code&gt; 命令显示系统内存使用情况，包括物理内存、交互区内存(&lt;code&gt;swap&lt;/code&gt;)和内核缓冲区内存。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 选项
-b #以Byte显示内存使用情况
-k #以kb为单位显示内存使用情况
-m #以mb为单位显示内存使用情况
-g #以gb为单位显示内存使用情况
-s&amp;#x3C;间隔秒数&gt; #持续显示内存
-t #显示内存使用总合

#实例：
free #显示内存使用情况
free -k #显示内存使用情况
free -m #显示内存使用情况
free -t #以总和的形式显示内存的使用信息
free -s 10 #周期性查询内存使用情况
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://linuxgeeks.github.io/2015/06/28/154139-shell%E4%B8%AD%E4%BD%BF%E7%94%A8echo%E5%91%BD%E4%BB%A4%E8%BE%93%E5%87%BA%E4%BF%A1%E6%81%AF%E5%8F%8A%E5%B8%A6%E9%A2%9C%E8%89%B2%E7%9A%84%E6%96%87%E6%9C%AC/&quot; title=&quot;shell中使用echo命令输出信息及带颜色的文本&quot;&gt;shell中使用echo命令输出信息及带颜色的文本&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.runoob.com/w3cnote/linux-common-command-2.html&quot; title=&quot;Linux 常用命令学习&quot;&gt;Linux 常用命令学习&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wangchujiang.com/linux-command/&quot; title=&quot;Linux命令搜索&quot;&gt;Linux命令搜索&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/linux-logo.03F4Kw33.png"/><enclosure url="/_astro/linux-logo.03F4Kw33.png"/></item><item><title>CSS自定义属性</title><link>https://clloz.com/blog/css-custom-properties</link><guid isPermaLink="true">https://clloz.com/blog/css-custom-properties</guid><pubDate>Tue, 11 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CSS&lt;/code&gt; 自定义属性也被称为 &lt;code&gt;CSS&lt;/code&gt; 变量。一直以来，在 &lt;code&gt;CSS&lt;/code&gt; 中使用变量和函数都是很多人的期待，没有变量和函数的 &lt;code&gt;CSS&lt;/code&gt; 不够灵活。目前的 &lt;code&gt;CSS&lt;/code&gt; 依然是依靠层叠和继承来实现元素之间的样式关联，但实际上一些在结构上“不相关”的元素之间的样式并不是完全不相关的，比如我们整个页面的风格，色调等。目前的 &lt;code&gt;CSS&lt;/code&gt; 更像是一个标记语言，一份对 &lt;code&gt;DOM&lt;/code&gt; 文档的配置表，是静态的。引入变量和函数是 &lt;code&gt;CSS&lt;/code&gt; 的一个发展方向，今天这篇文章就介绍一下目前已经被绝大多数浏览器支持的 &lt;code&gt;CSS&lt;/code&gt; 自定义属性。&lt;/p&gt;
&lt;h2&gt;兼容性&lt;/h2&gt;
&lt;p&gt;我们可以在&lt;a href=&quot;https://caniuse.com/#feat=css-variables&quot; title=&quot;Can I use&quot;&gt;Can I use&lt;/a&gt;上查看到目前的 &lt;code&gt;CSS variable&lt;/code&gt; 的兼容性，除了 &lt;code&gt;IE&lt;/code&gt;、 &lt;code&gt;QQ&lt;/code&gt; 和 &lt;code&gt;baidu&lt;/code&gt;，其他的主流浏览器都已经支持该特性。该特性目前处于&lt;a href=&quot;https://www.clloz.com/programming/front-end/2018/10/03/w3c-standard-drafts/&quot; title=&quot;CR&quot;&gt;CR&lt;/a&gt;状态。&lt;/p&gt;
&lt;h2&gt;使用方法&lt;/h2&gt;
&lt;p&gt;带有前缀 &lt;code&gt;--&lt;/code&gt; 的属性名，比如 &lt;code&gt;--example&lt;/code&gt;、&lt;code&gt;--name&lt;/code&gt;，表示的是带有值的自定义属性，其可以通过 &lt;code&gt;var&lt;/code&gt; 函数在全文档范围内复用的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;自定义属性名是&lt;strong&gt;大小写敏感&lt;/strong&gt;的，&lt;code&gt;--my-color&lt;/code&gt; 和 &lt;code&gt;--My-color&lt;/code&gt; 会被认为是两个不同的自定义属性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;自定义属性的值可以是任何合法的 &lt;code&gt;CSS&lt;/code&gt; 属性值。自定义属性也可以像别的 &lt;code&gt;CSS&lt;/code&gt; 属性指定选择器。但是一般情况下我们是定义全局变量，选择器为 &lt;code&gt;:root&lt;/code&gt;，&lt;code&gt;:root&lt;/code&gt; 这个 &lt;code&gt;CSS&lt;/code&gt; 伪类匹配文档树的根元素。对于 &lt;code&gt;HTML&lt;/code&gt; 来说，&lt;code&gt;:root&lt;/code&gt; 表示 &lt;code&gt;&amp;#x3C;html&gt;&lt;/code&gt;元素，除了优先级更高之外，与 &lt;code&gt;html&lt;/code&gt; 选择器相同。。（我们也可以在非常复杂的选择器中比如 &lt;code&gt;#wrap .container li.active&lt;/code&gt; 中定义变量，但一般不会有这种需求）&lt;/p&gt;
&lt;p&gt;自定义属性的使用也非常简单，像函数调用一样 &lt;code&gt;var()&lt;/code&gt;。&lt;code&gt;var()&lt;/code&gt; 函数可以代替元素中任何属性中的值的任何部分。&lt;code&gt;var()&lt;/code&gt; 函数不能作为属性名、选择器或者其他除了属性值之外的值。（这样做通常会产生无效的语法或者一个没有关联到变量的值。）语法 &lt;code&gt;var( &amp;#x3C;custom-property-name&gt; , &amp;#x3C;declaration-value&gt;? )&lt;/code&gt;，方法的第一个参数是要替换的自定义属性的名称。函数的可选第二个参数用作回退值。如果第一个参数引用的自定义属性无效，则该函数将使用第二个值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* 只要合法的CSS属性值就可以作为变量值 */
--somekeyword: left;
--somecolor: #0000ff;
--somecomplexvalue: 3px 6px rgb(20, 32, 54);

/* 只能在选择器内使用 */
selector {
  --theme-color: gray;
}

/* 设置全局变量 */
:root {
  --theme-color: gray;
}

/* 使用变量 */
.button {
  background-color: var(--theme-color);
}

.title {
  color: var(--theme-color);
}

.image-grid &gt; .image {
  border-color: var(--theme-color);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义属性并不是你在其他编程语言中遇到的实际的变量。这些值仅当需要的时候才会计算，而并不会按其他规则进行保存。比如，你不能为元素设置一个属性，然后让它从兄弟或旁支子孙规则上获取值。属性仅用于匹配当前选择器及其子孙，这和通常的 &lt;code&gt;CSS&lt;/code&gt; 是一样的。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;自定义属性的回退值允许使用逗号。例如， &lt;code&gt;var(--foo, red, blue)&lt;/code&gt; 将 &lt;code&gt;red, blue&lt;/code&gt; 同时指定为回退值；即是说任何在第一个逗号之后到函数结尾前的值都会被考虑为回退值。&lt;/p&gt;
&lt;p&gt;实际上回退值可以包含任何字符，但是部分有特殊含义的字符除外，例如换行符、不匹配的右括号（如 &lt;code&gt;)&lt;/code&gt;、&lt;code&gt;]&lt;/code&gt; 或 &lt;code&gt;}&lt;/code&gt;）、感叹号以及顶层分号（不被任何非 &lt;code&gt;var()&lt;/code&gt; 的括号包裹的分号，例如&lt;code&gt;var(--bg-color, --bs;color)&lt;/code&gt;是不合法的，而 &lt;code&gt;var(--bg-color, --value(bs;color))&lt;/code&gt; 是合法的）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;var()&lt;/code&gt; 的第二个参数也可以用 &lt;code&gt;var()&lt;/code&gt; 嵌套。但是不建议这样使用，性能会有影响。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.two {
  color: var(--my-var, red); /* Red if --my-var is not defined */
}

.three {
  background-color: var(
    --my-var,
    var(--my-background, pink)
  ); /* pink if --my-var and --my-background are not defined */
}

.three {
  background-color: var(--my-var, --my-background, pink); /* Invalid: &quot;--my-background, pink&quot; */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;当浏览器遇到无效的 &lt;code&gt;var()&lt;/code&gt; 时（比如变量未设定并且没给出缺省值，或者当前元素不支持该属性），会使用继承值或初始值代替。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/*若为一个p元素设置如下的CSS*/
:root {
  --text-color: 16px;
}
p {
  color: blue;
}
p {
  color: var(--text-color);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器会将 &lt;code&gt;--text-color&lt;/code&gt; 的值替换给了 &lt;code&gt;var(--text-color)&lt;/code&gt;，但是 &lt;code&gt;16px&lt;/code&gt; 并不是 &lt;code&gt;color&lt;/code&gt; 的合法属性值。代换之后，该属性不会产生任何作用。当遇到一个无效的 &lt;code&gt;var()&lt;/code&gt; 时，浏览器会先看父元素有没有设置对应的属性，有则继承；如果没有则使用该属性的默认值。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;自定义属性还可以和 &lt;code&gt;calc()&lt;/code&gt; 结合实现更强大的功能。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root {
  --base-size: 4px;
}
.title {
  text-size: calc(5 * var(--base-size));
}
.body {
  text-size: calc(3 * var(--base-size));
}
:root {
  --base-size: 4px;
  --title-multiplier: 5;
  --body-multiplier: 3;
}
.title {
  text-size: calc(var(--title-multiplier) * var(--base-size));
}
.body {
  text-size: calc(var(--body-multiplier) * var(--base-size));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中获取或者修改 &lt;code&gt;CSS&lt;/code&gt; 变量和操作普通 &lt;code&gt;CSS&lt;/code&gt; 属性是一样的，并且有了自定义属性后我们也可以更抽象地用 &lt;code&gt;JavaScript&lt;/code&gt; 控制 &lt;code&gt;CSS&lt;/code&gt; 了，一定程度上实现 &lt;code&gt;JavaScript&lt;/code&gt; 和 &lt;code&gt;CSS&lt;/code&gt; 的解耦，将一些需要用 &lt;code&gt;JavaScript&lt;/code&gt; 操作的 &lt;code&gt;CSS&lt;/code&gt; 属性用变量代替，我们只要操作这个变量即可，而不用管在哪些元素中使用了这个变量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 获取一个 Dom 节点上的 CSS 变量
element.style.getPropertyValue(&apos;--my-var&apos;)

// 获取任意 Dom 节点上的 CSS 变量
getComputedStyle(element).getPropertyValue(&apos;--my-var&apos;)

// 修改一个 Dom 节点上的 CSS 变量
element.style.setProperty(&apos;--my-var&apos;, jsVar + 4)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;作用&lt;/h2&gt;
&lt;p&gt;我在前言中已经提到过自定义属性的作用，虽然我们在 &lt;code&gt;CSS&lt;/code&gt; 中利用级联和继承来实现属性的复用，但是一些在 &lt;code&gt;DOM&lt;/code&gt; 结构上并不相关的元素可能也有要复用的属性。复杂的网站都会有大量的 &lt;code&gt;CSS&lt;/code&gt; 代码，通常也会有许多重复的值，比如同一个颜色值可能在很多地方被使用，如果这个值发生了变化，需要全局搜索并且一个一个替换。自定义属性很好的解决了这个问题。另一个好处是语义化的标识。比如，&lt;code&gt;--main-text-color&lt;/code&gt; 会比 &lt;code&gt;#00ff00&lt;/code&gt; 更易理解，尤其是这个颜色值在其他上下文中也被使用到。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/*不用自定义属性的实现*/
.image-grid {
  display: flex;
  flex-wrap: wrap;
  padding: 8px;
}

.image-grid &gt; .image {
  margin: 8px;
  width: calc(100% - 16px);
}

@media (min-size: 600px) {
  /* 3 images per line */
  .image-grid &gt; .image {
    width: calc(100% / 3 - 16px);
  }
}

@media (min-size: 1024px) {
  /* 6 images per line */
  .image-grid &gt; .image {
    width: calc(100% / 6 - 16px);
  }
}

/*用自定义属性的实现*/
:root {
  --grid-spacing: 16px;
  --grid-columns: 1;
}

.image-grid {
  display: flex;
  flex-wrap: wrap;
  padding: calc(var(--grid-spacing) / 2);
}

.image-grid &gt; .image {
  margin: calc(var(--grid-spacing) / 2);
  width: calc(100% / var(--grid-columns) - var(--grid-spacing));
}

@media (min-size: 600px) {
  :root {
    --grid-columns: 3;
  }
}

@media (min-size: 1024px) {
  :root {
    --grid-columns: 6;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/CSS/Using_CSS_custom_properties&quot; title=&quot;使用CSS自定义属性（变量）&quot;&gt;使用CSS自定义属性（变量）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/CSS/--*&quot; title=&quot;CSS自定义属性（变量）&quot;&gt;CSS自定义属性（变量）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/25714131&quot; title=&quot;CSS自定义属性&quot;&gt;CSS自定义属性&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>enca查看修改文件charset</title><link>https://clloz.com/blog/enca</link><guid isPermaLink="true">https://clloz.com/blog/enca</guid><pubDate>Sun, 09 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 的默认文本编辑器 &lt;code&gt;TextEdit&lt;/code&gt; 不能查看和修改编码格式的，有时候打开一些 &lt;code&gt;GB2312&lt;/code&gt; 的中文文本会有乱码。虽然 &lt;code&gt;vscode&lt;/code&gt; 可以修改，但是比较麻烦。这里介绍一个可以查看、修改 &lt;code&gt;charset&lt;/code&gt; 的工具 &lt;code&gt;enca&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;安装和使用&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;完整的使用教程查看&lt;a href=&quot;https://linux.die.net/man/1/enca#:~:text=Charset%20is%20a%20set%20of,(bits)%20constituting%20the%20file.&quot; title=&quot;官方文档&quot;&gt;官方文档&lt;/a&gt;，本文只介绍一些基础的使用方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 上的安装非常简单：&lt;code&gt;brew install enca&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;使用方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;//帮助
enca --help
//查看语言列表
enca --list language
//查看编码
enca -L zh_CN filename
//修改编码，覆盖源文件
enca -L zh_CN -x UTF-8 filename
//修改编码，写入新文件
enca -L zh_CN -x UTF-8 source_filename target_filename
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>简单的nodejs爬虫</title><link>https://clloz.com/blog/nodejs-crawler</link><guid isPermaLink="true">https://clloz.com/blog/nodejs-crawler</guid><pubDate>Sun, 09 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们一般在&lt;a href=&quot;https://www.w3.org/TR/&quot; title=&quot;https://www.w3.org/TR/&quot;&gt;https://www.w3.org/TR/&lt;/a&gt;中查找前端相关的标准文档，但是各种各样的文档非常多，想要找到我们想要的文档有时候比较麻烦。今天就写一个简单的 &lt;code&gt;NodeJS&lt;/code&gt; 的小爬虫来把所有的 &lt;code&gt;CSS&lt;/code&gt; 相关文档爬取然后以列表的形式输出到一个页面上。&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;p&gt;新建一个项目，然后 &lt;code&gt;npm init&lt;/code&gt; 初始化。然后安装依赖：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;superagent&lt;/code&gt;：一个轻量的渐进式的 &lt;code&gt;ajax API&lt;/code&gt;，我们用来请求目标页面。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cheerio&lt;/code&gt;：相当于 &lt;code&gt;jQuery&lt;/code&gt;，我们用来处理 &lt;code&gt;DOM&lt;/code&gt; 元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;express&lt;/code&gt;：用来搭建一个简单的 &lt;code&gt;HTTP&lt;/code&gt; 服务器，也可以直接用 &lt;code&gt;node&lt;/code&gt; 自带的 &lt;code&gt;http&lt;/code&gt; 模块。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open&lt;/code&gt;：在浏览器中打开目标页面，完成数据处理后展示用。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;主要代码&lt;/h2&gt;
&lt;p&gt;整个处理逻辑非常简单，用 &lt;code&gt;superagent&lt;/code&gt; 请求目标页面，分析页面的 &lt;code&gt;DOM&lt;/code&gt; 解构，取出需要的部分，然后对数据进行包装，返回给页面。&lt;/p&gt;
&lt;h2&gt;请求页面&lt;/h2&gt;
&lt;p&gt;这里就是直接调用 &lt;code&gt;superagent&lt;/code&gt; 来获取页面的 &lt;code&gt;DOM&lt;/code&gt; 文档，返回的 &lt;code&gt;res&lt;/code&gt; 是一个对象，我们需要使用的是其中的 &lt;code&gt;text&lt;/code&gt; 属性的值，也就是 &lt;code&gt;DOM&lt;/code&gt; 文档对应的字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;superagent.get(&apos;https://www.w3.org/TR/&apos;).end((err, res) =&gt; {
  if (err) {
    console.log(`抓取失败：${err}`)
  } else {
    CSS_std = getCSSStd(res)
    console.log(&apos;complete!&apos;)
    open(&apos;http://localhost:3000/&apos;)
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解析文档&lt;/h2&gt;
&lt;p&gt;当我们已经获取了 &lt;code&gt;DOM&lt;/code&gt; 文档以后我们要做的就是分析 &lt;code&gt;DOM&lt;/code&gt; 解构，然后取出我们需要的内容。&lt;code&gt;w3.org&lt;/code&gt; 的页面解构也很简单，我们在浏览器的开发者工具的帮助下很快能定位到我们需要的 &lt;code&gt;DOM&lt;/code&gt; 解构。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;li
  data-title=&quot;media queries level 5&quot;
  data-tag=&quot;css&quot;
  data-status=&quot;wd&quot;
  data-version=&quot;upcoming ed&quot;
  style=&quot;display: inline-block; opacity: 1;&quot;
&gt;
  &amp;#x3C;div class=&quot;profile&quot;&gt;WD&amp;#x3C;/div&gt;
  &amp;#x3C;h2 class=&quot;WorkingDraft&quot;&gt;
    &amp;#x3C;a
      href=&quot;https://www.w3.org/TR/2020/WD-mediaqueries-5-20200731/&quot;
      title=&quot;Latest draft of Media Queries Level 5 formally approved by the group&quot;
      &gt;Media Queries Level 5&amp;#x3C;/a
    &gt;
  &amp;#x3C;/h2&gt;
  &amp;#x3C;p class=&quot;deliverer&quot;&gt;Cascading Style Sheets (CSS) Working Group&amp;#x3C;/p&gt;
  &amp;#x3C;p class=&quot;pubdetails&quot;&gt;
    2020-07-31 -
    &amp;#x3C;a title=&quot;Media Queries Level 5 publication history&quot; href=&quot;/standards/history/mediaqueries-5&quot;
      &gt;History&amp;#x3C;/a
    &gt;-
    &amp;#x3C;a
      href=&quot;https://drafts.csswg.org/mediaqueries-5/&quot;
      title=&quot;Latest editor&apos;s draft of Media Queries Level 5&quot;
      &gt;Editor&apos;s Draft&amp;#x3C;/a
    &gt;
  &amp;#x3C;/p&gt;
  &amp;#x3C;ul class=&quot;editorlist&quot;&gt;
    &amp;#x3C;li&gt;Dean Jackson&amp;#x3C;/li&gt;
    &amp;#x3C;li&gt;Florian Rivoal&amp;#x3C;/li&gt;
    &amp;#x3C;li&gt;Tab Atkins Jr.&amp;#x3C;/li&gt;
  &amp;#x3C;/ul&gt;
  &amp;#x3C;ul class=&quot;taglist&quot;&gt;
    &amp;#x3C;li class=&quot;css&quot;&gt;CSS&amp;#x3C;/li&gt;
  &amp;#x3C;/ul&gt;
&amp;#x3C;/li&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有的标准还有草稿都是以列表的形式展示，在每一个列表元素中，我们需要的内容一个是标准或者草稿的名字，一个是 &lt;code&gt;url&lt;/code&gt;，将他们解析出来放到一个对象中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getCSSStd(res) {
  let stdArr = []
  let $ = cheerio.load(res.text)

  $(&apos;#container [data-tag=&quot;css&quot;]&apos;).each((idx, el) =&gt; {
    let std = {
      name: $(el).children(&apos;h2&apos;).children(&apos;a&apos;).attr(&apos;title&apos;),
      url: $(el).children(&apos;h2&apos;).children(&apos;a&apos;).attr(&apos;href&apos;)
    }
    stdArr.push(std)
  })
  return stdArr
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们用 &lt;code&gt;cheerio.load&lt;/code&gt; 方法将 &lt;code&gt;DOM&lt;/code&gt; 字符串转化成一个类似 &lt;code&gt;jQuery&lt;/code&gt; 对象的形式，然后就利用对象的方法将我们需要的属性取出依次存入一个数组中，几乎和 &lt;code&gt;jQuery&lt;/code&gt; 没有什么区别，非常简单。&lt;/p&gt;
&lt;h2&gt;返回页面&lt;/h2&gt;
&lt;p&gt;现在我们已经获得了所有 &lt;code&gt;CSS&lt;/code&gt; 相关的标准或草稿的名称和 &lt;code&gt;url&lt;/code&gt;，剩下的就是将这些内容拼成一个 &lt;code&gt;HTML&lt;/code&gt; 文档，通过 &lt;code&gt;express&lt;/code&gt; 搭建的 &lt;code&gt;http&lt;/code&gt; 服务器返回到浏览器上。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function buildTemplate(arr) {
  let str = &apos;&apos;
  for (let i = 0; i &amp;#x3C; arr.length; i++) {
    str += `&amp;#x3C;li&gt;&amp;#x3C;a href=&quot;${arr[i].url}&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;${arr[i].name}&amp;#x3C;/a&gt;&amp;#x3C;/li&gt;`
  }
  return `&amp;#x3C;html name=clloz&gt;
    &amp;#x3C;head&gt;
    &amp;#x3C;/head&gt;
    &amp;#x3C;body&gt;
        &amp;#x3C;ul&gt;
           ${str}
        &amp;#x3C;/ul&gt;
    &amp;#x3C;/body&gt;
    &amp;#x3C;/html&gt;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;循环遍历数组，将名称和 &lt;code&gt;url&lt;/code&gt; 拼接成要返回的 &lt;code&gt;HTML&lt;/code&gt; 文档字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let server = app.listen(3000, function () {
  let host = server.address().address
  let port = server.address().port
  console.log(&apos;Your App is running at http://%s:%s&apos;, host, port)
})

app.get(&apos;/&apos;, (req, res) =&gt; {
  res.setHeader(&apos;Content-Type&apos;, &apos;text/html&apos;)
  res.end(buildTemplate(CSS_std))
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 &lt;code&gt;express&lt;/code&gt; 搭建 &lt;code&gt;HTTP&lt;/code&gt; 服务器，监听 &lt;code&gt;3000&lt;/code&gt; 端口，返回我们拼接完成的 &lt;code&gt;HTML&lt;/code&gt; 文档。当解析完成之后，&lt;code&gt;open(&apos;http://localhost:3000/&apos;);&lt;/code&gt; 会帮我们打开默认浏览器，我们就能看到所有 &lt;code&gt;CSS&lt;/code&gt; 相关的标准链接。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/css-spec-list.BYr-WLuG_ZbXmWb.webp&quot; alt=&quot;css-spec-list&quot; title=&quot;css-spec-list&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;完整代码查看&lt;a href=&quot;https://github.com/Clloz/W3C_crawler&quot; title=&quot;Github&quot;&gt;Github&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844903640268800008&quot; title=&quot;分分钟教你用nodejs写个爬虫&quot;&gt;分分钟教你用nodejs写个爬虫&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jiayisheji/blog/issues/7&quot; title=&quot;十分钟教你撸一个nodejs爬虫系统&quot;&gt;十分钟教你撸一个nodejs爬虫系统&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/crawler.ycHR6NDf.png"/><enclosure url="/_astro/crawler.ycHR6NDf.png"/></item><item><title>选择器匹配元素</title><link>https://clloz.com/blog/selector-match-element</link><guid isPermaLink="true">https://clloz.com/blog/selector-match-element</guid><pubDate>Sun, 09 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;用原生的 &lt;code&gt;JavaScript&lt;/code&gt; 实现给定一个选择器（复合选择器和子选择器，不考虑逗号），和一个元素，判断该元素是否与该选择器匹配。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;对于一个选择器比如 &lt;code&gt;div #myid .class1.class2&lt;/code&gt;，我们应该是从后向前匹配。我们用 &lt;code&gt;String.prototype.split()&lt;/code&gt; 方法将嵌套的选择器放入一个数组，然后从后向前依次进行匹配。最内层的选择器如果匹配失败则直接返回 &lt;code&gt;false&lt;/code&gt;，其他父层级选择器则要依次向上遍历匹配。&lt;/p&gt;
&lt;h2&gt;复合选择器的匹配&lt;/h2&gt;
&lt;p&gt;解决了基本的逻辑，我们需要处理的就是如何将一个复合选择器和元素进行匹配。比如 &lt;code&gt;div#myid.class1.class2&lt;/code&gt; 这样的复合选择器。我的思路是用正则表达式将复合选择器分解为简单选择器，然后和元素的属性进行对比。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function specificity(selector) {
  let reg = /(?&amp;#x3C;tagname&gt;(\w+)?)(?&amp;#x3C;id&gt;(#\w+)?)(?&amp;#x3C;classname&gt;(.[\w.]+)?)/
  let result = selector.match(reg)
  return result.groups
}
console.log(specificity(&apos;div#myid.class1.class2&apos;))
//[Object: null prototype] {
//  tagname: &apos;div&apos;,
//  id: &apos;#myid&apos;,
//  classname: &apos;.class1.class2&apos;
//}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们利用正则表达式中的分组提取出 &lt;code&gt;tagname&lt;/code&gt;，&lt;code&gt;id&lt;/code&gt; 和 &lt;code&gt;class&lt;/code&gt;。关于正则表达式可以看我的另一篇文章&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/08/05/regex-javascript-apply/&quot; title=&quot;正则表达式的入门和JavaScript中的应用&quot;&gt;正则表达式的入门和JavaScript中的应用&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;将复合选择器转化为简单选择器后，我们要做的就是取得元素的简单选择器然后进行比较。元素的几个对应属性很简单，&lt;code&gt;tagName&lt;/code&gt; 可以用 &lt;code&gt;element.tagName&lt;/code&gt; 直接获得；&lt;code&gt;id&lt;/code&gt; 和 &lt;code&gt;class&lt;/code&gt; 可以通过 &lt;code&gt;element.getAttribute()&lt;/code&gt; 方法获得（形式略有不同，需要处理）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function compare(result, element) {
  if (result.tagname !== &apos;&apos; &amp;#x26;&amp;#x26; element.tagName.toLowerCase() !== result.tagname) {
    return false
  }
  if (result.id !== &apos;&apos; &amp;#x26;&amp;#x26; element.getAttribute(&apos;id&apos;) !== result.id.slice(1)) {
    return false
  }
  if (result.classname !== &apos;&apos;) {
    let classnames = result.classname.split(&apos;.&apos;).filter((val) =&gt; !!val)
    let el_classnames = element.getAttribute(&apos;class&apos;).split(&apos; &apos;)
    let isContain = classnames.every((x) =&gt; el_classnames.includes(x))
    if (!isContain) return false
  }
  return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;最后我们要做的就是循环选择器数组，与当前元素匹配。元素用 &lt;code&gt;while&lt;/code&gt; 向上回溯，直到 &lt;code&gt;element.parentElement&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function match(selector, element) {
  let selectors = selector.split(&apos; &apos;)
  for (let i = selectors.length - 1; i &gt;= 0; i--) {
    let result = specificity(selectors[i])

    if (i === selectors.length - 1) {
      if (!compare(result, element)) return false
    } else {
      let isMatch = false
      element = element.parentElement
      console.log(element)

      while (element !== null &amp;#x26;&amp;#x26; isMatch === false) {
        console.log(result, element)
        if (compare(result, element)) isMatch = true
        element = element.parentElement
      }
      console.log(isMatch)
      if (!isMatch) return false
    }
  }
  return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;完整代码可以查看&lt;a href=&quot;https://www.clloz.com/study/selector_match_element/index.html&quot;&gt;选择器元素匹配&lt;/a&gt;，打开开发者工具查看，&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;匹配机制&lt;/h2&gt;
&lt;p&gt;我们上面说过，元素和选择器的匹配是从后往前（从右往左），这里来简单解释一下为什么。当我们要知道当前的元素是否和某个选择器匹配的时候，比如选择器 &lt;code&gt;#id .class1 .class2&lt;/code&gt;。如果我们从左往右匹配，当我们遇到 &lt;code&gt;#id&lt;/code&gt; 的元素的时候，我们需要检查所有的子元素来寻找 &lt;code&gt;.class1&lt;/code&gt;，这在复杂的文档解构中是非常低效率的。而如果我们从后往前匹配，当我们遇到一个 &lt;code&gt;.class2&lt;/code&gt; 的元素，我们只需要看他的父元素（有些选择器也要看兄弟元素）是否you 匹配 &lt;code&gt;.class1&lt;/code&gt; ，依次向上追溯就可以了。并且如果 &lt;code&gt;.class2&lt;/code&gt; 没有匹配成功我们就可以直接确定这个元素不符合。&lt;/p&gt;
&lt;p&gt;浏览器中的 &lt;code&gt;CSS&lt;/code&gt; 是如何计算的可以参考这篇文章&lt;a href=&quot;https://zhuanlan.zhihu.com/p/25380611&quot; title=&quot;从Chrome源码看浏览器如何计算CSS&quot;&gt;从Chrome源码看浏览器如何计算CSS&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/browser.gq-LwpuH.jpg"/><enclosure url="/_astro/browser.gq-LwpuH.jpg"/></item><item><title>JavaScript Function 的知识点整理</title><link>https://clloz.com/blog/es6-function</link><guid isPermaLink="true">https://clloz.com/blog/es6-function</guid><pubDate>Wed, 05 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文对 &lt;code&gt;JavaScript&lt;/code&gt; 中的 &lt;code&gt;Function&lt;/code&gt; 的一些比较容易忽略的知识点和新特性做一个整理。&lt;/p&gt;
&lt;h2&gt;new Function()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Function&lt;/code&gt; 构造函数创建一个新的 &lt;code&gt;Function&lt;/code&gt; 对象。直接调用此构造函数可用动态创建函数，但会遇到和 &lt;code&gt;eval&lt;/code&gt; 类似的的安全问题和(相对较小的)性能问题。然而，与 &lt;code&gt;eval&lt;/code&gt; 不同的是，&lt;code&gt;Function&lt;/code&gt; 创建的函数只能在全局作用域中运行。它的用法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const sum = new Function(&apos;a&apos;, &apos;b&apos;, &apos;return a + b&apos;) //注意，无论是参数标识符还是函数体，都是字符串，new 可以省略

console.log(sum(2, 6))
// expected output: 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然一般我们不太会使用这种方式来构建函数，不过还是需要了解一下，很多框架都有使用 &lt;code&gt;new Function&lt;/code&gt; 或者 &lt;code&gt;eval&lt;/code&gt; 来实现一些功能。关于 &lt;code&gt;eval&lt;/code&gt; 和 &lt;code&gt;new Function()&lt;/code&gt;，我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/16/ydnjs-note-1/#i-4&quot; title=&quot;YDNJS学习笔记-上卷-第一部分&quot;&gt;YDNJS学习笔记-上卷-第一部分&lt;/a&gt; 有做详细的阐述。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Function&lt;/code&gt; 构造函数和 &lt;code&gt;eval&lt;/code&gt; 最重要的区别是由 &lt;code&gt;Function&lt;/code&gt; 构造器创建的函数不会创建当前环境的闭包，它们总是被创建于全局环境，因此在运行时它们只能访问全局变量和自己的局部变量，不能访问 &lt;code&gt;Function&lt;/code&gt; 构造器创建时所在的作用域的变量。这一点与使用 &lt;code&gt;eval&lt;/code&gt; 执行创建函数的代码不同。在平时的编码中不建议使用，它无法享受引擎的基于词法的静态分析带来的优化，并且有影响性能的可能性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function foo(str, a) {
  Function(str)() //3
  console.log(a, b)
}
var b = 2
foo(&apos;var b = 3;console.log(b)&apos;, 1) // 1, 2

function foo(str, a) {
  eval(str) // 此处执行的代码声明了一个新的变量，改变了当前环境的词法作用域
  console.log(a, b)
}
var b = 2
foo(&apos;var b = 3;&apos;, 1) // 1, 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Function.name&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Function.name&lt;/code&gt; 在 &lt;code&gt;ES6&lt;/code&gt; 被写入标准。它的主要功能就是返回函数名，在 &lt;code&gt;ES6&lt;/code&gt; 之前该属性就存在，不过在 &lt;code&gt;ES6&lt;/code&gt; 才加入标准。在非标准的 &lt;code&gt;ES2015&lt;/code&gt; 之前的实现中，该属性的 &lt;code&gt;configurable&lt;/code&gt; 属性也是 &lt;code&gt;false&lt;/code&gt; ，现在为 &lt;code&gt;true&lt;/code&gt;，也就意味着这是一个可配置属性。&lt;/p&gt;
&lt;p&gt;针对不同方式声明的函数，这个属性会返回同的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 函数声明
function doSomething() {}
doSomething.name // &quot;doSomething&quot;

//Function 构造函数
new Function().name // &quot;anonymous&quot;

//变量和方法可以从句法位置推断匿名函数的名称（ECMAScript 2015中新增），在 ES6 之前匿名函数返回值为空字符串
var f = function () {}
var object = {
  someMethod: function () {}
}
console.log(f.name) // &quot;f&quot;
console.log(object.someMethod.name) // &quot;someMethod&quot;

//简写的对象方法
var o = {
  foo() {}
}
o.foo.name // &quot;foo&quot;;

//绑定函数
function foo() {}
foo.bind({}).name // &quot;bound foo&quot;

//getter 和 setter
var o = {
  get foo() {},
  set foo(x) {}
}

var descriptor = Object.getOwnPropertyDescriptor(o, &apos;foo&apos;)
descriptor.get.name // &quot;get foo&quot;
descriptor.set.name // &quot;set foo&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以在函数表达式中为后面的函数命名，但是这个名称是无法直接使用的，它只是改变了 &lt;code&gt;Function.name&lt;/code&gt; 的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let someMethod = function object_someMethod() {}

console.log(someMethod.name) // &quot;object_someMethod&quot;

console.log(object_someMethod) //ReferenceError: object_someMethod is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Function.name&lt;/code&gt; 是只读的，不可修改，要更改它只能通过 &lt;code&gt;Object.defineProperty()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var object = {
  // anonymous
  someMethod: function () {}
}

object.someMethod.name = &apos;otherMethod&apos;
console.log(object.someMethod.name) // someMethod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意，解释器只有在函数没有设置 name 属性的时候才会设置内置的 Function.name，并且 ES2015 规定静态方法也被认为是类的属性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们能取到一个函数的函数名 &lt;code&gt;Function.name&lt;/code&gt;，前提是我们没有手动为这个函数设置 &lt;code&gt;name&lt;/code&gt; 属性。由于类本质就是一个构造函数，所以我们也可以取到类的 &lt;code&gt;name&lt;/code&gt;，我们可以通过实例获取构造函数的 &lt;code&gt;name&lt;/code&gt;，方法就是 &lt;code&gt;instance.constructor.name&lt;/code&gt;。但是如果我没在类里面定义了一个名叫 &lt;code&gt;name&lt;/code&gt; 的静态方法，那么我们访问 &lt;code&gt;Function.name&lt;/code&gt; 得到的就是这个方法。&lt;/p&gt;
&lt;p&gt;如果函数名是 &lt;code&gt;Symbol&lt;/code&gt;，那么 &lt;code&gt;Function.name&lt;/code&gt; 将返回 &lt;code&gt;Symbol&lt;/code&gt; 的描述符。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var sym1 = Symbol(&apos;foo&apos;)
var sym2 = Symbol()
var o = {
  [sym1]: function () {},
  [sym2]: function () {}
}

o[sym1].name // &quot;[foo]&quot;
o[sym2].name // &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一点需要注意的是，代码压缩的过程中，我们的函数名很可能被改写，所以如果你使用了 &lt;code&gt;Function.name&lt;/code&gt; 要确保函数名没有被构建工具修改。&lt;/p&gt;
&lt;h2&gt;Function.length&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;length&lt;/code&gt; 属性指明函数的形参个数。形参的数量不包括剩余参数个数，仅包括第一个具有默认值之前的参数个数。与之对比的是， &lt;code&gt;arguments.length&lt;/code&gt; 是函数被调用时实际传参的个数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function a(m, n = &apos;a&apos;, b) {
  console.log(a.length) //1
  console.log(arguments.length) //3
  console.log(m, n, b) //1 a 2
}

a(1, undefined, 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;小知识：&lt;code&gt;Function&lt;/code&gt; 构造器本身也是个 &lt;code&gt;Function&lt;/code&gt;。他的 &lt;code&gt;length&lt;/code&gt; 属性值为 &lt;code&gt;1&lt;/code&gt; 。该属性 &lt;code&gt;Writable: false, Enumerable: false, Configurable: true&lt;/code&gt;。&lt;code&gt;Function.prototype&lt;/code&gt; 对象也是一个函数，它的 &lt;code&gt;length&lt;/code&gt; 属性值为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;call，apply 和 bind&lt;/h2&gt;
&lt;p&gt;这部分内容参考 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/07/simulation-of-call-apply-bind/#_bind&quot; title=&quot;模拟实现call，apply 和 bind&quot;&gt;模拟实现call，apply 和 bind&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;箭头函数&lt;/h2&gt;
&lt;p&gt;箭头函数表达式的语法比函数表达式更简洁，并且没有自己的 &lt;code&gt;this&lt;/code&gt;，&lt;code&gt;arguments&lt;/code&gt;，&lt;code&gt;prototype&lt;/code&gt;，&lt;code&gt;super&lt;/code&gt; 或 &lt;code&gt;new.target&lt;/code&gt;。箭头函数表达式更适用于那些本来需要匿名函数的地方，并且它不能用作构造函数，也不能用作生成器函数。引入箭头函数有两个方面的作用：更简短的函数并且不绑定 &lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;箭头函数不会创建自己的 &lt;code&gt;this&lt;/code&gt;,它只会从自己的作用域链的上一层继承 &lt;code&gt;this&lt;/code&gt;。下面列出一些箭头函数注意点，更多关于箭头函数的内容可以参考 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/30/js-this/#i-5&quot; title=&quot;JavaScript 中的 this 指向&quot;&gt;JavaScript 中的 this 指向&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对象的方法：对象的方法如果使用箭头函数则箭头函数中的 &lt;code&gt;this&lt;/code&gt; 指向的是对象所在环境的 &lt;code&gt;this&lt;/code&gt;。如果是在全局环境中创建的对象，&lt;code&gt;this&lt;/code&gt; 指向全局对象 &lt;code&gt;window&lt;/code&gt;。如果实在 &lt;code&gt;node&lt;/code&gt; 模块中则指向 &lt;code&gt;module.exports&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;原型上的方法逻辑也和上面一样，不过要注意一点，在 &lt;code&gt;class&lt;/code&gt; 中定义方法如果使用箭头函数的话，这个函数会被 &lt;code&gt;babel&lt;/code&gt; 转换到构造函数中。结合上面一点，不要在对象的方法或类方法中使用箭头函数。&lt;/li&gt;
&lt;li&gt;箭头函数的 &lt;code&gt;this&lt;/code&gt; 并不是不会变的，只是它确定指向它所在环境的 &lt;code&gt;this&lt;/code&gt;，这个环境可能会变化。&lt;/li&gt;
&lt;li&gt;箭头函数不能作为构造函数。&lt;/li&gt;
&lt;li&gt;箭头函数没有自己的 &lt;code&gt;this&lt;/code&gt;，&lt;code&gt;arguments&lt;/code&gt;，&lt;code&gt;super&lt;/code&gt; 或 &lt;code&gt;new.target&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;箭头函数不能作为生成器函数。&lt;/li&gt;
&lt;li&gt;由于箭头函数没有自己的 &lt;code&gt;this&lt;/code&gt; 指针，通过 &lt;code&gt;bind()&lt;/code&gt;，&lt;code&gt;call()&lt;/code&gt; 或 &lt;code&gt;apply()&lt;/code&gt; 方法调用一个函数时，只能传递参数（不能绑定 &lt;code&gt;this&lt;/code&gt;），他们的第一个参数会被忽略。&lt;/li&gt;
&lt;li&gt;箭头函数没有 &lt;code&gt;prototype&lt;/code&gt; 属性。&lt;/li&gt;
&lt;li&gt;箭头函数在参数和箭头之间不能换行。&lt;/li&gt;
&lt;li&gt;箭头函数中的箭头不是运算符，但箭头函数具有与常规函数不同的特殊运算符优先级解析规则。&lt;/li&gt;
&lt;li&gt;严格模式下函数中的 &lt;code&gt;this&lt;/code&gt; 不能指向全局对象，如果箭头函数的 &lt;code&gt;this&lt;/code&gt; 指向全局对象，会返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;函数参数的逗号&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES2017&lt;/code&gt; 允许函数的最后一个参数有尾逗号（&lt;code&gt;trailing comma&lt;/code&gt;）。这样主要是为了以后添加参数或者修改参数的顺序提供方便，和数组对象的逗号行为保持一致。&lt;/p&gt;
&lt;h2&gt;Funtion.prototype.toString()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES2019&lt;/code&gt; 对函数实例的 &lt;code&gt;toString()&lt;/code&gt; 方法做出了修改。&lt;code&gt;toString()&lt;/code&gt; 方法返回函数代码本身，以前会省略注释和空格。修改后的 &lt;code&gt;toString()&lt;/code&gt; 方法，明确要求返回一模一样的原始代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function /* foo comment */ foo() {}

foo.toString()
// &quot;function /* foo comment */ foo () {}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;catch 可省略参数&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 语言的 &lt;code&gt;try...catch&lt;/code&gt; 结构，以前明确要求 &lt;code&gt;catch&lt;/code&gt; 命令后面必须跟参数，接受 &lt;code&gt;try&lt;/code&gt; 代码块抛出的错误对象。 很多时候，&lt;code&gt;catch&lt;/code&gt; 代码块可能用不到这个参数。但是，为了保证语法正确，还是必须写。&lt;code&gt;ES2019&lt;/code&gt; 做出了改变，允许 &lt;code&gt;catch&lt;/code&gt; 语句省略参数。&lt;/p&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>正则表达式入门以及JavaScript中的应用</title><link>https://clloz.com/blog/regex-javascript-apply</link><guid isPermaLink="true">https://clloz.com/blog/regex-javascript-apply</guid><pubDate>Wed, 05 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;字符串的操作是我们在写程序中经常遇到的问题，有时我们会遍历字符串以及用已有的 &lt;code&gt;String&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt; 来达到我们的目的。但处理字符串最强大的还是正则表达式。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串，我们可以用正则表达式找出字符串中的目标子串，来进行操作。几乎所有的和字符串相关的操作都离不开正则表达式，比如大部分编辑器现在都能够用正则表达式来检索文本，大部分编程语言都有内置了强大的正则引擎，我们所用的很多字符串的内置 &lt;code&gt;API&lt;/code&gt;，其背后也是正则表达式。本文主要讲一讲正则表达式的入门和使用。正则表达式全称是 &lt;code&gt;regular expression&lt;/code&gt;，一般简称 &lt;code&gt;regex&lt;/code&gt; 或者 &lt;code&gt;regexp&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;正则引擎的工作原理需要理解 &lt;code&gt;DFA&lt;/code&gt; 和 &lt;code&gt;NFA&lt;/code&gt; 需要一定的离散数学和编译原理基础，如果你想要实现一个正则表达式引擎，可以深入了解，本文不做介绍。对于集合、关系以及 &lt;code&gt;DFA&lt;/code&gt; 和 &lt;code&gt;NFA&lt;/code&gt; 可以看《计算理论基础》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;p&gt;最简单的正则表达式就是直接匹配对应的字符，比如 &lt;code&gt;abc&lt;/code&gt; 就可以直接匹配。为了实现更为复杂的模式，正则表达式提供了很多元字符来表达丰富的意义。查看元字符可以看&lt;a href=&quot;https://docs.microsoft.com/zh-cn/dotnet/standard/base-types/regular-expression-language-quick-reference?redirectedfrom=MSDN&quot; title=&quot;MSDN-正则表达式语言 - 快速参考&quot;&gt;MSDN-正则表达式语言 - 快速参考&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是不同语言的正则表达式实现并不完全相同，特别是一些高级特性的支持，比如平衡组等，使用之前查阅一下相关文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我用 &lt;code&gt;xmind&lt;/code&gt; 制作了一个比较全的正则表达式常用的语法结构图，下面是导出的 &lt;code&gt;PNG&lt;/code&gt;，你也可以下载&lt;a href=&quot;https://img.clloz.com/blog/writing/regular-expression.xmind&quot; title=&quot;regular-expression.xmind&quot;&gt;regular-expression.xmind&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/regular-expression.D_VTJyop_8WRd4.webp&quot; alt=&quot;regular-expression&quot; title=&quot;regular-expression&quot;&gt;&lt;/p&gt;
&lt;p&gt;正则表达式的常用语法基本都在其中了，如果你下载了 &lt;code&gt;xmind&lt;/code&gt;，大部分规则都有一个 &lt;code&gt;note&lt;/code&gt; 写了一个示例。关于语法方面有一个表里面没有提到的问题就是转义问题，需要转义的符号一般情况下是 &lt;code&gt;* . ? + $ ^ [ ] ( ) { } | \ /&lt;/code&gt; (如果启用了 &lt;code&gt;x&lt;/code&gt; 模式， 忽略正则表达式模式中的非转义空白，那么 &lt;code&gt;#&lt;/code&gt; 也需要转义)。而在字符组 &lt;code&gt;[]&lt;/code&gt; 中情况又不太一样，在字符组中需要转义的有 &lt;code&gt;[ ] \&lt;/code&gt;，还有处于字符组第一位表示取反的 &lt;code&gt;^&lt;/code&gt; 以及类似 &lt;code&gt;[0-9a-z]&lt;/code&gt; 中间的 &lt;code&gt;-&lt;/code&gt; 是需要转义的，其他不需要。字符组中的 &lt;code&gt;.&lt;/code&gt; 就表示 &lt;code&gt;.&lt;/code&gt; 不表示除换行符以外的任意字符，&lt;code&gt;\b&lt;/code&gt; 也不表示单词边界，而表示退格符。(这里为个人测试，不同的实现也可能不同，如有错误欢迎指正)&lt;/p&gt;
&lt;p&gt;图中的字符转义部分和 &lt;code&gt;\cX&lt;/code&gt; 模式(&lt;code&gt;X&lt;/code&gt; 取值从 &lt;code&gt;A&lt;/code&gt; 到 &lt;code&gt;Z&lt;/code&gt; 用来表示 &lt;code&gt;ASCII&lt;/code&gt; 的 &lt;code&gt;1-26&lt;/code&gt; 个控制符或 &lt;code&gt;Ctrl+X&lt;/code&gt;。在 &lt;code&gt;Java&lt;/code&gt; 中必须大写，其他语言中大小写都可以)可以查看&lt;a href=&quot;https://www.regular-expressions.info/nonprint.html&quot; title=&quot;Non-Printable Characters&quot;&gt;Non-Printable Characters&lt;/a&gt;，他们都是对 &lt;code&gt;ASCII&lt;/code&gt; 码控制符(&lt;code&gt;0-31&lt;/code&gt;位位控制符，可以查看&lt;a href=&quot;https://www.w3schools.com/charsets/ref_html_ascii.asp&quot; title=&quot;ASCII&quot;&gt;ASCII&lt;/a&gt;)的一些匹配。&lt;/p&gt;
&lt;p&gt;还有一点需要说的是 &lt;code&gt;Unicode的匹配&lt;/code&gt;，主要就是几种用法。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;\0 nn&lt;/code&gt; 首位为 &lt;code&gt;0&lt;/code&gt; 的八进制表示，可以表示扩展 &lt;code&gt;ASCII&lt;/code&gt; 码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\x nn&lt;/code&gt; 两位十六进制数表示扩展 &lt;code&gt;ASCII&lt;/code&gt; 码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\u nnnn&lt;/code&gt; 四位十六进制数表示 &lt;code&gt;Unicode&lt;/code&gt; 基础平面 &lt;code&gt;BMP&lt;/code&gt; 的码点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\p{name}&lt;/code&gt; 用&lt;a href=&quot;https://www.php.net/manual/zh/regexp.reference.unicode.php&quot; title=&quot;Unicode字符属性&quot;&gt;Unicode字符属性&lt;/a&gt;来匹配。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;各个语言、平台或者库都有不同的实现，比如 &lt;code&gt;JavaScript&lt;/code&gt; 中的 &lt;code&gt;\u{}&lt;/code&gt; 来表示 &lt;code&gt;Unicode&lt;/code&gt; 码点。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;正则表达式的运算符优先级&lt;/p&gt;
&lt;p&gt;| 运算符                        | 描述                                                                                                                                                           |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| &lt;code&gt;\&lt;/code&gt;                           | 转义符                                                                                                                                                         |
| &lt;code&gt;(), (?:), (?=), []&lt;/code&gt;          | 圆括号和方括号                                                                                                                                                 |
| &lt;code&gt;*, +, ?, {n}, {n,}, {n,m}&lt;/code&gt;   | 限定符                                                                                                                                                         |
| &lt;code&gt;^, $, \&lt;/code&gt;任何元字符、任何字符 | 定位点和序列（即：位置和顺序）                                                                                                                                 |
| &lt;code&gt;\|&lt;/code&gt;                          | 替换，&lt;code&gt;或&lt;/code&gt; 操作字符具有高于替换运算符的优先级，使得 &lt;code&gt;m\|food&lt;/code&gt; 匹配 &lt;code&gt;m&lt;/code&gt; 或 &lt;code&gt;food&lt;/code&gt; 。若要匹配 &lt;code&gt;mood&lt;/code&gt; 或 &lt;code&gt;food&lt;/code&gt; ，请使用括号创建子表达式，从而产生 &lt;code&gt;(m\|f)ood&lt;/code&gt; 。 |&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我自己觉得正则表达式的规则并不难理解，但是如何搭配使用这些规则则是需要大量的练习的，如果能够自己实现一个正则表达式引擎那就掌握得更透彻了。&lt;/p&gt;
&lt;p&gt;这里给大家推荐一些学习资料。一个是我用来检验正则表达式的工具&lt;a href=&quot;https://www.apptorium.com/expressions&quot; title=&quot;expression-setapp&quot;&gt;expression-setapp&lt;/a&gt;，然后是 &lt;code&gt;《精通正则表达式》&lt;/code&gt; 这本书，最后是一个正则表达式可视化的在线工具&lt;a href=&quot;https://regexper.com/&quot; title=&quot;regexper&quot;&gt;regexper&lt;/a&gt;。关于网络上的正则表达式教程，比如比较出名的&lt;a href=&quot;https://deerchao.cn/tutorials/regex/regex.htm#metacode&quot; title=&quot;正则表达式30分钟入门教程&quot;&gt;正则表达式30分钟入门教程&lt;/a&gt;或者&lt;a href=&quot;https://www.runoob.com/regexp/regexp-tutorial.html&quot; title=&quot;正则表达式-菜鸟教程&quot;&gt;正则表达式-菜鸟教程&lt;/a&gt;用来入门都是可以的。&lt;code&gt;Github&lt;/code&gt; 上有一个&lt;a href=&quot;https://github.com/ziishaned/learn-regex/blob/master/translations/README-cn.md&quot; title=&quot;learn-regex&quot;&gt;learn-regex&lt;/a&gt;的教程也是不错的。&lt;/p&gt;
&lt;h2&gt;JavaScript 中的正则表达式&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中正则表达式语法和上面的表中没有太大不同，语法部分就不介绍了，可以查看&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions&quot; title=&quot;正则表达式 - MDN&quot;&gt;正则表达式 - MDN&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这里主要说一下 &lt;code&gt;JavaScript&lt;/code&gt; 中和正则表达式相关的两个对象 &lt;code&gt;RegExp&lt;/code&gt; 和 &lt;code&gt;String&lt;/code&gt;。这两个对象的属性和方法可以参考另一篇文章：&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/10/built-in-objects-api/&quot; title=&quot;JavaScript常用内置对象API&quot;&gt;JavaScript常用内置对象API&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;RegExp&lt;/h2&gt;
&lt;p&gt;创建正则表达式的方法有三种，字面量或者构造函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;;/ab+c/i
new RegExp(&apos;ab+c&apos;, &apos;i&apos;)
new RegExp(/ab+c/, &apos;i&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;RegExp&lt;/code&gt; 对象有很多属性方法，比如获取 &lt;code&gt;flag&lt;/code&gt; 相关的就有多个（&lt;code&gt;JavaScript&lt;/code&gt; 的正则引擎一共支持 &lt;code&gt;gimsuy&lt;/code&gt; 共六个 &lt;code&gt;flag&lt;/code&gt;)，还有一些在 &lt;code&gt;ES5&lt;/code&gt; 之前没有暴露给用户的属性方法（用 &lt;code&gt;symbol&lt;/code&gt; 来指向这些内部方法），&lt;code&gt;ES6&lt;/code&gt; 中我们可以通过内部定义的 &lt;code&gt;symbol&lt;/code&gt; 来访问到这些方法，比如 &lt;code&gt;Symbol.hasInstance&lt;/code&gt; 以及大多数和正则表达式相关的方法等，具体细节可以参考&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/10/built-in-objects-api/&quot; title=&quot;JavaScript常用内置对象API&quot;&gt;JavaScript常用内置对象API&lt;/a&gt;。属性中我们需要特别记得的一个就是静态属性 &lt;code&gt;RegExp.lastIndex&lt;/code&gt;，每一个 &lt;code&gt;RegExp&lt;/code&gt; 对象都有该属性，他表示下次匹配的起始索引，默认值为 &lt;code&gt;0&lt;/code&gt;，这个属性是可读写的，只有正则表达式使用了表示全局检索的 &lt;code&gt;g&lt;/code&gt; 标志时，该属性才会起作用（该属性的具体规则参考&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex&quot; title=&quot;RegExp.lastIndex - MDN&quot;&gt;RegExp.lastIndex - MDN&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;而关于 &lt;code&gt;RegExp&lt;/code&gt; 对象我们主要要讨论的方法有两个。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;exec&lt;/code&gt;： 一个在字符串中执行查找匹配的 &lt;code&gt;RegExp&lt;/code&gt; 方法，它返回一个数组（未匹配到则返回 &lt;code&gt;null&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test&lt;/code&gt;： 一个在字符串中测试是否匹配的 &lt;code&gt;RegExp&lt;/code&gt; 方法，它返回 &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;RegExp.prototype.exec()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;RegExp.prototype.exec()&lt;/code&gt; 接受一个 &lt;code&gt;str&lt;/code&gt; 作为参数，即用正则表达式在这个 &lt;code&gt;str&lt;/code&gt; 中进行匹配。&lt;/p&gt;
&lt;p&gt;在设置了 &lt;code&gt;global&lt;/code&gt; 或 &lt;code&gt;sticky&lt;/code&gt; 标志位的情况下（如 &lt;code&gt;/foo/g or /foo/y&lt;/code&gt;），&lt;code&gt;JavaScript RegExp&lt;/code&gt; 对象是有状态的。他们会将上次成功匹配后的位置记录在 &lt;code&gt;lastIndex&lt;/code&gt; 属性中。使用此特性，&lt;code&gt;exec()&lt;/code&gt; 可用来对单个字符串中的多次匹配结果进行逐条的遍历（包括捕获到的匹配），而相比之下， &lt;code&gt;String.prototype.match()&lt;/code&gt; 只会返回匹配到的结果。如果只是为了判断是否匹配，可以使用 &lt;code&gt;RegExp.test()&lt;/code&gt; 方法，或者 &lt;code&gt;String.search()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;我们重点来看一下 &lt;code&gt;RegExp.prototype.exec()&lt;/code&gt; 的返回值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var re = /quick\s(?&amp;#x3C;group1&gt;brown).+?(?&amp;#x3C;group2&gt;jumps)/gi
console.log(re.lastIndex) //0
var result = re.exec(&apos;The Quick Brown Fox Jumps Over The Lazy Dog&apos;)
console.log(result)

//[
//  &apos;Quick Brown Fox Jumps&apos;,
//  &apos;Brown&apos;,
//  &apos;Jumps&apos;,
//  index: 4,
//  input: &apos;The Quick Brown Fox Jumps Over The Lazy Dog&apos;,
//  groups: { group1: &apos;Brown&apos;, group2: &apos;Jumps&apos; }
//]

console.log(re.lastIndex) //25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的代码我么可以看书，返回值是一个数组，但是包含了三个额外属性 &lt;code&gt;index input groups&lt;/code&gt;。具体含义参看表格。&lt;/p&gt;
&lt;p&gt;| 属性/索引      | 描述                                          | 例子                                                                  |
| -------------- | --------------------------------------------- | --------------------------------------------------------------------- | ---------- |
| &lt;code&gt;[0]&lt;/code&gt;          | 匹配的全部字符串                              | &lt;code&gt;Quick Brown Fox Jumps&lt;/code&gt;                                               |
| &lt;code&gt;[1], ...[n ]&lt;/code&gt; | 括号中的分组捕获                              | &lt;code&gt;[1] = Brown&lt;/code&gt;、&lt;code&gt;[2] = Jumps&lt;/code&gt;                                          |
| &lt;code&gt;index&lt;/code&gt;        | 匹配到的字符位于原始字符串的基于 &lt;code&gt;0&lt;/code&gt; 的索引值 | &lt;code&gt;4&lt;/code&gt;                                                                   |
| &lt;code&gt;input&lt;/code&gt;        | 原始字符串                                    | &lt;code&gt;The Quick Brown Fox Jumps Over The&lt;/code&gt;                                  | &lt;code&gt;Lazy Dog&lt;/code&gt; |
| &lt;code&gt;groups&lt;/code&gt;       | 有命名的分组捕获对象                          | &lt;code&gt;{ group1: &apos;Brown&apos;, group2: &apos;Jumps&apos; }&lt;/code&gt; 若分组都未命名则为 &lt;code&gt;undefined&lt;/code&gt; |&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;当我们使用 &lt;code&gt;g&lt;/code&gt; 标志时，&lt;code&gt;RegExp.prototype.exec()&lt;/code&gt; 执行后会更新正则表达式 &lt;code&gt;lastIndex&lt;/code&gt; 属性。如果我们在匹配成功后再次执行 &lt;code&gt;RegExp.prototype.exec()&lt;/code&gt;，下次匹配将会从 &lt;code&gt;lastIndex&lt;/code&gt; 开始匹配，注意，即使再次查找的字符串不是原查找字符串时，&lt;code&gt;lastIndex&lt;/code&gt; 也不会被重置，它依旧会从记录的 &lt;code&gt;lastIndex&lt;/code&gt; 开始，&lt;code&gt;test()&lt;/code&gt; 也会更新 &lt;code&gt;lastIndex&lt;/code&gt; 属性。我们可以利用这一特性可以查找一个字符串中多个复合正则表达式的子串。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var myRe = /ab*/g
var str = &apos;abbcdefabh&apos;
var myArray
while ((myArray = myRe.exec(str)) !== null) {
  var msg = &apos;Found &apos; + myArray[0] + &apos;. &apos;
  msg += &apos;Next match starts at &apos; + myRe.lastIndex
  console.log(msg)
}
//Found abb. Next match starts at 3
//Found ab. Next match starts at 9
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;不要把正则表达式字面量（或者 &lt;code&gt;RegExp&lt;/code&gt; 构造器）放在 &lt;code&gt;while&lt;/code&gt; 条件表达式里。由于每次迭代时 &lt;code&gt;lastIndex&lt;/code&gt; 的属性都被重置，如果匹配，将会造成一个死循环。并且要确保使用了 &lt;code&gt;g&lt;/code&gt; 标记来进行全局的匹配，否则同样会造成死循环。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;RegExp.prototype.test()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;RegExp.prototype.test()&lt;/code&gt; 方法要比 &lt;code&gt;RegExp.prototype.exec()&lt;/code&gt; 方法简单很多，它只是执行一个检索，用来查看正则表达式与指定的字符串是否匹配。返回 &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;false&lt;/code&gt;。唯一需要注意的是，当使用 &lt;code&gt;g&lt;/code&gt; 标志时，&lt;code&gt;test()&lt;/code&gt; 和 &lt;code&gt;exec()&lt;/code&gt; 一样会改变 &lt;code&gt;lastIndex&lt;/code&gt; 的值，并且多次执行也会越过已经匹配成功的部分，即从字符串的 &lt;code&gt;lastIndex&lt;/code&gt; 索引开始下一次的匹配。&lt;/p&gt;
&lt;h3&gt;RegExp 的 flag&lt;/h3&gt;
&lt;p&gt;介绍以下 &lt;code&gt;JavaScript&lt;/code&gt; 中的正则表达式支持的 &lt;code&gt;flag&lt;/code&gt;。可以用 &lt;code&gt;RegExp.prototype.flags&lt;/code&gt; 查看正则表达式对象使用的 &lt;code&gt;flag&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;| 标志 | 对应属性                      | 描述                                                      |
| ---- | ----------------------------- | --------------------------------------------------------- |
| &lt;code&gt;g&lt;/code&gt;  | &lt;code&gt;RegExp.prototype.global&lt;/code&gt;     | 全局搜索。                                                |
| &lt;code&gt;i&lt;/code&gt;  | &lt;code&gt;RegExp.prototype.ignoreCase&lt;/code&gt; | 不区分大小写搜索。                                        |
| &lt;code&gt;m&lt;/code&gt;  | &lt;code&gt;RegExp.prototype.multiline&lt;/code&gt;  | 多行搜索。                                                |
| &lt;code&gt;s&lt;/code&gt;  | &lt;code&gt;RegExp.prototype.dotAll&lt;/code&gt;     | 允许 . 匹配换行符。                                       |
| &lt;code&gt;u&lt;/code&gt;  | &lt;code&gt;RegExp.prototype.unicode&lt;/code&gt;    | 使用 &lt;code&gt;unicode&lt;/code&gt; 码的模式进行匹配。                         |
| &lt;code&gt;y&lt;/code&gt;  | &lt;code&gt;RegExp.prototype.sticky&lt;/code&gt;     | 执行“粘性(&lt;code&gt;sticky&lt;/code&gt;)”搜索,匹配从目标字符串的当前位置开始。 |&lt;/p&gt;
&lt;p&gt;需要特别说明的是 &lt;code&gt;u&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt;。&lt;code&gt;u&lt;/code&gt; 表示的是 &lt;code&gt;unicode&lt;/code&gt;，使用这个标志，意味着支持 &lt;code&gt;\u{}&lt;/code&gt; 形式的 &lt;code&gt;unicode&lt;/code&gt; 表示，即非基本平面的 &lt;code&gt;unicode&lt;/code&gt; 也能够被支持。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let regex = new RegExp(&apos;\u{1D306}&apos;, &apos;u&apos;)
let result = regex.test(&apos;𝌆&apos;)
console.log(regex.unicode) // true
console.log(result) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 &lt;code&gt;y&lt;/code&gt; 表示的是 &lt;code&gt;stciky&lt;/code&gt;，使用这个标志意味着，要匹配成功，子串必须是从 &lt;code&gt;lastIndex&lt;/code&gt; 开始的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let str = &apos;#foo#foo&apos;
let regex = /foo/y

regex.lastIndex = 1
console.log(regex.test(str)) // true
regex.lastIndex = 2
console.log(regex.test(str)) // false
console.log(regex.lastIndex) // 匹配失败，lastIndex归零
regex.lastIndex = 5
console.log(regex.test(str)) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码设置了三个 &lt;code&gt;lastIndex&lt;/code&gt; 分别进行了匹配，&lt;code&gt;1&lt;/code&gt; 和 &lt;code&gt;5&lt;/code&gt; 分别位于两个 &lt;code&gt;f&lt;/code&gt; 的索引，自然能匹配成功。但是 &lt;code&gt;2&lt;/code&gt; 然后后面还有能够成功匹配的 &lt;code&gt;foo&lt;/code&gt; ，但是在 &lt;code&gt;y&lt;/code&gt; 模式下返回 &lt;code&gt;false&lt;/code&gt;（去掉 &lt;code&gt;y&lt;/code&gt; 将返回 &lt;code&gt;true&lt;/code&gt;）。若匹配失败则 &lt;code&gt;lastIndex&lt;/code&gt; 归零。&lt;/p&gt;
&lt;h3&gt;替代匹配属性&lt;/h3&gt;
&lt;p&gt;在正则表达式语法结构图的左下角有一块是替代，在 &lt;code&gt;JavaScript&lt;/code&gt; 中也支持，但是是非标准的，不过大部分浏览器都进行了实现。兼容性请查看&lt;a href=&quot;https://caniuse.com/#search=regexp.&quot; title=&quot;Can I use&quot;&gt;Can I use&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;支持的几个属性分别是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;RegExp.$1-$9 // 捕获括号分组，上线九个，只读属性，且只在成功匹配时有效。
RegExp.input ($_) // 返回整个输入字符串，括号中为别名。
RegExp.lastMatch ($&amp;#x26;) // 静态只读属性，只在匹配成功时改变。返回最后匹配成功的子字符串，$&amp;#x26;为别名，必须用方括号访问 RegExp[&apos;$&amp;#x26;&apos;]。
RegExp.lastParen ($+) // 静态只读属性，只在匹配成功时改变。返回最后匹配到的括号分组，别名也要用方括号访问。
RegExp.leftContext ($`) // 静态只读属性，只在匹配成功时改变。返回最新匹配的左侧子串，别名也要用方括号访问
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;String&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt; 的方法中和正则表达式相关的有如下几个。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;String.prototype.match()&lt;/code&gt;: 一个在字符串中执行查找匹配的 &lt;code&gt;String&lt;/code&gt; 方法，它返回一个数组，在未匹配到时会返回 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String.prototype.matchAll()&lt;/code&gt;: 一个在字符串中执行查找所有匹配的 &lt;code&gt;String&lt;/code&gt; 方法，它返回一个迭代器（&lt;code&gt;iterator&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String.prototype.search()&lt;/code&gt;： 一个在字符串中测试匹配的 &lt;code&gt;String&lt;/code&gt; 方法，它返回匹配到的位置索引，或者在失败时返回 &lt;code&gt;-1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String.prototype.replace()&lt;/code&gt;: 一个在字符串中执行查找匹配的 &lt;code&gt;String&lt;/code&gt; 方法，并且使用替换字符串替换掉匹配到的子字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String.prototype.split()&lt;/code&gt;: 一个使用正则表达式或者一个固定字符串分隔一个字符串，并将分隔后的子字符串存储到数组中的 &lt;code&gt;String&lt;/code&gt; 方法。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;String.prototype.match()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;String.prototype.match()&lt;/code&gt; 内部调用的是 &lt;code&gt;RegExp.prototype[@@match]()&lt;/code&gt;，接受一个正则表达式对象作为参数，如果传入一个非正则表达式对象，则会隐式地使用 &lt;code&gt;new RegExp(obj)&lt;/code&gt; 将其转换为一个 &lt;code&gt;RegExp&lt;/code&gt; 。如果你没有给出任何参数并直接使用 &lt;code&gt;match()&lt;/code&gt; 方法 ，你将会得到一 个包含空字符串的 &lt;code&gt;Array ：[&quot;&quot;]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;详细说一下返回值：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果使用 &lt;code&gt;g&lt;/code&gt; 标志，那么返回所有匹配正则表达式的子串的数组。不返回捕获组。&lt;/li&gt;
&lt;li&gt;如果没有使用 &lt;code&gt;g&lt;/code&gt; 标志，则返回包含第一个完整匹配及其相关的捕获组的数组。数组有三个额外属性 &lt;code&gt;index input groups&lt;/code&gt;（和 &lt;code&gt;RegExp.prototype.exec()&lt;/code&gt; 相同）。&lt;/li&gt;
&lt;li&gt;如果匹配失败则返回 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们可以看出如果没有 &lt;code&gt;g&lt;/code&gt; 标志，&lt;code&gt;String.prototype.match()&lt;/code&gt; 和 &lt;code&gt;RegExp.prototype.exec()&lt;/code&gt; 是相同的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//without g flag
var str = &apos;For more information, see Chapter 3.4.5.1&apos;
var re = /see (?&amp;#x3C;group1&gt;chapter \d+(?&amp;#x3C;group2&gt;\.\d)*)/i
var found = str.match(re)

console.log(found)
//[
//  &apos;see Chapter 3.4.5.1&apos;,
//  &apos;Chapter 3.4.5.1&apos;,
//  &apos;.1&apos;,
//  index: 22,
//  input: &apos;For more information, see Chapter 3.4.5.1&apos;,
//  groups:{ group1: &apos;Chapter 3.4.5.1&apos;, group2: &apos;.1&apos; }
//]

//with g flag
var str = &apos;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&apos;
var regexp = /[A-E]/gi
var matches_array = str.match(regexp)

console.log(matches_array) // [&apos;A&apos;, &apos;B&apos;, &apos;C&apos;, &apos;D&apos;, &apos;E&apos;, &apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;, &apos;e&apos;]

// no arguments
var str = &apos;Nothing will come of nothing.&apos;

str.match() // returns [&quot;&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;String.prototype.matchAll()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;String.prototype.matchAll()&lt;/code&gt; 内部调用的是 &lt;code&gt;RegExp.prototype[@@matchAll]()&lt;/code&gt;。接受一个正则表达式对象为参数，如果所传参数不是一个正则表达式对象，则会隐式地使用 &lt;code&gt;new RegExp(obj)&lt;/code&gt; 将其转换为一个 &lt;code&gt;RegExp&lt;/code&gt; 。&lt;code&gt;RegExp&lt;/code&gt; 必须是设置了全局模式 &lt;code&gt;g&lt;/code&gt; 的形式，否则会抛出异常 &lt;code&gt;TypeError&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;返回值为一个迭代器，可以配合 &lt;code&gt;for...of...&lt;/code&gt;、扩展运算符 &lt;code&gt;...&lt;/code&gt; 或者 &lt;code&gt;Array.from()&lt;/code&gt; 使用来替代 &lt;code&gt;while&lt;/code&gt; 循环的 &lt;code&gt;RegExp.prototype.exec()&lt;/code&gt; 方法。迭代器中的每一项和 &lt;code&gt;RegExp.prototype.exec()&lt;/code&gt; 是相同的，包括额外属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const regexp = RegExp(&apos;foo[a-z]*&apos;, &apos;g&apos;)
const str = &apos;table football, foosball&apos;
const matches = str.matchAll(regexp)

for (const match of matches) {
  console.log(`Found ${match[0]} start=${match.index} end=${match.index + match[0].length}.`)
}
//Found football start=6 end=14.
//Found foosball start=16 end=24.

console.log(Array.from(str.matchAll(regexp), (m) =&gt; m[0]))
//[ &quot;football&quot;, &quot;foosball&quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;String.prototype.matchAll()&lt;/code&gt; 内部做了一个正则表达式对象的复制，所以不像 &lt;code&gt;RegExp.prototype.exec()&lt;/code&gt;, &lt;code&gt;lastIndex&lt;/code&gt; 在字符串扫描时不会改变。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;String.prototype.matchAll()&lt;/code&gt; 还有一个优势就是它可以一次性获取捕获组，无论是带 &lt;code&gt;g&lt;/code&gt; 的 &lt;code&gt;String.prototype.match()&lt;/code&gt; 还是循环匹配的 &lt;code&gt;RegExp.prototype.exec()&lt;/code&gt; 都不能一次取得所有匹配的捕获组。如果有对应的需求，可以使用 &lt;code&gt;String.prototype.matchAll()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var regexp = /t(e)(st(\d?))/g
var str = &apos;test1test2&apos;

console.log(str.match(regexp)) //[ &apos;test1&apos;, &apos;test2&apos; ]

let array = [...str.matchAll(regexp)]
console.log(array)
//[
//  [
//      &apos;test1&apos;,
//      &apos;e&apos;,
//      &apos;st1&apos;,
//      &apos;1&apos;,
//      index: 0,
//      input: &apos;test1test2&apos;,
//      groups: undefined
//  ],
//  [
//      &apos;test2&apos;,
//      &apos;e&apos;,
//      &apos;st2&apos;,
//      &apos;2&apos;,
//      index: 5,
//      input: &apos;test1test2&apos;,
//      groups: undefined
//  ]
//]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;String.prototype.search()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;String.prototype.search()&lt;/code&gt; 内部使用的是 &lt;code&gt;RegExp.prototype[@@search]()&lt;/code&gt;。接受一个正则表达式对象为参数，如果所传参数不是一个正则表达式对象，则会隐式地使用 &lt;code&gt;new RegExp(obj)&lt;/code&gt; 将其转换为一个 &lt;code&gt;RegExp&lt;/code&gt; 。如果匹配成功，则返回正则表达式在字符串中首次匹配项的索引，否则返回 &lt;code&gt;-1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;类似于 &lt;code&gt;RegExp.prototype.test()&lt;/code&gt; 方法，只是返回值不同。该方法比 &lt;code&gt;String.prototype.match()&lt;/code&gt; 更快，如果不需要知道匹配的详细信息，则建议使用该方法。&lt;/p&gt;
&lt;h3&gt;String.prototype.replace()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;String.prototype.replace()&lt;/code&gt; 内部使用的是 &lt;code&gt;RegExp.prototype[@@replace]()&lt;/code&gt;。该方法返回一个由替换值(&lt;code&gt;replacement&lt;/code&gt;)替换部分或所有的模式(&lt;code&gt;pattern&lt;/code&gt;)匹配项后的新字符串。模式可以是一个字符串或者一个正则表达式，替换值可以是一个字符串或者一个每次匹配都要调用的回调函数。如果 &lt;code&gt;pattern&lt;/code&gt; 是字符串，则仅替换第一个匹配项。&lt;strong&gt;该方法不会改变原字符串&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;正则表达式语法结构图左下角的替换部分的内容在该方法中也是可用的，可以作为第二个参数使用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$$&lt;/code&gt;: 插入一个 &lt;code&gt;$&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$&amp;#x26;&lt;/code&gt;: 插入匹配的子串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$` &lt;/code&gt;: 插入当前匹配的子串左边的内容。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$&apos;&lt;/code&gt;: 插入当前匹配的子串右边的内容。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$n&lt;/code&gt;: 假如第一个参数是 &lt;code&gt;RegExp&lt;/code&gt; 对象，并且 &lt;code&gt;n&lt;/code&gt; 是个小于 &lt;code&gt;100&lt;/code&gt; 的非负整数，那么插入第 &lt;code&gt;n&lt;/code&gt; 个括号匹配的字符串.索引是从 &lt;code&gt;1&lt;/code&gt; 开始。$&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;第二个参数可以是函数，在这种情况下，当匹配执行后，该函数就会执行。 函数的返回值作为替换字符串。 如果第一个参数是正则表达式，并且其为全局匹配模式，那么这个方法将被多次调用，每次匹配都会被调用。该函数有以下参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;match&lt;/code&gt;：匹配的子串。(对应于上述的&lt;code&gt;$&amp;#x26;&lt;/code&gt;。）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p1,p2, ...&lt;/code&gt;: 假如 &lt;code&gt;replace()&lt;/code&gt; 方法的第一个参数是一个 &lt;code&gt;RegExp&lt;/code&gt; 对象，则代表第 &lt;code&gt;n&lt;/code&gt; 个括号匹配的字符串。（对应于上述的&lt;code&gt;$1&lt;/code&gt;，&lt;code&gt;$2&lt;/code&gt;等。）例如，如果是用 &lt;code&gt;/(\a+)(\b+)/&lt;/code&gt; 这个来匹配，&lt;code&gt;p1&lt;/code&gt; 就是匹配的 &lt;code&gt;\a+&lt;/code&gt;，&lt;code&gt;p2&lt;/code&gt; 就是匹配的 &lt;code&gt;\b+&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;offset&lt;/code&gt;: 匹配到的子字符串在原字符串中的偏移量。（比如，如果原字符串是 &lt;code&gt;abcd&lt;/code&gt;，匹配到的子字符串是 &lt;code&gt;bc&lt;/code&gt;，那么这个参数将会是 &lt;code&gt;1&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;string&lt;/code&gt;: 被匹配的原字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NamedCaptureGroup&lt;/code&gt;: 命名捕获组匹配的对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function replacer(match, p1, p2, p3, offset, string) {
  return [p1, p2, p3].join(&apos; - &apos;)
}
var newString = &apos;abc12345#$*%&apos;.replace(/([^\d]*)(\d*)([^\w]*)/, replacer)
console.log(newString) // abc - 12345 - #$*%
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;精确的参数个数依赖于 &lt;code&gt;replace()&lt;/code&gt; 的第一个参数是否是一个正则表达式（&lt;code&gt;RegExp&lt;/code&gt;）对象，以及这个正则表达式中指定了多少个括号子串，如果这个正则表达式里使用了命名捕获， 还会添加一个命名捕获的对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;如果你需要知道一个字符串是否与一个正则表达式匹配，可使用 &lt;code&gt;RegExp.test()&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;如果你只是需要第一个匹配结果，你可以使用 &lt;code&gt;RegExp.prototype.exec()&lt;/code&gt; 或者 &lt;code&gt;String.prototype.match()&lt;/code&gt;，不使用 &lt;code&gt;g&lt;/code&gt; 标志。&lt;/li&gt;
&lt;li&gt;如果你想要获得捕获组，并且设置了全局标志，你需要用 &lt;code&gt;RegExp.exec()&lt;/code&gt; 或者 &lt;code&gt;String.prototype.matchAll()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;http://www.pcre.org/pcre.txt&quot; title=&quot;pcre&quot;&gt;pcre&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F#PCRE%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%85%A8%E9%9B%86&quot; title=&quot;正则表达式 - Wikipedia&quot;&gt;正则表达式 - Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/regex.OG3ssSDB.jpeg"/><enclosure url="/_astro/regex.OG3ssSDB.jpeg"/></item><item><title>开源许可证的选择</title><link>https://clloz.com/blog/open-source-license</link><guid isPermaLink="true">https://clloz.com/blog/open-source-license</guid><pubDate>Tue, 04 Aug 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;开源许可证授予任何人以任何目的使用，修改和共享许可软件的权限，但要遵守保护该软件的来源和开放性的条件。对于常用的开源许可证我们要理解他们的区别才能正确使用。常用的许可证区别可以看下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/open-source-license.BUrjCbON_NJgQ1.webp&quot; alt=&quot;open-source-license&quot; title=&quot;open-source-license&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;github&lt;/code&gt; 有一个&lt;a href=&quot;https://choosealicense.com/&quot; title=&quot;页面&quot;&gt;页面&lt;/a&gt;帮助你选择开源许可证，另外还有一个网站&lt;a href=&quot;https://choosealicense.com/licenses/&quot; title=&quot;choosealicense&quot;&gt;choosealicense&lt;/a&gt;也有主要的许可证介绍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;许可证介绍&lt;/h2&gt;
&lt;p&gt;开源许可证可以分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;宽松式（&lt;code&gt;permissive&lt;/code&gt;）许可证：最基本的类型，对用户几乎没有限制。用户可以修改代码后闭源。用户可以使用代码，做任何想做的事情，不保证代码质量，用户自担风险，用户必须披露原始作者。常见的宽松式许可证有：&lt;code&gt;BSD&lt;/code&gt;，&lt;code&gt;MIT&lt;/code&gt;，&lt;code&gt;Apache 2.0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Copyleft&lt;/code&gt; 许可证：比宽松式许可证严格。如果分发二进制格式，必须提供源码修改后的源码，必须与修改前保持许可证一致，不得在原始许可证以外，附加其他限制。核心就是修改后的代码不可以闭源。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Copyleft&lt;/code&gt; 是理查德·斯托曼发明的一个词，作为 &lt;code&gt;Copyright&lt;/code&gt; （版权）的反义词。&lt;code&gt;Copyright&lt;/code&gt; 直译是&quot;复制权&quot;，这是版权制度的核心，意为不经许可，用户无权复制。作为反义词，&lt;code&gt;Copyleft&lt;/code&gt; 的含义是不经许可，用户可以随意复制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;BSD&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;BSD&lt;/code&gt; 有两个版本，两个版本的共同要求是分发软件时，必须保留原始的许可证声明。另外一个附加条款是不得使用原始作者的名字为软件促销。&lt;/p&gt;
&lt;h2&gt;MIT License&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MIT License&lt;/code&gt; 是一个非常宽松的许可证，你只要保留版权声明和许可声明就可以。你可以用代码做任何事情，包括闭源你的项目。使用这个许可证的项目有 &lt;code&gt;Babel&lt;/code&gt;, &lt;code&gt;.NET Core&lt;/code&gt;, &lt;code&gt;Rails&lt;/code&gt; 和 &lt;code&gt;jQuery&lt;/code&gt; 等。&lt;/p&gt;
&lt;h2&gt;Apache License 2.0&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Apache License&lt;/code&gt; 和 &lt;code&gt;MIT License&lt;/code&gt; 类似，都很宽松，但是他包含了贡献者向用户提供专利授权相关的条款，分发软件时，必须保留原始的许可证声明。凡是修改过的文件，必须向用户说明该文件修改过；没有修改过的文件，必须保持许可证不变。使用该许可证的项目有 &lt;code&gt;Apache&lt;/code&gt;、&lt;code&gt;SVN&lt;/code&gt; 和 &lt;code&gt;NuGet&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;GNU GPLv3&lt;/h2&gt;
&lt;p&gt;许可证。这是一种 &lt;code&gt;Copyleft&lt;/code&gt; 许可证，要求修改项目代码的用户再次分发源码或二进制代码时，必须公布他的相关修改。&lt;/p&gt;
&lt;h2&gt;GNU LGPLv3&lt;/h2&gt;
&lt;p&gt;和 &lt;code&gt;GPL&lt;/code&gt; 的区别是：如果项目采用接口调用该许可证的库，项目可以不用开源。&lt;/p&gt;
&lt;h2&gt;Mozilla Public License 2.0&lt;/h2&gt;
&lt;p&gt;只要该许可证的代码在单独的文件中，新增的其他文件可以不用开源。&lt;/p&gt;
&lt;h2&gt;不添加许可证&lt;/h2&gt;
&lt;p&gt;如果你不添加任何许可证，那么你的代码默认具有专有版权，别人不可以复制、分发和修改。当代码超过一个贡献者时，连你自己也不可以进行复制，分发和修改。&lt;code&gt;Github&lt;/code&gt; 有用户协议，可以让其他用户查看和复制你的代码，但不代表可以用这个代码去做其他的事情。&lt;/p&gt;
&lt;h2&gt;Github 添加许可证&lt;/h2&gt;
&lt;p&gt;想要知道一个 &lt;code&gt;Github&lt;/code&gt; 仓库的许可证，看项目的页面右边的 &lt;code&gt;About&lt;/code&gt; 区域即可（老版的 &lt;code&gt;Github&lt;/code&gt; 在文件列表上方的 &lt;code&gt;tab&lt;/code&gt; 末尾）&lt;/p&gt;
&lt;p&gt;如何为我们的项目添加一个 &lt;code&gt;license&lt;/code&gt; 呢，我们只要点击页面上的 &lt;code&gt;creat new file&lt;/code&gt; 然后文件名填写 &lt;code&gt;LICENSE&lt;/code&gt; （全大写），右边会出现一个 &lt;code&gt;choose a license template&lt;/code&gt; 按钮，点进去选择自己想要的 &lt;code&gt;license&lt;/code&gt; 然后完成文件的创建即可。具体流程参考&lt;a href=&quot;https://docs.github.com/cn/github/building-a-strong-community/adding-a-license-to-a-repository&quot; title=&quot;添加许可到仓库&quot;&gt;添加许可到仓库&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2017/10/open-source-license-tutorial.html&quot; title=&quot;开源许可证教程&quot;&gt;开源许可证教程&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/open-source-heading.gkXjj1ig.jpg"/><enclosure url="/_astro/open-source-heading.gkXjj1ig.jpg"/></item><item><title>ES6 的 Map Set WeakMap 和 WeakSet</title><link>https://clloz.com/blog/es6-map-set-weakmap-weakset</link><guid isPermaLink="true">https://clloz.com/blog/es6-map-set-weakmap-weakset</guid><pubDate>Wed, 29 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文讲一讲 &lt;code&gt;ES6&lt;/code&gt; 新增的两种数据结构，&lt;code&gt;Set&lt;/code&gt; 和 &lt;code&gt;Map&lt;/code&gt; 以及和它们相对应的 &lt;code&gt;WeakSet&lt;/code&gt; 和 &lt;code&gt;WeakMap&lt;/code&gt;。&lt;code&gt;Map&lt;/code&gt; 和 &lt;code&gt;Set&lt;/code&gt; 是两种非常中要的数据结构，在其他语言中都是一种预设机制。&lt;code&gt;JavaScript&lt;/code&gt; 在 &lt;code&gt;ES6&lt;/code&gt; 中也引入了这种数据结构。它们都有对应的数学概念，&lt;code&gt;Map&lt;/code&gt; 表示的是映射，而 &lt;code&gt;Set&lt;/code&gt; 表示的是集合，其实理清这两个概念也就大致了解了 &lt;code&gt;Map&lt;/code&gt; 和 &lt;code&gt;Set&lt;/code&gt; 是什么样的结构了，&lt;code&gt;Map&lt;/code&gt; 是一种双射形式的映射，集合的概念就不同多接受了，三大特性：确定性，互异性，无序性。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;ES6&lt;/code&gt; 之前，我们能使用的主要数据结构就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存储带键的数据（&lt;code&gt;keyed&lt;/code&gt;）集合的对象。&lt;/li&gt;
&lt;li&gt;存储有序集合的数组。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实在底层的实现中，它们都是 &lt;code&gt;Map&lt;/code&gt; 这种数据结构。下面我们来讲一讲这个新的 &lt;code&gt;Map&lt;/code&gt; 和 &lt;code&gt;Set&lt;/code&gt; 具体有哪些不同。&lt;/p&gt;
&lt;h2&gt;Set&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Set&lt;/code&gt; 对象允许你存储任何类型的唯一值，无论是原始值或者是对象引用。&lt;code&gt;Set&lt;/code&gt; 对象是值的集合，你可以按照插入的顺序迭代它的元素。 &lt;code&gt;Set&lt;/code&gt; 中的元素只会出现一次，即 &lt;code&gt;Set&lt;/code&gt; 中的元素是唯一的。&lt;/p&gt;
&lt;p&gt;创建 &lt;code&gt;Set&lt;/code&gt; 对象的方法是 &lt;code&gt;new Set([iterable])&lt;/code&gt;，&lt;code&gt;new&lt;/code&gt; 是必须的，&lt;code&gt;Set&lt;/code&gt; 构造函数可以接受一个数组(或者具有 &lt;code&gt;iterable&lt;/code&gt; 接口的其他数据结构)作为参数， 用来初始化，它的所有元素将不重复地被添加到新的 &lt;code&gt;Set&lt;/code&gt; 中。如果不指定此参数或其值为 &lt;code&gt;null&lt;/code&gt;，则新的 &lt;code&gt;Set&lt;/code&gt; 为空。&lt;/p&gt;
&lt;p&gt;向 &lt;code&gt;Set&lt;/code&gt; 加入值的时候，不会发生类型转换，所以 &lt;code&gt;5&lt;/code&gt; 和 &lt;code&gt;&quot;5&quot;&lt;/code&gt; 是两个不同的值。&lt;code&gt;Set&lt;/code&gt; 内部判断两个值是否不同，使用的算法叫做 &lt;code&gt;Same-value equality&lt;/code&gt;，它类似于精确相等运算符( &lt;code&gt;===&lt;/code&gt; )，主要的区别是 &lt;code&gt;NaN&lt;/code&gt; 等于自身，而精确相等运算符认 为 &lt;code&gt;NaN&lt;/code&gt; 不等于自身。&lt;/p&gt;
&lt;p&gt;Set 结构的实例有以下属性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Set.prototype.constructor&lt;/code&gt;:构造函数，默认就是 &lt;code&gt;Set&lt;/code&gt; 函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Set.prototype.size&lt;/code&gt; ：返回实例的成员总数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add(value)&lt;/code&gt;:添加某个值，返回 &lt;code&gt;Set&lt;/code&gt; 结构本身。支持链式添加&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete(value)&lt;/code&gt;:删除某个值，返回一个布尔值，表示删除是否成功。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;has(value)&lt;/code&gt;:返回一个布尔值，表示该值是否为 Set 的成员。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear()&lt;/code&gt;:清除所有成员，没有返回值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;keys()&lt;/code&gt;:返回键名的遍历器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value()&lt;/code&gt;:返回键值的遍历器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;entires()&lt;/code&gt;:返回键值对的遍历器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forEach()&lt;/code&gt;:使用回调函数遍历每个成员&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Array.from&lt;/code&gt; 方法可以将 &lt;code&gt;Set&lt;/code&gt; 结构转为数组。去除数组的重复成员 &lt;code&gt;[...new Set(array)]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Set&lt;/code&gt; 的遍历顺序就是插入顺序。这个特性有时非常有用，比如使用 &lt;code&gt;Set&lt;/code&gt; 保存一个回调函数列表，调用时就能保证按照添加顺序调用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;keys&lt;/code&gt; 方法、 &lt;code&gt;values&lt;/code&gt; 方法、 &lt;code&gt;entries&lt;/code&gt; 方法返回的都是遍历器对象。由于 &lt;code&gt;Set&lt;/code&gt; 结构没有键名，只有键值(或者说键名和键值 是同一个值)，所以 &lt;code&gt;keys&lt;/code&gt; 方法和 &lt;code&gt;values&lt;/code&gt; 方法的行为完全一致。&lt;code&gt;Set&lt;/code&gt; 结构的实例默认可遍历，它的默认遍历器生成函数就是它的 &lt;code&gt;values&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Set&lt;/code&gt; 结构的实例的 &lt;code&gt;forEach&lt;/code&gt; 方法，用于对每个成员执行某种操作，没有返回值。 &lt;code&gt;forEach&lt;/code&gt; 方法的参数就是一个处理函数。该函数的参数依次为键 值、键名、集合本身。另外， &lt;code&gt;forEach&lt;/code&gt; 方法还可以有第二个参数，表示绑定的 &lt;code&gt;this&lt;/code&gt; 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let set = new Set([1, 2, 3])
set.forEach((value, key) =&gt; console.log(value * 2)) // 2
// 4
// 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;扩展运算符，数组的 &lt;code&gt;map&lt;/code&gt;，&lt;code&gt;filter&lt;/code&gt; 都可以用于 &lt;code&gt;Set&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果想在遍历操作中，同步改变原来的 &lt;code&gt;Set&lt;/code&gt; 结构，目前没有直接的方法，但有两种 变通方法。一种是利用原 &lt;code&gt;Set&lt;/code&gt; 结构映射出一个新的结构，然后赋值给原来的 &lt;code&gt;Set&lt;/code&gt; 结 构;另一种是利用 &lt;code&gt;Array.from&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Set&lt;/code&gt; 转数组可以用 &lt;code&gt;Array.from()&lt;/code&gt; 和 扩展运算符。&lt;/p&gt;
&lt;h2&gt;WeakSet&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WeakSet&lt;/code&gt; 结构与 &lt;code&gt;Set&lt;/code&gt; 类似，也是不重复的值的集合。但是，它与 &lt;code&gt;Set&lt;/code&gt; 有两个区 别。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WeakSet&lt;/code&gt; 的成员只能是对象，而不能是其他类型的值，&lt;code&gt;null&lt;/code&gt; 也不是一个合法的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WeakSet&lt;/code&gt; 中的对象都是弱引用，即垃圾回收机制不考虑 &lt;code&gt;WeakSet&lt;/code&gt; 对该对象 的引用，也就是说，如果其他对象都不再引用该对象，那么垃圾回收机制会自动回收该对象所占用的内存，不考虑该对象还存在于 &lt;code&gt;WeakSet&lt;/code&gt; 之中。这也意味着 &lt;code&gt;WeakSet&lt;/code&gt; 中没有存储当前对象的列表。 正因为这样，&lt;code&gt;WeakSet&lt;/code&gt; 是不可枚举的（没有方法能给出所有的值）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，&lt;code&gt;WeakSet&lt;/code&gt; 适合临时存放一组对象，以及存放跟对象绑定的信息。只要这些对象在外部消失，它在 &lt;code&gt;WeakSet&lt;/code&gt; 里面的引用就会自动消失。由于上面这个特点，&lt;code&gt;WeakSet&lt;/code&gt; 的成员是不适合引用的，因为它会随时消失。另外， 由于 &lt;code&gt;WeakSet&lt;/code&gt; 内部有多少个成员，取决于垃圾回收机制有没有运行，运行前后很可能成员个数是不一样的，而垃圾回收机制何时运行是不可预测的，因此 &lt;code&gt;ES6&lt;/code&gt; 规定 &lt;code&gt;WeakSet&lt;/code&gt; 不可遍历。&lt;/p&gt;
&lt;p&gt;创建一个 &lt;code&gt;WeakMap&lt;/code&gt; 使用 &lt;code&gt;new WeakMap(value)&lt;/code&gt;，&lt;code&gt;new&lt;/code&gt; 不可以省略。如果传入一个可迭代对象作为参数, 则该对象的所有迭代值都会被自动添加进生成的 &lt;code&gt;WeakSet&lt;/code&gt; 对象中。&lt;code&gt;null&lt;/code&gt; 会被当做 &lt;code&gt;undefined&lt;/code&gt;，即和没有传入参数一样，创建一个空的 &lt;code&gt;WeakSet&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;WeakSet 结构有以下三个方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。&lt;/li&gt;
&lt;li&gt;WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。&lt;/li&gt;
&lt;li&gt;WeakSet.prototype.has(value):返回一个布尔值，表示某个值是否在 WeakSet 实例之中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;WeakMap&lt;/code&gt; 一个简单用法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const foos = new WeakSet()
class Foo {
  constructor() {
    foos.add(this)
  }
  method() {
    if (!foos.has(this)) {
      throw new TypeError(&apos;Foo.prototype.method 只能在Foo的实例上调用!&apos;)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WeakSet&lt;/code&gt; 的一个应用是可以用来检测对象的循环引用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 对 传入的subject对象 内部存储的所有内容执行回调
function execRecursively(fn, subject, _refs = null) {
  if (!_refs) _refs = new WeakSet()

  // 避免无限递归
  if (_refs.has(subject)) return

  fn(subject)
  if (&apos;object&apos; === typeof subject) {
    _refs.add(subject)
    for (let key in subject) execRecursively(fn, subject[key], _refs)
  }
}

const foo = {
  foo: &apos;Foo&apos;,
  bar: {
    bar: &apos;Bar&apos;
  }
}

foo.bar.baz = foo // 循环引用!
execRecursively((obj) =&gt; console.log(obj), foo)
// { foo: &apos;Foo&apos;, bar: { bar: &apos;Bar&apos;, baz: [Circular] } }
// Foo
// { bar: &apos;Bar&apos;, baz: { foo: &apos;Foo&apos;, bar: [Circular] } }
// Bar
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Map&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 的对象(&lt;code&gt;Object&lt;/code&gt;)，本质上是键值对的集合(&lt;code&gt;Hash&lt;/code&gt; 结构)，但是传统上只能用字符串（现在加入了 &lt;code&gt;Symbol&lt;/code&gt;）当作键。这给它的使用带来了很大的限制。为了解决这个问题，&lt;code&gt;ES6&lt;/code&gt; 提供了 &lt;code&gt;Map&lt;/code&gt; 数据结构。它类似于对象，也是键值对的集合，但是“键”的范围不限于字符串或者 &lt;code&gt;Symbol&lt;/code&gt;，各种类型的值(包括对象)都可以当作键。也就是说，&lt;code&gt;Object&lt;/code&gt; 结构提供了“字符串为 &lt;code&gt;key&lt;/code&gt; 的映射，&lt;code&gt;Map&lt;/code&gt; 结构提供了任意值为 &lt;code&gt;key&lt;/code&gt; 的映射，是一种更完善的 &lt;code&gt;Hash&lt;/code&gt; 结构实现。如果你需要&lt;strong&gt;键值对&lt;/strong&gt;的数据结构，&lt;code&gt;Map&lt;/code&gt; 比 &lt;code&gt;Object&lt;/code&gt; 更合适。&lt;code&gt;Map&lt;/code&gt; 对象能够记住键的原始插入顺序。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Map&lt;/code&gt; 和 &lt;code&gt;Object&lt;/code&gt; 的区别如下：&lt;/p&gt;
&lt;p&gt;|          | &lt;code&gt;Map&lt;/code&gt;                                                                                | &lt;code&gt;Object&lt;/code&gt;                                                                                                                                      |
| -------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 意外的键 | &lt;code&gt;Map 默认情况不包含任何键。只包含显式插入的键。&lt;/code&gt;                                     | 一个 &lt;code&gt;Object&lt;/code&gt; 有一个原型, 原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。（也可以使用 &lt;code&gt;Object.create(null)&lt;/code&gt; 创建一个没有原型的对象 |
| 键的类型 | 一个 &lt;code&gt;Map&lt;/code&gt; 的键可以是任意值，包括函数、对象或任意基本类型。                          | 一个 &lt;code&gt;Object&lt;/code&gt; 的键必须是一个 &lt;code&gt;String&lt;/code&gt; &lt;code&gt;或是Symbol&lt;/code&gt;。                                                                                          |
| 键的顺序 | &lt;code&gt;Map&lt;/code&gt; 中的 &lt;code&gt;key&lt;/code&gt; 是有序的。因此，当迭代的时候，一个 &lt;code&gt;Map&lt;/code&gt; 对象以插入的顺序返回键值。 | 自 &lt;code&gt;ECMAScript 2015&lt;/code&gt; 规范以来，对象保留了字符串和 &lt;code&gt;Symbol&lt;/code&gt; 键的创建顺序； 因此，在只有字符串键的对象上进行迭代将按插入顺序产生键。            |
| &lt;code&gt;Size&lt;/code&gt;   | &lt;code&gt;Map&lt;/code&gt; 的键值对个数可以轻易地通过 &lt;code&gt;size&lt;/code&gt; 属性获取                                     | &lt;code&gt;Object&lt;/code&gt; 的键值对个数只能手动计算                                                                                                             |
| 迭代     | &lt;code&gt;Map&lt;/code&gt; 是 &lt;code&gt;iterable&lt;/code&gt; 的，所以可以直接被迭代。                                         | 迭代一个 &lt;code&gt;Object&lt;/code&gt; 需要以某种方式获取它的键然后才能迭代。                                                                                      |
| 性能     | 在频繁增删键值对的场景下表现更好。                                                   | 在频繁添加和删除键值对的场景下未作出优化。                                                                                                    |&lt;/p&gt;
&lt;p&gt;Map 的键实际上是跟内存地址绑定的，只要内存地址不一样，就视为两个键。这就解决了同名属性碰撞(&lt;code&gt;clash&lt;/code&gt;)的问题，我们扩展别人的库的时候，如果使用对象作为键名，就不用担心自己的属性与原作者的属性同名。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;Map&lt;/code&gt; 的键是一个简单类型的值(数字、字符串、布尔值)，则只要两个值严格相等，&lt;code&gt;Map&lt;/code&gt; 将其视为一个键，比如 &lt;code&gt;0&lt;/code&gt; 和 &lt;code&gt;-0&lt;/code&gt; 就是一个键，布尔值 &lt;code&gt;true&lt;/code&gt; 和字符串 &lt;code&gt;&apos;true&apos;&lt;/code&gt; 则是两个不同的键。另外， &lt;code&gt;undefined&lt;/code&gt; 和 &lt;code&gt;null&lt;/code&gt; 也是两个不同的键。虽然 &lt;code&gt;NaN&lt;/code&gt; 不严格相等于自身，但 &lt;code&gt;Map&lt;/code&gt; 将其视为同一个键。&lt;/p&gt;
&lt;p&gt;Map 的属性和方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt;：返回 Map 结构的成员总数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set(key, value)&lt;/code&gt;:设置键名 key 对应的键值为 value ，然后返回整个 Map 结构。如 果 已经有值，则键值会被更新，否则就新生成该键。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get(key)&lt;/code&gt;:读取 key 对应的键值，如果找不到 key ，返回 undefined 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;has(key)&lt;/code&gt;:返回一个布尔值，表示某个键是否在当前 Map 对象之中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete(key)&lt;/code&gt;:删除某个键，返回 true 。如果删除失败，返回 false 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear()&lt;/code&gt;:清除所有成员，没有返回值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key()&lt;/code&gt;:返回键名的遍历器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value()&lt;/code&gt;:返回键值的遍历器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;entries()&lt;/code&gt;:返回所有成员的遍历器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forEach()&lt;/code&gt;:遍历 Map 的所有成员。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Map&lt;/code&gt; 的遍历顺序就是插入顺序。&lt;code&gt;map[Symbol.iterator] === map.entries&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt; 结构的默认遍历器接口 ( &lt;code&gt;Symbol.iterator&lt;/code&gt;属性)，就是 &lt;code&gt;entries&lt;/code&gt; 方法。一个 &lt;code&gt;Map&lt;/code&gt; 对象在迭代时会根据对象中元素的插入顺序来进行， 一个 &lt;code&gt;for...of&lt;/code&gt; 循环在每次迭代后会返回一个形式为 &lt;code&gt;[key，value]&lt;/code&gt; 的数组。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Map&lt;/code&gt; 结构转为数组结构，比较快速的方法是使用扩展运算符。结合数组的 &lt;code&gt;map&lt;/code&gt; 方法、&lt;code&gt;filter&lt;/code&gt; 方法，可以实现 &lt;code&gt;Map&lt;/code&gt; 的遍历和过滤(&lt;code&gt;Map&lt;/code&gt; 本身没有 &lt;code&gt;map&lt;/code&gt; 和 &lt;code&gt;filter&lt;/code&gt; 方法)。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const myMap = new Map().set(true, 7).set({ foo: 3 }, [&apos;abc&apos;])
;[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ &apos;abc&apos; ] ] ]

//数组转为Map
new Map([
  [true, 7],
  [{ foo: 3 }, [&apos;abc&apos;]]
])
// Map {
// true =&gt; 7,
//   Object {foo: 3} =&gt; [&apos;abc&apos;]
// }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果所有 &lt;code&gt;Map&lt;/code&gt; 的键都是字符串，它可以转为对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function strMapToObj(strMap) {
  let obj = Object.create(null)
  for (let [k, v] of strMap) {
    obj[k] = v
  }
  return obj
}
const myMap = new Map().set(&apos;yes&apos;, true).set(&apos;no&apos;, false)
strMapToObj(myMap)
// { yes: true, no: false }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Map&lt;/code&gt; 转为 &lt;code&gt;JSON&lt;/code&gt; 要区分两种情况。一种情况是，&lt;code&gt;Map&lt;/code&gt; 的键名都是字符串，这时可以选择转为对象 &lt;code&gt;JSON&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap))
}
let myMap = new Map().set(&apos;yes&apos;, true).set(&apos;no&apos;, false)
strMapToJson(myMap)
// &apos;{&quot;yes&quot;:true,&quot;no&quot;:false}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一种情况是，&lt;code&gt;Map&lt;/code&gt; 的键名有非字符串，这时可以选择转为数组 &lt;code&gt;JSON&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function mapToArrayJson(map) {
  return JSON.stringify([...map])
}
let myMap = new Map().set(true, 7).set({ foo: 3 }, [&apos;abc&apos;])
mapToArrayJson(myMap)
// &apos;[[true,7],[{&quot;foo&quot;:3},[&quot;abc&quot;]]]&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Map&lt;/code&gt; 的迭代：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let myMap = new Map()
myMap.set(0, &apos;zero&apos;)
myMap.set(1, &apos;one&apos;)
for (let [key, value] of myMap) {
  //解构
  console.log(key + &apos; = &apos; + value)
}
// 将会显示两个log。一个是&quot;0 = zero&quot;另一个是&quot;1 = one&quot;

for (let key of myMap.keys()) {
  console.log(key)
}
// 将会显示两个log。 一个是 &quot;0&quot; 另一个是 &quot;1&quot;

for (let value of myMap.values()) {
  console.log(value)
}
// 将会显示两个log。 一个是 &quot;zero&quot; 另一个是 &quot;one&quot;

for (let [key, value] of myMap.entries()) {
  console.log(key + &apos; = &apos; + value)
}
// 将会显示两个log。 一个是 &quot;0 = zero&quot; 另一个是 &quot;1 = one&quot;

myMap.forEach(function (value, key) {
  console.log(key + &apos; = &apos; + value)
})
// 将会显示两个logs。 一个是 &quot;0 = zero&quot; 另一个是 &quot;1 = one&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;合并 &lt;code&gt;Map&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let original = new Map([[1, &apos;one&apos;]])

let clone = new Map(original)

console.log(clone.get(1)) // one
console.log(original === clone) // false. 浅比较 不为同一个对象的引用

//合并的 Map 中存在重复的键名，后面的会覆盖前者
let first = new Map([
  [1, &apos;one&apos;],
  [2, &apos;two&apos;],
  [3, &apos;three&apos;]
])

let second = new Map([
  [1, &apos;uno&apos;],
  [2, &apos;dos&apos;]
])

// 合并两个Map对象时，如果有重复的键值，则后面的会覆盖前面的。
// 展开运算符本质上是将Map对象转换成数组。
let merged = new Map([...first, ...second])

console.log(merged.get(1)) // uno
console.log(merged.get(2)) // dos
console.log(merged.get(3)) // three

//也可以与数组合并
let first = new Map([
  [1, &apos;one&apos;],
  [2, &apos;two&apos;],
  [3, &apos;three&apos;]
])

let second = new Map([
  [1, &apos;uno&apos;],
  [2, &apos;dos&apos;]
])

// Map对象同数组进行合并时，如果有重复的键值，则后面的会覆盖前面的。
let merged = new Map([...first, ...second, [1, &apos;eins&apos;]])

console.log(merged.get(1)) // eins
console.log(merged.get(2)) // dos
console.log(merged.get(3)) // three
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WeakMap&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WeakMap&lt;/code&gt; 结构与 &lt;code&gt;Map&lt;/code&gt; 结构类似，也是用于生成键值对的集合，其中的键是弱引用的。&lt;code&gt;WeakMap&lt;/code&gt; 与 &lt;code&gt;Map&lt;/code&gt; 的区别有两点。首先， &lt;code&gt;WeakMap&lt;/code&gt; 只接受对象作为键名( &lt;code&gt;null&lt;/code&gt; 除外)，不接受其他类型的值作为键名。其次，&lt;code&gt;WeakMap&lt;/code&gt; 的键名所指向的对象，不计入垃圾回收机制。&lt;code&gt;WeakMap&lt;/code&gt; 的 &lt;code&gt;key&lt;/code&gt; 是不可枚举的 (没有方法能给出所有的 &lt;code&gt;key&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;创建 &lt;code&gt;WeakMap&lt;/code&gt; 的语法 &lt;code&gt;new WeakMap([iterable])&lt;/code&gt;，&lt;code&gt;Iterable&lt;/code&gt; 是一个数组（二元数组）或者其他可迭代的且其元素是键值对的对象。每个键值对会被加到新的 &lt;code&gt;WeakMap&lt;/code&gt; 里。&lt;code&gt;null&lt;/code&gt; 会被当做 &lt;code&gt;undefined&lt;/code&gt;，即和没有传入参数一样，创建一个空的 &lt;code&gt;WeakMap&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WeakMap&lt;/code&gt; 的设计目的在于，有时我们想在某个对象上面存放一些数据，但是这会形成对于这个对象的引用。请看下面的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const e1 = document.getElementById(&apos;foo&apos;)
const e2 = document.getElementById(&apos;bar&apos;)
const arr = [
  [e1, &apos;foo 元素&apos;],
  [e2, &apos;bar 元素&apos;]
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中， &lt;code&gt;e1&lt;/code&gt; 和 &lt;code&gt;e2&lt;/code&gt; 是两个对象，我们通过 &lt;code&gt;arr&lt;/code&gt; 数组对这两个对象添加一些 文字说明。这就形成了 &lt;code&gt;arr&lt;/code&gt; 对 &lt;code&gt;e1&lt;/code&gt; 和 &lt;code&gt;e2&lt;/code&gt; 的引用。一旦不再需要这两个对象，我们就必须手动删除这个引用，否则垃圾回收机制就不会释放 &lt;code&gt;e1&lt;/code&gt; 和 &lt;code&gt;e2&lt;/code&gt; 占用的内存。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 不需要 e1 和 e2 的时候 // 必须手动删除引用
arr[0] = null
arr[1] = null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这样的写法显然很不方便。一旦忘了写，就会造成内存泄露。&lt;code&gt;WeakMap&lt;/code&gt; 就是为了解决这个问题而诞生的，它的键名所引用的对象都是弱引用， 即垃圾回收机制不将该引用考虑在内。因此，只要所引用的对象的其他引用都被清 除，垃圾回收机制就会释放该对象所占用的内存。也就是说，一旦不再需要，&lt;code&gt;WeakMap&lt;/code&gt; 里面的键名对象和所对应的键值对会自动消失，不用手动删除引用。基本上，如果你要往对象上添加数据，又不想干扰垃圾回收机制，就可以使用 &lt;code&gt;WeakMap&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;一个典型应用场景是，在网页的 &lt;code&gt;DOM&lt;/code&gt; 元素上添加数据，就可以使 用 &lt;code&gt;WeakMap&lt;/code&gt; 结构。当该 &lt;code&gt;DOM&lt;/code&gt; 元素被清除，其所对应的 &lt;code&gt;WeakMap&lt;/code&gt; 记录就会自动移除。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const wm = new WeakMap()
const element = document.getElementById(&apos;example&apos;)
wm.set(element, &apos;some information&apos;)
wm.get(element) // &quot;some information&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总之，&lt;code&gt;WeakMap&lt;/code&gt; 的专用场合就是，它的键所对应的对象，可能会在将来消失。该数据结构有助于防止内存泄漏。注意，&lt;code&gt;WeakMap&lt;/code&gt; 弱引用的只是键名，而不是键值。键值依然是正常引用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WeakMap&lt;/code&gt; 只有四个方法可用: &lt;code&gt;get()、set() 、has() 、 delete()&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WeakMap&lt;/code&gt; 的一个例子： &lt;code&gt;myElement&lt;/code&gt; 是一个 &lt;code&gt;DOM&lt;/code&gt; 节点，每当发生 &lt;code&gt;click&lt;/code&gt; 事件，就更新一 下状态。我们将这个状态作为键值放在 &lt;code&gt;WeakMap&lt;/code&gt; 里，对应的键名就是 &lt;code&gt;myElement&lt;/code&gt; 。一旦这个 &lt;code&gt;DOM&lt;/code&gt; 节点删除，该状态就会自动消失，不存在内存泄漏风险。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let myElement = document.getElementById(&apos;logo&apos;)
let myWeakmap = new WeakMap()

myWeakmap.set(myElement, { timesClicked: 0 })

myElement.addEventListener(
  &apos;click&apos;,
  function () {
    let logoData = myWeakmap.get(myElement)
    logoData.timesClicked++
  },
  false
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现一个带有 &lt;code&gt;clear()&lt;/code&gt; 方法的 &lt;code&gt;WeakMap&lt;/code&gt; 类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class ClearableWeakMap {
  constructor(init) {
    this._wm = new WeakMap(init)
  }
  clear() {
    this._wm = new WeakMap()
  }
  delete(k) {
    return this._wm.delete(k)
  }
  get(k) {
    return this._wm.get(k)
  }
  has(k) {
    return this._wm.has(k)
  }
  set(k, v) {
    this._wm.set(k, v)
    return this
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;《ES6 标准入门》 —— 阮一峰&lt;/li&gt;
&lt;li&gt;MDN&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/frontend9/fe9-library/issues/275&quot; title=&quot;深入理解 Set&quot;&gt;深入理解 Set Map WeakSet WeakMap&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>ignore文件语法</title><link>https://clloz.com/blog/ignore-file-syntax</link><guid isPermaLink="true">https://clloz.com/blog/ignore-file-syntax</guid><pubDate>Mon, 27 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在使用 &lt;code&gt;Github&lt;/code&gt; 的时候我们有 &lt;code&gt;.gitignore&lt;/code&gt; 文件来避免提交一些不需要提交的文件（比如 &lt;code&gt;node_modules&lt;/code&gt;），在使用 &lt;code&gt;eslint&lt;/code&gt; 和 &lt;code&gt;prettier&lt;/code&gt; 时也有对应的 &lt;code&gt;.eslintignore&lt;/code&gt; 和 &lt;code&gt;.prettierignore&lt;/code&gt;，它们的功能都类似，语法也差不多，本文就总结一下 &lt;code&gt;ignore&lt;/code&gt; 文件的常用语法。&lt;/p&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;p&gt;这里的语法主要是结合 &lt;a href=&quot;https://git-scm.com/docs/gitignore&quot; title=&quot;Git 官方文档&quot;&gt;Git 官方文档&lt;/a&gt; 和自己的理解写的。&lt;code&gt;pattern&lt;/code&gt; 就是匹配的模式，我们每一条规则就是一个 &lt;code&gt;pattern&lt;/code&gt;。&lt;code&gt;ignore&lt;/code&gt; 文件的作用就是让程序不再跟踪 &lt;code&gt;track&lt;/code&gt; 某些文件。根目录指的是跟 &lt;code&gt;ignore&lt;/code&gt; 文件在同一层级的目录。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空行没有任何意义，可以用来分隔不同的规则增强可读性。&lt;/li&gt;
&lt;li&gt;以 &lt;code&gt;#&lt;/code&gt; 开头的行即注释行将被忽略，可用反斜杠转义。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pattern&lt;/code&gt; 中的空格会被忽略除非用反斜杠转义&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!&lt;/code&gt; 用来否定 &lt;code&gt;pattern&lt;/code&gt;，比如 &lt;code&gt;!pattern&lt;/code&gt;，如果有一个文件被某一条规则排除了，这条文件又能匹配 &lt;code&gt;!&lt;/code&gt; 之后的 &lt;code&gt;pattern&lt;/code&gt;，那么它将被重新跟踪 &lt;code&gt;track&lt;/code&gt;。如果一个文件的父级目录被排除跟踪，那么将没有办法重新将它包含进来。比如你的 &lt;code&gt;src&lt;/code&gt; 文件夹被排除了，&lt;code&gt;src&lt;/code&gt; 文件夹下有一个 &lt;code&gt;index.js&lt;/code&gt;，此时即使你加上 &lt;code&gt;!/src/index.js&lt;/code&gt; 也不能让这个文件恢复跟踪。这样做是问了性能考虑，即一个文件夹只要被排除了，和文件内相关的所有 &lt;code&gt;pattern&lt;/code&gt; 都将被忽略。&lt;code&gt;!&lt;/code&gt; 同样可以用反斜杠转义，比如 &lt;code&gt;\!important!.txt&lt;/code&gt;，就是匹配 &lt;code&gt;!important!.txt&lt;/code&gt; 这个文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; 用来分隔目录，可以出现在一条 &lt;code&gt;pattern&lt;/code&gt; 的任意位置。&lt;/li&gt;
&lt;li&gt;如果没有使用 &lt;code&gt;/&lt;/code&gt;，那么我们的 &lt;code&gt;pattern&lt;/code&gt; 会递归到各个目录中去（沿着目录的嵌套一直向下寻找），比如你写了一个 &lt;code&gt;common&lt;/code&gt; 不仅根目录中的 &lt;code&gt;common&lt;/code&gt; 会被排除，你整个项目中所有的 &lt;code&gt;common&lt;/code&gt; 都会被排除。如果 &lt;code&gt;common&lt;/code&gt; 是一个目录，那么该目录就会被排除跟踪。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;/&lt;/code&gt; 在 &lt;code&gt;pattern&lt;/code&gt; 中出现，就意味着找到 &lt;code&gt;/&lt;/code&gt; 前的目录（必须是一个目录，因为 &lt;code&gt;/&lt;/code&gt; 是目录分隔符），比如 &lt;code&gt;src/app&lt;/code&gt;，就代表找到 &lt;code&gt;src&lt;/code&gt; 目录，然后在这个目录下（不能递归，只能在目录下的第一层）找 &lt;code&gt;app&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;找到 &lt;code&gt;/&lt;/code&gt; 前对应的目录之后，在目录的第一层寻找 &lt;code&gt;/&lt;/code&gt; 之后对应的文件或者目录。注意这里不像没有 &lt;code&gt;/&lt;/code&gt; 的情况可以一直像更深的层级递归寻找，因为 &lt;code&gt;/&lt;/code&gt; 指定了目录，所以只能在目录的第一层寻找。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;/&lt;/code&gt; 出现在 &lt;code&gt;pattern&lt;/code&gt; 的第一个字符，就代表在根目录下寻找。如果 &lt;code&gt;/&lt;/code&gt; 出现在 &lt;code&gt;pattern&lt;/code&gt; 的最后一个字符，表示你要找的是一个目录而不是文件。所以如果你要找根目录下的一个 &lt;code&gt;test&lt;/code&gt; 的文件夹 &lt;code&gt;/test&lt;/code&gt;，&lt;code&gt;/test/&lt;/code&gt; 和 &lt;code&gt;test&lt;/code&gt; 都能找到，但是只有 &lt;code&gt;/test/&lt;/code&gt; 是确保找的根目录下的 &lt;code&gt;test&lt;/code&gt; 文件夹，&lt;code&gt;/test&lt;/code&gt; 有可能找到的是一个文件，而不是文件夹，而 &lt;code&gt;test&lt;/code&gt; 则会往更深的嵌套目录去寻找 &lt;code&gt;test&lt;/code&gt;，可能会匹配到其他 &lt;code&gt;test&lt;/code&gt; 文件和文件夹。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;用来匹配零个或多个字符（不包括 &lt;code&gt;/&lt;/code&gt;)，如 &lt;code&gt;*.[oa]&lt;/code&gt; 忽略所有以 &lt;code&gt;&quot;.o&quot;&lt;/code&gt; 或 &lt;code&gt;&quot;.a&quot;&lt;/code&gt; 结尾，&lt;code&gt;*~&lt;/code&gt; 忽略所有以 &lt;code&gt;~&lt;/code&gt; 结尾的文件（这种文件通常被许多编辑器标记为临时文件）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[]&lt;/code&gt; 用来匹配括号内的任一字符，如 &lt;code&gt;[abc]&lt;/code&gt;，也可以在括号内加连接符，如 &lt;code&gt;[0-9]&lt;/code&gt; 匹配0至9的数，类似正则表达式；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt; 用来匹配单个字符（不包括 &lt;code&gt;/&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;**&lt;/code&gt; ：与嵌套目录匹配，比如 &lt;code&gt;a/**/z&lt;/code&gt; 与以下项匹配 &lt;code&gt;a/z&lt;/code&gt;、&lt;code&gt;a/b/z&lt;/code&gt;、a/b/c/z。&lt;/li&gt;
&lt;li&gt;可以使用标准的 &lt;code&gt;glob&lt;/code&gt; 模式匹配。所谓的 &lt;code&gt;glob&lt;/code&gt; 模式是指 &lt;code&gt;shell&lt;/code&gt; 所使用的简化了的正则表达式。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;# 忽略 .a 文件
*.a

# 但否定忽略 lib.a, 尽管已经在前面忽略了 .a 文件
!lib.a

# 忽略 doc/notes.txt, 不包括 doc/server/arch.txt
doc/*.txt

# 忽略所有的 .pdf 文件 在 doc/ directory 下的
doc/**/*.pdf

# /dir 将匹配.gitignore所在层级一个文件，目录，链接，任何名为dir的内容
# /dir/  将只会匹配.gitignore所在层级一个名为dir的目录
# /dir/* 将匹配所有文件，目录和其他任何在名为dir的目录（也在.gitignore所在层级）里的内容（但是不包括这个目录本身）
# 如果你使用 !.gitkeep 并且有个 dir/.gitkeep 文件，对于 /dir 和 /dir/ 这两种匹配规则，你写的 !.gitkeep 不会生效，因为 Git不会去 dir 文件夹的内部检查；对于 /dir/*，Git会检查.gitkeep，并且dir文件夹会被提交，因为这条模式不会应用到文件夹，而是应用到文件夹里面的内容。
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;可以用 &lt;code&gt;git check-ignore&lt;/code&gt; 来查看我们某个文件是否被忽略，命令的细节查看&lt;a href=&quot;https://git-scm.com/docs/git-check-ignore&quot; title=&quot;官方文档&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 &lt;code&gt;.gitingore&lt;/code&gt; 文件中，每一行指定一个忽略规则，&lt;code&gt;Git&lt;/code&gt; 检查忽略规则的时候有多个来源，它的优先级如下（由高到低）： 1、从命令行中读取可用的忽略规则 2、当前目录定义的规则 3、父级目录定义的规则，依次递推 4、&lt;code&gt;$GIT_DIR/info/exclude&lt;/code&gt; 文件中定义的规则 5、&lt;code&gt;core.excludesfile&lt;/code&gt; 中定义的全局规则&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git&lt;/code&gt; 对于 &lt;code&gt;.gitignore&lt;/code&gt; 配置文件是按行从上到下进行规则匹配的，意味着如果前面的规则匹配的范围更大，则后面的规则将不会生效；如果你不慎在创建 &lt;code&gt;.gitignore&lt;/code&gt; 文件之前就 &lt;code&gt;push&lt;/code&gt; 了项目，那么即使你在 &lt;code&gt;.gitignore&lt;/code&gt; 文件中写入新的过滤规则，这些规则也不会起作用，&lt;code&gt;Git&lt;/code&gt; 仍然会对所有文件进行版本管理。所以在项目创建时就设计好对应的 &lt;code&gt;.gitignore&lt;/code&gt; 文件是一个好习惯。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Github&lt;/code&gt; 官方有一个仓库提供了各种语言的 &lt;code&gt;.gitignore&lt;/code&gt; 模版，可以用来进行参考，仓库地址 &lt;a href=&quot;https://github.com/github/gitignore&quot; title=&quot;gitignore&quot;&gt;gitignore&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文虽然是以 &lt;code&gt;.gitignore&lt;/code&gt; 进行语法说明，不过大多数的 &lt;code&gt;ignore&lt;/code&gt; 文件语法都类似，可以直接套用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/docs/gitignore&quot; title=&quot;git-scm document&quot;&gt;git-scm document&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/17888695/difference-between-gitignore-rules-with-and-without-trailing-slash-like-dir-an/38559600#38559600&quot; title=&quot;Difference between .gitignore rules with and without trailing slash like /dir and /dir/&quot;&gt;Difference between .gitignore rules with and without trailing slash like /dir and /dir/&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/gitignore.BqTuwPsZ.png"/><enclosure url="/_astro/gitignore.BqTuwPsZ.png"/></item><item><title>状态机和KMP算法</title><link>https://clloz.com/blog/fsm-kmp</link><guid isPermaLink="true">https://clloz.com/blog/fsm-kmp</guid><pubDate>Fri, 24 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;字符串的匹配是编写程序的时候经常遇到的一个问题，也是计算机处理的基础问题之一。最简单的方法是循环字符串，一位一位地进行匹配，也可以使用正则表达式。今天本文讨论用状态机的模型如何理解和解决字符串匹配的问题。&lt;/p&gt;
&lt;h2&gt;状态机 FSM&lt;/h2&gt;
&lt;p&gt;状态机全称有限状态机（英语：&lt;code&gt;finite-state machine&lt;/code&gt;，缩写：&lt;code&gt;FSM&lt;/code&gt;）又称有限状态自动机（英语：&lt;code&gt;finite-state automation&lt;/code&gt;，缩写：&lt;code&gt;FSA&lt;/code&gt;），简称状态机，是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。我个人的理解是我们的某一类型的问题在解决过程中能够抽象出有限个不同的状态，不同的问题只是问题解决过程中状态的转移过程不同，当我们将状态和状态之间的关系抽象出来，那么这一类问题我们全部能用一套逻辑来解决。每一个状态机应该能够接受相同的输出，每一个状态机都应该知道自己的下一个状态是什么（输出只依赖于当前状态，和输入无关的称为 &lt;code&gt;moore&lt;/code&gt; 机，输出依赖于输出和当前状态的称为 &lt;code&gt;mealy&lt;/code&gt; 机）。用 &lt;code&gt;JavaScript&lt;/code&gt; 模拟状态机的模型大概如下面的形式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function fsm_start(input) {
  //当前状态机的逻辑
  return next_fsm //返回下一个状态函数
}

let state = fsm_start //设置初始状态

while (condition) {
  state = state(input) //执行当前状态机函数，并将
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;状态机的字符串匹配&lt;/h2&gt;
&lt;p&gt;其实我们在编程的过程中其实一直和状态打交道，比如 &lt;code&gt;if/else&lt;/code&gt;，&lt;code&gt;switch/case&lt;/code&gt;等，有时候当我们面对一些比较复杂的问题的时候如果能够用有限状态机来描述，那么问题的逻辑可能更清晰。代码结构的可读性也更强。&lt;/p&gt;
&lt;p&gt;比如字符串的匹配问题，当我们想要知道某个字符串中是否包含另一个字符串时，状态机就能够帮我们解决问题。比如我们要寻找一个字符串中是否有 &lt;code&gt;abcdef&lt;/code&gt; 这串字符，如果使用状态机模型，我们可以这样实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function match(string) {
  let foundA = (foundB = foundC = foundD = foundE = false)
  for (let c of string) {
    if (c === &apos;a&apos;) {
      foundA = true
    } else if (foundA &amp;#x26;&amp;#x26; c === &apos;b&apos;) {
      foundB = true
    } else if (foundB &amp;#x26;&amp;#x26; c === &apos;c&apos;) {
      foundC = true
    } else if (foundC &amp;#x26;&amp;#x26; c === &apos;d&apos;) {
      foundD = true
    } else if (foundD &amp;#x26;&amp;#x26; c === &apos;e&apos;) {
      foundE = true
    } else if (foundE &amp;#x26;&amp;#x26; c === &apos;f&apos;) {
      return true
    }
  }
  return false
}

console.log(match(&apos;abcdeabcdef&apos;)) //true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是如果我们想要匹配的字符串时 &lt;code&gt;abcabx&lt;/code&gt; 这种有重复部分的时候我们就没法用上面那种方法了，因为当我们匹配到 &lt;code&gt;x&lt;/code&gt; 这一位的时候有可能是 &lt;code&gt;c&lt;/code&gt; 我们需要回到检测到 &lt;code&gt;c&lt;/code&gt; 的状态，这种情况用上面的方法就没法实现，因为 &lt;code&gt;if/else&lt;/code&gt; 结构是没法跳转到某一步的。在不用状态机的情况下我们可以用下面的“暴力”方法实现，用 &lt;code&gt;i&lt;/code&gt; 和 &lt;code&gt;j&lt;/code&gt; 分别表示字符串和 &lt;code&gt;pattern&lt;/code&gt; 当前正在匹配的字符的下标，匹配成功则都 &lt;code&gt;+1&lt;/code&gt;，否则 &lt;code&gt;i = i - j +1&lt;/code&gt;，&lt;code&gt;j = 0&lt;/code&gt;。循环继续的条件是字符串还没匹配完，并且也没有成功匹配出 &lt;code&gt;pattern&lt;/code&gt;。大概过程如下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/kmp1.w25wy2Ru_ZTTjAp.webp&quot; alt=&quot;kmp1&quot; title=&quot;kmp1&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function match(string, pattern) {
  let i = (j = 0)
  for (; i &amp;#x3C; string.length &amp;#x26;&amp;#x26; j &amp;#x3C; pattern.length; i++) {
    if (string[i] === pattern[j]) {
      j++
    } else {
      i -= j
      j = 0
    }
  }
  if (j === pattern.length) {
    let s_index = i - pattern.length
    return [s_index, string.slice(s_index, s_index + pattern.length)]
  } else {
    return &apos;failed&apos;
  }
}

console.log(...match(&apos;abababababcabcabxababab&apos;, &apos;abcabx&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用上面的方式我们可以对任意 &lt;code&gt;pattern&lt;/code&gt; 进行字符串的匹配。但有个问题是当匹配失败的时候我们是将 &lt;code&gt;i&lt;/code&gt; 前进一位，然后对 &lt;code&gt;pattern&lt;/code&gt; 从头进行匹配，对一些有重复部分的 &lt;code&gt;pattern&lt;/code&gt; 显然是没有效率的。比如 &lt;code&gt;abcabx&lt;/code&gt;，当我们匹配到 &lt;code&gt;abcabc&lt;/code&gt; 的时候我们没必要从 &lt;code&gt;bcabc&lt;/code&gt; 从头匹配，只需匹配第二个 &lt;code&gt;abc&lt;/code&gt; 后面是不是 &lt;code&gt;abx&lt;/code&gt; 就可以了。而如果使用状态机模型，则可以利用状态的跳转来实现这样的目的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function match(string) {
  let state = start
  for (let c of string) {
    state = state(c)
  }
  return state === end
}

function start(c) {
  if (c === &apos;a&apos;) {
    return foundA
  } else {
    return start
  }
}

function foundA(c) {
  if (c === &apos;b&apos;) {
    return foundB
  } else {
    return start(c)
  }
}

function foundB(c) {
  if (c === &apos;c&apos;) {
    return foundC
  } else {
    return start(c)
  }
}

function foundC(c) {
  if (c === &apos;a&apos;) {
    return foundA2
  } else {
    return start(c)
  }
}

function foundA2(c) {
  if (c === &apos;b&apos;) {
    return foundB2
  } else {
    return start(c)
  }
}

function foundB2(c) {
  if (c === &apos;x&apos;) {
    return end
  } else {
    return foundB(c) //跳转到找到第一个b的状态
  }
}

function end() {
  return end
}

console.log(match(&apos;ababaabcabcabxab&apos;)) //true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用状态机模型的方式实现，代码结构也很清晰，每一个状态直接是没有关系的，我们可以通过控制状态的跳转来控制流程，提高匹配的效率。但是这样的实现方式显然也不好，每次出现一个新的 &lt;code&gt;pattern&lt;/code&gt; 我们都要重新写代码，并没有将状态机的逻辑完全抽象出来。&lt;/p&gt;
&lt;h2&gt;KMP 算法&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;KMP&lt;/code&gt; 算法的主要思想是提前判断如何重新开始查找，而这种判断只取决于模式字符串本身。—— 《算法4》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在匹配字符串这个问题上，最重要的就是当一个字符匹配失败的时候，我们怎么进行下面的匹配才是最高效的。比如上面的非状态机的实现方法，当我们匹配失败的时候，我们已经匹配成功了 &lt;code&gt;[0, j - 1]&lt;/code&gt; 位了，此时我们将 &lt;code&gt;i&lt;/code&gt; 回退到未匹配状态并右移一位再从 &lt;code&gt;j = 0&lt;/code&gt; 开始匹配，这显然是非常低效的。比如 &lt;code&gt;abcabx&lt;/code&gt; 这样的 &lt;code&gt;pattern&lt;/code&gt;，当我们在 &lt;code&gt;x&lt;/code&gt; 这一位匹配失败的时候，我们并不一定需要全部重新匹配，因为如果我们当前这一位是 &lt;code&gt;c&lt;/code&gt; 的话我们可以优化我们的匹配过程，如下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/kmp2.BDUNYowg_Z1a3oPA.webp&quot; alt=&quot;kmp2&quot; title=&quot;kmp2&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果我们能找到 &lt;code&gt;pattern&lt;/code&gt; 的规律就能够很好地优化我们的过程，&lt;code&gt;kmp&lt;/code&gt; 算法正是一个很好字符串匹配的算法。&lt;code&gt;kmp&lt;/code&gt; 算法有两种实现方式，一种是基于确定有限状态机的 &lt;code&gt;DFM(deterministic finite-state automaton)&lt;/code&gt;，另一种是基于部分匹配表 &lt;code&gt;PMT(Partial Match Table)&lt;/code&gt; 的。我个人觉得 &lt;code&gt;PMT&lt;/code&gt; 的思路更容易理解一点，但是 &lt;code&gt;DFM&lt;/code&gt; 则是一种更”优雅“的方式。&lt;/p&gt;
&lt;h2&gt;DFM&lt;/h2&gt;
&lt;p&gt;在谷歌上搜索 &lt;code&gt;DFM&lt;/code&gt; 的中文结果很少，大部分的 &lt;code&gt;KMP&lt;/code&gt; 算法的文章都是讲解 &lt;code&gt;PMT&lt;/code&gt; 中的 &lt;code&gt;next&lt;/code&gt; 数组的含义，通过状态机来讲解这个算法的非常少，而且质量也不是很高。大部分的 &lt;code&gt;DFM&lt;/code&gt; 文章都来源于 &lt;code&gt;《算法》第四版&lt;/code&gt; 中的 &lt;code&gt;5.3&lt;/code&gt; 章节 &lt;code&gt;Substring Search&lt;/code&gt; 中的 &lt;code&gt;Knuth-Morris-Pratt substring search&lt;/code&gt;，以及作者 &lt;code&gt;Robert Sedgewick&lt;/code&gt; 对该算法的讲解视频（视频地址贴在&lt;a href=&quot;https://www.coursera.org/lecture/algorithms-part2/knuth-morris-pratt-TAtDr&quot; title=&quot;这里&quot;&gt;这里&lt;/a&gt;）。我最后也是直接看原文和视频来理解的（虽然英文很烂，硬着头皮看，不过视频还好，没有什么听力难点，&lt;strong&gt;学好英语&lt;/strong&gt;对程序员太重要了），这里整理一下我的理解。我认为重点是要理解状态转移之间的关系，这是 &lt;code&gt;DFM&lt;/code&gt; 的核心。&lt;/p&gt;
&lt;p&gt;首先贴上《算法》中的对字符串 &lt;code&gt;ABABAC&lt;/code&gt; 作为 &lt;code&gt;pattern&lt;/code&gt; 的状态机图示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/kmp3.BR5EEYae_Z234gNN.webp&quot; alt=&quot;kmp3&quot; title=&quot;kmp3&quot;&gt;&lt;/p&gt;
&lt;p&gt;对这两个图做一个简单的解释，上方的是一个矩阵，下方是一个图表，他们表达的是同一个有限状态机，只是形式不同。这两张图表达的就是 &lt;code&gt;ABABAC&lt;/code&gt; 进行匹配的状态机。所有的数字表示的都是状态，而这个状态数字可以理解成当前已经成功匹配了几个字符，比如状态 &lt;code&gt;4&lt;/code&gt; 表示已经成功匹配到了 &lt;code&gt;ABAB&lt;/code&gt; 才进入状态 &lt;code&gt;4&lt;/code&gt;。下方图表的箭头则表示状态的转移，注意每个箭头上都有一个字母，表示这个转移是因为这个字母的输入。以状态 &lt;code&gt;3&lt;/code&gt; 为例，我们此时已经成功匹配到了 &lt;code&gt;ABA&lt;/code&gt; 三个字符，下面我们接受的输入有三种可能 &lt;code&gt;A B C&lt;/code&gt;，当接收 &lt;code&gt;B&lt;/code&gt; 时，我们成功匹配进入状态 &lt;code&gt;4&lt;/code&gt;，当接收 &lt;code&gt;A&lt;/code&gt; 时，我们回到了状态 &lt;code&gt;1&lt;/code&gt;，当我们接收 &lt;code&gt;C&lt;/code&gt; 时回到状态 &lt;code&gt;0&lt;/code&gt;。我们的状态机接收两个参数，一个是当前状态，一个是输入，得到的结果就是应该转移的状态。在矩阵图中，&lt;code&gt;j&lt;/code&gt; 是当前所处的状态，&lt;code&gt;pat.charAt(j)&lt;/code&gt; 表示 &lt;code&gt;pattern&lt;/code&gt; 在当前要匹配的字符，纵向的 &lt;code&gt;dfa[][j]&lt;/code&gt; 则表示输入，中间的矩阵表示要转移到的状态，比如矩阵中位置 &lt;code&gt;[0,0]&lt;/code&gt; 的 &lt;code&gt;1&lt;/code&gt; 表示，在状态 &lt;code&gt;0&lt;/code&gt; 接收输入 &lt;code&gt;A&lt;/code&gt;，状态转移到 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里需要注意，我们的状态机只和 &lt;code&gt;pattern&lt;/code&gt; 有关，就是不管我们要进行匹配的字符串是什么，只要我们的 &lt;code&gt;pattern&lt;/code&gt; 不变，状态机就不变，举个例子就是不管待匹配的字符串是 &lt;code&gt;ababababaaacaba&lt;/code&gt; 还是 &lt;code&gt;abafabafagaba&lt;/code&gt;，只要 &lt;code&gt;pattern&lt;/code&gt; 是 &lt;code&gt;ABABAC&lt;/code&gt;，状态机就不变，所以在分析状态机的时候我们只需要分析在 &lt;code&gt;pattern&lt;/code&gt; 中出现的字符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;我们将待匹配的字符串叫做 string，把要匹配的子串成为 pattern&lt;/strong&gt; 这里的状态是我们自己分析的，但是要写成程序我们必须抽象出一个所有 &lt;code&gt;pattern&lt;/code&gt; 的状态机规律。我们想一想我们是如何得到状态转移 (&lt;code&gt;state transition&lt;/code&gt;) 的结果的。以状态 &lt;code&gt;5&lt;/code&gt; 为例，当我们在状态 &lt;code&gt;5&lt;/code&gt; 匹配失败 (&lt;code&gt;mismatch&lt;/code&gt;) 的时候，我们如何确定我们要转移到哪个状态呢。这里可以借助上面的暴力解法来理解，当我们在状态 &lt;code&gt;5&lt;/code&gt; 匹配失败的时候，我们已经成功匹配了 &lt;code&gt;ABABA&lt;/code&gt;，此时按照暴力解法，我们应该是从 &lt;code&gt;BABA&lt;/code&gt; 开始重新匹配。我们可以确定的是 &lt;code&gt;BABA&lt;/code&gt; 也是 &lt;code&gt;pattern&lt;/code&gt; 的子串，所以其实我们只要搞清楚 &lt;code&gt;BABA&lt;/code&gt; 在状态机中执行后会到什么状态，这个状态被称为 &lt;code&gt;restart state&lt;/code&gt;。也就是说，当我们在 &lt;code&gt;j&lt;/code&gt; 处匹配失败，我们只要知道知道 &lt;code&gt;1 ~ (j-1)&lt;/code&gt; 这个子串在状态机中运行后的状态，这个状态就是状态 &lt;code&gt;j&lt;/code&gt; 的重启状态 &lt;code&gt;restart state&lt;/code&gt;，所以 &lt;code&gt;j&lt;/code&gt; 处的某个输入 &lt;code&gt;c&lt;/code&gt; 匹配失败了，我们获得其状态转移的方法就是将这个输入 &lt;code&gt;c&lt;/code&gt; 交给重启状态即可。而 &lt;code&gt;j&lt;/code&gt; 的 &lt;code&gt;restart state&lt;/code&gt; 也有自己的 &lt;code&gt;restart state&lt;/code&gt;，我们只要找到重启状态之间的关系就可以用迭代来解决这个问题，因为状态 &lt;code&gt;0&lt;/code&gt; 没有 &lt;code&gt;restart&lt;/code&gt; 状态，而状态 &lt;code&gt;1&lt;/code&gt; 的 &lt;code&gt;restart state&lt;/code&gt; 肯定是 &lt;code&gt;0&lt;/code&gt;（状态 &lt;code&gt;1&lt;/code&gt; 去掉首位就是个空字符串），有了初始状态和迭代规律，我们并不需要真的把 &lt;code&gt;BABA&lt;/code&gt; 放到状态机中运行然后记录结果，只是帮助我们理解这个算法。&lt;/p&gt;
&lt;p&gt;我们用一个变量 &lt;code&gt;X&lt;/code&gt; 表示当前状态下匹配失败要跳转到的状态。我们上面说过，当状态 &lt;code&gt;5&lt;/code&gt; 匹配失败时，我们实际上是将 &lt;code&gt;ABABA&lt;/code&gt; 去掉首位变成 &lt;code&gt;BABA&lt;/code&gt; 进入状态机运算得到的结果作为状态 &lt;code&gt;5&lt;/code&gt; 的 &lt;code&gt;X&lt;/code&gt;，也就是说当我们在状态 &lt;code&gt;j&lt;/code&gt; 匹配失败时，我们是将 &lt;code&gt;pattern&lt;/code&gt; 中已经匹配成功的 &lt;code&gt;pattern[0] - pattern[j - 1]&lt;/code&gt;,去掉首位，也就是 &lt;code&gt;pattern[1] - pattern[j - 1]&lt;/code&gt; 放入状态机中运算，得到的结果作为 &lt;code&gt;X&lt;/code&gt;。我们还是以 &lt;code&gt;ABABAC&lt;/code&gt; 为例，分析一下各个状态的 &lt;code&gt;X&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;语言能力不太好，下面的表述可能不太清晰。主要有三个概念：X是当前状态的restart state，目标字符是当前状态能匹配成功的字符，还有一个就是求X需要的当前状态已匹配的字符串（去掉首位，比如上面的 BABA)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;状态 &lt;code&gt;0&lt;/code&gt;，目标字符 &lt;code&gt;A&lt;/code&gt;，一个字符都没有匹配成功，不考虑。&lt;/li&gt;
&lt;li&gt;状态 &lt;code&gt;1&lt;/code&gt;，目标字符 &lt;code&gt;B&lt;/code&gt;，匹配成功 &lt;code&gt;A&lt;/code&gt; 去掉首位为 &lt;code&gt;&quot;&quot;&lt;/code&gt;，放入状态机中运算结果为 &lt;code&gt;0&lt;/code&gt;，&lt;code&gt;X&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;状态 &lt;code&gt;2&lt;/code&gt;，目标字符 &lt;code&gt;A&lt;/code&gt;，匹配成功 &lt;code&gt;AB&lt;/code&gt; 去掉首位为 &lt;code&gt;B&lt;/code&gt;，放入状态机中运算结果为 &lt;code&gt;0&lt;/code&gt;，&lt;code&gt;X&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;状态 &lt;code&gt;3&lt;/code&gt;，目标字符 &lt;code&gt;B&lt;/code&gt;，匹配成功 &lt;code&gt;ABA&lt;/code&gt; 去掉首位为 &lt;code&gt;BA&lt;/code&gt;, 放入状态机中运算结果为 &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;X&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;状态 &lt;code&gt;4&lt;/code&gt;，目标字符 &lt;code&gt;A&lt;/code&gt;，匹配成功 &lt;code&gt;ABAB&lt;/code&gt; 去掉首位为 &lt;code&gt;BAB&lt;/code&gt;，放入状态机中运算结果为 &lt;code&gt;2&lt;/code&gt;，&lt;code&gt;X&lt;/code&gt; 为 &lt;code&gt;2&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;状态 &lt;code&gt;5&lt;/code&gt;，目标字符 &lt;code&gt;C&lt;/code&gt;，匹配成功 &lt;code&gt;ABABA&lt;/code&gt; 去掉首位为 &lt;code&gt;BABA&lt;/code&gt;，放入状态机中运算结果为 &lt;code&gt;3&lt;/code&gt;，&lt;code&gt;X&lt;/code&gt; 为 &lt;code&gt;3&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我观察一下可以发现，去掉没意义的状态 &lt;code&gt;0&lt;/code&gt;，剩下的五个状态，求 &lt;code&gt;X&lt;/code&gt; 所用的字符串分别是 &lt;code&gt;&quot;&quot;&lt;/code&gt;，&lt;code&gt;B&lt;/code&gt;，&lt;code&gt;BA&lt;/code&gt;，&lt;code&gt;BAB&lt;/code&gt;,&lt;code&gt;BABA&lt;/code&gt;，而目标字符分别是 &lt;code&gt;B&lt;/code&gt;，&lt;code&gt;A&lt;/code&gt;，&lt;code&gt;B&lt;/code&gt;，&lt;code&gt;A&lt;/code&gt;，&lt;code&gt;C&lt;/code&gt;，状态 &lt;code&gt;2&lt;/code&gt; 的 &lt;code&gt;X&lt;/code&gt; 值就是状态 &lt;code&gt;1&lt;/code&gt; 的 &lt;code&gt;X&lt;/code&gt; 对应状态输入状态 &lt;code&gt;2&lt;/code&gt;的目标字符 &lt;code&gt;A&lt;/code&gt; 得到的状态。&lt;strong&gt;注意这里的X代表的也是一个状态&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可能有点绕，不是很好表述，状态 &lt;code&gt;5&lt;/code&gt; 的 &lt;code&gt;X&lt;/code&gt; 就是 &lt;code&gt;BABA&lt;/code&gt; 在状态机中的运算结果，状态 &lt;code&gt;4&lt;/code&gt; 的 &lt;code&gt;X&lt;/code&gt; 就是 &lt;code&gt;BAB&lt;/code&gt; 在状态机中的运算结果，&lt;code&gt;BABA&lt;/code&gt; 的运算结果就是 &lt;code&gt;BAB&lt;/code&gt; 的运算结果再输入一个 &lt;code&gt;A&lt;/code&gt;，而这个 &lt;code&gt;A&lt;/code&gt; 就是状态 &lt;code&gt;4&lt;/code&gt; 的目标字符。也就是说我们知道状态 &lt;code&gt;4&lt;/code&gt; 的 &lt;code&gt;X&lt;/code&gt; 就已经能求得状态 &lt;code&gt;5&lt;/code&gt; 的 &lt;code&gt;X&lt;/code&gt;，以此类推我们从状态 &lt;code&gt;1&lt;/code&gt; 就能求到之后所有状态的 &lt;code&gt;X&lt;/code&gt;，而状态 &lt;code&gt;1&lt;/code&gt; 的 &lt;code&gt;X&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;restart state&lt;/code&gt; 我认为两个作用： 1. &lt;code&gt;j&lt;/code&gt; 和 &lt;code&gt;j-1&lt;/code&gt; 状态的 &lt;code&gt;restart state&lt;/code&gt; 有明确的迭代关系，我们可以利用状态 &lt;code&gt;1&lt;/code&gt; 的 &lt;code&gt;restart state&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;和迭代关系两个信息求出所有状态的 &lt;code&gt;restart state&lt;/code&gt;。 2. 当前状态的不匹配输入是交给 &lt;code&gt;restart state&lt;/code&gt; 处理的（如果这个输入给 &lt;code&gt;restart state&lt;/code&gt; 也不匹配，那么就会找 &lt;code&gt;restart state&lt;/code&gt; 的 &lt;code&gt;restart state&lt;/code&gt;也就是说，我们某个状态的不匹配输入 &lt;code&gt;c&lt;/code&gt; 的结果是从其 &lt;code&gt;restart state&lt;/code&gt; 上复制的。&lt;/p&gt;
&lt;p&gt;根据这两条规律，我们只要有状态 &lt;code&gt;0&lt;/code&gt; 的所有输入结果，我们就可以得到所有状态的状态跳转矩阵。具体到代码上，我们可以创建一个二维数组 &lt;code&gt;dfa[][]&lt;/code&gt;，我们初始化 &lt;code&gt;dfa[0][]&lt;/code&gt; 的所有项，然后根据上面两条规律，可以生成整个 &lt;code&gt;dfa&lt;/code&gt; 矩阵，我们的问题也就迎刃而解。&lt;/p&gt;
&lt;p&gt;《算法》第四版中有两张图可以来辅助理解。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/kmp6.C5uzDTrt_Z2lyubD.webp&quot; alt=&quot;kmp6&quot; title=&quot;kmp6&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/kmp5.D1OcVA2L_ZQoii3.webp&quot; alt=&quot;kmp5&quot; title=&quot;kmp5&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;下面就是具体代码，我们可以分成两个部分，一个部分是检索子字符串，另一部分是管理状态机。第一部分的代码非常简单，循环字符串，将字符交给状态机，当字符循环完毕没匹配成功则返回 &lt;code&gt;false&lt;/code&gt;，若匹配成功则返回 &lt;code&gt;success&lt;/code&gt;。第二部分则是生成对应 &lt;code&gt;pattern&lt;/code&gt; 的状态机。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function match(s, p) {
  let M = p.length,
    N = s.length,
    i = 0,
    j = 0,
    dfa = KMP(p)

  for (; i &amp;#x3C; N &amp;#x26;&amp;#x26; j &amp;#x3C; M; i++) j = dfa[j][s.charCodeAt(i)]
  if (j === M) return &apos;match success at index of &apos; + (i - M)
  return &apos;false&apos;
}

function KMP(p) {
  let X = 0,
    R = 256,
    M = p.length,
    dfa = new Array(M)
  for (let i = 0; i &amp;#x3C; dfa.length; i++) dfa[i] = new Array(R) //创建长度为dfa.length的数组，每一项为一个对象

  //初始化dfa[0],即初始的X状态，后面的状态要用这一状态来复制
  for (let i = 0; i &amp;#x3C; R; i++) dfa[0][i] = 0
  dfa[0][p.charCodeAt(0)] = 1 //状态0时匹配到第一位总是进入状态1

  //生成后面的状态机
  for (let j = 1; j &amp;#x3C; M; j++) {
    for (let c = 0; c &amp;#x3C; R; c++) dfa[j][c] = dfa[X][c] //设置状态j的匹配失败项，从状态X复制
    dfa[j][p.charCodeAt(j)] = j + 1 //设置匹配成功项
    X = dfa[X][p.charCodeAt(j)] //计算下一状态的 X
  }
  return dfa
}

console.log(match(&apos;asdfasdfsafabababafabababacasdf&apos;, &apos;ababac&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的代码使用 &lt;code&gt;javascript&lt;/code&gt; 写的，所以 &lt;code&gt;KMP&lt;/code&gt; 中我们需要手动初始化状态 &lt;code&gt;0&lt;/code&gt;（也就是默认的 &lt;code&gt;X&lt;/code&gt;）的状态跳转。《算法》中的代码使 &lt;code&gt;Java&lt;/code&gt;，所以整形数组的项的默认值是 &lt;code&gt;0&lt;/code&gt;，不需要处理。 &lt;code&gt;javascript&lt;/code&gt; 中，&lt;code&gt;new Array()&lt;/code&gt; 方法创建的固定长度的数组其实只是一个确定了 &lt;code&gt;length&lt;/code&gt; 属性的数组对象，里面并没有任何元素（不要理解成都是 &lt;code&gt;undefined&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;还有一点就是算法中是用 &lt;code&gt;charAt&lt;/code&gt;，也就是 &lt;code&gt;javascript&lt;/code&gt; 中的 &lt;code&gt;charCodeAt&lt;/code&gt; 方法来设置状态机的输入，每个状态对应 &lt;code&gt;256&lt;/code&gt; 种输入，也就是扩展 &lt;code&gt;ASCII&lt;/code&gt; 码的全部字符数，这样做的好处是我们完全不需要考虑 &lt;code&gt;match&lt;/code&gt; 函数中的输入字符（只要在扩展 &lt;code&gt;ASCII&lt;/code&gt; 范围内）。不过这样创建的二维数组特别大，我认为可以先对 &lt;code&gt;pattern&lt;/code&gt; 进行一个处理，取出 &lt;code&gt;pattern&lt;/code&gt; 中所有的不重复的字符放入对象，只要匹配的字符不在这个对象中，直接将 &lt;code&gt;j&lt;/code&gt; 设置为 &lt;code&gt;0&lt;/code&gt;。具体代码如下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function match(s, p) {
  let M = p.length,
    N = s.length,
    i = 0,
    j = 0,
    o = {},
    dfa = KMP(p)

  //生成pattern中不重复元素的对象
  for (let t of p) {
    if (!o[t]) o[t] = t
  }

  for (; i &amp;#x3C; N &amp;#x26;&amp;#x26; j &amp;#x3C; M; i++) {
    j = !!o[s[i]] ? dfa[j][s[i]] : 0
  }
  if (j === M) return &apos;match success at index of &apos; + (i - M)
  return &apos;false&apos;
}

function KMP(p) {
  let X = 0,
    R = 256,
    M = p.length,
    o = {},
    dfa = new Array(M)

  //生成pattern中不重复元素的对象
  for (let t of p) {
    if (!o[t]) o[t] = t
  }

  for (let i = 0; i &amp;#x3C; dfa.length; i++) dfa[i] = { ...o } //创建长度为dfa.length的数组，每一项为一个对象

  //初始化dfa[0],即初始的X状态，后面的状态要用这一状态来复制
  for (let key in dfa[0]) {
    dfa[0][key] = 0
  }
  dfa[0][p[0]] = 1 //状态0时匹配到第一位总是进入状态1

  //生成后面的状态机
  for (let j = 1; j &amp;#x3C; M; j++) {
    for (let c in o) dfa[j][c] = dfa[X][c] //设置状态j的匹配失败项，从状态X复制
    dfa[j][p[j]] = j + 1 //设置匹配成功项
    X = dfa[X][p[j]] //计算下一状态的 X
  }
  console.log(dfa)
  return dfa
}

console.log(match(&apos;asdfasdfsafabababafabababacasdf&apos;, &apos;ababac&apos;))

//[
//  { a: 1, b: 0, c: 0 },
//  { a: 1, b: 2, c: 0 },
//  { a: 3, b: 0, c: 0 },
//  { a: 1, b: 4, c: 0 },
//  { a: 5, b: 0, c: 0 },
//  { a: 1, b: 4, c: 6 }
//]
//match success at index of 21
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;状态机的运用让我们在搜索的效率上得到了很大的提高，但是我们需要额外维护一个状态机，也是用空间换时间。暴力解法的时间复杂度最坏情况是 &lt;code&gt;O(m*n)&lt;/code&gt;，&lt;code&gt;DFM&lt;/code&gt; 的解法的时间复杂度则为 &lt;code&gt;O(n)&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;PTM 部分匹配表&lt;/h2&gt;
&lt;p&gt;在搜索引擎上能找到的关于 &lt;code&gt;KMP&lt;/code&gt; 算法的文章大部分都是 &lt;code&gt;PTM&lt;/code&gt; 实现的，所以我也是最先看的 &lt;code&gt;PTM&lt;/code&gt; 实现。相对于 &lt;code&gt;DFM&lt;/code&gt; 的实现，&lt;code&gt;PTM&lt;/code&gt; 的实现可能更简单粗暴一点，也更好理解。而且 &lt;code&gt;PTM&lt;/code&gt; 的思维可能会影响你对 &lt;code&gt;DFM&lt;/code&gt; 的理解。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PMT&lt;/code&gt; 叫做部分匹配表，其实就是把 &lt;code&gt;DFM&lt;/code&gt; 中每个状态对应的 &lt;code&gt;X&lt;/code&gt; 的值进行单独计算。在 &lt;code&gt;DFM&lt;/code&gt; 中我们对 &lt;code&gt;X&lt;/code&gt; 的理解是一个在匹配失败的时候跳转的状态，在 &lt;code&gt;PTM&lt;/code&gt; 中对这个值的理解就是当前状态下的最长前后缀。&lt;/p&gt;
&lt;p&gt;我们先来介绍前后缀的概念。对于一个字符串，包含该字符串的首位且不包含末位的子串就是这个字符串的前缀，而包含该字符串的末位且不包含首位的子串就是这个字符串的后缀。比如对于字符串 &lt;code&gt;ababa&lt;/code&gt; ，他的前缀包括 &lt;code&gt;a ab aba abab&lt;/code&gt;， 他的后缀包括 &lt;code&gt;a ba aba abab&lt;/code&gt;。我们在 &lt;code&gt;DFM&lt;/code&gt; 中找的 &lt;code&gt;X&lt;/code&gt; 在 &lt;code&gt;PTM&lt;/code&gt; 的理解中就是寻找最长的相同前后缀。比如 &lt;code&gt;ababa&lt;/code&gt; 的最长相同前后缀就是 &lt;code&gt;aba&lt;/code&gt;，长度为 &lt;code&gt;3&lt;/code&gt;。我们在 &lt;code&gt;DFM&lt;/code&gt; 中的 &lt;code&gt;ABABAC&lt;/code&gt; 的 &lt;code&gt;j&lt;/code&gt; 为 &lt;code&gt;5&lt;/code&gt; 的情况下对应的 &lt;code&gt;X&lt;/code&gt; 就是 &lt;code&gt;3&lt;/code&gt;。这个原理其实也很简单，看下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/kmp7.Dzj3BRzw_Z16cGz1.webp&quot; alt=&quot;kmp7&quot; title=&quot;kmp7&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DFM&lt;/code&gt; 中我们理解这张图是我们在 &lt;code&gt;j&lt;/code&gt; 为 &lt;code&gt;5&lt;/code&gt; 的状态匹配失败，此时我们将前面匹配成功的五位 &lt;code&gt;ABABA&lt;/code&gt; 去掉首位再放到状态机中计算得到的输出作为 &lt;code&gt;X&lt;/code&gt;。在 &lt;code&gt;PTM&lt;/code&gt; 中其实就没有所谓的状态了，我们在当前这一位匹配失败了，就找出前面匹配成功的字符串 &lt;code&gt;ABABA&lt;/code&gt; 中的最长前后缀，也就是 &lt;code&gt;ABA&lt;/code&gt;。其实我们仔细思考一下，这两种方法其实并没有本质的区别，把 &lt;code&gt;BABA&lt;/code&gt; 放到状态机中计算就是找最长前后缀，只不过 &lt;code&gt;DFM&lt;/code&gt; 更巧妙一些，我们不需要真的去找最长的前后缀。可以说 &lt;code&gt;DFM&lt;/code&gt; 的做法是着眼于各个状态的关系，用这个关系来解决问题；而 &lt;code&gt;PTM&lt;/code&gt; 则更简单直接一点，我这个状态的问题就利用这个状态自己解决。&lt;/p&gt;
&lt;p&gt;所以在 &lt;code&gt;PTM&lt;/code&gt; 中我们可以得出 &lt;code&gt;pattern&lt;/code&gt; 的各位对应的最长相同前后缀的长度，这个长度所形成的表就叫做部分匹配表。我们用两个 &lt;code&gt;pattern&lt;/code&gt; 来看一下对应的 &lt;code&gt;PTM&lt;/code&gt; 。第一个是 &lt;code&gt;ababac&lt;/code&gt;，第二个是 &lt;code&gt;abababca&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/kmp8.BTQgQWus_2eC44e.webp&quot; alt=&quot;kmp8&quot; title=&quot;kmp8&quot;&gt;&lt;/p&gt;
&lt;p&gt;如何求这个 &lt;code&gt;PTM&lt;/code&gt; 呢？其实 &lt;code&gt;PTM&lt;/code&gt; 就是 &lt;code&gt;pattern&lt;/code&gt; 自己和自己匹配然后得出的结果，因为我们在用 &lt;code&gt;pattern&lt;/code&gt; 和字符串匹配的时候，匹配成功的组合必然就在 &lt;code&gt;pattern&lt;/code&gt; 中，匹配失败的时候我们需要移动的也只是 &lt;code&gt;pattern&lt;/code&gt; 的指针，所以 &lt;code&gt;PTM&lt;/code&gt; 只和 &lt;code&gt;pattern&lt;/code&gt; 相关。大概的逻辑是：从 &lt;code&gt;pattern[1]&lt;/code&gt; 开始不断用 &lt;code&gt;pattern&lt;/code&gt; 进行匹配，用两个指针分别指向两个 &lt;code&gt;pattern&lt;/code&gt; 当前的位置（如下图），用一个数组 &lt;code&gt;arr&lt;/code&gt; 储存 &lt;code&gt;i&lt;/code&gt; 指针对应位的最长前后缀的长度。匹配成功则两个指针都右移，&lt;code&gt;arr[i] = j&lt;/code&gt;（得到当前位置的最长前后缀）；失败则 &lt;code&gt;i&lt;/code&gt; 不动，&lt;code&gt;j&lt;/code&gt; 赋值为 &lt;code&gt;arr[j-1]&lt;/code&gt; 的值（在运行过程中就利用我们已经得到的结果）；如果 &lt;code&gt;j&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;，则 &lt;code&gt;arr[i] = 0&lt;/code&gt; 并且 &lt;code&gt;i&lt;/code&gt; 向右移动一位。整个过程就是 &lt;code&gt;DFM&lt;/code&gt; 中的某一位匹配失败时，用前面匹配成功的部分去掉第一位放进状态机匹配的详细过程。详细的过程和代码看下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/kmp11.0mezsUix_2dPhUi.webp&quot; alt=&quot;kmp11&quot; title=&quot;kmp10&quot;&gt; &lt;img src=&quot;https://clloz.com/_astro/kmp12.CU92tDdG_28ch1b.webp&quot; alt=&quot;kmp12&quot; title=&quot;kmp10&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function PMT(p) {
  let i = 1, //i和j错开
    j = 0,
    arr = [0] //第一位的最长前后缀为0

  while (i &amp;#x3C; p.length) {
    if (p[i] === p[j]) {
      j++
      arr[i] = j
      i++
    } else if (j === 0) {
      arr[i] = 0
      i++
    } else {
      j = arr[j - 1] //匹配失败的时候，j回到j-1位的最长前后缀的位置
    }
  }
  return arr
}
console.log(PMT(&apos;ababac&apos;))
console.log(PMT(&apos;abababca&apos;))
//[ 0, 0, 1, 2, 3, 0 ]
//[ 0, 0, 1, 2, 3, 4, 0, 1 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们上面求得的这个 &lt;code&gt;PTM&lt;/code&gt; 在实际编码中并不方便，我们上面的 &lt;code&gt;i&lt;/code&gt; 指针实际上相当于我们在 &lt;code&gt;i + 1&lt;/code&gt; 位匹配失败，求得前 &lt;code&gt;1 ~ i&lt;/code&gt; 位的最长相同前后缀（即 &lt;code&gt;DFM&lt;/code&gt; 中的把 &lt;code&gt;1 ~ i&lt;/code&gt; 位放到状态机中执行），所以 &lt;code&gt;i&lt;/code&gt; 位的 &lt;code&gt;PTM&lt;/code&gt; 值其实是给 &lt;code&gt;i + 1&lt;/code&gt; 位用的，基于这个原因，我们将 &lt;code&gt;PTM&lt;/code&gt; 表向右移动一位，最左边补上一个 &lt;code&gt;-1&lt;/code&gt;（单纯是为了编程方便），最右边的位舍去，得到如下的 &lt;code&gt;next&lt;/code&gt; 表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/kmp9.CCGArdVp_Z1f5O31.webp&quot; alt=&quot;kmp9&quot; title=&quot;kmp9&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们也可以直接求出 &lt;code&gt;next&lt;/code&gt; 数组，代码如下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function PTM2(p) {
  let arr = [-1]
  let i = 0,
    j = -1

  while (i &amp;#x3C; p.length) {
    if (j == -1 || p[i] == p[j]) {
      ++i
      ++j
      arr[i] = j
    } else j = arr[j]
  }
  //arr.pop();
  return arr
}
console.log(PTM2(&apos;ababac&apos;))
console.log(PTM2(&apos;abababca&apos;))
//[ -1, 0, 0, 1, 2, 3, 0 ]
//[ -1, 0, 0, 1, 2, 3, 4, 0, 1 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看出我们设置 &lt;code&gt;arr[0]&lt;/code&gt; 和 &lt;code&gt;j&lt;/code&gt; 为 &lt;code&gt;-1&lt;/code&gt; 的情况下，我们可以将 &lt;code&gt;j&lt;/code&gt; 回到第一位和匹配成功两个状况合并，因为 &lt;code&gt;j&lt;/code&gt; 回到第一位理论上只要移动 &lt;code&gt;i&lt;/code&gt;，但当我们设置 &lt;code&gt;j&lt;/code&gt; 为 &lt;code&gt;-1&lt;/code&gt; 的时候，我们也需要 &lt;code&gt;j++&lt;/code&gt;，这就和匹配成功的逻辑相同了，让我们的代码更简单更清晰。得到 &lt;code&gt;next&lt;/code&gt; 数组之后剩下的就是写 &lt;code&gt;match&lt;/code&gt; 函数，逻辑和 &lt;code&gt;next&lt;/code&gt; 数组的逻辑基本相同。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function match(s, p) {
  let i = 0,
    j = 0,
    arr = PMT(p)

  while (i &amp;#x3C; s.length) {
    if (j === -1 || s[i] === p[j]) {
      if (j === arr.length - 1) return &apos;success&apos;
      i++
      j++
    } else {
      j = arr[j]
    }
  }
  return &apos;failed&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;我个人认为动态规划是有限状态机的一种特殊形态。&lt;code&gt;DFM&lt;/code&gt; 的实现花了很长时间才领会，主要就是因 &lt;code&gt;PTM&lt;/code&gt; 的想法有点先入为主的理解，他们的本质是一样的，但是 &lt;code&gt;DFM&lt;/code&gt; 用状态之间的关系迭代直接避免了比较复杂的前后缀计算，我们只关心每个状态之间的关系，有一点动态规划中走楼梯问题的感觉。不过&lt;code&gt;KMP&lt;/code&gt; 算法似乎也不是查找子串效率最高的方法，在 《算法》第四版中还有一个 &lt;code&gt;Boyer–Moore&lt;/code&gt; 算法。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA&quot; title=&quot;有限状态机 - Wikipedia&quot;&gt;有限状态机 - Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html&quot; title=&quot;字符串匹配的KMP算法 - 阮一峰&quot;&gt;字符串匹配的KMP算法 - 阮一峰&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/5eb7b5656fb9a0437e0e9596#sec-5&quot; title=&quot;KMP 算法的两种实现&quot;&gt;KMP 算法的两种实现&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cgiirw.github.io/2018/04/22/KMP/&quot; title=&quot;使用确定有限状态自动机解KMP算法&quot;&gt;使用确定有限状态自动机解KMP算法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;《算法 第四版》&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/21923021/answer/281346746&quot; title=&quot;如何更好地掌握KMP算法&quot;&gt;如何更好地掌握KMP算法&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>YDNJS学习笔记：上卷-第二部分</title><link>https://clloz.com/blog/ydnjs-note-2</link><guid isPermaLink="true">https://clloz.com/blog/ydnjs-note-2</guid><pubDate>Sat, 18 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;YDNJS 学习笔记&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/16/ydnjs-note/&quot; title=&quot;YDNJS学习笔记：上卷第一部分&quot;&gt;YDNJS学习笔记：上卷第一部分&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;承接第一卷第一部分&lt;/p&gt;
&lt;h2&gt;this&lt;/h2&gt;
&lt;p&gt;关于 &lt;code&gt;this&lt;/code&gt; 的行为我已经写过一篇文章：&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/30/js-this/&quot; title=&quot;JavaScript中this的指向&quot;&gt;JavaScript中this的指向&lt;/a&gt;；关于 &lt;code&gt;bind call apply&lt;/code&gt; 也写过一篇文章：&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/03/apply-call-bind/&quot; title=&quot;apply和call, bing方法的应用&quot;&gt;apply和call, bing方法的应用&lt;/a&gt;。所以这一部分重复的内容就不写了。只补充一些自己没有掌握的细节。&lt;/p&gt;
&lt;h2&gt;为什么要使用 this&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt; 给我提供了一种优雅的方式来隐式传递一个对象的引用，因此我们可以将 &lt;code&gt;API&lt;/code&gt; 设计的更加简介并且易于复用。随着代码越来越复杂，显示传递上下文的方式会让代码越来越混乱而难以理解和维护。特别是在 &lt;code&gt;JavaScript&lt;/code&gt; 这样一个基于原型的面向对象语言中，&lt;code&gt;this&lt;/code&gt; 更加显得重要。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 用this隐式传递对象的引用
function identify() {
  return this.name.toUpperCase()
}

var me = {
  name: &apos;Kyle&apos;
}

var you = {
  name: &apos;Reader&apos;
}

identify.call(me) // KYLE
identify.call(you) // READER

//显式传入对象
function identify(context) {
  return context.name.toUpperCase()
}
function speak(context) {
  var greeting = &quot;Hello, I&apos;m &quot; + identify(context)
  console.log(greeting)
}
identify(you) // READER
speak(me) //hello, I&apos;m KYLE
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;熟练的运用 &lt;code&gt;devtools&lt;/code&gt; 能够让我们更有效率地调试我们的代码。比如查看调用栈。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;采用默认调用的函数只有内部使用严格模式才能限制 &lt;code&gt;this&lt;/code&gt; 绑定到全局对象上，如果只是在函数调用的部分使用严格模式，而函数体内部使用非严格模式，函数内的 &lt;code&gt;this&lt;/code&gt; 还是会绑定到全局对象上。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//函数内部为严格模式
function foo() {
  &apos;use strict&apos;
  console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefined

//函数内部非严格，调用环境严格
function foo() {
  console.log(this.a)
}
var a = 2
;(function () {
  &apos;use strict&apos;
  foo() // 2
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;不要在代码中混合使用严格模式和非严格模式，可能造成兼容性问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;参数的传递也是一种隐式的赋值，在传递过程中也会丢失对象的绑定。不管是我们自己定义的回调函数还是内置方法的回调函数都一样，因为实参传递给形参的时候已经丢失了原来绑定的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function foo() {
  console.log(this.a)
}
function doFoo(fn) {
  // fn 其实引用的是 foo fn(); // &amp;#x3C;-- 调用位置!
}
var obj = {
  a: 2,
  foo: foo
}
var a = &apos;oops, global&apos; // a 是全局对象的属性 doFoo( obj.foo ); // &quot;oops, global&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;硬绑定在 &lt;code&gt;ES5&lt;/code&gt; 中已经提供了标准化的内置方法 &lt;code&gt;bind&lt;/code&gt;，它的原型如下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function foo(something) {
  console.log(this.a, something)
  return this.a + something
}
// 简单的辅助绑定函数
function bind(fn, obj) {
  return function () {
    return fn.apply(obj, arguments)
  }
}
var obj = {
  a: 2
}
var bar = bind(foo, obj)
var b = bar(3) // 2 3
console.log(b) // 5

//ES5的bind
function foo(something) {
  console.log(this.a, something)
  return this.a + something
}

var obj = {
  a: 2
}

var bar = foo.bind(obj)

var b = bar(3) // 2 3
console.log(b) // 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三方库的许多函数，以及 &lt;code&gt;JavaScript&lt;/code&gt; 语言和宿主环境中许多新的内置函数，都提供了一 个可选的参数，通常被称为“上下文”(&lt;code&gt;context&lt;/code&gt;)，其作用和 &lt;code&gt;bind(..)&lt;/code&gt; 一样，确保你的回调 函数使用指定的 &lt;code&gt;this&lt;/code&gt;。这些函数实际上就是通过 &lt;code&gt;call(..)&lt;/code&gt; 或者 &lt;code&gt;apply(..)&lt;/code&gt; 实现了显式绑定，这样你可以少写一些代码。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;四种绑定模式：默认绑定，隐式绑定，显示绑定和 &lt;code&gt;new&lt;/code&gt; 绑定，优先级从低到高。其中比较容易忽略的一点就是 &lt;code&gt;bind&lt;/code&gt; 和 &lt;code&gt;new&lt;/code&gt; 的优先级以及应用。如果我们对一个 &lt;code&gt;bind&lt;/code&gt; 硬绑定的函数执行 &lt;code&gt;new&lt;/code&gt; 运算，那么函数执行过程中 &lt;code&gt;new&lt;/code&gt; 运算新创建的对象会覆盖 &lt;code&gt;bind&lt;/code&gt; 绑定的对象。书中有一处我觉得表述的比较有问题，就是模拟 &lt;code&gt;bind&lt;/code&gt; 的那个函数的运行结果，代码如下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//模拟 bind
function bind(fn, obj) {
  return function () {
    fn.apply(obj, arguments)
  }
}

function foo(something) {
  this.a = something
}

var obj1 = {}

var bar = bind(foo, obj1)

bar(2)
console.log(obj1.a) // 2

var baz = new bar(3)
console.log(obj1.a) // 3
console.log(baz.a) // undefined

//标准bind
function foo(something) {
  this.a = something
}

var obj1 = {}
var bar = foo.bind(obj1)
bar(2)

console.log(obj1.a) // 2
var baz = new bar(3)
console.log(obj1.a) // 2
console.log(baz.a) // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两段代码看似没什么不同，但是模拟 &lt;code&gt;bind&lt;/code&gt; 中最后实际 &lt;code&gt;new&lt;/code&gt; 的函数是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function() {
        fn.apply( obj, arguments );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;fn&lt;/code&gt; 外面嵌套了一层，而标准中的则是直接 &lt;code&gt;new&lt;/code&gt; 的 &lt;code&gt;bind&lt;/code&gt; 返回的函数。内部嵌套的函数中的 &lt;code&gt;this&lt;/code&gt; 和 所在环境的 &lt;code&gt;this&lt;/code&gt; 是不相关的，所以这两者的类比其实没什么意义。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;嵌套的原因主要是 &lt;code&gt;apply&lt;/code&gt; 和 &lt;code&gt;call&lt;/code&gt; 是立即执行的，不像 &lt;code&gt;bind&lt;/code&gt; 是返回一个带参数的函数，所以 &lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;call/apply&lt;/code&gt; 无法一起使用。&lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;bind&lt;/code&gt; 一起使用还有一个功能就是能够预设参数，达到和函数柯里化一样的效果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;最后说一下 &lt;code&gt;mdn&lt;/code&gt; 给出的 &lt;code&gt;bind&lt;/code&gt;的 &lt;code&gt;polyfill&lt;/code&gt;，这个 &lt;code&gt;polyfill&lt;/code&gt; 能够检测是否是 &lt;code&gt;new&lt;/code&gt; 操作符，将标准中的 &lt;code&gt;bind&lt;/code&gt; 对 &lt;code&gt;new (funcA.bind(thisArg, args))&lt;/code&gt; 的行为也实现。在 &lt;code&gt;ES6&lt;/code&gt; 中有 &lt;code&gt;new.target&lt;/code&gt; 可以轻松实现这个功能，但这个 &lt;code&gt;polyfill&lt;/code&gt; 使用的场景是 &lt;code&gt;bind&lt;/code&gt; 都没有，更不用说 &lt;code&gt;new.target&lt;/code&gt; 了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;polyfill&lt;/code&gt; 就是我们常说的刮墙用的腻子，&lt;code&gt;polyfill&lt;/code&gt; 代码主要用于旧浏览器的兼容，比如说在旧的浏览器中并没有内置 &lt;code&gt;bind&lt;/code&gt; 函数，因此可以使用 &lt;code&gt;polyfill&lt;/code&gt; 代码在旧浏览器中实现新的功能.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//  Yes, it does work with `new (funcA.bind(thisArg, args))`
//第一层
if (!Function.prototype.bind)
  (function () {
    var ArrayPrototypeSlice = Array.prototype.slice
    //第二层
    Function.prototype.bind = function (otherThis) {
      if (typeof this !== &apos;function&apos;) {
        // closest thing possible to the ECMAScript 5
        // internal IsCallable function
        throw new TypeError(&apos;Function.prototype.bind - what is trying to be bound is not callable&apos;)
      }

      var baseArgs = ArrayPrototypeSlice.call(arguments, 1),
        baseArgsLength = baseArgs.length,
        fToBind = this,
        fNOP = function () {},
        //第三层
        fBound = function () {
          baseArgs.length = baseArgsLength // reset to default base arguments
          baseArgs.push.apply(baseArgs, arguments)
          return fToBind.apply(fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs)
        }

      if (this.prototype) {
        // Function.prototype doesn&apos;t have a prototype property
        fNOP.prototype = this.prototype
      }
      fBound.prototype = new fNOP()

      return fBound
    }
  })()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我把代码分为了三层，第一层是判断 &lt;code&gt;API&lt;/code&gt; 是否有 &lt;code&gt;bind&lt;/code&gt;，第二层就是在 &lt;code&gt;Function.prototype&lt;/code&gt; 上添加 &lt;code&gt;bind&lt;/code&gt; 方法，第三层则是我们调用 &lt;code&gt;bind&lt;/code&gt; 后返回的函数。对于非 &lt;code&gt;new&lt;/code&gt; 调用，就直接是 &lt;code&gt;apply&lt;/code&gt; 并传入 &lt;code&gt;otherThis&lt;/code&gt;，非常简单。但对于 &lt;code&gt;new&lt;/code&gt; 我还是有点疑问的。&lt;/p&gt;
&lt;p&gt;我们 &lt;code&gt;new&lt;/code&gt; 的实际上是 &lt;code&gt;第三层&lt;/code&gt; 的 &lt;code&gt;fBound&lt;/code&gt; 函数。根据 &lt;code&gt;new&lt;/code&gt; 的行为会创建一个 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向 &lt;code&gt;fBound.prototype&lt;/code&gt; 的新对象，然后以这个新对象作为 &lt;code&gt;this&lt;/code&gt; 执行 &lt;code&gt;fBound&lt;/code&gt; 并返回新对象。要注意的一点是，第二层中的 &lt;code&gt;this&lt;/code&gt; 是调用 &lt;code&gt;bind&lt;/code&gt; 的函数（我们设这个方法为 &lt;code&gt;fn&lt;/code&gt;），而第三层中的 &lt;code&gt;this&lt;/code&gt; 是 &lt;code&gt;new&lt;/code&gt; 运算符创造的新对象。&lt;/p&gt;
&lt;p&gt;我们最后 &lt;code&gt;new&lt;/code&gt; 的新对象的 &lt;code&gt;[[prototype]]&lt;/code&gt; 应该是第二层的调用 &lt;code&gt;bind&lt;/code&gt; 函数（标准中的 &lt;code&gt;bind&lt;/code&gt; 就是如此）的 &lt;code&gt;prototype&lt;/code&gt;，这条线索也就是整个方法的核心，在第三层用 &lt;code&gt;fNOP.prototype.isPrototypeOf(this)&lt;/code&gt; 来验证 &lt;code&gt;this&lt;/code&gt; 的原型链上是不是有 &lt;code&gt;fn.prototype&lt;/code&gt;，如果有就说明这是个 &lt;code&gt;new&lt;/code&gt; 调用。但是在 &lt;code&gt;mdn&lt;/code&gt; 的这个实现中，是用一个空对象 &lt;code&gt;fNOP&lt;/code&gt; 的 &lt;code&gt;prototype&lt;/code&gt; 指向&lt;code&gt;fn.prototype&lt;/code&gt;，然后将 &lt;code&gt;fBound.prototype&lt;/code&gt; 指向一个 &lt;code&gt;fNOP&lt;/code&gt; 的实例。这样操作虽然 &lt;code&gt;fn.prototype&lt;/code&gt; 还在 &lt;code&gt;new&lt;/code&gt; 的对象的原型链上，但是和标准中的 &lt;code&gt;bind&lt;/code&gt; 行为不一致，中间多了一个 &lt;code&gt;fNOP&lt;/code&gt; 的实例。而且在第二层将 &lt;code&gt;fBound.prototype&lt;/code&gt; 设为和 &lt;code&gt;fNOP.prototype&lt;/code&gt; 一样的 &lt;code&gt;this.prototype&lt;/code&gt; 并不影响整个方法的逻辑（这样设置 &lt;code&gt;new&lt;/code&gt; 的新对象的 &lt;code&gt;[[prototyep]]&lt;/code&gt; 指向 &lt;code&gt;fn.prototype&lt;/code&gt;），执行结果也没有异常，不影响第三层 &lt;code&gt;fNOP.prototype.isPrototypeOf(this)&lt;/code&gt; 的验证。不知道 &lt;code&gt;mdn&lt;/code&gt; 上的方法是不是有什么其他我没想到的用意，如果哪位读者知道，希望指点一下。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;书中总结的 &lt;code&gt;this&lt;/code&gt; 的判断规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;函数是否在 &lt;code&gt;new&lt;/code&gt; 中调用(&lt;code&gt;new&lt;/code&gt; 绑定)?如果是的话 &lt;code&gt;this&lt;/code&gt; 绑定的是新创建的对象。&lt;code&gt;var bar = new foo()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;函数是否通过 &lt;code&gt;call&lt;/code&gt;、&lt;code&gt;apply&lt;/code&gt; (显式绑定)或者硬绑定调用?如果是的话，&lt;code&gt;this&lt;/code&gt; 绑定的是 指定的对象。&lt;code&gt;var bar = foo.call(obj2)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;函数是否在某个上下文对象中调用(隐式绑定)?如果是的话，this 绑定的是那个上 下文对象。&lt;code&gt;var bar = obj1.foo()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果都不是的话，使用默认绑定。如果在严格模式下，就绑定到 &lt;code&gt;undefined&lt;/code&gt;，否则绑定 到全局对象。 &lt;code&gt;var bar = foo()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果你把 &lt;code&gt;null&lt;/code&gt; 或者 &lt;code&gt;undefined&lt;/code&gt; 作为 &lt;code&gt;this&lt;/code&gt; 的绑定对象传入 &lt;code&gt;call&lt;/code&gt;、&lt;code&gt;apply&lt;/code&gt; 或者 &lt;code&gt;bind&lt;/code&gt;，这些值在调用时会被忽略，实际应用的是默认绑定规则。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用 &lt;code&gt;null&lt;/code&gt; 作为参数的情况一般是用 &lt;code&gt;apply&lt;/code&gt; 展开数组（有些函数只接受一个个单独的参数，我们想直接传入数组用 &lt;code&gt;apply&lt;/code&gt; 是个方便的方法，当然 &lt;code&gt;ES6&lt;/code&gt; 中有扩展运算符 &lt;code&gt;...&lt;/code&gt; 可以直接使用）；用 &lt;code&gt;bind&lt;/code&gt; 进行函数柯里化（预先传入参数）。&lt;/p&gt;
&lt;p&gt;绑定 &lt;code&gt;null&lt;/code&gt; 作为 &lt;code&gt;this&lt;/code&gt; 不是一个安全的方法，最好是用 &lt;code&gt;var ø = Object.create( null );&lt;/code&gt; 创建一个空对象来代替 &lt;code&gt;null&lt;/code&gt;，这样可以避免发生意外。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;(p.foo = o.foo)();&lt;/code&gt; 会应用默认绑定，赋值表达式返回值是右值，此处为对应方法的引用。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>YDNJS学习笔记-上卷-第一部分</title><link>https://clloz.com/blog/ydnjs-note-1</link><guid isPermaLink="true">https://clloz.com/blog/ydnjs-note-1</guid><pubDate>Thu, 16 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;YDNJS 学习笔记&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/16/ydnjs-note/&quot; title=&quot;YDNJS上卷第一部分&quot;&gt;YDNJS上卷第一部分&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文整理阅读 &lt;code&gt;YDNJS&lt;/code&gt; 过程中的一些自己没有掌握的知识点，查漏补缺。&lt;/p&gt;
&lt;h2&gt;作用域&lt;/h2&gt;
&lt;p&gt;所谓 &lt;code&gt;作用域&lt;/code&gt; 就是由引擎管理的一套严格的规则，管理引擎如何在当前作用于以及嵌套的子作用域中根据标识符名称进行变量的查找。（任何语言有作用域的机制，&lt;code&gt;JS&lt;/code&gt; 中的作用域机制比较特别）&lt;/p&gt;
&lt;h2&gt;LHS 和 RHS&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;LHS&lt;/code&gt; 就是 &lt;code&gt;left-hand-side expression&lt;/code&gt;，&lt;code&gt;RHS&lt;/code&gt; 就是 &lt;code&gt;right-hand-side expression&lt;/code&gt;，在&lt;a href=&quot;https://www.ecma-international.org/ecma-262/11.0/index.html#sec-intro&quot; title=&quot;ecma-262&quot;&gt;ecma-262&lt;/a&gt;的第 &lt;code&gt;12&lt;/code&gt; 章有对 &lt;code&gt;LHS&lt;/code&gt; 的详细定义，所有 &lt;code&gt;LHS&lt;/code&gt; 都可以作为 &lt;code&gt;RHS&lt;/code&gt;，非 &lt;code&gt;LHS&lt;/code&gt; 的合法表达式都是 &lt;code&gt;RHS&lt;/code&gt;。标准主要讲的是哪些是合法的左值表达式。这里的 &lt;code&gt;left&lt;/code&gt; 和 &lt;code&gt;right&lt;/code&gt; 指的是在赋值操作符的左边和右边，但语句中并不一定要出现赋值符号，比如 &lt;code&gt;++&lt;/code&gt; 和 &lt;code&gt;--&lt;/code&gt;，他们在执行的过程中实际是有赋值行为的，这也就是为什么 &lt;code&gt;++a++&lt;/code&gt; 报错的原因。所以区分左手还是右手关键是看有没有赋值行为发生（赋值行为不一定需要赋值操作符，可以有其他形式），&lt;code&gt;LHS&lt;/code&gt; 可以理解为 &lt;code&gt;找到要赋值的目标&lt;/code&gt;，而 &lt;code&gt;RHS&lt;/code&gt; 可以理解为 &lt;code&gt;找到某个已经被赋值的结果&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;回到作用域中，在引擎查找变量的时候，如果查找的目的是对变量进行赋值，就是用 &lt;code&gt;LHS&lt;/code&gt; 查询，如果目的是获取变量的值，就是用 &lt;code&gt;RHS&lt;/code&gt; 查询。引擎在处理未声明的 &lt;code&gt;LHS&lt;/code&gt; 和 &lt;code&gt;RHS&lt;/code&gt; 是不同的，&lt;code&gt;RHS&lt;/code&gt; 如果在作用域链中查询不到引擎会抛出 &lt;code&gt;ReferenceError&lt;/code&gt; 异常。而 &lt;code&gt;LHS&lt;/code&gt; 如果沿着作用域链查询到顶层（全局作用域）中都没有查询到的话，在非严格模式下就会在全局作用域中创建一个该名称变量，返回给引擎，如果是在严格模式下，会和 &lt;code&gt;RHS&lt;/code&gt; 一样抛出一个 &lt;code&gt;ReferenceError&lt;/code&gt; 异常。&lt;/p&gt;
&lt;p&gt;如果在作用域链中查询到 &lt;code&gt;RHS&lt;/code&gt; 对应变量，但是尝试对这个变量进行不合理的操作，比如一个非函数类型的值进行函数调用，或者引用 &lt;code&gt;null&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt; 的属性，引擎会抛出 &lt;code&gt;TypeError&lt;/code&gt;。&lt;code&gt;ReferenceError&lt;/code&gt; 异常和作用域判别失败相关，而 &lt;code&gt;TypeError&lt;/code&gt; 则表示作用域判别成功了，但是对结果的操作是非法或不合理的。&lt;/p&gt;
&lt;h2&gt;词法作用域&lt;/h2&gt;
&lt;p&gt;编译的三个阶段： 1. 分词/词法分析（&lt;code&gt;Tokenizing/Lexing&lt;/code&gt;），将代码分解成对编程语言来说不可再分的此法单元（&lt;code&gt;token&lt;/code&gt;）。此法单元的识别是有状态的成为词法分析，无状态则成为分词。 2. 解析/语法分析（&lt;code&gt;Parsing&lt;/code&gt;），将词法单元流（数组）转换成一个由元素逐级嵌套所著称的代表了程序与法结构的树，成为抽象语法树（&lt;code&gt;Abstract Syntax Tree AST&lt;/code&gt;)。 3. 代码生成：将 &lt;code&gt;AST&lt;/code&gt; 装换为可执行代码（机器指令）的过程成为代码生成。&lt;/p&gt;
&lt;p&gt;作用域共有两种主要的工作模型。第一种是最为普遍的，被大多数编程语言所采用的词法 作用域，我们会对这种作用域进行深入讨论。另外一种叫作动态作用域，仍有一些编程语言在使用(比如 &lt;code&gt;Bash&lt;/code&gt; 脚本、&lt;code&gt;Perl&lt;/code&gt; 中的一些模式等)。我们在 &lt;code&gt;JavaScript&lt;/code&gt; 中使用的作用域模型也是比较普遍的词法作用域。&lt;/p&gt;
&lt;p&gt;词法作用域顾名思义就是发生在上面编译三个阶段的第一阶段（由引擎的专门负责作用域的部分来管理），词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。这种机制能够保证代码在词法分析阶段的作用域保持不变（大部分情况下）。词法作用域某种意义上是一种静态的作用域，而另一种对应的模式被称为 &lt;code&gt;动态作用域&lt;/code&gt;。结论：无论函数在哪里被调用，也无论他如何被调用，他的词法作用域都只由函数被声明时所处的位置决定。作用域查找会在找到第一个匹配的标识符时停止。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;关于引擎对代码的处理可以看一个简单的例子 &lt;code&gt;var a = 2;&lt;/code&gt;,事实上编译器会进行如下处理。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;遇到 &lt;code&gt;var a&lt;/code&gt;，编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的 集合中。如果是，编译器会忽略该声明，继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量，并命名为 &lt;code&gt;a&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;接下来编译器会为引擎生成运行时所需的代码，这些代码被用来处理 &lt;code&gt;a = 2&lt;/code&gt; 这个赋值 操作。引擎运行时会首先询问作用域，在当前的作用域集合中是否存在一个叫作 &lt;code&gt;a&lt;/code&gt; 的 变量。如果是，引擎就会使用这个变量；如果否，引擎会继续查找该变量。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;改变词法作用域&lt;/h2&gt;
&lt;p&gt;有两个方法可以改变我们静态的词法作用域，&lt;code&gt;with&lt;/code&gt; 和 &lt;code&gt;eval&lt;/code&gt;。两个方法都不推荐使用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;with&lt;/code&gt; 的作用是扩展作用域链。一般情况下我们的词法作用域是静态的，由我们的语句在代码中的位置决定的。引擎根据嵌套的作用域链来寻找变量，但是当使用 &lt;code&gt;with&lt;/code&gt; 语句以后，我们的作用域链的第一层不再是当前所处的执行上下文的变量对象，而是 &lt;code&gt;with&lt;/code&gt; 语句的参数，而执行上下文的变量对象则变为第二层。也就是 &lt;code&gt;with&lt;/code&gt; 语句中的变量搜索会现在参数所给的对象中进行，找不到才会进入我们一般语句的搜索模式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function a(obj) {
  with (obj) {
    a = 10
    var b = 20
  }
  console.log(b)
}
obj = {
  a: 1,
  b: 2
}
a(obj) //undefined 如果obj中没有b属性则此处输出20
console.log(obj) //{a:1,b:20}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 &lt;code&gt;with&lt;/code&gt; 总是先在指定的对象中查找属性，如果我们在 &lt;code&gt;with&lt;/code&gt; 语句中使用不是指定对象中的变量，查找起来就会变慢。还有如果我们在 &lt;code&gt;with&lt;/code&gt; 语句中操作的变量不是指定对象的属性（比如上面代码中 &lt;code&gt;obj&lt;/code&gt; 对象没有 &lt;code&gt;a&lt;/code&gt; 属性），那么这个变量会被泄露到全局作用域上。如果是用 &lt;code&gt;var&lt;/code&gt; 声明的变量在指定对象上不存在，这个变量相当于生命在 &lt;code&gt;with&lt;/code&gt; 语句所处的执行上下文中。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;严格模式不可以使用 &lt;code&gt;with&lt;/code&gt;语句。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;eval&lt;/code&gt; 函数是全局对象的一个方法，接收表达式或语句的字符串作为参数，把该参数当做 &lt;code&gt;js&lt;/code&gt; 代码来执行，如果参数不是字符串，参数会被直接返回。&lt;/p&gt;
&lt;p&gt;直接调用 &lt;code&gt;eval&lt;/code&gt;，那么代码执行的执行环境就是当前环境（就好像我们把参数中的代码卸载当前位置一样），而如果间接调用（比如将 &lt;code&gt;eval&lt;/code&gt; 引用赋值给其他变量让后通过赋值后的变量调用，或者类似 &lt;code&gt;(0, eval)(&apos;x + y&apos;);&lt;/code&gt; 的表达式计算也算间接调用），那么代码执行的执行环境就是全局作用域。在严格模式中，&lt;code&gt;eval&lt;/code&gt; 在运行时有自己的词法作用域，所以无法修改内部声明的作用域。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;eval&lt;/code&gt; 强烈不建议使用主要有以下几个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;eval&lt;/code&gt; 的可读性非常差，而且也不容易调试。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全风险：&lt;code&gt;eval&lt;/code&gt; 的参数是一个字符串，自然也可以拼接，如果我们的拼接字符串中有来自用户的输入（比如 &lt;code&gt;input&lt;/code&gt;），那么就是一个非常危险的行为，并且 &lt;code&gt;eval&lt;/code&gt; 会暴露自己的作用域。当然这种情况一般不太会发生&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;eval&lt;/code&gt; 的性能问题，&lt;code&gt;eval&lt;/code&gt; 必须调用解释器来解释执行，而且现代 &lt;code&gt;JavaScript&lt;/code&gt; 解释器会将 &lt;code&gt;javascript&lt;/code&gt; 转换为机器代码，而在执行过程中才解析的 &lt;code&gt;eval&lt;/code&gt; 中的代码很可能需要诉诸环境重新执行已经生成的机器码来应对，这必然造成性能的损失。引擎在编译阶段的各种优化方式也是依赖于词法的静态分析，预先确定变量和函数的位置，让代码在执行的时候能够快速找到对应的变量和函数，而 &lt;code&gt;eval&lt;/code&gt; 函数中接受的代码使不确定的，所以很多优化是无法进行的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于 &lt;code&gt;eval&lt;/code&gt; 中的代码相当于执行 &lt;code&gt;eval&lt;/code&gt; 的位置，所以其内部的声明会影响到当前环境的词法作用域作用域。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function foo(str, a) {
  eval(str) // 此处执行的代码声明了一个新的变量，改变了当前环境的词法作用域
  console.log(a, b)
}
var b = 2
foo(&apos;var b = 3;&apos;, 1) // 1, 3
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;Function&lt;/code&gt; 函数也可以像 &lt;code&gt;eval&lt;/code&gt; 一样把字符串当做代码执行，&lt;code&gt;Function&lt;/code&gt; 可以使用 &lt;code&gt;new&lt;/code&gt; 也可以不用，它们的效果是一样的，最后一个参数会被当做函数体，前面的参数是参数名，必须要要用 &lt;code&gt;javascript&lt;/code&gt; 中合法的标识符字符串，所有被传递到构造函数中的参数，都将被视为将被创建的函数的参数，并且是相同的标示符名称和传递顺序，可以是 &lt;code&gt;Function(&apos;a&apos;, &apos;b&apos;, functionBody)&lt;/code&gt; 的形式，也可以是 &lt;code&gt;Function(&apos;a, b&apos;, functionBody)&lt;/code&gt; 的形式，&lt;code&gt;functionBody&lt;/code&gt; 是一个含有包括函数定义的 JavaScript 语句的字符串。由 &lt;code&gt;Function&lt;/code&gt; 构造器创建的函数不会创建当前环境的闭包，它们总是被创建于全局环境，因此在运行时它们只能访问全局变量和自己的局部变量，不能访问 &lt;code&gt;Function&lt;/code&gt; 构造器创建时所在的作用域的变量。这一点与使用 eval 执行创建函数的代码不同。这种方式要比 &lt;code&gt;eval&lt;/code&gt; 安全很多，但是依然不推荐使用，使用 &lt;code&gt;Function&lt;/code&gt; 构造器生成的 &lt;code&gt;Function&lt;/code&gt; 对象是在函数创建时解析的。这比你使用函数声明或者函数表达式并在你的代码中调用更为低效，因为使用后者创建的函数是跟其他代码一起解析的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function foo(str, a) {
  Function(str)() //3
  console.log(a, b)
}
var b = 2
foo(&apos;var b = 3;console.log(b)&apos;, 1) // 1, 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;作用域&lt;/h2&gt;
&lt;p&gt;最小暴露原则：在软件设计中，应该最小限度地暴露必 要内容，而将其他内容都“隐藏”起来，比如某个模块或对象的 &lt;code&gt;API&lt;/code&gt; 设计。我认为有几点好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;代码按功能分隔开来，可读性更强，更容易维护。对于模块和对象的使用完全不用关心内部的细节，只要知道对应的接口即可。&lt;/li&gt;
&lt;li&gt;避免了变量名的冲突。特别是我们加载各种第三方库的时候很容易发生这样的情况。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;区别函数声明和函数表达式的方法就是看函数声明的语句中 &lt;code&gt;function&lt;/code&gt; 关键字是不是在语句的第一个词。如果是第一个词那么这就是一个函数声明，否则就是一个函数表达式（只要这是一个合法的语句）。另外就是函数表达式可以匿名，而函数声明则不可以。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;匿名函数和 IIFE&lt;/h2&gt;
&lt;p&gt;函数表达式中可以使用匿名函数，但是匿名函数有几个缺点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;匿名函数在栈追踪中不会显示出有意义的函数名，调试困难&lt;/li&gt;
&lt;li&gt;匿名函数没有函数名，当函数需要调用自身的时候（比如递归中），就只能用已经不推荐使用的（&lt;code&gt;arguments.callee&lt;/code&gt;）。另一个函数需要引用自身的例子，是在事件触发后事件监听器需要解绑自身。&lt;/li&gt;
&lt;li&gt;函数名可以增加代码可读性，一个好的函数名能够让函数的功能一目了然。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以，一个好的习惯是我们始终给函数表达式命名。&lt;/p&gt;
&lt;p&gt;立即执行函数的独立词法作用域有个小技巧就是 &lt;code&gt;undefined&lt;/code&gt; 在局部环境被赋值的情况，我们可以设置立即执行函数的形参为 &lt;code&gt;undefined&lt;/code&gt; 但是不传入任何参数，在函数体内 &lt;code&gt;undefined&lt;/code&gt; 就不会受外部的影响。当然最好是不要随便给 &lt;code&gt;undefined&lt;/code&gt; 赋值，使用 &lt;code&gt;undefined&lt;/code&gt; 的地方尽量用 &lt;code&gt;void 0&lt;/code&gt; 代替。&lt;/p&gt;
&lt;p&gt;立即执行函数有一种特殊的写法，将需要执行的内容当做参数传递进去，叫做 &lt;code&gt;UMD(Universal Module Definition)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var a = 2
;(function IIFE(def) {
  def(window)
})(function def(global) {
  var a = 3
  console.log(a) // 3
  console.log(global.a) // 2
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;try...catch 语句&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;try...catch&lt;/code&gt; 语句是用来监测一段语句的执行是否抛出异常。如果try代码块中的语句（或者 &lt;code&gt;try&lt;/code&gt; 代码块中调用的方法）一旦抛出了异常（也可以是我们主动 &lt;code&gt;throw&lt;/code&gt;），那么执行流程会立即进入 &lt;code&gt;catch&lt;/code&gt; 代码块。如果 &lt;code&gt;try&lt;/code&gt; 代码块没有抛出异常，&lt;code&gt;catch&lt;/code&gt; 代码块就会被跳过。&lt;code&gt;finally&lt;/code&gt; 代码块总会紧跟在 &lt;code&gt;try&lt;/code&gt; 和 &lt;code&gt;catch&lt;/code&gt; 代码块之后执行，但会在 &lt;code&gt;try&lt;/code&gt; 和 &lt;code&gt;catch&lt;/code&gt; 代码块之后的其他代码之前执行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;try...catch&lt;/code&gt; 语句至少有一个 &lt;code&gt;try&lt;/code&gt; 块（由要尝试执行的语句组成），一个 &lt;code&gt;catch&lt;/code&gt; 块或者一个 &lt;code&gt;finally&lt;/code&gt; 块，&lt;code&gt;catch&lt;/code&gt; 和 &lt;code&gt;finally&lt;/code&gt; 块可以两者都有，所以一共有三种形式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;try...catch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;try...finally&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;try...catch...finally&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;你可以嵌套一个或者更多的 &lt;code&gt;try&lt;/code&gt; 语句。如果内部的 &lt;code&gt;try&lt;/code&gt; 语句没有 &lt;code&gt;catch&lt;/code&gt; 子句，那么将会进入包裹它的 &lt;code&gt;try&lt;/code&gt; 语句的 &lt;code&gt;catch&lt;/code&gt; 子句（可以理解为离自己最近的 &lt;code&gt;catch&lt;/code&gt; 语句）。&lt;code&gt;try&lt;/code&gt; 块中抛出的异常会作为 &lt;code&gt;catch&lt;/code&gt; 块的参数，这个参数只在 &lt;code&gt;catch&lt;/code&gt; 内能够访问。无论是否抛出异常 &lt;code&gt;finally&lt;/code&gt; 子句都会执行。如果抛出异常，即使没有 &lt;code&gt;catch&lt;/code&gt; 子句处理异常，&lt;code&gt;finally&lt;/code&gt; 子句中的语句也会执行。最后要注意的就是 &lt;code&gt;try...catch&lt;/code&gt; 语句的返回值（语句只有在函数内返回值才有意义），三种块都能用 &lt;code&gt;return&lt;/code&gt; 返回，但是有一定的规则，大致如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只要存在 &lt;code&gt;finally&lt;/code&gt; 块的 &lt;code&gt;return&lt;/code&gt;，无论是否抛出异常，也无论 &lt;code&gt;try&lt;/code&gt; 和 &lt;code&gt;catch&lt;/code&gt; 是否有 &lt;code&gt;return&lt;/code&gt; ，最后的返回值都是 &lt;code&gt;finnaly&lt;/code&gt; 块的 &lt;code&gt;return&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果没有 &lt;code&gt;finally&lt;/code&gt; 块不存在，那么如果抛出异常就输出 &lt;code&gt;catch&lt;/code&gt; 块的 &lt;code&gt;return&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果没有 &lt;code&gt;finally&lt;/code&gt; 块不存在，如果没有抛出异常，那么输出 &lt;code&gt;try&lt;/code&gt; 块的 &lt;code&gt;return&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里我们可以发现 &lt;code&gt;return&lt;/code&gt; 的行为在 &lt;code&gt;try...catch&lt;/code&gt; 里面是不同的。一般情况下 &lt;code&gt;return&lt;/code&gt; 会中指当前函数的执行并返回值，但是在 &lt;code&gt;try...catch&lt;/code&gt; 中并不会。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;try...catch&lt;/code&gt; 语句中的 &lt;code&gt;catch&lt;/code&gt; 块的参数是有独立的词法作用域的，也就是他无法在语句外访问，利用这一点我们可以在没有 &lt;code&gt;let&lt;/code&gt; , &lt;code&gt;const&lt;/code&gt; 的情况下（&lt;code&gt;ES6&lt;/code&gt; 之前）实现块级作用域，因为 &lt;code&gt;try...catch&lt;/code&gt; 语句在 &lt;code&gt;ES3&lt;/code&gt; 就有了，并且一直都是这么工作的。比如 &lt;code&gt;Google&lt;/code&gt; 的 &lt;code&gt;Traceur&lt;/code&gt; （类似于 &lt;code&gt;babel&lt;/code&gt;，将 &lt;code&gt;ES6&lt;/code&gt; 代码转换成兼容 &lt;code&gt;ES6&lt;/code&gt; 之前 的环境）就是这样实现块级作用域的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  try {
    throw undefined
  } catch (a) {
    a = 2
    console.log(a) //2
  }
}
console.log(a) //ReferenceError: a is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;大括号 &lt;code&gt;{}&lt;/code&gt; 在 &lt;code&gt;ES6&lt;/code&gt; 中可以作为块级作用域的（配合 &lt;code&gt;let&lt;/code&gt;，&lt;code&gt;const&lt;/code&gt; 和 &lt;code&gt;class&lt;/code&gt;，函数声明也因为兼容性保持特殊行为），在 &lt;code&gt;ES6&lt;/code&gt; 之前他只是一种组织代码的方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;let 和 const&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;YDKJS&lt;/code&gt; 建议为块作用域进行显式的创建，能够让变量的附属关系更清晰。因为 &lt;code&gt;{}&lt;/code&gt; 本身就是分隔代码块的一种方式，一般不会改变语义。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var foo = true
if (foo) {
  {
    // &amp;#x3C;-- 显式的块
    let bar = foo * 2
    bar = something(bar)
    console.log(bar)
  }
}
console.log(bar) // ReferenceError
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;let&lt;/code&gt; 在循环中的使用看似和 &lt;code&gt;var&lt;/code&gt; 没太大区别，其实 &lt;code&gt;for&lt;/code&gt; 循环头部的 &lt;code&gt;let&lt;/code&gt; 不仅将 &lt;code&gt;i&lt;/code&gt; 绑定到了 &lt;code&gt;for&lt;/code&gt; 循环的块中，事实上它将其重新绑定到了循环 的每一个迭代中，确保使用上一个循环迭代结束时的值重新进行赋值。所以 &lt;code&gt;for&lt;/code&gt; 循环小括号和大括号并不是同一个词法作用域，小括号在大括号的上层。下面两段代码分别问一般的 &lt;code&gt;for&lt;/code&gt; 循环和实际的迭代过程模拟。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//for 循环
for (let i = 0; i &amp;#x3C; 10; i++) {
  console.log(i)
}
console.log(i) // ReferenceError

//迭代过程模拟
{
  let j
  for (j = 0; j &amp;#x3C; 10; j++) {
    let i = j // 每个迭代重新绑定!
    console.log(i)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;函数作用域我个人认为也属于一个单独的块级作用域，所以他们在作用域的行为上是一致的，任何声明在 某个作用域内的变量，都将附属于这个作用域。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;垃圾回收问题&lt;/h2&gt;
&lt;p&gt;这个问题也是我一直思考的问题，当一个函数 &lt;code&gt;a&lt;/code&gt; 内部返回了一个函数 &lt;code&gt;b&lt;/code&gt;，那么即使 &lt;code&gt;a&lt;/code&gt; 执行完了其内部变量也无法释放，因为 &lt;code&gt;b&lt;/code&gt; 的闭包覆盖 &lt;code&gt;a&lt;/code&gt; 的环境。当然我想现在的 &lt;code&gt;JS&lt;/code&gt; 引擎应该有一定的优化，但是这个问题应该是无法彻底解决的，因为我们无法确定这个被返回的 &lt;code&gt;b&lt;/code&gt; 函数何时会执行，也不知道他内部是否要访问在 &lt;code&gt;a&lt;/code&gt; 内部定义的变量或者方法。&lt;code&gt;YDNJS&lt;/code&gt; 也给出了一个类似的例子，不过不是用的返回函数，而是用的 &lt;code&gt;DOM&lt;/code&gt; 事件监听。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function process(data) {
// 在这里做点有趣的事情
}

var someReallyBigData = { .. };

process( someReallyBigData );

var btn = document.getElementById( &quot;my_button&quot; );

btn.addEventListener( &quot;click&quot;, function click(evt) {
    console.log(&quot;button clicked&quot;);
}, /*capturingPhase=*/false );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子中事件监听函数的回调并不需要用到所在环境的其他变量或者方法，但是因为他的闭包覆盖了自己所在的环境，所以会导致那些已经 &lt;code&gt;不需要&lt;/code&gt; 的变量或者方法不能被释放（取决于具体的引擎实现）。使用块作用域可以解决这个问题。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function process(data) {
// 在这里做点有趣的事情
}

// 在这个块中定义的内容完事可以销毁!
{
    let someReallyBigData = { .. };
    process( someReallyBigData );
}

var btn = document.getElementById( &quot;my_button&quot; );

btn.addEventListener( &quot;click&quot;, function click(evt){
     console.log(&quot;button clicked&quot;);
}, /*capturingPhase=*/false );
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提升&lt;/h2&gt;
&lt;p&gt;引擎会在执行代码前对代码进行编译，这里除了编译器的工作，还有一个重要的工作就是作用域。这些工作都为了提升代码的执行效率，编译成机器码让计算机能快速执行，而作用域的存在可以让引擎在运行时对变量的查找更加快速和有效率。当然中间还有很多其他的优化，在引擎的发展过程中不断进步（比如 &lt;code&gt;JIT&lt;/code&gt; 可以延迟编译甚至实施重编译）。而对变量和函数声明的处理也是其中的重要一环。&lt;/p&gt;
&lt;p&gt;关于变量和函数的提升，我已经在另一篇&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/01/variable-hoist/#let-2&quot; title=&quot;文章&quot;&gt;文章&lt;/a&gt;里面详细写过了，这里就不在重复了。&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;YDNJS&lt;/code&gt; 第一版（第二版英文版在 &lt;code&gt;github&lt;/code&gt; 上已经可以看了，只有 &lt;code&gt;scope and closure&lt;/code&gt; 一本）已经是 &lt;code&gt;2015&lt;/code&gt; 版本了，所以有些内容已经不适合现在的 &lt;code&gt;JS&lt;/code&gt;，比如 &lt;code&gt;提升&lt;/code&gt; 这个章节小结前的最后一段代码，在现在的 &lt;code&gt;JS&lt;/code&gt; 宿主环境执行就会报 &lt;code&gt;TypeError&lt;/code&gt;，具体原因就是函数声明在块级作用域中的行为发生了改变，后面随便版本的更新可能还会改变，现在的行为也是为了兼容以前的老代码而做的妥协，因为函数声明是很早就有的概念，而块作用域则是 &lt;code&gt;ES6&lt;/code&gt; 才出现的，如果把函数声明也全部变成块作用域的话，很多以前的代码将无法运行。函数声明在块级作用域的具体行为查看上面链接的文章中的 &lt;code&gt;let -&gt; 没有变量提升&lt;/code&gt; 这一小节。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;虽然我们要清楚引擎是如何处理变量和函数的提升的，但是在实际编码中还是要避免重复的声明，保持好的编码习惯。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;作用域闭包&lt;/h2&gt;
&lt;p&gt;我个人对闭包的理解就是一个函数和函数对它定义时的词法环境的引用一起够成一个闭包，所以每一个函数都有自己的闭包。理解闭包就是理解词法作用域，也就是理解 &lt;code&gt;JavaScript&lt;/code&gt; 中的变量和方法的访问机制。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function foo() {
  var a = 2
  function bar() {
    console.log(a)
  }
  return bar
}
var baz = foo()
baz() // 2 bar在定义时的词法作用域以外执行

function foo() {
  var a = 2
  function baz() {
    console.log(a) // 2
  }
  bar(baz)
}
function bar(fn) {
  fn() // baz在定义时的词法作用域以外执行
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的例子中 &lt;code&gt;foo&lt;/code&gt; 函数执行的结果被赋值给 &lt;code&gt;baz&lt;/code&gt;（实际只是将 &lt;code&gt;foo&lt;/code&gt; 内部函数 &lt;code&gt;bar&lt;/code&gt; 的引用赋值给 &lt;code&gt;baz&lt;/code&gt;，最后执行的也是 &lt;code&gt;bar&lt;/code&gt;） 。在 &lt;code&gt;foo()&lt;/code&gt; 执行后，通常会期待 &lt;code&gt;foo()&lt;/code&gt; 的整个内部作用域都被销毁，因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 &lt;code&gt;foo()&lt;/code&gt; 的内容不会再被使用，所以很 自然地会考虑对其进行回收。而事实上因为 &lt;code&gt;bar&lt;/code&gt; 对 &lt;code&gt;foo()&lt;/code&gt; 内部作用域的引用还存在，所以闭包会阻止 &lt;code&gt;foo()&lt;/code&gt; 内部作用域的销毁，并且会一直存在，因为随时有可能再次执行 &lt;code&gt;bar()&lt;/code&gt;。&lt;code&gt;bar&lt;/code&gt; 函数和他对 &lt;code&gt;foo()&lt;/code&gt; 的内部作用域的引用就是闭包（事实上整个作用域链都是可以访问的，只是对我们有意义的是已经执行完成的函数内部的作用域）。这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。&lt;/p&gt;
&lt;p&gt;事件绑定很多时候也是闭包：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function setupBot(name, selector) {
  $(selector).click(function activator() {
    console.log(&apos;Activating: &apos; + name)
  })
}
setupBot(&apos;Closure Bot 1&apos;, &apos;#bot_1&apos;)
setupBot(&apos;Closure Bot 2&apos;, &apos;#bot_2&apos;)
//setupBot函数执行完后为 #bot_1 和 #bot_2绑定了 click 事件，但是当事件触发时，我们依然可以访问name和selector
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;无论通过何种手段将内部函数传递到所在的词法作用域以外，它都会持有对原始定义作用 域的引用，无论在何处执行这个函数都会使用闭包。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;闭包在 &lt;code&gt;JavaScript&lt;/code&gt; 中最重要的应用就是当我们把函数作为值到处传递的时候，这些函数在定义时的词法作用域之外执行，保持对定义时的词法作用域的引用，让我们还能访问到内部的变量或者方法。在定时器、事件监听器、 &lt;code&gt;Ajax&lt;/code&gt; 请求、跨窗口通信、&lt;code&gt;Web Workers&lt;/code&gt; 或者任何其他的异步(或者同步)任务中，只要使 用了回调函数，实际上就是在使用闭包!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;YDNJS&lt;/code&gt; 中的看法是函数在定义的词法作用域以外执行才算闭包，但我觉得每个函数都是闭包，都有对自己所在词法作用域的引用，只不过让函数在定义的词法环境以外执行时符合我们需求的一种重要应用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;模块&lt;/h2&gt;
&lt;p&gt;书中介绍了两种模块化方式，一种是利用立即执行函数进行包装，通过返回的函数闭包来访问立即执行函数内部的作用域，返回的函数就作为模块的 &lt;code&gt;API&lt;/code&gt;。而模块的管理则一般利用模块加载器，书中给出了一个简单的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i&amp;#x3C;deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply( impl, deps );
        console.log(modules)
    }

    function get(name) {
        return modules[name];
    }
    return {
        define: define,
        get: get
    };
})();

MyModules.define( &quot;bar&quot;, [], function() {
    function hello(who) {
        return &quot;Let me introduce: &quot; + who;
    }
    return {
        hello: hello
    };
});
MyModules.define( &quot;foo&quot;, [&quot;bar&quot;], function(bar) {
    var hungry = &quot;hippo&quot;;
    function awesome() {
        console.log( bar.hello( hungry ).toUpperCase() );
    }
    return {
        awesome: awesome
    };
});

var bar = MyModules.get( &quot;bar&quot; );  //{ bar: { hello: [Function: hello] } }
var foo = MyModules.get( &quot;foo&quot; );  //{bar: { hello: [Function: hello] },foo: { awesome: [Function: awesome] }}
console.log(bar.hello( &quot;hippo&quot; ));  //Let me introduce: hippo
foo.awesome(); //LET ME INTRODUCE: HIPPO
console.log(bar.hello()) Let me introduce: undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种则是 &lt;code&gt;ES6&lt;/code&gt; 中的 &lt;code&gt;import&lt;/code&gt; 和 &lt;code&gt;export&lt;/code&gt;。&lt;code&gt;ES6&lt;/code&gt; 为模块添加了语法支持，文件被当做单独的模块来处理，每个模块都可以导入其他模块或特定的 &lt;code&gt;API&lt;/code&gt; 成员，同样也可以导出自己的 &lt;code&gt;API&lt;/code&gt; 成员。&lt;code&gt;ES6&lt;/code&gt; 的模块没有“行内”格式，必须被定义在独立的文件中(一个文件一个模块)。浏览器或引擎有一个默认的模块加载器，可以在导入模块时同步地加载模块文件。与基于函数的模块不同的是，&lt;code&gt;ES6&lt;/code&gt; 的模块是静态的，&lt;code&gt;API&lt;/code&gt; 不能再运行时改变，所以可以在编译时就检查模块是否存在，不存在则报错，模块文件中的内容会被当作好像包含在作用域闭包中一样来处理，就和前面介绍的函数闭包模块一样。。而基于函数的模块则是在运行时才能知道 &lt;code&gt;API&lt;/code&gt;，并且我们可以随时改变（事实上我觉得基于函数的模块只是利用闭包的一种代码包装，本质还是函数）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于模块化的内容书中并没有介绍太多，我认为模块化的内容还是非常重要的，需要单独拿出来学习一下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;附录&lt;/h2&gt;
&lt;h2&gt;动态作用域&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;javascript&lt;/code&gt; 和大多数语言一样，作用域为词法作用域。词法作用域就是根据定义的位置来寻找变量和方法，可以理解为静态的，在编译的时候就已经确定变量的位置了，之后运行时引擎也根据这套规则寻找变量。而动态作用域则是根据调用的位置来确定变量。我们可以用下面的代码说明。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//静态
function foo() {
  console.log(a) // 2
}
function bar() {
  var a = 3
  foo()
}
var a = 2
bar()

//动态
function foo() {
  console.log(a) // 3
}
function bar() {
  var a = 3
  foo()
}
var a = 2
bar()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;简单一点说就是词法作用域根据定义的位置寻找变量（在写代码或者说定义时确定的），而动态作用域则是根据调用的位置来寻找变量（在运行时确定的）。虽然 &lt;code&gt;JavaScript&lt;/code&gt; 中并没有动态作用域，但是 &lt;code&gt;this&lt;/code&gt; 关键字的机制却和动态作用域很类似。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="/_astro/ydnjs.D91PPQmo.jpg"/><enclosure url="/_astro/ydnjs.D91PPQmo.jpg"/></item><item><title>定时器的一些思考</title><link>https://clloz.com/blog/settimeout</link><guid isPermaLink="true">https://clloz.com/blog/settimeout</guid><pubDate>Wed, 15 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中的定时器有两个 &lt;code&gt;setTimeout&lt;/code&gt; 和 &lt;code&gt;setInterval&lt;/code&gt;，在浏览器环境他们都是全局对象 &lt;code&gt;window&lt;/code&gt; 的属性（在 &lt;code&gt;web worker&lt;/code&gt; 中则是对应的 &lt;code&gt;WorkerGlobalScope&lt;/code&gt;，本文主要讨论 &lt;code&gt;window&lt;/code&gt; 其他的环境可以类推），他们不是 &lt;code&gt;JavaScript&lt;/code&gt; 标准里的东西，是浏览器的 &lt;code&gt;API&lt;/code&gt;，不过在 &lt;code&gt;nodejs&lt;/code&gt; 中也模拟浏览器进行也实现，也是挂载在全局对象上的。定时器由于有自己的一些特殊行为，所以写一篇文章来总结一下。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;nodejs&lt;/code&gt; 中的定时器的第一个函数不能是字符串，只能是一个函数。&lt;code&gt;nodejs&lt;/code&gt; 上的实现虽然是浏览器的翻版，但是还是略有不同，本文主要讨论浏览器环境，具体的 &lt;code&gt;nodejs&lt;/code&gt; 中的不同参考 &lt;code&gt;nodejs&lt;/code&gt; 文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;标准&lt;/h2&gt;
&lt;p&gt;我们直接一步到位，从标准中看定时器的定义，定时器 &lt;code&gt;timer&lt;/code&gt; 在&lt;a href=&quot;https://html.spec.whatwg.org/multipage/&quot; title=&quot;HTML标准&quot;&gt;HTML标准&lt;/a&gt;的第 &lt;code&gt;8.6&lt;/code&gt; 章节。&lt;/p&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;handle = self . setTimeout( handler [, timeout [, arguments... ] ] )
//Schedules a timeout to run handler after timeout milliseconds. Any arguments are passed straight through to the handler.

handle = self . setTimeout( code [, timeout ] )
//Schedules a timeout to compile and run code after timeout milliseconds.

self . clearTimeout( handle )
//Cancels the timeout set with setTimeout() or setInterval() identified by handle.

handle = self . setInterval( handler [, timeout [, arguments... ] ] )
//Schedules a timeout to run handler every timeout milliseconds. Any arguments are passed straight through to the handler.

handle = self . setInterval( code [, timeout ] )
//Schedules a timeout to compile and run code every timeout milliseconds.

self . clearInterval( handle )
Cancels the timeout set with setInterval() or setTimeout() identified by handle.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;self&lt;/code&gt; 就是 &lt;code&gt;window&lt;/code&gt; 或者 &lt;code&gt;web worker&lt;/code&gt; 的 &lt;code&gt;WorkerGlobalScope&lt;/code&gt;，在 &lt;code&gt;window&lt;/code&gt; 全局对象下我们可以直接写 &lt;code&gt;window&lt;/code&gt; 也可以不写。&lt;code&gt;handle&lt;/code&gt; 可以理解为定时器的编号，清除定时器的两个函数依靠 &lt;code&gt;handle&lt;/code&gt; 在定时器列表中找到对应的定时器清除。&lt;code&gt;setTimeout&lt;/code&gt; 和 &lt;code&gt;setInterval&lt;/code&gt; 都至少接收一个参数作为回调函数，这个参数可以是一个函数也可以是一个字符串（不建议使用字符串，会有和 &lt;code&gt;eval&lt;/code&gt; 一样的问题，在 &lt;code&gt;nodejs&lt;/code&gt; 中默认不可以使用字符串）；第二个可选参数是回调函数的执行间隔，默认值为 &lt;code&gt;0&lt;/code&gt;；从第三个参数开始就是回调函数执行的参数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;clearTimeout&lt;/code&gt; 和 &lt;code&gt;clearInterval&lt;/code&gt; 虽然命名不一样，但他们都是依靠 &lt;code&gt;handle&lt;/code&gt; 来取消定时器的，所以他们都能够清除 &lt;code&gt;setTimeout&lt;/code&gt; 和 &lt;code&gt;setIntervel&lt;/code&gt; 设置的定时器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;两个提示&lt;/h2&gt;
&lt;p&gt;标准中给出了两个提示，一个是定时器可以嵌套，但是当嵌套超过 &lt;code&gt;5&lt;/code&gt; 层的时候，最短间隔将被设为 &lt;code&gt;4ms&lt;/code&gt;，这个我已经在 &lt;code&gt;chrome&lt;/code&gt; 测试过，确实如此。但是在 &lt;code&gt;nodejs&lt;/code&gt; 中不受影响。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.time(&apos;first&apos;)
setTimeout(function () {
  console.timeEnd(&apos;first&apos;)
  console.time(&apos;second&apos;)
  setTimeout(function () {
    console.timeEnd(&apos;second&apos;)
    console.time(&apos;third&apos;)
    setTimeout(function () {
      console.timeEnd(&apos;third&apos;)
      console.time(&apos;fourth&apos;)
      setTimeout(function () {
        console.timeEnd(&apos;fourth&apos;)
        console.time(&apos;fifth&apos;)
        setTimeout(function () {
          console.timeEnd(&apos;fifth&apos;)
          console.time(&apos;sixth&apos;)
          setTimeout(function () {
            console.timeEnd(&apos;sixth&apos;)
          })
        })
      })
    })
  })
})
//chrome 输出
//first: 1.2001953125ms
//second: 1.420166015625ms
//third: 1.416259765625ms
//fourth: 1.527099609375ms
//fifth: 4.43798828125ms
//sixth: 5.159912109375ms

//nodejs输出
//first: 1.678ms
//second: 1.792ms
//third: 1.270ms
//fourth: 1.599ms
//fifth: 1.561ms
//sixth: 1.259ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二点就是我们设置的 &lt;code&gt;delay&lt;/code&gt; 延迟时间并不是精确的，要根据 &lt;code&gt;CPU&lt;/code&gt; 负载，其他的任务的执行时间。关于这一点要理解浏览器工作过程中非常重要的 &lt;code&gt;event loop&lt;/code&gt; （&lt;code&gt;nodejs&lt;/code&gt; 也有相同的设施），这个要详细说明比较复杂。大致可以这么理解，引擎只负责处理要执行的任务，但是异步任务的执行时不确定的，所以宿主环境都提供了一种设施来管理异步任务何时进入引擎的调用栈执行。引擎遇到一个异步的回调函数交给管理异步任务的模块，当这个回调函数触发了（比如我们的定时器时间到了，或者元素绑定事件触发了等），并不是直接把这个回调函数交给引擎执行（&lt;code&gt;JS&lt;/code&gt; 是单线程的，任务只能一个一个执行），而是放进浏览器管理的一个任务队列，触发的回调函数会加入这个队列，等待引擎执行完再到队列里面来取任务（这也就是所谓的 &lt;code&gt;event loop&lt;/code&gt;）。也就是我们设定的这个 &lt;code&gt;delay&lt;/code&gt; 指的是我们的回调函数什么时候进入任务队列，而不是什么时候执行。这里讲的只是一个大概的过程，具体的内容可以看我的两篇文章：&lt;a href=&quot;clloz.com/programming/front-end/js/2019/05/13/how-javascript-works-1/#_The_Callback_Queue&quot; title=&quot;JavaScript如何工作一：引擎，运行时和调用栈概述&quot;&gt;JavaScript如何工作一：引擎，运行时和调用栈概述&lt;/a&gt;和 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/11/01/event-loop/&quot; title=&quot;事件循环 Event Loop&quot;&gt;事件循环 Event Loop&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;执行细节&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WindowOrWorkerGlobalScope&lt;/code&gt; 的实例对象（即 &lt;code&gt;window&lt;/code&gt; 或者 &lt;code&gt;WorkerGlobalScope&lt;/code&gt;）都会管理一个 &lt;code&gt;list of active timers&lt;/code&gt;，也就是活动的计时器的列表。列表中的每一个项都用一个唯一的数来标记。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;setTimeout()&lt;/code&gt; 和 &lt;code&gt;setInterval()&lt;/code&gt; 的执行过程类似，唯一不同的就是 &lt;code&gt;repeat flag&lt;/code&gt;。关于执行的过程标准原文：&lt;code&gt;The setTimeout() method must return the value returned by the timer initialization steps, passing them the method&apos;s arguments, the object on which the method for which the algorithm is running is implemented (a Window or WorkerGlobalScope object) as the method context, and the repeat flag set to false.&lt;/code&gt;大致意思是 &lt;code&gt;setTimeout()&lt;/code&gt; 方法必须返回 &lt;code&gt;timer initialization steps&lt;/code&gt; 的返回值，把方法的参数传递给 &lt;code&gt;timer initialization steps&lt;/code&gt; ，&lt;code&gt;setTimeout&lt;/code&gt;方法所处的对象（&lt;code&gt;window&lt;/code&gt; 或者 &lt;code&gt;WorkerGlobalScope&lt;/code&gt; 对象）作为方法的执行上下文，最后设置 &lt;code&gt;repeat flag&lt;/code&gt;。从这里我们已经能看出方法的执行是在全局环境中，这也是为什么非严格模式下 &lt;code&gt;setTimeout&lt;/code&gt; 中的函数内的 &lt;code&gt;this&lt;/code&gt; 返回全局对象的原因。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;timer initialization steps&lt;/code&gt; 的调用需要几个参数，方法参数（&lt;code&gt;setTimeout&lt;/code&gt; 从第三个参数开始都是方法的参数），&lt;code&gt;a method context&lt;/code&gt;（&lt;code&gt;window&lt;/code&gt; 或者 &lt;code&gt;WorkerGlobalScope&lt;/code&gt;对象），&lt;code&gt;a repeat flag&lt;/code&gt; 和一个可选的 &lt;code&gt;previous handle&lt;/code&gt;（用作 &lt;code&gt;setInterval&lt;/code&gt; 的多次调用） 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设置方法的执行上下文 &lt;code&gt;method context&lt;/code&gt; 为 &lt;code&gt;window&lt;/code&gt; 或者 &lt;code&gt;WorkerGlobalScope&lt;/code&gt;对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果传递了 &lt;code&gt;previous handle&lt;/code&gt; 就用 &lt;code&gt;previous handle&lt;/code&gt; 作为 &lt;code&gt;handle&lt;/code&gt; ，否则就创建一个大于 &lt;code&gt;0&lt;/code&gt; 的整数作为 &lt;code&gt;handle&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;previous handle&lt;/code&gt; 没有提供，那么就在 &lt;code&gt;list of active timers&lt;/code&gt; 用生成的 &lt;code&gt;handle&lt;/code&gt; 添加一项。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Let callerRealm be the current Realm Record, and calleeRealm be method context&apos;s JavaScript realm.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将初始化脚本作为活动脚本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;断言：初始化脚本不为 &lt;code&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行下面的子步骤&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果对应的 &lt;code&gt;handle&lt;/code&gt; 在 &lt;code&gt;list of active timers&lt;/code&gt; 中被清除了则终止这些步骤。&lt;/li&gt;
&lt;li&gt;如果方法的第一个参数是 &lt;code&gt;Function&lt;/code&gt;，用后续的参数调用该方法，将 &lt;code&gt;method context proxy&lt;/code&gt; 作为回调函数的 &lt;code&gt;this&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;repeat flag&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，则再次调用 &lt;code&gt;timer initialization steps&lt;/code&gt;，传递相同的参数，当前的 &lt;code&gt;handle&lt;/code&gt; 作为 &lt;code&gt;previous handle&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法的第二个参数作为 &lt;code&gt;timeout&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前正在运行的任务是相同的算法创建的（我的理解是都是 &lt;code&gt;setTimeout&lt;/code&gt;，即当前的步骤是在一个 &lt;code&gt;setTimeout&lt;/code&gt; 中或者是 &lt;code&gt;repeat flag&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 的 &lt;code&gt;setInterval&lt;/code&gt;），将嵌套层级设置为当前执行的定时器的其那套层级。否则嵌套层级为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;timeout&lt;/code&gt; 小于 &lt;code&gt;0&lt;/code&gt;， 设 &lt;code&gt;timeout&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果嵌套层级大于 &lt;code&gt;5&lt;/code&gt;,并且 &lt;code&gt;timeout&lt;/code&gt; 小于 &lt;code&gt;4&lt;/code&gt; ， 设置 &lt;code&gt;timeout&lt;/code&gt; 为 &lt;code&gt;4&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;嵌套层级加一。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置任务的嵌套层级为上面计算出的嵌套层级。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;返回 &lt;code&gt;handle&lt;/code&gt;，并行运行这个算法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fully avtive&lt;/code&gt; 概念参考&lt;a href=&quot;https://html.spec.whatwg.org/multipage/browsers.html#fully-active&quot; title=&quot;标准&quot;&gt;标准&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;等待其他开始于本计时器之前，并且事件小于等于本计时器 &lt;code&gt;timeout&lt;/code&gt; 的计时器执行完成。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入任务队列，等待 &lt;code&gt;event loop&lt;/code&gt; 执行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以上就是定时器的执行过程，内容完全是个人理解翻译，可能有理解错误，欢迎指正。&lt;/p&gt;
&lt;h2&gt;注意点&lt;/h2&gt;
&lt;p&gt;从标准我们可以看出，回调函数是在全局环境执行的，有一个特殊的地方就是，无论是否在严格模式下，回调函数的 &lt;code&gt;this&lt;/code&gt;都返回 &lt;code&gt;window&lt;/code&gt; 对象。想要获得 &lt;code&gt;setTimeout&lt;/code&gt; 执行位置的词法作用域的 &lt;code&gt;this&lt;/code&gt;，一个有效的方法就是箭头函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function a() {
  setTimeout(() =&gt; {
    console.log(this)
  }, 0)
}

let obj = {
  fun: a
}

obj.fun() // { fun: [Function: a] }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;setTimeout&lt;/code&gt; 回调函数也可以获得块级作用域闭包。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  let a = 10
  setTimeout(function () {
    console.log(a) //10
  })
}
let a = 20
console.log(a) //20
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;我们上面说了 &lt;code&gt;delay&lt;/code&gt; 的最短间隔问题，同时 &lt;code&gt;delay&lt;/code&gt; 也是有上限的。&lt;code&gt;javascript&lt;/code&gt; 规定 &lt;code&gt;delay&lt;/code&gt; 是一个 &lt;code&gt;32&lt;/code&gt; 位无符号整数，这意味着 &lt;code&gt;delay&lt;/code&gt; 的上限是 $2^{32} - 1$ 即 &lt;code&gt;2147483647&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;想要清除定时器我们需要将 &lt;code&gt;setTimeout&lt;/code&gt; 或者 &lt;code&gt;setInterval&lt;/code&gt; 的返回值储存到一个变量中，当我们有嵌套的定时器或者管理的定时器较多时，如何命名和清除对应的定时器是一个要解决的问题。我今天就想到一个场景，两个嵌套的 &lt;code&gt;setInterval&lt;/code&gt;，外层的 &lt;code&gt;delay&lt;/code&gt; 比内层的 &lt;code&gt;delay&lt;/code&gt; 要短的情况下，并且我们只希望内层的 &lt;code&gt;setInterval&lt;/code&gt; 执行几次就停止，如何有效的清除对应的定时器。&lt;/p&gt;
&lt;p&gt;我们将场景设置地具体一点，我们希望内层的每个定时器执行五次后被清除，我们要如何储存定时器 &lt;code&gt;id&lt;/code&gt;，我们需要给每一个定时器不同的命名，同时需要确保我们使用的外部变量补鞥呢影响到其他定时器。我最终的解决方案是用一个对象 &lt;code&gt;timerPool&lt;/code&gt;来保存所有的定时器，属性名用 &lt;code&gt;timerPool[`timer${index}`]&lt;/code&gt;，&lt;code&gt;index&lt;/code&gt; 是一个自增的变量，外层的定时器每执行一次就自增，这样就能确保每个定时器 &lt;code&gt;id&lt;/code&gt; 保存在不同的变量中。同时这个 &lt;code&gt;index&lt;/code&gt; 是在变化的，当我们进行 &lt;code&gt;clearIterval(timerPool[`timer${index}`])&lt;/code&gt; 的时候，&lt;code&gt;index&lt;/code&gt; 已经不是我们要的那个 &lt;code&gt;index&lt;/code&gt; 了，所以需要用一个立即执行函数将内层的定时器包裹起来，将 &lt;code&gt;index&lt;/code&gt; 传递进去以保存，其他的可能会被影响的外部变量也可以参照处理。最后的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let outer = 0
let timerPool = {}
setInterval(() =&gt; {
  ;(function (outer) {
    let index = 0,
      inner = 0
    timerPool[`timer${outer}`] = setInterval(() =&gt; {
      if (index &gt;= 5) {
        inner = 0
        console.log(outer)
        clearInterval(timerPool[`timer${outer}`])
      } else {
        console.log(index, outer, inner)
        index++
        inner++
      }
    }, 1000)
  })(outer)
  outer++
}, 4000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检验结果解释每一个 &lt;code&gt;outer&lt;/code&gt; 都只执行了 &lt;code&gt;5&lt;/code&gt; 次，比如 &lt;code&gt;outer&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt; 的输出只有 &lt;code&gt;5&lt;/code&gt; 次，分别对应 &lt;code&gt;inner&lt;/code&gt; 为 &lt;code&gt;0, 1, 2, 3, 4&lt;/code&gt; 的情况。&lt;/p&gt;
&lt;p&gt;关于清除定时器还有一点需要注意的就是，如果我们在某个上下文内定义了一个定时器，同时想在该环境外部清除定时器，那我们需要将保存定时器 &lt;code&gt;id&lt;/code&gt; 的变量在外部声明。&lt;/p&gt;
&lt;h2&gt;requestAnimationFrame&lt;/h2&gt;
&lt;p&gt;在没有 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 之前，我们用 &lt;code&gt;JS&lt;/code&gt; 实现动画都是用定时器实现的，但是定时器实现动画有很多问题。&lt;/p&gt;
&lt;p&gt;先说 &lt;code&gt;setTimeout&lt;/code&gt;，&lt;code&gt;setTimeout&lt;/code&gt; 的逻辑是过一个指定长度的时间执行代码，当执行一次的时候，这不存在什么问题。当我们要实现一些递归的效果，比如每个 &lt;code&gt;1s&lt;/code&gt; 将 &lt;code&gt;div&lt;/code&gt; 换个颜色，我们就需要递归的调用 &lt;code&gt;setTimeout&lt;/code&gt;。那么这个时候我们会发现，每次执行的时间间隔不是我们指定的 &lt;code&gt;1000ms&lt;/code&gt;，而是 &lt;code&gt;1000ms&lt;/code&gt; 加上函数执行的时间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let i = 0
setTimeout(function fn() {
  console.log(i++)
  setTimeout(fn, 1000)
}, 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;setInterval&lt;/code&gt; 是定时将函数推入任务队列（参考 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/11/01/event-loop/&quot; title=&quot;事件循环 Event Loop&quot;&gt;事件循环 Event Loop&lt;/a&gt;），我们能确定的只是回调函数进入任务队列的时间间隔。而且如果 &lt;code&gt;js&lt;/code&gt; 线程很忙碌，在我们下一个定时器要进去的时候发现上一个定时器的回调函数还没执行，那么这次插入任务队列就会失败。&lt;code&gt;setInterval&lt;/code&gt; 会尝试在下一次间隔到了再次查看任务队列，只有任务队列没有来自同一个定时器的任务才会插入成功。此时还有一个问题就是我们会发现相邻的任务之间的时间间隔消失了，变成了连续执行。比如下面这段代码，我们会发现执行之间没有了间隔。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.time(&apos;log&apos;)
setInterval(() =&gt; {
  console.timeLog(&apos;log&apos;)
  let start = new Date().getTime()
  while (new Date().getTime() - start &amp;#x3C; 1000) {}
  console.timeLog(&apos;log&apos;)
}, 500)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，&lt;code&gt;NodeJS&lt;/code&gt; 的 &lt;code&gt;setInterval&lt;/code&gt; 的逻辑和浏览器不同。&lt;code&gt;NodeJS&lt;/code&gt; 中是回调函数执行完毕之后才会尝试向任务队列插入新的任务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从这个角度看 &lt;code&gt;setTimeout&lt;/code&gt; 比 &lt;code&gt;setInterval&lt;/code&gt; 要好一些，我们的执行不会被丢弃，并且我们可以在回调函数中对时间进行修正来改善 &lt;code&gt;setTimeout&lt;/code&gt; 时间不准的问题。但是它们两个都有一个问题是无法跟浏览器的渲染同步。&lt;/p&gt;
&lt;p&gt;我们都知道 &lt;code&gt;setTimeout&lt;/code&gt; 和 &lt;code&gt;setInterval&lt;/code&gt; 是宏任务，一个 &lt;code&gt;Event Loop&lt;/code&gt; 执行一次。但是可能很多人不知道，并不是每一次 &lt;code&gt;Event Loop&lt;/code&gt; 都会执行 &lt;code&gt;UI render&lt;/code&gt; 的。浏览器会根据自己的分析决定在哪一个 &lt;code&gt;Event Loop&lt;/code&gt; 渲染。比如下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;setTimeout(() =&gt; {
  document.body.style.background = &apos;red&apos;
  setTimeout(() =&gt; {
    document.body.style.background = &apos;blue&apos;
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们是希望页面先变成红色在变成蓝色，但是实际执行我们会发现很多时候浏览器并不会渲染红色，而是直接显示蓝色，有时候能够正常渲染出颜色的变化，这是为什么呢？其实道理也很简单，如果这两个 &lt;code&gt;setTimeout&lt;/code&gt; 任务之间正好遇上了浏览器渲染，那么就能成功看到红色变蓝色。如果两次任务之间浏览器没有渲染，那么就只能看到蓝色。也就是说，丢帧了。如果我们用 &lt;code&gt;setTimeout&lt;/code&gt; 或者 &lt;code&gt;setInterval&lt;/code&gt; 做动画，很可能最后的效果不是很好，因为浏览器的渲染是不确定的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果是用定时器做动画，我们一般设置间隔为 &lt;code&gt;17ms&lt;/code&gt;，因为一般情况下浏览器渲染是一秒钟 &lt;code&gt;60&lt;/code&gt; 帧，也就是 &lt;code&gt;16.7ms&lt;/code&gt; 渲染一次。但是根据具体的浏览器和机器，帧数不一定相同。细节参考 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/11/01/event-loop/&quot; title=&quot;事件循环 Event Loop&quot;&gt;事件循环 Event Loop&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为了能够实现平滑的动画，&lt;code&gt;HTML5&lt;/code&gt; 提供了 &lt;code&gt;window.requestAnimationFrame()&lt;/code&gt; 这个 &lt;code&gt;API&lt;/code&gt;， 它的功能是告诉浏览器——你希望执行一个动画，并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数，该回调函数会在浏览器下一次重绘之前执行。所以 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 能够和页面渲染同步，能够完美解决我们平滑执行动画的问题。&lt;/p&gt;
&lt;p&gt;若你想在浏览器下次重绘之前继续更新下一帧动画，那么回调函数自身必须再次调用 &lt;code&gt;window.requestAnimationFrame()&lt;/code&gt;。返回值是一个 &lt;code&gt;long&lt;/code&gt; 整数，请求 &lt;code&gt;ID&lt;/code&gt; ，是回调列表中唯一的标识。是个非零值，没别的意义。你可以传这个值给 &lt;code&gt;window.cancelAnimationFrame()&lt;/code&gt; 以取消回调函数。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 还有一些好处就是在页面被最小化或者我们切换到别的页面时，它是不会触发的，因为此时页面不会渲染，所以它能够节省 &lt;code&gt;CPU&lt;/code&gt; 的开销。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844903877976981517&quot; title=&quot;浅谈 requestAnimationFrame&quot;&gt;浅谈 requestAnimationFrame&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/xiaohuochai/p/5777186.html&quot; title=&quot;深入理解定时器系列第二篇——被誉为神器的requestAnimationFrame&quot;&gt;深入理解定时器系列第二篇——被誉为神器的requestAnimationFrame&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/142742003&quot; title=&quot;深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系&quot;&gt;深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>解构赋值的一些细节</title><link>https://clloz.com/blog/destructure-details</link><guid isPermaLink="true">https://clloz.com/blog/destructure-details</guid><pubDate>Wed, 08 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;解构赋值 &lt;code&gt;Destructuring assignment&lt;/code&gt; 是 &lt;code&gt;ES6&lt;/code&gt; 提供的新语法，通过解构赋值我们可以从对象或数组（类数组对象也可）中取出属性或值，赋值给其他变量。本文整理一下比较容易忽略和不太好理解的点。&lt;/p&gt;
&lt;h2&gt;知识点&lt;/h2&gt;
&lt;h2&gt;undefined 的确定&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 用严格相等运算符来判断一个位置是否有值。在解构赋值中只有一个位置的值严格等于 &lt;code&gt;undefined&lt;/code&gt;，我们设置的默认值才会生效。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let [x = 1] = [undefined]
x // 1

let [x = 1] = [null]
x // null
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;默认值表达式惰性求值&lt;/h2&gt;
&lt;p&gt;如果解构赋值中某个变量的默认值是个表达式，那么这个表达式是惰性求值的，也就是只有需要执行的时候才会求值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function f() {
  console.log(&apos;aaa&apos;)
}

let [x = f()] = [1] //f() 不会执行
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;对象解构机制&lt;/h2&gt;
&lt;p&gt;数组的解构赋值是根据变量的位置来确定其值的。由于对象不像数组一样是按次序排列的，所以对象的解构赋值只能根据变量的名称到对象中查找。但是需要注意的是，我们要区分好用于匹配的模式（可以理解为键值对中的键）和具体的对象，特别是在嵌套的对象解构中。我的理解就是解构表达式中的变量表示中不管嵌套多少层，有多少标识符，第一个无法在对象中找到的标识符就是变量的名称，如果每一个标识符都能找到，那么就是隐藏了一个和最后一个标识符同名的变量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let { foo: baz } = { foo: &apos;aaa&apos;, bar: &apos;bbb&apos; }
baz // &quot;aaa&quot;
foo // error: foo is not defined
//foo是匹配的模式，baz才是变量，模式只是用来到对象中查找属性，而变量则是最后赋值的目标

let {
  foo: { bar }
} = { baz: &apos;baz&apos; } //foo 无法在对象中找到，所以是 undefined，此时再想向下找属性就会报错

//对象的解构赋值可以找原型上的属性
const obj1 = {}
const obj2 = { foo: &apos;bar&apos; }
Object.setPrototypeOf(obj1, obj2)

const { foo } = obj1
foo // &quot;bar&quot;

//数组是特殊的对象，所以可以对数组进行对象属性的解构
let arr = [1, 2, 3]
let { 0: first, [arr.length - 1]: last } = arr
first // 1
last // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解构赋值用引擎的内部方法 &lt;code&gt;toObject()&lt;/code&gt;（我们无法在 &lt;code&gt;runtime&lt;/code&gt; 访问到这个方法）强制将源数据转为对象。也就是说如果 &lt;code&gt;source&lt;/code&gt; 是一个原始数据类型，会被转为对应的包装对象。由于 &lt;code&gt;null&lt;/code&gt; 和 &lt;code&gt;undefined&lt;/code&gt; 无法转为对象，所以会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let { length } = &apos;foobar&apos;
console.log(length) // 6

let { constructor: c } = 4
console.log(c === Number) // true

let { _ } = null // TypeError

let { _ } = undefined // TypeError
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果解构赋值语句不是变量声明语句（前面没有 &lt;code&gt;var&lt;/code&gt;，&lt;code&gt;let&lt;/code&gt;，&lt;code&gt;const&lt;/code&gt;），即对已经声明的变量进行进行解构赋值需要注意加上括号。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let x;
{x} = {x: 1}; //Uncaught SyntaxError: Unexpected token &apos;=&apos;  行首的大括号会被引擎认为是代码块
({x} = {x: 1}); //这是正确写法，但是和立即执行函数一样，该语句的前面一行最好加上分号，否则可能会被当做函数调用。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;嵌套的对象的解构赋值可以用来复制对象属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let person = {
  name: &apos;Matt&apos;,
  age: 27,
  job: {
    title: &apos;Software engineer&apos;
  }
}
let personCopy = {}

;({ name: personCopy.name, age: personCopy.age, job: personCopy.job } = person)

// Because an object reference was assigned into personCopy, changing a property
// inside the person.job object will be propagated to personCopy:
person.job.title = &apos;Hacker&apos;

console.log(person)
// { name: &apos;Matt&apos;, age: 27, job: { title: &apos;Hacker&apos; } }

console.log(personCopy)
// { name: &apos;Matt&apos;, age: 27, job: { title: &apos;Hacker&apos; } }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于包含了多个属性赋值的解构赋值语句，多个属性的赋值是依次执行，相互独立的，也就是说如果第一个属性赋值成功，第二个失败报错的话，第一个赋值依然是成功的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let person = {
  name: &apos;Matt&apos;,
  age: 27
}

let personName, personBar, personAge

try {
  // person.foo is undefined, so this will throw an error
  ;({
    name: personName,
    foo: { bar: personBar },
    age: personAge
  } = person)
} catch (e) {
  console.log(e) //TypeError: Cannot read property &apos;bar&apos; of undefined
}

console.log(personName, personBar, personAge)
// Matt, undefined, undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构赋值还可以配合 &lt;code&gt;for ... of&lt;/code&gt; 进行使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var people = [
  {
    name: &apos;Mike Smith&apos;,
    family: {
      mother: &apos;Jane Smith&apos;,
      father: &apos;Harry Smith&apos;,
      sister: &apos;Samantha Smith&apos;
    },
    age: 35
  },
  {
    name: &apos;Tom Jones&apos;,
    family: {
      mother: &apos;Norah Jones&apos;,
      father: &apos;Richard Jones&apos;,
      brother: &apos;Howard Jones&apos;
    },
    age: 25
  }
]

for (var {
  name: n,
  family: { father: f }
} of people) {
  console.log(&apos;Name: &apos; + n + &apos;, Father: &apos; + f)
}

// &quot;Name: Mike Smith, Father: Harry Smith&quot;
// &quot;Name: Tom Jones, Father: Richard Jones&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解构赋值可以使用属性名表达式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let key = &apos;z&apos;
let { [key]: foo } = { z: &apos;bar&apos; }

console.log(foo) // &quot;bar&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;剩余参数也可以运用到对象的解构赋值中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let { a, b, ...rest } = { a: 10, b: 20, c: 30, d: 40 }
console.log(a) // 10
console.log(b) // 20
console.log(rest) // { c: 30, d: 40 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解构赋值的属性查找会查找原型链上的属性：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 声明对象 和 自身 self 属性
var obj = { self: &apos;123&apos; }
// 在原型链中定义一个属性 prot
obj.__proto__.prot = &apos;456&apos;
// test
const { self, prot } = obj
// self &quot;123&quot;
// prot &quot;456&quot;（访问到了原型链）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组的解构赋值&lt;/h2&gt;
&lt;p&gt;数组的解构赋值表达式的右值不是一个可遍历结构，则会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 报错 Uncaught TypeError: xxx is not iterable
let [foo] = 1
let [foo] = false
let [foo] = NaN
let [foo] = undefined
let [foo] = null
let [foo] = {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数组的解构赋值也可以这样使用 &lt;code&gt;let [,m] = [2,3]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;数组的解构赋值还支持剩余模式：&lt;code&gt;var [a, ...b] = [1, 2, 3];&lt;/code&gt;，但是要注意如果剩余元素右侧有逗号，会抛出 &lt;code&gt;SyntaxError&lt;/code&gt;，因为剩余元素必须是数组的最后一个元素。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var [a, ...b] = [1, 2, 3]
// SyntaxError: rest element may not have a trailing comma
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数值、字符串和布尔型&lt;/h2&gt;
&lt;p&gt;解构赋值的规则是，只要等号右边的值不是对象或数组，就先将其转为对象，字符串被转为类数组对象，数值和布尔型则转为包装对象。由于 &lt;code&gt;undefined&lt;/code&gt; 和 &lt;code&gt;null&lt;/code&gt; 无法转为对象，所以对它们进行解构赋值，都会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const [a, b, c, d, e] = &apos;hello&apos;
a // &quot;h&quot;
b // &quot;e&quot;
c // &quot;l&quot;
d // &quot;l&quot;
e // &quot;o&quot;

//字符串转为的对象有length属性
let { length: len } = &apos;hello&apos;
len // 5

let { toString: s } = 123
s === Number.prototype.toString // true

let { toString: s } = true
s === Boolean.prototype.toString // true

let { prop: x } = undefined // TypeError
let { prop: y } = null // TypeError
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;函数参数的解构赋值&lt;/h2&gt;
&lt;p&gt;函数参数的解构赋值如果是一个对象，不会影响 &lt;code&gt;arguments&lt;/code&gt; 的 &lt;code&gt;length&lt;/code&gt;，它只是允许你在函数签名中声明变量，并且能够立即在函数体中使用它。&lt;/p&gt;
&lt;p&gt;一个函数签名 (或类型签名，或方法签名) 定义了函数或方法 的输入与输出。一个签名可以包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数及参数的类型&lt;/li&gt;
&lt;li&gt;一个返回值及其类型&lt;/li&gt;
&lt;li&gt;可能会抛出或传回的异常&lt;/li&gt;
&lt;li&gt;有关面向对象程序中方法可用性的信息 (例如关键字 &lt;code&gt;public&lt;/code&gt;、&lt;code&gt;static&lt;/code&gt; 或 &lt;code&gt;prototype&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let person = {
  name: &apos;Matt&apos;,
  age: 27
}

function printPerson(foo, { name, age }, bar) {
  console.log(arguments)
  console.log(name, age)
}

function printPerson2(foo, { name: personName, age: personAge }, bar) {
  console.log(arguments)
  console.log(personName, personAge)
}

printPerson(&apos;1st&apos;, person, &apos;2nd&apos;)
// [&apos;1st&apos;, { name: &apos;Matt&apos;, age: 27 }, &apos;2nd&apos;]
// &apos;Matt&apos;, 27

printPerson2(&apos;1st&apos;, person, &apos;2nd&apos;)
// [&apos;1st&apos;, { name: &apos;Matt&apos;, age: 27 }, &apos;2nd&apos;]
// &apos;Matt&apos;, 27
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数参数的解构赋值需要注意的是默认参数设定的机制。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//设置解构赋值的默认值
function move({ x = 0, y = 0 } = {}) {
  return [x, y]
}

move({ x: 3, y: 8 }) // [3, 8]
move({ x: 3 }) // [3, 0]
move({}) // [0, 0]
move() // [0, 0]
move({ x: undefined, y: undefined }) //[0, 0]

//设置参数默认值
function move({ x, y } = { x: 0, y: 0 }) {
  return [x, y]
}

move({ x: 3, y: 8 }) // [3, 8]
move({ x: 3 }) // [3, undefined]
move({}) // [undefined, undefined]
move() // [0, 0]
move({ x: undefined, y: undefined }) //[undefined, undefined]

//注意下面这种会直接报错
function move({ x = 0, y = 0 }) {
  return [x, y]
}
move() //Uncaught TypeError: Cannot read property &apos;x&apos; of undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里说实话不是很好理解，阮一峰老师的书里也没有讲的非常清楚，是一笔带过。最后我说一下自己的理解。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ES6标准入门&lt;/code&gt; 中关于函数的解构赋值的例子中关于默认值的设定都是非对象的，比如 &lt;code&gt;let {x = 10, y = 20} = {x: 3}&lt;/code&gt; 。但是在函数参数的情况里面并不是这样。函数参数中的例子是 &lt;code&gt;function move({x = 0, y = 0} = {}){}&lt;/code&gt; 这样的形式，用之前的那种理解无法解释这种行为。&lt;/p&gt;
&lt;p&gt;函数的参数是一个 &lt;code&gt;arguments&lt;/code&gt;，一个迭代器，类数组对象，所以上面的函数可以类比为这样的一个式子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function move({ x = 0, y = 0 } = {}) {}
;[{ x = 10, y = 20 } = {}] = arguments[({ x = 10, y = 20 } = {})] = []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过这种转化之后似乎清晰一些，但是如何理解呢。我们把 &lt;code&gt;{x = 10, y = 20}&lt;/code&gt; 看做一个整体，上面的式子变为 &lt;code&gt;[obj = {}] = []&lt;/code&gt;，也就是我们设置 &lt;code&gt;obj&lt;/code&gt; 的默认值为 &lt;code&gt;{}&lt;/code&gt;（也可以理解为设置参数默认值，是另一种思路，不过本质也没有区别），只要我们传入的 &lt;code&gt;arguments&lt;/code&gt; 中有参数的话，就不会用这个默认值 &lt;code&gt;{}&lt;/code&gt;，只有当我们的 &lt;code&gt;arguments&lt;/code&gt; 的索引为 &lt;code&gt;0&lt;/code&gt; 的元素严格等于 &lt;code&gt;undefined&lt;/code&gt; 的时候才会用到默认值 &lt;code&gt;{}&lt;/code&gt;，使用默认值就相当于执行 &lt;code&gt;{x = 10, y = 20} = {}&lt;/code&gt; 的解构赋值。当 &lt;code&gt;arguments&lt;/code&gt; 的索引为 &lt;code&gt;0&lt;/code&gt; 的元素不严格等于 &lt;code&gt;undefined&lt;/code&gt; 的时候则会把这个元素转为一个对象 &lt;code&gt;object&lt;/code&gt;，执行 &lt;code&gt;{x = 10, y = 20} = object&lt;/code&gt; 的解构赋值，如果找不到，则使用默认值 &lt;code&gt;10 ，20&lt;/code&gt;，不管传入的是数字还是字符串等，都能正常执行；唯一会报错的就是 &lt;code&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;有了这个逻辑，我们分析其他的两种情况就很简单了。比如 &lt;code&gt;function move({x, y} = {x:0, y:0}){}&lt;/code&gt; 这种情况，就可以理解为 &lt;code&gt;[obj = {x:0, y:0}] = []&lt;/code&gt;，只有当 &lt;code&gt;arguments&lt;/code&gt; 索引为 &lt;code&gt;0&lt;/code&gt; 的元素严格等于 &lt;code&gt;undefined&lt;/code&gt; 的时候才会变成执行 &lt;code&gt;{x, y} = {x:0, y:0}&lt;/code&gt;，否则就是把索引为 &lt;code&gt;0&lt;/code&gt; 的元素转为对象 &lt;code&gt;object&lt;/code&gt; 执行 &lt;code&gt;{x, y} = object&lt;/code&gt;，这也是为什么只有 &lt;code&gt;move()&lt;/code&gt; 不传参的时候才能输出 &lt;code&gt;[0, 0]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;function move({x = 0, y = 0}){}&lt;/code&gt; 为什么在不传参的时候报错，它相当于执行 &lt;code&gt;[{x = 0, y = 0}] = []&lt;/code&gt;，要在 &lt;code&gt;arguments&lt;/code&gt; 索引为 &lt;code&gt;0&lt;/code&gt; 的元素转为的对象上找 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 属性，但这个值是 &lt;code&gt;undefined&lt;/code&gt;，所以最终报错。&lt;/p&gt;
&lt;p&gt;逻辑可能有点绕，最后我总结一下比较重要的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于有嵌套结构（且嵌套的数组或对象没有设置初始值）的解构赋值，等号左右两边嵌套结构必须相同。属于模式匹配，所以等号两边模式要相同。&lt;/li&gt;
&lt;li&gt;对对象或数组设置初始值（也必须是一个对象或数组），则该对象和数组被看做一个整体，在完成第一步结构赋值以后才能确定目标进行第二次解构赋值。比如 &lt;code&gt;[{x = 10, y = 20} = {}] = [{x: 1}]&lt;/code&gt; 先进行 &lt;code&gt;[obj = {}] = [{x: 1}]&lt;/code&gt;，确定了 &lt;code&gt;obj&lt;/code&gt; 的解构赋值目标是 &lt;code&gt;{x: 1}&lt;/code&gt; 之后进行第二步的解构赋值 &lt;code&gt;{x = 10, y = 20} = {x: 1}&lt;/code&gt;，最后的结果是 &lt;code&gt;x: 1, y: 20&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对于嵌套结构中的对象，如果设置了初始值（也是对应的对象），则等号右边结构的对应位置可以是除 &lt;code&gt;null&lt;/code&gt; 以外的任何值，因为 &lt;code&gt;undefined&lt;/code&gt; 会将目标设置为初始值，而其他值都能转为对象。&lt;/li&gt;
&lt;li&gt;对于嵌套结构中的数组，如果设置了初始值（应是一个 &lt;code&gt;Iterator&lt;/code&gt;），则等号右边结构对应位置必须是一个 &lt;code&gt;Iterator&lt;/code&gt;，否则报错。比如 &lt;code&gt;[[a = 1, b = 2]] = [1]&lt;/code&gt; 会报错，而 &lt;code&gt;[[a = 1, b = 2]] = [&apos;ab&apos;]&lt;/code&gt; 则能正确解构赋值，结果为 &lt;code&gt;a: &apos;a&apos;, b: &apos;b&apos;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;多层的嵌套每一层都可以给任意结构设定初始值，逻辑和上面相同，不顾一般不会使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;函数默认参数的使用也和解构赋值的默认值的生效类似，当传入的参数严格等于 &lt;code&gt;undefined&lt;/code&gt; 即使用默认参数，&lt;code&gt;[1, undefined, 3].map((x = &apos;yes&apos;) =&gt; x); // [ 1, &apos;yes&apos;, 3 ]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的函数用的是对象作为参数，其实数组作为参数的情形也是一样的。但是注意数组解构赋值的右值必须是一个可遍历结构。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//设置默认参数
function a([x, y] = [5, 6]) {
  console.log(x, y)
}
a() //5, 6
a([1]) //1, undefined
a(&apos;asdf&apos;) // a, s
a(1) //VM1241:1 Uncaught TypeError: undefined is not a function(不知道为什么不是not iterable的报错)

//设置默认值
function a([x = 5, y = 6] = []) {
  console.log(x, y)
}
a() // 5, 6
a([1]) //1, 6
a(&apos;asdf&apos;) // a, s
a(1) //VM1241:1 Uncaught TypeError: undefined is not a function
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你希望能够在不提供任何参数的情况下调用该函数，就使用默认值模式。如果你只是想用解构赋值给函数一个默认参数则使用另一种。&lt;/p&gt;
&lt;h2&gt;解构赋值中的括号&lt;/h2&gt;
&lt;p&gt;解构赋值虽然很方便，但是解析起来并不容易。对于编译器来说，一个式子到底是模式，还是表达式，没有办法从一开始就知道，必须解析到（或解析不到）等号才能知道。由此带来的问题是，如果模式中出现圆括号怎么处理。&lt;code&gt;ES6&lt;/code&gt; 的规则是，只要有可能导致解构的歧义，就不得使用圆括号。但是，这条规则实际上不那么容易辨别，处理起来相当麻烦。因此，建议只要有可能，就不要在模式中放置圆括号。&lt;/p&gt;
&lt;p&gt;总结起来就是两个规则： 1. 变量声明语句中不可以使用（函数参数也属于变量声明） 2. 不可以把模式（键）包含在小括号中（数组的模式是按位置匹配，所以把数组元素括起来可以）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 变量声明 报错
let [(a)] = [1];

let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};

let { o: ({ p: p }) } = { o: { p: 2 } };

//函数参数 报错
function f([(z)]) { return z; }
function f([z,(x)]) { return x; }

//模式括号 报错
({ p: a }) = { p: 42 };
([a]) = [5];

[({ p: a }), { x: c }] = [{}, {}];

//正确
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;除了大括号在行首用括号将表达式括起来，其他情况尽量不要使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;用途&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将对象的方法赋值给某个变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 例一
let { log, sin, cos } = Math

// 例二
const { log } = console
log(&apos;hello&apos;) // hello
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;交换变量的值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let x = 1
let y = 2

;[x, y] = [y, x]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数返回多个值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 返回一个数组

function example() {
  return [1, 2, 3]
}
let [a, b, c] = example()

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  }
}
let { foo, bar } = example()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数参数定义&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提取 &lt;code&gt;JSON&lt;/code&gt; 数据&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let jsonData = {
  id: 42,
  status: &apos;OK&apos;,
  data: [867, 5309]
}

let { id, status, data: number } = jsonData

console.log(id, status, number)
// 42, &quot;OK&quot;, [867, 5309]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数参数默认值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;jQuery.ajax = function (
  url,
  {
    async = true,
    beforeSend = function () {},
    cache = true,
    complete = function () {},
    crossDomain = false,
    global = true
    // ... more config
  } = {}
) {
  // ... do stuff
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历 &lt;code&gt;Map&lt;/code&gt; 解构&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;jQuery.ajax = function (
  url,
  {
    async = true,
    beforeSend = function () {},
    cache = true,
    complete = function () {},
    crossDomain = false,
    global = true
    // ... more config
  } = {}
) {
  // ... do stuff
}

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [, value] of map) {
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入模块的指定方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const { SourceMapConsumer, SourceNode } = require(&apos;source-map&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;《ECMAScript6入门》 —— 阮一峰&lt;/li&gt;
&lt;li&gt;MDN&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>ECMAScript标准版本</title><link>https://clloz.com/blog/ecmascript-version</link><guid isPermaLink="true">https://clloz.com/blog/ecmascript-version</guid><pubDate>Tue, 07 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ECMAScript&lt;/code&gt;，简称 &lt;code&gt;ES&lt;/code&gt;，是由 &lt;code&gt;ecma-international&lt;/code&gt;（前身为欧洲计算机制造商协会,英文名称是 &lt;code&gt;European Computer Manufacturers Association&lt;/code&gt;）按照 &lt;code&gt;ECMA-262&lt;/code&gt; 和 &lt;code&gt;ISO/IEC 16262&lt;/code&gt; 标准制定的一种脚本语言规范。本文讲一讲 &lt;code&gt;ES&lt;/code&gt; 标准的发展历史以及几个重要版本的特性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ES&lt;/code&gt; 是语言的标准 &lt;code&gt;Standard&lt;/code&gt;，浏览器厂商根据标准的实现 &lt;code&gt;Implementation&lt;/code&gt; 有多种，比如网景的&lt;code&gt;JavaScript&lt;/code&gt; 和 微软的 &lt;code&gt;JScript&lt;/code&gt;，后来随着浏览器和标准的发展以及浏览器厂商之间的妥协，逐渐合并为现在的 &lt;code&gt;JavaScript&lt;/code&gt;，但实际上 &lt;code&gt;JavaScript&lt;/code&gt; 是 &lt;code&gt;ECMA-262&lt;/code&gt; 标准的实现和扩展。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;历史和发展&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/ecma-history.CNQTh_FM_1Kh7q.webp&quot; alt=&quot;ecma-history&quot; title=&quot;ecma-history&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图直观的展示了 &lt;code&gt;JavaScript&lt;/code&gt; 标准和实现的发展时间轴。&lt;/p&gt;
&lt;p&gt;事实上 &lt;code&gt;JavaScript&lt;/code&gt; 的发展历程是比较奇葩的，&lt;code&gt;1995&lt;/code&gt; 年浏览器厂商网景出于需求要开发一种能在浏览器使用的脚本语言，于是招募了布兰登·艾克，其在该年五月花了十天时间设计出了 &lt;code&gt;JavaScript&lt;/code&gt; 的原型，这门语言最初是叫 &lt;code&gt;Mocha&lt;/code&gt;，然后在同年发布的 &lt;code&gt;Netscape Navigator 2.0&lt;/code&gt; 中被改名为 &lt;code&gt;LiveScript&lt;/code&gt;，又随着 &lt;code&gt;12月&lt;/code&gt; 发布的 &lt;code&gt;Netscape Navigator 2.0 Beta 3&lt;/code&gt; 被改名为 &lt;code&gt;JavaScript&lt;/code&gt;，一直沿用至今。使用这个名字的原因也只是当时网景和 &lt;code&gt;Sun&lt;/code&gt; 结为开发联盟，为了搭上 &lt;code&gt;Java&lt;/code&gt; 这个热词，导致了很多人对这个名字产生误解，其实 &lt;code&gt;JavaScript&lt;/code&gt; 和 &lt;code&gt;Java&lt;/code&gt; 没什么关系。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 在浏览器上取得了成功，微软也马上跟进，在自己的 &lt;code&gt;Internet Explorer 3&lt;/code&gt; 上推出了自己的 &lt;code&gt;JScript&lt;/code&gt;。接着就是二者各行其是，很长一段时间，各大浏览器厂商的实现都不尽相同，对于语言标准化的推进造成非常大的阻力，也成为开发者的灾难，要让自己的网页兼容不同的浏览器需要耗费非常大的精力。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1996&lt;/code&gt; 年 &lt;code&gt;11&lt;/code&gt; 月，网景正式向 &lt;code&gt;ECMA&lt;/code&gt;（欧洲计算机制造商协会）提交语言标准。&lt;code&gt;1997&lt;/code&gt; 年 &lt;code&gt;6&lt;/code&gt; 月，&lt;code&gt;ECMA&lt;/code&gt; 以 &lt;code&gt;JavaScript&lt;/code&gt; 语言为基础制定了 &lt;code&gt;ECMAScript&lt;/code&gt; 标准规范 &lt;code&gt;ECMA-262&lt;/code&gt;。&lt;code&gt;JavaScript&lt;/code&gt; 成为了 &lt;code&gt;ECMAScript&lt;/code&gt; 最著名的实现之一。&lt;/p&gt;
&lt;h2&gt;ES 版本&lt;/h2&gt;
&lt;p&gt;| 版本 | 发表日期   | 与前版本的差异                                                                                                                                                                                                                                                                                                   |
| ---- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1    | 1997年6月  | 首版                                                                                                                                                                                                                                                                                                             |
| 2    | 1998年6月  | 格式修正，以使得其形式与ISO/IEC16262国际标准一致                                                                                                                                                                                                                                                                 |
| 3    | 1999年12月 | 强大的正则表达式，更好的词法作用域链处理，新的控制指令，异常处理，错误定义更加明确，数据输出的格式化及其它改变                                                                                                                                                                                                   |
| 4    | 放弃       | 由于关于语言的复杂性出现分歧，第4版本被放弃，其中的部分成为了第5版本及Harmony的基础                                                                                                                                                                                                                              |
| 5    | 2009年12月 | 新增“严格模式（strict mode）”，一个子集用作提供更彻底的错误检查,以避免结构出错。澄清了许多第3版本的模糊规范，并适应了与规范不一致的真实世界实现的行为。增加了部分新功能，如getters及setters，支持JSON以及在对象属性上更完整的反射                                                                                |
| 5.1  | 2011年6月  | ECMAScript标5.1版形式上完全一致于国际标准ISO/IEC 16262:2011。                                                                                                                                                                                                                                                    |
| 6    | 2015年6月  | ECMAScript 2015（ES2015），第 6 版，最早被称作是 ECMAScript 6（ES6），添加了类和模块的语法，其他特性包括迭代器，Python风格的生成器和生成器表达式，箭头函数，二进制数据，静态类型数组，集合（maps，sets 和 weak maps），promise，reflection 和 proxies。作为最早的 ECMAScript Harmony 版本，也被叫做ES6 Harmony。 |
| 7    | 2016年6月  | ECMAScript 2016（ES2016），第 7 版，多个新的概念和语言特性                                                                                                                                                                                                                                                       |
| 8    | 2017年6月  | ECMAScript 2017（ES2017），第 8 版，多个新的概念和语言特性                                                                                                                                                                                                                                                       |
| 9    | 2018年6月  | ECMAScript 2018 （ES2018），第 9 版，包含了异步循环，生成器，新的正则表达式特性和 rest/spread 语法。                                                                                                                                                                                                             |
| 10   | 2019年6月  | ECMAScript 2019 （ES2019），第 10 版                                                                                                                                                                                                                                                                             |
| 11   | 2020年6月  | ECMAScript 2020 （ES2020），第 11 版                                                                                                                                                                                                                                                                             |&lt;/p&gt;
&lt;p&gt;其中比较重要的几个版本分别是 &lt;code&gt;ES3&lt;/code&gt;， &lt;code&gt;ES5&lt;/code&gt;，&lt;code&gt;ES6&lt;/code&gt;，现在的标准制定委员会&lt;a href=&quot;https://github.com/tc39&quot; title=&quot;TC39&quot;&gt;TC39&lt;/a&gt;希望是每年提供一些新的特性，而不是进行非常巨大的改动，这样标准和实现更加能够协同。&lt;/p&gt;
&lt;p&gt;从 &lt;code&gt;ES3&lt;/code&gt; 到 &lt;code&gt;ES5&lt;/code&gt; 经过了十年，各大厂商对于标准的跟进也是兴趣缺缺。我个人认为还是需求导致的，说白了就是目前有的就够用了，整个 &lt;code&gt;10&lt;/code&gt; 年，除了 &lt;code&gt;05&lt;/code&gt; 年的 &lt;code&gt;Ajax&lt;/code&gt; 并没有什么太多的新技术和新需求出现。但是从 &lt;code&gt;10&lt;/code&gt; 年开始，随着智能手机，平板电脑的出现，以及个人电脑性能的不断提升，我们在各种设备商能做的事情越来越多，而且前端作为直接与用户交互的一个重要部分，也随着这股浪潮出现了爆炸式的增长，需求的增加自然推动了新的技术的产生和标准化的需求，后面标准的更新就越来越频繁了。&lt;/p&gt;
&lt;p&gt;想要查看各个版本的标准的新特性以及浏览器的支持情况可以查看 &lt;a href=&quot;https://kangax.github.io/compat-table/es6/&quot; title=&quot;compat-table&quot;&gt;compat-table&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;TC39 和 ECMAScript&lt;/h2&gt;
&lt;p&gt;标准的制定是一个比较长的过程，可以参考 &lt;a href=&quot;https://mp.weixin.qq.com/s/BJNb3vlvdOP1pLgGq46-nw&quot; title=&quot;关于查看W3C文档&quot;&gt;关于查看W3C文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ECMAScript&lt;/code&gt; 的标准制定主要是由 &lt;code&gt;TC39&lt;/code&gt; 这个组织进行的，该组织主要由各个主流浏览器厂商的代表构成。一个标准的落地基本都是从 &lt;code&gt;stage0&lt;/code&gt; 到 &lt;code&gt;stage4&lt;/code&gt;。分别对应 &lt;code&gt;strawman&lt;/code&gt;，&lt;code&gt;proposal&lt;/code&gt;，&lt;code&gt;draft&lt;/code&gt; ，&lt;code&gt;candidate&lt;/code&gt; 和 &lt;code&gt;finished&lt;/code&gt;，可以参考 &lt;a href=&quot;https://zhuanlan.zhihu.com/p/27762556&quot; title=&quot;精读 TC39 与 ECMAScript 提案&quot;&gt;精读 TC39 与 ECMAScript 提案&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;一般来说最新的标准我们可以到 &lt;a href=&quot;https://tc39.es/ecma262/&quot; title=&quot;TC39 - lastest ecma262&quot;&gt;TC39 - lastest ecma262&lt;/a&gt;， &lt;code&gt;stage3&lt;/code&gt; 的一些特性我们可以到 &lt;a href=&quot;https://github.com/tc39/proposals&quot; title=&quot;proposals - tc39&quot;&gt;proposals - tc39&lt;/a&gt; 查看。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/xiaomowang/p/12312655.html&quot; title=&quot;闲谈一下，ES3、ES4、ES5、ES6 分别是什么&quot;&gt;闲谈一下，ES3、ES4、ES5、ES6 分别是什么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/ECMAScript&quot; title=&quot;ECMAScript-wiki&quot;&gt;ECMAScript-wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/chuangxin/article/details/85088485&quot; title=&quot;谈谈JavaScript版本演进史及ES3、ES5区别和特性&quot;&gt;谈谈JavaScript版本演进史及ES3、ES5区别和特性&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/js-his.B5vdGUaq.png"/><enclosure url="/_astro/js-his.B5vdGUaq.png"/></item><item><title>JavaScript 中 Object 相关知识点整理</title><link>https://clloz.com/blog/javascript-object-api</link><guid isPermaLink="true">https://clloz.com/blog/javascript-object-api</guid><pubDate>Sat, 04 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文介绍 &lt;code&gt;JavaScript&lt;/code&gt; 中 &lt;code&gt;Object&lt;/code&gt; 相关的知识点。&lt;/p&gt;
&lt;h2&gt;JavaScript 中的对象&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ECMA-262&lt;/code&gt; 把对象定义为:“无序属性的集合，其属性可以包含基本值、对象或者函数。”严格来讲，这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字，而每个名字都映射到一个值。我们可以把 &lt;code&gt;ECMAScript&lt;/code&gt; 的对象想象成散列表: 无非就是一组名值对，其中值可以是数据或函数。&lt;/p&gt;
&lt;p&gt;我们创建对象有三种方法：&lt;code&gt;new Object()&lt;/code&gt;，&lt;code&gt;Object.create()&lt;/code&gt; 和字面量标记 &lt;code&gt;{}&lt;/code&gt;。关于三者的区别，参考我的文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/10/object-create-null/&quot; title=&quot;Object.create(null) 和 {…}&quot;&gt;Object.create(null) 和 {…}&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下面介绍一些 &lt;code&gt;Object&lt;/code&gt; 相关的知识点和 &lt;code&gt;API&lt;/code&gt;，包括 &lt;code&gt;ES5&lt;/code&gt; 和 &lt;code&gt;ES6&lt;/code&gt; 之后的。&lt;/p&gt;
&lt;h2&gt;属性类型&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中属性有两种，数据属性 &lt;code&gt;data property&lt;/code&gt; 和访问器属性 &lt;code&gt;accessor property&lt;/code&gt;。关于两种属性类型我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/09/javascript-object-prop-assign/#i-4&quot; title=&quot;JavaScript对象属性类型和赋值细节&quot;&gt;JavaScript对象属性类型和赋值细节&lt;/a&gt; 中已经详细介绍，想要了解可以去看一下。要对属性特性进行定义和修改，必须使用 &lt;code&gt;ES6&lt;/code&gt; 给出的方法：&lt;code&gt;Object.defineProperty&lt;/code&gt; 和 &lt;code&gt;Object.defineProperties()&lt;/code&gt;，要读取属性特性则可以使用 &lt;code&gt;Object.getOwnProperty&lt;/code&gt; 或者 &lt;code&gt;Object.getOwnProperties()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当我们尝试访问一个对象的属性时，会调用其 &lt;code&gt;[[get]]&lt;/code&gt;，而当我们尝试修改一个对象的属性时，会调用其 &lt;code&gt;[[set]]&lt;/code&gt;。对于数据属性而言，他们都是使用内置的 &lt;code&gt;[[set]]&lt;/code&gt; 和 &lt;code&gt;[[get]]&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;合并对象&lt;/h2&gt;
&lt;p&gt;有时候我们需要将多个对象进行合并，这一般是为了实现多继承。很多时候这种操作也被称为混入 &lt;code&gt;mixin&lt;/code&gt;，我们通过将源对象的属性 &lt;code&gt;mixin&lt;/code&gt; 到目标对象上而实现对目标对象的增强。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 为我们提供了一个 &lt;code&gt;Object.assign()&lt;/code&gt; 的方法让我们合并对象的属性，它会将一个或多个源对象的可枚举属性复制到目标对象，键名为 &lt;code&gt;String&lt;/code&gt; 类型或 &lt;code&gt;Symbol&lt;/code&gt; 类型的属性都会被拷贝，返回合并后的对象（注意，目标对象也会改变）。如果目标对象中的属性具有相同的键，则属性将被源对象中的属性覆盖。该方法拷贝时无法拷贝属性的特性，而且访问器属性会被转换成数据属性（返回值为访问器属性的 &lt;code&gt;getter&lt;/code&gt; 的返回值，如果访问器属性没有设置 &lt;code&gt;getter&lt;/code&gt;，那么值为 &lt;code&gt;undefined&lt;/code&gt;）。&lt;code&gt;Object.assign&lt;/code&gt; 不会在那些 &lt;code&gt;source&lt;/code&gt; 对象值为 &lt;code&gt;null&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt; 的时候抛出错误。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object.assign()&lt;/code&gt; 可以用于浅拷贝，参考我的文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/09/javascript-shallow-deep-copy/#Objectassingn&quot; title=&quot;JavaScript浅拷贝和深拷贝&quot;&gt;JavaScript浅拷贝和深拷贝&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;实现一个 &lt;code&gt;Object.assign()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (typeof Object.assign !== &apos;function&apos;) {
  // Must be writable: true, enumerable: false, configurable: true
  Object.defineProperty(Object, &apos;assign&apos;, {
    value: function assign(target) {
      // .length of function is 2
      &apos;use strict&apos;
      if (target === null || target === undefined) {
        throw new TypeError(&apos;Cannot convert undefined or null to object&apos;)
      }

      var to = Object(target)

      for (var index = 1; index &amp;#x3C; arguments.length; index++) {
        var nextSource = arguments[index]

        if (nextSource !== null &amp;#x26;&amp;#x26; nextSource !== undefined) {
          for (var nextKey in nextSource) {
            // Avoid bugs when hasOwnProperty is shadowed
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey]
            }
          }
        }
      }
      return to
    },
    writable: true,
    configurable: true
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Object.assign()&lt;/code&gt; 常用来为对象添加属性方法，克隆对象，合并多个对象，为对象的属性指定默认值（&lt;code&gt;source&lt;/code&gt; 会覆盖 &lt;code&gt;target&lt;/code&gt; 上的同名属性）&lt;/p&gt;
&lt;h2&gt;Object.create()&lt;/h2&gt;
&lt;p&gt;参考文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/10/object-create-null/&quot; title=&quot;Object.create(null) 和 {…}&quot;&gt;Object.create(null) 和 {…}&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;判断恒等&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;===&lt;/code&gt; 恒等比较有两个问题，一个是 &lt;code&gt;+0&lt;/code&gt; 和 &lt;code&gt;-0&lt;/code&gt; 视为相等，而 &lt;code&gt;NaN === NaN&lt;/code&gt; 则返回 &lt;code&gt;false&lt;/code&gt;。&lt;code&gt;ES6&lt;/code&gt; 的 &lt;code&gt;Object&lt;/code&gt; 对象新增了一个静态方法就是 &lt;code&gt;Object.is()&lt;/code&gt;，用来判断两个值是否相等，你可以理解为它是处理了上面了个情况的恒等。&lt;/p&gt;
&lt;p&gt;实现一个 &lt;code&gt;Object.is()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (!Object.is) {
  Object.is = function (x, y) {
    if (x === y) {
      //如果 x !== 0 直接返回 `true`，如果 `x === 0`，判断 1 / x 和 `1 / y` 是否相等，1 / -0 返回 -Infinity，1 / 0 或者 1 / +0 都返回 Infinity
      return x !== 0 || 1 / x === 1 / y
    } else {
      // Step 6.a: NaN == NaN
      return x !== x &amp;#x26;&amp;#x26; y !== y //利用 NaN === NaN 返回 false，判断x和y是不是都为 NaN
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;属性方法简写&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 提供了属性值的简写，我们在给对象创建属性的时候，经常会设置变量名为属性为，变量值为属性值。为了简化这种情况 &lt;code&gt;ES6&lt;/code&gt; 提供了一种简写属性的方式。属性的赋值器(&lt;code&gt;setter&lt;/code&gt;)和取值器(&lt;code&gt;getter&lt;/code&gt;)，事实上也是采用这种写法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var foo = &apos;bar&apos;
var baz = { foo }
baz // {foo: &quot;bar&quot;}
// 等同于
var baz = { foo: foo }

//方法也可以简写
var o = {
  method() {
    return &apos;Hello!&apos;
  }
}
// 等同于
var o = {
  method: function () {
    return &apos;Hello!&apos;
  }
}

//Generator函数前要加 *
var obj = {
  *m() {
    yield &apos;hello world&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种简写的方式用在函数返回多个值，以及模块的输出都非常方便。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getPoint() {
  var x = 1
  var y = 10
  return { x, y }
}
console.log(getPoint()) // {x:1, y:10}

module.exports = { getItem, setItem, clear } // 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**注意：**简写的方法不能作为构造函数，会报错。大概是简写方法没有内部 &lt;code&gt;[[construct]]&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var obj = {
  method: function () {}
}
new obj.method() // 正确

var obj = {
  method() {}
}
new obj.method() // TypeError: obj.method is not a constructor
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;属性名表达式&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;ES6&lt;/code&gt; 之前，对象字面量的属性值只能是字符串，想要使用表达式作为变量的属性，只能使用 &lt;code&gt;[]&lt;/code&gt; 成员访问运算符。&lt;code&gt;ES6&lt;/code&gt; 允许字面量定义对象时，用表达式作为对象的属性名或方法名，即把表达式放在方括号内。属性名表达式与简洁表示法，不能同时使用，会报错（简洁表示法就是对象属性直接写一个变量，不用写键值对，表示键名就用这个变量的标识符）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let propKey = &apos;foo&apos;
let obj = {
  [propKey]: true,
  [&apos;a&apos; + &apos;bc&apos;]: 123
}

//方法名也可以
let obj = {
  [&apos;h&apos; + &apos;ello&apos;]() {
    return &apos;hi&apos;
  }
}
obj.hello() // hi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性名表达式和属性的简写不能同时使用，属性名表达式如果是一个对象，默认情况下会自动将对象转为字符串 &lt;code&gt;[object Object]&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const keyA = { a: 1 }
const keyB = { b: 2 }
const myObject = {
  [keyA]: &apos;valueA&apos;,
  [keyB]: &apos;valueB&apos;
}
myObject // Object {[object Object]: &quot;valueB&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;对象的解构赋值&lt;/h2&gt;
&lt;p&gt;解构赋值 &lt;code&gt;destructuring&lt;/code&gt; 参考我的文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/08/destructure-details/&quot; title=&quot;解构赋值的一些细节&quot;&gt;解构赋值的一些细节&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;对象的遍历&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 提供了很多遍历对象的方法，我们先来看看它们有什么不同，方便我们在对应的场景选择合适的遍历方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for ... in&lt;/code&gt;：遍历对象自身可其原型链上的可枚举属性，不包括 &lt;code&gt;Symbol&lt;/code&gt; 属性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.keys()&lt;/code&gt;：返回一个数组，包括对象自身的(不含原型链上的)所有可枚举属性 (不含 &lt;code&gt;Symbol&lt;/code&gt; 属性)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.getOwnPropertyNames()&lt;/code&gt;：返回一个数组，包含对象自身的所有属性(不含 &lt;code&gt;Symbol&lt;/code&gt; 属性，但是包括不可枚举属性)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.getOwnPropertySymbols()&lt;/code&gt;：返回一个数组，包含对象自身的所有 &lt;code&gt;Symbol&lt;/code&gt; 属性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.ownKeys()&lt;/code&gt;：返回一个数组，包含对象自身的所有属性，不管属性名是 &lt;code&gt;Symbol&lt;/code&gt; 或字符串，也不管是否可枚举。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.values()&lt;/code&gt;：返回一个给定对象自身的所有可枚举属性值的数组(不含 &lt;code&gt;Symbol&lt;/code&gt; 属性)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.entries()&lt;/code&gt;：返回一个给定对象自身可枚举属性的键值对数组（不含 &lt;code&gt;Symbol&lt;/code&gt; 属性）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;他们遍历属性的顺序都遵循以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先遍历所有属性名为数值的属性，按照数字排序。&lt;/li&gt;
&lt;li&gt;其次遍历所有属性名为字符串的属性，按照生成时间排序。&lt;/li&gt;
&lt;li&gt;最后遍历所有属性名为 &lt;code&gt;Symbol&lt;/code&gt; 值的属性，按照生成时间排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;深浅拷贝的实现&lt;/h2&gt;
&lt;p&gt;对象的深浅拷贝参考文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/09/09/javascript-shallow-deep-copy/#Objectassingn&quot; title=&quot;JavaScript浅拷贝和深拷贝&quot;&gt;JavaScript浅拷贝和深拷贝&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;Object.getOwnPropertyDescriptors() 用法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Object.getOwnPropertyDescriptors()&lt;/code&gt; 可以用来获取所有自身属性的的描述对象（包括 &lt;code&gt;Symbol&lt;/code&gt; 属性，不可枚举属性，不包括继承的属性）。这个属性有一些比较有用的用法这里列出来。&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;Object.assign()&lt;/code&gt; 方法不能复制访问器属性（访问器属性会被调用 &lt;code&gt;[[get]]&lt;/code&gt; 转为数据属性），所以 &lt;code&gt;Object.getOwnPropertyDescriptors()&lt;/code&gt; 可以配合 &lt;code&gt;Object.defineProperties()&lt;/code&gt; 成功拷贝访问器属性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const source = {
  set foo(value) {
    console.log(value)
  }
}
const target2 = {}
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source))
Object.getOwnPropertyDescriptor(target2, &apos;foo&apos;)
// { get: undefined,
//   set: [Function: foo],
//   enumerable: true,
//   configurable: true }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Object.getOwnPropertyDescriptors()&lt;/code&gt; 的另一个用法就是方法的另一个用处，是配合 &lt;code&gt;Object.create()&lt;/code&gt; 方法，将对象属性克隆到一个新对象，属于浅拷贝。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
// 或者
const shallowClone = (obj) =&gt;
  Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;获取和设置对象原型&lt;/h2&gt;
&lt;p&gt;我们都知道可以通过 &lt;code&gt;__proto__&lt;/code&gt; 属性访问到对象的原型，该属性没有写入 &lt;code&gt;ES6&lt;/code&gt; 的正文，而是写入了附录，原因是 &lt;code&gt;__proto__&lt;/code&gt; 前后的双下划线，说明它本质上是一个内部属性 &lt;code&gt;[[prototype]]&lt;/code&gt;，而不是一个正式的对外的 &lt;code&gt;API&lt;/code&gt;，只是由于浏览 器广泛支持，才被加入了 &lt;code&gt;ES6&lt;/code&gt;。标准明确规定，只有浏览器必须部署这个属性，其他运行环境不一定需要部署，而且新的代码最好认为这个属性是不存在的。因此， 无论从语义的角度，还是从兼容性的角度，都不要使用这个属性，而是使用下面 的 &lt;code&gt;Object.setPrototype()&lt;/code&gt; (写操作)、 &lt;code&gt;Object.getPrototypeOf()&lt;/code&gt; (读 操作)、&lt;code&gt;Object.create()&lt;/code&gt; (生成操作)代替。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object.setPrototypeOf(object, prototype)&lt;/code&gt; 用来设置对象的原型，返回被设置的对象。如果第一个参数不是对象，会自动转为对象。但是由于返回的还是第一个参数，所 以这个操作不会产生任何效果。&lt;code&gt;null&lt;/code&gt; 和 &lt;code&gt;undefined&lt;/code&gt; 无法转为对象，所以第一个参数是 &lt;code&gt;null&lt;/code&gt; 或者 &lt;code&gt;undefined&lt;/code&gt; 会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Object.setPrototypeOf(1, {}) === 1 // true
Object.setPrototypeOf(&apos;foo&apos;, {}) === &apos;foo&apos; // true
Object.setPrototypeOf(true, {}) === true // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Object.getPrototypeOf(obj)&lt;/code&gt; 用于读取一个对象的原型，如果参数不是对象，会被自动转为对象。同样 &lt;code&gt;null&lt;/code&gt; 和 &lt;code&gt;undefined&lt;/code&gt; 同样会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 等同于 Object.getPrototypeOf(new Number(1))
console.log(Object.getPrototypeOf(1)) // Number {[[PrimitiveValue]]: 0}
// 等同于 Object.getPrototypeOf(new String(&apos;foo&apos;))
Object.getPrototypeOf(&apos;foo&apos;) // String {length: 0, [[PrimitiveValue]]: &quot;&quot;}
// 等同于 Object.getPrototypeOf(new Boolean(true))
Object.getPrototypeOf(true) // Boolean {[[PrimitiveValue]]: false}

Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf(&apos;foo&apos;) === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;警告&lt;/strong&gt;: 由于现代 &lt;code&gt;JavaScript&lt;/code&gt; 引擎优化属性访问所带来的特性的关系，更改对象的 &lt;code&gt;[[Prototype]]&lt;/code&gt; 在各个浏览器和 &lt;code&gt;JavaScript&lt;/code&gt; 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的，这不仅仅限于 &lt;code&gt;obj.__proto__ = ...&lt;/code&gt; 语句上的时间花费，而且可能会延伸到任何代码，那些可以访问任何 &lt;code&gt;[[Prototype]]&lt;/code&gt; 已被更改的对象的代码。如果你关心性能，你应该避免设置一个对象的 &lt;code&gt;[[Prototype]]&lt;/code&gt;。相反，你应该使用 &lt;code&gt;Object.create()&lt;/code&gt; 来创建带有你想要的 &lt;code&gt;[[Prototype]]&lt;/code&gt; 的新对象。&lt;/p&gt;
&lt;h2&gt;Object.prototype.toString()&lt;/h2&gt;
&lt;p&gt;我在 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/30/data-type-indicate/#ObjectprototypetoStringcall&quot; title=&quot;JS数据类型和判断方法&quot;&gt;JS数据类型和判断方法&lt;/a&gt; 和 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/29/symbol/&quot; title=&quot;Symbol&quot;&gt;Symbol&lt;/a&gt; 中都介绍过&lt;code&gt;Object.prototype.toString()&lt;/code&gt; 方法。当我们调用 &lt;code&gt;Object.prototype.toString()&lt;/code&gt; 时，本质就是访问对象的 &lt;code&gt;Symbol.toStringTag&lt;/code&gt; 属性。&lt;/p&gt;
&lt;p&gt;每个对象都有一个 &lt;code&gt;toString()&lt;/code&gt; 方法，当该对象被表示为一个文本值时，或者一个对象以预期的字符串方式引用时自动调用。默认情况下，&lt;code&gt;toString()&lt;/code&gt; 方法被每个 &lt;code&gt;Object&lt;/code&gt; 对象继承。如果此方法在自定义对象中未被覆盖，&lt;code&gt;toString()&lt;/code&gt; 返回 &lt;code&gt;[object type]&lt;/code&gt;，其中 &lt;code&gt;type&lt;/code&gt; 是对象的类型。&lt;/p&gt;
&lt;p&gt;当然，许多内置对象并没有 &lt;code&gt;Symbol.toStringTag&lt;/code&gt; 属性，但是依然能够通过 &lt;code&gt;toString()&lt;/code&gt; 获得其类型，这是引擎的内置。当然要注意，虽然 &lt;code&gt;Object.prototype&lt;/code&gt; 在绝大多数对象的原型链上，不过内置对象大部分都重写了 &lt;code&gt;toString()&lt;/code&gt; 方法。所以需要使用 &lt;code&gt;call&lt;/code&gt; 或者 &lt;code&gt;apply&lt;/code&gt; 来调用。内置对象的 &lt;code&gt;toString()&lt;/code&gt; 结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let toString = Object.prototype.toString //=&gt;Object.prototype.toString

console.log(toString.call(10)) //=&gt;&quot;[object Number]&quot;
console.log(toString.call(NaN)) //=&gt;&quot;[object Number]&quot;
console.log(toString.call(&apos;xxx&apos;)) //=&gt;&quot;[object String]&quot;
console.log(toString.call(true)) //=&gt;&quot;[object Boolean]&quot;
console.log(toString.call(null)) //=&gt;&quot;[object Null]&quot;
console.log(toString.call(undefined)) //=&gt;&quot;[object Undefined]&quot;
console.log(toString.call(Symbol())) //=&gt;&quot;[object Symbol]&quot;
console.log(toString.call(BigInt(10))) //=&gt;&quot;[object BigInt]&quot;
console.log(toString.call({ xxx: &apos;xxx&apos; })) //=&gt;&quot;[object Object]&quot;
console.log(toString.call([10, 20])) //=&gt;&quot;[object Array]&quot;
console.log(toString.call(/^\d+$/)) //=&gt;&quot;[object RegExp]&quot;
console.log(toString.call(function () {})) //=&gt;&quot;[object Function]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外一些对象类型则不然，&lt;code&gt;toString()&lt;/code&gt; 方法能识别它们是因为引擎为它们设置好了 &lt;code&gt;toStringTag&lt;/code&gt; 标签，我们能够通过 &lt;code&gt;Symbol.toStringTag&lt;/code&gt; 来访问到对应的值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Object.prototype.toString.call(new Map()) // &quot;[object Map]&quot;
Object.prototype.toString.call(function* () {}) // &quot;[object GeneratorFunction]&quot;
Object.prototype.toString.call(Promise.resolve()) // &quot;[object Promise]&quot;
// ... and more
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是我们创建的对象如果想要有一个自定义的 &lt;code&gt;toString()&lt;/code&gt; 效果，则需要自己手动设置对象的 &lt;code&gt;Symbol.toStringTag&lt;/code&gt; 属性。&lt;/p&gt;
&lt;p&gt;这里顺带说一下内置对象自己的 &lt;code&gt;toString&lt;/code&gt; 方法的返回值，所有的返回值类型都是 &lt;code&gt;String&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log((10).toString()) //10
console.log(&apos;clloz&apos;.toString()) //clloz
console.log(true.toString()) //true
console.log([1, 2, 3].toString()) //1,2,3
console.log(function a() {}.toString()) //function a() {}
console.log(/abc/.toString()) // /abc/
console.log(new Date().toString()) //Thu Nov 05 2020 12:59:46 GMT+0800 (China Standard Time)
console.log(Symbol(&apos;a&apos;).toString()) //Symbol(a)
console.log(BigInt(10).toString()) //10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Object.prototype.valueOf()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;valueOf&lt;/code&gt; 就是返回对象的原始值，这个方法是在 &lt;code&gt;Object.prototype&lt;/code&gt; 上。&lt;code&gt;JavaScript&lt;/code&gt; 调用 &lt;code&gt;valueOf&lt;/code&gt; 方法将对象转换为原始值。你很少需要自己调用 &lt;code&gt;valueOf&lt;/code&gt; 方法；当遇到要预期的原始值的对象时，&lt;code&gt;JavaScript&lt;/code&gt; 会自动调用它。&lt;/p&gt;
&lt;p&gt;默认情况下，&lt;code&gt;valueOf&lt;/code&gt; 方法由 &lt;code&gt;Object&lt;/code&gt; 后面的每个对象继承。 每个内置的核心对象都会覆盖此方法以返回适当的值。如果对象没有原始值，则 &lt;code&gt;valueOf&lt;/code&gt; 将返回对象本身。&lt;code&gt;JavaScript&lt;/code&gt; 的许多内置对象都重写了该函数，以实现更适合自身的功能需要。因此，不同类型对象的 &lt;code&gt;valueOf()&lt;/code&gt; 方法的返回值和返回值类型均可能不同。不同类型对象的 &lt;code&gt;valueOf()&lt;/code&gt; 方法的返回值如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Array&lt;/code&gt;：返回数组对象本身。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Boolean&lt;/code&gt;：返回布尔值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Date&lt;/code&gt;：存储的时间从 &lt;code&gt;1970 年 1 月 1 日&lt;/code&gt; 午夜开始计的毫秒数 &lt;code&gt;UTC&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Function&lt;/code&gt;：函数本身。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Number&lt;/code&gt;：数字值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object&lt;/code&gt;：对象本身。这是默认情况。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt;：字符串值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Math&lt;/code&gt; 和 &lt;code&gt;Error&lt;/code&gt; 对象没有 &lt;code&gt;valueOf&lt;/code&gt; 方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以在自己的代码中使用 &lt;code&gt;valueOf&lt;/code&gt; 将内置对象转换为原始值。 创建自定义对象时，可以覆盖 &lt;code&gt;Object.prototype.valueOf()&lt;/code&gt; 来调用自定义方法，而不是默认 &lt;code&gt;Object&lt;/code&gt; 方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;toString()&lt;/code&gt; 和 &lt;code&gt;valueOf()&lt;/code&gt; 会在类型转换时使用，参考 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/13/type-conversion/#valueOf_toString&quot; title=&quot;深入 JavaScript 类型转换&quot;&gt;深入 JavaScript 类型转换&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;对象的冻结、封闭&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Object&lt;/code&gt; 构造函数提供了三组静态方法，设置对象的可扩展性和可配置性。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object.preventExtensions()&lt;/code&gt;：方法让一个对象变的不可扩展，也就是永远不能再添加新的属性。一般来说，不可扩展对象的属性可能仍然可被删除。尝试将新属性添加到不可扩展对象将静默失败或抛出 &lt;code&gt;TypeError&lt;/code&gt;（最常见的情况是 &lt;code&gt;strict mode&lt;/code&gt;中，但不排除其他情况）。&lt;code&gt;Object.preventExtensions()&lt;/code&gt; 仅阻止添加自身的属性。但其对象类型的原型依然可以添加新的属性。&lt;/p&gt;
&lt;p&gt;数组作为一个对象，如果变成不可扩展就不能添加新的元素。并且如果你删除了一个元素，将不能再添加回去，数组长度变短是不可逆的，只能删不能加，元素值可以修改。如果属性是一个对象，该嵌套对象不受影响。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let arr = [1, 2, 3]

Object.preventExtensions(arr)
arr[3] = 10
console.log(arr) //[ 1, 2, 3 ]
arr.push(10)
console.log(arr) //TypeError: Cannot add property 3, object is not extensible
arr.pop()
console.log(arr) //[1, 2]
arr.push(10)
console.log(arr) //TypeError: Cannot add property 2, object is not extensible
arr[0] = 100
console.log(arr) //[100, 2]

let obj = {
  a: {
    name: &apos;clloz&apos;
  }
}

Object.preventExtensions(obj)
obj.a.age = 28
console.log(obj.a) //{ name: &apos;clloz&apos;, age: 28 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方法使得目标对象的 &lt;code&gt;[[prototype]]&lt;/code&gt; 不可变；任何重新赋值 &lt;code&gt;[[prototype]]&lt;/code&gt; 操作都会抛出 &lt;code&gt;TypeError&lt;/code&gt; 。这种行为只针对内部的 &lt;code&gt;[[prototype]]&lt;/code&gt; 属性， 目标对象的其它属性将保持可变。一旦将对象变为不可扩展的对象，就再也不能使其可扩展。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;ES5&lt;/code&gt; 中，如果参数不是一个对象类型（而是原始类型），将抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 异常。在 &lt;code&gt;ES2015&lt;/code&gt; 中，非对象参数将被视为一个不可扩展的普通对象，因此会被直接返回。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object.isExtensible()&lt;/code&gt; 方法判断一个对象是否是可扩展的（是否可以在它上面添加新的属性）。在 &lt;code&gt;ES5&lt;/code&gt; 中，如果参数不是一个对象类型，将抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 异常。在 &lt;code&gt;ES6&lt;/code&gt; 中， &lt;code&gt;non-object&lt;/code&gt; 参数将被视为一个不可扩展的普通对象，因此会返回 &lt;code&gt;false&lt;/code&gt; 。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;Object.seal()&lt;/code&gt; 方法封闭一个对象，阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。一旦将对象变为封闭，就再也不能变为非封闭状态。&lt;/p&gt;
&lt;p&gt;封闭一个对象会让这个对象变的不能添加新属性，且所有已有属性会变的不可配置。属性不可配置的效果就是属性变的不可删除，以及一个数据属性不能被重新定义成为访问器属性，或者反之，即不能修改属性描述符。但属性 &lt;code&gt;writeable&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 的值仍然可以修改。尝试删除一个密封对象的属性或者将某个密封对象的属性从数据属性转换成访问器属性，结果会静默失败或抛出 &lt;code&gt;TypeError&lt;/code&gt;（在严格模式中最常见的，但不唯一）。&lt;/p&gt;
&lt;p&gt;数组作为一个对象，如果变成封闭就不能添加新的元素并且元素不可配置，并且不能删除元素，元素值可以修改。如果属性是一个对象，该嵌套对象不受影响。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let arr = [1, 2, 3]

Object.seal(arr)

arr[0] = 100
console.log(arr) //[ 100, 2, 3 ]

arr.pop() //TypeError: Cannot delete property &apos;2&apos; of [object Array]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不会影响从原型链上继承的属性。该方法使得目标对象的 &lt;code&gt;[[prototype]]&lt;/code&gt; 不可变。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object.isSealed()&lt;/code&gt; 方法判断一个对象是否被密封。在 &lt;code&gt;ES5&lt;/code&gt; 中，如果这个方法的参数不是一个对象（一个原始类型），那么它会导致 &lt;code&gt;TypeError&lt;/code&gt;。在 &lt;code&gt;ES2015&lt;/code&gt; 中，非对象参数将被视为是一个密封的普通对象，只返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;Object.freeze()&lt;/code&gt; 方法可以冻结一个对象。一个被冻结的对象再也不能被修改；冻结了一个对象则不能向这个对象添加新的属性，不能删除已有属性，不能修改该对象已有属性的可枚举性、可配置性、可写性，以及不能修改已有属性的值。此外，冻结一个对象后该对象的原型也不能被修改。&lt;code&gt;freeze()&lt;/code&gt; 返回和传入的参数相同的对象。&lt;/p&gt;
&lt;p&gt;被冻结对象自身的所有属性都不可能以任何方式被修改。任何修改尝试都会失败，无论是静默地还是通过抛出 &lt;code&gt;TypeError&lt;/code&gt; 异常（最常见但不仅限于 &lt;code&gt;strict mode&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;数据属性的值不可更改，访问器属性（有 &lt;code&gt;getter&lt;/code&gt; 和 &lt;code&gt;setter&lt;/code&gt;）也同样（但由于是函数调用，给人的错觉是还是可以修改这个属性）。如果一个属性的值是个对象，则这个对象中的属性是可以修改的，除非它也是个冻结对象。数组作为一种对象，被冻结，其元素不能被修改，也不能添加或者删除元素。这个方法返回传递的对象，而不是创建一个被冻结的副本。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;ES5&lt;/code&gt; 中，如果这个方法的参数不是一个对象（一个原始值），那么它会导致 &lt;code&gt;TypeError&lt;/code&gt;。在 &lt;code&gt;ES2015&lt;/code&gt; 中，非对象参数将被视为要被冻结的普通对象，并被简单地返回。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object.isFrozen()&lt;/code&gt; 方法判断一个对象是否被冻结。在 &lt;code&gt;ES5&lt;/code&gt; 中，如果参数不是一个对象类型，将抛出一个 &lt;code&gt;TypeError&lt;/code&gt; 异常。在 &lt;code&gt;ES2015&lt;/code&gt; 中，非对象参数将被视为一个冻结的普通对象，因此会返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们可以看到从 &lt;code&gt;preventExtensions()&lt;/code&gt; 到 &lt;code&gt;seal()&lt;/code&gt; 再到 &lt;code&gt;freeze()&lt;/code&gt;，对对象属性的限制越来越多，从不能扩展，到不可配置不能删除，在到不能修改。我们可以再需要的时候使用对应的功能（&lt;code&gt;vue&lt;/code&gt; 中就使用到了 &lt;code&gt;freeze()&lt;/code&gt;）&lt;/p&gt;
&lt;h2&gt;Object.fromEntries()&lt;/h2&gt;
&lt;p&gt;该方法把键值对列表转换为一个对象。&lt;code&gt;Object.fromEntries()&lt;/code&gt; 方法接收一个键值对的列表参数，并返回一个带有这些键值对的新对象。这个迭代参数应该是一个能够实现 &lt;code&gt;@@iterator&lt;/code&gt; 方法的的对象，返回一个迭代器对象。它生成一个具有两个元素的类数组的对象，第一个元素是将用作属性键的值，第二个元素是与该属性键关联的值。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object.fromEntries()&lt;/code&gt; 执行与 &lt;code&gt;Object.entries()&lt;/code&gt; 互逆的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//Map -&gt; Object
const map = new Map([
  [&apos;foo&apos;, &apos;bar&apos;],
  [&apos;baz&apos;, 42]
])
const obj = Object.fromEntries(map)
console.log(obj) // { foo: &quot;bar&quot;, baz: 42 }

//Array -&gt; Object
const arr = [
  [&apos;0&apos;, &apos;a&apos;],
  [&apos;1&apos;, &apos;b&apos;],
  [&apos;2&apos;, &apos;c&apos;]
]
const obj = Object.fromEntries(arr)
console.log(obj) // { 0: &quot;a&quot;, 1: &quot;b&quot;, 2: &quot;c&quot; }

//对象转换
const object1 = { a: 1, b: 2, c: 3 }

const object2 = Object.fromEntries(Object.entries(object1).map(([key, val]) =&gt; [key, val * 2]))

console.log(object2)
// { a: 2, b: 4, c: 6 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;in 和 delete&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;delete&lt;/code&gt; 操作符用于删除对象的某个属性；如果没有指向这个属性的引用，那它最终会被释放，即不会起任何作用，并返回 &lt;code&gt;true&lt;/code&gt;。对于所有情况都是 &lt;code&gt;true&lt;/code&gt;，除非属性是一个自身的不可配置的属性，在这种情况下，非严格模式返回 &lt;code&gt;false&lt;/code&gt;。在严格模式下，会抛出 &lt;code&gt;TypeError&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;几个注意点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果你试图删除的属性不存在，那么 &lt;code&gt;delete&lt;/code&gt; 将不会起任何作用，但仍会返回 &lt;code&gt;true&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果对象的原型链上有一个与待删除属性同名的属性，那么删除属性之后，对象会使用原型链上的那个属性（也就是说， &lt;code&gt;delete&lt;/code&gt; 操作只会在自身的属性上起作用）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任何使用 &lt;code&gt;var&lt;/code&gt; 声明的属性不能从全局作用域或函数的作用域中删除。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这样的话，&lt;code&gt;delete&lt;/code&gt; 操作不能删除任何在全局作用域中的函数（无论这个函数是来自于函数声明或函数表达式）&lt;/li&gt;
&lt;li&gt;除了在全局作用域中的函数不能被删除，在对象(&lt;code&gt;object&lt;/code&gt;)中的函数是能够用 &lt;code&gt;delete&lt;/code&gt; 操作删除的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任何用 &lt;code&gt;let&lt;/code&gt; 或 &lt;code&gt;const&lt;/code&gt; 声明的属性不能够从它被声明的作用域中删除。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不可设置的(&lt;code&gt;Non-configurable&lt;/code&gt;)属性不能被移除。这意味着像 &lt;code&gt;Math, Array, Object&lt;/code&gt; 内置对象的属性以及使用 &lt;code&gt;Object.defineProperty()&lt;/code&gt; 方法设置为不可设置的属性不能被删除。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;封闭和冻结的属性不能被删除。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对变量使用 &lt;code&gt;delete&lt;/code&gt; 在非严格模式下返回 &lt;code&gt;false&lt;/code&gt;，严格模式下报错。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除数组元素不会改变数组长度，该数组会变成一个稀疏数组，被删除的元素位置变为 &lt;code&gt;empty&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;如果指定的属性在指定的对象或其原型链中，则 &lt;code&gt;in&lt;/code&gt; 运算符返回 &lt;code&gt;true&lt;/code&gt;。&lt;code&gt;in&lt;/code&gt; 右操作数必须是一个对象值。例如，你可以指定使用 &lt;code&gt;String&lt;/code&gt; 构造函数创建的字符串，但不能指定字符串文字。&lt;/p&gt;
&lt;p&gt;如果你使用 &lt;code&gt;delete&lt;/code&gt; 运算符删除了一个属性，则 &lt;code&gt;in&lt;/code&gt; 运算符对所删除属性返回 &lt;code&gt;false&lt;/code&gt;。如果你只是将一个属性的值赋值为 &lt;code&gt;undefined&lt;/code&gt;，而没有删除它，则 &lt;code&gt;in&lt;/code&gt; 运算仍然会返回true。&lt;/p&gt;
&lt;p&gt;如果一个属性是从原型链上继承来的，&lt;code&gt;in&lt;/code&gt; 运算符也会返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;其实我们平时遍历对象时使用的 &lt;code&gt;for ... in&lt;/code&gt; 也是 &lt;code&gt;in&lt;/code&gt; 操作符的一种使用方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Reflect&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 新增了一个新的内置对象 &lt;code&gt;Reflect&lt;/code&gt;，也就是新增了反射对象。主要是将一些原来放在 &lt;code&gt;Object&lt;/code&gt; 或 &lt;code&gt;Function&lt;/code&gt; 上的应该属于语言内部的方法集中起来，这样 &lt;code&gt;API&lt;/code&gt; 的设计更合理更清晰。主要有如下 &lt;code&gt;API&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Reflect.apply(target, thisArg, args)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.construct(target, args)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.get(target, name, receiver)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.set(target, name, value, receiver)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.defineProperty(target, name, desc)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.deleteProperty(target, name)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.has(target, name)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.ownKeys(target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.isExtensible(target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.preventExtensions(target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.getOwnPropertyDescriptor(target, name)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.getPrototypeOf(target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reflect.setPrototypeOf(target, prototype)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于 &lt;code&gt;Reflect&lt;/code&gt; 的详细内容看另一篇文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/19/javascript-reflect-proxy/#ReflectdeleteProperty&quot; title=&quot;代理和反射&quot;&gt;代理和反射&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;《Professional JavaScript for Web Developers, 4th Edition》&lt;/li&gt;
&lt;li&gt;MDN&lt;/li&gt;
&lt;li&gt;《ES6 标准入门》&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>call, apply 和 bing方法的应用</title><link>https://clloz.com/blog/apply-call-bind</link><guid isPermaLink="true">https://clloz.com/blog/apply-call-bind</guid><pubDate>Fri, 03 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中所有函数的构造函数为 &lt;code&gt;ƒ Function() { [native code] }&lt;/code&gt;，所有函数的 &lt;code&gt;[[prototype]]&lt;/code&gt; 默认指向 &lt;code&gt;Function.prototype&lt;/code&gt;，在 &lt;code&gt;Function.prototype&lt;/code&gt; 上有三个方法 &lt;code&gt;call&lt;/code&gt;，&lt;code&gt;apply&lt;/code&gt; 和 &lt;code&gt;bind&lt;/code&gt;，他们共同的作用就是为函数调用指定的执行上下文，也是就是 &lt;code&gt;this&lt;/code&gt; 的指向。本文来说一说这三个方法的区别和使用场景。&lt;/p&gt;
&lt;h2&gt;call 和 apply&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 基本上没有什么区别，不同的地方是他们所接受的参数不同。两者的第一个参数都是函数执行时使用的 &lt;code&gt;this&lt;/code&gt; 值，后面的参数就有所不同。&lt;code&gt;apply&lt;/code&gt; 接受一个数组或类数组对象（比如 &lt;code&gt;arguments&lt;/code&gt;），而 &lt;code&gt;call&lt;/code&gt; 接受一组参数列表。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//apply
func.apply(thisArg, [argsArray])

//call
function.call(thisArg, arg1, arg2, ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中参数的个数是有上限的，&lt;code&gt;JavaScriptCore&lt;/code&gt; 引擎中有被硬编码的 参数个数上限：&lt;code&gt;65536&lt;/code&gt;。但是实际能接受多少参数取决于当前的系统和浏览器，并不确定。比如我用下面的代码生成元素值为元素下标的数组，在 &lt;code&gt;safari&lt;/code&gt; 中的上限是 &lt;code&gt;65536&lt;/code&gt;，在 &lt;code&gt;chrome&lt;/code&gt; 中是 &lt;code&gt;125382&lt;/code&gt;。&lt;code&gt;let a = Object.keys(Array.apply(null, {length:125382}))&lt;/code&gt;。任何用到超大栈空间的行为都有可能出现这个现象，超出限制则会报错 &lt;code&gt;Uncaught RangeError: Maximum call stack size exceeded&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;请注意，&lt;code&gt;this&lt;/code&gt; 可能不是该方法看到的实际值：如果这个函数处于非严格模式下，则指定为 &lt;code&gt;null&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt; 时会自动替换为指向全局对象，原始值会被包装。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;h5&gt;类数组对象调用数组方法&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中有类数组对象，最常见的就是所有非箭头函数中都可以使用的局部变量 &lt;code&gt;arguments&lt;/code&gt;，还有 &lt;code&gt;DOM&lt;/code&gt; 操作返回的 &lt;code&gt;NodeList&lt;/code&gt; 集合，它类似于 &lt;code&gt;Array&lt;/code&gt;，但除了 &lt;code&gt;length&lt;/code&gt; 属性和索引元素之外没有任何 &lt;code&gt;Array&lt;/code&gt; 属性。例如，它没有 &lt;code&gt;pop&lt;/code&gt; 方法。但是它可以被转换为一个真正的 &lt;code&gt;Array&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var args = Array.prototype.slice.call(arguments)
var args = [].slice.call(arguments)

// ES2015
const args = Array.from(arguments)
const args = [...arguments] //扩展运算符
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上我们也可以自己定义类数组对象，只要有索引和 &lt;code&gt;length&lt;/code&gt; 即可，&lt;code&gt;{&apos;length&apos;: 2, &apos;0&apos;: &apos;eat&apos;, &apos;1&apos;: &apos;bananas&apos;}&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;数组拼接&lt;/h5&gt;
&lt;p&gt;我们可以用 &lt;code&gt;push&lt;/code&gt; 方法为数组添加新的元素，虽然 &lt;code&gt;push&lt;/code&gt; 方法接受可变参数，但是如果我们以数组作为参数的话，它只是把这个数组所谓一个元素添加，如果想把两个数组进行拼接，那么可以用 &lt;code&gt;concat&lt;/code&gt;，但是 &lt;code&gt;concat&lt;/code&gt; 是返回一个新的数组，如果只是想要将两个数组拼接，可以用 &lt;code&gt;apply&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var array = [&apos;a&apos;, &apos;b&apos;]
var elements = [0, 1, 2]
array.push.apply(array, elements)
console.info(array) // [&quot;a&quot;, &quot;b&quot;, 0, 1, 2]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;用数组替代参数列表&lt;/h5&gt;
&lt;p&gt;其实从上面的例子就可以看出，对于一些可能需要很长参数列表的函数，&lt;code&gt;apply&lt;/code&gt; 都可以让我们用数组来代替参数列表，比如 &lt;code&gt;Math.max&lt;/code&gt; 和 &lt;code&gt;Math.min&lt;/code&gt; 等。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;/* 找出数组中最大/小的数字 */
var numbers = [5, 6, 2, 3, 7]

/* 应用(apply) Math.min/Math.max 内置函数完成 */
var max = Math.max.apply(
  null,
  numbers
) /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这样使用的风险就是如果数组非常大，在函数调用的时候可能参数个数会超出引擎的限制（JavaScript 核心中已经做了硬编码 参数个数限制在 &lt;code&gt;65536&lt;/code&gt;，具体数值由引擎决定），如果遇到这种情况可以把数组切块循环执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;继承&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Animal(name) {
  this.name = name
  this.showName = function () {
    console.log(this.name)
  }
}

function Cat(name) {
  Animal.call(this, name)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;bind&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;bind&lt;/code&gt; 和前面两者不一样的是他并不是立即调用函数，而是返回一个新的函数。&lt;code&gt;bind()&lt;/code&gt; 方法创建一个新的函数，在 &lt;code&gt;bind()&lt;/code&gt; 被调用时，这个新函数的 &lt;code&gt;this&lt;/code&gt; 被指定为 &lt;code&gt;bind()&lt;/code&gt; 的第一个参数，而其余参数将作为新函数的参数，供调用时使用。&lt;code&gt;bind&lt;/code&gt; 方法返回一个原函数的拷贝，并拥有指定的 &lt;code&gt;this&lt;/code&gt; 值和初始参数。还有一点是 如果 &lt;code&gt;bind&lt;/code&gt; 函数的参数列表为空，或者第一个参数是 &lt;code&gt;null&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt;，执行作用域的 &lt;code&gt;this&lt;/code&gt; 将被视为新函数的 &lt;code&gt;thisArg&lt;/code&gt;。&lt;code&gt;apply&lt;/code&gt; 和 &lt;code&gt;call&lt;/code&gt; 则是如果这个函数处于非严格模式下，则指定为 &lt;code&gt;null&lt;/code&gt; 或 &lt;code&gt;undefined&lt;/code&gt; 时会自动替换为指向全局对象，原始值会被包装。&lt;/p&gt;
&lt;h2&gt;bind 的 new 调用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;bind&lt;/code&gt; 的另一个不同于 &lt;code&gt;apply，bind&lt;/code&gt; 的特点就是 &lt;code&gt;bind&lt;/code&gt; 创建一个指定了 &lt;code&gt;this&lt;/code&gt; 的绑定函数，但是这个函数支持 &lt;code&gt;new&lt;/code&gt; 调用。当绑定函数进行这种 &lt;code&gt;new&lt;/code&gt; 形式的构造函数调用的时候，绑定的 &lt;code&gt;this&lt;/code&gt; 将不再生效，而是用 &lt;code&gt;new&lt;/code&gt; 创建的新对象作为 &lt;code&gt;this&lt;/code&gt;，但是绑定的参数依然可用。可以看一下例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Bound(a, b) {
  console.log(this.a, this.b)
  this.a = a
  this.b = b
  console.log(this.a, this.b)
}

let obj = { a: 10, b: 20 }

let bFun = Bound.bind(obj, 1, 2)
bFun() //10 20   1 2

let bObj = new bFun(3, 4) //undefined,undefined    1 2
console.log(bObj instanceof Bound) //true
console.log(bObj instanceof bFun) //true
console.log(bObj.__proto__ === Bound.prototype) //true
console.log(bFun.prototype) //undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到当我们直接调用绑定函数，我们的绑定的 &lt;code&gt;this&lt;/code&gt; 也就是 &lt;code&gt;obj&lt;/code&gt; 生效了，传入的参数也生效了。当我们用 &lt;code&gt;new&lt;/code&gt; 调用绑定函数，绑定的 &lt;code&gt;this&lt;/code&gt; 并没有生效（第一个 &lt;code&gt;console.log(this.a, this.b);&lt;/code&gt; 输出 &lt;code&gt;undefined&lt;/code&gt;），但是传入的参数生效了（&lt;code&gt;Bound&lt;/code&gt; 只接受两个参数，最后生效的是 &lt;code&gt;bind&lt;/code&gt; 时传入的参数，而不是 &lt;code&gt;new&lt;/code&gt; 的时候传入的参数）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bind&lt;/code&gt; 的 &lt;code&gt;new&lt;/code&gt; 比较奇怪的地方就是生成的绑定函数的 &lt;code&gt;prototype&lt;/code&gt; 是 &lt;code&gt;undefined&lt;/code&gt;，但是生成对象 &lt;code&gt;bObj&lt;/code&gt; 进行 &lt;code&gt;bObj instanceof bFun&lt;/code&gt; 依然返回 &lt;code&gt;true&lt;/code&gt;，而且生成对象 &lt;code&gt;bObj&lt;/code&gt; 的 &lt;code&gt;[[prototype]]&lt;/code&gt; 指向的是原函数 &lt;code&gt;Bound&lt;/code&gt; 的 &lt;code&gt;prototype&lt;/code&gt;。这里我没有找到具体的原因，在模拟实现 &lt;code&gt;bind&lt;/code&gt; 的时候也无法达到这个效果。&lt;/p&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;h5&gt;创建绑定函数&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;this.x = 9 // 在浏览器中，this 指向全局的 &quot;window&quot; 对象
var module = {
  x: 81,
  getX: function () {
    return this.x
  }
}

module.getX() // 81

var retrieveX = module.getX
retrieveX()
// 返回 9 - 因为函数是在全局作用域中调用的

// 创建一个新函数，把 &apos;this&apos; 绑定到 module 对象
// 新手可能会将全局变量 x 与 module 的属性 x 混淆
var boundGetX = retrieveX.bind(module)
boundGetX() // 81
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;预设函数初始参数&lt;/h5&gt;
&lt;p&gt;这种用法可以为函数设置一些初始参数，有点类似函数柯里化。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function list() {
  return Array.prototype.slice.call(arguments)
}

function addArguments(arg1, arg2) {
  return arg1 + arg2
}

var list1 = list(1, 2, 3) // [1, 2, 3]

var result1 = addArguments(1, 2) // 3

// 创建一个函数，它拥有预设参数列表。
var leadingThirtysevenList = list.bind(null, 37)

// 创建一个函数，它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37)

var list2 = leadingThirtysevenList()
// [37]

var list3 = leadingThirtysevenList(1, 2, 3)
// [37, 1, 2, 3]

var result2 = addThirtySeven(5)
// 37 + 5 = 42

var result3 = addThirtySeven(5, 10)
// 37 + 5 = 42 ，第二个参数被忽略
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;快捷调用&lt;/h5&gt;
&lt;p&gt;这是一个 &lt;code&gt;mdn&lt;/code&gt; 上给出的示例。当我们需要频繁调用一个指定 &lt;code&gt;this&lt;/code&gt; 的函数，我们可以用 &lt;code&gt;bind&lt;/code&gt; 来实现快捷调用。举个例子子，我们相对类数组对象（比如 &lt;code&gt;arguments&lt;/code&gt;）执行数组方法（比如 &lt;code&gt;slice&lt;/code&gt;），我们一般是 &lt;code&gt;Array.prototype.slice.apply(arguments)&lt;/code&gt;，当我们需要频繁使用这个方法的时候，我们可能会这样 &lt;code&gt;let slice = Array.prototype.slice; slice.apply(arguments);&lt;/code&gt;。如果我们使用 &lt;code&gt;bind&lt;/code&gt;，我们可以直接 &lt;code&gt;slice(arguments)&lt;/code&gt; 这样调用，更方便，具体实现看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var slice = Array.prototype.slice

// ...

slice.apply(arguments)

// 与前一段代码的 &quot;slice&quot; 效果相同
var unboundSlice = Array.prototype.slice
var slice = Function.prototype.apply.bind(unboundSlice)

// ...

slice(arguments)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要理解这段代码我们需要理解 &lt;code&gt;apply&lt;/code&gt; 本身也是一个函数，它是在 &lt;code&gt;Function.prototype&lt;/code&gt; 上定义的一个函数，所有函数都能调用它。当我们用 &lt;code&gt;func.apply()&lt;/code&gt; 调用 &lt;code&gt;apply&lt;/code&gt; 的时候，本质就是以 &lt;code&gt;func&lt;/code&gt; 作为 &lt;code&gt;this&lt;/code&gt; 调用 &lt;code&gt;apply&lt;/code&gt;。那么第二种实现就是用 &lt;code&gt;Array.prototype.slice&lt;/code&gt; 作为 &lt;code&gt;this&lt;/code&gt; 创建 &lt;code&gt;apply&lt;/code&gt; 的一个绑定函数。当我们调用这个绑定函数的时候就相当于调用 &lt;code&gt;Array.prototype.slice.apply()&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;模拟实现 call，apply 和 bind&lt;/h2&gt;
&lt;p&gt;关于如何手写 &lt;code&gt;call， apply，bind&lt;/code&gt; 我在另一篇文章中写下了详细的过程，点击查看 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/07/simulation-of-call-apply-bind/&quot; title=&quot;模拟实现call，apply 和 bind&quot;&gt;模拟实现call，apply 和 bind&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;MDN&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>var，let，const和变量提升（hoist）</title><link>https://clloz.com/blog/variable-hoist</link><guid isPermaLink="true">https://clloz.com/blog/variable-hoist</guid><pubDate>Wed, 01 Jul 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;ES6&lt;/code&gt; 以前，&lt;code&gt;JavaScript&lt;/code&gt; 中是不存在块级作用域的，变量的作用域是靠执行环境来控制的，要么是在某个函数内要么是在全局作用于中。由于 &lt;code&gt;JS&lt;/code&gt; 中的变量提升 &lt;code&gt;Hoist&lt;/code&gt; 机制的存在，我们的定义的变量或者函数很可能发生命名冲突引发错误。所以在 &lt;code&gt;ES6&lt;/code&gt; 中引入了 &lt;code&gt;let&lt;/code&gt; 和 &lt;code&gt;const&lt;/code&gt; 来应对这个问题，本文就讨论一下三个命令之间的区别，以及 &lt;code&gt;JS&lt;/code&gt; 中的变量提升机制。&lt;/p&gt;
&lt;h2&gt;block 块语句&lt;/h2&gt;
&lt;p&gt;上面我们已经说过，在 &lt;code&gt;ES5&lt;/code&gt; 中，&lt;code&gt;JS&lt;/code&gt; 的作用域只有两种可能，要么在某个函数中，要么在全局作用域。对于像 &lt;code&gt;if&lt;/code&gt; 或者 &lt;code&gt;for&lt;/code&gt; 这样的语句，虽然他们也有大括号，但因为他们不是函数，所以在这些语句中定义的变量一样可以在外部访问的。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;ES6&lt;/code&gt; 中引入了块级作用域，用于组合零个或多个语句。块级作用域由一对大括号界定，可以添加 &lt;code&gt;label&lt;/code&gt;。块级作用域的出现主要是为了配合 &lt;code&gt;let&lt;/code&gt;，&lt;code&gt;const&lt;/code&gt; 和 &lt;code&gt;class&lt;/code&gt;。现在我们在块级作用域中的 &lt;code&gt;let&lt;/code&gt;，&lt;code&gt;const&lt;/code&gt; 和 &lt;code&gt;class&lt;/code&gt; 声明将不能在块级作用域之外访问。并且块级作用域可以任意嵌套，每一对大括号都是一个独立的块级作用域。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，块级作用域只对 &lt;code&gt;let&lt;/code&gt;，&lt;code&gt;const&lt;/code&gt; 和 &lt;code&gt;class&lt;/code&gt; 生效。&lt;code&gt;var&lt;/code&gt; 声明的变量是没有块级作用域的，无论严格模式还是非严格模式。函数声明的行为则比较特别，我在下面介绍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  let m = 10
  const n = 20
}
console.log(m) //ReferenceError: m is not defined
console.log(n) //ReferenceError: n is not defined

var x = 1
{
  var x = 2
}
console.log(x) // 输出 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有块级作用域的情况下，结合变量提升机制，经常会产生一些奇怪的现象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var arr = []
for (var i = 0; i &amp;#x3C; 5; i++) {
  arr[i] = function () {
    return i
  }
}
console.log(arr[0]()) //5
console.log(arr[1]()) //5
console.log(arr[2]()) //5
console.log(arr[3]()) //5
console.log(arr[4]()) //5

var tmp = new Date()
function f() {
  console.log(tmp) // 想打印外层的时间作用域
  if (false) {
    var tmp = &apos;hello world&apos; // 这里声明的作用域为整个函数
  }
}
f() // undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 引入了块级作用域，明确允许在块级作用域之中声明函数（&lt;code&gt;ES5&lt;/code&gt; 中是不允许函数声明在块级作用域中）。&lt;code&gt;ES6&lt;/code&gt; 规定，块级作用域之中，函数声明语句的行为类似于 &lt;code&gt;let&lt;/code&gt;，在块级作用域之外不可引用。但是实际上各个浏览器并没有遵循标准进行实现，因为如果按标准进行实现的话对老代码的影响会非常大。具体规定参考标准：&lt;a href=&quot;https://tc39.es/ecma262/#sec-block-level-function-declarations-web-legacy-compatibility-semantics&quot; title=&quot;https://tc39.es/ecma262/#sec-block-level-function-declarations-web-legacy-compatibility-semantics&quot;&gt;https://tc39.es/ecma262/#sec-block-level-function-declarations-web-legacy-compatibility-semantics&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对于支持 &lt;code&gt;ES6&lt;/code&gt; 的浏览器，块级作用域中的函数声明表现如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;允许在块级作用域内声明函数。&lt;/li&gt;
&lt;li&gt;在非严格模式中，函数声明类似于 &lt;code&gt;var&lt;/code&gt;，即会提升到全局作用域或函数作用域的头部。同时，函数声明还会提升到所在的块级作用域的头部。&lt;/li&gt;
&lt;li&gt;在严格模式中函数声明则只会提升到所在块级作用域的头部。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他环境的 &lt;code&gt;JavaScript&lt;/code&gt; 实现不用遵守这个规定，还是将块级作用域的函数声明当作 &lt;code&gt;let&lt;/code&gt; 处理。可以看下面两个例子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//非严格模式
// &apos;use strict&apos;;
function test() {
  console.log(a) //undefined
  {
    console.log(a) //[Function: a]
    function a() {
      console.log(&apos;a&apos;)
    }
  }
  a() //a
}
test()

//严格模式
;(&apos;use strict&apos;)
function test() {
  // console.log(a); //ReferenceError: a is not defined
  {
    console.log(a) //[Function: a]
    function a() {
      console.log(&apos;a&apos;)
    }
  }
}
test()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;考虑到环境导致的行为差异太大，应该避免在块级作用域内声明函数。如果确实需要，也应该写成函数表达式，而不是函数声明语句。&lt;/p&gt;
&lt;p&gt;有了块级作用域，原来一些为了防止全局环境被污染的立即执行函数 &lt;code&gt;IIFE&lt;/code&gt; 不在必要。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// IIFE 写法
(function () {
    var tmp = ...;
    ...
}());

// 块级作用域写法
{
    let tmp = ...;
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;补充：&lt;code&gt;block&lt;/code&gt; 块语句有一个用法就是我们可以在函数内部用花括号把函数分成一个个独立的小结，我们在小结内部可以放心地用 &lt;code&gt;let&lt;/code&gt; 和 &lt;code&gt;const&lt;/code&gt; 定义变量常量和方法。这也是现在框架发展的一个功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;块语句还可以使用 &lt;code&gt;label&lt;/code&gt; 标记，在块语句内进行 &lt;code&gt;break&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;test: {
  console.log(1)
  break test
  console.log(2) //这一句不会执行
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;var&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;var&lt;/code&gt; 语句会声明一个函数作用域或者全局作用域的变量，取决于声明所处的执行环境。当重复声明一个变量时，变量的值不会丢失。当赋值给未声明的变量, 则执行赋值后, 该变量会被隐式地创建为全局变量（它将成为全局对象的属性）。声明变量在任何代码执行前创建，而非声明变量只有在执行赋值操作的时候才会被创建。声明变量是它所在上下文环境的不可配置属性，非声明变量是可配置的（如非声明变量可以被删除）。在严格模式下，使用未声明变量时不合法的。&lt;/p&gt;
&lt;p&gt;变量的声明有多种形式，特别是声明多个变量的时候。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var a = 0,
  b = 0

var a = &apos;A&apos;
var b = a

// 等效于：
var a,
  b = (a = &apos;A&apos;)

var x = y,
  y = &apos;A&apos;
console.log(x + y) // undefinedA

var x = 0

function f() {
  var x = (y = 1) // x在函数内部声明，y不是！
}
f()

console.log(x, y) // 0, 1
// x 是全局变量。
// y 是隐式声明的全局变量。
//JS在执行语句之前会先检查是否有未声明的变量，如果有则将其声明提升到全局作用域
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，&lt;code&gt;var&lt;/code&gt; 语句中的逗号不是逗号操作符，因为它不是存在于一个表达式中。尽管从实际效果来看，那个逗号同逗号运算符的表现很相似。但确切地说，它是 &lt;code&gt;var&lt;/code&gt; 语句中的一个特殊符号，用于把多个变量声明结合成一个。&lt;/p&gt;
&lt;h2&gt;let&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;let&lt;/code&gt; 和 &lt;code&gt;var&lt;/code&gt; 的不同主要有以下几点。&lt;/p&gt;
&lt;h2&gt;let 只在代码块内有效&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  let a = 10
  var b = 1
}
a // ReferenceError: a is not defined. b // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面 &lt;code&gt;for&lt;/code&gt; 循环的问题也可以用 &lt;code&gt;let&lt;/code&gt; 解决&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var arr = []
for (let i = 0; i &amp;#x3C; 5; i++) {
  arr[i] = function () {
    return i
  }
}
console.log(arr[0]()) //0
console.log(arr[1]()) //1
console.log(arr[2]()) //2
console.log(arr[3]()) //3
console.log(arr[4]()) //4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，&lt;code&gt;for&lt;/code&gt; 语句的作用域是比较特殊的，小括号内是一个父级块作用域，而大括号内是一个子级块作用域。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;for (let i = 0; i &amp;#x3C; 3; i++) {
  let i = &apos;abc&apos;
  console.log(i)
}
// abc
// abc
// abc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;for&lt;/code&gt; 循环头部的 &lt;code&gt;let&lt;/code&gt; 不仅将 &lt;code&gt;i&lt;/code&gt; 绑定到了 &lt;code&gt;for&lt;/code&gt; 循环的块中，事实上它将其重新绑定到了循环的每一个迭代中，确保使用上一个循环迭代结束时的值重新进行赋值。真实的执行过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  let j
  for (j = 0; j &amp;#x3C; 10; j++) {
    let i = j // 每次迭代重新绑定
    console.log(i)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;没有变量提升&lt;/h2&gt;
&lt;p&gt;当使用 &lt;code&gt;var&lt;/code&gt; 语句声明的变量会发生变量提升，也就是进入执行环境的时候，引擎最先做的就是扫描所有的 &lt;code&gt;var&lt;/code&gt; 语句，把这些变量声明提到执行环境顶部并赋值为 &lt;code&gt;undefined&lt;/code&gt;。这样即使我们在变量声明之前使用变量也不会报错，因为引擎已经把变量提升到执行环境顶部，但是初始化依然要到执行到对应语句才会执行。&lt;/p&gt;
&lt;p&gt;由于这种行为是有点违反逻辑的，所以 &lt;code&gt;let&lt;/code&gt; 就修复了这个问题，我们必须在 &lt;code&gt;let&lt;/code&gt; 语句执行之后才能使用对应的变量，否则会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// var 的情况
console.log(foo) // 输出undefined var foo = 2;
// let 的情况
console.log(bar) // 报错ReferenceError let bar = 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;暂时性死区&lt;/h2&gt;
&lt;p&gt;只要在一个块级作用域能用 &lt;code&gt;let&lt;/code&gt; 语句声明了一个变量，那么在该块级作用域内，将只有一个该变量，外部的同名变量将无法被访问。可以理解为 &lt;code&gt;let&lt;/code&gt; 语句创建的变量与所在的语句块绑定了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var tmp = 123
if (true) {
  tmp = &apos;abc&apos; // Uncaught ReferenceError: Cannot access &apos;tmp&apos; before initialization
  let tmp
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个地方和上面的没有变量提升似乎是有冲突的，我们在块语句内的 &lt;code&gt;let&lt;/code&gt; 语句声明 &lt;code&gt;tmp&lt;/code&gt; 之前使用该变量，报错是 &lt;code&gt;无法在tmp初始化之前访问&lt;/code&gt;，说明在声明语句之前就引擎就已经知道这个变量的存在了。我的理解是还是存在某种形式的变量提升，只不过这种提升并没有像 &lt;code&gt;var&lt;/code&gt; 那样给变量一个个初始的 &lt;code&gt;undefined&lt;/code&gt;，并且变量的使用在初始化之前是被拒绝的。也就是所谓的暂时性死区 &lt;code&gt;temporal dead zone，简称 TDZ&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;暂时性死区机制也意味着 &lt;code&gt;typeof&lt;/code&gt; 不再是一个百分之百安全的操作，在 &lt;code&gt;let&lt;/code&gt; 语句前对变量进行 &lt;code&gt;typeof&lt;/code&gt; 操作一样会报错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用 &lt;code&gt;let&lt;/code&gt; 声明变量时，只要变量在还没有 声明完成前使用，就会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var x = x //undefined

let x = x //Uncaught SyntaxError: Identifier &apos;x&apos; has already been declared
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外还有一点就是函数参数似乎也有和 &lt;code&gt;let&lt;/code&gt; 的相似的行为，我们不能再函数内部用 &lt;code&gt;let&lt;/code&gt; 或 &lt;code&gt;const&lt;/code&gt;给参数重新赋值，也不能像 &lt;code&gt;x = x&lt;/code&gt; 这样给函数初始值。但是与 &lt;code&gt;let&lt;/code&gt; 不同的是，用 &lt;code&gt;var&lt;/code&gt; 可以给参数赋值。具体的过程可能需要查看标准。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function foo(x = 5) {
  var x = 1
  console.log(x) //1
}

function foo(x = 5) {
  let x = 1 // Uncaught SyntaxError: Identifier &apos;x&apos; has already been declared
}

function a(x = x) {} //Uncaught ReferenceError: Cannot access &apos;x&apos; before initialization
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不允许重复声明&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;let&lt;/code&gt; 不可以在同一个块级作用域内重复声明。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 报错
function () {
    let a = 10;
    var a = 1;
}

// 报错
function () {
    let a = 10;
    let a = 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;全局环境下用 &lt;code&gt;let&lt;/code&gt; 或 &lt;code&gt;const&lt;/code&gt; 声明的变量不会作为属性挂载在全局对象上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;const&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt; 的大部分行为和 &lt;code&gt;let&lt;/code&gt; 是保持一致的，不同的地方时，&lt;code&gt;const&lt;/code&gt; 声明的是一个只读的常量，声明的时候必须进行初始化，且不能更改。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const PI = 3.1415
PI = 3 //Uncaught TypeError: Assignment to constant variable.

const foo // SyntaxError: Missing initializer in const declaration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但其实 &lt;code&gt;const&lt;/code&gt; 并不是绝对安全的，因为当 &lt;code&gt;const&lt;/code&gt; 声明的变量保存的是一个引用类型的时候，他保存的只是一个指向引用类型的指针，他能保证的是这个指针不变，但如果指针指向的的引用类型的内容发生变化，它是无法控制的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const foo = {}
// 为 foo 添加一个属性，可以成功
foo.prop = 123
foo.prop // 123
// 将 foo 指向另一个对象，就会报错
foo = {} // Uncaught SyntaxError: Identifier &apos;foo&apos; has already been declared
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;class&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt; 关键字也是有块作用域的，即使在非严格模式下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  class A {}
  let a = new A()
  console.log(a) //A {}
}
let a = new A() //ReferenceError: A is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;变量提升&lt;/h2&gt;
&lt;p&gt;从概念的字面意义上说，“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面，但这么说并不准确。变量和函数声明在代码结构里的位置是不会动的，而是在编译阶段被放入内存中。实际上变量提升行为是 &lt;code&gt;JavaScript&lt;/code&gt; 预编译机制中的一种行为，搞懂预编译过程中发生了什么，我们自然就知道变量提升是如何进行的了。&lt;/p&gt;
&lt;p&gt;引擎在接收到 &lt;code&gt;JavaScript&lt;/code&gt; 文件到执行中间大概分为三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;词法分析&lt;/li&gt;
&lt;li&gt;预编译&lt;/li&gt;
&lt;li&gt;解释执行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里我们主要说一下第二步 &lt;code&gt;预编译&lt;/code&gt;，全局环境的预编译和函数执行环境的预编译的略有不同的。全局预编译发生在页面加载完成时执行，而局部预编译发生在函数执行的前一刻。预编译阶段发生变量声明和函数声明的提升行为，但没有初始化行为（赋值），匿名函数不参与预编译，未声明的变量也不会参与提升（虽然未声明变量也是作为全局变量，但是未声明变量在预编译阶段是不会处理的，只有到解释执行阶段才会进行处理，严格模式下不可以使用未声明变量） 。只有在解释执行阶段才会进行变量初始化 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//未声明变量只有在解释执行阶段才会处理，没有提升行为
console.log(b)
var a = (b = 110) //Uncaught ReferenceError: b is not defined

console.log(b)
b = 110 //Uncaught ReferenceError: b is not defined

b = 110
console.log(b) //110

//严格模式下不可以使用未声明变量
;(&apos;use strict&apos;)
m = 10
console.log(window.m) //Uncaught ReferenceError: m is not defined

//未声明的对象属性也会在解释执行之前提前创建
var a = { n: 1 }
var b = a
a.x = a = { n: 2 } //a.x 在执行完 a = {n:2} 表达式之后依然能正确赋值是因为执行之前已经县创建了 {n:1} 对象的 x属性，这一行语句可以等价为 {n:1}.x = a = {n:2}
console.log(a.x) //undefined
console.log(b.x) //{n:2}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于全局环境，预编译大概分为如下几步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;GO&lt;/code&gt; 对象（ &lt;code&gt;Global Object&lt;/code&gt; ）全局对象。&lt;/li&gt;
&lt;li&gt;找到用 &lt;code&gt;var&lt;/code&gt; 语句进行的变量声明，将变量名作为 &lt;code&gt;GO&lt;/code&gt; 属性名，值为 &lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查找函数声明，作为 &lt;code&gt;GO&lt;/code&gt; 属性，属性名为函数名，值为函数体（如果函数名与上一步的变量名冲突，那么上一步值为 &lt;code&gt;undefined&lt;/code&gt; 的变量提升将被函数声明的提升所覆盖）&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是只有函数声明被提升，函数表达式和变量是没有区别的，因为引擎是扫描 &lt;code&gt;var&lt;/code&gt; 语句来寻找变量，他在预编译阶段只关心 &lt;code&gt;var&lt;/code&gt; 语句声明的变量名，而不关心初始化的值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;函数的执行环境是当引擎解释执行到函数调用的地方才会创建，预编译也是在这时进行，预编译完成后才会解释执行函数。函数执行上下文的预编译整体步骤和全局环境是差不多的，不同的地方就是在多了形参和实参的加入。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建执行上下文的活动对象 &lt;code&gt;AO（Activation Object）&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;找形参和 &lt;code&gt;var&lt;/code&gt; 语句进行的变量声明，将变量和形参名作为 &lt;code&gt;AO&lt;/code&gt; 属性名，值为 &lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将实参值和形参统一。&lt;/li&gt;
&lt;li&gt;在函数体里面找函数声明，值赋予函数体。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;在同一个执行环境中出现的形参，变量声明，函数声明，只要出现重名，在预编译中我们可以完全看做同一个东西，即 &lt;code&gt;AO&lt;/code&gt; 对象中的一个属性。我们需要注意的是他们发生的先后顺序。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面来分析几个例子&lt;/p&gt;
&lt;h2&gt;例一&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function s() {
  m() //123
  var m = 10
  function m() {
    console.log(123)
  }
  m() //m is not a function
}
s()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子比较简单，在进入 &lt;code&gt;s&lt;/code&gt; 函数的执行环境，创建活动对象后，先是 &lt;code&gt;var&lt;/code&gt; 语句声明的 &lt;code&gt;m&lt;/code&gt; 变量被提升（在 &lt;code&gt;AO&lt;/code&gt; 中创建一个属性 &lt;code&gt;m&lt;/code&gt;，然后是 &lt;code&gt;function m(){}&lt;/code&gt; 被提升，由于和 &lt;code&gt;m&lt;/code&gt; 对象重名，直接覆盖变量的声明（即修改 &lt;code&gt;AO.m&lt;/code&gt; 的值为函数体），所以第一个 &lt;code&gt;m()&lt;/code&gt; 就是执行的 &lt;code&gt;function m&lt;/code&gt;。但是之后出现的 &lt;code&gt;m&lt;/code&gt; 变量的初始化语句再次将 &lt;code&gt;AO.m&lt;/code&gt; 修改为初始化语句中的 &lt;code&gt;10&lt;/code&gt;，所以当再次想要执行 &lt;code&gt;m()&lt;/code&gt; 时，此时 &lt;code&gt;m&lt;/code&gt; 已经不是一个函数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;函数声明的提升也就是为什么我们能够在函数声明之前调用函数的原因。在代码结构上我们好像是在函数声明之前调用了函数，其实对于引擎来说，我们还是在函数声明之后进行的调用。而且函数声明在预编译阶段被提前后，我们在解释执行阶段就可以无视它了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;例二&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function fn(a) {
  console.log(a) //function a() {}
  // 变量声明+变量赋值（只提升变量声明，不提升变量赋值）
  var a = 123
  console.log(a) //123
  // 函数声明
  function a() {}
  console.log(a) //123
  // 函数表达式
  var b = function () {}
  console.log(b) //function () {}
  // 函数
  function d() {}
}
//调用函数
fn(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子我们按照上面的局部环境预编译的四步来分析：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建执行上下文的活动对象 &lt;code&gt;AO（Activation Object）&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;AO{

}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;找形参和 &lt;code&gt;var&lt;/code&gt; 语句进行的变量声明，将变量和形参名作为 &lt;code&gt;AO&lt;/code&gt; 属性名，值为 &lt;code&gt;undefined&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;AO{
     a : undefined,
     b : undefined
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将实参值和形参统一。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;AO{
     a : 1,
     b : function(){...}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在函数体里面找函数声明，值赋予函数体。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;AO{
     a : function a(){...},
     b : undefined,
     d : function d(){...}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以显然第一个 &lt;code&gt;console.log(a)&lt;/code&gt; 输出的是 &lt;code&gt;AO&lt;/code&gt; 中的 &lt;code&gt;a&lt;/code&gt;，为 &lt;code&gt;function a() {}&lt;/code&gt;，然后 &lt;code&gt;a&lt;/code&gt; 被初始化语句修改为 &lt;code&gt;123&lt;/code&gt; ，所以第二个 &lt;code&gt;console.log(a)&lt;/code&gt; 的结果为 &lt;code&gt;123&lt;/code&gt;。下面的函数声明在解释执行阶段可以忽略，所以第三个 &lt;code&gt;console.log(a)&lt;/code&gt; 依然输出 &lt;code&gt;123&lt;/code&gt;。接下来是对 &lt;code&gt;b&lt;/code&gt; 进行初始化，值为一个匿名函数的引用，所以 &lt;code&gt;b&lt;/code&gt; 的值输出该匿名函数。&lt;/p&gt;
&lt;h2&gt;例三&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var foo = { n: 1 }

;(function (foo) {
  console.log(foo.n) //1
  foo.n = 3
  var foo = { n: 2 }
  console.log(foo.n) //2
})(foo)

console.log(foo.n) //3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子跟上面两个不一样的地方时这一题不再是单纯的变量，而是引用类型。在立即执行函数内部，预编译后的活动对象就是 &lt;code&gt;{foo: {n:1}}&lt;/code&gt;，所以第一个输出结果为 &lt;code&gt;1&lt;/code&gt;。然后改 &lt;code&gt;foo.n&lt;/code&gt; 为 &lt;code&gt;3&lt;/code&gt;。这里需要注意的是引用类型并不是值传递，所以我们此时的 &lt;code&gt;foo&lt;/code&gt; 和全局那个 &lt;code&gt;foo&lt;/code&gt; 指向同一个对象，也就是全局 &lt;code&gt;foo&lt;/code&gt; 指向的对象也被修改了，所以最后一行的 &lt;code&gt;foo.n&lt;/code&gt; 就是输出修改后的值 &lt;code&gt;3&lt;/code&gt;。回到函数内部，&lt;code&gt;var foo = {n:2}&lt;/code&gt; 这一句直接将 &lt;code&gt;AO.foo&lt;/code&gt; 指向了一个新的对象 &lt;code&gt;{n:2}&lt;/code&gt;，这个新对象已经跟全局的那个 &lt;code&gt;foo&lt;/code&gt; 指向的对象没有关系了，所以函数内部的最后一个输出结果为 &lt;code&gt;2&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;《ECMAScript6 入门》&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/28140450&quot; title=&quot;我花了两个月的时间才理解let&quot;&gt;我花了两个月的时间才理解let&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/50236805&quot; title=&quot;JavaScript预编译&quot;&gt;JavaScript预编译&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>一道有趣的JS基础题</title><link>https://clloz.com/blog/js-question</link><guid isPermaLink="true">https://clloz.com/blog/js-question</guid><pubDate>Tue, 30 Jun 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在查阅资料的时候，看到一道考差 &lt;code&gt;JavaScript&lt;/code&gt; 基础知识的题目，其中还是考查到一些自己掌握的不好的知识。&lt;/p&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Foo() {
  getName = function () {
    console.log(1)
  }
  return this
}
Foo.getName = function () {
  console.log(2)
}
Foo.prototype.getName = function () {
  console.log(3)
}
var getName = function () {
  console.log(4)
}
function getName() {
  console.log(5)
}

Foo.getName() //2
getName() //4
Foo().getName() //1
getName() //1
new Foo.getName() //2
new Foo().getName() //3
new new Foo().getName() //3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的一点是这段代码因为牵扯到全局作用于下定义的对象是绑定在全局对象上的属性，所以这段代码使要运行在全局作用域，否则会报错。&lt;/p&gt;
&lt;h2&gt;第一问&lt;/h2&gt;
&lt;p&gt;第一问很简单，执行 &lt;code&gt;Foo&lt;/code&gt; 函数上的 &lt;code&gt;getName&lt;/code&gt; 属性上引用的匿名函数。每一个函数也同时是对象，也可以添加属性。很显然输出结果是 &lt;code&gt;2&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;第二问&lt;/h2&gt;
&lt;p&gt;第二问考查的是函数声明提升和变量声明提升之间的关系。最后的两个 &lt;code&gt;getName&lt;/code&gt; 函数分别是函数表达式和函数声明，它们之间的覆盖关系是这一问的关键。&lt;/p&gt;
&lt;p&gt;变量和函数声明的提升主要有这两点： 1. 变量和函数声明都会提升到函数顶部。 2. 初始化不会被提升。 3. 函数声明的提升优先级是高于变量的提升，且不会被变量的提升所覆盖，但是后面变量的初始化会覆盖函数声明提升。&lt;/p&gt;
&lt;p&gt;所以最后两个同名的函数可以等同于下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getName() {
  console.log(5)
}
var getName

getName = function () {
  console.log(4)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到最终变量的初始化覆盖了函数声明，所以这一问的结果就是 &lt;code&gt;4&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;第三问&lt;/h2&gt;
&lt;p&gt;这一问主要考察的是对 &lt;code&gt;this&lt;/code&gt; 的理解，直接调用的函数在非严格模式下，其 &lt;code&gt;this&lt;/code&gt; 是指向全局对象的（详细的 &lt;code&gt;this&lt;/code&gt; 指向分析请看&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/30/js-this/&quot; title=&quot;JavaScript中的this指向&quot;&gt;JavaScript中的this指向&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Foo&lt;/code&gt; 函数做了两件事，首先将全局作用域下定义的 &lt;code&gt;getName&lt;/code&gt; 变量重新赋值（一个新的匿名函数引用），然后返回全局对象。所以 &lt;code&gt;Foo().getName()&lt;/code&gt; 相当于 &lt;code&gt;window.getName()&lt;/code&gt;，此时的全局对象上的 &lt;code&gt;getName&lt;/code&gt; 函数已经在 &lt;code&gt;Foo&lt;/code&gt; 函数中被重写，所以最后的结果是 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;第四问&lt;/h2&gt;
&lt;p&gt;第四问是直接执行全局作用域下的 &lt;code&gt;getName&lt;/code&gt; 函数，因为第三问中重新给 &lt;code&gt;getName&lt;/code&gt; 重新赋值了，所以结果跟第三问是相同的。&lt;/p&gt;
&lt;h2&gt;第五~七问&lt;/h2&gt;
&lt;p&gt;其实最后三问的核心问题是相同的，就是 &lt;code&gt;new&lt;/code&gt; 运算符的优先级问题，我在&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/29/new-operator/&quot; title=&quot;new操作符的解析和实现&quot;&gt;new操作符的解析和实现&lt;/a&gt;中最后也提到了，带参数的 &lt;code&gt;new&lt;/code&gt; 运算符的优先级是高于不带参数的 &lt;code&gt;new&lt;/code&gt; 运算符的。比带参数 &lt;code&gt;new&lt;/code&gt; 优先级高的运算符只有圆括号和成员访问的两种方式 &lt;code&gt;.&lt;/code&gt; 和 &lt;code&gt;[]&lt;/code&gt;。在带参数和不带参数 &lt;code&gt;new&lt;/code&gt; 之间还有一个就是函数调用运算符 &lt;code&gt;()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以第五问的 &lt;code&gt;new Foo.getName();&lt;/code&gt; 可以理解为 &lt;code&gt;new (Foo.getName)()&lt;/code&gt; ，把 &lt;code&gt;Foo.getName&lt;/code&gt; 看做是一个整体，然后执行 &lt;code&gt;new&lt;/code&gt; 运算符。最后的结果就是以 &lt;code&gt;Foo.getName&lt;/code&gt; 作为构造函数实例化了一个对象，在实例化过程中会执行构造函数，所以返回 &lt;code&gt;2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;第六问 &lt;code&gt;new Foo().getName()&lt;/code&gt;中优先级最高的是 &lt;code&gt;.&lt;/code&gt;，然后是 &lt;code&gt;new ()&lt;/code&gt;，那么可以这么理解 &lt;code&gt;(new Foo()).getName()&lt;/code&gt;，&lt;code&gt;new Foo()&lt;/code&gt; 相当于以 &lt;code&gt;Foo&lt;/code&gt; 为构造函数创建了一个新对象，对象的 &lt;code&gt;[[ptototype]]&lt;/code&gt; 指向 &lt;code&gt;Foo.prototype&lt;/code&gt;，新对象本身没有 &lt;code&gt;getName&lt;/code&gt; 属性，但是 &lt;code&gt;Foo.prototype&lt;/code&gt; 上有，所以相当于执行 &lt;code&gt;Foo.prototype.getName()&lt;/code&gt;，结果为 &lt;code&gt;3&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;理论上 &lt;code&gt;.&lt;/code&gt; 的优先级是高于带参数 &lt;code&gt;new&lt;/code&gt; 的，这一问结果是先执行带参数 &lt;code&gt;new&lt;/code&gt; 了。左边要么两种可能 &lt;code&gt;Foo().&lt;/code&gt; 或者 &lt;code&gt;new Foo().&lt;/code&gt;，单参数 &lt;code&gt;new&lt;/code&gt; 的优先级是高于函数调用的，所以在执行 &lt;code&gt;.&lt;/code&gt; 的时候是把左边看做一个整体的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第七问 &lt;code&gt;new new Foo().getName()&lt;/code&gt; 可以理解为 &lt;code&gt;new ((new Foo()).getName)()&lt;/code&gt; 就相当于 &lt;code&gt;new Foo.prototype.getName()&lt;/code&gt;，最终结果也是 &lt;code&gt;3&lt;/code&gt;，生成一个以 &lt;code&gt;Foo.prototype.getName&lt;/code&gt; 为构造函数的实例。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;运算符的优先级可以查看 &lt;code&gt;mdn&lt;/code&gt; 或者是我的文章&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/04/05/operator-precedence/&quot; title=&quot;运算符优先级&quot;&gt;运算符优先级&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是我对这道题目的分析，可能有错漏之处，欢迎指正。这样的题目虽然在看上去没什么实际意义，有点刁钻，但是一些背后的知识点还是有用的，比如 &lt;code&gt;this&lt;/code&gt; 指向，命名冲突的变量和函数的表现等等。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>JavaScript中的this指向</title><link>https://clloz.com/blog/js-this</link><guid isPermaLink="true">https://clloz.com/blog/js-this</guid><pubDate>Tue, 30 Jun 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JS&lt;/code&gt; 中的 &lt;code&gt;this&lt;/code&gt; 指向是一个经常被问到的问题，网上也有很多文章是关于 &lt;code&gt;this&lt;/code&gt; 的。本文整理一下我理解下的 &lt;code&gt;this&lt;/code&gt; 以及一些我比较疑惑的关于 &lt;code&gt;this&lt;/code&gt; 问题。&lt;/p&gt;
&lt;h2&gt;this 指向&lt;/h2&gt;
&lt;p&gt;有几个 &lt;code&gt;this&lt;/code&gt; 的指向问题是几乎每篇文章都会说的，比如作为函数直接调用，作为对象的方法调用，&lt;code&gt;new&lt;/code&gt; 运算符执行中的 &lt;code&gt;this&lt;/code&gt; 行为。比较通用的说法是，&lt;code&gt;this&lt;/code&gt; 指向的是直接调用该函数的对象。其实也很好理解，就是为什么需要 &lt;code&gt;this&lt;/code&gt; 这个关键字，就是我们有需要在函数内部对调用函数的对象进行操作的需求。但是有时候我们遇到的情况并不是像书上或 &lt;code&gt;mdn&lt;/code&gt; 上遇到的典型的情况，&lt;code&gt;this&lt;/code&gt; 的行为可能就会让我们感到有点疑惑。&lt;/p&gt;
&lt;h2&gt;函数的直接调用&lt;/h2&gt;
&lt;p&gt;当我们直接调用一个已经声明的函数，那么在非严格模式下，该函数内部的 &lt;code&gt;this&lt;/code&gt; 指向的是全局对象，浏览器环境下就是 &lt;code&gt;window&lt;/code&gt; 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function f1() {
  return this
}
//在浏览器中：
f1() === window //在浏览器中，全局对象是window

//在Node中：
f1() === global
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当函数是在全局环境下定义的时候，这种现象是可以理解的，因为全局环境下定义的函数其实就是挂载在全局对象上的一个属性，比附上面的 &lt;code&gt;f1&lt;/code&gt; 也可以理解为 &lt;code&gt;window.f1&lt;/code&gt;。但我认为严格模式下的行为才是更符合 &lt;code&gt;this&lt;/code&gt; 这个关键字的目的的，特别是我们的函数可能是在非全局环境（比如另一个函数中）定义和调用的，这种情况下 &lt;code&gt;this&lt;/code&gt; 还指向 &lt;code&gt;window&lt;/code&gt; 是不太合理的。所以在严格模式下，一个函数直接调用，它的 &lt;code&gt;this&lt;/code&gt; 指向的是 &lt;code&gt;undefined&lt;/code&gt;，如果我们想要得到非严格模式下的结果，那我们调用函数的方法就要改为 &lt;code&gt;window.f1()&lt;/code&gt;，而如果函数是在非全局环境下定义的话，那么始终返回的是 &lt;code&gt;undefined&lt;/code&gt;。我认为这样的行为是更符合逻辑的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&apos;use strict&apos;
function d() {
  function e() {
    console.log(this)
  }
  console.log(this)
}

d()
//undefined
//undefined

window.d()
//Window{}
//undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里在全局模式下使用 &lt;code&gt;use strict&lt;/code&gt; 只是为了测试，实际使用还是尽量放在函数内局部使用严格模式，全局下的严格模式很容易导致出错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;函数作为对象的属性调用&lt;/h2&gt;
&lt;p&gt;这也是在代码中非常常见的场景，我认为这是比函数调用更好理解，也更能帮助我们理解 &lt;code&gt;this&lt;/code&gt; 行为的场景。简单的来说就是 &lt;code&gt;this&lt;/code&gt; 指向的是 &lt;strong&gt;&lt;code&gt;直接&lt;/code&gt;&lt;/strong&gt; 调用函数的那个对象。并且要注意的是，这跟函数在哪里定义的是无关的，我们看 &lt;code&gt;this&lt;/code&gt;，看的就是从哪里调用的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//在对象内部定义
var o = {
  prop: 37,
  f: function () {
    return this.prop
  }
}

console.log(o.f()) // 37

//在对象外部定义
var o = { prop: 37 }

function independent() {
  return this.prop
}

o.f = independent

console.log(o.f()) // 37

//在对象内部定义，但是给外部变量引用并执行
var o = {
  prop: 37,
  f: function () {
    console.log(this)
    return this.prop
  }
}
var prop = 100
var m = o.f
console.log(m())
//Window{}
//100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的段落我给 &lt;code&gt;直接&lt;/code&gt; 这两个字加粗了，想要表达的意思是当我们通过多个对象的属性嵌套找到并调用函数，那么最后那个最接近函数的对象就是函数 &lt;code&gt;this&lt;/code&gt; 的指向。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var o = {
  a: 10,
  b: {
    a: 12,
    fn: function () {
      console.log(this.a) //12
    }
  }
}
o.b.fn()

var o = {
  a: 10,
  b: {
    // a:12,
    fn: function () {
      console.log(this.a) //undefined
    }
  }
}
o.b.fn()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么我说这个场景能够帮助我们理解，原因就是它反映出 &lt;code&gt;this&lt;/code&gt; 这个关键字的本质。&lt;code&gt;JS&lt;/code&gt; 中的函数也是一种对象，在我们的执行环境中的活动对象保存的也只是函数对象的一个引用，如果这个引用是保存在活动对象中的某个对象的属性中（即我们通过活动对象中的某个对象的属性找到该函数），那么函数执行的时候 &lt;code&gt;this&lt;/code&gt; 就会指向这个对象，这也是为什么多层对象的调用，还是最靠近函数的那个对象作为 &lt;code&gt;this&lt;/code&gt;。虽然在代码中我们的函数是在对象中定义的，但是实际在内存中，对象中只保存着函数的引用，函数自己是在一个单独的内存空间中。所以我们通过哪个对象找到函数并执行，函数中的 &lt;code&gt;this&lt;/code&gt; 就指向这个对象。上面的直接调用 &lt;code&gt;this&lt;/code&gt; 返回 &lt;code&gt;undefined&lt;/code&gt; 也是说得通的。&lt;/p&gt;
&lt;h2&gt;通过原型的调用&lt;/h2&gt;
&lt;p&gt;有时我们是通过原型来执行公用的函数，此时已然符合我们上面的逻辑，我们通过哪个实例 &lt;code&gt;找到&lt;/code&gt; 函数，那么 &lt;code&gt;this&lt;/code&gt; 就指向那个实例。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var o = {
  f: function () {
    return this.a + this.b
  }
}
var p = Object.create(o)
p.a = 1
p.b = 4

console.log(p.f()) // 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;箭头函数&lt;/h2&gt;
&lt;p&gt;箭头函数不会创建自己的 &lt;code&gt;this&lt;/code&gt;，它只会从自己的作用域链的上一层继承 &lt;code&gt;this&lt;/code&gt; （&lt;code&gt;mdn&lt;/code&gt; 写的是封闭的词法环境）。当你遇到箭头函数中的 &lt;code&gt;this&lt;/code&gt; 不确定的时候，你可以想象把这个箭头函数换成 &lt;code&gt;console.log(this)&lt;/code&gt;，这个 &lt;code&gt;console&lt;/code&gt; 的输出就是箭头函数中 &lt;code&gt;this&lt;/code&gt; 的值，并且箭头函数的 &lt;code&gt;this&lt;/code&gt; 是绑定的，不会改变（有时候看上去改变了是所在的 &lt;code&gt;context&lt;/code&gt; 改变了）。还有一点需要注意的是，用 &lt;code&gt;call&lt;/code&gt;，&lt;code&gt;apply&lt;/code&gt;，&lt;code&gt;bind&lt;/code&gt; 来调用箭头函数，第一个参数是没有意义的，也就是无法改变 &lt;code&gt;this&lt;/code&gt;，如果仍需要使用，第一个参数应该传 &lt;code&gt;null&lt;/code&gt;。看 &lt;code&gt;mdn&lt;/code&gt; 给出的示例。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var globalObject = this
var foo = () =&gt; this
console.log(foo() === globalObject) // true

// 接着上面的代码
// 作为对象的一个方法调用
var obj = { foo: foo }
console.log(obj.foo() === globalObject) // true

// 尝试使用call来设定this
console.log(foo.call(obj) === globalObject) // true

// 尝试使用bind来设定this
foo = foo.bind(obj)
console.log(foo() === globalObject) // true

// 创建一个含有bar方法的obj对象，
// bar返回一个函数，
// 这个函数返回this，
// 这个返回的函数是以箭头函数创建的，
// 所以它的this被永久绑定到了它外层函数的this。
// bar的值可以在调用中设置，这反过来又设置了返回函数的值。
var obj = {
  bar: function () {
    var x = () =&gt; this
    return x
  }
}

// 作为obj对象的一个方法来调用bar，把它的this绑定到obj。
// 将返回的函数的引用赋值给fn。
var fn = obj.bar()

// 直接调用fn而不设置this，
// 通常(即不使用箭头函数的情况)默认为全局对象
// 若在严格模式则为undefined
console.log(fn() === obj) // true

// 但是注意，如果你只是引用obj的方法，
// 而没有调用它
var fn2 = obj.bar
// 那么调用箭头函数后，this指向window，因为它从 bar 继承了this。
console.log(fn2()() == window) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;由于箭头函数没有自己的 &lt;code&gt;this&lt;/code&gt;，所以在一些情况下不要使用箭头函数，会导出错误或者意外的行为。下面是一些总结的箭头函数的一些规则。关于 &lt;code&gt;this&lt;/code&gt; 其实总的来说就是一条，箭头函数没有自己的 &lt;code&gt;this&lt;/code&gt;，如果在箭头函数中使用 &lt;code&gt;this&lt;/code&gt;，这个 &lt;code&gt;this&lt;/code&gt; 指向函数定义时所在的环境中的 &lt;code&gt;this&lt;/code&gt;，这一这个环境是可能变化的，这将导致箭头函数中的 &lt;code&gt;this&lt;/code&gt; 发生变化。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对象的方法：对象的方法如果使用箭头函数则箭头函数中的 &lt;code&gt;this&lt;/code&gt; 指向的是对象所在环境的 &lt;code&gt;this&lt;/code&gt;。如果是在全局环境中创建的对象，&lt;code&gt;this&lt;/code&gt; 指向全局对象 &lt;code&gt;window&lt;/code&gt;。如果实在 &lt;code&gt;node&lt;/code&gt; 模块中则指向 &lt;code&gt;module.exports&lt;/code&gt; 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let outerObj = {
  name: &apos;clloz&apos;
}
function outer() {
  console.log(this) // outerObj { name: &apos;clloz&apos; }
  const obj = {
    arr: [1, 2, 3],
    sun: () =&gt; {
      console.log(this) // outerObj { name: &apos;clloz&apos; }
    }
  }
  obj.sun()
}
outer.apply(outerObj)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原型上的方法逻辑也和上面一样，不过要注意一点，在 &lt;code&gt;class&lt;/code&gt; 中定义方法如果使用箭头函数的话，这个函数会被 &lt;code&gt;babel&lt;/code&gt; 转换到构造函数中。结合上面一点，不要在对象的方法或类方法中使用箭头函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class Point {
  constructor(x, y) {
    // ...
    this.say = () =&gt; {
      // ...
    }
  }

  toString() {
    // ...
  }
}

//等同于
class Point {
  constructor(x, y) {
    // ...
    this.say = function () {
      const _this = this
      return function () {}.bind(_this)
    }
  }

  toString() {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;箭头函数的 &lt;code&gt;this&lt;/code&gt; 并不是不会变的，只是它确定指向它所在环境的 &lt;code&gt;this&lt;/code&gt;，这个环境可能会变化。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var handler = {
  id: &apos;123456&apos;,
  init: function () {
    let func = () =&gt; {
      console.log(this)
    }
    func()
  }
}
handler.init() //{ id: &apos;123456&apos;, init: [Function: init] }
let m = handler.init
m() //全局对象
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;箭头函数不能作为构造函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;箭头函数没有自己的 &lt;code&gt;this&lt;/code&gt;，&lt;code&gt;arguments&lt;/code&gt;，&lt;code&gt;super&lt;/code&gt; 或 &lt;code&gt;new.target&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;箭头函数不能作为生成器函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于箭头函数没有自己的 &lt;code&gt;this&lt;/code&gt; 指针，通过 &lt;code&gt;bind()&lt;/code&gt;，&lt;code&gt;call()&lt;/code&gt; 或 &lt;code&gt;apply()&lt;/code&gt; 方法调用一个函数时，只能传递参数（不能绑定 &lt;code&gt;this&lt;/code&gt;），他们的第一个参数会被忽略。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;箭头函数没有 &lt;code&gt;prototype&lt;/code&gt; 属性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;箭头函数在参数和箭头之间不能换行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;箭头函数中的箭头不是运算符，但箭头函数具有与常规函数不同的特殊运算符优先级解析规则。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;严格模式下函数中的 &lt;code&gt;this&lt;/code&gt; 不能指向全局对象，如果箭头函数的 &lt;code&gt;this&lt;/code&gt; 指向全局对象，会返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let callback;

callback = callback || function() {}; // ok

callback = callback || () =&gt; {};
// SyntaxError: invalid arrow-function arguments

callback = callback || (() =&gt; {});    // ok
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;vue methods 中的 this&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;vue&lt;/code&gt; 的 &lt;code&gt;methods&lt;/code&gt; 中使用 &lt;code&gt;throttle&lt;/code&gt; 的时候一般这样使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//...
methods: {
  func: throttle(function () {
    // function body
  })
}
//...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将一个函数作为参数传给 &lt;code&gt;throttle&lt;/code&gt; 函数，返回的函数作为 &lt;code&gt;methods&lt;/code&gt; 中的一个方法。在使用的时候我发现，如果 &lt;code&gt;throttle&lt;/code&gt; 的函数参数用箭头函数，&lt;code&gt;this&lt;/code&gt; 是 &lt;code&gt;undefined&lt;/code&gt;（应该是在严格模式中）。这里分析一下原因，可以看下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const obj = {
  func: getFunc(() =&gt; {
    console.log(this)
  })
}

function getFunc(func) {
  console.log(this)
  return func
}

obj.func()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要获取 &lt;code&gt;func&lt;/code&gt; 属性，我们先要计算 &lt;code&gt;getFunc()&lt;/code&gt;，这相当于直接执行 &lt;code&gt;getFunc&lt;/code&gt; 函数，和我们 &lt;code&gt;2.1&lt;/code&gt; 中的函数直接调用是一样的，所以这里的 &lt;code&gt;getFunc&lt;/code&gt; 中的 &lt;code&gt;this&lt;/code&gt; 必然是全局对象(严格模式下是 &lt;code&gt;undefined&lt;/code&gt;)。执行完之后返回了一个函数，我们的 &lt;code&gt;obj.func&lt;/code&gt; 指向的就是这个返回的函数，而这个函数的调用方式是 &lt;code&gt;obj.func&lt;/code&gt;，作为 &lt;code&gt;obj&lt;/code&gt; 的属性调用，和 &lt;code&gt;2.2&lt;/code&gt; 中是一样的，所以内部的 &lt;code&gt;this&lt;/code&gt; 指向 &lt;code&gt;obj&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当然我们的 &lt;code&gt;throttle&lt;/code&gt; 实际返回的不是我们传入的参数，而是一个如下的形式&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function throttle(fn, interval) {
  var executing = false
  return function () {
    if (executing) return
    executing = true
    setTimeout(() =&gt; {
      fn.apply(this, arguments)
      executing = false
    }, interval)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内部 &lt;code&gt;return&lt;/code&gt; 的 &lt;code&gt;function&lt;/code&gt; 中的 &lt;code&gt;this&lt;/code&gt; 是 &lt;code&gt;obj&lt;/code&gt;，而我们的 &lt;code&gt;fn&lt;/code&gt; 则用 &lt;code&gt;apply&lt;/code&gt; 绑定了这个 &lt;code&gt;this&lt;/code&gt;，而如果我们的 &lt;code&gt;fn&lt;/code&gt; 是箭头函数的话，这个绑定则无法生效，因为箭头函数没有自己的 &lt;code&gt;this&lt;/code&gt;，这也是我上面说的现象的原因。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;vue&lt;/code&gt; 中的 &lt;code&gt;throttle&lt;/code&gt; 使用的是 &lt;code&gt;lodash&lt;/code&gt;，其内部也是用的 &lt;code&gt;apply&lt;/code&gt; 绑定的 &lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;定时器的 &lt;code&gt;this&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;由于 &lt;code&gt;setTimeout&lt;/code&gt; 和 &lt;code&gt;setInterval&lt;/code&gt; 是全局对象的方法，所以它们回调函数中的 &lt;code&gt;this&lt;/code&gt; 指向的是全局对象 &lt;code&gt;window&lt;/code&gt;（&lt;strong&gt;注意：不管是严格模式还是非严格模式&lt;/strong&gt;）。在 &lt;code&gt;NodeJS&lt;/code&gt; 中，&lt;code&gt;setTimeout&lt;/code&gt; 和 &lt;code&gt;setInterval&lt;/code&gt; 的回到函数中的 &lt;code&gt;this&lt;/code&gt; 指向的是一个 &lt;code&gt;Timeout&lt;/code&gt; 对象。&lt;/p&gt;
&lt;p&gt;解决 &lt;code&gt;setTimeout&lt;/code&gt; 的 &lt;code&gt;this&lt;/code&gt; 指向问题有如下几个方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用箭头函数&lt;/li&gt;
&lt;li&gt;使用中间变量&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;bind&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//箭头函数
let obj = Object.create(null)
obj.func = function () {
  setTimeout(() =&gt; {
    console.log(this)
  }, 1000)
}
obj.func()

// 使用中间变量
let obj = Object.create(null)
obj.func = function () {
  let that = this
  setTimeout(function () {
    console.log(that)
  }, 1000)
}
obj.func()

// 使用 bind
let obj = Object.create(null)
obj.func = function () {
  setTimeout(
    function () {
      console.log(this)
    }.bind(this),
    1000
  )
}
obj.func() //[Object: null prototype] { func: [Function] }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其他情况&lt;/h2&gt;
&lt;p&gt;还有一些情况我觉得比较简单，就一笔带过。 1. 当函数被用作事件处理函数时，它的 &lt;code&gt;this&lt;/code&gt; 指向触发事件的元素。 2. 当代码被内联 &lt;code&gt;on-event&lt;/code&gt; 处理函数调用时，它的this指向监听器所在的 &lt;code&gt;DOM&lt;/code&gt; 元素，需要注意的是只有最外层的 &lt;code&gt;this&lt;/code&gt; 是这样，如果里面还有嵌套函数，则嵌套函数的 &lt;code&gt;this&lt;/code&gt; 在非严格模式下仍然指向全局对象。 3. 构造函数中的 &lt;code&gt;this&lt;/code&gt; 请看之前的文章&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/06/29/new-operator/&quot; title=&quot;JavaScript中new操作符的解析和实现&quot;&gt;JavaScript中new操作符的解析和实现&lt;/a&gt; 4. &lt;code&gt;bind&lt;/code&gt;，&lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 都一样，函数的 &lt;code&gt;this&lt;/code&gt; 被绑定到第一个参数上。 5. 在全局作用域中的 &lt;code&gt;this&lt;/code&gt; 无论是否在严格模式下，都指向 &lt;code&gt;window&lt;/code&gt;。在全局作用域中用 &lt;code&gt;var&lt;/code&gt; 声明的变量都是 &lt;code&gt;window&lt;/code&gt; 对象上的属性，函数声明和 &lt;code&gt;var&lt;/code&gt; 声明的函数表达式则是全局对象上的方法。（用 &lt;code&gt;var&lt;/code&gt; 声明的变量虽然是全局对象上的属性，但是不能用 &lt;code&gt;delete&lt;/code&gt; 删除）&lt;/p&gt;
&lt;h2&gt;NodeJS 中的 this&lt;/h2&gt;
&lt;p&gt;这里特别提一下 &lt;code&gt;NodeJS&lt;/code&gt; 中的 &lt;code&gt;this&lt;/code&gt;。因为它和浏览器环境还是有些不同的。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;NodeJS&lt;/code&gt; 中我们无法定义全局变量，所有变量都是在模块中的。不过 &lt;code&gt;NodeJS&lt;/code&gt; 提供了一些全局对象，&lt;code&gt;global&lt;/code&gt;，&lt;code&gt;process&lt;/code&gt; 和 &lt;code&gt;console&lt;/code&gt;，他们是所有模块都可以调用的。由于不能像浏览器一样直接声明全局变量，所以 &lt;code&gt;global&lt;/code&gt; 就成为全局变量的一个宿主（&lt;code&gt;Node&lt;/code&gt; 不推荐全局变量）。&lt;/p&gt;
&lt;p&gt;浏览器的全局对象是 &lt;code&gt;window&lt;/code&gt;，&lt;code&gt;NodeJS&lt;/code&gt; 的全局对象是 &lt;code&gt;global&lt;/code&gt;，为了应对不同环境全局对象的名称不一样，所以引入了 &lt;code&gt;globalThis&lt;/code&gt;，它是指向当前环境全局对象的一个引用。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;NodeJS&lt;/code&gt; 中，每一个模块中最外层的 &lt;code&gt;this&lt;/code&gt; 指向的是 &lt;code&gt;module.exports&lt;/code&gt;。这一点跟浏览器很不同，浏览器最外层的 &lt;code&gt;this&lt;/code&gt; 是指向全局对象的，而且模块中用 &lt;code&gt;var&lt;/code&gt; 声明的变量也不是 &lt;code&gt;module.exports&lt;/code&gt; 的属性。&lt;code&gt;node&lt;/code&gt; 中最外层的 &lt;code&gt;this&lt;/code&gt; 和全局对象 &lt;code&gt;global&lt;/code&gt; 没有关系。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(this === module.exports) //true
console.log(this === exports) //true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而函数中的 &lt;code&gt;this&lt;/code&gt; 则是指向全局对象，严格模式下则为 &lt;code&gt;undefined&lt;/code&gt;，这和浏览器逻辑一致的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function a() {
  console.log(this === globalThis) //true
}
a()
console.log(globalThis === global) //true

function a() {
  &apos;use strict&apos;
  console.log(this) //undefined
}
a()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他的没有提到的，基本跟浏览器的逻辑保持一致。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是我所总结的 &lt;code&gt;JS&lt;/code&gt; 中的 &lt;code&gt;this&lt;/code&gt; 的一些要点，如果有什么遗漏或者错误的地方，欢迎指正。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this&quot; title=&quot;this - MDN&quot;&gt;this - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/23804247&quot; title=&quot;this 的值到底是什么？一次说清楚&quot;&gt;this 的值到底是什么？一次说清楚&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;cnblogs.com/pssp/p/5216085.html&quot; title=&quot;彻底理解JS中this的指向&quot;&gt;彻底理解JS中this的指向&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>JS数据类型和判断方法</title><link>https://clloz.com/blog/data-type-indicate</link><guid isPermaLink="true">https://clloz.com/blog/data-type-indicate</guid><pubDate>Mon, 29 Jun 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中目前有 &lt;code&gt;7&lt;/code&gt; 种基本（原始&lt;code&gt;primitives&lt;/code&gt;）数据类型 &lt;code&gt;Undefined&lt;/code&gt;， &lt;code&gt;Null&lt;/code&gt;，&lt;code&gt;Boolean&lt;/code&gt;， &lt;code&gt;Number&lt;/code&gt;， &lt;code&gt;String&lt;/code&gt;，&lt;code&gt;BigInt&lt;/code&gt;，&lt;code&gt;Symbol&lt;/code&gt;，以及一种引用类型 &lt;code&gt;Object&lt;/code&gt;，&lt;code&gt;Object&lt;/code&gt; 中又包括 &lt;code&gt;Function&lt;/code&gt;，&lt;code&gt;Date&lt;/code&gt;，&lt;code&gt;JSON&lt;/code&gt;，&lt;code&gt;RegExp&lt;/code&gt;等，除了 &lt;code&gt;7&lt;/code&gt; 种原始类型，其他的所有能够用 &lt;code&gt;new&lt;/code&gt; 实例化的内置类型都是 &lt;code&gt;Object&lt;/code&gt; 构造的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 是一种弱类型或者说动态语言。这意味着你不用提前声明变量的类型，在程序运行过程中，类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据。&lt;/p&gt;
&lt;h2&gt;数据类型&lt;/h2&gt;
&lt;p&gt;对于数据了类型我们可以通过 &lt;code&gt;typeof&lt;/code&gt; 运算符来判断，具体结果看下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/typeof.B59R5GSo_Z21rqR.webp&quot; alt=&quot;typeof&quot; title=&quot;typeof&quot;&gt;&lt;/p&gt;
&lt;h2&gt;基本数据类型&lt;/h2&gt;
&lt;p&gt;基本数据类型 &lt;code&gt;primitive values&lt;/code&gt; 也成为原始数据或原始类型。它们是一种既非对象也无方法的数据。在 &lt;code&gt;JavaScript&lt;/code&gt; 中，共有 &lt;code&gt;7&lt;/code&gt; 种基本类型：&lt;code&gt;string，number，bigint，boolean，null，undefined，symbol&lt;/code&gt; (&lt;code&gt;ECMAScript 2016&lt;/code&gt; 新增)。多数情况下，基本类型直接代表了最底层的语言实现。&lt;/p&gt;
&lt;p&gt;所有基本类型的值都是不可改变的。但需要注意的是，基本类型本身和一个赋值为基本类型的变量的区别。变量会被赋予一个新值，而原值不能像数组、对象以及函数那样被改变。即基本类型值可以被替换，但不能被改变。比如，&lt;code&gt;JavaScript&lt;/code&gt; 中对字符串的操作一定返回了一个新字符串，原始字符串并没有被改变。&lt;/p&gt;
&lt;p&gt;原始类型中两个比较特殊的就是 &lt;code&gt;Undefined&lt;/code&gt; 和 &lt;code&gt;Null&lt;/code&gt;，他们两个类型都只有一个值就是 &lt;code&gt;undefined&lt;/code&gt; 和 &lt;code&gt;null&lt;/code&gt;。除了 &lt;code&gt;null&lt;/code&gt; 和 &lt;code&gt;undefined&lt;/code&gt; 之外，所有基本类型都有其对应的包装对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt; 为字符串基本类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Number&lt;/code&gt; 为数值基本类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BigInt&lt;/code&gt; 为大整数基本类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Boolean&lt;/code&gt; 为布尔基本类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol&lt;/code&gt; 为字面量基本类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Undefined&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;undefined&lt;/code&gt; 一般表示我们未对变量进行初始化。使用 &lt;code&gt;typeof&lt;/code&gt; 操作符的时候，如果是未声明变量，同样也会返回 &lt;code&gt;undefined&lt;/code&gt;，再比如未设置返回值的函数执行的结果是 &lt;code&gt;undefined&lt;/code&gt;，未设置的参数也会被默认为 &lt;code&gt;undefined&lt;/code&gt;。最后还有一点需要注意的是，&lt;code&gt;undefined&lt;/code&gt; 并不是 &lt;code&gt;js&lt;/code&gt; 的保留字，并且是全局对象的一个属性，在浏览器环境中就是 &lt;code&gt;window.undefined&lt;/code&gt;。当在局部环境中，我们是可以自己给 &lt;code&gt;undefined&lt;/code&gt; 属性赋值的，也就是重写 &lt;code&gt;undefined&lt;/code&gt;，所以为了确保我们使用的 &lt;code&gt;undefined&lt;/code&gt; 是未被重写的，我们可以使用 &lt;code&gt;void 0&lt;/code&gt; 来代替。&lt;/p&gt;
&lt;h2&gt;Null&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;null&lt;/code&gt; 可以简单理解为一个未被创建的对象，比如我们使用 &lt;code&gt;document.getElementById&lt;/code&gt; 并没有找到对应元素的时候就会返回 &lt;code&gt;null&lt;/code&gt;。&lt;code&gt;null&lt;/code&gt; 和 &lt;code&gt;undefined&lt;/code&gt; 有一些区别，在转换为 &lt;code&gt;Number&lt;/code&gt; 的时候，&lt;code&gt;null&lt;/code&gt; 会转换为 &lt;code&gt;0&lt;/code&gt; 而 &lt;code&gt;undefined&lt;/code&gt; 会转换为 &lt;code&gt;NaN&lt;/code&gt;，执行 &lt;code&gt;typeof&lt;/code&gt; 运算的时候，&lt;code&gt;undefined&lt;/code&gt;返回 &lt;code&gt;undefined&lt;/code&gt; 而 &lt;code&gt;null&lt;/code&gt; 返回 &lt;code&gt;object&lt;/code&gt;。使用 &lt;code&gt;==&lt;/code&gt; 判断两者相等会返回 &lt;code&gt;true&lt;/code&gt;，所以为了防止误判一般我们使用全等 &lt;code&gt;===&lt;/code&gt;。&lt;code&gt;null&lt;/code&gt; 是所有对象原型链的终点，&lt;code&gt;Object.prototype.__proto__ === null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;null&lt;/code&gt; 的使用，当我们声明一个变量是用来存储对象我们可以先赋值为 &lt;code&gt;null&lt;/code&gt;，当一个对象不再需要的时候，我们可以设置为 &lt;code&gt;null&lt;/code&gt; 解除这个引用。&lt;/p&gt;
&lt;h2&gt;Number&lt;/h2&gt;
&lt;p&gt;根据 &lt;code&gt;ECMAScript&lt;/code&gt; 标准，&lt;code&gt;JavaScript&lt;/code&gt; 中只有一种数字类型：&lt;code&gt;基于 IEEE 754&lt;/code&gt; 标准的双精度 &lt;code&gt;64&lt;/code&gt; 位二进制格式的值。关于 &lt;code&gt;Number&lt;/code&gt; 我已经在其他的文章中进行了深入的研究，你可以参考另外的文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/10/built-in-objects-api/#Number&quot; title=&quot;JavaScript 常用内置对象API&quot;&gt;JavaScript 常用内置对象API - Number&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/06/11/javascript-number/&quot; title=&quot;JavaScript 中的 Number&quot;&gt;JavaScript 中的 Number&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/10/04/bitwise-operator/&quot; title=&quot;JavaScript 中的按位操作符&quot;&gt;JavaScript 中的按位操作符&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里特别提一点上面几篇文章没有说的小细节，数字类型中只有一个整数有两种表示方法（正负）： &lt;code&gt;0&lt;/code&gt; 可表示为 &lt;code&gt;-0&lt;/code&gt; 和 &lt;code&gt;+0&lt;/code&gt;（&lt;code&gt;0&lt;/code&gt; 是 &lt;code&gt;+0&lt;/code&gt; 的简写）。 在实践中，这也几乎没有影响。 例如 &lt;code&gt;+0 === -0&lt;/code&gt; 为真。 但是，你可能要注意除以 &lt;code&gt;0&lt;/code&gt; 的时候：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;42 / +0 // Infinity
42 / -0 // -Infinity
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;String&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 中的 &lt;code&gt;String&lt;/code&gt; 类型用于表示文本型的数据. 它是由无符号整数值（&lt;code&gt;16bit&lt;/code&gt;）作为元素而组成的集合. 字符串中的每个元素在字符串中占据一个位置. 第一个元素的 &lt;code&gt;index&lt;/code&gt; 值是 &lt;code&gt;0&lt;/code&gt;, 下一个元素的 &lt;code&gt;index&lt;/code&gt; 值是 &lt;code&gt;1&lt;/code&gt;, 以此类推. 字符串的长度就是字符串中所含的元素个数.你可以通过 &lt;code&gt;String&lt;/code&gt; 字面值或者 &lt;code&gt;String&lt;/code&gt; 对象两种方式创建一个字符串。&lt;/p&gt;
&lt;p&gt;如果你想对字符串有更多的了解可以参考我的另外几篇文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/26/character-encoding/&quot; title=&quot;搞懂字符编码&quot;&gt;搞懂字符编码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/10/built-in-objects-api/#String&quot; title=&quot;JavaScript 常用内置对象 API - String&quot;&gt;JavaScript 常用内置对象 API - String&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/08/05/regex-javascript-apply/&quot; title=&quot;正则表达式入门以及JavaScript中的应用&quot;&gt;正则表达式入门以及JavaScript中的应用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/07/24/fsm-kmp/&quot; title=&quot;状态机和KMP算法&quot;&gt;状态机和KMP算法&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Symbol 和 BigInt&lt;/h2&gt;
&lt;p&gt;这两个都是 &lt;code&gt;ES6&lt;/code&gt; 新增的原始数据类型，&lt;code&gt;Symbol&lt;/code&gt; 类型的数据通过 &lt;code&gt;Symbol()&lt;/code&gt; 方法的执行产生，不过需要注意的是 &lt;code&gt;Symbol&lt;/code&gt; 不能作为构造函数，每个从 &lt;code&gt;Symbol()&lt;/code&gt; 返回的 &lt;code&gt;symbol&lt;/code&gt; 值都是唯一的。一个 &lt;code&gt;symbol&lt;/code&gt; 值能作为对象属性的标识符；这是该数据类型仅有的目的。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;BigInt&lt;/code&gt; 则是为了精确表示超出双精度浮点数的最大安全表示范围的大数而新增的基本数据类型。&lt;/p&gt;
&lt;h2&gt;判断数据类型的方法&lt;/h2&gt;
&lt;h2&gt;typeof 运算符&lt;/h2&gt;
&lt;p&gt;最简单的判断数据类型的方法是 &lt;code&gt;typeof&lt;/code&gt; 运算符，返回值为字符串。&lt;code&gt;typeof&lt;/code&gt; 的缺点是除了 &lt;code&gt;Function&lt;/code&gt; 以外的其他所有对象的返回值都是 &lt;code&gt;object&lt;/code&gt; （&lt;code&gt;null&lt;/code&gt; 的返回值也是 &lt;code&gt;object&lt;/code&gt;），如果我们需要区分不同的对象，就无法使用 &lt;code&gt;typeof&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;js&lt;/code&gt; 在底层存储变量的时候，会在变量的机器码的低位 &lt;code&gt;1-3&lt;/code&gt; 位存储其类型信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;000&lt;/code&gt;：对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;010&lt;/code&gt;：浮点数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;100&lt;/code&gt;：字符串&lt;/li&gt;
&lt;li&gt;&lt;code&gt;110&lt;/code&gt;：布尔&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt;：整数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;but&lt;/code&gt;, 对于 undefined 和 null 来说，这两个值的信息存储是有点特殊的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt;：所有机器码均为0&lt;/li&gt;
&lt;li&gt;&lt;code&gt;undefined&lt;/code&gt;：用 &lt;code&gt;−2^30&lt;/code&gt; 整数来表示&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，&lt;code&gt;typeof&lt;/code&gt; 在判断 &lt;code&gt;null&lt;/code&gt; 的时候就出现问题了，由于 &lt;code&gt;null&lt;/code&gt; 的所有机器码均为 &lt;code&gt;0&lt;/code&gt;，因此直接被当做了对象来看待。&lt;/p&gt;
&lt;h2&gt;instanceof 运算符&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;instanceof&lt;/code&gt; 运算符是检测构造函数的原型是否出现在某个对象的原型链上。通过 &lt;code&gt;instanceof&lt;/code&gt; 运算符我们可以实现对 &lt;code&gt;Object&lt;/code&gt; 类型的细分，确定属于哪种对象。但是 &lt;code&gt;instanceof&lt;/code&gt; 的缺点是只能对对象进行检测，对于基本数据类型的实例无法检测（字面量无法检测，但是通过基本包装类型的构造函数创建的基本类型可以进行检测）。&lt;/p&gt;
&lt;h2&gt;constructor 属性&lt;/h2&gt;
&lt;p&gt;利用实例的 &lt;code&gt;constructor&lt;/code&gt; 属性来辅助判断实例的数据类型也是一种手段。一般来说，实例本身是没有 &lt;code&gt;constructor&lt;/code&gt; 属性的，我们所看到的属性都是 &lt;code&gt;实例.__proto__.constructor&lt;/code&gt;，换言之也就是实例的构造函数，这种方式对基本数据类型也是有效的。这种方法的一个比较大的问题是 &lt;code&gt;constructor&lt;/code&gt; 属性是个不受保护的属性，随时可能被更改，我们既可以给实例增加 &lt;code&gt;constructor&lt;/code&gt; 属性，也可以修改构造函数的 &lt;code&gt;prototype&lt;/code&gt; 的引用，也可以直接修改原型的 &lt;code&gt;constructor&lt;/code&gt; 属性。&lt;/p&gt;
&lt;h2&gt;Object.prototype.toString.call()&lt;/h2&gt;
&lt;p&gt;这是最安全准确的检测数据类型的方法，每一种数据类型的构造函数的原型上都有 &lt;code&gt;toString&lt;/code&gt; 方法，但是除了 &lt;code&gt;Object.prototype&lt;/code&gt;上的 &lt;code&gt;toString&lt;/code&gt; 是用来返回当前实例所属类的信息（检测数据类型的），其余的都是转换为字符串的。该方法可以准确检测所有内置类型。自定义类型的返回值为 &lt;code&gt;Object Object&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所有 &lt;code&gt;typeof&lt;/code&gt; 返回值为 &lt;code&gt;&quot;object&quot;&lt;/code&gt; 的对象(如数组)都包含一个内部属性 &lt;code&gt;[[Class]]&lt;/code&gt; (我们可 以把它看作一个内部的分类，而非传统的面向对象意义上的类)。这个属性无法直接访问， 一般通过 &lt;code&gt;Object.prototype.toString(..)&lt;/code&gt; 来查看。所以当我们使用 &lt;code&gt;Object.prototype.toString.call()&lt;/code&gt; 的时候，实际上访问的是对象的内容不属性 &lt;code&gt;[[Class]]&lt;/code&gt;，基本类型会返回他们的包装对象的 &lt;code&gt;[[Class]]&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let toString = Object.prototype.toString //=&gt;Object.prototype.toString

console.log(toString.call(10)) //=&gt;&quot;[object Number]&quot;
console.log(toString.call(NaN)) //=&gt;&quot;[object Number]&quot;
console.log(toString.call(&apos;xxx&apos;)) //=&gt;&quot;[object String]&quot;
console.log(toString.call(true)) //=&gt;&quot;[object Boolean]&quot;
console.log(toString.call(null)) //=&gt;&quot;[object Null]&quot;
console.log(toString.call(undefined)) //=&gt;&quot;[object Undefined]&quot;
console.log(toString.call(Symbol())) //=&gt;&quot;[object Symbol]&quot;
console.log(toString.call(BigInt(10))) //=&gt;&quot;[object BigInt]&quot;
console.log(toString.call({ xxx: &apos;xxx&apos; })) //=&gt;&quot;[object Object]&quot;
console.log(toString.call([10, 20])) //=&gt;&quot;[object Array]&quot;
console.log(toString.call(/^\d+$/)) //=&gt;&quot;[object RegExp]&quot;
console.log(toString.call(function () {})) //=&gt;&quot;[object Function]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/5aa4f7cc518825557e780256&quot; title=&quot;JavaScript深入理解之undefined与null&quot;&gt;JavaScript深入理解之undefined与null&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhangxinxu.com/wordpress/2018/04/known-es6-symbol-function/&quot; title=&quot;简单了解ES6/ES2015 Symbol() 方法&quot;&gt;简单了解ES6/ES2015 Symbol() 方法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/5e88a683f265da47db2e38b8&quot; title=&quot;JS中数据类型检测四种方式的优缺点&quot;&gt;JS中数据类型检测四种方式的优缺点&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Data_structures&quot; title=&quot;JavaScript 数据类型和数据结构 - MDN&quot;&gt;JavaScript 数据类型和数据结构 - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>JavaScript中new操作符的解析和实现</title><link>https://clloz.com/blog/new-operator</link><guid isPermaLink="true">https://clloz.com/blog/new-operator</guid><pubDate>Mon, 29 Jun 2020 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;new&lt;/code&gt; 运算符是我们在用构造函数创建实例的时候使用的，本文来说一下 &lt;code&gt;new&lt;/code&gt; 运算符的执行过程和如何自己实现一个类似 &lt;code&gt;new&lt;/code&gt; 运算符的函数。&lt;/p&gt;
&lt;h2&gt;new 运算符的运行过程&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;new&lt;/code&gt; 运算符的主要目的就是为我们创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例（比如箭头函数就没有构造函数，所以是不能 &lt;code&gt;new&lt;/code&gt; 的）。&lt;code&gt;new&lt;/code&gt; 操作符的执行大概有以下几个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个新的空对象&lt;/li&gt;
&lt;li&gt;把新对象的 &lt;code&gt;__proto__&lt;/code&gt; 链接到构造函数的 &lt;code&gt;prototype&lt;/code&gt; 对象（每一个用户定义函数都有一个 &lt;code&gt;prototype&lt;/code&gt; 属性指向一个对象，该对象有一个 &lt;code&gt;constructor&lt;/code&gt; 属性指向该函数），让我们的公共属性和方法可以从原型上继承，不用每个实例都创建一次。&lt;/li&gt;
&lt;li&gt;将第一步创建的新的对象作为构造函数的 &lt;code&gt;this&lt;/code&gt; 的上下文，执行构造函数，构造函数的执行让我们配置对象的私有属性和方法。&lt;/li&gt;
&lt;li&gt;执行构造函数，如果构造函数没有返回值或者返回值不是一个对象，则返回 &lt;code&gt;this&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我么可以用代码简单表示上面的逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function new_(constr, ...rests) {
  var obj = {}
  obj.__proto__ = constr.prototype
  var ret = constr.apply(obj, rests)
  return isPrimitive(ret) ? obj : ret //判断构造函数的返回值是否为对象，不是则直接返回创建的obj对象
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;new 的实现&lt;/h2&gt;
&lt;p&gt;上面讲了 &lt;code&gt;new&lt;/code&gt; 运算符的执行过程，下面我们来自己动手实现一个 &lt;code&gt;new&lt;/code&gt; 运算符。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function new_(constr, ...rests) {
  if (typeof constr !== &apos;function&apos;) {
    throw &apos;the first param must be a function&apos;
  }
  new_.target = constr
  var obj = Object.create(constr.prototype)
  var ret = constr.apply(obj, rests)
  var isObj = typeof ret !== null &amp;#x26;&amp;#x26; typeof ret === &apos;object&apos;
  var isFun = typeof ret === &apos;function&apos;
  //var isObj = typeof ret === &quot;function&quot; || typeof ret === &quot;object&quot; &amp;#x26;&amp;#x26; !!ret;
  if (isObj || isFun) {
    return ret
  }
  return obj
}

function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.say = function () {
  console.log(this.name)
}
var p1 = new_(Person, &apos;clloz&apos;, &apos;28&apos;)
var p2 = new_(Person, &apos;csx&apos;, &apos;31&apos;)
console.log(p1) //Person {name: &quot;clloz&quot;, age: &quot;28&quot;}
p1.say() //clloz
console.log(p2) //Person {name: &quot;csx&quot;, age: &quot;31&quot;}
p2.say() //csx

console.log(p1.__proto__ === Person.prototype) //true
console.log(p2.__proto__ === Person.prototype) //true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上就是一个简单的 &lt;code&gt;new&lt;/code&gt; 实现，判断是否为对象那里可能不是很严谨，不过没有想到更好的方法。&lt;/p&gt;
&lt;p&gt;一个小补充，在 &lt;code&gt;mdn&lt;/code&gt; 的 &lt;code&gt;Function.prototype.apply()&lt;/code&gt; 词条中看到的直接把方法写到 &lt;code&gt;Function.prototype&lt;/code&gt; 上，也是个不错的思路，&lt;code&gt;Function.prototype&lt;/code&gt; 在所以函数的原型链上，所以这个方法可以在每个函数上调用，方法内部的 &lt;code&gt;this&lt;/code&gt; 也是指向调用方法的函数的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Function.prototype.construct = function (aArgs) {
  var oNew = Object.create(this.prototype)
  this.apply(oNew, aArgs)
  return oNew
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;强制用 new 调用构造函数&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Clloz(...arguments) {
  if (!(this instanceof Clloz)) {
    return new Clloz(...arguments)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;new 的特殊行为&lt;/h2&gt;
&lt;p&gt;我今天偶然突然奇想，如果一个函数的 &lt;code&gt;prototype&lt;/code&gt; 属性被我设为一个非对象的属性，再 &lt;code&gt;new&lt;/code&gt; 会发生什么。结果非常出乎我的意料。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let a = function () {};

a.prototype = false;

let b = new a();

b instance of a; //Uncaught TypeError: Function has non-object prototype &apos;false&apos; in instanceof check

a.prototype === false;  //true

b.__proto__ === Object.prototype //true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由 &lt;code&gt;a&lt;/code&gt; 创建的对象 &lt;code&gt;b&lt;/code&gt; 现在好像被 &lt;code&gt;function Object()&lt;/code&gt; 创建的一样。我在标准中没有找到讲 &lt;code&gt;new&lt;/code&gt; 执行细节的，标准中只是说执行构造函数 内部的一个 &lt;code&gt;[[contruct]]&lt;/code&gt; 方法。&lt;/p&gt;
&lt;h2&gt;Tips&lt;/h2&gt;
&lt;p&gt;补充三个关于 &lt;code&gt;new&lt;/code&gt; 运算符的知识点。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;上面提到 &lt;code&gt;new&lt;/code&gt; 的执行过程的最后一步，如果构造函数没有返回值或者返回值不是一个对象，则返回 &lt;code&gt;this&lt;/code&gt;。但是如果返回的是一个 &lt;code&gt;null&lt;/code&gt; 的话，依然返回 &lt;code&gt;this&lt;/code&gt;，虽然 &lt;code&gt;null&lt;/code&gt; 也算是 &lt;code&gt;object&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt; 操作符后面的构造函数可以带括号也可以不带括号，除了带括号可以传递参数以外，还有一个重要的点是两种用法的运算符优先级不一样，在&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/04/05/operator-precedence/&quot; title=&quot;JS运算符优先级&quot;&gt;JS运算符优先级&lt;/a&gt;这篇文章中有提到，带参数的 &lt;code&gt;new&lt;/code&gt; 操作符的优先级是比不带参数的要高的，&lt;code&gt;new Foo() &gt; Foo() &gt; new Foo&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;运算符优先级相关：对于 &lt;code&gt;new&lt;/code&gt;，函数调用，成员访问三者结合的情况，比如 &lt;code&gt;new obj1.obj2.obj3.fun().prop&lt;/code&gt; 这样的情况，我个人总结就是函数调用直接到前面的 &lt;code&gt;new&lt;/code&gt; 先计算，得到的结果在进行成员访问。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一般不太会遇到，可能有些题目会问这些问题。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/5bde7c926fb9a049f66b8b52&quot; title=&quot;能否实现JS的new操作符&quot;&gt;能否实现JS的new操作符&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>Emacs的小技巧，注释，tab，undo和lsp补全</title><link>https://clloz.com/blog/emacs-tab-redo</link><guid isPermaLink="true">https://clloz.com/blog/emacs-tab-redo</guid><pubDate>Tue, 03 Dec 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在使用 &lt;code&gt;emacs&lt;/code&gt; 的 &lt;code&gt;tab&lt;/code&gt; 和 &lt;code&gt;undo&lt;/code&gt;、&lt;code&gt;redo&lt;/code&gt; 的时候，&lt;code&gt;emacs&lt;/code&gt; 的反应和我们平常使用的一些编辑器或者 &lt;code&gt;IDE&lt;/code&gt; 很不一样，有时候按 &lt;code&gt;tab&lt;/code&gt; 缩进是 &lt;code&gt;4&lt;/code&gt; 格，有时候缩进是 &lt;code&gt;2&lt;/code&gt; 格，有时候不管怎么按都没反应。&lt;code&gt;undo&lt;/code&gt; 一般大家都知道，一般有几个快捷键可用：&lt;code&gt;C-_&lt;/code&gt;，&lt;code&gt;C-x u&lt;/code&gt; 和 &lt;code&gt;C-/&lt;/code&gt;，但是 &lt;code&gt;redo&lt;/code&gt; 的操作新手可能不太了解。最后还有注释和 &lt;code&gt;lsp&lt;/code&gt; 补全的使用，以及在命令行中用 &lt;code&gt;emacs&lt;/code&gt; 的一些注意点。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文的内容是在 &lt;code&gt;spacemacs&lt;/code&gt; 的基础上的一些配置，可能有些内容需要单独在进行一些配置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;alt 问题&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;mac&lt;/code&gt; 的终端上会出现 &lt;code&gt;alt&lt;/code&gt; 无法使用的情况，因为 &lt;code&gt;mac&lt;/code&gt; 的 &lt;code&gt;option&lt;/code&gt; 默认是用来输入特殊符号的，所以我们要禁用掉特殊符号这个功能，在 &lt;code&gt;iTerm2&lt;/code&gt; 里面直接在 &lt;code&gt;Profiles&lt;/code&gt; 里面的 &lt;code&gt;keys&lt;/code&gt; 选项卡中的 &lt;code&gt;left option&lt;/code&gt; 选择为 &lt;code&gt;+ESC&lt;/code&gt; 即可。如果是 &lt;code&gt;terminal&lt;/code&gt; 则直接在 &lt;code&gt;preferences-&gt;Profiles-&gt;keyboard&lt;/code&gt; 的最下面勾选上 &lt;code&gt;用 option 键作为 meta 键&lt;/code&gt;。如果是在 &lt;code&gt;windows&lt;/code&gt; 中一般不会在终端使用 &lt;code&gt;emacs&lt;/code&gt;，如果是通过 &lt;code&gt;ssh&lt;/code&gt; 链接服务器，那么也可以在 &lt;code&gt;xshell&lt;/code&gt; 中设置 &lt;code&gt;将左alt键用作meta键&lt;/code&gt;。其实 &lt;code&gt;emacs&lt;/code&gt; 中的 &lt;code&gt;M-x&lt;/code&gt; 中的 &lt;code&gt;M&lt;/code&gt; 就是 &lt;code&gt;meta&lt;/code&gt; 键的意思，它在不同的系统代表不同的键，&lt;code&gt;emacs&lt;/code&gt; 中的文档也说了 &lt;code&gt;M-x, it means &quot;press Alt/Esc/Option/Edit key and x together&quot;&lt;/code&gt;，所以在 &lt;code&gt;linux&lt;/code&gt; 中其实用 &lt;code&gt;esc&lt;/code&gt; 也可以代替 &lt;code&gt;meta&lt;/code&gt; 但是比较麻烦，所以我们还是对终端进行配置，一般是将 &lt;code&gt;alt&lt;/code&gt; 或者 &lt;code&gt;alt&lt;/code&gt; 设置为 &lt;code&gt;meta&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;tab 的使用&lt;/h2&gt;
&lt;p&gt;上面说到 &lt;code&gt;emacs&lt;/code&gt; 中的 &lt;code&gt;tab&lt;/code&gt; 并不像大家平时用的 &lt;code&gt;IDE&lt;/code&gt; 或者编辑器的表现，刚开始很不习惯。其实你可以把 &lt;code&gt;emacs&lt;/code&gt; 中的 &lt;code&gt;tab&lt;/code&gt; 理解为格式化代码，对选中区域或光标所在行进行代码的格式化，类似在 &lt;code&gt;vscode&lt;/code&gt; 中的快捷键 &lt;code&gt;Shift + Alt + f&lt;/code&gt; 的作用，在有些插件里面，补全也会用到 &lt;code&gt;tab&lt;/code&gt; 键。那么如果我们需要输入正常的 &lt;code&gt;tab&lt;/code&gt; 要怎么做呢，&lt;code&gt;emacs&lt;/code&gt; 默认提供了一个 &lt;code&gt;quoted insert&lt;/code&gt; 方法来让你输入一些特殊的键或者字符的八进制编码，比如 &lt;code&gt;&amp;#x3C;del&gt;&lt;/code&gt; 或者 &lt;code&gt;&amp;#x3C;tab&gt;&lt;/code&gt; 等，所以要输入 &lt;code&gt;tab&lt;/code&gt; 就用快捷键 &lt;code&gt;C+q tab&lt;/code&gt; 就可以了，如果你觉得这很麻烦，也可以自己绑定快捷键。&lt;code&gt;emacs&lt;/code&gt; 提供的一个方法 &lt;code&gt;tab-to-tab-stop&lt;/code&gt; 来输入 &lt;code&gt;tab&lt;/code&gt;，快捷键是 &lt;code&gt;M + i&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于 &lt;code&gt;quoted insert&lt;/code&gt; 的内容可以查看官方文档 &lt;a href=&quot;https://www.gnu.org/software/emacs/manual/html_node/emacs/Inserting-Text.html&quot; title=&quot;Insert Text&quot;&gt;Insert Text&lt;/a&gt;，或者用 &lt;code&gt;C-h k&lt;/code&gt; 来查看快捷键对应的用法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;另外关于 &lt;code&gt;tab&lt;/code&gt; 的缩进 &lt;code&gt;indent&lt;/code&gt;，不同的语言可以设置不同的缩进，如果你使用的是别人的配置那么可能你需要改多个地方的配置，可以用 &lt;code&gt;M-x customize-variable&lt;/code&gt; 来查看 &lt;code&gt;indent&lt;/code&gt; 相关的配置，比如 &lt;code&gt;js&lt;/code&gt; 的 &lt;code&gt;indent&lt;/code&gt; 设置在 &lt;code&gt;js-indent-level&lt;/code&gt; 这个变量里面，而 &lt;code&gt;html&lt;/code&gt; 的设置在 &lt;code&gt;web-mode-code-indent-offset&lt;/code&gt; 这个变量里面，具体要更改哪个，要看你的语言对应的模式然后查找对应的变量。&lt;/p&gt;
&lt;p&gt;最后再提一点就是 &lt;code&gt;backtab&lt;/code&gt; 就是我们在其他编辑器常用的 &lt;code&gt;Shift + tab&lt;/code&gt;，这个我是使用的一个自定义函数来实现的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lisp&quot;&gt;(defun un-indent-by-removing-4-spaces ()
    &quot;remove 4 spaces from beginning of of line&quot;
    (interactive)
    (save-excursion
      (save-match-data
        (beginning-of-line)
        ;; get rid of tabs at beginning of line
        (when (looking-at &quot;^\\s-+&quot;)
          (untabify (match-beginning 0) (match-end 0)))
        (when (looking-at (concat &quot;^&quot; (make-string tab-width ?\ )))
          (replace-match &quot;&quot;)))))

(global-set-key (kbd &quot;&amp;#x3C;backtab&gt;&quot;) &apos;un-indent-by-removing-4-spaces)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;undo 和 redo&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;emacs&lt;/code&gt; 中提供了好几种 &lt;code&gt;undo&lt;/code&gt; 的快捷键 &lt;code&gt;C-_&lt;/code&gt;，&lt;code&gt;C-x u&lt;/code&gt; 和 &lt;code&gt;C-/&lt;/code&gt;，其中在命令行中 &lt;code&gt;C-x u&lt;/code&gt; 可能无效，我一般是使用 &lt;code&gt;C-_&lt;/code&gt;。&lt;code&gt;redo&lt;/code&gt; 操作一般我们在其他编辑器中用 &lt;code&gt;Ctrl + Shift + z&lt;/code&gt; 来操作，在 &lt;code&gt;emacs&lt;/code&gt; 上显然不可以这样。其实在 &lt;code&gt;emacs&lt;/code&gt; 中，每个缓冲区都有一个 &lt;code&gt;undo&lt;/code&gt; 记录，每次更改缓冲区都会放入这个 &lt;code&gt;undo&lt;/code&gt; 记录中，我们可以通过连续的 &lt;code&gt;c-_&lt;/code&gt; 进行 &lt;code&gt;undo&lt;/code&gt;，如果在连续的 &lt;code&gt;undo&lt;/code&gt; 命令序列中插入其他命令比如文档中提到的 &lt;code&gt;C-f&lt;/code&gt; 或者是 &lt;code&gt;C-g&lt;/code&gt;，那么前面的连续的 &lt;code&gt;undo&lt;/code&gt; 操作都会被作为单独的修改集合放入 &lt;code&gt;undo&lt;/code&gt; 记录中，从而可以重新应用刚才被 &lt;code&gt;undo&lt;/code&gt; 的操作。&lt;/p&gt;
&lt;p&gt;也就是说，比如你进行了 &lt;code&gt;操作1 操作2 操作3&lt;/code&gt;，此时你是用 &lt;code&gt;undo&lt;/code&gt; 回到了 &lt;code&gt;操作2&lt;/code&gt; 执行后的状态，这时候你想要 &lt;code&gt;redo&lt;/code&gt; 回到&lt;code&gt;操作3&lt;/code&gt; 执行后的状态，就先输入一个非 &lt;code&gt;undo&lt;/code&gt; 命令，比如 &lt;code&gt;C-g&lt;/code&gt;，然后在输入 &lt;code&gt;undo&lt;/code&gt; 就编程 &lt;code&gt;redo&lt;/code&gt; 操作了，具体的说明请看官方文档：&lt;a href=&quot;https://ftp.gnu.org/old-gnu/Manuals/emacs-20.7/html_node/emacs_19.html&quot; title=&quot;Undoing Changes&quot;&gt;Undoing Changes&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;当然更好的做法是使用 &lt;code&gt;undo tree&lt;/code&gt; 这个插件，安装好插件后 &lt;code&gt;C-x u&lt;/code&gt; 就称为打开&lt;code&gt;undo-tree-visualizer-mode&lt;/code&gt; 这个 &lt;code&gt;buffer&lt;/code&gt; 的快捷键，打开后我们能在一个新的 &lt;code&gt;buffer&lt;/code&gt; 里面看到我们所有的历史操作，并且细分出了各个分支。&lt;code&gt;undo tree&lt;/code&gt; 的操作方式有如下几种，&lt;code&gt;p，n&lt;/code&gt; 在节点上上下移动，&lt;code&gt;b，f&lt;/code&gt; 来选择分支（选中的分支会显示黑色），&lt;code&gt;t&lt;/code&gt; 显示时间戳，&lt;code&gt;q&lt;/code&gt; 退出。&lt;code&gt;undo tree&lt;/code&gt; 同时提供了一个 &lt;code&gt;undo tree redo&lt;/code&gt; 的方法，我们可以直接进行 &lt;code&gt;redo&lt;/code&gt; 不在需要那么复杂的逻辑，比价符合我们正常的思维。&lt;/p&gt;
&lt;h2&gt;注释 comment`&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;emacs&lt;/code&gt; 提供的注释默认快捷键是 &lt;code&gt;C-;&lt;/code&gt;，但是在命令行里面几乎所有和标点符号有关的快捷键都不能生效，这种情况下我们可以给 &lt;code&gt;comment-line&lt;/code&gt; 这个函数定义两组快捷键，这样我们可以在命令行和 &lt;code&gt;GUI&lt;/code&gt; 都能够使用注释的功能。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lisp&quot;&gt;(global-set-key (kbd &quot;C-c C-k&quot;) &apos;comment-line)
(global-set-key (kbd &quot;C-;&quot;) &apos;comment-line)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;lsp 自动补全&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/emacs-lsp/lsp-mode&quot; title=&quot;LSP&quot;&gt;LSP&lt;/a&gt; (&lt;code&gt;Language Server Protocol&lt;/code&gt;) 是微软领导开发的编程语言语法补全和代码分析框架，&lt;code&gt;Emacs&lt;/code&gt; 的 &lt;code&gt;lsp-mode&lt;/code&gt; 是 &lt;code&gt;LSP&lt;/code&gt; 协议在 &lt;code&gt;Emacs&lt;/code&gt; 的客户端实现. &lt;code&gt;lsp-mode&lt;/code&gt; 现在能够很好的支持 &lt;code&gt;C++, Python, Ruby, Golang, Haskell, OCamel, Rust, PHP&lt;/code&gt; 等语言. 当然也包括 &lt;code&gt;JavaScript&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;spacemacs&lt;/code&gt; 已经默认集成了 &lt;code&gt;lsp&lt;/code&gt;，不过想要实现补全还需要安装对应语言的对应服务，比如补全 &lt;code&gt;js&lt;/code&gt; 需要安装 &lt;code&gt;typescript-language-server;&lt;/code&gt;，其他语言参考&lt;a href=&quot;https://github.com/emacs-lsp/lsp-mode#supported-languages&quot; title=&quot;lsp-supported-languages&quot;&gt;lsp-supported-languages&lt;/a&gt;。在 &lt;code&gt;spacemacs&lt;/code&gt; 中要给对应的 &lt;code&gt;mode&lt;/code&gt; 开启 &lt;code&gt;lsp&lt;/code&gt;，加入如下内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lisp&quot;&gt;(use-package lsp-mode
  :hook (XXX-mode . lsp)
  :commands lsp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以在 &lt;code&gt;dotspacemacs-configuration-layers&lt;/code&gt; 对应的模块中直接设置，比如 &lt;code&gt;(javascript :variables javascript-backend &apos;lsp)&lt;/code&gt;。需要注意的是，如果是手动启用，不是启动 &lt;code&gt;lsp-mode&lt;/code&gt;，而是启动 &lt;code&gt;lsp&lt;/code&gt;，如果出现 &lt;code&gt;spacemacs-jump-handlers-xxx-mode&lt;/code&gt; 报错的话，在 &lt;code&gt;user-config&lt;/code&gt; 里面加上 &lt;code&gt;(defvar spacemacs-jump-handlers-xxx-mode nil)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果不小心将文件加入到 &lt;code&gt;lsp&lt;/code&gt; 的 &lt;code&gt;blacklist&lt;/code&gt; 中，可以用 &lt;code&gt;lsp-workspace-blacklist-remove&lt;/code&gt; 方法来移除。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/gamesun/archive/2012/12/23/2830184.html&quot; title=&quot;Emacs的Tab键&quot;&gt;Emacs的Tab键&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/emacs-logo.CVWUvzc8.png"/><enclosure url="/_astro/emacs-logo.CVWUvzc8.png"/></item><item><title>tmux的基础用法</title><link>https://clloz.com/blog/tmux-usage</link><guid isPermaLink="true">https://clloz.com/blog/tmux-usage</guid><pubDate>Wed, 27 Nov 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在用 &lt;code&gt;ssh&lt;/code&gt; 远程登录服务器的时候，长时间不操作，或不小心按了 &lt;code&gt;ctrl + d&lt;/code&gt;，连接就断开了，在重连上去，一切又恢复到初始状态，想要查看之前的命令和一些操作都无法找到记录了。&lt;/p&gt;
&lt;p&gt;命令行的典型使用方式是，打开一个终端窗口在里面输入命令。用户与计算机的这种临时的交互，称为一次&quot;会话&quot;（&lt;code&gt;session&lt;/code&gt;） 。会话的一个重要特点是，窗口与其中启动的进程是连在一起的。打开窗口，会话开始；关闭窗口，会话结束，会话内部的进程也会随之终止，不管有没有运行完。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tmux&lt;/code&gt; 就是为了解决这个问题，把窗口和会话分开，即使我们关闭窗口，会话也还在，我们在下次需要的时候又可以打开，继续上次的内容。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Ubuntu 或 Debian
$ sudo apt-get install tmux

# CentOS 或 Fedora
$ sudo yum install tmux

# Mac
$ brew install tmux
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;会话管理&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 新建会话
$ tmux new -s &amp;#x3C;session-name&gt;

# 分离会话，关闭当前窗口，保留会话
$ tmux detach #或者用快捷键 ctrl+b d

# 查看所有会话
$ tmux ls

# 重新进入会话
$ tmux attach -t 0     # 使用会话编号，第一个启动的 tmux 窗口，编号是0，第二个窗口的编号是1，以此类推。
$ tmux attach -t &amp;#x3C;session-name&gt;    # 使用会话名称

# 结束会话，关闭窗口也关闭会话
$ tmux kill-session -t 0    # 使用会话编号
$ tmux kill-session -t &amp;#x3C;session-name&gt; # 使用会话名称 也可以直接使用快捷键 ctrl+d，和退出远程登录的ssh一样

# 切换会话
$ tmux switch -t 0    # 使用会话编号
$ tmux switch -t &amp;#x3C;session-name&gt; # 使用会话名称

# 重命名会话
$ tmux rename-session -t 0 &amp;#x3C;new-name&gt;

# 快捷键
Ctrl+b d：分离当前会话。
Ctrl+b s：列出所有会话。
Ctrl+b $：重命名当前会话。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;窗格操作&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tmux&lt;/code&gt; 可以将窗口分成多个窗格（&lt;code&gt;pane&lt;/code&gt;），每个窗格运行不同的命令。以下命令都是在 &lt;code&gt;Tmux&lt;/code&gt; 窗口中执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 划分上下两个窗格
$ tmux split-window

# 划分左右两个窗格
$ tmux split-window -h

# 光标切换到上方窗格
$ tmux select-pane -U

# 光标切换到下方窗格
$ tmux select-pane -D

# 光标切换到左边窗格
$ tmux select-pane -L

# 光标切换到右边窗格
$ tmux select-pane -R

# 当前窗格上移
$ tmux swap-pane -U

# 当前窗格下移
$ tmux swap-pane -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;窗口操作快捷键&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b %&lt;/code&gt;：划分左右两个窗格。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b &quot;&lt;/code&gt;：划分上下两个窗格。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b &amp;#x3C;arrow key&gt;&lt;/code&gt;：光标切换到其他窗格。&lt;code&gt;&amp;#x3C;arrow key&gt;&lt;/code&gt; 是指向要切换到的窗格的方向键，比如切换到下方窗格，就按方向键 &lt;code&gt;↓&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b ;&lt;/code&gt;：光标切换到上一个窗格。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b o&lt;/code&gt;：光标切换到下一个窗格。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b {&lt;/code&gt;：当前窗格左移。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b }&lt;/code&gt;：当前窗格右移。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b Ctrl+o&lt;/code&gt;：当前窗格上移。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b Alt+o&lt;/code&gt;：当前窗格下移。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b x&lt;/code&gt;：关闭当前窗格。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b !&lt;/code&gt;：将当前窗格拆分为一个独立窗口。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b z&lt;/code&gt;：当前窗格全屏显示，再使用一次会变回原来大小。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b Ctrl+&amp;#x3C;arrow key&gt;&lt;/code&gt;：按箭头方向调整窗格大小。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b q&lt;/code&gt;：显示窗格编号。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;窗口操作&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 创建新的窗口
$ tmux new-window

# 新建一个指定名称的窗口
$ tmux new-window -n &amp;#x3C;window-name&gt;

# 切换到指定编号的窗口
$ tmux select-window -t &amp;#x3C;window-number&gt;

# 切换到指定名称的窗口
$ tmux select-window -t &amp;#x3C;window-name&gt;

# 重命名当前窗口
$ tmux rename-window &amp;#x3C;new-name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;窗口操作快捷键&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b c&lt;/code&gt;：创建一个新窗口，状态栏会显示多个窗口的信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b p&lt;/code&gt;：切换到上一个窗口(按照状态栏上的顺序)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b n&lt;/code&gt;：切换到下一个窗口。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b &amp;#x3C;number&gt;&lt;/code&gt;：切换到指定编号的窗口，其中的 &lt;code&gt;&amp;#x3C;number&gt;&lt;/code&gt; 是状态栏上的窗口编号。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b w&lt;/code&gt;：从列表中选择窗口。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+b ,&lt;/code&gt;：窗口重命名。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;其他命令&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 列出所有快捷键，及其对应的 Tmux 命令
$ tmux list-keys

# 列出所有 Tmux 命令及其参数
$ tmux list-commands

# 列出当前所有 Tmux 会话的信息
$ tmux info

# 重新加载当前的 Tmux 配置
$ tmux source-file ~/.tmux.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2019/10/tmux.html&quot; title=&quot;Tmux 使用教程&quot;&gt;Tmux 使用教程&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/tmux.Cs9kWA4W.png"/><enclosure url="/_astro/tmux.Cs9kWA4W.png"/></item><item><title>HTML character entity HTML字符实体</title><link>https://clloz.com/blog/html-character-entity</link><guid isPermaLink="true">https://clloz.com/blog/html-character-entity</guid><pubDate>Tue, 19 Nov 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我在&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/26/character-encoding/&quot; title=&quot;搞懂字符编码&quot;&gt;搞懂字符编码&lt;/a&gt;这篇文章中对计算机字符编码的原理和常用的字符编码都进行了说明，编码的本质就是为了方便信息的传输，以及计算机对于信息的处理。&lt;/p&gt;
&lt;p&gt;在文章中我也提到，而在 &lt;code&gt;HTML&lt;/code&gt; 里面，我们的每一个字符其实也都是以 &lt;code&gt;unicode&lt;/code&gt; 编码的形式保存和传递的。而 &lt;code&gt;HTML&lt;/code&gt;，&lt;code&gt;CSS&lt;/code&gt; 和 &lt;code&gt;JavaScript&lt;/code&gt; 都给我们提供了对应的直接使用 &lt;code&gt;unicode&lt;/code&gt; 的方法，具体使用方法可以看&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/26/character-encoding/&quot; title=&quot;搞懂字符编码&quot;&gt;搞懂字符编码&lt;/a&gt;这篇文章。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;HTML&lt;/code&gt; 中的 &lt;code&gt;unicode&lt;/code&gt; 使用一般被称为 &lt;code&gt;HTML character entity&lt;/code&gt;，是用 &lt;code&gt;unicode&lt;/code&gt; 的字符串来表示对应的字符，使用情况主要是应对一些 &lt;code&gt;reserved characters&lt;/code&gt; （保留字符），比如在 &lt;code&gt;HTML&lt;/code&gt; 中的 &lt;code&gt;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&lt;/code&gt;，他们出现在 &lt;code&gt;HTML&lt;/code&gt; 文档中时会被浏览器识别为标签，为了显示这些保留字，我们需要使用 &lt;code&gt;entity&lt;/code&gt;。还有一种情况是我们要显示一些无法用键盘输入的特殊字符。&lt;/p&gt;
&lt;h2&gt;HTML entity 的使用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;character entity&lt;/code&gt; 表示字符的方式有两种，一种是直接用 &lt;code&gt;unicode&lt;/code&gt; 的形式表示字符，另一种是在 &lt;code&gt;HTML&lt;/code&gt; 标准里面为了方便使用已经被命名的 &lt;code&gt;entity&lt;/code&gt;，他们的使用方法如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;unicode&lt;/code&gt;：&lt;code&gt;&amp;#x26;#nnnn;&lt;/code&gt; 或者 &lt;code&gt;&amp;#x26;#xhhhh;&lt;/code&gt;，前者是 &lt;code&gt;unicode&lt;/code&gt; 编码的十进制，后者是十六进制。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：&lt;code&gt;&amp;#x26;name;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;两种方式并不冲突，他们可以用来表示同一个字符，&lt;code&gt;name&lt;/code&gt; 的形式是 &lt;code&gt;HTML&lt;/code&gt; 标准替一些常用的符号直接给出了命名，方便记忆。因为 &lt;code&gt;unicode&lt;/code&gt; 需要使用的话每次都需要去查询，而命名的话比较方便记忆。比如 &lt;code&gt;©&lt;/code&gt; 可以用 &lt;code&gt;# copy;&lt;/code&gt; 也可以用 &lt;code&gt;&amp;#x26;# 169;&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 里面表示二进制用 &lt;code&gt;0bXXXXXXX&lt;/code&gt;，八进制用 &lt;code&gt;0XXXXXX&lt;/code&gt;，十六进制 &lt;code&gt;0xXXXXXXX&lt;/code&gt;，&lt;code&gt;X&lt;/code&gt; 表示数字。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;想要查询字符和实体之间对应的编码和解码可以到&lt;a href=&quot;https://mothereff.in/html-entities&quot; title=&quot;HTML entity encoder/decoder&quot;&gt;HTML entity encoder/decoder&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;最后在分享两个前端的标准制定协会&lt;a href=&quot;https://en.wikipedia.org/wiki/World_Wide_Web_Consortium&quot; title=&quot;W3C - Wikipedia&quot;&gt;W3C - Wikipedia&lt;/a&gt; 和 &lt;a href=&quot;https://en.wikipedia.org/wiki/WHATWG&quot; title=&quot;WHATWG - Wikipedia&quot;&gt;WHATWG - Wikipedia&lt;/a&gt;给出的 &lt;code&gt;HTML Named character references&lt;/code&gt;，&lt;code&gt;WHATWG&lt;/code&gt; 主要是维护 &lt;code&gt;HTML&lt;/code&gt; 相关的标准，&lt;code&gt;W3C&lt;/code&gt; 则是维护几乎所有跟 &lt;code&gt;Web&lt;/code&gt; 有关的标准，目前他们已经开始合作推动 &lt;code&gt;web&lt;/code&gt; 标准：&lt;a href=&quot;https://www.w3.org/blog/2019/05/w3c-and-whatwg-to-work-together-to-advance-the-open-web-platform/&quot; title=&quot;W3C AND WHATWG TO WORK TOGETHER TO ADVANCE THE OPEN WEB PLATFORM&quot;&gt;W3C AND WHATWG TO WORK TOGETHER TO ADVANCE THE OPEN WEB PLATFORM&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references&quot; title=&quot;HTML Named character references - WHATWG&quot;&gt;HTML Named character references - WHATWG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.w3.org/html5/html-author/charref&quot; title=&quot;- W3C&quot;&gt;HTML Named character references- W3C&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在使用 &lt;code&gt;markdown&lt;/code&gt; 的时候有时候会遇到需要转义一些有特殊含义字符的需要比如 &lt;code&gt;$&lt;/code&gt;，或者是在代码块里面要 加入转义的内容，可以用使用 &lt;code&gt;&amp;#x3C;code&gt;&amp;#x3C;/code&gt;&lt;/code&gt; 中间加入 &lt;code&gt;character entity&lt;/code&gt;。当你的 &lt;code&gt;markdown&lt;/code&gt; 解析出现问题的时候可以看一看是不是某些有含义的字符解析出错导致的，如果是的话， 将这些字符换成字符实体即可。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;entity&lt;/code&gt; 中使用最多的应该是 &lt;code&gt;&amp;#x3C; ;&lt;/code&gt;（&lt;code&gt;lt&lt;/code&gt;&gt;（&lt;code&gt;lt&lt;/code&gt; 就是 &lt;code&gt;left tag&lt;/code&gt;）的意思，当我们想在文档中插入标签结构的时候，如果直接使用 &lt;code&gt;&amp;#x3C;&gt;&lt;/code&gt; 会被解析成元素，此时我们可以将左标签用 &lt;code&gt;&amp;#x3C; ;&lt;/code&gt; 来代替 &lt;code&gt;&amp;#x3C;&lt;/code&gt;，就可以正常显示了。比如下面的代码用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;pre&gt;
    &amp;#x3C; ;html&gt;
        &amp;#x3C; ;head&gt;
            &amp;#x3C; ;title&gt;this is a title&amp;#x3C;/title&gt;
        &amp;#x3C; ;/head&gt;
        &amp;#x3C; ;body&gt;this is a body&amp;#x3C;/body&gt;
    &amp;#x3C; ;/html&gt;
&amp;#x3C;/pre&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;由于实体在 &lt;code&gt;markdown&lt;/code&gt; 会解析成对应的字符，所以我在 &lt;code&gt;;&lt;/code&gt; 前留一个空格。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Entity&quot; title=&quot;Entity-MDN&quot;&gt;Entity-MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/html4/charset.html#entities&quot; title=&quot;HTML Document Representation&quot;&gt;HTML Document Representation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references&quot; title=&quot;List of XML and HTML character entity references - Wikipedia&quot;&gt;List of XML and HTML character entity references - Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/W3C.6iu9Wlfx.png"/><enclosure url="/_astro/W3C.6iu9Wlfx.png"/></item><item><title>translate-shell 常用命令</title><link>https://clloz.com/blog/translate-shell-usage</link><guid isPermaLink="true">https://clloz.com/blog/translate-shell-usage</guid><pubDate>Mon, 18 Nov 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我在 &lt;code&gt;Mac&lt;/code&gt; 的命令行里面使用的翻译工具是&lt;a href=&quot;https://www.npmjs.com/package/fanyi&quot; title=&quot;fanyi&quot;&gt;fanyi&lt;/a&gt;，安装方法为 &lt;code&gt;npm install -g fanyi&lt;/code&gt;，这个工具只能进行中英文翻译，翻译的来源是&lt;a href=&quot;http://www.iciba.com/&quot; title=&quot;金山词霸&quot;&gt;金山词霸&lt;/a&gt;和&lt;a href=&quot;http://fanyi.youdao.com/&quot; title=&quot;有道词典&quot;&gt;有道词典&lt;/a&gt;，如果只是要翻译英文的话其实挺好用的。不过我日常经常需要查询日语，&lt;code&gt;Mac&lt;/code&gt; 自带的 &lt;code&gt;dictionary&lt;/code&gt; 里面的日语字典只有 &lt;code&gt;大辞林&lt;/code&gt; 和 &lt;code&gt;日英字典&lt;/code&gt;，所以对我来说翻译日语经常需要用 &lt;code&gt;Google&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;今天给大家介绍另一个功能更强的命令行翻译工具，叫做&lt;a href=&quot;https://github.com/soimort/translate-shell&quot; title=&quot;Translate Shell&quot;&gt;Translate Shell&lt;/a&gt;，它的翻译来源主要是 &lt;code&gt;Google Translate (default)&lt;/code&gt;, &lt;code&gt;Bing Translator&lt;/code&gt;, &lt;code&gt;Yandex.Translate&lt;/code&gt;, 和 &lt;code&gt;Apertium&lt;/code&gt;。它的功能要比 &lt;code&gt;fanyi&lt;/code&gt; 强很多，它能够在多种语言之间进行翻译，也能翻译句子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ trans &apos;Saluton, Mondo!&apos;
Saluton, Mondo!

Hello, World!

Translations of Saluton, Mondo!
[ Esperanto -&gt; English ]
Saluton ,
    Hello,
Mondo !
    World!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以进行简要翻译，不限时那么多结果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ trans -brief &apos;Saluton, Mondo!&apos;
Hello, World!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以在命令行进入翻译交互的状态，不用每次都输入命令，在阅读文章或资料的时候很有用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ trans -shell -brief
&gt; Rien ne réussit comme le succès.
Nothing succeeds like success.
&gt; Was mich nicht umbringt, macht mich stärker.
What does not kill me makes me stronger.
&gt; Юмор есть остроумие глубокого чувства.
Humor has a deep sense of wit.
&gt; 學而不思則罔，思而不學則殆。
Learning without thought is labor lost, thought without learning is perilous.
&gt; 幸福になるためには、人から愛されるのが一番の近道。
In order to be happy, the best way is to be loved by people.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Translate Shell 的安装和使用&lt;/h2&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;官方提供了三种安装方法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载&lt;a href=&quot;http://git.io/trans&quot; title=&quot;the self-contained executable&quot;&gt;the self-contained executable&lt;/a&gt;，然后放置到 &lt;code&gt;path&lt;/code&gt; 对应的位置。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wget git.io/trans
chmod +x ./trans
sudo mv trans /usr/bin/
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;通过 &lt;code&gt;Git&lt;/code&gt; 安装，克隆 &lt;code&gt;Translate Shell&lt;/code&gt; 的 &lt;code&gt;GitHub&lt;/code&gt; 仓库然后手工编译。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git clone https://github.com/soimort/translate-shell
$ cd translate-shell/
$ make
$ [sudo] make install
# In case you have only zsh but not bash in your system, build with:
$ make TARGET=zsh
# The default PREFIX of installation is /usr/local. To install the program to somewhere else (e.g. /usr, ~/.local), use:
$ [sudo] make PREFIX=/usr install
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;通过包管理工具安装，有些发行版的官方仓库中包含了 &lt;code&gt;Translate Shell&lt;/code&gt;，可以通过包管理器来安装。在 &lt;code&gt;Mac&lt;/code&gt; 上直接通过 &lt;code&gt;homebrew&lt;/code&gt; 安装即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install translate-shell
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Translate Shell 的使用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Google Translate&lt;/code&gt; 能够自动识别待翻译文本，&lt;code&gt;Translate Shell&lt;/code&gt; 会将系统 &lt;code&gt;locale&lt;/code&gt; 中的语言作为目标语言。所以如果你可以根据你的日常翻译需求来设置 &lt;code&gt;locale&lt;/code&gt;，一般来说就设为英语就可以了。当然不设置也可以，在使用 &lt;code&gt;Translate Shell&lt;/code&gt; 的时候加上目标语言参数就可以了&lt;/p&gt;
&lt;p&gt;这里科普一下 &lt;code&gt;locale&lt;/code&gt;，区域设置（&lt;code&gt;locale&lt;/code&gt;），也称作“本地化策略集”、“本地环境”，是表达程序用户地区方面的软件设定。通常一个区域设置标识符至少包括一个语言标识符和一个区域标识符。在&lt;code&gt;UNIX&lt;/code&gt; 和 &lt;code&gt;Windows&lt;/code&gt; 中，区域设置的控制是不同的。在 &lt;code&gt;UNIX&lt;/code&gt; 下，通常通过环境变量来控制区域设置。这些环境变量包括：&lt;code&gt;LC_ALL&lt;/code&gt;, &lt;code&gt;LC_CTYPE&lt;/code&gt;, &lt;code&gt;LC_TIME&lt;/code&gt;, 等等。你可以通过改变这些环境变量来控制你的程序或者命令所表现出来的区域设置，前提是这些程序或者命令必须是已经被国际化的和本地化的。在 &lt;code&gt;Windows&lt;/code&gt; 下，你可以通过改变控制面板上的“语言/区域”中的区域的值来设定 &lt;code&gt;Windows&lt;/code&gt; 的当前用户的区域设置。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Mac&lt;/code&gt; 中设置英文 &lt;code&gt;locale&lt;/code&gt; 的方法如下，在 &lt;code&gt;.bash_profile&lt;/code&gt; 中加入如下内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ vim ~/.bash_profile
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8

$ source ~/.bash_profile
# 再次查看locale
$ locale
LANG=&quot;en_US.UTF-8&quot;
LC_COLLATE=&quot;en_US.UTF-8&quot;
LC_CTYPE=&quot;en_US.UTF-8&quot;
LC_MESSAGES=&quot;en_US.UTF-8&quot;
LC_MONETARY=&quot;en_US.UTF-8&quot;
LC_NUMERIC=&quot;en_US.UTF-8&quot;
LC_TIME=&quot;en_US.UTF-8&quot;
LC_ALL=&quot;en_US.UTF-8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;查看语言代码&lt;/h5&gt;
&lt;p&gt;想要翻译到对应的非 &lt;code&gt;locale&lt;/code&gt; 语言，需要加入对应语言的参数，查看参数的方法如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ trans -R
┌───────────────────────┬───────────────────────┬───────────────────────┐
│ Afrikaans      - af │ Hebrew         - he │ Portuguese     - pt │
│ Albanian       - sq │ Hill Mari      - mrj │ Punjabi        - pa │
│ Amharic        - am │ Hindi          - hi │ Querétaro Otomi- otq │
│ Arabic         - ar │ Hmong          - hmn │ Romanian       - ro │
│ Armenian       - hy │ Hmong Daw      - mww │ Russian        - ru │
│ Azerbaijani    - az │ Hungarian      - hu │ Samoan         - sm │
│ Bashkir        - ba │ Icelandic      - is │ Scots Gaelic   - gd │
│ Basque         - eu │ Igbo           - ig │ Serbian (Cyr...-sr-Cyrl
│ Belarusian     - be │ Indonesian     - id │ Serbian (Latin)-sr-Latn
│ Bengali        - bn │ Irish          - ga │ Sesotho        - st │
│ Bosnian        - bs │ Italian        - it │ Shona          - sn │
│ Bulgarian      - bg │ Japanese       - ja │ Sindhi         - sd │
│ Cantonese      - yue │ Javanese       - jv │ Sinhala        - si │
│ Catalan        - ca │ Kannada        - kn │ Slovak         - sk │
│ Cebuano        - ceb │ Kazakh         - kk │ Slovenian      - sl │
│ Chichewa       - ny │ Khmer          - km │ Somali         - so │
│ Chinese Simp...- zh-CN│ Klingon        - tlh │ Spanish        - es │
│ Chinese Trad...- zh-TW│ Klingon (pIqaD)tlh-Qaak Sundanese      - su │
│ Corsican       - co │ Korean         - ko │ Swahili        - sw │
│ Croatian       - hr │ Kurdish        - ku │ Swedish        - sv │
│ Czech          - cs │ Kyrgyz         - ky │ Tahitian       - ty │
│ Danish         - da │ Lao            - lo │ Tajik          - tg │
│ Dutch          - nl │ Latin          - la │ Tamil          - ta │
│ Eastern Mari   - mhr │ Latvian        - lv │ Tatar          - tt │
│ Emoji          - emj │ Lithuanian     - lt │ Telugu         - te │
│ English        - en │ Luxembourgish  - lb │ Thai           - th │
│ Esperanto      - eo │ Macedonian     - mk │ Tongan         - to │
│ Estonian       - et │ Malagasy       - mg │ Turkish        - tr │
│ Fijian         - fj │ Malay          - ms │ Udmurt         - udm │
│ Filipino       - tl │ Malayalam      - ml │ Ukrainian      - uk │
│ Finnish        - fi │ Maltese        - mt │ Urdu           - ur │
│ French         - fr │ Maori          - mi │ Uzbek          - uz │
│ Frisian        - fy │ Marathi        - mr │ Vietnamese     - vi │
│ Galician       - gl │ Mongolian      - mn │ Welsh          - cy │
│ Georgian       - ka │ Myanmar        - my │ Xhosa          - xh │
│ German         - de │ Nepali         - ne │ Yiddish        - yi │
│ Greek          - el │ Norwegian      - no │ Yoruba         - yo │
│ Gujarati       - gu │ Papiamento     - pap │ Yucatec Maya   - yua │
│ Haitian Creole - ht │ Pashto         - ps │ Zulu           - zu │
│ Hausa          - ha │ Persian        - fa │                       │
│ Hawaiian       - haw │ Polish         - pl │                       │
└───────────────────────┴───────────────────────┴───────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以通过 &lt;code&gt;trans -T&lt;/code&gt; 来查询，不过语言名称显示的就不是英语了，而是对应的文字，比如 &lt;code&gt;Japanese&lt;/code&gt; 显示的就是 &lt;code&gt;日本語&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;基础用法&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 翻译到locale语言
$ trans [Words]

# 翻译到指定语言
$ trans :zh [word]

# 翻译到多种目标语言
$ trans :zh+ja word

# :可用=代替，但是在zsh中=是有特殊含义的，需要用引号或{}转义。:也可以用参数-t代替
$ trans {=zh+ja} word
$ trans &apos;=zh+ja&apos; word
$ trans -t zh+ja word
$ trans -t japanese word
$ trans -t 日本語 word

# Google Translate 可能无法准确分辨你的待翻译文本的语言，最好用后置冒号指定，也可以使用参数-s。
$ trans ja: 手紙
$ trans zh: 手紙
$ trans -s ja 手紙

# 翻译句子和短语
$ trans en:zh &quot;word processor&quot;
$ trans :zh &quot;To-morrow, and to-morrow, and to-morrow,&quot;

# 简洁模式：默认情况下，Translate Shell 尽可能多的显示翻译信息。如果你希望只显示简要信息，只需要加上 -b选项。
$ trans -b :fr &quot;Saluton, Mondo&quot;
$ trans -b :@ja &quot;Saluton, Mondo&quot; #显示发音符号，如果有的话，如日语罗马音，汉语拼音

# 字典模式：当待翻译文本和目标语言一致的时候进入字典模式，或者使用-d参数，不是每种语言都支持
$ trans :en word
$ trans -d fr: mot

# 识别源文本的语言，用-id参数
$ trans -id 言葉

# 听取翻译结果或源文本的发音
$ trans -b -p :ja &quot;Saluton, Mondo&quot;
$ trans -sp &quot;你好，世界&quot;

# 翻译文件或网页
$ trans :fr file://input.txt
$ trans :fr http://www.w3.org/

# 查看语言的详情
$ trans -L fr
$ trans -L de+en

# 进入交互模式
$ trans -shell
$ trans -shell en:fr
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;跟 &lt;code&gt;fanyi&lt;/code&gt; 比起来，&lt;code&gt;Translate Shell&lt;/code&gt; 功能更强大，不过美中不足的是，它的英语翻译没有音标，&lt;code&gt;fanyi&lt;/code&gt; 有英音和美音的音标，具体的使用可以根据自己的需求来调整。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/soimort/translate-shell&quot; title=&quot;Translate Shell&quot;&gt;Translate Shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.cn/article-9107-1.html&quot; title=&quot;Translate Shell ：一款在 Linux 命令行中使用谷歌翻译的工具&quot;&gt;Translate Shell ：一款在 Linux 命令行中使用谷歌翻译的工具&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/terminal.BZCEBg10.png"/><enclosure url="/_astro/terminal.BZCEBg10.png"/></item><item><title>USB标准版本和接口类型</title><link>https://clloz.com/blog/usb-standard-and-connector-type</link><guid isPermaLink="true">https://clloz.com/blog/usb-standard-and-connector-type</guid><pubDate>Sun, 17 Nov 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;对于 &lt;code&gt;usb&lt;/code&gt; （&lt;code&gt;Universal Serial Bus&lt;/code&gt;）大家都非常熟悉，通用串行总线，是连接计算机系统与外部设备的一种串口总线标准，也是一种输入输出接口的技术规范，被广泛地应用于个人电脑和移动设备等信息通讯产品，并扩展至摄影器材、数字电视（机顶盒）、游戏机等其它相关领域。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;10&lt;/code&gt; 年我进入大学的时候，大部分电脑还是标配 &lt;code&gt;usb2.0&lt;/code&gt; 的接口，后来才慢慢出了 &lt;code&gt;usb3.0&lt;/code&gt;，&lt;code&gt;usb3.1&lt;/code&gt; 等，现在接口越来越多，包括我知道的早起安卓使用的 &lt;code&gt;micro usb&lt;/code&gt;，现在的 &lt;code&gt;type c&lt;/code&gt; 接口。而苹果的接口虽然不在 &lt;code&gt;usb&lt;/code&gt; 标准里面，但是苹果的设备市场占有率也很大，他的 &lt;code&gt;lighting接口&lt;/code&gt;，&lt;code&gt;thunderbolt接口&lt;/code&gt; 使用率也很高，很多时候这么多的接口类型让人困惑，本文就来讲一讲 &lt;code&gt;usb&lt;/code&gt; 标准的版本和接口类型。&lt;/p&gt;
&lt;h2&gt;串口和并口&lt;/h2&gt;
&lt;p&gt;不管什么接口，作用都是为了连接外部设备，然后传递数据，而接口发展的目标当然是能够连接更多不同的设备以及获得更高的传输速率。但凡事都要考虑实现的难度和实现的成本，很多时候由于技术或成本的限制我们只能在具体的应用场景采取最合适的方案。串口和并口就是在早起传输速率不是很快的情况下，针对不同的应用场景采取的不同方案。&lt;/p&gt;
&lt;p&gt;串口和并口的定义：在一个独立的信道上，每次同时传输1bit为串口，每次同时传输多个bit为并口。从定义上看，并口的传输速率似乎要比串口快，但是为什么在大部分应用场景下并口已经慢慢被淘汰了呢。我们知道就是在我们使用的 &lt;code&gt;usb&lt;/code&gt; 以及以前的主办上的并行接口，串行接口内部都是有很多条线路的（比如 &lt;code&gt;usb&lt;/code&gt; 或者 &lt;code&gt;lighting接口&lt;/code&gt; 上的触电），他们有的用于供电，有的用于接地，有的用于传输信号，还可能有一些其他操作比如控制等。我们想要提高接口的传输速率无非两种方法，一种是提高单根线路的传输速率，另一种是增加线的数量。但是在并行接口的实践中已经发现，多条线路的信号会相互干扰，并且传输距离要有限制，不能太远，比如主板上的并行接口一次传 &lt;code&gt;8bits&lt;/code&gt;，如果其中一个信号出错，&lt;code&gt;8&lt;/code&gt; 个全部要重发。而串行的结构要简单很多，干扰的问题要小很多，同时成本也更低，我们只要提高单根线路的传输速率就好了，这要比解决干扰的问题成本小很多，而且并口的多条线路之间还要解决数据同步的问题。&lt;/p&gt;
&lt;p&gt;简单的说就是串口形容一下就是一条车道，而并口就是有 &lt;code&gt;8&lt;/code&gt; 个车道同一时刻能传送8位（一个字节）数据。但是并不是说并口快，由于 &lt;code&gt;8&lt;/code&gt; 位通道之间的互相干扰（串扰），传输时速度就受到了限制，传输容易出错。串口没有互相干扰。并口同时发送的数据量大，但要比串口慢。如果并口的干扰问题解决，同时单线速率能够跟串口相同，它的传输速率必然更快，这也是在 &lt;code&gt;21&lt;/code&gt; 世纪之前，在单根线路传输速度很慢的情况下，在需要较大传输速度的地方，例如打印机，并口得到广泛使用的原因。但是当前，并口只能在一些需求特殊的场景下发挥作用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;串行接口、并行接口是按照数据传输方式来划分的，串行接口是一大类接口。&lt;code&gt;USB&lt;/code&gt;、&lt;code&gt;RS232&lt;/code&gt;、&lt;code&gt;SATA&lt;/code&gt;、&lt;code&gt;PS/2&lt;/code&gt;、&lt;code&gt;RS485&lt;/code&gt; 等等，这些都属于串行接口；但一般情况下，如果没有特殊说明而只是说“串口”的话，通常特指 &lt;code&gt;RS232&lt;/code&gt; 接口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;usb 标准和接口类型&lt;/h2&gt;
&lt;p&gt;多媒体电脑刚问世时，外接式设备的传输接口各不相同，如打印机只能接 &lt;code&gt;LPT&lt;/code&gt;、调制解调器只能接 &lt;code&gt;RS232&lt;/code&gt;、鼠标键盘只能接 &lt;code&gt;PS/2&lt;/code&gt; 等。繁杂的接口系统，加上必须安装驱动程序并重启才能使用的限制，都会造成用户的困扰。因此，创造出一个统一且支持易插拔的外接式传输接口，便成为无可避免的趋势，&lt;code&gt;USB&lt;/code&gt; 应运而生。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;USB&lt;/code&gt; 最初是由英特尔与微软倡导发起，最大的特点是尽可能得实现热插拔和即插即用。当设备插入时，主机枚举到此设备并加载所需的驱动程序，因此其在使用上远比 &lt;code&gt;PCI&lt;/code&gt; 和 &lt;code&gt;ISA&lt;/code&gt; 等总线方便。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;USB&lt;/code&gt; 可以连接的外设有鼠标、键盘、游戏手柄、游戏杆、扫描仪、数字相机、打印机、硬盘和网络等部件。对数字相机这样的多媒体外设 &lt;code&gt;USB&lt;/code&gt; 已经是缺省接口；由于大大简化与计算机的连接，&lt;code&gt;USB&lt;/code&gt; 也逐步取代并行接口成为打印机的主流连接方式之一。&lt;code&gt;2004&lt;/code&gt; 年已经有超过 &lt;code&gt;1亿&lt;/code&gt; 台 &lt;code&gt;USB&lt;/code&gt; 设备；到 &lt;code&gt;2007&lt;/code&gt; 年时，高清晰度数字视频外设是仅有的 &lt;code&gt;USB&lt;/code&gt; 未能染指的外设类别，因为他需要更高的传输速率，不过 &lt;code&gt;USB3.1&lt;/code&gt; 和 &lt;code&gt;2019&lt;/code&gt; 年 &lt;code&gt;USB4&lt;/code&gt; 的问世，高清晰度数字视频外设和外接式显卡也能在 &lt;code&gt;USB&lt;/code&gt; 播放。&lt;/p&gt;
&lt;p&gt;现 &lt;code&gt;USB&lt;/code&gt; 标准中，按照速度等级和连接方式分为以下七种版本。注意 &lt;code&gt;USB-IF&lt;/code&gt; （&lt;code&gt;USB&lt;/code&gt;开发者论坛，&lt;code&gt;USB&lt;/code&gt;标准的制定组织）当前正式的主版本号只有 &lt;code&gt;USB 2.0&lt;/code&gt; 和 &lt;code&gt;USB 3.2&lt;/code&gt; 两个。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/usb2.Bslzt-bW_11h5IF.webp&quot; alt=&quot;usb-version&quot; title=&quot;usb-version&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/USB-IF&quot; title=&quot;USB开发者论坛&quot;&gt;USB开发者论坛&lt;/a&gt;负责 &lt;code&gt;USB&lt;/code&gt; 标准制订，其成员包括：&lt;code&gt;Apple&lt;/code&gt;、&lt;code&gt;HP&lt;/code&gt;、&lt;code&gt;NEC&lt;/code&gt;、&lt;code&gt;Microsoft&lt;/code&gt; 和 &lt;code&gt;Intel&lt;/code&gt;。&lt;code&gt;2001&lt;/code&gt; 年底，&lt;code&gt;USB-IF&lt;/code&gt; 公布 &lt;code&gt;USB 2.0&lt;/code&gt; 规范，与之前的 &lt;code&gt;USB 0.9&lt;/code&gt;、&lt;code&gt;USB 1.0&lt;/code&gt; 和 &lt;code&gt;USB 1.1&lt;/code&gt; 一样，该规范完全向下兼容。随后，&lt;code&gt;USB-IF&lt;/code&gt; 公布 &lt;code&gt;USB On-The-Go&lt;/code&gt;（&lt;code&gt;USB OTG&lt;/code&gt;，当前版本：&lt;code&gt;1.0a&lt;/code&gt;）作为 &lt;code&gt;USB 2.0&lt;/code&gt; 规范的补充标准，使其能够用于在便携设备之间直接交换数据。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;USB&lt;/code&gt; 的连接器分为 &lt;code&gt;A&lt;/code&gt;、&lt;code&gt;B&lt;/code&gt; 两种，分别用于主机和设备；其各自的小型化的连接器是 &lt;code&gt;Mini-A&lt;/code&gt;, &lt;code&gt;Mini-B&lt;/code&gt; 和 &lt;code&gt;Micro-A&lt;/code&gt;, &lt;code&gt;Micro-B&lt;/code&gt;，另外还有 &lt;code&gt;Mini-AB&lt;/code&gt;（可同时支持 &lt;code&gt;Mini-A&lt;/code&gt; 及 &lt;code&gt;Mini-B&lt;/code&gt;）的插口。&lt;code&gt;USB 3.1&lt;/code&gt; 版本中引入了支持正反面不区分插入的 &lt;code&gt;C&lt;/code&gt; 型。每一种连接器有对应的公口和母口，并且我们用来连接两种不同设备的 &lt;code&gt;USB&lt;/code&gt; 线两端会用不同的连接器，这些内容在 &lt;code&gt;USB-IF&lt;/code&gt; 都有规定。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;紫色的 &lt;code&gt;Type-C&lt;/code&gt; 充电速度最高支持 &lt;code&gt;5A&lt;/code&gt; 充电、充电功率最高达 &lt;code&gt;100W&lt;/code&gt;；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体的版本和对应的接口看下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/usb1.DxVWoxz8_1Dn4u8.webp&quot; alt=&quot;usb-connector&quot; title=&quot;usb-connector&quot;&gt;&lt;/p&gt;
&lt;p&gt;从上面的内容中可以看出，所谓的 &lt;code&gt;2.0&lt;/code&gt;，&lt;code&gt;3.0&lt;/code&gt;，&lt;code&gt;3.1&lt;/code&gt;，&lt;code&gt;3.2&lt;/code&gt; 对应的就是 &lt;code&gt;USB-IF&lt;/code&gt; 制定的新的 &lt;code&gt;USB&lt;/code&gt; 标准的命名，而所谓的 &lt;code&gt;type C&lt;/code&gt;，&lt;code&gt;type A&lt;/code&gt; 则是在某个标准实现下具体使用的物理接口（连接器），同一个标准会为不同的设备设计不同的接头。不过几家参与制定标准的大厂都是向着统一接口的目标努力的。&lt;/p&gt;
&lt;h2&gt;USB Connectors 接头&lt;/h2&gt;
&lt;p&gt;单独说一下接头，其实真正让使用者搞不清的其实并不是 &lt;code&gt;USB&lt;/code&gt; 标准的版本，其实大部分用户也不会去真的把版本区分那么清楚，只要大致知道哪个快哪个慢就行了。真正让大家搞不清的是花样百出的各种接头，&lt;code&gt;Android&lt;/code&gt; 上的早起 &lt;code&gt;Micro-B&lt;/code&gt; 和现在的 &lt;code&gt;Type-C&lt;/code&gt;，&lt;code&gt;Type-A&lt;/code&gt; 里面的 &lt;code&gt;2.0&lt;/code&gt;，&lt;code&gt;3.0&lt;/code&gt;，&lt;code&gt;3.1&lt;/code&gt;，&lt;code&gt;3.2&lt;/code&gt;，再加上苹果自己的 &lt;code&gt;lighting&lt;/code&gt; 和 &lt;code&gt;thunderbolt&lt;/code&gt;，种类繁多，眼花缭乱。&lt;/p&gt;
&lt;p&gt;首先我们要知道接头是由 &lt;code&gt;USB-IF&lt;/code&gt; 所指定，接头的设计一方面为了支持众多 &lt;code&gt;USB&lt;/code&gt; 的基本需求，另一方面也避免以往许多类似串行接头所出现的问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接头设计的相当耐用。许多以往使用的接头较脆弱，即使受力不大，有时针脚或零件也会折弯甚至断裂。而 &lt;code&gt;USB&lt;/code&gt; 接头的金属导电部分周围有塑料作为保护，而且整个连接部分被金属的保护套围住，因此 &lt;code&gt;USB&lt;/code&gt; 接头不论插拔，都不容易受损。由于金属保护套和外围塑料护套的保护，需要较大的力量才能造成 &lt;code&gt;USB&lt;/code&gt; 接头明显的损坏。&lt;/li&gt;
&lt;li&gt;具有防呆设计，方向相反的插头不可能插到插座里，方向正反很容易感觉出来。所以不可能把 &lt;code&gt;USB&lt;/code&gt; 接口插错。&lt;/li&gt;
&lt;li&gt;接头能相对便宜地大量生产。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;USB&lt;/code&gt; 网络中，接头被强制使用定向拓扑。&lt;code&gt;USB&lt;/code&gt; 不支持环形网络，因此不兼容的 &lt;code&gt;USB&lt;/code&gt; 设备之间接口也不兼容。不像其他通讯系统（如 &lt;code&gt;RJ-45&lt;/code&gt; 电缆）不能使用转换插头，防止环形 &lt;code&gt;USB&lt;/code&gt; 网络产生。&lt;/li&gt;
&lt;li&gt;适度的插拔力。&lt;code&gt;USB&lt;/code&gt; 电缆和小型 &lt;code&gt;USB&lt;/code&gt; 设备能被插口卡住（不需要夹子、螺丝或者其他接口那样的锁扣）。只需要适当力量插拔即可连接周边设备。&lt;/li&gt;
&lt;li&gt;由于接头的构造，在将 &lt;code&gt;USB&lt;/code&gt; 插头插入 &lt;code&gt;USB&lt;/code&gt; 座时，插头外面的金属保护套会先接触到 &lt;code&gt;USB&lt;/code&gt; 座内对应的金属部分，之后插头内部的四个触点才会接触到 &lt;code&gt;USB&lt;/code&gt; 座。金属保护套会连接到系统的地线，提供路径使静电可以放电，避免因静电通过电子零件而造成损坏。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;USB&lt;/code&gt; 电缆最长允许 &lt;code&gt;5&lt;/code&gt; 米，更长的距离需要 &lt;code&gt;HUB&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;USB&lt;/code&gt; 的连接器的插头和插座配对，以及连接不同设备的 &lt;code&gt;USB&lt;/code&gt; 线的两端连接器标准都是有规定的，见下两张图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/usb5.D88Pf_vW_Z1CbEMf.webp&quot; alt=&quot;usb-connector1&quot; title=&quot;usb-connector1&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/usb3.DyCyawuq_Z1jNpEY.webp&quot; alt=&quot;usb-connector2&quot; title=&quot;usb-connector2&quot;&gt;&lt;/p&gt;
&lt;p&gt;大部分的连接器都是只能和合身对应的插头插座连接，&lt;code&gt;USB 3.0&lt;/code&gt; 的插座大部分都向下兼容。而在 &lt;code&gt;USB&lt;/code&gt; 线的部分我们可以看到应用的最广泛的 &lt;code&gt;Type-A&lt;/code&gt; 和 &lt;code&gt;Type-C&lt;/code&gt; 是适用场景最多的。&lt;/p&gt;
&lt;p&gt;这里提一下 &lt;code&gt;USB 4&lt;/code&gt;，&lt;code&gt;USB 4&lt;/code&gt; 于 &lt;code&gt;2019年9月3日&lt;/code&gt; 发布。采用 &lt;code&gt;Thunderbolt 3&lt;/code&gt; 协议规格，使 &lt;code&gt;Thunderbolt 3&lt;/code&gt;设备将能兼容于 &lt;code&gt;USB 4&lt;/code&gt;，现有 &lt;code&gt;3.2&lt;/code&gt; 及 &lt;code&gt;2.0&lt;/code&gt; 也向下兼容。速度方面加倍来到两条通道总共 &lt;code&gt;40Gb/s&lt;/code&gt; 的传输速度。&lt;/p&gt;
&lt;h2&gt;苹果的接口&lt;/h2&gt;
&lt;p&gt;最后单独说一下苹果的接口，苹果作为一家特立独行，什么东西都自己来的公司，它的接口也都是自己的一套标准。目前在 &lt;code&gt;iphone&lt;/code&gt;，&lt;code&gt;ipad&lt;/code&gt;，&lt;code&gt;ipod&lt;/code&gt; 还有一系列周边设备上使用的都是苹果自己的闪电接口 &lt;code&gt;lighting&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;闪电是由苹果公司所制作的专属连接器规格，首次出现在 &lt;code&gt;2012&lt;/code&gt; 年所发表的 &lt;code&gt;iPhone 5&lt;/code&gt;、 &lt;code&gt;iPod Touch&lt;/code&gt; 及 &lt;code&gt;iPod nano&lt;/code&gt; 等新款手持式消费性电子产品。此连接器针脚为 &lt;code&gt;8 pin&lt;/code&gt;，正反面皆可插，尺寸与 &lt;code&gt;Micro USB&lt;/code&gt; 相近。闪电取代了使用多年的 &lt;code&gt;30pin&lt;/code&gt; 连接器。闪电连接器也是移动市场首个成为主流的正反可插接口，由于插头采用了对称式设计，所以插头的上下两面均分布有相同的针脚。无论用户以何种方向将插头插入接口，其中一组针脚都会同基座中的针脚相连接。当前还有 &lt;code&gt;USB Type-C&lt;/code&gt; 也跟着采用这种设计。不同的是，闪电接头的引脚在外面，&lt;code&gt;USB-C&lt;/code&gt; 接头的引脚则在里面，且闪电的插座引脚只有一边，&lt;code&gt;USB-C&lt;/code&gt; 则两边都有。&lt;/p&gt;
&lt;p&gt;相对于移动设备上的 &lt;code&gt;Lighting&lt;/code&gt;，在 &lt;code&gt;Mac&lt;/code&gt; 上苹果自家的接口就是 &lt;code&gt;Intel&lt;/code&gt; 发表的 &lt;code&gt;Thunderbolt&lt;/code&gt; 高速串行接口标准，是用来连接电脑和其他设备通用总线。&lt;code&gt;13&lt;/code&gt; 年的 &lt;code&gt;Macbook Pro&lt;/code&gt; 开始搭载 &lt;code&gt;thunderbolt2&lt;/code&gt;，&lt;code&gt;16&lt;/code&gt; 年的 &lt;code&gt;Macbook Pro&lt;/code&gt; 开始搭载 &lt;code&gt;Thunderbolt3&lt;/code&gt;，而 &lt;code&gt;Thunderbolt3&lt;/code&gt; 的 连接器也选择了 &lt;code&gt;Type-C&lt;/code&gt; 标准，而最新的 &lt;code&gt;USB 4&lt;/code&gt; 标准也采用了 &lt;code&gt;Thunderbolt 3&lt;/code&gt; 的协议。&lt;/p&gt;
&lt;p&gt;由于支持 &lt;code&gt;Thunderbolt 1, 2&lt;/code&gt; 的厂商不多，而且采用 &lt;code&gt;Thunderbolt&lt;/code&gt; 的设备大多是高端产品，价格昂贵，加上接口使用的是苹果 &lt;code&gt;Mini Displayport&lt;/code&gt;，配件无法用在其他电子设备，普及程度远低于对手 &lt;code&gt;USB&lt;/code&gt;。所以 &lt;code&gt;Thunderbolt 3&lt;/code&gt; 才与 &lt;code&gt;USB Type-C&lt;/code&gt; 的接头兼容，使 &lt;code&gt;Thunderbolt&lt;/code&gt; 接口变得更普及。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文整理了一些关于硬件接口的知识，看完后相信对于各种流行的高速串行接口的标准以及各种连接器的标准都能够有一定的了解。如有错漏，欢迎指正。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%B9%B6%E8%A1%8C%E7%AB%AF%E5%8F%A3&quot; title=&quot;并行端口&quot;&gt;并行端口&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/27815296/answer/38699109&quot; title=&quot;为什么串口比并口快？ - 又见山人的回答 - 知乎 &quot;&gt;为什么串口比并口快？ - 又见山人的回答 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/USB&quot; title=&quot;USB-维基百科&quot;&gt;USB-维基百科&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/Lightning%E6%8E%A5%E5%A4%B4&quot; title=&quot;闪电接头-维基百科&quot;&gt;闪电接头-维基百科&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/Thunderbolt&quot; title=&quot;Thunderbolt-维基百科&quot;&gt;Thunderbolt-维基百科&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/usb-logo-scaled.Bb_yZ0BI.png"/><enclosure url="/_astro/usb-logo-scaled.Bb_yZ0BI.png"/></item><item><title>mac和iphone之间传输文件</title><link>https://clloz.com/blog/apple-continuity-to-connect</link><guid isPermaLink="true">https://clloz.com/blog/apple-continuity-to-connect</guid><pubDate>Mon, 11 Nov 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;以前我在 &lt;code&gt;mac&lt;/code&gt; 和 &lt;code&gt;iphone&lt;/code&gt; 之间传输文件都是用 &lt;code&gt;icloud&lt;/code&gt;，如果是一些链接我则是通过微信的文件传输助手。不过这两天研究了一下 &lt;code&gt;apple&lt;/code&gt; 产品的文件传输功能，在官方的文档中找到了很多原来不知道的功能，本文整理和总结一下比较实用的交互方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文主要介绍使用方法，具体的版本要求和使用条件请参阅下方的 &lt;code&gt;apple&lt;/code&gt; 官方文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Handoff 接力&lt;/h2&gt;
&lt;p&gt;接力的意思就是对支持接力的 &lt;code&gt;apple&lt;/code&gt; 应用或者第三方应用可以跨设备继承之前的应用状态，比如你正在手机上用 &lt;code&gt;Chrome&lt;/code&gt; 浏览一个网页，这个网页的移动端支持不是很好，这时候可以在 &lt;code&gt;mac&lt;/code&gt; 的 &lt;code&gt;dock&lt;/code&gt; 栏直接看到一个如下图的图标，点击以后会直接打开你当前正在手机上浏览的网页。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/continuity-to-connect1.rdYkdHVm_ZBTCTe.webp&quot; alt=&quot;connect1&quot; title=&quot;connect1&quot;&gt;&lt;/p&gt;
&lt;p&gt;同样，在 &lt;code&gt;mac&lt;/code&gt; 上使用的应用只要支持 &lt;code&gt;handoff&lt;/code&gt; 同样可以在手机上打开。见下图中的红框，点击最下方的横条，就可以打开当前在 &lt;code&gt;mac&lt;/code&gt; 上浏览的网页，不过奇怪的是我在 &lt;code&gt;mac&lt;/code&gt; 上是使用 &lt;code&gt;Chrome&lt;/code&gt; 浏览的，在 &lt;code&gt;iphone&lt;/code&gt; 上却是用 &lt;code&gt;Safari&lt;/code&gt; 打开。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/continuity-to-connect2.DVZdwc-f_ZLV8X2.webp&quot; alt=&quot;connect2&quot; title=&quot;connect2&quot;&gt;&lt;/p&gt;
&lt;h2&gt;通用剪贴板&lt;/h2&gt;
&lt;p&gt;只要你在设备上开启了 &lt;code&gt;Handoff&lt;/code&gt;（开启方式看参考文档中的链接），那么通用剪贴板功能自动就开启了，这个功能我觉的是非常有用的，借助通用剪贴板，可以在一台 &lt;code&gt;Apple&lt;/code&gt; 设备上拷贝文本、图像、照片和视频等内容，然后在另一台 &lt;code&gt;Apple&lt;/code&gt; 设备上粘贴该内容。&lt;/p&gt;
&lt;p&gt;原来我要发送链接的时候都是通过微信上的文件传输助手，但其实只要 &lt;code&gt;mac&lt;/code&gt; 和 &lt;code&gt;iphone&lt;/code&gt; 上的接力功能都开启（也可以是其他 &lt;code&gt;apple&lt;/code&gt; 设备，比如 &lt;code&gt;ipad&lt;/code&gt; 和 &lt;code&gt;touch&lt;/code&gt;），然后两台设备都打开蓝牙，在同一个 &lt;code&gt;wifi&lt;/code&gt; 下，我们就能够像在同一个设备上一样使用剪贴板，比如我在 &lt;code&gt;iphone&lt;/code&gt; 的浏览器里面复制了一个网页的链接，或者在微信里面复制了一段话， 这时候我在 &lt;code&gt;mac&lt;/code&gt; 上直接使用 &lt;code&gt;⌘ + v&lt;/code&gt; 就会粘贴上我们在 &lt;code&gt;iphone&lt;/code&gt; 上复制的内容。而且这个内容不限于文本，图片和其他类型的文件都可以，不过经过我的测试，照片里的图片复制后是不能粘贴的，必须是在 &lt;code&gt;文件&lt;/code&gt; 应用中复制的文件才能够直接粘贴，不管是图片或者音频。&lt;/p&gt;
&lt;h2&gt;用 mac 接听和拨打电话&lt;/h2&gt;
&lt;p&gt;这个功能我一直都在使用，具体的使用条件查阅&lt;a href=&quot;https://support.apple.com/zh-cn/HT209456&quot; title=&quot;在 Mac、iPad 或 iPod touch 上拨打和接听电话&quot;&gt;在 Mac、iPad 或 iPod touch 上拨打和接听电话&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;利用 &lt;code&gt;mac&lt;/code&gt; 收发短信的方式也和电话类似。智能热点的使用方法也类似。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;连续互通相机&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;mac&lt;/code&gt; 上可以直接开启 &lt;code&gt;iphone&lt;/code&gt; 或者 &lt;code&gt;ipad&lt;/code&gt; 的相机，并且直接将拍摄的照片传到指定的位置或应用，也可以用相机扫描文稿，然后传递到指定的位置或应用。&lt;/p&gt;
&lt;p&gt;使用方法就是在指定的应用需要插入图片或扫描文稿的地方右键，然后选择 &lt;code&gt;import from iphone&lt;/code&gt;，然后选择 &lt;code&gt;take photo&lt;/code&gt; 或者 &lt;code&gt;scan documents&lt;/code&gt;，在一些第三方应用（比如 &lt;code&gt;outlook&lt;/code&gt;）没有 &lt;code&gt;import from iphone&lt;/code&gt; 而是菜单栏直接显示 &lt;code&gt;take photo&lt;/code&gt; 和 &lt;code&gt;scan documents&lt;/code&gt; 选项，选中之后会打开 &lt;code&gt;iphone&lt;/code&gt; 或 &lt;code&gt;ipad&lt;/code&gt; 上的相机，然后拍摄或者扫描，最后保存即可。&lt;code&gt;apple&lt;/code&gt; 自带应用支持连续互通相机的有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;访达&lt;/li&gt;
&lt;li&gt;Keynote 讲演 8.2 或更高版本&lt;/li&gt;
&lt;li&gt;邮件&lt;/li&gt;
&lt;li&gt;信息&lt;/li&gt;
&lt;li&gt;备忘录&lt;/li&gt;
&lt;li&gt;Numbers 表格 5.2 或更高版本&lt;/li&gt;
&lt;li&gt;Pages 文稿 7.2 或更高版本&lt;/li&gt;
&lt;li&gt;文本编辑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第三方应用是否支持可以实际进行测试。&lt;/p&gt;
&lt;h2&gt;速绘和标记&lt;/h2&gt;
&lt;p&gt;借助速绘连续互通和标记连续互通功能，您可以使用 &lt;code&gt;iPad&lt;/code&gt;、&lt;code&gt;iPhone&lt;/code&gt; 或 &lt;code&gt;iPod touch&lt;/code&gt; 将速绘轻松插入 &lt;code&gt;Mac&lt;/code&gt; 文稿中，或在 &lt;code&gt;Mac&lt;/code&gt; 上对 &lt;code&gt;PDF&lt;/code&gt; 和图像进行实时标记。&lt;/p&gt;
&lt;p&gt;在支持的应用中右键选择 &lt;code&gt;import from iphone&lt;/code&gt;，然后选择 &lt;code&gt;add sketch&lt;/code&gt; 就会在你的 &lt;code&gt;iphone&lt;/code&gt; 或者 &lt;code&gt;ipad&lt;/code&gt; 上打开速绘页面，绘制好后点击完成，这个速绘的内容就会添加到 &lt;code&gt;mac&lt;/code&gt; 上对应的应用中。&lt;/p&gt;
&lt;p&gt;借助标记连续互通，还可以使用 &lt;code&gt;Mac&lt;/code&gt; 请求从 &lt;code&gt;iPad&lt;/code&gt;、&lt;code&gt;iPhone&lt;/code&gt; 或 &lt;code&gt;iPod touch&lt;/code&gt; 来标记文稿。在设备上添加标记时，您会在 &lt;code&gt;Mac&lt;/code&gt; 上实时看到这一过程。这是签署文稿、改考卷或圈出重要详情的绝佳方式。&lt;/p&gt;
&lt;p&gt;使用方法就是找到要标记的图片或 &lt;code&gt;pdf&lt;/code&gt;，然后按下空格键以打开预览窗口。点按窗口顶部的标记按钮 。 然后从预览窗口顶部的标记工具栏中，点按注解按钮。此时&lt;code&gt;iphone&lt;/code&gt; 或 &lt;code&gt;ipad&lt;/code&gt; 上将打开标记窗口。然后我们就可以利用 &lt;code&gt;Apple Pencil&lt;/code&gt; 或手指与速绘工具搭配使用，或点按加号 并使用标记工具添加文本、签名、放大镜或形状和箭头。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/continuity-to-connect3.m2sLCImy_1vvrWA.webp&quot; alt=&quot;connect3&quot; title=&quot;connect3&quot;&gt;&lt;/p&gt;
&lt;h2&gt;隔空投送 Airdrop&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;mac&lt;/code&gt; 上有一个 &lt;code&gt;airdrop&lt;/code&gt; 文件夹，在 &lt;code&gt;iphone&lt;/code&gt; 的文件共享选项中第一个就是 &lt;code&gt;airdrop&lt;/code&gt; 的图标，当我们的设备靠近时就能够在 &lt;code&gt;airdrop&lt;/code&gt; 中发现其他设备从而进行文件的互传。&lt;/p&gt;
&lt;h2&gt;在 iPhone 和电脑之间传输文件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Catalina&lt;/code&gt; 已经取消了 &lt;code&gt;itunes&lt;/code&gt;，现在 &lt;code&gt;iphone&lt;/code&gt; 直接显示在 &lt;code&gt;finder&lt;/code&gt; 测侧边栏中，我们要在电脑和 &lt;code&gt;iphone&lt;/code&gt; 间传输文件，可以选择用 &lt;code&gt;usb连接&lt;/code&gt;，也可以选择通过无线局域网同步，后者显然更方便，只要在 &lt;code&gt;finder&lt;/code&gt; 的 &lt;code&gt;iphone&lt;/code&gt; 中钩上如下选项即可。然后我们就可以在上方的 &lt;code&gt;file&lt;/code&gt; 选项中进行文件的传输。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/continuity-to-connect4.II3zmRjb_ZkOVGK.webp&quot; alt=&quot;connect4&quot; title=&quot;connect4&quot;&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://support.apple.com/zh-cn/HT204681&quot; title=&quot;使用“连续互通”连接 Mac、iPhone、iPad、iPod touch 和 Apple Watch&quot;&gt;使用“连续互通”连接 Mac、iPhone、iPad、iPod touch 和 Apple Watch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.apple.com/zh-cn/guide/iphone/iphf2d851b9/ios&quot; title=&quot;在 iPhone 和电脑之间传输文件&quot;&gt;在 iPhone 和电脑之间传输文件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.apple.com/zh-cn/guide/iphone/iph875319a3a/13.0/ios/13.0#ipha85600a9b&quot; title=&quot;将 iPhone 与电脑同步&quot;&gt;将 iPhone 与电脑同步&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/macos.RfaH1x-K.jpg"/><enclosure url="/_astro/macos.RfaH1x-K.jpg"/></item><item><title>Shell脚本语法</title><link>https://clloz.com/blog/shell-script-syntax</link><guid isPermaLink="true">https://clloz.com/blog/shell-script-syntax</guid><pubDate>Fri, 01 Nov 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Shell&lt;/code&gt; 是一个用 &lt;code&gt;C&lt;/code&gt; 语言编写的程序，一般是一个命令行界面，是 &lt;code&gt;UNIX&lt;/code&gt; 操作系统下传统的用户和计算机的交互界面。之所以称为 &lt;code&gt;Shell&lt;/code&gt;，即壳，是和内核相对应，因为它隐藏了操作系统底层的细节，用户通过这个界面访问操作系统内核的服务，&lt;code&gt;Ken Thompson&lt;/code&gt; 的 &lt;code&gt;sh&lt;/code&gt; 是第一种 &lt;code&gt;Unix Shell&lt;/code&gt;，&lt;code&gt;Windows Explorer&lt;/code&gt; 是一个典型的图形界面 &lt;code&gt;Shell&lt;/code&gt;。&lt;code&gt;Shell&lt;/code&gt; 既是一种命令语言，又是一种程序设计语言。&lt;code&gt;Shell&lt;/code&gt; 编程跟 &lt;code&gt;JavaScript&lt;/code&gt;、&lt;code&gt;php&lt;/code&gt; 编程一样，只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。常见的 &lt;code&gt;Shell&lt;/code&gt; 程序有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bourne Shell（/usr/bin/sh或/bin/sh）&lt;/li&gt;
&lt;li&gt;Bourne Again Shell（/bin/bash）&lt;/li&gt;
&lt;li&gt;C Shell（/usr/bin/csh）&lt;/li&gt;
&lt;li&gt;K Shell（/usr/bin/ksh）&lt;/li&gt;
&lt;li&gt;Shell for Root（/sbin/sh）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;等，其中 &lt;code&gt;bash&lt;/code&gt; 是大部分 &lt;code&gt;Linux&lt;/code&gt; 系统默认的 &lt;code&gt;Shell&lt;/code&gt;。本文整理一下 &lt;code&gt;linux&lt;/code&gt; 下常用的的 &lt;code&gt;shell&lt;/code&gt; 命令，方便查询。在一般情况下，人们并不区分 &lt;code&gt;Bourne Shell&lt;/code&gt; 和 &lt;code&gt;Bourne Again Shell&lt;/code&gt;，所以，像 &lt;code&gt;#!/bin/sh&lt;/code&gt;，它同样也可以改为 &lt;code&gt;#!/bin/bash&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意 &lt;code&gt;Shell Script&lt;/code&gt; 都是在一个子 &lt;code&gt;Shell&lt;/code&gt; 中运行，所以如果像执行 &lt;code&gt;cd&lt;/code&gt; 这样的命令会发现不生效，因为在子 &lt;code&gt;Shell&lt;/code&gt; 中完成后又退出了，像这样的命令应该用 &lt;code&gt;alias&lt;/code&gt; 或者函数&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;shell 中的一些操作符和规则&lt;/h2&gt;
&lt;h2&gt;首行 &lt;code&gt;#!/bin/bash&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Shell&lt;/code&gt; 脚本的第一行 &lt;code&gt;#!&lt;/code&gt; 告诉系统其后路径所指定的程序即是解释此脚本文件的 &lt;code&gt;Shell&lt;/code&gt; 程序。&lt;code&gt;Shell&lt;/code&gt; 脚本的后缀名并不影响其执行，不过一般是使用 &lt;code&gt;sh&lt;/code&gt;，以免引起误解。&lt;/p&gt;
&lt;h2&gt;执行脚本的方法&lt;/h2&gt;
&lt;p&gt;运行 Shell 脚本有两种方式。 1. 作为解释器参数：直接调用对应的 &lt;code&gt;Shell&lt;/code&gt; 解释并执行脚本，比如 &lt;code&gt;sh test.sh&lt;/code&gt;，&lt;code&gt;/bin/sh test.sh&lt;/code&gt;，用这种执行方法在脚本的第一行就不用指定 &lt;code&gt;Shell&lt;/code&gt; 程序了，指定也会被忽略。 2. 作为可执行程序：需要先给对应的脚本文件添加执行权限 &lt;code&gt;chmod +x ./test.sh&lt;/code&gt;，然后直接执行 &lt;code&gt;./test.sh&lt;/code&gt;。需要注意的是，不能够直接用 &lt;code&gt;test.sh&lt;/code&gt;，必须加上 &lt;code&gt;./&lt;/code&gt;，因为如果直接用 &lt;code&gt;test.sh&lt;/code&gt; 的话系统会去 &lt;code&gt;PATH&lt;/code&gt; 里寻找有没有叫 &lt;code&gt;test.sh&lt;/code&gt;，加上 &lt;code&gt;./&lt;/code&gt; 就是告诉 &lt;code&gt;Shell&lt;/code&gt; 在当前目录下找。&lt;/p&gt;
&lt;h2&gt;shell 中的变量&lt;/h2&gt;
&lt;p&gt;变量名和等号之间不能有空格，命名只能使用英文字母，数字和下划线，首个字符不能以数字开头。中间不能有空格，不能使用标点符号，不能使用bash里的关键字。使用一个定义过的变量，只要在变量名前面加美元符号即可，变量名外面的花括号是可选的，加不加都行，加花括号是为了帮助解释器识别变量的边界。&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;shell&lt;/code&gt; 时，会同时存在三种变量： 1. 局部变量 局部变量在脚本或命令中定义，仅在当前 &lt;code&gt;shell&lt;/code&gt; 实例中有效，其他 &lt;code&gt;shell&lt;/code&gt; 启动的程序不能访问局部变量。 2. 环境变量 所有的程序，包括 &lt;code&gt;shell&lt;/code&gt; 启动的程序，都能访问环境变量，有些程序需要环境变量来保证其正常运行。必要的时候 &lt;code&gt;shell&lt;/code&gt; 脚本也可以定义环境变量。 3. &lt;code&gt;shell&lt;/code&gt; 变量 &lt;code&gt;shell&lt;/code&gt; 变量是由 &lt;code&gt;shell&lt;/code&gt; 程序设置的特殊变量。&lt;code&gt;shell&lt;/code&gt; 变量中有一部分是环境变量，有一部分是局部变量，这些变量保证了 &lt;code&gt;shell&lt;/code&gt; 的正常运行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;your_name=&quot;clloz&quot;
echo $your_name
echo ${your_name}

for skill in Ada Coffe Action Java; do
    echo &quot;I am good at ${skill}Script&quot;
done

# 重新定义已定义的变量
your_name=&quot;tom&quot;
echo $your_name
your_name=&quot;alibaba&quot;
echo $your_name

# 用readonly命令将变量变为只读
#!/bin/bash
myUrl=&quot;http://www.clloz.com&quot;
readonly myUrl
myUrl=&quot;http://www.clloz1992.com&quot; #This variable is read only.

#删除变量，unset不能删除只读变量
unset variable_name
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字符串&lt;/h2&gt;
&lt;p&gt;字符串是 &lt;code&gt;Shell&lt;/code&gt; 编程中最常用最有用的数据类型(除了数字和字符串，也没啥其它类型好用了)，字符串可以用单引号，也可以用双引号，也可以不用引号。单引号里的任何字符都会原样输出，单引号字符串中的变量是无效的，单引号必须成对出现。双引号中除了 美元符号、&lt;code&gt;\&lt;/code&gt;、 反引号有特殊含义外，其余字符(&lt;code&gt;如IFS&lt;/code&gt;、换行符、回车符等)没有特殊含义，所以双引号里面可以引用变量，并且可以转义特殊字符。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;your_name=&quot;clloz&quot;
# 使用双引号拼接
greeting=&quot;hello, &quot;$your_name&quot; !&quot;
greeting_1=&quot;hello, ${your_name} !&quot;
echo $greeting  $greeting_1 #hello, clloz ! hello, clloz !
# 使用单引号拼接
greeting_2=&apos;hello, &apos;$your_name&apos; !&apos;
greeting_3=&apos;hello, ${your_name} !&apos;
echo $greeting_2  $greeting_3 #hello, clloz ! hello ${your_name} !

# 获取字符串长度
string=&quot;abcd&quot;
echo ${#string} #输出 4

# 提取字符串
string=&quot;clloz.com&quot;
echo ${string:1:5}  # 输出 lloz.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 定义数组
array_name=(value0 value1 value2 value3)

array_name=(
value0
value1
value2
value3
)

# 单独定义数组分量，下标可以不连续，且下标的
array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen

# 读取数组
valuen=${array_name[n]}
echo ${array_name[@]} #输出所有数组元素

# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;|&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;竖线 &lt;code&gt;|&lt;/code&gt;，在 &lt;code&gt;shell&lt;/code&gt; 中是作为管道符的，将 &lt;code&gt;|&lt;/code&gt; 前面命令的输出作为 &lt;code&gt;|&lt;/code&gt; 后面的输入。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;||&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;用双竖线 &lt;code&gt;||&lt;/code&gt; 分割的多条命令，执行的时候遵循如下规则，如果前一条命令为真，则后面的命令不会执行，如果前一条命令为假，则继续执行后面的命令。比如判断文件是否存在，不存在则创建，存在就什么都不执行。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;&amp;#x26;&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x26;&lt;/code&gt; 同时执行多条命令，不管命令是否执行成功。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt; 可同时执行多条命令，当碰到执行错误的命令时，将不再执行后面的命令。如果一直没有错误的，则执行完毕。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;.&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;一个 &lt;code&gt;dot&lt;/code&gt; 代表当前目录，两个 &lt;code&gt;dot&lt;/code&gt; 代表上层目录。&lt;code&gt;dot&lt;/code&gt; 如果位于文件或目录的第一个字符，该档案就属特殊档案，用 &lt;code&gt;ls&lt;/code&gt; 指令必须加上 &lt;code&gt;-a&lt;/code&gt; 选项才会显示。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;~&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;~&lt;/code&gt;代表使用者的 &lt;code&gt;home&lt;/code&gt; 目录：&lt;code&gt;cd ~&lt;/code&gt;；也可以直接在符号后加上某帐户的名称：&lt;code&gt;cd ~user&lt;/code&gt;或者当成是路径的一部份。&lt;code&gt;~+&lt;/code&gt; 表示当前工作目录，和 &lt;code&gt;pwd&lt;/code&gt; 相同；&lt;code&gt;~-&lt;/code&gt; 表示上次工作目录。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;;&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;shell&lt;/code&gt; 中，担任&quot;连续指令&quot;功能的符号就是&quot;分号&quot;。譬如以下的例子：&lt;code&gt;cd ~/backup ; mkdir startup ;cp ~/.* startup/&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;`&lt;/code&gt; 倒引号 backticks&lt;/h2&gt;
&lt;p&gt;倒引号中的字符串会被当做命令执行。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;[]&lt;/code&gt; 方括号&lt;/h2&gt;
&lt;p&gt;常出现在流程控制中，扮演括住判断式的作用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt; if [ &quot;$?&quot; != 0 ] then echo &quot;Executes error&quot; exit 1 fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;[[]]&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;与 &lt;code&gt;[]&lt;/code&gt; 基本上作用相同，但允许在其中直接使用 &lt;code&gt;||&lt;/code&gt; 与&lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt; 逻辑等符号。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;()&lt;/code&gt; 指令群组&lt;/h2&gt;
&lt;p&gt;用括号将一串连续指令括起来，这种用法对 &lt;code&gt;shell&lt;/code&gt; 来说，称为指令群组。如下面的例子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;(cd ~ ; vcgh=`pwd` ;echo $vcgh)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;指令群组有一个特性，&lt;code&gt;shell&lt;/code&gt; 会以产生 &lt;code&gt;subshell&lt;/code&gt; 来执行这组指令。因此，在其中所定义的变数，仅作用于指令群组本身。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;(())&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;这组符号的作用与 &lt;code&gt;let&lt;/code&gt; 指令相似，用在算数运算上，是 &lt;code&gt;bash&lt;/code&gt; 的内建功能。所以，在执行效率上会比使用 &lt;code&gt;let&lt;/code&gt; 指令要好许多。&lt;/p&gt;
&lt;h2&gt;美元符号&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$0  表示shell的命令本身，包括完整路径，$0 是脚本本身的名字
$1 - $9 表示shell 的第几个参数
$# 表示传递到脚本的参数个数
$* 表示以一个单字符串显示所有向脚本传递的参数
双$ 表示脚本运行的ID号
$! 表示后台运行的最后一个进程的ID号
$@ 与$*相同。
$- 显示shell使用的当前选项。
$? 显示最后命令的执行状况。0表示没有错误。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;文件描述符&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;Linux&lt;/code&gt; 系统中一切皆可以看成是文件，文件又可分为：普通文件、目录文件、链接文件和设备文件。文件描述符（&lt;code&gt;file descriptor&lt;/code&gt;）是内核为了高效管理已被打开的文件所创建的索引，其是一个非负整数（通常是小整数），用于指代被打开的文件，所有执行 &lt;code&gt;I/O&lt;/code&gt; 操作的系统调用都通过文件描述符。程序刚刚启动的时候，&lt;code&gt;0&lt;/code&gt; 是标准输入 &lt;code&gt;stdin&lt;/code&gt;，&lt;code&gt;1&lt;/code&gt;是标准输出 &lt;code&gt;stdout&lt;/code&gt;，&lt;code&gt;2&lt;/code&gt;是标准错误 &lt;code&gt;stderr&lt;/code&gt;。如果此时去打开一个新的文件，它的文件描述符会是 &lt;code&gt;3&lt;/code&gt;。&lt;code&gt;POSIX&lt;/code&gt; 标准要求每次打开文件时（含 &lt;code&gt;socket&lt;/code&gt;）必须使用当前进程中最小可用的文件描述符号码。&lt;/p&gt;
&lt;h2&gt;重定向 &lt;code&gt;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;大多数 &lt;code&gt;UNIX&lt;/code&gt; 系统命令从你的终端接受输入并将所产生的输出发送回到终端。一个命令通常从一个叫标准输入的地方读取输入，默认情况下，这恰好是你的终端。同样，一个命令通常将其输出写入到标准输出，默认情况下，这也是你的终端。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;command &gt; file&lt;/code&gt;：将输出重定向到 &lt;code&gt;file&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;command &amp;#x3C; file&lt;/code&gt;：将输入重定向到 &lt;code&gt;file&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;command &gt;&gt; file&lt;/code&gt;：将输出以追加的方式重定向到 &lt;code&gt;file&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n &gt; file&lt;/code&gt;：将文件描述符为 &lt;code&gt;n&lt;/code&gt; 的文件重定向到 &lt;code&gt;file&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; &gt;&gt; &lt;code&gt;file&lt;/code&gt; 将文件描述符为 &lt;code&gt;n&lt;/code&gt; 的文件以追加的方式重定向到 &lt;code&gt;file&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; &gt;&amp;#x26; &lt;code&gt;m&lt;/code&gt; 将输出文件 &lt;code&gt;m&lt;/code&gt; 和 &lt;code&gt;n&lt;/code&gt; 合并。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; &amp;#x3C;&amp;#x26; &lt;code&gt;m&lt;/code&gt; 将输入文件 &lt;code&gt;m&lt;/code&gt; 和 &lt;code&gt;n&lt;/code&gt; 合并。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;&amp;#x3C; tag&lt;/code&gt; 将开始标记 &lt;code&gt;tag&lt;/code&gt; 和结束标记 &lt;code&gt;tag&lt;/code&gt; 之间的内容作为输入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在输出重定向中，&lt;code&gt;&gt;&lt;/code&gt; 代表的是覆盖，&lt;code&gt;&gt;&gt;&lt;/code&gt; 代表的是追加。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 实例
command &gt;file #以覆盖的方式，把 command 的正确输出结果输出到 file 文件中。
command &gt;&gt;file #以追加的方式，把 command 的正确输出结果输出到 file 文件中。
command 2&gt;file #以覆盖的方式，把 command 的错误信息输出到 file 文件中。
command 2&gt;&gt;file #以追加的方式，把 command 的错误信息输出到 file 文件中。
command &gt;file 2&gt;&amp;#x26;1 #以覆盖的方式，把正确输出和错误信息同时保存到同一个文件（file）中。
command &gt;&gt;file 2&gt;&amp;#x26;1 #以追加的方式，把正确输出和错误信息同时保存到同一个文件（file）中。
command &gt;file1 2&gt;file2 #以覆盖的方式，把正确的输出结果输出到 file1 文件中，把错误信息输出到 file2 文件中。
command &gt;&gt;file1  2&gt;&gt;file2 #以追加的方式，把正确的输出结果输出到 file1 文件中，把错误信息输出到 file2 文件中。
command &amp;#x3C;file #将 file 文件中的内容作为 command 的输入。
command &amp;#x3C;&amp;#x3C;END #从标准输入（键盘）中读取数据，直到遇见分界符 END 才停止（分界符可以是任意的字符串，用户自己定义）。
command &amp;#x3C;file1 &gt;file2 #将 file1 作为 command 的输入，并将 command 的处理结果输出到 file2。
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;关于 &lt;code&gt;shell&lt;/code&gt; 中的单引号和双引号的用法请看我的另一篇文章&lt;a href=&quot;https://www.clloz.com/programming/computer-science/operating-system/2019/04/12/shell-quote-escape/&quot; title=&quot;shell中的引号和转义&quot;&gt;shell中的引号和转义&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;常用命令&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;Linux&lt;/code&gt; 中，可执行的文件也进行了分类：&lt;/p&gt;
&lt;p&gt;内置命令：出于效率的考虑，将一些常用命令的解释程序构造在 &lt;code&gt;Shell&lt;/code&gt; 内部。 外置命令：存放在 &lt;code&gt;/bin&lt;/code&gt;、&lt;code&gt;/sbin&lt;/code&gt; 目录下的命令 实用程序：存放在 &lt;code&gt;/usr/bin&lt;/code&gt;、&lt;code&gt;/usr/sbin&lt;/code&gt;、&lt;code&gt;/usr/share&lt;/code&gt;、&lt;code&gt;/usr/local/bin&lt;/code&gt; 等目录下的实用程序 用户程序：用户程序经过编译生成可执行文件后，可作为 &lt;code&gt;Shell&lt;/code&gt; 命令运行 &lt;code&gt;Shell&lt;/code&gt; 脚本：由 &lt;code&gt;Shell&lt;/code&gt; 语言编写的批处理文件，可作为 &lt;code&gt;Shell&lt;/code&gt; 命令运行&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;Linux&lt;/code&gt; 常用命令已经在另一篇文章&lt;a href=&quot;https://www.clloz.com/programming/computer-science/operating-system/2020/08/18/linux-command/&quot; title=&quot;Linux常用命令&quot;&gt;Linux常用命令&lt;/a&gt;中进行了详细的介绍。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/jpfss/p/10077390.html&quot; title=&quot;linux中竖线&amp;#x27;|&amp;#x27;，双竖线‘||’，&amp;#x26;和&amp;#x26;&amp;#x26;的意思&quot;&gt;linux中竖线&apos;|&apos;，双竖线‘||’，&amp;#x26;和&amp;#x26;&amp;#x26;的意思&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/36801617&quot; title=&quot;看完这篇Linux基本的操作就会了 - 知乎&quot;&gt;看完这篇Linux基本的操作就会了 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.runoob.com/linux/linux-shell.html&quot; title=&quot;Shell教程 - 菜鸟教程&quot;&gt;Shell教程 - 菜鸟教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/MiltonZhong/article/details/10344163#:~:text=Linux%20Shell%E4%B8%AD%E7%9A%84%E7%BE%8E%E5%85%83,%E5%88%B0%249%20%E6%95%B0%E5%AD%97%E8%A1%A8%E7%A4%BAshell&quot; title=&quot;Shell中的美元符号&quot;&gt;Shell中的美元符号&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/balaamwe/archive/2012/03/15/2397998.html&quot; title=&quot;Linux特殊符号大全&quot;&gt;Linux特殊符号大全&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.runoob.com/linux/linux-shell-io-redirections.html&quot; title=&quot;Shell输入/输出重定向&quot;&gt;Shell输入/输出重定向&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/linux-logo.03F4Kw33.png"/><enclosure url="/_astro/linux-logo.03F4Kw33.png"/></item><item><title>Mac自带拼音输入法的使用技巧</title><link>https://clloz.com/blog/mac-input-method</link><guid isPermaLink="true">https://clloz.com/blog/mac-input-method</guid><pubDate>Thu, 31 Oct 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;自从大学毕业以后我几乎就没有用过 &lt;code&gt;360&lt;/code&gt;，&lt;code&gt;baidu&lt;/code&gt; 以及 &lt;code&gt;sougo&lt;/code&gt; 等国内的软件了，一方面是安全考虑，另一方面是越来越喜欢简单，直接，没有花里胡哨的功能和广告的应用，这也是我为什么后来转向 &lt;code&gt;Mac&lt;/code&gt;，不管是硬件还是软件系统，都很简单，直接，虽然 &lt;code&gt;Mac&lt;/code&gt; 也又很多这样那样的问题，不过总的来说设计理念跟我的想法还是比较契合的。说会主题输入法，我在 &lt;code&gt;Windows&lt;/code&gt; 和 &lt;code&gt;Mac&lt;/code&gt; 上都用的是自带的输入法，其实现在无论是 &lt;code&gt;Windows&lt;/code&gt; 和 &lt;code&gt;Mac&lt;/code&gt; 默认的一些软件，包括输入法和杀毒软件都已经做的非常好了，完全没必要另外安装第三方的，特别是我这种追求极致的简单的，功能一目了然的。但是其实 &lt;code&gt;Mac&lt;/code&gt; 上的默认拼音输入法还是又很多小技巧，可能是大家不知道的，这篇文章整理一下 &lt;code&gt;Mac&lt;/code&gt; 上的输入法使用技巧。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;也曾经试过 &lt;code&gt;Rime&lt;/code&gt; 输入法，在 &lt;code&gt;Mac&lt;/code&gt; 上的版本是叫做鼠须管，严格来说它是一个输入引擎，几乎所有东西都可以自己定制的，但是使用起来过于麻烦了，想要真的制定到自己非常满意成本太高了，不过源码可以学习一下。另外也想过学五笔，五笔其实本身我到觉得很不错，不过我发现五笔输入的词库是一个很大的问题，比如我想输入 &lt;code&gt;长泽雅美&lt;/code&gt;，&lt;code&gt;新垣结衣&lt;/code&gt;，基本都是没有的，如果完全单字输入跟我的思维习惯是不符合的，所以也放弃了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用技巧&lt;/h2&gt;
&lt;h2&gt;拼音转大写&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 上的拼音输入法是用大写锁定 &lt;code&gt;CapsLock&lt;/code&gt; 键来切换中英文的，这和我们通常在 &lt;code&gt;Windows&lt;/code&gt; 中使用 &lt;code&gt;Shift&lt;/code&gt; 键不同，不过很多人不知道如何在拼音状态下切换到大写锁定的状态。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;System Preferences -&gt; Keyboard -&gt; Input Sources&lt;/code&gt; 中 勾选下面的第二项，如下图。现在在拼音输入状态下，长按 &lt;code&gt;CapsLock&lt;/code&gt; 键就是切换到大写英文，短按就是切换到小写英文。在这里另外给大家推荐一个软件 &lt;code&gt;CapsLocker&lt;/code&gt;，这个软件在大写锁定切换的时候会发出声音，对我这种使用蓝牙键盘且看不到大写锁定状态的还是很有用的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/capslock-pinyin.SenWREh__Zx46mk.webp&quot; alt=&quot;capslock-pinyin&quot; title=&quot;capslock-pinyin&quot;&gt;&lt;/p&gt;
&lt;h2&gt;不同应用不同输入法&lt;/h2&gt;
&lt;p&gt;在上图中的第三个复选框就是不同的应用不同的输入法的功能，钩上即开启。这个功能刚开始用可能有点反直觉，至少我觉得输入法不全局有时候会造成混乱，但是如果习惯了还是挺有用的。比如我们写程序或者在终端一般都是英文状态，但是当我们切换到聊天软件或者浏览器的时候可能需要输入中文，这样在频繁切换的时候就会很麻烦，这时候钩上不同的应用自动切换输入法就会比较方便，不过这个需要适应一段时间。&lt;/p&gt;
&lt;h2&gt;选择文字注音&lt;/h2&gt;
&lt;p&gt;我们可以在输拼音的时候按 &lt;code&gt;tab&lt;/code&gt; 会在四声之间切换，比如 &lt;code&gt;tān，tán，tǎn，tàn&lt;/code&gt;，有时候这个功能还是有用的，能够帮我们快速找到目标字。&lt;/p&gt;
&lt;h2&gt;显示生僻字和繁体字&lt;/h2&gt;
&lt;p&gt;在使用 &lt;code&gt;Mac&lt;/code&gt; 默认的拼音输入法的时候，有时候会发现有些字找不到，或者发现一个读音的字非常少，可能是你没有钩上下图中的 &lt;code&gt;show traditional and rare characters&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/show-rare-character.C93ybcrF_Z13h7o9.webp&quot; alt=&quot;show-rare-character&quot; title=&quot;show-rare-character&quot;&gt;&lt;/p&gt;
&lt;h2&gt;中英文数字混输&lt;/h2&gt;
&lt;p&gt;在输入拼音的状态下，按住 &lt;code&gt;Shift&lt;/code&gt; 就能输大写字母，按住 &lt;code&gt;Option&lt;/code&gt; 则能输数字。&lt;/p&gt;
&lt;h2&gt;拆字输入&lt;/h2&gt;
&lt;p&gt;当遇到我们不知道一个字的读音的时候，可以通过拆字来输入这几个字的部分来输入这个字。这个功能在 &lt;code&gt;Windows&lt;/code&gt; 和 &lt;code&gt;搜狗&lt;/code&gt; 输入法中是按 &lt;code&gt;u&lt;/code&gt; 然后输入，在 &lt;code&gt;Mac&lt;/code&gt; 中是先输入单独部分然后使用快捷键 &lt;code&gt;Shift+空格键&lt;/code&gt;，比如输入 &lt;code&gt;jiji&lt;/code&gt; 然后按 &lt;code&gt;Shift+空格键&lt;/code&gt;，就能输入 &lt;code&gt;喆&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;查找输入码&lt;/h2&gt;
&lt;p&gt;通过快捷键 &lt;code&gt;Option+Shift+L&lt;/code&gt; 可以打开查找输入码的界面，然后复制字到里面就可以查看各种输入法中的输入码，在遇到不认识的字，或者想要查看五笔的输入码的时候都可以使用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/input-code.CbfOj6Xv_1BMigq.webp&quot; alt=&quot;input-code&quot; title=&quot;input-code&quot;&gt;&lt;/p&gt;
&lt;h2&gt;使用手写和虚拟键盘&lt;/h2&gt;
&lt;p&gt;快捷键 &lt;code&gt;Control+Shift+空格键&lt;/code&gt; 可以打开手写界面，点击上方任务栏中的输入法图标点击 &lt;code&gt;show keyboard viewer&lt;/code&gt; 可以打开虚拟键盘。&lt;/p&gt;
&lt;h2&gt;emoji 以及特殊符号&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Command+Control+空格键&lt;/code&gt; 可以打开 &lt;code&gt;emoji&lt;/code&gt; 界面，点击右上角的图标可以选择更多图标。&lt;code&gt;Option+Shift+B&lt;/code&gt; 也可以选择一些特殊符号以及颜文字。&lt;/p&gt;
&lt;h2&gt;自定义文本&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;System Preferences -&gt; Keyboard -&gt; Text&lt;/code&gt; 中我们可以自定义一些我们常用的短语或一些很生僻的字，甚至可以是一些我们常用的颜文字，自定义对应的输入码，这样我们就能够快速地输入这些内容。&lt;/p&gt;
&lt;h2&gt;任务栏图标&lt;/h2&gt;
&lt;p&gt;最后说一个和输入法不相关的 &lt;code&gt;Mac&lt;/code&gt; 使用技巧，就是按住 &lt;code&gt;⌘&lt;/code&gt; 拖动顶部任务栏的图标能够排列任务栏图标的顺序，并且下次打开应用还是按这个顺序排列的。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ifanr.com/app/800591&quot; title=&quot;Mac 自带输入法这么好用，不看不知道&quot;&gt;Mac 自带输入法这么好用，不看不知道&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/macos.RfaH1x-K.jpg"/><enclosure url="/_astro/macos.RfaH1x-K.jpg"/></item><item><title>英语国际音标</title><link>https://clloz.com/blog/english-ipa</link><guid isPermaLink="true">https://clloz.com/blog/english-ipa</guid><pubDate>Thu, 24 Oct 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;英语一直是我学生生涯最差的一门学科，我的学生时代好像是从六年级开始学的英文，不过几乎是上着玩的，也不用考试。我正式学习英语是从初中开始，我上学那会儿，书上的单词还是有有音标的，不过我印象中老师并没有教过，很多比较有兴趣或有上进心的同学都自己学音标。我以前英语很差，导致自己对英语也非常没兴趣，几乎学生生涯的英语老师对我的印象都很差，我自然也没有自学过音标，单词的读音都是模仿，很多读的都不对，看到一个陌生的单词也完全不知道该怎么发音，有些单词知道发音也不知道该怎么拼，我也没在意过。自从学了日语以后，我开始注重发音，因为很多读音你不开口根本不知道自己读的完全变形，甚至有些音因为是我们的母语中是没有的，根本发不出来。&lt;/p&gt;
&lt;p&gt;我现在学习语言并不是为了考试，开口说并能够灵活运用是我的最主要的目标。其实母语者一般不学习音标，因为一直在母语的环境里耳濡目染，自然而然就对发音养成了习惯。但是当你想学习另一门语言的时候，音标还是很重要的，因为不同的语言发音是不同的，可能你要学的语言里面有些音是你的母语里没有的，这时候音标就很有用，能够让你准确的知道某个音应该怎么发，国际音标就相当于给全世界不同的民族不同的语言制定了一套通用的标准，能够套用到各种语言的发音上。&lt;/p&gt;
&lt;h2&gt;KK 音标、DJ 音标和 IPA&lt;/h2&gt;
&lt;p&gt;比较流行的音标有三种，&lt;code&gt;KK&lt;/code&gt; 音标，&lt;code&gt;DJ&lt;/code&gt; 音标和 &lt;code&gt;IPA&lt;/code&gt; ，其中 &lt;code&gt;KK&lt;/code&gt; 音标是主要在台湾地区流行的美式英语的音标，而大陆和香港地区主要使用的是英式英语的 &lt;code&gt;DJ&lt;/code&gt; 音标，我印象中我们上学的时候课本上的音标都是 &lt;code&gt;DJ&lt;/code&gt; 音标。下图展示了三种音标对之间的关系。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/phonetic-symbols.CdCqKVdq_1FzcSc.webp&quot; alt=&quot;phonetic-symbols&quot; title=&quot;phonetic-symbols&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;维基百科图中的几个问题，&lt;code&gt;DJ&lt;/code&gt; 音标中的 &lt;code&gt;[ɔ]&lt;/code&gt; 应该为 &lt;code&gt;[ɒ]&lt;/code&gt; 才对。&lt;code&gt;[ɑ] [ɑː]&lt;/code&gt; 那行举的例子似乎不太对，&lt;code&gt;hot&lt;/code&gt; 的英音音标为 &lt;code&gt;[hɒt]&lt;/code&gt;，美音音标为 &lt;code&gt;[hɑt]&lt;/code&gt;，用 &lt;code&gt;father&lt;/code&gt; 来做例子更好。最后漏了&lt;code&gt;DJ&lt;/code&gt; 音标中的 &lt;code&gt;[eə] [ʊə]&lt;/code&gt; 两个双元音。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;DJ&lt;/code&gt; 音标一共 &lt;code&gt;20&lt;/code&gt; 个元音，&lt;code&gt;24&lt;/code&gt; 个辅音，对比国内常说的 &lt;code&gt;48&lt;/code&gt; 个国际音标，少了 &lt;code&gt;[dr] [dz] [tr] [ts]&lt;/code&gt; 这几个辅音，原因是一些语音学者认为它们不是独立的音素，而是辅音连缀。&lt;code&gt;KK&lt;/code&gt; 音标一共 &lt;code&gt;17&lt;/code&gt; 个元音，&lt;code&gt;24&lt;/code&gt; 个辅音。两者的辅音是相同的，但是元音有一些不同，我感觉区分起来还是挺有难度的，主要有很多长得很像的。 比如 &lt;code&gt;[ɛ]&lt;/code&gt; 和 &lt;code&gt;[ɜː]&lt;/code&gt;，&lt;code&gt;[e]&lt;/code&gt; 两者都有但是表达不同的音，&lt;code&gt;[ɑ]&lt;/code&gt;，&lt;code&gt;[ɑː]&lt;/code&gt;和 &lt;code&gt;[ɒ]&lt;/code&gt;。总结起来，两者的元音共有一下这些不同，&lt;code&gt;[i:] vs [i]&lt;/code&gt;，&lt;code&gt;[ɑ:] vs [ɑ]&lt;/code&gt;、&lt;code&gt;[ɔ:] vs [ɔ]&lt;/code&gt;，&lt;code&gt;[u:] vs [u]&lt;/code&gt;，&lt;code&gt;[e] vs [ɛ]&lt;/code&gt;，&lt;code&gt;[ɒ] vs [ɔ]&lt;/code&gt;，&lt;code&gt;[eɪ] vs [e]&lt;/code&gt;，&lt;code&gt;[əʊ] vs [o]&lt;/code&gt;，&lt;code&gt;[ɜː] vs [ɝ]&lt;/code&gt; 和&lt;code&gt;[ə] vs [ɚ]&lt;/code&gt;，最后还有 &lt;code&gt;DJ&lt;/code&gt; 音标中的三个双元音 &lt;code&gt;[ɪə]&lt;/code&gt;，&lt;code&gt;[eə]&lt;/code&gt; 和 &lt;code&gt;[ʊə]&lt;/code&gt; 在 &lt;code&gt;KK&lt;/code&gt; 音标中没有，其实它们可以用辅音表示为 &lt;code&gt;[ir]&lt;/code&gt;，&lt;code&gt;[ɛr]&lt;/code&gt; 和 &lt;code&gt;[ur]&lt;/code&gt;，具体例子分别为 &lt;code&gt;near&lt;/code&gt;，&lt;code&gt;care&lt;/code&gt; 和 &lt;code&gt;tour&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;KK&lt;/code&gt; 和 &lt;code&gt;DJ&lt;/code&gt; 的主要几个区别： 1. &lt;code&gt;[e] vs [ɛ]&lt;/code&gt; 和 &lt;code&gt;[eɪ] vs [e]&lt;/code&gt; 表示的是同一个音，很容易混淆，比如 &lt;code&gt;[bet]&lt;/code&gt; 这个音标在两种音标表示下是完全不同的意思。 2. &lt;code&gt;KK&lt;/code&gt; 音标中没有长音符号，&lt;code&gt;[i:] vs [i]&lt;/code&gt;、&lt;code&gt;[ɑ:] vs [ɑ]&lt;/code&gt;、&lt;code&gt;[ɔ:] vs [ɔ]&lt;/code&gt;、&lt;code&gt;[u:] vs [u]&lt;/code&gt;。 3. &lt;code&gt;[eɪ] vs [e]&lt;/code&gt; 和 &lt;code&gt;[əʊ] vs [o]&lt;/code&gt;，DJ音标认为这2个音标是双元音, 而KK音标认为这2个音标是单元音。因为在美式发音中, 很多人的确是把这2个音按照单元音来发的。 4. &lt;code&gt;[ɔ:] vs [ɔ]&lt;/code&gt; 和 &lt;code&gt;[ɒ] vs [ɔ]&lt;/code&gt;，&lt;code&gt;DJ&lt;/code&gt; 音标中的 &lt;code&gt;2&lt;/code&gt; 个音标到了 &lt;code&gt;KK&lt;/code&gt; 音标变成了 &lt;code&gt;1&lt;/code&gt; 个音标，但其实部分 &lt;code&gt;[ɒ]&lt;/code&gt; 到 &lt;code&gt;KK&lt;/code&gt; 音标里面实际发音是 &lt;code&gt;[ɑ]&lt;/code&gt;，比如 &lt;code&gt;hot&lt;/code&gt; 这个单词。 5. &lt;code&gt;[ɜː] vs [ɝ]&lt;/code&gt; 和 &lt;code&gt;[ə] vs [ɚ]&lt;/code&gt;，&lt;code&gt;KK&lt;/code&gt; 音标给这 &lt;code&gt;2&lt;/code&gt; 个音标符号后边加上了小尾巴表示卷舌。在英音中一般比较少涉及卷舌，如果要用来标注美式发音, 则会写作 &lt;code&gt;[ɜːr]&lt;/code&gt; 和 &lt;code&gt;[ər]&lt;/code&gt; 来表示卷舌。实际上, 现在一些词典已经不对这两个音区分了, 不论是 &lt;code&gt;[ɝ]&lt;/code&gt; 还是 &lt;code&gt;[ɚ]&lt;/code&gt;, 都统一写作 &lt;code&gt;[ər]&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是 &lt;code&gt;KK&lt;/code&gt; 音标和 &lt;code&gt;DJ&lt;/code&gt; 音标都是根据 &lt;code&gt;IPA&lt;/code&gt; 所编的音标符号，本质上发音是同一个东西，只是使用的符号不同，一个主要用于标注英式发音，一个主要用于标注美式发音。并且我们在英语中使用的 &lt;code&gt;48&lt;/code&gt; 个音标只是 &lt;code&gt;IPA&lt;/code&gt; 中的一部分，一般也称为英语国际音标。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果想听并跟读标准的英式和美式音标可以看下面两个视频： 1. 英式音标发音和口型：&lt;a href=&quot;https://www.bilibili.com/video/av54685652?p=22&quot; title=&quot;BBC音标教程&quot;&gt;BBC音标教程&lt;/a&gt; 2. 美式音标和口型：&lt;a href=&quot;https://www.bilibili.com/video/av19733761/?p=2&quot; title=&quot;KK美式英语音标发音口型教学&quot;&gt;KK美式英语音标发音口型教学&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;英式英语中的翘舌音非常少，在发音中最让我迷惑的是 &lt;code&gt;[r]&lt;/code&gt;，在美式英语中位于句首类似于 &lt;code&gt;若&lt;/code&gt;，句尾就是翘舌，但是在英式英语中就比较微妙。具体大家可以看上面的两个视频体会。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;音标发音（ DJ 标注）&lt;/h2&gt;
&lt;h2&gt;软件&lt;/h2&gt;
&lt;p&gt;在这里推荐两个在手机上的发音软件，一个是 &lt;code&gt;IPA Phonetics&lt;/code&gt;，另一个是&lt;code&gt;英语标准国际音标&lt;/code&gt;，前一个完全根据 &lt;code&gt;IPA&lt;/code&gt; 来的，有发音的口腔视频，虽然看着有点恶心，不过还是挺有用的。不过对于我们这样的初学者，第二个软件我认为更好用，每个音都有很多例子，讲解也挺详细&lt;/p&gt;
&lt;h2&gt;元音&lt;/h2&gt;
&lt;p&gt;元音包含 &lt;code&gt;12&lt;/code&gt; 个单元音与 &lt;code&gt;8&lt;/code&gt; 个双元音，其中单元音又包含 &lt;code&gt;5&lt;/code&gt; 个长元音与 &lt;code&gt;7&lt;/code&gt; 个短元音。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;长元音：&lt;code&gt;[ɑː] [ɔː] [ɜː] [iː] [uː]&lt;/code&gt;。这样的排列顺序是可以和我们的拼音韵母 &lt;code&gt;ɑ o e i u&lt;/code&gt; 对应起来&lt;/li&gt;
&lt;li&gt;短元音：&lt;code&gt;[ʌ] [ɒ] [ə] [ɪ] [ʊ] [æ] [e]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;双元音：&lt;code&gt;[eɪ] [aɪ] [ɔɪ] [eə] [ʊə] [ɪə] [aʊ] [əʊ]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;汉语拼音中的单韵母没有长短音之分，英语中的单元音又分长元音和短元音。双元音由两个元音组成，发音时由前一个元音向后一个元音滑动，口型有变化。前一个元音发音清晰响亮，且时间长；后一个元音发音模糊较弱，且时间短。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;辅音&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;清辅音：&lt;code&gt;[p] [t] [k] [f] [θ] [s] [ʃ] [tʃ] [ts] [tr] [h]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;浊辅音：&lt;code&gt;[b] [d] [ɡ] [v] [ð] [z] [ʒ] [dʒ] [dz] [dr] [r] [m] [n] [ŋ] [l] [w] [j]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中前面九个清辅音和浊辅音是一一对应的，清辅音声带不震动，浊辅音声带震动。&lt;/p&gt;
&lt;p&gt;大部分辅音还是比较容易发音的，我认为有几个点需要注意： 1. &lt;code&gt;[m] [n] [ŋ]&lt;/code&gt; 的发音区别 2. &lt;code&gt;[r] [m] [n] [l]&lt;/code&gt; 这几个音在词中的不同位置发音有所不同，当这几个音位于元音前面的时候需要带动后面的元音，就会感觉多出一个 &lt;code&gt;[ə]&lt;/code&gt;，比如 &lt;code&gt;[r]&lt;/code&gt; 就发音类似 &lt;code&gt;若&lt;/code&gt;。当这几个音位于元音后面时，就发他们本来的音，具体可以听上面的视频中的标准发音。&lt;/p&gt;
&lt;h2&gt;音节与重音&lt;/h2&gt;
&lt;p&gt;音节是读音的基本单位，任何单词的读音，都是分解为一个个音节来朗读。在英语中元音发音特别响亮，是构成音节的主要音，一个元音可构成一个音节，一个元音和一个或几个辅音结合也可以构成一个音节。通常情况下，一个单词中有几个元音就有几个音节（看音标而不是看字母）。汉语拼音中的声母和韵母拼在一起构成一个汉字，英语中的辅音就相当于声母，元音相当于韵母，因此，英语单词中元音前的辅音要给该元音当头，划在一起，构成一个音节。划分音节时，先从最后一个元音开始，其前的辅音要划给该元音当头，依次向前按此划分。&lt;/p&gt;
&lt;p&gt;如： &lt;code&gt;ru-ler[&apos;ru: lə]&lt;/code&gt; 尺子(最后一个元音是&lt;code&gt;[ə]&lt;/code&gt;，其前的辅音l要给该元音当头，构成一个音节 &lt;code&gt;[lə]&lt;/code&gt; ；依次向前的元音是 &lt;code&gt;[u:]&lt;/code&gt;，其前的辅音 &lt;code&gt;[r]&lt;/code&gt; 要给该元音当头，构成一个音节 &lt;code&gt;[ru:]&lt;/code&gt;，两个音节合起来读作 &lt;code&gt;[&apos;ru: lə]&lt;/code&gt;), &lt;code&gt;e-le-phant&lt;/code&gt; &lt;code&gt;[&apos;e lɪ fənt]&lt;/code&gt; 大象(最后一个元音是 &lt;code&gt;[ə]&lt;/code&gt;，其前的辅音 &lt;code&gt;[f]&lt;/code&gt; 要给该元音当头，构成一个音节 &lt;code&gt;[fənt]&lt;/code&gt;；依次向前的元音是 &lt;code&gt;[ɪ]&lt;/code&gt;，其前的辅音 &lt;code&gt;[l]&lt;/code&gt; 要给该元音当头，构成一个音节 &lt;code&gt;[lɪ]&lt;/code&gt;， 最前面的元音 &lt;code&gt;[e]&lt;/code&gt; 单独构成一个音节，三个音节合起来读作 &lt;code&gt;[&apos;e lɪ fənt]&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;关于重读音节的规律可以看这篇文章：&lt;a href=&quot;https://www.fluentu.com/blog/english-chi/%E8%8B%B1%E8%AF%AD-%E5%8D%95%E8%AF%8D-%E9%87%8D%E9%9F%B3/&quot; title=&quot;重读音节规律&quot;&gt;重读音节规律&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;拼读&lt;/h3&gt;
&lt;p&gt;看到陌生的单词如何把音标写出来并读出来，以及如何根据音标拼出单词可以看这个在网上流传很广的视频：&lt;a href=&quot;https://www.bilibili.com/video/av13735431?from=search&amp;#x26;seid=14346599940000501255&quot; title=&quot;周育如音标教学&quot;&gt;周育如音标教学&lt;/a&gt;，里面的规律总结的很不错，不过教学用的是 &lt;code&gt;KK&lt;/code&gt; 音标。拼读的主要规律就是把字母和音标对应起来，辅音对应的字母一般变化较少，可以直接标注，元音则根据不同的情况来分辨，最后在记住一些特殊的固定组合。这几种规律基本可以应对大多数单词，剩下的一些不在这些规律中的单词则需要单独记忆。规律总结如下表（用 &lt;code&gt;KK&lt;/code&gt; 音标标注）：&lt;/p&gt;
&lt;p&gt;| 字母 | 音标             | 字母 | 音标   | 字母 | 音标               | 字母 | 音标   |
| ---- | ---------------- | ---- | ------ | ---- | ------------------ | ---- | ------ |
| &lt;code&gt;a&lt;/code&gt;  | &lt;code&gt;[e][æ] [ə]&lt;/code&gt;     | &lt;code&gt;b&lt;/code&gt;  | &lt;code&gt;[b]&lt;/code&gt;  | &lt;code&gt;c&lt;/code&gt;  | &lt;code&gt;[k][s]&lt;/code&gt;           | &lt;code&gt;d&lt;/code&gt;  | &lt;code&gt;[d]&lt;/code&gt;  |
| &lt;code&gt;e&lt;/code&gt;  | &lt;code&gt;[i][ɛ] [ə][ɪ]&lt;/code&gt;  | &lt;code&gt;f&lt;/code&gt;  | &lt;code&gt;[f]&lt;/code&gt;  | &lt;code&gt;g&lt;/code&gt;  | &lt;code&gt;[g]&lt;/code&gt;              | &lt;code&gt;h&lt;/code&gt;  | &lt;code&gt;[h]&lt;/code&gt;  |
| &lt;code&gt;i&lt;/code&gt;  | &lt;code&gt;[ai][ɪ] [ə][ɪ]&lt;/code&gt; | &lt;code&gt;j&lt;/code&gt;  | &lt;code&gt;[dʒ]&lt;/code&gt; | &lt;code&gt;k&lt;/code&gt;  | &lt;code&gt;[k]&lt;/code&gt;              | &lt;code&gt;l&lt;/code&gt;  | &lt;code&gt;[l]&lt;/code&gt;  |
| &lt;code&gt;m&lt;/code&gt;  | &lt;code&gt;[m]&lt;/code&gt;            | &lt;code&gt;n&lt;/code&gt;  | &lt;code&gt;[n]&lt;/code&gt;  | &lt;code&gt;o&lt;/code&gt;  | &lt;code&gt;[o][ɑ][ɔ][ʌ] [ə]&lt;/code&gt; | &lt;code&gt;p&lt;/code&gt;  | &lt;code&gt;[p]&lt;/code&gt;  |
| &lt;code&gt;q&lt;/code&gt;  | &lt;code&gt;[kw]&lt;/code&gt;           | &lt;code&gt;r&lt;/code&gt;  | &lt;code&gt;[r]&lt;/code&gt;  | &lt;code&gt;s&lt;/code&gt;  | &lt;code&gt;[s][z]&lt;/code&gt;           | &lt;code&gt;t&lt;/code&gt;  | &lt;code&gt;[t]&lt;/code&gt;  |
| &lt;code&gt;u&lt;/code&gt;  | &lt;code&gt;[ju][ʌ] [ə][ʊ]&lt;/code&gt; | &lt;code&gt;v&lt;/code&gt;  | &lt;code&gt;[v]&lt;/code&gt;  | &lt;code&gt;w&lt;/code&gt;  | &lt;code&gt;[w]&lt;/code&gt;              | &lt;code&gt;x&lt;/code&gt;  | &lt;code&gt;[ks]&lt;/code&gt; |
| &lt;code&gt;y&lt;/code&gt;  | &lt;code&gt;[j][ɪ]&lt;/code&gt;         | &lt;code&gt;z&lt;/code&gt;  | &lt;code&gt;z&lt;/code&gt;    | &lt;code&gt;ar&lt;/code&gt; | &lt;code&gt;[ɑr]&lt;/code&gt;             | &lt;code&gt;er&lt;/code&gt; | &lt;code&gt;[ɝ]&lt;/code&gt;  |&lt;/p&gt;
&lt;p&gt;当元音字母 &lt;code&gt;a e i o u&lt;/code&gt; 位于重读音节的时候，最常发的音是 &lt;code&gt;[æ] [ɛ] [I] [ɑ] [ʌ]&lt;/code&gt;，其次是跟它们本身的读音一样的 &lt;code&gt;[e] [i] [ai] [o] [ju]&lt;/code&gt;，字母 &lt;code&gt;o&lt;/code&gt; 还有其他两种变化 &lt;code&gt;[ɔ][ʌ]&lt;/code&gt;。当它们不位于重读音节的时候，绝大多数情况五个字母都发 &lt;code&gt;[ə]&lt;/code&gt;，另外的情况就是 &lt;code&gt;e&lt;/code&gt; 和 &lt;code&gt;i&lt;/code&gt; 发 &lt;code&gt;[ɪ]&lt;/code&gt;，&lt;code&gt;u&lt;/code&gt; 发 &lt;code&gt;[ʊ]&lt;/code&gt;。当然元音变化非常多，这里只是一些常见的规律，还有很多的特殊情况需要单独记忆。&lt;/p&gt;
&lt;p&gt;特殊规则： 1. 当字母 &lt;code&gt;e&lt;/code&gt; 位于单词末尾不发音，则单词中其他元音字母发它们本身的读音，比如 &lt;code&gt;name&lt;/code&gt; 发音 &lt;code&gt;[nem]&lt;/code&gt;，&lt;code&gt;ride&lt;/code&gt; 发音 &lt;code&gt;[raid]&lt;/code&gt;，&lt;code&gt;came&lt;/code&gt; 发音 &lt;code&gt;[kem]&lt;/code&gt;，&lt;code&gt;take&lt;/code&gt; 发音 &lt;code&gt;[tek]&lt;/code&gt;。 2. &lt;code&gt;er ir or ur&lt;/code&gt; 位于重读音节时，发音 &lt;code&gt;[ɝ]&lt;/code&gt; ，比如 &lt;code&gt;bird work her&lt;/code&gt;，不是重读音节且在字尾发 &lt;code&gt;[ɚ]&lt;/code&gt;，比如 &lt;code&gt;worker [wɝkɚ] doctor Mister&lt;/code&gt;（现在很多字典直接将&lt;code&gt;[ɝ]&lt;/code&gt; 和 &lt;code&gt;[ɚ]&lt;/code&gt; 统一写作 &lt;code&gt;[ər]&lt;/code&gt;）。位于重读音节时，&lt;code&gt;ar&lt;/code&gt; 可能发作 &lt;code&gt;[ɑr]&lt;/code&gt;，&lt;code&gt;or&lt;/code&gt; 可能发作 &lt;code&gt;[or] [ɔr]&lt;/code&gt;。 3. &lt;code&gt;oo&lt;/code&gt; 常常发音 &lt;code&gt;[ʊ][u]&lt;/code&gt;. 4. &lt;code&gt;au ou aw ow&lt;/code&gt; 常常发音 &lt;code&gt;[au]&lt;/code&gt;，比如 &lt;code&gt;house hour how&lt;/code&gt;。 5. &lt;code&gt;th&lt;/code&gt; 通常发音 &lt;code&gt;[θ][ð]&lt;/code&gt;，&lt;code&gt;ch&lt;/code&gt; 通常发音 &lt;code&gt;[tʃ]&lt;/code&gt;，&lt;code&gt;sh&lt;/code&gt; 通常发音 &lt;code&gt;[ʃ]&lt;/code&gt;，&lt;code&gt;ph&lt;/code&gt; 通常发 &lt;code&gt;[f]&lt;/code&gt;，&lt;code&gt;ng&lt;/code&gt; 在通常发 &lt;code&gt;[ŋ][ŋg]&lt;/code&gt;，&lt;code&gt;nk&lt;/code&gt; 通常发 &lt;code&gt;[ŋk]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里只是一些简单的规则，其实要搞清楚各种拼读规则这些远远不够，而且规则只是辅助，最好还是通过多练习单词的拼读然后查看音标来熟悉拼读规则，练多了自然能够掌握。这里还有一个比较详细的拼读规则总结：&lt;a href=&quot;https://1drv.ms/w/s!AlHFIlOnArmLpgxul_zhExQPVPC4&quot; title=&quot;OneDrive分享链接 - 密码9527&quot;&gt;OneDrive分享链接 - 密码9527&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;音标的规律总结如上，剩下的就是不断地练习，融会贯通了，一方面是单词的拼读，另一方面就是发音的练习。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/KK%E9%9F%B3%E6%A8%99&quot; title=&quot;维基百科-KK音标&quot;&gt;维基百科-KK音标&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/21535804&quot; title=&quot;一张表看懂美式音标和英式音标的差异&quot;&gt;一张表看懂美式音标和英式音标的差异&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/19913374/answer/740176529&quot; title=&quot;知乎用户 英语老师Della 的回答&quot;&gt;知乎用户 英语老师Della 的回答&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en-yinbiao.xiao84.com/biao/4986.html&quot; title=&quot;KK音标-DJ音标对照表&quot;&gt;KK音标-DJ音标对照表&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/english-logo.DCX2-CjV.jpg"/><enclosure url="/_astro/english-logo.DCX2-CjV.jpg"/></item><item><title>Linux和Mac系统目录结构</title><link>https://clloz.com/blog/linux-directory-structure</link><guid isPermaLink="true">https://clloz.com/blog/linux-directory-structure</guid><pubDate>Tue, 08 Oct 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;介绍一下 &lt;code&gt;Linux&lt;/code&gt; 系统的目录结构，&lt;code&gt;MacOS&lt;/code&gt; 的目录结构也类似。&lt;/p&gt;
&lt;h2&gt;目录结构&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/bin&lt;/code&gt;：&lt;code&gt;bin&lt;/code&gt; 是 &lt;code&gt;Binary&lt;/code&gt; 的缩写, 这个目录存放着最经常使用的命令。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/boot&lt;/code&gt;：这里存放的是启动 &lt;code&gt;Linux&lt;/code&gt; 时使用的一些核心文件，包括一些连接文件以及镜像文件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/dev&lt;/code&gt; ：&lt;code&gt;dev&lt;/code&gt; 是 &lt;code&gt;Device&lt;/code&gt; (设备)的缩写, 该目录下存放的是 &lt;code&gt;Linux&lt;/code&gt; 的外部设备，在 &lt;code&gt;Linux&lt;/code&gt; 中访问设备的方式和访问文件的方式是相同的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc&lt;/code&gt;：这个目录用来存放所有的系统管理所需要的配置文件和子目录。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/home&lt;/code&gt;：用户的主目录，在 &lt;code&gt;Linux&lt;/code&gt; 中，每个用户都有一个自己的目录，一般该目录名是以用户的账号命名的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/lib&lt;/code&gt;：这个目录里存放着系统最基本的动态连接共享库，其作用类似于 &lt;code&gt;Windows&lt;/code&gt; 里的 &lt;code&gt;DLL&lt;/code&gt; 文件。几乎所有的应用程序都需要用到这些共享库。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/lost+found&lt;/code&gt; ：这个目录一般情况下是空的，当系统非法关机后，这里就存放了一些文件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/media&lt;/code&gt;：&lt;code&gt;linux&lt;/code&gt;系统会自动识别一些设备，例如 &lt;code&gt;U&lt;/code&gt; 盘、光驱等等，当识别后，&lt;code&gt;linux&lt;/code&gt; 会把识别的设备挂载到这个目录下。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/mnt&lt;/code&gt;：系统提供该目录是为了让用户临时挂载别的文件系统的，我们可以将光驱挂载在 &lt;code&gt;/mnt/&lt;/code&gt; 上，然后进入该目录就可以查看光驱里的内容了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/opt&lt;/code&gt;：这是给主机额外安装软件所摆放的目录。比如你安装一个 &lt;code&gt;ORACLE&lt;/code&gt; 数据库则就可以放到这个目录下。默认是空的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/proc&lt;/code&gt;：这个目录是一个虚拟的目录，它是系统内存的映射，我们可以通过直接访问这个目录来获取系统信息。这个目录的内容不在硬盘上而是在内存里，我们也可以直接修改里面的某些文件，比如可以通过下面的命令来屏蔽主机的 &lt;code&gt;ping&lt;/code&gt; 命令，使别人无法 &lt;code&gt;ping&lt;/code&gt; 你的机器：&lt;code&gt;echo 1 &gt;/proc/sys/net/ipv4/icmp_echo_ignore_all&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/root&lt;/code&gt; ：该目录为系统管理员，也称作超级权限者的用户主目录。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/sbin&lt;/code&gt;：&lt;code&gt;s&lt;/code&gt; 就是 &lt;code&gt;Super User&lt;/code&gt; 的意思，这里存放的是系统管理员使用的系统管理程序。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/selinux&lt;/code&gt;： 这个目录是 &lt;code&gt;Redhat/CentOS&lt;/code&gt; 所特有的目录，&lt;code&gt;Selinux&lt;/code&gt; 是一个安全机制，类似于&lt;code&gt;windows&lt;/code&gt; 的防火墙，但是这套机制比较复杂，这个目录就是存放 &lt;code&gt;selinux&lt;/code&gt; 相关的文件的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/srv&lt;/code&gt;：该目录存放一些服务启动之后需要提取的数据。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/sys&lt;/code&gt;：这是 &lt;code&gt;linux2.6&lt;/code&gt; 内核的一个很大的变化。该目录下安装了 &lt;code&gt;2.6&lt;/code&gt; 内核中新出现的一个文件系统 &lt;code&gt;sysfs&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sysfs&lt;/code&gt; 文件系统集成了下面 &lt;code&gt;3&lt;/code&gt; 种文件系统的信息：针对进程信息的 &lt;code&gt;proc&lt;/code&gt; 文件系统、针对设备的 &lt;code&gt;devfs&lt;/code&gt; 文件系统以及针对伪终端的 &lt;code&gt;devpts&lt;/code&gt; 文件系统。 该文件系统是内核设备树的一个直观反映。&lt;/p&gt;
&lt;p&gt;当一个内核对象被创建的时候，对应的文件和目录也在内核对象子系统中被创建。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/tmp&lt;/code&gt;：这个目录是用来存放一些临时文件的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/usr&lt;/code&gt;：这是一个非常重要的目录，用户的很多应用程序和文件都放在这个目录下，类似于 &lt;code&gt;windows&lt;/code&gt; 下的 &lt;code&gt;program files&lt;/code&gt; 目录。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/usr/bin&lt;/code&gt;：系统用户使用的应用程序。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/usr/sbin&lt;/code&gt;：超级用户使用的比较高级的管理程序和系统守护程序。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/usr/src&lt;/code&gt;：内核源代码默认的放置目录。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/var&lt;/code&gt;：这个目录中存放着在不断扩充着的东西，我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/run&lt;/code&gt;：是一个临时文件系统，存储系统启动以来的信息。当系统重启时，这个目录下的文件应该被删掉或清除。如果你的系统上有 &lt;code&gt;/var/run&lt;/code&gt; 目录，应该让它指向 &lt;code&gt;run&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Mac 文件系统结构&lt;/h2&gt;
&lt;p&gt;查看 &lt;code&gt;Mac&lt;/code&gt; 的文件系统结构很简单，在终端输入 &lt;code&gt;man hier&lt;/code&gt; 查看即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;     /             #root directory of the filesystem

     /bin/        # user utilities fundamental to both single-user and multi-user environments

     /dev/         #block and character device files

                   fd/  #file descriptor files; see fd(4)

     /etc/         #system configuration files and scripts

     /mach_kernel  #kernel executable (the operating system loaded into memory at boot time).

     /sbin/        #system programs and administration utilities fundamental to both single-user and multi-user environments

     /tmp/         #temporary files

     /usr/         #contains the majority of user utilities and applications

                   bin/     # common utilities, programming tools, and applications
                   include/  #standard C include files

                             arpa/       #C include files for Internet service protocols
                             hfs/        #C include files for HFS
                             machine/    #machine specific C include files
                             net/        #misc network C include files
                             netinet/   # C include files for Internet standard protocols; see inet(4)
                             nfs/        #C include files for NFS (Network File System)
                             objc/       #C include files for Objective-C
                             protocols/  #C include files for Berkeley service protocols
                             sys/        #system C include files (kernel data structures)
                             ufs/        #C include files for UFS

                   lib/      #archive libraries
                   libexec/  #system daemons &amp;#x26; system utilities (executed by other programs)
                   local/   #executables, libraries, etc. not included by the basic operating system
                   sbin/     #system daemons &amp;#x26; system utilities (executed by users)
                   share/    #architecture-independent data files
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果要从终端进入 &lt;code&gt;U&lt;/code&gt; 盘或者移动硬盘的存储设备，可以进入 &lt;code&gt;/volumes&lt;/code&gt; 目录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是 &lt;code&gt;Liunx&lt;/code&gt; 和 &lt;code&gt;Mac&lt;/code&gt; 文件系统的目录结构。&lt;/p&gt;</content:encoded><h:img src="/_astro/macos.RfaH1x-K.jpg"/><enclosure url="/_astro/macos.RfaH1x-K.jpg"/></item><item><title>日语送气音和不送气音</title><link>https://clloz.com/blog/japanese-aspirate</link><guid isPermaLink="true">https://clloz.com/blog/japanese-aspirate</guid><pubDate>Sun, 06 Oct 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在刚接触日语的时候都会遇到一个疑问，而且一般教科书上都没有给出这个问题的答案，就是不送气音和送气音的问题，比如 &lt;code&gt;わたし&lt;/code&gt; 我们在听录音的时候感觉日本人好像都说的是 &lt;code&gt;わだし&lt;/code&gt;，在比如说 &lt;code&gt;ですか&lt;/code&gt;，录音里面好像说的 &lt;code&gt;ですが&lt;/code&gt;，很让人摸不着头脑，教材或者老师也很少解答这个问题，我是在初学日语的时候对这点很疑惑，带着不了解的问题学习感觉就很难受，但是一直都没找到合理的解释，现在学日语也有好一段时间了，结合自己的练习和一些资料总算有了一些了解。&lt;/p&gt;
&lt;h2&gt;送气音和不送气音&lt;/h2&gt;
&lt;p&gt;先说一说常用的几个词语的意思： 1. 清音：发音时声带不震动的音 2. 浊音：发音时声带震动的音 3. 元音：发音过程中由气流通过口腔而不受阻碍发出的音 4. 辅音：发音时气流在口腔或咽头受到阻碍而形成的音 5. 送气音：发音时呼出的气流较强的塞音或者塞擦音 6. 不送气音：发音时气流呼出较弱的音 其中清音和浊音相对，元音和辅音相对，送气音和不送气音相对。&lt;/p&gt;
&lt;p&gt;先要说明的是送气音和不送气音在日语里面是对意思的表达没有影响的，所以如果是初学者，即使不会发不送气音也没关系，就直接按照假名读就行了，不要为了发不送气音而把清音发成浊音。&lt;/p&gt;
&lt;p&gt;在中文里面，清音有 &lt;code&gt;b，p，d，t，g，k，z，c，zh，ch，j，q&lt;/code&gt;，其中送气音有 &lt;code&gt;p，t，k，c，chi，q&lt;/code&gt;，剩下的都是不送气音，在中文里面我们的送气不送气是区分意思的，但是在我们的中文里面浊音非常少，汉语普通话的 &lt;code&gt;22&lt;/code&gt; 个辅音中共有 &lt;code&gt;5&lt;/code&gt; 个浊音，其中鼻音三个：&lt;code&gt;n、m、ng&lt;/code&gt;，边音一个：&lt;code&gt;l&lt;/code&gt;，擦音（也是翘舌音）一个：&lt;code&gt;r&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;普通话中&lt;code&gt;p[pʰ]&lt;/code&gt;和 &lt;code&gt;b[p]&lt;/code&gt; 均为清音，其分别在于送气与不送气，是两个音位。由于普通话中 &lt;code&gt;b[p]&lt;/code&gt;、 &lt;code&gt;d[t]&lt;/code&gt;、&lt;code&gt;g[k]&lt;/code&gt;实为清音却标作浊音字母，使得中国大部分学习有浊音和不送气清音的外语（如法语、日语等）时时常把不送气清音跟浊音混淆，因为在普通话中的 &lt;code&gt;b[p]&lt;/code&gt;、&lt;code&gt;p[pʰ]&lt;/code&gt; 等都是一个音，就是清辅音。&lt;/p&gt;
&lt;p&gt;因为上面的这些原因，我们汉语母语者在学习日语的时候很难区分不送气清音和浊音的区别，因为在我们的概念里很少遇到浊音，都是送气和不送气，所以我们对送气和不送气十分敏感，对清浊不敏感。日语母语者和我们相反，他们很难分辨送气音和不送气音，但是对声带是否震动十分敏感。比如日本人很难区分中文的 &lt;code&gt;兔子跑了&lt;/code&gt; 和 &lt;code&gt;肚子饱了&lt;/code&gt;，而我们则对所谓的不送气清音和浊音就有点分辨不清。&lt;/p&gt;
&lt;p&gt;另外根据我的经验，由于日语经常出现一句话很长，所以如果一直发送气音的话，气会不够，所以因为气的关系，日语母语者在说话的时候就会有各种省气的方式，比如不送气清音。而我们也可以试一试一口气快速的念 &lt;code&gt;た&lt;/code&gt;，到后面就会就会发成不送气的形式。这应该是在漫长的过程中自然而然地形成的语言习惯，这样念起来比较轻松，比如 &lt;code&gt;仕事　しごと&lt;/code&gt; 这个词，按理说应该发音 &lt;code&gt;shigoto&lt;/code&gt;，但是在听日本人说的时候经常听到他们在 &lt;code&gt;ご&lt;/code&gt; 的前面有一个 &lt;code&gt;n&lt;/code&gt; 的鼻音，这是他们的语言习惯，但是这样确实读起来更轻松。&lt;/p&gt;
&lt;p&gt;最后还是那句话，由于日语区分意思是根据清浊，而不是送气不送气，所以我们在初学的时候尽量还是按照假名的正确发音读，不要刻意把清音发成浊音，至于地道的感觉我们可以在后面熟练了以后在慢慢地模仿锻炼，其实都是水到渠成的。&lt;/p&gt;
&lt;h2&gt;送气音的一些规律&lt;/h2&gt;
&lt;p&gt;一、音读音变规则： 1. 当两个送气音相遇时，就会将前一个送气音变为“っ”&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt; 学（がく）＋　校（こう）→　学校（がっこう）
 日（にち）＋　記（き）　→　日記（にっき）
 一（いち）＋　冊（さつ）→　一冊（いっさつ）
 察（さつ）＋　する　　　→　察する(さっする)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;前一个送气音变“っ”后，后一个送气音如为は行则变为半浊音。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt; 失（しつ）＋　敗（はい）→　失敗（しっぱい）
 一（いち）＋　匹（ひき）→　一匹（いっぴき）
 烈（れつ）＋　風（ふう）→　烈風（れっぷう）
 鉄（てつ）＋　片（へん）→　鉄片（てっぺん）
 一（いち）＋　本（ほん）→　一本（いっぽん）
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;前一个汉字以ん结尾，后一汉字发音的第一假名若是は行开头的变为ぱ行,也有少数变成ば行的。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt; 先（せん）＋　輩（はい）→先輩（せんぱい）
 心（しん）＋　配（はい）→　心配（しんぱい）
 神（しん）＋　秘（ひ）　→　神秘（しんぴ）
 何（なん）＋　分（ふん）→　何分（なんぷん）
 南（なん）＋　北（ほく）→　南北（なんぼく）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;二、训读汉字发音规则 1. &lt;code&gt;か、さ、た、は&lt;/code&gt;即送气音开头的单词，接在其他词后构成复合词时，发生浊音化。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt; 物（もの）　＋　語り（かたり）→　物語（ものがたり）
 鼻（はな）　＋　血（ち）　　　→　鼻血（はなぢ）
 足（あし）　＋　取り（とり　）→　足取り（あしどり）
 昔（むかし）＋　話（はなし）　→　昔話（むかしばなし）
 人（ひと）　＋　人（ひと）　　→　人々（ひとびと）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三、例外的情况： 1. 但本身含浊音的训读词，不发生连浊。 &lt;code&gt;紙屑（かみくず）大風（おおかぜ）&lt;/code&gt; 2. 动词与动词或动词与宾语的复合不发生连浊。 &lt;code&gt;読み書き（よみかき）飯炊き（めしたき）&lt;/code&gt; 3. 前一个汉字最后一个假名在え段的，变为同一行的あ段假名。 &lt;code&gt;雨（あめ）水（みず）雨水（あまみず）&lt;/code&gt; &lt;code&gt;稲（いね）光（ひかり）稲光（いなびかり）&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这篇文章是我对结合自己的经验以及一些网络上的资料整理的关于日语学习中送气音和不送气音的一些只是，如有错误，希望指正。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/20689046/answer/149331998&quot; title=&quot;日语中送气音、不送气音的区分重要吗？ - ishimura的知乎回答&quot;&gt;日语中送气音、不送气音的区分重要吗？ - ishimura的回答 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/20689046/answer/15868243&quot; title=&quot;日语中送气音、不送气音的区分重要吗？ - 高天原的回答 - 知乎 &quot;&gt;日语中送气音、不送气音的区分重要吗？ - 高天原的回答 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/fujisan.BBwHHsHK.jpeg"/><enclosure url="/_astro/fujisan.BBwHHsHK.jpeg"/></item><item><title>阿里云OSS和CDN的配置</title><link>https://clloz.com/blog/aliyun-cdn-oss</link><guid isPermaLink="true">https://clloz.com/blog/aliyun-cdn-oss</guid><pubDate>Thu, 03 Oct 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我的网站是部署在阿里云上面的，使用差不多两年了，随着自己对于建站的知识了解的更多，以及网络和 &lt;code&gt;web服务器&lt;/code&gt; 的更多的了解，在解决问题的过程中也遇到过不少坑。这篇文章说一下阿里云的 &lt;code&gt;oss&lt;/code&gt; 和 &lt;code&gt;cdn&lt;/code&gt; 的配置问题。&lt;/p&gt;
&lt;h2&gt;关于搭建个人网站&lt;/h2&gt;
&lt;p&gt;其实我搭建个人网站主要是让自己在建站的过程中了解一个网站的开发，部署过程，从实践中来学习，因为看书或者看别人的文章有很多细节是无法了解的，自己试过了才知道会遇到哪些问题，以及哪些自己不知道的知识点。就好像看别人做菜感觉很简单，真的上手发现很多东西其实并不像表面那样简单，有很多细节别人得心应手没有讲，但是你并不知道，甚至连切菜都不同的菜不同的切法。同时自己搭建个人网站也能把自己的一些学习笔记记录下来，和别人分享，把自己对于网站的一些想法付诸实践，也是挺有趣的。&lt;/p&gt;
&lt;p&gt;虽然可能个人网站的访问量不会很大，不过能用上的东西还是尽量用上，比如 &lt;code&gt;cdn&lt;/code&gt;，&lt;code&gt;https&lt;/code&gt;，比如对象存储，虽然对于我们的这种没什么人访问的网站可能并没有什么本质的区别，但是在使用的过程中我们会去了解这些内容，能学到很多。&lt;/p&gt;
&lt;h2&gt;CDN&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cdn&lt;/code&gt; 其实说简单点就是我们网站的资源存储在一台服务器上，不论哪个用户从什么地方发起请求最后都要到我们的服务器去取对应的资源，这样可能会造成拥堵和延迟，为了解决这个问题才有 &lt;code&gt;Content Delivery Network 内容分发网络&lt;/code&gt;，将源站内容分发至最接近用户的节点，使用户可就近取得所需内容，提高用户访问的响应速度和成功率。解决因分布、带宽、服务器性能带来的访问延迟问题，适用于站点加速、点播、直播等场景。&lt;/p&gt;
&lt;p&gt;最简单的CDN网络由一个DNS服务器和几台缓存服务器组成： 1. 当用户点击网站页面上的内容URL，经过本地DNS系统解析，DNS系统会最终将域名的解析权交给CNAME指向的CDN专用DNS服务器。 2. CDN的DNS服务器将CDN的全局负载均衡设备IP地址返回用户。 3. 用户向CDN的全局负载均衡设备发起内容URL访问请求。 4. CDN全局负载均衡设备根据用户IP地址，以及用户请求的内容URL，选择一台用户所属区域的区域负载均衡设备，告诉用户向这台设备发起请求。 5. 区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务，选择的依据包括：根据用户IP地址，判断哪一台服务器距用户最近；根据用户所请求的URL中携带的内容名称，判断哪一台服务器上有用户所需内容；查询各个服务器当前的负载情况，判断哪一台服务器尚有服务能力。基于以上这些条件的综合分析之后，区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址。 6. 全局负载均衡设备把服务器的IP地址返回给用户。 7. 用户向缓存服务器发起请求，缓存服务器响应用户请求，将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容，而区域均衡设备依然将它分配给了用户，那么这台服务器就要向它的上一级缓存服务器请求内容，直至追溯到网站的源服务器将内容拉到本地。&lt;/p&gt;
&lt;p&gt;以上就是 &lt;code&gt;cdn&lt;/code&gt; 的一些基础只是，&lt;code&gt;cdn&lt;/code&gt; 最主要的目的就是加速网站的访问。&lt;/p&gt;
&lt;h2&gt;对象存储&lt;/h2&gt;
&lt;p&gt;阿里云的 &lt;code&gt;oss&lt;/code&gt; 就是对象存储的一种实现，对象存储是和快存储，文件存储相对应的概念，想了解对象存储的可以看知乎上的这个问题 &lt;a href=&quot;https://www.zhihu.com/question/21536660&quot; title=&quot;块存储、文件存储、对象存储这三者的本质差别是什么？&quot;&gt;块存储、文件存储、对象存储这三者的本质差别是什么？&lt;/a&gt;，我一般是用来放一些网站要用的图片、视频以及其他小文件。&lt;/p&gt;
&lt;h2&gt;OSS 和 CDN 相关问题&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;oss&lt;/code&gt; 和 &lt;code&gt;cdn&lt;/code&gt; 的配置都很简单，而且阿里云的文档也很详细，我就不介绍了，下面把我遇到的一些问题跟大家说一说。&lt;/p&gt;
&lt;h2&gt;OSS 跨域&lt;/h2&gt;
&lt;p&gt;关于跨域的问题可以看我的这篇文章：&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/05/09/cors/&quot; title=&quot;浏览器同源策略和跨域方法&quot;&gt;浏览器同源策略和跨域方法&lt;/a&gt; 如果你想要在页面上有对 &lt;code&gt;oss&lt;/code&gt; 资源的请求，或者是脚本中要请求对应资源，都会因为浏览器的同源策略而请求失败，比如我在我的服务器上就有一个文件夹专门放文章要用到的页面实例，之前做的一个音乐播放器要访问 &lt;code&gt;oss&lt;/code&gt; 上的图片和音频文件，就报错了。我们可以在 &lt;code&gt;阿里云控制台 -&gt; 对象存储OSS -&gt; 对应bucket -&gt; 基础设置 -&gt; 跨域设置&lt;/code&gt; 中来配置我们的对应的域名，比如我可以配置 &lt;code&gt;*.clloz.com&lt;/code&gt; 来完成跨域访问。&lt;/p&gt;
&lt;h2&gt;网站字体跨域问题&lt;/h2&gt;
&lt;p&gt;配置了 &lt;code&gt;cdn&lt;/code&gt; 以后，我们的页面会到 &lt;code&gt;cdn&lt;/code&gt; 服务器去请求字体文件，此时我们的 &lt;code&gt;origin&lt;/code&gt; 是我们的网站域名，所以访问字体文件会导致跨域，解决方法是我们在 &lt;code&gt;cdn&lt;/code&gt; 控制台对应域名的配置中选择 &lt;code&gt;缓存配置 -&gt; http头&lt;/code&gt;，然后添加 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;，值为 &lt;code&gt;*&lt;/code&gt; 或者你的网站域名。需要注意的是这里是服务器请求 &lt;code&gt;cdn&lt;/code&gt; 服务器的资源导致跨域，而不是服务器上的资源被请求跨域，所以在服务器上配置是无效的。&lt;/p&gt;
&lt;h2&gt;配置防盗链&lt;/h2&gt;
&lt;p&gt;当没有为 &lt;code&gt;oss&lt;/code&gt; 绑定 &lt;code&gt;cdn&lt;/code&gt; 域名的时候，我们可以直接在 &lt;code&gt;oss&lt;/code&gt; 里面配置，不过这样的做法不好，我咨询过阿里云的工程师，他们还是建议把 &lt;code&gt;oss&lt;/code&gt; 的 &lt;code&gt;bucket&lt;/code&gt; 设置为私有，然后通过 &lt;code&gt;cdn&lt;/code&gt; 来访问 &lt;code&gt;bucket&lt;/code&gt; 中的资源。需要注意的是，当 &lt;code&gt;bucket&lt;/code&gt; 设置为私有以后，我们通过设置白名单来访问 &lt;code&gt;bucket&lt;/code&gt; 上的资源都是有时间限制的，所以一定要通过 &lt;code&gt;cdn&lt;/code&gt; 访问。当我们为 &lt;code&gt;bucket&lt;/code&gt; 配置了 &lt;code&gt;cdn&lt;/code&gt; 以后，我们就可以通过 &lt;code&gt;cdn&lt;/code&gt; 的域名来访问对应资源，我们打开 &lt;code&gt;oss&lt;/code&gt; 中的文件还是能看到长长的链接后面跟着 &lt;code&gt;expire&lt;/code&gt; 等各种参数，但其实我们只要选择自由域名，后面的参数不要管就可以了，相当于 &lt;code&gt;oss&lt;/code&gt; 为 &lt;code&gt;cdn&lt;/code&gt; 开辟了一个绿色通道，我们虽然访问私有的 &lt;code&gt;oss&lt;/code&gt; 有各种限制，但是通过 &lt;code&gt;cdn&lt;/code&gt; 我们可以直接访问，这里也体现出 &lt;code&gt;cdn&lt;/code&gt; 的另一个有点，对网站其实有一定的保护作用，因为很多内容的访问都是访问的 &lt;code&gt;cdn&lt;/code&gt; 服务器，请求并不会发送到真正的服务器。当 &lt;code&gt;cdn&lt;/code&gt; 和 &lt;code&gt;oss&lt;/code&gt; 绑定以后，对 &lt;code&gt;cdn&lt;/code&gt; 的配置会覆盖 &lt;code&gt;oss&lt;/code&gt; 的配置，比如 &lt;code&gt;cdn&lt;/code&gt; 和 &lt;code&gt;oss&lt;/code&gt; 都可以设置白名单，但是真正生效的是 &lt;code&gt;cdn&lt;/code&gt; 的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/cdn-oss.IiP-bugd_ZIwLP.webp&quot; alt=&quot;cdn-oss&quot; title=&quot;cdn-oss&quot;&gt;&lt;/p&gt;
&lt;h2&gt;工单系统&lt;/h2&gt;
&lt;p&gt;最后说一下阿里云的工单系统，我用过几次，总的来说体验还是不错的，反应很及时，问题也都解决了，如果你的服务器遇到什么问题，不妨试一试。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是我对 &lt;code&gt;cdn&lt;/code&gt; 和 &lt;code&gt;oss&lt;/code&gt; 的一些理解和使用心得，希望对你有用。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/36514327/answer/193768864&quot; title=&quot;阿里云云栖社区知乎回答&quot;&gt;阿里云云栖社区知乎回答&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/aliyun.Bd237PDx.jpeg"/><enclosure url="/_astro/aliyun.Bd237PDx.jpeg"/></item><item><title>静态资源的一些问题</title><link>https://clloz.com/blog/static-source</link><guid isPermaLink="true">https://clloz.com/blog/static-source</guid><pubDate>Thu, 03 Oct 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在写 &lt;code&gt;cdn&lt;/code&gt; 和 对象存储文章的时候，看到了一些跟静态资源有关的问题，这里就来做一些整理。&lt;/p&gt;
&lt;h2&gt;什么是静态资源&lt;/h2&gt;
&lt;p&gt;不根据访问的条件变化的资源就是静态资源，比如 &lt;code&gt;html，js，css，webfont&lt;/code&gt; 等文件。&lt;/p&gt;
&lt;h2&gt;为什么很多网站的静态资源使用独立的域名&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;避免发送无意义的 &lt;code&gt;cookie&lt;/code&gt;，当我们的浏览器第一次请求服务器的时候，会根据服务器响应报文中的 &lt;code&gt;set-Cookie&lt;/code&gt; 来保存 &lt;code&gt;cookie&lt;/code&gt;，以后再次向这个服务器发送请求的时候都会带上 &lt;code&gt;cookie&lt;/code&gt;，所以我们即使向服务器请求静态资源，这个 &lt;code&gt;cookie&lt;/code&gt; 也会发送，并且服务器对于这些 cookie 不会做任何处理，它们只是在毫无意义的消耗带宽。所以你应该确保对于静态内容的请求是无coockie的请求。需要注意的是子域名也会受到感染，所以要使用独立域名。&lt;/li&gt;
&lt;li&gt;动静分离。静态资源与动态内容分离，有利于部署于CDN。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HTTP&lt;/code&gt; 协议对同一个域名的同时下载线程数有限制。主要是为了优化下载速度，防止同一域名下下载线程数过多，导致下载速度变慢。各个浏览器都会遵守这个规定，但是限制的数目可能不一致。基于这个原因，可将资源部署于不同的域名，以达到最大化并发下载。&lt;/li&gt;
&lt;li&gt;静态资源独立部署，为全局产品服务。方便复用，放在一个服务器上的文件可以共其他服务器上的产品使用。 比如 &lt;code&gt;taobao.com&lt;/code&gt; 和 &lt;code&gt;tmall.com&lt;/code&gt; 都会用到 &lt;code&gt;tbcdn.cn&lt;/code&gt; 上的静态资源，这样同时也有利于最大化利用客户端缓存。比如访问 &lt;code&gt;taobao.com&lt;/code&gt;，缓存了 &lt;code&gt;tbcdn.cn&lt;/code&gt; 上的某个 &lt;code&gt;js&lt;/code&gt; 文件，之后再访问 &lt;code&gt;tmall.com&lt;/code&gt; 时，也用到此 &lt;code&gt;js&lt;/code&gt; 文件，不必再从 &lt;code&gt;tbcdn.cn&lt;/code&gt; 上下载，直接用客户端缓存即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;静态资源加速方法&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/30780216&quot; title=&quot;Web静态资源缓存及优化&quot;&gt;Web静态资源缓存及优化&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/ziwozizhan/article/details/53420501&quot; title=&quot;静态资源放置于独立域名下的好处&quot;&gt;静态资源放置于独立域名下的好处&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>SSH简介和使用</title><link>https://clloz.com/blog/ssh-rsa</link><guid isPermaLink="true">https://clloz.com/blog/ssh-rsa</guid><pubDate>Wed, 02 Oct 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ssh&lt;/code&gt; 是 &lt;code&gt;Secure Shell&lt;/code&gt; 的缩写，是应用层上的协议，主要是保证远程登录的安全。我们使用 &lt;code&gt;GitHub&lt;/code&gt; 以及登录远程服务器都经常需要使用 &lt;code&gt;ssh&lt;/code&gt;，这篇文章简单介绍一下 &lt;code&gt;ssh&lt;/code&gt; 和使用方法。&lt;code&gt;ssh&lt;/code&gt; 有多种实现，一般我们使用的是免费开源的 &lt;code&gt;openssh&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;ssh 基本原理&lt;/h2&gt;
&lt;p&gt;传统的网络服务程序，如FTP、Pop和Telnet其本质上都是不安全的；因为它们在网络上用明文传送数据、用户帐号和用户口令，很容易受到中间人（&lt;code&gt;man-in-the-middle&lt;/code&gt;）攻击方式的攻击。就是存在另一个人或者一台机器冒充真正的服务器接收用户传给服务器的数据，然后再冒充用户把数据传给真正的服务器。为了解决这个问题，&lt;code&gt;ssh&lt;/code&gt; 协议提供了两种身份验证方式，都是使用非对称加密，关于非对称加密的内容可以看我的这篇文章&lt;a href=&quot;https://www.clloz.com/programming/network/2019/05/02/https&quot; title=&quot;HTTPS&quot;&gt;HTTPS&lt;/a&gt;。一般我们把被登录的服务器称为服务端，而请求登录的机器称为客户端。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基于口令的安全验证：只要你知道自己帐号和口令，就可以登录到远程主机。所有传输的数据都会被加密，但是不能保证你正在连接的服务器就是你想连接的服务器。可能会有别的服务器在冒充真正的服务器，也就是受到“中间人”这种方式的攻击。&lt;/li&gt;
&lt;li&gt;在客户端生成公钥和私钥，然后将公钥储存到服务端。当客服端请求连接到SSH服务器时，客户端软件就会向服务器发出请求，请求用你的密匙进行安全验证。服务器收到请求之后，先在该服务器上你的主目录下寻找你的公用密匙，然后把它和你发送过来的公用密匙进行比较。如果两个密匙一致，服务器就用公用密匙加密“质询”（&lt;code&gt;challenge&lt;/code&gt;）并把它发送给客户端软件。客户端软件收到“质询”之后就可以用你的私人密匙解密再把它发送给服务器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;口令登录的过程大概如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;远程 &lt;code&gt;Server&lt;/code&gt; 收到 &lt;code&gt;Client&lt;/code&gt; 端用户的登录请求，&lt;code&gt;Server&lt;/code&gt; 把自己的公钥发给用户。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Client&lt;/code&gt; 使用这个公钥，将密码进行加密。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Client&lt;/code&gt; 将加密的密码发送给 &lt;code&gt;Server&lt;/code&gt; 端。&lt;/li&gt;
&lt;li&gt;远程 &lt;code&gt;Server&lt;/code&gt; 用自己的私钥，解密登录密码，然后验证其合法性。&lt;/li&gt;
&lt;li&gt;若验证结果，给 &lt;code&gt;Client&lt;/code&gt; 相应的响应。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种登录方式的风险是我们不知道现在响应我们的是不是目标服务器，如果一个攻击者中途拦截 &lt;code&gt;Client&lt;/code&gt; 的登录请求，向其发送自己的公钥，&lt;code&gt;Client&lt;/code&gt; 端用攻击者的公钥进行数据加密。攻击者接收到加密信息后再用自己的私钥进行解密，不就窃取了 &lt;code&gt;Client&lt;/code&gt; 的登录信息了吗？这就是所谓的中间人攻击。所以我们使用口令登录的方式第一次登录的时候客户端会出现如下提醒：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;The authenticity of host &apos;ssh-server.example.com (12.18.429.21)&apos; can&apos;t be established.
RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d.
Are you sure you want to continue connecting (yes/no)?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你输入 &lt;code&gt;yes&lt;/code&gt; 的时候，就会出现如下信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Warning: Permanently added &apos;ssh-server.example.com,12.18.429.21&apos; (RSA) to the list of known hosts. Password: (enter password)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;告诉你该 &lt;code&gt;host&lt;/code&gt; 已被确认，并被追加到文件 &lt;code&gt;known_hosts&lt;/code&gt; 中，以后你就可以正常使用口令登录了。&lt;/p&gt;
&lt;p&gt;公钥登录的过程大概如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Client&lt;/code&gt; 将自己的公钥存放在 &lt;code&gt;Server&lt;/code&gt; 上，追加在文件 &lt;code&gt;authorized_keys&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Server&lt;/code&gt; 端接收到 &lt;code&gt;Client&lt;/code&gt; 的连接请求后，会在 &lt;code&gt;authorized_keys&lt;/code&gt; 中匹配到 &lt;code&gt;Client&lt;/code&gt; 的公钥 &lt;code&gt;pubKey&lt;/code&gt;，并生成随机数 &lt;code&gt;R&lt;/code&gt;，用 &lt;code&gt;Client&lt;/code&gt; 的公钥对该随机数进行加密得到 &lt;code&gt;pubKey(R)&lt;/code&gt;，然后将加密后信息发送给 &lt;code&gt;Client&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Client&lt;/code&gt; 端通过私钥进行解密得到随机数 &lt;code&gt;R&lt;/code&gt; ，然后对随机数R和本次会话的 &lt;code&gt;SessionKey&lt;/code&gt; 利用 &lt;code&gt;MD5&lt;/code&gt; 生成摘要 &lt;code&gt;Digest1&lt;/code&gt;，发送给 &lt;code&gt;Server&lt;/code&gt; 端。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Server&lt;/code&gt; 端会也会对&lt;code&gt;R&lt;/code&gt; 和 &lt;code&gt;SessionKey&lt;/code&gt; 利用同样摘要算法生成 &lt;code&gt;Digest2&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Server&lt;/code&gt;端会最后比较 &lt;code&gt;Digest1&lt;/code&gt; 和 &lt;code&gt;Digest2&lt;/code&gt; 是否相同，完成认证过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个过程看起来有点复杂，还是建议线看一下文章&lt;a href=&quot;https://www.clloz.com/programming/network/2019/05/02/https&quot; title=&quot;HTTPS&quot;&gt;HTTPS&lt;/a&gt;中的非对称加密的知识。这种方式在网络上传递的只有公钥，所以是相对来说安全很多的。并且免去了我们每次登录都要输入密码的麻烦，也很方便。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;~/.ssh&lt;/code&gt; 文件夹中一般会出现四种文件&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;id_rsa&lt;/code&gt;：保存私钥&lt;/li&gt;
&lt;li&gt;&lt;code&gt;id_rsa.pub&lt;/code&gt;：保存公钥&lt;/li&gt;
&lt;li&gt;&lt;code&gt;authorized_keys&lt;/code&gt;：保存已授权的客户端公钥 &lt;strong&gt;注意，该文件在服务器的 .ssh 文件夹中默认没有，需要自己创建，需要给该文件 600 的权限，否则可能出现 ssh 无法登录的情况&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;known_hosts&lt;/code&gt;：存储是已认证的远程主机 &lt;code&gt;host key&lt;/code&gt;，每个 &lt;code&gt;SSH Server&lt;/code&gt; 都有一个 &lt;code&gt;secret&lt;/code&gt;, &lt;code&gt;unique ID&lt;/code&gt;, 叫做 &lt;code&gt;host key&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;每次 &lt;code&gt;Client&lt;/code&gt; 向 &lt;code&gt;Server&lt;/code&gt; 发起连接的时候，不仅仅 &lt;code&gt;Server&lt;/code&gt; 要验证 &lt;code&gt;Client&lt;/code&gt; 的合法性，&lt;code&gt;Client&lt;/code&gt; 同样也需要验证 &lt;code&gt;Server&lt;/code&gt; 的身份，&lt;code&gt;SSH client&lt;/code&gt; 就是通过 &lt;code&gt;known_hosts&lt;/code&gt; 中的 &lt;code&gt;host key&lt;/code&gt; 来验证&lt;code&gt;Server&lt;/code&gt; 的身份的。&lt;/p&gt;
&lt;h2&gt;安装使用&lt;/h2&gt;
&lt;h2&gt;安装 ssh&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 默认就安装了 &lt;code&gt;ssh&lt;/code&gt; 的客户端和服务端，不过服务端默认是关闭的，开启和关闭的命令如下，不过一般我们是不需要开启的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#启动服务：
sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist

#停止服务：
sudo launchctl unload -w /System/Library/LaunchDaemons/ssh.plist

#查看服务器状态：
sudo launchctl list | grep sshd
#0   com.openssh.sshd 开启成功
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他的环境安装都大同小异，我就用 &lt;code&gt;CentOS&lt;/code&gt; 举个例子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#查看是否已经安装ssh-server
rpm -qa | grep ssh #看看有没有openssh-server

#安装
yum install -y openssl openssh-server

#开启PermitRootLogin，RSAAuthentication，PubkeyAuthentication
vim /etc/ssh/sshd_config

#启动ssh的服务：
systemctl start sshd.service

#设置开机自动启动ssh服务
systemctl enable sshd.service

#查看ssh服务是否开启
ps -e | grep sshd

#设置文件夹访问权限
$ cd ~
$ chmod 700 .ssh
$ chmod 600 .ssh/*
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 默认已经安装了 &lt;code&gt;ssh-keygen&lt;/code&gt; 和 &lt;code&gt;ssh-copy-id&lt;/code&gt;，如果这两个命令不能使用，请安装。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#在客户端生成密钥对
ssh-keygen -t rsa -C &quot;your_email@example.com&quot; #一路回车即可，中间有一个设置私钥口令passphrase，直接回车设置为空即可

#将公钥复制到远程主机
ssh-copy-id root@1.1.1.1 #填入服务器的用户名和ip，当然你也可以手动复制文件到服务器
ssh user@remote -p port &apos;mkdir -p .ssh &amp;#x26;&amp;#x26; cat &gt;&gt; .ssh/authorized_keys&apos; &amp;#x3C; ~/.ssh/id_rsa.pub #没有安装ssh-copy-id的话这条命令也可以

#远程登录服务器
ssh root@1.1.1.1 #如果修改了默认的22端口，可以加入 -P 参数来指定端口
ssh 192.168.1.100      # 默认利用当前宿主用户的用户名登录
ssh omd@192.168.1.100  # 利用远程机的用户登录
ssh omd@192.168.1.100  -o stricthostkeychecking=no # 首次登陆免输yes登录
ssh omd@192.168.1.100 &quot;ls /home/omd&quot;  # 当前服务器A远程登录服务器B后执行某个命令
ssh omd@192.168.1.100 -t &quot;sh /home/omd/ftl.sh&quot;  # 当前服务器A远程登录服务器B后执行某个脚本

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ssh-keygen&lt;/code&gt; 参数说明：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;-t&lt;/code&gt;: 密钥类型, 可以选择 &lt;code&gt;dsa&lt;/code&gt; | &lt;code&gt;ecdsa&lt;/code&gt; | &lt;code&gt;ed25519&lt;/code&gt; | &lt;code&gt;rsa&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-f&lt;/code&gt;: 密钥目录位置, 默认为当前用户 &lt;code&gt;home&lt;/code&gt; 路径下的 &lt;code&gt;.ssh&lt;/code&gt; 隐藏目录, 也就是 &lt;code&gt;~/.ssh/&lt;/code&gt;, 同时默认密钥文件名以 &lt;code&gt;id_rsa&lt;/code&gt; 开头. 如果是 &lt;code&gt;root&lt;/code&gt; 用户, 则在 &lt;code&gt;/root/.ssh/id_rsa&lt;/code&gt;, 若为其他用户, 则在 &lt;code&gt;/home/username/.ssh/id_rsa&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-C&lt;/code&gt;: 指定此密钥的备注信息, 需要配置多个免密登录时, 建议携带;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-N&lt;/code&gt;: 指定此密钥对应的密码, 如果指定此参数, 则命令执行过程中就不会出现交互确认密码的信息了.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在配置好 &lt;code&gt;ssh&lt;/code&gt; 以后我们也可以用 &lt;code&gt;scp&lt;/code&gt; 来传输文件了，具体的使用方法看这篇文章&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/09/mac-scp/&quot; title=&quot;Mac用scp上传或下载文件&quot;&gt;Mac用scp上传或下载文件&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是 &lt;code&gt;ssh&lt;/code&gt; 的基础知识和使用方法了，对于 &lt;code&gt;GitHub&lt;/code&gt; 的配置基本是一样的，将公钥复制到 &lt;code&gt;GitHub&lt;/code&gt; 的设置中去即可。如果 &lt;code&gt;.ssh&lt;/code&gt; 中有超过一对密钥，可以用 &lt;code&gt;config&lt;/code&gt; 文件来配置，形式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# --- Sourcetree Generated ---
Host Clloz-GitHub
    HostName github.com
    User Clloz
    PreferredAuthentications publickey
    IdentityFile /Users/clloz/.ssh/Clloz-GitHub
    UseKeychain yes
    AddKeysToAgent yes
# ----------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ssh 配置文件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ssh&lt;/code&gt; 的配置文件的位置是 &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;，其中有几个会用到的配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 是否允许用密码登录
PasswordAuthentication yes

#启用密钥验证
RSAAuthentication yes
PubkeyAuthentication yes
#指定公钥数据库文件
AuthorsizedKeysFile.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置文件修改后要重启 &lt;code&gt;ssh&lt;/code&gt; 服务后才能生效 &lt;code&gt;systemctl restart sshd&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;ssh-agent&lt;/h2&gt;
&lt;p&gt;最后说一说 &lt;code&gt;ssh-agent&lt;/code&gt;。一般我们在用 &lt;code&gt;ssh-keygen&lt;/code&gt; 生成密钥的时候默认不给私钥设置 &lt;code&gt;passphrase&lt;/code&gt;。如果你有安全需求，担心私钥泄露的风险，那么你可以为私钥设置密码，在创建密钥的时候可以使用 &lt;code&gt;-N passphrase&lt;/code&gt; 来给生成的私钥设置密码，也可以用 &lt;code&gt;ssh-keygen -p&lt;/code&gt; 来为已经生成的私钥设置或修改密码，如果你要取消密码也可以用这个命令，输入旧密码后新密码直接回车即可。&lt;/p&gt;
&lt;p&gt;给私钥设置密码后，安全性是提高了，但是每次我们要用私钥进行登录的时候都需要手动输入一次密码，非常麻烦。&lt;code&gt;ssh-agent&lt;/code&gt; 就能帮我们解决这个问题。它相当于一个代理，跟踪我们的私钥和私钥对应的 &lt;code&gt;passphrase&lt;/code&gt;，当服务器请求私钥的时候，它会帮我们处理，我们也不用重复输入我们的密码了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ssh-agent&lt;/code&gt; 在 &lt;code&gt;mac&lt;/code&gt; 和 &lt;code&gt;linux&lt;/code&gt; 中默认是开机自动启动的，你可以用 &lt;code&gt;ps -ef | grep ssh-agent&lt;/code&gt; 查看，如果没有可以使用 &lt;code&gt;eval &quot;$(ssh-agent -s)&quot;&lt;/code&gt; 启动。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;要让 &lt;code&gt;ssh-agent&lt;/code&gt; 处理对应的私钥要用 &lt;code&gt;ssh-add&lt;/code&gt; 将私钥加入。我们可以先用 &lt;code&gt;ssh-add -l&lt;/code&gt; 先查看是否已经加入 &lt;code&gt;ssh-agent&lt;/code&gt; 的管理，如果没有则使用 &lt;code&gt;ssh-add -K key_location&lt;/code&gt; 来加入。&lt;code&gt;ssh-add&lt;/code&gt; 有如下一下参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-D #删除ssh-agent中所有私钥(指纹)

-d key_file #删除指定私钥

-L #列出agent当前主机上所有公钥参数，即公钥文件中的内容

-l #列出agent当前已保存的指纹信息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;mac&lt;/code&gt; 上我们还要为对应的私钥添加配置，配置文件的路径是 &lt;code&gt;~/.ssh/config&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Host *
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_rsa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你为你的私钥进行了独立的命名，将上面的 &lt;code&gt;id_rsa&lt;/code&gt; 换成对应的名字即可。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;ssh-agent&lt;/code&gt; 还有一个比较好用的功能，就是 &lt;code&gt;forwarding&lt;/code&gt;。如果你有服务器 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt;，你用自己的电脑登录上了 &lt;code&gt;a&lt;/code&gt;，现在又想到 &lt;code&gt;b&lt;/code&gt; 上进行某个操作，你不想退出 &lt;code&gt;a&lt;/code&gt; 也不想把密钥放到 &lt;code&gt;a&lt;/code&gt; 上，那么你知道配置 &lt;code&gt;forwarding&lt;/code&gt; 就能够在 &lt;code&gt;a&lt;/code&gt; 上直接登录 &lt;code&gt;b&lt;/code&gt;。只要在 &lt;code&gt;client， a， b&lt;/code&gt; 上的 &lt;code&gt;~/.ssh/config&lt;/code&gt; 里都加上如下配置即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Host *
　　ForwardAgent yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 &lt;code&gt;ssh-agent&lt;/code&gt; 的配置可以参考 &lt;a href=&quot;https://docs.github.com/cn/free-pro-team@latest/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent&quot; title=&quot;Github 帮助文档&quot;&gt;Github 帮助文档&lt;/a&gt; 还有 &lt;a href=&quot;https://www.fythonfang.com/blog/2017/12/27/ssh-agent-and-ssh-agent-forwarding&quot; title=&quot;配置SSH agent 和 SSH agent forwarding转发&quot;&gt;配置SSH agent 和 SSH agent forwarding转发&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/diffx/p/9553587.html&quot; title=&quot;图解ssh&quot;&gt;图解ssh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/wanglemao/article/details/88413573&quot; title=&quot;Mac 启动 SSH&quot;&gt;Mac 启动 SSH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/liuhouhou/p/8975812.html&quot; title=&quot;CentOS7安装和配置SSH&quot;&gt;CentOS7安装和配置SSH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/shoufeng/p/11022258.html&quot; title=&quot;Linux - 配置SSH免密通信 - “ssh-keygen”的基本用法&quot;&gt;Linux - 配置SSH免密通信 - “ssh-keygen”的基本用法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/21999778&quot; title=&quot;ssh的基本用法&quot;&gt;ssh的基本用法&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/ssh.NNEIEtI7.png"/><enclosure url="/_astro/ssh.NNEIEtI7.png"/></item><item><title>ffmpeg的安装和使用和gyao视频下载</title><link>https://clloz.com/blog/ffmpeg-install-usage-gyao-stream</link><guid isPermaLink="true">https://clloz.com/blog/ffmpeg-install-usage-gyao-stream</guid><pubDate>Tue, 01 Oct 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ffmpeg&lt;/code&gt; 是大名鼎鼎的开源免费跨平台的视频和音频流方案，提供了录制、转换以及流化音视频的完整解决方案。我以前用过来转视频格式，后来一直就没用过了。十月的日剧新番，暌违13年的经典日剧 &lt;code&gt;結婚できない男&lt;/code&gt; 要出续作了，作为特别喜欢这部剧的忠实粉丝，我一直关注这关西电视台的 &lt;code&gt;まだ結婚できない男&lt;/code&gt; 的主页，&lt;code&gt;20&lt;/code&gt; 号的时候推出了 &lt;code&gt;チェインストーリー　#0.5　中川家の人々&lt;/code&gt;，不过 &lt;code&gt;gyao&lt;/code&gt; 上的视频似乎锁了 &lt;code&gt;IP&lt;/code&gt;，而且我挂日本的 &lt;code&gt;vpn&lt;/code&gt; 也无济于事，看不了。这时候 &lt;code&gt;ffmpeg&lt;/code&gt; 就派上用场了。本文就介绍一下关于 &lt;code&gt;ffmpeg&lt;/code&gt; 的安装和使用。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 上的安装十分简单，直接用 &lt;code&gt;homebrew&lt;/code&gt; 就可以了，其他平台的自己 &lt;code&gt;google&lt;/code&gt; 一下，应该也很容易。&lt;a href=&quot;https://github.com/FFmpeg/FFmpeg&quot; title=&quot;这里&quot;&gt;这里&lt;/a&gt;是 &lt;code&gt;ffmpeg&lt;/code&gt; 的 &lt;code&gt;GitHub&lt;/code&gt; 地址。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install ffmpeg
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ffmpeg Document&lt;/code&gt; 链接：&lt;a href=&quot;https://ffmpeg.org/ffmpeg.html&quot; title=&quot;https://ffmpeg.org/ffmpeg.html&quot;&gt;https://ffmpeg.org/ffmpeg.html&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;常用命令参数&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...

ffmpeg -i [输入文件名] [参数选项] -f [格式] [输出文件]

#主要参数
-i #设定输入流
-f #设定输出格式
-ss #开始时间

#视频参数
-b #设定视频流量(码率)，默认为200Kbit/s
-r #设定帧速率，默认为25
-s #设定画面的宽与高
-aspect #设定画面的比例
-vn #不处理视频
-vcodec #设定视频编解码器，未设定时则使用与输入流相同的编解码器

#音频参数
-ar #设定采样率
-ac #设定声音的Channel数
-acodec #设定声音编解码器，未设定时则使用与输入流相同的编解码器
-an #不处理音频
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数选项： 1. -an: 去掉音频 2. -vn: 去掉视频 3. -acodec: 设定音频的编码器，未设定时则使用与输入流相同的编解码器。音频解复用在一般后面加copy表示拷贝 4. -vcodec: 设定视频的编码器，未设定时则使用与输入流相同的编解码器，视频解复用一般后面加copy表示拷贝 5. –f: 输出格式（视频转码） 6. -bf: B帧数目控制 7. -g: 关键帧间隔控制(视频跳转需要关键帧) 8. -s: 设定画面的宽和高，分辨率控制(352*278) 9. -i: 设定输入流 10. -ss: 指定开始时间（0:0:05） 11. -t: 指定持续时间（0:05） 12. -b: 设定视频流量，默认是200Kbit/s 13. -aspect: 设定画面的比例 14. -ar: 设定音频采样率 15. -ac: 设定声音的Channel数 16. -r: 提取图像频率（用于视频截图） 17. -c:v: 输出视频格式 18. -c:a: 输出音频格式 19. -y: 输出时覆盖输出目录已存在的同名文件&lt;/p&gt;
&lt;h2&gt;查看媒体文件信息&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg -i video.mp4
ffmpeg -i video.mp4 -hide_banner
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;视频格式转换&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg -i input.avi output.mp4
ffmpeg -i input.mp4 output.ts
ffmpeg -i input.webm -qscale 0 output.mp4 #维持源视频文件的质量
ffmpeg -formats #检查支持的格式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般我们在网站上看到的视频，都能够用 &lt;code&gt;ffmpeg&lt;/code&gt; 下载下来，打开控制台，找到形如 &lt;code&gt;https://vdn.vzuu.com/Act-ss-m3u8......&lt;/code&gt; 的请求，然后用 &lt;code&gt;ffmpeg -i &quot;https://vdn.vzuu.com/SD/49c8...&quot; output.mp4&lt;/code&gt; 即可下载下来。&lt;/p&gt;
&lt;h2&gt;提取音频和视频&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg -i input.mp4 -acodec copy -vn output.aac #提取音频
ffmpeg -i input.mp4 -vcodec copy -an output.mp4 #提取视频
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;剪切视频&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg -ss 00:00:15 -t 00:00:05 -i input.mp4 -vcodec copy -acodec copy output.mp4 #-ss表示开始切割的时间，-t表示要切多少
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;码率控制&lt;/h2&gt;
&lt;p&gt;码率 &lt;code&gt;bitrate&lt;/code&gt; 就是单位时间内传输的位数。我们的视频音频传输的时候也是二进制流，我们说的视频音频大小就是一个媒体文件的总位数，比如一个 &lt;code&gt;20M&lt;/code&gt; 的文件，它的总位数就是 &lt;code&gt;20 * 1024 * 1024 * 8 = 167772160bit&lt;/code&gt;，如果这个视频的时常是 &lt;code&gt;60s&lt;/code&gt;，那么它的码率就是 &lt;code&gt;167772160 / 60 = 2796 kbps&lt;/code&gt;，所以码率的计算公式就为 &lt;code&gt;bitrate = file size / duration&lt;/code&gt;，媒体文件越大，一般码率就越高，因为单位时间内要传输的内容更多。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg -i input.mp4 -b:v 2000k output.mp4 #不破坏分辨率压缩码率
ffmpeg -i input.mp4 -b:v 2000k -bufsize 2000k output.mp4 #减少码率波动
ffmpeg -i input.mp4 -b:v 2000k -bufsize 2000k -maxrate 2500k output.mp4 #设置码率波动的阈值
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编码格式转换&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg -i input.mp4 -vcodec h264 output.mp4
ffmpeg -i input.mp4 -vcodec mpeg4 output.mp4
#调用外部的x265或者X264编码器
ffmpeg -i input.mp4 -c:v libx265 output.mp4
ffmpeg -i input.mp4 -c:v libx264 output.mp4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;滤镜 filter&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg -i input.mp4 -vf scale=960:540 output.mp4 #将输入的1920x1080缩小到960x540输出
ffmpeg -i input.mp4 -i logo.png -filter_complex overlay output.mp4 #为视频添加logo
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;抓去视频帧保存为图片&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#-r 表示每一秒几帧
#-q:v表示存储jpeg的图像质量，一般2是高质量。
#-ss 表示开始时间
#-t表示共要多少时间。
ffmpeg -i input.mp4 -ss 00:00:20 -t 10 -r 1 -q:v 2 -f image2 pic-%03d.jpeg
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提取 gyao 的视频&lt;/h2&gt;
&lt;p&gt;有了上面的内容，提取 &lt;code&gt;gyao&lt;/code&gt; 上的视频就很简单了，只要找到文件的请求路径就可以了，我们可以到这个网站&lt;a href=&quot;http://kmake.net/gyaommsurl/?url=&quot; title=&quot;Gyao MMS URL&quot;&gt;Gyao MMS URL&lt;/a&gt;来提取，进入网站直接输入就能得到不同分辨率的文件地址，得到地址后我们直接用 &lt;code&gt;ffmpeg&lt;/code&gt; 下载到本地即可。不过下载的时候最好还是挂着梯子，文件很大的时候由于网络问题很难下载成功，我下 &lt;code&gt;720p&lt;/code&gt; 的时候就失败了，最后只能下载低清晰度的。&lt;/p&gt;
&lt;p&gt;趁着日本节点的网络环境比较好，顺利把 &lt;code&gt;720p&lt;/code&gt; 的下载下来了，已经上传到百度云了，下载地址和提取码&lt;code&gt;https://pan.baidu.com/s/1MlC7kHXHJXGyqH50e3SBgg jzaq&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;关于视音频技术&lt;/h2&gt;
&lt;p&gt;学习就像不断的画圆，自己掌握的知识越多，自己的半径就越大，面积也越大，不过同时增长的是自己未知的东西，并且增长更快，也就是所谓的知道的越多，不知道的就越多。&lt;code&gt;ffmpeg&lt;/code&gt; 是视音频技术的基础和入门，如果想学习这方面技术的可以看： 1. &lt;a href=&quot;https://blog.csdn.net/leixiaohua1020&quot; title=&quot;雷霄骅的专栏&quot;&gt;雷霄骅的专栏&lt;/a&gt; 2. &lt;a href=&quot;https://blog.csdn.net/leixiaohua1020/article/details/15811977&quot; title=&quot;FFMPEG视音频编解码零基础学习方法&quot;&gt;FFMPEG视音频编解码零基础学习方法&lt;/a&gt; 人的一生太短暂了，知识的汪洋能取一勺就不简单了，只能抓紧有限的时间尽量多的学习。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jianshu.com/p/ddafe46827b7&quot; title=&quot;ffmpeg基础使用  合肥懒皮&quot;&gt;ffmpeg基础使用 合肥懒皮&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://koukokutou-club.com/gyao/ffmpeg.html&quot; title=&quot;GyaO（ギャオ）をFFmpegで完全攻略&quot;&gt;GyaO（ギャオ）をFFmpegで完全攻略&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/46903150&quot; title=&quot;FFMPEG常用命令-慕课网知乎&quot;&gt;FFMPEG常用命令-慕课网知乎&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/ffmpeg.CykN9RqK.png"/><enclosure url="/_astro/ffmpeg.CykN9RqK.png"/></item><item><title>如何使用IRC</title><link>https://clloz.com/blog/how-to-use-irc</link><guid isPermaLink="true">https://clloz.com/blog/how-to-use-irc</guid><pubDate>Thu, 26 Sep 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;lazycat&lt;/code&gt; 的博客看到他对 &lt;code&gt;emacs&lt;/code&gt; 学习的建议，了解到了 &lt;code&gt;IRC&lt;/code&gt; 也就是 &lt;code&gt;Internet Relay Chat&lt;/code&gt;，于是便做了了解，这篇文章告诉大家如何使用 &lt;code&gt;IRC&lt;/code&gt;。关于 &lt;code&gt;IRC&lt;/code&gt; 的介绍请看&lt;a href=&quot;https://zh.wikipedia.org/wiki/IRC&quot; title=&quot;维基百科&quot;&gt;维基百科&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;IRC&lt;/code&gt; 的使用形式有多种，可以有 &lt;code&gt;GUI&lt;/code&gt; 程序，也可以在 &lt;code&gt;shell&lt;/code&gt; 中使用，在 &lt;code&gt;emacs&lt;/code&gt; 里也有相应的插件，这里我介绍两个客户端软件，一个是 &lt;code&gt;GUI&lt;/code&gt; 软件 &lt;code&gt;LimeChat&lt;/code&gt;，另一个是我在终端使用的 &lt;code&gt;irssi&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LimeChat&lt;/code&gt; 可以直接在 &lt;code&gt;app store&lt;/code&gt; 下载，是免费的。 &lt;code&gt;irssi&lt;/code&gt; 的安装也很简单 &lt;code&gt;brew install irssi&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;注册&lt;/h2&gt;
&lt;p&gt;要进入目标频道聊天，我们先要连接服务器，比如&lt;code&gt;freenode&lt;/code&gt; 的服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/connect irc.freenode.net 6667
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们需要注册一个我们的 &lt;code&gt;ID&lt;/code&gt;，注册方法（可以查看 &lt;code&gt;freenode&lt;/code&gt; 的&lt;a href=&quot;https://freenode.net/kb/answer/registration&quot; title=&quot;网站&quot;&gt;网站&lt;/a&gt;具体了解）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#设置昵称
/nick xxxx
#进行注册
/msg NickServ REGISTER password youremail@example.com
# 收到邮件后，执行邮件中收到的命令
/msg NickServ VERIFY REGISTER yourname xxxxxxx
# 如果不想公开邮箱可以设置隐藏:
/msg NickServ SET HIDEMAIL ON
# 登录
/msg NickServ IDENTIFY password
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们就可以用 &lt;code&gt;join&lt;/code&gt; 命令加入我们想要加入的频道了，比如你想要加入 &lt;code&gt;emacs&lt;/code&gt; 频道就执行如下命令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/join #emacs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们就可以在自己喜欢的频道聊天了。&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;不管是用 &lt;code&gt;GUI&lt;/code&gt; 软件还是在终端使用，基本都是用命令来控制，关于命令可以查看维基百科的页面&lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_Internet_Relay_Chat_commands&quot; title=&quot;List of Internet Relay Chat commands&quot;&gt;List of Internet Relay Chat commands&lt;/a&gt;以及 这个&lt;a href=&quot;https://kiwiirc.com/docs/client/commands&quot; title=&quot;页面&quot;&gt;页面&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;常用的一些命令有：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/join ubuntu-cn # 中文频道  ubuntu-cn；linuxba
/list # 频道列表
/names [#聊天室] # 列出当前服务器或指定聊天室下的所有人员名称（无法列出隐藏人员）
/who # 查看频道的所有人
/whois [name] # 查看某人的基本资料
/ison &amp;#x3C;name1&gt; &amp;#x3C;name2&gt; … # 查询指定别名是否在线
/info # 查询服务器信息
/admin # 查询当前服务器上的Admin
/lusers # 查询当前服务器上的统计信息
/motd # 查询当前服务器今日的统计信息
/links # 查询当前的服务器，解析当前的有几个服务器
/msg &amp;#x3C;name&gt; &amp;#x3C;msg&gt; # 向某人发私消息（会打开新窗口）
/query &amp;#x3C;name&gt; &amp;#x3C;msg&gt; # 向某人发私消息（新开窗口且转换到这个窗口）
/say &amp;#x3C;name&gt; &amp;#x3C;msg&gt; # 向某人说话（不新开窗口）
/notice &amp;#x3C;name&gt; &amp;#x3C;msg&gt; # 向指定人发出注意消息
/me &amp;#x3C;动作&gt;，在当前聊天室窗口中做出动作。 如做出晕倒动作：/me 晕倒
/away &amp;#x3C;auto reply msg&gt; # 留下信息说明暂时离开，别人向你发出私聊时将会返回此消息，再重新输入 /away（不指定参数）则解除离开状
/ignore &amp;#x3C;name&gt; # 忽略某人的聊天内容
/set autolog on # 自动保存聊天记录
/part &amp;#x3C;channel&gt; &amp;#x3C;msg&gt; # 退出一个频道，不加频道名退出当前频道，后面可以跟退出原因。
/disconnect #退出服务器
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;发送代码和图片&lt;/h2&gt;
&lt;p&gt;发送代码使用代码粘贴网站来发送，不要直接发送大段代码，代码粘贴网站可以使用 &lt;a href=&quot;https://paste.ubuntu.com/&quot; title=&quot;paste.ubuntu.com&quot;&gt;paste.ubuntu.com&lt;/a&gt;，发送图片可以使用&lt;a href=&quot;http://img.vim-cn.com/&quot; title=&quot;img.vim-cn.com&quot;&gt;img.vim-cn.com&lt;/a&gt;，当然也可以搜索其他可用的 &lt;code&gt;paste&lt;/code&gt; 网站。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.ubuntu.com.cn/IRC%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5&quot; title=&quot;IRC基本概念&quot;&gt;IRC基本概念&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.lenky.info/archives/2013/09/2341?utm_source=tuicool&amp;#x26;utm_medium=referral&quot; title=&quot;IRC快速发图&quot;&gt;IRC快速发图&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/irssi.CqO0RUWH.png"/><enclosure url="/_astro/irssi.CqO0RUWH.png"/></item><item><title>Japanese Number 日语数字</title><link>https://clloz.com/blog/japanese-number</link><guid isPermaLink="true">https://clloz.com/blog/japanese-number</guid><pubDate>Fri, 20 Sep 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;日语中的数字有很多种不同的说法，一般来说，一种是依据汉子的发音的读法：&lt;code&gt;いち、に、さん、し、ご、ろく、しち、はち、きゅう、じゅう&lt;/code&gt;，另一种就是日语自己的读法：&lt;code&gt;ひ、ふ、み、よ、いつ、む、なな、や、ここ、とお&lt;/code&gt;。但是在很多时候具体的使用并没有特定的规律，可能两种都可以，也可能有固定的用法，比如量词的使用，以及日期等，这篇文章整理一下目前我学到的数字用法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;知乎上有个回答对日语里面的几套数字做了更系统的整理，具体看回答&lt;a href=&quot;https://www.zhihu.com/question/30467965/answer/48257319&quot; title=&quot;如何正确地背诵日语中的日期？ - 雪见yukimi的回答 - 知乎&quot;&gt;如何正确地背诵日语中的日期？ - 雪见yukimi的回答 - 知乎&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;数字&lt;/h2&gt;
&lt;p&gt;单纯地数数还是比较简单的，没有太多特别的用法，也比较好记，而且一般学习日语的初期，这些数字接触非常多，所以印象还是比较深刻的。&lt;/p&gt;
&lt;p&gt;| 0：&lt;code&gt;ゼロ、れい&lt;/code&gt;          | １：&lt;code&gt;いち&lt;/code&gt;                     | ２：&lt;code&gt;に&lt;/code&gt;                       | ３：&lt;code&gt;さん&lt;/code&gt;             | ４：&lt;code&gt;し、よん&lt;/code&gt;                 |
| :----------------------- | :----------------------------- | :----------------------------- | :--------------------- | :----------------------------- |
| ５：&lt;code&gt;ご&lt;/code&gt;                 | ６：&lt;code&gt;ろく&lt;/code&gt;                     | ７：&lt;code&gt;しち、なな&lt;/code&gt;               | ８：&lt;code&gt;はち&lt;/code&gt;             | ９：&lt;code&gt;く、きゅう&lt;/code&gt;               |
| １０：&lt;code&gt;じゅう&lt;/code&gt;           | １１：&lt;code&gt;じゅういち&lt;/code&gt;             | １２：&lt;code&gt;じゅうに&lt;/code&gt;               | １３：&lt;code&gt;じゅうさん&lt;/code&gt;     | １４：&lt;code&gt;じゅうよん、じゅうし&lt;/code&gt;   |
| １５：&lt;code&gt;じゅうご&lt;/code&gt;         | １６：&lt;code&gt;じゅうろく&lt;/code&gt;             | １７：&lt;code&gt;じゅうなな、じゅうしち&lt;/code&gt; | １８：&lt;code&gt;じゅうはち&lt;/code&gt;     | １９：&lt;code&gt;じゅうく、じゅうきゅう&lt;/code&gt; |
| ２０：&lt;code&gt;にじゅう&lt;/code&gt;         | ３０：&lt;code&gt;さんじゅう&lt;/code&gt;             | ４０：&lt;code&gt;よんじゅう&lt;/code&gt;             | ５０：&lt;code&gt;ごじゅう&lt;/code&gt;       | ６０：&lt;code&gt;ろくじゅう&lt;/code&gt;             |
| ７０：&lt;code&gt;ななじゅう&lt;/code&gt;       | ８０：&lt;code&gt;はちじゅう&lt;/code&gt;             | ９０：&lt;code&gt;きゅうじゅう&lt;/code&gt;           | １００：&lt;code&gt;ひゃく&lt;/code&gt;       | ２００：&lt;code&gt;にひゃく&lt;/code&gt;             |
| ３００：&lt;code&gt;さんびゃく&lt;/code&gt;     | ４００：&lt;code&gt;よんひゃく&lt;/code&gt;           | ５００：&lt;code&gt;ごひゃく&lt;/code&gt;             | ６００：&lt;code&gt;ろっぴゃく&lt;/code&gt;   | ７００：&lt;code&gt;ななひゃく&lt;/code&gt;           |
| ８００：&lt;code&gt;はっぴゃく&lt;/code&gt;     | ９００：&lt;code&gt;きゅうひゃく&lt;/code&gt;         | １０００：&lt;code&gt;せん&lt;/code&gt;               | ３０００：&lt;code&gt;さんぜん&lt;/code&gt;   | ８０００：&lt;code&gt;はっせん&lt;/code&gt;           |
| １００００：&lt;code&gt;いちまん&lt;/code&gt;   | 十万：&lt;code&gt;じゅうまん&lt;/code&gt;             | 一百万：&lt;code&gt;ひゃくまん&lt;/code&gt;           | 一千万：&lt;code&gt;いっせんまん&lt;/code&gt; | 一亿：&lt;code&gt;いちおく&lt;/code&gt;               |
| ９００２：&lt;code&gt;きゅうせんに&lt;/code&gt; | ９０２０：&lt;code&gt;きゅうせんにじゅう&lt;/code&gt; | ９２００：&lt;code&gt;きゅうせんにひゃく&lt;/code&gt; | ０.１：&lt;code&gt;れいてんいち&lt;/code&gt;  | 2/3：&lt;code&gt;んぶんのに&lt;/code&gt;              |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;千的说法除了列出的三千和八千其他都是 &lt;code&gt;～せん&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;年龄&lt;/h2&gt;
&lt;p&gt;| 年齢 | 読み方                 |
| :--- | :--------------------- |
| 0岁  | &lt;code&gt;れいさい、ぜろさい&lt;/code&gt;   |
| 1岁  | &lt;code&gt;いっさい&lt;/code&gt;             |
| 2岁  | &lt;code&gt;にさい&lt;/code&gt;               |
| 3岁  | &lt;code&gt;さんさい&lt;/code&gt;             |
| 4岁  | &lt;code&gt;よんさい&lt;/code&gt;             |
| 5岁  | &lt;code&gt;ごさい&lt;/code&gt;               |
| 6岁  | &lt;code&gt;ろくさい&lt;/code&gt;             |
| 7岁  | &lt;code&gt;ななさい&lt;/code&gt;             |
| 8岁  | &lt;code&gt;はっさい&lt;/code&gt;             |
| 9岁  | &lt;code&gt;きゅうさい&lt;/code&gt;           |
| 10岁 | &lt;code&gt;じゅっさい&lt;/code&gt;           |
| 20岁 | &lt;code&gt;にじゅうさい、はたち&lt;/code&gt; |
| 30岁 | &lt;code&gt;みそじ&lt;/code&gt;               |
| 40岁 | &lt;code&gt;よそじ&lt;/code&gt;               |
| 50岁 | &lt;code&gt;いそじ&lt;/code&gt;               |
| 60岁 | &lt;code&gt;むそじ&lt;/code&gt;               |
| 70岁 | &lt;code&gt;ななそじ&lt;/code&gt;             |
| 80岁 | &lt;code&gt;やそじ&lt;/code&gt;               |
| 90岁 | &lt;code&gt;ここのそじ&lt;/code&gt;           |&lt;/p&gt;
&lt;h2&gt;时刻&lt;/h2&gt;
&lt;p&gt;| 一点：&lt;code&gt;いちじ&lt;/code&gt;       | 2点：&lt;code&gt;にじ&lt;/code&gt;      | 3点：&lt;code&gt;さんじ&lt;/code&gt;                | 4点：&lt;code&gt;よじ&lt;/code&gt;              | 5点：&lt;code&gt;ごじ&lt;/code&gt;          | 6点：&lt;code&gt;ろくじ&lt;/code&gt;          |
| :------------------- | :--------------- | :--------------------------- | :----------------------- | :------------------- | :--------------------- |
| 7点：&lt;code&gt;しちじ&lt;/code&gt;        | 8点：&lt;code&gt;はちじ&lt;/code&gt;    | 9点：&lt;code&gt;くじ&lt;/code&gt;                  | 10点：&lt;code&gt;じゅうじ&lt;/code&gt;         | 11点：&lt;code&gt;じゅういちじ&lt;/code&gt; | 12点：&lt;code&gt;じゅうにじ&lt;/code&gt;     |
| 0点：&lt;code&gt;れいじ&lt;/code&gt;        | 1分：&lt;code&gt;いっぷん&lt;/code&gt;  | 2分：&lt;code&gt;にふん&lt;/code&gt;                | 3分：&lt;code&gt;さんぷん&lt;/code&gt;          | 4分：&lt;code&gt;よんふん&lt;/code&gt;      | 5分：&lt;code&gt;ごふん&lt;/code&gt;          |
| 6分：&lt;code&gt;ろっぷん&lt;/code&gt;      | 7分：&lt;code&gt;ななふん&lt;/code&gt;  | 8分：&lt;code&gt;はっぷん&lt;/code&gt;              | 9分：&lt;code&gt;きゅうふん&lt;/code&gt;        | 10分：&lt;code&gt;じゅっぷん&lt;/code&gt;   | 11分：&lt;code&gt;じゅういっぷん&lt;/code&gt; |
| 15分：&lt;code&gt;じゅうごふん&lt;/code&gt; | 几分：&lt;code&gt;なんぷん&lt;/code&gt; | 30分：&lt;code&gt;さんじゅうぷん、はん&lt;/code&gt; | 45分：&lt;code&gt;よんじゅうごふん&lt;/code&gt; |                      |                        |&lt;/p&gt;
&lt;h2&gt;日期&lt;/h2&gt;
&lt;p&gt;| １日 &lt;code&gt;ついたち&lt;/code&gt;             | ２日 &lt;code&gt;ふつか&lt;/code&gt;             | ３日 &lt;code&gt;みっか&lt;/code&gt;             | ４日 &lt;code&gt;よっか&lt;/code&gt;           | ５日 &lt;code&gt;いつか&lt;/code&gt;           |
| :-------------------------- | :------------------------ | :------------------------ | :---------------------- | :---------------------- |
| ６日 &lt;code&gt;むいか&lt;/code&gt;               | ７日 &lt;code&gt;なのか&lt;/code&gt;             | ８日 &lt;code&gt;ようか&lt;/code&gt;             | ９日 &lt;code&gt;ここのか&lt;/code&gt;         | １０日 &lt;code&gt;とおか&lt;/code&gt;         |
| 11日 &lt;code&gt;じゅういちにち&lt;/code&gt;       | 12日 &lt;code&gt;じゅうににち&lt;/code&gt;       | 13日 &lt;code&gt;じゅうさんにち&lt;/code&gt;     | １４日 &lt;code&gt;じゅうよっか&lt;/code&gt;   | １５日 &lt;code&gt;じゅうごにち&lt;/code&gt;   |
| １６日 &lt;code&gt;じゅうろくにち&lt;/code&gt;     | １７日 &lt;code&gt;じゅうしちにち&lt;/code&gt;   | １８日 &lt;code&gt;じゅうはちにち&lt;/code&gt;   | １９日 &lt;code&gt;じゅうくにち&lt;/code&gt;   | ２０日 &lt;code&gt;はつか&lt;/code&gt;         |
| ２１日 &lt;code&gt;にじゅういちにち&lt;/code&gt;   | ２２日 &lt;code&gt;にじゅうににち&lt;/code&gt;   | ２３日 &lt;code&gt;にじゅうさんにち&lt;/code&gt; | ２４日 &lt;code&gt;にじゅうよっか&lt;/code&gt; | ２５日 &lt;code&gt;にじゅうごにち&lt;/code&gt; |
| ２６日 &lt;code&gt;にじゅうろくにち&lt;/code&gt;   | ２７日 &lt;code&gt;にじゅうしちにち&lt;/code&gt; | ２８日 &lt;code&gt;にじゅうはちにち&lt;/code&gt; | ２９日 &lt;code&gt;にじゅうくにち&lt;/code&gt; | ３０日 &lt;code&gt;さんじゅうにち&lt;/code&gt; |
| ３１日 &lt;code&gt;さんじゅういちにち&lt;/code&gt; | 何日 &lt;code&gt;なんにち&lt;/code&gt;           | 一月 &lt;code&gt;いちがつ&lt;/code&gt;           | 二月 &lt;code&gt;にがつ&lt;/code&gt;           | 三月 &lt;code&gt;さんがつ&lt;/code&gt;         |
| 四月 &lt;code&gt;しがつ&lt;/code&gt;               | 五月 &lt;code&gt;ごがつ&lt;/code&gt;             | 六月 &lt;code&gt;ろくがつ&lt;/code&gt;           | 七月 &lt;code&gt;しちがつ&lt;/code&gt;         | 八月 &lt;code&gt;はちがつ&lt;/code&gt;         |
| 九月 &lt;code&gt;くがつ&lt;/code&gt;               | 十月 &lt;code&gt;じゅうがつ&lt;/code&gt;         | 十一月 &lt;code&gt;じゅういちがつ&lt;/code&gt;   | 十二月 &lt;code&gt;じゅうにがつ&lt;/code&gt;   | 何月 &lt;code&gt;なんがつ&lt;/code&gt;         |&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>终端和chorme常用快捷键以及快捷键工具keycue</title><link>https://clloz.com/blog/terminal-chrome-shortcurs</link><guid isPermaLink="true">https://clloz.com/blog/terminal-chrome-shortcurs</guid><pubDate>Wed, 18 Sep 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;命令和快捷键系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/09/18/terminal-chrome-shortcurs/&quot; title=&quot;终端和chorme常用快捷键以及快捷键工具keycue&quot;&gt;终端和chorme常用快捷键以及快捷键工具keycue&lt;/a&gt;()&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/emacs/2019/04/14/emacs-keybinding/&quot; title=&quot;emacs常用快捷键&quot;&gt;emacs常用快捷键&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/05/15/git-command/&quot; title=&quot;常用Git命令&quot;&gt;常用Git命令&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/07/mac-pathnvm/&quot; title=&quot;Mac的环境变量和nvm的使用&quot;&gt;Mac的环境变量和nvm的使用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/09/08/homebrew-tsinghua-mirror/&quot; title=&quot;Homebrew更换清华镜像以及常用命令&quot;&gt;Homebrew更换清华镜像以及常用命令&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;快捷键能够有效提高我们使用工具的效率，不管是使用播放器还是用编辑器写程序，能够记住快捷键使用起来都会更流畅。不过大部分的工具快捷键都非常多，我们无法记住所有快捷键，所以尽量记住常用的就可以了。&lt;/p&gt;
&lt;h2&gt;终端常用快捷键&lt;/h2&gt;
&lt;p&gt;| 按键/命令                          | 描述                                                                          |
| ---------------------------------- | ----------------------------------------------------------------------------- |
| 按住 &lt;code&gt;option&lt;/code&gt; 同时点击要插入的位置 | 重新定位插入点                                                                |
| &lt;code&gt;option + D&lt;/code&gt;                       | 向后删除一个字词                                                              |
| &lt;code&gt;Ctrl + W&lt;/code&gt;                         | 向后删除到字词的开头                                                          |
| &lt;code&gt;Control-T&lt;/code&gt;                        | 调换两个字符的顺序                                                            |
| &lt;code&gt;Ctrl + A&lt;/code&gt;                         | 移动光标至行首                                                                |
| &lt;code&gt;Ctrl + E&lt;/code&gt;                         | 移动光标至行尾                                                                |
| &lt;code&gt;Command + K&lt;/code&gt;                      | 清屏                                                                          |
| &lt;code&gt;Ctrl + U&lt;/code&gt;                         | 删除光标前的所有文字。如果光标位于行尾则删除整行。                            |
| &lt;code&gt;Ctrl + K&lt;/code&gt;                         | 删除到行尾                                                                    |
| &lt;code&gt;Ctrl + R&lt;/code&gt;                         | 检索使用过的命令                                                              |
| &lt;code&gt;Ctrl + C&lt;/code&gt;                         | 终止当前执行                                                                  |
| &lt;code&gt;Ctrl + D&lt;/code&gt;                         | 退出当前shell                                                                 |
| &lt;code&gt;Ctrl + Z&lt;/code&gt;                         | 将执行中的任何东西放入后台进程。fg可以将其恢复。                              |
| &lt;code&gt;Ctrl + W&lt;/code&gt;                         | 删除光标之前的单词                                                            |
| &lt;code&gt;Ctrl + K&lt;/code&gt;                         | 删除光标后的所有文字                                                          |
| &lt;code&gt;Ctrl + T&lt;/code&gt;                         | 将光标前的两个文字进行互换                                                    |
| &lt;code&gt;Option + →&lt;/code&gt;                       | 光标向前移动一个单词                                                          |
| &lt;code&gt;Option + ←&lt;/code&gt;                       | 光标向后移动一个单词                                                          |
| &lt;code&gt;Esc + T&lt;/code&gt;                          | 将光标前的两个单词进行互换                                                    |
| &lt;code&gt;Tab&lt;/code&gt;                              | 自动补全文件或文件夹的名称                                                    |
| &lt;code&gt;History + a number&lt;/code&gt;               | 显示历史命令                                                                  |
| &lt;code&gt;!!&lt;/code&gt;                               | 这将执行输入的最后一个命令。如果遇到权限问题，请尝试在 &lt;code&gt;!!&lt;/code&gt; 之前输入 &lt;code&gt;sudo&lt;/code&gt;。 |&lt;/p&gt;
&lt;p&gt;更多快捷键请查看 &lt;a href=&quot;https://support.apple.com/zh-cn/guide/terminal/trmlshtcts/mac&quot; title=&quot;apple官网&quot;&gt;apple官网&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 常用快捷键&lt;a href=&quot;https://support.apple.com/zh-cn/HT201236&quot; title=&quot;链接&quot;&gt;链接&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;chorme 常用快捷键&lt;/h2&gt;
&lt;p&gt;| 功能                                     | 快捷键                                       |
| ---------------------------------------- | -------------------------------------------- |
| 打开新窗口                               | &lt;code&gt;⌘ + n&lt;/code&gt;                                      |
| 打开新的无痕窗口                         | &lt;code&gt;⌘ + Shift + n&lt;/code&gt;                              |
| 打开新的标签                             | &lt;code&gt;⌘ + t&lt;/code&gt;                                      |
| 按标签关闭顺序重新打开之前的标签         | &lt;code&gt;⌘ + Shift + t&lt;/code&gt;                              |
| 跳转到下一个标签                         | &lt;code&gt;⌘ + Option + →&lt;/code&gt;                             |
| 跳转到上一个标签                         | &lt;code&gt;⌘ + Option + ←&lt;/code&gt;                             |
| 跳转到特定标签页                         | &lt;code&gt;⌘ + 1 到 ⌘ + 8&lt;/code&gt;                             |
| 跳转到最后一个标签页                     | &lt;code&gt;⌘ + 9&lt;/code&gt;                                      |
| 前进                                     | &lt;code&gt;⌘ + [&lt;/code&gt;                                      |
| 后退                                     | &lt;code&gt;⌘ + ]&lt;/code&gt;                                      |
| 关闭当前窗口                             | &lt;code&gt;⌘ + Shift + w&lt;/code&gt;                              |
| 显示或隐藏书签栏                         | &lt;code&gt;⌘ + Shift + b&lt;/code&gt;                              |
| 打开书签管理器                           | &lt;code&gt;⌘ + Option + b&lt;/code&gt;                             |
| 打开设置页                               | &lt;code&gt;⌘ + ,&lt;/code&gt;                                      |
| 打开历史记录                             | &lt;code&gt;⌘ + y&lt;/code&gt;                                      |
| 查找                                     | &lt;code&gt;⌘ + f&lt;/code&gt;                                      |
| 匹配下一条查找结果                       | &lt;code&gt;⌘ + g&lt;/code&gt;                                      |
| 匹配上一条查找结果                       | &lt;code&gt;⌘ + Shift + g&lt;/code&gt;                              |
| 将选中的内容作为查找文本                 | &lt;code&gt;⌘ + e&lt;/code&gt;                                      |
| 开发者工具                               | &lt;code&gt;⌘ + Option + i&lt;/code&gt; 或 &lt;code&gt;F12&lt;/code&gt;                    |
| 清除浏览记录                             | &lt;code&gt;⌘ + Shift + Delete&lt;/code&gt;                         |
| 切换地址栏搜索引擎                       | &lt;code&gt;输入搜索引擎keyword然后按tab，在设置中设置&lt;/code&gt; |
| 跳转到地址栏                             | &lt;code&gt;⌘ + l&lt;/code&gt;                                      |
| 使用 &lt;code&gt;chrome&lt;/code&gt; 打开文件                   | &lt;code&gt;⌘ + o&lt;/code&gt;                                      |
| 显示当前网页的 &lt;code&gt;HTML&lt;/code&gt; 源代码（不可修改） | &lt;code&gt;⌘ + Option + u&lt;/code&gt;                             |
| 打开 &lt;code&gt;JavaScript&lt;/code&gt; 控制台                 | &lt;code&gt;⌘ + Option + j&lt;/code&gt;                             |
| 开启或关闭全屏模式                       | &lt;code&gt;⌘ + Ctrl + f&lt;/code&gt;                               |
| 放大                                     | &lt;code&gt;⌘ 和 +&lt;/code&gt;                                     |
| 缩小                                     | &lt;code&gt;⌘ 和 -&lt;/code&gt;                                     |
| 恢复默认大小                             | &lt;code&gt;⌘ + 0&lt;/code&gt;                                      |
| 向上/向下滚动                            | &lt;code&gt;space&lt;/code&gt;/&lt;code&gt;Shift + space&lt;/code&gt;                      |
| 跳转到地址栏搜索                         | &lt;code&gt;⌘ + Option + f&lt;/code&gt;                             |
| 页面全屏，关闭书签栏、地址栏和标签栏     | &lt;code&gt;⌘ + Shift + f&lt;/code&gt;                              |&lt;/p&gt;
&lt;p&gt;查看全部快捷键点击 &lt;a href=&quot;https://support.google.com/chrome/answer/157179?hl=zh-Hans&quot; title=&quot;chrome快捷键&quot;&gt;chrome快捷键&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;keycue&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;keycue&lt;/code&gt; 是一个软件，长按 &lt;code&gt;command&lt;/code&gt; 可以显示当前所在软件的常用快捷键，比较方便，点击&lt;a href=&quot;https://www.ergonis.com/products/keycue/&quot; title=&quot;下载链接&quot;&gt;下载链接&lt;/a&gt;下载。激活码：&lt;code&gt;KC-ERG-082019-S-1-473199-1019709-1&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_15015129/article/details/78109050&quot; title=&quot;mac终端快捷键&quot;&gt;mac终端快捷键&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>如何给美服Apple ID充值</title><link>https://clloz.com/blog/appleid-us-gift-card</link><guid isPermaLink="true">https://clloz.com/blog/appleid-us-gift-card</guid><pubDate>Mon, 09 Sep 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;由于国内的 &lt;code&gt;app store&lt;/code&gt; 有很多应用是没有的，所以现在大家一般都有一个美服的 &lt;code&gt;apple id&lt;/code&gt;，具体的注册方法大家可以自行搜索，不是特别困难。不过有些软件是收费的，比如 &lt;code&gt;shadowrocket&lt;/code&gt; 从国内的 &lt;code&gt;app store&lt;/code&gt; 下架以后，可以在美服的 &lt;code&gt;app store&lt;/code&gt; 上搜索到，不过是收费的，价格为 &lt;code&gt;$2.99&lt;/code&gt;。于是想着换成这个新的，至少软件是在更新的，而且也不贵。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;下架后的 &lt;code&gt;shadowrocket&lt;/code&gt; 即使在已够项目中也无法找到，所以如果你不小心删除了，那么就无法再下载了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;可选的付款方式&lt;/h2&gt;
&lt;p&gt;美服的 &lt;code&gt;apple id&lt;/code&gt; 有个问题就是只能绑定美国的信用卡，以及美服的 &lt;code&gt;paypal&lt;/code&gt;，所以绑定付款方式这种方法基本是堵死了，剩下的只能通过 &lt;code&gt;gift card&lt;/code&gt; 了。很多人都会从淘宝去购买 &lt;code&gt;gift card&lt;/code&gt;，不过我在多家淘宝店观察了一下，似乎都有黑卡现象，运气不好的话号直接就没了。当然有美国的朋友可以让他们帮你充值，不过大部分人只能靠自己了。&lt;/p&gt;
&lt;h2&gt;自己充值 gift card&lt;/h2&gt;
&lt;p&gt;进入 &lt;a href=&quot;https://www.apple.com/shop/gift-cards&quot;&gt;apple-gift-cards&lt;/a&gt; 页面，滚动到下方，点击下图中的 &lt;code&gt;Email a gift card&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/giftcard1.CqwWvn1__Z1rwE7a.webp&quot; alt=&quot;gift1&quot; title=&quot;gift1&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后进入下图所示的页面，上面的 &lt;code&gt;design&lt;/code&gt; 随便选择一个即可，然后填入要充值的金额，最小金额为 &lt;code&gt;$10&lt;/code&gt;，在最下面填入发送和接收的邮箱，红框标注的位置是接收的邮箱，注意不要写错。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/giftcard2.djDcXR-o_Cz2LB.webp&quot; alt=&quot;gift2&quot; title=&quot;gift2&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来进入检查的页面，看看自己的邮箱信息是否有填错，没错的话就 &lt;code&gt;check out&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/giftcard3.DcoJDGpu_ZxgPCD.webp&quot; alt=&quot;gift3&quot; title=&quot;gift3&quot;&gt;&lt;/p&gt;
&lt;p&gt;选择 &lt;code&gt;check out&lt;/code&gt; 以后会让你登录，这里不要登录，直接选择 &lt;code&gt;continue as a guest&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/giftcard4.DnjYg_YI_1uTgi9.webp&quot; alt=&quot;gift4&quot; title=&quot;gift4&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后就进入付款页面了，选择信用卡，用国内的信用卡即可，我使用的是工行的 &lt;code&gt;visa&lt;/code&gt;。下面会有一个账单地址，随便到往上找一个美国地址就可以了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/giftcard6.CF5W09cT_1rtwaW.webp&quot; alt=&quot;gift6&quot; title=&quot;gift6&quot;&gt;&lt;/p&gt;
&lt;p&gt;到这一步完成以后，我们的付款就结束了，你的邮箱也会收到 &lt;code&gt;apple&lt;/code&gt; 发来的邮件，然后会有一个账单好，你可以查看你的充值是否到账，我印象中大概扣款后 &lt;code&gt;10 ~ 15&lt;/code&gt; 分钟充值就到账了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;一开始我选择了登录充值，不过需要美国的电话号码，现在搞虚拟电话都很麻烦，本来想搞一个 &lt;code&gt;Google Voice&lt;/code&gt; 的号码，不过似乎现在审查很严格，注册 &lt;code&gt;Google Voice&lt;/code&gt; 的号码必须要有一个美国的真实电话号码才行，虚拟的不可以，我还花了 &lt;code&gt;40&lt;/code&gt; 块买了个虚拟号码，结果注册失败，有点坑。总的来说，通过 &lt;code&gt;gift card&lt;/code&gt; 充值的方法还是挺简单的，到账也很快，也不存在封号的风险，建议大家尝试。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;黑卡指的是不是通过合法渠道取得的 &lt;code&gt;gift card&lt;/code&gt;，&lt;code&gt;apple&lt;/code&gt; 如果查到你充值的是黑卡，那么你的账号就会被永封，淘宝卖的有好的也有坏的，完全看你的运气，建议大家还是不要购买。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="/_astro/gift-card.B78n8dq1.png"/><enclosure url="/_astro/gift-card.B78n8dq1.png"/></item><item><title>Homebrew更换源以及常用命令</title><link>https://clloz.com/blog/homebrew-tsinghua-mirror</link><guid isPermaLink="true">https://clloz.com/blog/homebrew-tsinghua-mirror</guid><pubDate>Sat, 07 Sep 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;命令和快捷键系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/09/18/terminal-chrome-shortcurs/&quot; title=&quot;终端和chorme常用快捷键以及快捷键工具keycue&quot;&gt;终端和chorme常用快捷键以及快捷键工具keycue&lt;/a&gt;()&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/emacs/2019/04/14/emacs-keybinding/&quot; title=&quot;emacs常用快捷键&quot;&gt;emacs常用快捷键&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/05/15/git-command/&quot; title=&quot;常用Git命令&quot;&gt;常用Git命令&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/07/mac-pathnvm/&quot; title=&quot;Mac的环境变量和nvm的使用&quot;&gt;Mac的环境变量和nvm的使用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/09/08/homebrew-tsinghua-mirror/&quot; title=&quot;Homebrew更换清华镜像以及常用命令&quot;&gt;Homebrew更换清华镜像以及常用命令&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Homebrew&lt;/code&gt; 有时候更新会卡很久，换成国内的源会改善，更换和恢复的方法如下。&lt;/p&gt;
&lt;h2&gt;中科大源&lt;/h2&gt;
&lt;p&gt;官方文档：&lt;a href=&quot;https://mirrors.ustc.edu.cn/help/brew.git.html&quot; title=&quot;Homebrew 源使用帮助&quot;&gt;Homebrew 源使用帮助&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 替换 Homebrew
cd &quot;$(brew --repo)&quot;
git remote set-url origin https://mirrors.ustc.edu.cn/brew.git

# 替换 Homebrew Core
cd &quot;$(brew --repo)/Library/Taps/homebrew/homebrew-core&quot;
git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git

# 替换 Homebrew Cask
cd &quot;$(brew --repo)&quot;/Library/Taps/homebrew/homebrew-cask
git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-cask.git

# 替换 Homebrew-bottles
# 对于 bash 用户：
echo &apos;export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles&apos; &gt;&gt; ~/.bash_profile
source ~/.bash_profile
# 对于 zsh 用户：
echo &apos;export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles&apos; &gt;&gt; ~/.zshrc
source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;重设官方地址&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd &quot;$(brew --repo)&quot;
git remote set-url origin https://github.com/Homebrew/brew.git

cd &quot;$(brew --repo)/Library/Taps/homebrew/homebrew-core&quot;
git remote set-url origin https://github.com/Homebrew/homebrew-core

cd &quot;$(brew --repo)&quot;/Library/Taps/homebrew/homebrew-cask
git remote set-url origin https://github.com/Homebrew/homebrew-cask

#注释掉bash或zsh配置文件里的有关Homebrew Bottles即可恢复官方源。 重启bash或让bash重读配置文件。

#如果出现问题可以使用以下命令
brew doctor
brew update-reset

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用命令&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#安装依赖工具
xcode-select --install
/usr/bin/ruby -e &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)&quot;

#查看帮助
brew help

#查看版本
brew -v

#更新homebrew
brew update

#显示更新信息
brew update -v

#安装软件包
brew install [包名]

#查询可更新的包
brew outdated

#更新所有
brew upgrade

#更新指定包
brew upgrade [包名]

#清理所有包的旧版本
brew cleanup

#清理指定包的旧版本
brew cleanup [包名]

#查看可清理的旧版本包，不执行实际操作
brew cleanup -n

#锁定某个包
brew pin $FORMULA
#取消锁定
brew unpin $FORMULA

#卸载软件包
brew uninstall [包名]

#查看包信息
brew info [包名]

#查看已经安装软件列表
brew list

#查询可用包
brew search [包名]

#卸载homebrew
cd `brew --prefix`
rm -rf Cellar
brew prune
rm `git ls-files`
rm -r Library/Homebrew Library/Aliases Library/Formula Library/Contributions
rm -rf .git
rm -rf ~/Library/Caches/Homebrew
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装位置&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;brew&lt;/code&gt; 安装软件后，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置文件在 &lt;code&gt;/usr/local/etc&lt;/code&gt; 中&lt;/li&gt;
&lt;li&gt;软件安装在 &lt;code&gt;/usr/local/Cellar&lt;/code&gt; 中&lt;/li&gt;
&lt;li&gt;二进制可执行程序的软连接在 &lt;code&gt;/usr/local/bin&lt;/code&gt; 中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前比较新的 &lt;code&gt;MacOS 12&lt;/code&gt; 中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置文件在 &lt;code&gt;/opt/homebrew/etc&lt;/code&gt; 中&lt;/li&gt;
&lt;li&gt;软件安装路径 &lt;code&gt;/opt/homebrew/Cellar&lt;/code&gt; 中&lt;/li&gt;
&lt;li&gt;二进制可执行程序的软连接在 &lt;code&gt;/opt/homebrew/bin&lt;/code&gt; 中&lt;/li&gt;
&lt;li&gt;日志文件在 &lt;code&gt;/opt/homebrew/var/log&lt;/code&gt; 中&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;keg-only 的意思&lt;/h2&gt;
&lt;p&gt;我们在用 &lt;code&gt;homebrew&lt;/code&gt; 安装一些软件特别是系统中本来就有的软件时，比如 &lt;code&gt;llvm&lt;/code&gt;，&lt;code&gt;flex&lt;/code&gt;，&lt;code&gt;bison&lt;/code&gt; 等，都会看到安装日志中有 &lt;code&gt;keg-only&lt;/code&gt; 的说明，然后有配置环境变量的命令。这个 &lt;code&gt;key-only&lt;/code&gt; 的意思就是 &lt;code&gt;homebrew&lt;/code&gt; 不会帮我们做符号链接，避免和系统中原有的软件冲突，如果我们需要使用 &lt;code&gt;homebrew&lt;/code&gt; 安装的这个就需要自己手动配置环境变量。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/5a561685f265da3e2b164fe7&quot;&gt;MAC上Homebrew常用命令&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/196667957&quot;&gt;有趣的Homebrew 命名及 keg-only 的意思&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/homebrew.CevpFSDB.png"/><enclosure url="/_astro/homebrew.CevpFSDB.png"/></item><item><title>iTerm2+zsh实现好用美观的Mac终端</title><link>https://clloz.com/blog/iterm2-zsh-terminal</link><guid isPermaLink="true">https://clloz.com/blog/iterm2-zsh-terminal</guid><pubDate>Sat, 07 Sep 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我在 &lt;code&gt;Mac&lt;/code&gt; 上一直都是使用的默认的终端 &lt;code&gt;terminal&lt;/code&gt;，&lt;code&gt;shell&lt;/code&gt; 也是使用的默认的 &lt;code&gt;bash&lt;/code&gt;，一直也看到别人对于 &lt;code&gt;iterm2+zsh&lt;/code&gt; 的推崇，不过觉得麻烦一直觉得没折腾。今天看到了一篇写的比较明了的文章，发现其实安装起来也不是很麻烦，于是就折腾了一下，确实很不错，不过对于 &lt;code&gt;item2&lt;/code&gt; 和 &lt;code&gt;zsh&lt;/code&gt; 我都还不是太熟悉，具体的使用要慢慢体会了。&lt;/p&gt;
&lt;h2&gt;安装 iTerm2&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#第一次使用brew cask 命令
brew tap caskroom/cask

#cask安装iTerm2
brew cask install iterm2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装成功后在 &lt;code&gt;Launchpad&lt;/code&gt; 中可以看到有一个新图标出现，打开 &lt;code&gt;iTerm2&lt;/code&gt;。如果你想用快捷键快速启动程序而不是点击图标，参考我的&lt;a href=&quot;https://www.clloz.com/programming/assorted/2018/10/23/terminal-shortcut/&quot; title=&quot;这篇文章&quot;&gt;这篇文章&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;快捷键修改&lt;/h2&gt;
&lt;p&gt;在自带的 &lt;code&gt;terminal&lt;/code&gt; 中 &lt;code&gt;⌥ + 左右方向键&lt;/code&gt; 可以用来定位光标到前一个或后一个单词，不过在 &lt;code&gt;iterm2&lt;/code&gt; 中这两个快捷键被用作其他功能，我们可以手动改回来。按 &lt;code&gt;⌘ + O&lt;/code&gt; 打开 &lt;code&gt;profile&lt;/code&gt; 然后选择 &lt;code&gt;edit profile&lt;/code&gt;，在 &lt;code&gt;profile&lt;/code&gt; 中选择 &lt;code&gt;keys&lt;/code&gt; 选项卡，找到 &lt;code&gt;⌥→&lt;/code&gt; 和 &lt;code&gt;⌘←&lt;/code&gt; 选项，双击修改 &lt;code&gt;Action&lt;/code&gt; 为 &lt;code&gt;Send Escape Sequence&lt;/code&gt;，&lt;code&gt;ESC+&lt;/code&gt; 改为 &lt;code&gt;b&lt;/code&gt;（左箭头）和 &lt;code&gt;f&lt;/code&gt; （右箭头）。&lt;/p&gt;
&lt;h2&gt;选择配色方案&lt;/h2&gt;
&lt;p&gt;我们可以根据自己的喜好来选择 &lt;code&gt;iTerm2&lt;/code&gt; 的配色方案，先检查下终端颜色配置为 &lt;code&gt;xterm-256color&lt;/code&gt;，位置在 &lt;code&gt;iTerm2 -&gt; Preferences -&gt; Profiles -&gt; Terminal&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/xterm256.BpF0YxMm_Zqa2UK.webp&quot; alt=&quot;xterm256&quot; title=&quot;xterm256&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后我们可以在 &lt;code&gt;iTerm2 -&gt; Preferences -&gt; Profiles -&gt; Colors&lt;/code&gt; 中选择我们的配色方案，不过自带的配色方案比较少，我们可以自己导入配色方案。&lt;/p&gt;
&lt;p&gt;有人就开源了一款叫 &lt;code&gt;iTerm2-Color-Schemes&lt;/code&gt; 的配色合集，里面有各种经典、常用的配色方案，来使用 &lt;code&gt;Git&lt;/code&gt;下载到本地。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#克隆到自己想要的位置
git clone https://github.com/mbadolato/iTerm2-Color-Schemes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://cdn1.clloz.com/blog/writing/iterm-color-import.png&quot; title=&quot;iterm-color-import&quot;&gt;&lt;img src=&quot;https://clloz.com/_astro/iterm-color-import.BRbR-vM5_1skdGc.webp&quot; alt=&quot;iterm-color-import&quot; title=&quot;iterm-color-import&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;然后点击图中红框位置的下拉框选择其中的 &lt;code&gt;import&lt;/code&gt; 然后选择刚刚克隆的 &lt;code&gt;iTerm2-Color-Schemes&lt;/code&gt; 中的 &lt;code&gt;schemes&lt;/code&gt; 文件夹中的全部文件导入，我们就有非常多的配色方案可用了， 大家可以选择自己喜欢的，我使用的是 &lt;code&gt;Dracula&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;安装字体&lt;/h2&gt;
&lt;p&gt;想要让 &lt;code&gt;iTerm2&lt;/code&gt; 显示图标我们就得用一些特别的字体，图标字体并不是 &lt;code&gt;ASCII&lt;/code&gt; 码字体，在 &lt;code&gt;iTerm2&lt;/code&gt; 中可以进行配置，所以先要安装这个字体。这款字体叫 &lt;code&gt;nerd-fonts&lt;/code&gt;，它支持下面这么多种图标。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cdn1.clloz.com/blog/writing/nerd-font.png&quot; title=&quot;nerd-font&quot;&gt;&lt;img src=&quot;https://clloz.com/_astro/nerd-font.DtOj3v5p_ZXHPSt.webp&quot; alt=&quot;nerd-font&quot; title=&quot;nerd-font&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew tap caskroom/fonts
brew cask install font-hack-nerd-font
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;安装的时候会去 Github 下载字体，现在 &lt;code&gt;github&lt;/code&gt; 安装似乎非常慢，最好还是用梯子或者代理，否则可能安装失败。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;安装成功后需要在 &lt;code&gt;iTerm2&lt;/code&gt; 中配置一下，在 &lt;code&gt;iTerm2 -&gt; Preferences -&gt; Profiles -&gt; Text -&gt; Font -&gt; Change Font&lt;/code&gt; 栏位中，&lt;code&gt;Text&lt;/code&gt; 下面勾选 &lt;code&gt;Use a different font for non-ASCII text&lt;/code&gt;，然后再选择字体以及设置字体大小，需要注意的是 &lt;code&gt;Font&lt;/code&gt; 和 &lt;code&gt;Non-ASCII Font&lt;/code&gt; 所选择的字体和字体大小必须一致，否则会出现显示异常。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cdn1.clloz.com/blog/writing/iterm-font.png&quot; title=&quot;iterm-font&quot;&gt;&lt;img src=&quot;https://clloz.com/_astro/iterm-font.BQOCxpRq_ZULEay.webp&quot; alt=&quot;iterm-font&quot; title=&quot;iterm-font&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;安装 zsh&lt;/h2&gt;
&lt;p&gt;我印象中我的 &lt;code&gt;Mac&lt;/code&gt; 是没有手动安装 &lt;code&gt;zsh&lt;/code&gt; 的，系统是 &lt;code&gt;mojave 10.14.5&lt;/code&gt;，应该是现在的系统自带的，不过默认 &lt;code&gt;shell&lt;/code&gt; 还是 &lt;code&gt;bash&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#查看系统支持的shell
cat  /etc/shells

#如果没有zsh，则用brew安装
brew install zsh

#查看当前使用shell
echo $SHELL

#设置zsh为默认shell
chsh -s /bin/zsh

#设置bash为默认shell
chsh -s /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;更新 zsh 到最新版本&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 现在自带 &lt;code&gt;zsh&lt;/code&gt;，据说下个 &lt;code&gt;MacOS&lt;/code&gt; 版本 &lt;code&gt;Catalina&lt;/code&gt; 会将 &lt;code&gt;zsh&lt;/code&gt; 作为默认 &lt;code&gt;shell&lt;/code&gt;。&lt;code&gt;Mac&lt;/code&gt; 自带的 &lt;code&gt;zsh&lt;/code&gt; 并不是最新版本的，想要安装最新版本的可以使用 &lt;code&gt;Homebrew&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# check the zsh info
brew info zsh

# install zsh
brew install zsh

# add shell path
sudo vim /etc/shells

# add the following line into the very end of the file(/etc/shells)
/usr/local/bin/zsh

# change default shell
chsh -s /usr/local/bin/zsh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CentOS7 更新 zsh&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CentOS7&lt;/code&gt; 的通过 &lt;code&gt;yum&lt;/code&gt; 安装的 &lt;code&gt;zsh&lt;/code&gt; 版本为 &lt;code&gt;5.0.2&lt;/code&gt;，但是很多主题需要更高的版本。比如我使用的&lt;code&gt;Powerlevel9k&lt;/code&gt; 主题，在使用 &lt;code&gt;git init&lt;/code&gt; 的时候就经常会报错，每次都只能把主题换了再操作，很麻烦。这里给大家说一下如何通过源码安装最新的 &lt;code&gt;zsh&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;最新版 &lt;code&gt;zsh&lt;/code&gt; 下载地址 &lt;a href=&quot;http://zsh.sourceforge.net/Arc/source.html&quot; title=&quot;zsh-5.8.tar.xz&quot;&gt;zsh-5.8.tar.xz&lt;/a&gt;，下载速度可能比较慢，不过还好文件不大，耐心等待。&lt;/p&gt;
&lt;p&gt;下载好 &lt;code&gt;zsh-5.8.tar.xz&lt;/code&gt; 后用 &lt;code&gt;scp&lt;/code&gt; 上传到服务器，然后执行以下操作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yum -y install gcc perl-ExtUtils-MakeMaker
yum -y install ncurses-devel
# 编译安装
tar xvf zsh-5.8.tar.xz
cd zsh-5.8
./configure
make &amp;#x26;&amp;#x26; make install
# 将zsh加入/etc/shells
vim /etc/shells # 添加：/usr/local/bin/zsh
#设置zsh为默认shell
chsh -s /usr/local/bin/zsh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我没有指定安装路径，默认的安装路径就是 &lt;code&gt;/usr/local/bin/zsh&lt;/code&gt;，我们将这个路径添加到 &lt;code&gt;/etc/shells&lt;/code&gt; 中然后设置为默认的 &lt;code&gt;shell&lt;/code&gt; 即可。重启终端我们就可以看到版本已经变为最新的了。&lt;/p&gt;
&lt;h2&gt;安装 oh-my-zsh&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt; sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后 &lt;code&gt;iTerm2&lt;/code&gt; 已经产生了明显的变化了，同时我们的用户文件夹 &lt;code&gt;~&lt;/code&gt; 中生成了一个 &lt;code&gt;.zshrc&lt;/code&gt; 的 &lt;code&gt;oh-my-zsh&lt;/code&gt; 的配置文件。到这一步基本已经大功告成了，下面我们要做的就是对这个配置文件进行改动来达到我们的目标。&lt;/p&gt;
&lt;h2&gt;关联 .bash_profile&lt;/h2&gt;
&lt;p&gt;需要注意的是当前我们使用的 &lt;code&gt;shell&lt;/code&gt; 换成了 &lt;code&gt;zsh&lt;/code&gt;，我们原来在 &lt;code&gt;.bash_profile&lt;/code&gt; 中的配置不会被读取，所以我们要在 &lt;code&gt;.zshrc&lt;/code&gt; 中加上一句 &lt;code&gt;source ~/.bash_profile&lt;/code&gt;，这样我们原来的配置也会生效。&lt;/p&gt;
&lt;h2&gt;配置主题&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;oh-my-zsh&lt;/code&gt; 默认的主题是 &lt;code&gt;robbyrussell&lt;/code&gt;，并不是很好看，我们可以使用 &lt;a href=&quot;https://github.com/romkatv/powerlevel10k&quot;&gt;powerlevel10k&lt;/a&gt; 主题，让我们的 &lt;code&gt;powerline&lt;/code&gt; 更酷炫。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#安装主题
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
# 或者 gitee 这个版本安装
git clone --depth=1 https://gitee.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k

#修改.zshrc中的ZSH_THEME参数
vim ~/.zshrc
ZSH_THEME=&quot;powerlevel10k/powerlevel10k&quot;

#用source命令让配置立即生效
source ~/.zshrc
# 后面会出来 p10k 的配置向导，跟着配置即可。如果后面想要修改配置，一种方法是直接修改 ~/.p10k.zsh 文件，另一种方法是执行 p10k configure 命令
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;p10k&lt;/code&gt; 不需要像 &lt;code&gt;p9k&lt;/code&gt; 那样自己进行复杂的定制，设置向导中已经囊括了比较常用的配置，如果你还是要自己定制可以参考 &lt;a href=&quot;https://github.com/romkatv/powerlevel10k&quot; title=&quot;p10k - README&quot;&gt;p10k - README&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;插件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;oh-my-zsh&lt;/code&gt; 有很多非常好用的插件，下面是一些推荐的插件已经安装方法，安装好的插件只要在 &lt;code&gt;~/.zshrc&lt;/code&gt; 中的 &lt;code&gt;plugins=(git autojump cp extract sudo z zsh-syntax-highlighting zsh-autosuggestions)&lt;/code&gt; 依次写入用空格分开即可。&lt;/p&gt;
&lt;h2&gt;autojump&lt;/h2&gt;
&lt;p&gt;能够记忆我们之前去过的目录，不需要多次 &lt;code&gt;cd&lt;/code&gt; ，直接 &lt;code&gt;j 目录名&lt;/code&gt; 就可以直接进入。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#安装
brew install autojump

#在 ~/.zshrc 中加入如下配置
[[ -s $(brew --prefix)/etc/profile.d/autojump.sh ]] &amp;#x26;&amp;#x26; . $(brew --prefix)/etc/profile.d/autojump.sh
source $ZSH/oh-my-zsh.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;zsh-autosuggestion&lt;/h2&gt;
&lt;p&gt;如图所示，输入命令时可提示自动补全(灰色部分)，然后按键盘 &lt;code&gt;→&lt;/code&gt; 即可补全(&lt;a href=&quot;https://github.com/zsh-users/zsh-autosuggestions&quot; title=&quot;详细介绍&quot;&gt;详细介绍&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#安装
$ git clone git://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;zsh-syntax-highlighting&lt;/h2&gt;
&lt;p&gt;日常用的命令会高亮显示，命令错误显示红色，如下图(&lt;a href=&quot;https://github.com/zsh-users/zsh-syntax-highlighting&quot; title=&quot;详细介绍&quot;&gt;详细介绍&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#安装
$ git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;colorls&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;colorls&lt;/code&gt; 是一个 &lt;code&gt;Ruby&lt;/code&gt; 实现的脚本，它可以配合 &lt;code&gt;powerlevel9k&lt;/code&gt; 显示电脑上的文件图标(应该是通过后缀判断的)，效果如下&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cdn1.clloz.com/blog/writing/colorls.png&quot; title=&quot;colorls&quot;&gt;&lt;img src=&quot;https://clloz.com/_astro/colorls.D_XkIZlS_Z12Cvae.webp&quot; alt=&quot;colorls&quot; title=&quot;colorls&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#安装
sudo gem install colorls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我在 &lt;code&gt;CentOS&lt;/code&gt; 中安装 &lt;code&gt;colorls&lt;/code&gt; 的时候提示 &lt;code&gt;ruby&lt;/code&gt; 版本太低，用 &lt;code&gt;rvm&lt;/code&gt; 安装高版本 &lt;code&gt;ruby&lt;/code&gt;，但是重启后 &lt;code&gt;rvm&lt;/code&gt; 加载有问题。&lt;code&gt;google&lt;/code&gt; 后发现，要在 &lt;code&gt;.zshrc&lt;/code&gt; 中加上 &lt;code&gt;[[ -s &quot;$HOME/.rvm/scripts/rvm&quot; ]] &amp;#x26;&amp;#x26; source &quot;$HOME/.rvm/scripts/rvm&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;一些小技巧&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;连续按两次 &lt;code&gt;tab&lt;/code&gt; 会补全列表，补全项可以使用 &lt;code&gt;ctrl+n/p/f/b&lt;/code&gt; 上下左右切换&lt;/li&gt;
&lt;li&gt;输入目录名即可进入，不用 &lt;code&gt;cd&lt;/code&gt; 了，输入 &lt;code&gt;..&lt;/code&gt; 即可到上级目录，返回上次目录输入 &lt;code&gt;-&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;输入 d 即可看到目录列表&lt;/li&gt;
&lt;li&gt;智能的命令纠错功能（需开启 ENABLE_CORRECTION 配置）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cmd + Shift + H&lt;/code&gt; 可以查看剪切板的历史记录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cmd + Option + B&lt;/code&gt; 可以利用时间轴来查看之前输入的命令&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;vscode 的配置&lt;/h2&gt;
&lt;p&gt;为了能够在 &lt;code&gt;vscode&lt;/code&gt; 中能够正常使用新的终端，我们需要在 &lt;code&gt;vscode&lt;/code&gt; 的 &lt;code&gt;setting.json&lt;/code&gt; 中加入如下配置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&quot;terminal.external.osxExec&quot;: &quot;iTerm.app&quot;,
&quot;terminal.integrated.shell.osx&quot;: &quot;/bin/zsh&quot;,
&quot;terminal.integrated.fontFamily&quot;: &quot;Hack Nerd Font&quot;,
&quot;terminal.integrated.fontSize&quot;: 14,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://cdn1.clloz.com/blog/writing/iterm-vscode.png&quot; title=&quot;iterm-vscode&quot;&gt;&lt;img src=&quot;https://clloz.com/_astro/iterm-vscode.C5wei-7I_Z1iKQNK.webp&quot; alt=&quot;iterm-vscode&quot; title=&quot;iterm-vscode&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Linux 配置&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;zsh&lt;/code&gt; 和 &lt;code&gt;oh-my-zsh&lt;/code&gt; 也是可以在 &lt;code&gt;Linux&lt;/code&gt; 上配置的，我在自己的阿里云服务器上也配置了和 &lt;code&gt;mac&lt;/code&gt; 上相同的效果，包括了上面所用到的插件，现在远程的服务器和本地的 &lt;code&gt;Mac&lt;/code&gt; 操作上没什么差别，都非常好用。关于 &lt;code&gt;nerd&lt;/code&gt; 字体的问题，如果是 &lt;code&gt;ssh&lt;/code&gt; 远程连接自己的服务器，那么在远程的服务器上没有安装也没有关系，本地只要安装了就可以正常显示，比如我是用的 &lt;code&gt;iTerm2&lt;/code&gt; 来连接阿里云的服务器，显示没有什么问题，而如果你用阿里云网站上给的在线远程连接即使安装了字体也无法正常显示。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.biezhi.me/2018/11/build-a-beautiful-mac-terminal-environment.html&quot;&gt;打造 Mac 下高颜值好用的终端环境&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zrahh.com/archives/167.html&quot;&gt;oh-my-zsh插件推荐&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000002658335&quot;&gt;那些我希望在一开始使用 Zsh(oh-my-zsh) 时就知道的&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yugasun.com/post/iterm2-shortcut-key.html&quot;&gt;iTerm2 快捷键集锦&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/iterm2-logo.CIN3O3VA.jpg"/><enclosure url="/_astro/iterm2-logo.CIN3O3VA.jpg"/></item><item><title>JavaScript中的Number</title><link>https://clloz.com/blog/javascript-number</link><guid isPermaLink="true">https://clloz.com/blog/javascript-number</guid><pubDate>Tue, 11 Jun 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;由于 &lt;code&gt;JavaScript&lt;/code&gt; 是个动态弱类型语言，入门也比较简单，所以刚开始大家不是那么关心类型的细节。今天这篇文章来说一说 &lt;code&gt;JavaScript&lt;/code&gt; 中的 &lt;code&gt;Number&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;定点数和浮点数&lt;/h2&gt;
&lt;p&gt;我在 &lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/06/05/true-form-ones-complement-twos-complement/&quot;&gt;原码，反码和补码&lt;/a&gt; 这篇文章中介绍了关于计算机中如何存储和计算整数。不过我们的现实需求中显然不止又整数，小数的计算可能才是更加频繁的。但是我们知道计算机只能进行二进制的存储和计算，显然要加入小数点就和符号位不参与计算一样需要设计非常复杂的电路，这是没意义的。计算机科学家们自然要设计一种用二进制计算小数的方法，最终确定了两种方案，定点数和浮点数。&lt;/p&gt;
&lt;p&gt;定点数其实很好理解，就是小数点后位数固定，比如一个 &lt;code&gt;8&lt;/code&gt; 位存储单元，我们规定精度为 &lt;code&gt;4&lt;/code&gt;，也就是说小数点后 &lt;code&gt;4&lt;/code&gt; 位，&lt;code&gt;11001001&lt;/code&gt; 和 &lt;code&gt;00110101&lt;/code&gt; 分别表示 &lt;code&gt;1100.1001&lt;/code&gt; 和 &lt;code&gt;11.0101&lt;/code&gt; 两个数字。&lt;/p&gt;
&lt;p&gt;浮点数借用了我们日常使用的科学计数法的思维，在字长中截取一段作为偏移量，剩下的部分表示有效数字 &lt;code&gt;1.xxxxxxx&lt;/code&gt;，有效数字的第一位为1，通常省略。结构如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/float-point.BSUQR_A6_Z1kjQyy.webp&quot; alt=&quot;float-point&quot; title=&quot;float-point&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以看出定点数和浮点数各自又自己的适用场景，比如定点数的精度不会变化，但是表示的范围很有限，比如我们想表示很大和很小的数都无法做到。为了解决这一问题才引入了浮点数，目前大部分编程语言和 &lt;code&gt;cpu&lt;/code&gt; 都是按照 &lt;code&gt;IEEE754&lt;/code&gt; 标准来进行浮点数的存储和运算的，在 &lt;code&gt;JavaScript&lt;/code&gt; 中的 &lt;code&gt;Number&lt;/code&gt; 类型都是标准中的双精度浮点数。&lt;/p&gt;
&lt;h2&gt;IEEE754 标准&lt;/h2&gt;
&lt;p&gt;我一直都做前端的工作，对定点数和浮点数的理解也仅限于大学中的计算机组成原理的课程中，虽然这些知识可能在前端工作中用处不大，不过我对计算机的工作原理和设计思路还是很好奇，并且不喜欢云里雾里的感觉，对于有能力搞懂的内容还是尽量搞懂，毕竟这个行业变化很快，这些底层的知识掌握肯定有用处，对于我们的整个知识框架肯定是很有帮助的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IEEE754&lt;/code&gt; 标准是二进制浮点数运算标准，规定了计算机中的二进制浮点数的存储和计算的标准。我们在上面的已经介绍了浮点数的三个组成部分：符号位，指数偏移值和有效数字。浮点数又分为两种，单精度和双精度： 1. 单精度：符号位 &lt;code&gt;1bit&lt;/code&gt;，指数偏移量 &lt;code&gt;8bit&lt;/code&gt;，有效数字 &lt;code&gt;23bit&lt;/code&gt;，共 &lt;code&gt;32bit&lt;/code&gt; 2. 双精度：符号位 &lt;code&gt;1bit&lt;/code&gt;，指数偏移量 &lt;code&gt;11bit&lt;/code&gt;，有效数字 &lt;code&gt;52bit&lt;/code&gt;，共 &lt;code&gt;64bit&lt;/code&gt; 符号位不用讲了，&lt;code&gt;0&lt;/code&gt; 为正，&lt;code&gt;1&lt;/code&gt; 为负。我们主要说一说指数偏移值和有效数字的部分。&lt;/p&gt;
&lt;h2&gt;有效数字&lt;/h2&gt;
&lt;p&gt;有效数字和我们的科学计数法基本相同，小数点右边只保留一位，也就是 &lt;code&gt;1 ≤ 有效数字 &amp;#x3C; 2&lt;/code&gt;。因为这一位都是 &lt;code&gt;1&lt;/code&gt;，所以并不保存，相当于一个默认位。比如保存&lt;code&gt;1.01&lt;/code&gt;的时候，只保存 &lt;code&gt;01&lt;/code&gt;，等到读取的时候，再把第一位的 &lt;code&gt;1&lt;/code&gt; 加上去。这样做的目的，是节省 &lt;code&gt;1&lt;/code&gt; 位有效数字。以 &lt;code&gt;32&lt;/code&gt; 位浮点数为例，留给有效数字只有 &lt;code&gt;23&lt;/code&gt; 位，将第一位的 &lt;code&gt;1&lt;/code&gt; 舍去以后，等于可以保存 &lt;code&gt;24&lt;/code&gt; 位有效数字。&lt;/p&gt;
&lt;h2&gt;指数偏移值&lt;/h2&gt;
&lt;p&gt;指数偏移值理解起来很简单，就比如我们十进制的科学计数法 &lt;code&gt;1.2 * 10 ^ 21&lt;/code&gt;，这个 &lt;code&gt;21&lt;/code&gt; 就是指数偏移值，但是我们在 &lt;a href=&quot;https://zh.wikipedia.org/wiki/IEEE_754#%E9%9D%9E%E8%A7%84%E7%BA%A6%E5%BD%A2%E5%BC%8F%E7%9A%84%E6%B5%AE%E7%82%B9%E6%95%B0&quot;&gt;wikipedia&lt;/a&gt; 对标准中的指数偏移值的介绍中可以发现，这个指数偏移值并不是我们前面介绍的 &lt;code&gt;补码&lt;/code&gt; 方式存储的，而是用了一种所谓的移码加偏移量的形式。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Wikipedia&lt;/code&gt; 对 &lt;code&gt;移码&lt;/code&gt; 是这么介绍的：在计算机科学中，移码（英语：&lt;code&gt;Offset binary&lt;/code&gt; ）是一种将全 &lt;code&gt;0&lt;/code&gt; 码映射为最小负值、全 &lt;code&gt;1&lt;/code&gt; 码映射为最大正值的编码方案。理解起来并不困难，但是我们可能很难理解为什么要这么做。我们想想我们对科学计数法的计算过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;比较两个数的指数大小&lt;/li&gt;
&lt;li&gt;把两个数化成相同指数&lt;/li&gt;
&lt;li&gt;对有效数字进行计算&lt;/li&gt;
&lt;li&gt;将结果格式化为科学计数法的正常格式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;计算机对浮点数的计算其实也遵循这三个步骤：求阶差、对阶，尾数相加，结果规格化。也就是说我们首先要比较指数偏移量的大小。我们以单精度浮点数为例，我们的指数偏移值一共是 &lt;code&gt;8bit&lt;/code&gt;，如果按照我们之前的补码方式存储，那么就是用 &lt;code&gt;10000000&lt;/code&gt; ～ &lt;code&gt;01111111&lt;/code&gt; 来表示 &lt;code&gt;-128&lt;/code&gt; ～ &lt;code&gt;127&lt;/code&gt;。但是我们可以发现，补码在整数的计算中虽然很方便不用考虑符号位，但是在大小比较的时候却并不方便。因为我们首先要比较符号位，然后在比较后面的值，这对我们来说可能很直观，可是在计算机的电路设计上会不方便，这就是使用移码的原因。&lt;/p&gt;
&lt;p&gt;上面我们已经介绍了移码的概念，我们在补码那篇文章中也说了，同余运算我们只要数字成环即可，原点终点可以自定，所以我们可以给定任意偏移量生成一个新的环。移码的偏移量其实没有标准，&lt;code&gt;IEEE 754&lt;/code&gt;标准规定该固定值为$2 ^ {n -1} - 1$，在这里就是 $2 ^ 7 - 1$ &lt;code&gt;127&lt;/code&gt;，于是我们从 &lt;code&gt;10000000&lt;/code&gt; ～ &lt;code&gt;01111111&lt;/code&gt; 来表示 &lt;code&gt;-128&lt;/code&gt; ～ &lt;code&gt;127&lt;/code&gt; 变成了 &lt;code&gt;11111111&lt;/code&gt; ～ &lt;code&gt;11111110&lt;/code&gt; 来表示 &lt;code&gt;-128&lt;/code&gt; ～ &lt;code&gt;127&lt;/code&gt;（其中 &lt;code&gt;11111111&lt;/code&gt; 和 &lt;code&gt;00000000&lt;/code&gt; 用来表示特殊值，具体说明在下一小节），更具体的说是 &lt;code&gt;00000001&lt;/code&gt; ～ &lt;code&gt;01111111&lt;/code&gt; 表示 &lt;code&gt;-126&lt;/code&gt; ～ &lt;code&gt;0&lt;/code&gt;，&lt;code&gt;10000000&lt;/code&gt; ～ &lt;code&gt;11111110&lt;/code&gt; 表示 &lt;code&gt;1&lt;/code&gt; ～ &lt;code&gt;127&lt;/code&gt;，符合上面我们对移码的定义。此时我们所有的数按照从小到大排列，此时要比较大小就非常简单了。我们计算真实指数也很容易，将指数减去偏移量即可。&lt;/p&gt;
&lt;p&gt;上面我们给出的偏移值是 $2 ^ {n -1}$ ，但实际 &lt;code&gt;IEEE754&lt;/code&gt; 标准中给出的偏移值是 $2 ^ {n -1} -1$ ，在单精度浮点数中就是 &lt;code&gt;127&lt;/code&gt;（这样刚好将最小负数 &lt;code&gt;-128&lt;/code&gt; 也就是 &lt;code&gt;10000000&lt;/code&gt;，移动到 &lt;code&gt;11111111&lt;/code&gt;，这样刚好用 &lt;code&gt;11111111&lt;/code&gt; 和 &lt;code&gt;00000000&lt;/code&gt; 做特殊情况使用），并且给出了几种 &lt;code&gt;特殊情况&lt;/code&gt; 的定义($e$ 表示指数部分的位数即指数偏移量，&lt;code&gt;64&lt;/code&gt; 位双精度就是 &lt;code&gt;11&lt;/code&gt;，&lt;code&gt;32&lt;/code&gt; 位单精度就是 &lt;code&gt;8&lt;/code&gt;):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果指数是0并且尾数的小数部分是 &lt;code&gt;0&lt;/code&gt;，这个数 &lt;code&gt;±0&lt;/code&gt;（和符号位相关）&lt;/li&gt;
&lt;li&gt;如果指数 = $2 ^e -1$并且尾数的小数部分是 &lt;code&gt;0&lt;/code&gt;，这个数是 &lt;code&gt;±∞&lt;/code&gt;（同样和符号位相关）&lt;/li&gt;
&lt;li&gt;如果指数 = $2 ^e -1$并且尾数的小数部分非 &lt;code&gt;0&lt;/code&gt;，这个数表示为不是一个数（ &lt;code&gt;NaN&lt;/code&gt; ）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;根据这几条规则，当偏移量为 &lt;code&gt;127&lt;/code&gt; 的时候，&lt;code&gt;11111111&lt;/code&gt; ～ &lt;code&gt;11111110&lt;/code&gt; 表示 &lt;code&gt;-128&lt;/code&gt; ～ &lt;code&gt;127&lt;/code&gt;，由于全为 &lt;code&gt;0&lt;/code&gt;或全为 &lt;code&gt;1&lt;/code&gt; 都是特殊值，所以指数取值范围从 &lt;code&gt;00000001&lt;/code&gt; ～ &lt;code&gt;11111110&lt;/code&gt; 表示 &lt;code&gt;-126&lt;/code&gt; ～ &lt;code&gt;127&lt;/code&gt;。除上面的特殊规则以外还有一种情况：指数全为 &lt;code&gt;0&lt;/code&gt;，并且有效数字不为 &lt;code&gt;0&lt;/code&gt;，此时舍弃有效数字小数点右边的 &lt;code&gt;1&lt;/code&gt; （就是没有保存的默认 &lt;code&gt;1&lt;/code&gt; ），而指数则为 $0 - (2 ^e - 2)$，即 &lt;code&gt;0 - 126&lt;/code&gt; 或者 &lt;code&gt;0 - 1022&lt;/code&gt;（ &lt;strong&gt;&lt;code&gt;IEEE 754&lt;/code&gt;标准规定：非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小&lt;code&gt;1&lt;/code&gt;&lt;/strong&gt;），这主要是用来表示非常接近 &lt;code&gt;0&lt;/code&gt; 的小数，这类数称为非规约数，而其他指数在 &lt;code&gt;0&lt;/code&gt; 到 $2 ^e -1$ (开区间)中的数字则为规约数。$2 ^e -1$ 即全为 &lt;code&gt;1&lt;/code&gt; 的情况。&lt;/p&gt;
&lt;p&gt;将上面的规则总结为下面的表格：&lt;/p&gt;
&lt;p&gt;| 形式       | 指数             | 小数部分              |
| ---------- | ---------------- | --------------------- |
| 零         | &lt;code&gt;0&lt;/code&gt;              | &lt;code&gt;0&lt;/code&gt;                   |
| 非规约形式 | &lt;code&gt;0&lt;/code&gt;              | 大于 &lt;code&gt;0&lt;/code&gt; 小于 &lt;code&gt;1&lt;/code&gt;     |
| 规约形式   | &lt;code&gt;1&lt;/code&gt; 到 $2 ^e -2$ | 大于等于 &lt;code&gt;1&lt;/code&gt; 小于 &lt;code&gt;2&lt;/code&gt; |
| 无穷       | $2 ^e -1$        | 0                     |
| NaN        | $2 ^e -1$        | 非0                   |&lt;/p&gt;
&lt;h2&gt;浮点数表示范围&lt;/h2&gt;
&lt;p&gt;以单精度浮点数为例：&lt;/p&gt;
&lt;p&gt;| 类别               | 正负号 | 实际指数 | 有偏移指数 | 指数域      | 尾数域                         | 数                                                                |
| ------------------ | ------ | -------- | ---------- | ----------- | ------------------------------ | ----------------------------------------------------------------- |
| 零                 | &lt;code&gt;0&lt;/code&gt;    | &lt;code&gt;-127&lt;/code&gt;   | &lt;code&gt;0&lt;/code&gt;        | &lt;code&gt;0000 0000  | 000 0000 0000 0000 0000 0000&lt;/code&gt;  | &lt;code&gt;0.0&lt;/code&gt;                                                             |
| 负零               | &lt;code&gt;1&lt;/code&gt;    | &lt;code&gt;-127&lt;/code&gt;   | &lt;code&gt;0&lt;/code&gt;        | &lt;code&gt;0000 0000&lt;/code&gt; | &lt;code&gt;000 0000 0000 0000 0000 0000&lt;/code&gt; | &lt;code&gt;−0.0&lt;/code&gt;                                                            |
| &lt;code&gt;1&lt;/code&gt;                | &lt;code&gt;0&lt;/code&gt;    | &lt;code&gt;0&lt;/code&gt;      | &lt;code&gt;127&lt;/code&gt;      | &lt;code&gt;0111 1111&lt;/code&gt; | &lt;code&gt;000 0000 0000 0000 0000 0000&lt;/code&gt; | &lt;code&gt;1.0&lt;/code&gt;                                                             |
| &lt;code&gt;-1&lt;/code&gt;               | &lt;code&gt;1&lt;/code&gt;    | &lt;code&gt;0&lt;/code&gt;      | &lt;code&gt;127&lt;/code&gt;      | &lt;code&gt;0111 1111&lt;/code&gt; | &lt;code&gt;000 0000 0000 0000 0000 0000&lt;/code&gt; | &lt;code&gt;−1.0&lt;/code&gt;                                                            |
| 最小的非规约数     | &lt;code&gt;*&lt;/code&gt;    | &lt;code&gt;-126&lt;/code&gt;   | &lt;code&gt;0&lt;/code&gt;        | &lt;code&gt;0000 0000&lt;/code&gt; | &lt;code&gt;000 0000 0000 0000 0000 0001&lt;/code&gt; | $\pm2^{23}\times2^{-126}=\pm2^{-149}\approx\pm1.4\times10^{-45}$  |
| 中间大小的非规约数 | &lt;code&gt;*&lt;/code&gt;    | &lt;code&gt;-126&lt;/code&gt;   | &lt;code&gt;0&lt;/code&gt;        | &lt;code&gt;0000 0000&lt;/code&gt; | &lt;code&gt;100 0000 0000 0000 0000 0000&lt;/code&gt; | $\pm2^{-1}\times2^{-126}=\pm2^{-127}\approx\pm5.88\times10^{-39}$ |
| 最大的非规约数     | &lt;code&gt;*&lt;/code&gt;    | &lt;code&gt;-126&lt;/code&gt;   | &lt;code&gt;0&lt;/code&gt;        | &lt;code&gt;0000 0000&lt;/code&gt; | &lt;code&gt;111 1111 1111 1111 1111 1111&lt;/code&gt; | $\pm(1-2^{-23})\times2^{-126}\approx\pm1.18\times10^{-38}$        |
| 最小的规约数       | &lt;code&gt;*&lt;/code&gt;    | &lt;code&gt;-126&lt;/code&gt;   | &lt;code&gt;1&lt;/code&gt;        | &lt;code&gt;0000 0001&lt;/code&gt; | &lt;code&gt;000 0000 0000 0000 0000 0000&lt;/code&gt; | $\pm2^{-126}\approx\pm1.18\times10^{-38}$                         |
| 最大的规约数       | &lt;code&gt;*&lt;/code&gt;    | &lt;code&gt;127&lt;/code&gt;    | &lt;code&gt;254&lt;/code&gt;      | &lt;code&gt;1111 1110&lt;/code&gt; | &lt;code&gt;111 1111 1111 1111 1111 1111&lt;/code&gt; | $\pm(2-2^{-23})\times2^{127}\approx\pm3.4\times10^{38}$           |
| 正无穷             | &lt;code&gt;0&lt;/code&gt;    | &lt;code&gt;128&lt;/code&gt;    | &lt;code&gt;255&lt;/code&gt;      | &lt;code&gt;1111 1111&lt;/code&gt; | &lt;code&gt;000 0000 0000 0000 0000 0000&lt;/code&gt; | $+\infty$                                                         |
| 负无穷             | &lt;code&gt;1&lt;/code&gt;    | &lt;code&gt;128&lt;/code&gt;    | &lt;code&gt;255&lt;/code&gt;      | &lt;code&gt;1111 1111&lt;/code&gt; | &lt;code&gt;000 0000 0000 0000 0000 0000&lt;/code&gt; | $-\infty$                                                         |
| &lt;code&gt;NaN&lt;/code&gt;              | &lt;code&gt;*&lt;/code&gt;    | &lt;code&gt;128&lt;/code&gt;    | &lt;code&gt;255&lt;/code&gt;      | &lt;code&gt;1111 1111&lt;/code&gt; | &lt;code&gt;non zero&lt;/code&gt;                     | &lt;code&gt;NaN&lt;/code&gt;                                                             |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;*&lt;/code&gt; 符号位可以为 &lt;code&gt;0&lt;/code&gt; 或 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于 &lt;code&gt;JavaScript&lt;/code&gt; 中使用的浮点数一共可以表示 $2^{64}-2^{53}+3$ 共 &lt;code&gt;18437736874454810627&lt;/code&gt; 个值，至于计算很简单，&lt;code&gt;64&lt;/code&gt; 位一共可以表示 $2^{64}$ 个数，但是上面 &lt;code&gt;特殊情况&lt;/code&gt; 第 &lt;code&gt;3&lt;/code&gt; 点中的指数为 $2 ^e -1$并且尾数的小数部分非 &lt;code&gt;0&lt;/code&gt;排除掉了 $2 ^ {53}$ 个值（除指数位外还有 &lt;code&gt;53&lt;/code&gt; 位），但是根据 &lt;code&gt;2&lt;/code&gt;，&lt;code&gt;3&lt;/code&gt; 两种情况，这$2 ^ {53}$个数中又有 &lt;code&gt;正无穷&lt;/code&gt;，&lt;code&gt;负无穷&lt;/code&gt; 和 &lt;code&gt;NaN&lt;/code&gt; 三个数是有意义的，要加上，所以得出上面的结论。&lt;/p&gt;
&lt;h2&gt;精度&lt;/h2&gt;
&lt;p&gt;面试题中经常出现 &lt;code&gt;0.1 + 0.2&lt;/code&gt; 为什么不等于 &lt;code&gt;0.3&lt;/code&gt;，这就和浮点数计算的精度有关系了。&lt;/p&gt;
&lt;p&gt;我们在使用十进制的时候也是又误差的，比如 &lt;code&gt;1/3&lt;/code&gt;，或者 $\sqrt2$的时候我们是无法用有限的小数精确的表示这个值的，只能根据我们的使用场景来确定我们需要的精度。使用二进制也一样，二进制中的 &lt;code&gt;0.1&lt;/code&gt; 表示十进制的 &lt;code&gt;0.5&lt;/code&gt;，&lt;code&gt;0.01&lt;/code&gt; 表示十进制的 &lt;code&gt;0.25&lt;/code&gt;，显然又很多小数我们无法表示，比如 &lt;code&gt;0.1&lt;/code&gt; ～ &lt;code&gt;0.9&lt;/code&gt; 这九个小数（小数点后只有一位的小数），只有 &lt;code&gt;0.5&lt;/code&gt; 在二进制中可以精确表示，其他都是有误差的。&lt;/p&gt;
&lt;p&gt;IEEE二进制浮点数算术标准中定义了以下几种舍入规则:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;朝0方向舍入: 即截尾，直接将需要精确的位数以后的数位舍去。&lt;/li&gt;
&lt;li&gt;舍入到最接近: 即四舍五入，结果可能会变大或变小。&lt;/li&gt;
&lt;li&gt;朝&lt;code&gt;-∞&lt;/code&gt;方向舍入: 总是向数轴的左方向舍入。&lt;/li&gt;
&lt;li&gt;朝&lt;code&gt;+∞&lt;/code&gt;方向舍入: 总是向数轴的右方向舍入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以当我们计算无法精确计算的值的时候产生误差是不可避免的，可能有些同学会有疑问，为什么 &lt;code&gt;0.3&lt;/code&gt; 能正确表示，而 &lt;code&gt;0.1 + 0.2&lt;/code&gt; 却表示有问题呢？这是因为误差会在计算中被扩大，计算次数越多，误差越大。举个十进制的例子：&lt;code&gt;1.6 + 2.8 = 4.4&lt;/code&gt;，但是如果我们的计算机精度只能到整数，那么我们的就算就变为了 &lt;code&gt;2 + 3 = 5&lt;/code&gt; ，虽然这个例子很简单，但其实计算机中的计算虽然字长长很多，但也是一样的道理。&lt;/p&gt;
&lt;h2&gt;0.1 + 0.2 != 0.3 的推导&lt;/h2&gt;
&lt;p&gt;这是一个非常常用的题目考察我们对浮点数的理解，大部分人都知道在 &lt;code&gt;IEEE 754&lt;/code&gt; 标准下的双精度浮点数的计算结果位 &lt;code&gt;0.30000000000000004&lt;/code&gt;，至于为什么下面给出具体的计算过程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个题目使用的非常多，甚至有人专门搞了一个&lt;a href=&quot;https://0.30000000000000004.com/&quot; title=&quot;https://0.30000000000000004.com/&quot;&gt;https://0.30000000000000004.com/&lt;/a&gt;的网站。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;JS中能表示的最大精确整数是多少呢？这个问题其实就是问 &lt;code&gt;IEEE 754&lt;/code&gt; 标准下的双精度浮点数能表示的最大精确整数，结果很简单，&lt;code&gt;64&lt;/code&gt; 个 &lt;code&gt;bit&lt;/code&gt; 中，有 &lt;code&gt;52&lt;/code&gt; 位用来表示有效数字，加上默认的 &lt;code&gt;1&lt;/code&gt; ，一共是 &lt;code&gt;53&lt;/code&gt; 位，所以结果就是 $2 ^ {53} -1$ &lt;code&gt;9007199254740991&lt;/code&gt;。注意这里说的是最大精确整数，不是最大的数，要求精确就是从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;9007199254740991&lt;/code&gt; 中的每一个整数都能准确表示，超过这个范围则会损失精度。&lt;code&gt;64&lt;/code&gt; 位双精度浮点数能表示的最大规约数位 $(2 - 2 ^ {-52}) \times 2 ^ {1023}$，结果约为 &lt;code&gt;1.7976931348623157e+308&lt;/code&gt;，具体的计算方法参考上面的 &lt;code&gt;32&lt;/code&gt; 位单精度浮点数的计算方法，这两个值可以通过 &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt; 和 &lt;code&gt;Number.MAX_VALUE&lt;/code&gt; 来表示。&lt;/p&gt;
&lt;p&gt;要知道 &lt;code&gt;0.1 + 0.2&lt;/code&gt; 的计算结果，我们先要知道计算机是如何保存 &lt;code&gt;0.1&lt;/code&gt; 和 &lt;code&gt;0.2&lt;/code&gt; 这两个数的。&lt;/p&gt;
&lt;p&gt;可以到这个&lt;a href=&quot;http://www.binaryconvert.com/result_double.html?decimal=048046049&quot; title=&quot;Double (IEEE754 Double precision 64-bit) Converter&quot;&gt;Double (IEEE754 Double precision 64-bit) Converter&lt;/a&gt;进行转换，结果如下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;0.1&lt;/code&gt; ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sign &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;exponent &lt;code&gt;01111111011&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;mantissa &lt;code&gt;1001100110011001100110011001100110011001100110011010&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;0.2&lt;/code&gt; ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sign &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;exponent &lt;code&gt;01111111100&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;mantissa &lt;code&gt;1001100110011001100110011001100110011001100110011010&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以看出两个数字的有效数字部分是完全相同的，唯一不同的就是阶码（指数偏移量），&lt;code&gt;0.2&lt;/code&gt; 的阶码比 &lt;code&gt;0.1&lt;/code&gt; 大 &lt;code&gt;1&lt;/code&gt;，我们把这两个数进行十进制的转化可以得出结果（&lt;code&gt;64&lt;/code&gt; 位双精度浮点数的指数偏移量为 $2 ^ {11 - 1} -1 = 1023$），由于计算精度要求较高，可以到这个网站进行求值&lt;a href=&quot;https://www.ttmath.org/online_calculator&quot; title=&quot;Big online calculator&quot;&gt;Big online calculator&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;0.1&lt;/code&gt;：$(2702159776422298 \times 2 ^ {-52} + 1) \times 2 ^ {-4} = 0.1000000000000000055511151231257827021181583404541015625$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;0.2&lt;/code&gt;：$(2702159776422298 \times 2 ^ {-52} + 1) \times 2 ^ {-3} = 0.200000000000000011102230246251565404236316680908203125$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;你可以是试试在 &lt;code&gt;javascript&lt;/code&gt; 中输出 &lt;code&gt;0.1 === 0.1000000000000000055511151231257827021181583404541015625&lt;/code&gt; 是返回 &lt;code&gt;true&lt;/code&gt; 的，而且不管你删除多少位（保留 &lt;code&gt;0.1）&lt;/code&gt;都是 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这两个数相加，注意不能将上面的结果直接相加，计算机中的相加先对阶（低阶的向高阶对齐，减少精度损失），然后相加，在这里我们要先将 &lt;code&gt;0.1&lt;/code&gt; 的阶也变为$2 ^ {-3}$,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0.1&lt;/code&gt; ：
&lt;ul&gt;
&lt;li&gt;sign &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;exponent &lt;code&gt;01111111100&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;mantissa &lt;code&gt;1100110011001100110011001100110011001100110011001101&lt;/code&gt; 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相当于把 &lt;code&gt;0.1&lt;/code&gt; 的小数点向左移一位，注意的是最左边还有一个隐藏的 &lt;code&gt;1&lt;/code&gt;，最后一个 &lt;code&gt;0&lt;/code&gt; 就被舍弃了，需要注意的是被截断的最后一位如果是 &lt;code&gt;1&lt;/code&gt; 则需要在新数的末位再加一。&lt;/p&gt;
&lt;p&gt;对阶完成后就是有效数字部分相加，需要注意的是 &lt;code&gt;0.2&lt;/code&gt; 的左侧有一个隐藏的 &lt;code&gt;1&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;00.1100110011001100110011001100110011001100110011001101&lt;/code&gt; + &lt;code&gt;01.1001100110011001100110011001100110011001100110011010&lt;/code&gt; &lt;code&gt;10.0110011001100110011001100110011001100110011001100111&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;可以看出结果溢出了，这时候我们需要截断最后一位，阶数加一，由于截断的是 &lt;code&gt;1&lt;/code&gt;， 在新的尾数末位要加一，结果就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sign &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;exponent &lt;code&gt;01111111101&lt;/code&gt; 十进制&lt;code&gt;1021&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;mantissa &lt;code&gt;0011001100110011001100110011001100110011001100110100&lt;/code&gt; 十进制&lt;code&gt;900719925474100&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后再将结果转化为十进制 $(900719925474100 \times 2 ^ {-52} + 1) \times 2 ^ {-2} = 0.3000000000000000444089209850062616169452667236328125$&lt;/p&gt;
&lt;p&gt;这里就得到了我们在计算 &lt;code&gt;0.1 + 0.2&lt;/code&gt; 时得到的 &lt;code&gt;0.30000000000000004&lt;/code&gt;。误差的主要来源就是 &lt;code&gt;0.1&lt;/code&gt; 和 &lt;code&gt;0.2&lt;/code&gt; 在计算机中的保存就是存在误差的，之后在二者相加计算的时候又有舍入的误差。其实不仅仅是 &lt;code&gt;0.1 + 0.2&lt;/code&gt; 有误差，包括我们连续将 &lt;code&gt;0.1&lt;/code&gt; 相加，你会发现加 &lt;code&gt;8&lt;/code&gt; 次会得到 &lt;code&gt;0.7999999999999999&lt;/code&gt;，所以我们在进行对精度有要求的运算的时候一定要注意。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于最后的浮点数计算这一部分，一定要记住把隐藏的 &lt;code&gt;1&lt;/code&gt; 拿出来参与尾数的计算，因为这个隐藏的 &lt;code&gt;1&lt;/code&gt; 也是尾数的一部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里再插一点，上面提到在双精度浮点数中最大精确整数是&lt;code&gt;9007199254740991&lt;/code&gt;，就是 &lt;code&gt;9e16&lt;/code&gt; 这个数量级，而且 &lt;code&gt;JS&lt;/code&gt;是不区分整形和浮点型，整数也是用浮点数的形式表示的，所以在 &lt;code&gt;JS&lt;/code&gt; 中，超出 &lt;code&gt;15&lt;/code&gt; 位的数就不一定是绝对精确的了。这会导致一个问题，就是 &lt;code&gt;64&lt;/code&gt; 位整形能表示的最大数是 &lt;code&gt;19&lt;/code&gt; 位，后端数据库的 &lt;code&gt;id&lt;/code&gt; 通常是 &lt;code&gt;64&lt;/code&gt; 位整型其大小有可能会超过 &lt;code&gt;9e16&lt;/code&gt;，所以比较安全的方法是在后端转成字符串再传给前端。所以 &lt;code&gt;JS&lt;/code&gt; 灵活的类型转换，带来的缺点就是整数的精确表达范围小了很多。&lt;/p&gt;
&lt;p&gt;关于误差 &lt;code&gt;Wikipedia&lt;/code&gt; 上还给出了几个具体的例子，比如 &lt;code&gt;1990&lt;/code&gt; 年 &lt;code&gt;2&lt;/code&gt; 月 &lt;code&gt;25&lt;/code&gt; 日，海湾战争期间，在沙特阿拉伯宰赫兰的爱国者导弹防御系统因浮点数舍入错误而失效，该系统的计算机精度仅有 &lt;code&gt;24&lt;/code&gt; 位，存在 &lt;code&gt;0.0001%&lt;/code&gt; 的计时误差，所以有效时间阙值是 &lt;code&gt;20&lt;/code&gt; 个小时。当系统运行 &lt;code&gt;100&lt;/code&gt; 个小时以后，已经积累了 &lt;code&gt;0.3422&lt;/code&gt; 秒的误差。这个错误导致导弹系统不断地自我循环，而不能正确地瞄准目标。结果未能拦截一枚伊拉克飞毛腿导弹，飞毛腿导弹在军营中爆炸，造成 &lt;code&gt;28&lt;/code&gt; 名美国陆军死亡。&lt;/p&gt;
&lt;p&gt;小数有误差，整数同样会产生误差。我们上面的表格给出了单精度最大的规约数大概是 $\pm(2-2^{-23})\times2^{127}\approx\pm3.4\times10^{38}$，但其实当我们超出了有效数字的表示范围以后就一样会产生误差，单精度的有效数字位数是 &lt;code&gt;24&lt;/code&gt;，双精度的有效数字位数是 &lt;code&gt;53&lt;/code&gt;（都要加上默认的 &lt;code&gt;1&lt;/code&gt;），所以单精度的最大安全整数是 $2^{24}-1$，而双精度的最大安全整数是 $2^{24}-1$，所谓的最大安全整数是指能够连续表示的整数，在有效数字范围内的整数都是能连续表示的，而超出有效数字范围则会产生误差。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;当我们要对浮点数进行比较的时候，比较安全的方式是用 &lt;code&gt;Number.EPSILON&lt;/code&gt;。&lt;code&gt;Number.EPSILON&lt;/code&gt; 表示 &lt;code&gt;1&lt;/code&gt; 与 &lt;code&gt;Number&lt;/code&gt; 可表示的大于 &lt;code&gt;1&lt;/code&gt; 的最小的浮点数之间的差值，对于双精度浮点数，这个值为 $2^{-52}$，接近于 &lt;code&gt;2.2204460492503130808472633361816E-16&lt;/code&gt;，利用这个属性比较浮点数可以得出正确的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(Math.abs(0.1 + 0.2 - 0.3) &amp;#x3C;= Number.EPSILON)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Number.EPSILON&lt;/code&gt; 是 &lt;code&gt;ES6&lt;/code&gt; 添加的新属性，如果考虑兼容性，可以使用如下 &lt;code&gt;polyfill&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (Number.EPSILON === undefined) {
  Number.EPSILON = Math.pow(2, -52)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他还有 &lt;code&gt;Number.prototype.toprecision()&lt;/code&gt; 和 &lt;code&gt;Number.prototype.toFixed()&lt;/code&gt; 方法，他们根据我们给出的精度返回一个字符串，我们可以再用 &lt;code&gt;parseFloat&lt;/code&gt; 获得我们想要的数字。&lt;code&gt;ECMA-262&lt;/code&gt; 只需要最多 &lt;code&gt;21&lt;/code&gt; 位显示数字。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Number.prototype.toprecision()&lt;/code&gt; 和 &lt;code&gt;Number.prototype.toFixed()&lt;/code&gt; 的区别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;toPrecision&lt;/code&gt; 是处理精度，精度是从左至右第一个不为 &lt;code&gt;0&lt;/code&gt; 的数开始数起。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toFixed&lt;/code&gt; 是小数点后指定位数取整，从小数点开始数起。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;他们都能够对精度进行处理，进行四舍五入或添 &lt;code&gt;0&lt;/code&gt;，不过需要注意的是，我们的有效数字部分一共只有 &lt;code&gt;53&lt;/code&gt; 位，所以真正能精确表示的也就 &lt;code&gt;9007199254740991&lt;/code&gt; 个数，&lt;code&gt;16&lt;/code&gt; 位，超过这个范围，都是只能取近似了，小数也是一样。但是他们也会有 &lt;code&gt;bug&lt;/code&gt;，比如 &lt;code&gt;1.005.toFixed(2)&lt;/code&gt; 返回的结果是 &lt;code&gt;1.00&lt;/code&gt; 而不是 &lt;code&gt;1.01&lt;/code&gt;，因为 &lt;code&gt;1.005&lt;/code&gt; 的实际值是 &lt;code&gt;1.00499999999999989&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;还有一个比较完美的解法就是第三方库 &lt;a href=&quot;https://github.com/dt-fe/number-precision&quot; title=&quot;number-precision&quot;&gt;number-precision&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;JS 中的数字进制&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;JS&lt;/code&gt; 中我们也可以用不同的进制来表示数字字面量，&lt;code&gt;0b&lt;/code&gt; 或 &lt;code&gt;0B&lt;/code&gt; 前缀表示二进制，如果 &lt;code&gt;0b&lt;/code&gt; 后面的数字不是 &lt;code&gt;0&lt;/code&gt; 或者 &lt;code&gt;1&lt;/code&gt; 会提示愈发爱错误 &lt;code&gt;Missing binary digits after 0b&lt;/code&gt;；&lt;code&gt;0o&lt;/code&gt; 前缀表示八进制，&lt;code&gt;0&lt;/code&gt; 开头也可以表示八进制，假如 &lt;code&gt;0&lt;/code&gt; 后面的数字不在 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;7&lt;/code&gt; 的范围内，该数字将会被转换成十进制数字。&lt;code&gt;ECMAScript 5&lt;/code&gt; 严格模式下禁止使用八进制语法。八进制语法并不是 &lt;code&gt;ECMAScript 5&lt;/code&gt; 规范的一部分， &lt;code&gt;ECMAScript 6&lt;/code&gt; 中使用八进制数字是需要给一个数字添加前缀 &lt;code&gt;0o&lt;/code&gt;；&lt;code&gt;0x&lt;/code&gt; 或者 &lt;code&gt;0X&lt;/code&gt; 前缀表示十六进制，假如 &lt;code&gt;0x&lt;/code&gt; 后面的数字超出规定范围(&lt;code&gt;0123456789ABCDEF&lt;/code&gt;)，那么就会提示这样的语法错误(&lt;code&gt;SyntaxError&lt;/code&gt;)：&lt;code&gt;Identifier starts immediately after numeric literal&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;还有一点需要注意的是，&lt;code&gt;JS&lt;/code&gt; 中的小数点只要一边有数字即为合法的字面量，也就是说 &lt;code&gt;0.&lt;/code&gt; 和 &lt;code&gt;.0&lt;/code&gt; 都是合法的字面量。当我们调用数字的 &lt;code&gt;toString&lt;/code&gt; 方法的时候，如果表达式为 &lt;code&gt;0.toString()&lt;/code&gt; 是会报错的，合法的写法是 &lt;code&gt;0 .toString()&lt;/code&gt;，在 &lt;code&gt;0&lt;/code&gt; 和 &lt;code&gt;.&lt;/code&gt; 之间留一个空格。这其实是一个 &lt;code&gt;bug&lt;/code&gt;，不过目前我们只能这么使用。其他进制转十进制用 &lt;code&gt;parseInt&lt;/code&gt;，注意 &lt;code&gt;parseInt&lt;/code&gt; 的第一个参数需要是 &lt;code&gt;String&lt;/code&gt;，如果传入的不是一个字符串，会调用 &lt;code&gt;toString&lt;/code&gt; 方法转换为字符（这里的机制还是比较复杂的，参考 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt;），第二个数字表示字符串的进制。十进制转其他进制使用 &lt;code&gt;Number.prototype.toString()&lt;/code&gt;，&lt;code&gt;toString&lt;/code&gt; 接受一个参数即要转换的进制。&lt;code&gt;parseInt&lt;/code&gt; 和 &lt;code&gt;Number.prototype.toString&lt;/code&gt; 的进制数都在 &lt;code&gt;2-36&lt;/code&gt; 范围内，超出这个范围将会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//JS中的进制转换
parseInt(num, 8) //八进制转十进制
parseInt(num, 16) //十六进制转十进制
parseInt(num).toString(8) //十进制转八进制
parseInt(num).toString(16) //十进制转十六进制
parseInt(num, 2).toString(8) //二进制转八进制
parseInt(num, 2).toString(16) //二进制转十六进制
parseInt(num, 8).toString(2) //八进制转二进制
parseInt(num, 8).toString(16) //八进制转十六进制
parseInt(num, 16).toString(2) //十六进制转二进制
parseInt(num, 16).toString(8) //十六进制转八进制
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这篇文章主要是讲了浮点数的标准和误差产生的原因，自己的总结可能会有一些错误，望指正。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/24115452/answer/95805511&quot;&gt;浮点数指数为什么使用移码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2010/06/ieee_floating-point_representation.html&quot;&gt;浮点数的二进制表示&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/IEEE_754#%E9%9D%9E%E8%A7%84%E7%BA%A6%E5%BD%A2%E5%BC%8F%E7%9A%84%E6%B5%AE%E7%82%B9%E6%95%B0&quot;&gt;Wikipedia-IEEE754&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://justjavac.com/codepuzzle/2012/11/11/codepuzzle-float-who-stole-your-accuracy.html&quot;&gt;谁偷了你的精度&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rrfed.com/2017/05/13/float-number/&quot; title=&quot;为什么0.1 + 0.2不等于0.3？&quot;&gt;为什么0.1 + 0.2不等于0.3？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/92150145&quot; title=&quot;JS与数 - 李银城的文章 - 知乎 &quot;&gt;JS与数 - 李银城的文章 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/camsong/blog/issues/9&quot; title=&quot;JavaScript 浮点数陷阱及解法&quot;&gt;JavaScript 浮点数陷阱及解法&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>终端不区分大小写</title><link>https://clloz.com/blog/completion-ignore-case</link><guid isPermaLink="true">https://clloz.com/blog/completion-ignore-case</guid><pubDate>Mon, 10 Jun 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在使用 &lt;code&gt;Linux&lt;/code&gt; 或者 &lt;code&gt;Mac&lt;/code&gt; 的时候，有时候会遇到路径需要区分大小写并且 &lt;code&gt;tab&lt;/code&gt; 不能自动补全的情况，非常不方便。其实只要稍微配置一下即可。&lt;/p&gt;
&lt;h2&gt;配置方法&lt;/h2&gt;
&lt;p&gt;在用户目录 &lt;code&gt;~&lt;/code&gt; 中新建 &lt;code&gt;.inputrc&lt;/code&gt; 文件输入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;set completion-ignore-case on
set show-all-if-ambiguous on
TAB: menu-complete
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在 &lt;code&gt;mac&lt;/code&gt; 中该文件可能需要 &lt;code&gt;sudo&lt;/code&gt; 才能又权限编辑。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="/_astro/terminal.BZCEBg10.png"/><enclosure url="/_astro/terminal.BZCEBg10.png"/></item><item><title>原码，反码和补码</title><link>https://clloz.com/blog/true-form-ones-complement-twos-complement</link><guid isPermaLink="true">https://clloz.com/blog/true-form-ones-complement-twos-complement</guid><pubDate>Wed, 05 Jun 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;以前在学习计算机科学导论的时候，仔细研究过原码，反码，补码之间的关系以及他们是用来解决什么问题产生的。今天在看 &lt;code&gt;JavaScript&lt;/code&gt; 中的 &lt;code&gt;Number&lt;/code&gt; 的时候想到这三个概念，用一篇文章来整理一下相关的知识。&lt;/p&gt;
&lt;h2&gt;机器数和真值&lt;/h2&gt;
&lt;p&gt;虽然我们有反码补码的概念，但是对于硬件来说，他们完全不明白这是什么。对于硬件来说，有 &lt;code&gt;n&lt;/code&gt; 位存储单元，就有 $2^n$ 个状态，也就能表示 $2^n$ 个数。也就是说原码，反码和补码都是人为创造出来的概念，其目的就是用数学的方法来简化硬件的设计。比如我们规定了最高位为符号位，那么一个 &lt;code&gt;8&lt;/code&gt; 位的存储单元可以用 &lt;code&gt;10000001&lt;/code&gt; 来表示 &lt;code&gt;-1&lt;/code&gt;，其中这个 &lt;code&gt;10000001&lt;/code&gt; 就是机器数，而 &lt;code&gt;-1&lt;/code&gt; 就是真值。&lt;/p&gt;
&lt;h2&gt;原码&lt;/h2&gt;
&lt;p&gt;计算机要进行计算就必须存储数值，最简单的方法就是用机器码作为我们的真值，比如一个 &lt;code&gt;8&lt;/code&gt; 位的存储单元可以存储 &lt;code&gt;00000000&lt;/code&gt; ～ &lt;code&gt;11111111&lt;/code&gt; 共 &lt;code&gt;256&lt;/code&gt; 种数字，真值从 &lt;code&gt;0&lt;/code&gt; ～ &lt;code&gt;255&lt;/code&gt;，但是这样设计显然意义不大，因为我们需要计算负数。为了引入负数，所以设计出了原码：用存储单元的第一位表示符号，&lt;code&gt;1&lt;/code&gt; 位负，&lt;code&gt;0&lt;/code&gt; 为正，后面的位表示真值的绝对值。同样以 &lt;code&gt;8&lt;/code&gt; 位存储单元位例，在用原码的方式下我们依然可以表示 &lt;code&gt;256&lt;/code&gt; 个数，从 &lt;code&gt;11111111&lt;/code&gt; ～ &lt;code&gt;01111111&lt;/code&gt;，真值范围为 &lt;code&gt;-127&lt;/code&gt; ～ &lt;code&gt;127&lt;/code&gt;，可能你发现真值一共只有&lt;code&gt;255&lt;/code&gt; 个，因为 &lt;code&gt;10000000&lt;/code&gt; 和 &lt;code&gt;00000000&lt;/code&gt; 表示的都是 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/true-form.6qHAPGb0_1Q3L63.webp&quot; alt=&quot;true-form&quot; title=&quot;true-form&quot;&gt;&lt;/p&gt;
&lt;p&gt;从真值的二进制到原码，我们可以看出，对于机器来说没有区别，只是我们用数学手段赋予了同一个二进制数不同的意义，为的是能够让计算机满足我们的计算需求。跟随这个思想，我们同样也能理解反码和补码。&lt;/p&gt;
&lt;h2&gt;反码&lt;/h2&gt;
&lt;p&gt;从真值的二进制到原码，我们跨出了一步，在计算机中引入了负数。但是原码存在很严重的问题，就是对于 &lt;code&gt;-1&lt;/code&gt;：&lt;code&gt;10000001&lt;/code&gt;和 &lt;code&gt;+1&lt;/code&gt;：&lt;code&gt;00000001&lt;/code&gt;，他们的和是 &lt;code&gt;10000010&lt;/code&gt;，按照原码的约定规则，这个数是 &lt;code&gt;-2&lt;/code&gt;，这样显然是不行的，电路也无法设计。对于人类来说，分辨第一位是符号为很简单，我们会根据符号选择对后面的绝对值进行加还是减并得出结果。但是对于计算机来说将符号位分开处理是一件非常复杂的事情，如果我们把基础的加减乘除运算的电路都设计的非常复杂，那么这个计算机的性能显然是很差的。现在的问题是如何让符号位能够参与运算，并且形成一个完善的逻辑。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们引入了反码，反码的定义是：如果是正数则保持不变，如果是负数，则除符号位之外按位取反。具体看下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/ones-complement.CT72OAhY_Z1kuJgp.webp&quot; alt=&quot;ones-complement&quot; title=&quot;ones-complement&quot;&gt;&lt;/p&gt;
&lt;p&gt;通过反码逻辑的设计，我们可以发现现在正负数相加获得的都是 &lt;code&gt;1111&lt;/code&gt;，也就是 &lt;code&gt;-0&lt;/code&gt;，现在我们已经可以设计电路然后用计算机进行存储和计算了。&lt;/p&gt;
&lt;h2&gt;补码&lt;/h2&gt;
&lt;p&gt;反码解决了带符号位运算的问题，但是还有一个问题没有解决，就是现在我们有两个 &lt;code&gt;0&lt;/code&gt;，一个 &lt;code&gt;+0&lt;/code&gt;， 一个 &lt;code&gt;-0&lt;/code&gt;，这两者在数学上是没有区别的，并且占了两个编码。为了解决这个问题，又引入了补码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/twos-complement.LinABt0n_Z1qtQ4C.webp&quot; alt=&quot;twos-complement&quot; title=&quot;twos-complement&quot;&gt;&lt;/p&gt;
&lt;p&gt;补码的定义就是正数和 &lt;code&gt;0&lt;/code&gt; 的补码不变，负数的补码是其对应的正数按位取反再 &lt;code&gt;+1&lt;/code&gt;。负数也可以说成是反码 &lt;code&gt;+1&lt;/code&gt;。还以 &lt;code&gt;8&lt;/code&gt; 为存储单位为例，&lt;code&gt;-1&lt;/code&gt; 的原码是 &lt;code&gt;10000001&lt;/code&gt;，反码是 &lt;code&gt;11111110&lt;/code&gt;，补码在反码的基础上 &lt;code&gt;+1&lt;/code&gt;，即为 &lt;code&gt;11111111&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;从图中可以看出，&lt;code&gt;-0&lt;/code&gt; 取补码后跟 &lt;code&gt;0&lt;/code&gt; 是一致的，解决了两个 &lt;code&gt;0&lt;/code&gt; 的问题。在 &lt;code&gt;-1&lt;/code&gt; ～ &lt;code&gt;-7&lt;/code&gt; 之后，还多处一个编码 &lt;code&gt;1000&lt;/code&gt; 可以用来表示 &lt;code&gt;-8&lt;/code&gt;，这也就是我们今天计算机所用的存储和计算的方式。&lt;/p&gt;
&lt;h2&gt;数学原理&lt;/h2&gt;
&lt;p&gt;其实上面的补码定义肯定让很多人产生疑问，最后这个 &lt;code&gt;-8&lt;/code&gt; 到底是哪来的？这是因为这个通用的教科书定义只是给出了补码之形，并没有告诉我们补码之质，表面上是要让我们更好的理解补码，其实更容易让人产生误导。上面的内容一步一步介绍补码，感觉似乎补码是 &lt;code&gt;设计&lt;/code&gt; 出来的，但其实只是编码背后存在一定的数学规律，而补码正式最合适的那个，所以被用来实现计算机了。&lt;/p&gt;
&lt;h2&gt;同余&lt;/h2&gt;
&lt;p&gt;先来说一说同余概念，我们来看看下面的表盘&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/watch.Ch5WWvOS_26HqTl.webp&quot; alt=&quot;watch&quot; title=&quot;watch&quot;&gt;&lt;/p&gt;
&lt;p&gt;表盘上的指针指向 &lt;code&gt;8&lt;/code&gt;，那如果我现在要调节指针指向 &lt;code&gt;1&lt;/code&gt; 要如何操作呢？ 1. 顺时针拨动 &lt;code&gt;5&lt;/code&gt; 个小时 &lt;code&gt;(8 + 5) mod 12 = 1&lt;/code&gt; 2. 逆时针拨动 &lt;code&gt;7&lt;/code&gt; 个小时 &lt;code&gt;(8 -7) mod 12 = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;对于表盘来说，我们顺时针拨动 &lt;code&gt;5&lt;/code&gt; 个小时和逆时针拨动 &lt;code&gt;7&lt;/code&gt; 个小时是没有区别的。甚至我们可以顺时针拨动 &lt;code&gt;5 + 12n&lt;/code&gt; 个小时，或者逆时针拨动 &lt;code&gt;7 + 12n&lt;/code&gt; 个小时。换一个角度，现在指针指向 &lt;code&gt;8&lt;/code&gt; ，如果只看这个表盘你并不能分辨现在是早上 &lt;code&gt;8&lt;/code&gt; 点还是晚上 &lt;code&gt;20&lt;/code&gt; 点。&lt;/p&gt;
&lt;p&gt;从上面的现象我们可以总结出两点结论， 1. 如果把顺时针看作加法，把逆时针看作减法，那么一个闭合的连续自然数环的减法和加法是可以互相替代的。 2. &lt;code&gt;5&lt;/code&gt; 和 &lt;code&gt;5 + 12n&lt;/code&gt; 的效果相同，也就是说只要相对于环的数字总数的余数相同，那么效果相同。&lt;/p&gt;
&lt;p&gt;这个规律就是数学中的同余：两个整数 &lt;code&gt;a&lt;/code&gt;，&lt;code&gt;b&lt;/code&gt;，若它们除以整数 &lt;code&gt;m&lt;/code&gt; 所得的余数相等，则称 &lt;code&gt;a&lt;/code&gt;，&lt;code&gt;b&lt;/code&gt; 对于模 &lt;code&gt;m&lt;/code&gt; 同余。记作 &lt;code&gt;a ≡ b (mod m)&lt;/code&gt;，&lt;code&gt;a&lt;/code&gt; 与 &lt;code&gt;b&lt;/code&gt; 关于模 &lt;code&gt;m&lt;/code&gt; 同余。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;5 mod 12 = 5

17 mod 12 = 5

29 mod 12 = 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以 &lt;code&gt;5&lt;/code&gt;，&lt;code&gt;17&lt;/code&gt;，&lt;code&gt;29&lt;/code&gt; 关于模 &lt;code&gt;12&lt;/code&gt; 同余。&lt;/p&gt;
&lt;p&gt;同余的数学定义是：&lt;/p&gt;
&lt;p&gt;$$ x \bmod y = x\,-\,y\,\llcorner\,x/y\,\lrcorner $$&lt;/p&gt;
&lt;p&gt;&lt;code&gt;x mod y&lt;/code&gt; 等于 &lt;code&gt;x&lt;/code&gt; 减去&lt;code&gt;y&lt;/code&gt; 乘上 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;y&lt;/code&gt; 的商的下界。举个例子 &lt;code&gt;-3 mod 2&lt;/code&gt;等于$ -3 - 2 \times \llcorner -1.5\lrcorner $，等于 &lt;code&gt;-3 - 2 * (-2)&lt;/code&gt;，结果为 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;同余的深入&lt;/h2&gt;
&lt;p&gt;介绍完同余的概念，要理解为什么计算机使用补码，我们还需要知道一点：由于计算机的字长是有限的，所以计算机的运算本质就是同余运算，比如字长 &lt;code&gt;32&lt;/code&gt; 位，一共能表示 $2^{32}$ 共 &lt;code&gt;4294967296&lt;/code&gt; 个状态，当我们计算 &lt;code&gt;1 + 2&lt;/code&gt; 的时候，本质是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;a ≡ 1 (mod 4294967296)
b ≡ 2 (mod 4294967296)
r ≡ a + b ≡ 3 (mod 4294967296)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在回到上面的表盘，我们把表盘上的 &lt;code&gt;12&lt;/code&gt; 写作 &lt;code&gt;0&lt;/code&gt;，现在我们的表盘就表示从 &lt;code&gt;0&lt;/code&gt; ～ &lt;code&gt;11&lt;/code&gt; 一共 &lt;code&gt;12&lt;/code&gt; 个数。我们以 &lt;code&gt;0&lt;/code&gt; 为起点，&lt;code&gt;12&lt;/code&gt; 为模，逆时针拨动指针就得到负数，顺时针拨动指针就是正数。比如我们逆时针拨动一格，我们就得到 &lt;code&gt;-1&lt;/code&gt;，&lt;code&gt;-1&lt;/code&gt; 和 &lt;code&gt;11&lt;/code&gt; 关于模 &lt;code&gt;12&lt;/code&gt; 同余，所以他们等价。利用同余的这种性质，我们引入了负数，&lt;/p&gt;
&lt;p&gt;那么如果我们把&lt;code&gt;00000000&lt;/code&gt; ～ &lt;code&gt;11111111&lt;/code&gt; 放到一个表盘上呢，它的规律和只有 &lt;code&gt;12&lt;/code&gt; 格的表盘是一样的，&lt;code&gt;11111111&lt;/code&gt; 可以表示 &lt;code&gt;255&lt;/code&gt; 也可以表示 &lt;code&gt;-1&lt;/code&gt;，在同余计算中他们是等价的。那么&lt;code&gt;8位&lt;/code&gt; 一共 &lt;code&gt;256&lt;/code&gt; 个状态，我们既然要引入负数当然希望正数负数一样多，于是我们从 &lt;code&gt;00000001&lt;/code&gt; ～ &lt;code&gt;01111111&lt;/code&gt; 表示 &lt;code&gt;1&lt;/code&gt; ～ &lt;code&gt;127&lt;/code&gt;，&lt;code&gt;11111111&lt;/code&gt; ～ &lt;code&gt;10000001&lt;/code&gt; 表示 &lt;code&gt;-1&lt;/code&gt; ～ &lt;code&gt;-127&lt;/code&gt;，还多出两个数 &lt;code&gt;00000000&lt;/code&gt; 和 &lt;code&gt;10000000&lt;/code&gt;。&lt;code&gt;00000000&lt;/code&gt; 自然是表示 &lt;code&gt;0&lt;/code&gt;，那么 &lt;code&gt;10000000&lt;/code&gt; 表示什么呢？它既可以表示 &lt;code&gt;128&lt;/code&gt; 也可以表示 &lt;code&gt;-128&lt;/code&gt;，但是为了统一性以方便数据处理，我们用 &lt;code&gt;10000000&lt;/code&gt; 来表示 &lt;code&gt;-128&lt;/code&gt;，这样所有的负数的最高位都是 &lt;code&gt;1&lt;/code&gt;。这些负数的二进制编码就是所谓的补码了。&lt;/p&gt;
&lt;p&gt;介绍了半天，告诉大家 &lt;code&gt;-8&lt;/code&gt; 到底是哪来的，可以好像又脱离的补码的概念了，补码的本质到底是什么？其实补码只是上面一段内容背后的数学规律，我们用 &lt;code&gt;11111111&lt;/code&gt; ～ &lt;code&gt;10000000&lt;/code&gt; 表示了 &lt;code&gt;-1&lt;/code&gt; ～ &lt;code&gt;-128&lt;/code&gt;，那比如我想知道 &lt;code&gt;-57&lt;/code&gt; 的二进制码是多少，要怎么计算呢？&lt;/p&gt;
&lt;p&gt;在二进制计算中我们很容易可以发现一个规律就是 &lt;code&gt;X + ~X + 1 = 0&lt;/code&gt;，&lt;code&gt;~X&lt;/code&gt; 表示 &lt;code&gt;X&lt;/code&gt; 的反码，我们将这个式子处理一下：&lt;code&gt;-X = ~X +1&lt;/code&gt;，是不是很熟悉，我们要求一个负数的二进制表示，只要将其对应的正数的反码 &lt;code&gt;+1&lt;/code&gt; 就可以了。&lt;/p&gt;
&lt;h2&gt;反码和补码的英文&lt;/h2&gt;
&lt;p&gt;反码的英文是 &lt;code&gt;Ones&apos; complement&lt;/code&gt;，这是因为原码加上反码得到的是全为 &lt;code&gt;1&lt;/code&gt; 的二进制，所以这个翻译的意思就是原码相对于全 &lt;code&gt;1&lt;/code&gt; 的编码的补，也就是字面意思，这里的 &lt;code&gt;Ones&apos;&lt;/code&gt; 就是很多一的意思。&lt;/p&gt;
&lt;p&gt;补码的英文是 &lt;code&gt;Two&apos;s complement&lt;/code&gt;，一个 &lt;code&gt;n&lt;/code&gt; 位的二进制数，其模为 $2^n$，一个负数 &lt;code&gt;m&lt;/code&gt; 的补码即为 $2^n + m$，所以这里的翻译就是相对于模来说的。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;补码的本质就是由于计算机字长的限制，让我们在计算机中的数的表示是有限的，形成了一个环，计算的本质也是同余运算。说直白一点，就像钟表上的 &lt;code&gt;12&lt;/code&gt; 个点，我们可以认为是 &lt;code&gt;0～11&lt;/code&gt; ，也可以是 &lt;code&gt;1～12&lt;/code&gt; ，甚至可以是 &lt;code&gt;3～14&lt;/code&gt;，它们都会遵循同余运算的特点，而我们选择哪一种取决于我们的计算需求。而在计算机中我们选择了对计算机设计最有利的一半负数，一半非负数的表示，并不是人为设计出来的。而在这种情形下的负数的二进制码是不直观的，书本上的补码概念正是阐述负数二进制码的数学规律，方便我们能够计算负数的编码。&lt;/p&gt;
&lt;p&gt;可能内容讲的有点乱，写文章的花了很久，不知道如何用更通俗的语言表述，肯能是自己的理解还不够透彻，如果又表述的不清晰或者不对的地方欢迎指正。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/21511392/answer/82989720&quot; title=&quot;知乎 - coldwinds&quot;&gt;知乎 - coldwinds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/20159860/answer/71256667&quot; title=&quot;知乎 - 插画-李俊达&quot;&gt;知乎 - 插画-李俊达&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html#!comments&quot; title=&quot;原码，反码，补码详解&quot;&gt;原码，反码，补码详解&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Github markdown 锚点</title><link>https://clloz.com/blog/github-markdown-anchor</link><guid isPermaLink="true">https://clloz.com/blog/github-markdown-anchor</guid><pubDate>Tue, 04 Jun 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;一般的 &lt;code&gt;HTML&lt;/code&gt; 锚点都是通过 &lt;code&gt;url&lt;/code&gt; 中的 &lt;code&gt;hash&lt;/code&gt; 来让浏览器知道要滚动到哪个元素。这里说的 &lt;code&gt;hash&lt;/code&gt; 就是 &lt;code&gt;location.hash&lt;/code&gt; 可以简单理解为 &lt;code&gt;url&lt;/code&gt; 中 &lt;code&gt;#&lt;/code&gt; 后面的内容。如果我们的页面中有元素的 &lt;code&gt;id&lt;/code&gt; 和 &lt;code&gt;hash&lt;/code&gt; 相同，并且它处在一个可滚动的容器中，那么浏览器会在页面加载的时候直接滚动到该元素。&lt;/p&gt;
&lt;p&gt;这个功能很有用，通常是用在一个很长的页面中的目录上，比如我们经常看到的 &lt;code&gt;HTML&lt;/code&gt;，&lt;code&gt;CSS&lt;/code&gt;，&lt;code&gt;ECMAScript&lt;/code&gt; 等标准一般都会给目录加上锚点，方便我们直接跳转到对应内容，否则靠滚动的话太不方便。&lt;code&gt;markdown&lt;/code&gt; 自然也有需要锚点的场景，不过不同的 &lt;code&gt;markdown&lt;/code&gt; 实现可能不相同，今天我在整理书单的时候想把书单也同步到 &lt;code&gt;Github&lt;/code&gt; 上，&lt;code&gt;README.md&lt;/code&gt; 中就需要用到锚点，这里说一下 &lt;code&gt;Github&lt;/code&gt; 的锚点使用。&lt;/p&gt;
&lt;h2&gt;锚点使用&lt;/h2&gt;
&lt;h2&gt;标题的锚点链接&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;markdown&lt;/code&gt; 中我们使用一个或多个 &lt;code&gt;#&lt;/code&gt; 来创建段落标题，在 &lt;code&gt;Github&lt;/code&gt; 上显示的 &lt;code&gt;markdown&lt;/code&gt; 的标题默认会加上与标题名称相同的锚点标记也就是 &lt;code&gt;id&lt;/code&gt;。所以我们可以使用如下方法来使用锚点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;[标题1](#标题1)
[标题2](#标题2)
[标题3](#标题3)

# 标题1

## 标题2

## 标题3
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是，我们经常使用中文标题，中文标题的锚点会进行 &lt;code&gt;url&lt;/code&gt; 的编码操作，&lt;code&gt;url&lt;/code&gt; 编码细节可以看&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/05/09/url-encode-decode/&quot; title=&quot;这篇文章&quot;&gt;这篇文章&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;锚点名称小写&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;[Github标题1](#github标题1)

## Github标题1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;空格用 - 代替&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;[Github 标题2 Test](#github-标题2-test)

## Github 标题2 Test
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/github.CXd4_sPJ.png"/><enclosure url="/_astro/github.CXd4_sPJ.png"/></item><item><title>void(0)和undefined</title><link>https://clloz.com/blog/void0-undefined</link><guid isPermaLink="true">https://clloz.com/blog/void0-undefined</guid><pubDate>Mon, 03 Jun 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们经常看到在一些框架中用 &lt;code&gt;void(0)&lt;/code&gt; 来代替 &lt;code&gt;undefined&lt;/code&gt;，这篇文章来说一说这种做法的原因。&lt;/p&gt;
&lt;h2&gt;关键字和保留字&lt;/h2&gt;
&lt;p&gt;每种语言都有自己的关键字和保留字（ &lt;code&gt;reserved words&lt;/code&gt; ），&lt;code&gt;JavaScript&lt;/code&gt; 自然也不例外。在&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Reserved_words&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt;可以查看保留字。值得注意的是我们经常使用的 &lt;code&gt;window&lt;/code&gt;， &lt;code&gt;undefined&lt;/code&gt;，都不是标准中的保留字，也就是说 &lt;code&gt;window&lt;/code&gt;，&lt;code&gt;undefined&lt;/code&gt; 都可以作为变量名或者属性名。虽然在浏览器实现中，我们在全局作用域中无法声明或改变这些变量，但是在函数作用域中我们可以使用 &lt;code&gt;window&lt;/code&gt; 或者 &lt;code&gt;undefined&lt;/code&gt; 作为变量名或者属性名。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function a() {
  var window = &apos;aaa&apos;
  var undefined = &apos;bbb&apos;
  console.log(window, undefined)
}
a() //aaa bbb

function b() {
  var window = {
    undefined: &apos;ccc&apos;
  }
  console.log(window.undefined)
}
b() //ccc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中可以看出我们在函数内可以重新声明并赋值，和我们声明的普通的标识符并没有什么区别，所以使用 &lt;code&gt;undefined&lt;/code&gt; 或者 &lt;code&gt;window.undefined&lt;/code&gt; 都是不可靠的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 &lt;code&gt;nodejs&lt;/code&gt; 环境中，&lt;code&gt;global&lt;/code&gt; 同样也不是保留字，可以赋值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;void 运算符&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.ecma-international.org/publications/standards/Ecma-262.htm&quot; title=&quot;ECMA-262&quot;&gt;ECMA-262&lt;/a&gt;规范中对 &lt;code&gt;void&lt;/code&gt; 运算符的定义&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The void Operator The production UnaryExpression : void UnaryExpression is evaluated as follows: - Let expr be the result of evaluating UnaryExpression. - Call GetValue(expr). - Return undefined. NOTE: GetValue must be called even though its value is not used because it may have observable side-effects.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;void&lt;/code&gt; 运算符的执行分为三步，先执行运算符后的表达式，对表达式的返回值执行 &lt;code&gt;GetValue&lt;/code&gt;，返回 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GetValue&lt;/code&gt; 是规范内部的 &lt;code&gt;Reference Specification Type&lt;/code&gt; 的一个方法，它将返回 &lt;code&gt;Reference&lt;/code&gt; 的具体值。这个 &lt;code&gt;Reference&lt;/code&gt; 和 &lt;code&gt;js&lt;/code&gt; 中的引用不是同一个东西，这里的 &lt;code&gt;Reference&lt;/code&gt; 是标准中的一个抽象类型，由三个部分组成：&lt;code&gt;base&lt;/code&gt;， &lt;code&gt;reference name&lt;/code&gt;，&lt;code&gt;stict mode flag&lt;/code&gt;。比如 &lt;code&gt;a.b&lt;/code&gt;，&lt;code&gt;a&lt;/code&gt; 对应 &lt;code&gt;base&lt;/code&gt;， &lt;code&gt;b&lt;/code&gt; 对应 &lt;code&gt;reference name&lt;/code&gt;。具体细节参考规范&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;void&lt;/code&gt; 的底层实现是返回 &lt;code&gt;undefined&lt;/code&gt;，所以用 &lt;code&gt;void expr&lt;/code&gt; 来表示 &lt;code&gt;undefined&lt;/code&gt; 是可靠的，通常我们用 &lt;code&gt;void 0&lt;/code&gt; 或者 &lt;code&gt;void (0)&lt;/code&gt; 来表示 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;void&lt;/code&gt; 和其他运算符一样，可以让 &lt;code&gt;JavaScript&lt;/code&gt; 引擎把一个 &lt;code&gt;function&lt;/code&gt; 关键字识别成函数表达式而不是函数声明（语句）。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>关于prototype和constructor的思考</title><link>https://clloz.com/blog/prototype-constructor</link><guid isPermaLink="true">https://clloz.com/blog/prototype-constructor</guid><pubDate>Fri, 31 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;今天在看 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 语法的时候在知乎上看到一个提问，具体内容看下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Dog() {
  this.tail = true
}
Dog.prototype.say = function () {
  return &apos;Woof&apos;
}
var dog = new Dog()
dog.say() // &quot;Woof&quot;
dog.constructor // Dog()
Dog.prototype = {
  paws: 4
}
var newDog = new Dog()

newDog.constructor // ƒ Object() { [native code] }
typeof newDog.constructor.prototype.paws // &quot;undefined&quot;
typeof dog.constructor.prototype.paws // &quot;number&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;题主对这几个输出的结果比较疑惑，我也来分析一下这几个结果，看看自己对原型这部分知识掌握得如何。&lt;/p&gt;
&lt;h2&gt;分析图&lt;/h2&gt;
&lt;p&gt;要搞清楚这些对象之间的关系，我们来画一画这些对象的关系图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/prototype-issue1.CoE-pKsz_16Q0iM.webp&quot; alt=&quot;prototype-issue&quot; title=&quot;prototype-issue&quot;&gt;&lt;/p&gt;
&lt;p&gt;图中不同类型的连线我已经用不同颜色标注出来，还是可以清楚的看出我们的各个对象之间的关系的。对象被我分成了三行，下面的分析中有时我会用第几行来说名对象的位置。&lt;/p&gt;
&lt;h2&gt;细节&lt;/h2&gt;
&lt;h2&gt;问题 1&lt;/h2&gt;
&lt;p&gt;第一个问题 &lt;code&gt;newDog.constructor&lt;/code&gt;，我们先来看看 &lt;code&gt;new&lt;/code&gt; 做了几件事&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个新的空对象&lt;/li&gt;
&lt;li&gt;把新对象的 &lt;code&gt;__proto__&lt;/code&gt; 链接到构造函数的 &lt;code&gt;prototype&lt;/code&gt; 对象（每一个函数都有一个 &lt;code&gt;prototype&lt;/code&gt; 属性指向一个对象，该对象有一个 &lt;code&gt;constructor&lt;/code&gt; 属性指向该函数）&lt;/li&gt;
&lt;li&gt;将第一步创建的新的对象作为 &lt;code&gt;this&lt;/code&gt;的上下文&lt;/li&gt;
&lt;li&gt;执行构造函数，如果构造函数没有返回值或者返回值不是一个对象，则返回 &lt;code&gt;this&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过上面的步骤我们可以知道我们的 &lt;code&gt;newDog&lt;/code&gt; 对象有一个 &lt;code&gt;__proto__&lt;/code&gt; 属性指向代码中的 &lt;code&gt;{paws: 4}&lt;/code&gt; 这个对象，因为在代码中我们人为地将构造函数的 &lt;code&gt;prototype&lt;/code&gt; 引用改变了，它现在指向了一个我们自定义的对象。需要注意的是，我们上面的步骤中说函数的 &lt;code&gt;prototype&lt;/code&gt; 指向的对象有一个 &lt;code&gt;constructor&lt;/code&gt; 属性指向函数，这种情况只限于引擎自动生成的 &lt;code&gt;prototype&lt;/code&gt; 对象，对于我们自己创建的对象是没有该属性的。&lt;/p&gt;
&lt;p&gt;可能有些同学有个疑问就是通过构造函数实例化的对象（比如代码中的 &lt;code&gt;dog&lt;/code&gt; 和 &lt;code&gt;newDog&lt;/code&gt; ）是否有 &lt;code&gt;constructor&lt;/code&gt; 属性呢？答案是没有，虽然我们经常看到有这样的用法，但是其实访问的是原型链上的某个 &lt;code&gt;prototype&lt;/code&gt; 对象中的 &lt;code&gt;constructor&lt;/code&gt; 属性。那么我们代码中的 &lt;code&gt;newDog.constructor&lt;/code&gt; 是哪一个呢？&lt;/p&gt;
&lt;p&gt;从图中我们可以看出 &lt;code&gt;newDog&lt;/code&gt; 对象的 &lt;code&gt;__proto__&lt;/code&gt; 指向了自定义的 &lt;code&gt;prototype&lt;/code&gt; 对象（第二行第三个），我们的 &lt;code&gt;newDog&lt;/code&gt; 和这个自定义的对象中都没有 &lt;code&gt;constructor&lt;/code&gt; 的属性，那么引擎自然会沿着原型链继续寻找。自定义对象的构造函数是 &lt;code&gt;Object&lt;/code&gt; 对象（也就是&lt;code&gt;ƒ Object() { [native code] }&lt;/code&gt;，这是引擎底层函数，用 &lt;code&gt;C++&lt;/code&gt; 编写），是函数自然就有 &lt;code&gt;prototype&lt;/code&gt; 属性，这个属性指向的对象也是 &lt;code&gt;newDog&lt;/code&gt; 原型链上的一个对象，并且它有 &lt;code&gt;constructor&lt;/code&gt; 属性，指向 &lt;code&gt;Object&lt;/code&gt; 对象，结果出来了，&lt;code&gt;newDog.constructor&lt;/code&gt; 就是 &lt;code&gt;ƒ Object() { [native code] }&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实例化对象中没有 &lt;code&gt;constructor&lt;/code&gt; 属性很好验证，用 &lt;code&gt;hasOwnProperty&lt;/code&gt; 方法即可。另外提一点就是引擎沿着原型链搜索属性，&lt;code&gt;Object.prototype&lt;/code&gt; 是最后一环，这里就是原型链的终点，&lt;code&gt;Object.prototype.__proto__ === null&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;问题 2&lt;/h2&gt;
&lt;p&gt;其实解决了问题 &lt;code&gt;1&lt;/code&gt;，问题 &lt;code&gt;2&lt;/code&gt; 也就迎刃而解了，&lt;code&gt;typeof newDog.constructor.prototype.paws&lt;/code&gt; 等价于 &lt;code&gt;Object.prototype.paws&lt;/code&gt;，显然这个属性是不存在的。&lt;/p&gt;
&lt;h2&gt;问题 3&lt;/h2&gt;
&lt;p&gt;问题 &lt;code&gt;3&lt;/code&gt; 的核心是当我们手动改变了构造函数的 &lt;code&gt;prototype&lt;/code&gt; 指向，之前实例化的对象的 &lt;code&gt;__proto__&lt;/code&gt; 指向会改变吗？从图中的对象关系和代码运行结果我们可以得出否定的结论。&lt;code&gt;dog.__proto__&lt;/code&gt; 此时依然指向 &lt;code&gt;prototype&lt;/code&gt; 改变之前由引擎自动生成的对象，该对象中的 &lt;code&gt;constructor&lt;/code&gt; 属性指向 &lt;code&gt;Dog&lt;/code&gt; 构造函数，&lt;code&gt;dog.constructor.prototype.paws&lt;/code&gt; 此时等价与 &lt;code&gt;Dog.prototype.paws&lt;/code&gt;，&lt;code&gt;Dog.prototype&lt;/code&gt; 此时指向 &lt;code&gt;{paws: 4}&lt;/code&gt;，自然能够获取该属性。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;我们在构造函数原型上添加方法的时候尽量使用 &lt;code&gt;Obj.prototype.xxx&lt;/code&gt; 的方式，而不要覆盖原型对象，这样会导致很多问题。即使在需要覆盖的时候，也要给新对象加上 &lt;code&gt;constructor&lt;/code&gt; 属性。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>文字跑马灯效果</title><link>https://clloz.com/blog/text-scroll</link><guid isPermaLink="true">https://clloz.com/blog/text-scroll</guid><pubDate>Wed, 29 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在前面制作音乐播放器的时候想要实现歌曲名称的跑马灯效果，因为有些歌曲的名字特别长，不能现实完全，大部分播放器都是在歌曲名称超过长度的时候滚动显示。但是跑马灯的效果一直无法做到完美衔接。因为整个页面是响应式的，所以歌曲名称所在的元素的长度也是不固定的，完美衔接的跑马灯是用两个相同的文本来进行动画滚动，如果长度无法固定那么效果肯定不好，这篇文章来研究一下如何实现比较流畅的跑马灯效果。&lt;/p&gt;
&lt;h2&gt;CSS 实现&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;CSS&lt;/code&gt; 来实现文字的滚动效果是可以的，不过有很多局限，特别是在文字和元素的宽度不确定的情况下。因为在动画的每个状态节点我们给出的属性必须是确定的属性而不能是一个动态的。可以很容易的分析出来，滚动的元素的长度必须是包含元素的整数倍才能够实现无缝的滚动，否则就会出现动画跳帧的情况，实现代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .wrap {
    margin: 0 auto;
    width: 200px;
    border: 1px solid;
    white-space: nowrap;
    overflow: hidden;
  }

  .wrap .content {
    height: 20px;
    font-size: 0;
  }

  .wrap p {
    position: relative;
    display: inline-block;
    left: 0;
    margin: 0;
    width: 200%;
    font-size: 16px;
    line-height: 20px;
    background: lightblue;
    animation: scroll 6s infinite linear;
    overflow: hidden;
    white-space: nowrap;
  }

  .wrap .content:hover p {
    animation-play-state: paused;
  }

  @keyframes scroll {
    0% {
      left: 0;
    }
    100% {
      left: -200%;
    }
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;wrap eg1&quot;&gt;
  &amp;#x3C;div class=&quot;content&quot;&gt;
    &amp;#x3C;p&gt;&amp;#x3C;span&gt;1.当文字超出范围的时候开始滚动&amp;#x3C;/span&gt;&amp;#x3C;/p&gt;
    &amp;#x3C;p&gt;&amp;#x3C;span&gt;2.当文字超出范围的时候开始滚动&amp;#x3C;/span&gt;&amp;#x3C;/p&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/css-text-scroll.CWC_aa1j_ZsT6oT.webp&quot; alt=&quot;css-scroll&quot; title=&quot;css-scroll&quot;&gt;&lt;/p&gt;
&lt;p&gt;这样看上去效果尚可，不过用 &lt;code&gt;CSS&lt;/code&gt; 实现是很容易出问题的，比如文字的长度超出了规定的长度（上面的代码中是 &lt;code&gt;200%&lt;/code&gt; ）。不过如果我们只是需要一个简单的滚动效果，选择 &lt;code&gt;CSS&lt;/code&gt; 是一个比较便捷的方式。&lt;/p&gt;
&lt;h2&gt;JS 实现&lt;/h2&gt;
&lt;h2&gt;利用 transition&lt;/h2&gt;
&lt;p&gt;是用 &lt;code&gt;JS&lt;/code&gt; 实现又两种思路，一种是结合 &lt;code&gt;transition&lt;/code&gt; 属性来调节 对应的&lt;code&gt;CSS&lt;/code&gt; 属性。我的思路是两个相同的文本，&lt;code&gt;inline-block&lt;/code&gt; 排列，改变第一个的 &lt;code&gt;left&lt;/code&gt;，监听 &lt;code&gt;transitioned&lt;/code&gt; 事件，当滚动结束后，将 &lt;code&gt;left&lt;/code&gt; 归零然后重复上述步骤。需要注意的是，在归零的时候，比如去掉元素样式中的 &lt;code&gt;transition&lt;/code&gt; 属性，否则归零的过程也是一个动画，就不符合我们的效果了。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .wrap {
    margin: 0 auto;
    width: 200px;
    border: 1px solid;
    white-space: nowrap;
    overflow: hidden;
  }

  .wrap .content {
    height: 20px;
  }

  .wrap p {
    position: relative;
    left: 0;
    display: inline-block;
    margin: 0;
    padding-right: 15px;
    font-size: 16px;
    line-height: 20px;
    background: lightblue;
    box-sizing: border-box;
  }

  .wrap p.transitioned {
    transition: left linear 3s;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;wrap eg1&quot;&gt;
  &amp;#x3C;div class=&quot;content&quot;&gt;
    &amp;#x3C;p&gt;1.当文字超出范围的时候开始滚动&amp;#x3C;/p&gt;
    &amp;#x3C;p&gt;1.当文字超出范围的时候开始滚动&amp;#x3C;/p&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var $text = $(&apos;.wrap p&apos;)
var $wrap = $(&apos;.wrap&apos;)
var t_w = $text.outerWidth()
console.log(t_w)
var wrap_w = $wrap.width()

function textScroll() {
  if (t_w &gt; wrap_w) {
    $text.addClass(&apos;transitioned&apos;)
    $text.css(&apos;left&apos;, -t_w + &apos;px&apos;)
  }
}

$(document).ready(function () {
  textScroll()
})

$text.on(&apos;transitionend webkitTransitionEnd oTransitionEnd&apos;, function () {
  console.log(&apos;end&apos;)
  $(this).removeClass(&apos;transitioned&apos;)
  $(this).css(&apos;left&apos;, 0)
  setTimeout(() =&gt; {
    $text.addClass(&apos;transitioned&apos;)
    $text.css(&apos;left&apos;, -t_w + &apos;px&apos;)
  }, 4)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是在归零后再次调用滚动方法的时候我用了 &lt;code&gt;setTimeout&lt;/code&gt; 来延迟触发，这是因为如果不实用异步的话，浏览器会把这多个渲染步骤优化为一个，动画不会触发，具体大家可以去测试一下。还有一个问题就是想要作出效果比较好的暂停不是很容易，因为 &lt;code&gt;transition&lt;/code&gt; 的时间是固定的，而我们鼠标移动到元素上的时间是不确定的，当我们移开鼠标的时候，&lt;code&gt;transition&lt;/code&gt; 的时间又重新计算了。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.clloz.com/study/text-scroll.html&quot;&gt;点击&lt;/a&gt;查看效果。&lt;/p&gt;
&lt;h2&gt;纯 JS 实现&lt;/h2&gt;
&lt;p&gt;最后一种方法是纯 &lt;code&gt;JS&lt;/code&gt; 的方法，利用定时器来实现动画。实现代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  .wrap {
    margin: 0 auto;
    width: 200px;
    border: 1px solid;
    white-space: nowrap;
    overflow: hidden;
  }

  .wrap .content {
    height: 20px;
  }

  .wrap p {
    position: relative;
    left: 0;
    display: inline-block;
    margin: 0;
    padding-right: 15px;
    font-size: 16px;
    line-height: 20px;
    background: lightblue;
    box-sizing: border-box;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;wrap eg1&quot;&gt;
  &amp;#x3C;div class=&quot;content&quot;&gt;
    &amp;#x3C;p&gt;1.当文字超出范围的时候开始滚动当文字超出范围的时候开始滚动&amp;#x3C;/p&gt;
    &amp;#x3C;p&gt;1.当文字超出范围的时候开始滚动当文字超出范围的时候开始滚动&amp;#x3C;/p&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var $text = $(&apos;.wrap p&apos;)
var $wrap = $(&apos;.wrap&apos;)
var t_w = $text.outerWidth()
var wrap_w = $wrap.width()
var mouse = false
var timer = null

function init() {
  animation()
}

function animation() {
  timer = setInterval(() =&gt; {
    scroll()
  }, 10)
}

function scroll() {
  var left = parseInt($text.css(&apos;left&apos;))
  if (left &gt; -t_w) {
    //   console.log(left)
    $text.css(&apos;left&apos;, left - 1 + &apos;px&apos;)
  } else {
    $text.css(&apos;left&apos;, &apos;0&apos;)
  }
}

$(&apos;.content&apos;).on(&apos;mouseenter&apos;, function (e) {
  e.stopPropagation()
  console.log(1)
  clearInterval(timer)
})
$(&apos;.content&apos;).on(&apos;mouseout&apos;, function (e) {
  console.log(2)
  e.stopPropagation()
  animation()
})

init()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;JS&lt;/code&gt; 我们能够非常容易的完成鼠标移动到文字上暂停滚动的效果。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.clloz.com/study/text-scroll2.html&quot;&gt;点击&lt;/a&gt;查看效果。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>解决hover生成border造成的元素移动</title><link>https://clloz.com/blog/hover-border</link><guid isPermaLink="true">https://clloz.com/blog/hover-border</guid><pubDate>Mon, 27 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们有时候会遇到 &lt;code&gt;hover&lt;/code&gt; 伪类给元素添加边框的时候，元素中的内容发生位移，虽然我们设置了 &lt;code&gt;box-sizing: border-box&lt;/code&gt;并且规定了元素的宽高，但是内容依然被边框挤开了。如下面这种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style type=&quot;text/css&quot; media=&quot;screen&quot;&gt;
  .test {
    height: 30vmin;
    width: 30vmin;
    background: lightblue;
    box-sizing: border-box;
  }
  .test:hover {
    border: 5px solid black;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;test&quot;&gt;this is a div.&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/hover-border.BTrmYSWh_oTHza.webp&quot; alt=&quot;hover-border&quot; title=&quot;hover-border&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里的原因很明显，我们的元素大小并没有变（如果没有设置元素宽高或者 &lt;code&gt;box-sizing: border-box&lt;/code&gt; 则元素大小会改变），&lt;code&gt;box-sizing: border-box&lt;/code&gt; 是生效的，但是元素中的内容因为突然添加的边框而被挤开了，我们的盒模型从外到内依次是&lt;code&gt;margin&lt;/code&gt;，&lt;code&gt;border&lt;/code&gt;，&lt;code&gt;padding&lt;/code&gt;，&lt;code&gt;content&lt;/code&gt;，所以新加入的 &lt;code&gt;border&lt;/code&gt; 必然将 &lt;code&gt;content&lt;/code&gt; 压缩的更小，并且 &lt;code&gt;content&lt;/code&gt; 的边界坐标也变了，因为导致视觉上的内容移动。所以解决问题的办法就是让边框的添加不影响 &lt;code&gt;content&lt;/code&gt; 的位置。&lt;/p&gt;
&lt;h2&gt;为元素添加边框&lt;/h2&gt;
&lt;p&gt;贸然出现的边框改变了原有的布局，让内容移动了，既然如此，我们可以在之前的布局中就让边框存在就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.test {
  height: 30vmin;
  width: 30vmin;
  background: lightblue;
  border: 5px solid transparent;
  box-sizing: border-box;
}
.test:hover {
  border: 5px solid black;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用 box-shadow&lt;/h2&gt;
&lt;p&gt;使用不占用盒模型空间的 &lt;code&gt;box-shadow&lt;/code&gt; 或者 &lt;code&gt;outline&lt;/code&gt; 也是一种选择，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.test:hover {
  /* border: 5px solid black; */
  box-shadow: 0 0 0 5px black;
  outline: 5px solid black;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;用 padding&lt;/h2&gt;
&lt;p&gt;我们可以通过改变 &lt;code&gt;padding&lt;/code&gt; 大小来给 &lt;code&gt;border&lt;/code&gt; 预留空间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.test {
  height: 30vmin;
  width: 30vmin;
  background: lightblue;
  box-sizing: border-box;
  padding: 5px;
}
.test:hover {
  padding: 0;
  border: 5px solid black;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>去掉模糊背景或图片的白边</title><link>https://clloz.com/blog/blur-white-border</link><guid isPermaLink="true">https://clloz.com/blog/blur-white-border</guid><pubDate>Thu, 23 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;有时候我们会对页面的背景或者某个图片使用 &lt;code&gt;blur&lt;/code&gt; 属性来达到模糊的毛玻璃效果，这样会让我们的内容更突出，不会被背景图片而干扰。但是使用模糊背景的一个问题是，在模糊的图片边缘，由于模糊效果会让底层的颜色露出来，比如我们对我们页面的背景进行模糊的时候会把 &lt;code&gt;body&lt;/code&gt; 的背景色透出来，一般我们的 &lt;code&gt;body&lt;/code&gt; 是白色的，所以会有一圈模糊的白边，下面来分享几种解决问题的方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/blur1.AmYCMPra_Z1vDsnq.webp&quot; alt=&quot;blur1&quot; title=&quot;blur1&quot;&gt;&lt;/p&gt;
&lt;p&gt;查看代码点击&lt;a href=&quot;https://www.clloz.com/study/blur/blur1.html&quot;&gt;页面&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;小图片&lt;/h2&gt;
&lt;p&gt;铺满全屏的背景和页面上的小图片的处理方法的不同的，对于页面上的小图片我们只需要给它的包含元素添加一个 &lt;code&gt;overflow: hidden&lt;/code&gt; 就可以。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  html,
  body {
    height: 100%;
    width: 100%;
    margin: 0;
    padding: 0;
  }

  .wrap {
    height: 300px;
    width: 450px;
    margin: 20px auto;
  }
  .wrap.s2 {
    overflow: hidden;
  }
  .wrap img {
    height: 300px;
    width: 450px;
    filter: blur(5px);
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;wrap s1&quot;&gt;&amp;#x3C;img src=&quot;https://img.clloz.com/blog/writing/totoro.jpg&quot; alt=&quot;&quot; /&gt;&amp;#x3C;/div&gt;
&amp;#x3C;div class=&quot;wrap s2&quot;&gt;&amp;#x3C;img src=&quot;https://img.clloz.com/blog/writing/totoro.jpg&quot; alt=&quot;&quot; /&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以明显的看出第一个图片的边缘也是模糊效果，&lt;code&gt;body&lt;/code&gt; 的白色透出，而第二张图片虽然也是模糊的，但是边缘却很清晰。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/blur2.BV5Yqj4l_Z1i04wd.webp&quot; alt=&quot;blur2&quot; title=&quot;blur2&quot;&gt;&lt;/p&gt;
&lt;p&gt;查看代码点击&lt;a href=&quot;https://www.clloz.com/study/blur/blur2.html&quot;&gt;页面&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;全屏背景处理&lt;/h2&gt;
&lt;p&gt;处理小图片的方式非常简单，但是这个方法在页面背景上却不生效，至于为什么我也不太清楚，不过我尝试了多次，只使用 &lt;code&gt;overflow: hidden&lt;/code&gt; 并不能让页面的背景边缘变清晰。&lt;/p&gt;
&lt;p&gt;只能另辟蹊径了，大家的解决方法虽然代码有所不同，不过手段都差不多，都是通过扩大背景所在元素的大小来实现的，也就是说把模糊的那部分移动到可视范围之外，具体有如下几种做法。&lt;/p&gt;
&lt;h2&gt;transform: scale() 扩大&lt;/h2&gt;
&lt;p&gt;既然要改变大小，自然会想到 &lt;code&gt;transform&lt;/code&gt; 属性，使用这个属性需要注意的是，扩大的比例。如果你的图片不需要考虑细节，那么你可以把比例调大一点。如果图片细节比较重要，那么就要选择适当的比例，还要考虑在不同大小的屏幕上的效果。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  html,
  body {
    height: 100%;
    width: 100%;
  }
  .cover {
    height: 100%;
    width: 100%;
    background: url(&apos;https://img.clloz.com/blog/writing/totoro.jpg&apos;);
    background-size: cover;
    filter: blur(5px) brightness(0.5);
    transform: scale(1.02);
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;cover&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/blur3.CsyIN_43_IUI20.webp&quot; alt=&quot;blur3&quot; title=&quot;blur3&quot;&gt;&lt;/p&gt;
&lt;p&gt;查看代码点击&lt;a href=&quot;https://www.clloz.com/study/blur/blur3.html&quot;&gt;页面&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;绝对定位&lt;/h2&gt;
&lt;p&gt;不设置背景所在元素的大小，用绝对定位的定位属性来设置图片的大小也可以达到效果。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  html,
  body {
    height: 100%;
    width: 100%;
  }
  .cover {
    position: absolute;
    top: -8px;
    right: -8px;
    bottom: -8px;
    left: -8px;
    background: url(&apos;https://img.clloz.com/blog/writing/totoro.jpg&apos;);
    background-size: cover;
    filter: blur(5px) brightness(0.5);
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;cover&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看代码点击&lt;a href=&quot;https://www.clloz.com/study/blur/blur4.html&quot;&gt;页面&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;可能还有其他更好的方法，这几个方法都需要设置 &lt;code&gt;body&lt;/code&gt; 的 &lt;code&gt;overflow&lt;/code&gt; 为 &lt;code&gt;hideen&lt;/code&gt;，不过这也不算什么问题，需要滚动的话再包一层就可以了。如果你有更好的办法欢迎回复。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.zhihu.com/question/43602522/answer/113510488&quot; title=&quot;知乎回答：大漠&quot;&gt;知乎回答：大漠&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://volkerotto.net/2014/07/03/css-background-image-blur-without-blury-edges/&quot; title=&quot;CSS Background Image Blur without blurry edges&quot;&gt;CSS Background Image Blur without blurry edges&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/blur2.BV5Yqj4l.png"/><enclosure url="/_astro/blur2.BV5Yqj4l.png"/></item><item><title>函数节流和函数防抖</title><link>https://clloz.com/blog/throttle-debounce</link><guid isPermaLink="true">https://clloz.com/blog/throttle-debounce</guid><pubDate>Tue, 21 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文介绍函数节流 &lt;code&gt;throttle&lt;/code&gt; 和 函数防抖 &lt;code&gt;debounce&lt;/code&gt; 的实现方法。&lt;/p&gt;
&lt;h2&gt;函数节流 throttle&lt;/h2&gt;
&lt;p&gt;函数节流就是高频触发事件，指定时间内只执行一次。&lt;/p&gt;
&lt;p&gt;我们来想象一下需要函数节流的场景，比如典型的监听页面滚动触发回调函数，我们写一段测试代码，然后来看看多次触发的情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  div {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  .wrap {
    position: absolute;
    top: 50%;
    left: 50%;
    height: 200px;
    width: 200px;
    padding: 0 10px;
    transform: translate(-50%, -50%);
    border: 1px solid black;
    overflow: auto;
  }

  .content {
    width: 100%;
    height: 500px;
    background: lightblue;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div class=&quot;wrap&quot;&gt;
  &amp;#x3C;div class=&quot;content&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&amp;#x3C;script&gt;
  var wrap = document.querySelector(&apos;.wrap&apos;)
  var content = document.querySelector(&apos;.content&apos;)
  wrap.addEventListener(&apos;scroll&apos;, function () {
    console.log(&apos;dispatch&apos;)
  })
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们为 &lt;code&gt;wrap&lt;/code&gt; 绑定了一个滚动事件的监听，这种需求是经常用到的，比如我们需要根据用户的滚动显示不同的内容，但是很明显的是我们并不需要如此频繁地执行回调函数，我们需要让这个监听的回调函数执行的次数少一点，这时候就需要用到函数节流。&lt;/p&gt;
&lt;p&gt;浏览器的监控肯定是固定频率持续发生的，这是我们无法控制的，我们能做的就是在执行流进入到回调函数的时候进行判断，如果不符合条件我们就不执行，这就是函数节流的原理。如何设计这个逻辑呢，我们可以利用 &lt;code&gt;JavaScript&lt;/code&gt; 的定时器配合一个表示状态的锁来达到目的，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;wrap.addEventListener(
  &apos;scroll&apos;,
  throttle(function () {
    console.log(&apos;dispatch: &apos; + new Date().getTime())
  }, 300)
)

function throttle(fn, interval) {
  var executing = false
  return function () {
    if (executing) return
    executing = true
    setTimeout(() =&gt; {
      fn.apply(this, arguments)
      executing = false
    }, interval)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们的回调函数在 &lt;code&gt;executing&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 的时候会直接返回，而我们的功能代码也就是 &lt;code&gt;fn&lt;/code&gt; 每隔 &lt;code&gt;300ms&lt;/code&gt; 才会执行一次，效果如下:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是 &lt;code&gt;setTimeout&lt;/code&gt; 的回调函数中的 &lt;code&gt;this&lt;/code&gt; 默认指向 &lt;code&gt;window&lt;/code&gt;，因为 &lt;code&gt;setTimeout&lt;/code&gt; 是 &lt;code&gt;window&lt;/code&gt; 对象上的一个方法，调用 &lt;code&gt;setTimeout&lt;/code&gt; 实际上是 &lt;code&gt;window.setTimeout&lt;/code&gt;，而我们的 &lt;code&gt;fn&lt;/code&gt; 中的 &lt;code&gt;this&lt;/code&gt; 需要指向绑定事件的 &lt;code&gt;DOM&lt;/code&gt; 元素，所以需要 &lt;code&gt;bind&lt;/code&gt; ，箭头函数或者中间变量。这里我使用的箭头函数，用 &lt;code&gt;apply&lt;/code&gt; 来设置函数执行的 &lt;code&gt;this&lt;/code&gt; 和参数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;函数防抖 debounce&lt;/h2&gt;
&lt;p&gt;防抖 &lt;code&gt;debounce&lt;/code&gt; 的概念其实是从机械开关和继电器的“去弹跳”（&lt;code&gt;debounce&lt;/code&gt;）衍生出来的，基本思路就是把多个信号合并为一个信号。&lt;/p&gt;
&lt;p&gt;编程中的函数防抖就是在事件被触发 &lt;code&gt;n&lt;/code&gt; 秒后再执行回调，如果在这 &lt;code&gt;n&lt;/code&gt; 秒内又被触发，则重新计时，也就是把一些高频触发事件的多次触发信号合并成一个，达到 &lt;code&gt;debounce&lt;/code&gt; 的效果。主要针对的是回调只需要执行一次，但是事件却频繁触发，如果每次触发我们都执行回调，会造成性能的浪费。这在生活中也很常见，比如电梯门打开后默认 &lt;code&gt;3s&lt;/code&gt; 后关闭，如果在这段时间中有人又按了按钮，会从头开始计算 &lt;code&gt;3s&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里我们用一个最常见的例子来说明，用户修改输入框输入后向后台发请求，如果用户每次修改输入我们都发请求，那么是很浪费的。我们可以设置一个 &lt;code&gt;500ms&lt;/code&gt; 的延迟，&lt;code&gt;500ms&lt;/code&gt; 内用户又进行了输入则重新计时，如果 &lt;code&gt;500ms&lt;/code&gt; 内用户都没有输入则发送请求。&lt;/p&gt;
&lt;p&gt;主要的实现思路就是用 &lt;code&gt;setTimeout&lt;/code&gt; 和 &lt;code&gt;clearTimeout&lt;/code&gt;，我们将待执行函数用一个 &lt;code&gt;debounce&lt;/code&gt; 函数进行包装。当事件触发的时候我们首先进行 &lt;code&gt;clearTimeout&lt;/code&gt; 删除之前设置的定时器，然后重新用 &lt;code&gt;setTimeout&lt;/code&gt; 设置一个定时器。只有当事件在指定时间内没有触发了，&lt;code&gt;setTimeout&lt;/code&gt; 的回调函数才会执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;title&gt;debounce&amp;#x3C;/title&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;input id=&quot;inp&quot; type=&quot;text&quot; /&gt;
    &amp;#x3C;script&gt;
      function debounce(fn, interval) {
        let timeout = null
        return function () {
          clearTimeout(timeout)
          timeout = setTimeout(() =&gt; {
            fn.apply(this, arguments)
          }, interval)
        }
      }
      function sayHi() {
        console.log(&apos;debounce success!&apos;)
      }
      var inp = document.getElementById(&apos;inp&apos;)
      inp.addEventListener(&apos;input&apos;, debounce(sayHi, 500)) // 防抖
    &amp;#x3C;/script&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;函数节流和函数防抖主要是应用在一些高频触发事件上，当我们的事件不需要那么高的触发频率的时候，可以用节流或者防抖处理。&lt;/p&gt;
&lt;p&gt;想要查看文章中的示例请&lt;a href=&quot;https://www.clloz.com/study/throttle-debounce.html&quot;&gt;点击&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/5&quot; title=&quot;什么是防抖和节流？有什么区别？如何实现？&quot;&gt;什么是防抖和节流？有什么区别？如何实现？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/38313717&quot; title=&quot;函数防抖与函数节流&quot;&gt;函数防抖与函数节流&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>jQuery动画队列</title><link>https://clloz.com/blog/jquery-animation-queue</link><guid isPermaLink="true">https://clloz.com/blog/jquery-animation-queue</guid><pubDate>Fri, 17 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;队列是 &lt;code&gt;jQuery&lt;/code&gt; 内部的基础设施，&lt;code&gt;animate&lt;/code&gt; 动画依赖的基础设施,整个 &lt;code&gt;jQuery&lt;/code&gt; 中队列仅供给动画使用。&lt;/p&gt;
&lt;p&gt;那么 &lt;code&gt;jQuery&lt;/code&gt; 引入队列其实从一个角度上可以认为：允许一系列函数被异步地调用而不会阻塞程序。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;$(selector).slideUp().fadeIn()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 &lt;code&gt;jQuery&lt;/code&gt; 的一组动画链式序列，它的内部其实就是一组队列 &lt;code&gt;Queue&lt;/code&gt;， 当 &lt;code&gt;slideUp&lt;/code&gt; 运行时，&lt;code&gt;fadeIn&lt;/code&gt; 被放到 &lt;code&gt;fx&lt;/code&gt; 队列中，当 &lt;code&gt;slideUp&lt;/code&gt; 完成后，从队列中被取出运行。&lt;code&gt;queue&lt;/code&gt; 函数允许 直接操作这个链式调用的行为。同时，&lt;code&gt;queue&lt;/code&gt; 可以指定队列名称获得其他能力，而不局限于 &lt;code&gt;fx&lt;/code&gt; 队列。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;jQuery&lt;/code&gt; 提供了 &lt;code&gt;2&lt;/code&gt; 组队列操作的 &lt;code&gt;API&lt;/code&gt; ： - &lt;code&gt;jQuery.queue/dequeue&lt;/code&gt; - &lt;code&gt;jQuery.fn.queue/dequeue&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;队列方法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;$.queue&lt;/code&gt; : 显示或操作匹配的元素上已经执行的函数列队。&lt;/p&gt;
&lt;p&gt;这个方法有两个作用，它既是 &lt;code&gt;setter&lt;/code&gt;，又是 &lt;code&gt;getter&lt;/code&gt;。第一个参数 &lt;code&gt;elem&lt;/code&gt; 是 &lt;code&gt;DOM&lt;/code&gt; 元素，第二个参数 &lt;code&gt;type&lt;/code&gt; 是字符串，第三个参数 &lt;code&gt;data&lt;/code&gt; 可以是 &lt;code&gt;function&lt;/code&gt; 或数组。&lt;code&gt;type&lt;/code&gt; 默认是 &lt;code&gt;fx&lt;/code&gt;，也就是默认是给 &lt;code&gt;fx&lt;/code&gt;动画队列使用的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var body = $(&apos;body&apos;)
function cb1() {
  alert(1)
}
function cb2() {
  alert(2)
}

//set
$.queue(body, &apos;aa&apos;, cb1) // 第三个参数为function
$.queue(body, &apos;aa&apos;, cb2)

//get
$.queue(body, &apos;aa&apos;) //[function ,function]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Queue&lt;/code&gt; 源码，用 &lt;code&gt;jquery&lt;/code&gt; 内部的 &lt;code&gt;Data&lt;/code&gt; 对象进行缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;queue: function(elem, type, data) {
    var queue;
    if (elem) {
        type = (type || &quot;fx&quot;) + &quot;queue&quot;;
        queue = data_priv.get(elem, type);
        // Speed up dequeue by getting out quickly if this is just a lookup
        if (data) {
            if (!queue || jQuery.isArray(data)) {
                queue = data_priv.access(elem, type, jQuery.makeArray(data));
            } else {
                queue.push(data);
            }
        }
        return queue || [];
    }
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$.dequeue&lt;/code&gt; : 匹配的元素上执行队列中的下一个函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var body = $(&apos;body&apos;)
function cb1() {
  console.log(11)
}
function cb2() {
  console.log(22)
}

//set
$.queue(body, &apos;aa&apos;, cb1) // 第三个参数为function
$.queue(body, &apos;aa&apos;, cb2)

$.dequeue(body, &apos;aa&apos;) //11
$.dequeue(body, &apos;aa&apos;) //22
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将回调函数出列执行，每调用一次仅出列一个，因此当回调有 &lt;code&gt;N&lt;/code&gt; 个时，需要调用 &lt;code&gt;$.dequeue&lt;/code&gt; 方法 &lt;code&gt;N&lt;/code&gt; 次元素才全部出列。源码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var dequeue = jQuery.dequeue(elem, type),
  startLength = queue.length,
  fn = queue.shift(),
  hooks = jQuery._queueHooks(elem, type),
  next = function () {
    jQuery.dequeue(elem, type)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;动画队列&lt;/h2&gt;
&lt;p&gt;动画的链式调用就是用队列来实现的，当我们链式执行动画的时候，当前执行的动画在缓存中会变为 &lt;code&gt;inprogress&lt;/code&gt; 状态，只有等这个状态结束了，后面的动画才能执行。当第一个动画执行完毕后，那么必须有一个回调通知这个去把队列中下一个执行给取出来，然后要删掉这个 &lt;code&gt;inprogress&lt;/code&gt; 状态，依次循环。&lt;/p&gt;
&lt;p&gt;参考文章 1. &lt;a href=&quot;https://www.cnblogs.com/aaronjs/p/3813237.html&quot; title=&quot;动画队列&quot;&gt;动画队列&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>【翻译】JavaScript如何工作二：在V8引擎中的五个代码优化技巧</title><link>https://clloz.com/blog/how-javascript-works-2</link><guid isPermaLink="true">https://clloz.com/blog/how-javascript-works-2</guid><pubDate>Tue, 14 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;几个星期前我们开始了旨在深度挖掘 &lt;code&gt;JavaScript&lt;/code&gt; 以及它是如何工作的系列文章，我们相信通过了解 &lt;code&gt;JavaScript&lt;/code&gt; 的底层构建模块以及它们是如何工作的能够帮助我们写出更好的代码和应用。&lt;/p&gt;
&lt;p&gt;第一篇文章主要关注的是引擎，运行时 &lt;code&gt;runtime&lt;/code&gt; 和调用栈的概述。第二篇文章将深入剖析谷歌 &lt;code&gt;V8&lt;/code&gt; 引擎的内部，我们还提供了一些关于如何编写更好的 &lt;code&gt;JavaScript&lt;/code&gt; 代码的快速技巧 —— 我们 &lt;code&gt;SessionStack&lt;/code&gt; 开发团队在开发产品的时候遵循的最佳实践。&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;所谓 &lt;code&gt;JavaScript&lt;/code&gt; 引擎就是一个用来执行 &lt;code&gt;JavaScript&lt;/code&gt; 代码的程序或者解释器。一个 &lt;code&gt;JavaScript&lt;/code&gt; 引擎可以被实现成一个标准解释器或者将 &lt;code&gt;JavaScript&lt;/code&gt; 以某种形式编译成字节码的即使编译器。&lt;/p&gt;
&lt;p&gt;下面是一些流行的实现 &lt;code&gt;JavaScript&lt;/code&gt; 引擎的项目：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/V8_%28JavaScript_engine%29&quot; title=&quot;V8&quot;&gt;V8&lt;/a&gt; — 由 Google 开发，使用 C++ 编写的开源引擎&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Rhino_%28JavaScript_engine%29&quot; title=&quot;Rhino&quot;&gt;Rhino&lt;/a&gt; — 由 Mozilla 基金会管理，完全使用 Java 开发的开源引擎&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/SpiderMonkey_%28JavaScript_engine%29&quot; title=&quot;SpiderMonkey&quot;&gt;SpiderMonkey&lt;/a&gt; — 第一个 JavaScript 引擎，在当时支持了 Netscape Navigator，现在是 Firefox 的引擎&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/JavaScriptCore&quot; title=&quot;JavaScriptCore&quot;&gt;JavaScriptCore&lt;/a&gt; — 由苹果公司为 Safari 浏览器开发，并以 Nitro 的名字推广的开源引擎。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/KJS_%28KDE%29&quot; title=&quot;KJS &quot;&gt;KJS&lt;/a&gt; — KDE 的引擎，最初是由 Harri Porten 为 KDE 项目的 Konqueror 网络浏览器开发&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Chakra_%28JScript_engine%29&quot; title=&quot;Chakra&quot;&gt;Chakra&lt;/a&gt; (JScript9) — IE 引擎&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Chakra_%28JavaScript_engine%29&quot; title=&quot;Chakra&quot;&gt;Chakra&lt;/a&gt; (JavaScript) — 微软 Edge 的引擎&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Nashorn_%28JavaScript_engine%29&quot; title=&quot;Nashorn&quot;&gt;Nashorn&lt;/a&gt; — 开源引擎，由 Oracle 的 Java 语言工具组开发，是 OpenJDK 的一部分&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/JerryScript&quot; title=&quot;JerryScript&quot;&gt;JerryScript&lt;/a&gt; — 这是物联网的一个轻量级引擎&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么要创建 &lt;code&gt;V8&lt;/code&gt; 引擎&lt;/h2&gt;
&lt;p&gt;由谷歌创建的 &lt;code&gt;V8&lt;/code&gt; 引擎是一款用 &lt;code&gt;C++&lt;/code&gt; 开发的开源引擎，这款引擎被用在了谷歌的 &lt;code&gt;Chrome&lt;/code&gt; 浏览器中。但是不同于其他引擎的是，&lt;code&gt;V8&lt;/code&gt; 引擎同样被用到了非常流行的 &lt;code&gt;Node.js&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/v8.Do9Z6XCk_Z1OqBzH.webp&quot; alt=&quot;v8&quot; title=&quot;v8&quot;&gt;&lt;/p&gt;
&lt;p&gt;最穿创建 &lt;code&gt;V8&lt;/code&gt; 引擎是为了提高浏览器执行 &lt;code&gt;JavaScript&lt;/code&gt; 的性能。为了获得更快的速度，&lt;code&gt;V8&lt;/code&gt; 并没有使用解释器而是将 &lt;code&gt;JavaScript&lt;/code&gt; 代码编译成了更有效率的机器码。它就像 &lt;code&gt;SpiderMonkey&lt;/code&gt; 或者 &lt;code&gt;Rhino&lt;/code&gt; (&lt;code&gt;Mozilla&lt;/code&gt;) 等许多现代 &lt;code&gt;JavaScript&lt;/code&gt; 引擎一样，通过运用即时编译器（ &lt;code&gt;Just-In-Time Complier&lt;/code&gt; ）将 &lt;code&gt;JavaScript&lt;/code&gt; 代码编译为机器码。而这之中最主要的区别就是 &lt;code&gt;V8&lt;/code&gt; 不生成字节码或者任何中间代码。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;V8&lt;/code&gt; 曾经有两个编译器&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;V8&lt;/code&gt; 引擎的 &lt;code&gt;5.9&lt;/code&gt; 版本之前，有两个编译器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;full-codegen&lt;/code&gt;  —— 一个简单快速的编译器，可以生成简单但是相对比较慢的机器码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Crankshaft&lt;/code&gt; —— 一个更加复杂的（即时）优化编译器，可以产生高度优化的代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;V8&lt;/code&gt; 引擎内部也使用了多个线程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主线程完成你所期望的任务：编译并执行你的代码。&lt;/li&gt;
&lt;li&gt;有一个独立的线程用来编译，当主线程执行的时候，前者可以优化代码。&lt;/li&gt;
&lt;li&gt;分析器（ &lt;code&gt;profiler&lt;/code&gt; ）线程可以告诉运行时 &lt;code&gt;runtime&lt;/code&gt; 哪些方法会花费大量时间，以便 &lt;code&gt;Crankshaft&lt;/code&gt; 可以优化他们。&lt;/li&gt;
&lt;li&gt;其他的一些线程用来处理垃圾回收扫描&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当第一次执行 &lt;code&gt;JavaScript&lt;/code&gt; 代码的时候，&lt;code&gt;V8&lt;/code&gt; 利用 &lt;code&gt;full-codegen&lt;/code&gt; 直接将解析过的 &lt;code&gt;JavaScript&lt;/code&gt; 代码不经过任何转换地翻译成机器码。这使它可以非常快速地开始执行机器码。需要注意的是，&lt;code&gt;V8&lt;/code&gt; 不实用任何中间字节码表示，所以不需要解释器。&lt;/p&gt;
&lt;p&gt;当你的代码已经运行了一段时间，分析器线程已经收集到足够多的数据来告诉运行时 &lt;code&gt;runtime&lt;/code&gt; 哪些方法应该被优化。&lt;/p&gt;
&lt;p&gt;接下来，&lt;code&gt;Crankshaft&lt;/code&gt; 在另一个线程开始优化，它将 &lt;code&gt;JavaScript&lt;/code&gt; 抽象语法树转换成一个叫 &lt;code&gt;Hydrogen&lt;/code&gt; 的高级静态单元分配表示( &lt;code&gt;SSA&lt;/code&gt; )，并且尝试去优化这个 &lt;code&gt;Hydrogen&lt;/code&gt; 图。大多数优化都是在这个层级完成。&lt;/p&gt;
&lt;h2&gt;代码嵌入 &lt;code&gt;Inlining&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;优化的第一步是尽可能多地提前嵌入代码。代码嵌入就是把调用函数的地方（函数被调用的行）用调用函数的函数体替换的过程。这简单的一步会使接下来的优化更有用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/inlining.Brb7dsE7_ZNjUrN.webp&quot; alt=&quot;inlining&quot; title=&quot;inlining&quot;&gt;&lt;/p&gt;
&lt;h2&gt;隐藏类 &lt;code&gt;Hidden Classes&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 是基于原型的语言：没有类或者对象是通过克隆的方法创建的。同时 &lt;code&gt;JavaScript&lt;/code&gt; 也是一门动态的编程语言，这意味着为一个实例化的对象添加或删除属性是非常容易的。&lt;/p&gt;
&lt;p&gt;大多数 &lt;code&gt;JavaScript&lt;/code&gt; 解释器使用类似字典的结构 (基于&lt;a href=&quot;http://en.wikipedia.org/wiki/Hash_function&quot; title=&quot;散列函数&quot;&gt;散列函数&lt;/a&gt;) 去存储对象属性值在内存中的位置。这种结构使得在 &lt;code&gt;JavaScript&lt;/code&gt; 中检索一个属性值比在像 &lt;code&gt;Java&lt;/code&gt; 或者 &lt;code&gt;C#&lt;/code&gt; 这种非动态语言中计算量大得多。在 &lt;code&gt;Java&lt;/code&gt; 中, 编译之前所有的属性值以一种固定的对象布局确定下来了，并且在运行时不能动态的增加或者删除 (当然，&lt;code&gt;C#&lt;/code&gt; 也有 动态类型，但这是另外一个话题了)。因此，属性值 (或者说指向这些属性的指针) 能够以连续的 &lt;code&gt;buffer&lt;/code&gt; 存储在内存中，并且每个值之间有一个固定的偏移量。根据属性类型可以很容易地确定偏移量的长度，而在 &lt;code&gt;JavaScript&lt;/code&gt; 中这是不可能的，因为属性类型可以在运行时更改。&lt;/p&gt;
&lt;p&gt;因为用字典的方式在内存中查找对象属性值的方法效率非常低，所以 &lt;code&gt;V8&lt;/code&gt; 使用了一个不同的方法：隐藏类（ &lt;code&gt;Hidden classes&lt;/code&gt; ）。隐藏类的工作方式和 &lt;code&gt;Java&lt;/code&gt; 中的固定对象布局（类）类似，除了它是在运行时 &lt;code&gt;runtime&lt;/code&gt; 创建的。现在，让我们来看看他们实际的样子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Point(x, y) {
  this.x = x
  this.y = y
}
var p1 = new Point(1, 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一旦 &lt;code&gt;new Point(1, 2)&lt;/code&gt; 被调用， &lt;code&gt;V8&lt;/code&gt; 会创建一个隐藏类叫做 &lt;code&gt;C0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/hidden-class.T7DgyiOt_2mtPWo.webp&quot; alt=&quot;hidden-class&quot; title=&quot;hidden-class&quot;&gt;&lt;/p&gt;
&lt;p&gt;由于&lt;code&gt;Point&lt;/code&gt; 还没有被定义任何属性，所以 &lt;code&gt;C0&lt;/code&gt; 是空的。&lt;/p&gt;
&lt;p&gt;只要第一条语句被执行 &lt;code&gt;this.x = x&lt;/code&gt;（在 &lt;code&gt;Point&lt;/code&gt; 函数内），&lt;code&gt;V8&lt;/code&gt; 将会基于 &lt;code&gt;C0&lt;/code&gt; 创建第二个隐藏类叫做 &lt;code&gt;C1&lt;/code&gt;。&lt;code&gt;C1&lt;/code&gt; 描述了 &lt;code&gt;x&lt;/code&gt; 属性在内存中的位置（相对于对象指针）。在这个例子中，&lt;code&gt;x&lt;/code&gt;被保存在&lt;a href=&quot;http://en.wikipedia.org/wiki/Offset_%28computer_science%29&quot; title=&quot;偏移量&quot;&gt;偏移量&lt;/a&gt;为 &lt;code&gt;0&lt;/code&gt; 的位置。这意味着当把一个 &lt;code&gt;point&lt;/code&gt; 对象看作内存中的连续 &lt;code&gt;buffer&lt;/code&gt; 时，第一个偏移量对应的就是 &lt;code&gt;x&lt;/code&gt; 属性。&lt;code&gt;V8&lt;/code&gt; 也会使用类转换来更新 &lt;code&gt;C0&lt;/code&gt;，当 &lt;code&gt;x&lt;/code&gt; 属性被添加到一个 &lt;code&gt;point&lt;/code&gt; 对象中的时候，隐藏类就从 &lt;code&gt;C0&lt;/code&gt; 切换到 &lt;code&gt;c1&lt;/code&gt;，此时 &lt;code&gt;point&lt;/code&gt; 对象的隐藏类就是 &lt;code&gt;c1&lt;/code&gt; 了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/hidden-class2.DnXt-3ZG_1s4cC3.webp&quot; alt=&quot;hidden-class2&quot; title=&quot;hidden-class2&quot;&gt;&lt;/p&gt;
&lt;p&gt;每当一个新属性添加到对象，老的隐藏类就会通过一个转换路径更新成一个新的隐藏类。隐藏类转换非常重要，因为它们允许以相同方法创建的对象共享隐藏类。如果两个对象共享一个隐藏类，并给它们添加相同的属性，隐藏类转换能够确保这两个对象都获得新的隐藏类以及与之相关联的优化代码。&lt;/p&gt;
&lt;p&gt;当执行语句 &lt;code&gt;this.y = y&lt;/code&gt; (同样，在 &lt;code&gt;Point&lt;/code&gt; 函数内部，&lt;code&gt;this.x = x&lt;/code&gt; 语句之后) 时，将重复此过程。一个新的隐藏类 &lt;code&gt;C2&lt;/code&gt; 被创建了，如果属性 &lt;code&gt;y&lt;/code&gt; 被添加到 &lt;code&gt;Point&lt;/code&gt; 对象(已经包含了 &lt;code&gt;x&lt;/code&gt; 属性)，同样的过程，类型转换被添加到 &lt;code&gt;C1&lt;/code&gt; 上，然后隐藏类开始更新成 &lt;code&gt;C2&lt;/code&gt;，并且 &lt;code&gt;Point&lt;/code&gt; 对象的隐藏类就要更新成 &lt;code&gt;C2&lt;/code&gt; 了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/hidden-class3.B9csqFnv_ZE4oii.webp&quot; alt=&quot;hidden-class-3&quot; title=&quot;hidden-class-3&quot;&gt;&lt;/p&gt;
&lt;p&gt;隐藏类的转换是根据属性添加到对象上的顺序来进行的，看看下面这段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function Point(x, y) {
  this.x = x
  this.y = y
}
var p1 = new Point(1, 2)
p1.a = 5
p1.b = 6
var p2 = new Point(3, 4)
p2.b = 7
p2.a = 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在你可能认为 &lt;code&gt;p1&lt;/code&gt; 和 &lt;code&gt;p2&lt;/code&gt; 使用相同的隐藏类和转换。事实上并不是，对于 &lt;code&gt;p1&lt;/code&gt; 来说首先是 &lt;code&gt;a&lt;/code&gt; 属性被添加然后是 &lt;code&gt;b&lt;/code&gt; 属性。对于 &lt;code&gt;p2&lt;/code&gt; 来说，首先分配 &lt;code&gt;b&lt;/code&gt;， 然后才是 &lt;code&gt;a&lt;/code&gt;。因此，&lt;code&gt;p1&lt;/code&gt; 和 &lt;code&gt;p2&lt;/code&gt; 会以不同的隐藏类和转化路径来执行。从这个例子中我们可以看出，以相同的顺序来初始化动态属性是更好的，因为这样隐藏类可以复用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里关于隐藏类有很多疑问，比如如果两个对象初始化顺序相同，但是数据类型不同，比如上面的 &lt;code&gt;a&lt;/code&gt;，&lt;code&gt;b&lt;/code&gt; 添加的顺序相同但是数据类型不同，比如一个是 &lt;code&gt;Number&lt;/code&gt; 一个是 &lt;code&gt;String&lt;/code&gt;，那么他们还能共享一个隐藏类吗，如果不能那么隐藏类的优势到底是什么呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;内联缓存 &lt;code&gt;inline caching&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;V8&lt;/code&gt; 还利用了另一种叫做内联缓存的技术来优化动态语言。内联缓存依赖于一个现象：同一个方法的重复调用是发生在相同类型的对象上的。关于内联缓存更深层次的解读请看&lt;a href=&quot;https://github.com/sq/JSIL/wiki/Optimizing-dynamic-JavaScript-with-inline-caches&quot; title=&quot;这里&quot;&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;我们来大致了解一下内联缓存的基本概念 (如果你没有时间去阅读上面的深层次的解读)。&lt;/p&gt;
&lt;p&gt;内联缓存是如何工作的呢？&lt;code&gt;V8&lt;/code&gt; 维护了一个对象类型的缓存，存储的是在最近的方法调用中作为参数传递的对象类型，然后 &lt;code&gt;V8&lt;/code&gt; 会使用这些信息去预测将来什么类型的对象会再次作为参数进行传递。如果 &lt;code&gt;V8&lt;/code&gt; 对传递给方法的对象的类型做出了很好的预测，那么它就能够绕开获取对象属性的计算过程，取而代之的是使用先前查找这个对象的隐藏类时所存储的信息。&lt;/p&gt;
&lt;p&gt;那么隐藏类和内联缓存的概念是如何联系到一起的呢？当一个对象的方法被调用的时候，&lt;code&gt;V8&lt;/code&gt; 引擎会查看这个对象的隐藏类以便获取访问这个对象虽对应的偏移量。当用同一个隐藏类成功调用了两次某个方法，引擎就会跳过查找隐藏类这个步骤，而把偏移量当作属性添加到对象指针里面。以后每次调用这个方法，&lt;code&gt;V8&lt;/code&gt; 引擎都会假定对象的隐藏类没有变化，直接用对象属性上的偏移量到内存中获取特定的属性。这将大幅提高代码执行的速度。&lt;/p&gt;
&lt;p&gt;内联缓存也是隐藏类如此重要的原因，如果我们使用不同的隐藏类创建了两个同类型的对象(就如同我们前面做的那样)，V8 就不能使用内联缓存，因为即使两个对象是相同的，但是它们对应的隐藏类对它们的属性分配了不同的偏移值。&lt;/p&gt;
&lt;h2&gt;编译成机器代码&lt;/h2&gt;
&lt;p&gt;一旦 &lt;code&gt;Hydrogen&lt;/code&gt; 图被优化，&lt;code&gt;Crankshaft&lt;/code&gt; 就会把这个图降低到一个比较低层次的表现形式 —— 叫做 &lt;code&gt;Lithium&lt;/code&gt;。大多数 &lt;code&gt;Lithium&lt;/code&gt; 实现都是面向特定的结构的。寄存器分配就发生在这一层次。   最后，&lt;code&gt;Lithium&lt;/code&gt; 被编译成机器码。然后，&lt;code&gt;OSR&lt;/code&gt; 就开始了：一种运行时替换正在运行的栈帧的技术( &lt;code&gt;on-stack replacement&lt;/code&gt; )。在我们开始编译和优化一个明显耗时的方法时，我们可能会运行它。&lt;code&gt;V8&lt;/code&gt; 不会把它之前运行的慢的代码抛在一旁，然后再去执行优化后的代码。相反，&lt;code&gt;V8&lt;/code&gt; 会转换这些代码的上下文(栈， 寄存器)，以便在执行这些慢代码的途中转换到优化后的版本。这是一个非常复杂的任务，要知道 &lt;code&gt;V8&lt;/code&gt; 已经在其他的优化中将代码嵌入了。当然了，V8 不是唯一能做到这一点的引擎。   &lt;code&gt;V8&lt;/code&gt; 还有一种保护措施叫做反优化，能够做相反的转换，将代码逆转成没有优化过的代码以防止引擎做的猜测不再正确。&lt;/p&gt;
&lt;h2&gt;垃圾回收&lt;/h2&gt;
&lt;p&gt; 对于垃圾回收，&lt;code&gt;V8&lt;/code&gt; 使用一种传统的分代式标记清除的方式去清除老的数据。标记阶段会阻止 &lt;code&gt;JavaScript&lt;/code&gt; 的运行。为了控制垃圾回收的成本，并且使 &lt;code&gt;JavaScript&lt;/code&gt; 的执行更加稳定，&lt;code&gt;V8&lt;/code&gt; 使用增量标记：与遍历整个堆去标记每一个可能的对象的不同，取而代之的是它只遍历部分堆，然后就恢复正常执行。下一次垃圾回收就会从上一次遍历停下来的地方开始，这就使得每一次正常执行之间的停顿都非常短。就像前面说的，清理的操作是由独立的线程的进行的。&lt;/p&gt;
&lt;h2&gt;如何写出优化的 &lt;code&gt;JavaScript&lt;/code&gt; 代码&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;对象属性的顺序: 在实例化你的对象属性的时候一定要使用相同的顺序，这样隐藏类和随后的优化代码才能共享。&lt;/li&gt;
&lt;li&gt;动态属性: 在对象实例化之后再添加属性会强制使得隐藏类变化，并且会减慢为旧隐藏类所优化的代码的执行。所以，要在对象的构造函数中完成所有属性的分配。&lt;/li&gt;
&lt;li&gt;方法: 重复执行相同的方法会运行的比不同的方法只执行一次要快 (因为内联缓存)。&lt;/li&gt;
&lt;li&gt;数组: 避免使用 &lt;code&gt;keys&lt;/code&gt; 不是递增的数字的稀疏数组，这种 &lt;code&gt;key&lt;/code&gt; 值不是递增数字的稀疏数组其实是一个 &lt;code&gt;hash&lt;/code&gt; 表。在这种数组中每一个元素的获取都是昂贵的代价。同时，要避免提前申请大数组。最好的做法是随着你的需要慢慢的增大数组。最后，不要删除数组中的元素，因为这会使得 &lt;code&gt;keys&lt;/code&gt; 变得稀疏。&lt;/li&gt;
&lt;li&gt;标记值 (&lt;code&gt;Tagged values&lt;/code&gt;): &lt;code&gt;V8&lt;/code&gt; 用 &lt;code&gt;32&lt;/code&gt; 位来表示对象的地址和数字。它使用最后一位一位来区分它是对象 ( &lt;code&gt;flag = 1&lt;/code&gt; ) 还是一个整型 ( &lt;code&gt;flag = 0&lt;/code&gt; )，也被叫做小整型( &lt;code&gt;SMI&lt;/code&gt; )，因为它只有 &lt;code&gt;31&lt;/code&gt; 位。然后，如果一个数值大于 &lt;code&gt;31&lt;/code&gt; 位，&lt;code&gt;V8&lt;/code&gt; 将会对其进行 &lt;code&gt;box&lt;/code&gt; 操作，然后将其转换成 &lt;code&gt;double&lt;/code&gt; 型，并且创建一个新的对象来装这个数。所以，为了避免代价很高的 &lt;code&gt;box&lt;/code&gt; 操作，尽量使用 &lt;code&gt;31&lt;/code&gt; 位的有符号数。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>【翻译】JavaScript如何工作一：引擎，运行时和调用栈概述</title><link>https://clloz.com/blog/how-javascript-works-1</link><guid isPermaLink="true">https://clloz.com/blog/how-javascript-works-1</guid><pubDate>Sun, 12 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;这是一篇翻译，在查 &lt;code&gt;setInterval&lt;/code&gt; 的浏览器如何处理的过程中看到这一系列文章，感觉对自己理解 &lt;code&gt;JS引擎&lt;/code&gt; 以及运行时 &lt;code&gt;runtime&lt;/code&gt; 的工作细节有很大的帮助，决定翻译一下这一系列文章。英文水平非常烂，只能作为自己的一项练习了，虽然之前也写了浏览器渲染过程和 &lt;code&gt;JS&lt;/code&gt; 引擎浅析，但是对很多细节理解的还不够，翻译这个系列文章应该能让我的理解更透彻和全面。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原文地址&lt;a href=&quot;https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf&quot; title=&quot;How JavaScript works: an overview of the engine, the runtime, and the call stack&quot;&gt;How JavaScript works: an overview of the engine, the runtime, and the call stack&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;随着 &lt;code&gt;JavaScript&lt;/code&gt; 变得越来越流行，很多团队开始把它运用到不同的层级的技术栈中，前端，后端，混合式应用，嵌入式系统等。&lt;/p&gt;
&lt;p&gt;这篇文章是系列中的第一篇，旨在深度挖掘 &lt;code&gt;JavaScript&lt;/code&gt; 和它的工作原理：我们认为通过了解 &lt;code&gt;JavaScript&lt;/code&gt; 的构建模块和它们如何协同工作能够帮助你写出更好的代码和应用。我们也将分享一些我们在开发 &lt;code&gt;SessionStack&lt;/code&gt; 中的经验法则，为了保持竞争力而开发的健壮的高性能的轻量 &lt;code&gt;JavaScript&lt;/code&gt; 应用。&lt;/p&gt;
&lt;p&gt;如 &lt;a href=&quot;http://githut.info/&quot;&gt;Githut Stats&lt;/a&gt; 所示，&lt;code&gt;JavaScript&lt;/code&gt; 拥有 &lt;code&gt;GitHub&lt;/code&gt; 上最高的活跃仓库数和最高的总提交数，在其他方面也没有落后太多。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/js-githut.DAxr0MJ8_ahlS3.webp&quot; alt=&quot;githut&quot; title=&quot;githut&quot;&gt;&lt;/p&gt;
&lt;p&gt;越来越多的项目开始依赖 &lt;code&gt;JavaScript&lt;/code&gt;，也就意味着想要开发出惊艳的软件就必须深入理解这门语言和 &lt;code&gt;JavaScript&lt;/code&gt; 生态圈提供的一切。&lt;/p&gt;
&lt;p&gt;事实证明，虽然有大量的开发者每天用 &lt;code&gt;JavaScript&lt;/code&gt; 作为工作但是并不理解内部的工作原理。&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;几乎所有人都听说过 &lt;code&gt;V8&lt;/code&gt; 引擎的概念，并且大部分人都知道 &lt;code&gt;JavaScript&lt;/code&gt; 是单线程的以及它会用到一个回调队列。&lt;/p&gt;
&lt;p&gt;在这篇文章中，我们将详细介绍这些概念并且说明 &lt;code&gt;JavaScript&lt;/code&gt; 内部的运行机制。通过了解这些细节，你讲能够正确地运用提供的 &lt;code&gt;API&lt;/code&gt; 写出更好的非阻塞的应用。&lt;/p&gt;
&lt;p&gt;如果你是一个 &lt;code&gt;JavaScript&lt;/code&gt; 新手，这篇文章能够帮助你理解 &lt;code&gt;JavaScript&lt;/code&gt; 相比于其他编程语言的“奇怪”行为。&lt;/p&gt;
&lt;p&gt;如果你是个有经验的 &lt;code&gt;JavaScript&lt;/code&gt; 程序员，相信我，这篇文章会让你对于 &lt;code&gt;JavaScript&lt;/code&gt; 运行时 &lt;code&gt;runtime&lt;/code&gt; 如何工作有全新的理解。&lt;/p&gt;
&lt;h2&gt;JavaScript 引擎&lt;/h2&gt;
&lt;p&gt;最流行的 &lt;code&gt;JavaScript&lt;/code&gt; 引擎是谷歌的 &lt;code&gt;V8&lt;/code&gt; 引擎，他被用在 &lt;code&gt;Chrome&lt;/code&gt; 和 &lt;code&gt;Node.js&lt;/code&gt; 中，他的样子如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/js-engine.DgfZP465_HKJlH.webp&quot; alt=&quot;js-engine&quot; title=&quot;js-engine&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个引擎由两个主要部分组成： 1. 内存堆：用来分配内存。 2. 调用栈：代码执行栈帧所在位置。&lt;/p&gt;
&lt;h2&gt;运行时 runtime&lt;/h2&gt;
&lt;p&gt;浏览器提供了许多 &lt;code&gt;API&lt;/code&gt; 供开发者使用（比如 &lt;code&gt;setTimeout&lt;/code&gt; ），这些 &lt;code&gt;API&lt;/code&gt; 并不是由引擎所提供。那么这些 &lt;code&gt;API&lt;/code&gt; 到底是哪来的呢？想要说清楚这个有点复杂。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/js-engine-detail.Cl5EARWe_Z15t39N.webp&quot; alt=&quot;js-engine-detail&quot; title=&quot;js-engine-detail&quot;&gt;&lt;/p&gt;
&lt;p&gt;除了引擎我们还有很多其他东西。浏览器提供了很多叫做 &lt;code&gt;Web APIs&lt;/code&gt; 的内容，比如 &lt;code&gt;DOM&lt;/code&gt;，&lt;code&gt;AJAX&lt;/code&gt;，&lt;code&gt;setTimeout&lt;/code&gt; 等。&lt;/p&gt;
&lt;p&gt;之后，我们还有非常受欢迎的事件循环（&lt;code&gt;Event Loop&lt;/code&gt;）和回调队列 （&lt;code&gt;Callback Queue&lt;/code&gt;）。&lt;/p&gt;
&lt;h2&gt;调用栈 The Callback Queue&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 是一个单线程语言，这意味着它只有一个调用栈，同一时间只能做一件事。&lt;/p&gt;
&lt;p&gt;调用栈是一种数据结构，它记录了我们在程序中的实际位置。当执行流进入一个函数，我们将这个函数压入调用栈顶，当这个函数执行完毕返回，我们将它从栈顶弹出。这就是调用栈所做的事情。&lt;/p&gt;
&lt;p&gt;我们来看一个例子。看看下面这段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function multiply(x, y) {
  return x * y
}
function printSquare(x) {
  var s = multiply(x, x)
  console.log(s)
}
printSquare(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当引擎开始执行这段代码的时候，调用栈是空的，然后执行步骤如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/call-stack-step.WCUULHVD_Z1ILK8m.webp&quot; alt=&quot;call-stack-step&quot; title=&quot;call-stack-step&quot;&gt;&lt;/p&gt;
&lt;p&gt;每一进栈的函数都称为一个栈帧（ &lt;code&gt;Stack Frame&lt;/code&gt; ）。&lt;/p&gt;
&lt;p&gt;当一个异常被抛出的时候，栈轨迹（ &lt;code&gt;Stack Traces&lt;/code&gt; ）被创建，从本质上来说这是调用栈的状态。看下面这段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function foo() {
  throw new Error(&apos;SessionStack will help you resolve crashes :)&apos;)
}
function bar() {
  foo()
}
function start() {
  bar()
}
start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这段代码在 &lt;code&gt;Chrome&lt;/code&gt; 中运行（假定代码在一个叫做 &lt;code&gt;foo.js&lt;/code&gt; 的文件中），将产生下面的栈轨迹。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/stack-traces.RGa0wJ9w_ZgASqe.webp&quot; alt=&quot;stack-traces&quot; title=&quot;stack-traces&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Blowing the stack&lt;/code&gt; ——当你到达栈容量的上限就会发生。这非常容易发生，特别是当你使用递归而没有详细测试你的代码。看看下面这个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function foo() {
  foo()
}
foo()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当引擎开始执行这段代码的时候，它先调用函数 &lt;code&gt;foo&lt;/code&gt; ，但是这个函数会无限的调用它自身，所以每执行一次，同样的函数就会被添加到调用栈，一直添加到触发 &lt;code&gt;Blowing the stack&lt;/code&gt;。具体情况大概如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/over-stack.Dl-t3h1h_PMJmG.webp&quot; alt=&quot;over-stack&quot; title=&quot;over-stack&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是，当调用栈中的函数调用的数量超过调用栈的容量的时候，浏览器会抛出一个错误，像下图这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/blow-stack.CniEObKf_enNz9.webp&quot; alt=&quot;blow-stack&quot; title=&quot;blow-stack&quot;&gt;&lt;/p&gt;
&lt;p&gt;在单个线程下运行代码是一件很容易的事情，因为你不需要处理多线程环境下的复杂情况，比如死锁。&lt;/p&gt;
&lt;p&gt;但是在单线程下运行也相当有局限，因为 &lt;code&gt;JavaScript&lt;/code&gt; 只有一个调用栈，如果有一个任务执行的非常慢该怎么办？&lt;/p&gt;
&lt;h2&gt;并行和事件队列&lt;/h2&gt;
&lt;p&gt;如果在调用栈中有函数调用需要花费大量的时间会发生什么呢？举个例子，当你想用 &lt;code&gt;JavaScript&lt;/code&gt; 在浏览器上实现复杂的图片变形。&lt;/p&gt;
&lt;p&gt;你可能会问：这算是一个问题码？真正的问题是当调用栈的函数在执行的时候，浏览器什么也做不了——它被锁死了。这意味着浏览器无法渲染，也无法执行其他代码，它卡住了。如果你想让你的应用拥有流畅的 &lt;code&gt;UI&lt;/code&gt; 这将是一个大问题。&lt;/p&gt;
&lt;p&gt;而且这不是唯一的问题。如果你的浏览器开始处理非常多的调用栈任务，浏览器将开始陷入长时间的未响应状态。大部分的浏览器会采取抛出错误的解决办法，询问你是否要终止这个页面。这将会毁了你产品的用户体验。&lt;/p&gt;
&lt;p&gt;那么我们如何执行复杂的代码但是不会令 &lt;code&gt;UI&lt;/code&gt; 卡死，浏览器未响应呢？解决办法是异步回调。&lt;/p&gt;
&lt;p&gt;关于异步回调的内容会在下一片文章详细介绍。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>HTML attribute 和 DOM property 的区别</title><link>https://clloz.com/blog/html-attribute-dom-property</link><guid isPermaLink="true">https://clloz.com/blog/html-attribute-dom-property</guid><pubDate>Sat, 11 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;jQuery&lt;/code&gt; 对象有两个方法 &lt;code&gt;$(selector).prop()&lt;/code&gt; 和 &lt;code&gt;$(selector).attr()&lt;/code&gt;，这两个方法分别对应 &lt;code&gt;DOM&lt;/code&gt; 对象的 &lt;code&gt;property&lt;/code&gt; 属性和 &lt;code&gt;HTML&lt;/code&gt; 标签的 &lt;code&gt;attribute&lt;/code&gt;。那么 &lt;code&gt;property&lt;/code&gt; 和 &lt;code&gt;attribute&lt;/code&gt; 之间有什么区别呢，一般为了区分我们把 &lt;code&gt;property&lt;/code&gt; 翻译为属性，而把 &lt;code&gt;attribute&lt;/code&gt; 翻译为特性。在 &lt;code&gt;JS&lt;/code&gt; 中这两者还是存在一定的差异的。&lt;/p&gt;
&lt;h2&gt;语意&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;property&lt;/code&gt; 我们可以理解为事物本身的属性，与身俱来的，比如我们的头发是蛋白质构成的，那么蛋白质可以称为一个属性，这不可改变，改变了头发就不是头发了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;attribute&lt;/code&gt; 可以认为是附加在物体上的一些特性，比如我们可以改变头发的物理形状，烫卷拉直，或者改变头发的颜色，这样头发还是头发，但是外观变了。&lt;/p&gt;
&lt;p&gt;在前端领域，我们可以认为 &lt;code&gt;property&lt;/code&gt; 是一个 &lt;code&gt;DOM&lt;/code&gt; 对象创建的时候就会初始化在对象中的一些属性（我们也可以自定义属性），而 &lt;code&gt;attribute&lt;/code&gt; 是附在 &lt;code&gt;HTML&lt;/code&gt; 文档中的某个元素上的特性，我们可以改变删除自定义一个特性，但是对 &lt;code&gt;property&lt;/code&gt; 不可以，对于 &lt;code&gt;DOM&lt;/code&gt; 内置的属性我们无法删除，也只能按照规定类型赋值，并且内置属性会在每次 &lt;code&gt;DOM&lt;/code&gt; 对象初始化的时候产生，比如 &lt;code&gt;name&lt;/code&gt; 属性，无论我们设置什么类型的值，最后返回的都是字符类型。但是对于我们自定义的 &lt;code&gt;DOM&lt;/code&gt; 属性，则可以是任何 &lt;code&gt;JS&lt;/code&gt; 支持的数据类型，而 &lt;code&gt;attribute&lt;/code&gt; 的数据类型只能是字符串。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;node.getAttribute(attr)&lt;/code&gt; 中的参数 &lt;code&gt;attr&lt;/code&gt; 是大小写不敏感的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;两者之间的关系&lt;/h2&gt;
&lt;p&gt;对于非自定义的 &lt;code&gt;attribute&lt;/code&gt; 特性，它和 &lt;code&gt;property&lt;/code&gt; 之间有一一对应的关系，比如：&lt;code&gt;id&lt;/code&gt;，&lt;code&gt;class&lt;/code&gt;，&lt;code&gt;title等&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div id=&quot;test&quot; class=&quot;button&quot; foo=&quot;1&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;script&gt;
  document.getElementById(&apos;test&apos;).id // return string: &quot;test&quot;
  document.getElementById(&apos;test&apos;).className // return string: &quot;button&quot;
  document.getElementById(&apos;test&apos;).foo // return undefined 因为foo是一个自定义的attr特性
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;当我们通过 &lt;code&gt;property&lt;/code&gt; 属性进行设置或获取 &lt;code&gt;class&lt;/code&gt; 时，我们需要使用 &lt;code&gt;className&lt;/code&gt; ，因为在 &lt;code&gt;js&lt;/code&gt; 中 &lt;code&gt;class&lt;/code&gt; 是关键字。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;DOM&lt;/code&gt; 对象内置的 &lt;code&gt;property&lt;/code&gt; 改变的时候通常对应的 &lt;code&gt;attribute&lt;/code&gt; 也会改变。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div id=&quot;test&quot; class=&quot;button&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;script&gt;
  var div = document.getElementById(&apos;test&apos;)
  div.className = &apos;red-input&apos;
  div.getAttribute(&apos;class&apos;) // return string: &quot;red-input&quot;
  div.setAttribute(&apos;class&apos;, &apos;green-input&apos;)
  div.className // return string: &quot;green-input&quot;
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一些特殊情况&lt;/h2&gt;
&lt;h2&gt;href&lt;/h2&gt;
&lt;p&gt;当我们在 &lt;code&gt;HTML&lt;/code&gt; 中设置元素的 &lt;code&gt;href&lt;/code&gt; 属性的时候，&lt;code&gt;getAttribute()&lt;/code&gt; 方法返回的是我们设置的字符串，不管这是一个相对路径还是绝对路径，而 &lt;code&gt;node.href&lt;/code&gt; 属性则是获得的能够访问的绝对路径。&lt;/p&gt;
&lt;h2&gt;input 的 value&lt;/h2&gt;
&lt;p&gt;当我们在 &lt;code&gt;HTML&lt;/code&gt; 中设置 &lt;code&gt;input&lt;/code&gt; 的 &lt;code&gt;value&lt;/code&gt; 的时候，这个 &lt;code&gt;value&lt;/code&gt; 只是给了 &lt;code&gt;input&lt;/code&gt; 一个初始化的值，&lt;code&gt;property&lt;/code&gt; 也被这个值初始化，但当我们改变 &lt;code&gt;property&lt;/code&gt; 的时候，元素内的文本会被改变，但是 &lt;code&gt;attribute&lt;/code&gt; 的 &lt;code&gt;value&lt;/code&gt; 是不变的，它相当于保存这这个元素的初始化状态。&lt;/p&gt;
&lt;h2&gt;布尔型属性&lt;/h2&gt;
&lt;p&gt;因为 &lt;code&gt;attribute&lt;/code&gt; 的值只能是字符串，当我们设置的 &lt;code&gt;attribute&lt;/code&gt; 默认是一个布尔型的值的时候，不论我们是否设置值以及设置什么值，他的初始化值都是 &lt;code&gt;true&lt;/code&gt;，比如 &lt;code&gt;disabled&lt;/code&gt; 和 &lt;code&gt;checked&lt;/code&gt;。并且我们只能通过 &lt;code&gt;property&lt;/code&gt; 来修改他们。&lt;/p&gt;
&lt;h2&gt;如何使用&lt;/h2&gt;
&lt;p&gt;对于非自定义 &lt;code&gt;attribute&lt;/code&gt; 使用 &lt;code&gt;property&lt;/code&gt; 是一种比较安全并且有效率的做法，虽然对于 &lt;code&gt;id&lt;/code&gt;， &lt;code&gt;class&lt;/code&gt; 等会跟随 &lt;code&gt;property&lt;/code&gt; 一起变化的属性我们也可以用 &lt;code&gt;getAttribute()&lt;/code&gt; 或者 &lt;code&gt;setAttribute()&lt;/code&gt; ，不过显然 &lt;code&gt;property&lt;/code&gt; 是更好的选择。对于自定义属性，我们只能选择用 &lt;code&gt;attribtue&lt;/code&gt; 的方法。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000008781121&quot; title=&quot;[译]HTML attribute与DOM property之间的区别？&quot;&gt;[译]HTML attribute与DOM property之间的区别？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/elcarim5efil/p/4698980.html&quot; title=&quot;DOM 中 Property 和 Attribute 的区别&quot;&gt;DOM 中 Property 和 Attribute 的区别&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>前端数据Mock</title><link>https://clloz.com/blog/data-mock</link><guid isPermaLink="true">https://clloz.com/blog/data-mock</guid><pubDate>Fri, 10 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;对于前后端分离的开发，两边的开发进度不同是常有的事情，对于已经开发的功能，如何快速有效地模拟接口的请求是提高开发效率的关键，下面来讲讲几种数据 &lt;code&gt;mock&lt;/code&gt; 的方法。&lt;/p&gt;
&lt;h2&gt;http-server&lt;/h2&gt;
&lt;p&gt;如果我们只是测试一段 &lt;code&gt;JS&lt;/code&gt; 代码在对应数据下是否能够跑通，那么我们可以直接使用 &lt;code&gt;nodejs&lt;/code&gt; 提供的静态服务器 &lt;code&gt;http-server&lt;/code&gt;，将我们需要的数据保存到一个 &lt;code&gt;json&lt;/code&gt; 文件中，然后通过请求获取。需要注意的是我们访问请求发起的页面的时候必须也通过静态服务器访问，否则会因为同源策略禁止访问对应文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 前端 代码 index.html --&gt;
&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &amp;#x3C;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot; /&gt;
    &amp;#x3C;title&gt;test&amp;#x3C;/title&gt;
    &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;test.css&quot; /&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    this is a test page.
    &amp;#x3C;script&gt;
      var xhr = new XMLHttpRequest()
      xhr.open(&apos;GET&apos;, &apos;./test.json&apos;, true)
      xhr.onload = function () {
        console.log(xhr.responseText)
      }
      xhr.send()
    &amp;#x3C;/script&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话，我们只要在 &lt;code&gt;index.html&lt;/code&gt; 的同级目录下创建对应的数据 &lt;code&gt;json&lt;/code&gt; 文件，就可以模拟 &lt;code&gt;GET&lt;/code&gt; 请求了。当我们访问 &lt;code&gt;localhost:8080/index.html&lt;/code&gt; 的时候，对应文件夹的静态资源我们都能够请求到。&lt;/p&gt;
&lt;h2&gt;Mock.js&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mock.js&lt;/code&gt; 是一个用于拦截前端 &lt;code&gt;ajax&lt;/code&gt; 请求并生成随机数据响应的工具，可以用来模拟服务器响应。使用非常简单方便，不需要更改任何代码就可以直接拦截 &lt;code&gt;ajax&lt;/code&gt; 请求并返回随机数据，支持多种数据类型，也支持正则表达式。&lt;/p&gt;
&lt;p&gt;使用方法：&lt;/p&gt;
&lt;h2&gt;npm安装&lt;/h2&gt;
&lt;p&gt;安装：&lt;code&gt;npm install mockjs&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 使用 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))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;bower&lt;/h2&gt;
&lt;p&gt;安装：&lt;code&gt;npm install -g bower bower install --save mockjs&lt;/code&gt; 使用：&lt;code&gt;&amp;#x3C;script type=&quot;text/javascript&quot; src=&quot;./bower_components/mockjs/dist/mock.js&quot;&gt;&amp;#x3C;/script&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;或者直接在页面引入 &lt;code&gt;&amp;#x3C;script src=&quot;http://mockjs.com/dist/mock.js&quot;&gt;&amp;#x3C;/script&gt;&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;mock.js&lt;/code&gt; 还可以用可视化工具 &lt;code&gt;ease-mock&lt;/code&gt;，在线就可以添加接口和数据，使用也很方便&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;搭建后台&lt;/h2&gt;
&lt;p&gt;最好的 &lt;code&gt;mock&lt;/code&gt; 数据的方法其实还是自己用 &lt;code&gt;nodejs&lt;/code&gt; 写一个简单的后端来通信，这样可以自己制定各种数据，各种请求，实现起来也不是非常麻烦，接收请求，做好路由，返回数据就可以了，对于需要跨域的请求也可以更好的处理，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var http = require(&apos;http&apos;)
var url = require(&apos;url&apos;)
var querystring = require(&apos;querystring&apos;)
var util = require(&apos;util&apos;)

var routes = {
  &apos;/test&apos;: function (req, res) {
    console.log(req.method)
    if (req.method === &apos;GET&apos;) {
      console.log(req.headers.cookie)
      res.setHeader(&apos;Access-Control-Allow-Origin&apos;, &apos;http://localhost:8081&apos;)
      res.setHeader(&apos;Access-Control-Allow-Credentials&apos;, true)
      res.writeHead(200, &apos;Ok&apos;)
      res.write(`success`)
      res.end()
    } else {
      var post = &apos;&apos;
      req.on(&apos;data&apos;, function (chunk) {
        post += chunk
      })
      req.on(&apos;end&apos;, function () {
        res.setHeader(&apos;Access-Control-Allow-Origin&apos;, &apos;http://localhost:8081&apos;)
        res.setHeader(&apos;Access-Control-Allow-Credentials&apos;, true)
        res.setHeader(&apos;Access-Control-Request-Method&apos;, &apos;PUT,POST,GET,DELETE,OPTIONS&apos;)
        res.setHeader(
          &apos;Access-Control-Allow-Headers&apos;,
          &apos;Origin, X-Requested-With, Content-Type, Accept, t&apos;
        )
        res.end(&apos;success&apos;)
      })
    }
  }
}

var server = http.createServer(function (req, res) {
  var pathObj = url.parse(req.url, true)
  var handleFn = routes[pathObj.pathname]
  if (handleFn) {
    handleFn(req, res)
  }
})

server.listen(8080)
console.log(&apos;server on 8080...&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000008839142&quot; title=&quot;mock.js使用&quot;&gt;mock.js使用&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>URL特殊字符和JS编码解码方法</title><link>https://clloz.com/blog/url-encode-decode</link><guid isPermaLink="true">https://clloz.com/blog/url-encode-decode</guid><pubDate>Thu, 09 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们在操作 &lt;code&gt;URL&lt;/code&gt; 的时候经常需要处理其中的特殊字符，因为在 &lt;code&gt;URL&lt;/code&gt; 中有很多字符是有特殊意义的，比如 &lt;code&gt;?&lt;/code&gt;，&lt;code&gt;&amp;#x26;&lt;/code&gt;，&lt;code&gt;=&lt;/code&gt;等等，如果在处理 &lt;code&gt;URL&lt;/code&gt; 的时候不对这些特殊字符进行转义，那么浏览器很可能会错误解析 &lt;code&gt;URL&lt;/code&gt;，所以一般的编程语言都提供了对 &lt;code&gt;URL&lt;/code&gt; 进行编码解码的 &lt;code&gt;API&lt;/code&gt;，在 &lt;code&gt;JS&lt;/code&gt; 中也有对应的方法 &lt;code&gt;encodeURI&lt;/code&gt;，&lt;code&gt;decodeURI&lt;/code&gt; 和 &lt;code&gt;encodeURIComponent&lt;/code&gt;，&lt;code&gt;decodeURIComponent&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;escape&lt;/code&gt; 方法已经从标准中移除，虽然有些浏览器还支持，但是尽量使用上面的方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;URL编码&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;URL&lt;/code&gt; 编码也称作百分号编码 &lt;code&gt;Percent-encoding&lt;/code&gt;，是对我们在 &lt;code&gt;HTTP&lt;/code&gt; 请求中的 &lt;code&gt;URL&lt;/code&gt; 实行的编码机制。因为我们的资源路径中的字符很有可能是在 &lt;code&gt;URL&lt;/code&gt; 中有特殊意义的字符，比如我在本地保存一个文件名叫 &lt;code&gt;test test.txt&lt;/code&gt;，中间有一个空格，当我把这个文件上传到服务器比如说阿里云 &lt;code&gt;oss&lt;/code&gt; 中的时候，如果我要复制这个资源的路径就会编程 &lt;code&gt;https://cdn1.clloz.com/test%20test.txt&lt;/code&gt;，我们的空格被转义称为 &lt;code&gt;%20&lt;/code&gt;（这里的百分号有点像我们平常用来转义的 &lt;code&gt;\&lt;/code&gt; ），如果不进行转义，浏览器无法明白这个 &lt;code&gt;URL&lt;/code&gt; 到底是什么意思。我们对资源或者目录的命名是可以包含任意字符，比如空格，&lt;code&gt;?&lt;/code&gt;甚至是各种不同的语言的字符编码，为了能让我们在请求资源的时候有一个统一的标准，必须对 &lt;code&gt;URL&lt;/code&gt; 进行编码。&lt;/p&gt;
&lt;p&gt;再举两个例子：例如 &lt;code&gt;URL&lt;/code&gt; 参数字符串中使用 &lt;code&gt;key=value&lt;/code&gt; 键值对这样的形式来传参，键值对之间以 &lt;code&gt;&amp;#x26;&lt;/code&gt; 符号分隔，如 &lt;code&gt;/s?q=abc&amp;#x26;ie=utf-8&lt;/code&gt;。如果你的 &lt;code&gt;value&lt;/code&gt; 字符串中包含了 &lt;code&gt;=&lt;/code&gt; 或者 &lt;code&gt;&amp;#x26;&lt;/code&gt;，那么势必会造成接收&lt;code&gt;URL&lt;/code&gt;的服务器解析错误，因此必须将引起歧义的 &lt;code&gt;&amp;#x26;&lt;/code&gt; 和 &lt;code&gt;=&lt;/code&gt; 符号进行转义，也就是对其进行编码。&lt;/p&gt;
&lt;p&gt;又如，&lt;code&gt;URL&lt;/code&gt; 的编码格式采用的是 &lt;code&gt;ASCII&lt;/code&gt; 码，而不是 &lt;code&gt;Unicode&lt;/code&gt;，这也就是说你不能在 &lt;code&gt;URL&lt;/code&gt; 中包含任何非 &lt;code&gt;ASCII&lt;/code&gt; 字符，例如中文。否则如果客户端浏览器和服务端浏览器支持的字符集不同的情况下，中文可能会造成问题。&lt;/p&gt;
&lt;h2&gt;URL中的字符&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;URL&lt;/code&gt; 中的字符分为保留字符和未保留字符，保留字符是那些具有特殊含义的字符，例如：斜线字符用于&lt;code&gt;URL&lt;/code&gt;（或 &lt;code&gt;URI&lt;/code&gt; ）不同部分的分界符；未保留字符没有这些特殊含义。百分号编码把保留字符表示为特殊字符序列。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;保留字符： &lt;code&gt;*&lt;/code&gt; &lt;code&gt;&apos;&lt;/code&gt; &lt;code&gt;(&lt;/code&gt; &lt;code&gt;)&lt;/code&gt; &lt;code&gt;;&lt;/code&gt; &lt;code&gt;:&lt;/code&gt; &lt;code&gt;@&lt;/code&gt; &lt;code&gt;&amp;#x26;&lt;/code&gt; &lt;code&gt;=&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; &lt;code&gt;$&lt;/code&gt; &lt;code&gt;,&lt;/code&gt; &lt;code&gt;/&lt;/code&gt; &lt;code&gt;?&lt;/code&gt; &lt;code&gt;#&lt;/code&gt; &lt;code&gt;[&lt;/code&gt; &lt;code&gt;]&lt;/code&gt; (&lt;a href=&quot;https://tools.ietf.org/html/rfc3986&quot; title=&quot;RFC 3986&quot;&gt;RFC 3986&lt;/a&gt; section 2.2 保留字符 (2005年1月))&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;未保留字符：&lt;code&gt;A&lt;/code&gt; &lt;code&gt;B&lt;/code&gt; &lt;code&gt;C&lt;/code&gt; &lt;code&gt;D&lt;/code&gt; &lt;code&gt;E&lt;/code&gt; &lt;code&gt;F&lt;/code&gt; &lt;code&gt;G&lt;/code&gt; &lt;code&gt;H&lt;/code&gt; &lt;code&gt;I&lt;/code&gt; &lt;code&gt;J&lt;/code&gt; &lt;code&gt;K&lt;/code&gt; &lt;code&gt;L&lt;/code&gt; &lt;code&gt;M&lt;/code&gt; &lt;code&gt;N&lt;/code&gt; &lt;code&gt;O&lt;/code&gt; &lt;code&gt;P&lt;/code&gt; &lt;code&gt;Q&lt;/code&gt; &lt;code&gt;R&lt;/code&gt; &lt;code&gt;S&lt;/code&gt; &lt;code&gt;T&lt;/code&gt; &lt;code&gt;U&lt;/code&gt; &lt;code&gt;V&lt;/code&gt; &lt;code&gt;W&lt;/code&gt; &lt;code&gt;X&lt;/code&gt; &lt;code&gt;Y&lt;/code&gt; &lt;code&gt;Z&lt;/code&gt; &lt;code&gt;a&lt;/code&gt; &lt;code&gt;b&lt;/code&gt; &lt;code&gt;c&lt;/code&gt; &lt;code&gt;d&lt;/code&gt; &lt;code&gt;e&lt;/code&gt; &lt;code&gt;f&lt;/code&gt; &lt;code&gt;g&lt;/code&gt; &lt;code&gt;h&lt;/code&gt; &lt;code&gt;i&lt;/code&gt; &lt;code&gt;j&lt;/code&gt; &lt;code&gt;k&lt;/code&gt; &lt;code&gt;l&lt;/code&gt; &lt;code&gt;m&lt;/code&gt; &lt;code&gt;n&lt;/code&gt; &lt;code&gt;o&lt;/code&gt; &lt;code&gt;p&lt;/code&gt; &lt;code&gt;q&lt;/code&gt; &lt;code&gt;r&lt;/code&gt; &lt;code&gt;s&lt;/code&gt; &lt;code&gt;t&lt;/code&gt; &lt;code&gt;u&lt;/code&gt; &lt;code&gt;v&lt;/code&gt; &lt;code&gt;w&lt;/code&gt; &lt;code&gt;x&lt;/code&gt; &lt;code&gt;y&lt;/code&gt; &lt;code&gt;z&lt;/code&gt; &lt;code&gt;0&lt;/code&gt; &lt;code&gt;1&lt;/code&gt; &lt;code&gt;2&lt;/code&gt; &lt;code&gt;3&lt;/code&gt; &lt;code&gt;4&lt;/code&gt; &lt;code&gt;5&lt;/code&gt; &lt;code&gt;6&lt;/code&gt; &lt;code&gt;7&lt;/code&gt; &lt;code&gt;8&lt;/code&gt; &lt;code&gt;9&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;_&lt;/code&gt; &lt;code&gt;.&lt;/code&gt; &lt;code&gt;~&lt;/code&gt; (&lt;a href=&quot;https://tools.ietf.org/html/rfc3986&quot; title=&quot;RFC 3986&quot;&gt;RFC 3986&lt;/a&gt; section 2.2 保留字符 (2005年1月))&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;URI&lt;/code&gt; 中的其它字符必须用百分号编码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为保留字符在 &lt;code&gt;URL&lt;/code&gt; 中具有特殊的含义，所以当我们的路径中使用保留字符作为普通字符的时候就必须进行转义，其实保留字符的转义非常简单，就是在对应保留字符的 &lt;code&gt;ASCII&lt;/code&gt; 码对应的十六进制数前面加上转义字符 &lt;code&gt;%&lt;/code&gt; 即可。对于非 &lt;code&gt;ASCII&lt;/code&gt; 字符, 需要转换为 &lt;code&gt;UTF-8&lt;/code&gt; 字节序, 然后每个字节按照上述方式表示。&lt;/p&gt;
&lt;p&gt;例如，&lt;code&gt;/&lt;/code&gt;, 如果用作 &lt;code&gt;URI&lt;/code&gt; 的路径成分的分界符, 则是具有特殊含义的保留字符. 如果该字符需要出现在 &lt;code&gt;URI&lt;/code&gt; 一个路径成分的内部, 则三字符序列&lt;code&gt;%2F&lt;/code&gt;或 &lt;code&gt;%2f&lt;/code&gt; 就用于代替原本的 &lt;code&gt;/&lt;/code&gt; 出现在该 &lt;code&gt;URI&lt;/code&gt; 路径成分的内部。&lt;/p&gt;
&lt;p&gt;| 保留字符 | 编码   |
| -------- | ------ |
| 空格     | &lt;code&gt;%20&lt;/code&gt;  |
| &lt;code&gt;!&lt;/code&gt;      | &lt;code&gt;%21&lt;/code&gt;  |
| &lt;code&gt;&quot;&lt;/code&gt;      | &lt;code&gt;%22&lt;/code&gt;  |
| &lt;code&gt;#&lt;/code&gt;      | &lt;code&gt;%23&lt;/code&gt;  |
| &lt;code&gt;$&lt;/code&gt;      | &lt;code&gt;%24&lt;/code&gt;  |
| &lt;code&gt;%&lt;/code&gt;      | &lt;code&gt;%25&lt;/code&gt;  |
| &lt;code&gt;&amp;#x26;&lt;/code&gt;      | &lt;code&gt;%26&lt;/code&gt;  |
| &lt;code&gt;&apos;&lt;/code&gt;      | &lt;code&gt;%27&lt;/code&gt;  |
| &lt;code&gt;(&lt;/code&gt;      | &lt;code&gt;%28&lt;/code&gt;  |
| &lt;code&gt;)&lt;/code&gt;      | &lt;code&gt;%29&lt;/code&gt;  |
| &lt;code&gt;*&lt;/code&gt;      | &lt;code&gt;%2A&lt;/code&gt;  |
| &lt;code&gt;+&lt;/code&gt;      | &lt;code&gt;%2B&lt;/code&gt;  |
| &lt;code&gt;,&lt;/code&gt;      |  &lt;code&gt;%2C&lt;/code&gt; |
| &lt;code&gt;/&lt;/code&gt;      | &lt;code&gt;%2F&lt;/code&gt;  |
| &lt;code&gt;:&lt;/code&gt;      | &lt;code&gt;%3A&lt;/code&gt;  |
| &lt;code&gt;;&lt;/code&gt;      |  &lt;code&gt;%3B&lt;/code&gt; |
| &lt;code&gt;&amp;#x3C;&lt;/code&gt;      |  &lt;code&gt;%3C&lt;/code&gt; |
| &lt;code&gt;=&lt;/code&gt;      |  &lt;code&gt;%3D&lt;/code&gt; |
| &lt;code&gt;&gt;&lt;/code&gt;      | &lt;code&gt;%3E&lt;/code&gt;  |
| &lt;code&gt;?&lt;/code&gt;      | &lt;code&gt;%3F&lt;/code&gt;  |
| &lt;code&gt;@&lt;/code&gt;      |  &lt;code&gt;%40&lt;/code&gt; |
| &lt;code&gt;[&lt;/code&gt;      | &lt;code&gt;%5B&lt;/code&gt;  |
| &lt;code&gt;\&lt;/code&gt;      |  &lt;code&gt;%5C&lt;/code&gt; |
| &lt;code&gt;]&lt;/code&gt;      | &lt;code&gt;%5D&lt;/code&gt;  |&lt;/p&gt;
&lt;h2&gt;JS中的URL编码解码方法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JS&lt;/code&gt; 中提供了两组用来编码解码 &lt;code&gt;URL&lt;/code&gt; 的方法，&lt;code&gt;encodeURI&lt;/code&gt;，&lt;code&gt;decodeURI&lt;/code&gt; 和 &lt;code&gt;encodeURIComponent&lt;/code&gt; 和 &lt;code&gt;decodeURIComponent&lt;/code&gt;。两组方法功能一样，都是用来对 &lt;code&gt;URI&lt;/code&gt; 进行编码或者解码，不同的是对编码字符的范围有所不同，&lt;code&gt;encodeURI&lt;/code&gt; 的范围要比 &lt;code&gt;encodeURIComponent&lt;/code&gt; 小一些。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;encodeURI&lt;/code&gt; 会转义除以下字符的所有字符：&lt;/p&gt;
&lt;p&gt;| 类型         | 包含                                                  |
| ------------ | ----------------------------------------------------- |
| 保留字符     | &lt;code&gt;;&lt;/code&gt; &lt;code&gt;,&lt;/code&gt; &lt;code&gt;/&lt;/code&gt; &lt;code&gt;?&lt;/code&gt; &lt;code&gt;:&lt;/code&gt; &lt;code&gt;@&lt;/code&gt; &lt;code&gt;&amp;#x26;&lt;/code&gt; &lt;code&gt;=&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; &lt;code&gt;$&lt;/code&gt;               |
| 非转义的字符 | &lt;code&gt;A-Z&lt;/code&gt; &lt;code&gt;a-z&lt;/code&gt; &lt;code&gt;0-9&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;_&lt;/code&gt; &lt;code&gt;.&lt;/code&gt; &lt;code&gt;!&lt;/code&gt; &lt;code&gt;~&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; &lt;code&gt;&apos;&lt;/code&gt; &lt;code&gt;(&lt;/code&gt; &lt;code&gt;)&lt;/code&gt; |
| 数字符号     | &lt;code&gt;#&lt;/code&gt;                                                   |&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;encodeURIComponent&lt;/code&gt; 会转义除了转义除了&lt;code&gt;字母&lt;/code&gt;、&lt;code&gt;数字&lt;/code&gt;、&lt;code&gt;(&lt;/code&gt;、&lt;code&gt;)&lt;/code&gt;、&lt;code&gt;.&lt;/code&gt;、&lt;code&gt;!&lt;/code&gt;、&lt;code&gt;~&lt;/code&gt;、&lt;code&gt;*&lt;/code&gt;、&lt;code&gt;&apos;&lt;/code&gt;、&lt;code&gt;-&lt;/code&gt;和 &lt;code&gt;_&lt;/code&gt; 之外的所有字符。&lt;/p&gt;
&lt;p&gt;对于非 &lt;code&gt;ASCII&lt;/code&gt; 码的字符，这两个方法都会将对应的字符的多字节的 &lt;code&gt;UTF-8&lt;/code&gt; 编码转义成单字节的 &lt;code&gt;16&lt;/code&gt; 进制数，比如 &lt;code&gt;你好吗&lt;/code&gt; 的 &lt;code&gt;UTF-8&lt;/code&gt; 编码是 &lt;code&gt;E4BDA0 E5A5BD E59097&lt;/code&gt; 就会被这两个方法转义为 &lt;code&gt;%E4%BD%A0 %E5%A5%BD %E5%90%97&lt;/code&gt;，每个字都是三个字节，就被转义为三个两位的 &lt;code&gt;16&lt;/code&gt; 进制数。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/pcyph/article/details/45010609&quot; title=&quot;网址URL中特殊字符转义编码&quot;&gt;网址URL中特殊字符转义编码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E7%99%BE%E5%88%86%E5%8F%B7%E7%BC%96%E7%A0%81&quot; title=&quot;百分号编码&quot;&gt;百分号编码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/html/ig/zh/wiki/ES5/%E6%A0%87%E5%87%86_ECMAScript_%E5%86%85%E7%BD%AE%E5%AF%B9%E8%B1%A1#x15.1.3.1&quot; title=&quot;ES5/标准 ECMAScript 内置对象&quot;&gt;ES5/标准 ECMAScript 内置对象&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>浏览器同源策略和跨域方法</title><link>https://clloz.com/blog/cors</link><guid isPermaLink="true">https://clloz.com/blog/cors</guid><pubDate>Wed, 08 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;了解浏览器的同源策略和各种跨域方式是所有前端都必须熟练掌握的知识，因为在开发的过程中遇到跨域请求是常有的事情，包括我们自己 &lt;code&gt;mock&lt;/code&gt; 数据的时候也可能遇到跨域的问题，如果不能理解同源策略那么每次遇到跨域都可能不能快速解决。&lt;/p&gt;
&lt;h2&gt;同源策略&lt;/h2&gt;
&lt;p&gt;同源策略是一个重要的安全策略，它用于限制一个 &lt;code&gt;origin&lt;/code&gt; 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档，减少可能被攻击的媒介。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同源策略是浏览器的策略，会在请求从服务返回的时候检查响应头中的 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 和请求头中的 &lt;code&gt;origin&lt;/code&gt; 是否匹配，如果不匹配则报错。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;源 origin&lt;/h2&gt;
&lt;p&gt;我们使用浏览器浏览网页的时候，大多数情况都是通过 &lt;code&gt;http&lt;/code&gt; 请求去访问对应主机（ &lt;code&gt;host&lt;/code&gt; ）上的资源（ &lt;code&gt;resource&lt;/code&gt; ），一般同一个主机同一个端口同一个协议就会被认为是一个源，一般我们会说同协议同域名同端口的请求浏览器会认为是同源的请求。可能很多人刚看到这个策略的时候跟我有一样的想法，为什么是同一个域名而不是同一个 &lt;code&gt;IP&lt;/code&gt; 呢？在 &lt;code&gt;MDN&lt;/code&gt; 的&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy&quot; title=&quot;英文文档&quot;&gt;英文文档&lt;/a&gt;里面写的是 &lt;code&gt;host&lt;/code&gt; 也就是主机，要更好的理解什么是源我们要从服务器的角度来理解。我们的服务器上用来处理 &lt;code&gt;http&lt;/code&gt; 请求的一般是 &lt;code&gt;web&lt;/code&gt; 服务器，比如 &lt;code&gt;apapce&lt;/code&gt; 、 &lt;code&gt;nginx&lt;/code&gt; 等，在 &lt;code&gt;web&lt;/code&gt; 服务器的配置中我们会配置我们的网站域名和根目录一般默认绑定到 &lt;code&gt;80&lt;/code&gt; 端口，比如 &lt;code&gt;/var/www/html&lt;/code&gt; 当 &lt;code&gt;web&lt;/code&gt; 服务器接收到 &lt;code&gt;http&lt;/code&gt; 请求对应目录的资源的时候就会去我们绑定的目录搜索对应的资源。但是一个 &lt;code&gt;web&lt;/code&gt; 服务器下面可以绑定多个主机，我们可以用虚拟主机来做域名和目录的映射，如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;VirtualHost 127.0.0.1:80&gt;
    ServerAdmin yourname@domain.com
    DocumentRoot &quot;E:/server110.com/wordpress-latest&quot;
    ServerName server110.com
    ServerAlias www.server110.com
    ErrorLog &quot;logs/wplatest.com-error.log&quot;
    CustomLog &quot;logs/server110.com-access.log&quot; combined
&amp;#x3C;/VirtualHost&gt;
&amp;#x3C;VirtualHost 127.0.0.2:80&gt;
    ServerAdmin yourname@domain.com
    DocumentRoot &quot;E:/server110.com/wordpress-2.9.2&quot;
    ServerName server110.com
    ServerAlias www.server110.com
    ErrorLog &quot;logs/server110.com-error.log&quot;
    CustomLog &quot;logs/server110.com-access.log&quot; combined
&amp;#x3C;/VirtualHost&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;web&lt;/code&gt; 服务器接受到请求的时候会看看是请求头中的 &lt;code&gt;host&lt;/code&gt; 参数，在根据配置文件到对应的目录寻找资源。正因为这个原因，同源的定义就是 &lt;code&gt;same-host&lt;/code&gt;，同一个主机。而 &lt;code&gt;web&lt;/code&gt; 配置目录的方法不止虚拟主机一种方式，还可以利用不同的端口进行映射，比如网站 &lt;code&gt;a&lt;/code&gt; 的目录 &lt;code&gt;/var/www/a&lt;/code&gt; 映射到 &lt;code&gt;80&lt;/code&gt; 端口，而另一个网站 &lt;code&gt;b&lt;/code&gt; 的目录 &lt;code&gt;/var/www/b&lt;/code&gt; 的目录映射到 &lt;code&gt;8080&lt;/code&gt; 端口，配置方法就是把上面的配置文件中的端口改成自己需要的。我们在往上购买的虚拟主机，大部分都是通过这种办法来配置多个网站的，也就是说这些网站的 &lt;code&gt;IP&lt;/code&gt; 地址都是相同的，但是他们的拥有者不同，这也就是浏览器要对源之间的互动进行限制的原因。最后就是 &lt;code&gt;http&lt;/code&gt; 和 &lt;code&gt;https&lt;/code&gt;，这两者如果不同，那么通信的过程都是不相同的，浏览器自然是不允许的，而且一般网站配置了 &lt;code&gt;https&lt;/code&gt; 那么所有的资源请求都会是 &lt;code&gt;https&lt;/code&gt; ，一般不会出现混用。&lt;/p&gt;
&lt;p&gt;根据上面的规则我们举个是否同源的例子，以我的域名 &lt;code&gt;https://www.clloz.com&lt;/code&gt; 为例，我这个域名解析到了我阿里云主机的 &lt;code&gt;ip&lt;/code&gt;，&lt;code&gt;web&lt;/code&gt;服务器根据配置文件可以知道该 &lt;code&gt;host&lt;/code&gt; 的请求去对应的文件夹取资源，比如有用户请求 &lt;code&gt;https://www.clloz.com/index.html&lt;/code&gt;, 那么服务器就会返回这个页面。如果这个 &lt;code&gt;index.html&lt;/code&gt; 中的脚本发送如下请求，我们可以判断是否同源：&lt;/p&gt;
&lt;p&gt;| URL                                     | 结果 | 原因         |
| --------------------------------------- | ---- | ------------ |
| &lt;code&gt;https://www.clloz.com/study/test.html&lt;/code&gt; | 成功 | 只有路径不同 |
| &lt;code&gt;http://www.clloz.com/test.json&lt;/code&gt;        | 失败 | 协议不同     |
| &lt;code&gt;https://www.clloz.com:8080&lt;/code&gt;            | 失败 | 端口不同     |
| &lt;code&gt;https://test.clloz.com/test.json&lt;/code&gt;      | 失败 | 域名不同     |
| &lt;code&gt;https://clloz.com/test.json&lt;/code&gt;           | 失败 | 域名不同     |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;主机和域名的区别：一般来说我们申请一个域名是一个二级域名比如 &lt;code&gt;clloz.com&lt;/code&gt;（也有认为顶级域和二级域之间还有一级域，阿里云就是这样的方式），顶级域名就是就是域名最后的那个部分，比如我们常见的 &lt;code&gt;.com&lt;/code&gt; &lt;code&gt;.cn&lt;/code&gt; &lt;code&gt;.org&lt;/code&gt; &lt;code&gt;.edu&lt;/code&gt; 等，顶级域名前面一个就是二级域名比如我的域名中的 &lt;code&gt;clloz&lt;/code&gt;，以此类推。当我们购买了一个域名以后，我们可以为其添加主机记录进行解析，比如我可以添加一个 &lt;code&gt;www&lt;/code&gt; 的主机记录解析到我的服务器 &lt;code&gt;ip&lt;/code&gt;，也可以添加一个 &lt;code&gt;test&lt;/code&gt; 主机记录解析到 &lt;code&gt;http://www.clloz.com:8080&lt;/code&gt;，这些添加了主机记录的能访问到服务器上具体文件的域名就称为 &lt;code&gt;host&lt;/code&gt; 主机名，在我们发送请求的时候，二者可以混用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;为什么要有同源策略&lt;/h2&gt;
&lt;p&gt;其实上面解释源的时候就已经能够明白为什么浏览器要使用同源策略了，我们来看看文档和历史。&lt;code&gt;MDN&lt;/code&gt; 的解释如下 &lt;code&gt;The same-origin policy is a critical security mechanism that restricts how a document or script loaded from one origin can interact with a resource from another origin. It helps isolate potentially malicious documents, reducing possible attack vectors.&lt;/code&gt; 大概意思就是同源策略限制了一个源上的文档或者脚本和另一个源上的资源互动的方式。主要的目的是为用户的安全，隔离潜在恶意文件的重要安全机制。&lt;/p&gt;
&lt;p&gt;同源策略最早有网景在1995年引入，现在所有的浏览器都实行这个策略。最早同源是为了针对客户端上保存状态的 &lt;code&gt;cookie&lt;/code&gt;。为了解决 &lt;code&gt;http&lt;/code&gt; 协议无状态带来的用户状态无法保存的情况引入了 &lt;code&gt;cookie&lt;/code&gt;，如果不同源的网站能够共享 &lt;code&gt;cookie&lt;/code&gt; 会带来非常严重的安全问题，比如我们登录了某个支付网站或者网上银行没有登出，这时候点进了一个危险的网页，这个网页可以利用我们的 &lt;code&gt;cookie&lt;/code&gt; 去登录，这是非常危险的，所以最早的同源策略就是针对这样的情况，每个源之间的 &lt;code&gt;cookie&lt;/code&gt; 都是独立的（父域子域可以共享，后面会说）。但是随着 &lt;code&gt;web&lt;/code&gt; 的发展，网站提供的服务越来越多，越来越复杂，也出现了更多的攻击手段，所以为了安全，浏览器不得不提升同源策略覆盖的范围。&lt;/p&gt;
&lt;p&gt;再举个例子比如钓鱼网站，有人给你发邮件引诱你点击支付宝链接 &lt;code&gt;alipay.com&lt;/code&gt;，但实际你打开的链接是一个 &lt;code&gt;aliipay.com&lt;/code&gt;，这个页面用 &lt;code&gt;iframe&lt;/code&gt; 显示支付宝页面。如果没有同源策略，它完全可以获取你输入的用户名和密码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;;&amp;#x3C;iframe name=&apos;alipay&apos; src=&apos;www.alipay.com&apos;&gt;&amp;#x3C;/iframe&gt;
// JS
// 由于没有同源策略的限制，钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames[&apos;alipay&apos;]
const node = iframe.document.getElementById(&apos;你输入账号密码的Input&apos;)
console.log(`拿到了这个${node}，我还拿不到你刚刚输入的账号密码吗`)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安全和灵活的矛盾&lt;/h2&gt;
&lt;p&gt;同源策略确实提高了网站的安全性，让攻击者攻击网站的难度提高，用户也不会因为误点恶意链接而遭受损失，但是对于开发者来说，多个子系统之间的互动是必要的，浏览器一刀切的同源策略有时候会带来很大的麻烦，从这方面看安全性和交互的灵活性是一对矛盾。所以浏览器在同源策略的制定上还是对交互做了一定的妥协，比如我们都知道的直接用链接嵌入其他源中的 &lt;code&gt;css&lt;/code&gt;，&lt;code&gt;js&lt;/code&gt; 和 &lt;code&gt;image&lt;/code&gt;，父域和子域之间可以共享 &lt;code&gt;cookie&lt;/code&gt;等。&lt;/p&gt;
&lt;h2&gt;跨源交互细节&lt;/h2&gt;
&lt;p&gt;为了解决跨域导致的跨源交互不便，浏览器制定了跨源交互的规则，通常情况下： 1. 允许跨源写( &lt;code&gt;cross-origin write&lt;/code&gt; )，例如链接(&lt;code&gt;links&lt;/code&gt;)，重定向以及表单提交。特定少数的 &lt;code&gt;HTTP&lt;/code&gt; 请求需要添加 &lt;code&gt;preflight&lt;/code&gt;。经过测试，用 &lt;code&gt;XMLHttpRequest&lt;/code&gt; 对象给后台发送文件也不会被同源策略拦截。 2. 允许跨域资源嵌入。 3. 不允许跨源读取资源，但常可以通过内嵌资源来巧妙的进行读取访问。例如，你可以读取嵌入图片的高度和宽度，调用内嵌脚本的方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;之所以允许跨源写而不允许跨源读，我认为是跨源写的操作不会泄露关键信息，只是将信息发送到服务器。而跨源读操作则可能造成用户信息的泄露。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;总的来说同源策略的影响是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cookie、LocalStorage 和 IndexDB 无法读取&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DOM和JS对象无法获得&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AJAX&lt;/code&gt; 请求无法接受响应(请求成功发出，响应也返回浏览器，但浏览器抛错)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;跨域嵌入的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;script src=&quot;...&quot;&gt;&amp;#x3C;/script&gt;&lt;/code&gt; &lt;code&gt;标签嵌入跨域脚本&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;...&quot;&gt;&lt;/code&gt; 标签嵌入&lt;code&gt;CSS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt;嵌入图片&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;audio&gt;&lt;/code&gt; 嵌入多媒体资源&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;object&gt;&lt;/code&gt;, &lt;code&gt;&amp;#x3C;embed&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;applet&gt;&lt;/code&gt;的插件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@font-face&lt;/code&gt; 引入的字体。一些浏览器允许跨域字体( &lt;code&gt;cross-origin fonts&lt;/code&gt;)，一些需要同源字体(&lt;code&gt;same-origin fonts&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;frame&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;iframe&gt;&lt;/code&gt; 载入的任何资源。站点可以使用 &lt;code&gt;X-Frame-Options&lt;/code&gt; 消息头来阻止这种形式的跨域交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;浏览器的具体同源策略没有找到标准的文档，不过大致的思路就是我们可以向不同源的发送信息，不可以从不同的源接收信息，我把上面的内容和查到的规则整理如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于嵌入到页面的 &lt;code&gt;ifram&lt;/code&gt; (如果 &lt;code&gt;X-Frame-Options&lt;/code&gt; 允许)，无法访问 &lt;code&gt;iframe&lt;/code&gt; 的文档，也就是不能操作 &lt;code&gt;DOM&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;css&lt;/code&gt; 文件可以通过 &lt;code&gt;link&lt;/code&gt; 标签嵌入或者 &lt;code&gt;@import&lt;/code&gt; 方式引入，可能需要 &lt;code&gt;Content-type&lt;/code&gt; 支持。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;form&lt;/code&gt; 表单，&lt;code&gt;action&lt;/code&gt; 可以使用跨源 &lt;code&gt;URL&lt;/code&gt;，利用表单的提交可以将表单中的数据写入跨源目标。&lt;/li&gt;
&lt;li&gt;可以用 &lt;code&gt;img&lt;/code&gt; 标签嵌入图像，但是无法读取图像的数据(例如 &lt;code&gt;canvas&lt;/code&gt; 使用 &lt;code&gt;JavaScript&lt;/code&gt; 将跨源图像加载到元素中)，如果需要读取图像，需要为图片所在服务器开启 &lt;code&gt;cors&lt;/code&gt;，并且为图片加上属性 &lt;code&gt;crossOrigin=anonymous&lt;/code&gt;，其实是和开启 &lt;code&gt;cors&lt;/code&gt; 的 &lt;code&gt;ajax&lt;/code&gt; 请求没有区别。&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image&quot; title=&quot;CORS_enabled_image&quot;&gt;CORS_enabled_image&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;可以使用 &lt;code&gt;video&lt;/code&gt; 和 &lt;code&gt;audio&lt;/code&gt; 元素嵌入跨源视频和音频。&lt;/li&gt;
&lt;li&gt;可以嵌入跨源脚本; 但是，可能会阻止对某些API的访问，例如跨源的 &lt;code&gt;ajax&lt;/code&gt; 或者 &lt;code&gt;fetch&lt;/code&gt; 请求。根据我的测试，用 &lt;code&gt;ajax&lt;/code&gt; 对跨源接口发送文件并不会触发同源策略，能够成功发送。&lt;/li&gt;
&lt;li&gt;存储在浏览器中的数据，如 &lt;code&gt;localStorage&lt;/code&gt; 和 &lt;code&gt;IndexedDB&lt;/code&gt;，以源进行分割。每个源都拥有自己单独的存储空间，一个源中的 &lt;code&gt;Javascript&lt;/code&gt; 脚本不能对属于其它源的数据进行读写操作。&lt;/li&gt;
&lt;li&gt;一个页面可以为本域和任何父域设置 &lt;code&gt;cookie&lt;/code&gt;，只要是父域不是公共后缀( &lt;code&gt;public suffix&lt;/code&gt; )即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于嵌入图片的读取可以测试如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 嵌入一张跨域的google logo --&gt;
&amp;#x3C;img
  crossorigin=&quot;anonymous&quot;
  src=&quot;https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png&quot;
  alt=&quot;&quot;
/&gt;
&amp;#x3C;script&gt;
  function main() {
    var img = document.querySelector(&apos;img&apos;)
    img.onload = function () {
      var canvas = document.createElement(&apos;canvas&apos;)
      canvas.width = img.width
      canvas.height = img.height

      // Copy the image contents to the canvas
      var ctx = canvas.getContext(&apos;2d&apos;)
      ctx.drawImage(img, 0, 0)

      var dataURL = canvas.toDataURL(&apos;image/png&apos;)

      var data = dataURL.replace(/^data:image\/(png|jpg);base64,/, &apos;&apos;)
      console.log(data)
    }
  }
  main()
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行这个 &lt;code&gt;HTML&lt;/code&gt; 浏览器会告诉你跨域了，解决的方式就是给图片加上 &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt; 属性，并且图片所在服务器要开启 &lt;code&gt;cors&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;ajax&lt;/code&gt; 发送的文件，大家可以用 &lt;code&gt;nodejs&lt;/code&gt; 写一个简单的服务端，前端用 &lt;code&gt;formdata&lt;/code&gt; 发送即可，并不会被浏览器拦击。&lt;/p&gt;
&lt;h2&gt;CSRF 跨站请求伪造&lt;/h2&gt;
&lt;p&gt;由于同源策略允许跨源写操作，所以攻击者可以利用这一点来攻击，这种攻击称为跨站请求伪造 &lt;code&gt;Cross Site Request Forgery&lt;/code&gt;，它通常发音为 &lt;code&gt;sea-surf&lt;/code&gt;，也经常被称为 &lt;code&gt;XSRF&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;跨站点请求伪造(&lt;code&gt;CSRF&lt;/code&gt;)迫使最终用户在当前已通过身份验证的Web应用程序上执行不需要的操作。在社会工程学的一点帮助下(例如通过电子邮件或聊天发送链接)，攻击者可能会诱骗 &lt;code&gt;Web&lt;/code&gt; 应用程序的用户执行攻击者选择的操作。如果受害者是普通用户，则成功的 &lt;code&gt;CSRF&lt;/code&gt; 攻击会迫使用户执行状态更改请求，例如转移资金，更改其电子邮件地址等。如果受害者是管理帐户，&lt;code&gt;CSRF&lt;/code&gt; 可能会破坏整个 &lt;code&gt;Web&lt;/code&gt; 应用程序。&lt;/p&gt;
&lt;p&gt;当我们登录了一个网站，会将登录信息保存在 &lt;code&gt;cookie&lt;/code&gt; 中，当我们下次发送请求的时候就连同 &lt;code&gt;cookie&lt;/code&gt; 一起发送来验证身份，而不需要重新登录。跨站请求伪造就是利用这种机制，向你已经登录的网站再次发起请求，并带上自己的参数，这是一个跨源写操作，所以能够正常发送。&lt;/p&gt;
&lt;p&gt;比如你登录了银行账户给别人转账，链接&lt;code&gt;https://your-bank.com/transfer/xxx&lt;/code&gt;，此时如果攻击者给你发了个链接是一个表单：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;form action=&quot;https://your-bank.com/transfer&quot; method=&quot;POST&quot; id=&quot;stealMoney&quot;&gt;
  &amp;#x3C;input type=&quot;hidden&quot; name=&quot;to&quot; value=&quot;attacker_account&quot; /&gt;
  &amp;#x3C;input type=&quot;hidden&quot; name=&quot;account&quot; value=&quot;your_account&quot; /&gt;
  &amp;#x3C;input type=&quot;hidden&quot; name=&quot;amount&quot; value=&quot;$1,000&quot; /&gt;
&amp;#x3C;/form&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;攻击者将收款账户改为自己的，此时提交表单，由于发送给 &lt;code&gt;https://your-bank.com/&lt;/code&gt; 的请求会带上你刚刚登录的 &lt;code&gt;cookie&lt;/code&gt;，验证成功，你的钱就打到了攻击者的账户。当然这只是一个例子，说明了攻击的可能性。在你没有登录的情况下，这种攻击是无效的。所以攻击者一般会利用社会工程学，诱骗你先登录银行账户，然后在诱骗你点击攻击用的链接。一般有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发送带有 &lt;code&gt;HTML&lt;/code&gt; 内容的未经请求的电子邮件&lt;/li&gt;
&lt;li&gt;在受害者也进行在线银行业务时可能会访问的页面上植入漏洞利用 &lt;code&gt;URL&lt;/code&gt; 或脚本，甚至可能只是一张图片。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于跨站请求伪造攻击可以参考：&lt;a href=&quot;https://owasp.org/www-community/attacks/csrfhttps://owasp.org/www-community/attacks/csrf&quot; title=&quot;跨站请求伪造（CSRF）&quot;&gt;跨站请求伪造（CSRF）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;要避免跨站请求伪造可以在服务端检查 &lt;code&gt;origin&lt;/code&gt; 和 &lt;code&gt;referer&lt;/code&gt;，当然这也不是绝对安全的。&lt;/p&gt;
&lt;p&gt;还可以通过将一个称为 &lt;code&gt;CSRF&lt;/code&gt; 令牌的令牌发送到网页。每次发出新请求时，都会发送并验证此令牌。因此，向服务器发出的恶意请求将通过 &lt;code&gt;cookie&lt;/code&gt; 身份验证，但 &lt;code&gt;CSRF&lt;/code&gt; 验证会失败。大多数 &lt;code&gt;Web&lt;/code&gt; 框架为防止 &lt;code&gt;CSRF&lt;/code&gt; 攻击提供了开箱即用的支持，而 &lt;code&gt;CSRF&lt;/code&gt; 攻击现在并不像以前那样常见。&lt;/p&gt;
&lt;h2&gt;跨域方法&lt;/h2&gt;
&lt;p&gt;同源策略我们已经掌握，但是浏览器的这种一刀切的做法有时候会为开发带来不便。特别是在有多个子系统的网站中，需要跨域通信的情况肯定会多，我们会把各个子系统布置在不同的主机上，所以如何饶考同源策略进行跨域请求，是每个前端必须熟练掌握的。&lt;/p&gt;
&lt;h2&gt;JSONP&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JSONP&lt;/code&gt;就是利用同源策略中允许跨域资源嵌入的这条规定来进行跨域请求的，&lt;code&gt;script&lt;/code&gt; 标签请求的脚本会立即执行，那么只要请求中传给后端一个函数名，后端将函数名和数据拼接成执行函数的字符串返回给前端，浏览器解析的时候就相当于直接执行这个带参数的函数。 前端代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;body&gt;
  &amp;#x3C;script&gt;
    function success(data) {
      console.log(data)
    }
  &amp;#x3C;/script&gt;

  &amp;#x3C;script src=&quot;http://localhost:8080/test?callback=success&quot;&gt;&amp;#x3C;/script&gt;
&amp;#x3C;/body&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后端代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var http = require(&apos;http&apos;)
var url = require(&apos;url&apos;)

var routes = {
  &apos;/test&apos;: function (req, res) {
    var cb_str = url.parse(req.url, true).search
    res.writeHead(200, &apos;Ok&apos;)
    var cb = cb_str.split(&apos;=&apos;)[1]
    console.log(cb)
    res.write(cb + `({result: &quot;success&quot;})`)
    res.end()
  }
}

var server = http.createServer(function (req, res) {
  var pathObj = url.parse(req.url, true)
  var handleFn = routes[pathObj.pathname]
  if (handleFn) {
    console.log(pathObj)
    handleFn(req, res)
  }
})

server.listen(8080)
console.log(&apos;server on 8080...&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前端嵌入的 &lt;code&gt;script&lt;/code&gt; 标签在请求的时候带上了函数名 &lt;code&gt;success&lt;/code&gt; 作为请求参数，后端接收到请求后将前端需要的数据 &lt;code&gt;{result: &quot;success&quot;}&lt;/code&gt; 连带函数名拼接成 &lt;code&gt;success({result: &quot;success&quot;})&lt;/code&gt; 返回给浏览器，浏览器会直接将返回的字符串当作 &lt;code&gt;js&lt;/code&gt; 执行，由于我们前面已经定义了 &lt;code&gt;success&lt;/code&gt; 函数，所以这段代码会直接给 &lt;code&gt;success&lt;/code&gt; 函数带上参数执行，这样就实现了跨域请求。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;JSONP&lt;/code&gt; 只能发送 &lt;code&gt;GET&lt;/code&gt; 请求&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;利用 form 提交跨域请求&lt;/h2&gt;
&lt;p&gt;由于 &lt;code&gt;form&lt;/code&gt; 表单的功能是把数据发送给对应 &lt;code&gt;action&lt;/code&gt;，所以并没有被同源策略限制，所以我们可以用在脚本中创建 &lt;code&gt;form&lt;/code&gt; 并提交的方法来和跨域接口进行通信，用这种方法我们可以发送 &lt;code&gt;GET&lt;/code&gt; 和 &lt;code&gt;POST&lt;/code&gt; 请求，但是我们没法接收服务器返回的数据，不过可以利用设置 &lt;code&gt;form&lt;/code&gt; 的 &lt;code&gt;target&lt;/code&gt; 到一个空的 &lt;code&gt;iframe&lt;/code&gt; 并监听 &lt;code&gt;iframe&lt;/code&gt; 的 &lt;code&gt;load&lt;/code&gt; 事件来确定请求是否发送成功。&lt;/p&gt;
&lt;h2&gt;CORS&lt;/h2&gt;
&lt;p&gt;跨域资源共享(&lt;code&gt;CORS&lt;/code&gt;) 是一种机制，它是 &lt;code&gt;HTTP&lt;/code&gt; 的一部分，它使用额外的 &lt;code&gt;HTTP&lt;/code&gt; 头来告诉浏览器 让运行在一个 &lt;code&gt;origin&lt;/code&gt;(domain) 上的 &lt;code&gt;Web&lt;/code&gt;应用被准许访问来自不同源服务器上的指定的资源。&lt;code&gt;cors&lt;/code&gt; 的标准参考 &lt;a href=&quot;https://fetch.spec.whatwg.org/#http-cors-protocol&quot; title=&quot;Fetch - whatwg&quot;&gt;Fetch - whatwg&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CORS&lt;/code&gt; 需要浏览器和服务器同时支持。目前，所有浏览器都支持该功能，IE浏览器不能低于 &lt;code&gt;IE10&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;整个 &lt;code&gt;CORS&lt;/code&gt; 通信过程，都是浏览器自动完成，不需要用户参与。对于开发者来说，&lt;code&gt;CORS&lt;/code&gt; 通信与同源的AJAX通信没有差别，代码完全一样。浏览器一旦发现&lt;code&gt;AJAX&lt;/code&gt; 请求跨源，就会自动添加一些附加的头信息，有时还会多出一次附加的请求，但用户不会有感觉。&lt;/p&gt;
&lt;p&gt;因此，实现 &lt;code&gt;CORS&lt;/code&gt; 通信的关键是服务器。只要服务器实现了 &lt;code&gt;CORS&lt;/code&gt; 接口，就可以跨源通信。&lt;/p&gt;
&lt;p&gt;跨源域资源共享（ &lt;code&gt;CORS&lt;/code&gt; ）机制允许 &lt;code&gt;Web&lt;/code&gt; 应用服务器进行跨源访问控制，从而使跨源数据传输得以安全进行。现代浏览器支持在 &lt;code&gt;API&lt;/code&gt; 容器中（例如 &lt;code&gt;XMLHttpRequest&lt;/code&gt; 或 &lt;code&gt;Fetch&lt;/code&gt; ）使用 &lt;code&gt;CORS&lt;/code&gt;，以降低跨源 &lt;code&gt;HTTP&lt;/code&gt; 请求所带来的风险。&lt;/p&gt;
&lt;p&gt;在哪些情况下需要使用 &lt;code&gt;cors&lt;/code&gt; 呢：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由 &lt;code&gt;XMLHttpRequest&lt;/code&gt; 或 &lt;code&gt;Fetch&lt;/code&gt; 发起的跨源 &lt;code&gt;HTTP&lt;/code&gt; 请求。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Web&lt;/code&gt; 字体 (&lt;code&gt;CSS&lt;/code&gt; 中通过 &lt;code&gt;@font-face&lt;/code&gt; 使用跨源字体资源)，因此，网站就可以发布 &lt;code&gt;TrueType&lt;/code&gt; 字体资源，并只允许已授权网站进行跨站调用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WebGL&lt;/code&gt; 贴图&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;drawImage&lt;/code&gt; 将 &lt;code&gt;Images/video&lt;/code&gt; 画面绘制到 &lt;code&gt;canvas&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;浏览器将 &lt;code&gt;CORS&lt;/code&gt; 请求分成两类：简单请求（ &lt;code&gt;simple request&lt;/code&gt; ）和非简单请求（ &lt;code&gt;not-so-simple request&lt;/code&gt; ）。只要同时满足以下两大条件，就属于简单请求。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;请求方法是以下三种方法之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HEAD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;除了被用户代理自动设置的首部字段（例如 &lt;code&gt;Connection&lt;/code&gt; ，&lt;code&gt;User-Agent&lt;/code&gt;）和在 &lt;code&gt;Fetch&lt;/code&gt; 规范中定义为禁用首部名称的其他首部，允许人为设置的字段为 &lt;code&gt;Fetch&lt;/code&gt; 规范定义的对 &lt;code&gt;CORS&lt;/code&gt; 安全的首部字段集合。该集合为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Accept&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Accept-Language&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Language&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Last-Event-ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Type&lt;/code&gt;：只限于三个值 &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;、&lt;code&gt;multipart/form-data&lt;/code&gt;、&lt;code&gt;text/plain&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DPR&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Downlink&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Save-Data&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Viewport-Width&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Width&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请求中的任意 &lt;code&gt;XMLHttpRequestUpload&lt;/code&gt; 对象均没有注册任何事件监听器；&lt;code&gt;XMLHttpRequestUpload&lt;/code&gt; 对象可以使用 &lt;code&gt;XMLHttpRequest.upload&lt;/code&gt; 属性访问。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请求中没有使用 &lt;code&gt;ReadableStream&lt;/code&gt; 对象。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;注意: 这些跨站点请求与浏览器发出的其他跨站点请求并无二致。如果服务器未返回正确的响应首部，则请求方不会收到任何数据。因此，那些不允许跨站点请求的网站无需为这一新的 &lt;code&gt;HTTP&lt;/code&gt; 访问控制特性担心。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;简单请求&lt;/h3&gt;
&lt;p&gt;对于简单请求，前端什么都不需要做，浏览器会自动在我们的请求头中加一个字段 &lt;code&gt;origin&lt;/code&gt; 向后端说明我们的源，服务器根据这个字段来决定是否同意该请求，如果 &lt;code&gt;Origin&lt;/code&gt; 指定的源，不在许可范围内，服务器会返回一个正常的 &lt;code&gt;HTTP&lt;/code&gt; 回应。浏览器发现，这个回应的头信息没有包含 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 字段，就知道出错了，从而抛出一个错误，被 &lt;code&gt;XMLHttpRequest&lt;/code&gt; 的 &lt;code&gt;onerror&lt;/code&gt; 回调函数捕获。注意，这种错误无法通过状态码识别，因为HTTP回应的状态码有可能是 &lt;code&gt;200&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果服务器同意该次跨域请求，那么在响应头中会多出以下字段&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; ：指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求，服务器可以指定该字段的值为通配符，表示允许来自所有域的请求。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Credentials&lt;/code&gt; ： 该字段可选。它的值是一个布尔值，表示是否允许发送 &lt;code&gt;Cookie&lt;/code&gt;。默认情况下，&lt;code&gt;Cookie&lt;/code&gt; 不包括在 &lt;code&gt;CORS&lt;/code&gt; 请求之中。设为 &lt;code&gt;true&lt;/code&gt;，即表示服务器明确许可， &lt;code&gt;Cookie&lt;/code&gt; 可以包含在请求中，一起发给服务器。这个值也只能设为 &lt;code&gt;true&lt;/code&gt;，如果服务器不要浏览器发送 &lt;code&gt;Cookie&lt;/code&gt;，删除该字段即可。该字段为 &lt;code&gt;true&lt;/code&gt; 的时候，&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 必须为一个具体的值，不能设为通配符，并且需要前端配合设置 &lt;code&gt;xhr.withCredentials = true;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt;： 该字段可选。&lt;code&gt;CORS&lt;/code&gt; 请求时，&lt;code&gt;XMLHttpRequest&lt;/code&gt; 对象的 &lt;code&gt;getResponseHeader()&lt;/code&gt; 方法只能拿到6个基本字段：&lt;code&gt;Cache-Control&lt;/code&gt;、&lt;code&gt;Content-Language&lt;/code&gt;、&lt;code&gt;Content-Type&lt;/code&gt;、&lt;code&gt;Expires&lt;/code&gt;、&lt;code&gt;Last-Modified&lt;/code&gt;、&lt;code&gt;Pragma&lt;/code&gt;。如果想拿到其他字段，就必须在&lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt;里面指定。如 &lt;code&gt;Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;简单请求的前后端示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//前端请求
document.cookie = &apos;name=clloz&apos;
var xhr = new XMLHttpRequest()
xhr.open(&apos;get&apos;, &apos;http://localhost:8080/test&apos;, true)
xhr.withCredentials = true //请求想要发送cookie必须设置withCreadentials
xhr.onload = function () {
  console.log(xhr.responseText)
}
xhr.send()

//后端代码
var http = require(&apos;http&apos;)
var url = require(&apos;url&apos;)
var querystring = require(&apos;querystring&apos;)
var util = require(&apos;util&apos;)

var routes = {
  &apos;/test&apos;: function (req, res) {
    console.log(req.method)
    if (req.method === &apos;GET&apos;) {
      console.log(req.headers.cookie)
      res.setHeader(&apos;Access-Control-Allow-Origin&apos;, &apos;http://localhost:8081&apos;)
      res.setHeader(&apos;Access-Control-Allow-Credentials&apos;, true) //允许前端发送cookie
      res.writeHead(200, &apos;Ok&apos;)
      res.write(`success`)
      res.end()
    }
  }
}

var server = http.createServer(function (req, res) {
  var pathObj = url.parse(req.url, true)
  var handleFn = routes[pathObj.pathname]
  if (handleFn) {
    handleFn(req, res)
  }
})

server.listen(8080)
console.log(&apos;server on 8080...&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;非简单请求&lt;/h3&gt;
&lt;p&gt;非简单请求是那种对服务器有特殊要求的请求，比如请求方法是 &lt;code&gt;PUT&lt;/code&gt; 或 &lt;code&gt;DELETE&lt;/code&gt;，或者 &lt;code&gt;Content-Type&lt;/code&gt; 字段的类型是 &lt;code&gt;application/json&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;非简单请求的 &lt;code&gt;CORS&lt;/code&gt;请求，会在正式通信之前，增加一次 &lt;code&gt;HTTP&lt;/code&gt; 查询请求，称为&quot;预检&quot;请求（ &lt;code&gt;preflight request&lt;/code&gt; ）。浏览器先询问服务器，当前网页所在的域名是否在服务器的许可名单之中，以及可以使用哪些 &lt;code&gt;HTTP&lt;/code&gt; 方法和头信息字段。只有得到肯定答复，浏览器才会发出正式的 &lt;code&gt;XMLHttpRequest&lt;/code&gt; 请求，否则就报错。看一个预检请求报文和响应报文：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var invocation = new XMLHttpRequest()
var url = &apos;http://bar.other/resources/post-here/&apos;
var body = &apos;&amp;#x3C;?xml version=&quot;1.0&quot;?&gt;&amp;#x3C;person&gt;&amp;#x3C;name&gt;Arun&amp;#x3C;/name&gt;&amp;#x3C;/person&gt;&apos;

function callOtherDomain() {
  if (invocation) {
    invocation.open(&apos;POST&apos;, url, true)
    invocation.setRequestHeader(&apos;X-PINGOTHER&apos;, &apos;pingpong&apos;)
    invocation.setRequestHeader(&apos;Content-Type&apos;, &apos;application/xml&apos;)
    invocation.onreadystatechange = handler
    invocation.send(body)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
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
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://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
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&quot;预检&quot;请求用的请求方法是 &lt;code&gt;OPTIONS&lt;/code&gt;，表示这个请求是用来询问的。头信息里面，关键字段是 &lt;code&gt;Origin&lt;/code&gt;，表示请求来自哪个源。除了 &lt;code&gt;Origin&lt;/code&gt; 字段，&quot;预检&quot;请求的头信息包括两个特殊字段。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Request-Method&lt;/code&gt;：该字段是必须的，用来列出浏览器的 &lt;code&gt;CORS&lt;/code&gt; 请求会用到哪些 &lt;code&gt;HTTP&lt;/code&gt; 方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Request-Headers&lt;/code&gt;：该字段是一个逗号分隔的字符串，指定浏览器 &lt;code&gt;CORS&lt;/code&gt; 请求会额外发送的头信息字段&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;服务器收到&quot;预检&quot;请求以后，检查了&lt;code&gt;Origin&lt;/code&gt;、&lt;code&gt;Access-Control-Request-Method&lt;/code&gt; 和&lt;code&gt;Access-Control-Request-Headers&lt;/code&gt; 字段以后，确认允许跨源请求，就可以做出回应。如果服务器否定了&quot;预检&quot;请求，会返回一个正常的 &lt;code&gt;HTTP&lt;/code&gt; 回应，但是没有任何 &lt;code&gt;CORS&lt;/code&gt; 相关的头信息字段。这时，浏览器就会认定，服务器不同意预检请求，因此触发一个错误，被 &lt;code&gt;XMLHttpRequest&lt;/code&gt; 对象的 &lt;code&gt;onerror&lt;/code&gt; 回调函数捕获。控制台会打印出如下的报错信息。通过的预检请求，服务器响应头中会有如下字段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt;：该字段必需，它的值是逗号分隔的一个字符串，表明服务器支持的所有跨域请求的方法。注意，返回的是所有支持的方法，而不单是浏览器请求的那个方法。这是为了避免多次&quot;预检&quot;请求。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt;：如果浏览器请求包括 &lt;code&gt;Access-Control-Request-Headers&lt;/code&gt; 字段，则 &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt; 字段是必需的。它也是一个逗号分隔的字符串，表明服务器支持的所有头信息字段，不限于浏览器在&quot;预检&quot;中请求的字段。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Credentials&lt;/code&gt;： 和简单请求中相同。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Max-Age&lt;/code&gt; : 该字段可选，用来指定本次预检请求的有效期，单位为秒。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果服务器通过了预检请求，在有效期内的正常的&lt;code&gt;CORS&lt;/code&gt;请求，就都跟简单请求一样，会有一个 &lt;code&gt;Origin&lt;/code&gt; 头信息字段。服务器的回应，也都会有一个 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 头信息字段。&lt;/p&gt;
&lt;p&gt;非简单请求的示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//前端代码
var json = {
  name: &apos;clloz&apos;,
  age: &apos;27&apos;,
  sex: &apos;male&apos;
}
document.cookie = &apos;name=clloz&apos;
var xhr = new XMLHttpRequest()
xhr.open(&apos;post&apos;, &apos;http://localhost:8080/test&apos;, true)
xhr.setRequestHeader(&apos;content-type&apos;, &apos;application/json&apos;)
xhr.withCredentials = true
xhr.onload = function () {
  console.log(xhr.responseText)
}
xhr.send(json)

//后端代码
var http = require(&apos;http&apos;)
var url = require(&apos;url&apos;)
var querystring = require(&apos;querystring&apos;)
var util = require(&apos;util&apos;)

var routes = {
  &apos;/test&apos;: function (req, res) {
    console.log(req.method)
    if (req.method === &apos;GET&apos;) {
      console.log(req.headers.cookie)
      res.setHeader(&apos;Access-Control-Allow-Origin&apos;, &apos;http://localhost:8081&apos;)
      res.setHeader(&apos;Access-Control-Allow-Credentials&apos;, true)
      res.writeHead(200, &apos;Ok&apos;)
      res.write(`success`)
      res.end()
    } else {
      var post = &apos;&apos;
      req.on(&apos;data&apos;, function (chunk) {
        post += chunk
      })
      req.on(&apos;end&apos;, function () {
        res.setHeader(&apos;Access-Control-Allow-Origin&apos;, &apos;http://localhost:8081&apos;)
        res.setHeader(&apos;Access-Control-Allow-Credentials&apos;, true)
        res.setHeader(&apos;Access-Control-Request-Method&apos;, &apos;PUT,POST,GET,DELETE,OPTIONS&apos;)
        res.setHeader(
          &apos;Access-Control-Allow-Headers&apos;,
          &apos;Origin, X-Requested-With, Content-Type, Accept, t&apos;
        )
        res.end(&apos;success&apos;)
      })
    }
  }
}

var server = http.createServer(function (req, res) {
  var pathObj = url.parse(req.url, true)
  var handleFn = routes[pathObj.pathname]
  if (handleFn) {
    handleFn(req, res)
  }
})

server.listen(8080)
console.log(&apos;server on 8080...&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大多数浏览器不支持针对于预检请求的重定向。如果一个预检请求发生了重定向，浏览器将报告错误。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;XMLHttpRequest&lt;/code&gt; 或 &lt;code&gt;Fetch&lt;/code&gt; 与 &lt;code&gt;CORS&lt;/code&gt; 的一个有趣的特性是，可以基于 &lt;code&gt;HTTP cookies&lt;/code&gt; 和 &lt;code&gt;HTTP&lt;/code&gt; 认证信息发送身份凭证。一般而言，对于跨源 &lt;code&gt;XMLHttpRequest&lt;/code&gt; 或 &lt;code&gt;Fetch&lt;/code&gt; 请求，浏览器不会发送身份凭证信息。如果要发送凭证信息，需要设置 &lt;code&gt;XMLHttpRequest&lt;/code&gt; 的某个特殊标志位 &lt;code&gt;withCredentials&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;。如果服务器端的响应中未携带 &lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt; ，浏览器将不会把响应内容返回给请求的发送者。&lt;/p&gt;
&lt;p&gt;对于附带身份凭证的请求，服务器不得设置 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 的值为 &lt;code&gt;*&lt;/code&gt;（会有安全风险）。请求的首部中携带了 &lt;code&gt;Cookie&lt;/code&gt; 信息，如果 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 的值为&lt;code&gt;*&lt;/code&gt;，请求将会失败。而将 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 的值设置为请求的源，则请求将成功执行。&lt;/p&gt;
&lt;p&gt;另外，响应首部中也携带了 &lt;code&gt;Set-Cookie&lt;/code&gt; 字段，尝试对 &lt;code&gt;Cookie&lt;/code&gt; 进行修改。如果操作失败，将会抛出异常。&lt;/p&gt;
&lt;h2&gt;代理&lt;/h2&gt;
&lt;p&gt;同源策略只是浏览器的限制，对于服务器上的 &lt;code&gt;web&lt;/code&gt; 服务器是没有影响的，所以当我们需要请求跨域资源的时候，可以先向同源的 &lt;code&gt;web&lt;/code&gt; 服务器提交请求，由 &lt;code&gt;web&lt;/code&gt; 服务器再向对应的服务器请求到数据后返回给前端。&lt;/p&gt;
&lt;h2&gt;postMessage&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;window.postMessage()&lt;/code&gt; 方法可以安全地实现跨源通信。使用方法是获得对另一个窗口的引用（比如&lt;code&gt;targetWindow = window.opener&lt;/code&gt;），然后在窗口上调用 &lt;code&gt;targetWindow.postMessage()&lt;/code&gt; 方法分发一个 &lt;code&gt;MessageEvent&lt;/code&gt; 消息。接收消息的窗口可以根据需要自由处理此事件。传递给 &lt;code&gt;window.postMessage()&lt;/code&gt; 的参数（比如 &lt;code&gt;message&lt;/code&gt; ）将通过消息事件对象暴露给接收消息的窗口。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;postMessage&lt;/code&gt; 的语法是 &lt;code&gt;otherWindow.postMessage(message, targetOrigin, [transfer]);&lt;/code&gt;，&lt;code&gt;otherWindow&lt;/code&gt; 是其他窗口的引用，将要发送到其他 &lt;code&gt;window&lt;/code&gt; 的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;targetOrigin&lt;/code&gt; 指定哪些窗口能接收到消息事件，其值可以是字符串 &lt;code&gt;*&lt;/code&gt;（表示无限制）或者一个 &lt;code&gt;URI&lt;/code&gt; 。在发送消息的时候，如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 &lt;code&gt;targetOrigin&lt;/code&gt; 提供的值，那么消息就不会被发送，有三者完全匹配，消息才会被发送。&lt;/p&gt;
&lt;p&gt;这个机制用来控制消息可以发送到哪些窗口；例如，当用 &lt;code&gt;postMessage&lt;/code&gt; 传送密码时，这个参数就显得尤为重要，必须保证它的值与这条包含密码的信息的预期接受者的 &lt;code&gt;origin&lt;/code&gt; 属性完全一致，来防止密码被恶意的第三方截获。如果你明确的知道消息应该发送到哪个窗口，那么请始终提供一个有确切值的 &lt;code&gt;targetOrigin&lt;/code&gt;，而不是 &lt;code&gt;*&lt;/code&gt;。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;transfer&lt;/code&gt; 是一个可选参数，是一串和 &lt;code&gt;message&lt;/code&gt; 同时传递的 &lt;code&gt;Transferable&lt;/code&gt; 对象. 这些对象的所有权将被转移给消息的接收方，而发送一方将不再保有所有权。&lt;/p&gt;
&lt;p&gt;以上是发送发的 &lt;code&gt;API&lt;/code&gt; 使用发放，接收方的使用方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;window.addEventListener(&apos;message&apos;, receiveMessage, false)

function receiveMessage(event) {
  // For Chrome, the origin property is in the event.originalEvent
  // object.
  // 这里不准确，chrome没有这个属性
  // var origin = event.origin || event.originalEvent.origin;
  var origin = event.origin
  if (origin !== &apos;http://example.org:8080&apos;) return

  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接收方监听 &lt;code&gt;message&lt;/code&gt; 事件，回到函数 &lt;code&gt;receiveMessage&lt;/code&gt; 的 &lt;code&gt;event&lt;/code&gt; 中可以取到 &lt;code&gt;data&lt;/code&gt;，发送过来的数据；&lt;code&gt;origin&lt;/code&gt;，发送方的源，由协议，域名和端口组成（&lt;code&gt;http&lt;/code&gt; 默认为 &lt;code&gt;80&lt;/code&gt; 端口，&lt;code&gt;https&lt;/code&gt; 默认为 &lt;code&gt;443&lt;/code&gt; 端口）；&lt;code&gt;source&lt;/code&gt; 是对发送消息的窗口对象的引用，可以使用此来在具有不同 &lt;code&gt;origin&lt;/code&gt; 的两个窗口之间建立双向通信。&lt;/p&gt;
&lt;p&gt;下面是一个例子：用 &lt;code&gt;http-server&lt;/code&gt; 启动两个服务来测试，分别为 &lt;code&gt;localhost:8080&lt;/code&gt; 和 &lt;code&gt;localhost:8081&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- localhost:8080 --&gt;
&amp;#x3C;body&gt;
  &amp;#x3C;button&gt;btn&amp;#x3C;/button&gt;
  &amp;#x3C;iframe name=&quot;myframe&quot; src=&quot;http://localhost:8081&quot; frameborder=&quot;1&quot;&gt;&amp;#x3C;/iframe&gt;
  &amp;#x3C;script&gt;
    window.addEventListener(&apos;message&apos;, function (e) {
      if (e.origin === &apos;http://localhost:8081&apos;) {
        console.log(e.data)
      }
    })

    var iframe = window.frames[&apos;myframe&apos;]

    var btn = document.querySelector(&apos;button&apos;)
    btn.addEventListener(&apos;click&apos;, function () {
      iframe.postMessage(&apos;this is 8080&apos;, &apos;http://localhost:8081&apos;)
    })
  &amp;#x3C;/script&gt;
&amp;#x3C;/body&gt;

&amp;#x3C;!-- localhost:8081 --&gt;
&amp;#x3C;body&gt;
  this is frame!
  &amp;#x3C;script&gt;
    window.addEventListener(&apos;message&apos;, function (e) {
      if (e.origin === &apos;http://localhost:8080&apos;) {
        console.log(e.data)
        e.source.postMessage(&apos;this is 8081&apos;, e.origin)
      }
    })
  &amp;#x3C;/script&gt;
&amp;#x3C;/body&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点击第一个页面的按钮，会向第二页面发送消息，第二个页面收到消息会立即返回。&lt;/p&gt;
&lt;h2&gt;document.domain&lt;/h2&gt;
&lt;p&gt;这种方式只适合主域名相同，但子域名不同的 &lt;code&gt;iframe&lt;/code&gt; 跨域。比如主域名是&lt;code&gt;http://clloz.com&lt;/code&gt;，子域名是&lt;code&gt;http://test.crossdomain.com&lt;/code&gt;，这种情况下给两个页面指定一下&lt;code&gt;document.domain&lt;/code&gt;即&lt;code&gt;document.domain = clloz.com&lt;/code&gt;就可以访问各自的&lt;code&gt;window&lt;/code&gt;对象了。&lt;/p&gt;
&lt;h2&gt;location.hash + iframe跨域&lt;/h2&gt;
&lt;p&gt;实现原理： &lt;code&gt;a&lt;/code&gt; 欲与 &lt;code&gt;b&lt;/code&gt; 跨域相互通信，通过中间页 &lt;code&gt;c&lt;/code&gt; 来实现。 三个页面，不同域之间利用 &lt;code&gt;iframe&lt;/code&gt; 的 &lt;code&gt;location.hash&lt;/code&gt; 传值，相同域之间直接 &lt;code&gt;js&lt;/code&gt; 访问来通信。具体实现：&lt;code&gt;A&lt;/code&gt;域：&lt;code&gt;a.html&lt;/code&gt; -&gt; &lt;code&gt;B&lt;/code&gt; 域：&lt;code&gt;b.html&lt;/code&gt; -&gt; &lt;code&gt;A&lt;/code&gt; 域：&lt;code&gt;c.html&lt;/code&gt;，&lt;code&gt;a&lt;/code&gt; 与 &lt;code&gt;b&lt;/code&gt; 不同域只能通过 &lt;code&gt;hash&lt;/code&gt; 值单向通信，&lt;code&gt;b&lt;/code&gt; 与 &lt;code&gt;c&lt;/code&gt; 也不同域也只能单向通信，但 &lt;code&gt;c&lt;/code&gt; 与 &lt;code&gt;a&lt;/code&gt; 同域，所以 &lt;code&gt;c&lt;/code&gt; 可通过 &lt;code&gt;parent.parent&lt;/code&gt; 访问a页面所有对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- a.html --&gt;
&amp;#x3C;iframe id=&quot;iframe&quot; src=&quot;http://www.domain2.com/b.html&quot; style=&quot;display:none;&quot;&gt;&amp;#x3C;/iframe&gt;
&amp;#x3C;script&gt;
  var iframe = document.getElementById(&apos;iframe&apos;)

  // 向b.html传hash值
  setTimeout(function () {
    iframe.src = iframe.src + &apos;#user=admin&apos;
  }, 1000)

  // 开放给同域c.html的回调方法
  function onCallback(res) {
    alert(&apos;data from c.html ---&gt; &apos; + res)
  }
&amp;#x3C;/script&gt;

&amp;#x3C;!-- b.html --&gt;
&amp;#x3C;iframe id=&quot;iframe&quot; src=&quot;http://www.domain1.com/c.html&quot; style=&quot;display:none;&quot;&gt;&amp;#x3C;/iframe&gt;
&amp;#x3C;script&gt;
  var iframe = document.getElementById(&apos;iframe&apos;)

  // 监听a.html传来的hash值，再传给c.html
  window.onhashchange = function () {
    iframe.src = iframe.src + location.hash
  }
&amp;#x3C;/script&gt;

&amp;#x3C;!-- c.html --&gt;
&amp;#x3C;iframe id=&quot;iframe&quot; src=&quot;http://www.domain1.com/c.html&quot; style=&quot;display:none;&quot;&gt;&amp;#x3C;/iframe&gt;
&amp;#x3C;script&gt;
  var iframe = document.getElementById(&apos;iframe&apos;)

  // 监听a.html传来的hash值，再传给c.html
  window.onhashchange = function () {
    iframe.src = iframe.src + location.hash
  }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WebScoket&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WebSocket protocol&lt;/code&gt; 是 &lt;code&gt;HTML5&lt;/code&gt; 一种新的协议。它实现了浏览器与服务器全双工通信，同时允许跨域通讯，是 &lt;code&gt;server push&lt;/code&gt; 技术的一种很好的实现。 原生 &lt;code&gt;WebSocket API&lt;/code&gt; 使用起来不太方便，可以使用 &lt;code&gt;Socket.io&lt;/code&gt;，它很好地封装了 &lt;code&gt;WebSocket&lt;/code&gt; 接口，提供了更简单、灵活的接口，也对不支持 &lt;code&gt;WebSocket&lt;/code&gt; 的浏览器提供了向下兼容。&lt;/p&gt;
&lt;h2&gt;window.name&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;window.name&lt;/code&gt; 有一个奇妙的性质，页面如果设置了&lt;code&gt;window.name&lt;/code&gt;，那么在不关闭页面的情况下，即使进行了页面跳转 &lt;code&gt;location.href=...&lt;/code&gt;，这个 &lt;code&gt;window.name&lt;/code&gt; 还是会保留。&lt;/p&gt;
&lt;p&gt;所以我们可以利用这一点，和跨域页面通信。当我们在 &lt;code&gt;a.html&lt;/code&gt; 要访问一个跨域页面 &lt;code&gt;b.html&lt;/code&gt;，此时我们可以在 &lt;code&gt;a.html&lt;/code&gt; 中用一个 &lt;code&gt;iframe&lt;/code&gt; 加载 &lt;code&gt;b.html&lt;/code&gt;。&lt;code&gt;b.html&lt;/code&gt; 加载时要自动设置 &lt;code&gt;window.name&lt;/code&gt;，存放我们要传递的信息，然后进行跳转，跳转到一个和 &lt;code&gt;a.html&lt;/code&gt; 同源的页面 &lt;code&gt;c.html&lt;/code&gt;，此时由于 &lt;code&gt;c.html&lt;/code&gt; 和 &lt;code&gt;a.html&lt;/code&gt; 同源，我们可以拿到 &lt;code&gt;iframe&lt;/code&gt; 的 &lt;code&gt;$(&apos;iframe&apos;).contentWindow&lt;/code&gt;，这就是 &lt;code&gt;iframe&lt;/code&gt; 的 &lt;code&gt;window&lt;/code&gt; 对象，此时我们就取到了 &lt;code&gt;b&lt;/code&gt; 中设置的 &lt;code&gt;window.name&lt;/code&gt;。实际操作中，我们一般使用一个隐藏的 &lt;code&gt;iframe&lt;/code&gt;，然后监听它第二次 &lt;code&gt;onload&lt;/code&gt; 事件，就知道该 &lt;code&gt;iframe&lt;/code&gt; 已经跳到同域页面了，然后使用 &lt;code&gt;$(&apos;iframe&apos;).contentWindow.name&lt;/code&gt; 即可。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy&quot; title=&quot;same-origin policy&quot;&gt;same-origin policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html&quot; title=&quot;浏览器同源策略&quot;&gt;浏览器同源策略&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS&quot; title=&quot;CORS-MDN&quot;&gt;CORS-MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2016/04/cors.html&quot; title=&quot;CORS-阮一峰&quot;&gt;CORS-阮一峰&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000015597029&quot; title=&quot;不要再问我跨域的问题了&quot;&gt;不要再问我跨域的问题了&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844903882083024910&quot; title=&quot;9种常见的前端跨域解决方案（详解）&quot;&gt;9种常见的前端跨域解决方案（详解）&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/browser.gq-LwpuH.jpg"/><enclosure url="/_astro/browser.gq-LwpuH.jpg"/></item><item><title>实现一个简单的音乐播放器</title><link>https://clloz.com/blog/musicplayer</link><guid isPermaLink="true">https://clloz.com/blog/musicplayer</guid><pubDate>Mon, 06 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;[toc]&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;花了一个下午带晚上的事件做了一个简单的音乐播放器，大部分时间都花在了 &lt;code&gt;audio&lt;/code&gt; 的自动播放问题上，原来浏览器只是不允许在移动设备上自动播放，从 &lt;code&gt;chrome66&lt;/code&gt; 开始 &lt;code&gt;PC&lt;/code&gt; 端也不允许自动播放了，&lt;code&gt;audio.play()&lt;/code&gt; 必须写在点击事件的回调函数中才能生效，不然就会报错 &lt;code&gt;DOMException: play() failed because the user didn&apos;t interact with the document first.&lt;/code&gt;，也就是说现在想要让页面上的音频播放必须经过用户点击以后才行，不能不经过和用户的互动直接播放了。这方面的资料比较少，&lt;code&gt;chrome&lt;/code&gt; 有一篇文档说明&lt;a href=&quot;https://developers.google.com/web/updates/2017/09/autoplay-policy-changes&quot; title=&quot;autoplay policy changes&quot;&gt;autoplay policy changes&lt;/a&gt;。除开这个问题，其他功能并没有遇到太大的问题，样式的事件花的多一点，播放器的逻辑比较简单，JS基本都是操作样式和 &lt;code&gt;Audio&lt;/code&gt; 对象，下面来分享以下构思和实现的过程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;HTMLMediaElement&lt;/code&gt; 的属性、方法和事件很多，建议大家到&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt;仔细阅读以下文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;项目预览&lt;/h2&gt;
&lt;p&gt;项目完成后我放在了服务器上，访问地址是&lt;a href=&quot;https://www.clloz.com/study/musicPlayer/index.html&quot; title=&quot;music player&quot;&gt;音乐播放器&lt;/a&gt;，&lt;code&gt;GitHub&lt;/code&gt; 地址 &lt;a href=&quot;https://github.com/Clloz/musicPlayer&quot; title=&quot;音乐播放器-github&quot;&gt;音乐播放器-github&lt;/a&gt;。其实本来是想在 &lt;code&gt;GitHub Pages&lt;/code&gt;上也能够预览的，配置了 &lt;code&gt;apache&lt;/code&gt; 的跨域访问，但是在请求阿里云的静态资源（专辑封面和歌曲）的时候一直403，明明以及把 &lt;code&gt;GitHub Pages&lt;/code&gt; 的地址加入到白名单了，试了半天只能放弃了。下面是整个页面的预览图片：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/screenshot.BocS_raV_1PBXYt.webp&quot; alt=&quot;screenshot&quot; title=&quot;screenshot&quot;&gt;&lt;/p&gt;
&lt;h2&gt;项目需求&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;页面设计：歌曲封面，歌曲名，进度条，音量控制，歌曲播放控制（上一曲，下一曲，播放暂停），歌曲列表。&lt;/li&gt;
&lt;li&gt;初始化：打开页面时需要根据后台的数据生成播放列表，以及初始化播放控件（进度条归零，初始化音量，待播放歌曲设置为第一首，显示第一首歌的名称和时间）&lt;/li&gt;
&lt;li&gt;播放逻辑，点击播放暂停，上一首下一首和直接点击歌曲列表的逻辑&lt;/li&gt;
&lt;li&gt;给播放列表做一个打开和关闭的动画&lt;/li&gt;
&lt;li&gt;进度条控制：让用户可以自己选择歌曲的进度&lt;/li&gt;
&lt;li&gt;音量控制&lt;/li&gt;
&lt;li&gt;断网控制&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;页面结构和样式&lt;/h2&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;背景比较简单，我选了一张自己比较喜欢的龙猫的图片，设置了一个 &lt;code&gt;filter: blur(5px)&lt;/code&gt;，效果还不错。&lt;/p&gt;
&lt;h2&gt;播放器主体&lt;/h2&gt;
&lt;p&gt;播放器主体就参照一般的音乐播放器，左边是歌曲封面，右边是播放控件，用&lt;code&gt;display: inline-block&lt;/code&gt;，播放器的宽高都是固定的，&lt;code&gt;inline-block&lt;/code&gt; 的空格空隙问题我是用 &lt;code&gt;font-size: 0&lt;/code&gt;来处理的。需要注意的是要设置以下图片或者右边元素的 &lt;code&gt;vertical-align&lt;/code&gt;，因为图片下边缘默认与当前 &lt;code&gt;inline box&lt;/code&gt; 中的基线对齐的，如果不设置 &lt;code&gt;vertical-align&lt;/code&gt; 图片默认和右边元素中的最后一行文本的基线对齐。结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;img src=&quot;&quot; alt=&quot;cover&quot; class=&quot;left&quot; /&gt;
&amp;#x3C;div class=&quot;right&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;右侧的播放器控件部分就分为上中下三块，最上面显示歌曲信息，中间一层是进度条和音量按钮，最下面是几个播放按钮。结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;h4&gt;music info&amp;#x3C;/h4&gt;
&amp;#x3C;div class=&quot;mid&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;div class=&quot;controls&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最上面的歌曲信息部分没啥好说的，设置合适的字体就可以了，&lt;code&gt;white-space: nowrap&lt;/code&gt;，&lt;code&gt;text-overflow: ellipsis&lt;/code&gt;，处理歌曲信息过长。中间一层我放了进度条和音量控制，进度条就是总长度一个元素，当前长度一个元素，两个元素选择不同的颜色重叠就可以了，时间我用的是总长度的进度条元素的伪元素来实现的，伪元素的 &lt;code&gt;content&lt;/code&gt; 用进度条元素的属性来设置 &lt;code&gt;content = attr(data-time)&lt;/code&gt;。 本来我是想当前进度也用伪元素来实现的，但是伪元素并不算是 &lt;code&gt;DOM&lt;/code&gt; 元素，无法用 &lt;code&gt;JS&lt;/code&gt; 来操作，并且没法添加事件（虽然有各种黑科技）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;理论上来说伪元素不算 &lt;code&gt;DOM&lt;/code&gt; 元素，所以无法用JS操作以及绑定事件，不过我看到 &lt;code&gt;segmentfault&lt;/code&gt;上有两个答案对这两种行为都提出了解答，&lt;a href=&quot;https://segmentfault.com/q/1010000002452755&quot; title=&quot;通过JS改变伪元素样式&quot;&gt;通过JS改变伪元素样式&lt;/a&gt;, &lt;a href=&quot;https://segmentfault.com/q/1010000003759156/&quot; title=&quot;pointer-event为伪元素绑定事件&quot;&gt;pointer-event为伪元素绑定事件&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;中间这一层我直接给包裹层一个高度，内部的几个块都用绝对定位，保持两个进度条的位置一致，并且和音量键对齐。同时音量的控制模块也用绝对定位。用绝对定位是为了让这几个元素对齐比较方便，特别是音量和进度条都是两个元素重叠在一起，并且两个元素都是需要点击计算距离的，所以 &lt;code&gt;1px&lt;/code&gt; 的误差都不能有，用绝对定位比较好处理。结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;mid&quot;&gt;
  &amp;#x3C;div class=&quot;progress&quot; data-time=&quot;00:00/00:00&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;current&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;volume&quot;&gt;
    &amp;#x3C;div class=&quot;vol-control&quot;&gt;&amp;#x3C;/div&gt;
    &amp;#x3C;div class=&quot;vol-current&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;vol&quot;&gt;
    &amp;#x3C;span&gt;&amp;#x3C;i class=&quot;fa fa-volume-up&quot;&gt;&amp;#x3C;/i&gt;&amp;#x3C;/span&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最下面的一层控制按钮，直接用列表 &lt;code&gt;inline-block&lt;/code&gt; 就可以了，计算好宽度间隔。对于列表的横向排列，我们经常希望第一个元素和边界没有间隔，后面的元素留有相同的空间，处理方法有很多种。 1. 用 &lt;code&gt;flex&lt;/code&gt; 布局 2. 对每个 &lt;code&gt;li&lt;/code&gt; 使用相同的 &lt;code&gt;margin-left&lt;/code&gt;， 然后 &lt;code&gt;ul&lt;/code&gt; 使用一个负 &lt;code&gt;margin&lt;/code&gt;，这样做 &lt;code&gt;ul&lt;/code&gt; 的宽度需要增加一个间隙的值 3. 使用 &lt;code&gt;li + li&lt;/code&gt; 选择器，选择所有 &lt;code&gt;li&lt;/code&gt;的相邻兄弟元素，第一个元素不会被选到 4. 使用 &lt;code&gt;li:first-child&lt;/code&gt; 给第一个元素单独设置样式 5. 使用&lt;code&gt;li:first-child ~ li&lt;/code&gt; 选择第一个元素的所有兄弟元素&lt;/p&gt;
&lt;p&gt;最后每个元素添加一个 &lt;code&gt;hover&lt;/code&gt; 样式，然后加上一个 &lt;code&gt;transition: all .3s ease-in-out;&lt;/code&gt; 给 &lt;code&gt;hover&lt;/code&gt; 效果添加一个过度状态。&lt;/p&gt;
&lt;h2&gt;歌曲列表&lt;/h2&gt;
&lt;p&gt;歌曲列表的样式并没有什么难的地方，一个列表就可以解决。不过我想给歌曲列表的打开和关闭制作一个动画，当打开的时候，歌曲列表从播放器主体下方滑出；当关闭的时候，列表缓慢上移，上移的部分在主体下方消失，效果如下面的动图：&lt;/p&gt;
&lt;p&gt;上移的效果非常容易，&lt;code&gt;transform: translateY(-300px)&lt;/code&gt; 就可以了，但是上移的部分还是会显示在画面中，并且挡住整个主体了。我的解决方案是在 &lt;code&gt;ul&lt;/code&gt; 外面再套一层 &lt;code&gt;div&lt;/code&gt;，&lt;code&gt;div&lt;/code&gt; 设置一个 &lt;code&gt;overflow: hidden;&lt;/code&gt;，在 &lt;code&gt;ul&lt;/code&gt; 向上移动的过程中 &lt;code&gt;div&lt;/code&gt; 的高度也同步减小，保持两者的 &lt;code&gt;transition&lt;/code&gt; 的时间相同，这样两者的过度效果能保持相同。因为 &lt;code&gt;overflow: hidden&lt;/code&gt; 的存在，&lt;code&gt;ul&lt;/code&gt; 滑动到上方的部分将会被隐藏。具体代码看上面的 &lt;code&gt;GitHub&lt;/code&gt; 链接。&lt;/p&gt;
&lt;h2&gt;功能实现&lt;/h2&gt;
&lt;p&gt;所有功能基本都围绕 &lt;code&gt;HTMLMediaElement&lt;/code&gt; 的属性、方法和事件来实现，基本都是对 &lt;code&gt;Audio&lt;/code&gt; 的操作，说以下主要思路。&lt;/p&gt;
&lt;h2&gt;播放逻辑&lt;/h2&gt;
&lt;p&gt;上一曲，下一曲和点击播放列表中的歌曲逻辑基本是相同的。在请求到歌曲信息后保存到一个对象中，除了将对象中的信息添加到歌曲列表中之外，也是我们操作按钮的时候取数据的地方。我们需要一个用来标记当前播放歌曲 &lt;code&gt;index&lt;/code&gt; 的变量，初始化为 &lt;code&gt;0&lt;/code&gt;，也就是打开播放器的时候默认从第一首开始播放。然后点击上一首就对变量 &lt;code&gt;+1&lt;/code&gt;（需要判断是不是第一首，如果是第一首就跳到最后一首），点击下一曲就对变量 &lt;code&gt;-1&lt;/code&gt;（需要判断是不是最后一首，如果是最后一首就跳到第一首），点击歌曲列表直接通过 &lt;code&gt;li&lt;/code&gt; 的 &lt;code&gt;data-num&lt;/code&gt; 属性来设置变量的值。剩下的初始化进度条，名称，时间等功能都是一样的，可以写到一个函数里，分别调用就可以了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;重复点击同一首歌曲默认不操作，也就是歌曲的 &lt;code&gt;data-num&lt;/code&gt; 和 当前播放的歌曲的 &lt;code&gt;index&lt;/code&gt; 相同的时候不处理，但是当页面初始化完成直接点击第一首歌的时候 &lt;code&gt;data-num&lt;/code&gt; 和 &lt;code&gt;index&lt;/code&gt; 都是 &lt;code&gt;0&lt;/code&gt;，这个逻辑需要处理一下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;播放和暂停时候按钮的样式不同，点击的时候需要处理一下，包括上面的第三个按钮点击的时候一样要看看播放按钮是否处于暂停的样式。&lt;/p&gt;
&lt;p&gt;播放逻辑中要用到的 &lt;code&gt;Audio&lt;/code&gt; 方法基本就是 &lt;code&gt;audio.play()&lt;/code&gt; 和 &lt;code&gt;audio.pause()&lt;/code&gt;，属性就是一个 &lt;code&gt;audio.src&lt;/code&gt;，需要注意的是 &lt;code&gt;audio&lt;/code&gt; 对象在一首歌播放结束后就会暂停，触发 &lt;code&gt;ended&lt;/code&gt; 事件，此时我们要触发一下下一首的按钮就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;audio.onended = function () {
  var event = new Event(&apos;click&apos;)
  nextBtn.dispatchEvent(event)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;点击按钮获取歌曲信息的方式有两种，一种按照 &lt;code&gt;index&lt;/code&gt; 到歌曲信息数组中去取；第二种是在生成歌曲列表的时候用 &lt;code&gt;data-*&lt;/code&gt; 属性将信息保存到每个歌曲对应的 &lt;code&gt;li&lt;/code&gt; 中。从操作上来说第一种方法是比较简单的，但可能播放器逻辑更复杂的时候第二种方法也有用武之地，比如用户有多个播放列表，后台给我们的是一个数组，数组中每个对象有一个 &lt;code&gt;type&lt;/code&gt; 属性来标记歌单，可能第二种方法会更好一点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;进度条&lt;/h2&gt;
&lt;p&gt;进度条要处理的主要是三个方面：播放新的歌曲需要初始化进度条和时间，跟随歌曲播放的进度前进，可以让用户手动设置进度。&lt;/p&gt;
&lt;p&gt;每次用户点击上一曲下一曲或者播放列表的时候就需要初始化进度条，我们可以用 &lt;code&gt;audio.oncanplay&lt;/code&gt; 事件来监听，这个事件是当 &lt;code&gt;audio&lt;/code&gt; 已经加载可以开始播放的时候触发，当这个事件触发的时候我们将时间归零，进度条的长度重置为 &lt;code&gt;0&lt;/code&gt; 就可以了。&lt;/p&gt;
&lt;p&gt;播放进度我们可以用 &lt;code&gt;audio.ontimeupdate&lt;/code&gt; 事件来监听，在 &lt;code&gt;Chrome&lt;/code&gt; 上这个事件一秒触发四次，每次触发我们就获取 &lt;code&gt;audio.currentTime&lt;/code&gt; 属性，然后根据 &lt;code&gt;currenTime&lt;/code&gt; 和 &lt;code&gt;duration&lt;/code&gt; 之间的比值来设置我们的进度条长度即可，同时也可以设置时间，&lt;code&gt;currentTime&lt;/code&gt; 和 &lt;code&gt;duration&lt;/code&gt; 都是以秒为单位的浮点数，我们进行一些处理就行了。&lt;/p&gt;
&lt;p&gt;点击进度条我们利用 &lt;code&gt;MouseEvent&lt;/code&gt; 对象的 &lt;code&gt;offsetX&lt;/code&gt; 属性来获取鼠标事件触发时，鼠标的坐标和触发事件的对象的左边缘在 &lt;code&gt;X&lt;/code&gt; 轴上的偏移量，获得这个偏移量之后，我们将进度条的宽度移动至此，并且根据进度条长度和总长度的比例计算出 &lt;code&gt;currenTime&lt;/code&gt;，然后设置 &lt;code&gt;currentTime&lt;/code&gt;。&lt;code&gt;currentTime&lt;/code&gt; 是一个可以赋值的属性，并且它的值的改变也会触发 &lt;code&gt;timeupdate&lt;/code&gt; 事件。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;音量的控制本来是想使用滑动的方式来处理的，奈何水平不行，对滑动的几个事件处理怎么都做到平滑，要改进这个功能要把滑动相关的几个事件深入了解一下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;音量控制&lt;/h2&gt;
&lt;p&gt;音量控制的原理本质上跟进度条一样的，只不过因为音量是竖直方向上的，所以用 &lt;code&gt;offsetY&lt;/code&gt; 来获取偏移量，通过比例的计算来设置 &lt;code&gt;audio.volume&lt;/code&gt; 属性即可，这个控件在手机上不生效，手机还是要通过自身的音量按钮来控制声音大小。&lt;/p&gt;
&lt;h2&gt;断网控制&lt;/h2&gt;
&lt;p&gt;当断网时，如果继续执行页面逻辑或出现请求不到资源，图片加载不出，页面报错。所以在断网发生时我们需要立即处理。通过 &lt;code&gt;window&lt;/code&gt; 对象的 &lt;code&gt;offine&lt;/code&gt; 和 &lt;code&gt;online&lt;/code&gt; 事件我们可以很好地处理这个逻辑。当断网发生时我们应该禁止用户点击控件和歌曲列表，如果用解绑事件的方式太麻烦了，我直接采取用一个遮罩层盖住控件，收起播放列表，暂停当前播放，在遮罩层上显示一个断网信息。当网络恢复的时候我们再打开播放列表，如果断网前音乐是在播放的，那我们就恢复播放。效果如下:&lt;/p&gt;
&lt;h2&gt;思维拓展&lt;/h2&gt;
&lt;p&gt;要制作一个比较完善的播放器还有几个功能需要加上： 1. 显示歌词 2. 歌曲搜索&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;虽然只是一个小小的音乐播放器，不过还是补充了不少知识的，当程序员永远都是 &lt;code&gt;纸上得来终觉浅，绝知此事要躬行&lt;/code&gt; 啊，不过纸上得来的的也是多多益善，没有这个纸做铺垫，写再多代码也只是原地踏步。&lt;/p&gt;</content:encoded><h:img src="/_astro/musicplayer.PbM0c0OF.png"/><enclosure url="/_astro/musicplayer.PbM0c0OF.png"/></item><item><title>CentOS7更新git版本</title><link>https://clloz.com/blog/centos7-git-update</link><guid isPermaLink="true">https://clloz.com/blog/centos7-git-update</guid><pubDate>Fri, 03 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;今天在服务器上部署代码的时候，发现服务器上的 &lt;code&gt;git&lt;/code&gt; 版本非常老，是 &lt;code&gt;git 1.8.0&lt;/code&gt;，于是就把 &lt;code&gt;git&lt;/code&gt; 的版本更新到了最新的 &lt;code&gt;2.21.0&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;更新步骤&lt;/h2&gt;
&lt;h2&gt;卸载旧版本&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yum remove git -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装依赖&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel asciidoc gcc perl-ExtUtils-MakeMaker -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编译安装libiconv&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wget http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.15.tar.gz
tar zxvf libiconv-1.15.tar.gz
cd libiconv-1.15
./configure --prefix=/usr/local/libiconv
make &amp;#x26;&amp;#x26; make install
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;下载安装最新版本git&lt;/h2&gt;
&lt;p&gt;下载地址：&lt;a href=&quot;https://github.com/git/git/releases&quot; title=&quot;release&quot;&gt;release&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wget https://github.com/git/git/archive/v2.21.0.tar.gz
tar xzvf v2.17.0.tar.gz
cd git-2.17.0
make configure
./configure --prefix=/usr/local/git --with-iconv=/usr/local/libiconv
make install
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置环境变量&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;export PATH=$PATH:/usr/local/git/bin&quot; &gt;&gt; /etc/profile
source /etc/profile

echo &quot;export PATH=$PATH:/usr/local/git/bin&quot; &gt;&gt; /etc/bashrc
source /etc/bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成上面的步骤就已经可以用 &lt;code&gt;git --version&lt;/code&gt; 看到最新版本的 &lt;code&gt;git&lt;/code&gt; 安装好了。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>浏览器存储方案</title><link>https://clloz.com/blog/cookie-session-localstorage</link><guid isPermaLink="true">https://clloz.com/blog/cookie-session-localstorage</guid><pubDate>Thu, 02 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;浏览器有多种存储数据的办法，包括 &lt;code&gt;Cookie&lt;/code&gt;，&lt;code&gt;web storage&lt;/code&gt;（ &lt;code&gt;localStorage&lt;/code&gt; 和 &lt;code&gt;sessionStorage&lt;/code&gt; ），&lt;code&gt;indexedDB&lt;/code&gt;。它们的主要目的都是快速访问数据而不需要建立请求去服务器获取。它们有着不一样的大小，生命周期和作用域，所以需要在合适的场景选择合适的数据存储办法。&lt;/p&gt;
&lt;h2&gt;Cookie&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cookie&lt;/code&gt; 是为了解决 &lt;code&gt;HTTP&lt;/code&gt; 协议的无状态而引入的技术。&lt;code&gt;HTTP Cookie&lt;/code&gt;（也叫 &lt;code&gt;Web Cookie&lt;/code&gt; 或浏览器 &lt;code&gt;Cookie&lt;/code&gt;）是服务器发送到用户浏览器并保存在本地的一小块数据，它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常，它用于告知服务端两个请求是否来自同一浏览器，如保持用户的登录状态。&lt;code&gt;Cookie&lt;/code&gt; 使基于无状态的 &lt;code&gt;HTTP&lt;/code&gt; 协议记录稳定的状态信息成为了可能。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cookie&lt;/code&gt;主要用于以下三个方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;会话状态管理（如用户登录状态、购物车、游戏分数或其它需要记录的信息）&lt;/li&gt;
&lt;li&gt;个性化设置（如用户自定义设置、主题等）&lt;/li&gt;
&lt;li&gt;浏览器行为跟踪（如跟踪分析用户行为等）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Cookie&lt;/code&gt; 曾一度用于客户端数据的存储，因当时并没有其它合适的存储办法而作为唯一的存储手段，但现在随着现代浏览器开始支持各种各样的存储方式，&lt;code&gt;Cookie&lt;/code&gt; 渐渐被淘汰。由于服务器指定 &lt;code&gt;Cookie&lt;/code&gt; 后，浏览器的每次请求都会携带 &lt;code&gt;Cookie&lt;/code&gt; 数据，会带来额外的性能开销（尤其是在移动环境下）。新的浏览器API已经允许开发者直接将数据存储到本地，如使用 &lt;code&gt;Web storage API&lt;/code&gt;（本地存储和会话存储）或 &lt;code&gt;IndexedDB&lt;/code&gt; 。&lt;/p&gt;
&lt;h2&gt;创建Cookie&lt;/h2&gt;
&lt;p&gt;当服务器收到HTTP请求时，服务器可以在响应头里面添加一个 &lt;code&gt;Set-Cookie&lt;/code&gt; 选项。浏览器收到响应后通常会保存下 &lt;code&gt;Cookie&lt;/code&gt;，之后对该服务器每一次请求中都通过 &lt;code&gt;Cookie&lt;/code&gt; 请求头部将 &lt;code&gt;Cookie&lt;/code&gt; 信息发送给服务器。另外，&lt;code&gt;Cookie&lt;/code&gt; 的过期时间、域、路径、有效期、适用站点都可以根据需要来指定。&lt;/p&gt;
&lt;p&gt;服务器使用 &lt;code&gt;Set-Cookie&lt;/code&gt; 响应头部向用户代理（一般是浏览器）发送Cookie信息，比如 &lt;code&gt;Set-Cookie: status=enable; expires=Tue, 05 Jul 2011 07:26:31 GMT; path=/; domain=clloz.com;&lt;/code&gt;，服务器通过该头部告知客户端保存 &lt;code&gt;Cookie&lt;/code&gt; 信息。&lt;/p&gt;
&lt;p&gt;如果不对 &lt;code&gt;Cookie&lt;/code&gt; 设置 &lt;code&gt;expires&lt;/code&gt; 则该 &lt;code&gt;Cookie&lt;/code&gt; 被认为是会话期 &lt;code&gt;Cookie&lt;/code&gt;，会话期 &lt;code&gt;Cookie&lt;/code&gt; 是最简单的 &lt;code&gt;Cookie&lt;/code&gt;：浏览器关闭之后它会被自动删除，也就是说它仅在会话期内有效。会话期&lt;code&gt;Cookie&lt;/code&gt;不需要指定过期时间（ &lt;code&gt;Expires&lt;/code&gt; ）或者有效期（ &lt;code&gt;Max-Age&lt;/code&gt; ）。需要注意的是，有些浏览器提供了会话恢复功能，这种情况下即使关闭了浏览器，会话期 &lt;code&gt;Cookie&lt;/code&gt; 也会被保留下来，就好像浏览器从来没有关闭一样。而设置了 &lt;code&gt;expires&lt;/code&gt; 的 &lt;code&gt;Cookie&lt;/code&gt; 则被认为是持久性的 &lt;code&gt;Cookie&lt;/code&gt;，规定了 &lt;code&gt;Cookie&lt;/code&gt; 的过期时间，而 &lt;code&gt;max-age&lt;/code&gt; 参数则规定了 &lt;code&gt;Cookie&lt;/code&gt; 过期的相对时间。&lt;/p&gt;
&lt;h2&gt;Cookie的安全&lt;/h2&gt;
&lt;p&gt;标记为 &lt;code&gt;Secure&lt;/code&gt; 的 &lt;code&gt;Cookie&lt;/code&gt; 只应通过被 &lt;code&gt;HTTPS&lt;/code&gt; 协议加密过的请求发送给服务端。但即便设置了 &lt;code&gt;Secure&lt;/code&gt; 标记，敏感信息也不应该通过 &lt;code&gt;Cookie&lt;/code&gt; 传输，因为 &lt;code&gt;Cookie&lt;/code&gt; 有其固有的不安全性，&lt;code&gt;Secure&lt;/code&gt; 标记也无法提供确实的安全保障。从 &lt;code&gt;Chrome 52&lt;/code&gt; 和 &lt;code&gt;Firefox 52&lt;/code&gt; 开始，不安全的站点（ &lt;code&gt;http:&lt;/code&gt; ）无法使用 &lt;code&gt;Cookie&lt;/code&gt; 的 &lt;code&gt;Secure&lt;/code&gt; 标记。&lt;/p&gt;
&lt;p&gt;为避免跨域脚本 ( &lt;code&gt;XSS&lt;/code&gt; ) 攻击，通过 &lt;code&gt;JavaScript&lt;/code&gt; 的 &lt;code&gt;Document.cookie&lt;/code&gt; API无法访问带有 &lt;code&gt;HttpOnly&lt;/code&gt; 标记的 &lt;code&gt;Cookie&lt;/code&gt;，它们只应该发送给服务端。如果包含服务端 &lt;code&gt;Session&lt;/code&gt; 信息的 &lt;code&gt;Cookie&lt;/code&gt; 不想被客户端 &lt;code&gt;JavaScript&lt;/code&gt; 脚本调用，那么就应该为其设置 &lt;code&gt;HttpOnly&lt;/code&gt; 标记。&lt;/p&gt;
&lt;h2&gt;Cookie的作用域&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Domain&lt;/code&gt;和 &lt;code&gt;Path&lt;/code&gt; 标识定义了 &lt;code&gt;Cookie&lt;/code&gt; 的作用域：即 &lt;code&gt;Cookie&lt;/code&gt; 应该发送给哪些 &lt;code&gt;URL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Domain&lt;/code&gt; 标识指定了哪些主机可以接受 &lt;code&gt;Cookie&lt;/code&gt;。如果不指定，默认为当前文档的主机（不包含子域名）。如果指定了 &lt;code&gt;Domain&lt;/code&gt;，则一般包含子域名。&lt;/p&gt;
&lt;p&gt;例如，如果设置 &lt;code&gt;Domain=mozilla.org&lt;/code&gt;，则Cookie也包含在子域名中（如 &lt;code&gt;developer.mozilla.org&lt;/code&gt; ）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Path&lt;/code&gt; 标识指定了主机下的哪些路径可以接受 &lt;code&gt;Cookie&lt;/code&gt;（该 &lt;code&gt;URL&lt;/code&gt; 路径必须存在于请求 &lt;code&gt;URL&lt;/code&gt; 中）。以字符 &lt;code&gt;%x2F (&quot;/&quot;)&lt;/code&gt; 作为路径分隔符，子路径也会被匹配。&lt;/p&gt;
&lt;p&gt;例如，设置 &lt;code&gt;Path=/docs&lt;/code&gt;，则以下地址都会匹配：&lt;code&gt;/docs&lt;/code&gt;，&lt;code&gt;/docs/Web/&lt;/code&gt;，&lt;code&gt;/docs/Web/HTTP&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;JS操作Cookie&lt;/h2&gt;
&lt;p&gt;通过 &lt;code&gt;Document.cookie&lt;/code&gt; 属性可创建新的 &lt;code&gt;Cookie&lt;/code&gt;，也可通过该属性访问非 &lt;code&gt;HttpOnly&lt;/code&gt; 标记的 &lt;code&gt;Cookie&lt;/code&gt; 。由于 &lt;code&gt;document.cookie&lt;/code&gt; 属性只给出了 &lt;code&gt;cookie&lt;/code&gt; 字符串，而没有给出操作 &lt;code&gt;cookie&lt;/code&gt; 的方法，所以 &lt;code&gt;cookie&lt;/code&gt; 的读写删需要自己写方法封装。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JS&lt;/code&gt; 设置 &lt;code&gt;cookie&lt;/code&gt;：&lt;code&gt;document.cookie=&quot;age=12; expires=Thu, 26 Feb 2116 11:50:25 GMT; domain=sankuai.com; path=/&quot;;&lt;/code&gt; 设置多个 &lt;code&gt;cookie&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;document.cookie = &apos;name=Jonh&apos;
document.cookie = &apos;age=12&apos;
document.cookie = &apos;class=111&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要想修改一个 &lt;code&gt;cookie&lt;/code&gt;，只需要重新赋值就行，旧的值会被新的值覆盖。但要注意一点，在设置新 &lt;code&gt;cookie&lt;/code&gt; 时，&lt;code&gt;path/domain&lt;/code&gt; 这几个选项一定要旧 &lt;code&gt;cookie&lt;/code&gt; 保持一样。否则不会修改旧值，而是添加了一个新的 &lt;code&gt;cookie&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;删除一个 &lt;code&gt;cookie&lt;/code&gt; 也挺简单，也是重新赋值，只要将这个新 &lt;code&gt;cookie&lt;/code&gt; 的 &lt;code&gt;expires&lt;/code&gt; 选项设置为一个过去的时间点就行了。但同样要注意，&lt;code&gt;path/domain&lt;/code&gt; /这几个选项一定要旧 &lt;code&gt;cookie&lt;/code&gt; 保持一样。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cookie&lt;/code&gt; 其实是个字符串，但这个字符串中逗号、分号、空格被当做了特殊符号。所以当 &lt;code&gt;cookie&lt;/code&gt; 的 &lt;code&gt;key&lt;/code&gt; 和 &lt;code&gt;value&lt;/code&gt; 中含有这3个特殊字符时，需要对其进行额外编码，一般会用 &lt;code&gt;escape&lt;/code&gt; 进行编码，读取时用 &lt;code&gt;unescape&lt;/code&gt; 进行解码；当然也可以用 &lt;code&gt;encodeURIComponent/decodeURIComponent&lt;/code&gt; 或者 &lt;code&gt;encodeURI/decodeURI&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Web Storage&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Web Storage&lt;/code&gt; 包含如下两种机制：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;sessionStorage&lt;/code&gt; ：为每一个给定的源（ &lt;code&gt;given origin&lt;/code&gt; ）维持一个独立的存储区域，该存储区域在页面会话期间可用（即只要浏览器处于打开状态，包括页面重新加载和恢复）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;localStorage&lt;/code&gt; ：同样的功能，但是在浏览器关闭，然后重新打开后数据仍然存在。一般我们会在用户登出后清除 &lt;code&gt;localStorage&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两种机制是通过 &lt;code&gt;Window.sessionStorage&lt;/code&gt; 和 &lt;code&gt;Window.localStorage&lt;/code&gt; 属性使用（更确切的说，在支持的浏览器中 &lt;code&gt;Window&lt;/code&gt; 对象实现了 &lt;code&gt;WindowLocalStorage&lt;/code&gt; 和 &lt;code&gt;WindowSessionStorage&lt;/code&gt; 对象并挂在其 &lt;code&gt;localStorage&lt;/code&gt; 和 &lt;code&gt;sessionStorage&lt;/code&gt; 属性下）—— 调用其中任一对象会创建 &lt;code&gt;Storage&lt;/code&gt; 对象，通过&lt;code&gt;Storage&lt;/code&gt;对象，可以设置、获取和移除数据项。对于每个源 &lt;code&gt;（origin）sessionStorage&lt;/code&gt; 和 &lt;code&gt;localStorage&lt;/code&gt; 使用不同的 &lt;code&gt;Storage&lt;/code&gt; 对象——独立运行和控制。&lt;/p&gt;
&lt;h2&gt;sessionStorage&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sessionStorage&lt;/code&gt; 属性允许你访问一个 &lt;code&gt;session Storage&lt;/code&gt; 对象。它与 &lt;code&gt;localStorage&lt;/code&gt; 相似，不同之处在于 &lt;code&gt;localStorage&lt;/code&gt; 里面存储的数据没有过期时间设置，而存储在 &lt;code&gt;sessionStorage&lt;/code&gt; 里面的数据在页面会话结束（&lt;code&gt;tag&lt;/code&gt; 关闭）时会被清除。页面会话在浏览器打开期间一直保持，并且重新加载或恢复页面（浏览器以外关闭下次再打开会有恢复按钮）仍会保持原来的页面会话。在新标签或窗口打开一个页面时会在顶级浏览上下文（&lt;code&gt;browsing context&lt;/code&gt; 指的就是 &lt;code&gt;window&lt;/code&gt;， &lt;code&gt;iframe&lt;/code&gt; 或者 &lt;code&gt;tab&lt;/code&gt;）中初始化一个新的会话，这点和&lt;code&gt;session cookies&lt;/code&gt; 的运行方式不同。用同一个 &lt;code&gt;URL&lt;/code&gt; 打开的多个 &lt;code&gt;tab&lt;/code&gt; 或者 &lt;code&gt;window&lt;/code&gt; 中都会创建独立的 &lt;code&gt;sessionStorage&lt;/code&gt;。复制 &lt;code&gt;duplicate&lt;/code&gt; 一个 &lt;code&gt;tab&lt;/code&gt; 会同时复制这个 &lt;code&gt;tab&lt;/code&gt; 的 &lt;code&gt;seesionStorage&lt;/code&gt; 到新的 &lt;code&gt;tab&lt;/code&gt; 中。关闭 &lt;code&gt;tab&lt;/code&gt; 或者 &lt;code&gt;window&lt;/code&gt; 会结束会话并清除 &lt;code&gt;sessionStorage&lt;/code&gt; 中的对象。&lt;/p&gt;
&lt;p&gt;我们用 &lt;code&gt;window.open&lt;/code&gt; 打开的新页面如果和原页面&lt;strong&gt;同源&lt;/strong&gt;也能继承原 &lt;code&gt;tag&lt;/code&gt; 的 &lt;code&gt;sessionStorage&lt;/code&gt;，注意要同源，如果协议、域名或者端口不同则不可以继承。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sessionStorage&lt;/code&gt; 的 &lt;code&gt;key&lt;/code&gt; 和 &lt;code&gt;value&lt;/code&gt; 都是用 &lt;code&gt;UTF-16&lt;/code&gt; 的形式存储的。和对象一样，&lt;code&gt;integer&lt;/code&gt; 的 &lt;code&gt;key&lt;/code&gt; 会自动转换为字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 保存数据到 sessionStorage
sessionStorage.setItem(&apos;key&apos;, &apos;value&apos;)

// 从 sessionStorage 获取数据
let data = sessionStorage.getItem(&apos;key&apos;)

// 从 sessionStorage 删除保存的数据
sessionStorage.removeItem(&apos;key&apos;)

// 从 sessionStorage 删除所有保存的数据
sessionStorage.clear()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;localStorage&lt;/h2&gt;
&lt;p&gt;只读的&lt;code&gt;localStorage&lt;/code&gt; 属性允许你访问一个&lt;code&gt;Document&lt;/code&gt; 源（&lt;code&gt;origin&lt;/code&gt;）的对象 &lt;code&gt;Storage&lt;/code&gt;；其存储的数据能在跨浏览器会话保留。&lt;code&gt;localStorage&lt;/code&gt; 类似 &lt;code&gt;sessionStorage&lt;/code&gt;，但其区别在于：存储在 &lt;code&gt;localStorage&lt;/code&gt; 的数据可以长期保留；而当页面会话结束——也就是说，当页面被关闭时，存储在&lt;code&gt;sessionStorage&lt;/code&gt;的数据会被清除 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;incognito&lt;/code&gt; 页面，也就是隐身窗口的 &lt;code&gt;localStorage&lt;/code&gt; 是不和非隐身窗口的共享的，隐身窗口中的同源页面会共享 &lt;code&gt;localStorage&lt;/code&gt;，在隐身窗口中的所有 &lt;code&gt;tag&lt;/code&gt; 被关闭的时候即隐身窗口关闭的时候这个隐身窗口中的所有 &lt;code&gt;localStorage&lt;/code&gt; 都会销毁。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; 的 &lt;code&gt;key&lt;/code&gt; 和 &lt;code&gt;value&lt;/code&gt; 都是用 &lt;code&gt;UTF-16&lt;/code&gt; 的形式存储的。和对象一样，&lt;code&gt;integer&lt;/code&gt; 的 &lt;code&gt;key&lt;/code&gt; 会自动转换为字符串。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;file:&lt;/code&gt; 协议的 &lt;code&gt;URL&lt;/code&gt; 没有给出 &lt;code&gt;localStorage&lt;/code&gt; 的定义，所以各个浏览器的实现可能不同。我在 &lt;code&gt;Chrome&lt;/code&gt; 中的测试是各个 &lt;code&gt;file:&lt;/code&gt; &lt;code&gt;URL&lt;/code&gt; 共享一个 &lt;code&gt;localStorage&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//增加了一个数据项目。
localStorage.setItem(&apos;myCat&apos;, &apos;Tom&apos;)

//读取 localStorage 项
let cat = localStorage.getItem(&apos;myCat&apos;)

//移除 localStorage 项
localStorage.removeItem(&apos;myCat&apos;)

// 移除所有
localStorage.clear()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000004556040&quot; title=&quot;聊一聊Cookie&quot;&gt;聊一聊Cookie&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/browser.gq-LwpuH.jpg"/><enclosure url="/_astro/browser.gq-LwpuH.jpg"/></item><item><title>前端网络基础和HTTP</title><link>https://clloz.com/blog/http</link><guid isPermaLink="true">https://clloz.com/blog/http</guid><pubDate>Thu, 02 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/04/25/how-browser-work/&quot; title=&quot;浏览器渲染过程及JS引擎浅析&quot;&gt;浏览器渲染过程及JS引擎浅析&lt;/a&gt;这篇文章里，解答了 &lt;code&gt;在浏览器输入url回车后发生了什么&lt;/code&gt; 问题中的一部分，也就是浏览器获得了服务器返回的 &lt;code&gt;html&lt;/code&gt; 文档(实际可能是其他类型的资源）后的一系列行为。但是还有一部分行为并没有涉及，就是浏览器是如何和服务器通信的，以及数据是怎么传输的。在 &lt;code&gt;图解HTTP&lt;/code&gt; 这本书里面很好地解答了这部分问题。&lt;/p&gt;
&lt;p&gt;本文主要是讲网络知识，关于 &lt;code&gt;在浏览器输入url回车后发生了什么&lt;/code&gt; 可以参考另一篇文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/2018/12/05/url/&quot; title=&quot;从URL输入到页面展现&quot;&gt;从URL输入到页面展现&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;文章较长，主要内容是计算机网络的基础和HTTP协议&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;web 和 http 产生的背景&lt;/h2&gt;
&lt;p&gt;还是老样子，学习一个东西之前，我们先来了解以下他是为了解决什么问题而产生的，以及它是如何解决这些问题的。&lt;/p&gt;
&lt;h2&gt;什么是web&lt;/h2&gt;
&lt;p&gt;我们经常听到这个词，也经常说 &lt;code&gt;web&lt;/code&gt; 开发，可是什么是 &lt;code&gt;web&lt;/code&gt; 呢，是网页吗？我们平时见到的 &lt;code&gt;www(worldwideweb)&lt;/code&gt; 和 &lt;code&gt;web&lt;/code&gt; 指的是同一个东西，我们叫他万维网，是一个透过互联网访问的，由许多互相链接的超文本组成的系统，是1989年伯纳斯·李发明的。简单的说就是整个借助互联网形成访问超文本的系统构成这个所谓的 &lt;code&gt;web&lt;/code&gt; 或是称作 &lt;code&gt;www&lt;/code&gt;，为了在互联网上实现访问超文本设计的技术和程序都算是 &lt;code&gt;web&lt;/code&gt; 系统的一部分，比如 &lt;code&gt;http&lt;/code&gt; 超文本传输协议，&lt;code&gt;html&lt;/code&gt; 超文本标记语言、浏览器、&lt;code&gt;web&lt;/code&gt; 服务器等。而 &lt;code&gt;www(worldwideweb)&lt;/code&gt; 这个名称则是来自于1990年伯纳斯·李写的世界上第一款网页浏览器和编辑器，它的名字就叫 &lt;code&gt;worldwideweb&lt;/code&gt;，同时他也实现了第一个 &lt;code&gt;web&lt;/code&gt; 服务器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;超文本的意思就是能够显示在电子设备上的包含超链接的文本（以及多媒体内容），允许从当前文本切换到超链接所指向的文字，超文本文档通过超链接互相连结。&lt;code&gt;html&lt;/code&gt; 称为超文本标记语言，&lt;code&gt;http&lt;/code&gt; 称为超文本传输协议，它们都是为了在 &lt;code&gt;web&lt;/code&gt; 上浏览超文本产生的技术。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;互联网、英特网和万维网&lt;/h2&gt;
&lt;p&gt;互联网 &lt;code&gt;internet&lt;/code&gt; 是指凡是能彼此通信的设备组成的网络就叫互联网。英特网 &lt;code&gt;Internet&lt;/code&gt; 指的是利用 &lt;code&gt;TCP/IP&lt;/code&gt; 通讯协定所创建的各种网络，是国际上最大的互联网，也称“国际互联网”。万维网 &lt;code&gt;www&lt;/code&gt; 是一个由许多互相链接的超文本组成的系统，通过因特网访问。在此定义下，万维网是因特网的一项服务，因特网是互联网的一种。国际互联网是互联网中的一个，也有独立于国际互联网之外的互联网，比如朝鲜的光明网。&lt;code&gt;web&lt;/code&gt; 是一个依靠互联网访问超文本的系统，是互联网提供的服务之一，其他的服务比如邮件，文件共享等。&lt;/p&gt;
&lt;h2&gt;web 产生的原因&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;web&lt;/code&gt; 产生之前，计算机和网络只是极少数人使用的，当时出现的需求是希望能够实现借助超文本在全世界研究者中间共享知识和信息的需求，伯纳斯·李为了实现这个需求构建了 &lt;code&gt;web&lt;/code&gt; 的三个关键技术： 1. 超文本标记语言 &lt;code&gt;HTML&lt;/code&gt; 2. 统一资源定位符 &lt;code&gt;URL&lt;/code&gt; 3. 超文本传输协议 &lt;code&gt;HTTP&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;为什么要有 http&lt;/h2&gt;
&lt;p&gt;计算机与网络设备要相互通信，双方就必须基于相同的方法。比如，如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通 信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之间 的通信，所有的这一切都需要一种规则。而我们就把这种规则称为协议 &lt;code&gt;protocol&lt;/code&gt;。协议中存在各式各样的内容。从电缆的规格到 &lt;code&gt;IP&lt;/code&gt; 地址的选定方法、 寻找异地用户的方法、双方建立通信的顺序，以及 &lt;code&gt;Web&lt;/code&gt; 页面显示需要 处理的步骤，等等。而 &lt;code&gt;http&lt;/code&gt; 协议就是众多网络协议中的一种，它产生的目的就是为了确定 &lt;code&gt;web&lt;/code&gt; 中传输超文本的具体标准，让这一共享知识的设想能够实现。&lt;/p&gt;
&lt;h2&gt;web技术的标准化&lt;/h2&gt;
&lt;p&gt;随着 &lt;code&gt;web&lt;/code&gt; 的诞生，浏览器和 &lt;code&gt;web&lt;/code&gt; 服务器都开始飞速发展，浏览器方面由微软和网景齐头并进，而 &lt;code&gt;web&lt;/code&gt; 服务器则由 &lt;code&gt;apache&lt;/code&gt; 开疆拓土（ &lt;code&gt;apache&lt;/code&gt; 如今已经成为 &lt;code&gt;web&lt;/code&gt; 服务器的标准之一）。之后 &lt;code&gt;web&lt;/code&gt; 技术不断的迅猛发展，并爆发了微软和网景之间的浏览器大战，两者无视标准的恶性竞争也让人们意识到 &lt;code&gt;web&lt;/code&gt; 标准化的重要性，并成为了 &lt;code&gt;web&lt;/code&gt; 业界一直到今天都在努力的目标。&lt;/p&gt;
&lt;p&gt;而作为 &lt;code&gt;web&lt;/code&gt; 技术中重要一环的 &lt;code&gt;http&lt;/code&gt; 协议被正式标准化是在1996年，版本为 &lt;code&gt;http/1.0&lt;/code&gt;，被记载于 &lt;code&gt;RFC1945&lt;/code&gt;，该协议标准至今被广泛应用于 &lt;code&gt;web&lt;/code&gt; 服务器。1997 年 1 月公布的 &lt;code&gt;HTTP/1.1&lt;/code&gt; 是目前主流的 &lt;code&gt;HTTP&lt;/code&gt; 协议版本。当初 的标准是 &lt;code&gt;RFC2068&lt;/code&gt;，之后发布的修订版 &lt;code&gt;RFC2616&lt;/code&gt; 就是 &lt;code&gt;HTTP/1.1&lt;/code&gt; 的最新版本。 而新一代 &lt;code&gt;HTTP/2&lt;/code&gt; 标准于2015年5月以 &lt;code&gt;RFC 7540&lt;/code&gt; 正式发表。&lt;code&gt;HTTP/2&lt;/code&gt; 的标准化工作由&lt;code&gt;Chrome&lt;/code&gt;、&lt;code&gt;Opera&lt;/code&gt;、&lt;code&gt;Firefox&lt;/code&gt;、&lt;code&gt;Internet Explorer 11&lt;/code&gt;、&lt;code&gt;Safari&lt;/code&gt;、&lt;code&gt;Amazon Silk&lt;/code&gt; 及&lt;code&gt;Edge&lt;/code&gt; 等浏览器提供支持，多数主流浏览器已经在2015年底支持了该协议。&lt;code&gt;apache&lt;/code&gt;、&lt;code&gt;nginx&lt;/code&gt; 等主流web服务器也都支持新的标准，目前的主流网站都以启用新的标准。关于 &lt;code&gt;HTTP/2&lt;/code&gt; 后面在讨论。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;普通的 &lt;code&gt;HTTPS&lt;/code&gt; 网站浏览会比 &lt;code&gt;HTTP&lt;/code&gt; 网站稍微慢一些，因为需要处理加密任务，而配置了 &lt;code&gt;h2&lt;/code&gt; 的 &lt;code&gt;HTTPS&lt;/code&gt;，在低延时的情况下速度会比 &lt;code&gt;HTTP&lt;/code&gt; 更快更稳定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;网络基础&lt;/h2&gt;
&lt;h2&gt;TCP/IP 协议族&lt;/h2&gt;
&lt;p&gt;全世界有几十亿的电脑，加上网络设备还会多出更多，它们都在同一个互联网上，也就意味着在这个网络上的任意两个设备之间都是可以通信的。比如我在上海，我的电脑的网卡发出一个信号，而你在北京的电脑就能马上收到，甚至你在太平洋彼岸的美国也可以收到，要做到这些，就必须以来上面说的各种网络协议。互联网的核心是一系列协议，总称为&quot;互联网协议&quot; &lt;code&gt;Internet Protocol Suite&lt;/code&gt;，通常也被我们称为 &lt;code&gt;TCP/IP&lt;/code&gt; 协议族。它们对电脑如何连接和组网，做出了详尽的规定。理解了这些协议，就理解了互联网是如何设计的了。&lt;/p&gt;
&lt;p&gt;上面的背景介绍中说明了计算机和网络设备要互相通信必须要制定详细的标准，于是产生了各种各样的网络协议，&lt;code&gt;http&lt;/code&gt; 只是其中的一种，不同的协议都是因为不同的需求和使用场景而定制的，从电缆的规格到 &lt;code&gt;IP&lt;/code&gt; 地址的选定方法、 寻找异地用户的方法、双方建立通信的顺序，以及 &lt;code&gt;Web&lt;/code&gt; 页面显示需要 处理的步骤等等，比如 &lt;code&gt;FTP&lt;/code&gt; 在计算机网络上在客户端和服务器之间进行文件传输的协议，而 &lt;code&gt;SMTP&lt;/code&gt; 是互联网上传输电子邮件的协议，而我们常说的 &lt;code&gt;DNS&lt;/code&gt; 域名系统也是应用层的一个协议，作用是将域名和IP地址相互映射的一个分布式数据库。而之所以这些协议的集合被称为 &lt;code&gt;TCP/IP&lt;/code&gt; 协议族是因为该协议家族的两个核心协议：&lt;code&gt;TCP&lt;/code&gt;（传输控制协议）和 &lt;code&gt;IP&lt;/code&gt;（网际协议），为该家族中最早通过的标准。整个 &lt;code&gt;TCP/IP&lt;/code&gt; 协议族非常庞大和复杂，在这里我们只讨论学习 &lt;code&gt;HTTP&lt;/code&gt; 需要用到的网络基础。如果你想深入学习计算机网络的知识有这三本书推荐： 1. &lt;a href=&quot;https://www.amazon.cn/dp/B07F3QKG13/ref=sr_1_2?__mk_zh_CN=%E4%BA%9A%E9%A9%AC%E9%80%8A%E7%BD%91%E7%AB%99&amp;#x26;keywords=%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C&amp;#x26;qid=1556717384&amp;#x26;s=gateway&amp;#x26;sr=8-2&quot; title=&quot;计算机网络-自顶向下方法&quot;&gt;计算机网络-自顶向下方法&lt;/a&gt; 2. &lt;a href=&quot;https://www.amazon.cn/dp/B007JFRQ0G/ref=sr_1_1?__mk_zh_CN=%E4%BA%9A%E9%A9%AC%E9%80%8A%E7%BD%91%E7%AB%99&amp;#x26;keywords=%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C&amp;#x26;qid=1556717384&amp;#x26;s=gateway&amp;#x26;sr=8-1&quot; title=&quot;计算机网络&quot;&gt;计算机网络&lt;/a&gt; 3. &lt;a href=&quot;http://https://www.amazon.cn/dp/B078JFNPTM/ref=sr_1_1?__mk_zh_CN=%E4%BA%9A%E9%A9%AC%E9%80%8A%E7%BD%91%E7%AB%99&amp;#x26;keywords=tcp%2Fip%E8%AF%A6%E8%A7%A3&amp;#x26;qid=1556717553&amp;#x26;s=gateway&amp;#x26;sr=8-1&quot; title=&quot;TCP/IP详解&quot;&gt;TCP/IP详解&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;网络分层管理&lt;/h2&gt;
&lt;h3&gt;为什么网络设计要分层&lt;/h3&gt;
&lt;p&gt;网络的分层大家都知道，&lt;code&gt;OSI&lt;/code&gt; 七层网络模型大家都听说过，要说为什么网络设计要分层，归根结底就是为了解耦，特别是在底层涉及到很多硬件的情况下，工作在高层的技术人员不需要了解底层的硬件的工作细节。如果互联网只由一个协议统 筹，某个地方需要改变设计时，就必须把所有部分整体替换掉。而分层 之后只需把变动的层替换掉即可。把各层之间的接口部分规划好之后，每个层次内部的设计就能够自由改动了。 并且层次化之后，设计也变得相对简单了。处于应用层上的应用可以只考虑分派给自己的任务，而不需要弄清对方在地球上哪 个地方、对方的传输路线是怎样的、是否能确保传输送达等问题。分层设计的有点可以归纳为三点： 1. 各层之间相互独立：高层是不需要知道底层的功能是采取硬件技术来实现的，它只需要知道通过与底层的接口就可以获得所需要的服务； 2. 灵活性好：各层都可以采用最适当的技术来实现，例如某一层的实现技术发生了变化，用硬件代替了软件，只要这一层的功能与接口保持不变，实现技术的变化都并不会对其他各层以及整个系统的工作产生影响； 3. 易于实现和标准化：由于采取了规范的层次结构去组织网络功能与协议，因此可以将计算机网络复杂的通信过程，划分为有序的连续动作与有序的交互过程，有利于将网络复杂的通信工作过程化解为一系列可以控制和实现的功能模块，使得复杂的计算机网络系统变得易于设计，实现和标准化。&lt;/p&gt;
&lt;h3&gt;OSI 七层模型和 TCP/IP 四层模型&lt;/h3&gt;
&lt;p&gt;网络模型不是一开始就有的，在网络刚发展时，网络协议是由各互联网公司自己定义的，比如那时的巨头网络公司 &lt;code&gt;IBM&lt;/code&gt;、微软、苹果、思科等等，他们每家公司都有自己的网络协议，各家的协议也是不能互通的，那时候大家觉得这是可以的，但对消费者来说这实际上是技术垄断，因为你买了苹果的设备就不能用微软的设备，因为他们的协议不是一样的，没有统一的标准来规范网络协议，都是这些公司的私有协议。这样大大的阻碍了互联网的发展，为了解决这个问题，国际标准化组织 1984 提出的概念模型，简称 &lt;code&gt;OSI&lt;/code&gt;（&lt;code&gt;Open Systems Interconnection Model&lt;/code&gt;），这是一个概念，并非实现。&lt;code&gt;TCP/IP&lt;/code&gt; 协议就是基于此模型设计实现的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OSI&lt;/code&gt; 七层模型： 1. 物理层（ &lt;code&gt;Physical Layer&lt;/code&gt; ）在局部局域网上传送数据帧（ &lt;code&gt;data frame&lt;/code&gt; ），它负责管理计算机通信设备和网络媒体之间的互通。包括了针脚、电压、线缆规范、集线器、中继器、网卡、主机接口卡等。 2. 数据链路层（ &lt;code&gt;Data Link Layer&lt;/code&gt; ）负责网络寻址、错误侦测和改错。当表头和表尾被加至数据包时，会形成帧。数据链表头（ &lt;code&gt;DLH&lt;/code&gt; ）是包含了物理地址和错误侦测及改错的方法。数据链表尾（ &lt;code&gt;DLT&lt;/code&gt;）是一串指示数据包末端的字符串。例如以太网、无线局域网（ &lt;code&gt;Wi-Fi&lt;/code&gt;）和通用分组无线服务（ &lt;code&gt;GPRS&lt;/code&gt; ）等。 3. 网络层（ &lt;code&gt;Network Layer&lt;/code&gt; ）决定数据的路径选择和转寄，将网络表头（ &lt;code&gt;NH&lt;/code&gt; ）加至数据包，以形成分组。网络表头包含了网络数据。例如:网际协议（ &lt;code&gt;IP&lt;/code&gt;）等。 4. 传输层（ &lt;code&gt;Transport Layer&lt;/code&gt;）把传输表头（ &lt;code&gt;TH&lt;/code&gt;）加至数据以形成数据包。传输表头包含了所使用的协议等发送信息。例如:传输控制协议（ &lt;code&gt;TCP&lt;/code&gt;）等。 5. 会话层（ &lt;code&gt;Session Layer&lt;/code&gt;）负责在数据传输中设置和维护计算机网络中两台计算机之间的通信连接。 6. 表达层（ &lt;code&gt;Presentation Layer&lt;/code&gt; ）把数据转换为能与接收者的系统格式兼容并适合传输的格式。 7. 应用层（ &lt;code&gt;Application Layer&lt;/code&gt; ）提供为应用软件而设的接口，以设置与另一应用软件之间的通信。例如: &lt;code&gt;HTTP&lt;/code&gt;，&lt;code&gt;HTTPS&lt;/code&gt;，&lt;code&gt;FTP&lt;/code&gt;，&lt;code&gt;TELNET&lt;/code&gt;，&lt;code&gt;SSH&lt;/code&gt;，&lt;code&gt;SMTP&lt;/code&gt;，&lt;code&gt;POP3&lt;/code&gt;等。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;OSI&lt;/code&gt; 参考模型并没有提供一个可以实现的方法，而是描述了一些概念，用来协调进程间通信标准的制定。即&lt;code&gt;OSI&lt;/code&gt; 参考模型并不是一个标准，而是一个在制定标准时所使用的概念性框架。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;TCP/IP&lt;/code&gt; 四层模型 1. 应用层：网络应用程序使用的层，提供网络应用程序需要的接口，将程序中的数据包装成协议的标准格式。，对应 &lt;code&gt;OSI&lt;/code&gt; 七层模型的最上面三层（会话层，表达层和应用层），如 &lt;code&gt;HTTP&lt;/code&gt;，&lt;code&gt;SSH&lt;/code&gt;，&lt;code&gt;FTP&lt;/code&gt;，&lt;code&gt;SMTP&lt;/code&gt;，&lt;code&gt;TELENT&lt;/code&gt;，&lt;code&gt;POP3&lt;/code&gt; 2. 传输层：提供计算机和网络设备之间的设备传输，确保数据传输的可靠和顺序。包括 &lt;code&gt;TCP&lt;/code&gt; 和 &lt;code&gt;UDP&lt;/code&gt;，对应 &lt;code&gt;OSI&lt;/code&gt; 模型中的传输层 3. 网络层（网络互联层）：处理数据包，通过 &lt;code&gt;IP&lt;/code&gt; 地址规定数据包的传输路径，并将数据从源发送到目的地。对应 &lt;code&gt;OSI&lt;/code&gt; 模型中的网络层。主要协议是 &lt;code&gt;IP&lt;/code&gt; 协议 4. 链路层：网络连接的硬件部分，将数据包从一个设备的网络层传输到另一个设备的网络层。包括控制操作系统、硬件设备的驱动、&lt;code&gt;NIC&lt;/code&gt; 网卡，光纤，双绞线等物理可见部分。包括以太网，&lt;code&gt;wifi&lt;/code&gt; 等。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多模型细节可以看阮一峰的&lt;a href=&quot;https://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html&quot; title=&quot;互联网协议入门&quot;&gt;互联网协议入门&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;TCP/IP 网络通信流程&lt;/h2&gt;
&lt;p&gt;当我们在浏览器中输入 &lt;code&gt;url&lt;/code&gt; 请求 &lt;code&gt;web&lt;/code&gt; 服务器上的 &lt;code&gt;HTML&lt;/code&gt; 文档的时候，通信的过程是什么样的呢？见下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/tcp-ipstream.CrNoQKN-_bclmK.webp&quot; alt=&quot;tcp-ip-stream&quot; title=&quot;tcp-ip-stream&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;应用层浏览器将请求封装成符合 &lt;code&gt;HTTP&lt;/code&gt; 协议的数据报文，将这个报文交给传输层。&lt;/li&gt;
&lt;li&gt;传输层的 &lt;code&gt;TCP&lt;/code&gt; 协议将从应用层收到的 &lt;code&gt;HTTP&lt;/code&gt; 报文分割成数据包的形式，并在每个数据包上打上标记和序号，然后交给网络层。&lt;/li&gt;
&lt;li&gt;网络层的 &lt;code&gt;IP&lt;/code&gt; 协议给传输层传来的 &lt;code&gt;TCP&lt;/code&gt; 报文段加上通信目的地的 &lt;code&gt;mac&lt;/code&gt; 地址在转发给链路层。&lt;/li&gt;
&lt;li&gt;链路层将封装好的数据通过物理设备传递给目标服务器，到达目标服务器的链路层以后再将封装好的数据按照网络分层一层层解封，最终服务器上的处理程序获得 &lt;code&gt;http&lt;/code&gt; 请求，并找到对应的资源按照请求发送的方式再将资源发送给客户端。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;HTTP 请求中的重要协议&lt;/h2&gt;
&lt;p&gt;在上面的&lt;code&gt;http&lt;/code&gt; 请求的通信流程中，不同的层级涉及到了几个不同协议，它们都是 &lt;code&gt;HTTP&lt;/code&gt; 请求中的&lt;/p&gt;
&lt;h3&gt;IP 协议（网际协议）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;IP&lt;/code&gt; 协议 &lt;code&gt;internet protocol&lt;/code&gt; 工作在网络层，它的作用是根据 &lt;code&gt;IP&lt;/code&gt; 地址和 &lt;code&gt;mac&lt;/code&gt; 地址规定数据包的传输路径，我们的网络上有无数台电脑，怎么选择一个合适的路线才能准确高效地进行数据包的传输是IP协议的主要目标。&lt;code&gt;IP&lt;/code&gt; 地址指向一个网络，而 &lt;code&gt;mac&lt;/code&gt; 地址指向一个具体的设备，一个网络中可以有很多台设备。虽然用 &lt;code&gt;mac&lt;/code&gt; 地址就可以实现数据的传输，但是由于网络十分巨大，用 &lt;code&gt;mac&lt;/code&gt; 地址进行广播的效率非常低下，并且会消耗网络设备上的很大的存储空间，而用 &lt;code&gt;ip&lt;/code&gt; 地址进行广播不断缩小目标的范围，最后再利用 &lt;code&gt;mac&lt;/code&gt; 地址定位到具体的设备是比较高效的。就像寄快递，我们从上海寄快递到北京的某个小区，在运输的过程中会有非常多的集散中心，每个集散中心的送货员不会关心最终的目的地，而是下一个集散中心选择哪个是最有效率的（比如有两个距离相同的集散中心，我们可以先打电话过去问问哪个比较不忙），随着这个过程的不断推进，我们会越来越接近我们的目的地，当快递寄到我们所属街区的集散中心以后，快递员就可以根据我们的真实地址来送货了，这当中的集散中心就像 &lt;code&gt;IP&lt;/code&gt; 地址，而我们的真实住址就像 &lt;code&gt;mac&lt;/code&gt; 地址一样，而且网络中的中间设备比我们想象的要多得多，没有任何一台设备能保存整个网络中所有的设备的 &lt;code&gt;mac&lt;/code&gt; 地址以及线路细节，这就更需要我们利用 &lt;code&gt;ip&lt;/code&gt; 缩小范围，它们工作的层级也不一样，&lt;code&gt;IP&lt;/code&gt; 地址工作在网络层，&lt;code&gt;mac&lt;/code&gt; 地址工作在链路层&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/ip-broadcast.DAg12T2w_2uDDxq.webp&quot; alt=&quot;ip-broadcast&quot; title=&quot;ip-broadcast&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;每块网卡出厂的时候，都有一个全世界独一无二的 &lt;code&gt;MAC&lt;/code&gt; 地址，长度是 &lt;code&gt;48&lt;/code&gt; 个二进制位，通常用 &lt;code&gt;12&lt;/code&gt; 个十六进制数表示，前&lt;code&gt;6&lt;/code&gt; 个十六进制数是厂商编号，后 &lt;code&gt;6&lt;/code&gt; 个是该厂商的网卡流水号。有了 &lt;code&gt;MAC&lt;/code&gt; 地址，就可以定位网卡和数据包的路径了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;TCP 协议&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;TCP&lt;/code&gt; 协议位于传输层，提供可靠的字节流服务，&lt;code&gt;TCP&lt;/code&gt; 协议会把应用层传来的 &lt;code&gt;HTTP&lt;/code&gt; 报文分割成方便传输的报文段数据包，而 &lt;code&gt;TCP&lt;/code&gt; 协议的关键之处在于要把分割后的数据包一个不差的准确地传输到目的地。&lt;/p&gt;
&lt;p&gt;为了准确无误地将数据送达目标处，&lt;code&gt;TCP&lt;/code&gt; 协议采用了三次握手 ( &lt;code&gt;three-way handshaking&lt;/code&gt; )策略。用 &lt;code&gt;TCP&lt;/code&gt; 协议把数据包送出去后，&lt;code&gt;TCP&lt;/code&gt; 不会对传送后的情况置之不理，它一定会向对方确认是否成功送达。握 手过程中使用了 &lt;code&gt;TCP&lt;/code&gt; 的标志( &lt;code&gt;flag&lt;/code&gt; )—— &lt;code&gt;SYN&lt;/code&gt; ( &lt;code&gt;synchronize&lt;/code&gt; )和 &lt;code&gt;ACK&lt;/code&gt; ( &lt;code&gt;acknowledgement&lt;/code&gt; )。&lt;/p&gt;
&lt;p&gt;发送端首先发送一个带 &lt;code&gt;SYN&lt;/code&gt; 标志的数据包给对方。接收端收到后， 回传一个带有 &lt;code&gt;SYN/ACK&lt;/code&gt; 标志的数据包以示传达确认信息。最后，发送 端再回传一个带 &lt;code&gt;ACK&lt;/code&gt; 标志的数据包，代表“握手”结束。若在握手过程中某个阶段莫名中断，&lt;code&gt;TCP&lt;/code&gt; 协议会再次以相同的顺序 发送相同的数据包。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/tcp-shake.CNi7c0D5_ZoF18j.webp&quot; alt=&quot;tcp-shake&quot; title=&quot;tcp-shake&quot;&gt;&lt;/p&gt;
&lt;h3&gt;DNS 服务&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DNS&lt;/code&gt; ( &lt;code&gt;Domain Name System&lt;/code&gt; )服务是和 &lt;code&gt;HTTP&lt;/code&gt; 协议一样位于应用层 的协议。它提供域名到 &lt;code&gt;IP&lt;/code&gt; 地址之间的解析服务。计算机既可以被赋予 &lt;code&gt;IP&lt;/code&gt; 地址，也可以被赋予主机名和域名。比如 &lt;code&gt;www.clloz.com&lt;/code&gt;。用户通常使用主机名或域名来访问对方的计算机，而不是直接通过 &lt;code&gt;IP&lt;/code&gt; 地址访问。因为与 &lt;code&gt;IP&lt;/code&gt; 地址的一组纯数字相比，用字母配合数字的表 示形式来指定计算机名更符合人类的记忆习惯。但要让计算机去理解名称，相对而言就变得困难了。因为计算机更 擅长处理一长串数字。为了解决上述的问题，&lt;code&gt;DNS&lt;/code&gt; 服务应运而生。&lt;code&gt;DNS&lt;/code&gt; 协议提供通过域 名查找 &lt;code&gt;IP&lt;/code&gt; 地址，或逆向从 &lt;code&gt;IP&lt;/code&gt; 地址反查域名的服务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/dns.D9prEz_o_1PUkIb.webp&quot; alt=&quot;dns&quot; title=&quot;dns&quot;&gt;&lt;/p&gt;
&lt;h3&gt;多协议在http请求中的工作流程&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;http&lt;/code&gt; 请求的过程中，上述协议的完整工作流程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/http-request.DpqbkkEi_1x5Vqz.webp&quot; alt=&quot;http-request&quot; title=&quot;http-request&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于 &lt;code&gt;URI&lt;/code&gt; 和 &lt;code&gt;URL&lt;/code&gt; 的区别，看我的这篇文章&lt;a href=&quot;https://www.clloz.com/programming/front-end/2018/12/05/url/&quot; title=&quot;从url输入到页面展现&quot;&gt;从url输入到页面展现&lt;/a&gt;。&lt;code&gt;RFC&lt;/code&gt; 文件为互联网规范、协议、过程等的标准文件，由互联网工程任务组（ &lt;code&gt;IETF&lt;/code&gt; ）维护。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;HTTP协议&lt;/h2&gt;
&lt;p&gt;上面的内容介绍了要学习 &lt;code&gt;HTTP&lt;/code&gt; 协议的一些必备的网络基础，下面来讲一讲开发 &lt;code&gt;web&lt;/code&gt; 应用面对的主要对象：&lt;code&gt;HTTP&lt;/code&gt; 协议。我们的 &lt;code&gt;web&lt;/code&gt; 开发主要是都是在应用层，但是理解应用层之下的网络传输过程对我们的开发也是很有用处的，现在前端也需要掌握后端的技能，特别是如果我们要接触 &lt;code&gt;web&lt;/code&gt; 服务器以及后端开发的时候，计算机网络的知识储备是非常重要的。&lt;/p&gt;
&lt;p&gt;其实刨开应用层底层之下的网络知识，单纯讨论工作在应用层的 &lt;code&gt;HTTP&lt;/code&gt; 协议是非常简单的，&lt;code&gt;HTTP&lt;/code&gt; 协议用于客户端和服务器之间的通信，常见的客户端主要就是浏览器，而常见的服务器就是 &lt;code&gt;web&lt;/code&gt; 服务器，比如&lt;code&gt;apache&lt;/code&gt;，&lt;code&gt;nginx&lt;/code&gt;，&lt;code&gt;IIS&lt;/code&gt;。发起请求的一端为客户端，而响应请求的一端为服务器。两台计算机之间使用 &lt;code&gt;HTTP&lt;/code&gt; 协议通信时，必然有一端是客户端，一端是服务器。&lt;code&gt;HTTP&lt;/code&gt; 协议能够区分哪端是发起请求的客户端，哪一端是响应请求的服务器。请求必然从客户端发起，经过传递后到达服务器，最后服务器响应请求并返回。也就是只有客户端可以主动联系服务器，但是服务器是无法主动联系客户端的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 协议是一个文本型协议（和二进制协议相对），文本型协议意味着所有的内容都是字符串。比如我们要传一个 &lt;code&gt;1&lt;/code&gt;，不是传一个 &lt;code&gt;1&lt;/code&gt; 的 &lt;code&gt;bit&lt;/code&gt; 或者一个字节，而是传一个 &lt;code&gt;Unicode&lt;/code&gt; 的编码。&lt;code&gt;HTTP&lt;/code&gt; 协议在 &lt;code&gt;TCP&lt;/code&gt; 协议的上层，所以我们可以所有流淌在 &lt;code&gt;TCP&lt;/code&gt; 协议中的流的所有内容都可以视为字符。&lt;code&gt;TCP&lt;/code&gt; 传的是流，&lt;code&gt;IP&lt;/code&gt; 传的是包，所谓 &lt;code&gt;流&lt;/code&gt; 就是没有明显分割单位的事物，唯一能够确定的就是先后顺序，比如水流，字节流。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;HTTP 协议不保存状态&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 协议无状态的意思是对于客户端和服务器，只有当前正在进行的请求是有意义的，任何已经结束的请求或响应都不会在客户端和服务器上留下记录，也就是 &lt;code&gt;HTTP&lt;/code&gt; 协议不会对发送过的请求或响应做持久化处理。正是因为这种简单化的设计，&lt;code&gt;HTTP&lt;/code&gt; 协议能够快速地处理大量事务。但是随着web的不断发展，&lt;code&gt;HTTP&lt;/code&gt; 协议的无状态设计在面对一些问题的时候无法很好地解决，比如我们登录了一个购物网站，那么当我们跳转到该网站的其他页面的时候我们也应该保持登录状态，为了实现这种保持状态的功能， &lt;code&gt;HTTP&lt;/code&gt; 协议引入了 &lt;code&gt;Cookie&lt;/code&gt; 技术。&lt;/p&gt;
&lt;h2&gt;HTTP协议中的请求方法&lt;/h2&gt;
&lt;p&gt;| 方法    | 说明                                                                                                                                                                                                                                                                                                                             | 支持的 HTTP 协议版本 |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| GET     | GET请求会显示请求指定的资源。一般来说GET方法应该只用于数据的读取，而不应当用于会产生副作用的非幂等的操作中。它期望的应该是而且应该是安全的和幂等的。这里的安全指的是，请求不会影响到资源的状态。                                                                                                                                 | 1.0、1.1             |
| POST    | POST请求会 向指定资源提交数据，请求服务器进行处理，如：表单数据提交、文件上传等，请求数据会被包含在请求体中。POST方法是非幂等的方法，因为这个请求可能会创建新的资源或/和修改现有资源。                                                                                                                                           | 1.0、1.1             |
| PUT     | PUT请求会身向指定资源位置上传其最新内容，PUT方法是幂等的方法。通过该方法客户端可以将指定资源的最新数据传送给服务器取代指定的资源的内容。                                                                                                                                                                                         | 1.0、1.1             |
| HEAD    | HEAD方法与GET方法一样，都是向服务器发出指定资源的请求。但是，服务器在响应HEAD请求时不会回传资源的内容部分，即：响应主体。这样，我们可以不传输全部内容的情况下，就可以获取服务器的响应头信息。HEAD方法常被用于客户端查看服务器的性能。                                                                                            | 1.0、1.1             |
| DELETE  | DELETE请求用于请求服务器删除所请求URI（统一资源标识符，Uniform Resource Identifier）所标识的资源。DELETE请求后指定资源会被删除，DELETE方法也是幂等的。                                                                                                                                                                           | 1.0、1.1             |
| OPTIONS | OPTIONS请求与HEAD类似，一般也是用于客户端查看服务器的性能。 这个方法会请求服务器返回该资源所支持的所有HTTP请求方法，该方法会用&apos;*&apos;来代替资源名称，向服务器发送OPTIONS请求，可以测试服务器功能是否正常。JavaScript的XMLHttpRequest对象进行CORS跨域资源共享时，就是使用OPTIONS方法发送嗅探请求，以判断是否有对指定资源的访问权限。 | 1.1                  |
| TRACE   | TRACE请求服务器回显其收到的请求信息，该方法主要用于HTTP请求的测试或诊断。                                                                                                                                                                                                                                                        | 1.1                  |
| CONNECT | CONNECT方法是HTTP/1.1协议预留的，能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接与非加密的HTTP代理服务器的通信。                                                                                                                                                                                                 | 1.1                  |&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;幂等：对同一个系统，使用同样的条件，一次请求和重复的多次请求对系统资源的影响是一致的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PUT&lt;/code&gt; 和 &lt;code&gt;DELETE&lt;/code&gt; 方法在 &lt;code&gt;HTTP/1.1&lt;/code&gt; 中不带验证机制，任何人都可以上传或删除文件 , 存在安全性问题，因此一般的 &lt;code&gt;Web&lt;/code&gt; 网站不使用该方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http&lt;/code&gt; 协议报文中的内容都是区分大小写的，所有不区分大小写的情况都是环境做的兼容，也就是我们的请求方法应该是都要大写的。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;GET 和 POST 的区别&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;HTTP&lt;/code&gt; 协议标准中，&lt;code&gt;GET&lt;/code&gt; 的语义是请求获取指定的资源，&lt;code&gt;GET&lt;/code&gt; 方法是安全、幂等、可缓存的（除非有 &lt;code&gt;Cache-ControlHeader&lt;/code&gt;的约束）,&lt;code&gt;GET&lt;/code&gt; 方法的报文主体没有任何语义。&lt;code&gt;POST&lt;/code&gt; 的语义是根据请求负荷（报文主体）对指定的资源做出处理，具体的处理方式视资源类型而不同。&lt;code&gt;POST&lt;/code&gt; 不安全，不幂等，（大部分实现）不可缓存。对于 &lt;code&gt;GET&lt;/code&gt; 请求，在参数相同的情况下，多次请求和一次请求获取的都是相同的资源，并且不会对服务器上的资源产生影响。而 &lt;code&gt;POST&lt;/code&gt; 请求传输实体，会对服务器上的资源产生副作用。比如微博的获取最新的十篇微博就适用于 &lt;code&gt;GET&lt;/code&gt; 方法，而发微博，评论则适用于 &lt;code&gt;POST&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;在实际的浏览器环境中，&lt;code&gt;GET&lt;/code&gt; 和 &lt;code&gt;POST&lt;/code&gt; 有如下几点区别：
&lt;ul&gt;
&lt;li&gt;浏览器对 &lt;code&gt;URL&lt;/code&gt; 的长度有限制（ &lt;code&gt;URL&lt;/code&gt; 的最大长度是 &lt;code&gt;2048&lt;/code&gt; 个字符），所以 &lt;code&gt;GET&lt;/code&gt; 请求不能代替 &lt;code&gt;POST&lt;/code&gt; 请求发送大量数据；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HTML&lt;/code&gt; 标准规定 &lt;code&gt;GET&lt;/code&gt; 使用 &lt;code&gt;URL&lt;/code&gt; 或 &lt;code&gt;Cookie&lt;/code&gt; 传参，而 &lt;code&gt;POST&lt;/code&gt; 将数据放在 &lt;code&gt;BODY&lt;/code&gt; 中；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST&lt;/code&gt; 比 &lt;code&gt;GET&lt;/code&gt; 安全，因为数据在地址栏上不可见&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;持久连接&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 协议的初始版本中，每进行一次 &lt;code&gt;HTTP&lt;/code&gt; 通信就要断开一次 &lt;code&gt;TCP&lt;/code&gt; 连接。以当年的通信情况来说，因为都是些容量很小的文本传输，所以即 使这样也没有多大问题。可随着 &lt;code&gt;HTTP&lt;/code&gt; 的普及，文档中包含大量图片的 情况多了起来。比如，使用浏览器浏览一个包含多张图片的 &lt;code&gt;HTML&lt;/code&gt; 页面时，在发 送请求访问 &lt;code&gt;HTML&lt;/code&gt; 页面资源的同时，也会请求该 &lt;code&gt;HTML&lt;/code&gt; 页面里包含的 其他资源。因此，每次的请求都会造成无谓的 &lt;code&gt;TCP&lt;/code&gt; 连接建立和断开， 增加通信量的开销。&lt;/p&gt;
&lt;p&gt;为解决上述 &lt;code&gt;TCP&lt;/code&gt; 连接的问题，&lt;code&gt;HTTP/1.1&lt;/code&gt; 和一部分的 &lt;code&gt;HTTP/1.0&lt;/code&gt; 想出 了持久连接( &lt;code&gt;HTTP Persistent Connections&lt;/code&gt;，也称为 &lt;code&gt;HTTP keep-alive&lt;/code&gt; 或 &lt;code&gt;HTTP connection reuse&lt;/code&gt; )的方法。持久连接的特点是，只要任意一端没 有明确提出断开连接，则保持 &lt;code&gt;TCP&lt;/code&gt; 连接状态。持久连接的好处在于减少了 &lt;code&gt;TCP&lt;/code&gt; 连接的重复建立和断开所造成的 额外开销，减轻了服务器端的负载。另外，减少开销的那部分时间，使 HTTP 请求和响应能够更早地结束，这样 &lt;code&gt;Web&lt;/code&gt; 页面的显示速度也就相应 提高了。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;HTTP/1.1&lt;/code&gt; 中，所有的连接默认都是持久连接，但在 &lt;code&gt;HTTP/1.0&lt;/code&gt; 内 并未标准化。虽然有一部分服务器通过非标准的手段实现了持久连接，但服务器端不一定能够支持持久连接。毫无疑问，除了服务器端，客户 端也需要支持持久连接。&lt;/p&gt;
&lt;p&gt;持久连接使得多数请求以管线化( &lt;code&gt;pipelining&lt;/code&gt; )方式发送成为可能。 从前发送请求后需等待并收到响应，才能发送下一个请求。管线化技术 出现后，不用等待响应亦可直接发送下一个请求。这样就能够做到同时并行发送多个请求，而不需要一个接一个地等 待响应了。比如，当请求一个包含 10 张图片的 &lt;code&gt;HTML Web&lt;/code&gt; 页面，与挨个连接 相比，用持久连接可以让请求更快结束。而管线化技术则比持久连接还 要快。请求数越多，时间差就越明显。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/http-keep-alive%20.png&quot; alt=&quot;keep-alive&quot; title=&quot;keep-alive&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Cookie 管理状态&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 协议是无状态的，无状态的优点是可以减少服务器的 &lt;code&gt;CPU&lt;/code&gt; 和内存资源消耗，正是因为 &lt;code&gt;HTTP&lt;/code&gt; 协议本身是非常简单的，所以才会被应用在各种场景里。但是如今的 &lt;code&gt;web&lt;/code&gt; 应用已经有越来越多的状态保存需求，于是引入了 &lt;code&gt;Cookie&lt;/code&gt; 技术，通过在请求和响应报文中写入 &lt;code&gt;Cookie&lt;/code&gt; 信 息来控制客户端的状态。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cookie&lt;/code&gt; 会根据从服务器端发送的响应报文内的一个叫做 &lt;code&gt;Set-Cookie&lt;/code&gt; 的首部字段信息，通知客户端保存 &lt;code&gt;Cookie&lt;/code&gt;。当下次客户端再往该服务器发送请求时，客户端会自动在请求报文中加入 &lt;code&gt;Cookie&lt;/code&gt; 值后发送出去。服务器端发现客户端发送过来的 &lt;code&gt;Cookie&lt;/code&gt; 后，会去检查究竟是从哪一个客户端发来的连接请求，然后对比服务器上的记录，最后得到之前的状态信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/set-cookie.BICHyVAe_Z1bwXVG.webp&quot; alt=&quot;set-cookie&quot; title=&quot;set-cookie&quot;&gt;&lt;/p&gt;
&lt;h2&gt;HTTP 报文&lt;/h2&gt;
&lt;p&gt;我们的请求或响应数据会被客户端按照 &lt;code&gt;HTTP&lt;/code&gt; 协议的标准封装成符合标准的 &lt;code&gt;HTTP&lt;/code&gt; 报文再传递给传输层进行处理。我们平时在浏览器开发者工具中看到的 &lt;code&gt;Request Header&lt;/code&gt; 等信息就是 &lt;code&gt;HTTP&lt;/code&gt; 报文中的内容。有客户端发出的 &lt;code&gt;HTTP&lt;/code&gt; 报文叫做请求报文，而服务器发出的 &lt;code&gt;HTTP&lt;/code&gt; 报文叫做响应报文。&lt;code&gt;HTTP&lt;/code&gt; 报文本身是由多行(用 &lt;code&gt;CR+LF&lt;/code&gt; 作换行符)数据构成的字符串文本。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 报文大致可分为报文首部和报文主体两块。两者由最初出现 的空行( &lt;code&gt;CR+LF&lt;/code&gt; )来划分。通常，并不一定要有报文主体。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;CR&lt;/code&gt; 对应 &lt;code&gt;ASCII&lt;/code&gt; 码中的 &lt;code&gt;13&lt;/code&gt;，表示 &lt;code&gt;Carriage Return&lt;/code&gt; 回车符，&lt;code&gt;LF&lt;/code&gt; 对应 &lt;code&gt;ASCII&lt;/code&gt; 码中的 &lt;code&gt;10&lt;/code&gt;，对应 &lt;code&gt;Line Feed&lt;/code&gt; 空格符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/http-message.BZSePiUR_2spMHc.webp&quot; alt=&quot;http-message&quot; title=&quot;http-message&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/http-message-eg.Cb8DAbI8_Z2ft6Ac.webp&quot; alt=&quot;http-message-eg&quot; title=&quot;http-message-eg&quot;&gt;&lt;/p&gt;
&lt;p&gt;请求报文和响应报文的首部内容由以下数据组成: 1. 请求行:包含用于请求的方法，请求 &lt;code&gt;URI&lt;/code&gt; 和 &lt;code&gt;HTTP&lt;/code&gt; 版本。 2. 状态行:包含表明响应结果的状态码，原因短语和 &lt;code&gt;HTTP&lt;/code&gt; 版本。 3. 首部字段:包含表示请求和响应的各种条件和属性的各类首部。一般有 4 种首部，分别是:通用首部、请求首部、响应首部和实体 首部。 4. 其他:可能包含 &lt;code&gt;HTTP&lt;/code&gt; 的 &lt;code&gt;RFC&lt;/code&gt; 里未定义的首部(&lt;code&gt;Cookie&lt;/code&gt; 等)。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;报文 &lt;code&gt;message&lt;/code&gt; 主体也叫做实体 &lt;code&gt;entity&lt;/code&gt; 主体，实体包括实体首部和实体主体。实体会在传输前被压缩，达到客户端的时候再解压。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;HTTP 状态码&lt;/h2&gt;
&lt;p&gt;状态码是 &lt;code&gt;web&lt;/code&gt; 开发中经常需要面对的，比如 &lt;code&gt;JS&lt;/code&gt; 原生 &lt;code&gt;ajax&lt;/code&gt; 请求的回调函数就需要用到状态码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var xhr = new XMLHttpRequest()
xhr.open(type, url, true)
xhr.onload = function () {
  if ((xhr.status &gt;= 200 &amp;#x26;&amp;#x26; xhr.status &amp;#x3C; 300) || xhr.status == 304) {
    onsuccess(xhr.responseText)
  } else {
    onerror()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的 &lt;code&gt;xhr.status&lt;/code&gt; 就是返回的状态码。&lt;code&gt;HTTP&lt;/code&gt; 协议中，&lt;code&gt;HTTP&lt;/code&gt; 状态码负责表示客户端 &lt;code&gt;HTTP&lt;/code&gt; 请求的返回结果、标记服务器 端的处理是否正常、通知出现的错误等工作。状态码分类如下：&lt;/p&gt;
&lt;p&gt;| 状态码 | 类别                           | 原因短语                   |
| ------ | ------------------------------ | -------------------------- |
| 1XX    | Informational(信息性状态码)    | 接收的请求正在处理         |
| 2XX    | Success(成功状态码)            | 请求正常处理完毕           |
| 3XX    | Redirection(重定向状态码)      | 需要进行附加操作以完成请求 |
| 4XX    | Client Error(客户端错误状态码) | 服务器无法处理请求         |
| 5XX    | Server Error(服务器错误状态码) | 服务器处理请求出错         |&lt;/p&gt;
&lt;h2&gt;200 OK&lt;/h2&gt;
&lt;p&gt;是我们最常遇到的状态码，表示客户端的请求已经被服务器正常处理了。根据请求的方法不同，返回的内容也不同，比如使用 &lt;code&gt;GET&lt;/code&gt; 方法请求时，对应的请求资源会作为报文实体返回给客户端；而使用 &lt;code&gt;HEAD&lt;/code&gt; 方法请求时只返回请求资源的实体首部，而实体主体（也就是报文主体）不会返回。&lt;/p&gt;
&lt;h2&gt;204 No Content&lt;/h2&gt;
&lt;p&gt;表示服务器接收的请求已成功处理，但在返回的响应报文中不包含实体的部分。一般用在只需要给服务器发送信息但不需要在客户端更新页面的时候使用。比如在 &lt;code&gt;PUT&lt;/code&gt; 请求中进行资源更新，但是不需要改变当前展示给用户的页面，那么返回 &lt;code&gt;204 No Content&lt;/code&gt;。如果新创建了资源，那么返回 &lt;code&gt;201 Created&lt;/code&gt; 。如果页面需要更新以反映更新后的资源，那么需要返回 &lt;code&gt;200&lt;/code&gt; 。&lt;code&gt;204&lt;/code&gt; 响应默认是可以被缓存的。在响应中需要包含头信息 &lt;code&gt;ETag&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;206 Partical Content&lt;/h2&gt;
&lt;p&gt;该状态码表示客户端只请求了某个资源的一部分，比如客户端的HTTP报文首部有 &lt;code&gt;Range: bytes=5001-10000&lt;/code&gt; 就意味着它只想请求对应资源的第 5001 字节至第 10000 字节的内容。服务器成功执行该请求后会返回状态码 &lt;code&gt;206 Partical Content&lt;/code&gt;，响应报文中会包含指定的实体内容。&lt;/p&gt;
&lt;h2&gt;301 Moved Permarnently&lt;/h2&gt;
&lt;p&gt;永久重定向。当请求的资源已经被分配到新的 &lt;code&gt;URL&lt;/code&gt; ，比如网站更换了域名，对应网站的 &lt;code&gt;web&lt;/code&gt; 服务器配置了 &lt;code&gt;301&lt;/code&gt; 重定向到新的域名，这时候我们用原来的域名访问网站就会返回 &lt;code&gt;301&lt;/code&gt;，然后重定向到新的域名。&lt;/p&gt;
&lt;h2&gt;302 Found&lt;/h2&gt;
&lt;p&gt;和 &lt;code&gt;301&lt;/code&gt; 类似，不同的是 &lt;code&gt;302&lt;/code&gt; 的意思是网站资源被临时转移，所以这个重定下是临时的，只有本次请求生效，当你下次访问资源的时候还按原来的地址请求即可。&lt;/p&gt;
&lt;h2&gt;303 See Other&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;303&lt;/code&gt; 是 &lt;code&gt;HTTP/1.1&lt;/code&gt; 中新加入的状态码，因为在 &lt;code&gt;RFC1945&lt;/code&gt; 和 &lt;code&gt;RFC2068&lt;/code&gt; 规范中，&lt;code&gt;302&lt;/code&gt; 状态码是不允许客户端在重定向的时候更改请求方法的，比如你是用 &lt;code&gt;POST&lt;/code&gt; 请求给服务器的程序接口发送数据，那么不能自动的向新的 &lt;code&gt;URI&lt;/code&gt; 发送重复请求，必须跟用户确认是否该重发，因为第二次 &lt;code&gt;POST&lt;/code&gt; 时，环境可能已经发生变化（ &lt;code&gt;POST&lt;/code&gt; 请求不是幂等的）。为了应对这种情况添加了 &lt;code&gt;303&lt;/code&gt; 状态码，当服务器对 &lt;code&gt;POST&lt;/code&gt; 请求返回 &lt;code&gt;303&lt;/code&gt; 状态码时，客户端直接用 &lt;code&gt;GET&lt;/code&gt; 方法重定向到新的 &lt;code&gt;URI&lt;/code&gt; 即可，而不需要询问用户。其实在标准制定之前，大部分浏览器就是按照 &lt;code&gt;303&lt;/code&gt; 的方式来处理 &lt;code&gt;302&lt;/code&gt; 状态码的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当 &lt;code&gt;301&lt;/code&gt;、&lt;code&gt;302&lt;/code&gt;、&lt;code&gt;303&lt;/code&gt; 响应状态码返回时，几乎所有的浏览器都会把 &lt;code&gt;POST&lt;/code&gt; 改成 &lt;code&gt;GET&lt;/code&gt;，并删除请求报文内的主体，之后请求会自动再次发送。&lt;code&gt;301&lt;/code&gt;、&lt;code&gt;302&lt;/code&gt; 标准是禁止将 &lt;code&gt;POST&lt;/code&gt; 方法改变成 &lt;code&gt;GET&lt;/code&gt; 方法的，但实际使用时大家都会这么做。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;304 Not Modified&lt;/h2&gt;
&lt;p&gt;当客户端发送附带条件的安全请求（采用 &lt;code&gt;GET&lt;/code&gt; 或者 &lt;code&gt;HEAD&lt;/code&gt; 方法的请求报文中包含 &lt;code&gt;If-Match&lt;/code&gt;，&lt;code&gt;If-Modified- Since&lt;/code&gt;，&lt;code&gt;If-None-Match&lt;/code&gt;，&lt;code&gt;If-Range&lt;/code&gt;，&lt;code&gt;If-Unmodified-Since&lt;/code&gt; 中任一首部）时，服务器端允许请求访问资源，但因发生请求未满足条件的情况后，直接返回 &lt;code&gt;304 Not Modified&lt;/code&gt; (服务器端资源未改变，可直接使用客户端未过期的缓存)。 &lt;code&gt;304&lt;/code&gt; 状态码返回时，不包含任何响应的主体部分。&lt;code&gt;304&lt;/code&gt; 虽然被划分在 &lt;code&gt;3XX&lt;/code&gt; 类别中，但是和重定向没有关系。&lt;/p&gt;
&lt;h2&gt;307 Temporary Redirect&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;307&lt;/code&gt; 和 &lt;code&gt;303&lt;/code&gt; 是配套产生的，它们将 &lt;code&gt;302&lt;/code&gt; 的细分为了更换请求方法和不更换请求方法两种，&lt;code&gt;303&lt;/code&gt; 会强制要求客户端用GET方法重定向，而 &lt;code&gt;307&lt;/code&gt; 则是对于 &lt;code&gt;POST&lt;/code&gt; 请求的重定向还是使用 &lt;code&gt;POST&lt;/code&gt; 方法请求。 1. &lt;code&gt;303&lt;/code&gt; :对于 &lt;code&gt;POST&lt;/code&gt; 请求，它表示请求已经被处理，客户端可以接着使用 &lt;code&gt;GET&lt;/code&gt; 方法去请求 &lt;code&gt;Location&lt;/code&gt; 里的URI。 2. &lt;code&gt;307&lt;/code&gt; ：对于 &lt;code&gt;POST&lt;/code&gt; 请求，表示请求还没有被处理，客户端应该向 &lt;code&gt;Location&lt;/code&gt;里的 &lt;code&gt;URI&lt;/code&gt; 重新发起 &lt;code&gt;POST&lt;/code&gt; 请求。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;虽然标准中规定了 &lt;code&gt;302&lt;/code&gt; 的细分 &lt;code&gt;303&lt;/code&gt; 和 &lt;code&gt;307&lt;/code&gt;，但是在浏览器的实现上把 &lt;code&gt;302&lt;/code&gt; 实现成了标准中的 &lt;code&gt;303&lt;/code&gt;，而且标准现在也妥协了，允许了浏览器的这种实现。2014 年 6 月的 &lt;code&gt;RFC 7231&lt;/code&gt; 中，修改了对 &lt;code&gt;302&lt;/code&gt; 的定义：&lt;code&gt;The user agent MAY use the Location field value for automatic redirection.&lt;/code&gt;，原来这里是 &lt;code&gt;must not&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;400 Bad Request&lt;/h2&gt;
&lt;p&gt;该状态码表示请求报文中存在语法错误。当错误发生时，需修改请求 的内容后再次发送请求。另外，浏览器会像 &lt;code&gt;200 OK&lt;/code&gt; 一样对待该状态码。&lt;/p&gt;
&lt;h2&gt;401 Unauthorized&lt;/h2&gt;
&lt;p&gt;该状态码表示发送的请求需要有通过 &lt;code&gt;HTTP&lt;/code&gt; 认证( &lt;code&gt;BASIC&lt;/code&gt; 认证、 &lt;code&gt;DIGEST&lt;/code&gt; 认证)的认证信息。另外若之前已进行过 &lt;code&gt;1&lt;/code&gt; 次请求，则表示用 户认证失败。返回含有 &lt;code&gt;401&lt;/code&gt; 的响应必须包含一个适用于被请求资源的 &lt;code&gt;WWW- Authenticate&lt;/code&gt; 首部用以质询( &lt;code&gt;challenge&lt;/code&gt; )用户信息。当浏览器初次接收 到 &lt;code&gt;401&lt;/code&gt; 响应，会弹出认证用的对话窗口。这个状态类似于 &lt;code&gt;403&lt;/code&gt;， 但是在该情况下，依然可以进行身份验证。&lt;/p&gt;
&lt;h2&gt;403 Forbidden&lt;/h2&gt;
&lt;p&gt;该状态码表明对请求资源的访问被服务器拒绝了。服务器端没有必 要给出拒绝的详细理由，但如果想作说明的话，可以在实体的主体部分 对原因进行描述，这样就能让用户看到了。未获得文件系统的访问授权，访问权限出现某些问题(从未授权的 发送源 &lt;code&gt;IP&lt;/code&gt; 地址试图访问)等列举的情况都可能是发生 &lt;code&gt;403&lt;/code&gt; 的原因。&lt;/p&gt;
&lt;h2&gt;404 Not Found&lt;/h2&gt;
&lt;p&gt;该状态码表明服务器上无法找到请求的资源。除此之外，也可以在 服务器端拒绝请求且不想说明理由时使用。&lt;/p&gt;
&lt;h2&gt;500 Internal Server Error&lt;/h2&gt;
&lt;p&gt;该状态码表明服务器端在执行请求时发生了错误。也有可能是 &lt;code&gt;Web&lt;/code&gt; 应用存在的 &lt;code&gt;bug&lt;/code&gt; 或某些临时的故障。&lt;/p&gt;
&lt;h2&gt;503 Service Unavailable&lt;/h2&gt;
&lt;p&gt;该状态码表明服务器暂时处于超负载或正在进行停机维护，现在无 法处理请求。如果事先得知解除以上状况需要的时间，最好写入 &lt;code&gt;Retry- After&lt;/code&gt; 首部字段再返回给客户端。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不少返回的状态码响应都是错误的，但是用户可能察觉不到这点。比 如 &lt;code&gt;Web&lt;/code&gt; 应用程序内部发生错误，状态码依然返回 &lt;code&gt;200 OK&lt;/code&gt;，这种情况也经常遇到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;HTTP首部&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 首部是我们在 &lt;code&gt;web&lt;/code&gt; 开发中了解请求和响应最重要的部分，能够读懂报文的首部才能处理好请求和响应。前面的内容我们已经看到了 &lt;code&gt;HTTP&lt;/code&gt; 报文的结构，其中在开发中最重要的就是报文的首部，首部中包含了为客户端和服务器分别处理请求和响应提供的所需要的信息比如主体大小、所使用 的语言、认证信息等内容。通过首部中字段的值，我们能了解这次请求或者响应的细节，以及采取对应的处理方式。同样的，对于有特定的需求的请求我们也可以通过改变封装报文的头部来传达给服务器。这一节就着重讨论 &lt;code&gt;HTTP&lt;/code&gt; 首部中的重要字段，根据实际用途被分为四种类型： 1. 通用首部字段( &lt;code&gt;General Header Fields&lt;/code&gt; )：请求报文和响应报文两方都会使用的首部。 2. 请求首部字段( &lt;code&gt;Request Header Fields&lt;/code&gt; )：从客户端向服务器端发送请求报文时使用的首部。补充了请求的附 加内容、客户端信息、响应内容相关优先级等信息。 3. 响应首部字段( &lt;code&gt;Response Header Fields&lt;/code&gt; )：从服务器端向客户端返回响应报文时使用的首部。补充了响应的附 加内容，也会要求客户端附加额外的内容信息。 4. 实体首部字段( &lt;code&gt;Entity Header Fields&lt;/code&gt; )：针对请求报文和响应报文的实体部分使用的首部。补充了资源内容 更新时间等与实体有关的信息。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 首部字段是由首部字段名和字段值构成的，中间用冒号“:” 分隔。：&lt;code&gt;首部字段名: 字段值&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例如，在 &lt;code&gt;HTTP&lt;/code&gt; 首部中以 &lt;code&gt;Content-Type&lt;/code&gt; 这个字段来表示报文主体的媒体对象类型。&lt;code&gt;Content-Type: text/html&lt;/code&gt; 首部字段名为 &lt;code&gt;Content-Type&lt;/code&gt;，字符串 &lt;code&gt;text/html&lt;/code&gt; 是字段值。&lt;/p&gt;
&lt;p&gt;字段值对应单个 &lt;code&gt;HTTP&lt;/code&gt; 首部字段可以有多个值 &lt;code&gt;Keep-Alive: timeout=15, max=100&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 首部字段将定义成缓存代理和非缓存代理的行为，分成 2 种类型。 1. 端到端首部( &lt;code&gt;End-to-end Header&lt;/code&gt; ) ：分在此类别中的首部会转发给请求 / 响应对应的最终接收目标，且 必须保存在由缓存生成的响应中，另外规定它必须被转发。 2. 逐跳首部( &lt;code&gt;Hop-by-hop Header&lt;/code&gt; ) ：分在此类别中的首部只对单次转发有效，会因通过缓存或代理而不 再转发。&lt;code&gt;HTTP/1.1&lt;/code&gt; 和之后版本中，如果要使用 &lt;code&gt;hop-by-hop&lt;/code&gt; 首部， 需要在 &lt;code&gt;Connection&lt;/code&gt; 字段中写入。&lt;code&gt;HTTP/1.1&lt;/code&gt; 一共提供了8个逐条首部的字段&lt;code&gt;Connection&lt;/code&gt;，&lt;code&gt;Keep-Alive&lt;/code&gt;，&lt;code&gt;Proxy-Authenticate&lt;/code&gt;，&lt;code&gt;Proxy-Authorization&lt;/code&gt;，&lt;code&gt;Trailer&lt;/code&gt;，&lt;code&gt;TE&lt;/code&gt;，&lt;code&gt;Transfer-Encoding&lt;/code&gt;，&lt;code&gt;Upgrade&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当 &lt;code&gt;HTTP&lt;/code&gt; 报文首部中出现了两个或两个以上具有相同首部字段名时会 怎么样?这种情况在规范内尚未明确，根据浏览器内部处理逻辑的不同， 结果可能并不一致。有些浏览器会优先处理第一次出现的首部字段，而有 些则会优先处理最后出现的首部字段。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;通用首部字段&lt;/h2&gt;
&lt;p&gt;| 首部字段名        | 说明                       |
| ----------------- | -------------------------- |
| Cache-Control     | 控制缓存的行为             |
| Connection        | 逐跳首部、连接的管理       |
| Date              | 创建报文的日期时间         |
| Pragma            | 报文指令                   |
| Trailer           | 报文末端的首部一览         |
| Transfer-Encoding | 指定报文主体的传输编码方式 |
| Upgrade           | 升级为其他协议             |
| Via               | 代理服务器的相关信息       |
| Warning           | 错误通知                   |&lt;/p&gt;
&lt;h2&gt;请求首部字段&lt;/h2&gt;
&lt;p&gt;| 首部字段名          | 说明                                          |
| ------------------- | --------------------------------------------- |
| Accept              | 用户代理可处理的媒体类型                      |
| Accept-Charset      | 优先的字符集                                  |
| Accept-Encoding     | 优先的内容编码                                |
| Accept-Language     | 优先的语言(自然语言)                          |
| Authorization       | Web 认证信息                                  |
| Expect              | 期待服务器的特定行为                          |
| From                | 用户的电子邮箱地址                            |
| Host                | 请求资源所在服务器                            |
| If-Match            | 比较实体标记(ETag)                            |
| If-Modified-Since   | 比较资源的更新时间                            |
| If-None-Match       | 比较实体标记(与 If-Match 相反)                |
| If-Range            | 资源未更新时发送实体 Byte 的范围请求          |
| If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) |
| Max-Forwards        | 最大传输逐跳数                                |
| Proxy-Authorization | 代理服务器要求客户端的认证信息                |
| Range               | 实体的字节范围请求                            |
| Referer             | 对请求中 URI 的原始获取方                     |
| TE                  | 传输编码的优先级                              |
| User-Agent          | HTTP 客户端程序的信息                         |&lt;/p&gt;
&lt;h2&gt;响应首部字段&lt;/h2&gt;
&lt;p&gt;| 首部字段名         | 说明                         |
| ------------------ | ---------------------------- |
| Accept-Ranges      | 是否接受字节范围请求         |
| Age                | 推算资源创建经过时间         |
| ETag               | 资源的匹配信息               |
| Location           | 令客户端重定向至指定 URI     |
| Proxy-Authenticate | 代理服务器对客户端的认证信息 |
| Retry-After        | 对再次发起请求的时机要求     |
| Server             | HTTP 服务器的安装信息        |
| Vary               | 代理服务器缓存的管理信息     |
| WWW-Authenticate   | 服务器对客户端的认证信息     |&lt;/p&gt;
&lt;h2&gt;实体首部字段&lt;/h2&gt;
&lt;p&gt;| 首部字段名       | 说明                       |
| ---------------- | -------------------------- |
| Allow            | 资源可支持的 HTTP 方法     |
| Content-Encoding | 实体主体适用的编码方式     |
| Content-Language | 实体主体的自然语言         |
| Content-Length   | 实体主体的大小(单位 :字节) |
| Content-Location | 替代对应资源的 URI         |
| Content-MD5      | 实体主体的报文摘要         |
| Content-Range    | 实体主体的位置范围         |
| Content-Type     | 实体主体的媒体类型         |
| Expires          | 实体主体过期的日期时间     |
| Last-Modified    | 资源的最后修改日期时间     |&lt;/p&gt;
&lt;h2&gt;Cookie 服务的首部字段&lt;/h2&gt;
&lt;p&gt;管理服务器与客户端之间状态的 &lt;code&gt;Cookie&lt;/code&gt;，虽然没有被编入标准化 &lt;code&gt;HTTP/1.1&lt;/code&gt; 的 &lt;code&gt;RFC2616&lt;/code&gt; 中，但在 &lt;code&gt;Web&lt;/code&gt; 网站方面得到了广泛的应用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cookie&lt;/code&gt; 的工作机制是用户识别及状态管理。&lt;code&gt;Web&lt;/code&gt; 网站为了管理用户 的状态会通过 &lt;code&gt;Web&lt;/code&gt; 浏览器，把一些数据临时写入用户的计算机内。接着当用户访问该 &lt;code&gt;Web&lt;/code&gt; 网站时，可通过通信方式取回之前存放的 &lt;code&gt;Cookie&lt;/code&gt;。调用 &lt;code&gt;Cookie&lt;/code&gt; 时，由于可校验 &lt;code&gt;Cookie&lt;/code&gt; 的有效期，以及发送方的域、 路径、协议等信息，所以正规发布的 &lt;code&gt;Cookie&lt;/code&gt; 内的数据不会因来自其他 &lt;code&gt;Web&lt;/code&gt; 站点和攻击者的攻击而泄露。&lt;/p&gt;
&lt;p&gt;不仅服务器可以设置 &lt;code&gt;cookie&lt;/code&gt; 传给客户端，客户端也可以设置 &lt;code&gt;cookie&lt;/code&gt; 在 &lt;code&gt;HTTP&lt;/code&gt; 报文中传递给服务器。&lt;/p&gt;
&lt;p&gt;| 首部字段名 | 说明                             | 首部类型     |
| ---------- | -------------------------------- | ------------ |
| Set-Cookie | 开始状态管理所使用的 Cookie 信息 | 响应首部字段 |
| Cookie     | 服务器接收到的 Cookie 信息       | 请求首部字段 |&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Set-Cookie&lt;/code&gt; 字段：&lt;code&gt;Set-Cookie: status=enable; expires=Tue, 05 Jul 2011 07:26:31 GMT; path=/; domain=clloz.com;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| 属性         | 说明                                                                            |
| ------------ | ------------------------------------------------------------------------------- |
| NAME=VALUE   | 赋予 Cookie 的名称和其值(必需项)                                                |
| expires=DATE | Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止)                           |
| path=PATH    | 将服务器上的文件目录作为 Cookie 的适用对象(若不指定则 默认为文档所在的文件目录) |
| domain= 域名 | 作为 Cookie 适用对象的域名(若不指定则默认为创建 Cookie 的服务器的域名)          |
| Secure       | 仅在 HTTPS 安全通信时才会发送 Cookie                                            |
| HttpOnly     | 加以限制，使 Cookie 不能被 JavaScript 脚本访问                                  |&lt;/p&gt;
&lt;h2&gt;其他首部字段&lt;/h2&gt;
&lt;h3&gt;X-Frame-Options&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 响应首部，用于控制网站内容在其他 &lt;code&gt;Web&lt;/code&gt; 网站的 &lt;code&gt;Frame&lt;/code&gt; 标签内的显示问题。其主要目的是为了防止点击劫持( &lt;code&gt;clickjacking&lt;/code&gt; )攻击。&lt;code&gt;DENY&lt;/code&gt; :拒绝；&lt;code&gt;SAMEORIGIN&lt;/code&gt; :仅同源域名下的页面( &lt;code&gt;Top-level-browsing-context&lt;/code&gt; )匹配时许可。( 比如，当指定 &lt;code&gt;http://clloz.com/index.html&lt;/code&gt; 页面为 &lt;code&gt;SAMEORIGIN&lt;/code&gt; 时，那么 &lt;code&gt;clloz.com&lt;/code&gt; 上所有页面的 &lt;code&gt;frame&lt;/code&gt; 都被允许可加载该页面，而 &lt;code&gt;example.com&lt;/code&gt; 等其他域名的页面就不行了)。&lt;/p&gt;
&lt;p&gt;所有主流浏览器都支持这个字段，我们也可以在 &lt;code&gt;web&lt;/code&gt; 服务器中设置 &lt;code&gt;X-Frame-Options&lt;/code&gt; 的默认值，比如在 &lt;code&gt;apache2&lt;/code&gt; 中写入如下配置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;&amp;#x3C;IfModule mod_headers.c&gt;
     Header append X-FRAME-OPTIONS &quot;SAMEORIGIN&quot;
&amp;#x3C;/IfModule&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;X-XSS-Protection&lt;/h3&gt;
&lt;p&gt;首部字段 &lt;code&gt;X-XSS-Protection&lt;/code&gt; 属于 &lt;code&gt;HTTP&lt;/code&gt; 响应首部，它是针对跨站脚本攻击( &lt;code&gt;XSS&lt;/code&gt; )的一种对策，用于控制浏览器 &lt;code&gt;XSS&lt;/code&gt; 防护机制的开关。首部字段 &lt;code&gt;X-XSS-Protection&lt;/code&gt; 可指定的字段值如下。 1. 0 :将 XSS 过滤设置成无效状态 2. 1 :将 XSS 过滤设置成有效状态&lt;/p&gt;
&lt;h2&gt;HTTP 的瓶颈&lt;/h2&gt;
&lt;p&gt;现在无论是我们浏览网页还是用 &lt;code&gt;app&lt;/code&gt; 订餐，都会大量使用 &lt;code&gt;HTTP&lt;/code&gt; 协议，如今开发的接入互联网的软件大多都使用了 &lt;code&gt;HTTP&lt;/code&gt; 协议，为什么 &lt;code&gt;HTTP&lt;/code&gt; 协议为什么受欢迎呢。从底层来考虑的话，不同的协议在访问计算机或网络设备的时候需要通过不同的接口，如果你安装的应用使用的协议是当前设备上防火墙关闭的，那么你必须修改防火墙设置，而大部分用户不会特地去修改。在互联网功能中使用者最多的就是 &lt;code&gt;Web&lt;/code&gt; ，不管你懂不懂电脑，你都会用浏览器浏览网页，所以绝大部分计算机或者网络设备都开放了Web访问的接口，&lt;code&gt;Web&lt;/code&gt; 是基于 &lt;code&gt;HTTP&lt;/code&gt; 协议运作 的，因此在构建 &lt;code&gt;Web&lt;/code&gt; 服务器或访问 &lt;code&gt;Web&lt;/code&gt; 站点时，需事先设置防火墙 &lt;code&gt;HTTP(80/tcp)&lt;/code&gt; 和 &lt;code&gt;HTTPS(443/tcp)&lt;/code&gt; 的权限。大部分设备的 &lt;code&gt;80&lt;/code&gt; 和 &lt;code&gt;443&lt;/code&gt; 端口都是打开的，正是因为这一接入的优势使得大家开发应用的时候会选择 &lt;code&gt;HTTP&lt;/code&gt; ，因为不再需要用户特地去修改防火墙设置，并且浏览器作为 &lt;code&gt;HTTP&lt;/code&gt; 的客户端之一，可以说是全世界安装得最多的应用之一， &lt;code&gt;HTTP&lt;/code&gt; 的服务器已经具有相当规模，也让 &lt;code&gt;HTTP&lt;/code&gt; 更加有优势。&lt;/p&gt;
&lt;p&gt;但是，正是由于 &lt;code&gt;HTTP&lt;/code&gt; 的使用者越来越多，凸显出当前 &lt;code&gt;HTTP&lt;/code&gt; 标准中的许多不足。比如如今有很多 &lt;code&gt;SNS&lt;/code&gt; 网站，数以千万记的用户在网站上发布自己的内容，服务器在短时间内就会发生非常多的资源更新，但是HTTP却无法及时把这些更新显示到客户端，因为服务器是无法主动和客户端通信的，必须频繁地通过客户端发起请求询问服务器是否有更新，相信做过前端开发的同学都遇到过这种情况，如果有更新还好，可是如果没有更新，那么这次请求就是浪费了。虽然2005年的 &lt;code&gt;AJAX&lt;/code&gt; 技术让我们可以做到局部刷新页面了，但是对于访问量众多的大型应用来说，这依然不是一个好的解决办法。&lt;/p&gt;
&lt;p&gt;HTTP的瓶颈可以总结为以下几点： 1. 一条连接上只可发送一个请求。 2. 请求只能从客户端开始。客户端不可以接收除响应以外的指令。 3. 请求 / 响应首部未经压缩就发送。首部信息越多延迟越大。 4. 发送冗长的首部。每次互相发送相同的首部造成的浪费较多。 5. 可任意选择数据压缩格式。非强制压缩发送。&lt;/p&gt;
&lt;h2&gt;HTTP/2&lt;/h2&gt;
&lt;p&gt;为了消除&lt;code&gt;HTTP&lt;/code&gt;瓶颈，&lt;code&gt;Google&lt;/code&gt;在2010年首次作出了尝试，在 &lt;code&gt;HTTP&lt;/code&gt; 和 &lt;code&gt;SSL&lt;/code&gt; 之间加入了一个会话层，实现了一个 &lt;code&gt;TCP&lt;/code&gt; 连接可以无限制地处理多个 &lt;code&gt;HTTP&lt;/code&gt; 请求，所有的请求都在一次 &lt;code&gt;TCP&lt;/code&gt; 连接上完成，并且支持给这些请求设置优先级。&lt;code&gt;SPDY&lt;/code&gt; 压缩了 &lt;code&gt;HTTP&lt;/code&gt; 首部，这样 &lt;code&gt;HTTP&lt;/code&gt; 通信产生的数据包能够变得更小更少。&lt;code&gt;SPDY&lt;/code&gt; 同时也支持服务器主动推送消息给客户端，不必等待客户端的请求。&lt;/p&gt;
&lt;p&gt;于1999年制定的 &lt;code&gt;HTTP/1.1&lt;/code&gt; 显然已经无法适应今天的 &lt;code&gt;Web&lt;/code&gt;，下一代 &lt;code&gt;HTTP&lt;/code&gt; 协议的标准呼之欲出。在SPDY和 &lt;code&gt;WebScoket&lt;/code&gt; 等技术的基础上，&lt;code&gt;HTTP/2&lt;/code&gt; 的首个草稿在2012年完成，内容基本和 &lt;code&gt;SPDY&lt;/code&gt; 相同。&lt;code&gt;HTTP/2&lt;/code&gt; 标准于2015年5月以 &lt;code&gt;RFC 7540&lt;/code&gt; 正式发表。如今大多数浏览器和 &lt;code&gt;web&lt;/code&gt; 服务器都支持 &lt;code&gt;HTTP/2&lt;/code&gt;，同样标准也在一直向前推进，大部分网站以及启用 &lt;code&gt;HTTP/2&lt;/code&gt; 标准。&lt;code&gt;HTTP/2&lt;/code&gt; 带来的提升可以看&lt;a href=&quot;http2.akamai.com/demo&quot; title=&quot;http2.akamai.com/demo&quot;&gt;http2.akamai.com/demo&lt;/a&gt;，这里的地球图片是用361块小图片拼接而成，两边分别是用 &lt;code&gt;HTTP/1.1&lt;/code&gt; 和 &lt;code&gt;HTTP/2&lt;/code&gt; 进行请求，可以看出速度差距很大。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/http2.CfCtXTz4_Znt6PD.webp&quot; alt=&quot;http2&quot; title=&quot;http2&quot;&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;由于网络的内容太多，文章已经太长了，本来想加在本文中的 &lt;code&gt;HTTPS&lt;/code&gt; 和 &lt;code&gt;Web&lt;/code&gt; 安全的内容只能写到另一篇文章中了。这篇文章结合&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/04/25/how-browser-work/&quot; title=&quot;浏览器渲染过程及JS引擎浅析&quot;&gt;浏览器渲染过程及JS引擎浅析&lt;/a&gt;，我们已经能够清晰地了解从浏览器发送HTTP请求给服务器到服务器返回文档，再到浏览器接收到文档进行渲染，最后我们能够看到渲染好的画面的过程，也就可以回答从输入 &lt;code&gt;URL&lt;/code&gt; 到页面展示中间发生了什么，两篇文章都非常长，因为内容真的很多，可能也有一些不正确和表述的不好的地方，欢迎指出问题，或者有遗漏的细节也欢迎讨论。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/59a0472f5188251240632f92&quot; title=&quot;网络七层模型与四层模型区别&quot;&gt;网络七层模型与四层模型区别&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/TCP/IP%E5%8D%8F%E8%AE%AE%E6%97%8F&quot; title=&quot;互联网协议套件&quot;&gt;互联网协议套件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/zh-hans/%E4%B8%87%E7%BB%B4%E7%BD%91&quot; title=&quot;万维网&quot;&gt;万维网&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html&quot; title=&quot;互联网协议入门&quot;&gt;互联网协议入门&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/entry/5b004085f265da0b886daf7c&quot; title=&quot;HTTP请求方法详解&quot;&gt;HTTP请求方法详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000004014583&quot; title=&quot;GET和POST的区别&quot;&gt;GET和POST的区别&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/5b10be81518825139e0d8160&quot; title=&quot;完整一次HTTP请求响应过程&quot;&gt;完整一次HTTP请求响应过程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/cswuyg/p/3871976.html&quot; title=&quot;HTTP状态码302，303和307&quot;&gt;HTTP状态码302，303和307&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/network.D8pkh64H.jpg"/><enclosure url="/_astro/network.D8pkh64H.jpg"/></item><item><title>HTTPS</title><link>https://clloz.com/blog/https</link><guid isPermaLink="true">https://clloz.com/blog/https</guid><pubDate>Thu, 02 May 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在上一篇介绍 &lt;code&gt;HTTP&lt;/code&gt; 的文章中，我们了解了 &lt;code&gt;HTTP&lt;/code&gt; 协议在通信过程中的细节，但是其实 &lt;code&gt;HTTP&lt;/code&gt; 除了我们说的一些性能瓶颈之外，还有一些安全性的问题。&lt;/p&gt;
&lt;h2&gt;HTTP 的安全性问题&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;通信使用明文，不加密，内容可能会被窃听&lt;/li&gt;
&lt;li&gt;不会验证通信方的身份，可能会被伪装的请求或响应欺骗&lt;/li&gt;
&lt;li&gt;无法验证 &lt;code&gt;HTTP&lt;/code&gt; 报文的完整性，内容可能被篡改&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;通信使用明文会被窃听&lt;/h2&gt;
&lt;p&gt;我们计算机之间的通信要经过很多中间设备才能传输到对方，这些数据包在中间设备上流动的时候是能够被看到的，如果我们遇到了恶意窥视，那么我们的信息就会被别人获取，如果这个信息是明文传输的，那么对方就直接能够知道我们所发送的内容。即使是信息加密过了，依然能够被通信线路上的其他人获取，但是如果我们通过安全的方法加密，那么黑客即使拿到我们的数据包也不能破解报文中的内容，那么就是没意义的，我们的数据仍然是安全的。就好像我们寄快递，如果没有包装，每个人人都能直接看到我们寄的什么，但是如果我们用纸箱抱起来（相当于简单加密，很容易被破解），那么我们寄的东西不那么容易被看到了，但是对于别有用心的人还是可以打开箱子看到里面的内容。所以我们可以采取更安全的加密方式，用密码箱把我们的东西放进去，然后再寄这个密码箱就可以了，现在除非有人能够知道我们密码箱的密码，否则是不能破解我们的内容了，基本就是安全的了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/http-danger.DM83mN14_Z1BhsCb.webp&quot; alt=&quot;http-danger&quot; title=&quot;http-danger&quot;&gt;&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;HTTP&lt;/code&gt; 请求，我们可以非常容易地获得报文中地内容，比如下图就是我用 &lt;code&gt;wireshark&lt;/code&gt; 捕获的我本地的 &lt;code&gt;HTTP&lt;/code&gt; 请求报文，我们可以看出报文是直接可以从 &lt;code&gt;16&lt;/code&gt; 进制翻译过来地，我们的首部字段以及请求链接等信息都能看的一清二楚。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/wireshark-http.CoNxvt91_Z1SIrXO.webp&quot; alt=&quot;wireshark-http&quot; title=&quot;wireshark-http&quot;&gt;&lt;/p&gt;
&lt;h2&gt;身份地伪装&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 协议中地请求和响应不会对通信方地身份进行确认，比如任何人都可以伪装成用户想服务器发起请求，如果服务器不进行身份地验证，那么用户地资料很有可能泄漏。同样我们客户端发起地请求也有可能发送给一台伪装成目标服务器的设备，那么我们的信息安全也会受到威胁。甚至有人恶意地对服务器不断发送无效请求也会被受理最终导致服务器瘫痪。不能确定通信方的身份带来的风险可以归纳为以下几点： 1. 无法确定请求发送至目标的 &lt;code&gt;Web&lt;/code&gt; 服务器是否是按真实意图返回 响应的那台服务器。有可能是已伪装的 &lt;code&gt;Web&lt;/code&gt; 服务器。 2. 无法确定响应返回到的客户端是否是按真实意图接收响应的那个 客户端。有可能是已伪装的客户端。 3. 无法确定正在通信的对方是否具备访问权限。因为某些 &lt;code&gt;Web&lt;/code&gt; 服 务器上保存着重要的信息，只想发给特定用户通信的权限。 4. 无法判定请求是来自何方、出自谁手。 5. 即使是无意义的请求也会照单全收。无法阻止海量请求下的 &lt;code&gt;DoS&lt;/code&gt; 攻击( &lt;code&gt;Denial of Service&lt;/code&gt;，拒绝服务攻击)。&lt;/p&gt;
&lt;h2&gt;无法验证报文的完整性&lt;/h2&gt;
&lt;p&gt;既然我们的报文在传输过程中可能被监听，那么同样，在请求从客户端发出到服务器接收到请求的这一过程中，我们的报文也有可能被篡改，而我们的服务器是无法判断报文是否被修改过。&lt;/p&gt;
&lt;p&gt;比如，从某个 &lt;code&gt;Web&lt;/code&gt; 网站上下载内容，是无法确定客户端下载的文 件和服务器上存放的文件是否前后一致的。文件内容在传输途中可能已 经被篡改为其他的内容。即使内容真的已改变，作为接收方的客户端也 是觉察不到的。&lt;/p&gt;
&lt;p&gt;像这样，请求或响应在传输途中，遭攻击者拦截并篡改内容的攻击 称为中间人攻击( &lt;code&gt;Man-in-the-Middle attack&lt;/code&gt;，&lt;code&gt;MITM&lt;/code&gt; )。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/middle-attack.uflspxqY_Z7x7GR.webp&quot; alt=&quot;middle-attack&quot; title=&quot;middle-attack&quot;&gt;&lt;/p&gt;
&lt;h2&gt;HTTPS&lt;/h2&gt;
&lt;p&gt;我们上面介绍了 &lt;code&gt;HTTP&lt;/code&gt; 的三个安全性问题，如何解决这三个问题呢？ 1. 对明文进行加密 2. 对通信方身份进行认证 3. 保护报文完整性&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; 协议加上这三个技术就是我们所说的 &lt;code&gt;HTTPS&lt;/code&gt;，&lt;code&gt;HTTPS&lt;/code&gt; 并不是一个全新的协议，它只是在 &lt;code&gt;HTTP&lt;/code&gt; 协议上增加了加密，认证和完整性保护机制的 &lt;code&gt;HTTP&lt;/code&gt; 协议（将 &lt;code&gt;HTTP&lt;/code&gt; 通信接口部分用 &lt;code&gt;SSL&lt;/code&gt; ( &lt;code&gt;Secure Socket Layer&lt;/code&gt; )和 &lt;code&gt;TLS&lt;/code&gt; ( &lt;code&gt;Transport Layer Security&lt;/code&gt; )协议代替），全称叫做 &lt;code&gt;HTTP Secure&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/https.AfsQb5NT_11Slqg.webp&quot; alt=&quot;https&quot; title=&quot;https&quot;&gt;&lt;/p&gt;
&lt;p&gt;经常会在 &lt;code&gt;Web&lt;/code&gt; 的登录页面和购物结算界面等使用 &lt;code&gt;HTTPS&lt;/code&gt; 通信。使用 &lt;code&gt;HTTPS&lt;/code&gt; 通信时，不再用 &lt;code&gt;http://&lt;/code&gt;，而是改用 &lt;code&gt;https://&lt;/code&gt;。另外，当浏览器 访问 &lt;code&gt;HTTPS&lt;/code&gt; 通信有效的 &lt;code&gt;Web&lt;/code&gt; 网站时，浏览器的地址栏内会出现一个带 锁的标记。对 &lt;code&gt;HTTPS&lt;/code&gt; 的显示方式会因浏览器的不同而有所改变。&lt;/p&gt;
&lt;p&gt;通常，&lt;code&gt;HTTP&lt;/code&gt; 直接和 &lt;code&gt;TCP&lt;/code&gt; 通信。当使用 &lt;code&gt;SSL&lt;/code&gt; 时，则演变成先和 &lt;code&gt;SSL&lt;/code&gt; 通信，再由 &lt;code&gt;SSL&lt;/code&gt; 和 &lt;code&gt;TCP&lt;/code&gt; 通信了。简言之，所谓 &lt;code&gt;HTTPS&lt;/code&gt;，其实就是 身披 &lt;code&gt;SSL&lt;/code&gt; 协议这层外壳的 &lt;code&gt;HTTP&lt;/code&gt;。在采用 &lt;code&gt;SSL&lt;/code&gt; 后，&lt;code&gt;HTTP&lt;/code&gt; 就拥有了 &lt;code&gt;HTTPS&lt;/code&gt; 的加密、证书和完整性保 护这些功能。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SSL&lt;/code&gt; &lt;code&gt;Secure Sockets Layer&lt;/code&gt; 是独立于 &lt;code&gt;HTTP&lt;/code&gt; 的协议，和 &lt;code&gt;HTTP&lt;/code&gt; 协议一样都是应用层协议，所以不光是 &lt;code&gt;HTTP&lt;/code&gt; 协议，其他运行在 应用层的 &lt;code&gt;SMTP&lt;/code&gt; 和 &lt;code&gt;Telnet&lt;/code&gt; 等协议均可配合 &lt;code&gt;SSL&lt;/code&gt; 协议使用。可以说 &lt;code&gt;SSL&lt;/code&gt; 是当今世界上应用最为广泛的网络安全技术。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/ssl.y8Vziooq_Z7AkUi.webp&quot; alt=&quot;ssl&quot; title=&quot;ssl&quot;&gt;&lt;/p&gt;
&lt;h2&gt;加密&lt;/h2&gt;
&lt;p&gt;加密的方式有两种，一种是对称加密，加密和解密用同一个密钥；另一种是非对称加密，加密和解密用不同的密钥。对称加密需要把密钥通过网络进行发送，既然报文能被监听，那么密钥同样也会被监听，密钥被攻击者获取，我们的加密报文和没加密没区别，换句话说，如果密钥能够安全传递，那么我们的报文也能安全传递，对称加密是行不通的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/Symmetric-key.Dwe8ub0W_ZVgnSh.webp&quot; alt=&quot;Symmetric-key&quot; title=&quot;Symmetric-key&quot;&gt;&lt;/p&gt;
&lt;p&gt;非对称加密是运用数学原理生成的一对公钥和私钥，公钥可以交给任何人，只能用来加密，而私钥保存在服务器，只能用来解密。公钥加密后的内容用公钥是无法解开的，这一点是非常重要的，至于为什么，你可以这么理解，有一个函数$ f(x) = a + b $，当你知道a和b的时候要你计算$ f(x) $很容易，但是如果给你$ f(x) $要你求a和b的值就有非常多的可能行，特别是当这个值足够大的时候，拿几乎就是不可能的，公钥的作用就是这样，给你公钥你进行加密非常容易，但是如果给你加密后的结果你是无法解密的。公钥和私钥是存在数学联系的，比如常用的RSA加密算法，就是利用大整数因式分解难以破解的性质来设计的，如果你对非对称加密的设计细节很感兴趣可以看这两篇文章： 1. &lt;a href=&quot;https://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html&quot; title=&quot;RSA算法原理（一）&quot;&gt;RSA算法原理（一）&lt;/a&gt; 2. &lt;a href=&quot;https://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html&quot; title=&quot;RSA算法原理（二）&quot;&gt;RSA算法原理（二）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;李永乐老师的这个视频也能让你对 &lt;code&gt;RSA加密算法&lt;/code&gt; 有一个简单的了解 &lt;a href=&quot;https://www.bilibili.com/video/av26639065&quot; title=&quot;银行的密码系统安全吗&quot;&gt;银行的密码系统安全吗&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/Public-key.BHh1zDqK_Z1kM5Yc.webp&quot; alt=&quot;Public-key&quot; title=&quot;Public-key&quot;&gt;&lt;/p&gt;
&lt;p&gt;HTTP协议没有加密功能但可以通 过和 &lt;code&gt;SSL&lt;/code&gt; (&lt;code&gt;Secure Socket Layer&lt;/code&gt; ，安全套接层)或 &lt;code&gt;TLS&lt;/code&gt; ( &lt;code&gt;Transport Layer Security&lt;/code&gt;，安全传输层协议)的组合使用，加密 &lt;code&gt;HTTP&lt;/code&gt; 的通信内容。用 &lt;code&gt;SSL&lt;/code&gt; 建立安全通信线路之后，就可以在这条线路上进行 &lt;code&gt;HTTP&lt;/code&gt; 通信了。与 &lt;code&gt;SSL&lt;/code&gt; 组合使用的 &lt;code&gt;HTTP&lt;/code&gt; 被称为 &lt;code&gt;HTTPS&lt;/code&gt; ( &lt;code&gt;HTTP Secure&lt;/code&gt;，超文 本传输安全协议)或 &lt;code&gt;HTTP over SSL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTTPS&lt;/code&gt; 在通信过程中并不是一直采用非对称加密，而是采取一种混合加密的方式，也就是说我们将对称加密的密钥用非对称加密的方式进行传递，当确保对称加密的密钥安全地送达之后，我们就可以安全地使用对称加密来通信了，这样做的目的是因为非对称加密虽然安全，但是因为处理的过程要比对称加密更加复杂，所以效率比较低。混合加密的大致过程如下：客户端拿到服务端的公钥后，产生一个随机的 &lt;code&gt;key&lt;/code&gt; 作为对称加密的共享密钥，然后用服务端的公钥加密传递给服务端，服务端拿到加密后的 &lt;code&gt;key&lt;/code&gt;，利用服务端原有的私钥解密，得到 &lt;code&gt;key&lt;/code&gt; 的原文。然后后面跟客户端通讯就使用key作为对称加密的私钥，进行通讯了。其实 &lt;code&gt;https&lt;/code&gt; 真正的数据传递过程，走的是对称加密。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/mix-key.juv72deu_2su7R2.webp&quot; alt=&quot;mix-key&quot; title=&quot;mix-key&quot;&gt;&lt;/p&gt;
&lt;h2&gt;身份认证&lt;/h2&gt;
&lt;p&gt;加密虽然实现了，但是获得正确公钥的前提是我们当前通信的对象确实是目标服务器，而不是一个伪装的攻击者。如果我们收到的是攻击者替换过后的公钥，那么当我们以为通信已经安全，将数据用这个假的公钥加密后传给目标服务器，在这个过程中攻击者截获我们的报文，用它的私钥就可以解密得到我们的数据。所以非对称加密要想实现，前提就是能确定公钥发送者的身份。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们必须借助第三方数字证书认证机构( &lt;code&gt;CA&lt;/code&gt;，&lt;code&gt;Certificate Authority&lt;/code&gt; )和其相关机关颁发的公开密钥证书。这个数字认证机构必须是客户端和服务器都信任的第三方机构。有了这个第三方机构的证书我们才能够确定服务器端的身份。需要了解的一点是，我们在加密通信的时候是用公钥加密，私钥解密，这其中用的是加密算法。其实我们还可以用私钥加密，用对应的公钥解密，这种情况往往是用来发布一个公告或者广播信息，也正是我们数字签名当中用到的算法。 HTTPS中的证书验证的流程如下： 1. 服务器端将自己的公钥和信息发送给 &lt;code&gt;CA&lt;/code&gt;，申请数字证书，&lt;code&gt;CA&lt;/code&gt; 在验证过申请者身份之后，开始制作数字证书，制作过程如下：将我们发送给 &lt;code&gt;CA&lt;/code&gt; 的公钥，以及证书签发单位，申请者等信息记录为内容 &lt;code&gt;M&lt;/code&gt;，然后通过哈希运算获得 &lt;code&gt;M&lt;/code&gt; 的摘要 &lt;code&gt;H&lt;/code&gt;，用 &lt;code&gt;CA&lt;/code&gt; 的私钥对 &lt;code&gt;H&lt;/code&gt; 进行加密，加密得到的就是 &lt;code&gt;CA&lt;/code&gt; 的数字签名 &lt;code&gt;S&lt;/code&gt;。&lt;code&gt;CA&lt;/code&gt; 会把数字签名 &lt;code&gt;S&lt;/code&gt; 附在内容 &lt;code&gt;M&lt;/code&gt; 之后，整个 &lt;code&gt;M+S&lt;/code&gt; 就被称作数字证书，&lt;code&gt;CA&lt;/code&gt; 会把这个数字证书返回给服务器。 2. 服务器获得数字证书之后将 &lt;code&gt;CA&lt;/code&gt; 颁发的带着公钥数字证书发送给客户端 3. 客户端拿到数字证书之后会用 &lt;code&gt;CA&lt;/code&gt; 的公钥（一般预装在浏览器中）对数字签名 &lt;code&gt;S&lt;/code&gt; 进行解密，获得 &lt;code&gt;M&lt;/code&gt; 经过哈希运算后的摘要 &lt;code&gt;H&lt;/code&gt;，然后在对数字证书中的内容 &lt;code&gt;M&lt;/code&gt; 进行哈希运算获得 &lt;code&gt;H&apos;&lt;/code&gt;，如果 &lt;code&gt;H&lt;/code&gt; 和 &lt;code&gt;H&apos;&lt;/code&gt; 相同，说明服务器的数字证书是 &lt;code&gt;CA&lt;/code&gt; 认证的，数字证书中的公钥也是可靠的，内容没有经过篡改。&lt;/p&gt;
&lt;p&gt;之所以数字签名是可靠的，是因为私钥只有 &lt;code&gt;CA&lt;/code&gt; 知道，而对摘要的加密只有私钥才能做到，所以只要验证出加密内容没问题，说明证书就是可靠的。而即使攻击者截获证书，将内容 &lt;code&gt;M&lt;/code&gt; 改掉，由于数字签名 &lt;code&gt;S&lt;/code&gt; 攻击者无法篡改，当客户端进行验证的时候就能够发现内容被篡改。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/ca-secure.CZ7-Js1W_1oPKDH.webp&quot; alt=&quot;ca-secure&quot; title=&quot;ca-secure&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;私钥可以用来解密和签名，公钥可以用来加密和验证。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一般来说，只有被请求方服务器需要证明自己的身份，但是如银行用户这样的特殊情况，客户端也需要证明自己的身份，这就是为什么在计算机上使用网上银行需要安装证书的原因。&lt;/p&gt;
&lt;h2&gt;HTTPS安全通信机制&lt;/h2&gt;
&lt;p&gt;运用SSL的加密和证书验证，我们已经能够确保通信的安全了，回顾一下HTTPS安全通信的全过程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/https-process.BIDdnBUK_Z1eGGN2.webp&quot; alt=&quot;https-process&quot; title=&quot;https-process&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端通过发送 &lt;code&gt;Client Hello&lt;/code&gt; 报文开始 &lt;code&gt;SSL&lt;/code&gt; 通信。报文中 包含客户端支持的 &lt;code&gt;SSL&lt;/code&gt; 的指定版本、加密组件( &lt;code&gt;Cipher Suite&lt;/code&gt; )列表(所使用的加密算法及密钥长度等)。&lt;/li&gt;
&lt;li&gt;服务器可进行 &lt;code&gt;SSL&lt;/code&gt; 通信时，会以 &lt;code&gt;Server Hello&lt;/code&gt; 报文作为应 答。和客户端一样，在报文中包含 &lt;code&gt;SSL&lt;/code&gt; 版本以及加密组 件。服务器的加密组件内容是从接收到的客户端加密组件 内筛选出来的。&lt;/li&gt;
&lt;li&gt;之后服务器发送 &lt;code&gt;Certificate&lt;/code&gt; 报文。报文中包含公开密钥 证书。&lt;/li&gt;
&lt;li&gt;最后服务器发送 &lt;code&gt;Server Hello Done&lt;/code&gt; 报文通知客户端，最初 阶段的 &lt;code&gt;SSL&lt;/code&gt; 握手协商部分结束。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SSL&lt;/code&gt; 第一次握手结束之后，客户端以 &lt;code&gt;Client Key Exchange&lt;/code&gt; 报文作为回应。报文中包含通信加密中使用的一种被称为 &lt;code&gt;Pre-master secret&lt;/code&gt; 的随机密码串。该报文已用步骤 &lt;code&gt;3&lt;/code&gt; 中的公 开密钥进行加密。&lt;/li&gt;
&lt;li&gt;接着客户端继续发送 &lt;code&gt;Change Cipher Spec&lt;/code&gt; 报文。该报文会 提示服务器，在此报文之后的通信会采用 &lt;code&gt;Pre-master secret&lt;/code&gt; 密钥加密。&lt;/li&gt;
&lt;li&gt;客户端发送 &lt;code&gt;Finished&lt;/code&gt; 报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功，要以服务器 是否能够正确解密该报文作为判定标准。&lt;/li&gt;
&lt;li&gt;服务器同样发送 &lt;code&gt;Change Cipher Spec&lt;/code&gt; 报文。&lt;/li&gt;
&lt;li&gt;服务器同样发送 &lt;code&gt;Finished&lt;/code&gt; 报文。&lt;/li&gt;
&lt;li&gt;服务器和客户端的 &lt;code&gt;Finished&lt;/code&gt; 报文交换完毕之后，&lt;code&gt;SSL&lt;/code&gt; 连接 就算建立完成。当然，通信会受到 &lt;code&gt;SSL&lt;/code&gt; 的保护。从此处开 始进行应用层协议的通信，即发送 &lt;code&gt;HTTP&lt;/code&gt; 请求。&lt;/li&gt;
&lt;li&gt;应用层协议通信，即发送 &lt;code&gt;HTTP&lt;/code&gt; 响应。&lt;/li&gt;
&lt;li&gt;由客户端断开连接。断开连接时，发送 &lt;code&gt;close_notify&lt;/code&gt; 报文。&lt;/li&gt;
&lt;li&gt;最后发送 &lt;code&gt;TCP FIN&lt;/code&gt; 报文来关闭与 &lt;code&gt;TCP&lt;/code&gt; 的通信。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在以上流程中，应用层发送数据时会附加一种叫做 &lt;code&gt;MAC&lt;/code&gt; ( &lt;code&gt;Message Authentication Code&lt;/code&gt; )的报文摘要。&lt;code&gt;MAC&lt;/code&gt; 能够查知报文是否遭到篡改， 从而保护报文的完整性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/https-process2.mA9qBnuI_BNCgR.webp&quot; alt=&quot;http-process2&quot; title=&quot;http-process2&quot;&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/u014401141/article/details/74219479&quot; title=&quot;rsa加密算法&quot;&gt;rsa加密算法&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>在terminal中用daemon方式启动emacs</title><link>https://clloz.com/blog/daemon-emacs</link><guid isPermaLink="true">https://clloz.com/blog/daemon-emacs</guid><pubDate>Sat, 27 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;由于个人喜好，虽然 &lt;code&gt;emacs&lt;/code&gt; 不像 &lt;code&gt;vim&lt;/code&gt; 那样在 &lt;code&gt;Linux&lt;/code&gt; 中默认安装，不过我在 &lt;code&gt;mac&lt;/code&gt; 上还是习惯于用 &lt;code&gt;emacs&lt;/code&gt; 作为默认的 &lt;code&gt;terminal&lt;/code&gt; 编辑工具，不过 &lt;code&gt;emacs&lt;/code&gt; 有个坏毛病就是配置挺繁琐，没有 &lt;code&gt;vim&lt;/code&gt; 那么轻量，并且当配置越来越多，启动会比较慢，用 &lt;code&gt;emacs&lt;/code&gt; 命令的话每次都得等 &lt;code&gt;emacs&lt;/code&gt; 加载会很头疼，下面就分享以下让 &lt;code&gt;emacs&lt;/code&gt; 在后台驻留的方式。&lt;/p&gt;
&lt;h2&gt;如何快速启动 emacs&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;emacs&lt;/code&gt; 提供了 &lt;code&gt;--daemon&lt;/code&gt; 的后台驻留启动参数，不过我们可以配置以下，让启动更便捷，首先创建一个 &lt;code&gt;.emacs_client.sh&lt;/code&gt; 文件，在其中加入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
#filename: emacs_client.sh

if [ `ps axu | grep &quot;Emacs.*app&quot; | grep daemon | wc -l` -eq 1 ]
# if [ `ps axu | grep &quot;Emacs&quot;` -eq 1 ]
then
    echo &quot;Ready.&quot;
else
    echo &quot;Starting server.&quot;
    /usr/local/bin/emacs --daemon
fi

emacsclient -c &quot;$@&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段的主要功能就是检查进程中是否有 &lt;code&gt;emacs&lt;/code&gt;，如果没有则用 &lt;code&gt;--daemon&lt;/code&gt; 方式启动，如果有就直接打开。然后我们在环境变量给我们的命令起个别名，并且加入一个关闭进程的方法，打开 &lt;code&gt;.bash_profile&lt;/code&gt;，在其中加入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;alias emacs=&quot;~/.emacs_client.sh -t&quot;
alias em=&quot;emacs&quot;
# alias emd=&quot;emacs -e &apos;(kill-emacs)&apos;&quot;
alias emd=&quot;kill-emacs&quot;

# add kill emacs function
function kill-emacs(){
    emacsclient -e &quot;(kill-emacs)&quot;
    emacs_pid=$( ps x | grep &quot;Emacs.*app&quot; | grep daemon | awk &apos;{print $1}&apos; )
    if [[ -n &quot;${emacs_pid}&quot; ]];then
        kill -9 &quot;${emacs_pid}&quot;
    fi
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一段的主要功能就是用 &lt;code&gt;em&lt;/code&gt; 代替 &lt;code&gt;emacs&lt;/code&gt; 指令，然后 &lt;code&gt;emd&lt;/code&gt; 指令表示关掉 &lt;code&gt;emacs&lt;/code&gt; 进程。&lt;/p&gt;
&lt;p&gt;现在如果你要启动 &lt;code&gt;emacs&lt;/code&gt; 只要键入 &lt;code&gt;em&lt;/code&gt; 就可以了，第一次会加载 &lt;code&gt;emacs&lt;/code&gt; 配置，然后进程会一直在后台驻留，当你下次再要进入的时候再键入 &lt;code&gt;em&lt;/code&gt; 命令，&lt;code&gt;emacs&lt;/code&gt; 就会秒开。如果你想关掉进程，就直接输入 &lt;code&gt;emd&lt;/code&gt;，不过下次再启动就有需要加载配置了。&lt;/p&gt;</content:encoded><h:img src="/_astro/emacs-logo.CVWUvzc8.png"/><enclosure url="/_astro/emacs-logo.CVWUvzc8.png"/></item><item><title>获取文档中元素的宽高</title><link>https://clloz.com/blog/get-dom-size</link><guid isPermaLink="true">https://clloz.com/blog/get-dom-size</guid><pubDate>Sat, 27 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;获取元素的尺寸和位置是我们经常需要遇到的问题，获取文档的高度有各种不同的方法，每个属性的作用各不相同。比如 &lt;code&gt;window&lt;/code&gt; 对象的 &lt;code&gt;innerHeight&lt;/code&gt; 和 &lt;code&gt;outerHeight&lt;/code&gt; 就表示不一样的意思，元素的属性也一样。获取元素尺寸和位置的时间也很重要，如果我们在元素还没加载的时候获取肯定是得不到正确结果的。比较安全的方法当然是 &lt;code&gt;load&lt;/code&gt; 事件触发以后获取。&lt;/p&gt;
&lt;h2&gt;load 事件和 onload 属性&lt;/h2&gt;
&lt;p&gt;我们一般都是使用 &lt;code&gt;window&lt;/code&gt; 对象的 &lt;code&gt;onload&lt;/code&gt; 属性来监控 &lt;code&gt;load&lt;/code&gt; 事件，但其实文档中所有异步加载的资源都可以用 &lt;code&gt;load&lt;/code&gt; 事件监控是否加载完成（非内联的 &lt;code&gt;script&lt;/code&gt;，&lt;code&gt;link&lt;/code&gt; ，以及 &lt;code&gt;image&lt;/code&gt; ），这些资源的加载成功都会触发 &lt;code&gt;load&lt;/code&gt; 事件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;js/test.js&quot;&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;css/test.css&quot; /&gt;
    &amp;#x3C;style&gt;
      div {
        height: 50px;
        width: 80px;
        background-color: lightblue;
      }
    &amp;#x3C;/style&gt;
    &amp;#x3C;script&gt;
      console.log(123)
    &amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;img src=&quot;img/totoro.jpg&quot; alt=&quot;&quot; /&gt;
    &amp;#x3C;div&gt;this is a div element!&amp;#x3C;/div&gt;
    &amp;#x3C;script&gt;
      function get(selector) {
        return document.querySelector(selector)
      }

      function getAll(selector) {
        return document.querySelectorAll(selector)
      }
      var img = get(&apos;img&apos;)
      var link = get(&apos;link&apos;)
      var style = get(&apos;style&apos;)
      var script = get(&apos;script&apos;)
      var div = get(&apos;div&apos;)
      var inner = getAll(&apos;script&apos;)[1]

      img.onload = function () {
        console.log(&apos;img&apos;)
      }

      link.onload = function () {
        console.log(&apos;link&apos;)
      }

      style.onload = function () {
        console.log(&apos;style&apos;)
      }

      script.onload = function () {
        console.log(&apos;script&apos;)
      }

      inner.onload = function () {
        console.log(&apos;inner&apos;)
      }

      div.onload = function () {
        console.log(&apos;div&apos;)
      }
    &amp;#x3C;/script&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;123
link
script
img
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;非内联的 &lt;code&gt;js，css，image&lt;/code&gt; 的加载完毕都触发了 &lt;code&gt;load&lt;/code&gt; 事件。&lt;/p&gt;
&lt;h2&gt;获取元素宽高&lt;/h2&gt;
&lt;h2&gt;不要用 element.style.width&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;element.style&lt;/code&gt; 这个属性获取的是元素的内联样式，也就是写在标签中 &lt;code&gt;style&lt;/code&gt; 属性里的样式，内联样式本来就不推介，如果你是用 &lt;code&gt;style&lt;/code&gt; 标签或者外部样式表引入的样式，&lt;code&gt;element.style.width&lt;/code&gt; 是无法获取值的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div style=&quot;width: 300px&quot;&gt;this is a div element!&amp;#x3C;/div&gt;
&amp;#x3C;script&gt;
  function get(selector) {
    return document.querySelector(selector)
  }

  var div = get(&apos;div&apos;)

  window.onload = function () {
    console.log(div.style.width)
  }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果去掉div中的 &lt;code&gt;style&lt;/code&gt; 属性，&lt;code&gt;div.style.width&lt;/code&gt; 将输出空字符串。&lt;/p&gt;
&lt;h2&gt;offsetHeight, offsetWidth&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;offsetHeight&lt;/code&gt; 可以用来计算元素的物理空间，此空间包括内容，&lt;code&gt;padding&lt;/code&gt; 和 &lt;code&gt;border&lt;/code&gt;（还包括滚动条的宽度，但大多时候滚动条的宽度是计算到 &lt;code&gt;padding&lt;/code&gt; 和内容中的）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  div {
    height: 50px;
    width: 80px;
    padding: 10px;
    margin: 10px;
    border: 3px solid;
    background-color: lightblue;
    overflow: scroll;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div&gt;sdfdsfsfdsfsdfdsfsfsfdsffsfsdfasfasfasfsafasfsafsafsafsafs&amp;#x3C;/div&gt;
&amp;#x3C;script&gt;
  function get(selector) {
    return document.querySelector(selector)
  }
  var div = get(&apos;div&apos;)

  window.onload = function () {
    console.log(div.offsetHeight, div.offsetWidth) //76, 106
  }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;scrollHeight,scrollWidth&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;scrollHeight&lt;/code&gt; 用来计算可滚动容器的大小，包括不可见的部分，比如一个 &lt;code&gt;300*300&lt;/code&gt; 的容器放入一个 &lt;code&gt;600*600&lt;/code&gt; 的图片，此时 &lt;code&gt;scrollHeight&lt;/code&gt; 为 &lt;code&gt;600&lt;/code&gt;，当然，&lt;code&gt;scrollHeight&lt;/code&gt; 的值需要加上 &lt;code&gt;padding&lt;/code&gt; 的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  div {
    height: 50px;
    width: 80px;
    padding: 10px;
    margin: 10px;
    border: 3px solid;
    background-color: lightblue;
    overflow: scroll;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div&gt;sdfdsfsfdsfsdfdsfsfsfdsffsfsdfasfasfasfsafasfsafsafsafsafs&amp;#x3C;/div&gt;
&amp;#x3C;script&gt;
  function get(selector) {
    return document.querySelector(selector)
  }
  var div = get(&apos;div&apos;)

  window.onload = function () {
    console.log(div.scrollWidth) //382
  }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;clientHeight,clientWidth&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;clientHeight&lt;/code&gt; 表示可视区域，包括内容和 &lt;code&gt;padding&lt;/code&gt; (不包括边框），如果有滚动条，还需要减去滚动条的宽度。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style&gt;
  div {
    height: 50px;
    width: 80px;
    padding: 10px;
    margin: 10px;
    border: 3px solid;
    background-color: lightblue;
    overflow: scroll;
  }
&amp;#x3C;/style&gt;
&amp;#x3C;div&gt;sdfdsfsfdsfsdfdsfsfsfdsffsfsdfasfasfasfsafasfsafsafsafsafs&amp;#x3C;/div&gt;
&amp;#x3C;script&gt;
  function get(selector) {
    return document.querySelector(selector)
  }
  var div = get(&apos;div&apos;)

  window.onload = function () {
    console.log(div.clientWidth) //100
  }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;window.getComputedStyle&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Window.getComputedStyle()&lt;/code&gt; 方法返回一个对象，该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有 &lt;code&gt;CSS&lt;/code&gt; 属性的值。 私有的 &lt;code&gt;CSS&lt;/code&gt; 属性值可以通过对象提供的 &lt;code&gt;API&lt;/code&gt; 或通过简单地使用 &lt;code&gt;CSS&lt;/code&gt; 属性名称进行索引来访问。这个方法是最推荐的，因为它是浏览器渲染引擎对外开放的接口，返回对象中的属性都是浏览器最终渲染计算的结果。需要注意的是，返回的宽高是跟你的 &lt;code&gt;box-sizing属性有关的&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;js/test.js&quot;&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;css/test.css&quot; /&gt;
    &amp;#x3C;style&gt;
      div {
        height: 50px;
        /* width: 80px; */
        padding: 10px;
        margin: 10px;
        border: 3px solid;
        background-color: lightblue;
        overflow: scroll;
        box-sizing: border-box;
      }
    &amp;#x3C;/style&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;img src=&quot;img/totoro.jpg&quot; alt=&quot;&quot; /&gt;
    &amp;#x3C;div&gt;sdfdsfsfdsfsdfdsfsfsfdsffsfsdfasfasfasfsafasfsafsafsafsafs&amp;#x3C;/div&gt;
    &amp;#x3C;script&gt;
      function get(selector) {
        return document.querySelector(selector)
      }
      var div = get(&apos;div&apos;)

      window.onload = function () {
        console.log(
          window.getComputedStyle(div, null).padding,
          window.getComputedStyle(div, null).border,
          window.getComputedStyle(div, null).width
        )
      }
    &amp;#x3C;/script&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回结果： 1. &lt;code&gt;box-sizing: border-box;&lt;/code&gt;:&lt;code&gt;10px 3px solid rgb(0, 0, 0) 1003px&lt;/code&gt; 2. &lt;code&gt;box-sizng: content-box&lt;/code&gt;:&lt;code&gt;10px 3px solid rgb(0, 0, 0) 977px&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;div.getBoundingClientRect()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Element.getBoundingClientRect()&lt;/code&gt; 方法返回元素的大小及其相对于视口的位置。 返回值是一个 &lt;code&gt;DOMRect&lt;/code&gt; 对象，这个对象是由该元素的 &lt;code&gt;getClientRects()&lt;/code&gt; 方法返回的一组矩形的集合, 即：是与该元素相关的 &lt;code&gt;CSS&lt;/code&gt; 边框集合 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DOMRect&lt;/code&gt; 对象包含了一组用于描述边框的只读属性——&lt;code&gt;left&lt;/code&gt;、&lt;code&gt;top&lt;/code&gt;、&lt;code&gt;right&lt;/code&gt;和&lt;code&gt;bottom&lt;/code&gt;，单位为像素。除了 &lt;code&gt;width&lt;/code&gt; 和 &lt;code&gt;height&lt;/code&gt; 外的属性都是相对于视口的左上角位置而言的。 &lt;img src=&quot;https://clloz.com/_astro/getBoundingClientRect.CCwTy8IE_25Ooia.webp&quot; alt=&quot;getBoundingClinetRect&quot; title=&quot;getBoundingClinetRect&quot;&gt;&lt;/p&gt;
&lt;p&gt;空边框盒（译者注：没有内容的边框）会被忽略。如果所有的元素边框都是空边框，那么这个矩形给该元素返回的 &lt;code&gt;width、height&lt;/code&gt; 值为 &lt;code&gt;0&lt;/code&gt;，&lt;code&gt;left、top&lt;/code&gt; 值为第一个 &lt;code&gt;css&lt;/code&gt; 盒子（按内容顺序）的 &lt;code&gt;top-left&lt;/code&gt;值。&lt;/p&gt;
&lt;p&gt;当计算边界矩形时，会考虑视口区域（或其他可滚动元素）内的滚动操作，也就是说，当滚动位置发生了改变，&lt;code&gt;top&lt;/code&gt; 和 &lt;code&gt;left&lt;/code&gt; 属性值就会随之立即发生变化（因此，它们的值是相对于视口的，而不是绝对的）。如果你需要获得相对于整个网页左上角定位的属性值，那么只要给 &lt;code&gt;top、left&lt;/code&gt; 属性值加上当前的滚动位置（通过 &lt;code&gt;window.scrollX&lt;/code&gt; 和 &lt;code&gt;window.scrollY&lt;/code&gt; ），这样就可以获取与当前的滚动位置无关的值。&lt;/p&gt;
&lt;h2&gt;offsetTop,offsetLeft&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;offsetTop&lt;/code&gt; 和 &lt;code&gt;offsetLeft&lt;/code&gt; 表示该元素的左上角（边框外边缘）与已定位的父容器（ &lt;code&gt;offsetParent&lt;/code&gt; 对象）左上角的距离.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTMLElement.offsetParent&lt;/code&gt; 是一个只读属性，返回一个指向最近的（ &lt;code&gt;closest&lt;/code&gt;，指包含层级上的最近）包含该元素的定位元素。如果没有定位的元素，则 &lt;code&gt;offsetParent&lt;/code&gt; 为最近的 &lt;code&gt;table&lt;/code&gt;, &lt;code&gt;table cell&lt;/code&gt; 或&lt;code&gt;根元素&lt;/code&gt;（标准模式下为 &lt;code&gt;html&lt;/code&gt;；&lt;code&gt;quirks&lt;/code&gt; 模式下为 &lt;code&gt;body&lt;/code&gt; ）。当元素的 &lt;code&gt;style.display&lt;/code&gt; 设置为 &lt;code&gt;none&lt;/code&gt; 时，offsetParent 返回 null。&lt;code&gt;offsetParent&lt;/code&gt; 很有用，因为 &lt;code&gt;offsetTop&lt;/code&gt; 和 &lt;code&gt;offsetLeft&lt;/code&gt; 都是相对于其内边距边界的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 &lt;code&gt;Webkit&lt;/code&gt; 中，如果元素为隐藏的（ &lt;code&gt;display: none;&lt;/code&gt; ）（该元素或其祖先元素的 &lt;code&gt;style.display&lt;/code&gt; 为 &lt;code&gt;none&lt;/code&gt;），或者该元素的 &lt;code&gt;style.position&lt;/code&gt; 被设为 &lt;code&gt;fixed&lt;/code&gt;，则该属性返回 &lt;code&gt;null&lt;/code&gt;。在 &lt;code&gt;IE 9&lt;/code&gt; 中，如果该元素的 ˚ 被设置为 &lt;code&gt;fixed&lt;/code&gt;，则该属性返回 &lt;code&gt;null&lt;/code&gt;。（ &lt;code&gt;display:none&lt;/code&gt; 无影响。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;clientLeft,clientTop&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;clientTop&lt;/code&gt; 和 &lt;code&gt;clientLeft&lt;/code&gt; 返回内边距的边缘和边框的外边缘之间的水平和垂直距离，也就是左，上边框宽度&lt;/p&gt;
&lt;h2&gt;获取图片的原始大小&lt;/h2&gt;
&lt;p&gt;我们在js中获取的图片的 &lt;code&gt;width&lt;/code&gt; 和 &lt;code&gt;height&lt;/code&gt; 是被 &lt;code&gt;css&lt;/code&gt; 修改过的大小，如果我们想在 &lt;code&gt;JS&lt;/code&gt; 中获取图片的原始大小来操作 &lt;code&gt;DOM&lt;/code&gt; 的话，我们有两个方法可以选择： 1. 新建一个 &lt;code&gt;img&lt;/code&gt; 对象，把文档中的 &lt;code&gt;img&lt;/code&gt; 对象的 &lt;code&gt;src&lt;/code&gt; 赋值给新的对象，然后获取这个新的对象的宽高，不需要把新的对象添加到文档中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;img src=&quot;img/totoro.jpg&quot; alt=&quot;&quot; /&gt;
&amp;#x3C;script&gt;
  function $(selector) {
    return document.querySelector(selector)
  }
  var img = $(&apos;img&apos;)
  img.onload = function () {
    var image = document.createElement(&apos;img&apos;)
    image.src = img.src
    console.log(image.width, image.height)
  }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;code&gt;HTML5&lt;/code&gt; 提供了一个新属性 &lt;code&gt;naturalWidth/naturalHeight&lt;/code&gt; 可以直接获取图片的原始宽高。这两个属性在 &lt;code&gt;Firefox/Chrome/Safari/Opera&lt;/code&gt; 及 &lt;code&gt;IE9&lt;/code&gt; 里已经实现。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;img src=&quot;img/totoro.jpg&quot; alt=&quot;&quot; /&gt;
&amp;#x3C;script&gt;
  function $(selector) {
    return document.querySelector(selector)
  }
  var img = $(&apos;img&apos;)
  img.onload = function () {
    console.log(img.naturalHeight, img.naturalWidth)
  }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文档&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000007687940&quot; title=&quot;原生 JS 获取元素的尺寸和位置&quot;&gt;原生 JS 获取元素的尺寸和位置&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/snandy/p/3704218.html&quot; title=&quot;JavaScript获取图片的原始尺寸&quot;&gt;JavaScript获取图片的原始尺寸&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>动态加载的样式/脚本对渲染的影响</title><link>https://clloz.com/blog/async-load</link><guid isPermaLink="true">https://clloz.com/blog/async-load</guid><pubDate>Fri, 26 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/04/25/how-browser-work/&quot; title=&quot;浏览器渲染过程和JS引擎浅析&quot;&gt;浏览器渲染过程和JS引擎浅析&lt;/a&gt;这篇文章里，我提到了用同步脚本异步加载（脚本同步，但是请求是由渲染引擎的异步请求线程异步请求的）的外部资源会影响 &lt;code&gt;load&lt;/code&gt; 事件发生的时候，如果外部资源加载时间过长，那么 &lt;code&gt;load&lt;/code&gt; 时间发生的时间也会推迟。但其实在脚本中动态加载脚本或者异步请求外部资源还有一些细节可以挖掘。&lt;/p&gt;
&lt;h2&gt;异步请求样式表&lt;/h2&gt;
&lt;p&gt;在脚本中异步请求样式或脚本是不会阻塞DOM的解析和渲染，唯一影响的就是 &lt;code&gt;load&lt;/code&gt; 事件发生的时间以及 &lt;code&gt;load&lt;/code&gt; 事件发生之前的最后一次渲染。看下面这段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;js/test.js&quot;&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div id=&quot;test&quot;&gt;test1&amp;#x3C;/div&gt;

    &amp;#x3C;script&gt;
      let script = document.createElement(&apos;link&apos;)
      script.setAttribute(&apos;rel&apos;, &apos;stylesheet&apos;)
      script.setAttribute(
        &apos;href&apos;,
        &apos;https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css&apos;
      )

      var a = document.body.appendChild(script)
      console.log(123)

      window.onload = function () {
        console.log(&apos;window load...&apos;)
      }
    &amp;#x3C;/script&gt;
    &amp;#x3C;div&gt;test2&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//test.js
console.log(&apos;domcontentloaded&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过设置浏览器的 &lt;code&gt;network throttling&lt;/code&gt;（选择 &lt;code&gt;slow 3g&lt;/code&gt; 就可以，或者你可以自定义到更慢，能够区分出效果就可以），让异步请求的 &lt;code&gt;css&lt;/code&gt; 的加载时间变长，然后观察之后的脚本是否执行，脚本之后的元素是否被渲染到页面上。通过结果的观察我们可以发现在 &lt;code&gt;css&lt;/code&gt; 还没有加载完成的时候 &lt;code&gt;console.log(123)&lt;/code&gt; 已经执行，并且 &lt;code&gt;test2&lt;/code&gt; 元素也已经被渲染到页面上，&lt;code&gt;defer&lt;/code&gt;标签中的代码也已经执行，说明 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件已经触发，说明文档解析已经完成。当异步的 &lt;code&gt;css&lt;/code&gt; 请求到的时候，页面会对新的 &lt;code&gt;css&lt;/code&gt; 进行解析，然后对页面进行 &lt;code&gt;load&lt;/code&gt; 之前的最后一次渲染，此时我们看到 &lt;code&gt;test1&lt;/code&gt; 和 &lt;code&gt;test2&lt;/code&gt; 的字体变了，随后触发 &lt;code&gt;load&lt;/code&gt; 事件。&lt;/p&gt;
&lt;h2&gt;同步添加内联样式表&lt;/h2&gt;
&lt;p&gt;如果我们用同步 &lt;code&gt;JS&lt;/code&gt; 为文档添加 &lt;code&gt;style&lt;/code&gt; 标签会怎么样呢，下面一段代码可以测试出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;js/test.js&quot;&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div id=&quot;test&quot;&gt;test1&amp;#x3C;/div&gt;

    &amp;#x3C;script&gt;
      var style = document.createElement(&apos;style&apos;)
      style.textContent = &apos;*{ color: red }&apos;

      document.body.appendChild(style)
      var el = document.getElementById(&apos;test&apos;)
      console.log(window.getComputedStyle(el, null).color)

      window.onload = function () {
        console.log(&apos;window load...&apos;)
      }
    &amp;#x3C;/script&gt;
    &amp;#x3C;div&gt;test2&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//test.js
console.log(&apos;domcontentloaded&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;rgb(255, 0, 0)
domcontentloaded
window load...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到在页面还没有解析完成的时候我们获取 &lt;code&gt;test1&lt;/code&gt; 元素的 &lt;code&gt;computedstyle&lt;/code&gt; 已经是 &lt;code&gt;rgb(255,0,0)&lt;/code&gt; 了，说明我们添加的 &lt;code&gt;style&lt;/code&gt; 标签已经解析并渲染成功。也就是说同步的内联样式表是会阻塞解析和渲染的，不过同步的内联样式表基本不会产生延迟，也就不会影响页面的性能。&lt;/p&gt;
&lt;h2&gt;异步请求外部脚本&lt;/h2&gt;
&lt;p&gt;在同步的 &lt;code&gt;JS&lt;/code&gt; 中异步请求外部脚本也会在 &lt;code&gt;load&lt;/code&gt; 事件之前加载完成，但是对页面解析和渲染的影响，我们看如下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;js/test.js&quot;&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div id=&quot;test&quot;&gt;test1&amp;#x3C;/div&gt;

    &amp;#x3C;script&gt;
      var script = document.createElement(&apos;script&apos;)
      script.src = &apos;https://cdn.jsdelivr.net/npm/react@15.4.0/dist/react.js&apos;
      document.body.appendChild(script)
      console.log(&apos;after script: &apos;, window.React)

      window.onload = function () {
        console.log(&apos;window load...&apos;)
        console.log(&apos;onload: &apos;, window.React)
      }
    &amp;#x3C;/script&gt;
    &amp;#x3C;div&gt;test2&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//test.js
console.log(&apos;domcontentloaded: &apos; + window.React)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;after script:  undefined
domcontentloaded: undefined
window load...
onload:  {__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {…}, Children: {…}, Component: ƒ, PureComponent: ƒ, createElement: ƒ, …}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果显而易见，和异步请求外部样式表的效果相似，请求外部脚本并不影响页面的解析和渲染，也没有阻塞后面的脚本执行， 这意味着动态插入一个外部脚本后不可立即使用其内容，需要等待加载完毕。在这段代码中，直到 &lt;code&gt;load&lt;/code&gt; 事件发生后，我们在 &lt;code&gt;onload&lt;/code&gt; 函数中才能调用外部脚本中的变量。同时也佐证了 &lt;code&gt;load&lt;/code&gt; 事件发生之前会加载执行完所有的异步请求的外部资源。&lt;/p&gt;
&lt;h2&gt;在 DOM 中插入内联脚本&lt;/h2&gt;
&lt;p&gt;这个直接说结论通过 &lt;code&gt;DOM API&lt;/code&gt; （ &lt;code&gt;appendChild()&lt;/code&gt;、&lt;code&gt;append()&lt;/code&gt;、&lt;code&gt;before()&lt;/code&gt; 等等）插入的 &lt;code&gt;script&lt;/code&gt; 元素，如果这个 &lt;code&gt;script&lt;/code&gt; 元素没有 &lt;code&gt;src&lt;/code&gt; 属性且 &lt;code&gt;type&lt;/code&gt; 属性不是 &lt;code&gt;module&lt;/code&gt;，则这个 &lt;code&gt;script&lt;/code&gt; 元素的 &lt;code&gt;textContent&lt;/code&gt; 就会像你所说的那样，立刻“同步”执行。关于这一点&lt;code&gt;whatwg&lt;/code&gt;的&lt;code&gt;html&lt;/code&gt;文档中有详细说明&lt;a href=&quot;https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model:immediately-2&quot; title=&quot;https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model:immediately-2&quot;&gt;4.12.1 The script element&lt;/a&gt;，按照文档的理解，这个内联的脚本会立即被压到执行栈的顶部立即执行，相当于包含脚本内的函数一样，具体看下面这段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;js/test.js&quot;&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div id=&quot;test&quot;&gt;test1&amp;#x3C;/div&gt;

    &amp;#x3C;script&gt;
      let script = document.createElement(&apos;script&apos;)
      script.text = &apos;console.log(1)&apos;
      document.body.appendChild(script)
      console.log(2)

      window.onload = function () {
        console.log(&apos;window load...&apos;)
        console.log(&apos;onload: &apos;, window.React)
      }
    &amp;#x3C;/script&gt;
    &amp;#x3C;div&gt;test2&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//test.js
console.log(&apos;domcontentloaded&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;1
2
domcontentloaded
window load...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从结果可以看书，添加的内联 &lt;code&gt;script&lt;/code&gt; 立即执行了，比后面的 &lt;code&gt;console.log(1)&lt;/code&gt; 还要更早执行，文档中说的是 &lt;code&gt;Immediately execute the script block, even if other scripts are already executing.&lt;/code&gt;，这种感觉就像是包含脚本中的同步函数，执行到这里立即压栈执行。显而易见，动态添加的内联的脚本是会阻塞页面的解析和渲染的，和原本就在文档中的内联脚本并没有不同，如果把 &lt;code&gt;script.test&lt;/code&gt; 换成如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;script.text = `
        var now = new Date().getTime();
        while(new Date().getTime() - now &amp;#x3C; 3000) {
          continue;
        }
      `
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到页面会白屏直到这段代码执行完成，后面的脚本才能执行，文档才能继续解析和渲染。&lt;/p&gt;
&lt;h2&gt;未连接的 CSS/JS 不会被载入&lt;/h2&gt;
&lt;p&gt;如果你创建了一个 &lt;code&gt;&amp;#x3C;link rel=&quot;stylesheet&quot;&gt;&lt;/code&gt; （或 &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; ）但并未连接到 &lt;code&gt;DOM&lt;/code&gt; 树，那么它不会被加载。 这是标准行为与浏览器实现方式无关，因此你可以放心地利用该特性。 该特性很容易测试，只需创建一个 &lt;code&gt;&amp;#x3C;link rel=&quot;stylesheet&quot;&gt;&lt;/code&gt; （或 &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; ）标签并查看是否产生网络请求：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var link = document.createElement(&apos;link&apos;)
link.rel = &apos;stylesheet&apos;
link.href = &apos;https://cdn.jsdelivr.net/npm/animate.css@3.5.2/animate.css&apos;
var script = document.createElement(&apos;script&apos;)
script.src = &apos;https://cdn.jsdelivr.net/npm/react@15.4.0/dist/react.js&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;监听异步资源载入&lt;/h2&gt;
&lt;p&gt;我们可以利用 &lt;code&gt;document.onload&lt;/code&gt; 事件来监听资源是否已经从外部加载完成，代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;js/test.js&quot;&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div id=&quot;test&quot;&gt;test1&amp;#x3C;/div&gt;

    &amp;#x3C;script&gt;
      var link = document.createElement(&apos;link&apos;)
      link.rel = &apos;stylesheet&apos;
      link.href = &apos;https://cdn.jsdelivr.net/npm/animate.css@3.5.2/animate.css&apos;
      var script = document.createElement(&apos;script&apos;)
      script.src = &apos;https://cdn.jsdelivr.net/npm/react@15.4.0/dist/react.js&apos;
      document.body.appendChild(script)
      script.onload = function () {
        console.log(&apos;source loaded!&apos;)
      }

      window.onload = function () {
        console.log(&apos;window load...&apos;)
      }
    &amp;#x3C;/script&gt;
    &amp;#x3C;div&gt;test2&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//test.js
console.log(&apos;domcontentloaded&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;domcontentloaded
source loaded!
window load...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想直到具体加载事件还可以用 &lt;code&gt;new Date().getTime()&lt;/code&gt; 来查看具体的毫秒数。&lt;/p&gt;
&lt;h2&gt;innerHTML&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;innerHTML&lt;/code&gt; 属性可用来设置 DOM 内容，但不可用来插入并执行 &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt;。 下面的内联脚本和外部脚本都不会被执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;document.body.innerHTML = &apos;&amp;#x3C;script src=&quot;foo.js&quot;&gt;&amp;#x3C;/script&gt;&apos;
document.body.innerHTML = &apos;&amp;#x3C;script&gt;console.log(&quot;foo&quot;)&amp;#x3C;/script&gt;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在设置 &lt;code&gt;innerHTML&lt;/code&gt; 时，浏览器会初始化一个新的 &lt;code&gt;HTML Parser&lt;/code&gt; 来解析它。 只要与该 &lt;code&gt;Parser&lt;/code&gt; 关联的 &lt;code&gt;DOM&lt;/code&gt; 启用了 &lt;code&gt;JavaScript&lt;/code&gt;（通常是启用的），脚本的 &lt;code&gt;scripting flag&lt;/code&gt; 就为真， 但是即便如此，&lt;a href=&quot;https://html.spec.whatwg.org/#other-parsing-state-flags&quot; title=&quot;HTML 片段的解析过程中，脚本是不会执行的&quot;&gt;HTML 片段的解析过程中，脚本是不会执行的&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create a new HTML parser, and associate it with the just created Document node. – 12.4 Parsing HTML fragments, WHATWG The scripting flag can be enabled even when the parser was originally created for the HTML fragment parsing algorithm, even though script elements don’t execute in that case. – 12.2.3.5 Other parsing state flags, WHATWG&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;事实上，设置 &lt;code&gt;innerHTML&lt;/code&gt; 和 &lt;code&gt;outerHTML&lt;/code&gt; 都不执行脚本，但 &lt;code&gt;document.write()&lt;/code&gt; 是会同步执行的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When inserted using the document.write() method, script elements execute (typically blocking further script execution or HTML parsing), but when inserted using innerHTML and outerHTML attributes, they do not execute at all. – 4.12.1 The script element WHATWG&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;结合&lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2019/04/25/how-browser-work/&quot; title=&quot;浏览器渲染过程和JS引擎浅析&quot;&gt;浏览器渲染过程和JS引擎浅析&lt;/a&gt;和这篇文章，对于整个浏览器的渲染过程，和我们的代码会在哪个阶段执行，是否会被阻塞应该都能有一个清晰的认识了，虽然扣了很多细节，不过这对于我们写出更好的代码还是有帮助的。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://harttle.land/2016/11/26/dynamic-dom-render-blocking.html&quot; title=&quot;异步渲染的下载和阻塞行为&quot;&gt;异步渲染的下载和阻塞行为&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://harttle.land/2017/01/16/dynamic-script-insertion.html&quot; title=&quot;在 DOM 中动态执行脚本&quot;&gt;在 DOM 中动态执行脚本&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/65188909/answer/238020047&quot; title=&quot;在DOM中 append 一个 script 元素，该元素内的 JavaScript 为何同步执行？ - 紫云飞的回答 - 知乎 &quot;&gt;在DOM中 append 一个 script 元素，该元素内的 JavaScript 为何同步执行？ - 紫云飞的回答 - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>搞懂字符编码</title><link>https://clloz.com/blog/character-encoding</link><guid isPermaLink="true">https://clloz.com/blog/character-encoding</guid><pubDate>Fri, 26 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们经常听到 &lt;code&gt;ASCII&lt;/code&gt;，&lt;code&gt;UTF-8&lt;/code&gt;，&lt;code&gt;UTF-16&lt;/code&gt;，这些都是字符的编码格式，它们之间有什么区别，为什么要搞这么多字符的编码格式，在写代码的过程中我们会遇到各种编码格式的字符，所以经常需要 &lt;code&gt;encode&lt;/code&gt;，&lt;code&gt;decode&lt;/code&gt;，如果不搞清楚字符编码到底是什么，每次遇到编码问题都会很头疼，搜索引擎都很难帮助你解决问题，久而久之对字符编码产生厌恶。所以还是一次性把这个问题搞清楚最好，免除后顾之忧。&lt;/p&gt;
&lt;h2&gt;给字符编码&lt;/h2&gt;
&lt;p&gt;我们的计算机只能处理数字，所有的信息无论是在硬盘还是内存里都是二进制字节流，8个二进制位 &lt;code&gt;bit&lt;/code&gt; 组成一个字节 &lt;code&gt;byte&lt;/code&gt;，每一个二进制位可以表示 &lt;code&gt;0&lt;/code&gt; 和 &lt;code&gt;1&lt;/code&gt; 两种状态，一个字节就可以表示$2^8$&lt;code&gt;256&lt;/code&gt;种状态，从 &lt;code&gt;00000000&lt;/code&gt; 到 &lt;code&gt;11111111&lt;/code&gt;。但是我们生活中使用的不只有数字，还有各种各样的字符，我们怎么才能在计算机中使用和操作字符呢？所以我们就需要制定一种规则，把我们要使用的字符和计算机能识别的二进制数一一对应起来，这样计算机在解析到字符对应的二进制数就知道要显示哪个字符，再把这个字符渲染到显示器上就可以了。这样我们就把我们使用的字符转化为计算机能识别的数字，最后计算机再把这个数字渲染成我们认识的字符，就实现了我们在计算机中操作字符的需求。&lt;/p&gt;
&lt;p&gt;现在还有一个问题是，我们要显示多少种字符，每一个字符对应一个状态，有多少字符我们就有多少种状态，从而知道我们要用多少位二进制数来显示全部字符。由于计算机最早是在美国发明的，上世纪60年代的时候，计算机科学家就根据当时的需求制定了一套字符编码，就是我们现在说的 &lt;code&gt;ASCII&lt;/code&gt; 码，这套编码一直到今天还在使用。&lt;code&gt;ASCII&lt;/code&gt; 码一共规定了 &lt;code&gt;128&lt;/code&gt; 个字符的编码，包含常见的英语字符和一些控制符号，比如空格 &lt;code&gt;SPACE&lt;/code&gt; 是 &lt;code&gt;32&lt;/code&gt;（二进制 &lt;code&gt;00100000&lt;/code&gt; ），大写的字母 &lt;code&gt;A&lt;/code&gt; 是 &lt;code&gt;65&lt;/code&gt;（二进制 &lt;code&gt;01000001&lt;/code&gt; ）。这 &lt;code&gt;128&lt;/code&gt; 个符号（包括 &lt;code&gt;32&lt;/code&gt; 个不能打印出来的控制符号），只占用了一个字节的后面 &lt;code&gt;7&lt;/code&gt; 位，最前面的一位统一规定为 &lt;code&gt;0&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/ascii.Bvmbxbup_Z1mRvwc.webp&quot; alt=&quot;ascii&quot; title=&quot;ascii&quot;&gt;&lt;/p&gt;
&lt;h2&gt;编码扩展&lt;/h2&gt;
&lt;p&gt;由于当时计算机刚刚开始发展，使用的人还很少，在英语环境中，&lt;code&gt;ASCII&lt;/code&gt; 码基本上也够用了。可是这个世界上语言众多，不是所有国家都是用英文的，当欧洲人开始使用计算机发现，我们的字母也需要编码使用呀，比如，在法语中，字母上方有注音符号，它就无法用 &lt;code&gt;ASCII&lt;/code&gt; 码表示。不过一个字节表示 &lt;code&gt;ASCII&lt;/code&gt; 码不是还空余一位嘛，用上这一位又可以表示 &lt;code&gt;128&lt;/code&gt; 个字符了，于是欧洲国家纷纷用这一位闲置的位来表示新的符号，比如，法语中的 &lt;code&gt;é&lt;/code&gt; 的编码为 &lt;code&gt;10000010&lt;/code&gt;。这样一来，这些欧洲国家使用的编码体系，可以表示最多 &lt;code&gt;256&lt;/code&gt; 个符号。&lt;/p&gt;
&lt;p&gt;但是世界上的语言实在是太多了，就 &lt;code&gt;128&lt;/code&gt;个编码空间实在是不够，于是各个国家用后 &lt;code&gt;128&lt;/code&gt; 个二进制数表示自己的语言，比如，&lt;code&gt;10000010&lt;/code&gt; 在法语编码中代表了 &lt;code&gt;é&lt;/code&gt;，在希伯来语编码中却代表了字母 &lt;code&gt;Gimel (ג)&lt;/code&gt;，在俄语编码中又会代表另一个符号。每个国家的字符编码都兼容 &lt;code&gt;ASCII&lt;/code&gt; 码，也就是前 &lt;code&gt;128&lt;/code&gt; 个编码都是 &lt;code&gt;ASCII&lt;/code&gt;，后面的 &lt;code&gt;128&lt;/code&gt; 个就根据自己国家的语言来。&lt;/p&gt;
&lt;p&gt;而到了东亚这边，情况就更严重了，中文，日文，韩文等等东亚国家的文字都非常多，远不是一个字节能表示的。光是中文就好几千常用字，而且很多人的姓名里面有一些生僻字，总不能连自己的名字也打不出来把。一个字符明显不够，于是我们就加一个字节，设计出了 &lt;code&gt;GB2312&lt;/code&gt; 字符集。一个字节功能表示&lt;code&gt;256&lt;/code&gt;种状态，两个字节一共是 &lt;code&gt;16&lt;/code&gt; 位二进制数，可以表示$2^{16}$共 &lt;code&gt;65546&lt;/code&gt; 种状态。不过 &lt;code&gt;GB2312&lt;/code&gt; 只收录了一些常用汉字 &lt;code&gt;7445&lt;/code&gt; 个。由于这些常用字符还是不太够用，后来有扩展成 &lt;code&gt;21886&lt;/code&gt; 个字符的 &lt;code&gt;GKB&lt;/code&gt;，也就是现在最常用的中文字符集，windows的中文系统就用的 &lt;code&gt;GBK&lt;/code&gt; 编码。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们的 &lt;code&gt;GBK&lt;/code&gt; 和 &lt;code&gt;GB2312&lt;/code&gt; 同样是兼容 &lt;code&gt;ASCII&lt;/code&gt; 码的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;字符集和字符编码&lt;/h2&gt;
&lt;p&gt;这两个词容易引起误解，要清楚地解释它们的区别不太容易，特别是因为大家对 &lt;code&gt;ASCII&lt;/code&gt; 的概念比较深就容易混淆，因为 &lt;code&gt;ASCII&lt;/code&gt; 因为字符较少可以直接把字符集中的元素按自然数排列拿来做编码。因为 &lt;code&gt;ASCII&lt;/code&gt; 中每一个字符都在一个字节里面，我们直接就用这种最简单的方式实现就可以了，不会引起计算机的误解。但是中文是两个字节表示，英文是一个字节表示，如果这两种字符混合在一起，计算机该怎么分辨呢？可能你会觉得英文也用两个字节不就可以了，但是这回造成空间的浪费，如果一篇全是英文的文章，用这样的方法大小就会是原来的两倍。那么混合的编码怎么处理呢？在 &lt;code&gt;GB2312&lt;/code&gt; 里面，当一个字节的第一位是 &lt;code&gt;0&lt;/code&gt;，那么就代表这是一个 &lt;code&gt;ASCII&lt;/code&gt; 码，而其他字符都是第一位为 &lt;code&gt;1&lt;/code&gt; 的两个字节组成。这样计算机在解码的时候就知道，遇到字节是以 &lt;code&gt;0&lt;/code&gt; 开头的，就知道这一个字节就表示了一个字符；遇到字节是以1开头的，就知道要加上下一个字节合起来表示一个字符。这样就在 &lt;code&gt;GB2312&lt;/code&gt; 中既把 &lt;code&gt;ASCII&lt;/code&gt; 的字符包含了进来，又能将它们区分出来，能达到兼容的效果了。&lt;/p&gt;
&lt;p&gt;比如用 &lt;code&gt;GB2312&lt;/code&gt; 来写 &lt;code&gt;我叫ABC&lt;/code&gt; ，那么二进制编码是 &lt;code&gt;11001110 11010010 10111101 11010000 01000001 01000002 01000003&lt;/code&gt;，解码的时候，当遇到 &lt;code&gt;1&lt;/code&gt; 开头的字节，就把两个字节合起来解释为一个字符，于是 &lt;code&gt;11001110 11010010&lt;/code&gt; 会被解释为我；遇到 &lt;code&gt;0&lt;/code&gt; 开头的字节，就只把这个字节解释为一个字符，于是 &lt;code&gt;01000001&lt;/code&gt; 就会被解释为 &lt;code&gt;A&lt;/code&gt; 了。&lt;/p&gt;
&lt;p&gt;我认为可以这么理解， 字符集就是我们所有要用的字符的集合，集合的三大特性相信大家都学过 &lt;code&gt;确定性，无序性，互异性&lt;/code&gt; （实际操作不会是无序的，会有一个最简单的映射，比如自然数排列），而字符编码用二进制数对集合中的字符进行一一映射，这种一一映射可以有无数种，比如我有 &lt;code&gt;100&lt;/code&gt; 个字符，我可以是从 &lt;code&gt;0～99&lt;/code&gt; 的自然数，我也可以是从 &lt;code&gt;0～198&lt;/code&gt; 的偶数，甚至如果我高兴，我可以是从 &lt;code&gt;1000～901&lt;/code&gt; 的倒序数，对于集合中的元素也是，比如 &lt;code&gt;啊&lt;/code&gt; 这个字，我可以把它放在映射的第一个，也可以把它放在最后一个，最重要的是，我们选择的这种映射能够最有效率地利用字节空间同时让计算机能够轻松地识别每一组映射，这因为这个需求我们的 &lt;code&gt;GB2312&lt;/code&gt; 字符编码才选择了上述的映射方式，因为这是比较有效率，计算机也能轻松识别的。而 &lt;code&gt;ASCII&lt;/code&gt; 选择的映射就是最简单的从 &lt;code&gt;0&lt;/code&gt; 开始按自然数排列，因为它的字符少也不需要考虑兼容，这中方式就是最有效率最合适的，但是对于一些字符数量非常多还要考虑兼容其他字符的字符集来说，就需要考虑更好的实现方案。理解这两者的却别对于后面的 &lt;code&gt;Unicode&lt;/code&gt; 字符集和它的多种编码方式有帮助。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;维基百科上有&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81#%E7%8E%B0%E4%BB%A3%E7%BC%96%E7%A0%81%E6%A8%A1%E5%9E%8B&quot; title=&quot;现代编码模型&quot;&gt;现代编码模型&lt;/a&gt;，整体我的理解还是没问题的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Unicode&lt;/h2&gt;
&lt;p&gt;在早期，网路还不是那么发达的时候，大家基本都在自己同语言范围内的网络进行活动，大家的系统以及软件的字符编码方式几乎都是差不多的，所以并不会引起什么大问题。但是随着网络越来越发达，各个国家之间的交流越来越频繁，不同字符编码导致的乱码问题让出现一个同一的编码的需求越来越强烈，这也就是现在大家所知的 &lt;code&gt;Unicode&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Unicode&lt;/code&gt; 对世界上大部分的文字系统进行了整理、编码，使得计算机可以用更为简单的方式来呈现和处理文字。&lt;code&gt;Unicode&lt;/code&gt; 有两种格式：&lt;code&gt;UCS-2&lt;/code&gt; 和 &lt;code&gt;UCS-4&lt;/code&gt;。&lt;code&gt;UCS-2&lt;/code&gt; 就是用两个字节编码，一共 &lt;code&gt;16&lt;/code&gt; 个比特位，这样理论上最多可以表示 &lt;code&gt;65536&lt;/code&gt; 个字符，不过要表示全世界所有的字符显示 &lt;code&gt;65536&lt;/code&gt; 个数字还远远不过，因为光汉字就有近 &lt;code&gt;10&lt;/code&gt; 万个，因此 &lt;code&gt;Unicode4.0&lt;/code&gt;规范定义了一组附加的字符编码，&lt;code&gt;UCS-4&lt;/code&gt; 就是用&lt;code&gt;4&lt;/code&gt; 个字节（实际上只用了 &lt;code&gt;31&lt;/code&gt; 位，最高位必须为 &lt;code&gt;0&lt;/code&gt; ）。理论上完全可以涵盖一切语言所用的符号。世界上任何一个字符都可以用一个 &lt;code&gt;Unicode&lt;/code&gt; 编码来表示，一旦字符的 &lt;code&gt;Unicode&lt;/code&gt; 编码确定下来后，就不会再改变了。&lt;/p&gt;
&lt;p&gt;这样的大型字符集在实现的时候就需要解决我们上面说的两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于不需要多个字节表示的字符，怎么避免存储空间的浪费。&lt;/li&gt;
&lt;li&gt;对于多个字节，比如两个字节，到底是一个两字节字符还是两个单字节字符。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Unicode&lt;/code&gt; 字符集有一套自己的字符和编码的映射（即下面说的码点），但是具体到计算机上的实现（即编码方式）需要考虑上述两个问题。如 &lt;code&gt;汉&lt;/code&gt; 字的 &lt;code&gt;Unicode&lt;/code&gt; 编码是 &lt;code&gt;6C49&lt;/code&gt;，我们可以直接按这个编码传输，也可以用 &lt;code&gt;utf-8&lt;/code&gt; 编码的3个连续的字节 &lt;code&gt;E6 B1 89&lt;/code&gt; 来表示它，&lt;code&gt;Unicode&lt;/code&gt; 编码有不同的实现方式，比如：&lt;code&gt;UTF-8&lt;/code&gt;、&lt;code&gt;UTF-16&lt;/code&gt; 和 &lt;code&gt;UTF-32&lt;/code&gt;等等，具体使用哪一种要根据我们的需求和使用场景，有时候也有一些历史因素。&lt;/p&gt;
&lt;p&gt;几个重要的概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Character&lt;/code&gt;：字符，这里可以理解成用户实际看到的实体，比如：&lt;code&gt;A&lt;/code&gt;、&lt;code&gt;好&lt;/code&gt; 等等；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Code Point&lt;/code&gt;：码点/码位，对应 &lt;code&gt;Unicode&lt;/code&gt; 字符集中每个 &lt;code&gt;Character&lt;/code&gt; 的数字编号，比如 &lt;code&gt;好&lt;/code&gt; 的码点是：&lt;code&gt;U+597D&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Code Unit&lt;/code&gt;：编码方案对码点进行编码后的结果，比如 &lt;code&gt;好&lt;/code&gt; 的 &lt;code&gt;UTF-16&lt;/code&gt;编码结果为：&lt;code&gt;Ox597D&lt;/code&gt;，&lt;code&gt;UTF-8&lt;/code&gt; 编码结果为：&lt;code&gt;E5A5BD&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Normalization&lt;/code&gt;：字符标识标准化，有时候，一个字符看起来是多个字符的组成，比如 &lt;code&gt;ö&lt;/code&gt;，可以看成一个字符，也可以看成由字符 &lt;code&gt;o&lt;/code&gt; 和 &lt;code&gt;¨&lt;/code&gt; 组合而成，而在 &lt;code&gt;Unicode&lt;/code&gt; 通过对每个字符对应一个码点而达到标准化字符标识的目的；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BMP&lt;/code&gt;：&lt;code&gt;Basic Multilingual Plane&lt;/code&gt;，基本平面，也称零号平面，&lt;code&gt;Unicode Code Point&lt;/code&gt; 处于 &lt;code&gt;U+0000 - U+FFFF&lt;/code&gt;之间的字符；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SMP&lt;/code&gt;：&lt;code&gt;supplementary planes&lt;/code&gt; 或 &lt;code&gt;astral planes&lt;/code&gt;，辅助平面，&lt;code&gt;Unicode Code Point&lt;/code&gt; 处于 &lt;code&gt;U+10000 - U+10FFFF&lt;/code&gt; 之间的字符；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UTF&lt;/code&gt; 即是 &lt;code&gt;Unicode&lt;/code&gt; 转换格式（&lt;code&gt;Unicode (or UCS) Transformation Format&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们来看看 &lt;code&gt;Unicode&lt;/code&gt; 的码点范围，现在 &lt;code&gt;Unicode&lt;/code&gt; 标准的表示范围为 &lt;code&gt;U+0000~U+10FFFF&lt;/code&gt;，共有 &lt;code&gt;110000&lt;/code&gt; 个状态，十进制为 &lt;code&gt;1114112&lt;/code&gt; 个码点。其中我们最常用的字符都集中在 &lt;code&gt;U+0000 - U+FFFF&lt;/code&gt; 共 $2^{16}$ &lt;code&gt;65536&lt;/code&gt; 个码点，我们称之为基础平面。基础平面的分布看下图。每一个方块中都是 &lt;code&gt;256&lt;/code&gt; 个码点，葛优 &lt;code&gt;FF(256)&lt;/code&gt; 个方格，不同颜色的方格用来表示不同的类型的字符，比如图中间一大块粉色的区域 &lt;code&gt;CJK characters&lt;/code&gt; 就是我们比较熟悉的 &lt;code&gt;Chinese, Japanese, and Korean&lt;/code&gt; 中日韩文字的 &lt;code&gt;Unicode&lt;/code&gt; 码点区。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.clloz.com/blog/writing/Roadmap_to_Unicode_BMP.svg&quot; alt=&quot;零号平面&quot; title=&quot;零号平面&quot;&gt;&lt;/p&gt;
&lt;p&gt;以&lt;code&gt;U+0000 - U+FFFF&lt;/code&gt; 为一个平面，&lt;code&gt;U+0000~U+10FFFF&lt;/code&gt; 的全部码点就可以分为 &lt;code&gt;17&lt;/code&gt; 个平面，由于基本平面已经能满足我们的使用的，所以后面的平面成为 &lt;code&gt;辅助平面&lt;/code&gt;。基础平面的码点范围在 &lt;code&gt;0 ~ 65535&lt;/code&gt;，在计算机中可以用 &lt;code&gt;16&lt;/code&gt; 个二进制位表示，也就是两个字节，而所有辅助平面都已经不能只用两个字节来表示了。需要注意的是，&lt;code&gt;Unicode&lt;/code&gt; 目前虽然定义了 &lt;code&gt;1114112&lt;/code&gt; 个码点，但其实很多是空的，还未填充，目前大概只定义了十多万。这么多的码点只是预先规划的，因为我们生活的世界字符是在不断增加的，目前的看来一百多万是完全够用了。&lt;code&gt;Unicode&lt;/code&gt; 自版本 &lt;code&gt;2.0&lt;/code&gt;开始保持了向后兼容，即新的版本仅仅增加字符，原有字符不会被删除或更名。从下面的平面分布图可以看出 &lt;code&gt;unicode&lt;/code&gt; 还预留了两个私有平面，是用来自定义字符的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/unicode-supp-chars.XhxYeaFP_1bJnAN.webp&quot; alt=&quot;平面分布图&quot; title=&quot;平面分布图&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GNU Unifont&lt;/code&gt; 制作了一张基本平面的全部字符的图，全图大小 &lt;code&gt;4000 x 4000&lt;/code&gt;，点击&lt;a href=&quot;https://www.clloz.com/study/unifont/unifont.html&quot;&gt;基本平面字符图&lt;/a&gt;查看（由于图片比较大，打开可能有点慢）。&lt;/p&gt;
&lt;h2&gt;UTF-8&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;UTF-8&lt;/code&gt; （ &lt;code&gt;Unicode Transformation Format&lt;/code&gt; ）作为 &lt;code&gt;Unicode&lt;/code&gt; 的一种实现方式，广泛应用于互联网，它是一种变长的字符编码，可以根据具体情况用 &lt;code&gt;1-4&lt;/code&gt;个字节来表示一个字符。比如英文字符这些原本就可以用 &lt;code&gt;ASCII&lt;/code&gt; 码表示的字符用&lt;code&gt;UTF-8&lt;/code&gt;表示时就只需要一个字节的空间，和 &lt;code&gt;ASCII&lt;/code&gt; 是一样的。对于多字节（ &lt;code&gt;n&lt;/code&gt; 个字节）的字符，第一个字节的前 &lt;code&gt;n&lt;/code&gt; 为都设为 &lt;code&gt;1&lt;/code&gt; ，第 &lt;code&gt;n+1&lt;/code&gt;位设为 &lt;code&gt;0&lt;/code&gt;，后面字节的前两位都设为10。剩下的二进制位全部用该字符的 &lt;code&gt;unicode&lt;/code&gt;码填充。&lt;/p&gt;
&lt;p&gt;| Unicode符号范围 (十六进制) | UTF-8编码方式（二进制）             | 十进制表示                 |
| -------------------------- | ----------------------------------- | -------------------------- |
| U+0000 0000 - U+0000 007F  | 0xxxxxxx                            | 0 - 127                    |
| U+0000 0080 - U+0000 07FF  | 110xxxxx 10xxxxxx                   | 128 - 2047                 |
| U+0000 0800 - U+0000 FFFF  | 1110xxxx 10xxxxxx 10xxxxxx          | 2048 - 65535               |
| U+0001 0000 - U+0010 FFFF  | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 65536 - 1114111（2097152） |&lt;/p&gt;
&lt;p&gt;比如 &lt;code&gt;严&lt;/code&gt; 的 Unicode 是 &lt;code&gt;4E25(100111000100101&lt;/code&gt;，根据上表，可以发现 &lt;code&gt;4E25&lt;/code&gt; 处在第三行的范围内 &lt;code&gt;(0000 0800 - 0000 FFFF)&lt;/code&gt;，因此严的 &lt;code&gt;UTF-8&lt;/code&gt; 编码需要三个字节，即格式是 &lt;code&gt;1110xxxx 10xxxxxx 10xxxxxx&lt;/code&gt;。然后，从严的最后一个二进制位开始，依次从后向前填入格式中的 &lt;code&gt;x&lt;/code&gt;，多出的位补 &lt;code&gt;0&lt;/code&gt;。这样就得到了，严的 &lt;code&gt;UTF-8&lt;/code&gt; 编码是 &lt;code&gt;11100100 10111000 10100101&lt;/code&gt;，转换成十六进制就是 &lt;code&gt;E4B8A5&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;乱码&lt;/h2&gt;
&lt;p&gt;由于字符编码多种多样，不同的字符编码之间互相不能兼容就会造成乱码现象。首先要明确的一点是，我们在显示器上看到的字符都是经过计算机用对应的字符编码解码以后渲染给我们的，在计算机存储设备上保存的，以及在网络上传输的，都是字符经过编码后的二进制字节流。打个简单的比方，你在一个网页复制了一段文本到你的 &lt;code&gt;word&lt;/code&gt; 或者 &lt;code&gt;txt&lt;/code&gt; 里面去，在计算机内部，你也不过是复制了这个字符对应的编码值过去，比如我在 &lt;code&gt;vscode&lt;/code&gt; 里面创建了一个 &lt;code&gt;GBK&lt;/code&gt; 格式的文本，然后在用 &lt;code&gt;UTF-8&lt;/code&gt; 的格式打开，那么就会出现乱码。但是如果我们直接复制文本到其他 &lt;code&gt;utf-8&lt;/code&gt; 的文本中去，不会乱码，这应该是软件自动帮你转码过了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/encode-gbk.D6WuB3Yy_OVhiG.webp&quot; alt=&quot;encode-gkb&quot; title=&quot;encode-gkb&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/encode-utf-8.CsrENJZF_Ks509.webp&quot; alt=&quot;encode-utf-8&quot; title=&quot;encode-utf-8&quot;&gt;&lt;/p&gt;
&lt;p&gt;不同的系统，不同的编辑器，不同的程序编译器解释器，默认支持的编码可能都是不同的，我们复制或者从浏览器获取的字符，都只是一段二进制字符编码，如果我们所在的环境的字符编码中没有我们所要显示的这段字符的映射，自然就会出现乱码。比如 &lt;code&gt;windows&lt;/code&gt; 的中文系统默认编码是 &lt;code&gt;GBK&lt;/code&gt;，那么在 &lt;code&gt;windows&lt;/code&gt; 的命令行运行的程序如果输出的是 &lt;code&gt;utf-8&lt;/code&gt; 的字符，将会出现乱码。不过现在大部分系统和环境都是支持&lt;code&gt;Unicode&lt;/code&gt;的，比如windows系统就能够把Unicode映射到 &lt;code&gt;GBK&lt;/code&gt;，映射表见&lt;a href=&quot;https://www.unicode.org/charts/&quot; title=&quot;Unicode 12.0 Character Code Charts&quot;&gt;Unicode 12.0 Character Code Charts&lt;/a&gt;，那么如果你的环境能够帮你把你的字符编码转换成 &lt;code&gt;Unicode&lt;/code&gt; 编码，那么大部分的程序和系统都能够识别了。所以在写代码的过程中如果遇到乱码，检查我们的字符的编码格式和环境的编码格式是否一直，如果不一致可以利用语言提供的转码工具转成 &lt;code&gt;Unicode&lt;/code&gt; 来解决乱码问题。比如 &lt;code&gt;python&lt;/code&gt; 中的 &lt;code&gt;encode&lt;/code&gt; 和 &lt;code&gt;decode&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;UTF-8&lt;/code&gt; --&gt; &lt;code&gt;decode&lt;/code&gt; 解码 --&gt; &lt;code&gt;Unicode&lt;/code&gt;，&lt;code&gt;Unicode&lt;/code&gt; --&gt; &lt;code&gt;encode&lt;/code&gt; 编码 --&gt; &lt;code&gt;GBK&lt;/code&gt; / &lt;code&gt;UTF-8&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;编程语言中的字符编码&lt;/h2&gt;
&lt;p&gt;我们经常会看到某某语言默认用的什么编码，这种说法让人很疑惑，因为我们把编程语言的默认编码和我们使用的编辑器支持的编码当成了同一个东西，我们的编辑器可以支持各种编码，但当我们写好程序，要用进行程序的编译或者解释运行的时候，编译器或解释器对变量在内存中的处理使用的字符编码就是程序的默认编码。比如 &lt;code&gt;python2&lt;/code&gt; 默认编码就是 &lt;code&gt;ascii&lt;/code&gt;，也就是说当我们的程序被 &lt;code&gt;python&lt;/code&gt; 解释器装载到内存运行的时候，解释器会把编辑器中保存的编码识别成 &lt;code&gt;ascii&lt;/code&gt;，比如我们在编辑器中用了 &lt;code&gt;utf-8&lt;/code&gt;，而我们保存了一个字符 &lt;code&gt;严&lt;/code&gt;，它的 &lt;code&gt;utf-8&lt;/code&gt; 编码是 &lt;code&gt;11100100 10111000 10100101&lt;/code&gt; 三个字节，可是 &lt;code&gt;python2&lt;/code&gt; 的解释器会把它当作三个 &lt;code&gt;ascii&lt;/code&gt; 来处理，这必然会出错，所以要在程序文件的开头声明文档的编码格式，这样解释器才知道怎么转码。不过 &lt;code&gt;python3&lt;/code&gt; 已经会自动把我们的编码转成 &lt;code&gt;unicode&lt;/code&gt;，&lt;code&gt;Unicode&lt;/code&gt; 是能够被各种环境识别的。如果你还想更多地了解，可以看这篇文章：&lt;a href=&quot;https://pycoders-weekly-chinese.readthedocs.io/en/latest/issue5/unipain.html&quot; title=&quot;Unicode之痛&quot;&gt;Unicode之痛&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;UTF-16&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;UTF-16&lt;/code&gt;是一种变长的 &lt;code&gt;2&lt;/code&gt; 或 &lt;code&gt;4&lt;/code&gt; 字节编码模式。对于BMP内的字符使用 &lt;code&gt;2&lt;/code&gt; 字节编码，其它的则使用 &lt;code&gt;4&lt;/code&gt; 字节组成所谓的代理对来编码。对于 &lt;code&gt;BMP&lt;/code&gt; 内的字符两个字节能够完全表示，很容易理解，关键问题是如何理解 &lt;code&gt;UTF-16&lt;/code&gt; 是如何用代理吗来表示辅助平面上的码点的。&lt;/p&gt;
&lt;p&gt;在上面基础平面表示的那张方格图上，有一块灰色区域，也就是 &lt;code&gt;D8 - DF&lt;/code&gt;，右边的实例图表给出的说明是 &lt;code&gt;UTF-16 surrogates&lt;/code&gt;，也就是 &lt;code&gt;UTF-16代理&lt;/code&gt;。如果你还打开了上面基本平面字符图你就会发现这块区域是空白的。为什么要留这么一块空白呢，不是造成浪费么。这块空白就是 &lt;code&gt;UTF-16&lt;/code&gt; 表示辅助平面字符的关键。&lt;/p&gt;
&lt;p&gt;从 &lt;code&gt;D8 - DF&lt;/code&gt; 被分成两块，&lt;code&gt;D8 - DB&lt;/code&gt; 和 &lt;code&gt;DC - DF&lt;/code&gt;，分别叫做&lt;code&gt;搞代理区 High Surrogate Area&lt;/code&gt; 和 &lt;code&gt;低代理区 Low Surrogate Area&lt;/code&gt;。上面我们说过每一个方格里有 &lt;code&gt;256&lt;/code&gt; 个码点，也就是说搞代理区和低代理区分别有 &lt;code&gt;1024&lt;/code&gt; 个状态。通过排列组合我们可以知道，两者结合一共可以表示 $2^{10} * 2 ^{10} = 2^ {20}$个状态。而我们的辅助平面一共有多少个码点呢？&lt;code&gt;16 * 65536&lt;/code&gt; 也就是$2^4 * 2^{16}=2^{20}$，两者是相等的。这正是 &lt;code&gt;UTF-16&lt;/code&gt; 的代理码的原理。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;下面这一段落数字全部为十六进制&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;辅助平面的范围是从 &lt;code&gt;10000 - 10FFFF&lt;/code&gt;，去除掉基础平面的 &lt;code&gt;10000&lt;/code&gt; 个状态，也就是代理码要表示 &lt;code&gt;0000 - 9FFFF&lt;/code&gt;。具体的数学计算就很简单了，用辅助平面的码点先减去基础平面的 &lt;code&gt;10000&lt;/code&gt;，然后用除法计算出高代理区的值，用取余得出低代理区的值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高代理区：&lt;code&gt;(码点值 - 10000) / 400 + D800&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;低代理区：&lt;code&gt;(码点值 - 10000) % 400 + DC00&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以 &lt;code&gt;Unicode Character “𝌆” (U+1D306)&lt;/code&gt; 来计算，高代理区为 &lt;code&gt;(1D306 - 10000) / 400 + D800 = D834&lt;/code&gt;，低代理区为 &lt;code&gt;(1D306 - 10000) % 400 + DC00 = DF06&lt;/code&gt;，关于不同编码模式的值得查询可以参考&lt;a href=&quot;https://www.compart.com/en/unicode/&quot; title=&quot;Unicode编码查询&quot;&gt;Unicode编码查询&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;前端使用 Unicode&lt;/h2&gt;
&lt;p&gt;字符 &lt;code&gt;unicode&lt;/code&gt; 编码查询点击&lt;a href=&quot;http://www.mytju.com/classcode/tools/encode_utf8.asp&quot; title=&quot;查询链接&quot;&gt;查询链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;CSS 中的使用&lt;/h2&gt;
&lt;p&gt;比如在伪元素的 &lt;code&gt;content&lt;/code&gt; 中使用 &lt;code&gt;unicode&lt;/code&gt;，使用方法是 &lt;code&gt;\&lt;/code&gt; 后加上 &lt;code&gt;unicode&lt;/code&gt; 编码的 &lt;code&gt;16&lt;/code&gt; 进制的表示，比如&lt;code&gt;你&lt;/code&gt; 的 &lt;code&gt;unicode&lt;/code&gt; 编码的 &lt;code&gt;16&lt;/code&gt; 进制的表示是 &lt;code&gt;4F60&lt;/code&gt;，我们可以这样使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;h4::after {
  content: &apos;\4F60&apos;;
  font-size: 20px;
  color: red;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTML 中的使用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HTML&lt;/code&gt; 中我们经常使用的 &lt;code&gt;HTML entity&lt;/code&gt; 实体 比如 &lt;code&gt;&amp;#x26;&lt;/code&gt;，&lt;code&gt;unicode&lt;/code&gt; 的使用方法也与这个相同就是在 &lt;code&gt;&amp;#x26;&lt;/code&gt; 后加上 &lt;code&gt;#&lt;/code&gt; 和 &lt;code&gt;unicode&lt;/code&gt; 对应的十进制表示或者是 &lt;code&gt;&amp;#x26;#x&lt;/code&gt;加上十六进制码点值表示，最后要接分号。比如 &lt;code&gt;𝌆&lt;/code&gt; 就用如下方法表示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;h4&gt;𝌆 𝌆  &amp;#x3C;/h4&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JavaScript 中的使用&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;只使用到 &lt;code&gt;Unicode&lt;/code&gt; 基本平面时（&lt;code&gt;\u&amp;#x3C;码点&gt;&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;有使用到 &lt;code&gt;Unicode&lt;/code&gt; 辅助平面时（&lt;code&gt;\u{ &amp;#x3C;码点&gt; }&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;只使用到 &lt;code&gt;ASCII&lt;/code&gt; 字符时（&lt;code&gt;\x&amp;#x3C;码点&gt;&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 使用 2 位的十六进制
console.log(&apos;\u{41}\u{42}\u{43}&apos;) // &apos;ABC&apos;

// 使用 4 位的十六进制
console.log(&apos;\u{0041}\u{0042}\u{0043}&apos;) // ABC

// 使用超过 4 位以上的十六进制
console.log(&apos;\u{1F4A9}&apos;) // &apos;💩&apos; U+1F4A9
console.log(&apos;\u{1F923}&apos;) // &apos;🤣&apos; U+1F923
console.log(&apos;\u{1F436}&apos;) // &apos;🐶&apos; U+1F436
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 &lt;code&gt;JavaScript&lt;/code&gt; 中使用哪种编码有几个不同的方面。&lt;/p&gt;
&lt;h3&gt;对 JS 文件解码&lt;/h3&gt;
&lt;p&gt;这个其实跟 &lt;code&gt;“JavaScript”&lt;/code&gt; 没什么关系，只是文件传输过来用什么编码来解码文件，类似于上面的乱码章节，编码和解码必须要统一，所以必须要有标记告诉解码方，我是用的哪种模式编码的。大致的优先级规则如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果文件有 &lt;code&gt;BOM&lt;/code&gt; 标记，则会使用对应的 &lt;code&gt;Unicode&lt;/code&gt; 编码，比如&lt;code&gt;FFFE&lt;/code&gt;、&lt;code&gt;FEFF&lt;/code&gt; 就会使用 &lt;code&gt;UTF-16&lt;/code&gt;，详见&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E7%B5%84%E9%A0%86%E5%BA%8F%E8%A8%98%E8%99%9F&quot; title=&quot;字节顺序标记&quot;&gt;字节顺序标记&lt;/a&gt;；&lt;/li&gt;
&lt;li&gt;由 &lt;code&gt;HTTP(S)&lt;/code&gt; 请求的相应头来决定，比如：&lt;code&gt;Content-Type: application/javascript; charset=utf-8&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;由 &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; 标签的 &lt;code&gt;charset&lt;/code&gt; 属性决定，比如：&lt;code&gt;&amp;#x3C;script charset=&quot;utf-8&quot; src=&quot;./main.js&quot;&gt;&amp;#x3C;/script&gt;&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;由 &lt;code&gt;html&lt;/code&gt; 本身的 &lt;code&gt;charset&lt;/code&gt; 决定，比如：；&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;引擎解析执行源码时&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;ECMAScript source text is represented as a sequence of characters in the Unicode character encoding, version 3.0 or later. ……ECMAScript source text is assumed to be a sequence of 16-bit code units for the purposes of this specification. Such a source text may include sequences of 16-bit code units that are not valid UTF-16 character encodings. If an actual source text is encoded in a form other than 16-bit code units it must be processed as if it was first converted to UTF-16. 上面是 &lt;code&gt;ECMAScript&lt;/code&gt; 标准对编码的一些说明，可以看到 &lt;code&gt;Javascipt&lt;/code&gt; 源码支持 &lt;code&gt;UTF-16&lt;/code&gt; 编码。更实用点的理解是：&lt;code&gt;Javascript&lt;/code&gt; 引擎总会尝试把源码转成 &lt;code&gt;UTF-16&lt;/code&gt; 编码的文本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于标识符，正则表达式，字符串字面量有如下一些规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字符串字面量、正则表达式对象字面量、变量名支持使用\u加四位十六进制数值来表示 &lt;code&gt;UTF-16&lt;/code&gt; 编码的字符，比如：&lt;code&gt;\u0061&lt;/code&gt;表示英文字母小写 &lt;code&gt;a&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;在注释中会忽略转义序列；&lt;/li&gt;
&lt;li&gt;字符串字面量、正则表达式对象字面量中一个转义序列对应一个字符；&lt;/li&gt;
&lt;li&gt;变量名中一个转义序列对应一个字符；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于 &lt;code&gt;JavaScript&lt;/code&gt; 的设计完成是在 &lt;code&gt;1995&lt;/code&gt; 年，当时还没有 &lt;code&gt;UTF-16&lt;/code&gt; 的标准，也没有辅助平面的概念，所以设计之初就没有考虑到代理码这种模式。这就导致了在 &lt;code&gt;ES6&lt;/code&gt; 标准之前的 &lt;code&gt;JS&lt;/code&gt; 在处理辅助平面上的 &lt;code&gt;UTF-16&lt;/code&gt; 编码会出问题，因为 &lt;code&gt;UTF-16&lt;/code&gt; 的代理码都是四字节的，&lt;code&gt;JS&lt;/code&gt; 会当做两个字符来处理。比如上面也有提到的 &lt;code&gt;𝌆&lt;/code&gt; 字符，他的码点是 &lt;code&gt;0x1D306&lt;/code&gt;，&lt;code&gt;UTF-8&lt;/code&gt; 编码是 &lt;code&gt;0xF0 0x9D 0x8C 0x86&lt;/code&gt;，&lt;code&gt;UTF-16&lt;/code&gt; 编码是 &lt;code&gt;0xD834 0xDF06&lt;/code&gt;，用 &lt;code&gt;JavaScript&lt;/code&gt; 来处理就会出现如下问题。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&apos;𝌆&apos;.length
//2
&apos;𝌆&apos; === &apos;\u1D306&apos;
//false
&apos;𝌆&apos;.charCodeAt(0)
//55348 0xD834
&apos;𝌆&apos;.charCodeAt(1)
//57094 0xDF06
&apos;𝌆&apos;.codePointAt(0)
//119558 0x1D306
&apos;𝌆&apos;.codePointAt(1)
//57094
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出字符的长度被解析为 &lt;code&gt;2&lt;/code&gt;，因为 &lt;code&gt;JS&lt;/code&gt; 无法正确识别代理码，这种现象会出现在几乎所有 &lt;code&gt;JS&lt;/code&gt; 的字符处理函数中。不过 &lt;code&gt;String.prototype.codePointAt()&lt;/code&gt; 函数还是能正确返回码点值的，当然我们除了用 &lt;code&gt;/u{码点}&lt;/code&gt; 的方式可以正确解码以外，还可以将 &lt;code&gt;UTF-16&lt;/code&gt; 代理码写在同一个字符串里也是可以解析的，比如 &lt;code&gt;\uD834\uDF06&lt;/code&gt; 来表示 &lt;code&gt;𝌆&lt;/code&gt;，&lt;code&gt;&quot;𝌆&quot; === &quot;\uD834\uDF06&quot;&lt;/code&gt; 的结果为 &lt;code&gt;true&lt;/code&gt;。所以即使我们不能使用 &lt;code&gt;ES6&lt;/code&gt; 的方法也是能解析代理码的 &lt;code&gt;UTF-16&lt;/code&gt; 字符的，方法就是用 &lt;code&gt;String.prototype.charCodeAt()&lt;/code&gt; 对字符的编码进行检测，如果发现有编码位于高代理区 &lt;code&gt;D800 ~ DBFF&lt;/code&gt; 的字符就手动将他和后面一个字符的编码连结起来解析。&lt;/p&gt;
&lt;p&gt;这里说一下几个关于编码的 &lt;code&gt;JS&lt;/code&gt; 函数。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;String.prototype.codePointAt()&lt;/code&gt; 返回一个 &lt;code&gt;Unicode&lt;/code&gt; 编码点值的非负整数。如果在指定的位置没有元素则返回 &lt;code&gt;undefined&lt;/code&gt; 。如果在索引处开始没有 &lt;code&gt;UTF-16&lt;/code&gt; 代理对，将直接返回在那个索引处的编码单元。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String.fromCodePoint()&lt;/code&gt; 返回指定码点序列的创建字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String.prototype.charCodeAt()&lt;/code&gt; 方法返回 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;65535&lt;/code&gt; 之间的整数，表示给定索引处的 &lt;code&gt;UTF-16&lt;/code&gt; 代码单元 (在 &lt;code&gt;Unicode&lt;/code&gt; 编码单元表示一个单一的 &lt;code&gt;UTF-16&lt;/code&gt; 编码单元的情况下，&lt;code&gt;UTF-16&lt;/code&gt; 编码单元匹配 &lt;code&gt;Unicode&lt;/code&gt; 编码单元。但在——例如 &lt;code&gt;Unicode&lt;/code&gt; 编码单元 &gt; &lt;code&gt;0x10000&lt;/code&gt; 的这种——不能被一个 &lt;code&gt;UTF-16&lt;/code&gt; 编码单元单独表示的情况下，只能匹配 &lt;code&gt;Unicode&lt;/code&gt; 代理对的第一个编码单元)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String.fromCharCode()&lt;/code&gt; 方法返回由指定的UTF-16代码单元序列创建的字符串，参数范围介于 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;65535&lt;/code&gt;（&lt;code&gt;0xFFFF&lt;/code&gt;）之间。 大于 &lt;code&gt;0xFFFF&lt;/code&gt; 的数字将被截断。 不进行有效性检查。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String.prototype.charAt()&lt;/code&gt; 方法根据索引从一个字符串中返回指定的字符。参数为一个介于 &lt;code&gt;0&lt;/code&gt; 和字符串长度减 &lt;code&gt;1&lt;/code&gt;之间的整数。如果没有提供索引，参数默认为 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;ECMAScript 6&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 基本解决了代理码无法识别的问题。 &lt;code&gt;ES6&lt;/code&gt; 可以自动识别4字节的码点。因此，遍历字符串就简单多了。也有方法可以正确返回代理码表示的字符的长度。上面的 &lt;code&gt;JS&lt;/code&gt; 码点表示法用大括号表示辅助平面也是 &lt;code&gt;ES6&lt;/code&gt; 的新特性。&lt;code&gt;String.prototype.codePointAt()&lt;/code&gt; 和 &lt;code&gt;String.fromCodePoint()&lt;/code&gt; 也都是 &lt;code&gt;ES6&lt;/code&gt; 中新的方法。也对正则表达式添加了四字节码点的支持。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;for (let s of string) {
  // ...
}

Array.from(&apos;𝌆&apos;).length //1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//String.prototype.codePointAt()
&apos;ABC&apos;.codePointAt(1);          // 66
&apos;\uD800\uDC00&apos;.codePointAt(0); // 65536
&apos;XYZ&apos;.codePointAt(42); // undefined

//String.fromCodePoint()
console.log(String.fromCodePoint(9731, 9733, 9842, 0x2F804));
// expected output: &quot;☃★♲你&quot;

//String.prototype.charCodeAt()
const sentence = &apos;The quick brown fox jumps over the lazy dog.&apos;;
const index = 4;
console.log(`The character code ${sentence.charCodeAt(index)} is equal to ${sentence.charAt(index)}`);
//  &quot;The character code 113 is equal to q&quot;

//String.fromCharCode()
console.log(String.fromCharCode(189, 43, 190, 61));
// expected output: &quot;½+¾=&quot;

//String.prototype.charAt()
var anyString = &quot;Brave new world&quot;;
console.log(&quot;The character at index 0   is &apos;&quot; + anyString.charAt(0)   + &quot;&apos;&quot;);
console.log(&quot;The character at index 999 is &apos;&quot; + anyString.charAt(999) + &quot;&apos;&quot;);
//The character at index 0 is &apos;B&apos;
//The character at index 999 is &apos;&apos;

/^.$/u.test(&apos;𝌆&apos;)
//true
/^.$/.test(&apos;𝌆&apos;)
//false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外一个特别的地方就是有些字符除了字母以外，还有附加符号。比如，汉语拼音的 &lt;code&gt;Ǒ&lt;/code&gt;，字母上面的声调就是附加符号。对于许多欧洲语言来说，声调符号是非常重要的。&lt;code&gt;Unicode&lt;/code&gt; 提供了两种表示方法。一种是带附加符号的单个字符，即一个码点表示一个字符，比如 &lt;code&gt;Ǒ&lt;/code&gt; 的码点是 &lt;code&gt;U+01D1&lt;/code&gt;；另一种是将附加符号单独作为一个码点，与主体字符复合显示，即两个码点表示一个字符，比如 &lt;code&gt;Ǒ&lt;/code&gt; 可以写成 &lt;code&gt;O（U+004F） + ˇ（U+030C）&lt;/code&gt;。这两种表示方法是等价的，但是在 &lt;code&gt;JS&lt;/code&gt; 中他们严格相等判断返回的是 &lt;code&gt;false&lt;/code&gt;。在 &lt;code&gt;ES6&lt;/code&gt; 中提供了一个 &lt;code&gt;normalize&lt;/code&gt; 方法对那些等价的 &lt;code&gt;Unicode&lt;/code&gt; 序列进行判断。关于 &lt;code&gt;Unicode&lt;/code&gt; 的等价性可以参考&lt;a href=&quot;https://zh.wikipedia.org/wiki/Unicode%E7%AD%89%E5%83%B9%E6%80%A7&quot; title=&quot;Unicode等价性&quot;&gt;Unicode等价性&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 方法一
&apos;\u01D1&apos;
// &apos;Ǒ&apos;

// 方法二
&apos;\u004F\u030C&apos;
// &apos;Ǒ&apos;

&apos;\u01D1&apos; === &apos;\u004F\u030C&apos;
//false

&apos;\u01D1&apos;.normalize() === &apos;\u004F\u030C&apos;.normalize()
// true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;综合上面的内容，现在的 &lt;code&gt;ES6&lt;/code&gt; 标准下公有 &lt;code&gt;6&lt;/code&gt; 种用字符串字面量表示同一个字符的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&apos;\z&apos; === &apos;z&apos; // true //一些特殊意义的字符不能这么表示，见下方正则表达式部分
&apos;\172&apos; === &apos;z&apos; // true //8进制
&apos;\x7A&apos; === &apos;z&apos; // true //16进制，代理码需要高低代理码合并解析
&apos;\u007A&apos; === &apos;z&apos; // true //码点
&apos;\u{7A}&apos; === &apos;z&apos; // true //码点
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;将 String 转换为 UTF-8 编码&lt;/h3&gt;
&lt;p&gt;这里写了一个小函数，实现将 &lt;code&gt;String&lt;/code&gt; 转换为其对应的 &lt;code&gt;UTF-8&lt;/code&gt; 编码，加深自己的理解。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function utf8_encoding1(str) {
  const code = encodeURIComponent(str)
  const byte = []
  for (let i = 0; i &amp;#x3C; code.length; i++) {
    const c = code.charAt(i)
    if (c === &apos;%&apos;) {
      const hex = code.charAt(i + 1) + code.charAt(i + 2)
      const hexVal = parseInt(hex, 16)
      byte.push(hexVal)
      i += 2
    } else byte.push(c.charCodeAt(0))
  }
  return &apos;0x&apos; + byte.map((c) =&gt; c.toString(16)).join(&apos;&apos;)
}
console.log(utf8_encoding1(&apos;𝌆&apos;)) //0xf09d8c86

function utf8_encoding2(str) {
  const length = str.length
  let arr = []
  for (let i = 0; i &amp;#x3C; length; i++) {
    let cp = str.codePointAt(i)
    let result = &apos;0x&apos;
    if (cp &amp;#x3C;= 0x7f) {
      result += cp &amp;#x26; 0x7f
    } else if (cp &gt;= 0x80 &amp;#x26;&amp;#x26; cp &amp;#x3C;= 0x7ff) {
      result += (((cp &gt;&gt; 6) &amp;#x26; 0x1f) | 0xc0).toString(16)
      result += ((cp &amp;#x26; 0x3f) | 0x80).toString(16)
    } else if (cp &gt;= 0x800 &amp;#x26;&amp;#x26; cp &amp;#x3C;= 0xffff) {
      result += (((cp &gt;&gt; 12) &amp;#x26; 0xf) | 0xe0).toString(16)
      result += (((cp &gt;&gt; 6) &amp;#x26; 0x3f) | 0x80).toString(16)
      result += ((cp &amp;#x26; 0x3f) | 0x80).toString(16)
    } else if (cp &gt;= 0x10000 &amp;#x26;&amp;#x26; cp &amp;#x3C;= 0x10ffff) {
      result += (((cp &gt;&gt; 18) &amp;#x26; 0x7) | 0xf0).toString(16)
      result += (((cp &gt;&gt; 12) &amp;#x26; 0x3f) | 0x80).toString(16)
      result += (((cp &gt;&gt; 6) &amp;#x26; 0x3f) | 0x80).toString(16)
      result += ((cp &amp;#x26; 0x3f) | 0x80).toString(16)
    }
    arr.push(result)
  }
  return arr
}

console.log(utf8_encoding2(&apos;𝌆&apos;)) //[ &apos;0xf09d8c86&apos;, &apos;0xedbc86&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个方法是用 &lt;code&gt;encodeURIComponent&lt;/code&gt;，最开始我还觉得这个方法行不通，因为 &lt;code&gt;encodeURIComponent&lt;/code&gt; 有些字符是不转义的，但后来发现不转义的都是 &lt;code&gt;ASCII&lt;/code&gt; 码范围内的字符，也非常好处理。第二个方法则是比较容易理解的用 &lt;code&gt;codePointAt()&lt;/code&gt; 方法获取码点然后用位运算转码，需要注意的就是码点超过 &lt;code&gt;0xFFFF&lt;/code&gt; 的字符可能长度是 &lt;code&gt;2&lt;/code&gt;， 返回两个码点。&lt;/p&gt;
&lt;h2&gt;字符串的正则表达式&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;winter&lt;/code&gt; 给出的 &lt;code&gt;ES5&lt;/code&gt; 中的字符串的正则表达式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const str_reg_sq =
  /&quot;(?:[^&quot;\n\\\r\u2028\u2029]|\\(?:[&apos;&quot;\\bfnrtv\n\r\u2028\u2029]|\r\n)|\\x[0-9a-fA-F]{2}|\\u[0-9a-fA-F]{4}\\[^0-9ux&apos;&quot;\\bfnrtv\n\\\r\u2028\u2029])*&quot;/

const str_reg_dq =
  /&apos;(?:[^&apos;\n\\\r\u2028\u2029]|\\(?:[&apos;&quot;\\bfnrtv\n\r\u2028\u2029]|\r\n)|\\x[0-9a-fA-F]{2}|\\u[0-9a-fA-F]{4}\\[^0-9ux&apos;&quot;\\bfnrtv\n\\\r\u2028\u2029])*&apos;/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个特殊的字符都是 &lt;code&gt;ASCII&lt;/code&gt; 中的早期计算机中就有存在的，正则表达式中也有对应的元字符。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;\b&lt;/code&gt; 通常是单词分界位置，但如果在字符类里使用代表退格&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\t&lt;/code&gt; 制表符，Tab&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\r&lt;/code&gt; 回车&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\v&lt;/code&gt; 竖向制表符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\f&lt;/code&gt; 换页符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\n&lt;/code&gt; 换行符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\u2028&lt;/code&gt; 行分隔符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\u2029&lt;/code&gt; 段落分隔符&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;JavaScript字符的转义&lt;/h2&gt;
&lt;p&gt;除了普通的可打印字符以外，一些有特殊功能的字符可以通过转义字符的形式放入字符串中：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;\0&lt;/code&gt;：空字符 &lt;code&gt;\&apos;&lt;/code&gt;：单引号 &lt;code&gt;\&quot;&lt;/code&gt;：双引号 &lt;code&gt;\\&lt;/code&gt;：反斜杠 &lt;code&gt;\n&lt;/code&gt;：换行 &lt;code&gt;\r&lt;/code&gt;：回车 &lt;code&gt;\v&lt;/code&gt;：垂直制表符 &lt;code&gt;\t&lt;/code&gt;：水平制表符 &lt;code&gt;\b&lt;/code&gt;：退格 &lt;code&gt;\f&lt;/code&gt;：换页 &lt;code&gt;\uXXXX&lt;/code&gt;：&lt;code&gt;unicode&lt;/code&gt; 码（基础平面的码点） &lt;code&gt;\u{X} ... \u{XXXXXX}&lt;/code&gt;：&lt;code&gt;unicode codepoint&lt;/code&gt;（包括辅助平面的码点） &lt;code&gt;\xXX&lt;/code&gt;：&lt;code&gt;Latin-1&lt;/code&gt; 字符(&lt;code&gt;x&lt;/code&gt;小写) （扩展 &lt;code&gt;ASCII&lt;/code&gt; 码对应的码点，&lt;code&gt;2&lt;/code&gt; 个 &lt;code&gt;16&lt;/code&gt; 进制数，共 &lt;code&gt;256&lt;/code&gt; 个）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;和其他语言不同，&lt;code&gt;javascript&lt;/code&gt; 的字符串不区分单引号和双引号，所以不论是单引号还是双引号的字符串，上面的转义字符都能运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;USVString，DOMString，CSSOMString&lt;/h2&gt;
&lt;p&gt;在看 &lt;code&gt;MDN&lt;/code&gt; 文档的时候我们经常会看到 &lt;code&gt;USVString&lt;/code&gt;，&lt;code&gt;DOMString&lt;/code&gt;，&lt;code&gt;CSSOMString&lt;/code&gt; 和 &lt;code&gt;JavaScript Binary String&lt;/code&gt; 等概念。这里补充说明一下这些概念的区别。&lt;/p&gt;
&lt;h2&gt;USVString&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;USVString&lt;/code&gt; 指的是 &lt;a href=&quot;https://www.unicode.org/glossary/#unicode_scalar_value&quot; title=&quot;Unicode Scalar Value&quot;&gt;Unicode Scalar Value&lt;/a&gt;，&lt;code&gt;Unicode&lt;/code&gt; 标量值序列，即除了 &lt;code&gt;high-surrogate&lt;/code&gt; 和 &lt;code&gt;low-surrogate&lt;/code&gt; 的 &lt;code&gt;code points&lt;/code&gt;。即从 &lt;code&gt;U+0000 - U+D7FF&lt;/code&gt; 和 &lt;code&gt;U+E000 - U+10FFFF&lt;/code&gt; 的 &lt;code&gt;Unicode&lt;/code&gt; 码点。&lt;strong&gt;这里的一个理解关键就是不要理解为 &lt;code&gt;UTF-16&lt;/code&gt;，这里指的是 &lt;code&gt;Unicode&lt;/code&gt; 码点。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以一个 &lt;code&gt;USVString&lt;/code&gt; 中全是原始 &lt;code&gt;Unicode&lt;/code&gt; 码点，不包括给 &lt;code&gt;UTF-16&lt;/code&gt; 用的代理区。一般 &lt;code&gt;USVString&lt;/code&gt; 一般用在执行文本处理的 &lt;code&gt;API&lt;/code&gt; 中。因为 &lt;code&gt;Unicode&lt;/code&gt; 是统一的，在任何平台都能识别，所以使用文本处理的 &lt;code&gt;API&lt;/code&gt; 使用 &lt;code&gt;Unicode&lt;/code&gt; 可以确保兼容性。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;JavaScript&lt;/code&gt; 中使用 &lt;code&gt;USVString&lt;/code&gt; 的时候，它会被映射到用 &lt;code&gt;UTF-16&lt;/code&gt; 编码的 &lt;code&gt;JavaScript&lt;/code&gt; 原始 &lt;code&gt;String&lt;/code&gt; 类型。也就是说 &lt;code&gt;USVString&lt;/code&gt; 会被编码成 &lt;code&gt;UTF-16&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果我们在接受 &lt;code&gt;USVString&lt;/code&gt; 作为参数的 &lt;code&gt;API&lt;/code&gt; 中使用了代理区的编码，这个代理区编码会被替换成 &lt;code&gt;repalcement character&lt;/code&gt; 也就是 &lt;code&gt;U+FFFD&lt;/code&gt;，对应的字符是 &lt;code&gt;�&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里有一点容易误解的就是我们不要用 &lt;code&gt;UTF-16&lt;/code&gt; 的思维来理解 &lt;code&gt;USVString&lt;/code&gt;，虽然我们前面说过 &lt;code&gt;JavaScript&lt;/code&gt; 引擎总是尝试把源码转成 &lt;code&gt;UTF-16&lt;/code&gt; 编码，但是在处理 &lt;code&gt;USVString&lt;/code&gt; 时则不能这么理解。比如 &lt;code&gt;WebSocket.prototype.send&lt;/code&gt; 接受的字符串就是 &lt;code&gt;USVString&lt;/code&gt;，如果我们传入一个代理平面的 &lt;code&gt;𝌆&lt;/code&gt;，应该处理成 &lt;code&gt;U+1D306&lt;/code&gt; 的 &lt;code&gt;Unicode code point&lt;/code&gt; 而不是 &lt;code&gt;UTF-16&lt;/code&gt; 编码。至于引擎之后如何映射处理我们则不用考虑，在 &lt;code&gt;WebSocket&lt;/code&gt; 这里应该是先映射到 &lt;code&gt;UTF-16&lt;/code&gt;，然后在转 &lt;code&gt;UTF-8&lt;/code&gt; 传递。&lt;/p&gt;
&lt;p&gt;如果我们使用单个的代理平面的码点，或者错误顺序的代理平面码点都会被替换成 &lt;code&gt;�&lt;/code&gt;，比如 &lt;code&gt;ws.send(&apos;\uDC00\uD800&apos;)&lt;/code&gt; 我们会在后端接收到 &lt;code&gt;��&lt;/code&gt;，&lt;code&gt;ws.send(&apos;\uDC00&apos;)&lt;/code&gt; 或者 &lt;code&gt;ws.send(&apos;\uD800&apos;)&lt;/code&gt; 则都会收到 &lt;code&gt;�&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;DOMString&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;DOMString&lt;/code&gt; 是 &lt;code&gt;16&lt;/code&gt; 位无符号整数序列，通常被解释为 &lt;code&gt;UTF-16&lt;/code&gt; 编码单元。这就是 &lt;code&gt;JavaScript&lt;/code&gt; 中最常用的 &lt;code&gt;String&lt;/code&gt; 类型。有些 &lt;code&gt;API&lt;/code&gt; 会把 &lt;code&gt;null&lt;/code&gt; 字符串化为空字符串而不是 &lt;code&gt;&apos;null&apos;&lt;/code&gt;。根据我的测试，当我们在 &lt;code&gt;DOMString&lt;/code&gt; 中使用单个代理区的码点的时候，就会直接返回，比如 &lt;code&gt;&apos;\ud800&apos;&lt;/code&gt; 就会直接返回我们输入的这个，也不会报错。&lt;/p&gt;
&lt;h2&gt;CSSOMString&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CSSOMString&lt;/code&gt; 就是在 &lt;code&gt;CSSOM&lt;/code&gt; 规范中出现的字符串类型，根据浏览器不同，代指 &lt;code&gt;DOMString&lt;/code&gt; 或者 &lt;code&gt;USVString&lt;/code&gt;。根据 &lt;code&gt;MDN&lt;/code&gt; 给出的表格，目前所有的浏览器都是 &lt;code&gt;USVString&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里我总结一下，其实我并不清楚目前的浏览器引擎将字符串存储到内存中用的是什么形式，可能是 &lt;code&gt;Unicode&lt;/code&gt;，也可能是 &lt;code&gt;UTF-16&lt;/code&gt;。不过我们认为我们可以简单理解 &lt;code&gt;USVString&lt;/code&gt; 就是 &lt;code&gt;Unicode&lt;/code&gt;，而 &lt;code&gt;DOMString&lt;/code&gt; 就是 &lt;code&gt;UTF-16&lt;/code&gt;。字符串编码本质也就是为了用一套标准解析二进制对应的字符，我们在内存中存的可能是一种形式，引擎处理的时候可能是另一种编码格式，到网络上传播的时候可能又是另一种格式，重要的就是我们知道我们当前处理的是什么编码格式。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;任何技术的产生都是有历史原因的，也都是为了解决问题的。所以我们学习知识要带着问题去学，知道这个技术是为了解决什么问题而产生的，自然能把知识形成体系，而不容易遗忘，并且运用到合适的地方。如果不知道问题只是背了个答案，那么可能你并没有真的“学会”这个知识。&lt;/p&gt;
&lt;p&gt;如果你也和我一样对字符是如何从键盘敲击到渲染到屏幕上的过程很感兴趣，你可以看看知乎答主乌鸦给出的答案：&lt;a href=&quot;https://www.zhihu.com/question/24340504/answer/28902204&quot; title=&quot;计算机系统是如何显示一个字符的？&quot;&gt;计算机系统是如何显示一个字符的？&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://unicode.org/&quot; title=&quot;Unicode Consortium&quot;&gt;The Unicode Consortium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flaviocopes.com/javascript-unicode/&quot; title=&quot;Unicode in JavaScript&quot;&gt;Unicode in JavaScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/31833164/answer/381137073&quot; title=&quot;Python 编码为什么那么蛋疼？&quot;&gt;Python 编码为什么那么蛋疼？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/SamHwang1990/blog/issues/2&quot; title=&quot;Javascript 与字符编码&quot;&gt;Javascript 与字符编码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2014/12/unicode.html&quot; title=&quot;Unicode与javascript详解&quot;&gt;Unicode与javascript详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pjchender.blogspot.com/2018/06/guide-unicode-javascript.html&quot; title=&quot;Unicode在javascript中的使用&quot;&gt;Unicode在javascript中的使用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://my.oschina.net/goldenshaw/blog/310331&quot; title=&quot;字符集与编码&quot;&gt;字符集与编码&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>浏览器渲染过程</title><link>https://clloz.com/blog/how-browser-work-2</link><guid isPermaLink="true">https://clloz.com/blog/how-browser-work-2</guid><pubDate>Thu, 25 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;本文由于作者水平有限，肯定有错误之处，如果你看到，希望能够指出，感谢。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;相信大家都听过一道经典的面试题：“在浏览器输入 &lt;code&gt;URL&lt;/code&gt; 后回车之后发生了什么”，我一直想解答这个问题，不过这个题目涉及的知识面非常广，想要解答需要一定的知识储备。这篇文章我们讨论这个问题中的一部分，当浏览器拿到服务器传回的 &lt;code&gt;html&lt;/code&gt; 文档后如何处理文档然后呈现在显示器上呢？&lt;/p&gt;
&lt;p&gt;本文主要讲的是浏览器端如何解析渲染文档，如果想看网络方面的内容可以参考我的另外两篇文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/front-end/2018/12/05/url/&quot; title=&quot;从URL输入到页面展现&quot;&gt;从URL输入到页面展现&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/network/2019/05/02/http/&quot; title=&quot;前端网络基础和HTTP&quot;&gt;前端网络基础和HTTP&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在进入正题之前我们来想几个问题，然后在跟着问题的脚步来分析： 1. 我们都知道 &lt;code&gt;DOM&lt;/code&gt;，&lt;code&gt;CSS&lt;/code&gt; 的加载和渲染和 &lt;code&gt;JS&lt;/code&gt; 的执行之间存在阻塞，阻塞发生的时候浏览器会加载其他页面资源吗 2. 都说 &lt;code&gt;JS&lt;/code&gt; 是单线程的，那么 &lt;code&gt;Event Loop&lt;/code&gt; 线程是什么呢，为什么 &lt;code&gt;JS&lt;/code&gt; 要设计成单线程呢 3. &lt;code&gt;setTimeout&lt;/code&gt; 是如何执行的，为什么 &lt;code&gt;setTimeout&lt;/code&gt; 的 &lt;code&gt;delay&lt;/code&gt; 和执行时间不同 4. 浏览器的具体渲染流程是什么样的&lt;/p&gt;
&lt;p&gt;希望我写完这篇文章和你看完这篇文章之后都能解答这几个问题。&lt;/p&gt;
&lt;h2&gt;浏览器&lt;/h2&gt;
&lt;p&gt;在解决问题之前，我们要先了解我们的对象：浏览器。浏览器的功能很简单，就是根据我们给出的 &lt;code&gt;URI&lt;/code&gt;，替我们向服务器发出请求，获取服务器上的资源并在展示给我们。这里的资源主要是 &lt;code&gt;HTML&lt;/code&gt; 文档也可以是图片，&lt;code&gt;pdf&lt;/code&gt; 或其他类型的文件，取决于我们给出的 &lt;code&gt;URI&lt;/code&gt; 采取的协议以及请求的文件类型。&lt;/p&gt;
&lt;h2&gt;浏览器的主要组件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用户界面（ &lt;code&gt;The userinterface&lt;/code&gt; ）：包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外，其他显示的各个部分都属于用户界面。&lt;/li&gt;
&lt;li&gt;浏览器引擎（ &lt;code&gt;The browser engine&lt;/code&gt; ）：用户页面和渲染引擎的中间层，负责在用户界面和渲染引擎之间传递信息。&lt;/li&gt;
&lt;li&gt;渲染引擎（ &lt;code&gt;The rendering engine&lt;/code&gt; ）：负责请求并渲染显示内容，这是最核心的一个部分，也就是我们常说的浏览器内核。比如请求的是 &lt;code&gt;HTML&lt;/code&gt; 文档，那么渲染引擎负责解析 &lt;code&gt;HTML&lt;/code&gt; 和 &lt;code&gt;CSS&lt;/code&gt; ，并将解析的结果显示到显示器上。&lt;/li&gt;
&lt;li&gt;网络组件：用于网络调用，比如 &lt;code&gt;http&lt;/code&gt; 请求，是渲染引擎的一部分&lt;/li&gt;
&lt;li&gt;用户界面后端（ &lt;code&gt;UI backend&lt;/code&gt; ）：用于绘制一些小部件，是渲染引擎的一部分。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JavaScript&lt;/code&gt; 解释器（ &lt;code&gt;JavaScript Interpreter&lt;/code&gt; ）：解析并执行 &lt;code&gt;JavaScript&lt;/code&gt; 代码，是渲染引擎的一部分&lt;/li&gt;
&lt;li&gt;数据存储（ &lt;code&gt;Data Storage&lt;/code&gt; ）：浏览器需要保存各种类型的数据到本地，比如 &lt;code&gt;cookies&lt;/code&gt;， &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;IndexedDB&lt;/code&gt;, &lt;code&gt;WebSQL and FileSystem&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./images/browser-structure%20.png&quot; alt=&quot;browser-structure&quot; title=&quot;browser-structure&quot;&gt;&lt;/p&gt;
&lt;p&gt;这些组件之间是如何配合工作的呢，我们都知道 &lt;code&gt;chrome&lt;/code&gt; 是内存杀手，那么我们打开的多个 &lt;code&gt;tab&lt;/code&gt; 又是如何分配资源的，不同的 &lt;code&gt;tab&lt;/code&gt; 之间会争夺资源吗？&lt;/p&gt;
&lt;h2&gt;进程与线程&lt;/h2&gt;
&lt;p&gt;很多前端开发人员都知道 &lt;code&gt;JavaScript&lt;/code&gt; 是单线程语言，但具体是什么单线程呢，其实说的是 &lt;code&gt;JS&lt;/code&gt; 引擎，也就是上面的 &lt;code&gt;JavaScript&lt;/code&gt; 解释器是单线程的，那么这个单线程到底是什么意思呢，我们要来说说线程和进程的概念和关系，以及浏览器中的进程与线程。&lt;/p&gt;
&lt;p&gt;进程 &lt;code&gt;process&lt;/code&gt; 和线程 &lt;code&gt;thread&lt;/code&gt; 在大学的操作系统课程上讲的很多，不过可能很多同学忘记了，那么我们来说说进程和线程的概念。&lt;/p&gt;
&lt;p&gt;我们的计算机中的所有计算都是在 &lt;code&gt;cpu&lt;/code&gt; 中完成的，&lt;code&gt;cpu&lt;/code&gt; 的计算速度非常快，大部分的设备是完全跟不上 &lt;code&gt;cpu&lt;/code&gt;的速度的，那么怎么办呢。用金字塔式存储体系来根据信息处理的紧急程度分开存储，它们一般从下到上越来越小，越来越快，越来越贵。如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/Memory.BLqXJ_Bm_1s1Dz.webp&quot; alt=&quot;memeory&quot; title=&quot;memeory&quot;&gt;&lt;/p&gt;
&lt;p&gt;大部分人都知道计算机有硬盘和内存，而我们的软件一般就安装在硬盘中，在硬盘中的数据是我们最不急需处理的，它们就静静地呆在那里。当我们想要运行一个程序，这些文件就被装载到内存中去了，而对于内存中最急需处理的文件会被传输到 &lt;code&gt;CPU&lt;/code&gt; 的高速缓存中，也就是我们买 &lt;code&gt;CPU&lt;/code&gt; 的时候会看到三级，二级，一级缓存，而在这些缓存中的文件最后会被依次传到 &lt;code&gt;CPU&lt;/code&gt; 的寄存器中让 &lt;code&gt;CPU&lt;/code&gt; 执行，只有寄存器的速度勉强能跟上 &lt;code&gt;CPU&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但是我们可以同时开很多软件，同时做很多工作，比如我可以一边听着音乐，一边修改着 &lt;code&gt;Word&lt;/code&gt;，同时还在后台开着浏览器后台播放着直播，不仅如此，我们的操作系统还有很多程序需要运行。那么它们是按照什么顺序进 &lt;code&gt;CPU&lt;/code&gt; 运行的呢。我们前面已经说过 &lt;code&gt;CPU&lt;/code&gt; 非常快，所以当我们的程序进入 &lt;code&gt;CPU&lt;/code&gt; 运行的时候，其他资源比如显卡都应该就位了，这些资源就构成了程序执行的上下文。这个程序执行上下文就是我们的进程，也就是操作系统分配系统资源的最小单位，说白了进程就是操作系统用来管理程序和计算机资源之间的分配关系的手段。当我们的程序终于等到能够进入CPU运行的机会，先装载执行上下文，然后执行程序，程序执行完成或系统分配的时间结束，就必须保存执行上下文，让排队的下一个进程进入计算。&lt;code&gt;CPU&lt;/code&gt; 就不断重复着装载上下文，执行程序，保存上下文的过程。&lt;/p&gt;
&lt;p&gt;那么线程是什么呢，当我们的程序终于获得 &lt;code&gt;CPU&lt;/code&gt; 的临幸，我们当然希望我们的程序能尽快执行完成。如果我们的程序只有一个逻辑要执行，那么我们其实只需要一个线程就可以了，但如果有几个并行的任务需要执行，我们可以借助多核 &lt;code&gt;CPU&lt;/code&gt; 同时开多个线程，在一个执行上下文中共享系统分配的资源，来协同更快地完成任务。（单核 &lt;code&gt;CPU&lt;/code&gt; 也有多线程，操作系统在不同的线程之间快速切换，在进程间交替运行，减少 &lt;code&gt;CPU&lt;/code&gt; 闲置的时间）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;操作系统对进程的处理和资源的分配会复杂很多，我们只是了解一下大致的概念。阮一峰的博客有一篇更形象一点的比喻，大家可以借助比喻帮助理解：&lt;a href=&quot;https://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html&quot; title=&quot;进程与线程的一个简单解释&quot;&gt;进程与线程的一个简单解释&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;浏览器的进程&lt;/h2&gt;
&lt;p&gt;现在大家应该已经对进程和线程有一定的了解了，那么我们来说一说浏览器的进程和线程，首先我们要说，浏览器程序是多进程的，我们的每一个标签页（ &lt;code&gt;tab&lt;/code&gt; ）都是一个独立的进程。我们可以打开 &lt;code&gt;Chrome&lt;/code&gt; 的 &lt;code&gt;more tools&lt;/code&gt; 中的 &lt;code&gt;task manager&lt;/code&gt;，就可以看到当前浏览器的进程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/chrome-process.CWLqKlAF_1uiJ5W.webp&quot; alt=&quot;chrome-process&quot; title=&quot;chrome-process&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图是我只打开了一个 &lt;code&gt;tab&lt;/code&gt; 显示的进程，我们可以看到有一个 &lt;code&gt;Browser&lt;/code&gt; 进程，一个 &lt;code&gt;GPU Process&lt;/code&gt; 进程，一个&lt;code&gt;Plugin Broker&lt;/code&gt; 进程，一个 &lt;code&gt;Audio Service&lt;/code&gt; 进程，一个 &lt;code&gt;Network Service&lt;/code&gt; 进程，一个打开的 &lt;code&gt;tab&lt;/code&gt; 对应的渲染进程，一个 &lt;code&gt;Spare Renderer&lt;/code&gt; 进程 和若干个 &lt;code&gt;Extension&lt;/code&gt; 进程。&lt;/p&gt;
&lt;p&gt;我们结合上面浏览器的主要组件来看（以 &lt;code&gt;Chrome&lt;/code&gt; 为例）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Browser&lt;/code&gt; 进程：控制 &lt;code&gt;chrome&lt;/code&gt; 应用界面的一些组件，比如地址栏，书签栏，前进后退按钮等。还控制一些浏览器的不可见部分，比如网络请求和文件访问等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Renderer&lt;/code&gt; 进程（浏览器内核）：控制标签页内部网页要显示的一切，也就是我们访问的内容都是有渲染引擎控制，比如页面渲染，脚本执行，事件的处理。在 &lt;code&gt;chrome&lt;/code&gt;，每一个 &lt;code&gt;tab&lt;/code&gt; 都是一个独立的渲染进程，渲染引擎在其中工作。它会计算出我们的页面最后需要绘制成什么样子，然后交给 &lt;code&gt;GPU&lt;/code&gt; 进程进行绘制。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Plugin&lt;/code&gt; 进程：控制网站应用的插件，比如 &lt;code&gt;flash&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GPU&lt;/code&gt; 进程：独立于其他进程处理 &lt;code&gt;GPU&lt;/code&gt; 任务，它被独立出来是因为 &lt;code&gt;GPU&lt;/code&gt; 处理来自不同程序的请求。渲染引擎最后渲染出的内容要交给 &lt;code&gt;GPU&lt;/code&gt; 进程绘制到显示器上。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/browser-arch.Bbjy6AU2_Z27u0B8.webp&quot; alt=&quot;browserarch&quot; title=&quot;browserarch&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/browserui.CbRebl5C_Z1QVqSo.webp&quot; alt=&quot;browserui&quot; title=&quot;browserui&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;第四点翻译的可能有点问题，原文&lt;a href=&quot;https://developers.google.com/web/updates/2018/09/inside-browser-part1&quot; title=&quot;Inside look at modern web browser&quot;&gt;Inside look at modern web browser&lt;/a&gt;，这个系列文章非常不错，推荐大家看看。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;多进程什么好处呢，最简单的比方，我打开了三个 &lt;code&gt;tab&lt;/code&gt; 浏览三个网页，每个页面都有单独的渲染进程，如果其中一个页面的代码非常糟糕崩溃了，那么你的另外两个页面不会受到影响，你依然可以继续浏览。而如果三个页面共享一个进程，那么你的两外两个页面也将崩溃。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不过正因为如此，&lt;code&gt;chrome&lt;/code&gt; 简直是内存杀手，相信大家都有体会。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;渲染引擎（浏览器内核）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;也有一些说法是把渲染引擎和 &lt;code&gt;JS&lt;/code&gt; 引擎分开，它们共同组成内核，我是以 &lt;a href=&quot;https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/&quot; title=&quot;How Browsers Work&quot;&gt;How Browsers Work&lt;/a&gt; 这篇文章和 &lt;code&gt;Chrome&lt;/code&gt; 的结构为准。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于我们前端来说，最重要的就是渲染引擎以及它工作的渲染进程，这是我们打交道最多的地方，学期其他的浏览器知识能让我们更清楚渲染引擎在浏览器中的定位和工作流程。渲染引擎也就是我们经常说的浏览器内核。目前主要的浏览器内核如下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Trident/EdgeHTML&lt;/code&gt;： &lt;code&gt;Trident&lt;/code&gt; 是微软的IE浏览器使用的渲染引擎，它是从从早期一款商业性的专利网页浏览器 &lt;code&gt;Spyglass Mosaic&lt;/code&gt; 派生出来的。&lt;code&gt;Window 10&lt;/code&gt; 发布之后，微软将其内置的浏览器命名为 &lt;code&gt;Edge&lt;/code&gt;，而 &lt;code&gt;Edge&lt;/code&gt; 浏览器的渲染引擎便是 &lt;code&gt;EdgeHTML&lt;/code&gt;。在 &lt;code&gt;2019&lt;/code&gt; 年，微软宣布 &lt;code&gt;Edge&lt;/code&gt; 浏览器将采用 &lt;code&gt;Chromium&lt;/code&gt; 的内核 &lt;code&gt;Blink&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Webkit&lt;/code&gt;：&lt;code&gt;WebKit&lt;/code&gt; 是苹果公司的 &lt;code&gt;Safari&lt;/code&gt; 浏览器使用的渲染引擎，是 &lt;code&gt;KDE&lt;/code&gt;（&lt;code&gt;Linux&lt;/code&gt; 桌面系统）小组的 &lt;code&gt;KHTML&lt;/code&gt; 引擎的一个开源的分支。早期的 &lt;code&gt;Chrome&lt;/code&gt; 也是使用的 &lt;code&gt;Webkit&lt;/code&gt; 引擎。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Chromium/Blink&lt;/code&gt;：&lt;code&gt;Chromium&lt;/code&gt; 是谷歌公司的一个开源浏览器项目，每隔一段时间，谷歌会在最新的比较稳定的 &lt;code&gt;Chromium&lt;/code&gt; 版本上加入一些其他的功能使之成为新版本的 &lt;code&gt;Chrome&lt;/code&gt; 浏览器。而 &lt;code&gt;Chromium&lt;/code&gt; 的渲染引擎采用的是 &lt;code&gt;Blink&lt;/code&gt;，其前身是 &lt;code&gt;Webkit&lt;/code&gt;。&lt;code&gt;Blink&lt;/code&gt; 是从早期的 &lt;code&gt;Webkit&lt;/code&gt; 项目（而非后来的 &lt;code&gt;Webkit2&lt;/code&gt;）复制出来另外维护的一个新的分支项目。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Presto&lt;/code&gt;：&lt;code&gt;Presto&lt;/code&gt; 是挪威 &lt;code&gt;Opera Software ASA&lt;/code&gt; 公司的 &lt;code&gt;Opera&lt;/code&gt; 浏览器使用的渲染引擎，后来该公司为了减少研发成本，在 &lt;code&gt;2013&lt;/code&gt; 年跟随 &lt;code&gt;Chrome&lt;/code&gt; 浏览器将渲染引擎改为 &lt;code&gt;Blink&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Gecko&lt;/code&gt;：&lt;code&gt;Gecko&lt;/code&gt; 是 &lt;code&gt;Mozilla&lt;/code&gt; 公司的 &lt;code&gt;FireFox&lt;/code&gt; 浏览器使用的渲染引擎，它是一款开源的跨平台渲染引擎，可以在 &lt;code&gt;Windows&lt;/code&gt;、 &lt;code&gt;BSD&lt;/code&gt;、&lt;code&gt;Linux&lt;/code&gt; 和 &lt;code&gt;Mac OS X&lt;/code&gt; 中使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在的浏览器内核可以说是谷歌一家独大，特别是在微软也宣布投向 &lt;code&gt;Chromium&lt;/code&gt;。关于浏览器内核的详细可以参考知乎的这个回答 &lt;a href=&quot;https://www.zhihu.com/question/290767285/answer/1200063036&quot; title=&quot;浏览器内核真的很复杂吗？- 龙泉寺&quot;&gt;浏览器内核真的很复杂吗？- 龙泉寺&lt;/a&gt;，我这里只是做一个简单的介绍。&lt;/p&gt;
&lt;p&gt;渲染进程是多线程的，那到底有哪些线程，分别做什么工作呢？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;GUI&lt;/code&gt; 渲染线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;负责渲染浏览器界面，解析&lt;code&gt;HTML&lt;/code&gt; ，&lt;code&gt;CSS&lt;/code&gt; ，构建 &lt;code&gt;DOM&lt;/code&gt; 树和 &lt;code&gt;RenderObject&lt;/code&gt; 树，布局和绘制等。&lt;/li&gt;
&lt;li&gt;当界面需要重绘（ &lt;code&gt;Repaint&lt;/code&gt; ）或由于某种操作引发回流( &lt;code&gt;reflow&lt;/code&gt; )时，该线程就会执行。&lt;/li&gt;
&lt;li&gt;注意，&lt;code&gt;GUI&lt;/code&gt; 渲染线程与 &lt;code&gt;JS&lt;/code&gt; 引擎线程是互斥的，当 &lt;code&gt;JS&lt;/code&gt; 引擎执行时 &lt;code&gt;GUI&lt;/code&gt; 线程会被挂起（相当于被冻结了），&lt;code&gt;GUI&lt;/code&gt; 更新会被保存在一个队列中等到 &lt;code&gt;JS&lt;/code&gt; 引擎空闲时立即被执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 引擎&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;负责处理 &lt;code&gt;Javascript&lt;/code&gt; 脚本程序，&lt;code&gt;JavaScript&lt;/code&gt; 引擎是一个专门处理 &lt;code&gt;JavaScript&lt;/code&gt; 脚本的虚拟机，一般会附带在网页浏览器之中，现在有 &lt;code&gt;NodeJS&lt;/code&gt; 让我们在服务端也能使用 &lt;code&gt;JavaScript&lt;/code&gt;。（例如 &lt;code&gt;V8&lt;/code&gt; 引擎），&lt;code&gt;JS&lt;/code&gt; 引擎是基于事件驱动单线程执行的， &lt;code&gt;JavaScript&lt;/code&gt; 引擎一直等待着任务队列中任务的到来，然后加以处理，浏览器无论什么时候都只有一个 &lt;code&gt;JavaScript&lt;/code&gt; 线程在运行 &lt;code&gt;JavaScript&lt;/code&gt; 程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事件触发线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;管理 &lt;code&gt;Event Loop&lt;/code&gt;，&lt;code&gt;Event Loop&lt;/code&gt; 的标准是在&lt;a href=&quot;https://html.spec.whatwg.org/multipage/webappapis.html#event-loops&quot; title=&quot;HTML5&quot;&gt;HTML5&lt;/a&gt;中，是渲染引擎的一个线程来处理，所以并不和 &lt;code&gt;JS&lt;/code&gt; 单线程执行矛盾。&lt;/li&gt;
&lt;li&gt;当一个事件被触发时，该线程会把事件添加到待处理队列的队尾，等待 &lt;code&gt;JavaScript&lt;/code&gt; 引擎的处理。这些事件可来自 &lt;code&gt;JavaScript&lt;/code&gt; 引擎当前执行的代码块如 &lt;code&gt;setTimeout&lt;/code&gt; 、也可来自浏览器内核的其他线程如鼠标点击、&lt;code&gt;Ajax&lt;/code&gt; 异步请求等，但由于 &lt;code&gt;JavaScript&lt;/code&gt; 的单线程关系，所有这些事件都得排队等待 &lt;code&gt;JavaScript&lt;/code&gt; 引擎处理（当线程中没有执行任何同步代码的前提下才会执行异步代码）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定时器触发线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器定时计数器并不是由 &lt;code&gt;JavaScript&lt;/code&gt; 引擎计数的,（因为 &lt;code&gt;JavaScript&lt;/code&gt; 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确），因此通过单独线程来计时并触发定时（计时完毕后，添加到事件队列中，等待 &lt;code&gt;JS&lt;/code&gt; 引擎空闲后执行）。&lt;code&gt;W3C&lt;/code&gt; 在 &lt;code&gt;HTML&lt;/code&gt; 标准中规定，规定要求 &lt;code&gt;setTimeout&lt;/code&gt; 中低于 &lt;code&gt;4ms&lt;/code&gt; 的时间间隔算为 &lt;code&gt;4ms&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异步请求线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;XMLHttpRequest&lt;/code&gt; 在连接后是通过浏览器新开一个线程请求&lt;/li&gt;
&lt;li&gt;检测到状态变更时，如果设置有回调函数，异步线程就产生状态变更事件，将这个回调再放入事件队列中。再由 &lt;code&gt;JavaScript&lt;/code&gt; 引擎执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中前三个是常驻线程，是所有浏览器内核必须实现的，后两个线程执行完就会终止。在 &lt;code&gt;Chrome&lt;/code&gt; 中，每一个 &lt;code&gt;tab&lt;/code&gt; 都是一个独立的渲染进程，都有独立的渲染引擎在工作。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于JS引擎为什么是单线程的，因为大部分程序控制 &lt;code&gt;UI&lt;/code&gt; 的都会是单一单线程，因为 &lt;code&gt;JS&lt;/code&gt; 主要使用场景是与用户交互和操作 &lt;code&gt;DOM&lt;/code&gt;，如果两个线程同时操作一个 &lt;code&gt;DOM&lt;/code&gt; 会很复杂。而H5也提供了多线程方法 &lt;code&gt;web worker&lt;/code&gt;，可以创建多个线程，但是子线程完全受控于主线程且不得操作 &lt;code&gt;DOM&lt;/code&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;JavaScript 引擎&lt;/h2&gt;
&lt;p&gt;渲染引擎中最重要的就是 &lt;code&gt;JavaScript&lt;/code&gt; 引擎，&lt;code&gt;JavaScript&lt;/code&gt; 作为一门动态弱类型语言，一般来说引擎就是解释器 &lt;code&gt;Interpreter&lt;/code&gt;，直接解析并将代码运行结果输出。像 &lt;code&gt;Java、C++、C&lt;/code&gt; 这样的静态语言才需要经过编译到机器码或字节码的过程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译型语言：程序在执行之前需要一个专门的编译过程，把程序编译成为机器语言的文件（如 &lt;code&gt;exe&lt;/code&gt; 文件），运行时不需要重新编译，直接用编译后的文件就行了。优点：执行效率高。缺点：跨平台性差。&lt;/li&gt;
&lt;li&gt;解释型语言：程序不需要编译，程序在运行的过程中才用解释器编译成机器语言，边编译边执行。优点：跨平台性好。缺点：执行效率低。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是现代的 &lt;code&gt;JavaScript&lt;/code&gt; 引擎，比如说 &lt;code&gt;V8&lt;/code&gt;，它其实为了提高 &lt;code&gt;JS&lt;/code&gt; 的运行性能，在运行之前会先将 &lt;code&gt;JS&lt;/code&gt; 编译为本地的机器码（&lt;code&gt;native machine code&lt;/code&gt;），然后再去执行机器码，也就是 &lt;code&gt;JIT（Just-in-time）&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt; 引擎和渲染引擎之间是可以交互的，浏览器提供了相当丰富的 &lt;code&gt;Web API&lt;/code&gt; 让 &lt;code&gt;JavaScript&lt;/code&gt; 能操作 &lt;code&gt;DOM&lt;/code&gt;，&lt;code&gt;BOM&lt;/code&gt;，能够绑定事件，发起请求等等。渲染引擎也可以通过事件循环让 &lt;code&gt;JS&lt;/code&gt; 引擎执行代码。&lt;/p&gt;
&lt;p&gt;目前主要的 &lt;code&gt;JavaScript&lt;/code&gt; 引擎有如下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;V8(Google)&lt;/code&gt;，用 &lt;code&gt;C++&lt;/code&gt; 编写，开放源代码，&lt;code&gt;Google&lt;/code&gt; (丹麦)研发小组在 &lt;code&gt;2006&lt;/code&gt; 年开始研发 &lt;code&gt;V8&lt;/code&gt; ，部分的原因是 &lt;code&gt;Google&lt;/code&gt; 对既有 &lt;code&gt;JavaScript&lt;/code&gt; 引擎的执行速度不满意, 在 &lt;code&gt;2008&lt;/code&gt; 年推出 &lt;code&gt;Chrome&lt;/code&gt;，也用于 &lt;code&gt;Node.js&lt;/code&gt;, 巨大的速度优势，迅速占领市场.。&lt;code&gt;2017&lt;/code&gt; 年 &lt;code&gt;Chrome&lt;/code&gt; 的市场占有达到 &lt;code&gt;59%&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JavaScriptCore(Apple)&lt;/code&gt;，开放源代码，用于 &lt;code&gt;webkit&lt;/code&gt; 型浏览器，如 &lt;code&gt;Safari&lt;/code&gt; ，&lt;code&gt;2008&lt;/code&gt; 年实现了编译器和字节码解释器，升级为了 &lt;code&gt;SquirrelFish&lt;/code&gt;。苹果内部代号为 &lt;code&gt;Nitro&lt;/code&gt; 的 &lt;code&gt;JavaScript&lt;/code&gt; 引擎也是基于 &lt;code&gt;JavaScriptCore&lt;/code&gt; 引擎的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Rhino&lt;/code&gt;，由 &lt;code&gt;Mozilla&lt;/code&gt; 基金会管理，开放源代码，完全以 &lt;code&gt;Java&lt;/code&gt; 编写，用于 &lt;code&gt;HTMLUnit&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SpiderMonkey(Mozilla)&lt;/code&gt;，第一款 &lt;code&gt;JavaScript&lt;/code&gt; 引擎，早期用于 &lt;code&gt;Netscape Navigator&lt;/code&gt;，现时用于 &lt;code&gt;Mozilla Firefox&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Chakra (Microsoft)&lt;/code&gt;：&lt;code&gt;JScript&lt;/code&gt; 引擎，用于 &lt;code&gt;Internet Explorer&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Chakra (Microsoft)&lt;/code&gt;：&lt;code&gt;JavaScript&lt;/code&gt; 引擎，用于 &lt;code&gt;Microsoft Edge&lt;/code&gt;。目前 &lt;code&gt;Edge&lt;/code&gt; 已经使用 &lt;code&gt;Chromium&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KJS&lt;/code&gt;，&lt;code&gt;KDE&lt;/code&gt; 的 &lt;code&gt;ECMAScript／JavaScript&lt;/code&gt; 引擎，最初由哈里·波顿开发，用于 &lt;code&gt;KDE&lt;/code&gt; 项目的 &lt;code&gt;Konqueror&lt;/code&gt; 网页浏览器中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Hermes&lt;/code&gt;：&lt;code&gt;Facebook&lt;/code&gt; 在 &lt;code&gt;ChainReact 2019&lt;/code&gt; 大会上正式推出了新一代 &lt;code&gt;JavaScript&lt;/code&gt; 执行引擎 &lt;code&gt;Hermes&lt;/code&gt;。&lt;code&gt;Hermes&lt;/code&gt; 是一款小巧轻便的 &lt;code&gt;JavaScript&lt;/code&gt; 引擎，专门针对在 &lt;code&gt;Android&lt;/code&gt; 上运行 &lt;code&gt;React Native&lt;/code&gt; 进行了优化。对于许多应用程序，只需启用 &lt;code&gt;Hermes&lt;/code&gt; 即可缩短启动时间、减少内存使用量并缩小应用程序大小，此外因为它采用 &lt;code&gt;JavaScript&lt;/code&gt; 标准实现，所以很容易在 &lt;code&gt;React Native&lt;/code&gt; 应用中集成。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;QuickJS&lt;/code&gt;：&lt;code&gt;QuickJS&lt;/code&gt; 是一个小型并且可嵌入的 &lt;code&gt;Javascript&lt;/code&gt; 引擎，它支持 &lt;code&gt;ES2020&lt;/code&gt; 规范，包括模块，异步生成器和代理器，由 &lt;code&gt;Bellard&lt;/code&gt; 开发。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;渲染过程&lt;/h2&gt;
&lt;p&gt;浏览器接收到服务器返回的 &lt;code&gt;HTML&lt;/code&gt; 文档后就开始解析并渲染 &lt;code&gt;HTML&lt;/code&gt; 文档，主要流程如下： 1. 解析 &lt;code&gt;HTML&lt;/code&gt; 文档，将元素按层次转化成一棵 &lt;code&gt;DOM&lt;/code&gt;树，根节点为 &lt;code&gt;document&lt;/code&gt;。 2. 解析 &lt;code&gt;CSS&lt;/code&gt; 样式文件（包括外部 &lt;code&gt;CSS&lt;/code&gt;文件和样式元素以及 &lt;code&gt;js&lt;/code&gt; 生成的样式），获取样式数据，生成 &lt;code&gt;CSSOM&lt;/code&gt;。 3. 结合 &lt;code&gt;CSSOM&lt;/code&gt; 和 &lt;code&gt;DOM&lt;/code&gt;树计算出节点的样式，生成渲染树（ &lt;code&gt;render tree&lt;/code&gt; ），渲染树包含多个带有样式属性的矩形，这些矩形的排列顺讯就是它们在屏幕上显示的顺序。 4. 进入布局阶段，从根节点递归调用，计算每一个元素的大小、位置等，给每个节点所应该出现在屏幕上的精确坐标。 5. 遍历渲染树，每个节点将使用 &lt;code&gt;UI&lt;/code&gt; 后端层来绘制。&lt;/p&gt;
&lt;p&gt;对于渲染引擎的渲染细节感兴趣的同学可以看看这篇文章，&lt;a href=&quot;https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/&quot; title=&quot;How browsers work&quot;&gt;How browsers work&lt;/a&gt;，中文翻译的不是很好，推荐大家结合英文看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/browser_rending.asuGBM_3_ZSM77r.webp&quot; alt=&quot;browser-rendering&quot; title=&quot;browser-rendering&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Reflow 和 Repaint&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Reflow&lt;/code&gt; : 元素的盒模型或者布局发生了变化，浏览器需要重新计算其大小和位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Repaint&lt;/code&gt; : 当一个元素的外观发生改变，但没有改变布局,重新把元素外观绘制出来的过程，叫做重绘。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们可以发现 &lt;code&gt;Reflow&lt;/code&gt; 对应的是渲染过程中的第四步，而 &lt;code&gt;Repaint&lt;/code&gt; 对应的是渲染过程的第五步。直白一点说就是当 &lt;code&gt;DOM&lt;/code&gt; 被修改后需要重新计算渲染树 &lt;code&gt;render tree&lt;/code&gt; 的一部分或者全部的时候，我们就需要 &lt;code&gt;Reflow&lt;/code&gt;，而如果元素的修改不影响渲染树，那么只要 &lt;code&gt;Repaint&lt;/code&gt; 就可以了。回流必将引起重绘，重绘不一定会引起回流。&lt;/p&gt;
&lt;p&gt;显而易见，&lt;code&gt;Reflow&lt;/code&gt; 的成本要比 &lt;code&gt;Repaint&lt;/code&gt; 高得多，&lt;code&gt;DOM Tree&lt;/code&gt; 里的每个结点都会有 &lt;code&gt;reflow&lt;/code&gt;方法，一个结点的 &lt;code&gt;reflow&lt;/code&gt; 很有可能导致子结点，甚至父点以及同级结点的 &lt;code&gt;reflow&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;会导致 &lt;code&gt;Reflow&lt;/code&gt; 的操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;页面首次渲染&lt;/li&gt;
&lt;li&gt;浏览器窗口大小发生改变&lt;/li&gt;
&lt;li&gt;元素尺寸或位置发生改变&lt;/li&gt;
&lt;li&gt;元素内容变化（文字数量或图片大小等等）&lt;/li&gt;
&lt;li&gt;元素字体大小变化&lt;/li&gt;
&lt;li&gt;添加或者删除可见的 &lt;code&gt;DOM&lt;/code&gt; 元素&lt;/li&gt;
&lt;li&gt;激活 &lt;code&gt;CSS&lt;/code&gt; 伪类（例如：&lt;code&gt;:hover&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;查询某些属性或调用某些方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一些常用且会导致 &lt;code&gt;Reflow&lt;/code&gt; 的属性和方法：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;clientWidth、clientHeight、clientTop、clientLeft、offsetWidth、offsetHeight、offsetTop、offsetLeft、scrollWidth、scrollHeight、scrollTop、scrollLeft、scrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、getBoundingClientRect()、scrollTo()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;当页面中元素样式的改变并不影响它在文档流中的位置时（例如：&lt;code&gt;color、background-color、visibility&lt;/code&gt;等），浏览器会将新样式赋予给元素并重新绘制它，这个过程称为重绘。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Reflow&lt;/code&gt; 和 &lt;code&gt;Repaint&lt;/code&gt; 对性能可能造成很大的影响：有时即使仅仅回流一个单一的元素，它的父元素以及任何跟随它的元素也会产生 &lt;code&gt;Reflow&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现代浏览器会对频繁的回流或重绘操作进行优化：浏览器会维护一个队列，把所有引起回流和重绘的操作放入队列中，如果队列中的任务数量或者时间间隔达到一个阈值的，浏览器就会将队列清空，进行一次批处理，这样可以把多次回流和重绘变成一次。比如下面的代码我们对同一个元素连续进行 &lt;code&gt;transform&lt;/code&gt; 的话，浏览器只会执行最后一个。如果我们想看到两段动画效果则要借助 &lt;code&gt;setTimiout&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&amp;#x3C;div class=&quot;cube&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;button&gt;transform&amp;#x3C;/button&gt;
&amp;#x3C;script&gt;
    let cube = document.querySelector(&apos;.cube&apos;);
let btn = document.querySelector(&apos;button&apos;);
btn.addEventListener(&apos;click&apos;, e =&gt; {
    cube.style.transform = &apos;translateX(30px)&apos;;
    setTimeout(() =&gt; {
        // cube.style.transform = &apos;translateX(-30px)&apos;;
        console.log(&apos;timeout&apos;);
    }, 0);
    cube.style.transform = &apos;translateX(-30px)&apos;;
});
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当访问以下属性或方法时，浏览器会立刻清空队列：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clientWidth、clientHeight、clientTop、clientLeft&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;offsetWidth、offsetHeight、offsetTop、offsetLeft&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scrollWidth、scrollHeight、scrollTop、scrollLeft&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;width、height&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getComputedStyle()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getBoundingClientRect()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为队列中可能会有影响到这些属性或方法返回值的操作，即使你希望获取的信息与队列中操作引发的改变无关，浏览器也会强行清空队列，确保你拿到的值是最精确的。&lt;/p&gt;
&lt;p&gt;现在的浏览器已经对渲染的过程尽可能的优化，不过我们还是可以在编码的过程只能够注意一些细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CSS&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免使用 &lt;code&gt;table&lt;/code&gt; 布局。浏览器使用流式布局，对 &lt;code&gt;Render Tree&lt;/code&gt; 的计算通常只需要遍历一次就可以完成，但 &lt;code&gt;table&lt;/code&gt; 及其内部元素除外，他们可能需要多次计算，通常要花 &lt;code&gt;3&lt;/code&gt; 倍于同等元素的时间&lt;/li&gt;
&lt;li&gt;尽可能在 &lt;code&gt;DOM&lt;/code&gt; 树的最末端改变 &lt;code&gt;class&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;避免设置多层内联样式。&lt;/li&gt;
&lt;li&gt;将动画效果应用到 &lt;code&gt;position&lt;/code&gt; 属性为 &lt;code&gt;absolute&lt;/code&gt; 或 &lt;code&gt;fixed&lt;/code&gt; 的元素上。&lt;/li&gt;
&lt;li&gt;避免使用 &lt;code&gt;CSS&lt;/code&gt; 表达式（例如：&lt;code&gt;calc()&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;javascript&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免频繁操作样式，最好一次性重写 &lt;code&gt;style&lt;/code&gt; 属性，或者将样式列表定义为 &lt;code&gt;class&lt;/code&gt; 并一次性更改 &lt;code&gt;class&lt;/code&gt; 属性。&lt;/li&gt;
&lt;li&gt;避免频繁操作 &lt;code&gt;DOM&lt;/code&gt;，创建一个 &lt;code&gt;documentFragment&lt;/code&gt;，在它上面应用所有 &lt;code&gt;DOM&lt;/code&gt; 操作，最后再把它添加到文档中。&lt;/li&gt;
&lt;li&gt;也可以先为元素设置 &lt;code&gt;display: none&lt;/code&gt;，操作结束后再把它显示出来。因为在 &lt;code&gt;display&lt;/code&gt; 属性为 &lt;code&gt;none&lt;/code&gt; 的元素上进行的 &lt;code&gt;DOM&lt;/code&gt; 操作不会引发回流和重绘。&lt;/li&gt;
&lt;li&gt;避免频繁读取会引发回流/重绘的属性，如果确实需要多次使用，就用一个变量缓存起来。&lt;/li&gt;
&lt;li&gt;对具有复杂动画的元素使用绝对定位，使它脱离文档流，否则会引起父元素及后续元素频繁回流。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;DOM, CSS, JS的阻塞&lt;/h2&gt;
&lt;p&gt;渲染过程并不像上面描述的那么简单，特别是页面的绘制是一件开销非常大的事情，所以浏览器尽量最有效率的绘制，避免那些没必要的重绘和回流。要避免这个问题，&lt;code&gt;DOM&lt;/code&gt; 加载和解析，&lt;code&gt;CSS&lt;/code&gt; 加载和解析，和 &lt;code&gt;JS&lt;/code&gt; 的加载执行之间的顺序和阻塞就非常重要，如果处理不当，页面很可能要重绘。因为渲染树是 &lt;code&gt;DOM&lt;/code&gt; 和 &lt;code&gt;CSS&lt;/code&gt; 结合生成的，而 &lt;code&gt;JS&lt;/code&gt; 可以操作 &lt;code&gt;DOM&lt;/code&gt; 和样式，必须处理好三者之间的逻辑。&lt;/p&gt;
&lt;h2&gt;load 和 DOMContentLoaded&lt;/h2&gt;
&lt;p&gt;在分析具体情况之前我们先说两个事件 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 和 &lt;code&gt;load&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;load&lt;/code&gt;：当一个资源及其依赖资源已完成加载时，将触发 &lt;code&gt;load&lt;/code&gt; 事件，也就是当页面的 &lt;code&gt;html&lt;/code&gt;、&lt;code&gt;css&lt;/code&gt;、&lt;code&gt;js&lt;/code&gt;、图片等资源都已经加载完之后才会触发 &lt;code&gt;load&lt;/code&gt; 事件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DOMContentLoaded&lt;/code&gt; ：当初始的 &lt;code&gt;HTML&lt;/code&gt; 文档被完全加载和解析完成之后就会被触发，而无需等待样式表、图像和子框架的完成加载。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面两个说法都是 &lt;code&gt;MDN&lt;/code&gt; 给出的，但是其实具体结合加载和解析的阻塞情况会不一样（结合下面的阻塞情况来看这几点）： 1. 因为 &lt;code&gt;js&lt;/code&gt; 会阻塞 &lt;code&gt;DOM&lt;/code&gt; 的解析，所以当文档解析完成触发 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件的时候，文档中所有的同步的 &lt;code&gt;js&lt;/code&gt; 任务都已经执行完毕。 2. 因为 &lt;code&gt;CSS&lt;/code&gt; 会阻塞 &lt;code&gt;JS&lt;/code&gt; 而不会阻塞 &lt;code&gt;DOM&lt;/code&gt; 解析，并且 &lt;code&gt;JS&lt;/code&gt; 的执行会让该 &lt;code&gt;JS&lt;/code&gt; 之前的以加载 &lt;code&gt;DOM&lt;/code&gt; 和 &lt;code&gt;CSS&lt;/code&gt; 渲染，所有在 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件触发时，所有在 &lt;code&gt;JS&lt;/code&gt; 之前的 &lt;code&gt;CSS&lt;/code&gt; 已经加载并渲染完成。 3. 当 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件触发时，文档解析完毕，页面的 &lt;code&gt;DOM&lt;/code&gt; 树已经构建完成，所有页面上的 &lt;code&gt;JS&lt;/code&gt; 同步任务执行完成，所有在 &lt;code&gt;JS&lt;/code&gt; 之前的 &lt;code&gt;CSS&lt;/code&gt; 都已经加载并渲染完成。（页面每遇到一个 &lt;code&gt;script&lt;/code&gt; 标签都会把当前已经构建的 &lt;code&gt;DOM&lt;/code&gt; 树和 &lt;code&gt;CSSOM&lt;/code&gt;树结合渲染一次） 4. 当 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件触发之后，浏览器会继续加载在 &lt;code&gt;JS&lt;/code&gt; 之后的 &lt;code&gt;CSS&lt;/code&gt;，以及在JS中同步添加的 &lt;code&gt;img，css，js&lt;/code&gt; 文件，当这些资源全部加载完成，会进行 &lt;code&gt;load&lt;/code&gt; 事件触发之前的最后一次渲染，之后load事件被触发。&lt;/p&gt;
&lt;p&gt;这段代码可以验证，同步的 &lt;code&gt;js&lt;/code&gt; 代码中的外部资源文件（ &lt;code&gt;js&lt;/code&gt;，&lt;code&gt;css&lt;/code&gt;，&lt;code&gt;img&lt;/code&gt; ）都加载（由异步请求线程完成的异步加载）完毕后才会触发 &lt;code&gt;load&lt;/code&gt; 事件，加载速度可以通过设置 &lt;code&gt;network throttling&lt;/code&gt; 配置，把加载速度设置非常慢，可以看到资源文件加载完成以后load事件才触发。而异步事件 &lt;code&gt;setTimeout&lt;/code&gt; 回调函数中的资源文件则会在 &lt;code&gt;load&lt;/code&gt; 事件触发后在执行，不会阻塞 &lt;code&gt;load&lt;/code&gt; 的触发。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;js/test.js&quot;&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div id=&quot;test&quot;&gt;&amp;#x3C;/div&gt;

    &amp;#x3C;script&gt;
      let script = document.createElement(&apos;link&apos;)
      script.setAttribute(&apos;rel&apos;, &apos;stylesheet&apos;)
      script.setAttribute(
        &apos;href&apos;,
        &apos;https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css&apos;
      )

      var a = document.body.appendChild(script)

      // setTimeout(function() {
      //   let script = document.createElement(&quot;link&quot;);
      //   script.setAttribute(&quot;rel&quot;, &quot;stylesheet&quot;);
      //   script.setAttribute(
      //     &quot;href&quot;,
      //     &apos;https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css&apos;
      //   );
      //   var a = document.body.appendChild(script);
      //   console.log(&quot;setTimeout...&quot;);
      // }, 3000);

      window.onload = function () {
        console.log(&apos;window load...&apos;)
      }
    &amp;#x3C;/script&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;下面有提到预加载监视器 &lt;code&gt;preload scanner&lt;/code&gt;，浏览器对同一域名下的最大并发连接数不超过 &lt;code&gt;6&lt;/code&gt; 个。超过 &lt;code&gt;6&lt;/code&gt; 个的话，剩余的将会在队列中等待，这就是为什么我们要将资源放到不同的域名下，也是为了充分利用该机制，最大程度的并发下载所需资源，尽快的完成页面的渲染。 下载和加载在浏览器渲染过程中应该是相同的意思。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;2020-10-23补充&lt;/strong&gt; 按照一般的理论来说，&lt;code&gt;DOMContentLoaded&lt;/code&gt; 是在文档解析完成，也就是 &lt;code&gt;DOM&lt;/code&gt; 树构建完成的时候就会触发，也就是样式表的加载与否不会影响 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件发生的时间，但是经过实际的测试我发现浏览请的行为并不一致。比如下面的代码，我让服务器延迟 &lt;code&gt;5000ms&lt;/code&gt; 返回 &lt;code&gt;css&lt;/code&gt; 文件，按理说这个 &lt;code&gt;css&lt;/code&gt; 不会影响 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 的发生时间，但是在 &lt;code&gt;Chrome&lt;/code&gt; 中，只要你将这个 &lt;code&gt;css&lt;/code&gt; 放到 &lt;code&gt;body&lt;/code&gt; 中就会出现 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 知道 &lt;code&gt;CSS&lt;/code&gt; 文件加载完成才触发，但是把 &lt;code&gt;link&lt;/code&gt; 放到 &lt;code&gt;head&lt;/code&gt; 中就不会。在 &lt;code&gt;safari&lt;/code&gt; 中则是立即触发 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件。&lt;code&gt;Opera&lt;/code&gt; 的表现跟 &lt;code&gt;chrome&lt;/code&gt; 一致。&lt;code&gt;firefox&lt;/code&gt; 虽然也是 &lt;code&gt;css&lt;/code&gt; 加载完成后触发，但是它先渲染出 &lt;code&gt;div&lt;/code&gt; 本来的绿色，&lt;code&gt;css&lt;/code&gt; 请求回来后再渲染成 &lt;code&gt;蓝色&lt;/code&gt;，其他几个浏览器则是全部只有一次渲染，就是只有 &lt;code&gt;css&lt;/code&gt; 回来之后才渲染 &lt;code&gt;div&lt;/code&gt;，直接显示蓝色。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Title&amp;#x3C;/title&gt;
    &amp;#x3C;style&gt;
      div {
        width: 100px;
        height: 100px;
        background: lightgreen;
      }
    &amp;#x3C;/style&gt;
    &amp;#x3C;!-- &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;/css/sleep5000-common.css&quot; /&gt; --&gt;
    &amp;#x3C;!-- &amp;#x3C;script defer src=&quot;/js/blok.js&quot;&gt;&amp;#x3C;/script&gt; --&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;!-- &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;/css/sleep5000-common.css&quot; /&gt; --&gt;
    &amp;#x3C;button&gt;click&amp;#x3C;/button&gt;
    &amp;#x3C;script defer src=&quot;/js/block.js&quot;&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;/css/sleep5000-common.css&quot; /&gt;
    &amp;#x3C;!-- 服务器延迟返回，设置div为蓝色 --&gt;
    &amp;#x3C;div&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//block.js
console.log(&apos;DOMContentLoaded&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我进行一个简单的总结就是：放在 &lt;code&gt;body&lt;/code&gt; 之前的 &lt;code&gt;link&lt;/code&gt; 不会阻塞 &lt;code&gt;DOM&lt;/code&gt; 的解析（&lt;code&gt;DOMContentLoaded&lt;/code&gt; 不会等待 &lt;code&gt;CSS&lt;/code&gt; 加载），但是会阻塞渲染，渲染会在 &lt;code&gt;CSS&lt;/code&gt; 请求成功以后再渲染，在之前浏览器将是白屏。而放在 &lt;code&gt;body&lt;/code&gt; 中和 &lt;code&gt;body&lt;/code&gt; 后的 &lt;code&gt;CSS&lt;/code&gt; 则会同时阻止 &lt;code&gt;DOM&lt;/code&gt; 的解析和渲染，&lt;code&gt;DOMContentLoaded&lt;/code&gt; 会等待 &lt;code&gt;CSS&lt;/code&gt; 的加载，&lt;code&gt;link&lt;/code&gt; 之前的元素会成功渲染，但是 &lt;code&gt;link&lt;/code&gt; 之后的元素会等待 &lt;code&gt;CSS&lt;/code&gt; 加载成功后才渲染。从某个角度看，&lt;code&gt;body&lt;/code&gt; 中的 &lt;code&gt;link&lt;/code&gt; 和 &lt;code&gt;script&lt;/code&gt; 的处理逻辑是一致的。当然具体的行为每个浏览器并不完全相同。&lt;/p&gt;
&lt;p&gt;为了验证自己的想法我又写了一个例子，我用几个 &lt;code&gt;link&lt;/code&gt; 分别将几个 &lt;code&gt;div&lt;/code&gt; 隔开，调节 &lt;code&gt;chorme devtools&lt;/code&gt; 的 &lt;code&gt;network throttling&lt;/code&gt; 为 &lt;code&gt;slow 3g&lt;/code&gt; 你也可以设置自己想要的速度。然后看看这些元素的渲染顺序。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &amp;#x3C;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot; /&gt;
    &amp;#x3C;title&gt;css-dom-parse&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;parse.js&quot;&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;style&gt;
      div {
        height: 100px;
        width: 100px;
      }
      .red {
        background: red;
      }
      .black {
        background: black;
      }
      .green {
        background: green;
      }
    &amp;#x3C;/style&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div&gt;test&amp;#x3C;/div&gt;
    &amp;#x3C;!-- &amp;#x3C;script defer src=&quot;parse1.js&quot;&gt;&amp;#x3C;/script&gt; --&gt;
    &amp;#x3C;link
      href=&quot;https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css&quot;
      rel=&quot;stylesheet&quot;
    /&gt;
    &amp;#x3C;div class=&quot;red&quot;&gt;&amp;#x3C;/div&gt;
    &amp;#x3C;link
      href=&quot;https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.css&quot;
      rel=&quot;stylesheet&quot;
    /&gt;
    &amp;#x3C;div class=&quot;green&quot;&gt;&amp;#x3C;/div&gt;
    &amp;#x3C;link
      href=&quot;https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap-grid.css&quot;
      rel=&quot;stylesheet&quot;
    /&gt;
    &amp;#x3C;div class=&quot;black&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个 &lt;code&gt;bootstrap&lt;/code&gt; 的 &lt;code&gt;css&lt;/code&gt; 文件，加载的顺序是 &lt;code&gt;3 1 2&lt;/code&gt;，加载是并行的，而渲染是等到全部加载完才开始，渲染顺序就是从上到下。&lt;/p&gt;
&lt;h2&gt;阻塞关系&lt;/h2&gt;
&lt;p&gt;总的原则就是一点 &lt;code&gt;JS&lt;/code&gt; 引擎和渲染引擎是互斥的。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;外部资源文件的加载不会被阻塞（js，css，image） 我们的 &lt;code&gt;HTML&lt;/code&gt; 文档一般都会包含外部链接，引入资源文件，包括 &lt;code&gt;js&lt;/code&gt;，&lt;code&gt;css&lt;/code&gt;，图片等，我们的主线程会一个一个的请求这些链接，不过现代浏览器一般会有一个预加载监视器 &lt;code&gt;preload scanner&lt;/code&gt; 来加速这些链接的加载，因为我们的文档解析需要事件，所以提前发送请求获取资源文件会提高效率。这个预加载监视器会找到 &lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt; 和 &lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt; 之类的标签，发送给网络线程请求资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/preload.DPVkpyWY_ZAMcaT.webp&quot; alt=&quot;preload&quot; title=&quot;preload&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;code&gt;CSS&lt;/code&gt; 不会阻塞 &lt;code&gt;DOM&lt;/code&gt; 解析 这一点其实很好理解，&lt;code&gt;CSS&lt;/code&gt; 不会引起 &lt;code&gt;DOM&lt;/code&gt;的变化，它们两个只要尽早解析完成，然后生成渲染树就可以了，并行加载并不影响。我们可以设计一个场景模拟出这个状态，现在 &lt;code&gt;chrome&lt;/code&gt; 中把 &lt;code&gt;network throttling&lt;/code&gt; 设置一个较小的数值（我设置的 &lt;code&gt;50kb/s&lt;/code&gt; ），然后加载一个稍大的 &lt;code&gt;CSS&lt;/code&gt;，然后在 &lt;code&gt;css&lt;/code&gt; 之前用一个 &lt;code&gt;defer&lt;/code&gt; 属性的 &lt;code&gt;js&lt;/code&gt; 获取页面上的 &lt;code&gt;DOM&lt;/code&gt;（ &lt;code&gt;defer&lt;/code&gt; 属性表示这个 &lt;code&gt;js&lt;/code&gt; 会在 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件发生后立即执行），如果能够在 &lt;code&gt;css&lt;/code&gt; 加载好之前获取 &lt;code&gt;dom&lt;/code&gt; 节点，说明 &lt;code&gt;CSS&lt;/code&gt; 是不会阻塞 &lt;code&gt;DOM&lt;/code&gt; 解析的。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &amp;#x3C;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot; /&gt;
    &amp;#x3C;title&gt;css-dom-parse&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;css-dom-parse.js&quot;&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;link
      href=&quot;https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css&quot;
      rel=&quot;stylesheet&quot;
    /&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div&gt;test&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//css-dom-parse.js
const div = document.querySelector(&apos;div&apos;)
console.log(div)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/css-dom-parse.CL3WFqXZ_2ivjI4.webp&quot; alt=&quot;css-dom-parse&quot; title=&quot;css-dom-parse&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时我们的 &lt;code&gt;css&lt;/code&gt; 还没加载完成，但是我们可以看到 &lt;code&gt;console&lt;/code&gt; 选项卡里面 &lt;code&gt;div&lt;/code&gt; 标签已经被打印出来了，说明，此时 &lt;code&gt;DOM&lt;/code&gt; 已经解析完成触发 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2020-10-23 补充&lt;/strong&gt; 在多个浏览器中测试，如果把 &lt;code&gt;link&lt;/code&gt; 标签放到 &lt;code&gt;body&lt;/code&gt; 中将会阻塞 &lt;code&gt;DOM&lt;/code&gt; 解析（只有 &lt;code&gt;safari&lt;/code&gt; 不会），详细的例子可以参考上面的 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 章节最后的补充部分。&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CSS&lt;/code&gt; 会阻塞 &lt;code&gt;DOM&lt;/code&gt; 渲染 因为 &lt;code&gt;DOM&lt;/code&gt; 的渲染需要 &lt;code&gt;DOM&lt;/code&gt; 树和 &lt;code&gt;CSSOM&lt;/code&gt; 树共同来生成渲染树，所以在 &lt;code&gt;CSS&lt;/code&gt; 加载完成之前， &lt;code&gt;DOM&lt;/code&gt; 是不会进行渲染的。还是用上面那个例子，我们会发现 &lt;code&gt;CSS&lt;/code&gt; 没有加载完成时，页面上是不显示 &lt;code&gt;test&lt;/code&gt; 的，而当 &lt;code&gt;CSS&lt;/code&gt; 加载完成的瞬间，标签就被渲染到页面上了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CSS&lt;/code&gt; 会阻塞 &lt;code&gt;JS&lt;/code&gt; 这一点可能大家有点疑惑，但是仔细想一想，我们的脚本可以在文档解析阶段请求样式信息，如果此时我们的样式还没有加载和解析，那么脚本必然会获得错误的结果，这样会产生很多问题，目前的浏览器做法就是在 &lt;code&gt;CSS&lt;/code&gt;加载解析的过程中会阻塞 &lt;code&gt;JS&lt;/code&gt; 的加载执行。虽然我们不常遇到这样的情况，但是也要清楚为什么会发生这样的情况。我们还用刚刚的方法，不过这次我们需要快速看到结果，可以把速度设置的快一点，&lt;code&gt;network throttling&lt;/code&gt; 设置为 &lt;code&gt;slow 3G&lt;/code&gt;，代码如下：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &amp;#x3C;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot; /&gt;
    &amp;#x3C;title&gt;css-dom-parse&amp;#x3C;/title&gt;
    &amp;#x3C;script&gt;
      var starttime = new Date().getTime()
      console.log(&apos;page start&apos; + starttime)
    &amp;#x3C;/script&gt;
    &amp;#x3C;link
      href=&quot;https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css&quot;
      rel=&quot;stylesheet&quot;
    /&gt;
    &amp;#x3C;script src=&quot;css-dom-parse.js&quot;&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div&gt;test&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var endtime = new Date().getTime()
console.log(&apos;delay:&apos; + (endtime - starttime))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在 &lt;code&gt;CSS&lt;/code&gt; 加载之前记录时间并保存到全局变量 &lt;code&gt;starttime&lt;/code&gt; 中，然后在 &lt;code&gt;CSS&lt;/code&gt; 之后加载一个 &lt;code&gt;JS&lt;/code&gt; 文件，当这个 &lt;code&gt;JS&lt;/code&gt; 文件执行的时候，我们输出执行时间并计算时间差。结果如下图： &lt;img src=&quot;https://clloz.com/_astro/css-js-block.BwgYx6lf_21jDWt.webp&quot; alt=&quot;css-js-block&quot; title=&quot;css-js-block&quot;&gt; 我们把时间线拖到页面加载的最开始，我们发现 &lt;code&gt;html&lt;/code&gt; 和 &lt;code&gt;js&lt;/code&gt;都已经加载完毕，但是 &lt;code&gt;css&lt;/code&gt; 的请求还没有完成，而我们在看看输出的时间，我们的 &lt;code&gt;js&lt;/code&gt; 是在 &lt;code&gt;2487ms&lt;/code&gt; 后才执行，正是 &lt;code&gt;css&lt;/code&gt; 加载完成后才执行的 &lt;code&gt;js&lt;/code&gt;。&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;&lt;code&gt;JS&lt;/code&gt; 会阻塞渲染引擎 当解析起遇到 &lt;code&gt;script&lt;/code&gt; 标签时，文档会立即停止解析直到脚本执行完毕，如果脚本是外部的，还要加上从服务器下载的时间。这里注意一个比较细节的部分，渲染引擎此时不会继续往下解析和渲染 &lt;code&gt;DOM&lt;/code&gt; 了，在下载脚本的过程中，我们还是可以堆页面上已经渲染的 &lt;code&gt;DOM&lt;/code&gt; 进行操作，一旦下载完成，&lt;code&gt;JS&lt;/code&gt; 引擎开始执行脚本，整个浏览器就处于卡死状态，我们只能等脚本执行完成才能继续和页面交互。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面的例子中，我在给页面的一个 &lt;code&gt;button&lt;/code&gt; 元素绑定了点击事件，点击以后会 &lt;code&gt;console.log(123)&lt;/code&gt;，然后在这个 &lt;code&gt;button&lt;/code&gt; 下方我引入了一个 &lt;code&gt;script&lt;/code&gt;，我在服务器上设置 &lt;code&gt;5000ms&lt;/code&gt; 后才返回这个脚本，模拟下载超大脚本或网速很慢的情况。然后这个脚本的执行则是一个非常大的数组操作，也要花费一点时间。这个脚本后面还有一个新的 &lt;code&gt;div&lt;/code&gt; 元素。当页面开始加载后 &lt;code&gt;btn&lt;/code&gt; 会先加载出来并成功绑定事件，此时我们点击 &lt;code&gt;btn&lt;/code&gt; 能够成功输出 &lt;code&gt;123&lt;/code&gt;，此时已经开始从服务器下载脚本了，在下载脚本的这段时间内我们依然能够点击按钮。一旦 &lt;code&gt;DOM&lt;/code&gt; 成功返回（我在脚本开头执行了一个 &lt;code&gt;console.log&lt;/code&gt;），我们点击 &lt;code&gt;DOM&lt;/code&gt; 就没有反应了，也就是浏览器 &lt;strong&gt;卡死&lt;/strong&gt; 了。并且如果我们添加多个 &lt;code&gt;script&lt;/code&gt; 标签我们可以发现请求是并行的，现代浏览器也对这种 &lt;strong&gt;卡死&lt;/strong&gt; 进行了优化，它会在卡死的这段时间查看后面的 &lt;code&gt;DOM&lt;/code&gt; 是否有资源要加载，如果有就进行提前加载。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;button&gt;click&amp;#x3C;/button&gt;
&amp;#x3C;script&gt;
  let btn = document.querySelector(&apos;button&apos;)
  btn.addEventListener(&apos;click&apos;, (e) =&gt; {
    console.log(123)
  })
&amp;#x3C;/script&gt;
&amp;#x3C;script src=&quot;/js/sleep5000-logDiv.js&quot;&gt;&amp;#x3C;/script&gt;
&amp;#x3C;script src=&quot;https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js&quot;&gt;&amp;#x3C;/script&gt;
&amp;#x3C;div&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const div = document.querySelector(&apos;div&apos;)
console.log(div)
const arr = []
for (let i = 0; i &amp;#x3C; 100000000; i++) {
  arr.push(i)
  arr.splice(i % 3, i % 7, i % 5)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务端代码参考：&lt;a href=&quot;https://github.com/ljf0113/how-js-and-css-block-dom/blob/master/index.js&quot; title=&quot;how js css block Dom&quot;&gt;how js css block Dom&lt;/a&gt;，这种现象的因为我们的脚本会操作 &lt;code&gt;DOM&lt;/code&gt;，所以在脚本跑完之前浏览器不知道脚本会把 &lt;code&gt;DOM&lt;/code&gt; 改成什么样，所以就等脚本执行完再进行解析。还有一个简单测试的例子，我们用一个有 &lt;code&gt;defer&lt;/code&gt; 属性的 &lt;code&gt;js&lt;/code&gt; 在 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 的时候输出内容，然后再写一个一定时常的死循环，看看输出结果。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &amp;#x3C;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot; /&gt;
    &amp;#x3C;title&gt;css-dom-parse&amp;#x3C;/title&gt;
    &amp;#x3C;script&gt;
      var starttime = new Date().getTime()
      console.log(&apos;starttime&apos; + starttime)
    &amp;#x3C;/script&gt;
    &amp;#x3C;script defer src=&quot;domcontentloaded.js&quot;&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;script src=&quot;css-dom-parse.js&quot;&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div&gt;test&amp;#x3C;/div&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//domcontentloaded.js
console.log(&apos;DOMContentLoaded! &apos; + (new Date().getTime() - starttime))

//css-dom-parse.js
var now = new Date().getTime()

while (new Date().getTime() - now &amp;#x3C; 3000) {
  continue
}
console.log(&apos;time out&apos; + new Date().getTime())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下图: &lt;img src=&quot;https://clloz.com/_astro/js-dom-block.BT6WmlHz_Z2wFiXY.webp&quot; alt=&quot;js-dom-block&quot; title=&quot;js-dom-block&quot;&gt; 可以看出 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件一直等到 &lt;code&gt;JS&lt;/code&gt; 执行完成以后才触发。&lt;/p&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;浏览器解析到非 &lt;code&gt;defer&lt;/code&gt; 和 &lt;code&gt;async&lt;/code&gt; 的 &lt;code&gt;script&lt;/code&gt; 标签会立即触发一次渲染 这个机制可能很多朋友不是很清楚，当我们的浏览器在解析文档的过程中遇到 &lt;code&gt;script&lt;/code&gt; 标签（ &lt;code&gt;body&lt;/code&gt; 中的，因为 &lt;code&gt;body&lt;/code&gt; 之前并没有需要渲染到页面上的元素），他会立即把已经解析的部分渲染了，这当然是个比较极端的情况，一般良好的代码不会遇到。大概是因为解析到一半的时候渲染树还没有，如果此时 &lt;code&gt;JS&lt;/code&gt; 要操作的 &lt;code&gt;DOM&lt;/code&gt; 的话，浏览器不知道如何处理，只能先把当前内容渲染了。测试代码如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
    &amp;#x3C;script&gt;
      console.log(&apos;page start&apos; + new Date().getTime())
    &amp;#x3C;/script&gt;
    &amp;#x3C;style&gt;
      div {
        background: lightblue;
        width: 500px;
        height: 500px;
      }
    &amp;#x3C;/style&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div&gt;test3&amp;#x3C;/div&gt;
    &amp;#x3C;script src=&quot;js/test.js&quot;&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;style&gt;
      div {
        background: lightgray;
      }
    &amp;#x3C;/style&gt;
    &amp;#x3C;script src=&quot;js/test1.js&quot;&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;style&gt;
      div {
        background: lightpink;
      }
    &amp;#x3C;/style&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//test.js 和 test1.js相同，都是执行三秒
var now = new Date().getTime()

while (new Date().getTime() - now &amp;#x3C; 3000) {
  continue
}
console.log(&apos;js11 start&apos; + new Date().getTime())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行后我们会发现 &lt;code&gt;div&lt;/code&gt; 先被渲染成 &lt;code&gt;lightblue&lt;/code&gt;，三秒后被渲染成 &lt;code&gt;lightgray&lt;/code&gt;，再过三秒被渲染成 &lt;code&gt;lightpink&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;CSS&lt;/code&gt; 虽然不会阻塞 &lt;code&gt;DOM&lt;/code&gt; 解析，但是如果在 &lt;code&gt;CSS&lt;/code&gt; 后有一个 &lt;code&gt;JS&lt;/code&gt;，&lt;code&gt;CSS&lt;/code&gt; 会阻塞这个 &lt;code&gt;JS&lt;/code&gt;，而这个 &lt;code&gt;JS&lt;/code&gt; 会阻塞 &lt;code&gt;DOM&lt;/code&gt;，所以有时候会造成 &lt;code&gt;CSS&lt;/code&gt; 阻塞 &lt;code&gt;DOM&lt;/code&gt; 的错觉。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对这个阻塞分析进行一个总结：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;head&lt;/code&gt; 中 &lt;code&gt;link&lt;/code&gt; 只阻塞 &lt;code&gt;DOM&lt;/code&gt; 的渲染而不阻塞 &lt;code&gt;DOM&lt;/code&gt; 的解析。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;body&lt;/code&gt; 中的 &lt;code&gt;link&lt;/code&gt; 会同时组织 &lt;code&gt;DOM&lt;/code&gt; 的解析和渲染。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;link&lt;/code&gt; 无论在 &lt;code&gt;body&lt;/code&gt; 还是在 &lt;code&gt;head&lt;/code&gt; 中都会阻塞 &lt;code&gt;js&lt;/code&gt; 的执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;js&lt;/code&gt; 会阻塞 &lt;code&gt;DOM&lt;/code&gt; 的解析和渲染，&lt;code&gt;js&lt;/code&gt; 在加载过程中不会卡死页面，在执行过程中会卡死页面，无法进行交互。&lt;/li&gt;
&lt;li&gt;浏览器解析到非 &lt;code&gt;defer&lt;/code&gt; 和 &lt;code&gt;async&lt;/code&gt; 的 &lt;code&gt;script&lt;/code&gt; 标签会立即触发一次渲染&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;白屏时间&lt;/h2&gt;
&lt;p&gt;阻塞的第六种情况说到每次 &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; 的执行渲染引擎都会将当前已经解析的 &lt;code&gt;DOM&lt;/code&gt; 和 &lt;code&gt;CSSOM&lt;/code&gt; 立即合并成一棵 &lt;code&gt;Render&lt;/code&gt; 树进行一次渲染，而第一个 &lt;code&gt;body&lt;/code&gt; 中的 &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; 标签引发的渲染就是页面结束白屏的时间点。如果渲染引擎解析到第一个 &lt;code&gt;script&lt;/code&gt; 标签时，对应的 &lt;code&gt;js&lt;/code&gt; 文件还没有加载到本地，那么会立即进行渲染，如果已经加载好，那么会在 &lt;code&gt;JS&lt;/code&gt; 引擎执行完这段脚本之后立即进行渲染。下面的代码可以测试这个机制(依然是把 &lt;code&gt;network throttling&lt;/code&gt; 设置慢速，让css的加载时间较长以达到测试的效果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;UTF-8&quot; /&gt;
    &amp;#x3C;title&gt;Test&amp;#x3C;/title&gt;
    &amp;#x3C;script defer src=&quot;js/test.js&quot;&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div id=&quot;test&quot;&gt;test1&amp;#x3C;/div&gt;

    &amp;#x3C;script&gt;
      var start = new Date().getTime()
      while (new Date().getTime() - start &amp;#x3C; 500) {
        continue
      }
      console.log(&apos;time out&apos;)
    &amp;#x3C;/script&gt;
    &amp;#x3C;link
      rel=&quot;stylesheet&quot;
      href=&quot;https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css&quot;
    /&gt;
    &amp;#x3C;script&gt;
      console.log(123)
    &amp;#x3C;/script&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//test.js
console.log(&apos;domcontentloaded&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当timeout输出时，div正好被渲染出来，此时 &lt;code&gt;bootstrap&lt;/code&gt; 的 &lt;code&gt;css&lt;/code&gt; 依然在加载中，因为 &lt;code&gt;css&lt;/code&gt; 的加载阻塞了下面的 &lt;code&gt;console.log(123)&lt;/code&gt; 的执行，当css加载完毕后，&lt;code&gt;console.log(123)&lt;/code&gt; 执行，然后触发 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 时间，最后 &lt;code&gt;defer&lt;/code&gt; 属性的js执行。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;window&lt;/code&gt; 的 &lt;code&gt;performance&lt;/code&gt; 属性可以帮助我们了解页面的加载时间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;关于白屏时间和首屏时间我并没有找到非常严格的定义，一般来说是称为 &lt;code&gt;first paint&lt;/code&gt;，也就是说在白屏时间结束之前，页面上没有渲染出任何东西，这个在 &lt;code&gt;chrome devtools&lt;/code&gt; 中的 &lt;code&gt;performance&lt;/code&gt; 中都能看到， 总的规则就是如果第一脚本前的 &lt;code&gt;JS&lt;/code&gt; 和 &lt;code&gt;CSS&lt;/code&gt; 加载完了，&lt;code&gt;body&lt;/code&gt; 中的脚本还未下载完成，那么浏览器就会利用构建好的局部 &lt;code&gt;CSSOM&lt;/code&gt; 和 &lt;code&gt;DOM&lt;/code&gt; 提前渲染第一脚本前的内容（触发 &lt;code&gt;FP&lt;/code&gt;）；如果第一脚本前的 &lt;code&gt;JS&lt;/code&gt; 和 &lt;code&gt;CSS&lt;/code&gt; 都还没下载完成，&lt;code&gt;body&lt;/code&gt; 中的脚本就已经下载完了，那么浏览器就会在所有 &lt;code&gt;JS&lt;/code&gt; 脚本都执行完之后才触发 &lt;code&gt;FP&lt;/code&gt;。详细的分析可以看这篇文章 &lt;a href=&quot;https://cloud.tencent.com/developer/article/1124484&quot; title=&quot;Chrome的First Paint触发的时机探究&quot;&gt;Chrome的First Paint触发的时机探究&lt;/a&gt;，写的非常细致。&lt;/p&gt;
&lt;p&gt;其实特别纠结白屏时间也不必，我们最关注的还是页面从请求到能和用户交互的时间，这中间涉及的内容也很多，后面会单独在研究性能的文章中讨论。&lt;/p&gt;
&lt;h2&gt;defer和async的区别&lt;/h2&gt;
&lt;p&gt;上面有几段代码用了 &lt;code&gt;defer&lt;/code&gt; 属性，其实还有个属性 &lt;code&gt;async&lt;/code&gt;，这里说一下这两个属性。 1. &lt;code&gt;&amp;#x3C;script src=&quot;script.js&quot;&gt;&amp;#x3C;/script&gt;&lt;/code&gt;：没有 &lt;code&gt;defer&lt;/code&gt; 或 &lt;code&gt;async&lt;/code&gt;，浏览器会立即加载并执行指定的脚本，“立即”指的是在渲染该 &lt;code&gt;script&lt;/code&gt; 标签之下的文档元素之前，也就是说不等待后续载入的文档元素，读到就加载并执行。 2. &lt;code&gt;&amp;#x3C;script async src=&quot;script.js&quot;&gt;&amp;#x3C;/script&gt;&lt;/code&gt;：有 &lt;code&gt;async&lt;/code&gt;，加载和渲染后续文档元素的过程将和 &lt;code&gt;script.js&lt;/code&gt; 的加载并行进行（异步），一旦加载成功就中断渲染引擎的渲染，立即执行脚本。 3. &lt;code&gt;&amp;#x3C;script defer src=&quot;myscript.js&quot;&gt;&amp;#x3C;/script&gt;&lt;/code&gt;：有 &lt;code&gt;defer&lt;/code&gt;，加载后续文档元素的过程将和 &lt;code&gt;script.js&lt;/code&gt; 的加载并行进行（异步），但是 &lt;code&gt;script.js&lt;/code&gt; 的执行要在所有元素解析完成之后，&lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件触发之前完成。&lt;/p&gt;
&lt;p&gt;把所有脚本都丢到 &lt;code&gt;&amp;#x3C;/body&gt;&lt;/code&gt; 之前是最佳实践，因为对于旧浏览器来说这是唯一的优化选择，可以保证非脚本的其他一切元素能够以最快的速度得到加载和解析。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/defer-async.DRiZxlqD_Z1NjeuV.webp&quot; alt=&quot;defer-async&quot; title=&quot;defer-async&quot;&gt; 1. &lt;code&gt;defer&lt;/code&gt; 和 &lt;code&gt;async&lt;/code&gt; 在网络读取（加载）过程中是一样的，都是异步的（相较于 &lt;code&gt;HTML&lt;/code&gt; 解析） 2. 两者差别在于脚本加载完之后何时执行，显然 &lt;code&gt;defer&lt;/code&gt; 是最接近我们对于应用脚本加载和执行的要求的 3. &lt;code&gt;defer&lt;/code&gt; 是按照加载顺序执行脚本的 4. &lt;code&gt;async&lt;/code&gt; 则是乱序执行的，对它来说脚本的加载和执行是紧紧挨着的，所以不管你声明的顺序如何，只要它加载完了就会立刻执行 5. &lt;code&gt;async&lt;/code&gt; 对于应用脚本的用处不大，因为它完全不考虑依赖（哪怕是最低级的顺序执行），不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的，最典型的例子：&lt;code&gt;Google Analytics&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Event Loop&lt;/code&gt; 的分析见另一片文章 &lt;a href=&quot;https://www.clloz.com/programming/front-end/js/2020/11/01/event-loop/&quot; title=&quot;事件循环 Event Loop&quot;&gt;事件循环 Event Loop&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文只是阅读的文章结合自己的一点见解，只是浏览器的渲染过程的一点皮毛，而且在前端技术日新月异的今天，浏览器也在不断地进步和优化，想要更好的掌握其原理还需要不断学习。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/59c60691518825396f4f71a1&quot; title=&quot;https://juejin.im/post/59c60691518825396f4f71a1&quot;&gt;原来 CSS 与 JS 是这样阻塞 DOM 解析和渲染的&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/43282197&quot; title=&quot;https://zhuanlan.zhihu.com/p/43282197&quot;&gt;css加载会造成阻塞吗？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/q/1010000000640869&quot; title=&quot;https://segmentfault.com/q/1010000000640869&quot;&gt;defer和async的区别&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.dailichun.com/2018/01/21/js_singlethread_eventloop.html&quot; title=&quot;从浏览器多进程到JS单线程，JS运行机制最全面的一次梳理&quot;&gt;从浏览器多进程到JS单线程，JS运行机制最全面的一次梳理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/#Introduction&quot; title=&quot;浏览器的工作原理&quot;&gt;浏览器的工作原理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/5c35cf62f265da615e05a67d&quot; title=&quot;浏览器渲染原理（性能优化之如何减少重排和重绘）&quot;&gt;浏览器渲染原理（性能优化之如何减少重排和重绘）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/5b2a508ae51d4558de5bd5d1&quot; title=&quot;再谈 load 与 DOMContentLoaded&quot;&gt;再谈 load 与 DOMContentLoaded&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/browser.gq-LwpuH.jpg"/><enclosure url="/_astro/browser.gq-LwpuH.jpg"/></item><item><title>BOM</title><link>https://clloz.com/blog/bom</link><guid isPermaLink="true">https://clloz.com/blog/bom</guid><pubDate>Tue, 23 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;BOM&lt;/code&gt; ( &lt;code&gt;browser object model&lt;/code&gt; )并没有像 &lt;code&gt;DOM&lt;/code&gt; 一样有一个现行的标准，应该是为了对应 &lt;code&gt;DOM&lt;/code&gt; 而产生的一个概念，可以理解为浏览器向 &lt;code&gt;Javascript&lt;/code&gt; 暴露的控制浏览器行为的 &lt;code&gt;API&lt;/code&gt;，比如经常使用的 &lt;code&gt;window对象，location对象&lt;/code&gt;，因为主流浏览器都实现了这些属性和方法，所以被大家称为 &lt;code&gt;BOM&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;window对象&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;window&lt;/code&gt; 对象是 &lt;code&gt;BOM&lt;/code&gt; 中最重要的一个对象，它表示了一个浏览器实例，浏览器的每一个tag都有一个 &lt;code&gt;window&lt;/code&gt; 对象。 &lt;code&gt;window&lt;/code&gt; 对象是 &lt;code&gt;JavaScript&lt;/code&gt; 访问浏览器窗口属性和方法的接口，更重要的一点是它是 &lt;code&gt;ECMAScript&lt;/code&gt; 中 &lt;code&gt;Global&lt;/code&gt; 对象在浏览器环境中的实现。所有全局的变量，对象和方法都会自动的成为 &lt;code&gt;window&lt;/code&gt; 对象的成员，全局变量就是 &lt;code&gt;window&lt;/code&gt; 对象的属性，全局函数就是 &lt;code&gt;window&lt;/code&gt; 对象的方法，包括 &lt;code&gt;document&lt;/code&gt; 对象也是 &lt;code&gt;window&lt;/code&gt; 对象的一个属性(指向当前窗口对应的 &lt;code&gt;HTML&lt;/code&gt; 文档的引用），我们可以通过 &lt;code&gt;window.document&lt;/code&gt; 来访问这个只读属性。包括我们后面说到的 &lt;code&gt;history，navigator， location， screen&lt;/code&gt; 对象也都是 &lt;code&gt;window&lt;/code&gt; 对象的属性，这也体现了 &lt;code&gt;window&lt;/code&gt; 就是 &lt;code&gt;Global&lt;/code&gt; 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var age = 24

function printName() {
  console.log(age)
}

console.log(window.age) //24
window.printName() //24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BOM和DOM的关系如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/bom-dom.DJPBCKU0_Z2hhinJ.webp&quot; alt=&quot;dom-bom&quot; title=&quot;dom-bom&quot;&gt;&lt;/p&gt;
&lt;p&gt;可能有些同学发现我们平时使用 &lt;code&gt;location&lt;/code&gt; 对象是不加 &lt;code&gt;window.&lt;/code&gt; 的，在控制台打印它们也没有什么区别，返回的是同一个对象，其实理论上在全局执行环境中，&lt;code&gt;window&lt;/code&gt; 的属性是可以直接访问的，不加 &lt;code&gt;window.&lt;/code&gt; 前缀，但是在非全局执行环境中，也就是在函数中这样做就可能出错，因为如果你在函数中定义了一个同名的变量，由于这个变量在作用域链中的位置更靠前，所以输出的将会是你声明的这个变量。比如下图这种情况：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/window-scope.CpXwz1Bc_Z13oOwp.webp&quot; alt=&quot;window-scope&quot; title=&quot;window-scope&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用var声明的全局属性不能被 &lt;code&gt;delete&lt;/code&gt; 操作符删除，而通过 &lt;code&gt;window.property&lt;/code&gt; 来声明的属性则可以被删除。&lt;code&gt;let&lt;/code&gt; 和 &lt;code&gt;const&lt;/code&gt; 声明的的属性无论是否是全局的都不能被删除。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;窗口调用和控制&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;window&lt;/code&gt; 对象提供了一套操作窗口的属性和方法,比较常用的有如下几种：&lt;/p&gt;
&lt;p&gt;| 名称                      | 属性/方法 | 是否只读 | 功能                                                                                                   |
| ------------------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------ |
| window.innerHeight        | 属性      | 只读     | 返回浏览器窗口的内容区域的高度,包含水平滚动条(如果有的话)                                              |
| window.innerWidth         | 属性      | 只读     | 浏览器窗口的内容区域的宽度,包含垂直滚动条(如果有的话)                                                  |
| window.outerHeight        | 属性      | 只读     | 返回浏览器窗口的外部高度                                                                               |
| window.outerWidth         | 属性      | 只读     | 返回浏览器窗口的外部宽度                                                                               |
| window.closed             | 属性      | 只读     | 返回窗口是否关闭的布尔值                                                                               |
| window.document           | 属性      | 只读     | 返回当前窗口所包含的文档的引用                                                                         |
| window.localStorage       | 属性      | 只读     | 返回用来存储只能在创建它的源下访问的数据的本地存储对象的引用                                           |
| window.sessionStorage     | 属性      | 只读     | 返回用来存储只能在创建它的源下访问的数据的session storage对象的引用                                    |
| window.screen             | 属性      | 只读     | 返回screen对象的引用                                                                                   |
| window.screenX            | 属性      | 只读     | 返回浏览器左边框到显示器左边框的距离                                                                   |
| window.screenY            | 属性      | 只读     | 返回浏览器上边框到显示器上边框的距离                                                                   |
| window.scrollX            | 属性      | 只读     | 返回水平方向上页面滚动的像素数                                                                         |
| window.scrollY            | 属性      | 只读     | 返回垂直方向上页面滚动的像素数                                                                         |
| window.btoa()             | 方法      |          | 从 String 对象中创建一个 base-64 编码的 ASCII 字符串，其中字符串中的每个字符都被视为一个二进制数据字节 |
| window.atob()             | 方法      |          | 对用base-64编码过的字符串进行解码                                                                      |
| window.close()            | 方法      |          | 关闭当前窗口                                                                                           |
| window.getComputedStyle() | 方法      |          | 返回目标元素经浏览器计算后的样式对象，支持伪类                                                         |
| window.getSelection()     | 方法      |          | 返回一个 Selection 对象，表示用户选择的文本范围或光标的当前位置。                                      |
| window.prompt()           | 方法      |          | 显示一个输入框，点击确定返回用户输入字符串，点击取消返回null                                           |
| window.confirm()          | 方法      |          | 显示一个确认框，点击确定返回true，点击取消返回false                                                    |
| window.scroll()           | 方法      |          | 滚动窗口到指定的坐标，从初始坐标 &lt;code&gt;(0, 0)&lt;/code&gt; 开始                                                         |
| window.scrollBy()         | 方法      |          | 从当前位置滚动窗口一定距离                                                                             |
| window.scrollTo()         | 方法      |          | 和scroll()方法相同                                                                                     |
| window.stop()             | 方法      |          | 停止页面的加载                                                                                         |&lt;/p&gt;
&lt;p&gt;更全的 &lt;code&gt;Window&lt;/code&gt; 对象属性和方法查看&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;从 &lt;code&gt;Firefox 7&lt;/code&gt;，依据下面的规则，不能再调整浏览器内一个窗口的默认大小了：1. 不能调整非 &lt;code&gt;window.open&lt;/code&gt; 方法打开的窗口或 &lt;code&gt;Tab&lt;/code&gt; 的大小。2. 当一个窗口内包含有一个以上的 &lt;code&gt;Tab&lt;/code&gt; 时，不能调正窗口的大小。所以 &lt;code&gt;window.resizeBy和window.resizeTo&lt;/code&gt; 都无法使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;超时调用和间歇调用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;window&lt;/code&gt; 对象有两个方法 &lt;code&gt;setTimeout()&lt;/code&gt; 和 &lt;code&gt;setInterval()&lt;/code&gt;,可以让代码超时执行或者间歇调用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;setTimeout()&lt;/code&gt; 接收两个参数，要执行的代码和超时的时间。第一个参数要执行的时间可以是 &lt;code&gt;javascript&lt;/code&gt; 字符串（就和在 &lt;code&gt;eval()&lt;/code&gt; 函数中使用的字符串一样）也可以是匿名函数或者函数引用，和 &lt;code&gt;eval()&lt;/code&gt; 一样，不推荐用字符串的形式。虽然第二个参数叫做超时时间，但是并不意味着经过超时时间代码就会执行，而是经过超时时间，代码被加入执行的队列，如果前面的所有任务都执行完了，那么我们的函数也会立即执行，但如果队列里的任务还在执行中，那么我们要等待前面的任务执行完成才能执行。详细的内容会在 &lt;code&gt;event loop&lt;/code&gt; 的文章中分析，不过下一段代码可以表现出执行的规律。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function sleep(delay) {
  var start = new Date().getTime()
  while (new Date().getTime() - start &amp;#x3C; delay) {
    //死循环，只是在测试中使用
    continue
  }
}

function test() {
  console.log(&apos;111&apos;, new Date().getTime())
  setTimeout(function () {
    console.log(123, new Date().getTime())
  }, 3000) //设置超时时间为1000和3000看看有什么区别
  sleep(2000)
  console.log(222)
}

test()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;setTimeout&lt;/code&gt; 方法会返回一个超时调用的 &lt;code&gt;ID&lt;/code&gt;，这个超时调用 &lt;code&gt;ID&lt;/code&gt; 是计划执 行代码的唯一标识符，可以通过它来取消超时调用。要取消尚未执行的超时调用计划，可以调用 &lt;code&gt;clearTimeout()&lt;/code&gt; 方法并将相应的超时调用 &lt;code&gt;ID&lt;/code&gt; 作为参数传递给它。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;超时调用的代码都是在全局作用域中执行的，因此函数中 &lt;code&gt;this&lt;/code&gt; 的值在非严格模 式下指向 &lt;code&gt;window&lt;/code&gt; 对象，在严格模式下是 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;间歇调用与超时调用类似，只不过它会按照指定的时间间隔重复执行代码，直至间歇调用被取消或 者页面被卸载。一般认为，使用超时调用来模拟间歇调用的是一种最佳模式。在开 发环境下，很少使用真正的间歇调用，原因是后一个间歇调用可能会在前一个间歇调用结束之前启动。&lt;/p&gt;
&lt;h2&gt;location对象&lt;/h2&gt;
&lt;h2&gt;location 对象的属性&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;location&lt;/code&gt; 对象也是非常常用的 &lt;code&gt;BOM&lt;/code&gt; 对象，比如我们常用的 &lt;code&gt;location.href&lt;/code&gt; ,它提供了与当前窗口中加载的文档有关的信息，还提供了一 些导航功能。&lt;code&gt;window&lt;/code&gt; 和 &lt;code&gt;document&lt;/code&gt; 对象都有对 &lt;code&gt;location&lt;/code&gt; 对象的引用，也就是我们可以用 &lt;code&gt;window.location&lt;/code&gt; 和 &lt;code&gt;document.location&lt;/code&gt; 来访问 &lt;code&gt;location&lt;/code&gt; 对象。&lt;code&gt;location&lt;/code&gt; 对象的用处不只表现在它保存着当前文档的信息，还表现在它将 &lt;code&gt;URL&lt;/code&gt; 解析为独立的片段，让开发人员可以通过不同的属性访问这些片段。具体属性见下表：&lt;/p&gt;
&lt;p&gt;| 属性名   | 例子                      | 说明                                                                       |
| -------- | ------------------------- | -------------------------------------------------------------------------- |
| href     | &lt;code&gt;&quot;https://www.clloz.com&quot;&lt;/code&gt; | 返回当前页面的完整URL，&lt;code&gt;location.toString()&lt;/code&gt; 效果相同                      |
| protocol | &lt;code&gt;&quot;https:&quot;&lt;/code&gt;                | 返回当前页面使用的协议                                                     |
| host     | &lt;code&gt;&quot;www.clloz.com&quot;&lt;/code&gt;         | 返回当前页面域名，如有有端口号会加上&lt;code&gt;&quot;:port&quot;&lt;/code&gt;                              |
| hostname | &lt;code&gt;&quot;www.clloz.com&quot;&lt;/code&gt;         | 返回不带端口号的域名                                                       |
| port     | &lt;code&gt;&quot;8080&quot;&lt;/code&gt;                  | 返回当前页面的端口号，如果没有则返回空字符串                               |
| pathname | &lt;code&gt;&quot;/s/programming/&quot;&lt;/code&gt;       | 返回URL中的路径部分，开头有一个&lt;code&gt;/&lt;/code&gt;                                         |
| search   | &lt;code&gt;&quot;?q=javascript&quot;&lt;/code&gt;         | 返回url中的参数部分，开头有&lt;code&gt;?&lt;/code&gt;                                             |
| hash     | &lt;code&gt;#content&lt;/code&gt;                | 返回URL中的hash(#号后跟零或多个字符)，如果URL 中不包含散列，则返回空字符串 |&lt;/p&gt;
&lt;p&gt;&lt;code&gt;location&lt;/code&gt; 的这些属性都不是只读的，通过给这些属性赋值，我们能够改变当前的 &lt;code&gt;URL&lt;/code&gt; ，每个属性作用不同，我们假设初始 &lt;code&gt;URL&lt;/code&gt; 为 &lt;code&gt;https://www.clloz.com/study&lt;/code&gt;： 1. &lt;code&gt;location.hash = &quot;#section1&quot;;&lt;/code&gt;将URL变为&lt;code&gt;https://www.clloz.com/study/#section1&lt;/code&gt; 2. &lt;code&gt;location.search = &quot;?q=javascript&quot;;&lt;/code&gt;见URL变为&lt;code&gt;https://www.clloz.com/study/?q=javascript&quot;&lt;/code&gt; 3. &lt;code&gt;location.hostname = &quot;www.google.com&quot;&lt;/code&gt;将URL变为&lt;code&gt;https://www.google.com/study/&lt;/code&gt; 4. &lt;code&gt;location.pathname = &quot;dir&quot;&lt;/code&gt;将URL变为&lt;code&gt;https://www.clloz.com/dir/&lt;/code&gt; 5. &lt;code&gt;location.port = 8080&lt;/code&gt;将URL变为&lt;code&gt;https://www.clloz.com:8080/study/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;除了 &lt;code&gt;hash&lt;/code&gt; 属性，其他的四种属性每次被修改，当前页面都会以新 &lt;code&gt;URL&lt;/code&gt; 重载。&lt;/p&gt;
&lt;p&gt;除了属性给出的这些可以直接访问的信息外，我们也可以用对一些参数加工获得我们需要的内容，比如经常需要用到的，获取 &lt;code&gt;url&lt;/code&gt; 后的参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getQueryStringArgs() {
  var qs = location.search.length &gt; 0 ? location.search.substring(1) : &apos;&apos;
  var args = {}
  var items = qs.length ? qs.split(&apos;&amp;#x26;&apos;) : []
  var item = null,
    name = null,
    value = null
  var i = 0,
    len = items.length
  for (i = 0; i &amp;#x3C; len; i++) {
    item = items[i].split(&apos;=&apos;)
    name = decodeURIComponent(item[0])
    value = decodeURIComponent(item[1])
    if (name.length) {
      args[name] = value
    }
  }
  return args
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;location对象的方法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;location&lt;/code&gt; 对象有三个主要的方法： 1. &lt;code&gt;location.assign()&lt;/code&gt; : 接受一个 &lt;code&gt;URL&lt;/code&gt; 字符串作为参数，打开新的 &lt;code&gt;URL&lt;/code&gt; 并在浏览器的历史记录中生成一条记录，作用和对 &lt;code&gt;location.href&lt;/code&gt; 或者 &lt;code&gt;window.location&lt;/code&gt; 直接赋值没有区别。 2. &lt;code&gt;location.replace()&lt;/code&gt; : 接受一个 &lt;code&gt;URL&lt;/code&gt; 字符串作为参数，用该 &lt;code&gt;URL&lt;/code&gt; 替换当前的 &lt;code&gt;URL&lt;/code&gt; 并载入界面，与 &lt;code&gt;location.assign()&lt;/code&gt; 方法不同的是，用户无法利用后退按钮来回到替换前的页面. 3. &lt;code&gt;location.reload()&lt;/code&gt; ：有一个可选参数 &lt;code&gt;true&lt;/code&gt;，如果不传入参数，会用比较有效率的方式加载，如果页面没有改变，直接从缓存中加载。如果传入了参数 &lt;code&gt;true&lt;/code&gt;，会强制从服务器重新加载。&lt;/p&gt;
&lt;h2&gt;history对象&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;history&lt;/code&gt; 对象允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。主要有三个方法： 1. &lt;code&gt;history.go()&lt;/code&gt; ：接受一个数字参数，通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面。比如：参数为 &lt;code&gt;-1&lt;/code&gt; 的时候为上一页，参数为 &lt;code&gt;1&lt;/code&gt; 的时候为下一页. 当整数参数超出界限时( 原文为&lt;code&gt;When integerDelta is out of bounds&lt;/code&gt; )，例如: 如果当前页为第一页，前面已经没有页面了，我传参的值为 &lt;code&gt;-1&lt;/code&gt;，那么这个方法没有任何效果也不会报错。调用没有参数的 &lt;code&gt;go()&lt;/code&gt; 方法或者不是整数的参数时也没有效果。( 这点与支持字符串作为 &lt;code&gt;url&lt;/code&gt; 参数的 &lt;code&gt;IE&lt;/code&gt; 有点不同)。 2. &lt;code&gt;history.back()&lt;/code&gt;：前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于 &lt;code&gt;history.go(-1)&lt;/code&gt;. 3. &lt;code&gt;history.forward()&lt;/code&gt; ：在浏览器历史记录里前往下一页，用户可点击浏览器左上角的前进按钮模拟此方法. 等价于 &lt;code&gt;history.go(1)&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;navigator对象&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;navigator&lt;/code&gt; 对象表示用户代理的状态和标识。 它允许脚本查询它和注册自己进行一些活动。可以使用只读的 &lt;code&gt;window.navigator&lt;/code&gt; 属性检索 &lt;code&gt;navigator&lt;/code&gt; 对象。 具体内容查看&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator&quot; title=&quot;MDN-navigator&quot;&gt;MDN-navigator&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;BOM&lt;/code&gt; 对象是浏览器厂商实现的一套接口，没有形成标准，最主要的是 &lt;code&gt;window&lt;/code&gt; 对象，它不仅表示浏览器的实例，同时也是 &lt;code&gt;ECMAScript&lt;/code&gt; 中的&lt;code&gt;Global&lt;/code&gt;对象。掌握 &lt;code&gt;BOM&lt;/code&gt; 最主要的是知道 &lt;code&gt;BOM&lt;/code&gt; 以及 &lt;code&gt;DOM&lt;/code&gt; 的关系，以及对常用的属性和方法的熟练使用。&lt;/p&gt;
&lt;h2&gt;window对象属性方法图&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/window-properties.BhvMfaSe_15Sgbp.webp&quot; alt=&quot;window-properties&quot; title=&quot;window-properties&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>JS操作符拾遗</title><link>https://clloz.com/blog/js-operator-supplements</link><guid isPermaLink="true">https://clloz.com/blog/js-operator-supplements</guid><pubDate>Tue, 23 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;对于 &lt;code&gt;js&lt;/code&gt; 操作符的一些特性和有趣题目的整理。&lt;/p&gt;
&lt;h2&gt;逗号操作符&lt;/h2&gt;
&lt;p&gt;逗号操作符有两个作用，一个是用于当你想要在期望一个表达式的位置包含多个表达式时，可以使用逗号操作符。这个操作符最常用的一种情况是：&lt;code&gt;for&lt;/code&gt; 循环中提供多个参数。另一个使用逗号操作符的例子是在返回值前处理一些操作。如同下面的代码，只有最后一个表达式被返回，其他的都只是被求值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function myFunc() {
  var x = 0

  return ((x += 1), x) // the same of return ++x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;赋值多个变量用 &lt;code&gt;var a = 3, b = 4, c = 5;&lt;/code&gt; 和 &lt;code&gt;var a = 3; var b = 4; var c = 5;&lt;/code&gt; 本质并没有什么不同，只是一种编码习惯，用知乎上一位朋友的话说，前者是 &lt;code&gt;高耦合，低冗余&lt;/code&gt;，后者是 &lt;code&gt;低耦合，高冗余&lt;/code&gt;，后者更严谨一点，在维护的时候也不容易出错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;关于逗号的第二个用法，有时会引起一些现象，比如下面的代码就改变了函数中的this指向，因为表达式 &lt;code&gt;(0, obj.fun)&lt;/code&gt; 返回了 &lt;code&gt;obj.fun&lt;/code&gt; 的引用，相当于 &lt;code&gt;window&lt;/code&gt; 调用这个函数，所以 &lt;code&gt;this&lt;/code&gt; 发生了变化：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var obj = {
  fun: function () {
    console.log(this)
  }
}

obj.fun() //输出obj
;(0, obj.fun)() //输出window对象
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;链式赋值&lt;/h2&gt;
&lt;p&gt;我的赋值是可以用链式的方法把一个值赋值给多个变量，当所有变量都被声明，这样做并没有什么问题，但是如果是函数作用域内这样做有时候会引起一些奇怪的现象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function test() {
    var a;
    a = b = 3;
}
test();
console.log(b); //3
console.log(a); //报错
```html

从结果我们可以看出 `b` 成为了一个全局变量，要明白原因我们先要知道 `js`引擎是如何解析 `a = b =3;` 这条表达式的，我们知道赋值是基于右值的值给左值赋值。根据这个规则我们可以知道，引擎的第一步是把 `a` 当作左值，`b = 3` 当作右值，因为 `b = 3` 是一个表达式，算出值以后才能赋值，所以计算 `b = 3` 的值，此时引擎发现变量 `b` 并没有声明，也就是在当前执行环境的变量对象找不到这个变量，于是 `b` 被处理成了 `var b = 3`，成了一个全局变量。

&gt; 变量声明提前不能跨 `&amp;#x3C;script&gt;` 标签，更不能跨文件，也就是说变量提升仅仅是把当前js文件或标签中的 `var` 声明变量或 `function` 的声明提升到当前文件或标签的开头。并且当函数和变量重名的时候，被提升的声明会是函数。

上面的代码赋值表达式的右值都是 `js` 的基本类型，如果右值是对象的时候，情况会更复杂一些。因为对象被保存在堆中，我们对对象的赋值只是把对象的引用赋值给变量，而引用的变化不会引起对象的变化，堆中的对象不会因为变量之间引用的传递而发生改变，记住这一点就不太容易被迷惑。不过有时候情况比我们想象的还要复杂一下，比如下面这道经典的题目：

```javascript
var a  = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x);//undefined
console.log(b.x);//{n: 2};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果用上面那一套方式来想这道题，第一步左值为 &lt;code&gt;a.x&lt;/code&gt;，右值为 &lt;code&gt;a = {n: 1}&lt;/code&gt;，第二步计算 &lt;code&gt;a = {n: 2}&lt;/code&gt;，左值为&lt;code&gt;a&lt;/code&gt;，右值为 &lt;code&gt;{n: 2}&lt;/code&gt;，把 &lt;code&gt;{n: 2}&lt;/code&gt; 的引用给 &lt;code&gt;a&lt;/code&gt; 就可以了，然后赋值给第一步的左值，得到结果 &lt;code&gt;a.x = {n: 2}&lt;/code&gt;。流程其实没什么问题，但是在第一步的时候忘记一件事，就是 &lt;code&gt;a对象并没有x属性&lt;/code&gt;，而且 &lt;code&gt;.&lt;/code&gt;运算符的优先级是仅次于括号的，在还没开始进行赋值分析的时候，已经对 &lt;code&gt;a.x&lt;/code&gt; 进行了处理，就是在 &lt;code&gt;a&lt;/code&gt; 对象中加入 &lt;code&gt;x&lt;/code&gt; 属性，虽然此时并没有对该属性初始化，&lt;code&gt;a.x&lt;/code&gt; 应该为 &lt;code&gt;undefined&lt;/code&gt;。有了这个思路我们再按刚才的流程走一遍，当完成第二步 &lt;code&gt;a = {n: 2}&lt;/code&gt; 的时候 &lt;code&gt;a&lt;/code&gt; 的引用已经改变，此时 &lt;code&gt;a.x&lt;/code&gt; 代表的是 &lt;code&gt;{n:1, x: undefined}&lt;/code&gt; 中的 &lt;code&gt;x&lt;/code&gt;（可能有人疑惑 &lt;code&gt;a&lt;/code&gt; 已经改变引用，&lt;code&gt;a.x&lt;/code&gt; 中的 &lt;code&gt;a&lt;/code&gt; 应该也变了，但因为 &lt;code&gt;a.x&lt;/code&gt; 和 &lt;code&gt;a&lt;/code&gt; 在同一个表达式内，&lt;code&gt;a.x&lt;/code&gt; 已经在前面被引擎解析，&lt;code&gt;a.x&lt;/code&gt;此时应被理解成堆中对象的属性），最后把这个 &lt;code&gt;{n: 2}&lt;/code&gt; 的引用赋给 &lt;code&gt;x&lt;/code&gt; 属性。&lt;/p&gt;
&lt;p&gt;我们可以发现赋值运算的解析是从左向右，但是计算是从右向左的。链式赋值有时候会遇到这样的副作用，所以还是尽量避免使用。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Macbook Pro 更换 SSD</title><link>https://clloz.com/blog/macbook-pro-ssd</link><guid isPermaLink="true">https://clloz.com/blog/macbook-pro-ssd</guid><pubDate>Mon, 22 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我的 &lt;code&gt;macbook pro&lt;/code&gt; 是 &lt;code&gt;2015&lt;/code&gt; 款的(&lt;code&gt;early&lt;/code&gt;)，&lt;code&gt;ssd&lt;/code&gt; 只有 &lt;code&gt;128GB&lt;/code&gt;，当时买的时候买的最便宜的版本，空间着实不太够用，前两天无意间浏览到 &lt;code&gt;2015&lt;/code&gt; 款的可以自己更换 &lt;code&gt;ssd&lt;/code&gt;，在经过一番详细了解之后我决定自己来更换一块 &lt;code&gt;ssd&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;换 SSD 之前需要确定自己的系统在 10.13 以上，详细内容可以参考 &lt;a href=&quot;https://post.smzdm.com/p/a783vk9g/&quot; title=&quot;15款MacBook Pro升级1TB SSD 升级指南&quot;&gt;15款MacBook Pro升级1TB SSD 升级指南&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;工具&lt;/h2&gt;
&lt;p&gt;需要准备的工具如下： 1. 一个安装 &lt;code&gt;mac&lt;/code&gt; 系统的 &lt;code&gt;u盘&lt;/code&gt;（新的 &lt;code&gt;ssd&lt;/code&gt; 是无法识别的，必须要用 &lt;code&gt;u盘&lt;/code&gt; 引导安装） 2. &lt;code&gt;time machine&lt;/code&gt; 备份当前系统用于恢复在新的 &lt;code&gt;ssd&lt;/code&gt; 上 3. 内六角 &lt;code&gt;T4&lt;/code&gt; 和内五角 &lt;code&gt;P5&lt;/code&gt; 的螺丝刀，我买的米家的 &lt;code&gt;wiha&lt;/code&gt; 工具，&lt;code&gt;89&lt;/code&gt; 块。 4. 一块新的 &lt;code&gt;m.2&lt;/code&gt; 接口的 &lt;code&gt;ssd&lt;/code&gt;，我选择的是 &lt;code&gt;intel 760p 1T&lt;/code&gt; 的，网上说这款兼容性比较好。 5. 一个 &lt;code&gt;m.2 nvme&lt;/code&gt; 的转接器，因为 &lt;code&gt;mac&lt;/code&gt; 主板上的 &lt;code&gt;ssd&lt;/code&gt; 接口是不支持 &lt;code&gt;m.2&lt;/code&gt; 的，所以需要一个转接器，我在x宝上的迪奥科技买的，一般换 &lt;code&gt;ssd&lt;/code&gt; 教程都推荐这家，我用了以后一次点亮，价格也很便宜，&lt;code&gt;18&lt;/code&gt; 一个，运费 &lt;code&gt;6&lt;/code&gt; 块。 6. 一个聚酰亚胺胶带，这个纯属看的教程里面的，因为贴在主板上，要做好绝缘，很多人用的电工胶布，这款聚酰亚胺的胶带不仅绝缘而且耐高温，也非常薄，防患于未然把。&lt;/p&gt;
&lt;h2&gt;安装过程&lt;/h2&gt;
&lt;h2&gt;如何创建可引导的 macOS 安装器&lt;/h2&gt;
&lt;p&gt;我安装的是最新的 &lt;code&gt;macOS Mojave&lt;/code&gt; 系统，在 &lt;code&gt;app store&lt;/code&gt; 中搜索下载，下载完成后不要选择安装，此时你的 &lt;code&gt;Applications&lt;/code&gt; 文件夹中会有一个 &lt;code&gt;Install macOS Mojave.app&lt;/code&gt; 的文件，大小差不多 &lt;code&gt;6G&lt;/code&gt; 出头，然后根据 &lt;code&gt;mac&lt;/code&gt; 官方的&lt;a href=&quot;https://support.apple.com/zh-cn/HT201372&quot; title=&quot;网页&quot;&gt;网页&lt;/a&gt;在&lt;code&gt;terminal&lt;/code&gt;输入命令就可以了。&lt;/p&gt;
&lt;h2&gt;time machine备份&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;time machine&lt;/code&gt; 是&lt;code&gt;macOS&lt;/code&gt;自带的备份软件，在&lt;code&gt;launchpad&lt;/code&gt;里面的&lt;code&gt;other&lt;/code&gt;文件夹中可以找到，你需要一块移动硬盘，把当前系统的所有文件都写进去，需要注意的是移动硬盘会被格式化，如果你的移动硬盘里面有什么重要的文件，备份到其他地方。&lt;/p&gt;
&lt;h2&gt;购买工具&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ssd&lt;/code&gt; 我是在京东买的，&lt;code&gt;1T&lt;/code&gt; 的 &lt;code&gt;intel 760p&lt;/code&gt; 是 &lt;code&gt;1300&lt;/code&gt; 多，不是很贵。然后是胶布，螺丝刀，转接器都是在淘宝上买的，加在一起 &lt;code&gt;100&lt;/code&gt; 多。&lt;/p&gt;
&lt;h2&gt;开始更换&lt;/h2&gt;
&lt;p&gt;如果你的机器版本跟我相同&lt;code&gt;macbook pro 2015 early&lt;/code&gt;，那么你可以在&lt;a href=&quot;https://zh.ifixit.com/Guide/MacBook+Pro+13+%E8%8B%B1%E5%AF%B8%E9%85%8D%E5%A4%87+Retina+%E6%98%BE%E7%A4%BA%E5%B1%8F2015%E5%B9%B4%E6%97%A9%E6%9C%9F%E7%89%88SSD%E6%9B%B4%E6%8D%A2/38520&quot; title=&quot;ifixit&quot;&gt;ifixit&lt;/a&gt;上看到详细的更换教程，我只指出几点要注意的： 1. 背板上一共 &lt;code&gt;10&lt;/code&gt; 个螺丝，靠近显示器的一边 &lt;code&gt;4&lt;/code&gt; 个，中间两个，最下面 &lt;code&gt;4&lt;/code&gt; 个，其中靠近显示器那边的 &lt;code&gt;4&lt;/code&gt; 个的中间两个是和其他螺丝不同的比较短的，安装的时候记得区分。 2. 螺丝拆外以后从靠近显示器的那边揭开背板，是比较容易的，只要抓着靠着显示器那边的背板沿，稍稍用力就可以揭开（背板除了螺丝，就只有中间的两个卡扣固定，不是很紧，很容易揭开） 3. 换 &lt;code&gt;ssd&lt;/code&gt; 为了安全起见不要带电操作，把电源先断开，在&lt;code&gt;ifixit&lt;/code&gt;的那个页面也有看到，第一次拔那个电源可能有点紧，可以先在一边用力，会比较好拔一点，力气要稍微大一点。 4. 转接器的插入不是那么容易，要用点力，转接器和 &lt;code&gt;ssd&lt;/code&gt; 一定要插好，不然螺丝不好上（固定 &lt;code&gt;ssd&lt;/code&gt; 的就一个内五角螺丝） 5. 安装完成以后记得把电源线插回去&lt;/p&gt;
&lt;p&gt;如果你局的&lt;code&gt;ifixit&lt;/code&gt;上面的图片还是有点不直观，这里还有两个&lt;code&gt;YouTube&lt;/code&gt;视频，可以给你参考，注意的是，第一个视频的机型不是跟我的机型一样的，可能是更老一点的，具体型号我不太清楚，不过这个视频比较详细，包括系统的安装也告诉你了；第二个视频则是跟我型号相同的，不过两个视频都解释的很清楚，你可以作为参考。 1. &lt;a href=&quot;https://www.youtube.com/watch?v=5bCSczoWnek&quot; title=&quot;科技小巴跟换ssd&quot;&gt;科技小巴跟换ssd&lt;/a&gt; 2. &lt;a href=&quot;https://www.youtube.com/watch?v=TG0mJDwVpuk&quot; title=&quot;一个视频更换ssd&quot;&gt;一个视频更换ssd&lt;/a&gt; 基本把这两个视频看完你就知道具体流程了。&lt;/p&gt;
&lt;h2&gt;安装系统以及恢复&lt;/h2&gt;
&lt;p&gt;由于新装的 &lt;code&gt;ssd&lt;/code&gt; 系统是无法识别的，我们需要一个u盘安装器来引导安装并格式化新的 &lt;code&gt;ssd&lt;/code&gt;，插上 &lt;code&gt;u盘&lt;/code&gt;，启动电脑（第一次启动可能比较慢，不要着急）。系统会直接读取 &lt;code&gt;U盘&lt;/code&gt; 开始安装，在安装系统之前，先进入磁盘工具格式化自己的新 &lt;code&gt;ssd&lt;/code&gt;。格式化的格式选择按 &lt;code&gt;apple&lt;/code&gt; 官方的说法是&lt;code&gt;macOS 日志&lt;/code&gt;和&lt;code&gt;afps&lt;/code&gt;都可以，我原来的 &lt;code&gt;ssd&lt;/code&gt; 是&lt;code&gt;afps&lt;/code&gt;的，不过也有说要设置一下&lt;code&gt;GUID分区图&lt;/code&gt;的，所以我的做法是先格式化了一边&lt;code&gt;macOS日志&lt;/code&gt;，在格式化成&lt;code&gt;afps&lt;/code&gt;格式。&lt;/p&gt;
&lt;p&gt;格式化好 &lt;code&gt;ssd&lt;/code&gt; 以后此时 &lt;code&gt;ssd&lt;/code&gt; 已经能被系统识别的，我们退出磁盘工具，选择安装 &lt;code&gt;macOS&lt;/code&gt;，安装的位置就选择我们的新 &lt;code&gt;ssd&lt;/code&gt;，安装过程很快我感觉没 &lt;code&gt;10&lt;/code&gt; 分钟就安装好了。安装好系统以后就会进入到系统设置阶段（选择地区，键盘以及隐私政策），前面的直接下一步就行了，直到要你选择&lt;code&gt;是否要传输信息到这台mac&lt;/code&gt;，接上你的备份硬盘，选择第一个从时间机器传输，然后选择最新的备份就可以了，恢复系统的时间比较长，我的系统文件就 &lt;code&gt;60&lt;/code&gt; 多个G也恢复了一个小时，如果你的文件更多可能要更久。&lt;/p&gt;
&lt;p&gt;上面是我自己当时的做法，但是似乎不需要这么做，可以直接用 &lt;code&gt;time machine&lt;/code&gt; 进行系统的恢复，参考文章 &lt;a href=&quot;https://post.smzdm.com/p/a783vk9g/&quot; title=&quot;15款MacBook Pro升级1TB SSD 指南&quot;&gt;15款MacBook Pro升级1TB SSD 指南&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;不需要制作系统 &lt;code&gt;u&lt;/code&gt; 盘，只要在换 &lt;code&gt;SSD&lt;/code&gt; 之前用 &lt;code&gt;time machine&lt;/code&gt; 备份系统，换上新 &lt;code&gt;SSD&lt;/code&gt; 之后，开机按住 &lt;code&gt;option+command+R&lt;/code&gt; 直接进恢复模式，连上网，你什么都能干了，然后进磁盘工具把 &lt;code&gt;SSD&lt;/code&gt; 格了，记得选择 &lt;code&gt;APFS&lt;/code&gt; 分区，然后选择从 &lt;code&gt;time machine&lt;/code&gt; 恢复系统就可以了&lt;/p&gt;
&lt;h2&gt;重置SMC和NVRAM&lt;/h2&gt;
&lt;p&gt;很多教程都提示重置这两个东西，以防发生睡眠唤醒重启和耗电量大的问题，重启的方法也比较简单。 1. 重启 &lt;code&gt;SMC&lt;/code&gt;：等 &lt;code&gt;Mac&lt;/code&gt; 关机后，按下内建键盘左侧的 &lt;code&gt;Shift-Control-Option&lt;/code&gt;，然后同时按下电源按钮。按住这些按键和电源按钮 &lt;code&gt;10&lt;/code&gt; 秒钟。 2. 重启 &lt;code&gt;NVRAM&lt;/code&gt;：将 &lt;code&gt;Mac&lt;/code&gt; 关机，然后开机并立即同时按住以下四个按键：&lt;code&gt;Option&lt;/code&gt;、&lt;code&gt;Command&lt;/code&gt;、&lt;code&gt;P&lt;/code&gt; 和 &lt;code&gt;R&lt;/code&gt;。您可以在大约 &lt;code&gt;20&lt;/code&gt; 秒后松开这些按键，在此期间您的 &lt;code&gt;Mac&lt;/code&gt; 可能看似在重新启动。在发出启动声的 &lt;code&gt;Mac&lt;/code&gt; 电脑上，您可以在两次启动声之后松开这些按键。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这两个重启其实都没啥提示，我就按照 &lt;code&gt;apple&lt;/code&gt; 官方给出的操作做了，具体重启成功了没我也不知道，不过目前用着还没什么异常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;关于onedrive的特别说明&lt;/h2&gt;
&lt;p&gt;在系统恢复以后，和原来的系统几乎没有区别，连我自定义的快捷键和配置文件都一样可以用，不过 &lt;code&gt;onedrive&lt;/code&gt; 文件夹里面的文件都不能显示同步状态了，我试了好多方法，最后发现是要给 &lt;code&gt;onedrive extension&lt;/code&gt; 的权限，在 &lt;code&gt;system preference -&gt; extensions -&gt; finder extensions&lt;/code&gt; 中给 &lt;code&gt;onedrive&lt;/code&gt; 打上勾就可以了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;换上新的 &lt;code&gt;ssd&lt;/code&gt; 之后一下子空间就宽敞了，写入的速度会比原来的自带 &lt;code&gt;ssd&lt;/code&gt; 快一些，读取的速度差不太多，由于我刚还完，还不知道会不会有使用异常，不过温度倒是挺正常的。至于一些睡眠唤醒，重启以及耗电快的问题只能等用一段时间才知道了。这个 &lt;code&gt;ssd&lt;/code&gt; 的升级总体来说是非常简单的，基本没什么特别难的地方，就是插转换器的时候我一直以为没插进去，废了好大的劲。感觉换了 &lt;code&gt;ssd&lt;/code&gt; 之后这台机器又能再战一段时间了！&lt;/p&gt;</content:encoded><h:img src="/_astro/macos.RfaH1x-K.jpg"/><enclosure url="/_astro/macos.RfaH1x-K.jpg"/></item><item><title>Emacs常用快捷键</title><link>https://clloz.com/blog/emacs-keybinding</link><guid isPermaLink="true">https://clloz.com/blog/emacs-keybinding</guid><pubDate>Sat, 13 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;命令和快捷键系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/09/18/terminal-chrome-shortcurs/&quot; title=&quot;终端和chorme常用快捷键以及快捷键工具keycue&quot;&gt;终端和chorme常用快捷键以及快捷键工具keycue&lt;/a&gt;()&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/emacs/2019/04/14/emacs-keybinding/&quot; title=&quot;emacs常用快捷键&quot;&gt;emacs常用快捷键&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/05/15/git-command/&quot; title=&quot;常用Git命令&quot;&gt;常用Git命令&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/07/mac-pathnvm/&quot; title=&quot;Mac的环境变量和nvm的使用&quot;&gt;Mac的环境变量和nvm的使用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/09/08/homebrew-tsinghua-mirror/&quot; title=&quot;Homebrew更换清华镜像以及常用命令&quot;&gt;Homebrew更换清华镜像以及常用命令&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;回到mac上又开始用Emacs了，可能是先入为主，相较于&lt;code&gt;Vim&lt;/code&gt;我还是比较喜欢用&lt;code&gt;Emacs&lt;/code&gt;，虽然在大部分系统中都是默认装&lt;code&gt;Vim&lt;/code&gt;而没有&lt;code&gt;Emacs&lt;/code&gt;并且&lt;code&gt;Emacs&lt;/code&gt;用起来似乎更麻烦，不过我还是更喜欢&lt;code&gt;Emacs&lt;/code&gt;的风格。如果要把&lt;code&gt;Emacs&lt;/code&gt;作为一个日常工作中的主力工具，确实是非常折腾，特别是我对&lt;code&gt;Lisp&lt;/code&gt;不太了解，有时候为了解决一个小问题，可能要花半天，有可能还解决不了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Emacs&lt;/code&gt;算是一个比较小众的编辑器，虽然很多人对这种极客感觉的编辑器有点憧憬，但是实际的使用过程中，你想让它达到IDE的效果，是需要费相当大的功夫的，并且由于各种插件，软件以及系统的版本并不是由同一批人维护的，很可能你更新了其中一个部分，另一部分就报错了。而且&lt;code&gt;Emacs&lt;/code&gt;的社区并不算活跃，特别是中文，你很难在网上找到有用的文档，并且同样的问题可能每个人的原因并不相同，解决方法也不同。在不了解&lt;code&gt;elisp&lt;/code&gt;的情况下，你想要很好的驯服&lt;code&gt;Emacs&lt;/code&gt;是相当难的，不过&lt;code&gt;Emacs&lt;/code&gt;的现在的魅力也可能源于此把。其实&lt;code&gt;Emacs&lt;/code&gt;的配置说起来也简单，就一份&lt;code&gt;.emacs.d&lt;/code&gt;就可以，如果你真的只是当一个编辑器来使用可以像&lt;code&gt;Vim&lt;/code&gt;一样，只进行简单的配置就可以，或者你可以直接用别人的配置，但是虽然你用别人的插件或者配置，但是你的&lt;code&gt;Emacs&lt;/code&gt;配置维护者其实只有你，要像真的学好&lt;code&gt;Emacs&lt;/code&gt;是需要花费相当大的精力的，如果不是学习&lt;code&gt;Lisp&lt;/code&gt;我觉得并不适合把&lt;code&gt;Emacs&lt;/code&gt;作为主力开发工具。&lt;/p&gt;
&lt;h2&gt;Emacs主要快捷键&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;mode line&lt;/code&gt;中第一个字符表示字符集，&lt;code&gt;c&lt;/code&gt;代表&lt;code&gt;chinese-gbk&lt;/code&gt;，后面那个&lt;code&gt;\&lt;/code&gt;符号表示换行类型，&lt;code&gt;\&lt;/code&gt;是指&lt;code&gt;DOS&lt;/code&gt;的&lt;code&gt;CRLF&lt;/code&gt;换行，另外还有&lt;code&gt;Unix&lt;/code&gt;的&lt;code&gt;LF&lt;/code&gt;换行和&lt;code&gt;Mac&lt;/code&gt;的&lt;code&gt;CR&lt;/code&gt;换行。然后一个字符，表示打开的文件是否可写（先称为文件便于理解，实际上是buffer），&lt;code&gt;%&lt;/code&gt;表示只读，&lt;code&gt;-&lt;/code&gt;和 &lt;code&gt;*&lt;/code&gt; 表示可写。再一个字符表示文件是否已写，&lt;code&gt;%&lt;/code&gt; 或 &lt;code&gt;-&lt;/code&gt; 表示还没动，&lt;code&gt;*&lt;/code&gt; 表示已经更改。这两个字符组合起来有四个状态。&lt;/p&gt;
&lt;h2&gt;基本操作&lt;/h2&gt;
&lt;p&gt;| 命令    | 功能                       |
| ------- | -------------------------- |
| C-x C-f | 打开/新建文件              |
| C-x C-s | 保存当前缓冲区             |
| C-x C-w | 当前缓冲区另存为           |
| C-x C-v | 关闭当前Buffer并打开新文件 |
| C-x i   | 光标处插入文件             |
| C-x b   | 切换Buffer                 |
| C-x C-b | 显示Buffer列表             |
| C-x k   | 关闭当前Buffer             |
| C-x C-c | 关闭Emacs                  |
| C-c C-z | 终止shell中的进程          |&lt;/p&gt;
&lt;h2&gt;光标移动&lt;/h2&gt;
&lt;p&gt;| 按键          | 命令                | 功能                                      |
| ------------- | ------------------- | ----------------------------------------- |
| C-q tab / M-i |                     | 输入制表符                                |
| C-f           | forward-char        | 向前一个字符                              |
| C-b           | backward-char       | 向后一个字符                              |
| C-p           | previous-line       | 上移一行                                  |
| C-n           | next-line           | 下移一行                                  |
| M-f           | forward-word        | 向前一个单词                              |
| M-b           | backward-word       | 向后一个单词                              |
| C-a           | beginning-of-line   | 移到行首                                  |
| C-e           | end-of-line         | 移到行尾                                  |
| M-e           | forward-sentence    | 移到句首                                  |
| M-a           | backward-sentence   | 移到句尾                                  |
| M-}           | forward-paragraph   | 下移一段                                  |
| M-{           | backward-paragraph  | 上移一段                                  |
| C-v           | scroll-up           | 下移一屏                                  |
| M-v           | scroll-down         | 上移一屏                                  |
| C-x ]        | forward-page        | 下移一页                                  |
| C-x [        | backward-page       | 上移一页                                  |
| M-&amp;#x3C;           | beginning-of-buffer | 移到文档头                                |
| M-&gt;           | end-of-buffer       | 移到文档尾                                |
| M-g g num     | goto-line           | 移到第n行                                 |
| (none)        | goto-char           | 移到第n个字符                             |
| C-l           | recenter            | 将当前位置放到页面中间(Emacs最喜欢的地方) |
| M-num         | digit-argument      | 重复下个命令n次                           |
| C-u num       | universal-argument  | 重复下个命令n次，n默认为4                 |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在mac上多个emacs的快捷键是全局绑定的，在任何地方都可以使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;文本操作&lt;/h2&gt;
&lt;p&gt;| 按键            | 命令                    | 功能                                               |
| --------------- | ----------------------- | -------------------------------------------------- |
| C-x C-f         | find-file               | 打开文件                                           |
| C-x C-v         | find-alternate-file     | 打开另一个文件                                     |
| C-x C-s         | save-buffer             | 保存文件                                           |
| C-x C-w         | write-file              | 另存文件                                           |
| C-q (n)         | quoted-insert           | 插入字符，n表示字符的八进制ASCII码                 |
| C-x 8           | ucs-insert              | 插入Unicode字符                                    |
| C-d             | delete-char             | 删除光标处字符                                     |
| Backspace       | delete-backward-char    | 删除光标前字符                                     |
| M-d             | kill-word               | 删除光标起单词                                     |
| M-Backspace     | backward-kill-word      | 删除光标前单词                                     |
| C-k             | kill-line               | 删除光标起当前行                                   |
| M-k             | kill-sentence           | 删除光标起句子                                     |
| C-x Backspace   | backward-kill-sentence  | 删除光标前句子                                     |
| (none)          | kill-paragraph          | 删除光标起段落                                     |
| (none)          | backward-kill-paragraph | 删除光标前段落                                     |
| C-/             | undo                    | 撤销                                               |
| C-_            | undo                    | 撤销                                               |
| C-x u           | undo                    | 撤销                                               |
| C-g             | keyboard-quit           | 撤销命令                                           |
| C-h t           | help-with-tutorial      | 调出Emacs Tutorial                                 |
| C-h r           | info-emacs-manual       | 调出Emacs Manual                                   |
| C-h k (command) | describe-key            | 查看对应command帮助                                |
| C-o             | open-line               | 插入空行                                           |
| C-x C-o         | delete-blank-line       | 删除空行                                           |
| C-x z           | repeat                  | 重复前个命令                                       |
| C-@             | set-mark-command        | 设定标记                                           |
| C-x C-x         | exchange-point-and-mark | 交换标记和光标位置                                 |
| C-w             | kill-region             | 删除区域中内容                                     |
| C-x C-u         | upcase-region           | 将区域中字母改为大写                               |
| C-x C-l         | upcase-region           | 将区域中字母改为小写                               |
| C-x h           | mark-whole-buffer       | 全选                                               |
| C-x C-p         | mark-page               | 选取一页                                           |
| M-h             | mark-paragraph          | 选取一段                                           |
| M-@             | mark-word               | 选取一个单词                                       |
| C-@ C-@         |                         | 加入点到标记环                                     |
| C-u C-@         |                         | 在标记环中跳跃                                     |
| C-x C-@         | pop-global-mark         | 在全局标记环中跳跃                                 |
| (none)          | transient-mark-mode     | 非持久化标记模式                                   |
| M-              | delete-horizontal-space | 删除光标处的所有空格和Tab字符                      |
| M-SPC           | just-one-space          | 删除光标处的所有空格和Tab字符，但留下一个          |
| C-x C-o         | delete-blank-lines      | 删除光标周围的空白行，保留当前行                   |
| M-^             | delete-indentation      | 将两行合为一行，删除之间的空白和缩进               |
| C-S-Backspace   | kill-whole-line         | 删除整行                                           |
| C-w             | kill-region             | 删除区域                                           |
| M-w             | kill-ring-save          | 复制到kill 环，而不删除                            |
| M-z char        | zap-to-char             | 删至字符char为止                                   |
| C-y             | yank                    | 召回                                               |
| M-y             | yank-pop                | 召回前一个                                         |
| C-M-w           | append-next-kill        | 下一个删掉内容和上次删除合并                       |
| C-h v           | describe-variable       | 显示变量内容                                       |
| (none)          | append-to-buffer        | 将区域中内容加入到一个buffer中                     |
| (none)          | prepend-to-buffer       | 将区域中内容加入到一个buffer光标前                 |
| (none)          | copy-to-buffer          | 区域中内容加入到一个buffer中，删除该buffer原有内容 |
| (none)          | insert-buffer           | 在该位置插入指定的buffer中所有内容                 |
| (none)          | append-to-file          | 将区域中内容复制到一个文件中                       |
| (none)          | cua-mode                | 启用/停用CUA绑定                                   |
|                 | kill-read-only-ok       | 是否在只读文件启用kill 命令                        |
|                 | kill-ring               | kill环                                             |
|                 | kill-ring-max           | kill环容量                                         |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;除了&lt;code&gt;Del&lt;/code&gt;和&lt;code&gt;C-d&lt;/code&gt;其他的删除命令都会按顺序保存起来，用&lt;code&gt;C-y&lt;/code&gt;或者&lt;code&gt;M-y&lt;/code&gt;来取出，如果想更好的使用&lt;code&gt;undo&lt;/code&gt;功能，可以了解&lt;code&gt;undo tree&lt;/code&gt;，在&lt;code&gt;Emacs&lt;/code&gt;中一切皆可&lt;code&gt;undo&lt;/code&gt;，包括&lt;code&gt;undo&lt;/code&gt;本身也可以被&lt;code&gt;undo&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;查找替换操作&lt;/h2&gt;
&lt;p&gt;| 按键          | 命令                    | 功能                                         |
| ------------- | ----------------------- | -------------------------------------------- |
| C-s           | isearch-forward         | 向前进行增量查找                             |
| C-r           | isearch-backward        | 向后进行增量查找                             |
| M-c           |                         | (查找状态)切换大写敏感                       |
| C-j           | newline-and-indent      | (查找状态)输入换行符                         |
| M-Tab         | isearch-complete        | (查找状态)自动匹配                           |
| C-h C-h       |                         | (查找状态)进入查找帮助                       |
| C-w           |                         | (查找状态)将光标处单词复制到查找区域         |
| C-y           |                         | (查找状态)将光标处直到行尾内容复制到查找区域 |
| M-y           |                         | (查找状态)把kill 环中最后一项复制到查找区域  |
| C-M-w         |                         | (查找状态)删除查找区域最后一个字符           |
| C-M-y         |                         | (查找状态)将光标处字符复制到查找区域最后     |
| C-f           |                         | (查找状态)将光标处字符复制到查找区域最后     |
| C-s RET       | search-forward          | 向前进行简单查找                             |
| C-r RET       | search-backward         | 向后进行简单查找                             |
| M-s w         | isearch-forward-word    | 向前进行词组查找                             |
| M-s w RET     | word-search-forward     | 向前进行词组查找（非增量方式）               |
| M-s w C-r RET | word-search-backward    | 向后进行词组查找（非增量方式）               |
| C-M-s         | isearch-forward-regexp  | 向前进行正则查找                             |
| C-M-r         | isearch-backward-regexp | 向后进行正则查找                             |
|               | replace-string          | 全文替换                                     |
|               | replace-regexp          | 全文正则替换                                 |
| M-%           | query-replace           | 查找替换                                     |
|               | recursive-edit          | 进入递归编辑                                 |
|               | abort-recursive-edit    | 退出递归编辑                                 |
|               | top-level               | 退出递归编辑                                 |&lt;/p&gt;
&lt;h2&gt;M-%的回答&lt;/h2&gt;
&lt;p&gt;| 输入       | 响应                                |
| ---------- | ----------------------------------- |
| SPC 或者 y | 替换当前匹配并前进到下一个匹配处    |
| DEL 或者 n | 忽略此次匹配并前进到下一个匹配处    |
| .          | 替换当前匹配并退出                  |
| ,          | 替换当前匹配并停在此处，再按y后前进 |
| !          | 替换所有剩余匹配                    |
| ^          | 回到前一个匹配处                    |
| RET 或者 q | 直接退出                            |
| e          | 修改新字符串                        |
| C-r        | 进入递归编辑状态                    |
| C-w        | 删除当前匹配并进入递归编辑状态      |
| C-M-c      | 退出递归编辑状态，返回查找替换      |
| C-]       | 退出递归编辑状态，同时退出查找替换  |
| C-h        | 显示帮助                            |&lt;/p&gt;
&lt;h2&gt;窗口操作&lt;/h2&gt;
&lt;p&gt;| 按键      | 命令                                | 功能                                   |
| --------- | ----------------------------------- | -------------------------------------- |
| C-x 2     | split-window-vertically             | 垂直拆分窗口                           |
| C-x 3     | split-window-horizontally           | 水平拆分窗口                           |
| C-x o     | other-window                        | 选择下一个窗口                         |
| C-M-v     | scroll-other-window                 | 滚动下一个窗口                         |
| C-x 4 b   | switch-to-buffer-other-window       | 在另一个窗口打开缓冲                   |
| C-x 4 C-o | display-buffer                      | 在另一个窗口打开缓冲，但不选中         |
| C-x 4 f   | find-file-other-window              | 在另一个窗口打开文件                   |
| C-x 4 d   | dired-other-window                  | 在另一个窗口打开文件夹                 |
| C-x 4 m   | mail-other-window                   | 在另一个窗口写邮件                     |
| C-x 4 r   | find-file-read-only-other-window    | 在另一个窗口以只读方式打开文件         |
| C-x 0     | delete-window                       | 关闭当前窗口                           |
| C-x 1     | delete-other-windows                | 关闭其它窗口                           |
| C-x 4 0   | kill-buffer-and-window              | 关闭当前窗口和缓冲                     |
| C-x ^     | enlarge-window                      | 增高当前窗口                           |
| C-x {     | shrink-window-horizontally          | 将当前窗口变窄                         |
| C-x }     | enlarge-window-horizontally         | 将当前窗口变宽                         |
| C-x -     | shrink-window-if-larger-than-buffer | 如果窗口比缓冲大就缩小                 |
| C-x +     | balance-windows                     | 所有窗口一样高                         |
|           | windmove-right                      | 切换到右边的窗口(类似：up, down, left) |&lt;/p&gt;
&lt;h2&gt;buffer操作&lt;/h2&gt;
&lt;p&gt;| 按键            | 命令                          | 功能                               |
| --------------- | ----------------------------- | ---------------------------------- |
| C-x b           | switch-to-buffer              | 打开或新建一个缓冲                 |
| C-x 4 b         | switch-to-buffer-other-window | 在另一个window中打开或新建一个缓冲 |
| C-x 5 b         | switch-to-buffer-other-frame  | 在另一个frame中打开或新建一个缓冲  |
| C-x LEFT        | next-buffer                   | 移动到下一个缓冲                   |
| C-x RIGHT       | previous-buffer               | 移动到前一个缓冲                   |
| C-x C-b         | list-buffers                  | 显示所有缓冲                       |
| C-u C-x C-b     |                               | 显示映射到文件的缓冲               |
| C-x k           | kill-buffer                   | 关闭缓冲                           |
|                 | kill-some-buffers             | 关闭多个缓冲                       |
|                 | clean-buffer-list             | 关闭三天未使用的缓冲               |
| C-x C-q         | toggle-read-only              | 切换缓冲只读属性                   |
| C-u M-g M-g num |                               | 跳至前一缓冲num行                  |
|                 | rename-buffer                 | 重命名缓冲                         |
|                 | rename-uniquely               | 重命名缓冲，在其名后加数字         |
|                 | view-buffer                   | 只读方式打开缓冲                   |
|                 | buffer-menu                   | 打开Buffer Menu                    |
|                 | make-indirect-buffer          | 建立间接缓冲                       |
|                 | clone-indirect-buffer         | 建立当前缓冲的间接缓冲             |&lt;/p&gt;
&lt;p&gt;输入&lt;code&gt;M-x buffer-menu&lt;/code&gt;进入&lt;code&gt;buffer menu&lt;/code&gt;对&lt;code&gt;buffer&lt;/code&gt;进行管理，操作方式如下：&lt;/p&gt;
&lt;p&gt;| 按键                      | 功能                               | 备注      |
| ------------------------- | ---------------------------------- | --------- |
| SPC, n                    | 移动到下一项                       |           |
| p                         | 移动到上一项                       |           |
| d, k                      | 标记删除缓冲，并移动到下一项       | 按x后生效 |
| C-d                       | 标记删除缓冲，并移动到上一项       | 按x后生效 |
| s                         | 标记保存缓冲                       | 按x后生效 |
| x执行标记删除或保存的缓冲 |                                    |           |
| u                         | 取消当前缓冲的标记，并移动到下一项 |           |
| Backspace                 | 取消当前缓冲的标记，并移动到上一项 |           |
| ~                         | 设置缓冲为未修改                   |           |
| %                         | 切换缓冲的只读属性                 |           |
| 1                         | 将选中缓冲满窗口显示               |           |
| 2                         | 将选中缓冲显示在一半窗口中         |           |
| t                         | 缓冲用tags table 方式显示          |           |
| f, RET                    | 显示选择缓冲                       |           |
| o                         | 缓冲在新窗口显示，并选中该窗口     |           |
| C-o                       | 缓冲在新窗口显示，但不选中该窗口   |           |
| b                         | 将选中缓冲移动到最后一行           |           |
| m                         | 标记缓冲在新窗口显示               | 按v后生效 |
| v                         | 显示标记的缓冲                     |           |
| g                         | 刷新buffer menu                    |           |
| T                         | 切换显示文件关联缓冲               |           |
| q                         | 退出Buffer Menu                    |           |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是大部分功能是立即生效的，但像d,s,m这些只会起标记作用，在确认之后才会执行，而且按了这三个键后对应会在缓冲名前显示&quot;D&quot;, &quot;S&quot;, &quot;&gt;&quot; 三个符号用作提示。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;注释的使用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Emacs 25&lt;/code&gt; 引入了一个新的命令 &lt;code&gt;comment-line&lt;/code&gt; 就是注释当前行，默认快捷键是 &lt;code&gt;C-x C-;&lt;/code&gt;，在之前版本的 &lt;code&gt;Emacs&lt;/code&gt; 中只有一个 &lt;code&gt;comment-dwim （M-;）&lt;/code&gt;用来在当前行后面加上注释。我通常使用 &lt;code&gt;emacs&lt;/code&gt; 是在终端中使用，终端里面似乎类似 &lt;code&gt;C-; C-: C-&apos;&lt;/code&gt;这类的命令都无效，具体原因没查到，所以只能修改命令了，用 &lt;code&gt;(global-set-key (kbd &quot;C-;&quot;) &apos;comment-line)&lt;/code&gt; 来设置快捷键，可以对同一个命令设置多个快捷键，我对 &lt;code&gt;comment-line&lt;/code&gt; 设置了两个快捷键，分别是 &lt;code&gt;C-;&lt;/code&gt; 和 &lt;code&gt;C-c C-c&lt;/code&gt;，这样前面一个在 &lt;code&gt;GUI&lt;/code&gt; 中使用，后面一个在 &lt;code&gt;shell&lt;/code&gt; 中使用。&lt;/p&gt;
&lt;h2&gt;一点看法&lt;/h2&gt;
&lt;p&gt;其实我开始使用&lt;code&gt;Emacs&lt;/code&gt;是纯属好奇，觉得这样一个工具很极客，估计大部分人也是这样的，刚开始用一些大神的配置，但其实每个人对开发工具的需求不同，别人的配置很多东西是你用不到的，使用&lt;code&gt;Emacs&lt;/code&gt;应该尽量精简到日常要用的东西，装上太多用不到的东西难以维护，经常一个错误需要查半天，而且真的想要用好&lt;code&gt;Emacs&lt;/code&gt;，&lt;code&gt;Lisp&lt;/code&gt;是个绕不过去要学的东西，只有真的理解了&lt;code&gt;Emacs&lt;/code&gt;才能用好它，否则还是仅仅当个玩具来看把。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/robertzml/archive/2010/03/24/1692737.html&quot; title=&quot;Emacs学习教程&quot;&gt;Emacs学习教程&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/emacs-logo.CVWUvzc8.png"/><enclosure url="/_astro/emacs-logo.CVWUvzc8.png"/></item><item><title>shell中的引号和转义</title><link>https://clloz.com/blog/shell-quote-escape</link><guid isPermaLink="true">https://clloz.com/blog/shell-quote-escape</guid><pubDate>Fri, 12 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在学习正则表达式的时候，在 &lt;code&gt;shell&lt;/code&gt; 里面用 &lt;code&gt;egrep&lt;/code&gt; 来验证正则表达式，&lt;code&gt;egrep&lt;/code&gt; 的基本语法是 &lt;code&gt;egrep [regular expression] [file path]&lt;/code&gt;，结果会返回目标文件中能匹配到正则表达式的对应行，由于正则表达式中一些元字符在 &lt;code&gt;shell&lt;/code&gt; 中也是有特殊意义的，所以为了能正确匹配，需要用单引号 &lt;code&gt;&apos;&lt;/code&gt; 把正则表达式包含起来，但是如果正则表达式里面包含单引号呢，带着这个问题去查了一下，了解了一下shell中的不同的转义方式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我的 &lt;code&gt;shell&lt;/code&gt; 用的是 &lt;code&gt;Mac&lt;/code&gt; 上的 &lt;code&gt;bash&lt;/code&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;code&gt;bash&lt;/code&gt; 中的三种不同转义方式&lt;/h2&gt;
&lt;p&gt;| 字符       | 说明                                                                                                    |
| ---------- | ------------------------------------------------------------------------------------------------------- |
| 单引号 &lt;code&gt;&apos;&lt;/code&gt; | 又叫硬转义，其内部所有的shell 元字符、通配符都会被当作普通字符。注意，硬转义中不允许出现 &lt;code&gt;&apos;&lt;/code&gt; (单引号)。 |
| 双引号 &lt;code&gt;&quot;&lt;/code&gt; | 又叫软转义，其内部只允许出现特定的 &lt;code&gt;shell&lt;/code&gt; 元字符：&lt;code&gt;$&lt;/code&gt; 用于参数代换 ，反引号&lt;code&gt;`&lt;/code&gt; 用于命令代替        |
| 反斜杠&lt;code&gt;\&lt;/code&gt;  | 又叫转义，去除其后紧跟的元字符或通配符的特殊意义。                                                      |&lt;/p&gt;
&lt;p&gt;反引号 和 &lt;code&gt;$()&lt;/code&gt; 是一样的。在执行一条命令时，会先将反引号中或者是 &lt;code&gt;$()&lt;/code&gt; 中的语句当作命令执行一遍，再将结果加入到原命令中重新执行，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo `ls`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会先执行 &lt;code&gt;ls&lt;/code&gt; ，假设得到的是 &lt;code&gt;xx.sh&lt;/code&gt;，那么最后执行 &lt;code&gt;echo xx.sh&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;假设我们要匹配 &lt;code&gt;html&lt;/code&gt; 文件中的 &lt;code&gt;lang&lt;/code&gt; 为 &lt;code&gt;en&lt;/code&gt; 或 &lt;code&gt;zh&lt;/code&gt; 的行，那么我们可以用这样的正则 &lt;code&gt;lang=&quot;(en|zh)&quot;&lt;/code&gt; ，如果我们的正则表达式中没有出现单引号，那么在shell中用单引号包裹我们的正则表达式是最方便的一种做法 &lt;code&gt;&apos;lang=&quot;(en|zh)&quot;&apos;&lt;/code&gt;，单引号意味着其中的所有字符都被shell当作普通字符来处理。 但是如果你的html中都是用的单引号 &lt;code&gt;&amp;#x3C;html lang=&apos;en&apos;&gt;&lt;/code&gt;，那么用单引号shell就无法解析命令了，这个时候用双引号来代替原来的单引号，&lt;code&gt;&quot;lang=&apos;(en|zh)&apos;&quot;&lt;/code&gt; 来执行，需要注意的是如果使用双引号的话，&lt;code&gt;$&lt;/code&gt; 和 &lt;code&gt;`&lt;/code&gt; 不是普通字符，所以要在前面加上 &lt;code&gt;\&lt;/code&gt; 转义。&lt;/p&gt;
&lt;p&gt;或者还有一种方法，就是不用引号，所有的元字符全部用反斜杠转义，&lt;code&gt;lang=\&apos;\(en\|z。h\)\&apos;&lt;/code&gt;，这样也可以匹配到正确的文本，但是如果正则表达式比较负责，这样做就有点麻烦。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;因为正则表达式并没有一个统一的标准，所以在不同的环境和需求中实现都不相同，了解当前语言或者环境下正则表达式的语法是使用前必须要做的，比如在 &lt;code&gt;emacs&lt;/code&gt; 中使用正则表示不能像 &lt;code&gt;Perl正则表达式&lt;/code&gt; 中直接使用 &lt;code&gt;()和|&lt;/code&gt; 都需要用 &lt;code&gt;\&lt;/code&gt; 进行转义。&lt;/p&gt;</content:encoded><h:img src="/_astro/bash.DbLvWSLZ.png"/><enclosure url="/_astro/bash.DbLvWSLZ.png"/></item><item><title>Mac用scp上传或下载文件</title><link>https://clloz.com/blog/mac-scp</link><guid isPermaLink="true">https://clloz.com/blog/mac-scp</guid><pubDate>Tue, 09 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;由于 &lt;code&gt;Mac&lt;/code&gt; 上没有像 &lt;code&gt;windows&lt;/code&gt; 上的 &lt;code&gt;xftp&lt;/code&gt; 一样的软件，我搜索了一下也没什么好用的同类型软件，所以上传文件到服务器和下载都不是很方便，&lt;code&gt;Mac&lt;/code&gt; 自带了一个 &lt;code&gt;ftp&lt;/code&gt; 工具但是只能上传不能下载，新的 &lt;code&gt;Mac&lt;/code&gt; 系统也把 &lt;code&gt;telnet&lt;/code&gt; 和 &lt;code&gt;ftp&lt;/code&gt; 都移除了，我安装了个 &lt;code&gt;gnu&lt;/code&gt; 的 &lt;code&gt;Inetutils&lt;/code&gt; 来使用 &lt;code&gt;ftp&lt;/code&gt;，不过最后还是选择 &lt;code&gt;scp&lt;/code&gt; 了。因为 &lt;code&gt;scp&lt;/code&gt; 也是基于 &lt;code&gt;ssh&lt;/code&gt; 的，传输是加密的，不需要额外安装其他软件，这也许就是 &lt;code&gt;ftp&lt;/code&gt; 被新系统抛弃的原因把。&lt;/p&gt;
&lt;h2&gt;scp 的使用&lt;/h2&gt;
&lt;h2&gt;传输本地文件到服务器&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#scp 文件名 用户名@服务器ip:目标路径
scp /file-path/ root@server-ip:/file-path/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;传输本地文件夹到服务器&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;scp -r 文件夹目录 用户名@服务器ip:目标路径
scp -r /path/folder root@server-ip:/path/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;下载服务器文件到本地&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;scp 用户名@服务器ip:文件路径 目标路径
scp root@server-ip:/file-path/ /file-path/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;下载服务器文件夹到本地&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;scp -r 用户名@服务器ip:文件路径 目标路径
scp -r root@server-ip:/path/folder /path/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;如果服务器指定了端口&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;scp -P 端口号 文件路径 用户名@服务器ip:文件保存路径
scp -P port /file/path root@server-ip:/path/
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Mac无法写入文件到/usr</title><link>https://clloz.com/blog/sip</link><guid isPermaLink="true">https://clloz.com/blog/sip</guid><pubDate>Tue, 09 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;今天在 &lt;code&gt;Mac&lt;/code&gt; 上安装 &lt;code&gt;php&lt;/code&gt; 的环境，在安装 &lt;code&gt;Xdebug&lt;/code&gt; 的时候，复制文件到 &lt;code&gt;/usr/local&lt;/code&gt; 的时候，一直提示我 &lt;code&gt;permission denied&lt;/code&gt;，去手动创建文件夹也不行，到 &lt;code&gt;google&lt;/code&gt; 上看了一下才了解了 &lt;code&gt;System Integrity Protection&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;System Integrity Protection&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mac OS X&lt;/code&gt; 从 &lt;code&gt;10.11&lt;/code&gt; 以上开始默认开启 &lt;code&gt;SIP&lt;/code&gt;（ &lt;code&gt;System Integrity Protection&lt;/code&gt; ，系统完整性保护），即使是 &lt;code&gt;root&lt;/code&gt; 用户也没有权限修改 &lt;code&gt;/System /bin /usr /sbin&lt;/code&gt;，但是安装一些程序的时候我们不得不在这些文件夹中写入内容，解决的方法是关闭 &lt;code&gt;SIP&lt;/code&gt;，关闭方法如下： 1. 重启 &lt;code&gt;Mac&lt;/code&gt;，按住 &lt;code&gt;Command + R&lt;/code&gt;，进入 &lt;code&gt;recovery&lt;/code&gt; 模式;（不要等 &lt;code&gt;apple&lt;/code&gt; 图标亮了再按） 2. 选择打开 &lt;code&gt;Utilities&lt;/code&gt; 下的终端; 3. 输入 &lt;code&gt;csrutil disable&lt;/code&gt; 并回车; 4. 最后重启 &lt;code&gt;Mac&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;我在修改了这个属性之后，还是不能写入，这时候修改一下文件夹的权限：&lt;code&gt;sudo chown -R $(whoami) /usr/local&lt;/code&gt;就可以了。 如果安装完软件之后你想重新打开 &lt;code&gt;SIP&lt;/code&gt;，按上面的1，2步骤重新进入安全模式，然后在terminal里面输入 &lt;code&gt;csrutil enable&lt;/code&gt;，在重启电脑就可以了，不过我重启了两次才说生效。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;我想 &lt;code&gt;apple&lt;/code&gt; 设计这个功能肯定是又目的的，如果是开发人员，经常需要在这几个文件夹内操作，一直开着也无妨，如果不是就开着把。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.howtogeek.com/230424/how-to-disable-system-integrity-protection-on-a-mac-and-why-you-shouldnt/&quot; title=&quot;How to Disable System Integrity Protection on a Mac (and Why You Shouldn’t)&quot;&gt;How to Disable System Integrity Protection on a Mac (and Why You Shouldn’t)&lt;/a&gt; &lt;a href=&quot;https://segmentfault.com/q/1010000003095378?_ea=301917&quot; title=&quot;Mac 更改/usr/bin 目录权限失败&quot;&gt;Mac 更改/usr/bin 目录权限失败&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/automator.B4hb3j-r.png"/><enclosure url="/_astro/automator.B4hb3j-r.png"/></item><item><title>ssh登陆服务器locale警告</title><link>https://clloz.com/blog/ssh-locale</link><guid isPermaLink="true">https://clloz.com/blog/ssh-locale</guid><pubDate>Tue, 09 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 上 &lt;code&gt;ssh&lt;/code&gt; 登陆服务器一直有一个 &lt;code&gt;warning: setlocale: LC_CTYPE: cannot change locale (en_US.UTF-8): No such file or directory.&lt;/code&gt; 警告，看着很不舒服，来解决一下。&lt;/p&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;/etc/etc/environment&lt;/code&gt; 中加入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;LANG=en_US.utf-8
LC_ALL=en_US.utf-8
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000004378075&quot; title=&quot;CentOS 下解决ssh登录 locale 警告&quot;&gt;CentOS 下解决ssh登录 locale 警告&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>词法分析中的token</title><link>https://clloz.com/blog/lexical-analysis-token</link><guid isPermaLink="true">https://clloz.com/blog/lexical-analysis-token</guid><pubDate>Sun, 07 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;JS的正则表达式有两种声明方法，一种是构造函数 &lt;code&gt;var reg = new RegExp(pattern, flag);&lt;/code&gt;，另一种是字面量 &lt;code&gt;var reg = / pattern / flag&lt;/code&gt;，其中字面量表示法是用斜杠来分割正则表达式。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;JS&lt;/code&gt; 中斜杠&lt;code&gt;/&lt;/code&gt;表示除法，正则表达式，反斜杠 &lt;code&gt;\&lt;/code&gt; 一般用来转义，&lt;code&gt;Linux&lt;/code&gt; 中斜杠 &lt;code&gt;/&lt;/code&gt; 是路径分隔符， &lt;code&gt;Windows&lt;/code&gt; 中反斜杠 &lt;code&gt;\&lt;/code&gt; 是路径分隔符，要区分正斜杠和反斜杠只要记住正斜杠是撇，反斜杠是捺就行了。不过我很好奇字面量表示法表示正则表达式的时候，&lt;code&gt;JS&lt;/code&gt; 引擎是如何分析这个字面量的，斜杠有可能是除法、正则或者单纯就是一个字符，虽然是编译原理的知识，不过我还是去了解一下词法分析中的 &lt;code&gt;token&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;词法分析&lt;/h2&gt;
&lt;p&gt;词法分析是编译过程的第一步工作，将字符流转换为单词序列，输出到中间文件中，这个中间文件将会作为语法分析程序的输入，进行下一步工作。&lt;/p&gt;
&lt;p&gt;我们写的程序在形式上也只是有限个字符的排列，不同的语言对于排列的规则都有不同的约束，就是为了让词法分析器能够按照这个规则来对我们所写的源代码进行分析，分析的第一步就是词法分析，将字符流转换为编程语言中不可再分的单词序列，识别输入文件中的关键字、分隔符、标识符、数字、运算符、注释等。大小写不敏感，字母为 &lt;code&gt;a~z&lt;/code&gt;, &lt;code&gt;A~Z&lt;/code&gt;，数字为 &lt;code&gt;0~9&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;token&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;token&lt;/code&gt; 中文好像翻译为标记，是一个字符串，也是构成源代码的最小单位，从输入字符流中生成标记的过程叫作标记化（ &lt;code&gt;tokenization&lt;/code&gt; ），在这个过程中，词法分析器还会对标记进行分类。编译器会从左到右扫描我们的源代码，将其中的字符流分割成一个一个的 &lt;code&gt;token&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;token 类别&lt;/h2&gt;
&lt;p&gt;|     Token Key      |             Token Value              |
| :----------------: | :----------------------------------: | -------- |
|       Space        |                 空格                 |
|     Separator      |       分隔符( ;，{，}，(，) )        |
| single-ch operator | 单字符运算符( =，+，-，*，/，%，;， | ， :，!) |
|  two-ch operator   |        双字符运算符(:=，!=，)        |
|        num         |                 数字                 |
|     less equal     |                  &amp;#x3C;=                  |
|         NE         |                  &amp;#x3C;&gt;                  |
|     less than      |                  &amp;#x3C;                   |
|   greater equal    |                 &gt;=                  |
|    greater than    |                  &gt;                  |
|   reserved word    |                保留字                |
|     identifier     |                标识符                |
|       string       |                字符串                |
|      comment       |          注释（单行、多行）          |
|       error        |  一些错误情况，例如数字开头的字符串  |
|       other        |       其他符号（换行、制表等）       |&lt;/p&gt;
&lt;h2&gt;标记化&lt;/h2&gt;
&lt;p&gt;比如 &lt;code&gt;sum = 3 + 2;&lt;/code&gt; 经过标记化后会得到&lt;/p&gt;
&lt;p&gt;| 语素 | 标记类型   |
| ---- | ---------- |
| sum  | 标志符     |
| =   | 赋值操作符 |
| 3    | 数字       |
| +    | 加法操作符 |
| 2    | 数字       |
| ;    | 语句结束   |&lt;/p&gt;
&lt;p&gt;标记经常使用正则表达式进行定义，语法分析器读取输入字符流、从中识别出语素、最后生成不同类型的标记。其间一旦发现无效标记，便会报错。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;对编译原理一窍不通，只是在写代码的过程中经常好奇编译器或者解释器是如何理解这些字符的，好奇心也经常害自己钻进牛角尖，特别是在知识储备严重不足的情况下，不过好奇心也是学习知识的动力，能够分析出当前哪些问题是以自己目前的能力能够理解和解决的尤为重要，把精力花在当前的能力无法触及的领域会浪费大量时间。本文只是自己搜索的一些资料的总结，可能理解有很多错误之处。&lt;/p&gt;</content:encoded><h:img src="/_astro/lexical-analysis-process.DM_qJWWE.png"/><enclosure url="/_astro/lexical-analysis-process.DM_qJWWE.png"/></item><item><title>Mac的环境变量和nvm的使用</title><link>https://clloz.com/blog/mac-pathnvm</link><guid isPermaLink="true">https://clloz.com/blog/mac-pathnvm</guid><pubDate>Sun, 07 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;命令和快捷键系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/09/18/terminal-chrome-shortcurs/&quot; title=&quot;终端和chorme常用快捷键以及快捷键工具keycue&quot;&gt;终端和chorme常用快捷键以及快捷键工具keycue&lt;/a&gt;()&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/emacs/2019/04/14/emacs-keybinding/&quot; title=&quot;emacs常用快捷键&quot;&gt;emacs常用快捷键&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/05/15/git-command/&quot; title=&quot;常用Git命令&quot;&gt;常用Git命令&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/04/07/mac-pathnvm/&quot; title=&quot;Mac的环境变量和nvm的使用&quot;&gt;Mac的环境变量和nvm的使用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.clloz.com/programming/assorted/2019/09/08/homebrew-tsinghua-mirror/&quot; title=&quot;Homebrew更换清华镜像以及常用命令&quot;&gt;Homebrew更换清华镜像以及常用命令&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;今天在学习正则表达式，在测试 &lt;code&gt;零宽度正回顾后发断言&lt;/code&gt; 的时候，我发现 &lt;code&gt;node&lt;/code&gt; 提示我正则表达式错误，于是我去 &lt;code&gt;chrome&lt;/code&gt; 上试了一下，没问题。之后 &lt;code&gt;google&lt;/code&gt; 发现，&lt;code&gt;ES2019&lt;/code&gt; 才开始支持 &lt;code&gt;零宽度正回顾后发断言&lt;/code&gt;，但是既然 &lt;code&gt;chrome&lt;/code&gt; 已经支持了，没道理 &lt;code&gt;nodejs&lt;/code&gt; 不支持，而且我看文档里也说了 &lt;code&gt;v8&lt;/code&gt; 已经支持。于是我猜测我的 &lt;code&gt;node&lt;/code&gt; 版本应该是过低了。&lt;/p&gt;
&lt;h2&gt;更新 node&lt;/h2&gt;
&lt;p&gt;因为之前在家一直用的台式机，这台 &lt;code&gt;mac&lt;/code&gt; 上的东西很久没更新了，&lt;code&gt;node&lt;/code&gt; 的版本停留在 &lt;code&gt;8.11.0&lt;/code&gt;，于是着手更新，先把 &lt;code&gt;nvm&lt;/code&gt; 更新了一下，安装了最新的 &lt;code&gt;v11.13.0&lt;/code&gt;，但是在 &lt;code&gt;nvm alias default v11.13.0&lt;/code&gt; 之后，发现虽然 &lt;code&gt;default&lt;/code&gt; 已经变成最新版本，但是指针依然指向 &lt;code&gt;system&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;google&lt;/code&gt; 之后在 &lt;code&gt;github&lt;/code&gt; 的 &lt;code&gt;issue&lt;/code&gt; 里发现有人提了同样的问题，最后得出的结论就是在 &lt;code&gt;nvm&lt;/code&gt; 的环境变量之后，又把系统原来的 &lt;code&gt;node&lt;/code&gt; 在环境变量中写了一遍，也就是 &lt;code&gt;nvm&lt;/code&gt; 的 &lt;code&gt;default&lt;/code&gt; 又被系统的覆盖了，我打开 &lt;code&gt;~/.bash_profile&lt;/code&gt; 看了一下，果然 &lt;code&gt;nvm&lt;/code&gt; 的 &lt;code&gt;source ~/.nvm/nvm.sh&lt;/code&gt; 写在了最前面，后面紧跟着 &lt;code&gt;export PATH=/usr/local/bin:$PATH&lt;/code&gt; 也就是我系统 &lt;code&gt;node&lt;/code&gt; 的位置，把 &lt;code&gt;nvm&lt;/code&gt; 的位置移动了一下就一切正常了。&lt;/p&gt;
&lt;h2&gt;Mac 的环境变量&lt;/h2&gt;
&lt;p&gt;我们在系统中安装了某个程序，就可以使用这个程序对应的命令，之所以会这样是因为我们把该命令添加到了系统的环境变量中，也就是我们输入对应的命令，系统知道到哪个路径去找对应的文件执行。比如在 &lt;code&gt;Windows&lt;/code&gt; 中我们在 &lt;code&gt;path&lt;/code&gt; 中添加 &lt;code&gt;jdk&lt;/code&gt; 和 &lt;code&gt;jre&lt;/code&gt; 的 &lt;code&gt;bin&lt;/code&gt; 路径，我们在命令行中就可以使用 &lt;code&gt;java&lt;/code&gt; 和 &lt;code&gt;javac&lt;/code&gt; 命令了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 中同样需要设置环境变量，但是情况要比 &lt;code&gt;Windows&lt;/code&gt; 中复杂一些，&lt;code&gt;Mac&lt;/code&gt; 中又多个环境变量文件，不同的环境变量文件的作用域和加载时间也不同。而且不同的 &lt;code&gt;Shell&lt;/code&gt; 所对应的配置文件也不同。比如 &lt;code&gt;Mac&lt;/code&gt; 默认的 &lt;code&gt;bash&lt;/code&gt; 对应的配置文件就是 &lt;code&gt;.bash_profile&lt;/code&gt;，而 &lt;code&gt;zsh&lt;/code&gt; 对应的配置文件就是 &lt;code&gt;.zshrc&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;rc&lt;/code&gt; 即为 &lt;code&gt;run command&lt;/code&gt;，一般为脚本类文件的后缀，这些脚本通常在程序启动的时候被调用，比如 &lt;code&gt;.bashrc&lt;/code&gt; 就会在 &lt;code&gt;bash shell&lt;/code&gt; 启动时调用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;Mac&lt;/code&gt; 中环境变量配置文件的默认加载顺序如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/etc/profile
/etc/bashrc
/etc/paths
~/.bash_profile
~/.bash_login
~/.profile
~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;/etc/profile&lt;/code&gt; &lt;code&gt;/etc/bashrc&lt;/code&gt; 和 &lt;code&gt;/etc/paths&lt;/code&gt; 是系统级环境变量，对所有用户都有效。但它们的加载时机有所区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/profile&lt;/code&gt; 任何用户登陆时都会读取该文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/bashrc&lt;/code&gt; &lt;code&gt;bash shell&lt;/code&gt;执行时，不管是何种方式，读取此文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/paths&lt;/code&gt; 任何用户登陆时都会读取该文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.profile&lt;/code&gt; 为系统的每个用户设置环境信息,当用户第一次登录时,该文件被执行.并从 &lt;code&gt;/etc/profile.d&lt;/code&gt; 目录的配置文件中搜集&lt;code&gt;shell&lt;/code&gt;的设置。 如果你有对 &lt;code&gt;/etc/profile&lt;/code&gt; 有修改的话必须得重启你的修改才会生效，此修改对每个用户都生效。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;./bashrc&lt;/code&gt; 每一个运行 &lt;code&gt;bash shell&lt;/code&gt; 的用户执行此文件。当b&lt;code&gt;ash shell&lt;/code&gt;被打开时,该文件被读取。 对所有的使用 &lt;code&gt;bash&lt;/code&gt; 的用户修改某个配置并在以后打开的 &lt;code&gt;bash&lt;/code&gt; 都生效的话可以修改这个文件，修改这个文件不用重启，重新打开一个 &lt;code&gt;bash&lt;/code&gt; 即可生效。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;./bash_profile&lt;/code&gt; 该文件包含专用于你的 &lt;code&gt;bash shell&lt;/code&gt; 的 &lt;code&gt;bash&lt;/code&gt; 信息,当登录时以及每次打开新的 &lt;code&gt;shell&lt;/code&gt; 时,该文件被读取.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了前三个，后面几个是当前用户级的环境变量。&lt;code&gt;macOS&lt;/code&gt; 默认用户环境变量配置文件为 &lt;code&gt;~/.bash_profile&lt;/code&gt;，&lt;code&gt;Linux&lt;/code&gt; 为 &lt;code&gt;~/.bashrc&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果不存在 &lt;code&gt;~/.bash_profile&lt;/code&gt;，则可以自己创建一个 &lt;code&gt;~/.bash_profile&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;~/.bash_profile&lt;/code&gt; 文件存在，则后面的几个文件就会被忽略。 如果 &lt;code&gt;~/.bash_profile&lt;/code&gt; 文件不存在，才会以此类推读取后面的文件。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果使用的是 &lt;code&gt;shell&lt;/code&gt; 类型是&lt;code&gt;zsh&lt;/code&gt;，则还可能存在对应的 &lt;code&gt;/etc/zshrc&lt;/code&gt; 和 &lt;code&gt;~/.zshrc&lt;/code&gt;。任何用户登录&lt;code&gt;zsh&lt;/code&gt; 的时候，都会读取该文件。某个用户登录的时候，会读取其对应的 &lt;code&gt;~/.zshrc&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;添加新的环境变量&lt;/h2&gt;
&lt;p&gt;添加环境变量的语法：&lt;code&gt;export PATH=&quot;$PATH:&amp;#x3C;PATH 1&gt;:&amp;#x3C;PATH 2&gt;:&amp;#x3C;PATH 3&gt;:...:&amp;#x3C;PATH N&gt;&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;$PATH&lt;/code&gt; 相当于原来的环境变量，后面的 &lt;code&gt;PATH 1&lt;/code&gt;，&lt;code&gt;PATH2&lt;/code&gt; 就是我们新添加的变量，不同于 &lt;code&gt;Windows&lt;/code&gt; 用分号分隔，这里是用冒号分隔。&lt;code&gt;$PATH&lt;/code&gt; 可以放在前面，也可以放在后面，放在前面相当于把新的环境变量加在了环境变量的末尾，而放在后面则相当于把新的环境变量加在了开头。比如我上面的&lt;code&gt;nvm&lt;/code&gt;的问题，除了把 &lt;code&gt;nvm&lt;/code&gt; 的配置放到后面，把 &lt;code&gt;export PATH=/usr/local/bin:$PATH&lt;/code&gt; 改为 &lt;code&gt;export PATH=$PATH:/usr/local/bin&lt;/code&gt; 应该也是可以解决的，不过 &lt;code&gt;/usr/local/bin&lt;/code&gt; 中的文件较多，还是动 &lt;code&gt;nvm&lt;/code&gt; 比较好。&lt;/p&gt;
&lt;p&gt;查看 &lt;code&gt;path&lt;/code&gt;：&lt;code&gt;echo $PATH&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;nvm的常用命令&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;nvm&lt;/code&gt; 安装的 &lt;code&gt;node&lt;/code&gt; 路径为 &lt;code&gt;~/.nvm/versions/node&lt;/code&gt;（&lt;code&gt;mac&lt;/code&gt; 上）.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nvm ls-remote&lt;/code&gt;：查看当前支持的版本，&lt;code&gt;LTS&lt;/code&gt; 会特别标注，选择自己需要的版本安装。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm install v～&lt;/code&gt;：安装对应版本的 &lt;code&gt;node&lt;/code&gt;，&lt;code&gt;nvm instal node&lt;/code&gt; 会安装最新版本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm use &amp;#x3C;version&gt;&lt;/code&gt; : 切换当前环境使用的 &lt;code&gt;node&lt;/code&gt; 版本，关闭当前的 &lt;code&gt;shell&lt;/code&gt; 以后失效。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm current&lt;/code&gt; ：查看当前环境正在使用的 &lt;code&gt;node&lt;/code&gt; 版本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm alias default v～&lt;/code&gt; ：指定默认版本，关闭环境后仍然有效。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm use node&lt;/code&gt; ：切换到当前安装的版本中最新的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm alias &amp;#x3C;name&gt; v～&lt;/code&gt; ：为某个版本设置别名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm unalias &amp;#x3C;name&gt;&lt;/code&gt; ：取消设置的别名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm reinstall-packages &amp;#x3C;version&gt;&lt;/code&gt;：在当前版本 &lt;code&gt;node&lt;/code&gt; 环境下，重新全局安装指定版本号的全局安装包&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvm uninstall &amp;#x3C;version&gt;&lt;/code&gt;：卸载指定的版本&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;环境变量是每个操作系统关键的部分，搞清楚不同系统的环境变量规则在遇到软件出现异常时能够第一时间找出原因。&lt;code&gt;nvm&lt;/code&gt; 的使用是相当方便的，多个版本之间互不影响，相当于在自己的沙盒中，我自己是认为比 &lt;code&gt;n&lt;/code&gt; 好用的。之前一直在 &lt;code&gt;CentOS&lt;/code&gt; 上，许久不用 &lt;code&gt;emacs&lt;/code&gt;，已经快忘记怎么用了，&lt;code&gt;Mac&lt;/code&gt; 上能够配置 &lt;code&gt;emacs client&lt;/code&gt; 还是挺方便的，等过段时间有空闲了把 &lt;code&gt;emacs&lt;/code&gt; 拿出来好好练一练。&lt;/p&gt;</content:encoded><h:img src="/_astro/macos.RfaH1x-K.jpg"/><enclosure url="/_astro/macos.RfaH1x-K.jpg"/></item><item><title>JS运算符优先级和表达式</title><link>https://clloz.com/blog/operator-precedence</link><guid isPermaLink="true">https://clloz.com/blog/operator-precedence</guid><pubDate>Fri, 05 Apr 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;运算符优先级决定了表达式中运算执行的先后顺序，优先级越高的运算符会先执行。&lt;/p&gt;
&lt;h2&gt;运算符优先级&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/operation.BT8muJ9X_Z293ICa.webp&quot; alt=&quot;operator-precedence&quot; title=&quot;operator-precedence&quot;&gt;&lt;/p&gt;
&lt;p&gt;很多时候由于对运算符优先级的不确定，会用括号来确保表达式的执行顺序。如果搞清楚运算符的优先级，即使不用括号也能确保表达式按照正确的顺序执行。&lt;/p&gt;
&lt;p&gt;其实单纯靠运算符的优先级来确定表达式的执行过程并不是一个绝对稳妥的做法，比如 &lt;code&gt;new a()[&apos;b&apos;]&lt;/code&gt; 这样的表达式就没法套用运算符优先级。当然一般情况下，绝大多数表达式我们都可以利用运算符优先级来判断，如果确实遇到比较复杂的难以直观判断出来的情况还是要查询 &lt;code&gt;ECMAScript262&lt;/code&gt; 标准中的产生式。对于 &lt;code&gt;new&lt;/code&gt;，函数调用，成员访问三者结合的情况，比如 &lt;code&gt;new obj1.obj2.obj3.fun().prop&lt;/code&gt; 这样的情况，我个人总结就是函数调用直接到前面的 &lt;code&gt;new&lt;/code&gt; 先计算，得到的结果在进行成员访问。&lt;/p&gt;
&lt;h2&gt;表达式&lt;/h2&gt;
&lt;h2&gt;左手表达式&lt;/h2&gt;
&lt;p&gt;左手表达式即 &lt;code&gt;Left-Hand-Side Expression&lt;/code&gt;，即能出现在赋值运算左边的表达式。几乎所有的左手表达式都可以作为右手表达式，这在大部分编程语言中都是通用的。&lt;code&gt;JS&lt;/code&gt; 中的左手表达式的详细规则请看&lt;a href=&quot;https://www.ecma-international.org/ecma-262/11.0/index.html#sec-left-hand-side-expressions&quot; title=&quot;ECMAScript262-left-hand-side expression&quot;&gt;ECMAScript262-left-hand-side expression&lt;/a&gt;。标准中没有定义右手表达式，因为合法的表达式只要不是左手表达式就是右手表达式，所以不需要在单独定义。有一点需要注意的是 &lt;code&gt;++&lt;/code&gt; 和 &lt;code&gt;--&lt;/code&gt; 的操作数也要是一个左手表达式。&lt;/p&gt;
&lt;h2&gt;一些细节&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;关联性和优先级一样重要，关联性决定了拥有相同优先级的运算符的执行顺序。&lt;/li&gt;
&lt;li&gt;优先级为 &lt;code&gt;19&lt;/code&gt; 的除了图中 &lt;code&gt;member运算&lt;/code&gt;、&lt;code&gt;带参数new&lt;/code&gt; ，还有带标签的模版字符串（可以理解为和函数调用类似），&lt;code&gt;new.target&lt;/code&gt;,&lt;code&gt;super作为对象使用（和成员访问相同）&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;函数调用的优先级在图中也是 &lt;code&gt;19&lt;/code&gt;，但实际似乎优先级是要低于 &lt;code&gt;member运算&lt;/code&gt; 和单参数 &lt;code&gt;new&lt;/code&gt; 运算符的。当函数调用后接 &lt;code&gt;member&lt;/code&gt; 运算符整个式子还是函数调用等级的。&lt;/li&gt;
&lt;li&gt;一元加、一元减和算术运算符的加减是不同的，一元加和一元减都会讲操作数转为 &lt;code&gt;Number&lt;/code&gt;，这也是它可以将函数声明转为函数表达式的原因。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/javascript-logo.BFzkmCOS.jpg"/><enclosure url="/_astro/javascript-logo.BFzkmCOS.jpg"/></item><item><title>JS中的执行环境和作用域链</title><link>https://clloz.com/blog/execution-contextscope-chain</link><guid isPermaLink="true">https://clloz.com/blog/execution-contextscope-chain</guid><pubDate>Sat, 23 Mar 2019 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JS&lt;/code&gt; 中的执行环境和作用域链是非常重要的概念，它们是 &lt;code&gt;JS&lt;/code&gt; 引擎在处理 &lt;code&gt;JS&lt;/code&gt; 代码的时候对变量和函数的处理方式，这两个概念的正确理解能够帮助我们更好地理解和预测代码的行为。&lt;/p&gt;
&lt;h2&gt;执行环境&lt;/h2&gt;
&lt;p&gt;执行环境定义了变量或者函数有权访问的数据集合，每一个执行环境都有一个与之关联的变量对象，该执行环境中定义的所有变量和函数都保存在这个对象中。我们无法直接访问这个对象，这个对象只是在解析器处理数据的时候使用。&lt;/p&gt;
&lt;p&gt;我们平时说的全局变量就是在最外围的一个执行环境中定义的变量，全局执行环境根据 &lt;code&gt;ECMAScript&lt;/code&gt; 的不同实现而有不同的表示，在 &lt;code&gt;Web&lt;/code&gt; 浏览器中，全局执行环境就是 &lt;code&gt;window&lt;/code&gt; 对象，所有的全局变量和函数就是作为 &lt;code&gt;window&lt;/code&gt; 对象的属性和方法创建的。在 &lt;code&gt;nodejs&lt;/code&gt; 的实现中，全局执行环境就是&lt;code&gt;global&lt;/code&gt; 对象。 除了全局执行环境，每个函数都有自己的执行环境，当执行流进入一个函数时，函数的环境就会被推入一个环境栈中，而函数执行之后，栈将其环境弹出，把控制权返回给之前的执行环境。也就是说某个执行环境中的代码全部执行完毕之后，该环境就被销毁，保存在其中的所有变量和函数定义也随之销毁，全局执行环境直到应用程序额推出——例如网页或浏览器被关闭时才被销毁。&lt;/p&gt;
&lt;h2&gt;作用域链&lt;/h2&gt;
&lt;p&gt;前面说到每个执行环境都有一个变量对象来保存环境中定义的变量和函数，环境是层层嵌套的，所以当代码进入到一个新的环境开始执行时，会创建变量对象的一个作用域链，把嵌套的执行环境之间的变量对象做一个有序的联系。作用域链最主要的作用是确保当前执行环境有权访问的变量和函数，并且有序地查找。在作用域链的最前端始终是当前正在执行的代码所处的执行环境的变量对象，如果这个环境是一个函数，就把函数的活动对象作为其变量对象，在函数中没有定义新的变量时，这个活动独享就是函数的 &lt;code&gt;arguments&lt;/code&gt; 对象。作用域链的下一个变量都西昂来自于当前执行环境的包含环境，依次类推，逐层嵌套，知道全局执行环境；全局执行环境的变量对象始终都是作用域链中的最后一个都对象。&lt;/p&gt;
&lt;p&gt;当我们的代码在执行的时候，遇到的每一个标识符解析都会沿着作用域链一级一级地进行搜索，从作用域链的前端（当前执行环境的变量对象）逐级向后回溯，知道找到标识符为止，如果在作用域链上没有找到这个标识符，通常会导致错误。我们经常遇到的 &lt;code&gt;Uncaught ReferenceError: x is not defined&lt;/code&gt; 就是这个错误在浏览器中的表现。&lt;/p&gt;
&lt;p&gt;JS解释器在执行时会将变量和函数进行声明提前，在声明函数的时候，会给函数一个 &lt;code&gt;[[scope]]&lt;/code&gt; 属性，这个属性中包含了当前函数所有包含环境的变量对象，也就是我们的函数在声明提前的时候就已经生成了他的包含环境的作用域链了，然后当函数执行的时候会把自己的 &lt;code&gt;arguments&lt;/code&gt; 和内部定义的函数和变量打包成一个变量对象加到 &lt;code&gt;scope chain&lt;/code&gt; 的最后。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;函数参数也被当做变量来对待，因此起访问规则与执行环境中的其他变量相同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;作用域链的这种特性理解起来其实也是比较直观的，但是在实际的代码中由于情况非常多，有时候有些行为还是比较反直觉或者说容易产生误解的。比如下面的情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;作用域链看的是函数定义的位置而不是执行的位置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var x = 10
bar()
function foo() {
  console.log(x)
}
function bar() {
  var x = 30
  foo()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子里面，可能会有人误以为 &lt;code&gt;bar()&lt;/code&gt; 会输出 &lt;code&gt;30&lt;/code&gt;，我们只要理解函数其实是保存在堆中，我们给函数命名只是一个指向函数堆中地址的一个引用，当我们执行函数的时候根据这个引用去堆中找对应的函数执行。所以无论我们在哪里执行函数，函数的位置都是不变的，我们看作用域链也是，我们确定作用域链不是看函数是在哪里执行，而是要看函数是在哪里定义，作用域链可以认为是函数声明时就已经生成了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;个人认为 &lt;code&gt;ECMAScript&lt;/code&gt; 这样处理作用域链是为了作用域链能够保持不变而不用一直维护，并且根据环境的嵌套保持一致性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;闭包&lt;/h2&gt;
&lt;p&gt;除了全局执行环境的变量对象是始终存在的，其他局部函数的变量对象都只在函数的执行过程中存在，一般来讲，函数执行完毕之后，局部活动对象就被销毁了，内存中仅仅保存全局执行环境的变量对象，但是闭包的情况是不同的。&lt;/p&gt;
&lt;p&gt;闭包指的是有权访问另一个函数作用域中的变量的函数，比如下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function outer() {
  var scope = &apos;outer&apos;
  return function () {
    return scope
  }
}
var fn = outer()
fn()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在一个函数内部定义的函数会将包含函数（即外部函数）的活动对象添加到他的作用域链中，因此在 &lt;code&gt;outer&lt;/code&gt; 函数内部定义的匿名函数（我们下面把这个匿名函数称为 &lt;code&gt;inner&lt;/code&gt; 函数）的作用域链中，实际上会包含外部函数 &lt;code&gt;outer()&lt;/code&gt; 的活动对象，下图可以看书当代码执行时，&lt;code&gt;outer&lt;/code&gt; 和 &lt;code&gt;inner&lt;/code&gt; 函数的作用域链。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://img.clloz.com/blog/writing/scope-chain1.png&quot; title=&quot;scope-chain1&quot;&gt;&lt;img src=&quot;https://clloz.com/_astro/scope-chain.DamagMj2_Z263Hat.webp&quot; alt=&quot;scope-chain1&quot; title=&quot;scope-chain1&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;当匿名函数从 &lt;code&gt;outer()&lt;/code&gt; 中被返回后，&lt;code&gt;inner()&lt;/code&gt; 函数仍然可以访问在 &lt;code&gt;outer()&lt;/code&gt; 中定义的所有变量，也就是说，当 &lt;code&gt;outer()&lt;/code&gt; 函数执行完毕后，其活动对象也不会被销毁，因为匿名函数的作用域链依然在引用这个活动对象。换句话说，当 &lt;code&gt;outer()&lt;/code&gt; 函数执行完毕返回后，其执行环境和作用域链都被销毁，但它的活动对象依然保存在内存中，如果匿名函数不销毁，则这个活动对象会一直存在于内存中。&lt;/p&gt;
&lt;p&gt;js中的对象都是保存在堆中，我们在代码中写的都是对对象的引用，作用域链中也是，所以上面说的 &lt;code&gt;outer()&lt;/code&gt; 函数执行完毕后作用域链被销毁但是对象还存在，其实销毁的只是引用， &lt;code&gt;js&lt;/code&gt; 中的垃圾处理机制的一种策略是引用计数，当某个变量或对象的引用次数为 &lt;code&gt;0&lt;/code&gt; 的时候内存会被收回。&lt;code&gt;outer&lt;/code&gt; 函数的变量对象的引用有两个一个是 &lt;code&gt;outer&lt;/code&gt; 的作用域链和匿名函数的作用域链，所以只要匿名函数不被销毁，这个引用就一直存在，&lt;code&gt;outer()&lt;/code&gt; 的活动对象也会一直存在。&lt;/p&gt;
&lt;p&gt;轮子哥在知乎给过一个比较容易理解的说法：“闭”的意思不是封闭内部状态，而是封闭外部状态，一个函数如何能够封闭外部状态呢，当外部状态的 &lt;code&gt;scope&lt;/code&gt; 失效的时候，它自己还保留了一份。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://img.clloz.com/blog/writing/scope-chain2.png&quot; title=&quot;scope-chain2&quot;&gt;&lt;img src=&quot;https://clloz.com/_astro/scope-chain2.-LBwSEXK_Z1xAom1.webp&quot; alt=&quot;scope-chain2&quot; title=&quot;scope-chain2&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于闭包会携带包含它的函数的作用域，因此回避其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多，只在必要的时候使用闭包。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;ES3 执行上下文&lt;/h2&gt;
&lt;p&gt;对于 ES3 中的执行上下文，我们可以用下面这个列表来概括程序执行的整个过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;函数被调用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在执行具体的函数代码之前，创建了执行上下文&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入执行上下文的创建阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;初始化作用域链&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 arguments object 检查上下文中的参数，初始化名称和值并创建引用副本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;扫描上下文找到所有函数声明：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于每个找到的函数，用它们的原生函数名，在变量对象中创建一个属性，该属性里存放的是一个指向实际内存地址的指针&lt;/li&gt;
&lt;li&gt;如果函数名称已经存在了，属性的引用指针将会被覆盖&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;扫描上下文找到所有 var 的变量声明：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于每个找到的变量声明，用它们的原生变量名，在变量对象中创建一个属性，并且使用 undefined 来初始化&lt;/li&gt;
&lt;li&gt;如果变量名作为属性在变量对象中已存在，则不做任何处理并接着扫描&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定 this 值&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入执行上下文的执行阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在上下文中运行/解释函数代码，并在代码逐行执行时分配变量值。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;ES5 执行上下文&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ES6&lt;/code&gt; 将变量对象（活动对象）拆分成了两个概念：词法环境组件（ &lt;code&gt;LexicalEnvironment component&lt;/code&gt;） 和 变量环境组件（ &lt;code&gt;VariableEnvironment component&lt;/code&gt;）。简单来说 词法环境是一种持有 &lt;strong&gt;标识符—变量映射&lt;/strong&gt; 的结构。这里的 标识符指的是变量/函数的名字，而变量是对实际对象（包含函数类型对象）或原始数据的引用。变量环境也是一个词法环境 ，所以它有着词法环境的所有特性。之所以在 &lt;code&gt;ES5&lt;/code&gt; 的规范力要单独分出一个变量环境的概念是为 &lt;code&gt;ES6&lt;/code&gt; 服务的： 在 &lt;code&gt;ES6&lt;/code&gt; 中，词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量（&lt;code&gt;let&lt;/code&gt; 和 &lt;code&gt;const&lt;/code&gt;）绑定，而后者只用来存储 &lt;code&gt;var&lt;/code&gt; 变量绑定。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在上下文创建阶段，引擎检查代码找出变量和函数声明，变量最初会设置为 &lt;code&gt;undefined&lt;/code&gt;（&lt;code&gt;var&lt;/code&gt; 情况下），或者未初始化（&lt;code&gt;let&lt;/code&gt; 和 &lt;code&gt;const&lt;/code&gt; 情况下）。这就是为什么你可以在声明之前访问 &lt;code&gt;var&lt;/code&gt; 定义的变量（虽然是 &lt;code&gt;undefined&lt;/code&gt;），但是在声明之前访问 let 和 const 的变量会得到一个引用错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于 ES5 中的执行上下文，我们可以用下面这个列表来概括程序执行的整个过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;程序启动，全局上下文被创建&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建全局上下文的 词法环境&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建 对象环境记录器 ，它用来定义出现在 全局上下文 中的变量和函数的关系（负责处理 let 和 const 定义的变量）&lt;/li&gt;
&lt;li&gt;创建 外部环境引用，值为 null&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建全局上下文的 变量环境&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建 对象环境记录器，它持有 变量声明语句 在执行上下文中创建的绑定关系（负责处理 var 定义的变量，初始值为 undefined 造成声明提升）&lt;/li&gt;
&lt;li&gt;创建 外部环境引用，值为 null&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定 this 值为全局对象（以浏览器为例，就是 window ）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;函数被调用，函数上下文被创建&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建函数上下文的 词法环境&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建 声明式环境记录器 ，存储变量、函数和参数，它包含了一个传递给函数的 arguments 对象（此对象存储索引和参数的映射）和传递给函数的参数的 length。（负责处理 let 和 const 定义的变量）&lt;/li&gt;
&lt;li&gt;创建 外部环境引用，值为全局对象，或者为父级词法环境（作用域）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建函数上下文的 变量环境&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建 声明式环境记录器 ，存储变量、函数和参数，它包含了一个传递给函数的 arguments 对象（此对象存储索引和参数的映射）和传递给函数的参数的 length。（负责处理 var 定义的变量，初始值为 undefined 造成声明提升）&lt;/li&gt;
&lt;li&gt;创建 外部环境引用，值为全局对象，或者为父级词法环境（作用域）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定 this 值&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入函数执行上下文的执行阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在上下文中运行/解释函数代码，并在代码逐行执行时分配变量值。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关于执行上下文的标准一直在变化，目前 &lt;code&gt;EcmaScript 2021&lt;/code&gt;&lt;/strong&gt; 标准中又移除了关于词法环境和变量环境的概念，一切以标准为主，我个人建议还是按照 &lt;code&gt;ES3&lt;/code&gt; 的概念理解，然后 &lt;code&gt;let&lt;/code&gt; 和 &lt;code&gt;const&lt;/code&gt; 使用单独的规则。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;任何一种编程语言都有作用域的概念，我们的程序是围绕着变量操作的，那么在设计语言的时候，变量如何储存，储存到哪里，我们的程序如何找到对应的变量就是一个首先要解决的问题。而作用域就是语言设计者针对这个问题编写的一套设计良好的规则来存储并搜索对象，这就是作用域的概念。而 &lt;code&gt;JS&lt;/code&gt; 中的这个规则就是作用域链，我们在编写程序的时候也需要知道我们的变量（以及函数）是如何储存，以及 &lt;code&gt;JS&lt;/code&gt; 引擎在遇到标识符解析的时候是按照什么规则来搜索变量或者函数的，只有这样我们才能写出更可靠的代码。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.im/post/6844904158957404167#heading-16&quot; title=&quot;执行上下文&quot;&gt;执行上下文&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/scope-chain.DamagMj2.png"/><enclosure url="/_astro/scope-chain.DamagMj2.png"/></item><item><title>左侧固定，右侧自适应的布局方案</title><link>https://clloz.com/blog/2-colum-layout</link><guid isPermaLink="true">https://clloz.com/blog/2-colum-layout</guid><pubDate>Mon, 24 Dec 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;两栏布局是我们经常遇到的一种布局方式，实现两栏布局一般的需求是左侧固定宽度，右侧自适应，解决方法也多种多样，可以用 &lt;code&gt;float&lt;/code&gt;，&lt;code&gt;inline-block&lt;/code&gt;，配合 &lt;code&gt;calc&lt;/code&gt; 来设置宽度，也可以用 &lt;code&gt;CSS3&lt;/code&gt; 的新布局方法 &lt;code&gt;flex grid&lt;/code&gt;，其实核心问题就是如何让两个元素在同一行并且合理分配他们的宽度。下面来总结一下不同的方法以及其中的细节和注意点。&lt;/p&gt;
&lt;h2&gt;基本布局和基本样式&lt;/h2&gt;
&lt;p&gt;虽然解决的方法各不相同，但是整个 &lt;code&gt;HTML&lt;/code&gt; 的结构和一些必要的样式是一样，所以我们先把这个骨架搭好，后面只要给这个骨架一些小的装饰就可以完成功能了，这样对于我们的扩展和维护都比较有利。&lt;/p&gt;
&lt;p&gt;两栏布局的结构非常简单，我们需要一个 &lt;code&gt;wrap&lt;/code&gt; 和一左一右两个div就可以了，所以 &lt;code&gt;HTML&lt;/code&gt; 结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;wrap&quot;&gt;
  &amp;#x3C;div class=&quot;left&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;right&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们要把一些在各种实现方式下都统一的样式提取出来，比如左侧的宽度，两个块之间的间距，字体，背景色，内边距等，本文我们设置左侧 &lt;code&gt;div&lt;/code&gt; 宽度为 &lt;code&gt;200px&lt;/code&gt;，两个 &lt;code&gt;div&lt;/code&gt; 间距 &lt;code&gt;10px&lt;/code&gt;，如果设置左右不同的 &lt;code&gt;border&lt;/code&gt; 或者 &lt;code&gt;padding&lt;/code&gt; 需要设置 &lt;code&gt;box-sizing： border-box&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.wrap {
  height: 200px;
  margin-top: 30px;
  position: relative;
}

.wrap .left,
.wrap .right {
  height: 100%;
  padding: 10px;
  box-sizing: border-box;
  color: white;
  font-size: 30px;
  line-height: 1.5;
}

.wrap .left {
  background-color: pink;
  width: 200px;
}

.wrap .right {
  background-color: #32afd8;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成了基本框架我们就可以开始尝试各种不同的实现方法了。&lt;/p&gt;
&lt;h2&gt;float+margin-left方案&lt;/h2&gt;
&lt;p&gt;要让两个元素在同一行我们首先想到的就是 &lt;code&gt;float&lt;/code&gt;，宽度自适应是块级元素的特性。这个方案是设置左侧的div浮动，给右侧的 &lt;code&gt;div&lt;/code&gt; 一个 &lt;code&gt;margin-left&lt;/code&gt; 来实现的。块级元素有默认独占一行，并且填满父容器，随着父元素的宽度自适应的流动特性。而 &lt;code&gt;float&lt;/code&gt; 元素会脱离 &lt;code&gt;normal flow&lt;/code&gt;，后面的块级元素的布局会无视浮动元素的存在来渲染。所以我们只要让左侧的元素浮动，就能让两个块级元素存在在同一行，但是为了不让元素重叠，我们需要给左边的元素预留足够的距离以便两个元素都能完全展示出来，所以我们要为右边的元素设置 &lt;code&gt;margin-left&lt;/code&gt;，值为左侧元素的宽度加上两个元素之间的距离。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.eg1 .left {
  float: left;
}

.eg1 .right {
  margin-left: 210px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是如果父元素没有设置高度，那么由于浮动的存在会发生高度塌陷，需要清除浮动。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;双 float 方案&lt;/h2&gt;
&lt;p&gt;单个 &lt;code&gt;float&lt;/code&gt; 可以，那么我们设置两个元素都是 &lt;code&gt;float&lt;/code&gt; 自然也没问题，两个元素会在同一行，唯一的问题是元素浮动以后宽度是根据内容决定的，我们需要为右边的元素设置一个宽度，&lt;code&gt;calc&lt;/code&gt; 函数可以帮我们解决这个问题。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.eg3 .left,
.eg3 .right {
  float: left;
}

.eg3 .right {
  width: calc(100% - 210px);
  margin-left: 10px;
}

.clearfix::after {
  content: &apos;&apos;;
  display: block;
  clear: both;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这种方式同样需要注意父元素的清除浮动&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;方案缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要知道左侧元素的宽度和两个元素之间的距离，并且要设置 &lt;code&gt;box-sizing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;需要清除浮动&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;inline-block 方案&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;float&lt;/code&gt; 和 &lt;code&gt;inline-block&lt;/code&gt; 是非常相似的，一般用 &lt;code&gt;float&lt;/code&gt; 可以解决的问题，&lt;code&gt;inline-block&lt;/code&gt; 也一样可以，两栏布局同样可以用 &lt;code&gt;inline-block&lt;/code&gt; 来解决。我们设置左右两个元素 &lt;code&gt;display： inline-block&lt;/code&gt;，由于 &lt;code&gt;inline-block&lt;/code&gt; 的特性，我们需要设置父元素的 &lt;code&gt;font-size： 0;&lt;/code&gt; 以避免两个元素间的空格，同时因为两个元素的字体大小以及 &lt;code&gt;padding&lt;/code&gt; 不同等原因，我们需要用 &lt;code&gt;vertical-align&lt;/code&gt; 属性来对其两个元素。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.eg2 .left,
.eg2 .right {
  display: inline-block;
  vertical-align: top;
}

.eg2 .left {
  width: 200px;
}

.eg2 .right {
  width: calc(100% - 210px);
  margin-left: 10px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;对于vertical-align的细节不理解的可以看这篇&lt;a href=&quot;https://www.clloz.com/programming/front-end/css/2018/08/29/line-heightvertical-align/&quot; title=&quot;vertical-align&quot;&gt;文章&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;方案缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要知道左侧元素的宽度和两个元素之间的距离&lt;/li&gt;
&lt;li&gt;需要解决 &lt;code&gt;inline-block&lt;/code&gt; 缝隙问题&lt;/li&gt;
&lt;li&gt;需要用 &lt;code&gt;vertical-aling&lt;/code&gt; 来对齐元素&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;absolute+margin-left方案&lt;/h2&gt;
&lt;p&gt;用绝对定位同样可以让两个块级元素在同一行排列，和 &lt;code&gt;float+margin-left&lt;/code&gt; 的使用方法相似，浮动和绝对定位的区别是绝对定位完全脱离了文档流，而浮动依然保留了一些流动性。我们需要设置父元素的 &lt;code&gt;position&lt;/code&gt; 为非 &lt;code&gt;static&lt;/code&gt;，同时由于没有清除浮动类似的机制，我们必须为父元素限定 &lt;code&gt;min-height&lt;/code&gt;，否则左侧元素的高度超出右侧元素的话，就会溢出。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.eg4 .left {
  position: absolute;
}

.eg4 .right {
  margin-left: 210px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方案缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要设置父元素的 &lt;code&gt;position&lt;/code&gt; 为非 &lt;code&gt;static&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;需要限定高度&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;float+BFC方案&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;BFC&lt;/code&gt; 有一个特性是不会和 &lt;code&gt;float&lt;/code&gt; 元素重叠，利用这一特性，我们可以让左边的元素浮动，右边的元素生成一个新的 &lt;code&gt;BFC&lt;/code&gt;，在给一个边距就实现了需求。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.eg5 .left {
  float: left;
  margin-right: 10px;
}

.eg5 .right {
  overflow: hidden;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意，如果是用 &lt;code&gt;overflow&lt;/code&gt; 来生成的 &lt;code&gt;BFC&lt;/code&gt; 并使用 &lt;code&gt;margin-left&lt;/code&gt; 来预留编剧必须加上左边元素的宽度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;flex 布局方案&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;flex&lt;/code&gt; 可以说是最好的方案了，代码少，使用简单。有朝一日，大家都改用现代浏览器，就可以使用了。 需要注意的是，&lt;code&gt;flex&lt;/code&gt; 容器的一个默认属性值 &lt;code&gt;align-items: stretch;&lt;/code&gt; 。这个属性导致了列等高的效果。 为了让两个盒子高度自动，需要设置&lt;code&gt;align-items: flex-start;&lt;/code&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.wrap.wrap-flex {
  display: flex;
  align-items: flex-start;
}

.wrap.wrap-flex .left {
  flex: 0 0 auto;
}

.wrap.wrap-flex .right {
  flex: 1 1 auto;
  margin-left: 10px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;当父容器宽度放不下两个元素的情况&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;双 &lt;code&gt;float&lt;/code&gt; 和双 &lt;code&gt;inline-block&lt;/code&gt; 方案右侧元素会移动到下一行，并根据 &lt;code&gt;calc&lt;/code&gt; 函数计算宽度&lt;/li&gt;
&lt;li&gt;采用了 &lt;code&gt;margin-left&lt;/code&gt; 的方案右侧元素会不可见，宽度为 &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BFC&lt;/code&gt; 方案右侧元素也会掉到下一行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex&lt;/code&gt; 方案依然会按照原来的布局显示&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;文中示例查看&lt;a href=&quot;https://www.clloz.com/study/2-colum.html&quot;&gt;页面&lt;/a&gt; 更直观的示例查看&lt;a href=&quot;https://www.clloz.com/study/2-colum-layout.html&quot;&gt;页面&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000010698609&quot; title=&quot;七种实现左侧固定，右侧自适应两栏布局的方法&quot;&gt;七种实现左侧固定，右侧自适应两栏布局的方法&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CSS的值和单位</title><link>https://clloz.com/blog/css-values-and-units</link><guid isPermaLink="true">https://clloz.com/blog/css-values-and-units</guid><pubDate>Mon, 10 Dec 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在使用css的时候单位是我们经常要处理的一个细节，&lt;code&gt;px，em，percentage，number，rem，vh，vw&lt;/code&gt;等等，不同的单位可能渲染机制不同，如果对于单位的渲染机制理解不到位很可能会让我们在写样式的过程中遇到一些摸不着头脑的问题。&lt;/p&gt;
&lt;h2&gt;绝对长度单位&lt;/h2&gt;
&lt;p&gt;我没有使用过，只是在文档中看到，这些单位远在CSS出现之前就出现了，主要是在印刷排版上使用，包括&lt;code&gt;in，pt，pc，cm，mm&lt;/code&gt;，他们之间的关系为&lt;code&gt;1in = 2.54cm = 25.4mm = 72pt = 6pc&lt;/code&gt;，&lt;code&gt;in&lt;/code&gt; 表示 &lt;code&gt;inch&lt;/code&gt;，英寸，这些单位基本上只在打印上使用，对于我们现代的 &lt;code&gt;dpi&lt;/code&gt; 屏幕，不是很方便。比如 &lt;code&gt;pt&lt;/code&gt;（&lt;code&gt;point&lt;/code&gt;）指的是&lt;code&gt;1/72&lt;/code&gt;英寸，如果想转化成 &lt;code&gt;px&lt;/code&gt; 怎么算呢，&lt;code&gt;1px&lt;/code&gt; 为 &lt;code&gt;1&lt;/code&gt; 像素，而计算机的 &lt;code&gt;dpi&lt;/code&gt; 就是每英寸的像素数，他们之间的转化关系就是&lt;code&gt;px/dpi*72 = pt&lt;/code&gt;。似乎在 &lt;code&gt;IOS&lt;/code&gt; 开发上会遇到这个问题，不过在前端开发过程中很少用到这些绝对长度单位。&lt;/p&gt;
&lt;h2&gt;px&lt;/h2&gt;
&lt;p&gt;上面也有说到，&lt;code&gt;px&lt;/code&gt; 是相对长度单位，&lt;code&gt;px&lt;/code&gt; 的具体宽度取决于屏幕的 &lt;code&gt;dpi&lt;/code&gt;（&lt;code&gt;dots per inch&lt;/code&gt;）：每英寸像素数。但是在我们写样式的过程中，&lt;code&gt;px&lt;/code&gt; 也是一种绝对单位，因为只要我们设置了确定了的 &lt;code&gt;px&lt;/code&gt; 大小，那么该元素在该设备上的大小就确定了，不会因为其他元素（比如父元素）的属性而改变。&lt;/p&gt;
&lt;h2&gt;相对长度单位&lt;/h2&gt;
&lt;p&gt;相对长度单位包括&lt;code&gt;em,ex,ch,rem，vw，vh&lt;/code&gt;等其中 &lt;code&gt;em&lt;/code&gt; 和 &lt;code&gt;rem&lt;/code&gt; 我们使用比较多，&lt;code&gt;ch&lt;/code&gt; 表示字符 &lt;code&gt;0&lt;/code&gt; 的宽度，而 &lt;code&gt;ex&lt;/code&gt; 表示字符 &lt;code&gt;x&lt;/code&gt; 的高度，使用场景比较少。相对长度的单位的一个重要特点是子元素不会继承父元素的设定的该相对属性值，而是继承父元素的计算值。比如如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div style=&quot;font-size: 40px; line-height: 2em;background-color: black; color: white;&quot;&gt;
  这里是div-parent
  &amp;#x3C;div style=&quot;font-size: 16px; background-color: lightblue;&quot;&gt;这里是div-child&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到两个div嵌套，第一个 &lt;code&gt;div&lt;/code&gt; 的字体大小为&lt;code&gt;40px&lt;/code&gt;，第二个为&lt;code&gt;16px&lt;/code&gt;，我们设置了父元素的&lt;code&gt;line-height：2em&lt;/code&gt;，相当于字体的 &lt;code&gt;200%&lt;/code&gt;，&lt;code&gt;line-height&lt;/code&gt; 属性是可以继承的，但是我们发现父元素和子元素的行高是一样的，都是&lt;code&gt;80px&lt;/code&gt;，这跟我们的预想不一样，我们直觉觉得子元素继承了父元素的&lt;code&gt;line-height: 2em&lt;/code&gt;，那么他的行高应该是&lt;code&gt;16*2=32px&lt;/code&gt;，但是并不是这样，这就是上面说的相对长度单位其子元素只会继承出元素的计算值而不会继承相对值，或者你可以理解为子元素确实继承了父元素的&lt;code&gt;line-height：2em&lt;/code&gt;的属性，但是，这个 &lt;code&gt;2em&lt;/code&gt; 是根据父元素的字体大小来计算的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/unit1.Dvu11KhM_1HVERS.webp&quot; alt=&quot;unit1&quot; title=&quot;unit1&quot;&gt;&lt;/p&gt;
&lt;h2&gt;rem (root em)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;rem&lt;/code&gt; 和 &lt;code&gt;em&lt;/code&gt; 类似的，但是他的计算是根据 &lt;code&gt;HTML&lt;/code&gt; 根元素的字体大小来计算值的。&lt;code&gt;em&lt;/code&gt; 由于其工作方式可能在多个元素中表现不同，但是 &lt;code&gt;rem&lt;/code&gt; 在整个文档中的表现都是一致的，很多场景下非常有用。&lt;/p&gt;
&lt;h2&gt;vh，vw，vmax，vmin&lt;/h2&gt;
&lt;p&gt;这几个属性和 &lt;code&gt;rem&lt;/code&gt; 一样都是 &lt;code&gt;2013&lt;/code&gt; 年提出的新单位，目前已经得到广泛的支持和运用，他们都是相对于视窗的宽度和高度进行计算的，比如 &lt;code&gt;1vh&lt;/code&gt; 就是视窗高度的 &lt;code&gt;1%&lt;/code&gt;，而 &lt;code&gt;1vw&lt;/code&gt; 就是视窗宽度的 &lt;code&gt;1%&lt;/code&gt;，&lt;code&gt;vmax&lt;/code&gt; 是 &lt;code&gt;vh&lt;/code&gt; 和 &lt;code&gt;vw&lt;/code&gt; 中较大的那一个，&lt;code&gt;vmin&lt;/code&gt; 是 &lt;code&gt;vh&lt;/code&gt; 和 &lt;code&gt;vw&lt;/code&gt; 中较小的那一个。这几个单位在做响应式的页面的时候尤其有用。&lt;/p&gt;
&lt;h2&gt;无单位数值&lt;/h2&gt;
&lt;p&gt;在上面的 &lt;code&gt;line-height&lt;/code&gt; 问题中我们除了使用绝对长度单位和 &lt;code&gt;rem&lt;/code&gt; 单位以外，还可以使用无单位数值，比如 &lt;code&gt;2em&lt;/code&gt; 转化为 &lt;code&gt;2&lt;/code&gt;，一样可以解决继承问题。无单位数值会被子元素继承，并且计算是根绝当前元素的字体大小来计算的，效果如下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/unit2.fImED7go_byQOb.webp&quot; alt=&quot;unit2&quot; title=&quot;unit2&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CSS中的字体图标</title><link>https://clloz.com/blog/icon-font</link><guid isPermaLink="true">https://clloz.com/blog/icon-font</guid><pubDate>Mon, 10 Dec 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们在前端开发当中经常使用 &lt;code&gt;font-awesome&lt;/code&gt; 和&lt;code&gt;iconfont&lt;/code&gt; 等字体图标，今天来说一说字体图标的原理。&lt;/p&gt;
&lt;h2&gt;浏览器是如何渲染字体的&lt;/h2&gt;
&lt;p&gt;当浏览器接受 &lt;code&gt;HTML&lt;/code&gt; 文件开始渲染的时候，文档树中的文本都会被转化成对应的 &lt;code&gt;unicode&lt;/code&gt; 编码，然后根据我们设置的 &lt;code&gt;font-family&lt;/code&gt; 到系统中找到对应的字体，再将 &lt;code&gt;unicode&lt;/code&gt; 码渲染成对应的字体样式。如果没有找到或者我们没有设置 &lt;code&gt;font-family&lt;/code&gt; ，那么浏览器就会选择自己默认的字体来渲染。比如我们把&lt;code&gt;前端开发&lt;/code&gt;四个字转成 &lt;code&gt;unicode&lt;/code&gt; 是 &lt;code&gt;0x524D 0x7AEF 0x5F00 0x53D1&lt;/code&gt;，在 &lt;code&gt;HTML&lt;/code&gt; 中表示 &lt;code&gt;Unicode&lt;/code&gt; 编码用&lt;code&gt;&amp;#x26;#x hexadecimal;&lt;/code&gt;的形式 (或者是 &lt;code&gt;&amp;#x26;# decimal;&lt;/code&gt; 的形式)，我们把前面的四个 &lt;code&gt;Unicode&lt;/code&gt; 转成该形式写入 &lt;code&gt;html&lt;/code&gt; 文件中，可以发现依然可以解析的。&lt;/p&gt;
&lt;h2&gt;字体图标原理&lt;/h2&gt;
&lt;p&gt;有了上面的渲染过程，字体图标的制作就很简单了。 1. 制作字体文件 2. &lt;code&gt;font-face&lt;/code&gt; 引入字体文件（可以使用本地连接和第三方链接） 3. 使用 &lt;code&gt;font-family&lt;/code&gt;（&lt;code&gt;HTML&lt;/code&gt; 实体或者伪元素&lt;code&gt;:before&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;我们目前接触的字体图标包括 &lt;code&gt;iconfont&lt;/code&gt; &lt;code&gt;fontAwesom&lt;/code&gt;都是这样的原理，用 &lt;code&gt;font-face&lt;/code&gt; 引入自定义的字体文件，然后对要应用字体的元素约束自定义的 &lt;code&gt;font-family&lt;/code&gt;，最后用伪元素 &lt;code&gt;:before&lt;/code&gt; 的 &lt;code&gt;content&lt;/code&gt; 来给定元素的 &lt;code&gt;unicode&lt;/code&gt; 码来设置对应的图标，其实如果我们不用伪元素，直接在元素中输入字符 &lt;code&gt;unicode&lt;/code&gt; 码对应的HTML实体（ &lt;code&gt;HTML entity&lt;/code&gt; ）也可以实现效果，&lt;code&gt;&amp;#x3C;span class=&quot;iconfont icon-alipay&quot;&gt;&amp;#x3C;/span&gt;&lt;/code&gt; 也可以写成 &lt;code&gt;&amp;#x3C;span class=&quot;iconfont&quot;&gt;&amp;#x3C;/span&gt;&lt;/code&gt;,效果一样。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@font-face {
  font-family: &apos;iconfont&apos;;
  src: url(&apos;//at.alicdn.com/t/font_958524_6v80ih0o45k.eot?t=1544418917327&apos;); /* IE9*/
  src:
    url(&apos;//at.alicdn.com/t/font_958524_6v80ih0o45k.eot?t=1544418917327#iefix&apos;)
      format(&apos;embedded-opentype&apos;),
    /* IE6-IE8 */ url(&apos;//at.alicdn.com/t/font_958524_6v80ih0o45k.ttf?t=1544418917327&apos;)
      format(&apos;truetype&apos;),
    /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
      url(&apos;//at.alicdn.com/t/font_958524_6v80ih0o45k.svg?t=1544418917327#iconfont&apos;) format(&apos;svg&apos;); /* iOS 4.1- */
}

.iconfont {
  font-family: &apos;iconfont&apos; !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-alipay:before {
  content: &apos;\e63b&apos;;
}

.icon-wechat:before {
  content: &apos;\e658&apos;;
}

.icon-qq:before {
  content: &apos;\e603&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其他实现方法&lt;/h2&gt;
&lt;p&gt;实现图标的还有其他的方法： 1. &lt;code&gt;image&lt;/code&gt; ：在最早接触 &lt;code&gt;css&lt;/code&gt; 相信大家还不知道 &lt;code&gt;iconfont&lt;/code&gt; 等，都是用图标来实现的，使用很不方便，并且会增加请求数量（一个图标一个请求），不推荐 2. &lt;code&gt;CSS sprites&lt;/code&gt; ： 精灵图，即将多个图标放到一个图片上，用 &lt;code&gt;background-position&lt;/code&gt; 来控制图片的位置来显示图标，缺点是不能缩放，并且添加图标麻烦 3. &lt;code&gt;CSS icon&lt;/code&gt; ：纯 &lt;code&gt;CSS&lt;/code&gt; 写的 &lt;code&gt;icon&lt;/code&gt; ，不是很好，不推荐。 4. &lt;code&gt;SVG&lt;/code&gt; ：比较流行的方法，矢量图，可以任意缩放添加样式，多个图标也可以放大一个 &lt;code&gt;svg&lt;/code&gt; 文件中。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>理解浏览器对文本溢出的处理</title><link>https://clloz.com/blog/text-overflow</link><guid isPermaLink="true">https://clloz.com/blog/text-overflow</guid><pubDate>Sun, 09 Dec 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最早遇到文本溢出是因为有些链接内容太长，需要缺省展示，就要截断一部分文本用省略号代替，最早对 &lt;code&gt;CSS&lt;/code&gt; 还不是很懂的时候以为是手动写的，后来知道了一些文本溢出的相关属性开始使用，不过也都是看了个大致就直接拿来用了，没有细想那些属性的具体作用和使用场景，今天来谈一谈文本溢出的处理。&lt;/p&gt;
&lt;h2&gt;什么是文本溢出&lt;/h2&gt;
&lt;p&gt;首先说一说什么是文本溢出，就是父元素限定了宽度，而文本内容的长度超过了父元素的宽度且不能换行的时候就发了文本溢出，跟这个场景相关的几个属性&lt;code&gt;overflow，text-overflow，white-space，word-break，overflow-wrap&lt;/code&gt;。上面我们说到了文本溢出必须是文本所在元素的宽度被限定了，所以文本所在元素必须是一个块级元素，因为行内元素的宽度是由其中的文本内容决定的，并不能用&lt;code&gt;width&lt;/code&gt;来进行约束。我们仙来单独理解上面说到的五个属性。&lt;/p&gt;
&lt;h2&gt;overflow&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;overflow&lt;/code&gt; 是溢出的意思，该属性定义了一个元素太大而无法适应块级格式上下文时的渲染方式。他是&lt;code&gt;overflow-x&lt;/code&gt;和&lt;code&gt;overflow-y&lt;/code&gt;的简写属性，一共有四个属性值：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;visible&lt;/code&gt;（&lt;code&gt;default&lt;/code&gt;）：溢出内容不会被修剪，会呈现在元素框外。这就是我们最常看到的我们的元素超出父元素的边界，显示到了父元素外面。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hidden&lt;/code&gt;：溢出的内容会被修建，让元素能够适应元素框，不提供滚动条。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scroll&lt;/code&gt;：内容会被修剪来适应元素框,浏览器会提供滚动条让用户来查看被修剪的内容，用打印机打印文档的时候，被修剪的内容也会被打印。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto&lt;/code&gt;：由UA决定，当内容不溢出就正常显示，溢出则修剪并显示滚动条。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inherit&lt;/code&gt;：从父元素继承该属性。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;auto&lt;/code&gt; 和 &lt;code&gt;scroll&lt;/code&gt; 的区别就是：&lt;code&gt;scroll&lt;/code&gt; 无论当前是否需要滚动条，都会显示，只是当内容不需要滚动的时候，滚动条是不可用的。而 &lt;code&gt;auto&lt;/code&gt; 则会根据是否有内容被裁剪而选择是否显示滚动条。不过如果你使用的是 &lt;code&gt;mac&lt;/code&gt;，那么你没法看出两者的区别，&lt;code&gt;mac&lt;/code&gt; 上默认是隐藏不需要的滚动条。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们的文本要溢出至少要保证两个条件，就是文本所在元素的宽度要小于文本的宽度并且&lt;code&gt;overflow&lt;/code&gt;为&lt;code&gt;visible|hidden&lt;/code&gt;,而如果我们要用省略号代替截取文本则属性值只能选择&lt;code&gt;hidden&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;text-overflow&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;text-overflow&lt;/code&gt; 属性指导浏览器如何渲染溢出内容。该属性职工有四个值：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;clip&lt;/code&gt;（&lt;code&gt;default&lt;/code&gt;）：该属性告诉浏览器在内容区域的极限处进行截断，也就是内容只能显示到元素框的边界，因此有可能发生一个字符出现一半就被截断的情况，如果要避免这种情况就要在截断处添加空字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ellipsis&lt;/code&gt;：该属性值告诉浏览器用一个省略号&lt;code&gt;...&lt;/code&gt;来代替被截断的文本，省略号被添加到可见区的末尾，因此可见的文本要缩短一些，如果元素框小到省略号也放不下，那么省略号也会被截断。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;string&lt;/code&gt;：用任意自定义字符串来表示被截断内容。该属性目前只是个实验性功能，绝大多数浏览器都没有实现该功能，无法使用。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的一点是，如果文本没有发生溢出并且没有截断，那么该属性是不会生效的，也就是说要使用该属性必须是在文本溢出，并且&lt;code&gt;overflow: hidden;&lt;/code&gt;才能起作用，&lt;code&gt;overflow: scroll;&lt;/code&gt;虽然也能显示该属性，出现省略号或者截断，但是当&lt;code&gt;text-overflow: ellipsis&lt;/code&gt;是，溢出的文本将无法展示，省略号后面的内容被空白符代替。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;white-space&lt;/h2&gt;
&lt;p&gt;在说 &lt;code&gt;white-space&lt;/code&gt; 之前我们先说一说什么是空白符 &lt;code&gt;whitespace&lt;/code&gt;，一般来说我们在编程语言中使用空白符都是分隔 &lt;code&gt;token&lt;/code&gt;，但是在不同的语言中对空白符的定义并不相同。在 &lt;code&gt;HTML&lt;/code&gt; 中规定了五种空白符：&lt;code&gt;U+0009 TAB, U+000A LF (line feed), U+000C FF (form feed), U+000D CR (carriage return), and U+0020 SPACE&lt;/code&gt;，他们分别对应 &lt;code&gt;ASCII&lt;/code&gt; 中的 &lt;code&gt;9，10，12，13，32&lt;/code&gt; 位的控制符。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;white-space&lt;/code&gt; 属性告诉浏览器如何处理元素中的空白符。该属性有五个值: 1. &lt;code&gt;normal&lt;/code&gt;：多个连续空白符会被合并，换行符也会被当作一个空白符来处理。文本会适应包含元素进行换行。 2. &lt;code&gt;nowrap&lt;/code&gt;：会像 &lt;code&gt;normal&lt;/code&gt; 属性一样合并多个连续空白符，但是文本内的换行无效。 3. &lt;code&gt;pre&lt;/code&gt;: 连续空白符会被保留，只有遇到换行符或者&lt;code&gt;&amp;#x3C;br&gt;&lt;/code&gt;才会进行换行。 4. &lt;code&gt;pre-wrap&lt;/code&gt;: 连续空白符会被保留，遇到换行符或者&lt;code&gt;&amp;#x3C;br&gt;&lt;/code&gt;会换行，同时在包含元素边界也会换行。 5. &lt;code&gt;pre-line&lt;/code&gt;：连续空白符会被合并，遇到换行符或者&lt;code&gt;&amp;#x3C;br&gt;&lt;/code&gt;以及包含元素边界会换行。 6. &lt;code&gt;break-spaces&lt;/code&gt;: 与 &lt;code&gt;pre-wrap&lt;/code&gt; 的行为相同，除了：任何保留的空白序列总是占用空间，包括在行尾。每个保留的空格字符后都存在换行机会，包括空格字符之间。这样保留的空间占用空间而不会挂起，从而影响盒子的固有尺寸（最小内容大小和最大内容大小）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MDN&lt;/code&gt; 整理了这几个值的区别&lt;/p&gt;
&lt;p&gt;|                | 换行符 | 空格和制表符 | 文字换行 | 行尾空格 |
| -------------- | ------ | ------------ | -------- | -------- |
| &lt;code&gt;normal&lt;/code&gt;       | 合并   | 合并         | 换行     | 删除     |
| &lt;code&gt;nowrap&lt;/code&gt;       | 合并   | 合并         | 不换     | 删除     |
| &lt;code&gt;pre&lt;/code&gt;          | 保留   | 保留         | 不换行   | 保留     |
| &lt;code&gt;pre-wrap&lt;/code&gt;     | 保留   | 保留         | 换行     | 挂起     |
| &lt;code&gt;pre-line&lt;/code&gt;     | 保留   | 合并         | 换行     | 删除     |
| &lt;code&gt;break-spaces&lt;/code&gt; | 保留   | 保留         | 换行     | 换行     |&lt;/p&gt;
&lt;p&gt;空白符的合并其实比较好理解，比较难理解的是文字的换行。我个人的理解是，&lt;code&gt;normal&lt;/code&gt; 空格和换行符一样，都可以进行换行（换行符被当做空格一样，但是每个空格处都可以换行）；&lt;code&gt;nowrap&lt;/code&gt; 中任何换行符无效；&lt;code&gt;pre&lt;/code&gt; 只有在遇到换行符和 &lt;code&gt;br&lt;/code&gt; 标签的时候会换行；&lt;code&gt;pre-wrap&lt;/code&gt; 空格和换行符 &lt;code&gt;br&lt;/code&gt; 都可以进行换行；&lt;code&gt;pre-line&lt;/code&gt; 和 &lt;code&gt;pre-wrap&lt;/code&gt; 换行机制一致（它们的不同在于是否合并空格和制表符）；&lt;code&gt;break-spaces&lt;/code&gt; 和 &lt;code&gt;pre-wrap&lt;/code&gt; 的机制大致相同，不同的是行尾出现大段连续空白的时候，&lt;code&gt;break-spaces&lt;/code&gt; 会将这些空白符全部输出，并且他能在任何空格处换行。&lt;/p&gt;
&lt;p&gt;所以会在水平方向上超出父元素的只有 &lt;code&gt;nowrap&lt;/code&gt; 和 &lt;code&gt;pre&lt;/code&gt; 两个属性。几个属性最后的效果可以查看：&lt;a href=&quot;https://cdn.clloz.com/study/white-space.html&quot; title=&quot;white-space 效果Demo&quot;&gt;white-space 效果Demo&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;正常情况下，我们需要文本溢出的时候我们会希望文本显示在同一行，因为默认情况下我们的文本到达元素框边界的时候是会自动换行的，所以我们需要&lt;code&gt;white-space: nowrap&lt;/code&gt;来约束文本显示在一行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;word-break&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;word-break&lt;/code&gt; 属性指定浏览器怎样在单词内断行。该属性有四个值： 1. &lt;code&gt;normal&lt;/code&gt;：按默认规则进行换行，对于CJK（中文/日语/韩文）在每个字符之间都可以换行，而对于非CJK则需要在单词结束换行，如果一个单词的长度超过了元素框的宽度，那么该文本将溢出 2. &lt;code&gt;break-all&lt;/code&gt;：在任意字符之间都可以换行。 3. &lt;code&gt;keep-all&lt;/code&gt;： 对于非 &lt;code&gt;CJK&lt;/code&gt; 语言，按 &lt;code&gt;normal&lt;/code&gt; 规则，对于 &lt;code&gt;CJK&lt;/code&gt; 不换行。&lt;/p&gt;
&lt;p&gt;几个属性的最后效果可以查看：&lt;a href=&quot;https://cdn.clloz.com/study/word-break.html&quot; title=&quot;word-break 效果 Demo&quot;&gt;word-break 效果 Demo&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当&lt;code&gt;white-space: nowrap&lt;/code&gt;文本不换行的时候，该属性无效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;overflow-wrap&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;word-wrap&lt;/code&gt; 属性原本属于微软的一个私有属性，在 &lt;code&gt;CSS3&lt;/code&gt; 现在的文本规范草案中已经被重名为 &lt;code&gt;overflow-wrap&lt;/code&gt;。 &lt;code&gt;word-wrap&lt;/code&gt; 现在被当作 &lt;code&gt;overflow-wrap&lt;/code&gt; 的 “别名”。 稳定的谷歌 &lt;code&gt;Chrome&lt;/code&gt; 和 &lt;code&gt;Opera&lt;/code&gt; 浏览器版本支持这种新语法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;overflow-wrap&lt;/code&gt; 属性指导浏览器对于特别长的超出元素框宽度的单词是否可以从中间断开换行。有两个属性： 1. &lt;code&gt;normal&lt;/code&gt;：不允许断开单词。 2. &lt;code&gt;break-word&lt;/code&gt;：当单词过长，元素的宽度不能容纳单词则会在单词内部断开强制换行。 3. &lt;code&gt;anywhere&lt;/code&gt;：目前支持不是很好。&lt;/p&gt;
&lt;p&gt;几个属性的最后效果可以查看：&lt;a href=&quot;https://cdn.clloz.com/study/overflow-wrap.html&quot; title=&quot;overflow-wrap 效果 Demo&quot;&gt;overflow-wrap 效果 Demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;与 &lt;code&gt;word-break&lt;/code&gt; 相比，&lt;code&gt;overflow-wrap&lt;/code&gt; 仅在无法将整个单词放在自己的行而不会溢出的情况下才会产生中断。所以我们可以看到 &lt;code&gt;word-break: break-all&lt;/code&gt; 会在任意地方进行中断，文本当中不会留下很多空白；而 &lt;code&gt;overflow-wrap: break-word&lt;/code&gt; 只会在不得不断的地方（整行都放不下一个单词）进行中断，所以会出现很多空白。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当&lt;code&gt;overflow-wrap: break-word&lt;/code&gt;时，&lt;code&gt;word-break: keep-all&lt;/code&gt;将不会生效，元素依然会从中间断开。可以理解为&lt;code&gt;overflow-wrap&lt;/code&gt;的优先级是比&lt;code&gt;word-break&lt;/code&gt;高的，再加上&lt;code&gt;white-space: nowrap;&lt;/code&gt;属性，优先级为&lt;code&gt;white-space &gt; overflow-wrap &gt; word-break&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;多行文本的溢出处理&lt;/h2&gt;
&lt;p&gt;需要注意几个属性的兼容性。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/*单行文本溢出用省略号显示：*/
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

/*多行文本溢出用省略号显示：*/
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;当我们需要显示缩略文本时，我们要做到这几件事：文本保持在一行，文本长度超出元素宽度，元素不支持滚动条，用省略号代替溢出文本。依次实现代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.style {
  white-sapce: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>git reset 和 git revert</title><link>https://clloz.com/blog/git-undo</link><guid isPermaLink="true">https://clloz.com/blog/git-undo</guid><pubDate>Wed, 05 Dec 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git&lt;/code&gt; 中的撤销操作是我们经常要用到的操作，比如说当我们 &lt;code&gt;add&lt;/code&gt; 某个文件到暂存区后可能想要撤回，或者是我们已经 &lt;code&gt;commit&lt;/code&gt; 了但是 &lt;code&gt;push&lt;/code&gt; 的时候想取消这次 &lt;code&gt;commit&lt;/code&gt;，这是主要是用&lt;code&gt;git reset&lt;/code&gt;，&lt;code&gt;git checkout&lt;/code&gt;和&lt;code&gt;git revert&lt;/code&gt;三个命令。&lt;/p&gt;
&lt;h2&gt;git reset&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Git&lt;/code&gt; 的分支本质上是指向提交对象的可变指针，比如&lt;code&gt;master&lt;/code&gt;分支你多次提交后就会有一个指向最后一个提交对象的指针，每一个提交对象都包含一个指向上一个提交对象的指针，这就是 &lt;code&gt;git&lt;/code&gt; 分支的本质。而如何确定我们当前在哪个分支上呢？&lt;code&gt;git&lt;/code&gt; 给出了一个特殊的&lt;code&gt;HEAD&lt;/code&gt;指针，他指向当前所在的本地分支。比如说我们现在在&lt;code&gt;master&lt;/code&gt;分支，我们知道&lt;code&gt;master&lt;/code&gt;指针指向的是该分支的最后一个提交，而&lt;code&gt;HEAD&lt;/code&gt;指针此时也指向该处，所以我们可以理解&lt;code&gt;HEAD&lt;/code&gt;指针为当前所在分支的别名。如下图： &lt;img src=&quot;https://clloz.com/_astro/progit.DJRrzlFK_HYdJj.webp&quot; alt=&quot;progit&quot; title=&quot;progit&quot;&gt;&lt;/p&gt;
&lt;p&gt;理解了这些内容，我们就可以来理解&lt;code&gt;git reset&lt;/code&gt;是如何工作的了。我们把git工作的空间分为两个区域： 1. 已经 &lt;code&gt;commit&lt;/code&gt; 的文件和修改后未 &lt;code&gt;add&lt;/code&gt; 的文件，这一类文件都在工作目录（&lt;code&gt;Working Dictionary&lt;/code&gt;） 2. 已经 &lt;code&gt;add&lt;/code&gt; 还未 &lt;code&gt;commit&lt;/code&gt; 的文件在暂存区（&lt;code&gt;Index&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;还有一个 &lt;code&gt;Head&lt;/code&gt; 指针指向当前所在分支的最后一次提交对象。比如上图中的 &lt;code&gt;HEAD&lt;/code&gt; 和 &lt;code&gt;master&lt;/code&gt; 指针本来是指向&lt;code&gt;34ac2&lt;/code&gt;对象，此时&lt;code&gt;f30ab&lt;/code&gt;对象正在暂存区，当&lt;code&gt;f30ab&lt;/code&gt;提交完成以后， &lt;code&gt;HEAD&lt;/code&gt; 和 &lt;code&gt;master&lt;/code&gt; 的指针就指向了新的提交，而&lt;code&gt;34ac2&lt;/code&gt;则成为了最新提交的父节点。而我们的&lt;code&gt;git reset&lt;/code&gt;指令就是操作&lt;code&gt;HEAD&lt;/code&gt;指针，移动它的位置来实现撤销。下面我们用一个具体的例子来说明。&lt;/p&gt;
&lt;p&gt;我们在一个空的 &lt;code&gt;git&lt;/code&gt; 仓库中新建一个文件：&lt;code&gt;file.txt&lt;/code&gt;。用 &lt;code&gt;vim&lt;/code&gt; 编辑当前文件在当中输入字符串&lt;code&gt;first&lt;/code&gt;，提交文件。此时我们用&lt;code&gt;git log --decorate --oneline&lt;/code&gt;可以看到我们的 &lt;code&gt;master&lt;/code&gt; 指针和 &lt;code&gt;HEAD&lt;/code&gt; 指针都指向我们的第一次提交对象。 &lt;img src=&quot;https://clloz.com/_astro/reset1.l5jKAyv7_ZBGNsJ.webp&quot; alt=&quot;reset1&quot; title=&quot;reset1&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后我们再次修改文件，在文件中加入字符串&lt;code&gt;second&lt;/code&gt;，再次提交文件，调用上面的指令，我们会发现此时 &lt;code&gt;HEAD&lt;/code&gt; 和 &lt;code&gt;master&lt;/code&gt; 指针都指向我们的第二次提交。 &lt;img src=&quot;https://clloz.com/_astro/reset2.EF5Tav1r_Z19hOln.webp&quot; alt=&quot;reset2&quot; title=&quot;reset1&quot;&gt;&lt;/p&gt;
&lt;p&gt;再次重复上述操作，第三次修改提交。 &lt;img src=&quot;https://clloz.com/_astro/reset3.BWopo8Zw_Z2m4MkK.webp&quot; alt=&quot;reset3&quot; title=&quot;reset1&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时我们的整个git树应该是这样的： &lt;img src=&quot;https://clloz.com/_astro/reset4.HmAKxwAX_Z2YNm7.webp&quot; alt=&quot;reset4&quot; title=&quot;reset1&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时我们开始执行&lt;code&gt;reset&lt;/code&gt;指令： 首先执行&lt;code&gt;git reset --soft HEAD~&lt;/code&gt;： 我们发现HEAD和master指针指向了second，并且查看我们的&lt;code&gt;file.txt&lt;/code&gt;文件，我们会发现文件中依然保持这第三次修改，只不过文件从已经commit的状态被重新拉到了缓存区，所以我们可以看出&lt;code&gt;--soft&lt;/code&gt;下的指令只是向后倒退了一小步，把我们最后一个commit给取消了。 &lt;img src=&quot;https://clloz.com/_astro/reset5.BJu-oK_g_2wo8o6.webp&quot; alt=&quot;reset5&quot; title=&quot;reset1&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后我们重新提交文件，让git状态重新进入到三次提交的状态。然后执行&lt;code&gt;git reset HEAD~&lt;/code&gt;指令（这条指令没有加参数，其实有一个默认参数 &lt;code&gt;--mixed&lt;/code&gt;）：执行完指令后，我们发现 &lt;code&gt;HEAD&lt;/code&gt; 和 &lt;code&gt;master&lt;/code&gt; 指针又是指向了我们的第二次提交对象，而我们的 &lt;code&gt;file&lt;/code&gt; 文件依然没有改变，但是这次它从没有 &lt;code&gt;commit&lt;/code&gt; 的状态变成了没有 &lt;code&gt;add&lt;/code&gt; 的状态，我们可以看出&lt;code&gt;--mixed&lt;/code&gt; 比 &lt;code&gt;--soft&lt;/code&gt; 又向后倒退了一小步，把 &lt;code&gt;git&lt;/code&gt; 状态回复到了我们刚刚修改完文件还没有加入暂存区的状态。 &lt;img src=&quot;https://clloz.com/_astro/reset6.lMmigsOK_Z2uvXEG.webp&quot; alt=&quot;reset6&quot; title=&quot;reset1&quot;&gt;&lt;/p&gt;
&lt;p&gt;最后我们依然重新回到三次提交的状态然后执行&lt;code&gt;git reset --hard HEAD~&lt;/code&gt;指令：我们发现这次 &lt;code&gt;HEAD&lt;/code&gt; 和 &lt;code&gt;master&lt;/code&gt; 指针依然是指向我们第二次的提交对象，我们检查&lt;code&gt;git status&lt;/code&gt;发现没有任何状态，在查看&lt;code&gt;file.txt&lt;/code&gt;，我们发现我们第三次的修改消失了，也就是说&lt;code&gt;--hard&lt;/code&gt;指令把我们的状态直接从第三次提交完成回退到了第二次提交刚刚完成的状态，我们刚刚最后一次做的修改也消失了。&lt;/p&gt;
&lt;p&gt;综合上面的测试，做一个总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--soft&lt;/code&gt; – 缓存区和工作目录都不会被改变&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--mixed&lt;/code&gt; – 默认选项。缓存区和你指定的提交同步，但工作目录不受影响&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--hard&lt;/code&gt; – 缓存区和工作目录都同步到你指定的提交&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;git revert&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git revert&lt;/code&gt; 我们用相同的方法进行测试，我们可以发现&lt;code&gt;git revert&lt;/code&gt;并不是后退，而是把文件回到要回退的状态重新提交，如刚刚我们一共执行了三次提交，&lt;code&gt;git revert HEAD~&lt;/code&gt;以后会将文件回复到第二次提交的状态，重新提交，也就变成了第四次提交，可见 &lt;code&gt;revert&lt;/code&gt; 还是比较安全的，我们没有丢失任何历史记录，而 &lt;code&gt;reset&lt;/code&gt; 将我们回滚的记录丢掉了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是，当revert同一个文件的时候会出现冲突，需要手动修改冲突，然后手动提交&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="/_astro/git-logo.BKb-Bvwl.png"/><enclosure url="/_astro/git-logo.BKb-Bvwl.png"/></item><item><title>从URL输入到页面展现</title><link>https://clloz.com/blog/url</link><guid isPermaLink="true">https://clloz.com/blog/url</guid><pubDate>Wed, 05 Dec 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;这是一个经典的前端面试题，理解从&lt;code&gt;URL&lt;/code&gt;到页面展现的过程能够对于前端的工作以及前后端的配合有更透彻的理解，让我们在解决一些需求以及沟通的时候能够有更加清晰的思路。&lt;/p&gt;
&lt;p&gt;本文只是简单介绍以下，详细的内容可以参考另外两篇文章： 1. 网络：&lt;a href=&quot;https://clloz.com/blog/http/&quot; title=&quot;前端网络基础和HTTP&quot;&gt;前端网络基础和HTTP&lt;/a&gt; 2. 浏览器：&lt;a href=&quot;https://clloz.com/blog/how-browser-work-2/&quot; title=&quot;浏览器渲染过程及Event Loop浅析&quot;&gt;浏览器渲染过程及Event Loop浅析&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;URI、URL 和域名&lt;/h2&gt;
&lt;h2&gt;URI &amp;#x26; URL&lt;/h2&gt;
&lt;p&gt;在说这个问题之前，先来说一说 &lt;code&gt;URI&lt;/code&gt; 和 &lt;code&gt;URL&lt;/code&gt; 以及域名。我们经常看到这两个名称，但是不知道绝体有什么区别，好像两者可以互相替换，其实并不是。&lt;code&gt;URI&lt;/code&gt;（&lt;code&gt;Uniform Resource Identifier&lt;/code&gt;）统一资源标识符，&lt;a href=&quot;https://www.ietf.org/rfc/rfc2396.txt&quot; title=&quot;RFC 2396&quot;&gt;RFC 2396&lt;/a&gt;对这三个单词分别做了定义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Uniform&lt;/code&gt;：规定统一的格式可方便处理多种不同类型的资源，而不用根据上下文环境来识别资源指定的访问方式。另外，加入新增的协议方案（如 &lt;code&gt;http&lt;/code&gt;: 或 &lt;code&gt;ftp&lt;/code&gt;:）也更容易。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Resource&lt;/code&gt;：资源的定义是“可标识的任何东西”。不仅是文档文件，图像或服务（例如当天的天气预报）等能够区别于其他类型的，全都可作为资源。另外，资源不仅可以是单一的，也可以是多数的集合体。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Identifier&lt;/code&gt;：表示可标识的对象。也称为标识符。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;URI&lt;/code&gt; 说简单点就是在一个统一的规则下唯一标志资源的方法。比如每个人身份证号都不一样，那身份证号就是一个 &lt;code&gt;URI&lt;/code&gt;，他能唯一标志每一个人而且没有重复。在网络上我们是以协议来统一格式的，也就是我们熟悉的&lt;code&gt;http https ftp&lt;/code&gt;等（标准的 &lt;code&gt;URI&lt;/code&gt; 协议有 &lt;code&gt;30&lt;/code&gt; 种左右），也就是[scheme:] &lt;code&gt;scheme-specific-part&lt;/code&gt; 的形式，比如我们前面用身份证标记一个人就可以表示为&lt;code&gt;ID：12345678XXX&lt;/code&gt;。而 &lt;code&gt;URL&lt;/code&gt; 在唯一标志资源的前提下加了一个约束条件，就是必须用位置来唯一标志一个资源，刚刚我们用身份证号唯一标志一个人，现在我们相当于用住址来表示&lt;code&gt;address：China/Shanghai/Yangpu/XX street/XX&lt;/code&gt;，从这里我们可以看出URL其实是 &lt;code&gt;URI&lt;/code&gt; 的一个子集，每一个 &lt;code&gt;URL&lt;/code&gt; 同时也是一个 &lt;code&gt;URI&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RFC3986&lt;/code&gt;（&lt;code&gt;URI&lt;/code&gt; 通用语法）中列举了几种 &lt;code&gt;URI&lt;/code&gt; 例子，如下所示，其中有一些也是 &lt;code&gt;URL&lt;/code&gt;）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ftp://ftp.is.co.za/rfc/rfc1808.txt (also a URL because of the protocol)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.ietf.org/rfc/rfc2396.txt&quot;&gt;http://www.ietf.org/rfc/rfc2396.txt&lt;/a&gt; (also a URL because of the protocol)&lt;/li&gt;
&lt;li&gt;ldap://[2001: db8::7]/c=GB?objectClass?one (also a URL because of the protocol)&lt;/li&gt;
&lt;li&gt;mailto:John.Doe@example.com (also a URL because of the protocol)&lt;/li&gt;
&lt;li&gt;news:comp.infosystems.www.servers.unix (also a URL because of the protocol)&lt;/li&gt;
&lt;li&gt;tel:+1-816-555-1212&lt;/li&gt;
&lt;li&gt;telnet://192.0.2.16:80/ (also a URL because of the protocol)&lt;/li&gt;
&lt;li&gt;urn: oasis: names: specification: docbook: dtd: xml:4.1.2&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;域名&lt;/h2&gt;
&lt;p&gt;每台设备在网络上都有自己的 &lt;code&gt;IP&lt;/code&gt; 地址来定位自己的位置，但是由于 &lt;code&gt;IP&lt;/code&gt; 地址记忆复杂后来就引入了语义化的域名，用户输入域名即可访问网站，这中间其实有 &lt;code&gt;DNS&lt;/code&gt; 服务器在工作，把我们的域名翻译成 &lt;code&gt;IP&lt;/code&gt; 在进行定位。 我们的 &lt;code&gt;URL&lt;/code&gt; 可以写作以下形式&lt;code&gt;协议://主机名.域名：端口号/文件（资源）路径&lt;/code&gt;，比如&lt;code&gt;https://www.baidu.com/xxx&lt;/code&gt;，其中&lt;code&gt;baidu.com&lt;/code&gt;就是域名。&lt;/p&gt;
&lt;h2&gt;具体流程&lt;/h2&gt;
&lt;h2&gt;域名解析&lt;/h2&gt;
&lt;p&gt;在浏览器接收到 &lt;code&gt;URL&lt;/code&gt; 的时候他的首要任务是通过 &lt;code&gt;URL&lt;/code&gt; 中的域名找到该域名对应的 &lt;code&gt;IP&lt;/code&gt; 地址，找 &lt;code&gt;IP&lt;/code&gt; 地址的过程可以分为一下几个阶段： 1. 查看浏览器缓存，如果刚刚访问过该域名，缓存中仍然有该域名的 &lt;code&gt;dns&lt;/code&gt; 信息则直接从浏览器缓存中获取，各浏览器缓存 &lt;code&gt;DNS&lt;/code&gt; 信息的时间不同，&lt;code&gt;chrome&lt;/code&gt; 为一分钟（&lt;code&gt;chrome&lt;/code&gt; 查看缓存 &lt;code&gt;url&lt;/code&gt;：&lt;a href=&quot;chrome://net-internals#dns%EF%BC%9B&quot; title=&quot;chrome://net-internals#dns&quot;&gt;chrome://net-internals#dns&lt;/a&gt;）。 2. 浏览器缓存中没有该域名的 &lt;code&gt;dns&lt;/code&gt; 信息则搜索系统缓存，读取本地 &lt;code&gt;hosts&lt;/code&gt; 文件 3. &lt;code&gt;hosts&lt;/code&gt; 文件中也没有对应的 &lt;code&gt;dns&lt;/code&gt; 信息则会搜索路由器缓存 4. 浏览器发起 &lt;code&gt;dns&lt;/code&gt; 系统调用（向宽带运营商发起 &lt;code&gt;dns&lt;/code&gt; 解析请求）； a. 宽带运营商（&lt;code&gt;ISP&lt;/code&gt;）服务器查看本地缓存 b. 运营商服务器发起一个迭代 &lt;code&gt;dns&lt;/code&gt; 解析请求（根域-com域-所属域，拿到后返回给操作系统内核同时缓存起来，操作系统内核返回给浏览器）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/dns.BYEsDRdv_1TeHCb.webp&quot; alt=&quot;dns&quot; title=&quot;dns&quot;&gt;&lt;/p&gt;
&lt;h2&gt;TCP/IP 连接建立&lt;/h2&gt;
&lt;p&gt;浏览器获得对应的 &lt;code&gt;IP&lt;/code&gt; 地址后，就可以向服务器发送 &lt;code&gt;TCP&lt;/code&gt; 请求了，为了方便传输，&lt;code&gt;TCP&lt;/code&gt; 协议大块数据被分割成了以报文段（&lt;code&gt;sgment&lt;/code&gt;）为单位的数据包，同时为了准确无误地将数据送达目标，&lt;code&gt;TCP&lt;/code&gt; 协议采用了三次握手策略（&lt;code&gt;three-way handshaking&lt;/code&gt;），用 &lt;code&gt;TCP&lt;/code&gt; 协议把数据包送出去后，&lt;code&gt;TCP&lt;/code&gt; 不会对传送后的情况置之不理，它一定会向对方确认是否成功送达。握手过程中使用了 &lt;code&gt;TCP&lt;/code&gt; 的标志（&lt;code&gt;flag&lt;/code&gt;）——&lt;code&gt;SYN&lt;/code&gt;（&lt;code&gt;synchronize&lt;/code&gt;）和 &lt;code&gt;ACK&lt;/code&gt;（&lt;code&gt;acknowledgement&lt;/code&gt;）。发送端首先发送一个带 &lt;code&gt;SYN&lt;/code&gt; 标志的数据包给对方。接收端收到后，回传一个带有 &lt;code&gt;SYN/ACK&lt;/code&gt; 标志的数据包以示传达确认信息。最后，发送 端再回传一个带 &lt;code&gt;ACK&lt;/code&gt; 标志的数据包，代表“握手”结束。若在握手过程中某个阶段莫名中断，&lt;code&gt;TCP&lt;/code&gt; 协议会再次以相同的顺序发送相同的数据包。（图片来自图解 &lt;code&gt;HTTP&lt;/code&gt;） &lt;img src=&quot;https://clloz.com/_astro/tcp.DR6FhKiL_Z1LK8Ih.webp&quot; alt=&quot;tcp&quot; title=&quot;tcp&quot;&gt;&lt;/p&gt;
&lt;h2&gt;发起 HTTP 请求&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;TCP/IP&lt;/code&gt; 连接建立起来以后，浏览器就可以向服务器发送 &lt;code&gt;http&lt;/code&gt; 请求了；&lt;/p&gt;
&lt;h2&gt;web 服务器处理请求&lt;/h2&gt;
&lt;p&gt;服务器上的 &lt;code&gt;web&lt;/code&gt; 服务器负责接收用户发出的请求，在收到请求后把请求内容交给网站的代码或者是转发给对应的 &lt;code&gt;web&lt;/code&gt; 服务器。收到请求后对应的后台模块根据请求的资源和参数开始工作，处理完成后将对应的数据或者是 &lt;code&gt;HTML&lt;/code&gt; 页面交给 &lt;code&gt;web&lt;/code&gt; 服务器，再由 &lt;code&gt;web&lt;/code&gt; 服务器按上述的路径返回给浏览器。&lt;/p&gt;
&lt;h2&gt;浏览器渲染&lt;/h2&gt;
&lt;p&gt;浏览器收到返回结果以后进行解析和渲染，对于 &lt;code&gt;html&lt;/code&gt; &lt;code&gt;文件的css&lt;/code&gt;，&lt;code&gt;js&lt;/code&gt;，图片等静态资源都是 &lt;code&gt;http&lt;/code&gt; 请求都需要经过上述步骤。浏览器把完整的页面展示出来。&lt;/p&gt;
&lt;p&gt;完整的请求过程如下图（图来自《图解HTTP》） &lt;img src=&quot;https://clloz.com/_astro/http.B71HaTMK_Z1VDbnU.webp&quot; alt=&quot;http&quot; title=&quot;http&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/browser.gq-LwpuH.jpg"/><enclosure url="/_astro/browser.gq-LwpuH.jpg"/></item><item><title>最好的比赛型选手Mark Selby</title><link>https://clloz.com/blog/mark-selby</link><guid isPermaLink="true">https://clloz.com/blog/mark-selby</guid><pubDate>Fri, 09 Nov 2018 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我是个非常业余的斯诺克爱好者，准确的说我没怎么打过斯诺克，基本和朋友玩都是玩中式八球，打的也很一般，纯属娱乐的那种，在我还小的时候我看着电视上的桌球比赛就和看乒乓球比赛一样，完全不知道这些人在干嘛，真不知道大人怎么看得下去的。&lt;/p&gt;
&lt;p&gt;后来上了高中，由于篮球打的多，我突然对这种对力量角度控制极其严格的运动产生了极大兴趣，一有时间就和同学跑到学校对面的一家眼镜店的楼上打台球，那个台球桌质量非常差，空间也很小，经常遇到一些特殊角度的球球杆摆不开的情况，不过几个穷学生，去不起那种高级的桌球室，这种6块钱一个小时我们也的几个人凑一凑。&lt;/p&gt;
&lt;p&gt;也是从这个时候开始，一有斯诺克比赛就会看，虽然高中的学习生活还是很紧张的，但是父母可能也觉得这并不是什么不良嗜好，一般有比赛也会让我看的。从大大小小的比赛中，我开始认识了一些斯诺克选手，奥沙利文，亨德利，丁俊晖，等等，这其中我最喜欢的就是马克塞尔比，我从他还没啥成绩的时候就很喜欢他，今天就来说说我最喜欢的这名斯诺克选手。&lt;/p&gt;
&lt;p&gt;最初喜欢塞尔比他的外貌应该也是有一定影响的，我刚看球那会儿塞尔比刚刚二十三四岁，他梳着一个莫西干的头，总是面带搞怪的微笑，在80后的球员当中，塞尔比和尼尔罗伯逊的颜值应该算是最高的，塞尔比由于他的性格比较搞怪，心态总是比较好也愿意跟观众互动，让更多的人喜欢这个英国大男孩，我听庞卫国指导说过，塞尔比在中国是有相当多女粉丝的。&lt;/p&gt;
&lt;p&gt;不过真正让我喜欢上塞尔比的是他在比赛中强大的心理素质和韧性，我自己很喜欢运动，也打过比赛，所以我很欣赏这种类型的运动员，塞尔比在比赛中表现出来的坚韧我认为同样是前无古人的。在很多人会放弃该局的局面，他不会，有时候甚至对面超分要做四五杆斯诺克他依然去尝试，在自己状态不好的时候他会放慢比赛节奏，更多的去防守以调整自己的手感。对于任何一个选手，面对塞尔比都是很头疼的，因为他太难缠了。他的这种比赛风格也为他招来了很多非议，在他没出成绩的时候还好，出了成绩以后很多球迷就会觉得他这种风格的比赛胜之不武，毕竟大家都喜欢看酣畅淋漓的进攻，如火箭，小特，小丁这种，甚至有些人拿著名大“磨”王彼得艾伯顿类比塞尔比，认为他继承了艾伯顿的衣钵，是下一代磨王。当我一直认为职业运动员就应该是功利的，就像乒乓球运动员在看到你有一个明显的弱点会一直盯着你这个弱点攻击，只要你没找到解决办法，你就无法取胜，况且塞尔比只是控制比赛节奏，和艾伯顿那种故意拖延时间是不一样的，他的攻击火力并不差，塞尔比的比赛我也不觉得像足球场上摆大巴那样无聊，还是相当具有观赏性的，况且斯诺克本身就是一个防守相当重要的项目，因为进攻火力再强的选手也总有手感不好的时候，在你手感不好的时候怎么调整，有些选手是默默等待，塞尔比是主动求变，控制节奏，他之所以有时候会去打一些好像已经没机会的局面，一方面是努力争取，另一方面是多到台上去打一打，不要一直坐着，自己找找手感，当对手手感火热的时候，经常是另一名选手在座位上看别人表演。他的韧性和爱动脑筋是最让我欣赏的两个方面，他的韧性和心态也让他打出了很多绝地翻盘的比赛，比如10年两次对奥沙利文的逆转（世锦赛1/4决赛5：9 - 13：11;大师赛6：9 - 10：9），那时候很多人称他为逆转王，也是从那个时候开始，塞尔比渐渐开始崭露头角，一点一点向着自己的巅峰前进。&lt;/p&gt;
&lt;p&gt;塞尔比如此坚韧的精神和乐观的心态是与他人生经历分不开的，八岁的时候母亲和父亲关系恶化，抛下他和父亲离家出走从此再没出现过。后来他父亲带领他接触斯诺克，在莱斯特城的一家斯诺克俱乐部打球，他父亲就是该俱乐部的一名成员，他在这个俱乐部练习了两年，技术得到了很大的提高，一些成年人已经打不过他了，后来俱乐部不让他来打球了，原因是有些人认为输给一个孩子很丢人：），后来在沟通下他们同意他只能在每周六来练习两到三个小时，这时候塞尔比已经开始陆续击败一些俱乐部成员。还好后来莱斯特出生的斯诺克选手威利 ·索恩的兄弟马尔科姆·索恩发现塞尔比的斯诺克能力，为他提供了练习的场所塞尔比每天放学后就去他那里练习，一直持续了五六年，塞尔比说这是他最快乐的时光。在他还是个初中生的时候，他的父亲就带着他去全国各地参加比赛，他父亲尽自己所能赚更多的钱，把钱全部投入到他的身上，塞尔比一直很感激他的父亲为他所作的一切。不过短暂快乐的时光过后，塞尔比的人生遇到了极大的变故，1999年在他成为职业选手的两个月后，他的父亲以为癌症去世了，从检查到癌症到去世只过了一个月。他此时还未成年，只能寄居在堂哥家中，在人生遇到如此大挫折的时候，他没有想罗尼一样去用药物麻痹自己，他把自己埋在了训练室，疯狂的练习。同时可能因为缺乏母爱并且失去家庭的原因，他在19岁和一个大自己许多的女性结婚并且生了一个女儿，然后有草草离婚了。除了斯诺克他还会打一些其他类型的比赛比如花式桌球，因为有奖金。我想正是生活对他的这种磨砺让他如此坚韧，同时非常在意胜负，打球就是为了赢，这是他的信条，我认为对的。像奥沙利文那种享受比赛，完美主义同时还能赢球固然很吸引人，可是斯诺克这么多年，也就一个奥沙利文，其他选手要赢球只能找准自己的特点，找准自己的长处，让自己在比赛中尽量发挥自己的长处而规避自己的短处。他这种胜负心很重的心态在后来参加保罗亨特的葬礼后得到了改变，他觉得自己对于失败的态度更加成熟了。他父亲告诉他的无论输球或是赢球都不要把喜怒表现在脸上，我认为他做的非常好。上帝让他遭受了很多常人没有经历过的苦难，只留给他斯诺克，他紧紧的抓住了这跟稻草，并且通过自己的精神与智慧，成为了一名伟大的选手。早些年黑塞尔比成为了一种时尚，很多人说他没有风度，胜之不武，特别是很多罗尼的粉丝，但是伴随着他四年三夺世锦赛冠军，连续四年排名世界第一（仅次于亨德利6年和戴维斯5年），批评塞尔比的人要少很多了。说到底很多人没明白一个道理，职业运动员最终是靠成绩说话的。塞尔比自己也是一个不在乎舆论，非常自信固执的人，他会对你保持尊重，但同时他也会贯彻自己的想法执行到底。&lt;/p&gt;
&lt;p&gt;在奥沙利文点名的八个有斯诺克智商的球员，吉米-怀特、阿历克斯-希金斯，希金斯、亨特、亨得利、史蒂夫-戴维斯、丁俊晖，也许还包括马克-塞尔比。我认为就打球的天赋而言塞尔比是这九个人当中最差的，亨特英年早逝，吉米怀特，亚历克斯希金斯我基本只能在录像中看他们的比赛，亨德利和戴维斯虽然我也看到他们职业生涯末端，但是他们已经不在巅峰。在80后球员里面，小丁的天赋绝对是最高的，我觉得小丁是跟希金斯一个档次的，奥沙利文和亨德利要高一点（小丁之前一直没能保持很好的比赛心态，导致他有点失去信心了，但是这两年小丁还是比以前成熟的，我相信他一定能拿到世界赛冠军）。但就智商和比赛心理我觉得塞尔比是前无古人的，斯诺克大赛往往是很长的赛制，在你手感不那么好的时候，如何控制自己的心态，如何控制局势，是赢得最后胜利的重要条件，换句话说，这是一种天赋。心态容易爆炸的马奎尔，包括丁俊晖，都不是能很好控制自己状态。这也是史蒂夫戴维斯说他是最好的比赛型选手，他是唯一能在自己状态不好的时候还能赢球的选手。&lt;/p&gt;
&lt;p&gt;希望塞尔比保持状态，为我们奉献更多的精彩比赛，这位在自己球杆上印着小丑团的莱斯特城小丑，相信他还能在舞台上表演出更多精彩的篇章。 &lt;img src=&quot;https://clloz.com/_astro/selby1.BRURZqdh_2qmg3z.webp&quot; alt=&quot;selby&quot; title=&quot;selby&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/mark-selby.DT7-fPap.jpg"/><enclosure url="/_astro/mark-selby.DT7-fPap.jpg"/></item><item><title>罗技游戏软件Logitech Gaming Software 安装死机解决方法</title><link>https://clloz.com/blog/logitech-gaming-software-install-freeze</link><guid isPermaLink="true">https://clloz.com/blog/logitech-gaming-software-install-freeze</guid><pubDate>Mon, 29 Oct 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;这是好几年前遇到的问题了，我几年前配台式机的时候买的罗技G502，那时候安装罗技的软件就一直装不上，大概是从换Win10开始，每次安装罗技的Gaming Software，进度条走到一半稳稳死机，然后Win10会弹出一个收集错误的蓝屏，只能重启，搞得我很郁闷，一直没找到解决办法。&lt;/p&gt;
&lt;p&gt;当时的方法是我在笔记本上安装了这个软件把鼠标速度设置好以后再拿到台式机上用，有点麻烦。后来不知道是不是更新了Win10版本还是软件版本的更新，在台式机上再次尝试安装该软件，虽然也死机了，但是重启电脑后发现软件安装好了，大概是8.8的版本，莫名其妙的安装好了，我也就凑合着用了。&lt;/p&gt;
&lt;p&gt;前两天我的G900鼠标左键大概率双击，找罗技他们答应帮我换一个新的，因为G900停产了，所以给我换一个G903,我欣然接受了，毕竟G903还是G900的升级版，两者价格差不多，我的G900用了两年了，给我换个新的我何乐而不为呢。G903是支持无线充电的，要配合一个powerplay的无线充电鼠标垫，因为之前G900大概两三天就要充一次电还是挺麻烦的，所以既然他们给我换了G903我就买了一个powerplay的鼠标垫。问题来了，我当前用的罗技游戏软件的版本是不支持powerplay的需要下载新的版本重新安装，只能去挑战一下这个老问题了。&lt;/p&gt;
&lt;h2&gt;解决&lt;/h2&gt;
&lt;p&gt;下载了大概三个版本的Logitech Gaming Software，多次安装重启，都是同样的结果，大概进度条走到一半就挂掉了，我折腾电脑也听多年了，一半这种情况下就是硬件冲突，我以前一直以为是我之前安装rog的前置面板导致的冲突，后面我把rog的前置面板拿拆掉了，还是装不上，我当时怀疑主板的针脚被我搞得有问题了，所以一直搁置，毕竟鼠标速度可调就行了。&lt;/p&gt;
&lt;p&gt;这次必须安装新版本，我仔细地检查了一下设备管理器，没任何问题，试了在安全模式下安装，依然死机。我想剑走偏锋，到笔记本上安装完复制过来，不过家里只有mac，Windows的笔记本给我哥了。于是只能去Google了，在罗技的社区论坛上终于找到了跟自己遇到相同问题的人，他们有人也是按我刚刚说的&lt;strong&gt;找一台没有问题的电脑，安装然后复制&lt;/strong&gt;，该方法有效！&lt;/p&gt;
&lt;p&gt;我当前没这个条件，只能看看别的，最终定位问题大概是&lt;strong&gt;软件的安装与光驱冲突&lt;/strong&gt;。我现在知道为什么大部分人遇不到这个问题了，现在大部分PC用户已经舍弃光驱了，我当时配电脑的时候想着总归用得到也不贵就加了个光驱，没想到光驱才是罪魁祸首。我先尝试在设备管理器里面禁用光驱，但是安装依然死机，于是只能先把光驱拆了，把光驱连接主板的sata线拔下来就可以，然后运行软件，大概15秒就装好了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;写两点总结：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这个问题从我遇到到现在应该已经三四年了，罗技的软件版本也更新了很多次了，但是一直不解决这个问题，而且他们的网站上也没有就这个问题给出祥光说明，导致大家都云里雾里不知道解决方案，硬件冲突的问题只能一一排查，而且购买罗技鼠标的用户也不一定就很了解PC的硬件，很多人是没有能力解决的，罗技的社区问答也多是英文，对一些不明白的用户也不是很友好，而且罗技的网站真的是卡，我开着SS或者VPN都打开很慢。说白了还是现在使用光驱的人少了，罗技可能懒得花精力解决小部分用户的需求。&lt;/li&gt;
&lt;li&gt;虽然大家都说现在罗技的鼠标品控也大不如从前了，但是我觉得在雷蛇，赛睿和罗技里面我还是愿意选择罗技，雷蛇的产品已经多年不用了，我个人经历，雷蛇给我留下了非常差的印象，不知道他们现在怎么样了，不过我是不可能再去用雷蛇的产品了。罗技的鼠标用了两款G502，G900，都非常不错，虽然G900价格更高更轻便，但是我还是喜欢502的外形和左右键的触感。但是G900是无线的，就这点足以让我抛弃G502了，虽然我使用下来还是觉得对称鼠标不太适合我，不过还算可以吧。而且我这个用了快两年的鼠标还在保修期内他们给我换个新的还是很良心的，鼠标我应该还是会选择罗技的，也推荐游戏玩家使用。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;笔记本的话目前不知道有什么好的解决办法，另外如果你的G900或者G903有时会出现延迟，很可能是你把无线接收器连接在了机箱或者显示器上的usb接口上了，这些接口是通过线连在主板上的，机箱中的干扰还是很严重的，最好是直接连在主板的usb接口上。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="/_astro/logitech.IemIHvMn.jpg"/><enclosure url="/_astro/logitech.IemIHvMn.jpg"/></item><item><title>WordPress重置.htaccess文件</title><link>https://clloz.com/blog/htaccess-reset</link><guid isPermaLink="true">https://clloz.com/blog/htaccess-reset</guid><pubDate>Tue, 23 Oct 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在博客启用 &lt;code&gt;https&lt;/code&gt; 协议以及设置 &lt;code&gt;errorpages&lt;/code&gt; 完成后，遇到过多次 &lt;code&gt;https&lt;/code&gt; 强制跳转不生效，&lt;code&gt;errorpages&lt;/code&gt; 不跳转的情况，一开始以为是自己的 &lt;code&gt;rewrite&lt;/code&gt; 配置有问题，后来发现是 &lt;code&gt;wordpress&lt;/code&gt; 重置了我的 &lt;code&gt;.htaccess&lt;/code&gt; 文件。&lt;/p&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;p&gt;只要在 &lt;code&gt;wordpress&lt;/code&gt; 后台点击了&lt;code&gt;Settings&lt;/code&gt;中的固定链接菜单， &lt;code&gt;Wordpress&lt;/code&gt; 就会重置服务器根目录下的&lt;code&gt;.htaccess&lt;/code&gt;文件中的 &lt;code&gt;wordpress&lt;/code&gt; 部分，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# BEGIN WordPress
&amp;#x3C;IfModule mod_rewrite.c&gt;
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
&amp;#x3C;/IfModule&gt;
# END WordPress
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果把自己的配置写在&lt;code&gt;BEGIN WordPress&lt;/code&gt;和&lt;code&gt;END WordPress&lt;/code&gt;之间的话每次点击固定链接，都会被重置，所以解决方法就是吧我们自定的配置写到 &lt;code&gt;Wordpress&lt;/code&gt; 之外即可，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;#x3C;IfModule mod_rewrite.c&gt;
RewriteEngine On
RewriteBase /
RewriteCond %{SERVER_PORT} 80
RewriteRule ^(.*)$ https://www.clloz.com/$1 [R=301,L]
RewriteRule . /index.php [L]
ErrorDocument 403 /403.html
ErrorDocument 404 /404.html
&amp;#x3C;/IfModule&gt;

# BEGIN WordPress
&amp;#x3C;IfModule mod_rewrite.c&gt;
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
&amp;#x3C;/IfModule&gt;
# END WordPress
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/apache.DHQksI7g.png"/><enclosure url="/_astro/apache.DHQksI7g.png"/></item><item><title>Mac设置terminal快捷键</title><link>https://clloz.com/blog/terminal-shortcut</link><guid isPermaLink="true">https://clloz.com/blog/terminal-shortcut</guid><pubDate>Tue, 23 Oct 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mac&lt;/code&gt; 没有为 &lt;code&gt;terminal&lt;/code&gt; 的打开提供快捷键，有时候觉得不是很方便，今天和大家分享一下在 &lt;code&gt;mac&lt;/code&gt; 上绑定 &lt;code&gt;terminal&lt;/code&gt; 的快捷键的方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可以绑定任何操作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Automater&lt;/h2&gt;
&lt;p&gt;打开 &lt;code&gt;application&lt;/code&gt; 中的 &lt;code&gt;automator&lt;/code&gt;，&lt;code&gt;automator&lt;/code&gt; 其实就是用来自定义工作流以及服务的软件，用好这个软件可以自己定制自己的工作流或者自定义服务，提高自己使用 &lt;code&gt;mac&lt;/code&gt; 的效率。 我们要为“打开 &lt;code&gt;terminal&lt;/code&gt; ”动作新建一个服务流程如下， 1. 新建一个服务，选择任务栏上的 &lt;code&gt;File-&gt;New-&gt;Service&lt;/code&gt;,如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/terminal1.C4iiYhjN_Z5mS9P.webp&quot; alt=&quot;terminal1&quot; title=&quot;terminal1&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;添加服务内容，在左边的菜单栏依次选择 &lt;code&gt;Utilities-&gt;Run AppleScript&lt;/code&gt;,然后添加如下代码&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/terminal2.G3lpzhn8_ZzpvN.webp&quot; alt=&quot;terminal2&quot; title=&quot;terminal2&quot;&gt;&lt;/p&gt;
&lt;p&gt;添加完之后点击黑色箭头运行，如果能够打开 &lt;code&gt;terminal&lt;/code&gt; 说明服务没有问题，&lt;code&gt;command+S&lt;/code&gt; 保存该工作流，你可以将其命名为“&lt;code&gt;Open Terminal&lt;/code&gt;” 3. 绑定快捷键，在 &lt;code&gt;System Preferences-&gt;Keyboard-&gt;Shortcuts-&gt;Services-&gt;General&lt;/code&gt; 中找到刚刚添加的 &lt;code&gt;Open terminal&lt;/code&gt; 绑定自己想要的快捷键就可以了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Automator&lt;/code&gt; 有很多有用的功能，为应用绑定快捷键只是其中之一，工作流也可以极大的提高我们使用 &lt;code&gt;mac&lt;/code&gt; 的效率。&lt;/p&gt;</content:encoded><h:img src="/_astro/automator.B4hb3j-r.png"/><enclosure url="/_astro/automator.B4hb3j-r.png"/></item><item><title>flex布局学习笔记</title><link>https://clloz.com/blog/flexbox</link><guid isPermaLink="true">https://clloz.com/blog/flexbox</guid><pubDate>Mon, 08 Oct 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;flexbox&lt;/code&gt;（&lt;a href=&quot;https://www.w3.org/TR/css-flexbox-1/&quot; title=&quot;CSS Flexible Box Layout Module&quot;&gt;CSS Flexible Box Layout Module&lt;/a&gt;）是 &lt;code&gt;CSS3&lt;/code&gt; 新增的特性，是一种新的弹性盒模型布局，这是一种在二维层面上的布局模型，它不仅可以让我们方便的分配空间给盒子中的元素，甚至是分配给元素的周围，同时也提供了在二维方向上对齐元素的功能，弹性盒模型布局让我们在实现响应式的页面的时候能够更方便更自如的实现功能。&lt;/p&gt;
&lt;h2&gt;CSS 中的布局模式&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;https://www.w3.org/TR/2016/WD-CSS22-20160412/Overview.html#minitoc&quot; title=&quot;CSS2.2规范&quot;&gt;CSS2.2规范&lt;/a&gt;里出现了四种布局模式（&lt;code&gt;layout mode&lt;/code&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;块布局：用来布置文件。块布局包含以文档为中心的功能，例如 浮动元素或将其放置在多列上的功能。&lt;/li&gt;
&lt;li&gt;行内布局：用来布置文本。&lt;/li&gt;
&lt;li&gt;表格布局：用来布置表格。&lt;/li&gt;
&lt;li&gt;定位布局：用来对那些与其他元素无交互的定位元素进行布置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;布局模式其实是一种基于盒子与其兄弟和祖辈盒子的交互方式来确定盒子的位置和大小的算法，浏览器根据文档中元素的 &lt;code&gt;CSS&lt;/code&gt; 属性来确定用哪种布局。为了应对新的需求，在 &lt;code&gt;CSS3&lt;/code&gt; 里面新增了两种新的布局： - 弹性盒子布局：用来布置那些可以顺利调整大小的复杂页面。 - 网格布局：用来布置那些与一个固定网格相关的元素。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;flex&lt;/code&gt; 布局已经进入 &lt;code&gt;Candidate Recommendation (CR)&lt;/code&gt; 阶段，&lt;code&gt;grid&lt;/code&gt; 布局目前还处在 &lt;code&gt;Working Draft (WD)&lt;/code&gt; 工作草案阶段，属于实验性 &lt;code&gt;API&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;浮动，&lt;code&gt;display&lt;/code&gt; 和 &lt;code&gt;position&lt;/code&gt; 属性都会影响布局模式。&lt;/p&gt;
&lt;h2&gt;弹性布局模式&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CSS3&lt;/code&gt; 引入新的布局模式自然是因为已有的布局模式不能适应页面的发展，&lt;code&gt;CSS2&lt;/code&gt; 中的四种布局方式在目前的复杂页面上，尤其是大量页面需要响应式的适配各种设备，实现起来比较复杂，这样的需求自然引出了弹性布局的模式。弹性布局表面上看和块布局相似，但是失去了块布局中的一些属性，比如 &lt;code&gt;float&lt;/code&gt;，&lt;code&gt;clear&lt;/code&gt; 和 &lt;code&gt;vertical-align&lt;/code&gt;，相对的，弹性布局获得了分配空间和设置对齐方式的极大灵活性。我们通过设置元素的 &lt;code&gt;display: flex&lt;/code&gt; 或者 &lt;code&gt;diplay: inline-flex&lt;/code&gt; 来初始化一个弹性容器（ &lt;code&gt;flex container&lt;/code&gt; ），弹性容器中的流内（ &lt;code&gt;in-flow&lt;/code&gt; ）子元素称之为弹性项目（ &lt;code&gt;flex items&lt;/code&gt; ），在这个弹性容器中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以沿任何流动方向布置（向左，向右，向下，甚至向上！）&lt;/li&gt;
&lt;li&gt;可以在样式层中反转或重新排列其显示顺序（即，视觉顺序可以独立于源和语音顺序）&lt;/li&gt;
&lt;li&gt;可沿单个（主）轴线性布局或沿二级（交叉）轴包裹成多条线&lt;/li&gt;
&lt;li&gt;可以“弯曲”它们的尺寸以响应可用空间&lt;/li&gt;
&lt;li&gt;可以在二级（十字架）上相对于其容器或彼此对齐&lt;/li&gt;
&lt;li&gt;可以沿主轴动态折叠或不折叠，同时保留容器的交叉大小&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们在 &lt;code&gt;CSS2&lt;/code&gt; 中接触了 &lt;code&gt;IFC&lt;/code&gt; 和 &lt;code&gt;BFC&lt;/code&gt; ，&lt;code&gt;flex&lt;/code&gt; 容器同样生成一个 &lt;code&gt;flex formatting context&lt;/code&gt;，和 &lt;code&gt;BFC&lt;/code&gt;相似，不过用弹性布局代替了块布局。在 &lt;code&gt;FFC&lt;/code&gt; 中，容器的 &lt;code&gt;margin&lt;/code&gt; 不会和内容的 &lt;code&gt;margin&lt;/code&gt; 发生塌陷（ &lt;code&gt;collapse&lt;/code&gt; ）；在 &lt;code&gt;FFC&lt;/code&gt; 中，&lt;code&gt;float，clear&lt;/code&gt;，&lt;code&gt;vertical-align&lt;/code&gt;，&lt;code&gt;::first-line and&lt;/code&gt; &lt;code&gt;::first-letter&lt;/code&gt; 都是无效的。容器中的空白符不会显示，相当于添加了 &lt;code&gt;display: none&lt;/code&gt; 的文本节点一样。&lt;/p&gt;
&lt;h2&gt;flex布局&lt;/h2&gt;
&lt;p&gt;设置了 &lt;code&gt;display&lt;/code&gt; 为 &lt;code&gt;flex&lt;/code&gt; 或者 &lt;code&gt;inline-flex&lt;/code&gt; 的元素就成为了一个弹性容器，理解 &lt;code&gt;flex&lt;/code&gt; 布局就要理解 &lt;code&gt;flexible box&lt;/code&gt; 的基本概念，在容器中有两根轴，水平的主轴（ &lt;code&gt;main axis&lt;/code&gt; ）和垂直的交叉轴（ &lt;code&gt;cross axis&lt;/code&gt; ）。主轴的开始位置（与边框的交叉点）叫做 &lt;code&gt;main start&lt;/code&gt;，结束位置叫做 &lt;code&gt;main end&lt;/code&gt; ；交叉轴的开始位置叫做 &lt;code&gt;cross start&lt;/code&gt;，结束位置叫做 &lt;code&gt;cross end&lt;/code&gt;。项目默认沿主轴排列。弹性容器活弹性项目主轴空间叫做 &lt;code&gt;main size&lt;/code&gt;，占据的交叉轴空间叫做 &lt;code&gt;cross size&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;主轴和交叉轴不是绝对的，并不是水平的轴就是主轴，这取决于你的 &lt;code&gt;flex-direction&lt;/code&gt; 属性。 当时设置 &lt;code&gt;flex&lt;/code&gt; 布局之后，子元素的 &lt;code&gt;float&lt;/code&gt;、&lt;code&gt;clear&lt;/code&gt;、&lt;code&gt;vertical-align&lt;/code&gt; 的属性将会失效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://img.clloz.com/blog/writing/flex-direction-terms.svg&quot; alt=&quot;flex&quot; title=&quot;flex&quot;&gt;&lt;/p&gt;
&lt;h2&gt;容器（flex container）&lt;/h2&gt;
&lt;p&gt;容器有6个属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flex-direction&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-wrap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-flow&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;justify-content&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;align-items&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;align-content&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;flex-direction&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;flex-direction&lt;/code&gt; 决定了容器主轴的方向，也就是决定了 &lt;code&gt;flex items&lt;/code&gt; 在容器里如何排列，&lt;code&gt;flexbox&lt;/code&gt; 是单向布局概念，可以理解为 &lt;code&gt;item&lt;/code&gt; 在水平行或垂直列中排列。 &lt;code&gt;flex-direction&lt;/code&gt; 可以取下面四个值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  flex-direction: row | row-reverse | column | column-reverse;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex2.8NMF_ket_8LtU1.webp&quot; alt=&quot;flex-direction&quot; title=&quot;flex-direction&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;row（默认值）：主轴为水平方向，起点在左端。&lt;/li&gt;
&lt;li&gt;row-reverse：主轴为水平方向，起点在右端。&lt;/li&gt;
&lt;li&gt;column：主轴为垂直方向，起点在上沿。&lt;/li&gt;
&lt;li&gt;column-reverse：主轴为垂直方向，起点在下沿。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;container&lt;/code&gt; 的 &lt;code&gt;direction&lt;/code&gt; 属性会影响 &lt;code&gt;row&lt;/code&gt; 和 &lt;code&gt;row-reverse&lt;/code&gt; 的表现，当 &lt;code&gt;direction&lt;/code&gt; 为 &lt;code&gt;ltr&lt;/code&gt; 的时候表现为上述，当为 &lt;code&gt;rtl&lt;/code&gt; 的时候则相反。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;flex-wrap&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex3.COxkp8VV_Z1Q6icy.webp&quot; alt=&quot;flex-wrap&quot; title=&quot;flex-wrap&quot;&gt;&lt;/p&gt;
&lt;p&gt;默认情况下 &lt;code&gt;flex items&lt;/code&gt; 排在一条直线上，你可以通过 &lt;code&gt;flex-wrap&lt;/code&gt; 属性来设置项目的换行方式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  flex-wrap: nowrap | wrap | wrap-reverse;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;flex-wrap&lt;/code&gt; 有三个值： （1） &lt;code&gt;nowrap&lt;/code&gt;（默认）：不换行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex4.B1Gt78WH_GWHfp.webp&quot; alt=&quot;flex-wrap1&quot; title=&quot;flex-wrap1&quot;&gt;&lt;/p&gt;
&lt;p&gt;（2） &lt;code&gt;wrap&lt;/code&gt;：项目换行，按顺序从上到下排列。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex5.EyRm9zH7_1KJfjj.webp&quot; alt=&quot;flex-wrap2&quot; title=&quot;flex-wrap2&quot;&gt;&lt;/p&gt;
&lt;p&gt;（3） &lt;code&gt;wrap-reverse&lt;/code&gt;：项目换行，按顺序从下到上排列。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex6.DYooeJx0_Zd7wf2.webp&quot; alt=&quot;flex-wrap3&quot; title=&quot;flex-wrap3&quot;&gt;&lt;/p&gt;
&lt;h2&gt;flex-flow&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;flex-flow&lt;/code&gt; 属性是 &lt;code&gt;flex-direction&lt;/code&gt; 属性和 &lt;code&gt;flex-wrap&lt;/code&gt; 属性的简写形式，默认值为 &lt;code&gt;row nowrap&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  flex-flow: &amp;#x3C;‘flex-direction’&gt; || &amp;#x3C;‘flex-wrap’&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;justify-content&lt;/h2&gt;
&lt;p&gt;该属性定义了项目在主轴上的对齐方式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img.clloz.com/blog/writing/justify-content-2.svg&quot; alt=&quot;justify-content&quot; title=&quot;justify-content&quot;&gt;&lt;/p&gt;
&lt;p&gt;该属性一共可以取6个值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flex-start&lt;/code&gt;( &lt;code&gt;default&lt;/code&gt; ): 项目向 &lt;code&gt;start line&lt;/code&gt; 对齐排列，首项与 &lt;code&gt;start line&lt;/code&gt; 对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-end&lt;/code&gt; : 项目向 &lt;code&gt;end line&lt;/code&gt; 对齐排列，首项在与 &lt;code&gt;end line&lt;/code&gt; 对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;center&lt;/code&gt; : 项目居中对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;space-between&lt;/code&gt; : 项目均匀分布在主轴上，首项与 &lt;code&gt;start line&lt;/code&gt; 对齐，末项与 &lt;code&gt;end line&lt;/code&gt; 对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;space-around&lt;/code&gt; : 项目均匀地分布在主轴上，每个项目的环绕空间相等。注意不是绝对的“平均”，由于每个项目两侧的空间相等，所以第一个项目的左边和最后一个项目右边的空间只有其他间隔的一半。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;space-evenly&lt;/code&gt; : 所有空白空间均匀地分布在项目之间，任意两个项目之间的间隔都相等（包括首尾与边缘的间隔）&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;start line&lt;/code&gt; 和 &lt;code&gt;end line&lt;/code&gt; 不是固定的，这取决于你的 &lt;code&gt;container&lt;/code&gt; 的 &lt;code&gt;flex-direction&lt;/code&gt; 和 &lt;code&gt;direction&lt;/code&gt; 两个属性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;align-items&lt;/h2&gt;
&lt;p&gt;这个属性定义了项目沿交叉轴布局的默认行为，类似于交叉轴上的 &lt;code&gt;justify-content&lt;/code&gt; 属性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex8.D_MO19ej_Z163PVT.webp&quot; alt=&quot;align-items&quot; title=&quot;align-items&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  align-items: flex-start | flex-end | center | baseline | stretch;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该属性可以取五个值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flex-start&lt;/code&gt; : 项目的 &lt;code&gt;croess-start&lt;/code&gt; 方向的 &lt;code&gt;margin&lt;/code&gt; 边缘对齐 &lt;code&gt;cross-start line&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-end&lt;/code&gt; : 项目的 &lt;code&gt;croess-end&lt;/code&gt; 方向的 &lt;code&gt;margin&lt;/code&gt; 边缘对齐 &lt;code&gt;cross-end line&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;center&lt;/code&gt; : 项目在交叉轴上居中对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;baseline&lt;/code&gt; : 项目的 &lt;code&gt;content&lt;/code&gt; 里的第一行文字的基线对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stretch&lt;/code&gt; ( &lt;code&gt;default&lt;/code&gt; ): 项目未设置高度或者高度为 &lt;code&gt;auto&lt;/code&gt; 则 &lt;code&gt;items&lt;/code&gt; 会占满整个容器的高度&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;flex-start&lt;/code&gt; 和 &lt;code&gt;flex-end&lt;/code&gt; 依然不是绝对的，当我们的主轴为 &lt;code&gt;column&lt;/code&gt; 的时候，&lt;code&gt;direction&lt;/code&gt; 和 &lt;code&gt;flex-direction&lt;/code&gt; 依然会影响 &lt;code&gt;align-items&lt;/code&gt; 的表现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;align-content&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;align-items&lt;/code&gt; 适用于当我们只有一行或者一列 &lt;code&gt;items&lt;/code&gt; 的情况，当我们拥有多行或者多列 &lt;code&gt;items&lt;/code&gt; 的时候就需要 &lt;code&gt;align-content&lt;/code&gt; 属性，它相当于 &lt;code&gt;justify-content&lt;/code&gt; 在交叉轴上的扩展，我们可以把每一行（每一列）想象成一个 &lt;code&gt;item&lt;/code&gt;，他们在交叉轴上的排列就可以类比 &lt;code&gt;justify-content&lt;/code&gt; 在主轴上的效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex9.DetT9iXD_Z2pYpSJ.webp&quot; alt=&quot;align-content&quot; title=&quot;align-content&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  align-content: flex-start | flex-end | center | space-between | space-around | stretch;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该属性可以取六个值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flex-start&lt;/code&gt; : 每一行向交叉轴起点对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-end&lt;/code&gt; : 每一行向交叉轴终点对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;center&lt;/code&gt; : 在交叉轴居中对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;space-between&lt;/code&gt; : 行均匀的分布在交叉轴上，第一行位于交叉轴的起点，最后一行位于交叉轴的终点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;space-around&lt;/code&gt; : 空白空间均匀分布在行之间，首行和末行与边沿之间的空白是行与行之间空白的一半&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stretch&lt;/code&gt; ( &lt;code&gt;default&lt;/code&gt; ): 所有的行占满全部交叉轴，第一行位于交叉轴起点，占满的方式是增加每一行与下一行或者边沿的空白空间，首行与边沿除外。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;依然要注意 &lt;code&gt;flex-start&lt;/code&gt; 和 &lt;code&gt;flex-end&lt;/code&gt; 不是绝对的。当 &lt;code&gt;flex items&lt;/code&gt; 只有一行的时候，该属性无效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;项目（ flex item ）&lt;/h2&gt;
&lt;p&gt;容器内的每一个 &lt;code&gt;in-flow&lt;/code&gt; 元素都是一个 &lt;code&gt;flex item&lt;/code&gt;，每个项目都有六个属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;order&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-grow&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-shrink&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-basis&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;align-self&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;order&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;order&lt;/code&gt; 属性定义项目的排列顺序。数值越小，排列越靠前，默认为0。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex10.ByAshwhG_d5Jxt.webp&quot; alt=&quot;order&quot; title=&quot;order&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  order: &amp;#x3C;integer&gt;; /* default is 0 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;flex-grow&lt;/h2&gt;
&lt;p&gt;该属性定义了 &lt;code&gt;flex item&lt;/code&gt; 放大的能力，它接受一个无比的值作为一个比例。它规定了项目应占用的 &lt;code&gt;Flex&lt;/code&gt; 容器内可用空间量。 如果所有项目都 &lt;code&gt;flex-grow&lt;/code&gt; 设置为 &lt;code&gt;1&lt;/code&gt;，则容器中的剩余空间将平均分配给所有子项。如果其中一个孩子的值为 &lt;code&gt;2&lt;/code&gt; ，则剩余空间将占用其他空间的两倍（或者至少会尝试）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex11.C2lhTQGA_11DicS.webp&quot; alt=&quot;flex-grow&quot; title=&quot;flex-grow&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  flex-grow: &amp;#x3C;number&gt;; /* default 0 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;负数无效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;flex-shrink&lt;/h2&gt;
&lt;p&gt;该属性定义了 &lt;code&gt;flex item&lt;/code&gt; 的缩小能力,默认为 &lt;code&gt;1&lt;/code&gt; ，即如果空间不足，该项目将缩小。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex12.scHFg0f9_Z1bcf5Y.webp&quot; alt=&quot;shrink&quot; title=&quot;shrink&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  flex-shrink: &amp;#x3C;number&gt;; /* default 1 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果所有项目的 &lt;code&gt;flex-shrink&lt;/code&gt; 属性都为 &lt;code&gt;1&lt;/code&gt;，当空间不足时，都将等比例缩小。如果一个项目的 &lt;code&gt;flex-shrink&lt;/code&gt; 属性为 &lt;code&gt;0&lt;/code&gt;，其他项目都为 &lt;code&gt;1&lt;/code&gt;，则空间不足时，前者不缩小。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;负数无效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;flex-basis&lt;/h2&gt;
&lt;p&gt;该属性定义了在分配空余空间之前一个项目的默认大小，当我们设置一个具体的值的时候（比如 &lt;code&gt;20%&lt;/code&gt;，&lt;code&gt;5em&lt;/code&gt;，&lt;code&gt;100px&lt;/code&gt; 等），那么元素的默认大小就是这个值，如果没有设置具体的值，那么这个属性默认是 &lt;code&gt;auto&lt;/code&gt; ，当属性为 &lt;code&gt;auto&lt;/code&gt; 的时候，会根据元素的主轴长度属性来定义元素的默认大小，如果主轴长度属性也是 &lt;code&gt;auto&lt;/code&gt; 的话，那么 &lt;code&gt;flex item&lt;/code&gt; 就会根据 &lt;code&gt;content&lt;/code&gt; 来设置大小。当主轴长度属性和 &lt;code&gt;flex-basis&lt;/code&gt; 同时存在的时候，生效的是 &lt;code&gt;flex-basis&lt;/code&gt;。&lt;code&gt;min-width&lt;/code&gt; 和 &lt;code&gt;max-width&lt;/code&gt; 能对 &lt;code&gt;flex-basis&lt;/code&gt; 产生限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当主轴为水平方向的时候，当设置了 &lt;code&gt;flex-basis&lt;/code&gt;，项目的宽度设置值会失效，&lt;code&gt;flex-basis&lt;/code&gt; 需要跟 &lt;code&gt;flex-grow&lt;/code&gt; 和 &lt;code&gt;flex-shrink&lt;/code&gt; 配合使用才能发挥效果。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;主轴长度属性指的是 &lt;code&gt;flex item&lt;/code&gt; 在主轴方向上的大小，当横轴是主轴的时候指的是元素的 &lt;code&gt;width&lt;/code&gt;，当主轴是纵轴的时候指的是元素的 &lt;code&gt;height&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;需要注意的是，当 &lt;code&gt;flex-grow&lt;/code&gt; 不为 &lt;code&gt;0&lt;/code&gt;，设置 &lt;code&gt;flex-basis&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt; 和 &lt;code&gt;auto&lt;/code&gt; 会产生不同的效果（当 &lt;code&gt;item&lt;/code&gt; 中有 &lt;code&gt;content&lt;/code&gt; 的时候），当 &lt;code&gt;flex-basis&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt; 的时候，不会考虑环绕元素的空白，而是元素根据 &lt;code&gt;flex-grow&lt;/code&gt; 全部分完所有空间。而 &lt;code&gt;flex-basis&lt;/code&gt; 为 &lt;code&gt;auto&lt;/code&gt; 的时候，会把空白的空间按照 &lt;code&gt;flex-grow&lt;/code&gt; 的值分配给每个 &lt;code&gt;item&lt;/code&gt;，此时 &lt;code&gt;item&lt;/code&gt; 的实际宽度（或者高度）就是 &lt;code&gt;content&lt;/code&gt; 的宽度 + 分配的空间。效果 &lt;code&gt;W3C&lt;/code&gt; 文档给出如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.clloz.com/blog/writing/rel-vs-abs-flex.svg&quot; alt=&quot;flex-basis&quot; title=&quot;flex-basis&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  flex-basis: &amp;#x3C;length&gt; | auto; /* default auto */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;经过测试，对 &lt;code&gt;item&lt;/code&gt; 的大小起作用的优先级 &lt;code&gt;max-width = max-width &gt; content &gt; flex-basis &gt; width&lt;/code&gt;，&lt;code&gt;flex-basis&lt;/code&gt; 只是一个默认值以便浏览器用以计算空白空间，实际项目的大小还要根据剩余的空间以及 &lt;code&gt;flex-grow&lt;/code&gt; 或者 &lt;code&gt;flex-shrink&lt;/code&gt; 来计算，如果空间不够了就会缩小，空间超过则会放大。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;flex&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;flex&lt;/code&gt; 属性是 &lt;code&gt;flex-grow&lt;/code&gt;, &lt;code&gt;flex-shrink&lt;/code&gt; 和 &lt;code&gt;flex-basis&lt;/code&gt; 的简写，默认值为 &lt;code&gt;0 1 auto&lt;/code&gt;。后两个属性可选。关键字 &lt;code&gt;none&lt;/code&gt; 的计算值为 &lt;code&gt;0 0 auto&lt;/code&gt;。当 &lt;code&gt;flex-grow&lt;/code&gt; 被省略时取值为 &lt;code&gt;1&lt;/code&gt;，&lt;code&gt;flex-shrink&lt;/code&gt; 被省略时取值 &lt;code&gt;1&lt;/code&gt;，&lt;code&gt;flex-basis&lt;/code&gt; 被省略是取值 &lt;code&gt;0&lt;/code&gt; 。这里的省略值与我们上面提到的三个属性的默认值是不同的，这样的设计是为了让这个简写属性能够更好的匹配我们的常用情景。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  flex: none | [ &amp;#x3C; &apos;flex-grow&apos; &gt; &amp;#x3C; &apos;flex-shrink&apos; &gt;? || &amp;#x3C; &apos;flex-basis&apos; &gt;];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;flex&lt;/code&gt; 的取值和规律总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「&lt;code&gt;flex: initial&lt;/code&gt;」：元素会根据自身宽高设置尺寸。它会缩短自身以适应 &lt;code&gt;flex&lt;/code&gt; 容器，但不会伸长并吸收 &lt;code&gt;flex&lt;/code&gt; 容器中的额外自由空间来适应 &lt;code&gt;flex&lt;/code&gt; 容器 。相当于将属性设置为&lt;code&gt;flex: 0 1 auto&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;「&lt;code&gt;flex: auto&lt;/code&gt;」：元素会根据自身的宽度与高度来确定尺寸，但是会伸长并吸收 &lt;code&gt;flex&lt;/code&gt; 容器中额外的自由空间，也会缩短自身来适应 &lt;code&gt;flex&lt;/code&gt; 容器。这相当于将属性设置为 &lt;code&gt;flex: 1 1 auto&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;「&lt;code&gt;flex: none&lt;/code&gt;」：元素会根据自身宽高来设置尺寸。它是完全非弹性的：既不会缩短，也不会伸长来适应 &lt;code&gt;flex&lt;/code&gt; 容器。相当于将属性设置为 &lt;code&gt;flex: 0 0 auto&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;「&lt;code&gt;flex: positive-number&lt;/code&gt;」：与「&lt;code&gt;flex: positive-number 1 0&lt;/code&gt;」相同。该值使元素可伸缩，并将伸缩基准值设置为零，导致该项目会根据设置的比率占用伸缩容器的剩余空间。如果一个伸缩容器里的所有项目都使用此模式，则它们的尺寸会正比于指定的伸缩比率。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;flex&lt;/code&gt; 取值为一个长度或百分比，则视为 &lt;code&gt;flex-basis&lt;/code&gt; 值，&lt;code&gt;flex-grow&lt;/code&gt; 取 &lt;code&gt;1&lt;/code&gt;，&lt;code&gt;flex-shrink&lt;/code&gt; 取 &lt;code&gt;1&lt;/code&gt;，有如下等同情况（注意 &lt;code&gt;0%&lt;/code&gt; 是一个百分比而不是一个非负数字）&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;flex&lt;/code&gt; 取值为 &lt;code&gt;0&lt;/code&gt; 时，对应的三个值分别为 &lt;code&gt;0 1 0%&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;flex&lt;/code&gt; 取值为两个非负数字，则分别视为 &lt;code&gt;flex-grow&lt;/code&gt; 和 &lt;code&gt;flex-shrink&lt;/code&gt; 的值，&lt;code&gt;flex-basis&lt;/code&gt; 取 &lt;code&gt;0%&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;flex&lt;/code&gt; 取值为一个非负数字和一个长度或百分比，则分别视为 &lt;code&gt;flex-grow&lt;/code&gt; 和 &lt;code&gt;flex-basis&lt;/code&gt; 的值，&lt;code&gt;flex-shrink&lt;/code&gt; 取 &lt;code&gt;1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;flex-wrap&lt;/code&gt; 为 &lt;code&gt;wrap | wrap-reverse&lt;/code&gt;，且子项宽度和不及父容器宽度时，&lt;code&gt;flex-grow&lt;/code&gt; 会起作用，子项会根据 &lt;code&gt;flex-grow&lt;/code&gt; 设定的值放大（为 &lt;code&gt;0&lt;/code&gt; 的项不放大）&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;flex-wrap&lt;/code&gt; 为 &lt;code&gt;wrap | wrap-reverse&lt;/code&gt;，且子项宽度和超过父容器宽度时，首先一定会换行，换行后，每一行的右端都可能会有剩余空间（最后一行包含的子项可能比前几行少，所以剩余空间可能会更大），这时 &lt;code&gt;flex-grow&lt;/code&gt; 会起作用，若当前行所有子项的 &lt;code&gt;flex-grow&lt;/code&gt; 都为 &lt;code&gt;0&lt;/code&gt;，则剩余空间保留，若当前行存在一个子项的 &lt;code&gt;flex-grow&lt;/code&gt; 不为 &lt;code&gt;0&lt;/code&gt;，则剩余空间会被 &lt;code&gt;flex-grow&lt;/code&gt; 不为0的子项占据&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;flex-wrap&lt;/code&gt; 为 &lt;code&gt;nowrap&lt;/code&gt;，且子项宽度和不及父容器宽度时，&lt;code&gt;flex-grow&lt;/code&gt; 会起作用，子项会根据 &lt;code&gt;flex-grow&lt;/code&gt; 设定的值放大（为&lt;code&gt;0&lt;/code&gt;的项不放大）&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;flex-wrap&lt;/code&gt; 为 &lt;code&gt;nowrap&lt;/code&gt;，且子项宽度和超过父容器宽度时，&lt;code&gt;flex-shrink&lt;/code&gt; 会起作用，子项会根据 &lt;code&gt;flex-shrink&lt;/code&gt; 设定的值进行缩小（为&lt;code&gt;0&lt;/code&gt;的项不缩小）。但这里有一个较为特殊情况，就是当这一行所有子项 &lt;code&gt;flex-shrink&lt;/code&gt; 都为0时，也就是说所有的子项都不能缩小，就会出现讨厌的横向滚动条&lt;/li&gt;
&lt;li&gt;总结上面四点，可以看出不管在什么情况下，在同一时间，&lt;code&gt;flex-shrink&lt;/code&gt; 和 &lt;code&gt;flex-grow&lt;/code&gt; 只有一个能起作用，这其中的道理细想起来也很浅显：空间足够时，&lt;code&gt;flex-grow&lt;/code&gt; 就有发挥的余地，而空间不足时，&lt;code&gt;flex-shrink&lt;/code&gt; 就能起作用。当然，&lt;code&gt;flex-wrap&lt;/code&gt; 的值为 &lt;code&gt;wrap | wrap-reverse&lt;/code&gt; 时，表明可以换行，既然可以换行，一般情况下空间就总是足够的，&lt;code&gt;flex-shrink&lt;/code&gt; 当然就不会起作用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打开开发者工具可以查看简写属性对应的完整属性。&lt;/p&gt;
&lt;h2&gt;align-self&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;align-self&lt;/code&gt; 属性允许单个项目有与其他项目不一样的对齐方式，可覆盖 &lt;code&gt;align-items&lt;/code&gt; 属性。默认值为 &lt;code&gt;auto&lt;/code&gt;，表示继承父元素的 &lt;code&gt;align-items&lt;/code&gt; 属性，如果没有父元素，则等同于 &lt;code&gt;stretch&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/flex13.C5yZLAR-_Z1SpbML.webp&quot; alt=&quot;align-self&quot; title=&quot;align-self&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  align-self: auto | flex-start | flex-end | center | baseline | stretch;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实战&lt;/h2&gt;
&lt;h2&gt;flex 骰子&lt;/h2&gt;
&lt;h2&gt;基本网格布局&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;flex: auto&lt;/code&gt; 我们可以轻松实现基本网格布局。&lt;/p&gt;
&lt;h2&gt;自定义宽度布局&lt;/h2&gt;
&lt;h2&gt;圣杯布局&lt;/h2&gt;
&lt;h2&gt;input 样式&lt;/h2&gt;
&lt;h2&gt;聊天室布局&lt;/h2&gt;
&lt;h2&gt;一些问题&lt;/h2&gt;
&lt;p&gt;更新一些在使用 &lt;code&gt;flex&lt;/code&gt; 布局中的问题。&lt;/p&gt;
&lt;h2&gt;一侧固定另一侧自适应的布局&lt;/h2&gt;
&lt;p&gt;在使用 &lt;code&gt;element ui&lt;/code&gt; 的时候发现有时候 &lt;code&gt;flex&lt;/code&gt; 的自适应不生效，主要是在侧边栏固定宽度，然后另一边用 &lt;code&gt;flex: 1&lt;/code&gt; 来进行自适应的情况，无论 &lt;code&gt;flex-direction&lt;/code&gt; 是 &lt;code&gt;column&lt;/code&gt; 还是 &lt;code&gt;row&lt;/code&gt; 都会遇到，最后发现是文字溢出的问题。如果没有对文字溢出进行处理，当文字过长的时候，似乎 &lt;code&gt;flex&lt;/code&gt; 的自适应就失效了，加上对长文字的处理以后自适应就没有问题了。&lt;/p&gt;
&lt;p&gt;同样的问题还出现在 &lt;code&gt;element ui&lt;/code&gt; 的 &lt;code&gt;table&lt;/code&gt; 上，同样是侧边宽度固定，另一侧进行自适应，但是如果自适应的内部有 &lt;code&gt;table&lt;/code&gt; 组件则发现改变页面的大小的时候，自适应不生效，刷新页面则会变成正常大小。最后在 &lt;code&gt;segmentfault&lt;/code&gt; 上找到一个解答，对自适应的那个容器加上 &lt;code&gt;overflow:hidden&lt;/code&gt; 可以解决，不过不知道是什么原因导致的。&lt;/p&gt;
&lt;h2&gt;flex-shrink 不生效&lt;/h2&gt;
&lt;p&gt;有时候我们会发现我们设置了 &lt;code&gt;shrink&lt;/code&gt; 的元素并没有如我们想象的进行缩小，很大的可能是内部有一个确定高度的元素将它撑开了，我们的 &lt;code&gt;flex-shrink&lt;/code&gt; 只能控制当前的元素，而无法缩小内部元素。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;flex&lt;/code&gt; 布局其实在我看来主要解决了我们原来的四种布局方式不灵活的地方，有时我们需要一些固定宽度的元素，但想保持元素间的间隔是自适应的，实现器来可能就比较麻烦，有了flex布局我们能够更灵活的安排空白空间，不仅能控制我们的元素，同时能控制包裹在元素周围的空白空间，flex布局的文档目前已经处在 &lt;code&gt;Candidate Recommendation&lt;/code&gt; 候选建议书阶段了，应该说大部分功能都已经确定了，想要更好的掌握这种布局还有许多细节需要把握，想看文档的点击&lt;a href=&quot;https://www.w3.org/TR/2017/CR-css-flexbox-1-20171019/&quot; title=&quot;这里&quot;&gt;这里&lt;/a&gt;，有一个学习flex布局的游戏&lt;a href=&quot;http://flexboxfroggy.com/#zh-cn&quot; title=&quot;FLEXBOX FROGGY&quot;&gt;FLEXBOX FROGGY&lt;/a&gt;可以尝试一下。&lt;/p&gt;
&lt;p&gt;参考文章： - &lt;a href=&quot;https://css-tricks.com/snippets/css/a-guide-to-flexbox/&quot; title=&quot;A Complete Guide to Flexbox&quot;&gt;A Complete Guide to Flexbox&lt;/a&gt; - &lt;a href=&quot;https://www.jianshu.com/p/17b1b445ecd4&quot; title=&quot;[翻译]Flex Basis与Width的区别&quot;&gt;[翻译]Flex Basis与Width的区别&lt;/a&gt; - &lt;a href=&quot;https://segmentfault.com/q/1010000004080910&quot; title=&quot;flex设置成1和auto有什么区别&quot;&gt;flex设置成1和auto有什么区别&lt;/a&gt; - &lt;a href=&quot;https://zhuanlan.zhihu.com/p/25303493&quot; title=&quot;30 分钟学会 flex 布局&quot;&gt;30 分钟学会 flex 布局&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/flexbox.C5jXozGn.png"/><enclosure url="/_astro/flexbox.C5jXozGn.png"/></item><item><title>JS队列可视化</title><link>https://clloz.com/blog/queue-visual</link><guid isPermaLink="true">https://clloz.com/blog/queue-visual</guid><pubDate>Mon, 01 Oct 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;今天做了一个小的 &lt;code&gt;js&lt;/code&gt; 题目，感觉还是涉及到很多知识点，这篇文章整理遇到的问题。&lt;/p&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;题目分成两个部分，第一个部分很简单，就是写一个队列的可视化，支持左进右进，左出又出，做出来的效果大概如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/queue.7DZCyyzr_Zsi65u.webp&quot; alt=&quot;queue&quot; title=&quot;queue&quot;&gt;&lt;/p&gt;
&lt;p&gt;具体代码查看&lt;a href=&quot;https://www.clloz.com/study/ife2017-js4.html&quot;&gt;第一部分&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;第二部分是让这个队列可视化，数字变为生成元素的高，限制数字在 &lt;code&gt;10-100&lt;/code&gt;，元素上限 &lt;code&gt;60&lt;/code&gt;，给队列添加一个排序功能。 题目不是很复杂，不过在实现的过程中还是遇到了一些小问题，主要是基础问题，整理总结一下。&lt;/p&gt;
&lt;h2&gt;元素的添加和移除&lt;/h2&gt;
&lt;p&gt;对队列的操作就是对元素的添加和移除，我们可以用两个函数来实现这个功能，两个函数都需要有一个方向的参数，确定我们的元素是从队列的那一端添加活删除，虽然我们已经通过元素的高度来展示我们的元素大小，但是为了更直观还是要在元素上添加一个文本节点来展示元素的大小。另外为了后面的排序功能，我们要给我们的元素按先后顺序编号，显示在元素下方，这样我们排序后就能够知道元素是第几个添加的。效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/queue1.CWvb3Fqp_1Xd6sL.webp&quot; alt=&quot;queue1&quot; title=&quot;queue1&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果我们直接用输入的值设置高度当元素差距特别小的时候可能不是很直观，所以我们可以设置一个系数来设置高度。 添加和删除元素就用 &lt;code&gt;removeChild&lt;/code&gt; 和 &lt;code&gt;appendChild&lt;/code&gt; 两个方法很容易就实现了。&lt;/p&gt;
&lt;h2&gt;元素编号的重置&lt;/h2&gt;
&lt;p&gt;在我们删除元素的时候，我们会遇到元素编号的断裂，比如这种情况：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/queue2.BoxJl9Zp_wBTix.webp&quot; alt=&quot;queue2&quot; title=&quot;queue2&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果我们此时用左侧出来删除5号元素，那么元素的编号就会变成 &lt;code&gt;1，2，3，4，6&lt;/code&gt;，当元素比较多我们多次操作，元素的编号就非常混乱了，所以我们在删除元素的时候要注意重置一下元素的编号，逻辑非常简单，我们只要找到比当前删除元素编号大的元素，把他们的编号值减去1就可以了。但是这里比较两个文本节点的 &lt;code&gt;nodeValue&lt;/code&gt; 的时候会出现一个小问题，js的数据类型是动态的，所以我们在进行关系操作符运算的时候会自动帮我们转换类型，我们也经常不关注关系操作符运算的具体细节，在这里就会出现问题，因为比较的两个值都是 &lt;code&gt;string&lt;/code&gt; 类型，他们在比较的时候不会转成数值进行比较，而是比较两个字符串对应的字符的 &lt;code&gt;ASCII&lt;/code&gt; 码值，比如我们比较 &lt;code&gt;&quot;1&quot;&lt;/code&gt; 和 &lt;code&gt;&quot;11&quot;&lt;/code&gt;, 这两个字符串的值是相等的，因为 &lt;code&gt;11&lt;/code&gt; 的第一位和 &lt;code&gt;1&lt;/code&gt; 的 &lt;code&gt;ASCII&lt;/code&gt;码值是相等，所以我们在比较的时候需要把字符串转为数值来进行比较。&lt;/p&gt;
&lt;h2&gt;元素排序&lt;/h2&gt;
&lt;p&gt;最后要做的功能就是元素排序了，我们可以使用任意的排序算法，我用冒泡和快速排序都做了一遍，做出排序的可视化也可以让我们直观地看到不同排序方法的执行过程，帮助我们理解各种排序算法。这个功能有两点要实现，一个是两个元素的交换，另一个是排序的每一步设置一个时间间隔，方便我们看清楚排序过程。&lt;/p&gt;
&lt;h2&gt;两个元素的交换&lt;/h2&gt;
&lt;p&gt;实现两个元素的交换我没有想到特别好的方法，就直接用 &lt;code&gt;cloneNode(true)&lt;/code&gt; 来替换原先的元素的取巧的方法，我不知道这个方法性能如何，或者有更好的方法，限于目前自己知道的知识没想到特别好的方法。注意的是 &lt;code&gt;cloneNode&lt;/code&gt; 的参数如果设置 &lt;code&gt;true&lt;/code&gt; 是同时克隆子元素，一般我们需要的。&lt;/p&gt;
&lt;h2&gt;设置sleep&lt;/h2&gt;
&lt;p&gt;为了方便我们更清楚的看到排序的过程，我们给每一步排序设置一个时间间隔，也就是实现一个sleep函数，用Promis和await来实现&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function sleep(ms) {
  return new Promise((resolve) =&gt; setTimeout(resolve, ms))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;排序的效果如下（冒泡排序）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/queue4.Dqa7iJhS_Z1T3c38.webp&quot; alt=&quot;queue4&quot; title=&quot;queue4&quot;&gt;&lt;/p&gt;
&lt;p&gt;具体代码查看&lt;a href=&quot;https://www.clloz.com/study/ife2017-js5.html&quot;&gt;第二部分&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;一个很小的问题，不过在实现的过程中还是遇到一些小问题，js是一个简单而又复杂的语言，一些语言的基础概念要掌握的比较仔细，理解好这门语言才能用好这个工具。&lt;/p&gt;</content:encoded><h:img src="/_astro/front-end.3vvYUANU.jpeg"/><enclosure url="/_astro/front-end.3vvYUANU.jpeg"/></item><item><title>Flexible Images</title><link>https://clloz.com/blog/responsive-box</link><guid isPermaLink="true">https://clloz.com/blog/responsive-box</guid><pubDate>Wed, 26 Sep 2018 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;[toc]&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;前几天写了一个[ErrorPages](&quot;https://www.clloz.com/programming/front-end/css/2018/09/06/error-pages/&quot; &quot;&quot;ErrorPages&quot;&quot;)，本来在 &lt;code&gt;windows&lt;/code&gt; 上试了下 &lt;code&gt;PC&lt;/code&gt; 和 &lt;code&gt;iphone&lt;/code&gt; 浏览器显示都没问题，不过今天在 &lt;code&gt;mac&lt;/code&gt; 上看的时候发现由于 &lt;code&gt;mac&lt;/code&gt; 显示器的高度要比我的台式机小很多，背景图片的显示出现了异常，于是考虑怎么做能让图片跟随浏览器大小的变化缩放，同时要保持纵横比，不然图片的显示会出现异常。其实关于响应式的图片一直都是我比较头疼的一个点，一直没有找到特别满意的解决办法，这次来好好研究一下这个知识点。&lt;/p&gt;
&lt;h2&gt;用Viewport units：vh，vw，vmax，vmin&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CSS3&lt;/code&gt; 的新单位 &lt;code&gt;vh&lt;/code&gt;，&lt;code&gt;vw&lt;/code&gt;，&lt;code&gt;vmax&lt;/code&gt;，&lt;code&gt;vmin&lt;/code&gt; 是根据视窗的宽度或者高度确定大小的相对单位，这四个新单位的兼容性参考&lt;a href=&quot;https://caniuse.com/&quot; title=&quot;caniuse.com&quot;&gt;caniuse.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/compatibility.D6-NzCj-_1RuVIo.webp&quot; alt=&quot;compatibility&quot; title=&quot;compatibility&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你对兼容性没有特别高的要求，用这几个新单位的做法是最简单的。 根据你的需要设置 &lt;code&gt;vw&lt;/code&gt; 的具体数目，只要保证块的纵横比和图片的纵横比一致即可。 具体样式参看实例中的第一张图样式。&lt;/p&gt;
&lt;h2&gt;margin，padding的百分比特性&lt;/h2&gt;
&lt;p&gt;要让一个图片随着浏览器窗口的大小缩放，并且同时保持纵横比，我们就必须让宽度和高度使用具有相同基准的相对单位，比如上面提到的 &lt;code&gt;Viewport units&lt;/code&gt; 以及类似 &lt;code&gt;em&lt;/code&gt;，&lt;code&gt;rem&lt;/code&gt;，百分比是不行的，因为视窗的宽高是任意的。这里我们利用 &lt;code&gt;margin&lt;/code&gt; 和 &lt;code&gt;padding&lt;/code&gt; 纵向的百分比是基于包含块的宽度这一特性来解决这个问题。&lt;code&gt;margin-top&lt;/code&gt; 和 &lt;code&gt;margin-bottom&lt;/code&gt; 的 &lt;code&gt;CSS&lt;/code&gt; 规范解释见下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/margin%25.png&quot; alt=&quot;margin-percentages&quot; title=&quot;margin-percentages&quot;&gt;&lt;/p&gt;
&lt;p&gt;至于为什么 &lt;code&gt;margin&lt;/code&gt; 和 &lt;code&gt;padding&lt;/code&gt; 的纵向百分比是基于宽度的，我们看看贺老师的&lt;a href=&quot;https://www.zhihu.com/question/20983035&quot; title=&quot;解答&quot;&gt;解答&lt;/a&gt;：如果横向与纵向的百分比的相对方式不同，那用百分比就无法得到垂直和水平一致的留白。正因为 &lt;code&gt;margin&lt;/code&gt;，&lt;code&gt;padding&lt;/code&gt; 的这一特性我们可以实现一个宽高比固定的盒子。 具体操作是设置 &lt;code&gt;padding-top&lt;/code&gt; / &lt;code&gt;padding-bottom&lt;/code&gt; = &lt;code&gt;（图片的高度/图片的宽度）* 100%&lt;/code&gt;。 具体实现参照实例中的第二张图的样式。&lt;/p&gt;
&lt;h2&gt;img 实现&lt;/h2&gt;
&lt;p&gt;前面两种方法都是用 &lt;code&gt;background-image&lt;/code&gt; 属性来实现的，如果我们的需求场景需要用&lt;code&gt;img&lt;/code&gt; 标签实现的话就得用新的方法了。&lt;/p&gt;
&lt;h2&gt;max-width&lt;/h2&gt;
&lt;p&gt;如图3的例子，为 &lt;code&gt;img&lt;/code&gt; 添加 &lt;code&gt;max-width&lt;/code&gt; 属性，在现代浏览器中 &lt;code&gt;max-width&lt;/code&gt; 可以自动调整图像的比例，可以根据容器的大小缩放或者放大图像，并且图像的宽高比保持不变。 另外一点，在一些浏览器中仅指定图片的宽度，可能会导致浏览器重新处理布局，调整页面的时间周期会增加两到三倍，虽然周期不到一毫秒，但是累积起来，尤其是页面上有很多个这样的元素的时候，还是或多或少会影响页面的性能。为了解决这个问题，可以显式的指定图片的 &lt;code&gt;height&lt;/code&gt; 值为 &lt;code&gt;auto&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;padding&lt;/h2&gt;
&lt;p&gt;同样我们也可以利用 &lt;code&gt;padding-top&lt;/code&gt; 或者 &lt;code&gt;padding-bottom&lt;/code&gt; 来实现，如图 &lt;code&gt;4&lt;/code&gt;，我们给 &lt;code&gt;img&lt;/code&gt; 一个父级包含块，利用 &lt;code&gt;padding&lt;/code&gt; 的百分比特性让这个包含快成为一个自适应的包含块，再用&lt;code&gt;position: absolute;&lt;/code&gt;来定位 &lt;code&gt;img&lt;/code&gt; 的位置。&lt;/p&gt;
&lt;p&gt;本文的实例点击&lt;a href=&quot;https://www.clloz.com/study/flexible-image.html&quot;&gt;实现图片固定宽高比&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;padding&lt;/code&gt; 的百分比计算并不是根据当前元素的宽度计算，而是根据当前元素的父元素的宽度计算。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;实现图片的宽高比固定的自适应的主要方式其实就是固定宽度让浏览器自己计算高度，或者是利用 &lt;code&gt;padding&lt;/code&gt; 的特性自己计算出对应的高度。&lt;/p&gt;
&lt;p&gt;参考文章 &lt;a href=&quot;http://www.w3cplus.com/css/flexible-images.html&quot; title=&quot;Flexible Images&quot;&gt;Flexible Images&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>理解CSS中的元素层叠中的细节</title><link>https://clloz.com/blog/z-index</link><guid isPermaLink="true">https://clloz.com/blog/z-index</guid><pubDate>Tue, 18 Sep 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们经常用 &lt;code&gt;z-index&lt;/code&gt; 来控制文档树中元素的堆叠顺序，不过这个属性有时候并不能工作地很好，可能你明明给两个元素分别设置了该属性，但是并没有如设想的一般按 &lt;code&gt;z-index&lt;/code&gt; 的大小来堆叠，这是因为 &lt;code&gt;z-index&lt;/code&gt; 只是堆叠渲染中的一个条件，下面我们来详细地说一下渲染树是怎么确定元素的层叠顺序的。&lt;/p&gt;
&lt;h2&gt;堆叠上下文&lt;/h2&gt;
&lt;p&gt;和我们的理解可能有点不同，在 &lt;code&gt;CSS2.2&lt;/code&gt; 规范中，每一个盒子都是三维的，除了我们经常操纵的横向和纵向的位置，还有一条垂直于显示器的z轴，盒子沿着 &lt;code&gt;z&lt;/code&gt; 轴按照一定的规则前后排列，当我们的元素在视觉上是重叠在一起的，那么元素在这个 &lt;code&gt;z&lt;/code&gt; 轴上的位置就尤为重要，这也是我们经常遇到的需要调整元素在z轴上的前后关系，来达到我们预期的效果。&lt;code&gt;CSS2.2&lt;/code&gt; 规范中是在&lt;a href=&quot;https://www.w3.org/TR/2016/WD-CSS22-20160412/visuren.html#layers&quot; title=&quot;第九章&quot;&gt;第九章&lt;/a&gt;视觉格式化模型中对z-index的渲染规则进行说明的，首先要了解的一个概念就是堆叠上下文（&lt;code&gt;stacking context&lt;/code&gt;），在 &lt;code&gt;css2&lt;/code&gt; 的规范里面我们接触到了很多 &lt;code&gt;FC&lt;/code&gt;（ &lt;code&gt;format context&lt;/code&gt; ），都是很重要的渲染规则，这个堆叠上下文也不例外，元素的绘制顺序就是渲染树根据堆叠上下文确定的。堆叠上下文是能够互相嵌套的，而内部的堆叠上下文的堆叠等级是取决于他的父堆叠上下文的。从父堆叠上下文来看，内部的读堆叠上下文是原子的（ &lt;code&gt;atomic&lt;/code&gt; ），任何其他堆叠上下文的盒不会出现在该堆叠上下文中，也就是说每一个盒都属于且仅属于一个堆叠上下文。&lt;code&gt;CSS2.2&lt;/code&gt; 规范中并没有给出详细的形成层叠上下文的条件，不过&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/Guide/CSS/Understanding_z_index/The_stacking_context&quot; title=&quot;MDN&quot;&gt;MDN&lt;/a&gt;已经帮我们总结了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;文档根元素&lt;code&gt;&amp;#x3C;html&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;position&lt;/code&gt; 值为 &lt;code&gt;absolute&lt;/code&gt;（绝对定位）或 &lt;code&gt;relative&lt;/code&gt;（相对定位）且 &lt;code&gt;z-index&lt;/code&gt; 值不为 &lt;code&gt;auto&lt;/code&gt; 的元素；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;position&lt;/code&gt; 值为 &lt;code&gt;fixed&lt;/code&gt;（固定定位）或 &lt;code&gt;sticky&lt;/code&gt;（粘滞定位）的元素（沾滞定位适配所有移动设备上的浏览器，但老的桌面浏览器不支持）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;flex&lt;/code&gt; (&lt;code&gt;flexbox&lt;/code&gt;) 容器的子元素，且 &lt;code&gt;z-index&lt;/code&gt; 值不为 &lt;code&gt;auto&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;grid&lt;/code&gt; (&lt;code&gt;grid&lt;/code&gt;) 容器的子元素，且 &lt;code&gt;z-index&lt;/code&gt; 值不为 &lt;code&gt;auto&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;opacity&lt;/code&gt; 属性值小于 &lt;code&gt;1&lt;/code&gt; 的元素；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mix-blend-mode&lt;/code&gt; 属性值不为 &lt;code&gt;normal&lt;/code&gt; 的元素；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以下任意属性值不为 &lt;code&gt;none&lt;/code&gt; 的元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;transform&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;perspective&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clip-path&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mask / mask-image / mask-border&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;isolation&lt;/code&gt; 属性值为 &lt;code&gt;isolate&lt;/code&gt; 的元素；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-webkit-overflow-scrolling&lt;/code&gt; 属性值为 &lt;code&gt;touch&lt;/code&gt; 的元素；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;will-change&lt;/code&gt; 值设定了任一属性而该属性在 &lt;code&gt;non-initial&lt;/code&gt; 值时会创建层叠上下文的元素（参考这篇文章）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;contain&lt;/code&gt; 属性值为 &lt;code&gt;layout&lt;/code&gt;、&lt;code&gt;paint&lt;/code&gt; 或包含它们其中之一的合成值（比如 &lt;code&gt;contain&lt;/code&gt;: &lt;code&gt;strict、contain: content&lt;/code&gt;）的元素。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;任何元素只要符合上述任一条件及形成一个心得层叠上下文。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;虽然我们经常用 &lt;code&gt;z-index&lt;/code&gt; 来控制元素的堆叠顺序，但是还是要强调 &lt;code&gt;z-index&lt;/code&gt; 只是确定堆叠顺序中的一个条件，并不是全部，我们需要理解堆叠上下文，堆叠层级和堆叠顺序才能确定元素的渲染结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;堆叠层级&lt;/h2&gt;
&lt;p&gt;在给定堆叠上下文中的定位元素（ &lt;code&gt;position&lt;/code&gt; 不为 &lt;code&gt;static&lt;/code&gt; ）都有一个堆叠层级（ &lt;code&gt;stack level&lt;/code&gt; ）来确定他在z轴相对于该堆叠上下文中其他元素的位置，拥有更高层级的元素将更靠前。堆叠层级可以为负，同一个堆叠上下文中堆叠层级相同的盒按照文档树顺序从后向前堆叠，&lt;strong&gt;同一个堆叠上下文中堆叠层级相同的盒按照文档树顺序从后向前堆叠&lt;/strong&gt;。前面我们也说了堆叠上下文是可以嵌套的，我们的子堆叠上下文的层叠等级是受制与父堆叠上下文的，而元素的堆叠等级也只在它所在层叠上下文是有意义的。我们经常会发现把一个元素的 &lt;code&gt;z-index&lt;/code&gt;设置到很大但依然不能让他显示到某个元素的前面，那么很大的可能性是他和你所要比较的那个元素不在同一个堆叠上下文里面，也就是说，假设你的父堆叠上下文的堆叠等级是1，无论你怎么设置你当前元素的 &lt;code&gt;z-index&lt;/code&gt;，他都不可能小于他的父堆叠上下文。&lt;/p&gt;
&lt;p&gt;如果我们要比较两个元素在渲染上的先后顺序，我们就要先找到他们共有的父堆叠上下文，然后向下找到第一级的子堆叠上下文，比较他们的堆叠层级，就能够知道我们的目标元素的层叠顺序了。如下图 &lt;img src=&quot;https://clloz.com/_astro/stack-level.WVnhio_5_ZXwLt7.webp&quot; alt=&quot;stack-level&quot; title=&quot;stack-level&quot;&gt; 若我们要比较A和B的层叠顺序，那么我们首先要找到图中的公有父层叠上下文，然后找到A和B所在的子层叠上下文，比较他们的层叠等级，有两种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;二者相同，那么遵循后来居上的原则，文档树中位于后面的元素将覆盖在前面元素之上&lt;/li&gt;
&lt;li&gt;二者不同，那么我们就可以直接确定A和B的层叠顺序了，因为子层叠上下文的层叠顺序首先是取决于父层叠上下文的，内部元素无论怎么设置都不可能突破父级的层叠等级。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;堆叠顺序（stack order）&lt;/h2&gt;
&lt;p&gt;上面我们已经说了堆叠上下文是怎么形成的和堆叠上下文的一些特性以及堆叠层级的一些原则，那么一个堆叠上下文中的元素具体是按什么顺序排列的呢，&lt;code&gt;CSS2.2&lt;/code&gt; 规范给出了具体的排列顺序。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the background and borders of the element forming the stacking context. 元素的background和border生成的堆叠上下文.&lt;/li&gt;
&lt;li&gt;the child stacking contexts with negative stack levels (most negative first). 堆叠层级为负数的子级堆叠上下文（最负的优先）.&lt;/li&gt;
&lt;li&gt;the in-flow, non-inline-level, non-positioned descendants. 流内的，非行内级，非定位（non-positioned ）后代.&lt;/li&gt;
&lt;li&gt;the non-positioned floats. 非定位的浮动元素.&lt;/li&gt;
&lt;li&gt;the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks. 流内的，行内级，非定位后代，包括inline table和inline block.&lt;/li&gt;
&lt;li&gt;the child stacking contexts with stack level 0 and the positioned descendants with stack level 0. 堆叠层级为0的子级堆叠上下文，以及堆叠层级为0的定位的后.&lt;/li&gt;
&lt;li&gt;the child stacking contexts with positive stack levels (least positive first). 堆叠层级为正数的子级堆叠上下文（最小的优先）.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;看下图（来源于张鑫旭博客）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/z-index.CsWH3rkJ_12EmxM.webp&quot; alt=&quot;stack-order&quot; title=&quot;stack-order&quot;&gt; 这个绘制顺序被递归应用于每个堆叠上下文。&lt;/p&gt;
&lt;h2&gt;z-index&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;z-index&lt;/code&gt; 属性只对定位元素有意义，只有在元素所在层叠上下文才有意义，&lt;code&gt;z-index&lt;/code&gt; 属性的作用有两点:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;盒在当前堆叠上下文中的堆叠层级&lt;/li&gt;
&lt;li&gt;盒是否建立新的层叠上下文&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关于 &lt;code&gt;z-index&lt;/code&gt; 有两点要提一下，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;z-index&lt;/code&gt; 只对定位元素有意义，如果你为一个非定位元素设置了 &lt;code&gt;z-index&lt;/code&gt;，它不会生效。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;z-index: auto&lt;/code&gt;和&lt;code&gt;z-index: 0&lt;/code&gt;在渲染上都是位于第六层,但他们有个本质的区别，一个是堆叠层级为 &lt;code&gt;0&lt;/code&gt;的子堆叠上下文，一个是堆叠层级为 &lt;code&gt;0&lt;/code&gt; 的后代元素。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面这两点如果没理解好的话会出很多问题，我写了一个例子帮助大家理解，&lt;a href=&quot;https://www.clloz.com/study/stack-context.html&quot;&gt;请点击&lt;/a&gt; 在例子中，我们有&lt;code&gt;ABC&lt;/code&gt; 三个元素，其中 &lt;code&gt;BC&lt;/code&gt; 是 &lt;code&gt;A&lt;/code&gt; 的子元素&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A&lt;/code&gt; 元素的 &lt;code&gt;CSS&lt;/code&gt; 属性为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.div1 {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: 800px;
  height: 500px;
  margin: auto;
  background-color: lightblue;
  z-index: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;B&lt;/code&gt; 元素的属性为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.div2 {
  width: 200px;
  height: 350px;
  background: lightgray;
  margin-left: 300px;
  margin-top: 75px;
  z-index: -2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;C&lt;/code&gt; 元素的属性为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.div3 {
  width: 350px;
  height: 200px;
  background: lightcoral;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  margin: auto;
  z-index: -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到 &lt;code&gt;A&lt;/code&gt; 元素由于设置了 &lt;code&gt;position: absolute;&lt;/code&gt; 已经是一个非定位元素，并且 &lt;code&gt;z-index: 0;&lt;/code&gt; 已经形成了一个层叠上下文，而 &lt;code&gt;B&lt;/code&gt; 元素只是一个普通的块级元素，&lt;code&gt;C&lt;/code&gt; 元素是一个子层叠上下文，按我们的层叠顺序，&lt;code&gt;B&lt;/code&gt; 元素位于第三层，而 &lt;code&gt;C&lt;/code&gt; 元素位于第六层。&lt;/p&gt;
&lt;p&gt;我们先来看之前说的第一点： &lt;code&gt;z-index&lt;/code&gt; 只对定位元素有意义，如果你为一个非定位元素设置了 &lt;code&gt;z-index&lt;/code&gt;，它不会生效。 我们设置 &lt;code&gt;C&lt;/code&gt; 元素的 &lt;code&gt;z-index&lt;/code&gt; 为 &lt;code&gt;-1&lt;/code&gt;，这时候 &lt;code&gt;C&lt;/code&gt; 的层叠顺序从第六层跑到了第二层，因为它现在成了堆叠层级为负的子堆叠上下文，而我们再设置 &lt;code&gt;B&lt;/code&gt; 元素的 &lt;code&gt;z-index&lt;/code&gt; 为 &lt;code&gt;-2&lt;/code&gt; ，我们发现，&lt;code&gt;B&lt;/code&gt; 元素依然在 &lt;code&gt;C&lt;/code&gt; 元素的上面,并且我们打开开发者工具会发现 &lt;code&gt;z-index&lt;/code&gt; 实际渲染值是 &lt;code&gt;auto&lt;/code&gt;，我们可以发现这个 &lt;code&gt;z-index&lt;/code&gt; 的值并没有影响元素的定位。&lt;/p&gt;
&lt;p&gt;第二点： &lt;code&gt;z-index: auto&lt;/code&gt;和&lt;code&gt;z-index: 0&lt;/code&gt;在渲染上都是位于第 &lt;code&gt;六&lt;/code&gt; 层,但他们有个本质的区别，一个是堆叠层级为 &lt;code&gt;0&lt;/code&gt; 的子堆叠上下文，一个是堆叠层级为 &lt;code&gt;0&lt;/code&gt; 的后代元素。 我们页面初始化时，设置了 &lt;code&gt;A&lt;/code&gt; 元素的 &lt;code&gt;z-index&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt; ，按照堆叠上下文的形成规则，此时 &lt;code&gt;A&lt;/code&gt; 元素已经形成了一个堆叠上下文，也就是说，无论内部的元素如何改变，都不可能突破这个父元素的堆叠层级，我们也看到 &lt;code&gt;BC&lt;/code&gt; 元素都在 &lt;code&gt;A&lt;/code&gt; 元素的背景之上，此时我们点击下面的 &lt;code&gt;click&lt;/code&gt; 按钮，会发现C元素跑到了 &lt;code&gt;A&lt;/code&gt; 元素的后面去了，这里就是我们理解第二点的关键。当我们改变 &lt;code&gt;A&lt;/code&gt; 元素的 &lt;code&gt;z-index&lt;/code&gt; 的时候，它已经从一个堆叠上下文变为一个非堆叠上下文的普通元素，也就是说，他从堆叠顺序的第六层跑到了第三层（ &lt;code&gt;B&lt;/code&gt; 元素此时也在第三层，&lt;code&gt;CSS2.2&lt;/code&gt; 规范中已经说过，同一个堆叠上下文中的元素如果堆叠层级相同，文档流中后面的元素要更靠前），而我们的 &lt;code&gt;C&lt;/code&gt; 元素此时是一个堆叠层级为负的子堆叠上下文，自然跑到了第二层，所以它出现在了 &lt;code&gt;A&lt;/code&gt; 元素的后面。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;要很好地掌握这部分内容我觉得只要抓住三点就可以了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解什么情况下元素会创建堆叠上下文&lt;/li&gt;
&lt;li&gt;堆叠上下文中的堆叠顺序细节&lt;/li&gt;
&lt;li&gt;嵌套的堆叠上下文的理解&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;掌握这三点我相信遇到元素堆叠的问题都能轻松地解决了。&lt;/p&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>WordPress 添加 Google Analytics &amp; Google XML Sitemaps Plugin</title><link>https://clloz.com/blog/google-analyticssitemap</link><guid isPermaLink="true">https://clloz.com/blog/google-analyticssitemap</guid><pubDate>Mon, 10 Sep 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;今天试了下添加 &lt;code&gt;Google Analytics&lt;/code&gt; 来统计网站的一些信息，&lt;code&gt;Google Analytics&lt;/code&gt; 提供了非常丰富的数据统计，让你能够对自己的网站能够有非常全面的了解。然后试了一下 &lt;code&gt;sitemap&lt;/code&gt;，用了 &lt;code&gt;Google XML Sitemaps&lt;/code&gt; 插件，使用非常方便，来进一步提高自己的 &lt;code&gt;seo&lt;/code&gt; 效率。这两样东西使用都非常简单，在此简单记录一下。&lt;/p&gt;
&lt;h2&gt;Google Analytics 的使用&lt;/h2&gt;
&lt;p&gt;如果你没有谷歌账号的话需要先注册一个账号，如果你没有靠谱的科学上网工具可以看我的这篇文章：&lt;a href=&quot;https://www.clloz.com/programming/assorted/2018/09/07/shadowsocks/&quot; title=&quot;科学上网&quot;&gt;科学上网&lt;/a&gt;，然后登录&lt;a href=&quot;https://analytics.google.com/analytics/web/&quot; title=&quot;Google Analytics&quot;&gt;Google Analytics&lt;/a&gt;创建账号&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/google-analytics.DWLRDj39_Z19QJLr.webp&quot; alt=&quot;analytics&quot; title=&quot;analytics&quot;&gt;&lt;/p&gt;
&lt;p&gt;创建完成后，在&lt;code&gt;管理 --&gt; 媒体资源 --&gt; 跟踪信息 --&gt; 跟踪代码&lt;/code&gt;中可以看到全局网站代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/google-analytics2.BjB9_8_s_1RgbTU.webp&quot; alt=&quot;analytics&quot; title=&quot;analytics&quot;&gt;&lt;/p&gt;
&lt;p&gt;将页面中对应 &lt;code&gt;js&lt;/code&gt; 代码复制后放到你 &lt;code&gt;wordpress&lt;/code&gt; 当前所用主题的文件夹下 &lt;code&gt;footer.php&lt;/code&gt; 文件中的&lt;code&gt;&amp;#x3C;/body&gt;&lt;/code&gt;之前，具体路径为&lt;code&gt;/var/www/html/wp-content/themes/theme-name/footer.php&lt;/code&gt;，完成这些以后你就可以在 &lt;code&gt;Google Analytics&lt;/code&gt; 的首页看到自己的网站统计信息了，比如当前多少人在访问，访客们来自哪里，用的什么设备，什么语言，访客们是通过 &lt;code&gt;facebook&lt;/code&gt; 推荐还是搜索引擎来到你的网站的，你最受欢迎的页面是哪个等等。&lt;/p&gt;
&lt;h2&gt;Google XML Sitemaps 插件&lt;/h2&gt;
&lt;p&gt;作为一个安装两百万的插件，应该是非常靠谱的，&lt;code&gt;sitemap&lt;/code&gt; 能让搜索引擎了解你的网站结构，从而更好地分析你的网站，对 &lt;code&gt;seo&lt;/code&gt; 是有益的。这个插件的使用也是十分简单的，只要安装启用就可以了。这款插件的设置十分丰富，不过我感觉就默认就可以了，安装完插件后你可以访问&lt;code&gt;http://www.domian.com/sitemap.xml&lt;/code&gt;来查看插件生成的 &lt;code&gt;sub-sitemap&lt;/code&gt; 了。 设置直接默认就可以了，有两点设置单独说一下 1. 关于 &lt;code&gt;robots.txt&lt;/code&gt;，如果你自己编写了一个 &lt;code&gt;robots.txt&lt;/code&gt; 文件，请将这个选项去掉，如果没有，你可以保持默认，&lt;code&gt;Google XML Sitemaps&lt;/code&gt; 插件会为你自动生成一个虚拟 &lt;code&gt;robots.txt&lt;/code&gt; 文件。 2. 如果你的网站中有一些内容是自己额外增加的，比如在根目录下面自己添加了一些 &lt;code&gt;html&lt;/code&gt; 文件，可以通过插件的“附加页面”功能，将这些页面加入 &lt;code&gt;sitemap&lt;/code&gt;，以便搜索引擎抓取。&lt;/p&gt;
&lt;h2&gt;提交 sitemap.xml 到 Google 和百度&lt;/h2&gt;
&lt;h2&gt;提交到 Google Search Console&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;Search Console&lt;/code&gt; 主页添加自己的网站，然后在菜单中的 &lt;code&gt;sitemaps&lt;/code&gt; 栏中提交自己的 &lt;code&gt;sitemaps url&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;提交到百度搜索资源平台&lt;/h2&gt;
&lt;p&gt;在百度&lt;code&gt;搜索资源平台 --&gt; 链接提交 --&gt; 自动提交&lt;/code&gt;中选择 &lt;code&gt;sitemap&lt;/code&gt; 然后填写自己的 &lt;code&gt;sitemap url&lt;/code&gt; 即可。 &lt;img src=&quot;https://clloz.com/_astro/baidu.CYhJ6AF6_27CEML.webp&quot; alt=&quot;sitemap&quot; title=&quot;sitemap&quot;&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;博客上线一个多星期了，&lt;code&gt;sitemap&lt;/code&gt; 搞定后，网站的 &lt;code&gt;seo&lt;/code&gt; 基本都搞得差不多了，不过目前在百度首页居然是 &lt;code&gt;cdn&lt;/code&gt; 的域名，有点不解，&lt;code&gt;google&lt;/code&gt; 上就很正常，百度反而花的精力多，什么熊掌号，搜索资源平台都搞了，希望过段时间能好吧。それじゃ。&lt;/p&gt;</content:encoded><h:img src="/_astro/Google-Analytics.DLw4H4Qr.jpg"/><enclosure url="/_astro/Google-Analytics.DLw4H4Qr.jpg"/></item><item><title>FontSquirrel</title><link>https://clloz.com/blog/fontsquirrel</link><guid isPermaLink="true">https://clloz.com/blog/fontsquirrel</guid><pubDate>Wed, 05 Sep 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;今天在看别人的博客的时候，看到了一款很有趣的字体，&lt;a href=&quot;https://www.fontsquirrel.com/fonts/list/find_fonts?q%5Bterm%5D=sail&amp;#x26;q%5Bsearch_check%5D=Y&quot; title=&quot;Sail&quot;&gt;Sail&lt;/a&gt;（点击查看），字体的样子大概就是文章标题这样，我觉得还蛮有趣的，就去 &lt;code&gt;google&lt;/code&gt; 上搜索了一下，不仅找到了这款字体，还找到了一个有用的网站&lt;a href=&quot;https://www.fontsquirrel.com/&quot; title=&quot;FontSquirrel&quot;&gt;FontSquirrel&lt;/a&gt;，这个网站上都是免费字体，我看了一下，很多字体都很棒，同时能进行字体的格式转换，这篇文章就跟大家分享一下。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;网站的字体都是英文字体，不支持中文，所以如果你的网站英文比较多，可以去找一款自己喜欢的字体。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;转换字体格式&lt;/h2&gt;
&lt;p&gt;在网站的导航栏选择 &lt;code&gt;Generator&lt;/code&gt;，点击 &lt;code&gt;Upload&lt;/code&gt; 上传自己的字体文件，比如我下载 &lt;code&gt;sail&lt;/code&gt; 字体就是只有一个 &lt;code&gt;otf&lt;/code&gt; 文件，上传完成之后可以点击&lt;code&gt;download your kit&lt;/code&gt;来下载网站生成的字体包，这个字体包中不仅包含了字体文件，而且有 &lt;code&gt;html&lt;/code&gt; 和 &lt;code&gt;css&lt;/code&gt; 文件可以让你预览字体&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/fontsquirrel2.D3hQyUGP_1a8F6n.webp&quot; alt=&quot;sail&quot; title=&quot;sail&quot;&gt;&lt;/p&gt;
&lt;p&gt;打开下载的文件夹中的 &lt;code&gt;Html&lt;/code&gt; 可以看到你所要使用的字体的预览，如果你要在 &lt;code&gt;css&lt;/code&gt; 中使用字体，只要将字体复制到对应的位置，将 &lt;code&gt;stylesheet.css&lt;/code&gt; 中的&lt;code&gt;@font-face&lt;/code&gt;复制到对应的 &lt;code&gt;css&lt;/code&gt; 文件，记得修改字体文件的路径，我是将字体文件放在阿里云上了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/fontsquirrel4.RiKlqy5r_Z2s3Pli.webp&quot; alt=&quot;stylesheet&quot; title=&quot;stylesheet&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在你已经可以在自己的网站上使用自己喜欢的字体了。&lt;/p&gt;</content:encoded><h:img src="/_astro/fontsquirrel1.Cg03LFNx.png"/><enclosure url="/_astro/fontsquirrel1.Cg03LFNx.png"/></item><item><title>CentOS安装emacs</title><link>https://clloz.com/blog/centos-emacs</link><guid isPermaLink="true">https://clloz.com/blog/centos-emacs</guid><pubDate>Sun, 02 Sep 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;因为自己用 &lt;code&gt;emacs&lt;/code&gt; 已经习惯了，虽然 &lt;code&gt;vim&lt;/code&gt; 用起来也不错而且比 &lt;code&gt;emacs&lt;/code&gt; 方便，但是还是想在服务器上安装一下 &lt;code&gt;emacs&lt;/code&gt; 方便要在上面修改代码的时候使用，本来以为自己也算个老司机了，在 &lt;code&gt;windows&lt;/code&gt; 和 &lt;code&gt;mac&lt;/code&gt; 上都已经受过 &lt;code&gt;emacs&lt;/code&gt; 的洗礼，没想到在 &lt;code&gt;CentOS&lt;/code&gt; 里面还是遇到了很多问题，在这里总结一下，方便下次查看。&lt;/p&gt;
&lt;h2&gt;安装步骤&lt;/h2&gt;
&lt;p&gt;可以参考官方的安装说明：&lt;a href=&quot;https://www.gnu.org/software/emacs/manual/html_node/efaq/Installing-Emacs.html&quot; title=&quot;How do I install Emacs?&quot;&gt;How do I install Emacs?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Linux&lt;/code&gt; 上的大部分软件安装都是 &lt;code&gt;wget&lt;/code&gt; ，&lt;code&gt;tar&lt;/code&gt;，&lt;code&gt;./configure&lt;/code&gt;，&lt;code&gt;make&lt;/code&gt;，&lt;code&gt;make check&lt;/code&gt;，&lt;code&gt;make install&lt;/code&gt;，配置环境变量，按部就班来就行了。&lt;/p&gt;
&lt;h2&gt;依赖关系&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;./configure&lt;/code&gt; 的时候，第一个出现的问题是提示没有 &lt;code&gt;gnutls&lt;/code&gt;，去 &lt;code&gt;google&lt;/code&gt; 一下，&lt;code&gt;gnutls&lt;/code&gt; 还需要另外三个依赖，&lt;code&gt;nettle&lt;/code&gt;，&lt;code&gt;libasn1&lt;/code&gt;，&lt;code&gt;gmp&lt;/code&gt;，详见&lt;a href=&quot;http://amon.org/gnutls-3-5-11&quot; title=&quot;gnutls安装&quot;&gt;gnutls安装&lt;/a&gt;，因为我对 &lt;code&gt;linux&lt;/code&gt; 环境变量的配置不太熟悉，这个地方卡了好久，终于装好之后，又来了一边&lt;code&gt;./configure&lt;/code&gt; ,还是缺少各种各样的依赖，我放上在网上找的需要安装的依赖&lt;code&gt;sudo yum -y install libXpm-devel libjpeg-turbo-devel openjpeg-devel openjpeg2-devel turbojpeg-devel giflib-devel libtiff-devel gnutls-devel libxml2-devel GConf2-devel dbus-devel wxGTK-devel gtk3-devel libselinux-devel gpm-devel librsvg2-devel ImageMagick-devel&lt;/code&gt;，安装好这些以后，&lt;code&gt;emacs&lt;/code&gt; 终于安装上了，因为没有图形界面，不需要像在 &lt;code&gt;mac&lt;/code&gt; 终端一样用 &lt;code&gt;emacs -nw&lt;/code&gt; 启动了，直接 &lt;code&gt;emacs&lt;/code&gt; 启动就可以了。&lt;/p&gt;
&lt;h2&gt;alt 键无效&lt;/h2&gt;
&lt;p&gt;熟悉 &lt;code&gt;emacs&lt;/code&gt; 的朋友都知道，&lt;code&gt;emacs&lt;/code&gt; 的 &lt;code&gt;ctrl&lt;/code&gt; 和 &lt;code&gt;alt&lt;/code&gt; 都非常重要，是用来输入指令的，但是我安装完以后发现 &lt;code&gt;alt&lt;/code&gt; 键无法使用，经过一番搜索，终于找到了解决方法，我用的 &lt;code&gt;xshell&lt;/code&gt; 连接的阿里云，在 &lt;code&gt;xshell&lt;/code&gt; 的属性里面有个键盘的选项，把 &lt;code&gt;将左alt键用作meta键&lt;/code&gt; 选项勾上以后就可以使用 &lt;code&gt;M-x&lt;/code&gt; 指令了。其实在 &lt;code&gt;emacs&lt;/code&gt; 文档里面有写，&lt;code&gt;M-x&lt;/code&gt;, &lt;code&gt;it means &quot;press Alt/Esc/Option/Edit key and x together&quot;&lt;/code&gt;.在 &lt;code&gt;Linux&lt;/code&gt; 下用 &lt;code&gt;esc+x&lt;/code&gt; 是可以调用 &lt;code&gt;M-x&lt;/code&gt; 的，不过我觉得 &lt;code&gt;esc+x&lt;/code&gt; 还是没 &lt;code&gt;alt&lt;/code&gt; 方便，建议大家还是设置一下 &lt;code&gt;alt&lt;/code&gt; 键吧。&lt;/p&gt;
&lt;h2&gt;退格键变为 C-h&lt;/h2&gt;
&lt;p&gt;我在调试 &lt;code&gt;spacemacs&lt;/code&gt; 的时候，发现在 &lt;code&gt;emacs&lt;/code&gt; 里只要按退格键 &lt;code&gt;backspace&lt;/code&gt; ，就会自动调用 &lt;code&gt;C-h&lt;/code&gt; 帮助指令，百思不得其解，&lt;code&gt;google&lt;/code&gt; 以后，在 &lt;code&gt;emacs&lt;/code&gt; 的文档里找到这个问题的答案，&lt;a href=&quot;https://www.gnu.org/software/emacs/manual/html_node/emacs/DEL-Does-Not-Delete.html&quot; title=&quot;DEL-Does-Not-Delete - Emacs Manual&quot;&gt;DEL-Does-Not-Delete - Emacs Manual&lt;/a&gt;，我在 &lt;code&gt;init.el&lt;/code&gt; 里加上了 &lt;code&gt;(normal-erase-is-backspace-mode 1)&lt;/code&gt;以后，退格键就可以使用了，至此，&lt;code&gt;emacs&lt;/code&gt; 已经算基本可以使用了，如果是在服务器上使用我觉得这样就可以了，因为毕竟在服务器上使用的时候比较少，一般也是小改改代码什么的，没必要再花精力配置 &lt;code&gt;emacs&lt;/code&gt; 了。不过我还是作死搞了一下 &lt;code&gt;spacemacs&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;设置环境变量&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Linux&lt;/code&gt; 的环境变量有很多修改方式，我是将 &lt;code&gt;bin&lt;/code&gt; 文件夹的位置放到了 &lt;code&gt;/etc/profile&lt;/code&gt;中，然后 &lt;code&gt;source&lt;/code&gt; 一下，这样就永久加入环境变量中，重启后依然能用 &lt;code&gt;emacs&lt;/code&gt; 指令启动 &lt;code&gt;emacs&lt;/code&gt; 。 注意 &lt;code&gt;bash&lt;/code&gt; 中 &lt;code&gt;ctrl+s&lt;/code&gt; 是锁屏，用 &lt;code&gt;ctrl+q&lt;/code&gt; 解开。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;安装的详细过程参考&lt;a href=&quot;https://blog.csdn.net/benzun_yinzi/article/details/80830410&quot; title=&quot;这篇文章&quot;&gt;这篇文章&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="/_astro/emacs-logo.CVWUvzc8.png"/><enclosure url="/_astro/emacs-logo.CVWUvzc8.png"/></item><item><title>块元素居中和扇形</title><link>https://clloz.com/blog/blocksector</link><guid isPermaLink="true">https://clloz.com/blog/blocksector</guid><pubDate>Wed, 29 Aug 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;块元素居中的方法&lt;/h2&gt;
&lt;h2&gt;绝对定位居中&lt;/h2&gt;
&lt;p&gt;关于绝对定位，绝对定位的 &lt;code&gt;top,right,bottom,left&lt;/code&gt; 属性相当于给该元素的包含元素( &lt;code&gt;position&lt;/code&gt; 为 &lt;code&gt;relative&lt;/code&gt; 的父元素或者 &lt;code&gt;body&lt;/code&gt; )限定一个新的该元素的活动边界(见下面原文)。绝对定位的元素脱离文档流。如不指定定位的属性，则该元素的起点为该元素在文档中的对应位置。&lt;code&gt;absolute&lt;/code&gt; 和 &lt;code&gt;float&lt;/code&gt; 一样脱离文档流，所以相对于文档流他们都没有高度，但是 &lt;code&gt;absolute&lt;/code&gt; 与 &lt;code&gt;float&lt;/code&gt; 不同的地方是相对文档流来说他也没有宽度，所以它能够产生覆盖效果，而 &lt;code&gt;float&lt;/code&gt; 不可以。具体可以参考这篇文章 &lt;a href=&quot;http://www.zhangxinxu.com/wordpress/2010/01/absolute%E7%BB%9D%E5%AF%B9%E5%AE%9A%E4%BD%8D%E7%9A%84%E9%9D%9E%E7%BB%9D%E5%AF%B9%E5%AE%9A%E4%BD%8D%E7%94%A8%E6%B3%95/&quot;&gt;absolute绝对定位的非绝对定位用法&lt;/a&gt;，&lt;a href=&quot;http://www.zhangxinxu.com/wordpress/2010/12/css-%E7%9B%B8%E5%AF%B9%E7%BB%9D%E5%AF%B9%E5%AE%9A%E4%BD%8D%E7%B3%BB%E5%88%97%EF%BC%88%E4%B8%80%EF%BC%89/&quot;&gt;CSS 相对|绝对(relative/absolute)定位系列（一）&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For absolutely positioned elements, the top, right, bottom, and left properties specify offsets from the edge of the element&apos;s containing block (what the element is positioned relative to)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;设置元素的宽高可以是 &lt;code&gt;pixel&lt;/code&gt; 也可以是百分比，设置百分比的话有自适应效果，然后对定位属性都设置为 &lt;code&gt;0&lt;/code&gt;，&lt;code&gt;margin auto&lt;/code&gt; 就可以看到对应的元素已经实现居中定位，如果想要该元素有偏移量，就根据自己的需要修改定位属性，该元素的居中是相当于定位属性给出的边界框的居中。&lt;/p&gt;
&lt;p&gt;该方法的优点: 1. 兼容 &lt;code&gt;ie8-ie10&lt;/code&gt; 2. 一个标签，代码量少 3. 支持百分比和 &lt;code&gt;max|min&lt;/code&gt; 属性 4. 内容可以重绘 ? 5. 对于图片居中有效&lt;/p&gt;
&lt;p&gt;缺点: 1. 必须设置元素的高度宽度(图片例外)，否则元素将填满包含框&lt;/p&gt;
&lt;p&gt;还有一些注意点，可以给元素添加 &lt;code&gt;resize:auto&lt;/code&gt; 让用户可改变大小.( &lt;code&gt;overflow:auto&lt;/code&gt; )&lt;/p&gt;
&lt;p&gt;对于内容高度大于元素或容器高度的情况，建议使用 &lt;code&gt;overflow:auto&lt;/code&gt;，否则会出现溢出的情况.也可以使用 &lt;code&gt;display：table&lt;/code&gt;，不管文本多长元素会自动适应长度，不会溢出或者出现滚动条。&lt;/p&gt;
&lt;p&gt;图片可以使用 &lt;code&gt;hight：auto&lt;/code&gt; 其他元素不可以&lt;/p&gt;
&lt;h2&gt;负边距居中( Negative margins )&lt;/h2&gt;
&lt;p&gt;负边距居中比较简单,绝对定位后设置 &lt;code&gt;top left&lt;/code&gt; 均为 &lt;code&gt;50%&lt;/code&gt;，然后 &lt;code&gt;margin-left&lt;/code&gt; 为负的宽度的一半， &lt;code&gt;margin-top&lt;/code&gt; 为负的高度的一半。&lt;/p&gt;
&lt;p&gt;这个方法理解起来很简单，代码量也比较少，并且兼容 &lt;code&gt;ie6，ie7&lt;/code&gt;，但是不能自适应，内容也会溢出，要根据情况设置 &lt;code&gt;overflow&lt;/code&gt;，计算 &lt;code&gt;margin&lt;/code&gt; 要根据自己的 &lt;code&gt;padding&lt;/code&gt; 和 &lt;code&gt;box-sizing&lt;/code&gt; 来计算。&lt;/p&gt;
&lt;h2&gt;transform&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;transform&lt;/code&gt; 居中和负边距居中原理一致，不过他不是用 &lt;code&gt;margin&lt;/code&gt; 而是用 &lt;code&gt;translate: (-50%, -50%)&lt;/code&gt;,这样的好处是能够支持可变高度了，自适应比较好，但是ie8不支持 &lt;code&gt;translate&lt;/code&gt;，还有该属性要加上浏览器厂商的前缀。&lt;/p&gt;
&lt;h2&gt;表格单元格&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;Center-Container is-Table&quot;&gt;
  &amp;#x3C;div class=&quot;Table-Cell&quot;&gt;
    &amp;#x3C;div class=&quot;Center-Block&quot;&gt;
      &amp;#x3C;!-- CONTENT --&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&amp;#x3C;style&gt;
  .Center-Container.is-Table {
    display: table;
  }
  .is-Table .Table-Cell {
    display: table-cell;
    vertical-align: middle;
  }
  .is-Table .Center-Block {
    width: 50%;
    margin: 0 auto;
  }
&amp;#x3C;/style&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总的说来这可能是最好的居中实现方法，因为内容块高度会随着实际内容的高度变化，浏览器对此的兼容性也好。最大的缺点是需要大量额外的标记，需要三层元素让最内层的元素居中。&lt;/p&gt;
&lt;p&gt;优点： 1. 高度可变 2. 内容溢出会将父元素撑开。 3. 跨浏览器兼容性好。&lt;/p&gt;
&lt;h2&gt;inline-block 详解&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;vertical-aligin&lt;/code&gt; 是对于 &lt;code&gt;inline&lt;/code&gt; 元素所在的 &lt;code&gt;line box&lt;/code&gt; 来说的，父元素的 &lt;code&gt;line box&lt;/code&gt; 的高度取决于该 &lt;code&gt;line box&lt;/code&gt; 中的 &lt;code&gt;inline box&lt;/code&gt; 中最高的一个(在不设定 &lt;code&gt;line-height&lt;/code&gt; 的情况下)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于 &lt;code&gt;inline-block&lt;/code&gt; 的居中定位方法着实花了一点时间,现在总结一下。 对于我们所看到的 &lt;code&gt;html&lt;/code&gt; 的文档流，除开独占一行的块级元素，任何行内元素构成的行，都有一个 &lt;code&gt;line box&lt;/code&gt;，&lt;code&gt;line box&lt;/code&gt; 由内部的一个或者多个 &lt;code&gt;inline box&lt;/code&gt; 组成，他的高度可以直接由 &lt;code&gt;line-height&lt;/code&gt; 限定，如果不限定 &lt;code&gt;line-height&lt;/code&gt; 那么就由内部最高的 &lt;code&gt;inline box&lt;/code&gt; 决定。&lt;code&gt;inline box&lt;/code&gt; 的高度由 &lt;code&gt;line-height&lt;/code&gt; 决定。&lt;/p&gt;
&lt;h3&gt;inline box、line box、line-height、vertical-align 的关联&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对于纯文字的 &lt;code&gt;inline box&lt;/code&gt; 来说，外面会包裹一层 &lt;code&gt;content area&lt;/code&gt;，他的高度和 &lt;code&gt;font-size&lt;/code&gt; 以及 &lt;code&gt;font-family&lt;/code&gt; 有关，与 &lt;code&gt;line-height&lt;/code&gt; 无关，比如一个 &lt;code&gt;span&lt;/code&gt; 标签，设置他的 &lt;code&gt;fonts-size 14px&lt;/code&gt; 那么他的 &lt;code&gt;content area&lt;/code&gt; 高度是 &lt;code&gt;19px&lt;/code&gt; (不同的浏览器可能不同，&lt;code&gt;chrome&lt;/code&gt; 中是 &lt;code&gt;19px&lt;/code&gt; )，&lt;code&gt;css2.1&lt;/code&gt; 规范中只说明了跟字体有关，但是没有说是怎么计算的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &apos;height&apos; property does not apply. The height of the content area should be based on the font, but this specification does not specify how.A UA may, e.g., use the em-box or the maximum ascender and descender of the font.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;看下面的图 &lt;img src=&quot;https://clloz.com/_astro/inlinebox.B4KTADaG_Zjc0Ha.webp&quot; alt=&quot;行高细节&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;line box&lt;/code&gt; 的高度由 &lt;code&gt;line-height&lt;/code&gt; 决定，&lt;code&gt;line-height&lt;/code&gt; 的定义时从第一行基线到下一行基线的距离，一半的 &lt;code&gt;line-height&lt;/code&gt; 在 &lt;code&gt;content area&lt;/code&gt; 的上面，另一半在下面。&lt;code&gt;line-height&lt;/code&gt; 从 &lt;code&gt;content area&lt;/code&gt; 的水平中垂线开始计算。想得到 &lt;code&gt;inline&lt;/code&gt; 元素的 &lt;code&gt;inline box&lt;/code&gt; 高度，把 &lt;code&gt;inline&lt;/code&gt; 元素改变成 &lt;code&gt;inline-block&lt;/code&gt; (不手动设置高度)元素看看。&lt;code&gt;content area&lt;/code&gt; 中垂线和 &lt;code&gt;middle line&lt;/code&gt; 是有误差的，这也是导致有时候 &lt;code&gt;vertical-align&lt;/code&gt; 有误差的原因。&lt;/p&gt;
&lt;p&gt;如果直接用设置 &lt;code&gt;line-height&lt;/code&gt; 的方法来设置高度的话，那么这个方法就没有办法做到自适应，修改容器的高度就又不居中了，所以有一个变形的方法，就是用一个 &lt;code&gt;display&lt;/code&gt; 为 &lt;code&gt;inline-block&lt;/code&gt; 的 &lt;code&gt;after&lt;/code&gt; 或者 &lt;code&gt;before&lt;/code&gt; 伪元素撑满容器，这样就不需要设置容器的 &lt;code&gt;line-height&lt;/code&gt;，我们需要居中的元素和这个伪元素所构成的 &lt;code&gt;line box&lt;/code&gt; 高度就是容器的高度，而我们前面也说过，元素的 &lt;code&gt;vertical-align&lt;/code&gt; 属性是相对于它所在的 &lt;code&gt;line-box&lt;/code&gt; 的，现在由于伪元素的存在，&lt;code&gt;line box&lt;/code&gt; 的高度就是父元素的高度，所以能够实现居中。还有一点就是水平居中就给父元素添加 &lt;code&gt;text-align&lt;/code&gt; 属性就可以了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于没有标签包裹的文字来说，就是匿名 &lt;code&gt;inline-boxes&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div style=&quot;background: lavender;font-size: 0;&quot;&gt;
  &amp;#x3C;span style=&quot;line-height: 300px; font-size: 15px;word-break: break-all; line-height: 19px;&quot;&gt;
    test
  &amp;#x3C;/span&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码如果把 &lt;code&gt;span&lt;/code&gt; 的 &lt;code&gt;font-size&lt;/code&gt; 设置为0会发现 &lt;code&gt;div&lt;/code&gt; 还是有高度，因为 &lt;code&gt;div&lt;/code&gt; 有个默认的 &lt;code&gt;fontsize&lt;/code&gt;，各个浏览器不同。当元素内部有内联元素的时候，这个 &lt;code&gt;font-size&lt;/code&gt; 会自动生效，如果把 &lt;code&gt;span&lt;/code&gt; 注释掉或者 &lt;code&gt;display:none&lt;/code&gt; 就不会出现。&lt;/p&gt;
&lt;p&gt;综合上面几点，&lt;code&gt;font-size&lt;/code&gt; 决定了 &lt;code&gt;content area&lt;/code&gt; 的高度，&lt;code&gt;content area&lt;/code&gt; 或者 &lt;code&gt;line-height&lt;/code&gt; 来决定 &lt;code&gt;inline box&lt;/code&gt; 的高度，最高的 &lt;code&gt;inline box&lt;/code&gt; 或者父元素的 &lt;code&gt;line-height&lt;/code&gt; 决定了 &lt;code&gt;line box&lt;/code&gt; 的高度，而 &lt;code&gt;vertical-align&lt;/code&gt; 这个属性只对 &lt;code&gt;inline&lt;/code&gt; 或 &lt;code&gt;inline-block&lt;/code&gt; ( &lt;code&gt;tabel-cell&lt;/code&gt; 在这里理解为 &lt;code&gt;inline-block&lt;/code&gt; )有效，原理就是内部的 &lt;code&gt;inline box&lt;/code&gt; 对于父元素的 &lt;code&gt;line box&lt;/code&gt; 的高度垂直居中，但是要注意的是 &lt;code&gt;vertical-align&lt;/code&gt; 的 &lt;code&gt;middle&lt;/code&gt; 属性是指的上图中的中线，中线的位置和 &lt;code&gt;content area&lt;/code&gt; 的水平中垂线位置是不同，所以会产生一定的误差，可以对父元素用 &lt;code&gt;font-size 0&lt;/code&gt; 来解决这个误差。&lt;/p&gt;
&lt;h2&gt;flex 布局&lt;/h2&gt;
&lt;p&gt;flexible box弹性布局，css3新特性，具体参考阮一峰的博文&lt;a href=&quot;http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html&quot;&gt;Flex 布局教程：语法篇&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;http://www.zhangxinxu.com/wordpress/2010/05/%E6%88%91%E5%AF%B9css-vertical-align%E7%9A%84%E4%B8%80%E4%BA%9B%E7%90%86%E8%A7%A3%E4%B8%8E%E8%AE%A4%E8%AF%86%EF%BC%88%E4%B8%80%EF%BC%89/&quot;&gt;我对CSS vertical-align的一些理解与认识（一）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.zhangxinxu.com/wordpress/2015/08/css-deep-understand-vertical-align-and-line-height/&quot;&gt;CSS深入理解vertical-align和line-height的基友关系&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.zhangxinxu.com/wordpress/2015/06/about-letter-x-of-css/&quot;&gt;字母’x’在CSS世界中的角色和故事&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.zhangxinxu.com/wordpress/2010/01/css-float%E6%B5%AE%E5%8A%A8%E7%9A%84%E6%B7%B1%E5%85%A5%E7%A0%94%E7%A9%B6%E3%80%81%E8%AF%A6%E8%A7%A3%E5%8F%8A%E6%8B%93%E5%B1%95%E4%B8%80/&quot;&gt;CSS float浮动的深入研究、详解及拓展(一)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.cnblogs.com/samwu/p/3936271.html&quot;&gt;关于line box，inline box，line-height，vertical-align之间的关系&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;扇形&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在左上角和右下角绝对定位两个宽高都为50px的元素，然后设置&lt;code&gt;border-radius&lt;/code&gt;为&lt;code&gt;0 0 100% 0&lt;/code&gt;和&lt;code&gt;100% 0 0 0&lt;/code&gt;两种，当&lt;code&gt;border-radius&lt;/code&gt;超过&lt;code&gt;50%&lt;/code&gt;的时候，依然可以正常显示，但是如果两个相邻的角的&lt;code&gt;border-radius&lt;/code&gt;半径和超过两个角所在边的长度的时候，浏览器会重新计算让&lt;code&gt;border-radius&lt;/code&gt;适应当前的状况。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在左上角和右下角绝对定位两个宽高都为&lt;code&gt;100px&lt;/code&gt;的元素，&lt;code&gt;border-radius&lt;/code&gt;都为&lt;code&gt;50%&lt;/code&gt;，就形成了两个半径为50px的圆，讲圆心分别定位到左上角和右下角，设置居中的父元素的&lt;code&gt;overflow&lt;/code&gt;为&lt;code&gt;hidden&lt;/code&gt;就可以将超出的部分隐藏起来，实现需要的效果&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;应该还有很多其他的方法，目前就想到这两种比较简单的，后面如果学习到新的会来补充。任务四基本完成了。&lt;/p&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>三栏布局</title><link>https://clloz.com/blog/colum</link><guid isPermaLink="true">https://clloz.com/blog/colum</guid><pubDate>Wed, 29 Aug 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;三栏布局是应用最广泛的布局之一，一般是左右两侧固定宽度，中间自适应的方式。下面介绍几种三栏布局的解决方案和细节。&lt;/p&gt;
&lt;h2&gt;基础HTML结构和样式&lt;/h2&gt;
&lt;p&gt;不同的方法 &lt;code&gt;HTML&lt;/code&gt; 结构顺序会有不同，但是基本都是 &lt;code&gt;左中右&lt;/code&gt; 三个部分，能够提取的样式主要是背景色，字体，宽高，内边距，盒模型。样式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.wrap {
  height: 250px;
  margin-top: 30px;
  position: relative;
  font-size: 30px;
  color: white;
}
.wrap .left,
.wrap .right {
  width: 200px;
  height: 200px;
}
.wrap .left {
  background-color: pink;
}
.wrap .right {
  background-color: lightblue;
}
.wrap .middle {
  background-color: lightgreen;
  padding: 10px;
  box-sizing: border-box;
  height: 100%;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;float方案&lt;/h2&gt;
&lt;p&gt;左右侧元素 &lt;code&gt;float&lt;/code&gt;，中间元素用 &lt;code&gt;margin&lt;/code&gt; 给两侧元素预留空间。需要注意的是 &lt;code&gt;HTML&lt;/code&gt; 结构需要是 &lt;code&gt;左右中&lt;/code&gt; 的顺序，流内元素无法感知浮动元素，但是浮动元素可以感知流内元素，如果中间的流内元素先渲染好了，那么最后的浮动元素将会渲染在下一行，因为该行已经没有空间了。&lt;code&gt;HTML&lt;/code&gt; 结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;wrap eg1&quot;&gt;
  &amp;#x3C;div class=&quot;left&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;right&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;middle&quot;&gt;例一：左右元素float，中间元素margin&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* eg1 */
.eg1 .left {
  float: left;
}
.eg1 .right {
  float: right;
}
.eg1 .middle {
  margin: 0 210px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;绝对定位方案&lt;/h2&gt;
&lt;p&gt;和 &lt;code&gt;float&lt;/code&gt; 类似，最好也采取左右中的结构，如果采取其他结构，注意定位属性 &lt;code&gt;top：0&lt;/code&gt; 要加上。另外由于绝对定位元素完全脱离文档流，需要给父元素加上 &lt;code&gt;position: relative&lt;/code&gt;，并且限定高度。&lt;code&gt;HTML&lt;/code&gt; 结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;wrap eg2&quot;&gt;
  &amp;#x3C;div class=&quot;left&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;right&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;middle&quot;&gt;例二：左右元素绝对定位，中间元素margin&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* eg2 */
.eg2 .left {
  position: absolute;
  left: 0;
}
.eg2 .right {
  position: absolute;
  right: 0;
  top: 0;
}
.eg2 .middle {
  margin: 0 210px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;flex布局&lt;/h2&gt;
&lt;p&gt;flex布局是最简单也是代码量最少的，兼容性也还不错。唯一的问题就是 &lt;code&gt;CSS3&lt;/code&gt; 的新特性 &lt;code&gt;ie6-9&lt;/code&gt; 不支持。&lt;code&gt;HTML&lt;/code&gt; 结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;wrap wrap-flex eg3&quot;&gt;
  &amp;#x3C;div class=&quot;left&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;middle&quot;&gt;例三：中间元素flex-grow为1，自动放大&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;right&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* eg3 */
.wrap-flex {
  display: flex;
}
.eg3 .middle {
  flex: 1;
  margin: 0 10px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;table方案&lt;/h2&gt;
&lt;p&gt;用表格布局来实现该需求，三个部分相当于三个单元格，&lt;code&gt;display: table-cell&lt;/code&gt;。使用表格布局会使得三个部分高度统一，三个部分之间的缝隙需要用属性 &lt;code&gt;border-collapse border-spacing&lt;/code&gt; 来设置，每个单元格的左右都会预留。在浏览器窗口宽度变化的时候，适应性较好，不会出现结构改变的现象。&lt;code&gt;HTML&lt;/code&gt; 结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;wrap wrap-table eg4&quot;&gt;
  &amp;#x3C;div class=&quot;left&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;middle&quot;&gt;
    例四：table布局，三栏高度一致，元素之间的缝隙只能通过border-collapse和border-spacing属性设置
  &amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;right&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* eg4 */
.wrap-table {
  display: table;
  width: 100%;
  border-collapse: separate;
  border-spacing: 10px 0px;
}
.eg4 .left,
.eg4 .middle,
.eg4 .right {
  display: table-cell;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;inline-block和calc函数&lt;/h2&gt;
&lt;p&gt;和两栏布局时一样，&lt;code&gt;inline-block&lt;/code&gt; 和 &lt;code&gt;calc函数&lt;/code&gt; 也是一种解决方案，设置三个部分的 &lt;code&gt;display: inline-block&lt;/code&gt;，中间元素的宽度用 &lt;code&gt;calc&lt;/code&gt; 函数计算。同样需要注意 &lt;code&gt;inline-block&lt;/code&gt; 的缝隙问题，设置父元素的 &lt;code&gt;font-size: 0&lt;/code&gt;，因为三个部分的字体大小以及内边距可能有所不同，所以最好用 &lt;code&gt;vertical-align&lt;/code&gt; 来确保三个元素的顶端对齐。&lt;code&gt;HTML&lt;/code&gt; 结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;wrap wrap-inline eg5&quot;&gt;
  &amp;#x3C;div class=&quot;left&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;middle&quot;&gt;例五： inline-block+calc函数和负margin&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;right&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* eg5 */
.wrap-inline {
  margin-left: -10px;
  font-size: 0;
}
.wrap-inline div {
  display: inline-block;
  margin-left: 10px;
  vertical-align: top;
  font-size: 30px;
}
.wrap-inline .middle {
  width: calc(100% - 430px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;圣杯布局&lt;/h2&gt;
&lt;p&gt;圣杯布局的原理其实就是利用的 &lt;code&gt;float&lt;/code&gt; 元素生成的 &lt;code&gt;BFC&lt;/code&gt; 从左到右依次排列，利用宽度、负 &lt;code&gt;margin&lt;/code&gt; 和定位来解决该问题。对于 &lt;code&gt;wrap&lt;/code&gt; 元素，用 &lt;code&gt;padding&lt;/code&gt; 预留出左右两个元素的空间，&lt;code&gt;middle元素&lt;/code&gt; 第一个渲染，占满父元素，&lt;code&gt;left和right&lt;/code&gt; 元素依次排在第二行。&lt;code&gt;left&lt;/code&gt; 设置 &lt;code&gt;margin: -100%&lt;/code&gt; 则左边界和 &lt;code&gt;middle&lt;/code&gt; 的左边界对齐，再利用 &lt;code&gt;position: relative&lt;/code&gt; 的定位属性 &lt;code&gt;left&lt;/code&gt; 在确定 &lt;code&gt;left&lt;/code&gt; 元素的位置。 &lt;code&gt;right&lt;/code&gt; 元素用同样的原理，设置 &lt;code&gt;margin: -(自身宽度)&lt;/code&gt;，再利用定位属性确定位置。圣杯布局有一个问题就是当 &lt;code&gt;middle&lt;/code&gt; 的宽度小于左边元素的宽度时，就没有足够的空间排列三个元素，&lt;code&gt;left和right&lt;/code&gt; 会被渲染到下一行。&lt;code&gt;HTML&lt;/code&gt; 结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;wrap eg6&quot;&gt;
  &amp;#x3C;div class=&quot;middle&quot;&gt;例六： 圣杯布局&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;left&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;right&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* eg6 */
.eg6 {
  padding: 0 210px;
}
.eg6 div {
  float: left;
}
.eg6 .middle {
  width: 100%;
}
.eg6 .left {
  margin-left: -100%;
  position: relative;
  left: -210px;
}
.eg6 .right {
  margin-left: -200px;
  position: relative;
  left: 210px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;双飞翼布局&lt;/h2&gt;
&lt;p&gt;双飞翼布局是为了解决圣杯布局中 &lt;code&gt;middle&lt;/code&gt; 宽度过小排列不下结构发生变化的问题。圣杯布局之所以会产生这种问题，本质上是因为我们在父元素上用 &lt;code&gt;padding&lt;/code&gt; 给两边的元素预留空间，导致父元素的 &lt;code&gt;content&lt;/code&gt; 区域变小，如果我们两侧的元素宽度比较大，那么父元素的 &lt;code&gt;padding&lt;/code&gt; 就比较大，留下的空间自然就很小，可能无法排列三个元素了。双飞翼布局不再用 &lt;code&gt;padding&lt;/code&gt; 来改变父元素内容的宽度，而是在 &lt;code&gt;middle&lt;/code&gt; 中嵌套一层 &lt;code&gt;div.content&lt;/code&gt;，这层 &lt;code&gt;div&lt;/code&gt; 用 &lt;code&gt;margin&lt;/code&gt; 来给左右元素预留空间，这样的话父元素和 &lt;code&gt;middle&lt;/code&gt; 元素都有整个 &lt;code&gt;body&lt;/code&gt; 的宽度，不会发生因为宽度不够而导致的结构改变的问题。同时因为父元素宽度未被限制，&lt;code&gt;left&lt;/code&gt; 和 &lt;code&gt;right&lt;/code&gt; 元素不再需要用定位属性来修改相对位置。&lt;code&gt;HTML&lt;/code&gt; 结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;wrap eg7&quot;&gt;
  &amp;#x3C;div class=&quot;middle&quot;&gt;
    &amp;#x3C;div class=&quot;content&quot;&gt;例七： 双飞翼布局&amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;left&quot;&gt;&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;right&quot;&gt;&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* eg7 */
.eg7 &gt; div {
  float: left;
}
.eg7 .middle {
  width: 100%;
  padding: 0;
  background-color: transparent;
}
.eg7 .middle .content {
  margin: 0 210px;
  background-color: lightgreen;
  height: 100%;
  padding: 10px;
  box-sizing: border-box;
}
.eg7 .left {
  margin-left: -100%;
}
.eg7 .right {
  margin-left: -200px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;本文示例代码查看&lt;a href=&quot;https://www.clloz.com/study/3-colum.html&quot;&gt;页面&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>对line-height和vertical-align的一些理解</title><link>https://clloz.com/blog/line-heightvertical-align</link><guid isPermaLink="true">https://clloz.com/blog/line-heightvertical-align</guid><pubDate>Wed, 29 Aug 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近遇到了很多关于 &lt;code&gt;line-height&lt;/code&gt; 和 &lt;code&gt;vertical-align&lt;/code&gt; 等行盒模型相关问题，决定单独写一篇彻底搞清楚原理。行内元素的排列是一个很特殊的东西，不同的浏览器的表现很可能不一样。&lt;/p&gt;
&lt;h2&gt;深入理解line-height&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;line-height&lt;/code&gt; 翻译成行高，指的是一行的基线到下一行基线的距离，具体的细节如图&lt;img src=&quot;https://clloz.com/_astro/line-height1.CauRA3EV_ZbSa2y.webp&quot; alt=&quot;行高细节&quot;&gt; 任何一个行内框中的结构都如图所示，顶线和底线之间显示文字，他们的距离就等于font-size，中线的位置是基线向上 &lt;code&gt;1/2&lt;/code&gt; 个 &lt;code&gt;x&lt;/code&gt; 的高度，我们可以近似的看做 &lt;code&gt;x&lt;/code&gt; 的那个交叉点。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;line-height&lt;/code&gt; 不允许负值，为 &lt;code&gt;font&lt;/code&gt; 的上下间距 &lt;code&gt;normal&lt;/code&gt; 的情况为默认值，浏览器会计算出“合适”的行高，多数浏览器（ &lt;code&gt;Georgia&lt;/code&gt; 字体下）取值为 &lt;code&gt;1.14&lt;/code&gt; ，即为 &lt;code&gt;font-size&lt;/code&gt; 的 &lt;code&gt;1.14&lt;/code&gt; 倍，如果未设定&lt;code&gt;font-size&lt;/code&gt;，那既是基准值 &lt;code&gt;16px&lt;/code&gt; 的 &lt;code&gt;1.14&lt;/code&gt; 倍, &lt;code&gt;normal&lt;/code&gt; 和具体的数值相比，会因为字体的不同而不同。&lt;/p&gt;
&lt;p&gt;解释一下关于上图的几个概念，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;content area&lt;/code&gt; ：就是顶线和底线之间包裹的区域，高度只和 &lt;code&gt;font&lt;/code&gt; 有关,宽度就和字数有关，就理解为包裹文字的一个区域。&lt;/p&gt;
&lt;p&gt;The height of the content area should be based on the font, but this specification does not specify how.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;行高、行距：行高就是相邻的文本行之间的基线的距离，这个高度包括了 &lt;code&gt;content area&lt;/code&gt; 和行距。 行距就是行高减去 &lt;code&gt;content area&lt;/code&gt; 的高度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;inline box&lt;/code&gt; (行内框)：所有的行内元素都会生成看不见的行内框，就是标准中的 &lt;code&gt;inline box&lt;/code&gt; ，我们无法看到他，它是在浏览器渲染中使用的，在不设置其他样式的情况下， &lt;code&gt;inline box&lt;/code&gt; 的高度和 &lt;code&gt;content area&lt;/code&gt; 高度是一样的。我们理解 &lt;code&gt;inline box&lt;/code&gt; 的时候,其实可以不必太刻意，他也有高度。为了排版行内元素，&lt;code&gt;inline box&lt;/code&gt; 和 &lt;code&gt;line box&lt;/code&gt; 都是 &lt;code&gt;css&lt;/code&gt; 来规定的，&lt;code&gt;inline box&lt;/code&gt; 水平方向排列在 &lt;code&gt;line box&lt;/code&gt; 里。&lt;code&gt;inline box&lt;/code&gt; 的高度并不会受内部元素的影响，我们只要设置了 &lt;code&gt;line-height&lt;/code&gt; ，那么这个行内框的高度就已经确定了，就像 &lt;code&gt;div&lt;/code&gt; 一样，我们只要设置了一个 &lt;code&gt;div&lt;/code&gt; 的高度，不管里面的内容多长，&lt;code&gt;div&lt;/code&gt; 的高度也不会产生变化，内容超出了那就溢出了，&lt;code&gt;line box&lt;/code&gt; 也是一样，当 &lt;code&gt;line-height&lt;/code&gt; 小于内部的文字的高度，内部的文字就会溢出了，见例 &lt;code&gt;5&lt;/code&gt; ,我们可以看到红色背景的 &lt;code&gt;span&lt;/code&gt; 超出了父元素的范围，这里的红色背景其实是 &lt;code&gt;content area&lt;/code&gt; 的，不是 &lt;code&gt;inline box&lt;/code&gt; 的，父元素的高度为&lt;code&gt;span&lt;/code&gt; 的 &lt;code&gt;line-height&lt;/code&gt; 。这里要注意的是，父元素要设置 &lt;code&gt;font-size&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt; ，因为浏览器默认字体为 &lt;code&gt;16px&lt;/code&gt; ，如果不设置 &lt;code&gt;font-size&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt; 的话，父元素的高度就会为 &lt;code&gt;16+2(border)px&lt;/code&gt;，还有就是要设置 &lt;code&gt;span&lt;/code&gt; 的 &lt;code&gt;vertical-align&lt;/code&gt; 为 &lt;code&gt;top&lt;/code&gt; ，不然也会引起高度变化，原因下面讲。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;line box&lt;/code&gt; 刚刚在 &lt;code&gt;inline box&lt;/code&gt; 哪里我们就提到了行框 &lt;code&gt;line box&lt;/code&gt; 的概念，&lt;code&gt;css&lt;/code&gt; 规定了，所有的&lt;code&gt;inline box&lt;/code&gt; 排列在&lt;code&gt;line box&lt;/code&gt; 中，这个 &lt;code&gt;line box&lt;/code&gt; 的高度就是非常关键的跟我们的 &lt;code&gt;vertical-align&lt;/code&gt; 还有 &lt;code&gt;line-height&lt;/code&gt; 相关的，&lt;code&gt;line box&lt;/code&gt; 的高度怎么计算呢？网上大多的说法是 &lt;code&gt;line box&lt;/code&gt; 中高度最高的那个 &lt;code&gt;inline box&lt;/code&gt; 决定了 &lt;code&gt;line box&lt;/code&gt; 的高度，我一直觉得这种说法不严谨，应该是以 &lt;code&gt;inline box&lt;/code&gt; 最高的上边界线和最低的下边界构成 &lt;code&gt;line box&lt;/code&gt; 的边界，不过我没有找到例子来证明这一点，&lt;code&gt;css&lt;/code&gt; 的渲染是以 &lt;code&gt;line box&lt;/code&gt; 的高度尽量小为目的，所以我想最高的 &lt;code&gt;inline box&lt;/code&gt; 来决定高度也是有道理的，如果下次找到反例我会来补充。 &lt;code&gt;line-height&lt;/code&gt; 决定了 &lt;code&gt;inline box&lt;/code&gt; 的高度，而 &lt;code&gt;vertical-align&lt;/code&gt; 则决定了 &lt;code&gt;inline box&lt;/code&gt; 在 &lt;code&gt;line box&lt;/code&gt; 中的位置。&lt;code&gt;line box&lt;/code&gt; 的高度由行内最高的 &lt;code&gt;inline box&lt;/code&gt; 决定，但同时也可以直接用 &lt;code&gt;line-height&lt;/code&gt; 来决定，给父元素设置 &lt;code&gt;line-height&lt;/code&gt; 就可以给 &lt;code&gt;line box&lt;/code&gt; 设定高度，而我们知道浏览器有默认字体大小，也就有默认的 &lt;code&gt;line-height&lt;/code&gt; ，所以行内元素一旦确定了高度和 &lt;code&gt;vertical-align&lt;/code&gt; 我们就能确定它在行内的位置。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;line box&lt;/code&gt; 的高度和&lt;code&gt;inline box&lt;/code&gt; 的&lt;code&gt;vertical-align&lt;/code&gt; 是互相依赖的，他们两者的确定都要知道对方的值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;  字母 &lt;code&gt;x&lt;/code&gt; 在 &lt;code&gt;line-height&lt;/code&gt; 中有很重要的作用，这里单独说一说，首先基线是以字母 &lt;code&gt;x&lt;/code&gt; 的下边界为基准的，在 &lt;code&gt;css&lt;/code&gt; 中有一个 &lt;code&gt;x-height&lt;/code&gt; 的概念，就是用来形容 &lt;code&gt;x&lt;/code&gt; 的高度，我们在 &lt;code&gt;css&lt;/code&gt; 中描述大小可以用 &lt;code&gt;px&lt;/code&gt;，&lt;code&gt;em&lt;/code&gt;，百分比，还可以用 &lt;code&gt;ex&lt;/code&gt;，这个 &lt;code&gt;ex&lt;/code&gt; 就是 &lt;code&gt;x&lt;/code&gt; 的高度，不过很多浏览器用 &lt;code&gt;0.5em&lt;/code&gt; ，也就是字体大小的一半来渲染。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/line-height2.DrxmvV-0_Z15nN33.webp&quot; alt=&quot;x-height示意图&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ascender height&lt;/code&gt; : 上下线高度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cap height&lt;/code&gt; : 大写字母高度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;median&lt;/code&gt; : 中线&lt;/li&gt;
&lt;li&gt;&lt;code&gt;descender height&lt;/code&gt; : 下行线高度&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;比如例子中的第三块的代码，图片高度是 &lt;code&gt;200px&lt;/code&gt;；父元素的 &lt;code&gt;line-height：240px&lt;/code&gt;，在给图片设置 &lt;code&gt;vertical-align&lt;/code&gt; 的情况下，在 &lt;code&gt;chrome&lt;/code&gt; 下，父元素的高度为 &lt;code&gt;312px&lt;/code&gt; ，这个 &lt;code&gt;312px&lt;/code&gt; 是怎么来的呢，我们给父元素里面添加一些文字，他们会生成匿名 &lt;code&gt;inline box&lt;/code&gt; ， &lt;code&gt;line-height&lt;/code&gt; 为 &lt;code&gt;240px&lt;/code&gt; ，图片的默认对齐方式为 &lt;code&gt;baseline&lt;/code&gt;，也就是基线和父元素基线对齐，图片没有基线，就按照 &lt;code&gt;bottom margin&lt;/code&gt; 来计算。也就是我们图片的下边缘要和文字中的 &lt;code&gt;x&lt;/code&gt; 的下边缘对齐，图片的下面的空间的是由基线到底线的距离加上 &lt;code&gt;1/2&lt;/code&gt; 行距构成的，行距很好计算，&lt;code&gt;line-height&lt;/code&gt; 减去字体大小就得到行距，不过这个基线到底线的距离是不好计算的，目前还不知道方法。这个例子中，&lt;code&gt;1/2&lt;/code&gt; 行距为 &lt;code&gt;(240 - 20)/2 = 110px&lt;/code&gt;；图片下面的高度应该为 &lt;code&gt;110+x&lt;/code&gt; ( &lt;code&gt;x&lt;/code&gt; 为基线到底线的距离)，父元素的高度为 &lt;code&gt;200+110+x&lt;/code&gt;，在 &lt;code&gt;chrome&lt;/code&gt; 中为 &lt;code&gt;312px&lt;/code&gt;, &lt;code&gt;firefox&lt;/code&gt; 中为 &lt;code&gt;312.5px&lt;/code&gt;，而且在我们微调字体的时候，有时候发现&lt;code&gt;div&lt;/code&gt; 的高度不会发生变化，比如设置字体为 &lt;code&gt;21,22&lt;/code&gt; 等，我认为是这样的，字体增大或减小的时候，&lt;code&gt;x&lt;/code&gt; 也会随之增大和减小，字体越大，行距越小，&lt;code&gt;x&lt;/code&gt; 越大，行距和 &lt;code&gt;x&lt;/code&gt; 的变化是相反的，在一定程度内，会抵消，所以微调字体的时候，不会发生变化。当我们将字体设置为 &lt;code&gt;0&lt;/code&gt; 的时候，各个浏览器的表现基本一致，高度都是 &lt;code&gt;320&lt;/code&gt;。 注：以上高度均没有就算 &lt;code&gt;border&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Align the baseline of the box with the baseline of the parent box. If the box does not have a baseline, align the bottom margin edge with the parent&apos;s baseline.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.clloz.com/study/line-height.html&quot;&gt;查看文中实例&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/css.D7sdqkE4.jpg"/><enclosure url="/_astro/css.D7sdqkE4.jpg"/></item><item><title>活了100万次的猫</title><link>https://clloz.com/blog/neko</link><guid isPermaLink="true">https://clloz.com/blog/neko</guid><pubDate>Thu, 21 Jun 2018 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;活了100万次的猫 100万回生きたねこ&lt;/h2&gt;
&lt;p&gt;活了一百万次的猫是我非常喜欢的一本童话绘本，讲了一只活了一百万次的猫如何找到了真正生命的意义的故事。我想每个人都曾经思考过自己来到这个世界上的意义，是学习更多的知识，赚更多的钱，还是获得更多人的尊重，每个人的想法各不相同，这本绘本的作者佐野洋子女士跟我的想法相通，我想我们都是渴望爱着别人也渴望被爱的，找寻到了一个自己爱的人和一份自己爱的工作并且能每天微笑着度过，即使将来我们老了病了，躺在床上等待生命的尽头的时候，我们也能微笑着，回首这一生，我的笑和泪都给了我最爱的人和事，这是一件多么幸福的事情。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;１００万年も　しなない　ねこが　いました。 &lt;strong&gt;有一只一百万年都没有死的猫。&lt;/strong&gt; １００万回も　しんで　１００万回も　生きたのです。 &lt;strong&gt;他死过100万次也活过一百万次。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430626.CB_ick0m_Z2jg9JY.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;りっぱな とら猫でした。 &lt;strong&gt;他是一只漂亮的虎斑猫。&lt;/strong&gt; １００万人の人が　そのねこをかわいがり。 &lt;strong&gt;一百万个人疼爱过这只猫。&lt;/strong&gt; １００万人のひとが　そのねこが死んだとき泣きました。 &lt;strong&gt;一百万个人在这只猫死的时候为他哭过。&lt;/strong&gt; ねこは 1 回もなきませんでした。 &lt;strong&gt;猫一次也没有哭过。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430627.BV8de14d_1CHWnf.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;あるとき ねこは 王さまの ねこでした。 &lt;strong&gt;有一次，猫是国王的猫。&lt;/strong&gt; ねこは 王さまなんか きらいでした。 &lt;strong&gt;猫讨厌什么国王。&lt;/strong&gt; 王さまは せんそうが じょうずで いつも せんそうを していました。 &lt;strong&gt;国王很会打仗，一直打仗打个不停。&lt;/strong&gt; そして ねこをりっぱなかごにいれて せんそうにつれていきました。 &lt;strong&gt;猫被国王放在一个漂亮的篮子里带到了战场。&lt;/strong&gt; ある日 ねこは とんできた やに あたって しんでしまいました。 &lt;strong&gt;有一天，猫被突然飞来的乱箭射死了。&lt;/strong&gt; 王さまは たたかいの まっさいちゅうに ねこを だいて なきました。 &lt;strong&gt;国王在战场的中间抱着猫痛哭。&lt;/strong&gt; 王さまは せんそうを やめて おしろに 帰ってきました。 &lt;strong&gt;国王不打仗了，回到了城堡。&lt;/strong&gt; そして おしろの にわに ねこを うめました。 &lt;strong&gt;把猫埋在了城堡的花园里。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430628.CgRgmtJv_8pq2c.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;あるとき ねこは 船のりの ねこでした。 &lt;strong&gt;有一次，猫是水手的猫。&lt;/strong&gt; ねこは 海なんか きらいでした。 &lt;strong&gt;猫讨厌大海什么的。&lt;/strong&gt; 船のりは せかいじゅうの海と せかいじゅうのみなとに ねこをつれていきました。 &lt;strong&gt;水手带着猫，游遍了世界上的大海和港口。&lt;/strong&gt; ある日 ねこは船からおちてしまいました。 &lt;strong&gt;有一天，猫从船上掉下去了。&lt;/strong&gt; ねこはおよげなかったのです。 &lt;strong&gt;猫不会游泳。&lt;/strong&gt; 船のりが いそいであみですくいあげると ねこは びしょぬれになって しんでいました。 &lt;strong&gt;水手赶紧用网把猫捞了上来，猫已经被淹死了。&lt;/strong&gt; 船のりは ぬれた ぞうきんのようになった ねこを だいて 大きな声で なきました。 &lt;strong&gt;水手把像湿抹布一样的猫抱在怀里放声大哭。&lt;/strong&gt; そして遠い みなと町のこうえんの木の下にねこをうめました。 &lt;strong&gt;然后，水手把猫葬在了遥远港口的公园的树下。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430629.qBAoomb4_1JWk6r.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;あるとき ねこは サーカスの手品つかいのねこでした。 &lt;strong&gt;有一次，猫是马戏团魔术师的猫。&lt;/strong&gt; ねこは サーカスなんか きらいでした。 &lt;strong&gt;猫讨厌什么魔术师。&lt;/strong&gt; 手品つかいは 毎日ねこを はこの中に入れて のこぎりで まっぷたつに しました。 &lt;strong&gt;魔术师每天把猫放到箱子里用锯子锯成两半。&lt;/strong&gt; それから まるのままのねこを はこからとりだし はくしゅかっさいをうけました。 &lt;strong&gt;然后，当他把完好的猫从箱子里取出来的时候，观众们拍手叫好。&lt;/strong&gt; ある日 手品つかいは まちがえて ほんとうに ねこをまっぷたつに してしまいました。 &lt;strong&gt;有一天，魔术师失误了，真的把猫切成了两半。&lt;/strong&gt; 手品つかいは まっぷたつに なってしまったねこを 両手にぶらさげて大きな声で なきました。 &lt;strong&gt;魔术师用两手拎着已经被锯成两半的猫大声痛哭。&lt;/strong&gt; だれも はくしゅかっさいを しませんでした。 &lt;strong&gt;没有一个观众鼓掌了。&lt;/strong&gt; 手品つかいは サーカス小屋のうらに ねこをうめました。 &lt;strong&gt;魔术师把猫葬在了马戏团小屋的后面。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430630.DiMFpt67_1xhcwC.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;あるとき ねこは どろぼうの ねこでした。 &lt;strong&gt;有一次，猫是小偷的猫。&lt;/strong&gt; ねこは どろぼうなんか だいきらいでした。 &lt;strong&gt;猫最讨厌什么小偷。&lt;/strong&gt; どろぼうは ねこと いっしょに くらい町を ねこのように しずかに歩きまわりました。 &lt;strong&gt;小偷和猫一起在黑暗的街道上像猫一样轻手轻脚的游荡。&lt;/strong&gt; どろぼうは いぬのいる 家にだけ どろぼうに はいりました。 &lt;strong&gt;小偷只到养狗的人家去偷东西。&lt;/strong&gt; いぬが ねこに ほえているあいだに どろぼうは 金庫をこじあけました。 &lt;strong&gt;趁着够对着猫叫的时候打开保险箱。&lt;/strong&gt; ある日 ねこは いぬにかみころされてしまいまいした。 &lt;strong&gt;有一天，猫被狗咬死了。&lt;/strong&gt; どろぼうは ぬすんだ ダイヤモンドと いっしょに ねこをだいて 夜の町を 大きな声で なきながら 歩きました。 &lt;strong&gt;小偷抱着偷来的钻石和猫在夜晚的街道上边走边大声哭泣。&lt;/strong&gt; そして いえにかえって 小さなにわに ねこをうめました。 &lt;strong&gt;然后，回到家，小偷把猫葬在了庭院里。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430631.O_TJPM3A_1XNOU3.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;あるとき ねこは ひとりぼっちの おばあさんの ねこでした。 &lt;strong&gt;有一次，猫是孤独的老婆婆的猫。&lt;/strong&gt; ねこは おばあさんなんか だいきらいでした。 &lt;strong&gt;猫最讨厌什么老婆婆。&lt;/strong&gt; おばあさんは 毎日 ねこをだいて 小さなまどから 外を 見ていました。 &lt;strong&gt;老婆婆每天抱着猫从小小的窗户看着外面。&lt;/strong&gt; ねこは 一日じゅう おばあさんの ひざの上で ねむっていました。 &lt;strong&gt;猫一整天都在老婆婆的膝盖上睡觉。&lt;/strong&gt; やがて ねこは 年をとって しにました。 &lt;strong&gt;不久，猫年纪大了，死了。&lt;/strong&gt; よぼよぼの おばあさんは よぼよぼの しんだねこを だいて 一日じゅう なきました。 &lt;strong&gt;步履蹒跚的老婆婆抱着老死的猫哭了一整天。&lt;/strong&gt; おばあさんは にわの木の下に ねこをうめました。 &lt;strong&gt;老婆婆把猫葬在了庭院的树下。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430632.lt6zCBPg_ePRpT.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;あるとき ねこは 小さな 女の子の ねこでした。 &lt;strong&gt;有一次，猫是小女孩的猫。&lt;/strong&gt; ねこは 子どもなんか だいきらいでした。 &lt;strong&gt;猫最讨厌什么小女孩了。&lt;/strong&gt; 女の子は ねこを おんぶしたり しっかり だいて ねたりしました。 &lt;strong&gt;小女孩不是背着猫就是紧紧地抱着猫睡觉。&lt;/strong&gt; ないたときは ねこの せなかで なみだを ふきました。 &lt;strong&gt;哭的时候就在猫的背上擦眼泪。&lt;/strong&gt; ある日 ねこは 女の子の せなかで おぶいひもが 首に まきついて しんでしまいました。 &lt;strong&gt;有一天，猫在小女孩的背上被背带缠住脖子勒死了。&lt;/strong&gt; ぐらぐらの頭に なってしまった ねこを だいて 女の子は 一日じゅう なきました。 &lt;strong&gt;小女孩抱着头断了的猫哭了一整天。&lt;/strong&gt; そして ねこを にわの 木の下に うめました。 &lt;strong&gt;然后，小女孩把猫葬在了庭院的树下。&lt;/strong&gt; ねこは しぬのなんか へいきだったのです。 &lt;strong&gt;猫对死亡已经麻木了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430633.DBoiBh4k_Z1jm3Qh.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;あるとき ねこは だれの ねこでも ありませんでした。 &lt;strong&gt;有一次，猫再也不是别人的猫了。&lt;/strong&gt; のらねこだったのです。 &lt;strong&gt;猫是一只野猫。&lt;/strong&gt; ねこは はじめて 自分のねこに なりました。 &lt;strong&gt;猫第一次成了自己的猫。&lt;/strong&gt; ねこは 自分がだいすきでした。 &lt;strong&gt;猫最喜欢自己了。&lt;/strong&gt; なにしろ りっぱなとらねこだったので りっぱなのらねこになりました。 &lt;strong&gt;原本他就是一直漂亮的虎斑猫，这次当然也是一直漂亮的野猫。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430634.CDYLASB6_Z1sX7Dq.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;どんな めすねこも ねこのおよめさんに なりたがりました。 &lt;strong&gt;所有的母猫都想做他的新娘。&lt;/strong&gt; 大きなさかなを プレゼントする ねこも いました。 &lt;strong&gt;有的猫送给他大鱼。&lt;/strong&gt; 上等のねずみを さしだす ねこも いました。 &lt;strong&gt;有的猫送给他上等的老鼠。&lt;/strong&gt; めずらしい またたびを おみやげにする ねこも いました。 &lt;strong&gt;有的猫送给他珍贵的木天蓼。&lt;/strong&gt; りっぱな とらもようを なめてくれる ねこも いました。 &lt;strong&gt;有的猫为他舔毛。&lt;/strong&gt; ねこは いいました。 &lt;strong&gt;猫说了：&lt;/strong&gt; 「おれは １００万回も しんだんだぜ。いまさら おっかしくて！」 &lt;strong&gt;我可是死了一百万次的猫，就这些东西我可不稀罕。&lt;/strong&gt; ねこは だれよりも 自分が すきだったのです。 &lt;strong&gt;猫还是最喜欢自己。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430635.B_VyKiuT_2fFcXd.webp&quot; alt=&quot;&quot;&gt; たった １ぴき ねこに 見むきも しない白いうつくしいねこがいました。 &lt;strong&gt;只有一只漂亮的白猫，看都没看一眼。&lt;/strong&gt; ねこは 白いねこの そばに いって &lt;strong&gt;猫来到白猫的身边说：&lt;/strong&gt; 「おれは １００万回も しんだんだぜ！」 と いいました。 &lt;strong&gt;“我已经死了一百万次了。”&lt;/strong&gt; 白いねこは「そう。」と いったきりでした。 &lt;strong&gt;白猫只说了一句：“是吗”&lt;/strong&gt; ねこは すこしはらをたてました。なにしろ 自分がだいすきでしたからね。 &lt;strong&gt;猫有点生气，毕竟他是那么喜欢自己。&lt;/strong&gt; つぎの日も つぎの日も ねこは白いねこの ところへいって いいました。 &lt;strong&gt;第二天，第三天，猫又到白猫那边说：&lt;/strong&gt; 「きみは まだ １回も 生きおわって いないんだろ。」 &lt;strong&gt;“你一次还没活完吧”&lt;/strong&gt; 白いねこは「そう。」と いったきりでした。 &lt;strong&gt;白猫还是只说了一句“是吗”&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430636.CZqpcbaK_xwwsA.webp&quot; alt=&quot;&quot;&gt; ある日ねこは白いねこの前でくるくると３回ちゅうがえりをしていいました。 &lt;strong&gt;一天，猫跑到白猫面前骨碌骨碌地翻了三个跟头。&lt;/strong&gt; 「おれ サーカスの ねこだったことも あるんだぜ。」 &lt;strong&gt;“我曾经是马戏团的猫”&lt;/strong&gt; 白いねこは「そう。」と いったきりでした。 &lt;strong&gt;白猫说“是吗”&lt;/strong&gt; 「おれは１００万回も・・・・・・。」と いいかけて ねこは &lt;strong&gt;“我可是活了一百万次。。。”猫说道一半&lt;/strong&gt; 「そばに いても いいかい。」と 白いねこに たずねました。 &lt;strong&gt;“我可以待在你身边么” 猫问白猫。&lt;/strong&gt; 白いねこは「ええ。」と いいました。 &lt;strong&gt;白猫说：“好”&lt;/strong&gt; ねこは 白いねこの そばに いつまでも いました。 &lt;strong&gt;猫从此一直待在了白猫身边。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430637.BGtBg43u_ZCAk5K.webp&quot; alt=&quot;&quot;&gt; 白いねこは かわいい子ねこを たくさんうみました。 &lt;strong&gt;白猫生了很多可爱的小猫。&lt;/strong&gt; ねこは もう「１００万回も・・・・・・。」とは けっしていいませんでした。 &lt;strong&gt;猫再也不说我活了一百万次了。&lt;/strong&gt; ねこは 白いねことたくさんの 子ねこを自分よりも すきなくらいでした。 &lt;strong&gt;比起自己，猫更喜欢白猫和小猫们。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430638.BNJScMxi_ZRMUWX.webp&quot; alt=&quot;&quot;&gt; やがて 子ねこたちは 大きくなって それぞれ どこかへ いきました。 &lt;strong&gt;不久，小猫们长大了，一个接一个地离开了。&lt;/strong&gt; 「あいつらも りっぱな のらねこに なったなあ。」 &lt;strong&gt;“孩子们也成了不赖的野猫呢”&lt;/strong&gt; と ねこは まんぞくして いいました。 &lt;strong&gt;猫满足地说道。&lt;/strong&gt; 「ええ。」と 白いねこは いいました。 &lt;strong&gt;“是啊” 白猫回道。&lt;/strong&gt; そして グルグルと やさしく のどを ならしました。 &lt;strong&gt;然后，白猫的喉咙里发出了轻柔的咕噜咕噜声。&lt;/strong&gt; 白いねこは すこし おばあさんに なっていました。 &lt;strong&gt;白猫慢慢变老了。&lt;/strong&gt; ねこは いっそう やさしく グルグルと のどを ならしました。 &lt;strong&gt;猫也从喉咙里发出了轻柔的咕噜咕噜声。&lt;/strong&gt; ねこは 白いねこといっしょに いつまでも生きていたいと思いました。 &lt;strong&gt;猫想和白猫永远一起生活下去。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430639.B0YbLW50_Z1oN3IN.webp&quot; alt=&quot;&quot;&gt; ある日 白いねこは ねこのとなりで しずかにうごかなくなっていました。 &lt;strong&gt;一天，白猫在猫的身边一动不动了。&lt;/strong&gt; ねこは はじめてなきました。 夜になって 朝になって また夜になって 朝になって ねこは１００万回もなきました。 &lt;strong&gt;猫第一次哭了，从夜里哭到早上，又从早上哭到夜里，整整哭了一百万次。&lt;/strong&gt; 朝になって 夜になって ある日のお昼に ねこはなきやみました。 &lt;strong&gt;一天又一天过去了，一天中午，猫停止了哭泣。&lt;/strong&gt; ねこは 白いねこの となりで しずかに うごかなくなりました。 &lt;strong&gt;猫在白猫的身边安静地一动不动了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://clloz.com/_astro/430640.hlZUzg4f_b9O9U.webp&quot; alt=&quot;&quot;&gt; ねこは もう けっして 生きかえりませんでした。 &lt;strong&gt;猫再也没有活过来了。&lt;/strong&gt; &lt;img src=&quot;https://clloz.com/_astro/timg.DDPk3E6q_2jfmpG.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;这本绘本中的插画都很漂亮，要说我最喜欢哪一张，那就是最后虎斑猫抱着白猫痛哭的那张，我既替他难过，又为他开心，一种复杂的心情交织着，让自己的眼泪也止不住。难过的是，再美好的事物也终有结束的一天，不管我们做什么努力，有些事情终究是无法改变的；开心的是，找寻到自己活着的意义比什么都重要，在爱里出生，又在爱里死亡，即使我在痛哭，但为了失去的爱而痛哭，何尝不是一种幸福。最后一幅插画位于整本书的背面，当我们看完整本书合上书的时候，能看到他们两个肩并肩看着远方的背影，是那么的温馨，安静，携手一生，最后在爱中为自己的生命画上完美的句点。&lt;/p&gt;
&lt;p&gt;佐野洋子在六十六岁完成这部作品，也曾经来北京图书馆朗读过这部作品，我想这是她对一生经历的思考得出的故事，是她这一生对爱的理解，我也会努力追寻这样的人生。&lt;/p&gt;</content:encoded><h:img src="/_astro/timg.DDPk3E6q.jpg"/><enclosure url="/_astro/timg.DDPk3E6q.jpg"/></item></channel></rss>