一提起面向对象编程,首先想到的应该就是类和对象的概念。然而,作为一门使用对象的语言,JavaScript 并没有所谓的类的概念,严格来讲,JavaScript 不同于类似于 Java 或 C# 这样的面向对象的编程语言。那么,JavaScript 又是如何实现面向对象编程的一些特性的呢?本文将针对 JavaScript 对象中的原型和继承方面讨论 JavaScript 的面向对象编程特性,来揭开 JavaScript 对象的神秘面纱。

对象

我们先来说明一下,在 JavaScript 中,什么是对象。关于 JavaScript 中对对象,ECMAScript 有如下定义:

无序属性的集合,其属性可以包含基本值、对象或者函数。

从定义上可以看出,对象就相当于一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都对应到一个值(或函数)。JavaScript 中的对象似乎要更简单一些,我们可以把它想象成散列表,无非就是一组键值对,其中值可以是数据或函数。这是 JavaScript 中对象的特点,也是和其他面向对象语言所不同的地方。

创建对象

那么接下来,我们要如何获得一个对象,或者说如何创建一个对象呢?当然,在 JavaScript 中可以使用对象字面量表达式来创建一个新对象,通常这样的方式简单直观,也比较常用。但这里我们不过多讨论这种方式,而是考虑另外一种使用函数创建对象的方式。

如果你了解过面向对象编程语言,知道像 Java 这样的语言在创建对象时,使用的是 new 关键字,并通过类来创建实例对象。实际上,new 也就是表示调用类中的构造函数来创建一个对象。然而,就像前文提到过的,JavaScript 中没有类的概念,但是我们可以仿照创建对象的过程,给出构造函数的定义,在形式上通过构造函数和 new 操作符的组合,完成创建对象的任务。实际上,JavaScript 中许多内置对象就是用这种方式来获得的,比如:

这里面 Object 表示的是一个构造函数(按照约定习惯把函数名称的第一个字母大写,表示该函数是一个构造函数)。

先等一下,在进一步说明构造函数之前,我们先来看一看函数是个什么东西。

函数

在 JavaScript 中,我们对函数使用 typeof 操作符,将返回“function”。虽然如此,函数在 JavaScript 中也是一个对象,只不过它是个常用的特殊对象。好了,既然函数是一个对象,那么当我们定义一个函数的时候,函数名表示的不过是一个指向函数内部执行代码的一个指针,也就是 JavaScript 中的引用变量。记住这一点相当重要。

我们需要记住的是,函数是一个对象,函数名就是一个引用变量。因此,我们可以将函数名作为参数或返回值进行传递,同一个函数可以使用不同名称的变量表示,只需要在变量名称后加上小括号就完成了函数的调用。

关于函数的其他特性超出了本文的范畴,下面我们就要仔细研究一下,构造函数又有什么不一样的特点了。

构造函数

首先,先给出一个构造函数的例子:

嗯……这看上去确实就像是一个普通的函数,但是 this 又是什么?为什么没有返回值?不要着急,让我们一步一步来解释这个构造函数。

事实上,构造函数是一种创建对象的机制,可以说就是一种规定写法,它的内部隐藏了一些过程,所以看上去很神秘,充满了疑惑。当我们运行 new Person(…) 这行代码时,总共完成四件事情(实际上可能还要多做一点点其他额外的事情):

  • 创建一个新对象
  • 将 this 指向新对象(即 this 表示的就是创建好的新对象)
  • 执行构造函数中的代码
  • 返回新对象

所以,构造函数只是创建对象的一个语法糖,可以认为它等价于以下的代码:

注意,当使用构造函数时,一定不要忘了使用 new 操作符,只有使用 new 操作符,才会执行上述的四个步骤,完成创建对象,如果忘记 new 而直接调用 Person 函数,那么它就像调用一个普通的函数一样,仅仅简单地执行其内部的代码,而无法完成新对象的创建。这时 this 在非严格模式下将指向全局的 window 对象,而在严格模式下为 undefined,所以将会报错。

说了这么多,原型的概念在那里?我们这就说到原型了。

原型模式

首先,正如上文所说,在 JavaScript 中函数(包括构造函数,下文中提到函数,主要指的是构造函数)也是一个对象,因此它也可以拥有自己的属性和方法。而构造函数也是函数的一种,只不过可以通过 new 操作符创建对象而已。明确了这个关系以后,我们就可以提出原型的概念了。

其实原型的概念非常简单,所谓的原型,就是函数的一个属性,被称为 prototype。记住,prototype 属性只存在于函数上面,其他对象不会有这个属性。prototype 属性是一个引用变量,它指向一个对象,这个对象被称为原型对象。

如此看来,函数的原型属性和原型对象并没有特别大的关联,不过就是一个引用的关系,原型对象可以是任意一个对象。当然,在定义函数的时候,这个原型对象是由系统创建的,它上面也存在一个特殊的属性,那就是 constructor 属性,该属性则指回原型对象所在的函数,此时函数和原型对象形成一个循环引用的状态。

虽然定义函数时,原型对象已经被创建好了,但是我们依然可以通过给函数的 prototype 属性复制的方式来改变函数的原型,因此函数的原型可以是任意一个对象。

那么函数的原型和函数创建的实例又有什么关系呢?

在 JavaScript 中,所有实例对象上实际包含一个隐藏属性,所谓的隐藏属性就是系统添加的属性,一般无法访问,但多数浏览器的实现都会提供一个 _proto_ 属性用来访问这个隐藏属性。当使用函数创建实例对象的时候,将把函数的原型对象赋给实例对象的这个隐藏属性。也就是说,实例对象不仅包含自身的属性和方法,还包含一个指针,指向一个原型对象。在访问某个属性或方法时,JavaScript 首先会在实例对象中寻找,如果找到就直接返回,如果找不到,就会到原型对象中去寻找。因为函数只有一个原型对象,而通过函数创建出来的实例对象都会指向这同一个原型对象,因此,在原型对象上定义的属性和方法将被所有的实例对象所共用。

下图是一个函数、原型对象和实例对象关系的示意图。

继承

既然有了原型,我们就可以实现继承功能了。因为函数的原型对象可以随意赋值,我们只要将父类型的实例对象作为原型对象赋值给子类型的构造函数,那么所有子类型的实例对象将可以使用父类型实例对象中的所有方法了。JavaScript 就是通过原型来实现继承的。

原型链

因为原型对象本身也是一个对象,和其他的普通对象没有什么区别,因此它也有一个隐藏属性指向另外一个原型对象,另外的这个原型对象也可能指向下一个原型对象(最终将指向 Object 的原型对象),因此形成的一条链状结构就被成为原型链。通过使用对象构造原型链,就可以实现实例对象之间的继承关系。

继承的最佳实践

这里,我们用一个简单的代码示例来展示 JavaScript 是如何在实践中使用原型链进行继承的。

注意,在原型对象中的方法将成为共享的静态属性,所有子类型的实例对象将访问到同一个属性。

代码中,Child 构造函数中使用调用 Father 构造函数,目的是将所有的实例属性绑定到子类型的实例对象上,这样没个实例对象的属性都是独立的,每个实例对象具有一份,互相之间不会产生影响。

Object.create(Father.prototype) 的作用是创建一个空对象,并将它的原型对象设置为 Father.prototype。实际上同下面代码类似,只是避免创建多余的不需要的实例属性。

至此,JavaScript 中对象的原型和继承就介绍完了,只要清晰地认清原型这一概念,对 JavaScript 中对象的继承行为就会显而易见,不在产生困扰了。

小结

本文介绍了 JavaScript 中对象的概念和特点,并从创建对象入手,介绍了 JavaScript 中创建对象时最常用的构造函数和原型模式,阐述了原型的概念。

JavaScript 通过原型链,在没有类这一概念时,仍能实现面向对象编程中继承这一特性,让 JavaScript 可以同样做到子类对象继承父类对象中的属性和方法。

发表评论

电子邮件地址不会被公开。 必填项已用*标注