Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端必会设计模式之(一) 单例模式 #4

Open
LyzSg opened this issue Jun 11, 2018 · 0 comments
Open

前端必会设计模式之(一) 单例模式 #4

LyzSg opened this issue Jun 11, 2018 · 0 comments

Comments

@LyzSg
Copy link
Owner

LyzSg commented Jun 11, 2018

单例构造函数

单例模式的思想在于保证一个特定类仅有一个实例。这意味着当您第二次使用同一个类创建新对象时,应该得到与第一次所创建对象完全相同的对象。

由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。

也就是说,我们希望通过构造函数创建的所有的对象实例完全相等。

有的人可能会问,每一次的new操作,构造函数都会隐式地创建一个this对象,这样内存里不是还是会有不止一个对象吗?是的,但是垃圾回收机制会帮我们立刻释放掉那些后面没有再被引用的值,所以到最后内存里还是只有一个对象。

接下来,我们来想如何实现这样一个构造函数:

function Test(name) {
    this.name = name;
    /* ... */
}
var a = new Test();
var b = new Test();

a === b; // true

Test里面应该怎么实现呢?

我们知道对象是引用值,要完全相等,必须是地址相同,即new Test()返回的this对象始终是同一个。

  • 于是我们想到可以把这个this对象保存在全局,但是这样并不好,因为任何人都可以修改该全局变量。

  • 那么把this对象保存到构造函数的静态属性中呢?静态属性同样是公开可访问的属性,外部可以修改,并不符合开闭原则。

  • 利用闭包,重写构造函数。代价是带来了额外的闭包开销。

function Test(name) {
    var instance = this;
    this.name = name;
    Test = function () {
        return instance;
    }
}
var a = new Test();
var b = new Test();

a === b;  // true

这样确实使得a和b完全相等,但是这样写好不好呢? 接着看下面代码:

function Test(name) {
    var instance = this;
    this.name = name;
    Test = function () {
        return instance;
    }
}

Test.prototype.hi = 'hi';

var a = new Test();

Test.prototype.hello = 'hello';

var b = new Test();

console.log(a.hi, b.hi);  // hi hi
console.log(a.hello, b.hello)  // undefined undefined

首先我在创建实例a之前,往Test的原型上添加了hi属性。然后,在创建实例b之前,往Test的原型上添加了hello属性。结果a和b都能找到hi,这是当然的也是我们所期待的,但都不能找到hello,这就不是我们所期待的了。这是为什么呢?

显而易见的是,前后两Test.prototype.hi = 'hi'和Test.prototype.hello = 'hello'这两个Test并不是同一个,后者指向的是function () {return instance;}。

第一次生成的this对象,也就是instance,他的__proto__属性指向了原来的Test的prototype,hi属性也是添加到这个上面的,而且后期new Test()返回的都是这个instance对象,所以a和b都能够找到hi。

而第二次是往新的Test的prototype上添加hello属性,而instance的__proto__始终指向原来的Test的prototype,所以都找不到hello属性。

第一种写法

知道了这个以后,我们就可以通过把新的Test的prototype指向原来的Test的Prototype来解决这个问题。

function Test(name) {
    var instance = this;
    this.name = name;
    var ptt = Test.prototype;  // 保存原Test的原型
    Test = function () {
        return instance;
    }
    Test.prototype = ptt; // 新Test的原型指向原Test的原型
}

Test.prototype.hi = 'hi';

var a = new Test();

Test.prototype.hello = 'hello';

var b = new Test();

console.log(a.hi, b.hi);  // hi hi
console.log(a.hello, b.hello)  // hello hello

第二种写法

此外,我们还可以通过立即执行函数,产生私有变量instance,再返回一个函数作为构造函数,那么,无论什么时候往Test的prototype身上添加属性,都是添到这个返回的构造函数里。

var Test = (function () {
    var instance;
    return function () {
        if(instance) {
            return instance;
        }
        instance = this;
        this.name = name;
    }
}())

var a = new Test();

Test.prototype.hello = 'hello';

var b = new Test();

console.log(a.hello, b.hello);  // hello hello

将构造函数变成单例构造函数

以上只是实现了一个可以产生单例的构造函数。假如有很多个构造函数需要修改成单例的形式,那我们是不是要修改全部的构造函数呢 ?我们更希望提取出一个公共方法,可以把普通的构造函数转成单例构造函数,这就是我们接下来要解决的问题。

首先我们来看一个场景:点击按钮,然后弹窗(动态创建一个div,并显示)。

<button id="oBtn">login</button>
<script>
    // 创建弹窗
    var CreateAlert = function (text) {
        var oDiv = document.createElement('div');
        oDiv.style.display = 'none';
        oDiv.innerText = text;
        document.body.appendChild(oDiv);
        return oDiv;
    }
    oBtn.onclick = function () {
        var oDiv = CreateAlert('hello');
        oDiv.style.display = 'block';
    }
</script>

