前言
在前端面试中,原型和原型链是经常被问到的问题,曾经字节面试官就问到:所有对象最终都会继承自 Object.prototype ? 这需要对JS基础知识运用相对牢固,和对原型和原型链的熟练掌握。这里总结了一些关于原型和原型链的一些知识点,可以帮助大家去深入了解原型
原型
原型,是函数function对象的一个属性,它定义了构造函数制造的对象的公共祖先。通过构造函数产生对象,该对象可以继承原型的属性和方法,原型也是对象
*注:使用Object.create(null)创建对象没有原型,所以上面的面试问题的答案就是不对
原型主要分为显示原型(函数原型)和隐式原型(对象原型),前者是函数天生具有的原型,后者就是对象具有的原型,在浏览器中分别表示为 prototype
和 __proto__
,现在也把隐式原型表示为 [[Prototype]]
图上p为通过Person()构造函数构造的实例对象,我们还在构造函数Person()的原型(显示原型)上挂上变量 lastName,访问 p 时展开可以看到 p 具有隐式原型[[Prototype]],类型为对象,而p的隐式原型展开可以看到构造函数Person()的 prototype(显示原型)上的变量 lastName,值为’lin’,这里我们可以得到一个很重要的结论:实例对象的隐式原型等于构造函数的显示原型
Person.prototype.lastName = 'lin'
function Person(){
this.name = 'asd'
}
let p = new Person()
console.log(p);
console.log(p.lastName);
console.log(p.name);
从上面的打印结果我们可以看到,当我们直接访问 p 时,p 中并没有 lastName 属性,但是能继承到构造函数显示原型的 lastName 属性,所以实例对象是隐式具有构造函数显示原型上的变量
我们需要清楚,函数的显示原型是为了定义构造函数制造的对象的公共祖先,让实例对象可以继承和访问其构造函数的原型(显示原型)中定义的变量、函数等,显示原型的作用就是构造函数不需要在每次构造实例对象时都执行隐式原型中的代码, 降低了代码的耦合度,利用原型的特点和概念,可以提取公有属性
//Car.prototype.Carname = 'BMW'
//Car.prototype.height = 1400
//Car.prototype.lang = 4900
//上面代码同下面一样
Car.prototype = {
CarNmae:'BWM',
height:1400,
lang:4900
}
function Car(owner , color){
this.owner = owner
this.color = color
}
var car1 = new Car('lin','red')
var car2 = new Car('zhang','blue')
上面设计了构造函数Car(),在其每次构造实例对象时,只需要执行设置 owner 和 color ,其隐式原型中的属性在实例对象中都会隐式具有,不需要执行。
原型的特点
1. 所有的对象都具有原型
2. 利用原型的特点和概念,可以提取公有属性
3. 实例对象的隐式原型(`__proto__`)等于构造函数的显示原型(`prototype`)
4. 实例对象无法对显示原型增删改查
这里我们着重解释第四条:实例对象无法对原型增删改查
Person.prototype.lastName = 'yang'
function Person(name){
this.name = name;
}
var p = new Person('kiki')
console.log(p.lastName); //yang
p.lastName = 'zhang'
var p2 = new Person('jiejie')
console.log(p2.lastName); //yang
Person.prototype.lastName = 'zhang'
var p3 = new Person('mumu')
console.log(p3.lastName); //zhang
执行上面的代码,可以知道,通过实例对象p是无法改变构造函数显示原型上的属性,只有通过构造函数原型直接重新赋值,才能更改其属性值
delete p.lastName
console.log(p.lastName)
同理使用实例对象的 delete 方法不能删除构造函数原型上的属性
p.sex = 'boy'
Person.prototype.sex = 'girl'
在实例对象上添加属性并赋值,只是在实例对象上增加一个属性,并不能添加到构造函数原型上,只能直接往构造函数原型上增加属性才能成功添加,继承给之后构造的实例对象使用
Person.prototype.lastName = 'yang'
function Person(name){
this.lastName = name;
}
var p = new Person('zhang')
console.log(p.lastName) //zhang
实例对象查找属性会先找自己是否具有这个属性,没有的话就去自己的隐式原型上查找,也就是构造函数的显示原型上查找,所以我们一般说不能查找构造函数显示原型的属性,但不绝对
*注:实例对象的隐式原型(__proto__
)可以被主动修改,修改后就不等于该实例对象的构造函数的显示原型
Person.prototype.lastName = 'yang'
function Person(name){
this.name = name;
}
var p = new Person('jiejie')
var obj = {
lastName:'lin'
}
p.__proto__ = obj
console.log(p.lastName); //lin
当实例对象的隐式原型被修改后,访问实例对象中的属性时,依然先从自己找,没有就从自己的隐式原型上找,自己的隐式原型被重新赋值为 obj,所以找的最后 lastName 的值为’lin’
原型链
原型链,可以理解在原型上加一个原型,再加一个原型…把原型连成链状,这种链状的访问顺序就是原型链。访问属性时,先在自己对象里面找,我对象没有,就去我对象的隐式原型上面找(也就是自己对象构造函数的显示原型里面找),自己对象构造函数的显示原型里面没有,就去构造函数显示原型对象的隐式原型里面找(也就会我构造函数的父级构造函数的显示原型里面找),以此类推。
Grand.prototype.lastName = 'zhang'
function Grand(){
}
var grand = new Grand()
Father.prototype=grand
function Father(){
}
var father = new Father()
Son.prototype = father
function Son(){
}
var son = new Son()
console.log(son.lastName);
以上,访问 son.lastName 时,先从自己实例对象(son)里面找,没有,就在 son 的构造函数的显示原型(Son.prototype)里面找,没有,就在 son 的构造函数显示原型对象的构造函数显示原型(Father.prototype)里面找,没有,再从 Father.prototype 对象的构造函数显示原型(Grand.prototype)里面找,最后找到 lastName 的值为’zhang’
理解原型和原型链的关系,就应该理解下图:
注:对象的原型(prototype
)中有一个constructor 属性,是用来指明该原型对象是被谁创建的。函数是对象,函数原型对象的隐式原型(Foo.prototype.__proto__
)是其构造函数的显示原型(Object.prototyoe
);函数本身又是 Function 创建的,所以函数原型对象中有 constructor 指向自己函数,然后自己函数对象的隐式原型(function Foo().__proto__
)就是其构造函数的显示原型(Function.prototype
)