笔记之javascript--04--数据类型、执行环境和作用域

1JavaScript中基本数据类型和引用数据类型的区别

  基本数据类型指的是简单的数据段,引用数据类型指的是有多个值构成的对象。

1)、基本数据类型和引用数据类型

  ECMAScript包括两个不同类型的值:基本数据类型和引用数据类型。

  基本数据类型指的是简单的数据段,引用数据类型指的是有多个值构成的对象。

  当我们把变量赋值给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值。

2)、常见的基本数据类型:

  Number、String 、Boolean、Null和Undefined。基本数据类型是按值访问的,因为可以直接操作保存在变量中的实际值。示例:

  var a = 10;

  var b = a;

  b = 20;

  console.log(a); // 10值

  上面,b获取的是a值得一份拷贝,虽然,两个变量的值相等,但是两个变量保存了两个不同的基本数据类型值。

  b只是保存了a复制的一个副本。所以,b的改变,对a没有影响。

  下图演示了这种基本数据类型赋值的过程:

     

3)、引用类型数据:

  也就是对象类型Object type,比如:Object 、Array 、Function 、Data等。

     引用类型的值是保存在内存中的对象,js不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时

实际上实在操作对象的引用,而不是实际的对象,为此,引用类型的值是按引用访问的。

  javascript的引用数据类型是保存在堆内存中的对象。

  与其他语言的不同是,你不可以直接访问堆内存空间中的位置和操作堆内存空间。只能操作对象在栈内存中的引用地址。

  所以,引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象。

  var obj1 = new Object();

  var obj2 = obj1;

  obj2.name = "我有名字了";

  console.log(obj1.name); // 我有名字了

  说明这两个引用数据类型指向了同一个堆内存对象。obj1赋值给onj2,实际上这个堆内存对象在栈内存的引用地址复制了一份给了obj2,

  但是实际上他们共同指向了同一个堆内存对象。实际上改变的是堆内存对象。

  下面我们来演示这个引用数据类型赋值过程:

    

4)、总结区别

  a 声明变量时不同的内存分配: 

  1)原始值:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置

    这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈中。这样存储便于迅速查寻变量的值。

  2)引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。

     这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。

     地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。

   b 不同的内存分配机制也带来了不同的访问机制
   
  1)在javascript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,
    首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的 按引用访问
  2)而原始类型的值则是可以直接访问到的。
  
  c 复制变量时的不同
  
  1)原始值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量, 此后这两个变量是完全独立的,他们只是拥有相同的value而已。
  2)引用值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,
    也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。
    (这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了)。 多了一个指针
 
   d 参数传递的不同(把实参复制给形参的过程
  
  首先我们应该明确一点:ECMAScript中所有函数的参数都是按值来传递的。                 
  但是为什么涉及到原始类型与引用类型的值时仍然有区别呢?还不就是因为内存分配时的差别。  
  1)原始值:只是把变量里的值传递给参数,之后参数和这个变量互不影响。
  2)引用值:对象变量它里面的值是这个对象在堆内存中的内存地址,这一点你要时刻铭记在心!
    因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象。
参考文献:https://www.cnblogs.com/cxying93/p/6106469.html

              http://bosn.me/js/js-call-by-sharing/

2、怎么理解ECMAScript所有函数的参数是按值传递的。

      js中所以函数都是按值传递的,也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。


如果num是按引用传递的话,那count的值也会变成30.

如果是对象呢?


        当给obj添加name属性后,person也将有所反映,很多人认为i,在局部作用域中修改的对象会在全局作用域中反映出来,

就说明参数是按引用传递的。继续看下面的例子:


当重新给obj赋值后,在调用person.name,仍然输出jack说明即使在函数内部修改了参数的值,但原始的引用仍然保持不变。

3、执行环境及作用域:


4、执行环境及作用域:

          执行环境(execution context)是js中最为重要的一个概念,执行环境定义了变量或者函数有权访问的其他数据,决定了他们各自的行为。每当程序的执行流进入到一个可执行的代码时,就进入到了一个执行环境中。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

        全局执行环境是最外围的一个执行环境,在web浏览器中,全局执行函数被认为是window对象,因此所有全局变量和函数都作为window对象的属性和方法创建的。某个执行环境中所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。全局执行环境直到应用程序退出也随之销毁(关闭网页和浏览器的时候)。

       当执行流进入一个函数时,函数的环境就会被推入这个环境栈中,当函数执行完毕之后,栈将这个执行环境弹出,然后把控制权返回给之前的执行环境。这样实现的原因是由于 Javascript 解释器是单线程的,也就是同一时刻只能发生一件事情,其他等待执行的上下文或事件就会在这个环境栈中排队等待。值得注意的一点是:每次函数的调用都会创建一个执行环境压入栈中,无论是函数内部的函数、还是递归调用等。

        当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象在一开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样一直延续到全局执行环境;全局执行环境始终是作用域链中的最后一个对象。(引自《javascript高级编程第四章73页》)。

        作用域分为局部作用域全局作用域。有如下几种情况可归纳为全局作用域:①最外层函数和在最外层函数外面定义的变量拥有全局作用域。②所有末定义直接赋值的变量自动声明为拥有全局作用域。③所有window对象的属性拥有全局作用域。而局部作用域:是函数内部的作用域,一般只在固定的代码片段内可访问到,有时候也成为函数作用域。

        变量对象:定义着执行上下文的所有变量函数以及执行上下文函数的参数列表。也就是说变量对象定义着一个函数内定义的参数列表,内部变量和内部函数。注意,函数表达式(与函数声明相对)不包含在变量对象之中。变量对象是在函数被调用,但是函数尚未执行的时候被创建的,这个创建变量对象的过程实际就是函数内数据(函数参数、内部变量、内部函数)初始化的过程。

       内部对象:未进入执行阶段之前,变量对象中的属性都不能访问,但是在进入执行阶段之后,变量对象转变为活动对象,里面的对象都能被访问了,然后开始进行执行阶段的操作。