image
我们点击多少次,他就创建多少个div,这明显不是我们想要的。我们希望不管点多少次,他出现的始终是同一个div。

这时我们就可以运用单例的思想,把这个CreateAlert改成只有在第一次执行的时候才创建div,其他情况直接引用第一次创建好的div。

var singleAlert = (function () {
    var oDiv = null;
    return function (text) {
        if (oDiv) {
            return oDiv;
        } else {
            oDiv = document.createElement('div');
            oDiv.style.display = 'none';
            oDiv.innerText = text;
            document.body.appendChild(oDiv);
            return oDiv;
        }
    }
}())
oBtn.onclick = function () {
    var oDiv = singleAlert('hello');
    oDiv.style.display = 'block';
}

这里运用的是立即执行函数的写法。细心的网友可能会问,你这个不是构造函数啊?然而我把这里var oDiv = singleAlret('hello');改成var oDiv = new singleAlret('hello');不就是了吗,而且实现效果都是一样的,无伤大雅。
这样,我们无论点击多少次按钮,他始终就只有一个div了:
image

假如这时又需要出现一个单例的构造函数,例如singleIframe什么的,如果不提取公共方法,我们又要再实现这个单例构造函数,比较麻烦。

因此为了提高复用性,我们需要实现一个getSingle方法,把普通的函数改造成单例的函数并返回。

var getSingle = function (fn) {
    /* ... */
    return function () {
        /* ... */
    }
}
// 创建弹窗
var CreateAlert = function (text) {
    var oDiv = document.createElement('div');
    oDiv.style.display = 'none';
    oDiv.innerText = text;
    document.body.appendChild(oDiv);
    return oDiv;
}
// 得到产生单例的函数
var singleAlert = getSingle(CreateAlert);

oBtn.onclick = function () {
    var oDiv = singleAlert('hello');
    oDiv.style.display = 'block';
}

那么该如何写getSingle这个方法呢?很简单,我们只需要保存第一次函数的返回值,第二次开始直接返回这个返回值即可:

var getSingle = function (fn) {
    var result = null;
    return function () {
        if(result === null) {
            result = fn.apply(this, arguments);
        }
        return result;
    }
}

这样下来,通过改造的到的singleAleat,再尝试一下点击多次按钮,发现只存在一个div。成功,了吗?

好的,新的风暴又出现了。以上这个getStyle确实可以把普通的函数改造成单例模式的函数,但并没有把普通的构造函数改造成单例的形式。看下面:

var getSingle = function (fn) {
    var result = null;
    return function () {
        if(result === null) {
            result = fn.apply(this, arguments);
        }
        return result;
    }
}

function Test(name) {
    this.name = name;
}
var singleTest = getSingle(Test);

var a = new singleTest('xixi');
singleTest.prototype.hello = 'hello';
var b = new singleTest();

console.log(a === b);  // false
console.log(a.hello, b.hello);  // hello hello
console.log(a.name, b.name); // xixi undefined

你会发现 a 和 b 并不绝对相等了,而且成员属性都不一样,简直本末倒置。可恶,怎么会这样?

因为前面我们并没有考虑到 new 的情况。
上面的例子在 var a = new singleTest('xixi')的时候,singleTest发生了下面的变化:

var singleTest = function () {
    // 1. 隐式创建了一个this对象 {}
    if (result === null) {
        // 2. 执行Test函数,把函数里面的this指向改成当前隐式创建的this,于是name属性被添加到this上。然而我们并没有new Test(),所以并没有返回值,即result是undefined。
        result = fn.apply(this, arguments);
    }
    // 3. 因为result不是引用值,所以覆盖不了this,所以最后返回的是this对象{name: 'xixi'}
    return result;
}

然后 var b = new singleTest() 的时候,又隐式创建了一个this对象,因为result不绝对等于undefined,所以直接返回这个this对象,它是一个空对象。

好的,破案了。那么我们要怎么结案呢?
关键就是把new时候隐式创建的this对象作为result保存下来,那么如果判断有没有new呢?
我们知道new时,隐式创建的this对象的__proto__指向当前函数的prototype;而没有new时,this表示的是window。于是,我们就可以利用这一点判断是否有new 了。

var getSingle = function (fn) {
    var result = null;
    // 给一个函数名,方便判断原型
    var F = function () {
        if(result === null) {
            result = fn.apply(this, arguments);
            // 通过instanceof来判断this的原型链上有没有F的原型
            if(this instanceof F) {
                result = this;
            }
        }
        return result;
    }
    return F;
}

这样,我们就完美地提取出一个公有方法,将普通的函数转为单例的形式了。

再来执行一下下面的代码:

function Test(name) {
    this.name = name;
}
var singleTest = getSingle(Test);
var a = new singleTest('xixi');
singleTest.prototype.hello = 'hello';
var b = new singleTest();
console.log(a === b);
console.log(a.hello, b.hello);
console.log(a.name, b.name);

image
完美!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant