1.原型(prototype)
JavaScript的每个函数都有一个prototype
属性,它默认指向一个空object实例对象。
我们可以在里面存放一些公用的属性和方法。构造函数的实例对象会自动拥有这些定义在构造函数原型对象中的属性和方法。
function Student(name) {
this.name = name
}
Student.prototype.getName = function () {
return 'My name is ' + this.name
}
let s1 = new Student('jack')
console.log(s1.getName())
-----------------------------
>>>My name is jack
上面的例子中,Student
有一个属性name
。我们通过prototype
向Student
的原型中添加了一个叫getName()
的方法。
然后我们创建了Student
的实例对象s1
,并通过s1
执行getName()
方法。可以看到执行成功并正确输出了结果。
相对应,原型对象中有一个属性constructor
,它指向函数对象。通过prototype
和constructor
,构造函数和它的原型对象实现了相互引用。
2.显式原型和隐式原型
每个函数function都有一个prototype
属性,这就是显式原型。每个实例对象都有一个__proto__
属性,称为隐式原型。
- 函数的
prototype
属性是函数定义的时候自动添加的,默认指向一个空object实例对象。 - 对象的
__proto__
属性是创建对象的时候自动添加的,默认值是构造函数的prototype属性的值。
在内存中,它们的关系如下图所示
当执行
Student
函数定义的时候,堆(Heap)中产生Student
函数的对象(假设地址为0x111),同时在栈(stack)中划出一块区域存储此地址。然后JavaScript又在堆中生成一个空Object对象(假设地址为0x333),并将地址值存储在
Student
函数对象的prototype
属性里。
当创建Student
的实例对象s1
时,堆中产生s1
实例对象(假设地址为0x222),同时在栈(stack)中划出一块区域存储此地址。而s1
实例对象有一个__proto__
属性,初识值也被设置为了0x333
。
所以在上面的例子中,Student.prototype
和s1.__proto__
指向的完全是同一个对象。通过代码可以验证这一点。
console.log(Student.prototype === s1.__proto__)
-----------------------------
>>>true
这就解释了为什么s1
实例对象可以访问到添加在Student
的原型中的方法。因为我们通过prototype
向Student
的原型中添加方法时,方法实际是被添加到了地址0x333的object
对象里面。
Student.prototype.getName = function () {
return 'My name is ' + this.name
}
当实例对象s1
尝试执行getName()
方法时,会首先在地址0x222的自身中查找该方法。
由于s1
中没有getName()
方法,所以读取__proto__
属性的值,自动往上到地址为0x333的object
对象里去寻找。
3.原型链
访问一个对象的属性时,会先在自身的属性中查找,如果找到了就返回该属性。如果没有找到就会沿着__proto__
这条链向上查找,一旦找到,就返回该属性。如果最终都没有找到,则返回undefined。
像这样一条抽象的链条,就是原型链。因为原型链是由__proto__
属性连接起来的,所以也叫隐式原型链。
下面是一个例子:
function Student(name, age) {
this.name = name
this.age = age
this.getAge = function () {
return 'My age is ' + this.age
}
}
Student.prototype.getName = function () {
return 'My name is ' + this.name
}
let s1 = new Student('jack', 10)
console.log(s1.getAge())
console.log(s1.getName())
console.log(s1.toString())
console.log(s1.getAddress())
-----------------------------
>>>My age is 10
>>>My name is jack
>>>[object Object]
>>>TypeError: s1.getAddress is not a function
例子中有两个方法getAge()
和getName()
,分别定义在Student
自身以及Student
的原型上。然后创建了Student
的实例对象s1
并依次执行getAge()
,getName()
,toString()
和getAddress()
这4个函数,输出结果。
-
getAge()
:在s1
自身的属性里找到然后执行。 -
getName()
:在s1
自身的属性里无法找到,根据__proto__
的值到Student
的原型对象中查找,找到然后执行。 -
toString()
:在s1
以及Student
的原型对象中都无法找到,进一步根据Student
的原型对象的__proto__
的值查找Student
原型对象的原型对象(也就是Object的原型对象)。在里面找到toString()
函数(JavaScript提供)然后执行。 -
getAddress()
:在s1
,Student
的原型对象,Object的原型对象中都无法找到,所以返回undefined。例子中把返回的undefined作为函数来执行,所以系统输出了TypeError
错误信息。
它们在内存中的关系如下图所示:
4.原型链的经典结构图
这是一张流行已久的神图,它是如此经典以至于只要讲原型链,基本都得把它拿出来展示一下。
图上的一部分内容其实已经在前面的例子中已经说明过了,下面是一些补充内容。
- 构造函数
Foo()
除了有prototype
属性,其实也是有__proto__
属性的。这其实很好理解,因为Foo()
也是一个对象。我们在定义函数对象function Foo(){}
的时候其实相当于执行了var Foo = new Function(){}
。只要是对象就有__proto__
属性。 - 一般而言,函数对象的
prototype
属性指向一个空object对象,而它们的__proto__
属性都指向Function
对象的原型。但有两个函数对象是例外。-
Function
函数对象的prototype
属性和__proto__
属性均指向Function
对象的原型。这是因为Function
函数对象是由自身创建的,它是自身的实例对象。 -
Object
函数对象的prototype
属性并不指向一个空object实例对象,它直接指向Object的原型对象。
-
-
Object的原型对象是原型链的尽头。它的
__proto__
属性的值为null。