原型与原型链

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

原型链:由对象的__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的面向对象编程至关重要。

相关推荐
程序研1 天前
JAVA之原型模式
java·原型模式
workflower2 天前
CHAIN OF RESPONSIBILITY(职责链)—对象行为型模式
需求分析·责任链模式·uml·原型模式·软件需求·统一建模语言
power-辰南2 天前
设计模式之原型模式
原型模式
果冻~3 天前
构造函数的原型&原型链
开发语言·javascript·原型模式
计算机小混子3 天前
C++实现设计模式---原型模式 (Prototype)
c++·设计模式·原型模式
勇敢一点♂3 天前
设计模式学习手册(四)(原型模式)
学习·设计模式·原型模式
*猪耳朵*5 天前
Java 原型模式、建造者模式、单例模式
java·建造者模式·原型模式·設計模式
胖虎15 天前
iOS中的设计模式(二)- 原型模式
设计模式·原型模式
angen20186 天前
二十三种设计模式-原型模式
设计模式·原型模式