JavaScript 如何判断函数为构造函数?

343 阅读5分钟

导读

在日常的开发工作中我时常会遇到需要判断函数是否为构造函数的应用场景。那么,在 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</code></pre>

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

<pre><cdoe>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() 分支,认为它不是一个构造函数。我也是经过仔细的推敲使用现在的内置函数的判断逻辑。避免这些内置构造函数创建实例的时候,构造函数的参数不正确报错导致的错误判断。

附录

以下是本文示例代码中需要另外两个方法:isFunction() 和 isNativeFunction()

isFunction()

const isFunction = (fn) => {
 return fn && {}.toString.call(fn) === '[object Function]'
}

export default isFunction

isNativeFunction

import isFunction from './isFunction'

/**
 * 检测测试数据是否为 JavaScript 内置函数
 * ========================================================================
 * @method isNativeFunction
 * @param {Function|Object} fn - 要测试的函数
 * @returns {Boolean} - fn 是内置函数,返回 true,否则返回 false;
 */
const isNativeFunction = (fn) => {
  return isFunction(fn) && /\{\s*\[native code\]\s*\}/.test('' + fn)
}

export default isNativeFunction