使用原生 JavaScript 操作 DOM

一、查询 DOM

1.1 .querySelector()

使用 CSS 选择器获取元素(一个),是网页中符合查询条件的元素快照,不是即时的。

const myElement = document.querySelector('#foo > div.bar');复制代码

1.2 .matches()

元素是否匹配指定选择器?

myElement.matches('div.bar') === true复制代码

1.3 .querySelectorAll()

.querySelector() 使用 CSS 选择器获取元素(多个),是网页中符合查询条件的元素快照,不是即时的。

const myElements = document.querySelectorAll('.bar');复制代码

1.4 在 HTMLElement 元素上使用

.querySelector()/.querySelectorAll() 不仅可以在 document 上使用,还可以在 HTMLElement 元素上使用。

const myChildElemet = myElement.querySelector('input[type="submit"]');

// 等同于
// document.querySelector('#foo > div.bar input[type="submit"]');复制代码

1.5 .getElementsByTagName()

根据标签来查询元素,是即时的。

// HTML
<div></div>

// JavaScript
const elements1 = document.querySelectorAll('div')
const elements2 = document.getElementsByTagName('div')
const newElement = document.createElement('div')

document.body.appendChild(newElement)
elements1.length // 1
elements2.length // 2复制代码

二、操作 NodeList

.querySelectorAll() 查询的结果是 NodeList 类型的,没有法使用数组方法(比如 .forEach() 方法),所以需要:

  1. NodeList 元素装换成数组。
  2. 借用数组的方法。

2.1 把 NodeList 元素装换成数组。

Array.prototype.slice.call(myElements).forEach(doSomethingWithEachElement);

// 或者使用 ES6 方法 `Array.from()`

Array.from(myElements).forEach(doSomethingWithEachElement);复制代码

2.2 借用数组的方法

Array.prototype.forEach.call(myElements, doSomethingWithEachElement);

// 或者

[].forEach.call(myElements, doSomethingWithEachElement);复制代码

2.3 查询亲属

每个 Element 元素还提供了查询亲属结点的只读属性。

myElement.children
myElement.firstElementChild
myElement.lastElementChild
myElement.previousElementSibling
myElement.nextElementSibling复制代码

Element 元素又继承自 Node,所以还拥有下面的属性:

myElement.childNodes
myElement.firstChild
myElement.lastChild
myElement.previousSibling
myElement.nextSibling
myElement.parentNode
myElement.parentElement复制代码

可以通过结点的 nodeType 属性值,确定结点类型。

myElement.firstChild.nodeType === 3 // 判断是否为文本结点复制代码

三、修改类和特性

3.1 .classList API

myElement.classList.add('foo')
myElement.classList.remove('bar')
myElement.classList.toggle('baz')复制代码
// 获取元素的属性 `value` 的值
const value = myElement.value

// 设置元素的属性 `value` 的值
myElement.value = 'foo'复制代码

3.2 Object.assign()

// 使用 `Object.assign()` 为元素同是设置多个属性
Object.assign(myElement, {
  value: 'foo',
  id: 'bar'
})

// 删除元素属性
myElement.value = null复制代码

.getAttibute().setAttribute().removeAttribute() 会直接修改 HTML 特性,会引起浏览器重绘,代价高,不建议使用。如要永久更改 HTML,可以通过使用父元素的 .innerHTML 做到。

3.3 添加 CSS 样式

myElement.style.marginLeft = '2em';复制代码

通过 .style 属性获得的属性值是没有经过计算的。要获取经过计算的值,使用 .window.getComputedStyle()

window.getComputedStyle(myElement).getPropertyValue('margin-left');复制代码

四、 修改 DOM

4.1 .appendChild().insertBefore()

// 将 element2 追加为 element1 的最后一个孩子
element1.appendChild(element2);

// 在 element1 的孩子 element3 之前插入 element2
element1.insertBefore(element2, element3);复制代码

4.2 .cloneNode()

要插入一个克隆的元素,可以使用 .cloneNode() 方法。

const myElementClone = myElement.cloneNode();
myParentElement.appendChild(myElementClone);复制代码

.cloneNode() 还可接收一个布尔值参数,true 表示深复制——元素的孩子也会被克隆。

4.3 创建元素

const myNewElement = document.createElement('div');
const myNewTextNode = document.createTextNode('some text');复制代码
myParentElement.removeChild(myElement);

myElement.parentNode.removeChild(myElement);复制代码

4.4 .innerHTML.textContent

每个元素都有属性 .innerHTML.textContent(或者类似的 .innerText)。

// 替换掉 myElement 内部的 HTML
myElement.innerHTML = `
  <div>
    <h2>New content</h2>
    <p>beep boop beep boop</p>
  </div>
`

