再探原型模式

再探原型模式

一切都是对象

在JavaScript这门语言中,获取对象的唯一途径就是克隆,而JavaScript中的根对象是Object.prototype,我们遇到的每一个对象实际上都是从这个Object.prototype克隆而来的。但我们并不需要关系克隆的细节,因为这是引擎内部负责实现的,我们只需要调用var obj1 = new Object()或者var obj2 = {},内部引擎就会从Object.prototype上克隆一个对象出来。所以可以这么说:在JavaScript中,一切都是对象。

原型模式

我们先来看一下下面这2行代码

function F() {}
var f = new F()

这段简单的代码包含了好几个概念:

  1. 构造函数
  2. prototype属性
  3. constructor属性
  4. __proto__属性
  5. new操作符

所谓的构造函数其实就是普通的函数,叫作构造函数仅仅是告诉我们:这个函数将来是被用作创建对象的。

在JavaScript中,每一个函数都有天然的拥有5个属性:length, name, arguments, caller, prototype。

image

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__ ;而在其他实现中,这个属性对脚本则是完全不可见的。

image

但是现在__proto__属性已在ES6中标准化,现在更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOf 和Object.setPrototypeOf/Reflect.setPrototypeOf来读写[[Prototype]]。

__proto__的读取器(getter)暴露了一个对象的内部 [[Prototype]] :

  1. 对于使用对象字面量创建的对象,这个值是 Object.prototype。
  2. 对于使用数组字面量创建的对象,这个值是 Array.prototype。
  3. 对于functions,这个值是Function.prototype。
  4. 对于使用 new fun 创建的对象,其中fun是由js提供的内建构造器函数之一(Array, Boolean, Date, Number, Object, String 等等),这个值总是fun.prototype。
  5. ==对于用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步:

  1. 创建一个新对象
  2. 将this指向这个新对象
  3. ==执行构造函数(为新对象添加属性)==
    (PS: 这就是为什么使用new来实现继承会导致额外的构造函数调用,戳这里见详情:额外的构造函数调用
  4. 返回这个对象

理解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的相关内容移步这里:

关于Object.create的那点事

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容