Skip to content

new function


它实际上是通过运行时通过参数传递过来的字符串创建的。 new Function 允许我们将任意字符串变为函数。例如,我们可以从服务器接收一个新的函数并执行它

setTimeOut setaInterval


目前有两种方式可以实现:

  • setTimeout 允许我们将函数推迟到一段时间间隔之后再执行。
  • setInterval 允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。 这两个方法并不在 JavaScript 的规范中。但是大多数运行环境都有内建的调度程序,并且提供了这些方法。目前来讲,所有浏览器以及 Node.js 都支持这两个方法。参数说明:

func|code 想要执行的函数或代码字符串。 一般传入的都是函数。由于某些历史原因,支持传入代码字符串,但是不建议这样做。

delay 执行前的延时,以毫秒为单位(1000 毫秒 = 1 秒),默认值是 0;

arg1arg2… 要传入被执行函数(或代码字符串)的参数列表(IE9 以下不支持)

javascript
function eat(name, food) {
    console.log(name+" eat "+food);
}

setTimeout(eat, 1000, "dano", "apple");//dano eat apple

setTimeout(eat("dano", "apple"), 1000);//The "callback" argument must be of type function.

setTimeout 在调用时会返回一个“定时器标识符(timer identifier)”,在我们的例子中是 timerId,我们可以使用它来取消执行。

javascript
function eat(name, food) {
    console.log(name+" eat "+food);
}

let timerId= setTimeout(eat, 1000, "dano", "apple");
clearTimeout(timerId);//没有输出

在浏览器中,定时器标识符是一个数字。在其他环境中,可能是其他的东西。例如 Node.js 返回的是一个定时器对象,这个对象包含一系列方法。这些方法没有统一的规范定义,所以这没什么问题。

setInterval 方法和 setTimeout 的语法相同

在大多数浏览器中,包括 Chrome 和 Firefox,在显示 alert/confirm/prompt 弹窗时,内部的定时器仍旧会继续“嘀嗒”。

所以,在运行上面的代码时,如果在一定时间内没有关掉 alert 弹窗,那么在你关闭弹窗后,下一个 alert 会立即显示。两次 alert 之间的时间间隔将小于 2 秒。

使用嵌套的setTimeout,其效果和setInterval一样,但更加灵活

javascript
let timerId = setTimeout(function tick () {
    console.log("tick");
    timerId = setTimeout(tick,1000)
},1000)

函数绑定


javascript
let user = {
    name: 'dano',
    eat() {
        console.log(this.name + ' is eating!');
    }
}

user.eat();//dano is eating!
setTimeout(user.eat, 1000);//undefined is eating! 发生了this丢失。

一旦==方法被传递到与对象分开的某个地方== —— this 就丢失。

1.使用函数包装器 在要回调的函数外套一层函数,在外部的函数中调用。

javascript
setTimeout(function () { user.eat() }, 1000);//dano is eating!
setTimeout(() => user.eat(), 1000);//dano is eating!

看起来不错,但是我们的代码结构中出现了一个小漏洞。如果在 setTimeout 触发之前(有一秒的延迟!)user 的值改变了怎么办?那么,突然间,它将调用错误的对象!

2.使用bind:

javascript
let boundEat = user.eat.bind(user);
let timerId = setTimeout(boundEat, 1000);//dano is eating!
boundEat()//dano is eating!

func.bind(context) 的结果是一个特殊的类似于函数的“外来对象(exotic object)”,它可以像函数一样被调用,并且透明地(transparently)将调用传递给 func 并设定 this=context。 绑定后可以不使用对象调用。

bind还可以绑定参数:

javascript
function add(a,b) {
    return a + b;
}

let boundAdd = add.bind(null, 2,2);
console.log(boundAdd(1),boundAdd(22,33));

一但参数绑定了,再传入参数就无效了。

透明缓存


