函数
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中值类型和引用类型进行传参时在内存中的实际复制情况:
值类型
引用类型
对于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,就会继续执行递归。