目录
- 6.1 理解对象
- 6.1.1 属性类型
- 6.1.2 定义多个属性
- 6.1.3 读取属性的特性
- 6.2 创建对象
- 6.2.1 工厂模式
- 6.2.2 构造函数模式
- 6.2.3 原型模式
- 6.2.4 组合使用构造函数和原型模式(广泛使用)
- 6.2.5 动态原型模式
- 6.2.6 寄生构造函数模式
- 6.2.7 稳妥构造函数模式
- 6.3 继承
- 6.3.1 原型链
- 6.3.2 借用构造函数
- 6.3.3 组合继承(combination inheritance)——最常用继承
- 6.3.4 原型式继承
- 6.3.5 寄生式继承
- 6.3.6 寄生组合式继承(最理想的继承范式)
- 6.4 小结
对象定义:无序属性的集合,属性可包括基本值、对象或者函数。对象的每个属性或方法都有一个名字(名值对)
6.1 理解对象
1、创建Object实例,再为其添加属性和方法
2、对象字面量
6.1.1 属性类型
数据属性和访问器属性
1、数据属性
描述数据行为的4个特性:
[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,默认true
[[Enumrable]]:能否通过for-in循环返回属性,默认true
[[Writable]]:能否修改属性的值,默认true
[[Value]]:属性的数据值,默认undefined
Object.defineProperty(obj, propName, descriptor)
descriptor描述符对象包含Configurable、Enumrable、Writable、Value属性
【注】用该函数创建新属性时,前三个默认false;若调用该函数修改已定义的属性的特性,则无此限制
例:定义只读数据属性
var person = {};
Object.definedProperty(person, 'name', {
writable: false, //只读
value: 'Nicholas',
})
alert(person) //"Nicholas"
person.name = 'Greg'
alert(person.name) //"Nicholas"
例:定义不可配置(修改/删除)属性
var person = {}
Object.defineProperty(person, 'name', {
configurable: false,
value: 'Nicholas'
})
alert(person.name) //Nicholas
delete person.name
alert(person.name) //Nicholas
【注】定义不可配置的属性,不能再把它变回可配置
Object.defineProperty(person, 'name', {
configurable: true,
value: 'Nicholas'
})
//Uncaught TypeError: Cannot redefine property: name
//at Function.defineProperty (<anonymous>)
2、访问器属性
访问器属性包含一对getter、setter函数(非必需)
读取访问器属性时会调用getter函数,返回有效值;写入访问器属性时会调用setter函数并传入新值
访问器属性的4个特性:
[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,默认true
[[Enumrable]]:能否通过for-in循环返回属性,默认true
[[Get]]:读取属性时调用的函数,默认undefined
[[Set]]:写入属性时调用的函数,默认undefined
例:
var book = {
_year: 2018,
edition: 1
};
Object.defineProperty(book, 'year', {
get: function() {
return this._year
},
set: function(value) {
if (value > 2018) {
this._year = value
this.edition += value - 2018
}
}
});
book.year = 2019;
alert(book.edition); //2
【注】1、book对象字面量定义中未year,后面也为this.year,则会报错(RangeError:超过堆栈最大值)
因为set属性中,this.year = value 相当于调用了一次set函数,即无限递归;
2、_year属性只能通过对象方法访问,访问器属性year包含getter、setter函数(改变了其它属性)
3、只指定了getter函数的属性无法写入,只指定setter函数的属性无法读取
非标准方法创建访问器
defineGetter(), defineSetter()
例:
var book = {
_year: 2018,
edition: 1
};
book.__defineGetter__('year', function() {
return this._year
})
book.__defineSetter__('year', function(value) {
if (value > 2018) {
this._year = value
this.edition += value - 2018
}
});
book.year = 2019;
alert(book.edition); //2
6.1.2 定义多个属性
Object.defineProperties(obj, {props: descriptor})
var books = {}
Object.defineProperties(books, {
_year: {
writable: true,
value: 2018
},
edition: {
writable: true,
value: 1
},
year: {
get: function() {
return this._year
},
set: function(value) {
if (value > 2018) {
this._year = value
this.edition += value - 2018
}
}
})
6.1.3 读取属性的特性
Object.getOwnPropertyDescriptor(obj, prop) //return descriptor
参数是访问器属性时,descriptor包含属性:configurable、enumerable、get和set
参数是数据属性时,descriptor包含属性:configurable、enumerable、writable和value
var book = {}
Object.defineProperties(book, {
_year: {
writable: true,
value: 2018
},
edition: {
writable: true,
value: 1
},
year: {
get: function() {
return this._year
},
set: function(value) {
if (value > 2018) {
this._year = value
this.edition += value - 2018
}
}
})
var descriptor = Object.getOwnPropertyDescriptor(book, '_year')
alert(descriptor.value) //2018
alert(descriptor.configurable) //false
alert(typeof descriptor.get) //undefined
var descriptor = Object.getOwnPropertyDescriptor(book, 'year')
alert(descriptor.value) //undefined
alert(descriptor.enumerable) //false
alert(typeof descriptor.get) //function
6.2 创建对象
6.2.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 person1 = createPerson("Greg", 22, "Doctor")
person1 instanceof createPerson //false
工厂模式解决了创建多个相似对象的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型)
6.2.2 构造函数模式
类似Object、Array原生构造函数,在运行时自动出现在执行环境中。
自定义构造函数可以将它的实例标示为一种特定类型(胜过工厂模式)
function Person(name, age, job) {
this.name = name; //将属性赋给this对象
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name)
}
}
var person1 = new Person("Greg", 22, "Doctor")
person1 instanceof Object //true
person1 instanceof Person //true
【注】a. 构造函数应以大写字母开头,b. 创建Person实例需要使用new操作符,c. 实例对象包含constructor属性(值是该构造函数)
构造实际步骤:
(1)创建一个新对象
(2)将构造函数的作用域赋给新对象(this指向新对象)
(3)执行构造函数中代码
(4)返回新对象
将构造函数当做函数
a. 当做构造函数,使用new调用
b. 作为普通函数,直接调用。浏览器中,this为window(即给window添加属性和方法)
c. 在另一个对象的作用域中调用
var o = new Object()
Person.call(o, 'Jack', 24, 'Nurse')
o.sayName() //"Jack"
构造函数的问题
每个方法都要在每个实例上重新创建一遍,即构造出的不同对象person1、person2,其同名方法(sayName)并不是同一个Function实例
this.sayName = new Function("console.log(this.name)") //每个person实例都包含一个不同Function实例
实际并不需要在执行代码前就把函数绑定到特定对象上面。解决:将函数定义转移到构造函数外部
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name)
}
var person1 = new Person("Nicholas", 29, "Doctor")
var person2 = new Person("Nicholas", 27, "Nurse")
alert(person1.sayName == person2.sayName) //true
【注】person1和person2对象共享了在全局作用域定义的同一个 sayName() 函数;但是这样一来,自定义引用类型就失去了封装性。
6.2.3 原型模式
prototype:通过构造函数创建的那个对象实例的原型对象
每个函数都有一个prototype指针属性,指向一个包含由特定类型的所有实例共享的属性和方法
使用原型对象的好处:让所有对象实例共享它所包含的属性和方法(不必在构造函数中定义对象实例的信息)
function Person(name, age, job) {
Person.prototype.name = 'Nicholas'; //将属性赋给this对象
Person.prototype.age = 24;
Person.prototype.job = 'Doctor';
Person.prototype.sayName = function() {
console.log(this.name)
}
}
var person1 = new Person()
person1.sayName() //Nicholas
var person2 = new Person()
person2.sayName() //Nicholas
alert(person1.sayName == person2.sayName) //true
【注】此时构造函数变成了空函数,person1和person2访问的都是同一组属性和同一个sayName函数
1. 理解原型对象
每个函数都有一个prototype属性,它指向函数的原型对象。
默认情况下,所有原型对象都会自动获得一个constructor属性,其指向prototype属性所在函数的指针。前述Person.prototype.constructor指向Person函数。
每个构造函数创建的新实例都包含一个[[Prototype]]指针属性(浏览器中为__proto__),指向构造函数的原型对象。指针连接存在于实例与构造函数的原型对象之间(实例对象与构造函数没有直接关系)
person1
Person {}
__proto__:
age:24
job:"Doctor"
name:"Nicholas"
sayName:ƒ ()
constructor:ƒ Person(name, age, job)
__proto__:Object
Person
ƒ Person(name, age, job) { Person.prototype.name = 'Nicholas'; //将属性赋给this对象 Person.prototype.age = 24;
Person.prototype.job = 'Doctor'; Person.prototype.sayName = function() { consol…
Person.prototype
{name: "Nicholas", age: 24, job: "Doctor", sayName: ƒ, constructor: ƒ}
age:24
job:"Doctor"
name:"Nicholas"
sayName:ƒ ()
constructor:ƒ Person(name, age, job)
__proto__:Object
确定实例对象与原型对象之间的关系:(通过实例对象获取[[Prototype]]指针指向的原型对象)
1、[[Prototype]]在浏览器中支持的__proto__属性
2、原型对象的 isPrototypeOf(obj) 方法(若参数的[[Prototype]]指向调用该方法的对象,则返回true)
3、Object.getPrototypeOf(obj)
Person.prototype == person1.__proto__ //true
Person.prototype.isPrototypeOf(person1) //true
Object.getPrototypeOf(person1) == Person.prototype //true
对象实例共享原型属性和方法的基本原理:
调用person1.sayName()时,会先后执行两次搜索。首先解析器搜索实例person1是否有sayName属性,结果是没有;然后搜索person1的原型是否有sayName属性,结果有;于是读取保存在原型对象中的函数。
重定义原型属性:
可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。
在实例中添加的属性与该原型中的属性同名时,该实例属性会被创建,并屏蔽原型中的同名属性。
var person1 = new Person()
var person2 = new Person()
person1.name = "Greg"
alert(person1.name) //"Greg" ——来自实例
alert(person2.name) //"Nicholas" ——来自原型
使用delete删除实例属性:
var person1 = new Person()
person1.name = "Greg"
alert(person1.name) //Greg
delete person1.name
alert(person1.name) //Nicholas
hasOwnProperty(prop)方法检测该属性是否在实例中:
便于知道访问的是实例属性还是原型属性
var person1 = new Person()
alert(person1.hasOwnProperty('name')) //false
person1.name = "Greg"
alert(person1.hasOwnProperty('name')) //true
delete person1.name
alert(person1.hasOwnProperty('name')) //false
2、原型与in操作符
in操作符的两种使用场景:单独使用、for-in循环
(1)单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中
var person1 = new Person()
alert('name' in person1) //true
person1.name = "Greg"
alert('name' in person1) //true
(2)使用for-in循环时,返回是是所有能通过对象访问的、可枚举的属性(既包括实例中的属性,也包括原型中的属性)
var person1 = new Person() //实例无属性
for (var prop in person1) {
console.log(prop)
}
//name
//age
//job
//sayName
Object.keys(obj) 取得对象所有可枚举属性 //return [props]
参数为原型对象时,返回原型对象属性;参数为实例对象时,返回实例对象属性。
Object.keys(Person.prototype) //["name", "age", "job", "sayName"]
var person1 = new Person()
person1.name = "Rob"
person1.age = 24
Object.keys(person1) //["name", "age"]
Object.getOwnPropertyNames(obj) 获取所有实例属性(无论是否可枚举)
var person1 = new Person()
Object.getOwnPropertyNames(person1) //[]
Object.getOwnPropertyNames(Person.prototype) //["constructor", "name", "age", "job", "sayName"]
【注】constructor为不可枚举属性。Object.keys(person1), Object.getOwnPropertyNames(person1) 都可以替代 for-in 循环
3、更简单的原型语法
用一个包含所有属性和方法的对象字面量来重写整个原型对象
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 25,
sayName: function() {
console.log(this.name)
}
}
Person.prototype.constructor //ƒ Object() { [native code] }
new Person().constructor == Person //false
new Person() instanceof Person //true
【注】:constructor属性不在指向Person。当创建一个函数时,同时会创建其prototype对象,该对象自动获得constructor属性。而这里本质上是重写了默认的prototype对象,因此constructor属性指向Object构造函数。
设置恢复constructor:
function Person() {}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 25,
sayName: function() {
console.log(this.name)
}
}
Person.prototype.constructor //ƒ Person() {}
new Person().constructor == Person //true
Person.prototype //{constructor: ƒ, name: "Nicholas", age: 25, sayName: ƒ}
【注】上述添加constructor属性,使得constructor的 [[Enumerable]] 特性被设置为true。默认情况下,原生的constructor属性是不可枚举的
Object.defineProperty(obj, prop, {descriptor}) 重设构造函数
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 25,
sayName: function() {
console.log(this.name)
}
}
Object.defineProperty(Person.prototype, 'consructor', {
enumerable: false,
value: Person
})
Person.prototype //{name: "Nicholas", age: 25, sayName: ƒ, constructor: ƒ}
4、原型的动态性
由于在原型中查找值是一次搜索,因此对原型所做的修改都可反映出来。允许先创建实例,再为原型对象添加方法,然后实例调用该方法
var person1 = new Person()
Person.prototype.sayHi = function() {
alert('Hi')
}
person1.sayHi() //Hi
但是当重写整个原型对象时,等于切断了构造函数与最初原型之间的联系 [[Prototype]] (__proto__)
实例对象的__proto__属性引用的仍是重写前的原型
function Person() {}
var person1 = new Person()
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 25,
sayName: function() {
console.log(this.name)
}
}
person1.sayName() //Uncaught TypeError: person1.sayName is not a function
person1.__proto__ //{constructor: ƒ}
5、原生对象的原型
所有原生引用类型都在其构造函数上定义了方法:
typeof Array.prototype.sort //function
typeof String.prototype.substring //function
通过原生对象的原型,还可以定义新方法:
例:为String包装类型添加 startsWith() 方法
String.prototype.startsWith = function(text) {
return this.indexOf(text) == 0
}
'Hello world'.startsWith('Hello') //true
【注】不推荐产品化的程序中修改原生对象的原型
6、原型对象的问题
原型模式的最大问题是由其共享的本性导致的:
对于包含引用类型的属性来说,实例1对原型引用类型属性的修改会同时表现在其他实例中(而无法有属于自己的属性)——所以一般很少单独使用
function Person() {}
var person1 = new Person()
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 25,
friends: ['Jack', 'Rob'],
sayName: function() {
console.log(this.name)
}
}
var person1 = new Person()
var person2 = new Person()
person1.friends.push('Van')
alert(person1.friends) //Jack,Rob,Van
alert(person2.friends) //Jack,Rob,Van
6.2.4 组合使用构造函数和原型模式(广泛使用)
构造函数用于定义实例属性(支持传参),原型模式用于定义方法和共享属性
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['Jack', 'Rob']
}
Person.prototype = {
constructor: Person,
sayName: function () {
alert(this.name)
}
}
var person1 = new Person('Nicholas', 25, 'Software Engineer')
var person2 = new Person('Greg', 22, 'Doctor')
person1.friends.push('Van')
alert(person1.friends) //Jack,Rob,Van
alert(person2.friends) //Jack,Rob
alert(person1.sayName === person2.sayName) //true
6.2.5 动态原型模式
把所有信息封装在构造函数中,通过在构造函数中初始化原型,检查某个应该存在的方法是否有效,来决定是否需要初始化原型方法
function Person(name, age, job) {
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != 'Function') {
Person.prototype.sayName = function() {
alert(this.name)
}
}
var person1 = new Person('Greg', 28, 'Doctor')
person1.sayName() //Greg
【注】只在sayName()方法不存在情况下才会将它加到原型中。首次调用构造函数会执行if中的原型方法初始化语句,此后不需要在修改了。
使用动态原型模式时,不能使用对象字面量重写原型。(避免在已经创建了实例情况下重写原型,会切断现有实例与新原型之间的联系)
6.2.6 寄生构造函数模式
创建的函数仅仅是包装对象,然后返回新创建的对象,使用new操作符实例化(仅这一点区别于工厂模式)
function Person(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 person1 = new Person('Greg', 27, 'Doctor')
person1.sayName() //Greg
【注】返回对象与构造函数(原型)之间没有关系,函数返回的对象与在构造函数外部创建的对象没什么不同,所以不能依赖instanceof操作符确定对象类型
6.2.7 稳妥构造函数模式
稳妥对象(durable objects):没有公共属性,新创建对象的实例方法不引用this,不使用new操作符调用构造函数
function Person(name, age, job) {
var o = new Object(); //显式创建对象
//可定义私有变量和函数
//公共方法
o.sayName = function() {
console.log(name)
};
return o; //返回对象
}
var person1 = Person('Greg', 27, 'Doctor')
person1.sayName() //Greg
【注】除了调用sayName()方法外,没有别的方法可以访问其数据成员,提供了安全性
6.3 继承
面向对象语言支持:接口继承(继承方法签名)、实现继承(继承实际方法)
ECMAScript只支持实现继承,依靠原型链来实现
6.3.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()
alert(instance.getSuperValue()) //true
SuperType.prototype
{getSuperValue: ƒ, constructor: ƒ}
SubType.prototype
SuperType {property: true, getSubValue: ƒ}
getSubValue:ƒ ()
property:true
__proto__:
getSuperValue:ƒ ()
constructor:ƒ SuperType()
__proto__:Object
SubType.prototype.__proto__
{getSuperValue: ƒ, constructor: ƒ}
SubType.prototype.constructor
ƒ SuperType() {
this.property = true
}
instance.__proto__
SuperType {property: true, getSubValue: ƒ}
instance.constructor
ƒ SuperType() {
this.property = true
}
【注】a. 子类原型不仅具有父类实例所拥有的全部属性和方法,还有一个指针,指向父类的原型。
b.子类实例的constructor属性等于子类原型的constructor属性。而子类原型又指向父类原型,而父类原型有constructor属性,于是子类实例的constructor属性等于父类原型的constructor属性。
1、别忘记默认的原型
所有引用类型默认都继承了Object,所有函数的默认原型都是Object的实例,因此默认原型都会有一个内部指针,指向Object.prototype。(所有自定义类型都会继承toString()、valueOf()等默认方法)完整的原型链如图:
2、确定原型和实例的关系
(1)instanceof 操作符测试实例与原型链中出现过的构造函数
alert(instance instanceof Object) //true
alert(instance instanceof SuperType) //true
alert(instance instanceof SubType) //true
(2)isPrototypeof() 方法,只要是原型链中出现过的原型,都可以说是该原型链所派生出的实例的原型
alert(Object.prototype.isPrototypeOf(instance)) //true
alert(SuperType.prototype.isPrototypeOf(instance)) //true
alert(SubType.prototype.isPrototypeOf(instance)) //true
3、先替换原型再添方法
子类覆盖(重定义)父类方法或添加父类没有的方法,需要在替换原型之后进行。
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
}
//重写父类的方法
SubType.prototype.getSuperValue = function() {
return false
}
var instance = new SubType()
alert(instance.getSuperValue()) //false
【注】在通过原型链实现继承时,不能使用对象字面量(Object实例)创建原型方法(会重写原型链)
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
SubType.prototype = new SuperType()
//使用对象字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
getSubType: function() {
return this.subproperty
}
}
var instance = new SubType()
alert(instance.getSuperValue()) //Uncaught TypeError: instance.getSuperValue is not a function
4、原型链的问题
(1)最主要问题来自包含引用类型的原型:在构造函数中定义引用类型属性,并通过原型实现继承,父类实例的引用类型属性变成了子类原型的引用类型属性。相当于为子类原型创建引用类型属性一样:SubType.prototype.colors = [xxx]
,所有子类实例都会共享这一引用属性。
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
function SubType() {}
//继承了SuperType
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('black')
alert(instance1.colors) //red,blue,green,black
var instance2 = new SubType()
alert(instance2.colors) //red,blue,green,black
(2)在创建子类实例时,不能向父类构造函数中传递参数(实践中很少单独使用原型链)
6.3.2 借用构造函数
借用构造函数(constructor stealing)(伪造对象或经典继承):在子类构造函数的内部使用父类构造函数。
函数是:在特定环境中执行代码的对象
方法:使用 apply() 和 call() 在(将来)新创建的对象上执行构造函数(在子类对象上执行父类构造函数中的初始化代码)
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
function SubType() {
//继承了SuperType
SuperType.call(this)
}
var instance1 = new SubType()
instance1.colors.push('black')
alert(instance1.colors) //red,blue,green,black
var instance2 = new SubType()
alert(instance2.colors) //red,blue,green
1、向父类构造函数传递参数
function SuperType(name) {
this.name = name
}
function SubType() {
//继承了SuperType,同时传递参数
SuperType.call(this, 'Nicholas')
//实例属性
this.age = 29
}
var instance = new SubType()
console.log(instance.name, instance.age) //Nicholas 29
【注】为避免父类构造函数重写子类属性,可在调用父类构造函数后,添加子类自己的属性
2、借用构造函数的问题
方法都在构造函数中定义,函数复用无从谈起;父类原型中的方法对子类不可见。(?)
6.3.3 组合继承(combination inheritance)——最常用继承
又称伪经典继承:将原型链和借用构造函数组合到一块
思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承
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.prototype.constructor = SubType //重写子类构造函数以便实例化传参
SubType.prototype.sayAge = function() {
console.log(this.age)
}
var instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
console.log(instance1.colors) //["red", "blue", "green", "black"]
instance1.sayName() //Nicholas
instance1.sayAge() //29
var instance2 = new SubType('Greg', 27)
console.log(instance2.colors) //["red", "blue", "green"]
instance2.sayName() //Greg
instance2.sayAge() //27
【注】组合继承最大问题:必须调用两次父类构造函数,一次是创建子类原型时,另一次是在子类构造函数内部。
即子类最终包含父类对象的全部实例属性,我们不得不在调用子类构造函数时重写这些属性。
instance1
SubType {name: "Nicholas", colors: Array(4), age: 29}
age:29
colors:(4) ["red", "blue", "green", "black"]
name:"Nicholas"
__proto__:SuperType
colors:(3) ["red", "blue", "green"]
constructor:ƒ SubType(name, age)
name:undefined
sayAge:ƒ ()
__proto__:Object
6.3.4 原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型
function object(o) {
function F() {} //临时构造函数
F.prototype = o //将传入对象作为此构造函数的原型
return new F() //返回临时构造函数新实例
}
本质:object() 对传入其中的对象执行一次浅复制
原型式继承要求必须有一个对象可以作为新对象的基础(原型),原型属性会被其他新对象所共享(相当于创建了初始对象的副本)
function object(o) {
function F() {} //临时构造函数
F.prototype = o //将传入对象作为此构造函数的原型
return new F() //返回临时构造函数新实例
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
var person1 = object(person)
person1.name = 'Greg'
person1.friends.push('Rob')
var person2 = object(person)
person2.name = 'Linda'
person2.friends.push('Jack')
alert(person.friends) //Shelby,Court,Van,Rob,Jack
ECMAScript 5 新增了Object.create(protoObj, [propObj]) 方法规范了原型式继承
方法接收两参数:用作新对象原型的对象、为新对象定义额外属性的对象(可选)
传入一个参数时,Object.create(protoObj) 等于 Object(protoObj)
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
var person1 = Object.create(person)
person1.name = 'Greg'
person1.friends.push('Rob')
var person2 = Object(person)
person2.name = 'Linda'
person2.friends.push('Jack')
alert(person.friends) //Shelby,Court,Van,Rob,Jack
Object.create() 方法的第二个参数与Object.defineProperties() 方法的第二个参数格式相同:每个属性都通过自己的描述符定义
(会覆盖原型对象上的同名属性)
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
var person1 = Object.create(person, {
name: {
value: 'Greg'
}
})
alert(person1.name) //Greg
6.3.5 寄生式继承
寄生式继承与原型式继承紧密相关,与寄生构造函数和工厂模式相似:即创建一个仅用于封装继承过程的函数,函数内部增强对象,最后返回对象
function object(o) {
function F() {} //临时构造函数
F.prototype = o //将传入对象作为此构造函数的原型
return new F() //返回临时构造函数新实例
}
function createAnother(original) {
var clone = object(original) //调用构造函数创建新对象
clone.sayHi = function() { //增强对象
alert('hi')
}
return clone
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
var person1 = createAnother(person)
person1.sayHi() //hi
【注】使用寄生式继承为对象添加函数,不能做到函数复用,与构造函数模式类似
6.3.6 寄生组合式继承(最理想的继承范式)
组合继承最大问题:必须调用两次父类构造函数,一次是创建子类原型时(子类原型得到两属性),另一次是在子类构造函数内部(在新对象上创建两实例属性)。新对象创建的实例属性屏蔽了原型中的两同名属性。
instance1
SubType {name: "Nicholas", colors: Array(4), age: 29}
age:29
colors:(4) ["red", "blue", "green", "black"]
name:"Nicholas"
__proto__:SuperType
colors:(3) ["red", "blue", "green"]
constructor:ƒ SubType(name, age)
name:undefined
sayAge:ƒ ()
__proto__:Object
寄生式组合继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
思路:不必为了指定子类的原型而调用父类的构造函数(避免了父类构造实例属性)。本质上,就是使用寄生式继承父类原型,再将结果指定给子类原型
function object(o) {
function F() {} //临时构造函数
F.prototype = o //将传入的原型对象作为此构造函数的原型
return new F() //返回临时构造函数新实例
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); //创建子类原型对象,其原型为父类原型
prototype.constructor = subType; //增强原型对象
subType.prototype = prototype; //指定原型对象
}
改写前面组合继承的例子:
function SuperType(name) {
this.name = name
this.colors = ['red','blue','green']
}
SuperType.prototype.sayName = function() {
alert(this.name)
}
function SubType(name, age) {
SuperType.call(this, name) //调用一次父类构造函数
this.age = age
}
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function() {
alert(this.age)
}
var instance1 = new SubType('Jack', 33)
instance1
SubType {name: "Jack", colors: Array(3), age: 33}
age:33
colors:(3) ["red", "blue", "green"]
name:"Jack"
__proto__:SuperType
constructor:ƒ SubType(name, age)
sayAge:ƒ ()
__proto__:
sayName:ƒ ()
constructor:ƒ SuperType(name)
__proto__:Object
6.4 小结
创建对象的模式
- 工厂模式:使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。被构造函数模式所取代。
- 构造函数模式:可以使用new操作符,但其成员无法复用(包括函数)
- 原型模式:使用构造函数的prototype属性来指定哪些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,使用原型定义共享的属性和方法。
继承
- 原型链继承:通过将一个父类实例赋给子类原型实现,子类就能访问父类的所有属性和方法。
原型链的问题:对象实例共享所有继承的属性和方法(不宜单独使用)
解决原型链问题:借用构造函数。即在子类构造函数内部调用父类构造函数(call),每个实例都具有自己的属性,只使用构造函数模式来定义类型。组合继承用的最多,其使用原型链继承共享的属性和方法,通过借用构造函数继承实例属性。 - 原型式继承:在不必预先定义构造函数情况下实现继承,本质是执行对给定对象的浅复制Object.create()。而复制得到的副本可再改造。
- 寄生式继承:与原型式继承相似,结合了原型式继承和工厂模式,但对象函数无法复用。
- 寄生组合式继承:集寄生式继承和组合继承的优点,最有效。