面试官:手撕new操作符

201 阅读3分钟

了解new

在JavaScript中,new是用于创建对象实例的操作符,创建一个自定义的对象类型的实例或具有构造函数的内置对象的实例

实现new

我们常见的new用法

    // 构造函数
    function Person (name, age) {
        this.name = name
        this.age = age
    }
    
    let p2 = new Person('p2', 12)

    console.log(p2)
    // {name: 'p2', age: 12}

话不多说,直接代码操作,通过构建函数来返回一个自定义的对象类型的实例

// Con就是我们的构造函数入参,args是构造函数的入参
function myNew (Con, ...args) {
    let o = {}
    Con.apply(o, args) // 将构造函数的内置对象赋值给o
    return o
}

搞定!我们就完成了通过构造函数返回一个对象实例了,我们来测试下:

    // 构造函数
    function Person (name, age) {
        this.name = name
        this.age = age
    }
    let p1 = myNew(Person, 'p1', 12)
    let p2 = new Person('p2', 12)

    console.log(p1)
    // {name: 'p1', age: 12}
    console.log(p2)
    // {name: 'p2', age: 12}

看着没毛病,但是我们仔细瞧瞧打印出来的p1、p2实例

在两个实例的[[Prototype]]上,可以看到两个构造函数是不同的,myNew的是Object,而new的是Person,那如何将构造函数指向Person呢?这里补充下在JavaScript的原型对象上的一些特性:

// 构造函数原型的constructor指向构造函数
Person.prototype.constructor === Person
// 实例对象的[[Prototype]]指向它的构建函数的原型
p1.__proto__ === Object.prototype
p2.__proto__ === Person.prototype

借助这些特性,我们只需要将myNew创建实例的[[Prototype]]指向构造函数Person的prototype属性:

function myNew (Con, ...args) {
    let o = {}
    // 将实例和构建函数关联,实现构建函数创建的实例共享相同的原型
    o.__proto__ = Con.prototype
    Con.apply(o, args) // 将构造函数的内置对象赋值给o
    return o
}

考虑到_ proto _的兼容性,我们可以通过Object.setPrototypeOf来实现:

function myNew (Con, ...args) {
    let o = {}
    // 将实例和构建函数关联,实现构建函数创建的实例共享相同的原型
    Object.setPrototypeOf(o, Con.prototype)
    Con.apply(o, args) // 将构造函数的内置对象赋值给o
    return o
}

结合着MDN的描述,看看我们的myNew还有什么问题:

1、创建一个空的简单对象obj

2、如果构建函数的prototype属性是一个对象,将obj的[[Prototype]]指向构造函数的prototype属性,否则obj就是普通对象,[[Prototype]]只会指向Object.prototype

3、在构造函数中的所有 this 引用都指向 obj

4、如果构造函数返回非原始值,则覆盖正常的对象创建过程

以上,我们除了第4点,都完成了;接下来我们来实现第4点的功能;实现前我们先看看第4点的返回情况:

function Person1 (name, age) {
    this.name = name
    this.age = age
    return 1 // 这里可以是基本类型number、string、boolean、null、undefined
}
function Person2 (name, age) {
    this.name = name
    this.age = age
    return {} // Object类型
}
let p1 = new Person1('p1', 12)
let p2 = new Person2('p2', 12)

console.log(p1)
// Person1 {name: 'p1', age: 12}
console.log(p2)
// {}

结合第4点,我们再来处理下我们的myNew,判断构造函数返回的类型,再决定返回值:

function myNew (Con, ...args) {
    let o = {}
    // 将实例和构建函数关联,实现构建函数创建的实例共享相同的原型
    Object.setPrototypeOf(o, Con.prototype)
    let result = Con.apply(o, args) // result用于判断构造函数返回的类型
    
    return result && result instanceof Object ? result : o
}

至此,我们实现了new操作符