函数

JavaScript函数、回调函数、柯里化

一、函数

函数的概念

函数:就是封装了一段可被重复调用执行的代码块,通过此代码块可以实现大量代码的重复使用。

在js里面,可能会定义非常多的相同代码或者功能相似的代码,这些代码可能需要大量重复使用。

虽然for循环语句也能实现一些简单的重复操作,但是比较具有局限性,此时我们就可以使用JS中的函数。

函数的使用

函数在使用时分为两步:声明函数和调用函数

声明函数

//声明函数
function 函数名(){
    //函数体代码
}

  • function是声明函数的关键字,必须小写
  • 由于函数一般是为了实现某个功能才定义的,所有通常我们将函数名命名为动词,比如getSum

调用函数

//调用函数
函数名();//通过调用函数名来执行函数体代码

  • 调用的时候千万不要忘记添加小括号
  • 口诀:函数不调用,自己不执行

注意:声明函数本身并不会执行代码,只有调用函数时才会执行函数体代码

函数的封装

  • 函数的封装是把一个或者多个功能通过函数的方式封装起来,对外只提供一个简单的函数接口
  • 简单理解:封装类似于将电脑配件整合组装到机箱中(类似快递打包)
// 函数使用分为两步: 声明函数 和 调用函数
// 1、声明函数
    // function 函数名(){
    //     // 函数体
    // }
    function sayHi() {
        console.log('hi~~');
    }
// (1)function 声明函数的关键字  全部小写
// (2)函数是做某件事情,函数名一般是动词 sayHi
// (3)函数不调用自己不执行
// 2、调用函数
// 函数名();
    sayHi();
// 调用函数的时候千万不要忘记加小括号

案例

// 利用函数计算1~100之间的累加和
// 1、声明函数
function getSum() {
    var sum = 0;
    for (var i = 1; i <= 100; i++) {
        sum += i;
    }
    console.log(sum);
}
// 2、调用函数
getSum();

函数的参数

形参和实参

  • 形参:形式上的参数,函数定义的时候传递的参数,当时并不知道是什么
  • 实参:实际上的参数,函数调用的时候传递的参数,实参是传递给形参的

在声明函数时,可以在函数名称后面的小括号中添加一些参数,这些参数被称为形参,而在调用该函数时,同样也需要传递相应的参数,这些参数被称为实参。

参数的作用:在函数内部某些值不能固定,我们可以通过参数在调用函数时传递不同的值进去

按值传递、按引用传递

  • 按值传递:按值传递是一种比较容易理解又使用比较广泛的传参方式,这种方式在传参的时候,在内存中会直接把实参的值复制一份再把副本传递给形参,对于形参的修改并不会影响到实参
  • 按引用传递:由于在js中,引用类型在内存中分两部分存放,实际的值存放在堆中,在栈中会存放引用类型位于堆中的地址,而我们平时操作的,都是通过栈中的地址对对象进行操作的,那么如果使用按引用传递,就意味着操作的是同一个地址,对于形参的修改就会影响到实参。

js中的传参策略

那么按照上面的分析,可能会有人认为,在js中,对于值类型是按值传递,对于引用类型是按引用传递,然而,这是错误的。事实上,在js中,不管对于值类型还是引用类类型,都是按值传递的,区别在于,对于值类型,传参发生时,复制的是类型本身的值,而对于引用类型,复制的是类型的地址。我们来看下面这段代码,可以用来否定引用类型是按引用传参这个观点。

var testC={};
function testObject(example){
    example={b:1};
}
testObject(testC)
console.log(testC);               //输出{},实参并没有改变

通过上面的代码我们可以看出,如果是按引用传参,那么直接修改形参,是会对实参造成影响的,但是我们发现事实上并没有,为了方便理解,下面给出JavaScript中值类型和引用类型进行传参时在内存中的实际复制情况:

值类型

Untitled

引用类型

Untitled

对于js中的变量,值类型存放在栈中,引用类型的地址存放在栈中,对应的值存放在堆中。当传参发生的时候,值类型会直接将栈中的值进行复制,形参和实参此时实际上是两个完全不相干的变量。对于引用类型,传参发生时,会将实参变量位于栈中的地址进行复制,此时栈中会有两个指向同一个堆地址的指针。

我们可以把ECMAScript函数的参数想象成局部变量。在向参数传递基本类型的值时,被传递的值被复制给一个局部变量(即命名参数,或者用ECMAScript的概念来说,就是arguments对象中的一个元素)。在向参数传递引用类型时,会把这个值在内存中的地址(指针)复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。

