函数

2023/6/15

# 函数名

函数名就是指向函数的指针

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

log(sum(10,10)) // 20
let anotherSum = sum;
log(anotherSum(10,10)) // 20
sum = null;
log(anotherSum(10,10)) // 20

anotherSumsum都指向同一个函数。调用anotherSum()也可以返回结果。把sum设置为null之后,就切断了它与函数之间的关联。而anotherSum()还是可以照常调用,没有问题。

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

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

如果函数是一个获取函数、设置函数,或者使用bind()实例化,那么标识符前面会加上一个前缀:

function foo(){}
console.log(foo.bind(null).name); // bound foo
let dog = {
    years:1,
    get age(){
        return this.years;
    },
    set age(newAge){
        this.years = newAge;
    }
}

let propertyDescriptor = Object.getOwnPropertyDescriptor(dog,"age"); // bound foo
log(propertyDescriptor.get.name) // get age
log(propertyDescriptor.set.name) // set age

# 参数

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

function sayHi_one(name,message){
    log("hello " + name + ", " + message)
}

function sayHi_two(){
    log("hello " + arguments[0] + ", " + arguments[1])
}

sayHi_one("张三","Z") // hello 张三, Z
sayHi_two("张三","Z") // hello 张三, Z

传入参数的个数

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

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

多参数传递

function doAdd(){
    if(arguments.length === 1){
        log(arguments[0] + 10)
    }else if(arguments.length === 2){
        log(arguments[0] + arguments[1])
    }
}

doAdd(10);   // 20
doAdd(30,20) // 50

箭头函数没有arguments对象

let bar = ()=>{
    log(arguments);
}

bar(5); // Uncaught ReferenceError: arguments is not defined

虽然箭头函数中没有arguments对象,但可以在包装函数中把它提供给箭头函数

function foo(){
    let bar = ()=>{
        log(arguments[0]); // 5
    }
    bar()
}

foo(5);

# 没有重载

定义了两个同名函数,则后定义的会覆盖先定义的

function addSomeNumber(num){
    return num + 100;
}

function addSomeNumber(num){
    return num + 200;
}

let result = addSomeNumber(100);
log(result) // 300

# 默认参数

function makeKing(name = "Henry"){
    return `King ${name} VIII`;
}

log(makeKing("Louis")); // King Louis VIII
log(makeKing()); // King Henry VIII

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

function makeKing(name = "Henry", numerals = 'VIII'){
    return `King ${name} ${numerals}`;
}

log(makeKing()); // King Henry VIII
log(makeKing('Louis')); // King Louis VIII
log(makeKing(undefined,'VI')); // King Henry VI

修改命名参数也不会影响arguments对象,它始终以调用函数时传入的值为准

function makeKing(name = 'Henry'){
    name = 'Louis';
    return `King ${arguments[0]}`;
}

console.log(makeKing()); // King undefined'
console.log(makeKing('Louis2')); // King Louis

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

let romanNumerals = [`I`,'II','III','IV','V','VI'];
let ordinality = 0;

function getNumerals(){
    return romanNumerals[ordinality++];
}

function makeKing(name = 'Henry',numerals = getNumerals()){
    return `King ${name} ${numerals}`;
}

console.log(makeKing()); // King Henry I
console.log(makeKing("Louis",'XVI')); // King Louis XVI
console.log(makeKing()); // King Henry II
console.log(makeKing()); // King Henry III

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

function makeKing(name = 'Henry', numerals = 'VIII') {
    return `King ${name} ${numerals}`;
}

// 想象一下这个过程
function makeKing() {
    let name = 'Henry';
    let numerals = 'VIII';
    return `King ${name} ${numerals}`;
}

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

function makeKing(name = 'Henry',numerals=name){
    return `King ${name} ${numerals}`
}

console.log(makeKing()); // King Henry Henry

参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。像这样就会抛出错误

// 调用时不传第一个参数会报错
function makeKing(name = numerals,numerals="VIII"){
    return `King ${name} ${numerals}`
}

console.log(makeKing()); // 报错
console.log(makeKing('Louis')); // King Louis VIII

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

// 调用时不传第二个参数会报错
function makeKing(name = 'Henry', numerals=defaultNumeral) {
    let defaultNumeral = 'VIII';
    return `King ${name} ${numerals}`;
}

# 参数扩展与收集

传入的参数累加

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

// 方法一
console.log(getSum.apply(null,values)); // 10
// 方法二
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 getSum(...values){
    // 顺序累加values中的所有值
    // 初始值的总和为0
    return values.reduce((x,y)=>x+y,0)
}

console.log(getSum(1,2,3)); // 6

因为收集参数的结果可变,所以只能把它作为最后一个参数

// 不可以
function getProduct(...values, lastValue) {}
// 可以
function ignoreFirst(firstValue, ...values) {
    console.log(values);
}
ignoreFirst();         // []
ignoreFirst(1);        // []
ignoreFirst(1,2);     // [2]
ignoreFirst(1,2,3);   // [2, 3]

使用收集参数并不影响arguments对象,它仍然反映调用时传给函数的参数

