JS - 包装数组

0 阅读9分钟

题目描述

创建一个名为 ArrayWrapper 的类,它在其构造函数中接受一个整数数组作为参数。该类应具有以下两个特性:

  • 当使用 + 运算符将两个该类的实例相加时,结果值为两个数组中所有元素的总和。
  • 当在实例上调用 String() 函数时,它将返回一个由逗号分隔的括在方括号中的字符串。例如,[1,2,3] 。

 

示例 1:

输入: nums = [[1,2],[3,4]], operation = "Add"
输出: 10
解释:
const obj1 = new ArrayWrapper([1,2]);
const obj2 = new ArrayWrapper([3,4]);
obj1 + obj2; // 10

示例 2:

输入: nums = [[23,98,42,70]], operation = "String"
输出: "[23,98,42,70]"
解释:
const obj = new ArrayWrapper([23,98,42,70]);
String(obj); // "[23,98,42,70]"

示例 3:

输入: nums = [[],[]], operation = "Add"
输出: 0
解释:
const obj1 = new ArrayWrapper([]);
const obj2 = new ArrayWrapper([]);
obj1 + obj2; // 0

题解

这个问题引入了 JavaScript 编程中一个有趣的方面:修改 JavaScript 的加法运算符(+)和 String() 函数的标准行为,以模仿其他编程语言中的行为。例如,在 Python 中,可以直接添加数组,这是 JavaScript 不原生支持的功能。然而,通过正确的逻辑,这个功能可以在 JavaScript 中复制,这是这个问题的关键。这个挑战还为我们提供了一个探索 JavaScript 操作符重载能力和数据类型转换的机会。

为了理解问题及其解决方法,首先要理解我们将在 JavaScript 实现中使用的关键概念和技术,这些包括函数、数组,以及理解 JavaScript 中操作符重载和数据类型转换的工作方式。

JavaScript 操作符重载

操作符重载是许多编程语言中的一个功能,它允许单个运算符根据其操作数的不同而具有不同的行为。例如,在 Python 或 C++ 中,可以直接定义或修改诸如 + 或 == 之类的运算符的行为,以实现诸如使用 + 运算符进行向量加法之类的功能。

然而,在 JavaScript 中,这种级别的操作符重载是不可能的。重要的是要理解,无法以与 Python 或 C++ 中相同的方式在 JavaScript 中复制使用 + 运算符的向量添加等行为。

尽管如此,JavaScript 提供了特定的方法,具体来说是 valueOf() 和 toString(),它们影响对象与操作符的交互方式。这些方法是 JavaScript 的类型转换机制的一部分,可以在自定义对象中重写,以实现一定程度的操作符行为修改。

以下是使用这些方法的示例:

let obj = {
  valueOf: function() {
    return 5;
  },
  toString: function() {
    return 'Hello';
  }
};

console.log(obj + 2); // 7 - 由于 obj.valueOf()
console.log(String(obj)); // 'Hello' - 由于 obj.toString()

这些方法提供了一种影响 JavaScript 内置类型转换的机制,但它们不允许像 Python 或 C++ 中那样进行全面的操作符重载。

JavaScript String() 函数

JavaScript 中的 String() 函数是一个全局对象构造函数,用于将对象的字符串表示形式转换并返回。它可以用于将所有类型的 JavaScript 数据类型转换为字符串。例如,String(10) 会返回 '10',String(true) 会返回 'true'。

以下是如何使用 String() 函数的示例:

let num = 10;
let bool = true;

console.log(String(num)); // '10'
console.log(String(bool)); // 'true'

在 JavaScript 中,我们还可以通过在对象上提供自定义的 toString() 方法来修改 String() 的行为。这允许我们控制对象如何表示为字符串。然而,虽然我们可以使用 JavaScript 的内部方法在一定程度上修改 + 运算符和 String() 函数的行为,但在 JavaScript 中创建一个模拟数组加法和字符串转换(如在 Python 中)的机制需要一些创造性的解决方法,考虑到语言设计的限制。

JavaScript toString() 方法

在 JavaScript 中,toString() 方法返回表示对象的字符串。当需要将对象显示为字符串时(例如在字符串连接操作中),JavaScript 会自动调用此方法。

你还可以为对象定义自定义的 toString() 方法。通常在处理复杂对象时这么做,因为它允许你控制对象作为字符串的表示方式。

// 为 Person 定义构造函数
function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

