第6章 面向对象的程序设计(重点)

48 阅读22分钟

理解对象

对象:一组名值对,值可以是数据和函数。每个对象都是基于一个引用类型创建的,包括原生类型和自定义类型。

创建对象的两种方式

  • new Object()
  • 对象字面量

属性类型

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

1)数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。

var person = {
  name: "Alice",
};

name就是person对象的数据属性,它是一个包含数据值'Alice'的位置,在这个位置可以读取和写入值

数据属性有4个描述其行为的特性

  • [[Configurable]]: 可配置,包括能否用delete删除该属性,能否修改属性的特性(就这4个特性),能否把属性修改为访问器属性。直接在对象上定义的属性,Configurable默认值是true
  • [[Enumerable]]: 可枚举,能否通过for-in 循环返回属性。直接在对象上定义的属性,Enumerable默认值是true
  • [[Writable]]: 可写,表示能否修改属性的值。直接在对象上定义的属性,Writable默认值是true
  • [[Value]]:值,包含这个属性的数据值。读取属性值,从这个位置读;写入属性值,把新值保存在这个位置。默认值是undefined。

描述符对象的属性有4个:configurable、enumerable、writable、value

var person = {};

Object.defineProperty(person, "name", {
  value: "Alice",
  writable: false, // 不可写设置为false,就不能再修改这个属性
});

// 上面设置了不可写,所以这里修改不成功。name属性的值还是"Alice"
person.name = 'Jack'

console.log(person.name); // Alice
var person = {};

Object.defineProperty(person, "name", {
  value: "Alice",
  configurable: false,
});

// 1. 设置为不可配置后,相当于设置了不可修改,以下不生效
person.name = "Jack";
// 2. 也不可删除
delete person.name;

person.name = "Jack";

console.log(person.name); // Alice
2)访问器属性

访问器属性包含一对儿getter和setter函数。读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;写入访问器函数时,会调用wetter函数并传入新值,这个函数决定如何处理数据。

访问器属性有4个特性

  • [[Configurable]]: 可配置,包括能否用delete删除该属性,能否修改属性的特性(就这4个特性),能否把属性修改为数据属性。直接在对象上定义的属性,Configurable默认值是true
  • [[Enumerable]]: 可枚举,能否通过for-in 循环返回属性。直接在对象上定义的属性,Enumerable默认值是true
var person = {
  name: "Alice",
  hobby: "reading",
  _age: 30, // 只能通过对象方法访问
  type: "middle age",
};

// 定义一个访问器属性
Object.defineProperty(person, "age", {
  enumerable: true,
  get: function () {
    return this._age;
  },
  set: function (value) {
    this._age = value;
    if (value > 60) {
      this.type = "old age";
    } else if (value < 30) {
      this.type = "youth";
    } else {
      this.type = "middle age";
    }
  },
});

for (const property in person) {
  console.log(property);
}

person.age = 61

console.log(
  person.age, // 61, 其实调用了get()
  person.type // 'old age'
);

设置年龄会导致状态发生变化

定义多个属性

Object.defineProperties() 一次定义多个属性

读取属性的特性

Object.getOwnPropertyDescriptor(对象名,属性名),获取属性的特性描述对象

var person = {
  name: "Alice",
  hobby: "reading",
  _age: 30, // 只能通过对象方法访问
  type: "middle age",
};

// 定义一个访问器属性
Object.defineProperty(person, "age", {
  enumerable: true,
  get: function () {
    return this._age;
  },
  set: function (value) {
    this._age = value;
    if (value > 60) {
      this.type = "old age";
    } else if (value < 30) {
      this.type = "youth";
    } else {
      this.type = "middle age";
    }
  },
});

console.log(
  // {value: 'Alice', writable: true, enumerable: true, configurable: true}
  Object.getOwnPropertyDescriptor(person, "name"),
  // {value: 30, writable: true, enumerable: true, configurable: true}
  Object.getOwnPropertyDescriptor(person, "_age"),
  // {enumerable: true, configurable: false, get: ƒ, set: ƒ}
  Object.getOwnPropertyDescriptor(person, "age"),
);

