跳到主要内容

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>
  );
}
结果
Loading...

将按钮的渲染逻辑和标记放在一起,确保每次编辑时它们保持同步。相反,不相关的细节(如按钮的标记和侧边栏的标记)相互隔离,使得单独修改任何一个都更安全。


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>
  );
}
结果
Loading...

如果你不想添加额外的 DOM 节点,可以使用 Fragment:

实时编辑器
function FragmentDemo() {
  return (
    <>
      <h3 style={{ color: '#059669' }}>使用 Fragment</h3>
      <p style={{ color: '#64748b' }}>Fragment 不会在 DOM 中留下痕迹。</p>
    </>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

在 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>
  );
}
结果
Loading...

常用属性对照表

HTML 属性JSX 属性
classclassName
forhtmlFor
tabindextabIndex
readonlyreadOnly
maxlengthmaxLength
onclickonClick
onchangeonChange
stroke-widthstrokeWidth

4. 大括号:JavaScript 的窗口

大括号 {} 是 JSX 中嵌入 JavaScript 表达式的关键。你可以在两个地方使用大括号:

4.1 作为标签内的文本

实时编辑器
function TextContent() {
  const name = 'Gregorio Y. Zara';
  
  return (
    <h1 style={{ color: '#1e40af' }}>
      {name} 的待办事项
    </h1>
  );
}
结果
Loading...

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%' }}
    />
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

注意:大括号内必须是表达式,不能是语句。像 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>
  );
}
结果
Loading...

等价于:

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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

返回 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>
  );
}
结果
Loading...

三元运算符可以理解为:"如果条件为真,则渲染 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>
  );
}
结果
Loading...

重要警告&& 左边的条件如果是 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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

选择建议

  • 简单条件:使用 && 或三元运算符
  • 复杂条件:使用 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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

6.4 Key 的正确使用

Key 帮助 React 识别哪些元素发生了变化、添加或删除。

Key 的规则

  1. 兄弟节点中唯一:同一列表中的 key 不能重复
  2. 稳定不变:key 不应该在重新渲染时改变
  3. 不要使用索引:除非列表是静态的、不会重新排序
实时编辑器
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>
  );
}
结果
Loading...

为什么索引作为 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>
  );
}
结果
Loading...

在渲染之前,所有值都会被转换为字符串。这确保了你永远不会注入那些你没有明确写在应用中的内容。

如果你确实需要渲染 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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

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

练习

  1. 创建一个组件,根据时间(上午/下午/晚上)显示不同的问候语
  2. 实现一个简单的任务列表,支持添加和删除任务
  3. 使用 filter() 创建一个可以根据分类过滤商品的组件
  4. 实现一个表格组件,展示用户数据,并支持按字段排序

参考资源