Prototype

JS为了模拟面向对象“类”的实现,为了模拟类的复制行为,可能会使用一种叫做“混入”的方法,当然,这种方法和我们今天要说的原型并没有多大的关系。使用mixin的方式来模拟“类”的实现不常见,当然为了模拟”类“所付出的代价也会让我们得不偿失,JS中不并存在”类”,而是存在一种叫做原型的东西,请容我细细说来。

Prototype

我们直接来讲文章的主角Prototype,其实JavaScript中每个对象都有一个叫做[[prototype]]的属性,这个属性就是对其他对象的一个引用。基本上所有的对象在初始化时[[prototype]]都会被赋予一个值,关于这个值是什么以及如何访问这个[[prototype]]属性,我会在后面提到。

还是先看一个例子:

1
2
3
4
5
var obj = {
a: 1
}
var obj2 = Object.create(obj)
console.log(obj2.a); //1

上面的代码我们创建了一个对象obj,其包含一个属性a,我使用Object.create(obj)创建了一个新的对象,并把该对象的[[prototype]]属性赋值为obj,最后我们打印obj2中并没有显式声明的变量a,令人惊奇的是,我们成功的访问到了变量a,并且该变量的值为[[prototype]]属性引用对象obj的属性a的值!

我想解释下为什么要对这段代码写这么详细的解释,因为对于大多数接触过JS的童鞋而言,原型已经是见怪不怪了,可是当初我学习JS的时候,脑子里完全没有原型的概念,直到有一天我慢慢开始懂得原型,那个时刻,我的心情就像现在写这段解释的时候这么激动!

看完上面这段代码和冗长的解释,即使不了解JS的童鞋也对原型有了一定的认识。在这里我想再说一下,[[prototype]]到底有什么用,其实很简单,当我们试图引用某个对象的时候,在底层其实调用的是一个GET方法,而这个方法首先会查找对象本身存在这个属性与否,如果不存在则通过[[prototype]]访问其原型对象,如果还是不存在的话,则访问原型的原型对象(别忘了原型对象也是普通对象),知道找到或者达到尽头(Object.prototype)。这个道理很简单,如果你使用for…in循环遍历一个数组的话,也许你得到的结果除了数组成员,还包含一些其它成员(不信你试试看),这些成员就来自原型对象,并且是可枚举的,而对于in关键字,也会查找原型链上面属性。

在说类的时候,也许更恰当的是给类打上一个引号,因为JS中根本就不存在”类”,JavaScript中只存在对象,我们不使用“类”创建对象,更多时候我们直接创建对象。可有些时候,我们使用new关键字来初始化一个对象,我们甚至在ES6后开始使用class,extend等属于类的关键字,这貌似和我前面说的矛盾了…
接着看一个例子:

1
2
3
4
5
6
function A(){

}
console.log(A.prototype);// {}
var a = new A();
console.log(typeof a); // object

我们创建了一个函数A,并且这个函数有一个属性prototype,如果没记错的话,这是本篇文章第一次访问原型,然后我们使用new初始化了一个对象,有传统面向对象语言基础的同学就知道,这简直像极了“类”!我再次强调,JS中不存在类,而且此new非彼new,这里函数A在new关键字的作用下,新建了一个空白对象,并让其prototype指向的对象赋值给新建对象a的[[prototype]]属性(关联),当然这里面还会做一些其它的工作,不过大体上就这样了,很简单吧!

在JavaScript中,并不存在类的复制,我们不能创建一个类的多个实例,只能创建过个对象,只不过通过new这种方式创建的对象,其内部的[[prototype]]属性关联到同一个对象,这里所说的关联是建立一个联系,并不存在复制。

构造函数

既然不存在类了,这构造函数听着也很别扭,我们暂且给它打个引号吧。上面我们在说“类”的时候,我们就用到了”构造函数”,函数A就是所谓的“构造函数”,其本质上就是一普通函数,是JS的一等公民,要说真要有什么区别,函数名首字母大写算吗?也许是吧。

再来写一个例子:

1
2
3
4
5
function B() {}
console.log(B.prototype);
console.log(B.prototype.constructor === B);
var b = new B();
console.log(b.constructor === B);

我感觉我放了一个大招,突然让自己迷惑起来,这里我要说明的是,B.prototype和对象b有一个叫做constructor的属性,并且默认指向函数B。这个属性的名字会让我们对JS的误解加深,四级没过的都知道,constructor翻译过来可叫做“构造器”啊,那么既然B.prototype.constructor指向了B,我们还有什么理由不说B不是“构造函数”?讲到这里,我很无奈…