创建对象

创建单个对象:Object构造函数、对象字面量

创建多个对象:比如创建批量对象,拥有相同结构,相同的属性名和方法名;比如一个班的50名学生就是50个对象,都有name、age、height、weight等属性,以及一些共同的方法。如果按照Object构造函数或者对象字面量的方式,就会产生很多重复代码。

为了解决创建多个对象而产生大量重复代码的问题,产生了7中创建对象的模式

1. 工厂模式

工厂模式抽象了创建具体对象的过程,用函数封装以特定接口创建对象的细节。

在函数中创建一个对象,给对象设置一些属性和方法,再返回对象。每次调用该函数,就可创建一个对象。

function createPerson(name, age, job) {
  // 1. 创建一个对象
  var obj = new Object();
  // 2. 给这个新创建的对象设置属性和方法
  obj.name = name;
  obj.age = age;
  obj.job = job;
  obj.sayName = function () {
    console.log(this.name);
  };
  // 3. 返回这个创建好的对象
  return obj;
}

var person1 = createPerson("Alice", 19, "actor");
var person2 = createPerson("Jack", 30, "Font-end Developer");

console.log(
  person1 instanceof createPerson, // false
  person1 instanceof createPerson // true
);
缺点

没法知道创建的对象的类型。

2. 构造函数模式

使用new 构造函数()会经历4个步骤

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象
function Person(name, age, job) {
  // 一旦 使用 new Person(), this 就指向了新对象
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function () {
    console.log(this.name);
  };
}
// 使用new 构造函数()会经历4个步骤
// 1. 创建一个新对象
// 2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
// 3. 执行构造函数中的代码(为这个新对象添加属性)
// 4. 返回新对象
var person1 = new Person("Jack", "35", "Java Developer");
person1.sayName() // Jack
console.log(
  person1.constructor === Person, // true, 实例对象的constructor属性,指向构造函数Person
  person1 instanceof Object, // true , 所有对象均继承自Object
  person1 instanceof Person // true
);

上面代码可以看到已经可以判断实例对象的类型了。

判断实例对象的所属类型可以用两种方式:

  • 实例对象.constructor, 得到的是创建实例对象的构造函数
  • 实例对象 instanceof 构造函数(推荐)
导致的问题

每个方法都要在每个实例上重新创建一遍。

把构造函数中的函数移到函数外面,就能解决每个实例都要重新创建一个函数的问题。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  // 函数名只是一个指针,把指针copy一份赋给实例对象的sayName, 共享了在全局作用域中的同一个sayName函数
  this.sayName = sayName;
}

// 转移到了构造函数外部
function sayName() {
  console.log(this.name);
}

var person1 = new Person("Jack", "35", "Java Developer");
var person2 = new Person("Alice", "19", "actress");
person1.sayName(); // Jack
person2.sayName(); // Alice

但是,如果构造函数中的方法特别多,就会在全局作用域中创建特别多的函数,而且这些函数只能被某个对象调用,让全局作用域名不副实。

3. 原型模式

原型对象:包含特定类型的所有实例共享的属性和方法。使用原型的好处是所有实例可以共享原型中的属性和方法。

理解原型对象

只要创建了一个新函数,就会为该函数创建一个prototype属性,指向函数的原型对象。默认情况下,所有原型对象都会获得一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。

函数与原型对象的关系

函数、原型对象和实例的关系

函数的prototype属性,指向原型

原型的constructor属性,指向函数

实例对象的内部属性[[Prototype]],指向原型对象

自定义创建的构造函数,原型默认只有constructor属性;其他方法则是从Object继承来的

function Person() {}
console.log(Person.prototype);
// constructor: ƒ Person(name, age, job)
// [[Prototype]]: Object

打印原型对象,可以看到只有constructor属性,和继承自Object的属性和方法

function Person() {}

var p1 = new Person();

console.log(p1); // [[Prototype]]: Object

实例对象p1只有一个[[Prototype]]属性,指向原型对象

