前端入门13-JavaScript进阶之原型

声明

本系列文章内容所有梳理自如下几个来源:javascript

做为一个前端小白,入门跟着这几个来源学习,感谢做者的分享,在其基础上,经过本身的理解,梳理出的知识点,或许有遗漏,或许有些理解是错误的,若有发现,欢迎指点下。html

PS:梳理的内容以《JavaScript权威指南》这本书中的内容为主,所以接下去跟 JavaScript 语法相关的系列文章基本只介绍 ES5 标准规范的内容、ES6 等这系列梳理完再单独来说讲。前端

正文-原型

JavaScript 中并无 Java 里的类,但它有构造函数,也有继承,只是它是动态的基于原型的继承。因此,原型有点相似于 Java 中父类的概念。java

可是,JavaScript 中的关于实例、继承等这些跟 Java 仍是有很大的区别。git

先来讲说在 Java 里面:github

类是静态的,类是可继承的,是对象的抽象模型的表现,每一个具体的对象都是从类上实例化出来的,一个类中定义了这类对象的属性和行为,一旦定义完了运行时就没法改变了。web

但对于 JavaScript 来讲,它并无类的存在,在 JavaScript 里,除了原始类型外,其他皆是对象。编程

它是动态的基于原型的继承机制,原型本质上也是对象,也就是说对象是继承自对象而来的。数组

而对象这个概念是实例化后的每个具体个体表明,它是运行期动态生成的,再加上 JavaScript 里对象的特性,如可动态添加属性,这就让 JavaScript 里的继承机制很是强大,由于这样一来,它是可动态继承的,原型对象上发生的变化可以同步让继承它的子对象都跟随着变化。浏览器

原型概念

函数和构造函数的区别就在于,全部的函数,当和 new 关键字一块儿使用时,此时称它为构造函数。相似的关系,全部的对象,当它被设置为某个构造函数的 prototype 属性值时,此时称它为原型。

也就是说,任何对象均可以当作其余对象的原型。

在 Java 中,对象通常经过 super 关键字指向它的父类,而在 JavaScript 中,对象可经过 _proto_ 来指向它的原型对象,或者经过构造函数的 prototype 指向对象的原型。

prototype & _proto_

这两个虽然指向的是同一个原型对象,但它们的宿主却不同,须要区分一下,prototype 是构造函数的属性,_proto_ 是经过构造函数建立出来的对象的属性。

_proto_ 属性并不在 ES5 标准规范中,但基本大部分浏览器都为引用类型实现了这么一个属性,用于查看当前对象所继承的原型,它的值等于该对象的构造函数的 prototype 属性值。

prototype 是每一个函数对象的一个属性,其余对象并无这个属性,由于基本全部的对象其实都是经过构造函数建立出来的,因此也只有函数才能来实现继承的机制。这个属性值表示着从这个构造函数建立的对象的原型是什么。

对象一节学习过,建立一个对象的三种方式:

//对象直接量:
var a = {};//实际上是 var a = new Object(); 的语法糖
var a = [];//实际上是 var a = new Array(); 的语法糖

//构造函数
var a = new Array();

//Object.crate()
var a = Object.crate(null);

因此,对象直接量的方式本质上也是经过构造函数的方式建立对象。

这也是为何会在对象一节中说,全部经过直接量方式建立的对象都继承自 Object.prototype 的理由。

而经过 Object.create() 方式建立的对象,其原型就是参数指定的对象,可手动传入 null,表示建立的对象没有原型。

因此,在 JavaScript 中,绝大部分的对象都有原型,即便不手动指定,也会有默认的内置原型对象。之因此说绝大部分,是由于原型链顶部的 Object.prototype 对象的原型是 null,或者经过 Object.create() 建立对象时手动指定 null。

默认的继承结构

