shift.call(arguments)和slice.call(arguments)的区别


前言

在 JavaScript 中,arguments 是一个类数组对象,它包含了函数调用时传入的所有参数。由于它不是一个真正的数组,所以不能直接调用数组的方法。但是,我们可以通过 call()apply() 方法来借用数组的方法,其中最常用的就是 Array.prototype.slice.call(arguments)Array.prototype.shift.call(arguments)

一、核心原理

1.1 call() 方法的作用

call() 方法允许我们调用一个函数,并指定函数执行时的 this 指向。它的语法如下:

function.call(thisArg, arg1, arg2, ...)
  • thisArg:函数执行时的 this
  • arg1, arg2, …:传递给函数的参数

1.2 类数组对象

类数组对象是指具有 length 属性和索引元素的对象,但不具备数组的方法。常见的类数组对象包括:

  • arguments 对象
  • DOM 元素集合(如 document.querySelectorAll() 返回的对象)
  • 字符串

1.3 类数组对象的标准

一个符合标准的类数组对象需要满足:

  • 具有 length 属性,且为非负整数
  • 具有索引访问能力(可以通过 obj[0], obj[1] 等方式访问)
  • 可选:具有 splice 方法(某些方法可能依赖)

二、slice.call(arguments) 详解

2.1 基本用法

function foo() {
    const args = Array.prototype.slice.call(arguments);
    console.log(args); // 输出: [1, 2, 3]
}

foo(1, 2, 3);

2.2 工作原理

Array.prototype.slice() 方法的工作原理是:

  1. 创建一个新的空数组
  2. 从原数组中复制指定范围的元素到新数组
  3. 返回新数组

当我们使用 call() 方法将 this 指向 arguments 时,slice() 方法会:

  1. 检查 this 对象是否有 length 属性
  2. 遍历 this 对象的索引元素
  3. 将这些元素复制到新数组中
  4. 返回新数组

2.3 简化写法

// 简化写法 1
const args = [].slice.call(arguments);

// 简化写法 2 (ES6+)
const args = Array.from(arguments);

// 简化写法 3 (ES6+)
const args = [...arguments];

2.4 slice 方法的内部实现

// 简化版 slice 实现
Array.prototype.slice = function (start, end) {
    const result = [];
    const length = this.length;
    start = start || 0;
    end = end === undefined ? length : end;

    for (let i = start; i < end && i < length; i++) {
        result.push(this[i]);
    }

    return result;
};

三、shift.call(arguments) 详解

3.1 基本用法

function foo() {
    const firstArg = Array.prototype.shift.call(arguments);
    console.log(firstArg); // 输出: 1
    console.log(arguments); // 输出: { '0': 2, '1': 3, length: 2 }
}

foo(1, 2, 3);

3.2 shift 方法的内部实现

// 简化版 shift 实现
Array.prototype.shift = function () {
    if (this.length === 0) {
        return undefined;
    }

    const first = this[0];

    // 将所有元素向前移动一位
    for (let i = 0; i < this.length - 1; i++) {
        this[i] = this[i + 1];
    }

    // 删除最后一个元素
    delete this[this.length - 1];
    // 更新 length 属性
    this.length--;

    return first;
};

3.3 工作原理

Array.prototype.shift() 方法的工作原理是:

  1. 检查数组是否为空,如果为空则返回 undefined
  2. 保存数组的第一个元素
  3. 将数组中的所有元素向前移动一位
  4. 更新数组的 length 属性
  5. 返回保存的第一个元素

当我们使用 call() 方法将 this 指向 arguments 时,shift() 方法会:

  1. 检查 arguments 对象是否为空
  2. 保存 arguments[0] 的值
  3. arguments 对象中的所有元素向前移动一位
  4. 更新 arguments 对象的 length 属性
  5. 返回保存的值

3.4 内部实现机制

// 简化版 shift 实现
Array.prototype.shift = function () {
    if (this.length === 0) {
        return undefined;
    }

    const first = this[0];

    for (let i = 0; i < this.length - 1; i++) {
        this[i] = this[i + 1];
    }

    delete this[this.length - 1];
    this.length--;

    return first;
};

四、两者的区别

