js高程读书笔记 第六章 面向对象的程序设计

[TOC]

本章内容

  • 理解对象属性
  • 理解并创建对象
  • 理解继承

面向对象(Obejct-Oriented, OO)

ECMA-262把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。

理解对象

创建自定义对象的最简单方式就是创建一个Object的实例,再为它添加属性和方法。

1
2
3
4
5
6
7
8
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";

person.sayName = function(){
alert(this.name);
};

另一种方法,对象字面量成为创建对象的首选模式。

1
2
3
4
5
6
7
8
9
var person = {
name: "Nicholas";
age: 29;
job: "Software Engineer";

sayName: function(){
alert(this.name);
}
}

属性类型

ECMA-262第五版在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特性。特性用两对方括号包含。

ECMAScript中有两种属性:数据属性和访问器属性

  1. 数据属性
  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性
  • [[Enumerable]]:表示能否通过for-in循环返回属性
  • [[Writable]]:表示能否修改属性的值
  • [[Value]]:包含这个属性的数据值

要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。

1
2
3
4
5
6
7
8
9
var person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});

console.log(person.name);//Nicholas
person.name = "Greg";
console.log(person.name);//Nicholas

1
2
3
4
5
6
7
8
9
var person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "Nicholas"
});

console.log(person.name);//Nicholas
delete person.name;
console.log(person.name);//Nicholas

在把configurable定义为false后,再调用Object.defineProperty()就有限制了。

  1. 访问器属性
  • [[Configurable]]:
    表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性
  • [[Enumerable]]: 表示能否通过for-in循环返回属性
  • [[Get]]: 在读取属性的时候调用的函数
  • [[Set]]: 在写入属性的时候调用的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

_year前面的下下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。
这是使用访问器属性的常见方法,即设置一个属性的值会导致其他属性发生变化。

只指定getter意味着属性是不能写。

定义多个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var book = {};

Object.defineProperties(book,{
_year: {
value: 2004,
writable: true,//原书上未添加
},

edition: {
value: 1,
writable: true,//原书上未添加
},

year: {
get: function(){
return this._year;
},
set: function(newValue){
if(newValue > 2004){
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
console.log(book);
book.year = 2005;
console.log(book);

高程中与MDN描述属性的默认值说法不一,实际测试后,以MDN为准

configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。

enumerable
当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。

读取属性的特性

Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var book = {};

Object.defineProperties(book,{
_year: {
value: 2004,
writable: true,//原书上未添加
},

edition: {
value: 1,
writable: true,//原书上未添加
},

year: {
get: function(){
return this._year;
},
set: function(newValue){
if(newValue > 2004){
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor.value);//2004
console.log(descriptor.configurable);//false
console.log(typeof descriptor.get);//undefined

var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value);//undefined
console.log(descriptor.configurable);//false
console.log(typeof descriptor.get);//function

对于数据属性_year value等于最初的值,configurable是false,而get等于undefined。

对于访问器属性year,value等于undefined,enumerable是false,get是一个指向getter的指针。

创建对象

object构造函数或者对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。

工厂模式

用函数分装以特定接口创建对象的细节。

1
2
3
4
5
6
7
8
9
10
11
12
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
}
retrun o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(怎样知道一个对象的类型)。

构造函数模式

1
2
3
4
5
6
7
8
9
10
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

Person()与createPerson函数存在以下不同之处:

  • 没有显示地创建对象
  • 直接将属性和方法赋给了this对象
  • 没有return语句

要创建Person的新实例,必须使用new操作符。

  • 创建一个新对象;
  • 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  • 执行构造函数中的代码(为这个新对象添加属性);
  • 返回新对象。

person1和person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数),该属性指向Person。

1
2
alert(person1.constructor == Person);//true
alert(person2.constructor == Person);//true

instanceof操作符要更可靠一些,在以下例子中创建的所有对象即使Object的实例,同时也是Person的实例。

1
2
3
4
alert(person1 instanceof Object);//true
alert(person1 instanceof Person);//true
alert(person2 instanceof Object);//true
alert(person2 instanceof Person);//true
  1. 将构造函数当作函数
    构造函数与其他函数的唯一区别,就在于调用它们的方式不同。任何函数,只要通过new操作符,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。
1
2
3
4
5
6
7
8
9
10
//当作构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();
//作为普通函数调用
Person("Greg", 27, "Doctor");//添加到window
window.sayName();//"Greg"
//在另外一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName();
  1. 构造函数的问题
    前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是Function的实例。在ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。

之前的代码按下面方式写更好理解。

1
2
3
4
5
6
7
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)");
//与声明函数在逻辑上是等价的
}

通过把函数定义转移到构造函数外部来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
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, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

这么做问题又来了,全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域名不副实;并且如果有多个对象,对象又各自有多个方法,那就要定义很多个全局函数,就没有分装性可言了。

于是引入原型模式来解决这个问题。

原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针指向了一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(){}

Person.prototype.name = "Nocholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}

var person1 = new Person();
person1.sayName();//"Nicholas"

var person2 = new Person();
person2.sayName();//"Nicholas"

alert(person1.sayName == person2.sayName); //true

理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。

在默认情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含了一个指向prototype属性所在函数的指针。

person1和person2两个实例的内部将包含一个指针([[prototype]]),指向构造函数的原型对象。

可以通过isPrototypeOf()方法来确定对象之间是否存在[[prototype]]关系。

1
2
alert(Person.prototype.isPrototypeOf(person1));//true
alert(Person.prototype.isPrototypeOf(person2));//true

Object.getPrototypeOf()可以方便地取回一个对象的原型

1
2
alert(Object.getPrototypeOf(person1) == Person.prototype);//true
alert(Object.getPrototypeOf(person1).name);//"Nicholas"

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象是里重写原型中的值。如果在实例种添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例种创建该属性,该属性将会屏蔽原型中的那个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(){}

Person.prototype.name = "Nocholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}

var person1 = new Person();
var person2 = new Person();

person1.name = "Greg";
console.log(person1.name);//"Greg"
console.log(person2.name);//"Nicholas"

使用delete操作符可以完全删除实例属性,从而能重新访问原型中的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(){}

Person.prototype.name = "Nocholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}

var person1 = new Person();
var person2 = new Person();

person1.name = "Greg";
console.log(person1.name);//"Greg"
console.log(person2.name);//"Nicholas"

delete person1.name;
console.log(person1.name);//"Nicholas"

使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。存在实例中将返回true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Person(){}

Person.prototype.name = "Nocholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name"));//flase

person1.name = "Greg";
console.log(person1.name);//"Greg"来自实例
console.log(person1.hasOwnProperty("name"));//true

console.log(person2.name);//"Nicholas"来自原型
console.log(person2.hasOwnProperty("name"));//false

delete person1.name;
console.log(person1.name);//"Nicholas"来自原型
console.log(person1.hasOwnProperty("name"));//false

原型与in操作符

单独使用in操作符时,in操作符会通过对象能够访问给定属性时返回true。无论该属性存在于实例中还是原型中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Person(){}

Person.prototype.name = "Nocholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name"));//flase
console.log("name" in person1);//true

