MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript函数实参与形参的并发传递

2022-11-123.7k 阅读

JavaScript函数参数基础回顾

在深入探讨JavaScript函数实参与形参的并发传递之前,我们先来回顾一下函数参数的基础知识。

在JavaScript中,函数是一等公民,它可以接受零个或多个参数。函数定义时声明的参数被称为形参(Formal Parameters),而在调用函数时传入的值则被称为实参(Actual Parameters)。

例如:

function addNumbers(a, b) {
    return a + b;
}

let result = addNumbers(3, 5);
console.log(result); 

在上述代码中,addNumbers函数定义了两个形参ab。当调用addNumbers(3, 5)时,35就是实参,它们分别被传递给形参ab

按值传递与按引用传递

JavaScript中参数传递的方式有两种:按值传递(Pass - by - Value)和按引用传递(Pass - by - Reference),但JavaScript实际上只有按值传递这一种机制,对于对象和数组等复杂数据类型,看似是按引用传递,实则有微妙的区别。

基本数据类型的按值传递

对于基本数据类型(如numberstringbooleannullundefined),参数传递是按值传递。这意味着函数接收到的是实参值的一个副本。

function increment(num) {
    num++;
    return num;
}

let original = 5;
let newNum = increment(original);
console.log(original); 
console.log(newNum); 

在这个例子中,original的值5被复制给了形参num。在函数内部对num的修改并不会影响到original

复杂数据类型的“按引用传递”表象

对于复杂数据类型(如objectarray),情况看起来有所不同。

function addElement(arr) {
    arr.push(4);
    return arr;
}

let myArray = [1, 2, 3];
let newArray = addElement(myArray);
console.log(myArray); 
console.log(newArray); 

在这个例子中,myArray作为实参传递给addElement函数。函数内部对arr(形参)的修改似乎影响到了myArray。这是因为在JavaScript中,当传递对象或数组时,实际上传递的是对象或数组在内存中的引用地址的副本。所以,函数内部通过这个引用地址副本操作的仍然是同一个对象或数组。但本质上还是按值传递,只不过传递的值是引用地址。

并发传递的概念

并发传递在JavaScript函数参数传递的语境下,并不是传统意义上多线程环境中的并发执行传递。在JavaScript单线程的执行模型下,并发传递更多是指在函数调用时,实参以一种相对独立的方式被计算和传递给形参。

JavaScript引擎在处理函数调用时,会先计算实参表达式的值,然后将这些值传递给函数的形参。这个过程中,实参的计算和传递可以看作是一种“并发”的行为,尽管它们是在单线程的不同阶段完成的。

例如:

function multiply(a, b) {
    return a * b;
}

function getValue() {
    return 5;
}

let result = multiply(getValue(), 3);

在这个例子中,在调用multiply函数时,JavaScript引擎首先会计算getValue()表达式的值,得到5,然后将这个值和3一起作为实参传递给multiply函数的形参ab。这里getValue()的计算和3这个值的准备,可以看作是一种类似并发的操作(虽然在单线程中依次执行)。

实参与形参并发传递中的作用域

在JavaScript中,作用域在实参与形参并发传递过程中起着关键作用。

全局作用域与局部作用域

当函数被调用时,实参的计算和形参的接收都在特定的作用域内进行。

let globalVar = 10;

function useGlobal() {
    return globalVar;
}

function passValue() {
    let localVar = 20;
    return useGlobal();
}

let result = passValue();
console.log(result); 

在这个例子中,useGlobal函数在全局作用域中查找globalVar。而passValue函数有自己的局部作用域,在这个局部作用域内定义了localVar。当passValue函数调用useGlobal函数时,useGlobal函数不受passValue函数局部作用域的影响,仍然在全局作用域中获取globalVar的值。

块级作用域与函数参数传递

ES6引入了块级作用域(通过letconst关键字),这也会影响函数参数传递过程中的作用域链。

function blockScopeExample() {
    let localVar = 10;
    if (true) {
        let blockVar = 20;
        function innerFunction() {
            return localVar + blockVar;
        }
        return innerFunction();
    }
    // blockVar在此处不可用
    return localVar;
}

let result = blockScopeExample();
console.log(result); 

