【JavaScript系列】带你手写实现new运算符

1,767 阅读7分钟

我们经常会使用new运算符创建一个数组(Array)、对象(Object)等,那么new到底是什么?

new的定义

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

new的作用

根据new的定义,我们很容易地得出new的作用就是创建一个对象,其实并不是这样,new是用来继承的,我们通过这样的实例理解new继承

  • new的使用实例
function person(name, age){
    this.name = name;
    this.age = age;
}
person.prototype.job = 'web前端开发工程师';
person.prototype.salary = function(){
    return `${this.name}的薪资是20k`;
}

const Person = new person('小明','22');
console.log(Person);                      // person对象
console.log('name:', Person.name);        // 小明
console.log('age:', Person.age);          // 22
console.log('job:', Person.job);          // web前端开发工程师
console.log('salary:', Person.salary());  // 小明的薪资是20k
  • 我们使用new关键字创建了person对象,并赋值给Person,使Person具有了person中的属性和方法,从而得出结论:new关键字用来继承。

  • 上述的代码中,person具有的特点

  • 可以访问构造函数(person)中的属性

  • 可以访问到原型中(person.prototype)的方法

  • 如何实现访问构造函数中的属性
    对于这个问题,我们可以利用callapply实现继承,假设首先有一个父类函数

function parent({
    // 此时this指向Child
    this.name = '小明';
    this.age = 22;
}

接下来,有一个子类方法要继承上面的父类

function child({
    parent.call(this);  //call的作用:对象的冒充,可实现继承
}

我们看看发生了什么

const Child = new child(); 
console.log(Child);         //child: {name: '小明', age: 22}
console.log(Child.name);    // '小明'
console.log(Child.age);     // 22

这样,child拥有了parent的属性,Child也拥有了parent的属性,那么我们就实现了访问构造函数中的属性,也进一步说明了new是用来继承的

  • 如何实现访问构造函数中的方法
    通过上面例子中的输出,我们发现,person构造函数的方法是在原型链中,那么我们就可以直接使用赋值的方法实现访问构造函数中的方法,像这样:
let object = {};
object.__proto__ = Person.__proto__;
console.log(object'object');

它的输出:

我们可以看到在object的原型链上拥有了person构造函数中的方法。

解决了这两个难题,我们就可以自己写一个方法来模拟实现new运算符。

  • 模拟实现new运算符

第一步

const Person = NEW(person, '小明''22');

第二步

function NEW({
    let obj = {};
    construct = [].shift.call(arguments);   // 得到构造函数 Array.protptype原型上的方法对传入参数进行操作
    obj.__proto__ = construct.prototype;    // 访问方法
    construct.apply(obj, arguments);        // 让obj拥有Construct中的属性
    return obj;
}

插播,这里我们没有使用上面的call方法,而是使用了apply,这是因为下面的方法我们需要让obj拥有构造函数中的属性,这个属性它是以类数组形式接收,所以我们要使用apply方法传递数组参数。我们看一下结果

console.log(Person);                      // person对象
console.log('name:', Person.name);        // 小明
console.log('age:', Person.age);          // 22
console.log('job:', Person.job);          // web前端开发工程师
console.log('salary:', Person.salary());  // 小明的薪资是20k

到这里,看似是已经实现了new的方法,但是,设想一下,如果有人把构造函数写成了这样,我们看到nameageundefined

function person(name, age){
    return {
        name'小明',
        age22
    }
}
console.log('name:', Person.name);    // undefined
console.log('age:', Person.age);      // undefined

那么,针对这个问题,要如何改进代码?

function NEW({
    let obj = {};
    construct = [].shift.call(arguments);           // 得到构造函数 Array.protptype原型上的方法对传入参数进行操作
    obj.__proto__ = construct.prototype;            //访问方法
    const result = construct.apply(obj, arguments); // 让obj拥有Construct中的属性
    return typeof result === 'object' ? result : obj;
}

又如果有人把代码写成了这样呢

    function person(name, age){
        return 1;
    }
    console.log('name:', Person.name);  // undefined
    console.log('age:', Person.age);    // undefined

可以看到,return数字的时候是没有问题的。如果返回一个null

function person(name, age){
    return null;  // null是一个对象
}

这个时候就会报错

应该如何解决这个问题,null是一个对象,也就是说类型检测它会返回object,所以执行了三目运算中正确的条件,所以我们用||即可解决问题:

function NEW({
    let obj = {};
    construct = [].shift.call(arguments);           // 得到构造函数 Array.protptype原型上的方法对传入参数进行操作
    obj.__proto__ = construct.prototype;            //访问方法
    const result = construct.apply(obj, arguments); // 让obj拥有Construct中的属性
    return typeof result === 'object' ? result || obj : obj;
}

这个时候,我们要思考这个代码还有优化的余地么?答案显然是肯定的

function NEW({
    construct = [].shift.call(arguments);           // 得到构造函数 Array.protptype原型上的方法对传入参数进行操作
    let obj = Object.create(construct.prototype);
    // obj.__proto__ = construct.prototype;         //访问方法
    const result = construct.apply(obj, arguments); // 让obj拥有Construct中的属性
    return typeof result === 'object' ? result || obj : obj;
}
const Person = NEW(person, '小明''22');
console.log(Person);                               // person对象
console.log('name:', Person.name);                 // 小明
console.log('age:', Person.age);                   // 22
console.log('job:', Person.job);                   // web前端开发工程师
console.log('salary:', Person.salary());           // 小明的薪资是20k

上面的方法中,运用了Object.create()方法创建对象,它创建的对象可以直接使得新对象拥有原型上面的方法,所以可用其替换掉赋值访问原型的办法

上述文章如有错误之处,还请大家指正出来,我们共同进步~

最后,分享一下我的个人公众号「web前端日记」~