zoukankan      html  css  js  c++  java
  • 值和引用

    引用《你不知道的JavaScript》中卷 2.5值和引用

    在许多编程语言中,赋值和参数传递可以通过值复制(value-copy)或者引用复制 (reference-copy)来完成,这取决于我们使用什么语法。

    例如,在 C++ 中如果要向函数传递一个数字并在函数中更改它的值,就可以这样来声明参 数int& myNum,即如果传递的变量是x,myNum就是指向x的引用。引用就像一种特殊的指 针,是来指向变量的指针(别名)。如果参数不声明为引用的话,参数值总是通过值复制 的方式传递,即便对复杂的对象值也是如此。

    JavaScript 中没有指针,引用的工作机制也不尽相同。在 JavaScript 中变量不可能成为指向 另一个变量的引用。

    JavaScript 引用指向的是值。如果一个值有 10 个引用,这些引用指向的都是同一个值,它 们相互之间没有引用 / 指向关系。

    JavaScript 对值和引用的赋值 / 传递在语法上没有区别,完全根据值的类型来决定。

    下面来看一个例子:

      var a = 2;
      var b = a; // b是a的值的一个副本 b++;
      a; // 2
      b; // 3

      var c = [1,2,3];
      var d = c; // d是[1,2,3]的一个引用 d.push( 4 );
      c; // [1,2,3,4]
      d; // [1,2,3,4]

    简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值 / 传递,包括 null、undefined、字符串、数字、布尔和 ES6 中的 symbol。

    复合值(compound value)——对象(包括数组和封装对象,参见第 3 章)和函数,则总 是通过引用复制的方式来赋值 / 传递。

    上例中 2 是一个标量基本类型值,所以变量 a 持有该值的一个复本,b 持有它的另一个复 本。b 更改时,a 的值保持不变。

    c 和 d 则分别指向同一个复合值 [1,2,3] 的两个不同引用。请注意,c 和 d 仅仅是指向值 [1,2,3],并非持有。所以它们更改的是同一个值(如调用 .push(4)),随后它们都指向更 改后的新值 [1,2,3,4]。

    由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。
    
         var a = [1,2,3];
         var b = a;
         a; // [1,2,3]
         b; // [1,2,3]
       //然后
       b = [4,5,6]; a; // [1,2,3] b; // [4,5,6]      

    b=[4,5,6] 并不影响 a 指向值 [1,2,3],除非 b 不是指向数组的引用,而是指向 a 的指针, 但在 JavaScript 中不存在这种情况!

    函数参数就经常让人产生这样的困惑:
    
           function foo(x) {
              x.push( 4 );
              x; // [1,2,3,4]
           // 然后
           x = [4,5,6]; x.push( 7 );
           x; // [4,5,6,7]
        }
        var a = [1,2,3];
        foo( a );
        a; // 是[1,2,3,4],不是[4,5,6,7]
    

      

    我们向函数传递 a 的时候,实际是将引用 a 的一个复本赋值给 x,而 a 仍然指向 [1,2,3]。 在函数中我们可以通过引用x来更改数组的值(push(4)之后变为[1,2,3,4])。但x = [4,5,6] 并不影响 a 的指向,所以 a 仍然指向 [1,2,3,4]。

    我们不能通过引用 x 来更改引用 a 的指向,只能更改 a 和 x 共同指向的值。

    如果要将 a 的值变为 [4,5,6,7],必须更改 x 指向的数组,而不是为 x 赋值一个新的数组。

        function foo(x) {
             x.push( 4 );
             x; // [1,2,3,4]
             // 然后
             x.length = 0; // 清空数组 
    x.push( 4, 5, 6, 7 ); x; // [4,5,6,7] } var a = [1,2,3]; foo( a ); a; // 是[4,5,6,7],不是[1,2,3,4]

      

    从上例可以看出,x.length = 0和x.push(4,5,6,7)并没有创建一个新的数组,而是更改 了当前的数组。于是 a 指向的值变成了 [4,5,6,7]。

    请记住:我们无法自行决定使用值复制还是引用复制,一切由值的类型来决定。
    

    如果通过值复制的方式来传递复合值(如数组),就需要为其创建一个复本,这样传递的 就不再是原始值。例如:

    foo( a.slice() );
    slice(..) 不带参数会返回当前数组的一个浅复本(shallow copy)。由于传递给函数的是指

    向该复本的引用,所以 foo(..) 中的操作不会影响 a 指向的数组。

    相反,如果要将标量基本类型值传递到函数内并进行更改,就需要将该值封装到一个复合值(对象、数组等)中,然后通过引用复制的方式传递。

        function foo(wrapper) {
             wrapper.a = 42;
        }
    
        var obj = { 
            a: 2
        };
        foo( obj );
        obj.a; // 42    
    

      

    这里 obj 是一个封装了标量基本类型值 a 的封装对象。obj 引用的一个复本作为参数 wrapper 被传递到 foo(..) 中。这样我们就可以通过 wrapper 来访问该对象并更改它的属 性。函数执行结束后 obj.a 将变成 42。

    这样看来,如果需要传递指向标量基本类型值(比如 2)的引用,就可以将其封装到对应 的数字封装对象中(参见第 3 章)。

    与预期不同的是,虽然传递的是指向数字对象的引用复本,但我们并不能通过它来更改其 中的基本类型值:

     1 function foo(x) {
     2      x = x + 1;
     3      x; // 3 
     4 }
     5 
     6 var a = 2;
     7 var b = new Number( a ); // Object(a)也一样
     8 
     9 foo( b );
    10 console.log( b ); // 是2,不是3    

    原因是标量基本类型值是不可更改的(字符串和布尔也是如此)。如果一个数字对象的标量基本类型值是 2,那么该值就不能更改,除非创建一个包含新值的数字对象。

    x = x + 1中,x中的标量基本类型值2从数字对象中拆封(或者提取)出来后,x就神不 知鬼不觉地从引用变成了数字对象,它的值为2 + 1等于3。然而函数外的b仍然指向原 来那个值为 2 的数字对象。

    我们还可以为数字对象添加属性(只要不更改其内部的基本类型值即可),通过它们间接 地进行数据交换。

    不过这种做法不太常见,大多数开发人员可能都觉得这不是一个好办法。

    相对而言,前面用 obj 作为封装对象的办法可能更好一些。这并不是说数字等封装对象没 有什么用,只是多数情况下我们应该优先考虑使用标量基本类型。

    引用的功能很强大,但有时也难免成为阻碍。赋值 / 参数传递是通过引用还是值复制完全 由值的类型来决定,所以使用哪种类型也间接决定了赋值 / 参数传递的方式。

  • 相关阅读:
    linux性能指令分析进阶篇
    数据库提升篇
    linux之基础命令大全
    数据库事务测试以及级联更新级联删除
    【Comet OJ】—模拟赛测试 Day1题解
    【Comet OJ】—模拟赛测试 Day1题解
    【LOJ # 6268】—分拆数(生成函数+多项式Ln/Exp+NTT)
    【LOJ # 6268】—分拆数(生成函数+多项式Ln/Exp+NTT)
    【LOJ #6041】【雅礼集训 2017 Day7】—事情的相似度(后缀自动机+LCT+树状数组)
    【LOJ #6041】【雅礼集训 2017 Day7】—事情的相似度(后缀自动机+LCT+树状数组)
  • 原文地址:https://www.cnblogs.com/PasserByOne/p/12296649.html
Copyright © 2011-2022 走看看