GitHub 链接:javascript-the-definitive-guide
上一章链接:JavaScript 中的事件
Scripting Documents
JavaScript 诞生的目的就是为了将静态的页面转换成
每一个 Window 对象都拥有 document 属性,它指向了 Document 对象。Document 对象代表了网页中的内容。Document 对象并不会单独存在,而它是 DOM 的中心对象,用于表示和操纵 document 内容。
DOM 已经在前面介绍过了,这一章会解释一下它包含的 API:
- 如何查询以及选择 document 中的元素
- 如何遍历 document,并寻找指定元素的 ancestors/siblings/descendants 元素
- 如何查询以及改变 document 元素属性
- 如何查询以及改变 document 内容
- 如何通过增删改节点来改变 document tree 结构
选择 Document 元素
客户端 JavaScript 中全局的 document 属性指向了 Document 对象,其中的 head 和 body 属性又分别指向了 head 和 body 标签所对应的 Element 对象。
通过 CSS 选择器选择元素
CSS 拥有一个强大的句法,被称为选择器(selectors),可以用于描述单个或多个元素。DOM 中的方法 querySelector() 和 querySelectorAll() 可以使我们通过 CSS 选择器来查找元素。
CSS 选择器可以通过标签,id(#),class(.)来描述元素:
div // 任何 div 标签的元素
#abc // 拥有 id='abc' 的元素
.abc // 拥有 class='abc' 的元素
或者可以通过更为指定的属性来选择:
p[lang='fr'] // 任何法语编写的 p 标签(<p lang="fr">)
*[name='abc'] // 任何拥有 name='x' 的元素
也可以是更为复杂的组合,或者指定了 document 中的位置:
span.fatal.error // 任何拥有 fatal 和 error 同时作为 class 的 span 标签的元素
span[lang='fr'].warning // 任何拥有 warning 作为 class,且是法语的 span 标签的元素
h1, h2 // 使用逗号分割代表符合其一即可
#log span // id='log' 元素下所有 span 标签的 descendant(所有后裔)元素
#log>span // id='log' 元素下所有 span 标签的 child(仅子元素,孙元素不行) 元素
body>h1:first-child // body 中第一个 h1 的子元素
img + p.caption // 在 img 之后紧跟的 拥有 class='caption' 的 p
h2 ~ p // 任何拥有 h2 作为 sibling(兄妹)的 p 元素
querySelector() 方法就可以接收用字符串表示的 CSS 选择器作为 argument,在找到第一个匹配元素时,返回它,或者在找不到任何匹配时返回 null;querySelectorAll() 则会返回所有匹配值:
let h1 = document.querySelector("h1"); // 返回第一个 h1
let h2 = document.querySelectorAll("h2"); // 返回所有 h2
querySelectorAll() 的返回值不是包含 Element 对象的数组,而是一个类数组对象,被称为 NodeList。NodeList 对象也拥有 length 属性,也拥有和数组一样的索引,所以可以使用 for of 循环等遍历,我们可以通过 Array.from() 方法来将其转换成一个数组。
这两个方法在 Element 类和 Document 类中都被实现了。对于 Document 使用,会在整个 Document 中查找,而对 Element 使用,则只会查找它的 descendants
对于 CSS 中 ::first-line 等只截取 node 中的一部分的选择器无法在这两个方法中使用。许多浏览器页不允许 :link 和 :visited 选择器,因为可能会暴露用户隐私。
另一种 CSS 选择器的方法时 closest(),它只在 Element 类中被定义。它也会接收一个 CSS 选择器作为 argument,如果有匹配元素,返回它;若没有匹配元素,它会返回最近的匹配的 ancestor(祖先),若还是找不到,则返回 null。
其他的元素选择方法
除了通过 CSS 选择器来选择之外,还可以通过以下方法来选择:
let a1 = document.getElementById("a1"); // 通过 id 属性选择
let a2 = document.getElementsByName("a2"); // 通过 name 属性选择
let a3 = document.getElementsByClassName("a3"); // 通过 class 属性选择
let h2 = document.getElementsByTagName("h2"); // 通过标签名选择
getElementById() 会返回一个 Element 对象,而剩余的方法则和 querySelectorAll() 一样,返回一个 NodeList。
预设元素选择
出于历史原因,Document 类也定义了一些属性来直接访问特定的 nodes。比如 images,forms,links 属性会选择所有 img,form,a 标签的元素。这些属性指向了 HTMLCollection 对象,它和 NodeList 对象十分相似,不过 HTMLCollection 也可以通过 name 或者 id 查找:
document.links[0]; // 第一个超链接
document.links.first // id 为 first 的超链接
还有一个过时的 API:document.all,它可以用 HTMLCollection 来返回 document 中的所有元素。不过目前已被遗弃,不应该再被使用。
Document 的结构和遍历
在选择了一个 Element 之后,我们可以调用其 traversal API 来遍历 document tree 中的 Element 对象,并且忽略 Text 节点:
- parentNode: Element 对象上的该属性指向了该元素的 parent 对象
- children: 该属性为包含了 Element 对象上的是所有子元素的 NodeList
- childElementCount: 该属性为子元素数量,等同于 children.length
- firstElementChild: 该属性指向了第一个子元素
- lastElementChild: 该属性指向了最后一个子元素
- nextElementSibling: 该属性指向了下一个兄妹元素
- previousElementSibling: 该属性指向了上一个兄妹元素
深度优先遍历 document:
// 第一种
function dfs(element) {
// 可以在这做些什么
// ...
for(let child of e.childern) {
dfs(child); // 递归遍历
}
}
// 第二种
function dfs(element) {
// 可以在这做些什么
// ...
let child = element.firstElementChild; // 使用链表风格
while(child !== null) {
dfs(child); // 递归遍历
child = child.nextElementSibling;
}
}
而如果我们不想忽略 Text 节点时,我们可以使用以下属性,所有的 Node 对象都拥有它们:
- parentNode: 该属性指向了该元素的 parent 节点对象
- childNodes: 该属性为包含了该对象上的是所有子节点对象的 NodeList
- firstChild: 该属性指向了第一个子节点
- lastChild: 该属性指向了最后一个子节点
- nextSibling: 该属性指向了下一个兄妹节点
- nextSibling: 该属性指向了上一个兄妹节点
- nodeType: 表示节点种类的数字,9 为 Document 节点,1 为 Element 节点,3 为 Text 节点,8 为 Comment 节点
- nodeValue: Text 和 Comment 节点的文字内容
- nodeName: HTML 标签名,转换成大写
元素属性
HTML 的 element 都拥有一个标签名,以及一系列表示属性的 name/value pairs。我们可以使用 getAttribute(), setAttribute(), hasAttribute(), removeAttribute() 来取得,改变,测试,移除特定的属性。但其实 HTMLElement 对象上拥有对应的属性作为属性名,所以用起来会更方便。
以 ELement 对象属性表示 HTML 属性
Element 对象表示的 HTML 元素通常定义了用于读写的属性。所有的 Element 对象都定义了 id,title,lang,dir 以及事件处理函数。对于某些特定类型的 Element 还定义附加的属性,如 img 的 src 属性。
HTML 的属性不用区分大小写,但是 JavaScript 属性需要,使用 camelCase(驼峰命名法)来命名。某些 HTML 属性名是 JavaScript 中的保留字,对于他们的解决方案通常是加上 html 前缀。比如 HTML 的 for 属性,进入 JavaScript 属性就成为了 htmlFor。class 也是一个保留字,但是它比较特殊,所以改名为了 className。
虽然 Element 对象上的属性定义了读写方法,但没有定义移除的方法,所以再移除属性时调用 removeAttribute() 来实现。
class 属性
对于 HTML 元素来说,class 属性十分重要,它的值时一列通过空格分割的 CSS 类。JavaScript 中使用 classsName 属性表示。我们可以直接更改 className,不过更多的时候,我们只需要修改其中一个类。
处于这个原因,Element 对象也定义了 classList 属性,是我们可以将 HTML 的 class 属性当做一个 list 来处理。classList 的值是一个类数组对象,可以被迭代,也可以使用以下方法:
let a = document.querySelector('#a1');
a.classList.remove(); // 删除指定类
a.classList.add(); // 增加指定类
a.classList.contains(); // 是否包含指定类
a.classList.toggle(); // 若该类存在,删除它;若不存在,添加它
dataset 属性
某些时候我们会需要为 HTML 元素添加更多的数据,然后让 JavaScript 来操纵或者选取它们。在 HTML 中,任何以 data- 前缀开头的属性都是合理的数据,它们并不会改变 HTML 元素展示的样子,而是一种标准的增加附加数据的方法。
在 DOM 中,Element 对象拥有一个 dataset 属性,它指向了一个包含对应的 data- 属性作为属性的对象,不过不再拥有前缀。比如 dataset.x 就指向了 HTML 标签中的 data-x 属性。对应时还会从连字符转到 camelCase,即 data-my-age 会变成 dataset.myAge:
<h2 id="age" data-my-age="22">age</h2>
对应
document.querySelector("#age").dataset.myAge; // '22'
元素内容
假设 HTML 中有一个 p 元素:
<p>This is a <i>simple</i> example</p>
那么它的内容是什么呢?
- 是 'This is a <i>simple</i> document' 嘛?
- 又或是 'This is a simple document' 呢?
其实它们都可以被称之为答案,并且拥有特定的用处
以 HTML 作为元素内容
读取 Element 的 innerHTML 属性时,返回的就会是 'This is a <i>simple</i> document',即包含了 HTML 标签的字符串。我们对其进行修改时,也可以加入 HTML 标签,而它们也会被正常的解析。
因为浏览器十分擅长解析 HTML,所以通常改变 innerHTML 是比较高效的,但如果对 innerHTML 使用 += 操作符就不再高效了,因为需要格外的序列化步骤,将 HTML 元素转换成字符串,然后再将新的字符串解析回去。
还有一点需要注意,不要让用户输入的数据进入 document,不然他可能再你的程序中恶意插入它的代码。
outerHTML 和 innerHTML 很像,不过会包含元素本身:'<p>This is a <i>simple</i> example</p>'。一个相关的方法是 insertAdjacentHTML(),它使我们可以再 HTML 中为指定元素的相邻位置插入字符串标识的 markup。markup 会作为第二个 argument 表示,而第一个 argument 定义了 adjacent 的位置,可以是 'beforebegin', 'afterbegin', 'beforeend', 'afterend' 中的一个:
以纯文本作为元素内容
在我们只需要元素的内容的文字部分时,或者需要插入纯文本时(没有 markup 的标签等),我们可以使用 textContent 属性来实现,比如前面的例子就会返回 'This is a simple document'。
textContent 属性是在 Node 类上定义的,所以其子类 Element node 和 Text node 都拥有该属性。
创建,插入,删除节点
除去查询和修改之外,我们也可以直接创建,插入或删除节点,然后直接改变 document 的结构。Document 类中顶一个用于创建 Element 对象的方法,Element 对象和 Text 对象又拥有了插入,删除,以及替换节点的方法。
想要创建一个新的 Element 对象并修改它十分的简单:
let p = document.createElement('p'); // 创建新的 p 元素
let em = document.createElement('em'); // 创建新的 em 元素
em.append('world'); // 为 em 从右增加字符串
p.append(em, '!'); // 将 em 加入 p 中
p.prepend('Hello '); // 为 p 从左增加字符串
p.innertHTML // "Hello <em>World</em>!"
append() 和 preend() 方法接收任意数量的 arguments,且可以是字符串或者 Node 对象,字符串会自动转换成 Text 节点。(或使用 document.createTextNode() 来显式创建,但是没必要)
如果要在 Element 或者 Text 节点的中间插入,则需要先得到对于某个 sibling 节点的引用,然后调用 before() 或 after() 方法:
// 找到拥有 class='greetings' 的 h2 元素
let greetings = document.querySelector("h2.greetings");
// 在它后面添加 p 和一个 horizontal rule(hr)
greetings.after(p, document.createElement("hr"));
如果想要复制一个节点,可以使用 cloneNode() 方法,若传入 true 作为 argument,则会将其中所有内容复制;若传入 false 或不传 argument,则只会复制标签和属性。若想要移除一个节点,我们可以使用 document 的 remove() 方法,或者 replaceWith() 用于替换:
let p2 = p.cloneNode(true); // 复制 p
greetings.replaceWith(p2); // 删除 greetings,并用 p2 替代
p2.remove(); // 删除 p2
Scripting CSS
除了操纵 HTML document 的内容和结构之外,JavaScript 也可以操纵 CSS 来改变其展示风格。以下属性是最常在 JavaScript 中被用到的 CSS 风格:
- display 属性: 通过将其设置为 none 来隐藏元素
- position 属性: 通过将其设置为 absolute,relative 或者 fixed,然后修改元素展示位置
- transform 属性: 用于移动,旋转,拉伸元素
- transition 属性: 为元素添加动画,这些动画会通过浏览器自动处理,而不需要额外的 JavaScript
CSS 类
最基础的操纵 CSS 的用法就是从 HTML 的 class 标签新增或删除 CSS 类名。使用 classList 属性来操作会很简单。比如我们可以定义一个 CSS 类:
.hidden { // 该类元素会被隐藏
display: none;
}
然后通过 JavaScript 修改 class 名来隐藏/显示元素:
document.querySelector("#x").classList.add("hidden"); // 隐藏 id 为 x 的元素
document.querySelector("#x").classList.remove("hidden"); // 展示它
Inline Styles
我们可以通过 JavaScript 来修改 HTML 元素的 style 属性来改变它的风格。DOM 为每一个 Element 对象定义了一个 style 属性,指向了对应的 HTML style 属性。style 和大多属性不同,他不是一个字符串,而是一个 CSSStyleDeclaration 对象:一个被解析的,拥有以字符串表示的 CSS 风格的对象。比如我们可以这样定义一个修改元素位置的函数:
function displayAt(element, x, y) {
element.style.display = 'block';
element.style.position = 'absolute';
element.style.left = `${x}px`;
element.style.top = `${y}px`;
}
CSSStyleDeclaration 的命名风格和 HTML 中的不同,因为连字符在 JavaScript 会被当成减号,所以会将 kebab-case 转换成 camelCase,比如 border-left-width 会变成 borderLeftWidth。
在和 CSSStyleDeclaration 打交道时,要记住所有的属性值都是字符串:
.e { // CSS 中不用字符串来表示
display: block;
font-family: sans-serif;
background-color: #ffffff;
margin-left: 300px;
}
对应:
// JavaScript 中则需要是字符串
e.style.display = "block"; // 注意分号在字符串外,它们是 JavaScript 的分号
e.style.fontFamily = "sans-serif"; // 我们不需要为 CSS 传入分号
e.style.backgroundColor = "#ffffff";
e.style.marginLeft = "300px"; // 注意是字符串而非数字,也要注意单位
我们也可以使用 getAttribute 和 setAttribute,或者 cssText 属性来查询 inline style,而且会简单一些:
// 将 e 的 inline styles 复制到 f 上,下面两个方法都可以
f.setAttribute("style", e.getAttribute("style"));
f.style.cssText = e.style.cssText;
在通过 style 属性查询元素的 CSS 时,我们只能得到其 inline style,而无法取得外部 CSS 的风格。
Computed Styles
一个元素的 Computed Styles 就是其所有风格的属性和值的集合(包括 inline 的,浏览器定义的,CSS 定义的):这也是展示元素时用到的属性。Computed Styles 也是通过 CSSStyleDeclaration 对象来代表的,不过和 inline style 不同,它是一个只读对象,我们不能修改它。
我们可以通用 Window 对象上的 getComputedStyle() 方法来取得元素的风格,第一个 argument 是需要查询的元素,第二个可选的 argument 是用于指定 CSS 中的伪代码(类似 ::before 等):
let title = document.querySelector("#section1title");
let styles = window.getComputedStyle(title);
let beforeStyles = window.getComputedStyle(title, "::before");
getComputedStyle() 方法的返回值是一个 CSSStyleDeclaration,它代表了指定元素上所有的风格。虽然都是 CSSStyleDeclaration 对象,但是 inline style 表示的和 computed style 表示的还是有许多区别的:
- computed style 的属性是只读的
- computed style 的属性是绝对的:所以百分比等非绝对值都会转换成绝对值,任何需要 size 的属性(如 font-size 和 margin size)都会转换成 pixels(px 后缀);颜色则会被转换成 rgb() 或者 rgba() 形式
- computed style 不包含简写属性:比如 margin 不被包含,只会包含其中的 marginLeft,marginTop 等
- computed style 的 cssText 属性是 undefined
通常来说,getComputedStyle() 返回的 CSSStyleDeclaration 对象会拥有更多的信息。但是某些时候它包含的信息是很狡猾的,比如 top 和 left 的属性通常会返回 auto 作为值,虽然一点都不 make sense,不过这是一个合理的 CSS 值。
虽然 CSS 可以用于精确的指定元素的 position 和 size,但是通过查询 computed style 并不是最完美的查询 position 和 size 的方法。等下会介绍更好的方法。
Scripting Stylesheets
除去操纵 inline style 之外,JavaScript 也可以操纵 stylesheets(即以 sstyle 标签的元素,或 rel='stylesheet' 的 link)。因为它们都是 HTML 标签,所以可以通过 doncument.querySelector() 等方法查找。style 和 link 元素对应的 Element 对象拥有 disabled 属性,我们可以用它来 disable 整个 stylesheet:
function switchTheme () { // 切换主题
let light = document.querySelector('#light-theme');
let dark = document.querySelector('#-theme');
if (dark.disabled) {
light.disable = true;
dark.disable = false;
} else {
light.disable = false;
dark.disable = true;
}
}
或者我们可以为 DOM 添加 link 来加入 stylesheet:
let link = document.createElement('link'); // 创建 link 对象
link.id = "style"; // 设置 id
link.rel = "stylesheet"; // 设置 rel
link.href = `style.css`; // 设置 href
document.head.append(link); // 加入 document 中
CSS 动画和事件
假设这有两个 CSS 类:
.transparent { opacity: 0; }
.fade { transition: opacity .5s ease-in }
第一个会使元素完全透明化,而第二个为其增加了 fade 效果。假设我们已经有了一个拥有 fade 类的元素,我们在 JavaScript 中为其增加 transparent 类时,它会自动触发动画;在删除时也会触发。我们不需要在 JavaScript 中额外的定义什么,它们都是纯粹的 CSS 特效,JavaScript 仅仅使用来触发它们的。
JavaScript 也可以被用来监听 CSS transition 的进度,因为浏览器会在 transition 开始和结束时触发事件。transitionrun 事件会在 transition 效果触发时触发,它会在视觉效果变化前触发(如果设有 trainsition-delay);transitionstart 事件会在视觉效果开始时触发;transitionend 会在动画结束时触发。传入该类事件的处理函数的 argument 会是 TransitionEvent 对象,拥有额外的 propertyName 属性,指定了用于动画的 CSS 属性,以及 elapsedTime 属性指定了从 transitionstart 到 transitionend 花了多少时间。
除去 transition,CSS 还支持 animation。它包含比如 animation-name 和 animation-duration 属性,和一个指定的 @keyframes rule 用于指定其细节。我们也可以通过 JavaScript 增删 CSS 类来触发它。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!