Home >Web Front-end >JS Tutorial >An article explaining the parameters in JavaScript functions in detail

An article explaining the parameters in JavaScript functions in detail

青灯夜游
青灯夜游forward
2022-08-03 19:49:523771browse

Function parameters are the bridge between the inside of the function and the outside of the function. The following article will take you through the parameters in JavaScript functions. I hope it will be helpful to you!

An article explaining the parameters in JavaScript functions in detail

1. Function parameters and actual parameters

The parameters of a function will appear in two places, namely the function definition There is a difference between the parameters in these two places and the function call place.

  • Formal parameters (formal parameters)

    The parameters that appear in the function definition can be regarded as a placeholder, which has no data. You can only wait until the function is called to receive the data passed in, so it is called a formal parameter, or formal parameter for short.

  • Actual parameters (actual parameters)

    The parameters given when the function is called contain real data and will be used internally by the function. The code is used, so it is called actual parameters, or actual parameters for short.

The difference and connection between formal parameters and actual parameters

  • 1) Formal parameter variables are only used when the function is called Memory will be allocated only after the call is completed, and the memory will be released immediately. Therefore, the formal parameter variables are only valid inside the function and cannot be used outside the function.

  • 2) Actual parameters can be constants, variables, expressions, functions, etc. No matter what type of data the actual parameters are, they must have certain values ​​when making function calls. values ​​in order to transfer these values ​​​​to the formal parameters, so assignment, input, etc. should be used in advance to obtain a certain value for the actual parameters.

  • 3) The actual parameters and formal parameters must be strictly consistent in number, type, and order, otherwise a "type mismatch" error will occur. Of course, if automatic type conversion is possible or forced type conversion is performed, the actual parameter type can also be different from the formal parameter type.

  • 4) The data transfer that occurs in a function call is one-way. Only the value of the actual parameter can be transferred to the formal parameter, but the value of the formal parameter cannot be transferred in the reverse direction. Actual parameters; in other words, once the data transfer is completed, the actual parameters and the formal parameters have nothing to do with each other. Therefore, during the function call, changes in the value of the formal parameters will not affect the actual parameters.

  • 5) Although the formal parameters and actual parameters can have the same name, they are independent of each other and do not affect each other, because the actual parameters are valid outside the function, while the formal parameters are inside the function efficient.

The function of formal parameters and actual parameters is to pass data. When a function call occurs, the value of the actual parameter will be passed to the formal parameter.

2. Parameter passing

The function allows us to pass data in, and the passed data affects the function execution result, making the function more flexible and reusable. Stronger sex.

function foo(a, b) {
    console.log([a, b]);
}

foo(1, 2); // 输出 [1, 2]

In this example, a and b are local variables in the function and can only be accessed within the function. When calling the function, the passed data will be matched according to the position and assigned to a and b respectively.

When creating a function, the parameters set in brackets after the function function name are called formal parameters; when calling a function, the parameters passed in brackets after the function name Parameters are called actual parameters. In the above example, a and b are formal parameters, and the passed in 1 and 2 are actual parameters.

Because the formal parameter is a declared variable, it cannot be declared repeatedly with let and const.

function foo(a, b) {
    let a = 1; // 报错,a 已声明
    const b = 1; // 报错,b 已声明
}

All function passes in JavaScript are passed by value, not by reference. The so-called value refers to the value stored directly on the variable. If the object is passed as a parameter, then the value is a reference to the object, not the object itself. This is actually an implicit assignment process, so when passing parameters to a function, it is equivalent to assigning a value from one variable to another variable.

Original value:

function add(num) {
    return num + 1;
}

let count = 5;
let result = add(count); // 此处参数传递的过程可以看作是 num = count

console.log(count); // 5
console.log(result); // 6

Reference value:

function setName(obj) {
    obj.name = "小明";
}

let person = {};

setName(person); // 此处参数传递的过程可以看作是 obj = person;
console.log(person); // {name: "小明"}

3. Understanding parameters

Functions in JavaScript will neither detect The type of parameters will not be detected, nor will the number of parameters passed in be detected. Setting two formal parameters when defining a function does not mean that two parameters must be passed in when calling. When actually calling, no matter whether one or three parameters are passed, no error will be reported even if no parameters are passed.

