类和类型


JavaScript 定义了少量的数据类型:nullundefined、布尔值、数字、字符串、函数和对象,我们可以通过 typeof 计算值的类型,我们也可以把类作为类型来对待,这样就可以根据对象所属的类来区分它们。本节就来讨论检测对象所属类的技术。

instanceof 运算符

前面讲运算符时已经讨论过该运算符,左操作数是待检测对象,右操作数是类的构造函数。如果 o 继承自 c.prototype,那么 o instanceof c 的值就是 true,这里的继承可以不是直接继承。

需要注意的是,正如之前所言,构造函数是类的公共标识,原型是类的唯一标识,instanceof 实际上检测的是对象的继承关系,而不是检测对象的构造函数。类似的检测还有 isPrototypeOf() 方法:

c.prototype.isPrototypeOf(r);

instanceof 运算符和 isPrototypeOf() 方法有一个共同的缺点,就是只能检测从属关系,无法通过对象获取类名。此外在客户端 JavaScript 中,多窗口和多框架子页面具有不同的上下文环境,每个上下文环境都包含独有的全局变量和构造函数,在两个不同框架页面中创建的数组继承自相同但相互独立的原型对象,其中一个框架页面创建的数组不是另一个框架页面 Array 构造函数的实例,instanceof 的计算结果也是 false

constructor 属性

另一个识别对象是否属于某个类的方法是使用 constructor 属性:

这种方式通过对象实例虽然可以获取类名,但是在客户端多窗口或多框架子页面 JavaScript 应用中面临的问题和 instanceof 一样。

此外,在 JavaScript 中并非所有对象都包含 constructor 属性,比如前面自定义的原型对象就没有这个属性。

构造函数的名称

使用 instanceof 运算符和 constructor 属性的主要问题都是在多个执行上下文存中在构造函数的多个副本时,虽然构造函数看起来一模一样,但是它们是相互独立的对象,因此彼此并不相等。

对此,一种可能的解决方案是使用构造函数的名字而不是构造函数本身作为类标识符。不同窗口的 Array 构造函数不相等,但是它们的名字是一样的。一些 JavaScript 的实现为函数对象提供了非标准的 name 属性,用来表示函数的名称,对于那些没有 name 属性的函数,可以将函数转化为字符串再提取函数名。下面,我们根据这个思路来编写判断判断值的类型的 type() 函数:

/**
 * 以字符串的形式返回对象 o 的类型
 * @param o
 * @returns {string}
 */
function type(o) {
  var t, c, n; // type, class, name

  if (o === null)
    return "null";

  if (o !== o)
    return "NaN";

  // 类型不是对象,直接返回
  if ((t = typeof o) !== "object")
    return t;

  // 内置对象,直接返回类名
  if ((c = classof(o)) !== 'Object')
    return c;

  if (o.constructor && typeof o.constructor === "function" && (n = o.constructor.getName()))
    return n;

  return "Object";
}

// 返回对象的类
function classof(o) {
  return Object.prototype.toString.call(o).slice(8, -1);
}

// 返回函数的名字
Function.prototype.getName = function () {
  if ("name" in this) 
    return this.name;
  return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
};

这种通过函数名来识别对象的类的方式和 constructor 属性有个一样的问题:并不是所有函数都有 constructor 属性,此外,不是所有的函数都有名字。下面是对上面代码的测试:

鸭式辩型

上述三种检测对象所属类的技术都或多或少存在一些问题,尤其是在客户端 JavaScript 中,解决这些问题的终极办法就是规避这些问题:不要关注「对象的类是什么」,而是关注「对象能做什么」。这种思考问题的方式在 Python 和 Ruby 中都非常常见,称为「鸭式辩型」:

像鸭子一样走路、游泳并嘎嘎叫的鸟就是鸭子。

在 JavaScript 中,这句话可以理解为:如果一个对象可以像鸭子一样走路、游泳并且嘎嘎叫,就认为这个对象是鸭子对象,哪怕它不是从鸭子类的原型对象继承而来。

举两个例子,一个是前面定义的 Range 类,我们的初衷是使用这个类来描述数字的范围,但构造函数中并没有对传入参数类型做校验,所以我们可以这么调用:

var letters = new Range("a", "z");
var thisYear = new Range(new Date(2018, 0, 1), new Date(2019, 0, 1));

我们可以在这两个对象上调用 includestoString 方法,但是由于 foreach 方法中存在 Math.ceil 函数,所以会调用失败。

另外一个例子是数组中提到的类数组对象,我们可以在类数组对象上调用多个数组函数,但是类数组对象并不是真正的数组。

下面我们按照鸭式辩型的理念定义了 quacks() 函数,该函数用以检查一个对象(第一个参数)是否实现了剩下的参数所表示的方法。对于除第一个参数外的每个参数,如果是字符串的话则直接检查是否存在以它命名的方法;如果是对象的话则检查第一个对象中的方法是否在这个对象中也有同名方法;如果参数是函数则假定它是构造函数,函数将检查第一个对象实现的方法是否在构造函数的原型对象中也具有同名方法:

/**
 * 如果 o 实现了除第一个参数外(自己)的所有其他参数所表示的方法,返回true
 * @param o
 * @returns {boolean}
 */
function quacks(o /*,...*/) {
  for (var i = 1; i < arguments.length; i++) {
    var arg = arguments[i];
    switch (typeof arg) {
      case 'string':
        if (typeof o[arg] !== "function")
          return false;
        break;
      case 'function':
        arg = arg.prototype;
      case 'object':
        for (var m in arg) {
          if (typeof arg[m] !== "function")
            continue;
          if (typeof o[m] !== "function")
            return false;
        }
    }
  }
  return true;
}

关于 quacks 函数有一些需要注意的点。首先,该函数只是通过特定的名称来检测对象是否包含有一个或多个值为函数的属性,但我们无法获知这些属性的细节信息,比如函数的作用,传入的参数等,但这也是鸭式辩型的本质所在 —— 更具有灵活性。还有一个问题就是该函数不能用于内置类,比如不能通过 quacks(o, Array)了 检测 o 是否实现了 Array 中所有同名的方法,因为内置类的方法是不可枚举的,quacks 中的 for/in 循环无法遍历到它们。在 ECMAScript 5 中倒是有一个补救办法就是使用 Object.getOwnPropertyNames(Array)


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 类的扩展

>> 下一篇: JavaScript 中的面向对象技术