3.2 属性操作:读写病灶档案 每个元素身上都挂着三本账:HTML 标签上写的是特性(attribute),JavaScript 对象上存在的是属性(property),开发者自定义的是数据(data)。三本账多数时候记着相同的数,但在勾选框、输入框这类控件身上,它们会各记各的——分不清这三本账,"为什么我改了属性页面却没反应"这类悬案就破不了。本节按账本逐个讲清读取与写入的正确姿势。 本节摘要: 读写 HTML 特性,反映标签的初始状态; 读写 DOM 属性,反映运行时的当前状态; 读写挂在元素上的私有数据,优先取自 data- 特性但有缓存陷阱; 是表单取值的专用通道。布尔类控件一律用 ,自定义信息一律用 。
每个元素身上都挂着三本账:HTML 标签上写的是特性(attribute),JavaScript 对象上存在的是属性(property),开发者自定义的是数据(data)。三本账多数时候记着相同的数,但在勾选框、输入框这类控件身上,它们会各记各的——分不清这三本账,"为什么我改了属性页面却没反应"这类悬案就破不了。本节按账本逐个讲清读取与写入的正确姿势。
本节摘要:
attr读写 HTML 特性,反映标签的初始状态;prop读写 DOM 属性,反映运行时的当前状态;data读写挂在元素上的私有数据,优先取自 data- 特性但有缓存陷阱;val是表单取值的专用通道。布尔类控件一律用prop,自定义信息一律用data。
翻开控制台做个实验,三行代码胜过千言:
<input type="checkbox" id="agree" checked>
var box = document.getElementById('agree'); box.getAttribute('checked'); // 'checked':特性账本,记录标签上写了什么 box.checked; // true:属性账本,记录当前状态 box.checked = false; box.getAttribute('checked'); // 仍是 'checked':特性账本没动 box.checked; // false:属性账本已更新
用户点了一下勾选框,属性账本立刻改写,特性账本纹丝不动。特性是"出生证明",属性是"实时体征"——页面交互读的永远是后者。jQuery 把这两本账封装成 attr 与 prop:
$('#agree').attr('checked'); // 'checked':特性视图 $('#agree').prop('checked'); // true / false:属性视图 $('#agree').prop('checked', true); // 勾上它:改的是实时状态
| 场景 | 用 attr | 用 prop |
|---|---|---|
checked、selected、disabled、readonly |
不建议 | 一律用它 |
href、src、title、alt、class |
合适 | 少用 |
自定义特性如 data-role |
可以 | 不相关 |

data 账本承接开发者的私有数据。HTML 里写 data-vital="high",jQuery 里就能读:
<li class="item urgent" data-vital="high" data-bed-no="7">张三 · 心悸</li>
$('.urgent').data('vital'); // 'high':自动把连字符转成驼峰 $('.urgent').data('bedNo'); // 7:纯数字串会自动转成数字 $('.urgent').data('vital', 'low'); // 写入:注意,写进的是缓存
陷阱藏在最后一步:.data('vital', 'low') 只改了 jQuery 内部缓存,DOM 上的 data-vital 特性还是 high。此后再用 attr('data-vital') 与 data('vital') 会读出两个不同的答案。原则一句话:一个元素从头到尾只用一种方式读写 data,中途换枪必出灵异事件。需要"特性也同步更新"的场景,老老实实用 attr('data-vital', 'low')。
原生对照是 dataset:el.dataset.vital 读写特性本身、没有缓存层,与 .data() 的差异恰好就是上面那个陷阱。要跟原生保持完全一致的心智,就统一用 attr 系。
$('input[name="guest"]').val(); // 读当前输入 $('input[name="guest"]').val('张三'); // 写入输入框 $('select[name="bed"]').val('b'); // 直接选中三号床对应的选项
反面教材是 attr('value'):它读的是标签上的初始值,用户敲了半天键盘它也看不见。凡是"页面上看起来对、代码里读到旧值"的表单怪象,先查是不是绕了这条远路。
val 的返回值形态取决于控件类型,写校验代码前先对准:
$('input[name="guest"]').val(); // 单行文本:字符串,空输入是 '' $('select[name="bed"]').val(); // 单选下拉:选中项的 value $('select[multiple]').val(); // 多选下拉:数组,空选是 [] $('input[name="symptom"]:checked').val();// 单选组:被选中的那一个的值 $('input[name="extra"]:checked').map(function () { return this.value; }).get(); // 复选组:自己拼数组
两个高频坑都在这上面:判断空文本写 if (v === '') 而不是 if (!v.length)(后者对数组也成立,语义却完全不同);判断多选是否为空要 if (!v.length) 而不是 if (v === '')。复选组没有现成的数组返回,map 加 get 是标准拼法——get 负责把 jQuery 集合拆成原生数组,呼应第 1 章的拆包话题。
removeAttr('disabled') 抹掉整个特性,等价原生 removeAttribute;对布尔状态更推荐 prop('disabled', false)——前者通过删特性间接恢复默认,后者直接写运行时状态,语义更准。removeData('vital') 清的是 jQuery 缓存里那一本,特性上的 data-vital 原样保留,下次再 data('vital') 会回到从特性重新读取——它常被用来做"缓存复位":测试或长驻页面怀疑缓存与特性不一致时,先 removeData() 再读,就能验证两本账是否已经漂移。
背景:标本页的提交按钮要求"勾选知情同意后才可点",一次会诊写了 $('#submit').attr('disabled', !agree)。测试回报:勾上同意后按钮偶尔仍然点不动,多勾一次又好了。
排查:在控制台分别读两本账——attr('disabled') 时而是字符串 'disabled'、时而是 undefined,prop('disabled') 恒定反映真实状态。根因是 attr 这条路对布尔特性的写入行为在版本间有过调整,且字符串与布尔的混用让判断时灵时不灵。修法一行:改走 prop('disabled', !agree),传入的始终是布尔,两本账的歧义从根上消失。解读:布尔类状态的所有权属于属性账本,attr 只负责出生证明——这正是本节账本模型的直接推论。变式:只读场景同理,判断"当前是否禁用"用 is(':disabled') 或 prop('disabled'),别拿 attr 的返回值做条件。
attr),属性是实时体征(prop);勾选、禁用、选中类状态一律走 prop。attr 读出来是字符串,prop 读出来是布尔或对象,别拿字符串跟布尔硬比。data 首次读取后进缓存,写入不再回写特性;同一个键全生命周期只用一种读写方式。val,attr('value') 只是初始快照。getAttribute、el.checked、dataset、el.value 与上述四件一一对应。档案改完还差外观。下一节进入两种缝合针法——类名管批量、内联样式管精修,它们是把状态画到脸上的两条路。