阅读视图

发现新文章,点击刷新页面。

AI辅助开发实战:会问问题比会写代码更重要

AI辅助开发实战:会问问题比会写代码更重要

系列第二篇。我想聊聊怎么用好 AI 这个工具。不是教你怎么敲代码,而是教你,怎么真正用好AI辅助开发工具。


原文地址

墨渊书肆/AI辅助开发实战:会问问题比会写代码更重要


你有没有过这样的经历?

打开Cursor(或者TraeCopilot),对着空白编辑器发了半天呆,不知道该让AI帮你干什么。

或者你问了一句「帮我写个登录功能」,AI 噼里啪啦写了一大堆代码,你看都看不懂,最后只能硬着头皮复制粘贴。

再或者,你问 AI:「这个报错是什么意思?」它回了一堆你看不懂的术语,你更迷茫了。

如果你有以上任何一种经历,这篇文章就是写给你的。


会问问题,比会写代码更重要

这是我最近一年用 AI 辅助开发最大的感悟。

以前我觉得,AI 嘛,就是个更聪明的搜索引擎。我不会的代码问它,它告诉我怎么写呗。

后来发现不是这么回事。

同样一个问题,不同的问法,AI 给出的答案质量可以差十倍。

AI 不会读心术。你得把自己的需求翻译成 AI 能理解的语言。

举两个例子感受一下:

第一种问法:「帮我写个登录功能。」

AI给你一个标准答案:用户名密码输入框、提交按钮、后端接口、数据库查询。看起来很全,但放到你的项目里可能完全不适用。你要改吧,改到猴年马月。不要吧,扔掉又可惜。

第二种问法:「我的项目用Next.jsPrisma,用户表字段是 email 和 passwordHash。请帮我写一个登录API,要支持邮箱密码登录,密码用 bcrypt 加密,返回 JWT token,7天有效期。」

AI给你的代码,直接就能用。稍微调一下就能跑。

这就是差距。好的 Prompt 不是更长的Prompt,而是更精确的 Prompt。


几个基本概念

在开始讲技巧之前,先简单说几个你经常会遇到的术语:

LLM:Large Language Model,大语言模型。你可以把LLM理解为"大脑",GPT、Claude、DeepSeek 都是 LLM。ChatGPT、Cursor背后的 AI 都是LLM在驱动。

Prompt:提示词,你给AI说的话。「帮我写个登录功能」就是一个Prompt。

Agent:你可以理解为"能自己干活"的AI。传统AI是你问一句它答一句,Agent 是你告诉它一个目标,它自己规划步骤去执行。Cursor 的 Agent 模式就是这个原理。

MCP:Model Context Protocol,模型上下文协议。这是 2024 年出来的一个标准,让 AI 能统一地访问外部工具和数据。比如 AI 可以通过 MCP 直接读取你电脑上的文件、查询你的数据库、控制浏览器。2026年的 Cursor 已经支持 MCP,用起来很方便。

Token:你可以理解为 AI 处理文字的"计量单位"。英文约4个字符=1个 Token,中文约1-2个汉字=1个 Token。

为什么要注意 Token?因为 AI API 是按 Token 收费的。你输入的文字要花钱,AI输出的文字也要花钱。知道这些就够了,继续往下看。


我的AI辅助开发经验

2026年了,AI辅助开发工具已经成为程序员的标配。CursorTraeCopilotOpenCode……不管你用哪个,核心技巧都是互通的。

我用了一年多,从一开始的「这有啥」到现在的「真香」,总结了一些真正有用的经验。

1. 搞清楚什么时候用什么模式

Cursor 有两个核心模式:AgentChat。用对了,效率翻倍;用错了,就是折磨。

Chat模式:你问一句,它答一句。像跟人聊天一样。

我一般用来:

  • 问具体问题:「这个报错是什么意思?」
  • 查知识点:「PostgreSQL的索引类型有哪些?」
  • 解释代码:「这个函数做了什么?」
  • 帮我想名字:「帮我给这个函数起个名字」

Agent模式:你描述一个任务,它自己去分析和改代码。威力更大,但需要把需求说清楚。

我一般用来:

  • 帮我重构整个模块:「把这个登录从JWT改成Session」
  • 帮我修bug:「登录一直返回401,帮我看看是什么原因」
  • 帮我转换代码:「把这个JavaScript文件改成TypeScript」
  • 帮我实现一个功能:「帮我实现用户注册功能,包含表单验证、数据库存储、发送欢迎邮件」

简单说:小问题用Chat,大任务用Agent。

2. 喂上下文是有技巧的

Cursor 最强的地方是它能理解你的整个项目。你打开一个文件,问它这个组件是做什么的,它能根据文件名、代码内容、项目结构给你答案。

但有时候它也会犯傻——给你一些牛头不对马嘴的回答。

这时候,你得学会喂上下文

我犯过的错误:

「怎么优化这个查询?」

AI回了半天,什么加索引、分页、缓存讲了一套,我根本不知道它说的是什么,因为我连我的表结构都没告诉它。

后来我学乖了:

「我的Prisma查询是这样的:prisma const users = await prisma.user.findMany() 数据量大概10万条,现在查询要3秒,请问怎么优化?」

这次AI直接告诉我:1. 加索引 2. 用select只查需要的字段 3. 考虑分页。

我的习惯是:至少告诉AI三件事

  1. 我用的技术栈是什么(Next.js + Prisma + PostgreSQL)
  2. 当前代码长什么样(贴上代码)
  3. 遇到了什么问题(查询慢、报错、不知道怎么做)

3. Tab键补全真的好用

大部分 AI 辅助开发工具都有代码补全功能,会预测你下一行要写什么。按 Tab 键直接采纳预测。

刚开始我还不太信这个功能,觉得 AI 哪有那么聪明。后来真香了。

我经常这样用:

  • 写TypeScript类型定义,AI能猜到我要的类型
  • 写React组件props,AI能帮我补全大部分
  • 写数据库schema,AI知道我想要什么字段
  • 写import语句,AI知道我要导入什么

10次有8次是准的,能省很多打字的时间。

4. 选中代码让AI帮你改(核心技巧)

选中一段代码,让AI帮你修改。这是一个通用技巧,大部分工具都支持,只是快捷键不太一样。

这是我最常用的功能,没有之一。

比如我选中一个函数,这样用:

「请帮我添加错误处理和类型定义」

AI直接在原代码基础上帮我改好了,我只需要确认一下就行。

比让它生成一段新代码然后我再替换,效率高很多。

再举几个我常用的场景:

  • 选中一段面条式代码:「请帮我重构这段代码」
  • 选中一个API接口:「请帮我添加参数校验」
  • 选中一个组件:「请把这个组件改成响应式」

5. 打开对话窗口做复杂任务

有时候你想让AI帮你做比较复杂的任务,比如生成一个完整的组件。

选中代码后打开对话窗口,可以详细描述你的需求。

我经常这样用:

  1. 选中一段代码
  2. 打开对话窗口
  3. 详细描述我要做什么
  4. AI生成代码,我可以逐行确认

这个功能特别适合:

  • 生成一个新组件
  • 实现一个复杂功能
  • 写测试用例

6. @符号引用文件

@符号引用特定内容。

  • @File :引用当前打开的文件
  • @components/UserCard.tsx :直接引用某个文件
  • @Folder :引用整个文件夹
  • @Docs :引用官方文档
  • @Search :搜索项目内的代码

最常用的场景:

@components/UserCard.tsx 请帮我在这个组件里添加一个编辑用户信息的功能

AI直接读取文件内容,在正确的位置帮我添加代码。

@Docs 请帮我查一下Next.js的metadata怎么用来做SEO

AI直接读官方文档,给我准确的答案。

7. 设置好项目规范

我在每个项目都会设置Rules。这是Cursor的一个特色功能,其他工具类似功能还在发展中。

在项目根目录创建.cursor/rules/目录,放.mdc文件:

---
name: 项目规范
description: Next.js 15 App Router 项目规范
---

# 技术栈
- 框架:Next.js 15 App Router
- 语言:TypeScript strict
- 样式:Tailwind CSS
- 数据库:PostgreSQL + Prisma

# 目录结构
- app/:页面
- components/:组件
- lib/:工具函数
- prisma/:数据库schema

# 代码规范
1. 默认使用 Server Components
2. 客户端用 'use client' 标记
3. API错误格式:{ success: boolean, error?: string }

设置好之后,Cursor每次生成代码都会自动遵循这些规范。

举个例子:我不用每次都说「API错误要返回success和error字段」,Cursor自己就知道。

而且Rules是可以复用的。我做了几个模板:

  • Next.js项目规范
  • NestJS项目规范
  • React组件规范

每次建新项目,直接复制过来改一下就行。

8. 用好Skills,让AI更专业

如果说Rules是「项目规范」,那Skills就是「专业能力」。

你可以在.cursor/skills/目录放一些专业技能文件:

# .cursor/skills/database.md

你是一个数据库专家,精通 PostgreSQL 和 Prisma。

在回答数据库相关问题时:
1. 优先考虑查询性能,避免 N+1 问题
2. 合理使用索引,解释为什么加这个索引
3. 更新用 update,删除用 delete,别用 updateMany

回答时先解释原理,再给代码示例。

用的时候告诉AI:「请用数据库专家的角度,帮我审查以下Prisma查询...」

它回答的专业度明显比普通模式高。

我目前积累了几个Skills:

  • database.md:数据库专家
  • security.md:安全专家
  • performance.md:性能优化专家
  • typescript.md:TypeScript专家

9. MCP让AI更强大

前面提到了MCP,这是2026年特别值得关注的特性。

简单说,MCP 让 AI 能从"只懂训练数据"变成"能操作真实世界":

  • MCP + 文件系统:AI 可以直接读取、修改你本地项目的代码
  • MCP + 数据库:AI 可以直接查询你的数据库
  • MCP + 浏览器:AI 可以控制浏览器,帮你填表单、截图
  • MCP + 搜索:AI 可以帮你搜Google、搜文档

Cursor、Trae 等新一代工具已经开始支持 MCP,装好对应的插件就能用。

装好 MCP 插件后,我可以直接问AI:「帮我查询数据库里最近注册的10个用户」

AI真的会去查数据库,然后给我结果。

这个功能还在快速进化中,未来能做的事情会越来越多。

10. 节省Token是有技巧的

前面提到了 Token 的概念。Token 是 AI 处理文字的计量单位,AI API 是按 Token 收费的。

这是我总结的节省 Token 经验:

  1. 别一上来就贴全栈代码:只贴和问题相关的代码片段,AI不需要看你的整个项目才能回答问题。

  2. 问完一个问题可以开新会话:如果新问题和上一个问题不相关,别在同一个会话里继续聊。AI需要记住之前对话的内容,这些也会算Token。

  3. 让AI一次性完成:比如你要写一个组件,别分开问「先帮我写HTML」「再帮我写样式」「再加个交互」。直接说「帮我写一个登录组件,包含表单验证、错误提示、暗色模式支持」,一次搞定。

  4. 精简你的Prompt:Prompt不是越长越好,是越精确越好。把无关的废话去掉,AI能更专注,Token也花得值。

  5. 用@引用代替复制粘贴:用@File引用文件,AI会自己去读,比你复制粘贴一长串代码省Token。


这些场景我天天用AI

1. 读报错信息

以前遇到报错,我要把错误信息复制到Google搜半天。

现在直接问Cursor:「这个报错是什么意思?TypeError: Cannot read properties of undefined (reading 'map')」

它会告诉我:错误原因是什么、最可能出在哪个地方、怎么修复。

80%的情况下,它能帮我省掉搜索的时间。

有时候我甚至直接截图给它看,它也能分析个大概。

2. 代码Review

以前代码Review都是同事做。现在我先让AI Review一遍,发现低级问题,再交给同事。

效率高很多,而且有些话AI说得,我作为开发者反而不好开口。

「请审查以下代码,指出:1. 潜在安全问题 2. 性能问题 3. 代码规范问题」

它会从安全性、性能、代码规范等角度帮我分析一遍。

3. 重构代码

觉得某段代码写得烂,但不知道怎么改?

问AI:「请帮我重构以下代码,要求:1. 使用TypeScript类型 2. 提取可复用逻辑 3. 增加错误处理」

AI会给一个全新的版本,我可以参考它的思路自己改,也能学到东西。

有时候我还会让它用不同的方式重构,让我对比学习。

4. 帮我想名字

我经常让AI帮我给变量、函数起名字。

「我有一个函数,接受用户ID,返回用户名、邮箱、头像、最后登录时间。请帮我想一个合适的函数名」

AI会给三四个建议:

  • getUserById
  • fetchUserDetails
  • getUserProfile

我会选一个最合适的。

比自己想半天强多了。而且AI起的名字通常都比较规范,符合命名习惯。

5. 写测试

写测试很枯燥,但很重要。

我会让AI帮我:

「请为以下函数编写单元测试,覆盖:正常情况、空输入、错误输入」

AI生成测试代码,我再根据需要调整。能省不少时间。

有时候我还会让它帮我补充边界情况的测试。

6. 查文档

以前遇到问题,我先去 Google 搜,然后看 Stack Overflow,最后实在不行才去翻文档。

现在我直接问 Cursor:

`@Docs 请帮我查一下Next.js 15怎么做密码重置」

或者

`@Docs Vercel AI SDK怎么实现流式响应」

