js 原型与原型链

Posted 王新焱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了js 原型与原型链相关的知识,希望对你有一定的参考价值。

什么是原型链

原型链是利用原型,让一个引用类型继承另一个 引用类型的属性和方法。说明白原型链,需要从构造函数、原型与实例的关系讲起。


构造函数、原型与实例的关系:

每个构造函数都有一个原型对象,
原型对象都包含一个指向构造函数的指针(显式原型),
而实例都包含一个指向原型对象的内部指针(隐式原型)。
那么,假如让原型对象等于另一个类型的实例,结果会让此事的原型对象包含一个指向另一个原型的指针,相应地,另一个原型中也包含着指向另一个构造函数的指针。

假如另一个原型又是另一个类型的实例,上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是原型链的基本概念。

ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

上图所示:

红色箭头表示 __proto__ 属性指向;
绿色箭头表示 prototype 属性指向;
棕色箭头表示本身具有的 constructor 属性;
蓝色方块表示对象;
浅绿方块表示函数;
javascript 中,这三者之间依附在不同的引用对象类型上。

对象:__proto__ 和 constructor 是对象独有的。
函数:prototype 是函数独有的。但是函数也是对象,所以函数也有 __proto__ 和 constructor。

什么是原型?

每个实例对象有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层构成一个原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

function Person()



Person.prototype.name = '王新焱'

let p1 = new Person()

let p2 = new Person()

console.log(p1.name)  // 王新焱

console.log(p2.name)  // 王新焱

console.log(p2.age) // undefined

Person.prototype 就是实例p1和p2的原型,实例对象会继承原型的属性和方法。所以你会看到即使两个实例p1和p2没有自有属性name,但还是有值。

显式原型和隐式原型

显式原型对象 prototype 由函数所独有,它是从一个函数指向另一个对象。它的含义是函数的原型对象,也就是这个函数(其实所有函数都可以作为构造函数)所创建的实例的原型对象。由此可知:foo.__proto__ === Foo.prototype,它们两个完全一样。

那 prototype 属性作用又是什么呢?它的作用就是包含可以由特定类型的所有实例共享的属性和方法,也就是让该函数的实例化对象们都可以找到公用的属性和方法。

任何函数在创建的时候,其实会默认同时创建该函数的 prototype 对象。

在 JavaScript 中的对象中都有一个 __proto__ 属性,从上图可以看出一个对象指向另一个对象,即指向相对应的对象的原型对象。这个原型对象称为隐式原型对象。

隐式原型对象的作用在于,当访问一个对象的属性或方法时,如果该对象内部不存在这个属性,那么就会从它的 __proto__ 属性所指向的(原型)对象中寻找(原型也是对象,也有它自己的原型),如果原型对象中也找不到,就会继续在该原型对象的原型对象中找,以此类推,直到找到属性或方法为止,或者查找到顶层原型对象 null,就结束查找,返回 undefined。

整个查找过程中,从当前对象出发沿着原型对象(__proto__)构成的链条查找相关属性和方法直到结束,这些相互关联的对象组成的链条就是原型链。

显式原型对象隐式原型对象
属性 prototype属性 __proto__
函数独有对象独有(函数也是对象,因此函数也有该属性)
定义函数时被自动赋值,值默认为 在创建实例对象时被自动添加,并赋值为构造函数的 prototype 值
用于实现基于原型的继承与属性的共享构成原型链,同样用于实现基于原型的继承

原型链的作用?

原型链的作用是实现继承,即每个对象拥有一个原型对象,通过__proto__ 指针指向上一个原型 ,并从中继承方法和属性到新对象中。


原型链继承的优缺点

优点:非常纯粹的继承关系,实例是子类的实例,也是父类的实例。子类可以访问父类新的原型方法和属性。

缺点:子类实例共享属性,造成实例间的属性会相互影响


构造函数

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数),否则也只能称之为普通函数。对于构造函数,通常我们会将函数名首字母大写。

function Person()
    this.name = '王新焱',
    this.age = 30


// 添加自身属性 name、age


Person.prototype.address = '湖北省黄冈市'

let person = new Person()   // 创建了一个Parent构造函数的实例 person 

console.log(person) // Person  name: '王新焱', age: 30 

console.log(person.address) // 湖北省黄冈市

在javascript中,每个构造函数都有一个属性叫prototype(原型)

首先我们来了解三个属性:__proto__、prototype、constructor

①.__proto__、 constructor 属性是对象所独有的;
②.prototype 属性是函数独有的;上面说过js中函数也是对象的一种,那么函数同样也有属性__proto__、 constructor;
③.构造函数Person通过prototype属性引用实例的原型, 返过来原型通过constructor属性引用构造函数,原型和构造函数通过属性互相引用对方。


可通过如下代码检验Person.prototype.constructor是不是指向构造函数

console.log(Person.prototype.constructor === Person) // true

总结

①.每个对象的__proto__都是指向它的构造函数的原型对象prototype的

person1.__proto__ === Person.prototype

②.构造函数是一个函数对象,是通过 Function构造器产生的

Person.__proto__ === Function.prototype

③.原型对象本身是一个普通对象,而普通对象的构造函数都是Object

Person.prototype.__proto__ === Object.prototype

④.所有的构造器都是函数对象,函数对象都是 Function构造产生的

Object.__proto__ === Function.prototype

⑤.Object的原型对象也有__proto__属性指向null,null是原型链的顶端

Object.prototype.__proto__ === null

汇总如下:

1.一切对象都是继承自Object对象,Object 对象直接继承根源对象null

2.一切的函数对象(包括 Object 对象),都是继承自 Function 对象

3.Object 对象直接继承自 Function 对象

4.Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象

Js 原型对象与原型链

