JavaScript 中变量、作用域和内存问题的学习

这是我学习JavaScript的第二篇文章,之前做过几年的Java开发,发现JavaScript虽然也是面向对象的语言但是确实有很多不同之处。就本篇博客,主要学习总结一下最近学习到的JavaScript的知识,其中有些是网络上的,不过对于理解JavaScript,和在工作总是会很实用的,所以总结了下来:

那么就开始吧,首先是变量

在JavaScript中变量分为两种:一种是基本类型,基本类型值在内存中占用固定大小的空间,因此被保存在栈内存中。从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本。另一种类型则是引用类型,引用类型的值是对象,保存在堆内存中,对象的引用保存在栈中。包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向对象的指针,从一个变量向另一个变量复制引用类型的值,复制的是指针,最终指向同一个对象。

那么在实际的使用中要确定一个值是那种基本类型的可以使用typeof操作符,而确定一个值是哪种引用类型则需要使用instanceof 操作符。

基本数据类型:undefined、Null、boolean、number和String

引用类型:Object、Array、Date、RegExp、Function、基本包装类型、单体内置对象(Gloabal、Math)。关于引用类型各个类型的详细使用,下次再详细描述。

JavaScript是面向对象的语言,同样支持继承,只是JavaScript支持实现继承,不支持接口继承。

JavaScript是一种非强类型的语言,不需要严格的如同Java、C等语言的声明类型然后复制,也一定要赋值声明类型的值。JavaScript有两种值类型,所以涉及到两个地方复制,一种就是复制变量值,另一个则是方法调用的时候存在参数传递赋值。基本类型是值复制,引用类型复制是对象的引用。

 

作用域

js中没有块作用域的概念。在没有var进行声明则会生成为全局变量污染全局环境。所以在实际的使用过程中,一定要记得var,js对变量的搜索是一层一层往上搜索,如果搜索到变量则停止往上搜索(所以搜索变量的层次越多肯定会小小的影响程序性能)。

延长作用域链 

虽然执行环境的类型总共只有两种--全局和局部(函数),但还是有其他办法来延长作用域链。这是因为有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。那么具体是哪写情况呢?具体就两种:

(1)、try-catch 语句的catch块。

(2)、with语句块。

这两个语句都会在作用域链的前端添加一个变量对象。对with语句来说会指定对象添加到作用域链中。对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的对象的声明。所以在try-catch 和with要慎用,with是不推荐使用的。在严格模式下,是不能使用,但是我们要对with有一些了解。用一个例子解说:

with(location){
var qs=search.substring(1);
var hostname=hostname;
var url=href;
}

 href 和hostname 都是location.hostname 和location.url

好了,稍后来说一下JS的执行环境,js的执行环境是很复杂的,我肯定也是不能全部说清楚的,下面就根据我所了解的简单总结了几个重点,后期了解到更多后,在加入到本次的博客中。

执行环境和全局执行环境、异步执行机制、事件和回调函数、Event Loop、定时器、Js垃圾收集 这六个方面。

1、执行环境和全局执行环境

执行环境有全局执行环境(也称为全局环境)和函数执行环境之分。

每次进入一个新环境,都会创建一个用于搜索变量和函数的作用域链,这些搜索向前面说的一样,是一层一层向上搜索的。

函数的的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境。

有全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据。变量的执行环境有助于确定应该何时释放内存。

2、异步执行机制

JavaScript是单线程的,也就是说同一个时间只能做一件事情。那么所有的任务需要排队执行,如果某一个任务执行很慢,则会影响整个性能,那么JavaScript又是怎么处理的呢?

JavaScript中的任务分为两种:一种是同步任务,另一种是异步任务;也就是synchronous 和asynchronous 两种任务。同步任务指的是在主线程上排队执行的任务。异步任务指的是,不进入主线程,而进入任务队列(task queue)的任务,只有“任务队列” 通知主线程,某个异步任务可执行了,该任务才会进入主线程执行。

