1. 面向对象
面向对象语言有一个标志:都有类的概念。通过类可以创建任意多个具有相同属性和方法的对象。ECMAScript中没有类的概念,因此JavaScript中的对象夜雨基于类的语言中的面向对象有所不同。
定义:
- 无序属性的机会
- 属性可以包含:基本值、对象或者函数
对象:一组没有特定顺序的值,对象的每个属性或方法都有一个名字,每个名字都映射到一个值,想象成散列表:无非就是一组名值对,其中值可以是数据或者函数。每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型(Object类型、Array类型、Date类型、RegExp类型、Function类型、基本包装类型[Boolean类型、Number类型、String类型]、单体内置对象[Global对象、Math对象]),也可以是开发人员定义的类型。
2. 理解对象
示例代码1:
var person = new Object(); person.name = 'Tim Zhang'; person.age = 22; person.job = "Software Enginner"; person.sayName = function () { console.log(this.name); };
- 创建了person对象
- person对象有三各属性:name, age, job,一个方法:sayName()
- sayName()调用了name属性
- 早起JS开发人员创建对象的流行方式
示例代码2:
var person = { name: 'Tim Zhang', age: 22, job: "Software Engineer", sayName: function () { console.log(this.name); } };
- 效果与示例1一样
- name, age, job, sayName都没有带引号
- 属性、方法间采用“逗号“分割
2.1. 属性类型
ECMAScript中有两种属性:数据属性、访问器属性。标准文档中使用两队方括号标识特性是内部值,如:[[Enumerable]]
2.1.1. 数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性有4个描述其行为的特性:
- [[Configurable]]:这个特性设置为true或者false,用于控制能否执行:delete操作删除属性、修改属性的特性、把属性修改为访问器属性。对于直接在对象上定义的属性,默认为true
- [[Enumerable]]:这个特性设置为true或者false,用于控制:能否通过for-in循环访问属性。对于直接在对象上定义的属性,默认为true
- [[Writable]]:这个特性设置为true或者false,用于控制:能否修改属性的值。对于直接在对象上定义的属性,默认为true
- [[Value]]:包含这个属性的数据值、读取操作从这个位置读、写入操作写入值都这个位置,默认值为undefined
以person对象为例,person对象定义的属性,每个属性的Configurable、Enumerable、Writable特性都被设置为true,而Value特性被设置为指定的值。
var person = { name: 'Tim Zhang' }; /* 1. 创建了一个 person 对象 2. 创建一个 name 属性 * name 属性为数据属性 * name 属性具有4个特性:Configurable、Enumerable、Writable、Value * name 属性的前三各特性为true * name 属性的Value特性被设置为 "Tim Zhang" */
修改4个特性的默认值方法:使用ECMAScript 5的Object.defineProperty()方法,这个方法接收三各参数:属性所在的对象,属性名(字符串格式),一个描述符对象(可以使用字典的方式来定义,属性必须是:configurable, enumerable, writable, value)。例:
var person = {}; Object.defineProperty( person, 'name', { configurable: false, writable: false, value: 'Tim Zhang' } ); /* 1. 创建了一个 person 对象 2. 给 person 对象创建了 'name' 属性 3. 'name' 属性设置了3个特性值: * configurable: false 表示 name 属性不可被删除(delete person.name 不成功,严格模式下会抛出异常),
并且一旦定义为false,不能再次通过defineProperty来修改这个特性为true了。 * writable: false 表示 name 的属性为只读 * value: 'Tim Zhang' 表示 name 的值为 'Tim Zhang' */
可以多次调用defineProperty,但是把configurable特性设置为false之后,就有了限制:
*. 如果不指定configurable: configurable、enumerable、writable特性的默认值都设置为false
*. 第一次指定了 writable 特性,则之后可以修改writable, 否则都不能再次调用defineProperty
2.1.2. 访问器属性
访问器属性不包含数据值:包含一对getter、setter函数(两个函数不是必需的),读取访问器属性时,会调用getter函数,由这个函数返回有效的值,写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据,访问器属性也有4个特性:
- [[Configurable]]:这个特性设置为true或者false,用于控制能否执行:delete操作删除属性、修改属性的特性、把属性修改为访问器属性。默认为true
- [[Enumerable]]:这个特性设置为true或者false,用于控制:能否通过for-in循环访问属性。对于直接在对象上定义的属性,默认为true
- [[Get]]:在读取属性时调用的函数,默认值undefined
- [[Set]]:在写入属性时调用的函数,默认值undefined
访问器属性不能直接定义,必须使用Object.defineProperty()来定义,示例:
var book = { _year: 2004, edition: 1 }; Object.defineProperty( book, 'year', { get: function() { return this._year; }, set: function(newValue) { if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } } ); book.year = 2005; console.log(book.edition); // 2
2.2. 定义多个属性
使用Object.defineProperties()方法,一次定义多个属性,方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中药添加或修改的属性一一对象。示例:
var book = {}; Object.defineProperties(book, { _year: { value: 2004 }, edition: { value: 1 }, year: { get: function () { return this._year }, set: function (newValue) { if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } }, });
2.3. 读取属性的特性
ECMAScript 5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符,这个方法接收两个参数,属性所在的对象、尧都区描述符的属性名称,返回值是一个对象:访问属性(返回的对象属性有:configurable、enumerable、get、set),数据属性(返回的对象属性有:configurable、enumerable、writable、value)。例:
var descriptor = Object.getOwnPropertyDescriptor(book, '_year'); console.log(descriptor.value); // 2004 console.log(descriptor.configurable); // false console.log(descriptor.get) // 'undefined' descriptor = Object.getOwnPropertyDescriptor(book, 'year'); console.log(descriptor.value); // undefined console.log(descriptor.enumerable); // false console.log(descriptor.get) // 'function'
3. 创建对象
虽然Object构造函数或者对象字面量都可以用来创建单个对象,单这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
3.1. 工厂模式
工厂模式是一种设计模式:抽象了创建具体对象的过程,通过采用一种函数来创建对象:用函数来封装以特定接口创建对象的细节,示例:
function createPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function () { console.log('this.name); }; return o; } var p1 = createPerson('Tim', 22, 'SE'); var p2 = createPerson('Tom', 23, 'PM');
工厂函数虽然解决了创建多个相似对象的问题,但是没有解决对象识别的问题(即:怎样知道一个对象的类型)。
3.2. 构造函数模式
ECMAScript中的构造函数可用来创建特定类型的对象,比如:Object、Array这样的原生构造函数,在运行时会自动出现在执行环境中,此外也可以创建自定义的构造函数,从而定义自定义对象类型的属性、方法,例:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function () { console.log(this.name); }; } var p1 = new Person('Tim', 22, 'SE'); var p2 = new Person('Tom', 23, 'PM');
与工厂函数的不同:
- 没有显示地创建对象
- 直接将属性、方法赋值给 this 对象
- 没有 return 语句
创建Person的新实例,必须使用 new 操作符,以这种方式调用构造函数实际上会经历一下4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋值给新对象(因此 this 就指向了这个心对象)
- 执行构造函数中的代码(为这个心对象添加属性)
- 返回心对象
上面创建的p1、p2两个对象都有一个 constructor (构造函数)属性,该属性指向 Person,如下判断都为true:
p1.constructor === Person;
p2.constructor === Person;
对象的contructor属性最初是用来表示对象类型的,但是提到检测对象类型,还是 instanceof 操作符要更可靠一些,如:
p1 instanceof Object; // true p1 instanceof Person; // true p2 instanceof Object; // true p2 instanceof Person; // true
创建自定义的构造函数意味着将来可以将它的示例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。
3.2.1. 将构造函数当作函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同,如下示例可看成使用不同的调用方式的差异:
// 当作构造函数使用 var p1 = new Person('Tim', 22, 'SE'); p1.sayName(); // Tim // 当作普通函数使用 Person('Tom', 23, 'PM'); // 添加到window对象 window.sayName(); // Tom // 在另一个对象的作用域中调用 var o = new Object(); Person.call(o, 'Bob', 24, 'AE'); o.sayName() // Bob
3.2.2. 构造函数的问题
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍,p1、p2都有一个名为sayName()的方法,但这两个方法不是同一个Function的示例(ECMAScript中的函数是对象),因此每定义一个函数,也就是实例化了一个对象。
p1.sayName == p2.sayName // false // 虽然可以通过全局函数的方式来解决,但是缺乏了类的封装含义 function Person(name, age, job) { ... this.sayName = sayName; } function sayName() { console.log(this.name); }
3.3. 原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性时一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有势力共享的属性和方法(目的就是只提供一次定义,然后可以在每个创建的对象引用)。
function Person() { } Person.prototype.name = 'Tim'; Person.prototype.age = 22; Person.prototype.job = "SE"; Person.prototype.sayName = function () { console.log(this.name); }; var p1 = new Person(); p1.sayName(); // Tim var p2 = new Person(); p2.sayName(); // Tim p1.sayName == p2.sayName; // true
3.3.1. 理解原型对象
任何一个函数,都会存在有一个prototype属性,这个属性指向函数的原型对象,原型对象自身会自动获得一个constructor(构造函数)属性(一个指向prototype属性所在函数的指针,其实就是一个相互引用的过程)。原型对象默认只有constructor属性、从Object对象继承来的属性。由构造函数创建的对象实例,实例内部有一个指针(内部属性,用户无法访问)指向构造函数的原型对象(表示为:[[Prototype]]),没有直接访问这个属性的API,但是可以访问__proto__(这个是浏览器厂商实现的)。
+-------------------+ +----------------------+ | 构造函数Person | ---------> | Person Prototype对象 | 原型对象 +-------------------+ +----------------------+ | prototype | <--------- | constructor | 构造函数属性执行了Person函数 +-------------------+ +----------------------+ | name, job, sayName | 实在在原型对象上的属性,还有继承自Ojbect对象的一些没有列出来 +----------------------+ +-----------------+ | 实例对象 p1 | -----------> Person 原型对象 +-----------------+ | [[Prototype]] | 原型对象属性,在一些浏览器厂商通过__proto__属性来访问,但是在JavaScript代码里面是无法访问到的 +-----------------+ +-----------------+ | 实例对象 p2 | -----------> Person 原型对象 +-----------------+ | [[Prototype]] | +-----------------+
Person.isPrototypeOf(p1); // true
Person.isPrototypeOf(p2); // true
Object.getPrototypeOf(p1) === Person.prototype; // true
Object.getPrototypeOf(p1).name; // Tim
p1.sayName属性查找顺序:
- 对象实例本身,找到即返回
- 对象原型对象内,找到即返回
- 都没有找到,报错
p1.sayName 赋值一个新的函数时,这个函数是只属于p1的,并没有修改到原型对象内的sayName属性。
function Person() {} Person.prototype.name = "Tim"; var p1 = new Person(); var p2 = new Person(); console.log(p1.name); // Tim console.log(p2.name); // Tim
p1.hasOwnProperty('name'); // false, name属性不是属于p1对象自身的,而是属于原型的 p1.name = 'Tom'; console.log(p1.name); // Tom,p1.name被一个新值给屏蔽了,在原型中的这个值还在,访问的时候先查找了对象自身的属性了。 console.log(p2.name); // Tim
p1.hasOwnProperty('name'); // true, name属性时属于p1对象自身的
delete p1.name; // 删除了属于p1对象自身的name属性
console.log(p1.name); // Tim, 这个时候访问到的又是原型中的name属性了
delete p1.name; // true, 虽然显示成功,但是无法成功删除属于原型对象中的属性。
3.3.2. 原型与 in 操作符
两种方式使用in操作符:
- 单独使用:'name' in p1,通过p1能够访问'name'属性时,返回true,否则返回false(不论该属性是来自原型还是对象自身,只有能够访问到即返回true)
- for-in循环中使用:for (var i in p1) { console.log(i); },返回的是所有能够同对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性(包括[[Enumerable]]特性标记为false的属性)。要获取对象上所有可枚举的实例属性,可以使用Object.keys()方法。
function Person() {} Person.prototype.name = 'Tim'; Person.prototype.age = 22; Person.prototype.job = 'SE'; Person.prototype.sayName = function () { console.log(this.name); }; var keys = Object.keys(Person.prototype); // keys为字符串数组 keys; // ]'name', 'age', 'job', 'sayName']
var p1 = new Person();
p1.name = 'Tom';
p1.age = 23;
var p1keys = Object.keys(p1); // 返回字符串数组
p1keys; // ['name', 'age']
Object.getOwnPropertyNames(Person.prototype); // ['constructor', 'name', 'age', 'job', 'sayName']
结果中包含了不可枚举的constructor属性
3.3.3. 更简单的原型语法
function Person() {} Person.prototype = { name: 'Tim', age: 22, job: 'SE', sayName: function () { console.log(this.name); } }; var p1 = new Person(); p1 instanceof Object; // true p1 instanceof Person; // true p1.constructor == Person; // false, 与之前的方式比,这点发生了变化 p1.constructor == Object; // true 1. 减少每次属性赋值都需要写上"Person.prototype." 2. 问题:导致 原型对象的 constructor 属性不再指向 Person了,而指向了Object. 为了解决问题#2 function Person() {} Person.prototype = { constructor: Person, // 明确constuctor指向Person name: 'Tim', age: 22, job: 'SE', sayName: function () { console.log(this.name); } }; 这个方法比初始方式另外一个问题是,constructor变为可枚举的了。 可以使用下面的方式来定义constructor属性: Object.defineProperty(Person.prototype, 'constructor', { enumerable: false, value: Person }); 这样简化后的prototype就跟最初方式的prototype完全一致了。
3.3.4. 原型的动态性
1. 原型动态扩展 var p1 = new Person(); p1.sayHi(); // 出错,因为没有定义sayHi Person.prototype.sayHi = function () { console.log('Hi'); }; p1.sayHi(); // 成功,因为原型扩展了sayHi访问,在调用的时候,搜索到原型里面有这个方法。 2. 重新定义原型 function Person() {} var p1 = new Person(); // 当前原型里面没有sayName方法 Person.prototype = { constructor: Person, name: 'Tom', sayName: function() { console.log(this.name); } }; p1.sayName(); // 出错,重写原型,并没有使之前创建的对象原型指针指向新的原型对象。 重写原型对象之前
+-----------------+
| 构造函数Person | ----> Person 原型对象
+-----------------+
| [[Prototype]] |
+-----------------+
+-----------------+ | p1 实例对象 | ----> Person 原型对象 +-----------------+ | [[Prototype]] | +-----------------+
重写原型对象之后
+-----------------+ | 构造函数Person | ----> Person 新的原型对象 +-----------------+ sayName属性 | [[Prototype]] | +-----------------+
+-----------------+ | p1 实例对象 | ----> Person 原型对象 +-----------------+ 这里还是之前的原型对象,没有sayName属性 | [[Prototype]] | +-----------------+
3.3.5. 原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,原生(内建)的引用类型也是采用这种模式创建的,所有具有原型模式的特点,比如我们可以扩展原生类型的方法集合。
String.prototype.startsWith = function (text) { return this.indexOf(text) === 0; }; var msg= "Hello world!"; console.log(msg.startsWith('Hello')); // true
3.3.6. 原型对象的问题
原型模式存在的缺点:
- 省略了构造函数传递初始化参数这一环节,导致所有实例在默认情况下都取得相同的属性值
- 原型中所有属性被多实例共享,这种共享对于函数非常适合,但是对于引用类型的属性值则可能存在问题
function Person() {} Person.prototype = { constructor: Person, name: 'Tim', friends: ['Tom', 'Bob'] }; var p1 = new Person(); var p2 = new Person(); p1.friends.push("John"); // 本来只是想给p1加个朋友的 p1.friends; // ['Tom', 'Bob', 'John'] p2.friends; // ['Tom', 'Bob', 'John'], p2 的朋友圈也扩大了。。。
3.4. 组合使用构造函数和原型模式
常用以创建自定义类型的方式,是结合构造函数和原型模式,将对象私有属性放在构造函数中,而将对象间公共的属性(方法)存放在原型中。重写上面的示例:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ['Tom', 'Bob']; } Person.prototype = { constructor: Person, sayName: function () { console.log(this.name); } }; var p1 = new Person('Tim', 22, 'SE'); var p2 = new Person('Tom', 23, 'PM'); p1.friends.push('John'); p1.friends === p2.friends; // false p1.sayName === p2.sayName; // true
3.5. 动态原型模式
动态原型模式,只是增加了原型中需要的内容,并不重写整个原型。
function Person(name, age, job) { // 属性 this.name = name; this.age = age; this.job = job; // 方法 if (typeof this.sayName != 'function') { Person.prototype.sayName = function () { console.log(this.name); }; } }
3.6. 寄生构造函数模式
寄生(parasitic)构造函数模式基本思想是:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。看起来像工厂函数,但是实例化的时候又用了new操作符。注:没有什么特别的用处,看不明白也没关系。
function SpecialArray() { // 创建数组 var values = new Array(); // 添加值 values.push.apply(values, arguments); // 添加方法 values.toPipedString = function() { return this.join("|"); }; // 返回数组 return values; } var colors = new SpecialArray('red', 'blue', 'green'); console.log(colors.toPipedString()); // red|blue|green
3.7. 稳妥构造函数模式
稳妥对象(durable objects):指的是没有公共属性,而且其方法也不引用this的对象。最适合在一些安全的环境中(这些环境中禁止使用this 和 new),或者在防止数据被其他应用程序(如:Mashup程序)改动时使用。稳妥构造函数与寄生构造函数类似,有两点不同:
- 新创建对象的实例方法不引用this
- 不使用new操作符调用构造函数
function Person(name, age, job) { // 创建要返回的对象 var o = new Object(); // 可以在这里定义私有变量和函数 ... // 添加方法 o.sayName = function () { console.log(name); }; // 返回对象 return o; } var p1 = Person('Tim', 22, 'SE'); p1.sayName(); 变量p1中保持的是一个稳妥对象,除了sayName方法外,没有其他方法访问数据成员。
4. 继承
继承两种方式:
- 接口继承:只继承方法签名
- 实现继承:继承实际的方法
ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
4.1. 原型链
基本思想:利用原型让一个引用类型继承另外一个引用类型的属性和方法。
构造函数、原型和实例的关系:
- 构造函数有一个原型对象
- 原型对象有一个指向构造函数的指针
- 实例对象有一个指向原型对象的内部指针
如果:让一个构造函数的原型对象 === 另外一个类型的实例对象。通过这种方式实现了原型链
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 继承了SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function () { return this.subproperty; }; var instance = new SubType(); console.log(instance.getSuperValue()); // true 调用了父类的方法
通过实现原型链,本质上扩展了原型搜索机制,调用s1.getSuperValue()经历的步骤:
- 搜索s1实例对象本身
- 搜索SubType.prototype
- 搜索SuperType.prototype,再这步才找到了方法
其实上图中,SuperType.prototype也是Object类的一个实例对象,所以这个对象内部有一个[[Prototype]]指向了Object的原型。
原型与示例的关系,可以使用两种方式来判断:instanceof 操作符,isPrototypeOf()方法:
s1 instanceof Object; // true s1 instanceof SuperType; // true s1 instanceof SubType; // true Object.prototype.isPrototypeOf(s1); // true SuperType.prototype.isPrototypeOf(s1); // true SubType.prototype.isPrototypeOf(s1); // true
给一个子类型添加、覆盖方法的行为,必须存放在给子类型原型赋值语句之后,另外也不能使用原型字面量的方式来添加方法(必须是SubType.prototype.xxx的方式):
// 先确定继承关系 SubType.prototype = new SuperType(); // 然后再添加方法 SubType.prototype.getSubValue = function () {...}; // 或者覆盖之前的方法 SubType.prototype.getSuperValue = function () {...};
原型链的问题:
- 包含引用类型的原型,可能导致被其他对象修改了值!
- 创建子类型的实例对象时,无法给父类构造函数传递参数,参数在确定继承关系的时候已经固定好了。
// 引用类型问题 function SuperType() { this.colors = ['red', 'blue', 'green']; } function SubType() {} SubType.prototype = new SuperType(); var s1 = new SubType(); s1.colors.push('black'); console.log(s1.colors); // ['red', 'blue', 'green', 'black'] var s2 = new SubType(); console.log(s2.colors); // ['red', 'blue', 'green', 'black']
基于以上几点,下面的方法是原型链的更实用方法:call(), apply()方法。
4.2. 借用构造函数
借用构造函数技术:或称伪造对象、或者经典继承。即:在子类型构造函数的内部调用超类型构造函数,函数因为是在特定环境中执行代码的对象,通过call、apply方法可以在(将来)新创建的对象上执行构造函数,例:
function SuperType() { this.colors = ['red', 'blue', 'green']; } function SubType() { SuperType.call(this); }
// SubType.prototype = new SuperType(); 这句在借用构造函数中没有,下面的组合继承模式才有。
var s1 = new SubType(); s1.colors.push('black'); console.log(s1.colors); // ['red', 'blue', 'green', 'black'] var s2 = new SubType(); console.log(s2.colors); // ['red', 'blue', 'green']
SubType构造函数内“借调“了超类型的构造函数,通过是用call()、apply()实际上是在创建SubType示例的时候调用了SuperType构造函数,这样在示例话的所有对象中都具有自己的colors对象。
4.2.1. 传递参数
function SuperType(name) { this.name = name; } function SubType() { // 继承了SuperType,同时还传递了参数 SuperType.call(this, 'Tim'); // 实例属性 this.age = 22; }
// SubType.prototype = new SuperType(); 这句在借用构造函数中没有,下面的组合继承模式才有
var s1 = new SubType();
s1.name; // Tim
s1.age; // 22
4.2.2. 问题
无法避免构造函数模式存在的问题:方法都在构造函数中定义,(因为没有使用原型),函数服用无从谈起,另外父类的原型中定义的方法,在子类中不可见。
4.3. 组合继承
组合继承,又称伪经典继承:将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式(在原型上定义方法实现函数复用继承、在借用构造函数内定义属性实现属于独立存在实例中)。思路:
- 使用原型链实现对原型属性、方法的继承
- 使用借用构造函数实现对示例属性的继承。
function SuperType(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } SuperType.prototype.sayName = function () { console.log(this.name); }; function SubType(name, age) { // 继承属性 SuperType.call(this, name); this.age = age; } // 继承方法 SubType.prototype = new SuperType(); SubType.constructor = SubType; SubType.prototype.sayAge = function () { console.log(this.age); }; var s1 = new SubType('Tim', 22); s1.colors.push('black'); console.log(s1.colors); // ['red', 'blue', 'green', 'black'] s1.sayName(); // 继承父类的方法, Tim s1.sayAge(); // 子类的方法, 22 var s2 = new SubType('Tom', 23); console.log(s2.colors); // ['red', 'blue', 'green'] 独立属性 s2.sayName(); // 继承父类的方法, Tom s2.sayAge(); // 子类的方法, 23
组合继承避免了原型链、借用构造函数的缺陷,融合它们的优点,是比较常用的模式,而且 instanceof、isPrototypeOf() 也能够用于识别组合继承创建的对象。
4.4. 原型式继承
原型式继承:并没有使用严格意义上的构造函数,而是借助原型可以基于已有的对象创建新对象,同时不必创建自定义类型。
function newobj(o) { function F() {} F.prototype = o; return new F(); }
newobj()函数内部,创建了一个临时构造函数F(),然后将传入的对象(类似SuperType创建的实例对象)作为构造函数的原型(父类),然后返回了临时构造函数的实例对象(F构造函数类似SubType,并且返回了一个SubType的实例化对象)。
var person = { name: 'Tim', friends: ['Tom', 'Bob'] }; var p1 = newobj(person); p1.name = 'Tom'; p1.friends.push('John'); var p2 = newobj(person); p2.name = 'Linda'; p2.friends.push('Lucy'); console.log(person.friends); // ['Tom', 'Bob', 'John', 'Lucy']
条件:
- 传入的对象作为基础对象
- 新创建的对象,类似在基础对象上进行些修改
- 引用属性将被基础对象、新创建的对象间共享
- ECMAScript 5提供了默认newobj函数的功能函数:Object.create()方法
- Object.create()接收2个参数:新对象原型的对象(基础对象),新对象定义额外属性的的对象(可选)
var person = { name: 'Tim', friends: ['Tom', 'Bob'] }; var p1 = Object.create(person); p1.name = 'Tom'; p1.friends.push('John'); var p2 = Object.create(person); p2.name = 'Linda'; p2.friends.push('Lucy'); person.friends; // ['Tom', 'Bob', 'John', 'Lucy']
var p3 = Object.create(person, { name: { value: 'Greg' } });
p3.name; // Greg
场景:在只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。
4.5. 寄生式继承
寄生式(parasitic)继承与寄生构造函数和工厂模式类似:场景一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。例:
function newobj(o) { function F() {} F.prototype = o; return new F(); } function anotherNewObj(o) { var clone = newobj(o); clone.sayHi = function () { console.log('hi'); }; return clone; }
anotherNewObj函数内使用了原型式继承模式,另外给新创建的对象复制了特有的方法,示例:
var person = { name: 'Tim', friends: ['Tom', 'Bob'] }; var p1 = anotherNewObj(person); p1.sayHi(); // Hi
4.6. 寄生组合式继承
组合继承有一个问题,无论如何实现,都存在调用两次父类构造函数的情况:一次是在创建子类原型的时候,一次是在子类构造函数内部。
function SuperType(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } SuperType.prototype.sayName = function () {console.log(this.name); }; function SubType(name, age) { SuperType.call(this, name); // 第二次调用SuperType() this.age = age; } SubType.prototype = new SuperType(); // 第一次调用SuperType() SubType.prototype.constructor = SubType(); SubType.prototype.sayAge = function () {console.log(this.age'); }
过程:
- 第一次调用SuperType(), SubType.prototype会得到两个属性:name, colors
- 本来name, colors是属于SuperType的实例属性,现在属于SubType的原型属性
- 调用SubType构造函数时,又调用一次SuperType构造函数,这次得到两个实例对象直接属性:name, colors,这两个属性屏蔽原型属性。
解决办法是采用寄生组合继承模式:通过借用构造函数来继承属性、通过原型链的混成形式来继承方法。思路:子类的原型不直接使用父类构造函数实例化的对象,而是使用父类原型对象的一个拷贝。
function newobj(o) { function F() {}; F.prototype = o; return new F(); } function inheritPrototype(subclass, superclass) { var prototype = newobj(superclass.prototype); // 创建对象 prototype.constructor = subclass; // 增强对象 subclass.prototype = prototype; // 指定对象 } 1. 由父类的原型对象,创建对象prototype; 2. 新得到的原型对象指定 constructor 属性 3. 子类的原型对象,指向新得到的原型对象,进行关联。
4. 达到了原型链实现继承的功能,又没有调用父类的构造函数。
新的继承实例:借用构造函数还是需要的(父类属性继承给子类),寄生方式实现原型链(继承原型时不调用父类构造函数了)
function SuperType(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } SuperType.prototype.sayName = function () {console.log(this.name); } function SubType(name, age) { SuperType.call(this, name); // 这里还是需要调用构造函数,以继承父类的属性 this.age = age; } inheritPrototype(SubType, SuperType); // 这里不再调用父类的构造函数了 SubType.prototype.sayAge = function () {console.log(this.age);};