# 面向对象-封装和继承

# 封装

# 工厂模式

工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。比如下面的例子:

// createPerson.js
function createPerson(name, age, job) {
  let o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function () {
    console.log(this.name);
  };
  return o;
}
let person = createPerson("gausszhou", 18, "software enginer");

# 构造函数模式

ECMAScript 中的构造函数是用于创建特定类型对象的。

// Person.js
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function () {
    console.log(this.name);
  };
}

实际上,Person 内部的代码和 createPerson 基本上是一样的,只是有如下区别:

  • 没有显示的创建对象
  • 属性和方法直接赋值给 this
  • 没有 return

首字母大写

按照惯例,构造函数名称的首字母都是要大写的

let person = new Person("GaussZhou", NaN, "Software Engineer");

要创建 Person 的实例,应使用 new 操作符,以这种方式调用构造函数会执行如下操作:

  • 在内存中创建一个新对象
  • 这个新对象内部的[[ProtoType]]特性被赋值为构造函数的prototype属性
  • 构造函数内部的 this 被赋值为这个新对象
  • 执行构造内部的代码
  • 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

// EmuNew.js
function EmuNew (name){
  let obj = new Object() // 1
  Object.setPrototypeOf(obj, EmuNew.prototype); // 2
  // object.__proto__ = EmuNew.prototype; // 不推荐
  // let obj = Object.create(EmuNew.prototype); // 两步合并
  this = obj // 3
  this.name = name // 4
  return this // 5
}

骚操作

如果不想传递参数,那么构造函数后面的括号可以不加

let person1 = new Person;
let person2 = new Person;

构造函数的不足

构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。当我们创建大量同类的实例对象的时候,就会非常占据内存空间。

# 原型模式

针对单纯的构造函数的不足,我们引入原型模式。

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上这个对象就是通过调用构造函数传创建的对象的原型。

使用原型对象的好处是,在它上面 dinginess 的属性和方法可以被实例共享。

// Person.prototyoe.js
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}
Person.prototype.sayName = function () {
  console.log(this.name);
};
let alice = new Person("alice", 28, "teacher");
let bob = new Person("bob", 18, "student");
alice.sayName(); // alice
bob.sayName(); // bob
console.log(alice == bob); // false
console.log(alice.sayName == bob.sayName); // true

# 属性枚举

  • for-in 循环

  • Object.keys()

    枚举顺序是不确定的,取决于 JavaScript 引擎。

  • Object.getOwnPropertyNames()

  • Object.getOwnPropertySymbols()

    的枚举顺序是确定的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。

# 对象迭代

ECMAScript 2017 新增两个静态方法,用于将对象内容转换为序列化的可迭代格式。

  • Object.values() 返回对象值的数组。
  • Object.entries() 返回键值对的数组。

# 继承

继承是面向对象编程中讨论最多的话题。很多面向对象的语言都支持两种继承:接口继承和实现继承。

实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

实现原型链涉及如下代码模式:

// SuperType.prototype.js

function SuperType() {
  this.property = "super";
}
SuperType.prototype.getSuperValue = function () {
  return this.property;
};
function SubType() {
  this.subProperty = "sub"; // 属性不要重名
}
// 继承
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function () {
  return this.subProperty;
};

let instance = new SubType();

console.log(instance.getSuperValue()); // super
console.log(instance.getSubValue()); // sub

原型链的问题

原型链虽然是实现继承的强大工具,但它也有问题,主要问题出现在原型中包含引用值的时候,原型中包含的引用值会在所有实例共享,而由于是引用值,原型实际上变成了另一个类型实例。

function SuperType() {
  this.colors = ["red", "green", "blue"];
}
function SubType() {}
SubType.prototype = new SuperType();
let instance1 = new SubType();
let instance2 = new SubType();
instance1.colors.push("black");
console.log(instance2.colors); // [ 'red', 'green', 'blue', 'black' ]

# 盗用构造函数

为了解决原型包含引用值导致的继承问题,一种叫做“盗用构造函数”的技术开始流行起来。

基本思路很简单:在子类构造函数中调用父类构造函数。

因为函数就是在特定上下文中执行代码的简单对象,所以可以使用 applay 和 call 方法以新创建的对象为上下文执行父类构造函数

