原型对象、原型、原型链以及原型链继承
一、什么是原型对象
首先明确一下定义,每个函数都会有它的原型对象,也就是prototype,这是在函数创建的时候浏览器会根据一定规则自动生成的。看下图:
可以看到,函数的prototype里面包含一个construction,它会指向函数本身。另外一个__proto__,也就是我们下面将会讲到的原型。
二、什么是原型
每一个JavaScript对象(null除外,谨记)都有它的原型,也就是__proto__这个属性。我们拿上面的构造函数创建一个实例来举例:
看到没有,有没有觉得很熟悉!!对,其实它指向的就是Person的原型对象。换一句人话来说就是,JavaScript对象的原型是一个指针,它指向的是它的构造函数的原型对象(可能有点绕口,建议多念几遍理解一下)。千万不要把原型和原型对象弄混了,原型对象只有函数才有的,非函数是不存在原型对象的,原型是JavaScript对象都有的。
console.log(person.prototype); // undefined
console.log(person.__proto__ === Person.prototype); // true
三、什么是原型链
在对JavaScript进行学习的时候,你一定听过“作用域链”这个名词,即在某一作用域里使用到的变量,若在当前作用域里找不到,便会继续往上层作用域里进行查找,直至到全局变量中。其实原型链与其是十分相似的。我们前面说到,实例对象的原型是会指向它的构造函数的原型对象的。那么就会出现一种情况,如果我需要通过Person这个构造函数来创建多个实例对象person1、person2、person3....它们除了名字不同,但是我想要它们拥有相同的操作方法,比如说它们都会说话speak、都会吃东西eat,这种情况需要怎么实现呢。先自己思考一下再继续往下看!
聪明的你一定能想到,那在Person的原型对象上添加这些操作方法不就可以了!因为每一个实例的原型都会指向Person的原型对象。
是不是很好理解。但这还不是完整的原型链,我们说作用域链是往上层作用域去查找变量,原型也是一样的,如果在对象本身中找不到变量,那么它就会往它的原型中查找,还找不到,就再往原型的原型中进行查找,直至null(这个具体可以自己动手去试一下,这里稍微提一下Person.prototype.__proto__ === Object.prototype,Object.prototype.__proto__ === null)。那么这个闭环就形成了一条原型链,是不是觉得跟作用域链十分相似。原型的应用十分丰富,下面提一下一个比较重要的应用 --- 利用原型链来实现继承(在ES5之前基本都是靠原型链来实现继承的,在ES6之后才引入了class这个语法糖)。
四、原型链继承
不多逼逼直接看代码,再来慢慢分析。
1 // 初始化父类 2 function Person(name) { 3 this.name = name; 4 } 5 6 // 在父类的原型对象上添加操作方法,方便后面测试子类是否有继承到 7 Person.prototype.speak = function() { 8 console.log(`My name is ${this.name}.`); 9 } 10 11 // 子类初始化 12 function Student(name, age) { 13 // 将父类构造函数内部的属性指向改为指向子类 14 Person.call(this, name); 15 this.age = age; 16 // 子类自己构造函数内部的操作方法 17 this.speak1 = function() { 18 console.log(`I am ${this.age} years old.`); 19 } 20 } 21 22 // 初始化一个父类实例,并赋值给子类的原型对象 23 Student.prototype = new Person(); 24 Student.prototype.speak2 = function() { 25 console.log('I am a student.'); 26 } 27 28 // 用子类实例化一个对象student 29 var student = new Student('Peter', '16');
大部分代码应该都看得懂,比较有问题的可能就是23行,为什么要将父类实例化并且赋值给子类的原型对象。我们前面说到,一个实例对象,是可以访问到其构造函数的原型对象上的方法的,是不是恍然大悟了!然后在子类初始化的时候,使用call/apply的目的是为了让子类能够访问到父类构造函数内部的属性/方法,这样就能够完全继承到父类的所有属性和方法了。具体大家可以自己动手尝试一下。