// 删除 myElement 元素的所有子节点
myElement.innerHTML = null

// 为 myElement 元素追加内部的 HTML
myElement.innerHTML += `
  <a href="foo.html">continue reading...</a>
  <hr/>
`复制代码

为元素追加内部的 HTML 并不好,因为会丢失之前的已更改的所有属性和事件监听绑定。

追加元素较好的方式是这样的:

const link = document.createElement('a');
const text = document.createTextNode('continue reading...');
const hr = document.createElement('hr');

link.href = 'foo.html';
link.appendChild(text);

myElement.appendChild(link);
myElement.appendChild(hr);复制代码

上面的追加代码会导致浏览器两次重绘,而不是 .innerHTML 的一次重绘。这时可以借助 DocumentFragment

const fragment = document.createDocumentFragment();

fragment.appendChild(text);
fragment.appendChild(hr);
myElement.appendChild(fragment);复制代码

五、事件监听

5.1 DOM 0 级

myElement.onclick = function onclick (event) {
  console.log(event.type + ' got fired')
}复制代码

这种方式只能为某一事件添加一个事件处理函数。若想添加多个,可以使用 .addEventListener()

5.2 DOM 3 级

myElement.addEventListener('click', function (event) {
  console.log(event.type + ' got fired');
})

myElement.addEventListener('click', function (event) {
  console.log(event.type + ' got fired again');
})复制代码

在事件处理函数内部,event.target 指向触发事件的元素(或使用箭头函数里的 this)。

5.3 阻止浏览器的默认行为

使用 .preventDefault() 可以阻止浏览器的默认行为(比如点击超链接、提交表单时)。

myForm.addEventListener('submit', function (event) {
  const name = this.querySelector('#name');

  if (name.value === 'Donald Duck') {
    alert('You gotta be kidding!');
    event.preventDefault();
  }
})复制代码

另外一个重要的方法是 .stopPropagation()——阻止事件冒泡至祖先结点。

绑定监听事件时,还可以指定第三个参数:可选的配置对象/是否在捕获阶段触发事件的布尔值(默认 false,即在冒泡阶段触发事件)。

5.4 .addEventListener() 可选的第三个参数

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);复制代码

可选的配置对象有下列 3 个布尔值属性(默认都为 false):

  1. capture:为 true 时,表示在捕获阶段触发事件(即到达事件目标之前,会立即触发事假)。
  2. once:为 true 时,表示事件只能被触发一次。
  3. passive:为 true 时,会忽略 event.preventDefault() 代码,不会阻止默认行为的发生(通常会引起控制台发出警告)。

这三个中,最经常使用的是 .capture,这样,就可以使用可选的表示“是否在捕获阶段触发事件的布尔值”替代“可选的配置对象”了。

// 在捕获阶段触发事件
myElement.addEventListener(type, listener, true);复制代码

5.5 .removeEventListener()

删除事件监听使用 .removeEventListener()。比如可选的配置对象的 once 属性可以这样实现:

myElement.addEventListener('change', function listener (event) {
  console.log(event.type + ' got triggered on ' + this);
  this.removeEventListener('change', listener);
})复制代码

六、事件代理

这是一个很有用的模式。现在有一个表单,当表单元素里的输入框发生 change 事件时,我们要对此监听。

myForm.addEventListener('change', function (event) {
  const target = event.target;
  if (target.matches('input')) {
    console.log(target.value);
  }
})复制代码

这样的一个好处是——即是元素中的输入框个数发生了改变,也不会影响监听事件起作用。

七、动画

使用 CSS 原生的动画效果已经很好了(通过 transition 属性和 @keyframes),但如果需要更加复杂的动画效果,可以使用 JavaScript。

JavaScript 实现动画效果,主要有两种方式:

  1. window.setTimeout():在动画完成后,停止对 window.setTimeout() 的调用,但可能会出现动画不连续。
  2. window.requestAnimationFrame()
const start = window.performance.now();
const duration = 4000;

window.requestAnimationFrame(function fadeIn (now) {
  const progress = now - start;
  myElement.style.opacity = progress / duration;

  if (progress < duration) {
    window.requestAnimationFrame(fadeIn);
  }
})复制代码

八、写辅助方法

第一种方式:

const $ = function $ (selector, context = document) {

    const elements = (selector, context = document) => context.querySelectorAll(selector);
    const element = elements[0];

    return {
        element,
        elements,

        html (newHtml) {
          this.elements.forEach(element => {
            element.innerHTML = newHtml;
          })

          return this;
        },

        css (newCss) {
          this.elements.forEach(element => {
            Object.assign(element.style, newCss);
          })

          return this;
        },

        on (event, handler, options) {
          this.elements.forEach(element => {
            element.addEventListener(event, handler, options);
          })

          return this;
        }
    };

};复制代码

