原型与原型链

原型链 在结构上很像链表 ,每个对象中都保存着一个地址 ,指向当前对象的原型,可以层层向上查找,起到继承的效果。

原型链:由对象的__proto__属性串联起来直到Object.prototype.__proto__为null的链,就叫原型链。

原型

1、prototype:显式原型

每个函数 都有一个prototype(显式原型)属性,都默认指向一个Object对象 【全名:Object构造函数的实例对象。这个对象内部有两个默认携带的属性(方法):proto、constructor。在被创建时就携带着这两个属性。】

但是有一个对象除外:Object构造函数 (又叫Object函数对象 )的prototype属性指向 → Object原型对象 ,又叫Object.prototype

几乎所有的对象 都可以看做Object的实例对象,但这个被指向的对象Object原型对象 却不能被叫做Object构造函数的实例化对象,原因是:

所有的Object实例对象 必须满足一个要求,其内部的**__proto__属性要有值** ,但是Object原型对象 是整个原型链的终点 ,它的__proto__属性为null

验证这个特殊的对象:

console.log(Object.prototype.proto); // null

(1)查看对象的显式原型
Function原型
javascript 复制代码
function Fun() {};
console.log( Fun.prototype );

这里的__proto__实际上是对象的[[prototype]]属性的非标准表示。

从上述控制台输出可以看到,里面除了constructor 、**proto**之外,没有其他任何属性。

Date原型

Date函数的显式原型:console.log(Date.prototype);

可以看到,Date函数的原型,是一个Object实例对象。但是这个对象身上有很多的属性(方法),这是因为,在Date构造函数中,通过Date.prototype.xxx向它的原型中添加 了很多属性

(2)向构造函数的原型添加属性

例如:向构造函数Fun的原型中添加一个test属性(方法):

javascript 复制代码
let Fun = new Function()
Fun.prototype.test = function(){
    console.log("test");
}
console.log( Fun.prototype );  // 检验一下是否添加成功

发现构造函数 Fun 的原型中已经多了一个 test 方法。

图2-1

从上图可以看到,红色箭头 就是引用,声明函数和变量会在栈中存放引用地址,实际内容存放在堆中。蓝色箭头 表明fun的__proto__指向的是Fun的原型对象紫色箭头 表示Fun的原型对象中constructor指向了Fun的构造函数 ,而绿色箭头 表明Fun构造函数的prototype指针指向的就是Fun的原型对象

再结合代码,Fun被声明创建了红色箭头 的引用。fun是Fun的实例对象,也创建了红色箭头 的引用,同时new关键字实例化对象会把fun与Fun的原型进行关联,形成了蓝色箭头。然后在Fun.prototype上面新增了test方法。因此fun可以通过__proto__属性找到构造函数中的test方法,从而进行调用。

Fun函数是Object构造函数的实例化对象,引用类型都会继承Object对象的原型 (也就是空Object对象)。结合原型链知识,原型对象 中会存在一个指针constructor 指向对象的构造函数 ,也就是图中的紫色箭头 ,而构造函数中会存在一个prototype指针指向对象的原型对象,也就是图中的绿色箭头

(3)构造函数的prototype属性指向一个空Object对象(Fun.prototype),在这个对象的内部有一个属性:constructor,它指向生成这个空Object对象的构造函数。

根据以上判断,可以理解下面代码:

javascript 复制代码
console.log( Date.prototype.constructor === Date );  // true
console.log( Fun.prototype.constructor === Fun );   // true
(4)所有函数的显式原型属性都指向一个空Object实例对象

根据图2-1我们截取一小部分,从局部来看

虽然每个函数原型 都是一个空Object对象 ,但这些对象并不是同一个对象。每个构造函数 都有自己的空Object对象 。这个空Object对象还有一个别名,对于Fun来说,它的原型对象就可以直接叫做Fun.prototype(Fun的显式原型)。

从图上可以看出prototype指针是指向Fun.prototype这个空Object对象的。

总结:构造函数 中存在prototype指针 指向原型对象 (空Object对象),原型对象 (空Object对象)中存在constructor 指针指向构造函数

我们可以记这个图:

2、proto:隐式原型

每个对象 都有一个**proto** 属性。对象的隐式原型 属性的值对应"生成它的构造函数"的显式原型 的值,都指向构造函数的原型:空的Object对象

从上面解释,可以得出,在原型对象 中还有一个**proto** 属性值,这个值指向的是,当前原型对象 被生成的构造函数prototype ,都指向构造函数的原型(空的Object对象)。

