手写apply、call和bind

178 阅读4分钟

1. call的实现

  • 首先 context 为可选参数,如果不传的话默认上下文为 window
  • 为了避免函数名与上下文(context)的属性发生冲突,使用Symbol类型作为唯一值
  • 因为call可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
  • 最后执行并删除ctx[func]
Function.prototype.myCall = function(context) {
    // 判断调用者是否为函数 this指向调用myCall的函数
    if(typeof this !== 'function') {
        throw new Error('caller must be a function')
    }
    // 1. 第一个参数为null或者undefined时,this指向全局对象window; 
    // 2. this 参数可以传基本类型数据,原生的 call 会自动用 Object() 转换
    const ctx = Object(context) || window; 
    
    //新建一个唯一的Symbol变量避免重复
    const func = Symbol();
    ctx[func] = this; // 将函数设置为对象的属性
    
    const args = [...arguments].slice(1);
    
    const res = ctx[func](...args);
    delete ctx[func];
    
    return res;
}

2. apply的实现

Function.prototype.myApply = function (context) {
  // 判断调用者是否为函数 this指向调用myApply的函数
  if(typeof this !== 'function') {
    throw new Error('caller must be a function')
  }
  const ctx = Object(context) || window;
  const func = Symbol();
  ctx[func] = this;

  let res;
  // 判断是否有参数传入
  if(arguments[1]) {
    res =ctx[func](...arguments[1])
  } else {
    res = ctx[func]
  }

  delete ctx[func];
  return res;
}

3. bind的实现

我们一步步实现以下bind的四点特性:

  • 可以指定this
  • 返回一个函数
  • 可以传入参数
  • 柯里化

模拟实现第一步

Function.prototype.myBind = function(context) {
    const self = this; // this 指向调用者
    return function() { // 实现第2点
        return self.apply(context); // 实现第1点
    }
}

模拟实现第二步

Function.prototype.myBind = function(context) {
    const self = this; // this 指向调用者
    
    // 实现第3点,因为第一个参数是指定的this,所以只汲取第1个之后的参数
    const args = [...arguments].slice(1);
    
    return function() { // 实现第2点
        // 实现第4点,这时的arguments是指bind返回的函数传入的参数,即return function的参数
        const bindArgs = [...arguments];
        return self.apply(context, args.concat(bindArgs)); // 实现第1点
    }
}

模拟实现第三步

到现在已经完成大部分了,但是还有一个难点,bind有以下特性:

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

太抽象了,举个例子来理解下:

var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'Jack');
var obj = new bindFoo(20);
// undefined
// Jack
// 20

obj.habit;
// shopping

obj.friend;
// kevin

上面示例中,运行结果this.value输出为undefined, 这不是全局value也不是foo对象中的value,这说明bind的this对象失效了,new的实现中生成一个新的对象,这个时候的this指向的是obj。

这里可以通过修改返回函数的原型来实现,代码如下:

// 第三版
Function.prototype.bind2 = function (context) {
    const self = this;
    
    const args = [...arguments].slice(1);
    
    const fBound = function() {
        const bindArgs = [...arguments];
        
        // 注释1
        return self.apply(this instanceof fBound? this : context, args.concat(bindArgs
        ));
    }
    
    // 注释2
    fBound.prototype = this.prototype;
    return fBound;
}

注释1:

  • 当作为构造函数时,this指向实例,此时this instance of fBound 结果为true,可以让实例获得来自绑定函数的值,即上例中实例会具有habit属性
  • 当作为普通函数时,this指向window,此时结果为false,集那个绑定函数的this指向context

注释2:

修改返回函数的prototype为绑定函数的prototype,实例久可以继承绑定函数的原型中的值,即上面例子中obj可以获取到bar原型上的friend.

模拟实现第四步

上面实现中 fBound.prototype = this.prototype有一个缺点,直接修改 fBound.prototype 的时候,也会直接修改 this.prototype。

我们来测试一下:

// 测试用例
var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

var bindFoo = bar.bind2(foo, 'Jack'); // bind2
var obj = new bindFoo(20); // 返回正确
// undefined
// Jack
// 20

obj.habit; // 返回正确
// shopping

obj.friend; // 返回正确
// kevin

obj.__proto__.friend = "Kitty"; // 修改原型

bar.prototype.friend; // 返回错误,这里被修改了
// Kitty

解决方案是用一个空对象作为中介,把fBound.prototype赋值为空对象的实例(原型式继承)。

const fNOP = function() {}; // 创建一个空对象
fNOP.prototype = this.prototype; // 空对象的原型指向绑定函数的原型
fBound.prototype = new fNOP(); // 空对象的实例赋值给 fBound.prototype


这里可以直接使用ES5Object.create()方法生成一个新对象
fBound.prototype = Object.create(this.prototype)

第四版目前ok了,代码如下:

Function.prototype.myBind = function(context) {
    const self = this;
    const fNOP = function () {};
    
    const args = [...arguments].slice(1);
    
    const fBound = function() {
        const bindArgs = [...arguments];
        return self.apply(this instanceof fNOP? this : context, args.concat(bindArgs));
    }
    
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

模拟实现第五步(最终版)

到这里其实已经差不多了,但有一个问题是调用 bind 的不是函数,这时候需要抛出异常。

Function.prototype.myBind = function(context) {
    if(typeof this !== 'function') {
        throw new Error('caller must be a function!')
    }
    const self = this;
    const fNOP = function () {};
    
    const args = [...arguments].slice(1);
    
    const fBound = function() {
        const bindArgs = [...arguments];
        return self.apply(this instanceof fNOP? this : context, args.concat(bindArgs));
    }
    
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

参考: github.com/yygmind/blo…