原型对象

  每个javascript对象都有一个原型对象,这个对象在不同的解释器下的实现不同。比如在firefox下,

每个对象都有一个隐藏的__proto__属性,这个属性就是“原型对象”的引用。

原型链

  由于原型对象本身也是对象,根据上边的定义,它也有自己的原型,而它自己的原型对象又可以有自

己的原型,这样就组成了一条链,这个就是原型链,JavaScritp引擎在访问对象的属性时,如果在对象本

身中没有找到,则会去原型链中查找,如果找到,直接返回值,如果整个链都遍历且没有找到属性,则返

回undefined.原型链一般实现为一个链表,这样就可以按照一定的顺序来查找。

我们先用一个构造器来实现一个构造函数:

function A(){
    this.mark = "A";
    this.changeMark = function(){
        this.mark += "_changed";
    }
}

A.prototype.mark2 = "A2";
A.prototype.changeMark2 = function(){
    this.mark2 += "_changed";
}

var a = new A();
var a2 = new A();


//下面则说明构造函数实例化后,分配着不同的实例对象,互不相关
console.log(a.mark);  //"A"
console.log(a2.mark); //"A"
    a.changeMark();   //使用实例对象中的方法
console.log(a.mark);  //"A_changed"
console.log(a2.mark); //"A"

//下面则说明了new操作符的一项作用,即将原型中的this指向当前对象,
//在a.changeMark2执行时,changMark2中的方法先找 this.mark2 的值,
//但是实例对象this中没有mark2值,则在原型链向上寻找,得到A原型对象中的mark2值,
//在赋值时,将修改后的值添加在了a实例中。
//总:虽然调用的是prototype方法,但是不会对prototype属性做修改,只会说是在实例中新增属性,但是在使用时,会最使用最近得到的属性(在后面原型链中可以加以理解)
console.log(a.mark2);  //"A2"
console.log(a2.mark2); //"A2"
    a.changeMark2();   //使用原型链中的方法
console.log(a.mark2);  //"A2_changed"
console.log(a2.mark2); //"A2"

为什么a可以使原型中的changeMark2方法?这就和js巧妙的原型链相关,在Firefox中我们可以打印出对象并可查看到对象下面的__proto__。

我们把上面的过程用流程图来表示:

 

 

 

只有构造函数才会有prototype属性,而实例化出来的对象会拥有__proto__,而不会有prototype。

像上图画的那样,两个实例化的对象都通过__proto__属性指向了A.prototype(即构造函数的原型对象)

而原型对象的__proto__指向Object对象,就像a.toString()的toString方法就是存在于Object原型对象(Object.prototype)中。

 

so:当使用对象的方法或属性时,对象会在一步一步通过__proto__向上寻找,找到最近的则是最终的获取到的方法或属性。

  ————这就是js中的原型链。

 就像图上看到的一样,所有对象的原型链最终都指向了Object对象,而Object的原型对象(Object.prototype)是为数不多的不继承自任何属性的对象,即Object.prototype没有__proto__,是原型链的顶峰。

通过上面我们可以了解到,当我们对A.prototype或Object.prototype添加属性或方法时,在a和a2实例中都会查看到该属性或方法,因为这两个实例都通过原型链与A和Object的原型对象相连。

 

 再来看看原型对象和原型链在继承方面的实现:

再构造一个函数A和一个函数B,并让B继承A,如下:

 

function A(mark){
    this.mark = mark;
}
A.prototype.getMark = function(){
    return this.mark;
}

function B(mark){
  this.mark = mark
}

//var temp = new A("A");
//B.prototype = temp;
//上面语句和下语句作用相同

B.prototype = new A("A"); //实例化一个A,其赋值于B.prototype

var b = new B("B");

console.log(b.mark); //B, 结果如上面原型链分析的那样,向上找到最近的属性,则为b实例中的mark:"B"

 

其中的结构示意大概如下图:

 

这时我们可以看到,在B.prototype中是没有constructor的,因为B.prototype只是简单的new A("A")对象赋值的结果。

在js中的constructor有什么作用呢?如:

var arr = new Array();
arr instanceof Array;      //true
arr.constructor === Array; //true


function TEMP(){
}
var temp = new TEMP();
temp instanceof TEMP;      //true
temp.constructor === TEMP; //true

 

用《JavaScript权威指南》中的对于constructor的解释为:对象通常继承的constructor均指代它们的构造函数,而构造函数是类的“公共标识”。即constructor可用来判断对象所属的类。

 

在上面的小例子中,用instanceof也可判断对象的类,但是有自身的缺陷,instanceof的实现方法为:

instanceof不会去检查temp是不是由TEMP()构造函数初始化的,面是判断temp是否继承自TEMP.prototype,这样,范围就宽了很多。

如在上面的大例中,使用

b instaceof B //true 因为在b的原型链中可以找到B.prototype对象

 

b instaceof A //true 在b的原型链中也可以找到A.prototype对象

  可以说instanceof是用来检测继承关系的。而当

console.log(b.constructor) //function A()
//因为在b的原型链中,最近的constructor就是A.prototype中有constructor指向了构造函数A();

  但我们知道的b是属于B类的,那最后所以要做的就是:

B.prototype.constructor = B; //将constructor指向自身的构造函数


var new_b = new B("B");
console.log(new_b.constructor) //function B() 

  

 

 

 

 

 

 

 

以上是关于js 原型与原型链的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript面向对象——成员属性静态属性原型属性与JS原型链

关于JavaScript的原型继承与原型链

图解Javascript原型链

JavaScript继承基础讲解,原型链借用构造函数混合模式原型式继承寄生式继承寄生组合式继承

原型模式 -- JavaScript语言的灵魂

[javascript]js原型链以及原型链继承