无法解决Javascript函数之谜


16

我试图了解Javascript幕后的景象,并迷恋于了解内置对象的创建,尤其是对象函数以及它们之间的关系。

当我读取到所有内置对象(如Array,String等)都是从Object扩展(继承)时,我假定Object是第一个被创建的内置对象,其余对象都继承自该对象。但是,当您知道对象只能由函数创建时,然后函数也不过是Function的对象而已,这没有任何意义。听起来开始像母鸡和鸡肉的困境。

另一个非常令人困惑的事情是,如果我console.log(Function.prototype)打印了一个函数,但是当我打印时console.log(Object.prototype)它打印了一个对象。为什么Function.prototype当它打算成为一个对象时是一个函数?

同样,根据Mozilla文档,每个javascript function都是Function对象的扩展,但是当您console.log(Function.prototype.constructor)再次将其作为函数时。现在,您如何使用自己创建的东西(思维=吹动)。

最后一件事Function.prototype是一个函数,但是我可以constructor使用来访问该函数Function.prototype.constructor,这意味着Function.prototype一个返回prototype对象的函数


由于函数是对象,因此意味着Function.prototype可以是函数并具有内部字段。因此,不,在遍历其结构时不要执行原型函数。最后要记住,有一个引擎在解释Javascript,因此对象和函数可能是在引擎内创建的,而不是像Javascript和特殊引用那样创建的Function.prototypeObject.prototype可能只是由引擎以特殊方式解释。
Walfrat '18

1
除非您打算实现符合标准的JavaScript编译器,否则您实际上不必担心这些东西。如果您想做一些有用的事情,那您就偏离了路线。
贾里德·史密斯

5
仅供参考,英语中“鸡和鸡的困境”的惯用语是“鸡和鸡蛋的问题”,即“先出现的是鸡还是鸡蛋?” (当然,答案是鸡蛋。卵生动物在鸡面前已经存在了数百万年。)
埃里克·利珀特

Answers:


32

我试图了解Javascript幕后的景象,并且卡在理解内置对象(尤其是对象和函数)及其之间的关系的创建方面。

它很复杂,很容易造成误解,并且许多初学者的Javascript书籍都弄错了,所以不要相信您阅读的所有内容。

我是1990年代Microsoft JS引擎的实现者之一,并且是标准化委员会的成员,在将这个答案汇总在一起时,我犯了很多错误。(尽管自从我从事这项工作已有15年以上,我也许可以原谅。)这是一件棘手的事情。但是,一旦您了解了原型继承,一切都将变得有意义。

当我读取到所有内置对象(如Array,String等)都是从Object扩展(继承)时,我假定Object是第一个被创建的内置对象,其余对象都继承自该对象。

首先,丢弃所有有关基于类的继承的知识。JS使用基于原型的继承。

接下来,请确保您对“继承”的含义有一个非常清晰的定义。习惯于C#或Java或C ++等OO语言的人们认为继承意味着子类型化,但是继承并不意味着子类型化。 继承意味着一件事的成员也是另一事物的成员。这并不一定意味着这些事物之间存在子类型关系!类型理论中的许多误解是人们没有意识到存在差异的结果。

但是,当您知道对象只能由函数创建时,然后函数也不过是Function的对象而已,这没有任何意义。

这是完全错误的。有些对象不是通过调用new F某些函数来创建的F。JS运行时创建的某些对象一无所有。有些鸡蛋不是任何鸡都产的。它们只是在运行时启动时创建的。

让我们说说规则是什么,也许会有所帮助。

  • 每个对象实例都有一个原型对象。
  • 在某些情况下,原型可以是null
  • 如果访问对象实例上的成员,并且该对象没有该成员,则该对象将遵循其原型,如果原型为null,则停止。
  • prototype对象的成员通常不是对象的原型。
  • 相反,prototype功能对象F 的成员是将成为所创建对象原型的对象new F()
  • 在某些实现中,实例会获得__proto__确实提供其原型的成员。(现在不建议使用。不要依赖它。)
  • 函数对象prototype在创建时会分配一个全新的默认对象。
  • 当然,功能对象的原型是Function.prototype

让我们总结一下。

  • 的原型ObjectFunction.prototype
  • Object.prototype 是对象原型对象。
  • 的原型Object.prototypenull
  • 的原型FunctionFunction.prototype-这是罕见的情况,Function.prototype实际上是的原型Function
  • Function.prototype 是函数原型对象。
  • 的原型Function.prototypeObject.prototype

假设我们创建一个函数Foo。

  • 的原型FooFunction.prototype
  • Foo.prototype 是Foo原型对象。
  • 的原型Foo.prototypeObject.prototype

假设我们说 new Foo()

  • 新对象的原型是 Foo.prototype

确保有道理。让我们画一下。椭圆是对象实例。边缘的__proto__含义是“的原型”或prototypeprototype属性”。

在此处输入图片说明

运行时所要做的就是创建所有这些对象并相应地分配它们的各种属性。我相信您可以看到将如何完成。

现在,让我们看一个测试您的知识的示例。

function Car(){ }
var honda = new Car();
print(honda instanceof Car);
print(honda.constructor == Car);

该打印什么?

好吧,这instanceof是什么意思? honda instanceof Car表示“ Car.prototype等于honda原型链上的任何对象吗?”

是的。 honda的原型是Car.prototype,所以我们完成了。这打印真实。

那第二个呢?

honda.constructor不存在,因此我们参考了原型Car.prototype。当Car.prototype对象被创建它被自动赋予属性constructor等于Car,所以这是真的。

现在呢?

var Animal = new Object();
function Reptile(){ }
Reptile.prototype = Animal;
var lizard = new Reptile();
print(lizard instanceof Reptile);
print(lizard.constructor == Reptile);

该程序打印什么?

同样,lizard instanceof Reptile意味着“ Reptile.prototype等于lizard原型链上的任何对象吗?”

是的。 lizard的原型是Reptile.prototype,所以我们完成了。这打印真实。

现在呢

print(lizard.constructor == Reptile);

您可能会认为这也是正确的,因为它lizard是用构造的,new Reptile但是您会错的。推理出来。

  • 是否lizard有一个constructor属性?否。因此,我们看一下原型。
  • 的原型lizardReptile.prototype,这是Animal
  • 是否Animal有一个constructor属性?否。所以我们看它的原型。
  • 的原型AnimalObject.prototype,并且Object.prototype.constructor由运行时创建的并等于Object
  • 因此,此打印错误。

我们应该Reptile.prototype.constructor = Reptile;在那儿说些什么,但我们不记得要说!

确保一切对您有意义。如果仍然令人困惑,请绘制一些方框和箭头。

另一个非常令人困惑的事情是,如果我console.log(Function.prototype)打印了一个函数,但是当我打印时console.log(Object.prototype)它打印了一个对象。为什么Function.prototype当它打算成为一个对象时是一个函数?

函数原型定义为一个函数,该函数在调用时返回undefined。我们已经知道那Function.prototypeFunction原型,这很奇怪。因此Function.prototype()是合法的,并且当您这样做时,您会undefined回来。所以这是一个功能。

Object原型没有这个属性; 它是不可调用的。这只是一个对象。

当您console.log(Function.prototype.constructor)再次使用该功能时。

Function.prototype.constructor只是Function,很明显。并且Function是一个功能。

现在,您如何使用自己创建的东西(思维=吹动)。

您对此考虑过多。所需要做的就是运行时在启动时创建一堆对象。对象只是将字符串与对象相关联的查找表。当运行时启动时,所有它做的是创造了几十个空对象,然后开始分配prototype__proto__constructor,等每一个对象的属性,直到他们做他们需要做的图表。

如果您采用我上面提供的示意图并添加constructor边缘,将会很有帮助。您将很快看到这是一个非常简单的对象图,并且运行时创建它不会有任何问题。

一个好的练习是自己做。在这里,我先开始。我们将使用my__proto__“的原型对象”和myprototype“的原型属性”来表示。

var myobjectprototype = new Object();
var myfunctionprototype = new Object();
myfunctionprototype.my__proto__ = myobjectprototype;
var myobject = new Object();
myobject.myprototype = myobjectprototype;

等等。您是否可以填写该程序的其余部分,以构建具有与“真实” Javascript内置对象相同的拓扑的一组对象?如果这样做,您会发现它非常简单。

JavaScript中的对象只是将字符串与其他对象相关联的查找表。而已!这里没有魔术。您之所以陷入困境,是因为您在想象实际上并不存在的约束,例如每个对象都必须由构造函数创建。

函数只是具有附加功能的对象:被调用。因此,通过您的小型仿真程序,.mycallable向每个对象添加一个属性,以指示该对象是否可调用。就这么简单。


