在ES6下可以通过class 关键字创建一个类, 也可以通过class .. extends 实现继承, 但只是一个语法糖, 最终还是通过原型链实现继承
JavaScript里每个function都有一个Prototype对象, 使用new 操作符去操作function时会产生一个新对象, 这个新对象会继承Prototype所有的属性和方法
代码如下
function Person (){}; //相当定义一个类
Person.say2 = function(){ console.log("Hi2") }; //在方法增加一个方法
Person.prototype.say = function () {console.log("hi!"); } //增加一个方法
Person.prototype.name = "小明"; //增加一个属性
Person (); //undefined; 如果直接调用相当一个方法调用, 没有任何特别之处
var man = new Person(); //加上new之后可以当作一个构造函数
console.log(man.constructor === Person); //true, 构造函数
man.say(); // hi!, 实例继承了say方法
console.log(man.name) //小明,实例继承了name属性
man.say2(); //报错,没找到该方法, 因为实例化时只会继承prototype
对象实例化出来只会继承prototype本身的属性和方法, 并不会继承function的属性和方法。 上面的代码看上去感觉像混合或复制prototype的方法和属性, 并没有什么神奇之处, 可以看下代码体会下神奇之处
function Person (){}; //相当定义一个类
Person.prototype.say = function () {console.log("hi!"); } //增加一个方法
var man = new Person();
var woman = new Person();
man.say = function(){console.log("Hi, man")};
man.say(); //Hi, man ,重写say方法
woman.say(); // hi!, 方法没有受到影响
Person.prototype.show = function(){ console.log("HAHA") }
man.show (); //HAHA ,动态继承了show方法
woman.show (); //HAHA, 动态继承了show方法
Person.prototype.say = function () {console.log("hi! Person"); } //增加一个方法
man.say(); //Hi, man ,say方法不受影响
woman.say(); // hi! Person!, 方法已改变
从上面代码可以发现到几个规律, 实例重写方法并不会影响到prototype, 更不会影响到其它实例, prototype重写方法会影响到没有重写方法的实例, 并且prototype增加方法所有实例也会动态继承下来。 原理其实很简单, 可以通过console.dir(man),console.dir(woman), console.dir(Person.prototype) 打印结构分析
man和woman实例都有一个__proto __属性(谷歌浏览器会显示出来), __proto __属性指向Person的prototype, 它们是相等的, Person的prototype也有一个__proto __属性, 这个属性指向Object的Prototype, 而Object.Prototype.__proto __指向null, null处于最顶端, 这些__proto __连接在一起就组成了原型链。当试图访问一个对象的属性时, 首先会在对象的属性中查找, 找不到再沿__proto __这条原型链一直找, 直到找到或到达顶端才结束
通过以上的结论可以解释man.say方法为什么不受影响了, 当执行man.say = function(){console.log("Hi, man")}
代码时, man相当于定义一个方法, 执行man.say()时, 首先就在man对象方法里找到了, 不用沿着原型链找。 所有不管Person.prototype.say怎么变都不会影响到man.say方法。而show方法在对象没有定义 , 所有要沿原型链查找
有了以上的知识,该考虑如何延续原型链, 达到类与类之间的继承。 仔细分析Person类就可以发现它是继承Object, man和woman实例都有Object的prototype的方法, 那是因为Person.prototype.__proto __属性指向Object.prototype, 所以延续原型链关键点在于__proto __属性,只要__proto __指向父类的prototype就算完成了继承, 但是__proto __是怎么来的? 由上面的例子得知是通过new function实例化才会有, 所以子类的prototype实际就是父类的实例
接上面的例子, 我们需要增加一个学生的子类, 学生子类比多一个报学号的方法。
function Person (){}; //相当定义一个类
Person.prototype.say = function () {console.log("hi!"); } //增加一个方法
function Student(){}; //定义一个学生类
Student.prototype = new Person(); //继承Person类
Student.prototype.sayNo = function(){console.log("1号")}; //定义一个报学号的方法
var xiaoming = new Student();
xiaoming.say();//hi!, 成功继承下来了
xiaoming.sayNo(); //1号, 方法正常
Person.prototype.show = function(){console.log("大家好")};
xiaoming.show(); //大家好, 动态继承也没问题
//再增加一个子类, 三好学生
function GoodsStudent (){};
GoodsStudent.prototype = new Student();
GoodsStudent.prototype.prize = function(){console.log("领奖~")}; //定义一个领奖的方法
var xiaohong = new GoodsStudent();
xiaohong.say();//hi!, 成功继承下来了
xiaohong.sayNo();//1号, 成功继承下来了
xiaohong.prize();//领奖~, 正常
console.log(xiaohong instanceof GoodsStudent && xiaohong instanceof Student && xiaohong instanceof Person && xiaohong instanceof Object);//true, instanceof也没问题
一个简单的继承已经完成了, 但不完整, Student.prototype = new Person(); //继承Person类
这段代码有问题, 如果Person是有参的构造函数就行不通了。 问题还有很多, 面向对象中当子类正在实例化时,总是链式调用父类构造方法初始化父类空间, prototype中constructor必须指向当前构造函数。
最终版代码如下:
function Person (name){this.name = name}; //相当定义一个类
Person.prototype.say = function () {console.log("hi!" + this.name); } //增加一个方法
function Student(name){Person.apply(this, arguments)}; //当子类正在实例化时,总是链式调用父类构造方法初始化父类空间
//定义一个原型的构造函数
var prototypeFn = function(){ this.constructor = Student; };// prototype中constructor必须指向当前构造函数
prototypeFn.prototype = Person.prototype;
Student.prototype = new prototypeFn();
var xiaoming = new Student("小明");
xiaoming.say(); //hi!, 小明, 已经继承下来了, 没问题
console.log(xiaoming instanceof Student && xiaoming instanceof Person && xiaoming instanceof Object); //true, instanceof也没问题
console.dir(xiaoming)
打印结构看上去也没问题