🗒️函数对象和构造器对象

这篇不是原创,是整理的笔记

在 JavaScript 中,可以用对象来模拟函数和构造器。JavaScript 为这类对象预留了私有字段机制,并规定了抽象的函数对象和构造器对象的概念。

  • 函数对象:具有私有字段 [[call]] 的对象

  • 构造器对象:具有私有字段 [[construct]] 的对象

JavaScript 里的函数是用对象来模拟的,它们和一般编程语言中的函数一样,可以被调用和传参。任何宿主只要提供了“具有私有字段 [[call]] 的对象”,就可以被 JavaScript 的函数调用语法支持。私有字段 [[call]] 必须是一个引擎中定义的函数,需要接受 this 值和参数,并且会产生域(realm)的切换。

我们可以说,任何对象只要实现了私有字段 [[call]] 它就是一个函数对象了——可以作为函数被调用,只要实现了私有字段 [[construct]] 它就是一个构造器对象了——可以作为构造器被调用。只要字段符合,我们就可以用 JavaScript 里的固有对象来模拟函数和构造器了。

1. 不总是一致

然而,JavaScript 固有对象实现 [[call]][[construct]] 的方式并不总是一致的。比如:

  1. 浏览器宿主环境提供的 Image 对象,只支持构造器调用,不支持函数调用

  2. 基本类型(String, Number, Boolean)的构造器在被当作函数调用时会执行类型转换,而不返回对象

  3. 用 ES6 => 语法创建的函数仅仅是函数,而不能作为构造器来使用

new Image(); // <img>
Image();  // Uncaught TypeError: Failed to construct 'Image': Please use the 'new' operator, this DOM object constructor cannot be called as a function.

let myFunc = () => {};
myFunc(); // 可以正常执行
new myFunc(); // TypeError: myFunc is not a constructor

2. 函数

对于使用 function 语法或 Function 构造器创建的对象来说,[[call]][[construct]] 的行为总是相似的,因为它们会执行同一段代码。

但是 [[construct]] 会比 [[call]] 多干几件事情:

  1. 首先,以 Object.prototype 为原型创建一个新对象 A

  2. 然后,以新对象 A 为 this 来执行函数的私有字段 [[call]]

  3. 如果 [[call]] 的返回值是对象 B,那么就返回该对象 B,否则就返回第 1 步中创建的对象 A

如果 [[construct]] 最终返回的是对象 B,那么对象 A 就成了在构造函数之外完全没法被访问的对象,这在一定程度上可以实现私有。示例代码如下:

function myFunc() {
    this.year = 2022;
    return {
        getYear: () => this.year
    }
}
let myObj = new myFunc();
myObj.getYear(); // 2022
// 而字段 year 在外面是没法被访问的

3. 特殊行为

和普通对象的行为相比,有一些对象的行为比较特殊。包括但不限于:

  1. Object.prototype:作为所有普通对象的默认原型,不能再给它设置原型了

  2. Array:length 属性既不是数据属性也不是访问器属性,它还可以根据数组的最大下标自动发生变化

  3. String:为了支持下标运算,String 的正整数属性访问会去查字符串,也不算一般意义上的属性

  4. Arguments:非负整数型的下标属性,会和对应的形参变量联动(同时修改)

  5. 类型数组和数组 buffer:和内存块相关联,下标运算比较特殊

  6. bind 后的 function:和原函数相关联

  7. 模块的 namespace 对象:特殊的地方很多,和普通对象完全不一样。尽量只用它来 import

4. 写在最后

4.1 附代码

找固有对象和宿主对象的代码:

// 标准里的固有对象 https://anjia1.gitbook.io/web/
const objects = [
    eval,
    isFinite,
    isNaN,
    parseFloat,
    parseInt,
    decodeURI,
    decodeURIComponent,
    encodeURI,
    encodeURIComponent,
    Array,
    Date,
    RegExp,
    Promise,
    Proxy,
    Map,
    WeakMap,
    Set,
    WeakSet,
    Function,
    Boolean,
    String,
    Number,
    Symbol,
    Object,
    Error,
    EvalError,
    RangeError,
    ReferenceError,
    SyntaxError,
    TypeError,
    URIError,
    ArrayBuffer,
    DataView,
    Float32Array,
    Float64Array,
    Int8Array,
    Int16Array,
    Int32Array,
    Uint8Array,
    Uint16Array,
    Uint32Array,
    Uint8ClampedArray,
    Atomics,
    JSON,
    Math,
    Reflect
];
const set = new Set();
objects.forEach(o => set.add(o));

function addFunc(v) {
    if (!set.has(v)) {
        set.add(v);
        objects.push(v);
    }
}

for (let o of objects) {
    for (let p of Object.getOwnPropertyNames(o)) {
        let desc = Object.getOwnPropertyDescriptor(o, p);
        if ((desc.value !== null && typeof desc.value === "object") || (typeof desc.value === "function")) addFunc(desc.value);
        if (desc.get) addFunc(desc.get);
        if (desc.set) addFunc(desc.set);
    }
}

console.log(set);

执行结果如下:

4.2 主要参考

Last updated