详解call()

什么是call()?

在 JavaScript 中,call 是函数对象的一个方法,用于在特定的作用域(上下文)中调用函数。call 方法允许你指定函数运行时的 this 值,并且可以传入一组参数作为函数的参数。

语法如下:

js 复制代码
functionName.call(thisArg, arg1, arg2, ...)

其中:

  • functionName 是要调用的函数名。
  • thisArg 是函数执行时的上下文对象,在函数内部通过 this 关键字访问。如果将 thisArg 设置为 nullundefined,则在严格模式下 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

注意两点:

  1. call 改变了 this 的指向,指向到 foo;
  2. bar 函数执⾏了;

第⼀步

上述⽅式等同于:

js 复制代码
let foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1

这个时候 this 就指向了 foo,但是这样却给 foo 对象本身添加了⼀个属性,所以们⽤ delete 再删除它即可。

所以我们模拟的步骤可以分为:

  1. 将函数设为对象的属性;
  2. 执⾏该函数;
  3. 删除该函数;

以上个例⼦为例,就是:

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

第三步

  1. this 参数可以传 null,当为 null 的时候,视为指向 window

举个例⼦:

js 复制代码
var value = 1;

function bar() {
    console.log(this.value);
}

bar.call(null); // 1
  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 是否为 undefinednull,如果是,则将 context 设置为全局对象(非严格模式下为 window,严格模式下为 undefined)。
  • 然后,创建一个唯一的 Symbol 作为临时属性的键,以避免和对象原有的属性名冲突。
  • 将当前函数设置为 context 对象的临时属性。
  • 调用函数,并传入参数。
  • 最后,删除添加的临时属性,以避免对 context 对象造成污染,并返回调用结果。

这个方法的作用与原生的 call 方法一样,只不过用了一种更安全的方式来设置临时属性,确保不会污染原有的属性。

相关推荐
四喜花露水30 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy39 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
工业甲酰苯胺1 小时前
C# 单例模式的多种实现
javascript·单例模式·c#
程序员爱技术5 小时前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js
悦涵仙子6 小时前
CSS中的变量应用——:root,Sass变量,JavaScript中使用Sass变量
javascript·css·sass
兔老大的胡萝卜6 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
cs_dn_Jie10 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic10 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿10 小时前
webWorker基本用法
前端·javascript·vue.js