簡體   English   中英

如何檢查 Javascript 函數是否是構造函數

[英]How to check if a Javascript function is a constructor

我注意到並非所有的 Javascript 函數都是構造函數。

var obj = Function.prototype;
console.log(typeof obj === 'function'); //true
obj(); //OK
new obj(); //TypeError: obj is not a constructor

問題1:如何檢查一個函數是否是一個構造函數,以便可以用new關鍵字調用它?

問題2:當我創建一個函數時,是否可以讓它不是構造函數?

一點背景:

ECMAScript 6+ 區分了可調用(可以不用new調用)和可構造(可以用new調用)函數:

  • 通過箭頭函數語法或通過類或對象文字中的方法定義創建的函數是不可構造的
  • 通過class語法創建的函數不可調用
  • 以任何其他方式創建的函數(函數表達式/聲明、 Function構造函數)都是可調用和可構造的。
  • 除非另有明確說明,否則內置函數不可構造。

關於Function.prototype

Function.prototype是所謂的內置函數,它是不可構造的 從規范:

除非在特定函數的描述中另有說明,否則未被標識為構造函數的內置函數對象不會實現[[Construct]]內部方法。

Function.prototype的值是在運行時初始化的最開始時創建的。 它基本上是一個空函數,並沒有明確說明它是可構造的。


如何檢查函數是否是構造函數,以便可以使用 new 調用它?

沒有內置的方法可以做到這一點。 您可以try使用new調用該函數,然后檢查錯誤或返回true

function isConstructor(f) {
  try {
    new f();
  } catch (err) {
    // verify err is the expected error and then
    return false;
  }
  return true;
}

但是,這種方法不是故障安全的,因為函數可能有副作用,所以在調用f ,您不知道環境處於哪種狀態。

此外,這只會告訴你一個函數是否可以作為構造函數調用,而不是它是否打算作為構造函數調用。 為此,您必須查看文檔或函數的實現。

注意:永遠沒有理由在生產環境中使用這樣的測試。 一個函數是否應該用new調用應該可以從它的文檔中辨別出來。

當我創建一個函數時,如何使它不是構造函數?

要創建一個真正不可構造的函數,可以使用箭頭函數:

var f = () => console.log('no constructable');

箭頭函數根據定義是不可構造的。 或者,您可以將函數定義為對象或類的方法。

否則,您可以通過檢查this值來檢查一個函數是否被new (或類似的東西)調用,如果是,則拋出錯誤:

function foo() {
  if (this instanceof foo) {
    throw new Error("Don't call 'foo' with new");
  }
}

當然,由於還有其他方法可以設置this的值,因此可能會出現誤報。


例子

 function isConstructor(f) { try { new f(); } catch (err) { if (err.message.indexOf('is not a constructor') >= 0) { return false; } } return true; } function test(f, name) { console.log(`${name} is constructable: ${isConstructor(f)}`); } function foo(){} test(foo, 'function declaration'); test(function(){}, 'function expression'); test(()=>{}, 'arrow function'); class Foo {} test(Foo, 'class declaration'); test(class {}, 'class expression'); test({foo(){}}.foo, 'object method'); class Foo2 { static bar() {} bar() {} } test(Foo2.bar, 'static class method'); test(new Foo2().bar, 'class method'); test(new Function(), 'new Function()');

您正在尋找函數是否具有[[Construct]]內部方法。 內部方法IsConstructor詳細說明了步驟:

IsConstructor(argument)

 ReturnIfAbrupt(argument). // (Check if an exception has been thrown; Not important.) If Type(argument) is not Object, return false. // argument === Object(argument), or (typeof argument === 'Object' || typeof argument === 'function') If argument has a [[Construct]] internal method, return true. Return false.

現在我們需要找到使用IsConstructor但沒有調用[[Construct]] (通常由Construct內部方法調用)。

我發現是在String函數的newTarget (js中的new.target )中使用的,可以和Reflect.construct一起使用:

function is_constructor(f) {
  try {
    Reflect.construct(String, [], f);
  } catch (e) {
    return false;
  }
  return true;
}

(我真的可以使用任何東西,比如Reflect.construct(Array, [], f); ,但String是第一個)

