再探原型模式
一切都是对象
在JavaScript这门语言中,获取对象的唯一途径就是克隆,而JavaScript中的根对象是Object.prototype,我们遇到的每一个对象实际上都是从这个Object.prototype克隆而来的。但我们并不需要关系克隆的细节,因为这是引擎内部负责实现的,我们只需要调用var obj1 = new Object()或者var obj2 = {},内部引擎就会从Object.prototype上克隆一个对象出来。所以可以这么说:在JavaScript中,一切都是对象。
原型模式
我们先来看一下下面这2行代码
function F() {}
var f = new F()
这段简单的代码包含了好几个概念:
- 构造函数
- prototype属性
- constructor属性
- __proto__属性
- new操作符
所谓的构造函数其实就是普通的函数,叫作构造函数仅仅是告诉我们:这个函数将来是被用作创建对象的。
在JavaScript中,每一个函数都有天然的拥有5个属性:length, name, arguments, caller, prototype。
PS: 几个获取对象属性名的API
Object.keys(obj) // 获取obj的属性名(无法获取Symbol属性) Object.getOwnPropertyNames(obj) // 获取obj的属性名(无法获取Symbol属性) Object.getOwnPropertySymbols(obj) // 获取obj的Symbol属性名 Reflect.ownKeys(obj) // 获取obj所有类型的键名,包括常规键名和 Symbol 键名。
函数的prototype属性指向一个对象,这个对象在创建函数的时候自动生成,并且拥有一个constructor属性,constructor属性指向这个函数。
在上面的那2行代码中,f是构造函数F()生成的对象,通过设置构造函数的prototype实现原型继承的时候,除了根对象Object.prototyp,任何对象都有一个内部属性[[Prototype]],它指向这个构造函数的原型
《javaScript高级程序设计(第三版)P148》
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性) ,指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫 [[Prototype]] 。虽然在脚本中没有标准的方式访问 [[Prototype]] ,但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性__proto__ ;而在其他实现中,这个属性对脚本则是完全不可见的。
但是现在__proto__属性已在ES6中标准化,现在更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOf 和Object.setPrototypeOf/Reflect.setPrototypeOf来读写[[Prototype]]。
__proto__的读取器(getter)暴露了一个对象的内部 [[Prototype]] :
- 对于使用对象字面量创建的对象,这个值是 Object.prototype。
- 对于使用数组字面量创建的对象,这个值是 Array.prototype。
- 对于functions,这个值是Function.prototype。
- 对于使用 new fun 创建的对象,其中fun是由js提供的内建构造器函数之一(Array, Boolean, Date, Number, Object, String 等等),这个值总是fun.prototype。
- ==对于用js定义的其他js构造器函数创建的对象,这个值就是该构造器函数的prototype属性。==(这是最常见的形式)
__proto__ 的设置器(setter)允许对象的 [[Prototype]]被变更。前提是这个对象必须通过 Object.isExtensible(): 进行扩展,如果不这样,一个 TypeError 错误将被抛出。要变更的值必须是一个object或null,提供其它值将不起任何作用。
默认情况下,对象是可扩展的:即可以为他们添加新的属性。以及它们的__proto__ 属性可以被更改。Object.preventExtensions,Object.seal 或 Object.freeze 方法都可以标记一个对象为不可扩展(non-extensible)。
// 新对象默认是可扩展的. var empty = {}; Object.isExtensible(empty); // === true // ...可以变的不可扩展. Object.preventExtensions(empty); Object.isExtensible(empty); // === false // 密封对象是不可扩展的. var sealed = Object.seal({}); Object.isExtensible(sealed); // === false // 冻结对象也是不可扩展. var frozen = Object.freeze({}); Object.isExtensible(frozen); // === false
那么new操作符做了什么事情呢?通过new调用构造函数实际上经历了4步:
- 创建一个新对象
- 将this指向这个新对象
- ==执行构造函数(为新对象添加属性)==
(PS: 这就是为什么使用new来实现继承会导致额外的构造函数调用,戳这里见详情:额外的构造函数调用) - 返回这个对象
理解new操作符
为了理解new操作符,我们自己动手模拟一个new(或者说,假如没有new,我们该如何实现创建对象?)
function Point(x, y) {
this.x = x
this.y = y
}
Point.prototype.getLength = function () {
let {x, y} = this
return Math.sqrt(x * x + y * y)
}
function defineClass(initializer) {
return function f(...args) {
f.prototype = initializer.prototype // 确保instanceof正确
let obj = Object.create(initializer.prototype) //创建一个新队对象
initializer.apply(obj, args) // 将this指向这个对象,并执行构造函数
return obj // 返回这个对象
}
}
var p1 = defineClass(Point)(3, 4)
var p2 = new Point(5, 12)
console.log([p1.x, p1.y, p1.getLength(), p1 instanceof Point])
console.log([p2.x, p2.y, p2.getLength(), p2 instanceof Point])
// [3, 4, 5, true]
// [5, 12, 13, true]
其他
Object.create
这里用到了Object.create。关于Object.create的相关内容移步这里: