一、构造函数和原型
1.1 构造函数的问题
通过构造函数实例化对象虽然很好用,但是他也存在一些问题,首先一个就是内存浪费的问题。
下面我们来看一个例子
我们可以给函数的原型添加函数,这样的话,函数所有的实例化对象都可以使用这个函数,大大减少函数重复占用内存。
1.2 构造函数原型prototype
获取原型的方法:
- 通过对象的__proto__来获取
- 通过构造函数的prototype属性来拿到原型
构造函数通过原型分配的函数是所有对象所共享的
JavaScript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象。
注意这个prototype也是 一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
Star.prototype.sing = function() {
console.log('我会唱歌');
}
var zsf = new Star('张三丰', 18);
var zxy = new Star('张学友', 19);
zsf.sing();//我会唱歌
zxy.sing();//我会唱歌
1.3 对象原型
刚才我们说了,每个函数都有一个prototype属性,这个属性就是构造函数的原型,那当函数被实例化以
后呢?
构造函数被实例化后的对象都会有一个属性 __proto __ 指向构造函数的 prototype 原型对象,之所以
我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __proto __ 原型的
存在。
从而我们知道: __proto __ 对象原型和原型对象 prototype 是等价的,
__proto__ 对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非
标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype。
1.4 constructor构造函数
上面我们说了构造函数和对象的原型,在对象的原型(proto)和构造函数(prototype)原型对象里
面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。
1.5 原型链
每一个实例对象又有一个proto属性,指向的构造函数的原型对象,构造函数的原型对象也是一个对
象,也有proto属性,这样一层一层往上找就形成了原型链。
我们看如下图所示:
1.6 构造函数实例和原型对象三角关系
综上所述:
构造函数的prototype属性指向了构造函数原型对象
实例对象是由构造函数创建的,实例对象的proto属性指向了构造函数的原型对象
构造函数的原型对象的constructor属性指向了构造函数,实例对象的原型的constructor属性也指向
了构造函数
由上面所述内容,我总结构造函数,实例化对象,原型对象三者之间的关系如下图所示:
二、继承
面向对象的三个特点就是封装,继承和多态,通过继承,我们可以实现对象和方法的复用,从而提高代
码的利用率,也是代码优化的一种方案。下面我们看一下js的继承是怎么实现的?
2.1 子构造函数继承父构造函数中的属性
上面说了call方法的基本使用,下面我们看一下如何使用call完成函数的继承呢?也就是我们经常所说的
构造函数继承,主要分为三个步骤:
- 先定义一个父构造函数
- 再定义一个子构造函数
- 子构造函数继承父构造函数的属性(使用call方法)
// 1. 父构造函数
function Father(uname, age) { // this 指向父构造函数的对象实例
this.uname = uname;
this.age = age;
}
// 2 .子构造函数
function Son(uname, age, score) { // this 指向子构造函数的对象实例 3.使用call方式实现子继承父的属性
Father.call(this, uname, age);
this.score = score;
}
var son = new Son('张三丰', 18, 100);
console.log(son);
2.2 借用原型对象继承方法
当然我们也可以借用原型来继承函数,步骤大致如下所示:
- 先定义一个父构造函数
- 再定义一个子构造函数
- 子构造函数继承父构造函数的属性(使用call方法),把父方法的实例化对象赋给子方法的原型。
代码如下所示:
// 1. 父构造函数
function Father(uname, age) { // this 指向父构造函数的对象实例
this.uname = uname; this.age = age;
}
Father.prototype.money = function() {
console.log(100000);
};
// 2 .子构造函数
function Son(uname, age, score) { // this 指向子构造函数的对象实例
Father.call(this, uname, age);
this.score = score;
}
// Son.prototype = Father.prototype; 这样直接赋值会有问题,如果修改了子原型对象,父原型 对象也会跟着一起变化
Son.prototype = new Father(); // 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数
Son.prototype.constructor = Son; // 这个是子构造函数专门的方法
Son.prototype.exam = function() {
console.log('孩子要考试');
}
var son = new Son('张三丰', 18, 100);
console.log(son);
2.3 组合继承(伪经典继承)
function SuperType (name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType (name, age) { // 继承属性
SuperType.call(this, name) // 第二次调用SuperType()
this.age = age
}
//继承方法
SubType.prototype = new SuperType() // 第一次调用 SuperType()
Subtype.prototype.sayAge = function () {
alert(this.age)
}
实现思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承
注意: 组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为js中最常用的继承方式
2.5 ES6继承
先看下如何定义类
class Parent {
constructor(name,age) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
}
类的所有方法都定义在类的prototype属性上面。且class不存在变量提升,如果在class声明之前调用,会报错。
类的继承
class Child extends Parent {
constructor(name,age,sex) {
super(name,age);
this.sex = sex;
}
}
var child = new Child('xiaoyu',12,'man');
在子类的构造函数中,如果显示声明了constructor,则必须要显示的调用super函数(这一点和Java有
点不一样)。
只有调用super之后,才可以使用this关键字,否则会报错。