理解 JavaScript 中的call、apply 和 bind

57 阅读7分钟

面试中可能会问到js中关于call、apply、bind的问题,例如

  • 怎么利用call、apply来求一个数组中最大或者最小值
  • 如何利用call、apply来做继承
  • apply、call、bind的区别和主要应用场景
  • .....

首先,要明白这三个函数的存在意义?答案是改变函数执行时的上下文,再具体一点就是改变函数运行时的this指向。

call/apply/bind的核心理念:借用方法

借用网上的例子:

生活中:平时没时间做饭的我,周末想给孩子炖个乌鸡汤尝尝。但是没有适合的锅,而我又不想出去买。所以就问邻居借了一个锅来用,这样既达到了目的,又节省了开支,一举两得。

程序中:A对象有个方法,B对象因为某种原因也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?当然是借用 A 对象的方法啦,既达到了目的,又节省了内存。

这就是call/apply/bind的核心理念:借助已实现的方法,改变方法中数据的this指向,减少重复代码,节省内存。

1、基本语法

1.1 call():Function.call(thisArg, param1, param2, ...)
  • 调用 call 的对象,必须是个函数 Function。
  • call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。如果不传,则默认为全局对象 window。
  • 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。
1.2 apply():Function.apply(thisArg, [param1,param2,...])
  • 它的调用者必须是函数 Function,并且只接收两个参数,第一个参数的规则与 call 一致。
  • 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。
1.3 bind():Function.bind(thisArg, param1, param2, ...)
  • bind 方法 与 apply 和 call 比较类似,也能改变函数体内的 this 指向。
  • 不同的是,bind 方法的返回值是函数,并且需要稍后调用,才会执行,而 apply 和 call 则是立即调用。

2、call、apply、bind基本使用介绍

let obj = {name: 'tony'};
  
  function Child(name){
    this.name = name;
  }
  Child.prototype = {
    constructor: Child,
    showName: function(){
      console.log(this.name);
    }
  }
  var child = new Child('thomas');
  child.showName(); // thomas
  
  //  call,apply,bind使用
  child.showName.call(obj);// tony
  child.showName.apply(obj);// tony
  let bind = child.showName.bind(obj); // 返回一个函数
  bind(); // tony

我们拿别人的showName方法,并动态改变其上下文帮自己输出了信息,说到底就是实现了复用

3、应用场景

3.1 判断数据类型

借用Object.prototype.toString用来判断几乎所有类型的数据,它返回的是一个表示对象内部 [[Class]] 属性的字符串,这个属性是对象在JavaScript内部使用的类型标识

// 定义一个函数isType,用于判断给定的数据data是否属于指定的类型type  
function isType(data, type) {  
    // 定义一个对象typeObj,包含JavaScript中各种数据类型对应的字符串表示及其简化名称  
    const typeObj = {  
        '[object String]': 'string',  // 字符串类型  
        '[object Number]': 'number',  // 数字类型  
        '[object Boolean]': 'boolean', // 布尔类型  
        '[object Null]': 'null',      // null类型  
        '[object Undefined]': 'undefined', // undefined类型  
        '[object Object]': 'object',  // 普通对象类型  
        '[object Array]': 'array',    // 数组类型  
        '[object Function]': 'function', // 函数类型  
        '[object Date]': 'date',      // 日期类型  
        '[object RegExp]': 'regExp',  // 正则表达式类型  
        '[object Map]': 'map',        // Map类型  
        '[object Set]': 'set',        // Set类型  
        '[object HTMLDivElement]': 'dom', // DOM元素类型(以HTMLDivElement为例)  
        '[object WeakMap]': 'weakMap', // WeakMap类型  
        '[object Window]': 'window',  // Window对象类型  
        '[object Error]': 'error',    // Error对象类型  
        '[object Arguments]': 'arguments', // arguments对象类型(注意:在ES6及更高版本中,arguments对象已被废弃)  
    }  
      
    // 使用Object.prototype.toString.call(data)获取data的内部类型字符串  
    let name = Object.prototype.toString.call(data)   
      
    // 在typeObj中查找与data类型字符串相匹配的简化类型名称,如果未找到则默认为'未知类型'  
    let typeName = typeObj[name] || '未知类型'   
      
    // 返回typeName是否与传入的type参数相等,即判断data是否属于指定的类型  
    return typeName === type   
}  
  
// 测试函数isType  
console.log(  
    isType({}, 'object'), // true,因为空对象{}的类型是'object'  
    isType([], 'array'), // true,因为空数组[]的类型是'array'  
    isType(new Date(), 'object'), // false,因为Date对象的类型是'date',不是'object'  
    isType(new Date(), 'date'), // true,因为Date对象的类型是'date'  
)
3.2 将类数组转化为数组,以借用方法

数组,它的特征有:可以通过角标调用,如 array[0];具有长度属性length;可以通过 for 循环或forEach方法,进行遍历。

那么,类数组呢?顾名思义,就是具备与数组特征类似的对象。比如,下面的这个对象,就是一个类数组:let arrayLike = { 0: 1, 1: 2, 2: 3, length: 3},但是类数组无法使用 forEach、splice、push 等数组原型链上的方法


