JavaScript中call、apply及bind的深度解析

函数原型链中的 apply,call 和 bind 方法是 JavaScript 中相当重要的概念,与 this 关键字密切相关,相当一部分人对它们的理解还是比较浅显,所谓js基础扎实,绕不开这些基础常用的API,这次让我们来彻底掌握它们吧!

call,apply,bind的基本介绍

语法:

fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)

返回值:

call/apply:fun执行的结果
bind:返回fun的拷贝,并拥有指定的this值和初始参数

参数:

thisArg(可选):

  • funthis指向thisArg对象 。
  • 非严格模式下:thisArg指定为null,undefined,fun中的this指向window对象.。
  • 严格模式下:funthisundefined
  • 值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如 String、Number、Boolean

param1,param2(可选): 传给fun的参数。

  • 如果param不传或为 null/undefined,则表示不需要传入任何参数.。
  • apply第二个参数为数组,数组内的值为传给fun的参数。
调用call/apply/bind的必须是个函数

call、apply和bind是挂在Function对象上的三个方法,只有函数才有这些方法。

只要是函数就可以,比如: Object.prototype.toString就是个函数,我们经常看到这样的用法:Object.prototype.toString.call(data)

作用:

改变函数执行时的this指向,目前所有关于它们的运用,都是基于这一点来进行的。

如何不弄混call和apply?

弄混这两个API的不在少数,不要小看这个问题,记住下面的这个方法就好了。

apply是以a开头,它传给fun的参数是Array,也是以a开头的。

区别:

call与apply的唯一区别: 传给fun的参数写法不同

  • call/apply改变了函数的this上下文后马上执行该函数
  • bind则是返回改变了上下文后的函数,不执行该函数

返回值: 返回值这段在下方bind应用中有详细的示例解析。

  • call/apply 返回fun的执行结果 。
  • bind返回fun的拷贝,并指定了fun的this指向,保存了fun的参数。

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

看到一个非常棒的例子:

生活中:

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

程序中:

A对象有个方法,B对象因为某种原因也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?

当然是借用 A 对象的方法啦,既达到了目的,又节省了内存。

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

借助已实现的方法,改变方法中数据的this指向,减少重复代码,节省内存。

call和apply的应用场景:

这些应用场景,多加体会就可以发现它们的理念都是:借用方法

1、判断数据类型:

Object.prototype.toString用来判断类型再合适不过,借用它我们几乎可以判断所有类型的数据:

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',
        '[object HTMLDivElement]': 'dom', // document.querySelector('#app')
        '[object WeakMap]': 'weakMap',
        '[object Window]': 'window',  // Object.prototype.toString.call(window)
        '[object Error]': 'error', // new Error('1')
        '[object Arguments]': 'arguments',
    }
    let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()获取数据类型
    let typeName = typeObj[name] || '未知类型' // 匹配数据类型
    return typeName === type // 判断该数据类型是否为传入的类型
}
console.log(
    isType({}, 'object'), // true
    isType([], 'array'), // true
    isType(new Date(), 'object'), // false
    isType(new Date(), 'date'), // true
)

2、类数组借用数组的方法:

类数组因为不是真正的数组所有没有数组类型上自带的种种方法,所以我们需要去借用数组的方法。

比如借用数组的push方法:

var arrayLike = {
  0: 'OB',
  1: 'Koro1',
  length: 2
}
Array.prototype.push.call(arrayLike, '添加元素1', '添加元素2');
console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4}

3、apply获取数组最大值最小值:

apply直接传递数组做要调用方法的参数,也省一步展开数组,比如使用Math.maxMath.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、继承

ES5的继承也都是通过借用父类的构造方法来实现父类方法/属性的继承:

// 父类
function supFather(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green']; // 复杂类型
}
supFather.prototype.sayName = function (age) {
    console.log(this.name, 'age');
};
// 子类
function sub(name, age) {
    // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
    supFather.call(this, name);
    this.age = age;
}
// 重写子类的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
    sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
    sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}
inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
    console.log(this.age, 'foo');
};
// 实例化子类,可以在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18} 

类似的应用场景还有很多,就不赘述了,关键在于它们借用方法的理念,不理解的话多看几遍。

call、apply,该用哪个?

call,apply的效果完全一样,它们的区别也在于 :

  • 参数数量/顺序确定就用call,参数数量/顺序不确定的话就用apply。
  • 考虑可读性:参数数量不多就用apply,参数数量比较多的话,把参数整合成数组,使用apply。
  • 参数集合已经是一个数组的情况,用apply,比如上文的获取数组最大值/最小值。

参数数量/顺序不确定的话就用apply,比如以下示例:

const obj = {
    age: 24,
    name: 'OBKoro1',
}
const obj2 = {
    age: 777
}
callObj(obj, handle)
callObj(obj2, handle)
// 根据某些条件来决定要传递参数的数量、以及顺序
function callObj(thisAge, fn) {
    let params = []
    if (thisAge.name) {
        params.push(thisAge.name)
    }
    if (thisAge.age) {
        params.push(thisAge.age)
    }
    fn.apply(thisAge, params) // 数量和顺序不确定 不能使用call
}
function handle(...params) {
    console.log('params', params) // do some thing
}

bind的应用场景:

1、保存函数参数

首先来看下一道经典的面试题:

for (var i = 1; i <= 5; i++) {
   setTimeout(function test() {
        console.log(i) // 依次输出:6 6 6 6 6
    }, i * 1000);
}

造成这个现象的原因是等到setTimeout异步执行时,i已经变成6了。

那么如何使他输出: 1,2,3,4,5呢?

方法有很多: 闭包、bind及let

for (var i = 1; i <= 5; i++) {
    (function (i) {
        setTimeout(function () {
            console.log('闭包:', i); // 依次输出:1 2 3 4 5
        }, i * 1000);
    }(i));
}