原型.isPrototypeOf(object): 判断调用者是否是object的原型

Object.getPrototypeOf(object) : 获取object的原型

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}

var person1 = new Person();

console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true

读取一个对象的属性时,先在实例中搜索,再到原型中搜索。同名属性,实例中的属性会屏蔽掉原型中的属性。

原型与in操作

单独使用in时,格式为 属性名 in 对象

判断对象是否能够访问到属性:实例中和原型中。

function Person() {}

Person.prototype.name = "Jack";
Person.prototype.age = 28;

var person1 = new Person();
person1.job = "Java Developer";

for (const key in person1) {
  // 原型上的属性 和 实例属性都被遍历了
  console.log(key); // job name age
}
更简单的原型语法

以对象字面量的形式设置原型对象,会导致原型的constructor不再指向Person,而是指向Object

function Person() {}

// 设置以对象字面量的形式
Person.prototype = {
  name: "Jack",
  age: 29,
  job: "Java developer",
  sayName: function () {
    console.log(this.name);
  },
};

var friend = new Person();

// 原型的constructor不再指向Person, 而是指向Object
console.log(Person.prototype.constructor === Object);  // true
原型的动态性

如果是在替换整个原型对象之前实例化了对象,那么这个对象的内部对象[[Prototype]]就指向了原来的原型。

function Person() {}
var p1 = new Person();

Person.prototype = {
  constructor: Person,
  name: "Jack",
  age: 28,
  job: "Developer",
};

var p2 = new Person()

// 实例对象上没有age属性,去原型对象找;创建时实例对象的[[Prototype]]指向的老的原型对象,上面只有一个constructor属性
console.log(p1.age); // undefined, 
// 创建时实例对象的[[Prototype]]指向的新的原型对象,上面除了有一个constructor属性,还有name、age、job属性
console.log(p2.age); // 28
原生对象的原型
原型对象的问题
function Person() {}

Person.prototype = {
  name: "Jack",
  age: 28,
  job: "Developer",
  color: ["red", "green"],
};

Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person,
});

var p1 = new Person();
var p2 = new Person();

p1.age = 30;
p1.color.push("blue");

console.log(
  p1.age, // 30 生效了
  p2.age, // 28, 值类型,没影响别的实例属性
  p1.color, // ['red', 'green', 'blue']
  p2.color // ['red', 'green', 'blue'], 引用类型值,影响了别的实例属性
);

给一个只在原型上的属性设置值,如果是值类型,相当于直接在实例上添加一个同名属性,可以隐藏原型中对应属性。因此,直接修改实例中的age属性,不会影响另一个实例中的age属性。

但是如果修改的是原型上的一个引用类型的值,实例中并不会新建一个同名的属性,而是属性直接引用到原型的引用类型值上;这样别的实例对象访问这个值时,原型中的值已经是被修改过的。

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

构造函数模式:用于定义实例属性

原型模式:用于定义共享属性和方法

function Person(name, age, job) {
  // 构造函数中定义实例属性,每个实例对象都有一份实例属性的副本
  this.name = name;
  this.age = age;
  this.job = job;
  this.color = ["red", "green"];
}

// 原型中只定义共享属性constructor和方法,节约内存
Person.prototype = {
  sayName: function () {
    console.log(this.name);
  },
};

Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person,
});

var p1 = new Person("Jack", 25, "Java Developer");
var p2 = new Person("Rose", 19, "English Teacher");

p1.sayName(); // Jack
p2.sayName(); // Rose

p1.color.push("blue", "yellow");

console.log(
  p1.color, // ['red', 'green', 'blue', 'yellow']
  p2.color, // ['red', 'green']
  p1.color === p2.color, // false, 在各自的实例中,不是同一个引用值
  p1.sayName === p2.sayName // true 方法是同一个方法,在原型中
);

这是应用最广泛、认同度最高的一种创建自定义类型的方法。定义引用类型的一种默认模式。

5. 动态原型模式

为了解决有独立的构造函数和原型的情况。产生了动态原型模式。