这么说可能不好理解,再根据上面的例子,Fun.prototypeFun的空Object对象Fun.prototype 本身就是对象 ,既然是对象就会有**proto** 属性,而这个__proto__指向的是生成Fun.prototype的构造函数的prototype (原型对象)。相当于这里的Fun构造函数的__proto__属性,指向Fun.prototype

同理,(生成Fun.prototype对象)的构造函数的__proto__属性,指向(生成Fun.prototype对象).prototype 。

proto → (生成当前对象的原型对象构造函数prototype ),再结合上面的图,也就是说__proto__ 指向的就是当前对象的原型对象

注意:对比显示原型对象 ,区别在于,prototype显示原型 针对的是函数 ,而**__proto__隐式原型** 针对的是对象。因为函数本质上就是Object的扩展。(注意联系)

(1)获取对象的隐式原型
javascript 复制代码
let Fun = new Function();
let fun = new Fun();
console.log( fun.__proto__ );
(2)prototype、__proto__本质上都是一个指针,指向了同一片区域 → 构造函数的显式原型(prototype)指向的空Object对象。

按照这种说法,fun对象中会生成一个属性__proto__指向构造函数Fun的原型空Object对象Fun.prototype(这个空Object对象的名字可以直接叫做Fun.prototype)。验证一下这种说法:

javascript 复制代码
let Fun = new Function();
let fun = new Fun();
console.log( Fun.prototype === fun.__proto__ );  //true

思考:构造函数Fun的原型Object是一个对象。每个实例对象都有一个__proto__属性,那么这个空Object对象的原型(Object.prototype)拥有__proto__属性吗?

答案:没错,就是值比较特殊,为null。原型链 就是通过隐式原型**proto** 向上查找的,Object.prototype 是整个原型链的尽头 ,所以这里自然就是null了,表示原型链的终止

javascript 复制代码
console.log( Object.prototype.__proto__ );  // null

接着推测,既然所有的函数 都有prototype属性,那Object的构造函数有没有这个属性呢?

答案:有。打印Fun.prototype,其实就是打印构造函数Fun内部生成的空Object对象(原型对象)空Object对象 中的__proto__属性,就是由Object构造函数 中的prototype复制而来。
这个空的Object对象中__proto__属性,指向的又是谁呢?

答案:空的Object对象中的__proto__属性,指向的就是Object原型对象 。所以Object构造函数的prototype属性 ,指向的也是这个Object原型对象

javascript 复制代码
let Fun = new Function();
console.log( Fun.prototype.__proto__ );
// Fun.prototype对应的就是这个空Object对象
// 等价于 console.log(Object.prototype) --> 打印 Object 原型对象


从上述的打印可以知道,为什么我们随便创建一个函数 ,这个函数和它的实例对象都可以使用toString()方法 。因为本身Object原型 中存在该方法,每个引用对象都会在构造函数中生成这个空Object对象(原型对象)。

每个对象都有__proto__属性,Object原型对象就没有该属性吗?

答案:有,但这个属性值为null。这就是整个原型链的尽头。

总结:每个构造函数 中都有一个prototype 属性,指向它的原型对象 ,这个原型对象 内部默认是空的。每个实例对象 都有一个**proto** 属性,指向生成它的构造函数 的原型(空Object对象)。每个构造函数 的原型(空Object对象)都有一个constructor 属性来执行构造函数

prototype和__proto__创建时间

构造函数Fun 而言,函数 Fun的prototype 属性在定义这个构造函数的时候就创建出来了;

对于实例对象 fun来说,__proto__则是在通过new创建对象的时候才添加的。在实例化一个对象的时候,还做了一件事:this.proto = Fun.prototype ,将构造函数prototype 属性赋值给实例对象的**proto**属性。(看到上面的三角图了吗?这样是不更好理解了。就是图中__proto__指向)

往下推理,在创建Fun构造函数 的时候,必然还做了一件事:this.prototype = { } 【也可以写成:this.prototype = new Object()】,为Fun构造函数创建一个空Object对象。

原型链

看看原型链的经典图解,看起来很复杂,但要求每个前端程序员都能自己画出来。

由于上述代码不好看,且不好理解,我们用原型三角图的形式表示,如下:

对画原型图做一个总结:

  1. 所有的" 构造函数 "都有一个 prototype 属性指向其原型:" 空 Object 对象 "。
  2. 所有的" 实例对象 "都有一个 proto 属性,指向其构造函数的原型" 空 Object 对象 "。
  3. 所有" 空Object对象 "都有一个 constructor 属性,指向创建它的" 构造函数 "。
  4. 每个构造函数的原型 " 空Object对象 "也是个对象 ,它们均是由"Object构造函数"实例化 而来,因此它们的 proto 均指向 Object 构造函数的原型:" Object.prototype "。
  5. 所有的" 构造函数 "(包括Function自身)均是由" 构造函数 Function "实例化而来,因此每个构造函数都有一个 proto 属性 ,指向 Function 的原型 " Function.prototype "。(如下图2-2-1)
  6. " Object.prototype "作为整个原型链的终点,其 proto 为 null。

图2-2-1

JS引擎在加载页面的时候,首先 会把一些内置的函数加载出来,这其中就包括 Object 构造函数 【除 Object 构造函数的原型之外,所有其它构造函数的原型空 Object 对象 都是它的实例。】、Object 原型对象Object 构造函数 是一个全局对象 ,在 内存中有一个变量名 Object,它内部存储的就是这个全局 Object 构造函数的地址。

1. 原型链查找:

在调用一个方法时,如果对象自身找不到,则通过__proto__属性,沿着原型链 不断向上查找,最终会来到Object原型对象 ,看Object原型对象 上是否有此方法,有的话就会调用这个方法,像toString()、hasOwnProperty()、valueOf()等方法就是一层一层向上查找,最终Object原型对象上找到了该方法。假如在Object原型对象上找不到该方法,就会打印undefined。

原型链查找属性例子:

fun.test1():test1在Fun构造函数中被添加,因此实例化对象fun就拥有了该方法,调用test1的时候,在自己身上就能找到。

fun.test2():test2是在Fun的原型对象上添加的,fun对象在自身寻找test2方法没找到,此时会通过fun.__proto__找到该对象的隐式原型对象,也就是Fun.prototype(构造函数Fun的原型空Object对象),最终在Fun.prototype身上找到了test2方法。

fun.toString():fun在自身寻找toString方法没找到,接着通过fun.__proto__向上找,在Fun.prototype身上也没找到,继续沿着__proto__上找,在Object.prototype身上找到了toString方法并调用。

fun.test3():fun在自身寻找test3方法没找到,接着通过fun.__proto__向上找,在Fun.prototype身上也没找到,继续沿着__proto__上找,在Object.prototype身上也没找到,打印undefined。

javascript 复制代码
console.log(fun.test3);  // undefined
fun.test3();  // fun.test3 is not a function

总结:方法 一般会被定义在原型 中,属性 一般通过构造函数 定义在对象身上,因为属性 都带有个性特征,但方法 可以普遍地供每个实例对象 使用,每个对象用的时候只需要沿着原型链向上查找即可,就不需要额外占用内存。

instanceof经典问题辨析

instanceof 用来判断一个对象是否是另一个对象的实例,例如可以使用A instanceof B来判断A是否为B的实例。即B的显式原型是否位于A的原型链上。

javascript 复制代码
console.log(Fun.prototype instanceof Object) //true

举例:

javascript 复制代码
console.log(Object instanceof Function)  
console.log(Object instanceof Object)  
console.log(Function instanceof Function)  
console.log(Function instanceof Object) 

(1)判断A是否为B的实例

(2)判断B.prototype是否位于A的原型链上。

结论:

  1. 所有函数都是由Function构造函数实例化而来
  2. Function也是由Function构造函数构造出来的实例
  3. 基本上所有的对象都是Object构造函数的实例(Object.prototype除外)

Function instanceof Object

  • prototype:Object -> Object.prototype
  • protoFunction -> Function.prototype -> Object.prototype

Object instance of Object

  • prototype:Object -> Object.prototype
  • protoObject -> Function.prototype -> Object.prototype

Function instance of Function

  • prototype:Function -> Function.prototype
  • protoFunction -> Function.prototype

Object instanceof Function

  • prototype:Function -> Function.prototype
  • protoObject -> Function.prototype

因此:

javascript 复制代码
console.log(Object instanceof Function)  //true
console.log(Object instanceof Object)  //true
console.log(Function instanceof Function)  //true
console.log(Function instanceof Object)  //true

假如在这个例子上加一个函数对象Foo呢,Object instanceof Foo,如何判断?

Object instanceof Foo

  • prototype:Foo -> Foo.prototype
  • proto:Object -> Function.prototype -> Object.prototype

