js的call, apply, bind函数的实现,这是老生常谈了,这些都是改变函数执行上下文的函数;今天我也来实现一下,实现前提是用es5 语法,然后可以直接当做一个polyfill用。
模拟call函数
在Function.prototype上挂载一个newCall函数表示是对call的模拟,具体逻辑看注释
Function.prototype.newCall = function(context){
// 1 判断context是否为object,如果是object就代表可能是Object 或者 null,如果不是就赋值一个空对象
if (typeof context === 'object') {
context = context || window // context 如果是null就会赋值为window
} else {
context = Object.create(null)
}
// 2 在context下挂载一个函数,函数所在的key随机生成,防止context上已有同名key
var fn = +new Date() + '' + Math.random() // 用时间戳+随机数拼接成一个随机字符串作为一个新的key
context[fn] = this
// 3 newCall如果还有其他的参数传入也要考虑用到
var args = []
for(var i = 1; i < arguments.length; i++) {
args.push('arguments['+ i + ']')
}
// 4 重点在这里,执行context[fn]这个函数,只能用eval,因为newCall的入参参数不确定
var result = eval('context[fn]('+args+')') // args是一个数组,但是当它和字符串相加时自动调用内部的toString方法转成字符串
delete context[fn] // 用完后从context上删除这个函数
// 5 返回结果
return result
}
原理:
- 在要挂载的对象context上临时添加一个方法 f
- 用eval执行这个临时添加的方法f,并拿到执行后的结果result
- 删除这个额外的方法f,并返回执行结果result
模拟apply函数
apply与call原理基本一样,只是入参不一样,区别是apply(context, Array),call(context, arg1,arg2,arg3...)
调用的参数:apply是用数组表示,call是一个个传
Function.prototype.newApply = function(context){
if (typeof context === 'object') {
context = context || window
} else {
context = Object.create(null)
}
var fn = +new Date() + '' + Math.random()
context[fn] = this
// 敲黑板!!!
var args = null
if(arguments[1]){
args = []
for(var i = 0; i < arguments[1].length; i++) {
args.push('arguments[1]['+ i + ']')
}
}
var result = eval('context[fn]('+args+')')
delete context[fn]
return result
}
写完之后发现还可以优化,在于apply的第二个入参是数组,这个参数是确定了的,不像call那样需要从argument那里取参数。所以,应该给它一个默认参数,请看优化版:
// 优化版:入参加个array
Function.prototype.newApply = function(context, array){
if (typeof context === 'object') {
context = context || window
} else {
context = Object.create(null)
}
var fn = +new Date() + '' + Math.random()
context[fn] = this
var args = null
if(array){
args = []
// 这里不需要argument
for(var i = 0; i < array.length; i++) {
args.push('array['+ i + ']')
}
}
var result = eval('context[fn]('+args+')')
delete context[fn]
return result
}
// 用法:
var fn = function(a,b){console.log(this.name,a,b)}
var context = {name: 'name'}
fn.newApply(context, ['a', 'b']) // 打印 => 'name a b'
bind模拟
实现了apply call 之后,bind更加简单了,在外面包层function壳
Function.prototype.newBind = function(context){
if (typeof context === 'object') {
context = context || window
} else {
context = Object.create(null)
}
var args = []
for(var i = 1; i < arguments.length; i++) {
args.push(arguments[i])
}
var fn = +new Date() + '' + Math.random()
context[fn] = this
// 重点这里包个function形成闭包
return function(){
var result = eval('context[fn]('+args+')')
delete context[fn]
return result
}
}
// 或者 newBind1
Function.prototype.newBind1 = function(context){
var args = []
for(var i = 1; i < arguments.length; i++) {
args.push(arguments[i])
}
return function(){
Function.prototype.newApply(context, args)
}
}
// 或者 newBind2
Function.prototype.newBind2 = function(context){
var args = []
for(var i = 1; i < arguments.length; i++) {
args.push(arguments[i])
}
return function(){
eval('Function.prototype.newCall(context, '+args+')')
}
}