AI直接从文档里给我准确的答案,比我自己搜快多了。

7. 帮我写SQL

有时候我需要写一些复杂的SQL查询,直接问AI:

「帮我写一个SQL,查询过去7天每天的新增用户数,按日期排序」

AI会给我SQL,我稍微改改就能用。

8. 帮我理解别人的代码

接手别人的项目,看不懂代码怎么办?

问AI:

`@components/OldCode.tsx 请帮我解释这个组件做了什么」

AI会把代码逻辑梳理一遍,比我自己看快多了。


积累自己的Prompt模板库

这是我想聊的最后一个话题。

有些 Prompt 我会反复使用,慢慢就积累了一套模板:

// 解释代码
请用三句话解释以下代码做了什么

// 解释报错
这个报错是什么意思?{报错信息}

// 生成类型
请为以下接口生成 TypeScript 类型定义

// 代码审查
请审查以下代码,指出:1. 潜在问题 2. 性能优化点 3. 代码规范问题

// 重构
请重构以下代码,要求:{你的要求}

// 写测试
请为以下函数编写测试用例,覆盖:{场景1}、{场景2}、{场景3}

// 查文档
@Docs {你的问题}

我保存在一个markdown文件里,用的时候直接复制粘贴,稍微改改就能用。

我的经验总结

用多了,你会发现有些规律:

  • 1. 模板要简单通用

    我的模板都很简单,就是一个开头。比如「请用三句话解释」,这个模板可以用在任何代码上。

    不要把模板写得太具体,比如「帮我写一个登录表单要包含用户名、密码、验证码」。这样反而不好复用。

  • 2. 遇到好的Prompt就保存下来

    有时候你会发现,同样的问题,不同的问法,AI回答的质量差很多。

    遇到好的Prompt,就把它保存下来。下次遇到类似的问题,直接用或者改改再用。

  • 3. Rules模板可以复用

    Rules模板也是一样的道理。

    我做了几个模板:

    • Next.js项目规范
    • NestJS项目规范
    • React组件规范

    每次建新项目,直接复制过来改一下就能用。做到第三个项目,你会发现很多规范是可以复用的。

  • 4. 定期整理和迭代

    我的模板库每个月会整理一次。把不用的删掉,好的留下来。

    有时候会发现之前写的模板不够好,就改改。

    这是一个持续迭代的过程,不用急。


写在最后

回到开头的问题:会问问题,为什么比会写代码更重要?

因为 AI 时代,写代码的门槛会越来越低。但提问的能力——把模糊的需求翻译成精确的描述——这个能力反而越来越值钱。

你能不能清楚地描述你想要什么?能不能给 AI 足够的上下文?能不能判断 AI 给出的答案对不对?

这些才是 AI 时代真正的核心竞争力。

AI 辅助开发工具会越来越好用,Cursor、Trae、Copilot、OpenCode……不管你用哪个,核心技巧都是互通的。用好工具的人,永远是那些懂得思考的人。

下一篇文章,我们会开始真正的技术内容:《全栈开发环境搭建:Git + monorepo + 开发工具链》。

感兴趣的话,下一篇见。

为什么2026年还要学全栈?

为什么2026年还要学全栈?

系列开篇,写给想要真正做事的人。


原文地址

墨渊书肆/为什么2026年还要学全栈


你有没有过这样的经历?

做了一套很酷的前端界面,发到群里求赞。朋友问:「能线上访问吗?」你愣了一下:「还在本地跑着呢。」

搭建了一个API接口,测试数据跑得好好的。放到线上就开始报错,你对着日志看了半天,不知道是数据库连接问题还是CORS没配好。

买了个云服务器,SSH连上后对着黑屏发呆——接下来该干什么?域名怎么绑定?HTTPS怎么配置?

如果你有过类似的经历,说明你和我一样,曾经被困在某个技术边界里。

前端会一点,后端也懂一点,但真的要把一个想法变成线上能用的东西,总是差了那么一口气。

我想聊聊这件事。


全栈这件事,被误解了很多年

一提到「全栈工程师」,很多人脑海里浮现的是这样一个形象:什么框架都会,什么语言都能写,数据库也能碰,服务器也能捣鼓。

换句话说,「什么都会一点」。

这种理解,在五年前或许还能成立。那时候做Web开发,确实需要前后端都懂一点才能混得下去。

但2026年了,这种理解该过时了。

真正的全栈,不是「什么都会一点」,而是「能独立交付一个完整的、可运行的互联网产品」。

这两个定义有本质区别。

「什么都会一点」说的是技术广度,你掌握了ABCDE各种技术。 「能交付完整产品」说的是能力深度,你能够从0到1,把想法变成现实。

前者是堆砌,后者是整合。

这十年,全栈经历了什么

让我简单回顾一下这段历史,你可能会更有感触。

  • 2010-2015年:全栈的黄金时代

那时候,一个创业者想要做个网站,真的需要一个人搞定所有事情。PHP就是最典型的全栈语言——一个文件,从数据库到HTML全写了。

没有选择,只能全栈。

  • 2015-2020年:前后端分离,全栈「衰落」

前端技术越来越复杂,React、Vue、Angular各自一套生态。后端技术也在深化,微服务、容器化、云原生,一个领域比一个领域深。

很多人开始专注于一个方向。全栈这个词渐渐变成了「什么都会一点,什么都不精」的代名词。

我见过很多前端工程师,后端代码一行都不敢改。也见过很多后端工程师,写个表单样式就头皮发麻。

技术栈在变宽,人在变窄。

  • 2020年至今:AI时代,全栈复兴

转折来自两个力量:

一是Serverless和全栈框架的成熟。Next.jsSupabase让一个人能覆盖的场景越来越广。

二是AI的爆发。代码可以自动生成了,一个人能做的事情边界再次扩大。

但这次不一样。

这次的全栈,不是回到过去那种「什么都会一点」的状态,而是有了AI的辅助,你可以更专注于「整合」而非「实现」

你不需要记住每个API的用法,AI可以帮你查。但你需要知道一个系统需要哪些模块、它们怎么配合。

这才是2026年「全栈」的真正含义。


我见过太多「会技术」但「做不出东西」的人

我自己也是这么走过来的。

刚学编程的那几年,我痴迷于学新东西。React出来了,学React。Vue火了,学Vue。Node.js流行,学Node。Docker热门,学Docker。

感觉自己越来越厉害,简历上技术栈越来越长。

但有一次,我做一个个人博客系统,前前后后做了俩个月。

不是技术难,而是我在每个环节都卡住:

  • 前端写到一半,发现后端API设计不合理,推倒重来
  • 数据库表结构改了三版,每次都要改前端对应的字段
  • 好不容易做完了,部署上线又折腾了一周
  • 刚上线就被别人注册了一堆垃圾数据,才发现自己没做接口限流

一个看似简单的博客系统,真正从零做到上线,才发现之前学的那些技术都是散的,根本连不起来。

后来我反思:不是我技术不够,而是我从来没有站在「完整产品」的角度去规划一个系统。

这就是问题所在。

但现在,在春节前,我使用 AI 辅助开发和腾讯云的轻量服务器,3天就成功上线了我的个人博客站。

————墨渊书肆

后面,也会根据这个博客站,和我在开发的另一个出海产品,分享我的实战经验。


全栈到底学什么?

说了这么多,你可能想问:所以全栈到底要学什么?

我的回答是:不是学更多技术,而是理解技术之间的关系。

举两个例子。

第一个例子。

你想实现「用户登录后可以评论」这个功能。你需要懂:

  • 前端表单验证
  • 后端接口设计
  • 数据库表结构
  • 密码怎么加密存储
  • Token怎么验证
  • HTTPS怎么配
  • Rate Limiting怎么加

每一项单拎出来都不难。但如果你不懂它们之间的关系,就会出现:前端验证了后端没验证、密码存明文了、Token没过期时间、接口被人刷爆等各种问题。

第二个例子。

你做一个博客系统。要发文章、要看文章、要评论、要搜索、要做SEO、要做推荐。

每个功能你都能找到对应的技术方案。但关键问题是:

  • 先做哪个后做哪个?
  • 数据库表之间怎么关联?
  • 哪些数据要缓存哪些不用?
  • 搜索要做全文检索还是简单like查询?

这些问题没有标准答案,需要你根据实际需求去权衡去决策。

全栈的核心能力,就是理解这些技术怎么配合,然后做出合理的决策。


2026年的全栈技术图谱

既然说到全栈,我把一个现代 Web 应用涉及到的技术领域整理一下。不用全部记住,但需要知道大概有哪些东西,以及每个部分是干嘛的。


前端部分 —— 用户能看到的一切

前端就是用户打开浏览器能看到的所有东西。按钮能不能点、页面好不好看、表单能不能提交,这些都归前端管。

框架:用来构建用户界面。React是现在最主流的选择,Vue在国内用得也比较多,Next.js比较特殊,它既是前端框架,又自带后端能力,属于「全栈框架」。

样式:让界面好看。Tailwind CSS是现在的主流,因为它不用写单独的CSS文件,直接在HTML里写样式,很方便。

状态管理:管理页面数据。比如用户登录了,他的信息存在哪里?购物车有几件商品?这些数据的变化需要统一管理,Zustandmobx是轻量级的选择,Redux功能更全但也 更重。

UI组件:现成的界面零件。shadcn/ui现在特别火,它不是传统意义上的组件库,而是提供代码让你自己修改,这样你可以完全控制样式。


后端部分 —— 用户看不到但每天在用的

后端是服务器上运行的代码,你看不见它,但它在默默处理各种请求。用户登录、提交订单、查询数据——这些都需要后端来处理。

运行时:JS 可以在服务器上运行了,这就是Node.js,目前最成熟。Bun更快,Deno更现代(Node.js的原作者重新写的)。

框架:写后端代码的工具。Next.js API Routes是前后端一起写的方式,适合小项目。Hono非常轻量,而且天然支持 Edge 部署(边缘计算,后面会讲)。NestJS是企业级的,结构更严谨,适合大项目。

数据库:存数据的地方。PostgreSQL是目前最强悍的关系型数据库,MySQL是老牌稳定选手。简单理解:重要数据放数据库。

ORM:数据库和代码之间的翻译官。Prisma用起来很舒服,Drizzle更快且更轻, typeORM 功能更全。它们让你用 JS 的语法去操作数据库,不用写原始SQL。


基础设施 —— 让你的应用能跑起来

这部分是很多前端开发者最头疼的——代码写完了,怎么让它能被所有人访问?这就是基础设施要解决的问题。

服务器:一台24小时开机的电脑。国内的阿里云腾讯云,国外的VercelNetlify,都是提供服务器的服务商。

容器:把应用和它依赖的所有东西打包,这样在任何环境下都能跑。Docker是标配,Docker Compose用来在本机编排多个服务。

CDN:让用户访问更快。CDN就是一堆分布在世界各地的服务器,用户访问时从最近的服务器拿资源,速度会快很多。国际首选Cloudflare,国内用阿里云CDN

域名和SSL:域名是网站的地址,SSL是让访问变成https://的那个加密协议。Let's Encrypt提供免费SSL,Cloudflare可以自动帮你处理HTTPS。


运维监控 —— 保障服务稳定运行

应用上线了,怎么知道用户访问快不快?出错了怎么知道?这些就是运维监控要做的。

日志:记录系统发生了什么。ELK(Elasticsearch + Logstash + Kibana)是经典方案,Loki更轻量。现在很多云服务也自带日志功能。

监控:看系统健康不健康。Sentry专门追踪错误,谁的代码出错了第一时间知道。Prometheus + Grafana是看指标的,比如服务器CPU用了多少、数据库响应多快。

CI/CD:自动化部署。代码提交后自动测试、自动部署到服务器。GitHub Actions最常用,国内有阿里云效腾讯云CODING


安全 —— 保护你的应用

不做安全防护的应用,就像没装门的房子,谁都能进来。

前端安全:XSS是别人在你的页面里注入恶意脚本,CSRF是别人伪造你的身份发请求,CSP是限制页面能加载哪些资源。

后端安全:SQL注入是通过输入框往数据库里塞恶意代码,参数校验是确保用户传的数据是你期望的,Rate Limiting是限制一个人1分钟内只能发10次请求,防止被刷。

数据安全:HTTPS加密传输是最基本的,敏感数据(比如密码)要加密存储,密钥不要写在代码里。


AI能力 —— 新时代的必备技能

2026年了,如果你说自己是全栈但不懂 AI 用法,就像做前端不会用Git一样说不过去。

集成框架Vercel AI SDK是最流行的AI功能集成框架,支持流式响应(就是 ChatGPT 那种一个字一个字蹦出来的效果),对接各种模型很方便。

模型提供商:国外用OpenAI(GPT)、Anthropic(Claude),国内用硅基流动DeepSeek。国内外使用体验和成本差异很大,后面实战会分别讲。

向量数据库:AI场景专用。传统数据库存文字,向量数据库存「意思」。比如你搜「苹果」,它不仅能匹配到「苹果」,还能匹配到「iPhone」、「水果」,因为它理解「苹果」的含义。PineconeMilvus是代表。


这就是现代全栈的完整图谱。你不需要每样都精通,但需要知道它们各自负责什么,以及什么时候该用什么。


AI时代,全栈反而更重要了

我知道你可能会有疑问:现在AI这么强,Cursor敲几下代码就出来了,我还需要学全栈吗?

我的答案是:恰恰相反。

AI可以帮你写一个登录API,但它不知道:

  • 你的产品需不需要短信验证码登录
  • 你的用户数据存储在哪里
  • 你要不要支持微信登录
  • 登录失败几次要锁号
  • Token过期时间设多长

AI可以帮你写一个数据库查询,但它不知道:

  • 你的数据量级需要什么索引
  • 哪些查询需要加缓存
  • 读写分离怎么做

AI可以帮你部署上线,但它不知道:

  • 选择Vercel还是阿里云
  • 国内用户访问慢怎么办
  • 怎么做成本优化

AI擅长的是「点」,你需要的是「面」。

你告诉AI「帮我写个用户登录」,它会给你写一个标准答案。但具体怎么设计,这是你需要决策的事情。

而且,只有当你真正理解一个系统是怎么运转的,你才能:

  • 准确描述你想要什么(而不是永远在改需求)
  • 发现AI写的代码哪里有问题(而不是全盘接受)
  • 把不同模块组合在一起(而不是拼都拼不起来)

这才是整合能力的价值。

AI不是取代你,而是放大你。你原本只能做前端,AI帮你写了后端,你就能做全栈。但前提是,你本来就具备全栈思维,知道一个完整的产品需要什么。


怎么学?T型发展

说了这么多,到底怎么学?我的建议是「T型发展」:

先广度,后深度。

首先,对全栈技术有个整体认知。前端、后端、数据库、运维、安全……每个领域都了解一下,知道它们各自负责什么、解决什么问题。

这个阶段不需要深入,掌握概念就够了。

然后,选择一个方向深挖。

如果你对前端感兴趣,就深入React/Next.js。如果你对后端感兴趣,就深入Node.js/PostgreSQL。深入到能独立完成一个完整项目的程度。

最后,按需补充。

在实际项目中遇到什么问题,就去学什么。需要做支付,就去学Stripe。需要做搜索,就去学Elasticsearch。需要做 AI 功能,就去学Vercel AI SDK

这种「实战驱动」的学习方式,效率最高。


这个系列想带你做什么

市面上不缺技术教程。React入门、Node.js实战、Docker部署——这种内容一搜一大把。

但我发现很多人学完这些教程,还是做不出东西。

因为技术是散的,需要一条线把它们串起来。

这个系列我想带你做的事情很简单:从零开始,构建一个真正能上线的产品。

不是demo,不是练习,而是真实的、可访问的、能在生产环境跑的系统。

我会分成这几个阶段:

  • 第一阶段:认知重建

先理解全栈到底要学什么,怎么学(就是这篇)。

  • 第二阶段:基础设施

服务器、域名、CDN、Docker、日志、监控——那些「不太技术」但非常重要的东西。

  • 第三阶段:前端开发

React、Next.js、TypeScript、UI体系。

  • 第四阶段:后端开发

API设计、数据库、认证、缓存。

  • 第五阶段:AI集成

Vercel AI SDK、流式响应。

  • 第六阶段:部署上线

国内(阿里云)和国外(Vercel)两套方案。

  • 第七阶段:安全与性能

生产环境必须注意的那些事。

  • 第八阶段:实战

两个完整项目,从0到上线的全过程。

在这个过程中,你会看到我踩过的坑、做过的错误决策、总结出的经验。我不是为了告诉你「这个技术怎么用」,而是告诉你「这个系统该怎么搭」。


写在最后

回到开头的问题。

你是不是经常感觉学了很多技术,但真正要用的时候还是不知道从哪里开始?

这很正常。

技术本身不是目的,产品才是。

2026年了,AI 可以帮你写代码,但不能帮你交付产品。能做到这一点的人,永远有市场。

而这,就是我们这个系列要一起做的事情。

下一篇文章,我会讲讲AI辅助开发这件事——怎么用好CursorTraeOpenCode,以及一个更重要的道理:会问问题比会写代码更重要。

感兴趣的话,下一篇见。

你不知道的JS(下):深入编程

你不知道的JS(下):深入编程

本文是《你不知道的JavaScript(下卷)》的阅读笔记,第一部分:深入编程。 供自己以后查漏补缺,也欢迎同道朋友交流学习。

原文地址

墨渊书肆/你不知道的JS(下):深入编程

代码与语句

程序是一组特定的计算机指令。指令的格式和组合规则被称为计算机语言(语法)。

语句

执行特定任务的一组单词、数字和运算符被称为语句。

a = b * 2;
  • ab:变量
  • 2:字面值
  • =*:运算符
  • JS 语句通常以分号 ; 结尾。

表达式

语句由一个或多个表达式组成。表达式是对变量、值的引用,或者是其与运算符的组合。

执行程序

程序需要通过解释器或编译器翻译成计算机可理解的命令后执行。 JS 引擎实际上会即时编译(JIT)程序,然后立即执行编译后的代码。虽然 JS 常被称为解释型语言,但现代引擎的 JIT 过程使得其运行速度非常快。

实践环境

最简单的方法是使用浏览器(Chrome、Firefox 等)的开发者工具。

  • 输出console.log()(控制台输出)或 alert()(弹窗输出)。
  • 输入prompt()(获取用户输入)。

运算符

JavaScript 常用运算符包括:

  • 赋值=(将值保存在变量中)。
  • 算术+-*/%(取模)。
  • 复合赋值+=-=*=/=(如 a += 2 等同于 a = a + 2)。
  • 递增/递减++(递增)、--(递减)。
  • 对象属性访问.(如 obj.a)或 [](如 obj["a"])。
  • 相等==(宽松相等)、===(严格相等)。
  • 逻辑&&(与)、||(或)、!(非),用于表示复合条件。

值与类型

在编程术语中,对值的不同表示方法称为类型。JavaScript 提供了以下内置基本类型:

  • 数字 (number):用于数学计算。
  • 字符串 (string):一个或多个字符组成的文本。
  • 布尔值 (boolean)truefalse,用于决策判断。
  • 除此之外,还提供 数组对象函数 等复合类型。

类型转换

JavaScript 提供显式和隐式两种类型转换机制。

var a = "42"; 
var b = Number(a); // 显式类型转换
console.log( a ); // "42" 
console.log( b ); // 42
console.log( a == b ); // true,隐式类型转换(宽松相等)

代码注释

编写代码不仅是给计算机看,也是给开发者阅读。良好的注释能显著提高代码的可读性,解释器会忽略这些内容。

变量

变量是用于跟踪值变化的符号容器。JavaScript 采用动态(弱)类型机制,变量可以持有任意类型的值。

ES6 块作用域声明

除了传统的 var,ES6 引入了更强大的变量声明方式:

  • let 声明:创建块级作用域变量。相比 var,它解决了提升导致的逻辑混乱,并引入了“暂时性死区”(TDZ)。

  • const 声明:用于创建只读常量。注意,const 锁定的是变量的赋值,而不是值本身。

    const a = [1, 2, 3]; 
    a.push( 4 ); // 成功!内容可以修改
    console.log( a ); // [1, 2, 3, 4] 
    a = 42; // TypeError! 赋值被锁定
    

模板字面量

ES6 引入了反引号 ( ` ) 界定的模板字面量,支持变量插值和多行字符串。

