红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

JavaScript 学习笔记 3:函数

2026年4月7日 4点热度 0人点赞 0条评论

剩余参数

对于变长参数的函数,在 JavaScript 中可以这么定义:

function sum(...numbers) {
    let sum = 0;
    for (let number of numbers) {
        sum += number;
    }
    return sum;
}
​
console.log(sum(1, 2, 3));
console.log(sum());
// 6
// 0

可以在变长参数前添加若干确定的参数:

function display(message, ...names) {
    console.log(message, names);
}
​
display("Hello", "Alice", "Bob", "Charlie");
// Hello [ 'Alice', 'Bob', 'Charlie' ]

需要注意的是,变长参数只能有一个,且必须要在结尾处,否则会报错。

在旧版本的 JavaScript 中,可以使用一个特殊参数arguments获取所有的函数参数:

function sum2() {
    let sum = 0;
    for (let number of arguments) {
        sum += number;
    }
    return sum;
}
​
console.log(sum2(1, 2, 3));
// 6

比较特殊的是,箭头函数没有自己的arguments:

function test() {
    console.log("test", arguments);
    let test2 = () => {
        console.log("test2", arguments);
    }
    test2();
}
​
test(1, 2, 3);
// test [Arguments] { '0': 1, '1': 2, '2': 3 }
// test2 [Arguments] { '0': 1, '1': 2, '2': 3 }

在这个示例中,箭头函数的 arguments 所在的外部函数的 arguments。这与箭头函数没有自己的this是类似的。

展开语法

对于变长参数,可以用数组结合展开语法进行调用:

let numbers = [1, 2, 3];
console.log(sum(...numbers));

展开语法还可以用于合并数组:

let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5];
console.log(arr2);
// [ 1, 2, 3, 4, 5 ]

或者拷贝数组:

let oldArr = [1, 2, 3];
let newArr = [...oldArr];
oldArr.push(4);
console.log(oldArr);
console.log(newArr);
// [ 1, 2, 3, 4 ]
// [ 1, 2, 3 ]

拷贝对象:

let person1 = { name: "Alice", age: 20 };
let person2 = { ...person1};
person1.name = "Bob";
console.log(person1);
console.log(person2);
// { name: 'Bob', age: 20 }
// { name: 'Alice', age: 20 }

展开语法可以用于任何可迭代对象,相当于使用for...of进行遍历,比如:

let chars = "abcdefg";
console.log([...chars]);
// [
//   'a', 'b', 'c',
//   'd', 'e', 'f',
//   'g'
// ]

这相当于:

let char2 = "abcdefg";
console.log(Array.from(char2))

只不过Array.from更通用,它接收的参数可以是类数组或可迭代对象。

函数

在 JavaScript 中,函数也是对象,因此它有一些属性:

name

name属性是函数名:

function func1(){
}
console.log(func1.name);
// func1

匿名函数也可以获取到正确的函数名:

let func2 = function(){
}
console.log(func2.name);
// func2

对象中的函数(方法)也可以获取到函数名:

let person = {
    func1: ()=>{},
    func2: function(){},
}
​
console.log(person.func1.name);
console.log(person.func2.name);
// func1
// func2

特殊情况下也可能获取不到函数名,比如:

let arr = [function(){}];
console.log(arr[0].name);
//

length

函数的length属性表示函数的参数个数:

function func3(){}
function func4(num1,num2){}
function func5(num1, num2, ...rest){}
​
console.log(func3.length);
console.log(func4.length);
console.log(func5.length);
// 0
// 2
// 2

自定义属性

可以为函数添加自定义属性:

function countFunc(){
    console.log(countFunc.count);
    countFunc.count++;
}
countFunc.count = 0;
countFunc();
countFunc();
countFunc();
// 0
// 1
// 2

命名函数表达式

有时候,会在使用函数表达式时涉及函数的自调用问题:

let func6 = function (param) {
    if (param) {
        console.log(param);
    }
    else {
        func6("Tom");
    }
};
func6();
// Tom

一般而言这样做没有什么问题,但有时候会因为外部的函数变量的改变而出错:

let func7 = function (param) {
    if (param) {
        console.log(param);
    }
    else {
        func7("Tom");
    }
};
let func8 = func7;
func7 = null;
func8();
// TypeError: func7 is not a function

在这里匿名函数内部使用外部变量名func7,而该变量在之后被重新赋值为null,因此这里会报错。

为了解决这个问题,我们需要为匿名函数提供一个可靠的名字:

let func7 = function funcSelf(param) {
    if (param) {
        console.log(param);
    }
    else {
        funcSelf("Tom");
    }
};
let func8 = func7;
func7 = null;
func8();

这里为函数表达式添加了一个名字funcSelf,这个名字仅能在函数表达式内部使用。现在函数表达式的自调用不再依赖于外部变量,因此外部变量被重新赋值后也不会影响函数表达式本身的调用。