There is a special array-like object (not an instance of Array) named arguments in all functions (non-arrows), It saves a copy of all actual parameters. We can use it to obtain the values ​​​​of all actual parameters according to the index access method of the array, or we can access its arguments.length property to determine the actual parameters passed in when the function is called. Number of parameters.

For example:

function foo(a, b) {
	console.log(arguments[0]);
    console.log(arguments[1]);
    console.log(arguments.length);
}

foo(10, 20); // 依次输出 10、20、2

In the above example, the first parameter of the foo() function is a, and the second parameter is b. The same parameters can be obtained separately through arguments[x]. value. Therefore, you can even declare a function without setting formal parameters.

function foo() {
	console.log(arguments[0]);
    console.log(arguments[1]);
}

foo(10, 20); // 依次输出 10、20

It can be seen that the formal parameters of JavaScript functions are only written for convenience. Passing as many parameters as you want will not cause an error.

还有一个要注意的是,arguments 可以跟形参一起使用,并且 arguments 对象中的值会和对应的形参保持同步。例如:

function foo(a) {
	arguments[0] ++;
    console.log(a);
}

foo(10); // 输出 11
//------------------------------------
function foo2(a) {
	a++;
    console.log(arguments[0]);
}

foo2(10); // 输出 11

当修改 arguments[0] 或 a 的值时,另一个也被改变了。这并不意味着它们访问同一个内存地址,毕竟我们传入的是一个原始值。它们在内存中还是分开的,只是由于内部的机制使它们的值保持了同步。

另外,如果缺少传参,那这个形参的值就不会和 arguments 对象中的对应值进行同步。例如下面这个例子,只传了一个参数,那么arguments 中只有一个实参值,这时候在函数中把 arguments[1] 设置为某个值,这个值并不会同步给第二个形参,例如:

function foo(a,b) {
    arguments[1] = 2;
    console.log(b);
}

foo(1); // 输出 undefined

这个例子中,形参 b 没有传入实参,它的值会默认为 undefined。但如果:

foo(1, undefined); // 输出 2

手动传入 undefined 时, arguments 数组中会出现一个值为 undefined 的元素,依然能和 b 的值进行同步。

严格模式下,arguments 对象中的值和形参不会再同步,当然,如果传入的是引用值,它们依然会互相影响,但这只是引用值的特性而已。因此,在开发中最好不要依赖这种同步机制,也就是说不要同时使用形参和它在arguments 对象中的对应值。

箭头函数中没有 arguments

如果函数是使用箭头语法定义的,那么函数中是没有 arguments 对象的,只能通过定义的形参来访问。

let foo = () => {
    console.log(arguments[0]);
}foo(); // 报错,arguments 未定义

在某些情况可能会访问到 arguments

function fn1(){
    let fn2 = () => {
    	console.log(arguments[0]);
    }
    
    fn2();
}fn1(5);

但这个 arguments,并不是箭头函数的,而是属于外部普通函数的,当箭头函数中访问 arguments 时,顺着作用域链找到了外部函数的arguments

四、将对象属性用作实参

当一个函数包含的形参有多个时,调用函数就成了一种麻烦,因为你总是要保证传入的参数放在正确的位置上,有没有办法解决传参顺序的限制呢?

由于对象属性是无序的,通过属性名来确定对应的值。因此可以通过传入对象的方式,以对象中的属性作为真正的实参,这样参数的顺序就无关紧要了。

function foo(obj) {
    console.log(obj.name, obj.sex, obj.age);
}

foo({ sex: '男', age: 18, name: '小明' }); // 小明 男 18

五、参数默认值

如果调用函数时缺少提供实参,那么形参默认值为 undefined

有时候我们想要设置特定的默认值,在 ES6 之前还不支持显式地设置默认值的时候,只能采用变通的方式:

function sayHi(name) {
    name = name || 'everyone';
    
	console.log( 'Hello ' + name + '!');
}

sayHi(); // 输出 'Hello everyone!'

通过检查参数值的方式判断有没有赋值,上面的做法虽然简便,但缺点在于如果传入的实参对应布尔值为 false ,实参就不起作用了。需要更精确的话可以用 if 语句或者三元表达式,判断参数是否等于 undefined,如果是则说明这个参数缺失 :

// if 语句判断
function sayHi(name) {
	if (name === undefined) {
		name = 'everyone';
	}
    
	console.log( 'Hello ' + name + '!');
}

