如何优雅的手写a,b,c三兄弟

4,482 阅读1分钟

bind,apply,call三兄弟

实现思想🤨

皆是通过给目标对象赋值一个临时属性来改变this指向。

注意事项😱

  • 对于传入的context,当为null | undefined的时候指向全局对象,剩余其它任何类型应调用Object方法。
  • 使用globalThis代替window。
  • 对于apply来说判断第二个参数是否为数组(类数组),不要直接遍历。
  • 判断调用者类型是否为function。
  • 注意bind返回的函数的length属性。

apply

//apply
Function.prototype.apply = function (context, args = []) {
  if(typeof this !== 'function'){
      throw new Error(this + "it's not callable");
  }
  //事实上args是支持一个类数组对象的,这里没有在进行细致的判断。
  //类数组对象:只要有一个 `length` 属性和`(0..length-1)`范围的整数属性。
  if(!Array.isArray(args)){
      throw new TypeError('CreateListFromArrayLike called on non-object');
  }
  context = Object(context || globalThis);

  const _symbol = Symbol();
  context[_symbol] = this;
  const result = context[_symbol](...args);
  delete context[_symbol];
  return result;
};

call

//call
Function.prototype.call = function (context,...args) {
  if(typeof this !== 'function'){
      throw new Error(this + "it's not callable");
  }
  //args默认值为[],这里不用进行判断。
  context = Object(context || globalThis);
  const _symbol = Symbol();
  context[_symbol] = this;
  let result = context[_symbol](...args);
  delete context[_symbol];
  return result;
};

bind

Function.prototype.bind = function(context,...args){
    if(typeof this !== 'function'){
      throw new Error(this + "it's not callable");
    }
    context = Object(context || globalThis);
    const _symbol = Symbol();
    const target = this;
    return function(...rest){
        context[_symbol] = target;
        let result = context[_symbol](...args,...rest);
        delete context[_symbol];
        return result;
    }
}

bind-考虑length

下述代码看不懂的可以先看一下juejin.cn/post/705567…

Function.prototype.bind = function (context, ...rest) {
  if(typeof this !== 'function'){
      throw new Error(this + "it's not callable");
  }
  let target = this;
  const targetLength = Math.max(0, target.length - rest.length);
  const list = [];
  for (let i = 0; i < targetLength; i++) {
    list[i] = `$${i}`;
  }
  const binder = function(){
      return target.apply(context,[...rest,...arguments]);
  }
  return Function(
    `binder`,
    `return function(${list.join(",")}){
        return binder(...arguments);
    }`
  )(binder);
};

补充

本篇文章刚发布时我并没有进行认真的测试,导致很多错误的发生😟(在此也感谢评论区的各位指正),目前我已使用了jest对上述代码进行了简单测试,确保类似的低级错误不会发生(当然肯定还会有不那么明显的错误,欢迎各位指正)。

全部测试用例-github

//bind.test.js
Function.prototype.myBind = function (context, ...rest) {
  if (typeof this !== "function") {
    throw new Error(this + "it's not callable");
  }
  let target = this;
  const targetLength = Math.max(0, target.length - rest.length);
  const list = [];
  for (let i = 0; i < targetLength; i++) {
    list[i] = `$${i}`;
  }
  const binder = function () {
    return target.apply(context, [...rest, ...arguments]);
  };
  return Function(
    `binder`,
    `return function(${list.join(",")}){
          return binder(...arguments);
      }`
  )(binder);
};
function fn(a, b, c) {
  return [a, b, c];
}
function fn1() {
  return this;
}
test("bind", () => {
  //常规测试
  expect(fn.myBind(this, 1, 2, 3)()).toEqual(fn.bind(this, 1, 2, 3)());
  //空参数测试1
  expect(fn.myBind(this)()).toEqual(fn.bind(this)());
  //空参数测试2
  expect(fn1.myBind()()).toEqual(fn1.bind()());
  //this指向测试
  let objTest = { test: 111 };
  expect(fn1.myBind(objTest)()).toEqual(fn1.bind(objTest)());
  //多次改变this指向测试
  expect(fn1.myBind(objTest).apply({ test: 222 })).toEqual(
    fn1.bind(objTest).apply({ test: 333 })
  );
  //length测试
  expect(fn.myBind(this, 1).length).toBe(fn.bind(this, 1).length);
  //ToObject测试
  expect(typeof fn1.myBind(1)()).toBe(typeof fn1.bind(1)());
});

参考

developer.mozilla.org/zh-CN/docs/…

tc39.es/ecma262/#se…