JS对象创建常用方式及原理分析

====此文章是稍早前写的,本次属于文章迁移@2017.06.27====

前言

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

最简单的方式

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

对象字面量形式

1 var person = new Object();
2   person.name = "jack";
3   person.sayName = function () {
4   alert(this.name)
5 }

使用Object构造函数

1 var person = {
2   name: "jack";
3   sayName: function () {
4     alert(this.name)
5   }
6 }

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

工厂模式

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

 1 function createPerson (name) {
 2   var o = new Object();
 3   o.name = name;
 4   o.sayName = function () {
 5     alert(this.name)
 6   }
 7   return o
 8 }
 9
10 var p1 = new createPerson("jack");

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

1 var p1 = new createPerson("jack");
2 var p2 = new createPerson("lucy");
3
4 console.log(p1 instanceof Object); //true
5 console.log(p2 instanceof Object); //true

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

构造函数模式

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

 1 function Person (name) {
 2   this.name = name;
 3   this.sayName = function () {
 4     alert(this.name)
 5   }
 6 }
 7
 8 function Animal (name) {
 9   this.name = name;
10   this.sayName = function () {
11     alert(this.name)
12   }
13 }
14
15 var p1 = new Person("jack")
16 p1.sayName() //"jack"
17
18 var a1 = new Animal("doudou")
19 a1.sayName() //"doudou"
20
21 console.log(p1 instanceof Person) //true
22 console.log(a1 instanceof Animal) //true
23 console.log(p1 instanceof Animal) //false(p1显然不是Animal类型,所以是false)
24 console.log(a1 instanceof Person) //false(a1也显然不是Person类型,所以同样是false)

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

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

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

原型模式

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

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

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

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

1 function Person (name) {
2   this.name = name
3 }
4 Person.prototype = {
5   //constructor: Person,
6   age: 30
7 }
8 console.log(Person.prototype) //Object {age: 30}
9 console.log(Person.prototype.constructor === Person) //false

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

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

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

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

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

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

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

1 function Person (name) {
2   this.name = name
3 }
4 Person.prototype = {
5   constructor: Person, //constructor一定不要忘了!!
6   age: 30
7 }

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

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

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

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

1 function Person (name) {
2   this.name = name
3 }
4 Person.prototype = {
5   constructor: Person,
6   name: ‘lucy‘
7 }
8 var p1 = new Person(‘jack‘);
9 console.log(p1.name); //jack

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


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

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

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

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

 1 function Person (name) {
 2   this.name = name //实例的属性,在构造函数中声明
 3 }
 4
 5 Person.prototype = {
 6   constructor: Person,
 7   sayName: function () { //共享的方法存在原型中
 8     alert(this.name)
 9   }
10 }

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

-----------------

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

动态原型模式

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

 1 function Person (name) {
 2   this.name = name;
 3   if (typeof this.sayName !== ‘function‘) { //检查方法是否存在
 4     console.log(‘sayName方法不存在‘)
 5     Person.prototype.sayName = function () {
 6       alert(this.name)
 7     }
 8   } else {
 9     console.log(‘sayName方法已存在‘)
10   }
11 }
12
13 var p1 = new Person(‘jack‘); //‘sayName方法不存在‘
14 p1.sayName(); //因为sayName不存在,我们来创建它,所以这里输出‘jack‘
15 var p2 = new Person(‘lucy‘); //‘sayName方法已存在‘
16 p2.sayName(); //这时sayName已存在,所以输出‘lucy‘

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

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

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

寄生构造函数模式

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

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

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

 1 function Person (name) {
 2   var o = new Object();
 3   o.name = name;
 4   o.sayName = function () {
 5     alert(this.name)
 6   };
 7   return o
 8 }
 9
10 var p1 = new Person(‘jack‘); //与工厂模式唯一不同之处:使用new调用
11 p1.sayName(); //jack

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

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

 1 function SpecialArray () {
 2   var values = new Array();
 3   Array.prototype.push.apply(values,arguments);
 4     values.toPipedString = function () {
 5     return this.join(‘|‘)
 6   }
 7   return values
 8 }
 9
10 var colors = new SpecialArray(‘red‘,‘blue‘,‘green‘);
11 alert(colors.toPipedString()) //‘red|blue|green‘

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

