JS学习之-原型和原型链

123 阅读11分钟

学习和使用JS的同学们都知道,原型和原型链是JS当中绕不开的知识点,我这里再重新总结介绍一下。

其实想要明白“原型”和“原型链”,只需把下面几个知识点掌握,那么原型和原型链也就明白了:

  1. JS中的对象
  2. 原型(Prototype)
  3. constructor 属性
  4. __proto__属性
  5. 原型链(Prototype Chain)
  6. 原型链的作用

总结

我这里先给结论,如果您不想了解详细内容,直接看这里就行,这里我用简洁的语言总结了相关概念:

  1. JS中一切引用类型皆为对象,函数也是一个对象,所以构造函数也是一个对象。
  2. 原型(Prototype),JS中规定,每个函数都有一个prototype属性,这个prototype属性也是一个对象,对于普通函数来说prototype属性基本没什么用,对于构造函数来说,这个prototype对象就是通过构造函数创建的对象的原型。
  3. constructor 属性,默认情况下,prototype对象(原型对象)都会自动获得一个constructor 属性。constructor 属性默认指向与之关联的构造函数。
  4. __proto__属性,每个对象以及函数都包含这个属性。我们每次通过构造函数创建一个新实例的时候,这个实例内部都会有个内部属性[[Prototype]],[[Prototype]]会被赋值为构造函数的原型对象。这句话很拗口,大致意思就是指[[Prototype]]指针指向当前对象的原型。JS中没有提供访问这个[[Prototype]]特性的标准方式。后来Firefox、Safari和Chrome浏览器就自己创建了一个__proto__属性。通过这个__proto__属性可以访问对象的原型。

我们还要理解这一点:“实例与构造函数原型之间有直接联系,但实例与构造函数之间没有。”

  1. 原型链,对象通过__proto__可以找到它的原型对象,原型对象中还可以通过原型对象中的__proto__找到更上一层的原型,这样一级一级的就形成了一个链条关系,我们称为原型链。
  2. 原型链的作用,继承。

如果上面的总结理解起来还是很不好理解,那么您可以通过下面详细内容做进一步了解:

吸血鬼

我们应该都看过吸血鬼的小说或者电影,吸血鬼都有一个始祖吸血鬼,而且可以不断发展后代吸血鬼,而且后代吸血鬼会继承上几代吸血鬼的能力。

网络图片,侵权请联系删除

克隆

再举一个例子,克隆技术大家应该也都知道,科学家可以通过一个动物的胚胎细胞克隆一个基因完全一样的动物,比如克隆羊多莉。

JS的对象系统

JS的作者Brendan Eich在设计JS面向对象系统时,借鉴了Self和Smalltalk这两门基于原型的语言,而没有选择像Java那种以类为中心的语言。在以类为中心的语言当中类和对象的关系相当于模具和模件的关系,模件总是从模具中铸出来,对象总是从类中创建出来的。但是在原型类语言中是没有类的概念的(虽然JS现在也有类的写法但那也只是一个语法糖)。

在JS当中的对象是基于原型创建的,就像克隆羊一样,克隆出一只和原型对象基因完全相同的对象来。

构造函数

前端的同学应该都知道,JS中的函数既可以作为普通函数被调用也可以作为构造函数被调用。JS规定当使用new运算符来调用函数时,此时函数就是构造函数。

function Person( name ){
  this.name = name;
};
const personOne = new Person( '张三' )

上面的代码中,new Person就是把Person函数作为构造函数创建对象的示例。

原型(Prototype)

好了从这里开始进入正题,我们先了解一下什么是原型。

JS中规定,每个函数都有一个prototype属性,这个prototype属性也是一个对象,或者说这个prototype属性指向一个对象。

function Persion() {}
typeof Persion.prototype // "object", 这个prototype属性也是一个对象

上面代码和图片中展示了函数Persion默认具有prototype属性,并且指向一个对象。

对于普通函数来说prototype属性没什么用,而对于构造函数来说,这个prototype属性指向的是通过构造函数创建的对象的原型。具体看下图:

通过图片中我们可以看到打印Person的prototype,prototype是一个对象,这个对象就是原型对象。我们再看一段代码:

function Persion(name) {
    this.name = name;
}
//构造函数的prototype指向一个对象,称为原型对象
Persion.prototype.age = '18';
// 创建两个实例对象
const persion1 = new Persion('张三');
const persion2 = new Persion('李四');
//实例对象可以可以共享原型对象上面的属性和方法
console.log(persion1.age); //18
console.log(persion2.age); //18

上面代码中,我们给构造函数Persion的prototype设置了一个age属性并赋值18,然后创建了两个对象persion1和persion2。通过打印访问age,实例对象都访问到了该属性。然后再看下面的图片:

图片中我又打印了persion1和persion2对象的__proto__(这个后面会单独介绍,这里只需要知道__proto__指向的是当前对象的原型)和Persion函数的prototype都是同一个对象。所以印证了前面的结论“prototype属性指向的 是 通过构造函数 创建的 对象 的原型”。

我们为了好记些,也可以简单直接说:函数中有个prototype属性,prototype就是原型。(这样说虽然简洁好记,但是自己必须明白,这样说其实是不严谨的,我们不能说prototype就是原型,prototype其实是相对于创建的新对象的原型)

constructor 属性

我们上一个图片中打印prototype的时候里面有个constructor属性。这个constructor是个什么东东呢?

