Function.prototype.myApply = function(context, argArr) {
context = context || window
context.fn = this
let result
if (argArr) {
let targetArg = []
for (let i = 0; i < argArr.length; i++) {
targetArg.push('argArr[' + i + ']')
}
result = eval('context.fn(' + targetArg + ')')
} else {
result = context.fn()
}
return result
}
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new Error('Function.prototype.bind - what is trying to be bound is not callable.')
}
let self = this
// 获取传入的从下标为[1]开始的参数作为「预设参数」:
let args = Array.prototype.slice.call(arguments, 1)
return fBound = function () {
// 预设参数之外传入的参数,即 bind() 返回的新函数调用时传入的『新增参数』
let bindArgs = Array.prototype.slice.call(arguments)
// 改变绑定函数中 this 的指向,并将「预设参数」和『新增参数』合并
self.myApply(context, args.concat(bindArgs))
}
}
模拟构造函数版:
Function.prototype.myBind = function(context) {
if (typeof this !== 'function') {
throw new Error('Function.prototype.bind - what is trying to be bound is not callable.')
}
let self = this
let args = Array.prototype.slice.call(arguments, 1)
let fBound = function() {
let bindArgs = Array.prototype.slice.call(arguments)
self.myApply(this instanceof self ? this : context, args.concat(bindArgs))
}
/*
如果只是 fBound.prototype = this.prototype,
当我们直接修改 fBound.prototype 的时候,
也会一并顺带修改绑定函数的prototype,这不是我们想要的结果。
这个时候,我们可以通过一个空函数来进行中转:
*/
let fEmpty = function() {}
fEmpty.prototype = self.prototype
fBound.prototype = new fEmpty()
return fBound
}
为什么「构造函数版」需要特殊处理?
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.myBind(foo, 'daisy');
var obj = new bindFoo('18');
.call
、.apply
和.bind
方法是函数对象的原型方法(Function.prototype),有「改变this
指向的」特殊作用,初学 JavaScript 不太容易理解其机制,本文将通过逐步模拟实现这些方法,以加深对这些方法的理解。一、Function.prototype.call()
1. 简介
call()
简单来说,call方法的作用就是:在指定一个this值和若干个指定的参数值的前提下调用某个函数或方法。
因为接下来介绍的函数方法和 this 指针都有很密切的关系,所以我首先向各位推荐一篇文章帮助理解 this 指针:《关于 this 你想知道的一切都在这里》。
举个栗子来说明:
2. 模拟实现:
我们模拟的步骤是:
call
方法可带任意个参数。null
和undefined
。1.为什么
targetArg.push('arguments['+i+']')
的值是字符串,而非targetArg.push(arguments[i])
的值?答:为了兼容字符串类型的值,
例如参数为
myCall(obj, "me", 21)
时,push(arguments[i])
后的targetArg
是:["me", 21]
,经过'context.fn('+targetArg+')'
之后,会变成字符串:"context.fn(me,21)"
, 把me
当成了一个变量,而非原本的字符串,但me
作为一个变量,并未声明过,所以会报错、不可行。而
targetArg.push('arguments['+i+']')
的值是字符串,经过'+targetArg+'
后,是"context.fn(arguments[1],arguments[2])"
,对字符串类型不会报错,可行!2. 为什么用
eval()
这么 hack 的方式实现?还有别的实现方式吗?首先,因为
targetArg
最终为「字符串」类型,所以需要通过eval
转化为对应的变量其次,如果不考虑兼容性,不需要 ES3 就支持的
eval
,也可以用 ES6 的 spread operator:...
替代eval
,执行这一步,即:3. 嗅探兼容
这种给内置对象扩展方法的实现形式,通常称为
Monkey patching(猴子补丁)
,可以做一步嗅探
来保证兼容:后续的几个模拟实现,都可以用嗅探兼容保证兼容性。
二、Function.prototype.apply()
1.简介
apply()
语法:
func.apply(thisArg, [argsArray])
apply 和 call 的用法基本相同,两者的第一个参数都是 func 函数运行时的 this值。 唯一的区别在于,call 可以接受传入多个参数,而 bind 只能接受一个数组传入作为参数。2. 模拟实现
三、Function.prototype.bind()
1. 简介
bind()
语法:
func.bind(thisArg[, arg1[, arg2[, ...]]])
这个方法比较特别,该方法会创建一个新函数并返回,称之为绑定函数,绑定函数会以创建它时传入bind方法的第一个参数作为
this
值,第二个以及以后的参数,将当做这个新的绑定函数的初始预设参数。之后调用新绑定函数时,传递给绑定函数的其他参数会跟在预设参数之后传入。
看一个栗子就明白了:
2. 模拟实现
注意
bind()
的几个特点:new
操作符创建对象。因为模拟构造函数的效果比较不容易理解,且不被推荐,所以在这里我们分别来实现。
不模拟构造函数:
模拟构造函数版:
为什么「构造函数版」需要特殊处理?
使用 new 时,绑定函数内的 this 值会被改成
fBound()
(仍然是this指向的基本规则:this 永远指向最后调用它的那个对象),而外层的
self = this
,self 则会指向调用myBind()
的对象,即bar
通过这3行代码,使 fBound.prototype 指向了 self.prototype, 所以用
this instanceof self
可以判断「绑定后函数」是否用作了构造函数。这部分的内容已经逐步深入到 JavaScript 的内部实现原理之中,确实有些晦涩,可以通过亲手写代码并多多调试观察来加深理解。
参考资料:
JavaScript深入之call和apply的模拟实现, JavaScript深入之bind的模拟实现, 可能遇到假的面试题:不用call和apply方法模拟实现ES5的bind方法