js有两种运行环境,一个是浏览器,一个是服务器(NodeJS)
js的本质是es,因为运行环境的不同,为了操作环境内的api做了升级
在浏览器上js分为es + dom + bom
在服务器上js又有另外的功能,这个在NodeJS里讲
dom和bom就是环境自带的东西
在css的dom树里讲过,浏览器把标签解析成一个巨大的对象renderTree
,然后js出现了能操作renderTree的功能,每一个标签都是一个独立的可以被js单独操作的对象
注意:在html的标签,元素,在js叫节点node 都是同个意思,他们是同个东西
就拿把一个按钮的字变成红色来说
原生的js都需要这样几个步骤
指定获取这个按钮,通过什么呢,跟css一样
//这个就是dom对象
document.querySelector("#btn")
//怎么改颜色呢?控制这个dom对象的style属性,这个属性也是一个对象,这个对象存的就是有关样式的资料,有些可以改变,有些是只读属性
document.querySelector("#btn").style.color = "red";
那我怎么知道这个对象里有一个叫style
的属性对象呢
这就是写多了就知道了,那他还有什么属性对象呢?这就是dom元素的调试
按下F12,从左数第4个console就是调试窗口,这个窗口是前端工作者的家
,是最常用的地方
自己写一个有div的页面打开后,打开console,依次输入下面三句js
// 这个只能拿到页面上的一样的标签
document.querySelector("div")
// 把上面的标签转成详细dom对象
console.dir(document.querySelector("div"))
// 上面的简化版,偷懒就这么写
[document.querySelector("div")]
然后打开返回的数据的箭头,你会看到一个特别长的对象格式的数据,里面就有上面说的style属性对象,这样的dom对象一个页面有无数个
我们去重复的写document.querySelector("xxx")是非常的恶心的,而且有个悲伤的故事不得不讲,就是不同的浏览器的dom是不一样的,因为有个叫内核的东西,内核决定了解析效果,如果接触过IE等非chorme浏览器,你就会知道什么叫内核,因为渲染的不同,属性也会不一样,比如在这个浏览器颜色是color,另一个可能叫myColor,当然这只是比喻,导致我本地运行没问题的代码,给不同的用户使用就没效果,然后被领导疯狂的怼,于是有个优秀的团队封装了一个叫jquery
的插件,专门用来操作dom对象,并且做了简化和浏览器内核兼容,简称jq
下面对比原生的js-dom和jq的使用
有很多人的原生js一点都不懂,刚接触就用jq,这是非常不好的
下面的内容不是告诉你jq有多好用,而是原生应该怎么实现jq的方法
大佬对jq的源码解读
元素获取
// jq写法
$("#id")
$(".class")
$("div")
$("[name=xx]")
$("[type=radio]")
$("input[type=radio]:checked")
$("select option:selected")
$("[disabled]")
// jq把获取一个和获取多个都封装进了$()里
// 原生js就要根据自己的情况去选择
// 使用 document.querySelector()
// 还是 document.querySelectorAll()
// 括号里的写法在原生一样适用
通过name取form表单里的带有name的form表单标签
// 比如有个name属性是myform的form标签里有个name属性是name叫nameInp的输入框
document.myform.nameInp //不放在form里是拿不到的
选择多个的第N个
// jq写法
$("div").eq(2)
// 原生js写法
document.querySelectorAll("div")[2]
jq转js,jq的核心就是把节点存进一个数组里
$("#id")[0] 跟 document.querySelector("#id") 是一样的
$(".class")[4] 跟 document.querySelectorAll(".class")[4] 是一样的
// 一旦把jq转成js就不能再使用jq的方法
节点的循环
// jq写法
$("div").each(function(index,node){ ... })
// 原生js写法
var divs = document.querySelectorAll("div")
for(var i=0;i<divs.length;i++){ ... }
元素的样式和属性
window的高度获取
// jq写法
$(window).height();
// 原生js写法
// 含 scrollbar
window.document.documentElement.clientHeight;
// 不含 scrollbar,与 jQuery 行为一致
window.innerHeight;
document的高度
// jq写法
$(document).height();
// 原生js写法
var body = document.body;
var html = document.documentElement;
var height = Math.max(
body.offsetHeight,
body.scrollHeight,
html.clientHeight,
html.offsetHeight,
html.scrollHeight
);
某个元素的高度
// jq写法
$el.height();
// 原生js写法
function getHeight(el) {
var styles = this.getComputedStyle(el);
var height = el.offsetHeight;
var borderTopWidth = parseFloat(styles.borderTopWidth);
var borderBottomWidth = parseFloat(styles.borderBottomWidth);
var paddingTop = parseFloat(styles.paddingTop);
var paddingBottom = parseFloat(styles.paddingBottom);
return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom;
}
获得匹配元素相对上一级div的坐标位置
// jq写法
$el.position();
// 原生js写法
el.offsetTop/offsetLeft
获得匹配元素相对body的偏移
// jq写法
$el.offset();
// 原生js写法
function getOffset (el) {
const box = el.getBoundingClientRect();
return {
top: box.top + window.pageYOffset - document.documentElement.clientTop,
left: box.left + window.pageXOffset - document.documentElement.clientLeft
}
}
获取元素滚动条垂直位置
// jq写法
$(window).scrollTop();
// 原生js写法
(document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
标签的属性
checked,disabled,placeholder,自定义属性都可以用这个方法
自定义属性在css篇里我说用来储存数据就是这个用法
// jq获取属性
$el.attr("src")
// 原生js获取属性
el.getAttribute("src")
// jq设置属性
$el.attr("data-xx","xx")
// 原生js设置属性
el.setAttribute("data-xx","xx")
// 返回标签上所有data的自定义属性和值,格式是一个对象
el.dataset
// jq删除属性
$el.removeAttr("data-xx")
// 原生js删除属性
el.removeAttribute("data-xx")
// 原生js判断属性
el.hasAttribute("data-xx")
关于单选多选有个注意的点
一般我们默认选中是用 checked="checked" 这是没错的
如果选择是通过鼠标去点击选择或者取消选择,最后用 [xxx]:checked 去取值也是没错的
但是
判断有没有被选中不能通过判断节点是否有 checked 属性
想通过js去选中或者取消选中不能通过添加和移除 checked 属性去实现
复制下面的代码可以知道为什么
<input type="checkbox" checked="checked" id="aa">
document.querySelector("#aa").checked = false
console.log($("#aa").attr("checked"))
//虽然页面显示为选择,但是打印出来的属性还是选中状态,这就是BUG产生的隐患
正确操作如下
// 要判断有没有选中,true是有,false是没有
document.querySelector("#aa").checked
// 选中或者取消选中
document.querySelector("#aa").checked = true/false
form表单元素的值
包括输入框,单选多选,下拉框,大输入框textarea
// jq获取value
$el.val()
// js获取value
el.value
// jq修改value
// 下拉框select传入option的value一样的值会修改选中选项
$el.val(123456)
// js获取value
el.value = 123456
关于下拉框的操作
获取下拉框的值上面提到是value,但是value只是获取被选中的option标签里的value属性的值,那option的内容要怎么获取呢,怎么知道当前下拉框选中的是第几个option呢,怎么去修改当前选中的下拉框的值呢
//获取当前选中的下拉框内容的序号
var index = $('select').selectedIndex
// 所有options的数组
$('select').options
// option的内容
$('select').options[index].text
//选另一个
$('select').options[index].setAttribute("selected","selected")
获取内容html
包括div,p,span等等,图片和表单元素没有这个值
// jq获取html
$el.html()
// js获取html
el.innerHTML
// jq修改html,清空填""
$el.html("<div>123456</div>")
// js修改html,清空等于""
el.innerHTML = "<div>123456</div>"
操作class
// jq添加,移除,有就移除没有就添加
$el.addClass("xxx")
$el.removeClass("xxx")
$el.toggleClass("xxx")
// 原生js添加,移除,有就移除没有就添加
el.classList.add("xxx")
el.classList.removeClass("xxx")
el.classList.toggle("xxx")
插入节点
Append 插入到子节点的末尾
// jq写法
$el.append("<div id='container'>hello</div>");
// 原生js写法
el.insertAdjacentHTML('beforeend', '<div id="container">Hello World</div>');
// 也可以先创建一个标签元素
var newEl = document.createElement("div")
el.appendChild(newEl);
Append 插入到子节点的开头
// jq写法
$el.prepend("<div id='container'>hello</div>");
// 原生js写法
el.insertAdjacentHTML('afterbegin', '<div id="container">Hello World</div>');
// 也可以先创建一个标签元素
var newEl = document.createElement("div")
el.insertBefore(newEl, el.firstChild);
在选中元素前插入新节点
// jq写法
$el.insertBefore("<div id='container'>hello</div>");
// 原生js写法
el.insertAdjacentHTML('beforebegin ', '<div id="container">Hello World</div>');
// 也可以
const el = document.querySelector(selector);
if (el.parentNode) {
var newEl = document.createElement("div")
el.parentNode.insertBefore(newEl, el);
}
在选中元素后插入新节点
// jq写法
$el.insertAfter("<div id='container'>hello</div>");
// 原生js写法
el.insertAdjacentHTML('afterend', '<div id="container">Hello World</div>');
// 也可以
const el = document.querySelector(selector);
if (el.parentNode) {
var newEl = document.createElement("div")
el.parentNode.insertBefore(newEl, el.nextSibling);
}
替换元素
// jq写法
$el.replaceWith("<b>Paragraph. </b>");
// 原生js写法
var newEl = document.createElement("div");
el.parentNode.replaceChild(newEl,el);
移除一个元素
// jq写法
$el.remove()
// 原生js写法
el.parentNode.removeChild(el)
//现在也是可以直接el.remove()的,只是不知道兼容性如何
在html的第一篇里说标签除了属性就是方法,dom节点如何绑定一个方法
让dom响应方法的方式有3种
- 在标签上去添加全局暴露的方法
- 通过选择器添加方法(这个又分为直接赋值和订阅发布)
在标签上去添加全局暴露的方法
关键词是全局
,下面写的所有方法,必须是全局的,如果被私有的作用域保护,是找不到的,什么叫全局的方法,就是window.xx()可以执行的,或者在F12的调试框输入方法名找得到的,这个写法很不安全,因为可以被随意被调用,但是快,简单
<div onclick="click()">点击时</div>
<input type="text" onblur="blur()" oninput="input()" />光标选择时,输入时
<input type="checkbox" onchange="change()" />选中或者取消选中
<input type="file" onchange="change(this.files[0])" />传入文件时
// 讲个特别的,阻止a标签的路径跳转
// 但是为什么要用a标签,还要专门去阻止跳转,这就是傻逼行为
// 知识点 void(0) 就是undefined的意思,很老很装逼的写法
<a href="javascript:void(0)">跳转不了</a>
通过选择器添加方法(这个又分为直接赋值和订阅发布)
先说直接赋值,一个标签的同个方法只能赋值一次,新的会替换掉旧的
//原生js写法,jq没有
document.querySelector(el).onclick = function(){ ... }
document.querySelector(el).onchange = function(){ ... }
window.onscroll = function(){ ... }
window.onload = function(){ ... }
再说说订阅发布
下面的写法不会因为新添加方法移除上一个方法,
添加多少次方法就会执行多少次,会引起多次执行
支持移除,但需要方法名作为标识
//jq写法
//这种写法不支持移除
$el.click(function(){ ... })
$el.change(function(){ ... })
//这种写法可以移除
$el.on("click",functionName)
$el.remove("click",functionName)
//这种写法不能移除,没有标识
$el.on("click",function(){ ... })
// 原生js写法
document.querySelector(el).addEventListener("input",functionName)
document.querySelector(el).removeEventListener("input",functionName)
//这种写法不能移除,没有标识
document.querySelector(el).addEventListener("input",function(){ ... })
虽然上面有很多绑定方法的写法,但是通过id和class等标识去绑定的方式我不是很喜欢,我常用的写法有两种
- 直接把方法写在标签里,方法写成全局的方法
- 创建标签不用字符串,用creatElment,然后把创建好的dom元素直接addEventListener绑定方法
注意
上面的所有的事件绑定都必须保持原标签不改变的前提,一旦标签被父元素innerHTML=""清空,或者本身remove,之后就算再添加一个一样id一样class的标签,他也不是原来的标签了,因为他原本被赋予的方法被删除时已经消失了,需要重新添加方法
dom方法的event对象
任何关于dom元素的方法,他的作用域里都有一个隐藏的叫做event的对象
event的对象有很多种,
最原始的window的onload的Event,
比如输入框失去光标的FocusEvent,
比如键盘按下的KeyboardEvent,
比如鼠标的MouseEvent,
还有手机屏幕滑动的TouchEvent,
等等
$el.click(function(){ console.log(event) })
window.onload = function(){ console.log(event) }
document.querySelector(el).onclick = function(){ console.log(event) }
window.ontouchmove = function(){ console.log(event) }
当给window添加点击事件时认真的去看那个event对象,会发现,event对象里有一个target属性,这个属性里的值就是被鼠标点到的节点,有了这个节点就可以进行节点的判断了,如果点击到的节点的className是aa执行aa方法,如果点到的节点是个img,就如何如何,event就是这样一个俯视节点的上帝的存在,这也是一种绑定事件的方法,叫做事件代理,除了点击事件外,其他事件做不出同样的效果
我用到event的三个地方
- 一是写一个打气球的游戏;
- 还有解决苹果手机ios系统页面不会弹的BUG;
- 手机滑动手势计算
TouchEvent
• clientX:触摸目标在视口中的x坐标。
• clientY:触摸目标在视口中的y坐标。
• pageX:触摸目标在页面中的x坐标。
• pageY:触摸目标在页面中的y坐标。
• screenX:触摸目标在屏幕中的x坐标。
• screenY:触摸目标在屏幕中的y坐标。
事件冒泡和阻止冒泡
在页面上我们会遇到这样的情况,divA有个点击事件A,还有个子元素divB,divB也有个点击事件B,这时鼠标点击B会执行什么事件呢?
点击事件的执行是一种冒泡的模式,从最里面往外面执行,也就是先执行B,然后执行A,但是我们点击B却不想执行A,怎么办
两种方法
一种是把divB从divA里移出来
另一种是阻止冒泡行为,就是在B方法里面写上
function B(){ event.stopPropagation() }
event的其他事件
// 阻止默认行为
event.preventDefault()
// 阻止剩余的事件处理函数执行并且防止事件冒泡到DOM树上
// 这个方法不接受任何参数。
// 例如注册了A、B两个 click 事件,在 A 的方法中阻止后,不会执行 B 的方法
event.stopImmediatePropagation()
dom方法的this对象
每个function都有执行者,function的执行者就是function作用域内的this,这句概念使用在整个js领域,function的执行者就是一个对象,可以是构造对象,可以是dom对象,最常见的是全局对象(全局对象在浏览器端是window对象,在服务器端叫global对象)
上面的几种事件绑定方式
第一种标签绑定事件的默认this是window,需要让标签把自己传过来
<div onclick="aa(this)"></div>
最后一种event.target就是this
其他几种就是直接
function x(){ console.log(this) }
》》其他
这里的内容都很少用到,要么找插件,要么找插件
画布/视频/音频
// 画布api,画布比较常用会单独做一篇
var canvas = document.getElementById("myCanvas");
var cxt = canvas.getContext("2d");
// 视频api
var mp4 = document.getElementById("myVideo");
mp4.onplay = function() { alert("The video has started to play") }
// 音频api
var mp3 = document.getElementById("myAudio");
mp3.onplay = function() { alert("The Audio has started to play") }
还有文件读取fileReader,文件详情DataView,IntersectionObserver是否在可见区域,文件容器FromData,iframe,富文本,拖拽上传,复制上传,地址,摄像头,录音,websocket,打印机
// 文件读取会跟画布做在一期
var fr = new FileReader()
// iframe的高度等于内容的高度
document.querySelector('#iframe').onload = function () {
this.height = this.contentWindow.top.innerHeight + "px";
if(this.contentDocument){
// 不跨域的情况
}else{
// 跨域的情况
this.height = this.contentWindow.top.innerHeight + "px";
this.style.marginLeft = "16px";
document.body.style.overflowX = "hidden";
}
};
// 富文本有相关3个api,会专门总结一期
var selection = window.getSelection();
var range = selection.getRangeAt;
document.execCommand("Copy");
// 拖拽上传
$("#id")
.on("dragover", function (event) {
event.preventDefault();
})
.on("drop", function(event) {
event.preventDefault();
// 数据在event的dataTransfer对象里
let file = event.originalEvent.dataTransfer.files[0];
console.log(file)
// 然后就可以使用FileReader进行操作
// var fr = new FileReader();
// fr.readAsDataURL(file);
// 或者是添加到一个FormData
// let formData = new FormData();
// formData.append("file", file);
})
// 复制上传,查看富文本篇
// IntersectionObserver查看面试题二
后续会继续补充