手撕 call()、apply()、bind 源码
# 前言
都知道 call、apply 与 bind 三兄弟都是用来替换函数中不想用的 this
,使用方法如:
- call(this, ...arguments)
=> 立即执行,参数单个传入 - apply(this, [arguments])
=> 立即执行,参数是需放在数组里 - let fn = bind(this, ...arguments)
=> 绑定之后返回一个新函数但是并不立即执行,需调用的时候才执行。
且绑定的时候可以额外传参数,执行的时候也可以额外传参数。
# Call 与 Apply
call
和 apply
执行的本质是:往要绑定的 context
对象下添加该函数,然后执行,最后将属性删除。
当 context
值为 null
或 undefined
时,非严格模式下,它将替换为 window
或者 global
全局变量。
call()
首选说下这个 call
的原理:call()
方法用改变函数的 this
指向,它接收多个参数,第一个参数为执行作用域,第二个及以后的参数是传递给函数的参数。
第一
步:把我们的 call
方法 myCall
定义到函数原型对象上
// 全局添加一个 myCall 方法
Function.prototype.myCall = function(context) {
}
第二
步:接收一个参数 context
// 参数是否存在,如果存在则转为 Object 类型,否则直接取 window 对象为默认对象
let _context = context? Object(context) : window;
第三
步:要让函数的 this
指向参数 context
,则这个函数必须是 context
的一个属性或方法
因此这里为 context
添加一个 fn
方法,并把这个函数赋给这个方法
_context.fn = this;
第四
步:遍历 arguments
对象,把这些参数转为真正的数组,并拆分为参数传给函数并执行
var argArr = []
// 遍历参数,因为首项是 context,所以要从次项开始遍历才是参数
for (let i = 1; i < arguments.length; i++) {
argArr.push('arguments['+ i + ']');
}
// 或者用一下两种方法得到 argArr:
// 1. let argArr = [...arguments].slice(1)
// 2. let argArr = Array.from(arguments).slice(1)
// 执行 _context 的 fn 方法,把 argArr 拆分
eval("_context.fn(" + argArr + ")"); // => _context.fn(...argArr)
第五
步:要把 _context
对象中的 fn
方法移除,完整代码如下
Function.prototype.myCall = function(context) {
// 参数是否存在,如果存在则转为 Object 类型,否则直接取 window 对象为默认对象
let _context = context? Object(context) : window;
_context.fn = this;
var argArr = []
// 遍历参数,因为首项是 context,所以要从次项开始遍历才是参数
for (let i = 1; i < arguments.length; i++) {
argArr.push('arguments['+ i + ']');
}
// 或者用一下两种方法得到 argArr:
// 1. let argArr = [...arguments].slice(1)
// 2. let argArr = Array.from(arguments).slice(1)
// 执行 _context 的 fn 方法,把 argArr 拆分
eval("_context.fn(" + argArr + ")"); // => _context.fn(...argArr);
// 移除 fn 方法
delete _context.fn;
}
apply()
实现了 call()
方法,apply()
方法就信手拈来了。因为 apply()
与 call()
方法的区别在于第二位参数。
apply()
方法第二位参数也是传递给函数的参数,但是它是一个数组类型的,如下完整代码:
Function.prototype.myApply = function(context, argArr) {
// 参数是否存在,如果存在则转为 Object 类型,否则直接取 window 对象为默认对象
let _context = context? Object(context) : window;
_context.fn = this;
var argList = []
// 当这个参数数组不存在或者为空时,直接执行函数,否则把数组拆分后传递给函数并执行
if (!argArr || argArr.length == 0) {
_context.fn()
} else {
for (let i = 0; i < argArr.length; i++) {
argList .push('argArr['+ i + ']')
}
// 执行 _context 的 fn 方法,把 argList 拆分
eval("_context.fn(" + argList + ")")
}
// 移除 fn 方法
delete _context.fn;
}
# Bind()
bind()
原理:bind()
依然用来改变函数 this
指向,但它不会像 call()
与 apply()
方法会立即执行这个函数,而是返回一个 新函数
给外部,外部用一个变量去接收这个新函数并执行。
注意:bind()
方法返回的那个函数不仅仅可以作为普通函数调用,还可以作为一个构造函数被调用。
第一
步:首先判断执行 myBind()
方法的是不是一个函数
Function.prototype.myBind = function(context) {
// 判断调用 myBind 方法的是否为函数
if (typeof(this) !== "function") {
throw Error("调用 myBind 方法的必须为函数")
}
}
第二
步:截取第一个参数
注意:这里建立在存在 call()
方法的条件下,可以直接使用 Array.slice.call()
将 arguments
对象转为真正的数组并截取从第二项开始的参数:
// 截取传给函数的参数
let args = Array.prototype.slice.call(arguments, 1)
第三
步:将执行 bind()
方法的这个函数保存在一个变量中
let _fn = this
第四
步:再创建一个新的函数变量,用来改变函数 this
let bindFn = function() {
// 获取_bind方法返回的函数的参数
let newArgs = Array.prototype.slice.call(arguments)
// 通过 apply 去改变 this 指向
let _obj = this.constructor === _fn ? this : context
_fn.apply(_obj, args.concat(newArgs))
}
特别注意:
let newArgs = Array.prototype.slice.call(arguments)
这段代码不要跟
let args = Array.prototype.slice.call(arguments,1)
这段代码搞混淆了
newArgs
是返回的新函数的参数,而args
是myBind ()
方法接收并传递给调用它的函数的参数。
再来看看
let _obj = this.constructor === _fn ? this : context
这段代码
上面说到bind()
方法返回的新函数,可以普通调用也可以构造函数方式调用,
当为构造函数时,this
是指向实例的,因此才会做这样的处理。
args.concat(newArgs)
是什么意思呢?
bind()
方法的参数具有一个特性,就是函数柯里化:保留一个参数的位置,再第二次传参的时候自动把参数存入到这个位置中,而这段代码正是用来实现函数柯里化的。
第五
步:既然 bind()
返回的函数可以作为构造函数,那么它得继承调用它的那个函数的原型对象以及属性,这里创建一个媒介函数,用来实现寄生组合式继承:
let ProtoFn = function(){ };
ProtoFn.prototype = _fn.prototype;
bindFn.prototype = new ProtoFn();
完整代码如下:
Function.prototype.myBind = function(obj) {
// 判断调用 myBind 方法的是否为函数
if (typeof(this) !== "function") {
throw Error("调用_bind方法的必须为函数")
}
// 截取传给函数的参数
let args = Array.prototype.slice.call(arguments, 1)
// 保存这个函数,以便后续使用
let _fn = this
// 创建一个待会儿返回出去的函数,这个函数会赋到外部变量中
let bindFn = function() {
// 获取_bind方法返回的函数的参数
let newArgs = Array.prototype.slice.call(arguments)
// 通过apply去改变this指向,实现函数柯里化
let _obj = this.constructor === _fn ? this : context
_fn.apply(_obj, newArgs.concat(args))
}
// 创建一个中介函数,以便实现原型继承
let ProtoFn = function(){}
ProtoFn.prototype = _fn.prototype
bindFn.prototype = new ProtoFn()
// 返回bindFn的函数给外部
return bindFn;
}