深入理解JavaScript函数参数与闭包机制

作者:半夏

时间:2026-10-31

浏览:77

本文深入解析JavaScript函数的核心机制,涵盖参数定义、省略规则、默认值设置及传值传址区别。重点讲解arguments对象的特性与转换方法,并详细阐述闭包的定义、作用域链及内存保留特性。通过IIFE解决循环中闭包引用的经典案例,帮助开发者掌握函数进阶用法。

文章目录

JavaScript函数参数与闭包概述

近期在钻研JavaScript函数机制,作为语言中的一等公民,函数是掌握JS的关键。本文梳理学习心得,旨在深化对函数的理解,并为读者提供清晰的学习路径,避免常见误区。内容虽多,但均为对函数特性的系统总结。

1.函数参数

1.1:参数是什么

1.2:参数的省略

1.3:参数默认值

1.4:参数传递方式

1.5:同名参数

1.6:arguments对象

2.闭包

2.1:闭包定义

2.2:立即调用的函数表达式(IIFE, Immediately invoked function expression)

函数参数详解

1.函数参数

1.1:参数是什么

定义函数时,常需传入额外数据以获取不同结果,这些外部输入数据即称为参数。


 function keith(a){
 return a+a;
 }
 console.log(keith(3)); //6

上述代码中,keith函数接收参数a,并返回a与a相加的结果。

1.2:参数的省略

函数参数并非强制要求,JavaScript规范允许调用时省略实际传递的参数。


 function keith(a, b, c) {
 return a;
 }
 console.log(keith(1, 2, 3)); //1
 console.log(keith(1)); //1
 console.log(keith()); // 'undefined'

上述代码中,keith函数定义了三个参数,但调用时无论传入多少参数,JS均不会报错。被省略的参数默认值为undefined。熟悉函数定义与作用域的读者知道,函数的length属性返回形式参数的个数。需注意,length属性与实际传入参数个数无关,仅反映定义时的参数数量。

(实际参数:调用时传递的参数。 形式参数:定义时传递的参数。)

然而,无法直接省略靠前的参数而保留靠后的参数。若必须省略靠前参数,需显式传入undefined。


 function keith(a, b) {
 return a;
 }
 console.log(keith(, 1)); //SyntaxError: expected expression, got ','
 console.log(keith(undefined, 2)); //'undefined'

上述代码中,若省略第一个参数,浏览器将报错。若显式传递undefined,则不会报错。

1.3:默认值

JavaScript中函数参数默认值为undefined。但在某些场景下设置不同默认值更为实用。通用策略是在函数体内检查参数是否为undefined,若是则赋予默认值,否则使用传入的实际参数。


 function keith(a, b) {
 (typeof b !== 'undefined') ? b = b: b = 1;
 return a * b;
 }
 console.log(keith(15)); //15
 console.log(keith(15, 2)) //30

上述代码中进行了判断。当调用时未传入b参数,则默认值为1。

自ECMAScript 6起,引入了默认参数(default parameters)语法。使用此语法后,函数体内的显式检查便不再必要。


 function keith(a, b = 1) {
 return a * b;
 }
 console.log(keith(15)); //15
 console.log(keith(15, 2)) //30

1.4:参数传递方式

函数参数传递有两种方式:传值传递和传址传递。

当参数为原始数据类型(字符串、数值、布尔值)时,采用传值传递。即在函数体内修改参数值,不会影响函数外部的原始值。


 var a = 1;
 function keith(num) {
 num = 5;
 }
 keith(a);
 console.log(a); //1

上述代码中,全局变量a为原始类型值,以传值方式传入keith函数。因此,函数内部a的值是原始值的拷贝,无论内部如何修改,均不影响原始值。

然而,若参数为复合类型值(数组、对象、其他函数),则采用传址传递(pass by reference)。即传入的是原始值的地址,因此在函数内部修改参数,将影响原始值。


 var arr = [2, 5];
 function keith(Arr) {
 Arr[0] = 3;
 }
 keith(arr);
 console.log(arr[0]); //3

上述代码中,传入keith函数的是参数对象arr的地址。因此,在函数内部修改arr的第一个值,会影响原始值。

注意,若函数内部修改的不是参数对象的某个属性,而是替换整个参数,则不会影响原始值。


 var arr = [2, 3, 5];
 function keith(Arr) {
 Arr = [1, 2, 3];
 }
 keith(arr);
 console.log(arr); // [2,3,5]

上述代码中,在keith函数内部,参数对象arr被整体替换为另一个值。此时不影响原始值。这是因为形式参数(Arr)与实际参数arr之间存在赋值关系。

1.5:同名参数

若存在同名参数,则取最后出现的那个值;若未提供最后一个参数的值,则取值变为undefined。


 function keith(a, a) {
 return a;
 }
 console.log(keith(1, 3)); //3
 console.log(keith(1)); //undefined

若想访问同名参数中的第一个参数,可使用arguments对象。


 function keith(a, a) {
 return arguments[0];
 }
 console.log(keith(2));  //2

1.6 arguments对象

