IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> 执行上下文 -> 正文阅读

[游戏开发]执行上下文

执行上下文

在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) // 100001

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";//leak成为一个全局变量,不会被回收
}

2)闭包引起的内存泄露

function bindEvent(){
  var obj=document.createElement("XXX");
  obj.οnclick=function(){
    //Even if it's a empty function
  }
}

闭包可以维持函数内局部变量,使其得不到释放。 上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调的引用外暴了,形成了闭包。
解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。

//将事件处理函数定义在外部
function onclickHandler(){
  //do something
}
function bindEvent(){
  var obj=document.createElement("XXX");
  obj.οnclick=onclickHandler;
}


//在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent(){
  var obj=document.createElement("XXX");
  obj.οnclick=function(){
    //Even if it's a empty function
  }
  obj=null;
}

闭包:状态保存
函数执行时会形成一个私有作用域,通常情况下当函数执行完成,栈内存会自动释放(垃圾回收)

但是如果函数执行完成,当前私有作用域(栈内存)中的某一部分内容被内存以外的其它东西(变量/元素的事件)占用了,那么当前的栈内存就不会释放掉,也就形成了不销毁的私有作用域(里面的私有变量也不会销毁)

通常, 函数的作用域及其所有变量都会在函数执行结束后被销毁。
但是, 在创建了一个闭包以后, 这个函数的作用域就会一直保存到闭包不存在为止。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

通过闭包,start的状态被保留了,闭包(上例的inc)用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。

只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取,所以闭包inc使得函数createIncrementor的内部环境,一直存在

function a() {
	var num = 0;
	function b() {
		console.log(++num);
	};
	return b;
};
var c = a();
c() // 1 
c() //2
因为a函数被存进了变量c 他是全局变量,不会被垃圾回收机制回收,
下次再调用C()的时候c的值在内存中保存,所以每次都是在原有的基础上加一
释放对闭包的引用 c = null

function two() {
	var a = 1;
	return function() {
		a++;
		console.log(a);
	};
};

two() //2
two() //2
这个函数执行完 就销毁了,下次在调用的时候又是一次新的调用跟一起没有关系,所以无论调多少次都是2

闭包随处可见,一个 Ajax 请求的成功回调,一个事件绑定的回调方法,一个 setTimeout 的延时回调,或者一个函数内部返回另一个匿名函数,这些都是闭包。

3)没有清理的DOM元素引用

// 在对象中引用DOM
var elements = {
  btn: document.getElementById('btn'),
}
function doSomeThing() {
  elements.btn.click()
}
 
function removeBtn() {
  // 将body中的btn移除, 也就是移除 DOM树中的btn
  document.body.removeChild(document.getElementById('button'))
  // 但是此时全局变量elements还是保留了对btn的引用, btn还是存在于内存中,不能被GC回收
}

解决方法:手动删除,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();//执行完毕之后a、b又被标记离开环境,被回收

2.引用计数

引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值(function object array)赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。

function test(){
  var a={};//a的引用次数为0
  var b=a;//a的引用次数加1,为1
  var c=a;//a的引用次数加1,为2
  var b={};//a的引用次数减1,为1
}
  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-04-06 16:21:40  更:2022-04-06 16:24:22 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/16 20:17:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码