1

那是个夜黑风高的夜晚,我遇到了一个按钮:

<button type="submit">搜索</button>

嗯,我要选中它,我敲下了一行代码:

const submitButton = document.querySelector('button[type="submit"]');

这对于精通 document.querySelector 的函数名书写方式的我来说,简直就像吃下四两饭一样简单!

但是。

我们知道,document.querySelector 接收一个选择器字符串,返回第一个匹配的 DOM 元素,所以如果页面上只有一个 button[type="submit"] 或者这个 button[type="submit"] 在 html 中是第一个时,我这个方法是无懈可击的。

然后,我发现页面上竟然存在两个 button[type="submit"] 类型的按钮(黑人问号???)。

我对比了一下差异:

<button type="submit">提交</button>
<button type="submit">搜索</button>

先不八卦为什么页面上有两个差不多的按钮,但能初步判定的是,他们长得很像,嗯。

那么,问题来了,我怎么选中那个搜索框,我把问题抛给了自以为是的自己,得到了几个回答。

分身 A:改用 selectorSelectorAll 拿到第二个不就行了嘛!

const allSubmitButton = document.querySelectorAll('button[type="submit"]');
const submitButton = allSubmitButton[1];

这方法貌似不错,但万一这两个按钮对调位置了怎么办?

分身 B: 那选择器写全一点,也匹配『搜索』这两个字不就可以把『提交』那个按钮给拍除掉了嘛!

const submitButton = document.querySelector('button[type="submit"][innerText="搜索"]');

打印出来一看,嗯,怎么输出是 null???

分身 B: 那个,那个。。。innerText 是我编出来的,我不知道正确的要怎么写。

嗯,虽然 B 写错了语法(哪个程序员会不写错语法?),但限定了两个条件,type="submit"innerText="搜索", 能唯一匹配搜索按钮了,这个思路是对的。

那么,正确的语法是什么呢?

苦苦搜索半天,无果。

分身 C: 那就是写不出这样的选择器吧,还是用 JS 来做吧 :)

分身 A: 这么简单的需求,CSS 是万能的,为什么要用 Javascript?

分身 B: 对,Hail CSS!

。。。那我们就借此机会温习一下找到一个 DOM 元素有哪些方法吧。

浏览器原生提供的几个找到 DOM 元素的方法

document.getElementById

Id 为网页全局唯一。

document.getElementById('id');

注意,与其他方法不一样,getElementById 只能被全局对象 document 调用,否则会报错:

document.getElementById('parentId').getElementById('childId');
// Uncaught TypeError: document.getElementById(...).getElementById is not a function

因为 Id 是全局唯一的,所以上面第二种写法自然也显得没有必要。

Id 对大小写敏感(除非文档被声明为特殊类型如: XHTML, XUL),所以以下两种写法并不等同:

document.getElementById('id');
documen.getElementById('ID');

element.getElementsByClassName

这里的 element 指代网页中有效的 DOM 元素,包含 document

返回一个 HTMLCollection

// 匹配 class 包含 'submit' 的元素
document.getElementsByClassName('submit');

// 匹配 class 包含 'submit' 和 'button' 的元素
document.getElementsByClassName('submit button');

// 级联
cons selectedElement = document.getElementById('app-container');
selectedElement.getElementsByClassName('submit button');

element.getElementsByName

用法和 getElementsByClassName 相似, 返回一个 HTMLCollection

element.getElementsByTagName

用法和 getElementsByClassName 相似, 返回一个 HTMLCollection

element.querySelector

再熟悉不过的接口了,传入 CSS 选择器,返回第一个找到的元素。

element.querySelectorAll

以上返回数组的方法,getElementsByNamegetElementsByTagNamegetElementsByTagNameNSquerySelectorAll 返回的都是 Array-like 的 HTMLCollection

要像对数组一样对 HTMLCollection 进行操作(使用数组的 forEachfilter 等方法),则需要将他们转化为数组:

const buttonCollection = document.getElementsByClassName('submit');

// const buttonArray = Array.prototype.slice.call(buttonCollection);
const buttonArray = [].slice.call(buttonCollection);

buttonArray.forEach(item => {
    // do something
});

querySelector 与 getElementsByName、getElementsByClassName 和 getElementsByTagName 的细微差异

