博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
javascript原型链
阅读量:7044 次
发布时间:2019-06-28

本文共 9633 字,大约阅读时间需要 32 分钟。

javascript 原型链

说到 javascript 面向对象技术,就免不了说到 javascript的原型继承机制,众所周知的一个事实是 javascript 中的所有对象都继承于 Object,像一些通用的方法 toStringvalueOf等都是从 Object 的原型对象(Object.prototype)中继承来的,当调用一个对象的 toString方法时,如果对象自身没有这个方法,则会从该对象的原型上查找,如果还是没有查找到这个方法,则继续在原型的原型上查找,这样就构成了一个原型链,由于在 Object.prototype 中定义了默认的 toString 方法,如果在原型链中除Object.prototype对象之外都没有查找到这个方法,则会一直沿着原型链查找到Object.prototype 对象并调用其中的 toString 方法。反过来说,如果在原型链中的某个对象中定义了一些方法,则所有继承该原型对象的对象都继承了这些方法,这给 javascript 提供了极大的灵活性,在 ECMAScript2015 标准之前,我们也是通过原型链去实现自定义继承,其实 ECMAScript2015 也只是提供了一些语法糖,其内部实现机制还是基于原型链继承去实现的。

var obj = {};//[object, Object],对象自身没有定义这个方法,javascript沿着原型链向上查找,最后调用了Object.prototype的toString()方法obj.toString();obj.toString = ()=> {  return '{}';}//{},对象自身定义了这个方法,javascript找到这个方法后,不再沿着原型链向上查找,直接调用obj.toString();//在原型对象上定义了一个方法(在实际业务开发中我们一般不会这么做)Object.prototype.sayHello = function() {  return 'Hello';}//Helloobj.sayHello();复制代码

原型和原型对象(__proto__与prototype)

在说 javascript 原型链的时候,有一个概念一定要理清楚,上面的叙述中特意用了两个名词,一个是原型,一个是原型对象,这两个完全是不同的概念。

这里有一个容易让人误解的地方,因为 prototype 直接翻译成中文就是原型,笔者在之前很长的一段时间里,都错以为 obj 的原型就是 obj.prototype,我相信很多人和我一样会有这样的误解,其实不然,如果没有给这个属性赋值的话,obj.prototype应该是undefinedobj 的原型其实是obj内部维护的一个指针,在 ECMAScript5 之前的标准中,并没有定义获取对象原型的方法,但一直以来浏览器厂商都有一个非标准的实现,obj__proto__ 属性就是其内部维护的指针,指向原型对象,在ECMAScript5标准中规范了获取对象原型的方法,可以使用 Object.getPrototypeOf 方法获取一个对象的原型。本文为了方便,就使用 __proto__ 这个属性指代对象的原型了。

再来说说原型对象,原型对象是一个实实在在存在的对象,可以包含属性和方法,如果是通过 new 关键字和构造方法实例化对象的话,原型对象一般都是和构造函数(普通函数也有,但是一般没有意义)关联的一个对象,构造函数中的prototype属性指向这个原型对象,所以Object.prototype其实也是一个指针,这个指针指向原型对象,当通过 new 关键字实例化一个对象时,这个实例化对象的原型就指向构造函数关联的原型对象,上面这个例子,使用对象直接量创建的对象和使用 new 关键字调用Object构造函数效果是一样的,所以obj的原型 __proto__Object.prototype 指向同一个原型对象,这个对象定义了一些通用的方法,如 toString 等。与构造函数关联的原型对象中一般还会有一个 constructor 属性,这个属性指向这个构造函数,由于这个属性在整个原型链上,所以在 obj 对象上应该有一个 constructor 属性,它指向 Object

这个示意图展示了 javascript 中最简单的原型链,图中的红线标明了 obj 的原型链,我们通过这个示意图再来分析一下原型链机制,当我们读取 obj.x时,javascript先从该对象中查找这个属性,obj中定义了这个属性,直接读取并返回,当我们调用 toString 方法时,javascript 先从本对象中查找 toString 方法,由于 obj 并未定义这个方法,所以 javascript 继续从 obj 的原型中查找,这个例子中,obj 的原型指向 Object.prototype,其中定义了 toString 方法,于是,javascript 就调用了这个方法,返回默认实现 [object, Object]

从上面这个示意图我们还能看到,Object.prototype中有个 constructor 属性指向 Object, Object.prototype 的原型指向 null,所以我们可以说 Object.prototype 是所有原型链的顶端。

下面这段代码验证了上面这个示意图。

var obj = {};//trueobj.__proto__ === Object.prototype;//trueObject.prototype.__proto__ === null;//trueObject.prototype.constructor === Object;复制代码

自定义构造函数的原型链

我们升级一下难度,讨论一下自定义构造函数的原型链,如果对上面的概念理解的话,理解自定义构造函数的原型链应该不复杂,我们定义了一个 Person 类,并实例化了这个 Person 类。

function Person(firstName, lastName) {  this.firstName = firstName;  this.lastName = lastName;}var person = new Person('Chen', 'Fuqiang');复制代码

我们可以看到,自定义构造函数实例化的对象的原型链相比于对象直接量创建的对象多了一层,这一层正是与 Person 构造函数关联的原型对象,该原型对象的原型指向 Object.prototype,这样就构成了继承关系,Person 类继承 Object 类, 默认所有的构造函数关联的原型对象的原型都是 Object.prototype,所以我们可以说 javascript 所有的对象都继承于 Object,或者说是 Object 的子类。 回到 Person 构造函数,与 Object 相似,Person 构造函数的 prototype 属性指向与 Person 构造函数的关联原型对象,原型对象中的 constructor 属性指向 Person 构造函数,person 实例的原型 __proto__ 指向Person.prototype, person --> Person.prototype --> Object.prototype --> null 构成了 person 的原型链,示意图中已经用红线标示出来了。下面这段代码验证了上面这个示意图。

function Person(firstName, lastName) {  this.firstName = firstName;  this.lastName = lastName;}var person = new Person('Chen', 'Fuqiang');//trueperson.__proto__ === Person.prototype;//truePerson.prototype.constructor === Person;//truePerson.prototype.__proto__ === Object.prototype;复制代码

基于这个特性,我们经常在定义一些实例方法时,将实例方法定义在与构造函数关联的原型对象中,而不是在构造函数中定义在每个实例中,这样可以避免重复实例方法,比如下面这个示例我们在 Person 关联的原型对象中定义了 sayHello 方法。

function Person(firstName, lastName) {  this.firstName = firstName;  this.lastName = lastName;}Person.prototype.sayHello = function() {  return `${
this.firstName} ${
this.lastName}`}var person = new Person('Chen', 'Fuqiang');复制代码

这里还有一个比较好玩的地方,由于 javascript 一切皆对象,所以 Person 构造函数也是一个对象,也拥有自己的原型链,那构造函数这个对象是谁的子类呢,很明显,是 Function 的子类,我们把上面这个示意图拓展下。

可以看到由于Person 构造函数继承于 Function, 所以 Person 的原型 __proto__ 指向 Function.prototype,在 Function.prototype 中也有一个 constructor 属性指向 Function。由于 Function.prototype 又是 Object 的实例,所以 Function.prototype 的原型 __proto__ 指向 Object.prototype,我们用代码验证下。

function Person(firstName, lastName) {  this.firstName = firstName;  this.lastName = lastName;}//truePerson.__proto__ === Function.prototype;//trueFunction.prototype.constructor === Function;//trueFunction.prototype.__proto__ === Object.prototype;复制代码

Object.create() 的原型链

ECMAScript5 标准中定义了一个方法 Object.create() 用于创建对象,该方法接受两个参数,第一个参数是返回对象的原型,第二个参数是可选的,用于定义返回对象自身属性的属性描述对象,与Object.defineProperties() 的方法的第二个参数一致,由于这边只考虑原型链,我们暂且忽略这个参数,只关注第一个参数,由于第一个参数是返回对象的原型,所以返回对象的 __proto__ 属性应该指向这个对象,我们来验证下。

var a = {
x:1};var b = Object.create(a);//trueb.__proto__ === a;复制代码

由于原型对象 a 中没有定义 constructor 属性,当我们试图获取 b.constructor 属性时,由于在 a 中找不到这个属性,javascript 沿着原型链一直向上找,最终在 Object.prototype 中找到了这个属性,这个属性指向 Object,我们继续验证下。

var a = {
x:1};var b = Object.create(a);//trueb.constructor === Object;复制代码

最后上个示意图

内置对象Function与Object的原型链