若是不手动指定继承关系,默认的几种引用类型的继承关系(原型链)以下:

  • 声明的每一个函数 -> Function.prototype –> Object.prototype -> null
  • 数组对象 -> Array.prototype -> Object.prototype -> null
  • 对象直接量建立的对象 -> Object.prototype -> null
  • 自定义构造函数建立的对象 -> {} -> Object.prototype -> null

全部对象继承的顶层原型是 Object.prototype。

这也是为何函数对象、数组对象、普通对象均可以使用一些内置的方法,由于建立这些对象的时候,默认就会有一些继承关系,跟 Java 中全部的类都继承自 Object 的机制相似。

构造函数和原型的关系

构造函数自己是一个函数对象,它的属性 prototype 指向的是另外一个对象,因此这两个概念自己就是两个不一样的东西。

经过一个构造函数建立一个新的对象,不能说,这个对象继承自构造函数,而是应该说,这对象继承自构造函数的属性 prototype 指向的对象。

因此,能够通俗的理解,构造函数只是做为第三方相似于工具的角色,用来建立一个新对象,而后让这个新对象继承自 prototype 属性指向的对象。

不过构造函数和原型之间是相互引用的关联关系,构造函数有个属性 prototype 指向原型,而原型也有一个属性 constructor 指向构造函数。

因此,全部从这个构造函数建立的新对象,都继承了原型的属性,那么这些新对象也就能够经过继承而来的 constructor 的属性访问构造函数。

若是不手动破坏原型链,那么经过构造函数建立新对象时,三者间的关系:

三者关系

而更多的时候,咱们须要借助原型来让对象继承一些公有行为,有两种作法,一种是经过直接在原型对象上动态添加相关属性,这种方式不破坏原型链,比较推荐。

还有一种,定义一个新的原型对象,而后从新赋值构造函数的 prototype 属性值,将它指向新的原型对象。但这种方式会破坏默认的原型链,同时也会破坏构造函数、原型、实例化对象三者间的默认关联关系。

举个例子:

function A(){}   //定义构造函数A
A.prototype.c = 1;
var b = new A(); //经过构造函数建立对象b

经过构造函数建立一个新对象b,且在构造函数的 prototype 上手动添加新的属性c,会被 b 继承,因为这种方式是没有破坏原型链的,因此三者间关系以下:

构造函数示例

b._proto_ 表示 b 的原型,原型对象的 constructor 属性指向构造函数 A,name 是函数对象的属性,用于输出函数名。

并且对象 b 因为继承自原型 A.prototype,因此也继承它的 constructor 属性,因此也指向构造函数 A。

此时对象 b 的继承关系:b -> {} -> Object.prototype

以上是默认的不破坏原型链下三者的关系,但若是手动破坏了原型链呢:

function A(){}   //定义构造函数A
A.prototype.c = 1;
var a = [];      //建立数组对象a
a.c = 0;
A.prototype = a; //手动修改构造函数A的prototype,让其指向 a
var b = new A(); //经过构造函数建立对象b,b继承自原型a

上面的代码手动修改了 A.prototype 的属性值,让 b 是继承自手动建立的对象 a,因此这里就破坏了默认的原型链,同时,三者间的关系也被破坏了:

修改原型示例

首先,c 属性验证了 b 是继承自对象 a了。

而咱们说过,b._proto_ 指向 b 的原型,在这里,b 的原型就是对象 a 了。而对象 a 是手动建立的,因此它的 constructor 属性是继承自它的原型对象。数组直接量建立的数组对象,本质上是经过 new Array(),因此a的构造函数是 Array(),对象 a 继承自 Array.prototype。

对于对象 a,咱们建立它的方式并无手动去修改它的原型链,因此按默认的三者间的关系,Array.prototype 的 constructor 属性指向构造函数 Array(),这就是为何 b._proto_.constructor.name 的值会是 Array 了。

而,对象 b 继承自对象 a,因此 b.constructor 的值也才会是 Array。

此时,对象 b 的继承关系: b-> a -> Array.prototype -> Object.prototype

