js创建对象的多种方式和优缺点

参考文章:
《JavaScript高级程序设计》
JavaScript深入之创建对象的多种方式以及优缺点

创建对象

虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同 一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。

工厂模式

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, "hello word");
var person2 = createPerson("greg", 24, "Doctor");

函数 createPerson()能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。可以无 数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建 多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
缺点:对象无法识别,因为所有的实例都指向一个原型

构造函数模式

重写上面的例子:

 function Person(name, age, obj) {
  this.name = name;
  this.age = age;
  this.obj = obj;
  this.sayName = function() {
    alert(this.name);
  }
}
var person1 = new Person("nicholas", 29, "hello word");
var person2 = new Person("grag", 27, "doctor");

在这个例子中,Person()函数取代了 createPerson()函数。我们注意到,Person()中的代码 除了与 createPerson()中相同的部分外,还存在以下不同之处:

  • 没有显式地创建对象;
  • 直接将属性和方法赋给了 this 对象;
  • 没有 return 语句.
    要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:
    (1) 创建一个新对象;
    (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
    (3) 执行构造函数中的代码(为这个新对象添加属性);
    (4) 返回新对象。
    在这个例子中创建的所有对象既是 Object 的实例,同时也是 Person 的实例.
    创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式 胜过工厂模式的地方。在这个例子中,person1 和 person2 之所以同时是 Object 的实例,是因为所 有对象均继承自 Object(详细内容稍后讨论) .
    缺点:构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个 实例上重新创建一遍。。在前面的例子中,person1 和 person2 都有一个名为 sayName()的方法,但那 两个方法不是同一个 Function 的实例。
 this.sayName = new Function("alert(this.name)"); // 与声明函数在逻辑上是等价的 

更容易明白每个 Person 实例都包含一个不同的 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"); 

优点:解决了每个方法都要被重新创建的问题

缺点:如果对象需要定义很多方 法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

原型模式

function Person(name) {
}
Person.prototype = {
    name: 'kevin',
    getName: function () {
        console.log(this.name);
    }
};
var person1 = new Person();
var person2 = new Person(); 
alert(person1.sayName == person2.sayName);  //true

hasOwnProperty()方法,访问的是实例属性,有才会返回true,原型上的会返回false。
in 操作符会在通 过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中
由于 in 操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于 实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确 定属性是原型中的属性。

我们将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。 终结果相同,但有一个例外:constructor 属性不再指向 Person 了。前面曾经介绍过,每创建一 个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。而我们在 这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新 对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数

alert(friend.constructor == Person);    //false 
alert(friend.constructor == Object);   //true 

原型模式优化

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

以上代码特意包含了一个 constructor 属性,并将它的值设置为 Person,从而确保了通过该属 性能够访问到适当的值。
注意,以这种方式重设 constructor 属性会导致它的[[Enumerable]]特性被设置为 true。默认 情况下,原生的 constructor 属性是不可枚举的,因此如果你使用兼容 ECMAScript 5的 JavaScript引 擎,可以试一试 Object.defineProperty()。

原型模式再优化

function Person(name) {
}
Person.prototype = {
   name : "Nicholas",     age : 29,    
   job: "Software Engineer",     
   sayName : function () {
     alert(this.name);    
   } 
}; 
Object.defineProperty(Person.prototype,"constructor" {
  enumerable: false,
  value: Person
});

优点:实例可以通过constructor属性找到所属构造函数
缺点:原型模式该有的缺点还是有.

组合模式

构造函数模式与原型模式双剑合璧。构造函数模式用于定义实 例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本, 但同时又共享着对方法的引用,大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参 数;可谓是集两种模式之长。

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,Count,Van" 
alert(person2.friends);    //"Shelby,Count" 
alert(person1.friends === person2.friends);    //false 
alert(person1.sayName === person2.sayName);    //true 

在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方 法 sayName()则是在原型中定义的。而修改了 person1.friends(向其中添加一个新字符串),并不 会影响到 person2.friends,因为它们分别引用了不同的数组。
优点:该共享的共享,该私有的私有,使用最广泛的方式
缺点:有的人就是希望全部都写在一起,即更好的封装性

动态原型模式

function Person(name, age, obj) {
   //属性     
  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();

注意:使用动态原型模式时,不能用对象字面量重写原型
解释下为什么:

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype = {
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }
    }
}

var person1 = new Person('kevin');
var person2 = new Person('daisy');

// 报错 并没有该方法
person1.getName();

// 注释掉上面的代码,这句是可以执行的。
person2.getName();

使用字面量方式直接覆盖 Person.prototype,并不会更改实例的原型的值,person1 依然是指向了以前的原型,而不是 Person.prototype。而之前的原型是没有 getName 方法的,所以就报错了!

寄生构造函数模式

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

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" 

返回的对象与构造函数或者与构造函数的原型属 性之间没有关系
寄生构造函数模式,我个人认为应该这样读:

寄生-构造函数-模式,也就是说寄生在构造函数的一种方法。

也就是说打着构造函数的幌子挂羊头卖狗肉,你看创建的实例使用 instanceof 都无法指向构造函数.

彩蛋

1.使用字面量方式直接覆盖 Person.prototype,并不会更改实例的原型的值?

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype = {
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }
    }
}
var person1 = new Person('kevin');

以这个例子为例,当执行 var person1 = new Person('kevin') 的时候,
person1.的原型并不是指向 Person.prototype,
而是指向 Person.prototype 指向的原型对象,
我们假设这个原型对象名字为 O,
然后再修改 Person.prototype 的值为一个字面量,
只是将一个新的值赋值给 Person.prototype,
并没有修改 O 对象,也不会切断已经建立的 person1 和 O 的原型关系,
访问 person.getName 方法,依然会从 O 上查找.

再看个例子:

var a = {
     b: O
}

你看 a.b 指向了 O 对象,就相当于 Person.prototype 指向了原型对象这句话。

再看这个例子:

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype = {
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }
    }
}
var person1 = new Person('kevin');

当 new Person() 的时候,是先建立的原型关系,
即 person .proto = Person.prototype,
而后修改了 Person.prototype 的值,这就相当于:

// O 表示原型对象
var O = {};

var a = {
     b: O
}

先建立原型关系,指的是 c.proto = a.b = O
而后修改 Person.prototype 的值,相当于

var anotherO = {};
a.b = anotherO;

即便修改了 Person.prototype 的值,但是 c.proto 还是指向以前的 O
不知道这样解释的清不清楚,欢迎交流~
https://github.com/mqyqingfeng/Blog/issues/15

2.请问组合模式哪些方法和属性写在prototype,哪些写在构造函数里?
共享的写在 prototype 中,独立的写在构造函数中。
我们以弹窗组件举个例子:

function Dialog(options) {
   this.options = options;
}
Dialog.prototype.show = function(){...}

如果我们一个页面用到多个 dialog:

var dialog1 = new Dialog({value: 1});
var dialog2  = new Dialog({value: 2});

dialog1 和 dialog2 传入的参数不一样,写在构造函数中,我们可以通过

console.log(dialog1.options) 访问 dialog1 的配置选项

而对于 show 方法而言,所以的 dialog 都是公用的,所以写在 prototype 属性中.

加油~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342