JavaScript变量、作用域与内存(笔记及抄录《JavaScript高级程序设计(第4版)》)

  • 原始值与引用值
    • js变量可以包含两种不同类型的数据:原始值和引用之。
    • 原始值就是最简单的数据:undefined、null、boolean、number、string和symbol。引用值则是由多个值构成的对象。引用值是保存在内存中的对象。
    • 动态属性
      • 原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。不过,在变量保存了这个值之后,可以对这个值做什么,则大有不同。对于引用值而言,可以随时添加、修改和删除其属性和方法。如
        let person = new Object();
        person.name = "Nicholas";
        console.log(person.name); // "Nicholas"
    • 复制值
      • 除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到信变量的位置。如:
        let num1 = 5;
        ​let num2 = num1;
      • 这里,num1包含数值5。当把num2初始化为num1时,num2也会得到数值5.这个值跟存储在num1中的5时完全独立的,因为它时num1的值的副本。如图

         

      • 在把引用值从一个变量赋值给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上时一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一对象,因此一个对象上面的变化会在另一个对象上反映出来,如:
        let obj1 = new Object();
        let obj2 = obj1;
        obj1.name = "Nicholas";
        console.log(obj2.name); // "Nicholas"
      • 变量obj1保存了一个新对象的实例。然后,这个值被复制到obj2,此时两个变量都指向了同一个对象。在给obj1创建属性name并赋值后,通过obj2也可以访问这个属性,因为它们都指向同一对象。

         

    • 传递参数
      • es中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像一个变量复制到另一个变量一样。如果原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。
      • 在按值传递参数时,值会被复制到另一个局部变量。在按引用传递参数时,值在内存中的位置会被保存在一个局部变量,这意味着对本地变量的修改会反应到函数外部。
    • 确定类型
      • 使用instanceof,用法:
        console.log(person instanceof Object); // 变量person时Object吗?
  • 执行上下文与作用域
    • 执行上下文(以下简称“上下文”)的概念在js中时颇为重要的。变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它。
    • 全局上下文是最外层的上下文。根据es实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。使用let和const的顶级声明不会定义在全局上下文,但在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
    • 每个函数调用都有自己的上下文。当代码执行流进行函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返回给之前的执行上下文。es程序的执行六就是通过这个上下文栈进行控制的。
    • 上下文中的代码在执行的时候,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文时函数,则其活动对象用作变量对象。活动对象最初只有一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上下文,再下一个对象来自在下一个包含行下文。以次类推直至全局上下文:全局上下文的变量对象始终时作用域链的最后一个变量对象。
    • 代码执行时的标识符解析时通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直至找到标识符。
    • 注意:函数参数被认为时当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则。
    • 作用域链增强
      • 虽然执行上下文要有全局上下文和函数上下文两种(evak()调用内部存在第三种上下文),但有其他方式来增强作用域链。某些语句会导致再作用域链前端临时添加一个上下文,这个上下文再代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况是:
        • try/catch语句的catch
        • with语句
      • 这两种情况下,都会在作用域链前端添加一个变量对象。对with语句来说,会向作用域链前端添加指定的对象:对catch语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。
        function buildUrl(){
        let qs = "?debug=true";

        ​ with(location){
        ​ let url = href + qs;
        ​ }

        ​ return url;
        ​}
      • 这里,with语句将location对象作为上下文,因此location会被添加到作用域链前端。buildUrl()函数中定义了一个变量qs。当with语句中的代码引用变量href时,实际上引用的时location.href,也就是自己变量对象的属性。在引用qs时,引用的则是定义在buildUrl()中的哪个变量,它定义在函数上下文的变量对象上。而在with语句中使用var声明的变量url会成为函数上下文的一部分,可以作为函数的值被返回;但向这里使用let声明的变量url,因为被限制在块级作用域,所以在with块之外没有定义。
    • 变量声明
      • 使用var的函数作用域声明
        • 在使用var声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函数的局部上下文。在with语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了,那么他就会自动被添加到全局上下文
      • 使用let的块级作用域声明
        • es6新增的let关键字跟var很相似,但它的作用域时块级的,这也是js中的新概念。块级作用域由最近的一堆包含花括号{}界定。但菊花说,if块、while块、function块,甚至连单独的块也是let声明变量的作用域。
      • 使用const的常量声明
        • 除了let,es6同时还增加了const关键字。使用const声明的变量必须同时初始化为某个值,一经声明,在其生命周期的任何时候都不能再重新赋予新值。
        • 注意:开发实践表明,如果开发流程并不会因此而受很大影响,就应该尽可能地多使用const声明,除非确实需要一个将来会重新赋值的变量。这样可以从根本上保证提前发现重新赋值导致的bug。
      • 标识符查找
        • 当再特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。搜索开始于作用域链前端,以给定的名称搜索对应的标识符。如果再局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。(注意,作用域链中的对象也有一个原型链,因此搜索可能涉及每个对象的原型链。)这个过程一直持续到搜索至全局上下文的变量对象。如果仍然没有找到标识符,则说明其未声明。
  • 垃圾回收
    • js是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。基本思路:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集事件)就会自动运行。垃圾回收过程是一个近似且不完美的方案,因为某块内存是否还有用,属于“不可判定”问题,意味着靠算法是解决不了的。
    • 我们以函数中局部变量的正常生命周期为例。函数中的局部变量会在函数执行时存在。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要哪个局部变量了,它占用的内存可以释放,供后面使用。这种情况下显然不在需要局部变量了,但并不是所有时候都会这么明显。垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。如何标记未使用的变量也许有不同的实现方式。不过,在浏览器的发展史上,用到过两种主要的标记策略:标记清理和引用计数。
    • 标记清理
      • js最常用的垃圾回收策略时标记清理。当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而不再上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。
      • 给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现并不重要,关键时策略。
      • 垃圾回收程序运行的时候,会标记内存中存储的所有变量(标记方法有很多种)。然后,它会讲所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因时任何再上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并回收他们的内存。
    • 引用计数
      • 另一种没那么常用的垃圾回收策略是 引用计数。其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为1.如果同一个值又被赋给另一个变量,那么计用数加1.类似地,如果保存对该值引用的变量被其他值覆盖了,那么引用数减1.当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为0的内存。
    • 性能
      • 垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。开发者不知道什么时候运行时会收集垃圾,因此最好的办法是在写代码时就要做到:无论什么时候开始收集垃圾,都能让他尽快结束工作。
      • 现代垃圾回收程序会基于对js运行时环境的探测来决定何时运行。探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的。比如,根据V8团队2016年的一篇博文的说法:“再一次完整的垃圾回收之后,V8的堆增长策略会根据活动对象的数量外加一些余量来确定合适再次垃圾回收”。
      • 内存管理
        • 在使用垃圾回收的编程环境中,开发者通常无需关心内存管理。不过js运行在一个内存管理与垃圾回收都特殊很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动浏览器的就更少了。这更多处于安全考虑而不是别的。就是为了避免运行大量js的网页耗尽系统内存而导致操作系统崩溃。这个内存限制不仅影响变量分配,也影响用栈以及能够同时在一个线程中执行的语句数量。
        • 将内存占用保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证再执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为null,从而释放其引用。这也可以叫做接触引用。这个建议最适合全局变量和全局对象的属性。
        • 通过const和let声明提升性能
          • es6增加这两个关键字不仅有助于改善代码风格,同样有助于改进垃圾回收的过程。因为const和let都以块(而非函数)为作用域,所以相比于使用var,使用这两个新关键字可能会更早地让垃圾回收程序介入,今早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。
        • 隐藏类和删除操作
          • 根据js所在的运行环境,有时候需要根据浏览器使用的js引擎来踩去不同的性能优化策略。截至2017年。
        • 内存泄漏
          • 写得不好的js可能出现难以察觉且有害的内存泄漏问题。在内存有限的设备上,或者在函数会被调用很多次的情况下,内存泄漏可能是个大问题。js中的内存泄露大部分是由不合理的引用导致的。
        • 静态分配与对象池
          • 为了提升js性能,最后要考虑的一点往往就是压榨浏览器了。此时,一个关键问题就是如何减少浏览器执行代码回收的次数。开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免对于的垃圾回收,那就可以保住因释放内存而损失的性能。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值