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

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

在 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 就成了在构造函数之外完全没法被访问的对象,这在一定程度上可以实现私有。示例代码如下:

3. 特殊行为

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

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

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

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

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

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

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

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

4. 写在最后

4.1 附代码

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

执行结果如下:

4.2 主要参考

Last updated