纵观JS对象的“简”与“繁”(下)

上篇文的最后,我们聊到了JS对象的一个重磅成员——原型模式,本以为迎来了对象领域的终极大boss,却发现它仍然存在局限性,这种局限就是:

不需要共享的也会被共享,导致出现期望之外的结果

什么不需要共享?比如,如果我们这样操作:

function Person(){
}

Person.prototype.friends=["1","2","3","4"];

var person1 = new Person();
var person2 = new Person();

person1.friends.push("5");

alert(person1.friends);
alert(person2.friends);`

会输出什么?你应该猜对了,两个都是

1,2,3,4,5

person2需要friends么,不一定,他需要push进来一个新的“5”吗?也不一定,那么在完全被动的情况下,因为我们把friends定义在了原型里,且由于person1对其进行了操作,就同时影响到了person2,显然,这是不合适的。

怎么破?

当我们进行了代码的简化,作用域的优化之后,似乎仍有进一步改善的空间。要么是纯粹的私有,要么是纯粹的共享,有中和的方式吗?

构造函数和原型组合模式

看到这里,你脑海中是否闪过一个念头——“我早该想到的!”。

没错,既然他们一个那么自私,一个那么大方,把它们结合起来不就有所平衡了么?

看代码:

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["lili","lucy"];
}
Person.prototype = {
    constructor : Person;
    sayName : function(){
        alert(this.name);
    }
}

这段代码,既让每个实例都有自己的一份属性副本,同时又共享着对方法的引用,是现在使用最广泛的方式,最大限度地节省了内存。

其实这里,相比具体的方案,我们更应该重视一个思路,就是“组合”,我们常常面临方案的选择,选A,或者B,或者C,多数情况下,每种方案都有其优点和局限性,而组合使用不失为一种“两全”之策。

but,虽然功能上它是兼备的,并不能说它是完美的。

回想一下,前面我们看到过的,直接创建对象也好,对象字面量也好,或者构造函数、原型模式,我们都倾向于去使其具有封装性,而这里它们却是相互独立的,能否将其封装起来呢?

动态原型模式

所谓动态原型,似乎不太好理解,不论是书籍还是网上能够查到的文章,大都简单罗列,而没有解释得很清楚,我反复看过一些代码和短文,下面是我的理解。

其实这个模式是在“构造函数和原型模式”的基础上做了两个方面的改良,看代码:

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        }
    }
}

一、封装,sayName没有单独放在函数的外部,而是内部。

二、创造私有实例的时候,去判断某个需要的共有方法是否已经存在,因为你可能已经在别的地方创建过了,而这种共有的方法只需要创建一次即可,如果有,忽略此段,没有,则对其初始化,避免时间和空间的浪费,动态体现在这里,从用意上来看,应该是一种预判,并不是共享方法这样写有什么好处。

需要注意的一点是:在这种模式下,不宜使用字面量重写原型,因为在已经创建了实例的情况下重写原型会切断现有实例和原型之间的联系。

到了这里,有关对象的高潮似乎已经过去,其实除此之外,还有两个“小”角色值得我们关注:

寄生构造函数模式

《高程》上说,在前面几种模式都不适用的情况下,使用这个模式,我觉得这么说有点不负责任,这样说等于没交代它的使用场景,有敷衍之嫌,不知道是原文的问题还是翻译的问题,或者是篇幅所限,暂且不管。

不妨顾名思义,分拆来看:

寄生:对某个东西有所依托

构造函数:用起来像构造函数

上代码:

function Person(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    }
    return o;
}

var friends = new Person("alien",29,"teacher");
friends.sayName();

你没看错,我也没写错,这里,除了创建实例的时候用了new操作符之外,它和工厂模式一模一样…

它看起来是个函数,但这个函数只起到封装作用,执行的结果是将在其中创建的对象给返回出来。

这种模式(就目前研究)主要是可以用来给一些内置构造函数增加新的方法,大家都知道,构造函数的属性和方法是可以改的,但直接改原生方法是不推荐的,那么寄生模式就派上用场,比如这样:

function SpecialArray(){
    var values = new Array();
        values.push.apply(values, arguments);
        values.toPipedString = function(){
            return this.join("|");
        }
        return values;
}
var a = new SpecialArray(2,6,8,9,4);
document.write(a.toPipedString());

上面这段代码输出的值将会是:

2|6|8|9|4

而正常情况下,都会输出

2,6,8,9,4

也就是说,根据个性化需要改变了数组的输出方式。

上面提过,“寄生”是一种依存关系,它是给已经存在的东西添加“功能”。

这么说你应该已经有大概的理解了,但到这一步我并不是很满意,感觉还可以挖掘出更多东西,如果你看到这篇文章,并且有不同的意见或者看法,欢迎交流。

稳妥构造函数模式

稳妥,听起来就很保守,也意味着安全。

先看代码:

function Person(name,age,job){
    // 创建要返回的对象
    var o = new object();
    //私有变量和方法

    //添加方法
    o.sayName = function(){
        alert(name);
    }
    //返回
    return o;
}

稳妥构造函数的两个特点:

1、没有公共属性

2、方法不引用this的对象

这种模式是在某些禁用了this和new的环境下可使用的。

最后这两种模式,使用的场景较少,知道就好,重点还是前面那些方法的练习和运用。

总结

写这两篇文章,是因为“对象”这个东西一直都像是难啃的骨头,但其实任何显得复杂或者困难的东西,都是从简单慢慢演变而来的,如果循序渐进地加入一些有血有肉的思考,就能更容易地对其进行理解和记忆。

写文过程中,我尽量做到不生搬概念,加入个人的思考过程,但认知有限,不足在所难免,还望读者朋友不吝赐教。

后面还会继续跟大家一起啃硬骨头,下次见!