【前端JS交互篇】函数、参数、返回值、闭包函数、递归函数、内存、模块化编程

一、函数的声明与调用

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引擎在解析代码时,分成两步:

  1. 预解析阶段:将所有变量 和 函数的声明,提升到当前作用域的最顶部
  2. 运行阶段:进行变量的赋值操作

在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");
    

使用标准的方式创建的函数,与其它两种方式在调用时的不同:

  1. 如果都是先声明,再去调用,看不出有什么区别:上面已经验证过 f() f1() f3() 结果一致

  2. 如果时先调用,再去声明的情况,那么结果就不同了,如下代码

    非标准的方式,利用的是一个变量来接收一个函数,那么其本质就是变量。前面讲过变量的提升,相当于将变量的声明提升到当前作用域的最顶部,这时还没有赋值,在调用的时候,变量的值是undefined。所以后面两种创建函数的方式,只能先声明再调用。

    **标准的方式:**会将整体作为一个声明部分,再预解析阶段,将其整体提升到当前作用域的最顶部,所以标准的方式创建的函数可以在当前作用域的任何为止被调用。

小问题:如果在同一个文件中,存在同名的变量 和 函数,函数优先于变量提升;而前面讲到,在一个文件中,声明多个变量,只有第一个会生效,而函数也可以看作是变量,不管使用var还是使用function都叫做声明。所以函数优先于变量提升后,其它变量的声明都变成了赋值操作。

六、模块化编程

假设有一个酒店:酒店中有保安负责安保、前台负责接待、服务生负责提供服务、厨师负责做饭、大堂经理负责协调等等。

这时,如果将安保、前台、服务生、厨师、协调等工作全部交给大堂经理一个人去做,这样效率会很慢,酒店几乎没有多久肯定会倒闭。此时,酒店提供所有的功能都在大堂经理一个人身上,肯定是不行。

这时,酒店考虑到经营问题,又招来了保安、前台等等人,负责其自己相应的领域工作,然后大堂经理只需要调配这些人即可。这样效果就会很高了。

那么,代码中也是一样,尽可能的减少代码的耦合性。我们可以利用函数,将功能封装的尽量单一,在不同的场景下,可以被重复的利用,优化了代码又提高了执行的效果。这些,其实就是模块化的思想。

后面随着课程的深入,我们会接触到 seajs、requirejs、nodejs 这些东西,都是用来进行模块化开发的,不同之处在于其将一个js文件作为一个模块,可以避免全局变量的污染。

这里,我们使用函数来体会模块化编程的思想。

人类从古至今,习惯将事情分工,将一些内容做成一些公共模块,模块可以重复反复使用。
模块化编程:将一些基础的公共的部分单独封装到一个函数内,可以多次被调用。

**案例:**输出100以内的质数,模块化编程。
逆向思维的过程:输出100以内的质数 → 判断是不是质数 → 找约数个数

练习:找100以内的完美数:(一个数的约数除了它本身外其他约数和还等于这个数)。
注意:模块化编程,可以让我们的程序更加优化,各个小模块要尽量功能单一,提高重复使用率。

**练习:**赢数是一种特殊的自然数,除去它本身以外所有的约数和大于其本身。请输出100以内所有的赢数

练习:如果整数 A 的全部约数(包括1,不包括 A 本身)之和等于 B,且 整数 B 的全部约数(包括1,不包括B 本身)之和等于 A,则称整数 A 和 B是一对亲密数。输出1000以内的亲密数。

七、作业

  1. 封装一个函数,实现功能:不借助临时变量,进行两个整数的交换。

    提示:运算符、函数有两个参数

  2. 封装一个函数,求3个数中的最大值

    提示:函数有三个参数、要求至少使用两种方式实现

  3. 封装一个函数,实现在页面上输出 100 ~ 1000之间所有的质数,并要求每行显示 6 个数字

  4. 封装一个函数,实现 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,其它,前端,javascript,函数,闭包,模块化编程)