Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >js基础-关于call,apply,bind的一切

js基础-关于call,apply,bind的一切

原创
作者头像
OBKoro1
修改于 2019-08-05 03:46:19
修改于 2019-08-05 03:46:19
1.5K0
举报

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

目录

  1. call,apply,bind的基本介绍
  2. call/apply/bind的核心理念:借用方法
  3. call和apply的应用场景
  4. bind的应用场景
  5. 中高级面试题:手写call/apply、bind

call,apply,bind的基本介绍

语法:
代码语言:txt
AI代码解释
复制
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)
返回值:

call/apply:fun执行的结果

bind:返回fun的拷贝,并拥有指定的this值和初始参数

参数

thisArg(可选):

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

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

  1. 如果param不传或为 null/undefined,则表示不需要传入任何参数.
  2. apply第二个参数为数组,数组内的值为传给fun的参数。

调用call/apply/bind的必须是个函数

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

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

作用:

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

如何不弄混call和aaply

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

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

区别:

call与apply的唯一区别

传给fun的参数写法不同:

  • apply是第2个参数,这个参数是一个数组:传给fun参数都写在数组中。
  • call从第2~n的参数都是传给fun的。
call/apply与bind的区别

执行

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

返回值:

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

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

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

看到一个非常棒的例子

生活中:

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

程序中:

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

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

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

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

call和apply的应用场景:

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

  1. 判断数据类型:

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

代码语言:txt
AI代码解释
复制
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
)
  1. 类数组借用数组的方法:

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

比如借用数组的push方法:

代码语言:txt
AI代码解释
复制
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}
  1. apply获取数组最大值最小值:

apply直接传递数组做要调用方法的参数,也省一步展开数组,比如使用Math.maxMath.min来获取数组的最大值/最小值:

代码语言:txt
AI代码解释
复制
const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6
  1. 继承

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

代码语言:txt
AI代码解释
复制
// 父类
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,比如以下示例:

代码语言:txt
AI代码解释
复制
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. 保存函数参数:

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

代码语言:txt
AI代码解释
复制
for (var i = 1; i <= 5; i++) {
   setTimeout(function test() {
        console.log(i) // 依次输出:6 6 6 6 6
    }, i * 1000);
}

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

关于js事件循环机制不理解的同学,可以看我这篇博客:Js 的事件循环(Event Loop)机制以及实例讲解

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

方法有很多:

  • 闭包, 保存变量
代码语言:txt
AI代码解释
复制
for (var i = 1; i <= 5; i++) {
    (function (i) {
        setTimeout(function () {
            console.log('闭包:', i); // 依次输出:1 2 3 4 5
        }, i * 1000);
    }(i));
}

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

  • bind
代码语言:txt
AI代码解释
复制
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

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

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

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

代码语言:txt
AI代码解释
复制
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为何会丢失?

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

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

代码语言:txt
AI代码解释
复制
this.pageClass = new Page(this.handleMessage)

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

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

代码语言:txt
AI代码解释
复制
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指向,用做回调函数。

代码语言:txt
AI代码解释
复制
this.pageClass = new Page(this.handleMessage.bind(this)) // 绑定回调函数的this指向

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

  1. 箭头函数绑定this指向

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

这块内容,可以看下我之前写的博客:详解箭头函数和普通函数的区别以及箭头函数的注意事项、不适用场景

代码语言:txt
AI代码解释
复制
this.pageClass = new Page(() => this.handleMessage()) // 箭头函数绑定this指向

中高级面试题-手写call/apply、bind:

在大厂的面试中,手写实现call,apply,bind(特别是bind)一直是比较高频的面试题,在这里我们也一起来实现一下这几个函数。

你能手写实现一个call吗?

思路

  1. 根据call的规则设置上下文对象,也就是this的指向。
  2. 通过设置context的属性,将函数的this指向隐式绑定到context上
  3. 通过隐式绑定执行函数并传递参数。
  4. 删除临时属性,返回函数执行结果
代码语言:txt
AI代码解释
复制
Function.prototype.myCall = function (context, ...arr) {
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    context.testFn = this; // 函数的this指向隐式绑定到context上
    let result = context.testFn(...arr); // 通过隐式绑定执行函数并传递参数
    delete context.testFn; // 删除上下文对象的属性
    return result; // 返回函数执行结果
};
判断函数的上下文对象:

很多人判断函数上下文对象,只是简单的以context是否为false来判断,比如:

