在我们的日常开发中会时常遇到需要判断数据类型的引用场景,针对这个问题在《JavaScript 中数据类型的判断》一文中,我做过一个基本介绍。本文要介绍的内容这个是针对一个特殊的应用场景,JavaScript 中如何判断一个函数是否为构造函数的应用场景。那么,在 JavaScript 中构造函数有些什么特征,以及如何利用这些特征用来判断构造函数的身份呢?

构造函数的特征

构造函数,首先能想到的应该就是它是一个函数。更进一步的说,它是应该是一个函数对象

函数对象的原型(prototype)

在《理解 JavaScript 中的继承》一文中有提到:JavaScript 中的对象划分为函数对象普通对象两大类。而 JavaScript 中另一个非常重要的概念就是原型。普通对象的原型属性是 __proto__,而 prototype(原型) 则只存在于函数对象上。它是一个标准的属性,可以通过 obj.prototype 的方式访问。例如:

Function.prototype
Array.prototype
Object.prototype

通过示例代码可以发现,Function、Array 和 Object 都是函数,也都是构造函数。而需要再次强调的是:普通对象是没有 prototype 属性的:

const sum = (a, b) => {
  return a + b
}
sum.prototype // -> 'undefined'
[].prototype // -> 'undefined'
{}.prototype // -> 'undefined'

new 关键字

对于构造函数,另外能想到应该就是可以结合 new 关键字创建对象实例。例如:

const int8 = new Int8Array([1,2])
const fn = new Function() {}

普通函数(对象中的方法,内置的方法和箭头函数)是不能和 new 关键一起使用创建对象实例的。例如:

const sum = (a, b) => {
  return a + b
}

new sum() // -> 'TypeError: sum is not a constructor'

new alert() // -> 'TypeError: sum is not a constructor'

// BigInt() 只是一个普通函数
new BigInt(42) // -> 'TypeError: sum is not a constructor'

constructor 属性

JavaScript 中所有的对象都有一个 constructor 属性指向它的构造函数。当然我们通过 new 关键字关键的实例,它的 constructor 属性也”应该“是指向它的构造函数。

const Person = function(name, age) {
  this.name = name
  this.age = age
} 

const robert = new Person('Robert', 25)

robert.constructor === Person // -> true
robert instanceof fn // => true

之所以说“应该”,是因为如果编写构造函数的姿势不正确,实例的 constructor 属性可能会指向 Object。例如下面这段代码:

const Person = function(name, age) {
  this.name = name
  this.age = age
} 

Person.prototype = {
   doJob() {
     console.log("do my job")  
   } 
}

let robert = new Person('robert', 25)

robert.constructor === Person // -> false
robert.constructor === Object // -> true

之所以会出现示例代码中的问题,是因为开发者将 Person.prototype 原型指向了一个对象字面量,而对象字面量的 constructor 就是 Object 了。示例代码的写法打破了原型链,需要手动指定 constructor 属性到 Person,就可以恢复原型链了。由于本文不是介绍 JavaScript 原型链的,所以就不再多述原型链的内容了。

const Person = function(name, age) {
  this.name = name
  this.age = age
} 

Person.prototype = {
   // 显示的指定 constructor 属性为 Person
   constructor: Person, 
   doJob() {
     console.log("do my job")  
   } 
}

let robert = new Person('robert', 25)

robert.constructor === Person // -> true

总之,我们了解到以下几个重要的构造函数特征:

  • 构造函数是函数;
  • 构造函数是函数对象,函数对象有 prototype 原型;
  • 构造函数可以通过 new 关键创建的实例;
  • 实例(对象)的 constructor 应该是它的构造函数或者 Object(至于胡乱指定 constructor 属性的情况,我们只能表示遗憾!);

到此为止,我们已经将构造函数的特征基本都列举出来了。可以开始动手编写 isConstructor() 方法了。

isConstructor() 方法的实现

import isFunction from './isFunction'
import isNativeFunction from './isNativeFunction'

/**
 * 检测测试函数是否为构造函数
 * ========================================================================
 * @method isConstructor
 * @category Lang
 * @param {Function|Object} fn - 要测试的(构造)函数
 * @returns {Boolean} - fn 是构造函数,返回 true,否则返回 false;
 */
const isConstructor = (fn) => {
  const proto = fn.prototype
  const constructor = fn.constructor
  let instance

  // 特征1:必须是函数;
  // 特征2:有 prototype 原型;
  if (!isFunction(fn) || !proto) {
    return false
  }

  // 针对内置构造函数的特殊判断:
  // 特征1:必须是内置函数;
  // 特征2:函数对象的 constructor 指向本身或者指向 Function 构造函数
  if (
    isNativeFunction(fn) &&
    (constructor === Function || constructor === fn)
  ) {
    return true
  }

  // 特征3:可以通过 new 关键创建的实例;
  instance = new fn()

  // 特征4:实例(对象)的 constructor 应该是它的构造函数或者 Object
  return (
    (instance.constructor === fn && instance instanceof fn) ||
    (instance.constructor === Object && instance instanceof Object)
  )
}

export default isConstructor

JavaScript 内置构造函数的特殊判断

isConstructor() 方法中特表要解释以下的是这段代码:

// 针对内置构造函数的特殊判断:
// 特征1:必须是内置函数;
// 特征2:函数对象的 constructor 指向本身或者指向 Function 构造函数
if (
  isNativeFunction(fn) &&
  (constructor === Function || constructor === fn)
) {
  return true
}

在使用 new 关键字创建实例前,先对 JavaScript 内置(Build in)的构造函数做了一个特殊的判断。这是因为例如:URL、File、Promise、Proxy 等内置对象,初始化对象的时候这些构造函数会对参数进行校验,没有传递参数或者参数格式不正确都会直接报错。

相应有朋友应该类似这样的构造函数的判断:

try {
    // 特征3:可以通过 new 关键创建的实例;
    instance = new fn()
  } catch() {
    // 不是构造函数(xxx function is not a constructor)
	return false
  }

很明显,我以上给出的内置构造函数使用 new fn() 会进入 catch() 分支,认为它不是一个构造函数。我也是经过仔细的推敲使用现在的内置函数的判断逻辑。避免这些内置构造函数创建实例的时候,构造函数的参数不正确报错导致的错误判断。

总结

由于函数的 constructor 对象不是一个只读属性,所以手动改变了 constructor 的指向,我的 isConstructor() 方法也不能保证完美兼容。不过本文的关键是告诉大家在 JavaScript 中构造函数都有哪些特点,以及需要掌握哪些重要的 JavaScript 的知识点。

当然,对于如何判断构造函数,你有更好或者更全面的方法,也请在文章下方留言交流。


0 条评论

发表回复

Avatar placeholder