JavaScript作用域及内存理解

JavaScript作用域

作用域概念:

作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突。

--摘自百度百科。

JavaScript两种作用域:

函数作用域、全局作用域。

函数拥有自己的作用域,而块(如while、if和for语句)则没有。

JavaScript中变量作用域的工作方式:

在js中,所有全局变量实际上是作为window对象的属性存在的。

 

//设置全局变量f1;

var f1 = '1';

//在if块中

if (true) {

//再次设置f1

var f1 = '2';

//由于块没有作用域,此时f1位于全局作用域

}

//查看f1的值:2

f1;

//创建函数修改f1的值

function modify(){

var f1= '3';

console.log('该函数作用域中,f1='+f1);

}

//查看函数作用域f1的值:3

modify();

//查看全局作用域f1的值:2

console.log('全局作用域中,f1='+f1);

 

 

作用域链:

js作用域内部可以访问外部,但外部的不能访问内部的:如果函数体内还包含着函数,只有这个内函数才可以访问外一层的函数的变量;  

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数;  

 

var box = 'blue';  

function setBox(){  

function setColor(){  

var b = 'orange';  

console.log(box);  

console.log(b);  

}

//setColor()的执行环境在setBox()内;  

setColor();   

}  

setBox();

 

var a=10;

function aaa(){

 console.log(a);

};            

function bbb(){

var a=20;

aaa();

}

bbb(); 

//结果为10,因为aaa()函数不能访问到bbb()里面的局部变量,所以访问到的是a=10,这个全局变量。

 

 

js隐式全局变量声明:

当变量没有没有明确声明作用域时(使用var),他会被定义为全局作用域(即使他在函数中声明时)

 

//定义函数,设置变量

function test1(){

f2 = 'aa';

}

//调用函数

test1();

//查看f2的值

console.log(window.f2);

 

 

所以尽量在希望的作用域使用var来初始化变量,避免出现不需要的全局变量

 

变量提升:

看一个代码:3-1

 

      var scope = "global";//声明全局变量

      function fn(){

        console.log(scope);         

var scope = "local";//声明局部变量

        console.log(scope);

      }

      fn();

 

函数中所有变量声明都会提升到作用域顶部。3-2

 

function test3(){

f3 = 'bb';

//变量f3此时并没有声明;

console.log(f3);

var f3

}

//执行函数;

test3();//bb

 

f3虽然在函数test3()最底部声明,但会提升到test3()顶部。f3就成了test3作用域内的变量。函数声明和变量声明都会被解释器"提升"到方法体(函数和全局)的最顶部。

变量提升不包括初始值:3-3

 

function test4(){

//f4虽然在后面声明了,但初始化值并没有提升

console.log(f4);

//声明变量并初始化

var f4 = 'dd';

//现在才有初始化值

console.log(f4);

}

//执行函数test4();

test4();

 

所以3-1代码最终是按照这个来执行的:3-4

      var scope = "global";

      function fn(){

        var scope;//提前声明了局部变量

        console.log(scope);         

scope = "local";

        console.log(scope);      }

      fn();

 

 

总结:js有两种作用域;隐式全局变量声明;变量提升;

 

作用域链:

每个函数在第一次被调用时,会创建一个执行环境,随之创建作用域链、变量对象。

执行环境Execution Context

每个函数在调用时都会创建并进入自己的一个执行环境,而执行环境会被压入一个逻辑上的环境栈。在函数执行后,环境栈将其环境弹出,把控制权返回给之前的执行环境。

 当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。

 

      var scope = "global";

      function fn1(){

         return scope;

      }

      function fn2(){

         return scope;

      }

      fn1();

      fn2();

 

 

全局执行环境是最外围的一个执行环境。当浏览器第一次加载js脚本程序的时候, 默认进入全局执行环境, 此次的全局环境变量对象为window,因此所有全局变量和函数都是作为window对象的属性和方法创建的。

 

变量对象Variable Object:

每个执行环境会关联一个变量对象。该执行环境中定义的所有变量和函数都存放在这个变量对象中,对于全局执行环境,变量对象为window。对于函数,变量对象叫做活动对象,此时变量对象是不可通过代码来访问的。

 

 

作用域链:

作用域会被链赋值给执行环境中一个特殊的内部属性[scope]。作用域链中保存的是所有执行环境的变量对象引用列表,而当前执行环境的变量对象引用总是在最前面的,然后是外部函数一层层执行环境的变量对象,最后是全局变量对象的引用。

 

 

可以看到在执行函数fn1时,首先进入fn1的执行环境,此时需要返回scope的值,于是沿着作用域链从头到尾开始寻找scope,在自己的变量对象中并没有发现scope,于是来到第二个变量对象中找,就在全局变量中找到了属性scope的值。

 

执行环境的创建

分为两个阶段:进入阶段(解析阶段)和执行阶段。

当解析器进入执行环境时,变量对象就会添加执行环境中声明的变量和函数作为它的属性,变量值为undefined,这就是变量和函数声明提升(Hoisting)的原因,与此同时作用域链和this确定,此过程为解析阶段。然后解析器开始执行代码,为变量添加相应值的引用,得到执行结果,此过程为执行阶段。

(1)解析阶段:发生在函数调用时,但在执行具体代码之前。具体完成创建作用域链;创建变量、函数和参数以及this的值。

(2)执行阶段:主要完成变量赋值、函数引用和解释/执行其他代码 。

 

例1:在全局环境中有如下代码:

var a=123;

var b="abc";

function c(){

    alert('11');

}

