第1章.基础篇(上)
Abstract:文档树、节点操作、属性操作、样式操作、事件
DOM (Document Object Model) - 文档对象模型
以对象的方式来表示对应的html,它有一系列的规范
i.e.
在浏览器中,DOM是通过JS实现的。
DOM:
DOM Core:核心结构、API的定义
DOM HTML: 定义HTML如何转化成对象(HTML对应的对象)-- 操作节点
DOM Style:样式转换成对象 -- 操作样式
DOM Event:事件对象的模型 -- 响应用户的操作
文档树
HTML -> DOM树
节点遍历
node.parentNode
.firstChild
.lastChild
.previousSibling
.nextSibling
.firstElementChild
.lastElementChild
.nextElementSibling
.previousElementSibling
i.e.
p.parentNode是body
p.firstChild是hello,
p.firstElementChild是span
p.lastElementChild是img
p.lastChild是img
p.previousSibling没有,则返回null
p.nextSibling是div
节点类型:
ELEMENT_NODE:元素节点 (如上body, p, div, span, img)
TEXT_NODE:文本节点(如上hello,, 微专业, mooc)
COMMENT_NODE
DOCUMENT_TYPE_NODE
课堂交流:如何实现浏览器兼容版的element.child
element.children能够获取元素的元素子节点,但是低版本的ie不支持,如何在低版本的ie上兼容类似的功能。
http://www.jianshu.com/p/b7e111015c48
节点操作
Abstract: getElementById, getElementsByClassName, getElementsByTagName, querySelector(All), createElement, innerText, appendChild, insertBefore, removeChild, innerHTML
浏览器读取HTML渲染出页面结构以后,还可以通过JS改变页面的结构
获取节点:
通过节点关系可以获取节点(父子关系、兄弟关系)
缺点:可维护性差,如果一个节点的位置发生了变化,则关系也可能会被打乱
所以,一般使用接口来获取节点(获得的是节点对象:
getElementById:
element = document.getElementById(id):id在document中是唯一标识
getElementsByTagName:
collection = element.getElementsByTagName(tagName):通过元素来调用来获取元素内的节点
若tagName为"*", 则会获取指定元素element包含的所有的后代元素节点
注:collection是动态的集合
getElementsByClassName:
collection = element.getElementsByClassName(className)
通过空格分割,可以指定多个类名(无序),获取同时具有多个类名的元素
但是IE 6/7/8不兼容getElementsByClassName
function getElementsByClassName(element, classNames) { if (element.getElementsByClassName) { // 特性侦测,如果兼容则优先使用W3C规范的方式 return element.getElementsByClassName(classNames); } else { var elements = element.getElementsByTagName("*"); // 所有后代元素 var result = []; var element, classNameStr, flag; classNames = classNames.split(' '); for (var i = 0; element = elements[i]; i++) { classNameStr = ' ' + element.className + ' '; flag = true; for (var j = 0, className; className = classNames[j]; j++) { if (classNameStr.indexOf(' ' + className + ' ') == -1) { flag = false; break; } } if (flag) { result.push(element); } } return result; } }
querySelector:
element = element.querySelector(selector)
返回第一个符合的元素
querySelectorAll:
list = element.querySelectorAll(selector)
i.e.
<div id="users"> <h2>....</h2> <ul> <li class="user">Satoshi</li> <li class="user">春来草青</li> <li class="user last">Kash</li> </ul> </div>
var users = document.querySelector(#users"); // 获取到元素div#users
users.querySelectorAll(".user"); // 获取到 [ li.user li.user li.user.last ]
document.querySelectorAll("#users .user"); // 获取到同上 [ li.user li.user li.user.last ]
注:list和collection不同之处:list静态的,获取到的结果之后就不会自动同步改变了;而collection是动态的
IE6/7不兼容,IE 8部分兼容
若有如下场景:
想要在<ul> ... </ul>的末尾处增加一个<li>节点,应该怎么做
<ul> <li>...</li> ... <li class="user"> <img src="lf.jpg"> <a href="/user/lf">lifeng</a> </li> </ul>
1. 创建li节点;设置li节点的class属性;插入li节点
2. 创建img节点;设置img节点的src属性;插入img节点
3. 创建a节点;设置a节点的hrf属性;设置a节点的内容;插入a节点
var li = document.createElement("li"); li.className = 'user'; ul.appendChild(li); var img = document.createElement("img"); img.src = 'xxx.jpg'; li.appendChild(img); var a = document.createElement("a"); a.href = '/user/xxx'; a.innerText = "lifeng"; li.appendChild(a);
创建节点
element = document.createElement(tagName);
i.e. var li = document.createElement("li");
修改节点
element.textContent:表示节点及其后代节点的文本内容,但是IE9不支持
i.e. a.textContent = "lifeng";
innerText:不规范,但是经常使用(Firefox不支持)(几乎和textContext一模一样)
i.e. a.innerText = "lifeng";
如果要让Firefox兼容的话(使用textContent)
if(!('innerText' in document.body)) { // 特性侦测 HTMLElement.prototype._defineGetter_("innerText", function() { return this.textContent; }); HTMLElement.prototype._defineSetter_("innerText", function(s) { return this.textContent = s; }); }
插入节点
var achild = element.appendChild(achild); // 在element元素内的末尾追加achild节点
i.e. ul.appendChild("li");
var achild = element.insertBefore(achild, referenceChild); // 在referenceChild元素之前插入achild节点
i.e. users.insertBefore(li, ul.firstChild);
删除节点
var child = element.removeChild(child);
i.e. user.parentNode.removeChild(user);
innerHTML:节点的HTML内容
刚才的例子中,为了添加一个lifeng的<li>节点需要那么长的代码,可以使用innerHTML来提高效率
i.e. 设已经添加了一个li节点在ul的末尾;
li.innerHTML = '<img src="xxx.jpg">
<a href="/user/xxx">lifeng</a>';
li.innerHTML = ""; // 起到删除所有子节点的作用
那可以使用 li.innerHTML += "<li>.......</li>"; 来实现吗?
可以,但是,相当于重新设置了HTML的内容,之前修改过/添加了的事件/样式/状态就会被清除 (覆盖)
innerHTML的问题:内存泄露、安全问题
i.e. 安全问题:利用innerHTML运行代码
var userName = '</a><a onclick="alert("我是黑客");" href="#">lifeng'; li.innerHTML = '<img src="xxx.jpg"> <a href="/user/xxx/">' + userName + '</a>';
建议仅用于创建新节点
属性操作
实例:登录框的登陆按钮在点击一次后为了避免重复提交会修改按钮的disabled属性让其不可点击
每个HTML attribute都可以对应一个DOM property,修改DOM property就能实现对HMLT attribute的修改
<div> <label for="userName">用户名:</label> <input id="userName" type="text" class="u-txt"> </div>
i.e. 上例中对应的DOM property为:label.htmlFor="userName"; input.id="userName"; input.type="text"; input.className="u-txt"(因为for和class是关键字)
三种方法进行属性操作:属性访问器、get/setAttribute、自定义属性dataset
property accessor:
i.e. input.className; // "u-txt"
input["id"]; // "userName"
input.value = 'www@163.com'; // 给input增加一个value属性并赋值
转换的类型:
(Boolean类型的变量除非设置成false,否则不论是空还是0都表示true)
转换过的为实用对象
优缺点:通用性差--名字异常;扩展性差--每增加一个html属性就需要对应一个DOM属性;优点:获得的是一个实用对象
set/getAttribute:
var attribute = element.getAttribute(attributeName); // 读
element.setAttribute(attributeName, value); // 写
i.e. input.getAttribute("class"); // "u-txt"
input.setAttribute("value", "www@163.com");
input.setAttribute("disabled", ""); // 设置disabled属性为true(前面提到了,只要属性出现了而且不是false,那么就是true)
转换的类型都为String,获得的是属性字符串
优缺点:只能处理字符串,推荐如果是纯字符串操作就使用get/setAttribute()
dataset:自定义属性
HTMLElement.dataset
data-*属性集
一般用于元素上保存数据(自定义的数据属性)
<div id="user" data-id="123456" data-account-name="wwq" data-name="smith" data-email="wwq123@163.com" data-mobile="123123">wwq</div>
属性名:将"data-"去掉,如果有连接符,会以大写开头表示
i.e. 鼠标悬停在姓名上时会显示对应详细信息的表格
<body> <ul> <li data-id="123456" data-account-name="wwq" data-name="魏文庆" data-email="wwq123@163.com" data-mobile="13524543878">wwq</li> <li data-id="123457" data-account-name="cjf" data-name="蔡剑飞" data-email="cjf123@163.com" data-mobile="13968789868">cjf</li> </ul> <div id="card" style="display:none"> <!-- 将空卡片隐藏 --> <table> <caption id="accountName"></caption> <tr><th>姓名:</th><td id="name"></td></tr> <tr><th>邮箱:</th><td id="email"></td></tr> <tr><th>手机:</th><td id="mobile"></td></tr> </table> </div> <script> function $(id){ return document.getElementById(id); } var lis = document.getElementsByTagName('li'); for(var i = 0, li;li = lis[i]; i++){ li.onmouseenter = function(event){ event = event || window.event; var user = event.target|| event.srcElement; var data = user.dataset; // 插入相关数据 $('accountName').innerText = data.accountName; $('name').innerText = data.name; $('email').innerText = data.email; $('mobile').innerText = data.mobile; // 显示卡片 $('card').style.display = 'block'; }; li.onmouseleave = function(event){ $('card').style.display = 'none'; }; } </script> </body>
dataset在低版本浏览器中不兼容,怎么实现?
function dataset(element) { // my own version(haven't checked yet) if (element.dataset) { // check whether the original version od dataset is available return element.dataset; } else { var data = []; for (var i = 0; i < element.attributes.length; i++) { // traverse all the attributes element has if (/^data-/.test(element.attributes[i].nodeName) { // attribute_name starts with "data-" data[element.attributes[i].nodeName.replace("data-","")] = element.attributes[i].nodeValue; } } return data; } }
样式操作
样例:
格式不正确
QQ空间换皮肤等
-- 可通过JS动态修改样式
CSS --> DOM
<head> <link rel="stylesheet" href="base.css"> <style> body {margin: 30;} p {color: #aaa; line-height: 20px; } </style> </head> <body> <p style="color:red;"> paragraph</p> </body>
3中css的引用:
<link> 对应的为element.sheet
<style> 对应的为element.sheet
style="" 对应的为element.style
整张页面的所有样式对应的为document.styleSheets
i.e. element.sheet:
element.sheet.cssRules : 对应body{...} 和 p{...}
element.sheet.cssRules[1] : 对应p{...}
element.sheet.cssRules[1].selectorText : 对应选择器p
element.sheet.cssRules[1].style : 属于类CSSStyleDeclaration的对象。对应p{...}中的css声明color:#aaa; line-height:20px;
element.sheet.cssRules[1].style.lineHeight : 对应属性值20px
element.style:属于CSSStyleDeclaration类的对象,遇上相同,对应css声明color:red;
element.style.color : 对应red
对样式的增删查改:
更新样式:
element.style.borderColor = 'red';
element.style.color = 'red';
-- 更新一个属性需要一条语句,而且不是css格式
更好的方式:cssText
i.e. element.style.cssText = 'border-color:red; color:red;';
-- 一条语句可以设置一个元素,符合css格式
但是:样式与逻辑混合
更好的方式:更新class -- 开发中使用的方法
首先,在css样式中增加样式 .invalid { border-color:red; color:red; }
在JS中输入框对象处:element.className += 'invalid';
但是如果要一次性修改批量元素的样式呢,比如上述的QQ空间换肤实例:可以使用更换样式表
原版本 <link rel="stylesheet" href="style.css">
可以拆分为两个样式表 base.css 和 skin.spring.css
要换肤时,比如换成夏天皮肤,则将skin.spring.css 换成 skin.summer.css即可
<head> <title>换肤 - 更新样式</title> <link rel="stylesheet" href="base.css"> <link id="skin" rel="stylesheet" href="skin.spring.css"> </head> <body> <div class="m-tw clearfix"> <div class="u-img"> <a href="#"><img src="zhm.jpg" alt=""></a> </div> <div class="txt"> <h3><a href="#">张惠妹</a></h3> <p>亞洲國寶級傳奇天后「 a MEI」我是a MEI,一個你認識很久,卻認識不完的女人。</p> </div> </div> <button id="change">换肤</button> <script src="../util.js"></script> <script> Util.addEventListener($('change'), 'click', changeSkin); function changeSkin(){ $('skin').href = "skin.summer.css"; } </script> </body>
/* skin.spring.css */ body{background-color: #d6e6c6;} .m-tw .u-img{border-color: #6e9d41;} .m-tw p{color: #367701;} .m-tw h3{background-color: #6e9d41;} .m-tw h3 a, .m-tw h3 a:hover{color: #fff;}
/* skin.summer.css */ body{background-color: #fefaf7;} .m-tw .u-img{border-color: #a84c5b;} .m-tw p{color: #6d3e48;} .m-tw h3{background-color: #a84c5b;} .m-tw h3 a, .m-tw h3 a:hover{color: #fff;}
相似的,也可以进行删除样式表、添加样式表等操作
获取样式:
i.e. 有一<input type="text">
element.style.color; // ""
字体是黑色,为什么获取到的为空呢?因为element.style对应的为内嵌样式表,确实为空
若是<input type="text" style="color:red"> 则可获取到"red"
实际上有三种样式设置方式,而且style获取到的不一定是实际样式,所以不采取上述方法获取样式属性
window.getComputedStyle()
var style = window.getComputedStyle(element [, pseudoElt]);
// 返回值的类型也是CSSStyleDeclaration,但是是只读的,包含了所有的属性名和属性值的键值对
i.e. window.getComputedStyle(input).color; // "rgb(0,0,0)"
IE9以下不兼容,可以使用element.currentStyle(不规范)
http://web.zhydaxq.com/2017/04/14/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%BC%E5%AE%B9%E7%89%88%E7%9A%84window-getcomputedstyle/
事件
什么是DOM事件?当我们点击一个DOM元素时,或键盘按下一个键,或在输入框中输入内容,或页面加载完成时,都是DOM事件
事件流:
DOM事件的处理过程 http://www.w3.org/TR/uievents/#dom-event-architecture
i.e.
当点击了a标签时,就会产生一个DOM的click事件,事件的处理过程:
capture phase:从DOM树的根节点开始捕获直到事件节点的父元素:window->document->html->body->div->p
target phase:事件的触发过程 从父节点p到事件发生所在节点a
bubble phase:冒泡过程:从事件所在节点的父节点开始,冒泡到最顶层的window对象
(IE低版本没有捕获过程;而且也不是所有事件都有这三个过程,比如页面load事件并没有冒泡过程)
事件注册与触发
注册事件
eventTarget.addEventListener(type, listener [,useCapture])
type: 事件类型;listener:事件处理函数;useCapture:是否为捕获过程(默认处理的是冒泡过程)
var element = document.getElementById('div1'); var clickHandler = function(event) { // TO-DO } element.addEventListener('click', clickHandler, false); // 默认为false
还有一种方法进行注册:
element.onclick = clickHandler;
缺陷:这种注册方式下的事件处理函数只能有一个,但很多情况下需要注册多个事件处理函数
取消事件注册
eventTarget.removeEventListener(type, listener [,useCapture])
i.e. element.removeEventListener('click', clickHandler, false);
或者通过 element.onclick = null; 来取消注册
事件触发:使用代码来触发事件(不包括点击之类的触发)
eventTarget.dispatchEvent(type); // type:事件类型;就能触发相应类型的DOM事件了
i.e. elem.dispatchEvent('click'); // 使用代码触发elem元素的单击事件
低版本浏览器的兼容:IE6/7/8没有采用这些W3C标准
没有capture捕获阶段,只能处理冒泡阶段
事件注册与取消:attchEvent/detachEvent
事件触发:fireEvent(e)
var addEvent = document.addEventListener ? function(elem, type, listener, useCapture) { elem.addEventListener(type, listener, useCapture); } : function(elem, type, listener, useCapture) { elem.attachEvent('on' + type, listener); }; // 区别:1. 事件类型前缀了'on',2. 没有useCapture参数 var delEvent = document.removeEventListener ? function(elem, type, listener, useCapture) { elem.removeEventListener(type, listener, useCapture); } : function(elem, type, listener, useCapture) { elem.detachEvent('on' + type, listener); };
事件对象
当事件触发时,会调用事件处理函数,此时需要事件状态的信息,事件对象就包含了这些信息。引擎在调用处理函数时会传入事件对象
i.e. 上例elem.addEventListener('click', clickHandler, false);中
触发click事件时会调用clickHandler函数,执行时会传入event对象,即事件对象(var clickHandler = function(event) {...})
可能该事件对象包含了鼠标位置等信息
在IE的低版本里有一些不同,事件的event对象并不是直接通过函数传入的,而是放在window对象中
兼容版本:
var elem = document.getElementById('div1'); var clickHandler = function(event) { event = event || window.event; // TO-DO }
事件对象的属性和方法:
不同分类的事件在不同场景下的事件对象都有可能不同
通用的属性和方法:
属性:
type: 事件类型,如click
target(IE低版本为srcElement): 事件触发的节点,如a元素
currentTarget: 当前处理事件的节点(处理一个事件时,不一定要把事件注册在target上,可以注册在父节点(由于冒泡))
只有事件处于目标阶段时,currentTarget和target的值才是肯定相同的
方法:
stopPropagation: 阻止传播,使事件的冒泡停止
preventDefault: 阻止默认行为
默认行为:比如双击文字时文字会被选中,比如单击连接时连接会被打开
stopImmediatePropagation: 阻止冒泡
i.e.
event.stopPropagation() (W3C): 阻止冒泡到父节点
event.cancelBubble=true (IE低版本)
event.stopImmediatePropagation() (W3C): 两个结果:1. stopPropagation(); 2. 所有该节点的后续事件也不会触发
event.preventDefault() (W3C)
event.returnValue=false (IE低版本)
事件分类 http://www.w3.org/TR/uievents/
Abstract: 事件分类及继承关系;鼠标事件类型、鼠标事件对象、鼠标事件举例;键盘、输入、焦点事件类型、事件对象、事件举例
MouseEvent:
事件类型:
click:单击
dbclick:双击
mousedown:按下鼠标
mousemove:鼠标移动
mouseout:鼠标从某元素上移开
mouseover:鼠标在某元素上(若鼠标在元素子元素上,也是可行的--可冒泡)
mouseup:释放鼠标
mouseenter:鼠标进入元素(只有鼠标在该元素上时,不可冒泡)
mouseleave:鼠标离开元素(不可冒泡,与mouseout(可冒泡)的区别和mouseenter和mouseover的区别一样)
属性:
clientX, clientY:到页面的左上的距离
screenX, screenY:到屏幕的左上的距离
ctrlKey, shiftKey, altKey, metaKey:事件触发时若键按下则为true
button:值为0/1/2,表示按下的是鼠标的左键/中间键/右键
顺序:
i.e. 鼠标从元素A外面移动到元素A上面,再划出去的过程中:
mousemove -> mouseover(A) -> mouseenter(A) -> mousemove -> mouseout(A) -> mouseleave(A)
(mousemove一直在触发,触发间隔由浏览器决定)
i.e. 点击元素:
mousedown -> [mousemove] -> mouseup -> click
(click事件是在鼠标松开之后才触发的)
实例. 拖拽div
<div id="div1"></div>
<style type="text/css"> #div1{ position:absolute; top:0; left:0; border:1px solid #000; width:100px; height:100px; } </style>
var elem = document.getElementById("div1"); var clientX, clientY, moving; var mouseDownHandler = function (event) { // 鼠标按下 event = event || window.event; clientX = event.clientX; clientY = event.clientY; moving = !0; // 点下去后设置moving为true } var mouseMoveHandler = function(event) { // 鼠标移动(drag的移动过程) if (!moving) return; // 鼠标还未down event = event || window.event; var newClientX = event.clientX, newClientY = event.clientY; var left = parseInt(elem.style.left) || 0, top = parseInt(elem.style.top) || 0; elem.style.left = left + (newClientX - clientX) +'px'; // 用offset量来确定div的移动 elem.style.top = top + (newClientY - clientY) +'px'; clientX = newClientX; // 更新clientX和clientY,用于下次move触发 clientY = newClientY; } var mouseUpHandler = function (event) { moving = !1; } addEvent(elem, 'mousedown', mouseDownHandler); addEvent(elem, 'mousemove', mouseMoveHandler); addEvent(elem, 'mouseup', mouseUpHandler);
WheelEvent:从MouseEvent继承
事件类型只有一种 wheel
属性:
deltaMode:指定delta值的单位
deltaX、deltaY、deltaZ:在X/Y/Z方向上的偏移量
FocusEvent:元素获取或失去焦点的时候触发(比如点击输入框出现光标/点击页面其他地方取消输入框可输入状态)
事件类型:
blur:元素失去焦点时
focus:元素获得焦点时
focusin:元素即将获得焦点时(获得焦点之前)
focusout:元素即将失去焦点时(失去焦点之前)
属性:
relatedTarget:当一个元素失去焦点时,另一个元素就会获得焦点,blur/focusout中获得焦点的元素就是relatedTarget
当一个元素获得焦点时,另一个失去焦点的元素就是focus/focusin里面的relatedTarget
InputEvent:处理输入事件
事件类型:
beforeinput:输入在页面上还不能看到时
input:当输入框里的内容已经有输入内容时(继续输入时也会不断触发input事件)
在IE低版本中没有input事件,使用onpropertychange
KeyboardEvent:处理键盘事件
事件类型:
keydown:按下键
keyup:松开键
属性:
key:字符串,按下的按键
code:字符串,按键对应码
ctrlKey/shiftKey/altKey/metaKey:标识是否被按下
repeat:一个键持续按着
以上为W3C,以下为常用非标准
keyCode、charCode、which:用于获取按键对应的ASCII码,实际编程用这些ASCII码来判断
Event 最基本的事件:
事件类型:
load:代表元素 (如window, image, iframe等依赖网络加载的元素) 加载完成
unload:与load对应,代表元素退出时(被关闭)
error:加载错误,比如讲img的src路径写错了
select:比如input/textarea输入框被选择时
abort:window/image等元素正在加载时,按下了esc
按对象来解释这些事件:
window:
load:页面的所有请求都完成了,所有需要加载的元素都加载了的时候
unload:当关闭当前页面时
error:浏览器加载当前页面异常
abort:退出时
image:
load:图片按照src地址通过网络连接加载完成时
error:图片加载异常
abort:图片加载时,按下了esc等中断图片的加载
<!-- 通常会这么写 --> <image alt="photo" src=".../photo.jpg" onerror="this.src='...default.jpg'"/>
UIEvent:
事件类型:
resize:修改浏览器或iframe窗体大小时,
scroll:页面发生滚动时触发。若滚动是在一个元素上触发的,则会冒泡;如果是系统(document)滚动条,则不会冒泡
事件代理
场景:一个ul中有很多li,要对这些li都注册一个click事件,需要一个一个注册吗?
-- 直接将click事件注册到ul上即可,因为click事件支持冒泡:li上的事件最终肯定会冒泡到ul上,只需在ul上处理所有li的事件即可
-- 事件代理:将事件注册到元素的父节点上
优点:1. 不用注册那么多的事件;2. 内存分配少
缺点:如果把事件全放入父元素,事件管理起来会很复杂(比如将所有事件都注册到window对象上,事件处理函数会很复杂)
(数据通信、数据存储、动画、音频与视频、canvas、BOM、表单操作、列表操作见基础篇(下))