前言
使用自己写的call()、apply()
函数实现防抖:在input输入框中输入内容后,隔一段时间在进行操作(比如在控制台打印出input的内容),而不是每次输入都触发操作
<body>
<input type="text" name="" id="">
<script>
const input = document.querySelector('input');
// 实现call()函数
Function.prototype.myCall = function(){
}
function debounce(fn, delay){
var timer = null;
return function(){
if(timer) clearTimeout(timer);
var _this = this;
var _arguments = arguments;
timer = setTimeout(() => {
fn.myCall(_this, _arguments);
}, delay);
}
}
function searchChange(){
console.log(this.value);
}
input.oninput = debounce(searchChange, 2000);
</script>
</body>
分析(以call()为例):
- 使用
call()
的时候,call需要实现:1. 改变函数fn的this指向,2. 将bendoun()的参数传递过来的参数传递给绑定call的函数fn
一、call、apply概述
call()
call()
方法接收两个参数- 第一个参数代表调用call的函数体内的 this 指向
- 与apply不同,call的第二个参数是依次传入函数的,不是带有下标的集合
function sum(num1, num2){
return num1 + num2;
}
function callSum2(num1, num2){
return sum.call(this, num1, num2);
}
console.log(callSum2(10, 10)); // 20
apply()
-
apply()
方法接收两个参数- 第一个参数代表调用apply的函数体内的 this 指向
- 第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数
function sum(num1, num2){ return num1 + num2; } function callSum2(num1, num2){ return sum.call(this, [num1, num2]); // return sum.call(this, arguments); } console.log(callSum2(10, 10)); // 20
apply和call的区别
- 它们的作用一模一样,区别仅在于传入参数的形式的不同
- 它们的第一个参数是一样的,关键在于第二个参数
- apply 接受两个参数,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
- call 传入的参数数量不固定,从第二个参数开始往后,每个参数被依次传入函数。
关于apply和call改变函数this指向的问题(重点)
-
apply和call的强大之处在于控制函数调用上下文即函数体内this值的能力,可以将任意对象设置为任意函数的作用域
-
通过下面的例子来理解(以call为例,call和apply都是一样的):
window.color = 'red'; let obj = { color: 'blue' }; function setColor(){ console.log(this.color); } setColor(); // red setColor.call(this); // red setColor.call(window); // red setColor.call(obj); // blue
-
在全局作用域下调用
setColor()
时,setColor()
的this是指向window
的,所以调用setColor();
得到的颜色为red
-
当使用call在全局作用域下调用
setColor.call(this);
或者setColor.call(window);
,得到的也是red
,这里就涉及到:-
使用
call/apply
来改变函数的this值时,如果传入的第一个参数不是一个对象(比如上面的obj),而是this
/window
,参数为this
的时候,this的指向跟当前调用函数的上下文有关,因为setColor
是在全局环境中调用的,所以this值就是window
。如果是一个对象,那么他就指向这个对象 -
如果
setColor
是在另外一个函数b体内被调用了,那么this的指向就跟函数b的调用环境有关,例如:- 在全局环境下调用fn,
setColor
的this就是window
function setColor(color){ console.log(this); // window console.log(color); // red } // 1. fn是一个全局函数时 function fn(color){ return setColor.call(this, color); } // 在全局环境下调用fn fn('red');
- obj对象调用fn,
setColor
的this就指向obj对象
function setColor(color){ console.log(this); // obj console.log(color); // blue } // 2. fn是obj对象的一个属性时 let obj = { color: blue, fn: function(){ return setColor.call(this, this.color); } } // obj调用fn obj.fn(); // blue
- 在全局环境下调用fn,
-
-
使用call将
setColor
函数的this切换为obj对象,所以颜色为blue
。
-
二、bind
bind语法和call一模一样,区别在于立即执行还是等待执行,bind不兼容IE6~8
fn.call(obj, 1, 2); // 改变fn中的this,并且把fn立即执行
fn.bind(obj, 1, 2); // 改变fn中的this,fn并不执行
bind会把fn中的this预处理为obj,此时fn没有执行,当点击的时候才会把fn执行
document.onclick = fn.bind(obj);
-
使用bind可以在
setColor
上调用bind传入对象obj,创建一个新的函数fn, 但是call和apply都不行(会报错)function setColor(color){ console.log(this); // obj console.log(color); // blue } let obj = { color: 'blue' } let a = setColor.bind(obj, obj.color); a();
let a = setColor.call(obj, obj.color);
三、实现call、apply、bind
实现call
- 第一种写法
Function.prototype.myCall = function(){
if (typeof this !== "function") {
console.error("type error");
}
const args = [...arguments];
// 得到的t是myCall所在上下文环境的this
const t = args.shift();
// 这里this的指向是:谁调用了myCall,this就指向谁
// 按照测试,是sum()调用了mycall(),所以this就指向sum()
const _this = this;
console.log(_this); // f sum(num1, num2){}
// 1. 让t改为指向sum(), 即调用了myCall的函数
t.fn = _this;
// 2. 经过上面一步,t.fn就相当于sum()
// 2. 现在要实现把参数通过myCall传给sum
// 2. 直接把删除掉第一个参数args传给t.fn
const res = t.fn(...args);
delete t.fn;
return res;
}
-
第二种写法
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 处理传入的参数,截取第一个参数后的所有参数。
- 将函数作为上下文对象的一个属性。
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性。
- 返回结果。
Function.prototype.myCall = function(context) { // context代表的就是传给myCall()的第一个参数 console.log(context); // this指向的对象 // 判断调用对象 if (typeof this !== "function") { console.error("type error"); } // 获取参数 // slice()返回数组中被选中的元素。 let args = [...arguments].slice(1), result = null; //console.log(...args); // 20,30 // 判断 context 是否传入,如果未传入则设置为 window context = context || window; // 将调用函数设为对象的方法 context.fn = this; // 调用函数 result = context.fn(...args); // 在需要返回值的情况下,如果调用myCall()的函数有返回值,那么context.fn()就是那个返回值 console.log(context.fn()); // hello console.log(result); // hello // 将属性删除 delete context.fn; return result; }; // 测试 function fn(a,b){ console.log('a: ',a); console.log('b: ',b); console.log('this: ',this); return 'hello'; } const res = fn.myCall(this, 20, 30);
实现apply
-
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
-
判断传入上下文对象是否存在,如果不存在,则设置为 window 。
-
将函数作为上下文对象的一个属性。
-
判断参数值是否传入
-
使用上下文对象来调用这个方法,并保存返回结果。
-
删除刚才新增的属性
-
返回结果
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
// 将函数设为对象的方法
context.fn = this;
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 将属性删除
delete context.fn;
return result;
};
实现bind
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 保存当前函数的引用,获取其余传入参数值。
- 创建一个函数返回
- 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
var args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};