在JavaScript的世界里,原型是一个核心概念,它构成了对象继承的基础。理解原型能帮助我们深入理解JavaScript这一门语言的独特特性。通过具体的代码实例以及原型链的图解,我们将逐步揭开原型机制的神秘面纱,帮助你轻松拿捏这一重要概念。
原型
显式原型
每一个JavaScript函数都有一个特殊的属性,称为prototype。这个属性是该函数的显式原型,它定义了通过该构造函数创建的所有实例对象的公共祖先。prototype属性是一个对象,包含了构造函数创建的所有实例可以共享的属性和方法。
当我们使用构造函数创建对象时,这些对象会自动继承构造函数的原型对象上的属性和方法。举个栗子:
Person.prototype.say = function(){
console.log('hello');
}
Person.prototype.age = 18
function Person(){
this.name = 'jesper'
}
let p = new Person()
p.say()//输出hello
console.log(p.age)//输出18
上面的例子中,Person是一个构造函数,其prototype对象上有一个say方法和age属性,通过new Person()创建的p对象能够访问并调用say方法和age属性。这是因为p对象的原型链中包含了Person.prototype。关于什么是原型链,我们下面会讲到。那原型有什么用呢,我们来看下面的例子
function Car(color,owner){
this.name = 'Su7'
this.long = 4997
this.height = 1440
this.color = color
this.owner = owner
}
let car1 = new Car('green','jesper')
let car2 = new Car('yellow','mike')
我们定义了一个Car的构造函数,里面有很多属性,在这些属性中,只有车主和颜色两种属性需要变动,而其它属性如车长,车高等因为是同一类型的车所以基本不会改变,我们是不是就可以把这些基本固定的属性放到构造函数的prototype对象上呢,这样可以减少代码的耦合度。
Car.prototype.name = 'Su7'
Car.prototype.long = '5000'
Car.prototype.height = '1500'
function Car(color,owner){
// this.name = 'Su7'
// this.long = 5000
// this.height = 1500
this.color = color
this.owner = owner
}
let car1 = new Car('green','jesper')
let car2 = new Car('yellow','mike')
我们再来看一段代码
Person.prototype.hobby = 'music'
function Person(){
this.name = 'jesper'
}
let p = new Person()
p.hobby = 'game'
delete p.hobby
console.log(p.__proto__.hobby);//输出music
console.log(p.hobby);//输出game
从上面的例子中可以看出,实例对象隐式具有构造函数原型上的属性,且是只读属性,所以p.hobby = 'game' 只是自身增加了一个属性,并没有修改原型上的属性。
隐式原型
对象上的__proto__属性是它的隐式原型,它等于该对象的构造函数的prototype属性。虽然__proto__在标准中已经被标记为非标准,但它仍被大多数浏览器广泛支持。
在浏览器的控制台中输出对象后展开,我们可以看到obj也有有一个原型即隐式原型__proto__它等于创建它的构造函数Object的显式原型prototype。
原型链
当试图访问对象的某个属性时,JavaScript引擎首先会在对象自身的属性中查找。如果未找到该属性,便会沿着原型链向上查找,即从对象的隐式原型__proto__开始,一直查找到null为止。这个链状的查找过程称为原型链。
我们来看一段代码,深度理解一下原型链
Grandfather.prototype.lastname = 'MGill'
function Grandfather(){
this.firstname = 'jimmy'
}
function Father(){
this.age = 51
}
function Son(){
this.hobby = 'music'
}
let p = new Son()
console.log(p.lastname);//输出undefined
console.log(p.age);//输出undefined
要怎么让Son构造函数创建的对象p能访问Father和Grandfather中的属性呢,我们只需把用Father构造函数创建的对象赋值给Son的原型以及把用Grandfather构造函数创建的对象赋值给Father的原型,如下所示
Father.prototype = new Grandfather()
Son.prototype = new Father()
这时候有人可能就要问了,为什么不可以直接把Father的原型赋值给Son的原型,把Grandfather的原型赋值给Father的原型呢
Son.prototype = Father.prototype
Father.prototype = Grandfather.prototype
因为这样只是把显式原型的值传了进去,Son构造函数创建的对象p在查找属性时只能查找到Father和Grandfather中显示原型prototype中的属性,且其中的this也不会指向对象p,即对象p也访问不到属性age和firstname以及lastname。
接下来我们就来模拟分析前者的原型链查找情况,如下所示
{
like:'music'
__proto__:Son.prototype == new Father():{
__proto__:Father.prototype == new Grandfather(){
__proto__:Grandfather.prototype ==new Object():{
__proto__:Object.prototype:{
__proto__:null
}
}
}
}
}
我们在浏览器中的控制台输出p也可以看到
相信看到这里,你对原型已经有了一个基本的了解,最后我们来看一张图,只要你把下面这张图弄懂,你就基本掌握原型了。(其中constructor是实例对象用来记录自己是由谁创建的)
接下来让我们一起来分析一下
- 首先目光来到左上角,构造函数
Foo创建的对象f1的隐式原型__proto__指向创建它的构造函数的显式原型Foo.prototype
Foo.prototype显然是构造函数function Foo()创建的,所以constructor指向它,而构造函数function Foo()的原型就是Foo.prototype,所以function Foo()的prototype指向Foo.prototype,这两点以下的function Object()和function Function()也同理,所以下面就不多赘述。
- 继续往下走,
Foo.prototype也是对象,它的__proto__指向创建它的构造函数的显式原型object.prototype,左边的o1和右下的Function.prototype同理。
- 然后是
object.prototype的__proto__,但族谱已经查到顶了,所以指向null。
- 接下来我们回到构造函数
function Foo()和function Object(),因为他俩也是对象,所以他们的__proto__又找到构造函数的祖宗function Function的原型Function.prototype。
- 最后,
function Function的__proto__查族谱也到顶了,但不同的是object.prototype的__proto__指向null,而我们规定function Function的__proto__指向Function.prototype。
看到这会不会有点晕呢,没关系,只要多看几遍,你一定能理解原型。
所有对象都有原型?
上面的分析中,可以看到我们一直在说因为xxx也是个对象,所以xxx也有原型,指向xxx之类的。但是不是所有对象都有原型呢,其实不是的,有一个特例,那就是Object.create(null),Object.create()方法是创建一个对象隐式继承括号中对象,而如果我们填null的话,创建的对象就没有原型
如果正常创建的是下面这样
这点我们需要特别注意
结语
JS在ES6中引入class语法糖,虽然class有更为直观的语法,但它并没有改变JavaScript的原型继承本质。class只是对原型继承的封装,使代码更具可读性和维护性。理解原型机制有助于我们更加深入的掌握JavaScript这一门语言。