特性 slice.call(arguments) shift.call(arguments)
返回值 新的数组,包含所有参数 第一个参数的值
对原对象的影响 不修改原 arguments 对象 修改原 arguments 对象(移除第一个元素)
用途 将类数组对象转为数组 获取并移除第一个参数
性能 较慢(需要创建新数组) 较快(直接修改原对象)

五、应用场景

5.1 slice.call(arguments) 的应用场景

5.1.1 将类数组对象转为数组

function sum() {
    const args = Array.prototype.slice.call(arguments);
    return args.reduce((acc, val) => acc + val, 0);
}

console.log(sum(1, 2, 3, 4, 5)); // 输出: 15

5.1.2 处理可变参数

function max() {
    const args = Array.prototype.slice.call(arguments);
    return Math.max.apply(null, args);
}

console.log(max(1, 5, 3, 9, 2)); // 输出: 9

5.1.3 实现函数重载

function greet() {
    const args = Array.prototype.slice.call(arguments);

    if (args.length === 0) {
        return "Hello!";
    } else if (args.length === 1) {
        return `Hello, ${args[0]}!`;
    } else {
        return `Hello, ${args.join(" and ")}!`;
    }
}

console.log(greet()); // 输出: Hello!
console.log(greet("John")); // 输出: Hello, John!
console.log(greet("John", "Jane")); // 输出: Hello, John and Jane!

5.2 shift.call(arguments) 的应用场景

5.2.1 提取第一个参数

function log(level) {
    const args = Array.prototype.slice.call(arguments);
    args.shift(); // 移除第一个参数(level)

    console.log(`[${level.toUpperCase()}]`, ...args);
}

log("info", "This is an info message"); // 输出: [INFO] This is an info message
log("error", "This is an error message"); // 输出: [ERROR] This is an error message

5.2.2 实现默认参数

function createUser() {
    const name = Array.prototype.shift.call(arguments) || "Anonymous";
    const age = Array.prototype.shift.call(arguments) || 18;
    const email = Array.prototype.shift.call(arguments) || "no-email@example.com";

    return { name, age, email };
}

console.log(createUser()); // 输出: { name: 'Anonymous', age: 18, email: 'no-email@example.com' }
console.log(createUser("John")); // 输出: { name: 'John', age: 18, email: 'no-email@example.com' }
console.log(createUser("John", 30)); // 输出: { name: 'John', age: 30, email: 'no-email@example.com' }

5.2.3 处理可选参数

function sendMessage() {
    const recipient = Array.prototype.shift.call(arguments);
    const message = Array.prototype.shift.call(arguments);
    const options = Array.prototype.shift.call(arguments) || {};

    console.log(`Sending message to ${recipient}: ${message}`, options);
}

sendMessage("John", "Hello!"); // 输出: Sending message to John: Hello! {}
sendMessage("John", "Hello!", { priority: "high" }); // 输出: Sending message to John: Hello! { priority: 'high' }

六、原型链分析

当我们调用 Array.prototype.slice.call(arguments) 时:

  1. 首先,获取 Array.prototype.slice 方法的引用
  2. 然后,调用该方法的 call() 方法,将 this 指向 arguments 对象
  3. 最后,slice 方法执行时,this 就是 arguments 对象,它会遍历 arguments 的元素并创建新数组

七、性能对比

7.1 性能测试

// 测试 slice.call(arguments) 的性能
function testSlice() {
    console.time("slice.call(arguments)");
    for (let i = 0; i < 1000000; i++) {
        (function () {
            const args = Array.prototype.slice.call(arguments);
        })(1, 2, 3, 4, 5);
    }
    console.timeEnd("slice.call(arguments)");
}

// 测试 shift.call(arguments) 的性能
function testShift() {
    console.time("shift.call(arguments)");
    for (let i = 0; i < 1000000; i++) {
        (function () {
            const first = Array.prototype.shift.call(arguments);
        })(1, 2, 3, 4, 5);
    }
    console.timeEnd("shift.call(arguments)");
}

// 测试 ES6 扩展运算符的性能
function testSpread() {
    console.time("spread operator");
    for (let i = 0; i < 1000000; i++) {
        (function () {
            const args = [...arguments];
        })(1, 2, 3, 4, 5);
    }
    console.timeEnd("spread operator");
}

// 测试 Array.from() 的性能
function testArrayFrom() {
    console.time("Array.from()");
    for (let i = 0; i < 1000000; i++) {
        (function () {
            const args = Array.from(arguments);
        })(1, 2, 3, 4, 5);
    }
    console.timeEnd("Array.from()");
}

