吊打 JavaScript 之从原型到原型链
发布于 2 个月前 作者 yan 126 次浏览

(给前端大全加星标,提升前端技能)

作者: 达达前端 公号 / Jeskson (本文来自作者投稿)

目录

说一说原型模式 **

每个函数都有一个 prototype 原型属性,这个属性它是一个指针,指向一个对象,而这个对象的用途是可以由特定类型的所有实例共享的属性和方法。则这个 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。

原型对象的好处就是让所有对象实例共享它所包含的属性和方法。

由上述代码可知,构造函数为一个空函数,sayName()方法和所有属性直接添加到了Person的prototype属性中。

调用构造函数来创建新对象,这个新对象会具有相同的属性和方法。但是和构造函数不同的是,这个新对象的属性和方法是所有实例共享的。

就是,person1和person2访问的都是同一组属性和同一个sayName()函数。

原型对象 **

只要创建一个新函数,其内部就会创建一个 prototype 属性,这个属性指向函数的原型对象。而所有原型对象都会自动获得一个 constructor

构造函数 属性,这个属性指向prototype属性所在函数的指针。

示例:Person.protype.constructor指向Person。

JavaScript对象原型

所有JavaScript对象都从原型继承属性和方法。学习如何使用对象构造器。

您现在无法对已有的对象构造器添加新属性,如果要向构造器添加一个新属性,就要把它添加到构造函数里。

原型继承

所有JavaScript对象都从原型继承属性和方法。日期对象继承来自Date.prototype。数组对象继承来自Arrray.prototype。

Person对象继承来自Person.prototype。Object.prototype位于原型继承链的顶端。

日期对象,数组对象和Person对象都继承来自Object.prototype。

向对象添加属性和方法

需求一,当需要向所有给定类型的已有对象添加新属性,或者是方法。

需求二,当需要向对象构造器添加新属性,或者是是方法。

使用prototype属性

JavaScript中的prototype属性允许你为对象构造器添加新属性,或者是方法。

创建了构造函数后,其原型对象会取得 constructor属性,至于其他方法,都是从Object继承来的,当调用构造函数创建一个新实例后,该实例的内部包含一个指针,指向构造函数的原型对象。

这个指针就叫prototype,每个对象上都有一个属性叫 proto。注意的是这个指针存在于实例与构造函数的原型对象之间,不是存在于实例与构造函数之间。

你可以把Person构造函数,Person的原型属性,和Person的两个实例之间的关系结构画出来分析分析。

一个Person构造函数有一个prototype。指向了原型对象,Person Prototype。

一个Person Prototype中有constructor,name,age,job,sayName。而Person.prototype.constructor又指回了Person。

原型对象中除了包含一个constructor属性外,还有后来添加的其他属性。

Person的每个实例都有一个内部属性,该属性仅仅指向了Person.prototype,严格说,它们和构造函数没有直接关系。

重点之一,当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象,这个指针叫[[Prototype]]。在每个对象上都支持一个属性__proto__。

讲解概念

什么是对象?对象就是属性和方法的集合,即变量和函数的封装,每个对象都有一个__proto__属性,指向这个对象的构造函数的原型对象。

什么是构造函数?构造函数就是用于创建对象的函数,通过new关键字的方法生成对象,函数名一般首字母大写。

什么是原型对象?每个函数都有一个prototype属性,它是一个指向原型对象的指针,原型对象在定义函数时同时被创建。

对象中的__proto__属性

对象中的__proto__属性在所有实现中都是无法访问到的,但是可以通过 isPrototypeOf()方法来确定对象之间是否存在这种关系。

person1和person2中内部有一个指向Person.prototype的指针,返回就是true了。

在ECMAScript5中增加了一个新的方法,叫

Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。

所以有示例如下:

确认Object.getPrototypeOf()返回的对象实际是这对象的原型,使用Object.getPrototypeOf()可以方便地取得一个对象的原型。

代码调用过程,调用对象的某个属性时,会首先搜索从对象实例本身开始,如果找到了给定名字的属性,则返回该属性的值,如果没有找到。

会第二次搜索,从指针指向的原型对象开始,在原型对象中查找给定名字的属性,如果在原型对象中查找具有给定名字的属性,就返回该属性值。

简单来说,解析器会有两问,第一次找到就一问。

