1.3 JSX 语法 本节摘要:JSX 是 JavaScript 的语法扩展,让你在 JS 代码里写类似 HTML 的结构,本质是 的语法糖。本节从 JSX 与 HTML 的关键差异入手——className、花括号表达式、Fragment、注释写法——再解释 JSX 如何被编译成 JS 调用,以及 背后的 XSS 风险。 你能学到什么 阅读完本节,你应当能够: 写出符合规范的 JSX,正确使用花括号插入 JavaScript 表达式 解释 JSX 与 HTML 的四类关键差异及其原因 说出 JSX 被编译成什么,理解 createElement 调用链 说明 Fragment 的作用,知道何时需要它 描述 JSX 的自动转义机制与 XSS 防护原理 一、问题与直觉:为什么 React
本节摘要:JSX 是 JavaScript 的语法扩展,让你在 JS 代码里写类似 HTML 的结构,本质是
React.createElement的语法糖。本节从 JSX 与 HTML 的关键差异入手——className、花括号表达式、Fragment、注释写法——再解释 JSX 如何被编译成 JS 调用,以及dangerouslySetInnerHTML背后的 XSS 风险。
阅读完本节,你应当能够:
你在写第一个组件时会发现一个奇怪的现象:写的东西看起来像 HTML,但又不是 HTML。比如 HTML 里写 class="app",JSX 里却要写 className="app"。为什么要这样?
答案藏在 JSX 的本质里。JSX 不是模板语言,它是 JavaScript 的一部分——更准确地说,是 React.createElement 的语法糖。既然要编译成 JavaScript 调用,它就得遵守 JavaScript 的规则:class 是 JavaScript 的保留字,不能用;for 也是保留字,所以要写成 htmlFor。
把 JSX 想成「用 HTML 的外表包裹 JavaScript 的表达式语言」,很多怪异规则就说得通了。SOURCE 原文给的定义很准:JSX 是 JavaScript XML 的缩写,是一种允许在 JavaScript 代码中编写类似 HTML 结构的语法扩展。
先看一段最朴素的 JSX:
<h1>Hello, JSX!</h1>
这段代码在浏览器里没法直接运行。它经过 Babel 编译后会变成:
React.createElement("h1", null, "Hello, JSX!");
React.createElement 接收三个参数:元素类型、属性对象、子元素。它返回一个描述「界面上该有什么」的普通 JavaScript 对象——这个对象就是虚拟 DOM 节点(第 2.1 节详述)。
嵌套结构同理。下面这段 JSX:
<div className="container"> <h1>Hello</h1> <p>World</p> </div>
会被编译成嵌套的 createElement 调用,形成一个树状结构。每层对应一个虚拟 DOM 节点,最终组合成一棵完整的虚拟 DOM 树。
值得注意的一点:JSX 里的花括号 {} 用于插入 JavaScript 表达式。字符串字面量可以直接写在标签里,但数字、变量、函数调用、对象都要包花括号:
const name = 'Alice'; const element = <h1>Hello, {name}!</h1>; // 变量 const count = <p>{1 + 1}</p>; // 表达式 const url = 'https://example.com/img.png'; const img = <img src={url} alt="示例" />; // 动态属性
规则只有一条:JSX 里凡是需要「运行 JavaScript」的地方,都用花括号包起来。花括号里的内容可以是任意 JavaScript 表达式——三元运算、函数调用、map 遍历都行,但不能再写花括号开头的对象字面量以外的语句(比如 if、for 不能直接写进去,要写在函数体里)。
为了直观看到 JSX 编译成什么,我们把上面的 JSX 手动展开成对应的虚拟 DOM 结构。这一步能帮你建立「JSX → 对象树」的直觉:
// JSX: <div><h1>Hello</h1><p>World</p></div> // 编译后(简化示意): { type: 'div', props: {}, children: [ { type: 'h1', props: {}, children: ['Hello'] }, { type: 'p', props: {}, children: ['World'] } ] }
这个对象就是 React 眼中「界面该长什么样」。它轻量、可比较、可复用——虚拟 DOM 的底座就是它。

