什么是call()?
在 JavaScript 中,call
是函数对象的一个方法,用于在特定的作用域(上下文)中调用函数。call
方法允许你指定函数运行时的 this
值,并且可以传入一组参数作为函数的参数。
语法如下:
js
functionName.call(thisArg, arg1, arg2, ...)
其中:
functionName
是要调用的函数名。thisArg
是函数执行时的上下文对象,在函数内部通过this
关键字访问。如果将thisArg
设置为null
或undefined
,则在严格模式下this
值会被设置为全局对象,非严格模式下则会被设置为window
对象。arg1, arg2, ...
是函数的参数列表。
使用 call
方法可以将一个对象的方法应用到另一个对象上,同时保持 this
关键字指向新对象,从而实现方法的复用。
例如:
js
const person1 = {
fullName: function() {
return this.firstName + " " + this.lastName;
}
}
const person2 = {
firstName: "John",
lastName: "Doe"
}
// 使用 call 方法将 person1 的 fullName 方法应用到 person2 上
const fullName = person1.fullName.call(person2);
console.log(fullName); // 输出 "John Doe"
在函数式编程中,call
方法还经常用于将函数应用到数组或类数组对象上,以方便地使用数组的方法来操作这些对象。
例如:
js
function doubleValue(value) {
return value * 2;
}
const numbers = [1, 2, 3, 4, 5];
// 使用 call 方法将 doubleValue 函数应用于 numbers 数组,并将数组的每个元素作为参数传递给函数
const doubledNumbers = Array.prototype.map.call(numbers, doubleValue);
console.log(doubledNumbers); // 输出 [2, 4, 6, 8, 10]
在这个示例中,Array.prototype.map.call(numbers, doubleValue)
实际上是调用了 Array
对象的 map
方法,将 doubleValue
函数应用于 numbers
数组的每个元素上,返回了一个新的数组 doubledNumbers
,其中每个元素都是原数组中对应元素的两倍。
为什么要使用call()?
实际上,在大多数情况下,使用 [1, 2.3, 4, 5].map((item) => item * 2)
是更为简洁和直观的方式来实现对数组中每个元素进行操作的。这种方式使用了数组的原生 map
方法,并且通过箭头函数实现了对每个元素的操作,非常符合现代 JavaScript 的语法习惯,易读性也更好。
而使用 Array.prototype.map.call(numbers, doubleValue)
的方式,虽然也可以实现相同的功能,但通常只在特殊情况下才会使用,比如处理类数组对象时,或者需要对非数组对象使用数组方法时。在这种情况下,我们需要使用 call
方法来显式指定 map
方法的上下文,以及传递参数。
总的来说,在处理普通的数组时,推荐使用更简洁的 [1, 2.3, 4, 5].map((item) => item * 2)
的方式。
举一个类数组对象的例子
当处理类数组对象时,比如 arguments
对象,或者 DOM 元素列表(比如 querySelectorAll
返回的 NodeList 对象),由于它们并不是真正的数组,所以不能直接使用数组的方法。在这种情况下,就可以使用 call
方法来调用数组的方法,从而对类数组对象进行操作。
举一个处理类数组对象的例子,假设有一个类数组对象 arguments
,我们想要对其中的每个元素进行处理,可以使用 Array.prototype.map.call(arguments, callback)
:
js
function sum() {
// 使用 Array.prototype.map.call 将 arguments 对象转换为数组,然后对数组中的每个元素进行处理
const argsArray = Array.prototype.map.call(arguments, function(item) {
return item * 2;
});
// 对处理后的数组求和
const total = argsArray.reduce(function(acc, currentValue) {
return acc + currentValue;
}, 0);
return total;
}
console.log(sum(1, 2, 3)); // 输出 12,因为 1*2 + 2*2 + 3*2 = 12
在这个例子中,sum
函数接受任意数量的参数,并且对每个参数进行加倍处理,然后返回它们的总和。在函数内部,我们使用 Array.prototype.map.call(arguments, callback)
将 arguments
对象转换为数组,并对数组中的每个元素进行加倍处理。
如何⼿写 call()
call() :在使⽤⼀个指定的 this 值和若⼲个指定的参数值的前提下调⽤某个函数或⽅法。
js
let foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
注意两点:
- call 改变了 this 的指向,指向到 foo;
- bar 函数执⾏了;
第⼀步
上述⽅式等同于:
js
let foo = {
value: 1,
bar: function() {
console.log(this.value)
}
};
foo.bar(); // 1
这个时候 this 就指向了 foo,但是这样却给 foo 对象本身添加了⼀个属性,所以们⽤ delete 再删除它即可。
所以我们模拟的步骤可以分为:
- 将函数设为对象的属性;
- 执⾏该函数;
- 删除该函数;
以上个例⼦为例,就是:
js
// 第⼀步
// fn 是对象的属性名,反正最后也要删除它,所以起什么都可以。
foo.fn = bar
// 第⼆步
foo.fn()
// 第三步
delete foo.fn
根据上述思路,提供⼀版:
js
// 第⼀版
Function.prototype.call2 = function(context) {
// ⾸先要获取调⽤call的函数,⽤this可以获取
context.fn = this;
context.fn();
delete context.fn;
}
// 测试⼀下
let foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call2(foo); // 1
第⼆步
call除了可以指定this,还可以指定参数
js
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call(foo, 'kevin', 18);
// kevin
// 18
// 1
可以从 Arguments 对象中取值,取出第⼆个到最后⼀个参数,然后放到⼀个数组⾥。 上述代码的Arguments中取第⼆个到最后⼀个的参数
js
// 以上个例⼦为例,此时的arguments为:
// arguments = {
// 0: foo,
// 1: 'kevin',
// 2: 18,
// length: 3
// }
// 因为arguments是类数组对象,所以可以⽤for循环
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
// 执⾏后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"]
接下来使⽤eval拼接成⼀个函数
js
eval('context.fn(' + args +')')
考虑到⽬前⼤部分浏览器在console中限制eval的执⾏,也可以使⽤rest
此处代码为:
js
// 第⼆版
Function.prototype.call2 = function(context) {
context.fn = this;
let arg = [...arguments].slice(1)
context.fn(...arg)
delete context.fn;
}
// 测试⼀下
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call2(foo, 'kevin', 18);
// kevin
// 18
// 1
第三步
- this 参数可以传 null,当为 null 的时候,视为指向 window
举个例⼦:
js
var value = 1;
function bar() {
console.log(this.value);
}
bar.call(null); // 1
- 针对函数,可以实现返回值
js
var obj = {
value: 1
}
function bar(name, age) {
return {
value: this.value,
name: name,
age: age
}
}
console.log(bar.call(obj, 'kevin', 18));
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
这⾥
js
// 第三版
Function.prototype.call2 = function (context) {
// 如果传入的 context 是 null 或者 undefined,则默认为全局对象(非严格模式下为 window,严格模式下为 undefined)
var context = context || window;
// 将当前函数设为传入的 context 对象的方法
context.fn = this;
//使用 `slice(1)` 将 `arguments` 对象中除第一个参数(即 `context`)之外的参数提取出来,以获取 `call` 方法传入的参数列表。
let arg = [...arguments].slice(1)
// 调用当前函数,并传入参数
let result = context.fn(...arg)
// 删除添加的方法
delete context.fn
// 返回调用结果
return result
}
// 测试⼀下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar.call2(null); // 2
console.log(bar.call2(obj, 'kevin', 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
也可以写成
js
Function.prototype.myCall = function(context, ...args) {
// 如果传入的 context 是 null 或者 undefined,则默认为全局对象(非严格模式下为 window,严格模式下为 undefined)
context = context || window;
// 将当前函数设为传入的 context 对象的方法
context.fn = this;
// 调用当前函数,并传入参数
const result = context.fn(...args);
// 删除添加的方法
delete context.fn;
// 返回调用结果
return result;
};
// 测试
function greet(name) {
console.log(`Hello, ${name}! My name is ${this.name}.`);
}
const obj = {
name: 'Alice'
};
// 使用手写的 myCall 方法调用 greet 函数
greet.myCall(obj, 'Bob');
最简化的写法:
js
Function.prototype.call2 = function(context, ...args) {
// 判断是否是undefined和null
if (typeof context === 'undefined' || context === null) {
context = window
}
let fnSymbol = Symbol()
context[fnSymbol] = this
let fn = context[fnSymbol](...args)
delete context[fnSymbol]
return fn
}
- 首先,判断传入的
context
是否为undefined
或null
,如果是,则将context
设置为全局对象(非严格模式下为window
,严格模式下为undefined
)。 - 然后,创建一个唯一的 Symbol 作为临时属性的键,以避免和对象原有的属性名冲突。
- 将当前函数设置为
context
对象的临时属性。 - 调用函数,并传入参数。
- 最后,删除添加的临时属性,以避免对
context
对象造成污染,并返回调用结果。
这个方法的作用与原生的 call
方法一样,只不过用了一种更安全的方式来设置临时属性,确保不会污染原有的属性。