1 实现call
函数通过call
调用时,函数体内的this
指向call
方法传入的第一个实参,而call
方法后续的实参会依次传入作为原函数的实参传入。
function setDetails(name,color){ this.name=name; this.color=color; } let cat1={}; let cat2={}; setDetails.call(cat1,'大毛','橘色') setDetails.call(cat2,'二毛','黑色') console.log(cat1.name)//大毛 console.log(cat2.name)//二毛
let person1 ={ name:'zs', say:function (hobby) { console.log(this.name); console.log('爱好:'+ hobby); } } let person2 = { name:'ls' } person1.say('打游戏') person1.say.call(person2,'健身')
开始实现:
先在原型链上挂上我们自定义的call2
方法,让所有函数共享此方法:
Function.prototype.call2 = function () { console.log(this); } setDetails.call2()
继续改进:
- 其实将原函数作为
context
的方法调用时,方法名并不影响功能,将方法名写死反而可能会造成方法名冲突。在ES6中可以用Symbol
来解决,如果不用Symbol
可以随机生成一个基本不可能冲突的字符串,万一冲突则继续生成到不冲突为止 - 在方法调用后删除方法,避免给
context
增加多余的方法。 - 原函数可能有返回值,要将改变
this
并调用后的返回值也返回
Function.prototype.call2 = function (context) { function mySymbol(obj) { let unique = (Math.random() + new Date()) if (obj.hasOwnProperty(unique)) { return mySymbol(obj) //如果还是冲突,递归调用 } else { return unique } } let uniqueName = mySymbol(context) //this === 原函数 //console.log(this); //将原函数作为context的方法调用 context[uniqueName] = this let result = context[uniqueName]() //用完删除 delete context[uniqueName] return result }
改进解决参数传递的问题:
- 因为不知道用户在调用时传参的个数,解决可以通过
arguments
或者剩余参数
来获取除了context
剩余的参数,将剩余参数传递给context[uniqueName]
Function.prototype.call2 = function (context) { function mySymbol(obj) { let unique = (Math.random() + new Date()) if (obj.hasOwnProperty(unique)) { return mySymbol(obj) //如果还是冲突,递归调用 } else { return unique } } let uniqueName = mySymbol(context) //获取除了第一个参数外剩余的参数 let args = Array.from(arguments).slice(1) //this === 原函数 //将原函数作为cat1的方法调用 context[uniqueName] = this //使用扩展运算符传参,可以解决参数不确定的问题 let result = context[uniqueName](...args) //用完删除 delete context[uniqueName] return result }
将剩余参数传递给context[uniqueName]
还可以通过eval
来拼接调用语句:
Function.prototype.call2 = function (context) { function mySymbol(obj) { let unique = (Math.random() + new Date()) if (obj.hasOwnProperty(unique)) { return mySymbol(obj) //如果还是冲突,递归调用 } else { return unique } } let uniqueName = mySymbol(context) // this === 原函数 //console.log(this); //获取除了第一个参数外剩余的参数 let args = []; for(let i = 1; i < arguments.length; i++) { args.push('arguments[' + i + ']'); } //将原函数作为cat1的方法调用 context[uniqueName] = this //使用扩展运算符传参,可以解决参数不确定的问题 let result = eval('context[uniqueName](' + args.join(',') + ')'); //用完删除 delete context[uniqueName] return result }
2 实现apply
apply与call功能一致,但是在调用方式上,是将剩余的实参以一个数组的方式传参:
function setDetails(name,color){ this.name=name; this.color=color; } let cat1={}; let cat2={}; setDetails.apply(cat1,['大毛','橘色']) setDetails.apply(cat2,['二毛','黑色']) console.log(cat1.name)//大毛 console.log(cat2.name)//二毛
实现与call基本一致,注意兼容apply
第二个参数没有传入的情况:
使用扩展运算符
Function.prototype.apply2 = function (context,args) { > function mySymbol(obj) { let unique = (Math.random() + new Date()) if (obj.hasOwnProperty(unique)) { return mySymbol(obj) //如果还是冲突,递归调用 } else { return unique } } let uniqueName = mySymbol(context) //this === 原函数 //console.log(this); args = args || [] //兼容没有传参的情况 //将原函数作为context的方法调用 context[uniqueName] = this //使用扩展运算符传参,可以解决参数不确定的问题 context[uniqueName]](...args) //用完删除 delete context[uniqueName] }
使用eval:
Function.prototype.apply2 = function (context,args) { function mySymbol(obj){ let unique = (Math.random()+ new Date()) if(obj.hasOwnProperty(unique)){ return mySymbol(obj) }else { return unique } } let uniqueName = mySymbol(context) //let args = Array.from(arguments).slice(1) let arr = [] for (let i = 0; i <args.length; i++) { arr.push('args[' + i + ']') } context[uniqueName] = this /* let result = context[uniqueName](args.join(',')) //等同于context[uniqueName]('大毛','橘色')*/ let result = eval('context[uniqueName](' + arr.join(',')+ ')') //删除临时方法 delete context[uniqueName] return result }
3 实现bind
bind
与call
和apply
的功能相似,但有不同,bind
不会立即调用函数,只做this
的绑定,并且返回一个新的函数,这个函数运行的逻辑与原函数一致,但是this
会指向之前绑定的对象。
function setDetails(name,color){ this.name = name this.color = color } let cat1 = {} let setDetails2 = setDetails.bind(cat1) setDetails2('大毛','橘色')
实现思路:bind
返回一个函数,在函数体内调用apply
Function.prototype.bind2 = function (context) { let originFn = this; return function () { return originFn.apply(context); } }
继续改进:
调用bind
后返回的函数是可以传参的。
bind
方法除了第一个参数,还可以额外传参,可以理解为预传参。
function setDetails(name,color){ this.name = name this.color = color } let cat1 = {} let setDetails2 = setDetails.bind(cat1,'大毛') setDetails2('橘色')
将第一次传参先存起来,在调用时将第一次和第二次传参进行拼接:
Function.prototype.bind2 = function (context) { let originFn = this; let args = Array.from(arguments).slice(1); return function () { let bindArgs = Array.from(arguments); return originFn.apply(context, args.concat(bindArgs)); } }
最后改进:
-
bind
返回的函数,如果之后是作为构造函数调用,则原函数中的this
会指向创建的对象,而不会指向之前绑定的对象,并且生成的实例仍然可以使用原型对象上的属性:
function Cat(name, color) { this.name = name this.color = color } Cat.prototype.miao = function(){console.log('喵~!')} let cat1 = {} let CatBind = Cat.bind(cat1) let cat2 = new CatBind('大毛','橘色') cat2.miao()
当返回的函数作为构造函数,通过new xxx()
调用时,返回的函数的this
指向以返回函数作为构造生成的实例。因此只需要判断this instanceof 返回的函数
,另外,返回的函数要继承原函数(将原型链连接起来):
Function.prototype.bind2 = function (context) { let originFn = this; let args = Array.from(arguments).slice(1); function fBind() { let bindArgs = Array.from(arguments); return originFn.apply(this instanceof fBind ? this /*作为构造函数调用*/: context, args.concat(bindArgs)); } fBind.prototype = Object.create(this.prototype) //继承 return fBind }