之前写过一篇关于JavaScript原型的文章:JavaScript原型和原型链的简单解释,回头想想,虽然分析到一些原型的细节,但是觉得分析的还不够全面,因为JavaScript的原型在整个JS编程里发挥了相当重要的作用,JS实现面向对象的继承也是使用了JavaScript的原型,必须得掌握好JavaScript的原型机制,发现有很多人不知道为什么要使用prototype,constructor也不知道为什么要指定,还漏掉指定constructor,这一定程度上是因为不理解原型链机制,这次我们主要讨论原型链和继承的原理和引用,本节先主要分析原型和原型链的原理和机制。
一、JavaScript原型概念全面解释
JS的原型并不好理解,JS的原型对象结构就像链表一样,稍不留神就可能会看懵了,不过首先我们先确定原型是一个实例对象,即称为原型对象,先看如下代码:
var user = {};
user.name = "Shift";
user.age = 18;
console.log(user);
如上图,哪个是原型对象?先别急,先说明一下,原型对象也是一个对象,普通对象也是一个对象,原型对象只不过是一种特殊的叫法,原型对象只是相对来说的,那么我们先找出相对于哪个对象?这里设参照对象为user,那么它的原型对象为__proto__(注意这个原型对象没有名称,也不知道它是什么,但是可以说它是user.__proto__)。
另外还注意到,原型对象constructor属性为Object()构造函数,该构造函数的的prototype属性刚好等于user的原型对象(user.__proto__)。
是不是觉得挺杂的?没关系,在这里我们只需要关心原型对象就行了,坑人的地方就是它就不是个东西,这个也要记住:原型对象是个无名氏,因为它是无名氏,所以得想办法称呼它才行,称呼它有两种方式,user.__proto__和Object.prototype,一般常用的是Object.prototype(默念一百遍。。。),所以你可以看到原型对象的写法是一种间接写法,下面我们画个图梳理一下:
再次说明一下,如果不是我们自己去手动更改原型对象,那么原型对象就是一个无名氏,我们称呼原型对象的方式,就是叫它做user.__proto__或者Object.prototype,对象的__proto__属性或者是构造函数的prototype属性。
另外,也要记得一个指向的关系,user.__proto__ => (原型对象) => (原型对象).constructor => (Object构造函数) => Object.prototype
=> (原型对象),这个关系也常在开发中用,例如一般我们可能会更改这个原型对象,(一般不使用user.__proto__)那么就使用Object.prototype更改原型,等于把无名氏原型对象换掉了,换一个新的对象那么它的constructor的值就会不同,同样还是Object构造函数,但是这个新的原型对象的constructor不指向Object了,因为是使用Object()构造函数创建实例的,所以这时应该更改为Object。
上面我们详细地讲解了什么是原型以及它的内部机制,现在我们要问:原型它的作用是什么?它的作用当然是JavaScript自己就规定了的,作用就是:一个对象可以访问它的原型对象的属性和方法,必须认清,这是两个对象之间的关系,在一个对象A上添加一个__proto__指定一个对象B,那么A就可以访问B的任意属性和方法了,而特殊又在于B称为A的原型对象,再次说明:这是两个对象之间的关系,它们的关系通过__proto__属性关联,既然是两个对象之间的关系,而且又可以单向共享另一个对象的属性和方法,那么这就可以实现继承了,继承就是子类可以共享父类的属性和方法。例如,上图中user可以使用原型对象Object.prototype中的toString()方法,另外,Object.prototype也有自己的原型对象,为null,null是最顶层的原型对象。
上图中主要有两个重要的属性和关系:prototype用于指定实例对象的原型,原型对象的constructor用于指定创建实例对象使用的构造函数,每个原型对象都有一个构造函数,这个构造函数的prototype属性指向自身。
二、JavaScript原型链解释
如果你看明白上面关于原型机制的解释,那么原型链就简单了,上面说到,原型是基于两个对象的关系(函数也是对象,所以函数也有自己的原型对象),首先有两个对象A和B,A添加一个__proto__属性赋值为B,则B就是A的原型对象,或简称为原型,那么JS的原型链是什么呢?很简单,再让B添加__proto__属性指向另一个对象C,依此类推,就形成了一个链条了,称之为原型链。
一般来说我们更关心的是一般对象的原型(而不是函数对象的原型),因为这关系到我们面向对象继承的设计,我们参考下面的常见代码,结合一张图理解这里面的原型链:
function Basic(username, age){
this.username = username;
this.age = age;
}
Basic.prototype.run = function(){
console.log(this.username + " " + this.age + " running...");
};
var basic = new Basic("Principle", 18);
basic.run();
上图红色的指向就是原型链了,上面说过一个对象可以共享访问它的原型对象的属性和方法,那么原型链的作用就是:一个对象可以共享它的原型链上所有对象的属性和方法,一直到Object.prototype,同样是面向对象的继承,子类可以共享它的所有祖先的属性和方法。
原型对象就是父类,对于使用继承父类,目前遇到的情况有两种,一种是为了让所有实例对象复用共享属性或方法,则使用父类;另一种是存在一个原型对象,需要继承这个对象,这是为了复用这个对象的属性和方法。对于前一种我们可以直接使用prototype指定相关的属性和方法,而后一种是为了复用目标的原型对象(注意是原型对象,不是普通实例对象,看到不少例子都是使用new普通实例对象,但是又没有复用到指定的属性或方法,复用已存在的对象的属性或方法倒是可以,不过这里的实例是有一个问题,不能直接将子类的原型直接执行父类的原型,这会造成共享同一个原型),这时候我们应该将子类原型对象例如User.prototype对象指向Basic.prototype,那么子类的实例就可以访问Basic.prototype下面的属性和方法了,下面是面向对象继承的一个实例:
// 父类原型为Basic.prototype
function Basic(username, age){
this.username = username;
this.age = age;
}
// 父类添加方法
Basic.prototype.run = function(){
console.log(this.username + " " + this.age + " running...");
};
// 创建父类的一个实例
var basic = new Basic("Principle", 18);
basic.run();
function User(username, age, password){
this.password = password;
Basic.call(this, username, age);
}
// 继承:子类原型指向父类原型
User.prototype = Basic.prototype; // 不能这样使用,可以new Basic()
User.prototype.constructor = User; // 将子类原型的构造函数指回自身,用于创建自身的实例对象
var user = new User("First", 28, "789456");
user.run();
关于JavaScript原型和原型链的内容现已基本讲完,这次都算解释得比较清晰了,下节我们再讨论原型链和继承的核心原理和应用,JavaScript的继承并不是一个简单的技术,涉及到原型链的理解,JavaScript真不简单,它在语法和机制上甚至比Java和C++都难搞,ECMA把JavaScript搞得贼复杂。