var name = "Kyle"; 
var greeting = `Hello ${name}!`; // 插值解析
var text = `
Now is the time 
for all good men
`; // 支持多行

解构

解构是一种“结构化赋值”方法,可以从数组或对象中快速提取值。

var [ a, b, c ] = [1, 2, 3]; 
var { x, y } = { x: 10, y: 20 };

块与条件判断

:使用 { .. } 将一系列语句组织在一起。

条件判断:最常用的是 if 语句,根据条件的真假决定是否执行后续代码块。

var bank_balance = 302.13; 
var amount = 99.99; 
if (amount < bank_balance) { 
    console.log( "I want to buy this phone!" ); 
}

循环

循环用于重复执行任务,每次执行被称为一次“迭代”。

  • while / do..while:根据条件循环。
  • for:更紧凑的循环形式,包含初始化、测试条件和更新。
var i = 0;
while (true) { 
    if ((i <= 9) === false) { 
        break; // 停止循环
    } 
    console.log(i); 
    i = i + 1; 
} 

for (var i = 0; i <= 9; i++) { 
    console.log( i ); 
}

函数

函数是可复用的代码片段,可以接受参数并返回值。

function printAmount(amt) { 
    console.log( amt.toFixed( 2 ) ); 
} 
function formatAmount() { 
    return "$" + amount.toFixed( 2 ); 
} 
var amount = 99.99; 
printAmount( amount * 2 ); // "199.98" 
amount = formatAmount(); 
console.log( amount ); // "$99.99"

作用域

在 JS 中,每个函数都有自己的作用域(词法作用域)。作用域是变量的集合及访问规则。

  • 只有函数内部的代码才能访问该作用域中的变量。
  • 作用域可以彼此嵌套:内层作用域可以访问外层作用域的变量,反之则不行。

小结

学习编程并不必然是复杂、费力的过程。我们需要熟悉几个基本的概念:

  • 运算符:在值上执行动作。
  • 值与类型:执行各种类型的动作需要值和类型,比如对数字进行数学运算,用字符串输出。
  • 变量:在程序的执行过程中需要变量来保存数据(也就是状态)。
  • 条件判断:需要 if 这样的条件判断来作出决策。
  • 循环:需要循环来重复任务,直到不满足某个条件。
  • 函数:需要函数将代码组织为逻辑上可复用的块。

在编程学习中,实践是绝对无法替代的。理论无法让你成为一个程序员,唯有动手尝试。

你不知道的JS(下):总结与未来

你不知道的JS(下):总结与未来

本文是《你不知道的JavaScript(下卷)》的阅读笔记,第四部分:总结与未来。 供自己以后查漏补缺,也欢迎同道朋友交流学习。

原文地址

墨渊书肆/你不知道的JS(下):总结与未来

深入“你不知道的JS”系列回顾

1. 作用域和闭包

深入理解编译器对代码的处理方式(如“提升”),掌握词法作用域。这是研究闭包的基础,让我们明白变量是如何在不同层级的作用域中被查找和管理的。

2. this 和对象原型

this 是根据函数执行方式动态绑定的,而非定义位置。原型机制是一个属性查找链(委托),模拟类继承通常是对该机制的误用。

3. 类型和语法

类型转换(强制转换)是被严重低估的工具。正确使用它能显著提升代码质量,而不是回避它。

4. 异步和性能

异步编程不仅关乎应用响应速度,更是现代 JS 开发中代码易读性和可维护性的关键。

5. ES6 及更新版本

ES6 是 JavaScript 的一个巨大飞跃。令人兴奋的新特性包括:

  • 语法糖:解构赋值、默认参数值、简洁方法、计算属性、箭头函数。
  • 作用域:块作用域(let/const)。
  • 处理能力:Promise、生成器(Generators)、迭代器(Iterators)。
  • 元编程:代理(Proxy)、反射(Reflect)。
  • 新结构与 API:Map、Set、Symbol、模块(Modules)。
  • 集合扩展:TypedArray。

6. 集合与数据结构

ES6 极大地丰富了处理数据的手段:

  • Map/WeakMap:真正的键值对映射,键可以是任意类型(包括对象)。WeakMap 允许键被垃圾回收,适合存储元数据。
  • Set/WeakSet:唯一值的集合。WeakSet 同样支持弱引用,成员必须是对象。
  • TypedArray:如 Uint8ArrayFloat64Array,提供了对二进制数据的结构化访问,是处理音频、视频及 Canvas 数据的利器。

7. 元编程 (Meta Programming)

元编程关注程序自身的结构和运行时行为:

  • Proxy (代理):通过自定义处理函数(traps)拦截并重新定义对象的底层操作(如 get、set、has 等)。

    var pobj = new Proxy( obj, {
        get(target, key) {
            console.log( "accessing: ", key );
            return target[key];
        }
    } );
    
  • Reflect (反射):提供了一套与 Proxy 拦截器一一对应的静态方法,用于执行对象的默认行为。

  • 尾调用优化 (TCE):ES6 规范要求在严格模式下支持尾调用优化,能够有效避免递归时的栈溢出问题。

8. 新增 API 亮点

  • ArrayArray.of(..) 解决了 Array(..) 构造器的单数字陷阱;Array.from(..) 将类数组轻松转换为真数组。
  • ObjectObject.assign(..) 用于对象混入/克隆。
  • String:新增 includes(..)startsWith(..)repeat(..) 等实用方法。

9. ES6 之后与未来展望

JavaScript 的进化从未停歇:

  • 异步增强async/await(ES2017)让异步代码看起来像同步一样自然。
  • Object.observe:虽然最终被 Proxy 取代,但它代表了数据绑定机制的早期探索。
  • SIMD:单指令多数据流,旨在利用 CPU 并行指令加速数值计算。
  • WebAssembly (WASM):为 JS 引擎引入二进制指令格式,让 C/C++ 等高性能语言能以接近原生的速度在浏览器运行。
  • 正则表达式:新增 u (Unicode) 和 y (Sticky) 标识符。
  • 数字扩展:新的二进制 (0b) 和八进制 (0o) 字面量形式。

10. 代码组织与封装

  • Iterators (迭代器):提供了一套标准化的数据遍历协议。
  • Generators (生成器):通过 yield 实现可暂停/恢复的函数执行。
  • Modules (模块):原生支持基于文件的模块系统,通过 exportimport 实现静态依赖分析。
  • Classes (类):虽然只是原型委托的语法糖,但极大地简化了“面向对象”风格代码的编写。

ES 的现在与未来

版本演进

JavaScript 标准的官方名称是 ECMAScript (ES)

  • ES3:早期的流行标准(IE6-8 时代)。
  • ES5:2009 年发布,现代浏览器的稳固基石。
  • ES6 (ES2015):具有里程碑意义,引入了模块化和类等大型特性。
  • 后续版本:采用基于年份的命名方式(如 ES2016, ES2017...),每年发布一次,使语言特性能够更快速地迭代。

持续进化与工具化

JavaScript 的发展速度已显著加快。为了解决开发者想用新特性与旧环境支持落后之间的矛盾,工具化变得至关重要。

Transpiling 的重要性

Transpiling(转换+编译)技术(如使用 Babel)允许开发者编写最前沿的 ES 代码,并将其自动转换为兼容旧环境(如 ES5)的代码。这让我们既能享受语言进化的红利,又能兼顾用户覆盖面。配合 Polyfilling(填补 API 缺失),构成了现代 JS 开发的基础设施。

小结

JavaScript 的旅程从未停止:

  • 核心积淀:通过对作用域、this、类型和异步的深入探讨,我们夯实了 JS 的底层知识架构。
  • ES6 飞跃:作为里程碑式的版本,ES6 彻底改变了我们编写 JavaScript 的方式,使其具备了开发大型复杂应用的能力。
  • 面向未来:随着年度版本的发布和 WebAssembly 等新技术的出现,JS 正在变得更强、更快、更无处不在。
  • 工具赋能:Transpiler 和 Polyfill 是我们保持技术领先、跨越版本鸿沟的得力助手。

学习这门语言的秘诀在于:不满足于“它能运行”,而要追求“它是如何运行的”。唯有如此,方能在这门不断进化的语言中游刃有余。

你不知道的JS(下):深入JS(下)

你不知道的JS(下):深入JS(下)

本文是《你不知道的JavaScript(下卷)》的阅读笔记,第三部分:深入JS(下)。 供自己以后查漏补缺,也欢迎同道朋友交流学习。

原文地址

墨渊书肆/你不知道的JS(下):深入JS(下)

严格模式 (Strict Mode)

ES5 引入了严格模式,通过 "use strict"; 开启。它可以使代码更安全、更易于引擎优化。

  • 不允许省略 var 的隐式自动全局变量声明。
  • 限制了某些不安全或不合理的语法行为。

函数进阶

作为值的函数

函数在 JavaScript 中是第一类对象,可以作为值赋给变量,也可以作为参数传递或从其他函数返回。

var foo = function() { /* .. */ };
var x = function bar(){ /* .. */ };

立即调用函数表达式 (IIFE)

IIFE 用于创建一个临时作用域并立即执行代码。它也可以有返回值:

var x = (function IIFE(){ 
    return 42; 
})(); 
x; // 42

闭包 (Closure)

闭包允许函数在其定义的词法作用域之外执行时,仍能“记忆”并访问该作用域。

模块模式

这是闭包最常见的应用。模块允许定义外部不可见的私有实现,同时提供公开 API。

function User(){ 
    var username, password; 
    function doLogin(user,pw) { 
        username = user; 
        password = pw; 
    } 
    var publicAPI = { 
        login: doLogin 
    }; 
    return publicAPI;
} 
var fred = User(); 
fred.login( "fred", "12Battery34!" );

this 标识符

this 指向哪个对象取决于函数是如何被调用的。遵循以下四条规则:

  1. 默认绑定:非严格模式下指向全局对象,严格模式下为 undefined
  2. 隐式绑定:由上下文对象调用(如 obj1.foo()),指向该对象。
  3. 显式绑定:通过 callapplybind 指定指向。
  4. new 绑定:指向新创建的空对象。
function foo() { console.log( this.bar ); } 
var bar = "global"; 
var obj1 = { bar: "obj1", foo: foo }; 
var obj2 = { bar: "obj2" }; 

foo();            // "global" (默认绑定)
obj1.foo();       // "obj1"   (隐式绑定)
foo.call( obj2 ); // "obj2"   (显式绑定)
new foo();        // undefined (new 绑定)

原型 (Prototype)

当访问对象不存在的属性时,JavaScript 会自动在内部原型链上查找。这是一种属性查找的备用机制(也称为委托)。

var foo = { a: 42 }; 
var bar = Object.create( foo ); 
bar.a; // 42 (委托给 foo 查找)

ES6 核心特性

符号 (Symbol)

Symbol 是 ES6 引入的新原生类型,没有字面量形式,主要用于创建唯一的、不会冲突的键值。

  • 单例模式:非常适合实现模块单例。
  • 符号注册:通过 Symbol.for(..) 在全局注册表中查找或创建符号。
  • 隐藏属性:符号属性不会出现在一般的属性枚举中(如 Object.keys),需使用 Object.getOwnPropertySymbols(..) 获取。

迭代器 (Iterator)

迭代器是一个结构化模式,用于从数据源一次提取一个值。

  • 接口:必须包含 next() 方法,返回 { value, done }

  • 自定义迭代器:可以手动实现 [Symbol.iterator] 接口。

    var Fib = { 
        [Symbol.iterator]() { 
            var n1 = 1, n2 = 1; 
            return { 
                next() { 
                    var current = n2; 
                    n2 = n1; n1 = n1 + current; 
                    return { value: current, done: false }; 
                } 
            }; 
        } 
    };
    

生成器 (Generator)

生成器是一种特殊的函数,可以在执行中暂停(yield)并恢复。

  • 语法function *foo() { .. }
  • 迭代器控制:生成器返回一个迭代器,通过调用 next() 来控制生成器的执行流。
  • 双向通信yield 不仅可以返回值,还可以接收 next(val) 传入的值。

模块 (Modules)

ES6 模块是基于文件的单例,具有静态 API。

  • 导出与导入:使用 exportimport
  • 静态加载:编译时确定依赖关系,支持模块间循环依赖。
  • 对比旧方法:不再需要依赖闭包和封装函数来实现模块化。

填补与转换 (Polyfilling & Transpiling)

Polyfilling

根据新特性定义,在旧环境中手动实现等价行为的代码。适用于新 API。

if (!Number.isNaN) { 
    Number.isNaN = function isNaN(x) { 
        return x !== x; // NaN 是唯一不等于自身的值
    }; 
}

Transpiling

通过工具(如 Babel)将新语法转换为等价的旧版代码。适用于新语法特性(如箭头函数、解构等),因为这些无法通过 Polyfill 实现。

小结

JavaScript 的进阶特性赋予了这门语言强大的表达能力:

  • 闭包与模块:通过词法作用域记忆功能实现私有化封装,是构建大型应用的基础。
  • this 与原型:理解动态绑定规则与原型委托机制,能够更高效地进行对象间的功能复用。
  • ES6 新范式:迭代器、生成器和原生模块系统标志着 JS 向更成熟、更工程化的方向迈进。
  • 兼容性保障:通过 Polyfill 和 Transpiling,我们可以在拥抱未来的同时,确保代码在旧环境中的稳健运行。

掌握这些核心机制,不仅能帮助我们写出更好的代码,更能让我们深入理解 JavaScript 的运行本质。

你不知道的JS(下):深入JS(上)

你不知道的JS(下):深入JS(上)

本文是《你不知道的JavaScript(下卷)》的阅读笔记,第二部分:深入JS(上)。 供自己以后查漏补缺,也欢迎同道朋友交流学习。

原文地址

墨渊书肆/你不知道的JS(下):深入JS(上)

值和类型

JavaScript 的值有类型,但变量无类型。内置类型包括:

  • 字符串 (string)
  • 数字 (number)
  • 布尔型 (boolean)
  • nullundefined
  • 对象 (object)
  • 符号 (symbol,ES6 新增)

使用 typeof 运算符可以查看值的类型。注意:typeof null 返回 "object",这是一个历史遗留问题。

对象

对象是 JavaScript 中最有用的值类型,可以设置属性。

var obj = { 
    a: "hello world", 
    b: 42, 
    c: true
}; 
obj.a; // "hello world" 
obj["b"]; // 42

数组与函数

数组和函数是对象的特殊子类型:

  • 数组:持有值的对象,通过数字索引位置管理。

    var arr = ["hello world", 42, true]; 
    arr[0]; // "hello world" 
    arr.length; // 3 
    typeof arr; // "object"
    
  • 函数:也是对象的一个子类型,可以拥有属性。

    function foo() { return 42; } 
    foo.bar = "hello world"; 
    typeof foo; // "function"
    

内置类型方法

内置类型及其子类型拥有作为属性和方法暴露出来的行为:

var a = "hello world"; 
a.length; // 11 
a.toUpperCase(); // "HELLO WORLD" 

值的比较

JavaScript 中任何比较的结果都是布尔值(truefalse)。

真与假 (Truthy & Falsy)

JavaScript 中的“假”值列表:

  • ""(空字符串)
  • 0-0NaN(无效数字)
  • nullundefined
  • false 除以上值外,所有其他值均为“真”值。

相等性

相等运算符有四种:=====!=!==

  • ==:允许类型转换情况下的相等性检查。
  • ===:不允许类型转换(严格相等)。
var a = "42"; 
var b = 42;
a == b;  // true (隐式转换)
a === b; // false (严格相等)

关系比较

<><=>= 用于比较有序值(如数字或字母序字符串 "bar" < "foo")。

变量与作用域

变量标识符必须由 a-zA-Z$_ 开始,可以包含数字。

ES6 语法扩展

  • spread/rest 运算符 (...):取决于使用位置,用于展开数组或收集参数。

    // 展开
    function foo(x,y,z) { console.log( x, y, z ); } 
    foo( ...[1,2,3] ); // 1 2 3
    // 收集
    var a = [2,3,4]; 
    var b = [ 1, ...a, 5 ]; // [1,2,3,4,5]
    
  • 默认参数值:为缺失参数提供默认值。

    function foo(x = 11, y = 31) { console.log( x + y ); } 
    foo(5); // 36 (y 使用默认值)
    foo(5, undefined); // 36 (undefined 触发默认值)
    foo(5, null); // 5 (null 被强制转换为 0)
    

提升 (Hoisting)

使用 var 声明的变量和函数声明会被“提升”到其所在作用域的最顶端。

var a = 2;
foo(); 
function foo() { 
    a = 3; 
    console.log( a ); // 3 
    var a; // 声明被提升到了 foo() 的顶端
} 
console.log( a ); // 2

作用域嵌套

声明后的变量在当前作用域及其所有内层作用域中随处可见。

function foo() { 
    var a = 1; 
    function bar() { 
        var b = 2; 
        function baz() { 
            var c = 3; 
            console.log( a, b, c ); // 1 2 3 (向上查找作用域链)
        } 
        baz(); 
    } 
    bar(); 
}

条件判断与循环

除了 if..else,JavaScript 还提供了多种控制流机制。

条件判断

  • switch:适用于多分支判断。
  • 三元运算符 ? ::简洁的条件表达式。

循环

  • for..of 循环:ES6 新增,直接在迭代器产生的上循环。

    var a = ["a","b","c"]; 
    for (var val of a) { 
        console.log( val ); // "a" "b" "c"
    }
    

箭头函数 (=>)

箭头函数不仅是更短的语法,它还解决了 this 绑定的常见痛点(采用词法 this)。

var controller = { 
    makeRequest: function(){ 
        btn.addEventListener( "click", () => { 
            this.makeRequest(); // this 继承自父层,即 controller
        }, false ); 
    } 
};

箭头函数是匿名函数表达式,没有自己的 argumentssupernew.target

小结

深入理解 JavaScript 的第一步是掌握其核心机制:

  • 值与类型:JS 的变量无类型但值有类型。
  • 强制类型转换:理解 ===== 的区别,以及真假值的判断规则。
  • 作用域与提升:掌握 var 的声明提升行为以及嵌套作用域的查找规则。
  • 现代语法:熟悉 ES6 带来的 spread 运算符、默认参数、for..of 循环以及箭头函数等新特性。

通过掌握这些基础,我们可以更从容地应对更高级的 JS 特性。

你不知道的JS(中):程序性能与测试

你不知道的JS(中):程序性能与测试

本文是《你不知道的JavaScript(中卷)》的阅读笔记,第四部分:程序性能与测试。 供自己以后查漏补缺,也欢迎同道朋友交流学习。

程序性能

异步对 JavaScript 来说真的很重要,最显而易见的原因就是性能。如果要发出两个 Ajax 请求,并且它们之间是彼此独立的,但是需要等待两个请求都完成才能执行下一步的任务,那么为这个交互建模有两种选择:顺序与并发。 通常后一种模式会比前一种更高效。而更高的性能通常也会带来更好的用户体验。

Web Worker

我们已经详细介绍了 JavaScript 是如何单线程运作的。但是,单线程并不是组织程序执行的唯一方式。 设想一下,把你的程序分为两个部分:一部分运行在主 UI 线程下,另外一部分运行在另一个完全独立的线程中。

