执行上下文
在js中,有三个比较场景会生成上下文对象
1、当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。 2、当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。 3、当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
作用域链增强 1.with语句
var o={href:"sssss"};
var href="1111";
function buildUrl(){
var qs="?debug=true";
with(o){
href="2222";
var url=href+qs;
}
return url;
}
var result=buildUrl();
console.log(result);
console.log(href);
with语句将o对象作为上下文。
2.try/catch语句的catch块
当try代码块中发生错误时,执行过程会跳转到catch语句,然后把异常对象推入一个可变对象并置于作用域的头部。在catch代码块内部,函数的所有局部变量将会被放在第二个作用域链对象中。示例代码:
try{
doSomething();
}catch(ex){
alert(ex.message);
}
请注意,一旦catch语句执行完毕,作用域链机会返回到之前的状态。try-catch语句在代码调试和异常处理中非常有用,因此不建议完全避免。你可以通过优化代码来减少catch语句对性能的影响。一个很好的模式是将错误委托给一个函数处理,例如
try{
doSomething();
}catch(ex){
handleError(ex);
}
优化后的代码,handleError方法是catch子句中唯一执行的代码。该函数接收异常对象作为参数,这样你可以更加灵活和统一的处理错误。由于只执行一条语句,且没有局部变量的访问,作用域链的临时改变就不会影响代码性能了。
var a = 2
function add(){
var b = 10
return a+b
}
add()
先是创建了一个 add 函数,接着在代码的最下面又调用了该函数。 那么下面我们就利用这段简单的代码来解释下函数调用的过程。 在执行到函数 add() 之前,JavaScript 引擎会为上面这段代码创建全局执行上下文,包含了声明的函数和变量,你可以参考下图: 从图中可以看出,代码中全局变量和函数都保存在全局上下文的变量环境中。 执行上下文准备好之后,便开始执行全局代码,当执行到 add 这儿时,JavaScript 判断这是一个函数调用,那么将执行以下操作:
1、首先,从全局执行上下文中,取出 add 函数代码。
2、其次,对 add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。
3、最后,执行代码,输出结果。
当执行到 add 函数的时候,我们就有了两个执行上下文了——全局执行上下文和 add 函数的执行上下文。
也就是说在执行 JavaScript 时,可能会存在多个执行上下文,那么 JavaScript 引擎是如何管理这些执行上下文的呢? 答案是通过一种叫栈的数据结构来管理的。
栈
关于栈,你可以结合这么一个贴切的例子来理解,一条单车道的单行线,一端被堵住了,而另一端入口处没有任何提示信息,堵住之后就只能后进去的车子先出来,这时这个堵住的单行线就可以被看作是一个栈容器,车子开进单行线的操作叫做入栈,车子倒出去的操作叫做出栈。
所以,栈就是类似于一端被堵住的单行线,车子类似于栈中的元素,栈中的元素满足后进先出的特点。你可以参看下图: JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。
调用栈
var a = 2
function add(b,c){
return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return a+result+d
}
addAll(3,6)
第一步,创建全局上下文,并将其压入栈底
如下图所示:
词法环境定义了由代码编译过程中,ecma规范词法对应的一些关系,比如记录函数内部的this内容,不对外暴露,可以理解为ecma内部自己的语法关系。 变量环境变量环境指的的是在词法环境中,代码运行时生成的变量关系,可以理解为由我们创建的变量。
从图中你也可以看出,变量 a、函数 add 和 addAll 都保存到了全局上下文的变量环境对象中。
全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中 a 的值设置为 2。设置后的全局上下文的状态如下图所示: 接下来,第二步是调用 addAll 函数。当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中,如下图所示: addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是 d=10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。
然后接着往下执行,第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈,如下图所示: 当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9。如下图所示: 紧接着 addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。最终如下图所示: 至此,整个 JavaScript 流程执行结束了。
我们写的代码,包括函数里的代码执行,在规范中叫可执行代码。于是,我们可以把代码的运行流程,更细致的概括为,那么执行上下文和可以执行代码会伴随在js的运行周期里: 为什么会存在变量提升这样的现象。本质上是因为js在编译过程中的词法解析阶段,就已经生成了执行上下文的关系,所以代码还没运行时候,变量的环境已经创建好了,而在代码运行时候。即使我们的执行代码是比变量更前的,依然可以拿到变量的引用,在代码运行时,上下文对象才会激活。 上下文对象生成时机在词法解析阶段,而上下文对象激活时机在运行阶段
调用栈应用
1、如何利用浏览器查看调用栈的信息
当你执行一段复杂的代码时,你可能很难从代码文件中分析其调用关系,这时候你可以在你想要查看的函数中加入断点,然后当执行到该函数时,就可以查看该函数的调用栈了。
这么说可能有点抽象,这里我们拿上面的那段代码做个演示,你可以打开“开发者工具”,点击“Source”标签,选择 JavaScript 代码的页面,然后在第 3 行加上断点,并刷新页面。你可以看到执行到 add 函数时,执行流程就暂停了,这时可以通过右边“call stack”来查看当前的调用栈的情况,如下图: 从图中可以看出,右边的“call stack”下面显示出来了函数的调用关系:栈的最底部是 anonymous,也就是全局的函数入口;中间是 addAll 函数;顶部是 add 函数。这就清晰地反映了函数的调用关系,所以在分析复杂结构代码,或者检查 Bug 时,调用栈都是非常有用的。
除了通过断点来查看调用栈,你还可以使用 console.trace() 来输出当前的函数调用关系,比如在示例代码中的 add 函数里面加上了 console.trace(),你就可以看到控制台输出的结果,如下图: 2、栈溢出(Stack Overflow)
现在你知道了调用栈是一种用来管理执行上下文的数据结构,符合后进先出的规则。不过还有一点你要注意,调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。
特别是在你写递归代码的时候,就很容易出现栈溢出的情况。比如下面这段代码:
function division(a,b){
return division(a,b)
}
console.log(division(1,2))
从上图你可以看到,抛出的错误信息为:超过了最大栈调用大小(Maximum call stack size exceeded)。
那为什么会出现这个问题呢?这是因为当 JavaScript 引擎开始执行这段代码时,它首先调用函数 division,并创建执行上下文,压入栈中;然而,这个函数是递归的,并且没有任何终止条件,所以它会一直创建新的函数执行上下文,并反复将其压入栈中,但栈是有容量限制的,超过最大数量后就会出现栈溢出的错误。
理解了栈溢出原因后,你就可以使用一些方法来避免或者解决栈溢出的问题,比如把递归调用的形式改造成其他形式,或者使用加入定时器的方法来把当前任务拆分为其他很多小任务。
测试浏览器的最大调用栈大小的限制
var i= 0
function fn(){
i++
fn()
}
try{
fn()
}catch(ex){
console.log('size=>'+i,'err=>'+ex)
}
Internet Explorer
IE6: 1130
IE7: 2553
IE8: 1475
IE9: 20678
IE10: 20677
Mozilla Firefox
3.6: 3000
4.0: 9015
5.0: 9015
6.0: 9015
7.0: 65533
17: 50762
18: 52596
19: 52458
42: 281810
Google Chrome
14: 26177
15: 26168
16: 26166
25: 25090
47: 20878
51: 41753
Safari
4: 52426
5: 65534
9: 63444
Opera
10.10: 9999
10.62: 32631
11: 32631
12: 32631
Edge
87: 13970
function sum(x,y){
if(y>0){
return sum(x+1,y-1)
}else{
return x
}
}
var a=sum(1,100000)
console.log(a)
报错内存溢出
解决方案
使用es6的蹦床函数解决递归造成的堆栈溢出
蹦床函数:结合.bind,使函数调用的时候是自己的方法,但是确是另一个函数对象,不是本身,这个时候就不会造成内存的泄露,发生堆栈溢出了,实现代码如下:
function trampoline(f){
while(f && f instanceof Function){
f=f()
}
return f
}
function sum1(x,y){
if(y>0){
return sum1.bind(null,x+1,y-1)
}else{
return x
}
}
var b=trampoline(sum1(1,100000))
console.log(b)
function foo() {
console.log(123)
foo()
}
foo()
function foo() {
console.log(123)
setTimeout(foo, 0)
}
foo()
function foo() {
console.log(123)
return Promise.resolve().then(foo)
}
foo()
只有第一段函数代码会导致栈溢出。第二段代码会正确执行,第三段代码也不会导致栈溢出但却会让整个页面卡住。
setTimeout封装的函数是一个宏任务,所以递归调用setTimeout里的foo函数时只会使得主线程不断重复的从消息队列中取出由setTimeout所产生的不同的宏任务,并且,每次执行完宏任务时都会及时退出foo函数的调用栈,所以不会导致栈溢出。
JavaScript 中引入了微任务,微任务会在当前(宏)任务执行结束之前被执行,这也就意味着在当前微任务执行结束之前,消息队列中的其他任务是不可能被执行的。
Promise.resolve()会触发微任务,解析引擎会将该微任务添加进微任务队列中,退出当前 foo函数的执行。然后,解析引擎在准备退出当前的宏任务之前,会检查微任务队列,发现微任务队列中有一个微任务,于是先执行微任务。由于这个微任务就是调用foo 函数本身,所以在执行微任务的过程中,需要继续调用 foo 函数,在执行 foo函数的过程中,又会触发了同样的微任务。那么这个循环就会一直持续下去,当前的宏任务无法退出,也就意味着消息队列中其他的宏任务是无法被执行的,比如通过鼠标、键盘所产生的事件。这些事件会一直保存在消息队列中,页面无法响应这些事件,具体的体现就是页面的卡死。同样,由于每次执行微任务时都会退出foo 函数的调用栈,所以不会导致栈溢出。
内存泄漏
1)意外的全局变量引起的内存泄露
function leak(){
leak="xxx";
}
2)闭包引起的内存泄露
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=function(){
}
}
闭包可以维持函数内局部变量,使其得不到释放。 上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调的引用外暴了,形成了闭包。
解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。
function onclickHandler(){
}
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=onclickHandler;
}
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=function(){
}
obj=null;
}
闭包:状态保存 函数执行时会形成一个私有作用域,通常情况下当函数执行完成,栈内存会自动释放(垃圾回收)
但是如果函数执行完成,当前私有作用域(栈内存)中的某一部分内容被内存以外的其它东西(变量/元素的事件)占用了,那么当前的栈内存就不会释放掉,也就形成了不销毁的私有作用域(里面的私有变量也不会销毁)
通常, 函数的作用域及其所有变量都会在函数执行结束后被销毁。 但是, 在创建了一个闭包以后, 这个函数的作用域就会一直保存到闭包不存在为止。
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc()
inc()
inc()
通过闭包,start的状态被保留了,闭包(上例的inc)用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。
只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取,所以闭包inc使得函数createIncrementor的内部环境,一直存在
function a() {
var num = 0;
function b() {
console.log(++num);
};
return b;
};
var c = a();
c()
c()
因为a函数被存进了变量c 他是全局变量,不会被垃圾回收机制回收,
下次再调用C()的时候c的值在内存中保存,所以每次都是在原有的基础上加一
释放对闭包的引用 c = null
function two() {
var a = 1;
return function() {
a++;
console.log(a);
};
};
two()
two()
这个函数执行完 就销毁了,下次在调用的时候又是一次新的调用跟一起没有关系,所以无论调多少次都是2
闭包随处可见,一个 Ajax 请求的成功回调,一个事件绑定的回调方法,一个 setTimeout 的延时回调,或者一个函数内部返回另一个匿名函数,这些都是闭包。
3)没有清理的DOM元素引用
var elements = {
btn: document.getElementById('btn'),
}
function doSomeThing() {
elements.btn.click()
}
function removeBtn() {
document.body.removeChild(document.getElementById('button'))
}
解决方法:手动删除,elements.btn = null。
4)被遗忘的定时器或者回调
var someResouce=getData();
setInterval(function(){
var node=document.getElementById('Node');
if(node){
node.innerHTML=JSON.stringify(someResouce)
}
},1000)
JavaScript垃圾回收
JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收系统(GC)会按照固定的时间间隔,周期性的执行。
到底哪个变量是没有用的?所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除和引用计数。引用计数不太常用,标记清除较为常用。
1.标记清除
js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
function test(){
var a=10;
var b=20;
}
test();
2.引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值(function object array)赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
function test(){
var a={};
var b=a;
var c=a;
var b={};
}
|