JS笔记-fn.apply、this、防抖函数

到底为什么写func.apply(this, arguments)掘金

防抖函数是指,在一定时间间隔内,如果没有重复触发函数,才会执行函数体的代码(防止在很短的时间里多次触发的函数),例如网络请求,input输入,浏览器resize监听等。

以下是防抖函数的一个基本实现,通过闭包将主函数体放在timer计时器里,如果二次调用了func,就会通过clearTimeout取消计时器的后续操作,达到防止函数重复触发的效果。

1
2
3
4
5
6
7
8
9
function debounce(func, delay) {
let timer = null;
return function () {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}

其中这个func.apply(this, arguments);看上去是触发函数的作用,但thisarguments两个参数是什么作用呢?

在js里,this指向当前作用域,只有在运行时才会明确其指向的对象。通过apply的使用,可以修改当前函数的作用域对象:

1
2
3
4
5
6
7
8
9
10
11
var name = 'a', age = 0, type = 'A'
const obj = {
name:'b',
age:1,
type:'B'
}
function fn () {
console.log( this.name, this.age, this.type );
}
fn() // a 0 A
fn.apply(obj) // b 1 B

如上述代码所示,fn通过apply成功修改了this指向,输出了obj的内容。

插个题外话,以上代码只能在浏览器中运行,如果放在node环境里直接调用fn()只会输出三个undefined。那这个this到底指向哪里了呢?

在浏览器或 Node.js 的全局作用域中,this 都默认指向全局对象。浏览器中的对象是 window,而Node.js 中是 global。与letconst不同,var 声明的变量会成为 window 的属性,因此 this.name 可以访问到值。在浏览器里通过var声明的全局变量是可以通过window直接拿到的:

1
2
3
4
5
6
var name1 = "name1";
var name2 = "name2";
function test() {
console.log(this.name1, this.name2); // 通过 `window.name1` 访问
}
test.apply(window); // 输出 "name1 name2"

但在node环境里,var 变量属于模块作用域,不会挂载到 global 对象,因此全局环境下 this.name1 始终为 undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
var name1 = "name1";
var name2 = "name2";

function test() {
console.log(this.name1, this.name2); // 输出 "undefined undefined"
}

test.apply(global); // 仍然无法访问

// 手动显式挂载到 global 对象
global.name1 = "name1";
global.name2 = "name2";
test.apply(global); // 输出 "name1 name2"

明白了this的作用,就很明确func.apply(this, arguments)的使用了。

除了apply()方法外,call()方法也与其类似,但在传参上略有不同。call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。

还有一个bind()方法,不同的是此方法会创建一个新的函数,需要重新调用。(bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var name = 'a', age = 0, type = 'A'
var x = {
name: 'XMAN',
age: 24,
type: this.type
}
function todo (para1, para2) {
console.log(this.name,'年龄是:', this.age, '血型是:', this.type, '爱好:', para1, para2)
}
todo('篮球', '足球') // a 年龄是: 0 血型是: A 爱好: 篮球 足球
todo.call(x, '篮球', '足球') // XMAN 年龄是: 24 血型是: A 爱好: 篮球 足球
todo.call(x, ['篮球', '足球']) // XMAN 年龄是: 24 血型是: A 爱好: ["篮球", "足球"] undefined
todo.apply(x, '篮球', '足球') // 报错,apply不能这样的,其第二个参数得是一个数组或者类数组对象
todo.apply(x, ['篮球', '足球']) // XMAN 年龄是: 24 血型是: A 爱好: 篮球 足球
todo.bind(x, '篮球', '足球')() // XMAN 年龄是: 24 血型是: A 爱好: 篮球 足球

理解了以上知识后,我们再回到防抖函数,观察一下是怎么调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const obj = {
name1: "objname1",
name2: "objname2",
}

function test(){
console.log(this.name1,this.name2)
}

const debounce = (fn,delay) => {
let timer = null;
return function(...args){
if(timer){
clearTimeout(timer)
}
timer = setTimeout(()=>{
fn.apply(this,args)
},delay)
}
}

const testDebounce = debounce(test,2000) // 调用防抖函数并接收新的函数
testDebounce.call(obj)
testDebounce.call(obj)
testDebounce.call(obj) // 三次调用只有最后一次成功

我们在调用时传入了一个作用域,这个作用域是如何传入fn函数里的呢?原理就在这句回调函数里:

1
2
3
timer = setTimeout(()=>{
fn.apply(this,args)
},delay)

setTimeout里的回调函数会自动继承父级的作用域,也就是testDebounce的作用域。我们通过call()修改了作用域,也就成功实现了作用域传递。