// 1、函数可以重复相同的代码
function cook() {
    console.log('酸辣土豆丝');
}
cook();
cook();
// 2、我们可以利用函数的参数实现函数重复不同的代码
function 函数名(形参1, 形参2...) { //在声明函数的小括号里面是 形参 (形式上的参数)

}
函数名(实参1, 实参2...); //在函数调用的小括号里面是实参(实际的参数)
// 3、形参和实参的执行过程
function cook(aru) { //形参是接受实参的 aru='酸辣土豆丝' 形参类似于一个变量
    console.log(aru);
}
cook('酸辣土豆丝');
cook('大肘子');
// 4、函数的参数可以有,也可以没有 个数不限

// 1、利用函数求任意两个数的和
function getSum(num1, num2) {
    console.log(num1 + num2);
}
getSum(1, 3);
getSum(3, 8);
// 2、利用函数求任意两个数之间的和
function getSums(start, end) {
    var sum = 0;
    for (var i = start; i <= end; i++) {
        sum += i;
    }
    console.log(sum);
}
getSums(1, 100);
getSums(1, 10);
// 3、注意点
// (1)多个参数之间用逗号隔开
// (2)形参可以看做是不用声明的变量

函数形参和实参合数不匹配问题

function sum(num1, num2) {
    console.log(num1 + num2);
}
sum(100,200);            //形参和实参个数相等,输出正确结果
sum(100,400,500,700);    //实参个数多于形参,只取到形参的个数
sum(200);                //实参个数少于形参,多的形参定义为undefined,结果为NaN

注意:在JavaScript中,形参的默认值是undefined

小结

  • 函数可以带参数也可以不带参数
  • 声明函数的时候,函数名括号里面的是形参,形参的默认值是undefined
  • 调用函数的时候,函数名括号里面的是实参
  • 多个参数中间用逗号分隔
  • 形参的个数可以和实参个数不匹配,但是结果不可预计,我们尽量要匹配

函数的返回值

return语句

有的时候,我们会希望函数将值返回给调用者,此时通过使用return语句就可以实现。

// 1.函数是做某件事或者实现某种功能
function cook(aru) {
    console.log(aru);
}
cook('大肘子');
// 2.函数的返回值格式
function 函数名() {
    return 需要返回的结果;
}
函数名();
// (1)我们函数只是实现某种功能,最终的结果需要返回给函数的调用者函数名() 通过return实现的
// (2)只要函数遇到return 就把后面的结果 返回给函数的调用者 函数名()=return后面的结果
// 3.代码验证
function getResult() {
    return 666;
}
getResult(); //getResult()=666
console.log(getResult());

function cook(aru) {
    return aru;
}
console.log(cook('大肘子'));

// 4.求任意两个数的和
function getSum(num1, num2) {
    return num1 + num2;
}
console.log(getSum(1, 2));

return终止函数

return语句之后的代码不被执行

return的返回值

return只能返回一个值,如果用逗号隔开多个值,以最后一个为准

函数没有return返回undefined

函数都是有返回值的

  • 如果有return则返回return后面的值
  • 如果没有return则返回undefined

break,continue,return的区别

  • break:结束当前的循环体(如for,while)
  • continue:跳出本次循环,继续执行下次循环(如for,while)
  • return:不仅可以退出循环,还能够返回return语句中的值,同时还可以结束当前的函数体内的代码
// 函数返回值注意事项
// 1.return终止函数
    function getSum(num1, num2) {
        return num1 + num2; //return后面的代码不会被执行
        alert('我是不会被执行的!')
    }
    console.log(getSum(1, 2));
// 2.return 只能返回一个值
    function fn(num1, num2) {
        return num1, num2; //返回的结果是最后一个值
    }
    console.log(fn(1, 2));
// 3.我们求任意两个数的 加减乘除结果
    function getResult(num1, num2) {
        return [num1 + num2, num1 - num2, num1 * num2, num1 / num2];
    }
    var re = getResult(1, 2); //返回的是一个数组
    console.log(re);
// 4.我们的函数如果有return 则返回的是 return 后面的值,如果函数没有 return 则返回undefined
    function fun1() {
        return 666;
    }
    console.log(fun1); //返回666

    function fun2() {

    }
    console.log(fun2()); //函数返回的结果是undefined

arguments的使用

当我们不确定有多少个参数传递的时候,可以用arguments来获取,在JavaScript中,arguments实际上它是当前函数一个内置对象,所有函数都内置了一个arguments对象,arguments对象中存储了传递的所有实参。

arguments展示形式是一个伪数组,因此可以进行遍历,伪数组具有以下特点:

  • 具有length属性
  • 按索引方式存储数据
  • 不具有数组的push,pop等方法
