JavaScript中函数详解

前言

函数实际上是对象,每个函数都是Function类型的实列,和其他引用类型一样Function也有属性和方法

箭头函数

ECMAScript 6新增了使用胖箭头(=>)语法定义函数表达式的能力

let arrowSum = (a, b)=>{
    return a+b;
};

let functionExpressionSum= function(a, b) {
    return a + b;
};

console.log(arrowSum(0,8)); //8
console.log(functionExpressionSum(100, 100)) //200

如果只有一个参数,那也可以不用括号。只有没有参数,或者多个参数的情况下,才需要使用括号

// 两种写法都有效
let double = (x) => {return 2 * x;};
let triple =x=> {return 3 * x;};
// 无参数需要括号
let getRandom = ()=>{return Math.random();};
// 多参数需要括号
let sum = (a, b) => {return a+b;};
// 无效的写法
let multiply = a, b => {return a * b;}; // 报错

函数名

因为函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。一个函数可以有多个名称

function sum(num1, num2){
    return num1 + num2;
}

console.log(sum(100,100)); //200
let anotherSum = sum;
console.log(anotherSum(100,100));//200
sum = null;
console.log(anotherSum(100, 100)); //200

使用不带括号的函数名会访问函数指针,而不会执行函数。此时,anotherSum和sum都指向同一个函数。调用anotherSum()也可以返回结果。把sum设置为null之后,就切断了它与函数之间的关联。而anotherSum()还是可以照常调用

ECMAScript 6的所有函数对象都会暴露一个只读的name属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。如果它是使用Function构造函数创建的,则会标识成"anonymous"

function foo(){};
let bar = function(){};
let baz = () =>{};
console.log(foo.name); //foo
console.log(bar.name); //bar
console.log(baz.name); //baz
console.log((()=>{}).name) //(空字符串)
console.log((new Function()).name) //anonymous

参数

ECMAScript函数的参数跟大多数其他语言不同。ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一个、三个,甚至一个也不传,解释器都不会报错

ECMAScript函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么

arguments对象是一个类数组对象(但不是Array的实例),因此可以使用中括号语法访问其中的元素(第一个参数是arguments[0],第二个参数是arguments[1])。而要确定传进来多少个参数,可以访问arguments.length属性

function sayHi(name, message){
    console.log("hello" + name + "," +message);
}

可以通过arguments[0]取得相同的参数值。因此,把函数重写成不声明参数也可以

function sayHi(name, message){
    console.log("hello" + arguments[0] + "," +arguments[1]);
}

在重写后的代码中,没有命名参数。name和message参数都不见了,但函数照样可以调用。这就表明,ECMAScript函数的参数只是为了方便才写出来的,并不是必须写出来的。与其他语言不同,在ECMAScript中的命名参数不会创建让之后的调用必须匹配的函数签名。这是因为根本不存在验证命名参数的机制

通过arguments对象的length属性检查传入的参数个数

function howManyArgs(){
    console.log(arguments.length);
}

howManyArgs("string", 100) //2
howManyArgs();  //0
howManyArgs(12) //1

重载

ECMAScript函数不能像传统编程那样重载。在其他语言比如Java中,一个函数可以有两个定义,只要签名(接收参数的类型和数量)不同就行。如前所述,ECMAScript函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载

默认参数值

在ECMAScript5.1及以前,实现默认参数的一种常用方式就是检测某个参数是否等于undefined,如果是则意味着没有传这个参数,那就给它赋一个值

function hello(name){
    name = (typeof name !== 'undefined')?name:'world';
    return `hello ${name}`;
}
console.log(hello())  //hello world
console.log(hello("holy")) //hello holy

ECMAScript 6之后支持显式定义默认参数

function hello(name = 'world'){
    return `hello ${name}`;
}
console.log(hello())  //hello world
console.log(hello("holy")) //hello holy

给参数传undefined相当于没有传值,这样可以利用多个独立的默认值

function hello(name = 'world', greet='hi'){
    return `hello ${name} ${greet}`;
}
console.log(hello())  //hello world hi
console.log(hello("holy")) //hello holy hi
console.log(hello(undefined, 'nice to meet you')) //hello world nice to meet you

默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值:

let romanNumberals = ['I', 'II', 'III', 'IV', 'V']
let ordinality = 0;

function getNumberals(){
    // 每次调用后递增
    return romanNumberals[ordinality++];
}

