在002-判断JS数据类型中我们使用了Object.prototype.toString.call()方法来判断数据类型,那么这个call方法是怎么使用的呢,这篇文章我们来探究这个问题。
call的用法
MDN定义:使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数
语法:fun.call(thisArg, arg1, arg2, ...)
应用:
- 使用call实现继承
- 获取数组中的最大值和最小值
var num = [3, 4, 1, 5]; console.log(Math.max.call(Math, ...num));
- 调用函数并且指定上下文的 this
var foo = { value: 1 }; function bar() { console.log(this.value); } bar.call(foo); // 1
2.实现原理
第一步
我们观察栗子3知道,call做了两件事情:
- 执行了函数bar()
- 改变了bar的this,指向了foo
我们抛开call,使用其他方式来做这两件事情:
执行了函数bar()
bar()
改变了bar的this,指向了foo
如果bar()是对象foo中的一个方法,那么会是怎样的呢
foo = {
value: 1,
bar: function() {
console.log(this.value)
}
}
foo.bar() // 1
我们得到的结果跟使用call是一样的,所以我们按照这个思路来实现call
- 将bar函数设置为foo的属性
- 执行bar函数
- 删除bar函数(因为此处只是借用该函数,使用之后删除)
代码实现
Function.prototype.myCall = function(context) { // 此处context传入的是foo
console.log(this) // 在这里可以看到this是bar()
context.fn = this; // 1.将bar()当做是foo的一个属性
context.fn(); // 2.执行foo.bar()
delete context.fn; // 3.删除foo.bar()
}
// 测试一下
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.myCall(foo) // 1
第二步
第一步我们解决了函数执行和this的问题,但是由call的语法可知,call还能传其他参数,来看看原本的call是怎么使用的吧
var foo = {
value: 1
};
function bar(name, age) {
console.log(this.value); // 1
console.log(name); // 'James'
console.log(age) // 20
}
bar.call(foo, 'James', 20);
由此可见,我们需要添加如下两个步骤
- 收集bar中的参数(在这里我们想到了arguments)
- 在执行bar函数的时候把参数传进去
Function.prototype.myCall = function(context) { // 此处context传入的是foo
console.log(this) // 在这里可以看到this是bar()
context.fn = this; // 1.将bar()当做是foo的一个属性
var args = [];
for(var i = 1; i < arguments.length; i++) {
args.push('arguments[' + i + ']'); // 2.收集bar的参数
}
eval('context.fn('+args+')'); // 3.将参数传入并执行foo.bar()
delete context.fn; // 4.删除foo.bar()
}
// 测试一下
var foo = {
value: 1
};
function bar(name, age) {
console.log(this.value); // 1
console.log(name); // 'James'
console.log(age) // 20
}
bar.myCall(foo, 'James', 20);
第三步
还有两个小问题需要解决
- this 参数可以传 null,当为 null 的时候,视为指向 window
var value = 1;
function bar() {
console.log(this.value);
}
bar.call(null); // 1
- 函数是可以有返回值的
var obj = {
value: 1
}
function bar(name, age) {
return {
value: this.value,
name: name,
age: age
}
}
console.log(bar.call(obj, 'James', 20)); // { value: 1, name: 'James', age: 20 }
现在我们来一举攻克这两个问题
Function.prototype.myCall = function(context) { // 此处context传入的是foo
var context = context || window; // 解决问题1,参数传null
context.fn = this; // 1.将bar()当做是foo的一个属性
var args = [];
for(var i = 1; i < arguments.length; i++) {
args.push('arguments[' + i + ']'); // 2.收集bar的参数
}
var result = eval('context.fn('+args+')'); // 3.将参数传入并执行foo.bar()
delete context.fn; // 4.删除foo.bar()
return result; // 解决问题2,有返回值
}
// 测试一下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value); // 2
return {
value: this.value,
name: name,
age: age
}
}
bar.myCall(null); // 1
console.log(bar.myCall(obj, 'James', 20)); // {value: 1, name: "James", age: 20}
参考:
https://github.com/mqyqingfeng/Blog/issues/11
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call