JavaScript函数
本章主要学习JavaScript的函数相关知识,包括递归和闭包,关于递归,其作用最主要就是实现深克隆,但现在structuredClone()可以实现,虽然有些情况不能使用,但是基本满足大多数情况了,这里学习递归是巩固基础知识,关于闭包,其作用最主要就是实现私有变量,现代开发中也有可以代替,#号关键词,但是其作用是在class,总之,这两个属于基础,有学习的必要,但是深入了解对现代开发意义不是很大
什么是函数
先从一个问题说起:假如我们要分别计算1到10、5到12、14到35的整数和,不用函数的情况下代码是这样的:
for (var i = 1, sum = 0; i <= 10; i++) {sum += i;}console.log(sum);for (var i = 5, sum = 0; i <= 12; i++) {sum += i;}console.log(sum);这些代码高度重复,只是变量不同,使用函数就可以解决这个问题;函数可以理解为:一个神奇的机器,送入机器两 个数字,它将产生两个数字之 间所有整数的和。
总结:
- 函数就是语句的封装,可以让这些代码方便地被复用
- 函数具有“一次定义,多次调用”的优点
- 使用函数,可以简化问题,让代码更具有可读性
函数的定义和调用
这里的函数声明提升,是JavaScript的机制,但是在现代开发中应该避免这种写法,很容易引发bug
函数的定义
和变量类似,函数必须先定义然后才能使用,函数定义有两种,分别是使用function关键字定义和函数表达式匿名函数
//使用function关键字定义函数function fun() { // 函数体语句}
//函数表达式匿名函数var fun = function () { // 函数体语句}函数的调用
执行函数体中的所有语句,就称为“调用函数”;调用函数非常简单,只需在函数名字后书写圆括号对即可
fun() // 调用函数;圆括号中是实参列表,如果没有实参,也要书写圆括号语句执行顺序
- 函数不调用,内部语句就不会执行
- 函数调用后,函数体内所有语句执行完毕, 将语句执行权交还给主程序
函数声明的提升
和变量声明提升类似,函数声明也可以被提升,这里需要注意,函数内部的变量也会提升,在函数内部提升
fun();function fun() { alert("函数被执行");}//在预解析阶段会被提升函数表达式不能提升
fun(); // 引发错误var fun = function () { alert("函数被执行");}这个var fun会被当做一个变量提升,但是提升的是变量,不是提升函数,所以会引发错误
函数优先提升
这是一种比较极端的现象,表达式函数和普通函数同时存在 一个相同的函数名,结论是:第一次弹出 B,第二次弹出 A。函数声明先准备好,代码再从上往下执行;函数表达式的赋值要等执行到那一行才生效。 因此,第一次调用得到 B,赋值之后第二次调用得到 A。
//函数优先提升//变量声明提升,无法覆盖提升的函数fun();// 弹出Bvar fun = function () { alert('A');}function fun() { alert('B');}fun();// 弹出A函数的参数和返回值
函数的参数
参数是函数内的一些待定值,在调用函数时,必须传入这些 参数的具体值函数的参数可多可少,函数可以没有参数,也可以有多个参 数,多个参数之间需要用逗号隔开
//圆括号中定义“形式参数”function add(a, b) { var sum = a + b; console.log('两个数字的和是' + sum);}add(3, 5);//调用函数时传入“实际参数”形实结合
- 在上面例子中,3对应a,5对应b
- 如果add(3, 5,6);而add(a, b),此时6没有形参接受他,会被直接忽略
- 如果add(3, 5);而add(a, b,c),此时c没有实参传递他,c值为undefined
- 以上两点中,被忽略或者undefined,开发中一般都不是我们想看到的,这也是JS松散语法容易导致bug的原因,在typescript中会得到解决
arguments
-
函数内arguments表示它接收到的实参列表,它是一个类数组对象
-
类数组对象:所有属性均为从0开始的自然数序列,并且有 length属性,和数组类似可以用方括号书写下标访问对象的某个属性值,但是不能调用数组的方法
//不管用户传递多少参数,都能计算它们和function fun(){var sum = 0for(var i = 0;i<arguments.length;i++){sum += arguments[i]}console.log('所有参数和' + sum);//console.log(arguments);//console.log(arguments[0]);// console.log(arguments[1]);}fun(33,44,55,66)
函数的返回值
-
函数体内可以使用return关键字表示“函数的返回值”
function sum(a, b) {return a + b;}var result = sum(3, 5); -
调用一个有返回值的函数,可以被当做一个普通值,从而可以出现在任何可以书写值的地方
function sum(a, b) {return a + b;}var result = sum(3, sum(4, 5));
遇见return即退出函数
-
调用函数时,一旦遇见return语句则会立即退出函数,将执行权交还给调用者
function fun() {console.log('A');return 'B';console.log('C');}console.log(1);var char = fun();console.log(char);console.log(2); -
结合if语句的时候,往往不需要写else分支了
-
比如题目:请编写一个函数,判断一个数字是否是偶数
function checkEven(n){if(n % 2 == 0){return true}return false}var result = checkEven(7);console.log(result);
函数算法题
寻找喇叭花数
-
题目:喇叭花数是这样的三位数:其每一位数字的阶乘之和 恰好等于它本身。即abc = a! + b!+ c!,其中abc表示一 个三位数。试寻找所有喇叭花数。
-
思路:将计算某个数字的阶乘封装成函数,这样可以让问题 简化。
// 求阶乘//5! = 5 × 4 × 3 × 2 × 1 = 120//3! = 3 × 2 × 1 = 6//1! = 1//0! = 1function factorial(n) {var result = 1;for (var i = 1; i <= n; i++) {result *= i;//result=1*1;result=1*2;result=2*3;result=6*4 等于1*2*3*4=24}return result;}// 寻找喇叭花数for (var num = 100; num <= 999; num++) {var a = parseInt(num / 100, 10); // 百位var b = parseInt(num / 10, 10) % 10; // 十位var c = num % 10; // 个位var sum = factorial(a) + factorial(b) + factorial(c);if (sum === num) {console.log(num);}}
JavaScript内置sort()方法
-
数组排序可以使用sort()方法,这个方法的参数又是一个函 数。
var arr = [33, 22, 55, 11];arr.sort(function (a, b) {}); -
这个函数中的a、b分别表示数组中靠前和靠后的项,如果 需要将它们交换位置,则返回任意正数;否则就返回负数。
var arr = [33, 22, 55, 11];//排序arr.sort(function (a, b) {if (a > b) {return 1;} else {return -1;}});console.log(arr);// 方式2var arr = [33, 22, 55, 11];arr.sort(function (a, b) {return a-b;});console.log(arr);
递归
什么是递归
- 函数的内部语句可以调用这个函数自身,从而发起对函数的一次迭 代。在新的迭代中,又会执行调用函数自身的语句,从而又产生一 次迭代。当函数执行到某一次时,不再进行新的迭代,函数被一层 一层返回,函数被递归。
- 递归是一种较为高级的编程技巧,它把一个大型复杂的问题层层转 化为一个与原问题相似的规模较小的问题来求解。
用求4的阶乘举例

