04-再探JavaScript

一. DOM介绍

  1. 什么是DOM?

  DOM:文档对象模型。DOM 为文档提供了结构化表示,并定义了如何通过脚本来访问文档结构。

  目的其实就是为了能让js操作html元素而制定的一个规范。

  DOM就是由节点组成的。

  2. 解析过程

  HTML加载完毕,渲染引擎会在内存中把HTML文档,生成一个DOM树,getElementById是获取内中DOM上的元素节点。然后操作的时候修改的是该元素的属性

  3. DOM树(一切都是节点)

  DOM的数据结构如下:

  上图可知,在HTML当中,一切都是节点:(非常重要)

    • 元素节点:HMTL标签。

    • 文本节点:标签中的文字(比如标签之间的空格、换行)

    • 属性节点::标签的属性。

  整个html文档就是一个文档节点。所有的节点都是Object。

  4. DOM可以做什么?
    • 找对象(元素节点)

    • 设置元素的属性值

    • 设置元素的样式

    • 动态创建和删除元素

    • 事件的触发响应:事件源、事件、事件的驱动程序

 二. DOM节点的获取

  DOM节点的获取方式其实就是获取事件源的方式

  操作元素节点,必须首先找到该节点。有三种方式可以获取DOM节点:

var div1 = document.getElementById("box");      //方式一:通过id获取单个标签

var arr1 = document.getElementsByTagName("div");     //方式二:通过 标签名 获得 标签数组,所以有s

var arr2 = document.getElementsByClassName("box");  //方式三:通过 类名 获得 标签数组,所以有s

  既然方式二、方式三获取的是标签数组,那么习惯性是先遍历之后再使用

  特殊情况:数组中的值只有1个。即便如此,这一个值也是包在数组里的。这个值的获取方式如下:

document.getElementsByTagName("div")[0];    //取数组中的第一个元素

document.getElementsByClassName("box")[0];  //取数组中的第一个元素

三. DOM访问关系的获取

  DOM的节点并不是孤立的,因此可以通过DOM节点之间的相对关系对它们进行访问。如下:

  节点的访问关系,是以属性的方式存在的。

  JS中的父子兄访问关系:

  这里我们要重点知道parentNodechildren这两个属性的用法。下面分别介绍。

  1. 获取父节点

  调用者就是节点。一个节点只有一个父节点,调用方式就是

节点.parentNode

  (1)nextSibling:

       指的是下一个节点(包括标签、空文档和换行节点)

    • 火狐、谷歌、IE9+版本:都指的是下一个节点(包括标签、空文档和换行节点)。

    • IE678版本:指下一个元素节点(标签)。

  (2)nextElementSibling:

    • 火狐、谷歌、IE9+版本:都指的是下一个元素节点(标签)。

  总结:为了获取下一个元素节点,我们可以这样做:在IE678中用nextSibling,在火狐谷歌IE9+以后用nextElementSibling,于是,综合这两个属性,可以这样写:

下一个兄弟节点 = 节点.nextElementSibling || 节点.nextSibling

  previous的中文是: 前一个

  (1)previousSibling:

    • 火狐、谷歌、IE9+版本:都指的是前一个节点(包括标签、空文档和换行节点)。

    • IE678版本:指前一个元素节点(标签)。

  (2)previousElementSibling:

    • 火狐、谷歌、IE9+版本:都指的是前一个元素节点(标签)。

  总结:为了获取前一个元素节点,我们可以这样做:在IE678中用previousSibling,在火狐谷歌IE9+以后用previousElementSibling,于是,综合这两个属性,可以这样写:

前一个兄弟节点 = 节点.previousElementSibling || 节点.previousSibling

  补充:获得任意一个兄弟节点:

节点自己.parentNode.children[index];  //随意得到兄弟节点
节点自己.parentNode.children;  //获取所有的兄弟节点(伪数组,包含自己在内)
  2. 获取单个的子元素

  2.1. 第一个子节点 | 第一个子元素节点

  (1)firstChild:

    • 火狐、谷歌、IE9+版本:都指的是第一个子节点(包括标签、空文档和换行节点)。

    • IE678版本:指第一个子元素节点(标签)。

  (2)firstElementChild:

    • 火狐、谷歌、IE9+版本:都指的是第一个子元素节点(标签)。

  总结:为了获取第一个子元素节点,我们可以这样做:在IE678中用firstChild,在火狐谷歌IE9+以后用firstElementChild,于是,综合这两个属性,可以这样写:

