面试必考:手写代码 new 做了什么?从原理到实现全解析

在 JavaScript 面试中,"new 做了什么"和"手写 new"几乎成了必考题。本文从底层原理出发,一步步实现 myNew,覆盖所有边界情况,帮你彻底吃透这个知识点。

目录

  1. [前言:为什么 new 这么重要](#前言:为什么 new 这么重要 "#section-1")
  2. [new 到底做了什么?------四步拆解](#new 到底做了什么?——四步拆解 "#section-2")
  3. 手写实现:从基础版到严谨版
  4. 边界情况分析:那些容易踩的坑
  5. 完整测试:验证我们的实现
  6. [原理深入:原型链、Object.create 与 new.target](#原理深入:原型链、Object.create 与 new.target "#section-6")
  7. 面试加分项:怎么回答更出彩
  8. 总结

前言:为什么 new 这么重要

如果你参加过前端面试,大概率被问过这个问题:"说说 new 操作符做了什么?" ,紧接着就是 "能手写一个 new 吗?"

这不是一个简单的背诵题。它考察的是你对 JavaScript 原型链this 绑定对象创建 三个核心机制的整合理解。很多候选人能说出概念,但一写代码就漏洞百出------返回值判断遗漏 null、原型指向写错、边界情况没考虑......

本文将从零开始,带你彻底吃透 new 的原理 ,并手写一个覆盖所有边界情况的 myNew

new 到底做了什么?------四步拆解

先来看一段最常见的代码:

JavaScript

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

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

const p = new Person('张三', 25);
p.sayHello();       // Hello, I'm 张三
console.log(p instanceof Person); // true

当我们写下 new Person('张三', 25) 时,JavaScript 引擎在背后做了四件事

1

创建一个新的空对象

引擎在内存中开辟空间,创建一个全新的普通对象 {}。这个对象此时没有任何自定义属性。

2

设置原型链(链接 prototype)

将新对象的 __proto__ 指向构造函数的 prototype。这一步建立了原型链,让实例能访问构造函数原型上的方法。

3

绑定 this 并执行构造函数

将构造函数的 this 指向新对象,传入参数执行。构造函数内部的 this.xxx = xxx 就是在给这个新对象添加属性。

4

判断返回值

如果构造函数显式返回了一个对象 (或函数),则 new 的结果就是那个返回值;否则返回第一步创建的新对象。

关键点

第四步是很多人容易忽略的。记住规则:返回对象类型则用返回值,返回原始值或无 return 则用新对象null 虽然 typeof 返回 'object',但它属于原始值,需要特殊处理。

手写实现:从基础版到严谨版

基础版:最直观的实现

根据上面的四步,我们可以直接翻译成代码:

JavaScript · 基础版

javascript 复制代码
// 基础版 myNew
function myNew(constructor, ...args) {
  // 第一步:创建一个新对象
  const obj = {};

  // 第二步:设置原型链
  obj.__proto__ = constructor.prototype;

  // 第三步:绑定 this 并执行构造函数
  const result = constructor.apply(obj, args);

  // 第四步:判断返回值
  return (typeof result === 'object' || typeof result === 'function') && result !== null
    ? result
    : obj;
}

这个版本已经能处理大部分场景了,但有一个问题:直接操作 __proto__ 并不是推荐做法 。虽然现代浏览器都支持 __proto__,但它本质上是内部属性,在极端环境(如某些非浏览器 JS 引擎)中可能不可用。

严谨版:使用 Object.create

更标准、更严谨的实现应该使用 Object.create() 来同时完成"创建对象 + 设置原型"两步:

JavaScript · 严谨版

javascript 复制代码
// 严谨版 myNew ------ 面试推荐写法
function myNew(constructor, ...args) {
  // 第一步 + 第二步:创建新对象并设置原型
  const obj = Object.create(constructor.prototype);

  // 第三步:绑定 this 并执行构造函数
  const result = constructor.apply(obj, args);

  // 第四步:判断返回值类型
  const isObject = typeof result === 'object' && result !== null;
  const isFunction = typeof result === 'function';

  return isObject || isFunction ? result : obj;
}

为什么用 Object.create?

Object.create(proto) 会创建一个以 proto 为原型的新对象,等价于创建空对象再设置 __proto__,但它是 ES5 标准 API,更规范、更安全。在面试中用 Object.create 是明确的加分项。

边界情况分析:那些容易踩的坑

面试官不会只满足于基础实现。他们会追问各种边界情况,来验证你是否真的理解了原理。下面逐一分析:

情况一:构造函数返回原始值

返回 stringnumberboolean 等原始值时,会被忽略new 仍然返回新创建的对象。

JavaScript

ini 复制代码
function Foo() {
  this.value = 1;
  return 2; // 返回原始值 → 被忽略
}

const foo = myNew(Foo);
console.log(foo.value); // 1 ✅ 返回的是新对象

情况二:构造函数返回对象

返回一个对象 时,new 的结果就是这个返回的对象,而非新创建的对象。

JavaScript

ini 复制代码
function Bar() {
  this.value = 1;
  return { value: 2 }; // 返回对象 → 作为 new 的结果
}

const bar = myNew(Bar);
console.log(bar.value); // 2 ✅ 返回的是构造函数的 return 值

情况三:构造函数返回 null

这是最经典的坑typeof null 返回 'object',但 null 实际上是原始值。如果不做特殊处理,myNew 会错误地返回 null 而非新对象。

JavaScript

javascript 复制代码
function Baz() {
  this.value = 1;
  return null; // typeof null === 'object',但 null 是原始值!
}

const baz = myNew(Baz);
console.log(baz.value); // 1 ✅ 正确返回新对象,而非 null

高频面试追问

"typeof null 为什么返回 'object'?"------这是 JS 的历史遗留问题。早期版本中,值的类型用低位标识:对象类型标记为 000,而 null 在大多数平台被表示为全 0 指针,所以被误判为对象类型。这个 bug 一直延续至今。

情况四:构造函数返回函数

函数也是对象类型,所以如果构造函数返回一个函数,new 的结果应该是那个返回的函数。

JavaScript

javascript 复制代码
function Qux() {
  this.value = 1;
  return function() { return 'I am a function'; };
}

const qux = myNew(Qux);
console.log(typeof qux); // 'function' ✅ 返回的是函数
console.log(qux());    // 'I am a function' ✅

完整测试:验证我们的实现

将所有边界情况整合在一起,写一个完整的测试套件:

JavaScript · 完整测试

javascript 复制代码
// ============ 完整测试套件 ============

// 测试 1:基本功能 + 原型链
function Animal(type) {
  this.type = type;
}
Animal.prototype.getType = function() {
  return this.type;
};

const animal = myNew(Animal, 'cat');
console.log(animal.getType());              // 'cat'
console.log(animal instanceof Animal);     // true
console.log(animal.__proto__ === Animal.prototype); // true

// 测试 2:返回对象 → 使用返回值
function ReturnObj() {
  this.name = 'inner';
  return { name: 'returned' };
}
console.log(myNew(ReturnObj).name); // 'returned'

// 测试 3:返回原始值 → 忽略,使用新对象
function ReturnPrimitive() {
  this.name = 'inner';
  return 'I am a string';
}
console.log(myNew(ReturnPrimitive).name); // 'inner'

// 测试 4:返回 null → 忽略,使用新对象
function ReturnNull() {
  this.name = 'inner';
  return null;
}
console.log(myNew(ReturnNull).name); // 'inner'

// 测试 5:返回函数 → 使用返回值
function ReturnFunc() {
  this.name = 'inner';
  return function() { return 'fn'; };
}
const rf = myNew(ReturnFunc);
console.log(typeof rf); // 'function'
console.log(rf());    // 'fn'

// 测试 6:无参数构造函数
function NoArgs() {
  this.flag = true;
}
console.log(myNew(NoArgs).flag); // true

// 测试 7:多参数构造函数
function User(name, age, role) {
  this.name = name;
  this.age = age;
  this.role = role;
}
const user = myNew(User, 'Alice', 28, 'admin');
console.log(user.name, user.age, user.role); // Alice 28 admin

所有测试用例均通过:

测试项 预期结果 状态
基本功能 + 原型链 getType() 返回 'cat'instanceoftrue 通过
返回对象 使用返回的对象,name'returned' 通过
返回原始值 忽略返回值,name'inner' 通过
返回 null 忽略 null,name'inner' 通过
返回函数 使用返回的函数,typeof'function' 通过
无参数 flagtrue 通过
多参数 三个参数正确赋值 通过

原理深入:原型链、Object.create 与 new.target

原型链:为什么要链接 prototype?

JavaScript 的继承机制是基于原型链的。当你在实例上访问一个属性或方法时,引擎会先在实例自身查找,找不到就沿着 __proto__ 往上找。

new 操作的第二步------将 obj.__proto__ 指向 constructor.prototype------正是为了建立这条链路:

原型链示意

javascript 复制代码
// 原型链查找路径
instance              →  查找自身属性
  ↓ __proto__
constructor.prototype  →  查找原型上的方法(如 sayHello)
  ↓ __proto__
Object.prototype       →  查找 toString、valueOf 等
  ↓ __proto__
null                   →  查找结束,返回 undefined

如果我们漏了第二步,实例就访问不到构造函数原型上的方法,instanceof 也会返回 false

Object.create vs 直接设置 proto

这两种方式在功能上等价:

JavaScript

ini 复制代码
// 方式 A:Object.create(推荐)
const obj1 = Object.create(Constructor.prototype);

// 方式 B:手动设置 __proto__(不推荐)
const obj2 = {};
obj2.__proto__ = Constructor.prototype;

// 两者效果相同,但 Object.create 更标准
对比维度 Object.create proto 赋值
标准化 ES5 标准 API ES6 才标准化(附录)
一步完成 创建 + 设置原型一步到位 需要先创建再赋值
性能 引擎优化更好 可能触发额外的隐藏类转换
可读性 意图清晰 需要了解 __proto__ 语义
兼容性 IE9+ IE11+(非 IE6-9)

new.target:判断函数是否被 new 调用

ES6 引入了 new.target 元属性,用于检测函数是否通过 new 调用:

JavaScript

javascript 复制代码
function Person(name) {
  if (!new.target) {
    throw new Error('必须使用 new 关键字调用 Person');
  }
  this.name = name;
}

Person('张三');           // ❌ Error: 必须使用 new 关键字
new Person('张三');      // ✅ 正常创建实例

当通过 new 调用时,new.target 指向构造函数本身;当作为普通函数调用时,new.targetundefined

箭头函数为什么不能作为构造函数?

箭头函数没有自己的 thisargumentssupernew.target,也没有 [[Construct]] 内部方法。因此对箭头函数使用 new 会直接抛出 TypeError

JavaScript

javascript 复制代码
const ArrowFn = () => { this.x = 1; };

new ArrowFn();
// TypeError: ArrowFn is not a constructor

同样的,我们的 myNew 如果传入箭头函数,constructor.apply() 不会绑定 this(箭头函数的 this 在定义时就确定了),所以行为会不符合预期。可以在 myNew 中加入防御检查:

JavaScript · 加防御版

javascript 复制代码
function myNew(constructor, ...args) {
  // 防御:箭头函数没有 prototype
  if (!constructor.prototype) {
    throw new TypeError(`${constructor.name} is not a constructor`);
  }

  const obj = Object.create(constructor.prototype);
  const result = constructor.apply(obj, args);

  return (typeof result === 'object' && result !== null) || typeof result === 'function'
    ? result
    : obj;
}

面试加分项:怎么回答更出彩

面试时,如果只是把代码写出来,只能拿到基础分。以下这些点能让你的回答脱颖而出:

面试加分清单

  • 使用 Object.create 而非 proto ------ 展示你对标准 API 的了解,说明 Object.create 更规范、性能更好。
  • 处理 null 返回值 ------ 主动提到 typeof null === 'object' 的历史遗留问题,说明你理解 JavaScript 类型系统的缺陷。
  • 箭头函数不能作为构造函数 ------ 主动说明箭头函数没有 [[Construct]] 内部方法,没有自己的 thisprototype
  • 提到 new.target ------ 说明 ES6 引入了 new.target 元属性,可以判断函数是否通过 new 调用,用于实现"必须 new 调用"的约束。
  • 提到 ES6 class 的 new 行为 ------ class 必须用 new 调用,否则报 TypeError;且 class 内部默认使用严格模式。
  • 用一行代码验证 ------ 写完后主动用 instanceof 和原型链检查验证正确性,展示测试意识。

总结

new 的原理浓缩成一张表:

步骤 操作 对应代码
1 创建新对象 Object.create(constructor.prototype)
2 设置原型链 ↑ 已包含在上面
3 绑定 this 并执行 constructor.apply(obj, args)
4 判断返回值 typeof result === 'object' && result !== null

最终,面试推荐版本(20 行以内,覆盖所有边界情况):

JavaScript · 终版

ini 复制代码
function myNew(constructor, ...args) {
  const obj = Object.create(constructor.prototype);
  const result = constructor.apply(obj, args);
  return result && (typeof result === 'object' || typeof result === 'function')
    ? result
    : obj;
}

一行简记

创建空对象 → 链接原型 → 绑定 this 执行 → 判断返回值 。四步缺一不可,null 要特判,Object.create 优于 proto

相关推荐
BillKu41 分钟前
TypeScript中,字符串字面量联合类型(Union Type)、enum的用法说明
前端·javascript·typescript
jayson.h43 分钟前
PDF 合并+添加页码 相关库、类、函数
开发语言·前端·python
数智启示录1 小时前
Apache Doris 4.0.8 混合检索实战(第 5 篇):三套系统只返回两条结果,一条 SQL 如何避开交集丢失
大数据·数据库·经验分享·sql·面试
慧一居士2 小时前
Sass和Less功能、使用场景、用法对比
前端·css·less·sass
leoZ2312 小时前
AI+前端提效-09 AI赋能前端测试:单元测试、E2E测试自动生成,提升覆盖率
前端·人工智能·opencv·目标检测·数据挖掘·单元测试·语音识别
计算机魔术师2 小时前
OpenAI 评定 Astra 达到网络安全 Critical 能力阈值,将受限发布
前端
JavaGuide2 小时前
SpaceX 工程师的 AI Coding 玩法太牛了, 200 多个 Agent 并行!
前端·后端
小猪code2 小时前
frp 80/443 Web 服务两种部署模式实操笔记
前端·笔记
JamesZhang800782 小时前
Chrome相关知识点
前端