基于原型的JavaScript面向对象

时间:2022-07-02 18:41:21
1、前言
从JavaScript开始流行到今天,学习它的人都会有个疑惑——JavaScript是不是面向对象语言?其实,ECMA-262早就给出了答案,在ECMAScript的第一个版本中就明确指出,ECMAScript是一种面向对象的语言,从JavaScript开始流行到今天,学习它的人都会有个疑惑——JavaScript是不是面向对象语言?其实,ECMA-262早就给出了答案,在ECMAScript的第一个版本中就明确指出,ECMAScript是一种面向对象的语言,在网上对JavaScript还有另一种评价——“JavaScript是一种基于(object-based)对象的语言”。这确实没有错,但在我认为,这是JavaScript语言的一种特性,与前面说的基于原型(prototype-based)的面向对象所不同,原型是JavaScript实现面向对象编程的一种方法,这个两个不同维度的概念,并不冲突。

2、JavaScript面向对象的基本概念

在讲基于原型的面向对象编程之前,需要先理解JavaScript三个很重要的概念:构造函数(constructor)、原型(prototype)、原型链(prototype chain)。

2.1构造函数(constructor)和原型(prototype)
与基于类的面向对象语言不同,JavaScript没有类(Class)的概念,取而代之的是构造函数(constructor)。构造函数是在实例化对象时用来初始化对象的,所有构造函数都包含一个名为“prototype”的不可枚举的属性,这个属性就是原型(prototype),JavaScript就是使用它来实现基于原型的继承以及属性共享。
同时,每个原型(prototype)对象又都包含一个名为“constructor”的不可枚举的属性,它应该始终指向到构造函数(constructor)。不论是构造函数(constructor)还是原型(prototype),都是对象。
**Note:**JavaScript的数据类型包括两类:5种原始类型和对象类型,函数(function)是一种特殊的对象。
function F() {}
alert(F.prototype.constructor === F); //true
上面为什么说“应该始终指向到构造函数(constructor)”呢?先看下面的例子:
function F() {}
F.prototype = {};
alert(F.prototype.constructor === F);  //false
alert(F.prototype.constructor === Object);  //true
在给“constructor”属性赋值之后,看起来非常的怪异,显然不太符合常理。有两种方式可以避免这个问题。

第一种,给原型对象添加一个构造函数:

function F() {}
F.prototype = {
     constructor: F,
     method1: function() {}
};
alert(F.prototype.constructor === F);  //true

第二种,使用预定义的原型对象,预定义的原型对象包含“constructor”属性,并且默认指向构造函数。
function F() {}
F.prototype.method1 = function() {};
alert(F.prototype.constructor === F); //true
面的这个问题虽然不会影响原型继承,但是很不符合逻辑,建议尽量修复这个问题。

2.2原型链(prototype chain)
理解原型链是基于原型面向对象编程中最重要的一个环节,我需要将原型链分成两部分说明。

2.2.1 使用new操作符实例化对象的原型链
__proto__是理解原型链的关键对象,每一个使用new操作符实例化的对象和函数对象都包含一个proto属性,它是构造函数“prototype”属性的引用,先看一段代码:
function Foo() {}
var foo = new Foo();
alert(foo.__proto__ === Foo.prototype);  //true,使用new运算符实例化对象的__proto__与构造函数Foo.prototype相等
在chrome控制台下能看到可访问的proto属性:
基于原型的JavaScript面向对象
通过上面这段代码,既可证明proto属性是构造函数“prototype”属性的引用。继续看一段代码:
alert(Foo.prototype.__proto__ === Object.prototype); //true
为什么上面这段代码会输出true呢?因为Foo的“prototype”属性是一个对象,Foo.prototype是一个预创建的Object类型实例,所以也会包含一个proto属性,而所有Object类型实例的proto属性都会指向到Object.prototype,所以结果输出true。
到这里原型链的脉络就比较清晰了,由于Object.prototype的proto属性指向到null,所以,foo正确的原型链如下图:
基于原型的JavaScript面向对象
Note:proto属性只有在chrome或firefox浏览器中才是公开允许访问。

2.2.2 函数(function)对象的原型链
在JavaScript中,函数(function)是一个特殊的对象,所有函数都是构造函数Function的实例,所以,函数的原型链与new操作符实例化对象的原型链会不同,先看下面代码:
function Foo() {}
alert(Foo.__proto__ === Object.prototype);  //false
alert(Foo.__proto__ === Function.prototype); //true
从上面代码可以看出,函数Foo的proto属性并不是指向到Object.prototype,而是指向到Function.prototype,这就说明函数Foo是Function的一个实例。继续看代码:
alert(Function.__proto__ === Function.prototype);  //true
alert(Function.prototype.__proto__ === Object.prototype); //true
上面代码可以看出,函数Function自己本身也是构造函数Function的一个实例,这段读起来非常拗口,看下面的图:
基于原型的JavaScript面向对象
由此可见,Object、Function、Array等等这些函数,都是构造函数Function的实例。

2.3 instanceof运算符
instanceof运算符返回一个指定的对象是否一个类的实例,格式如:A instanceof B。其中,左操作数必须是一个对象,右操作数必须是一个类(构造函数)。判断过程:如果函数B在对象A的原型链(prototype chain)中被发现,那么instanceof操作符将返回true,否则返回false。
对照上文中的原型链图,看下面的代码:

function Foo() {}
var foo = new Foo();
alert(foo instanceof Foo);  //true
alert(foo instanceof Object);  //true
alert(foo instanceof Function);  //false, foo原型链中没有Function.prototype
alert(Foo instanceof Function);  //true
alert(Foo instanceof Object);  //true
alert(Function instanceof Function);  //true
alert(Object instanceof Function);  //true
alert(Function instanceof Object); //true

**Note:**instanceof内部是通过[[HasInstance]]方法运算得到结果

这节最后,引用一张来自mollypages.org的JavaScript对象结构图:
基于原型的JavaScript面向对象