Checkson / blog

Checkson个人博客
12 stars 3 forks source link

彻底弄懂JavaScript中的原型链 #7

Open Checkson opened 5 years ago

Checkson commented 5 years ago

认识原型链

原型链的概念定义在 ECMAScript 中,它是 JavaScript 实现继承的主要方式,即使在 ES6 中提供了classextends 等关键字,能轻松实现类的继承,事实上这只是一种语法糖,底层还是沿用了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 类联系在一起的呢?请看下图:

default

从图中可以看到:

2

所以说,原型链就是由多个原型对象组成的链式结构。因为我们把这种特殊的对象称为“原型”,他们组成的链就叫“原型链”,其实,我们可以笼统理解为它也是一个“对象链”。

每个函数(类)都有 prototype 属性,除了 Function.proptype.bind(),该属性指向原型;每个对象都有 __proto__ 属性,除了 Object.create(null),该属性也指向原型。

原型链用途

相信读过jquerybootstrapbootstrap-tableselect2或者其他早期知名的库的同学,都会发现,他们的组织插件的写法一般都是一个构造函数和一些定义在这个构造函数原型上的函数,例如:

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

输出 maleMale.prototype 结果如下图:

3

这里我们会发现,male对象,并不存在sex这个属性,但是能输出male字符串,是因为这个属性存在于原型上。那么对象本身不存在的属性,JavaScript会自动在原型中找的?这就是JavaScript原型的特性了。假如对象本身不存在某个属性,JavaScript就会自动沿着对象__proto__这个属性,一级一级往上找,直到在某个原型对象上找到或者找到Object对象都没有的话,就停止搜索了。利用原型链这个特性,我们可以很好地在JavaScript中实现继承。

细心的同学还会发现,Male.prototype指向的是Person的实例,而不是Person本身。这里的用意是:

最后,我们回应上文,究竟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

输出maleMale.prototype结果如下图:

4

从输出结果可以看出,除了父类属性归类在当前对象上,其他函数定义都是放在原型对象上,也按照ES5原型链的结构来组织继承关系。