65.9K
CodeProject 正在变化。 阅读更多。
Home

Father Prototype and Mother Constructor

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2019年12月9日

GPL3

19分钟阅读

viewsIcon

8747

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

这意味着text1text2具有相同的值,逐字比较,尽管它们在计算机内存中物理上是不同的。有两个“鸟”。

更具启发性的解释是,JavaScript“使用变量对象跟踪特定作用域的变量。原始值直接存储在变量对象上”。 - 面向对象JavaScript原理

变量对象是与执行上下文相关联的特殊对象,它存储:该上下文中声明的变量、函数声明和函数形参。

这两个变量位于全局作用域,全局作用域的变量对象就是全局对象。幸运的是,当我们处于浏览器控制台中时,只需输入其名称window即可记录全局对象。您将获得window的所有属性,并且如果您将text1text2定义为“鸟”,您将看到它们两个。值相同,在全局对象中的位置不同。

如果您将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

尽管object2object1的逐字复制,但它们不被视为相等,因为它们不是按值比较的。这两个对象,就像那两个stringtext1text2一样,在计算机内存中物理上是不同的。它们具有相同的值,但这次,将比较它们的引用是否相等。当且仅当它们具有相同的地址时,引用才等于另一个。

地址不是一个复杂的概念,比如:引用、变量、值、类型等,需要通过其他概念的组合来定义。地址只是一个自然数,并且对每个对象来说都相当明确。假设在Windows操作系统的32位桌面版本中,操作系统可寻址内存的总容量为4294967296字节。这些字节编号为[0..4294967295],理论上此范围内的任何偶数值都可以是这两个对象:object1object2的有效地址。

当JavaScript运算符==比较object1object2时,它是在比较这些对象存在的地址。例如,如果object1的地址是4000,而object2的地址是777888,则==比较操作的结果将是false。当且仅当变量object1object2引用同一个内存地址(例如,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继承了大量功能,例如hasOwnPropertyisPrototypeOfpropertyIsEnumerabletoString等属性方法,即使myCar没有实现这些方法,它也可以自由使用。

但是,实现这些方法的对象是什么?那就是“原始”对象。所有默认的引用类型:对象、函数和数组都从它继承,前提是正常的原型链没有被破坏。

唉,这个默认的原型链并不以原始对象结束,它以null结束。也就是说,原始对象的[[Prototype]]属性引用null。这是完全合法的,原始值null也是一个对象

示例 6

typeof null      //"object"

“鸡生蛋”的循环!

Catch-22

我对JavaScript或任何其他事物的理解是,我正在寻找构建规则的事实。据说对象是引用类型,而null是原始类型之一,而不是引用。这打破了规则,“当机器损坏时,我们也会损坏”。

并非一切都 lost,如果您将null分配给两个变量,它们是相等的。看起来它们是通过值比较的,就像原始类型一样。还有另一种看待方式,如果这些变量引用同一个但无效的地址,例如null呢?无论哪种方式,无论您如何看待null,对象还是原始值,布尔表达式都会给您一致的结果。

nullundefinedNaN这样的东西很难理解,但我非常喜欢它们在JavaScript中的处理方式。null被视为一个对象,是一个空引用,独一无二,无法实例化任何东西。您可以将其视为一个不存在但听起来真实存在的国家,地图上没有它的位置…

在许多情况下,null被用作对象缺失的占位符,而您通常会获得一个对象。例如,通过一个应该从数据库返回对象的函数调用。最值得注意的是,null是原型链中的最后一个成员。

类似地,undefined不仅是未定义的值,而且更糟。它也是一种未定义的数据类型。NaN是类型为number的原始值,但没有数值,并且它是唯一一个不等于自身的那个值。这不仅打破了原始类型的规则,也打破了引用类型的规则。

示例 7

NaN === NaN    //false
NaN == NaN     //false

甚至,强制类型转换运算符==也无法返回true

看示例5中的myCarhisCar。我的前提是第一种情况是第二种情况的一个用例。

每个对象都有一个[[Prototype]]属性,它引用另一个对象。该属性恰好与myCarhisCar上的值相同。它引用的是同一个原始对象。

某些东西正在为myCarhisCar对象设置相同的[[Prototype]]属性。这个某些东西就是Object构造函数。

构造函数机制

JavaScript附带了一些预置的构造函数:ObjectFunctionArray…语言的某些规则依赖于这些。

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.honkmyltiplyByTwo并没有显式地为obj2obj3设置任何属性,但它们确实设置了某些东西。每个函数都有一个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

并且我可以覆盖obj4obj1name

示例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构造函数。

因此,multiplyByTwoprototypeconstructor属性被设置为函数multiplyByTwo(鸡生蛋)。因此,继承自multiplyByTwo.prototypeobj2也继承了这个属性。

示例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?不完全是。

实际上,当您像这样调用multiplyByTwoobj2.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其他值得注意的内置构造函数包括:RegExpArrayFunction…正如您所料,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.prototypeObject.__proto__,构造函数Object在原型链中最直接的祖先。那么Function.__proto__是什么?

规则是任何对象的__proto__属性都是构造函数的prototype属性。Function的构造函数是什么?

示例20,Catch-22

Function.constructor == Function            //true

//even worse

Function.__proto__ == Function.prototype    //true


最后,让我们看看构造函数的原型委托如何与内置JS构造函数FunctionObject相关联,例如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日:初始版本
© . All rights reserved.