js基础-重新认识call,apply,bind[不看后悔系列]

函数原型链中的 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的基本介绍

语法:

  
    
  
  
  
  1. fun.call(thisArg, param1, param2, ...)

  2. fun.apply(thisArg, [param1,param2,...])

  3. fun.bind(thisArg, param1, param2, ...)

返回值:

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

参数

thisArg(可选):

  1. fun的 this指向 thisArg对象

  2. 非严格模式下:thisArg指定为null,undefined,fun中的this指向window对象.

  3. 严格模式下: fun的 this为 undefined

  4. 值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如 String、Number、Boolean

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

  1. 如果param不传或为 null/undefined,则表示不需要传入任何参数.

  2. apply第二个参数为数组,数组内的值为传给 fun的参数。

调用 callapplybind的必须是个函数

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用来判断类型再合适不过,借用它我们几乎可以判断所有类型的数据:

  
    
  
  
  
  1. function isType(data, type) {

  2. const typeObj = {

  3. '[object String]': 'string',

  4. '[object Number]': 'number',

  5. '[object Boolean]': 'boolean',

  6. '[object Null]': 'null',

  7. '[object Undefined]': 'undefined',

  8. '[object Object]': 'object',

  9. '[object Array]': 'array',

  10. '[object Function]': 'function',

  11. '[object Date]': 'date', // Object.prototype.toString.call(new Date())

  12. '[object RegExp]': 'regExp',

  13. '[object Map]': 'map',

  14. '[object Set]': 'set',

  15. '[object HTMLDivElement]': 'dom', // document.querySelector('#app')

  16. '[object WeakMap]': 'weakMap',

  17. '[object Window]': 'window', // Object.prototype.toString.call(window)

  18. '[object Error]': 'error', // new Error('1')

  19. '[object Arguments]': 'arguments',

  20. }

  21. let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()获取数据类型

  22. let typeName = typeObj[name] || '未知类型' // 匹配数据类型

  23. return typeName === type // 判断该数据类型是否为传入的类型

  24. }

  25. console.log(

  26. isType({}, 'object'), // true

  27. isType([], 'array'), // true

  28. isType(new Date(), 'object'), // false

  29. isType(new Date(), 'date'), // true

  30. )

  1. 类数组借用数组的方法:

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

比如借用数组的push方法:

  
    
  
  
  
  1. var arrayLike = {

  2. 0: 'OB',

  3. 1: 'Koro1',

  4. length: 2

  5. }

  6. Array.prototype.push.call(arrayLike, '添加元素1', '添加元素2');

  7. console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4}

  1. apply获取数组最大值最小值:

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

  
    
  
  
  
  1. const arr = [15, 6, 12, 13, 16];

  2. const max = Math.max.apply(Math, arr); // 16

  3. const min = Math.min.apply(Math, arr); // 6

  1. 继承

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

  
    
  
  
  
  1. // 父类

  2. function supFather(name) {

  3. this.name = name;

  4. this.colors = ['red', 'blue', 'green']; // 复杂类型

  5. }

  6. supFather.prototype.sayName = function (age) {

  7. console.log(this.name, 'age');

  8. };

  9. // 子类

  10. function sub(name, age) {

  11. // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上

  12. supFather.call(this, name);

  13. this.age = age;

  14. }

  15. // 重写子类的prototype,修正constructor指向

  16. function inheritPrototype(sonFn, fatherFn) {

  17. sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法

  18. sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上

  19. }

  20. inheritPrototype(sub, supFather);

  21. sub.prototype.sayAge = function () {

  22. console.log(this.age, 'foo');

  23. };

  24. // 实例化子类,可以在实例上找到属性、方法

  25. const instance1 = new sub("OBKoro1", 24);

  26. const instance2 = new sub("小明", 18);

  27. instance1.colors.push('black')

  28. console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}

  29. console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18}

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

