JS核心--DOM

357 阅读8分钟

什么是DOM?

网页其实是一棵树。

为了让网页能实现各种各样的效果,我们需要对这些元素进行操作。

那么JS如何操作这些元素呢?

浏览器为此,给window添加了一个document对象,我们将网页抽象成一个document对象,通过这个document对象操作整个页面的所有的节点。

而上面所描述的模型 Document Object Model 就是 我们的 DOM。

题外话:DOM,非常反人类,通常我们会使用VUE框架来避免直接操作DOM,或者通过JQuery来比较人性化地操作DOM。

获取元素(标签)

元素也叫标签!

获取特定的元素

在整个DOM树中,有几个比较特殊的元素:html,head,body。以及一个非元素的特殊对象:窗口window

  1. 获取html元素:document.documentElement

  2. 获取head元素:document.head

  3. 获取body元素:document.body

  4. 获取窗口(非元素):window

  5. 获取页面中所有元素:document.all

    document.all是一个奇葩的falsy:

    document.all是一个falsy值。为什么:因为一开始这个API是 IE 发明的只在 IE 中才有,当时的程序员会通过document.all来判断浏览器环境,是 IE 还是其他浏览器:true就是 IE ,false就是其它。后来其它浏览器也使用了这个API,但是为了兼容以前的项目,所以在其他浏览器中,document.all是一个falsy值。

获取常规的元素

  1. 通过元素的id window.idid 直接就能获取到拥有id属性的元素,id不能和全局属性或关键字冲突。
  2. document.getElementById('id') 通过id获取元素。
  3. document.getElementsByTagName('div')[0] 通过标签名来获取元素,注意Elements是复数,获取的是一个数组。
  4. document.getElementsByClassName('red')[0]通过class来获取元素,注意Elements是复数,获取的是一个数组。
  5. document.querySelector('css选择器语法')通过css选择器来获取第一个满足条件的元素。
    • 如:document.querySelector('#haha')获取id为 haha 的元素。
    • 如:document.querySelector('.red')获取第一个class含有 red 的元素。
    • 如:document.querySelector('div')获取第一个 div 元素。
  6. document.querySelectorAll('css选择器语法')[0]通过css选择器来获取所有满足条件的元素,获取的是一个数组。

小贴士:

  • 上面 1 的方法是不太规范的方法,个人项目中可以使用,但是不要在实际开发中使用,而且前提是这个id名不和全局某些全局属性冲突如果冲突了,就只能通过 getElementById 来获取元素了。
  • 2、3、4 的方法是兼容 IE 浏览器的方法,如果不需要兼容 IE 使用 5、6 的方法更佳。

了解我们获取到的元素

我们通过 console.dir(div) 来查看页面某个 div 的详细信息。

可以发现,这个div元素也是一个对象,有着自己的属性,同时也有原型链。

  1. 第一层原型是 HTMLDivElement.prototype 。包含了所有 div元素 的共同属性。
  2. 第二层原型是 HTMLElement.prototype 。包含了所有HTML元素的属性。
  3. 第三层原型是 Element.prototype 。包含了XML、HTML标签的共有属性,因为浏览器不仅仅可以展示HTML,还能够展示其他标签语言。
  4. 第四层原型是 Node.prototype 。包含了所有节点的共有属性。
  5. 第五层原型是 EventTarget.prototype 。包含了 addEventListener 等几个方法。
  6. 第六层原型是 Object.prototype 。

新概念——节点

HTML文档中的所有内容都是节点。

如:元素Element或者叫标签Tag、文本Text、注释Comment、文档Document等,都属于节点。

我们可以通过 Node.nodeType 来查看Node节点属于哪种类型。

节点的增删改查

创建与增加节点

  1. 创建一个标签(Tag)节点

    let myDiv = document.createElement('div')
    let myStyle = document.createElement('style')
    document.createElement('script')
    document.createElement('li')
    
  2. 创建一个文本节点

    let myText = document.createTextNode('你好')
    
  3. 标签Tag中插入文本Text

    • myDiv.appendChild(myText)
    • myDiv.innerText = '你好'myDiv.textContent = '你好'
    • 不能使用 myDiv.appendChild('你好')
  4. 将创建的标签节点插入HTML中

    因为我们通过JS创建的节点是存储在内存中的JS线程里面的。

    如果要将这些节点展示在页面上,我们就需要将它们插入到 head 或 body 中它们才能让我看得见。

    // 插入到head 或 body中
    document.head.appendChild(myStyle)
    document.body.appendChild(myDiv)
    
    // 插入到页面中的其他元素中
    let div1 = document.getElementById('test')
    div1.appendChild(myDiv)
    
  5. 节点的唯一性

    // 节点的唯一性
    let myDiv = createElement('div')
    window.test1.appendChild(myDiv)
    window.test2.appendChild(myDiv)
    // myDiv只出现在了test2中,test1中并没有myDiv节点,这就是节点的唯一性
    

    如果要把同样的一个节点插入到页面的2个位置,我们需要先通过Node.cloneNode克隆一份节点(默认是浅拷贝,但可以设置为深拷贝)再把这克隆后的两节点分别插入到页面中。

移除节点

  1. 旧方法:先找自己的父元素(parentNode),再通过父元素删除自己

    myDiv.parentNode.removeChild(myDiv)

  2. 新方法:直接移除

    myDiv.remove()这个方法 IE 不支持。

  3. 上面的方法都只是把节点移除出页面,但是节点仍然存在于JS线程中,如果想要从新渲染到页面上,则再appendChild即可。

