在我们的日常开发中会时常遇到需要判断数据类型的引用场景,针对这个问题在《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 条评论