上面所说的已经讨论了大多数情况,包括对象直接量,自定义构造函数,以及 Object.create 方法构造出的对象,其实还有一种对象我们没有讨论,那就是 javascript 内置对象,我们都知道,javascript 帮我们内置了几个常用对象,包括几个原始类型的包装类,数组类 Array 以及工具类 Math 等,这几个类的在原型链上的外在表现其实与自定义构造函数的外在表现是一样的,我们可以认为内置对象是javascript帮我们内置了几个构造函数,在 javascript 的所有内置对象中,也包含着几个例外,他们的原型链很特殊,特殊到有必要单独拿出来讲下,他们是所有函数的父类 Function 以及所有对象的父类 Object

我们来一个一个分析,首先是 Function 类,Function 类本身也是一个构造函数,Function 是自身的一个实例,是自身的一个实例,所以 Function 的原型 __proto__ 应该指向 Function.prototype, 在Function 中,它的 __proto__prototype 同时指向同一个对象,这个是 Function 本身最特殊的地方,然后还有一些通用规则,Function.prototype.constructor 应该指向 Function, Function.prototype.__proto__应该指向 Object.prototype

再来分析一下 Object, Object 本身也是一个构造函数,所以Object.__proto__ 应该指向 Function.prototype

写个代码验证下

//trueFunction.__proto__ === Function.prototype;//trueFunction.prototype.constructor === Function;//trueObject.__proto__ === Function.prototype;//trueFunction.prototype.__proto__ === Object.prototype;复制代码

再来个示意图理解一下

instanceof 操作符

instanceof 操作符常常用于判断某个对象是否是一个对象的实例,举个实际的栗子。

function Person () {}var person = new Person();//trueperson instanceof Persion;//trueperson instanceof Object;复制代码

因为 personPerson 的一个实例,所以上面这个表达式返回 true,那这个又和原型链有什么关系呢,其实他们的关系大了去了,instanceof 左边的操作数是要判断的对象,右边是一个构造函数对象,javascript 在处理 instanceof 操作符时其实是在判断这个对象的原型链上的某个对象是否和该构造函数关联的原型对象指向同一个对象,这其实等价于下面这段代码。

