个人博客欢迎分享
。
大前端养成记欢迎Start
和Fork
。
前言:
手写call、apply、bind是前端面试必问的问题,但是也不用当太当心,因为实现起来也并不算太难。
call
、apply
、bind
都可以修改一个函数执行时候的this指向,虽然用法上略有差异,但是在实现的思想上如出一辙。
在实现之前,必须要知道这三个方法是如何使用的:
const obj = {
language: "javascript"
}
function fn(...arg) {
console.log("The current language is " + this.language)
console.log(arg)
}
fn() // The current language is undefined,[]
fn.call(obj, "javascript", "java", "c++") // "The current language is javascript",["javascript", "java", "c++"]
fn.apply(obj, ["javascript", "java", "c++"]) // "The current language is javascript",["javascript", "java", "c++"]
const bindFn = fn.bind(obj, "javascript", "java", "c++")
bindFn() // "The current language is javascript",["javascript", "java", "c++"]
new bindFn() // The current language is undefined,["javascript", "java", "c++"]
从上面的代码明显的发现,call
、apply
、bind
可以修改函数执行时内部的this指向,并且还能传参数到函数中。三者的使用方式都是通过函数点的方式使用,说明这三个方法都是在原型上(Function.prototype)。call
与apply
不同之处就是传递的参数方式不同。最大的不同就是bind
,bind
有两种用法,如果返回的函数当成普通函数调用的时候,里面的this还是传进去的obj,但是new
的时候,函数内部的this指向window
,返回值则是new
的实例。
了解了这些知识后我们就可以撸代码。。。
一. Function.prototype.call实现
从上面可以知道,call的第一个参数是修改函数内部的this指向,从第二个起则是传到函数的参数。
Function.prototype.call = function(context = window) {
// 创建一个唯一值 防止context或者window有相同的key
const symbol = Symbol()
context[symbol] = this // 这里的this是调用者 也就是函数
const ret = context[symbol](...Array.from(arguments).slice(1))
delete context[symbol] // 删除我们添加的属性 不修改传进来的对象或者污染全局变量
return ret
}
function fn() {
console.log(this.name)
}
fn.call({name: "Little Boy"}, "arg1", "arg2") // Little Boy
看到这里很多小伙伴肯定有很多疑问:为什么要用到Symbol,为什么要在context上挂载一个调用者函数,接下来就一一来解答。
首先先来看这段代码:
const obj = {
name: "Little Boy",
fn: function() {
console.log(this.name)
}
}
obj.fn() // Little Boy
这段代码的意思就是对象点的形式去调用函数,this是指向当前对象的(如果这里还不明白的小伙伴,需要对this
的指向好好复习了),那么利用这个套路我们就可以实现call
,使用对象点的方式修改函数运行时内部的this
指向,这就是为什么要在context
上挂载一个调用者函数,既然要在context
上挂载一个函数那么就必须要保证key唯一,因为Symbol
可以生成一个唯一的值,所以这里用到了Symbol
。
二. Function.prototype.apply实现
如果看懂了call
是如何实现了之后,apply
就很好实现了,因为它们两者不同的地方就是传递的参数不同。
Function.prototype.apply = function(context = window) {
// 创建一个唯一值 防止context或者window有相同的key
const symbol = Symbol()
context[symbol] = this // 这里的this是调用者 也就是函数
const args = arguments[1] || []
const ret = context[symbol](...args)
delete context[symbol] // 删除我们添加的属性 不修改传进来的对象或者污染全局变量
return ret
}
function fn() {
console.log(this.name)
}
fn.apply({name: "Little Boy"}, []) // Little Boy
三. Function.prototype.bind实现
bind
的方法有两种用法,一种是直接调用,另一种是new
的方式调用。实现代码如下:
Function.prototype.bind = function(context = window) {
// 创建一个唯一值 防止context或者window有相同的key
const symbol = Symbol()
context[symbol] = this // 这里的this是调用者 也就是函数
const firstArgs = Array.from(arguments).slice(1) // 获取第一次调用的参数
const bindFn = function() {
const secondArgs = Array.from(arguments) // 获取第二次传入的参数
const fn = context[symbol] // 获取调用函数
return this instanceof bindFn ? fn(...[...firstArgs, ...secondArgs]) : context[symbol](...[...firstArgs, ...secondArgs])
}
bindFn.prototype = Object.create(this.prototype)
return bindFn
}
const obj = {
language: "javascript"
}
function fn(...arg) {
console.log("The current language is " + this.language)
console.log(arg)
}
const newFn = fn.bind(obj, 1, 2)
newFn(3, 4) // The current language is javascript,[1, 2, 3, 4]
new newFn(3, 4) // The current language is undefined,{}
console.log(new newFn(3, 4)) // 输出的时候会发现是这个样子的 bindFn {} 发现名称并不是预期的效果,目前我并没有想到好的方案,有知道的小伙伴可以在评论区大显身手哦。
代码看到这里,疑问也会非常的多,大概有如下问题:
-
returnFn
函数内部this instanceof returnFn
为什么要这样判断,判断依据是什么。 -
returnFn
函数内部的fn
,为什么要赋值给fn
后再调用呢。
首先第一个问题,new
与不new
的区别就是函数内部的this
不同,new
的时候returnFn
内部的this
是指向实例的,不new
的时候,returnFn
内部的this
是指向调用者的,这里是window
。this instanceof returnFn
所以这样是为了判断是否new
下执行的不同的逻辑。(这里对this指向有问题的小伙伴,需要去补充这方面的知识了)。
第二个问题,我们使用bind
会发现,new
的时候fn
内部的this
是指向window
的,既然想达到这种效果,就必须在returnFn
中定义个变量把函数取出来,这样相当于window.fn
,fn
函数内部的this
就是指向window
,这就是赋值给fn
后再调用的目的。
最后,希望这篇文章帮助大家对call
、apply
、bind
的理解。