JavaScript中每个函数内部均可访问一个特殊变量arguments,该变量维护着传递给此函数的所有参数列表。

arguments对象包含函数运行时的所有参数,arguments[0]即第一个参数,arguments[1]即第二个参数,依此类推。该对象仅在函数体内部可用。

可访问arguments对象的length属性,以判断函数调用时携带的参数个数。


 function keith(a, b, c) {
 console.log(arguments[0]); //1
 console.log(arguments[2]); //3
 console.log(arguments.length); //4
 }
 keith(1, 2, 3, 4);

arguments对象与数组的关系

arguments对象并非数组(Array)。尽管语法上它具有数组相关的length属性,但它不继承自Array.prototype,实际上是一个类数组对象。因此,无法对arguments变量使用标准数组方法,如push、pop或slice。但可使用数组中的length属性。

通常使用以下方法将arguments对象转换为数组。

var arr = Array.prototype.slice.call(arguments);

闭包机制与应用

2.闭包

2.1:闭包定义

理解闭包需先明确全局作用域与局部作用域的区别。函数内部可访问全局作用域下定义的全局变量,而函数外部无法访问函数内部定义的(局部作用域)局部变量。


var a = 1;
function keith() {
 return a;
 var b = 2;
 }
 console.log(keith()); //1
 console.log(b); //ReferenceError: b is not defined

上述代码中,全局变量a可在keith函数内部访问。但局部变量b却无法在函数外部访问。

若需获取函数内部的局部变量,只能在函数内部再定义一个函数。


 function keith(){
 var a=1;
 function rascal(){
 return a;
 }
 return rascal;
 }
 var result=keith();
 console.log(result()); //1
 function keith(){
 var a=1;
 return function(){
 return a;
 };
 }
 var result=keith();
 console.log(result()) //1

上述代码中,两种写法等效,区别仅在于内部函数是否为匿名函数。函数rascal位于keith内部,此时keith内部所有局部变量对rascal均可见。反之则不成立,rascal内部的局部变量对keih不可见。这就是JavaScript特有的“链式作用域”结构(chain scope),子对象会一级一级向上寻找父对象的变量。因此,父对象的所有变量对子对象可见,反之不成立。keith的返回值即函数rascal,由于rascal可读取keith的内部变量,故可在外部获得keith的内部变量。

闭包即函数rascal,即能够读取其他函数内部变量的函数。因JavaScript中仅函数内部的子函数能读取内部变量,故可将闭包简单理解为“定义在一个函数内部的函数”。闭包最大特点在于其能“记住”诞生环境,如rascal记住其诞生环境keith,故从rascal可获取keith的内部变量。

闭包可使诞生环境持续存在。见下方示例,闭包使内部变量记住上一次调用时的运算结果。


 function keith(num) {
 return function() {
 return num++;
 };
 }
 var result = keith(2);
 console.log(result()) //2
 console.log(result()) //3
 console.log(result()) //4

上述代码中,参数num相当于keith函数内部定义的局部变量。通过闭包,num的状态得以保留,每次调用均基于上一次调用结果进行计算。由此可见,闭包result使keith的内部环境持续存在。

通过上述示例,总结闭包特点如下:

1:在一个函数内部定义另一个函数,并返回内部函数或立即执行内部函数。

2:内部函数可读取外部函数定义的局部变量

3:使局部变量始终保存在内存中。即闭包可使诞生环境持续存在。

闭包的另一用途是封装对象的私有属性和私有方法。


 function Keith(name) {
 var age;
 function setAge(n) {
 age = n;
 }
 function getAge() {
 return age;
 }
 return {
 name: name,
 setAge: setAge,
 getAge: getAge
 };
 }
 var person = Keith('keith');
 person.setAge(21);
 console.log(person.name); // 'keith'
 console.log(person.getAge()); //21

2.2:立即调用的函数表达式(IIFE)

通常仅对匿名函数使用“立即执行的函数表达式”。其目的有二:一是不必为函数命名,避免污染全局变量;二是IIFE内部形成独立作用域,可封装外部无法读取的私有变量。

循环中的闭包

一个常见错误出现在循环中使用闭包,假设需在每次循环中调用循环序号


 for(var i=0;i<10;i++){
 setTimeout(function(){
 console.log(i); //10
 }, 1000)
 }

上述代码不符合预期,不会输出0-9,而是输出10次数字10。

当匿名函数被调用时,它保持对全局变量i的引用,即记住i循环执行的结果。此时for循环结束,i的值被修改为10。

为获得预期效果,避免引用错误,应使用IIFE在每次循环中创建全局变量i的拷贝。


for(var i = 0; i < 10; i++) {
 (function(e) {
 setTimeout(function() {
 console.log(e); //1,2,3,....,10
 }, 1000);
 })(i);
 }

外部匿名函数立即执行,并将i作为参数传入,此时函数内e变量拥有i的一个拷贝。当传递给setTimeout的匿名函数执行时,它拥有对e的引用,且该值不会随循环改变。

以上为本文全部内容,希望内容对大家的学习或工作有所帮助,同时也感谢支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章