★文章内容学习来源:拉勾教育大前端就业集训营
本篇学习目标:
1.更深层次理解函数;
2.掌握函数表达式——另一种定义函数的方式;
3.清除函数数据类型;
4.理解arguments对象;
5.熟练运用函数递归;
6.理解变量类型(全局/局部)、作用域及作用域链;
7.理解预解析的好处;
8.掌握IIEF自调用函数实现方法。
一、 函数表达式
1. 含义
- 是函数定义的另外一种方式。
2. 方法
- 定义方法:就是将函数的定义、匿名函数赋值给一个变量。
- 函数定义赋值给一个变量,相当于将函数整体矮化成了一个表达式。
- 匿名函数:函数没有函数名。
3. 调用
- 调用函数表达式,方法是给
变量名()
执行,不能使用函数名()
执行。
//函数表达式——定义函数的另一种方式
//将函数fun1赋值给一个变量foo1
var foo1 = function fun1 ( ){
console.log("今天天气很好");
}; //注意结束处有分号
//调用——用变量名加括号,不用函数名了
foo1();
//因为调用也用不到函数名,所以其实函数名可以省略
//将匿名函数赋值给一个变量foo1
var foo1 = function (){
console.log("今天天气很好");
}; //注意结束处有分号
//调用——用变量名加括号,不用函数名了
foo1();
二、函数数据类型
1. 具体情况
- 函数是一种单独的数据类型
Function
(object
的一种)。
//检测函数数据类型
function fun1 () {
console.log(1);
}
console.log(typeof(fun1)); //function
//检测函数数据类型
var foo1 = function (a,b){
console.log(a + b);
};
console.log(typeof(foo1));//function
2. 可参与其他程序
- 由于函数是一种数据类型,可以参与其他程序。
- 例如,可以把函数作为另一个函数的参数,在另一个函数中调用。
//将函数作为另一个函数的参数
setInterval(function () {
console.log(1);
},1000);
//以上setInterval函数有两个参数,第一参数位置是匿名函数(实现控制台输出1),第二个参数位置是1000;
//其中1000表示1000毫秒也就是1秒
//总体的实现的功能:每隔1秒执行一下第一个参数位置的值。
- 或者,可以把函数可以作为返回值从函数内部返回。
三、arguments对象
1. 含义
-
JavaScript 中,
arguments
对象是比较特别的一个对象,实际上是当前函数的一个内置属性; 也就是说所有函数都内置了一个arguments
对象; -
有了arguments对象,形参不会成为实参的限制。
-
arguments
对象中存储了传递的所有的实参。 -
arguments
是一个伪数组,因此及可以进行遍历。 -
总结一句是: 函数的实参个数和形参个数可以不一致,所有的实参都会存储在函数内部的
arguments
类数组对象中。
//arguments对象
function sum1(a,b) {
console.log(a+b);
}
sum1(1,5);//6
sum1(2,6,6,7); //8,只加前两个
sum1(2); //2+undefined=NaN
function fun(a) {
console.log(a);
}
fun (1,2,3,4,5,6,7); //实参的个数是远远大于形参的
function fun() {
console.log(arguments);
console.log(arguments.length);//7
//使用数组遍历可以把所有实参都遍历一遍
for (var i = 0 ; i < arguments.length ; i++) {
console.log(arguments[i]);
}
}
2. 案例
定义一个求和函数,如果传入 1 个参数,返回它自己;如果传入两个参数,返回他们的和;如果传入三个参数,先比较前两个的大小,大的与第三个参数求和返回;如果传入 4 个及以上,输出错误提示。
//方法1:用多分支if语句
function sum(a,b,c) {
if (arguments.length == 1) {
return a
} else if (arguments.length == 2) {
return a + b;
} else if (arguments.length == 3) {
return a > b ? a+c : b+c ;
} else {
throw new Error("您输入的参数超过3个!");
}
}
console.log(sum(1)); //1
console.log(sum(1,2)); //3
console.log(sum(1,2,3)); //5
//console.log(sum(1,2,3,4)); //您输入的参数超过3个!
//方法2,用switch语句
function sum(a,b,c) {
switch (arguments.length) {
case 1:
return a;
break;
case 2:
return a+b;
break;
case 3:
return a>b?a+b:b+c;
break;
default:
throw new Error("您输入的参数超过3个!");
}
}
console.log(sum(1));
console.log(sum(1,2));
console.log(sum(1,2,3));
console.log(sum(1,2,3,4));
四、函数递归
1. 含义
- 函数内部可以通过函数名调用函数自身的方式,就是函数递归现象。
- 递归的次数太多容易出现错误(
RangeError
):超出计算机的计算最大能力。 - 更多时候,使用递归去解决一些数学中的现象,如下举例:
举例:如果实参为1,返回1;如果实参是大于1的数,返回实参+函数调用上一项。
//函数递归举例
function fun (a) {
if (a == 1) {
return 1;
} else if (a > 1) {
return a + fun(a-1);
}
};
console.log(fun(1)); //1
console.log(fun(2));// 2+ fun(1)=2+1=3
console.log(fun(3));//3+fun(2)=3+2+1=6
2. 案例
输出斐波那契数列的某一项的值。
斐波那契数列:后面的一项数据是前两项数据之和。1,1,2,3,5,8,13,21,34,55……
//其中参数a代表斐波那契数列的第a项,feibo(a)输出的结果是第a项的值
function feibo(a) {
if (a == 1) {
return 1;
} else if (a == 2) {
return 1;
} else if (a > 2) {
return feibo(a-1)+feibo(a-2);
}
}
console.log(feibo(1)); //1
console.log(feibo(2)); //1
console.log(feibo(3)); //2
console.log(feibo(4)); //3
console.log(feibo(5)); //5
console.log(feibo(6)); //8
console.log(feibo(7)); //13
console.log(feibo(8)); //21
console.log(feibo(9)); //34
console.log(feibo(10)); //44
//函数部分,也可以稍微省略一点写成这样
function feibo(a) {
if (a == 1 || a ==2) {
return 1;
} else if (a > 2) {
return feibo(a-1)+feibo(a-2);
}
}
五、作用域
1. 含义
- 作用域:变量可以起作用的范围。
- 如果变量定义在一个函数内部,只能在函数内部被访问到,在函数外部不能使用这个变量,函数就是变量定义的作用域。
//定义一个函数
function fun() {
//其中有个局部变量a
var a = 2;
//在函数内部是可以调用的
console.log(a);
}
//调用函数
fun ();//2
//外部调用函数内部的局部变量a,会出现错误!
console.log(a); //错误
- 任何一对花括号
{ }
中的结构体都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
但是现阶段(目前在学js5),可以认为 JavaScript 没有块级作用域。
因为在es6之前没有块级作用域的的概念,只有函数作用域。
2. 函数的作用域
- 函数也有自己的作用域,定义在哪个作用域内部,只能在这个作用域范围内被访问,出了作用域不能被访问的。
- 函数定义在另一个函数内部,如果外部函数没有执行时,相当于内部代码没写。
//目前没有块级作用域,只有函数作用域概念。
//函数定义在另一个函数内部,如果外部函数没有执行时,相当于内部代码没写。
function outer() {
var a = 1;
console.log(a);
function inner() {
var b = 2;
console.log(b);
}
//因为inner函数定义在outer函数内部,所以也要在outer函数内部调用
inner(); //但是只有外部的outer函数调用后,这个才会起作用
}
outer(); //outer函数是可以在外部调用的
inner();//inner函数就不可以在外部调用了,会出现错误
六、全局or局部变量
1. 含义
- 局部变量:定义在
函数内部的变量
,只能在函数作用域内部被访问到,在外面没有定义的。 - 全局变量:从广义上来说,也是一种局部变量,
定义在全局的变量
,作用域范围是全局,在整个 js 程序任意位置都能够被访问到。 - 局部变量退出作用域之后会销毁,全局变量关闭网页或浏览器才会销毁。
2. 参数也是局部变量
- 函数的参数本质是一个变量,也有自己的作用域,函数的参数也是属于函数自己内部的局部变量,只能在函数内部被使用,在函数外面没有定义。
3. 不写var关键字的影响
- 在函数内部想要定义新的变量,如果不加关键字 var,相当于定义的全局变量。如果全局也有相同的标识符,会被函数内部的变量影响,局部变量污染全局变量。
- 注意:每次定义变量时都必须写 var 关键字,否则就会定义在全局,可能污染全局。
七、作用域链
1. 含义
- 只有函数可以制造作用域结构, 那么只要是代码,就至少有一个作用域, 即全局作用域。
- 凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。
- 将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构,就称作作用域链。
2. 案例
function f1() {
function f2() {
}
}
var num = 456;
function f3() {
function f4() {
}
}
function f1() {
var num = 123;
function f2() {
console.log(num);
}
f2();
}
var num = 456;
f1();
3. 遮蔽效应
- 程序在遇到一个变量时,使用时作用域查找顺序,不同层次的函数内都有可能定义相同名字的变量,一个变量在使用时,会优先从自己所在层作用域查找变量,如果当前层没有变量定义会按照顺序从本层往外依次查找,直到找到第一个变量定义。
- 整个过程中会发生内层变量遮蔽外层变量的效果,叫做“遮蔽效应”。
//遮蔽效应
var a = 1;//全局作用域中的变量a
function outer() {
var a = 2; //外部函数作用域中的变量a
console.log(a); //2//从本层往外依次查找,直到找到第一个变量定义
function inner() {
var a =3; //内部函数作用域内的a
console.log(a); //3//从本层往外依次查找,直到找到第一个变量定义
}
console.log("再执行内部函数");
inner(); //3
}
console.log("先执行外部函数");
outer(); //2
八、预解析
1. 含义
- JavaScript 代码的执行是由浏览器中的 JavaScript 解析器来执行的。
- JavaScript 解析器执行 JavaScript 代码的时候,分为两个过程:预解析过程和代码执行过程。
(1) 预解析过程
1. 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值。
2. 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用。
3. 先提升 var,再提升 function。
(2) 执行过程
- 在预解析之后,根据新的代码顺序,从上往下按照既定规律执行 js 代码。
2. 变量声明提升
- 在预解析过程中,所有定义的变量,都会将声明的过程提升到所在的作用域最上面,在将来的代码执行过程中,按照先后顺序会先执行被提升的声明变量过程。
- 提升过程中,只提升声明过程,不提升变量赋值;
- 相当于变量定义未赋值,变量内存储 undefined 值。
- 因此,在 js 中会出现一种现象,在前面调用后定义的变量,不会报错,只会使用 undefined 值,如下例:
//变量声明提升
console.log(a); //虽然先调用,但是没有报错,而是输出了undefined值
var a = 1;
- 其实这是因为经历的预解析过程,会把变量声明提升,但是没有提升变量赋值,所以相当于代码是如下顺序执行的:
//相当于是如下执行的
var a;
console.log(a); //undefined
a = 1;
3. 函数声明提升
- 在预解析过程中,所有定义的函数,都会将声明的过程提升到所在的作用域最上面,在将来的代码执行过程中,按照先后顺序会先执行被提升的函数声明过程。
- 在预解析之后的代码执行过程中,函数定义过程已经在最开始就会执行,一旦函数定义成功,后续就可以直接调用函数。
- 因此,在 js 中会出现一种现象,在前面调用后定义的函数,不会报错,而且能正常执行函数内部的代码,如下例:
//函数声明提升
fun (); //先调用也可以正常执行,输出12
function fun () {
console.log(12);
}
4. 先提升变量声明,再提升函数声明
- 预解析过程中,先提升 var 变量声明,再提升 function 函数声明。
- 假设出现变量名和函数名相同,那么后提升的函数名标识符会覆盖先提升的变量名,那么在后续代码中出现调用标识符时,内部是函数的定义过程,而不是 undefined。
- 如果调用标识符的过程在源代码函数和变量定义后面,相当于函数名覆盖了一次变量名,结果在执行到变量赋值时,又被新值覆盖了函数的值,那么在后面再次调用标识符,用的就是变量存的新值。
//举例:先变量声明提升,再函数声明提升
console.log(fun);
var fun = "haha";
fun();
function fun () {
console.log(12);
}
- 因为预解析,所以代码执行顺序相当于下面这个过程:
//相当于下面这个过程
var fun;
function fun () {
console.log(12);
}
console.log(fun);//输出函数
fun = "haha";
fun();//错误类型提示,这时候fun已经被赋值成"haha",不是个函数了
- 建议:不要书写相同的标识符给变量名或函数名,避免出现覆盖。
5. 函数表达式进行的是变量声明提升
- 在预解析过程中,函数表达式进行的是变量声明提升,而不是函数声明提升。
- 提升后变量内部存的是一个 undefined;
- 在前面进行函数方法调用,数据类型会提示错误。
//函数表达式进行的是变量声明提升
foo1(1,5);
var foo1 = function (a,b) {
console.log(a+b);
}
- 建议:定义函数时,最好使用 function 关键字定义方式,这样函数声明提升可以永远生效。
6. 应用
- 函数声明提升可以用于调整代码的顺序;
- 将大段的定义过程放到代码最后,但是不影响代码执行效果。
//举例
fun1 ();
fun2 ();
fun3 ();
fun4 ();
function fun1 () {
console.log(1);
console.log(3);
console.log(5);
}
function fun2() {
console.log(2);
console.log(4);
console.log(6);
}
function fun3() {
console.log(-1);
console.log(-3);
console.log(-5);
}
function fun4() {
console.log(-2);
console.log(-4);
console.log(-6);
}
九、IIEF自调用函数
1. 含义
IIFE
:immediately-invoked function expression,叫做即时调用的函数表达式,也叫做自调用函数;- 表示函数在定义时就立即调用。
2. 调用方式
- 正常函数调用方式:函数名或函数表达式的变量名后面加
()
运算符。 - 而其中<mark>函数名定义的形式不能实现立即执行自调用</mark>;
//函数名定义的不可以实现自调用,比如这样的函数
function fun1 () {
console.log(1);
}
- 函数使用函数表达式形式可以实现立即执行;
- 原因是因为函数表达式定义过程中,将一个函数矮化成了一个表达式,后面加
()
运算符就可以立即执行。
//函数表达式的形式定义的函数可以实现自调用,方法是后面直接加()运算符
var foo1 = function() {
console.log("函数表达式的形式定义的函数可以实现自调用,方法是后面直接加()运算符");
} ();
3. 启发
- 如果想实现
IIFE
,可以想办法将函数矮化成表达式。 - 函数矮化成表达式,就可以实现自调用。
4. 函数矮化成表达式的方法
- 可以让函数参与一些运算,也就是说给函数前面加一些运算符:
(1) 数***算符
+
-
()
//将函数矮化成表达式,以实现自调用
//算术运算符 + - ()
+ function fun1() {
console.log("1前面加算术运算符+");
}();
- function fun2 () {
console.log("2前面加算术运算符-");
}();
( function fun3 () {
console.log("3整体加算术运算符()");
})();
(2) 逻辑运算符:
!
非运算
//将函数矮化成表达式,以实现自调用
//逻辑运算符!
! function fun4 () {
console.log("4前面加逻辑运算符!")
}();
5. 注意
IIFE
结构可以关住函数的作用域,在结构外面是不能调用函数的。IIFE
最常用的是 () 运算符,而且函数可以不写函数名,使用匿名函数。
下篇继续:【74】JS(7)——对象①基本介绍