// 三元表达式判断
function sayHi(name) {
	name =  (name !== undefined) ? name : 'everyone';
	
    console.log( 'Hello ' + name + '!');
}

ES6 就方便了许多,因为它支持了显式的设置默认值的方式,就像这样:

function sayHi(name = 'everyone') { // 定义函数时,直接给形参赋值
	console.log( 'Hello ' + name + '!');
}

sayHi(); // 输出 'Hello everyone!' 
sayHi('Tony'); // 输出 'Hello Tony!' 
sayHi(undefined); // 输出 'Hello everyone!'

这些结果表明了,它也是通过参数是否等于 undefined 来判定参数是否缺失的。

默认值不但可以是一个值,它还可以是任意合法的表达式,甚至是函数调用:

function sayHi(name = 'every'+'one') {
	console.log( 'Hello ' + name + '!');
}

sayHi(); // 输出 'Hello everyone!' 
//--------------------------------------
function foo() {
    console.log('调用foo');
    return 'Tony';
}

function sayHi(name = foo()) {
	console.log( 'Hello ' + name + '!');
}
		  
sayHi(); // 输出 '调用foo'
         // 输出 'Hello Tony!' 

sayHi(undefined); // 输出 '调用foo'
                  // 输出 'Hello Tony!' 

sayHi('John'); // 输出 'Hello John!'

可以看到,函数参数的默认值只有在函数调用时,参数的值缺失或者是 undefined 才会求值,不会在函数定义时求值。

参数默认值的位置

通常我们给参数设置默认值,是为了调用函数时可以适当省略参数的传入,这里要注意的是,有多个参数时,设置了默认值的参数如果不是放在尾部,实际上它是无法省略的。

function fn(x = 1, y) {
	console.log([x, y]);
}

fn(); // 输出 [1, undefined]
fn(2); // 输出 [2, undefined]
fn(, 2); // 报错,语法错误(这里不支持像数组那样的空槽)
fn(undefined, 2); // 输出 [1, 2] (那还不如传个 1 方便呢!)

上面例子中,给形参 x 设置的默认值就显得没有任何意义了。因此,设置默认值的参数放在尾部是最好的做法:

function fn(x, y = 2) {
	console.log([x, y]);
}

fn(); // 输出 [undefined, 2]
fn(1); // 输出 [1, 2]
fn(1, 1) // 输出 [1, 1]

参数的省略问题

在多个参数设置了默认值的情况下,那么问题又来了,你并不能省略比较靠前的参数,而只给最后的一个参数传入实参。

function fn(x, y = 2, z = 3) {
	console.log([x, y, z]);
}

fn(1, , 10) // 报错

前面我们知道,可以通过传入对象的这种方式去避免参数顺序的限制。那参数默认值如何实现呢?用 ||if 语句或者三元表达式去判断也是解决办法,但这样就显得有些落后了。接下来要讨论的是另外两种 ES6 中的全新方式。

参数默认值和 Object.assign() 结合使用

function fn(obj = {}) {
    let defaultObj = {
        x: undefined,
        y: 2,
        z: 3
    }
    
    let result = Object.assign(defaultObj, obj);
    
	console.log([result.x, result.y, result.z]);
}

fn(); // 输出 [undefined, 2, 3]
fn({ x: 1, z: 10 }); // 输出 [1, 2, 10]

上面的例子中,在函数中定义了一个对象 defaultObj ,变通地利用其中的属性作为参数的默认值,然后利用 Object.assagin() 把传入的对象和默认对象进行合并,defaultObj 中的属性会被 obj 的相同属性覆盖,obj 中如果有其他属性会分配给 defaultObj 。这里用一个变量接收返回的合并对象。

同时形参 obj 也设置了默认值为一个空对象,防止函数调用时不传任何参数,因为这会导致 Object.assign() 接收的第二个参数是 undefined ,从而产生报错。

参数默认值和解构赋值结合使用

函数调用时,实参和形参的匹配实际上是一个隐式的赋值过程,所以,参数传递也可以进行解构赋值:

function fn({ x, y = 2, z = 3 }) {
    console.log([x, y, z]);
}

fn({}); // 输出 [undefined, 2, 3]
fn({ x: 1, z: 10 }); // 输出 [1, 2, 10]

