When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.
如果一只鸟走起路来像鸭子,游泳像鸭子,叫起来也像鸭子,那它就可以叫做鸭子。
—— James Whitcomb Riley,1849 - 1916

本文主要包含以下内容:

  • 编程语言分类
  • 什么是鸭子类型
  • 为什么需要鸭子类型
  • JavaScript 中的鸭子类型

编程语言分类

要讲清楚什么是鸭子类型,首先我们得弄清楚编程语言的分类。

编程语言按照数据类型大体可以分为两类:

  • 静态类型语言
  • 动态类型语言

静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。

静态类型语言

静态类型语言的优点首先是在编译时就能发现类型不匹配的错误,编辑器可以帮助我们提前避免程序在运行期间有可能发生的一些错误。

其次,如果在程序中明确地规定了数据类型,编译器还可以针对这些信息对程序进行一些优化工作,提高程序执行速度。

静态类型语言的缺点首先是迫使程序员依照强契约来编写程序,为每个变量规定数据类型,归根结底只是辅助我们编写可靠性高程序的一种手段,而不是编写程序的目的,毕竟大部分人编写程序的目的是为了完成需求交付生产。

其次,类型的声明也会增加更多的代码,在程序编写过程中,这些细节会让程序员的精力从思考业务逻辑上分散开来。

常见的静态类型语言有:C++、Java、C#

image.png

动态类型语言

动态类型语言的优点是编写的代码数量更少,看起来也更加简洁,程序员可以把精力更多地放在业务逻辑上面。

虽然不区分类型在某些情况下会让程序变得难以理解,但整体而言,代码量越少,越专注于逻辑表达,对阅读程序是越有帮助的。

动态类型语言的缺点是无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误。这好像在商店买了一包牛肉辣条,但是要真正吃到嘴里才知道是不是牛肉味。

常见的动态语言有:JavaScript、Python、Ruby

image.png

JavaScript 中,当我们对一个变量赋值时,显然不需要考虑它的类型,因此,JavaScript 是一门典型的动态类型语言。

什么是鸭子类型

在明白了编程语言的分类后,接下来就来看一下什么是鸭子类型。

鸭子类型更多的是出现在动态语言当中,当然某些静态语言也存在,它指代的是一种对象推断风格。在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。

举个例子,判断一个对象是不是 X 类型,只要检查它是否具有 X 的特定属性或者方法,如果有,即可把它当成 X 类型的对象。

我们可以通过一个小故事来更深刻地了解鸭子类型:

从前在 JavaScript 王国里,有一个国王,他觉得世界上最美妙的声音就是鸭子的叫声,于是国王召集大臣,要组建一个 1000 只鸭子组成的合唱团。

大臣们找遍了全国,终于找到 999 只鸭子,但是始终还差一只,最后大臣发现有一只非常特别的鸡,它的叫声跟鸭子一模一样,于是这只鸡就成为了合唱团的最后一员。

image.png

这个故事告诉我们,国王要听的只是鸭子的叫声,这个声音的主人到底是鸡还是鸭并不重要。鸭子类型指导我们只关注对象的行为,而不关注对象本身。

可见,“鸭子类型”的特点就是描述的是事物的外部行为而非内部结构

支持“鸭子类型”的语言的解释器/编译器将会在解析或编译时,推断对象的类型。

这就给书写代码带来了很大的灵活性,特别是在动态类型的语言中,由于无需进行类型检测,我们可以尝试调用任何对象的任意方法,而无需去考虑它原本是否被设计为拥有该方法。

下面我们来使用代码模拟上面的场景:

  1. // 鸭子对象,具有 duckSing 方法
  2. var duck = {
  3. duckSing: function () {
  4. console.log('嘎嘎嘎');
  5. }
  6. }
  7. // 鸡对象,具有 duckSing 方法
  8. var chicken = {
  9. duckSing: function () {
  10. console.log('嘎嘎嘎');
  11. }
  12. }
  13. var choir = []; // 合唱团
  14. // 加入合唱团函数
  15. function joinChoir(animal) {
  16. // 只要加入进来的动物具有 duckSing 方法
  17. if (animal && typeof animal.duckSing === 'function') {
  18. choir.push(animal);
  19. console.log('恭喜加入合唱团');
  20. console.log(`当前合唱团成员为:${choir.length} 个成员`);
  21. }
  22. }
  23. joinChoir(duck);
  24. joinChoir(chicken);
  25. // 恭喜加入合唱团
  26. // 当前合唱团成员为:1 个成员
  27. // 恭喜加入合唱团
  28. // 当前合唱团成员为:2 个成员

在上面的代码中,我们看到大臣们根本无需检查加入合唱团的动物的类型,而是只需要保证它们拥有 duckSinging 方法。

如果下次期望加入合唱团的是一只小狗,而这只小狗刚好也会鸭子叫,那么这只小狗也能顺利加入。

为什么需要鸭子类型

在动态类型语言的面向对象设计中,鸭子类型的概念至关重要。

