JS面向对象之 创建对象的方法及其优缺点

以下是基于《JavaScript高级程序设计》一书,对创建对象的几种方法所进行的整理。虽然工作中经常用到,但是概念性的东西,也是要整明白的呀,不仅会用,还要明白这是什么,为什么这么用 才是王道。

1、工厂模式

用函数来封装以特定接口来创建对象。

function createPerson(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 person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

person1.sayName();   //"Nicholas"
person2.sayName();   //"Greg"

函数createPerson()能够根据接受的参数来创建一个包含所有必要信息的Person对象。可以无数次的调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。

  • 缺点:虽然解决来创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

2、构造函数模式

构造函数可以用来创建特定类型的对象,使用构造函数重写上个例子:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    };    
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

可见,Person()函数与createPerson()不同之处在于:

  • 没有显示的创建对象;
  • 直接将属性和方法赋给来this对象;
  • 没有return语句;
  • 使用new操作符来创建实例。
    扩展 - 使用new操作符来创建实例实际上会经历以下4个步骤:
    1、创建一个新对象;
    2、将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
    3、执行构造函数中的代码;
    4、返回新对象。

在这个例子中所创建的实例person1person2即是Person的实例,同时也是Object的实例,通过instanceof操作符可以进行验证。

alert(person1 instanceof Object);  //true
alert(person1 instanceof Person);  //true
alert(person2 instanceof Object);  //true
alert(person2 instanceof Person);  //true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定类型,而这正是构造函数模式胜过工厂模式的地方。

  • 缺点:使用构造函数模式的主要问题,就是每个方法都要在每个实例上重新创建一遍。在上面例子中,person1person2都有一个名为sayName()的方法,但是那两个方法不是同一个Function实例,会导致不同的作用域链和标识符解析,不同实例上的同名函数是不相等的。
alert(person1.sayName == person2.sayName);  //false 

创建两个完成同样任务的Function实例是没有必要的,况且有this对象在,根本不用在执行代码前就把函数绑定在特定对象上面。因此,会有人提出把函数定义在构造函数外部来解决这个问题。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

function sayName(){
    alert(this.name);
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.sayName();   //"Nicholas"
person2.sayName();   //"Greg"

alert(person1 instanceof Object);  //true
alert(person1 instanceof Person);  //true
alert(person2 instanceof Object);  //true
alert(person2 instanceof Person);  //true

alert(person1.constructor == Person);  //true
alert(person2.constructor == Person);  //true

alert(person1.sayName == person2.sayName);  //true 

sayName()函数定义到构造函数外部之后,从上面代码可见,除了sayName()函数外,其他的实现与前面代码完全一致。
而在构造函数内部,将sayName属性设置成等于全局的sayName()函数。由于sayName是一个指向函数的指针,因此person1person2对象就共享来在全局作用域中定义的一个sayName()函数,这样做确实解决来两个函数做同一件事。可是又产生来新的问题:

  • 在全局作用域定义的函数只能被某个对象调用,这样让全局作用域有点名不副实;
  • 若是对象需要定义很多方法,那么需要定义多个全局函数,于是这个自定义的引用类型就毫无封装性可言了。

3、原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性指向通过调用构造函数而创建的实例的原型对象,对原型还不熟悉的小伙伴可先看看这篇this、原型和作用域
使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};

var person1 = new Person();
person1.sayName();   //"Nicholas"

var person2 = new Person();
person2.sayName();   //"Nicholas"

alert(person1.sayName == person2.sayName);  //true

在此,我们将sayName()方法和所有属性直接添加到了Personprototype属性中,构造函数变成了空函数,即是如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的,person1person2访问的都是同一组属性和同一个sayName()函数。

  • 缺点:
    1、它省略来为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值;
    2、由原型共享的本性所导致的,修改一个实例的属性,会对另一个实例的属性也造成影响,见以下代码:
function Person(){}

Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};

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

person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court,Van"
alert(person1.friends === person2.friends);  //true

4、组合使用构造函数+原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式+原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,这样,每个实例都会有自己的一份实例属性的副本,并且同时又共享着怼方法的引用,最大限度的节省了内存。另外,这种混合模式还支持向构造函数传递参数,集两种模式之长。下面代码重写了前面的例子:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}

Person.prototype = {
    constructor: Person,
    sayName : function () {
        alert(this.name);
    }
};

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court"
alert(person1.friends === person2.friends);  //false
alert(person1.sayName === person2.sayName);  //true

在上面例子中,实例属性都是在构造函数中定义的,而所有实例共享的属性constructor和方法sayName()则是在原型中定义的。而修改了person1.friends(向其中添加一个新字符串),并不会影响到person2.friends,因为他们分别引用了不同的数组。
可以说,这是目前用来定义引用类型的一种默认模式。

5、动态原型模式

有其他OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过构造函数中初始化原型(仅在必要条件下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

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);
        };
    }
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

注意方法部分,这里只在sayName()方法不存在的情况下,才会将他添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了,这里对原型所做的修改,能够立即在所有实例中得到反映。
注意,使用动态原型模式时,不能使用对象字面量重写原型,在this、原型和作用域中提到的,如果在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的关系。

6、寄生构造函数模式

这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像经典的构造函数。

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 friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();  //"Nicholas"

在这个例子中,Person函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用 new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式可以在特殊情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可以使用这个模式。

function SpecialArray(){       
    // 创建数组
    var values = new Array();
    
    // 添加值
    values.push.apply(values, arguments);
    
    //添加方法
    values.toPipedString = function(){
        return this.join("|");
    };
    
    // 返回数组
    return values;        
}

var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); // "red|blue|green"

alert(colors instanceof SpecialArray);

关于寄生构造函数模式,有一点需要说明:返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象类型。由于存在上述问题,在可以使用其他模式的情况下,不要使用这种模式

7、稳妥构造函数模式

首先,要明白什么是稳妥对象:指没有公共属性,而其他方法也不引用this的对象。
稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用thisnew),或者防止数据被其他应用程序(如Mashup程序)改动时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不适用new操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person构造函数重写:

function Person(name, age, job){
    // 创建要返回的对象
    var o = new Object();
    // 可以在这里定义私有变量和函数

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

    // 返回对象
    return o;
}

var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName();  //"Nicholas"

注意,在这种模式创建的对象中,除了使用sayName()方法之外,没有其他方法访问name的值。即使有其他代码会给这个对象添加方法和数据成员,但也不可能有别的方法访问传入到构造函数中的原始数据。
稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境下使用。
与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此 instanceof操作符对这种对象也没有意义。

总结

  • 工厂模式:
    使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。
  • 构造函数模式:
    可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此在多个对象间共享函数。
  • 原型模式:
    使用构造函数prototype属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的方法和属性。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350