JS笔记——call,apply,bind用法与原理

101 阅读6分钟

前言

大家好,我是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

关键点:

  1. 函数使用bind方法时,不会立刻执行这个函数。
  2. 注意调用bind函数和自身函数调用时,两组参数需要合并起来。
  3. 使用闭包的绑定调用bind方法的函数。
  4. 使用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?[不看后悔系列]

2.this、apply、call、bind

3.原生JavaScript实现call、apply和bind - Web前端工程师面试题讲解

4.《你不知道的JavaScript》上卷