使用原生 JavaScript 操作 DOM

舞動乾坤發表於2017-10-21

一、查詢 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開始, 還提供了陣列的迭代方法, 歸併方法等, 這些方法將在後面做出補充.

要比昨天的自己更強


相關文章