第一个子元素节点 = 节点.firstElementChild || 节点.firstChild

  2.2. 最后一个子节点 | 最后一个子元素节点

  (1)lastChild:

    • 火狐、谷歌、IE9+版本:都指的是最后一个子节点(包括标签、空文档和换行节点)。

    • IE678版本:指最后一个子元素节点(标签)。

  (2)lastElementChild:

    • 火狐、谷歌、IE9+版本:都指的是最后一个子元素节点(标签)。

  总结:为了获取最后一个子元素节点,我们可以这样做:在IE678中用lastChild,在火狐谷歌IE9+以后用lastElementChild,于是,综合这两个属性,可以这样写:

最后一个子元素节点 = 节点.lastElementChild || 节点.lastChild
  3. 获取所有的子节点

  (1)childNodes:标准属性。返回的是指定元素的子节点的集合(包括元素节点、所有属性、文本节点)。是W3C的亲儿子。

    • 火狐 谷歌等高本版会把换行也看做是子节点。(了解)

  用法:

子节点数组 = 父节点.childNodes;   //获取所有节点。

  (2)children:非标准属性。返回的是指定元素的子元素节点的集合。【重要】

    • 它只返回HTML节点,甚至不返回文本节点。
    • 在IE6/7/8中包含注释节点(在IE678中,注释节点不要写在里面)。

  虽然不是标准的DOM属性,但它和innerHTML方法一样,得到了几乎所有浏览器的支持。

  用法:(用的最多

子节点数组 = 父节点.children;   //获取所有节点。用的最多。

四. 关于DOM的事件操作

  JS是以事件驱动为核心的一门语言。

  1. 事件的三要素

  事件的三要素:事件源、事件、事件驱动程序

  比如,我用手去按开关,灯亮了。这件事情里,事件源是:手。事件是:按开关。事件驱动程序是:灯的开和关。

  再比如,网页上弹出一个广告,我点击右上角的X,广告就关闭了。这件事情里,事件源是:X。事件是:onclick。事件驱动程序是:广告关闭了。

  于是我们可以总结出:谁引发的后续事件,谁就是事件源。

  总结如下:

    • 事件源:引发后续事件的html标签。

    • 事件:js已经定义好了(见下图)。

    • 事件驱动程序:对样式和html的操作。也就是DOM。

  代码书写步骤如下:(重要)

  1. 获取事件源:document.getElementById(“box”); 

  2. 绑定事件: 事件源box.事件onclick = function(){ 事件驱动程序 };

  3. 书写事件驱动程序:关于DOM的操作

  代码举例:

点击div盒子使背景色改为red。

<style>
    #box{
        100px;
        height: 100px;
        background: green;
    }
</style>


<div id="box"></div>


<script>
var odiv = document.getElementById("box");
odiv.onclick = function(){
    odiv.style.background = "red";
}
</script>

  常见事件如下:

  下面针对这事件的三要素,进行分别介绍。

  1. 获取事件源的方式

  这个参照上面的介绍。

  2. 绑定事件的方式

  方式一:直接绑定匿名函数

<script>
    var odiv = document.getElementById("box");
    odiv.onclick = function(){
        odiv.style.background = "red";
    }
</script>

  方式二:先单独定义函数,再绑定

<script>
    var odiv = document.getElementById("box");
    odiv.onclick = fn; //注意,这里是fn,不是fn()。fn()指的是返回值。
    //单独定义函数
    function fn(){
        odiv.style.background = "red";
    }
</script>

  方式三:行内绑定

// 行内绑定
<div id="box" onclick="fn()"></div>
// 注意第一行代码,绑定时,是写的"fn()",不是写的"fn"。因为绑定的这段代码不是写在js代码里的,而是被识别成了字符串。


<script>
    function fn(){
        odiv.style.background = "red";
    }
