JavaScript Study Notes: Implementing new
1. What Is new?
The core purpose of new is to execute a constructor and return an instance object. That object can access properties defined inside the constructor and follow the prototype chain to methods on the constructor's prototype.
For example:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log("Hi, I am " + this.name);
};
var p = new Person("muyu", 18);
p.sayHi(); // Hi, I am muyuBehind the scenes, new does four things:
Create a brand-new empty object.
Link the prototype chain: connect the new object's internal __proto__ property to the constructor's prototype object.
Bind this and execute: bind the constructor's this to the new object and run its code, adding properties to that object.
Handle the return value: if the constructor doesn't explicitly return an object, return the newly created object by default.
A complete myNew simulation therefore needs to reproduce at least these four steps faithfully.
2. Implementation
The Basic Version: Create an Object and Bind this
function myNew(constructor) {
// 1. 创建一个空对象
var obj = {};
// 2. 获取传入的参数
var args = Array.prototype.slice.call(arguments, 1);
// 3. 将构造函数的 this 指向这个新对象,并执行
constructor.apply(obj, args);
// 4. 返回这个新对象
return obj;
}
// ================= 测试用例 =================
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log("Hi, I am " + this.name);
};
// 正确用例:属性成功挂载
var p1 = myNew(Person, "muyu", 18);
console.log(p1.name); // muyu
console.log(p1.age); // 18
// 错误用例:丢失了原型链
try {
p1.sayHi();
} catch (error) {
console.log(error.message); // p1.sayHi is not a function
}What this version does:
Manually create an empty {}.
Use Array.prototype.slice.call(arguments, 1) to retrieve all arguments after the constructor.
Use apply to force Person's this to point to the empty obj.
Remaining problem: it doesn't handle the prototype chain at all. The returned obj is an ordinary object whose prototype is Object.prototype, so it cannot access methods such as sayHi on Person.prototype.
Linking the Prototype Chain
To fix the missing prototype chain, connect the object to the constructor's prototype during creation.
function myNew(constructor) {
// 使用 Object.create 直接创建一个带有正确原型链的空对象
var args = Array.prototype.slice.call(arguments, 1);
var obj = Object.create(constructor.prototype);
constructor.apply(obj, args);
return obj;
}
// ================= 测试用例 =================
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log("Hi, I am " + this.name);
};
function Car(brand) {
this.brand = brand;
// 故意返回一个新对象
return { error: "I am not a car" };
}
// 正确用例:原型链关联成功
var p2 = myNew(Person, "muyu", 18);
p2.sayHi(); // Hi, I am muyu
// 错误用例:无法正确处理构造函数的显式返回值
var c1 = myNew(Car, "BMW");
console.log(c1.brand); // BMW
console.log(c1.error); // undefined
// 对比原生 new 的行为:
var c2 = new Car("Benz");
console.log(c2.brand); // undefined
console.log(c2.error); // I am not a carWhat this version does:
Replace var obj = {} with Object.create(constructor.prototype). This elegant line creates an empty object and points its implicit __proto__ to the supplied prototype.
Remaining problem: it ignores an edge behavior of new: return-value replacement. If a constructor explicitly returns a reference value—an object, array, function, and so on—the new expression should return that object rather than the instance the engine created. If it returns a primitive such as a string or number, that value is ignored and the instance is returned. Our current version blindly returns obj.
Return Values and Validation: A More Complete Version
function myNew(constructor) {
// 1. 边界拦截:确保传入的第一个参数是一个函数
if (typeof constructor !== "function") {
throw new TypeError("myNew function the first argument must be a function");
}
// 2. 准备参数和实例对象
var args = Array.prototype.slice.call(arguments, 1);
var obj = Object.create(constructor.prototype);
// 3. 绑定 this 并执行构造函数,接收返回值
var result = constructor.apply(obj, args);
// 4. 判断返回值类型
var isObject = typeof result === "object" && result !== null;
var isFunction = typeof result === "function";
// 如果构造函数返回了对象或函数,则直接返回该结果;否则返回我们创建的 obj
return isObject || isFunction ? result : obj;
}
// ================= 测试用例 =================
function Person(name) {
this.name = name;
return "ignore me"; // 返回基本类型,应被忽略
}
function Car(brand) {
this.brand = brand;
return { custom: "object" }; // 返回引用类型,应替换掉实例
}
const arrow = () => {}; // 箭头函数不能作为构造函数
// 正确用例 1:处理基本类型返回值
var p3 = myNew(Person, "muyu");
console.log(p3.name); // muyu
// 正确用例 2:处理引用类型返回值
var c3 = myNew(Car, "BMW");
console.log(c3.custom); // object
console.log(c3.brand); // undefined (实例被丢弃了)
// 错误用例 3:对非函数调用会抛错
try {
myNew({}, "test");
} catch (e) {
console.log(e.message); // myNew function the first argument must be a function
}
// 注:虽然箭头函数没有 prototype,但 Object.create(undefined) 会在底层报错或创建无原型对象,
// 原生 new 调用箭头函数会抛出 TypeError: arrow is not a constructor。
// 我们的 myNew 如果强行传入箭头函数,会在 apply 阶段或 Object.create 阶段表现异常,这符合预期。What this version does:
Add the safety check typeof constructor !== "function".
Capture constructor.apply's return value as result.
Carefully check whether result is an object, excluding null, or a function. If so, the constructor is substituting another object, and we return result. Otherwise, it returned a primitive or had no explicit return, so we return the created instance obj.
Remaining problems: none. This covers the vast majority of native new's core logic.
3. Summary
Implementing myNew connects several important JavaScript concepts:
Prototype-chain inheritance: Object.create() clarifies how an instance's __proto__ connects to its constructor's prototype.
Dynamic context: apply again demonstrates that this can be changed dynamically when a function executes.
Array-like arguments: a review of the classic Array.prototype.slice.call(arguments) technique for creating a real array from an array-like object.
Reference values vs primitives: handling constructor returns reveals how differently the JavaScript engine treats these two categories.
@startuml
skinparam ArrowColor #666666
skinparam DefaultTextAlignment center
skinparam NoteBackgroundColor #FEFEFE
skinparam NoteBorderColor #CCCCCC
title Handwritten myNew vs. Native new
start
:Call new Foo(...args) \nor myNew(Foo, ...args);
fork
partition "Application-level Simulation (myNew)\nCombines Object.create and apply" #FFF0F5 {
:Check that the first argument is a function;
:**Manually set up the prototype chain** \nvar obj = Object.create(Foo.prototype);
note left
**Key Overhead**
A JS API creates the object at the application level.
Another call to apply changes this,
splitting execution into multiple steps observable from JS.
end note
:**Invoke the function with a different this** \nvar result = Foo.apply(obj, args);
:**Manually check the return type** \ntypeof result === 'object' ...;
if (Is result a reference type?) then (Yes)
:Discard obj and\nreturn result;
else (No)
:Return obj;
endif
}
fork again
partition "JS Engine (Native new)\nDirect dispatch through the internal [[Construct]] method" #E0FFFF {
:The engine reads the bytecode and checks whether the target\nhas the internal [[Construct]] method;
note right
**Native Advantage**
The engine recognizes arrow functions (no [[Construct]])
and throws TypeError directly, without relying only on typeof.
end note
:**Internal allocation (OrdinaryCreateFromConstructor)** \nThe engine allocates memory directly\nand atomically sets its [[Prototype]] slot\nto Foo.prototype;
:**Trigger [[Call]] machine code execution** \nUse the newly allocated memory address as this \npush the execution context, and run the function body;
if (Did execution explicitly return an object?) then (Yes)
:Return that memory address;
else (No)
:Return the originally allocated instance address;
endif
}
end fork
:Return the resulting instance;
stop
@endumlDiagram unavailable. Use Show code to inspect the source.