ES5 函数基础
函数表达式
函数表达式是一种定义函数的方法,就是在函数声明时,把函数赋值给一个变量
有名函数表达式
let funExp = function funName() {}
在函数外部只能使用 funExp
来调用该函数,funName
只能在函数内部使用
这时可以认为 funExp
才是真正地函数名
let funExp = function funName() {
console.log('funName', funName); // 在函数内部使用 funName 调用该函数
}
funExp(); // 在函数外部使用 funExp 调用该函数
匿名函数表达式
let funExp = function () {}
日常开发中,使用匿名函数表达式居多。毕竟函数表达式的函数名,作用不大对吧
let funExp = function (a, b) {
console.log(a + b); // 3
}
funExp(1, 2);
函数表达式 & 普通函数
- 在函数声明时,把函数赋值给变量:在函数外部,只能通过 [变量名] 调用该函数
- 在其他时候(非函数声明时),把函数赋值给变量:在函数外部,可以通过 [函数名] / [变量名] 调用该函数
let funExp = function funName() { // 函数表达式声明函数
return null;
}
console.log(funExp); // 在函数外,只能用 funExp 调用函数
function funName() { // 声明函数
return null;
}
let varName = funName; // 把函数赋值给变量
console.log(funName); // funName / varName 都可以调用函数
console.log(varName);
函数声明的提升
JS 在执行代码前,会有一个编译过程,先执行函数声明语句,再从上往下执行其他语句
所以,function
定义在哪里都可以,程序总能找到这个函数
fun(); // 先调用函数
function fun() { // 再定义
console.log("我执行了!"); // 我执行了!
}
注意:函数声明可以被提升,但是函数表达式不会被提升
函数表达式是将函数赋值给一个变量,所以会进行变量提升。而变量提升的是变量的声明语句,不会提升赋值语句
所以,一般情况下我们都是使用 function
关键字来定义函数,而不使用函数表达式来定义函数
变量 & 同名函数
如果声明了同名的变量和函数,我们可以先简单的认为:
- 若变量没有赋值,则优先级:函数 > 变量
- 若变量有赋值,则优先级:变量 > 函数
var num; // 声明变量 num
function num() {}; // 声明同名函数
console.log(num); // [Function: num]
var num = 5; // 声明变量 num 并赋值
function num() {}; // 声明同名函数
console.log(num); // 5
- 这里用的是
var
,因为不可以使用let
声明同名变量
Eg:优先级问题 & 变量与函数的提升问题
foo() // 调用函数,输出 1
function foo() { // 声明函数 foo
console.log(1);
}
var foo = function () { // 函数表达式声明函数 foo
console.log(2);
}
foo() // 调用函数,输出 2
上面的的代码等价于:
var foo; // 变量声明的提升
function foo() { // 同名函数声明的提升
console.log(1);
}
foo(); // 调用函数,因为变量没有赋值,函数的优先级比较高
foo = function () { // 函数表达式声明函数,相当于给 foo 进行赋值
console.log(2);
}
foo(); // 调用的是新的函数,因为变量 foo 已经被赋值了,原函数的优先级比较低
执行栈 & 递归
执行栈
- 运⾏单层函数时:函数进栈执行,执行完后出栈并销毁,然后下⼀个函数进栈执行…
- 当有函数嵌套调⽤时,栈中就会堆积栈帧:
function task1() {
console.log('task1 start');
task2();
console.log('task2 end');
}
function task2() {
console.log('task2 start');
task3();
console.log('task3 end');
}
function task3() {
console.log('task3 start');
}
task1();
console.log('task1 start');
上例中,函数 task1
进栈执行,输出 task1 start
,在 task1
执行过程中,函数 task2
也进栈执行,输出 task2 start
,在 task2
执行过程中,函数 task3
也进栈执行,输出 task3 start
;
ok task3
执行完毕,出栈并销毁,输出 task3 end
,然后 task2
执行完毕,出栈并销毁,输出 task2 end
,最后 task1
也执行完毕,出栈并销毁,然后 console.log('task1 start')
进栈执行,输出 task1 start
。
递归
- 递归其实就是函数自己调用自己
- 递归的效率比较低,比较复杂的程序最好不用递归
eg:使用递归计算累加
function sum(num) {
if (num == 1) { // 设置结束递归的条件
return 1;
} else {
return num + sum(num - 1); // 设置递归
}
}
let result = sum(10);
console.log(result); // 55
执行栈的深度
风险问题:递归函数就可以看成是在⼀个函数中嵌套 n 层,在执⾏过程中会触发⼤量的栈帧堆积,如果处理的数据过⼤,会导致执⾏栈的⾼度不够放置新的栈帧,造成栈溢出
不同的浏览器和 JS 引擎有着不同的执⾏栈深度,这⾥以 Edge 为例,来测试一下其执行栈的深度:
let i = 0;
function task() {
i++;
console.log(`递归了${i}次`)
task();
console.log(`完成了${i}次递归`);
}
task();
我们发现,Edge 的执行栈深度为 11443
如何跨越执行栈深度的限制
我们可以通过异步操作,跨越执行栈深度的限制。可以将代码做如下更改:
var i = 0;
function task() {
i++
console.log(`递归了${i}次`)
// 使⽤异步操作避免递归的溢出
setTimeout(task, 0);
console.log(`完成了${i}次递归`);
}
task()
原理:使⽤异步任务去调⽤递归中的函数,那么这个函数在执⾏的时候就不只使⽤执行栈进⾏操作了
- 如果直接递归,会出现栈帧无限叠加导致最终超过执行栈的深度
- 如果有了异步操作,函数在执行过程中发现异步任务,会将其异步执行,异步任务执行完后,被放在任务队列中等待,等到执行栈中原本正在执行的函数执行完后,任务队列中的函数才会进栈执行;然后函数在执行过程中发现异步操作…
如此,执行栈中将永远只有一个函数正在执行,就是说栈帧永远只有一个,就不会出现栈溢出的现象
不过异步递归⽆法保证运⾏速度,在实际的⼯作场景中,如果要考虑性能问题,还需使⽤ while
循环等解决⽅案,来保证运⾏效率。
在实际⼯作场景中,应尽量避免使用递归,因为递归的性能远不及指针循环。
IIFE
- Immediately-invoked function expression - 即时调用函数表达式
- IIFE 会自动执行,且只能执行一次,执行完后,调用栈会自动销毁
- 用括号把函数括起来,并在后面紧跟参数:
(function fun(){})()
(function sum(a, b) {
console.log(a + b);
})(10, 10); // 20
- 用括号把函数和参数都括起来:
(function fun(){}())
(function sum(a, b) {
console.log(a + b);
}(10, 10)); // 20
- 在函数前加
+
-
~
!
:+function fun(){}()
+ function sum(a, b) {
console.log(a + b);
}(10, 10); // 20
原理很简单,其实就是把函数降级为函数表达式,就可以通过 ()
立刻调用函数啦
用这种方法定义的函数,函数名是无效的,即其他地方调用不了这个函数。所以,IIFE 里面的函数其实都是匿名函数
+ function (a, b) {
console.log(a + b)
}(10, 10); // 20
Eg:设计一个函数,这个函数接收三个参数,返回的是前两个数字中比较大的那个数字,与第三个数字的和
function sum(a, b, c) {
return (function (a, b) { // 使用 IIFE
return a >= b ? a : b;
})(a, b) + c;
}
console.log(sum(1, 2, 3)); // 5
闭包
可以简单地认为:一个外部函数将内部函数作为返回值的话,就形成一个闭包
function outer() {
let a = 333;
function inner() {
console.log(a);
}
return inner; // 将内部函数 inner 作为返回值
}
let inn = outer(); // 将函数返回值 inner 函数赋值给变量 inn
inn(); // 调用内部函数,输出 333
在作用域链中,没有用的 AO 会被垃圾回收机制清除。而在闭包中,因为内部函数被赋值给了外部的变量,所以这条作用域链不会被销毁
- 闭包的弊端:因为闭包的作用域链不会被销毁,所以会一直占用内存,容易造成内存泄漏(占用的多,可用的就少了)
- 闭包的优点:可以长久的存储某些数据,并对其进行操控
// GO {
// inn: fun outer
// outer: fun
// }
function outer() {
// AO {
// a: 100
// inner: fun
// }
let a = 100;
function inner() {
// AO {}
a++;
console.log(a);
}
return inner; // 返回 inner 函数
}
let inn = outer(); // 把 inner 函数赋值给变量 inn
inn(); // 101
inn(); // 102, 每次执行 inn, 都会直接进入 inner 的作用域中,这条作用域链不会被销毁
- 每次生成的闭包都是新的!互不干扰!
function outer() {
let count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
let inn1 = outer(); // 形成第一个闭包
inn1(); // 1
inn1(); // 2
let inn2 = outer(); // 形成第二个闭包
inn2(); // 1
inn2(); // 2
闭包的变种
闭包:只要在外部有与内部函数的联系,内部函数的这条作用域链就会一直存在
除了 return
以外,还有一些方法可以维持内部函数与外部的联系:通过函数表达式,将内部函数赋值给外部变量
let inner; // 定义全局变量 inner
let aa = 300; // 定义全局变量 aa
function outer() { // 定义函数 outer
let aa = 200;
inner = function () { // 函数表达式定义内部函数
aa++;
console.log(aa);
}
}
outer(); // outer 必须先运行,否则 inner 不是一个函数,就不会形成闭包
inner(); // 直接进入 inner 作用域,打印 201
inner(); // 直接进入 inner 作用域,打印 202
闭包的作用
我们可以对外暴露一些方法,使外部只能通过我们提供的方法控制数据
function controlNum() {
let num = 100;
return {
add() {
num++
},
reduce() {
num--;
},
see: () => num
}
}
let outNum = controlNum();
console.log(outNum.see()); // 100
outNum.add();
console.log(outNum.see()); // 101
outNum.reduce();
console.log(outNum.see()); // 100