Javascript变量、作用域与内存

  • 通过变量使用原始值与引用值

  • 理解执行上下文

  • 理解垃圾回收

  • ES规定,JS变量是松散类型的。

  • 由于没有规定定义变量必须包含什么数据类型,变量的值和数据类型在脚本生命周期内可以改变。

  • ES变量有2种不同类型的数据:

    • 原始值(primitive value):最简单的数据
    • 引用值(reference value):由多个值构成的对象
  • 6种原始值:

    • Undefined
    • Null
    • Boolean
    • Number
    • String
    • Symbol
  • 保存原始值的变量是按值访问by value。因为我们操作的就是存储在变量中的实际值。

  • 引用值是保存在内存中的对象。JS不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。

  • 操作对象时,实际上操作的是该对象的引用reference而不是非实际的对象本身。

  • 保存引用值得变量是按引用by reference访问的。

  • 对于引用值而言,可以随时添加、修改和删除其属性和方法。

  • 原始值不能有属性,尽管尝试给原始值添加属性不会报错。

  • 【注意】原始类型的初始化可以只使用原始字面量形式。如果使用new关键字,则JavaScript会创建一个Object类型的实例,但其行为类似原始值

  let name1 = "Nich";
  let name2 = new String("Matt");
  name1.age = 27;
  name2.age = 26;
  console.log(name1.age); // undefined
  console.log(name2.age); // 26
  console.log(typeof name1); // string
  console.log(typeof name2); // object
  • 复制值

    • 原始值和引用值在通过变量复制时也有所不同。
    • 在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。
    • 原始值的复制,得到的新变量和原来的变量是互不干扰的。
    • 引用值得复制其实复制得是一个指针,它指向堆内存中得对象。
  • 传递参数

    • ES中所有函数都是按值传递得,这意味着函数之外的值会被复制到函数内部的参数中。
    • 如果是原始值,那么就跟原始值变量复制一样,如果是引用值,那么就和引用值变量复制一样。
    • 传参只有按值传递一种。
    • 在按值传递参数时,只会被复制到一个局部变量(即一个命名参数,用ES的话说就是arguments对象中的一个槽位)。
    • 在按值传递参数时,值在内存中的位置会被保存在一个局部变量,这意味着对本地变量的修改会反映到函数外部。(这在ES中式不可能的)
      function addTen(num) {
        num += 10;
        return num;
      }
    
      let count = 20;
      let result = addTen(count);
      count // 20
      result // 30
    
    • addTen(num)的参数num,是一个局部变量。变量count作为参数传入,使用的是值传递。count和num是互不干扰的。
    • 如果num是按照引用传递的,则count就会被修改。
      function setName(obj) {
        obj.name = 'Nicholas';
      }
      let person = new Obejct();
      setName(person);
      person.name // 'NIcholas'
    
    • 对象是按值传递的。
      function setName(obj) {
        obj.name = 'Nich';
        obj = new Object();
        obj.name = 'Greg';
      }
    
      let person = new Object();
      setName(person);
      person.name //'Nich' 
    
    • ES中的函数的参数就是局部变量
  • 确定类型

    • typeof操作符用来判断一个变量是否为原始类型

      • 字符串
      • 数值
      • 布尔值
      • undefined
      • null, object => 被判断成object
    • typeof对原始值很有用,但是对引用值用途不大。

    • instanceof操作符,可以让我们知道变量是什么类型的对象。

    • 通过instanceof操作符检测任何引用值和Object构造函数都会返回true。

    • 用instanceof检测原始值,始终会返回false。

  • ES规定,任何实现内部[Call]方法的对象都应该在typeof检测时返回function,但由于不同浏览器的实现方式,部分浏览器返回function,部分浏览器分会Object。

  • 执行上下文与作用域

    • 执行上下文的概念在js中颇为重要。
    • 变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。
    • 每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。
    • 无法通过代码访问变量对象,但后台处理数据会用到它。
  • 全局上下文

    • 是最外层的上下文。根据ES实现的宿主环境,表示全局上下文的对象可能不一样。
    • 在浏览器中,全局上下文就是我们常说的window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。
    • 使用let和const的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
    • 上下文在其代码执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)
  • 每个函数调用都有自己的上下文。

    • 当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返换给之前的执行上下文。
  • ES程序的执行流就是通过这个上下文栈进行控制的。

  • 上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数的顺序。

  • 代码正在执行的上下文的变量对象始终位于作用域链的最前端。

  • 如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments。(全局上下文没有这个变量)

  • 作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。 (包含上下文?????)

  • 全局上下文的变量对象始终是作用域链的最后一个变量对象。

  • 代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。

    • 搜索过程始终从作用域链的最前端开始,然后逐级往后直到找到标识符(找不到会报错)
  var color = "blue";

  function changeColor() {
    let antherColor = "red";

    function swapColors () {
      let tempColor = anotherColor;
      antherColor = color;
      color = tempColor;
      // 这里可以访问 color, antherColor, tempColor
    }
    // 这里可以访问color,antherColor
    swapColors()
  }
  // 这里只能访问呢 color
  changeColor()
  • 内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何事物。

  • 上下文是有序的、线性的。

    • 每一个上下文都可以到上级上下文中搜索变量和函数,但不能到下一级上下文中去搜索。
  • 函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则。

  • 作用域链增强

    • 执行上下文主要有全局上下文和函数上下文(eval()调用内部存在第三种上下文)
    • 但有其他方式来增强作用域链。
  • 通常在以下两种情况下,会出现:导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。

    • try/catch语句的catch

    • with语句

    • with语句,会向作用域前端添加指定的对象;

    • 对于catch,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明

      function buildUrl () {
        let qs = "?debug=true";
    
        width(location) {
          let url = href + qs;
        }
        return url;
      }
    
    • 这里将localion对象作为上下文,因此location会被添加到作用域前端。
    • buildUrl()中定义了一个变量qs。当with语句中的代码引用变量href时,实际上引用的是loaction.href,也就是自己变量对象的属性。。在引用 qs 时,引用的则是定义在 buildUrl()中的那个变量,它定义在函数上下文的变量对象上。而在 with 语句中使用 var 声明的变量 url 会成为函数上下文的一部分,可以作为函数的值被返回;但像这里使用 let 声明的变量 url,因为被限制在块级作用域(稍后介绍),所以在 with 块之外没有定义。
  • IE的实现在IE8之前是有偏差的,即它们会将catch语句中捕获的错误添加到执行上下文的变量对象上,而不是catch语句的变量对象上。IE9纠正了这个错误。

  • 变量声明

    • ES之后,JS的变量声明经历了翻天覆地的变化。ES6之前var是声明变量的唯一方式;之后新增了let和const。并让之成为首选。
  • 使用var的函数作用域声明

    • 在使用var声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函数的局部上下文。
    • 在with语句中,最接近的上下文也是函数上下文。
    • 如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文
  • 注意:未经声明而初始化变量是js编程中一个非常常见的错误,会导致很多问题。

  • 在初始化变量之前一定要先声明变量。在严格模式下,未经声明就初始化变量会报错。

  • 提升(hoisting):var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。

    • 提升让同一作用域的代码不必考虑变量是否已经声明就可以直接使用。
    • 在实践中,提升会导致合法却又奇怪的现象,即在变量声明之前使用变量。
      var name = "Jake";
      // 等价于
      name = "Jake";
      var name;
    
      // 下面是两个等价的函数
      function fn1 () {
        var name = 'Jake';
      }
    
      function fn2 () {
        var name;
        name = 'Jake';
      }
    
      // 通过在声明之前打印变量,可以验证变量会被提升。声明的提升意味着会输出undefined而不是Reference Error:
      console.log(name); // undefined
      var name = 'Jake';
    
      function () {
        console.log(name); // undefined
        var name = 'Jake';
      }
    
  • 使用let的块级作用域声明

    • ES6新增的let关键字和var相似,但它的作用域是块级的。
    • 块级作用域由最近的一对花括号{}界定。
    • if块、while块、function块,单独的块都是let声明变量的作用域
      // 这不是对象字面量,而是一个独立的块
      // js解释器会根据其中内容识别出它
      {
        let d;
      }
      console.log(d); // d没有定义
    
  • let 在同一作用域内不能声明两次。

  • 同一作用域内重复的var声明会被忽略。

  • let的行为适合在循环中声明迭代变量。使用var声明迭代变量会泄漏到循环外部。

  • 严格来讲,let在js运行时中也会被提升,但由于“暂时性死区(temporal dead zone)”的缘故,实际上不能再声明之前使用let变量。

  • 使用const的常量声明

    • 使用const声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何阶段都不能重新赋值。

    • const声明只应用到顶级原语或对象。

    • 赋值为对象的const变量不能再被重新赋值为其他引用值,但对象的键则不受限制。

      const o1 = {};
      o1 = {}; // TypeError:给常量赋值
    
      const o2 = {};
      o2.name = 'Jake';
      o2.age = 14;
    
    • 如果想让整个对象不能修改,可以使用Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败
      const o3 = Object.freeze({
        name: '大津',
        age: 14
      });
      o3.name = 'luke'
      console.log(o3) // { name: '大津', age: 14}
    
    • 由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8引擎就执行这种优化。
  • 标识符查找

    • 当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。搜索开始于作用域链前端,以给定的名称搜索对应的标识符。
    • 如果在局部上下文中找到该标识符,则搜索停止,变量确定。
    • 若没有找到变量名,则继续沿作用域搜索。
    • 作用域链中的对象也有一个原型链,因此搜索可能涉及每个对象的原型链。
    • 这个过程一直持续到搜索至全局上下文的变量对象。
    • 如果还没有找到则说明没有声明。
      var color = 'blue';
    
      function getColor () {
        return color;
      }
    
      console.log(getColor()); // 'blue'
    
    • 标识符查找并非没有代价。访问局部变量比访问全局变量要快,因为不用切换作用域。
    • js引擎在优化标识符查找上做了很多工作,将来这个差异可能就忽略不计了。
  • 垃圾回收

    • js是使用垃圾回收的语言,执行环境负责在代码执行时管理内存。
    • js通过自动内存管理实现内存分配和闲置资源回收。
    • 基本思路:
      • 确定哪个变量不会再使用,然后释放它占用的内存。
      • 这个过程是周期性的,即垃圾回收程序每隔一段时间就会自动运行。
    • 垃圾回收过程是一个近似且不完美的方案,因为某块内存在是否还有用,属于“不可判定的”问题,意味着靠算法是解决不了的。
  • 在浏览器的发展史上,用到过两种主要的标记策略:

    • 标记清理
    • 引用计数
  • 标记清理

    • 标记过程的实现并不重要,关键是策略。

    • 垃圾回收程序运行过程,会标记内存中存储的所有变量(标记方法有很多种)。然后它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。

    • 在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

  • 引用计数

    • 不常用

    • 思路:对每个值都记录它被引用的次数。

    • 引用计数最早记录的问题就是循环引用。

    • IE8及更早版本的IE中,并非所有对象都是原生JS对象。BOM和DOM中的对象是C++实现的组件对象模型(COM,Component Object Model)对象,COM对象使用引用计数实现垃圾回收。。因此,即使这些版本 IE的 JavaScript引擎使用标记清理,JavaScript存取的COM对象依旧使用引用计数。换句话说,只要涉及COM对象,就无法避开循环引用问题。

    • 把变量设置为null实际上会切断变量与其之前引用值之间的关系。当下次垃圾回收程序运行时,这些值就会被删除。内存会被回收。

    • IE9把BOM和DOM对象都改成了JS对象,这同时也避免了由于存在两套垃圾回收算法而导致的问题,还消除了常见的内存泄漏现象。

  • 内存管理

    • 在使用垃圾回收的编程环境中,开发者通常无须关心内存管理。
    • 由于JS运行在一个内存管理与垃圾回收都很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少很多。分配给移动浏览器的就更少了。避免运行大量js的网页耗尽系统内存导致操作系统崩溃。
    • 将内存占用量保持在较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫作解除引用。这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用。
      • 通过const和let声明提升性能
      • 隐藏类和删除操作
      • 内存泄漏
        • 使用js闭包很容易在不知不觉间造成内存泄漏。
      • 静态分配与对象池
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值