person1.name = "Greg";
console.log(person1.name);//"Greg"来自实例
console.log(person1.hasOwnProperty("name"));//true
console.log("name" in person1);//true

console.log(person2.name);//"Nicholas"来自原型
console.log(person2.hasOwnProperty("name"));//false
console.log("name" in person2);//true

delete person1.name;
console.log(person1.name);//"Nicholas"来自原型
console.log(person1.hasOwnProperty("name"));//false
console.log("name" in person2);//true

in操作符与hasOwnProperty()方法同时使用,就可以判断属性是否存在于原型中。

1
2
3
function hasPrototypeProperty(object, name){
return !object.hasOwnPrototype(name) && (name in object);
}

不存在与实例中且对象能够访问给定属性,就能判断存在于原型中。

使用for-in循环时,返回的是所有能够通过对象访问的、可枚举(enumerated)属性,其中包括存在于实例中的属性和原型中的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(){}

Person.prototype.name = "Nocholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
for(var prop in person1){
if(prop == "name"){
console.log("Found Name");
}
}

另外可以使用Object.keys()方法,接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(){}

Person.prototype.name = "Nocholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}

var keys = Object.keys(Person.prototype);
console.log(keys); //["name", "age", "job", "sayName"]

var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
console.log(p1keys); //["name", "age"]

如果要得到所有实例属性,无论是否可枚举,使用Object.getOwnPropertyNames()方法

1
2
var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); //["constructor", "name", "age", "job", "sayName"]

更简单的原型语言

用一个包含所有属性和方法的对象字面量来重写整个原型对象

1
2
3
4
5
6
7
8
9
10
11
function Person(){
}

Person.prototype = {
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function() {
alert(this.name);
}
}

但是这么写会将原本的constructor属性指向Object构造函数。

1
2
3
4
5
6
var friend = new Person();

console.log(friend instanceof Object);//true
console.log(friend instanceof Person);//true
console.log(friend.constructor == Person);//false
console.log(friend.constructor == Object);//true

因此可以将constructor设置回去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(){
}

Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function() {
alert(this.name);
}
};
keys = Object.keys(Person.prototype);
console.log(keys);//["constructor", "name", "age", "job", "sayName"]