testSlice();
testShift();
testSpread();
testArrayFrom();

7.2 性能结果分析

在大多数现代浏览器中,性能排序如下(从快到慢):

  1. shift.call(arguments) - 最快,因为它直接修改原对象,不需要创建新数组
  2. ES6 扩展运算符 ([...arguments]) - 非常快,现代 JavaScript 引擎对其进行了优化
  3. Array.from(arguments) - 较快,专门用于将类数组对象转为数组
  4. slice.call(arguments) - 较慢,因为它需要创建并填充新数组

7.3 内存使用分析

方法 内存占用 垃圾回收频率
slice.call(arguments) 高(创建新数组)
shift.call(arguments) 低(修改原对象)
扩展运算符
Array.from()

八、现代 JavaScript 中的替代方案

8.1 ES6 扩展运算符

function foo(...args) {
    console.log(args); // 输出: [1, 2, 3]
}

foo(1, 2, 3);

8.2 ES6 剩余参数

function foo(first, ...rest) {
    console.log(first); // 输出: 1
    console.log(rest); // 输出: [2, 3]
}

foo(1, 2, 3);

8.3 Array.from()

function foo() {
    const args = Array.from(arguments);
    console.log(args); // 输出: [1, 2, 3]
}

foo(1, 2, 3);

九、ES6+ 特性的深入应用

9.1 解构赋值与剩余参数

function processOptions(options = {}) {
    const { timeout = 3000, retries = 3, ...rest } = options;

    console.log("Timeout:", timeout);
    console.log("Retries:", retries);
    console.log("Rest:", rest);
}

processOptions({ timeout: 5000, retry: true, url: "https://api.example.com" });

9.2 默认参数值

// 替代传统的 shift 方式
function createUser(name = "Anonymous", age = 18, email = "no-email@example.com") {
    return { name, age, email };
}

console.log(createUser()); // 输出: { name: 'Anonymous', age: 18, email: 'no-email@example.com' }
console.log(createUser("John")); // 输出: { name: 'John', age: 18, email: 'no-email@example.com' }

9.3 生成器函数

// 处理无限参数的场景
function* processArguments() {
    for (let i = 0; i < arguments.length; i++) {
        yield arguments[i];
    }
}

const gen = processArguments(1, 2, 3, 4, 5);
for (const arg of gen) {
    console.log(arg); // 依次输出: 1, 2, 3, 4, 5
}

十、与其他数组方法的对比

10.1 splice.call(arguments)

function test() {
    // 移除前两个参数
    const removed = Array.prototype.splice.call(arguments, 0, 2);
    console.log("Removed:", removed); // 输出: [1, 2]
    console.log("Remaining:", arguments); // 输出: { '0': 3, '1': 4, length: 2 }
}

test(1, 2, 3, 4);

10.2 forEach.call(arguments)

function logAll() {
    Array.prototype.forEach.call(arguments, function (arg, index) {
        console.log(`Argument ${index}: ${arg}`);
    });
}

logAll("a", "b", "c");
// 输出:
// Argument 0: a
// Argument 1: b
// Argument 2: c

10.3 map.call(arguments)

function doubleAll() {
    const doubled = Array.prototype.map.call(arguments, function (arg) {
        return arg * 2;
    });
    console.log(doubled); // 输出: [2, 4, 6]
}

doubleAll(1, 2, 3);

十一、浏览器兼容性

方法 IE 6-8 IE 9+ Chrome Firefox Safari
Array.prototype.slice.call(arguments)
Array.prototype.shift.call(arguments)
ES6 扩展运算符
Array.from()

11.1 兼容性降级方案

// 兼容旧浏览器的类数组转数组方法
function toArray(obj) {
    if (Array.isArray(obj)) {
        return obj;
    }

    // 现代浏览器
    if (Array.from) {
        return Array.from(obj);
    }

    // 旧浏览器
    const result = [];
    for (let i = 0; i < obj.length; i++) {
        result.push(obj[i]);
    }
    return result;
}

十二、常见问题和注意事项

12.1 arguments 对象的特殊性

arguments 对象是一个特殊的对象,它的 length 属性是可写的,元素也是可写的。当使用 shift.call(arguments) 时,它会修改 arguments 对象的元素和 length 属性。