因此,在这个例子中,虽然对象 b 是从构造函数 A 建立的,但它的 constructor 其实并不指向 A,这点也能够稍微说明,构造函数的做用其实更相似于做为第三方协调原型和实例对象二者的角色。

一般是不建议经过这种方式来实现继承,由于这样会破坏默认的三者间的联系,除非手动修复,手动对 a 的 constructor 属性赋值为 A,这样能够手动修复三者间默认的关联。

来稍微小结一下

由于原型本质上也是对象,因此它也具备对象的特性,同时它也有本身的一些特性,总结下:

  • 全部的引用类型(数组、对象、函数),都具备对象特性,均可以自由扩展属性,null除外。
  • 全部的引用类型(数组、对象、函数),都有一个 _proto_ 属性,属性值的数据类型是对象,含义是隐式原型,指向这个对象的原型。
  • 全部的函数(不包括数组、对象),都有一个 prototype 属性,属性值的数据类型是对象,含义是显式原型。由于函数均可以当作构造函数来使用,当被用于构造函数建立新对象时,新对象的原型就是指向构造函数的 prototype 值。
  • 全部的内置构造函数(Array、Function、Object…),它的 prototype 属性值都是定义好的内置原型对象,因此从这些内置构造函数建立的对象都默认继承自内置原型,可以使用内置的属性。
  • 全部的自定义函数,它的 prototype 属性值都是 new Object(),因此全部从自定义构造函数建立的对象,默认的原型链为 (空对象){} ---- Object.prototype。
  • 全部的引用类型(数组、对象、函数),_proto_ 属性指向它的构造函数的prototype值,不手动破坏构造函数、原型之间的默认关系时
  • 全部的引用类型(数组、对象、函数),若是不手动破坏原型链,构造函数、原型、实例对象三者之间有默认的关联。

对象的标识

在 Java 中,因为对象都是从对应的类实例化出来的,所以类自己就能够作为对象的标识,用于区分不一样对象是否同属一个类的实例。运算符是 instanceof。

在 JavaScript 中,虽然也有 instanceof 运算符,但因为并无类的概念,虽然有相似的构造函数、原型的概念存在,但因为这些本质上也都是对象,因此很难有某个惟一的标识能够来区分 JavaScript 的对象。

下面从多种思路着手,讲解如何区分对象:

instanceof

在 Java 中,能够经过 instanceof 运算符来判断某个对象是不是从指定类实例化出来的,也能够用于判断一群对象是否属于同一个类的实例。

在 JavaScript 中有些区别,但也有些相似:

var b = {}
function A() {}
A.prototype = b;
var a = new A();
if (a instanceof A) { //符合,由于 a 是从A实例化的,继承自A.prototype即b
    console.log("true"); 
}

function B() {}
B.prototype = b;
var c = new B();
if (c instanceof A) {//符合,虽然c是从B实例化的,但c也一样继承自b,而A.prototype指向b,因此知足
    console.log("true");
}
if (c instanceof Object) {//符合,虽然 c 是继承自 b,但 b 继承自 Object.prototype,因此c的原型链中有 Object.prototype
    console.log("true");
}

在 JavaScript 中,instanceof 运算符的左侧是对象,右侧是构造函数。但他们的判断是,只要左侧对象的原型链中包括右侧构造函数的 prototype 指向的原型,那么条件就知足,即便左侧对象不是从右侧构造函数实例化的对象。

也就是说,在 JavaScript 中,判断某些对象是否属于同一个类的实例,不是根据他们是不是从同一个构造函数实例化的,而是根据他们的构造函数的 prototype 指向是否是相同的。

经过这种方式来区分对象有点局限是:在浏览器中多个窗口里,每一个窗口的上下文都是相互独立的,没法相互比较。

isPrototypeOf

instanceof 是判断的对象和构造函数二者间的关系,但本质上是判断对象与原型的关系,只是恰好经过构造函数的 prototype 属性值作中转。

