JavaScript引用类型的实现方法
这篇文章主要讲解了JavaScript引用类型的实现方法,内容清晰明了,对此有兴趣的小伙伴可以学习一下,相信大家阅读完之后会有帮助。
创新互联是一家专业提供蚌埠企业网站建设,专注与成都做网站、成都网站设计、成都外贸网站建设、H5响应式网站、小程序制作等业务。10年已为蚌埠众多企业、政府机构等服务。创新互联专业网络公司优惠进行中。
数组
在ECMAScript中数组是非常常用的引用类型
ECMAScript所定义的数组和其他语言中的数组有着很大的区别
数组也是一种对象
创建数组
//方法一 var arr = new Array(); //方法二 var arr1 = [];
特点
数组即一组数据的集合
js数组更加类似java的map容器。长度可变,元素类型任意
数组长度随时可变!随时可以修改!(length属性)
var arr1 = [123,324,true,'abc',1,4,5,new Date()]; arr1.length = 5; console.log( arr1 ); //log 里面是传递的字符串 , JS引擎默认会调用.toString(); 隐式的调用.
常用方法
push、pop
shift、unshift
splice、slice
concat、join、sort、reverse(逆序)
影响原数组 splice() arr.splice(开始项,[控制几位数,值]); //statr ,count , var arr = [1,2,3,4,5,6,7]; var zf = arr.splice(3); //控制几位数, 默认值 :arr.length-1 //包括3 原来项数 console.log( arr ); //[1,2,3] console.log( zf ); //[4,5,6,7] //返回值 -- 取出的内容 var arr = [1,2,3,4,5,6,7]; var zf = arr.splice(3,2); //截取 console.log( arr ); //[1, 2, 3, 6, 7] console.log( zf ); //[4,5] var arr = [1,2,3,4,5,6,7]; var t = arr.splice(3,2,'zf','dd'); //替换 console.log( arr ); //[1,2,'zf','dd',6,7] console.log( t ); //[4,5] var arr = [1,2,3,4,5,6,7]; var t = arr.splice(3,0,'zf','dd'); //插入 console.log(arr); //[1,2,3,'zf','dd',4,5,6,7] console.log( t ); //[] //如果为0 ,去除空数组 var arr = [1,2,3,4,5,6,7]; var 12 = arr.splice(-4); //截取 console.log( arr ); //[1,2,3] console.log( zf ); //[4,5,6,7]
// slice(stat,end) //去出了end-stat 项。 不包括end项。
var zf = arr.sort(function ( a,b ) { //传递匿名函数,通过匿名函数参数判断大小。 if( a>b ){ return 1; } else if( a
ES5数组新特性
位置方法:indexOf lastIndexOf
迭代方法:every filter forEach some map
缩小方法:reduce reduceRight
// indexOf(); //查找位置 var arr = [234,23,45,46,45,645,56]; //1个参数的时候,表示传值 返回索引位置 var idxVal = arr.indexOf(45); //2个参数的时候, 第一个表示查询的值,第二个参数是 传值,表示起始开始查询的起始位置 var idxVal = arr.indexOf(2,3); //查找数组比较的时候 是 "===" //找不到返回 -1
//迭代方法 //every : 对于数组每一个元素进行一个函数的运行 如果函数都返回true, 最后则返回true。 如果有一个返回false 最后结果则返回false。 // 测试数组的所有元素是否都通过了指定函数的测试 var arr = [1,2,3,4,3,2,4,6]; var reslut = arr.every(function ( item,index,array ) { return item > 0; }); console.log( reslut ); //true //filter : 对于数组的每一个元素进行一个函数的运行 给定的函数执行, 把过滤后的结果返回。 var arr = [1,2,3,4,3,2,4,6]; var reslut = arr.filter(function ( item,index,array ) { return item > 2; //所有大于2 的过滤出来 }) console.log( reslut ); //[3, 4, 3, 4, 6] //forEach : 循环数组每一项, 并执行一个方法 // 方法中的参数:数组成员的值,数组成员的索引,原数组(修改原数组会影响原来遍历的数组) var arr = [1,2,3,4,3,2,4,6]; arr.forEach(function ( item,index,array ) { console.log( item ); }); var arr1 = ['tan', 'cyan', 'pink', 'red']; arr1.forEach(function ( val, idx, arrs ) { return 1; // 返回返回并不会影响原数组 }); console.log(arr); //map : 对于数组的每一个元素进行一个函数的运行 可以经过函数执行完毕 把新的结果返回, 原数组不变。 var arr = [1,2,3,4,3,2,4,6]; var reslut = arr.map(function ( item,index,array ) { return item * 3; }); console.log( reslut ); //[3, 6, 9, 12, 9, 6, 12, 18] //some : 对于数组每一个元素进行一个函数的运行 如果有一项返回true 最后则返回true 如果每一项都返回false, 最后才返回false。 var arr = [1,2,3,4,3,2,4,6]; var reslut = arr.some(function ( item,index,array ) { return item > 5; //有一个返回true, 就返回true }); console.log( reslut ); //true
//模拟filter方法 Array.prototype.filter = function ( cb ) { var reslut = []; try{ if ( cb && cb.constructor === Function ) { for ( var i=0; i2; }); console.log( a ); // 模拟some Array.prototype.some = function ( fn ) { try{ if ( fn && fn.constructor === Function ) { for ( var i=0; i
var arr = [1,2,3,4,3,2,4,6]; // reduce reduceRight // 前一个值, 当前值, 索引位置, array // 数组中的每个值(从左到右)开始合并,最终为一个值。 // 接收一个函数作为累加器,数组中的每一个值(从左到右)开始合并,最终为一个值。 var reslut = arr.reduce(function ( prev,cur,index,array ) { return prev + cur; }); console.log( reslut ); //25 // reduceRight 从右开始遍历 var reslut1 = arr.reduceRight(function ( prev,cur,index,array ) { return prev + cur; }); console.log( reslut ); //25
// 得到接口的对象 var o = (function() { var person = { name: 'xixi', age: 22, } return { sayName: function(k) { return person[k]; }, } }()); var person = ['name', 'age'].reduce(function (obj, k) { // console.log(obj, k,'--'); obj[k] = o.sayName(k); return obj; },{}); console.log(person);
数组判断方法:
Array.isArray();
判断是否为数组,如果是,则返回true,否则返回false。
var arr = []; console.log( Array.isArray(arr) );
填充方法:
fill();
实现对数组的填充
参数:接受值,直接填充,如果是函数,也是直接填充
// arr.fill(1); // arr.fill(function () { // return 2; // }); // arr.fill([1,2,3]); arr.fill({x: 1}); console.log(arr);
Object
引用类型都是Object类型的实例,Object也是ECMAScript中使用最多的一种类型(就像java.lang.Object一样,Object类型是所有它的实例的基础) //所有类的 基础类。
Object类型的创建方式、使用
对于Object类型应用for in 枚举循环
Obj每个实例都具有属性和方法
Constructor: 保存着用于创建当前对象的函数。(构造函数)
hasOwnProperty(propertyName):用于检测给定的属性在当前对象实例中(而不是原型中)是否存在。
isPrototypeOf(Object): 用于检查传入的对象是否是另外一个对象的原型。
propertyIsEnumerable(propertyName):用于检查给定的属性是否能够使用for-in语句来枚举。
toLocaleString():返回对象的字符串表示。该字符串与执行环境的地区对应.
toString():返回对象的字符串表示。
valueOf():返回对象的字符串、数值或布尔表示。
OBject.prototype.toString()
Object.prototype.toString
作用:根据内部的this返回一个类似于这样的字符串 [object constructorName]
这个方法有个缺点,不能获取用户自定义对象的具体类型.
只能获取内置对象的类型.
自定义对象类型都返回:[object Object]
console.log(Object.prototype.toString.call([])); // [object Array] console.log(Object.prototype.toString.call(Array)); // [object Fcuntion] console.log(Object.prototype.toString.call(new Date())); // [object Date] // 简写方式 console.log(({}).toString.call([])); // [object Array]
toString();
// 字符串 ==> String.prototype.toString(); var a = 't'; console.log(a.toString()); // t var a = new String(); console.log(a.toString()); // 空字符串 var a = new String('string'); console.log(a.toString()); // string // 数组 ==> Array.prototype.toString(); var b = [1, 2, 3, 4]; console.log(b.toString()); // 1,2,3,4 var b = []; console.log(b.toString()); // 空字符串 var b = new Array(); console.log(b.toString()); // 空字符串 var b = new Array(3, 5); console.log(b.toString()); // 3,5 // 对象 ==> Object.prototype.toString(); var c = {}; console.log(c.toString()); // [object Object] // 函数 console.log(Function.toString()); // function Function() { [native code] } console.log(Array.toString()); // function Array() { [native code] } console.log(RegExp.toString()); // function RegExp() { [navtive code] }
关于JSON函数
JSON.parse()
作用:将JSON字符串解析成JavaScirpt值。在解析过程中,可以选择性的修改某些属性的原始解析值。
参数1:JSON字符串
参数2: reviver 函数,用来转换解析出的属性值。(可选参数)
返回值:解析出的一个 Object
console.log(JSON.parse(10)); // 10 console.log(JSON.parse(true)); // true console.log(JSON.parse('"xixi"')); // xixi console.log(JSON.parse(null)); // null console.log(JSON.parse('"undefined"')); // undefined console.log(JSON.parse("[]")); // []
如果指定了 reviver 函数,解析的出的Object, 解析值本身以及它所包含的所有属性,会按照一定的顺序(从最最里层的属性开始,一级级往外,最终到达顶层)分别去调用 指定的reviver 函数。
在调用过程中,当前属性所属的对象会作为this值,当前属性名和属性值会分别作为第一个参数和第二个参数传入 reviver 函数中,如果 reviver 函数返回undefeind,则当前属性会从属性对象中删除,如果返回了其它值,则返回的值会成为当前属性新的属性值。
当遍历到最顶层的值(解析值)时,传入reviver函数的参数会是空字符串''(因为此时已经没有真正的属性)和当前的解析值(有可能已经被修改过),当前的this值会是{"":修改过的解析值}
,
JSON.parse('{"p": 5}', function (key, val) { if(key === '') return val; // 如果到了最顶层,则直接返回属性值 return val * 2; });
JSON.stringify();
stringify(value [,replacer, [space]]);
将任意的JavaScript值序列化成JSON字符
参数1:value: 将序列化成JSON字符串的值
参数2:replacer: 可选,如果是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理。 如果是一个数组,则暴行在这数组中的属性名才会被序列化到最终的JSON字符串中。
参数3:space, 指定缩进可通的空白字符串,用于美化输出。 控制结果字符串里的间距。
注意:
不可枚举的属性会被忽略
非数组对象的属性不能保证以特定的顺序出现的序列化后的字符串中。
布尔值,数字,字符串的包装对象在序列化过程中会自动装换成对应的原始值。
undefeind,任意的函数,以及symbol值,在徐泪花过程横纵挥别忽略(出现在非数组对象的属性值中时)或者被转换成null(出现在数组中时)
所有以symbol为属性键的属性值都会被完全忽略掉,即便 replacer参数中强制指定包含了它们
console.log(JSON.stringify([undefined, Object, Symbol()])); // null, null, null console.log(JSON.stringify({x: undefined, y: Object, z: Symbol()})); // {}
Object.create
ES5为对象提供了一个Object.create();
作用:创建一个类,是一种寄生式继承。
返回一个类,这个类的原型指向传递进来的对象。
创建的实例化对象,构造函数指向的是继承的对象的类的构造函数。本身没有原型,可以通过原型链找到继承的对象类中的原型。具有继承对象上的属性以及方法。
因此,创建的实例化对象可以使用继承对象上的属性和方法。
var Book = function (title, price) { this.title = title; this.price = price; } Book.prototype.sayTitle = function () { return this.price; } var book = new Book('one', 10); // 创建一个继承类 var NewBook = Object.create(book); console.log(NewBook.constructor); var price = NewBook.sayTitle(); console.log(price);
// 模拟 Object.create(); // 寄生式继承 Object.prototype.create = function ( obj ) { try{ if ( obj && obj.constructor === Object ) { function F () {} F.prototype = obj; return F; } }catch(e){ //TODO handle the exception } } // 创建一个继承类 var NewBook = Object.create(book); console.log(NewBook.constructor); var price = NewBook.sayTitle(); console.log(price);
Object.defineProperty
Object.defineProperty();
直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象。
参数1:需要被设置的对象
参数2:设置的属性名
参数3:配置项,添加属性的特性属性
特性属性:value
:该属性值,默认值:undefeindwritable
: 可否被修改,默认值:false (不能被修改)configuarable
: 能否通过delete删除属性从而重新定义属性,能够修改属性的特性,或者能否把属性修改为访问属性。 默认值:false;(不可以重新定义或删除)enumerable
: 是否可以被for-in枚举。默认值:false
var obj = {}; Object.defineProperty(obj, 'title', { value: 'tan', // writable: true, configurable: false, // enumerable: true }); delete obj.title; // obj.title = 'pink'; // for ( var i in obj ) { // // console.log(obj[i]); // // } console.log(obj);
特性方法:
set:给属性提供setter的方法,如果没有setter则为undefined。默认值undefiend。
参数:该参数的新值分配给该属性。默认:undefined
Object.defineProperty(obj, 'title', { get: function () { console.log('get'); return this._title; }, set: function ( val ) { this._title = val; } }); obj.title = 'pink'; var t = obj.title;
defineProperties
在一个对象上添加或修改一个或多个自有属性,并返回该对象。
参数1:表示要被处理的对象
参数2:定义多个属性特性对象
var obj = {} Object.defineProperties(obj, { color: { value: 'tan' }, names: { value: 'zf' } }); console.log( obj );
getOwnPropertyNames
Object.getOwnPropertyNames();
返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性)组成的数组。
参数:需要获取的对象。
getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor();
指定对象上一个自由属性对应的属性描述符。(自由属性指的直接赋予该对象的属性,不需要从原型链上进行查找的属性)
参数1: 获取的对象。
参数2:获取的属性值
模拟map
模拟java中的Map
//简单实现 map function Map () { //priveate 的对象来保存 key 和 val var obj = {}; // put 方法 this.put = function ( key,val ) { obj[key] = val; //把键值对 绑定到object上. } //获得map 容器的个数 this.size = function () { var count = 0; for ( var i in obj ) { count++; } return count; } //根据key 得到 val this.get = function ( key ) { return obj[key] || (obj[key] === 0) || (obj[key] === false) ? obj[key] : null; } // remove 删除方法 this.remove = function ( key ) { if ( obj[key] || obj[key] === 0 || obj[key] === false ) delete obj[key]; } //eachMap 遍历 map 容器的方法 this.eachMap = function ( cb ) { if ( cb && cb.constructor === Function ) { for ( var i in obj ) { cb.call(this,i,obj[i]); } } } } var m = new Map(); m.put('01',120); m.put('02','tan'); // console.log( m.size() ); // // console.log( m.get('0') ); // m.remove('01'); // console.log( m.get('01'),'--' ); m.eachMap(function ( key,val ) { console.log( key,val,'---' ); });
去掉数组的重复项
var arr = [1,2,4,2,3,4,5,546,57,6,5,4,31,57]; //js 对象特性, 数组去重 // 在 js 对象 中 key 是永远 不会重复的. // 1, 把数组转成一个js的对象 // 2,把数组中的值,变成js 对象当中的key // 3,把对象再还原成数组 //数组转对象 var toObject = function ( arr ) { var reslutObj = {}; for ( var i=0; i
其他引用类型
单体对象(不需要实例化对象,就可以使用的方法):
Global对象(全局)这个对象不存在,无形的对象(特别的,特殊的存在)
其内部定义了一些方法和属性:encodeURI 、encodeURIComponent、decodeURI、decodeURIComponent、eval、parseInt、parseFloat、isNaN(在js 里面 只有NaN 自己不等于自己本身的)、Escape、 unescape
// encodeURI 、encodeURIComponent、 var uri = 'http://www.baidu.com cn'; var str1 = encodeURI(uri); //http://www.baidu.com%20cn //(url: //不会进行编码) var str2 = encodeURIComponent(uri); //http%3A%2F%2Fwww.baidu.com%20cn //任何不标准的文字都会进行编码 console.log( str1 ); console.log( str2 ); // decodeURI、decodeURIComponent、 console.log( decodeURI(str1) ); // http://www.baidu.com cn console.log( decodeURIComponent(str2) ); // http://www.baidu.com cn
//eval(string) 方法 无形的javascript 解析器 var str1 = 'var a = 10; var b = 20;'; eval(str1); console.log( a+b ); //数组字符串 直接使用: eval(strArr); var arr = '[10,203,345,45,6]'; var evalArr = eval(arr); console.log( evalArr ); //对象字符串 var obj = '{name: "123", age: 20}'; var evalObj = eval('(' + obj + ')' ); console.log( evalObj );//Object {name: "123", age: 20}
//escape unescape URI 转码 var str = '八百米'; var str2 = escape(str); //%u516B%u767E%u7C73 console.log( str2 );
Math对象
内置的Math对象可以用来处理各种数学运算
可以直接调用的方法:Math.数学函数(参数)
求随机数方法:Math.random(),产生 [0,1) 范围一个任意数
Date对象
获取当前时间的一系列详细方法
var date = new Date(); console.log( date.getTime() ); //当前时间的毫秒数
基本包装类型:Boolean、String、Number
Function类型、RegExp类型
简单单体和闭包单体
单体(singleton)模式是js中最基本但又最有用的模式之一,它可能比其他任何模式都常用。
这种模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一的变量进行访问。通过确保单体对象只存在一份实例,就可以确信自己的所有代码使用的都是同样的全局资源。
简单单体
// 简单单体模式 (只能创建一个实例)//无法通过new 关键字来实例化. var Singleton = { //当成模板类 attr1: true, attr2: 10, method1: function () { console.log( this.attr1 ); } } Singleton.method1(); //划分命名空间(区分代码)
闭包单体
// 利用闭包来创建单体 , 闭包主要的目的 , 保护数据 var alogy = {}; alogy.singleton = (function () { //添加私有成员 var a = 100; var fn1 = function () { console.log( a ); } // 块级作用域里的执行结果赋值 单体对象 return { attr1: 10, attr2: 20, method: function () { console.log( this.attr1 ); }, fn1: fn1 } })(); alogy.singleton.method(); alogy.singleton.fn1();
惰性单体
//惰性单体 (和闭包单体类似)
//通过 一个私有变量来控制是否 实例化对象, 初始化一个 init。 var Ext = {}; Ext.Base = (function () { //私有变量 控制返回的单体对象 var uniqInstance; //需要一个构造器 init 初始化单体对象的方法 function Init () { //私有成员 var a1 = 10; var a2 = true; var fun1 = function () { console.log( a1 ); } return { attr1: a1, attr2: a2, fun1: fun1 } } return { getInstance: function () { if ( !uniqInstance ) { //不存在 ,创建单体实例 uniqInstance = new Init(); } return uniqInstance; } } })() var init = Ext.Base.getInstance(); init.fun1(); //10
分支单体
//分支单体 (判断程序的分支 - 浏览器差异的检测) //简单判断 var Ext = {}; var def = true; Ext.More = (function () { var ff = { attr1: 'ff' }; var ie = { attr1: 'ie' } return def ? ff : ie; })() console.log( Ext.More.attr1 ); //ff
简单链式编程实现
简单链式调用。 return this;
//简单函数链式调用 function Dog () { this.run = function () { console.log( 'dog is run...' ); return this; } this.eat = function () { console.log( 'dog is eat...' ); return this; } this.slepp = function () { console.log('dog is sleep'); return this; } } var d1 = new Dog(); d1.run().eat().slepp();
模拟jquery底层代码
//模拟jquery底层链式编程 // 函数自执行 特点: // 1: 程序启动时候 里面代码自动执行 // 2: 内部的成员变量 外部无法访问 (除了不加var修饰的变量) //块级 作用域 (function ( window,undefined ) { //$ 最常用的对象 返回给外界 //大型程序开发 一般使用 '_'作为私有的对象 function _$ ( args ) { // 匹配id 选择器 var idSelect = /^\#[\w+]?/; this.dom; // 接收所得到的元素 if ( idSelect.test(args) ) { //匹配成功接收元素 // #div this.dom = document.getElementById(arguments[0].substr(1)); } else { throw new Error('选择器不正确!'); } } //在Function类上 扩展 一个可以实现 链式编程的方法 Function.prototype.method = function ( methodName,fn ) { //实现链式编程, 方法的名字 和 进行调用的函数是什么 this.prototype[methodName] = fn; return this; //链式编程 } // 在 _$ 原型对象上 加一些公共的方法 _$.prototype = { constructor: _$, addEvent: function ( type,cb ) { //ff chrome if ( window.addEventListener ) { this.dom.addEventListener(type,cb,false); //ie } else if ( window.attachEvent ) { this.dom.attachEvent('on'+type,cb); } return this; }, setStyle: function ( key,val ) { this.dom.style[key] = val; return this; } } //window上注册 一个 全局变量 window.$ = _$; //准备方法 _$.onReady = function ( cb ) { //1,实例化 _$ 对象 ,注册到 window上 window.$ = function ( args ) { return new _$(args); } //2: 执行传入的代码 cb.call(window); //3: 实现链式编程 _$.method('addEvent',function () {}).method('setStyle',function () {}); } })( window ) //程序的入口 window传入作用域中 $.onReady(function () { $('#div') .addEvent('click',function () { console.log('点击了'); }) .setStyle('background','pink') });
严格模式
严格模式是JavaScript中的一种限制性更强的变种方式。
严格模式与非严格模式可以共存,可以逐渐的选择性加入严格模式。
全局作用域
定义变量必须通过var
。
严格模式禁止删除声明变量。
delete关键字
使用delete删除一个变量名(而不是属性名): delete myVariable
'use strict'; delete Object.prototype; // error . // 删除一个不可配置的属性
函数参数
定义相同名称的参数
要求参数名唯一。
在正常模式下,最后一个重名参数名讳覆盖之前的重名参数,之前的参数仍然可以通过arguments[i]来访问,还不是完全无法访问。
关键字,保留字
使用eval或arguments作为变量名或函数名
严格模式下:
访问arguments.callee, arguments.caller, anyFunction.caller以及anyFunction.arguments都会抛出异常
禁止使用八进制
浏览器都支持以零(0)开头的八进制语法:0644 == 420
还有 '\045 === '%''
认为数字的前导零没有语法意义,但是会改变数字的意义。
eval
严格模式下不能向全局作用域下添加变量
在正常模式下,代码eval('var x;') 会给上层函数或全局引入一个新的变量 x
严格模式下,eval为被运行的代码创建变量,eval不会影响到名称映射到外部变量或其它局部变量。
var x = 22; var evalX = eval("var x = 42; x"); console.log(x === 22); console.log(evalX === 42);
函数内部this
在正常模式下函数调用,this的值会指向全局对象,在严格模式中,this的值会指向undefiend。
当函数通过call和apply调用时,如果传入的是thisvalue参数是一个null和undefiend除外的原始值(字符串,数字,布尔值),则this的值会成为那个原始值的对应的包装对象。如果thisavlue参数的值是undefeind或null,则this的值会指向全局变量。在严格模式中,this值就是thisvalue参数的值,没有任何类型转换。
this: 仅在this指向自己创建的对象时使用它
arguments
arguments对象属性不语对应的形参变量同步更新。
非严格模式下,修改arugmetns对象中的某个索引属性的值,和这个属性对应的形参变量的值也会同时变化。
严格模式下,arguments对象会以形参变量的拷贝的形式被创建和初始化。因此arguments对象的改变不会影响形参。
arguments: 总是通过形参的名字获取函数参数,或者在函数的第一行拷贝arguments
var args = Array.prototype.slice.call(arguments)
with
严格模式禁用with。
with问题:块内的任何变量都可以映射到with传进来的对象的属性,也可以映射到包围这个块的作用域内的变量(甚至是全局变量),在运行时才能够决定:在代码运行之前无法得到。
严格模式下,使用with会引起语法错误。
var x = 7; with (obj) { // 语法错误 // 如果没有开启严格模式,with中的这个x会指向with上面的那个x,还是obj.x? // 如果不运行代码,我们无法知道,因此,这种代码让引擎无法进行优化,速度也就会变慢。 x; }
看完上述内容,是不是对JavaScript引用类型的实现方法有进一步的了解,如果还想学习更多内容,欢迎关注创新互联行业资讯频道。
网站题目:JavaScript引用类型的实现方法
本文URL:http://myzitong.com/article/iedjeh.html