JavaScript深入之原型与原型链
构造函数创建对象
先使用构造函数创建一个对象:
function Person() {}
let person = new Person()
person.name = 'Janny'
在上述例子中,我们使用构造函数 Person,通过 new 创建了一个实例对象 person。
prototype
原型 prototype 上的方法是给实例对象使用的。
每个函数(只有函数才会有)都有一个 prototype 属性,其指向一个对象,这个对象就是该构造函数创建的实例对象的原型(通过 p.__proto__ 指向,后续会详细讲解)。
function Person() {}
Person.prototype.name = 'Janny'
let person1 = new Person()
let person2 = new Person()
console.log(person1.name) // Janny
console.log(person2.name) // Janny
// 构造函数Person的prototype属性指向了实例对象person1和person2的原型
Person.prototype === person1.__proto__ // true
Person.prototype === person2.__proto__ // true
prototype 指向的通常是一个 Object 空对象,我们可以手动为其添加属性和方法,例如:
Person.prototype.name = 'Danny'
Person.prototype.doing = function() {}
构造函数的 prototype 属性和实例对象的原型的关系为:
graph LR
a[构造函数 Person] --prototype--> b[实例对象person 的原型]
__protp__
每个 JavaScript 对象(除了 null)都具有 __proto__ 属性,这个属性会指向该对象的原型。
function Person() {}
// p是构造函数Person创建出来的一个对象
let person = new Person()
// 因为prototype那一节已经说了构造函数Person.prototype指向实例对象的原型,所以下面这句话就表明了对象的__proto__属性指向该对象的原型
person.__proto__ === Person.prototype // true
关系如下:
graph LR
a[构造函数 Person] --prototype--> b[实例对象person 的原型]
a --通过 new 创建了--> c[实例对象person]
c --__proto__--> b
constructor
实例对象的原型上有一个 constructor 属性,它指向关联的构造函数本身。
function Person() {}
// Person.prototype 实例对象的原型(这个原型也是一个对象)
Person.prototype.constructor === Person // true
let person = new Person()
// person.__proto__ 实例对象的原型(这个原型也是一个对象)
person.__proto__.constructor = Person // true
// Object上有个方法可以获取实例对象的原型
Object.getPrototypeOf(person) === Person.prototype // true
关系如下:
graph LR
a[构造函数 Person] --prototype--> b[实例对象person 的原型]
b --constructor--> a
a --通过 new 创建了--> c[实例对象person]
c --__proto__--> b
显式原型与隐式原型
每个构造函数都有一个 prototype,即显式原型(属性)
每个实例对象都有一个 __proto__,可称为隐式原型(属性)
显式原型属性与隐式原型属性都是一个地址值,二者均指向了同一个对象,就是上面说的那个实例对象的原型。
function Person() {} // 创建prototype原型:Person.prototype = {}
console.log(Person.prototype) // {}
let person = new Person() // 创建__proto__原型:peroson.__proto__ = Person.prototype
console.log(person.__proto__) // {}
console.log(Person.prototype === person.__proto__) // true
原型属性都是 JS 引擎自动添加的
显式原型的创建过程:
- 当构造函数创建的时候会自动添加一个
prototype属性 - 在内存中创建一个空对象(实例对象的原型)
- 让
prototype属性指向这个(原型)空对象
隐式原型的创建过程:
- 当通过构造函数创建实例对象的时候自动添加了一个
__proto__属性 - 将
prototype中存的地址值赋值给__proto__,此时二者指向了同一个对象
graph LR
a[构造函数 Person] --prototype--> b((一个JS对象 \n 该对象为实例的原型))
c[实例对象person]
c --__proto__--> b
原型链的查找
function Person() {
this.doing = function() {
console.log('方法doing')
}
}
Person.prototype.todo = function() {
console.log('方法todo')
}
let person = new Person()
person.ownFun = function() {
console.log('自身方法ownFun')
}
// person对象自身的方法
person.ownFun() // 自身方法ownFun
// 由于person自身没有,通过person.__proto__进行查找,在实例的原型上找到了doing和todo方法
// person.__proto__
person.doing() // 方法doing
person.todo() // 方法doing
// 实例的原型上也没有这个方法,就继续向上找,由于实例的原型也是一个对象,所以继续沿着隐士原型链进行查找,然后找到了toString()方法
// person.__proto__.__proto__ === Object.prototype
person.toString() // '[object Object]'
// 查找了整个原型链也没找到,最后报错
person.done() // 报错:Uncaught TypeError: person.done is not a function
原型链访问:原型链是沿着隐式原型来查找的 原型链作用:查找对象的属性(方法)
访问一个对象属性的时候:
- 先在自身属性中查找,找到了就返回
- 如果没有找到,再沿着
__proto__隐式原型链向上查找,找到了就返回 - 如果最终没有找到,返回
undefined
graph LR
a[实例对象person \n 方法ownFun] --__proto__--> b[实例对象的原型 \n 方法todo 方法doing]
c[Object构造函数] --prototype--> d[JS中所有对象的原型 \n Object相关的实例方法]
b --__proto__--> d
d --__proto__--> e[原型链的尽头 null]
Function 与 Object 的原型链
所有对象都有一个 __proto__ 隐式原型属性,指向了构造函数 Function 的 prototype 显式原型属性,即==实例对象的隐式原型等于其构造函数的显式原型==。
由于 JavaScript 中一切皆对象,所以 Function 既是构造函数又是实例对象,也就是说,Function 也是通过 new Function() 创建而来的。
所有构造函数(构造函数也是对象)的 __proto__ 属性都是一样的,因为所有构造函数都是 Funciton 通过 new 创建而来的,所以都指向了 Function 的显式原型 prototype 属性。
function Person() {}
function Car() {}
Person.__proto__ === Car.__proto__
Person.__proto__ === Function.prototype
Car.__proto__ === Function.prototype
因为构造函数 Object 是 Function 通过 new Function() 创建而来的,所以 Object 相当于实例对象,Function 是该实例对象的构造函数。
所以:
// 实例对象Object的隐式原型等于其构造函数Function的显示原型
Object.__proto__ === Function.prototype
这和原型链的尽头是不一样的
// 原型链的尽头
Object.prototype.__proto__ === null
构造函数的显示原型 prototype 指向的对象默认是空的 Object的实例对象,但是构造函数 Object 的显示原型 prototype 并不满足这一点,通过下述代码对比可以发现:普通构造函数的 prototype 属性指向的对象和 Object 的 prototype 属性指向的对象是不一样的。
function Person() {}
// Person.prototype.__proto__ === Object.prototype
Person.prototype instanceof Object // true
null instanceof Object // false
// Object.prototype.__proto__ === null
Object.prototype instanceof Object // false
所有的函数都是 Function 的实例(包括 Function 自身)
// Function.__proto__ === Function.prototype
Function instanceof Function // true
所有的函数都是 Function 的实例,而 Function 的实例的原型都是构造函数 Object 的实例。
// 构造函数Object也是Function的实例 Object.__proto__ === Function.prototype
// 这个实例的原型Function.prototype.__proto__是构造函数Object的实例,Function.prototype.__proto__ === Object.prototype
// 即:Object.__proto__.__proto__ === Object.prototype
Object instanceof Object // true
下面四条语句均是正确的,通过上面的原型链示意图就可以很轻松地理解了。
Function instanceof Function // true
Function instanceof Object // true
Object instanceof Object // true
Object instanceof Function // true
探索 instanceof
instanceof 是如何判断的?
- 表达式:
obj instanceof Fun 其中obj 是实例对象,Fun 是构造函数 - 如果
Fun构造函数的显式原型对象在obj 实例对象的隐式原型链上,返回true,否则返回false 即Fun只走到Fun.prototype就不走了,而obj要沿着隐式原型链obj.__proto__.__proto__......一直走下去,直到终点。
实例对象的隐式原型属性对象等于构造函数的显式原型属性对象。
Function 是通过 new 自身产生的实例。
function Person(){}
let person = new Person()
// person.__proto__ === Person.prototype
person instanceof Person // true
// person.__proto__.__proto__ === Object.prototype
person instanceof Object // true
// 实例对象的隐式原型属性指向构造函数的显式原型属性,Object是Function的实例
// Object.__proto__ === Function.prototype
Object instanceof Function // true
// 而Function的prototype也是一个实例对象,该实例对象的隐式原型__proto__指向Object.prototype
// Object.__proto__.__proto__ === Object.prototype
Object instanceof Object // true
// Function是new自己产生的
// Function.__proto__ === Function.prototype
Function instanceof Function // true
// 构造函数Function也是一个JS对象
// Function.__proto__.__proto__ === Object.prototype
Function instanceof Object // true
function Foo(){}
Object instanceof Foo // false
例题自测
例题一
function A(){}
A.prototype.n = 1
let b = new A()
A.prototype = {
n:2,
m:3
}
let c = new A()
b.n // 1
b.m // undefined
c.n // 2
c.m // 3
实例一旦创建,其原型就已经确定了,后续再修改原型不会影响到已经创建的实例。
因为当创建 b 之后,b.__proto__ 已经指向了 A.prototype 这个实例对象,即此时这个实例对象有两个指针指向着,分别是 b.__proto__ 和 A.prototype,后续进行了 A.prototype={n:2,m:3} 操作,让 A.prototype 指向了一个新的实例对象。但是这个操作并没有修改原来的实例对象,此时原来的实例对象还有一个指针指着呢,那就是 b.__proto__。所以实例对象 b 的原型还是原来的。
而新创建的实例对象 c ,其在创建前原型已经进行了修改,故创建 c 的时候,c.__proto__ 指向的是修改后的原型对象。
例题二
let Foo = function(){}
Object.prototype.a = function(){
console.log('obj_a()')
}
Function.prototype.b = function(){
console.log('fun_b()')
}
let f = new Foo()
f.a() // obj_a()
f.b() // 报错
Foo.a() // obj_a()
Foo.b() // fun_b()