在这个例子中,innerFunction函数可以访问blockVar,因为它在包含blockVar定义的块级作用域内。函数参数传递过程同样遵循这种作用域规则,实参和形参在各自的作用域内解析和使用变量。

并发传递中的隐式类型转换

在JavaScript函数实参与形参的并发传递过程中,隐式类型转换是一个常见的现象。

基本类型之间的隐式转换

当实参的类型与形参预期的类型不一致时,JavaScript会进行隐式类型转换。

function add(a, b) {
    return a + b;
}

let result1 = add(2, '3'); 
console.log(result1); 

let result2 = add('2', 3); 
console.log(result2); 

在这两个例子中,add函数期望两个数字类型的参数,但分别传入了一个数字和一个字符串。JavaScript会将数字转换为字符串,然后进行字符串拼接,结果都是'23'

复杂类型与基本类型的隐式转换

当传递复杂类型(如对象)作为实参,而形参预期是基本类型时,也会发生隐式类型转换。

function printLength(obj) {
    return obj.length;
}

let myObj = { length: 5 };
let result = printLength(myObj);
console.log(result); 

这里myObj是一个对象,当传递给printLength函数时,JavaScript会尝试将其转换为适合获取length属性的类型(在这个例子中,对象已经有length属性,所以转换顺利进行)。

函数重载与并发传递

在JavaScript中,并没有传统意义上基于参数列表不同的函数重载机制,但可以通过一些技巧模拟类似的行为,这与实参与形参的并发传递也有关系。

模拟函数重载

function add() {
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}

let result1 = add(1, 2); 
let result2 = add(1, 2, 3); 
console.log(result1); 
console.log(result2); 

在这个例子中,add函数可以接受任意数量的参数。通过arguments对象,我们可以在函数内部处理不同数量的实参,从而模拟函数重载的效果。在函数调用时,实参的并发传递使得arguments对象能够收集所有传递进来的值。

基于类型的模拟重载

function handleValue(value) {
    if (typeof value === 'number') {
        return value * 2;
    } else if (typeof value ==='string') {
        return value.toUpperCase();
    }
}

let numResult = handleValue(5); 
let strResult = handleValue('hello'); 
console.log(numResult); 
console.log(strResult); 

在这个例子中,handleValue函数根据实参的类型进行不同的操作,通过实参的并发传递和类型判断,模拟了基于类型的函数重载。

箭头函数中的并发传递

箭头函数在JavaScript中提供了一种简洁的函数定义方式,它在实参与形参的并发传递方面与传统函数有一些异同。

箭头函数的参数语法

let add = (a, b) => a + b;
let result = add(3, 5);
console.log(result); 

箭头函数的参数定义与传统函数类似,这里(a, b)是形参,35是实参,实参以同样的并发方式传递给形参。

箭头函数与词法作用域

箭头函数没有自己的this值,它的this值继承自外层作用域。这在实参与形参并发传递过程中,会影响到函数内部对this的使用。

let obj = {
    value: 10,
    getValue: function() {
        return () => this.value;
    }
};

let innerFunc = obj.getValue();
let result = innerFunc();
console.log(result); 

在这个例子中,箭头函数() => this.value继承了外层函数getValuethis值,也就是obj。当innerFunc被调用时,它能够正确获取到obj.value的值。

并发传递中的性能考量

在JavaScript函数实参与形参的并发传递过程中,性能是一个需要考虑的因素。

函数调用开销

每次函数调用都有一定的开销,包括创建函数执行上下文、传递实参等操作。

function simpleFunction() {
    return true;
}

for (let i = 0; i < 1000000; i++) {
    simpleFunction();
}

在这个简单的例子中,频繁调用simpleFunction函数会产生一定的性能开销,虽然这个函数本身不做复杂操作,但函数调用的开销仍然存在,这其中实参与形参的并发传递也是开销的一部分。

复杂实参计算

如果实参需要进行复杂的计算,这也会影响性能。

function complexCalculation() {
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
        sum += i;
    }
    return sum;
}

function useComplexResult(result) {
    return result * 2;
}

let finalResult = useComplexResult(complexCalculation());

