JS Evaluate-Strategy

Posted by Damon on 2017-11-13

介绍

最近在看到一个关于JS函数参数传值策略的讨论。很多人会认为JS的Object类型作为函数参数是按引用传递,而基础类型是按值传递,他们也提出了自己的佐证,而且网上搜索很多文章好像也这么说。但是这样的说发是不是正确的呢?让我们来探讨一下JS里面的求值策略。

维基百科搜索Evaluate-Strategy你可以看到求值策略其实是编程语言里面的一个常用术语。求值策略通常指对某种编程语言的表达式进行求值和计算的一个规则集。而函数参数的传值策略是其中一个特殊的例子。

一般业界常见的求值策略有严格和非严格策略。在“严格求值”中,给函数的实参总是在这个函数被调用之前求值,相应的“非严格求值”就是在函数调用时求值所以也叫“惰性求值”。

和大部分语言(C,java,Python,Ruby等)一样JS采用的也是严格求值策略,不同的是在JS里面参数求值顺序从左至右而其他的实现则是从右至左。

注:ES6里面函数增加了默认参数,参数默认值不是传值的,而是每次都重新计算默认值表达式的值也就是说,参数默认值是惰性求值的。

了解JS的函数传参策略对于我们理解JS来说意义重大。

问题

在此之前我们先来看一下问题:

1
2
3
4
5
6
7
8
9
10
11
12
function magic(num, objectA, objectB) {
num = num * 6;
objectA = {name: 'AA'}
objectB.name = 'BB';
}
const num = 1;
const objectA = {name: 'A'};
const objectB = {name: 'B'};
magic(num, objectA, objectB);
console.log(num); // 1
console.log(objectA); // {name: "A"}
console.log(objectB); // {name: "BB"} something change

JS红宝书里面说过这么一句话: ECMAScript中所有函数的参数都是按值传递的,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。
上述例子中传入了三个值,一个Number类型,两个引用类型。然而当我们objectB的一个属性改变之后,居然改变了传入的变量的值。到底咋回事?

Call By Value 按值传递

“传值调用”求值是最常见的求值策略, 被求值了的参数值会被绑定当前函数的变量里(也就是传递的是其值的拷贝)。此策略中,函数内部改变参数值不会影响到外部值,一般来说是给改参数值分配了新内存,并被函数内部调用。

Call By Reference 按引用传递

按引用传递接收的不是值拷贝,而是对象的隐式引用,如该对象在外部的直接引用地址。函数内部对参数的任何改变都是影响该对象在函数外部的值,因为两者引用的是同一个对象,也就是说:这时候参数就相当于外部对象的一个别名。
让我们来看一个PHP的例子:

1
2
3
4
5
6
7
8
9
10
<?php
function foo(&$var) // 这里& 表示将参数按引用传递
{

$var++;
}

$a=5;
foo($a);
// $a is 6
?>

Call By Sharing 按共享传递

这个策略还有一些代名词:“按对象传递”或“按对象共享传递”,该策略是1974年由Barbara Liskov为CLU编程语言提出的。
该策略的要点是:函数接收的是对象对于的拷贝(副本),该引用拷贝和形参以及其值相关联。
这里出现的引用,我们不能称之为“按引用传递”,因为函数接收的参数不是直接的对象别名,而是该引用地址的拷贝。
最重要的区别就是:函数内部给参数重新赋新值不会影响到外部的对象(和上例按引用传递的case),但是因为该参数是一个地址拷贝,所以在外面访问和里面访问的都是同一个对象(例如外部的该对象不是想按值传递一样完全的拷贝),改变该参数对象的属性值将会影响到外部的对象。

解析

看到这里或许你对上面那个问题有一些眉目了,我们就来解析一下:
之前的文章 我们有讲过JavaScript 是一种弱类型或者说动态语言其数据类型可以分为两类:

  • 基本类型 Undefined,Null,Boolean,Number,String。
  • 引用类型 Object,Array,Function,Date等。

在做变量声明时,不同的类型内存分配也不一样:

  • 基础类型存储在栈中的简单数据段,直接就是变量访问的位置
  • 引用类型则存储在堆中,而变量访问的则是一个指针即访问堆中对象的一个地址

所以在复制变量的时候也就会不同:

  • 基本类型: 将其副本赋值给新变量,此后便相对独立
  • 引用类型:只是把内存地址副本赋值给了新变量,而指向了相同的对象,他们中任何一个做出改变都会反应在另一个身上

其实红宝书的那句话还有后文:

基本类型的传递如同基本类型的复制一样,而引用类型值的传递,如同引用类型变量的复制一样。

下面我们再来看看,这两者的不同带来的参数传递的问题:

  • 基本类型: 只是把变量里的值传递给参数,之后参数和这个变量互不影响
  • 对象类型: 传递的是对象的内存地址,但是它们指向的还是同一个对象,当内部函数通过该内存地址改变对象时,当然也就会影响到该对象。

上面可以解释 为什么文章开头的例子:num 和 objectB两个参数对与外部的影响。但是还有一个疑问,为什么objectA在函数内部被赋值了一个全新的对象后没有对外部造成影响呢?
这里就要说到JS函数对于引用类型的传递的求值策略来说就是按共享传递。因此,如果你对这个引用进行第二次赋值的时候,实际上把这份引用指向了另外一个对象,所以之后对这个对象的操作不会影响到外部的对象。

还需要多说一句

为了便于操作基本类型ECMAScript还提供了3种特殊的引用类型:Boolean,Number和String 他与其他引用类型相似,但同时具有各自的基本类型相应的特殊行为我们把它叫做基本包装类型。

如:
Number是和基本数据类型的数值对应的引用类型。可以创建对象和调用本身的方法。例子代码如下:

1
2
3
4
5

var num = new Number(100);
//toFixed方法表示按照指定小数位返回字符串
var t = num.toFixed(3);
console.log(t); //t的值是100.000

那么我们来看这个问题:

1
2
3
4
5
6
7
8
9
let num = new Number(9);

function addOne(n) {
n.x = "xx";
n += 1;
}

addOne(num);
console.log(num); // Number {9, x: "xx"}

怎么来解释addOne里面的行为呢? 就留给各位各抒己见吧。。

总结

总的来说,js这门语言有很多可以细究的地方。对于求值策略理解有助于解释我们日常代码中遇到的一些疑问,规避一些反模式,提高我们的代码鲁棒性。以上是我的一些理解望各位批评指正。

参考