Web编程技术营地
研究、演示、创新

Prototype

撰写时间:2023-05-30

修订时间:2025-12-08

构造器

初识构造器

在JavaScript中,构造器constructor)是用以创建类的实例的函数。

let a = 25; pc.log(a.constructor);

上面代码查看变量a的名为constructor的属性值。在终端将显示:

function Number() { [native code] }

关系示意图:

digraph { rankdir = LR; subgraph clusterVariable { label="num"; node [shape=record]; constructor [label="constructor" fillcolor="#555" fontcolor="#999"]; } subgraph clusterConstructor { labelloc=t node [label=""]; colorscheme=rdbu11 color="#33669900"; fontcolor=4; bgcolor="#3A3A3A"; subgraph cluster_Number { Number [shape=box3d]; label="Number\nconstructor"; } subgraph cluster_function { function [shape=component]; label="constructor\nfunction"; } } constructor -> Number [label="reference" style=dashed color=gray fontcolor=gray]; Number -> function [label="is" arrowhead=inv style=dashed]; }

即,使用了Number这个构造器来创建了一个值为25的整数的实例。

在其它编程语言中,我们从类创建对象实例。但在JavaScript中,函数是最重要的一等公民,我们从函数创建对象实例。能创建对象实例的函数,就称为构造器。

因此,像上面一样,以后当我们看到一个函数,其函数名是大写的JavaScript内置的数据类型,我们就应当清醒地认识到,这不是一个普通的函数,而是一个能创建对象实例的构造器。

再探构造器

let date = new Date(); console.log(date);

显示:

Thu Nov 28 2024 15:50:07 GMT+0800 (中国标准时间)

Date是一个类,而dateDate这个类的一个实例。这是我们从其他编程语言中得到的普遍共识。

但在JavaScript中,Date是什么?

console.log(Date);

终端显示:

function Date() { [native code] }

当我们使用console.log(Date)来查看Date时,从C语言, Python语言一路走来的我们会想当然地认为,这是一个名为Date的类。但正如上面所见,Date不是一个类,而是构造器,一个可以创建对象实例的函数。

查看date这个实例的构造器:

console.log(date.constructor);

终端显示:

function Date() { [native code] }

两者是否为同一对象?

console.log(date.constructor === Date); // true

变量date的构造器就是Date,两者为同一对象。

综上,当我们编写如下代码:

console.log(Boolean); console.log(String); console.log(Array); ...

时,我们所看到的,就是各种用以创建相应实例的构造器。

终极构造器

let date = new Date(); console.log(date.constructor); // Date

变量date的构造器为Date,后者呢?它有没有相应的构造器?

console.log(Date.constructor);

终端显示:

function Function() { [native code] }

说明Date的构造器为Function。也即Function构造器先创建了Date构造器,然后再通过后者来创建各个相应与日期有关的实例。

查看Object的构造器:

console.log(Object.constructor); // Function

就连根对象Object,其构造器也为Function

Function呢?既然你这么牛,你的构造器又是哪个?

console.log(Function.constructor); // Function console.log(Function.constructor === Function); // true

不用再找了,Function就是传说中的终极构造器,它甚至是它自己的构造器!

神通广大的孙悟空,绝非凡人所生,因此它得从一块仙石中蹦出来。你能想像,孙悟空将它自己生下来吗?孙悟空做不到,而Function却可以做得到!Function简直就是宇宙第一推动力啊!

通过字面符创建对象

当我们看到:

let data = new Date();

的代码时,很清楚,显式地使用了Date构造器来创建实例。

而对于以下代码:

let a = 5; let b = "abc"; let c = {name: 'Mike'}; console.log(a.constructor); // Number console.log(b.constructor); // String console.log(c.constructor); // Object

则是通过字面符literal notation)的方式来创建对象实例。在其内幕,通过调用了相应的构造器来创建对象实例。

构造器的属性

构造器有两个重要属性,一个是name,返回构造器的字符串表示;另一个是prototype,下面详细谈到。

