一、JavaScript函数
1. JavaScript函数基本使用
思考:下面代码有什么问题?
alert('欢迎马先生光临红浪漫');
alert('男宾3位');
alert('马先生消费480');
alert('欢迎马先生光临红浪漫');
alert('男宾3位');
alert('马先生消费480');
alert('欢迎马先生光临红浪漫');
alert('男宾3位');
alert('马先生消费480');
JS中的函数:把一段需要重复使用的代码,用function语法包起来,方便重复调用,分块和简化代码。复杂一点的,也会加入封装、抽象、分类等思想。
函数:一组可以被重复调用的JavaScript语句
1.1 JavaScript函数声明
-
用function关键字进行函数声明
function 函数的名字(参数) { //函数体:要执行的代码 }
- 函数名字与变量名一样都必须是JavaScript合法的标识符。
- 在函数名之后是一个由小括号包含的参数列表,参数之间以逗号分隔,参数是可选的。
- 在小括号之后是一对大括号,大括号内包含的语句就是函数体的主要内容。
-
通过
函数表达式
得到函数可以用函数表达式声明 (如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。)
var sum = function() { console.log('谁在让我东张西望'); } sum();
-
通过
new Function()
得到函数//格式: new Function([参数,]'函数体'); var sum = new Function('a', 'b', 'console.log(a+b)'); sum(1, 2); //3
1.2 JavaScript函数调用
- 调用方式:名字(); 函数可以多次调用
//函数声明
function fn(){
console.log(1);
}
//函数的调用
fn();
fn();
思考:调用函数,名字不加()会返回什么?
2. JavaScript函数参数
思考:下面代码的变量(名字,性别,人数,花销)有什么问题吗?
alert('欢迎马先生光临红浪漫');
alert('男宾3位');
alert('马先生消费480');
2.1 JavaScript函数参数
-
形参:形式上的参数——给函数声明一个参数;
-
实参:实际的参数——在函数调用时给形参赋的值
function func(形参1,形参2){ //函数执行代码 } func(实参1,实参2);//调用时传参
-
一般情况下,函数的实参和形参的数量应该相同,但是JS并没有这样的要求。可以不相同
-
如果函数的实参数量少于形参数量,那么多出来的形参会默认会undefined
-
如果函数实参数量多余形参数量,那么多出来的实参就不能通过形参访问。函数忽略掉多余的实参。
案例1:正常传递对应参数
function f(a,b) {
alert('正常传递参数');
}
f(1,2);
案例2:实参少于形参
function f2(a,b,c) {
console.log(a,b,c);//1 2 undefined
console.log(typeof c);//undefined
}
f2(1,2);
案例3:实参多余形参
function f3(a,b,c) {
console.log(a,b,c)1
}
f3(1,2,3,4,5,6,7)
3. 函数的返回值
-
在函数内部通过return设置函数的返回值
-
没有return语句的函数,默认返回undefined
-
在函数体内部,遇到return语句,则终止函数的执行
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>函数的返回值</title> </head> <body> <script> //在函数内部通过return设置函数的返回值 function testFn() { // return; //这样写默认返回undefined // return 1; return 'hello world'; //可以返回任意类型的数据 console.log(321); console.log(321); console.log(321); console.log(321); console.log(321); return 123; } var res = testFn(); console.log(res); //思考下:这个会返回什么? function testFn2() { return 1+1, 2*3, 3*3; } console.log(testFn2()); /* 总结: 1. 在函数内部通过return设置函数的返回值 2. 没有return语句的函数,默认返回undefined 3. 在函数体内部,遇到return语句,则终止函数的执行 */ </script> </body> </html>
-
思考:如果return后返回多个值,结果是什么样的?
案例练习:
- 计算2和3的和
- 输入两个数,返回两个数字的和
- sum(1, 8); 返回2个数字的和 9
- sum(4, 7); //11
- 返回n到m之间所有数字的和
- sum(1, 10); 返回1~10之间所有数字相加的和 55
- sum(1, 100); //5050
- 输入两个数,返回两个数中的最大值
- max(2, 8); //8
- 输入三个数,返回三个数中的最大值
- max(2, 8, 3); //8
- 求一个数组中所有元素的和
- sum([1,2,3,4,5]); //15
4.JavaScript作用域
4.1 变量作用域
变量作用域(scope)是指变量在程序中可以访问的有效范围,也称为变量的可见性。分为全局变量和局部变量
- 全局变量:变量在整个页面中都是可见的,可以被自由的访问,在函数之外定义的变量是全局变量
- 局部变量,变量仅能在声明的函数内部可见,函数外是不允许访问的,在函数内部定义的变量是局部变量
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>变量作用域</title>
</head>
<body>
<script>
//作用域: 起作用的范围(scope)
/*
作用域分为局部和全局
变量分为局部变量和全局变量
局部变量: 在函数内部声明的变量叫局部变量
全局变量: 在全局范围(函数外部)声明的变量
*/
var name = '张三';
//声明一个函数就会创建一个函数(局部)作用域
function fn1() {
//函数内部可以访问函数外面的变量
console.log(name);
var innerName = '张四风';
console.log(innerName);
}
// console.log(name);
fn1();
//在函数外面不能访问函数内部的变量
console.log(innerName);
</script>
</body>
</html>
4.2 JavaScript 局部变量
变量在函数内声明,变量的作用域为局部作用域。局部变量的作用域 我们称之为局部作用域。
局部变量:只能在函数内部访问。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>局部变量</title>
</head>
<body>
<script>
//形参也是局部变量
function fn1(age) {
var innerVar = '我是个局部变量';
console.log('在内部访问innerVar:'+innerVar);
console.log('在内部访问age:'+age);
}
fn1(18);
// console.log('在外部访问innerVar:'+innerVar); //报错
// console.log('在外部访问age:'+age); //报错
//fn2的形参还叫age也行,不会跟fn1的age有任何关系
function fn2(age) {
console.log(innerVar); //报错:无法访问fn1里面的内部变量
}
// fn2();
</script>
</body>
</html>
因为局部变量只作用于函数内,所以不同的函数可以使用相同名称的变量。
局部变量在函数开始执行时创建,函数执行完后局部变量会自动销毁。
4.3 JavaScript 全局变量
变量在函数外定义,即为全局变量。
全局变量有全局作用域: 网页中所有脚本和函数均可使用。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>全局变量</title>
</head>
<body>
<script>
var a = 100;
function fn1() {
console.log(a);
//尽量避免在函数内部修改全局变量的值
a = 200;
}
function fn2() {
//为什么能访问呢?
//1. 在自己的作用域中找变量a,找到了则直接使用
//2. 如果没找到,则往上一级作用域中找,找到就用,没找到就报错
console.log(a);
//在函数内部,声明变量时,不加var,默认会弄成全局变量
// var money = 300;
money = 300; //应该尽量避免
}
fn1();
fn2();
console.log(money);
</script>
</body>
</html>
特别需要注意:如果变量在函数内没有声明(没有使用 var 关键字),该变量为全局变量。但是如果该变量出现在形式参数中,则为局部变量。
以下实例中 name在函数内,但是为全局变量。
function myFunction() {
name = "lisi";
}
myFunction();
// 此处可以访问 name 变量
console.log(name);
当函数内部写了一个变量不带var 有以下几种情况:
1、如果函数内部出现不带var的变量,那么首先找形参;如果形参有,当做是局部变量对待
2、如果函数内部出现不带var的变量,形参也没有这个变量,那么就把它当做全局变量来对待
4.4 作用域链
作用域链:JS中变量的查找规则。
作用域链和作用域不是一回事:
作用域描述是变量起作用的区域和范围
作用域链描述的程序在找变量的过程;
程序在查找变量的时候,先从自己的作用域去查找,如果找到就直接使用这个变量的值,如果没有找到,会继续往上一级作用域去找,同样也是找到就使用没有找到继续往上找;直到找到全局作用域,如果找到就使用,没找到就报错(引用错误,这个变量没有定义);
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>作用域链</title>
</head>
<body>
<script>
/*
作用域链:当作用域产生嵌套的时候就会形成作用域链
默认有个全局作用域,声明一个函数就会形成一个函数作用域
函数内部再声明函数,就会形成作用域嵌套
特点:
1. 在自己的作用域中找变量a,找到了则直接使用
2. 如果没找到,则往上一级作用域中找,找到就用
3. 直到找到全局作用域,如果还没找到,则报错
*/
var n1 = 10;
function fn1() {
var n2 = 20;
// console.log(n1);
// console.log(n2);
function fn2() {
console.log(n1);
console.log(n2);
}
fn2();
}
fn1();
</script>
</body>
</html>
案例练习:经典案例。
<script>
var a = 0;
function fn1() {
var a = 1;
function fn2() {
a = 20;
function fn3() {
var a = 3;
function fn4() {
console.log(a); //???
}
fn4();
}
fn3();
}
fn2();
}
fn1();
console.log(a); //这里会输出什么?
</script>
作用域不是在函数调用的时候确定的,而是在函数定义的时候就确定好了
5. JavaScript预解析
5.1 JavaScript函数提升
var语句和function语句都是声明语句,它们声明的变量和函数都在JavaScript预编译时被解析,也被称为变量提升
和函数提升
。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>预解析之提升</title>
</head>
<body>
<script>
/*
解析器会先分析我们写的js代码,然后再交给引擎去执行。
所有函数声明也会提升到当前作用域的最前面
所有带var的变量声明,都会被提升到当前作用域的最前面
函数提升优先级更高
*/
console.log(a);
var a = 1; //声明变量并赋值为1
fn1();
//函数声明也会提升,并且比变量的优先级更高
function fn1() {
console.log('我是函数1');
}
function fn1() {
console.log('我是函数2');
}
//解析器会拆成:
// var a;
// console.log(a);
// a = 1;
// fn1();
</script>
</body>
</html>
5.2 预解析
-
概念:
- 预解析分为变量预解析(变量提升)和函数预解析(函数提升)
-
说明:
- 程序在开始执行之前会做全局代码预解析
- 函数在开始执行之前也会做局部代码预解析
-
特点
- 程序在代码执行之前会先进行预解析;
- 预解析会解析
带var的变量
和函数声明定义的函数
function,解析函数优先级比解析变量要高;- 可以认为解析的时候分为两步,
先提升函数,再提升变量
;
- 可以认为解析的时候分为两步,
- 如果函数重名,会覆盖(后面的函数会把前面的覆盖掉)
- 如果变量和函数重名,会忽略;
练习:
-
console.log(a); a = 0;
-
console.log(a); var a = 0; console.log(a);
-
console.log(a); var a = '我是变量'; function a(){ console.log('我是函数') } console.log(a);
-
console.log(a); a++; console.log(a); var a = '我是变量'; function a(){ console.log('我是函数') } console.log(a);
-
console.log(a); var a = 0; console.log(a); function fn(){ console.log(a); var a = 1; console.log(a); } fn() console.log(a);
-
console.log(a); var a = 0; console.log(a); function fn(){ console.log(a); a = 1; console.log(a); } fn() console.log(a);
6. JavaScript函数类型
6.1 函数声明定义函数
function fn(){
console.log("i am 普通函数");
}
6.2 函数表达式定义函数
var sum = function(){
console.log('谁在让我东张西望');
}
sum();
6.3 匿名函数
匿名函数顾名思义指的是没有名字的函数,在实际开发中使用的频率非常高!也是学好JS的重点。
匿名函数:没有实际名字的函数。
6.3.1 为什么要学习IIFE
实际上,IIFE(Immediately Invoked Function Expression)的出现是为了弥补JS在scope方面的缺陷:JS只有全局作用域、局部作用域,从ES6开始才有块级作用域。对比现在流行的其他面向对象的语言可以看出,JS在访问控制这方面是多么的脆弱!那么如何实现作用域的隔离呢?在JS中,只有function,只有function,只有function才能实现作用域隔离,因此如果要将一段代码中的变量、函数等的定义隔离出来,只能将这段代码封装到一个函数中。
在我们通常的理解中,将代码封装到函数中的目的是为了复用。在JS中,当然声明函数的目的在大多数情况下也是为了复用,但是JS迫于作用域控制手段的贫乏,我们也经常看到只使用一次的函数:这通常的目的是为了隔离作用域了!既然只使用一次,那么立即执行好了!既然只使用一次,函数的名字也省掉了!这就是IIFE的由来。
6.3.1 立即执行函数 IIFE
Immediately Invoked Function Expression
立即执行函数,也叫做匿名函数自调用
function (){
console.log("i am IIFE");
}
发现报错啦!!!
- 解决方法只需要给匿名函数包裹一个括号即可:
(function (){
//由于没有执行该匿名函数,所以不会执行匿名函数体内的语句。
console.log("匿名函数不会报错了");
});
- 匿名函数后面加上一个括号即可立即执行!
(function (){
console.log("运行匿名函数");
})();
- 在函数前添加 !~ + - 一元运算符
!function (){
alert("heng");
}();
- 倘若需要传值,直接将参数写到括号内即可:
(function (str){
console.log("Hello "+str);
})("World");
- 另一种写法
(function(){
console.log(11111);
}());
6.3.2 IIFE特点
1、匿名函数自调用是在定义的同时执行函数
2、匿名函数自调用只能执行一次
如果需要一个函数可以执行多次,这个函数必须是有名函数
如果函数没有名字要想执行必须是自调用,但是只能执行一次
3、匿名函数自调用,在函数内部执行代码也要去预解析
6.3.3 IIFE作用
-
防止外部命名空间污染
- 隔离作用域,防止污染全局命名空间
- 因为一个页面中会有很多的重复变量名字 那么这样就会命名冲突 eg:都是name 所以使用匿名函数自调用可以隔离不同业务的同名的变量
-
隐藏内部代码,暴露接口
- 很多的js文件 只有引入的路径 引入之后就可以直接使用,代码的细节是看不到 这样保证了文件的安全
-
对项目的初始化,只执行一次
- 因为立即执行函数只执行一次 所以对于项目加载文件来说是非常有好的,省略了很多加载的时间
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>IIFE的作用</title> </head> <body> <script> //1. 防止命名污染 // var money = 100; // console.log(money); // console.log(money); // console.log(money); // console.log(money); // console.log(money); // console.log(money); // console.log(money); // console.log(money); // money = 200; // console.log(money); (function(){ var money = 100; // console.log(money); })(); money = 200; //2.隐藏细节,暴露接口 (function( window ) { var a; })( window ); //3.对项目的初始化,因为IIFE只会执行一次 </script> </body> </html>
6.3.4 全局污染
全局变量污染:大家都在全局中写代码,很容易造成命名冲突,导致代码冲突。ES6中代码冲突会直接报错。所以要养成好的习惯不要在全局去声明变量。
<!DOCTYPE html>
<html lang="en">
<head>
<title>全局污染</title>
</head>
<body>
<script>
var age = 12;
var age = 21;
function func2(){
var name = 'zhangsan';
var name = 'lisi';
console.log(name);
}
func2();
</script>
</body>
</html>
结果:发现最后获取的只有一个元素,所以很容易造成代码冲突
解决策略:使用IIFE
立即执行函数本身就是为了避免在全局写代码,避免冲突的。立即执行函数自执行也叫开启一个新的命名空间。即开启新的作用域,此作用域和其他的不会冲突。
7. JavaScript函数参数的高级
7.1 获取函数参数个数
-
使用arguments对象的length属性可以获取函数的实参个数。
-
arguments对象只能在函数内可见,因此arguments.length也只能在函数体内使用。
-
使用函数对象的length属性可以获取函数的形参个数,该属性为只读属性,在函数体内,体外都可以使用
function add(a,b,c) { alert(add.length)//函数形参的个数 alert(arguments.length)//获取实参的个数 return a+b+c; } alert(add.length)//函数形参的个数 console.log(add(1, 2));
7.2 使用arguments对象
- arguments对象表示函数的实参集合,仅能够在函数体内可见,并可以直接访间。
- 参数对象是一个伪类数组,不能够继承Array的原型方法。可以使用数组下标的形式访问每个实参,如参数[0]表示第一个实参
- 通过修改length属性值,可以改变函数的实参个数。
案例1:其实函数定义的形参可写可不写(ES5当中),但是虽然可以不写,建议以后写上形参
<script>
function add() {
console.log(arguments);
return arguments[0] + arguments[1];
}
console.log(add(10, 20));
</script>
案例2:获取实参的个数
案例3:输入一组数字,求平均值的函数
案例4:直接使用函数对象的length属性,就可以获取到函数的形参个数
案例5:传2个参数返回差值,传3个参数返回和
- t(2, 8);//6
- t(2,3,5); //10
function fn1(a,b,c) {
console.log(arguments.length);//获取实参的个数
}
fn1(1,2,3,4)
案例3:输入一组数字,求平均值的函数
function f() {
//先获取到所有的实参 使用arguments
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum / arguments.length;
}
console.log(f(12, 34, 54, 32, 100, 98, 45, 34, 120));
案例4:直接使用函数对象的length属性,就可以获取到函数的形参个数
function f3(a,b,c) {
console.log(f3.length);//3
}
f3(1,2,3,4,5,6);
案例5:arguments.length,拿得是实参的个数 可以让我们一个函数具有多种功能
<script>
function addOrSub(a, b, c) {
if (arguments.length == 2) {
return a - b;
} else if (arguments.length == 3) {
return a + b + c;
}
}
console.log(addOrSub(10, 20)); //传递两个实参就做减法
console.log(addOrSub(10, 20, 30)); //传递的是三个实参就做加法
</script>
7.3 形参的默认值
//es6新增的,可以直接给形参赋值
function fn1(n1, n2, n3 = 100) {
//如果没有给n3传值默认是undefined
if (n3 === undefined) n3 = 10;
return n1 + n2 + n3;
}
// console.log(fn1(1,2)); //NaN 因为第3个参数是undefined
// console.log(fn1(1,2));
console.log(fn1(1,2,0));
console.log(fn1(1,2));
案例6:写出执行结果
function foo(x, y) {
x = x || 0;
y = y || 0;
console.log(x + y);
}
foo(2, 3);
foo();
案例7:写出执行结果
function bar(x, y) {
x = (typeof x !== 'undefined') ? x : 10;
y = (typeof y !== 'undefined') ? y : 20;
console.log(x + y);
}
bar(2, 3);
bar();
8. 函数的递归
函数的递归调用,就是函数在内部自己调自己
函数的递归调用是一把双刃剑,如果设计的好,可以帮我们简单的处理事情,如果设计不好就是灾难
函数的递归要想设计好必须有两个条件
- 必须有一个明显的结束条件,不能一直递归下去
- 每一次调用都要有一个趋近结束条件的趋势
8.1 设计不好的递归
<script>
function fn() {
console.log('大哥大哥你好吗?');
fn();
}
fn();
</script>
8.2 好的递归
//必须要有明显的结束条件,不能一直调用下去
function fn1(n) {
console.log('来了,老弟~' + n);
//这个就是条件,通常用一个参数来控制
if (n > 1) {
fn1(n - 1);
}
console.log('走了,拜拜,老弟~' + n);
}
fn1(3);
8.3 递归实现判断用户输入的数字是否是质数
function isZhishu() {
var num = Number(prompt('请输入一个数字:'));
if (isNaN(num)) {
isZhishu();
// console.log('请输入一个正确的数字');
} else {
var flag = true;
for (var i = 2; i < num; i++) {
if (num % i == 0) {
flag = false;
break; //终止循环
}
}
if (flag) {
console.log(num + ':是质数');
} else {
console.log(num + ':不是质数');
}
}
}
isZhishu();
8.4 匿名函数如何递归?
(function(n){
//匿名函数如何递归?
//1. 使用 argumens.callee() 实现 (不推荐,新规范已经废弃了)
console.log(arguments.callee);
if (n > 1) {
arguments.callee(n-1);
}
console.log(n);
})(3);
//2. 给匿名函数取个名字呗 (推荐)
(function aa(n){
if (n > 1) {
// arguments.callee(n-1);
//这个 aa 也只是在局部范围有效,不会污染全局变量
aa(n-1);
}
console.log(n);
})(3);
9.回调函数(callback)
回调函数是指将函数作为参数传递给另一个函数,并在该函数内部被调用的函数。
9.1 基本语法
function foo(callback) {
// 执行一些操作
callback();
}
foo(function() {
console.log('回调函数被执行');
});
9.2 使用场景
//遍历数组
var arr = [1,2,3,4];
arr.forEach(function(item){
console.log(item);
});
//定时器函数
setTimeout(function() {
console.log('2 秒后执行');
}, 2000);
//点击事件
document.querySelector('button').addEventListener('click', function() {
console.log('按钮被点击了');
});