# 面向对象程序设计

# 一、理解对象

面向对象的语言 有一个标志,那就是它们都有 的概念,通过类可以创建任意多个具有 相同属性和方法 的对象。而 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);
  },
};
1
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"
1
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
1
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;
      }
    },
  },
});
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

# 1.3 读取属性的特性

Object.getOwnPropertyDescriptor()可以取得给定属性的 描述符。接收两个参数:属性所在的对象、要读取其描述符的属性名称。返回值是一个对象,这个对象有configurableenumerablewritablevalue(如果是访问器属性,那返回值属性是configurableenumerablegetset)。

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"
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

# 二、创建对象

使用 构造函数式 或者 对象字面量 来创建对象,有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,推出了 工厂模式 的一种变体。

# 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");
1
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");
1
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
1
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"
1
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
1

要解决这个问题,可以在构造函数的 外部 创建一个函数,让构造函数里的那个属性指向这个新函数,那么所有实例对象的那个属性也都指向了这个新函数。

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");
1
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
1
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的
1
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属性
1
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,不能通过对象实例重写原型中的值
1
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在原型对象中
}
1
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",不管它是实例中的还是原型中
}
1
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是个数组
});
1
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"是不可枚举的
});
1
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,
});
1
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]]
1
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,共享同一个引用类型
1
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,共享的
1
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);
    };
  }
}
1
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"
1
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,外部也不能访问到构造函数中的原始数据
1
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里查找
1
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
1
2
3

另一种使用isPrototypeOf()方法:

console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(A.prototype.isPrototypeOf(instance)); // true
console.log(B.prototype.isPrototypeOf(instance)); // true
1
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
1
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;
  },
};
1
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"]
1
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
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

这样避免了 原型链模式借用构造函数模式 的缺陷,又融合了它们的优点,是比较常用的一种继承模式。而且instanceofisPrototypeOf()也能用于识别基于组合继承创建的对象。
其实 组合继承 也有缺点:无论什么情况下都会 调用两次父类的构造函数;在第一次调用父类的构造函数,子类的原型上得到了父类的 实例属性和原型上的属性,在第二次调用父类的构造函数,子类的实例会得到父类的 实例属性,这样实例属性既存在于实例上也存在于原型上,会显得很冗余。

# 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",虽说也是共享,但之前使用“实例.属性名”重新赋值的手段屏蔽了原型上的同名属性
1
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",虽说也是共享,但之前了原型上的同名属性
1
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; // 返回这个新对象
}
1
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
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