JavaScript的值传递和引用传递


在不断的学习中,碰到一个有趣的问题:“在JavaScript中,到底是值传递,还是引用传递呢?函数参数是按什么传递呢?”

引用文章:

JavaScript的数据类型

JavaScript 有 5 种基本的数据类型(值类型),分别是:BooleannullundefinedStringNumber。还有3种引用数据类型:ObjectArrayFunction,它们通过引用来传递。

基本数据类型

如果一个基本的数据类型绑定到某个变量,我们可以认为该变量包含这个基本数据类型的值。
并且基本类型的变量是存放在栈内存(Stack)里的

var x = 10;
var y = "abc";
var z = null;

当我们使用=将这些变量赋值到另外的变量,实际上是将对应的值拷贝了一份,然后赋值给新的变量。我们把它称作值传递。

var x = 10;
var y = "abc";

var a = x;
var b = y;

console.log(x, y, a, b); // 10, 'abc', 10, 'abc'

ax都包含 10,by都包含’abc’,并且它们是完全独立的拷贝,互不干涉。如果我们将a的值改变,x不会受到影响。这就是值传递,不会因为被赋值后的变量改变而导致原变量改变。

引用数据类型

如果一个变量绑定到一个非基本数据类型(Array, Function, Object),那么它只记录了一个内存地址,该地址存放了具体的数据。注意之前提到指向基本数据类型的变量相当于包含了数据,而现在指向非基本数据类型的变量本身是不包含数据的。
并且引用类型的值是保存在堆内存(Heap)中的对象(Object)。
与其他编程语言不同,JavaScript 不能直接操作对象的内存空间(堆内存)。

栈内存中保存了变量标识符和指向堆内存中该对象的指针
堆内存中保存了对象的内容

var reference = [1];
var refCopy = reference;
reference.push(2);
console.log(reference, refCopy); // [1, 2], [1, 2]

referencerefCopy指向同一个数组。 如果我们更新referencerefCopy也会受到影响。可见,引用后的对象更新后,被引用的对象也更新了。
关键点:

运算符=就是创建或修改变量在内存中的指向.
初始化变量时为创建,重新赋值即为修改

举一个例子

var a = {b: 1};// a = {b: 1}
var c = a;// c = {b: 1}
a = 2;// 重新赋值a
console.log(c);// {b: 1}

接着是上一段代码在内存中的分布:

a,c b

然后一步一步执行代码:

  1. 创建变量a指向对象{b: 1};
  2. 创建变量c指向对象{b: 1};
  3. a重新指向常量区的2;
常量区
a 2
c {b:1}

所以c从始至终都是指向对象{b: 1}.

==和===的比较

对于基本类型的变量,==会对变量进行隐式转换,可以对不同类型的变量进行比较,===不仅进行值得比较,还要进行数据类型的比较。
对于引用类型的变量,==和===只会判断引用的地址是否相同,而不会判断对象具体里属性以及值是否相同。因此,如果两个变量指向相同的对象,则返回true

函数参数按值传递

定义

在《JavaScript高级程序设计》第三版 4.1.3,讲到传递参数:

ECMAScript中所有函数的参数都是按值传递的。

按值传递

举个简单的例子:

var value = 1;
function foo(v) {
    v = 2;
    console.log(v); //2
}
foo(value);
console.log(value) // 1

很好理解,当传递 value 到函数 foo 中,相当于拷贝了一份 value,假设拷贝的这份叫 _value,函数中修改的都是 _value 的值,而不会影响原来的 value 值。

按共享传递 call by sharing

准确的说,JS中的基本类型按值传递,对象类型按共享传递的(call by sharing,也叫按对象传递、按对象共享传递)。最早由Barbara Liskov. 在1974年的GLU语言中提出。该求值策略被用于Python、Java、Ruby、JS等多种语言。
该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:在共享传递中对函数形参的赋值,不会影响实参的值。如下面例子中,不可以通过修改形参o的值,来修改obj的值。

var obj = {x : 1};
function foo(o) {
    o = 100;
}
foo(obj);
console.log(obj.x); // 仍然是1, obj并未被修改为100.

然而,虽然引用是副本,引用的对象是相同的。它们共享相同的对象,所以修改形参对象的属性值,也会影响到实参的属性值。

var obj = {x : 1};
function foo(o) {
    o.x = 3;
}
foo(obj);
console.log(obj.x); // 3, 被修改了!

乍一看,函数的参数为对象时,好像是引用传递,但是红宝书说ECMAScript中所有函数的参数都是按值传递的, 这是没错的。但是拷贝副本也是一种值的拷贝,这时会觉得是引用传递,但实际上还是值的传递。理解这个问题的关键在于如何理解值传递和引用类型,这里要强调的是数据类型方法参数的传递方式没有半毛钱关系
简单来说, 引用类型的值传递, 在方法内部如果对形参重新赋值, 哪怕是同一个类的对象, 在赋值后修改对象的属性, 实参的对应的属性值都不会改变, 同时实参指向的对象也不变, 而形参在重新赋值后已经指向一个新的对象了; 而引用类型的引用传递, 在方法内部如果对形参重新赋值, 那么实参也跟着重新赋值, 实参最初所指向的那个对象将不被任何变量所指向。其实就是说形参也会被分配一个新的地址,指向实参的值。

一道简单的题

function changeAgeAndReference(person) {
    person.age = 25;
    person = {
        name: "John",
        age: 50
    };

    return person;
}
var personObj1 = {
    name: "Alex",
    age: 30
};
var personObj2 = changeAgeAndReference(personObj1);
console.log(personObj1); // -> ?
console.log(personObj2); // -> ?

最后,若有错误的地方,欢迎大家指出来!


文章作者: Marshall
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Marshall !
评论
  目录