</script>
  3. 事件驱动程序

  我们上面的例子中改变背景颜色,就是事件驱动程序。

  需求:

    1. 默认盒子宽度和高度为100px,背景色为绿色;

    2. 单击后的效果为盒子宽度和高度为200px,背景色为红色;

    3. 让其上面的效果可以来回进行切换。

  请看代码:

<script>
    var flag = false;
    var odiv = document.getElementById("box");
    odiv.onclick = function () {
        if (!flag){
            odiv.style.width = "200px";
            odiv.style.height = "200px";
            odiv.style.backgroundColor = "red";
            flag = true;
        }else{
            odiv.style.width = "100px";
            odiv.style.height = "100px";
            odiv.style.backgroundColor = "green";
            flag = false;
        }
    };
</script>

  上方代码的注意事项:

    • 在js里写属性值时,要用引号
    • 在js里写属性名时,是backgroundColor,不是CSS里面的background-Color。记得所有的像css属性的text-*,line-*、backgroun-*等在js中都写成驼峰
   4. onload事件

  当页面加载(文本和图片)完毕的时候,触发onload事件。

  举例:

<head>
    <script>
        window.onload = function () {
            console.log(111);
        }
    </script>
</head>
<body>
    <script>
        console.log(222);
    </script>
</body>

  有一点我们要知道:js的加载是和html同步加载的。因此,如果使用元素在定义元素之前,容易报错。这个时候,onload事件就能派上用场了,我们可以把使用元素的代码放在onload里,就能保证这段代码是最后执行。

  建议是:整个页面上所有元素加载完毕在执行js内容。所以,window.onload可以预防使用标签在定义标签之前。

  2. 事件案例

  案例一:

<input type="text" id="input1" value="隐藏">
<button id="btn1">隐藏</button>

<div id="box"></div>

<script>
    var oBtn = document.getElementById("btn1");
    var oDiv = document.getElementById("box");
    var oInput = document.getElementById("input1");
    var flag = false;
    oBtn.onclick = function () {
        if (!flag){
            oInput.value = "显示";
            oDiv.style.display = "none";
            oBtn.innerText = "显示";
            flag = true;
        }else{
            oInput.value = "隐藏";
            oDiv.style.display = "block";
            oBtn.innerHTML = "<span>隐藏</span>";
            flag = false;
        }
    }
</script>

  案例二:

  要求实现效果:

    1. 当鼠标悬停在img上时,更换为另外一张图片;

    2. 鼠标离开时,还原为本来的图片。

<img src="https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2121206715,2955288754&fm=26&gp=0.jpg" alt="" id="image">

<script>
    var oImg = document.getElementById("image");
    oImg.onmouseover = function () {
        this.src = "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3488187566,3835668069&fm=26&gp=0.jpg";
    };
    oImg.onmouseout = function () {
        this.src = "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2121206715,2955288754&fm=26&gp=0.jpg";
    }
</script>

五. DOM节点操作(重要)

  前面的内容:节点的访问关系都是属性

  节点的操作都是函数(方法)

  1. 创建节点

  格式如下:

新的标签(元素节点) = document.createElement("标签名");

  比如,如果我们想创建一个li标签,或者是创建一个不存在的adbc标签,可以这样做:

<script type="text/javascript">
    var a1 = document.createElement("li");   //创建一个li标签
    var a2 = document.createElement("adbc");   //创建一个不存在的标签

    console.log(a1);
    console.log(a2);

    console.log(typeof a1);
    console.log(typeof a2);
</script>

  结果:

  2. 插入节点

  插入节点有两种方式,它们的含义是不同的。

  方式1:

父节点.appendChild(新的子节点);

    解释:父节点的最后插入一个新的子节点。

  方式2:

父节点.insertBefore(新的子节点,作为参考的子节点);

    解释:

    • 在参考节点前插入一个新的节点。

    • 如果参考节点为null,那么他将在父节点最后插入一个子节点。

  3. 删除节点

  格式如下:

父节点.removeChild(子节点);

  解释:用父节点删除子节点。必须要指定是删除哪个子节点。

  如果我想删除自己这个节点,可以这么做:

node1.parentNode.removeChild(node1);
  4. 复制节点(克隆节点)

  格式如下:

要复制的节点.cloneNode();       //括号里不带参数和带参数false,效果是一样的。

