《JavaScript DOM编程艺术》第2版核心案例代码解析与实战

作者:竹影

时间:2026-10-30

浏览:273

深入剖析经典著作《JavaScript DOM编程艺术》(第2版)核心案例实现与架构思想,系统讲解平稳退化、非侵入式脚本、图片库构建、动态动画抽象与无障碍语义提取实战。

文章目录
本书并非简单堆砌API字典,而是确立了现代前端开发最重要的底线思维:平稳退化、结构表现行为三权分离、对象检测与渐进增强。本文聚焦《JavaScript DOM编程艺术》(第2版)中最具代表性的核心案例代码,逐行拆解其设计意图与工程权衡,提炼跨越框架周期的原生DOM编程硬功夫。

一、现代前端的基石底座:三大设计哲学与通用工具

在深入具体案例前,必须明确杰里米·基思(Jeremy Keith)贯穿全书的核心价值观。许多初学者容易直接在HTML标签里书写内联 onclick 属性,或者写出一旦浏览器禁用JavaScript就变成死链接的“伪页面”。第2版开篇即确立了三个不可动摇的技术规范:

  • 平稳退化(Graceful Degradation):确保即便JavaScript彻底未加载或被客户端安全策略禁用,核心内容与导航依然可以通过纯HTML完整访问。
  • 行为分离(Unobtrusive JavaScript):HTML负责内容结构,CSS负责视觉呈现,JavaScript仅通过外部脚本挂载交互,彻底从结构层剔除内联事件。
  • 对象检测与向后兼容(Feature Detection):严禁通过 navigator.userAgent 进行不可靠的浏览器嗅探,使用类似 if (!document.getElementById) return false; 的对象存在性检验确保执行安全。

1. 跨函数共享的事件挂载器:addLoadEvent

浏览器窗口的 window.onload 事件在原生时代只能绑定一个回调函数。如果多个独立的脚本模块直接给 window.onload 赋值,后赋值的函数会无情覆盖之前的逻辑。全书最常用的通用脚手架便是 addLoadEvent:

function addLoadEvent(func) {
  var oldonload = window.onload;
  if (typeof window.onload != 'function') {
    window.onload = func;
  } else {
    window.onload = function() {
      oldonload();
      func();
    }
  }
}

该函数的巧妙之处在于闭包暂存:它把先前的 window.onload 引用保存在 oldonload 中。在触发时,先调用旧任务,再执行新传入的 func,形成一条轻量级任务队列链。

2. 补齐原生缺失的方法:insertAfter 工具函数

原生DOM规范提供了 appendChild(追加在父节点末尾)和 insertBefore(插入在目标节点之前),却唯独没有 insertAfter。全书提供了精巧的经典实现:

function insertAfter(newElement, targetElement) {
  var parent = targetElement.parentNode;
  if (parent.lastChild == targetElement) {
    parent.appendChild(newElement);
  } else {
    parent.insertBefore(newElement, targetElement.nextSibling);
  }
}

如果目标节点恰好是父节点的最后一个子节点,直接 appendChild;若不是,目标节点的下一个兄弟节点 targetElement.nextSibling 一定存在,此时调用 insertBefore 插入到兄弟节点前方,逻辑严格闭环。

《JavaScript DOM编程艺术》第2版核心案例代码解析与实战
图 1:DOM 节点树结构与非侵入式事件流转机制

二、经典案例一:图片库(Image Gallery)从基础到健壮

图片库是全书的核心骨架案例,由浅入深贯穿了前7章。作者先展示了一个最简易的实现,随后指出了该实现的多处硬伤,并最终通过动态DOM创建重构为一个高弹性的健壮组件。

1. 原始实现的问题与平稳退化处理

初学者最常见的写法是在标签中混杂行为,例如:<a href="images/fireworks.jpg" onclick="showPic(this); return false;">Fireworks</a>。 一旦读者把 href 设为 # 或 javascript:void(0),无JS环境下的链接就变成了死路。正确的做法是 href 依然保留实际的图片资源路径,将逻辑迁移到独立的JS中:

function showPic(whichpic) {
  if (!document.getElementById("placeholder")) return false;
  var source = whichpic.getAttribute("href");
  var placeholder = document.getElementById("placeholder");
  placeholder.setAttribute("src", source);
  
  if (document.getElementById("description")) {
    var text = whichpic.getAttribute("title") ? whichpic.getAttribute("title") : "";
    var description = document.getElementById("description");
    if (description.firstChild.nodeType == 3) {
      description.firstChild.nodeValue = text;
    }
  }
  return true;
}
注意防御式编程细节:代码中没有直接假定 description 或其 firstChild 必然存在,而是先进行 nodeType == 3(文本节点)校验后再给 nodeValue 赋值。如果 placeholder 节点因页面变动被误删,函数立即返回 false,触发平稳退化逻辑。