利用鸭子类型的思想,我们不必借助超类的帮助,就能轻松地在动态类型语言中实现一个原则:“面向接口编程,而不是面向实现编程”。

例如,一个对象若有 pushpop 方法,并且这些方法提供了正确的实现,它就可以被当作栈来使用。一个对象如果有 length 属性,也可以依照下标来存取属性(最好还要拥有 slicesplice 等方法),这个对象就可以被当作数组来使用。

在静态类型语言中,要实现“面向接口编程”并不是一件容易的事情,往往要通过抽象类或者接口等将对象进行向上转型。

当对象的真正类型被隐藏在它的超类型身后,这些对象才能在类型检查系统的“监视”之下互相被替换使用。只有当对象能够被互相替换使用,才能体现出对象多态性的价值。

“面向接口编程”是设计模式中最重要的思想,但在 JavaScript 语言中,“面向接口编程”的过程跟主流的静态类型语言不一样,因此,在 JavaScript 中实现设计模式的过程与在一些我们熟悉的语言中实现的过程会大相径庭。

JavaScript 中的鸭子类型

JavaScript 中,就存在不少鸭子类型,下面举几个典型例子。

ArrayLike 类数组对象

如果一个 JavaScript 对象, 他的元素下标是数字,length 也是数字,如字符串、 arguments 等,我们统称这种对象为类数组对象(Array-like Object)。

我们都知道,数组上有 map、reduce 等方法,有趣的是:这些方法并不是跟数组严格绑定的。

利用 JavaScript 鸭子类型特性,我们可以对数组原型方法以 call、apply 调用,使数组原型方法能处理这些数据:

  1. // 类数组对象
  2. const arrLike = {
  3. '0': 1,
  4. '1': 2,
  5. '2': 3,
  6. length: 3
  7. }
  8. console.log(Array.prototype.slice.call(arrLike))
  9. // [1, 2, 3]
  10. console.log(Array.prototype.map.call(arrLike, item => item + 1))
  11. // [2, 3, 4]
  12. console.log(Array.prototype.filter.call(arrLike, item => item !== 2))
  13. // [1, 3]
  14. console.log(Array.prototype.reduce.call(arrLike, (prev, curr) => prev + curr, 0))
  15. // 6
  16. console.log(Array.prototype.map.call('123', Number))
  17. // [1, 2, 3]
  18. // 因为 arrLike 元素下标是数字,length 也是数字
  19. // 所以它是一个类数组对象,根据鸭子类型规则,符合数组使用条件,所以就可以把数组的方法用在它上面
  20. // 类似的还有字符串也可以这么用
  21. var str = "Hello";
  22. console.log(Array.prototype.filter.call(str, item => item !== 'l'))
  23. // [ 'H', 'e', 'o' ]

Iterable 可迭代对象

如果一个对象或者他的原型上具有 Symbol.iterator 方法,那么这个对象就是可迭代对象。

例如:

  1. const iterable = {
  2. *[Symbol.iterator]() {
  3. yield 1;
  4. yield 2;
  5. yield 3;
  6. }
  7. };
  8. console.log([...iterable]); // [1, 2, 3]

在上面的代码中,Symbol.iterator 方法叫做迭代器方法。对象通过调用迭代器方法,就能实现拓展运算符 … 拓展或者 for…of 迭代,我们称这个对象实现了迭代协议。

ES6 中,像 Array、String、arguments、Set、Map、FormData 等类型之所以可以迭代,正是因为这些类型的构造函数的原型上具有自己的 Symbol.iterator 迭代器函数。

所以,能不能迭代,取决的有没有 Symbol.iterator 迭代器函数,那么这也是一个鸭子类型思想的体现。

例如,我们现在有这么一个对象,默认是不能迭代的:

  1. var obj = {
  2. name : "zhangsan",
  3. age : 18,
  4. gender : "male"
  5. }
  6. console.log([...obj]); // TypeError: obj is not iterable

那么我们只需要为该对象添加上 Symbol.iterator 迭代器函数即可,如下:

  1. var obj = {
  2. name: "zhangsan",
  3. age: 18,
  4. gender: "male",
  5. // 添加 Symbol.iterator 方法
  6. // 注意 Symbol.iterator 是一个生成器函数
  7. *[Symbol.iterator]() {
  8. for (var key in this) {
  9. var value = this[key]
  10. yield value;
  11. }
  12. }
  13. }
  14. console.log([...obj]); // [ 'zhangsan', 18, 'male' ]

总结

  1. 编程语言可以分为两大类,静态语言和动态语言。

  2. 鸭子类型是根据对象行为推导出来的类型,JavaScript 在处理对象时只会判断其对象行为,并不会真正检查他的确切类型。

  3. 在动态类型语言的面向对象设计中,鸭子类型的意义在于不必借助超类的帮助,就能轻松的实现面向接口编程。

  4. JavaScript 中,存在不少鸭子类型,例如判断 ArrayLike,只需检查对象中有 length 属性,并且 length 值为数字即可,判断 Iterable,只需要检查对象上 Symbol.iterator 属性值是否为一个函数即可。

-EOF-