照旧,先分析bind的特点,再进行重现。只有清晰的了解一个方法的特点,才能完整重现并理解
bind的特点:
- bind方法可以绑定 this 指向
- bind方法会返回一个绑定了this的函数(返回一个函数,所以这是个高阶函数)
- bind支持柯里化(也就是调用时可以传入一部分参数,返回的函数可以继续传入其他参数)
- this一经绑定无法再被更改(call,apply也无效)
- 如果绑定的函数被new了,当前函数的this就是当前的实例
- 原函数的原型链上的属性,new出来的结果也可以找到
由于特点较多,所以需要一步步实现,先实现前四个特点
Function.prototype.bindModel = function (context) {
// 保留原函数,返回的函数被调用的时候才执行
let originFn = this;
// 保存的传入参数(柯里化实现)
let args = [].slice.call(arguments, 1);
// 返回绑定了this的函数
return function (parames) {
originFn.apply(context, args.concat(parames));
}
}
let obj = {
name: '二哈',
age: '1'
}
let obj2 = {
name: '折耳',
age: '2'
}
function fn1 (country, city) {
console.log('我养了一只' + this.name + ', 它今年' + this.age + '岁了,我们一起生活在' + country + '的' + city);
}
const boundFn1 = fn1.bind(obj, '中国');
boundFn1('深圳');
boundFn1.call(obj2, '深圳'); // 我养了一只二哈, 它今年1岁了,我们一起生活在中国的深圳
const boundFn2 = fn1.bindModel(obj, '俄罗斯');
boundFn2.call('莫斯科'); // 我养了一只二哈, 它今年1岁了,我们一起生活在中国的深圳
上面代码originFn.apply(context, args.concat(parames));
也可以看出,因为我们传入的context,其实一开始就被保存了,后面即使改变了返回函数的this指向,但是执行的时候apply的是context而不是this,所以再怎么call或apply也无法改变this指向
当然这个特点其实严谨的说也是不对的,其实返回函数的this指向是被更改了,只是它运行的时候会重新apply(context),所以更改无效。
因此正确的说法应该是:虽然返回函数的this被更改了,但是执行的时候this依然会重新指向回最初绑定的对象上
实现第5个特点,如果返回的函数被new,那么this将不再指向传入的context,而是指向该实例。
因为我们返回的函数现在是个匿名函数,那么去new这个匿名函数将无法判断,所以需要将其变成具名函数,再使用instanceof来判断this的指向
Function.prototype.bindModel = function (context) {
// 保留原函数,返回的函数被调用的时候才执行,如果这里不保存,调用的时候this会变成window
let originFn = this;
// 去掉第一个参数,这样arguments则剩下应该被保存的参数(柯里化实现)
let bindArgs = [].slice.call(arguments, 1);
function fBound (parames) {
// this instanceof fBound 如果是new出来的,那么this将会是fBound,否则将会是window
// 参考上一节new的实现,在Animal函数里打印this,打印出来是Animal {}
originFn.apply(this instanceof fBound ? this : context, bindArgs.concat(parames));
}
return fBound
}
const boundFn1 = fn1.bind(obj, '中国');
boundFn1('深圳');
let instance = new boundFn1('深圳');
const boundFn2 = fn1.bindModel(obj, '俄罗斯');
boundFn2('莫斯科');
let instance2 = new boundFn2('莫斯科'); // 如果是之前的实现,这里依然会指向obj,所以this.name和this。age还是能正常取到
最后实现第6个特点,原函数原型链上的属性会被继承,因此将fbound.prototype = this.prototype即可。当然这么写会导致原型链共享,所以还需要使用一个中间件来实现继承
Function.prototype.bindModel = function (context) {
// 保留原函数,返回的函数被调用的时候才执行,如果这里不保存,调用的时候this会变成window
let originFn = this;
// 去掉第一个参数,这样arguments则剩下应该被保存的参数(柯里化实现)
let bindArgs = [].slice.call(arguments, 1);
// Object.create()原理,创建一个空白函数来做中间件
function Fn() {};
function fBound (parames) {
// 改变this指向传入的上下文环境
// 或者直接使用arguments,这里的arguments已经是返回函数的参数了
// this instanceof fBound 如果是new出来的,那么this将会是fBound,否则将会是window
// 参考上一节new的实现,在Animal函数里打印this,打印出来是Animal {}
originFn.apply(this instanceof fBound ? this : context, bindArgs.concat(parames));
}
Fn.prototype = this.prototype;
// 通过原型链找到原函数中的属性
fBound.prototype = new Fn();
// 返回一个函数,第三步
return fBound
}
let obj = {
name: '二哈',
age: '1'
}
let obj2 = {
name: '二哈哈',
age: '2'
}
function fn1 (country, city) {
console.log('我养了一只' + this.name + ', 它今年' + this.age + '岁了,我们一起生活在' + country + '的' + city);
}
fn1.prototype = {
name: '折耳',
age: '0.5'
}
const boundFn1 = fn1.bind(obj, '中国');
boundFn1('深圳');
let instance = new boundFn1('深圳');
const boundFn2 = fn1.bindModel(obj, '俄罗斯');
boundFn2( '莫斯科');
// 会通过原型链去寻找(fn1.prototype),所以name和age不再是undefined
let instance2 = new boundFn2('莫斯科'); // 我养了一只折耳, 它今年0.5岁了,我们一起生活在俄罗斯的莫斯科
更加详细的文章:js 手动实现bind方法,超详细思路分析!
这篇文章分得比我细很多,但我不是一个喜欢啰里啰嗦的人,点到即可,看不懂我的可以看看这位作者的
网友评论