call、apply、bind的简单实现

173 阅读2分钟

call

//call方法需要添加到函数的原型对象上。让所有函数都能调用call方法。
Function.prototype.myCall = function (thisArg, ...rest) {
  //获取要调用的函数。fn.myCall();    this就是fn。
  const fn = this;
  //判断thisArg参数的值。null和undefined时指向window。
  thisArg = thisArg === null || thisArg === undefined ? window : thisArg;
  //假如thisArg是基本数据类型,需要进行包装成对象。
  thisArg = Object(thisArg);
  //创建唯一的key,避免跟thisArg上的key重复。
  const uniqueKey = Symbol();
  //使用隐式调用改变this。所以需要在对象的属性上添加函数,从而调用函数。
  thisArg[uniqueKey] = fn;
  //隐式调用。解构赋值传递参数。并保存函数返回值。
  const result = thisArg[uniqueKey](...rest);
  //删除对应的属性,减少对环境的影响。
  delete thisArg[uniqueKey];
  //返回函数调用的返回值。
  return result;
};

function foo(num1, num2) {
  console.log('foo的this:', this);
  return `foo的返回值:  ${num1 + num2}`;
}

console.log(foo.myCall({ name: 'feng' }, 2, 3));

apply

//需要给restArr默认值空数组。假如未设置默认值,用户未传值,...undefined就会报错。
Function.prototype.myApply = function (thisArg, restArr = []) {
  const fn = this;
  thisArg = thisArg === null || thisArg === undefined ? window : thisArg;
  thisArg = Object(thisArg);
  const uniqueKey = Symbol();
  thisArg[uniqueKey] = fn;
  const result = thisArg[uniqueKey](...restArr);
  delete thisArg[uniqueKey];
  return result;
};

function foo(num1, num2) {
  console.log('foo的this:', this);
  return num1 + num2;
}

console.log(foo.myApply(null, [1, 2]));

bind

Function.prototype.myBind = function (thisArg, ...rest) {
  const fn = this;
  thisArg = thisArg === null || thisArg === undefined ? window : thisArg;
  thisArg = Object(thisArg);
  //bind不会立即调用函数,而是返回一个新的函数,形成了闭包。
  return function (...restArgs) {
    //给thisArg设置属性写在闭包里更合适。写在外层函数,thisArg在未调用函数时就有uniqueKey属性。
    const uniqueKey = Symbol();
    thisArg[uniqueKey] = fn;
    //需要将两个参数序列合并成一个参数序列。
    const result = thisArg[uniqueKey](...rest, ...restArgs);
    delete thisArg[uniqueKey];
    return result;
  }
};


function foo(num1, num2, num3) {
  console.log('foo的this:', this);
  return num1 + num2 + num3;
}
const bar = foo.myBind({ name: 'feng' }, 1, 2);
const result = bar(3);
console.log(result);

总结

  1. 原生call、apply、bind的实现是由C++编写(V8引擎)的,所以使用JS只能尽可能地实现功能。
  2. 实现思路:使用隐式绑定从而改变this的指向。
  3. 在使用JS实现的过程中,主体逻辑是比较容易编写的,需要花费精力的地方更多是边界情况的处理。对于函数来说,边界情况通常从参数和返回值这两方面考虑。
  4. 函数的本质是延迟执行代码。从bind方法可以体现该本质。
  5. 涉及到的知识
    1. 原型prototype
    2. this的指向
    3. 类型转换(Object()
    4. Symbol的用处
    5. 剩余参数
    6. 展开运算符
    7. 默认参数
    8. 删除对象属性
    9. 闭包
    10. 原生call、apply、bind的使用与区别