2. 动态创建标记(preparePlaceholder)与分离行为

在重构阶段,作者明确指出:<img id="placeholder"> 和 <p id="description"> 完全是为JavaScript交互服务的展示容器,如果静态写在HTML里,在JS不工作时就是两块毫无意义的空白占位。因此,这些标记应该由JavaScript在运行时动态创建:

function preparePlaceholder() {
  if (!document.createElement) return false;
  if (!document.createTextNode) return false;
  if (!document.getElementById) return false;
  if (!document.getElementById("imagegallery")) return false;

  var placeholder = document.createElement("img");
  placeholder.setAttribute("id", "placeholder");
  placeholder.setAttribute("src", "images/placeholder.gif");
  placeholder.setAttribute("alt", "my image gallery");

  var description = document.createElement("p");
  description.setAttribute("id", "description");
  var desctext = document.createTextNode("Choose an image");
  description.appendChild(desctext);

  var gallery = document.getElementById("imagegallery");
  insertAfter(placeholder, gallery);
  insertAfter(description, placeholder);
}

3. 挂载点击事件与行为拦截(prepareGallery)

function prepareGallery() {
  if (!document.getElementsByTagName) return false;
  if (!document.getElementById) return false;
  if (!document.getElementById("imagegallery")) return false;

  var gallery = document.getElementById("imagegallery");
  var links = gallery.getElementsByTagName("a");
  for (var i = 0; i < links.length; i++) {
    links[i].onclick = function() {
      return showPic(this) ? false : true;
    }
  }
}

// 统一装配生命周期
addLoadEvent(preparePlaceholder);
addLoadEvent(prepareGallery);

注意匿名函数内部的判断:return showPic(this) ? false : true;。当 showPic 成功替换了画面并返回 true 时,onclick 返回 false,阻止浏览器继续导航;一旦底层条件不满足导致 showPic 返回 false,onclick 便返回 true,浏览器自动打开 href 中的大图。这种优雅的降级保证了极端网络或脚本阻断下的可用性。

《JavaScript DOM编程艺术》第2版核心案例代码解析与实战
图 2:moveElement 递归平滑位移与状态挂载防抖机制

三、经典案例二:移动与动画抽象(moveElement 与幻灯片)

在现代Web中,简单位移可以直接借助CSS transition 或 transform 实现。然而在IE6/7时代,理解JavaScript如何驱动元素位置重绘,是掌握浏览器时序与DOM属性控制的核心训练。《JavaScript DOM编程艺术》中的 moveElement 函数堪称纯JS缓动位移的工业级教学范本。

1. 防抖与属性挂载设计

初学者编写动画时最容易犯两个错误:一是使用全局变量存储定时器,导致多个动画相互覆盖;二是当用户鼠标快速划过多个导航项时,前一个动画尚未结束,后一个动画又被触发,导致元素在不同目标间“剧烈抽搐”。作者给出的优雅解法,是直接把定时器挂在目标DOM对象自身的自定义属性 movement 上:

function moveElement(elementID, final_x, final_y, interval) {
  if (!document.getElementById) return false;
  if (!document.getElementById(elementID)) return false;
  var elem = document.getElementById(elementID);
  
  // 核心防御:如果该元素已有动画在排队,直接打断并清除
  if (elem.movement) {
    clearTimeout(elem.movement);
  }
  
  // 安全初始化位置默认值
  if (!elem.style.left) elem.style.left = "0px";
  if (!elem.style.top) elem.style.top = "0px";
  
  var xpos = parseInt(elem.style.left, 10);
  var ypos = parseInt(elem.style.top, 10);
  
  if (xpos == final_x && ypos == final_y) {
    return true;
  }
  
  // 缓动渐进计算:距离越远步长越大,距离越近步长越小
  if (xpos < final_x) {
    var dist = Math.ceil((final_x - xpos) / 10);
    xpos = xpos + dist;
  }
  if (xpos > final_x) {
    var dist = Math.ceil((xpos - final_x) / 10);
    xpos = xpos - dist;
  }
  if (ypos < final_y) {
    var dist = Math.ceil((final_y - ypos) / 10);
    ypos = ypos + dist;
  }
  if (ypos > final_y) {
    var dist = Math.ceil((ypos - final_y) / 10);
    ypos = ypos - dist;
  }
  
  elem.style.left = xpos + "px";
  elem.style.top = ypos + "px";
  
  var repeat = "moveElement('" + elementID + "'," + final_x + "," + final_y + "," + interval + ")";
  elem.movement = setTimeout(repeat, interval);
}

注意数学计算中的 Math.ceil 与 Math.floor:在逐步逼近终点时,如果距离小于10像素,单纯的整除结果为0,会导致动画卡死在距离终点几像素的位置。向上取整保证了每次至少移动1个物理像素,直至精确命中目标点。

