【JavaScript】函数进阶


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 关键字来定义函数,而不使用函数表达式来定义函数



变量 & 同名函数

如果声明了同名的变量和函数,我们可以先简单的认为:

  1. 若变量没有赋值,则优先级:函数 > 变量
  2. 若变量有赋值,则优先级:变量 > 函数
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 会自动执行,且只能执行一次,执行完后,调用栈会自动销毁
  1. 用括号把函数括起来,并在后面紧跟参数:(function fun(){})()
(function sum(a, b) {
    console.log(a + b);
})(10, 10); // 20
  1. 用括号把函数和参数都括起来:(function fun(){}())
(function sum(a, b) {
    console.log(a + b);
}(10, 10)); // 20
  1. 在函数前加 + - ~ !+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



ES6 函数



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JS.Huang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值