JavaScript 手写 new 操作符:深入理解对象创建

当我们使用 new 关键字时,背后到底发生了什么?这个看似简单的操作,实际上完成了一系列复杂的步骤。理解 new 的工作原理,是掌握 JavaScript 面向对象编程的关键。

前言:从 new 的神秘面纱说起

javascript 复制代码
function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.greet = function() {
    return `你好,我是${this.name},今年${this.age}岁`;
};

const person = new Person('张三', 25);

上述代码中 new 到底创建了什么?为什么 this 指向了新对象?原型链是怎么建立的?如果构造函数有返回值会怎样?我们将通过本篇文章揭开 new 的神秘面纱,从零实现一个自己的 new 操作符。

理解 new

new 的四个核心步骤:

  1. 创建一个空对象
  2. 将对象的原型设置为构造函数的 prototype 属性
  3. 将构造函数的 this 绑定到新对象,并执行构造函数
  4. 判断返回值类型:如果构造函数返回一个对象(包括函数),则返回该对象;否则返回新创建的对象

手写实现 new 操作符

基础版本实现

javascript 复制代码
function myNew(constructor, ...args) {

  // 1. 创建一个空对象
  const obj = {};

  // 2. 将对象的原型设置为构造函数的 prototype 属性
  Object.setPrototypeOf(obj, constructor.prototype);

  // 3. 将构造函数的 this 绑定到新对象,并执行构造函数
  const result = constructor.apply(obj, args);

  // 4. 判断返回值类型
  // 如果构造函数返回一个对象(包括函数),则返回该对象
  // 否则返回新创建的对象
  const isObject = result !== null && (typeof result === 'object' || typeof result === 'function');

  return isObject ? result : obj;
}

处理边界情况

javascript 复制代码
function myNewEnhanced(constructor, ...args) {

  // 边界情况1:constructor 不是函数
  if (typeof constructor !== 'function') {
    throw new TypeError(`${constructor} is not a constructor`);
  }

  // 边界情况2:箭头函数(没有 prototype)
  if (!constructor.prototype) {
    throw new TypeError(`${constructor.name || constructor} is not a constructor`);
  }

  // 1. 创建新对象(改进方法):使用 Object.create 更优雅地设置原型
  const obj = Object.create(constructor.prototype);

  // 2. 调用构造函数
  let result;
  try {
    result = constructor.apply(obj, args);
  } catch (error) {
    // 如果构造函数抛出异常,直接传播
    throw error;
  }

  // 3. 处理返回值
  // 注意:null 也是 object 类型,但需要特殊处理
  const resultType = typeof result;
  const isObject = result !== null && (resultType === 'object' || resultType === 'function');

  return isObject ? result : obj;
}

完整实现与原型链优化

javascript 复制代码
function myNewComplete(constructor, ...args) {
  // 1. 参数验证
  if (typeof constructor !== 'function') {
    throw new TypeError(`Constructor ${constructor} is not a function`);
  }

  // 2. 检查是否为可构造的函数:箭头函数和部分内置方法没有 prototype
  if (!constructor.prototype && !isNativeConstructor(constructor)) {
    throw new TypeError(`${getFunctionName(constructor)} is not a constructor`);
  }

  // 3. 创建新对象并设置原型链
  const proto = constructor.prototype || Object.prototype;
  const obj = Object.create(proto);

  // 4. 绑定 constructor 属性
  obj.constructor = constructor; 

  // 5. 执行构造函数
  const result = Reflect.construct(constructor, args, constructor);

  // 6. 处理返回值
  // Reflect.construct 已经处理了返回值逻辑
  // 但我们还是实现自己的逻辑以保持一致
  return processConstructorResult(result, obj, constructor);
}

// 辅助函数:检查是否为原生构造函数
function isNativeConstructor(fn) {
  // 一些内置构造函数如 Symbol、BigInt 没有 prototype
  const nativeConstructors = [
    'Number', 'String', 'Boolean', 'Symbol', 'BigInt',
    'Date', 'RegExp', 'Error', 'Array', 'Object', 'Function'
  ];

  return nativeConstructors.some(name =>
    fn.name === name || fn === globalThis[name]
  );
}

// 辅助函数:获取函数名
function getFunctionName(fn) {
  if (fn.name) return fn.name;
  const match = fn.toString().match(/^function\s*([^\s(]+)/);
  return match ? match[1] : 'anonymous';
}

// 辅助函数:处理构造函数返回值
function processConstructorResult(result, defaultObj, constructor) {
  // 如果 result 是 undefined 或 null,返回 defaultObj
  if (result == null) {
    return defaultObj;
  }

  // 检查 result 的类型
  const type = typeof result;

  // 如果是对象或函数,返回 result
  if (type === 'object' || type === 'function') {
    // 额外检查:如果 result 是构造函数本身的实例,确保原型链正确
    if (result instanceof constructor) {
      return result;
    }
    return result;
  }

  // 原始值,返回 defaultObj
  return defaultObj;
}

深入原型链与继承

原型链的建立过程

javascript 复制代码
// 父构造函数
function Animal(name) {
  this.name = name;
}
// 父类方法
Animal.prototype.speak = function () {
  return `${this.name} 叫了`;
};
// 子构造函数
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}
// 建立原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// 子类方法
Dog.prototype.bark = function () {
  return `${this.name} 汪汪叫`;
};
// 创建实例
const myDog = new Dog('旺财', '金毛');
console.log(myDog.speak()); // 旺财 叫了
console.log(myDog.bark());  // 旺财 汪汪叫

