绝大部分支持面向对象的编程语言都会使用 class
关键字来定义一个类,然而 ES6 之前的 JavaScript 不知道咋想的,没有这个关键字。但是它又想实现跟类一样的功能该怎么办呢?这就用到了 原型 (prototype)。
封装、继承 和 多态 是类的三大特性,其本质上都是代码的复用,以及相同代码的基础上加上一些自己特有的东西。
在 JavaScript 中,对象 看起来就像是一个由 {}
定义的字典或者哈希,一个简单的对象并不需要我们手动去 new
它,例如 {name: 'barwe', age: 8}
就是一个 JavaScript 对象。
每个 JavaScript 对象都会有很多成员,例如简单的静态数据成员、函数和方法,以及一些其它的特殊的属性。
在对象的特殊属性中,有这样一个 [[Prototype]]
内置属性为所有对象所共有,它指向了另一个对象,其作用是:当我们尝试访问当前对象上一个不存在的成员时,解释器会自动去查找这个 [[Prototype]]
属性所指向的那个对象的成员,依次类推,直到查找到 [[Prototype]]
指向 null
的那个对象。
我们定义一个简单对象:
const x = { name: 'barwe' }
在 Firefox 中查看时,它有两个属性:
{
name: 'barwe',
<prototype>: Object;
}
其中的 <prototype>
属性就是解释器自动添加的属性,每个浏览器的显示可能存在差异。
它指向了另一个对象,我们可以通过 x.__proto__
访问到它:
// x.__proto__
Object {
__defineGetter__: function __defineGetter__()
__defineSetter__: function __defineSetter__()
__lookupGetter__: function __lookupGetter__()
__lookupSetter__: function __lookupSetter__()
__proto__: null
constructor: function Object()
hasOwnProperty: function hasOwnProperty()
isPrototypeOf: function isPrototypeOf()
propertyIsEnumerable: function propertyIsEnumerable()
toLocaleString: function toLocaleString()
toString: function toString()
valueOf: function valueOf()
<get __proto__()>: function __proto__()
<set __proto__()>: function __proto__()
}
我们可以看到,x.__proto__
属性指向的对象拥有很多方法,按照最开始约定的递归查找规则,这些属性和方法都能被 x
对象直接使用,所以我们能通过 x.toString()
将 x
转化为一个字符串。
此外,x.__proto__
指向的对象,本身也有一个 __proto__
属性,它指向了 null
,这表示递归查找到此结束了,这个 __proto__
属性指向 null
的对象就是所有对象的最终归宿。
在这个过程中,我们实际上只谈到 对象,而忽略了 类 的定义。实际上在 JavaScript 中我们可以非常轻易的创建一个对象。通过修改创建的对象的 __proto__
的指向,可以复用其指向的对象的属性和方法。修改多个对象使它们的 __proto__
属性指向同一个对象,可以让它们共享这个对象的成员。这就有点继承的味道了,但是明显有很大的缺陷,数据是共享的,修改了某个对象 __proto__
指向对象的成员,其它依赖于这个对象的子对象也会收到影响。
对于对象,我们可以通过 __proto__
属性来获取当前对象的 原型,即创建当前对象的 模板对象。
然而如果我们需要给某一类的所有对象添加一个共同的属性,例如将字符串 "true"
转换成布尔值 true
的成员方法,我们不可能去修改每个定义的字符串对象的属性,因为这个方法明显是所有该类对象共有的,我们应该将它写在字符串类的父类上。我们也不可能为每个字符串对象的 __proto__
属性指向的对象手动添加这个成员,虽然可以实现功能,但是想想就很呆:为了给类添加某个成员,却要通过对象实现。
JavaScript 为每个类提供了一个 prototype
属性,例如 String.prototype
。通过这个 prototype
属性,我们能为类指定一个原型对象,所有基于这个类实例化的对象都会自动以这个原型对象作为模板,即对象的 __proto__
属性指向这个原型对象,从而为这个类绑定了一下特殊的成员。
String.prototype.version = '1.0'
x = 'x'
x.version //=> 1.0
这样,所有的对象都通过原型对象串起来了,通过层层递进搜索,实现了代码的复用。
总之,JavaScript 企图通过 原型 来实现 类 的功能,但它有不完全同于经典类的使用方式。
评论区