12.2 箭头函数中的 arguments

箭头函数没有自己的 arguments 对象,它会继承外层函数的 arguments 对象:

function outer() {
    const inner = () => {
        console.log(arguments); // 输出: outer 函数的 arguments
    };
    inner();
}

outer(1, 2, 3); // 输出: { '0': 1, '1': 2, '2': 3, length: 3 }

12.3 性能陷阱

在性能敏感的场景中,应避免在循环中使用 slice.call(arguments),因为它会创建新数组,增加内存使用和垃圾回收的压力。

十三、常见错误和调试技巧

13.1 常见错误案例

13.1.1 错误:在箭头函数中使用 arguments

// 错误示例
const arrowFunc = () => {
    console.log(arguments); // 不是预期的参数
};

// 正确做法
const arrowFunc = (...args) => {
    console.log(args); // 正确获取参数
};

13.1.2 错误:修改 arguments 后影响原函数

// 错误示例
function badExample() {
    Array.prototype.shift.call(arguments);
    // 后续代码中 arguments 已经被修改
    console.log(arguments.length); // 少了一个参数
}

// 正确做法
function goodExample() {
    const args = Array.prototype.slice.call(arguments);
    const first = args.shift();
    // arguments 保持不变
    console.log(arguments.length); // 原始长度
}

13.2 调试技巧

13.2.1 使用 console.table 查看 arguments

function debugArgs() {
    console.table(Array.prototype.slice.call(arguments));
}

debugArgs("a", 1, true, { key: "value" });

13.2.2 使用断点调试

在浏览器开发者工具中设置断点,观察 arguments 对象的变化:

  1. 在函数中添加 debugger; 语句
  2. 打开开发者工具
  3. 观察 arguments 对象的结构和变化

十四、最佳实践

  1. 在现代 JavaScript 中:优先使用 ES6 扩展运算符或剩余参数,它们语法更简洁,性能也更好。

  2. 需要修改原 arguments 对象:使用 shift.call(arguments) 来获取并移除第一个参数。

  3. 需要保持原 arguments 对象不变:使用 slice.call(arguments)Array.from(arguments) 或扩展运算符来创建新数组。

  4. 性能考虑

    • 对于频繁调用的函数,考虑使用更高效的方法
    • 对于参数数量较少的情况,性能差异可以忽略不计
    • 对于参数数量较多的情况,选择性能更好的方法

14.1 代码风格指南

  • 统一参数处理方式:在项目中统一使用 ES6 扩展运算符或剩余参数
  • 注释说明:对于使用传统技巧的代码,添加注释说明原因
  • 命名规范:使用清晰的变量名,如 args 表示参数数组

14.2 团队协作建议

  • 制定编码规范:明确项目中参数处理的标准方式
  • 代码审查:在代码审查时关注参数处理的正确性和性能
  • 文档化:记录项目中使用的参数处理模式

14.3 性能敏感场景

  • 高频调用函数:使用更高效的参数处理方式
  • 大量参数:避免创建不必要的数组副本
  • 内存受限环境:优先使用修改原对象的方法

14.4 降级方案

function normalizeArgs() {
    const args = Array.prototype.slice.call(arguments);

    if (args.length === 1 && Array.isArray(args[0])) {
        return args[0];
    }

    return args;
}

console.log(normalizeArgs(1, 2, 3));
console.log(normalizeArgs([1, 2, 3]));

十五、扩展知识

15.1 arguments 对象的其他特性

15.1.1 arguments.callee

// 递归函数中使用
function factorial(n) {
    if (n <= 1) return 1;
    return n * arguments.callee(n - 1);
}

console.log(factorial(5)); // 输出: 120

15.1.2 arguments.caller

// 查看调用当前函数的函数
function inner() {
    console.log(arguments.caller);
}

function outer() {
    inner();
}

outer(); // 输出: outer 函数的代码

15.2 严格模式下的 arguments

在严格模式下:

  • arguments.calleearguments.caller 会抛出错误
  • arguments 对象与函数参数不再同步
"use strict";

function strictFunc(a) {
    console.log(arguments[0]); // 输出: 1
    a = 2;
    console.log(arguments[0]); // 仍然输出: 1,不再同步
}

strictFunc(1);

15.3 rest 参数与 arguments 的区别