那么,是否有能够直接判断对象和原型二者的操做呢?

这个就是 isPrototypeOf 的用法了:左边是原型对象,右边是实例对象,用于判断左边的原型是否在右边实例对象的原型链当中:

Object.prototype.isPrototypeOf(b);

但它跟 instanceof 有个本质上的区别,instanceof 是运算符,而 isPrototypeOf 是 Object.prototype 中的方法,因为基本全部对象都继承自这个,因此基本全部对象均可以使用这个方法。

instanceof 和 isPrototypeOf 更多使用的场景是用于判断语句中,若是须要主动对某个对象获取它的一些标识,可使用接下来介绍的几种方式:

typeof

在 JavaScript 中数据类型大致上分两类:原始类型和引用类型。

原始类型对应的值是原始值,引用类型对应的值为对象。

对于原始值而言,使用 typeof 运算符能够获取原始值所属的原始类型。

对于函数对象,也可使用 typeof 运算符来区分:

typeof

因此它的局限也很大,基本只能用于区分原始值的标识,对于对象,自定义对象,它的结果都是 object,没法进行区分。

对象的类属性

在对象一节中,介绍过,对象有一个类属性,其实也就是经过 Object.prototype.toString() 方法能够获取包含原始类型和引用类型名称的字符串,对其进行截取能够获取类属性。

对象类属性

相比于 typeof,它的好处在于能够区别全部的数据类型的本质,包括内置引用对象(数组、函数、正则等),也能够区分 null。

局限在于,须要本身封装个工具方法获取类属性,但这不是难点,问题在于,对于自定义的构造函数,都是返回 Function,而不少对象实际上是经过构造函数建立出来的,因此没法区分不一样的构造函数所建立的对象。

constructor 的 name 属性

constructor 是对象的一个属性,它的值是继承自原型的取值。而原型该属性的取值,在不手动破坏对象的原型链状况下,为建立对象的构造函数。

即,默认状况下,构造函数的 prototype 指向原型,原型的 constructor 指向构造函数,那么从该构造函数建立的对象都继承了原型的这个属性可指向构造函数。

因此,在这些场景下,可用对象的 constructor.name 来获取构造函数的函数名,用函数名做为对象的标识。

function A(){}   //定义构造函数A
var a = new A();
var b = {};

函数名

这种方式有个局限,若是手动修改构造函数的 prototype,破坏了对象的原型链,那么此时,新建立的对象的 constructor 就不是指向建立它的构造函数了,此时,这种方式就没法处理了。

因为 JavaScript 不像 Java 这种静态的类结构语言,因此没有一种完美的方式适用于各自场景中来区分对象的标识,只能是在适用的场景选择适合的方式。

因此,在 JavaScript 有一种编程理念:鸭式辩型

鸭式辩型

我不是很理解中文翻译为何是这个词,应该是某个英文词直译过来的。

它的理念是:像鸭子同样走路、游泳、嘎嘎叫的鸟就称它为鸭子。

通俗点说,编程时,不关心对象所属的标识,不关心对象继承自哪一个原型、由哪一个构造函数建立,只要这个对象含有相同的属性、行为,那么就认为它们归属于同一类。

有个例子就是:类数组对象,它本质并非数组对象,但因为具备数组对象的特征,因此基本上能够把它当作数组来使用。

对应到编程中,不该用判断对象是否拥有相同的标识来区分对象,而是应该判断对象是否含有指望的属性便可。


你们好,我是 dasu,欢迎关注个人公众号(dasuAndroidTv),公众号中有个人联系方式,欢迎有事没事来唠嗑一下,若是你以为本篇内容有帮助到你,能够转载但记得要关注,要标明原文哦,谢谢支持~ dasuAndroidTv2.png

原文出处:https://www.cnblogs.com/dasusu/p/10062259.html

相关文章
相关标签/搜索