new function
它实际上是通过运行时通过参数传递过来的字符串创建的。 new Function 允许我们将任意字符串变为函数。例如,我们可以从服务器接收一个新的函数并执行它
setTimeOut setaInterval
目前有两种方式可以实现:
setTimeout允许我们将函数推迟到一段时间间隔之后再执行。setInterval允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。 这两个方法并不在 JavaScript 的规范中。但是大多数运行环境都有内建的调度程序,并且提供了这些方法。目前来讲,所有浏览器以及 Node.js 都支持这两个方法。参数说明:
func|code 想要执行的函数或代码字符串。 一般传入的都是函数。由于某些历史原因,支持传入代码字符串,但是不建议这样做。
delay 执行前的延时,以毫秒为单位(1000 毫秒 = 1 秒),默认值是 0;
arg1,arg2… 要传入被执行函数(或代码字符串)的参数列表(IE9 以下不支持)
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,我们可以使用它来取消执行。
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一样,但更加灵活
let timerId = setTimeout(function tick () {
console.log("tick");
timerId = setTimeout(tick,1000)
},1000)函数绑定
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.使用函数包装器 在要回调的函数外套一层函数,在外部的函数中调用。
setTimeout(function () { user.eat() }, 1000);//dano is eating!
setTimeout(() => user.eat(), 1000);//dano is eating!看起来不错,但是我们的代码结构中出现了一个小漏洞。如果在 setTimeout 触发之前(有一秒的延迟!)user 的值改变了怎么办?那么,突然间,它将调用错误的对象!
2.使用bind:
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还可以绑定参数:
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)函数,该函数增加了缓存功能。正如我们将要看到的,这样做有很多好处。
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));//1cachingDecorator 是一个 装饰器(decorator):一个特殊的函数,它接受另一个函数并改变它的行为。 其思想是,我们可以为任何函数调用 cachingDecorator,它将返回缓存包装器。 从外部代码来看,包装的 slow 函数执行的仍然是与之前相同的操作。它只是在其行为上添加了缓存功能。
总而言之,使用分离的 cachingDecorator 而不是改变 slow 本身的代码有几个好处:
cachingDecorator是可重用的。我们可以将它应用于另一个函数。- 缓存逻辑是独立的,它没有增加
slow本身的复杂性(如果有的话)。 - 如果需要,我们可以组合多个装饰器(其他装饰器将遵循同样的逻辑)。
在对象的函数中,这样使用会丢失上下文this
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很像,但是仅仅提供一次,并不是绑定。
// 我们将对 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)); //2let 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)”。
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 61. 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
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
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
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。
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 操作符调用绑定函数时的行为。但是,它们覆盖了大部分常见的使用场景。
call和apply都是立即调用函数,区别在于传参方式。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) { ... }中的a和b就是参数(params)。arguments对象
是函数内部的一个类数组对象,自动包含函数调用时传递的所有实际参数,无论函数定义时是否声明了对应参数。
仅在非箭头函数中存在(箭头函数没有arguments对象)。
2. 核心区别
| 特性 | 函数参数(params) | arguments 对象 |
|---|---|---|
| 声明方式 | 函数定义时显式声明(如 a, b) | 函数内部自动生成,无需声明 |
| 数据类型 | 普通变量(按声明的类型接收值) | 类数组对象(有 length,可通过索引访问) |
| 与实参的关系 | 数量固定(由声明决定),多余实参无法直接访问 | 包含所有实参(数量由调用时传递的参数决定) |
| 箭头函数支持 | 支持 | 不支持(箭头函数中无 arguments) |
| 修改影响 | 修改参数不会影响 arguments(非严格模式下有例外) | 修改 arguments 可能影响参数(非严格模式) |
| 用途 | 清晰接收指定参数,便于代码可读性 | 处理不确定数量的参数(如动态传参场景) |
3. 示例对比
(1)基本用法
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访问。 - 实参数量 < 参数数量:未被赋值的参数为
undefined,arguments长度等于实参数量。
function fn(a, b) {
console.log(a, b); // 1, undefined(实参不足)
console.log(arguments.length); // 1(仅1个实参)
}
fn(1); // 只传递1个实参(3)严格模式与非严格模式的差异
- 非严格模式:
arguments与参数存在“联动”,修改一方会影响另一方。 - 严格模式(
"use strict"):arguments与参数完全独立,修改互不影响。
// 非严格模式
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):
const arrowFn = (...rest) => {
// 剩余参数(数组)替代 arguments
console.log(rest); // [1, 2, 3](接收所有实参)
// console.log(arguments); // 报错:arguments is not defined
};
arrowFn(1, 2, 3);4. 现代替代方案:剩余参数(...rest) 由于 arguments 是类数组且在箭头函数中不支持,ES6 引入了剩余参数(...rest),它是更优的替代方案:
- 本质是数组(可直接使用
map、forEach等方法); - 支持箭头函数;
- 语法更清晰,明确表示“接收剩余所有参数”。
function fn(a, ...rest) {
console.log(a); // 1(第一个参数)
console.log(rest); // [2, 3, 4](剩余参数,数组类型)
rest.forEach(item => 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 “展开”到参数列表中。
let arr = [1, 3, 4, 5, 6];
console.log(Math.max(1, ...arr, 9));//9let { 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 语法只适用于可迭代对象。
使用... 来进行拷贝数组和对象:
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 中,所有环境都应该支持该名称。所有主流浏览器都支持它。
全局对象的所有属性都可以被直接访问 如果一个值非常重要,以至于你想使它在全局范围内可用,那么可以直接将其作为属性写入
globalThis.userData = {
name: 'dano',
age:21,
}
console.log(userData.name);//dano
console.log(globalThis.userData.age);//21一般不建议使用全局变量。全局变量应尽可能的少。与使用外部变量或全局变量相比,函数获取“输入”变量并产生特定“输出”的代码设计更加清晰,不易出错且更易于测试。
polyfills(垫片): 如果浏览器版本过旧,存在代码的缺失,可以自行添加代码
函数对象
我已经知道在js中函数也是一个值。函数的值的类型是对象。
function f(){
console.log("dano");
}
console.log(typeof f);//function函数的名字可以通过name属性来访问
function f(){
console.log("dano");
}
let ff = function () {
console.log("ff");
}
console.log(f.name);
console.log(ff.name);如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个。
还有另一个内建属性 “length”,它返回函数入参的个数,rest 参数不参与计数.
也可以添加我们自己的属性。 这里我们添加了 counter 属性,用来跟踪总的调用次数
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 有两个特殊的地方,这就是添加它的原因:
- 它允许函数在内部引用自己。
- 它在函数外是不可见的。
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 的值可能会被函数外部的代码改变。如果该函数被赋值给另外一个变量(译注:也就是原变量被修改),那么函数就会开始报错