js new的原理


学习本文内容内容前,你需要了解js中对象,原型链call(),bind(),apply(),arguments的用法。

new操作符的原理

在调用 new 的过程中做了四件事:

  1. 新生成对象:new关键字会首先创建一个空对象
  2. 链接到原型:将这个空对象的原型对象指向构造函数的原型属性,从而继承原型上的方法
  3. 绑定this:将this指向这个空对象,执行构造函数中的代码,以获取私有属性
  4. 返回新对象:如果构造函数返回了一个对象res,就将该返回值res返回,如果返回值不是对象,就将创建的对象返回

理解了new的原理,我们就可以手动实现一个new的创建了。

//创建一个 _new
function _new() {
    //定义空对象
    /*
     * arguments获取调用_new时所传入的实参
     * 它并不是一个真正的数组
     * 我们约定,调用_new时,所传入的第一个元素是构造器
     */
    const obj = {};

    // 取出参数列表的第一个参数(构造函数)
    /*
     * shift作用:
     * 1.取出数组的第一个元素(构造器)
     * 2.同时它会修改数组
     */
    const fn = [].shift.call(arguments);

    //手动指正obj的构造函数为 fn(链接原型)
    obj.__proto__ = fn.prototype;

    //调用fn,改变this为obj,传入剩余参数arguments(此时arguments是取出第一个元素后的部分)
    const res = fn.apply(obj, arguments);

    //考虑到fn函数中有return的原因,需要对res进行判断
    // return typeof res === 'object' || typeof res === 'function' ? res : obj;
    return res instanceof Object ? res : obj;
}

案例

//构造函数
function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}
Person.prototype.getName = function () {
    return this.name;
};

const person1 = new Person("hugh", "男");
const person2 = _new(Person, "hugh", "男");
console.log(person1); //Person {name: 'hugh', sex: '男'}
console.log(person2); //Person {name: 'hugh', sex: '男'}

//共享原型上的方法
console.log(person1.getName === person2.getName); //true

注解:

1.关于[].shift.call(arguments)

因为 arguments 是类数组对象,虽然有length属性,但是没有 shift 方法,故通过 call 方法改变执行上下文调用 shift 方法

2.关于obj.proto = fn.prototype

原型链使用, __proto__ 是每个对象拥有的隐藏属性,指向它构造函数的原型对象(prototypeprototype 是每个构造函数(Fuction.prototype除外)拥有的属性,它是函数所独有的,它是从一个函数指向一个对象(构造函数的原型对象)

若是对上面的shiftcall和原型链不是特别熟悉,我们再看看下面一个。

//不携带参数
function new1() {
    // 获取构造函数和传递的参数
    const [fn, ...args] = [...arguments];

    //1.通过Object.create()方法创建一个构造函数实例原型引用的一个对象
    const obj = Object.create(fn.prototype);

    //2.将构造函数中的this指向obj,执行构造函数代码,获取返回值
    const res = fn.apply(obj, args);
    //3.判断返回值类型
    return res instanceof Object ? res : obj;
}

//携带参数
function new2(fn, ...args) {
    //1.创建一个空对象,并将对象的__proto__指向构造函数的prototype 这里我两步一起做了
    const obj = Object.create(fn.prototype);
    //2.将构造函数中的this指向obj,执行构造函数代码,获取返回值
    const res = fn.apply(obj, args);
    //3.判断返回值类型
    return res instanceof Object ? res : obj;
}

四步拆解示例

把上面四步对应到 new Person('hugh','男') 逐行看:

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}
Person.prototype.getName = function () {
    return this.name;
};

// 等价于下面的 _new 过程:
const fn = Person; // 第 0 步:拿到构造函数
const obj = {}; // 第 1 步:创建空对象
obj.__proto__ = fn.prototype; // 第 2 步:链接原型,继承 getName
const res = fn.apply(obj, ["hugh", "男"]); // 第 3 步:this 指向 obj,执行后 obj.name/sex 被赋值
const person = res instanceof Object ? res : obj; // 第 4 步:返回新对象

走完这 4 步,person 就拥有了 name / sex 属性,并能通过原型链调用 getName()

一个容易踩的返回值坑

第 4 步”返回新对象”有个前提:构造函数没有返回对象

function A() {
    this.x = 1;
    return { y: 2 }; // 返回了一个对象
}
const a = new A();
console.log(a.x); // undefined(因为被返回的 {y:2} 顶替了)
console.log(a.y); // 2

function B() {
    this.x = 1;
    return 123; // 返回的是基本类型
}
const b = new B();
console.log(b.x); // 1(基本类型被忽略,仍返回 this 对象)

记住:构造函数里尽量不要 return 对象,否则 new 出来的就不是你期望的那个实例了。日常写构造函数也几乎不需要写 return。


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录