关于赋值的一个问题

Canrz
  • 252
var foo = 1,
bar = foo;   
bar = 9; 
console.log(foo, bar); // => 1, 9

var foo = [1, 2],
bar = foo;
bar[0] = 9;
console.log(foo[0], bar[0]); // => 9, 9

var foo = [1, 2],
foo = bar;  
bar[0] = 9; 
console.log(foo[0], bar[0]); // => 9, 9

第二、三中为什么会输出9,9?

回复
阅读 2.7k
6 个回答

理解这个区别的关键就是理解 bar = 9bar[0] = 9 的区别。

先说 bar = 9,这等于是说:将一个对象(数字 9)赋值给变量 bar。在此之前,barfoo 都是指向数字 1 的引用,但是 bar = 9 这一句等于重写了 bar 的指向。所以 foo 是 1,bar 是 9。

再说 bar[0] = 9,首先 barfoo 一样都是数组 [1, 2] 的引用(换言之,它们指向的是同一个数组对象),那么 bar[0] = 9 并没有改变 bar 本身的指向,而是把它所指向的数组的第一个元素改成 9。即使数组里的第一个元素的值改变了,barfoo 依然还是指向这同一个数组。所以 foo[0] 是 9,bar[0] 也是 9。

本问题可以归根结底为数据和变量的对应关系。

在C/Java之类的编程语言里,我们可以很清楚地区分"赋值/赋引用"、"传值/传引用":

  • =操作符
    赋值则内存中两个变量各自保存一份数据;
    赋引用内存中只有一份数据。
  • 函数传参
    传值则会在内存中拷贝出第二份数据,用于函数修改,不影响原有的数据;
    传引用则不会在内存中拷贝出第二份数据,函数直接修改目标内存地址中的数据。

我们看区分这两个过程的用例:

var foo = 1,
bar = foo;   
bar = 9; 
console.log(foo, bar); // => 1, 9

很明显,=以后,两个变量各自保存一份数据(1和9),否则不会出现数字的不一致。hence, 赋值。

var x = 0;
function a(x)
{
    x++;
}
a(x);
console.log(x); // 结果: 0

没有改变变量的数据,函数执行上下文中拷贝出来了一份x,要不然这里的结果就应该是1了。hence, 传值。


纵观楼主的用例,出现了传值和传引用两种结果,这是由于JavaScript中有Primitive Value和Object的区别的缘故。

那么我们是否可以得出结论了?

  • Primitive Value,“赋值”,“传值”
  • Object,“赋引用”,“传引用”

如果要浅显地记忆,到这里就差不多了。


下面是非常非常容易混淆的分界线。

Primitive Value可以见MDN定义

All types except objects define immutable values (values, which are incapable of being changed). For example and unlike to C, Strings are immutable. We refer to values of these types as "primitive values".

一个关键词是,immutable values,不可改变。
这个定义即意味着他们作为传参和=操作符对象时,值本身不会修改。
在JS引擎执行的同一个生命周期里,1不会变成2"google"不会变成"google+"——immutable的实现方式,是引用。从这个意义来说,@nighttire的解释是正确的(尽管“对象”不准确,正确的说法是“引用”)。

再次解释JavaScript内的”赋值/赋引用”、”传值/传引用”:

  • =操作符
    两个变量各自保存同一个引用

  • 函数传参
    则内存中开辟一个新的变量空间,储存原有参数的引用
    (还记得JavaScript那个经典的闭包问题吗,函数调用过程中,会拷贝一份参数的引用,才会直接或间接调用函数的方案)

感觉到混淆了吗,C/C++中对值和引用的区分,在表述中完全没有必要,因为JavaScript里面,从始至终,只有引用

那么,在这些用例里面,是什么造成了最终的结果差异?答案是,是否允许变化。

LZ的赋值用例

var foo = 1, // foo -> (primitive 1)
bar = foo;  // foo -> (primitive 1) // bar-> (primitive 1)
bar = 9;  // 由于Primitive Value本身不允许改变,因此bar=9并不会改变(primitive 1)为(primitive 9) 因此不会改变foo的引用
// 此时 foo -> (primitive 9) // bar-> (primitive 1)
console.log(foo, bar); 
// hence 结果为 1, 9