在这个例子中,complexCalculation函数进行了大量的计算,作为实参传递给useComplexResult函数。这种复杂实参的计算会增加整个函数调用过程的时间开销,在性能敏感的场景下需要优化。

异常处理与并发传递

在JavaScript函数实参与形参的并发传递过程中,异常处理是确保程序健壮性的重要环节。

实参计算异常

如果实参的计算过程中抛出异常,函数调用将无法正常进行。

function divide(a, b) {
    return a / b;
}

function getDenominator() {
    throw new Error('Denominator cannot be zero');
    return 0;
}

try {
    let result = divide(10, getDenominator());
} catch (error) {
    console.error(error.message); 
}

在这个例子中,getDenominator函数抛出了一个异常,导致divide函数无法正常接收实参并执行,通过try - catch块捕获并处理了这个异常。

形参处理异常

函数内部对形参的处理也可能抛出异常。

function squareRoot(num) {
    if (num < 0) {
        throw new Error('Cannot calculate square root of a negative number');
    }
    return Math.sqrt(num);
}

try {
    let result = squareRoot(-5);
} catch (error) {
    console.error(error.message); 
}

在这个例子中,squareRoot函数在处理形参num时,如果num为负数则抛出异常,同样通过try - catch块进行处理。

函数柯里化与并发传递

函数柯里化是JavaScript中一个重要的概念,它与实参与形参的并发传递有着紧密的联系。

柯里化的概念

函数柯里化是指将一个多参数函数转换为一系列单参数函数的技术。

function addCurried(a) {
    return function(b) {
        return a + b;
    };
}

let add5 = addCurried(5);
let result = add5(3);
console.log(result); 

在这个例子中,addCurried函数接受一个参数a,并返回一个新的函数,这个新函数接受参数b。这里实参的传递以一种分步并发的方式进行,先传递a,然后在后续调用返回的函数时传递b

柯里化与性能

柯里化在某些场景下可以提高性能,特别是在需要重复使用部分参数的情况下。

function multiplyCurried(a) {
    return function(b) {
        return a * b;
    };
}

let multiplyBy2 = multiplyCurried(2);
for (let i = 0; i < 1000; i++) {
    multiplyBy2(i);
}

在这个例子中,multiplyBy2函数已经固定了一个参数2,后续调用时只需要传递另一个参数,避免了每次都传递两个参数的开销,在一定程度上提高了性能。

模块化与并发传递

在JavaScript的模块化编程中,函数实参与形参的并发传递也会受到模块作用域和导出导入机制的影响。

模块内的函数参数传递

在一个模块内部定义的函数,其实参与形参的并发传递遵循模块的局部作用域规则。

// module.js
let moduleVar = 10;

function moduleFunction(a) {
    return a + moduleVar;
}

export { moduleFunction };

在这个模块中,moduleFunction函数使用了模块内定义的moduleVar变量,实参a的并发传递在模块的局部作用域内进行。

模块间的函数参数传递

当从一个模块导入函数并调用时,实参与形参的并发传递同样受到影响。

// main.js
import { moduleFunction } from './module.js';

let result = moduleFunction(5);
console.log(result); 

在这个例子中,从module.js导入的moduleFunction函数在main.js中被调用,实参5以并发方式传递给形参a,并且函数内部对moduleVar的引用仍然在module.js模块的作用域内。

总结

JavaScript函数实参与形参的并发传递涉及到作用域、类型转换、性能、异常处理、函数柯里化以及模块化等多个方面的知识。理解这些概念对于编写高效、健壮的JavaScript代码至关重要。在实际开发中,我们需要根据具体的需求和场景,合理地设计函数参数,处理实参与形参的传递过程,以确保程序的正确性和性能。通过深入掌握这些细节,开发者能够更好地利用JavaScript函数这一强大的工具,构建出更加复杂和功能丰富的应用程序。无论是简单的函数调用还是复杂的模块化编程,对实参与形参并发传递的深刻理解都将成为提升代码质量和开发效率的关键因素。

希望通过本文的详细介绍,读者能够对JavaScript函数实参与形参的并发传递有更深入的认识和掌握,在实际编程中能够更加游刃有余地处理函数参数相关的问题。