call、apply,该用哪个?、

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

  • 参数数量/顺序确定就用call,参数数量/顺序不确定的话就用apply

  • 考虑可读性:参数数量不多就用apply,参数数量比较多的话,把参数整合成数组,使用apply。

  • 参数集合已经是一个数组的情况,用apply,比如上文的获取数组最大值/最小值。

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

  
    
  
  
  
  1. const obj = {

  2. age: 24,

  3. name: 'OBKoro1',

  4. }

  5. const obj2 = {

  6. age: 777

  7. }

  8. callObj(obj, handle)

  9. callObj(obj2, handle)

  10. // 根据某些条件来决定要传递参数的数量、以及顺序

  11. function callObj(thisAge, fn) {

  12. let params = []

  13. if (thisAge.name) {

  14. params.push(thisAge.name)

  15. }

  16. if (thisAge.age) {

  17. params.push(thisAge.age)

  18. }

  19. fn.apply(thisAge, params) // 数量和顺序不确定 不能使用call

  20. }

  21. function handle(...params) {

  22. console.log('params', params) // do some thing

  23. }

bind的应用场景:

1. 保存函数参数:

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

  
    
  
  
  
  1. for (var i = 1; i <= 5; i++) {

  2. setTimeout(function test() {

  3. console.log(i) // 依次输出:6 6 6 6 6

  4. }, i * 1000);

  5. }

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

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

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

方法有很多:

  • 闭包, 保存变量

  
    
  
  
  
  1. for (var i = 1; i <= 5; i++) {

  2. (function (i) {

  3. setTimeout(function () {

  4. console.log('闭包:', i); // 依次输出:1 2 3 4 5

  5. }, i * 1000);

  6. }(i));

  7. }

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

  • bind

  
    
  
  
  
  1. for (var i = 1; i <= 5; i++) {

  2. // 缓存参数

  3. setTimeout(function (i) {

  4. console.log('bind', i) // 依次输出:1 2 3 4 5

  5. }.bind(null, i), i * 1000);

  6. }

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

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

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

  • let

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

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

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

  
    
  
  
  
  1. class Page {

  2. constructor(callBack) {

  3. this.className = 'Page'

  4. this.MessageCallBack = callBack //

  5. this.MessageCallBack('发给注册页面的信息') // 执行PageA的回调函数

  6. }

  7. }

  8. class PageA {

  9. constructor() {

  10. this.className = 'PageA'

  11. this.pageClass = new Page(this.handleMessage) // 注册页面 传递回调函数 问题在这里

  12. }

  13. // 与页面通信回调

  14. handleMessage(msg) {

  15. console.log('处理通信', this.className, msg) // 'Page' this指向错误

  16. }

  17. }

  18. new PageA()

回调函数 this为何会丢失?

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

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

  
    
  
  
  
  1. this.pageClass = new Page(this.handleMessage)

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

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

  
    
  
  
  
  1. class Page {

  2. constructor(callBack) {

  3. this.className = 'Page'

  4. // callBack() // 直接执行的话 由于class 内部是严格模式,所以this 实际指向的是 undefined

  5. this.MessageCallBack = callBack // 回调函数的this 隐式绑定到class page

  6. this.MessageCallBack('发给注册页面的信息')

  7. }

  8. }

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

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

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

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

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

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

  1. 箭头函数绑定this指向

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

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

  
    
  
  
  
  1. this.pageClass = new Page(() => this.handleMessage()) // 箭头函数绑定this指向

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

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

你能手写实现一个 call吗?

思路

  1. 根据call的规则设置上下文对象,也就是 this的指向。

  2. 通过设置 context的属性,将函数的this指向隐式绑定到context上

  3. 通过隐式绑定执行函数并传递参数。

  4. 删除临时属性,返回函数执行结果

  
    
  
  
  
  1. Function.prototype.myCall = function (context, ...arr) {

  2. if (context === null || context === undefined) {

  3. // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)

  4. context = window

  5. } else {

  6. context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象

  7. }

  8. context.testFn = this; // 函数的this指向隐式绑定到context上

  9. let result = context.testFn(...arr); // 通过隐式绑定执行函数并传递参数

  10. delete context.testFn; // 删除上下文对象的属性

  11. return result; // 返回函数执行结果

  12. };