function myInstanceof(o, O) {  var proto = o.__proto__;  var prototype = O.prototype;  while(proto) {    if (proto === prototype) {      return true;    } else {      proto = proto.__proto__;    }  }  //如果代码走到这里,说明原型链上没有一个对象等于O.prototype,返回false  return false;}复制代码

上面这段代码第一个参数是要检验的对象,第二个参数是一个构造函数,该方法会沿着原型链一直向上查找,直到查找到原型链顶端,如果还没查找到,则返回 false,我们来验证下这个函数与instanceof 操作符的等价性。

function Person () {}var person = new Person();//trueperson instanceof Person;//trueperson instanceof Object;//truemyInstanceof(person, Person);//truemyInstanceof(person, Object);/** * 这里改变了 person 对象的原型指向 * 设置 prototype 是 person 对象的原型,并且是原型链的顶端 * 构造了 O 对象,使其 prototype 属性指向 prototype */var prototype = {};prototype.__proto__ = null;var O = function () {};O.prototype = prototype;person.__proto__ = prototype;//falseperson instanceof Person;//falseperson instanceof Object;//trueperson instanceof O;//falsemyInstanceof(person, Person);//falsemyInstanceof(person, Object);//truemyInstanceof(person, O);复制代码

与原型相关的几个有用的方法

最后在介绍几个和原型相关的一些方法

Object.getPrototypeOf(obj)

Object.getPrototypeOf(obj) 定义在 ECMAScript5 标准中,这个方法用于获取一个对象的原型,在 ECMAScript5 之前,并没有获取对象原型的标准方法,但是一直在浏览器厂商(IE除外)中都有一个非标准的实现,那就是对象的 __proto__ 属性指向当前对象的原型对象,本文中也是一直用的这个属性,但是由于是非标准的属性,在实际业务中,我们一般不会这么去获取对象的原型。Object.getPrototypeOf() 接受一个参数,就是要获取原型的对象。

function Person () {}var person = new Person();//trueObject.getPrototypeOf(person) === Person.prototype;//ECMAScript5 中会抛出一个 TypeError,ECMAScript5会返回 String.prototypeObject.getPrototypeOf('123');//TypeErrorObject.getPrototypeOf(null);//TypeErrorObject.getPrototypeOf(undefined);复制代码

ECMAScript5 标准中,如果传入的参数不是一个对象比如原始对象和 null,则会抛出一个 TypeError,在 ECMAScript2015 标准中如果传入的是原始对象类型,则会将该原始类型转换成对应的包装类型,但是 nullundefined 依旧会抛出一个 TypeError

Object.setPrototypeOf(obj, proto)

Object.setPrototypeOf(obj, proto) 定义在 ECMAScript2015 标准中,用于设置对象的原型,该方法接受两个参数,第一个参数是要设置原型的对象,第二个参数是要设置的原型对象。

var o = { y: 1 };var a = { x: 1 };Object.setPrototypeOf(a, o);//trueObject.getPrototypeOf(a) === o;//1,从原型中获取到了这个属性a.y;//TypeError,第二个参数只能是object和nullObject.setPrototypeOf(a, '213');Object.setPrototypeOf(a, undefined);//TypeError,第一个参数不能是undefined和nullObject.setPrototypeOf(null, o);Object.setPrototypeOf(undefined, o);//默认失败,第一个参数是原始类型的时候var b = 13;Object.setPrototypeOf(b, o);//trueObject.getPrototypeOf(b) === Number.prototype;复制代码

Object.setPrototypeOf(obj, proto) 方法在某些情况下会抛出 TypeError ,该方法的第一个参数不能是 undefinednull,第二个参数只能是 objectnull。如果第一个参数是原始类型,则默认什么都不做。

Object.prototype.isPrototypeOf(obj)

Object.prototype.isPrototypeOf(obj)ECMAScript3 中被纳入标准,注意该方法并没有挂载在 Object 上,而是挂载在 Object.prototype 中,这意味着我们可以在任何Object 的子类中调用这个方法,该方法用于判断一个对象是不是另一个对象的原型。

var o = { y: 1 };var a = { x: 1 };//falseo.isPrototypeOf(a);//把o设置为a的原型Object.setPrototypeOf(a, o);//trueo.isPrototypeOf(a);复制代码

总结

最后总结一下,如果对象a是构造函数ClassA的实例,那么a对象内部会维护一个指针,该指针是该对象的原型,默认指向与构造函数ClassA关联的原型对象ClassA.prototype,如果原型对象还有自己的原型,这样就构成了一条原型链,在这条原型链上的所有属性与方法,在对象a都可以获取到,同时,对象a中还可以通过定义自身属性用于覆盖原型链上的方法,这给 javascript 提供了极大的灵活性,一般来说,ClassA.prototype中还会有一个constructorinstanceof 操作符用于判断某个对象是不是某个构造函数的实例,该操作符的左操作数时一个对象实例,右操作数是一个构造函数,这里构造函数只是某个对象的外在表现,javascript 在处理 instanceof 操作符的时候,其内部是判断某个对象的原型链中的某个对象是否指向构造函数关联的原型对象。 除了构造函数的情况,在 ECMAScript5 标准里,我们还可以根据自身的需求去设置,获取和判断对象的原型,包括Object.create, Object.setPrototypeOf, Object.prototype.isPrototypeOf等方法。

下面是上面所有示意图的整合,如果上面所说的都理解了,下面这张图理解起来应该不难。

参考文档

  • javascript 权威指南中文版(第六版)

转载于:https://juejin.im/post/5c20f27be51d45351c4f2798

你可能感兴趣的文章
我的友情链接
查看>>
Java BufferString
查看>>
Android笔记——Socket通信实现简单聊天室
查看>>
js修改onclick事件的四种方法
查看>>
我的友情链接
查看>>
linux文件管理必会知识
查看>>
Cocos2d-xna : 横版战略游戏开发实验4 Layer构建丰富的交互
查看>>
我的友情链接
查看>>
EDM邮件营销如何防止邮件被拒收和进垃圾箱
查看>>
hadoop报错,随记
查看>>
Linux iostat监测IO状态
查看>>
No module named yum错误的解决办法
查看>>
清除电脑垃圾文件教程
查看>>
IPSEC 、GRE、PIX
查看>>
机器学习之sklearn——EM
查看>>
tengine整合tomcat加上memcached实现高并发、负载均衡、可扩展架构
查看>>
CloudStack追求简单易用
查看>>
declare 声明Shell变量
查看>>
敏捷开发般若敏捷系列之八:敏捷的未来会怎样?
查看>>
Java 编程
查看>>