简要分析JavaScript基于原型模式的面向对象

440 阅读6分钟

前言

最近在学习设计模式,正好看到了原型模式,解开了我以前的一些疑惑。也趁着复习的积极性,来回顾和加深一下JavaScript基于原型模式的面向对象机制。

原型模式

首先得弄清楚一些概念,JavaScript是没有类的概念的,对是没有的。JavaScript是一门基于原型模式的语言,什么是原型模式?先来看一个例子:

var Person = function(name) {
	this.name = name;
}
let tom = new Person('tom');

以上例子中,如果熟悉面向对象的同学很快就会发现,这是一个类的经典继承例子,但准确来说,在JavaScript,Person不是一个类,是一个函数构造器,也称为构造函数。 而且是通过原型链实现继承的。

对象的定义

得先看一下ECMAScript中关于对象的定义:

An object is a collection of properties and has a single prototype object. The prototype may be either an object or the null value.

Object是一个属性的集合,并且都拥有一个单独的原型对象[prototype object]. 这个原型对象[prototype object]可以是一个object或者null值。

意思就是每一个对象拥有一个原型对象,我们将它称为对象的原型,一个对象的原型是一个内部的[[prototype]]属性的引用。而浏览器厂商对这个属性的实现通常是__proto__,通过__proto__可以找到对象的原型对象。 我们可以还通过手动设置__proto__来调整对象的原型。

let foo = {
  x: 10,
  y: 20
};

可以看到foo有两个显式属性x和y,还有一个__proto__,指向它的原型对象。

对象是怎么创建的

在基于原型模式的语言系统中,有以下的特点,JavaScript也是如此:

要得到一个对象,不是通过实例类,而是找到一个对象作为一个原形并克隆它

实际上,JavaScript中的根对象是Object.prototype对象,所有的对象都是从Object.prototype对象克隆而来的,Object.prototype就是它们的原型。

let obj1 = new Object();
let obj2 = {}

// 利用ES5的Object.getPrototypeOf获得对象的原型
console.log(Object.getPrototypeOf(obj1) === Object.prototype) //true
console.log(Object.getPrototypeOf(obj2) === Object.prototype) //true

当我们调用new或者字面量创建一个对象的时候,内部引擎都会从Object.prototype上克隆一个对象出来,同时做了一些额外的处理,我们不需要关心这些细节,但可以通过一些例子验证这个结论。

现在看回文章开头的例子,调用new Person创建了一个对象tom,熟悉new的过程的同学会发现,在new的内部,首先是创建了一个新的对象,这个对象就是从Object.prototype上克隆而来的, 然后再改变新的对象的原型对象,改为Person.prototype,如果成功返回这个新对象,这个新对象就是tom。

所以tom的原型对象是Person.prototype

console.log(Object.getPrototypeOf(tom) === Person.prototype) //true

那么为什么要引入原型对象这个概念,这里又要引出一个概念:原型链

原型链的定义

ECMAScript中关于原型链(Prototype chain)的定义:

A prototype chain is a finite chain of objects which is used to implemented inheritance and shared properties. 原型链是一个由对象组成的,用于实现继承和共享属性的有限链

通俗来讲,原型对象也是普通的对象,并且也有可能有自己的原型,如果一个原型对象的原型不为null的话,我们就称之为原型链(prototype chain)。

在基于类[class-based]的系统中,重用相同的方法或者属性叫做类的继承,而原型链的设计也是为了重用代码,通过原型链实现的继承称为基于委托的继承 (delegation based inheritance),也叫做原型继承。

为什么说是基于委托的继承呢?看下面的例子

let a = {
	x: 10,
	print: function() {
		return this.x + this.y;
	}
};

let b = {
	y: 20,
	__proto__: a
}

b.print() // 30

以上的例子中,调用b.print()会执行以下过程:

  1. 首先尝试遍历对象b中的所有属性,但没有找到print方法
  2. 将查找print的请求委托给对象b的原型,在上面的例子中手动设置了b的__proto__为a,所以b的原型是a对象
  3. 在a对象中遍历查找print方法,如果找到了第一个print方法,返回它
  4. 如果在a中也没有找到print方法,会将请求委托给a的原型对象,重复这个过程直到遍历完整个原型链(null),如果没有找到,返回undefined

值得注意的是,当b调用print函数的时候,this是指向b的,所以y是20,而b中没有x,this.x是从原型链上查找的,也就是a的10,所以才会打印出30。

构造函数

构造函数(constructor) 做了一件有用的事情,自动为创建的新对象设置了原型对象(prototype object) 。原型对象存放于 ConstructorFunction.prototype 属性中。

let obj = new Object();
obj.__proto__ === Objcet.prototype; // true;

别忘了,Object也是一个构造函数,typeof Object输出的应该是"function"

// 构造函数
function Foo(y) {
  // 构造函数将会以特定模式创建对象:被创建的对象都会有"y"属性
  this.y = y;
}
 
// "Foo.prototype"存放了新建对象的原型引用
// 所以我们可以将之用于定义继承和共享属性或方法
// 所以,和上例一样,我们有了如下代码:
 
// 继承属性"x"
Foo.prototype.x = 10;
 
// 继承方法"calculate"
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};
 
// 使用foo模式创建 "b" and "c"
var b = new Foo(20);
var c = new Foo(30);
 
// 调用继承的方法
b.calculate(30); // 60
c.calculate(40); // 80
 
// 让我们看看是否使用了预期的属性
 
console.log(
 
  b.__proto__ === Foo.prototype, // true
  c.__proto__ === Foo.prototype, // true
 
  // "Foo.prototype"自动创建了一个特殊的属性"constructor"
  // 指向a的构造函数本身
  // 实例"b"和"c"可以通过授权找到它并用以检测自己的构造函数
 
  b.constructor === Foo, // true
  c.constructor === Foo, // true
  Foo.prototype.constructor === Foo // true
 
  b.calculate === b.__proto__.calculate, // true
  b.__proto__.calculate === Foo.prototype.calculate // true
 
)

构造函数本身也是一个对象,所以构造函数也有自己的原型,也就是__proto__属性,一般指向Function.prototype,而Function.prototype的原形是Object.prototype. 以上例子可以用这个图描述。

再来分析一个例子,通常我们想要实现一个“类“继承另一个“类“的时候,在JS中通常这么写:

let A = function() {};
A.prototype.name = 'A'
let a = new A();

let B = function() {};
B.prototype = a;

let b = new B();
console.log(b.name); // A;

new A后,返回一个对象a,a.__proto__ === A.prototype // true,将B.prototype设置为a,所以B.prototype.__proto__ === A.prototype // truenew B后,返回一个对象b,b.__proto__ === B.prototype // true,则b.__proto__.__proto__ === A.prototype // true,实现了原型链的连接

图我就不画了,懒。

多提一嘴,Object.create()来实现继承更能体现原型模式的精髓,但好像效率没有构造函数高,我们还可以通过Object.create(null)来创建一个没有原型的对象。

可以看到,obj是空的。

参考

www.cnblogs.com/TomXu/archi…

《javascript 设计模式与开发实践》