前言
大家好,我是NobodyDJ。一名默默无闻的前端学徒。
在学习节流与防抖的过程中,其中使用到了this绑定的问题,call,apply用法相似,但是无法辨别其中的具体差异,何时使用bind,何时使用call和apply,一直困扰着我,于是我想写下这篇文章,来帮助自己梳理这块的知识点,方便自己以后进行巩固。
this的指向
如下展示了一个节流的例子:
<button id="debounce">点我防抖!</button>
// 防抖:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。
window.onload = function () {
// 1、获取这个按钮,并绑定事件
var myDebounce = document.getElementById("debounce");
myDebounce.addEventListener("click", debounce(sayDebounce, 1000));
}
// 2、防抖功能函数,接受传参
function debounce(fn, delay) {
let timer;// 利用闭包的原理,如果不把Timer放到外面则clearTimeout这个函数没有作用
return function () {
clearTimeout(timer);
// 注意这里箭头函数this指向问题,定义是,它是指向了window,所以需要改变它的指向
let context = this;
timer = setTimeout(() => {
fn.apply(context, arguments); //将上下文始终绑定执行该函数的上下文,这里指的是#debounce这个DOM元素
// fn();
}, delay);
}
}
// 3、需要进行防抖的事件处理
function sayDebounce() {
// ... 有些需要防抖的工作,在这里执行
let str = "防抖成功!"
console.log(this);
console.log(str);
}
可以看到第16行代码使用了apply方法改变了this指向,如果不改变this指向,setTimeout中的this指向的是window(因为箭头函数的this指向指向的是定义箭头函数的上下文),这与我们需要防抖的button按钮DOM元素不符,所以我们使用了context保存了执行匿名函数的上下文,将fn指向了context即按钮DOM元素,这样就正确的调用了该函数。
由以上案例,我们可以看出,apply()函数可以改变函数的this指向,除了apply,还有call()和bind()这两个函数,接下来,我们来对这三个函数展开详细的介绍。
call,apply,bind的基本介绍
以下是call, apply, bind的语法与接受的参数介绍,详细介绍请查看MDN
1. call&apply
(1) apply: apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。
(2) call: call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
语法:
(1)apply
function.apply(thisArg);
function.apply(thisArg, argsArray);
(2) call
function.call(thisArg, arg1, arg2, ...);
相同点:
(1)两个方法同时接受两个参数:第一个参数是thisArg,即指向函数的this指向,在非严格模式下:thisArg指定为null, undefined, fun中的this默认指向了window;如果是严格模式下,this为赋予undeined;
(2)两个函数的调用对象,必须是一个函数。
(3)函数调用这两个方法时,函数(call和bind的调用者)会立即执行该函数
不同点:
(1)call与apply的接受的第二个参数不同,call的第二个参数接受的是一个参数列表,apply的第二参数是一个数组。
var a ={
name : "Nobody",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.apply(a,[1,2]) // 3
var a ={
name : "Nobody",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.call(a,1,2) // 3
2. bind
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法:
function.bind(thisArg, arg1, arg2, ...)
bind参数语法与call的参数用法相同,这里不再赘述。
与call&bind的不同点
这里很重要,bind与call&apply的最大不同点就是,函数调用call与apply后会该函数会立即执行,而函数调用bind方法后,知识改变了该函数的上下文,不会执行该函数。
3. 核心理念:
由此,可见这三个方法的核心理念就是借用方法。改变其他已经实现的函数的this执行为自己所用,减少重复代码,节省内存。
call,apply,bind的应用场景:
1.用于判断数据类型:
function isType(data, type) {
const typeObj = {
'[object String]': 'string',
'[object Number]': 'number',
'[object Boolean]': 'boolean',
'[object Null]': 'null',
'[object Undefined]': 'undefined',
'[object Object]': 'object',
'[object Array]': 'array',
'[object Function]': 'function',
'[object Date]': 'date', // Object.prototype.toString.call(new Date())
'[object RegExp]': 'regExp',
'[object Map]': 'map',
'[object Set]': 'set',
}
let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()获取数据类型
let typeName = typeObj[name] || 'unknown type' // 匹配数据类型
return typeName === type // 判断该数据类型是否为传入的类型
}
console.log(isType({},'object'));// true
console.loh(isType(new Date(), 'object')// false
2.类数组借用数组的方法:
因为类数组不是真正的数组,它没有Array.prototype.push()方法,因此,可以使用call或者apply方法来借用数组对象下的push函数。
let arrayLike = {
0: '我',
1: '是',
length: 2
}
Array.prototype.push.call(arrayLike,'Nobody','DJ');
console.log(arrayLike) // {0: '我', 1: '是', 2: 'Nobody', 3: 'DJ', length: 4}
3.apply获取数组最大值最小值
使用applly方法也可以节省进一步展开数组,比如使用Math.max,Math,min来获取数组的最大值/最小值
const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6
4.继承
// 父类
function supFather(name){
this.name = name;
this.colors = ['red','blue','green'];
}
supFather.prototype.sayName = function(){
console.log('My name is',this.name);
}
// 子类
function sub(name,age){
supFather.call(this,name);
this.age = age;
}
// 重写子类的prototype, 修正constructor的指向
function inheritPrototype(sonFn,fatherFn){
sonFn.prototype = Object.create(fatherFn.prototype);
sonFn.prototype.constructor = sonFn;
}
inheritPrototype(sub,supFather);
sub.prototype.sayAge = function (){
console.log(`${this.name}'s age is`,this.age);
}
const instance1 = new sub("Nobody", 23);
const instance2 = new sub("DJ W", 18);
console.log(instance1);
console.log(instance2);
instance1.sayName();
instance1.sayAge();
5.保存函数参数
for (var i = 1; i <= 5; i++) {
setTimeout(function test() {
console.log(i) // 依次输出:6 6 6 6 6
}, i * 1000);
}
都是输出6,因为这里的i变量是全局变量,每次循环遍历后,i都依次累加,最后将i的累加结果依次输出,输出的个数为循环的个数。
解决这个问题,可以采用两种方法一种是使用bind绑定执行上下文,第二种将var声明变量改变为let,存在于每个上下文中。
方法一:
for (var i = 1; i <= 5; i++) {
// 缓存参数
setTimeout(function (i) {
console.log(i) // 依次输出:1 2 3 4 5
}.bind(null, i), i * 1000);
}
方法二:
for (let i = 1; i <= 5; i++) {
setTimeout(function test() {
console.log(i) // 依次输出:1 2 3 4 5 6
}, i * 1000);
}
apply、call、bind的实现原理
1.实现call
关键点:
(1)要用一个特殊的属性来保存this即需要执行的函数,执行完该属性应当立马删除。
(2)如果出现绑定的对象为null或者undefined应该默认绑定到window上面。
(3)call方法第二个参数接受的是参数列表。
(4)注意执行call方法时,是立马执行该函数。
实现代码:
Function.prototype.myCall = function(obj, ...args){
let context;
// 默认绑定上下文的操作
if(obj === null || obj === undefined){
context = Window;
}else{
context = new Object(obj);
}
let specialAttribute = Symbol('Nobody');
context[specialAttribute] = this;
let result = context[specialAttribute](...args);
delete context[specialAttribute];
return result;
}
2.实现apply
关键点:
(1)apply与call唯一的不同点在于,apply的第二个参数接受的是数组
(2)需要判断第二个参数是否是数组,否则不符和条件
实现代码:
// 判断参数是否为类数组对象
function isArrayLike(obj){
if(
obj && // o不是null、undefined等
typeof obj === 'object' && // o是对象
isFinite(obj.length) && // obj.length是有限数值
obj.length >= 0 && // obj.length为非负值
obj.length === Math.floor(obj.length) && // obj.length是整数
obj.length < 4294967296){
return true;
}else{
return false;
}
}
Function.prototype.myApply = function(obj,arr){
let context;
let result;
if(obj === null || obj === undefined){
context = Window;
}else{
context = new Object(obj);
}
let specialAttribute = Symbol('Nobody');
context[specialAttribute] = this;
if(arr){
if(!Array.isArray(arr)&&!isArrayLike(arr)){
throw Error('第二个参数必须是一个数组');
}else{
// 别忘记转换为数组
let args = Array.from(arr);
result = context[specialAttribute](...args);
}
}else{
result = context[specialAttribute]();
}
delete context[specialAttribute];
return result;
}
3.实现bind
关键点:
- 函数使用bind方法时,不会立刻执行这个函数。
- 注意调用bind函数和自身函数调用时,两组参数需要合并起来。
- 使用闭包的绑定调用bind方法的函数。
- 使用bind之后,在使用new方法创建实例(有点复杂,涉及到new中的this绑定问题,和原型链上继承的优化)。
实现代码:
Function.prototype.myBind = function(obj,...args1){
if(typeof this !== 'function'){
console.log('调用对象必须是个函数')
}
let that = this,
o = function() {};
fn = function(...args2){
let arr = [...args1,...args2];
// 注意:这里使用instanceof来判断对返回的函数,是否使用了new
// 这里new中的this执行有点复杂,暂时不考虑
if(this instanceof o){
that.apply(this,arr);
}else{
that.apply(obj,arr);
}
}
// 原型链上的继承,利用了o这个空函数做过度,属于优化的部分
// fn.prototype = that.prototype
o.prototype = that.prototype;
fn.prototype = new o;
return fn;
}
总结
call和apply方法的实现其实还好,但是bind方法的实现有点复杂,需要不断巩固加深认识。
参考资料
1.js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]
3.原生JavaScript实现call、apply和bind - Web前端工程师面试题讲解
4.《你不知道的JavaScript》上卷