// 向 Person 原型添加 toString 方法
Person.prototype.toString = function() {
  return this.firstName + ' ' + this.lastName;
};

// 创建 Person 的实例
var personInstance = new Person('John', 'Doe');

console.log('Hello, ' + personInstance); // 将输出 "Hello, John Doe"

在这个示例中,首先定义了一个名为 Person 的构造函数,它接受两个参数:firstName 和 lastName。然后,向 Person 原型添加了一个 toString() 方法,该方法以 "firstName lastName" 的格式返回一个字符串。当我们使用 + 运算符将 personInstance 与字符串连接时,JavaScript 会自动在 personInstance 上调用 toString() 方法。因此,'Hello, ' + personInstance 会输出 "Hello, John Doe",因为自定义的 toString() 方法控制了 Person 对象在字符串上下文中的表示方式。这演示了如何重写 toString() 方法以确定对象在字符串上下文中的表示方式。

JavaScript valueOf() 方法

JavaScript 中的 valueOf() 方法是一个内置函数,它返回指定对象的原始值。默认情况下,所有继承自 Object 的对象都继承了 valueOf() 方法。此方法可用于 NumberBooleanObjectString 和 Date 对象。

当 JavaScript 尝试将对象转换为原始值(例如,在数学运算中),它首先在对象上调用 valueOf() 方法。如果 valueOf() 不返回原始值,JavaScript 将继续调用 toString() 方法。因此,通过覆盖 valueOf() 方法,你可以控制对象在数学运算中的行为,从而在一定程度上模拟操作符重载。

// 为 MyNumber 定义构造函数
function MyNumber(number) {
  this.number = number;
}

// 向 MyNumber 原型添加 valueOf 方法
MyNumber.prototype.valueOf = function() {
  return this.number * 2;
};

// 创建 MyNumber 的实例
var myNumberInstance = new MyNumber(5);

console.log(myNumberInstance + 1); // 将输出 11,而不是 6

在上面的代码中,我们定义了一个名为 MyNumber 的构造函数,并向 MyNumber 原型添加了一个 valueOf() 方法。这个方法将返回两倍的数字。然后,我们创建了一个值为 5 的 MyNumber 实例。当尝试将 1 添加到这个实例时,JavaScript 首先调用 myNumberInstance 上的 valueOf() 方法。由于我们已覆盖 valueOf() 来返回两倍的数字,myNumberInstance.valueOf() 的值是 10。因此,myNumberInstance + 1 等于 11,而不是 6。这演示了如何覆盖 valueOf() 方法以影响对象在数学运算中的行为。尽管这不是真正的操作符重载(因为我们实际上没有改变 + 运算符的工作方式),但在许多情况下,它可以模拟类似的结果。

JavaScript 原型

JavaScript 经常被描述为一种基于原型的语言,这意味着 JavaScript 中的继承是通过原型系统实现的。每个 JavaScript 对象都有一个链接到另一个对象(其原型)的链接。在尝试访问对象中不存在的属性时,JavaScript 会尝试在对象的原型中查找此属性,如果仍然找不到,则继续查找原型的原型,直到达到一个具有 null 原型的对象。

原型对象本身也有一个原型,大多数标准对象的原型通常是 Object.prototype。但是,也有一些例外,比如纯粹的 JavaScript 对象(POJO),特别是使用 Object.create(null) 创建的对象。这种结构构成了所谓的原型链。当 JavaScript 尝试访问对象的属性时,它首先检查对象本身。如果没有在对象本身找到属性,它会向上查找原型链。这个过程会一直持续下去,直到找到属性或达到一个具有 null 原型的对象。

修改 JavaScript 原型的示例:

function Vehicle(name) {
  this.name = name;
}

Vehicle.prototype.getName = function() {
  return this.name;
};

let vehicle = new Vehicle('Car');
console.log(vehicle.getName()); // 'Car'

getName 函数不是 vehicle 对象本身的属性,而是它的原型属性。当我们调用 vehicle.getName() 时,JavaScript 首先查找 vehicle 对象中的 getName,然后在那里找不到时,它会移动到 Vehicle.prototype,找到并执行该函数。

方法 1:在 ArrayWrapper 类中实现 valueOf 和 toString 方法

概述

我们需要创建一个名为 ArrayWrapper 的类,它接受一个数组作为输入,并具有两个方法:valueOf() 和 toString()valueOf() 方法应返回数组所有元素的和,toString() 方法应返回数组的字符串表示。这可以通过在类中定义所需的方法来实现。在方法内部,this.nums 将引用传递给类构造函数的数组。