要复制的节点.cloneNode(true);

  括号里带不带参数,效果是不同的。解释如下:

    • 不带参数/带参数false:只复制节点本身,不复制子节点。

    • 带参数true:既复制节点本身,也复制其所有的子节点。

六. 设置节点的属性

  我们可以获取节点的属性值、设置节点的属性值、删除节点的属性。

  我们就统一拿下面这个标签来举例:

<img src="https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2121206715,2955288754&fm=26&gp=0.jpg"
 class="image_box" title="美女图片" alt="美女走丢了" id="a1">

  下面分别介绍。

  1. 获取节点的属性值

  方式1:

元素节点.属性;
元素节点[属性];

  举例:(获取节点的属性值)

<script>
    var myNode = document.getElementsByTagName("img")[0];
    console.log(myNode.src);
    console.log(myNode.className);    //注意,是className,不是class
    console.log(myNode.title);
    console.log("------------");
    console.log(myNode["src"]);
    console.log(myNode["className"]); //注意,是className,不是class
    console.log(myNode["title"]);
</script>

  方式2:(推荐)

素节点.getAttribute("属性名称");

  例子:

console.log(myNode.getAttribute("src"));
console.log(myNode.getAttribute("class"));   //注意是class,不是className
console.log(myNode.getAttribute("title"));

  方式1和方式2的区别在于:前者是直接操作标签,后者是把标签作为DOM节点。推荐方式2。

  2. 设置节点的属性值

  方式1:

  举例:(设置节点的属性值)

myNode.src = "images/2.jpg"   //修改src的属性值
myNode.className = "image2-box";  //修改class的name
myNode['title'] = "漂亮吗";  // 修改title的属性值

  方式2:(推荐)

元素节点.setAttribute(属性名, 新的属性值);

  举例:(设置节点的属性值)

myNode.setAttribute("src","images/3.jpg");
myNode.setAttribute("class","image3-box");
myNode.setAttribute("id","aaa");
  3. 删除节点属性

  格式:

元素节点.removeAttribute(属性名);

  举例:(删除节点的属性)

myNode.removeAttribute("class");
myNode.removeAttribute("id");

  DOM操作案例请移步:戳我啊!

七. 定时器

  在js中的定时器分两种:1、setTimeout() 2、setInterval()

  1. setTimeOut()

  只在指定时间后执行一次

<button id="btn1">开始</button>
<button id="btn2">清除</button>

<script>
    var oBtn1 = document.getElementById("btn1");
    var timer = null;
    oBtn1.onclick = function () {
        timer = setTimeout(function () {
            console.log(1111);
        },3000)
    };
    var oBtn2 = document.getElementById("btn2");
    oBtn2.onclick = function () {
        clearTimeout(timer);
    }
</script>
  2.setInterval()

  在指定时间为周期循环执行

<button id="start">开启定时器</button>
<button id="stop">关闭定时器</button>
<div id="box"></div>

<script>
    
    var oDiv = document.getElementById("box");
    var oStart = document.getElementById("start");
    var oStop = document.getElementById("stop");
    var timer = null;
    var m_left = 0;
    oStart.onclick = function(){
        clearInterval(timer); //每次点击时,先清除原来的定时任务。
        timer = setInterval(function () {
            m_left += 10;
            oDiv.style.marginLeft = m_left + "px";
        },1000) // 单位为毫秒
    };
    // 清除定时任务
    oStop.onclick = function () {
        clearInterval(timer);
    }
</script>

  两种方法根据不同的场景和业务需求择而取之,

  对于这两个方法,需要注意的是如果要求在每隔一个固定的时间间隔后就精确地执行某动作,那么最好使用setInterval。

八. BOM介绍

  1. 什么是BOM

  BOM:Browser Object Model,浏览器对象模型。

  BOM的结构图:

  从上图也可以看出:

    • window对象是BOM的顶层(核心)对象,所有对象都是通过它延伸出来的,也可以称为window的子对象。

    • DOM是BOM的一部分。

  window对象:

    • window对象是JavaScript中的顶级对象

    • 全局变量、自定义函数也是window对象的属性和方法。

    • window对象下的属性和方法调用时,可以省略window。

  下面讲一下 BOM 的常见内置方法和内置对象

  2. 弹出系统对话框

  比如说,alert(1)window.alert(1)的简写,因为它是window的子方法。

  系统对话框有三种:

alert();    //不同浏览器中的外观是不一样的
confirm();  //兼容不好
prompt();   //不推荐使用
  3. 打开窗口和关闭窗口

  (1)打开窗口:

window.open(url,target)

  参数解释:

    • url:要打开的地址。

    • target:新窗口的位置。可以是:_blank、_self、_parent 父框架。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>

        <button>打开百度</button>
        <button>关闭百度</button>

    </body>
    <script>
        var oBtn = document.getElementsByTagName('button')[0];
        var closeBtn = document.getElementsByTagName('button')[1];
        var myWindow = null;
        oBtn.onclick = function(){
            myWindow = open('https://www.baidu.com');
            //打开空白页面
            // open('about:blank',"_self")
        };
        closeBtn.onclick = function(){
            if(confirm("是否关闭?")){
                myWindow.close();
            }
        }

    </script>
</html>
  4. location对象

  window.location可以简写成location。location相当于浏览器地址栏,可以将url解析成独立的片段。

  4.1. location对象的属性

    • href:跳转

    • hash 返回url中#后面的内容,包含#

    • host 主机名,包括端口

    • hostname 主机名

    • pathname url中的路径部分

    • protocol 协议 一般是http、https

    • search 查询字符串

    location.href属性举例

    举例:点击盒子时,进行跳转。

<body>
<div>点我</div>
<script>

    var div = document.getElementsByTagName("div")[0];

    div.onclick = function () {
        location.href = "http://www.baidu.com";   //点击div时,跳转到指定链接
 //     window.open("http://www.baidu.com","_blank");  //方式二
    }

</script>
</body>

  4.2. location对象的方法

    location.reload():重新加载

setTimeout(function(){
         //3秒之后让网页整个刷新
    window.location.reload();  
            
},3000)
  5. history对象

    5.1. 后退:

    • history.back()

    • history.go(-1):0是刷新

    5.2. 前进:

    • history.forward()
    • history.go(1)

    用的不多。因为浏览器中已经自带了这些功能的按钮

 九. js中的面向对象(了解)

  创建对象的几种常用方式

  1.使用Object或对象字面量创建对象

  2.工厂模式创建对象

  3.构造函数模式创建对象

  4.原型模式创建对象

  1.使用Object或对象字面量创建对象

  JS中最基本创建对象的方式:

var student = new Object();
student.name = "easy";
student.age = "20";

  这样,一个student对象就创建完毕,拥有2个属性name以及age,分别赋值为"easy"20

  如果你嫌这种方法有一种封装性不良的感觉。来一个对象字面量方式创建对象。

var sutdent = {
  name : "easy",
  age : 20
};

  这样看起来似乎就完美了。但是马上我们就会发现一个十分尖锐的问题:当我们要创建同类的student1,student2,…,studentn时,我们不得不将以上的代码重复n次....

var sutdent1 = {
  name : "easy1",
  age : 20
};

var sutdent2 = {
  name : "easy2",
  age : 20
};

...

var sutdentn = {
  name : "easyn",
  age : 20
};

  有个提问?能不能像工厂车间那样,有一个车床就不断生产出对象呢?我们看”工厂模式”。

  2.工厂模式创建对象

  JS中没有类的概念,那么我们不妨就使用一种函数将以上对象创建过程封装起来以便于重复调用,同时可以给出特定接口来初始化对象

function createStudent(name, age) {
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  return obj;
}

var student1 = createStudent("easy1", 20);
var student2 = createStudent("easy2", 20);
...
var studentn = createStudent("easyn", 20);

  这样一来我们就可以通过createStudent函数源源不断地”生产”对象了。看起来已经高枕无忧了,但贪婪的人类总有不满足于现状的天性:我们不仅希望”产品”的生产可以像工厂车间一般源源不断,我们还想知道生产的产品究竟是哪一种类型的。

  比如说,我们同时又定义了”生产”水果对象的createFruit()函数:

function createFruit(name, color) {
  var obj = new Object();
  obj.name = name;
  obj.color = color;
  return obj;
}