var foo = [1, 2], // foo -> (object [1,2])
bar = foo;  // bar ->  (object [1,2])
bar[0] = 9; // bar ->  (object [9,2]) <- foo
console.log(foo[0], bar[0]);
// hence 结果为 9, 9

var foo = [1, 2], // foo -> (object [1,2])
foo = bar;  // foo -> (object [1,2]) <- foo
bar[0] = 9; // bar -> (object [9,2]) <- foo
console.log(foo[0], bar[0]);
// hence 结果为 9, 9

传参用例

var x = 0; // 作用域链外层x -> (primitive 0)
function a(x)
{ // 作用域链内层实参x -> (primitive 0) 作用域链外层x -> (primitive 0)
    x++;
  // 作用域链内层实参x -> (primitive 1)
  // 作用域链外层x -> (primitive 0)
}
a(x);
console.log(x); // 作用域链外层x -> (primitive 0)
// hence 结果: 0
var x = [0]; // 作用域链外层x -> (object [0])
function a(x)
{ // 作用域链内层实参x -> (object [0]) <- 作用域链外层x
    x[0]++;
  // 作用域链内层实参x -> (object [1]) <- 作用域链外层x
}
a(x);
console.log(x[0]); // 作用域链外层x -> (object [1])
// hence 结果: 1

这是一个典型的引用类型(对象)进行复制的问题。

var foo = 1, // 基本类型的值
bar = foo; // 直接把 foo 的值 copy 一份存到 bar 中
bar = 9; // 改变 bar 的值,但并不影响 foo
console.log(foo, bar); // 1, 9

var foo = [1,2], // 对象(引用类型的值)
bar = foo; // 把 foo 指向 [1,2] 的指针 copy 一份给 bar,也就是说这里 bar 和 foo 引用的都是 [1,2] 这个对象
bar[0] = 9; // 改变 bar 第一个元素的时候,也就是把 [1,2] 改变为 [9, 2]
// 此时,foo, bar 都指向 [9, 2]
console.log(foo[0], bar[0]); // 9, 9

var foo = [1,2], // 这里重新定义一次 foo
foo = bar; // 到这一步赋值操作的时候又重新把 foo 的引用指向上面第二段代码的 bar 了, 也就是说这个时候 foo, bar 都指向 [9, 2]
// 剩下的和第二部一样的
bar[0] = 9;
console.log(foo[0], bar[0]);

参考:

图片描述

刚才贴了一张图,重新梳理一下。

这是一个典型的变量复制问题,引出两个概念:值传递和引用传递。

直接看代码:

// 基本类型值,值传递
var foo = 1,
bar = foo; // 复制变量 foo,但是 bar 和 foo 是相互独立的   
bar = 9; // 既然是独立的,改变只对 bar 有效
console.log(foo, bar); // => 1, 9

// 引用类型值(对象 - 数组也是对象),引用传递
var foo = [1, 2], // foo 指向 [1, 2] 这个真实的对象
bar = foo; // 复制 foo, 这里复制的是 foo 指向 [1,2] 的指针,也就是说 foo 和 bar 都指向 [1, 2]
bar[0] = 9; // 改变 bar[0] 意味着将 [1, 2] 改变为 [9, 2],至此 foo 和 bar 都指向 [9, 2]
console.log(foo[0], bar[0]); // => 9, 9 那么这个结果很明显了。

// 这里涉及到赋值问题
var foo = [1, 2],  // 首先,重新定义了 foo
foo = bar; // 然后这里重新复制上面第二步中的 bar(-> [9, 2])
bar[0] = 9; // 剩下的和上面的道理一样
console.log(foo[0], bar[0]); // => 9, 9

分析一下每一步 foobar 的结果就很好理解了。

关键字:

  • 变量复制
  • 值传递和引用传递

ECMAscript变量可能包括两种不同的数据类型的值:基本类型值和引用类型值。

不知道javascript高级程序设计这里为什么用可能两个字...但是大致可归纳,一般来说对象是引用类型;其他的基本类型是不引用的,就如你的第一段代码的效果。

引用类型

obj1 = {}
obj2 = obj1
obj1 === obj2 // true

楼上的图很形象,数组也是对象

宣传栏