一、什么是构造函数
首先在ES6之前,并没有引入类的概念,也可以说对象不是通过类来创建的,而是通过构造函数来定义对象。
1.1创建对象的几种方式
第一种: 字面量
var Obj1 = {name:"o1"}
第二种:利用 new Object() 创建对象
var Obj2 = new Object({name:"o2"})
第三种:构造函数
function O(name) {
this.name = name
};
var Obj3 = new O("o3")
第四种:Object.create
var P = {name:"o4"}
var Obj4 = Object.create(P)
当然不同方式创建的对象,略有不同。
1.2 构造函数使用
这里,我们主要以第三种方式为例,构造函数是一种特殊的函数,主要用来初始化对象,总与new一起使用。 new在执行时会做以下四件事:
(1)内存中创建一个新的空对象
(2)让this指向这个新对象
(3)执行构造函数中的方法,给这个新对象添加属性和方法。
(4)返回这个新对象,所以不需要在构造函数中写return
现在让我们改写一下第三种方式的代码(添加一个方法):
function O(name) {
this.name = name
this.doAction = function(){
console.log(this.name)
}
};
然后,我们使用这个构造函数创建两个对象(剧透,这样的方式会导致浪费内存!!!)
var Obj3_1 = new O("o3_1")
var Obj3_2 = new O("o3_2")
对比一下这两个对象的doAction方法
console.log(Obj3_1.doAction == Obj3_2.doAction)
console.log(Obj3_1)
console.log(Obj3_2)
打印结果:
也就是说明这两个对象的action方法是不同地址的,即各自开辟了doAction方法的内存空间(可以理解为使用了双倍的内存空间),这样就导致了创建的新对象比较多时,会占用比较大的内存空间。
那么怎么优化,或者说使用哪种方法能改变这种浪费内存空间的方法呢?
二、JS原型
我们希望所有对象能使用同一个函数,不用再新开辟内存空间。
2.1 构造函数原型 prototype
so,我们就需要使用构造函数原型 prototype。通过它,我们可以让所有对象共享构造函数!
我们打印一下构造函数O
可以看到构造函数中有一个prototype 对象,这个对象的所有属性和方法都会被构造函数拥有。
把那些不变的方法,直接定义到prototype 对象中,即可实现所有对象的实例共享这些方法。
上代码
// 第三种:构造函数
function O(name) {
this.name = name
// this.doAction = function(){
// console.log(this.name)
// }
};
O.prototype.doAction = function(){
console.log(this.name)
}
再对比一下这两个对象的doAction方法
console.log(Obj3_1.doAction == Obj3_2.doAction)
console.log(Obj3_1)
console.log(Obj3_2)
打印结果:
此时他们的doAction方法的内存空间一致。即他们使用了同一块空间。
此时新的对象实例就可以使用doAction方法了
Obj3_1.doAction()
Obj3_2.doAction()
2.2对象原型__proto__ 属性
但是为什么不同的对象实例可以使用构造函数原型prototype中的方法呢。
即为啥obj3_1and obj3_2在没有开辟自身的doAction方法空间的时候,却使用doAction方法呢?
我们打印一下这两个对象实例
我们发现每个对象实例中都会有一个__proto__ 对象。这个属性指向了构造函数原型prototype(类似指针·)
对比一下构造函数中的prototype属性
是不是和__proto__ 的内容一模一样!
当执行doAction方法的时候,首先会查找这个对象实例本身有没有这个方法,如果没有,就会通过这个对象实例的__proto__ 属性查找构造函数原型prototype上有没有这个方法,如此依次往上。
2.3 constructor 构造函数
细心的小伙伴往往会发现无论是__proto__ 还是prototype都会有一个属性constructor
console.log(O.prototype)
console.log(Obj3_1.__proto__)
我们震惊的发现constructor里面的内容即为构造函数O本身。
console.log(O.prototype.constructor == Obj3_1.__proto__.constructor)
console.log(O.prototype.constructor == O)
结果为 true, 即constructor指向了构造函数本身
三、JS原型链
每个构造函数都有一个原型对象protype,原型对象都包含一个指向构造函数的指针constructor,构造函数的实例也有一个指向原型对象的内部指针__proto__。
当这个原型对象是另一个类型的实例,那么这个原型对象就有一个内部指针指向另一个原型,以此类推就构成了一条原型链。原型链的根就是Object.prototype。
下图表示原型链关系:
由这张图想到是不是直接可以给Object的prototype增加一个方法,扩展一下js内置的函数呢,比如数组求和?
四、原型继承
4.1 组合继承
// 借用父构造函数继承属性
// 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);
4.2 拷贝继承
function Person() {}
Person.prototype.name = "小红"
Person.prototype.age = 18
function Student() {}
Student.prototype.score = 100
var p = Person.prototype;
var s = Student.prototype;
for (key in p) {
s[key] = p[key]
}
console.dir(Person)
console.dir(Student)
类似于复制,把一个对象中的属性和方法直接复制到另一个对象中
你可以封装一下拷贝代码
function extend(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
}
4.3 利用空对象作中介实现继承
function Person() {};
Person.prototype.name = "小红";
Person.prototype.age = 11;
function Student() {};
var F = function () {};
F.prototype = Person.prototype;
Student.prototype = new F();
Student.prototype.constructor = Student;
Student.prototype.age = 25;
console.dir(Person)
console.dir(Student)
封装一下
function extend(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.par = Parent.prototype;
}
突然想到ES6中增加了类和继承,从某种程度上来讲是不是就是给原型继承披了层好看的衣服呢?
最后,摘抄了网上一段对原型链继承的总结
属性共享和独立的控制,当你的对象实例需要独立的属性,所有做法的本质都是在对象实例里面创建属性。若不考虑太多,你大可以在Person里面直接定义你所需要独立的属性来覆盖掉原型的属性。总之,使用原型继承的时候,要对于原型中的属性要特别注意,因为他们都是牵一发而动全身的存在。