但是这样设置又会让constructor的Enumerable特性被设置为true,于是又可以使用Object.defineProperty()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(){
}

Person.prototype = {
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function() {
alert(this.name);
}
};
Object.defineProperty(Person.prototype,'constructor', {
enumerable : false,
value : Person
});
keys = Object.keys(Person.prototype);
console.log(keys);//["name", "age", "job", "sayName"]

原型的动态性

两个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(){
}

Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};

var friend = new Person();

Person.prototype.sayHi = function(){
alert("hi");
};

friend.sayHi(); //"hi"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(){
}

var friend = new Person();

Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function(){
console.log(this.name);
}
};

friend.sayName();//error

第二个情况会出现错误的原因是重写整个原型对象(修改constructor),会创建一个新的原型。
**实例中的指针仅指向原型,不指向构造函数。

原生对象的原型

Object、Array、String等原生引用类型都在其构造函数的原型上定义了方法。

1
2
console.log(typeof Array.prototype.sort);//function
console.log(typeof String.prototype.subString);//function

于是可以给基本包装类型添加方法

1
2
3
4
5
6
String.prototype.startWith = function (text) {
return this.indexOf(text) == 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello"));//true

不推荐在产品化程序种修改原生对象的原型

原型对象的问题

由于共享的本质引起原型模式的问题。如下例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(){

}

Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends : ["Shelby", "Counrt"],
sayName : function () {
alert(this.name);
}
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Van");
console.log(person1.friends);//["Shelby", "Counrt", "Van"]
console.log(person2.friends);//["Shelby", "Counrt", "Van"]

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

  • 构造函数模式用于定义实例属性
  • 原型模式用于定义方法和共享的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}

Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
};//对象字面量,用冒号表达属性

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 28, "Doctor");

person1.friends.push("Van");
console.log(person1.friends);// ["Shelby", "Court", "Van"]
console.log(person2.friends);// ["Shelby", "Court"]

动态原型模式

通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

不能使用对象字面量重写原型。会切断实例与新原型之间的联系

寄生(parasitic)构造函数模式

创造一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
}
return o;
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

与工厂模式的区别在于:使用new操作符并把使用的包装函数叫做构造函数。

这个模式下可以在特殊的情况下用来为对象构造函数。假设要创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function SpecialArray(){
//创建数组
var values = new Array();

//添加值
values.push.apply(values, arguments);

//添加方法
values.toPipeString = function() {
return this.join("|");
}

//返回数组
return values;
}

var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipeString());//red|blue|green

缺点,不能通过instanceof操作符确定对象类型。与工厂模式都存在对象识别问题。

稳妥构造函数模式

稳妥对象(durable objects)值的是没有公共属性,而其方法也不引用this的对象(也不使用new)。适合在安全的环境下使用。

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//在这里定义私有变量和函数
//添加方法
o.sayName = function(){
alert(name);
};
//返回对象
return o
}

在这种模式下,除了使用sayName()方法之外,没有其他方法访问name的值。

继承

许多OO语言都支持两种继承方式:
接口继承和实现继承。

  • 接口继承值继承方法签名
  • 实现继承则继承实际的方法

ECMAScript只支持实现继承,依靠原型链实现。

原型链

利用一个引用类型继承另一个引用类型的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

此时,调用instance.getSuperValue()会经历三个搜索步骤:

  1. 搜索实例
  2. 搜索SubType.prototype;
  3. 搜索SuperType.prototype,最后一部才会找到该方法

别忘记默认的原型

所有引用类型默认继承了Object,而这个继承也是通过原型链实现的。

确定原型和实例的关系

  • 用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。
1
2
3
console.log(instance instanceof Object);//true
console.log(instance instanceof SuperType);//true
console.log(instance instanceof SubType);//true
  • 用isPrototypeOf()方法,只要是原型链出现过的原型,都可以说是该原型链所派生的实例的原型。
1
2
3
console.log(Object.prototype.isPrototypeOf(instance));
console.log(SuperType.prototype.isPrototypeOf(instance));
console.log(SubType.prototype.isPrototypeOf(instance));

谨慎定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function SuperType(){
this.property = true;
}

SuperType.prototype.getSuperValue = function(){
return this.property;
};
var instance2 = new SuperType();
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();

console.log(instance.getSuperValue());//false
console.log(instance.getSuperValue());//false

通过SubType的实例调用getSuperValue()时,调用的就是重新定义的方法。而SuperType还是调用原来的方法(这里有疑问,不应该都调用重新定义的方法吗)。

通过原型链实现继承时,不能使用对象字面量创建原型方法,这么做会重写原型链。

原型链的问题

最主要的原因来自包含引用类型值的原型。

