原型,原型链的相关理解
在理解原型与原型链的相关概念之前,我们先来了解一些概念:
- JS分为函数对象和普通对象,每个对象都有__protp__属性,但只有函数对象才有prototype属性。
- Object,Function是JS的内置函数,类似的还有Array,RegExp,Date,Boolean,Number,String。
- 属性__proto__是一个对象,它有两个属性,constructor和[[Prototype]]。
- 原型对象prototypr有一个默认的constructor属性,用于记录实例是由哪个构造函数创建的。
构造函数
讲原型之前,离不开构造函数,在讲原型之前,我们先来了解一下构造函数。
构造函数的组成
构造函数里面有实例成员和静态成员
实例成员:在构造函数内部通过this添加的成员。实例成员只能通过实例化的对象来访问。
静态成员:在构造函数本身添加的成员,只能通过构造函数来访问。
function Person(name,age) {
// 实例成员
this.name = name
this.age = age
}
// 静态成员
Person.sex = '男'
let person1 = new Person('zz',22)
console.log(person1);
console.log(person1.sex);//实例对象无法访问静态成员
console.log(Person.name);//构造函数无法直接访问实例成员
console.log(Person.sex);//构造函数可以直接访问静态成员
构造函数创建对象
在JAVA中,对象一般都是通过new关键字来创建的,在JS中同样也可以通过new来创建一个对象。
function Animal(name,sex) {
this.name = name
this.sex = sex
}
let dog = new Animal('h',7)
console.log(dog);
此时就通过构造函数Animal创建了一个对象dog
通过new关键字创建一个对象时发生了什么
- 创建一个新对象dog{}
- 为dog准备原型链连接dog.__ proto__ = Animal.prototype
- 重新绑定this,使构造函数的this指向为新的对象Animal.call(dog)
- 为新对象重新赋值dog.name = h,dog.sex = 7
- 返回this return this,此时的新对象就有了构造函数的属性和方法。
实例上的方法
- 在构造函数上定义的方法
function Animal() {
this.eat = function () {
console.log('会吃饭');
}
}
let dog = new Animal()
let cat = new Animal()
dog.eat()
cat.eat()
console.log(dog.eat == cat.eat);
在构造函数上定义的方法,所有通过构造函数实例化的对象都能调用,但是他们调用的都不是同一个方法。学过JAVA的都知道,通过new关键字创建一个新的对象时,会在内存空间空间中的堆空间中开辟一个新的空间来存储。每new一个对象都会在堆空间中开辟一个新空间。所以每个对象都是不一样的。在JS中也差不多是这样的,所以每个实例对象都可以调到构造函数上定义的方法,但这个方法在每个实例对象上是不一样的,
- 通过原型添加方法
function Animal(name) {
this.name = name
}
Animal.prototype.eat = function () {
console.log('会吃饭',this.name);
}
let dog = new Animal('zz')
let cat = new Animal('hh')
dog.eat()
cat.eat()
console.log(dog.eat == cat.eat);
当将构造函数的方法放到它的原型上时,两个实例化对象也都可以使用,并且两个对象使用的方法是同一个方法。
原型
原型的理解
在学习JS之前,我学习过JAVA,我觉得JS的原型类似于JAVA的父类。
在JS中,创建一个函数(非箭头函数)时,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。默认情况下,所有的原型对象都会获得一个名为constructor的属性,指回与之相关的构造函数。
在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object。每次调用构造函数创建一个实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。脚本中没有访问[[Prototype]]特性的标准方法,但Firefox、Safari和Chrome会在每个对象上暴露__proto__属性来访问对象的原型。
function Person() {}
Person.prototype.name = 'zzz'
Person.age = 22
Person.prototype.sayName = function () {
console.log(this.name);
}
let person1 = new Person()
let person2 = new Person()
// 声明之后构造函数就有与之相关的原型对象
console.log(Object.prototype.toString.call(Person.prototype));
console.log(Person.prototype);
// 构造函数有一个prototype属性指向其原型对象,其原型对象有一个constructor的属性指回Person
console.log(Person.prototype.constructor == Person);
// 构造函数、原型对象是实例是三个完全不同的对象
console.log(person1 !== Person);
console.log(person1 !== Person.prototype);
console.log(Person.prototype !== Person);
// 实例通过__proto__链接到原型对象,它实际上是指向隐藏特性[[Prototype]]
// 构造函数通过prototype属性链接原型对象
// 实例与构造函数之间没有什么直接联系,但与原型对象有联系
console.log(person1.__proto__ == Person.prototype);
console.log(Person.prototype.constructor == Person);
// 同一个构造函数创建两个实例对象,两个实例对象共享一个原型
console.log(person1.__proto__ == person2.__proto__);
// Object.getPrototypeOf(),返回参数的内部特性[[Prototype]]的值,用于获取原型对象,兼容性更好
console.log(Object.getPrototypeOf(person1) == Person.prototype);
- 在构造函数Person中存在一个prototype属性指向其原型对象,而原型对象中有个constructor属性来反指回构造函数。
- 实例对象上有一个__proto__属性来指向原型对象。
- 实例对象和原型对象没有什么直接的联系,实例对象可以调取原型对象上的实例方法,实例属性,但每个实例对象都是不一样的。
- 通过同一个构造函数创建出来的实例对象直接是没有直接的联系的,他们在内存空间中都有自己独立的空间,但他们有同一个原型,可以通过在他们原型上添加方法,使他们有共享的方法。
- 实例对象和构造函数也是通过原型链接产生联系的。
更直面的关系如下:
原型的层级
在通过对象访问属性时,会通过这个属性的名称开始搜索,搜索的过程如下:
- 如果在这个实例上发现了给定的名称,则返回该名称对应的值。
- 如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回其对应的值。
例如我们在调用*person1.sayName()*时,会发生两步搜索:
JS引擎先问实例对象person1有sayName方法吗?如果没有,会接着问person1的原型对象,也就是Person.prototype有sayName方法吗?有则返回其值。
原型链
在原型中,构造函数与实例对象是通过原型对象直接产生了联系,构造函数有一个prototype指向原型对象,而原型对象有一个constructor属性返回构造函数,实例对象有一个__proto __ 指向原型对象,这是最基本的原型的理解。
那如果原型是另一个构造函数的实例呢?那就意味着这个原型本身就有一个内部指针指向另一个原型,相应的另一个原型也有一个内部指针指向其他的原型,如此重复下去就形成了一条原型链
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}
let instance = new SubType()
console.log(instance.getSuperValue());
SubType和SuperType两个构造函数分别定义了一个属性,同时他们的原型对象定义了一个方法,用来返回两个构造函数所定义的值,SubType通过创建SuperType的实例对象并将其值赋值给SubType的原型对象实现了对SuperType的继承,这个赋值重写了SubType最初的原型对象,并将其替换成SuperType的实例对象。
此时,SubType实例可以访问的属性和方法同样存在于SubType.prototype,同时SubType.prototype继承了SubType的实例,也就是说,在SubType的实例化对象中,我们可以访问到SuperType.prototype的所有属性和方法。此时就形成了一条简单的原型链。