有一个 CPU 重负载的函数 slow(x),但它的结果是稳定的。换句话说,对于相同的 x,它总是返回相同的结果。 如果经常调用该函数,我们可能希望将结果缓存(记住)下来,以避免在重新计算上花费额外的时间。 创建一个包装器(wrapper)函数,该函数增加了缓存功能。正如我们将要看到的,这样做有很多好处。

javascript
function add(a) {
    console.log('func is running!!');
    return a;
}

//装饰器函数
function cachingDecorator(func) {
    let cache = new Map();
    return function (x) {
        if (cache.has(x)) {
            return cache.get(x);
        }
        let result = func(x);

        cache.set(x, result);
        return result;
    }
}

add = cachingDecorator(add);

console.log(add(1,2));//func is running!!  1
console.log(add(1,2));//1

cachingDecorator 是一个 装饰器(decorator):一个特殊的函数,它接受另一个函数并改变它的行为。 其思想是,我们可以为任何函数调用 cachingDecorator,它将返回缓存包装器。 从外部代码来看,包装的 slow 函数执行的仍然是与之前相同的操作。它只是在其行为上添加了缓存功能。

总而言之,使用分离的 cachingDecorator 而不是改变 slow 本身的代码有几个好处:

  • cachingDecorator 是可重用的。我们可以将它应用于另一个函数。
  • 缓存逻辑是独立的,它没有增加 slow 本身的复杂性(如果有的话)。
  • 如果需要,我们可以组合多个装饰器(其他装饰器将遵循同样的逻辑)。

在对象的函数中,这样使用会丢失上下文this

javascript
let worker = {
    someMethod() {
        return 1;
    },

    slow(x) {
        // 可怕的 CPU 过载任务
        console.log("Called with " + x);
        return x * this.someMethod();
    }
};
// 和之前例子中的代码相同
function cachingDecorator(func) {
    let cache = new Map();
    return function (x) {
    
        if (cache.has(x)) {
            return cache.get(x);
        }

        let func1 = func.bind(this);
        let result = func1(x);
        cache.set(x, result);

        return result;
    };
}

console.log(worker.slow(1)); // 原始方法有效

worker.slow = cachingDecorator(worker.slow); // 现在对其进行缓存

console.log(worker.slow(2)); //Called with 2   2
console.log(worker.slow(2)); //2

使用内建函数方法func.call(context,...args)来提供上下文和参数,和bind很像,但是仅仅提供一次,并不是绑定。

javascript
// 我们将对 worker.slow 的结果进行缓存
let worker = {
    someMethod() {
        return 1;
    },

    slow(x) {
        // 可怕的 CPU 过载任务
        console.log("Called with " + x);
        return x * this.someMethod();
    }
};

// 和之前例子中的代码相同
function cachingDecorator(func) {

    let cache = new Map();

    return function (x) {

        if (cache.has(x)) {
            return cache.get(x);
        }

        let result = func.call(this, x);
        cache.set(x, result);

        return result;
    };
}

console.log(worker.slow(1)); // 原始方法有效

worker.slow = cachingDecorator(worker.slow); // 现在对其进行缓存

console.log(worker.slow(2)); //Called with 2   2
console.log(worker.slow(2)); //2
javascript
let user = {
    name:'dano'
}

let worker = {
    name:'daozhu'
}

function getName() {
    console.log(this.name);
}

getName();//undefined
getName.call(user);//dano
getName();//undefined
getName.call(worker);//worker

let getName1 = getName.bind(user);
getName1();//dano
getName1();//dano

getName1.call(worker)//dano 绑定后传对象无效

修改包装器:现在可以对任意参数数量的函数进行包装。 将所有参数连同上下文一起传递给另一个函数被称为“呼叫转移(call forwarding)”。

javascript
let worker = {
    slow(min, max) {
        console.log('function is running!');
        return min + max; // scary CPU-hogger is assumed
    }
};

function hash(args) {
    return args[0] + ',' + args[1];
}

