JS系列之如何创建对象

前言

俗话说“在js语言中,一切都对象”,而且创建对象的方式也有很多种,所以今天我们做一下梳理

最简单的方式

JavaScript创建对象最简单的方式是:对象字面量形式或使用Object构造函数

对象字面量形式
var person = new Object();
    person.name = "jack";
    person.sayName = function () {
    alert(this.name)
}
使用Object构造函数
var person = {
    name: "jack";
    sayName: function () {
      alert(this.name)
    }
}

明显缺点:创建多个对象时,会出现代码重复,于是乎,‘工厂模式’应运而生

工厂模式

通俗一点来理解工厂模式,工厂:“我创建一个对象,创建的过程全由我来负责,但任务完成后,就没我什么事儿了啊O(∩_∩)O哈哈~”

function createPerson (name) {
  var o = new Object();
  o.name = name;
  o.sayName = function () {
    alert(this.name)
  }
  return o
}

var p1 = new createPerson("jack");

明显缺点:所有的对象实例都是Object类型,几乎类型区分可言啊!你说无法区分类型,就无法区分啊,我偏不信!那咱们就来看代码吧:

var p1 = new createPerson("jack");
var p2 = new createPerson("lucy");

console.log(p1 instanceof Object);  //true
console.log(p2 instanceof Object);  //true

你看,是不是这个理儿;所以为了解决这个问题,我们采用‘构造函数模式’

构造函数模式

构造函数模式,就是这个函数我只管创建某个类型的对象实例,其他的我一概不管(注意到没有,这里已经有点类型的概念了,感觉就像是在搞小团体嘛)

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

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

var p1 = new Person("jack")
p1.sayName()  //"jack"

var a1 = new Animal("doudou")
a1.sayName()  //"doudou"

console.log(p1 instanceof Person)  //true
console.log(a1 instanceof Animal)  //true
console.log(p1 instanceof Animal)  //false(p1显然不是Animal类型,所以是false)
console.log(a1 instanceof Person)  //false(a1也显然不是Person类型,所以同样是false)

上面这段代码证明:构造函数模式的确可以做到对象类型的区分。那么该模式是不是已经完美了呢,然而并不是,我们来一起看看下面的代码:

//接着上面的代码
console.log(p1.sayName === a1.sayName)  //false

发现问题了吗?p1sayName竟然和a1sayName不是同一个,这说明什么?说明‘构造函数模式’根本就没有‘公用’的概念,创建的每个对象实例都有自己的一套属性和方法,‘属性是私有的’,这个我们可以理解,但方法你都要自己搞一套,这就有点没必要了
明显缺点:上面已经描述了,为了解决这个问题,又出现了一种新模式‘原型模式’,该模式简直就是一个阶段性的跳跃,下面我们来看分一下‘原型模式’

原型模式

这里要记住一句话:构造函数中的属性和方法在每个对象实例之间都不是共享的,都是各自搞一套;而要想实现共享,就要将属性和方法存到构造函数的原型中。这句话什么意思呢?下面我们来详细解释
当建立一个构造函数时(普通函数亦然),会自动生成一个prototype(原型),构造函数与prototype是一对一的关系,并且此时prototype中只有一个constructor属性(哪有,明明还有一个__proto__呢,这个我们先不在此讨论,后面会有解释)

WX20170527-205641@2x.png

这个constructor是什么?它是一个类似于指针的引用,指向该prototype的构造函数,并且该指针在默认的情况下是一定存在的

console.log(Person.prototype.constructor === Person)  //true

刚才说过prototype自动生成的,其实还有另外一种手动方式来生成prototype

function Person (name) {
  this.name = name
}
Person.prototype = {
  //constructor: Person,
  age: 30
}
console.log(Person.prototype)  //Object {age: 30}
console.log(Person.prototype.constructor === Person)  //false

Tips:为了证明的确可以为构造函数手动创建prototype,这里给prototype加了name属性。
可能你已经注意到了一个问题,这行代码:

console.log(Person.prototype.constructor === Person)  //false

结果为什么是false啊?大哥,刚才的prototype是默认生成的,然后我们又用了另外一种方式:手动设置。具体分析一下手动设置的原理:
1.构造函数的prototype其实也是一个对象

WX20170527-212325@2x.png

2.当我们这样设置prototype时,其实已经将原先Person.prototype给切断了,然后又重新引用了另外一个对象

WX20170527-212837@2x.png

3.此时构造函数可以找到prototype,但prototype找不到构造函数了

Person.prototype = {
  //constructor: Person,  // 因为constructor属性,我没声明啊,prototype就是利用它来找到构造函数的,你竟然忘了声明
  age: 30
}

4.所以,要想显示手动设置构造函数的原型,又不失去它们之间的联系,我们就要这样:

function Person (name) {
  this.name = name
}
Person.prototype = {
  constructor: Person,  //constructor一定不要忘了!!
  age: 30
}

画外音:“说到这里,你还没有讲原型模式是如何实现属性与方法的共享啊”,不要急,马上开始:

对象实例-构造函数-原型,三者是什么样的关系呢?

WX20170527-214615@2x.png

WX20170527-215233@2x.png