判断函数的上下文对象:

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

  
    
  
  
  
  1. // 判断函数上下文绑定到`window`不够严谨

  2. context = context ? Object(context) : window;

  3. context = context || window;

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

  
    
  
  
  
  1. // 网上的其他绑定函数上下文对象的方案: context = context || window;

  2. function handle(...params) {

  3. this.test = 'handle'

  4. console.log('params', this, ...params) // do some thing

  5. }

  6. handle.elseCall('') // window

  7. handle.elseCall(0) // window

  8. handle.elseCall(false) // window

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

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

  
    
  
  
  
  1. // 正确判断函数上下文对象

  2. if (context === null || context === undefined) {

  3. // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)

  4. context = window

  5. } else {

  6. context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象

  7. }

你能手写实现一个 apply吗?

思路:

  1. 传递给函数的参数处理,不太一样,其他部分跟 call一样。

  2. apply接受第二个参数为类数组对象, 这里用了JavaScript权威指南中判断是否为类数组对象的方法。

  
    
  
  
  
  1. Function.prototype.myApply = function (context) {

  2. if (context === null || context === undefined) {

  3. context = window // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)

  4. } else {

  5. context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象

  6. }

  7. // JavaScript权威指南判断是否为类数组对象

  8. function isArrayLike(o) {

  9. if (o && // o不是null、undefined等

  10. typeof o === 'object' && // o是对象

  11. isFinite(o.length) && // o.length是有限数值

  12. o.length >= 0 && // o.length为非负值

  13. o.length === Math.floor(o.length) && // o.length是整数

  14. o.length < 4294967296) // o.length < 2^32

  15. return true

  16. else

  17. return false

  18. }

  19. context.testFn = this; // 隐式绑定this指向到context上

  20. const args = arguments[1]; // 获取参数数组

  21. let result

  22. // 处理传进来的第二个参数

  23. if (args) {

  24. // 是否传递第二个参数

  25. if (!Array.isArray(args) && !isArrayLike(args)) {

  26. throw new TypeError('myApply 第二个参数不为数组并且不为类数组对象抛出错误');

  27. } else {

  28. args = Array.from(args) // 转为数组

  29. result = context.testFn(...args); // 执行函数并展开数组,传递函数参数

  30. }

  31. } else {

  32. result = context.testFn(); // 执行函数

  33. }

  34. delete context.testFn; // 删除上下文对象的属性

  35. return result; // 返回函数执行结果

  36. };

你能手写实现一个 bind吗?

划重点

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

思路

  1. 拷贝源函数:

  • 通过变量储存源函数

  • 使用 Object.create复制源函数的prototype给fToBind

  • 返回拷贝的函数

  • 调用拷贝的函数:

    • new调用判断:通过 instanceof判断函数是否通过 new调用,来决定绑定的 context

    • 绑定this+传递参数

    • 返回源函数的执行结果

      
        
      
      
      
    1. Function.prototype.myBind = function (objThis, ...params) {

    2. const thisFn = this; // 存储源函数以及上方的params(函数参数)

    3. let fToBind = function () {

    4. const isNew = this instanceof fToBind // this是否是fToBind的实例 也就是返回的fToBind是否通过new调用

    5. const context = isNew ? this : Object(objThis) // new调用就绑定到this上,否则就绑定到传入的objThis上

    6. return thisFn.apply(context, params); // 用apply调用源函数绑定this的指向并传递参数,返回执行结果

    7. };

    8. fToBind.prototype = Object.create(thisFn.prototype); // 复制源函数的prototype给fToBind

    9. return fToBind; // 返回拷贝的函数

    10. };

    小结

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

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

    前端进阶积累、公众号、GitHub、wx:OBkoro1、邮箱:[email protected]

    以上2019/8/30


    本文分享自微信公众号 - OBKoro1前端进阶积累(gh_8af2fb8e54a9)。
    如有侵权,请联系 [email protected] 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章