JavaScript - New之后发生了什么?

#new 操作符的官方定义

根据 MDN 文档new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。它是 JavaScript 中创建对象和实现面向对象编程的基础机制之一。

#new 操作符的工作原理

当我们使用 new 操作符调用构造函数时,会经历以下四个关键步骤:

  1. 创建新对象:首先创建一个空对象,作为将要返回的对象实例
  2. 设置原型链:将这个空对象的原型(__proto__)指向构造函数的 prototype 属性
  3. 绑定 this:将构造函数的 this 指向这个新创建的对象,并执行构造函数代码(为新对象添加属性和方法)
  4. 返回新对象:如果构造函数返回一个对象,则该返回值成为整个 new 表达式的结果。否则,返回新创建的对象

#MDN 文档补充说明

Classnew 操作符必需的,尝试不使用 new 调用一个类将抛出 TypeError。而对于构造函数,可以通过检查 new.target 属性来判断函数是否通过 new 调用:

检查是否通过new调用
JavaScript
function Car(color) {
if (!new.target) {
// 以函数形式调用
return `${color}`;
}
// 通过new调用
this.color = color;
}
const a = Car("红"); // a是"红车"
const b = new Car("红"); // b是 Car { color: "红" }

此外,不同的内置对象在使用 new 和不使用 new 调用时可能有不同的行为:

  • Array()Error()Function() 在被调用时和被构造时表现一致
  • Boolean()Number()String() 在调用时将参数转换为相应的原始类型,构造时返回包装对象
  • Date() 在调用时返回当前日期的字符串,相当于 new Date().toString()
  • Symbol()BigInt() 只能在不使用 new 的情况下调用
  • ProxyMap 只能通过 new 构造

#手动实现 new 操作符

理解了 new 操作符的工作原理后,我们可以手动实现一个模拟 new 操作符行为的函数:

手动实现new操作符
JavaScript
function myNew(Constructor, ...args) {
// 1.创建一个新对象
const obj = {};
// 2.设置原型链
Object.setPrototypeOf(obj, Constructor.prototype);
// 等价于: obj.__proto__ = Constructor.prototype;
// 3.绑定this并执行构造函数
const result = Constructor.apply(obj, args);
// 4.根据返回值类型决定返回结果
return result instanceof Object ? result : obj;
}

#使用示例

#基本使用

基本使用示例
JavaScript
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log(`你好,我是${this.name},今年${this.age}`);
};
}
// 使用内置new操作符
const p1 = new Person("晴天", 25);
// 使用自定义myNew函数
const p2 = myNew(Person, "下雨", 30);
console.log(p1 instanceof Person); // -> true
console.log(p2 instanceof Person); // -> true
p1.sayHello(); // -> 你好,我是晴天,今年25岁
p2.sayHello(); // -> 你好,我是下雨,今年30岁

#构造函数返回对象的情况

构造函数返回对象
JavaScript
function SpecialPerson(name) {
this.name = name;
// 显式返回一个对象
return {
specialName: name + '特别版',
type: 'special'
};
}
const sp = new SpecialPerson('多云');
console.log(sp); // -> { specialName: '多云特别版', type: 'special' }
console.log(sp instanceof SpecialPerson); // -> false

#对象属性为其他对象

对象的属性可以是另一个对象。以下是一个例子:

对象属性为其他对象
JavaScript
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
function Car(make, model, year, owner) {
this.make = make;
this.model = model;
this.year = year;
this.owner = owner; // owner是Person对象
}
const person1 = new Person("晴天", 30, "男");
const myCar = new Car("奔驰", "S级", 2023, person1);
console.log(myCar.owner.name); // -> "晴天"
console.log(myCar.owner.age); // -> 30

#原型属性和实例属性

要将新属性添加到相同类型的所有对象,你必须将该属性添加到构造函数的 prototype 属性中:

原型属性和实例属性
JavaScript
function Car() {}
const car1 = new Car();
const car2 = new Car();
console.log(car1.color); // undefined
Car.prototype.color = "原色";
console.log(car1.color); // '原色'
car1.color = "黑色";
console.log(car1.color); // '黑色'
console.log(Object.getPrototypeOf(car1).color); // '原色'
console.log(Object.getPrototypeOf(car2).color); // '原色'
console.log(car1.color); // '黑色'
console.log(car2.color); // '原色'

这个例子展示了实例属性与原型属性的区别,以及JavaScript原型链的工作方式。