function getSum(...values) {
    console.log(arguments.length);   // 3
    console.log(arguments);           // [1, 2, 3]
    console.log(values);               // [1, 2, 3]
}
console.log(getSum(1,2,3));

# 函数声明与函数表达式

JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

正确

console.log(sum(10,10)) // 20
function sum(num1,num2){
    return num1 + num2;
}

报错

console.log(sum(10,10)) // 报错
let sum = function(num1,num2){
    return num1 + num2;
}

并不是因为使用let而导致的,使用var关键字也会碰到同样的问题

console.log(sum(10,10)) // 报错
var sum = function(num1,num2){
    return num1 + num2;
}

# 函数作为值

函数作为参数传递

function callSomeFunction(someFunction,someArgument){
    return someFunction(someArgument);
}

function add10(num){
    return num + 10;
}
let result1 = callSomeFunction(add10,10);
console.log(result1) // 20
function getGreeting(name){
    return `Hello, ${name}`
}
let result2 = callSomeFunction(getGreeting,"Nicholas");
console.log(result2); // Hello, Nicholas

排序

// 分别按name和age排序
function createComparisonFunction(propertyName){
    return function (object1,object2){
        let value1 = object1[propertyName];
        let value2 = object2[propertyName];
        if(value1 < value2){
            return -1;
        }else if(value1 > value2){
            return 1;
        }else {
            return 0;
        }
    }
}

let data = [
    {name:"Zachary",age:28},
    {name:"Nicholas",age:29}
]
data.sort(createComparisonFunction("name"));
console.log(data[0].name) // Nicholas
data.sort(createComparisonFunction("age"));
console.log(data[0].name) // Zachary

# 函数内部

arguments中的callee属性

错误

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

let trueFactorial = factorial;
factorial = null;

console.log(trueFactorial(5)) // 出错

正确

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

let trueFactorial = factorial;
factorial = null;

console.log(trueFactorial(5)) // 120

trueFactorial变量被赋值为factorial,实际上把同一个函数的指针又保存到了另一个位置。然后,factorial函数又被重写为一个返回0的函数。如果像factorial()最初的版本那样不使用arguments.callee就会报错

this

this引用的是把函数当成方法调用的上下文对象

window.color = "red";

let o = {
    color:"blue"
}

function sayColor(){
    console.log(this.color)
}

sayColor(); // red    这里的this是window
o.sayColor = sayColor;
o.sayColor(); // blue 这里的this是o

new.target

function King(){
    if(!new.target){
        throw 'King must be instantiated using "new"'
    }
    console.log('King instantiated using "new"')
}

new King(); // King instantiated using "new"
King(); // Uncaught King must be instantiated using "new"

# 函数属性与方法

length属性保存函数定义的命名参数的个数

function sayName(name){
    console.log(name);
}

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

function sayHi(){
    console.log("hi")
}

console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0

prototype属性也许是ECMAScript核心中最有趣的部分。prototype是保存引用类型所有实例方法的地方,在ECMAScript 5中,prototype属性是不可枚举的,因此使用for-in循环不会返回这个属性。

apply

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

function callSum1(num1,num2){
    return sum.apply(this,arguments); // 传入arguments 对象
}

function callSum2(num1,num2){
    return sum.apply(this,[num1,num2]); // 传入数组
}

console.log(callSum1(10,10)); // 20
console.log(callSum2(10,10)); // 20

通过call()向函数传参时,必须将参数一个一个地列出来

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

function callSum(num1,num2){
    return sum.call(this,num1,num2); 
}

console.log(callSum(10,10)); // 20

apply()和call()真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内this值的能力。

window.color = 'red';
let o = {
    color:"blue"
};

function sayColor(){
    console.log(this.color);
}

sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue

bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象。

window.color = 'red';
let o = {
    color:"blue"
};

function sayColor(){
    console.log(this.color);
}

let objectSayColor = sayColor.bind(o);
objectSayColor(); // blue

sayColor()上调用bind()并传入对象o创建了一个新函数objectSayColor()。objectSayColor()中的this值被设置为o,因此直接调用这个函数,即使是在全局作用域中调用,也会返回字符串"blue"。

# 递归

arguments.callee就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用

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

在严格模式下运行的代码是不能访问arguments.callee的,因为访问会出错。此时,可以使用命名函数表达式达到目的

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

const b = factorial;
factorial = null;

console.log(b(5)); // 120

# 尾调用优化

# 闭包

匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量函数,通常是在嵌套函数中实现的。

# 立即调用的函数表达式

立即调用的匿名函数又被称作立即调用的函数表达式(IIFE, Immediately Invoked Function Expression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式。

(function (){
    for(var i = 0;i<4;i++){
        console.log(i)
    }
})();

console.log(i) // 抛出错误

块级作用域1

{
    let i;
    for(i = 0;i<4;i++){
        console.log(i)
    }
}

console.log(i) // 抛出错误

块级作用域2

for(let i = 0;i<4;i++){
    console.log(i)
}

console.log(i) // 抛出错误