原型机制小总结

186 阅读5分钟

写在最前面,文章若有不正确、不严谨之处,万望指正。

太长不看版:

  1. prototype 是新建函数时,自动为其创建的原型对象,值是包含一个 constructor 属性的对象。
  2. proto 是通过构造函数创建一个新的实例后,该实例的内部将会包含一个指针(built-in property),通常这个指针将指向构造函数的原型对象。

Why

一切都要从这张图开始说起,这是一张我在一个帖子上面看到的一个讲述 prototype 和 [[prototype]] (下面用 proto 区分)的关系图。初看这张图的时候,觉得非常迷惑。实不相瞒,我一直没有怎么搞懂这两个,长得像,功能似乎也挺像。直到开始研究起了继承之后,才慢慢扩展,搞懂了这个部分。接下来就进入正题。

What 's prototype

prototype 是在创建函数时,Function 的构造器产生函数对象时为其绑定的一个存放继承特征的对象属性。Function 的构造器产生函数对象时,会运行类似的代码:

this.prototype = { constructor : this}

内置的函数构造器为这个函数自动添加 prototype 属性,这个 prototype 的值,往往是包含一个 constructor 属性的对象, constructor 指向了对象的构造函数。这是因为,由于 JavaScript 无法确定哪一个函数是准备用来作为构造器函数的,所以通过了这个 constructor 来指向这个对象的构造函数。

function Parent(option) {
	// Explanation:
	// if option and option.name => this.name = option.name
  	// else this.name = 'no name 
	this.name = (option && option.name) || 'no name'

	this.introduceMyself = function () {
		console.log('my name is ' + this.name)
	}

	// your code here 
	// other parent properties
}

// prototype function and properties
Parent.prototype.sayhi = function(){ 
	console.log('hello')
}
Parent.prototype.species = 'human'

Parent.prototype 中包含 1, constructor 属性指向了 Parent ( 原型对象对应的构造函数 ),2, 以及其他的原型对象及方法

What's proto or [[prototype]] ?

创建了自定义的构造函数之后,当调用了构造函数创建一个新的实例后,该实例的内部将会包含一个指针(built-in property),通常这个指针指向构造函数的原型对象。举个例子:

function Parent(){ ... }
Parent.prototype.protoProperties = { ... } // 原型属性或者原型方法,供所有实例共享

var person = new Parent() // 构建一个以 Parent 作为构造函数的 person 实例

Parent 是一个构造函数(可以理解为以它为模板),person 是 Parent 的实例,person 内部属性 proto 指向了创建该实例的构造函数 由上图,我们可以看出,person 内部属性 proto 指向了 Parent.prototype。从图中,特地在 person 上面标注了 parent properties 和 instance properties,这是为了方便理解。在实例对象上的属性或者方法中,包括了从 parent 处继承过来的属性或者方法,这也是因为当在实例对象访问不到时,会沿着原型链一层层往回找直到找了该方法返回(就近返回)或者找不到返回 undefined 。同样的,如果我们要“覆写”某个方法时,只要在对象上写入一个同名的方法就可以实现覆盖,实际上只是由于已经找到了返回,没有继续沿着原型链往回找而已。

实现原型链,我们可以回顾一下 new 。如果我们用普通的方式实现 new 我们会发现,这个操作符如同下面的操作:

Function.prototype.new = function(){
	
	// 1. 创建一个对象,继承于构造器函数的原型对象
	var that = Object.create(this.prototype)

	// 2. 调用构造器函数,并将 this 指针绑定到新对象 that 上
	var other = this.apply(that, arguments)

	// 3. 判断构造函数返回值,如果是个对象,则返回 other; 如果不是,则返回新对象。
	return (typeof other === 'object' && other) || that
	
}

我们可以个发现,实际上,这个 new 操作符可以拆解成 3 个步骤,其中连接新对象和原型对象的步骤出现在第一步。 Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 通过这个方法,实例对象就可以和原型对象进行链接,从而方便访问原型方法和原型属性。

原型链继承

简单回顾一下前面提及的内容:

  1. 每个构造函数都有自己的原型对象 prototype
  2. 原型对象都有一个指向构造函数的指针 constructor
  3. 实例都包含着一个指向原型对象的内部指针 proto or [[prototype]](built-in)

如果现在一个对象的原型对象指向了 另一个对象的原型对象会发生什么事呢?

// 父类
function Parent(){ ... }

// 子类
function Child(){ ... }
Child.prototype = new Parent() // 重写原型对象

// 子类实例
var instance = new Child()

Parent 为父类,Child 为 Parent 的子类, instance 为 Child 的实例

我们可以通过此图发现,前面提及的关系依旧成立,如此便构成了实例与构造函数,构造函数与构造函数之间层层递进的原型链了。实现原型链继承的本质实际上就是重写了原型对象,取而代之的是一个其他构造函数的实例,这样就确立了构造函数之间的继承关系。

当我们读取 instance 上的实例属性时,一般会经历几个步骤:

  1. 搜索实例,检查实例是否存在该方法
  2. 继续一层层沿着原型链往回找,直到 找到! 或者找到 原型链末端
  3. 找到了? 返回! 找不到? 返回 undefined

Attention

Q: 下面的两种定义方式是一样的么?first & second

// 父类
function Parent(){ ... }

// 子类
function Child(){ ... }
Child.prototype = new Parent() // 重写原型对象

// 给原型添加原型链方法
// 1. 直接给 prototype 对象新增属性
Child.prototype.first = function(){
		...
}

// 2. 字面量方式新增对象属性 
Child.prototype = {
	second : function(){
		...
	}
}

A: Nope! 在使用原型链继承的时候,不要采用字面量的定义方式。原型链继承的核心是重写子类的原型对象,指向了父类的实例。而通过字面量的方式,将会重写了子类的原型对象,导致子类和父类之间的关系被切断。

reference

  1. Javascript prototype
  2. 从__proto__和 prototype 来深入理解 JS 对象和原型链
  3. JavaScript 高级程序设计 第三版 中英文。 Chap6.3