前端点滴(JS进阶)(一)----倾尽所有
一、JavaScript 执行流程
1. 执行流程
(1)全局环境
js在运行时,首先会创建一个全局执行环境,这个全局环境就是定义一个全局对象,页面中所有的内容(不同的script中的内容) 都是这个全局对象的成员,这个全局对象是window。
window对象比较大。包含所有的浏览器对象。
注意: 平时使用的alert 也属于全局环境中的属性。
有了这个知识点,前面学习的很多内容都可以解释了:
比如window.addEventListener,说明addEventListener在运行的时候也是全局对象window对象的成员。
(2)执行流程
js在执行的时候,会按照script标签来一个一个的执行,也就是先执行第一个script标签中的内容,然后在执行第二个script标签的内容。
一个script标签中,首先会先
- 编译代码(检查语法、词法是否错误,没有错误就加载到内存中,准备执行)
- 执行代码(运行或输出结果)。
执行完毕,继续按照相同的方式执行下一个script标签的内容。
(3)错误类型
这里的错误类型指的是编译型错误和执行过程中的错误。
发生编译类型错误时,对程序执行的影响:
发生执行过程中的错误时,对程序执行的影响:
注意: 执行性错误:调用没有声明的函数。
二、JavaScript 执行上下文
1. 什么是执行上下文(EC)
执行上下文: 指当前执行环境中的变量、函数声明,参数(arguments),作用域链,this等信息。分为全局执行上下文、函数执行上下文,其区别在于全局执行上下文只有一个,函数执行上下文在每次调用函数时候会创建一个新的函数执行上下文。
javascript中,EC分为三种:
- 全局级别的代码 –– 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
- 函数级别的代码 ––当执行一个函数时,运行函数体中的代码。
- Eval的代码 –– 在Eval函数内运行的代码。
EC建立分为两个阶段:进入执行上下文(创建阶段)和执行阶段(激活/执行代码)。
1)进入上下文阶段:发生在函数调用时,但是在执行具体代码之前(比如,对函数参数进行具体化之前)
- 创建作用域链(Scope Chain)
- 创建变量,函数和参数。
- 求”this“的值。
2)执行代码阶段:
- 变量赋值
- 函数引用
- 解释/执行其他代码。
- 我们可以将EC看做是一个对象。
值得注意的是:函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文。
2. 执行上下文实例
var a = 10;
var bar = function(x){
var b = 5;
foo(x+b);
}
var foo = function(y){
var c = 5;
console.log(a+c+y);
}
bar(10);
bar(20)
执行上下文栈图示:
执行上下文存在个数:5
详情可以参考:
https://segmentfault.com/a/1190000014876534#item-9
https://www.jb51.net/article/75032.htm
https://www.jianshu.com/p/0d2fb2f2f52c
http://caibaojian.com/js-call-stack.html
讲述 eval 与执行上下文情况,可参考论坛:
https://bbs.csdn.net/topics/390964361
三、JavaScript定时器
1. 什么是定时器
定时器可以完成定时作用,可以让一段代码延迟一段时间然后再执行。
很多地方都会用到定时器,比如用JS写一个会走的钟表;比如轮播图;
JS定时器有两种:(常用)
- 指定一段时间,然后在该时间段之后只执行一次代码,特点是只执行一次。
var t = window.setTimeout()
- 指定一段时间,然后每隔一段时间执行一次代码,也就是间隔一段时间执行一次,一直重复。
var t = window.setInterval()
以及其他定时器:
- 在浏览器完全结束当前运行的操作之后立即执行指定的函数(仅IE10和Node 0.10+中有实现),类似setTimeout(func, 0)
var t = window.setImmediate()
- 专门为实现高性能的帧动画而设计的API,但是不能指定延迟时间,而是根据浏览器的刷新频率而定(帧)
var t = window.requestAnimationFrame()
中止定时器:
clearTimeout(定时器变量名);
clearInterval(定时器变量名);
注意: 这些方法都是定义在window对象上面的,因此我们写window.setInterval和解setInterval的意思是一样的,前面的window可以省略掉。(因此注意:所有定时器中的 this 指向 window 对象)
实例:
var obj = {
msg: 'obj',
tip: function () {
alert(this.msg);
},
timer: function() {
setTimeout(function () {
this.tip();
}, 0);
}
};
obj.timer(); //=> "TypeError: this.tip is not a function
原因:this关键字会指向window (或全局)对象,window对象中并不存在tip方法,所以就会报错。
解决方法:(不要在定时器中使用this指向)
var obj = {
msg: 'obj',
tip: function () {
alert(this.msg);
},
timer: function() {
var timeThis = this; // 将this赋值给timeThis ,this指向obj对象
setTimeout(function () {
timeThis.tip();
}, 0);
}
};
obj.timer();
实例二:
function User(login) {
this.login = login;
this.sayHi = function() {
console.log(this.login);
}
}
var user = new User('yaodao');
setTimeout(user.sayHi, 1000); // undefined
原因如上,解决办法如下:
- 直接在定时器中调用函数,得到返回值
setTimeout(user.sayHi(), 1000); //yaodao
- 使用bind方法,将绑定sayHi绑定在user上面。(类似方法还有apply,call)改变this指向。
setTimeout(user.sayHi.bind(user), 1000); //yaodao
此外,setInterval这个定时器的功能是每过一段时间,就把我们想要执行的函数放到js的执行队列中等待执行。因为执行队列不一定是空的,需要等执行队列中的所有任务都执行完之后才会执行我们的函数,因此这个函数执行的时间也会有细微的差别。
这个方法的语法是:
window.setInterval(function () {}, 1000);
第一个参数是我们要执行的函数,第二个参数是每过多长时间把函数放入执行队列。
这里要说明的是,第一个参数的那个函数,不能带有参数。其次,里面的this默认指向window,因为前面提到过,谁调用方法,方法里面的this就指向谁,setInterval其实前面省略了window,因此里面的this默认一定指向window,不论这个setInterval是否是一个对象的方法。
setInterval其实很消耗内存,这个定时器一旦执行,就不会终止,因此需要我们的内核一直监听这个函数。
这个时候我们就需要一个方法来清除定时器了:clearInterval();
。
定时器其实会返回一个标记,我们可以通过定时器的这个标记来清除掉相对应的定时器。(或者将标记赋值给一个变量,通过变量找到定时器)
特别强调: 使用setInterval() ,就一定要使用clearInterval();
2. 定时器语法
(1)setTimeout ()
语法: var t = window.setTimeout(fn | code,delay,param)
定义与说明:
指定一段时间,然后在该时间段之后只执行一次代码,特点是只执行一次
- fn:延迟后执行的函数(回调函数)。
- code:延迟后执行的代码字符串,不推荐使用原理类似eval()。
- delay:延迟的时间(单位:毫秒),默认值为0
- param:向延迟函数传递额外的参数,IE9以上支持
实例:
<script type="text/javascript">
console.log(0);
var t = setTimeout(function(){console.log(1)},200);
window.setTimeout(function(){
clearTimeout(t);
console.log(2);
},100);
var t2 = setInterval(function(){
clearInterval(t2);
console.log(3)
},200)
console.log(4);
//=> 输出 0,4,2,3
</script>
注意: 0 最先输出说明JavaScript执行流程不等待,涉及js运行机制。
拓展:
var t = window.setTimeout(fn | code,delay,param)
的第三个参数问题。
param:向延迟函数传递额外的参数(明确他是一个实参)。
首先,来看一个例子:
for(var i = 0; i<6; i++){
setTimeout(function(){
console.log(i);
},1000);
}
//=> 输出 666666,而不是012345
原因: 再次出现 i 丢失问题。因为setTimeout是一个异步操作,而等到执行setTimeout时,for循环已经执行完毕,这时的i已经等于6,所以输出6次的6。
本质上的解释: 因为JS是单线程的,且定时器的回调将在等待当前正在执行的任务完成后才执行,所以当 i 等于 0 时,生成一个定时器,将回调插入到事件队列中,等待当前队列中无任务执行时立即执行,而此时for循环正在执行,所以回调被搁置。当for循环执行完成后,队列中存在着6个回调函数,他们的都将执行console.log(i)的操作,因为当前JS代码上中并没有使用块级作用域,所以i的值在for循环结束后一直为6,所以代码将输出6个6
解决方法:
- 立即调用模式(闭包)
for(var i=0; i<6; i++){
(function(j){
setTimeout(function(){
console.log(j);
},j*1000);
})(i);
}
//=> 输出0,1,2,3,4,5
function callback(i){
setTimeout(function(){
console.log(i);
},i*1000);
};
function click(callback){
for(var i=0; i<6; i++){
callback(i);
};
};
click(callback);
//=> 输出0,1,2,3,4,5
- setTimeout () 的 param 参数
for(var i = 0; i<6; i++){
setTimeout(function(j){
console.log(j);
},i*1000,i);
}
//=> 输出0,1,2,3,4,5
- 不是方法的方法,跳过定时器
for (var i = 0; i < 6; i++) {
setTimeout((function(i) {
console.log(i);
})(i), i * 1000);
}
//=> 输出0,1,2,3,4,5
此外,再看一个例子:
/* 第三个参数作为函数 */
var i=1;
setTimeout(function(){
console.log('第二次'+i)
},2000,setTimeout(function(){
console.log('第一次'+i);
i++;
},1000));
// 输出
// 第一次1
// 第二次2
(2)setInterval ()
语法: var t = window.setInterval(fn|code,delay)
定义与说明:
指定一段时间,然后每隔一段时间执行一次代码,也就是间隔一段时间执行一次,一直重复
- fn:延迟后执行的函数(回调函数)。
- code:延迟后执行的代码字符串,不推荐使用原理类似eval()。
- delay:延迟的时间(单位:毫秒),默认值为0
注意: 使用setInterval() ,就一定要使用clearInterval();不然会一直消耗内存直至浏览器卡死。
实例:
/* 每一秒输出 1 ,十秒后清除定时器 */
var t = setInterval(function(){
console.log(1);
},1000);
setTimeout(function({
clearInterval(t)
},9000)
拓展:
setInterval 和 setTimeout的区别?
- 先来看看,两种定时器同时存在会发生什么
setTimeout(function () {
console.log('setTimeout');
}, 1000);
setInterval(function () {
console.log('setInterval')
}, 1000);
会发现一秒过后同时出现"setTimeout"以及"setInterval",之后"setTimeout"仅输出一次,而"setInterval"在往后每一秒都会输出。
区别一: 执行次数上有区别,setTimeout一次,setInterval 无限次(直至clearInterval())。
- 再来看看,通过区别一修改setTimeout使其具有setInterval 的特性,再与setInterval 进行执行性能方面的比较:
var callback = function () {
console.log(times);
if (times++ > max) {
clearTimeout(timeoutId);
clearInterval(intervalId);
}
console.log('start', Date.now() - start);
for (var i = 0; i < 990000000; i++) {/* 模拟运行代码 */}
console.log('end', Date.now() - start);
},
/* 定义全局变量 */
delay = 100,
times = 0,
max = 5,
/* 保存最初时间 */
start = Date.now(),
intervalId, timeoutId;
/* 使用setTimeout 模拟 setInterval ,制造setTimeout 多次执行.*/
function imitateInterval(fn, delay) {
timeoutId = setTimeout(function () {
fn();
/* 重复多次 */
if (times <= max) {
imitateInterval(fn ,delay);
}
}, delay);
}
imitateInterval(callback, delay);
intervalId = setInterval(callback, delay);
先来看看:imitateInterval(callback, delay); 使用setTimeout模拟setInterval的模拟器。
再来看看,setInterval(callback, delay)。
区别二: setTimeout只有在回调完成之后才会去调用下一次定时器,而setInterval则不管回调函数的执行情况,当到达规定时间就会在事件队列中插入一个执行回调的事件。
(3)setImmediate ()
该方法用来把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的其他语句后,就立刻执行这个回调函数,即:先支持浏览器的同步代码,同步代码执行完成后立即执行setImmediate。(仅IE10和Node 0.10+中有实现)
代码:
/* 比较setImmediate()与setTimeout(fn,0)速度 */
此处存在一个疑惑?
setImmediate()在浏览器完全结束当前运行的操作之后立即执行指定的函数;setTimeout() 的delay参数为0,立即执行。两个都是立即执行,为什么先执行setImmediate()中的函数而不是setTimeout()的呢?
原因就是:
0毫秒实际上达不到的。根据HTML5标准,setTimeOut推迟执行的时间,最少是4毫秒。如果小于这个值,会被自动增加到4。这是为了防止多个setTimeout(f,0)语句连续执行或者与setImmediate(f)冲突,造成性能问题。
HTML5标准截图:
所以,setImmediate() 中的函数最先执行。
(4)requestAnimationFrame ()
requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。
设置这个API的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。
requestAnimationFrame的优势,在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。
不过有一点需要注意,requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。
requestAnimationFrame使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。
语法:
requestID = window.requestAnimationFrame(callback);
目前,主要浏览器Firefox 23 / IE 10 / Chrome / Safari)都支持这个方法。
实例: (滑动特效)
HTML
<div id="..."><!- SomeElementYouWantToAnimate -></div>
CSS
#id{
position: absolute;
left: 0px;
width: 100px;
height: 100px;
background-color: green;
}
JavaScript
/* 重复滑动 */
var elem = document.getElementById("anim");
var startTime = undefined;
function render(time) {
if (time === undefined)
time = Date.now();
if (startTime === undefined)
startTime = time;
elem.style.left = ((time - startTime)/10 % 500) + "px";
};
elem.onclick = function() {
(function animloop(){
render();
requestAnimationFrame(animloop);
})();
};
/* 单次滑动 */
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 500) + 'px';
if (progress < 5000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);