手撕大厂面试题:手写new的底层原理

9 阅读4分钟

前言

在 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 操作符执行时,实际上完成了以下几个核心步骤:

  1. 创建一个全新的空对象
  2. 将这个新对象的原型 (__proto__) 指向构造函数的 prototype 属性
  3. 执行构造函数,并将 this 绑定到新创建的对象上
  4. 根据构造函数的返回值类型,决定最终返回的对象

二、手动实现 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 类型的 100new Person() 仍然返回了 Person 的实例对象,且实例属性 name 正常初始化。

为什么??

  1. 构造函数的本质定位
    构造函数的核心职责是创建对象实例,而 new 操作符的设计目标是确保调用者最终获得一个对象实例。若允许构造函数返回基本类型,会破坏 " 通过 new 必然获得对象实例 " 的语义,导致逻辑混乱。

  2. JavaScript 数据类型的底层差异

    • 基本数据类型numberstring 等)是按存储的,没有原型链和构造函数关联关系
    • 对象类型 是按引用存储的,需要通过原型链实现继承等机制
    • 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 实例)类数组对象
语法显式通过...声明为参数隐式存在于函数内部
可变性不可直接修改(数组本身可修改)可修改(修改会影响实际参数)
参数位置限制必须作为最后一个参数无位置限制
解构支持支持数组解构不直接支持解构,需先转换为数组