每天5分钟 - 使用TDD的方式实现call

366 阅读3分钟

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」。

前言

大家好,我是轮子猫。本系列文章将带你使用TDD的方式每天花费5分钟,完成一个大厂笔试、面试必考手写题目。

  1. 防抖
  2. 节流
  3. call ✅
  4. EventEmitter
  5. 手动实现ES5继承
  6. 手动实现instanceof
  7. Object.create
  8. new
  9. 浅拷贝和深拷贝
  10. 单例模式
  11. 手动实现JSONP
  12. 数组去重、扁平、最值
  13. 数组乱序 - 洗牌算法
  14. 函数柯里化
  15. 模拟实现promise
  16. 基于Promise的ajax封装
  17. 异步循环打印
  18. 图片懒加载

需求

call,apply,bind都是用来改变this指向的。它们的工作方方式如下代码所示

function sayHi(a, b){
    console.log('Hello,', this.name, a, b);
}
const person = {
    name: 'nancy',
    sayHi: sayHi
}
const name = 'lee';
let Hi = person.sayHi;
Hi.call(person, '1', '2'); // Hello, nancy, 1, 2
Hi.apply(person, ['2', '2']); // Hello, nancy, 1, 2

Hi = person.sayHi.bind(person,1, 2)
Hi() // Hello, nancy, 1, 2

上面3个例子的打印结果都为: Hello, nancy, 1, 2。

  1. 它们都是用来手动指定函数中this指向的
  2. call和apply的区别在于传入的参数不同,call方法接受的是若干个参数列表,而apply接收的是一个包含多个参数的的数组
  3. call,apply和bind的区别在于,call和apply会直接调用函数,而调用bind函数会重新创建一个新的函数,这个函数的this指向bind调用时传入的对象,当这个新的函数被调用时,将其this关键字设置为该对象。

现在,我们已经知道了call,apply,bind都是用来做什么的以及它们的区别。下面我们以call为例来一步一步的拆解需求,示例如下

fn.call(context)
  1. 首先,call可以直接被函数调用
  2. 其次,当函数通过call调用且传入的context为原始值时,this的指向为什么呢?
    1. 打开浏览器的控制台
    2. 输入如下代码
function fn() {
  console.log(this, typeof this)
}

fn.call(1);
fn.call('');
fn.call(null);
fn.call(undefined);
fn.call(true);
  1. 打印结果如下:

image.png 所以,我们可以知道,

  1. 当context为布尔,数字,字符串时,this指向它们的包装对象。
  2. 当context为null,undefined时,在浏览器环境下this指向window
  3. 最后,当函数通过call调用且传入的context为引用值时,this的指向为该引用值

现在我们对需求已经有了很清楚的了解了,下面我们来一步一步实现call。

实现

  1. call可以直接被函数调用,也就是说
    1. call存在于函数的原型链上
    2. call是一个可以被调用的函数

测试代码如下:

// call.test.js
/**
 * @jest-environment jsdom
 */

describe('myCall', () => {
  test("fn", () => {
    const obj = Object.create(Function.prototype);
    expect(typeof obj.myCall).toBe("function");
  });
})

运行测试代码 image.png 现在,我们编写实现代码

// call.js
Function.prototype.myCall = function(context, ...args) {
  
}

再次运行测试代码 image.png

  1. 当函数通过call调用且传入的context为原始值,测试代码如下
test('string', () => {
  function play() {
    return this;
  }
  
  expect(play.myCall('')).toEqual(new String(''))
})

test('number', () => {
  function play() {
    return this;
  }
  
  expect(play.myCall(2)).toEqual(new Number(2));
})


test('boolean', () => {
  function play() {
    return this;
  }
  
  expect(play.myCall(false)).toEqual(new Boolean(false));
})

test("null, undefined for node enviroment", () => {
  function play() {
    return this;
  }

  expect(play.myCall(null)).toEqual(global);
  expect(play.myCall(undefined)).toEqual(global);
});

test("null, undefined for browser enviroment", () => {
  function play() {
    return this;
  }

  expect(play.myCall(null)).toEqual(window);
  expect(play.myCall(undefined)).toEqual(window);
});

运行测试代码: image.png 现在,我们编写实现代码

Function.prototype.myCall = function(context, ...args) {
  context = map[context] || (map[typeof context] && map[typeof context](context));
  
  const fn = Symbol();
  context[fn] = this;
  let result;
  try {
    result = context[fn](...args);
  } catch (err) {
    throw err;
  }
  delete context[fn];
  return result;
}

再次运行测试代码 image.png

  1. 调用call,传入的参数为引用对象时,测试代码如下
test('normoal', () => {
    function Product(name, price) {
      this.name = name;
      this.price = price;
    }

    function Food(name, price) {
      Product.myCall(this, name, price);
      this.category = "food";
    }

    expect(new Food("cheese", 5).name).toBe("cheese");
  });
});

运行测试代码: image.png 最后,实现

Function.prototype.myCall = function (context, ...args) {
  const map = {
    number: function (context) {
      return new Number(context);
    },
    string: function (context) {
      return new String(context);
    },
    boolean: function (context) {
      return new Boolean(context);
    },
    undefined: global || window,
    null: global || window,
  };
  context =
    map[context] ||
    (map[typeof context] && map[typeof context](context)) ||
    context;
  const fn = Symbol();
  context[fn] = this;
  let result;
  try {
    result = context[fn](...args);
  } catch (err) {
    throw err;
  }
  delete context[fn];
  return result;
};


OK,大功告成~ image.png