代码语言:txt
AI代码解释
复制
// 判断函数上下文绑定到`window`不够严谨
context = context ? Object(context) : window; 
context = context || window; 

经过测试,以下三种为false的情况,函数的上下文对象都会绑定到window上:

代码语言:txt
AI代码解释
复制
// 网上的其他绑定函数上下文对象的方案: context = context || window; 
function handle(...params) {
    this.test = 'handle'
    console.log('params', this, ...params) // do some thing
}
handle.elseCall('') // window
handle.elseCall(0) // window
handle.elseCall(false) // window

call则将函数的上下文对象会绑定到这些原始值的实例对象上:

原始值的实例对象
原始值的实例对象

所以正确的解决方案,应该是像我上面那么做:

代码语言:txt
AI代码解释
复制
// 正确判断函数上下文对象
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
你能手写实现一个apply吗?

思路:

  1. 传递给函数的参数处理,不太一样,其他部分跟call一样。
  2. apply接受第二个参数为类数组对象, 这里用了JavaScript权威指南中判断是否为类数组对象的方法。
代码语言:txt
AI代码解释
复制
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
    }
    context.testFn = this; // 隐式绑定this指向到context上
    const args = arguments[1]; // 获取参数数组
    let result
    // 处理传进来的第二个参数
    if (args) {
        // 是否传递第二个参数
        if (!Array.isArray(args) && !isArrayLike(args)) {
            throw new TypeError('myApply 第二个参数不为数组并且不为类数组对象抛出错误');
        } else {
            args = Array.from(args) // 转为数组
            result = context.testFn(...args); // 执行函数并展开数组,传递函数参数
        }
    } else {
        result = context.testFn(); // 执行函数
    }
    delete context.testFn; // 删除上下文对象的属性
    return result; // 返回函数执行结果
};
你能手写实现一个bind吗?

划重点

手写bind是大厂中的一个高频的面试题,如果面试的中高级前端,只是能说出它们的区别,用法并不能脱颖而出,理解要有足够的深度才能抱得offer归!

思路

  1. 拷贝源函数:
    • 通过变量储存源函数
    • 使用Object.create复制源函数的prototype给fToBind
  2. 返回拷贝的函数
  3. 调用拷贝的函数:
    • new调用判断:通过instanceof判断函数是否通过new调用,来决定绑定的context
    • 绑定this+传递参数
    • 返回源函数的执行结果
代码语言:txt
AI代码解释
复制
Function.prototype.myBind = function (objThis, ...params) {
    const thisFn = this; // 存储源函数以及上方的params(函数参数)
    let fToBind = function () {
        const isNew = this instanceof fToBind // this是否是fToBind的实例 也就是返回的fToBind是否通过new调用
        const context = isNew ? this : Object(objThis) // new调用就绑定到this上,否则就绑定到传入的objThis上
        return thisFn.apply(context, params); // 用apply调用源函数绑定this的指向并传递参数,返回执行结果
    };
    fToBind.prototype = Object.create(thisFn.prototype); // 复制源函数的prototype给fToBind
    return fToBind; // 返回拷贝的函数
};

小结

本来以为这篇会写的很快,结果断断续续的写了好几天,终于把这三个API相关知识介绍清楚了,希望大家看完之后,面试的时候再遇到这个问题,就可以海陆空全方位的装逼了^_^

觉得我的博客对你有帮助的话,就给我点个Star吧!

前端进阶积累公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

