JavaScript(红宝书)(四) |
您所在的位置:网站首页 › js红宝书有必要看吗贴吧 › JavaScript(红宝书)(四) |
4.变量、作用域与内存
通过变量使用原始值与引用值 理解执行上下文 理解垃圾回收 JavaScript 变量是 松散类型的,而且变量不过就是特定时间点一个特定值的名称而已。由于没有规则定义变量必须包含什 么数据类型,变量的值和数据类型在脚本生命期内可以改变。这样的变量很有意思,很强大,当然也有不少问题 原始值与引用值: ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。原始值(primitive value)就是最简单的数据,引用值(reference value)则是由多个值构成的对象。 基于此:js不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非 实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。 而对于非对象袁术,可以按值(value)访问。 在很多语言中,字符串是使用对象表示的,因此被认为是引用类型。ECMAScript 打破了这个惯例。 1.动态属性:对于引用值而言,可以随时添加、修改和删除其属性 和方法,对原始值添加的属性和方法虽然不会报错但是会使得其再次访问失败,返回undefined 2.复制值:原始值在赋值操作时会复制到新位置 产生新对象。在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区 别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。 3.传递参数:ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数 中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是 引用值,那么就跟引用值变量的复制一样。(js不能使用指针) 4.确定类型:typeof 操作符最适合用来判断一个变量是否为原始类型。更确切地说,它是判断一 个变量是否为字符串、数值、布尔值或 undefined 的最好方式。如果值是对象或 null,那么 typeof返回"object" 执行上下文与作用域: 变量或函数的上下文决定 了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object), 而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台 处理数据会用到它。 全局上下文时最外层的上下文:基于ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一 样。 例如:在浏览器中,全局上下文就是window对象,所有通过var定义的变量函数都会成为windoe对象的属性与方法。 虽然使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包括定义 在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。 每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的。(上下文环境,计基的执行栈,栈帧隔离函数域) 上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定 了各级上下文中的代码在访问变量和函数时的顺序。 代码正在执行的上下文的变量对象始终位于作用域 链的最前端。 如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有 一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上 下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终 是作用域链的最后一个变量对象。(调用的是变量对象里的值) 1.作用域链增强: 常规作用域链的产生:虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有 其他方式来增强作用域链。 某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执 行后会被删除。 try/catch 语句的 catch 块 with 语句 这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添 加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误 对象的声明。 IE 的实现在 IE8 之前是有偏差的,即它们会将 catch 语句中捕获的错误添加到执 行上下文的变量对象上,而不是 catch 语句的变量对象上,导致在 catch 块外部都可以 访问到错误。IE9 纠正了这个问题 2.变量声明: 使用var的函数作用域声明:在使用 var 声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函 数的局部上下文。 不使用声明关键字的声明会导致变量被添加到全局上下文中,在函数退出后依然存在。 声明提升:var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升” (hoisting)。提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。 使用 let 的块级作用域声明:ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JavaScript 中的新概念。块 级作用域由最近的一对包含花括号{}界定。 重复声明:var可以进行重复声明,而let的重复声明会导致程序抛出SyntaxError。let运行时虽然也会被提升,但由于“暂时性死区”(temporal dead zone)的 缘故,实际上不能在声明之前使用 let 变量。 使用 const 的常量声明:除了 let,ES6 同时还增加了 const 关键字。使用 const 声明的变量必须同时初始化为某个值。 一经声明,在其生命周期的任何时候都不能再重新赋予新值。 虽然const定义的引用不能更改,如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错, 但会静默失败。 由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例 都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化。 标识符查找:当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。搜 索开始于作用域链前端,以给定的名称搜索对应的标识符。如果在局部上下文中找到该标识符,则搜索 停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。(注意,作用域链中的对象也有一个 原型链,因此搜索可能涉及每个对象的原型链。)这个过程一直持续到搜索至全局上下文的变量对象。 如果仍然没有找到标识符,则说明其未声明。 3.垃圾回收:之前在浏览器章节有写过关于js的垃圾回收,现在还是重新再整理一遍。 JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。常规的c与c++都是需要开发者自己跟踪释放内存的,而js执行环境帮开发者卸下负担(其实js不能释放内存一定程度上因为其不能操控指针),通过自动内存管理实现内存分配和闲置资源回收。 垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收 内存。如何标记未使用的变量也许有不同的实现方式。不过,在浏览器的发展史上,用到过两种主要的 标记策略:标记清理和引用计数。 标记清理: 当变量进入上下文,这个变量会被加上存在于上下文中的标记。当变量离开上下文时, 也会被加上离开上下文的标记。(当上下文环境存在,这个变量就不应该被释放,因为它可能会被再次使用) 给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下 文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现 并不重要,关键是策略。 垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它 会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记 的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内 存清理,销毁带标记的所有值并收回它们的内存。 这个过程需要记载每一个变量,并且需要判断其是否带标记,在获得其句柄后还要进行释放操作,这些都是底层隐藏的步骤。且标记方法也被隐藏。 引用计数: 另一种没那么常用的垃圾回收策略是引用计数(reference counting)。其思路是对每个值都记录它被 引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变 量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一 个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序 下次运行的时候就会释放引用数为 0 的值的内存 引用计数存在的问题:循环引用,垃圾回收判断引用时如果不清除内部的属性引用,那么这些循环引用的对象会一直驻留在内存中。 性能:垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的 时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。 开发者不知道什么时候运行时会收集垃圾,因此最好的办法是在写代码时就要做到:无论什么时候开始 收集垃圾,都能让它尽快结束工作。 现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行。探测机制因引擎而异, 但基本上都是根据已分配对象的大小和数量来判断的。(在分配度较高的时候清理频率增加) 可以看几个回收时间确立的例子: 根据 V8 团队 2016 年的一篇博文的说法: “在一次完整的垃圾回收之后,V8 的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃 圾回收。 IE 曾饱受诟病。它的策略是根据分配数,比如 分配了 256 个变量、4096 个对象/数组字面量和数组槽位(slot),或者 64KB 字符串。只要满足其中某个 条件,垃圾回收程序就会运行。 IE7 发布后,JavaScript 引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触 发垃圾回收的阈值。IE7 的起始阈值都与 IE6 的相同。如果垃圾回收程序回收的内存不到已分配的 15%, 这些变量、字面量或数组槽位的阈值就会翻倍。如果有一次回收的内存达到已分配的 85%,则阈值重置 为默认值 (为什么要这样做呢 因为回收的内存过少 说明固化的字面量等较多,而回收较多,可以回复其默认值) 内存管理 在使用垃圾回收的编程环境中,开发者通常无须关心内存管理。不过,JavaScript 运行在一个内存 管理与垃圾回收都很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动 浏览器的就更少了。这更多出于安全考虑而不是别的,就是为了避免运行大量 JavaScript 的网页耗尽系 统内存而导致操作系统崩溃。这个内存限制不仅影响变量分配,也影响调用栈以及能够同时在一个线程 中执行的语句数量(js的内存受限因为浏览器内存受限) 将内存占用量保持在一个较小的值可以让页面性能更好: 优化内存占用的最佳手段就是保证在执行 代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫 作解除引用。这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用 1.通过const和let提升性能:使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。(引用中止的时间远小于var) 2.隐藏类和删除操作:根据 JavaScript 所在的运行环境,有时候需要根据浏览器使用的 JavaScript 引擎来采取不同的性能优 化策略。截至 2017 年,Chrome 是最流行的浏览器,使用 V8 JavaScript 引擎。V8 在将解释后的 JavaScript 代码编译为实际的机器码时会利用“隐藏类”。如果你的代码非常注重性能,那么这一点可能对你很重要。 对于没有明确类的构造函数,会为其公用的隐藏类,因为这两个实例共享同一个构造函数和原型,如果给其中一个对象增加新属性,两个实例会对应不同的隐藏类。会对性能产生影响。 在代码结束后,即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。动态删除属性 与动态添加属性导致的后果一样。最佳实践是把不想要的属性设置为 null。这样可以保持隐藏类不变 和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果 function Article(opt_author) { this.title = 'Inauguration Ceremony Features Kazoo Band'; this.author = opt_author; } let a1 = new Article(); let a2 = new Article('Jake');new关键字的隐藏 3.内存泄漏:内存在不经意间驻留了很多无用变量。 1.全局对象的意外声明 2.定时器或者回调在语句块中持有了变量的句柄和引用,导致其不能被回收(因为回调函数 可以在声明函数运行结束后仍然存在 形成的闭包一直在引用原函数的属性) 4.静态分配与对象池: 为了提升 JavaScript 性能,最后要考虑的一点往往就是压榨浏览器了。此时,一个关键问题就是如 何减少浏览器执行垃圾回收的次数。开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发 垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因 释放内存而损失的性能。 –减少垃圾回收线程启动的次数 function addVector(a, b) { let resultant = new Vector(); resultant.x = a.x + b.x; resultant.y = a.y + b.y; return resultant; }//因为垃圾回收是阶段性的 所以在时间段内未清除 会积累很多句柄 也就是新建很多对象 尽管在语句块上是单线的调用这个函数时,会在堆上创建一个新对象,然后修改它,最后再把它返回给调用者。如果这个 矢量对象的生命周期很短,那么它会很快失去所有对它的引用,成为可以被回收的值。假如这个矢量 加法函数频繁被调用,那么垃圾回收调度程序会发现这里对象更替的速度很快,从而会更频繁地安排 垃圾回收。 如何不让垃圾回收调度程序发现对象的更替速度加快? 一个策略是使用对象池。在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。 应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。 由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。 关于js引擎触发可变大小数组,就会进行多次垃圾回收。 小结:原始值和引用值有以下特点 原始值大小固定,因此保存在栈内存上。 从一个变量到另一个变量复制原始值会创建该值的第二个副本。 引用值是对象,存储在堆内存上。 包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身。 从一个变量到另一个变量复制引用值只会复制指针,因此结果是两个变量都指向同一个对象。 typeof 操作符可以确定值的原始类型,而 instanceof 操作符用于确保值的引用类型。 任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个 上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分。执行上下文可以总结 如下。 执行上下文分全局上下文、函数上下文和块级上下文。 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃 至全局上下文中的变量。 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。 变量的执行上下文用于确定什么时候释放内存。 JavaScript 是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。JavaScript 的垃圾回收 程序可以总结如下。 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript 引擎不再使用这种算 法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对 象(如 DOM 元素)。 引用计数在代码中存在循环引用时会出现问题。 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对 象、全局对象的属性和循环引用都应该在不需要时解除引用。 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |