前言
本文主要参考来源——《JavaScript 高级程序设计》。
明确说明:JavaScript 中的函数参数只能按值传递。大多数情况下,让我们对这句话感到困惑的原因,很大程度上是因为 JavaScript 中访问变量有按值和按引用两种方式,所以我们会想当然地认为函数的参数也有按值和按引用这两种传递方式。但,实际上并不是。
知识点回顾
在举例说明之前,我们需要回顾一些知识点:
- 从一个变量向另一个变量复制基本类型值和引用类型值的区别。
- 从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。看下面的例子:
let val = 10;
let val2 = val;
复制代码
变量 val 保存的值是 5。当我们把 val 的值复制给变量 val2 时,其保存的值也是 5。但这两个变量是完全独立且互不相识的,val2 的值只是 val 的值的一个副本。之后,无论你对这两个变量进行何种操作,它们都不会相互影响。下图展示了复制基本类型值的过程。
- 同复制基本类型值一样。从一个变量向另一个变量复制引用类型的值时,依旧是将存储在变量对象中的值复制一份放到为新变量分配的空间中。但区别是,这个值的副本是指向存储在堆中的一个对象的指针。换句话说,这两个变量引用的是同一个对象,只要改变它们中的任意一个,另一个就会受到影响。
let objVal = {
name: '你好'
};
let objVal2 = objVal;
objVal2.name = '哈哈';
console.log(objVal.name); // 哈哈
复制代码
定义变量 objVal 来保存一个对象,该对象有一个name属性,其值为 ‘你好’。然后,将变量 objVal 复制给变量 objVal2 并改变 name 属性值。由于这两个变量引用的是同一个对象。所以,objVal 访问 name 属性得到的值是 objVal2 修改后的值。下图展示了保存在变量对象中的变量和保存在堆中的对象之间的这种关系。
举例说明
注意点提示:
-
函数的参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用 ECMAScript 的概念来说,就是 arguments 对象中的一个元素)。
-
函数的参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,一旦这个局部变量发生变化,它便会立即反映在函数的外部。
看下面这个例子:
function changeNum(val) {
val = val + 10;
return val;
}
let num = 10;
let result = changeNum(num);
console.log(num); // 10
console.log(result); // 20
复制代码
具体过程:
-
调用 changeNum() 函数并将变量 num 作为参数传递给该函数。这个变量的值会被复制给参数 val,也就是该函数的局部变量。
-
在 changeNum() 函数内部,参数 val 的值加 10,然后再赋值给 val。
-
返回被重新赋值的参数 val。
但是,函数内部的变化并没有影响函数外部的 num 变量。因为,从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。也就是说,参数 val 与变量 num 仅仅是具有相同的值,它们完全独立且互不相识。
再看一个例子:
function getBookName(obj) {
obj.name = 'Js 红宝书';
}
const book = Object.create(null);
getBookName(book);
console.log(book.name); // Js 红宝书
复制代码
具体过程:
-
定义 getBookName() 函数,它接受一个参数(引用类型) obj 并为其添加name属性。
-
使用
Object.create(null)
创建一个空对象并保存在变量 book 中。 -
调用 getBookName() 函数,且把变量 book 作为参数传递到该函数中,也就是把它复制给 obj。
-
在函数内部为 obj 添加 name 属性并赋值。
因为函数的参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,一旦这个局部变量发生变化,它便会立即反映在函数的外部。所以,当在函数内部为 obj 添加 name 属性后,函数外部的 book 就会有所反映。
大多数开发人员之所以认为参数是按引用传递的,是因为他们看到在局部作用域中修改的对象会在全局作用域中反映出来。但实际上,这是错误的认知。为了证明对象是按值传递的,我们修改一下这个例子:
function getBookName(obj) {
obj.name = 'Js 红宝书';
obj = Object.create(null);
obj.name = "Js 犀牛书";
}
const book = Object.create(null);
getBookName(book);
console.log(book.name); // Js 红宝书
复制代码
这个例子改动很明显,就是在前一个例子的 getBookName() 函数中添加了两行代码:
obj = Object.create(null); // 为 obj 重新定义一个对象
obj.name = 'Js 犀牛书'; // 为新的 obj 对象定义一个带有不同值的 name 属性
复制代码
具体过程(1——4 步骤同前一个例子一致):
- 利用
Object.create(null)
创建一个新对象赋给变量 obj,同时将其 name 属性设为’Js 犀牛书’。
如果 book 是按引用传递的,那么 book 就应该指向其 name 属性值为’Js 犀牛书’的新对象。但是,当我们再次访问 book.name 时,其值并未改变,仍是’Js 红宝书’。这向我们说明了一点:即使在函数内部修改了参数的值,其原始的引用仍会保持不变。另外需要注意的是:当我们在函数内部重新定义obj 时,它就变成引用一个局部对象的变量了。等到函数执行完毕,它会立即被销毁。