算法

  1. 在类构造函数中,存储作为参数传递的数组。
  2. 定义一个 valueOf 方法,用于计算并返回数组元素的和。
  3. 定义一个 toString 方法,用于返回数组的字符串表示。

实现

这种方法可以以不同的方式实现。

实现 1:在 valueOf() 方法中使用 reduce

JavaScript

var ArrayWrapper = function(nums) {
    this.nums = nums;
};

ArrayWrapper.prototype.valueOf = function() {
    return this.nums.reduce((a, b) => a + b, 0);
}

ArrayWrapper.prototype.toString = function() {
    return "[" + this.nums.join(',') + "]";
}

valueOf 方法利用数组的 reduce 函数来计算所有数组元素的总和。如果数组为空,reduce 函数会默认使用初始值 0

toString 方法则返回数组的字符串表示。这是通过用逗号连接所有元素并将结果字符串包装在方括号中来完成的。

实现 2:在 valueOf() 方法中使用 for 循环

JavaScript

var ArrayWrapper = function(nums) {
    this.nums = nums;
};

ArrayWrapper.prototype.valueOf = function() {
    let sum = 0;
    for (let i = 0; i < this.nums.length; i++) {
        sum += this.nums[i];
    }
    return sum;
}

ArrayWrapper.prototype.toString = function() {
    return "[" + this.nums.join(',') + "]";
}

在这个实现中,我们将 valueOf() 中的 reduce 方法替换为传统的 for 循环。这对于初学者来说可能更容易理解,尽管它更加冗长。

实现 3:在 Array 中使用 toString() 方法

JavaScript

var ArrayWrapper = function(nums) {
    this.nums = nums;
};

ArrayWrapper.prototype.valueOf = function() {
    return this.nums.reduce((a, b) => a + b, 0);
};

ArrayWrapper.prototype.toString = function() {
    return '[' + this.nums.toString() + ']';
};

在这个实现中,我们将 toString() 方法中的手动字符串构建替换为 JavaScript 的数组内置 toString() 方法。这个方法会自动用逗号连接元素。

实现 4:在 toString() 方法中使用模板字符串

JavaScript

var ArrayWrapper = function(nums) {
    this.nums = nums;
};

ArrayWrapper.prototype.valueOf = function() {
    return this.nums.reduce((a, b) => a + b, 0);
}

ArrayWrapper.prototype.toString = function() {
    return `[${this.nums}]`;
}

在这个实现中,我们将 toString() 方法中的手动字符串构建替换为 JavaScript 的内置模板字面量。这允许更容易地格式化字符串,而在这种情况下,它将 nums 数组嵌入到方括号中。

实现 5:在 toString() 方法中使用 JSON.stringify

JavaScript

var ArrayWrapper = function(nums) {
    this.nums = nums;
};

ArrayWrapper.prototype.valueOf = function() {
    return this.nums.reduce((a, b) => a + b, 0);
}

ArrayWrapper.prototype.toString = function() {
    return JSON

.stringify(this.nums);
}

在这个实现中,我们使用 JSON.stringify 将 nums 数组转换为字符串。这个方法会自动在数组周围添加方括号,并用逗号分隔元素。这是将数组转换为字符串的简洁方法。然而,对于这个特定任务,JSON.stringify 可能不是最佳选择。它设计用于处理复杂的 JSON 对象,因此需要检查和处理接收到的数据结构的每个部分。当用于更简单的数据结构如数组时,这些全面的检查和处理可能会使其比专门设计来处理这种结构的其他方法慢。

实现 6:ES6 类语法

JavaScript

class ArrayWrapper {
    constructor(nums) {
        this.nums = nums;
    }

    valueOf() {
        return this.nums.reduce((a, b) => a + b, 0);
    }

    toString() {
        return `[${this.nums}]`;
    }
}

ES6 类提供了更简单的语法来创建类,包括简化的方法定义和构造函数处理。

复杂度分析

  • valueOf 方法的时间复杂度:O(N),其中 N 是数组的长度。这是因为我们需要遍历数组的所有元素来计算总和。
  • toString 方法的时间复杂度:O(N),其中 N 是数组的长度。这是因为将字符串数组连接涉及迭代所有字符串。
  • 两种方法的空间复杂度:O(1)。我们只使用了一个固定的额外空间来存储总和或生成的字符串。输入数组不计入空间复杂度,因为它不是随输入大小增加的额外空间。