ES6 类与 new 的关系

ES6 类的本质还是基于原型的语法糖:

ES6 基本写法

javascript 复制代码
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `你好,我是${this.name}`;
  }
}
const person = new Person('张三', 30);

对应 ES5 的写法

javascript 复制代码
function PersonES5(name, age) {
  // 类构造器中的代码
  if (!(this instanceof PersonES5)) {
    throw new TypeError("Class constructor Person cannot be invoked without 'new'");
  }

  this.name = name;
  this.age = age;
}

// 实例方法(添加到原型)
PersonES5.prototype.greet = function () {
  return `你好,我是${this.name}`;
};
const personES5 = new PersonES5('李四', 25);

类的重要特性

  1. 类必须用 new 调用
  2. 类方法不可枚举
  3. 类没有变量提升

ES6 实现继承的完整示例

javascript 复制代码
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + ' 叫了');
  }
}
class Dog extends Animal {
  constructor(name) {
    super(name);
  }

  speak() {
    console.log(this.name + ' 汪汪叫');
  }
}

ES6 继承的本质

ES6 通过 extends 关键字实现继承,就等价于 ES5 的寄生组合继承:

javascript 复制代码
function AnimalES5(name) {
  this.name = name;
}

AnimalES5.prototype.speak = function () {
  console.log(this.name + ' 叫了');
};

function DogES5(name) {
  AnimalES5.call(this, name);
}

// 设置原型链
DogES5.prototype = Object.create(AnimalES5.prototype);
DogES5.prototype.constructor = DogES5;

DogES5.prototype.speak = function () {
  console.log(this.name + ' 汪汪叫');
};

特殊场景与高级应用

单例模式与 new

方法1:使用静态属性

javascript 复制代码
class SingletonV1 {
  static instance = null;

  constructor(name) {
    if (SingletonV1.instance) {
      return SingletonV1.instance;
    }

    this.name = name;
    SingletonV1.instance = this;
  }

  static getInstance(name) {
    if (!this.instance) {
      this.instance = new SingletonV1(name);
    }
    return this.instance;
  }
}

方法2:使用闭包

javascript 复制代码
const SingletonV2 = (function () {
  let instance = null;

  return class Singleton {
    constructor(name) {
      if (instance) {
        return instance;
      }

      this.name = name;
      instance = this;
    }
  };
})();

方法3:代理模式

javascript 复制代码
function createSingletonProxy(Class) {
  let instance = null;

  return new Proxy(Class, {
    construct(target, args) {
      if (!instance) {
        instance = Reflect.construct(target, args);
      }
      return instance;
    }
  });
}

实现 Object.create 的 polyfill

javascript 复制代码
if (typeof Object.create !== 'function') {
  Object.create = function (proto, propertiesObject) {
    // 参数验证
    if (typeof proto !== 'object' && typeof proto !== 'function') {
      throw new TypeError('Object prototype may only be an Object or null');
    }

    // 核心实现:使用空函数作为中间构造函数
    function F() { }
    F.prototype = proto;

    // 创建新对象,原型指向proto
    const obj = new F();

    // 处理第二个参数(属性描述符)
    if (propertiesObject !== undefined) {
      Object.defineProperties(obj, propertiesObject);
    }

    // 处理 null 原型
    if (proto === null) {
      obj.__proto__ = null;
    }
    // 返回新对象
    return obj;
  };
}

常见面试问题与解答

问题1:new 操作符做了什么?

  1. 创建一个新的空对象',
  2. 将这个空对象的原型设置为构造函数的 prototype 属性',
  3. 将构造函数的 this 绑定到这个新对象,并执行构造函数',
  4. 如果构造函数返回一个对象(包括函数),则返回该对象;否则返回新创建的对象

问题2:如果构造函数有返回值会怎样?

  • 返回对象(包括函数):忽略 this 绑定的对象,返回该对象
  • 返回原始值(number, string, boolean等):忽略返回值,返回 this 绑定的对象
  • 没有 return 语句:隐式返回 undefined,返回 this 绑定的对象

问题3:如何判断函数是否被 new 调用?

  • ES5:检查 this instanceof Constructor'
  • ES6+:使用 new.target(更准确)
  • 箭头函数:不能作为构造函数,没有 new.target

结语

通过深入理解 new 操作符的工作原理,我们不仅能在面试中脱颖而出,还能在实际开发中做出更明智的设计决策。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

相关推荐
上海合宙LuatOS2 小时前
LuatOS核心库API——【fatfs】支持FAT32文件系统
java·前端·网络·数据库·单片机·嵌入式硬件·物联网
m0_528749002 小时前
linux编程----目录流
java·前端·数据库
集成显卡2 小时前
前端视频播放方案选型:主流 Web 播放器对比 + Vue3 实战
前端·vue·音视频
前端 贾公子2 小时前
Vue3 业务组件库按需加载的实现原理(中)
前端·javascript·vue.js
温轻舟2 小时前
前端可视化大屏【附源码】
前端·javascript·css·html·可视化·可视化大屏·温轻舟
北极象2 小时前
Flying-Saucer HTML到PDF渲染引擎核心流程分析
前端·pdf·html
weixin199701080162 小时前
Tume商品详情页前端性能优化实战
大数据·前端·java-rabbitmq
梦里寻码2 小时前
深入解析 SmartChat 的 RAG 架构设计 — 如何用 pgvector + 本地嵌入打造企业级智能客服
前端·agent
edisao2 小时前
第一章:L-704 的 0.00% 偏差
前端·数据库·人工智能