「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」。
前言
大家好,我是轮子猫。本系列文章将带你使用TDD的方式每天花费5分钟,完成一个大厂笔试、面试必考手写题目。
- 防抖 ✅
- 节流 ✅
- call ✅
- EventEmitter
- 手动实现ES5继承
- 手动实现instanceof
- Object.create
- new
- 浅拷贝和深拷贝
- 单例模式
- 手动实现JSONP
- 数组去重、扁平、最值
- 数组乱序 - 洗牌算法
- 函数柯里化
- 模拟实现promise
- 基于Promise的ajax封装
- 异步循环打印
- 图片懒加载
需求
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。
- 它们都是用来手动指定函数中this指向的
- call和apply的区别在于传入的参数不同,call方法接受的是若干个参数列表,而apply接收的是一个包含多个参数的的数组
- call,apply和bind的区别在于,call和apply会直接调用函数,而调用bind函数会重新创建一个新的函数,这个函数的this指向bind调用时传入的对象,当这个新的函数被调用时,将其this关键字设置为该对象。
现在,我们已经知道了call,apply,bind都是用来做什么的以及它们的区别。下面我们以call为例来一步一步的拆解需求,示例如下
fn.call(context)
- 首先,call可以直接被函数调用
- 其次,当函数通过call调用且传入的context为原始值时,this的指向为什么呢?
- 打开浏览器的控制台
- 输入如下代码
function fn() {
console.log(this, typeof this)
}
fn.call(1);
fn.call('');
fn.call(null);
fn.call(undefined);
fn.call(true);
- 打印结果如下:
所以,我们可以知道,
- 当context为布尔,数字,字符串时,this指向它们的包装对象。
- 当context为null,undefined时,在浏览器环境下this指向window
- 最后,当函数通过call调用且传入的context为引用值时,this的指向为该引用值
现在我们对需求已经有了很清楚的了解了,下面我们来一步一步实现call。
实现
- call可以直接被函数调用,也就是说
- call存在于函数的原型链上
- call是一个可以被调用的函数
测试代码如下:
// call.test.js
/**
* @jest-environment jsdom
*/
describe('myCall', () => {
test("fn", () => {
const obj = Object.create(Function.prototype);
expect(typeof obj.myCall).toBe("function");
});
})
运行测试代码
现在,我们编写实现代码
// call.js
Function.prototype.myCall = function(context, ...args) {
}
再次运行测试代码
- 当函数通过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);
});
运行测试代码:
现在,我们编写实现代码
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;
}
再次运行测试代码
- 调用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");
});
});
运行测试代码:
最后,实现
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,大功告成~