ECMAScript 使用 原型模式 来实现多个对象共享某个对象的所有属性的操作。
当创建一个函数对象时,会为函数对象创建一个属性 prototype 引用一个对象,被称为 原型对象 ,而原型对象会有一个属性 constructor 引用该函数对象,即函数和原型之间相互引用,形成循环引用。
对象拥有一个内部特性 [[Prototype]] ,是一个指针,指向某个原型对象。
通过对象可以直接访问原型的属性。
在访问对象的属性时,会根据属性名搜索属性,首先在当前对象中进行搜索,如果没有找到属性,就会通过内部特性 [[Prototype]] 找到对象的原型,在对象的原型中继续进行搜索。
使用同一个原型的多个对象会共享该原型的属性。
示例:
- 函数的属性 prototype 与原型的属性 constructor 。
function func() {} const prototype = func.prototype, // 函数的原型 constructor = prototype.constructor // 函数原型的属性 constructor console.log('prototype:', prototype) console.log('constructor:', constructor) console.log('constructor === func :', constructor === func) // 输出: // prototype: {constructor: ƒ} // constructor: ƒ func() {} // constructor === func : true
主要参考资料:
- 《JavaScript 高级程序设计(第4版)》
- P221(246/931)
- P224(249/931)
操作符 new 与构造函数
ECMAScript 使用操作符 new 和构造函数来实现模板化创建对象的操作。
构造函数提供模板。
操作符 new 根据提供的模板创建实例对象。
操作符 new
使用操作符 new 调用函数,会执行如下操作:
- 在内存中创建一个新对象。
- 新对象的内部特性 [[Prototype]] 被赋值为函数的属性 prototype 。
- 函数的对象 this 被赋值为新对象。
- 执行函数的函数体。
- 如果函数有非空的返回值,则返回函数的返回值,否则返回函数的对象 this 。
示例:
function Entity() {
console.log('execute Entity()')
}
const entity = new Entity() // 使用操作符 new 调用函数
console.log('entity:', entity)
// 输出:
// execute Entity()
// entity: Entity {}
构造函数
使用操作符 new 调用的函数,被称为 构造函数 。
ECMAScript 的构造函数是被设计用于创建实例对象的普通函数。
构造函数与普通函数在本质上、语法上没有任何区别。
构造函数的函数名一般采用首字母大写的形式。
在构造函数中,可以使用对象 this 为使用该构造函数创建的对象添加属性。
示例:
- 在构造函数中,使用对象 this 为使用该构造函数创建的对象添加属性。
function Entity() {
this.attrEntity_01 = '1948' // 为使用该构造函数创建的对象添加属性。
this.attrEntity_02 = '2017'
}
const entity = new Entity()
console.log('entity:', entity)
// 输出:
// entity: Entity {attrEntity_01: '1948', attrEntity_02: '2017'}
函数的内部属性 new.target
函数拥有一个内部属性 new.target ,值为布尔值,表示调用函数时是否使用了操作符 new 。
示例:
- 函数的内部属性 new.target
function Test() { if(new.target) { console.log('invoked with new') } else { console.log('invoked commonly') } } const test = new Test() Test() // 输出: // invoked with new // invoked commonly
对象的属性 constructor
通过对象可以访问对象原型的属性 constructor ,引用创建该对象时使用的构造函数。
示例:
- 对象的属性 constructor
function Entity() { console.log('instantiate Entity') } const entity = new Entity() console.log('entity.constructor:', entity.constructor) // 对象的属性 constructor // 输出: // instantiate Entity // entity.constructor: ƒ Entity() { //---- console.log('instantiate Entity') //-- }
实例对象
使用操作符 new 调用函数创建一个新对象的过程,被称为 实例化 。
使用构造函数创建的对象,被称为 实例对象 。
在实例对象上添加与原型属性同名的属性,会 遮蔽(shadow) 相应的原型属性,屏蔽对原型属性的访问,但不会覆盖原型属性。
示例:
- 在实例对象上添加与原型属性同名的属性,遮蔽相应的原型属性。
function Entity() {} const prototype = Entity.prototype prototype.attr_01 = 'prototype attr_01' prototype.attr_02 = 'prototype attr_02' const instance = new Entity() instance.attr_01 = 'instance attr_01' // 添加与原型的属性同名的属性 console.log('prototype:', prototype) console.log('instance:', instance) console.log('instance.attr_01:', instance.attr_01) console.log('instance.attr_02:', instance.attr_02) // 输出: // prototype: {attr_01: 'prototype attr_01', attr_02: 'prototype attr_02', constructor: ƒ} // instance: Entity {attr_01: 'instance attr_01'} // instance.attr_01: instance attr_01 // instance.attr_02: prototype attr_02
ECMAScript 没有提供访问对象内部特性 [[Prototype]] 的标准方式。
但一些浏览器会在对象添加一个属性 __proto__ 用于访问对象的原型,例如:Firefox、Safari、Chrome 等。
示例:
- 使用浏览器提供的属性 __proto__ 访问对象的原型。
function Entity() { this.attr = '1948' } const entity = new Entity() console.log('entity.__proto__:', entity.__proto__) // 使用属性 __proto__ 访问对象的原型 // 输出: // entity.__proto__: {constructor: ƒ}
可以通过对属性名使用操作符 in ,判断对象是否可以访问指定的属性。
示例:
- 对属性名使用操作符 in ,判断对象是否可以访问指定的属性。
function Entity() { this.instAttr = '2017' } Entity.prototype.protoAttr = '2021' Entity.entityAttr = '2035' const entity = new Entity() console.log('protoAttr in entity:', 'protoAttr' in entity) // 判断对象是否可以访问指定的属性 console.log('instAttr in entity:', 'instAttr' in entity) console.log('entityAttr in entity:', 'entityAttr' in entity) console.log('entityAttr in Entity:', 'entityAttr' in Entity) // 输出: // protoAttr in entity: true // instAttr in entity: true // entityAttr in entity: false // entityAttr in Entity: true
图示:
- 原型模式:“原型 - 构造函数 - 实例”
动态设置构造函数的属性 prototype
在动态设置构造函数的属性 prototype 时,可以使用对象字面量对构造函数的属性 prototype 进行赋值。
但使用对象字面量创建对象时,实际调用的是 new Object() ,所以使用对象字面量创建的对象的原型的属性 constructor 是 Object() 。
当将该对象作为构造函数的属性 prototype 时,意味着构造函数原型的属性 constructor 是 Object()。
如果希望修正构造函数的原型的属性 constructor ,可以使用对象字面量创建对象时,为对象添加自身的属性 constructor 来遮蔽对象的原型的属性 constructor 。
但使用对象字面量定义的属性 constructor 的内部特性 [[Enumerable]] 为 true ,而 ECMAScript 默认原型的属性 constructor 是不可枚举的。
如果希望遵循原生的属性 constructor 的设定,则需要单独使用方法 Object.defineProperty() 来重新定义构造函数原型的属性 constructor 。
示例:
-
使用对象字面量对构造函数的属性 prototype 进行赋值,并遮蔽原型的属性 constructor 。
function Entity() { this.instanceAttr = '1948' } Entity.prototype = { constructor: Entity, // 遮蔽原型的属性 constructor prototypeAttr: '2017' } // 使用对象字面量对构造函数的属性 prototype 进行赋值 const entity = new Entity() console.log('Entity.prototype:', Entity.prototype) console.log('Entity.prototype.constructor:', Entity.prototype.constructor) console.log('entity:', entity) console.log('entity.prototypeAttr:', entity.prototypeAttr) // 输出: // Entity.prototype: {prototypeAttr: '2017', constructor: ƒ} // Entity.prototype.constructor: ƒ Entity() // entity: Entity {instanceAttr: '1948'} // entity.prototypeAttr: 2017
-
使用对象字面量对构造函数的属性 prototype 进行赋值,并使用方法 Object.defineProperty() 重新定义构造函数原型的属性 constructor 。
function Entity() { this.instanceAttr = '1948' } Entity.prototype = { prototypeAttr: '2017' } // 对构造函数的属性 prototype 进行赋值 Object.defineProperty( Entity.prototype, 'constructor', { configurable: true, enumerable: false, writable: false, value: Entity } ) // 重新定义构造函数原型的属性 constructor const entity = new Entity() console.log('Entity.prototype:', Entity.prototype) console.log('Entity.prototype.constructor:', Entity.prototype.constructor) console.log('entity:', entity) console.log('entity.prototypeAttr:', entity.prototypeAttr) // 输出: // Entity.prototype: {prototypeAttr: '2017'} // Entity.prototype.constructor: ƒ Entity() // entity: Entity {instanceAttr: '1948'} // entity.prototypeAttr: 2017
主要参考资料:
- 《JavaScript 高级程序设计(第4版)》- P234(259/931)
相关 API
对象的方法 isPrototypeOf()
对象的方法 isPrototypeOf() :
-
功能:
判断指定对象的内部特性 [[Prototype]] 是否指向当前对象。 -
接收一个参数:
对象,目标对象。 -
返回值:
布尔值,目标对象的内部特性 [[Prototype]] 是否指向当前对象。
示例:
- 使用对象的方法 isPrototypeOf() ,判断指定对象的内部特性 [[Prototype]] 是否指向当前对象。
function Entity() {} const entity = new Entity() console.log('Entity.prototype is protoype of entity:', Entity.prototype.isPrototypeOf(entity)) // 判断对象的内部特性 [[Prototype]] 的指向 // 输出: // Entity.prototype is protoype of entity: true
Object.getPrototypeOf()
方法 Object.getPrototypeOf() :
-
功能:
获取指定对象的内部特性 [[Prototype]] 指向的原型。 -
接收一个参数:
对象,目标对象。 -
返回值:
对象,目标对象的内部特性 [[Prototype]] 指向的原型。
示例:
- 使用方法 Object.getPrototypeOf() ,获取指定对象的内部特性 [[Prototype]] 指向的原型。
function Entity() {} Entity.prototype.type = 'Entity' const entity = new Entity() console.log('protoype of entity:', Object.getPrototypeOf(entity)) // 获取对象的内部特性 [[Prototype]] 指向的原型 // 输出: // protoype of entity: {type: 'Entity', constructor: ƒ}
Object.setPrototypeOf()
方法 Object.setPrototypeOf() :
-
功能:
动态设置指定对象的内部特性 [[Prototype]] 。 -
接收两个参数:
-
对象
目标对象。 -
对象
目标对象的内部特性 [[Prototype]] 需要指向的对象。
-
-
返回值:
对象,动态设置内部特性 [[Prototype]] 后的目标对象。
但不建议在编程过程中重写实例的原型,因为在重写实例的原型之前,可能已经在很多地方使用了该实例,如果此时重写实例的原型,将会影响此前所有引用该实例的代码,造成严重的性能问题。
示例:
- 使用方法 Object.setPrototypeOf() ,动态设置指定对象的内部特性 [[Prototype]] 。
function EntityA() {} EntityA.prototype.type = 'EntityA' function EntityB() {} EntityB.prototype.type = 'EntityB' const entity = new EntityA() Object.setPrototypeOf(entity, EntityB.prototype) // 动态设置指定对象的内部特性 [[Prototype]] console.log('protoype of entity:', Object.getPrototypeOf(entity)) // 输出: // protoype of entity: {type: 'EntityB', constructor: ƒ}
Object.create()
方法 Object.create() :
-
功能:
创建一个内部特性 [[Prototype]] 指向指定对象的新对象。 -
接收两个参数:
-
对象
目标对象。 -
对象(可选)
属性定义对象,为新对象定义的属性。
-
-
返回值:
对象,一个内部特性 [[Prototype]] 指向目标对象的新对象。
示例:
- 使用方法 Object.create() ,创建一个内部特性 [[Prototype]] 指向指定对象的新对象。
const prototypeObj = { prototypeAttr: '1948' }, definePropertyObj = { instanceAttr: { configurable: true, enumerable: true, writable: true, value: '2017' } }, // 属性定义对象 obj = Object.create(prototypeObj, definePropertyObj) // 创建一个内部特性 [[Prototype]] 指向指定对象的新对象 console.log('obj:', obj) console.log('obj.prototypeAttr:', obj.prototypeAttr) console.log('prototypeObj is protoype of obj:', prototypeObj.isPrototypeOf(obj)) // 输出: // obj: {instanceAttr: '2017'} // obj.prototypeAttr: 1948 // prototypeObj is protoype of obj: true
对象的方法 hasOwnProperty()
对象的方法 hasOwnProperty() :
-
功能:
判断当前对象是否存在指定属性名的属性。 -
接收一个参数:
字符串 | 符号,属性名。 -
返回值:
布尔值,当前对象是否存在指定属性名的属性。
示例:
- 使用对象的方法 hasOwnProperty() ,判断当前对象是否存在指定属性名的属性。
function Entity() { this.instAttr = '2045' } Entity.entityAttr = '2049' const entity = new Entity() console.log('entity has instAttr attribute:', entity.hasOwnProperty('instAttr')) // 判断当前对象是否存在指定属性名的属性 console.log('entity has entityAttr attribute:', entity.hasOwnProperty('entityAttr')) console.log('Entity has entityAttr attribute:', Entity.hasOwnProperty('entityAttr')) // 输出: // entity has instAttr attribute: true // entity has entityAttr attribute: false // Entity has entityAttr attribute: true