這產生以下結果:

// true
is_constructor(function(){});
is_constructor(class A {});
is_constructor(Array);
is_constructor(Function);
is_constructor(new Function);

// false
is_constructor();
is_constructor(undefined);
is_constructor(null);
is_constructor(1);
is_constructor(new Number(1));
is_constructor(Array.prototype);
is_constructor(Function.prototype);
is_constructor(() => {})
is_constructor({method() {}}.method)

<注意>

我發現它唯一不起作用的值是Symbol ,盡管new Symbol拋出TypeError: Symbol is not a constructor in Firefox,但is_constructor(Symbol) === true 這在技術上是正確的答案,因為Symbol確實有一個[[Construct]]內部方法(這意味着它也可以被子類化),但是使用newsuperSymbol拋出錯誤的特殊情況(因此, Symbol是一個構造函數,報錯信息是錯誤的,不能當做一個。)你可以加if (f === Symbol) return false; 到頂部雖然。

對於這樣的事情也是如此:

function not_a_constructor() {
  if (new.target) throw new TypeError('not_a_constructor is not a constructor.');
  return stuff(arguments);
}

is_constructor(not_a_constructor);  // true
new not_a_constructor;  // TypeError: not_a_constructor is not a constructor.

所以作為構造函數的意圖不能像這樣(直到添加了像Symbol.is_constructor或其他標志這樣的Symbol.is_constructor )。

</note>

有一種快速簡便的方法可以確定函數是否可以實例化,而不必求助於 try-catch 語句(v8 無法對其進行優化)

function isConstructor(obj) {
  return !!obj.prototype && !!obj.prototype.constructor.name;
}
  1. 首先我們檢查對象是否是原型鏈的一部分。
  2. 然后我們排除匿名函數

有一個警告,即: 在定義中命名的函數仍然會產生 name 屬性並因此通過此檢查,因此在依賴函數構造函數的測試時需要謹慎。

在以下示例中,該函數不是匿名的,但實際上稱為“myFunc”。 它的原型可以擴展為任何 JS 類。

let myFunc = function () {};

:)

使用 ES6+ 代理,可以在不實際調用構造函數的情況下測試[[Construct]] 這是一個片段:

const handler={construct(){return handler}} //Must return ANY object, so reuse one
const isConstructor=x=>{
    try{
        return !!(new (new Proxy(x,handler))())
    }catch(e){
        return false
    }
}

如果傳遞的項不是對象,則Proxy構造函數會引發錯誤。 如果它不是可構造的對象,則new會拋出錯誤。 但是如果它是一個可構造的對象,那么它會返回handler對象而不調用它的構造函數,然后不調用它的構造函數為true

正如您所料, Symbol仍被視為構造函數。 那是因為它是,並且實現僅在調用[[Construct]]時拋出錯誤。 這可能是任何用戶定義的函數在new.target存在時拋出錯誤的情況,因此將其作為額外檢查專門清除似乎是不正確的,但如果您發現它是,請隨意這樣做有幫助。

如果該函數是一個構造函數,那么它將有一個“原型”成員,而該成員又具有一個等於函數本身的“構造函數”成員。

function isConstructor(func) {
    return (func && typeof func === "function" && func.prototype && func.prototype.constructor) === func;
}

對於問題 1,這個助手呢?

Function.isConstructor = ({ prototype }) => Boolean(prototype) && Boolean(prototype.constructor)

Function.isConstructor(class {}); // true
Function.isConstructor(function() {}); // true
Function.isConstructor(() => {}); // false
Function.isConstructor("a string"); // false

對於問題 2,箭頭函數是解決方案。 它不能用作構造函數,因為它不依賴與常規函數相同的作用域並且沒有原型(實例的定義,類似於真正的 OOP 的類定義)

const constructable = function() { console.log(this); };
const callable = () => { console.log(this); };

constructable(); // Window {}
callable(); // Window {}
new constructable(); // aConstructableFunction {}
new callable(); // Uncaught TypeError: callable is not a constructor

我嘗試了許多解決方法,但都不能滿足我的需求,所以我通過使用反射元數據制作了自己的解決方法。