你的浏览器这样的环境,很容易提供多个 JavaScript 引擎实例,各自运行在自己的线程上,这样你可以在每个线程上运行不同的程序。程序中每一个这样的独立的多线程部分被称为一个(Web)Worker。这种类型的并行化被称为任务并行,因为其重点在于把程序划分为多个块来并发运行。

从 JavaScript 主程序(或另一个 Worker)中,可以这样实例化一个 Worker:

// 主程序
var w1 = new Worker( "http://some.url.1/mycoolworker.js" );
// 监听事件
w1.addEventListener( "message", function(evt){ 
    // evt.data 
} );
// 发送事件
w1.postMessage( "something cool to say" );

worker内部,收发消息是完全对称的:

// "mycoolworker.js" 
addEventListener( "message", function(evt){ 
    // evt.data 
} ); 
postMessage( "a really cool reply" );

1. Worker环境 在 Worker 内部是无法访问主程序的任何资源的。这意味着你不能访问它的任何全局变量,也不能访问页面的 DOM 或者其他资源。记住,这是一个完全独立的线程。

但你可以执行网络操作Ajax、WebSockets以及设定定时器。还有Worker可以访问几个重要的全局变量和功能的本地复本,包括 navigator、location、JSON 和 applicationCache。

你还可以通过 importScripts(..) 向 Worker 加载额外的 JavaScript 脚本:

// 在Worker内部
importScripts( "foo.js", "bar.js" );

这些脚本加载是同步的。也就是说,importScripts(..) 调用会阻塞余下 Worker 的执行,直到文件加载和执行完成。

Web Worker 通常应用于哪些方面呢?

  • 处理密集型数学计算
  • 大数据集排序
  • 数据处理(压缩、音频分析、图像处理等)
  • 高流量网络通信

2. 数据传递 在线程之间通过事件机制传递大量的信息,可能是双向的。 特别是对于大数据集而言,就是使用 Transferable 对象。这时发生的是对象所有权的转移,数据本身并没有移动。一旦你把对象传递到一个 Worker 中,在原来的位置上,它就变为空的或者是不可访问的,这样就消除了多线程编程作用域共享带来的混乱。当然,所有权传递是可以双向进行的。

// 比如foo是一个Uint8Array 
postMessage( foo.buffer, [ foo.buffer ] );

3. 共享Worker 创建一个整个站点或 app 的所有页面实例都可以共享的中心 Worker 就非常有用了。这称为 SharedWorker,可通过下面的方式创建(只有 Firefox 和 Chrome 支持这一功能):

var w1 = new SharedWorker( "http://some.url.1/mycoolworker.js" );

在共享 Worker 内部,必须要处理额外的一个事件:"connect"。这个事件为这个特定的连接提供了端口对象。保持多个连接独立的最简单办法就是使用 port 上的闭包:

// 在共享Worker内部
addEventListener( "connect", function(evt){ 
    // 这个连接分配的端口
    var port = evt.ports[0]; 
    port.addEventListener( "message", function(evt){ 
        // .. 
        port.postMessage( .. ); 
        // .. 
    } ); 
    // 初始化端口连接
    port.start(); 
} );

SIMD

单指令多数据(SIMD)是一种数据并行(data parallelism)方式,与 Web Worker 的任务并行(task parallelism)相对,因为这里的重点实际上不再是把程序逻辑分成并行的块,而是并行处理数据的多个位。

asm.js

asm.js这个标签是指 JavaScript 语言中可以高度优化的一个子集。通过小心避免某些难以优化的机制和模式(垃圾收集、类型强制转换,等等),asm.js 风格的代码可以被 JavaScript 引擎识别并进行特别激进的底层优化。

1. 如何使用

var a = 42;
var b = a | 0;

此处我们使用了与 0 的 |(二进制或)运算,除了确保这个值是 32 位整型之外,对于值没有任何效果。这样的代码在一般的 JavaScript 引擎上都可以正常工作。 而对支持 asm.js 的JavaScript 引擎来说,这段代码就发出这样的信号,b 应该总是被当作 32位整型来处理,这样就可以省略强制类型转换追踪。

2. asm.js 模块 对一个 asm.js 模块来说,你需要明确地导入一个严格规范的命名空间——规范将之称为stdlib,因为它应该代表所需的标准库。 你还需要声明一个堆(heap)并将其传入。这个术语用于表示内存中一块保留的位置,变量可以直接使用而不需要额外的内存请求或释放之前使用的内存。这样,asm.js 模块就不需要任何可能导致内存扰动的动作了,只需使用预先保留的空间即可。

var heap = new ArrayBuffer( 0x10000 ); // 64k堆

var arr = new Float64Array( heap );

asm.js 代码如此高度可优化的那些限制的特性显著降低了这类代码的使用范围。asm.js 并不是对任意程序都适用的通用优化手段。它的目标是对特定的任务处理提供一种优化方法,比如数学运算(如游戏中的图形处理)。

程序性能小结

异步编码模式使我们能够编写更高效的代码,通常能够带来非常大的改进。但是,异步特性只能让你走这么远,因为它本质上还是绑定在一个单事件循环线程上。 因此,在这一章里,我们介绍了几种能够进一步提高性能的程序级别的机制。

性能测试与调优

性能测试

如果被问到如何测试某个运算的速度(执行时间),绝大多数 JavaScript 开发者都会从类似下面的代码开始:

var start = (new Date()).getTime(); // 或者Date.now() 
// 进行一些操作
var end = (new Date()).getTime(); 
console.log( "Duration:", (end - start) );

这样低可信度的测试几乎无力支持你的任何决策。这个性能测试基本上是无用的。更坏的是它是危险的,因为它可能提供了错误的可信度。

1. 重复 你可以不以固定次数执行运算,转而循环运行测试,直到达到某个固定的时间。这可能会更可靠一些。

2. Benchmark.js 一个统计学上有效的性能测试工具,名为 Benchmark.js,我们使用这个工具就好了。

环境为王

对特定的性能测试来说,不要忘了检查测试环境,特别是比较任务 X 和 Y 这样的比对测试。仅仅因为你的测试显示 X 比 Y 快,并不能说明结论 X 比 Y 快就有实际的意义。

引擎优化 现代引擎要比我们凭直觉进行的推导复杂得多。它们会实现各种技巧,比如跟踪记录代码在一小段时期内或针对特别有限的输入集的行为。

jsPerf.com

如果想要在不止一个环境下得出像“X 比 Y 快”这样的有意义的结论成立,那你需要在尽可能多的真实环境下进行实际测试。仅仅因为在 Chrome 上某个 X 运算比 Y 快并不意味着这在所有的浏览器中都成立。当然你可能还想要交叉引用多个浏览器上的测试运行结果,并有用户的图形展示。 有一个很棒的网站正是因这样的需求而诞生的,名为 jsPerf (jsperf.com)。它使用我们前面介绍的 Benchmark.js 库来运行统计上精确可靠的测试,并把测试结果放在一个公开可得的 URL 上,你可以把这个 URL 转发给别人。

写好测试

编写更好更清晰的测试。

微性能

var x = [ .. ]; 
// 选择1 
for (var i=0; i < x.length; i++) { 
    // .. 
} 
// 选择2 
for (var i=0, len = x.length; i < len; i++) { 
    // .. 
}

理论上说,这里应该在变量 len 中缓存 x 数组的长度,因为表面上它不会改变,来避免在每个循环迭代中计算 x.length 的代价。

如下是 v8 的一些经常提到的例子:

  • 不要从一个函数到另外一个函数传递 arguments 变量,因为这样的泄漏会降低函数实现速度.
  • 把 try..catch 分离到单独的函数里。浏览器对任何有 try..catch 的函数实行优化都有一些困难,所以把这部分移到独立的函数中意味着你控制了反优化的害处,并让其包含的代码可以优化。

尾调用优化

ES6 包含了一个性能领域的特殊要求。这与一个涉及函数调用的特定优化形式相关:尾调用优化(Tail Call Optimization,TCO)。

function foo(x) { 
    return x; 
} 
function bar(y) { 
    return foo( y + 1 ); // 尾调用
} 
function bar(y) { 
    return foo( y + 1 ); // 尾调用
} 
function baz() { 
    return 1 + bar( 40 ); // 非尾调用
} 
baz(); // 42

调用一个新的函数需要额外的一块预留内存来管理调用栈,称为栈帧。所以前面的代码一般会同时需要为每个 baz()、bar(..) 和 foo(..) 保留一个栈帧。 然而,如果支持 TCO 的引擎能够意识到 foo(y+1) 调用位于尾部,这意味着 bar(..) 基本上已经完成了,那么在调用 foo(..) 时,它就不需要创建一个新的栈帧,而是可以重用已有的 bar(..) 的栈帧。这样不仅速度更快,也更节省内存。

性能测试与调优小结

尾调用优化是 ES6 要求的一种优化方法。它使 JavaScript 中原本不可能的一些递归模式变得实际。TCO 允许一个函数在结尾处调用另外一个函数来执行,不需要任何额外资源。这意味着,对递归算法来说,引擎不再需要限制栈深度。

原文地址

墨渊书肆/你不知道的JS(中):程序性能与测试

你不知道的JS(中):Promise与生成器

你不知道的JS(中):Promise与生成器

本文是《你不知道的JavaScript(中卷)》的阅读笔记,第三部分:Promise与生成器。 供自己以后查漏补缺,也欢迎同道朋友交流学习。

Promise

什么是Promise

未来值 在具体解释 Promise 的 工作方式之前,先来推导通过我们已经理解的方式——回调——如何处理未来值。为了统一处理现在和将来,我们把它们都变成了将来,即所有的操作都成了异步的。

Promise值

function add(xPromise,yPromise) { 
    // Promise.all([ .. ])接受一个promise数组并返回一个新的promise,
    // 这个新promise等待数组中的所有promise完成
    return Promise.all( [xPromise, yPromise] ) 
    // 这个promise决议之后,我们取得收到的X和Y值并加在一起
    .then( function(values){ 
        // values是来自于之前决议的promisei的消息数组
        return values[0] + values[1]; 
    } ); 
} 
// fetchX()和fetchY()返回相应值的promise,可能已经就绪,
// 也可能以后就绪 
add( fetchX(), fetchY() ) 
// 我们得到一个这两个数组的和的promise
// 现在链式调用 then(..)来等待返回promise的决议
.then( function(sum){ 
    console.log( sum ); // 这更简单!
} );

完成事件 在典型的 JavaScript 风格中,如果需要侦听某个通知,你可能就会想到事件。因此,可以把对通知的需求重新组织为对 foo 发出的一个完成事件(completion event,或continuation 事件)的侦听。

function foo(x) { 
    // 开始做点可能耗时的工作
    // 构造一个listener事件通知处理对象来返回
    return listener; 
} 
var evt = foo( 42 ); 
evt.on( "completion", function(){ 
    // 可以进行下一步了!
} ); 
evt.on( "failure", function(err){ 
    // 啊,foo(..)中出错了
} );

promise中监听回调事件:

function foo(x) { 
    // 可是做一些可能耗时的工作
    // 构造并返回一个promise
    return new Promise( function(resolve,reject){ 
        // 最终调用resolve(..)或者reject(..)
        // 这是这个promise的决议回调
    } ); 
} 
var p = foo( 42 ); 
bar( p ); 
baz( p );

具有then方法的鸭子类型

识别 Promise(或者行为类似于 Promise 的东西)就是定义某种称为 thenable 的东西,将其定义为任何具有 then 方法的对象 and 函数。我们认为,任何这样的值就是Promise 一致的 thenable。thenable值的鸭子类型检测就大致类似于:

if ( 
 p !== null && 
 ( 
 typeof p === "object" || 
 typeof p === "function" 
 ) && 
 typeof p.then === "function" 
) { 
 // 假定这是一个thenable! 
} 
else { 
 // 不是thenable 
}

Promise信任问题

先回顾一下只用回调编码的信任问题。把一个回调传入工具 foo(..) 时可能出现如下问题:

  • 调用回调过早;
  • 调用回调过晚(或不被调用);
  • 调用回调次数过少或过多;
  • 未能传递所需的环境和参数;
  • 吞掉可能出现的错误和异常;

1. 调用过早 Promise 就不必担心这种问题,因为即使是立即完成的 Promise(类似于 new Promise(function(resolve){ resolve(42); }))也无法被同步观察到。

2. 调用过晚 Promise 创建对象调用 resolve 或 reject 时,这个 Promise 的then 注册的观察回调就会被自动调度。可以确信,这些被调度的回调在下一个异步事件点上一定会被触发。

3. 回调未调用 如果你对一个 Promise 注册了一个完成回调和一个拒绝回调,那么 Promise在决议时总是会调用其中的一个。 但是,如果 Promise 本身永远不被决议呢?即使这样,Promise 也提供了解决方案,其使用了一种称为竞态的高级抽象机制:

// 用于超时一个Promise的工具
function timeoutPromise(delay) { 
    return new Promise( function(resolve,reject){ 
        setTimeout( function(){ 
            reject( "Timeout!" ); 
        }, delay ); 
    } ); 
} 
// 设置foo()超时
Promise.race( [ 
    foo(), // 试着开始foo() 
    timeoutPromise( 3000 ) // 给它3秒钟
] ) 
.then( 
     function(){ 
         // foo(..)及时完成!
     },
    function(err){ 
        // 或者foo()被拒绝,或者只是没能按时完成
        // 查看err来了解是哪种情况
    } 
);

4. 调用次数过少或过多 如果你把同一个回调注册了不止一次(比如 p.then(f); p.then(f);),那它被调用的次数就会和注册次数相同。响应函数只会被调用一次。

5. 未能传递参数/环境值 Promise 至多只能有一个决议值(完成或拒绝)。 如果你没有用任何值显式决议,那么这个值就是 undefined,这是 JavaScript 常见的处理方式。但不管这个值是什么,无论当前或未来,它都会被传给所有注册的(且适当的完成或拒绝)回调。

6. 吞掉错误或异常 如果在 Promise 的创建过程中或在查看其决议结果过程中的任何时间点上出现了一个 JavaScript 异常错误,比如一个 TypeError 或ReferenceError,那这个异常就会被捕捉,并且会使这个 Promise 被拒绝。

var p = new Promise( function(resolve,reject){ 
    foo.bar(); // foo未定义,所以会出错!
    resolve( 42 ); // 永远不会到达这里
} ); 
p.then( 
    function fulfilled(){ 
        // 永远不会到达这里 :( 
    }, 
    function rejected(err){ 
        // err将会是一个TypeError异常对象来自foo.bar()这一行
    } 
);

链式流

这种方式可以实现的关键在于以下两个 Promise 固有行为特性:

  • 每次你对 Promise 调用 then,它都会创建并返回一个新的 Promise,我们可以将其链接起来;
  • 不管从 then 调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置为被链接 Promise的完成。
var p = Promise.resolve( 21 ); 
var p2 = p.then( function(v){ 
    console.log( v ); // 21 
    // 用值42填充p2
    return v * 2; 
} ); 
// 连接p2 
p2.then( function(v){ 
    console.log( v ); // 42 
} );

术语:决议、完成以及拒绝 对于术语决议(resolve)、完成(fulfill)和拒绝(reject),在更深入学习 Promise 之前,我们还有一些模糊之处需要澄清。先来研究一下构造器 Promise(..):

var p = new Promise( function(X,Y){ 
    // X()用于完成
    // Y()用于拒绝
} );

错误处理

错误处理最自然的形式就是同步的 try..catch 结构。遗憾的是,它只能是同步的,无法用于异步代码模式:

function foo() { 
    setTimeout( function(){ 
        baz.bar(); 
    }, 100 ); 
} 
try {
    foo(); 
    // 后面从 `baz.bar()` 抛出全局错误
} catch (err) { 
    // 永远不会到达这里
}

Promise 使用了分离回调风格。一个回调用于完成情况,一个回调用于拒绝情况:

var p = Promise.reject( "Oops" ); 
p.then( 
    function fulfilled(){ 
        // 永远不会到达这里
    }, 
    function rejected(err){ 
        console.log( err ); // "Oops" 
    } 
);

处理未捕获的情况 浏览器有一个特有的功能是我们的代码所没有的:它们可以跟踪并了解所有对象被丢弃以及被垃圾回收的时机。所以,浏览器可以追踪 Promise 对象。如果在它被垃圾回收的时候其中有拒绝,浏览器就能够确保这是一个真正的未捕获错误,进而可以确定应该将其报告到开发者终端。

Promise模式

1. Promise.all Promise.all 需要一个参数,是一个数组,通常由 Promise 实例组成。从 Promise.all([ .. ]) 调用返回的 promise 会收到一个完成消息。这是一个由所有传入 promise 的完成消息组成的数组,与指定的顺序一致(与完成顺序无关)。

// request(..)是一个Promise-aware Ajax工具
// 就像我们在本章前面定义的一样
var p1 = request( "http://some.url.1/" ); 
var p2 = request( "http://some.url.2/" ); 
Promise.all( [p1,p2] ) 
.then( function(msgs){ 
    // 这里,p1和p2完成并把它们的消息传入
    return request("http://some.url.3/?v=" + msgs.join(",")); 
}) 
.then( function(msg){ 
    console.log( msg ); 
});

2. Promise.race Promise.race也接受单个数组参数。这个数组由一个或多个 Promise、thenable 或立即值组成。一旦有任何一个 Promise 决议为完成,Promise.race就会完成;一旦有任何一个 Promise 决议为拒绝,它就会拒绝。

// request(..)是一个Promise-aware Ajax工具
// 就像我们在本章前面定义的一样
var p1 = request( "http://some.url.1/" ); 
var p2 = request( "http://some.url.2/" ); 
Promise.race( [p1,p2] ) 
.then( function(msg){ 
    // p1或者p2将赢得这场竞赛
    return request("http://some.url.3/?v=" + msg); 
}) 
.then( function(msg){ 
    console.log( msg ); 
});

all和race的变体

  • none([ .. ]) 这个模式类似于 all([ .. ]),不过完成和拒绝的情况互换了。所有的 Promise 都要被 拒绝,即拒绝转化为完成值,反之亦然。
  • any([ .. ]) 这个模式与 all([ .. ]) 类似,但是会忽略拒绝,所以只需要完成一个而不是全部。
  • first([ .. ]) 这个模式类似于与 any([ .. ]) 的竞争,即只要第一个 Promise 完成,它就会忽略后续的任何拒绝和完成。
  • last([ .. ]) 这个模式类似于 first([ .. ]),但却是只有最后一个完成胜出。

Promise API概述

new Promise构造器 有启示性的构造器 Promise(..) 必须和 new 一起使用,并且必须提供一个函数回调。这个回调是同步的或立即调用的。这个函数接受两个函数回调,用以支持 promise 的决议。通常我们把这两个函数称为 resolve(..) 和 reject(..):

var p = new Promise( function(resolve,reject){ 
    // resolve(..)用于决议/完成这个promise
    // reject(..)用于拒绝这个promise
} );

Promise.resolve和 Promise.reject 创建一个已被拒绝的 Promise 的快捷方式是使用 Promise.reject(..),所以以下两个promise 是等价的:

var p1 = new Promise( function(resolve,reject){ 
    reject( "Oops" ); 
} ); 
var p2 = Promise.reject( "Oops" );

then和catch then接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。如果两者中的任何一个被省略或者作为非函数值传入的话,就会替换为相应的默认回调。默认完成回调只是把消息传递下去,而默认拒绝回调则只是重新抛出其接收到的出错原因。 catch只接受一个拒绝回调作为参数,并自动替换默认完成回调。 then 和 catch 也会创建并返回一个新的 promise,这个 promise 可以用于实现Promise 链式流程控制。

Promise局限性

顺序错误处理 很多时候并没有为 Promise 链序列的中间步骤保留的引用。因此,没有这样的引用,你就无法关联错误处理函数来可靠地检查错误。

单一值 根据定义,Promise 只能有一个完成值或一个拒绝理由。在简单的例子中,这不是什么问题,但是在更复杂的场景中,你可能就会发现这是一种局限了。

  1. 分裂值: 这种方法更符合 Promise 的设计理念。如果以后需要重构代码把对 x 和 y 的计算分开,这种方法就简单得多。由调用代码来决定如何安排这两个 promise,而不是把这种细节放在 foo(..) 内部抽象,这样更整洁也更灵活。
function foo(bar,baz) { 
    var x = bar * baz; 
    // 返回两个promise
    return [ 
        Promise.resolve( x ), 
        getY( x ) 
    ]; 
} 
Promise.all( foo( 10, 20 ) ) 
.then( function(msgs){ 
    var x = msgs[0]; 
    var y = msgs[1]; 
    console.log( x, y ); 
} );
  1. 展开/传递参数:

ES6 提供了数组参数解构形式

Promise.all( foo( 10, 20 ) ) 
.then( function([x,y]){ 
    console.log( x, y ); // 200 599 
} );

单决议 Promise 最本质的一个特征是:Promise 只能被决议一次(完成或拒绝)。在许多异步情况中,你只会获取一个值一次,所以这可以工作良好。

无法取消的Promise 一旦创建了一个 Promise 并为其注册了完成或拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,你也没有办法从外部停止它的进程。

Promise的性能 Promise 使所有一切都成为异步的了,即有一些立即(同步)完成的步骤仍然会延迟到任务的下一步。这意味着一个 Promise 任务序列可能比完全通过回调连接的同样的任务序列运行得稍慢一点。

Promise小结

Promise 非常好,请使用。它们解决了我们因只用回调的代码而备受困扰的控制反转问题。 Promise 链也开始 provide 以顺序的方式表达异步流的一个更好的方法,这有助于我们的大脑更好地计划和维护异步 JavaScript 代码。

生成器

JS 开发者在代码中几乎普遍依赖的一个假定:一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插入其间。不过 ES6 引入了一个新的函数类型,它并不符合这种运行到结束的特性。这类新的函数被称为生成器。

打破完整运行

如果foo自身可以通过某种形式在代码的这个位置指示暂停的话,那就仍然可以以一种合作式的方式实现这样的中断(并发)。

var x = 1; 
function *foo() { 
    x++; 
    yield; // 暂停!
    console.log( "x:", x ); 
} 
function bar() { 
    x++; 
} 

// 构造一个迭代器it来控制这个生成器
var it = foo(); 

// 这里启动foo()!
it.next(); 
x; // 2 
bar(); 
x; // 3 
it.next(); // x: 3

解释 ES6 生成器的不同机制和语法之前,我们先来看看运行过程。

  1. it = foo() 运算并没有执行生成器 *foo(),而只是构造了一个迭代器(iterator),这个迭代器会控制它的执行。后面会介绍迭代器。
  2. 第一个 it.next() 启动了生成器 *foo(),并运行了 *foo() 第一行的 x++。
  3. *foo() 在 yield 语句处暂停,在这一点上第一个 it.next() 调用结束。此时 *foo() 仍在运行并且是活跃的,但处于暂停状态。
  4. 我们查看 x 的值,此时为 2。
  5. 我们调用 bar(),它通过 x++ 再次递增 x。
  6. 我们再次查看 x 的值,此时为 3。
  7. 最后的 it.next() 调用从暂停处恢复了生成器 *foo() 的执行,并运行 console.log(..)语句,这条语句使用当前 x 的值 3。

显然,foo() 启动了,但是没有完整运行,它在 yield 处暂停了。后面恢复了 foo() 并让它运行到结束,但这不是必需的。

输入和输出 生成器函数是一个特殊的函数,具有前面我们展示的新的执行模式。但是,它仍然是一个函数,这意味着它仍然有一些基本的特性没有改变。比如,它仍然可以接受参数(即输入),也能够返回值(即输出)。

function *foo(x,y) { 
    return x * y; 
} 
var it = foo( 6, 7 );

var res = it.next();
res.value; // 42

多个迭代器 同一个生成器的多个实例可以同时运行,它们甚至可以彼此交互:

function *foo() { 
    var x = yield 2; 
    z++; 
    var y = yield (x * z); 
    console.log( x, y, z ); 
} 
var z = 1; 
var it1 = foo(); 
var it2 = foo(); 
var val1 = it1.next().value; // 2 <-- yield 2 
var val2 = it2.next().value; // 2 <-- yield 2 
val1 = it1.next( val2 * 10 ).value; // 40 <-- x:20, z:2 
val2 = it2.next( val1 * 5 ).value; // 600 <-- x:200, z:3 
it1.next( val2 / 2 ); // y:300 
 // 20 300 3 
it2.next( val1 / 4 ); // y:10 
 // 200 10 3

我们简单梳理一下执行流程。

  1. *foo() 的两个实例同时启动,两个 next() 分别从 yield 2 语句得到值 2。
  2. val2 * 10 也就是 2 * 10,发送到第一个生成器实例 it1,因此 x 得到值 20. z 从 1 增加到 2,然后 20 * 2 通过 yield 发出,将 val1 设置为 40。
  3. val1 * 5 也就是 40 * 5,发送到第二个生成器实例 it2,因此 x 得到值 200. z 再次从 2递增到 3,然后 200 * 3 通过 yield 发出,将 val2 设置为 600。
  4. val2 / 2 也就是 600 / 2,发送到第一个生成器实例 it1,因此 y 得到值 300,然后打印出 x y z 的值分别是 20 300 3。
  5. val1 / 4 也就是 40 / 4,发送到第二个生成器实例 it2,因此 y 得到值 10,然后打印出x y z 的值分别为 200 10 3。

生成器产生值

我们提到生成器的一种有趣用法是作为一种产生值的方式。

生产者与迭代器 假定你要产生一系列值,其中每个值都与前面一个有特定的关系。要实现这一点,需要一个有状态的生产者能够记住其生成的最后一个值。

var gimmeSomething = (function(){ 
    var nextVal; 
    return function(){ 
        if (nextVal === undefined) { 
            nextVal = 1; 
        } 
        else { 
            nextVal = (3 * nextVal) +6; 
        } 
        return nextVal; 
    }; 
})(); 
gimmeSomething(); // 1 
gimmeSomething(); // 9 
gimmeSomething(); // 33 
gimmeSomething(); // 105

实际上,这个任务是一个非常通用的设计模式,通常通过迭代器来解决。迭代器是一个定义良好的接口,用于从一个生产者一步步得到一系列值。JavaScript 迭代器的接口,与多数语言类似,就是每次想要从生产者得到下一个值的时候调用 next()。

