继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

JavaScript函数的参数详解

为爱心太软
关注TA
已关注
手记 170
粉丝 1.4万
获赞 860

基本概念

JavaScript函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。也就是说,无论你在函数创建的时候声明了多少形参,都可以传入任意数量的参数(没有传递值的命名参数将自动被赋予undefined值)。之所以会这样,原因是JavaScript中的参数在内部是用一个数组来表示。

Arguments

在函数体内可以通过arguments对象来访问这个由参数组成的数组,但arguments并不是Array的实例。它只是可以使用方括号语法访问它的每一个元素,也可以使用length属性来确定传递进来多少个参数(不是创建函数时形参的个数),其他数组的方法,arguments并不能直接使用,但也可以把arguments转换成真正的数组。

例子1:

function demo(n1, n2) {
    var arr = [];
    var len = arguments.length;
    for (var i = 0; i < len; i++) {
        if (typeof arguments[i] == "number") {
            arr.push(arguments[i]);
        }
    }
    console.log(len);
    console.log(arr);
}
demo(); //输出:0,[]
demo(1); //输出:1,[1]
demo(1, 2); //输出:2,[1,2]
demo(1, 2, 3); //输出:3,[1,2,3]

例子2:

function demo(n1, n2) {
    //把arguments变成Array的实例
    var args = Array.prototype.slice.call(arguments);
    var newArgs = args.map(function(v) {
        return v + 1;
    })
    console.log(Array.isArray(arguments)); //输出:false
    console.log(Array.isArray(args)); //输出:true
    console.log(newArgs); //输出:[2,3,4]
}
demo(1, 2, 3);

在ES5非严格模式下,命名参数的变化会动态更新到arguments对象中,arguments值的变化也会更新到对应的命名参数上。没有传递值的命名参数如果在函数体内部进行赋值,不会更新到arguments对象中,反之对arguments赋值也不会同步更新到对应的命名参数上。

例子:

function demo1(n1, n2) {
    console.log(arguments[0]); //输出:1
    console.log(n1); //输出:1
    console.log(arguments[1]); //输出:undefined
    console.log(n2); //输出:undefined
    n1 = 3;
    n2 = 4;
    console.log(arguments[0]); //输出:3
    console.log(n1); //输出:3
    console.log(arguments[1]); //输出:undefined
    console.log(n2); //输出:4
}
demo1(1);

function demo2(n1, n2) {
    console.log(arguments[0]); //输出:1
    console.log(n1); //输出:1
    console.log(arguments[1]); //输出:undefined
    console.log(n2); //输出:undefined
    arguments[0] = 3;
    arguments[1] = 4;
    console.log(arguments[0]); //输出:3
    console.log(n1); //输出:3
    console.log(arguments[1]); //输出:4
    console.log(n2); //输出:undefined
}
demo2(1);

然而在ES5的严格模式下,取消了这种命名参数与arguments对象同步更新的现象。

例子:

function demo1(n1, n2) {
    "use strict"
    console.log(arguments[0]); //输出:1
    console.log(n1); //输出:1
    n1 = 3;
    console.log(arguments[0]); //输出:1
    console.log(n1); //输出:3
}
demo1(1);

function demo2(n1, n2) {
    "use strict"
    console.log(arguments[0]); //输出:1
    console.log(n1); //输出:1
    arguments[0] = 3;
    console.log(arguments[0]); //输出:3
    console.log(n1); //输出:1
}
demo2(1);

arguments对象还有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。一般在阶乘函数中会用到这个属性。

例子:

function fn(n) {
    if (n > 1) {
        return n * arguments.callee(n - 1);
    } else {
        return 1;
    }
}
fn(3);
//输出:6

箭头函数没有arguments的绑定,箭头函数中的arguments的值由最近一层非箭头函数决定。

例子:

function demo(n1, n2) {
    (() => {
        console.log(arguments[0]);
    })();
}
demo(1);
//输出:1

ES6中对函数参数的扩展

参数默认值

ES6允许为任意参数指定初始值。