2. 实战应用:幻灯片(prepareSlideshow)视口遮罩

利用 moveElement,作者为画廊制作了一个悬停预览幻灯片。这里的关键不在JS,而在CSS配合:定义一个具备 overflow: hidden 的固定尺寸容器(视口),内部嵌套一张把所有缩略图拼合在一起的长图(Sprite Sheet)。

function prepareSlideshow() {
  if (!document.getElementsByTagName) return false;
  if (!document.getElementById) return false;
  if (!document.getElementById("linklist")) return false;

  var slideshow = document.createElement("div");
  slideshow.setAttribute("id", "slideshow");
  var preview = document.createElement("img");
  preview.setAttribute("src", "topics.gif");
  preview.setAttribute("alt", "building blocks of web design");
  preview.setAttribute("id", "preview");
  slideshow.appendChild(preview);

  var list = document.getElementById("linklist");
  insertAfter(slideshow, list);

  var links = list.getElementsByTagName("a");
  links[0].onmouseover = function() {
    moveElement("preview", -100, 0, 10);
  };
  links[1].onmouseover = function() {
    moveElement("preview", -200, 0, 10);
  };
  links[2].onmouseover = function() {
    moveElement("preview", -300, 0, 10);
  };
}

鼠标悬停时,视口内部的图片根据偏移量平滑拉动,这种“视口裁剪+内部绝对定位”的思想,正是今天轮播图与雪碧图动画的标准底层模型。

《JavaScript DOM编程艺术》第2版核心案例代码解析与实战
图 3:DOM 内容提取、数据去重与定义列表动态注水流程

四、经典案例三:充实文档内容与无障碍语义提取

在全书第8章中,作者展示了DOM编程除做动画和图片库之外的另一项核心价值:利用既有标记中隐藏的信息增强页面的易读性与语义价值。代表案例是 displayAbbreviations(自动汇编缩略语表)和 displayCitations(提取引文出处链接)。

1. 自动汇编缩略语列表:displayAbbreviations

HTML中存在 <abbr title="Document Object Model">DOM</abbr> 标记,但读者很难在阅读长文时挨个将鼠标悬浮在每个单词上去看提示。通过纯DOM脚本,可以在文章底部自动生成一个规范的 <dl> 定义列表:

function displayAbbreviations() {
  if (!document.getElementsByTagName || !document.createElement || !document.createTextNode) return false;
  
  var abbreviations = document.getElementsByTagName("abbr");
  if (abbreviations.length < 1) return false;
  
  var defs = new Array();
  for (var i = 0; i < abbreviations.length; i++) {
    var current_abbr = abbreviations[i];
    // 兼容早前IE对abbr的解析漏洞
    if (current_abbr.childNodes.length < 1) continue;
    var definition = current_abbr.getAttribute("title");
    var key = current_abbr.lastChild.nodeValue;
    defs[key] = definition;
  }
  
  var dlist = document.createElement("dl");
  for (key in defs) {
    var definition = defs[key];
    var dtitle = document.createElement("dt");
    var dtitle_text = document.createTextNode(key);
    dtitle.appendChild(dtitle_text);
    
    var ddesc = document.createElement("dd");
    var ddesc_text = document.createTextNode(definition);
    ddesc.appendChild(ddesc_text);
    
    dlist.appendChild(dtitle);
    dlist.appendChild(ddesc);
  }
  if (dlist.childNodes.length < 1) return false;
  
  var header = document.createElement("h2");
  var header_text = document.createTextNode("Abbreviations");
  header.appendChild(header_text);
  
  document.body.appendChild(header);
  document.body.appendChild(dlist);
}

2. 提取文献引用来源:displayCitations

HTML的 <blockquote cite="http://example.com/source"> 拥有 cite 属性,但主流浏览器默认根本不把该URL渲染出来。通过下述代码,脚本遍历所有块级引用,在引用内部的最后一个段落末尾自动插入形如 [source] 的超链接:

function displayCitations() {
  if (!document.getElementsByTagName || !document.createElement || !document.createTextNode) return false;
  var quotes = document.getElementsByTagName("blockquote");
  
  for (var i = 0; i < quotes.length; i++) {
    if (!quotes[i].getAttribute("cite")) continue;
    var url = quotes[i].getAttribute("cite");
    
    // 必须挂载在引用块内的最后一个元素节点后
    var quoteChildren = quotes[i].getElementsByTagName('*');
    if (quoteChildren.length < 1) continue;
    var elem = quoteChildren[quoteChildren.length - 1];
    
    var link = document.createElement("a");
    var link_text = document.createTextNode("source");
    link.appendChild(link_text);
    link.setAttribute("href", url);
    
    var superscript = document.createElement("sup");
    superscript.appendChild(link);
    elem.appendChild(superscript);
  }
}

