第七章 函数
1 函数的概念
1.1 什么是函数
函数具有某种特定功能的代码块。
函数其实本质也是一种数据,属于对象数据类型。
1.2 为什么要有函数
1)解决代码的冗余问题,形成代码复用。
2)可以把整个代码项目,通过函数模块化。
3) 封装代码,让函数内部的代码对外部不可见。
2. 函数的组成
函数的声明:
函数的调用:
函数由如下部分组成:
- 函数名,命名规则同变量名一致。
- 函数体, 函数的内容,代码块。
- 参数, 分为形参和实参。
- 返回值, 函数调用表达式的结果
3 定义函数的三种方式
function关键字方式/字面量方式
function 函数名() { } function 函数名(参数) { }
表达式方式
var 函数名 = function(){ } var 函数名 = function(参数) { }
Function构造函数方式
var 函数名 = new Function('函数体'); var 函数名 = new Function('参数', '函数体')
4 函数调用
1) 在函数名后面加上 () 就是对函数的调用,函数内的代码会执行。
2) 函数名后面不加() 不会调用函数,函数内的代码也不会执行;函数名本质上是个变量名,通过函数名可以引用到函数本身。
5 函数的返回值
5.1 返回值
1)函数名()
被称之为函数调用表达式, 表表达式的值就是函数的返回值。
2)在函数体内,return
右边的表达式(或变量、直接量)便是函数的返回值。
3)函数体内没写 return
或者 return
的右边是空的,默认默认会返回 undefined
。
4)return
除了设置返回值外,还可以结束函数的执行,return 之后的代码不会执行。
5.2 那些函数需要些返回值
什么样的函数需要写返回值?
如果函数的作用是进行某种计算,得到的计算结果最后以返回值的形式返回。
什么样的函数不需要返回值?
函数的功能是实现某个具体的操作(界面操作),无需返回值。
6 函数的参数
6.1 形参和实参
形参: 声明函数的时候,给的参数, 类似于变量名;在声明函数的时候,参数是没有值。
实参:调用函数是给的参数; 实参会按照顺序赋值给形参。
6.2 形参和实参的数量问题
正常情况下,实参数量应该等于形参数量。
如果实参数量大于形参数量, 多出来的实参,将被忽略。
如果实参数量小于形参数量, 有的形参没有对应的实参,取默认值 undefined。
6.3 形参的默认值
JS函数允许形参有默认值,有默认值的形参,在调用函数的时候,可以没有与之对应的实参!
如何实现形参的默认值?
旧版语法:
function demo(a,b) {
// 判断形参的值,是否是undefined,如果是undefined说明函数调用的时候没有给值,可以设置默认值。
if (b === undefined) {
b = 默认值
}
}
新版语法:
function demo(a, b=默认值) {
}
注意: 有默认值的形参一定要放在后面!
6.4 arguments
arguments 只能在函数内使用。
arguments 是一个类数组对象,具有数组的一些特性。
arguments可以获取所有的实参,所以我门想获取实参的话有两种方式:①用形参;②使用arguments。
可以用来定义可变参数数量的函数:如计算所有参数和,取参数中的最大值,取参数中的最小值,求所有参数平均数。
/**
* 取所有参数里面的最大值
*/
function max() {
//设置遍历 默认值的最大值
var res = arguments[0];
// 循环比较
for (var i = 0; i < arguments.length; i ++) {
if (arguments[i] > res) {
res = arguments[i];
}
}
// 返回结果
return res;
}
7 函数的嵌套
函数体内是可以再嵌套函数的。
/**
* 冒泡排序
* @params arr Array 需要进行排序的数组
* @params isUp boolean 是否从大到小; 默认值是false(从小到大)
* @return 排好序的数组
*/
function sortArr(arr, isUp) {
// 数组遍历
for (var i = 0; i < arr.length; i ++) {
// 进行比较。把最大的元素搞到最后面取
for (var j = 0; j < arr.length-1-i; j ++) {
// 先判断是从大到小还是从小到大
if (isUp) { //从大到小
//如果前面的元素比后面的小,进行交换
if (arr[j] < arr[j+1]) {
exchange();
}
} else { //从小到大
//如果前面的元素比后面的大,进行交换
if (arr[j] > arr[j+1]) {
exchange();
}
}
}
}
// 把排好序的数组返回
return arr;
// 交换
function exchange(){
var temp = arr[j]; //第三方变量
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
8 作用域
8.1 变量的作用域
作用域是变量的可作用范围,变量只有在自己的作用域下才会生效。
函数会产生作用域,在函数内定义的变量只能在函数内使用。
8.2 作用域分类
局部作用域: 函数内定义的变量和形参的作用域就是局部作用域;这样的变量称之为局部变量。
全局作用域: 在函数外面定义的变量的作用域是全局作用域;这样的变量称之为全局变量。
块级作用域: 在代码块中定义的变量的作用域是块级作用域;这样的变量称之为块级变量。 ES6才支持
局部变量 只能在定义变量的函数内使用; 全局变量 在任意地方都可以使用。
注意:
函数内的形参也是局部变量,作用域范围就是所在函数。
了解:
函数内不使用关键字
var
声明的变量会被当做全局变量,但不建议这么做,在严格模式下,不使用关键字var
就声明变量,会报错!
8.3 作用域链
① 什么是作用域链
1) 函数会限制变量的作用域范围,而函数内是可以再嵌套函数的,函数的层层嵌套,就形成了一个作用域链。
2)作用域链描述的是程序在执行过程当中寻找变量的过程。
② 作用域链寻找变量的过程
当函数内使用某个变量的时候,会按照如下过程找到该变量:
1) 先从自身所在作用域去查找,如果没有再从上级作用域当中去查找,直到找到全局作用域当中。
2)如果其中有找到,就不会再往上查找,直接使用。
3)如果都没有找到,那么就会报引用错误提示变量没有定义。
③ 注意:
一个变量的作用域只与函数声明的位置有关,与函数调用的位置无关!8.4 变量作用域原理
8.5 作用域面试题
请写出以下程序的输出内容
var num = 10;
function fun() {
var num = 20;
fun2();
}
function fun2() {
console.log(num);
}
fun();
9 变量提升和函数提升
9.1 变量提升
JS 会把变量的声明提升到本作用域的最前面。(只是提前声明了变量,但并没有给变量赋值)
function demo(){
console.log(num); // undefined
var num = 200; //声明局部变量
console.log(num); // 200
}
demo();
9.2 函数提升
JS 会把函数连声明带值提升到本作用域的最前面, 函数可以在函数声明之前调用。
只有字面量方式(function关键字方式)声明的函数才能函数提升,表达式方式和构造函数方式声明的函数,只能提升声明没有值(与变量提升相同)!
相对于变量提升,函数提升权重更高(存在与函数同名变量的情况下)
console.log(a); // undefined
console.log(fn); // 函数提升,不但提升了声明,带着值也提升;
fn(); //可以调用
console.log(demo); //变量提升,提升了变量的声明,没有值
// demo(); 无法调用
var a = 100;
// 字面量方式声明
function fn() {
console.log('我是fn');
}
// 表达式方式声明
var demo = function(){
console.log('我是demo');
};
9.3 预解析
变量和函数之所以会提升,是因为程序在代码执行之前会先进行预解析。
与解析遵循如下规则:
- 预解析先去解析函数声明定义的函数,整体会被提升。
- 再去解析带
var
的变量。 - 函数重名会覆盖,变量重名会忽略。
- 变量如果不带var,变量是不会进行预解析的;只有带var的变量才会进行预解析。
- 表达式方式和构造函数方式定义的函数也是当做变量去解析。
9.4 变量提升面试题
// ① --------------------------------------------------------
alert(a);
a = 0;
// ② --------------------------------------------------------
alert(a);
var a = 0;
alert(a);
// ③ --------------------------------------------------------
alert(a);
var a = '我是变量';
function a(){ alert('我是函数') }
alert(a);
// ④ --------------------------------------------------------
alert(a);
a++;
alert(a);
var a = '我是变量';
function a(){ alert('我是函数') }
alert(a)
// ⑤ --------------------------------------------------------
alert(a);
var a = 0;
alert(a);
function fn(){
alert(a);
var a = 1;
alert(a);
}
fn()
alert(a);
10 自调用函数 IIFE
10.1 匿名函数
没有名字的函数称之为 匿名函数。
function() {
//匿名函数
}
匿名函数声明玩之后要立即调用,否则没有意义。
10.2 自调用函数
函数声明完立即调用,称之为自调用函数,也叫立即调用函数,英文简称 IIFE,英文全称 Immediately Invoked Function Expression。
// 函数允许匿名,但是匿名的函数要立即使用
// 自调用函数,立即调用函数
(function(){
console.log('哈哈哈,我被调用了');
})();
// 自调用函数 传参
(function(a, b){
console.log(a+'和'+b+'跳舞');
})('曹操', '刘姥姥');
// 当然不匿名的自调用函数也是可以的,不过没有意义
(function fn(){
console.log('fn 被调用了');
})();
注意:
两个连续的自调用函数,之间必须加分号,告诉浏览器是不同的函数,否则会有语法错误。
或者,在后面的自调用函数前加
!
等没有副作用的一元运算符。
10.3 自调用函数的作用
1)减少全局变量的使用,把自己代码或者每个特效的代码写到一个自调用函数中, 防止外部命名空间污染(全局变量污染)
2)隐藏内部代码暴露接口,实现模块化开发。
11 回调函数
11.1 什么是回调函数
回调函数满足三个条件:
1)函数是我定义的。
2)我没有调用。
3)函数最终执行了。
满足以上三个条件的函数就是回调函数。
11.2 常见使用回调函数的地方
作为其他函数的参数,函数的参数可以是个函数,而作为参数的那个函数就被称作回调函数。
2)事件函数
3)定时器函数
4)ajax 的回调函数
4)生命周期钩子函数
注意:
匿名函数很适合做回调函数,也就是回调函数很多时候是个匿名函数。
11.3 实现一个回调函数
1)声明一个函数 fn,函数的参数类型要求是函数。
2)fn 的函数体内,调用传递进来的回调函数。
3)fn 内部调用回调函数的时候,还可以给回调函数传个实参。
// 声明函数,参数的类型要求是函数
function fun(callback) {
//使用一下参数 调用参数 调用回调韩
callback();
}
// 调用fun,传一个匿名函数进去
fun(function(){
console.log('啊,作为一个回调函数,我被调用了');
});
// -------------------------------------
// 声明函数,参数的类型要求是函数
function demo(callback) {
callback(100, 200); // 调用回调函数的时候,还给回调函数传个实参
}
// 接收匿名函数作为参数,回调函数自己得定义两个形参
demo(function(a,b){
console.log(a, b);
});
//接收非匿名函数作为参数
demo(fn1);
function fn1(a, b) {
console.log(a+b);
}
/**
* @param num1
* @param num2
* @param call
*/
function progress(num1, num2, call) {
call(num1, num2);
}
progress(100, 200, function(a,b){console.log(a+b)});
progress(100, 200, function(a,b){console.log(a*b)});
12 递归函数
12.1 递归函数的概念
一个函数的内部如果又调用了自己,称作是函数的递归调用,这样的函数就是递归函数。
12.2 递归函数成功的条件
1)必须有一个明显的结束条件。
2)必须有一个趋近于结束条件的趋势。
/*
* 实现某个数字的阶乘
* */
function fn(n) {
// 当 n<=1 的时候,就结束了,不再进行递归了。
if (n <= 1) {
return 1;
}
return n * fn(n-1);
}
console.log(fn(3));
/**
* 调用 fn(3)
* 3 * fn(2)
* 调用fn(2)
* 2 * fn(1) 结果2
* 调用fn(1)
* return 1
* 调用完fn(1)
* 调用完fn(2)
* 调用完 fn(3) 结果6
* */
1.3 递归的缺点
1)函数递归调用很容易发生灾难(内存泄漏)而调用失败。
2)函数递归调用效率不高,能不用就不用。
12.4 递归的应用场景
后端的操作中有些场景必须要递归函数来完成,如:
1)删除文件夹以及里面的内容,需要递归删除(操作系统的原始接口只能删除文件和空文件夹)
2)复制文件夹以及里面的内容。
3)剪切文件夹以及里面的内容。