在这个例子中,使用的只是对象的解构赋值默认值,还没有使用函数参数的默认值。如果函数调用时不传任何参数,也会产生报错,因为这导致了参数初始化时解构赋值失败,相当于执行了 {x, y = 2, z = 3} = undefined 这样的代码。

同样的,你可以利用参数默认值的语法,给 {x, y = 2, z = 3} 设置一个默认的解构对象,使得不传参函数也能够顺利执行:

function fn({ x, y = 2, z = 3 } = {}) {
    console.log([x, y, z]);
}

fn(); // 输出 [undefined, 2, 3]

这里出现了双重的默认值,可能有些绕,那么用一段伪代码来解释以上的参数初始化过程就是:

if( 实参 === {...} ) { // 当 fn({...});     
    { x, y = 2, z = 3 } = {...};
                        
} else if ( 实参 === undefined ){ // 当 fn();
    { x, y = 2, z = 3 } = {};

}

双重默认值有一点细节需要特别注意,就是解构赋值默认值和函数参数默认值的差别,看下面例子:

function fn ({ x = 1 } = {}, { y } = { y: 2 }){
    console.log(x, y);
}

fn(); // 输出 1 2
fn({ x: 10 }, { y: 20 }); // 输出 10 20
fn({},{}); // 1 undefined

这个函数中,有两组参数采用了解构赋值的方式,看似 x 和 y 都设置了默认值,虽然是不同的两种形式,但显然不是任何情况下结果都相同的。当传入的参数是{}时,y 并没有获取到默认值 2 ,为什么会这样呢?结合前面的伪代码例子来看:

fn({ x: 10 }, { y: 20 }); // 初始化时: { x = 1 } = { x: 10 }, { y } = { y: 20 }

fn({},{}); // 初始化时: { x = 1 } = {}, { y } = {}

当传入的参数是{}时,函数参数没有缺失也不是 undefined ,所以函数参数默认值是不起作用的。同时 {} 里面也没有 x 和 y 的对应值,x 得到的 1 是解构赋值默认值,而 y 由于没有设置解构赋值默认值,所以它默认是 undefined

参数默认值的作用域与暂时性死区

还有一个小细节,一旦有参数设置了默认值,那么它们会形成自己的作用域(包裹在(...)中),因此不能引用函数体中的变量:

function foo(a = b) {
    let b = 1;
}

foo(); // 报错,b 未定义

但这个作用域只是临时的,参数初始化完毕后,这个作用域就不存在了。

它也符合普通作用域的规则:

let b = 2;

function foo(a = b) {
    let b = 1;
    return a;
}

foo(); // 2

上面例子中,存在一个全局变量 b,那么形参 a 会获取到全局变量 b 的值。

当然,如果形参作用域中存在一个形参 b 的话,它优先获取到的是当前作用域的:

let b = 2;

function foo(b = 3 ,a = b) {
    return a;
}

foo(); // 3

给多个参数设置默认值,它们会按顺序初始化的,遵循“暂时性死区”的规则,即前面的参数不能引用后面的参数:

function foo(a = b, b = 2) {
    return a + b;
}

foo(); // 报错,b 在初始化之前不能访问

六、参数的收集与展开

剩余参数

ES6 提供了**剩余参数(rest)**的语法(...变量名),它可以收集函数多余的实参(即没有对应形参的实参),这样就不再需要使用 arguments 对象来获取了。形参使用了 ... 操作符会变成一个数组,多余的实参都会被放进这个数组中。

剩余参数基本用法:

function sum(a, ...values) {
 
    for (let val of values) {
        a += val;
    }
    
    return a;
}

sum(0, 1, 2, 3); // 6

上面例子中,在参数初始化时,首先根据参数位置进行匹配,把 0 赋值给 a ,然后剩余的参数 1、2、3 都会被放进数组 values 中。

下面是分别用 arguments 对象和剩余参数来获取参数的对比例子:

// arguments 的写法
function sortNumbers() {
	return Array.prototype.slice.call(arguments).sort();
}

// 剩余参数的写法
const sortNumbers = (...numbers) => {
    return numbers.sort();
}

可以看出剩余参数的写法更加简洁。尽管 arguments 是一个类数组,也是可迭代对象,但它终究不是数组。它不支持数组方法,当我们使用 arguments 时,如果想要调用数组方法,就必须使用Array.prototype.slice.call先将其转为数组。

