和JS原型及原型链的第一次亲密接触

1,151 阅读9分钟

前言

作为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中可以看到。

那么,到目前为止,我们可以得出几个结论:

  1. Object.prototype应该是可用原型链顶端,它上面是null
  2. 使用字面量创建的对象,其原型便是万物之母Object.prototype,也就是说,原型链为一层
  3. 我们创建的函数,其原型的原型是万物之母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原型及原型链的粗浅理解,如果有错误的地方,请在评论区多多指正。不管如何,把自己理解下的原型给讲清楚了,希望能给大家带来一点帮助。 这篇文章是我在掘金的处女作,希望大家喜欢,如果点赞多的话,一定会再接再励。当然,如果没赞的话,应该也还是会写,毕竟,写完之后感觉收获最大的是自己。