带着问题出发:
- 什么是原型、原型链?
- 原型和原型链有什么关系?
- __proto__和prototype分别是什么?
- 构造函数、实例、对象原型之间的关系?
不妨先想想上面的问题,动一下大脑,是否能回答的上~
定义理解
- 原型
- 构造函数的定义:
构造函数是用于创建特定类型对象的。像Object和Array这样的原型构造函数,运行时可以直接在执行环境使用。
构造函数也是函数,按照惯例,构造函数名称的首字母都是要大写的;创建构造函数的实例应该使用 new 操作符;在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。 - 原型的定义:
每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。
实际上,prototype属性的这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上上面定义的属性和方法可以被对象实例共享。(普通对象没有prototype属性,有__proto__属性)
function Person(){}
Person.prototype.name = "guava"
Person.prototype.sayName = function(){
console.log(this.name)
}
let person1 = new Person()
person1.sayName() // "guava"
let person2 = new Person()
person2.sayName() // "guava"
console.log(person1.sayName === person2.sayName) // true
理解原型:
创建函数时,会创建一个prototype属性(指向原型对象);默认情况下,原型对象都会有一个 constructor 的属性(指向与之关联的构造函数),就上面的例子而言就是:Person.prototype.constructor === Person,而其自带的其他方法都继承于Object(如toString)。
每次调用构造函数创建新实例时,实例内部的[[Prototype]]指针就会赋值为构造函数的原型对象,而脚本中没有访问这个[[Prototype]]特性的标准方式,但一些浏览器Firefox、Safari和Chrome会在每个对象中暴露了__proto__属性,通过这个属性可访问到原型对象。
关键的在于理解一点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
function Person(){}
console.log(Person.prototype.constructor === Person) // true
/**
* 正常的原型链都会终止于Object的原型对象
* Object的原型对象是null
*/
console.log(Person.prototype === Object.prototype) // true
console.log(Person.prototype.constructor === Object) // true
console.log(Person.prototype.__proto__.__proto__ === null) // true
let person1 = new Person()
/**
* 构造函数、原型对象和实例,是三个完全不同的对象
*/
console.log(person1 !== Person) // true
console.log(person1 !== Person.prototype) // true
console.log(Person.prototype !== Person) // true
/**
* 实例的__proto__,指向原型对象(实际上指向隐藏特性[[Prototype]])
* 构造函数的prototype,链接到原型对象(可以理解成Prototype就是原型对象)
* 实例与构造函数没有直接联系,与原型对象有直接联系
*/
console.log(person1.__proto__ === Person.prototype) // true
console.log(person1.__proto__.constructor === Person) // true
let person2 = new Person()
/**
* 同一个构造函数创建的多个实例,会共享同一个一个原型对象
*/
console.log(person2.__proto__ === Person.prototype) // true
console.log(person1.__proto__ === person2.__proto__) // true
原型层级:
通过对象访问属性时,搜索开始与实例本身,如果没有找到这个属性,则沿着指针(proto)进入原型对象找到属性后,并返回对应的值。hasOwnProperty()方法用于确认某个属性是否再实例上还是再原型对象上。
function Person(){}
Person.prototype.name = 'guava'
let person1 = new Person()
console.log(person1.hasOwnProperty('name')) // false
person1.name = 'sixteen'
console.log(person1.hasOwnProperty('name')) // true
delete person1.name
console.log(person1.name) // "guava"
console.log(person1.hasOwnProperty('name')) // false
原型和 in 操作符:
有两种方式使用 in 操作符:单独使用和 for-in 使用。in 操作符可以访问实例或原型上的属性。
function Person(){}
Person.prototype.name = "guava"
Person.prototype.age = 25
let person1 = new Person()
console.log("name" in person1) // true 来自原型
person1.name = "sixteen"
console.log("name" in person1) // true 来自实例
for(let key in person1){
console.log(key)
}
//name
//age
其他原型语法:
在重写原型对象后,原型的constructor属性不再指向其原来的构造函数,会指向到完全不同的新对象(Object 构造函数)。重写构造上的原型之后再创建的实例,才会引用新的原型,而在此之前创建的实例仍然会引用最初的原型。
function Person(){}
//重写原型对象
Person.prototype = {
name:"guava"
}
let person1 = new Person()
//重写原型对象后,构造函数指向了新对象(Object 构造函数)
console.log(person1.__proto__.constructor === Person) // false
console.log(person1.__proto__.constructor === Object) // false
Person.prototype.sayHi = function(){
console.log("Hi!")
}
let person2 = new Person()
person2.sayHi() // "Hi!"
//person1是 添加原型方法sayHi 前实例化的,所以指向的是最初的原型,所以没有sayHi方法
person1.sayHi() // 报错
- 原型链
ECMA-262把原型链定义为ECMAScript的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。每个构造函数都有一个原型对象,原型有一个属性指向构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例,则原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。
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(),instance.getSubValue()) // true , false
代码中可以看到分别定义了两个类型 SuperType和SubType,并各自定义了一个属性和方法,SubType通过创建SuperType的实例并将其赋值给自己的原型Subtype.prototype实现了对SuperType的继承。还要注意,由于SubType.prototype的constructor属性被重写为指向SuperType,所以instance.constructor指向了SuperType。
总结
- 在创建构造函数时,自动生成一个prototype属性,此属性即原型对象,原型对象中默认生成一个constructor属性,指向其构造函数。
- 而通过构造函数创建实例时,内部会生成一个[[Prototype]]的特性(指向原型对象),脚本中没有标准的方式访问该属性,所以一些浏览器在每个对象中暴露了__proto__的属性来访问其原型对象。
- 访问对象属性时,会从实例上开始查找,如果没有存在该属性,会访问原型对象获取属性。如果查找对象属性的实例指向的原型,是其他类型的实例,则又会从该实例和其指向原型继续查找,这样不断向上查找就形成了原型链的基本思想。
参考
《JavaScript高级程序设计》