前言
作为JS新手,JS原型和原型链的概念一直是莫测高深,好似那雾山上的一朵娇花,很美的样子,但又看不清,摸不着。本文并不能带你深入理解JS原型,却能带着你和它来一次亲密接触。JS原型啊,即使我还不懂你,但我至少切切实实看到你,摸到你了。
一、原型到底是什么?它在哪里?怎么来的?
原型的英文为prototype,意思其实就是,批量制造一种产品时,作为母版的那个东西。
我们用js创建的每一个对象都拥有原型,每一个函数也拥有原型,经代码验证,函数的原型体现在其prototype属性里,对象的原型体现在其__proto__属性里。当然,函数也是对象,在这里,我们把函数对象简称为函数,而除函数外的其它对象简称为对象。
- 函数的原型存在形式prototype
- 对象的原型存在形式__proto__
这两个属性是不可枚举的,在使用node的console打印对象或函数时,它们是不会显示的,同时,Object.keys()也无法获取它们。但在浏览器的console里能够显示。
那么这两个属性是怎么出现在我们创建的对象和函数中的呢?我只能说,是JS自动创建的,因为不用写半句代码,创建了对象和函数,它们自己就存在了。
首先来感受一下吧
创建一个对象
var person1 = {
name: 'person no.1',
age: 1,
sayName: function () {
console.log(this.name)
}
}
person1.sayName()
这个对象创建的同时,js在对象内部创建了__proto__,即原型,在现在的node版本,可以访问到它,它就是JS中原始对象的构造函数Object的原型
console.log( person1.__proto__);//{}
console.log(person1.__proto__ === Object.prototype);//true
看,__proto__自己就存在在那里了,不需要敲打任何代码,打印结果不是undefined,而是空对象{}。
node打印的这个对象的__proto__,在这里也就是Object.prototype,是个空对象{},实际上如果在浏览器的console里查看Object.prototype,会发现很多东西,其中包括我们比较熟悉的toString()等方法,感兴趣的可以看一下,这里不贴图了。实际上Object.prototype,就是js所说万物皆对象的那个根对象。
那么Object.prototype这个东西,它的原型是什么呢,毕竟万物皆有原型啊!
console.log(Object.prototype.__proto__);//null
很有意思吧,首先,它有原型,因为打印结果并不是undefined,其次,它的原型是null。
原来,JS世界里,也是由无生有啊。这么看来,Object.prototype是万物之母,也就是原型链最顶端了,因为它的原型是null。或者说null是万物之母。无生一,一生万物。
我们再来看看函数,创建一个函数,然后打印它的prototype:
function xxxx(){
console.log('我是xxxx函数');
}
console.log(xxxx.prototype);//node打印显示不全,大家可以去浏览器打印查看
console.log(xxxx.prototype.__proto__===Object.prototype);//true
我们会发现,虽然我们没有去写代码,但这个函数的prototype是存在的,它并不是undefined。只能解释它是JS自动生成的了。实际上它有两个属性,constructor指向函数本身,__proto__指向Object.prototype,在浏览器中的console中可以看到。
那么,到目前为止,我们可以得出几个结论:
- Object.prototype应该是可用原型链顶端,它上面是null
- 使用字面量创建的对象,其原型便是万物之母Object.prototype,也就是说,原型链为一层
- 我们创建的函数,其原型的原型是万物之母Object.prototype,也就是说,原型链为两层
那么所有创建的对象的原型都是Object.prototype吗?当然不可能,如果万物皆一母,则何来原型链一说? 只能说,用字面量创建的,以及使用new Object()创建的对象,其原型为Object.prototype。
大家可以自己试一下使用new Object()创建对象,然后查看一下它的__proto__是否与Object.prototype相等。
二、原型到底有什么用?
关于这个问题,我必须很惭愧地说,作为一个新手,并不懂得它具体有多少巧妙用处,只知道两点,一是实现了面向对象语言的继承,二是实现了共享属性和方法,即其它面向对象语言中类似于类的静态成员和方法。关于这两点的演示,结合构造函数和Object.create()这两样东西一起讲会更清楚一点。
三、构造函数
构造函数和原型有关系吗?当然,关系密切,因为构造函数创建的对象改变了原型,由一个构造函数创建的所有对象,它们的原型都是构造函数自己的原型,而不万物之母Object.prototype,换句话说,由构造函数创建的所有对象,它们与Object.prototype还隔了一层,这一层就是构造函数的原型,但也因为有了这一层,可以搞很多事情。
那么先声明一个构造函数,然后用它创建一个对象:
function Person(name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log( this.name);
}
}
var person2=new Person('person no.2', 2)
console.log(person5.__proto__ === Person.prototype);//true
console.log(person5.__proto__.__proto__===Object.prototype);//true
我们之前说过,每个函数都有prototype,构造函数也是函数,因此看到Person.prototype不应该再困惑。 最后两句代码表达的含义很清楚了,Person构造函数制造的对象,其原型是Person的prototype,其原型的原型是万物之母。new Person()做了些什么呢?其实我们可以模拟一下:
var person3=new Object()
//call为函数内部方法,每一个js函数都有,它与apply、bind都可以改变函数内部this的指向
Person.call(person3,'person no.3',3)
person3.constructor=Person
person3.__proto__=Person.prototype
console.log('模拟的是否为Person:',Person.prototype.isPrototypeOf(person6));//true
person3.sayName()
第一步,创建新对象
第二步,将构造函数的作用域指向所创建的新对象(this指向新对象),并执行,相当于给person6添加了属性和方法
第三步,将新对象的构造器指向构造函数,与本文无关
第四步,将新对象的原型指向构造函数的prototype
现在,不去管是否模拟,我们把person3和person2看成一样,都是由Person构造的对象,它们俩都有一个sayName方法,可是它们是同一个方法吗?显然不是,尽管代码一模一样,但它们各自是独立的方法,在内存中开辟了独立的空间。可以比较一下:
console.log(person2.sayName===person3.sayName);//false
那么如何解决这个问题呢?难道如果创建一万个Person,就必须同时创建一万个相同的sayName方法吗?此时加的那层Person.prototype就可以派到用场了。我们用另一个方法sayAge()来演示,在Person的prototype上加入sayAge方法:
Person.prototype.sayAge=function(){
console.log(this.age)
}
person2.sayAge()//2
person3.sayAge()//3
很显然,加在原型上的sayAge方法person2和person3都能使用,并且能打印出它们age属性各自的值,怎么做到的呢?
- 为什么能调用sayAge()--因为js在执行这个方法时,首先在对象自己的属性里查找,如果没有,则将顺着原型链一直向上查找,直到查到为止,如果没有,则报错。在本例中,js在person2自己的方法中没有找到,于是查找person2的__proto__上的方法,也就是Person.prototype上的方法,找到了,然后调用。如果没找到,它还会查找Person.prototype.__proto__的方法,也就是万物之母Object.prototype的方法,有则调用,无则继续往上,但上面是null,于是中止,报错。
- 为什么能打印出各自age的值 -- 因为this,sayAge函数体里的this指向的是函数的作用域,person2调用sayAge时,this指向了person2,person3调用sayAge时,this指向了person3
这里需要注意的是,上面的代码段如果改成下面,则会出错:
Person.prototype={
sayAge:function(){
console.log(this.age)
}
}
person2.sayAge()//报错,sayAge undefined
person3.sayAge()
因为给Person.prototype重新赋值的话,之前创建的person2及person3的原型仍然指向原来的原型,所以找不到sayAge这个方法,除非是使用Person重新创建的对象,才能调用sayAge(),感兴趣的可以自己试一下。
讲到这里,大家会不会问,为什么要多这一层,直接把sayAge()加在Object.prototype不是更方便吗?如果是这样,不要忘了,Object.prototype是万物之母,加在这上面,所有的对象都会有sayAge这个方法了,这可不是我们想看到的。
好了,原型的共享属性和方法也很清楚了,后面演示使用原型实现继承。
四、Object.create()
Object.create()的作用是以其参数为原型,创建一个对象,也就是说,新创建的对象的__proto__指向提供的参数。我们使用之前的person2来创建:
var person4=Object.create(person2)
console.log(person4.__proto__===person2);//true
可以看到,person4的原型为person2。
而这时候,person4自己没有任何属性和方法:
console.log(person8);//{}
Object.keys(person8).map((v,i)=>{
console.log('key',i,v);//不会调用,因为keys数组为空
})
打印结果为空对象,Object.keys()也获取不到任何属性。
但是,它还是能访问到name这个属于person2的属性,因为person2是它的原型,而js在person4本身找不到name属性时,会顺着原型链向上查找,看下面代码:
console.log('name' in person4);//true
console.log(person4.name);//person no.2
in操作符的打印结果为true,并且直接访问name输出的值为person2的name值,这就是继承。
事实上,Person构造函数的原型的sayAge()也能被person4访问到,因为Person.prototype为person2的原型,于是也在person4的原型链之上:
person4.sayAge();//2
好,到这里,原型的继承功能应该也讲清楚了,我们和原型的第一次亲密接触到这里也就结束了,不知道这次接触能否为大家以后更深入的接触带来帮助,谢谢大家阅读!
写在最后
这篇文章讲了一些对于JS原型及原型链的粗浅理解,如果有错误的地方,请在评论区多多指正。不管如何,把自己理解下的原型给讲清楚了,希望能给大家带来一点帮助。 这篇文章是我在掘金的处女作,希望大家喜欢,如果点赞多的话,一定会再接再励。当然,如果没赞的话,应该也还是会写,毕竟,写完之后感觉收获最大的是自己。