第二种方式:

const $ = (selector, context = document) => context.querySelector(selector);
const $$ = (selector, context = document) => context.querySelectorAll(selector);

const html = (nodeList, newHtml) => {
  Array.from(nodeList).forEach(element => {
    element.innerHTML = newHtml;
  })
}复制代码





在入门Vue时, 列表渲染一节中提到数组的变异方法, 其中包括push(), pop(), shift(), unshift(), splice(), sort(), reverse(), 而concat()和slice()不属于变异方法. 在这里就复习一下Array所提供的这几个方法的使用.

栈方法

push方法和pop方法, 可以使数组的行为类似于栈, 先进后出, 并且推入和弹出操作只发生在一端.

push方法

push方法可以接收一个或多个参数, 把它们追加到数组末尾, 并返回修改后数组的长度.

var arr = ['a', 'b', 'c', 'd', 'e'];
var temp = arr.push('f');
console.info('temp: ' + temp); // temp: 6
console.info(arr); // ["a", "b", "c", "d", "e", "f"]

temp = arr.push('g', 'h');
console.info('temp: ' + temp); // temp: 8
console.info(arr); // ["a", "b", "c", "d", "e", "f", "g", "h"]复制代码

合并两个数组
我们可以通过Array.prototype.push.apply()来合并两个数组, 示例如下:

var arr1 = ['a', 'b', 'c'],
    arr2 = ['x', 'y', 'z'];
var temp = Array.prototype.push.apply(arr1, arr2);
console.info(arr1); // ["a", "b", "c", "x", "y", "z"]
console.info(arr2); // ["x", "y", "z"]
console.info(temp); // 6复制代码

pop方法

pop方法是将数组的最后一项移除, 将数组长度减1, 并返回移除的项.

var arr = ['a', 'b', 'c', 'd', 'e'];
var temp = arr.pop();
console.info('temp: ' + temp); // temp: e
console.info('length: ' + arr.length); // length: 4复制代码

如果在一个空数组上使用pop方法, 则返回undefined

队列方法

队列的访问规则是先进先出, 并且队尾添加项, 队头移除项. push方法和shift方法结合使用, 就可以像操作队列一样操作数组.

shift方法

shift方法将移除数组的第一项, 将数组长度减1, 并返回移除的项.

var arr = ['a', 'b', 'c', 'd', 'e'];
var temp = arr.shift();
console.info('temp: ' + temp); // temp: a
console.info('length: ' + arr.length); // length: 4复制代码

unshift方法

相反地, 还有一个unshift方法, 它的用途与shift方法相反
unshift也可以在接收一个或多个参数, 把它们依次添加到数组的前端, 并返回修改后数组的长度.

var arr = ['a', 'b', 'c', 'd', 'e'];
var temp = arr.unshift('x', 'y', 'z');
console.info('temp: ' + temp); // temp: 8
console.info(arr); // ["x", "y", "z", "a", "b", "c", "d", "e"]复制代码

重排序方法sort方法和reverse方法

sort方法和reverse方法是可以直接用来重排序的两个方法.
其中, reverse方法是用来反转数组的.

var arr = [1, 3, 2, 5, 4];
arr.reverse();
console.info(arr); // [4, 5, 2, 3, 1]复制代码

关于sort方法, 默认情况下, 它是对数组的每一项进行升序排列, 即最小的值在前面. 但sort方法会调用toString方法将每一项转成字符串进行比较(字符串通过Unicode位点进行排序), 那么这种比较方案在多数情况下并不是最佳方案. 例如:

var arr = [1, 3, 2, 5, 4];
arr.sort();
console.info(arr); // [1, 2, 3, 4, 5]

arr = [1, 5, 10, 20, 25, 30];
arr.sort();
console.info(arr); // [1, 10, 20, 25, 30, 5]复制代码

因此, sort方法可以接收一个比较函数作为参数, 由我们来决定排序的规则. 比较函数接收两个参数, 如果第一个参数小于第二个参数(即第一个参数应在第二个参数之前)则返回一个负数, 如果两个参数相等则返回0, 如果第一个参数大于第二个参数则返回一个正数, 例如:

var arr = [1, 5, 10, 20, 25, 30];
arr.sort(function(value1, value2){
    if(value1 < value2) {
        return -1;
    } else if(value1 > value2) {
        return 1;
    } else {
        return 0;
    }
});
console.info(arr); // [1, 5, 10, 20, 25, 30]复制代码