引用类型值的原型属性会被所有实例共享,这就是为什么要在构造函数种,而不是在原型对象中定义属性的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SuperType(){
this.colors = ["red", "blue", "green"];
}

function SubType(){

}
//继承了SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);//["red", "blue", "green", "black"]

var instance2 = new SubType();
console.log(instance2.colors);//["red", "blue", "green", "black"]

第二个问题是,在创建子类型的实例时,不能向超类型的构造函数中传递参数。没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

借用构造函数

通过使用apply()和call()方法在新创建的对象上执行构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(){
this.colors = ["red", "blue", "green"];
}

function SubType(){
//继承了SuperType
SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);//["red", "blue", "green", "black"]

var instance2 = new SubType();
console.log(instance2.colors);//["red", "blue", "green"]

  1. 传递函数
1
2
3
4
5
6
7
8
9
10
11
12
13
function SuperType(name){
this.name = name;
}

function SubType(){
//继承了SuperType
SuperType.call(this, "Nicholas");
//实例属性
this.age = 29;
}
var instance = new SubType();
console.log(instance.name);//Nicholas
console.log(instance.age);//29
  1. 借用构造函数的问题
  • 方法在构造函数中定义,函数的复用成问题
  • 在超类型的原型种定义的方法,对子类而言不可见,所有类型都只能使用构造函数模式。

组合继承

借用构造函数+原型链

  • 使用原型链实现对原型属性和方法的继承
  • 借用构造函数来实现对实例属性的继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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;
}

//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(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("Grag", 27);
console.log(instance2.colors);//["red", "blue", "green"]
instance2.sayName();//Grag
instance2.sayAge();//27

var instance3 = new SuperType("Linda", 25);
console.log(instance3.colors);//["red", "blue", "green"]
instance3.sayName();//Grag
instance3.sayAge();//error

SuperType构造函数定义了两个属性:name和colors。SuperType的原型定义了一个方法sayName()。SubType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了它自己的属性age。然后,将SuperType的实例赋值给SubType的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同SubType实例既分别拥有自己属性——包括color属性,又可以使用相同的方法。

原型式继承

基于已有的对象创建新对象,同时不必创建自定义类型。

1
2
3
4
5
function object(o){
function F(){}
F.prototype = o;
return new F();
}

先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了临时类型的一个新实例。相当于对传入的对象进行浅复制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name : "Nicholas",
friends : ["Shelby", "Court", "Van"]
}
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);//["Shelby", "Court", "Van", "Rob", "Barbie"]

需要有一个对象作为另一个对象的基础,然后进行拷贝。
ECMAScript5新增Object.create()方法规范化了原型式继承。

该方法接收两个参数:第一个是用作新对象原型的对象和一个(可选的)为新对象定义额外属性的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
var person = {
name : "Nicholas",
friends : ["Shelby", "Court", "Van"]
}
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);//["Shelby", "Court", "Van", "Rob", "Barbie"]

1
2
3
4
5
6
7
8
9
10
11
var person = {
name : "Nicholas",
friends : ["Shelby", "Court", "Van"]
}
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});

console.log(anotherPerson.name);//Greg

寄生式继承

与寄生构造函数和工厂模式类似,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

1
2
3
4
5
6
7
function createAnother(original){
var clone = obejct(original);//通过调用函数创建一个新对象
clone.sayHi = function(){
alert("Hi");
};
return clone;
}

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;与构造函数类似。

寄生组合式继承

组合继承的问题在于调用了两次超类型构造函数。
使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

1
2
3
4
5
function inheritPrototype(subType, superType){
var prototype = object(supterType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}

第一步创建超类型原型的一个副本。

第二步为创建的副本添加constructor属性,弥补因重写原型而失去的默认的constructor属性。

第三部,将创建的对象赋值给子类型的原型。

这样就可以替代之前的子类型原型赋值语句了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function SuperType(name){
this.name = name;
this.color = ["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);
};

小结

  • 工厂模式:使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。后来被构造函数模式取代。
  • 构造函数模式,可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符,不过缺点在于它的每个成员不能复用。
  • 原型模式,使用构造函数的prototype属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,使用原型定义共享的属性和方法。

javascript主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法,因此不宜单独使用。解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样可以做到每个实例都具有自己的属性,同时还能保证值使用构造函数模式来定义类型。使用最多的继承模式是组合继承,用原型链继承共享的属性和方法,通过借用构造函数继承实例属性。

  • 原型式继承,不必预先定义构造函数的情况下实现继承,本质是执行对给定对象的浅复制。
  • 寄生式继承,基于某个对象或者某些信息创建一个对象,然后增强对象,最后返回对象。
  • 寄生组合模式,解决了组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。