前端笔记 - JS函数参数,类比Python

前端笔记
parameter是函数定义时的参数,argument是函数调用时的参数。传值调用,传引用调用。

之前一直没有细看JavaScript这一部分,开发时感觉有点不自信,所以找了几篇资料去看了下,顺便现在写写。

之前也写过挺长时间Python的,Python函数中的传值也有点不自信,虽然在构建深度学习的BP网络时,总是很自然的直接认为参数可以影响外面的值,即传引用调用,而在用一般类型时,认为是传值调用,但是,还是对原理不够清楚,所以现在也一起整理下,系统性的总节。

调用方式

调用方式主要有两种,分别是传值调用(Call-by-value)传引用调用(Call-by-reference),实际的编译器实现中,还有很多其他的种类。

传值调用(Pass by value)

在传值调用中,传递给函数参数是函数被调用时所传实参的拷贝。在传值调用中实际参数被求值,其值被绑定到函数中对应的变量上(通常是把值复制到新内存区域)。cpp中默认的函数传递方式就是如此int main(int argc),这里的argc就是传值调用。

传引用调用(Pass by reference)

在传引用调用调用中,传递给函数的是它的实际参数的隐式引用而不是实参的拷贝。通常函数能够修改这些参数(比如赋值),而且改变对于调用者是可见的。cpp中void fun(int &num),这里的num就是传引用调用。

cpp🐂,手术刀级别的操控还是舒服,当然也需要编写者自身水平比较高。

引用类型调用(Nicholas)表示如下:

引用类型内存分配

除了上述两种外,实际上,还有一种调用方式,传共享调用(Call by sharing)

传共享调用(Call by sharing)

传共享调用和传引用调用的不同之处是,该求值策略传递给函数的参数是对象的引用的拷贝,即对象变量指针的拷贝。也有说法,说这也是传值调用,只不过对于非基本类型,传的是对象的引用,对于基本类型,直接传值。

也就是说如下方代码 changeStuff 函数内的 a b c 都分别与 num obj1 obj2 指向同一块内存,但不是其拷贝。函数内对 a b c 所做的任何修改,都将反映到 num obj1 obj2 上 。不过对于JavaScript,如果实参类型为默认的基本类型,那么就是值拷贝。

function changeStuff(a, b, c) {
  a = a * 10;               // 对 a 赋值,修改 a 的指向,新的值是 a * 10
  b.item = "changed";       // 因为 b 与 obj1 指向同一个对象,所以这里会修改原始对象 obj1.item 的内容
  c = {item: "changed"};    // 对 c 重新赋值,修改 c 的指向,其指向的对象内容是 {item: "changed"}
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num);
console.log(obj1.item);    
console.log(obj2.item);
// 10
// changed
// unchanged

JS 引用类型变量的值是一个指针,指向堆内存中的实际对象。

这点让我想到了Python,感觉这俩这地方挺像的,Python也是共享传参,所以我又去review了下Python基础。

Python的对象

Python中所有的变量实际上都是指向变量的内存地址,并且这个地址可以通过id函数来查找,而且Python在变量内存分配时,还会有许多有意思的地方,类似下方:

a = 1
id(a)   # 4498704688
b = 1
id(b)   # 4498704688
b = 2
id(b)   # 4498704720
c = 1
id(c)   # 4498704688   和a的地址相同

从上面我们就可以发现,Python里同样对象使用的是相同的内存空间,且新生成对象时,会优先去已经分配的空间查找是否已经有存在的,如果不存在,再新建空间存放新对象,否则就是直接建一个新引用。

当然上述表述只对Python的不可变对象有效。

可变对象和不可变对象

可变对象与不可变对象的区别在于对象本身是否可变。

python内置的一些类型中

  • 可变对象:list dict set
  • 不可变对象:tuple string int float bool

Python函数参数

Python函数参数都只能是引用传递,所有的变量都是引用,指向对象的地址,并且只有以下两种类型的对象。

不可变对象,修改变量值实际上都是新分配了空间,然后改变引用的地址,所以函数内引用的地址发生改变,表象就是变量值变了,但是函数外任然还是引用原值的地址。

可变对象,修改变量值是直接操作地址上的值,并没有新分配内存空间,所以函数内修改变量,函数外也会受到影响。

自定义类通常是可变的。要模拟类中的不变性,生成不可变对象,应该覆盖属性设置和删除以引发异常。

跑题了,跑题了,这里主题应该是JavaScript,现在回来

JS代码分析

还是上面那段代码

function changeStuff(a, b, c) {
  a = a * 10;               // 对 a 赋值,修改 a 的指向,新的值是 a * 10
  b.item = "changed";       // 因为 b 与 obj1 指向同一个对象,所以这里会修改原始对象 obj1.item 的内容
  c = {item: "changed"};    // 对 c 重新赋值,修改 c 的指向,其指向的对象内容是 {item: "changed"}
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num);
console.log(obj1.item);    
console.log(obj2.item);
// 10
// changed
// unchanged

变量初始化

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

变量初始化

调用函数

changeStuff(num, obj1, obj2);

调用函数时的内存分配

可以看到,变量 a 的值就是 num 值的拷贝,变量 b c 分别是 obj1 obj2 的指针的拷贝。

函数的参数其实就是函数作用域内部的变量,函数执行完之后就会销毁。

执行函数体

a = a * 10;
b.item = "changed";
c = {item: "changed"};

执行函数体

如图所示,变量 a 的值的改变,并不会影响变量 num

b 因为和 obj1 是指向同一个对象,所以使用 b.item = "changed"; 修改对象的值,会造成 obj1 的值也随之改变。

由于是对 c 重新赋值了,所以修改 c 的对象的值,并不会影响到 obj2

结论

从上面的例子可以看出,对于 JS 来说:

  • 基本类型是传值调用
  • 引用类型传共享调用

传值调用本质上传递的是变量的值的拷贝。

传共享调用本质上是传递对象的指针的拷贝,其指针也是变量的值。所以传共享调用也可以说是传值调用。

REFERENCE