第一问:实例person1有sayName属性吗?第二问:实例person1的原型有sayName属性吗?

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。

注意: person1的name被一个新值给屏蔽了。

person1.name返回的值来自对象实例;person2.name的取值来自原型。访问person1.name时,在实例上搜索这个名为name的属性,存在,则返回其值。

访问person2.name时,在实例上没有该属性,就会在原型上搜索,如果有该name属性,则返回其值。

为对象添加一个属性,这个属性会屏蔽掉原型对象中相同的属性名。添加一个属性,只会阻止我们访问原型对象中的那个属性,不会改变那个属性,而是访问实例对象上的属性。

接下来使用delete操作符删除实例属性试试,看看能否重新访问到原型中的属性。

使用delete操作符,把保存的值删除了,恢复了对原型中的name属性的连接。

如何判断一个属性是否存在于实例中呢,还是存在于原型中呢?

我们可以使用hasOwnProperty()方法来给指定属性判断是否存在于对象实例中,存在对象实例中时,返回值为true。

由以上代码可以知道,通过使用hasOwnProperty()方法,我们知道什么时候访问的是实例属性,什么时候访问的是原型属性。

使用in操作符

in操作符用来判断在通过对象能够访问给定属性时,返回为true。无论是该属性是在实例中还是在原型中。

同时使用hasOwnProperty()方法和In操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。

上述代码用来判断属性存在于原型中。

描述,in操作符通过对象能够访问到属性就返回true,hasOwnProperty()只要属性存在于实例中时才返回true。

所以只要in操作符返回true而hasOwnProperty()返回false,就可以确定属性是原型中的属性。

要取得对象上所有可枚举的实例属性,可使用Object.keys()方法,这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

可以看出,Object.keys()方法保存的是一个数组,循环顺序的出现,如果有Person的实例调用,那么出现的实例中的属性。

如果要出现所有实例属性,无论它是否可以枚举,都可以使用

Object.getOwnPropertyNames()方法。

返回结果中包含不可枚举的constructor属性。

使用这个方法创建,以对象字面量创建新对象的形式,缺少了constructor属性的指向,不再指向Person了,每个函数新创建,就会同时创建一个prototype对象,这个对象会自动获得一个constructor属性。

此时constructor属性变成了新对象的constructor属性,指向的是Object构造函数,不再指向Person函数。

如果想要保留constructor的指向,可以这样表示。

这样做,constructor属性会导致[[Enumerable]]的特性为true,默认情况下,原生的constructor属性是不可以枚举的。

重新设置构造函数,让constructor保持不可枚举。

上述代码就可以了,只适用于ESMAScript5兼容的浏览器。

friend.sayHi()调用可以进行访问。原因是实例与原型之间的松散连接的关系。实例与原型之间的连接只不过是一个指针,而不是一个副本,所以可以在原型中找到该想要的属性并返回保存在那里的函数。

由上述代码,第一,先创建了一个Person的构造函数,空函数,第二,创建了一个Person的实例,第三,重写了其原型对象,调用friend.sayName()函数时发生了错误。

因为friend的指向是Object的构造函数,而不是再指向Person函数。

原型模式的重要性

可以在Array.prototype中找到sort()方法,在Sting.prototype中可以找到substring()方法。

基本包装类型String

上述代码不建议使用该操作。

引用类型值的属性

使用构造函数模式和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。

有上述代码可以知道,在构造函数中定义实例属性,在原型中定义所有实例共享的属性constructor和方法sayName()。

在person1中添加内容,并不会影响到person2,因为它们分别引用了不同的数组。

定义应用类型或者是创建自定义类型的方式, 使用组合的构造函数模式和原型模式。

上述代码表示,只有sayName()方法不存在的情况下,才会将它添加到原型中。

寄生构造函数模式

继承,关于继承,有两种继承方式,接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。

原型Person.prototype,中constructor属性:

Person.prototype.constructor === Person,

在__proto__属性中:

Person.prototype.proto == Object.prototype

构造函数,prototype属性指向其原型对象,constructor属性指向Function。

Person.constructor === Function

__proto__属性:

Person.proto === Function.prototype

通过字面量方式创建对象时,它的原型就是Object.prototype,我们无法直接访问内置属性[[Prototype]],但是我们可以通过