var v1 = createStudent("easy1", 20);
var v2 = createFruit("apple", "green");

  对于以上代码创建的对象v1、v2,我们用instanceof操作符去检测,他们统统都是Object类型。我们的当然不满足于此,我们希望v1是Student类型的,而v2是Fruit类型的。为了实现这个目标,我们可以用自定义构造函数的方法来创建对象

  3.构造函数模式创建对象

  在上面创建Object这样的原生对象的时候,我们就使用过其构造函数:

var obj = new Object();

  在创建原生数组Array类型对象时也使用过其构造函数:

var arr = new Array(10);  //构造一个初始长度为10的数组对象

  在进行自定义构造函数创建对象之前,我们首先了解一下构造函数普通函数有什么区别。

  1、实际上并不存在创建构造函数的特殊语法,其与普通函数唯一的区别在于调用方法。对于任意函数,使用new操作符调用,那么它就是构造函数;不使用new操作符调用,那么它就是普通函数。

  2、按照惯例,我们约定构造函数名以大写字母开头,普通函数以小写字母开头,这样有利于显性区分二者。例如上面的new Array(),new Object()。

  3、使用new操作符调用构造函数时,会经历(1)创建一个新对象;(2)将构造函数作用域赋给新对象(使this指向该新对象);(3)执行构造函数代码;(4)返回新对象;4个阶段。

  ok,了解了构造函数和普通函数的区别之后,我们使用构造函数将工厂模式的函数重写,并添加一个方法属性: 

function Student(name, age) {
  this.name = name;
  this.age = age;
  this.alertName = function(){
    alert(this.name)
  };
}

function Fruit(name, color) {
  this.name = name;
  this.color = color;
  this.alertName = function(){
    alert(this.name)
  };
}

  这样我们再分别创建Student和Fruit的对象:

var v1 = new Student("easy", 20);
var v2 = new Fruit("apple", "green");

  这时我们再来用instanceof操作符来检测以上对象类型就可以区分出Student以及Fruit了:

alert(v1 instanceof Student);  //true
alert(v2 instanceof Student);  //false
alert(v1 instanceof Fruit);  //false
alert(v2 instanceof Fruit);  //true

alert(v1 instanceof Object);  //true 任何对象均继承自Object
alert(v2 instanceof Object);  //true 任何对象均继承自Object

  这样我们就解决了工厂模式无法区分对象类型的尴尬。那么使用构造方法来创建对象是否已经完美了呢?使用构造器函数通常在js中我们来创建对象。

  我们会发现Student和Fruit对象中共有同样的方法,当我们进行调用的时候这无疑是内存的消耗。

  我们完全可以在执行该函数的时候再这样做,办法是将对象方法移到构造函数外部:

function Student(name, age) {
  this.name = name;
  this.age = age;
  this.alertName = alertName;
}

function alertName() {
  alert(this.name);
}

var stu1 = new Student("easy1", 20);
var stu2 = new Student("easy2", 20);

  在调用stu1.alertName()时,this对象才被绑定到stu1上。

  我们通过将alertName()函数定义为全局函数,这样对象中的alertName属性则被设置为指向该全局函数的指针。由此stu1和stu2共享了该全局函数,解决了内存浪费的问题

  但是,通过全局函数的方式解决对象内部共享的问题,终究不像一个好的解决方法。如果这样定义的全局函数多了,我们想要将自定义对象封装的初衷便几乎无法实现了。更好的方案是通过原型对象模式来解决。

  4.原型的模式创建对象

  原型链甚至原型继承,是整个JS中最难的一部分也是最不好理解的一部分,在这里由于我们课程定位的原因,如果对js有兴趣的同学,可以去查阅一下相关JS原型的一些知识点。更加有助于你以后前端JS的面试。

function Student() {
    this.name = 'easy';
    this.age = 20;
}


Student.prototype.alertName = function(){
    alert(this.name);
};

var stu1 = new Student();
var stu2 = new Student();

stu1.alertName();  //easy
stu2.alertName();  //easy

alert(stu1.alertName == stu2.alertName);  //true 二者共享同一函数
原文地址:https://www.cnblogs.com/Michael--chen/p/10857703.html