<div class="div1">1</div>
<div class="div1">2</div>
<div class="div1">3</div>

let div = document.getElementsByTagName('div');
console.log(div); // HTMLCollection(3) [div.div1, div.div1, div.div1] 里面包含length属性

let arr2 = Array.prototype.slice.call(div);
console.log(arr2); // 数组 [div.div1, div.div1, div.div1]
3.3 apply获取数组最大值最小值:
const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6
3.4 数组拼接,添加
let arr1 = [1,2,3];
let arr2 = [4,5,6];

//利用数组的concat方法:返回一个新的数组
let arr3 = arr1.concat(arr2); 
console.log(arr3); // [1, 2, 3, 4, 5, 6]

console.log(arr1); // [1, 2, 3] 不变
console.log(arr2); // [4, 5, 6] 不变
// 用 apply方法
[].push.apply(arr1,arr2);  // 给arr1添加arr2
console.log(arr1); // [1, 2, 3, 4, 5, 6]
console.log(arr2); // 不变
3.5 利用call和apply做继承
function Animal(name){      
    this.name = name;      
    this.showName = function(){      
        console.log(this.name);      
    }      
}      

function Cat(name){    
    Animal.call(this, name);// 使用this对象代替Animal对象,那么Cat中就有Animal的所有属性和方法,就能直接调用Animal的方法以及属性
}      
var cat = new Cat("TONY");     
cat.showName();   //TONY
3.6 保存函数参数

请问下面的这道经典面试题会输出什么?

for (var i = 1; i <= 5; i++) {
   setTimeout(function test() {
        console.log(i) 
    }, i * 1000);
}

会依次输出:6 6 6 6 6,如何改进呢?下面使用了bind方法解决问题

for (var i = 1; i <= 5; i++) {
    // 缓存参数
    setTimeout(function (i) {
        console.log('bind', i) // 依次输出:1 2 3 4 5
    }.bind(null, i), i * 1000);
}

4、手写实现call、apply、bind

4.1 实现call
Function.prototype.myCall = function (context, ...arr) {
    debugger
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数
    context[specialPrototype] = this; // 函数的this指向隐式绑定到context上
    let result = context[specialPrototype](...arr); // 通过隐式绑定执行函数并传递参数
    delete context[specialPrototype]; // 删除上下文对象的属性
    return result; // 返回函数执行结果
};
function name(params) {
    console.log(params)
}
function a() {
    name.myCall(a,'333')
}
a();//333
4.2 实现apply
Function.prototype.myApply = function (context) {
    if (context === null || context === undefined) {
        context = window // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    // JavaScript权威指南判断是否为类数组对象
    function isArrayLike(o) {
        if (o &&                                    // o不是null、undefined等
            typeof o === 'object' &&                // o是对象
            isFinite(o.length) &&                   // o.length是有限数值
            o.length >= 0 &&                        // o.length为非负值
            o.length === Math.floor(o.length) &&    // o.length是整数
            o.length < 4294967296)                  // o.length < 2^32
            return true
        else
            return false
    }
    const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数
    context[specialPrototype] = this; // 隐式绑定this指向到context上
    let args = arguments[1]; // 获取参数数组
    let result
    // 处理传进来的第二个参数
    if (args) {
        // 是否传递第二个参数
        if (!Array.isArray(args) && !isArrayLike(args)) {
            throw new TypeError('myApply 第二个参数不为数组并且不为类数组对象抛出错误');
        } else {
            args = Array.from(args) // 转为数组
            result = context[specialPrototype](...args); // 执行函数并展开数组,传递函数参数
        }
    } else {
        result = context[specialPrototype](); // 执行函数 
    }
    delete context[specialPrototype]; // 删除上下文对象的属性
    return result; // 返回函数执行结果
};
function name(a,b,c) {
    console.log(a,b,c)
}
function a() {
    name.myApply(a,[2,3,4])
}
a();//2 3 4
4.3 实现bind


Function.prototype.myBind = function (objThis, ...params) {
    const thisFn = this; // 存储源函数以及上方的params(函数参数)
    // 对返回的函数 secondParams 二次传参
    let fToBind = function (...secondParams) {
        const isNew = this instanceof fToBind // this是否是fToBind的实例 也就是返回的fToBind是否通过new调用
        const context = isNew ? this : Object(objThis) // new调用就绑定到this上,否则就绑定到传入的objThis上
        return thisFn.call(context, ...params, ...secondParams); // 用call调用源函数绑定this的指向并传递参数,返回执行结果
    };
    if (thisFn.prototype) {
        // 复制源函数的prototype给fToBind 一些情况下函数没有prototype,比如箭头函数
        fToBind.prototype = Object.create(thisFn.prototype);
    }
    return fToBind; // 返回拷贝的函数
};
function name(a,b,c) {
    console.log(a,b,c)
}
var a = name.myBind(null,[4,4,4]);
a();//[4, 4, 4] undefined undefined