动态原型模式把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要时),又保持了同时使用构造函数和原型的优点。

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

function Person(name, age, job) {
  // 构造函数中定义实例属性,每个实例对象都有一份实例属性的副本
  this.name = name;
  this.age = age;
  this.job = job;

  // 如果还没有sayName方法,就在原型对象上添加共享属性和方法
  if (typeof this.sayName !== "function") {
    // 只要在原型对象上添加过一次该方法,后面都会存在在原型对象上;
    // 不管新建多少个实例,这里只会执行一次
    console.log('执行了');
    
    Person.prototype.sayName = function () {
      console.log(this.name);
    };
  }
}

var p1 = new Person("Jack", 25, "Java Developer");
var p2 = new Person("Rose", 19, "English Teacher");

p1.sayName() // Jack
p2.sayName() // Rose

只要在原型对象上添加过一次该方法,后面都会存在在原型对象上;

不管新建多少个实例,这里只会执行一次

6. 寄生构造模式

工厂模式+构造函数

// 工厂模式
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 p1 = new Person("Jack", 25, "Developer");
p1.sayName(); // Jack

可以使用其他模式的情况下不要用

7. 稳妥构造函数模式

稳妥构造与寄生构造函数类似,但有两点不同:

  • 新创建的实例方法不引用this;
  • 不使用new 操作符调用构造函数
// 工厂模式
function Person(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function () {
    console.log(name);
  };
  return o;
}

var p1 = Person("Jack", 25, "Developer");
p1.sayName(); // Jack

sayName中没有this;创建对象没有用new

除了调用sayName()外,没有别的方式可以访问其数据成员。

继承

1. 原型链

原型链继承的主要思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。

构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,实例都包含一个指向原型对象的内部指针。

原型链:让原型对象等于另一个类型的实例。

原型对象将包含一个指向另一个原型的指针[[Prototype]],另一个原型也包含着指向另一个构造函数的指针constructor。假如另一个原型又是另一个类型的实例,层层递进,就构成了实例与原型的链条。

function SuperType() {
  // 实例属性
  this.property = true;
}

// 原型上的方法
SuperType.prototype.getSuperValue = function () {
  return this.property;
};

function SubType() {
  this.subProperty = false;
}

// 父类的一个实例,赋给子类的原型对象
SubType.prototype = new SuperType();

// 子类原型上添加一个方法
SubType.prototype.getSubValue = function () {
  return this.subProperty;
};

// 创建一个子类的实例
var instance = new SubType();

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

新原型是父类的一个实例,新原型拥有父类实例的全部属性和方法,而且该原型还有个内部指针,指向父类原型对象(实例的[[Prototype]]指向原型对象)

instance的[[Prototype]]指向SubType的原型,SubType的原型的[[Prototype]]指向SuperType的原型。

1) 别忘记默认的原型

所有函数的默认原型都是Object的实例,默认原型都会包含一个内部指针,指向Object.prototype。

function fn() {}

console.log(fn.prototype);

随意定义了一个fn函数,打印它的原型对象

{
  constructor: f fn()
  [[Prototype]]: Object {
    constructor: ƒ Object()
    hasOwnProperty: ƒ hasOwnProperty()
    isPrototypeOf: ƒ isPrototypeOf()
    propertyIsEnumerable: ƒ propertyIsEnumerable()
    toLocaleString: ƒ toLocaleString()
    toString: ƒ toString()
    valueOf: ƒ valueOf()
  }
}

原型对象包含两个属性:constructor和内部属性[[Prototype]]

constructor是默认原型对象都有的一个属性,默认指向拥有这个原型对象的构造函数,这里是fn函数

[[Prototype]]内部属性,指向原型对象。函数的原型对象,默认是Object的一个实例,那么函数的原型对象的[[Prototype]]属性指向的就是Object的原型对象。

function fn() {}

// 所有函数的默认原型都是Object函数的实例(函数原型、Object函数的关系)
console.log(fn.prototype instanceof Object); // true

// Object的原型对象,是函数的默认原型对象的原型(函数原型、Object的原型的关系)
console.log(Object.prototype.isPrototypeOf(fn.prototype)); // true