注意,对于都是返回 HTMLCollection 的方法,与 querySelector 不同的是,getElementsByNamegetElementsByClassNamegetElementsByTagName 返回的 HTMLCollection 是一个引用类型的 HTMLCollection

也就是:

  1. querySelectorAll 获取的是当时文档中匹配选择器的 DOM 元素,不包含后面插入到文档中的元素,如果文档有更新,则需要重新获取
  2. getElementsByNamegetElementsByClassNamegetElementsByTagName 则能随时反映文档中匹配对应规则的元素,包含后面插入到文档中的元素,如果文档有更新,不需要重新获取

这个说法有点类似拷贝和引用的关系。

话不多说,我们直接写点代码做个测试:

const createElement = (func) => {
    const key = 'myCustomElement';
    let element;
    switch (func) {
        case 'getElementsByName':
            element = document.createElement('div');
            element.setAttribute('name', key);
            break;
        case 'getElementsByClassName':
            element = document.createElement('div');
            element.setAttribute('class', key);
            break;
        case 'getElementsByTagName':
            element = document.createElement(key);
            break;
        case 'querySelectorAll':
            element = document.createElement('div');
            element.className = key;
            break;
        default:
            element = document.createElement('div');
    }

    return element;
}

const getCollection = (root, func) => {
    const key = 'myCustomElement';
    let element;
    if (func === 'getElementsByName') {
        return document.getElementsByName(key);
    }
    if (func === 'querySelectorAll') {
        return root.querySelectorAll(`.${key}`);
    }
    return root[func](key);
}


const testFunc = (func) => {
    const result = [];

    // 避免 getElementsByClassName 和 querySelectorAll 在统一环境的影响,创建一个独立的容器测试
    const container = document.createElement('div');
    document.body.append(container);

    // 1. 插入一个
    container.append(createElement(func));

    // 2. 看下现在有多少个
    const collection = getCollection(container, func);
    result.push(collection.length);

    // 3. 继续插入一个
    container.append(createElement(func));

    // 4. 看下现在有多少个
    result.push(collection.length);

    return result;
};

console.log('getElementsByName', testFunc('getElementsByName')); // [1, 2]
console.log('getElementsByClassName', testFunc('getElementsByClassName')); // [1, 2]
console.log('getElementsByTagName', testFunc('getElementsByTagName')); // [1, 2]
console.log('querySelectorAll', testFunc('querySelectorAll')); // [1, 1] // 注意这个输出

// 输出的是:
/*
    getElementsByName [1, 2]
    getElementsByClassName [1, 2]
    getElementsByTagName [1, 2]
    querySelectorAll [1, 1]
*/

还有什么方法可以根据 innerText 找到指定元素

回到本文讨论的问题,貌似浏览器原生并没有提供类似 document.getElementByInnerText

找了一圈貌似没有,那只能借用 Javascript 了。

回忆一下需要找到的元素是:

<button type="submit">搜索</button>

使用原生 Javascript

可以使用 querySelectorAll

let foundElement = null;
const elementsCollection = document.querySelectorAll('button[type="submit"]');
const elementArray = [].slice.call(elementsCollection);
/*
// 使用 Array.forEach 遍历,缺点是找到后没法 break
elementArray.forEach(element => {
    if (element.innerText.trim() === '搜索') {
        foundElement = element;
    }

    // 或者使用正则
    /*
    if (/^\s{0,}搜索\s{0,}$/.test(element.innerText)) {
        foundElement = element;
    }
    */
});
*/

/*
// 或使用 for-loop 找到后提前 break
const len = inputElementArray.length;
for (let i = 0; i < len; i++) {
    if (elementArray[i].innerText.trim()) {
        foundElement = elementArray[i];
        break;
    }
}
*/

// 或使用 filter
const foundElementArray = elementArray.filter(element => element.innerText.trim() === '搜索');
if (foundElementArray.length) {
    foundElement = foundElementArray[0];
}

题外话:innerText 与 innerHTML、textContent 的差异?

简而言之:

innerText 获取元素第一层的文本内容,不包含标签;
innerHTML 如其名获取内部 HTML 片段,包含标签;
textContent 类似 innerText,不过会在去掉标签后将嵌套的纯文本内容也获取出来。

MDN textContent's difference from innerText and innerHTML


赖小赖小赖
1.1k 声望40 粉丝

笨鸟。