function cachingDecorator(func,hash) {
    let cache = new Map();
    return function () {
        let key = hash(arguments);
        if (cache.has(key)) {
            return cache.get(key);
        }

        let result = func.call(this, ...arguments);

        cache.set(key, result);
        return result;
    };
}

// 应该记住相同参数的调用
worker.slow = cachingDecorator(worker.slow,hash);

console.log(worker.slow(2, 4), worker.slow(2, 4));
//function is running! 6 6

1. call 方法

使用方式
call方法立即调用函数,并允许你指定函数内部this的指向,以及以参数列表的形式传入参数。

语法

javascript

func.call(thisArg, arg1, arg2, ...)

场景

  • 当你明确知道函数需要接收的参数数量时。

  • 需要改变函数内部this指向,并立即执行函数。

示例

javascript

function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation); }

const person = { name: 'Alice' };

greet.call(person, 'Hello', '!'); // 输出: Hello, Alice!

2. apply 方法

使用方式
apply方法立即调用函数,并允许你指定函数内部this的指向,以及以数组的形式传入参数。

语法

javascript

func.apply(thisArg, [argsArray])

场景

  • 当函数的参数数量不确定或者参数以数组形式存在时。

  • 需要改变函数内部this指向,并立即执行函数。

示例

javascript

function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation); }

const person = { name: 'Bob' };

greet.apply(person, ['Hi', '!']); // 输出: Hi, Bob!

3. bind 方法

使用方式
bind方法创建一个新的函数,这个新函数的this被指定为bind的第一个参数,其余参数将作为新函数的参数供调用时使用。

语法

javascript

func.bind(thisArg, arg1, arg2, ...)

场景

  • 当你需要创建一个函数,这个函数的this被永久绑定到某个对象,并且可以预先设置一些参数(部分应用函数)。

  • 常用于事件处理函数或者定时器,确保函数执行时this的正确指向。

示例

javascript

function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation); }

const person = { name: 'Charlie' }; const greetPerson = greet.bind(person, 'Hello'); greetPerson('!'); // 输出: Hello, Charlie!

手动实现


实现 call
javascript
Function.prototype.myCall = function(context, ...args) {
    // 如果context为null或undefined,则默认为全局对象(浏览器中为window)
    context = context || window;
    // 将当前函数作为context的一个属性
    context.fn = this;
    // 执行函数,并传入参数
    const result = context.fn(...args);
    // 删除添加的属性
    delete context.fn;
    return result;
};

说是call,其实是将函数转移到了call的参数的对象上面,在利用完对象执行完函数得到结果后,移除函数,再return结果。

实现 apply
javascript
Function.prototype.fakeApply = function (context, args) {
  context = context || globalThis;

  context.fn = this;
  let result;
  if (args) {
    result = context.fn(args);
  } else {
    result = context.fn();
  }
  delete context.fn;
  return result;
};

和call很像,就是参数变了

实现 bind
javascript
Function.prototype.fakeBind = function (obj, ...bargs) {
  return (...rest) => {
    this.call(obj, ...bargs, ...rest);
  };
};

基于call实现

softBind

是一种函数绑定技巧,目的是为函数设置一个“软绑定”的默认 this。当函数被调用时,如果没有显式指定 this(比如直接调用或作为回调),就会使用 softBind 绑定的默认对象;但如果调用时有明确的 this(如 obj.method() 或 call/apply/bind),则优先使用显式的 this。

javascript
Function.prototype.softBind = function(obj, ...bargs) {
  const fn = this;
  function bound(...args) {
    // 如果 this 是 undefined 或全局对象,则用 obj,否则用当前 this
    const context = (!this || this === globalThis) ? obj : this;
    return fn.apply(context, [...bargs, ...args]);
  }
  bound.prototype = Object.create(fn.prototype);
  return bound;
};

使用场景:比如你希望某个回调函数在没有绑定上下文时,自动使用你指定的默认对象,但又不影响显式绑定的情况。

简而言之,softBind 是 bind 的“弱化版”,只在没有显式 this 时才生效。