new Function

有一种很少使用的创建函数的方式:

let sum = new Function('a', 'b', 'return a + b');
console.log(sum(1, 2));
// 3

还可以使用变长参数:

let funcContent = `
  if(!arr){
    return 0;
  }
  let total = 0;
  for(num of arr){
    total += num;
  }
  return total;
`;
let sumArr = new Function('...arr', funcContent);
console.log(sumArr(1, 2, 3, 4, 5));
// 15

利用这个功能,我们可以从其它地方获取 JS 代码并用这些代码创建函数。

通过这种方式创建的函数不能访问外部变量:

function test(){
    let a = 1;
    let codeFunc = new Function('return a + 1');
    return codeFunc();
}
​
console.log(test());
// 报错

普通定义的函数不存在这个问题:

function test(){
    let a = 1;
    let codeFunc = function(){
        return a + 1;
    };
    return codeFunc();
}
​
console.log(test());
// 2

延迟执行

setTimeout

使用setTimeout可以在指定一段时间后执行某个函数:

function showTime(){
    console.log(Date.now());
}
​
setTimeout(showTime, 1000);

如果函数需要参数:

function sum2(a, b) {
    console.log(a + b);
}
​
setTimeout(sum2, 1000, 1, 2);
// 3

更常见的是使用箭头函数:

setTimeout(() => {
    console.log(Date.now());
}, 1000);

clearTimeout

setTimeout会返回一个标识,使用这个标识可以取消:

let timeout = setTimeout(() => console.log("hello"), 1000);
clearTimeout(timeout);

这里代码不会被执行。

setInterval

setInterval可以周期性执行代码:

let timeShowSchedule = setInterval(() => {
    console.log(Date.now());
}, 1000);
setTimeout(() => clearInterval(timeShowSchedule), 5000);

这里每1秒会打印一次时间,在5秒后停止。

嵌套 setTimeout

使用嵌套 setTimeout 可以起到和 setInterval 相同的效果:

setTimeout(function showTime(){
    console.log(formatTime(Date.now()));
    setTimeout(showTime, 1000);
}, 1000);

这里在内部的setTimeout中调用了外部setTimeout使用的命名函数showTime,因此形成了一种循环调用,最终效果和setInterval类似。

不同的是,嵌套setTimeout有更高的灵活性,可以按需要动态调整之后调用的延迟时间,比如:

function pingServer(){
    // 模拟测试服务器是否存活
    return Math.random() > 0.5;
}
​
setTimeout(function ping(delayN = 1){
    if(pingServer()){
        console.log('服务器正常');
        delayN = 1;
    }else{
        console.log('服务器异常');
        delayN *= 2;
        console.log(`延迟${delayN}秒重试`);
    }
    setTimeout(ping, 1000*delayN, delayN);
}, 1000)

如果服务端正常,每1秒检查一次服务器状态,如果服务器异常,每次检查时间延长2倍时间。

装饰器和转发

装饰器

// 计算斐波那契数列
function fib(n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}
​
console.log('fib(10)', fib(10));
console.log('fib(40)', fib(40));
console.log('fib(40)', fib(40));
console.log('fib(40)', fib(40));

示例展示的是一个计算斐波那契数列的函数,这个函数计算时间较长,即使连续调用的n相同,也需要消耗较长时间。

我们可以为这个函数添加缓存以优化执行性能:

let cache = new Map();
function cachedFib(n) {
    if (cache.has(n)) {
        return cache.get(n);
    }
    let result = fib(n);
    cache.set(n, result);
    return result;
}
​
console.log('cachedFib(40)', cachedFib(40));
console.log('cachedFib(40)', cachedFib(40));
console.log('cachedFib(40)', cachedFib(40));

可以看到,连续调用同一个n时,执行效率被提高了。

但这样做有两个缺陷:

  • 缓存逻辑与函数本身的逻辑混杂在一起,代码结构不清晰,不符合设计模式中的单一职责原则。

  • 代码无法复用,需要为每个业务函数编写缓存逻辑。

可以使用装饰器模式:

function cacheFunc(fn) {
    let cache = new Map();
    return function (...args) {
        let key = args.join(',');
        if (cache.has(key)) {
            return cache.get(key);
        }
        let result = fn(...args);
        cache.set(key, result);
        return result;
    }
}
​
let cachedFib2 = cacheFunc(fib);
console.log('cachedFib2(40)', cachedFib2(40));
console.log('cachedFib2(40)', cachedFib2(40));
console.log('cachedFib2(40)', cachedFib2(40));

Func.call

对于对象方法,之前的做法可能出错,比如:

let myMath = {
    tag: 'myMath',
    fib: function (n) {
        console.log(`${this.tag} fib(${n})`);
        if (n <= 1) {
            return n;
        }
        return fib(n - 1) + fib(n - 2);
    }
}
​
console.log(myMath.fib(10));
let cachedFib3 = cacheFunc(myMath.fib);
// myMath fib(10)
// 55
// undefined fib(10)
// 55

第二次输出undefined,因为被缓存的方法被执行时,JS 找不到关联的对象(this 指针为 undefined)。JS 提供一个func.call方法用于在执行对象方法时,显式指定方法关联的对象,比如:

function cacheObjFunc(obj, fn) {
    let cache = new Map();
    return function (...args) {
        let key = args.join(',');
        if (cache.has(key)) {
            return cache.get(key);
        }
        let result = fn.call(obj, ...args);
        cache.set(key, result);
        return result;
    }
}
​
let cachedFib4 = cacheObjFunc(myMath, myMath.fib);
console.log(cachedFib4(10))
// myMath fib(10)
// 55

函数绑定

之前有提到过,如果传递对象方法,可能出现this丢失的问题:

let user2 = { 
    name: 'Jack',
    hello() {
        console.log(`hello ${this.name}`);
    }
};
user2.hello();
setTimeout(user2.hello, 1000);
// hello undefined

第一种解决方式是使用包装函数:

setTimeout(function(){
    user2.hello();
},1000);
// hello Jack

第二种解决方式是使用绑定函数:

setTimeout(user2.hello.bind(user2), 1000);
// hello Jack

func.bind返回的函数是将对象与函数绑定后的函数,因此现在的this不再是 undefined。

绑定参数

除了常见的绑定this以外,还可以绑定参数,比如:

function multiply(a, b) {
    return a * b;
}
​
let double = multiply.bind(null, 2);
console.log(double(1));
console.log(double(2));
console.log(double(3));
// 2
// 4
// 6

就像示例中展示的,使用bind可以利用已有函数快速构建新的方法。需要注意的是,虽然这里绑定的是普通函数,没有关联的对象,但依然需要为bind函数指定一个对象参数作为第一个参数,所以这里使用null。

不指定上下文

如果在函数调用时,需要固定一部分参数,但不固定上下文的话,可以:

function partial(fn, ...args) {
    return function (...rest) {
        return fn.call(this, ...args, ...rest);
    }
}
​
let user3 = {
    name: 'Jack',
    hello(prefix, suffix) {
        console.log(`${prefix} ${this.name} ${suffix}`);
    }
}
​
user3.hello2 = partial(user3.hello, 'hello');
user3.hello2('!');
// hello Jack !

partial返回的函数中调用时使用的是this,因此user3.hello2调用时关联的上下文(this)就是user3。

箭头函数

this

let names = {
    title: 'Mr',
    names: ['Tom', 'Jerry'],
    hello() {
        this.names.forEach(function (name) {
            console.log(`hello, ${this.title} ${name}`);
        })
    }
}
names.hello();
// hello, undefined Tom
// hello, undefined Jerry

在这个示例中,this是匿名函数的this,并没有指向外部的对象names,因此这里输出是undefined。

当然可以直接使用:

console.log(`hello, ${names.title} ${name}`);

如果你习惯于使用this,可以:

let names = {
    title: 'Mr',
    names: ['Tom', 'Jerry'],
    hello() {
        this.names.forEach((name) => {
            console.log(`hello, ${this.title} ${name}`);
        })
    }
}
names.hello();
// hello, Mr Tom
// hello, Mr Jerry

这里用箭头函数代替普通匿名函数,区别在于箭头函数没有自己的this,因此这里箭头函数内使用this时,程序将在外部查找this,也就是names对象。

arguments

function delayCall(fn, delay){
    return function (){
        console.log('call function after delay time');
        setTimeout(()=>{
            fn.apply(this, arguments);
        }, delay);
    }
}
​
function sayHello(name) {
    console.log(`hello ${name}`);
}
​
delayCall(sayHello, 1000)('Tom');

在这个示例中,delayCall返回一个包装函数,它将目标函数进行包装,以在指定延时后调用。示例中的包装函数中setTimeout接收的是箭头函数,并使用特殊的arguments变量获取包装函数实际执行时接收的参数。之所以可以这样,是因为箭头函数没有arguments这个特殊变量,因此arguments是从外部(包装函数)获取的。

如果不是箭头函数而是普通函数:

function delayCall2(fn, delay){
    return function (...args){
        console.log('call function after delay time');
        let ctx = this;
        setTimeout(function () {
            fn.apply(ctx, args);
        }, delay);
    }
}
​
delayCall2(sayHello, 1000)('Tom');

这里就必须让包装函数显式接收参数(...args),并使用这个参数进行调用。

The End.

本文完整的示例代码可以从这里获取。

参考资料

  • 现代 JavaScript 教程

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2026年4月7日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号