在这里创建了一个闭包,每次循环都会把i的最新值传进去,然后被闭包保存起来。

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

实际上这里也用了闭包,我们知道bind会返回一个函数,这个函数也是闭包

它保存了函数的this指向、初始参数,每次i的变更都会被bind的闭包存起来,所以输出1-5。

具体细节,下面有个手写bind方法,研究一下,就能搞懂了。

let声明i也可以输出1-5: 因为let是块级作用域,所以每次都会创建一个新的变量,所以setTimeout每次读的值都是不同的。

2、回调函数this丢失问题:

这是一个常见的问题,下面是我在开发VSCode插件处理webview通信时,遇到的真实问题,一开始以为VSCode的API哪里出问题,调试了一番才发现是this指向丢失的问题。

class Page {
    constructor(callBack) {
        this.className = 'Page'
        this.MessageCallBack = callBack // 
        this.MessageCallBack('发给注册页面的信息') // 执行PageA的回调函数
    }
}
class PageA {
    constructor() {
        this.className = 'PageA'
        this.pageClass = new Page(this.handleMessage) // 注册页面 传递回调函数 问题在这里
    }
    // 与页面通信回调
    handleMessage(msg) {
        console.log('处理通信', this.className, msg) //  'Page' this指向错误
    }
}
new PageA()

回调函数this为何会丢失?

显然声明的时候不会出现问题,执行回调函数的时候也不可能出现问题。

问题出在传递回调函数的时候:

this.pageClass = new Page(this.handleMessage)

因为传递过去的this.handleMessage是一个函数内存地址,没有上下文对象,也就是说该函数没有绑定它的this指向。

那它的this指向于它所应用的绑定规则:

class Page {
    constructor(callBack) {
        this.className = 'Page'
        // callBack() // 直接执行的话 由于class 内部是严格模式,所以this 实际指向的是 undefined
        this.MessageCallBack = callBack // 回调函数的this 隐式绑定到class page
        this.MessageCallBack('发给注册页面的信息')
    }
}

既然知道问题了,那我们只要绑定回调函数的this指向为PageA就解决问题了。

回调函数this丢失的解决方案

1、bind绑定回调函数的this指向:

这是典型bind的应用场景, 绑定this指向,用做回调函数。

this.pageClass = new Page(this.handleMessage.bind(this)) // 绑定回调函数的this指向

PS: 这也是为什么reactrender函数在绑定回调函数的时候,也要使用bind绑定一下this的指向,也是因为同样的问题以及原理。

2、箭头函数绑定this指向

箭头函数的this指向定义的时候外层第一个普通函数的this,在这里指的是class类:PageA

this.pageClass = new Page(() => this.handleMessage()) // 箭头函数绑定this指向
Like (0)
Donate 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
ZEROZERO
Previous 2019年8月1日
Next 2019年8月8日

相关推荐

  • js数组去重(区分object、“NaN”、NaN)

    数组去重在前端面试中比较常见,今天来复习复习。 对这样一个数组进行去重,我尝试了几种方法,大多数不能对对象去重,或者不能区分true和”true”、NaN和…

    2021年2月23日
    1.2K
  • 前端遍历树形数据,返回满足条件的树形数据

    在一次做手机端小程序项目中,有一个机构表单项,需要在页面展示是树形层级结构,但是后端开发人员返回的数据却是一维数组,而且还要在前端做过滤筛选功能。但是在使用的手机端组件库中,却没有…

    2022年11月8日
    338
  • JavaScript基础知识八问

    JavaScript是前端开发中非常重要的一门语言,浏览器是他主要运行的地方。JavaScript是一个非常有意思的语言,但是他有很多一些概念,大家经常都会忽略。比如说,原型,闭包…

    2020年12月30日
    890
  • 函数防抖与函数节流

    函数防抖 定义 触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间;更直白一点就是:一个需要频繁触发的函数,在规定时间内,只让最后一次生效,…

    2020年7月17日
    1.3K
  • 创建JavaScript对象的六种方式

    第一种:Object 构造函数创建 这行代码创建了 Object 引用类型的一个新实例,然后把实例保存在变量 Person 中。 第二种:使用对象字面量表示法 对象字面量是对象定义…

    2020年6月24日
    1.2K
  • JavaScript 事件委托详解

    基本概念 事件委托,通俗地来讲,就是把一个元素响应事件(click、keydown……)的函数委托到另一个元素; 一般来讲,会把一个或者一组元素的事件委托到…

    2021年3月8日
    990
  • 日常开发 26 个常见的 JavaScript 代码优化方案

    1、NULL、Undefined、”【空】检查 我们在创建新变量赋予一个存在的变量值的时候,并不希望赋予null或undefined,我们可以采用以下简洁的赋值方式。 …

    2021年2月22日
    1.2K
  • 深入理解JS内存机制

    JS的内存机制在很多前端开发者看来并不是那么重要,但是如果你想深入学习JS,并将它利用好,打造高质量高性能的前端应用,就必须要了解JS的内存机制。对于内存机制理解了以后,一些基本的…

    2019年7月14日
    1.6K
  • 深入理解JS原型和继承

    在学习JS中的原型,原型链,继承这些知识之前,必须先了解并掌握基础知识:函数和对象的关系。 我们一直都知道,函数也是对象的一种,因为通过instanceof就可以判断出来。但是函数…

    2019年6月29日
    2.0K
  • 回调函数散记

    今天被将要入职的公司的开发人员询问了一个项目中遇到的问题,关于函数内访问外部函数的情况。大致现象如下:js文件中有两个同级函数FnA和FnB,想在函数FnA中调用FnB。 一看就是…

    2019年8月16日
    1.4K

发表回复

Please Login to Comment