注意
以上手动实现是简化版,没有考虑一些边界情况,例如如果 context 是原始值,在非严格模式下应该被包装为对象;以及使用 new 操作符调用绑定函数时的行为。但是,它们覆盖了大部分常见的使用场景。

  • callapply都是立即调用函数,区别在于传参方式。
  • bind是返回一个绑定了this和部分参数的新函数,不会立即调用。

apply和call


我们可以使用 func.apply(this, arguments) 代替 func.call(this, ...arguments)call 和 apply 之间唯一的语法区别是,call 期望一个参数列表,而 apply 期望一个包含这些参数的类数组对象。

只有一个关于 args 的细微的差别:

  • Spread 语法 ... 允许将 可迭代对象 args 作为列表传递给 call
  • apply 只接受 类数组 args。 对于即可迭代又是类数组的对象,例如一个真正的数组,我们使用 call 或 apply 均可,但是 apply 可能会更快,因为大多数 JavaScript 引擎在内部对其进行了优化。

通常,用装饰的函数替换一个函数或一个方法是安全的,除了一件小东西。如果原始函数有属性,例如 func.calledCount 或其他,则装饰后的函数将不再提供这些属性。因为这是装饰器。因此,如果有人使用它们,那么就需要小心。

函数参数Params和arguments对象


在 JavaScript 中,arguments函数参数(params) 都与函数接收的输入有关,但它们的本质、用法和特性有显著区别:

1. 定义与本质

  • 函数参数(params)
    是在函数定义时显式声明的变量,用于接收函数调用时传递的参数。
    例如:function fn(a, b) { ... } 中的 ab 就是参数(params)。

  • arguments 对象
    是函数内部的一个类数组对象,自动包含函数调用时传递的所有实际参数,无论函数定义时是否声明了对应参数。
    仅在非箭头函数中存在(箭头函数没有 arguments 对象)。

2. 核心区别

特性函数参数(params)arguments 对象
声明方式函数定义时显式声明(如 a, b函数内部自动生成,无需声明
数据类型普通变量(按声明的类型接收值)类数组对象(有 length,可通过索引访问)
与实参的关系数量固定(由声明决定),多余实参无法直接访问包含所有实参(数量由调用时传递的参数决定)
箭头函数支持支持不支持(箭头函数中无 arguments
修改影响修改参数不会影响 arguments(非严格模式下有例外)修改 arguments 可能影响参数(非严格模式)
用途清晰接收指定参数,便于代码可读性处理不确定数量的参数(如动态传参场景)

3. 示例对比

(1)基本用法

javascript
function fn(a, b) {
  // 函数参数(params)
  console.log(a, b); // 1, 2(接收前两个实参)
  
  // arguments 对象(类数组)
  console.log(arguments); // [1, 2, 3](包含所有实参)
  console.log(arguments.length); // 3(实参数量)
  console.log(arguments[2]); // 3(访问第三个实参)
}

fn(1, 2, 3); // 调用时传递3个实参

2)参数数量不匹配时

  • 实参数量 > 参数数量:参数只能接收前 N 个,剩余实参需通过 arguments 访问。
  • 实参数量 < 参数数量:未被赋值的参数为 undefinedarguments 长度等于实参数量。
javascript
function fn(a, b) {
  console.log(a, b); // 1, undefined(实参不足)
  console.log(arguments.length); // 1(仅1个实参)
}

fn(1); // 只传递1个实参

(3)严格模式与非严格模式的差异

  • 非严格模式arguments 与参数存在“联动”,修改一方会影响另一方。
  • 严格模式"use strict"):arguments 与参数完全独立,修改互不影响。
javascript
// 非严格模式
function nonStrict(a) {
  a = 100;
  console.log(arguments[0]); // 100(arguments 随参数变化)
}
nonStrict(1);

// 严格模式
function strictMode(a) {
  "use strict";
  a = 100;
  console.log(arguments[0]); // 1(arguments 不受参数影响)
}
strictMode(1);

