在掌握原型之前,请读者务必先掌握JS中的数据类型。
构造函数
一些读者可能对于面向类的语言有一定了解,导致混淆一些概念,但是JS中并没有类的固有概念,只是有类似的语法,如new和instanceof,以及在后来的es6中新增了类一些元素,如class关键字。所以暂时专注于了解JS中的对象机制。先来看看如何创建出一个对象吧!
function Person(){
...
}
var person = new Person()
首先要说明一点,Person 本身不是构造函数,它就是一个普通的函数,但是当使用new调用它,这个函数调用就变成了一个构造函数调用,构造出一个对象。只是不同于像Array,Object这样的原生构造函数,Person是我们自定义的构造函数。
原型的由来
使用 new 调用了 Person 函数,构造出一个对象 person。但是使用构造函数来创建对象有一个弊端:每个方法都会在每个实例上重新创建一遍。对于这样的创建方式,不同实例上的同名函数是不相等的,getInfo在这里是两个Function实例。
function Person(name, age){
this.name = name
this.age = age
this.getInfo = function(){
alert(this.name + this.age)
}
}
var person1 = new Person('Alice',23)
var person2 = new Person('Bob',25)
console.log(person1.getInfo === person2.getInfo) //false
虽说要想解决这个问题,只要把函数定义挂载到全局作用域中就可以,但是如果对象需要多个方法呢?因此原型模式应运而来。
prototype
在JS中,创建的每个函数(注意:不是所有数据类型都拥有原型属性,只有函数对象拥有)都有一个 prototype 属性。它是通过调用构造函数而创建的那个对象实例的原型对象,目的是让所有对象实例共享它包含的属性和方法。有点绕是不是?没事,先记住它的作用是什么,如你所见,我们给 Person.prototype 添加了同样的属性和方法,它确实解决了构造函数模式创建对象的问题:person1 和person2 访问的都是同一组属性和同一个getInfo函数。
function Person(){}
Person.prototype.name = 'Bob'
Person.prototype.age = 24
Person.prototype.getInfo=function(){
alert(this.name + this.age)
}
var person1 = new Person()
var person2 = new Person()
alert(person1.getInfo === person2.getInfo) //true
注意:有的人可能觉得 prototype 还是有点抽象,其实它就是一个对象,不过有普通对象和函数对象之分。
console.log(typeof Person.prototype) // Object
console.log(typeof Function.prototype) // Function
原型链
person 是如何关联 Person.prototype 的呢?原来当调用一个构造函数创建一个新实例后,这个实例的内部包含一个指针指向构造函数的原型对象。
person.__proto__ == Person.prototype //对象实例person指向构造函数Person的原型对象
可以用isPrototypeof()来确定是否存在这样的关系。
Person.prototype.isPrototypeof(person) //true
和prototype属性一样,只有函数对象拥有__proto__属性吗?没道理吧,以原生构造函数Object为例,
var obj = {}
obj.__proto__=== Object.prototype // true
所以一般的普通对象也有。并且表面上看基本类型也有这个属性。
这是因为当我们访问基本类型的属性的时候,会临时创建一个基本包装类型对象,可以像对象一样操作基本类型,执行完成之后立即销毁。正是因为Number, String, Boolean具有这种特殊的特性,所以它们又被称为基本包装类型。或许这也是大家认为”js中万物皆是对象“的原因吧,虽然从数据类型上来看并不是这样。
言归正传,proto 可以实现:如果在对象上没有找到需要的属性或者方法引用,会继续在原型链上进行查找。这个链接存在与实例与构造函数的原型对象之间,因此是__proto__实现了原型链机制。原型链的尽头是Object.prototype。
Object.prototype.__proto__ === null
constructor
原型对象会默认拥有一个 constructor 属性,并且指向这个构造函数。
Person.prototype.constructor == Person //true
但是 person 好像也有一个
constructor属性。
person.constructor == Person //true
字面意思好像是 person 由 Person 构造,但实际上 person.constructor 只是通过默认的__proto__属性与 Person 关联了起来,试着改变一下 Person.prototype,看看发生了什么吧。
function Person() { /* .. */ }
Person.prototype = { /* .. */ }; // 创建一个新原型对象
var person = new Person();
person.constructor === Person; //false
person.construtor === Object //true
其实 person 并没有 constructor 这个属性,但是它会寻找原型链上的 Person.prototype 是否有这个属性,它也没有,因为被修改了,于是找到了原型链的顶端,Object.prototype,这个对象有 .construtor 属性,指向内置的 Object() 函数。
总结
-
JS中的构造函数就是所有带 new 的函数调用;
-
原型对象本质上也是通过构造函数创建出的实例,只是有点特殊,因为其他的实例会继承 prototype 的所有属性和方法,实例通过设置自己的__proto__指向构造函数的prototype来实现这种继承;
-
原型对象本身其实就是普通对象(但
Function.prototype除外,它是函数对象); -
原型和原型链是JS实现继承的一种模型。原型链的形成是靠__proto__ 而非prototype;
-
至于原型的原型是什么?只有浏览器环境下可访问__proto__属性,代码中如何实现等问题,这里不再赘述,多点尝试和研究吧。