这一模式展现了早前Web标准运动中的“语义激活”:HTML不改动任何视觉标签,只保留纯正语义,JavaScript负责感知这些语义并将其实体化为读者可点击的交互物。

《JavaScript DOM编程艺术》第2版核心案例代码解析与实战
图 4:《JavaScript DOM编程艺术》四大支柱哲学与通用工具体系全景

五、综合实战案例:网站交互整合(以乐队网站为例)

在全书最后一章(第12章),作者将前述所有零散工具与模块合冶一炉,开发了一个完整的“Jay Skript and the Domsters”乐队官方网站。在这个综合案例中,作者补全了两个至关重要的基础交互工具:

1. 样式增强与 addClass 工具函数

虽然我们可以通过 element.style.color = "red" 直接改变内联样式,但这再次违反了“表现与行为分离”的原则。最规范的做法是通过修改 className,让预置在CSS中的规则生效。为了防止直接给 className 赋值覆盖已有的类名,书中给出了标准实现:

function addClass(element, value) {
  if (!element.className) {
    element.className = value;
  } else {
    var newClassName = element.className;
    newClassName += " ";
    newClassName += value;
    element.className = newClassName;
  }
}

2. 表格隔行换色(stripeTables)与悬停高亮(highlightRows)

在CSS3尚未普及 :nth-child(odd) 伪类之前,表格的斑马条纹也是通过DOM巡检实现的:

function stripeTables() {
  if (!document.getElementsByTagName) return false;
  var tables = document.getElementsByTagName("table");
  for (var i = 0; i < tables.length; i++) {
    var odd = false;
    var rows = tables[i].getElementsByTagName("tr");
    for (var j = 0; j < rows.length; j++) {
      if (odd == true) {
        addClass(rows[j], "odd");
        odd = false;
      } else {
        odd = true;
      }
    }
  }
}

function highlightRows() {
  if (!document.getElementsByTagName) return false;
  var rows = document.getElementsByTagName("tr");
  for (var i = 0; i < rows.length; i++) {
    rows[i].oldClassName = rows[i].className;
    rows[i].onmouseover = function() {
      addClass(this, "highlight");
    }
    rows[i].onmouseout = function() {
      this.className = this.oldClassName;
    }
  }
}

利用临时属性 rows[i].oldClassName 缓存原始类名,鼠标离开时直接还原,既避免了复杂的类名字符串正则替换,又保证了原有 odd 样式的完整保留。

3. 页面导航项的自动匹配高亮(highlightPage)

在多页面站点中,常常需要让导航栏对应当前页面的菜单项变色。传统的做法是在每个HTML文件里的 <li class="current"> 上硬编码类名,而作者展示了全自动的DOM识别方案:

function highlightPage() {
  if (!document.getElementsByTagName || !document.getElementById) return false;
  var headers = document.getElementsByTagName('header');
  if (headers.length == 0) return false;
  var navs = headers[0].getElementsByTagName('nav');
  if (navs.length == 0) return false;
  
  var links = navs[0].getElementsByTagName("a");
  var linkurl;
  for (var i = 0; i < links.length; i++) {
    linkurl = links[i].getAttribute("href");
    if (window.location.href.indexOf(linkurl) != -1) {
      addClass(links[i], "here");
      var linktext = links[i].lastChild.nodeValue.toLowerCase();
      document.body.setAttribute("id", linktext);
    }
  }
}

六、现代视角回顾:从 DOM Scripting 到组件化框架

站在现代前端工程的角度回望《JavaScript DOM编程艺术》(第2版),部分底层API已经被现代标准替代:

书中经典原生实现 现代 Web 标准 API React / Vue 时代形态
addLoadEvent document.addEventListener('DOMContentLoaded', fn) useEffect(() => {}, []) / onMounted
insertAfter element.after() / element.insertAdjacentElement() 虚拟DOM / 声明式JSX条件渲染
addClass element.classList.add() / classList.toggle() :class="{ active: isActive }"
moveElement CSS transition / requestAnimationFrame Framer Motion / CSS Modules
getElementsByTagName document.querySelectorAll() 组件状态驱动的模板绑定

然而,工具和API的更迭从未动摇这本书的灵魂价值。当今天各类单页面应用(SPA)因JavaScript报错导致白屏、搜索引擎爬虫无法抓取内容、或者屏幕阅读器无法理解毫无语义的 <div onclick="..."> 时,我们所面对的问题,与当年杰里米·基思所抗争的问题如出一辙。

《JavaScript DOM编程艺术》最宝贵的馈赠,不仅是这几套精巧的工具函数,更是对Web底色的一份敬畏:尊重标记的语义、敬畏网络的脆弱性、为每一个可能处于弱网或无脚本环境的用户保留一扇通往信息的窗户。这是每一位优秀工程师从初学者进阶到架构师所必须沉淀的底蕴。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章