Father Prototype and Mother Constructor





5.00/5 (7投票s)
Solid JavaScript from facts to rules
引言
"UML可能要为面向对象编程的毁灭负责。因为它完全关注类而不是对象,腐蚀了一整代程序员。" - Dave Thomas
"想法是‘来网景浏览器里写Scheme。把这个编程语言放进浏览器里。’" - Brendan Eich
大多数人熟悉JavaScript都是因为“嗯,它是一种面向对象的、动态类型的编程语言”,至少我是这样的。随着对它了解的深入,很明显,这门语言的力量主要在于其强大的函数,在于其一等公民函数。
原型是JavaScript最基本的功能之一。如果我以对象开始这篇文章,会遇到麻烦;如果我以函数开始,会有双重麻烦。无论哪种方式,我很快就会陷入“鸡生蛋还是蛋生鸡”的困境。
我会写一些例子并指出一些事实。让我们来探索原型的规则,并在此基础上构建构造函数。我感觉最好还是像大家学习这门语言时一样开始。你创建一个对象…就这样
示例 1
var myCar = {
color: "silver",
weight: 940
};
但是,什么是对象?更笼统地说,可以参见附录C。对于JavaScript,“对象代表命名值的集合。在JavaScript对象中,命名值被称为属性。对象属性可以是原始值、其他对象和函数。对象方法是包含函数定义的属性。” - w3schools
在JavaScript中,变量和属性分为两类:原始类型和引用类型。在过去几年里,语言发展迅速,所以我想具体、甚至精确一些。本文将遵循2011年6月的ES5.1标准,不多也不少。你应数五,你数之数应为五。
原始类型和引用类型
有五种原始类型:number、string、boolean、null和undefined。这些类型通过值传递给函数和运算符。按值传递是指将变量内容的副本传递给运算符或函数。
示例 2
var text1 = "bird";
var text2 = "bird";
text1 == text2 //true
这意味着text1
和text2
具有相同的值,逐字比较,尽管它们在计算机内存中物理上是不同的。有两个“鸟”。
更具启发性的解释是,JavaScript“使用变量对象跟踪特定作用域的变量。原始值直接存储在变量对象上”。 - 面向对象JavaScript原理
变量对象是与执行上下文相关联的特殊对象,它存储:该上下文中声明的变量、函数声明和函数形参。
这两个变量位于全局作用域,全局作用域的变量对象就是全局对象。幸运的是,当我们处于浏览器控制台中时,只需输入其名称window
即可记录全局对象。您将获得window
的所有属性,并且如果您将text1
和text2
定义为“鸟”,您将看到它们两个。值相同,在全局对象中的位置不同。
如果您将text1
作为函数参数传递,您将获得text1
的内容副本,并将其放入调用函数的已声明变量对象的新副本中。这次使用它在新名称下,如同在函数的参数列表中声明的那样。在此示例中为x
。
示例 3
function test(x) {
return x;
}
不幸的是,我们无法看到函数的变量对象。它是实现细节。虽然全局变量对象有一个我们明确命名的window
,但函数作用域内的变量对象没有特定的名称、句柄或标识符。
还有一点,原始值是不可变的,就这样。您无法再更改它们,但可以为您的变量重新分配新的原始值。
参考文献
JavaScript中的所有引用数据类型都代表对象。其中一些我们知道是数组,另一些是函数,还有一些是正则表达式…“引用值作为指针存储在变量对象中,该变量对象充当指向存储对象的内存位置的引用。” - 面向对象JavaScript原理
示例 4
var object1 = {
text: "paragraph"
};
var object2 = {
text: "paragraph"
};
object1 == object2 //false
object1.text == object2.text //true
尽管object2
是object1
的逐字复制,但它们不被视为相等,因为它们不是按值比较的。这两个对象,就像那两个string
:text1
和text2
一样,在计算机内存中物理上是不同的。它们具有相同的值,但这次,将比较它们的引用是否相等。当且仅当它们具有相同的地址时,引用才等于另一个。
地址不是一个复杂的概念,比如:引用、变量、值、类型等,需要通过其他概念的组合来定义。地址只是一个自然数,并且对每个对象来说都相当明确。假设在Windows操作系统的32位桌面版本中,操作系统可寻址内存的总容量为4294967296字节。这些字节编号为[0..4294967295],理论上此范围内的任何偶数值都可以是这两个对象:object1
和object2
的有效地址。
当JavaScript运算符==
比较object1
和object2
时,它是在比较这些对象存在的地址。例如,如果object1
的地址是4000,而object2
的地址是777888,则==
比较操作的结果将是false
。当且仅当变量object1
和object2
引用同一个内存地址(例如,123456
)时,运算符==
的结果将是true
。它们将引用同一个对象。
在任何语言中,引用必须至少代表一个对象的地址,但是JavaScript引用是什么,除了地址之外,与我无关。
对象创建
在JavaScript中有三种创建对象的方式,它们都使用函数来完成。本文不详述Object.create
函数的使用。它值得单独写一篇文章。
让我们回到第一个例子,通过指定对象字面量并调用Object
构造函数来创建对象。
示例 5
var myCar = {
color: "silver",
weight: 940
};
//or plainly call the Object constructor
var hisCar = new Object({color: "blue", weight: 1040});
现在您可以清楚地看到myCar
是Suzuki Baleno,而hisCar
是Hyundai i20。您看不到的是,第一种创建方式隐式地使用了Object
构造函数。我的目标就是证明这一点。
JavaScript具有原型继承。继承是一种代码重用方式,对象从其原型继承,而原型本身是另一个对象。新创建的对象重用了其他已存在对象中定义的代码和数据。
所有对象在语言中通过其内部[[Prototype]]
属性引用其父级。对象只有一个父级,而父级又在其[[Prototype]]
属性中引用其父级…这个线性查找列表被称为原型链。
对象myCar
继承了大量功能,例如hasOwnProperty
、isPrototypeOf
、propertyIsEnumerable
、toString
等属性方法,即使myCar
没有实现这些方法,它也可以自由使用。
但是,实现这些方法的对象是什么?那就是“原始”对象。所有默认的引用类型:对象、函数和数组都从它继承,前提是正常的原型链没有被破坏。
唉,这个默认的原型链并不以原始对象结束,它以null
结束。也就是说,原始对象的[[Prototype]]
属性引用null
。这是完全合法的,原始值null
也是一个对象。
示例 6
typeof null //"object"
“鸡生蛋”的循环!
Catch-22
我对JavaScript或任何其他事物的理解是,我正在寻找构建规则的事实。据说对象是引用类型,而null
是原始类型之一,而不是引用。这打破了规则,“当机器损坏时,我们也会损坏”。
并非一切都 lost,如果您将null
分配给两个变量,它们是相等的。看起来它们是通过值比较的,就像原始类型一样。还有另一种看待方式,如果这些变量引用同一个但无效的地址,例如null
呢?无论哪种方式,无论您如何看待null
,对象还是原始值,布尔表达式都会给您一致的结果。
像null
、undefined
或NaN
这样的东西很难理解,但我非常喜欢它们在JavaScript中的处理方式。null
被视为一个对象,是一个空引用,独一无二,无法实例化任何东西。您可以将其视为一个不存在但听起来真实存在的国家,地图上没有它的位置…
在许多情况下,null
被用作对象缺失的占位符,而您通常会获得一个对象。例如,通过一个应该从数据库返回对象的函数调用。最值得注意的是,null
是原型链中的最后一个成员。
类似地,undefined
不仅是未定义的值,而且更糟。它也是一种未定义的数据类型。NaN
是类型为number的原始值,但没有数值,并且它是唯一一个不等于自身的那个值。这不仅打破了原始类型的规则,也打破了引用类型的规则。
示例 7
NaN === NaN //false
NaN == NaN //false
甚至,强制类型转换运算符==
也无法返回true
…
看示例5中的myCar
和hisCar
。我的前提是第一种情况是第二种情况的一个用例。
每个对象都有一个[[Prototype]]
属性,它引用另一个对象。该属性恰好与myCar
和hisCar
上的值相同。它引用的是同一个原始对象。
某些东西正在为myCar
和hisCar
对象设置相同的[[Prototype]]
属性。这个某些东西就是Object
构造函数。
构造函数机制
JavaScript附带了一些预置的构造函数:Object
、Function
、Array
…语言的某些规则依赖于这些。
JavaScript中的所有函数都是现成的构造函数。我们不打算将它们都用作构造函数,但构造函数和普通函数之间没有区别。为了在脑海中标记一个函数被用作构造函数,我们将其首字母大写。这是一个约定。
当与new
关键字一起使用时,所有函数都会返回一个新创建的对象。它们都会在该对象上动态创建一个属性。即使您不打算让它们充当构造函数。即使您不在其中设置任何内容。当我all说all时,我指的是all。
示例 8
function Person(name) {
this.name = name;
}
function multiplyByTwo(n) {
return 2*n;
}
var herCar = {
color: "red",
weight: 1000,
honk: function() {
console.log("honk");
}
};
var obj1 = new Person("steve");
var obj2 = new multiplyByTwo(4);
var obj3 = new herCar.honk();
显然,herCar.honk
和myltiplyByTwo
并没有显式地为obj2
和obj3
设置任何属性,但它们确实设置了某些东西。每个函数都有一个prototype
属性,它是一个对象。每个使用new
调用的函数都会将其prototype
属性设置为它构造的对象的[[Prototype]]
属性。这是一个关键的事实。如果您在阅读本文后只记住一件事,那就让它成为这件事。
示例 9
obj1.__proto__ == Person.prototype //true
obj2.__proto__ == multiplyByTwo.prototype //true
obj3.__proto__ == herCar.honk.prototype //true
Person.prototype == herCar.honk.prototype //false
obj2.__proto__ == obj3.__proto__ //false
函数也是对象,所以它们也有一个内部的[[Prototype]]
属性。prototype
属性和隐藏的[[Prototype]]
属性不是同一回事。prototype
属性仅在函数上是默认的。
您可以为任何您喜欢的对象设置一个名为prototype
的属性。如果您没有更好的乐趣…您甚至可以将函数Person
构造函数的prototype
设置为数字值8
,但我觉得这不好笑。
但是,您不能将对象的[[Prototype]]
属性设置为原始值。
在本文中,对象的原型、[[Prototype]]
和__proto__
,这三者被认为是相等的,并且所有示例都将此对象引用暴露为__proto__
。
当您使用对象字面量表示法创建对象时,其原型(父级)将成为Object.prototype
。
示例10
myCar.__proto__ == hisCar.__proto__ //true
myCar.__proto__ == Object.prototype //true
因此,myCar
的数据和代码重用的原型链或查找列表将是:myCar --> Object.prototype --> null
。
对于obj1
,它将是:obj1 --> Person.prototype --> Object.prototype --> null
。
当您创建自己的构造函数时,至少,您会在Object.prototype
和您创建的、从构造函数返回的新对象之间插入一个成员(对象)到原型链中。在这种情况下,插入的对象是Person.prototype
。
您的构造函数的prototype
属性是一个非常非常重要的对象,您将在此基础上构建您创建的所有对象未来的继承。
示例11
Person.prototype.doubleItsName = function() {
return this.name + this.name;
}
obj1.doubleItsName() //"stevesteve"
var Pesonia = function(surname) { this.surname = surname };
Personia.prototype = obj1;
var obj4 = new Personia("mcconnell");
obj4.name //"steve"
obj4.surename //"mcconnell"
obj4.doubleItsName() //"stevesteve"
我已经将obj4
的原型链设置为:obj4 --> obj1 (Personia.prototype) --> Person.prototype --> Object.prototype --> null
。
或者应该说:obj4 --> obj4.__proto__ --> obj1.__proto__ --> Object.prototype --> null
。
并且我可以覆盖obj4
和obj1
的name
。
示例12
obj1.name = "joey";
obj4.name = "billy";
并且我可以重用obj1
中的代码在obj4
中。
示例13
obj4.doubleItsName() //"billybilly"
obj1.doubleItsName() //"joeyjoey"
在示例11中,我没有将name
属性作为obj4
的自有属性,因此,JavaScript通过obj4
的原型链委托。它在obj1
上找到了name
。
另外,方法doubleItsName
不在obj4
上,甚至不在obj1
上。它更深地位于原型链中。它在一个我没有其他名称可称呼的对象上,只能称之为Person.prototype
。
奇迹的诞生
当您在函数上使用new
关键字时,JavaScript会返回一个新构造的对象,除非该函数已返回一个对象。通常,函数herCar.honk
返回undefined
,函数multiplyByTwo
返回一个数字。它们完美符合该标准。这两个函数不会在新对象中设置任何内容,因此它只有一个[[Prototype]]
属性。
正如我们所发现的,对象的__proto__
属性是其构造函数的prototype
属性。函数的邪恶行为是它们将原型对象的constructor
属性设置为函数本身,而不是应该设置为Object
构造函数。
因此,multiplyByTwo
的prototype
的constructor
属性被设置为函数multiplyByTwo
(鸡生蛋)。因此,继承自multiplyByTwo.prototype
的obj2
也继承了这个属性。
示例14
multiplyByTwo.prototype.constructor == multiplyByTwo //true
obj2.__proto__.constructor == multipleByTwo //true
multiplyByTwo.prototype.hasOwnProperty("constructor") //true
obj2.constructor == multiplyByTwo //true
obj2.hasOwnProperty("constructor") //false
obj2.constructor(6) //12
obj2
通过其原型链继承constructor
属性。
您还记得我在创建obj2
时,调用了带有参数4的multipleByTwo
(示例8)吗?因为返回的新对象obj2
和函数multiplyByTwo
的参数n
在同一个作用域(JavaScript有函数作用域),即使在函数multiplyByTwo
返回后它们都存在。哇哦,一个闭包。
我无法访问参数4作为参数n
,因为我丢失了作用域,但当multipleByTwo
在执行堆栈上运行时,由执行上下文创建的活动对象仍然存在。它必须存在,obj2
是同一个活动对象的一部分。
我将重新定义multipleByTwo
来向您展示这一点。现在,新创建的对象将有一个名为giveN
的方法。
示例15
function multiplyByTwo(n) {
this.giveN = function() {
return n;
}
return 2*n;
}
var obj2 = new multiplyByTwo(5);
obj2.giveN() //5
obj2.constructor(7) //14
obj2.giveN() //7
当我使用new
关键字调用multiplyByTwo
作为构造函数时,上下文,即函数中的this
会改变。它是新创建的对象。在那一刻,新对象除了this
没有其他标识符,并且与参数n
在同一个作用域。新创建的匿名函数也是如此,我将其作为属性giveN
赋值给this
。
函数multiplyByTwo
返回后,this
将被赋值给obj2
,并且它将有一个成员函数giveN
,该函数与obj2
和参数5在同一个作用域。作用域再次丢失,但当multiplyByTwo
运行时,执行上下文的活动对象被保留。我们创建了一个闭包,因为我们至少有一个指向该作用域的成员(obj2
,但obj2.giveN
也一样)的引用。
现在当我运行obj2.constructor
作为一个函数,并带有另一个参数,例如7时,我得到giveN
成员函数的返回值。为什么giveN
的返回值现在从5变为7?multiplyByTwo
作为函数是否重写了闭包中的参数n
?不完全是。
实际上,当您像这样调用multiplyByTwo
:obj2.constructor(
7)
时,它与调用multiplyByTwo(
7)
不同。您正在将该函数作为obj2
的方法调用,因此函数内的this
变为obj2
。接下来,您创建一个新的匿名函数,并将其重新赋值为obj2
的属性giveN
。您覆盖了旧属性,并且该匿名函数仅知道参数7,因为它看到了参数n
。您没有重新创建obj2
,所以您仍然在同一个活动对象上拥有旧的参数5和第一次创建的函数giveN
,但您已经丢失了对它们的引用。
示例16,更多乐趣
var t;
function multiplyByTwo(n) {
t = this.giveN;
this.giveN = function() {
return n;
}
return 2*n;
}
var obj2 = new multiplyByTwo(9);
obj2.giveN() //9
obj2.constructor(11) //22
obj2.giveN() //11
t() //9
顺便说一下,如果我像这样调用multiplyByTwo
作为普通函数,而不使用new
关键字,我将在全局对象上创建一个新属性。函数giveN
作为副作用成为全局的。一个不该做事情的好例子。
面向大众编程,而非面向类编程
Bill Venners:“但是通过委托,您的意思是不是子类化,而是这个对象委托给那个对象?”
James Gosling:“是的——没有继承层次结构。与其说类继承特别糟糕,不如说它确实有问题。”
我创建Person
构造函数的方式不是JavaScript的模式,更像是Java程序员会做的事情。
示例17
function Person(name) {
this.itsName = function() {
return name;
}
}
var obj1 = new Person("jim");
obj1.itsName() //"jim"
这是一个JavaScript的Person
构造函数。现在您拥有一个比private
更私有的name
。
嵌套函数的一个副产品是闭包,但闭包实例化���真正原因在于JavaScript函数的一等公民身份。所有其他变量遵循的规则,函数也遵循。
当您在JavaScript和许多其他语言中调用函数时,函数内部定义的局部变量,也称为自动变量,在该时刻会被实例化。一等公民功能意味着局部(嵌套)函数也以这种方式自动实例化,并且它们与局部变量共享相同的范围。这也是您希望从本文中记住的一点,所以我会重复它。每当您调用一个在其主体内部定义了嵌套函数的函数时,它们都会自动重新实例化。
这些函数与自动变量拥有相同生命周期。如果您恰好从外部函数返回一个自动(嵌套)函数到某个变量,垃圾回收器将无法从内存中处理该自动函数,因为现在您有了它的引用。您可以调用它。并且由于该嵌套函数可以访问外部函数相同范围内的所有对等项:局部变量、参数、其他局部函数和局部对象,垃圾回收器也无法处理其对等项,所以我们就有了闭包。
标准Pascal也有嵌套函数,但它没有闭包,因为函数不具备一等公民身份。也就是说,它的嵌套函数不是作为局部变量新创建的,而是在编译时创建一次。只有嵌套函数的堆栈帧在运行时创建/销毁。静态噪音…
JavaScript其他值得注意的内置构造函数包括:RegExp
、Array
、Function
…正如您所料,Function
构造函数负责创建函数。与对象创建示例一致,无论您以何种方式创建函数。
示例18
function addTwo(n) { return n+2 }
var addThree = function(n) {return n+3};
var addFour = new Function("n", "return n+4");
addTwo.constructor == addThree.constructor //true
addThree.constructor == addFour.constructor //true
addTwo.constructor == Function //true
Object.constructor == addThree.constructor //true
Array.constructor == Function //true
所有函数都是由Function
构造函数创建的。因此,Function.prototype
作为规则被插入到它创建的所有对象的[[Prototype]]
属性中,您知道他们怎么说,JavaScript中的函数也是对象。
示例19
addTwo.__proto__ == Function.prototype //true
Object.__proto__ == Function.prototype //true
Array.__proto__ == Function.prototype //true
RegExp.__proto__ == Function.prototype //true
这个对象,Function.prototype
是什么?它是一个原生函数。一段代码,它始终以其立即可执行的形式由JavaScript引擎翻译。整个JavaScript运行时中最关键的代码片段之一。正如您所见,Function.prototype
是Object.__proto__
,构造函数Object
在原型链中最直接的祖先。那么Function.__proto__
是什么?
规则是任何对象的__proto__
属性都是构造函数的prototype
属性。Function的构造函数是什么?
示例20,Catch-22
Function.constructor == Function //true
//even worse
Function.__proto__ == Function.prototype //true
最后,让我们看看构造函数的原型委托如何与内置JS构造函数Function
和Object
相关联,例如Person
:
Person --> Function.prototype --> Object.prototype --> null
.
或者:Person --> Object.__proto__ --> Object.prototype --> null
。
函数稍微更丰富,它们的原型链中有原始函数和原始对象。对象仅继承原始对象。
附录C
当程序员谈论对象时,他们在同一级别的编程语言上谈论同一件事。有低级和高级编程语言。
“一种高级编程语言是一种在很大程度上抽象了计算机细节的编程语言。”
对于C程序员来说,对象是数据类型的实例。对于高级C++程序员来说,它也是数据类型的实例,但他们更常将对象称为类的实例。
高级C++程序员知道,只有类的成员数据才会实例化。它的方法是静态的,并且在编译程序���的文本或代码段中只有一个实例。
初级C++程序员认为对象是类的实例。Java(C++的残缺版本)开发者不仅将对象视为类的实例,而且仅仅视为‘class
’关键字的产物。
我故意没有说相同级别的抽象,而是使用了“相同级别的编程语言”。更高级的编程语言应该具有更高级别的抽象。JavaScript的抽象级别高于Java,Java高于C++,C++比C提供更高的抽象级别。
抽象可以被看作是将共同的属性与特定的属性分离,但在本初级示例中,Java对象是C++对象的特例,只能使用单继承(C++有多重继承)。Java和C++对对象的定义都属于C对象的一种特例。
您必须在脑海中区分对象和类的概念。
不要欺骗自己,因为C没有‘class
’关键字,它就不能实现类。“第一台Java编译器是由Sun Microsystems开发的,是用C编写的”,“JRE是用C编写的”,“Sun的大部分Java实现-JVM-是用C编写的”。C可以实现Java的特性,但反过来却不行。:)
有很多例子,通过使用Java类的实例,您最终会使用C类的实例,尤其是在Android系统中,上述Java类只是C共享库的包装器。
如果我必须非常简短地说,我会说一个类是数据类型及其相关函数的完整定义集。所有这些都整齐地封装在一个文件中。但那只是我粗略的说法。
这是一个更深刻的陈述:“能够产生在其调用后仍然存在的块实例的过程将被称为类;实例将被称为该类的对象。” - 结构化编程(Dahl,Dijkstra和Hoare)
历史
- 2019年12月9日:初始版本