function hello(name='world', numberals=getNumberals()){
    return `hello ${name} ${numberals}`;
}
console.log(hello()) //hello world I
console.log(hello('Louis', 'XVI')) //hello Louis XVI
console.log(hello()) //hello world II
console.log(hello()) //hello 

函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数只有在调用函数但未传相应参数时才会被调用

箭头函数同样也可以这样使用默认参数,只不过在只有一个参数时,就必须使用括号而不能省略了

let hello = (name = 'Herry') => `Hello ${name}`;
console.log(hello()); 

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

因为在求值默认参数时可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的

给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样

function makeKing(name='holy', numberals='VIIII'){
    return `King ${name} ${numberals}`;
}
console.log(makeKing());

这里的默认参数会按照定义它们的顺序依次被初始化,也就等同于

function makeKing(){
    let name='holy';
    let numberals='VIIII';
    return `King ${name} ${numberals}`;
}

因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数

function makeKing(name='holy', numberals=name){
    return `King ${name} ${numberals}`;
}
console.log(makeKing()); //King holy holy

参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的

// 这样会报错
function makeKing(name=numberals, numberals=name){
    return `King ${name} ${numberals}`;
}

参数也存在于自己的作用域中,它们不能引用函数体的作用域

// 这样会报错
function makeKing(name=numberals, numberals=defaultnum){
    let defaultnum = 100;
    return `King ${name} ${numberals}`;
}

参数扩展和参数收集

参数扩展

ECMAScript 6新增了扩展操作符,使用它可以非常简洁地操作和组合集合数据。扩展操作符最有用的场景就是函数定义中的参数列表,在这里它可以充分利用这门语言的弱类型及参数长度可变的特点。扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数

假设有如下函数定义,它会将所有传入的参数累加起来

let values = [1,2,3,4]
function getSum(){
    let sum = 0;
    for (let i=0;i<arguments.length;i++){
        sum += arguments[i];
    }
    return sum;
}

这个函数希望将所有加数逐个传进来,然后通过迭代arguments对象来实现累加。如果不使用扩展操作符,想把定义在这个函数这面的数组拆分,那么就得求助于apply()方法

console.log(getSum.apply(null, values)); //10

在ECMAScript 6中,可以通过扩展操作符极为简洁地实现这种操作。对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。

比如,使用扩展操作符可以将前面例子中的数组像这样直接传给函数

console.log(getSum(... values)); //10

使用扩展操作符可以在其前面或后面再传其他的值,包括使用扩展操作符传其他参数

console.log(getSum(-1, ...values)) //9
console.log(getSum(...values, 5)) //15
console.log(getSum(-1, ...values, 5)) //14
console.log(getSum(...values, ... [5,6,7])) //28

收集参数

在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组

形参使用了 ... 操作符会变成一个数组,多余的实参都会被放进这个数组中

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

收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数

function ignoreFirst(firstValue, ...values){
    console.log(values);
}
ignoreFirst(); //[]
ignoreFirst(1); //[]
ignoreFirst(1,2); //[2]
ignoreFirst(1,2,3); //[2,3]

函数作为值

函数也可以作为参数传递给另一个函数,而且还可以在一个函数中返回另一个函数

function callSomFunction(SomeFunction, someArgument){
    return someFunction(someArgument);
}

这个函数接收两个参数。第一个参数应该是一个函数,第二个参数应该是要传给这个函数的值

函数内部对象

在ECMAScript 5中,函数内部存在两个特殊的对象:arguments和this。ECMAScript 6又新增了new.target属性。

arguments

arguments是一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以function关键字定义函数才会有。arguments对象还有一个callee属性,是一个指向arguments对象所在函数的指针

function factorial(num){
    if (num<=1){
        return 1;
    }else{
        return num * factorial(num-1);
    }
}

阶乘函数一般定义成递归调用的,就像上面这个例子一样。只要给函数一个名称,而且这个名称不会变,这样定义就没有问题。但是,这个函数要正确执行就必须保证函数名是factorial,从而导致了紧密耦合。使用arguments.callee就可以让函数逻辑与函数名解耦:

function factorial(num){
    if (num<=1){
        return 1;
    }else{
        return num * arguments.callee(num-1);
    }
}

this

在标准函数中,this引用的是把函数当成方法调用的上下文对象,这时候通常称其为this值(在网页的全局上下文中调用函数时,this指向windows)





posted @ 2022-06-30 08:28  Apostle浩  阅读(40)  评论(0编辑  收藏  举报