一、函数的声明与调用
1.1 函数概述
函数可以封装一些功能,可以供外部去重复的调用。所以,一般我们把函数叫做具有重复功能的代码块。
JavaScript 基础到高级
Canvas游戏开发
原生JavaScipt案例合集
JavaScript +DOM基础
假设饭店就是一个函数,饭店的功能就是做各种各样的菜,但是具体做什么菜,需要用户来点,用户可以是我们当中的任何一个人,当我们点好菜付完帐,那么饭店就行驶它做饭的功能,开始为我们做指定的可口饭菜,这个我们点餐的过程,其实就是类似于函数调用的过程。最后,饭店做完菜给我们呈上来,这个菜就类似于函数返回的数据
1.2 函数的声明
函数使用关键字 function 来进行声明
在声明的函数内部,我们可以书写一些代码,使这个函数具有某一种特定的功能。在函数内部书写的代码我们习惯上称为函数体。
函数声明的语法格式:
function 函数名(形式参数){
函数体
}
思考:求任意两个数的和
函数名的命名规则:同变量命名规则
1.3 函数的调用
函数声明后,不会自动执行,需要进行调用
函数调用的语法 函数名()
函数的调用方式
- 直接在 JS 脚本的任何为止进行调用 函数名()
- 事件驱动
练习:使用函数包装流程语句,也就是将流程语句当作函数体。一个大于0的整数,打印它的所有约数。
分析:一个数它的最小约数是1,最大约数是它本身。如果还有其它约数,肯定在 1 和 它本身之间。
什么是约数?
约数,又称因数。整数a除以整数b(b≠0) 除得的商正好是整数而没有余数,我们就说a能被b整除,或b能整除a。a称为b的倍数,b称为a的约数。
使用函数封装,简单介绍几个好处:
- 避免全局变量的污染;
- 节约内存;
- 提高程序的执行效果;
- 减少重复代码的书写;
- 便于后期维护;
- 可以返回数据进行二次操作;
等等…
1.4 函数的类型
function f(){}
console.log(typeof f);//function
二、函数的参数
2.1 函数参数概念
参数可以类似的看作我们之前声明的变量,只不过它的位置在函数后面的小括号中,且不需要再使用var进行声明。函数的参数分为两种:形式参数 和 实际参数。
**形式参数:**简称形参。在函数声明的时候,小括号中给定的参数,此时,不能确定参数的数据类型 和 具体的值。类似于前面讲的去饭店服务员给我们的菜单,可以提供给这么多菜,但是具体做什么菜还需要客户指定。
**实际参数:**简称实参。在函数调用的时候,用来替换形参的具体的值(传递的参数),此时确定了数据类型和值。
类似于前面讲的去饭店点菜,根据服务员提供的菜单,点具体的菜名
**传参:**函数调用,有一个传递参数的过程,这个过程我们叫它传参。
需求:求一个大于0的整数,约数的个数
通过上面的描述和演示,总结出JS的另外一个特点:
JS是一门动态的、弱类型语言。
2.2 函数的重载
重载可以看作是函数参数的应用。
函数的重载:在一个程序中,声明多个同名函数,但是其数据类型以及参数的个数并不相同。
JS中不存在函数重载的概念,我们只能模拟。因为一旦在JS中声明多个同名函数,后面的会覆盖前面的。
2.3 arguments对象
arguments对象是JS中一个特殊的对象。它是一个类数组,存储的是函数调用时所有传递的实际参数。
arguments对象中内置了一个 length 属性,用来获取函数调用时实际传递的参数的个数 语法 arguments.length
类数组和数组都具有索引,索引从0开始,依次递增,如上图:arguments对象中,有对应的索引,一一对应于传递的参数。通过 arguments[索引] 可以获取执行的对应的参数值:
获取第一个参数 arguments[0]
获取最后一个参数 arguments[arguments.length - 1]
function sum() {
console.log("arguments:",arguments);
console.log("函数调用时传递的第一个参数:",arguments[0]);
console.log("函数调用时传递的参数个数:",arguments.length);
console.log("函数调用时传递的最后一个参数:",arguments[arguments.length - 1]);
}
模拟函数的重载
三、函数的返回值
很多情况下,我们封装函数的目的是为了利用函数的这个功能,得到某些数据,然后对这些数据进行二次操作。
那么,如果想要在函数中返回这些数据,那么需要用到关键字 return。
return 关键字用于在函数中返回数据,并跳出函数。
如果仅仅是为了跳出函数,而不需要返回任何数据,那么直接return即可。
练习: 判断一个大于0的数字是否是质数。
什么是质数?
质数也叫素数,是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。
分析: 可以根据约数的个数,当约数的个数是2的时候,那么这个数就是质数。
return作为跳出函数用。一般会在面试中经常被问道,return、break、continue的区别?
四、函数的作用域
4.1 作用域概述
**概述:**当声明函数后,在函数的内部(大括号函数体位置)形成自己一个独立的内部区域,这个内部区域就叫做函数作用域。
函数作用域相对于整个脚本区域来讲,只是一个很小的独立区域,这个很小的区域我们通常叫它局部作用域。而整个脚本区域,我们通常叫它全局作用域。
全局作用域:函数外的区域叫做全局作用域;在全局作用域中声明的变量,可以在脚本的任何位置(包裹函数内)都可以对其进行调用,这个变量我们称为全局变量。全局变量的生命周期:从文件被执行时创建(当前就是页面被加载),页面关闭后销毁。
局部作用域:函数内的区域叫做局部作用域;在局部作用域中声明的变量,只能在函数的内部进行访问调用,那么此时我们称其为局部变量。局部变量的生命周期:函数被调用时创建,函数调用完毕销毁。
4.2 作用域链问题:
嵌套函数中,存在多个被声明的同名函数,在调用时,首先查看当前作用域是否有这个变量,有则调用,没有则去它的上一级作用域中查找,如果有则使用,如果没有,继续向上查找,直到找到为止
//声明一个全局变量 num
var num = 1;
function fn() {
//在第一层函数中,声明一个变量 num
var num = 10;
//在第一层函数中,声明一个内部函数 fn2
//创建函数的另外一个方式 将声明的匿名函数赋值给一个变量
var fn2 = function () {
var num = 100;
return num;
}
return fn2;
}
4.3 局部作用域内不使用var声明的变量
如果在函数的内部,不使用var声明,首先这是不标准的写法,一般不建议,且在严格摸下会报错。其次,如果我们不小心这样用了,那么在函数被调用后,这个变量会提升为全局变量。如果函数没有被调用,则不会创建这个变量。
function f() {
num = 10;
console.log("num:",num);
}
这种情况,很容器造成全局变量的污染,不建议使用。
var i = 2;
console.log('i:',i);//i: 2
function f1() {
var i = 20;
console.log('i:',i);//i: 20
}
f1();
console.log('i:',i);//i: 2
var j = 3;
console.log("j:",j);//j:3
function f2() {
j = 30;
console.log("j:",j);//j:30
}
f2();
console.log("j:",j);//j:30
上面的 j 本身是想作为一个局部变量来处理,但是由于没有使用 var 声明,造成函数调用时,这个本要作为局部变量定义的变量 变成了 全局变量的赋值操作,这样就造成了全局变量的污染,或称为命名冲突。
4.4 同一个变量多次声明
如果在同一个作用域内,一个变量被多次声明,那么声明只会生效一次,以第一次声明为准,后面的声明不再作数,直接作为变量的赋值操作。
var num = 1;
console.log("num:",num);//num:1
var num = 10;
console.log("num:",num);//num:10
num = 100;
console.log("num:",num);//num:100
五、函数的提升
在 JS 中,有变量和函数的提升,JS引擎在解析代码时,分成两步:
- 预解析阶段:将所有变量 和 函数的声明,提升到当前作用域的最顶部
- 运行阶段:进行变量的赋值操作
在JS中,函数的创建方式有三种:
-
第一种标准的方式 function 函数名(参数列表){函数体}
function f() { num = 10; return "num:"+num; }
-
第二种函数表达式的方式:将一个匿名函数赋值给一个变量 var f = function(参数列表){函数体}
var f1 = function(){ num = 10; return "num:"+num; }
-
第三种内置对象的方式 var f = new Function(“参数列表”,“函数体”)
var f3 = new Function("num = 10; return 'num:'+num"); var f2 = new Function("a","b","return a+b");
使用标准的方式创建的函数,与其它两种方式在调用时的不同:
-
如果都是先声明,再去调用,看不出有什么区别:上面已经验证过 f() f1() f3() 结果一致
-
如果时先调用,再去声明的情况,那么结果就不同了,如下代码
非标准的方式,利用的是一个变量来接收一个函数,那么其本质就是变量。前面讲过变量的提升,相当于将变量的声明提升到当前作用域的最顶部,这时还没有赋值,在调用的时候,变量的值是undefined。所以后面两种创建函数的方式,只能先声明再调用。
**标准的方式:**会将整体作为一个声明部分,再预解析阶段,将其整体提升到当前作用域的最顶部,所以标准的方式创建的函数可以在当前作用域的任何为止被调用。
小问题:如果在同一个文件中,存在同名的变量 和 函数,函数优先于变量提升;而前面讲到,在一个文件中,声明多个变量,只有第一个会生效,而函数也可以看作是变量,不管使用var还是使用function都叫做声明。所以函数优先于变量提升后,其它变量的声明都变成了赋值操作。
六、模块化编程
假设有一个酒店:酒店中有保安负责安保、前台负责接待、服务生负责提供服务、厨师负责做饭、大堂经理负责协调等等。
这时,如果将安保、前台、服务生、厨师、协调等工作全部交给大堂经理一个人去做,这样效率会很慢,酒店几乎没有多久肯定会倒闭。此时,酒店提供所有的功能都在大堂经理一个人身上,肯定是不行。
这时,酒店考虑到经营问题,又招来了保安、前台等等人,负责其自己相应的领域工作,然后大堂经理只需要调配这些人即可。这样效果就会很高了。
那么,代码中也是一样,尽可能的减少代码的耦合性。我们可以利用函数,将功能封装的尽量单一,在不同的场景下,可以被重复的利用,优化了代码又提高了执行的效果。这些,其实就是模块化的思想。
后面随着课程的深入,我们会接触到 seajs、requirejs、nodejs 这些东西,都是用来进行模块化开发的,不同之处在于其将一个js文件作为一个模块,可以避免全局变量的污染。
这里,我们使用函数来体会模块化编程的思想。
人类从古至今,习惯将事情分工,将一些内容做成一些公共模块,模块可以重复反复使用。
模块化编程:将一些基础的公共的部分单独封装到一个函数内,可以多次被调用。
**案例:**输出100以内的质数,模块化编程。
逆向思维的过程:输出100以内的质数 → 判断是不是质数 → 找约数个数
练习:找100以内的完美数:(一个数的约数除了它本身外其他约数和还等于这个数)。
注意:模块化编程,可以让我们的程序更加优化,各个小模块要尽量功能单一,提高重复使用率。
**练习:**赢数是一种特殊的自然数,除去它本身以外所有的约数和大于其本身。请输出100以内所有的赢数
练习:如果整数 A 的全部约数(包括1,不包括 A 本身)之和等于 B,且 整数 B 的全部约数(包括1,不包括B 本身)之和等于 A,则称整数 A 和 B是一对亲密数。输出1000以内的亲密数。
七、作业
-
封装一个函数,实现功能:不借助临时变量,进行两个整数的交换。
提示:运算符、函数有两个参数
-
封装一个函数,求3个数中的最大值
提示:函数有三个参数、要求至少使用两种方式实现
-
封装一个函数,实现在页面上输出 100 ~ 1000之间所有的质数,并要求每行显示 6 个数字
-
封装一个函数,实现 12! - 10! 结果
要求:不能使用递归
八、数据类型在内存中的位置
数据类型有两大类:基本数据类型 和 引用数据类型
- 基本数据类型:Number数值类型 string字符串类型 Boolean布尔类型 Null空类型 Undefined未被定义的
- 引用数据类型:Object对象类型、Function函数类型
基本数据类型存储在内存的栈中,引用数据类型存储在堆中
代码演示:
var a = 10,
b = 20,
c = 30,
fn = function () {};
console.log("a:",a,"b:",b,"c:",c,"fn:",fn);//a: 10 b: 20 c: 30 fn: ƒ () {}
//修改b的值
b = 200;
console.log("a:",a,"b:",b,"c:",c,"fn:",fn);//a: 10 b: 200 c: 30 fn: ƒ () {}
//声明一个变量d来接收基本数据类型的变量c
var d = c;
console.log("c:",c,"d:",d);//c: 30 d: 30
//改变基本数据类型c的值
c = 300;
console.log("c:",c,"d:",d);//c: 300 d: 30
//总结:基本数据类型变量在进行赋值时,只是将变量保存的值复制了一份给另外一个变量进行赋值。此时变量值得改变不会互相影响。
//声明一个变量,来接收引用数据类型得变量
var fn2 = fn;
console.log("fn:",fn,"fn2:",fn2);//fn: ƒ () {} fn2: ƒ () {}
console.log(fn === fn2);//true 指向同一个内存地址
//改变其中一个引用数据类型的结构
fn.x = 100;
console.log("fn:",fn,"fn2:",fn2);//fn: ƒ () {} fn2: ƒ () {}
console.log(fn === fn2);//true 指向同一个内存地址
console.log("fn.x:",fn.x,"fn2.x:",fn2.x);//fn.x: 100 fn2.x: 100
//总结:引用数据类型变量在进行赋值时,将引用数据类型的变量保存的地址复制了一份给另外一个变量复制。此时两个变量指向同一个地址,其中一个结构发生改变会影响另外一个
九、递归函数
递归就是在函数的内部调用函数自己本身;
递归函数多用来解决一些 数字0以上 的数学问题;
递归函数必须给定一个条件,来退出函数,否则会造成内存堆栈溢出的问题。
如:
累加问题 1 + 2 + 3 + … + 98 + 99 + 100
累乘问题 100! = 100 * 99 * 98 * … * 3 * 2 * 1
生兔子的问题 有一对兔子,三个月开始每个月生一对小兔子,这一对小兔子在成长到三个月的时候,每个月也是生一对小兔子,问一年之后,在没有死亡的情况下,总共有多少对兔子?
换算成数学,就是一个斐波那契数列 1 1 2 3 5 8 13 21 34 55 89 144…
// 斐波那契数列 1 1 2 3 5 8 13 21 34 55 89 144...
console.log(feiBo(1));
console.log(feiBo(2));
console.log(feiBo(11));
console.log(feiBo(12));
// 求斐波那契数列中第 N 项的值
function feiBo(n) {
if(n === 1 || n === 2){
return 1;
}
return feiBo(n - 1) + feiBo(n - 2);
}
//feiBo(3) = feiBo(3 - 1) + feiBo(3 - 2) = feiBo(2) + feiBo(1) = 1 + 1 = 2
//feiBo(4) = feiBo(4 - 1) + feiBo(4 - 2) = feiBo(3) + feiBo(2) = feiBo(3 - 1) + feiBo(3 - 2) + 1 = 1 + 1 + 1 = 3
//feiBo(5) = ....
十、匿名函数和自执行函数
匿名函数:也叫拉姆达函数,说白了就是没有名字的函数
自执行函数:IIFE,也叫立即执行函数。不需要进行调用,随着程序的执行会自动调用。IIFE有两部分组成,函数体和执行部分。
//匿名函数的使用
//1. 声明一个变量,来接收一个匿名函数
var f = function () {
};
//事件驱动 window窗口对象的点击事件
var count = 0;
window.onclick = function () {
console.log("第" + (++count) +"次点击了窗口...")
};
//IIFE自执行函数
;(function (a,b) {
console.log(a + b);
})(10,20);
var result = (function (a,b) {
return a + b;
})(10,20);
console.log("result:",result);
十一、闭包函数
什么是闭包?
闭包就是一个函数可以访问另外一个函数内部的变量。
想要理解闭包,需要先了解 作用域链的问题 和 垃圾回收机制的问题(设置到变量的生命周期)。
在JS中,任何一个函数都可以认为是闭包;常见的情况是,函数中可以访问全局变量,嵌套函数可以访问祖先元素中的变量。
function f(){
var i = 1;
function f2(){
console.log(i);
}
return f2;
}
//f函数中的 变量i 是一个局部变量,局部变量只能在函数的内部被访问到
function f3(){
console.log(i);
}
如果想要在一个函数的外部,访问这个函数内部的变量,那么该怎么操作?
假设,我们将嵌套在内部的函数作为一个联通内外的桥梁,在调用外层函数时,返回这个内部的函数
function f() {
var i = 1;
return function () {
return i++;
}
}
console.log(f);
console.log(f());
console.log(f()());//1
console.log(f()());//1
//也就证明,每一次都是一个全新的环境,得到的值也是一样的 局部变量和函数在外部函数调用完毕后即销毁
//那么,利用对象之间的引用关系,可以保存当时执行的一个环境,进而将局部的变量保存下来
//垃圾回收机制中:对存在引用关系的对象 不会进行回收
var test = f();//此时test就是一个函数
console.log(test)
console.log(test());//1
console.log(test());//2
console.log(test());//3
var test2 = f();//此时又是一个新的闭包函数 当执行f()时,传递给test2的那个桥梁函数会记住当时所执行的上下文环境
console.log(test2);
console.log(test2());//1
console.log(test2());//2
闭包的问题:
闭包不宜使用过多,容易造成内存的泄漏问题;
但是,使用闭包可以避免全局变量的污染,很多高级程序中,都会有闭包的应用。后面在高级课,会利用闭包来私有化对象的属性和方法。
闭包目前可以用来解决:
循环中,有异步语句存在的情况下,无法正确获取索引的问题。
// console.log(1111);
//回调函数:将一个函数作为另外一个函数的参数
//在JS中,有一个内置的延迟器,在指定时间后,去执行相应的操作
// setTimeout(function () {
// console.log(2222);
// })
// console.log(3333);
//循环中,异步语句,获取索引的问题
// for (var i = 0; i < 10; i++) {
// console.log("i:",i);
// }
for (var i = 0; i < 10; i++) {
(function (j) {
setTimeout(function () {
console.log("内j:",j);
})
})(i);
}
console.log("外i:",i);
附:异常捕获与抛出
异常:当 JavaScript 引擎执行 JS 代码时,发生了错误,导致程序停止运行;
异常抛出:当异常产生,并且将这个异常生成一个错误信息。
初级开发人员往往很少使用 JS 的抛出和捕获异常,但抛出和捕获异常往往是非常必要的。
捕获异常的语法
-
第一种:
try{ //运行代码 }catch(err){ //处理错误 console.log(err); //错误信息 }
-
第二种:文章来源:https://www.toymoban.com/news/detail-609633.html
try{ //运行代码 }catch(err){ //处理错误 console.log(err); //错误信息 }finally{ //文件的关闭,标记的取消等等 }
- try块:包含的是可能产生异常的代码,在这里面直接或者在里面通过调用函数里间接抛出的异常都可以捕获到。部分浏览器还可以找到具体抛出的位置;
-
catch块:捕获异常,并处理异常的地方,包括条件捕获和非条件捕获;
-
条件捕获,如
catch(e instanceof obj)
的形式,用instanceof
判断异常的对象类型,实现指定的异常处理方式。 -
非条件捕获,如 catch(e) 的形式,当异常抛出时,无论异常的类型都进行捕获并处理。这里有两点注意
- 如果条件捕获和非条件捕获共用,那么非条件捕获必须放在最后,因为它是无条件的捕获类型,捕获后会忽略后面的任意 catch 块。
- 另外,对异常的处理必须考虑周全,在 catch 里要么处理所有的异常,要么再次抛出异常(假定外层还有异常处理),否则在调试过程中会非常困难,因为出现的异常被忽略了
-
条件捕获,如
- finally块:无论是否捕获异常,都会在 try 或 catch块后立即执行;finally块常常用以文件的关闭,标记的取消等操作,更多的时候作为一种 ”优雅的失败“ 而存在,常常代替 catch 块
- 在 JS 的DOM对象中,还有一个
window.onerror
作为事件监听来从全局处理JS运行时的错误。由于浏览器的差异,实际上许多错误事件不能触发window.onerror
,因此要小心使用 - 抛出异常的 throw 语句:JS 里用 throw 指定一个用户定义的异常类型, 并抛出。抛出点可以在 try 或 catch 块中(一般都是配合try…catch使用)
代码示例:文章来源地址https://www.toymoban.com/news/detail-609633.html
var sub = document.getElementById('submit');
sub.addEventListener("click",rex);
function rex(){
try{
//自定义异常抛出
var username = document.getElementById('username').value;
if(username == ""){
throw("输入的用户名为空");//抛出异常
}
}catch(e){
//TODO handle the exception
alert(e);//弹出异常信息
}
}
到了这里,关于【前端JS交互篇】函数、参数、返回值、闭包函数、递归函数、内存、模块化编程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!