特性 rest 参数 arguments
类型 真正的数组 类数组对象
可变性 不可变(是副本) 可变(修改会影响原函数)
作用域 仅在函数体内 仅在函数体内
ES6+ 支持 传统特性
性能 较好 较差(类数组操作)

十六、性能优化的具体数据

16.1 详细性能测试

// 测试不同参数数量下的性能
function testPerformance() {
    const testCases = [1, 10, 100, 1000, 10000];

    testCases.forEach(size => {
        console.log(`\nTesting with ${size} arguments:`);

        // 准备参数
        const args = Array.from({ length: size }, (_, i) => i);

        // 测试 slice.call
        console.time(`slice.call (${size})`);
        for (let i = 0; i < 10000; i++) {
            (function () {
                const result = Array.prototype.slice.call(arguments);
            }).apply(null, args);
        }
        console.timeEnd(`slice.call (${size})`);

        // 测试扩展运算符
        console.time(`spread (${size})`);
        for (let i = 0; i < 10000; i++) {
            (function (...args) {
                const result = args;
            }).apply(null, args);
        }
        console.timeEnd(`spread (${size})`);
    });
}

testPerformance();

十七、更多实际应用场景

17.1 在库和框架中的应用

17.1.1 jQuery 中的参数处理

// jQuery 如何处理参数
function jQuery(selector, context) {
    // 处理参数
    if (context) {
        selector = context.querySelector(selector);
    } else {
        selector = document.querySelector(selector);
    }
    // ...
}

// 事件处理函数中的参数处理
function on(eventName, handler) {
    const args = Array.prototype.slice.call(arguments, 2);
    // 处理额外参数
    // ...
}

17.1.2 函数柯里化实现

function curry(fn) {
    const args = Array.prototype.slice.call(arguments, 1);

    return function () {
        const newArgs = args.concat(Array.prototype.slice.call(arguments));
        return fn.apply(this, newArgs);
    };
}

// 使用示例
function add(a, b, c) {
    return a + b + c;
}

const add5 = curry(add, 5);
console.log(add5(1, 2)); // 输出: 8

17.2 参数预处理

17.2.1 统一参数格式

function normalizeArgs() {
    const args = Array.prototype.slice.call(arguments);

    // 处理不同类型的参数
    if (args.length === 1 && Array.isArray(args[0])) {
        return args[0];
    }

    return args;
}

// 使用示例
console.log(normalizeArgs(1, 2, 3)); // 输出: [1, 2, 3]
console.log(normalizeArgs([1, 2, 3])); // 输出: [1, 2, 3]

十八、实际场景代码示例

18.1 事件处理函数

// 处理事件监听器的参数
function addEventListener(element, event, handler) {
    const args = Array.prototype.slice.call(arguments, 3);

    element.addEventListener(event, function (e) {
        handler.apply(this, [e].concat(args));
    });
}

// 使用示例
const button = document.querySelector("button");
addEventListener(
    button,
    "click",
    function (e, message) {
        console.log(message); // 输出: Hello
    },
    "Hello"
);

18.2 错误处理

// 带错误处理的参数处理
function safeProcess() {
    try {
        const args = Array.prototype.slice.call(arguments);
        // 处理参数
    } catch (error) {
        console.error("参数处理错误:", error);
        return [];
    }
}

18.3 边界情况处理

// 处理空参数和 undefined 参数
function processArgs() {
    const args = Array.prototype.slice.call(arguments).filter(arg => arg !== undefined);

    if (args.length === 0) {
        return "No arguments provided";
    }

    return args;
}

console.log(processArgs()); // 输出: No arguments provided
console.log(processArgs(1, undefined, 3)); // 输出: [1, 3]

十九、总结

shift.call(arguments)slice.call(arguments) 都是 JavaScript 中常用的技巧,它们利用 call() 方法来借用数组的方法,处理类数组对象。

  • slice.call(arguments) 用于将类数组对象转为数组,不修改原对象
  • shift.call(arguments) 用于获取并移除第一个参数,会修改原对象

在现代 JavaScript 中,我们可以使用 ES6 扩展运算符、剩余参数和 Array.from() 等更简洁、更高效的方法来替代这些技巧。但是,了解这些传统技巧的原理和应用场景,对于理解 JavaScript 的原型链和函数调用机制仍然非常重要。

参考资料


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录