// arguments 的使用  只有函数才有arguments对象 而且是每个函数都内置好了这个arguments
    function fn() {
        console.log(arguments); //里面存储了所有传递过来的实参
        console.log(arguments.length);
        console.log(arguments[2]);
// 我们可以按照数组的方式遍历arguments
        for (var i = 0; i < arguments.length; i++) {
            console.log(arguments[i]);
        }
    }
    fn(1, 2, 3);
// 伪数组 并不是真正意义上的数组
// 1.具有数组的length属性
// 2.按照索引的方式进行存储的
// 3.它没有真正数组的一些方法 pop()  push()等等

案例:利用函数判断闰年

// 利用函数判断闰年
function isRunYear(year) {
    // 如果是闰年我们返回 true 否则 返回false
    var flag = false;
    if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {
        flag = true;
    }
    return flag;
}
console.log(isRunYear(2000));  //true
console.log(isRunYear(1999));  //false

函数可以调用另外一个函数

因为每个函数都是独立代码块,用于完成特殊任务,因此经常会用到函数相互调用的情况

// 函数是可以相互调用的
function fn1() {
    console.log(11);
    fn2(); //在fn1函数里面调用了fn2函数
}
fn1();

function fn2() {
    console.log(22);
}   //11,22

function fn1() {
    console.log(111);
    fn2();
    console.log('fn1');
}

function fn2() {
    console.log(222);
    console.log('fn2');
}
fn1();   //111,222,fn2, fn1

函数的2种声明方式

// 函数的2种声明方式
// 1.利用函数关键字自定义函数(命名函数)
function fn() {

}
fn();
// 2.函数表达式(匿名函数)
var 变量名 = function() {}
var fun = function(aru) {
    console.log('我是函数表达式');
    console.log(aru);
}
fun('blue');
// (1)fun是变量名 不是函数名
// (2)函数表达式声明方式跟声明变量差不多,只不过变量里面存的是值 而 函数表达式里面存的是函数
// (3)函数表达式也可以进行传递参数

二、回调函数(callback)

回调函数

函数A作为参数(函数引用)传递到另一个函数B中,并且这个函数B执行函数A。我们就说函数A叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。

三、柯里化(curry)

什么是柯里化

简单来说,就是构建一种可以实现参数部分应用的函数。直观点,一个函数需要 n 多参数,通过柯里化实现,我们可以多次调用函数每次只传部分参数,最终实现完整功能。最典型的场景是要实现一个函数,本函数的一部分参数每次都是一样的。

使用场景

最典型的场景是要实现一个函数,本函数的一部分参数每次都是一样的。

例子一,hello world:

function hello() {
    console.log('hello world');
}

hello(); // hello world

需求改为:say hello to anyone

// curry
function hello() {
    return function(name) {
        console.log('hello ' + name);
    };
}

hello();
hello()('Allen'); // hello Allen
var b = hello();
b('Bob'); // hello Bob
b('John'); // hello John

需求又改为:say anyword to anyone

// curry
function hello(word) {
    return function(name) {
        console.log(word, ',', name);
    };
}

var b = hello('Hi');
b('Bob'); // Hi , Bob
b('John'); // Hi , John

var c = hello('Bye');
c('John'); // Bye , John

需求又改了:word + line + name

// flexible curry !
function hello(word) {
    return function(name) {
        return function(line) {
            console.log(word, ',', name, ',', line);
        };
    };
}

var b = hello('Hi');
var toBob = b('Bob');
var toAllen = b('toAllen');

toBob('good day tody !'); // Hi , Bob , good day tody !
toBob('Have you eaten yet ?'); // Hi , Bob , Have you eaten yet ?
toAllen('get out of here !'); // Hi , toAllen , get out of here !

var c = hello('Bye');
c('John')('see you tomorrow ~'); // Bye , John , see you tomorrow ~

var d = hello('Hey')('Shawn');
d('this is my daily report .'); // Hey , Shawn , this is my daily report .
d('this is my monthly report .'); // Hey , Shawn , this is my monthly report .

封装柯里化

初步封装

// 初步封装
var currying = function(fn) {
    // args 获取第一个方法内的全部参数
    var args = Array.prototype.slice.call(arguments, 1)
    return function() {
        // 将后面方法里的全部参数和args进行合并
        var newArgs = args.concat(Array.prototype.slice.call(arguments))
        // 把合并后的参数通过apply作为fn的参数并执行
        return fn.apply(this, newArgs)
    }
}

这边首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。

但是好像还有些什么缺陷,这样返回的话其实只能多扩展一个参数,currying(a)(b)(c)这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。

// 支持多参数传递
function progressCurrying(fn, args) {

    var _this = this
    var len = fn.length;
    var args = args || [];

    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);

        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }

        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}

这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。