以上2019/8/30

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
call、apply、bind的应用和区别
首先,要明白这三个函数的存在意义是什么?答案是改变函数执行时的上下文,再具体一点就是改变函数运行时的this指向。有了这个认识,接下来我们来看一下,怎么使用这三个函数。
青梅煮码
2023/01/16
4010
call() 和 apply() 的相关介绍和应用场景
call() 和 apply() 返回函数应该返回的值,bind() 返回一个经过硬绑定的新函数。
Chor
2019/11/07
1.3K0
js call()、apply()、bind()用法和区别
thisArg: this要指向的对象,如果是null 和 undefined,则指向window全局对象;
IT工作者
2022/05/13
1.1K0
一文搞定this、apply、call、bind
JavaScript中最容易被误解的一个方面是this关键字。看我本篇文章,它们将不会是问题。
用户8921923
2022/10/24
2070
js原生函数之call和apply,bind
call 和 apply 和 bind 都是为了改变某个函数运行时的 context 即上下文而存在的,换句话说,就是为了改变函数体内部 this 的指向。
用户1394570
2018/08/08
9060
探究call、bind、apply知识点
Contents 1 call、bind、apply知识点 1.1 一、食用方式 1.2 二、使用场景 1.3 三、手写实现 1.4 关于 call、bind、apply知识点 一、食用方式 apply function.apply(obj, [param1,params2,...]) // obj:要绑定的this // 第二个参数:类数组或数组,作为function的参数传入 // 立即执行 call function.call(obj, param1, param2, ...) // obj:要绑定的
w候人兮猗
2020/07/01
3750
【THE LAST TIME】this:call、apply、bind
讲道理,这篇文章有些拿捏不好尺度。准确的说,这篇文章讲解的内容基本算是基础的基础了,但是往往这种基础类的文章很难在啰嗦和详细中把持好。文中道不到的地方还望各位评论多多补充指正。
Nealyang
2019/10/18
5450
【THE LAST TIME】this:call、apply、bind
50行javaScript代码实现简单版的 call , apply ,bind 【中级前端面试基础必备】
其实 this 的指向,始终坚持一个原理:this 永远指向最后调用它的那个对象,这就是精髓。最关键所在
Peter谭金杰
2019/08/15
3530
apply/call/bind 自我实现
面试题:“call/apply/bind源码实现”,事实上是对 JavaScript 基础知识的一个综合考核。
前端迷
2019/09/19
4000
apply/call/bind 自我实现
手动实现call apply bind
bind:当绑定函数被调用时,bind传入的参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。
ConardLi
2019/05/23
7230
手写JS函数的call、apply、bind
之所以要写这篇,是因为曾经面试被要求在白纸上手写bind实现  结果跟代码一样清晰明确,一阵懵逼,没写出来!  下面,撸起袖子就是干!~  把call、apply、bind一条龙都整一遍!~~call定义与使用Function.prototype.call(): developer.mozilla.org/zh-CN/docs/…// Function.prototype.call()样例function fun(arg1, arg2) { console.log(this.name) console.
helloworld1024
2022/10/05
1.1K0
手动实现apply、call、bind
每个Function对象都存在apply()、call()、bind()方法,其作用都是可以在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域。
WindRunnerMax
2020/08/27
5910
你知道多少this,new,bind,call,apply?那我告诉你
那么什么是this,new,bind,call,apply呢?这些你都用过吗?掌握这些内容都是基础中的基础了。如果你不了解,那还不赶快去复习复习,上网查阅资料啥的!
达达前端
2019/11/14
3880
解析call、apply、bind三者区别及实现原理
不管在写插件,还是写框架,又或者其它业务开发中。我们都会遇到在执行函数的时候,如果需要保证函数内部this不被污染或者说需要使函数内部this指向到指定对象的时候,都会按情况分别使用到call、apply、bind方法来实现需求。
吴佳
2022/09/26
1.9K0
【面试题解】你了解call,apply,bind吗?那你可以手写一个吗?
本系列面试题旨在学会相关知识点,从而轻松应对面试题的各种形式,本文讲解了 JavaScript 的基础 call,apply,bind 的作用。
一尾流莺
2022/12/10
2310
【面试题解】你了解call,apply,bind吗?那你可以手写一个吗?
this,call,apply,bind(万字长文)
前言 大家好啊,我是吒儿?,每天努力一点点?,就能升职加薪?当上总经理出任CEO迎娶白富美走上人生巅峰?,想想还有点小激动呢?。 这是我的第13期文章内容✍,希望能够把每一处知识点,说明白,(当然,如
达达前端
2020/06/16
1.2K0
call,apply,bind 的完全实现和理 解
apply(),call()是函数对象的一个方法,它的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象。因此,this指的就是第一个参数。
ZEHAN
2020/09/23
7820
对于bind、call、apply 区别?
call、apply、bind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向
青梅煮码
2023/01/16
2910
JavaScript 设计模式学习第二篇-关于this、new、bind、call、apply
虽然标题关于this、new、bind、call、apply,但实际上这些都离不开 this,因此本文将着重讨论 this,在此过程中分别讲解其他知识点。
越陌度阡
2020/11/26
2960
call、apply、bind
刚开始写技术笔记的时候,很浅显的写了一篇this的指向问题,现在看起来不能说错误百出,但也确实是没什么技术水平。今天分享自己对于call、apply、bind新的认识,并手写一个自己的call、apply、bind。
wade
2020/04/23
9870
相关推荐
call、apply、bind的应用和区别
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档