异步执行的允许机制如下:

(1)、所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)、在主线程队列之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列中放置一个事件。

(3)、一旦“执行栈”中的所有同步任务执行完成,系统就会查询“任务队列”,有哪些事件,哪些对应的异步任务,于是结束等待状态,进入执行栈。开始执行。

(4)、主线程不断重复上面的(3)。

只要主线程空了,就会读取“任务队列”。

3、事件和回调函数

“任务队列”是一个事件的队列,IO设备、鼠标点击、页面滚动等等,完成一项任务,就在“任务队列”中添加一个事件,表示相关的异步任务可以进入“执行栈”。也就是主线程执行的队列中。主线程执行。

所谓“回调函数”(callback),就是哪些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的callback函数。

“任务队列”,要等到主线程将主线程中的执行队列执行完之后,就进入执行任务队列。但是,由于存在后文提到的“定时器”功能,主线程首先要检查一下执行时间,某些时间只有到了规定的时间,才能返回主线程。

4、Event Loop

主线程从“任务队列”中读取事件,这个过程是不断循环,所以整个的这种运行机制称为Event Loop。一个进程只能执行一个任务要能执行多个任务,需要下面三种方法:

(1)、排队。因为一个进程一次只能执行一个任务,只好等当前的任务执行完了,再执行后面的任务。

(2)、新建进程。使用fork新建一个进程。

(3)、新建线程。因为进程太耗费资源,可以使用进程进行多任务执行。

JavaScript则是排队的方式处理的,但是存在一个始终运行的Event loop来进行监控哪些事件需要触发,如果需要触发,则通知主线程执行回调函数。也就是上面描述2、异步执行机制中的 (3)、(4)中的(3)。这样主线程在任务多的时候达到任务饱和。不像多线程中那样存在多个线程的上下文和执行环境的切换的消耗。

5、定时器

了解了上面的事件回调合 任务队列,那么定时器这个异步任务事件,就能很好的理解啦,即指定某些代码在多少时间之后执行。这叫“定时器”功能,也就是定时执行的代码。定时器功能有setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别是前者指定的代码是一次执行,后者反复执行。setTimeout()的第二个参数为0,就表示当前主线程任务清空后。立即执行指定的回调函数。 这种方式可以将耗时的方法,setTimeout到任务队列中,任务队列中存在先后顺序。

6、Js垃圾回收

上面说到执行环境有助于垃圾回收,是什么意思呢?就是离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。

现在垃圾收集比较常用的都是“标记删除”,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。

还有一种“引用计数算法”,这种算法的思想是跟踪记录所有值被引用的次数。之前IE在使用,但是现在JavaScript不再使用这种算法,因为可能会出现内存泄漏的问题,具体为什么,如果代码中存在循环引用现象,将会导致泄漏。垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很乐观,那么回收的工作量也是相当大的。如果需要清理的空间大。这种情况下,确定垃圾收集的事件间隔是一个非常重要的问题,存在垃圾收集机制的语言编写代码比较方便,但是内部的机制也相对比较复杂,确保占用的内存少可以让页面得到好更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不不再有用,最后通过将其设置为null来释放引用 这种做法叫做解除引用。这一做法适用于大多数全局变量和全局变量的熟悉。局部变量会在它们离开执行环境的时候自动解除引用。解除一个值的引用并不意味着自动回收该值占用的内存,而是表示该空间可以回收。解除引用的真正的目的也是让值脱离执行环境,以便垃圾收集器下次回收其空间。

关于JavaScipt的运行机制我总结的肯定不一定准确全面,希望在今后的工作和学习中,更加了解,这样才能知其然,并知其所以然。

 参考资料

《JavaScript高级程序设计》

http://www.ruanyifeng.com/blog/2014/10/event-loop.html

 

posted on 2016-10-13 18:09  刀锋诚心  阅读(638)  评论(1编辑  收藏  举报