Object.getPrototypeOf()或者是对象__proto__获取对象的原型。

创建对象的方式是使用Object.create()。这个方法会以你传入的对象作为创建出来的对象的原型。

什么是原型链?

由于__proto__是如何对象都有的属性,而js里万物皆对象,所以会形成一条__proto__连起来的链条,递归访问__proto__必须最终到头,并且值为null。

当js引擎查找对象的属性时,先查找对象本身是否存在该属性,如果不存在,会在原型链上查找,但不会查找自身的prototype。

继承的主要思路是利用原型链,原型链作为实现继承的主要方法,利用原型让一个引用类型继承另一个引用类型的属性和方法。

就是通过原型对象的constructor指向构造函数,实例对象通过__proto__指向原型对象。

一层层地上溯,所有对象的原型最终都可以上溯到

Object.prototype,即Object构造函数的prototype属性。就是所有对象都继承了Object.prototype属性。

那么Object.prototype对象有没有它的原型呢?是有的,Object.prototype的原型就是Null。由于Null没有任何属性,就是原型链的尽头。

读取对象中的某个属性,JavaScript引擎先寻找对象本身的属性,如果找不到就到它的原型去找,如果还是找不到,就到原型的原型中去找。直到最顶层的Object.prototype时,还是找不到,则返回Undefined。

我做了一个示意图如下:

当一个对象调用自身不存在的属性或者是方法时,就会去自己[proto]关联的前辈[prototype]对象上去找,如果没有找到,就会去该prototype原型[proto]关联的前辈[prototype]上去找。

以此类推,直到找到属性或者是方法或者是Undefined为止,这就是所谓的原型链。

总结:

  • 每创建一个函数,该函数都会自动带有一个prototype属性,这个属性是一个指针,指向一个对象,该对象称为原型对象。
  • 原型对象上默认有一个属性为constructor,该属性也是一个指针,指向其相关联的构造函数。
  • 通过调用构造函数产生的实例对象,都有一个内部属性,指向了原型对象,其实例对象能够访问原型对象上的所有属性和方法。
  • 每个构造函数都有一个原型对象,原型对象上包含着一个指向构造函数的指针,而实例都包含着一个指向原型对象的内部指针。
  • 实例可以通过内部指针访问到原型对象,原型对象可以通过constructor找到构造函数。
  • functions中有prototype,objects中有__proto__,prototype是函数才有的属性,__proto__是每个对象都有的属性,但__proto__不是一个规范属性,只是部分浏览器实现了此属性,对应的标准是[[Prototype]]
  • 大多数情况下,__proto__可以理解为“构造器的原型”,即:proto===constructor.prototype。
  • __proto__的指向取决于对象创建时的实现方式。
  • 构造函数实例,封装的函数,如果通过new操作符来调用,就是构造函数,如果没有通过new操作符来调用的,就是普通函数。
  • 函数Person(对象)有一个属性prototype指针,指向原型对象,Person.prototype原型对象,实质也是对象,它有个属性constructor指针,又指向Person函数对象。
  • 实例对象person1有个属性[[Prototype]](内部属性,chrome和firefix,Safari中这个属性叫__proto__)指向原型对象。
  • 可以通过实例对象的constructor访问构造函数,可以使用代码person1.constructor访问构造函数,但是constructor的本质是原型对象上的属性。
  • 所有对象都有valueOf和toString方法的原因就是从Object.prototype继承的。
  • 在整个原型链上寻找某个属性,对性能有影响的,越是上层的原型对象,对性能的影响就越大,如果寻找某个不存在的属性,将会遍历整个原型链。
  • js中一切都是对象,但是也区分普通对象和函数对象,通过new Function()出来的就是函数对象。普通对象的构造函数就是Object,而函数对象的构造函数就是Function。
  • Function prototype是一个空函数。
  • 每个对象都有__proto__属性,但是只有函数对象才有prototype属性。

☆ END ☆ ** 参考文档来源:《JavaScript 高级程序设计》

推荐阅读  点击标题可跳转 JS 中原型和原型链深入理解

前端面试必备 ES6 全方位总结

一文完全吃透 JavaScript 继承(面试必备良药)

觉得本文对你有帮助?请分享给更多人

关注「前端大全」加星标,提升前端技能

好文章,我在看❤️

回到顶部