而剩余参数它不同于 arguments 对象,它是真正的 Array 实例,能够很方便地使用数组方法。并且箭头函数也支持剩余参数。

另外,使用剩余参数不会影响 arguments 对象的功能,它仍然能够反映调用函数时传入的参数。

  • 剩余参数的位置

剩余参数必须是最后一个形参,否则会报错。

// 报错
function fn1(a, ...rest, b) {
	console.log([a, b, rest]);
} 

// 正确写法
function fn2(a, b, ...rest) {
    console.log([a, b, rest]);
}

fn2(1, 2, 3, 4) // 输出 [1, 2, [3, 4]]

展开语法

前面我们知道了如何把多余的参数收集为一个数组,但有时候我们需要做一些相反的事,例如要把一个数组中的元素分别传入给某个函数,而不是传入一个数组,像这样:

function sum(...values) {
    let sum = 0;
    
    for (let val of values) {
        sum += val;
    }
    
    return sum;
}

let arr = [1, 2, 3, 4];

sum(arr); // "01,2,3,4"

上面例子的函数会把所有传进来的数值累加,如果直接传入一个数组,就得不到我们想要的结果。

例子中传入一个数组, values 的值会变成 [[1, 2, 3, 4]],导致数组 values 中只有一个元素,而这个元素的类型是数组。那么函数返回值就是数值 0 和数组 [1, 2, 3, 4]相加的结果了,两者各自进行了类型的隐式转换变成字符串,然后再相加,是一个字符串拼接的效果。

要实现把数组拆解传入给函数,首先不可能一个个传入参数——sum(arr[0], arr[1], arr[2], arr[3]);,因为不是任何时候都知道数组中有多少个元素的,而且数组中可能会非常多的元素,手动传是不明智的。

比较可行的是借助 apply() 方法:

sum.apply(null, arr); // 10

但这还不是最优解,那么重点来了!

ES6 新增的**展开语法(spread)**可以帮助我们面对这种情况。它也是使用 ...变量名 的语法,虽然跟剩余参数语法一样,但是用途完全相反,它能够把一个可迭代对象拆分成逗号分隔的参数序列。

在函数调用时,它的应用是这样子的:

sum(...arr); // 10

// 相当于 sum(1,2,3,4);

它甚至可以随意搭配常规值使用,没有前后位置限制,还可以同时传入多个可迭代对象:

sum(-1, ...arr); // 9
sum(...arr, 5); // 15
sum(-1, ...arr, 5); // 14
sum(-1, ...arr, ...[5, 6, 7]); // 27

展开操作符 ... 相当于替我们完成了手动分别传参的操作,函数只知道接收的实参是单独的一个个值,不会因为展开操作符的存在而产生其他影响。

上面的示例虽然都是针对于数组的,但展开语法能做的还不止这些,其他可迭代对象例如字符串、字面量对象都可以展开,深入了解请参见 → 展开语法

总结

  • 形参是函数中已声明的局部变量,传递给函数的实参会被赋值给形参,函数参数传递实际上是一个隐式的赋值过程。

  • 形参和实参的数量可以不相等:

    ● 缺失实参的形参会得到默认值 undefined

    ● 额外的实参,可以通过 arguments 对象访问,箭头函数除外。

  • 可以通过传入对象的方式让传参顺序不再重要,让对象中的属性作为真正的实参。

  • ES6 的参数默认值——函数调用时参数的值缺失或者是 undefined ,才会获取默认值。

    ● 设置默认值的形参只有放在最后一位才可以省略传参。

    ● 形参设置默认值不能引用函数体中的变量,但可以引用前面的形参和外部变量。

    ● 通过 Object.assign() 或者解构赋值实现默认值,能让传参的方式更加灵活。

  • 剩余参数和 arguments 的主要区别:

    ● 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。

    ● 剩余参数是真正的 Array 实例,而 arguments 只是类数组对象。

  • 剩余参数和展开语法都采用 ... 操作符,在函数的相关场景中:

    ● 出现在函数形参列表的最后,它是剩余参数。

    ● 出现在函数调用时,它是展开语法。

【相关推荐:javascript学习教程

The above is the detailed content of An article explaining the parameters in JavaScript functions in detail. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:csdn.net. If there is any infringement, please contact admin@php.cn delete