个人博客欢迎分享。
大前端养成记欢迎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的理解。