1.3 JSX 语法


文档摘要

1.3 JSX 语法 本节摘要:JSX 是 JavaScript 的语法扩展,让你在 JS 代码里写类似 HTML 的结构,本质是 的语法糖。本节从 JSX 与 HTML 的关键差异入手——className、花括号表达式、Fragment、注释写法——再解释 JSX 如何被编译成 JS 调用,以及 背后的 XSS 风险。 你能学到什么 阅读完本节,你应当能够: 写出符合规范的 JSX,正确使用花括号插入 JavaScript 表达式 解释 JSX 与 HTML 的四类关键差异及其原因 说出 JSX 被编译成什么,理解 createElement 调用链 说明 Fragment 的作用,知道何时需要它 描述 JSX 的自动转义机制与 XSS 防护原理 一、问题与直觉:为什么 React

1.3 JSX 语法

本节摘要:JSX 是 JavaScript 的语法扩展,让你在 JS 代码里写类似 HTML 的结构,本质是 React.createElement 的语法糖。本节从 JSX 与 HTML 的关键差异入手——className、花括号表达式、Fragment、注释写法——再解释 JSX 如何被编译成 JS 调用,以及 dangerouslySetInnerHTML 背后的 XSS 风险。

你能学到什么

阅读完本节,你应当能够:

  1. 写出符合规范的 JSX,正确使用花括号插入 JavaScript 表达式
  2. 解释 JSX 与 HTML 的四类关键差异及其原因
  3. 说出 JSX 被编译成什么,理解 createElement 调用链
  4. 说明 Fragment 的作用,知道何时需要它
  5. 描述 JSX 的自动转义机制与 XSS 防护原理

一、问题与直觉:为什么 React 要造一套「类 HTML」

你在写第一个组件时会发现一个奇怪的现象:写的东西看起来像 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 被编译成什么

先看一段最朴素的 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 的转换细节

值得注意的一点: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 编译与渲染链路

这张图把从「源码里的 JSX」到「屏幕上像素」的完整链路画了出来:JSX 经编译变成 createElement 调用,调用产出虚拟 DOM 树,React 在内存里 Diff 后把差异应用到真实 DOM,最后浏览器渲染。箭头旁边标注了每一步的产物类型,对应你调试时能看到的东西——源码、编译产物、内存对象、DOM 树。

三、JSX 与 HTML 的关键差异

这一节是实操中报错率最高的部分,逐条过。

className 而不是 class

原因上面说过:class 是 JavaScript 保留字。类似的还有 forhtmlFortabindextabIndex

style 接收对象

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 习惯

四、安全:自动转义与 dangerouslySetInnerHTML

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 的常见场景

条件渲染

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 的配合

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 会把它当原生元素处理,行为完全不对。

八、一段完整的 JSX 练习

把本节知识串成一个完整组件,覆盖变量、属性、条件、列表、事件五种用法:

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 调用后还能成立吗?能,就合法。

九、props.children:JSX 的隐藏通道

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 节)里会再次出现。

JSX 中元素的两种身份

一个元素是组件还是原生 DOM,取决于标签名的首字母:

写法 身份 React 如何处理
<div> 小写 原生元素 createElement 的 type 是字符串
<Profile> 大写 React 组件 createElement 的 type 是函数/类

这个约定初学者容易忽略。把组件名写成小写,React 会当作原生标签渲染,页面一片空白且无报错——排查起来很头疼。遇到「组件没渲染出来」的灵异问题,先检查标签是否大写。

核心回顾

  • JSX 本质:createElement 的语法糖,编译后产出虚拟 DOM 节点对象
  • 花括号规则:插入 JavaScript 表达式的地方都用花括号
  • className 与驼峰:class/for 是保留字,style 用对象且驼峰命名
  • 单根元素:多个并列元素用 Fragment 包裹,不产生多余节点
  • 自动转义:JSX 默认转义值防 XSS,dangerouslySetInnerHTML 是例外且危险
  • key 属性:列表渲染必须有唯一、稳定的 key
  • 事件传引用:onClick 传函数名而非调用结果
  • props.children:子元素作为隐藏通道传入,是组合模式的基础
  • 大小写约定:大写开头是组件,小写开头是原生元素,写错会静默失败
  • 展开运算符:可用于批量传 props,注意覆盖顺序

下一节进入组件基础——JSX 只是「界面怎么写」,组件才是「界面怎么组织」。props 与 state 的分工将在这里真正落地,你也会第一次写出同时含状态和交互的完整组件。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U