看明白这张图的意思吗?
1.当对象实例访问一个属性时(方法依然),如果它自身没有该属性,那么它就会通过__proto__这条链去构造函数的prototype上寻找
2.构造函数与原型是一对一的关系,与对象实例是一对多的关系,而并不是每创建一个对象实例,就相应的生成一个prototype
这就是原型模式的核心所在,结论:在原型上声明属性或方法,可以让对象实例之间共用它们

然后原型模式就是完美的吗?并不是,它有以下两个主要问题:
问题1:如果对象实例有与原型上重名的属性或方法,那么,当访问该属性或方法时,实例上的会屏蔽原型上的

function Person (name) {
  this.name = name
}
Person.prototype = {
  constructor: Person,
  name: 'lucy'
}
var p1 = new Person('jack');
console.log(p1.name);  //jack

问题2:由于实例间是共享原型上的属性和方法的,所以当其中一个对象实例修改原型上的属性(基本值,非引用类型值或方法时,其他实例也会受到影响

WX20170527-222024@2x.png

原因就是,当实例自身的基本值属性与原型上的重名时,实例就会创建该属性,留着今后自己使用,而原型上的属性不会被修改;但如果属性是引用类型值,如:ArrayObject,当发生重名时,实例是不会拷贝一份新的留给自己使用的,还是坚持实例间共享,所以就会出现上图中的情况

以上两个问题就是原型模式的明显缺点,为了改掉这些缺点,我们一般会采用一种组合模式“组合使用构造函数模式和原型模式”,其实在原型模式这一节,该模式已经有所应用了

组合使用构造函数模式和原型模式

这种模式可谓是集构造函数模式和原型模式之所长,用构造函数模式来定义对象实例的属性或方法,而共享的属性或方法就交给原型模式

function Person (name) {
  this.name = name  //实例的属性,在构造函数中声明
}

Person.prototype = {
  constructor: Person,
  sayName: function () {  //共享的方法存在原型中
    alert(this.name)
  }
}

注:此模式目前是ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法


下面要介绍的几个模式是针对不同场景的,而不是说组合使用构造函数模式和原型模式有什么缺点,又用这几个模式来弥补,不是这样的

动态原型模式

特点:共享的方法是在构造函数中检测并声明的,原型并没有被显示创建

function Person (name) {
  this.name = name;
  if (typeof this.sayName !== 'function') {  //检查方法是否存在
      console.log('sayName方法不存在')
      Person.prototype.sayName = function () {
      alert(this.name)
    }
  } else {
    console.log('sayName方法已存在')
  }
}

var p1 = new Person('jack');  //'sayName方法不存在'
p1.sayName(); //因为sayName不存在,我们来创建它,所以这里输出'jack'
var p2 = new Person('lucy');  //'sayName方法已存在'
p2.sayName();  //这时sayName已存在,所以输出'lucy'

Person构造函数第一次被调用时,Person.prototype上就会被添加sayName方法;《Javascript高级程序设计》一书说到:使用动态原型模式时,不能使用对象字面量重写原型。我们来理解一下:

WX20170529-123047@2x.png

分析:
1.p1实例创建,此时原型没有sayName方法,那我们就为原型添加一个
2.随后,我们以字面量的形式重写了原型,这时旧的原型并没有被销毁,而且它和p1还保持着联系
3.之后的实例,也就是这里的p2,都是与新原型保持联系;所以p1p2有各自的构造器原型,即使它们的构造器是同一个

WX20170529-122827@2x.png

所以切记:当我们采用动态原型模式时,千万不要以字面量的形式重写原型

寄生构造函数模式

了解此模式之前,我们先来想一个问题:构造函数为什么要用new关键字调用?代码说话:

WX20170530-173851@2x.png
WX20170530-174054@2x.png

我们发现什么?如果不是new方法调用构造函数,那么就要显式的return,否则构造函数就不会有返回值;但如果使用new,那就没有这个问题了

下面我们再来看寄生构造函数模式:

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

var p1 = new Person('jack');  //与工厂模式唯一不同之处:使用new调用
p1.sayName(); //jack

其实new不new都无所谓,因为我们已经显式的return o

WX20170531-204035@2x.png

那么寄生构造函数模式到底有什么应用场景呢?据《javascript高级程序设计》一书记载,举例:如果我们想创建一个具有额外方法的特殊数组,那么我们可以这样做:

function SpecialArray () {
  var values = new Array();
  Array.prototype.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'

最后重要的一点:该模式和构造函数和原型无缘,也就是不能区分实例类型,因为该模式生成的实例,它的构造函数都是Object,原型都是Object.prototype

WX20170531-205208@2x.png

稳妥构造函数模式

该模式与寄生构造函数相比,主要有两点不同:
1.创建对象实例的方法不引用this
2.不使用new操作符调用构造函数
按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下:

function Person (name) {
  var o = new Object();
  o.sayName = function () {
    alert(name)  //这里其实涉及到了闭包的知识,因此产生了私有属性的概念
  }
  return o
}

此模式最适合在一些安全的环境中(这些环境中会禁止使用this和new),同理,此模式与构造函数和原型也无缘

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

推荐阅读更多精彩内容