【转】 前端笔记之JavaScript(九)定时器&JSON&同步异步/回调函数&函数节流&call/apply
一、快捷位置和尺寸属性
DOM已经提供给我们计算后的样式,但是还是觉得不方便,因为计算后的样式属性值都是字符串类型。
不能直接参与运算。
所以DOM又提供了一些API:得到的就是number类型的数据,不需要parseInt(),直接可以参与运算。
offsetLeft和offsetTop offsetWidth和offsetHeight clinetWidth和clinetHeight |
1.1 offsetWidth和offsetHeight
全线兼容,是自己的属性,和别的盒子无关的。
一个盒子的offsetWidth值就是自己的width+左右padding+左右border的宽度
一个盒子的offsetHeight值就是自己的height+上下padding+上下border的宽度
var oBox = document.getElementById("box"); alert(oBox.offsetWidth) alert(oBox.offsetHeight) |
如果盒子没有宽度,那么浏览器都将px值当做offsetWidth,而不是100%
如果盒子没有高度,用内容撑开,那么浏览器都将px值当做offsetWidth。
1.2 clientWidth和clientHeight
全线兼容,就IE6有一点点问题。
var oBox = document.getElementById("box"); alert(oBox.clientWidth) alert(oBox.clientHeight) |
clientWidth就是自己的width+padding的值,也就是说,比offsetWidth少了border
clientHeight就是自己的height+padding的值,也就是说,比offsetHeight少了border
如果盒子没有宽度,那么浏览器都将px值当做clientWidth,而不是100%
如果盒子没有高度,用内容撑开,IE6的clientHeight是0,其他浏览器都是合理数值。
1.3 offsetLeft和offsetTop属性
获取距离方法是一样,参考元素也是一样的,以offsetLeft举例。
offsetLeft:偏移某个元素的左侧的距离。
offsetTop:偏移某个元素的顶部的距离
这两个属性兼容性非常差,不要急,慢慢看。
IE9、IE9+、Chrome等高级浏览器中:
一个元素的offsetLeft值,就是这个元素的左边框外,到自己offsetParent对象的左边框内的距离(number类型)
offsetParent是:自己祖先元素中,已经定位的元素,不用考虑自己是否定位。
每个元素,天生都有一属性,叫“offsetParent”,表示自己的“偏移参考盒子”。offsetParent就是自己祖先元素中,离自己最近的已经定位的元素,如果自己祖先元素中,没有任何定位的盒子,那么offsetParent对象就是body。
<body class="body"> <div class="box1"> <div class="box2"> <div class="box3"> <p></p> </div> </div> </div> </body>
var op = document.getElementsByTagName('p')[0]; alert(op.offsetLeft); alert(op.offsetParent.className);
IE6、IE7中 offsetParent对象是谁,和高级浏览器有非常大的不同:
情形1:如果自己没有定位,那么自己的offsetParent对象就是自己的祖先元素中,离自己最近的有width或有height的元素
<body class="body"> <div class="box1"> → 有宽高,不是离的最近的 <div class="box2"> → ,有宽高,offsetParent <div class="box3"> → ,没有宽高 <p></p> → 没有定位 </div> </div> </div> </body>
情形2:自己如果有定位属性,那么自己的offsetParent就是自己祖先元素中离自己最近的有定位的元素,如果父亲都没有定位就的HTML元素。
<body class="body"> <div class="box1"> <div class="box2"> → 有宽高,有定位,offsetParent <div class="box3"> → 有宽高,没有定位 <p></p> → 有定位,找定位的父亲,没定位就找有宽高的父亲 </div> </div> </div> </body>
IE8的offsetParent是谁呢?和高级浏览器一致:
无论自己是否定位,自己的offsetParent就是自己祖先元素中,离自己最近的已经定位的元素。
这一点,没有任何兼容问题!但是,多算了一条父亲的边框。
总结:
IE6、7 |
IE8 |
IE9、IE9+、高级浏览器 |
|
offsetParent |
如果自己没有定位,那么自己的父亲中有width或有height或者有定位的元素。 如果自己有定位,那么就是和高级浏览器一致。 |
和高级浏览器一致 |
自己祖先元素中,离自己最近的已经定位的元素 |
offsetLeft offsetTop |
和高级浏览器一致 |
多算一条offsetParent(父亲)边框 |
自己的边框外到offsetParent对象的边框内 |
兼容性解决办法:不是能力检测,也不是版本检测,而是善用这个属性,确保顺序的使用条件:
自定位,父无边(父亲也要定位)
这样的话,所有浏览器的值都是引用的。
总结:这6个属性要铭记于心,offsetLeft和offsetTop比较闹腾,但是合理使用,也没有兼容性问题。
二、定时器
2.1定时器
window.setInterval(匿名函数,间隔时间); //间隔定时器 window.setTimeout(匿名函数,间隔时间); //单次定时器 |
第一个参数:函数,既可以是一个函数的函数名引用,也可以是一个匿名函数。不能加()。
第二个参数:时间间隔,单位是毫秒,1秒钟等于1000毫秒
能够使每间隔时间,调用函数一次。习惯叫做定时器,按理说叫做“间隔器”。
var i = 0; window.setInterval(function(){ i++; //每间隔1000毫秒,执行一次函数 console.log("间隔定时器,2秒执行一次,i的值:" + i ); },1000);
间隔时间是以毫秒为单位,1000毫秒就是1秒。
“毫”就是千分之一,
“厘”就是百分之一,
“分”就是十分之一
第一参数是函数,所以可以把一个匿名函数往里放,更可以用一个有名的函数的引用放里面:
function fun(){ alert("你好!"); } window.setInterval(fun,1000);
函数执行的方法:
①函数名或变量名加()执行。
②将一个函数绑定给某个事件,事件被触发,自动执行函数。
③将函数传给定时器的第一个参数,每隔时间间隔,自动执行函数。
定时器的开启不需要任何关键字,只要程序能够执行到定时器部分,就会立即被开启,到第一个时间间隔后,会第一次执行函数。
定时器是window对象的方法,可以省略window,所以:
setInterval(function(){ alert("你好!"); },1000);
单次定时器:
setTimeout(function(){ alert("boom~~~没有了"); },1000);
2.2简单运动模型
视觉暂留:是一种视觉欺诈效果,人眼有视觉残留,每移动一步足够短,连续起来看起来就像在运动。残留时间是0.1秒到0.4秒。把连续相关的画面,连续播放,就是运动了。
信号量编程:定义一个全局信号量,定义一个定时器,函数内部每执行一次,就让信号量自加,给css属性随时赋值。最终看起来就是在运动。
var oBox = document.getElementById("box"); var nowLeft = 0; //初始值 setInterval(function(){ //开启定时器,每间隔20毫秒执行一次函数,函数内部进行变量自加,并赋值 nowLeft += 5; console.log(nowLeft); oBox.style.left = nowLeft + "px"; },20);
间隔时间是20毫秒,那么1秒执行函数50次,也就是说,这个动画是每秒50帧。
控制简单运动速度的方法:
1、增加每一步的步长可以加快速度,更改信号量自加的值。
2、缩短间隔时间,相当于每一秒走的次数增加,1秒钟走的距离越远。Flash中有一个帧频的概念fps,每间隔多长时间走一帧,时间间隔如果是100毫秒,fps就是10。
注意性能问题:
Chrome浏览器能够支持最小5的间隔时间,每秒200帧。
IE6、7、8、9只能支持最小50的间隔时间,每秒执行20帧。
var nowLeft = 0; //初始值 setInterval(function(){ nowLeft += 5; },50);
注意:简单运动,不需要知道走的总步长,只要知道每一步走的步长和间隔时间,就能实现。
2.3清除定时器
clearInterval() 清除间隔定时器 clearTimeout() 清除单次定时器 |
清除定时器时,要将定时器赋值给某个变量,停止时只要清除变量的引用即可。
例如:clearInterval(定时器变量)
var btn = document.getElementsByTagName("button"); //开启间隔定时器 var timer01 = null; var timer02 = null; btn[0].onclick = function(){ timer01 = setInterval(function(){ alert("2秒执行一次间隔定时器"); },2000); } //开启单次定时器 btn[1].onclick = function(){ timer02 = setTimeout(function(){ alert("单次定时器"); },2000); } //清除间隔定时器 btn[2].onclick = function(){ clearInterval(timer01); } //清除间隔定时器 btn[3].onclick = function(){ clearTimeout(timer02); }
2.4简单运动需要注意的事项
问题1:如果将开启定时器的代码放在一个点击事件中,点击事情被多次触发,相当于开启了多个定时器,在一个时间点上有多个函数同时执行。而且timer变量是全局变量,点击一次相当于重新赋值,变量内部永远只能存最小的定时器,原来的定时器就没有了,不论怎么去定时器timer,都不能停止前面的定时器。
var oBox = document.getElementById("box"); var btn = document.getElementsByTagName("button"); var now = 0; //全局信号量 var timer = null; //存储定时器 // 开启定时器运动 btn[0].onclick = function(){ // 每点击一次,开启定时器,让元素运动 timer = setInterval(function(){ now += 10; oBox.style.left = now + "px"; },50); } // 停止定时器 btn[1].onclick = function(){ clearInterval(timer); }
解决方法:设表先关,在事件内部定义应定时器之前,关掉之前的定时器,这样每次开启事件时,都会先清除一下timer之前存的定时器,放入一个新的定时器,后面停止时只需要停止最新定时器。
// 开启定时器运动 btn[0].onclick = function(){ // 设表先关,避免多次点击按钮累加定时器,先关掉之前开启的定时器 clearInterval(timer); //当点击事件触发后,立即清除timer // 每点击一次,开启定时器,让元素运动 timer = setInterval(function(){ now += 10; oBox.style.left = now + "px"; },50); }
问题2:当盒子到终点,自己停止,但是有时候步长设置不合理,不能正好停在固定值位置。
下面方法是错误的:
var oBox = document.getElementById("box"); var nowLeft = 100; //初始值 var timer = setInterval(function(){ if(nowLeft < 600){//判断是否走到固定的位置,没走到继续,超过了停止。 nowLeft += 13; }else{ clearInterval(timer); } oBox.style.left = nowLeft + "px"; },50);
初始值100,所以盒子的运动轨迹是:100、113、126...607停止,盒子停下来的位置不是600,而是607。
解决方法:拉终停止,在定时器函数内部每次都要判断是否走到终点,走到终点先将变量值直接赋值一个终点值,然后停止定时器,拉到终点,停止定时器。
var timer = setInterval(function(){ nowLeft += 13; if(nowLeft > 600){//判断是否走到固定的位置,没走到继续,超过了停止。 nowLeft = 600; //强制拉到终点 clearInterval(timer); //停止定时器 } oBox.style.left = nowLeft + "px"; },50);
三、无缝连续滚动
3.1简单无缝滚动
原理:页面上是6个图片,编号0、1、2、3、4、5。
复制一倍在后面,长长的火车在移动:
当你赋值的后半段火车的0号头贴到了盒子的左边框的时候,那么就
瞬间移动到原点,重新执行动画:
视觉欺诈效果:连个0的位置发生了互换,所有元素一样,看不出变化。
var rolling = document.getElementById("rolling"); var unit = document.getElementById("unit"); //得到图片的数量,计算折返点,折返点就是210 * 图片数量(没复制之前的数量) var lisLength = unit.getElementsByTagName("li").length; //图片的数量 var HTML = unit.innerHTML += unit.innerHTML; //复制一倍的li var timer = null; //存储定时器 var nowLeft = 0;//初始值 //鼠标移入停止定时器 rolling.onmouseenter = function(){ clearInterval(timer); } // 离开重新开启定时器 rolling.onmouseleave = function(){ move(); } function move(){ timer = setInterval(function(){ nowLeft -= 3; //后验收,如果到了折返点,立即让left回到0的位置 if(nowLeft < -210 * lisLength){ nowLeft = 0; } unit.style.left = nowLeft + "px"; },10); } move();
3.2高级无缝滚动
简单无缝轮播,使用的一些标签都是手动复制的,而且一些数值都是确定的值,如果一个标签发生变化,需要改的地方很多。程序耦合性太强,不能多个情况使用同一段js代码。
改善:
①HTML结构中重复的代码,用js动态添加。
②折返点:不用计算,通过页面加载效果自动获取宽度,折返点的宽度应该等于ul内部所有元素宽度的一半。
方法:li不要添加宽度,浮动元素被img自动撑宽,ul也不加宽度,绝对定位的元素用内部的li元素撑宽。
下面的红箭头的长度,就是折返点的数值:
解决方法有两个:
方法1:遍历前半部分(复制一倍之前)所有的li,进行宽度累加,累加之后就是折返点。
上午学的offsetWidth,这个方法不带margin。所以累加的时候,需要得到计算后的margin十分麻烦。所以不考虑方法1。
方法2:折返点就是假火车第1张图的offsetLeft值。所以,如果原来的li个数是lilength,那么假火车的第1张图就是lis[length]
Chrome、火狐、IE10开始,不等图片加载完毕就执行代码,所以轮播图的li都没有宽度,li浮动了,浮动的父元素需要被子元素撑开宽高,图片有多宽li就有多宽。
Chrome运行的时候,图片没有加载到,js就急着读取offsetLeft值,如何解决?
解决方法:
1、如需图片撑开元素宽度,保证图片是加载完毕,将所有代码写在window.onload事件中。
2、图片加载事件image.onload
var rolling = document.getElementById("rolling"); var unit = document.getElementById("unit"); //得到图片的数量,计算折返点,折返点就是210 * 图片数量(没复制之前的数量) var zhefandian; //折返点,图片原来的数量 var HTML = unit.innerHTML += unit.innerHTML; //复制一倍的li var lis = unit.getElementsByTagName("li"); //得到li元素 var imgs = document.getElementsByTagName('img'); //获取所有图片 var lisLength = lis.length;//图片的数量 // 判断图片是否加载完毕,如果加载完毕再计算offsetLeft值 // 计算折返点,每个li宽度不同,所以家火车开头元素的offsetLeft就是折返点,这个元素lis[lisLength / // 但是由于浏览器执行代码不等图片加载完,所以要保证图片加载完后读取。 var count = 0; //累加图片个数 for(var i = 0; i < imgs.length;i++){ imgs[i].onload = function(){ count++; //如果加载成功累加1 if(count == imgs.length){ // 加载完毕得到折返点 zhefandian = lis[lisLength / 2].offsetLeft; move(); //所有图片加载完毕再开始运动 } } } var timer = null; //存储定时器 var nowLeft = 0;//初始值 //鼠标移入停止定时器 rolling.onmouseenter = function(){ clearInterval(timer); } // 离开重新开启定时器 rolling.onmouseleave = function(){ move(); } function move(){ timer = setInterval(function(){ nowLeft -= 3; //后验收,如果到了折返点,立即让left回到0的位置 if(nowLeft < -zhefandian){ nowLeft = 0; } unit.style.left = nowLeft + "px"; },10); }
四、JSON
4.1 最简单的JSON示例
JSON叫做JavaScript Object Notation, JavaScript对象表示法。由JS大牛Douglas发明。
类似数组,内部也可以存放多条数据,数组只能通过下标获取某一项,有时不方便使用,json对象每一项数据都有自己的属性名和属性值,通过属性名可以调用属性值。
JSON对象是引用类型值,所有是存储内存地址。
之前学习过的数组:
var arr = ["东风","西风","南风","北风"] |
数组很好用,arr[0]就是南风。但是发现,数组的下标,只能是阿拉伯数字,不能是我们任意取的。
语法:
{ "k" : v, "k" : v } |
var obj = { "name":"小黑", "age":18, "sex":"不详", "height":190 } console.log(typeof obj); console.log(obj); console.log(obj.age); //18 console.log(obj["age"]);//18
调用某一项数据:
1、通过obj变量名打“点”调用对应属性的属性名
console.log(obj.age); |
2、将属性名的字符串格式放在[]进行调用
console.log(obj["age"]); |
更改obj对象的某一项属性:就是调用属性名,通过“=”赋值
obj.sex = "男"; |
4.2 JSON的嵌套
JSON里面的v,可以是任意类型的值
var obj = { "name":"黄晓明", "age":38, "sex":"不详", "height":160, "cp" :{ "name" : "Angelababy", "age" :16, "height":168 } } // 所以想得到cp的age,以下写法都可以: console.log(obj) console.log(obj.cp) console.log(obj.cp.age); console.log(obj.cp["age"]); console.log(obj["cp"]["age"]);
4.3 JSON的添加和删除
如果想增加obj里面的项,那么就用“点”语法赋值:
var obj = { "name":"黄晓明", "age":38, "height":160, "cp" :"杨颖" } obj.age++; //改变属性值 obj.sex = "刚变性完"; //增加属性 console.log(obj); delete obj.cp; //删除obj的cp属性 console.log(obj);
新增属性:添加新的属性,就用JSON对象的变量打点添加新的属性名,等号赋值。
obj.cp = { "name" : "Angelababy", "age" :16, "height":168 } console.log(obj)
删除某一个属性,使用delete关键字
delete obj.cp; |
4.4 JSON的遍历
for..in循环语句,用于遍历数组或者JSON对象的属性(对数组或者JSON对象的属性进行循环操作)。
for循环根据对象的属性名,从第一个开始进行遍历,直到遍历到最后一个属性,循环结束。
语法:
for(变量 in 对象){
} |
遍历到最后一项,循环结束。k会依次等价于obj里面的属性名,在循环语句里,用obj[k]来读取属性值。
var obj = { "name":"黄晓明", "age":38, "sex":"不详", "height":160, "cp" :{ "name" : "Angelababy", "age" :16, "height":168 } } for(var k in obj){ console.log(k +"的值是:"+ obj[k]) }
创建一个新的JSON,属性名和属性值与原有旧的JSON完全相同,要求不是指向同一个内存地址。
不能直接用旧json的一个变量直接赋值给新的变量,否则就指向同一个内存地址。
方法:创建一个新的sjon,内部数据为空,通过循环遍历旧的JSON,得到所有的属性名添加给新的JSON,然后给新的属性赋值。
var obj1 = { "name":"黄晓明", "age":38, "sex":"不详", "height":160, "cp" :{ "name" : "Angelababy", "age" :16, "height":168 } } // var obj2 = obj1; //这样会指向同一个内存地址,修改其中一个,两个变量的值都会改变 // console.log(obj1 == obj2); var obj2 = {} //创建新的json对象,内存地址就不一样了 // 遍历旧的json,获取所有的属性名和属性值 // 等号左侧,给obj2的JSON添加属性 // 等号右侧,将旧JSON属性取出来,赋值给新JSON对象的属性 for(var k in obj1){ obj2[k] = obj1[k]; console.log(obj2) } console.log(obj1 == obj2);//结果false,所以修改obj1或2都不会互相影响
五、同步异步和回调函数
5.1同步和异步
同步:synchronous
程序从上到下执行:
console.log(1); console.log(2); console.log(3); console.log(4); |
假如程序中有for循环,非常耗时间,但是浏览器会用“同步”的方式运行:
console.log(1); console.log(2); console.log(3); for(var i = 0;i < 1000;i++){ console.log("★"); } console.log(4);
同步的意思:for、while循环等很耗费时间,但是程序就傻等,等到1000个星星循环执行完毕,然后输出4。
比如用洗衣机洗衣服,需要等很长时间,等待的过程就是傻等,不同时做别的事情。
异步:Asynchronous
console.log(1); console.log(2); console.log(3); setInterval(function(){ console.log("★"); },100); console.log(4);
出输4,提前执行了,然后输出星星
“异步”的意思:遇见一个特别耗费时间的事情,程序不会傻等,而是先执行后面的代码,再回头执行异步的依据。
比如用洗衣机洗衣服,需要等很长时间,等待的过程就是,可以做别的事情,比如扫地、做饭。
JS中的异步语句:setInterval、setTimeout、Ajax、Nodejs都是异步的。
如果有异步语句,那么一定的是通过“异步”的方式执行代码,如果没有异步语句,就是同步方式执行。
5.2回调函数
异步的事情做完了,我们想继续做点什么事情,此时怎么办?
回调函数:异步的语句做完后的事情。
var count = 0; var timer = setInterval(function(){ count++; //累加 // console.log(count); console.log("★"); if(count == 300){ clearInterval(timer); callback(); //回调函数,等异步语句结束后,执行函数 } },10); function callback(){ alert("所有星星输出完毕"); document.body.style.backgroundColor = "#000"; }
六、函数节流
6.1 setTimeout()方法
var oBox = document.getElementById("box"); var oTip = document.getElementById("tip"); oBox.onmouseenter = function(){ oTip.style.display = "block"; //鼠标移入显示 } oBox.onmouseleave = function(){ setTimeout(function(){ oTip.style.display = "none"; //鼠标移出,延迟1秒隐藏 },1000); }
6.2函数节流
所谓的函数节流,就是我们希望一些函数不要连续的触发,甚至于规定,触发这个函数的最小间隔时间。
这个就是“函数节流”。
var lock = true; //开锁 btn.onclick = function(){ // 检测锁的开关情况,如果是false就执行return(后面的代码都不会执行),不执行这个事件 if(lock == false){ return; } lock = false; //上锁 console.log(Math.random()); setTimeout(function(){ lock = true; //2000毫秒后开锁 },2000); }
优化写法:
btn.onclick = function(){ //检测锁开关情况,如果是false就执行return(后面代码都不会执行),不执行这个事件 if(!lock){ return; } lock = false; //关掉锁 console.log(Math.random()); setTimeout(function(){ lock = true; //2000毫秒之后再开锁 },2000); }
七、call和apply函数
探讨普通函数中是否也有this关键字,发现普通函数的this的指向是window
普通函数中this指向window对象。
控制函数内部的this指向:
函数都可以打点调用call()和apply()方法,这两个方法可以帮我们指定函数内部的this指向谁。在函数调用过程使用这两种方法。
var oBox = document.getElementById('box'); function fun(){ console.log(this); } // 两个作用 // 1、执行fun函数 // 2、在fun函数内部指定this指向div fun.call(oBox); fun.apply(oBox);
var oBox = document.getElementById('box'); function fun(a,b){ this.style.backgroundColor = "pink"; console.log(a,b); } fun.call(oBox,10,20); fun.apply(oBox,[10,20]);
说白了,call、apply功能是引用的,都是让函数调用,并且给函数设置this指向谁。
区别:函数传递参数的语法。
fun.call(oBox,10,20,30,40,50); fun.apply(oBox,[10,20,30,40,50]); |
call需要用逗号隔开罗列所有参数
apply是把所有参数写在数组中,即使只有一个参数,也必须写在数组中。
var obj = { "name":"小黑", "age" : 18, "sex" :"不详" } function showInfo(){ console.log(this.name); } showInfo.call(obj);