其实在默认情况下,每个对象都会自动获得一个constructor 属性,同样每个prototype对象也会自动获得一个constructor 属性。constructor 属性默认指向与之关联的构造函数。例子如下:

function P() {}
console.log(P.prototype.constructor === P); // true

此例中P.prototype.constructor指向P函数,可以看到他们两是相等的。

proto

在介绍原型的时候我们提到了一个__proto__属性,这个属性是做什么的呢?您往下看:

在JavaScript中,每个对象都有一个[[Prototype]] 属性,它是一个内部属性(在JavaScript中,内部属性是指那些由JavaScript引擎内部使用的属性,它们不直接暴露给开发者,但对语言的运行机制起着重要作用。这些内部属性通常以双方括号([[...]])表示,以区分普通的对象属性),[[Prototype]] 属性用于实现对象之间的继承关系。

再通俗点说的意思就是,我们创建的每个对象,JS引擎都会为每个对象添加一个 [[Prototype]] 属性,这个属性也是一个对象,或者说这个属性指向另一个对象,指向的对象就是原型对象。看下面的代码:

function Persion(name) {
    this.name = name;
}
//构造函数的prototype指向一个对象,称为原型对象
Persion.prototype.age = '18';
// 创建两个实例对象
const persion1 = new Persion('张三');

这里面persion1对象里JS引擎会给它添加一个 [[Prototype]] 属性,这个[[Prototype]] 属性就记录了persion1对象的原型。但是[[Prototype]] 属性是个内部属性,我们没法访问,后来为了访问它,Firefox、Safari和Chrome浏览器就自己创建了一个__proto__属性。通过这个__proto__属性可以访问对象的原型。

看到这里您应该已经明白了,浏览器厂家为了可以正常访问到对象的原型:[[Prototype]],创建了一个__proto__属性来供我们替代[[Prototype]],来访问对象的原型对象。可以通过下面的图片更直观的了解一下:

通过这个图中可以看到确实__proto__的值为构造函数Person的原型。__proto__就像一个纽带一样把“新创建的对象”和“对象的构造器的原型”联系起来。

好了到这里您已经知道什么是__proto__了。下面我们开始了解什么是原型链。

原型链(Prototype Chain)

JS中规定,任何一个对象都可以充当其他对象的原型,再者,原型对象也是一个对象,所以它也有自己的原型,因此这就形成了一个像链条一样的关联关系,我们称为“原型链”,对象到原型,再到原型的原型......。

实际表现就是通过__proto__属性一级一级的关联起来。就像吸血鬼发展后代一样一级一级都关联起来。正如上面我们介绍__proto__时提到了所有对象都有一个__proto__属性,用来访问当前对象的原型对象,__proto__就像一个纽带一样把“新创建的对象”和“对象的构造器的原型”联系起来。

如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。Object.prototype对象的原型是null。null没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null。我们还是使用前面的例子打印一下看看:

通过图中我们可以看到,我们首先通过__proto__数学查看persion1对象的原型,可以看到是来自Persion。接着打印persion1对象的原型对象的原型persion1.proto.proto,可以看到是来自于Object,然后再打印最后一层得到的结果是null。我们通过一张图来描述一下这个关系:

原型链关系

原型链的作用

原型链的作用就是JS的继承。我们还是拿上面的代码举例:

function Persion(name) {
    this.name = name;
}
//构造函数的prototype指向一个对象,称为原型对象
Persion.prototype.age = '18';
// 创建两个实例对象
const persion1 = new Persion('张三');
const persion2 = new Persion('李四');
//实例对象可以可以共享原型对象上面的属性和方法
console.log(persion1.age); //18
console.log(persion2.age); //18
console.log(persion1.name); //张三
console.log(persion2.name); //李四

上面代码中,Persion函数是一个构造函数,函数内部定义了name属性,所有实例对象(上例是persion1和persion2)都会有name这个属性,可以看下图打印出了张三和李四:

persion1和persion2persion1和persion2)都会有name这个属性

也就是说通过构造函数可以提前为实例对象定义属性。虽然很方便,但是有一个缺点:

如果实例之间有相同的方法或者属性,这个方法或者属性(示例中是name属性)会在每个实例上创建一遍,有的实例需要这个属性有的却不需要,这样显然会造成系统资源的浪费。而通过原型对象(prototype)的方式来的话就不一样了。

上面的示例中我们有这一行代码Persion.prototype.age = '18',我们给Persion的原型对象prototype设置了一个age属性并赋值18。后面打印可以看到,persion1和persion2对象都继承了age这个属性。但是与在构造函数中直接定义的name属性不同。persion1和persion2对象的__proto__对象都指向的是同一个对象即Persion.prototype,占用的是一份内存空间。而不像name属性是在persion1和persion2对象中分别创建一个属性,name是占用了两份空间。

通过图中可以看到,我们通过persion1对象改变了原型中的age的值,再通过persion2对象打印age的值也变成了22,这说明确实persion1和persion2的原型是指向了通一个对象。

总结

最后我们再做一个最精简的总结:

每个函数都有一个原型对象prototype,每个对象都有一个__proto__对象指向prototype对象,prototype就是对象的原型。对象中可以通过constructor获取当前对象的构造函数。对象中的__proto__像一个纽带一样把“新创建的对象”和“对象的构造器的原型”联系起来,形成了原型链。