javascript:新关键字在内部做了什么

javascript: what do the new keywords do internally

我知道可能已经有上百万个类似的问题,例如,

但请听我说完。

代码:

let f = function(){console.log(".f.")};
fn = new f();

// Now:
typeof fn === "object" //true
//! fn() //TypeError: fn is not a function
//! new fn() //TypeError: fn is not a constructor

一般的问题是:是否可以通过操作函数f.[=33 来创建一个“可更新的”对象fn =]

问题分解到“新”关键字的内部。

我的疑问是,根据 MDN 文档,当使用 new 关键字时,会调用 classfunctionconstructor。然而,尽管 fn.__proto__.constructor === f 与所有其他 javascript functions 一样为真,但 fn 属于 'object' 类型(我们能否以某种方式将其更改为 'function' ?),并且 new fn() 抛出 TypeError。

我们甚至可以通过以下方式添加更多内容:

fn.constructor = f.constructor 
fn.__proto__ = f.__proto__
fn.prototype = f.prototype
// f.constructor === Function //true
//! fn.call(this) //fn.call is not a function

仍然,fn() 不会工作,new fnnew fn() 也不会。

为什么?

JavaScript 中的 new-able 个对象是:

  1. 使用 function 关键字创建的函数(不包括生成器函数)
  2. 类(可以当作函数)
  3. 绑定函数外来对象(“绑定函数”)
  4. 一些宿主对象
  5. 代理,如果它们适用于上述之一

我知道这一点,因为唯一的对象类型是 new operator works with are "constructors"(规范术语)。构造函数是具有 [[Construct]] 内部方法的对象,您可以搜索 ECMAScript 规范以找出哪些类型的对象具有 [[Construct]] 内部方法。

要生成构造函数的结果 new-able,因此,您必须 return 上面列出的对象类型之一。

请注意,规范 specifically says 所有构造函数 definitionally 函数,因为它们必须支持 [[Call]] 内部方法(另请注意下面关于主机的警告对象)。

如果您想变得非常高级,那么您可能有兴趣了解 host objects do not appear to share the ordinary limitations for constructors(大概是出于遗留 Web 兼容性的原因),但这些都是例外。

解释 .constructor 属性

当声明一个 new-able 函数 f 时,two objects are created:function-object f 本身,以及 [=18= 上的默认对象] own-property 个 f。此默认 .prototype 对象的 .constructor 属性 由 运行 时间自动设置为 f。我相信 classes 的工作方式非常相似。请注意,这个 属性 的名称被选择为“原型”这一事实使得在 JavaScript 中讨论原型非常混乱(因为它与函数的 [[prototype]] 不同)。

位于 .prototype 属性 上的对象上的 constructor own-property 永远不会被任何 built-in 函数或操作读取(据我所知的)。我认为它是 JavaScript 早期的遗迹——它的初衷是作为一种在“class”之间维护 link 的方式,该 class 构建了一个对象作为开发人员的启示。主机环境(例如浏览器)有时会使用它来推断对象的“类型”以与用户通信(例如控制台输出),属性 是可写的,因此不可靠。

new 操作员执行的步骤

在较高级别,当针对构造函数调用 new 时,会发生以下步骤 (spec contains full details):

  1. 创建了一个新对象o
  2. o[[Prototype]] (“原型”)设置为构造函数的 .prototype 属性 的值(注意这意味着 .constructor属性被新对象继承)
  3. 构造函数体的目标(即this)设置为o
  4. 构造函数是运行,上面定义了this
  5. 如果没有明确的 object-type return 值,则 o 默认 returned

范围:我们仅在 Function 上检查 new。因为这是最容易混淆的部分。在 Class 上调用 new 会产生类似于其他主要 OOP 语言的结果。

原问题可以分解为以下两个问题:

  1. 调用new关键字时,详细的构建过程是什么

  2. JavaScript如何判断一个对象是否可调用? (感谢@BenAston 提到 new 关键字可能仅适用于一组有限的对象(例如,前缀为 ClassFunction))


  1. 第一个问题的答案:

回到MDN Document,

When the code new Foo(...) is executed, the following things happen:

  1. A new object is created, inheriting from Foo.prototype.
  2. The constructor function Foo is called with the specified arguments, and with this bound to the newly created object. new Foo is equivalent to new Foo(), i.e. if no argument list is specified, Foo is called without arguments.
  3. The object (not null, false, 3.1415 or other primitive types) returned by the constructor function becomes the result of the whole new expression. If the constructor function doesn't explicitly return an object, the object created in step 1 is used instead.(Normally constructors don't return a value, but they can choose to do so if they want to override the normal object creation process.)

文字可能有歧义, 但是PoC代码如下:

// Case1, function has no return value; 
// A new object is created, f0n.__proto__ === f0.prototype
let f0 = function() {};
f0.prototype.f0p = "f0p";

let f0n = new f0();
console.log(f0n) // f0n is a new Object, inheriting from f0.prototype
console.log(f0n.__proto__.f0p); // "f0p"

// Case3, function has an explicit return value, the value is an object
// (not null, false, 3.1415 or other primitive types); 
// the return value becomes the new object value.
let f3 = function() {
  return {
    "f3": "f3"
  }
};
f3.prototype.f3p = "f3p";

let f3n = new f3();
console.log(f3n) // {f3: "f3"}
// f3n is an Object, the return value of its constructor function `f3`
console.log(f3n.__proto__.f3p); // undefined

// Case4 (or Case1 again), function has an **implicit** return value.
let f4 = function(a) {
  return (a + a)
};
f4.prototype.f4p = "f4p";

let f4n = new f4();
console.log(f4n.__proto__.f4p); // "f4p"

2.Answer转第二题:

我还不知道 JavaScript 如何确定一个对象是否可调用。答案应该藏在ECMAScripts spec里了。 (感谢@BenAston 指出)

假设只有 Function 是可调用的可能是合理的。以下 post 提供了一种解决方法: How to make an object callable

  • 额外:如何return Callable?

使用案例3,let f = Function(){return Function(){}} 由于 return 值是一个 non-primitive 显式对象,它成为 new 指令的结果。结果是一个函数,然后可以调用它。