知识铺垫
构造函数
构造函数是用来批量创建对象的函数,本质上也是函数,区别于其他函数的方式就是调用的方式。
简单点说:如果没有使用new来调用的,就是普通函数。如果使用了new来调用的,就是构造函数。
话不多说直接上代码康康:
function Person(uname, age){
this.uname = uname;
this.age = age;
}
let IronMan = new Person('Tony stark', 30);
Person('Tony stark', 40);
console.log(IronMan);
console.log(uname);
console.log(age);
打印结果如下:
let IronMan = new Person('Tony stark', 30);
中,通过new调用了函数Person
,并且生成了IronMan
,这里的Person
就成了构造函数,IronMan
就成了Person
的一个实例。
原型对象
当我们每次创建一个函数的时候,函数对象都会有一个prototype
属性,这个属性是一个指针,指向它的原型对象,原型对象的本质也是一个对象。看代码:
function Person(uname, age){
this.uname = uname;
this.age = age;
}
console.log(Person.prototype);
打印如下:
Person.prototype
指向一个对象,就是Person的原型对象,在这个原型对象中有一个constructor
属性又指向了Person
构造函数。
构造函数,原型对象和实例的关系
直接上图!从上图可以看到:
- 函数对象
prototype
指向原型对象,原型对象的constructor
指向函数对象。 - 实例对象的
__proto__
指向原型对象,__proto__
的作用是允许实例通过该属性访问原型对象中的属性和方法
function Person(uname, age) {
this.uname = uname;
this.age = age;
}
Person.prototype.sex = 'man';
let IronMan = new Person('Tony stark', 28);
let BlackWidow = new Person('Natalia', 24);
BlackWidow.sex = 'woman'
console.log(IronMan.sex);
console.log(BlackWidow.sex);
打印结果如下:
IronMan
实例设置sex
属性,但因为__proto__
会访问原型对象中对应的属性,所以输出man
;同时,我们给BlackWidow
设置sex
属性后输出的是woman
,两者比较说明实例本身不存在对应的属性和方法时,才会去原型对象上查找对应的属性和方法补充:在这里我们打印一下这句话:
console.log(IronMan.constructor);
打印结果:
constructor
可以访问构造函数,但是constructor
本质上却是原型对象的属性而不是实例对象的,是实例对象通过__proto__
找到原型对象prototype
然后通过原型对象的constructor
指回的构造函数。如果难以理解,我们不妨打印一下IronMan
这个实例:IronMan
上并没有constructor
属性,而是通过__proto__
找到了原型对象,原型对象上拥有constructor
属性并且指向了Person
构造函数。
继承
原型链
在js中继承的主要思路就是利用了原型链。
原型链的原理是:让一个引用类型继承另一个引用类型的属性和方法。
既然我们知道了原型对象通过constructor
指向构造函数,实例通过__proto__
指向原型对象,那我们不妨想一想:如果让原型对象等于另一个构造函数的实例会怎么样?
function Father() {
}
Father.prototype.sayF = function () {
console.log('from Father');
}
function Son() {
}
Son.prototype = new Father();
Son.prototype.sayS = function () {
console.log('from Son');
}
let father = new Father();
let son = new Son();
son.sayS();
son.sayF();
结果:- 首先我们创建了Father和Son两个函数对象,同时也就生成了他们的原型对象。
- 接着我们给Father的原型对象添加了
sayF()
方法 -
Son.prototype = new Father()
,这一步我们让函数对象Son的prototype
指针指向了Father的实例,这也就是为什么Son原型对象里面不再有constructor
属性,其实Son本来有一个真正的原型对象可以通过Son.prototype
访问,结果我们手动修改了这个指针的指向,所以Son真正的原型对象现在没有办法被访问了,取而代之的是Father的一个实例,所以没有constructor
这个属性。(把之前的例子带进来看,其实father这个实例通过__proto__
可以访问到Father的原型对象,Father的原型对象上是有constructor
这个属性的,所以我们通过打印Son.prototype.constructor
,还是可以打印出结果,只不过这个结果却是Father这个构造函数,我们在写的时候通常还需要手动为Son.prototype
添加constructor
属性将它指回Son构造函数:Son.prototype.constructor = Son;
) - 我们给Son.prototype指向的对象,增加一个
sayS
方法 - 生成一个实例son,调用了son的sayS方法,可以执行:这是因为,
son.__proto__
可以访问到Son原型对象上的方法。 - 调用了son的sayF方法,也可以执行:这是因为,son沿着
__proto__
属性,可以访问Son的原型对象,这时并没有sayF这个方法,Son的原型对象继续沿着__proto__
属性访问Father的原型对象,最终在Father.prototype
上找到了sayF方法。
所以现在就相当于son继承了Father的属性和方法,这种由__proto__
不断把实例和原型对象联系起来的结构其实就是原型链。这也是es6之前继承的主要方式。