目標:檢查當前函數是否具有它自己的 ____ 元數據,這些元數據表示此函數是否為構造函數。

  • npm 安裝反射元數據
  • 創建類裝飾器工廠@Class()
  • 創建一個輔助函數來檢查是否附加到當前函數的__class__元數據。

注意:此解決方法中區分構造函數和普通函數或類的唯一方法是使用類裝飾器@Class()

import 'reflect-metadata';

type Constructor<T = any> = new (...args: any[]) => T;

function Class() {
    return function (target: Constructor) {
        if (!!Reflect.getOwnMetadata('__class__', target)) {
            throw new Error(`Cannot apply @Class decorator on ${target.name} multiple times.`);
        }
        Reflect.defineMetadata('__class__', target, target);
    };
}

function isConstructor<T>(type: Constructor<T>): boolean {
    if (typeof type !== 'function') return false;
    return !!Reflect.getOwnMetadata('__class__', type);
}

/*
 * ------------------
 * Example
 * ------------------
 */

@Class()
class ServiceClass1 {}

class ServiceClass2 {}

function Test() {}

console.log(isConstructor(ServiceClass1)) // true
console.log(isConstructor(ServiceClass2)) // false
console.log(isConstructor(Test)) // false

有一種快速簡便的方法可以確定函數是否可以被實例化,而不必求助於 try-catch 語句(v8 無法優化)

function isConstructor(value) {
    return !!value && !!value.prototype && !!value.prototype.constructor;
}
  1. 首先,我們檢查value是否真實。
  2. 然后檢查value是否是原型鏈的一部分。
  3. 最后,只需檢查是否設置了constructor ;-)

請注意,上面被命名為isConstructor不是isConstructable ,我的意思是,這將為Bound-constructors返回false ,如注釋中所述,因為“bound”意味着重定向到某些東西,而不是直接成為真正的構造函數。

所以這回答了標題的“ check ... is constructor ”問題,但不是以后“ check ... can be called with new

例子:

 const myClazz = {method() {}}; myClazz.method = myClazz.method.bind(myClazz); // We can call above with new keyword. new (myClazz.method); // But it's just a callback. if (isConstructor(myClass)) throw new Error('expected to return false for arrow-functions and similar.');

單元測試

下面是基於Jasmine的。

// Change import to wherever your common functions are 
// (for me they're in src directory, outside of tests directory).
import * as common from '../common-tools';

let isMyClassCalled = false;
class MyClass {
  constructor() {
      isMyClassCalled = true;
  }
}

describe('App isConstructor tool', () => {
    it('should detect constructor', function () {
        detect(class A {});
        detect(Array);
        detect(Function);
        detect(new Function);
        detect({method() {}}.method);
        detect((() => {}));
    });

    it('should NOT detect as constructor', function () {
        noDetect();
        noDetect(undefined);
        noDetect(null);
        noDetect(1);
        noDetect(new Number(1));
        noDetect(new (function(){}));
        noDetect(Array.prototype);
        noDetect(Function.prototype);
    });

    it('should NOT detect bound constructors', function () {
        const clazz = {method() {}};
        clazz.method = clazz.method.bind(clazz);
        noDetect(clazz.method);
    });

    it('should never call constructor', function () {
        common.isConstructor(MyClass);
        expect(isMyClassCalled).toBe(false);
    });

    function detect(value, expecting = true) {
        expect(common.isConstructor(value))
            .withContext('For "' + value + '" value')
            .toBe(expecting);
    }

    function noDetect(value) {
        detect(value, false);
    }
});

選擇

以上所有測試也通過以下測試。

function isConstructor(value) {
    return typeof value === 'function' && !!value.prototype && value.prototype.constructor === value;
}

作為Felix Kling 回答的補充,即使函數不可構造,如果它具有prototype屬性,我們仍然可以像構造函數一樣使用它。 我們可以在Object.create()的幫助下做到這一點。 示例:

// The built-in object Symbol is not constructable, even though it has a "prototype" property:
new Symbol
// TypeError: Symbol is not a constructor.
Object.create(Symbol.prototype);
// Symbol {}
//   description: (...)
//   __proto__: Symbol

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM