博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
JavaScript中的原型(prototype)与继承
阅读量:7180 次
发布时间:2019-06-29

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

在JavaScript中,原型是用来模仿其他「类」语言继承机制的基础。原型并不复杂,原型只是一个对象。

一、原型对象

1.1 什么是原型对象

每当我们创建了一个函数后,这个函数就拥有了一个prototype属性,这个属性指向一个对象——原型对象。原型对象一开始就有一个constructor属性,指向我们创建的函数。而当我们对这个函数进行构造函数调用创建了一个实例对象,这个对象将会有一个特殊的内置属性[[prototype]],这个属性就是对函数原型对象的引用。构造函数、实例对象和原型对象的关系请看下图:

Person为「构造函数」,person1与person2为实例对象、Person Prototype为函数的原型对象。通过同一个函数构造的对象内部的[[prototype]]属性都指向了同一个原型对象,这是一种共有的关联关系,原型中的所有属性和方法都被这些实例共享。

从图上也看出了,实例与构造函数之间不是直接关联的,而是通过原型的constructor间接关联的。

无论是通过构造函数实例化的对象还是通过对象字面量创建的对象,他们都有对应的原型,对于用对象字面量创建的对象来说,其默认的原型为Object.prototype。实际上,几乎所有对象都有原型。

var Person = function(name){    this.name = name;}var person1 = new Person('person1');var obj1 = { name: 'obj1' };console.log(Object.getPrototypeOf(obj1) === Object.prototype);//trueconsole.log(Object.getPrototypeOf(person1) === Person.prototype);//true复制代码

1.2 原型链

既然我们说了所有对象都有原型,那么就意味着原型也有原型,即原型对象的[[prototype]]属性也指向另一个对象。如此一来就形成了一个原型链,而这个链的尽头是Object.prototype。

通过上图的[[prototype]]属性的指向可以清晰地看到原型对象之间的连接。 ## 1.2 原型对象的作用 原型对象有什么用呢?大家一定还记得引擎是如何在作用域链中查找一个变量的,作用域链用于查找声明的变量,而对象的属性则在对象及其原型链中查找。当读取对象的属性时,首先在对象中查找,当查找不到时开始从对象的原型中查找。根据之前提到的原型也是一个对象,所以如果在对象的原型中找不到,那么将会到原型的原型中找,直到找到或者到了原型链的尾端返回undefined为止。
var Person = function(name){    this.name = name;}Person.prototype = {	constructor:Person,	age:23}console.log(person1.age);//23--原型中的属性复制代码

1.2.1 属性屏蔽与设置

假设我们查找myObject.foo属性,而原型链上有多个name属性,会发生什么呢?与作用域链类似,会发生「屏蔽」。根据查找的机制,总会返回第一个找到的同名属性而忽略后续的同名属性。

那么给对象设置属性的时候呢?这时候情况就有点复杂了:

  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没 有被标记为只读(writable:false),那就会直接在myObject中添加一个名为foo的新 属性,它是屏蔽属性。
  2. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在myObject上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
  3. 如果在[[Prototype]]链上层存在foo并且它是一个setter(参见第3章),那就一定会 调用这个setter。foo不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo这 个setter。 4.如果对象以及[[prototype]]链中都没有foo属性,foo属性将会被添加到myObject上。

简单地说分为四种情况:①有且可写 ②有但只读 ③有且是setter ④没有。只有①和④会在myObject上创建属性。当原型链上有同名只读属性时将会阻止同名属性的新增,如果是setter将会按照setter的逻辑操作。

var Person = function(name){    this.name = name;}//修改原型//Person.prototype.name = 'modifined';//重新连接(替换)原型而不是修改原型Person.prototype = {    constructor:Person,//恢复constructor属性,但变成了可枚举    name:'constructor'//默认是可写的}Object.defineProperty(Person.prototype,'age',{    value:23,    writable:false//设置为只读})var person1 = new Person('person1');console.log(person1.age);//23person1.name = 'tom';person1.age = 20;//严格模式下将会报错console.log(person1.age);//23——修改被忽略console.log(person1.name);//tom复制代码

在上面的代码中我们替换了Person的原型,注意这个操作将会导致实例失去与原原型的连接,转而关联替换后的原型。也就是说,实例再也无法访问原原型中的属性和方法。替换原型用于模仿继承机制,接下来将会讲到实际上并不是继承。

二、类和构造函数

2.1 没有「类」

JavaScript中并没有类似于Java的那种类,关于「类」的一切都是围绕着函数和原型来展开的。虽然ES6中增加了class特性,但是底层上它还是在操作函数和原型,也算不上是真正的类。我们看看Java的继承和JavaScript的继承:

//javapublic class Foo{	String name = "foo";	public String getName(){		System.out.println(this.name);	}}public class Bar extends Foo{}Bar bar1 = new Bar();Bar bar2 = new Bar();Foo foo = new Foo();foo.name = 'modifined';bar1.name = "bar1";bar2.getName();//foo--丝毫不受影响复制代码
//jsvar Foo = function Foo(){}Foo.prototype = {	name:'foo',	getName:function getName(){		console.log(this.name);	}}var Bar = function(){}Bar.prototype = Foo.prototype;//"继承"FooBar.prototype.sayHi = function sayHi(){
//拓展Foo console.log('hi!');}var bar1 = new Bar();var bar2 = new Bar();Foo.prototype.name = 'modifined';bar1.getName();//'modifined'bar2.getName();//'modifined'复制代码

从这两段代码可以看到,在Java中继承意味着子类实例对属性进行了私有化,无论是父类对象还是其他子类对象都无法影响到继承的属性。而在JavaScript中,"子类实例"只是关联了共同的对象,只要那个对象更改了就能马上反映到每一个"子类对象"。所以我觉得不应该用「继承」来形容这种机制了,用「委托」更形象,它们本质上是对象之间的关联。

2.2 没有「构造函数」

JavaScript中也没有构造函数。所有的函数都是一样的,都可以通过函数名+()调用,所有的函数都可以new实例化得到一个对象。如果你认为可以new的就是构造函数,那么JavaScript中所有的函数都是构造函数了。

var foo = function foo(){	console.log('foo');}var obj = new foo();//'foo'console.log(obj.constructor === foo);//truefoo.prototype.constructor = Object;console.log(obj.constructor === Object);//true复制代码

我们可以看到,一个普通的foo函数也可以进行new调用实例化一个对象,可能你还想通过constructor属性去证明他是obj的构造函数。但是可以看到我们随后修改了原型中的constructor属性,obj的"构造函数"也发生改变了。这说明什么?constructor根本就没那么「权威」,它只是原型中的一个属性,默认指向了被构造调用的函数,我们可以自行修改它。

2.3 还是想要「类」

可能有时候写「类」语言写多了一下子还没转过来,还是想去模仿类,怎么办?没关系有办法的——方法可以共用,属性要私有嘛,请看代码:

var father = function father(name,age){	this.name = name;	this.age = age;}father.prototype = {	constructor:father,//不让这个属性丢失	sayName:function sayName(){		console.log(this.name);	},	sayAge:function sayAge(){		console.log(this.age);	}}var son = function(name,age){	father.call(this,name,age);//构造函数借用,绑定son的this}son.prototype = Object.create(father.prototype);//新建对象,与原father.prototype隔离//不要这样:son.prototype = father.prototype;//否则后续对son.prototype的拓展都是在修改father.prototypevar son1 = new son('tom',12);var son2 = new son('jack',15);father.prototype.name = 'mike';son1.sayName();//tomson1.name = 'kobee';//屏蔽son2.sayName();//jack复制代码

这里主要有两点:一是构造函数借调、二是隔离原父函数的prototype。借调使得我们可以"借用"父函数的代码,当然我们要绑定this才能实现对新对象赋值;接着我们要隔离父函数的原型,以免我们拓展子函数原型时影响到父函数原型。通过Object.create(...)创建一个空的新对象,这个对象的[[prototype]]指向我们传递的对象,这里是father.prototype。我们对son.prototype的修改都是在修改这个新对象,而不是father.prototype。

好了,类模仿完毕了,不过我觉得如果用类的思维去理解这段代码效率会比较低,如果用函数+原型的概念去理解会更好,你觉得呢?

2.4 寻找原型(委托)对象

那么怎么寻找一个对象的原型呢。如果还是用类的思维去判断的话,要判断一个对象是否是一个"类"的实例就会这么做:

var foo = function foo(){};var obj = new foo();obj instanceof foo;//true复制代码

instanceof操作符的左操作数是一个对象,右操作数是一个函数,实际上它在问:在obj的[[prototype]]链中有没有foo.prototype?在这里明显有~所以我们知道了obj的原型(委托)对象为foo.prototype

但是如果想判断两个对象是否关通过[[prototype]]关联呢?instanceof肯定不行,它的右操作数是函数,而我们只有两个对象。如果你已经理解了JavaScript中对象关联(委托)的这个思想,可以这么做:

var foo = function foo(){};var a = new foo();var b = Object.create(o1);function isRelatedTo(o1, o2) {	function F(){}	F.prototype = o2;	return o1 instanceof F;}console.log(isRelatedTo(b,a));//true复制代码

说实话这样做没有错,o2被关联到F函数的原型上,然后用instanceof操作符就可判断o2是否在o1的[[prototype]]链上。这里对象a的确在对象b的原型链上。

但是我们有必要绕一个大圈吗?或者是说没有直接判断的工具吗?有的:

var foo = function foo(){};var a = new foo();console.log(foo.prototype.isPrototypeOf(a));//true复制代码

a.isPrototypeOf(b)在问:a是否出现在b的[[prototype]]链上?这里与上面不同的是,我们用两个对象进行判断,即进行了原型与对象之间的关联判断。而不是"类"与对象之间关联的判断,

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

你可能感兴趣的文章
大数据,让知识成为一种服务
查看>>
二十款免费WiFi黑客(渗透测试)工具
查看>>
《嵌入式设备驱动开发精解》——第1章 关于本教程
查看>>
Aviator(表达式执行引擎)发布1.0.1
查看>>
海量高性能列式数据库HiStore技术架构解析
查看>>
Linux块设备驱动之内存模拟块设备
查看>>
「技术大牛」是如何缩短事件平均解决时间的?
查看>>
新人成长:新人如何快速融入技术实力强的前端团队
查看>>
Testing Flutter apps翻译-性能分析
查看>>
手把手教你用 node 玩跳一跳
查看>>
SQL 优化
查看>>
如何在SpringBoot中集成JWT(JSON Web Token)鉴权
查看>>
Redis应用场景及常见问题
查看>>
Sass初入门
查看>>
js常见算法(一):数组去重,打乱数组,统计数组各个元素出现的次数, 字符串各个字符的出现次数,获取地址链接的各个参数...
查看>>
lua 学习总结
查看>>
spring+Kafka+springmvc Demo
查看>>
基于Docker下的MySQL主从复制
查看>>
VUE 面试总结
查看>>
React Native组件开发指南
查看>>