原型和原型链的理解:(面试题)
- 原型:每个函数都有 prototype 属性,该属性指向原型对象;使用原型对象的好处是所有对象实例共享它所包含的属性和方法。
- 原型链:主要解决了继承的问题;每个对象都拥有一个原型对象,通过__proto__ 指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null。
原型的作用:
1.数据共享 节约内存内存空间
2.实现继承
注意:函数也是一个对象,对象不一定是函数。(对象有__proto__属性,函数有prototype属性)此处说明,方便大家理解下文。
下面我将举例说明为什么要使用原型
例1:
function Person(name) {
this.name=name;
this.eat=function () {
console.log(this.name+"吃东西");
}
this.sleep=function () {
console.log(this.name+"睡觉");
}
}
var p1=new Person("小明");
p1.eat();//小明吃东西
p1.sleep();//小明睡觉
var p2=new Person("小利");
p2.eat();//小利吃东西
p2.sleep();//小利睡觉
console.dir(p1);//dir()打印结构
console.dir(p2);
每次使用构造函数Person()实例化出对象的时候,就会给每个实例对象的eat()方法和sleep()方法开辟空间。可是当实例化许多对象的时候,就会浪费大量的空间,因为每个实例对象的eat()方法和sleep()的功能都是一样的,所以我们没必要为每个实例对象添加eat()方法和sleep()方法。
这时原型就派上用场了,看下面经过改造的例子:
function Person(name) {
this.name=name;
}
Person.prototype.eat=function () {
console.log(this.name+"吃东西");
};
Person.prototype.sleep=function () {
console.log(this.name+"睡觉");
}
var p1=new Person("小明");
p1.eat();//小明吃东西
p1.sleep();//小明睡觉
var p2=new Person("小利");
p2.eat();//小利吃东西
p2.sleep();//小利睡觉
console.dir(p1);
console.dir(p2);
eat()方法和sleep()被添加到了Person()构造函数的原型(prototype)上了。因此只有一份eat()方法和sleep()方法。当实例对象需要使用这些方法的时候就在自己的__proto__属性中找到并调用这些方法实现相应的功能。
现在我们来捋一下构造函数,实例对象,原型对象之间的关系。
如以下代码和图所示:
Person()构造函数的prototype属性是一个对象,实例对象p1的__proto__属性也是一个对象,并且prototype对象和__proto__对象的指向相同。那么我们再回过头来理解一下为什么添加到原型的方法可以是共享的。因为prototype对象和__proto__对象的指向相同,所以将eat()方法和sleep()添加到Person()构造函数的prototype属性上之后,实例对象就可以通过自己__proto__属性去访问eat()方法和sleep()了。
console.dir(Person);
console.dir(p1);
console.log(typeof p1.__proto__);//object
console.log(typeof Person.prototype);//object
console.log(p1.__proto__ === Person.prototype);//true
__proto__指向该对象所在的构造函数的原型对象。
实例对象和构造函数之间没用直接的关系。原型对象与实例对象之间用原型(__proto__)关联,这种关系叫做原型链。
我是这样理解原型链的(可能不是很准确)我向我爸要零花钱,我爸也没有钱,那么我就向我奶奶要,奶奶要是也没有,就继续找别人要。
那么原型的指向可以改变吗?答案是可以的。
举个例子:
-
function Person(name) {
-
this.name=name;
-
}
-
Person.prototype.eat=function () {
-
console.log(this.name+"吃东西");
-
};
-
Person.prototype.sleep=function () {
-
console.log(this.name+"睡觉");
-
}
-
function Student(school) {
-
this.school=school;
-
}
-
Student.prototype.write=function () {
-
console.log("写作业");
-
}
-
Student.prototype=new Person("小华");//改变Student()构造函数的指向,让Student()构造函数的原型对象指向Person的实例对象
-
var s1=new Student("某某高中");
-
s1.eat();//小华吃东西
-
s1.sleep();//小华睡觉
-
s1.write();//Uncaught TypeError: s1.write is not a function,因为Student()的原型的指向改变,所以找不到write()方法
-
console.dir(Student);
-
console.dir(s1);
__proto__指向该对象所在的构造函数的原型对象。如上图所示:Studend()构造函数的原型(prototype)指向了Person()的实例对象(new Person("小华")),所以Studend()的实例对象s1的__proto__也指向了Person()的实例对象((new Person("小华"))。而实例对象((new Person("小华"))的__proto__指向了其所在的构造函数Person()的原型对象在这个原型对象中,找到了eat()方法和sleep()方法。
从这个例子中,可以发现,利用原型可以实现继承。面向对象的编程语言中有(class)类的概念,但是JavaScript不是面向对象的语言,所以js中没有类(class)(ES6中实现了class),但是js可以模拟面向对象的思想编程,js中通过构造函数来模拟类的概念。
改变原型的指向可以实现方法的继承。借用构造函数继承,主要解决属性的问题
function Person(name) {
this.name=name;
}
Person.prototype.eat=function () {
console.log(this.name+"吃东西");
};
Person.prototype.sleep=function () {
console.log(this.name+"睡觉");
}
function Student(name,school) {//name为父类构造器传参。子类构造器可以添加自己特有的属性school
Person.call(this,name);//调用父类构造器Person的属性,
this.school=school;
}
Student.prototype.write=function () {
console.log("写作业");
}
Student.prototype=new Person();//改变Student()构造函数的指向,让Student()构造函数的原型对象指向Person的实例对象
var s1=new Student("zx","某某高中");
s1.eat();//小华吃东西
s1.sleep();//小华睡觉
console.dir(Student);
console.dir(s1);
组合继承就是指:将改变原型的指向和借用构造函数两者结合在一起实现继承。
一、原型模式
我们创建的每一个函数都有一个prototype
(原型)属性,这个属性指向的是通过调用构造函数来创建出来的对象实例的原型对象,这个原型对象可以让所有对象实例共享它所包含的属性和方法。
function Person () {
}
Person.prototype.name = "xiao";
Person.prototype.sayName = function () {
alert('this.name')
}
var person1 = new Person();
var person2 = new Person();
person1.sayName()
console.log(person1.name == person2.name) // "true"
上面的例子当中我们创建了一个构造函数Person
,并通过它的prototype
属性在它的原型对象中定义了name
属性并赋值,然后通过调用构造函数Person
实例化出来两个对象实例,通过打印出来的值我们可以得知,person1
和person2
共享了原型对象中的属性和方法。
构造函数,原型对象和对象实例的关系
我们知道每个函数都有一个prototype
属性指向函数的原型对象。在默认情况下,所有原型对象都有一个constructor
(构造函数)属性,这个属性指向了prototype
属性所在的函数,比如前面的例子中,Person.prototype.constructor
就指向Person
。
另外,当调用构造函数创建一个新实例后,该实例的内部将包含一个__porto__
属性(仅在Firefox、Safari、Chrome中支持),这个属性指向的就是构造函数的原型对象。由此我们可以得出以下图示的结论:
通过代码来验证:
# 实例和原型对象之间的关系
console.log(person.__proto__ == Person.prototype) // true
# 也可以通过isPrototypeOf()和ES5中的Object.getPrototypeOf()方法来判断
console.log(Person.prototype.isPrototypeOf(person1)) // true
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
# 原型对象和构造函数的关系
console.log(Person.prototype.constructor == Person) // true
二、原型链
通过前面我们对构造函数,对象实例和原型对象三者关系的描述可知,实例都包含了指向原型对象的内部指针。
那么假如现在我们有两个构造函数A跟B,我们让构造函数A的原型对象等于构造函数B的实例,根据前面的推论,这个时候A的原型对象就包含指向B的原型对象的指针,再假如又有一个构造函数C,让A的原型对象等于C的实例,上述关系依旧成立,以此类推便形成了实例与原型的链条,即原型链,它主要作为JS中实现继承的主要方法。
原型链的基本实现
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
# 继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
var instance = new SubType();
console.log(instance.SuperValue()); // true
在上面的代码中,我们没有使用SubType
默认的原型,而是将SuperType
的实例赋给它,重写了SubType
的原型对象;这样一来SubType.prototype
的内部便具有一个指向SuperType
原型的指针,原来存在于SuperType
的实例中的所有属性和方法,现在也存在于SubType.prototype
中了。instance
同理,还要注意的是由于SubType
的原型指向了SuperType
的原型,而SuperType
的原型的constructor
属性指向的是SuperType
构造函数,那么instance.constructor
也就指向了SuperType
原型搜索机制:
当访问一个实例属性或方法时,在通过原型链实现继承的情况下,首先会在实例中搜索该属性,在没有找到属性或方法时,便会沿着原型链继续往上搜索,直到原型链末端才会停下来。
这里还有一个重要的点,事实上所有引用类型默认都继承了Object
,而这个继承也是通过原型链实现的,也就是说,所有函数的默认原型都是Object
的实例,这也是所有自定义类型都会继承toString()
、valueOf()
等默认方法的根本原因。
Object.prototype的原型
既然所有类型默认都继承了Object
,那么Object.prototype
又指向哪里呢,答案是null
,我们可以通过下面的代码打印试试看:
console.log(Object.prototype.__proto__ === null) // true
null
即没有值,也就是说属性或方法的查找到Object.prototype
就结束了。
本面试题为前端常考面试题,后续有机会继续完善。我是歌谣,一个沉迷于故事的讲述者。
欢迎一起私信交流。