5、执行环境的具体细节


我们同样也可以用一个对象来表示执行上下文:
ExecutionContextObj = {
    scopeChain: { 变量对象(variableObject)+ 所有父执行上下文的变量对象 },
    variableObject: { <arguments>对象,内部变量声明和函数声明 },
    this:{}
}
每当一个函数被调用的时候,就会随之创建一个执行上下文,在 Javascript 解释器内部处理执行上下文有两个步骤:
  • 第一步:创建阶段 (在函数调用之后,函数体执行之前),解释器扫描传递给函数的参数或arguments,本地函数声明和本地变量声明,并创建executionContextObj对象。扫描的结果将完成变量对象的创建
    *创建作用域链 (Scope Chain)
    • 扫描上下文中声明的形式参数、函数以及变量,并依次填充变量对象的属性

    • 函数的形参:形参作为属性,对应的实参作为值。对于没有实参的形参,值为undefined。

    • 函数声明(FunctionDeclaration FD):由函数对象创建出相应的名、值,名就是函数名、值就是函数体。如果变量对象已经包含了同名的属性,就会替换掉它的值。
    • 变量声明(VariableDeclaration):属性名是变量名,值初始化为 undefined。如果变量名和已经存在的属性同名,不会影响到同名的属性。
    • 注意:函数表达式(FunctionExpression FE)不会成为变量对象的属性,也就是说函数表达式不会影响到变量对象。

    • 求出上下文“this”的值

  • 第二步:代码执行阶段

    • 这一阶段就会给第一步中初始值为 undefined 的变量赋上相应的值
      我们来看下面这个例子:
    • (function foo(x,y,z){
  var a = 1;
  var b = function(){};
  function c(){}
  (function d(){})();

})(10,20);

函数调用后,相应的executionContextObj如下:
第一阶段
executionContextObj = {
    scopeChain:{...},
    VO: {
        arguments:{
            x:10,
            y:20,
            Z:undefined,
            length:2,//这里是实际传入参数的个数
            callee:pointer to function foo()
        }
        a:undefined,
        b:undefined,
        c:pointer to function c()
    },
    this:{...}
}

第二阶段:
executionContextObj = {
        scopeChain:{...},
  VO: {
          arguments:{
              x:10,
              y:20,
              Z:undefined,
              length:2,//这里是实际传入参数的个数
                    callee:pointer to function foo()
            }
              a:1,
              b:pointer to function b(),
              c:pointer to function c()
      },
        this:{...}
}


在第二阶段,就会为局部变量 a 、b 赋值,注意到 d 并没有在变量对象中,正如上文中提到的那样,函数表达式是不会影响变量对象的,所以在作用域中任何一个位置引用d都会出现“d is not defined”的错误。

现在你应该非常清楚JS中的变量、函数声明提升是怎么回事了吧。

举个例子吧:
(function foo(){
  console.log(typeof x);//"function"
  var x = 10;
  console.log(y);//undefined 而不是 “y is not defined” ,这就是变量声明提升!
  var y = 20;
  console.log(typeof x);//"number"
  function x(){}
})();

为什么第一次打印x的类型是函数,第二次打印x的类型又是数字呢。这是因为,根据创建上下文时的规则,函数调用之后会按照顺序依次把函数参数、函数声明、变量声明填充为VO的属性,并且填充变量声明的时候如果同名是不会造成任何影响的,x的值还是函数。
在进入上下文阶段,VO的状态:
VO = {
  x:pointer to function x()
}
//发现var x = 10;
//如果函数“x”还未定义,则 "x" 的值为undefined,
//但是,在这个例子中
//变量声明并不会影响同名的值为函数的x
VO[‘x’] 的值仍未改变
在代码执行阶段,VO的状态:
VO['x'] = 10;
这一阶段,局部变量 x 被赋值,此时之前同名的值为函数的 x 就会被覆盖,大家注意声明和赋值!!第一阶段,局部变量声明同名不会影响;第二阶段局部变量赋值就会产生影响了,毕竟人家是最后赋值的。
参考:https://blog.csdn.net/thumd_lee/article/details/53523744