解析器在进入该全局环境时有以下两个阶段:

                                 

例2:某函数有如下代码;

function testFn(a){

  var b="123";

  function c(){

    alert("abc");

  }

}  

testFn(10);

 

当解析器进入函数执行环境时,则会创建一个活动对象作为变量对象,活动对象还会创建一个Arguments对象,arguments对象是一个参数集合,用来保存参数。

 

作用域链特性原理:

全局执行环境中有如下代码:

var a='123';

function testFn(b){

       var c='abc'; 

       function testFn2(){

        var d='efg';

        console.log(a);

       } 

       testFn2();

}

testFn(10);

 

 

由于作用域链的特性,testFn2可以访问到全局执行环境中的变量a.

其过程如下;

创建全局执行环境-->创建testFn执行环境-->创建testFn2执行环境

 

当解析器进入testFn2函数执行环境时,函数内部属性[[scope]]首先填入父级的作用域链,然后再将当前的testFn2活动对象添加到作用域链的前端,形成一个新的作用域链。

testFn2调用变量a时,首先在当前的testFn2活动对象中查找,如果没有找到就顺着作用域链向上,在testFn活动对象中查找变量a,如果没有找到再顺着作用域链向上查找,直到在最后Global对象中找到为止,否则报错。所以函数内部可以调用外部环境的变量,外部环境不能调用函数内部的变量,这就是作用域特性的原理。

 

 

JavaScript内存空间

JavaScript没有严格意义上区分栈内存堆内存。首先我们可以通俗的认为JavaScript所有数据都是保存在堆内存中,但在某些时候我们又要用到堆栈数据结构的思路。

比如刚才说到的执行环境,逻辑上说它就实现了栈这样的结构。同样对于刚才讲的作用域链,它也总是把当前环境的活动对象添加到头部,可以说也是一种栈结构。

引用和值:

JavaScript保存数据采用引用和值来保存数据。对于字符串、数字、布尔值、null、undefined这样的数据我们称之为原始值(基础数据类型)。使用他们的时候,都是将原始值直接复制到变量中。

而对于没有保存原始值的变量,都是保存的对象的引用(内存位置)。实际的对象(数组、日期等)被称为指称对象。

前面说到,JavaScript的建立执行环境时,会创建一个叫做变量对象的特殊对象,JavaScript的基础数据类型往往都会保存在变量对象中,而引用对象会保存在堆内存中。

var a1 = 0; // 变量对象

var a2 = 'this is string'; // 变量对象

var a3 = null; // 变量对象

var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中

var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中

 

因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量对象中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。

理解内存中基础数据和引用对象:看以下几个例子

//将item设置为新的字符串对象

var item = "test";

//将ref指向同一个字符串对象

var ref = item;

//将item拼接为新的字符串

item += "ing";

//输出item和ref

console.log(item);

console.log(ref);

可以看到两者输出并不一样:为什么呢?


原因在于,字符串test是原始值,使用var ref = item 时,实际上是将test的值直接复制给了变量ref,此时的ref已和item是两个相互独立不影响的变量了。

//创建一个数组

var items = ['1','2','3'];

//创建数组的引用

var ref = items;

//在原数组中添加元素

items.push('4');

//输出两数组

console.log(items);

console.log(ref);

这里的数组是指称对象,当使用var ref = items给ref赋值时,实际上是把数组的引用复制给了ref,所以ref指向的仍是['1','2','3']这个值。同时,数组是自修改对象,当使用push添加元素时,实际上是改变了自己的值,并不会像字符串拼接那样会产生新的对象。所以一旦修改items指称对象的值,也就修改了ref指称对象的值。

 

另外注意:引用只能指向指称目标,不能指向另一个引用。

//创建一个数组

var items = ['1','2','3'];

//创建数组的引用

var ref = items;

//改变items的引用

items = ['4','5','6'];

//输出两数组

console.log(items);

console.log(ref);

items和ref现在指向了不同的两个数组。

JavaScript内存回收:

js是具有自动垃圾收集机制的,这种机制会自动的跟踪每一个变量的动向,并判断当前的变量是否还有存在的必要,然后将不必要的变量所占用的内存进行收回。对于这样的收回机制,实际真正的运用起来是有两种不同的方法:

1.方法一:标记清除算法

这一算法是为进入环境中的变量标记一个“进入环境”的标记。逻辑上讲,当我们的变量进入环境的时,变量实际上是不应该被删除的,因为上下文中可能会用到当前的变量进行相关的逻辑演算,而当变量离开环境的时候,竟会为其标记成为“离开环境”的状态。

2.方法二:引用计数

引用对象是放在堆中的,而这一内存清理方法是对这一值的引用次数进行统计,当我们声明了一个变量,并且将引用对象的值赋值给了这一变量,则引用对象的引用计数加   一,反之当我们的引用对象的相关引用变量其指向的内容发生了变化,则引用对象的引用计数减一。当引用对象的引用计数为0的时候这表明,此对象值可回收。

以上其实就是最为常用的内存回收机制,当然我们的内存回收机制是在一定的时间间隔后,自动的运行的,每次都会搜寻是否有变量可以收回,并回收内存。

JavaScript内存泄露:

全局变量:

不小心创建了全局变量,但并不需要全局。(隐式声明,this)

闭包:

闭包时,内部函数的作用域链仍然保持着对父函数活动对象的引用,但其参数和变量不会被垃圾回收机制回,常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。

 

 

 

 

猜你喜欢

转载自blog.csdn.net/qq_28181131/article/details/80012316