(4)箭头函数中的表现 箭头函数没有 arguments 对象,若需获取所有实参,需使用剩余参数(...rest

javascript
const arrowFn = (...rest) =&gt; {
  // 剩余参数(数组)替代 arguments
  console.log(rest); // [1, 2, 3](接收所有实参)
  // console.log(arguments); // 报错:arguments is not defined
};
arrowFn(1, 2, 3);

4. 现代替代方案:剩余参数(...rest 由于 arguments 是类数组且在箭头函数中不支持,ES6 引入了剩余参数(...rest,它是更优的替代方案:

  • 本质是数组(可直接使用 mapforEach 等方法);
  • 支持箭头函数;
  • 语法更清晰,明确表示“接收剩余所有参数”。
javascript
function fn(a, ...rest) {
  console.log(a); // 1(第一个参数)
  console.log(rest); // [2, 3, 4](剩余参数,数组类型)
  rest.forEach(item =&gt; console.log(item)); // 可直接使用数组方法
}

fn(1, 2, 3, 4);

总结

  • 函数参数(params):显式声明的变量,用于接收指定位置的实参,提升代码可读性。
  • arguments:非箭头函数中自动生成的类数组对象,包含所有实参,适合处理动态数量的参数,但在现代开发中逐渐被剩余参数替代。
  • 推荐优先使用显式参数 + 剩余参数(...rest,避免依赖 arguments(尤其是在严格模式和箭头函数中)。

rest和spread


在 JavaScript 中,很多内建函数都支持传入任意数量的参数。 例如:

  • Math.max(arg1, arg2, ..., argN) —— 返回参数中的最大值。
  • Object.assign(dest, src1, ..., srcN) —— 依次将属性从 src1..N 复制到 dest
  • ……等。

在 JavaScript 中,无论函数是如何定义的,你都可以在调用它时传入任意数量的参数。 我们可以在函数定义中声明一个数组来收集参数。语法是这样的:...变量名,这将会声明一个数组并指定其名称,其中存有剩余的参数。这三个点的语义就是“收集剩余的参数并存进指定数组中”。Rest 参数必须放到参数列表的末尾

有一个名为 arguments 的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。尽管 arguments 是一个==类数组==,也是可迭代对象,但它终究不是数组。它不支持数组方法,因此我们不能调用 arguments.map(...) 等方法。如果我们在箭头函数中访问 arguments,访问到的 arguments 并不属于箭头函数,而是属于箭头函数外部的“普通”函数。

Spread 语法它看起来和 rest 参数很像,也使用 ...,但是二者的用途完全相反。 当在函数调用中使用 ...arr 时,它会把可迭代对象 arr “展开”到参数列表中。

javascript
let arr = [1, 3, 4, 5, 6];
console.log(Math.max(1, ...arr, 9));//9
javascript
let { log } = console;
alert = log;
let str = "Hello";

alert([...str]); // [ 'H', 'e', 'l', 'l', 'o' ]
alert(Array.from(str));//[ 'H', 'e', 'l', 'l', 'o' ]

Array.from(obj) 和 [...obj] 存在一个细微的差别:

  • Array.from 适用于类数组对象也适用于可迭代对象。
  • Spread 语法只适用于可迭代对象。

使用... 来进行拷贝数组和对象:

javascript
let { log } = console;
alert = log;

let arr = [1, 2, 3];

let arrCopy = [...arr]; // 将数组 spread 到参数列表中
// 然后将结果放到一个新数组

// 两个数组中的内容相同吗?
alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true

// 两个数组相等吗?
alert(arr === arrCopy); // false(它们的引用是不同的)

// 修改我们初始的数组不会修改副本:
arr.push(4);
alert(arr); // 1, 2, 3, 4
alert(arrCopy); // 1, 2, 3

let obj = { a: 1, b: 2, c: 3 };

let objCopy = { ...obj }; // 将对象 spread 到参数列表中
// 然后将结果返回到一个新对象

// 两个对象中的内容相同吗?
alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true

// 两个对象相等吗?
alert(obj === objCopy); // false (not same reference)

// 修改我们初始的对象不会修改副本:
obj.d = 4;
alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4}
alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3}

这种方式比使用 let arrCopy = Object.assign([], arr) 复制数组,或使用 let objCopy = Object.assign({}, obj) 复制对象来说更为简便。因此,只要情况允许,我们倾向于使用它。

当我们在代码中看到 "..." 时,它要么是 rest 参数,要么是 spread 语法。 有一个简单的方法可以区分它们:

  • 若 ... 出现在函数参数列表的最后,那么它就是 rest 参数,它会把参数列表中剩余的参数收集到一个数组中。
  • 若 ... 出现在函数调用或类似的表达式中,那它就是 spread 语法,它会把一个数组展开为列表。

使用场景:

  • Rest 参数用于创建可接受任意数量参数的函数。
  • Spread 语法用于将数组传递给通常需要含有许多参数的函数。

我们可以使用这两种语法轻松地互相转换列表与参数数组。 旧式的 arguments(类数组且可迭代的对象)也依然能够帮助我们获取函数调用中的所有参数。

全局对象


全局对象提供可在任何地方使用的变量和函数。默认情况下,这些全局变量内建于语言或环境中。

在浏览器中,它的名字是 “window”,对 Node.js 而言,它的名字是 “global”,其它环境可能用的是别的名字。

最近,globalThis 被作为全局对象的标准名称加入到了 JavaScript 中,所有环境都应该支持该名称。所有主流浏览器都支持它。

全局对象的所有属性都可以被直接访问 如果一个值非常重要,以至于你想使它在全局范围内可用,那么可以直接将其作为属性写入

javascript
globalThis.userData = {
    name: 'dano',
    age:21,
}

console.log(userData.name);//dano
console.log(globalThis.userData.age);//21

一般不建议使用全局变量。全局变量应尽可能的少。与使用外部变量或全局变量相比,函数获取“输入”变量并产生特定“输出”的代码设计更加清晰,不易出错且更易于测试。

polyfills(垫片): 如果浏览器版本过旧,存在代码的缺失,可以自行添加代码

函数对象

我已经知道在js中函数也是一个值。函数的值的类型是对象。

javascript
function f(){
    console.log("dano");
}

console.log(typeof f);//function

函数的名字可以通过name属性来访问

javascript
function f(){
    console.log("dano");
}

let ff = function () {
    console.log("ff");
}

console.log(f.name);
console.log(ff.name);

如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个。

还有另一个内建属性 “length”,它返回函数入参的个数,rest 参数不参与计数.

也可以添加我们自己的属性。 这里我们添加了 counter 属性,用来跟踪总的调用次数

javascript
function sayHi() {
    console.log("Hi");

    // 计算调用次数
    sayHi.counter++;
}
sayHi.counter = 0; // 初始值

sayHi(); // Hi
sayHi(); // Hi

console.log(`Called ${sayHi.counter} times`); // Called 2 times

命名函数表达式(NFE,Named Function Expression),指带有名字的函数表达式的术语。 关于名字 func 有两个特殊的地方,这就是添加它的原因:

  1. 它允许函数在内部引用自己。
  2. 它在函数外是不可见的。
javascript
let sayHi = function func(who) {
    if (who) {
        console.log(`Hello, ${who}`);
    } else {
        func("Guest"); // 使用 func 再次调用函数自身
    }
};

sayHi(); // Hello, Guest

// 但这不工作:
func(); // Error, func is not defined(在函数外不可见)

我们为什么使用 func 呢?为什么不直接使用 sayHi 进行嵌套调用? 问题在于 sayHi 的值可能会被函数外部的代码改变。如果该函数被赋值给另外一个变量(译注:也就是原变量被修改),那么函数就会开始报错