彻底弄懂call、apply、bind, 以及把他们手写一遍
前言
前端在面试的时候其实挺容易遇到个call,apply,bind之类的面试题,其实理解实现原理之后实现起来非常的简单, 当然其中也有一些需要注意的点, 避坑之后你会发现仅仅几行代码即可实现手写,跟着我的思路我们一步步去试着实现
前置概念(关于this指向)
首先我们需要知道,这三个函数都是用于改变this指向的,那么,什么是this的指向呢, 简单出几个题, 大家可以试着思考一下, 下面的代码中, 在浏览器环境分别输出什么,
function fn() {
console.log(this)
}
var obj = {
fn: function () {
console.log(this)
},
fn1: function () {
function b() {
console.log(this)
}
b()
}
}
fn() // ?
obj.fn() // ?
obj.fn1() // ?
OK, 答案依次是 window对象, obj对象, window对象, 简单来说,函数中的this 谁调用就指向谁,如果没人调用就指向window(严格模式下指向undefined),而对于箭头函数而言,它们是没有this的, 他们的this指向就是父作用域块的this指向
明确思路
关于 call , apply , bind 的调用方式以及作用, 点击可跳转 mdn
查看对应的详细说明。
call
、apply
、bind
都是 Function
原型上的方法,用于修改 this 的指向,他们的区别在于: call
, apply
是调用时指定this, 会在调用时执行该函数, 而bind
是创建时指定(箭头函数也是创建时指定), 它并不会立即调用函数,而是会返回一个新的函数,this指向传入的对象
call
和 apply
的不同在于传参方式,call
参数第一个为 thisArg
, 表示调用函数要使用的this值, 后面可以跟上函数的传参,是一个args, apply
接受两个参数, 第一个也是 thisArg
, 第二个是一个数组,里面是调用函数时的传参.
myCall
的函数实现
Function.prototype.myCall = function (thisArg, ...argument) {
const type = typeof thisArg
switch (type) {
case 'undefined':
thisArg = window
break;
case 'null':
thisArg = window
break;
case 'number':
thisArg = new Number(thisArg)
break;
case 'string':
thisArg = new String(thisArg)
break;
case 'boolean':
thisArg = new Boolean(thisArg)
break
}
const key = Symbol('myCall key')
thisArg[key] = this
const result = thisArg[key](...argument)
delete thisArg[key]
return result
}
myCall
的实现讲解
- 我们需要在Function原型链上添加一个方法供我们调用,我们命名了一个
myCall
函数 - 调用方式大概是这样:
xxx.myCall(thisArg, arg1, arg2...)
, 所以我们用...argument
把参数拿到 - 注意的点来了,首先是 thisArg 的传值, 我们对比原生 call 方法发现,如果我没传值或者传了 undefined 或者 null, 这里会把 this 指向 window,这是call 内部做的处理,其次, 如果传值为基本类型,那么call会自动给我们写一个包装器, 那么我们如果要和原生做的一样,需要处理一下
thisArg
的类型 - 我们利用了函数内部this指向调用者这个特性,拿到了需要改变this指向的函数, 即
this
, 同时需要把this
这个函数中的this指向thisArg
, 我们还是利用函数内部this指向调用者这个特性,给thisArg
绑定一个属性为函数this
,然后去调用这个函数, 调用完成之后再删除这个属性,这时就可以实现this
这个函数中的this
指向thisArg
, 重点在于传入的thisArg
添加属性这一步, 可能会造成覆盖掉thisArg
原本有的属性,所以我们使用了Symbol来实现属性key的唯一性,避免了覆盖 - 最后就是返回值了, 原生
call
函数的返回值为调用函数后的返回值,我们需要保存一下返回值,在调用结束删除掉属性之后再将值返回
以上就是一个call
函数的手写了, 理解了这个,那么手写一个apply
就没什么压力了, 仅仅只是参数的差别,可以自行手写实现一下,这里我就不做讲解了, 贴一个代码大家参考一下
myApply
的函数实现
Function.prototype.myApply = function (thisArg, args) {
// 同 call, 只是接受的参数第二个为数组,传参方式不同罢了
// 1. 如果传值为 null|undefined , 则赋值为 window, 如果为 基本类型,则自动包装类型
const type = typeof thisArg
switch (type) {
case 'undefined':
thisArg = window
break;
case 'null':
thisArg = window
break;
case 'number':
thisArg = new Number(thisArg)
break;
case 'string':
thisArg = new String(thisArg)
break;
case 'boolean':
thisArg = new Boolean(thisArg)
break
}
const key = Symbol('myApply key')
thisArg[key] = this
const result = thisArg[key](...args)
delete thisArg[key]
return result
}
myBind 的函数实现
不同于 apply
和 call
, bind
是需要返回一个函数,我们借用箭头函数创建时 this
指向外部块this
这一特性简单实现一下, 我们只需要返回一个箭头函数, 函数内部调用原生的 call
方法,这里需要注意的是传参, 由于 bind 函数是可以传参的, 而bind 返回的函数也可以传参, 所以我们需要处理这两个地方的参数,注意先后顺序,先bind的参数,再是返回函数的参数, 这里借助了原生的 call
方法,这里可以自己简单扩展一下,自己试一试在不使用箭头函数以及原生call去写一下这个bind方法
Function.prototype.myBind = function (thisArg, ...args) {
return (...argument) => this.call(thisArg, ...args, ...argument)
}
结尾
好了,以上就是本文的全部内容了,最后说一句,其实很多知识理解原理之后就会变得很简单,只需要明确思路, 然后动手去实行即可。
转载自:https://juejin.cn/post/7280133121730199613