pc.log(Number.name);

Prototype

遍历自身属性

遍历自身属性分为2种情况,一是遍历特定类的属性,二是遍历一个实例的属性。

遍历特定类的属性时,使用Object.getOwnPropertyNames方法来遍历。

let propNameArray = Object.getOwnPropertyNames(Object); pc.log(propNameArray);

遍历一个实例的属性时,使用Object.getOwnPropertyNames方法或for ... in语句来遍历。

let obj = { name: 'Mike', age: 25 }; // method 1 let propNameArray = Object.getOwnPropertyNames(obj); pc.log(propNameArray); // method 2 for (let propName in obj) { pc.log(propName); }

Object.getOwnPropertyNames包括不可列举的所有自身属性,但不包括链路中的属性;for ... in语句可遍历所有链路中的属性,但仅可遍历可列举的属性。

Object构造器

Object所有自身属性

Object是创建对象的顶层构造器,下面列出其所有的自身属性:

let propNameArray = Object.getOwnPropertyNames(Object); const groupedObj = Object.groupBy(propNameArray, (element) => { return typeof Object[element] === 'function'; }); pc.group('functions'); for (let funcName of groupedObj[true].sort()) { pc.log('%s', funcName); } pc.groupEnd(); pc.group('other properties'); for (let propName of groupedObj[false].sort()) { if (propName !== 'prototype') { let typeSpecifier = typeof Object[propName] === 'number' ? '%d' : '"%s"'; pc.log(`%s: ${typeSpecifier}`, propName, Object[propName]); } else { pc.log('%s', propName); } } pc.groupEnd();

Object的prototype属性

Objectprototype属性是一个对象。下面列出该对象的所有属性。

pc.log(Object.prototype);

可以看到,当我们创建一个特定对象时,这个对象将自动共享Objectprototype对象的所有属性,包括我们比较熟悉的toString方法在内。

let obj = { name: 'mike', age: 25 }; pc.log(obj.toString()); pc.log(obj.hasOwnProperty('name'));

需指出的是,不是每个对象都有prototype属性,只有构造器才有此属性。如前所述,Object是一个创建任意对象实例的顶层构造器。

构造器的prototype对象有一个constructor属性,指向构造器自己。

pc.log(Object.prototype.constructor === Object);

prototype链

JavaScript通过prototype链来实现对象的继承。

具体细节是,每个对象都由一个特定的构造器来创建。每个构造器都有一个prototype属性。而prototype属性所指向的对象中,又有一个__proto__属性,指向父级构造器的prototype,从而形成一个prototype链。

作为最顶层的构造器,Object.prototype.__proto__的值为null,表示上面没有最高层的构造器了。

pc.log(Object.prototype.__proto__);

当我们以字面符方式来创建一个对象时,创建该对象的构造器是Object

let obj = { name: 'Mike', age: 25 }; pc.log(obj);

此时,prototype链中只有一级构造器。即objObject构造器创建。

从上面输出结果来看,若从其他编程语言的角度,我们可以说objObject的子类,继承了ObjecttoString等方法。但在JavaScript中,Object只不过是obj的构造器,由所创建的对象实例所共享的属性都全部统一置于prototype属性中。

从对象实例obj的角度,我们可以获知创建该对象是哪个构造器。

let obj = { name: 'Mike', age: 25 }; pc.log(obj.constructor); pc.log(obj.constructor === Object); pc.log(obj.constructor.name);

我们可以调用Object.getPrototypeOf,直接获取创建该对象实例的构造器的prototype

let obj = { name: 'Mike', age: 25 }; pc.log(Object.getPrototypeOf(obj)); pc.log(Object.getPrototypeOf(obj) === Object.prototype); pc.log(Object.getPrototypeOf(obj) === obj.constructor.prototype);

上面谈到,Objectprototype属性所指向的对象是专供子类继承的,而该对象中又有一个__proto__属性,因此,任何由Object所创建的实例都因继承的原因而也有一个__proto__属性。该属性值与Object.getPrototypeOf的返回值是同一对象。

