重读JS(六)面向对象的程序设计 - (2)创建对象
本章内容
本文内容
- 工厂模式
- 构造函数模式
- 原型模式
- 组合使用构造函数模式和原型模式
- 动态原型模式
- 寄生构造函数模式
- 稳妥构造函数模式
Object构造函数或对象字面量都可以用来创建单个对象,缺点是:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。
工厂模式
工厂模式是软工中一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。在ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装特定接口创建对象的细节。
函数createPerson()能够根据接收的参数来创建一个包含所有必要信息的person对象。每调用一次,就返回一个包含三个属性一个方法的对象。虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎么知道一个对象的类型)。请看构造函数模式
构造函数模式
ECMAScript中的构造函数可以用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以是用构造函数模式将前面的例子重写如下:
Person()中的代码与createPerson()不同:
- 没有显式地创建对象
- 直接将属性和方法赋给了this对象
- 没有return语句
因为构造函数本身也是函数,只不过可以用来创建对象,为区分,应以大写字母开头。
前面例子中,person1和person2分别保存Person的一个不同的实例。两个对象都有一个constructor(构造函数)属性,用来标识对象类型,该属性指向Person.
提到检测对象类型,还是instanceof
操作符更可靠。在这个例子中的两个对象都既是Object的实例,同时也是Person实例。因为所有对象均继承自Object
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
以这种方式定义的构造函数是定义在Global对象(在浏览器中是window对象)中的,第八章将详细介绍讨论浏览器对象模型(BOM)
构造函数当做函数
call()和apply()使用(第五章Function部分):在对象o的作用域中调用的,因此调用后o就有了所有属性和sayName()方法
构造函数的问题
问题:每个方法都要在每个实例上重新创建一遍。即person1和person2虽然都有一个sayName()方法,但这两个方法不是同一个Function的实例。在ECMAScript中的函数是对象,因此每定义一个函数,就是实例化了一个对象,逻辑上等同于:
然而创建两个完成同样任务的Function函数着实没必要,况且有this对象(第五章Function部分)存在,根本不用再执行代码前就把函数绑定到特定对象上面。因此,可以将函数定义转移到构造函数外部来解决这个问题,使得person1和person2对象共享全局作用域中的同一个sayName()函数:
新问题:在全局作用域中定义的函数实际上只能被某个对象调用,这样全局作用域有点名不副实。更不能接受的是如果对象需要定义很多方法,emmm...我们这个自定义的引用类型就丝毫没有封装性可言了。接下来看原型模式
原型模式
我们创建的每个函数都有一个prototype(原型),这个属性是一个指针,指向一个对象
而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法
字面意思:prototype就是通过调用构造函数而创建的那个对象实例的原型对象。
使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
换句话说:不必在构造函数中定义对象实例的信息,可以将这些信息直接添加到原型对象中。例子:
⭐理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。即Person.prototype.constructor指向Person。通过这个构造函数,我们还可以为原型对象添加其他属性和方法。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,则都是从Object继承而来。当创建一个新实例后,该实例的内部将包含一个指针(内部属性[[Prototype]])指向构造函数的原型对象。脚本中没有标准的方式访问[[Prototype]],但在Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__
。
注意:这连接存在于实例与构造函数的原型对象之间,而非实例与构造函数之间。(第一遍强调!!!)
到此为止,插播个个人理解:
对于构造函数来说,prototype是作为构造函数的属性,指向构造函数的原型对象。
对于对象实例来说,prototype是对象实例的原型对象。所以prototype即是属性,又是对象。表示person1.__proto__
即为person1实例的原型对象
person1.__proto__ === Person.prototype
为true
Object.getPrototypeOf(person1) == Person.protoytype
为true
Object.getPrototypeOf(person1).name
输出'Nicholas'
Person.prototype.isPrototypeOf(person1)
为true
小插播:看完三四五章以及第六章到此为止,终于将对象实例、constructor(构造函数)、property(属性)、prototype(原型)、原型对象(叫构造函数的原型对象更好理解)、__proto__搞明白了。鬼知道我看ES6的时候有多吃力,不得不返回来重新看这本书。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标时具有给定名字的属性。
先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,则返回属性的值。
如果没找到,则继续搜索指针指向的原型对象,如果找到了,则返回。
因此,当为实例新创建一个与它原型对象中已存在的属性相同的属性时,会屏蔽原型对象中的那个属性,但不会修改那个属性。可用通过delete(person1.属性名)来重新访问原型对象中的属性
可以用person1.hasOwnProperty(属性名)
检测一个属性是存在于实例中,还是存在于原型中。
true表示存在于实例中。
ECMAScript5的Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()方法
原型与in操作符
两种方式使用in操作符:单独使用、在for-in中使用。
单独使用
单独使用时,"name" in person1
无论该属性存在于实例还是原型中,都会返回true。和.hasOwnProperty不同。可以两者结合判断具体存在于实例还是原型中。
for-in
返回的是所有能够通过对象访问的、可枚举的属性。包括实例中的和原型中的属性。屏蔽了不可枚举的属性(即将[[Enumerable]]标记的属性)的实例属性也会在for-in中循环返回。因为根据规定,所有开发人员定义的属性都是可枚举的——只有在IE8及更早版本例外。
理解:屏蔽了不可枚举的属性(即将[[Enumerable]]标记的属性)的实例属性:
要取得对象上所有可枚举的实例属性,,可以使用ECMAScript5的Object.keys()方法。
如果想要得到所有实例属性,无论是否可以枚举,都可以使用Object.getOwnPropertyNames()
更简单的原型语法
在这里构造函数的prototype属性不再指向原型对象,而是指向了一个新对象。此时constructor属性不再指向Person了。这里的修改本质上是完全重写了默认的prototype对象,因此constructor属性也变成了新对象的constructor属性(指向Object构造函数),不再指向Person。此时:
用Object.defineProperty(,)
constructor指向修改回Person
原型的动态性
由于在原型中查找值的过程是一次搜索吗、,因此我们对原型对象所做的任何修改都能立即从实例上反映出来——即使先创建了实例后修改原型也照样如此。但不能重写整个原型对象:
记住:实例中的指针仅指向原型,而不指向构造函数(第二遍强调!!!)
原生对象的原型
原型模式的重要性仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式。例子:
在Array.prototype中可以找到sort()方法
在String.prototype中可以找到substring()方法
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。例子:
原型对象的问题
1.省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都取得想通的属性值。
2.共享属性对于函数合适,对于包含基本类型的属性也说得过去,但对于包含引用类型的属性来说,问题突出。实例一般都是要有属于自己的全部属性的,因此很少有人单独使用原型模式。
于是就出现了组合使用构造函数模式和原型模式。
⭐组合使用构造函数模式和原型模式
构造函数用于定义实例的属性,原型模式用于定义方法和共享的属性。使得每个实例都会有自己的一份实例属性的副本,又同时共享着对象的引用,最大限度的节省了内存,还实现了向构造函数传递初始化参数。
动态原型模式
注意加粗部分的代码,这里只在sayName()方法不存在的情况下,将它添加到原型中。这段代码只会在初次调用构造函数时才会执行,此后原型已经完成,不需要在做什么修改了。
寄生构造函数模式
通常,在前述的几种模式都不使用的情况下,可以使用寄生构造函数模式。基本思想是:床架拿一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面看,这个函数又很像典型的构造函数。
具体用的时候再做深入解释吧
稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其他方法也不引用this对象。最适合在一些安全环境中(这些环境会禁止使用this和new),或者防止数据被其他应用程序改动时使用。
具体用的时候再做深入解释吧
小节
本节方法集合
.constructor
每个实例只有一个构造函数:a.constructor == A
.__proto__
实例的原型对象:person1.__proto__ === Person.prototype
Object.getPrototypeOf()
Object.getPrototypeOf(person1) == Person.protoytype
.isPrototypeOf()
Person.prototype.isPrototypeOf(person1)
.hasOwnProperty()
检测一个属性是不是实例自己的(非原型的)true表示存在于实例中,false表示不在实例中,但也不一定在原型中呀~`person1.hasOwnProperty('name')
Object.getOwnPropertyDescriptor()
得到实例自己的属性的特性Object.getOwnPropertyDescriptor(person1, 'name');
Object.getOwnPropertyDescriptors()
得到实例自己的属性的所有特性Object.getOwnPropertyDescriptors(person1);
in
无论该属性存在于实例还是原型中,都会返回true。"name" in person1
for-in
返回的是所有能够通过对象访问的、可枚举的属性。因为根据规定,所有开发人员定义的属性都是可枚举的——只有在IE8及更早版本例外。不可枚举的属性是将[[Enumerable]]标记的属性
Object.keys()
取得对象上所有可枚举的实例属性Object.keys(Person.prototype)
Object.getOwnPropertyNames()
得到所有实例属性,无论是否可以枚举Object.getOwnPropertyNames(Person.prototype)
Object.defineProperty()
修改属性的默认特性,建议不要在IE8中使用。例子:
将Person的constructor指向修改回Person Object.defineProperty(Person.prototype,'construct',{enumerable:false;value:Person})