JavaScript 学习笔记:手写 new
一、new 是什么
new 的核心作用,是执行一个构造函数,并返回一个实例对象。这个对象能够访问构造函数内部的属性,也能顺着原型链访问构造函数 prototype 上的方法。
比如:
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 muyu
在这个过程中,new 默默在背后做了四件事:
-
创建一个全新的空对象。
-
链接原型链:将这个新对象的内部属性
__proto__链接到构造函数的prototype对象上。 -
绑定
this并执行:将构造函数内部的this绑定到这个新对象上,并执行构造函数内部的代码(为新对象添加属性)。 -
处理返回值:如果构造函数没有显式返回一个对象,则默认返回这个新创建的对象。
所以,一个完整的 myNew 模拟函数,至少要完美复现这四步。
二、实现
最基础的版本:创建对象与绑定 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
}
这个版本干了什么:
-
我们手动创建了一个
{}。 -
利用
Array.prototype.slice.call(arguments, 1)拿到了除构造函数之外的其余参数。 -
利用
apply强行把Person内部的this掰到了空对象obj上。
存在的问题: 完全没有处理原型链。返回的 obj 就是一个普通的对象(它的原型指向 Object.prototype),导致实例无法访问 Person.prototype 上的方法(比如 sayHi)。
关联原型链
为了解决原型链丢失的问题,我们需要在对象创建阶段,就把它和构造函数的原型绑定起来。
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 car
这个版本干了什么:
- 弃用了
var obj = {},改用Object.create(constructor.prototype)。这行代码极其优雅,它直接创建了一个空对象,并且把这个空对象的隐式原型__proto__指向了传入的prototype。
存在的问题: 忽略了 JavaScript 中 new 的一个边界特性——返回值拦截。如果原生的构造函数显式 return 了一个引用类型(对象、数组、函数等),那么 new 表达式的最终结果应该是这个被 return 的对象,而不是引擎偷偷创建的那个实例;如果 return 的是基本数据类型(如字符串、数字),则忽略该返回值,依然返回实例。我们现在的版本无脑返回了 obj。
处理返回值与边界校验(完善版)
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 阶段表现异常,这符合预期。
这个版本干了什么:
-
增加了
typeof constructor !== "function"的安全校验。 -
接收了
constructor.apply的执行结果result。 -
严谨地判断了
result是否为object(注意排除null)或者是function。如果满足,说明构造函数想要“狸猫换太子”,我们就把result返回出去;如果不满足,说明返回的是基本类型或没写return,我们乖乖返回创建的实例obj。
存在的问题: 无。这已经涵盖了原生 new 绝大多数的核心逻辑。
三、总结
在实现 myNew 的过程中,我们串联起了以下几个重要的 JavaScript 知识:
-
原型链继承 (Prototype Chain):通过
Object.create()深刻理解了实例的__proto__是如何与构造函数的prototype产生羁绊的。 -
动态上下文 (Dynamic Context):再次利用
apply方法,证明了this的指向是可以在函数执行时被动态篡改的。 -
参数类数组 (Array-like Object):复习了通过
Array.prototype.slice.call(arguments)将类数组转化为真数组的经典技巧。 -
引用类型与基本类型的区别:在处理构造函数返回值时,深刻体会到了 JavaScript 引擎对值类型和引用类型的区别对待。