结果是:false,可以看到Foo并没有出现在__proto__原型链上。

总结:每个函数的原型又叫构造函数的原型对象 。既然是一个对象 ,就是Object的实例。如果一个对象是Object的实例对象,则__proto__属性必须有值。但是Object原型对象Object.prototype并不是Object的实例对象,他是原型链的终点,所以__proto__属性值为null。因此Object.prototype instanceof Object 为false

javascript 复制代码
console.log(Object.prototype instanceof Object)  //false

2、案例分析

javascript 复制代码
// 练习一:
function A() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
    n: 2,
    m: 3
}
 
var c = new A();
console.log(b.n, b.m, c.n, c.m);

练习一:

b是A的实例对象,因此可以使用A原型定义的变量n=1,所以b.n输出为1。

注意在b实例化后,才在A的原型上新增了m和n属性,因此在b获取的时候并没有获取到m的值,所以为undefined。

c是A的实例化对象可以正常访问A中的属性,输出2,3

javascript 复制代码
// 练习二:
function F() {};
Object.prototype.a = function(){
    console.log('a()');
}
Function.prototype.b = function(){
    console.log('b()');
}
 
var f = new F();
f.a();
f.b();
F.a();
F.b();

练习二:

f是F的实例对象,调用a方法在自身找不到,则去__proto__上找到Object.prototype里面有a方法,因此正常打印。

f在自身找不到b方法,去Object.prototype上也找不到,因此报错,输出f.b is not a function

F是Function的实例对象,在自身找不到a方法,则去__proto__上找Function.prototype发现也没找到,继续向上找到Object.prototype,找到里面的a方法,正常输出a()

F在自身找不到b方法,去Function.prototype中找到了b方法,正常输出b()

原理

原型链 本质就是一个链表 ,是js实现继承的一种机制。

任何对象(函数)内部都有一个原型对象(prototype),new 实例对象创建成功后,会加上一个__proto__属性,称为它的隐式原型,当我们访问对象上的属性或方法的时候,如果在当前对象自身找不到,js就会沿着__proto__一层层向上找,直到找到该属性或者方法,或者到达原型链的终点,即Object.prototype.__proto__为null,为止。这就是原型链

好处优点

所有对象都可以共享原型链方法 ,从而实现属性和方法的继承,达到节省内存的效果。

原型链应用场景

  1. jQuery,**** 就放在jQuery的原型链上,我们用 拿属性
  2. Vue 的axios也是放在vue的原型链上,我们使用 $ axios在文件的任何位置都可以访问这个方法
  3. 数组方法Array.prototype

原型/原型链总结:

  • 原型链是JavaScript实现继承的一种机制
  • 任何函数都有一个prototype,称为这个函数的原型
  • 这个函数也可以把它当成一个构造函数,通过new出一个实例
  • 实例创建成功后自动加上一个__proto__,称为它的隐式原型

原型链 是JavaScript中一个复杂但强大的特性,它允许对象之间共享属性和方法 ,并通过原型链实现类似传统面向对象编程语言继承机制 ,掌握原型链的概念和用法对于深入理解JavaScript的面向对象编程至关重要。

相关推荐
想吃火锅10058 天前
【前端手撕】instanceof
前端·javascript·原型模式
UXbot8 天前
帮助企业低门槛开展AI应用开发的平台推荐
前端·低代码·ui·交互·产品经理·原型模式·web app
UXbot8 天前
如何选择适合公司项目的UI设计工具?企业选型指南
前端·低代码·ui·团队开发·原型模式·设计规范·web app
UXbot9 天前
原型设计工具如何帮助新人快速进入产品行业?
前端·低代码·ui·交互·团队开发·原型模式·web app
sunny.day13 天前
js原型与原型链
开发语言·javascript·原型模式·js原型链
UXbot14 天前
AI网页开发工具能替代工具吗?5大平台对比
前端·人工智能·低代码·ui·原型模式·web app
weixin_3077791314 天前
从“大海捞针”到“主动推理”:AI如何重塑云原生故障诊断的根因链
开发语言·人工智能·算法·自动化·原型模式
swordbob14 天前
prototype 注入到 singleton 里,prototype是否还是线程安全的
安全·spring·单例模式·原型模式
isNotNullX15 天前
企业数据中台建设,ETL工具选错了会踩哪些坑?
数据仓库·etl·原型模式
半个烧饼不加肉15 天前
JS 底层探究-- 普通函数和构造函数
开发语言·javascript·原型模式