# 面向对象程序设计
# 一、理解对象
面向对象的语言 有一个标志,那就是它们都有 类 的概念,通过类可以创建任意多个具有 相同属性和方法 的对象。而 ECMAScript 没有类的概念,与其他基于类的语言创建的对象不同的是,ECMAScript 定义的对象是 无序属性 的集合,其属性可以是基本值、对象或函数。通俗点说就是 ECMAScript 里的对象,其属性是一群没有特点顺序的值,属性或方法都有自己的名字,这些名字有自己映射的值,可以把这样的对象结构想象成 散列表(键值对)。
创建自定义对象早期用 构造函数 并 逐个将属性赋值,后来常用 对象字面量 来创建对象,例:
// 构造函数的方式
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function () {
console.log(this.name);
};
// 对象字面量的方式
var person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function () {
console.log(this.name);
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1.1 属性类型
ECAMScript 属性有两种:数据属性 和 访问器属性。
ECAMScript5 定义了内部才使用(实现 js 引擎)的 特性,是用来描述属性的各种特征的,特性是用两对方括号括起来表示的,例:[[Enumerable]]
。
# 1.1.1 数据属性
数据属性有 4 个描述其行为的特性:
[[Configurable]]
:可配置的,表示能否通过删除属性的行为从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,该特性默认为 true;[[Enumerable]]
:可枚举的,表示能否通过 for-in 循环返回属性,该特性默认为 true;[[Writable]]
:可重写的,表示能否修改属性的值,该特性默认为 true;[[Value]]
:包含这个属性的数据值,属性值的读写都在这个位置,该特性默认为 undefined。
想修改属性默认的特性,可以使用 ECMAScript5 的Object.defineProperty()
方法,接收三个参数:属性所在的对象、属性的名字 和 一个描述符对象。其中描述符对象的属性必须是 configurable、enumerable、writable 和 value 中的一个或多个。
var person = {};
Object.defineProperty(person, "name", {
configurable: false, // 不可配置,那么不能通过删除语句来删除对象的这个属性,并且其他特性也不能修改了,configurable也改不回true了
value: "Nicholas",
});
console.log(person.name); // "Nicholas"
delete person.name; // 因为[[Configurable]]特性为false,这里删除无效
console.log(person.name); // "Nicholas"
2
3
4
5
6
7
8
之前是使用Object.defineProperty()
修改属性,如果使用它 创建新属性,不指定[[Configurable]]
、[[Enumerable]]
、[[Writable]]
特性时,这三个会被默认设置为 false。
大多数情况下不会去使用Object.defineProperty()
操作属性的那些特性(没有使用的必要),只是为了加深对 JavaScript 对象的理解。
# 1.1.2 访问器属性
访问器属性不包含数据值,但包含一对 getter 和 setter 函数(非必需的),访问器属性有 4 个特性:
[[Configurable]]
:可配置的,表示能否通过删除属性的行为从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,该特性默认为 true;[[Enumerable]]
:可枚举的,表示能否通过 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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
只指定 getter 而不指定 setter,意味着属性不能写,尝试写入属性会被忽略,严格模式下会抛错;
只指定 setter 而不指定 getter,意味着属性不能读,尝试读出属性会返回 undefined,严格模式下会抛错。
# 1.2 定义多个属性
Object.defineProperties()
方法可以通过描述符一次定义多个属性。第一个参数是要添加和修改其属性的对象(目标对象),第二个参数是一个对象其属性与目标对象的属性一一对应。
var book = {};
Object.defineProperties(book, {
_year: {
// 数据属性
writable: true,
value: 2004,
},
edition: {
// 数据属性
writable: true,
value: 1,
},
year: {
// 访问器属性(三个属性同时创建)
get: function () {
return this._year;
},
set: function () {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
},
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1.3 读取属性的特性
Object.getOwnPropertyDescriptor()
可以取得给定属性的 描述符。接收两个参数:属性所在的对象、要读取其描述符的属性名称。返回值是一个对象,这个对象有configurable
、enumerable
、writable
和value
(如果是访问器属性,那返回值属性是configurable
、enumerable
、get
和set
)。
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004,
},
edition: {
value: 1,
},
year: {
get: function () {
return this._year;
},
set: function () {
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.enumerable); // false
console.log(typeof descriptor.get); // "function"
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
# 二、创建对象
使用 构造函数式 或者 对象字面量 来创建对象,有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,推出了 工厂模式 的一种变体。
# 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("L", 22, "Student");
var person2 = createPerson("S", 35, "Doctor");
2
3
4
5
6
7
8
9
10
11
12
函数 createPerson 可以根据入参来创建不同的对象(属性名相同,但属性值不同),下次创建新对象时不再需要写创建对象的代码了重新调用即可。
工厂模式虽然解决了创建多个相似对象的问题(代码重复的问题,还隐藏了细节),但却没有解决 对象识别 的问题(即怎么知道一个对象的类型)。
# 2.2 构造函数模式
构造函数模式 改进了工厂模式,在函数里 不显示 创建对象了,直接将属性和方法赋给了函数的 this 对象,去掉了 return 语句。然后参考其他语言的规范,将函数的首字母大写了,实例化时还使用了new
关键字,这样也就区分了 ECMAScript 中的其他函数,这种函数就叫做 构造函数。
function Person(name, age, job) {
// (1)隐式创建了一个新对象,
this.name = name; // (2)函数调用处的执行环境赋给了新对象,
this.age = age; // (3)为这个新对象添加属性,
this.job = job; // (4)最后隐式返回了这个新对象
this.sayName = function () {
console.log(this.name);
};
}
var person1 = new Person("L", 22, "Student");
var person2 = new Person("S", 35, "Doctor");
2
3
4
5
6
7
8
9
10
11
person1 和 person2 都拥有同一个constructor
(构造函数)属性,该属性指向Person
(解决了工厂模式不能识别对象类型的问题),检查对象可以使用这个属性,但最好还是使用instanceof
。
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
console.log(person1 instanceof Object); // true
console.log(person2 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Person); // true
2
3
4
5
6
7
# 2.2.1 将构造函数当作函数
// 当作构造函使用
var person = new Person("L", 22, "Student");
person.sayName(); // "L"
// 作为普通函数调用
Person("S", 35, "Doctor");
window.sayName(); // "S"
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "L", 22, "Student");
o.sayName(); // "L"
2
3
4
5
6
7
8
9
10
11
12
将构造函数当作函数一般就两种使用方式,一种是不使用 new 而直接调用,另一种就是不使用 new 但配合call()
使用。
- 不使用 new 而直接调用:属性和方法都被添加给 window 对象了,因为它是在全局作用域中调用的,this 对象指向 window 对象(Global 对象);
- 不使用 new 但配合
call()
使用:配合apply()
也可以的;外部新建了一个对象,让这个对象作为 call 的第一个参数,也就是说 Person 的执行环境就是这个新对象了,最后 Person 里的新属性和方法都会添加到这个新对象上。
# 2.2.2 构造函数的问题
构造函数的主要问题:每个方法都要在每个实例上 重新创建 一遍,也就是说每个实例拥有同名函数但值不同。
console.log(person1.sayName == person2.sayName); // false
要解决这个问题,可以在构造函数的 外部 创建一个函数,让构造函数里的那个属性指向这个新函数,那么所有实例对象的那个属性也都指向了这个新函数。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
var person1 = new Person("L", 22, "Student");
var person2 = new Person("S", 35, "Doctor");
2
3
4
5
6
7
8
9
10
11
这样确实解决了 重复创建函数 的问题,但是有了新问题:外部定义的新函数应该只能被对应的构造函数所使用,但实际上其他构造函数一样能使用,这就比较混乱了,然后构造函数里要是需要多个函数,那么在外部还要创建多个新函数(没有封装性)。
# 2.3 原型模式
函数都有一个prototype
属性,它指向一个对象(原型对象),这个对象上存储了 所有实例共享的属性和方法。意思就是,通过某个构造函数实例出的一些对象,它们都共享了 原型对象 上的属性和方法。
使用原型对象的好处是, 不必在构造函数里 定义对象实例的信息,可以直接将这些信息添加到原型对象中。
function Person() {}
Person.prototype.name = "S";
Person.prototype.age = 35;
Person.prototype.job = "Doctor";
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); // "S"
var person2 = new Person();
person2.sayName(); // "S"
console.log(person1.sayName == person2.sayName); // true
2
3
4
5
6
7
8
9
10
11
12
# 2.3.1 理解原型对象
函数的prototype
属性指向原型对象,而原型对象有个constructor
属性,这个属性是指向 原型对象所在函数 的,也就是说Person.prototype.constructor
是指向Person
的;原型对象也会有其他常见属性和方法,这是从 Object 继承而来的;然后剩下的就是在代码中自己添加的属性和方法了。
使用原型模式创建出来的实例,该实例有个内部属性[[Prototype]]
,该内部属性是指向 对应构造函数的原型对象 的,不是指向构造函数的,这点要区分开来。构造函数的prototype
和实例的[[Prototype]]
都指向原型对象的。
可以使用isPrototypeOf()
来确定当前原型对象是否被目标实例内部属性[[Prototype]]
指引着,例:
console.log(Person.prototype.isPrototypeOf(person1)); // true,person1的内部属性[[Prototype]]是指向Person.prototype的
console.log(Person.prototype.isPrototypeOf(person2)); // true,person2的内部属性[[Prototype]]是指向Person.prototype的
2
ECMAScript5 新增的Object.getPrototypeOf()
方法可以获得 实例内部属性[[Prototype]]
指向的原型对象,例:
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true,person1的内部属性[[Prototype]]是指向Person.prototype的
console.log(Object.getPrototypeOf(person1).name); // "S",原型对象上name属性
2
读取对象某个属性的过程:会先从 对象实例本身 开始搜索,如果在实例中搜索到该属性就会返回该属性的值,如果没有找到会去实例对应的 原型对象 上搜索,搜索到该属性会返回该属性的值。
虽然可以通过对象实例访问保存在原型中的值,但却 不能通过对象实例重写原型中的值;当然,如果在对象实例上添加了一个与原型对象里 同名 的属性,那实例上的这个属性会 屏蔽 原型对象上的同名属性(因为读取属性的过程就是实例属性优先)。 可以屏蔽但不能修改。
hasOwnProperty()
方法可以检测一个 属性是存在于实例对象中,存在于实例中会返回 true,否则返回 false(存在原型中或原型里也不存在)。例如上面使用delete
后,person1 里的 name 属性是属于原型对象的,这时使用hasOwnProperty()
会返回 false,person1.hasOwnProperty("name"); // false
。
function Person() {}
Person.prototype.name = "S";
Person.prototype.age = 35;
Person.prototype.job = "Doctor";
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "L";
console.log(person1.name); // "L",屏蔽了
console.log(person2.name); // "S"
delete person1.name; // 解除屏蔽
console.log(person1.name); // "S"
person1.hasOwnProperty("name"); // false,实例上没有name属性了
Object.getPrototypeOf(person1).name = "SS"; // error,不能通过对象实例重写原型中的值
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.3.2 原型与 in 操作符
单独使用in
操作符:判断属性是否存在于对象中,判断不了该属性是存在于 实例 中还是 原型 中。
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && name in object; // return结果为true时,name在原型对象中
}
2
3
在 for-in 循环中使用:遍历对象的可枚举属性,不管它存在于 实例 中还是 原型 中。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype.sayName = function () {
console.log(this.name);
};
var person = new Person("S", 35, "Doctor");
for (var prop in person) {
if (person.hasOwnProperty(prop)) {
console.log("prop1", prop); // "name"、"age"、"job",过滤掉了原型上的属性"sayName"
}
console.log("prop2", prop); // "name"、"age"、"job"、"sayName",不管它是实例中的还是原型中
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ECMAScript5 新增的Object.keys()
方法会返回 可枚举的实例属性(不会返回该对象的原型上的属性);上面的例子里的 for-in 可以替换为:
var keys = Object.keys(person);
keys.forEach(function (prop) {
console.log(prop); // "name"、"age"、"job",keys是个数组
});
2
3
4
不止Object.keys()
可替代 for-in,Object.getOwnPropertyNames()
也可以,只是它是返回所有 实例属性,不管是否可枚举。
// 可用在某个原型对象上,那就返回该原型对象的“实例属性”,不会返回该原型对象的原型上的属性;一定要区分“实例属性”和“原型上的属性”
var keys = Object.getOwnPropertyNames(Person.prototype);
keys.forEach(function (prop) {
console.log(prop); // "consctrutor"、"sayName","consctrutor"是不可枚举的
});
2
3
4
5
# 2.3.3 更简单的原型语法
使用字面量的方式优化在原型上添加属性,但是要注意,使用字面量形式相当于重写了 prototype,那么其 constructor 会被覆盖,这里得重新赋值 constructor;但又会引入一个新问题,重新赋值的 constructor 其[[Enumerable]]
特性变为了 true,也就可枚举的了,原生的 constructor 属性是不可枚举的。
function Person() {}
Person.prototype = {
constructor: Person, // 重新赋值,但是[[Enumerable]]特性变为了true
name: "S",
age: 35,
job: "Doctor",
sayName: function () {
console.log(this.name);
},
};
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person,
});
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2.3.4 原型的动态性
之前我们通过实例的[[Prototype]]
(也就是Object.getPrototypeOf(person)
)是修改不了原型里的值,现在可以通过Person.prototype.xxx
来修改原型里的值,并且能立即从实例上反映出这个修改。
修改没问题,但重写Person.prototype
得注意一下:如果实例化在先,重写Person.prototype
在后,实例对象的[[Prototype]]
指向的原型对象是没有变的,它没有指向重写后的Person.prototype
,还是指向着老原型对象。
function Person() {}
var friend = new Person(); // 先实例化
Person.prototype = {
// 后重写Person.prototype
constructor: Person,
name: "S",
age: 35,
job: "Doctor",
sayName: function () {
console.log(this.name);
},
};
friend.sayName(); // error,只是将新对象赋给了Person.prototype,并没有一并赋给对象的[[Prototype]]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
解决办法当然是将重写Person.prototype
这步骤放在 实例化之前 了。
# 2.3.5 原生对象的原型
原生的引用类型 创建时也采用了 原型模式,可在原型上找到一些方法:Array.prototype.sort()、String.prototype.substring()。可以在原型上新添方法,不过不推荐新添或修改方法。
# 2.3.6 原型对象的问题
原型模式 省略了为构造函数传递初始化参数这一环节,而会在原型上直接赋予属性的值,所有的实例也是 共享 了原型上的属性,一旦原型上的值发生改变(一般不会变,除了引用类型属性),就会立即同步到所有实例上。
这样的共享对于一些 固定值 的属性来说还是有帮助的,比如 方法复用 和 常量式的属性;而对一些 经常变化 的值来说,就非常不友好,因为无法重写原型上的属性的值,只能借助一些特殊的手段: - 原型里的基本类型属性:可以通过实例.属性名
的方式给某个实例对象添加对应的同名属性,这样就可以 屏蔽 原型上的同名属性了; - 原型里的引用类型属性:也可以使用屏蔽的方式,也可以直接 调用引用类型属性的方法 或者 修改引用类型属性的属性,但会有个问题,效果会立即同步到其他实例上,因为原型上的属性都是被所有实例共享的。
function Person() {}
Person.prototype = {
constructor: Person,
name: "S",
age: 35,
job: "Doctor",
friends: ["Shelby", "Court"],
sayName: function () {
console.log(this.name);
},
};
var person1 = new Person();
var person2 = new Person();
// 使用“实例.属性名”重新赋值的时候,其实是在实例上新增属性,这样就屏蔽了原型上的同名属性
person1.name = "L";
console.log(person1.name); // "L",虽然是共享同一个name,但上一步在person1上新建了一个name属性
console.log(person2.name); // "S",共享同一个name,还是原型上的name,person2里是没有name属性的
// “实例.属性名”可以屏蔽原型上的同名属性,这里是调用了原型上的引用类型属性的方法,效果会立即同步到其他实例上
person1.friends.push("Van");
console.log(person1.friends); // ["Shelby", "Court", "Van"],效果同步了;如果是person1.friends = xxx;就可以屏蔽了
console.log(person2.friends); // ["Shelby", "Court", "Van"],效果同步了
console.log(person1.friends === person2.friends); // true,共享同一个引用类型
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2.4 组合使用构造函数模式和原型模式
在原型与 in 操作符里讲“实例属性”和“原型上的属性”举过里例子,在构造函数里定义一些属性(独享),在原型里也定义一些属性(共享),这是组合使用 构造函数模式 和 原型模式 的一种混成模式。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["W", "Q"];
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(this.name);
},
};
var person1 = new Person("L", 22, "Student");
var person2 = new Person("S", 35, "Doctor");
person1.friends.push("T");
console.log(person1.friends); // ["W", "Q", "T"]
console.log(person2.friends); // ["W", "Q"]
console.log(person1.friends === person2.friends); // false,独享的
console.log(person1.sayName === person2.sayName); // true,共享的
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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 () {
console.log(this.name);
};
}
}
2
3
4
5
6
7
8
9
10
这种方式可谓非常完美,但要注意的是 添加到原型 并不是重写原型,因为在调用构造函数时相当于先实例一个对象,具体原因可以查看原型的动态性。
# 2.6 寄生构造函数模式
寄生构造函数模式 其实是一个 工厂模式,只是把定义函数和使用函数的方式改为了 构造函数模式 的方式。这个模式的作用,为一个类型添加额外的方法。能使用其他模式的情况下最好不要使用这个模式,因为在构造函数内部或者外部创建对象都一样,而在内部创建只是为了添加额外新方法,对这个对象使用instanceof
检测出来的结果跟构造函数本身没有关系。
function SpecialArray() {
var values = new Array();
values.push.apply(values, arguments);
values.toPipedString = function () {
// 为Array额外添加的方法
return this.join("|");
};
return values;
}
var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString()); // "red|blue|green"
2
3
4
5
6
7
8
9
10
11
# 2.7 稳妥构造函数模式
通过 稳妥构造函数模式 创建的对象是 稳妥对象,这种稳妥对象没有 公共属性 而且其方法也不引用 this 的对象。适合在一些安全的环境中或者防止数据给其他应用程序改动,因为在构造函数里没有使用 this,也没有将初始入参绑到对象上(只给你使用,但不能修改原始值),那么在外部也不能访问到构造函数中的原始数据。
function Person(name, age, job) {
var o = new Object();
o.sayName = function () {
// 为Object额外添加的方法
console.log(name); // 没有使用this
};
return o;
}
var person = Person("L", 22, "Student"); // 没有使用new
person.sayName(); // "L"
console.log(person.name); // undefined,外部也不能访问到构造函数中的原始数据
2
3
4
5
6
7
8
9
10
11
# 三、继承
一般有两种类型的继承:接口继承 和 实现继承。接口继承只继承 方法签名,而实现继承是继承了 实际的方法。ECMAScript 里函数没有签名,只支持实现继承,且主要依靠 原型链 来实现的。
# 3.1 原型链
ECMAScript 中的继承,是利用原型让一个引用类型继承另一个引用类型的属性和方法。让引用类型 A 的 实例 作为引用类型 B 的 原型对象,那么引用类型 B 的 原型对象 上就有了 [[Prototype]]
指针 并指向引用类型 A 的 原型对象,而引用类型 A 的原型对象里有个constructor
属性就是指向 引用类型 A 的。这样链式结构也就是所谓的 原型链,引用类型 B 可以使用引用类型 A 的属性和方法(B 继承了 A)。
function A() {
this.property = true;
}
A.prototype.getValueA = function () {
return this.property;
};
function B() {
this.subproperty = false;
}
B.prototype = new A(); // 引用类型A的实例作为引用类型B的原型对象,B继承了A
B.prototype.getValueB = function () {
// B除了继承了A的属性和方法,还给自己新添加了方法
return this.subproperty;
};
var instance = new B();
console.log(instance.getValueA()); // true,B继承了A,所以可以访问A的方法
console.log(instance.constructor === A); // true,B原型等于了A的实例,B原型的constructor不再是B了,会去A里查找
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
有个需要注意的地方,instance.constructor
现在指向了引用类型 A。这是因为引用类型 B 的原型指向了引用类型 A 的原型,而引用类型 A 的原型的constructor
是指向引用类型 A 的。
# 3.1.1 默认的原型
所有引用类型都继承了 Object,这个继承也是通过原型链实现的,那么所有引用类型的默认原型都是 Object 的实例,因此默认原型会指向 Object 的原型。这也是自定义类型都会继承 toString()、valueOf()等默认方法的根本原因。
# 3.1.2 确定原型和实例的关系
一种是使用instanceof
操作符:
console.log(instance instanceof Object); // true
console.log(instance instanceof A); // true
console.log(instance instanceof B); // true
2
3
另一种使用isPrototypeOf()
方法:
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(A.prototype.isPrototypeOf(instance)); // true
console.log(B.prototype.isPrototypeOf(instance)); // true
2
3
# 3.1.3 谨慎地定义方法
子类在原型上添加新方法或者重写父类的方法时,语句一定要在“继承语句”之后,下面这个例子就是“(2)(3)”必须得在“(1)”之后。
function A() {
this.property = true;
}
A.prototype.getValueA = function () {
return this.property;
};
function B() {
this.subproperty = false;
}
B.prototype = new A(); // (1)继承语句,B继承了A
B.prototype.getValueB = function () {
// (2)B给自己新添加了方法
return this.subproperty;
};
B.prototype.getValueA = function () {
// (3)重写了A里存在的方法
return false;
};
var instance = new B();
console.log(instance.getValueA()); // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
通过原型链实现继承时,不能在“继承语句”之后再使用字面量重写原型,这样会打乱原型链而构不成继承关系了,例:
function A() {
this.property = true;
}
A.prototype.getValueA = function () {
return this.property;
};
function B() {
this.subproperty = false;
}
B.prototype = new A(); // B继承了A
B.prototype = {
// 使用字面量添加新方法,会导致上一行代码无效
getValueB: function () {
return this.subproperty;
},
someOtherMethod: function () {
return false;
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3.1.4 原型链的问题
在继承里,原型链模式也有同样的问题。因为让 父类的实例 当作了 子类的原型对象,那么子类的所有实例 共享 父类的那个实例里的 所有属性,不管是父类的“实例属性”还是“原型上的属性”。
原型链模式还有一个问题:没有办法在不影响所有对象实例的情况下,给父类的构造函数中传递参数。
正因为上述两个问题,在实践中 很少单独使用原型链模式。
# 3.2 借用构造函数
借用构造函数,在子类的构造函数里调用父类的构造函数。在子类的实例里会执行父类的构造函数,那么子类的实例可以 独享 父类的实例属性。
function A() {
this.colors = ["red", "blue", "green"];
}
function B() {
A.call(this); // 使用apply也可以,并且也可以传递参数,比如给colors传值
}
var instance1 = new B();
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
var instance2 = new B();
console.log(instance2.colors); // ["red", "blue", "green"]
2
3
4
5
6
7
8
9
10
11
借用构造函数的方式可以解决原型链模式里“不能向父类的构造函数中传递参数”的问题,但跟“创建对象”里的“构造函数模式”有同样的问题,就是 函数无法复用,每次调用构造函数都会重复创建了一次函数。
# 3.3 组合继承
组合继承,就是将 原型链模式 和 借用构造函数模式 组合到一块,使用原型链实现对 原型属性和方法的继承,通过借用构造函数来实现对 实例属性的继承。
function A(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
A.prototype.sayName = function () {
// 方法写在原型上,达到复用方法的目的
console.log(this.name);
};
function B(name, age) {
A.call(this, name); // 第二次调用构造函数,“借用构造函数模式”,来继承父类的实例属性
this.age = age;
}
B.prototype = new A(); // 第一次调用构造函数,“原型链模式”,继承父类的实例属性和原型上的属性
B.prototype.constructor = B;
B.prototype.sayAge = function () {
// 方法写在原型上,达到复用方法的目的
console.log(this.age);
};
var instance1 = new B("L", 22);
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // "L"
instance1.sayAge(); // 22
var instance2 = new B("S", 35);
console.log(instance2.colors); // ["red", "blue", "green"]
instance2.sayName(); // "S"
instance2.sayAge(); // 35
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
这样避免了 原型链模式 和 借用构造函数模式 的缺陷,又融合了它们的优点,是比较常用的一种继承模式。而且instanceof
和isPrototypeOf()
也能用于识别基于组合继承创建的对象。
其实 组合继承 也有缺点:无论什么情况下都会 调用两次父类的构造函数;在第一次调用父类的构造函数,子类的原型上得到了父类的 实例属性和原型上的属性,在第二次调用父类的构造函数,子类的实例会得到父类的 实例属性,这样实例属性既存在于实例上也存在于原型上,会显得很冗余。
# 3.4 原型式继承
原型链模式 其实也是用了构造函数的写法。如果不使用构造函数而使用普通函数,并且在普通函数内部定义一个 临时引用类型 的构造函数,再让普通函数的 入参对象 作为这个临时引用类型的 原型,最后返回这个临时引用类型的 实例,那么这个实例是继承了函数入参对象的属性和方法,这种继承方式叫做 原型式继承。
function object(o) {
function F() {} // 定义临时引用类型F的构造函数
F.prototype = o; // 普通函数的入参对象o作为这个临时引用类型F的原型
return new F(); // 最后临时引用类型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"],之前的操作效果同步了(因为共享)
console.log(person.name); // "Nicholas"
console.log(anotherPerson.name); // "Greg",虽说也是共享,但之前使用“实例.属性名”重新赋值的手段屏蔽了原型上的同名属性
console.log(yetAnotherPerson.name); // "Linda",虽说也是共享,但之前使用“实例.属性名”重新赋值的手段屏蔽了原型上的同名属性
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ECMAScript5 通过新增Object.create()
方法规范化了原型式继承,该方法接收两个参数:作为新对象原型的对象、位新对象定义额外属性的对象。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"],
};
var anotherPerson = Object.create(person); // 生成了一个对象,这个对象的原型指向了person
anotherPerson.name = "Greg"; // 使用“实例.属性名”重新赋值的时候,其实是在实例上新增属性,这样就屏蔽了原型上的同名属性
anotherPerson.friends.push("Rob"); // 这不是屏蔽(屏蔽是用“=”),而是调用了原型上的引用类型属性的方法,效果会立即同步到其他实例上
var yetAnotherPerson = Object.create(person, {
name: {
value: "Linda", // 屏蔽了原型上的同名属性
},
});
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // ["Shelby", "Court", "Van", "Rob", "Barbie"],之前的操作效果同步了(因为共享)
console.log(person.name); // "Nicholas"
console.log(anotherPerson.name); // "Greg",虽说也是共享,但之前使用“实例.属性名”重新赋值的手段屏蔽了原型上的同名属性
console.log(yetAnotherPerson.name); // "Linda",虽说也是共享,但之前了原型上的同名属性
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
没必要兴师动众地创建构造函数,而只想让一个对象 浅复制 另一个对象,可以使用 原型式继承。但它与 原型链模式 的缺点一样,原型上的属性会被一直共享,特别是引用类型的属性。
浅复制,之前在变量、作用域和内存问题#复制变量值里提到过,复制对象的属性值,如果属性是引用类型,只会复制地址值,堆里的对象还是同一个(对象不会被复制)。
# 3.5 寄生式继承
寄生式继承 和 寄生构造函数模式 有些类似。使用了 工厂模式 并在里面调用了一个生成新对象的函数(例如上节的object()
),让一个对象复制另一个对象,然后给这个新对象添加自己的实例方法,最后返回这个新对象。
function createAnother(original) {
var clone = object(original); // 调用一个创建新对象的函数,不一定就是上节的object(),也可以是Object.create()
clone.sayHi = function () {
// 额外给新对象添加方法
console.log("Hi");
};
return clone; // 返回这个新对象
}
2
3
4
5
6
7
8
寄生式继承 里额外添加方法是做不到函数 复用 的。
# 3.6 寄生组合式继承
在组合继承那节说过它的缺点,继承到属性比较 冗余(原型和实例有同样的属性)。然后 寄生组合式继承 可以解决这个问题。
寄生组合式继承:不必为了指定子类的原型而调用父类的 构造函数,直接让子类的原型使用 寄生式继承(原型式继承也可以) 来继承父类的 原型,其他的同 组合继承。
function A(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
A.prototype.sayName = function () {
// 方法写在原型上,达到复用方法的目的
console.log(this.name);
};
function B(name, age) {
A.call(this, name); // “借用构造函数”,来继承实例属性
this.age = age;
}
B.prototype = Object.create(A.prototype); // “寄生式继承”或“原型式继承”,来继承原型上的属性
B.prototype.constructor = B;
B.prototype.sayAge = function () {
// 方法写在原型上,达到复用方法的目的
console.log(this.age);
};
var instance1 = new B("L", 22);
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // "L"
instance1.sayAge(); // 22
var instance2 = new B("S", 35);
console.log(instance2.colors); // ["red", "blue", "green"]
instance2.sayName(); // "S"
instance2.sayAge(); // 35
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