JavaScript对象创建与类数组对象处理学习笔记

32 阅读6分钟

JavaScript对象创建与类数组对象处理学习笔记

一、JavaScript中new运算符详解

1.1 new运算符的基本概念

new运算符是JavaScript中用于创建对象实例的关键操作符。它允许我们基于自定义的构造函数创建对象实例,是JavaScript原型式面向对象编程的核心机制之一。当使用new调用一个函数时,该函数会被当作构造函数执行,创建并返回一个新对象。

1.2 new运算符的作用

new运算符的主要作用体现在三个方面:首先,它创建了一个新的空对象;其次,它将新创建对象的原型链指向构造函数的prototype属性;最后,它执行构造函数,将新对象作为this的上下文,并返回该新对象。通过这种机制,我们可以实现对象的初始化和原型继承,从而构建复杂的面向对象结构。

1.3 new运算符的实例化过程

new运算符的实例化过程可以分解为以下四个关键步骤 :

步骤1:创建空对象

var obj = new Object(); // 创建一个空对象

步骤2:设置原型链

// 将新对象的[[Prototype]]指向构造函数的prototype属性
obj.__proto__ =Constructor.prototype;

步骤3:绑定this执行构造函数

// 使用apply方法将构造函数的this指向新对象,并传入参数
Constructor.apply(obj, args);

步骤4:返回新对象

// 如果构造函数返回非原始值,则返回该值,否则返回新对象
return (Constructor返回的对象 || obj);

这四个步骤共同构成了new运算符的完整工作流程,使我们能够通过构造函数创建具有特定属性和方法的对象实例。

二、手写new函数的实现原理

2.1 手写new函数的动机

理解new运算符的工作原理,有助于我们更好地掌握JavaScript的面向对象机制。通过手写一个模拟new功能的函数,我们可以直观地看到对象实例化过程中的每一步操作,从而深入理解原型链和构造函数的执行机制。

2.2 手写new函数的实现

根据上述new运算符的实例化过程,我们可以实现一个模拟new功能的函数:

// 方法1:使用剩余参数语法
function ObjectFactory(Constructor, ...args) {
    var obj = {}; // 创建空对象
   Constructor.apply(obj, args); // 绑定this执行构造函数
   obj.__proto__ =Constructor.prototype; // 设置原型链
    return obj; // 返回新对象
}

// 方法2:不使用剩余参数
function objectFactory() {
    var obj = new Object(); // 创建空对象
    varConstructor = [].shift.call(arguments); // 获取构造函数
   Constructor.apply(obj, [...arguments]); // 绑定this执行构造函数
    obj.__proto__ =Constructor.prototype; // 设置原型链
    return obj; // 返回新对象
}

2.3 实际应用示例

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype物种 = '人类';
Person.prototype.打招呼 = function() {
    console.log(`你好,我是${this.name}`);
};

// 使用原生new
let p1 = new Person('张三', 25);

// 使用手写ObjectFactory
let p2 = ObjectFactory(Person, '李四', 30);

// 使用手写objectFactory
let p3 = objectFactory(Person, '王五', 35);

console.log(p1, p2, p3); // 三个对象实例
console.log(p2.年龄, p2.物种); // 输出:30 '人类'
console.log(p3.年龄, p3.物种); // 输出:35 '人类'

在这个例子中,我们通过手写new函数创建了Person对象的实例,这些实例与使用原生new创建的对象具有相同的属性和方法,证明了我们对手写new函数实现的理解。

三、类数组对象(Arguments)详解

3.1 类数组对象的定义

类数组对象是一种特殊的数据结构,它满足两个核心条件:

  1. 具有length属性,表示元素的数量
  2. 具有从0开始的数字索引属性,可以通过索引访问元素

虽然类数组对象在结构上与数组相似,但它并不是数组类型,而是对象类型。因此,它不能直接使用数组的方法,如mapreducejoin等 。

3.2 arguments对象的特点

在JavaScript中,函数运行时的参数对象arguments是一个典型的类数组对象 :

function add() {
    // arguments是一个类数组对象
    console.log(Object.prototype.toString.call(arguments)); 
    // 输出:[object Arguments]
}

arguments对象具有以下特点:

  • 动态参数:可以接收任意数量的参数
  • 访问参数:通过索引arguments[i]访问参数
  • 具有length属性:表示传递给函数的参数数量
  • 可迭代:在ES6中可以使用扩展运算符或for...of循环

3.3 arguments对象与数组的区别

虽然arguments对象看起来像数组,但它与数组有本质区别:

特性数组arguments对象
类型Array类型Arguments类型
原型继承自Array.prototype继承自Arguments.prototype
方法支持所有数组方法不支持数组方法(如map、reduce)
可修改可以动态增删元素参数数量固定,不可直接修改
遍历支持数组遍历方法需要特殊处理才能遍历

这种区别导致我们不能直接对arguments对象使用数组方法,需要先将其转换为真正的数组。

四、类数组对象的常见场景

4.1 函数参数集合

最典型的类数组对象是函数内部的arguments对象,它包含了传递给函数的所有参数:

function sum() {
    let total = 0;
    for (let i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    return total;
}

console.log(sum(1, 2, 3)); // 输出:6

4.2 DOM查询结果

DOM操作中返回的集合对象,如document.querySelectorAll()返回的NodeList,也是类数组对象:

const elements = document.querySelectorAll('.box');
console.log(elements.length); // 输出匹配元素的数量
console.log(elements[0]); // 输出第一个匹配的元素
// elements.push()会报错,因为NodeList不是数组

4.3 HTML集合

通过getElementsByClassNamegetElementsByTagName等方法获取的HTMLCollection也是类数组对象:

const boxes = document.getElementsByClassName('box');
console.log(boxes.length); // 输出匹配元素的数量
console.log(boxes[0]); // 输出第一个匹配的元素
// boxes.map()会报错,因为HTMLCollection不是数组

这些场景中的类数组对象都具有length属性和数字索引,但无法直接使用数组方法,需要先转换为数组。

五、类数组对象转换为数组的方法

5.1 使用Array.prototype.slice.call()

这是最经典且兼容性最好的转换方法,适用于所有JavaScript环境:

function add() {
    const args = Array.prototype.slice.call(arguments);
    console.log(args, Object.prototype.toString.call(args));
    // 输出:[1, 2, 3] [object Array]
    return args.reduce((pre, cur) => pre + cur, 0);
}

console.log(add(1, 2, 3)); // 输出:6

原理:借用数组的slice方法,从索引0开始"切片"整个类数组 。

优点

  • 兼容性好,支持所有浏览器
  • 保留稀疏数组的结构

缺点

  • 代码相对冗长
  • 需要手动调用

适用场景:需要兼容旧版浏览器的项目,或处理稀疏类数组的场景。

5.2 使用Array.prototype.splice.call()

另一种借用数组方法的方式,但需注意其副作用:

function add() {
    const args = Array.prototype.splice.call(arguments, 0);
    console.log(args, Object.prototype.toString.call(args));
    // 输出:[1, 2, 3] [object Array]
    return args.reduce((pre, cur) => pre + cur, 0);
}

console.log(add(1, 2, 3)); // 输出:6

原理:使用数组的splice方法从索引0开始删除所有元素并返回 。

优点

  • 代码简洁
  • 直接返回数组

缺点

  • 副作用:会清空原始类数组对象
  • 不推荐用于需要保留原对象的场景

适用场景:在不需要保留原始类数组的情况下快速转换,或需要删除并获取元素的场景。

5.3 使用Array.prototype.concat.apply()

通过数组的concat方法将类数组合并到空数组中:

function add() {
    const args = Array.prototype.concat.apply([], arguments);
    console.log(args, Object.prototype.toString.call(args));
    // 输出:[1, 2, 3] [object Array]
    return args.reduce((pre, cur) => pre + cur, 0);
}

console.log(add(1, 2, 3)); // 输出:6

原理:将空数组与类数组合并,形成新数组 。

优点

  • 代码简洁
  • 兼容性好

缺点

  • 对于大型类数组可能有性能问题
  • 不支持稀疏数组的转换

适用场景:需要兼容旧环境且类数组不是特别大的场景。

5.4 使用Array.from()

ES6提供的专门用于将类数组和可迭代对象转换为数组的方法:

function add() {
    const args = Array.from(arguments);
    console.log(args, Object.prototype.toString.call(args));
    // 输出:[1, 2, 3] [object Array]
    return args.reduce((pre, cur) => pre + cur, 0);
}

console.log(add(1, 2, 3)); // 输出:6

原理:ES6新增方法,专为类数组和可迭代对象设计 。

优点

  • 语法简洁,语义清晰
  • 支持映射函数,可以转换元素
  • 自动压缩稀疏数组的空槽

缺点

  • 不支持ES5及以下环境
  • 需要环境支持ES6

适用场景:现代JavaScript项目,需要简洁代码和转换功能的场景。

5.5 使用扩展运算符[...arrayLike]

ES6提供的最简洁的转换方式:

function add() {
    const args = [...arguments];
    console.log(args, Object.prototype.toString.call(args));
    // 输出:[1, 2, 3] [object Array]
    return args.reduce((pre, cur) => pre + cur, 0);
}

console.log(add(1, 2, 3)); // 输出:6

原理:使用ES6扩展运算符,将类数组展开为数组 。

优点

  • 代码最简洁
  • 可与其他数组操作链式调用
  • 支持可迭代对象

缺点

  • 不支持ES5及以下环境
  • 依赖环境支持迭代器
  • 对稀疏数组的处理与Array.from相同

适用场景:现代JavaScript项目,追求代码简洁性的场景。

六、不同转换方法的比较与选择

6.1 转换方法对比

以下是不同转换方法的对比:

方法兼容性代码简洁性副作用稀疏数组处理是否支持映射
slice.call优秀一般保留空槽不支持
splice.call优秀一般保留空槽不支持
concat.apply优秀较好不保留空槽不支持
Array.from良好很好压缩空槽支持
扩展运算符良好最好压缩空槽不支持

6.2 根据场景选择合适方法

兼容性优先场景:如果项目需要支持旧版浏览器(如IE),应选择slice.callconcat.apply方法。

// 兼容旧版浏览器的转换
function sum() {
    const args = Array.prototype.slice.call(arguments);
    return args.reduce((pre, cur) => pre + cur, 0);
}

现代项目场景:在支持ES6的环境中,推荐使用Array.from或扩展运算符,它们语法简洁且功能强大。

// 现代ES6转换
function sum() {
    const args = [...arguments];
    return args.reduce((pre, cur) => pre + cur, 0);
}

需要保留稀疏结构场景:如果类数组是稀疏的(即某些索引没有值),且需要保留这种结构,应选择slice.call方法。

// 保留稀疏结构
const sparseArgs = {0:1, 2:3, length:3};
const sparseArray = Array.prototype.slice.call(sparseArgs);
console.log(sparseArray); // 输出:[1, empty, 3]

需要映射处理场景:如果转换时需要对元素进行处理,应选择Array.from方法,它支持映射函数。

// 带映射的转换
function uppercaseNames() {
    const names = Array.from(arguments, name => name.toUpperCase());
    return names.join(', ');
}

console.log(uppercaseNames('张三', '李四', '王五')); // 输出:Zhang San, Li Si, Wang Wu

七、类数组对象的迭代与访问

7.1 使用for循环访问类数组

由于类数组具有数字索引和length属性,可以使用传统的for循环访问其元素:

function logArgs() {
    for (let i = 0; i < arguments.length; i++) {
        console.log(`参数${i}: ${arguments[i]}`);
    }
}

logArgs(1, '二', true); // 输出三个参数

7.2 使用for...in循环遍历类数组

for...in循环可以遍历对象的所有可枚举属性,包括类数组的索引属性:

function logArgs() {
    for (let key in arguments) {
        if (typeof key === 'number' && key < arguments.length) {
            console.log(`参数${key}: ${arguments[key]}`);
        }
    }
}

logArgs(1, '二', true); // 输出三个参数

但需要注意,for...in会遍历所有可枚举属性,包括可能添加的非索引属性,因此需要添加条件判断。

7.3 使用forEach方法需要先转换为数组

由于类数组对象没有数组方法,需要先转换为数组才能使用forEach等方法:

function logArgs() {
    [...arguments].forEach((arg, index) => {
        console.log(`参数${index}: ${arg}`);
    });
}

logArgs(1, '二', true); // 输出三个参数

八、类数组对象的动态特性与限制

8.1 类数组对象的动态性

类数组对象在创建后,其参数数量通常是固定的,不能像数组那样动态增删元素:

function test() {
    arguments.push(4); // 报错:TypeError: arguments.push is not a function
    arguments.pop(); // 报错:TypeError: arguments.pop is not a function
}

test(1, 2, 3);

8.2 类数组对象的限制

类数组对象有以下主要限制:

  • 不能直接使用数组方法(如mapreducejoin等)
  • 不能使用数组的展开语法(ES6之前的环境)
  • 不能使用数组的迭代器方法(如for...of循环)

这些限制使得在处理类数组对象时,往往需要先将其转换为真正的数组,才能进行更复杂的操作。

8.3 类数组对象的其他特性

除了基本的索引访问和length属性外,类数组对象还有一些其他特性:

  • 具有沧'callee属性,指向被调用的函数
  • 具有沧'callers属性(某些环境支持),指向调用栈
  • 在函数内部,可以通过沧'arguments访问参数集合

这些特性使得arguments对象在函数参数处理中具有特殊地位,但也增加了其与普通数组的区别。

九、原型链与对象继承机制

9.1 原型链的概念

原型链是JavaScript实现继承的主要机制。每个对象都有一个内部属性[[Prototype]],指向其原型对象。通过原型链,对象可以继承原型对象的属性和方法。

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

Animal.prototype.type = '动物';

function Dog(name, breed) {
    Animal.call(this, name); // 继承Animal的构造函数
    this.breed = breed;
}

// 设置Dog的原型为Animal的实例
Dog.prototype = Object.create(Animal.prototype);

Dog.prototype物种 = '犬科';

const myDog = new Dog('大黄', '金毛');
console.log(myDog.name); // 输出:大黄
console.log(myDog.物种); // 输出:犬科
console.log(myDog.type); // 输出:动物

9.2 __proto__属性的作用

__proto__属性是访问和设置对象原型的便捷方式。它直接指向对象的原型对象:

const obj = {};
obj.__proto__ = { color: '红色' };
console.log(obj.color); // 输出:红色

在手写new函数时,我们通过obj.__proto__ =Constructor.prototype将新对象的原型指向构造函数的原型,从而实现继承 。

9.3 原型链的查找机制

当访问对象的属性时,JavaScript引擎会沿着原型链向上查找,直到找到该属性或到达原型链顶端(Object.prototype):

function Parent() {}
Parent.prototype.name = '父对象';

function Child() {}
Child.prototype = Object.create(Parent.prototype);

const child = new Child();
console.log(child.name); // 输出:父对象

这种机制使得JavaScript能够实现灵活的继承模式,而无需像传统类语言那样预先定义类结构。

十、总结与最佳实践

10.1 new运算符的理解与应用

通过手写new函数的过程,我们深入理解了JavaScript对象实例化的机制。掌握new运算符的工作原理,有助于我们更好地设计和实现面向对象的JavaScript程序。

10.2 类数组对象的处理技巧

在处理类数组对象时,需要根据具体场景选择合适的转换方法。对于需要兼容旧环境的项目,slice.callconcat.apply是可靠选择;对于现代项目,Array.from或扩展运算符提供了更简洁的语法。

10.3 原型链与对象设计

理解原型链机制,有助于我们设计高效、灵活的JavaScript对象结构。通过合理设置原型链,可以实现代码的复用和对象特性的继承,同时避免不必要的内存消耗。

10.4 实际开发中的最佳实践

在实际开发中,建议遵循以下最佳实践:

  • 使用letconst代替var声明变量,避免变量提升和作用域问题
  • 在需要严格检查的环境中添加'use strict';声明
  • 根据环境选择合适的类数组转换方法
  • 避免在类数组对象上直接使用数组方法,应先转换为数组
  • 合理利用原型链设计对象结构,提高代码复用性

通过深入理解new运算符和类数组对象的特性,我们可以更有效地利用JavaScript的面向对象能力和数据结构,构建出功能强大且高效的前端应用。