操作方法concat方法

concat方法可以将多个数组合并成一个新的数组. concat可以接收的参数可以是数组, 也可以是非数组值.

var arr1 = ['a', 'b', 'c'],
    arr2 = ['x', 'y', 'z'],
    val = 'hello';
var temp = arr1.concat(val, arr2);
console.info('arr1: ' + arr1); // arr1: a,b,c
console.info('arr2: ' + arr2); // arr2: x,y,z
console.info('val: ' + val); // val: hello
console.info('temp: ' + temp); // temp: a,b,c,hello,x,y,z复制代码

concat方法并不操作原数组, 而是新创建一个数组, 然后将调用它的对象中的每一项以及参数中的每一项或非数组参数依次放入新数组中, 并且返回这个新数组.

concat方法并不操作调用它的数组本身, 也不操作各参数数组, 而是将它们的每个元素拷贝一份放到新创建的数组中. 而拷贝的过程, 对于对象类型来说, 是将对象引用复制一份放到新数组中, 而对于基本类型来说, 是将其值放到新数组中.

slice方法

slice方法可以基于源数组中的部分元素, 对其进行浅拷贝, 返回包括从开始到结束(不包括结束位置)位置的元素的新数组.

var arr = ['a', 'b', 'c', 'd', 'e'];
var temp1 = arr.slice(),
    temp2 = arr.slice(1),
    temp3 = arr.slice(1, 2);
console.info(arr); // ["a", "b", "c", "d", "e"]
console.info(temp1); // ["a", "b", "c", "d", "e"]
console.info(temp2); // ["b", "c", "d", "e"]
console.info(temp3); // ["b"]复制代码

从示例中可以看出:

  1. slice方法并没有操作原数组, 而是创建了一个新的数组.
  2. 当没有传参数给slice方法时, 则返回从索引0开始拷贝的新数组.
  3. 传入一个参数, 如: arr.slice(1), 表示从索引1位置开始拷贝, 一直到原数组的最后一个元素.
  4. 传入两个参数, 如: arr.slice(1, 2), 表示从索引1位置开始拷贝, 一直拷贝到位置2但不包括位置2上的元素.

参数如果为负数, 表示从数组最后面的元素可以算起.
slice方法同样不操作调用它的数组本身, 而是将原数组的每个元素拷贝一份放到新创建的数组中. 而拷贝的过程, 也于concat方法相同.

splice方法

splice方法可以用途删除或修改数组元素. 它有如下几种用法:

  • 删除:
    当给splice方法中传入一个或两个参数时, 就可以从数组中删除任意元素.
    传入一个参数: 要删除的的第一个元素的位置, 此时将会删除从要删除的第一个元素的位置起, 后面的所有元素.
    传入两个参数: 要删除的第一个元素的位置和要删除的项数,
    返回值均为删除的元素组成的数组, 例如:

    var arr = ['a', 'b', 'c', 'd', 'e'];
    var temp = arr.splice(2);
    console.info(arr); // ["a", "b"]
    console.info(temp); // ["c", "d", "e"]
    
    arr = ['a', 'b', 'c', 'd', 'e'];
    temp = arr.splice(2, 2);
    console.info(arr); // ["a", "b", "e"]
    console.info(temp); // ["c", "d"]复制代码

  • 插入:
    使用splice方法可以向数组的指定位置插入任务数量的元素, 此时需要提供三个参数: 起始位置(要插入的位置), 0(表示要删除的项数, 0为不删除), 要插入的元素, 如果要插入多个元素可以添加更多的参数, 例如:

    var arr = ['a', 'b', 'c', 'd', 'e'];
    var temp = arr.splice(2, 0, 'x', 'y', 'z');
    console.info(arr); // ["a", "b", "x", "y", "z", "c", "d", "e"]
    console.info(temp); // [], 并没有删除元素复制代码

  • 替换:
    当splice接收三个参数, 且第二个参数不为0时, 可达到在数组中替换元素的效果. 例如:

    var arr = ['a', 'b', 'c', 'd', 'e'];
    var temp = arr.splice(2, 2, 'x', 'y', 'z');
    console.info(arr); // ["a", "b", "x", "y", "z", "e"]
    console.info(temp); // ["c", "d"]复制代码

    此示例表示, 从arr数组的位置为2的元素起, 删除2个元素, 并在位置2添加三个元素.
    如果移除的元素个数不等于添加的元素个数, 那么数组的长度将发生变化.

另外,
从ECMAScript5开始, 还提供了数组的迭代方法, 归并方法等, 这些方法将在后面做出补充.

要比昨天的自己更强


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值