# 面向对象-封装和继承
# 封装
# 工厂模式
工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。比如下面的例子:
// 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' ]
呃,怎么说呢,我感觉怪怪的,这不就是多个对象指向同一个原型对象嘛,直接赋值就取实例的,取引用就用公共的。get
和 set
的不一致,很让人迷惑。
# 寄生式继承
寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
// 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 () {};