ES中没有类的概念,这也使其对象和其他语言中的对象有所不同,ES中定义对象为:“无序属性的集合,其属性包含基本值、对象或者函数”。现在常用的创建单个对象的方法为对象字面量形式。在常见多个对象时,使用工程模式的一种变体。
1.理解对象
1)对象的属性分两种:数据属性和访问器属性,每个类型的属性都具有相应的特性。
数据属性:包含一个数据值的访问位置,在这个位置可以读取和写入数据。其包含四种特性:[[Configurable]]、[[Enumerable]]、[[Writable]]、[[Value]]四个属性;
访问器属性:不包含数据值,它们包含一对儿getter和setter函数(不过,这两个函数都不是必需的)。访问器属性不能直接定义,必须使用Object.defineProperty()来定义。其包含四种特性:[[Configurable]]、[[Enumerable]]、[[Get]]、[[Set]]。访问器属性常用的使用方式是设置一个属性值导致其他属性发生变化。
2)使用Object.defineProperties()方法给对象同时定义多个属性。
3)使用Object.getOwnPropertyDescriptor()方法获取给定属性的特性。
2.创建对象
1)工厂模式:通过函数,解决创建多个相似对象的问题,但是工厂模式没有解决对象识别的问题(怎么知道一个对象的类型)。
2)构造函数模式:可以将其创建的实例标识为一种特定的类型,这正是构造函数模式胜过工厂模式的地方。(但是构造函数模式也有弊端,在构造函数模式包含函数属性时,通过构造函数建立的不同对象都会有一个对应的函数属性,完成相同的功能,但是这个函数属性却是通过Function函数定义的不同对象实例,这就说明构造函数模式创建的多个实例对象中总是包含完成相同功能但是却占用空间不同的Function实例,有些浪费。)
丨当然我们有种方式可以避免这种弊端:(假如构造函数中需要创建的函数属性名为:sayName),我们把sayName()
丨函数的定义转移到构造函数外部,而在构数内部,我们将sayName属性设置成等于全局的sayName()函数,这样一来,
丨由于sayName属性包含的是一个指向函数的指针,因此不同对象就共享了在全局作用域中定义的同一个sayName()函数了。
丨但是这种方式也存在弊端,如果有很多方法,那么就需要定义很多个全局函数,这就完全失去了封装性了。而这种问题可以通过原型模式来解决。
3)原型模式:不在构造函数中定义对象实例的属性信息,而是将这些信息直接添加到prototype的原型属性值——原型对象中,这样可以让所有创建的对象实例共享它所包含的属性和方法。
a.理解原型对象:每一个函数都有一个prototype原型属性,而这个属性的属性值为一个原型对象,而我们在该原型对象中定义构造函数需要定义的公有属性,这样在构造函数实例化对象时,所有的对象都会通过原型链访问到这些公有的属性,解决了构造函数模式的弊端。isPrototypeOf(测试一个原型对象是和对象实例的隐藏原型之间存在关系)、Object.getPrototypeOf()(获取一个对象的隐藏原型)、Object.hasOwnProperty(属性名)(判断一个对象是否有自己对应的实例属性)。 更具体的内容在我的另一系列博客中已经讲解了,不了解可以跳转到(深入理解js原型和闭包)一文。
b.查询实例对象属性和原型对象属性的常用方法:in:判断某个属性是否能够通过对象访问,无论该属性存在于实例中还是原型中;
for-in:返回对象所有可访问的、可枚举的属性,无论该属性存在于实例中还是原型中。(但是IE8及更早版本,在实例中定义和原型中同名的属性,且如果该属性在原型中是不可枚举时,那该实例中该属性也是不可返回的,其他浏览器正常);
Object.keys():获取对象上所有可枚举的实例属性;
Object.getOwnPropertyNames():获取对象上所有实例属性;
c.更简单的原型语法:每次使用“构造函数名.prototype”形式添加属性太麻烦了,我们使用一个包含所有属性和方法的对象字面量来重写整个原型对象,减少不必要的输入。(但这种方式有个小问题:那就是改变了constructor属性的指向了,如果对constructor属性有特殊需要,可以手动给constructor赋值为“构造函数名”,如果还觉得这样会改变constructor的[[Enumerable]],属性值,可以使用ES5中的Object.defineProperty()函数修改其属性)。但是这种重写原型对象的方式还存在一个弊端:实例对象的_proto_始终指向最初的原型对象,原型对象换为新的后,实例对象并不能找到它,那么实例对象并不能获取新原型对象中修改新增的属性,如下图所示:
d.原型的动态性:可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来(因为它是改变的指针指向)。但是其也存在c中所说的缺点。
e.原生对象(Object、Array、String等等)的原型也是可以修改添加新属性的,但是一般不建议修改。
f.原型模式的弊端:1.原型模式省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。(小问题)
2.(大问题):由于原型中所有属性是被大部分实例所共享的,这种共享对于函数非常合适,对于属性值玩为基本值的属性也可以,但是对于包含引用类型的属性来说,问题比较突出了。因为有的时候我们想对某个实例的这个属性单独作出更改,但是对于引用类型的属性会反映到其他实例属性上,所以我们很少见到有人单数使用原型模式。
4)组合使用构造函数模式和原型模式
这是定义引用类型最创建的一种模式,也是目前在ES中使用最广泛、认同度最高的一种模式。构造函数模式用于定义实例对象专属属性,而原型模式用于定义方法和共享的属性。这样创建的实例都会有自己的一份实例属性副本,但同时又共享着对方的引用,最大限度地节省了内存。
5)动态原型模式:在构造函数中初始化原型(仅在必要的情况下),通常是通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
6)寄生构造函数模式:这种模式只有在前述集中方法不适用的情况下使用,该方法类似于工厂模式,区别在于初始化实例是使用new操作符,这种方法也具有工厂模式的弊端,不能使用instance of来确定对象类型,一般不建议使用。
7)稳妥构造函数模式:这种模式用于构造稳妥对象(稳妥对象:没有公共属性,而且其中方法也不引用this的对象。这种对象适合在一些安全的环境中(这些环境会禁止使用this和new),或者在防止数据被其他应用程序改动时使用)。这种模式与工程模式相似,有两点不同:一是新创建对象的实例方法不引用this;二是不适用new操作符调用函数。同样这种模式也有工厂模式的缺点,不能使用instance of来确定对象类型,一般只在有特殊安全要求的情况下使用。
3.继承
1)原型链:原型链是JS中实现继承的主要方法。实现方式是重写原型对象,代之以一个新类型的实例。比如A想继承B的属性和方法,则A.prototype=new B(),通过重写A构造函数的原型对象,代之以B的一个实例,这样A的所有实例中就可以继承B的属性和方法。
a.默认的继承:因为原型对象也是对象,所有对象都是是Object的实例,所以所有实例中都可以继承访问到Object定义的属性和方法。
b.确定实例和原型的关系:通过instace of,如果一个实例通过_proto_往上找,相应的函数通过prototype这条链往下找,能找到同一个对象引用,则instance of 返回值就是true。同时一个实例通过_proto_往上找的过程,就是原型链。
c.覆盖超类型中的方法或者添加超类型中不存在的方法时,一定要放在继承之后。(覆盖:因为如果放在前面,超类型的方法会覆盖 它;添加新方法或者说定义自己的方法:因为继承时,是替换子类型的prototype,会覆盖原来原型中的方法,而之前我们说过,我们一般讲方法定义在原型中)。
d.原型链继承的问题:1.会造成原型中存在引用类型值(如果父类属性中包含引用类型值,重写子类原型后会造成子类原型中包含引用类型属性,造成该属性共享,之前说过如果想每个实例都有自己的引用类型属性,引用类型属性是不能放在原型中的)。2.创建子类的实例时,不能在不影响所有对象实例的情况下向超类型的构造函数传参。
2)借用构造函数——在子类构造函数的内部调用超类构造函数。
a.优势:借用构造函数解决了原型链无法传参的问题
b.问题:借用构造函数也无法避免之前讲过的构造函数模式存在的问题——方法属性无法共享。同时父类原型中的属性也无法继承,要实现继承父类只能之后构造函数模式,这显然是不合理的,所以借用构造函数模式也很少单独只用。
3)组合继承(伪经典继承)
使用原型链实现对原型属性和方法的继承,而通过借用构造函数实现对实例属性的继承。既通过在原型上定义方法实现了函数的复用,又能够保证每个实例能够拥有自己的属性。是JS中最常见的继承模式。
问题:组合式继承会造成两次调用父类的构造函数,会在子类原型对象上创造不必要的多余属性,后面会讲寄生组合式继承,解决这一问题。
4)原型式继承(道格拉斯.克罗克福德 2006年提出的),本质是对给定对象的浅复制
使用一个对象作为新对象的一个基础利用objcet函数来创建新对象,ES5新增了Object.create()函数来规范化原型式继承。在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下可以使用原型式继承。
问题:因为使用原型重写,所有包含引用类型值得属性会共享。
5)寄生式继承——和寄生构造函数、工程模式类似,把原型式继承过程封装,并在内部以某种方式增强对象。(比原型式继承做了一层封装罢了)
问题:使用寄生式为对象添加函数,不能做到函数复用。
6)寄生组合式继承
利用寄生式继承来创建超类型的一个副本来赋值给子类原型,而不必使用超类的构造函数,这样只调用一次超类的构造函数,避免了在子类原型对象上创建不必要的属性。寄生组合式继承时引用类型最理想的继承范式。