首先我们复习下JavaScript中的变量类型。
- 值类型(基本类型):String,Number,Boolean,Undefined,Null ,Symbol
- 引用类型:Object,Array,Function
值类型:会保存在栈中,保存与复制的是值本身,随着方法的结束自行销毁;
引用类型:会保存在堆中,保存与复制的是指向对象的一个指针,不会随方法的结束而销毁,而是当没有任何变量引用它时才会被垃圾回收机制处理。
JavaScript中,对象分为普通对象和函数对象。
凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。
Object 、Function 是 JavaScript 自带的函数对象,此处不再证明。
函数对象都有一个prototype(原型对象)属性,这个属性对应该函数的原型对象,原型对象在定义函数时就被创建。
function Person(name,age){
this.name=name;
this.age=age;
this.sayName=function(){
console.log(this.name,this.age);}
}
Person.prototype.eat=function () {
console.log("吃");
}
var p1=new Person('a',1);
var p2=new Person('b',2);
p1.sayName();// a 1
p2.sayName();// b 2
console.log(p1.constructor == Person);//true
console.log(p2.constructor == Person);//true
console.log(Person.prototype);//{constructor: ƒ}
console.log(Person.prototype.constructor==Person);//true
console.log(p1.__proto__);//{constructor: ƒ}
console.log(p1.__proto__ === p1.constructor.prototype);//true
console.log(p1.__proto__ === Person.prototype);//true
prototype有一个默认的constructor属性,这个属性指向prototype所在的函数,即Person.prototype.constructor==Person;
发现没,实例p1与p2的构造函数和Person.prototype的构造函数都是Person,所以Person.prototype也是Person的实例,
也就是前面说的Person在创建的时候就创建了一个实例对象并赋值给它的prototype,
得出结论:原型对象是构造函数的一个实例。
说完了原型,那原型链又是啥?
普通对象没有prototype属性,但有__proto__ 属性。
换句话说,每个对象都有__proto__ 属性,但只有函数对象才有 prototype 属性。
所有对象的 __ proto __ 都指向其构造函数的 prototype,
实例p1的__ proto __ 指向创建它构造函数的原型对象,即p1.__ proto __=== p1.constructor.prototype。
那么问题来了,p1 . __ proto __ === Person.prototype,
Person.prototype这个对象也有__proto__属性,它指向谁呢,它的构造函数是谁呢?
console.log(typeof Person.prototype) //Object
答案就是Person.prototype. __ proto __ === Object.prototype,
好继续,Object.prototype. __ proto __ 又指向谁呢?
那就是null(null处于原型链顶端)!
原型链由原型对象组成,而JavaScript中万物皆对象,每个对象都有__proto__ 属性,指向其构造器的 prototype, __ proto__将对象连接起来组成了原型链。
原型链具有属性查找机制:当查找对象的属性时,如果实例自身不存在该属性,则会沿着原型链往上一级查找,找到时则输出相应内容,若是找到最顶层Object.prototype时还没找到,则输出undefined。
经典的栗子拿出来
var animal = function(){};
var dog = function(){};
animal.price = 2000;
dog.prototype = animal;
var tidy = new dog();
console.log(dog.price) //undefined
console.log(tidy.price) // 2000
当访问dog身上的price属性时,并没有找到,那么dog这个函数的__proto__ 是谁呢,
所有函数对象的 __ proto__ 都指向 Function.prototype,它是一个空函数,所以后面的链条上就更不会有price属性了。
而当访问tidy身上的price属性时,也没有找到,那么tidy. __ proto__ 指向其构造器的prototype,也就是dog.prototype。
得出结论:原型链的链接(__ proto __)是存在于实例与构造函数的原型之间,而不是实例与构造函数之间。
说了半天原型和原型链到底有啥用呢~
通过原型可以实现对象的属性继承,下面介绍4中经典继承方法。
- 原型链继承
核心思想:将父类的实例作为子类的原型 。
优点:操作简单,父类新增的原型方法和属性,子类都可以访问到。
缺点:无法实现多继承,无法向父类传参。
function child() {}
child.prototype=new Person("l",24);
var c = new child();
c.sayName(); // l 24
- 构造继承
核心思想:利用call或apply把父类中通过this指定的属性和方法复制到子类创建的实例中 。
优点:可以call多个父类,实现多继承,可向父类传参。
缺点:只能继承父类的实例属性和方法,不能继承原型属性和方法。
function child() {
Person.call(this,"j","25");
}
var c = new child();
c.sayName(); //j 25
c.eat(); // c.eat is not a function
- 组合继承
核心思想:通过调用父类构造,再将父类的实例作为子类的原型 。
优点:可向父类传参,可以继承原型属性和方法。
缺点:调用了两次父类构造函数。
function child() {
Person.call(this,"k","26");
}
child.prototype=new Person();
var c = new child();
c.sayName(); // k 26
c.eat(); // 吃
- 寄生组合继承
核心思想:通过寄生方式,砍掉了父类实例属性 。
优点:很完美了。
缺点:相对复杂一点点。
function child() {
Person.call(this,"h","27");
}
(function () {
var Super = function () {};
Super.prototype = Person.prototype;
child.prototype = new Super();
})();
var c = new child();
c.sayName(); //h 27
c.eat(); // 吃