JS单例模式; super return 改变this

简单的单例

首先 js 中的函数是一种特殊的对象,这使得他可以存储属性

function aaa(params) {}
//undefined
aaa.lalala = 123
//123

js中的类是通过原型链继承的,因此类就是函数的集合体,可以通过babel编译看看。

因此可以写出简单的单例模式

class Singleton {
    constructor(data) {
        if (Singleton.instance) {
            return Singleton.instance;
        }
        Singleton.instance = this;
        this.data = data;
    }

    getData() {
        return this.data;
    }
}
const Singleton = require('./Singleton');
const obj1 = new Singleton('Data for the first call');
const obj2 = new Singleton('Data for the second call');

console.log(obj1.getData()); // "Data for the first call"
console.log(obj2.getData()); // "Data for the first call"

可以看到,我们其实是将创建好的实例挂载到了Singleton这个函数对象上,以此维持它的引用。

这样的单例模式十分简单,但若是要继承一下,以便复用呢?

继承的单例

class Singleton {
    constructor(data) {
        if (this.constructor.instance) {
            return this.constructor.instance;
        }
        this.constructor.instance = this;
        this.data = data;
    }

    getData() {
        return this.data;
    }
}

class ChildSingleton extends Singleton {
    constructor(data) {
        super(data);
    }
}


new ChildSingleton() //会发生什么?

继承时,情况发生了变化,我们回顾new关键字的工作原理,分析它的过程。

首先,new创建一个新对象,执行原型连接,并将执行函数的this绑定为该对象,就像下面这样

let obj = Object.create(Singleton.prototype)
obj.prototype.constructor = ChildSingleton //其实这里还得设置enumerable: false, writable: true
ChildSingleton.constructor.call(obj,data)

ChildSingleton.constructor()中只有一句 super()​,所以将constructor()压入函数调用栈,执行父类的构造方法,即super()。

constructor(data) {
    if (this.constructor.instance) {
        return this.constructor.instance;
    }
    this.constructor.instance = this;
    this.data = data;
}

在这里,this指向的依然是new关键字创建的新对象,该对象的原型对象上保存着对函数对象ChildSingleton 的引用,因此 this.constructor.instance​实际上是ChildSingleton.instance​,所以我们能将对象存放在子类函数对象的内部,而不是放在父类中,否则继承时不就乱了。

接下来便是平平无奇的赋值。

然后constructor()从函数调用栈弹出,恢复到子类的构造方法。第一次执行就完成了。

那第二次呢?

前面的步骤都差不多,但是第二次进入时 this.constructor.instance 为true

这导致了return this.constructor.instance; 的执行。

由于return了 父类构造方法直接结束,回到子类构造方法中,由于也没有语句执行了,直接结束。此时,由于父类的return 已经返回了一个对象,所以整个new调用的返回对象就是我们的super() return回来的对象,而不是new创建的那个对象,这里需要注意。

还是不对

如果给子类也添加属性呢?

class Singleton {
    constructor(data) {
        if (this.constructor.instance) {
            return this.constructor.instance;
        }
        this.constructor.instance = this;
        this.data = data;
    }

    getData() {
        return this.data;
    }
}

class ChildSingleton extends Singleton {
    constructor(data, extraData) {
        super(data);
        // 添加子类特有的属性
        this.extraData = extraData;
    }

    // 你也可以添加子类特有的方法
    getExtraData() {
        return this.extraData;
    }
}

new ChildSingleton(123,321) === new ChildSingleton() //仔细想想第二次?

这样对吗?对也不对。

虽然控制台打印了true,证明是同一个对象,但是如果打印两次的对象,就会发现extraData属性被改变了!

我直说了:super内的return语句会改变this的指向。

在 return this.constructor.instance; 一句返回之后,this就从new关键字自动创建的新对象被替换成了this.constructor.instance指向的,挂载在ChildSingleton的原型对象上的单例对象。而后代码仍在继续执行,this.extraData = extraData;

因此发现extraData属性的值发生了变化。

解决方式就是防止他在初始化后被构造函数改变。

class ChildSingleton extends Singleton {
    constructor(data, extraData) {
        super(data);
        // 如果 extraData 属性不存在,那么我们才设置它
        if (!this.extraData) {
            this.extraData = extraData;
        }
    }

    // 你也可以添加子类特有的方法
    getExtraData() {
        return this.extraData;
    }
}

如此便好。

posted @ 2024-08-28 11:16  等号酱  阅读(18)  评论(0)    收藏  举报