前言
在 JavaScript 中,new
操作符是创建对象实例的重要方式。但你是否思考过 new
操作符背后的底层机制?本文将带你一步步手动实现 new
操作符,深入理解其工作原理。
一、 new 操作符的基本用法与功能
在正式实现之前,我们先回顾一下 new
操作符的基本用法:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`);
};
// 使用 new 操作符创建对象实例
const person = new Person("张三", 18);
console.log(person.name); // 输出: 张三
person.sayHi(); // 输出: 你好,我是张三
new
操作符执行时,实际上完成了以下几个核心步骤:
- 创建一个全新的空对象
- 将这个新对象的原型 (
__proto__
) 指向构造函数的prototype
属性 - 执行构造函数,并将
this
绑定到新创建的对象上 - 根据构造函数的返回值类型,决定最终返回的对象
二、手动实现 new 操作符:objectFactory
函数
现在,让我们一步步手动实现一个 new
操作符的替代函数 objectFactory
:
function objectFactory() {
const obj = {};
// shift 方法 -> 取出数组的第一个元素,并返回这个元素,同时原数组长度减1
// call 方法 -> 改变this指向,一开始指向前面的数组,后面改成了后面的对象
// call 方法 -> 调用一个对象的一个方法,以另一个对象替换当前对象,借用数组的shift方法。
// 因为 arguments 是类数组,没有真正数组的shift方法,所以可以使用 [].shift.call(arguments) 来取出第一个参数
const Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
const result = Constructor.apply(obj, arguments);
return typeof result === 'object' ? result || obj : obj;
}
// 使用示例
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`);
};
const p = objectFactory(Person, "李四", 20);
console.log(p.name); // 输出: 李四
p.sayHi(); // 输出: 你好,我是李四
console.log(p instanceof Person); // 输出: true
三、核心步骤解析
让我们详细解析 objectFactory
函数的每个关键步骤:
1.创建空对象:
const obj = {};
这行代码创建了一个全新的空对象 obj
,它将成为最终的实例对象。
2.设置原型链:
obj.__proto__ = Constructor.prototype;
这行代码将新对象 obj
的原型 (__proto__
) 指向构造函数的 prototype
属性,从而建立原型链关系。这使得实例对象可以访问构造函数原型上的方法和属性。
3.执行构造函数并绑定 this:
const result = Constructor.apply(obj, arguments);
使用 apply
方法执行构造函数,并将 this
显式绑定到新创建的对象 obj
上。这样,构造函数中对 this
的所有操作都会作用于 obj
。
4.处理返回值:
为什么要处理返回值??
我们来试想一下:如果构造函数加一个result 100;
会出现什么情况,我们用代码来验证一下:
function Person(name) {
this.name = name;
// 返回基本数据类型(number)
return 100;
}
// 实例化对象
const p = new Person('张三');
// 结果
console.log(p.name); // 张三
console.log(typeof p); // object
console.log(p instanceof Person); // true
现象总结:
即使构造函数返回了 number
类型的 100
,new Person()
仍然返回了 Person
的实例对象,且实例属性 name
正常初始化。
为什么??
构造函数的本质定位
构造函数的核心职责是创建对象实例,而new
操作符的设计目标是确保调用者最终获得一个对象实例。若允许构造函数返回基本类型,会破坏 " 通过new
必然获得对象实例 " 的语义,导致逻辑混乱。JavaScript 数据类型的底层差异
- 基本数据类型(
number
、string
等)是按值存储的,没有原型链和构造函数关联关系- 对象类型 是按引用存储的,需要通过原型链实现继承等机制
new
操作符的核心逻辑需要维护原型链关系(obj.__proto__ = Constructor.prototype
),而基本类型无法承载这一关系
有三种写法:
return result instanceof Object ? result : obj;
return typeof result === 'object' && result !== null ? result : obj;
return typeof result === 'object' ? result || obj : obj;
这是一个关键步骤。JavaScript 规范规定:如果构造函数返回一个对象(包括数组、函数等),则 new
表达式的结果就是这个返回的对象;否则返回新创建的对象。
需要特别注意排除
null
的情况,因为typeof null
也返回"object"
。
四、ES6 改进版本
使用 ES6 的 剩余参数 代替类数组,可以省去取第一个Constructor
参数步骤:
// es6改进版本
function objectFactory(Constructor,...args){
const obj = {};
// const Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
const result = Constructor.apply(obj,args);
return typeof result === 'object' ? result || obj : obj;
}
剩余参数 与 类数组
一、剩余参数的基本概念
剩余参数使用...
语法(展开运算符)来标识,它会将函数的多余参数收集到一个数组中。其核心特点包括:
- 语法形式:
function fn(...restParams) {}
- 作用:将不定数量的参数转换为数组,便于统一处理
- 位置限制:必须作为函数的最后一个参数
二、剩余参数与 arguments 对象的对比
特性 | 剩余参数(Rest Parameters) | arguments 对象 |
---|---|---|
数据类型 | 真正的数组(Array 实例) | 类数组对象 |
语法 | 显式通过... 声明为参数 | 隐式存在于函数内部 |
可变性 | 不可直接修改(数组本身可修改) | 可修改(修改会影响实际参数) |
参数位置限制 | 必须作为最后一个参数 | 无位置限制 |
解构支持 | 支持数组解构 | 不直接支持解构,需先转换为数组 |