JS原理 - 一些函数概念

匿名函数、自执行函数、高阶函数,柯里化

匿名函数

没有函数名的函数被称作匿名函数,形式为:function(){},匿名函数能让闭包更加简洁。

使用具名函数进行闭包:

function foo(){
    let a = 0;
    function child(){
        a+=1;
        console.log(a);
    }
    return child
}

const x = foo();
x(); // 1
x(); // 2

使用匿名函数进行闭包:

function foo(){
    let a = 0;
    return function(){
        a+=1;
        console.log(a);
    }
}

const x = foo();
x(); // 1
x(); // 2

自执行函数

自执行函数,顾名思义,是会自己执行的函数,不需要额外的调用,形式为:(function(){})()

因为匿名函数拥有独立的词法作用域,避免外部访问自执行函数内的变量,所以在自执行函数内的变量不会污染全局作用域。

上面的匿名函数闭包可以使用自执行函数进行修改:

const x = (function(){
    let a = 0;
    return function(){
        a+=1;
        console.log(a);
    }
})();
x(); // 1
x(); // 2

再来看一个古老的面试题:

for (var i = 0; i < 5; i++) {
    setTimeout(function(){
        console.log(i);
    }, 1000);
}

因为宏任务、微任务的工作原理,会先执行完 for 循环之后再去执行定时器,所以上面的代码会打印出 5 5 5 5 5。

那么如果要让他打印出 0 1 2 3 4 需要怎么样修改代码?

首先 JS 中调用函数传递参数是按值传递的,传入的参数会被复制一份,然后再创建函数执行上下文,所以拿到的都是当时的值,同时产生一个闭包,将值保存下来。

for (var i = 0; i < 5; i++) {
    function foo(a) {
        setTimeout(function(){  // 这个函数后面还会被调用,相当于是被return出去
            console.log(a);
        }, 1000);
    };
    foo(i)
}

// 使用立即执行函数去优化一下代码
for (var i = 0; i < 5; i++) {
    (function (a) {
        setTimeout(function(){
            console.log(a);
        }, 1000);
    })(i);
}

// 这样也是可以的
for (var i = 0; i < 5; i++) {
  setTimeout((function(a){
    return function(){
      console.log(a);
    }
  })(i), 1000);
}

再来看一个例子:

function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push(function () {
            return i * i;
        });
    }
    return arr;
}

var results = count();
var f1 = results[0];  
var f2 = results[1];  
var f3 = results[2];

f1();  // 16
f2();  // 16
f3();  // 16

这个理解很简单,往数组里 push 的是一个未执行的函数,所以最后执行出的结果都是 4 * 4 = 16,那如果想要让打印出来的结果分别是:1、4、9 ,需要如何修改函数?

第一种:将 push 方法包裹在自执行函数内,

function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        (function(a){  // 按值传递
            arr.push(function () {  // 放入数组后再调用,相当于return了函数出去
                return a * a;
            });
        })(i)
    }
    return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];

f1();  // 1
f2();  // 4
f3();  // 9

第二种:将 push 的回调函数用自执行函数进行嵌套,然后返回一个匿名函数,非常标准的闭包

function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push((function (n) {
            return function () {
                return n * n;
            }
        })(i));
    }
    return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];

f1(); // 1
f2(); // 4
f3(); // 9

高阶函数

满足下列条件之一的,可以被称作高阶函数:

  • 函数可以作为参数传递

    var arr = [1, 55, 23, 6];
    arr.sort(function(a, b){
        return a - b;
    })
    
  • 函数可以作为返回值输出

    function foo(){
        let a = 0;
        function child(){
            a+=1;
            console.log(a);
        }
        return child
    }
    

函数柯里化

柯里化是一种函数的转换,他是将一个函数从可调用的f(a,b,c)转换为可调用的f(a)(b)(c),柯里化不会调用函数,只是对函数进行转换。

举个例子:现在有一个 add 函数

function add(a, b){
    return a + b;
}

现在要对他实行柯里化,创建一个 curry 函数,将函数作为他的传入参数,然后返回一个函数:

function curry(f) { 
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

const curryAdd = curry(add);

curryAdd(1)(2);  // 3

可以看到,这里还是有闭包的思想存在,在调用curryAdd(1)的时候,会保留他的词法作用域,返回一个 function(b)

那么看起来让函数更加复杂的柯里化有什么意义呢?

举个例子:如果一类需求的 x 都是1,如果不使用柯里化,那么每一次调用,都是add(1,n)的形式,如果用柯里化的形式add(1)(n)add(1)的结果可以存储下来,减少重复运算。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);

increment(2);  // 3

callee & caller

callee

在函数的内部,有两个特殊的对象:arguments 和 this。其中 arguments 是一个类似数组的对象,包含着传入函数的所有参数。

虽然 arguments 的主要用途是保存函数参数,但这个对象有一个属性 callee,该属性是一个指针,指向拥有这个 arguments 对象的函数。

例如下面的例子中,在函数内调用了自身,如果外层函数修改了名称,而其函数内部的名称没有做修改,那么就会出现错误,使用 callee 就能解决这个问题。

function factorial(num){
    if(num <= 1){
        return 1;
    }else{
        return num * factorial(num-1);
    }
}
// 修改为
function factorial(num){
    if(num <= 1){
        return 1;
    }else{
        return num * arguments.callee(num-1);
    }
}

caller

caller是函数对象的一个属性,该属性保存着调用当前函数的函数的引用(指向当前函数的直接父函数)。

function a(){
    b();
};
function b(){
    console.info(b.caller);
};

a();  // ƒ a(){ b() }

callee 和 caller 也可以结合起来使用,例如下述代码能实现和上面代码一样的功能,且脱离了对函数名称的依赖。

function a(){
    b();
};
function b(){
    console.info(arguments.callee.caller);
};

a();  // ƒ a(){ b() }

JS原理 - 一些函数概念

https://hashencode.github.io/post/22071/

作者

BiteByte

发布于

2023-01-03

更新于

2024-01-11

许可协议