稳妥构造函数模式

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

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

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

结语

以上就是对js中创建对象的方式的总结,希望对大家有所帮助

时间: 2024-11-06 21:26:59

JS对象创建常用方式及原理分析的相关文章

Java Object 对象创建的方式 [ 转载 ]

Java Object 对象创建的方式 [ 转载 ] @author http://blog.csdn.net/mhmyqn/article/details/7943411 显式创建 有4种显式地创建对象的方式: 1.构造器:用new语句创建对象,这是最常用的创建对象的方式. 2.反射:运用反射手段,调用java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法. 3.克隆:调用对象的clone()方法. 4.序列化:运用反

js 对象创建及其继承的方法

重新看红皮书,觉得很多知识多看几遍确实是能看的更明白些.今天重温了一下对象创建和继承,就稍微记下来,巩固一下. js是面向对象的编程语言,ECMA-262吧对象定义为:“无序属性的集合,其属性可以包含基本值,对象或者函数”.每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是自定义的. 一,讲讲创建对象的几个方法 1. var person = new Object(); 2. var person = {}; 3. 工厂模式 4. 构造函数模式 5. 原型模式 6. 组合使

JAVA常用数据结构及原理分析

前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balabala讲了一堆,现在总结一下. java.util包中三个重要的接口及特点:List(列表).Set(保证集合中元素唯一).Map(维护多个key-value键值对,保证key唯一).其不同子类的实现各有差异,如是否同步(线程安全).是否有序. 常用类继承树: 以下结合源码讲解常用类实现原理及相互之间的差异. Collection (所有集合类的接口) List.Set都继承自Collection接口,查看JDK

js跨域请求方式 ---- JSONP原理解析

这篇文章主要介绍了js跨域请求的5中解决方式的相关资料,需要的朋友可以参考下 跨域请求数据解决方案主要有如下解决方法: 1 2 3 4 5 JSONP方式 表单POST方式 服务器代理 Html5的XDomainRequest Flash request 分开说明: 一.JSONP: 直观的理解: 就是在客户端动态注册一个函数 function a(data),然后将函数名传到服务器,服务器返回一个a({/*json*/})到客户端运行,这样就调用客户端的 function a(data),从而

js继承的常用方式

写在前面的话:这篇博客不适合对面向对象一无所知的人,如果你连_proto_.prototype...都不是很了解的话,建议还是先去了解一下JavaScript面向对象的基础知识,毕竟胖子不是一口吃成的. 我们都知道面向对象语言的三大特征:继承.封装.多态,但JavaScript不是真正的面向对象,它只是基于面向对象,所以会有自己独特的地方.这里就说说JavaScript的继承是如何实现的. 学习过Java和c++的都知道,它们的继承通过类实现,但JavaScript没有类这个概念,那它通过什么机

用js对象创建链表

//以下是一个链表类 function LinkedList(){ //Node表示要加入列表的项 var Node=function(element){ this.element=element; this.next=null; }; var length=0;//存储列表项的数量 var head=null;//head存储的是第一个节点的引用 //向链表尾部追加元素 this.append=function(element){ var node=new Node(element), cur

黑马day01xml 解析方式与原理分析

dom解析方式和sax解析

Spring Data 常用 API之原理分析 和 基本 API

Spring data 出现目的 为了简化.统一 持久层 各种实现技术 API所以 spring data 提供一套标准 API 和 不同持久层整合技术实现spring-data-commons 一套标准 APIspring-data-jpa 基于整合 JPA 实现 自己开发 Repository 只需要继承 JpaRepository 接口lCrudRepositorysave.delete.deteleAll.findAll.findOne.countPagingAndSortingRepo

Yii框架分析(六)——Yii的别名管理与对象创建管理

YiiBase类为YII框架的运行提供了公共的基础功能:别名管理与对象创建管理. 在创建一个php的对象时,需要先include这个类的定义文件,然后再new这个对象.在不同环境下(开发环境/测试环境/线上环境),apache的webroot路径的配置可能不一样,所以这个类的定义文件的全路径就会不同,Yii框架通过YiiBase的别名管理来解决了这个问题. 在创建对象时,需要导入对应类的定义,经常需要使用这5个函数:include().include_once().require().requi