var something = (function(){ 
    var nextVal; 
    return { 
        // for..of循环需要
        [Symbol.iterator]: function(){ return this; }, 
        // 标准迭代器接口方法
        next: function(){ 
            if (nextVal === undefined) { 
                nextVal = 1; 
            } 
            else { 
                nextVal = (3 * nextVal) + 6; 
            } 
            return { done:false, value:nextVal }; 
        } 
    }; 
})(); 
something.next().value; // 1 
something.next().value; // 9 
something.next().value; // 33
something.next().value; // 105

ES6 还新增了一个 for..of 循环,这意味着可以通过原生循环语法自动迭代标准迭代器:

for (var v of something) { 
    console.log( v ); 
    // 不要死循环!
    if (v > 500) { 
        break; 
    } 
} 
// 1 9 33 105 321 969

iterable 可迭代 下面代码片段中的 a 就是一个 iterable。for..of 循环自动调用它的 Symbol.iterator 函数来构建一个迭代器。我们当然也可以手工调用这个函数,然后使用它返回的迭代器:

var a = [1,3,5,7,9]; 
var it = a[Symbol.iterator](); 
it.next().value; // 1 
it.next().value; // 3 
it.next().value; // 5

生成器迭代器 严格说来,生成器本身并不是 iterable,尽管非常类似——当你执行一个生成器,就得到了一个迭代器:

function *something() { 
    var nextVal; 
    while (true) { 
        if (nextVal === undefined) { 
            nextVal = 1; 
        } 
        else { 
            nextVal = (3 * nextVal) + 6; 
        } 
        yield nextVal; 
    } 
}

停止生成器 for..of 循环的“异常结束”(也就是“提前终止”),通常由 break、return 或者未捕获异常引起,会向生成器的迭代器发送一个信号使其终止。

var it = something(); 
for (var v of it) { 
    console.log( v ); 
    // 不要死循环!
    if (v > 500) { 
        console.log( 
            // 完成生成器的迭代器
            it.return( "Hello World" ).value 
        ); 
        // 这里不需要break 
    } 
} 
// 1 9 33 105 321 969 
// 清理!
// Hello World

异步迭代生成器

同步错误处理 我们可以把错误抛入生成器中:

function *main() { 
    var x = yield "Hello World"; 
    yield x.toLowerCase(); // 引发一个异常!
} 
var it = main(); 
it.next().value; // Hello World 
try { 
    it.next( 42 ); 
} 
catch (err) { 
    console.error( err ); // TypeError 
}

生成器 + Promise

首先,把支持 Promise 的 foo(..) 和生成器 *main() 放在一起:

function foo(x,y) { 
    return request( 
        "http://some.url.1/?x=" + x + "&y=" + y 
    ); 
} 
function *main() { 
    try { 
        var text = yield foo( 11, 31 ); 
        console.log( text ); 
    } catch (err) { 
        console.error( err ); 
    } 
}

var it = main(); 
var p = it.next().value; 
// 等待promise p决议
p.then( 
    function(text){ 
        it.next( text ); 
    }, 
    function(err){ 
        it.throw( err ); 
    } 
);

ES7: async与await

function foo(x,y) { 
    return request( 
        "http://some.url.1/?x=" + x + "&y=" + y 
    ); 
} 
async function main() { 
    try { 
        var text = await foo( 11, 31 ); 
        console.log( text ); 
    } catch (err) { 
        console.error( err ); 
    } 
} 
main();

生成器委托

yield * 暂停了迭代控制,而不是生成器控制。当你调用 *foo() 生成器时,现在 yield 委托到了它的迭代器。但实际上,你可以 yield 委托到任意iterable,yield *[1,2,3] 会消耗数组值 [1,2,3] 的默认迭代器。

function *foo() { 
    var r2 = yield request( "http://some.url.2" ); 
    var r3 = yield request( "http://some.url.3/?v=" + r2 ); 
    return r3; 
} 
function *bar() { 
    var r1 = yield request( "http://some.url.1" );
    // 通过 yeild* "委托"给*foo()
    var r3 = yield *foo(); 
    console.log( r3 ); 
} 
run( bar );

为什么用委托 yield 委托的主要目的是代码组织,以达到与普通函数调用的对称。

生成器并发

两个同时运行的进程可以合作式地交替运作,而很多时候这可以产生非常强大的异步表示。 回想一下之前给出的一个场景:其中两个不同并发 Ajax 响应处理函数需要彼此协调,以确保数据交流不会出现竞态条件。我们把响应插入到 res 数组中,就像这样:

function response(data) { 
    if (data.url == "http://some.url.1") { 
        res[0] = data; 
    } 
    else if (data.url == "http://some.url.2") { 
        res[1] = data; 
    } 
}

但是这种场景下如何使用多个并发生成器呢?

// request(..)是一个支持Promise of Ajax工具
var res = []; 
function *reqData(url) { 
    res.push( 
        yield request( url ) 
    ); 
}

形实转换程序

你用一个函数定义封装函数调用,包括需要的任何参数,来定义这个调用的执行,那么这个封装函数就是一个形实转换程序。之后在执行这个 thunk 时,最终就是调用了原始的函数。

function foo(x,y,cb) { 
    setTimeout( function(){ 
        cb( x + y ); 
    }, 1000 ); 
} 
function fooThunk(cb) { 
    foo( 3, 4, cb ); 
} 
// 将来
fooThunk( function(sum){ 
    console.log( sum ); // 7 
} );

ES6之前的生成器

function foo(url) { 
    // .. 
    // 构造并返回一个迭代器
    return { 
        next: function(v) { 
        // .. 
        }, 
        throw: function(e) { 
            // .. 
        } 
    }; 
}

var it = foo( "http://some.url.1" );

生成器小结

生成器为异步代码保持了顺序、同步、阻塞的代码模式,这使得大脑可以更自然地追踪代码,解决了基于回调的异步的两个关键缺陷之一。

原文地址

墨渊书肆/你不知道的JS(中):Promise与生成器

你不知道的JS(中):强制类型转换与异步基础

你不知道的JS(中):强制类型转换与异步基础

本文是《你不知道的JavaScript(中卷)》的阅读笔记,第二部分:强制类型转换与异步基础。 供自己以后查漏补缺,也欢迎同道朋友交流学习。

强制类型转换

值类型转换

将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况;隐式的情况称为强制类型转换(coercion)。

var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换

抽象值操作

ToString 基本类型值的字符串化规则为:null转换为"null",undefined转换为"undefined",true转换为 "true"。数字的字符串化则遵循通用规则,不过那些极小和极大的数字使用指数形式:

// 1.07 连续乘以七个 1000
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 七个1000一共21位数字
a.toString(); // "1.07e21"

JSON字符串化 工具函数 JSON.stringify 在将 JSON 对象序列化为字符串时也用到了 ToString。但JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。

JSON.stringify(undefined); // undefined
JSON.stringify(function(){}); // undefined
JSON.stringify([1,undefined,function(){},4]); // "[1,null,null,4]"
JSON.stringify({ a:2, b:function(){} }); // "{"a":2}"

ToNumber 其中 true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。

ToBoolean JS中的值可以分为俩类:

  1. 可以被强制类型转换为false的值
  2. 其他

以下是假值,假值的布尔强制类型转换结果为false:

  • undefined
  • null
  • false
  • +0、-0和NaN
  • ""

假值对象是真值

var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );

Boolean( a && b && c ); // true

真值:假值列表之外的就是真值

var a = "false";
var b = "0";
var c = "''";
Boolean( a && b && c ); // true

var a = []; // 空数组——是真值还是假值?
var b = {}; // 空对象——是真值还是假值?
var c = function(){}; // 空函数——是真值还是假值?
Boolean( a && b && c ); // true

显式强制类型转换

日期显式转换为数字

var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
+d; // 1408369986000

奇特的~运算符 ~,它首先将值强制类型转换为 32 位数字,然后执行字位操作“非”(对每一个字位进行反转)。 ~ 返回 2 的补码

~42; // -(42+1) ==> -43

~ 的神奇之处在于进行检查字符串中是否有包含指定的字符串:

var a = "Hello World";
~a.indexOf( "lo" ); // -4 <-- 真值!
if (~a.indexOf( "lo" )) { // true
 // 找到匹配!
}
~a.indexOf( "ol" ); // 0 <-- 假值!
!~a.indexOf( "ol" ); // true
if (!~a.indexOf( "ol" )) { // true
 // 没有找到匹配!
}

显式解析数字字符串 解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但解析和转换两者之间还是有明显的差别。

var a = "42";
var b = "42px";
Number( a ); // 42
parseInt( a ); // 42
Number( b ); // NaN
parseInt( b ); // 42

解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败bing返回 NaN。

解析非字符串

parseInt( 1/0, 19 ); // 18

很多人想当然地以为“如果第一个参数值为 Infinity,解析结果也应该是 Infinity”,返回 18 也太无厘头了。实际的 JavaScript 代码中不会用到基数 19,它的有效数字字符范围是 0-9 和 a-i(区分大小写)。parseInt(1/0, 19) 实际上是 parseInt("Infinity", 19)。第一个字符是 "I",以 19 为基数时值为 18。第二个字符 "n" 不是一个有效的数字字符,解析到此为止。 此外还有一些看起来奇怪但实际上解释得通的例子:

parseInt( 0.000008 ); // 0 ("0" 来自于 "0.000008")
parseInt( 0.0000008 ); // 8 ("8" 来自于 "8e-7")
parseInt( false, 16 ); // 250 ("fa" 来自于 "false")
parseInt( parseInt, 16 ); // 15 ("f" 来自于 "function..")
parseInt( "0x10" ); // 16
parseInt( "103", 2 ); // 2

显式转换为布尔值 显式强制类型转换为布尔值最常用的方法是!!。

隐式强制类型转换

字符串和数字之间的隐式强制类型转换 通过+运算符进行字符串拼接

var a = "42";
var b = "0";
var c = 42;
var d = 0;
a + b; // "420"
c + d; // 42

因为数组的valueOf() 操作无法得到简单基本类型值,于是它转而调用 toString()。因此下面例子中的两个数组变成了 "1,2" 和 "3,4"。+ 将它们拼接后返回 "1,23,4"。

var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"

a + ""(隐式)和 String(a)(显式)之间有一个细微的差别需要注意。根据ToPrimitive 抽象操作规则,a + "" 会对 a 调用 valueOf() 方法,然后通过 ToString 抽象操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()。

var a = {
    valueOf: function() { return 42; },
    toString: function() { return 4; }
};
a + ""; // "42"
String( a ); // "4"

再来看看从字符串强制类型转换为数字的情况。- 是数字减法运算符,因此 a - 0 会将 a 强制类型转换为数字。

var a = "3.14";
var b = a - 0;
b; // 3.14

隐式强制类型转换为布尔值 相对布尔值,数字和字符串操作中的隐式强制类型转换还算比较明显。下面的情况会发生布尔值隐式强制类型转换。 (1) if (..) 语句中的条件判断表达式。 (2) for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。 (3) while (..) 和 do..while(..) 循环中的条件判断表达式。 (4) ? : 中的条件判断表达式。 (5) 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。

|| 和 && && 和 || 运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。

var a = 42;
var b = "abc";
var c = null;

a || b; // 42 
a && b; // "abc"

c || b; // "abc" 
c && b; // null

|| 和 && 首先会对第一个操作数(a 和 c)执行条件判断,如果其不是布尔值(如上例)就先进行 ToBoolean 强制类型转换,然后再执行条件判断。 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数(a 和 c)的值,如果为false 就返回第二个操作数(b)的值。 && 则相反,如果条件判断结果为 true 就返回第二个操作数(b)的值,如果为 false 就返回第一个操作数(a 和 c)的值。

符号的强制类型转换 ES6 中引入了符号类型,它的强制类型转换有一个坑,在这里有必要提一下。ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误:

var s1 = Symbol( "cool" );
String( s1 ); // "Symbol(cool)"
var s2 = Symbol( "not cool" );
s2 + ""; // TypeError

宽松相等和严格相等

常见的误区是“== 检查值是否相等,=== 检查值和类型是否相等”。听起来蛮有道理,然而 还不够准确。很多 JavaScript 的书籍和博客也是这样来解释的,但是很遗憾他们都错了。

正确的解释是:“== 允许在相等比较中进行强制类型转换,而 === 不允许。”

抽象相等 == 在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之一或两者都转换为相同的类型后再进行比较。

  • 字符串和数字之间的相等比较: (1) 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。 (2) 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果
var a = 42;
var b = "42";
a === b; // false
a == b; // true
  • 其他类型和布尔类型之间的相等比较: (1) 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果; (2) 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
var a = "42";
var b = true;
a == b; // false
  • null 和 undefined 之间的相等比较 (1) 如果 x 为 null,y 为 undefined,则结果为 true。 (2) 如果 x 为 undefined,y 为 null,则结果为 true。
var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true

a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false
  • 对象 and 非对象之间的相等比较 (1) 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果; (2) 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果。
var a = 42;
var b = [ 42 ];
a == b; // true

比较少见的情况

  1. 返回其他数字:
Number.prototype.valueOf = function() {
 return 3;
};
new Number( 2 ) == 3; // true
  1. 假值的相等比较:
"0" == null; // false
"0" == undefined; // false
"0" == false; // true
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false

false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true
false == ""; // true
false == []; // true
false == {}; // false

"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true
"" == []; // true
"" == {}; // false

