JavaScript 自定义函数

一、函数概述

函数是可重用代码块,完成特定功能,避免重复编码,且仅在调用时执行,可规避页面载入时脚本自动执行问题。

二、函数定义方式

(一)Function 构造函数定义函数

javascript

运行

var 函数名 = new Function("参数1", "参数2", … "函数体");

特点

  • Function 首字母大写,仅在 JavaScript 1.1+ 可用。
  • 每次调用构造函数会解析函数体、创建新函数对象,效率低,实际开发少用。

(二)function 语句定义函数(最常用)

javascript

运行

function 函数名(参数1, 参数2 … [形参]) {
    <语句块>
    return 返回值;
}

关键说明

  • function 关键字:用于声明函数。
  • 函数名:合法标识符,区分大小写,需唯一。
  • 参数:0 个或多个,多参数用逗号分隔,无论有无参数,小括号必选。
  • 函数体:大括号包裹的代码逻辑。
  • 返回值:return 关键字可选,决定函数返回结果,无 return 则默认返回 undefined。
  • 预解析特性:定义的函数会被预解析,优先执行,可在定义前调用。

(三)表达式中定义函数

javascript

运行

var 函数名 = function(参数1, 参数2, …) { 函数体 };

特点

  • 函数名本质是变量名,函数赋值给变量,JS 1.2+ 可用。
  • 示例(禁用鼠标右键):

javascript

运行

var rightKey = function() {
    if (event.button == 2) {
        alert("禁止使用鼠标右键");
    }  
};
window.onmousedown = rightKey;

三、函数调用

1. 直接调用

直接通过函数名调用,如 myFunction(); 或 window.myFunction();(函数挂载到 window 时)。

2. 事件处理调用

在 HTML 事件属性中绑定,如:

html

预览

<div onclick="myFunction()"></div>

3. 赋值调用

将函数返回值赋给变量,如:

javascript

运行

var t = myFunction();

四、函数参数

1. 形参 vs 实参

  • 形参:函数定义时,括号内声明的参数(形式参数 ),JS 支持给形参设默认值。
  • 实参:调用函数时,传入的实际参数 。

2. 参数匹配规则

默认形参与实参一一对应,不匹配时:

  • 实参数量 < 形参数量:多余形参值为 undefined。
  • 实参数量 > 形参数量:多余实参被忽略。

3. 特殊参数对象 arguments

函数内可通过 arguments 获取所有实参,它是类数组对象,支持下标访问、length 属性。

示例

javascript

运行

function Test(a, b) {
    alert(typeof(arguments)); // 输出 "object",类数组类型
    alert(arguments[1]);      // 获取下标为 1 的实参值
    alert(arguments.length);  // 实参数量
    alert(arguments.callee);  // 函数自身(严格模式禁用)
}
Test("zhang", "li");

4. 默认参数示例

定义带默认参数的函数,不传参时用默认值,如创建表格函数:

javascript

运行

function tab(row, color = 'pink', color2 = 'gray') {
    document.write('<table border="1" width="500">');
    for (var index = 0; index < row; index++) {
        if (index % 2 == 1) {
            document.write('<tr bgcolor="' + color + '"><td>' + index + '</td><td>1</td><td>1</td><td>1</td><td>1</td></tr>');
        } else {
            document.write('<tr bgcolor="' + color2 + '"><td>' + index + '</td><td>1</td><td>1</td><td>1</td><td>1</td></tr>');
        }
    }
    document.write('</table>');
}
// 调用,可传参覆盖默认值
tab(10, 'gray', 'orange');

五、函数返回值

  • return 关键字决定函数返回值,无 return 则默认返回 undefined。
  • 执行 return 后,函数立即停止执行,后续代码不运行。
  • 需在函数外使用内部变量结果时,用 return 返回值。

六、变量的作用域

(一)全局变量与局部变量

  • 全局变量:在函数外部声明的变量,作用域覆盖整个 JS 文档,任意位置(包括函数内部)可访问、修改。
  • 局部变量:在函数内部用 var 声明的变量,仅在当前函数内部生效,函数外部无法访问。

(二)作用域交互规则

1. 全局变量访问局部

全局变量的作用域包含所有局部作用域,因此可在函数内部的任意位置直接使用全局变量。

2. 局部变量暴露到全局

局部变量默认仅在函数内可用,若需在全局使用,有两种方式(需谨慎使用,避免全局污染):

  • 方式 1:移除局部变量的 var 声明,使其隐式成为全局变量(不推荐,易与现有全局变量冲突)。
  • 方式 2:通过函数的 return 语句,将局部变量的值返回给全局,再用全局变量接收(推荐,更规范)。

(三)块级作用域(ES6 新增)

在 ES6 之前,JS 无块级作用域(iffor 等代码块无法限制 var 变量的作用域);ES6 中通过 letconst 关键字配合 {} 形成块级作用域,变量仅在当前代码块(如 if {}for {})内有效,出块后无法访问。

(四)作用域链【了解】

1. 概念
  • 只要声明变量,就会产生作用域(全局作用域是默认顶层作用域)。
  • 函数内部会形成局部作用域,若函数内嵌套函数,会进一步产生嵌套作用域(子作用域)。
  • 当在当前作用域查找某个变量时,若未找到,会自动向上级作用域逐层查找,直至全局作用域,这个逐层查找的过程形成的链条,称为作用域链
2. 访问规则
  • 内部作用域(子作用域)可通过作用域链访问所有外部作用域(父作用域、祖父作用域… 全局作用域)的变量和函数。
  • 外部作用域无法访问内部作用域的任何变量和函数,即 “内层可访问外层,外层不可访问内层”。
3. 示例分析

javascript

运行

var n = 10; // 全局变量
function outer() {
  function inner() {
    function center() {
      console.log(n); // 查找链:center → inner → outer → 全局
    }
    center();
  }
  inner();
  var n = 15; // outer 内局部变量,预解析时声明提升,赋值在后
}
outer(); // 输出 undefined(因 outer 内 n 声明提升,center 查找时取 outer 内未赋值的 n)
4. 经典案例

javascript

运行

var a = 1;
function fn1() {
  function fn2() {
    console.log(a); // 查找链:fn2 → fn3 → fn1 → 全局,最终取 fn1 内的 a=2
  }
  function fn3() {
    var a = 4; // fn3 内局部变量,不影响 fn2 查找
    fn2();
  }
  var a = 2; // fn1 内局部变量,覆盖全局 a=1
  return fn3;
}
var fn = fn1(); // 调用 fn1,返回 fn3 函数
fn(); // 调用 fn3,执行 fn2,输出 2

七、预解析(变量 / 函数提升)

(一)核心原理

JS 代码由浏览器的 JS 解析器执行,执行过程分为两步:预解析 → 代码执行

  • 预解析:在代码真正执行前,解析器会提前处理带 var 和 function 关键字的内容,分为 “变量提升” 和 “函数提升”。
  • 代码执行:预解析完成后,按代码书写顺序自上而下执行。

(二)预解析规则

1. 变量提升(var 关键字)
  • 仅 “声明” 变量,不进行赋值,变量默认值为 undefined
  • 示例:var num = 10; 预解析时,仅执行 var num;num = 10 需在代码执行阶段完成。
2. 函数提升(function 关键字)
  • 既 “声明” 函数,又 “定义” 函数(即提前创建函数对象,可直接调用)。
  • 示例:function fun() {} 预解析时,fun 会被提前挂载到当前作用域,可在函数定义前调用。
3. 优先级
  • 函数提升优先级高于变量提升:若变量和函数同名,预解析后函数会覆盖变量的声明(但代码执行阶段,变量赋值会覆盖函数)。
  • 预解析仅在当前作用域生效:全局作用域预解析全局变量 / 函数,函数执行时才会预解析函数内部的变量 / 函数。

(三)经典案例解析

案例 1:函数内变量提升

javascript

运行

var num = 10;
fun(); // 调用函数(函数提升,可提前调用)
function fun() {
  console.log(num); // 预解析:fun 内提升 var num,未赋值 → 输出 undefined
  var num = 20; // 代码执行阶段:给 num 赋值 20
}
// 预解析流程:
// 全局:提升 var num; 、function fun() {}
// fun 内:提升 var num;
// 代码执行流程:
// 全局 num = 10 → 调用 fun → 执行 console.log(num)(undefined)→ num = 20
案例 2:变量与函数同名

javascript

运行

console.log(a); // 函数提升优先,输出函数体:function a() { console.log(123); }
var a = 'hello';
function a() {
  console.log(123);
}
console.log(a); // 代码执行阶段,变量赋值覆盖函数 → 输出 'hello'
// 预解析流程:
// 全局:提升 function a() {} → 提升 var a;(被函数覆盖)
// 代码执行流程:
// 第一个 console.log(a)(输出函数)→ a = 'hello' → 第二个 console.log(a)(输出 'hello')
案例 3:函数表达式的提升陷阱

javascript

运行

fn(); // 报错:fn 是变量提升(仅声明 undefined),不是函数提升,无法调用
var fn = function() {
  console.log(11);
};
// 预解析流程:
// 全局:提升 var fn;(值为 undefined),函数表达式不参与提升
// 代码执行流程:
// 调用 fn(undefined,类型错误)→ 后续代码不执行
案例 4:隐式全局变量

javascript

运行

f1(); // 调用 f1
console.log(c); // 输出 9(c 是隐式全局变量)
console.log(b); // 输出 9(b 是隐式全局变量)
console.log(a); // 报错:a 是 f1 内局部变量,全局不可访问
function f1() {
  var a = b = c = 9; // var a 是局部变量,b、c 无 var 声明,隐式全局
  console.log(a); // 输出 9
  console.log(b); // 输出 9
  console.log(c); // 输出 9
}
// 预解析流程:
// 全局:提升 function f1() {}
// f1 内:提升 var a;
// 代码执行流程:
// 调用 f1 → a=9(局部)、b=9(全局)、c=9(全局)→ 全局 log(c=9)、log(b=9)、log(a)(报错)
案例 5:多层作用域预解析

javascript

运行

var a = 18;
f1(); // 调用 f1
function f1() {
  var b = 9;
  console.log(a); // 预解析:f1 内提升 var a,未赋值 → 输出 undefined
  console.log(b); // 输出 9(f1 内局部变量)
  var a = 13; // 代码执行阶段:a 赋值 13
}
// 预解析流程:
// 全局:提升 var a; 、function f1() {}
// f1 内:提升 var b; 、var a;
// 代码执行流程:
// 全局 a=18 → 调用 f1 → b=9 → log(a=undefined) → log(b=9) → a=13

(四)核心结论

  1. 预解析是 JS 执行的重要特性,需重点关注 var 和 function 的提升差异,避免 “变量未定义” 或 “函数无法调用” 的问题。
  2. 推荐使用 ES6 的 let/const 替代 varlet/const 无提升特性,且支持块级作用域,能规避多数预解析陷阱。
  3. 函数优先用声明式(function fn() {}),避免函数表达式(var fn = function() {})的提升陷阱,确保函数可正常调用。

八、变量的生命周期【了解】

(一)核心规则

JavaScript 变量的生命周期始于声明,终于 “不再被需要”,具体销毁时机随作用域不同而差异:

  • 局部变量:在函数执行结束后,函数私有作用域会被销毁,局部变量随作用域释放而自动删除,无法再访问。
  • 全局变量:仅在页面关闭(或浏览器标签页关闭)时,全局作用域随页面销毁,全局变量才会被删除;页面运行期间,全局变量始终存在。

(二)经典问题案例:变量生命周期与事件绑定冲突

html

预览

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
</ul>
<script>
  var lis = document.getElementsByTagName("li");
  // 问题:var 声明的 i 是全局变量,循环结束后 i 固定为 6
  for(var i=0; i<lis.length;i++){
    lis[i].onclick = function(){
      alert(i); // 无论点击哪个 li,均弹出 6
    }
  }
</script>

原因var 无块级作用域,i 是全局变量,循环结束后 i 取值为 lis.length(6);点击事件触发时,访问的是全局 i,而非循环时的瞬时值(变量生命周期未随循环迭代结束而终止)。

九、变量作用域与生命周期案例解析

通过具体案例理解变量作用域对生命周期的影响,明确不同场景下变量的可访问性:

案例序号 代码示例 执行结果 核心原因
案例一

function test(){

  uname = "香水有毒";

}

console.log(uname);

报错(uname is not defined uname 是函数内隐式全局变量,但 test() 未调用,uname 未声明,全局作用域中无此变量。
案例二

function test(){

  uname = "香水有毒";

}

test();

console.log(uname);

输出 香水有毒 test() 调用后,uname 隐式成为全局变量,页面未关闭前全局可访问,生命周期延续至页面结束。
案例三

uname = "香水有毒";

function test(){

  console.log(uname);

}

test();

输出 香水有毒 全局变量 uname 声明在前,test() 内通过作用域链访问全局 uname,符合 “内层可访问外层” 规则。
案例四

var uname = "香水有毒";

function test(){

  console.log(uname);

}

function test2(){

  uname = "老鼠爱大米";

  test();

}

test2();

输出 老鼠爱大米 test2() 调用时修改全局 unametest() 访问的是修改后的全局变量(全局变量生命周期未中断,值可被修改)。
案例五

var uname = "香水有毒";

function test(){ 

  console.log(uname);

}            

function test2(){

  uname = "老鼠爱大米"; return test; } res=test2(); res();

输出 老鼠爱大米 test2() 返回 test 函数(未立即执行),res() 调用时,uname 已被 test2() 修改为全局值,全局变量生命周期覆盖函数调用全程。

十、自执行函数(匿名函数自调用)

(一)定义与核心作用

  • 定义:定义后立即执行,且后续不再调用的函数,因无需重复调用,通常省略函数名(匿名函数),语法上通过特殊符号触发 “立即执行”。
  • 核心作用:创建独立的局部作用域,保存变量的瞬时值,避免变量污染全局作用域(解决如 “li 点击事件索引异常” 等问题)。

(二)常见语法形式(6 种)

javascript

运行

// 1. 整个表达式用括号包裹(推荐,可读性高)
(function a(){ alert('我是自执行函数') } ()); 

// 2. 函数体用括号包裹
(function a(){ alert('我是自执行函数') }) (); 

// 3. 逻辑运算符触发(!、+、-、~,利用运算符强制表达式执行)
!function a(){ alert('我是自执行函数') }();
+function a(){ alert('我是自执行函数') }();
-function a(){ alert('我是自执行函数') }();
~function a(){ alert('我是自执行函数') }();

// 4. void 关键字触发(忽略返回值,仅执行函数)
void function a(){ alert('我是自执行函数') }();

// 5. new 关键字触发(创建临时对象,执行函数)
new function a(){ alert('我是自执行函数') }();

(三)语法注意事项

  • 分号必须性:若上一行代码未用分号结束,自执行函数前需加 ;,避免被解析为上一行代码的延续(如函数调用),导致语法错误:

    javascript

    运行

    ;(function(){ console.log('规范写法') })() // 推荐:前加分号,避免语法冲突
    

(四)核心应用:解决 li 点击事件索引问题

html

预览

<ul>
  <li>这是第1个li</li>
  <li>这是第2个li</li>
  <li>这是第3个li</li>
  <li>这是第4个li</li>
  <li>这是第5个li</li>
  <li>这是第6个li</li>
</ul>
<script>
var lis = document.querySelectorAll('li');
for(var i = 0;i<lis.length;i++){
  // 自执行函数创建独立作用域,接收当前循环的 i 值(实参)
  (function(i){
    lis[i].onclick = function (){
      console.log(i); // 点击第 n 个 li,输出 n-1(正确索引)
    }
  })(i) // 传入当前循环的 i,作为自执行函数的局部变量
}
</script>

原理:每次循环创建一个自执行函数,i 作为实参传入后成为函数局部变量,其生命周期被限制在自执行函数作用域内;点击事件访问的是局部 i(保存了循环瞬时值),而非全局 i,避免索引异常。

十一、闭包

(一)定义

闭包是 “能够读取其他函数内部变量的函数”,本质是连接函数内部与外部的桥梁。通常表现为 “函数内部嵌套的子函数”,且子函数被外部作用域引用(如返回给外部、赋值给全局变量)。

(二)核心特点

  1. 跨作用域访问:闭包(子函数)可访问外部函数的局部变量和参数,突破 “外层不可访问内层” 的作用域限制。
  2. 延长变量生命周期:外部函数执行结束后,其局部变量因被闭包引用,不会被垃圾回收机制回收,仍保存在内存中。
  3. 作用域依赖性:闭包的作用域链依赖于定义时的外部作用域,而非调用时的作用域(“词法作用域” 特性)。

(三)闭包的创建方式

方式 1:内部函数返回外部变量

javascript

运行

// 外部函数定义局部变量,内部函数返回该变量
function show() {
  var num = 10; // 外部函数局部变量
  return function() { // 闭包(内部函数)
    return num; // 访问外部函数的 num
  };
}
var closeFn = show(); // closeFn 引用闭包函数
console.log(closeFn()); // 输出 10(通过闭包访问外部函数局部变量)
方式 2:内部函数作为返回值调用

javascript

运行

function f1() {
  var n = 999; // 外部函数局部变量
  function f2() { // 闭包(内部函数)
    alert(n); // 访问 f1 的局部变量 n
  }
  return f2; // 返回闭包函数
}
f1()(); // 调用闭包函数,输出 999(f1 执行后,n 仍被闭包引用,未被回收)
方式 3:普通函数改造为闭包

将 “内部函数仅在内部调用” 的普通函数,改为 “内部函数被外部引用” 的闭包,核心是让内部函数脱离外部函数作用域并被外部访问:

javascript

运行

// 普通函数(非闭包):内部函数仅在外部函数内调用
function show() {
  var m = 1;
  function inner() {
    var n = 0;
    console.log(++n);
    console.log(++m);
  };
  inner(); // 内部函数仅在内部执行,m 随 show() 结束被回收
}
show(); // 输出 1、2;再次调用 show(),m 重新初始化为 1,输出 1、2

// 闭包函数:内部函数被外部引用
function show() {
  var m = 1;
  return function() { // 内部函数被返回给外部,成为闭包
    var n = 0;
    console.log(++n); // 每次调用输出 1(n 是闭包内局部变量,每次调用重新初始化)
    console.log(++m); // 每次调用输出 2、3、4...(m 被闭包引用,生命周期延长)
  };
}
var closeFn = show();
closeFn(); // 输出 1、2
closeFn(); // 输出 1、3

(四)闭包的用途

  1. 读取函数内部变量:突破作用域限制,在外部直接访问函数的局部变量(如上述案例中,通过闭包获取 show() 内的 m)。
  2. 保存变量瞬时状态:让变量值长期保存在内存中,实现 “状态记忆”(如累加计数器、记录函数调用次数)。
  3. 模块化开发:通过闭包创建私有变量和公有方法,实现 “数据封装”,避免全局变量污染(ES6 模块普及前的常用方案)。

(五)闭包的缺点与内存泄漏

1. 缺点
  • 内存占用高:闭包引用的外部变量不会被回收,长期积累会占用大量内存,导致网页性能下降(如频繁创建闭包未释放)。
  • 兼容性问题:在 IE 浏览器(尤其是 IE8 及以下)中,闭包若引用 DOM 元素,易形成 “循环引用”(闭包引用 DOM,DOM 引用闭包),导致内存泄漏。
2. 内存泄漏原理
  • 浏览器的垃圾回收机制(GC) 会自动回收 “无引用指向” 的内存;
  • 闭包引用外部函数的变量 → 外部函数作用域无法销毁 → 若闭包再被全局变量引用 → 相关内存始终被占用,无法被 GC 回收,形成内存泄漏。
3. 避免内存泄漏的方法
  • 手动解除引用:不再使用闭包时,将闭包变量赋值为 null,切断引用链(如 closeFn = null),让 GC 回收相关内存。
    示例:

    javascript

    运行

    function createClosure() {
      var data = "大量数据"; // 被闭包引用的变量
      return function() {
        console.log(data);
      };
    }
    var closeFn = createClosure(); // 引用闭包
    closeFn(); // 使用闭包:输出 "大量数据"
    
    // 不再使用时,手动解除引用
    closeFn = null; // 切断闭包与外部的关联,GC 可回收 data 占用的内存
    
  • 避免循环引用:不将 DOM 元素保存为闭包的局部变量,或在闭包执行后手动清除 DOM 引用。
    若业务需在闭包中使用 DOM 元素,需避免 “闭包引用 DOM + DOM 引用闭包” 的循环关系,使用后及时清除 DOM 引用:

    javascript

    运行

    function bindEvent() {
      var btn = document.getElementById("btn"); // DOM 元素
      var data = "按钮相关数据";
      
      // 避免闭包直接引用 btn,或使用后清除引用
      btn.onclick = function() {
        console.log(data); // 仅引用数据,不引用 DOM
      };
      
      // 或在事件执行后手动清除 DOM 引用(若闭包需保留)
      // btn.onclick = function() {
      //   console.log(data, btn);
      //   btn.onclick = null; // 清除事件绑定,切断 DOM 与闭包的关联
      //   btn = null; // 清除 DOM 引用
      // };
    }
    bindEvent();
    
  • 控制闭包创建频率:避免在循环、高频事件(如 resizescroll)中频繁创建闭包,减少内存占用。若需多次使用,可将闭包缓存复用,而非重复创建:

    javascript

    运行

    // 不推荐:循环中频繁创建闭包
    var lis = document.querySelectorAll("li");
    for (var i = 0; i < lis.length; i++) {
      (function(i) {
        lis[i].onclick = function() {
          console.log(i);
        };
      })(i);
    }
    
    // 优化:复用闭包(或用 let 块级作用域替代)
    function createClickHandler(index) {
      // 单个闭包模板,复用逻辑
      return function() {
        console.log(index);
      };
    }
    for (var i = 0; i < lis.length; i++) {
      lis[i].onclick = createClickHandler(i); // 复用闭包函数,减少内存占用
    }
    
  • 使用块级作用域替代部分闭包场景:ES6 引入的 let/const 支持块级作用域,可替代闭包实现 “保存变量瞬时值” 的需求,且无内存泄漏风险。例如解决 li 点击索引问题:

    javascript

    运行

    var lis = document.querySelectorAll("li");
    // let 声明 i,形成块级作用域,每次循环的 i 是独立变量
    for (let i = 0; i < lis.length; i++) {
      lis[i].onclick = function() {
        console.log(i); // 点击对应 li,输出正确索引,无需闭包
      };
    }
    

十二、函数封装经典案例

(一)封装数组反转方法

通过两种思路实现数组反转,兼容任意类型数组元素(数字、字符串、对象等):

方法一:正向遍历 + 反向赋值

javascript

运行

function reverseArray(arr) {
  // 校验参数:确保输入是数组
  if (!Array.isArray(arr)) {
    throw new Error("参数必须是数组");
  }
  var newArr = [];
  // 正向遍历原数组,将元素赋值到新数组的反向索引
  for (var i = 0; i < arr.length; i++) {
    newArr[arr.length - i - 1] = arr[i];
  }
  return newArr;
}
// 测试:反转数字数组
var numArr = reverseArray([1, 4, 7, 9, 12, 16]);
console.log(numArr); // 输出 [16, 12, 9, 7, 4, 1]
// 测试:反转字符串数组
var strArr = reverseArray(["a", "b", "c"]);
console.log(strArr); // 输出 ["c", "b", "a"]
方法二:反向遍历 + 正向赋值

javascript

运行

function reverseArray(arr) {
  if (!Array.isArray(arr)) {
    throw new Error("参数必须是数组");
  }
  var newArr = [];
  // 反向遍历原数组,按顺序添加到新数组
  for (var i = arr.length - 1; i >= 0; i--) {
    newArr[newArr.length] = arr[i]; // newArr.length 动态增长,无需计算索引
  }
  return newArr;
}
var testArr = reverseArray([{ name: "A" }, { name: "B" }]);
console.log(testArr); // 输出 [{ name: "B" }, { name: "A" }](对象数组也可反转)

(二)封装冒泡排序方法

冒泡排序核心逻辑:重复遍历数组,相邻元素两两比较,逆序则交换,逐步将最大元素 “冒泡” 到数组末尾:

javascript

运行

function bubbleSort(arr) {
  if (!Array.isArray(arr)) {
    throw new Error("参数必须是数组");
  }
  // 外层循环:控制排序轮次(n 个元素需 n-1 轮,已排序元素无需再比较)
  for (var i = 0; i < arr.length - 1; i++) {
    // 内层循环:每轮比较次数(每轮减少 i 次,排除已排序的末尾元素)
    for (var j = 0; j < arr.length - i - 1; j++) {
      // 数值比较:若前元素 > 后元素,交换位置(升序排序)
      if (arr[j] > arr[j + 1]) {
        var temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
  return arr; // 返回排序后的数组(原数组也被修改,若需不修改原数组,可先复制数组)
}
// 测试:排序无序数组
var unsortedArr = [40, 8, 15, 18, 12];
var sortedArr = bubbleSort(unsortedArr);
console.log(sortedArr); // 输出 [8, 12, 15, 18, 40]
优化:添加 “已排序” 判断

若数组在某轮遍历中无交换,说明数组已有序,可提前终止循环,减少性能消耗:

javascript

运行

function bubbleSortOptimized(arr) {
  if (!Array.isArray(arr)) {
    throw new Error("参数必须是数组");
  }
  var isSorted = false; // 标记数组是否已有序
  for (var i = 0; i < arr.length - 1 && !isSorted; i++) {
    isSorted = true; // 假设本轮已有序
    for (var j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        var temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
        isSorted = false; // 发生交换,数组未有序
      }
    }
  }
  return arr;
}
// 测试:已部分有序的数组
var almostSortedArr = [2, 3, 1, 4, 5];
console.log(bubbleSortOptimized(almostSortedArr)); // 输出 [1, 2, 3, 4, 5],轮次减少

(三)封装平年 / 闰年判断函数

根据闰年规则(能被 4 整除且不能被 100 整除,或能被 400 整除)实现判断,并扩展 “获取某年 2 月天数” 功能:

1. 判断平年 / 闰年

javascript

运行

function isLeapYear(year) {
  // 校验参数:确保输入是有效年份(整数)
  if (typeof year !== "number" || !Number.isInteger(year) || year < 0) {
    throw new Error("参数必须是正整数年份");
  }
  // 闰年规则:(能被4整除且不能被100整除) 或 能被400整除
  return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
// 测试:典型年份
console.log(isLeapYear(2000)); // 输出 true(能被400整除,闰年)
console.log(isLeapYear(2020)); // 输出 true(能被4整除且不能被100整除,闰年)
console.log(isLeapYear(2023)); // 输出 false(平年)
console.log(isLeapYear(1900)); // 输出 false(能被100整除但不能被400整除,平年)
2. 扩展:获取某年 2 月天数

javascript

运行

function getFebruaryDays(year) {
  // 复用闰年判断函数:闰年 29 天,平年 28 天
  return isLeapYear(year) ? 29 : 28;
}
// 测试
console.log(getFebruaryDays(2020)); // 输出 29(闰年)
console.log(getFebruaryDays(2023)); // 输出 28(平年)

(四)封装数组最大值获取函数

遍历数组,通过 “假设最大值” 逐步比较更新,获取数组中的最大值(兼容纯数字数组):

javascript

运行

function getArrayMax(arr) {
  // 校验参数:确保输入是数组且非空
  if (!Array.isArray(arr) || arr.length === 0) {
    throw new Error("参数必须是非空数组");
  }
  // 校验数组元素:确保所有元素是数字
  for (var i = 0; i < arr.length; i++) {
    if (typeof arr[i] !== "number" || isNaN(arr[i])) {
      throw new Error("数组元素必须是有效数字");
    }
  }
  var max = arr[0]; // 假设第一个元素是最大值
  // 遍历数组,更新最大值
  for (var i = 1; i < arr.length; i++) {
    if (arr[i] > max) {
      max = arr[i];
    }
  }
  return max;
}
// 测试
var numArr = [1, 34, 96, 23, 78];
console.log(getArrayMax(numArr)); // 输出 96
var negativeArr = [-5, -2, -8, 0];
console.log(getArrayMax(negativeArr)); // 输出 0(支持负数数组)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