美文网首页前端开发那些事儿
深度剖析new并手动封装(进阶)

深度剖析new并手动封装(进阶)

作者: 深度剖析JavaScript | 来源:发表于2020-09-15 12:40 被阅读0次

    昨晚做了一个梦,梦见有道关于new的笔试题,梦中的情景让我很困惑!
    所以今天再来彻底的理解new关键字到底做了什么,然后手动封装其功能

    首先,我们应该知道new的基本作用是用于构造函数生产对象

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    let p1 = new Person('alice', 12);
    console.log(p1)//Person {name: "alice", age: 12}
    

    上述例子中,通过new构造函数Person()产生p1对象,生产的对象可以继承Person原型上的属性和方法

    Person.prototype.show = function (name, age) {
        console.log(`我叫${this.name},我今年${this.age}岁了!`)
    }
    p1.show()
    

    上面代码执行结果是:我叫alice,我今年12岁了!
    补充说明一下什么是原型
    说白了原型(prototype)其实就是function对象的一个属性,它定义了构造函数构造出的对象的共有祖先,凡是该构造函数构造出的对象,都能继承原型上的属性和方法

    接着来看new关键字的内部原理
    我们知道,在JavaScript中,函数执行前面加new和不加new,执行的结果会变得不一样;那new到底做了什么?
    我们先来回顾一下,我之前提过通过new构造函数构造出对象内部原理

    1. 隐式创建this对象;在函数体最前面隐式的加上this = {}
    2. 执行this.xxx = xxx
    3. 隐式的返回this对象

    昨晚那个梦告诉我这不太对,有几个问题
    第一,生成的对象为什么会继承自构造函数的原型?如果按照上述步骤构建对象,构建过程跟原型并无关联;
    第二,执行的只是this.xxx=xxx这种格式的代码吗,那 其他语句执行吗?
    第三,new最终的结果都是返回this对象?如果构造函数有毛病,自己显示的return了呢,那结果是返回隐式的this对象和是返回显示return的东西呢?

    针对上诉三个疑问,我们一起来看看
    第一个问题,生成的对象为什么会继承自构造函数的原型?
    其实最开始构建对象的时候,隐式创建的this对象并非完全是空对象{},这个对象里面有个属性__proto__指向其构造函数的prototype,这就是对象能继承原型上的属性和方法的真实原因

    this.__proto__ : Student.prototype;
    

    第二个问题,执行的只是this.xxx=xxx这种格式的代码吗?
    下面我在构造函数Person中瞎填一些代码

    function Person(name, age) {
        console.log(this)//{name: "alice", age: 12}
        this.name = name;
        this.age = age;
        alert(1)//弹出1
        console.log(2);//控制台输出2
        function inner() {
            if (1) {
                console.log('inner')//inner函数执行打印出:inner
            }
        }
        inner();
    }
    let p1 = new Person('alice', 12);
    

    上面代码中,我们发现并非指执行this.xxx=xxx这种格式的代码,而是跟正常函数执行一致,会执行函数中的每条代码

    第三个问题,如果构造函数有显示return,结果返回的结果是显示的return还是隐式的this对象?
    我来显示的返回一些东西
    (1) 显示返回的类型为基础类型中原始值时,即number、string、boolean、null、undefined中的一种

    function Person(name, age) {
        this.name = name;
        this.age = age;
        return 1;
        //return 'abc';
        //return true;
        //return null;
        //return undefined;
    }
    let p1 = new Person('alice', 12);
    console.log(p1);
    

    显示返回原始值数据时,最终返回都原本的this对象

    显示返回原始值数据类型,返回结果都是隐式的this对象
    (2) 显示返回数组、对象、function
    function Person(name, age) {
        this.name = name;
        this.age = age;
        return [];
    }
    let p1 = new Person('alice', 12);
    console.log(p1);//[]
    

    若显示返回数组,返回的就是该数组

    function Person(name, age) {
        this.name = name;
        this.age = age;
        return {};
    }
    let p1 = new Person('alice', 12);
    console.log(p1);//{}
    

    若显示返回对象,返回的就是该对象

    function Person(name, age) {
        this.name = name;
        this.age = age;
        function fn(){
        }
        return fn;
    }
    let p1 = new Person('alice', 12);
    console.log(p1);//fn(){}
    

    若显示返回一个函数,最终返回的就是函数

    以上几个例子,可以得出跟我们最开始讲得不一致的地方,就是:
    如果构造函数本身有显示返回,显示返回的情况分两种
    第一,如果显示返回的数据类型是原始类型(number、string、boolean、undefined、null),那构造对象最终返回我们构建的this对象;
    第二,如果显示返回的值如果是引用值(比如array、object、function),那么new得出的结果不再是this对象,而是显示返回的东西;需特别注意这点

    所以,重新总结梳理一下new的内部原理:
    1. 在函数体最前头隐式创建this对象;相当于 let this = {}
    2. 给this对象身上添加__proto__属性指向构造函数的原型prototype
    3. 执行构造函数中的每一条语句(注意构造函数执行时,里面this指向绑定为新对象哦)
    4. 判断构造函数本身是否有显示的return数据:
    如果没有显示return数据,那么返回隐式的this对象
    如果有显示return数据,判断return的数据的是原始值还是引用值,如果是原始值,返回结果还是隐式的this对象;如果是引用值,返回的就是显示返回的东西

    经过上述总结梳理,可以模拟实现new的功能如下:

    Function.prototype._new = function (...arg) {
        let _this = {};//第一步,创建this对象
        _this.__proto__ = this.prototype;//第二步,添加属性指向构造函数原型
        let _constructorRes = this.apply(_this, arg);//第三步,让构造函数执行,并绑定构造函数中的this指向新对象
        //第四步,判断返回结果是什么类型
        if (typeof _constructorRes === 'number'
            || typeof _constructorRes === 'string'
            || typeof _constructorRes === 'boolean'
            || _constructorRes === null
            || _constructorRes === undefined) {//判断为原始值,返回_this对象
            return _this;
        } else {
            return _constructorRes;
        }
    }
    

    测试一下

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    let p1 = Person._new('alice', 12);
    console.log(p1);
    

    以上测试代码结果打印{name: "alice", age: 12},即let p1 = Person._new('alice', 12)let p1 = new Person('alice', 12)执行结果一致。
    经过多次测试以上模拟实现new没有问题!

    最后来解决我梦见的那道题目(大概)

    function Foo() {
        getName = function () {
            alert(1)
        }
        return this;
    }
    Foo.getName = function () {
        alert(2)
    }
    Foo.prototype.getName = function () {
        alert(3)
    }
    var getName = function () {
        alert(4)
    }
    function getName() {
        alert(5)
    }
    Foo.getName();
    getName()
    Foo().getName()
    getName()
    new Foo.getName()
    new Foo().getName()
    new new Foo().getName()
    

    前面四个问题,都比较容易,分别得出:2,4,1,1
    主要来看后面三个

    1. new Foo.getName()
      这里将Foo上的静态方法getName函数作为构造函数执行,所以得出结果为:2
      注意:这里是先Foo.getName,然后再new
    2. new Foo().getName()
      这里是先计算new Foo(),即调用的是Foo.prototype上的getName(),即得出结果为:3
    3. new new Foo().getName()
      以上代码相当于new((new Foo()).getName)();答案输出:3

    这题除了明白new的内部原理之外,得注意js运算符的优先级;
    以 new new Foo().getName()为例,小结一下上述new的优先级顺序:

    1. new Foo()
    2. 然后new Foo().getName
    3. 最后new new Foo().getName()

    以上就是关于new的全部内容!

    相关文章

      网友评论

        本文标题:深度剖析new并手动封装(进阶)

        本文链接:https://www.haomeiwen.com/subject/vjpxyktx.html