原型模式

January 15, 2018 by Sylvenas

什么是原型链

发现很多文章都是先讲prototype,这里我们换个思路来介绍,我们先讲__proto__。JS中每个对象(除了null和undefined)都有一个私有的只读属性__proto__

当我们调用一个对象(obj)的某个方法或者属性(fn)的时候,首先会在该对象本身去查找是否具有这个属性(obj.fn),如果找到,则直接返回;

如果没有找到则去查找obj.__proto__属性上有没有该属性(obj.__proto__.fn),__proto__属性就是指向该对象的构造函数的原型,如果该对象的构造函数上找到了该属性,则返回该属性;

如果没有,那么继续查找该对象的构造函数的构造函数的原型上是否有fn属性,也就是查找obj.__proto__.__proto__.fn上是否存在;

这个过程就叫做遍历原型链,直到原型链的顶端,也就是Object.prototype,而Object对象的原型指向为null,(Object.getPrototypeOf(Object.prototype) === null),根据定义,null没有原型,作为原型链的最后一个环节,如果整个原型链上都找不到,那么返回'undefined'。

上面描述的整个链状向上遍历的关系,就叫做原型链,JavaScript正是基于这个特性实现的“继承”(我认为在JavaScript中叫组合更为合理)。

举例说明

当我们有一个构造函数Animal,它的原型属性Animal.prototype上存在一个方法move

function Animal(type) {
	this.type = type;
}

Animal.prototype.move = function () {
	console.log('I am moving');
}

var animal = new Animal('dog')

console.log(animal.__proto__ === Animal.prototype);    // true
console.log(animal.type);                              // dog
console.log(animal.move())							   // 'I am moving'

console.log(Animal.prototype.constructor === Animal)   // true

console.log(Animal.__proto__ === Function.prototype)  // true

console.log(Animal.prototype.__proto__ === Object.prototype)  // true
  • 构造函数Animal的原型属性Animal.prototype里有共有的方法,所有构造函数声明的实例(animal)都可以共享这个方法。

  • 原型对象Animal.prototype保存着实例共享的方法,有一个指针constructor指回该构造函数(Animal.prototype.constructor === Animal)。

  • animal是构造函数Animal的实例,animal对象也有属性__proto__,指向Animal的原型对象(Animal.prototype)--(animal.__proto__ === Animal.prototype)。

  • 构造函数Animal除了是方法,它也是个对象啊,它也有__proto__属性,指向Animal的构造函数Function的原型对象Function.prototype。(Animal.__proto__ === Function.prototype)。

  • 构造函数Animal的原型属性Animal.prototype也是对象,那么Animal.prototype.__proto__又指向哪里呢,很明显指向Animal.prototype的构造函数的原型对象Object.prototype。(Animal.prototype.__proto__ === Object.prototype)。

总结:

  • 对象有属性__proto__,指向该对象的构造函数的原型对象。
  • 方法除了有__proto__还有属性prototype,prototype指向该方法的原型对象。

原型链也会有终点,终点就在Object.prototype.__proto__ === null,那么既然Object.prototype也是一个对象,那么Object.prototype.__proto__就应该指向Object.prototype,不过很遗憾,尽管Object.prototype也是一个对象,但是这个对象却不是由Object构造函数所创建的,而是有JS引擎(eg:V8)按照ECMAScript规范创建的。

原型链有一个例外就是,Function的原型指向Function的原型对象,也就是Function.__proto__ === Function.prototype。这是一个鸡生蛋,蛋生鸡的过程,说不清楚,还是看看规范吧。

实现继承

现在我们有一个子构造函数Dog,它啥都没有,需要让它的实例继承Animal的所有属性和方法

Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;

因为new Animal()__proto__指向Animal.prototype这样的效果相当于:

Dog.prototype.__proto__ === Animal.prototype   //true

当我们new Dog()的时候,那么就是

new Dog().__proto__ === Dog.prototype   //true

根据向上遍历原型链的规则,var dog = new Dog(),当访问dogmove方法时,会依次查询dog本身Dog.prototype,Animal.prototype,最终找到move方法。

但是有一点需要注意的是,整个原型链是动态的,也就是无论Dog生成了多少个实例,一旦更新了原型链上的属性、方法,则所有实例上的属性和方法,将跟随着改变,因为他们指向同一个引用。

我们还可以使用ES5的Object.create方法串起原型链,const newObj = Object.create({ a: 1 }, { b: { value: 1, writable: false, configurable: true } })方法的作用为,新创建的newObj对象的__proto__指向{a:1},而第二个参数为属性描述符,会被添加到newObj对象的本身属性上,而不是原型属性上。

const newObj = Object.create({ a: 1 }, { b: { value: 1, writable: false, configurable: true } })
console.log(newObj);             // {b:1}
console.log(newObj.__proto__);   // {a:1}

用在上面的例子中则是:

function Animal(type) {
	this.type = type;
}

Animal.prototype.move = function () {
	console.log('I am moving');
}

function Dog() { }

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

const dog = new Dog('金毛');
console.log(dog.type);       // undefined
console.log(dog.move());     // 'I am moving'

这样做很明显dog无法继承Animal构造函数中定义的属性和方法,例如dog.type,因为我们仅仅将Animal的原型对象添加到了Dog的原型上,那么我们可以这样解决:

function Dog() {
	Animal.call(this);
}

扩展一下,这个思路为我们提供了多父类继承的方法:

function MyClass() {
	SuperClass.call(this);
	OtherSuperClass.call(this);
}

// inherit one class
MyClass.prototype = Object.create(SuperClass.prototype);
// mixin another
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// re-assign constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function () {
	// do a thing
};

原型模式

原型模式是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象,原型模式在JavaScript里的使用简直是无处不在,其它很多模式有很多也是基于prototype的,上面的讲解中,我们多次提到了的概念,但是JavaScript本质上避免了class的概念,ES6中的class只是个语法糖,本质上还是使用原型,关于这一点我们从JavaScript的作者Brendan Eich后来的访谈中也能看出些许:

"我并非骄傲,只不过是很高兴我选择 Scheme 式的一等函数以及 Self 式(尽管很怪异)的原型作为主要因素。至于 Java 的影响,主要是把数据分成基本类型和对象类型两种(比如字符串和 String 对象),以及引入了Y2K 日期问题,这真是不幸。我把最终进入 JavaScript 中的一些"不幸"类似 Java 的特性加入到如下列表中":

  • 构造器函数和new关键字
  • class关键字加上单一祖先的extend作为主要继承机制
  • 用户的偏好是把class当作一个静态类型(实际上完全不是)。

JavaScript只是简单的从现有对象进行拷贝来创建新的对象,我们应该尽量使用对象组合的方式去构建对象,在JavaScript中应该尽量避免使用继承的思路。

使用继承脑袋去思考原型只会把简单的问题弄得越来越复杂。