JavaScript随笔
一、事件概述
1、事件的几个概念
· 事件
指的是文档或者浏览器窗口中发生的一些特定交互瞬间。我们可以通过侦听器(或者处理程序)来预定事件,以便触发事件的时候执行相应的代码。
事件处理程序
我们用户在页面中进行的点击动作(click)、鼠标移动动作(mousemove)等,都可以称之为事件名称。响应某个事件的函数则称为事件处理程序,或者叫做事件侦听器。
事件类型
UI事件: load、unload、error、resize、scroll、select,是用户与页面上的元素交互时触发的。
焦点事件:blur、DOMFocusIn、DOMFocusOut、focus、focusin、focusout,在元素获得或失去焦点的时候触发,这些事件当中,最为重要的是blur和focus,
有一点需要引起注意,这一类事件不会发生冒泡!
鼠标与滚轮事件:click、dblclick、mousedown、mouseenter、mouseleave、mousemove、mouseout、mouseover、mouseup,是当用户通过鼠标在页面执行
操作时所触发的。
滚轮事件:mousewheel(IE6+均支持)、DOMMouseScroll(FF支持的,与mousewheel效果一样)。是使用鼠标滚轮时触发的。
文本事件:textInput,在文档中输入文本触发。
键盘事件:keydown、keyup、keypress,当用户通过键盘在页面中执行操作时触发。
2、事件三要素
事件有三要素 : 事件源
、事件
、监听器
。
事件源:在哪个元素上发生的。比如: p标签、a标签、div标签、form表单 等等
事件:到底发生了什么事件。click(点击事件)、mouseover(鼠标事件)、focus(焦点事件) 等
监听器:事件源触发事件后,如何回应发生的事件,通常以函数(funtion)的形式来出现。
注意
事件不是以 on 开头的那个名称,如 onclick 不是事件,click才是事件。onclick引用的是一个元素对象的属性,它指向click事件类型绑定的实际处理函数。
二、绑定事件、解绑事件
常用的事件绑定的几种方式有三种:
1、直接在html元素上进行绑定事件。
2、用 on 绑定事件。
3、用 addEventListener、attachEvent 绑定事件。
1、直接在 html 元素上进行绑定
即以属性的方式直接写在行内
<input id="btn" type="button" onclick="test();" /> <!--点击按钮 触发事件-->
这样有个很大的缺点就是:
HTML与js代码紧密耦合。如果要更换 事件,就要改动两个地方:HTML代码和JS代码,这就不利于后期代码的维护。
2、用 on 绑定
兼容性
:在IE,FF,Chrome,Safari,Mozilla,Opera下都适用。
<body>
<div id="id">on绑定事件</div>
<script>
var div=document.getElementById('id');
// 甲
div.onclick=function(){
console.log('甲需要红背景');
div.setAttribute('style', 'background: #ff0000');
};
// 乙
div.onclick=function(){
console.log('乙需要黄背景');
div.setAttribute('style', 'background: #ffff00');
};
//这里最总只会输出 '乙需要黄背景' 因为用on绑定事件 同一事件下面会覆盖上面的
div.onclick=null; //解绑只要事件 = null 就可以了
</script>
</body>
优点
:它最大的优点是就是兼容性很好,所有浏览器都支持。
缺点
:同一个 dom 元素上,on 只能绑定一个同类型事件,后者会覆盖前者,不同类型的事件可以绑定多个。
这里就有一个问题,无法允许团队不同人员对同一元素监听同一事件但做出不用的响应。
3、 addEventListener、attachEvent 绑定事件
同一个 dom 元素上,用 addEventListener、attachEvent 可以绑定多个同类型事件。
但是,addEventListener 事件执行顺序按照事件绑定的先后顺序执行;attachEvent 事件执行顺序则是随机的。
1)addEventListener
var oBox = document.getElementById("container");
//绑定事件
oBox.addEventListener("click",fn(),false);
//解绑事件
oBox.removeEventListener("click",fn(),false);
function fn(){//执行代码}
参数说明
第一个参数:事件名称 比如onclick onmouseover
第一个参数:作为事件处理程序的函数
第一个参数:若为false,函数在冒泡阶段执行;若为true,函数在捕获阶段执行。默认为false。(有关冒泡和捕获单独抽时间讲)
注意:removeEventListener 第二个参数要和 addEventListener 指向 同一个函数 才能解绑成功。
2)attachEvent
var oBox = document.getElementById("container");
//绑定
oBox.attach("click",fn());
//解绑
oBox.detach("click",fn());
function fn(){//执行函数}
3)区别
总结下 addEventListener、attachEvent的区别
1)参数个数不一致
addEventListener三个参数,attachEvent两个参数
2)兼容问题
addEventListener 谷歌,火狐,IE11支持,IE8不支持
attachEvent 谷歌火狐不支持,IE11不支持,IE8支持
3)this指向不同
addEventListener 中的this是当前绑定事件的对象
attachEvent中的this是window
4)事件命名不同
addEventListener中事件的类型(事件的名字)没有on
attachEvent中的事件的类型(事件的名字)有on
这里再说下 addEventListener、attachEvent相对于上面两种绑定事件的优缺点
优点
它们可以支持 绑定多个同类型事件
缺点
兼容性并不好,它们只兼容相对应的浏览器才有用。
三、事件的兼容
上面说了3种绑定事件和解绑事件的方法,如果实际开发中如果只使用一种方法,那么会产生要么无法满足同一元素监听同一事件但做出不用的响应,要么系统的兼容性会有问题。
所以需要一个兼容的方法。这里举一个完整的例子,包含兼容绑定事件 和 兼容解绑事件 ,也看下解绑的含义是什么。
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件绑定和解绑</title>
</head>
<body>
<input type="button" value="绑架事件" id="btn1"/>
<input type="button" value="解绑事件" id="btn2"/>
<script>
//第一个按钮 同时绑定两个相同事件 执行不同方法
addEventListener(my$("btn1"),"click",f1);
addEventListener(my$("btn1"),"click",f2);
//第二个按钮点击后 让第一个按钮第一个事件解绑
my$("btn2").onclick=function () {
removeEventListener(my$("btn1"),"click",f1);
};
function f1() {
alert("第一个事件");
}
function f2() {
alert("第二个事件");
}
function my$(id) {
return document.getElementById(id);
}
//绑定事件的兼容
function addEventListener(element,type,fn) {
if(element.addEventListener){ //有没有用
element.addEventListener(type,fn,false);
}else if(element.attachEvent){ //有没有用
element.attachEvent("on"+type,fn);
}else{ //如果都不兼容 那就用这种来绑定事件
element["on"+type]=fn;
}
}
//解绑事件的兼容
function removeEventListener(element,type,fnName) {
if(element.removeEventListener){
element.removeEventListener(type,fnName,false);
}else if(element.detachEvent){
element.detachEvent("on"+type,fnName);
}else{
element["on"+type]=null;
}
}
</script>
</body>
</html>
运行结果
从运行结果我们很明显可以得出的结论:
1、一开始绑定事件的按钮 绑定了两个相同的事件。并且发现并没有发生事件覆盖,都成功了。
2、当点击解绑按钮后,它解绑是绑定按钮的第一个事件。
3、此时再点击绑定事件的按钮,发现只绑定了一个事件,因为另一个事件已经被解绑了。
一、DOM概念
什么是DOM
DOM全称为文本对象模型(Document Object Model),它定义了所有HTML元素的对象和属性,以及访问他们的方法。它的主要作用包括:
改变HTML 元素 , 改变HTML属性 , 改变CSS 样式,对页面中的所有事件做出反应。
1、DOM 节点树
概念
DOM模型将整个HTML文档看成一个树形结构,并用document对象表示该文档,树的每个子节点表示HTM档中的不同内容。
如图
上图对应的html代码如下
<!DOCTYPE html>
<html>
<head>
<title>标题</title>
</head>
<body>
<h1>小小</h1>
<a href="www.xx.com">链接</a>
</body>
</html>
通过这幅图也可以看出节点树中有以下几种关系
1、父级关系(图中 html 是 head 的父亲,head是title的父亲。)
2、子级关系(图中 head 是 html 的儿子,title是head的儿子。)
3、兄弟关系 (图中 head 和 body是兄弟关系。p 和 h1 是兄弟关系。)
2、DOM 节点类型
从上图部分,我用四种颜色区分了不同节点的类型,每个节点对应的nodeType也是不一致的。
3、DOM 节点操作
节点查询操作
节点增删操作
节点属性操作
二、查询示例
这里通过举例去更好的理解上面的一些操作。
1、查询子元素
children
: 可以获取当前元素的所有子元素,它是不包含空白的。
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div id="father">
<p>今天时间</p>
<p>2019.12.09</p>
<p>22:15分</p>
</div>
</body>
<script>
window.onload = function () {
var father = document.getElementById("father");
var all = father.children; // 获取 father 下边所有的子元素
console.log("数组的长度为:" + all.length);
};
</script>
</html>
输出
数组的长度为:3
2、查询父元素
parentNode
:获取当前元素的父节点(父元素)
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div id="father">
<p id="childNode">我是子节点</p>
</div>
</body>
<script>
window.onload = function () {
var p = document.getElementById("childNode"); //获取p元素
var parent = p.parentNode; // 获取父元素
console.log(parent);
};
</script>
</html>
输出
<div id="father">
<p id="childNode">我是子节点</p>
</div>
3、查询兄弟元素
previousElementSibling
获取当前元素的前一个兄弟元素(哥哥元素)nextElementSibling
获取当前元素的后一个兄弟元素(弟弟元素)
示例
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<p id="previous" name="1111">哥哥元素</p>
<p id="my">本元素</p>
<p id="next">弟弟元素</p>
</body>
<script>
window.onload = function () {
var my = document.getElementById("my"); // 获取我的元素
var previous = my.previousElementSibling; // 获取哥哥元素
var next = my.nextElementSibling; // 获取弟弟元素
console.log(previous);
console.log("---------------------");
console.log(next);
};
</script>
</html>
输出
<p id="previous" name="1111">哥哥节点</p>
---------------------
<p id="next">弟弟节点</p>
三、增删改示例
1、增加子节点
appendChild()
: 向一个父节点中添加一个新的子节点 父节点.appendChild(子节点);
代码示例
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<script>
window.onload = function () {
// 获取到 btn
var btn01 = document.getElementById("btn");
// 添加点击事件
btn01.onclick = function () {
// 创建一个 p 标签
var p = document.createElement("p");
// 创建一个文本节点
var txt = document.createTextNode("我是新创建p标签中的内容...");
// 把文本节点添加到 p 标签中
p.appendChild(txt);
// 把 p 标签添加到 body 中
var body = document.body; // 获取body
body.appendChild(p);
};
};
</script>
<body>
<button id="btn">创建一个p标签</button>
</body>
</html>
运行
2、移除子节点
removeChild()
:删除一个子节点 父节点.removeChild(子节点);
代码示例
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<button id="btn">删除一个子节点</button>
<ul id="uls">
<li>我是第一个子节点</li>
<li>我是第二个子节点</li>
<li>我是第三个子节点</li>
<li>我是第四个子节点</li>
</ul>
</body>
<script>
window.onload = function () {
// 获取到 btn
var btn = document.getElementById("btn");
// 添加点击事件,删除第三个子节点
btn.onclick = function () {
//获取第三个子节点
var node = document.getElementsByTagName("li")[2];
// 获取父节点
var ul = document.getElementById("uls");
ul.removeChild(node); // 删除子节点
};
};
</script>
</html>
运行
3、替换子节点
replaceChild()
: 可以使用指定的子节点来替换已有的子节点 父节点.replaceChild(新节点,旧节点);
代码示例
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<button id="btn">替换按钮</button>
<p id="pNode">我是旧节点 p </p>
</body>
<script>
window.onload = function () {
// 获取到 btn01
var btn = document.getElementById("btn");
// 添加点击事件
btn.onclick = function () {
// 获取p标签
var p = document.getElementById("pNode");
// 获取父节点
var body = document.body;
//创建一个新的节点,用于替换旧节点
var a = document.createElement("a");
var txt = document.createTextNode("我是新的节点 a 哦~");
a.appendChild(txt);
// 替换节点
body.replaceChild(a, p);
};
};
</script>
</html>
运行
4、替换指定位置节点
insertBefore()
: 可以在指定的子节点前插入一个新的子节点 父节点.insertBefore(新节点,旧节点);
代码示例
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<button id="btn01">替换子节点</button>
<p id="pNode">我是旧节点 p </p>
</body>
<script>
window.onload = function () {
// 获取到 btn01
var btn01 = document.getElementById("btn01");
// 添加点击事件
btn01.onclick = function () {
// 获取p标签
var p = document.getElementById("pNode");
// 获取父节点
var body = document.body;
//创建一个新的节点,用于插入到 p 元素前面
var a = document.createElement("a");
var txt = document.createTextNode("我是新的节点 a 哦~");
a.appendChild(txt);
// 插入一个新的节点 a
body.insertBefore(a, p);
};
};
</script>
</html>
运行
四、样式示例
语法
元素.style.样式名 = 样式值
这里有关修改样式注意的点有
1、 如果 CSS 样式名中含有 - 这种名称在JS 中是不合法的,例如: background-color我们需要将这种样式名,修改为 驼峰命名法backgroundColor
2、通过 style 属性设置的样式都是内联样式,而内联样式有较高的优先级,所以通过JS 修改的样式往往会立即显示。
3、但是如果在样式中写了 !important 则此时样式会有最高的优先级,即使通过 JS 也不能覆盖这个样式。此时将会导致 JS 修改的样式失效,所以尽量 不要给样式添加 !important
4、通过 style 读取也都是内联样式、无法读取样式表中的样式。
1、设置样式
代码示例
<!DOCTYPE html>
<html>
<head>
<title></title>
<style>
/* 设置一个基本的样式 */
#box{
width: 100px;
height: 100px;
background-color: darkcyan;
}
</style>
</head>
<body>
<button id="btn">更换样式</button>
<br>
<br>
<div id="box"></div>
</body>
<script>
window.onload = function () {
// 获取到 btn
var btn = document.getElementById("btn");
// 添加点击事件
btn.onclick = function () {
// 获取 box
var box = document.getElementById("box");
box.style.width = "150px"; // 修改宽度
box.style.height = "150px"; // 修改高度
box.style.backgroundColor = "deeppink"; // 修改背景颜色
};
};
</script>
</html>
运行
五、综合示例
1、霓虹灯特效示例
先看效果
代码示例
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body></body>
<script>
var tName="北京首都欢迎您";
var tLen=tName.length;
document.write("<div id='a' style='font-size:50px;'>"+tName+"</div>" );
var color=new Array("#FFCC00","#3333FF","FFCCAA","#FF0000","#FFCC33","#CC33FF");
var ic=0; //这里很关键,一定要设置全局变量
function DColor(){
var StrName=""; //这个很有必要,不然无法叠加单个汉字
for(var i=0;i<tLen;i++){ //下面一步才是精髓,设置每一个汉子不同颜色,然后拼接起来
var StrName=StrName+"<font color="+color[ic]+">"+tName.substring(i,i+1)+"</font>";
ic=ic+1; //下一个字下一个颜色
if(ic==color.length){
ic=0; //不设置为零,因为他是全局变量,那么会一直加到数组越界
}
}
a.innerHTML=StrName; //可以通过id.innerHTML改变界面的字的颜色
//设置时间的变化 改变情况
setTimeout("DColor()",200); //setTimeout代表每隔200毫秒运行一次函数
}
DColor(); //这个代表直接开始运行这个函数,而不用通过触发事件
</script>
</html>
核心点
:
核心点一 : 是ic=0写成全局变量,假如写在方法里,那么每一次初始化结果都一样,那么也不会有霓虹灯效果
核心点二 : color.length和tName.length如果相等同样也不会产生效果,因为每一次结束ic还是=0,而这里第一次DColor(),ic=0,第二次调用ic初始值=1,所以会有效果。
2、显示系统时间,秒一直动
效果
代码
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body onload="showTime()"> <!-- 当加载时就执行 -->
<label id="show">显示系统时间</label> <!-- 将系统时间显示在这个位置 -->
</body>
<script type="text/javascript">
function showTime(){
var objDate =new Date(); //得到系统时间对象
var year =objDate.getFullYear(); //获取年份
var month=objDate.getMonth()+1;//获取月份
var date =objDate.getDate();//获取日期
var hours=objDate.getHours();//小时
var minutes=objDate.getMinutes();//分钟
var seconds=objDate.getSeconds();//秒
var day =objDate.getDay(); //获取星期几
show.innerHTML=""+year+"年"+month+"月"+date+"日"+hours+":"+minutes+":"+seconds +" 周"+day; //通过id改变界面显示内容
//每隔1秒刷新一次
var timeId=setTimeout(showTime,1000);
}
</script>
</html>
一、理解冒泡与捕获
假设有这么一段代码
<body>
<div><p>标签</p>
</div>
</body>
转换成图如下
我们知道Dom是有节点关系的
body -> div -> p 之间的关系就是 爷爷 -> 父亲 -> 儿子。
我们来思考一个关键的问题
如果此时我们在 body div p 都绑定一个点击事件(click)。此时如果我们只点击 p标签,它会不会触发div绑定事件 和 body绑定事件?
答案是会的
那这个时候就会有一个问题,既然点击 p标签 会触发 div绑定事件 和 body绑定事件,那执行顺序是怎么样的呢?
body事件 -> div事件 -> p事件? 还是 p事件 -> div事件 -> body事件?
这两种不同的执行顺序就是对应上面的 事件冒泡 和 事件捕获。
事件捕获
事件从最上一级标签开始往下查找,直到捕获到事件目标(body事件 -> div事件 -> p事件)。
事件冒泡
事件从事件目标开始,往上冒泡直到页面的最上一级标签( p事件 -> div事件 -> body事件)
为了不混淆记忆它们,这里有个通俗的理解冒泡:
冒泡嘛,就像水里往上冒的泡泡,从一开始很小,然后慢慢变大直到破裂。所以是从小到大,也就是子标签到父标签传递的过程。那么事件捕获记住与冒泡相反就可以了。
二、事件冒泡 与 阻止冒泡
1、事件冒泡示例
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件冒泡</title>
<style>
#dv1{ /*为了直观 这里添加些样式*/
width: 300px;
height: 200px;
background-color: red;
}
#dv2{
width: 250px;
height: 150px;
background-color: green;
}
#dv3{
width: 200px;
height: 100px;
background-color: blue;
}
</style>
</head>
<body>
<div id="dv1">爷爷
<div id="dv2">父亲
<div id="dv3">儿子</div>
</div>
</div>
<script>
//事件冒泡:多个元素嵌套,有层次关系,这些元素都注册了相同的事件,如果里面的元素的事件触发了,外面的元素的该事件自动的触发了.
document.getElementById("dv1").onclick=function () {
console.log(this.id+" 爷爷");
};
document.getElementById("dv2").onclick=function () {
console.log(this.id+" 父亲");
};
//事件处理参数对象
document.getElementById("dv3").onclick=function (e) {
console.log(this.id+" 儿子");
//阻止事件冒泡
//e.stopPropagation();
};
</script>
</body>
</html>
运行结果
从这个示例我们可以看出3点
1、当点击儿子元素后,父亲和爷爷的点击事件也触发了。
2、onclick事件的顺序 儿子 - 父亲 - 爷爷。
3、当点击爷爷元素后,父亲和儿子的点击是不会触发的。
2、阻止事件冒泡
既然有冒泡事件,那肯定在实际开放过程中,你不需要冒泡,你只想儿子点击触发事件,父亲和爷爷不触发事件。
语法
1、window.event.cancelBubble=true; IE特有的,谷歌支持,火狐不支持
2、e.stopPropagation(); 谷歌和火狐支持
因为我用的是谷歌浏览器,所以这里用第二种方式阻止冒泡(只需把上面阻止事件冒泡的代码取消注释就可以了)
运行结果
从运行结果很明显看出,已经阻止了事件冒泡。
三、事件捕获
示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
<style>
#dv1 {
width: 300px;
height: 200px;
background-color: red;
}
#dv2 {
width: 250px;
height: 150px;
background-color: green;
}
#dv3 {
width: 200px;
height: 100px;
background-color: blue;
}
</style>
</head>
<body>
<div id="dv1">爷爷
<div id="dv2">父亲
<div id="dv3">儿子</div>
</div>
</div>
<script>
//为每个元素绑定事件 这里
/**
* 捕获时间 从外到里
* 1、 addEventListener不是每个浏览器都兼容的
* 2、 这里true为事件捕获 false为事件冒泡
*/
document.getElementById("dv1").addEventListener("click", function (e) {
console.log(this.id+" 爷爷");
}, true);
document.getElementById("dv2").addEventListener("click", function (e) {
console.log(this.id+" 父亲");
}, true);
document.getElementById("dv3").addEventListener("click", function (e) {
console.log(this.id+" 儿子");
}, true);
</script>
</body>
</html>
运行
很明显,这里是从外到里执行事件。
之前写过一篇有关DOM的博客:JavaScript(2)---DOM详解
DOM有个顶级对象叫:document
。同样BOM中也有顶级对象叫 window
。
它们的区别在于: DOM是一套操作HTML标签
的API。 BOM是一套操作浏览器
的API。
一、概念
1、什么是BOM
概念
BOM(浏览器对象模型) 提供了独立于内容而与浏览器窗口进行交互的对象。描述了与浏览器进行交互的方法和接口,可以对浏览器窗口进行访问和操作。
从这幅图可以看出,document也是window的子对象。因为页面中的所有内容也是属于浏览器的一部分,所以document也是属于window。
2、常见的BOM对象
1、window: # 代表整个浏览器窗口(window是BOM中的一个对象,并且是顶级的对象)
2、Navigator: # 代表浏览器当前的信息,通过Navigator我们可以获取用户当前使用的是什么浏览器
3、Location: # 代表浏览器当前的地址信息,通过Location我们可以获取或者设置当前的地址信息
4、History: # 代表浏览器的历史信息,通过History我们可以实现上一步/刷新/下一步操作
5、Screen: # 代表用户的屏幕信息
二、window对象
概念
window对象表示的是浏览器中打开的窗口,相当于是浏览器中的最顶层对象。
1、window常用方法
1、open(): # 在一个窗口中打开页面
2、setInterval(): # 设置定时器(执行n次)
3、clearInterval(): # 清除定时器
4、setTimeout(): # 设置定时器(只执行1次)
5、clearTimeout(): # 清除定时器
6、alert(): # 提示框
7、confirm(): # 确认提示框
8、propmt(): # 输入提示框
9、console(): # 浏览器控制台打印
10、onload(): # 页面加载完才触发的事件
注意:因为window对象使用非常频繁,所以当调用js中的window对象的方法时,可以省略对象名(window)不写。
2、示例
代码
<style type="text/css">
input{
display:block;
width: 120px;
height: 25px;
background-color: #cddc39;
margin-left: 250px;
margin-top:10px;
}
</style>
<script type="text/javascript">
function testAlert(){
window.alert("弹出alert");
}
/*
* 参数1:dialog中显示的内容
* 参数2,3:可点击的按钮(默认没有就是"ok","cancle")
* 返回值: ok->true cancle->false
*/
function testConfirm(){
var flag;
flag = window.confirm("请选择是否?","是","否");
if (flag)
window.alert("你点击的是-'确定'");
else
window.alert("你点击的是-'取消'");
}
/*
* 参数1:可以是一个资源地址(图片,本地地址...)
* 参数2:target of links
* 参数3:窗口特点......
*/
function testOpen (){
window.open("http://www.baidu.com","_blank","height=400px,width=400px,left=10px");
}
/*
* 参数1:提示语
* 返回值:在输入框中输入的内容
*/
function testPrompt (){
var str;
str = window.prompt("请输入您手机号码:");
window.alert("您刚才输入了:"+str);
}
/*
* 参数1:定时器要执行的方法(每隔一定时间执行)
* 参数2:定时器时间
*/
var intervalID;
function testInterval (){
intervalID = window.setInterval("testAlert()",2000);
}
/*
* 清除定时器
*/
function removeInterval (){
window.clearInterval(intervalID);
console.log("定时任务ID=" + intervalID);
}
/*
参数1:定时器要执行的方法(只执行一次)
参数2:定时器时
*/
var timeoutID;
function testTimeout (){
timeoutID = window.setTimeout("testAlert()",2000);
}
/*
* 清除定时器
*/
function removeTimeout (){
window.clearTimeout(timeoutID);
console.log("定时任务ID="+ timeoutID);
}
</script>
</head>
<body>
<input type="button" value="测试alert" onclick="testAlert()" />
<input type="button" value="测试open" onclick="testOpen()" />
<input type="button" value="测试Confirm" onclick="testConfirm()" />
<input type="button" value="测试testPrompt" onclick="testPrompt()" />
<input type="button" value="测试Interval" onclick="testInterval()" />
<input type="button" value="测试清除Interval" onclick="removeInterval()" />
<input type="button" value="测试Timeout" onclick="testTimeout()" />
<input type="button" value="测试清除Timeout" onclick="removeTimeout()" />
</body>
运行结果
三、location对象
概念
location对象提供了与当前窗口中加载的文档有关的信息,还提供了一些导航的功能,它既是window对象的属性,也是document对象的属性。
1、location常用属性
属性
1、location.href # 返回当前加载页面的完整URL
2、location.hash # 返回URL中的hash(#号后跟零或多个字符),如果不包含则返回空字符串。
3、location.host # 返回服务器名称和端口号(如果有)
4、location.hostname # 返回不带端口号的服务器名称。
5、location.pathname # 返回URL中的目录和(或)文件名。
6、location.port # 返回URL中指定的端口号,如果没有,返回空字符串。
7、location.protocol # 返回页面使用的协议。
8、location.search # 返回URL的查询字符串。这个字符串以问号开头。
位置操作
1、location.href = '网址' # 当前页面跳转到新的网址 <a href="网址"></a>
2、location.replace('网址') # 跳转但不会在历史记录中生成新纪录
3、location.reload() # 刷新当前页面
4、window.open('网址') # 会新建一个窗口打开页面<a href="网址" target='_blank'></a>
四、history对象
概念
history对象保存着用户上网的历史记录,在窗口打开的那一刻算起。因为history是window对象所以每个浏览器窗口都有自己的history对象与特定的window对象关联。
1、常用方法
1、history.back() # 后退一页
2、history.forward() # 前进一页
3、history.go(n) # 前进n页
4、history.go(-2) # 后退n页
5、history.go(0) # 相当于刷新
2、示例
为了实现 返回上一页和 返回下一页功能,这里需要两个简单页面
首页
<!DOCTYPE html>
<html lang="en">
<style type="text/css">
input{
width: 120px;
height: 25px;
background-color: #cddc39;
margin-top:10px;
}
</style>
<body>
<input type="button" value="跳转页面" id="btn1"/>
<input type="button" value="前进" id="btn2"/>
<script src="common.js"></script>
<script>
//跳转的
document.getElementById("btn1").onclick = function () {
window.location.href = "test.html";
};
//前进 前进一步就会又到跳转的页面
document.getElementById("btn2").onclick = function () {
window.history.forward();
};
</script>
</body>
</html>
test.html
<!DOCTYPE html>
<html lang="en">
<style type="text/css">
input{
width: 120px;
height: 25px;
background-color: #cddc39;
}
</style>
<body>
<input type="button" value="后退" id="btn"/>
<script src="common.js"></script>
<script>
//后退 后退一步就是到上一个页面
document.getElementById("btn").onclick = function () {
window.history.back();
};
</script>
</body>
</html>
运行结果
五、综合示例
1、一起摇起来
实现原理 :设置定时器 + 取消定时器 + 设置随机边距 实现
效果
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
<style>
img { width: 150px; height: 150px; }
div { margin-top: 15px; position: absolute; }
input { width: 120px; height: 25px; background-color: #cddc39; }
</style>
</head>
<body>
<input type="button" value="摇起来" id="btn1"/>
<input type="button" value="停止" id="btn2"/> <br>
<div id="dv">
<img src="11.jpg" alt=""/>
<img src="12.jpg" alt=""/>
</div>
<script src="common.js"></script>
<script>
//定时器ID 用于解除定时
var timeId="";
my$("btn1").onclick = function () {
clearInterval(timeId); //先清一次,因为不先清,如果用户多次点击“摇起来” 那么Id已经被覆盖 之前的定时任务不可能停止
timeId= setInterval(function () {
var x = parseInt(Math.random() * 100 + 1); //随机数
var y = parseInt(Math.random() * 100 + 1);
my$("dv").style.left = x + "px"; //设置元素的left和top属性值
my$("dv").style.top = y + "px";
}, 50); //定时时间50毫秒
};
my$("btn2").onclick=function () { //清除定时任务
clearInterval(timeId);
};
/**
* 获取指定标签对象
*/
function my$(id) {
return document.getElementById(id);
}
</script>
</body>
</html>
2、用户协议
使用场景 我们在阅读一些协议的时候,往往不能直接点同意,而是几秒后才能点同意。
效果
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<textarea name="texta" id="" cols="30" rows="10">
请仔细阅读协议,请仔细阅读协议,请仔细阅读协议,请仔细阅读协议.
</textarea>
<input type="button" value="请仔细阅读协议(5)" id="btn" disabled="disabled" />
<script>
var time=5; //5秒倒计时
var timeId= setInterval(function () {
time--;
my$("btn").value="请仔细阅读协议("+time+")";
if(time <= 0){
//停止定时器就可以
clearInterval(timeId);
//按钮可以被点击了
my$("btn").disabled=false;
my$("btn").value="同意";
}
},1000);
/**
* 获取指定标签对象
*/
面向对象这个概念并不陌生,如 C++、Java 都是面向对象语言。面向对象而言都会现有一个类的概念 ,先有类再有对象。类是实例的类型模板。
比如人类 是一个类 张三 李四 就是一个个对象,他们都是人类创建出的对象 所以都有人类的共同特性,比如 人类都会吃饭 人类都会走路 所以张三李四也会吃饭和走路。
JavaScript 没有类的概念,是基于原型的面向对象方式。它们的区别在于:
在基于类的面向对象方式中,对象(object)依靠类(class)来产生。
在基于原型的面向对象方式中,对象(object)则是依靠构造函数(constructor)和原型(prototype)构造出来的。
面向对象语言的第一个特性毫无疑问是封装,在 JS 中,封装的过程就是把一些 属性 和 方法 放到对象中“包裹”起来。
一、创建对象三种方式
1、原始方式创建对象
1) 字面量的方式
示例
var per = {
name: "张三",
age: 20,
sex: "男",
say: function () {
console.log("说话");
}
};
2) Object实例添加属性方法
示例
var per2=new Object();
per2.name="李四";
per2.age=30;
per2.sex="男";
per2.say=function () {
console.log("说话");
};
优点:代码简单。
缺点: 创建多个对象会产生大量的代码,编写麻烦,且并没有实例与原型的概念。
解决办法:工厂模式。
2、工厂模式
概念
工厂模式是非常常见的一种设计模式,它抽象了创建具体对象的过程。JS 中创建一个函数,把创建新对象、添加对象属性、返回对象的过程放到这个函数中,
用户只需调用函数来生成对象而无需关注对象创建细节。
示例
function createObject(name,age) {
this.name=name;
this.age=age;
this.say=function () {
console.log("说话");
};
}
var per1=createObject("张三",20);
var per2=createObject("李四",30);
优点:工厂模式解决了对象字面量创建对象代码重复问题,创建相似对象可以使用同一API。
缺点:因为是调用函创建对象,无法识别对象的类型。
解决办法:构造函数
3、构造函数
JS 中构造函数与其他函数的唯一区别,就在于调用它的方式不同。任何函数,只要通过new
操作符来调用,那它就可以作为构造函数。
示例
//自定义构造函数----->实例化对象
function Person(name,age,sex) {
this.name=name;
this.age=age;
this.sex=sex;
this.say=function () {
console.log("说话");
};
}
//构造函数---->创建对象
var per1=new Person("张三",20,"女");
var per2=new Person("李四",30,"女");
通过构造函数new
一个实例经历了四步:
1. 创建一个新对象;
2. 将构造函数内的 this 绑定到新对象上;
3. 为新对象添加属性和方法;
4. 返回新对象(JS 引擎会默认添加 return this;)。
而通过构造函数创建的对象都有一个constructor
属性,它是一个指向构造函数本身的指针,因此就可以检测对象的类型。
alert(per1.constructor === Person) //true
alert(per1 instanceof Person) // true
但是仍然存在问题:
alert(per1.say == per2.say) //false
同一个构造函数中定义了say()
,而不同对象的同名函数却是不相等的,意味着这两个同名函数的内存空间不一致,也就是构造函数中的方法要在每个实例上重新创建一次。
这显然增加不必要内存空间。
优点:解决了类似对象创建问题,且可以检测对象类型。
缺点:构造函数方法要在每个实例上新建一次。
解决办法:原型模式。
二、原型模式
1、概念
在JS中,创建对象的方式有工厂模式和构造函数模式等; 而构造函数模式最大的问题在于:构造函数中的每个方法都需要在实例对象中重新创建一遍,不能复用
,
所以为了解决这一个问题,就需要使用原型模式来创建对象。原型模式是把所有实例共享的方法和属性放在一个叫做 prototype(原型)
的属性中 ,在创建一个函数
时都会有个prototype属性, 这个属性是一个指针,指向一个对象,是通过调用构造函数而创建的那个对象实例的原型对象。
如果你学习过java,我们可以简单理解原型就好比我们的静态方法,任何对象都可以共享这个静态方法。
作用
共享数据,节省内存空间。
2、举例
使用原型,就意味着我们可以把希望实例共享的属性和方法放到原型对象中去,而不是放在构造函数中,这样每一次通过构造函数new
一个实例,原型对象中定义
的方法都不会重新创建一次。
示例
//原型的作用之一:共享数据,节省内存空间
function Person() {
}
//通过构造函数的原型添加属性和方法
Person.prototype.name = "张三";
Person.prototype.age = "20";
Person.prototype.say = function() {
alert('通过原型创建吃饭方法');
};
var person1 = new Person();
var person2 = new Person();
alert(person1.name); //"张三"
alert(person2.name); //"张三"
alert(person1.say == person2.say); //true 通过原型创建的方法就为true
优点:与单纯使用构造函数不一样,原型对象中的方法不会在实例中重新创建一次,节约内存。
缺点:使用空构造函数,实例 person1 和 person2 的 name
都一样了,我们显然不希望所有实例属性方法都一样,它们还是要有自己独有的属性方法。
并且如果原型中对象中有引用类型值,实例中获得的都是该值的引用,意味着一个实例修改了这个值,其他实例中的值都会相应改变。
解决办法:构造函数+原型模式组合使用。
三、构造函数+原型模式
最后一种方式就是组合使用构造函数和原型模式,构造函数用于定义实例属性,而共享属性和方法定义在原型对象中。这样每个实例都有自己独有的属性,
同时又有对共享方法的引用,节省内存。
//原型的作用之一:共享数据,节省内存空间
//构造函数
function Person(age,sex) {
this.age=age;
this.sex=sex;
}
//通过构造函数的原型添加一个方法
Person.prototype.eat=function () {
console.log("通过原型创建吃饭方法");
};
var per1=new Person(20,"男");
var per2=new Person(20,"女");
alert(per1.eat == per2.eat); //通过原型创建的方法就为true
这种构造函数与原型模式混成的模式,是目前在 JS 中使用最为广泛的一种创建对象的方法。
再上一篇有简单讲过原型:JavaScript(5)--- 面向对象 + 原型
讲原型链知识之前,先说几个重要的结论。
1、原型链就是 对象的__proto__所连接的链状结构
2、prototype 属性是函数独有的
3、__proto__ 属性是对象独有的,实例原型(Object.prototype)也是对象,所以也会有__proto__属性
下面我们一步一步来讲解原型链
一、prototype属性
1、构造函数创建对象
我们先使用构造函数创建一个对象:
function Person() {
}
var p = new Person();
p.name = 'xiaoxiao';
console.log(p.name); //xiaoxiao
2、 prototype属性
概念
它是 函数独有的属性,它从一个函数指向另一个对象,代表这个对象是这个函数的原型对象,这个对象也是当前函数所创建的实例的原型对象。
prototype设计之初就是为了实现继承,让构造函数创建的所有实例,都能够共享这个原型属性和方法。
有了prototype我们不需要为每一个实例创建重复的属性方法,而是将属性方法创建在构造函数的原型对象上(prototype)。那些不需要共享的才创建在构造函数中。
示例
function Person(){
}
Person.prototype.age=18; //原型属性
var p1 = new Person();
var p2 = new Person();
console.log(p1.age);//18
console.log(p2.age);//18
思考
这个函数的 prototype 属性到底指向的是什么呢?
其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型 。思考
那么什么是原型呢?
每一个 JavaScript 对象 (null 除外 ) 在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型 ” 继承 ” 属性。
这里p1和p2就是实例对象,而Person.prototype就是它们的原型对象。p1和p2可以去继承原型对象的方法和属性。
让我们用一张图表示构造函数和实例原型之间的关系:
二、__proto__属性
概念
__proto__属性是对象(包括函数,函数也是对象)独有的。__proto__属性是从一个对象指向另一个对象,该属性指向的就是该对象的原型对象(也可以理解为父对象)。
示例
function Person(){
}
var p = new Person();
console.log(p.__proto__ === Person.prototype);//true
更新下关系图
__proto__通常称为隐式原型,prototype为显示原型,那我们可以说一个对象的隐式原型指向了该对象的构造函数的显示原型。那么我们在显示原型上定义的属性方法,
通过隐式原型传递给了构造函数的实例。这样一来实例就能很容易的访问到构造函数原型上的方法和属性了。
三、constructor属性
概念
constructor是对象才有的属性,它是从一个对象指向一个函数的。指向的函数就是该对象的构造函数。
注意了 实例原型也是对象,所以也会有constructor属性,我们来验证一下
function Person(){
}
console.log(Person === Person.prototype.constructor);//true
console.log(person.constructor); // ƒ Person(){}
再更新下关系图
总结 通过上面的演示说明,我们可以得出
function Person(){
}
var person = new Person();
console.log(person.__proto__ === Person.prototype);//true
console.log(Person === Person.prototype.constructor);//true
了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲实例和原型的关系
四、实例与原型
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
示例
function Person(){
}
var person = new Person();
Person.prototype.name='张三';
person.name='李四';
console.log(person.name);//李四
delete person.name;
console.log(person.name);//张三
在这个例子中,我们设置了 person 的 name 属性,所以我们可以读取到为 ’李四’ ,当我们删除了 person 的 name 属性时,读取 person.name ,从 person 中找不到
就会从 person 的原型中查找,name为 ’张三’ 。但是万一Person.prototype原型中还没有找到呢?那会到原型的原型去查找。也就是Object.prototype
示例
function Person(){
}
var person = new Person();
Person.prototype.name='李四';
person.name='张三';
console.log(person.name);//张三
delete person.name;
console.log(person.name);//李四
Object.prototype.name='obj';
delete Person.prototype.name;
console.log(person.name);//obj
所以原型对象是通过 Object 构造函数生成的,结合之前所讲 , 实例的 _proto_ 指向构造函数的 prototype, 所
再更新下关系图
五、原型链
那 Object.prototypey也是对象,那它也应该会有原型。只不过有是有,只是为null
, 所以查到 Object.prototype 就可以停止查找了 。
所以最后一张关系图就是
总结
图中由相互关联的原型组成的链状结构就是原型链,也就是红色的这条线
。
概念
首先继承是一种关系,类(class)与类之间的关系,JS中没有类,但是可以通过构造函数模拟类,然后通过原型来实现继承,继承也是为了数据共享。
之间有讲过js中的原型和原型链:JavaScript(6)--- 原型链
原型链就是继承的一种,子类 没有的东西到 父类 去寻找, 父类 如果还是没有就会到 爷爷 那去寻找,直到顶层,也就是到 Object 这一层如果还是没有就真没有了。
当然除了原型链,还有其它继承方式,下面都会一一说明。
1、原型链继承
2、借用构造函数(经典继承)
3、组合继承
4、原型式继承
5、寄生式继承
6、寄生组合式继承
一、原型链继承
1、示例
//人类构造函数
function Person(name, age, sex) {
this.name = name;
this.sex = sex;
this.age = age;
}
//人的原型方法
Person.prototype.eat = function () {
console.log("人可以吃东西");
};
//学生构造函数
function Student(score) {
this.score = score;
}
//改变学生的原型的指向即可===>学生和人已经发生关系
Student.prototype = new Person("小明", 10, "男");
//添加学生原型方法
Student.prototype.study = function () {
console.log("学生需要学习");
};
var stu = new Student(100);
//父类 所拥有的特性 被继承来的
console.log(stu.name);
console.log(stu.age);
console.log(stu.sex);
stu.eat();
//学生自有的特性
console.log("---------------");
console.log(stu.score);
stu.study();
运行结果
关键点
让新实例的原型指向父类的实例,这样就可以继承父类的属性和方法,实现数据共享。
缺点
1、因为改变原型指向的同时实现继承,直接初始化了属性,继承过来的属性的值都是一样的了
2、引用类型的属性被所有实例共享
二、借用构造函数继承
1、示例
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
Person.prototype.say = function () {
console.log("父类方法");
};
function Student(name,age,sex,score) {
//借用构造函数 -> 构造函数名字.call(当前对象,属性,属性,属性....);
Person.call(this,name,age,sex);
this.score = score;
}
var stu1 = new Student("张三",10,"男","100");
console.log(stu1.name, stu1.age, stu1.sex, stu1.score);
var stu2 = new Student("李四",20,"女","120");
console.log(stu2.name, stu2.age, stu2.sex, stu2.score);
var stu3 = new Student("王五",30,"妖","130");
console.log(stu3.name, stu3.age, stu3.sex, stu3.score);
运行结果
重点
用 .call() 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))
解决:解决了属性继承,并且值不重复的问题
缺陷
1、只继承了父类构造函数的属性,没有继承父类原型的属性。
2、无法实现构造函数的复用。(每次用每次都要重新调用)
3、每个新实例都有父类构造函数的副本,臃肿。
三、组合继承
概念
:原型链继承+借用构造函数继承 = 组合继承
1、示例
//父类构造函数
function Person(name,age,sex) {
this.name=name;
this.age=age;
this.sex=sex;
}
//父类原型
Person.prototype.say=function () {
console.log("父类方法");
};
function Student(name,age,sex,score) {
//借用构造函数:属性值重复的问题
Person.call(this,name,age,sex);
this.score=score;
}
//改变原型指向----继承
Student.prototype=new Person();//不传值
//子类原型
Student.prototype.eat=function () {
console.log("子类方法");
};
//创建子类对象
var stu=new Student("张三",20,"男","100分");
console.log(stu.name,stu.age,stu.sex,stu.score);
stu.say();
stu.eat();
//创建子类对象
var stu2=new Student("李四",200,"女","90分");
console.log(stu2.name,stu2.age,stu2.sex,stu2.score);
stu2.say();
stu2.eat();
运行结果
解决
解决了 只继承了父类构造函数的属性,没有继承父类原型的属性 的缺陷。
缺陷
调用了两次父类构造函数(耗内存),(一次是在子类型构造函数内部,另一次是在创建子类型原型的时候)
四、原型式继承
1、示例
function CreateObj(o) { //传递一个字面量函数
function F() {} //创建一个构造函数
F.prototype = o; //把字面量函数赋值给构造函数的原型
return new F(); //最终返回出实例化的构造函数
}
var person = { //字面量对象
name: '小小',
friend: ['香蕉', '小鳄鱼', '青蛙']
};
var person1 = CreateObj(person); //传递
console.log(person1.name);
person1.name = "中中"
console.log(person1.name);
var person2 = CreateObj(person); //传递
console.log( person2.name);
运行结果
重点
用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
缺点
包含引用类型的属性值始终都会共享相应的值, 这点跟原型链继承一样。
注意 这里修改了person1.name的值,person2.name的值并未改变,是因为person1.name='person1'是给person1添加了name值,并非修改了原型上的name值。
因为我们找对象上的属性时,总是先找实例上对象,没有找到的话再去原型对象上的属性。实例对象和原型对象上如果有同名属性,总是先取实例对象上的值
五、寄生式继承
概念
它的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某张方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function createAnother(original){
var clone = Object.create(original); //这个就是上面的原型函数
clone.sayHi = function(){ //只是通过这个函数 可以新增加属性和方法
console.log('2020.1.18号');
}
return clone;
}
我理解了一下,就是又创建了一个函数,然后在函数内部定义一个对象来实现原型式继承方式,然后再给这个对象添加方法或属性值呀,最后再将这个对象返回,
这就成为了寄生式继承方式。让我们来调用这个函数吧!
var person = {
name:"Nick",
friends:["xiaowang","xiaochen"]
};
var person1 = createAnother(person); //新增加sayHi方法
person1.sayHi();
重点 就是给原型式继承外面套了个壳子。
优点
没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
缺点
没用到原型,无法复用。
六、寄生组合式继承(常用)
上面的组合继承最大的缺点就是:组合继承(构造函数和原型的组合)会调用两次父类构造函数的代码。
因此引入寄生组合式继承,寄生组合继承 是寄生继承跟组合继承的结合版。
即通过借用构造函数来继承属性,通过原型链的方式来继承方法
,而不需要为子类指定原型而调用父类的构造函数,我们需要拿到的仅仅是父类原型的一个副本。
因此可以通过传入子类和父类的构造函数作为参数,首先创建父类原型的一个复本,并为其添加constrcutor,最后赋给子类的原型。这样避免了调用两次父类的
构造函数,为其创建多余的属性。
1、代码示例
// 父类构造函数
function Parent(name){
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
//父类原型
Parent.prototype.sayName = function(){
console.log(this.name);
}
//子类构造函数
function Child(name,age){
//拷贝父类构造函数
Parent.call(this,name);
this.age = age;
}
//原型式继承
function CreateObj(o){
function F(){};
F.prototype = o;
return new F();
}
// Child.prototype = new Parent(); // 这里换成下面
function prototype(child,parent){
var prototype = CreateObj(parent.prototype); //创建对象
prototype.constructor = child; //增强对象
child.prototype = prototype; //指定对象
}
prototype(Child,Parent);
var child1 = new Child('小小', 18);
console.log(child1);
运行结果
优点
这种方式的高效率体现它只调用了一次Parent构造函数,并且因此避免了再Parent.prototype上面创建不必要的,多余的属性。普遍认为寄生组合式继承是引用类型
最理想的继承方式
七、总结
这么多种继承方式,本质上其实就两种
1.通过原型链,即子类的原型指向父类的实例从而实现原型共享。
2.借用构造函数,即通过js的apply、call实现子类调用父类的属性、方法;
原型链方式: 可以实现所有属性方法共享,但无法做到属性、方法独享(例如Sub1修改了父类的函数,其他所有的子类Sub2、Sub3...想调用旧的函数就无法实现了);
借用构造函数: 除了能独享属性、方法外还能在子类构造函数中传递参数,但代码无法复用。总体而言就是可以实现所有属性方法独享,但无法做到属性、方法共享
(例如,Sub1新增了一个函数,然后想让Sub2、Sub3...都可以用的话就无法实现了,只能Sub2、Sub3...各自在构造函数中新增)。
组合继承: 就是把以上两种继承方式一起使用,把共享的属性、方法用原型链继承实现,独享的属性、方法用借用构造函数实现。
理解闭包
我的理解是:闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以简单这样理解
"函数A中,有一个函数B,函数B中可以访问函数A中定义的变量或者是数据,此时形成了闭包"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
一、闭包的由来
1、函数嵌套函数
JS之所以会有闭包,是因为JS不同于其他规范的语言,JS允许一个函数中再嵌套子函数,正是因为这种允许函数嵌套,导致JS出现了所谓闭包。
function a(){
var a=1;
function b(){
alert(a);
};
b();
}
a();
在JS正常的函数嵌套中,父函数a调用时,嵌套的子函数b的结构,在内存中产生,然后子函数又接着调用了,子函数b就注销了,此时父函数a也就执行到尾,父函数a也会把
自己函数体内调用时生成的数据从内存都注销。
function a(){
var a=1;
function b(){
alert(a);
}
return b;
}
var f=a();
这个例子中,父函数调用时,函数体内创建了子函数b,但是子函数并没有立即调用,而是返回了函数指针,以备“日后再调用”,因为“准备日后调用”,此时父函数a执行完了
也不敢注销自己的作用域中的数据,因为一旦注销了,子函数b日后再调用时,沿着函数作用域链往上访问数据,就没有数据可以访问了,这就违背了JS函数作用域链的机制。
正因此,子函数要“日后调用”,导致父函数要维持函数作用域链,而不敢注销自己的作用域,那么这个子函数就是“闭包函数”。
2、JS变量的作用域
理解闭包还要理解一个知识点就是 JavaScript的变量作用域。与大多数语言相同,JavaScript变量的作用域分为全局变量和局部变量。**函数内部可以访问全局变量,但是
函数外部无法访问函数内部的局部变量**
示例
function f1(){
let n =100;
var m=99;
console.log(n);
console.log(m);
}
f1(); //输出:100 , 99
console.log(n); //输出:undefined
console.log(m); //输出:undefined
注意
在函数内部声明变量的时候一定要用let或者var。否则,实际上声明了一个全局变量
思考
函数外部如何读取局部变量?
要在函数外部读取局部变量,可以通过在函数内部再定义一个函数的方法来实现。
示例
function f1(){
var n =100;
function f2(){
console.log(n);
}
return f2;
}
let result =f1();
result(); //输出100
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript
语言特有的"链式作用域"结构,子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
二、闭包的作用
闭包可以用在许多地方。它的最大用处有两个 1、可以读取函数内部的变量。2、让变量的值始终保持在内存中。
第一个上面已经实现了,这里就不再重复说明。
1、让变量的值始终保持在内存中
一般来说,全局变量的生存周期是永久的,直到我们主动销毁。而在函数内的局部变量来说,当退出函数时,这些函数变量立即失去它们的价值,也就被垃圾回收机制
销毁了,也算寿终正寝。
示例
//普通的函数
function f1() {
var num = 10;
num++;
return num;
}
console.log(f1()); //11
console.log(f1()); //11
console.log(f1()); //11
可是在闭包中,却不是这样。它可以让这些变量的值使用保持在内存中(不被垃圾回收)
示例
//函数模式的闭包
function f2() {
var num = 10;
function f3() {
num++;
return num;
}
return f3;
}
var ff = f2();
console.log(ff());//11
console.log(ff());//12
console.log(ff());//13
由此可见,当退出函数后,局部变量 num 并没有立即消失,一直存在,这样在第二次调用时 num 才会是在 11的基础上加1,是12,以后每次调用也才会不断加1。
思考
为什么会这样呢?
原因就在于f2是f3的父函数,而f3被赋给了一个全局变量,这导致f3始终在内存中,而f3的存在依赖于f2,因此f2也始终在内存中,不会在调用结束后,被垃圾回收机制
(garbage collection)回收。
三、示例
为了更好理解闭包在实际开发中的应用,这里举几个简单例子来说明闭包。
1、产生三个随机数,但是都是相同的
代码
<script>
//非闭包
function showRandom() {
var num=parseInt(Math.random()*10+1);
console.log(num);
}
showRandom(); //随机
showRandom(); //随机
showRandom(); //随机
console.log("===========================");
//闭包的方式,产生三个随机数,但是都是相同的
function f1() {
//这个只执行一次
var num=parseInt(Math.random()*10+1);
return function () {
console.log(num);
}
}
var ff=f1();
ff(); //这里三个值都是一样的
ff();
ff();
</script>
运行结果
2、点赞示例
先展示运行结果
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>点赞应用</title>
<style>
ul {
list-style-type: none;
}
li {
float: left;
margin-left: 10px;
}
img {
width: 200px;
height: 180px;
}
input {
margin-left: 30%;
}
</style>
</head>
<body>
<ul>
<li><img src="images/ly.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
<li><img src="images/lyml.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
<li><img src="images/fj.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
<li><img src="images/bd.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
</ul>
<script>
//获取所有的按钮
//根据标签名字获取元素
function my$(tagName) {
return document.getElementsByTagName(tagName);
}
//闭包缓存数据
function getValue() {
var value=2;
return function () {
//每一次点击的时候,都应该改变当前点击按钮的value值
this.value="赞("+(value++)+")";
}
}
//获取所有的按钮
var btnObjs=my$("input");
//循环遍历每个按钮,注册点击事件
for(var i=0;i<btnObjs.length;i++){
//注册事件
btnObjs[i].onclick=getValue();
}
</script>
</body>
</html>
一、跨域原理(同源策略)
在项目搭建的初期,因为现在项目基本上都是前后端分离,所以不可避免地会遇到跨域问题,而造成跨域的罪魁祸首就是浏览器的同源策略。所以要解决跨域,
我们必须知道什么是浏览器的同源策略。
1、什么是同源策略
概念
它是一个著名的安全策略。所有支持JavaScript 的浏览器都会使用这个策略。所谓同源是指,域名,协议,端口相同
。
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。
注意
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
2、同源策略限制什么
概念
同源策略限制 从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。它的存在可以保护
用户隐私信息,防止身份伪造等(读取Cookie)。
同源策略限制内容有
1、Cookie、LocalStorage、IndexedDB 等存储性内容
2、DOM 节点
3、AJAX 请求不能发送
但是有三个标签是允许跨域加载资源
1.<script src=xxx>
2.<img src=xxx>
3.<link href=xxx>
也就是说这三个标签可以不受同源策略限制,这也是为什么使用<img>
标签的时候,可以引用外部的服务器图片。
解决跨域的方式很多,这里介绍两种最简单的方式:JSONP 和 CORS。
二、JSONP
原理:
<script>是可以跨域的,通过动态创建script标签,然后利用src属性进行跨域。
1、JavaScript示例
前端代码
<html lang="en">
<head>
<title>jsoup</title>
<script type="text/javascript" src="/jquery/jquery-2.1.3.min.js"></script>
<script>
// 1.定义方法 这个是会被回调的函数
function coming(data) {
console.log(data);
}
</script>
<script>
// 2.动态拼接<script>标签,把需要调用的本地方法名传递给后端
let url = "http://localhost:8088/getUser/1?callback=coming";
// 平时都是手写<script src="">,现在我们在代码中动态构造script标签并设置src属性
let script = document.createElement('script');
script.type = "text/javascript";
script.src = url;
// 把script标签加入head(因为script标签本来也是在html的head中),并发起请求,后端响应后浏览器会自动执行返回的script片段
document.getElementsByTagName('head')[0].appendChild(script);
</script>
</head>
</html>
后段代码
@RestController
public class UserController {
@GetMapping(value = "/getUser/{id}")
public String getUser(@PathVariable("id") String id, String callback) {
// 拼接方法名 + 参数(这就会调用上面script中的coming方法)
return callback + "({\"username\": \"小小\",\"age\": 4 "})";
}
}
2、jquery示例
上面说过了,JSONP其实算是一种对同源策略规则的投机取巧,实现方式可以多种多样。JQuery对JSONP也有实现
<html lang="en">
<head>
<title>jquery示例</title>
<script type="text/javascript" src="/jquery/jquery-2.1.3.min.js"></script>
</head>
<body>
<input type="text" id="result">
<input type="button" onclick="onClick()" value="点击">
</body>
<script>
/**
* 照常使用ajax,只要把dataType改为"jsonp"即可。
* JQuery在前端替我们做了两件事:
* 1.URL后拼接方法名(随机生成)
* 2.解析从后端得到的js片段,把正确的值传入success:function()
*
* 后端还是要按原来的做 JQuery自动帮我们在URL里加了callback参数。
*/
function onClick() {
$.ajax({
type: "GET",
url: "http://localhost:8088/getUser/1",
dataType: "jsonp",
success: function(data){
$('#result').val(data.name);
}
});
}
</script>
</html>
总结
1、JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,且jQuery默认就会给JSONP的请求清除缓存。
2、JSONP优点是 兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是 仅支持get方法具有局限性。
三、CORS
1、CORS原理
浏览器认为只要后端没返回CORS头(Access-Control-Allow-Origin),就认为后端不允许跨域,返回的数据不可靠。所以只要后端能够返回浏览器需要的请求头,
即可跨域(响应数据就不会被同源策略抛弃),这个是表层原理。
不过目前不是所有浏览器都支持该功能(会自动带上请求头),IE浏览器不能低于IE10。所以最终来看,CORS这种方案不需要前端做任何事情,只需后端配合即可
。
2、示例
前端代码
<html lang="en">
<head>
<script type="text/javascript" src="/jquery/jquery-2.1.3.min.js"></script>
</head>
<body>
<input type="text" id="result">
<input type="button" onclick="onButtonClick()" value="get_button">
</body>
<script>
/**
* 普通AJAX,没有设置jsonp
*/
function onButtonClick() {
$.ajax({
type: "GET",
url: "http://localhost:8080/getUser/1",
success: function(data){
$('#result').val(data.name);
}
});
}
</script>
</html>
后段代码
方式一 方法上加@CrossOrigin
@RestController
public class UserController {
/**
* 在跨域方法上加@CrossOrigin即可完美解决跨域问题
*/
@CrossOrigin("http://localhost:8088")
@GetMapping(value = "/getUser/{id}")
public User getUser(@PathVariable("id") String id) {
User user = new User();
user.setName("小小");
user.setAge(4);
return user;
}
}
方式二 Controller上加@CrossOrigin
@CrossOrigin还可以加载Controller上,这样Controller的所有方法都支持跨域。
@RestController
@CrossOrigin("http://localhost:8088")
public class UserController {
@GetMapping(value = "/getUser/{id}")
public User getUser(@PathVariable("id") String id) {
User user = new User();
user.setName("小小");
user.setAge(4);
return user;
}
}
方式三 @Bean配置跨域Filter
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
//1.添加CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//1) 允许的域,如果写*,那么cookie就无法使用了
config.addAllowedOrigin("http://localhost:8088");
//2) 是否发送Cookie信息
config.setAllowCredentials(true);
//3) 允许的请求方式
config.addAllowedMethod("*");
// 4)允许的头信息
config.addAllowedHeader("*");
// 5) 有效时长
config.setMaxAge(3600L);
//2.添加映射路径,我们拦截一切请求
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
//3.返回新的CorsFilter.
return new CorsFilter(configSource);
}
}
总结
本人在实际开发中都是通过CORS解决跨域问题,而且是通过第三种方式配置全局的bean对象。