理解原型和原型链
本文会采用 概念 + 习题 来帮助理解
概念
理解原型之前,我们先来说说 构造函数 和 使用构造函数创建对象
构造函数和创建实例
举个例子:
定义了一个普通的Person函数,此时它还是一个普通的函数
function Person(name,age) {
this.name = name,
this.age = age,
this.sayName = function(){
console.log(this.name)
}
}
这个时候我做这个操作
var p1 = new Person('zhangsan',18);
此时这个Person
函数不是普通的函数了,而变成了一个构造函数。并且实例化了一个对象 p1
实例化后的对象p1
内部就会有构造函数的属性以及方法了。
这里直接提出一个问题,为什么new
了一下Person
函数之后,就会创建了一个对象,并且这个对象还存有构造函数Person
的属性和方法呢。
这里就不得不提到new操作之后会执行那些步骤 :
1.创建了一个对象
var obj = new Object();
2.将新对象的__proto__
指向构造函数的prototype
obj.__proto__
= Person.prototype
3.将构造函数中的this指向
Person.call(obj)
这里就是为什么构造函数中使用 this.name = name
这样,创建不同的对象,this总是指向那个创建的被创建的对象。
4.执行构造函数中的代码
5.若构造函数返回一个对象,则返回改对象,否则返回新对象
理解创建对象的过程之后,我们来说一个构造函数的弊端。
举个例子:
//使用刚才的代码示例
function Person(name,age) {
this.name = name,
this.age = age,
this.sayName = function(){
console.log(this.name)
}
}
//创建两个对象
var p1 = new Person('zhangsan',18)
var p2 = new Person('lisi',20)
//判断两个对象的Function是不是同一个
console.log(p1.sayName == p2.sayName)//false
看上面的例子,说明构造函数中定义的方法在创建每一个实例的时候都会被创建一次,所以为什么两个方法同名却不相等。所以为了优化这种弊端,我们不得不把属性和方法分开定义。
这个时候直接上原型。
原型与原型链
我们刚才说了构造函数如果定义方法,会产生一定的弊端,那么我们将用原型来解决这一弊端
那么原型是什么呢?
每个函数都会创建一个 prototype
属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和
方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的
属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。
我个人简单来说 :
原型就是构造函数的另一个储存空间,实例化对象的对象属性继承自构造函数,方法则继承原型。
构造函数.prototype
就是 原型对象
举个例子:
function Person(name,age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() {
console.log('我是原型上的方法输出对象的名字:' + this.name)
}
var person1 = new Person('张三',18)
var person2 = new Person('李四',28)
console.log(person.name)
console.log(person.age)
console.log(person1.sayName == person2.sayName)//true 解决了构造函数的弊端。
person.sayName()
上面的例子,在Person
构造函数定义方法,在Person
原型定义了一个方法sayName
输出了对象的name
age
属性 和 调用了sayName
方法
这里可能有人会疑问 可以输出 姓名和年龄 是因为在构造函数定义了,而sayName
方法不是定义在构造函数的怎么调用的呢
原型链
这里我就引出三个属性 __proto__
constructor
prototype
我画个图让你理解一下这三个属性的关系
可以知道
__proto__
就是对象中一个可以指向Person.prototype
的属性指针
constructor
是对象中一个可以指向构造函数的属性
prototype
就是一个构造函数.prototype就是一个原型
看了图之后我们也就知道为什么 person
对象可以调用不是构造函数定义的方法sayName
person
会类似于链式作用域查找一样,找到它所要调用的方法,这种链式查找可以理解为原型链,图中只是原型链上的一小部分。
看了上面的概念或许还不是很理解原型和原型链的概念,下面直接做习题来加深理解吧。
习题
示例一:
function Fn() {
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x);
}
}
Fn.prototype.getX = function () {
console.log(this.x);
};
Fn.prototype.x = 20
Fn.prototype.getY = function () {
console.log(this.y);
};
let f1 = new Fn();
let f2 = new Fn();
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();
解释:
console.log(f1.getX === f2.getX); 1
console.log(f1.getY === f2.getY); 2
1
: f1
和 f2
是两个不同的对象,所以创建这两个对象就相当于创建了两个不同构造函数的方法getX
构造函数创建的弊端,创建每一个对象,都会给每一个对象创建一个
1
为 false
2
: 上面的概念说了,原型中的方法是公用的,所以不必创建的一个对象就就再创建一个方法
2
为 true
console.log(f1.__proto__.getY === Fn.prototype.getY); 3
console.log(f1.__proto__.getX === f2.getX); 4
console.log(f1.getX === Fn.prototype.getX); 5
console.log(f1.constructor); 6
console.log(Fn.prototype.__proto__.constructor); 7
3
: f1.__proto__
就是指向 Fn
的原型对象 获取Fn.prototype
中的方法 getY
所以为 true
4
: f1.__proto__
就是指向 Fn
的原型对象 获取Fn.prototype
中的方法 getX
,
f2.getX
中的方法getX
是从构造函数实例出来的,所以两个不相同 为 false
5
: 跟 4
的情况类似 false
6
: f1
的构造函数就是 Fn
7
: Fn.prototype.__proto__
这里就是指向 Fn
原型的原型 就是 Object.prototype
其实就相当于问 Object.prototype
的构造函数 Object
f1.getX(); 8
f1.__proto__.getX(); 9
f2.getY(); 10
Fn.prototype.getY(); 11
8
: 就是调用实例化对象里面的getX
答案为100
10
: 这里实例化对象中没有getY
,只能找原型上面的getY
,this
调用问题,谁调用指向谁,所以答案为200
9
: f1.__proto__
相当于 Fn.prototype
调用 ,this指向问题 原型上有x
所以为 20
11
: Fn.prototype.getY()
this指向问题 ,原型上没有y
则输出 undefined
总结:
1.先理解好创建构造函数的new过程
2.理清楚构造函数定义方法和原型构造函数的区别
3.三个属性 __proto__
constructor
prototype
关系
4.可以自己画图来理解
以上如有错误欢迎指正,一起学习 LOVE&&PEACE