6、延长作用域链:   

虽然javascript的执行环境的类型共有两种:全局和局部(函数)。不过可以通过别的方法来延长作用域链。这么说是因为有些语句儿科一在作用域的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。在两种情况下会发生这种现象,具体说就是当执行流进入下列任何一个语句时,作用域链就会得到加长。

1)try-catch语句的catch块

2)with语句

这两个语句都会在作用域的前端添加一个变量对象。对于with语句来说,会将指定的对象添加到作用域中,对于catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

7、没有块级作用域:

    在类C语言中,{}封闭起来的代码都有自己的作用域,因而支持根据条件创建变量,但是在中,if、for等语句中的变量声明会将变量添加到当前的执行环境中,对于for语句,由for语句创建的变量i即使在for循环执行结束后,也依旧会存在于循环外部的执行环境中。

8、垃圾收集:

        原文引用:https://www.cnblogs.com/dolphinX/p/3348468.html

        JavaScript有自动垃圾回收机制,也就是说执行环境会负责管理代码执行过程中使用的内存,在开发过程中就无需考虑内存分配及无用内存的回收问题了。JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

变量生命周期

        有同学看了上面就会问了,什么叫不再使用的变量?不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后再函数中使用这些变量,直至函数结束(闭包中由于内部函数的原因,外部函数并不能算是结束,了解闭包可以看看 JavaScript作用域链JavaScript 闭包究竟是什么)。

        一旦函数结束,局部变量就没有存在必要了,可以释放它们占用的内存。貌似很简单的工作,为什么会有很大开销呢?这仅仅是垃圾回收的冰山一角,就像刚刚提到的闭包,貌似函数结束了,其实还没有,垃圾回收器必须知道哪个变量有用,哪个变量没用,对于不再有用的变量打上标记,以备将来回收。用于标记无用的策略有很多,常见的有两种方式

标记清除(mark and sweep)

        这是JavaScript最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”。至于怎么标记有很多种方式,比如特殊位的反转、维护一个列表等,这些并不重要,重要的是使用什么策略,原则上讲不能够释放进入环境的变量所占的内存,它们随时可能会被调用的到。

        垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了,因为环境中的变量已经无法访问到这些变量了,然后垃圾回收器相会这些带有标记的变量机器所占空间。

引用计数(reference counting)

在低版本IE中经常会出现内存泄露,很多时候就是因为其采用引用计数方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值得引用次数减1,当这个值的引用次数变为0的时候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的空间。

看起来也不错的方式,为什么很少有浏览器采用,还会带来内存泄露问题呢?主要是因为这种方式没办法解决循环引用问题。比如对象A有一个属性指向对象B,而对象B也有有一个属性指向对象A,这样相互引用

复制代码
function test(){
            var a={};
            var b={};
            a.prop=b;
            b.prop=a;
        }
复制代码

这样a和b的引用次数都是2,即使在test()执行完成后,两个对象都已经离开环境,在标记清除的策略下是没有问题的,离开环境的就被清除,但是在引用计数策略下不行,因为这两个对象的引用次数仍然是2,不会变成0,所以其占用空间不会被清理,如果这个函数被多次调用,这样就会不断地有空间不会被回收,造成内存泄露。

在IE中虽然JavaScript对象通过标记清除的方式进行垃圾回收,但BOM与DOM对象却是通过引用计数回收垃圾的,也就是说只要涉及BOM及DOM就会出现循环引用问题。看上面的例子,有同学回觉得太弱了,谁会做这样无聊的事情,其实我们是不是就在做

window.οnlοad=function outerFunction(){
        var obj = document.getElementById("element");
        obj.onclick=function innerFunction(){};
    };

这段代码看起来没什么问题,但是obj引用了document.getElementById("element"),而document.getElementById("element")的onclick方法会引用外部环境中德变量,自然也包括obj,是不是很隐蔽啊。

解决办法

最简单的方式就是自己手工解除循环引用,比如刚才的函数可以这样

window.οnlοad=function outerFunction(){
        var obj = document.getElementById("element");
        obj.onclick=function innerFunction(){};
       obj=null;
    };

什么时候触发垃圾回收

        垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。IE6的垃圾回收是根据内存分配量运行的,当环境中存在256个变量、4096个对象、64k的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,这样按需调用不是很好吗?但是如果环境中就是有这么多变量等一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器一直在工作,这样浏览器就没法儿玩儿了。

        微软在IE7中做了调整,触发条件不再是固定的,而是动态修改的,初始值和IE6相同,如果垃圾回收器回收的内存分配量低于程序占用内存的15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把临街条件翻倍,如果回收的内存高于85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作职能了很多。







  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值