1. new
背后到底发生了什么?(规范语义视角)
当执行 new Constructor(...args)
时,大致等价于以下步骤:
- 创建一个全新的普通对象 ,并把它的内部
[[Prototype]]
指向Constructor.prototype
。 - 用新对象作为
this
调用Constructor(...args)
,且设置new.target
为Constructor
。 - 如果构造函数显式返回一个对象(object/function),则返回该对象;否则返回步骤 1 创建的新对象。
2. 构造函数与严格模式:遗漏 new
的后果
在非严格模式 下,遗漏 new
会把 this
绑定到全局对象 (浏览器为 window
),导致"把属性挂到全局"的隐蔽 Bug;在严格模式 下,this
为 undefined
,对其赋值会直接抛错。
js
'use strict'; // 开启严格模式,避免 this 指向全局
function Person(name) { // 声明一个构造函数(可被 new 调用)
this.name = name; // 期望把 name 放到"新对象实例"的 this 上
}
const a = new Person('Ankit'); // 正确:使用 new,创建实例
console.log(a.name); // 输出 "Ankit"
// const b = Person('Ankit'); // 错误示例:未使用 new,在严格模式下会抛 TypeError
3. 强制要求使用 new
:new.target
与回退模式
你可以在构造函数内通过 new.target
检查是否由 new
调用;若未使用 new
,可自动补救 或显式抛错。
js
function User(name) { // 声明构造函数
if (!new.target) return new User(name); // 若未用 new,则递归调用补上 new
this.name = name; // 继续正常初始化
}
const u1 = new User('Ada'); // 正确用法
const u2 = User('Grace'); // 漏写 new 也能得到实例(自动补救)
console.log(u1 instanceof User, u2 instanceof User); // 两者都为 true
若你担心继承场景(子类
extends
)或想要显式约束,也可改为抛错:if (!new.target) throw new Error('Must use new');
4. 类(class
)必须配合 new
使用
类声明天生不可作为普通函数调用,否则会直接抛错。
js
class Car { // 声明一个类
constructor(brand) { // 构造器,new 时被调用
this.brand = brand; // 在实例上记录品牌
} // 构造器结束
} // 类定义结束
const c1 = new Car('Tesla'); // 正确:使用 new
console.log(c1.brand); // 输出 "Tesla"
// const c2 = Car('BYD'); // 错误:类不能不带 new 调用,会抛 TypeError
5. 构造函数的返回值规则(再次严谨化)
return 对象/函数
→ 覆盖默认返回的新实例。return 基本类型(string/number/boolean/symbol/bigint/undefined/null)
→ 被忽略,仍返回新实例。
js
function Test() { // 声明构造函数
this.value = 10; // 给新实例设置属性
return { msg: 'I override!' }; // 显式返回对象,覆盖默认实例
}
const t = new Test(); // 使用 new
console.log(t); // 输出 { msg: 'I override!' }
6. 哪些可以 new
,哪些不行?(内置对象全景与差异)
- 既可
new
,也可直接调用 (两者结果等价或相近):Object
、Array
、Function
、RegExp
、Date
(但Date()
与new Date()
不等价 ,见下)、Error
、Map/Set/WeakMap/WeakSet
(注意:这些通常仍需new
才能有实例)。 - 包装对象的陷阱 :
new Number(5)
、new String('x')
、new Boolean(false)
返回对象而非基本类型,可能导致布尔判断异常。 - 彻底不能
new
:Symbol
、BigInt
(它们是函数/语法,但不可作为构造器使用)。
js
// 1) Date 的差异
console.log(typeof Date()); // "string":直接调用返回"当前时间"的字符串
console.log(Date() instanceof Date); // false:仅是字符串而非 Date 实例
console.log(new Date() instanceof Date); // true:使用 new 才得到 Date 实例
// 2) 包装对象陷阱
const x = new Boolean(false); // 这是一个 Boolean 对象
if (x) console.log('会执行'); // 会执行:对象在条件判断中为真
const y = Boolean(false); // 这是基本类型 false
if (y) console.log('不会执行'); // 不会执行:基本类型 false 为假
// 3) 不能 new 的类型
// new Symbol('id'); // TypeError:Symbol 不能作为构造器
// new BigInt(10); // TypeError:BigInt 不能作为构造器
7. 谁"能被 new
"?------可构造(constructible)与箭头函数限制
- 并不是所有函数都能被
new
:箭头函数 、类的原型方法 、某些内建函数 都不可构造。 - 能不能被
new
,取决于其是否具有 [[Construct]] 内部方法。
js
const Arrow = () => {}; // 声明箭头函数
// new Arrow(); // TypeError:箭头函数不可被构造
class A { // 声明类 A
m() {} // 这是原型方法,不是构造器
}
// new A.prototype.m(); // TypeError:方法不可被构造
8. instanceof
与原型链、Symbol.hasInstance
a instanceof Ctor
的判断逻辑:沿着 a
的 [[Prototype]]
链向上找,是否能找到 Ctor.prototype
。你还可以通过静态的 Symbol.hasInstance
自定义 instanceof
的行为。
js
function Ctor() {} // 声明构造函数
const obj = new Ctor(); // 基于 Ctor.prototype 构造实例
console.log(Object.getPrototypeOf(obj) === Ctor.prototype); // true:原型指向匹配
console.log(obj instanceof Ctor); // true:instanceof 也为真
class Range { // 自定义类
static [Symbol.hasInstance](x) { // 自定义 instanceof 判断逻辑
return typeof x === 'number' && x >= 0 && x <= 100; // 规定:0~100 的数字算"实例"
} // 自定义结束
} // 类结束
console.log(50 instanceof Range); // true:满足自定义规则
console.log(200 instanceof Range); // false:不在范围内
9. new
与 bind/call/apply
的交互:this
绑定被忽略
当你对构造函数做了 bind
,再用 new
调用时,this
会指向新实例 ,而不是 被绑定的对象;但通过 bind
预置的参数仍然生效 。call/apply
也无法改变 new
的 this
。
js
function Foo(x, y) { // 声明构造函数
this.sum = x + y; // 在实例上记录两个参数的和
}
const obj = {}; // 一个无关对象
const Bound = Foo.bind(obj, 1); // 绑定 this 为 obj,并预置第一个参数为 1
const ins = new Bound(2); // 使用 new:this 指向新实例,而不是 obj
console.log(ins.sum); // 3:预置参数 1 与传入参数 2 生效
console.log(ins instanceof Foo); // true:原型链正常
console.log(obj.sum); // undefined:obj 并未被写入
10. Object.create
vs new
:何时选择哪个?
new Ctor()
:会执行构造函数 逻辑,并把[[Prototype]]
设为Ctor.prototype
。Object.create(proto)
:只 创建一个以proto
为原型的对象,不执行任何构造逻辑 。适合你只想要某个原型,而不需要副作用或初始化过程的场景。
js
function Person(name) { // 声明构造函数
this.name = name; // 初始化副作用:写入实例属性
}
const a = new Person('Ada'); // 使用 new:会执行构造逻辑
const b = Object.create(Person.prototype); // 只建立原型链,不执行构造逻辑
console.log(a.name); // "Ada":a 有 name
console.log(b.name); // undefined:b 没有被初始化
11. Reflect.construct
:构造的高级玩法(改变 new.target)
Reflect.construct(Target, args, NewTarget)
允许你像 new
一样构造 Target
,但把 new.target
设为 NewTarget
,这在继承与"混入"工厂里很有用。
js
function Base() { // 基类构造
this.baseNewTarget = new.target; // 记录调用时的 new.target
}
function Mid() {} // 中间类(仅示意)
const obj = Reflect.construct(Base, [], Mid); // 用 Mid 作为 new.target 构造 Base
console.log(obj.baseNewTarget === Mid); // true:new.target 被定制为 Mid
console.log(Object.getPrototypeOf(obj) === Base.prototype); // true:原型仍指向 Base.prototype
12. 常见陷阱与最佳实践清单
- ✅ 在构造函数/类中始终使用
new
;构造函数内使用new.target
防止误用。 - ✅ 避免
new Boolean/Number/String
带来的真假判断混乱;更倾向 使用无new
的转换函数(Boolean/Number/String
)。 - ✅ 需要无副作用、仅定制原型时,考虑
Object.create
。 - ✅ 自定义
instanceof
行为时谨慎使用Symbol.hasInstance
,保持语义清晰。 - ✅ 和
bind/call/apply
混用时,理解new
对this
绑定具有更高优先级。 - ⚠️ 不能 对
Symbol/BigInt
使用new
。 - ⚠️ 箭头函数、原型方法不可构造,不要 对它们用
new
。 - ⚠️
Date()
与new Date()
行为不同:前者给你字符串,后者给你Date
实例。
13. 参考示例总览(逐行中文注释)
13.1 构造函数 + 严格模式 + 自动补救
js
'use strict'; // 开启严格模式,避免 this 误指向全局
function Person(name) { // 声明构造函数 Person
if (!new.target) return new Person(name); // 若未用 new 调用,自动补上 new
this.name = name; // 在实例上记录 name
} // 构造函数结束
const p1 = new Person('Lin'); // 标准用法:使用 new
const p2 = Person('He'); // 误用:未使用 new,但被自动补救
console.log(p1.name, p2.name); // 输出 "Lin He"
13.2 类构造与错误示范
js
class Car { // 声明类 Car
constructor(brand) { // 定义构造器
this.brand = brand; // 初始化实例字段 brand
} // 构造器结束
} // 类定义结束
const c = new Car('NIO'); // 正确:必须使用 new
console.log(c.brand); // 输出 "NIO"
// Car('XPeng'); // 错误:类不允许不带 new 调用,会抛 TypeError
13.3 返回对象覆盖实例
js
function Overrider() { // 声明构造函数
this.ok = true; // 默认给实例打个标记
return { replaced: true }; // 返回对象,覆盖默认实例
} // 构造函数结束
const o = new Overrider(); // 使用 new
console.log(o); // 输出 { replaced: true },说明覆盖成功
13.4 内置对象差异与包装类型陷阱
js
console.log(typeof Date()); // "string":直接调用返回日期字符串
console.log(new Date() instanceof Date); // true:使用 new 才得到 Date 实例
const nb = new Boolean(false); // Boolean 包装对象,始终为真(对象即真值)
if (nb) console.log('会执行'); // 会执行:对象在条件判断中为真
const pb = Boolean(false); // 基本类型 false
if (pb) console.log('不会执行'); // 不会执行:基本类型 false 为假
13.5 箭头函数与方法不可构造
js
const F = () => {}; // 箭头函数
// new F(); // TypeError:箭头函数没有 [[Construct]]
class T { // 声明类 T
m() {} // 原型方法 m
} // 类结束
// new T.prototype.m(); // TypeError:方法也不可构造
13.6 bind
与 new
的优先级
js
function Add(x, y) { // 声明构造函数
this.sum = x + y; // 累加并保存到实例
} // 构造函数结束
const other = {}; // 准备一个不相关对象
const BAdd = Add.bind(other, 10); // 绑定 this(会被 new 覆盖),并预置 x=10
const ins = new BAdd(5); // 使用 new:this 指向新实例,而非 other
console.log(ins.sum); // 15:预置参数 10 + 传入参数 5
console.log(other.sum); // undefined:other 未被污染
13.7 Object.create
仅设原型,不执行构造
js
function Profile(name) { // 声明构造函数
this.name = name; // 赋值到实例属性
} // 构造函数结束
const p = Object.create(Profile.prototype); // 基于原型创建对象,但不调用构造函数
console.log(Object.getPrototypeOf(p) === Profile.prototype); // true:原型无误
console.log(p.name); // undefined:未运行构造逻辑,因此无 name
13.8 Reflect.construct
自定义 new.target
js
function Base() { // 声明构造函数
this.nt = new.target; // 记录构造时的 new.target
} // 构造函数结束
function N() {} // 声明另一个构造函数,用作 new.target
const r = Reflect.construct(Base, [], N); // 以 N 作为 new.target 构造 Base
console.log(r.nt === N); // true:new.target 已被定制
console.log(r instanceof Base); // true:仍然是 Base 的实例
14. 一句话速查表(Cheat Sheet)
- 用构造函数/类创建实例 → 务必用
new
。 - 在构造函数里用
new.target
防漏用;必要时抛错 或自动补救。 - 需要仅设原型且无副作用 → 用
Object.create(proto)
。 - 避免
new Boolean/Number/String
带来的真假与类型混乱。 - 箭头函数、方法不可构造;
Symbol/BigInt
不能new
。 Date()
(字符串)≠new Date()
(实例)。- 想要更灵活的构造流程(控制
new.target
)→Reflect.construct
。