JSX 语法详解
JSX 是 JavaScript 的语法扩展,它让你可以在 JavaScript 文件中编写类似 HTML 的标记。虽然 React 不强制使用 JSX,但它让编写 UI 代码更直观,且能更好地利用 JavaScript 的全部能力。
1. 为什么使用 JSX?
在传统的 Web 开发中,HTML、CSS 和 JavaScript 通常分离在不同的文件中。但随着 Web 应用变得越来越交互化,逻辑越来越决定内容,JavaScript 开始接管 HTML 的渲染工作。
React 选择将渲染逻辑和标记放在一起,这就是组件的核心思想。JSX 让这种组合变得更加自然:
function Greeting() { const name = "React 19"; const element = ( <h1 style={{ color: '#2563eb' }}> 你好,{name}! </h1> ); return ( <div style={{ padding: '20px', border: '1px solid #e2e8f0', borderRadius: '12px', background: '#f8fafc' }}> {element} <p>这是用 JSX 定义的元素。</p> </div> ); }
将按钮的渲染逻辑和标记放在一起,确保每次编辑时它们保持同步。相反,不相关的细节(如按钮的标记和侧边栏的标记)相互隔离,使得单独修改任何一个都更安全。
2. JSX 的本质
JSX 看起来像 HTML,但底层它会被转换为普通的 JavaScript 对象。理解这一点对掌握 React 非常重要。
2.1 JSX 编译为 createElement
当你编写 JSX 代码时,构建工具(如 Babel)会将其转换为 React.createElement 调用:
// 你写的 JSX
const element = <h1 className="greeting">Hello, world!</h1>;
// 编译后的代码
const element = React.createElement(
'h1',
{ className: 'greeting' },
'Hello, world!'
);
createElement 函数返回一个描述 UI 的普通 JavaScript 对象,称为 React 元素:
// React 元素的结构(简化版)
{
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
},
key: null,
ref: null
}
关键点:创建这个对象并不会立即渲染组件或创建 DOM 元素。React 元素更像是一个描述、一个指令,告诉 React 接下来该做什么。创建元素的开销极小,你不需要尝试优化或避免它。
2.2 为什么不用模板字符串?
很多框架使用模板字符串(如 Vue 的模板),但 React 选择 JSX 的原因是:
- JavaScript 的全部能力:你可以在 JSX 中使用任何 JavaScript 语法
- 类型安全:编译时就能发现错误
- IDE 支持:代码补全、类型检查更好
- 表达能力:复杂的逻辑可以用熟悉的 JavaScript 表达
3. JSX 的三条规则
3.1 返回单个根元素
组件必须返回单个根元素。如果你有多个元素,需要用容器包裹:
function MultipleElements() { return ( <div style={{ padding: '10px', background: '#f0f9ff' }}> <h3>使用 div 包裹</h3> <p>多个元素需要放在一个父元素中。</p> </div> ); }
如果你不想添加额外的 DOM 节点,可以使用 Fragment:
function FragmentDemo() { return ( <> <h3 style={{ color: '#059669' }}>使用 Fragment</h3> <p style={{ color: '#64748b' }}>Fragment 不会在 DOM 中留下痕迹。</p> </> ); }
Fragment 让你分组元素而不留下任何 HTML 痕迹。你也可以使用显式的 <React.Fragment> 语法:
import { Fragment } from 'react';
function FragmentWithKey({ items }) {
return (
<dl>
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</Fragment>
))}
</dl>
);
}
注意:短语法 <>...</> 不支持传递 key,如果需要 key,必须使用 <Fragment> 。
3.2 正确关闭所有标签
JSX 要求所有标签必须显式关闭,包括自闭合标签:
function TagClosing() { return ( <div style={{ textAlign: 'center' }}> {/* 自闭合标签必须有 / */} <img src="https://react.dev/images/og-home.png" alt="React Logo" style={{ width: '100px', borderRadius: '8px' }} /> {/* 所有标签都需要关闭 */} <p style={{ fontSize: '12px', color: '#666', marginTop: '8px' }}> 图片使用自闭合标签语法 </p> </div> ); }
在 HTML 中,像 <img> 、<br> 、<input> 这样的标签可以不关闭,但在 JSX 中必须写成 <img /> 、<br /> 。
3.3 使用 camelCase 命名属性
JSX 属性使用 camelCase(驼峰命名法),而不是 HTML 的 kebab-case:
function CamelCaseDemo() { const divStyle = { padding: '20px', backgroundColor: '#ec4899', // 不是 background-color color: 'white', borderRadius: '8px', textAlign: 'center', fontWeight: 'bold' }; return ( <div style={divStyle} className="my-cool-box"> <p>className 而不是 class</p> <p>htmlFor 而不是 for</p> <p>onClick 而不是 onclick</p> <input type="text" tabIndex={1} // 不是 tabindex readOnly // 不是 readonly style={{ marginTop: '10px', padding: '5px' }} /> </div> ); }
常用属性对照表:
| HTML 属性 | JSX 属性 |
|---|---|
class | className |
for | htmlFor |
tabindex | tabIndex |
readonly | readOnly |
maxlength | maxLength |
onclick | onClick |
onchange | onChange |
stroke-width | strokeWidth |
4. 大括号:JavaScript 的窗口
大括号 {} 是 JSX 中嵌入 JavaScript 表达式的关键。你可以在两个地方使用大括号:
4.1 作为标签内的文本
function TextContent() { const name = 'Gregorio Y. Zara'; return ( <h1 style={{ color: '#1e40af' }}> {name} 的待办事项 </h1> ); }
4.2 作为属性值紧跟 = 号
function AttributeValue() { const avatar = 'https://i.imgur.com/7vQD0fPs.jpg'; const description = '科学家头像'; return ( <img className="avatar" src={avatar} alt={description} style={{ width: '100px', borderRadius: '50%' }} /> ); }
4.3 可以在大括号中放什么?
任何有效的 JavaScript 表达式都可以:
function ExpressionDemo() { const a = 10; const b = 25; const user = { firstName: 'Harper', lastName: 'Perez' }; const formatUser = (user) => `${user.firstName} ${user.lastName}`; const today = new Date(); const formatDate = (date) => new Intl.DateTimeFormat('zh-CN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }).format(date); return ( <div style={{ padding: '15px', background: '#f8fafc', borderRadius: '8px' }}> <p><strong>算术运算:</strong>10 + 25 = {a + b}</p> <p><strong>函数调用:</strong>欢迎, {formatUser(user)}!</p> <p><strong>对象属性:</strong>{user.firstName}</p> <p><strong>日期格式化:</strong>{formatDate(today)}</p> <p><strong>三元表达式:</strong>{a > b ? 'a 大于 b' : 'a 不大于 b'}</p> </div> ); }
注意:大括号内必须是表达式,不能是语句。像 if 语句、for 循环这样的语句不能直接放在大括号中。
4.4 双大括号:样式对象
当传递样式对象时,你会看到双大括号 {{ }} 。这不是特殊语法,只是一个对象包裹在 JSX 大括号中:
function DoubleBraces() { return ( <ul style={{ backgroundColor: '#1e293b', color: '#f8fafc', padding: '15px 25px', borderRadius: '8px' }}> <li>外层大括号:JSX 语法</li> <li>内层大括号:JavaScript 对象</li> </ul> ); }
等价于:
const theme = {
backgroundColor: '#1e293b',
color: '#f8fafc',
padding: '15px 25px',
borderRadius: '8px'
};
return <ul style={theme}>...</ul>;
5. 条件渲染
React 使用 JavaScript 的语法来控制条件渲染,而不是特殊的模板语法。
5.1 使用 if 语句
最直接的方式是在组件函数内部使用 if 语句:
function PackingItem({ name, isPacked }) { if (isPacked) { return <li className="item" style={{ color: '#10b981' }}>{name} ✅</li>; } return <li className="item">{name}</li>; } function PackingList() { return ( <section> <h3 style={{ marginBottom: '10px' }}>行李清单</h3> <ul style={{ listStyle: 'none', padding: 0 }}> <PackingItem isPacked={true} name="宇航服" /> <PackingItem isPacked={true} name="带金叶的头盔" /> <PackingItem isPacked={false} name="家人的照片" /> </ul> </section> ); }
5.2 返回 null 不渲染任何内容
如果不想渲染任何东西,可以返回 null :
function ConditionalNull({ isPacked, name }) { if (isPacked) { return null; // 不渲染任何内容 } return <li style={{ padding: '5px 0' }}>{name}(未打包)</li>; } function NullDemo() { return ( <ul style={{ listStyle: 'none', padding: 0 }}> <ConditionalNull isPacked={true} name="已打包的物品" /> <ConditionalNull isPacked={false} name="未打包的物品" /> </ul> ); }
返回 null 在实践中并不常见,因为它可能让使用组件的开发者感到困惑。更多时候,你会在父组件中条件性地包含或排除组件。
5.3 三元运算符 ? :
当你需要在两种结果之间选择时,三元运算符很简洁:
function TernaryItem({ name, isPacked }) { return ( <li className="item"> {isPacked ? ( <del style={{ color: '#94a3b8' }}>{name} ✅</del> ) : ( <span style={{ color: '#0ea5e9' }}>{name}</span> )} </li> ); } function TernaryDemo() { return ( <ul style={{ listStyle: 'none', padding: 0 }}> <TernaryItem isPacked={true} name="宇航服" /> <TernaryItem isPacked={false} name="家人的照片" /> </ul> ); }
三元运算符可以理解为:"如果条件为真,则渲染 A,否则渲染 B"。
5.4 逻辑与运算符 &&
当你只想在条件为真时渲染内容,否则什么都不渲染时:
function AndItem({ name, isPacked }) { return ( <li className="item"> {name} {isPacked && '✅'} </li> ); } function AndDemo() { return ( <ul style={{ listStyle: 'none', padding: 0 }}> <AndItem isPacked={true} name="宇航服" /> <AndItem isPacked={false} name="家人的照片" /> </ul> ); }
重要警告:&& 左边的条件如果是 0 ,React 会渲染 0 而不是什么都不渲染:
function AndWarning() { const unreadCount = 0; return ( <div style={{ padding: '10px', background: '#fef2f2', borderRadius: '8px' }}> <h4 style={{ color: '#dc2626' }}>常见陷阱</h4> {/* 有问题的写法:当 count 为 0 时会显示 0 */} <p style={{ color: '#ef4444' }}> 有问题:{unreadCount && <span>{unreadCount} 条未读消息</span>} </p> {/* 正确的写法:使用三元运算符 */} <p style={{ color: '#10b981' }}> 正确:{unreadCount > 0 && <span>{unreadCount} 条未读消息</span>} </p> {/* 或者更明确的三元运算符 */} <p> 推荐:{unreadCount > 0 ? `${unreadCount} 条未读消息` : '没有新消息'} </p> </div> ); }
5.5 将 JSX 赋值给变量
当条件逻辑变得复杂时,将 JSX 提取到变量中会让代码更清晰:
function VariableItem({ name, isPacked }) { let itemContent = name; if (isPacked) { itemContent = ( <del style={{ color: '#94a3b8' }}> {name} ✅ </del> ); } return ( <li className="item" style={{ padding: '8px 0', borderBottom: '1px solid #e2e8f0' }}> {itemContent} </li> ); } function VariableDemo() { return ( <ul style={{ listStyle: 'none', padding: 0 }}> <VariableItem isPacked={true} name="宇航服" /> <VariableItem isPacked={true} name="带金叶的头盔" /> <VariableItem isPacked={false} name="家人的照片" /> </ul> ); }
选择建议:
- 简单条件:使用
&&或三元运算符 - 复杂条件:使用
if语句或提取到变量 - 两种结果:三元运算符
? : - 一种结果:
&&运算符
6. 列表渲染
React 使用 JavaScript 的 map() 方法来渲染列表。
6.1 基本列表渲染
function BasicList() { const people = [ '张三:前端工程师', '李四:后端工程师', '王五:产品经理' ]; const listItems = people.map(person => ( <li style={{ padding: '8px', background: '#f8fafc', margin: '4px 0', borderRadius: '4px' }}> {person} </li> )); return ( <ul style={{ listStyle: 'none', padding: 0 }}> {listItems} </ul> ); }
6.2 使用结构化数据
实际项目中,数据通常是对象数组:
function StructuredList() { const products = [ { id: 1, name: 'iPhone 15', price: 6999, category: '手机' }, { id: 2, name: 'MacBook Pro', price: 14999, category: '电脑' }, { id: 3, name: 'AirPods Pro', price: 1899, category: '配件' } ]; return ( <ul style={{ listStyle: 'none', padding: 0 }}> {products.map(product => ( <li key={product.id} style={{ padding: '12px', background: '#f8fafc', margin: '8px 0', borderRadius: '8px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div> <span style={{ fontWeight: 'bold' }}>{product.name}</span> <span style={{ marginLeft: '10px', fontSize: '12px', background: '#e0f2fe', padding: '2px 8px', borderRadius: '12px' }}> {product.category} </span> </div> <span style={{ color: '#059669', fontWeight: 'bold' }}>¥{product.price}</span> </li> ))} </ul> ); }
6.3 使用 filter 过滤列表
你可以组合 filter() 和 map() 来过滤数据:
function FilterList() { const products = [ { id: 1, name: 'iPhone 15', price: 6999, category: '手机', inStock: true }, { id: 2, name: 'MacBook Pro', price: 14999, category: '电脑', inStock: true }, { id: 3, name: 'AirPods Pro', price: 1899, category: '配件', inStock: false }, { id: 4, name: 'iPad Air', price: 4799, category: '平板', inStock: true } ]; const inStockProducts = products.filter(p => p.inStock); return ( <div> <h4 style={{ marginBottom: '10px' }}>有库存的商品</h4> <ul style={{ listStyle: 'none', padding: 0 }}> {inStockProducts.map(product => ( <li key={product.id} style={{ padding: '10px', background: '#f0fdf4', margin: '4px 0', borderRadius: '4px' }}> {product.name} - ¥{product.price} </li> ))} </ul> </div> ); }
6.4 Key 的正确使用
Key 帮助 React 识别哪些元素发生了变化、添加或删除。
Key 的规则:
- 兄弟节点中唯一:同一列表中的 key 不能重复
- 稳定不变:key 不应该在重新渲染时改变
- 不要使用索引:除非列表是静态的、不会重新排序
function KeyDemo() { const [items, setItems] = useState([ { id: 'a1', text: '第一条' }, { id: 'b2', text: '第二条' }, { id: 'c3', text: '第三条' } ]); const shuffleItems = () => { setItems([...items].reverse()); }; return ( <div> <button onClick={shuffleItems} style={{ marginBottom: '10px', padding: '8px 16px' }} > 反转列表 </button> <ul style={{ listStyle: 'none', padding: 0 }}> {items.map(item => ( <li key={item.id} style={{ padding: '10px', background: '#fef3c7', margin: '4px 0', borderRadius: '4px' }}> <span style={{ color: '#92400e', fontFamily: 'monospace' }}> key={item.id} </span> {' - '}{item.text} </li> ))} </ul> </div> ); }
为什么索引作为 key 有问题?
想象一下,如果桌面上的文件没有名称,只能按顺序称呼它们——第一个文件、第二个文件……一旦删除一个文件,就会变得混乱。第二个文件变成第一个文件,第三个变成第二个……
文件夹中的文件名和数组中的 JSX key 服务于类似的目的。它们让我们在兄弟节点中唯一标识一项。一个精心选择的 key 提供的信息比数组中的位置更多。即使位置因重新排序而改变,key 也让 React 在整个生命周期中识别该项。
// 推荐:使用稳定的唯一 ID
items.map(item => <Item key={item.id} item={item} />)
// 避免:使用索引(仅在项目没有 ID 且不会重新排序时使用)
items.map((item, index) => <Item key={index} item={item} />)
// 错误:使用随机值或时间戳(每次渲染都会变化)
items.map(item => <Item key={Math.random()} item={item} />)
6.5 Key 的来源
不同来源的数据提供不同的 key:
- 数据库数据:使用数据库的主键/ID
- 本地生成数据:使用递增计数器、
crypto.randomUUID()或uuid库 - 静态数据:可以使用索引(但最好还是有唯一标识)
7. 安全性:防止注入攻击
React 会自动转义嵌入在 JSX 中的所有内容,这可以防止 XSS(跨站脚本攻击):
function SecurityDemo() { // 假设这是来自用户输入或 API 的内容 const userInput = '<script>alert("XSS攻击")</script>'; return ( <div style={{ padding: '15px', background: '#f0fdf4', borderRadius: '8px' }}> <h4 style={{ color: '#166534' }}>React 自动转义</h4> <p>用户输入:{userInput}</p> <p style={{ fontSize: '14px', color: '#64748b' }}> 上面的脚本标签被自动转义,不会执行。 </p> </div> ); }
在渲染之前,所有值都会被转换为字符串。这确保了你永远不会注入那些你没有明确写在应用中的内容。
如果你确实需要渲染 HTML 内容,可以使用 dangerouslySetInnerHTML 属性,但要非常小心:
// 仅在绝对必要时使用,且确保内容已消毒
<div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
8. 高级用法
8.1 展开运算符传递 Props
当 props 很多时,可以使用展开运算符:
function SpreadProps() { const buttonProps = { className: 'btn', style: { padding: '10px 20px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer' }, onClick: () => alert('按钮被点击!') }; return ( <div> <p style={{ marginBottom: '10px' }}>使用展开运算符传递多个 props:</p> <button {...buttonProps}> 点击我 </button> </div> ); }
8.2 条件渲染样式
function ConditionalStyle() { const [isActive, setIsActive] = useState(false); return ( <div onClick={() => setIsActive(!isActive)} style={{ padding: '20px', background: isActive ? '#22c55e' : '#e2e8f0', color: isActive ? 'white' : '#1e293b', borderRadius: '8px', cursor: 'pointer', transition: 'all 0.2s' }} > 点击切换状态:{isActive ? '激活' : '未激活'} </div> ); }
8.3 嵌套列表
function NestedList() { const data = [ { id: 1, category: '前端', items: ['React', 'Vue', 'Angular'] }, { id: 2, category: '后端', items: ['Node.js', 'Python', 'Java'] } ]; return ( <div> {data.map(group => ( <div key={group.id} style={{ marginBottom: '15px' }}> <h4 style={{ padding: '8px', background: '#e0f2fe', borderRadius: '4px' }}> {group.category} </h4> <ul style={{ listStyle: 'disc', marginLeft: '20px' }}> {group.items.map((item, index) => ( <li key={`${group.id}-${index}`} style={{ padding: '4px 0' }}> {item} </li> ))} </ul> </div> ))} </div> ); }
8.4 使用可选链和空值合并
function SafeAccess() { const user = { name: '张三', address: { city: '北京' // 没有 street 属性 } }; return ( <div style={{ padding: '15px', background: '#faf5ff', borderRadius: '8px' }}> <h4 style={{ color: '#7c3aed' }}>安全访问嵌套属性</h4> <p>用户名:{user.name}</p> <p>城市:{user.address?.city ?? '未知'}</p> <p>街道:{user.address?.street ?? '未填写'}</p> </div> ); }
9. JSX 最佳实践
9.1 保持简洁
// 好的做法:提取复杂逻辑
function UserCard({ user }) {
const fullName = `${user.firstName} ${user.lastName}`;
const isActive = user.status === 'active';
return (
<div className={isActive ? 'active' : 'inactive'}>
{fullName}
</div>
);
}
// 避免在 JSX 中写复杂表达式
function BadExample({ user }) {
return (
<div className={user.status === 'active' ? 'active' : 'inactive'}>
{user.firstName + ' ' + user.lastName}
</div>
);
}
9.2 合理使用 Fragment
// 不需要额外容器时使用 Fragment
function DefinitionList({ items }) {
return (
<dl>
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</Fragment>
))}
</dl>
);
}
9.3 多行 JSX 加括号
// 好的做法:多行 JSX 用括号包裹
function GoodExample() {
return (
<div>
<h1>Title</h1>
<p>Content</p>
</div>
);
}
// 避免:不加括号会导致自动分号问题
function BadExample() {
return // 这会导致错误!
<div>
<h1>Title</h1>
</div>;
}
9.4 组件名大写开头
// 正确:大写开头
function MyComponent() {
return <div>Hello</div>;
}
// 错误:小写开头会被当作 HTML 标签
function myComponent() {
return <div>Hello</div>;
}
小结
本章我们学习了 JSX 的核心知识:
基础语法
- JSX 是 JavaScript 的语法扩展,会被编译为
createElement调用 - 必须返回单个根元素
- 所有标签必须正确关闭
- 属性使用 camelCase 命名
JavaScript 表达式
- 大括号
{}用于嵌入 JavaScript 表达式 - 可以用于文本内容和属性值
- 双大括号
{{ }}用于传递样式对象
条件渲染
- 使用
if语句返回不同的 JSX - 三元运算符
? :用于二选一 &&运算符用于条件渲染(注意 0 的问题)- 复杂条件可以提取到变量
列表渲染
- 使用
map()方法渲染列表 - Key 必须在兄弟节点中唯一且稳定
- 避免使用索引作为 key(除非列表静态)
安全性
- React 自动转义内容,防止 XSS 攻击
- 慎用
dangerouslySetInnerHTML
练习
- 创建一个组件,根据时间(上午/下午/晚上)显示不同的问候语
- 实现一个简单的任务列表,支持添加和删除任务
- 使用
filter()创建一个可以根据分类过滤商品的组件 - 实现一个表格组件,展示用户数据,并支持按字段排序