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 类数组对象的定义
类数组对象是一种特殊的数据结构,它满足两个核心条件:
- 具有
length属性,表示元素的数量 - 具有从0开始的数字索引属性,可以通过索引访问元素
虽然类数组对象在结构上与数组相似,但它并不是数组类型,而是对象类型。因此,它不能直接使用数组的方法,如map、reduce、join等 。
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集合
通过getElementsByClassName或getElementsByTagName等方法获取的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.call或concat.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 类数组对象的限制
类数组对象有以下主要限制:
- 不能直接使用数组方法(如
map、reduce、join等) - 不能使用数组的展开语法(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.call或concat.apply是可靠选择;对于现代项目,Array.from或扩展运算符提供了更简洁的语法。
10.3 原型链与对象设计
理解原型链机制,有助于我们设计高效、灵活的JavaScript对象结构。通过合理设置原型链,可以实现代码的复用和对象特性的继承,同时避免不必要的内存消耗。
10.4 实际开发中的最佳实践
在实际开发中,建议遵循以下最佳实践:
- 使用
let或const代替var声明变量,避免变量提升和作用域问题 - 在需要严格检查的环境中添加
'use strict';声明 - 根据环境选择合适的类数组转换方法
- 避免在类数组对象上直接使用数组方法,应先转换为数组
- 合理利用原型链设计对象结构,提高代码复用性
通过深入理解new运算符和类数组对象的特性,我们可以更有效地利用JavaScript的面向对象能力和数据结构,构建出功能强大且高效的前端应用。