完整的原型链

2) 确定原型和实例的关系

instanceof:实例和原型链中出现过的构造函数的关系。

isPrototypeOf():原型链中出现的原型,和原型链派生的实例的关系。

instanceof是判断实例是否属于构造函数,且只要是在原型链中出现过的构造函数都返回true。

isPrototypeOf()是判断原型对象是否为实例对象的原型,且只要是在原型链中出现过的原型都返回true

function SuperType() {
  // 实例属性
  this.property = true;
}

// 原型上的方法
SuperType.prototype.getSuperValue = function () {
  return this.property;
};

function SubType() {
  this.subProperty = false;
}

// 父类的一个实例,赋给子类的原型对象
SubType.prototype = new SuperType();

// 子类原型上添加一个方法
SubType.prototype.getSubValue = function () {
  return this.subProperty;
};

// 创建一个子类的实例
var instance = new SubType();

console.log(
  instance instanceof SubType, // true
  instance instanceof SuperType, // true
  instance instanceof Object, // true
  SubType.prototype.isPrototypeOf(instance), // true
  SuperType.prototype.isPrototypeOf(instance), // true
  Object.prototype.isPrototypeOf(instance), // true
);

一定要判断是否在原型链中出现构造函数和原型对象

3)谨慎地定义方法

给原型添加方法的代码一定要放在替换原型的语句之后。

通过原型链继承,不能使用对象的字面量。

function SuperType() {
  // 实例属性
  this.property = true;
}

// 原型上的方法
SuperType.prototype.getSuperValue = function () {
  return this.property;
};

function SubType() {
  this.subProperty = false;
}

// 父类的一个实例,赋给子类的原型对象
SubType.prototype = new SuperType();

// 子类原型上添加一个方法
SubType.prototype.getSubValue = function () {
  return this.subProperty;
};

// 子类的原型对象中添加与父类原型中同名函数,会覆盖父类原型中的同名函数
SubType.prototype.getSuperValue = function () {
  return false;
};

// 创建一个子类的实例
var instance = new SubType();
// 创建一个父类的实例
var superInstance = new SuperType();

console.log(
  instance.getSuperValue(), // false
  superInstance.getSuperValue() // true
);

子类的原型对象中添加与父类原型中同名函数,会覆盖父类原型中的同名函数

4)原型链的问题
  1. 父类构造函数包含引用值如数组,它所有实例都会共享这个引用值;又因为它的一个实例被赋值给了子类的原型,而原型上的所有属性和方法都会被子类所有实例共享,就导致子类所有实例都共享了父类构造函数中的引用值;
  2. 向父类的构造函数传参,每个参数都会被其实例初始化(使用),而这个实例又被用于子类原型,原型上的属性又会被实例拥有,从而影响到了子类实例。
function SuperType() {
  // 实例属性
  this.colors = ["red", "green"];
}

function SubType() {}

// 把父类实例赋值给子类原型
SubType.prototype = new SuperType();

var instance1 = new SubType();
var instance2 = new SubType();

instance1.colors.push("yellow");
console.log(instance2.colors); //  ['red', 'green', 'yellow']

父类实例给了子类原型后,父类实例所拥有的属性,子类所有实例对象都能通过原型对象获取到,所以子类实例可以访问到父类实例中的colors引用值

2. 借用构造函数

为了解决原型中包含引用类型值带来的问题,有了借用构造函数(constructor stealing),又叫伪造对象或经典继承。

借用构造函数的基本思想:在子类型构造函数的内部调用超类型构造函数。

函数只是在特定环境中执行代码的对象,用apply或者call方法可以在新创建对象上执行构造函数。

function SuperType() {
  // 实例属性
  this.colors = ["red", "green"];
}

function SubType() {
  // 在new关键字新建实例时,执行父类构造函数,通过传this——子类实例 给父类函数
  // 其实是给每个子类实例对象添加了一个colors属性。
  // 与之前在父类构造函数中添加属性不同的是,这是给每个子类实例添加了colors副本,
  // 而之前是实例对象作为原型,导致每一个子类实例都共享一个引用值
  SuperType.call(this);
}