9
最后,简短,简洁,易懂的JavaScript解释!优秀!我们中的任何一个人怎么可能感到困惑?:)尽管非常认真,但是关于对象是查找表的最后一点确实是关键。有一种疯狂的方法---但仍然疯狂的……
Greg Burghardt

4
@GregBurghardt:我同意一开始看起来很复杂,但是复杂性是简单规则的结果。每个对象都有一个__proto__。该__proto__物体原型为null。的__proto__new X()X.prototype__proto__除函数原型本身以外,所有函数对象都具有函数原型。 ObjectFunction和函数原型是函数。这些规则都很简单,它们确定了初始对象图的拓扑。
埃里克·利珀特

6

您已经有很多出色的答案,但是我只是想对您的答案给出一个简短而明确的答案,说明所有这些工作原理,答案是:

魔法!!!

真的,就是这样。

实现ECMAScript执行引擎的人员必须实现 ECMAScript的规则,但其实现过程中不遵守这些规则。

ECMAScript规范说A从B继承而B是A的实例?没问题!首先创建一个原型指针为NULLA的A,然后将B创建为A的实例,然后修复A的原型指针以使其随后指向B。十分简单。

您说,但是,等等,无法在ECMAScript中更改原型指针!但是,这就是问题:此代码未 ECMAScript引擎运行,此代码 ECMAScript引擎。它确实可以访问引擎上运行的ECMAScript代码所没有的对象的内部。简而言之:它可以做任何想要的事情。

顺便说一句,如果您确实愿意,则只需执行一次:之后,例如,您可以转储内部内存并在每次启动ECMAScript引擎时加载此转储。

请注意,即使ECMAScript引擎本身是用ECMAScript编写的,所有这些仍然适用(例如,Mozilla Narcissus的情况就是如此)。即使这样,实现引擎的ECMAScript代码仍然可以完全访问正在实现的引擎,尽管它当然不能访问正在运行的引擎。


3

根据ECMA规范1

ECMAScript不包含诸如C ++,Smalltalk或Java中的适当类,而是支持构造函数,该构造函数通过执行为对象分配存储空间的代码来创建对象,并通过为它们的属性分配初始值来初始化所有或部分对象。包括构造函数在内的所有函数都是对象,但并非所有对象都是构造函数。

我不知道怎么回事!!! </sarcasm>

再往下看,我们看到:

原型原型是用于在ECMAScript中实现结构,状态和行为继承的对象。当构造函数创建对象时,该对象隐式引用构造函数的关联原型,以解决属性引用的问题。构造表达式的关联原型可以由程序表达式构造器.prototype引用,添加到对象原型的属性可以通过继承由共享原型的所有对象共享。

因此,我们可以看到原型是对象,但不一定是功能对象。

另外,我们有这个有趣的主旨

http://www.ecma-international.org/ecma-262/8.0/index.html#sec-object-objects

Object构造函数是%Object%内部对象和全局对象的Object属性的初始值。

Function构造函数是%Function%内部对象和全局对象的Function属性的初始值。


现在可以了。ECMA6允许您创建类并从中实例化对象。
ncmathsadist

2
@ncmathsadist ES6类只是一个语法糖,语义是相同的。
Hamza Fatmi '18

1
你的sarcasm名字,否则,这段文字真的是相当不透明的初学者。
罗伯特·哈维

没错,以后要加进更多的东西,需要做点挖掘
Ewan

1
嗯?要指出的是,从文档中还不清楚
Ewan

1

以下类型包含JavaScript中的每个值:

  • boolean
  • number
  • undefined(包括单个值undefined
  • string
  • symbol (通过引用比较的抽象“事物”)
  • object

JavaScript中的每个对象(即所有对象)都有一个原型,这是一种对象。

原型包含函数,它们也是一种对象1

对象也有一个构造函数,它是一个函数,因此是一种对象。

嵌套的

都是递归的,但是实现能够自动完成,因为与JavaScript代码不同,它可以创建对象而无需调用JavaScript函数(因为对象只是实现控制的内存)。

在许多动态类型语言中,大多数对象系统都是这样的循环2。例如,在Python中,类是对象,而类的类是type,因此它type也是其实例。

最好的想法是只使用语言提供的工具,而不要过多考虑它们的实现方式。

1函数是相当特殊的,因为它们是可调用的,并且它们是唯一可以包含不透明数据(它们的主体以及可能是闭包)的值。

2实际上,它实际上是折弯的分支状丝带,向后弯曲,但“圆形”足够接近。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.