function SuperType() {
  this.colors = ["red", "green", "blue"];
}
function SubType() {
  SuperType.call(this);
}
let instance1 = new SubType();
let instance2 = new SubType();
instance1.colors.push("black");
console.log(instance2.colors); // [ 'red', 'green', 'blue' ]

盗用构造函数的问题

盗用构造函数的主要缺点也是构造函数模式的问题:必须在构造函数中定义方法,因此方法不能被外界重用,此外子类也不能访问父类原型上的方法。

# 组合继承

组合继承综合了原型链和盗用构造函数,将两个的优点集中了起来。

基本的思路是:使用原型链继承原型上的属性和方法,通过盗用构造函数来继承实例属性。

// SuperType.compose.js
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "green", "blue"];
}

SuperType.prototype.sayName = function () {
  console.log(this.name);
};

function SubType(name, age) {
  // 继承实例属性
  SuperType.call(this, name);
  this.age = age;
}

// 继承原型方法
SubType.prototype = new SuperType();

// 此时 实例上 name colors age 原型上有 name colors 以及 原型的原型上有sayName方法
// 但是 实例上已经有的属性,不会再去原型上查找,所以不用担心

// 编写子类原型上的方法
SubType.prototype.sayAge = function () {
  console.log(this.age);
};

// 测试

let alice = new SubType("alice", 18);
let bob = new SubType("bob", 80);

alice.sayName(); // alice
bob.sayName(); // bob
alice.sayAge(); // 18
bob.sayAge(); // 80
alice.colors.push("black");
console.log(alice.colors); // [ 'red', 'green', 'blue', 'black' ]
console.log(bob.colors); // [ 'red', 'green', 'blue' ]

# 原型式继承

2006 年,Douglas Crockford 写了一篇文章《JavaScript 中的原型式继承》。这篇文章介绍了一种不涉及严格意义上的构造函数的继承方法,适用于在一个已有的对象上创建一个新的对象

// object.prototype.js

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

let person = {
  name: "gauss",
  friends: ["alice", "bob", "zhangsan"]
};

let person1 = object(person);
person1.name = "zhou";
person1.friends.push("lisi");

let person2 = object(person);
person2.name = "zhao";
person2.friends.push("wangwu");

console.log(person1.name); // zhou
console.log(person2.name); // zhao
console.log(person1.friends); // [ 'alice', 'bob', 'zhangsan', 'lisi', 'wangwu' ]
console.log(person2.friends); // [ 'alice', 'bob', 'zhangsan', 'lisi', 'wangwu' ]
person1.friends = [];
console.log(person1.friends); // []
console.log(person2.friends); // [ 'alice', 'bob', 'zhangsan', 'lisi', 'wangwu' ]

呃,怎么说呢,我感觉怪怪的,这不就是多个对象指向同一个原型对象嘛,直接赋值就取实例的,取引用就用公共的。getset 的不一致,很让人迷惑。

# 寄生式继承

寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

// object.parasitic.js

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

function createAnother(o) {
  let clone = object(o);
  clone.sayHi = function () {
    console.log("hi");
  };
  return clone;
}

let person = {
  name: "gauss",
  friends: ["alice", "bob", "zhangsan"]
};

let anotherPerson = createAnother(person);
console.log(anotherPerson.name); // gauss from proto
anotherPerson.name = "zhou";
console.log(anotherPerson.name); // zhou from instance
anotherPerson.sayHi(); // hi

寄生式继承的问题

老毛病了,和构造函数模式一样,函数必须在工厂函数内定义,难以被外界重用

# 寄生式组合继承

组合继承的效率问题在于父类构造函数始终会被调用两次:一次是创建子类的原型时,一次时调用子类构造函数生成实例时。

寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调 用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。

说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

// SuperType.parasitic.js
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

function inheritPrototype(subType, superType) {
  let proto = object(superType.prototype);
  proto.constructor = subType;
  subType.prototype = proto;
  // 将子类的原型的原型指向父类的原型
  // 并重写 constructor
  // 等价写法
  // subType.prototype = Object.create(superType.prototype)
  // subType.prototype.constructor = subType
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "green", "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);
};

TIP

本质就是通过这个中间的空的新对象来实现原型链继承。

# 寄生式组合继承的模板

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "green", "blue"];
}
SuperType.prototype.sayName = function () {};
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = subType;
SubType.prototype.sayAge = function () {};