修改属性

  1. 写标准属性

    • 改class

      • div.className='red blue'这种方法会直接覆盖原来的所有class。如果想要不影响原来的属性,使用div.className += 'red blue'.

        为什么要使用className来修改class属性,因为class是一个关键字,所以不能直接 Node.class='',这样JS引擎就会优先把这个class当作关键字看待。

      • div.classList.add('red')classList是所有class的数组,这种改写class的方法不会影响原来的class。

    • 改style

      • div.style='width:100px;color:blue'同样这种修改style的方式会覆盖原先的style,尽量不要使用,而是只修改希望改变的某属性。
      • 如果只是希望修改style的某样属性,请直接修改那个属性:div.style.width = '100px'
      • 关于style直接修改某属性的属性名:css中某些属性过长,以-符号连接,在DOM修改style时,这些以-符号连接的属性名要改用驼峰命名法来取代-。如:background-color 改写成 div.style.backgroundColor。
    • 改其他标准属性

      • 这些标准属性其实就是div对象中的属性,直接通过对象的点语法就能获取并修改属性。console.dir就能够获取对象的详细信息。
      • div.id = 'newID' 就可以修改 div 的id属性。
    • 1个特别的自定义属性 data-

      • 以 data- 开头的属性,如:data-x data-haha 等,可以使用 Node.dataset.x Node.dataset.haha来直接修改属性。
      let div = document.querySelector('#div')
      div.setAttribute('data-x','AnX') // 此时div节点多了一个 data-x 属性,值为:AnX
      div.dataset.x = 'BnX' // 此时 data-x 属性值被修改成了:BnX
      
  2. 读标准属性

    • 点语法:div.classList a.href
    • getAttribute函数:div.getAttribute('class') a.getAttribute('href')
    • 上面2种方法有些区别:
      • getAttribute函数获取的是直接在标签中显示的属性如:class、id、style等;但是div对象还有很多不直接显示在标签上的属性,如:classList、className等。
      • 如:a.href输出的是一个经过加工的完整链接地址;而a.getAttribute('href')则是标签上的原始值。
      • 如:div.classNamediv.getAttribute('class')效果是一样的,但是一个是className 一个是class。
  3. 改事件处理函数

    事件机制也是元素对象的一个属性,通过给这个属性添加处理函数,当事件触发时,浏览器就能调用这个处理函数。

    以onclick为例:

    • 点语法修改事件处理函数:
      • 默认情况下,div.onclick = null;此时点击div并没有什么效果。
      • 当传入处理函数后,div.onclick=fn(),点击div时浏览器就会调用这个函数
      • 调用函数是通过fn.call(div,event)这样调用的
      • 此时div就是函数中的this
      • event则包含了点击事件的所有信息,如点击的坐标等。
    • addEventListener函数
      • 这个方法是点语法修改事件处理函数的升级版。
  4. 改内容

    • 修改文本内容

      • div.innerText = '哈哈'
      • div.textContent = '哈哈'
      • 两种修改方式基本没有区别
    • 修改具有HTML结构的内容

      • div.innerHTML = '<strong>Hi</strong>' 这里的<strong>会被认为是一个html标签来处理
      div.innerHTML = '<strong>Hi</strong>' // 加粗的 Hi
      div.innerText = '<strong>Hi</strong>' // <strong>Hi</strong>
      
      • 使用 innerHTML 时注意内容长度,过长的内容会导致页面严重卡顿
    • 改标签

      • 先清空原先的内容:div.innerHTML = ''
      • 添加修改后的内容:div.appendChild(div2)
    • 改节点位置

      • newParent.appendChild(div)由于节点的唯一性,所以原来位置的 div 会消失
  5. 查节点及其相关节点

    • 查父级节点

      • Node.parentNodeNode.parentElement
    • 查爷爷级节点

      • Node.parentNode.parentNode
    • 查子代节点

      • Node.childNodesNode.children

      • 上面2种方法的区别:chilren只会关注元素节点,childNodes会关注文本和元素节点。

        <ul id='test1'>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        
        <ul id='test2'><li>1</li><li>2</li><li>3</li></ul>
        
        test1.childNodes.length //7 [text, li, text, li, text, li, text]
        test1.children.length // 3 [li, li, li]
        test2.childNodes.length //3 [li, li, li]
        
    • 查兄弟姐妹节点Siblings

      • 先找父亲节点 Node.parentNode

      • 让父亲节点找它的自己节点Node.parentNode.childNodes这里也包括自己

      • 把父亲节点的子节点排除掉自己就是所有的兄弟姐妹节点

        for(let i = 0;i<Node.parentNode.childNodes.length;i++){
            if(Node.parentNode.childNodes[i]===Node){
                return Node.parentNode.childNodes.splice(i,1)
            }
        }
        
    • 查第一个子节点

      • Node.firstChild
    • 查最后一个子节点

      • Node.lastChild
    • 查临近的兄弟节点

      • 上一个兄弟节点:Node.previousSibling
      • 上一个节点类型为Element的兄弟节点:Node.previousElementSibling
      • 下一个兄弟节点:Node.nextSibling
      • 下一个节点类型为Element的兄弟节点:Node.nextElementSibling
    • 查某个节点下的所有节点

      let traval (node,fn) =>{
          fn(node)
          if(node.children){
              for(let i = 0;i<node.children.length;i++){
                  traval (node.children[i],fn)
              }
          }
      }
      traval(Node,(a)=>{console.log(a)}) // 输出所有节点