这张图把从「源码里的 JSX」到「屏幕上像素」的完整链路画了出来:JSX 经编译变成 createElement 调用,调用产出虚拟 DOM 树,React 在内存里 Diff 后把差异应用到真实 DOM,最后浏览器渲染。箭头旁边标注了每一步的产物类型,对应你调试时能看到的东西——源码、编译产物、内存对象、DOM 树。
这一节是实操中报错率最高的部分,逐条过。
原因上面说过:class 是 JavaScript 保留字。类似的还有 for → htmlFor、tabindex → tabIndex。
HTML 里 style 是字符串,JSX 里是对象,属性名用驼峰:
<div style={{ backgroundColor: 'lightblue', fontSize: '16px' }}> 样式对象 </div>
外层花括号是「JSX 里写 JS」,内层花括号是「对象字面量」。CSS 属性名从 kebab-case(background-color)变成驼峰(backgroundColor),长度单位数字通常要带单位字符串。
HTML 里写 <!-- 注释 -->,JSX 里不行。JSX 的注释有两种合法写法:
<div> {/* 这是 JSX 注释 */} <h1>Hello</h1> {// 这也是注释 } <p>World</p> </div>
记住一条:注释本质是 JavaScript 表达式,所以必须包花括号。
组件返回的 JSX 只能有一个根元素。想返回多个并列元素而不引入多余的包裹节点,用 Fragment:
function MyComponent() { return ( <> <h1>Title</h1> <p>Content</p> </> ); }
<> 是 <React.Fragment> 的简写。它不产生真实的 DOM 节点,只把子元素组合在一起。好处是 DOM 结构干净——想想表格布局里多套一层 div 的痛,Fragment 就是为这个设计的。
| 对比项 | HTML | JSX | 原因 |
|---|---|---|---|
| class | class="a" |
className="a" |
class 是 JS 保留字 |
| style | 字符串 | 对象 + 驼峰 | 编译进 JS 对象 |
| 注释 | <!-- --> |
{/* */} |
注释是表达式 |
| 多根元素 | 允许 | 需 Fragment | 树结构要求单根 |
| 属性名 | kebab-case | camelCase | 贴近 JS 习惯 |
JSX 会自动转义嵌入的值。什么意思?你在 JSX 里写 {userInput},如果用户输入里有 <script>,React 会把它转义成普通文本,而不是当成 HTML 执行。这条防线挡住了绝大多数 XSS(跨站脚本)攻击。
但有些场景你确实需要渲染原始 HTML,比如后端返回的一段富文本。React 提供了 dangerouslySetInnerHTML:
function MyComponent({ htmlContent }) { return ( <div dangerouslySetInnerHTML={{ __html: htmlContent }} /> ); }
注意属性名里的两个词:dangerously(危险地)和 innerHTML(HTML 注入)。这是 React 在明确提醒你:跳过转义有风险。只有当 htmlContent 来源可信(比如你自己后端处理过的内容)时才用,否则等于把 XSS 大门敞开。
⚠️ 常见坑:把用户输入的 HTML 直接塞进 dangerouslySetInnerHTML。正确做法是先用白名单清洗(去 script、去事件属性),再渲染。千万不要因为「演示能跑」就放松——XSS 是真实生产事故的常客。
💡 关键直觉:JSX 默认的转义是免费的安全网,别主动拆了它。每当你产生「这里要渲染原始 HTML」的念头,先问一句:来源可信吗?这是 React 少有的「真危险」的 API,其余大部分写法顶多是性能问题,这个是安全问题。
JSX 里不能直接写 if,但三元运算符和逻辑与完全可以。这是 JSX 表达式特性带来的便利,也是它和模板引擎(如字符串拼接 HTML)最大的区别——条件逻辑和界面结构写在同一个表达式里,读起来是一条完整的因果链:条件成立则渲染什么,不成立则渲染什么。
function Greeting({ isLoggedIn }) { return ( <div> {isLoggedIn ? <h1>欢迎回来</h1> : <h1>请登录</h1>} {isLoggedIn && <p>今天有什么新鲜事?</p>} </div> ); }
&& 的用法值得说明:{isLoggedIn && <p>...</p>} 在 isLoggedIn 为真时渲染后面的元素,为假时渲染 false(React 会忽略布尔值的渲染)。但注意,如果左边是数字 0,React 会渲染出「0」——这是新手常踩的坑,用布尔值或三元来规避。
用 map 把数组变成 JSX 列表,每个元素要有唯一的 key:
function ListComponent({ items }) { return ( <ul> {items.map((item, index) => ( <li key={item.id}>{item.name}</li> ))} </ul> ); }
key 的作用和选择原则(用 id 而非 index)在第 2.1 节 Diff 算法里深入讲。这里先记住:key 必须唯一、稳定,用数据自带的 id 最好。
JSX 里事件用驼峰命名,值传函数引用而非字符串:
function ButtonComponent() { const handleClick = () => { alert('Button clicked!'); }; return <button onClick={handleClick}>点我</button>; }
注意传给 onClick 的是 handleClick 而不是 handleClick()——后者是立刻执行。这点在事件处理章节会反复强调。
为什么 JSX 不能在浏览器直接运行?
因为 JSX 不是标准 JavaScript,需要 Babel 编译成 createElement 调用。所有脚手架都在构建阶段做了这一步。
Fragment 有什么实际好处?
避免引入多余 DOM 节点。对样式布局、表格结构、无效 HTML 嵌套都有影响。比如在表格里,<tr> 之间的包裹层必须是 <tbody> 或另一个 <tr>,多一个 <div> 就破坏结构。
JSX 里能写对象字面量吗?
能,但要双重花括号:<div style={{color: 'red'}}>。外层是「JSX 里的 JS」,内层是对象本身。
大括号里能写什么?
任何 JavaScript 表达式:变量、运算、三元、函数调用、map 返回的数组。不能直接写 if、for 等语句。
展开运算符能用吗?
可以,这是高频用法。把 props 对象展开传给组件:
function App() { const user = { name: 'Alice', age: 30 }; return <Profile {...user} />; }
展开后等价于 <Profile name="Alice" age={30} />。但要注意覆盖顺序:写在展开之后的属性会覆盖展开来的同名属性。反过来,写在展开之前则会被展开值覆盖,这个先后关系记错会导致 props 静默丢失,是隐蔽 bug 的来源。
JSX 真正强大的地方在于和组件组合一起用。SOURCE 原文演示了把多个小组件拼成一个大组件的写法:
function Title({ text }) { return <h1>{text}</h1>; } function Message({ content }) { return <p>{content}</p>; } function CombinedComponent() { return ( <div> <Title text="欢迎" /> <Message content="这是组合出来的组件。" /> </div> ); }
这种「小组件嵌套进大组件」的写法是 React 组件的常态。你会在第 1.4 节组件基础和第 3.5 节组件设计模式里看到更系统的展开。这里只强调一个 JSX 层面的直觉:JSX 里 <Title text="欢迎" /> 中的标签大写开头,是「这是组件」的信号;小写开头如 <div>,是「这是原生元素」的信号。 这个大小写约定是 React 区分组件与 DOM 元素的方式,写错了 React 会把它当原生元素处理,行为完全不对。
把本节知识串成一个完整组件,覆盖变量、属性、条件、列表、事件五种用法:
function TodoList({ todos, onToggle }) { const doneCount = todos.filter(t => t.done).length; return ( <div className="todo-app"> <h1>待办清单</h1> <p>已完成 {doneCount} / {todos.length}</p> <ul> {todos.map(todo => ( <li key={todo.id} onClick={() => onToggle(todo.id)} style={{ textDecoration: todo.done ? 'line-through' : 'none' }} > {todo.done ? '✓ ' : ''}{todo.text} </li> ))} </ul> </div> ); }
这一小段几乎囊括了 JSX 的所有知识点:花括号里的表达式(filter 计数)、map 生成列表、key、style 对象、三元条件、onClick 传函数、文本里的 ✓。把它敲一遍,比读十遍语法规则都管用。
💡 关键直觉:JSX 是「HTML 外表、JS 内核」的表达式。判断一个写法是否合法,问自己一句话:这句话编译成 createElement 调用后还能成立吗?能,就合法。
JSX 有一个 HTML 里不存在、但极其常用的机制:子元素会作为 children 传给组件。看这个例子:
function Card(props) { return <div className="card">{props.children}</div>; } // 使用: function App() { return ( <Card> <h1>卡片标题</h1> <p>卡片内容</p> </Card> ); }
Card 组件本身不关心里面放了什么,只负责把 children 原样渲染在卡片容器里。这就是组合模式的起点——容器组件与内容组件解耦,同一个 Card 可以被塞进任何内容。props.children 可以是单个元素、多个元素、字符串甚至函数,它的灵活性在第 3.5 节组件设计模式里会充分体现。
这个机制也解释了为什么 JSX 强调单根元素:children 本身是一个内容集合,如果允许 JSX 返回多个并列根元素,children 的定位就会模糊。Fragment 的存在,就是在「必须单根」与「不想多包一层」之间找的平衡点。
关于 children 还有一个细节值得留意:它是被惰性求值的。只有当组件真正渲染时,children 里的表达式才会执行。这意味着你可以把耗时的子元素构建逻辑放到 children 位置,组件不渲染它就不会执行——这个特性在性能优化(第 3.1 节)里会再次出现。
一个元素是组件还是原生 DOM,取决于标签名的首字母:
| 写法 | 身份 | React 如何处理 |
|---|---|---|
<div> 小写 |
原生元素 | createElement 的 type 是字符串 |
<Profile> 大写 |
React 组件 | createElement 的 type 是函数/类 |
这个约定初学者容易忽略。把组件名写成小写,React 会当作原生标签渲染,页面一片空白且无报错——排查起来很头疼。遇到「组件没渲染出来」的灵异问题,先检查标签是否大写。
下一节进入组件基础——JSX 只是「界面怎么写」,组件才是「界面怎么组织」。props 与 state 的分工将在这里真正落地,你也会第一次写出同时含状态和交互的完整组件。