0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true
0 == {}; // false
  1. 极端情况

根据 ToBoolean 规则,它会进行布尔值的显式强制类型转换。所以 [] == ![] 变成了 [] == false。前面介绍 of false == [],最后的结果就顺理成章了

[] == ![] // true

安全运用隐式强制类型转换

  • 如果两边的值中有 true 或者 false,千万不要使用 ==。
  • 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==。 这时最好用 === 来避免不经意的强制类型转换。这两个原则可以让我们避开几乎所有强制类型转换的坑

抽象关系比较

a < b 中涉及的隐式强制类型转换: 比较双方首先调用 ToPrimitive,如果结果出现非字符串,就根据 ToNumber 规则将双方强制类型转换为数字来进行比较。

var a = [ 42 ];
var b = [ "43" ];
a < b; // true
b < a; // false

如果比较双方都是字符串,则按字母顺序来进行比较:

var a = [ "42" ];
var b = [ "043" ];
a < b; // false

var a = [ 4, 2 ];
var b = [ 0, 4, 3 ];
a < b; // false

还有个特殊情况:

var a = { b: 42 };
var b = { b: 43 };
a < b; // false
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true

因为 a 是 [object Object],b 也是 [object Object],所以按照字母顺序a < b 并不成立。

为什么 a == b 的结果不是 true ?它们的字符串值相同(同为 "[object Object]"),按道理应该相等才对?实际上不是这样,你可以回忆一下前面讲过的对象的相等比较。

但是 if a < b 和 a == b 结果为 false,为什么 a <= b 和 a >= b 的结果会是 true 呢?因为根据规范 a <= b 被处理为 b < a,然后将结果反转。因为 b < a 的结果是 false,所以 a <= b 的结果是 true。

这可能与我们设想的大相径庭,即 <= 应该是“小于或者等于”。实际上 JavaScript 中 <= 是“不大于”的意思(即 !(a > b),处理为 !(b < a))。同理 a >= b 处理为 b <= a。

强制类型转换小结

JS 的数据类型之间的转换,即强制类型转换:包括显式和隐式。

显式强制类型转换明确告诉我们哪里发生了类型转换,有助于提高代码可读性和可维护性。

隐式强制类型转换则没有那么明显,是其他操作的副作用。实际上隐式强制类型转换也有助于提高代码的可读性。在处理强制类型转换的时候要十分小心,尤其是隐式强制类型转换。

语法

语句和表达式

JS中语句相当于句子,表达式相当于短语,运算符则相当于标点符号和连接词。

语句的结果值 代码块的结果值就如同一个隐式的返回,即返回最后一个语句的结果值。

var b;
if (true) {
    b = 4 + 38;
}

表达式的副作用 函数调用的副作用:

function foo() {
 a = a + 1;
}
var a = 1;
foo(); // 结果值:undefined。副作用:a的值被改变

= 赋值运算符:

var a;
a = 42; // 42
a; // 42

运算符优先级

&& 先执行,然后是 ||:

(false && true) || true; // true
false && (true || true); // false

false && true || true; // true

那执行顺序是否就一定是从左到右呢?不妨将运算符颠倒一下看看:

true || false && false; // true
(true || false) && false; // false
true || (false && false); // true

这说明 && 运算符先于 || 执行,而且执行顺序并非我们所设想的从左到右。原因就在于运算符优先级。

短路 对于 && 和 || 来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。我们将这种现象称为“短路”(即执行最短路径)。

更强的绑定 因为 && 运算符的优先级高于 ||,而 || 的优先级又高于 ? :。

a && b || c ? c || b ? a : c && b : a
// 等同于
(a && b || c) ? (c || b) ? a : (c && b) : a

关联 一般多个&&和||执行顺序是从左到右,也被称为左关联,但? : 是右关联

a ? b : c ? d : e;
// 等同于
a ? b : (c ? d : e)

另一个右关联组合的例子是 = 运算符:

var a, b, c;
a = b = c = 42;
// 等同于
a = (b = (c = 42))

自动分号

JS会自动为代码行补上缺失的分号,即自动分号插入(Automatic Semicolon Insertion,ASI)。

错误

JS不仅有各种类型的运行时错误(TypeError、ReferenceError、SyntaxError 等),它的语法中也定义了一些编译时错误。

提前使用变量 ES6 规范定义了一个新概念,叫作 TDZ(Temporal Dead Zone,暂时性死区)。TDZ 指的是由于代码中的变量还没有初始化而不能被引用的情况。

{
    a = 2; // ReferenceError!
    let a; 
}

函数参数

在 ES6 中,如果参数被省略或者值为 undefined,则取该参数的默认值:

function foo( a = 42, b = a + 1 ) {
    console.log( a, b );
}
foo(); // 42 43
foo( undefined ); // 42 43
foo( 5 ); // 5 6
foo( void 0, 7 ); // 42 7
foo( null ); // null 1

try finally

finally 中的代码总是会在 try 之后执行,如果有 catch 的话则在 catch 之后执行。也可以将 finally 中的代码看作一个回调函数,即无论出现什么情况最后一定会被调用。

function foo() {
    try {
        return 42;
    } 
    finally {
        console.log( "Hello" );
    }
    console.log( "never runs" );
}
console.log( foo() );
// Hello
// 42

这里 return 42 先执行,并将 foo() 函数的返回值设置为 42。然后 try 执行完毕,接着执行 finally。最后 foo() 函数执行完毕,console.log(..) 显示返回值。 try 中的 throw 也是如此:

function foo() {
    try {
        throw 42; 
    }
    finally {
        console.log( "Hello" );
    }
    console.log( "never runs" );
}
console.log( foo() );
// Hello
// Uncaught Exception: 42

switch

switch,可以把它看作 if..else if..else.. 的简化版本:

switch (a) {
    case 2:
    // 执行一些代码
    break;
    case 42:
    // 执行另外一些代码
    break;
    default:
    // 执行缺省代码
}

a 和 case 表达式的匹配算法与 === 相同。通常case语句中switch都是简单值,但有时可能会需要通过强制类型转换来进行相等比较,这时就需要做一些特殊处理:

var a = "42";
switch (true) {
    case a == 10:
        console.log( "10 or '10'" );
        break;
    case a == 42;
        console.log( "42 or '42'" );
        break;
    default:
        // 永远执行不到这里
}
// 42 or '42'

尽管可以使用 ==,但 switch 中 true and true 之间仍然是严格相等比较。即 if case 表达式的结果为真值,但不是严格意义上的 true,则条件不成立。

var a = "hello world";
var b = 10;
switch (true) {
    case (a || b == 10):
        // 永远执行不到这里
        break;
    default:
        console.log( "Oops" );
}
// Oops

最后,default 是可选的,并非必不可少。break 相关规则对 default 仍然适用:

var a = 10;
switch (a) {
    case 1:
    case 2:
        // 永远执行不到这里
    default:
        console.log( "default" );
    case 3:
        console.log( "3" );
        break;
    case 4:
        console.log( "4" );
}
// default
// 3

上例中的代码是这样执行的,首先遍历并找到所有匹配的 case,如果没有匹配则执行default 中的代码。因为其中没有 break,所以继续执行已经遍历过的 case 3 代码块,直到 break 为止。

语法小结

JS的语法规则之上是语义规则,也称上下文。 JS还详细定义了运算符的优先级和关联。

异步:现在与将来

程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。

分块的程序

可以把 JavaScript 程序写在单个 .js 文件中,但是这个程序几乎一定是由多个块构成的。这些块中只有一个是现在执行,其余的则会在将来执行。最常见的块单位是函数。 大多数 JS 新手程序员都会遇到的问题是:程序中将来执行的部分并不一定在现在运行的部分执行完之后就立即执行。 从现在到将来的“等待”,最简单的方法是使用一个通常称为回调函数的函数:

// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", function myCallbackFunction(data){
    console.log( data ); // 耶!这里得到了一些数据!
});

异步控制台 在某些条件下,某些浏览器的 console.log 并不会把传入的内容立即输出。出现这种情况的主要原因是,在许多程序(不只是 JS)中,I/O 是非常低速的阻塞部分。所以浏览器在后台异步处理控制台 I/O 能够提高性能,这时用户甚至可能根本意识不到其发生。

事件循环

所有这些环境都有一个共同“点”(thread,也指线程。),即它们都提供了一种机制来处理程序中多个块的执行,且执行每块时调用 JS 引擎,这种机制被称为事件循环。 先通过一段伪代码了解一下这个概念 :

// eventLoop是一个用作队列的数组
// (先进,先出)
var eventLoop = [ ];
var event;
// “永远”执行
while (true) {
    // 一次tick
    if (eventLoop.length > 0) {
        // 拿到队列中的下一个事件
        event = eventLoop.shift();
        // 现在,执行下一个事件
        try {
            event();
        } catch (err) {
            reportError(err);
        }
    }
}

可以看到,有一个用 while 循环实现的持续运行的循环,循环的每一轮称为一个 tick。对每个 tick 而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行。这些事件就是你的回调函数。

并行线程

术语“异步”和“并行”常常被混为一谈,但实际上它们的意义完全不同。记住,异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。 并行计算最常见的工具就是进程和线程. 进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。

并发

两个或多个“进程”同时执行就出现了并发,不管组成它们的单个运算是否并行执行(在独立的处理器或处理器核心上同时运行)。可以把并发看作“进程”级(或者任务级)的并行,与运算级的并行(不同处理器上的线程)相对。

非交互 如果进程间没有相互影响的话,不确定性是完全可以接受的。

交互 并发的“进程”需要相互交流,通过作用域或 DOM 间接交互。正如前面介绍的,如果出现这样的交互,就需要对它们的交互进行协调以避免竞态的出现。

协作 还有一种并发合作方式,称为并发协作(cooperative concurrency)。这里的重点不再是通过共享作用域中的值进行交互。这里的目标是取到一个长期运行的“进程”,并将其分割成多个步骤或多批任务,使得其他并发“进程”有机会将自己的运算插入到事件循环队列中交替运行。

任务

在 ES6 中,有一个新的概念建立在事件循环队列之上,叫作任务队列(job queue)。对于任务队列最好的理解方式就是,它是挂在事件循环队列的每个 tick 之后的一个队列。在事件循环的每个 tick 中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前 tick 的任务队列末尾添加一个项目(一个任务)。

语句顺序

代码中语句的顺序和js引擎执行语句的顺序并不一定要一致。

异步小结

JS 程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前累积的修改之上进行的。 一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个tick。用户交互、IO 和定时器会向事件队列中加入事件。

回调

到目前为止,回调是编写和处理 JavaScript 程序异步逻辑的最常用方式。确实,回调是这门语言中最基础的异步模式。

延续(continuation)

回调函数包裹或者说封装了程序的延续(continuation)。

// A 
setTimeout( function(){ 
    // C 
}, 1000 ); 
// B

执行 A,设定延时 1000 毫秒,然后执行 B,然后定时到时后执行C

顺序的大脑

执行与计划 我们的大脑可以看作类似于单线程运行的事件循环队列,就像 JavaScript 引擎那样。这个比喻看起来很贴切。但是,我们的分析还需要比这更加深入细致一些。显而易见的是,在我们如何计划各种任务和我们的大脑如何实际执行这些计划之间,还存在着很大的差别。

嵌套回调和链式回调

listen( "click", function handler(evt){ 
    setTimeout( function request(){ 
        ajax( "http://some.url.1", function response(text){ 
            if (text == "hello") { 
                handler(); 
            } 
            else if (text == "world") { 
                request(); 
            } 
        } ); 
    }, 500) ; 
} );

这种代码常常被称为回调地狱(callback hell),有时也被称为毁灭金字塔。 让我们不用嵌套再把前面的嵌套事件 / 超时 /Ajax 的例子重写一遍吧:

listen( "click", handler ); 
function handler() { 
    setTimeout( request, 500 ); 
} 
function request(){ 
    ajax( "http://some.url.1", response ); 
} 
function response(text){ 
    if (text == "hello") { 
        handler(); 
    } 
    else if (text == "world") { 
        request(); 
    } 
}

信任问题

// A 
ajax( "..", function(..){ 
    // C 
} ); 
// B

在 JS 主程序的直接控制之下。而 // C 会延迟到将来发生,并且是在第三方的控制下——在本例中就是函数 ajax。从根本上来说,这种控制的转移通常不会给程序带来很多问题。 但是,请不要被这个小概率迷惑而认为这种控制切换不是什么大问题。实际上,这是回调驱动设计最严重(也是最微妙)的问题。它以这样一个思路为中心:有时候 ajax 不是你编写的代码,也不在你的直接控制下。多数情况下,它是某个第三方提供的工具。 我们把这称为控制反转,也就是把自己程序一部分的执行控制交给某个第三方。在你的代码和第三方工具之间有一份并没有明确表达的契约。

省点回调

为了更优雅地处理错误,有些 API 设计提供了分离回调(一个用于成功通知,一个用于出错通知):

function success(data) { 
    console.log( data ); 
} 
function failure(err) { 
    console.error( err ); 
} 
ajax( "http://some.url.1", success, failure );

在这种设计下,API 的出错处理函数 failure() 常常是可选的,如果没有提供的话,就是假定这个错误可以吞掉。

回调小结

回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。

原文地址

墨渊书肆/你不知道的JS(中):强制类型转换与异步基础

❌