-
最开始求4的阶乘,只需知道3的阶乘 4;3的阶乘等于2的阶乘 3;2的阶乘等于1的阶乘 * 2;1的阶乘等于1;
-
1的阶乘等于1;那2的阶乘等于1 * 2就是2;3的阶乘等于3 * 2就是6;4的阶乘等于4 * 6就是24,最终等于24
function factorial(n) {// 递归出口if (n === 0) return 1;// 递归调用return n * factorial(n - 1);}console.log(factorial(4)); -
递归的要素:边界条件:确定递归到何时终止,也称为递归出口;递归模式:大问题是如何分解为小问题的,也称为递归体
递归常见算法题
斐波那契数列
-
斐波那契数列是这样的数列:1、1、2、3、5、8、13、21
-
规律:数列下标为0和1的项的值都是1,从下标为2的项开始,每项等于 前面两项的和
//编写一个函数,返回斐波那契数列中下标为n的那项的值function fibonacci(n) {// 递归出口if(n == 0 || n ==1) return 1;// 递归调用return fibonacci(n - 1) + fibonacci(n - 2);}// console.log(fibonacci(6));function fun(n){for(var i = 0; i < n; i++){console.log(fibonacci(i));}}//计算前15项斐波那契数列fun(15)
递归实现深克隆
复习引用类型值的相关知识
| 类型 | 举例 | 当 var a = b 变量传值时 | 当用 == 比较时 |
|---|---|---|---|
| 基本类型值 | 数字型、字符串型、布尔型、undefined 型 | 内存中产生新的副本 | 比较值是否相等 |
| 引用类型值 | 对象、数组 | 内存中不产生新的副本,而是让新变量指向同一个对象 | 比较内存地址是否相同,即比较是否是同一个对象 |
复习浅克隆
-
使用var arr2 = arr1这样的语句不能实现数组的克隆
-
浅克隆:准备一个空的结果数组,然后使用for循环遍历原 数组,将遍历到的项都推入结果数组
var arr1 = [33,44,55,66,77,88,99,100];//var arr2 = arr1;//arr1.push(111);// arr2 也会被改变//console.log(arr2);// arr1 和 arr2 是同一个数组// console.log(arr1 === arr2);//准备一个结果数组var result = [];for(var i = 0; i < arr1.length; i++){result.push(arr1[i]);}arr1.push(222);// result 不会被改变console.log(result);console.log(arr1);// arr1 和 result 是不同的数组console.log(arr1 === result); -
浅克隆只克隆数组的一层,如果数组是多维数组,则克隆的 项会“藕断丝连”。
var arr1 = [33,44,55,66,[77,88]];var result = [];for(var i = 0; i < arr1.length; i++){result.push(arr1[i]);}arr1[4].push(222);console.log(result);//[33,44,55,66,[77,88,222]] 浅克隆
实现深克隆
-
使用递归思想,整体思路和浅克隆类似,但稍微进行一些改 动:如果遍历到项是基本类型值,则直接推入结果数组;如 果遍历到的项是又是数组,则重复执行浅克隆的操作。
var arr1 = [33,44,55,66,[77,88]];//实现深度拷贝function deepCopy(arr){var result = [];for(var i = 0; i < arr.length; i++){// 递归调用// 递归调用的条件:当前元素是数组if(Array.isArray(arr[i])){result.push(deepCopy(arr[i]));}else{// 非数组元素,直接添加到结果数组result.push(arr[i]);}}return result;}arr2 = deepCopy(arr1)console.log('arr2的数组:',arr2);//[33,44,55,66,[77,88]]console.log( arr1 === arr2);//falsearr1[4].push(222);console.log('arr2的数组:',arr2);//[33,44,55,66,[77,88]]console.log('arr1的数组:',arr1);//[33,44,55,66,[77,88,222]]
全局变量和局部变量
变量作用域
-
JavaScript是函数级作用域编程语言:变量只在其定义时所 在的function内部有意义。
function fun() {var a = 10;}fun();console.log(a); // 报错//变量a是在fun函数中被定义的,所以变量a只在fun函数内部有定义,fun函数就是a的“作用域”,变量a被称为局部变量。
全局变量
-
如果不将变量定义在任何函数的内部,此时这个变量就是全 局变量,它在任何函数内都可以被访问和更改。
var a = 10;function fun() {a++;console.log(a); // 输出11}fun();console.log(a); // 输出11变量a没有定义在任何函数内部,它是“全局变量”var a = 10;function fun() {var a = 5;a++;console.log(a); // 输出6}fun();console.log(a); // 输出10
遮蔽效应
-
如果不将变量定义在任何函数的内部,此时这个变量就是全 局变量,它在任何函数内都可以被访问和更改
var a = 10;//变量a没有定义在任何函数内部,它是“全局变量function fun() {a++;//这是全局变量aconsole.log(a); // 输出11}fun();console.log(a); // 输出11 -
如果函数中也定义了和全局同名的变量,则函数内的变量会 将全局的变量“遮蔽”
var a = 10;function fun() {var a = 5;//局部变量a将全局变量a“遮蔽”了a++;//改变的是局部变量a的值,不影响全局变量console.log(a); // 输出6}fun();console.log(a); // 输出10
注意考虑变量声明提升的情况
var a = 10;function fun() { //变量提升 var a a++; //局部变量a被自增1,a此时是undefined,自增1结果是NaN var a = 5;//重新将a赋值为5 console.log(a);// 输出5}fun();console.log(a);// 输出10形参也是局部变量
var a = 10;function fun(a) {//形参a也是函数内部的局部变量 a++;//8 console.log(a);}fun(7);console.log(a);//10作用域链
-
先来认识函数的嵌套:一个函数内部也可以定义一个函数。 和局部变量类似,定义在一个函数内部的函数是局部函数。
function fun() {function inner() {console.log('你好');}inner();// 调用内部函数}fun();// 调用外部函数 -
在函数嵌套中,变量会从内到外逐层寻找它的定义。
var a = 10;var b = 20;function fun() {var c = 30;function inner() {var a = 40;var d = 50;console.log(a, b, c, d);//使用变量时,JS会从当前层开始,逐层向上寻找定义 [40 20 30 50]}inner();}fun();
不加var将定义全局变量
-
在初次给变量赋值时,如果没有加var,则将定义全局变量。
function fun() {a = 3;}fun();console.log(a); // 3function fun() {var a = 3;}fun();console.log(a);//报错 a is not defined
闭包
什么是闭包
从一个题目开始
这个程序的运行结果是什么呢?
function fun() { var name = '慕课网'; function innerFun() { alert(name);} return innerFun;//返回了内部函数}var inn = fun();//内部函数被移动到了外部执行inn();- fun函数里面定义变量name是慕课网,并且定义一个函数innerFun,innerFun弹出name,也就是慕课网,fun函数返回innerFun函数,执行fun函数,inn变量接受,inn接受的是fun返回的innerFun函数,innerFun执行,弹出name,也就是慕课网
什么是闭包
- JavaScript中函数会产生闭包(closure)。闭包是函数本身和该函数声明时所处的环境状态的组合。
- 函数能够“记忆住”其定义时所处的环境,即使函数不在其 定义的环境中被调用,也能访问定义时所处环境的变量
观察闭包现象
- 在JavaScript中,每次创建函数时都会创建闭包。
- 但是,闭包特性往往需要将函数“换一个地方”执行,才能 被观察出来。
闭包非常实用
- 闭包很有用,因为它允许我们将数据与操作该数据的函数关联 起来。这与“面向对象编程”有少许相似之处。
- 闭包的功能:记忆性、模拟私有变量
闭包的记忆性-案例
- 当闭包产生时,函数所处环境的状态会始终保持在内存中,不会在外层函数调用后被自动清除。这就是闭包的记忆性。
闭包的记忆性举例
-
创建体温检测函数checkTemp(n),可以检查体温n是否正常 ,函数会返回布尔值。
-
但是,不同的小区有不同的体温检测标准,比如A小区体温合 格线是37.1℃,而B小区体温合格线是37.3℃,应该怎么编程 呢?
function createCheckTemp(standardTemp){function checkTemp(temp){if(temp <= standardTemp){console.log('你的体温正常');}else{console.log('你的体温异常');}}return checkTemp;}var checkTemp = createCheckTemp(37.1);//创建一个检查体温的函数,标准体温为37.1度var checkTemp1 = createCheckTemp(37.3);//创建一个检查体温的函数,标准体温为37.3度checkTemp(36.5);checkTemp(37.5);checkTemp1(37.2);
ES6版本实现记忆性:
class CheckTemp { #standardTemp; // 私有字段:标准体温
constructor(standardTemp) { this.#standardTemp = standardTemp; }
checkTemp(temp) { if (temp <= this.#standardTemp) { console.log('你的体温正常'); } else { console.log('你的体温异常'); } }}
// 创建两个不同标准的体温检测实例const checkTemp = new CheckTemp(37.1);const checkTemp1 = new CheckTemp(37.3);
// 检测体温checkTemp.checkTemp(36.5); // 你的体温正常checkTemp.checkTemp(37.5); // 你的体温异常
checkTemp1.checkTemp(37.2); // 你的体温正常checkTemp1.checkTemp(37.5); // 你的体温异常闭包模拟私有变量
-
题目:请定义一个变量a,要求是能保证这个a只能被进行指 定操作(如加1、乘2),而不能进行其他操作,应该怎么编 程呢?
-
在Java、C++等语言中,有私有属性的概念,但是JavaScript 中只能用闭包来模拟。
function fun(){var a = 0;return {getA: function(){console.log(a);},add:function(){a++;},pow:function(){a *=2;}}}var obj = fun();//如果想在fun函数中访问a变量,唯一的方法是通过getA方法访问a变量obj.getA();obj.add();obj.getA();obj.pow();obj.getA();
使用闭包的注意点
- 不能滥用闭包,否则会造成网页的性能问题,严重时可能导致 内存泄露。所谓内存泄漏是指程序中己动态分配的内存由于某 种原因未释放或无法释放。
- 所谓内存泄漏是指程序中己动态分配的内存由于某种原因未释 放或无法释放。
闭包的一道面试题
function addCount() { var count = 0; return function () { count = count + 1;//count++也可以;1 console.log(count);//1 };}var fun1 = addCount();var fun2 = addCount();fun1();//1fun2();//1fun2();//2fun1();//2- 闭包具有记忆性
立即执行函数IIFE
什么是IIFE
IIFE(Immediately Invoked Function Expression,立即 调用函数表达式)是一种特殊的JavaScript函数写法,一旦 被定义,就立即被调用。
(function () { statements})();形成IIFE的方法
函数不能直接加圆括号被调用
function() { alert(1)}();//错误函数必须转为“函数表达式”才能被调用。
(function() { alert(1)})();
+function() { alert(1)}();
-function() { alert(1)}();| 写法 | 作用 | 最终表达式的值 | 推荐程度 |
|---|---|---|---|
(function(){})() | 括号转换为表达式并立即调用 | 函数的返回值 | 推荐 |
+function(){}() | 一元加号转换为表达式并立即调用 | 返回值经过数值转换 | 不推荐 |
-function(){}() | 一元减号转换为表达式并立即调用 | 返回值数值转换后取负 | 不推荐 |
IIFE的作用1 - 为变量赋值
为变量赋值:当给变量赋值需要一些较为复杂的计算时(如 if语句),使用IIFE显得语法更紧凑。
var age = 12;var sex = '男';var title = (function () { if (age < 18) { return '小朋友' } else { if (sex == '男') { return '先生'; } else { return '女士'; } }})();IIFE的作用2
var arr = [];for (var i = 0; i < 5; i++) { arr.push(function () { console.log(i);//变量i是全局变量,所有函数都共享内存中的同一个变量i});}arr[1]();//5IIFE的作用 - 将全局变量变为局部变量
IIFE可以在一些场合(如for循环中)将全局变量变为局部变 量,语法显得紧凑
var arr = [];for (var i = 0; i < 5; i++) {(function (i) { arr.push(function () { console.log(i);
});})(i);//这是立即执行函数,i是当前循环的变量}arr[2](); // 弹出2文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!














