20200809一个坐着一动不动都能让人汗流浃背的日子,实在是懒得动弹,如果能来一场狂风暴雨就好了,果不其然,傍晚时分,电闪雷鸣,乌云密布。所以准备抽出来个整功夫好好把call、apply和bind总结一下(看完这篇文章,要是还不明白,发红包)。言归正传,下面开始我的表演
一。首先说一下它们三者的作用和区别:
1. 三者都是改变函数调用时this指向的,call和apply立即执行。
const Person = {name: '李鹏',say(x, age) {return console.log(`我叫${this.name},我喜欢女${x}号,今年${age}岁`)}
}const Person2 = {name: '二瑜',
}console.log(Person.say('四', 30)) // 我叫李鹏,我喜欢女四号,今年30岁
// 使用call 改变了this的指向
console.log(Person.say.call(Person2, '三', 28)) // 我叫二瑜,我喜欢女三号,今年28岁
// 使用apply 改变this指向
console.log(Person.say.apply(Person2,['三','20'])) // 我叫二瑜,我喜欢女三号,今年20岁
2. 通过上面的例子可以看到 call 可以有多个参数,(第一个参数是要改变的this,可以传入null,从第二个参数开始是要传入的参数列表),apply只有两个参数(第一个参数要传入的this,可以为null,第二个参数是要传入的参数列表组成的数组)
3. 相比call和apply,bind就比较复杂了(原理部分我会细心讲解),它不能立即执行,需要调用一次才执行,它可以和new关键字配合使用实现柯里化传值
// 1.改变this指向
function foo(something) {this.a = something
}var obj = {}var bar = foo.bind(obj)
bar(2)
// 以上两句话 可以总结为一句 foo.bind(obj)(2)
console.log(obj.a) // 2// 2.配合new关键字实现 柯里化传参
function bar(p1,p2) {this.val = p1 + p2
}
var bas = bar.bind(null,'p1')
var baz = new bas('p2')
console.log(baz.val) ; // p1p2
到这里,他们的作用和区别都说完了,你都明白了吗?
二。下面我们手动实现call、apply和bind ,看看底层到底是怎么实现的。
1.call的底层实现
Function.prototype.myCall = function (context) {// 判断this的类型,如果不是function 则返回if(typeof this !== 'function') return// 判断有没有传入上下文,如果没有传入则指向windowcontext = context || Window;// 在上下文中注入一个属性指向thiscontext.fn = this // 改变指向// 从第二个参数开始才是要传入的实参列表,截取传入的参数(第一个是传入的要改变的this)let args = [...arguments].slice(1)// 指行函数let res = context.fn(...args)// 删除属性,不然污染传入的上下文delete context.fnreturn res}
2.上面的实现如果还是不知所云,请看案例版
const Person = {name: '李鹏',say(x, age) {return console.log(`我叫${this.name},我喜欢女${x}号,今年${age}岁`)}
}const Person2 = {name: '二瑜',
}Function.prototype.myCall = function (context) {// 判断this的类型,如果不是function 则返回if(typeof this !== 'function') returnconsole.log('this =>',this) // this=> say(x, age) { return console.log(`我叫${this.name},我喜欢女${x}号,今年${age}岁`) }// 判断有没有传入上下文,如果没有传入则指向windowcontext = context || Window;console.log('context =>',context) // context=> { name: '二瑜' }// 在上下文中注入一个属性指向thiscontext.fn = this // 改变指向// 从第二个参数开始,截取传入的参数(第一个是传入的要改变的this)let args = [...arguments].slice(1)console.log('arguments=>',arguments) // arguments=> [Arguments] { '0': { name: '二瑜', fn: [Function: say] }, '1': '三', '2': '18' }console.log('args =>',args) // args => [ '三', '18' ]// 指行函数let res = context.fn(...args)// 删除属性,不然污染传入的上下文delete context.fnreturn res}console.log(Person.say.myCall(Person2,'三',"18")) // 我叫二瑜,我喜欢女三号,今年18岁
3.apply的手动实现
Function.prototype.myApply = function (context) {// 判断this的类型,如果不是function 则返回if(typeof this !== 'function') return// 判断有没有传入上下文,如果没有传入则指向windowcontext = context || window;// 注入属性 改变thiscontext.fn = this // 改变let res = ''// 判断参数列表所组成的数组是否存在,不存在即没有传入参数if (arguments[1]) {res = context.fn(...arguments[1])} else {res = context.fn()}// 删除注入的属性delete context.fnreturn res}
4.bind的手动实现(简化版)
Function.prototype.myBind = function (context) {// 不是函数类型,返回if (typeof this !== 'function') return;// 记录一下this,因为需要返回一个函数,this会被篡改let _self = this// 截取实参列表let args = Array.prototype.slice.call(arguments, 1)return function () {// 处理柯里化传入的参数,拼接到bind绑定的后面return _self.apply(context, args.concat(Array.prototype.slice.call(arguments)))}
}
为什么说是简化版呢,机智的你可能已经看出来了,我前面说过它可以配置new关键子来实现,这里好像没有做处理。不妨看个例子
function foo(name) {this.name = name
}
var obj = {}
Function.prototype.myBind = function (context) {// 不是函数类型,返回if (typeof this !== 'function') return;// 记录一下this,因为需要返回一个函数,this会被篡改let _self = this// 截取实参列表let args = Array.prototype.slice.call(arguments, 1)return function () {// 处理柯里化传入的参数,拼接到bind绑定的后面return _self.apply(context, args.concat(Array.prototype.slice.call(arguments)))}
}
var bar = foo.myBind(obj)
var alice = new bar('alice')
console.log(obj.name) //alice
console.log(alice.name) //undefined
可以看到new 关键字没有起作用。那应该怎么实现呢? 这里就要明白new关键字的作用了(看我的另一篇博客,new关键字到底干了些什么)。我这里再啰嗦一遍。
new 关键字的作用:
1. 创建一个全新的对象;
2.这个新对象会被执行[[原型]]连接;
3. 这个新对象会被绑定到函数调用的this;
4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
5.手动bind(终极版)
Function.prototype.myBind = function (context) {console.log('context =', context)if (typeof this !== 'function') return;let _self = this, // 原函数foo的引用args = Array.prototype.slice.call(arguments, 1),fNop = function () { },fnBound = function () {// 处理new关键字 console.log('this', this, _self, this instanceof _self)let _this = this instanceof _self ? this : contextreturn _self.apply(_this, args.concat(Array.prototype.slice.call(arguments)))};fNop.prototype = this.prototypefnBound.prototype = new fNop()return fnBound
}
再用刚才的例子,验证一下
function foo(name) {this.name = name
}
var obj = {}
Function.prototype.myBind = function (context) {console.log('context =', context)if (typeof this !== 'function') return;let _self = this, // 原函数foo的引用args = Array.prototype.slice.call(arguments, 1),fNop = function () { },fnBound = function () {// 处理new关键字 console.log('this', this, _self, this instanceof _self)let _this = this instanceof _self ? this : contextreturn _self.apply(_this, args.concat(Array.prototype.slice.call(arguments)))};fNop.prototype = this.prototypefnBound.prototype = new fNop()return fnBound
}
var bar = foo.myBind(obj) // fnBound
var alice = new bar('alice')
console.log(alice.name) //alice