{ }codelper

学习 › JavaScript

函数 · 15

函数进阶

变量的作用域、返回多个值、把函数当作值来传递。

把函数用得更进一步的三件事:变量住在哪里(作用域)、怎样返回两个值、怎样把函数像值一样传递。
下面的示例一次取回一组数中的最小值和最大值,传入条件函数来数出偶数和不小于 10 的数,并把一共检查了多少次条件记在函数外面的变量里。

let checks = 0;   // 함수 밖 변수: 프로그램이 끝날 때까지 남아요 function isEven(x) {  return x % 2 === 0;} function isBig(x) {  return x >= 10;} function minMax(nums) {  let lo = nums[0], hi = nums[0];  for (const x of nums) {    lo = Math.min(lo, x);    hi = Math.max(hi, x);  }  return [lo, hi];   // 두 값을 배열에 담아 돌려줘요} function countIf(nums, test) {   // test에는 함수가 들어와요  let count = 0;   // 함수 안 변수: 부를 때마다 새로 만들어져요  for (const x of nums) {    checks++;      // 밖의 변수를 바꿔요    if (test(x)) count++;  }  return count;} const nums = require("fs").readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const [lo, hi] = minMax(nums);console.log(lo + " " + hi);console.log(countIf(nums, isEven));console.log(countIf(nums, isBig));console.log(checks);
stdin
4 15 8 23 6 7
stdout
4 23
3
2
12

要点

  • 变量有自己住的地方(作用域)。
    在函数里创建的变量只能在这个函数里用,每次调用都会重新创建。
    示例中的 count 每次调用都从 0 重新开始。
  • 在函数外面创建的变量会一直保留到程序结束。
    示例中的 checks 在两次调用之间保持着值,最后是 12。
  • 在函数里修改外面的变量,各语言的做法不同。
    外面的变量如果到处被修改,就很难找到是哪里改的。
    所以只把它用在像示例的 checks 这样全程序只有一份的记录上,需要的值用参数接收,count 这样的计算结果用 return 交回。
  • 要返回两个值,就把它们打包成一个整体返回,接收的一方再拆开。
    用来打包的容器(元组、数组、pair、列表)因语言而异。
    C 则是接收用来写入结果的地址。
  • 函数也是值,所以可以传给别的函数。
    示例只用一个计数函数,就完成了“数偶数”和“数不小于 10 的数”。
    只把不同的部分(条件)抽成函数传进去。
  • 传递时不加括号,只写名字。
    is_even 是函数本身,is_even(4) 是调用函数得到的结果。

各语言的写法

Python在函数里只读外面的变量时直接用;要修改时,在函数开头写 global checks。
两个值用 return lo, hi(元组)返回,用 lo, hi = min_max(nums) 接收。
函数像 count_if(nums, is_even) 这样只传名字,简短的条件可以用 lambda x: x >= 10 当场写出
JavaScript在函数外面用 let 创建的变量,在函数里不用任何标记就能读写。
在函数里用 let 再创建同名变量,它就是和外面不同的另一个变量。
两个值用 return [lo, hi]; 返回,用 const [lo, hi] = minMax(nums); 接收。
函数像 countIf(nums, isEven) 这样传递,简短的条件用箭头函数 (x) => x >= 10
TypeScript和 JavaScript 一样。
返回两个值的类型写作 [number, number],接收函数的参数写作 test: (x: number) => boolean
Java类的外面不能放变量,所以函数外面的变量写成类的 static 字段(static int checks = 0;)。
两个值放进 int[],或者定义 record MinMax(int lo, int hi) {} 来返回。
函数用 IntPredicate test 接收,用 test.test(x) 调用;传递时写 Main::isEven 或 lambda x -> x >= 10。
在 lambda 里修改 main 的局部变量会编译错误
C#类的外面不能放变量,所以函数外面的变量写成类的 static 字段。
两个值用元组 (int, int),return (lo, hi);,接收时写 var (lo, hi) = MinMax(nums);。
也可以用 out 参数(out int lo)接收。
函数用 Func<int, bool> test 接收,传递时写 IsEven 或 lambda x => x >= 10
C++写在函数外面的变量是全局变量,任何函数都能读写。
两个值用 std::pair<int, int>,return {lo, hi};,接收时写 auto [lo, hi] = minMax(nums);。
函数用 std::function<bool(int)>(<functional>)接收,传递时写 isEven 或 lambda [](int x) { return x >= 10; }
C写在函数外面的变量是全局变量。
没有返回两个值的语法,所以像示例那样接收用来写入结果的地址(int *lo, int *hi,调用时写 &lo, &hi),或者放进结构体返回。
函数用函数指针 int (*test)(int) 接收,像 count_if(nums, n, is_even) 这样传名字。
没有 lambda
Craykon在最外层创建的变量在函数里也是同一个变量,不用任何标记就能读写。
在函数里第一次创建的变量只能在这个函数里用。
两个值像 返回 [lo, hi]; 这样放进列表返回,用 r[0]、r[1] 取出。
没有 lambda,所以像 countIf(nums, isEven) 这样传入有名字的函数

常见错误

想用 return lo, hi; 返回两个值JavaScript · C · C++

function minMax(nums) {
  let lo = nums[0], hi = nums[0];
  for (const x of nums) { lo = Math.min(lo, x); hi = Math.max(hi, x); }
  return lo, hi;
}
console.log(minMax([4, 15, 8]));

→ 15 — 逗号运算符只留下后面的值,所以只返回了 hi。C、C++ 也只给出警告就能编译,结果同样是 15。TypeScript 会在类型检查时报错(TS2695)拦住

JavaScript 写 return [lo, hi];,C++ 用 std::pair 写 return {lo, hi};,C 接收地址或用结构体返回。
return lo, hi 能返回两个值的是 Python。

没写 global 就修改外面的变量Python

checks = 0
def check(x):
    checks += 1
    return x % 2 == 0
print(check(4))

→ UnboundLocalError: cannot access local variable 'checks' where it is not associated with a value — 在函数里被赋值的名字会被当作这个函数自己的变量,于是要给还没有值的 checks 加 1 时就停下了

在函数开头写 global checks。
只读的时候不需要。

函数里的变量和最外层的变量同名Craykon

total = 100;
twice(x)
{
    total = x * 2;
    返回 total;
}
控制台.输出(twice(4));
控制台.输出(total);

→ 8、8 — 函数没有创建新变量,而是改了最外层的 total,100 不见了

只在函数里用的变量,要取和最外层变量不同的名字。
参数即使同名,也是这个函数自己的变量。
在其他语言里,在函数里用 let total、int total 新建(Python 是不写 global 直接赋值),得到的是和外面不同的变量,输出是 8、100。

各语言的差异
传递函数时如果像 countIf(nums, isEven()) 这样加了括号,就不是传递,而是当场调用了。
JavaScript 会以 TypeError: test is not a function 停止,Python 会以 TypeError: is_even() missing 1 required positional argument: 'x' 停止。
TypeScript 在类型检查时指出,Craykon 则报错说 isEven 需要 1 个值却给了 0 个。

练习题