let obj = { name: 'Mike', age: 25 }; pc.log(obj.__proto__); pc.log(obj.__proto__ === Object.getPrototypeOf(obj)); pc.log(obj.__proto__ === Object.prototype);

下面看有多级构造器的情况。

let date = new Date(); pc.log(date.__proto__); pc.log(date.__proto__.__proto__);

变量date的构造器是DateDate的构造器是Object。从而形成了一个两级的prototype链。

下面以Number为例来查看各个对象的关系图:

let num = 5; pc.log(num);

图示:

--- config: class: hideEmptyMembersBox: true --- classDiagram direction LR class Object ["Object (constructor)"]:::constructor { + name : String + prototype: Object + \_\_proto__: null } class Object_prototype ["Object Prototype Object"]:::prototype { + constructor : Object } class Number ["Number (constructor)"]:::constructor { + name : String + prototype: Object + \_\_proto__: Object } class Number_prototype ["Number Prototype Object"]:::prototype { + constructor : Number } class num { + constructor: Number } Object "1" --> "1" Object_prototype : prototype Object_prototype "1" --> "1" Object : constructor Number "1" --> "1" Number_prototype : prototype Object "1" ..|> "many" Number : initialize Number "1" --> "1" Object_prototype : \_\_proto__ Number "1" ..|> "many" num : initialize classDef constructor fill:rgb(60,60,200) classDef prototype fill:gray

自定义prototype链

可直接将实例对象的__proto__属性值指向另一对象,从而改变原有的prototype链。

let protoObj = { getName() { return this.name; }, greet() { pc.log(`Hi, I'm ${this.getName()}.`); } }; let obj = { name: 'Mike' }; obj.__proto__ = protoObj; pc.log(obj); obj.greet();

对于代码:

obj.__proto__ = protoObj;

JavaScript引擎将做以下3件事:

  1. 使用Object这个构造器来创建一个新的构造器。
  2. 将新构造器的prototype属性设置为protoObj
  3. 使用新构造器来创建obj实例。

Object.create()

可以通过Object.create方法以在创建对象时指定对象的原型。

let protoObj = { getName() { return this.name; }, greet() { pc.log(`Hi, I'm ${this.getName()}.`); } }; let obj = Object.create(protoObj); obj.name = 'John'; pc.log(obj); obj.greet();

可以在创建对象时同时设置对象的各个属性。

let protoObj = { getName() { return this.name; }, greet() { pc.log(`Hi, I'm ${this.getName()}.`); } }; let obj = Object.create(protoObj, { name: { value: 'John' }, age: { value: 25 } } ); pc.log(obj); obj.greet();

2个参数是一个称为属性描述器的对象。在该对象中,各个键名是字符串,键值是一个描述器对象 (descriptor object)。上面为nameage两个属性分别指定了属性值。

实现自定义构造器

可以创建自定义构造器,并通过设置其prototype属性以让所创建的实例共享相应属性。

function MyObj(name, age) { this.name = name; this.age = age; }; MyObj.prototype.address = "default address"; MyObj.prototype.add = function (x, y) { return x + y; }; let obj = new MyObj('sarkuya', 25); pc.log(obj); pc.log(obj.address);

在构造器的prototype内的属性,均可被所创建的对象访问。因此,上面代码中,add, address,以及Objectprototype属性内的所有属性及方法,均可被所创建的对象访问。而nameage只能被obj自己访问。

这就是JavaScript判断对象是否具有相应属性名称的机制。简而言之,它只需要检查该对象构造器的prototype的属性,并按从近到远的方式来确定。

了解此点后,作为开发人员,只需把握一条:将需要各个对象实例所共享的属性放在构造器的prototype的属性中即可。至于跟踪繁杂的prototype链,这是JavaScript内部操作的问题,从而大大减轻了开发人员的工作量。

参考资源

  1. ECMA262 Objects
  2. Native prototypes
  3. Inheritance and the prototype chain
  4. Object.defineProperties()