函数深入
return
reutrn
关键字用来在函数内部返回一个值作为函数的输出,函数会在执行完 return
语句后结束函数运行。return
语句可以返回任何表达式,比如一个三目表达式:
function foo(num) {
return num % 2 ? "奇数" : "偶数";
}
任何数据都可以作为函数的返回值,比如数组、对象或是另一个函数:
function foo() {
return function () {
console.log("返回一个函数");
};
}
function bar() {
return {
name: "一个对象",
};
}
function baz() {
return [
["多", "维"],
["数", "组"],
];
}
foo()(); // 返回值是一个函数表达式,那么可以再次执行
console.log(bar());
console.log(baz());
空返回
当一个函数没有返回值的时候,被称作 空返回,空返回会默认返回 undefined
,空返回有以下两种方式:
一是不使用 return
关键字:
function foo(x) {
console.log(x * x);
}
console.log(foo()); // undefined
二是使用 return
关键字,但是不返回值:
function foo (x) {
console.log(x * x);
return;
终止执行
使用 return
关键字会停止程序后续执行,很多时候,使用 return
只是为了控制函数不继续执行,比如:
function foo(x) {
if (typeof x !== "number") {
console.log("这不是一个数字");
return;
}
return x * 2;
}
上面代码中,我们对参数进行类型判断,如果不是数字,那么就停止后续执行。
终止执行有一个隐性的坑,查看下列代码:
function foo() {
return {
name: "foo",
other: "something",
};
}
有写人有强迫症,就是喜欢把花括号对齐,比如:
function foo()
{
return
{
name: "foo",
other: "something",
};
}
咋一看好像没什么问题,因为 JS 留白对代码影响不大。但是函数无法正确返回数据,这是因为 当语句的结尾没有分号时,JS 引擎自动添加分号,让语句结束,这种识别有一定规则,在 return
后面直接换行,会被视作 return
语句的结束,所以上面的代码没有返回值。程序甚至还会出现语法错误,无法解析 return
后的对象字面量。如果是数组同理:
参数默认值
函数参数可以设置默认值,但是这是新标准的规定,老旧浏览器不支持。新标准 JS 中的参数默认值按照一下方式设置:
function foo(x = 0) {
return x * x;
}
foo(); // 0
foo(5); // 25
默认值的语法如上,在不传递值,或者传入 undefined
作为参数时,函数会使用默认值代替传入的参数,减少程序的错误。
因为这语法是新标准,很多老旧的浏览器出现时它都还没有,所以无法在老旧的浏览器中使用,比如 IE 就不能用。
不过我们可以使用短路运算来模拟函数的默认值:
function foo(x) {
x = x || 10;
return x * x;
}
JS 新标准中提供了很多新的语法,默认值只是冰山一角,但是新标准的语法不在本系列课程讨论范围,会在其他系列课程中讲解。
arguments
在函数内部,可以使用 arguments
关键字来访问所有参数,他们会被放到一个类似数组的对象中,arguments
不能使用数组的内置函数,仅能使用 length
属性。
function foo() {
console.log(arguments[0]);
console.log(arguments[1]);
}
foo("a", "b"); // 'a' 'b'
当你的函数需要无限参数的时候,你需要用到它:
function sum() {
var re = 0;
for (var i = 0; i < arguments.length; i++) {
re += arguments[i];
}
return re;
}
console.log(sum(1, 2, 3, 4)); // 10
获取参数个数与函数名
每个函数有一个内置属性 length
,它记录了当前函数的形参个数:
function foo(a, b, c) {}
console.log(foo.length); // 3
普通开发很少会使用到这个属性,在编写库或搭建代码架构时可能会用到。
每个函数都由一个内置属性 name
,它记录了当前函数的名称:
var foo = function () {};
var bar = function barName() {};
function baz() {}
console.log(foo.name, bar.name, baz.name); // "foo" "barName" "baz"
这个属性也很少会使用。
逻辑分支与函数
条件声明 bug
在极少数情况下,需要使用逻辑分支来声明不同的函数,比如:
var foo = true;
if (foo) {
function bar() {
console.log("bar 1");
}
} else {
function bar() {
console.log("bar 2");
}
}
bar();
上述代码并不是所有浏览器都兼容,现代浏览器大部分会打印出 'bar 1'
,但是 IE11 以下的版本都会打印出 'bar 2'
。
如果真的需要条件声明,应该使用函数表达式:
var foo = true;
var bar;
if (foo) {
bar = function () {
console.log("bar 1");
};
} else {
bar = function () {
console.log("bar 2");
};
}
bar();
上面的代码可以消除函数条件声明产生的 bug。
立即执行表达式
在 JS 开发,可能会常常为了封装代码,编写一个函数,然后立即把函数执行后,该函数就被废弃,再也不会重复使用,类似下面的样子:
var bar = 10;
function foo(r) {
var PI = 3.14159265;
console.log(r * r * PI);
}
foo(bar);
这种写法看似多绕了一圈,实际上它帮助我们隐藏了一部分代码的实现,比如上面的 PI
变量,我们就无法在函数外访问。
在 JS 中,提供一个语法糖来简化上面的代码:
var bar = 10;
(function foo(r) {
var PI = 3.14159265;
console.log(r * r * PI);
})(bar);
我们可以 使用一个圆括号来包裹一个函数声明,此时这个函数声明会变成了一个函数表达式 ,我们可以在表达式的末尾使用圆括号来执行这个函数表达式。这种表达式被称作 立即执行表达式,有一个简化叫法 IIFE。
一个最简单的立即执行函数表达式如下:
(function () {})();
第一个圆括号内是一个函数表达式,第二个圆括号内时需要传入的参数。
立即执行函数表达式的优点在于它不会在当前作用域产生任何声明,内部声明的变量函数不会污染到全局,全局作用域不会因为各种声明变得混乱。比如十个人开发一个程序,如果把变量和函数全部都声明到全局上,并且代码有上万行,那么一旦出现重名的情况,程序会难以调试修改的,比如:
// 这里是第一个人写的
function foo() {
// ...
}
// 使用 foo 写了 200 行代码
// 这里是第二个人写的
function foo() {
// ....
}
// 使用 foo 写了 400 行代码
在他们的代码拼凑到一起前,它们并不知道使用了相同的函数名在开发,当代码合并后,前一个声明会被覆盖,前一个人使用 foo
函数就会出错。为了消除这种影响,就可以使用立即执行表达式:
(function () {
// 这里是第一个人写的
function foo() {
// ...
}
// 使用 foo 写了 200 行代码,
})();
(function () {
// 这里是第二个人写的
function foo() {
// ....
}
// 使用 foo 写了 400 行代码
})();
因为立即执行表达式内部产生了新的函数作用域,内部声明不会干扰到全局,所以两部分代码就可以正常运行。实际开发中,全局上我们只是存放一些模块代码,不会把业务逻辑写到全局上。
递归函数
大家应该都听过这么一个故事:
- 从前有座山,山里有座庙,庙里有个老和尚和一个小和尚,老和尚在给小和尚讲故事,讲的是:
- 从前有座山,山里有座庙,庙里有个老和尚和一个小和尚,老和尚在给小和尚讲故事,讲的是:
- 从前有座山,山里有座庙,庙里有个老和尚和一个小和尚,老和尚在给小和尚讲故事,讲的是:
- ......
现在我们定义一个函数,用来“讲讲”这个故事:
// 讲故事函数
function tellStory() {
console.log("从前有座山,山里有座庙,庙里有个老和尚和一个小和尚,老和尚在给小和尚讲故事,讲的是:");
tellStory();
}
tellStory(); // 开始讲
【代码解释】
上面的代码中,我们让计算机讲述了这个故事,这个故事是讲不完的,程序会因为一直讲这个故事而被挂起,直到内存溢出报错。这种函数调用自己的方式,我们称作 递归
。
递归函数用来处理逻辑的循环,和 for
循环类似,但是思维的点不一样,递归是在上一段逻辑的基础上执行下一段相同的逻辑,是相同逻辑的嵌套,而 for
循环更适合是相同逻辑的并行。
使用递归函数需要注意的是,必须记住设置一个结束的逻辑,不然就会想上面的代码一样,无限执行下去,直到程序报错。我们可以尝试使用递归来遍历一个数组:
var foo = [1, 2, 3, 4, 5];
function loop(a, i) {
i = i || 0;
if (i < a.length) {
console.log(a[i]);
loop(a, i + 1);
}
}
loop(foo);
在上面代码中,我们使用递归函数遍历了数组。在函数执行的时候,我们设置了一个下标用来追踪当前遍历的位置,递归成立的条件是下标不超过数组长度。
备注:递归非常耗费计算机性能,这在任何编程语言中都成立。如果使用递归的层数极高,比如上万层递归的情况,并不建议使用。
回调函数
前面介绍过,函数是引用类型,所以函数也是一个数据,函数可以赋值给变量或属性,并且相互传递:
function foo() {
console.log("foo function");
}
var bar = foo;
var baz = bar;
baz(); // "foo function"
上面代码中,我们把 foo
函数当做数据在变量之间传递,这种传递不仅仅可以在变量之间,还可以在参数传递中,比如:
function foo() {
console.log("this is foo");
}
function doFn(fn) {
fn();
}
doFn(foo);
上面代码会打印 this is foo
,我们把 foo
当做参数传递给 bar
函数,bar
函数通过参数 fn
来接收。这种把函数当做参数传递给另一个函数的行为称作 回调,被传递的函数称作 回调函数。
回调函数的作用在于,你可以在其他函数中,执行其他函数的逻辑。
比如我们需要编写一个数组的过滤器,通过某种规则,过滤掉数组中的部分数据:
function filterArray(arr, rule) {
var re = [];
for (var i = 0; i < arr.length; i++) {
if (rule(arr[i])) {
re[re.length] = arr[i];
}
}
return re;
}
var foo = [1, 2, 3, "A", "B", "C", { name: "foo" }, [1, 2, 3]];
var filterRe = filterArray(foo, function (val) {
return typeof val === "string";
});
console.log(filterRe);
上述代码中,我们自定义了过滤的规则,使用回调函数的方式传入。最后得到一个只有字符串类型数据的数组。
练习
题 1:编写一个递归函数,传入正整数 n,返回菲波那切数列的第 n 项,关于斐波那契数列可以到网上查询其规律。
题 2:在新标准的 JS 中,数组有一个内置函数
forEach
,它被用来遍历数组,用法如下:var arr = ["A", "B", "C", "D"]; arr.forEach(function (val, index) { console.log(val, index); });
上述代码会依次打印
arr
数组项的值和索引下标,尝试编写一个函数myForEach
模拟内置函数forEach
的功能。它有两个参数,第一个参数是被操作的数组,第二个参数是遍历的回调函数。
题 1
基于菲波那切数列的公式,其递归实现如下:
function fib(n) {
if (n <= 1) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
// 可以通过三目运算符简化:
function fib2(n) {
return n <= 1 ? 1 : fib2(n - 1) + fib2(n - 2);
}
注意,递归非常耗费性能,在流程控制章节我们使用 for
循环也编写过菲波那切数列,for
循环非常快,递归之所以慢,是因为递归的层数过多。上面代码的递归层数如下:
fib(0)
执行 1 次函数:
fib(0) => 1
fib(1)
执行 1 次函数:
fib(1) => 1
fib(2)
执行 3 次函数:
fib(2) => fib(1) + fib(0)
=> 1 + 1
=> 2
fib(3)
执行 5 次函数:
fib(3) => fib(2) + fib(1)
=> fib(1) + fib(0) + 1
=> 1 + 1 + 1
=> 3
fib(4)
执行 9 次函数:
fib(4) => fib(3) + fib(2)
=> fib(2) + fib(1) + fib(1) + fib(0)
=> fib(1) + fib(0) + 1 + 1 + 1
=> 1 + 1 + 3
=> 5
fib(5)
执行 15 次函数:
fib(5) => fib(4) + fib(3)
=> fib(3) + fib(2) + fib(2) + fib(1)
=> fib(2) + fib(1) + fib(1) + fib(0) + fib(1) + fib(0) + 1
=> fib(1) + fib(0) + 1 + 1 + 1 + 1 + 1 + 1
=> 1 + 1 + 6
=> 8
......
题 2
function myForEach(arr, fn) {
for (var i = 0; i < arr.length; i++) {
fn(arr[i], i);
}
}