基本概念
原型
原型是 JavaScript 函数的属性。每次在 JavaScript 中创建一个函数时,JavaScript 引擎都会为创建的函数添加一个名为原型的额外属性。
_proto__
和constructor
属性是对象所独有的
prototype
属性是函数所独有的,因为函数也是一种对象,所以函数也拥有_proto__和constructor 属性
对象的__proto__属性等于new 构造函数的prototype
function foo() {
}
console.log(foo.prototype) //foo {}
var f1 = new foo()
console.log(f1.__proto__ === foo.prototype) //true
constructor
属性:原型对象prototype上默认添加的一个属性,指向该对象的构造函数,所有函数(最终的构造函数都指向Function
原型可以使对象共享属性和方法,简化代码量
原型链
原型链即把原型按次序串联起来的链
每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
var obj = {}
obj.__proto__ = {
}
// 原型链
obj.__proto__.__proto__ = {
}
obj.__proto__.__proto__.__proto__ = {
value: "3"
}
console.log(obj.value) //3
查找对象属性或方法时的顺序:
对象本身查找 -->该对象的构造函数中查找-->该对象的原型(_proto_)-->构造函数的原型中 ( prototype)--->当前原型的原型中查找以此类推直至原型链顶端(Object.prototype)
,若还没找到且要查找的为属性则返回 undefined,要查找的为方法则报错
原型链一层层地向上查找,所有对象的原型最终都可以上溯到 Object,此时就没有 _proto_指向Object.prototype(顶层)
,故原型链最顶层的原型对象就是Object.prototype(他的__proto__值为null),该对象上有很多默认的属性和方法
var obj = { }
console.log(obj.__proto__) //{}
console.log(Object.prototype)//{}
console.log(obj.__proto__.__proto__) //null
console.log(Object.prototype.__proto__)//null
手写原型链继承
1.通过原型链实现继承
原型链继承是直接让Child构造函数的prototype直接指向Parent对象,这样Parent的属性Child对象可以直接从它的原型链上找到。核心:Child.prototype = new Parent(); //使用原型链继承
父类:
// 父类: 公共属性和方法
function Parent(name, age, friends) {
//属性
this.name = name || 'A';
this.friends = friends || [];
this.age = age || 18;
//实例方法
this.say = function () {
console.log(this.name + 'say hi');
};
}
//原型方法
Parent.prototype.eating = function() {
console.log(this.name + " eating")
}
子类:
// 子类: 特有属性和方法
function Child() {
this.sno = 111
}
Child.prototype = new Parent(); //原型链继承
Child.prototype.studying = function() {
console.log(this.sno + ' studying');
}
var stu = new Child()
console.log(stu.name) //'A'
stu.eating() //A eating
stu.studying()//111 studying
//子类成功继承父类属性与方法,成功使用自身特有的属性和方法
缺陷:
//创建两个stu对象
var stu1 = new Child()
var stu2 = new Child()
// 修改引用中的值, 不同子例会相互影响
stu1.friends.push("B")
console.log(stu1.friends) //B
console.log(stu2.friends) //也为B,而不是[]
// 无法传递参数
var stu3 = new Child("lilei", 112)
- 创建子类实例时,无法向父类构造函数传参
- 来自原型对象的引用属性是所有实例共享的,故当创建多个实例时,
如果修改的这个对象是一个引用类型
时不同实例会互相影响,因为两个实例使用的是同一个原型对象
2. 使用构造函数继承
原理是在Child构造函数中利用call改变了this指向,可实现向父类传参,无引用属性相互影响问题(没用到原型)
function Child(age) { Parent.call(this, age) }
父类:同原型链父类
子类:
//可传参
function Child( name,age,friends) {
Parent.call(this, name, age, friends);
}
var stu = new Child('AB', 20, ['lilei']);
var stu1 = new Child('C', 18, ['li']);
console.log(stu.name)//AB
// 子类实例不会相互影响
stu.friends.push('lucy');
console.log(stu.friends);//[ 'lilei', 'lucy' ]
console.log(stu1.friends);//[ 'li' ]
//stu.eating() 报错,父函数原型链上的属性与方法不能被继承
缺点:
-只能继承父类的实例属性和方法,父类原型链上的属性/方法不能被继承
3. 组合继承
组合继承 :可以继承父类原型链上的属性/方法,就是在使用构造函数继承的基础上加上对子类原型的操作,Parent的构造函数会多执行一次
function Child(age) { Parent.call(this, age) } Child.prototype = new Parent();
父类:同原型链父类
子类:
function Child(name, age, friends) {
Parent.call(this, name, age, friends);
}
Child.prototype = new Parent();
var stu = new Child('AB', 20, ['lilei']);
console.log(stu.name)//AB
stu.eating() //AB eating
缺点:
- 调用了两次父类构造函数(
new Parent()和Parent.call()
),多消耗了一点点内存(感觉可以忽略) - 父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中
( 当我们在子类型的构造函数中调用父类型.call(this, 参数)时, 就会将父类型中的属性和方法复制一份到了子类型中. 所以父类型本身里面的内容,我们不再需要)
4. 通过寄生组合继承(最推荐)
父类:同原型链父类
子类:
function Child(name, age, friends) {
Parent.call(this, name, age, friends)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
//如果利用对象的形式修改了原型对象,需要利用constructor 指回原来的构造函数,即修复 constructor
var stu = new Child("F", 18, ["FX"])
console.log(stu) //Parent { name: 'F', friends: [ 'FX' ], age: 18, say: [Function] }
stu.eating()//F eating
console.log(stu.constructor.name) //Child
//如果没把Child.prototype.constructor重新指回Child,这里的值会是Parent
/*Object.create()实际上做了两个动作,用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)*/
/*
手写Object.create()
Object.prototype.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
*/
总结:
这四种手写继承的方法每一种的出现都是为了解决前一种方法的缺点,建议手写时将四种方法联系起来思考学习