其实呢,JS中根本不存在什么“构造函数”,其就是普普通通的函数,只不过一旦加上new关键字,这个函数调用再也不是普通的函数调用,我们把它叫做“构造函数调用”。

这里不想再说下去了,写个复杂点的例子先:

1
2
3
4
5
6
7
8
9
function Student(name, city){
this.name = name;
this.city = city;
}
Student.prototype.showInfo = function(){
console.log(`name: ${this.name}, from: ${this.city}`);
}
var stu = new Student('limoer', 'Chongqing');
stu.showInfo(); // name: limoer, from: Chongqing

这里有两个值得注意的地方,每个通过”构造函数调用”而生成的对象都存在两个属性name和city;我们给Student.prototype上添加了一个“方法”,这样所有的新建对象都关联了这个对象,可以引用这个“方法”,关于this的使用,这里就不在提了。

在说了这么多过后,我想把“构造函数”称为“关联函数”,因为所谓的“构造函数”其实并不存在,或者说是,我们并不知道一个函数在创建好后是否是“构造函数”,而如果我们把它叫做“关联函数”,因为它本质上做的工作包含了建立对象和其原型对象的关联,当然,这个叫法是不恰当的。

再来看看constructor属性,一般情况下,任何一个普通对象都存在一个constructor属性,其实这个属性并不是其本身就有,而是当引用该属性的时候,其可以在该对象的原型链中找到。现在我急切的想写一个例子来表明一个问题:

1
2
3
4
function C(){}
C.prototype = {}
var c = new C();
console.log(c.constructor === C); // false

我不啰嗦了直接看问题,这里对象c的constructor属性竟然指向的不是创建它的那个函数C,这也侧面印证了我上面说的话,通过构造函数调用创建的对象不直接持有属性constructor而是从其原型链中“继承”而来,所以当我们想写一段包含继承的代码时,如果还想用constructor属性,需要做必要的修正。

1
2
3
4
5
6
function Main(){}
function Sub(){
Main.call(this)
}
Sub.prototype = Object.create(Main);
Sub.prototype.constructor = Sub;

在结束“构造函数”讨论的时候,提醒一句,尽量不要使用constructor属性,要问原因?我想我已经不那么直观的在前面说出来了。

如何关联

我在上面提到把“构造函数”叫做“关联函数”,这虽然是不恰当的,但也不是一无是处,因为使用new关键字的“构造函数调用”,其在创建一个对象过后,也把该对象的[[prototype]]属性关联到该函数的prototype上。当然,如何关联不止这一种方法,这里介绍一种使用更为普遍的方法,Object.create(proto)。

还是例子为先吧:

1
2
3
4
5
6
7
var obj = {
info: function(){
console.log('info');
}
}
var obj1 = Object.create(obj)
obj1.info(); // info

这里我们使用字面量的直接形式创建了一个对象obj,该对象包含一个方法info,然后使用Object.create()创建了一个新的对象,并且该对象内部的[[prototype]]属性指向obj,概括点来说,该方法创建了一个对象,并把其关联到指定的对象。

Object.create()是ES5才引进的,在这里我实现一个polyfill代码作为本篇的结束:

1
2
3
4
5
6
7
if(!Object.create){
Object.create = function(obj){
function Foo(){}
Foo.prototype = obj;
return new Foo();
}
}

当然,这个版本的polyfill代码无法做到更复杂的功能,而Object.create第二个参数可以指定要添加到新建属性的属性名、值等。

尾巴

我们如果要访问一个并不存在的属性,在内部将会使用[[GET]]方法,并且查找该对象[[prototype]]所关联的对象,该关联实际上定义了一条“原型链”,在查找属性的时候会遍历整个“原型链”。

关联两个对象的最常用的两种方法是:(1)使用new关键字进行“构造函数调用”,创建一个新对象并进行关联;(2)使用Object.create(),创建新的对象,并时期和传入对象关联。

最后再次强调,JavaScript并不存在类,所有继承的实现完全是基于原型链,不存在复制。

注:到底前面所说的原型链的尽头到底在哪里呢?答案是Object.prototype,对于一般的原型链而言,其最终都指向了Object.prototype,这个对象包含了许多对象通用的方法,例如obj.toString()&obj.valueOf()等。

分享到 评论