例子:

let demo1 = (str1, str2 = "world") => {
    console.log(str1 + " " + str2)
}
demo1(); //输出:undefined world
demo1("hello"); //输出:hello world
demo1("hello", "China"); //输出:hello China
demo1("hello", ""); //输出:hello
demo1("hello", undefined); //输出:hello world

let demo2 = (str1, str2 = "B", str3) => {
    console.log([str1, str2, str3]);
}
demo2(); //输出:[undefined, "B", undefined]
demo2("A"); //输出:["A", "B", undefined]
demo2("A", undefined); //输出:["A", "B", undefined]
demo2("A", undefined, "C"); //输出:["A", "B", "C"]
demo2("A", , "C"); //输出:报错

*通过上面的两个例子可以发现:只有当不为第二个参数传入值或主动为第二个参数传入undefined时才会使用str2的默认值。

参数的默认值除了可以是原始值,也可以是变量、表达式或函数。

例子:

参数默认值为变量

let n = 1;
let fn = (a, b = n) => {
    console.log(a + b);
}
fn(1);
//输出:2

参数默认值为表达式

let n = 1;
let fn = (a, b = n + 1) => {
    console.log(a + b);
}
fn(1);
//输出:3

参数默认值为函数执行的值

let n = () => 1;
let fn = (a, b = n()) => {
    console.log(a + b);
}
fn(1);
//输出:2

*通过上面的三个例子可以发现:只有当调用函数fn且不传入第二个参数的时候,第二个参数才会进行求值。

默认参数的临时死区

一旦为参数设置了默认值,当调用函数时,函数的参数会被添加到一个专属于函数参数的临时死区(与let的行为类似),其与函数体的作用域是各自独立的。等到参数初始化结束,这个临时死区就会消失。

例子1:

let n = 1;
let fn = (a, b = a + n) => {
    console.log(a + b);
}
fn(1, 1); 
//输出:2

*调用 fn(1, 1) 相当于在引擎的背后做了如下事情

{
    let a = 1;
    let b = 1;
}

例子2:

let n = 1;
let fn = (a, b = a + n) => {
    console.log(a + b);
}
fn(1); 
//输出:3

*调用 fn(1) 相当于在引擎的背后做了如下事情

{
    let a = 1;
    let b=1+1;
}

注意区分下面两种情况:

例子1:

let n = 1;
let fn = (a, b = n) => {
    let b = 9;
    console.log(a + b);
}
fn(1); 
//输出:报错

上例中,参数变量b是默认声明的,在函数体中,不能用let或const再次声明(var可以),否则会报错。

例子2:

let n = 1;
let fn = (a, b = n) => {
    let n = 9;
    console.log(a + b);
}
fn(1); 
//输出:2

上例中,函数fn调用时,参数a、b被添加到一个专属于函数参数的临时死区,这个死区里面,变量n没有定义,所以指向外层的全局变量n。而函数体内部的局部变量n与临时死区的变量n各自独立,影响不到默认值n。
如果此时,全局变量n不存在,就会报错。

例子:

let fn = (a, b = n) => {
    let n = 9;
    console.log(a + b);
}
fn(1); 
//输出:报错

上例中,如果同时存在全局变量n和参数n,默认值变量n会指向第一个参数 n。

例子:

let n = 1;
let fn = (n, b = n) => {
    console.log(n + b);
}
fn(2); 
//输出:4

调用函数fn时,参数形成一个单独的临时死区。在这个死区里面,默认值变量n指向第一个参数n,而不是全局变量n。
如果参数n与b交换位置,当b初始化时,n尚未初始化,所以会导致错误。

例子:

let n = 1;
let fn = (b = n, n) => {
    console.log(n + b);
}
fn(); 
//输出:报错

默认参数值对arguments对象的影响

在ES6中,如果函数使用了默认参数值,则无论是否定义了严格模式,arguments对象的行为都将与ES5严格模式下保持一致。默认参数值的存在使得arguments对象保持与命名参数的分离,我们随时都可以通过arguments对象将参数恢复为初始值。

例子:

function demo1(n1, n2 = 2) {
    console.log(arguments[0]); //输出:1
    console.log(n1); //输出:1
    n1 = 3;
    console.log(arguments[0]); //输出:1
    console.log(n1); //输出:3
}
demo1(1);

function demo2(n1, n2 = 2) {
    console.log(arguments[0]); //输出:1
    console.log(n1); //输出:1
    arguments[0] = 3;
    console.log(arguments[0]); //输出:3
    console.log(n1); //输出:1
}
demo2(1);

不定参数

在函数的命名参数前添加三个点(…)就表明这是一个不定参数。不定参数是一个数组,会将命名参数后面的多余参数放入数组中。不定参数之后不能再有其他参数。

例子:

let fn = (obj, ...items) => {
    let result = {};
    for (let i = 0; i < items.length; i++) {
        result[obj + "_" + i] = items[i];
    }
    console.log(result);
}
fn("name", "Tom", "Amy");
//输出:{name_0: "Tom", name_1: "Amy"}

不定参数与arguments对象的区别

1、arguments对象不是数组,而是一个类数组对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组;不定参数是一个真正的数组,数组特有的方法都可以使用。

例子:

((...items) => {
    console.log(Array.isArray(items));
})();
//输出:true

2、arguments对象包含所有传入函数的参数;不定参数包含自它之后的所有参数。

例子:

(function(n1, n2, ...nx) {
    console.log(arguments); //输出:[1, 2, 3, 4, 5]
    console.log(nx); //输出:[3, 4, 5]
})(1, 2, 3, 4, 5)

解构参数

解构参数需要使用对象或数组解构模式代替命名参数。
有关于对象和数组的解构,可以参考下面的手记内容:

例子:
以数组字面量的语法进行解构赋值

(function([n1, n2]) {
    console.log(n1 + n2);
})([1, 1]);
//输出:2

以对象字面量的语法进行解构赋值

(function(name, {
    age,
    job
}) {
    console.log(age);
})("Tom", {
    age: 19
})
//输出:19

解构参数有一点需要特别注意:在调用函数时,如果不提供以供解构的参数会导致错误。

例子:
调用函数时提供以供解构的对象字面量,不会报错

(function(name, {
    age,
    job
}) {
    console.log(age);
})("Tom", {})
//输出:undefined

调用函数时没有提供以供解构的对象字面量,报错

(function(name, {
    age,
    job
}) {
    console.log(age);
})("Tom")
//输出:报错

我们可以像为命名参数提供默认值那样,为解构参数提供默认值来解决这个问题。

例子:

(function(name, {
    age,
    job
} = {}) {
    console.log(age);
})("Tom")
//输出:undefined

*上例中,我们将解构参数{age,job}看成一个命名参数,并为它提供默认值{}。这样即使在调用函数时,没有提供第二个参数,也不会报错。

参数默认值与解构参数默认值相结合

注意区分下面两种写法:

例子1:

//解构内部的变量默认值为{x = 0,y = 0}
//解构作为参数的默认值为{}
let fn = ({
    x = 0,
    y = 0
} = {}) => {
    console.log([x, y])
}

fn(); //输出:[0, 0]
fn({}); //输出:[0, 0]
fn({
    x: 1,
    y: 1
}); //输出:[1, 1]
fn({
    y: 1
}); //输出:[0, 1]

例子2:

//解构内部的变量默认值为{x ,y}
//解构作为参数的默认值为{x = 0,y = 0}
let fn = ({
    x,
    y
} = {
    x: 0,
    y: 0
}) => {
    console.log([x, y])
}

fn(); //输出:[0, 0]
fn({}); //输出:[undefined, undefined]
fn({
    x: 1,
    y: 1
}); //输出:[1, 1]
fn({
    y: 1
}); //输出:[undefined, 1]

如有错误,欢迎指正,本人不胜感激。

打开App,阅读手记
4人推荐
发表评论
随时随地看视频慕课网APP