var instance1 = new SubType();
var instance2 = new SubType();

instance1.colors.push("pink");

console.log(
  instance1.colors, // ["red", "green", "pink"]
  instance2.colors // ["red", "green"]
);

仅仅只是借用构造函数,就会有构造函数模式存在的问题:

方法都在构造函数中定义,就没有函数复用了;而且定义在超类原型中的方法,子类也是不可见的。

3. 组合继承

组合继承,combination inheritance,也叫伪经典继承,将原型链和借用构造函数组合到一起。

  • 原型链:实现对原型属性和方法的继承;
  • 借用构造函数:实现对实例属性的继承。

既通过在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性。

function SuperType(name) {
  // 实例属性
  this.name = name;
  this.colors = ["red", "green"];
}

// 给基类原型上添加一个方法
SuperType.prototype.sayName = function () {
  console.log(this.name);
};

function SubType(name, age) {
  // 调用基类构造函数,给子类实例添加name属性,并把传入的值给到name属性
  // 同时给子类实例添加colors属性,为每个实例都添加了一个colors副本
  SuperType.call(this, name);
  // 又给子类实例添加了age属性(基类实例中没有age属性)
  this.age = age;
}

// 把基类的一个实例赋给子类原型:子类每个实例都拥有基类实例的属性和方法,name、colors、sayName
// name、colors属性在子类实例上已经存在了,并不会访问到子类原型上的name、colors属性
SubType.prototype = new SuperType();
// 原本是基类SuperType, 改成了子类SubType
SubType.prototype.constructor = SubType;
// 给子类原型添加方法
SubType.prototype.sayAge = function () {
  console.log(this.age);
};

var instance1 = new SubType("Jack", 30);
var instance2 = new SubType("Rose", 25);

instance1.colors.push("yellow");

console.log(
  instance1.colors, // ["red", "green", "yellow"]
  instance2.colors // ["red", "green"]
);

// sayName在子类原型上;子类实例有自己的name属性
instance1.sayName(); // "Jack"
instance2.sayName(); // "Rose"
instance1.sayAge(); // 30
instance2.sayAge(); // 25

console.log(SubType.prototype.constructor === SubType); // true
console.log(SuperType.prototype.constructor === SuperType); // true

在子类构造函数中调用基本,在创建子类实例时,会给子类实例添加name和colors属性;而把基类实例赋值给子类原型时,相当于基类实例所有的属性和方法都在子类原型中,但是查找属性的顺序是实例->原型->上一级原型,所以在实例上的name和age属性都会优先与原型上的name和age属性。

从上图可以看出

  • 子类原型的name和colors属性被实例中的name和colors属性屏蔽
  • 子类原型的constructor属性指向子类构造函数,只是在子类原型中用同名属性屏蔽了基类构造函数中的constructor属性

4. 原型式继承

借助原型可以基于已有的对象创建新对象,还不必因此创建自定义类型。

先创建一个临时性构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个原型。

function object(o) {
  // 1. 创建一个临时构造函数
  function F() {}
  // 2. 用传进来的对象,作为临时构造函数的原型
  F.prototype = o;
  // 3. 返回一个临时构造函数的实例
  return new F();
}

var person = {
  name: "Jack",
  friends: ["Shelby", "Court", "van"],
};

var p1 = object(person);
var p2 = object(person);

// 自己的实例属性name
p1.name = "Rose";
// 操作原型上的引用值friends
p1.friends.push("Rob");

// 自己的实例属性name
p2.name = "Cindy";
// 操作原型上的引用值friends
p2.friends.push("Barbie");

console.log(
  p1.name, // Rose
  p2.name, // Cindy
  p1.friends, // ["Shelby", "Court", "van", "Rob", "Barbie"]
  p2.friends // ["Shelby", "Court", "van", "Rob", "Barbie"]
);

Object.create()方法规范化了原型式继承,参数1是新对象的原型对象,参数2是为新对象定义额外属性的对象。

var person = {
  name: "Jack",
  friends: ["Shelby", "Court", "van"],
};

// 把person作为原型,基于person对象,创建实例对象
var p1 = Object.create(person);
var p2 = Object.create(person);

// 自己的实例属性name
p1.name = "Rose";
// 操作原型上的引用值friends
p1.friends.push("Rob");

// 自己的实例属性name
p2.name = "Cindy";
// 操作原型上的引用值friends
p2.friends.push("Barbie");

console.log(
  p1.name, // Rose
  p2.name, // Cindy
  p1.friends, // ["Shelby", "Court", "van", "Rob", "Barbie"]
  p2.friends // ["Shelby", "Court", "van", "Rob", "Barbie"]
);

Object.create()就相当于创建一个临时构造函数,再接收一个对象作为这个临时构造函数的原型,最后返回一个临时构造函数的实例。

有第二个属性,指定的任何属性都会覆盖原型对象上的同名属性。

var person = {
  name: "Jack",
  friends: ["Shelby", "Court", "van"],
};

// 把person作为原型,基于person对象,创建实例对象
var p1 = Object.create(person, {
  name: {
    value: "Rose",
  },
  friends: {
    value: ["Rachel", "Monica"],
  },
});

var p2 = Object.create(person);

console.log(
  p1.name, // Rose
  p1.friends, // ["Rachel", "Monica"]
  p2.friends // ["Rachel", "Monica"]
);

p1在实例对象上新建了一个与原型同名的属性friends,它指向的是新的堆地址,不是原型上那个friends。所以p2还是原来的数组,没有受到影响

5. 寄生式继承

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

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

function createAnother(original) {
  // 创建基于原型的实例
  var clone = object(original);
  // 给实例添加了一个方法
  clone.sayHi = function () {
    console.log("hi");
  };
  // 返回这个实例
  return clone;
}

var person = {
  name: "Jack",
  friends: ["Shelby", "Court", "van"],
};

var p1 = createAnother(person);

p1.sayHi() // hi

6. 寄生组合式继承

前面的组合式继承会调用两次超类型构造函数:一次是创建子类型原型,一次是在子类型构造函数内部。

寄生组合式继承,通过借用构造函数来继承属性,通过原型链混成形式来继承方法。使用寄生式继承超类型的原型,再将结果指定给子类型的原型。

// 寄生式继承:让子类寄生于父类
function inheritPrototype(subType, superType) {
  // 基于一个对象作为原型,生成一个新的实例
  var o = Object.create(superType.prototype);
  // 增强
  o.constructor = subType;
  // 将新的实例赋值给子类原型
  subType.prototype = o;
}

function SuperType(name) {
  this.name = name;
  this.color = ["red", "blue"];
}

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);
};

var instance1 = new SubType("Jack", 30);
var instance2 = new SubType("Rose", 25);

instance1.color.push("pink");
console.log(instance1.color); // ['red', 'blue', 'pink']
console.log(instance2.color); // ['red', 'blue']

console.log(instance1);

如果要考虑Object.create()的兼容性,可以替换为原型式继承

// 原型式继承
function object(o) {
  // 1. 创建一个临时构造函数
  function F() {}
  // 2. 用传进来的对象,作为临时构造函数的原型
  F.prototype = o;
  // 3. 返回一个临时构造函数的实例
  return new F();
}

// 寄生式继承:让子类寄生于父类
function inheritPrototype(subType, superType) {
  // 基于一个对象作为原型,生成一个新的实例
  // var o = Object.create(superType.prototype);
  var o = object(superType.prototype);
  // 增强
  o.constructor = subType;
  // 将新的实例赋值给子类原型
  subType.prototype = o;
}

解决了组合继承调用两次超类构造函数的缺点,只在子类调用了一次超类构造函数,目的是继承超类的属性,是子类实例自己的属性。

子类型的原型是父类原型的一个副本,且有增强,目的是继承父类原型上的方法。比起组合继承,不用在子类原型中多添加父类的构造函数中的属性。