面向对象的 JavaScript
动态语言和鸭子类型
编程语言按照数据类型的大体分为两类:
- 静态类型语言
- 编译时就能发现类型不匹配的错误
- 编辑器友好
- 编译器可以针对性优化,性能好
- 在编写时,需要时刻关心类型,心智负担大,代码量大
- 动态类型语言
- 代码量少,逻辑简单
- 可读性好,专注于逻辑表达
- 无法保证变量的类型,可能发生类型相关错误
鸭子类型(duck typing):
动态类型语言的设计建立在鸭子类型上,即我们只关心对象行为,而不关心对象本身。“如果它走起路来像鸭子,叫起来也像鸭子,那么他就是鸭子”。
比如圣上需要建立一个内阁,需要 10 位学者来传达圣旨,可是王国找来找去也只有 9 位学者,那么如果有 1 只鸭子,也能够传达圣旨,那么就可以把它加入到我们的内阁里来。
let scholar = {
conveyOrders: function (arg) {
console.log('奉天承运,皇帝诏曰', arg);
},
study: function () {
console.log('科学研究');
},
type: 'scholar',
};
let duck = {
conveyOrders: function (arg) {
console.log('奉天承运,皇帝诏曰', arg);
},
duckCall: function () {
console.log('嘎嘎嘎!');
},
type: 'duck',
};
const government = []; // 内阁
const joinGovernment = (anything) => {
if (anything && typeof anything.conveyOrders === 'function') {
government.push(anything);
console.log(`欢迎${anything.type}加入内阁`);
}
};
joinGovernment(scholar); // 欢迎scholar加入内阁
joinGovernment(duck); // 欢迎duck加入内阁在动态类型语言的设计中,鸭子类型的概念至关重要,利用鸭子类型的思想,不必借助超类型的帮助,就能轻松地在动态类型语言中实现一个原则:面向接口变成,而非面向实现编程。
多态
多态的含义是:
同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象,发送同一个消息的时候,这些对象会根据这个消息分别给出不同的结果。
比如圣上发出了圣旨——全天下的动物都叫起来,那么狗会汪汪叫,而鸭子会嘎嘎叫。这就蕴含了多态的思想。
多态的思想是:
把“做什么”和“谁去做,怎样去做”区分开,也就是将“不变的事物”和“可能改变的事物”区分开。“不变的事物”隔离开,“变化的事物”封装起来,这就给予了我们拓展程序的能力,符合开放-封闭原则。
多态的作用是:
通过把过程化的条件分支,转化为对象的多态性,从而消除这些分支语句。
多态的优点是:
将行为分布在各个对象中,并让这些对象各自负责自己的行为,对象“做什么”和“怎样去做”,达到了解耦的目的,在未来的拓展中,我们只需要加入相应的对象即可,而不必修改之前的代码。
let makeSound = function (animal) {
animal.sound();
};
let Duck = function () {};
Duck.prototype.sound = function () {
console.log('嘎嘎嘎!');
};
let Dog = function () {};
Dog.prototype.sound = function () {
console.log('汪汪汪!');
};
makeSound(new Duck()); // 嘎嘎嘎!
makeSound(new Dog()); // 汪汪汪!静态类型语言的多态
某些时候,在享受静态类型语言检查带来的安全性的同时,我们亦会感觉到被束缚了手脚。比如在之前的代码中,Java 需要限制 makeSound 函数的传参类型,所以我们需要一个超类,来囊括 Duck 和 Dog。
所以静态类型的面向对象语言通常设计成可以向上转型:当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。
而在 Java 中,这就是继承。比如在以下代码中,我们定义了一个 Animal 抽象类,让 Dog 和 Duck 来继承此类,来实现多态。
public abstract class Animal {
abstract void makeSound();
}
public class Duck extends Animal {
public void makeSound() {
System.out.println('嘎嘎嘎!');
}
}
public class Dog extends Animal {
public void makeSound() {
System.out.println('汪汪汪!');
}
}
public class AnimalSound {
public void makeSound(Animal animal) {
animal.makeSound();
}
}
public class Test {
public static void main(String args[]) {
AnimalSound animal = new AnimalSound();
Animal duck = new Duck();
Animal dog = new Dog();
animalSound.makeSound(duck);
animalSound.makeSound(dog);
}
}JavaScript 的多态
要实现多态,在静态语言中,归根结底,我们需要消除类型之间的耦合关系,在 Java 中,是向上转型来实现多态。
JavaScript 的变量类型在运行期间是可变的,这意味着,JavaScript 对象的多态性是与生俱来的。一个对象能否被使用,只取决于它是否有相应的方法,而不取决于它是什么类型的对象,所以 JavaScript 并不需要注入向上转型之类的技术来实现多态。
在 JavaScript 中,函数本身也是对象,函数能够被用来封装行为并且被到处传递。当我们对函数发出“调用”的消息时,这些函数会返回不同的结果,这是多态性的一种体现。所以很多设计模式在 JavaScript 中能够使用高阶函数实现。
封装
封装的目的是隐藏信息,狭义的封装指的是封装数据和封装实现。而广义上的封装,还应该包括,封装类型和封装变化。
- 封装数据 在 Java 类似的语言中,封装数据是由词法分析实现的,比如 Java 提供了 public、private、protected 等关键字来提供不同的访问权限。而 JavaScript 没有提供这些关键字的支持,我们只能依赖变量的作用域来实现封装数据,模拟 public 和 private。比如使用闭包、或者 ES6 的 Symbol 创建私有属性。
- 封装实现 封装实现使得对象内部的变化对其他对象而言是不可见的,对象对自己的行为负责,其他的对象不关心它的内部实现。封装使得对象之间的耦合变得松散,对象之间通过暴露的 API 来进行通讯。
- 封装类型 封装类型是静态类型语言中一种重要的封装方式。一般而言,封装类型是通过抽象类和接口来实现的。把对象的真正类型隐藏在抽象类或者接口后。
- 封装变化 封装变化是从设计模式角度出发而言的。我们把系统中稳定不变的部分和容易变化的部分隔离分开,在系统演化的过程中,我们只需要替换掉那些容易变化的部分,如果这部分也是封装好的,那么替换也是轻而易举地。这最大程度保证了系统的稳定性和可拓展性。
继承 - 原型模式和基于原型继承的 JavaScript 对象系统
在以类为中心的面向对象编程语言中,类和对象的关系可以类比成铸模和铸件的关系,对象总是从类里创建而来。而在原型编程的思想中,类并不是必须的,一个对象是从另一个对象克隆而来。
原型模式不单是一种设计模式,也被称为一种设计泛型。
原型模式的实现关键,是语言本身是否提供了 Clone 的方法,ECMAScript5 提供了 Object.create 方法,可以用来克隆对象。
let Duck = function () {
this.sound = '嘎嘎嘎!';
this.age = '3 Month';
};
let duck = new Duck();
duck.sound = '嘎嘎咕!';
duck.age = '4 Month';
let cloneDuck = Object.create(duck);
console.log(duck.sound); // '嘎嘎咕!'
console.log(duck.age); // '4 Month'
console.log(cloneDuck.sound); // '嘎嘎咕!'
console.log(cloneDuck.age); // '4 Month'原型模式至少包含着以下基本规则:
-
所有的数据都是对象。 JavaScript 设计者的本意是除了 undefined 之外,一切都应该是对象,为了实现这一目标,number、boolean、string 也可以通过“包装类”的方法变成对象类型数据来进行处理。 JavaScript 中的根对象就是 Object.prototype ,我们在 JavaScript 中遇到的每个对象,实际上都由它拷贝而来。
-
要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。 JavaScript 中没有类的概念,使用 new 也只是调用了构造函数,用 new 创建对象的过程,实际上也只是先克隆 Object.prototype 然后再进行其他操作。
-
对象会记住它的原型。 JavaScript 给对象提供了一个名为 proto 的隐藏属性,某个对象的 proto 属性默认会指向它的构造函数的原型对象,即 {Constructor}.prototype 。
-
如果对象无法响应某个请求,它会吧这个请求委托给自己的原型。 当一个对象无法响应某个请求的时候,它会顺着原型链把请求传递下去,直到遇到一个可以处理该请求的对象为止。
let parent = { name: 'john', }; let Child = function () {}; Child.prototype = parent; let child = new Child(); console.log(child.name); // john
在当下的 JavaScript 引擎下,通过 Object.create 来创建对象的效率通常比通过构造函数创建对象慢。
ES6 带来了新的 Class 愈发,这让 JavaScript 的写法看起来像是一门拥有类的语言,但是其背后仍然是通过原型机制来创建对象。