<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel><title>贝莉卡个人博客</title><link>https://www.beilika.com/</link><description>贝莉卡个人博客，分享JS，HTML，JAVA，PHP，Linux，Golang等编程语言技术</description><language>zh-cn</language><copyright>Copyright beilika.com. All Rights Reserved</copyright><lastBuildDate>Fri, 13 Feb 2026 10:20:38 +0800</lastBuildDate><item><title><![CDATA[ComfyUI部分节点输入输出及参数含义说明]]></title><link>https://www.beilika.com/article/c169.html</link><description><![CDATA[K采样器输入模型(Model)：加载的AI模型文件，通常是.safetensors或.ckpt格式，决定了图像生成的基本风格与能力。正面提示词(PositivePrompt)：描述你希望图像包含的内容。负面提示词(NegativePrompt)：描述你希望图像中避免出现的内容。潜在图像(LatentImage)：输入的图像潜变量，通常来自“空潜变量”节点，定义了初始噪声和图像尺寸。输出LATENT：它是一个经过多步去噪迭代后、包含生成图像信息的潜在变量。参数种子(Seed)：控制随机噪声的初始状态，固定种子可以在其他参数不变时生成相同的图像，用于结果的可复现性。步数(Steps)：定义采样过程的总迭代次数。通常步数越多，细节和质量可能越好，但生成时间也更长。一般在25步左右刚好。CFG值(CFGScale)：分类器自由引导强度。此值控制生成图像与提示词的贴合度。值越高越贴合提示词，但过高可能导致图像过于生硬或颜色过饱和。采样器(Sampler)：选择具体的采样算法，如euler、euler_ancestral、dpm_2m等。不同算法在速度、质量和创造性上各有侧重。调度器(Scheduler)：控制采样过程中噪声退火（去噪）的节奏或计划，如normal、karras、simple等，会影响图像细节和收敛性。降噪强度(Denoise)：控制对输入潜变量（或参考图）的保留程度。值为1.0时完全重新生成，值越低则越保留原始图像的结构与内容。]]></description><category>Ai</category><guid>https://www.beilika.com/article/c169.html</guid><pubDate>Fri, 13 Feb 2026 10:05:29 +0800</pubDate></item><item><title><![CDATA[ComfyUI安装包文件夹详解，及自定义模型位置]]></title><link>https://www.beilika.com/article/c168.html</link><description><![CDATA[安装目录部分文件作用解析📁ComfyUI_windows_portable├──📁ComfyUI//comfyUI主要文件夹│├──.git//Git版本控制文件夹，代码版本管理用│├──.github//GitHubActions工作流文件夹│├──📁comfy//│├──📁comfy_extras//│├──📁custom_nodes//comfyUI自定义节点文件目录（插件安装目录）│├──📁input//comfyUI上传文件夹，当你使用了如**loadimage**节点，对应上传的图片会存储到这个文件夹│├──📁models//对应模型文件配置文件夹│|├──📁checkpoints//检查点大模型文件存放路径│|├──📁clip//CLIP文件存放路径│|├──📁clip_vision//CLIP_vision文件存放路径│|├──📁configs│|├──📁controlnet//ControlNet模型存放路径│|├──📁diffusers│|├──📁embedding//embedding模型存放路径│|├──📁gligen│|├──📁hypernetworks//超网络模型│|├──📁loras//Lora模型存放路径│|├──📁style_models│|├──📁unet│|├──📁upscale_models//upscale_models放大模型存放路径│|├──📁vae//VAE模型存放路径│|└──📁vae_approx│├──📁notebooks│├──📁user//comfyUI用户信息（如配置文件，工作流信息等）│|├──📁default//默认comfyUI用户文件夹│||├─📁workflow//用户保存的workflow文件│||├─📄xxx.json//用户的配置文件│||└──...xxx.json//其它配置文件│|└──...[username]//如果你启用了多用户且存在多用户则会显示对应不同用户的名称│├──📁output//comfyUI图片输出文件夹，当使用类似**saveimage**节点时，生成的图片会存储到这个文件夹│|├──📁checkpoints//如果有使用模型合并节点，和保存合并后的模型相关功能，则合并后的模型会输出到这里│|└──...xxx.png//运行过程中生成的文件会保存到这里│├──extra_model_paths.yaml.example//额外模型文件路径配置文件，如设置此项，请删除**.example**后缀用记事本进行编辑│└──...//其它文件├──config//配置文件夹├──📁Python_embeded//嵌入的python文件├──📁update│├──update.py//用于comfyUI的python脚本│├──update_comfyUI.bat//comfyUI作者推荐使用此批处理命令对ComfyUI进行升级│└──update_comfyui_and_python_dependencies.bat//只有当你的python依赖文件存在问题时才需要运行此批处理命令├──comfyui.log//ComfyUI运行日志文件├──README_VERY_IMPORTANT.txt//README文件，包含了文件使用的方法和说明等等├──run_cpu.bat//批处理文件，当你的显卡为A卡或只有CPU时，双击运行它启动ComfyUI└──run_nvidia_gpu.bat//批处理文件，当你的显卡为N卡(Nvidia)时，双击运行它启动ComfyUI自定义模型位置Windows系统下查找ComfyUI目录下的extra_model_paths.yaml文件建议复制一份备份然后对原始文件进行修改对应原始文件内容示例如下：#ComfyUIextra_model_paths.yamlforwin32comfyui_desktop:is_default:"true"checkpoints:models\checkpoints\classifiers:models\classifiers\clip:models\clip\clip_vision:models\clip_vision\configs:models\configs\controlnet:models\controlnet\diffusers:models\diffusers\diffusion_models:models\diffusion_models\embeddings:models\embeddings\gligen:models\gligen\hypernetworks:models\hypernetworks\loras:models\loras\photomaker:models\photomaker\style_models:models\style_models\unet:models\unet\upscale_models:models\upscale_models\vae:models\vae\vae_approx:models\vae_approx\animatediff_models:models\animatediff_models\animatediff_motion_lora:models\animatediff_motion_lora\animatediff_video_formats:models\animatediff_video_formats\ipadapter:models\ipadapter\liveportrait:models\liveportrait\insightface:models\insightface\layerstyle:models\layerstyle\LLM:models\LLM\Joy_caption:models\Joy_caption\sams:models\sams\blip:models\blip\CogVideo:models\CogVideo\xlabs:models\xlabs\instantid:models\instantid\custom_nodes:custom_nodes/download_model_base:modelsbase_path:D:\ComfyUI如何修改配置下面是对于yaml文件的配置说明：模型类型配置键名（必须，不能修改）默认相对路径（可以修改）是否默认配置（只能有一个）is_defaulttrue/falseCheckpoint模型checkpointsmodels/checkpoints/LoRA模型lorasmodels/loras/VAE模型vaemodels/vae/控制网络controlnetmodels/controlnet/文本编码器clipmodels/clip/图像编码器clip_visionmodels/clip_vision/放大模型upscale_modelsmodels/upscale_models/嵌入模型embeddingsmodels/embeddings/超网络hypernetworksmodels/hypernetworks/风格模型style_modelsmodels/style_models/照片制作器photomakermodels/photomaker/IP适配器ipadaptermodels/ipadapter/动画模型animatediff_modelsmodels/animatediff_models/动画LoRAanimatediff_motion_loramodels/animatediff_motion_lora/分类器classifiersmodels/classifiers/生成模型diffusersmodels/diffusers/自定义节点custom_nodescustom_nodes/下载模型基础download_model_basemodels/基础路径（必须）base_pathD:/ComfyUI💡请注意，对应的表格只是说明可能的模型类型保存文件夹位置，并不是所有的模型文件都要按这个表格的位置来进行保存，对于模型保存位置，你需要参考对应具体的教程或者自定义节点的说明。因为不同自定义节点的作者可能会偏好使用不同的模型存放位置！如果下面是我提供的自定义的模型配置文件示例comfyui_desktop:is_default:"true"checkpoints:models\checkpoints\classifiers:models\classifiers\clip:models\clip\clip_vision:models\clip_vision\configs:models\configs\controlnet:models\controlnet\diffusers:models\diffusers\diffusion_models:models\diffusion_models\embeddings:models\embeddings\gligen:models\gligen\hypernetworks:models\hypernetworks\loras:models\loras\photomaker:models\photomaker\style_models:models\style_models\unet:models\unet\upscale_models:models\upscale_models\vae:models\vae\vae_approx:models\vae_approx\animatediff_models:models\animatediff_models\animatediff_motion_lora:models\animatediff_motion_lora\animatediff_video_formats:models\animatediff_video_formats\ipadapter:models\ipadapter\liveportrait:models\liveportrait\insightface:models\insightface\layerstyle:models\layerstyle\LLM:models\LLM\Joy_caption:models\Joy_caption\sams:models\sams\blip:models\blip\CogVideo:models\CogVideo\xlabs:models\xlabs\instantid:models\instantid\custom_nodes:custom_nodes/download_model_base:modelsbase_path:D:\ComfyUIcustom_models:base_path:E:\checkpoints:models\checkpoints\classifiers:models\classifiers\clip:models\clip\#...其他模型路径...a1111:base_path:D:\stable-diffusion-webuicheckpoints:models/Stable-diffusion#...其他模型路径...base_path这个是在每个配置里面都是有新增自定义的每个配置节点的名称你可以自定义比如custom_models或者a1111等等，但是不能重复，否则会报错is_default这个是用来指定这个配置文件是否是默认的配置文件，只能有一个，如果设置为true，那么这个配置文件就会成为默认的配置文件，否则就会成为非默认的配置文件键名需要与原始的配置文件一致，文件夹为你实际的文件夹请注意custom_nodes这个文件夹是用来存放自定义的插件的路径的这是在原本Portable便携版中没有的，在桌面版中是有的，最好保持默认ComfyUIPortable便携版如何配置与A1111共享绘图模型在对应ComfyUIPortable的安装目录里都可以找到extra_model_paths.yaml.example这个文件，路径如下ComfyUI_windows_portable├──ComfyUI│├──extra_model_paths.yaml.example//此文件为配置文件│└──...省略其它文件└──...省略其它文件找到以上文件后修改文件名extra_model_paths.yaml.example为extra_model_paths.yaml，然后用记事本软件进行编辑对应原始文件内容如下：#Renamethistoextra_model_paths.yamlandComfyUIwillloadit#configfora1111ui#allyouhavetodoischangethebase_pathtowhereyoursisinstalleda111:base_path:path/to/stable-diffusion-webui/checkpoints:models/Stable-diffusionconfigs:models/Stable-diffusionvae:models/VAEloras:|models/Loramodels/LyCORISupscale_models:|models/ESRGANmodels/RealESRGANmodels/SwinIRembeddings:embeddingshypernetworks:models/hypernetworkscontrolnet:models/ControlNet#configforcomfyui#yourbasepathshouldbeeitheranexistingcomfyinstalloracentralfolderwhereyoustoreallofyourmodels,loras,etc.#comfyui:#base_path:path/to/comfyui/#checkpoints:models/checkpoints/#clip:models/clip/#clip_vision:models/clip_vision/#configs:models/configs/#controlnet:models/controlnet/#embeddings:models/embeddings/#loras:models/loras/#upscale_models:models/upscale_models/#vae:models/vae/#other_ui:#base_path:path/to/ui#checkpoints:models/checkpoints#gligen:models/gligen#custom_nodes:path/custom_nodes你可以看到在a111:的设置部分有base_path:用来指定WebUI根目录所在路径，你可以把此处修改为你的WebUI或者自定义的模型文件夹位置所在的路径,记得在:后需要有一个空格，修改完成后保存对应文件，重启ComfyUI即可。你需要确保在base_path:路径之下的文件路径是正确的注意在不同系统比如Mac或者Linux系统下，路径的格式可能有所不同如果假设WebUI安装路径为D:\stable-diffusion-webui\则vae在上方配置文件设置下对应路径vae模型文件路径最终会是应该为D:\stable-diffusion-webui\models\VAE，请检查对应文件夹其它类似checkpoints、loras，也请检查对应的配置。如果你重启以后发现你的checkpoints或者VAE等没有顺利加载，请检查你的配置是否正确。对于其它UI你也可以参考上方配置文件进行修改，比如other_ui:等，你可以取消代码前的#注释来增加对应UI的设置，然后修改base_path:和对应各类绘图模型的路径即可]]></description><category>Ai</category><guid>https://www.beilika.com/article/c168.html</guid><pubDate>Fri, 13 Feb 2026 09:46:32 +0800</pubDate></item><item><title><![CDATA[AI文生图提示词常见错误]]></title><link>https://www.beilika.com/article/c167.html</link><description><![CDATA[很多同学犯的错误往往源于一个核心问题：把AI当成了能读懂你心思的“神笔马良”，而不是一个需要精确指令的“超级画师”。这位“画师”技艺高超，能画出任何风格，但他没有任何主观想法，你说的每一个字都是他下笔的依据。下面，我将新手最容易犯的“六大误区”进行拆解，并提供具体的新手与专家案例对比，全部使用中文语境，让你一看就懂。误区一：描述过于空洞，把AI当“神仙”这是最普遍的问题。新手往往只给出一个模糊的概念，期望AI能猜中自己脑海里的完美画面。新手写法：一幅漂亮的风景画问题分析：“漂亮”是什么标准？哪里是“风景”？是山是海？是白天是黑夜？AI完全无从下手，只能生成一张最大众化、最平庸的风景图，很可能和你想象的完全不一样。专家写法（将想象转化为描述）：一幅印象派油画​，①画风（印象派油画）描绘了瑞士阿尔卑斯山​的日落景象​。②主体（阿尔卑斯山）前景是开满黄色和紫色野花​的草地，远处是白雪皑皑的山峰​，夕阳的余晖将天空染成温暖的橙色和粉色​，（色彩）光线柔和，笔触明显。（光线）对比：​专家写法明确了①画风（印象派油画）、②主体（阿尔卑斯山）、③时间/氛围（日落）、④具体元素（野花、雪山）、⑤色彩与光线（橙粉色、柔和）。AI有了这些具体的“建材”，才能搭建出你想要的“宫殿”。误区二：滥用主观词，而非客观描述新手喜欢用“酷炫”、“震撼”、“有感觉”这类词，但AI无法理解这些主观情感。你需要把“感觉”翻译成“画面”。新手写法：一个非常酷的机器人问题分析：“酷”的定义千差万别。是苹果公司那种简洁的酷？还是《赛博朋克2077》里那种杂乱的酷？AI只能随便猜一个。专家写法（把“酷”拆解成具体特征）：一个未来主义​的战斗机器人，全身覆盖着哑光黑色​的装甲，关节处有轻微的磨损和划痕​，蓝色的LED光​从眼部和胸甲缝隙中透出，造型参考了日本机甲（Mecha）​风格，背景是废弃的工业城市对比：​专家写法将“酷”这个感觉，翻译成了材质（哑光黑）、细节（磨损划痕）、光效（蓝色LED）、风格参考（日本机甲）、环境（废弃城市）​等一系列客观的视觉元素。误区三：只说“画什么”，不说“怎么画”新手往往只关注画面里的“内容（Noun）”，而忽略了决定画面最终呈现形式的“媒介”和“风格”。新手写法：一只猫问题分析：是一张照片？一幅油画？一幅素描？还是一个3D模型？AI生成的默认风格可能不是你想要的。专家写法（明确艺术形式和风格）：水彩画风格，一只可爱的橘猫​蜷缩在窗台上​打盹，午后的阳光​透过窗户洒在它身上，毛茸茸的质感清晰可见，色彩明亮温暖​，画面干净。（或者换一种风格）工作室专业摄影，一只神情严肃的布偶猫​，特写镜头​对准它的蓝色眼睛​，背景是纯黑色的，布光均匀​，白色毛发细节根根分明，超高清画质​。对比：​专家写法加入了“水彩画风格”​或“工作室专业摄影”​这样的关键词，直接决定了图片的“基因”。误区四：缺乏构图和镜头感新手常常忘记自己是“导演”，没有告诉AI“摄像机”应该放在哪里，导致画面主体不突出，或者视角很奇怪。新手写法：一个站在森林里的女孩问题分析：女孩在画面中间还是旁边？是远景还是近景？是正面还是背面？AI随机生成的构图可能很平淡。专家写法（运用镜头语言）：全身像，一个穿着红色斗篷​的小女孩，从背后拍摄的视角​，她独自站在一片幽深的、长满苔藓的魔法森林​中。广角镜头​，阳光从茂密的树冠缝隙中投下几缕光束（耶稣光）​，营造出神秘的氛围。对比：​专家写法使用了“全身像”、“从背后拍摄”、“广角镜头”​等摄影术语，像导演一样指导AI如何构图和取景，画面立即有了故事感和视觉冲击力。误区五：元素堆砌，主次不分新手有时会贪心，想把所有喜欢的东西都放进一张图里，结果AI无法理解核心主体，生成一张混乱的“大杂烩”。新手写法：一个宇航员在火星上骑着马，旁边有一只狗，天上还有飞碟和地球，他正在喝咖啡。问题分析：元素太多，AI会感到困惑。谁是主角？马和狗的关系是什么？画面重点是什么？结果很可能是各个元素都画得不好，或者互相扭曲。专家写法（做减法，突出核心故事）：一张富有故事感的画面：一个宇航员孤独地站在火星的红色沙漠​上，他正在抚摸着他的同伴——一只穿着定制宇航服的金色寻回犬​。远处是火星荒凉的地平线，地球像一颗蓝色的星星​挂在漆黑的天空中。画面构图简洁​，突出孤独和伙伴情谊​的主题。对比：​专家懂得取舍，将核心故事（宇航员和他的狗在火星）提炼出来，并围绕这个核心添加细节，使得画面主题鲜明，情感饱满。误区六：忘记使用“反向提示词”（NegativePrompts）这是一个新手常常忽略，但极为重要的功能。它告诉AI“不要画什么”，可以有效避免画面出现奇怪的错误。新手做法：正向提示词：​高质量照片，一个美女。结果：​可能会出现6根手指、奇怪的肢体、模糊的背景、低质量的图像等。专家做法：正向提示词：​超高清人像摄影，一个优雅的亚洲女性，留着黑色长发，穿着简约的白色连衣裙，背景是模糊的城市夜景，灯光形成美丽的光斑（焦外成像），电影感。反向提示词：​畸形，多余的手指，丑陋，模糊，画质差，水印，签名，文字，身体部位融合，解剖不准确。对比：​专家通过反向提示词，像“清道夫”一样排除了大部分AI容易犯的错误，大大提高了出图的成功率和质量。总结给新手的核心建议：从“感觉”到“描述”：把你脑中的“美”、“酷”翻译成具体的形状、颜色、材质和光线。当个“导演”：想好你的“摄像机”放在哪，是特写还是远景，是仰视还是俯视。指定“风格”：先决定是照片、油画、水彩还是动漫，这是画面的基调。学会“排除”：大胆使用反向提示词，告诉AI你不想要什么。记住，写提示词的过程，就是你将脑海中的模糊想象，一步步翻译成AI能听懂的、精确的“施工图纸”的过程。多加练习，你也能成为一名出色的“AI画师指挥家”！]]></description><category>Ai</category><guid>https://www.beilika.com/article/c167.html</guid><pubDate>Fri, 13 Feb 2026 09:24:27 +0800</pubDate></item><item><title><![CDATA[更改Windows远程桌面的侦听端口]]></title><link>https://www.beilika.com/article/c166.html</link><description><![CDATA[更改计算机上的远程桌面的侦听端口适用于:✅WindowsServer2025,✅WindowsServer2022,✅WindowsServer2019,✅WindowsServer2016,✅Windows11,✅Windows10远程桌面允许通过远程桌面协议(RDP)远程连接到运行Windows或WindowsServer的计算机，并且默认情况下侦听端口3389。出于安全或配置目的，你可能需要更改此侦听端口。本文提供使用PowerShell或注册表编辑器修改侦听端口的分步说明。Prerequisites在开始之前，请确保具有以下项：要连接到的计算机的管理员访问权限或等效权限。启用了远程桌面的计算机。有关详细信息，请参阅启用远程桌面。用于测试更改的客户端，例如远程桌面连接（mstsc.exe）或Windows应用。配置远程桌面侦听端口远程桌面的侦听端口在注册表中指定。若要更改注册表值，请按照以下方法使用PowerShell进行更改。请执行以下步骤：1.以管理员身份打开PowerShell。2.通过运行以下PowerShell命令检查当前端口：Get-ItemProperty-Path'HKLM:\SYSTEM\CurrentControlSet\Control\TerminalServer\WinStations\RDP-Tcp'-name'PortNumber'3.输出类似于以下示例：PortNumber:3389PSPath:Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TerminalServer\WinStations\RDP-TcpPSParentPath:Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TerminalServer\WinStationsPSChildName:RDP-TcpPSDrive:HKLMPSProvider:Microsoft.PowerShell.Core\Registry4.运行以下PowerShell命令来更改此端口。请务必将替换为新的端口号。$portValue=''Set-ItemProperty-Path'HKLM:\SYSTEM\CurrentControlSet\Control\TerminalServer\WinStations\RDP-Tcp'-name'PortNumber'-Value$portValue将新端口添加到Windows防火墙中（如果是FRP内网穿透，则无需此步骤）如果使用Windows防火墙，则需要添加新的入站规则以允许新端口上的流量。有关可用于配置Windows防火墙的其他方法的详细信息，请参阅Windows防火墙工具。如果使用任何其他防火墙，请确保你或管理员允许连接到新端口号。若要创建新的Windows防火墙规则以允许新端口，请以管理员身份运行以下PowerShell命令。请务必将替换为新的端口号。这里只需创建入站规则。$portValue=''New-NetFirewallRule-DisplayName'RDPPORTLatest-TCP-In'-ProfilePublic-DirectionInbound-ActionAllow-ProtocolTCP-LocalPort$portValueNew-NetFirewallRule-DisplayName'RDPPORTLatest-UDP-In'-ProfilePublic-DirectionInbound-ActionAllow-ProtocolUDP-LocalPort$portValue测试新的远程桌面侦听端口下次使用远程桌面连接或其他客户端连接到此计算机时，请输入主机名和新端口。例如，如果将端口更改为在计算机pc1.contoso.com上使用3390，则地址为pc1.contoso.com:3390。]]></description><category>Windows</category><guid>https://www.beilika.com/article/c166.html</guid><pubDate>Tue, 10 Feb 2026 15:14:44 +0800</pubDate></item><item><title><![CDATA[Windows守护进程工具--Nssm详解（将exe注册为windows服务）]]></title><link>https://www.beilika.com/article/c165.html</link><description><![CDATA[一、nssm简介nssm是一个服务封装程序，它可以将普通exe程序封装成服务，实现开机自启动，同类型的工具还有微软自己的srvany，不过nssm更加简单易用，并且功能强大。它的特点如下：支持普通exe程序（控制台程序或者带界面的Windows程序都可以）安装简单，修改方便可以自动守护封装了的服务，程序挂掉了后可以自动重启官网地址：https://nssm.cc/二、nssm配置详解1、下载地址官网地址：https://nssm.cc/download下载链接（本文下方附件里也有）：https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip2、解压压缩包，根据系统位数选择64或32位程序3、打开命令行，运行nssm程序管理员权限打开命令行工具，切换到nssm.exe所在路径，运行nssminstall，打开程序配置界面配置项说明：Path：运行应用程序的程序Startupdirectory：应用程序所在的目录Arguments：应用运行的参数Servicename：生成服务的名称最后点击installservice完成windows服务安装，在windows服务列表就能看到创建的服务了。4、常用命令nssminstallservername//创建servername服务，弹出配置界面nssmstartservername//启动服务nssmstopservername//暂停服务nssmrestartservername//重新启动服务nssmremoveservername//删除创建的servername服务nssmeditservername//更改servername服务，弹出修改界面nssmsetservername参数名参数值//设置服务参数值scdeleteservername//windows删除服务命令直接使用windows的服务管理也可以实现服务的操作，服务右键属性-恢复即可设置服务挂掉重启等内容。三、实战：将应用做成服务1、程序说明这里演示将net6的web项目制作成windows服务应用的启动命令是：dotnetWebApplication_nssm.dll--urls=http://*:8888/--port=88882、安装服务nssminstall需要填写的内容如下（Arguments的配置，可以直接跟-c等参数，比如当安装frp服务时，Arguments的配置可为-cfrpc.toml）：Path：C:\ProgramFiles\dotnet\dotnet.exeStartupdirectory：D:\TechLearn\001、Tools--nssm\WebApplication_nssm\bin\Debug\net6.0Arguments：WebApplication_nssm.dll--urls=http://*:8888/--port=8888Servicename：webapp8888最后点击installservice完成windows服务安装，在windows服务列表就能看到创建的服务了。3、管理服务（1）启动服务D:\nssm-2.24\win64>nssmstartwebapp8888webapp8888:START:操作成功完成。（2）关闭服务D:\nssm-2.24\win64>nssmstopwebapp8888webapp8888:STOP:操作成功完成。（3）重启服务如果服务未启动就会直接启动，启动的会重启。D:\nssm-2.24\win64>nssmrestartwebapp8888webapp8888:STOP:服务尚未启动。webapp8888:START:操作成功完成。D:\nssm-2.24\win64>nssmrestartwebapp8888webapp8888:STOP:操作成功完成。webapp8888:START:操作成功完成。（4）修改服务输入命令，修改配置，不能修改服务名D:\nssm-2.24\win64>nssmeditwebapp8888（5）删除服务删除有二次确认，删除之后服务就找不到了，需要重新安装D:\nssm-2.24\win64>nssmremovewebapp8888Service"webapp8888"removedsuccessfully!]]></description><category>Windows</category><guid>https://www.beilika.com/article/c165.html</guid><pubDate>Tue, 10 Feb 2026 15:00:56 +0800</pubDate></item><item><title><![CDATA[ES6之async函数]]></title><link>https://www.beilika.com/article/c164.html</link><description><![CDATA[含义ES2017标准引入了async函数，使得异步操作变得更加方便。async函数是什么？一句话，它就是Generator函数的语法糖。前文有一个Generator函数，依次读取两个文件。constfs=require('fs');constreadFile=function(fileName){returnnewPromise(function(resolve,reject){fs.readFile(fileName,function(error,data){if(error)returnreject(error);resolve(data);});});};constgen=function*(){constf1=yieldreadFile('/etc/fstab');constf2=yieldreadFile('/etc/shells');console.log(f1.toString());console.log(f2.toString());};上面代码的函数gen可以写成async函数，就是下面这样。constasyncReadFile=asyncfunction(){constf1=awaitreadFile('/etc/fstab');constf2=awaitreadFile('/etc/shells');console.log(f1.toString());console.log(f2.toString());};一比较就会发现，async函数就是将Generator函数的星号（*）替换成async，将yield替换成await，仅此而已。async函数对Generator函数的改进，体现在以下四点。（1）内置执行器。Generator函数的执行必须靠执行器，所以才有了co模块，而async函数自带执行器。也就是说，async函数的执行，与普通函数一模一样，只要一行。asyncReadFile();上面的代码调用了asyncReadFile函数，然后它就会自动执行，输出最后结果。这完全不像Generator函数，需要调用next方法，或者用co模块，才能真正执行，得到最后结果。（2）更好的语义。async和await，比起星号和yield，语义更清楚了。async表示函数里有异步操作，await表示紧跟在后面的表达式需要等待结果。（3）更广的适用性。co模块约定，yield命令后面只能是Thunk函数或Promise对象，而async函数的await命令后面，可以是Promise对象和原始类型的值（数值、字符串和布尔值，但这时会自动转成立即resolved的Promise对象）。（4）返回值是Promise。async函数的返回值是Promise对象，这比Generator函数的返回值是Iterator对象方便多了。你可以用then方法指定下一步的操作。进一步说，async函数完全可以看作多个异步操作，包装成的一个Promise对象，而await命令就是内部then命令的语法糖。基本用法async函数返回一个Promise对象，可以使用then方法添加回调函数。当函数执行的时候，一旦遇到await就会先返回，等到异步操作完成，再接着执行函数体内后面的语句。下面是一个例子。asyncfunctiongetStockPriceByName(name){constsymbol=awaitgetStockSymbol(name);conststockPrice=awaitgetStockPrice(symbol);returnstockPrice;}getStockPriceByName('goog').then(function(result){console.log(result);});上面代码是一个获取股票报价的函数，函数前面的async关键字，表明该函数内部有异步操作。调用该函数时，会立即返回一个Promise对象。下面是另一个例子，指定多少毫秒后输出一个值。functiontimeout(ms){returnnewPromise((resolve)=>{setTimeout(resolve,ms);});}asyncfunctionasyncPrint(value,ms){awaittimeout(ms);console.log(value);}asyncPrint('helloworld',50);上面代码指定50毫秒以后，输出helloworld。由于async函数返回的是Promise对象，可以作为await命令的参数。所以，上面的例子也可以写成下面的形式。asyncfunctiontimeout(ms){awaitnewPromise((resolve)=>{setTimeout(resolve,ms);});}asyncfunctionasyncPrint(value,ms){awaittimeout(ms);console.log(value);}asyncPrint('helloworld',50);async函数有多种使用形式。//函数声明asyncfunctionfoo(){}//函数表达式constfoo=asyncfunction(){};//对象的方法letobj={asyncfoo(){}};obj.foo().then(...)//Class的方法classStorage{constructor(){this.cachePromise=caches.open('avatars');}asyncgetAvatar(name){constcache=awaitthis.cachePromise;returncache.match(`/avatars/${name}.jpg`);}}conststorage=newStorage();storage.getAvatar('jake').then(…);//箭头函数constfoo=async()=>{};语法async函数的语法规则总体上比较简单，难点是错误处理机制。返回Promise对象async函数返回一个Promise对象。async函数内部return语句返回的值，会成为then方法回调函数的参数。asyncfunctionf(){return'helloworld';}f().then(v=>console.log(v))//"helloworld"上面代码中，函数f内部return命令返回的值，会被then方法回调函数接收到。async函数内部抛出错误，会导致返回的Promise对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。asyncfunctionf(){thrownewError('出错了');}f().then(v=>console.log(v),e=>console.log(e))//Error:出错了Promise对象的状态变化async函数返回的Promise对象，必须等到内部所有await命令后面的Promise对象执行完，才会发生状态改变，除非遇到return语句或者抛出错误。也就是说，只有async函数内部的异步操作执行完，才会执行then方法指定的回调函数。下面是一个例子。asyncfunctiongetTitle(url){letresponse=awaitfetch(url);lethtml=awaitresponse.text();returnhtml.match(/([\s\S]+)/i)[1];}getTitle('https://tc39.github.io/ecma262/').then(console.log)//"ECMAScript2017LanguageSpecification"上面代码中，函数getTitle内部有三个操作：抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成，才会执行then方法里面的console.log。await命令正常情况下，await命令后面是一个Promise对象，返回该对象的结果。如果不是Promise对象，就直接返回对应的值。asyncfunctionf(){//等同于//return123;returnawait123;}f().then(v=>console.log(v))//123上面代码中，await命令的参数是数值123，这时等同于return123。另一种情况是，await命令后面是一个thenable对象（即定义了then方法的对象），那么await会将其等同于Promise对象。classSleep{constructor(timeout){this.timeout=timeout;}then(resolve,reject){conststartTime=Date.now();setTimeout(()=>resolve(Date.now()-startTime),this.timeout);}}(async()=>{constsleepTime=awaitnewSleep(1000);console.log(sleepTime);})();//1000上面代码中，await命令后面是一个Sleep对象的实例。这个实例不是Promise对象，但是因为定义了then方法，await会将其视为Promise处理。这个例子还演示了如何实现休眠效果。JavaScript一直没有休眠的语法，但是借助await命令就可以让程序停顿指定的时间。下面给出了一个简化的sleep实现。functionsleep(interval){returnnewPromise(resolve=>{setTimeout(resolve,interval);})}//用法asyncfunctionone2FiveInAsync(){for(leti=1;iconsole.log(v)).catch(e=>console.log(e))//出错了注意，上面代码中，await语句前面没有return，但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return，效果是一样的。任何一个await语句后面的Promise对象变为reject状态，那么整个async函数都会中断执行。asyncfunctionf(){awaitPromise.reject('出错了');awaitPromise.resolve('helloworld');//不会执行}上面代码中，第二个await语句是不会执行的，因为第一个await语句状态变成了reject。有时，我们希望即使前一个异步操作失败，也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面，这样不管这个异步操作是否成功，第二个await都会执行。asyncfunctionf(){try{awaitPromise.reject('出错了');}catch(e){}returnawaitPromise.resolve('helloworld');}f().then(v=>console.log(v))//helloworld另一种方法是await后面的Promise对象再跟一个catch方法，处理前面可能出现的错误。asyncfunctionf(){awaitPromise.reject('出错了').catch(e=>console.log(e));returnawaitPromise.resolve('helloworld');}f().then(v=>console.log(v))//出错了//helloworld错误处理如果await后面的异步操作出错，那么等同于async函数返回的Promise对象被reject。asyncfunctionf(){awaitnewPromise(function(resolve,reject){thrownewError('出错了');});}f().then(v=>console.log(v)).catch(e=>console.log(e))//Error：出错了上面代码中，async函数f执行后，await后面的Promise对象会抛出一个错误对象，导致catch方法的回调函数被调用，它的参数就是抛出的错误对象。具体的执行机制，可以参考后文的“async函数的实现原理”。防止出错的方法，也是将其放在try...catch代码块之中。asyncfunctionf(){try{awaitnewPromise(function(resolve,reject){thrownewError('出错了');});}catch(e){}returnawait('helloworld');}如果有多个await命令，可以统一放在try...catch结构中。asyncfunctionmain(){try{constval1=awaitfirstStep();constval2=awaitsecondStep(val1);constval3=awaitthirdStep(val1,val2);console.log('Final:',val3);}catch(err){console.error(err);}}下面的例子使用try...catch结构，实现多次重复尝试。constsuperagent=require('superagent');constNUM_RETRIES=3;asyncfunctiontest(){leti;for(i=0;i{await_;awaitdb.post(doc);},undefined);}上面例子中，reduce方法的第一个参数是async函数，导致该函数的第一个参数是前一步操作返回的Promise对象，所以必须使用await等待它操作结束。另外，reduce方法返回的是docs数组最后一个成员的async函数的执行结果，也是一个Promise对象，导致在它前面也必须加上await。如果确实希望多个请求并发执行，可以使用Promise.all方法。当三个请求都会resolved时，下面两种写法效果相同。asyncfunctiondbFuc(db){letdocs=[{},{},{}];letpromises=docs.map((doc)=>db.post(doc));letresults=awaitPromise.all(promises);console.log(results);}//或者使用下面的写法asyncfunctiondbFuc(db){letdocs=[{},{},{}];letpromises=docs.map((doc)=>db.post(doc));letresults=[];for(letpromiseofpromises){results.push(awaitpromise);}console.log(results);}第四点，async函数可以保留运行堆栈。consta=()=>{b().then(()=>c());};上面代码中，函数a内部运行了一个异步任务b()。当b()运行的时候，函数a()不会中断，而是继续执行。等到b()运行结束，可能a()早就运行结束了，b()所在的上下文环境已经消失了。如果b()或c()报错，错误堆栈将不包括a()。现在将这个例子改成async函数。consta=async()=>{awaitb();c();};上面代码中，b()运行的时候，a()是暂停执行，上下文环境都保存着。一旦b()或c()报错，错误堆栈将包括a()。async函数的实现原理async函数的实现原理，就是将Generator函数和自动执行器，包装在一个函数里。asyncfunctionfn(args){//...}//等同于functionfn(args){returnspawn(function*(){//...});}所有的async函数都可以写成上面的第二种形式，其中的spawn函数就是自动执行器。下面给出spawn函数的实现，基本就是前文自动执行器的翻版。functionspawn(genF){returnnewPromise(function(resolve,reject){constgen=genF();functionstep(nextF){letnext;try{next=nextF();}catch(e){returnreject(e);}if(next.done){returnresolve(next.value);}Promise.resolve(next.value).then(function(v){step(function(){returngen.next(v);});},function(e){step(function(){returngen.throw(e);});});}step(function(){returngen.next(undefined);});});}与其他异步处理方法的比较我们通过一个例子，来看async函数与Promise、Generator函数的比较。假定某个DOM元素上面，部署了一系列的动画，前一个动画结束，才能开始后一个。如果当中有一个动画出错，就不再往下执行，返回上一个成功执行的动画的返回值。首先是Promise的写法。functionchainAnimationsPromise(elem,animations){//变量ret用来保存上一个动画的返回值letret=null;//新建一个空的Promiseletp=Promise.resolve();//使用then方法，添加所有动画for(letanimofanimations){p=p.then(function(val){ret=val;returnanim(elem);});}//返回一个部署了错误捕捉机制的Promisereturnp.catch(function(e){/*忽略错误，继续执行*/}).then(function(){returnret;});}虽然Promise的写法比回调函数的写法大大改进，但是一眼看上去，代码完全都是Promise的API（then、catch等等），操作本身的语义反而不容易看出来。接着是Generator函数的写法。functionchainAnimationsGenerator(elem,animations){returnspawn(function*(){letret=null;try{for(letanimofanimations){ret=yieldanim(elem);}}catch(e){/*忽略错误，继续执行*/}returnret;});}上面代码使用Generator函数遍历了每个动画，语义比Promise写法更清晰，用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于，必须有一个任务运行器，自动执行Generator函数，上面代码的spawn函数就是自动执行器，它返回一个Promise对象，而且必须保证yield语句后面的表达式，必须返回一个Promise。最后是async函数的写法。asyncfunctionchainAnimationsAsync(elem,animations){letret=null;try{for(letanimofanimations){ret=awaitanim(elem);}}catch(e){/*忽略错误，继续执行*/}returnret;}可以看到Async函数的实现最简洁，最符合语义，几乎没有语义不相关的代码。它将Generator写法中的自动执行器，改在语言层面提供，不暴露给用户，因此代码量最少。如果使用Generator写法，自动执行器需要用户自己提供。实例：按顺序完成异步操作实际开发中，经常遇到一组异步操作，需要按照顺序完成。比如，依次远程读取一组URL，然后按照读取的顺序输出结果。Promise的写法如下。functionlogInOrder(urls){//远程读取所有URLconsttextPromises=urls.map(url=>{returnfetch(url).then(response=>response.text());});//按次序输出textPromises.reduce((chain,textPromise)=>{returnchain.then(()=>textPromise).then(text=>console.log(text));},Promise.resolve());}上面代码使用fetch方法，同时远程读取一组URL。每个fetch操作都返回一个Promise对象，放入textPromises数组。然后，reduce方法依次处理每个Promise对象，然后使用then，将所有Promise对象连起来，因此就可以依次输出结果。这种写法不太直观，可读性比较差。下面是async函数实现。asyncfunctionlogInOrder(urls){for(consturlofurls){constresponse=awaitfetch(url);console.log(awaitresponse.text());}}上面代码确实大大简化，问题是所有远程操作都是继发。只有前一个URL返回结果，才会去读取下一个URL，这样做效率很差，非常浪费时间。我们需要的是并发发出远程请求。asyncfunctionlogInOrder(urls){//并发读取远程URLconsttextPromises=urls.map(asyncurl=>{constresponse=awaitfetch(url);returnresponse.text();});//按次序输出for(consttextPromiseoftextPromises){console.log(awaittextPromise);}}上面代码中，虽然map方法的参数是async函数，但它是并发执行的，因为只有async函数内部是继发执行，外部不受影响。后面的for..of循环内部使用了await，因此实现了按顺序输出。]]></description><category>JS</category><guid>https://www.beilika.com/article/c164.html</guid><pubDate>Fri, 14 Nov 2025 09:48:17 +0800</pubDate></item><item><title><![CDATA[ES6之Generator函数的异步应用]]></title><link>https://www.beilika.com/article/c163.html</link><description><![CDATA[异步编程对JavaScript语言太重要。JavaScript语言的执行环境是“单线程”的，如果没有异步编程，根本没法用，非卡死不可。本章主要介绍Generator函数如何完成异步操作。传统方法ES6诞生以前，异步编程的方法，大概有下面四种。回调函数事件监听发布/订阅Promise对象Generator函数将JavaScript异步编程带入了一个全新的阶段。基本概念异步所谓”异步”，简单说就是一个任务不是连续完成的，可以理解成该任务被人为分成两段，先执行第一段，然后转而执行其他任务，等做好了准备，再回过头执行第二段。比如，有一个任务是读取文件进行处理，任务的第一段是向操作系统发出请求，要求读取文件。然后，程序执行其他任务，等到操作系统返回文件，再接着执行任务的第二段（处理文件）。这种不连续的执行，就叫做异步。相应地，连续的执行就叫做同步。由于是连续执行，不能插入其他任务，所以操作系统从硬盘读取文件的这段时间，程序只能干等着。回调函数JavaScript语言对异步编程的实现，就是回调函数。所谓回调函数，就是把任务的第二段单独写在一个函数里面，等到重新执行这个任务的时候，就直接调用这个函数。回调函数的英语名字callback，直译过来就是”重新调用”。读取文件进行处理，是这样写的。fs.readFile('/etc/passwd','utf-8',function(err,data){if(err)throwerr;console.log(data);});上面代码中，readFile函数的第三个参数，就是回调函数，也就是任务的第二段。等到操作系统返回了/etc/passwd这个文件以后，回调函数才会执行。一个有趣的问题是，为什么Node约定，回调函数的第一个参数，必须是错误对象err（如果没有错误，该参数就是null）？原因是执行分成两段，第一段执行完以后，任务所在的上下文环境就已经结束了。在这以后抛出的错误，原来的上下文环境已经无法捕捉，只能当作参数，传入第二段。Promise回调函数本身并没有问题，它的问题出现在多个回调函数嵌套。假定读取A文件之后，再读取B文件，代码如下。fs.readFile(fileA,'utf-8',function(err,data){fs.readFile(fileB,'utf-8',function(err,data){//...});});不难想象，如果依次读取两个以上的文件，就会出现多重嵌套。代码不是纵向发展，而是横向发展，很快就会乱成一团，无法管理。因为多个异步操作形成了强耦合，只要有一个操作需要修改，它的上层回调函数和下层回调函数，可能都要跟着修改。这种情况就称为”回调函数地狱”（callbackhell）。Promise对象就是为了解决这个问题而提出的。它不是新的语法功能，而是一种新的写法，允许将回调函数的嵌套，改成链式调用。采用Promise，连续读取多个文件，写法如下。varreadFile=require('fs-readfile-promise');readFile(fileA).then(function(data){console.log(data.toString());}).then(function(){returnreadFile(fileB);}).then(function(data){console.log(data.toString());}).catch(function(err){console.log(err);});上面代码中，我使用了fs-readfile-promise模块，它的作用就是返回一个Promise版本的readFile函数。Promise提供then方法加载回调函数，catch方法捕捉执行过程中抛出的错误。可以看到，Promise的写法只是回调函数的改进，使用then方法以后，异步任务的两段执行看得更清楚了，除此以外，并无新意。Promise的最大问题是代码冗余，原来的任务被Promise包装了一下，不管什么操作，一眼看去都是一堆then，原来的语义变得很不清楚。那么，有没有更好的写法呢？Generator函数协程传统的编程语言，早有异步编程的解决方案（其实是多任务的解决方案）。其中有一种叫做”协程”（coroutine），意思是多个线程互相协作，完成异步任务。协程有点像函数，又有点像线程。它的运行流程大致如下。第一步，协程A开始执行。第二步，协程A执行到一半，进入暂停，执行权转移到协程B。第三步，（一段时间后）协程B交还执行权。第四步，协程A恢复执行。上面流程的协程A，就是异步任务，因为它分成两段（或多段）执行。举例来说，读取文件的协程写法如下。function*asyncJob(){//...其他代码varf=yieldreadFile(fileA);//...其他代码}上面代码的函数asyncJob是一个协程，它的奥妙就在其中的yield命令。它表示执行到此处，执行权将交给其他协程。也就是说，yield命令是异步两个阶段的分界线。协程遇到yield命令就暂停，等到执行权返回，再从暂停的地方继续往后执行。它的最大优点，就是代码的写法非常像同步操作，如果去除yield命令，简直一模一样。协程的Generator函数实现Generator函数是协程在ES6的实现，最大特点就是可以交出函数的执行权（即暂停执行）。整个Generator函数就是一个封装的异步任务，或者说是异步任务的容器。异步操作需要暂停的地方，都用yield语句注明。Generator函数的执行方法如下。function*gen(x){vary=yieldx+2;returny;}varg=gen(1);g.next()//{value:3,done:false}g.next()//{value:undefined,done:true}上面代码中，调用Generator函数，会返回一个内部指针（即遍历器）g。这是Generator函数不同于普通函数的另一个地方，即执行它不会返回结果，返回的是指针对象。调用指针g的next方法，会移动内部指针（即执行异步任务的第一段），指向第一个遇到的yield语句，上例是执行到x+2为止。换言之，next方法的作用是分阶段执行Generator函数。每次调用next方法，会返回一个对象，表示当前阶段的信息（value属性和done属性）。value属性是yield语句后面表达式的值，表示当前阶段的值；done属性是一个布尔值，表示Generator函数是否执行完毕，即是否还有下一个阶段。Generator函数的数据交换和错误处理Generator函数可以暂停执行和恢复执行，这是它能封装异步任务的根本原因。除此之外，它还有两个特性，使它可以作为异步编程的完整解决方案：函数体内外的数据交换和错误处理机制。next返回值的value属性，是Generator函数向外输出数据；next方法还可以接受参数，向Generator函数体内输入数据。function*gen(x){vary=yieldx+2;returny;}varg=gen(1);g.next()//{value:3,done:false}g.next(2)//{value:2,done:true}上面代码中，第一个next方法的value属性，返回表达式x+2的值3。第二个next方法带有参数2，这个参数可以传入Generator函数，作为上个阶段异步任务的返回结果，被函数体内的变量y接收。因此，这一步的value属性，返回的就是2（变量y的值）。Generator函数内部还可以部署错误处理代码，捕获函数体外抛出的错误。function*gen(x){try{vary=yieldx+2;}catch(e){console.log(e);}returny;}varg=gen(1);g.next();g.throw('出错了');//出错了上面代码的最后一行，Generator函数体外，使用指针对象的throw方法抛出的错误，可以被函数体内的try...catch代码块捕获。这意味着，出错的代码与处理错误的代码，实现了时间和空间上的分离，这对于异步编程无疑是很重要的。异步任务的封装下面看看如何使用Generator函数，执行一个真实的异步任务。varfetch=require('node-fetch');function*gen(){varurl='https://api.github.com/users/github';varresult=yieldfetch(url);console.log(result.bio);}上面代码中，Generator函数封装了一个异步操作，该操作先读取一个远程接口，然后从JSON格式的数据解析信息。就像前面说过的，这段代码非常像同步操作，除了加上了yield命令。执行这段代码的方法如下。varg=gen();varresult=g.next();result.value.then(function(data){returndata.json();}).then(function(data){g.next(data);});上面代码中，首先执行Generator函数，获取遍历器对象，然后使用next方法（第二行），执行异步任务的第一阶段。由于Fetch模块返回的是一个Promise对象，因此要用then方法调用下一个next方法。可以看到，虽然Generator函数将异步操作表示得很简洁，但是流程管理却不方便（即何时执行第一阶段、何时执行第二阶段）。Thunk函数Thunk函数是自动执行Generator函数的一种方法。参数的求值策略Thunk函数早在上个世纪60年代就诞生了。那时，编程语言刚刚起步，计算机学家还在研究，编译器怎么写比较好。一个争论的焦点是”求值策略”，即函数的参数到底应该何时求值。varx=1;functionf(m){returnm*2;}f(x+5)上面代码先定义函数f，然后向它传入表达式x+5。请问，这个表达式应该何时求值？一种意见是”传值调用”（callbyvalue），即在进入函数体之前，就计算x+5的值（等于6），再将这个值传入函数f。C语言就采用这种策略。f(x+5)//传值调用时，等同于f(6)另一种意见是“传名调用”（callbyname），即直接将表达式x+5传入函数体，只在用到它的时候求值。Haskell语言采用这种策略。f(x+5)//传名调用时，等同于(x+5)*2传值调用和传名调用，哪一种比较好？回答是各有利弊。传值调用比较简单，但是对参数求值的时候，实际上还没用到这个参数，有可能造成性能损失。functionf(a,b){returnb;}f(3*x*x-2*x-1,x);上面代码中，函数f的第一个参数是一个复杂的表达式，但是函数体内根本没用到。对这个参数求值，实际上是不必要的。因此，有一些计算机学家倾向于”传名调用”，即只在执行时求值。Thunk函数的含义编译器的“传名调用”实现，往往是将参数放到一个临时函数之中，再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。functionf(m){returnm*2;}f(x+5);//等同于varthunk=function(){returnx+5;};functionf(thunk){returnthunk()*2;}上面代码中，函数f的参数x+5被一个函数替换了。凡是用到原参数的地方，对Thunk函数求值即可。这就是Thunk函数的定义，它是“传名调用”的一种实现策略，用来替换某个表达式。JavaScript语言的Thunk函数JavaScript语言是传值调用，它的Thunk函数含义有所不同。在JavaScript语言中，Thunk函数替换的不是表达式，而是多参数函数，将其替换成一个只接受回调函数作为参数的单参数函数。//正常版本的readFile（多参数版本）fs.readFile(fileName,callback);//Thunk版本的readFile（单参数版本）varThunk=function(fileName){returnfunction(callback){returnfs.readFile(fileName,callback);};};varreadFileThunk=Thunk(fileName);readFileThunk(callback);上面代码中，fs模块的readFile方法是一个多参数函数，两个参数分别为文件名和回调函数。经过转换器处理，它变成了一个单参数函数，只接受回调函数作为参数。这个单参数版本，就叫做Thunk函数。任何函数，只要参数有回调函数，就能写成Thunk函数的形式。下面是一个简单的Thunk函数转换器。//ES5版本varThunk=function(fn){returnfunction(){varargs=Array.prototype.slice.call(arguments);returnfunction(callback){args.push(callback);returnfn.apply(this,args);}};};//ES6版本constThunk=function(fn){returnfunction(...args){returnfunction(callback){returnfn.call(this,...args,callback);}};};使用上面的转换器，生成fs.readFile的Thunk函数。varreadFileThunk=Thunk(fs.readFile);readFileThunk(fileA)(callback);下面是另一个完整的例子。functionf(a,cb){cb(a);}constft=Thunk(f);ft(1)(console.log)//1Thunkify模块生产环境的转换器，建议使用Thunkify模块。首先是安装。$npminstallthunkify使用方式如下。varthunkify=require('thunkify');varfs=require('fs');varread=thunkify(fs.readFile);read('package.json')(function(err,str){//...});Thunkify的源码与上一节那个简单的转换器非常像。functionthunkify(fn){returnfunction(){varargs=newArray(arguments.length);varctx=this;for(vari=0;istream.once('data',resolve)),newPromise(resolve=>stream.once('end',resolve)),newPromise((resolve,reject)=>stream.once('error',reject))]);if(!res){break;}stream.removeAllListeners('data');stream.removeAllListeners('end');stream.removeAllListeners('error');valjeanCount+=(res.toString().match(/valjean/ig)||[]).length;}console.log('count:',valjeanCount);//count:1120});上面代码采用Stream模式读取《悲惨世界》的文本文件，对于每个数据块都使用stream.once方法，在data、end、error三个事件上添加一次性回调函数。变量res只有在data事件发生时才有值，然后累加每个数据块之中valjean这个词出现的次数。]]></description><category>JS</category><guid>https://www.beilika.com/article/c163.html</guid><pubDate>Fri, 14 Nov 2025 09:47:54 +0800</pubDate></item><item><title><![CDATA[ES6之Generator函数的语法]]></title><link>https://www.beilika.com/article/c162.html</link><description><![CDATA[简介基本概念Generator函数是ES6提供的一种异步编程解决方案，语法行为与传统函数完全不同。Generator函数有多种理解角度。语法上，首先可以把它理解成，Generator函数是一个状态机，封装了多个内部状态。执行Generator函数会返回一个遍历器对象，也就是说，Generator函数除了状态机，还是一个遍历器对象生成函数。返回的遍历器对象，可以依次遍历Generator函数内部的每一个状态。形式上，Generator函数是一个普通函数，但是有两个特征。一是，function关键字与函数名之间有一个星号；二是，函数体内部使用yield表达式，定义不同的内部状态（yield在英语里的意思就是“产出”）。function*helloWorldGenerator(){yield'hello';yield'world';return'ending';}varhw=helloWorldGenerator();上面代码定义了一个Generator函数helloWorldGenerator，它内部有两个yield表达式（hello和world），即该函数有三个状态：hello，world和return语句（结束执行）。然后，Generator函数的调用方法与普通函数一样，也是在函数名后面加上一对圆括号。不同的是，调用Generator函数后，该函数并不执行，返回的也不是函数运行结果，而是一个指向内部状态的指针对象，也就是之前介绍的遍历器对象（IteratorObject）。下一步，必须调用遍历器对象的next方法，使得指针移向下一个状态。也就是说，每次调用next方法，内部指针就从函数头部或上一次停下来的地方开始执行，直到遇到下一个yield表达式（或return语句）为止。换言之，Generator函数是分段执行的，yield表达式是暂停执行的标记，而next方法可以恢复执行。hw.next()//{value:'hello',done:false}hw.next()//{value:'world',done:false}hw.next()//{value:'ending',done:true}hw.next()//{value:undefined,done:true}上面代码一共调用了四次next方法。第一次调用，Generator函数开始执行，直到遇到第一个yield表达式为止。next方法返回一个对象，它的value属性就是当前yield表达式的值hello，done属性的值false，表示遍历还没有结束。第二次调用，Generator函数从上次yield表达式停下的地方，一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world，done属性的值false，表示遍历还没有结束。第三次调用，Generator函数从上次yield表达式停下的地方，一直执行到return语句（如果没有return语句，就执行到函数结束）。next方法返回的对象的value属性，就是紧跟在return语句后面的表达式的值（如果没有return语句，则value属性的值为undefined），done属性的值true，表示遍历已经结束。第四次调用，此时Generator函数已经运行完毕，next方法返回对象的value属性为undefined，done属性为true。以后再调用next方法，返回的都是这个值。总结一下，调用Generator函数，返回一个遍历器对象，代表Generator函数的内部指针。以后，每次调用遍历器对象的next方法，就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值，是yield表达式后面那个表达式的值；done属性是一个布尔值，表示是否遍历结束。ES6没有规定，function关键字与函数名之间的星号，写在哪个位置。这导致下面的写法都能通过。function*foo(x,y){···}function*foo(x,y){···}function*foo(x,y){···}function*foo(x,y){···}由于Generator函数仍然是普通函数，所以一般的写法是上面的第三种，即星号紧跟在function关键字后面。本书也采用这种写法。yield表达式由于Generator函数返回的遍历器对象，只有调用next方法才会遍历下一个内部状态，所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。遍历器对象的next方法的运行逻辑如下。（1）遇到yield表达式，就暂停执行后面的操作，并将紧跟在yield后面的那个表达式的值，作为返回的对象的value属性值。（2）下一次调用next方法时，再继续往下执行，直到遇到下一个yield表达式。（3）如果没有再遇到新的yield表达式，就一直运行到函数结束，直到return语句为止，并将return语句后面的表达式的值，作为返回的对象的value属性值。（4）如果该函数没有return语句，则返回的对象的value属性值为undefined。需要注意的是，yield表达式后面的表达式，只有当调用next方法、内部指针指向该语句时才会执行，因此等于为JavaScript提供了手动的“惰性求值”（LazyEvaluation）的语法功能。function*gen(){yield123+456;}上面代码中，yield后面的表达式123+456，不会立即求值，只会在next方法将指针移到这一句时，才会求值。yield表达式与return语句既有相似之处，也有区别。相似之处在于，都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield，函数暂停执行，下一次再从该位置继续向后执行，而return语句不具备位置记忆的功能。一个函数里面，只能执行一次（或者说一个）return语句，但是可以执行多次（或者说多个）yield表达式。正常函数只能返回一个值，因为只能执行一次return；Generator函数可以返回一系列的值，因为可以有任意多个yield。从另一个角度看，也可以说Generator生成了一系列的值，这也就是它的名称的来历（英语中，generator这个词是“生成器”的意思）。Generator函数可以不用yield表达式，这时就变成了一个单纯的暂缓执行函数。function*f(){console.log('执行了！')}vargenerator=f();setTimeout(function(){generator.next()},2000);上面代码中，函数f如果是普通函数，在为变量generator赋值时就会执行。但是，函数f是一个Generator函数，就变成只有调用next方法时，函数f才会执行。另外需要注意，yield表达式只能用在Generator函数里面，用在其他地方都会报错。(function(){yield1;})()//SyntaxError:Unexpectednumber上面代码在一个普通函数中使用yield表达式，结果产生一个句法错误。下面是另外一个例子。vararr=[1,[[2,3],4],[5,6]];varflat=function*(a){a.forEach(function(item){if(typeofitem!=='number'){yield*flat(item);}else{yielditem;}});};for(varfofflat(arr)){console.log(f);}上面代码也会产生句法错误，因为forEach方法的参数是一个普通函数，但是在里面使用了yield表达式（这个函数里面还使用了yield*表达式，详细介绍见后文）。一种修改方法是改用for循环。vararr=[1,[[2,3],4],[5,6]];varflat=function*(a){varlength=a.length;for(vari=0;i]]></description><category>JS</category><guid>https://www.beilika.com/article/c162.html</guid><pubDate>Fri, 14 Nov 2025 09:47:27 +0800</pubDate></item><item><title><![CDATA[ES6之Iterator和for…of循环]]></title><link>https://www.beilika.com/article/c161.html</link><description><![CDATA[Iterator（遍历器）的概念JavaScript原有的表示“集合”的数据结构，主要是数组（Array）和对象（Object），ES6又添加了Map和Set。这样就有了四种数据集合，用户还可以组合使用它们，定义自己的数据结构，比如数组的成员是Map，Map的成员是对象。这样就需要一种统一的接口机制，来处理所有不同的数据结构。遍历器（Iterator）就是这样一种机制。它是一种接口，为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口，就可以完成遍历操作（即依次处理该数据结构的所有成员）。Iterator的作用有三个：一是为各种数据结构，提供一个统一的、简便的访问接口；二是使得数据结构的成员能够按某种次序排列；三是ES6创造了一种新的遍历命令for...of循环，Iterator接口主要供for...of消费。Iterator的遍历过程是这样的。（1）创建一个指针对象，指向当前数据结构的起始位置。也就是说，遍历器对象本质上，就是一个指针对象。（2）第一次调用指针对象的next方法，可以将指针指向数据结构的第一个成员。（3）第二次调用指针对象的next方法，指针就指向数据结构的第二个成员。（4）不断调用指针对象的next方法，直到它指向数据结构的结束位置。每一次调用next方法，都会返回数据结构的当前成员的信息。具体来说，就是返回一个包含value和done两个属性的对象。其中，value属性是当前成员的值，done属性是一个布尔值，表示遍历是否结束。下面是一个模拟next方法返回值的例子。varit=makeIterator(['a','b']);it.next()//{value:"a",done:false}it.next()//{value:"b",done:false}it.next()//{value:undefined,done:true}functionmakeIterator(array){varnextIndex=0;return{next:function(){returnnextIndex2//c->3与其他遍历语法的比较以数组为例，JavaScript提供多种遍历语法。最原始的写法就是for循环。for(varindex=0;index1000)break;console.log(n);}上面的例子，会输出斐波纳契数列小于等于1000的项。如果当前项大于1000，就会使用break语句跳出for...of循环。]]></description><category>JS</category><guid>https://www.beilika.com/article/c161.html</guid><pubDate>Fri, 14 Nov 2025 09:47:02 +0800</pubDate></item><item><title><![CDATA[ES6之Promise对象]]></title><link>https://www.beilika.com/article/c160.html</link><description><![CDATA[Promise的含义Promise是异步编程的一种解决方案，比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现，ES6将其写进了语言标准，统一了用法，原生提供了Promise对象。所谓Promise，简单说就是一个容器，里面保存着某个未来才会结束的事件（通常是一个异步操作）的结果。从语法上说，Promise是一个对象，从它可以获取异步操作的消息。Promise提供统一的API，各种异步操作都可以用同样的方法进行处理。Promise对象有以下两个特点。（1）对象的状态不受外界影响。Promise对象代表一个异步操作，有三种状态：pending（进行中）、fulfilled（已成功）和rejected（已失败）。只有异步操作的结果，可以决定当前是哪一种状态，任何其他操作都无法改变这个状态。这也是Promise这个名字的由来，它的英语意思就是“承诺”，表示其他手段无法改变。（2）一旦状态改变，就不会再变，任何时候都可以得到这个结果。Promise对象的状态改变，只有两种可能：从pending变为fulfilled和从pending变为rejected。只要这两种情况发生，状态就凝固了，不会再变了，会一直保持这个结果，这时就称为resolved（已定型）。如果改变已经发生了，你再对Promise对象添加回调函数，也会立即得到这个结果。这与事件（Event）完全不同，事件的特点是，如果你错过了它，再去监听，是得不到结果的。注意，为了行文方便，本章后面的resolved统一只指fulfilled状态，不包含rejected状态。有了Promise对象，就可以将异步操作以同步操作的流程表达出来，避免了层层嵌套的回调函数。此外，Promise对象提供统一的接口，使得控制异步操作更加容易。Promise也有一些缺点。首先，无法取消Promise，一旦新建它就会立即执行，无法中途取消。其次，如果不设置回调函数，Promise内部抛出的错误，不会反应到外部。第三，当处于pending状态时，无法得知目前进展到哪一个阶段（刚刚开始还是即将完成）。如果某些事件不断地反复发生，一般来说，使用Stream模式是比部署Promise更好的选择。Promise.resolve()有时需要将现有对象转为Promise对象，Promise.resolve()方法就起到这个作用。constjsPromise=Promise.resolve($.ajax('/whatever.json'));上面代码将jQuery生成的deferred对象，转为一个新的Promise对象。Promise.resolve()等价于下面的写法。Promise.resolve('foo')//等价于newPromise(resolve=>resolve('foo'))Promise.resolve方法的参数分成四种情况。（1）参数是一个Promise实例如果参数是Promise实例，那么Promise.resolve将不做任何修改、原封不动地返回这个实例。（2）参数是一个thenable对象thenable对象指的是具有then方法的对象，比如下面这个对象。letthenable={then:function(resolve,reject){resolve(42);}};Promise.resolve方法会将这个对象转为Promise对象，然后就立即执行thenable对象的then方法。letthenable={then:function(resolve,reject){resolve(42);}};letp1=Promise.resolve(thenable);//这里的then是Promise的then，和上面那个不是同一个了//console.log(thenable.then==p1.then)falsep1.then(function(value){console.log(value);//42});上面代码中，thenable对象的then方法执行后，对象p1的状态就变为resolved，从而立即执行最后那个then方法指定的回调函数，输出42。（3）参数不是具有then方法的对象，或根本就不是对象如果参数是一个原始值，或者是一个不具有then方法的对象，则Promise.resolve方法返回一个新的Promise对象，状态为resolved。constp=Promise.resolve('Hello');p.then(function(s){console.log(s)});//Hello上面代码生成一个新的Promise对象的实例p。由于字符串Hello不属于异步操作（判断方法是字符串对象不具有then方法），返回Promise实例的状态从一生成就是resolved，所以回调函数会立即执行。Promise.resolve方法的参数，会同时传给回调函数。（4）不带有任何参数Promise.resolve()方法允许调用时不带参数，直接返回一个resolved状态的Promise对象。所以，如果希望得到一个Promise对象，比较方便的方法就是直接调用Promise.resolve()方法。constp=Promise.resolve();p.then(function(){//...});上面代码的变量p就是一个Promise对象。需要注意的是，立即resolve()的Promise对象，是在本轮“事件循环”（eventloop）的结束时执行，而不是在下一轮“事件循环”的开始时。setTimeout(function(){console.log('three');},0);Promise.resolve().then(function(){console.log('two');});console.log('one');//one//two//three上面代码中，setTimeout(fn,0)在下一轮“事件循环”开始时执行，Promise.resolve()在本轮“事件循环”结束时执行，console.log('one')则是立即执行，因此最先输出。Promise.reject()Promise.reject(reason)方法也会返回一个新的Promise实例，该实例的状态为rejected。constp=Promise.reject('出错了');//等同于constp=newPromise((resolve,reject)=>reject('出错了'))p.then(null,function(s){console.log(s)});//出错了上面代码生成一个Promise对象的实例p，状态为rejected，回调函数会立即执行。注意，Promise.reject()方法的参数，会原封不动地作为reject的理由，变成后续方法的参数。这一点与Promise.resolve方法不一致。constthenable={then(resolve,reject){reject('出错了');}};Promise.reject(thenable).catch(e=>{console.log(e===thenable)})//true上面代码中，Promise.reject方法的参数是一个thenable对象，执行以后，后面catch方法的参数不是reject抛出的“出错了”这个字符串，而是thenable对象。Promise.try()实际开发中，经常遇到一种情况：不知道或者不想区分，函数f是同步函数还是异步操作，但是想用Promise来处理它。因为这样就可以不管f是否包含异步操作，都用then方法指定下一步流程，用catch方法处理f抛出的错误。一般就会采用下面的写法。Promise.resolve().then(f)上面的写法有一个缺点，就是如果f是同步函数，那么它会在本轮事件循环的末尾执行。constf=()=>console.log('now');Promise.resolve().then(f);console.log('next');//next//now上面代码中，函数f是同步的，但是用Promise包装了以后，就变成异步执行了。那么有没有一种方法，让同步函数同步执行，异步函数异步执行，并且让它们具有统一的API呢？回答是可以的，并且还有两种写法。第一种写法是用async函数来写。constf=()=>console.log('now');(async()=>f())();console.log('next');//now//next上面代码中，第二行是一个立即执行的匿名函数，会立即执行里面的async函数，因此如果f是同步的，就会得到同步的结果；如果f是异步的，就可以用then指定下一步，就像下面的写法。(async()=>f())().then(...)需要注意的是，async()=>f()会吃掉f()抛出的错误。所以，如果想捕获错误，要使用promise.catch方法。(async()=>f())().then(...).catch(...)第二种写法是使用newPromise()。constf=()=>console.log('now');(()=>newPromise(resolve=>resolve(f())))();console.log('next');//now//next上面代码也是使用立即执行的匿名函数，执行newPromise()。这种情况下，同步函数也是同步执行的。鉴于这是一个很常见的需求，所以现在有一个提案，提供Promise.try方法替代上面的写法。constf=()=>console.log('now');Promise.try(f);console.log('next');//now//next事实上，Promise.try存在已久，Promise库Bluebird、Q和when，早就提供了这个方法。由于Promise.try为所有操作提供了统一的处理机制，所以如果想用then方法管理流程，最好都用Promise.try包装一下。这样有许多好处，其中一点就是可以更好地管理异常。functiongetUsername(userId){returndatabase.users.get({id:userId}).then(function(user){returnuser.name;});}上面代码中，database.users.get()返回一个Promise对象，如果抛出异步错误，可以用catch方法捕获，就像下面这样写。database.users.get({id:userId}).then(...).catch(...)但是database.users.get()可能还会抛出同步错误（比如数据库连接错误，具体要看实现方法），这时你就不得不用try...catch去捕获。try{database.users.get({id:userId}).then(...).catch(...)}catch(e){//...}上面这样的写法就很笨拙了，这时就可以统一用promise.catch()捕获所有同步和异步的错误。Promise.try(()=>database.users.get({id:userId})).then(...).catch(...)事实上，Promise.try就是模拟try代码块，就像promise.catch模拟的是catch代码块。Promise.all()Promise.all()方法用于将多个Promise实例，包装成一个新的Promise实例。constp=Promise.all([p1,p2,p3]);上面代码中，Promise.all()方法接受一个数组作为参数，p1、p2、p3都是Promise实例，如果不是，就会先调用下面讲到的Promise.resolve方法，将参数转为Promise实例，再进一步处理。另外，Promise.all()方法的参数可以不是数组，但必须具有Iterator接口，且返回的每个成员都是Promise实例。p的状态由p1、p2、p3决定，分成两种情况。（1）只有p1、p2、p3的状态都变成fulfilled，p的状态才会变成fulfilled，此时p1、p2、p3的返回值组成一个数组，传递给p的回调函数。（2）只要p1、p2、p3之中有一个被rejected，p的状态就变成rejected，此时第一个被reject的实例的返回值，会传递给p的回调函数。下面是一个具体的例子。//生成一个Promise对象的数组constpromises=[2,3,5,7,11,13].map(function(id){returngetJSON('/post/'+id+".json");});Promise.all(promises).then(function(posts){//...}).catch(function(reason){//...});上面代码中，promises是包含6个Promise实例的数组，只有这6个实例的状态都变成fulfilled，或者其中有一个变为rejected，才会调用Promise.all方法后面的回调函数。下面是另一个例子。constdatabasePromise=connectDatabase();constbooksPromise=databasePromise.then(findAllBooks);constuserPromise=databasePromise.then(getCurrentUser);Promise.all([booksPromise,userPromise]).then(([books,user])=>pickTopRecommendations(books,user));上面代码中，booksPromise和userPromise是两个异步操作，只有等到它们的结果都返回了，才会触发pickTopRecommendations这个回调函数。注意，如果作为参数的Promise实例，自己定义了catch方法，那么它一旦被rejected，并不会触发Promise.all()的catch方法。constp1=newPromise((resolve,reject)=>{resolve('hello');}).then(result=>result).catch(e=>e);constp2=newPromise((resolve,reject)=>{thrownewError('报错了');}).then(result=>result).catch(e=>e);Promise.all([p1,p2]).then(result=>console.log(result)).catch(e=>console.log(e));//["hello",Error:报错了]上面代码中，p1会resolved，p2首先会rejected，但是p2有自己的catch方法，该方法返回的是一个新的Promise实例，p2指向的实际上是这个实例。该实例执行完catch方法后，也会变成resolved，导致Promise.all()方法参数里面的两个实例都会resolved，因此会调用then方法指定的回调函数，而不会调用catch方法指定的回调函数。如果p2没有自己的catch方法，就会调用Promise.all()的catch方法。constp1=newPromise((resolve,reject)=>{resolve('hello');}).then(result=>result);constp2=newPromise((resolve,reject)=>{thrownewError('报错了');}).then(result=>result);Promise.all([p1,p2]).then(result=>console.log(result)).catch(e=>console.log(e));//Error:报错了Promise.race()Promise.race()方法同样是将多个Promise实例，包装成一个新的Promise实例。constp=Promise.race([p1,p2,p3]);上面代码中，只要p1、p2、p3之中有一个实例率先改变状态(不管是什么状态)，p的状态就跟着改变。那个率先改变的Promise实例的返回值，就传递给p的回调函数。Promise.race()方法的参数与Promise.all()方法一样，如果不是Promise实例，就会先调用下面讲到的Promise.resolve()方法，将参数转为Promise实例，再进一步处理。下面是一个例子，如果指定时间内没有获得结果，就将Promise的状态变为reject，否则变为resolve。constp=Promise.race([fetch('/resource-that-may-take-a-while'),newPromise(function(resolve,reject){setTimeout(()=>reject(newError('requesttimeout')),5000)})]);p.then(console.log).catch(console.error);上面代码中，如果5秒之内fetch方法无法返回结果，变量p的状态就会变为rejected，从而触发catch方法指定的回调函数。Promise.allSettled()Promise.allSettled()方法接受一组Promise实例作为参数，包装成一个新的Promise实例。只有等到所有这些参数实例都返回结果，不管是fulfilled还是rejected，包装实例才会结束。该方法由ES2020引入。constpromises=[fetch('/api-1'),fetch('/api-2'),fetch('/api-3'),];awaitPromise.allSettled(promises);removeLoadingIndicator();上面代码对服务器发出三个请求，等到三个请求都结束，不管请求成功还是失败，加载的滚动图标就会消失。该方法返回的新的Promise实例，一旦结束，状态总是fulfilled，不会变成rejected。状态变成fulfilled后，Promise的监听函数接收到的参数是一个数组，每个成员对应一个传入Promise.allSettled()的Promise实例。constresolved=Promise.resolve(42);constrejected=Promise.reject(-1);constallSettledPromise=Promise.allSettled([resolved,rejected]);allSettledPromise.then(function(results){console.log(results);});//[//{status:'fulfilled',value:42},//{status:'rejected',reason:-1}//]上面代码中，Promise.allSettled()的返回值allSettledPromise，状态只可能变成fulfilled。它的监听函数接收到的参数是数组results。该数组的每个成员都是一个对象，对应传入Promise.allSettled()的两个Promise实例。每个对象都有status属性，该属性的值只可能是字符串fulfilled或字符串rejected。fulfilled时，对象有value属性，rejected时有reason属性，对应两种状态的返回值。下面是返回值用法的例子。constpromises=[fetch('index.html'),fetch('https://does-not-exist/')];constresults=awaitPromise.allSettled(promises);//过滤出成功的请求constsuccessfulPromises=results.filter(p=>p.status==='fulfilled');//过滤出失败的请求，并输出原因consterrors=results.filter(p=>p.status==='rejected').map(p=>p.reason);有时候，我们不关心异步操作的结果，只关心这些操作有没有结束。这时，Promise.allSettled()方法就很有用。如果没有这个方法，想要确保所有操作都结束，就很麻烦。Promise.all()方法无法做到这一点。consturls=[/*...*/];constrequests=urls.map(x=>fetch(x));try{awaitPromise.all(requests);console.log('所有请求都成功。');}catch{console.log('至少一个请求失败，其他请求可能还没结束。');}上面代码中，Promise.all()无法确定所有请求都结束。想要达到这个目的，写起来很麻烦，有了Promise.allSettled()，这就很容易了。Promise.any()Promise.any()方法接受一组Promise实例作为参数，包装成一个新的Promise实例。只要参数实例有一个变成fulfilled状态，包装实例就会变成fulfilled状态；如果所有参数实例都变成rejected状态，包装实例就会变成rejected状态。该方法目前是一个第三阶段的提案。Promise.any()跟Promise.race()方法很像，只有一点不同，就是不会因为某个Promise变成rejected状态而结束。constpromises=[fetch('/endpoint-a').then(()=>'a'),fetch('/endpoint-b').then(()=>'b'),fetch('/endpoint-c').then(()=>'c'),];try{constfirst=awaitPromise.any(promises);console.log(first);}catch(error){console.log(error);}上面代码中，Promise.any()方法的参数数组包含三个Promise操作。其中只要有一个变成fulfilled，Promise.any()返回的Promise对象就变成fulfilled。如果所有三个操作都变成rejected，那么await命令就会抛出错误。Promise.any()抛出的错误，不是一个一般的错误，而是一个AggregateError实例。它相当于一个数组，每个成员对应一个被rejected的操作所抛出的错误。下面是AggregateError的实现示例。newAggregateError()extendsArray->AggregateErrorconsterr=newAggregateError();err.push(newError("firsterror"));err.push(newError("seconderror"));throwerr;捕捉错误时，如果不用try...catch结构和await命令，可以像下面这样写。Promise.any(promises).then((first)=>{//Anyofthepromiseswasfulfilled.},(error)=>{//Allofthepromiseswererejected.});下面是一个例子。varresolved=Promise.resolve(42);varrejected=Promise.reject(-1);varalsoRejected=Promise.reject(Infinity);Promise.any([resolved,rejected,alsoRejected]).then(function(result){console.log(result);//42});Promise.any([rejected,alsoRejected]).catch(function(results){console.log(results);//[-1,Infinity]});]]></description><category>JS</category><guid>https://www.beilika.com/article/c160.html</guid><pubDate>Fri, 14 Nov 2025 09:45:26 +0800</pubDate></item></channel></rss>
