-
Notifications
You must be signed in to change notification settings - Fork 3
Description
认识原型链
原型链的概念定义在 ECMAScript 中,它是 JavaScript 实现继承的主要方式,即使在 ES6 中提供了class
和 extends
等关键字,能轻松实现类的继承,事实上这只是一种语法糖,底层还是沿用了JavaScript 中原型链的原理。
在我看来,几乎每一种面向对象的语言,例如:JavaScript、Java、C#、Python等,他们的数据类型,或者更准确来说是引用(复杂)类型,都是继承于一个最基本的类(类型)。 在 JavaScript 中所有的引用类型,都是继承于 Object 对象,除了用 Object.create(null)
这种方式创建的对象。在Java、C#、Python中Object类,同样也是其他所有类的超类。可见,许多高级编程语言的设计,都会有一个类似根类的东西。
JavaScript 继承之所以与其他面向对象语言很不一样,是因为在ES6之前,JavaScript 要实现继承,就需要各种“骚操作”,而这些“骚操作”大多都是基于原型链继承。所以,总体来说,很多时候,在JavaScript 中谈及到原型链,大多都会与继承扯上关系。就好比谈到“数据结构”,后面总会跟上“算法”。
想仔细深入学习继承和原型链的用法的同学,可以翻看红宝书《JavaScript高级程序设计(第三版)》中的第六章,第三小节的介绍,非常详细,网上很多博客和教程,都是基于它来展开或者补充的。没有这本书的同学,可以点击这里。
我们进入正题,请先看以下的例子:
示例:
// ES5利用函数声明类
function Person (name, age) {
this.name = name;
this.age = age;
}
// 实例化
var person = new Person("Checkson", 23);
// 判断实例类型
console.log(person instanceof Person);
console.log(person instanceof Object);
输出结果:
true
true
这里为什么都是输出 true
呢?第一个输出语句判断的是 person
是否是 Person
的实例,那么很简单,因为 person
是通过 Person
new 出来的,所以结果返回是true
;第二个输出语句是判断person
是否是 Object
的实例,这里我们并没有看到 Person
类有显示声明是继承于 Object
类的,但是最后输出的结果是 true
,证明,我之前提到过的:默认情况下,基类Object是所有类的超类。那么,在 JavaScript 中,以上例子中的 Person
类是怎么与 Object
类联系在一起的呢?请看下图:
从图中可以看到:
Person
函数(类)有一个名为prototype
的属性,指向了Person
的原型;而实例person
有一个名为__proto__
的属性,也指向了Person
的原型prototype
。所以我们可以得出一条公式:Person.prototype = person.__proto__
。
Person.prototype
和person.__proto__
都是指向同一个对象,我们称它为“原型”。这个原型对象有两个属性,分别是constructor
和__proto__
。constructor
顾名思义,是指向构造函数的属性,那么我们又可以得出一个公式:Person.prototype.constructor = person.__proto__.constructor = Person
;__proto__
就是这里提到的原型对象的原型,这个新的原型同样具有constructor
属性,但没有__proto__
属性,因为它已经指向了基类Object
了。
所以说,原型链就是由多个原型对象组成的链式结构。因为我们把这种特殊的对象称为“原型”,他们组成的链就叫“原型链”,其实,我们可以笼统理解为它也是一个“对象链”。
每个函数(类)都有 prototype
属性,除了 Function.proptype.bind()
,该属性指向原型;每个对象都有 __proto__
属性,除了 Object.create(null)
,该属性也指向原型。
原型链用途
相信读过jquery、bootstrap、bootstrap-table和select2或者其他早期知名的库的同学,都会发现,他们的组织插件的写法一般都是一个构造函数和一些定义在这个构造函数原型上的函数,例如:
function JQuery (el) {
this.el = el;
...
}
JQuery.prototype.addClass (className) {
// pass
}
JQuery.prototype.removeClass (className) {
// pass
}
...
这样写的原因是:每实例化 JQuery
类的时候,私有属性 this.el
会在内存中开辟一个新的内存空间,而在定义在原型上的函数,则只会开辟一份内存空间,不会随着实例化对象增多而开辟更多的内存空间,这样就大大节省了内存,提高程序整体的性能,其作用就类似C#、Java中的静态方法。
原型链另外一种用途就是我们之前提到的继承了。我们先看一下ES5中最简单的继承方式:
function Person (sex) {
this.sex = sex;
}
function Male (name, age) {
this.name = name;
this.age = age;
}
// 将Male的原型指向实例化后的Person对象
Male.prototype = new Person('male');
// 将构造函数constructor指向Male
Male.prototype.constructor = Male;
// 实例化Male
var male = new Male('Checkson', 23);
// 输出male的性别
console.log(male.sex); // male
输出 male
和 Male.prototype
结果如下图:
这里我们会发现,male
对象,并不存在sex
这个属性,但是能输出male字符串,是因为这个属性存在于原型上。那么对象本身不存在的属性,JavaScript会自动在原型中找的?这就是JavaScript原型的特性了。假如对象本身不存在某个属性,JavaScript就会自动沿着对象__proto__
这个属性,一级一级往上找,直到在某个原型对象上找到或者找到Object对象都没有的话,就停止搜索了。利用原型链这个特性,我们可以很好地在JavaScript中实现继承。
细心的同学还会发现,Male.prototype
指向的是Person
的实例,而不是Person
本身。这里的用意是:
- 保持原型链的完整性。上面提到,每个对象都有
__proto__
属性,而原型链依赖这个属性连接起来,将函数类型实例化为对象类型是为了获得这个属性。 - 实例化后的对象,才会拥有该函数(类)中定义的属性和方法。
最后,我们回应上文,究竟ES6中的继承,也是不是原型链继承中的一个语法糖呢?我们先看例子:
class Person {
constructor (sex) {
this.sex = sex;
}
getSex () {
return this.sex;
}
}
class Male extends Person {
constructor (name, age) {
super('male');
this.name = name;
this.age = age;
}
getName () {
return this.name;
}
getAge () {
return this.age;
}
}
// 实例化Male
const male = new Male('Checkson', 23);
// 输出sex属性值
console.log(male.sex); // male
输出male
和Male.prototype
结果如下图:
从输出结果可以看出,除了父类属性归类在当前对象上,其他函数定义都是放在原型对象上,也按照ES5原型链的结构来组织继承关系。