手搓 call、apply,“函数柯里化” + “圣杯模式继承” 实现 bind

一、call

作用:改变 this 指向。

用法:

function person(age, hobby){
  console.log( '姓名:', this.name ); // 姓名: 翠花
  console.log( '年龄:', age );       // 年龄: 18
  console.log( '爱好:', hobby );     // 爱好: 坤坤
}

const obj = {
  name: '翠花',
}

person.call( obj, 18, '坤坤' );

原理:

1. 在对象的属性中,增加一个方法

function person(){
  console.log( '姓名:', this.name ); // 姓名: 翠花
  console.log( '年龄:', age );       // 年龄: 18
  console.log( '爱好:', hobby );     // 爱好: 坤坤
}

const obj = {
  name: '翠花',
  fun: person,
}

person.call( obj, 18, '坤坤' ); // → obj.fun()

2. 调用完方法后,再将这个方法清除掉

完整代码:

Function.prototype.myCall = function( obj ){
  obj = obj || window;
  obj.fun = this;
  
  const res = obj.fun( ...([].slice.call(arguments, 1)) );
  delete obj.fun;

  return res; 
}

// --------------- 测试 CODE ------------------
function person(age, hobby){
  console.log( '姓名:', this.name ); // 姓名: 翠花
  console.log( '年龄:', age );       // 年龄: 18
  console.log( '爱好:', hobby );     // 爱好: 坤坤
}

const obj = {
  name: '翠花'
}

person.myCall( obj, 18, '坤坤' );
// --------------- 测试 CODE ------------------

问题1:obj = obj || window; 的作用?

myCall 的第一个参数为 null 时,需要将 obj 指向 window

function fun(){
    console.log('this: ', this); // this:  Window {0: Window, 1: Window, window: Window, self: Window, document: document, name: '', location: Location, …}
}

fun.call(null, 1, 2)

问题2:myCall 函数中的 this 指向?

函数的调用者 personperson 调用了 myCall,所以 this 就指向 person

问题3:为什么需要用 “[ ].clice.call(arguments, 1)” 获取 arguments 中第 2~n 个参数?

function fun(){
    console.log(arguments.slice(1)); // Uncaught TypeError: arguments.slice is not a function
}
fun('1', '2');

1. arguments 是一个伪数组,并非真正的数组,所以在 arguments 的原型上不存在 slice 方法,arguments 并不能直接调用 slice。

借助空数组调用 slice 方法,将 this 指向 arguments,就可以实现让一个伪数组调用数组的 slice 方法了。

2. 调用 call 传入的参数,除了第一个,剩下的都要传递到函数中去。

问题4:第五行代码,将函数执行结果记录起来并返回的作用?

调用 myCall 的函数可能有返回值

function person( age, hobby ){
  return {
    name: this.name,
    age: age,
    hobby: hobby,
  }
}

const obj = {
  name: '翠花',
}

let objInfo = person.myCall( obj, 18, '坤坤' );
console.log( objInfo );  // {name: '翠花', age: 18, hobby: '坤坤'}

如果只在 myCall 中执行了 person 函数,而不将函数返回值返回出去,调用 myCall 得到的结果就会是 undefined 

二、apply

作用:和 call 一样,改变 this 指向。

用法:参数和 call 有一些区别,call 传递给函数的参数是一个平铺结构,而 apply 保存在一个数组中:

function person(age, hobby){
  console.log( '姓名:', this.name ); // 姓名: 翠花
  console.log( '年龄:', age );       // 年龄: 18
  console.log( '爱好:', hobby );     // 爱好: 坤坤
}

const obj = {
  name: '翠花'
}

person.call( obj, 18, '坤坤' );
person.apply( obj, [18, '坤坤'] );

完整代码:

Function.prototype.myApply = function( obj, arg ){
  obj.fun = this || window;
  
  const res = obj.fun( ...(arg || []) );
  delete obj.p;

  return res; 
}

总结

  • 调用 call、apply 在本质上,都相当于将函数绑定到对象内,执行函数,然后删除对象中的函数,实现 this 的调整
  • 调用 myCall 时如果传入第一个参数为 null,指定 this 为 window
  • 最后,一定要将 person 的执行结果返回

三、“函数柯里化” + “圣杯模式继承” 实现 bind

作用:返回一个改变了 this,但其它均和原函数基本一致的函数

用法:

function person(age, hobby){
  console.log( '姓名:', this.name ); // 姓名: 翠花
  console.log( '年龄:', age );       // 年龄: 18
  console.log( '爱好:', hobby );     // 爱好: 坤坤
}

const obj = {
  name: '翠花'
}

const newPerson = person.bind( obj, 18 );
newPerson('坤坤'); // 可以像调用 person 一样调用 newPerson

1. “基本用法”实现

Function.prototype.myBind = function( obj ){
  obj = obj || window;
  const fun = this;
  const args = [].slice.call(arguments, 1);

  return function (){
    return fun.apply( obj, args.concat( ...arguments) ); // 将两个函数的参数合并到一个数组中
  }
}

问题:args.concat( ...arguments) 的作用?

函数柯里化(Currying)。把接受多个参数的函数变换成接收一个单一参数。

调用 bind 时,传递的第 2~n  个参数,与被返回函数的参数,一并传入到 person 中去

const obj = {
  name: '翠花'
}

// 下面三种用法效果是相同的
const newPerson1 = person.bind( obj );
newPerson1(18, '坤坤');

const newPerson2 = person.bind( obj, 18 );
newPerson2('坤坤');

const newPerson3 = person.bind( obj, 18, '坤坤' );
newPerson3();

2. “实例化用法” 实现

bind 返回的函数,可以作为构造函数,通过 new 来实例化对象

但这样使用 myBind 时,this 指向及原型链会存在一些问题:

// myBind 校验代码:
function person(){
  console.log('this: ', this);
  console.log('this instanceof person: ', this instanceof person);
  console.log('this.name: ', this.name);
}

const obj = {
  name: '翠花'
}

const BindPerson = person.bind( obj );
let iKun1 = new BindPerson(); 
// this: person {}
// this instanceof person: true
// this.name: undefined

const MyBindPerson = person.myBind( obj );
let iKun2 = new MyBindPerson(); 
// this: {name: '翠花'}
// this instanceof person: false
// this.name: '翠花'

可以很明显的看到,构造函数用法中,myBind 内 this 的指向是错误的。

思路是借助:不同的用法,被返回函数内的 this 指向是不同的

Function.prototype.myBind = function(){
  const myBindReturnFun = function (){
    console.log('inner-this: ', this);
  }

  return myBindReturnFun;
}

function person(){}

const obj = {
  name: '翠花'
}

const MyBindPerson = person.myBind( obj );
const iKun1 = MyBindPerson();     // inner-this: Window {0: Window, 1: Window, window: Window, self: Window, document: document, name: '', location: Location, …}
const iKun2 = new MyBindPerson(); // inner-this: myBindReturnFun {} (由 MyBindPerson 构造出来的实例)

借助这一点,就可以做区分,针对性地修改 this 指向

Function.prototype.myBind = function( obj ){
  const fun = this;
  const args = [].slice.call( arguments, 1 );

  const myBindReturnFun = function (){
    const twoArgs = args.concat( ...arguments );

    //  ---------------- 针对不同的用法,修改不同的 this -----------------
    fun.apply( this instanceof myBindReturnFun ? this : obj, twoArgs );
    //  --------------------------------------------------------------
  }

  return myBindReturnFun;
}

再次运行上面的校验代码

const BindPerson = person.bind( obj );
let iKun1 = new BindPerson(); 
// this: person {}
// this instanceof person: true
// this.name: undefined

const MyBindPerson = person.myBind( obj );
let iKun2 = new MyBindPerson(); 
// this: myBindReturnFun {}
// this instanceof person: false
// this.name: undefined

this 已经指向 myBindReturnFun 的实例化对象(myBindReturnFun 和 person 是等效的)

但是原型链的问题依然没有被解决

解决这个问题也很简单,调整 myBindReturnFun 的原型链即可:

Function.prototype.myBind = function( obj ){
  const fun = this;
  const args = [].slice.call( arguments, 1 );

  // 创建一个空函数
  var emptyFun = function () {};

  const myBindReturnFun = function (){
    const twoArgs = args.concat( ...arguments );
    fun.apply( this instanceof myBindReturnFun ? this : obj, twoArgs );
  }

  // 圣杯继承:调整后的原型链 myBindReturnFun.prototype.__proto__ → emptyFun.prototype = this.prototype
  emptyFun.prototype = this.prototype;
  myBindReturnFun.prototype = new emptyFun();

  return myBindReturnFun;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

麦田里的POLO桔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值