对Javascript 类、原型链、继承的理解
一、序言
和其他面向对象的语言(如Java)不同,Javascript语言对类的实现和继承的实现没有标准的定义,而是将这些交给了程序员,让程序员更加灵活地(当然刚开始也更加头疼)去定义类,实现继承。(以下不讨论ES6中利用class、extends关键字来实现类和继承;实质上,ES6中的class、extends关键字是利用语法糖实现的)
Javascript灵活到甚至可以实现接口的封装(类似Java中的Interface和implements)。
二、类的实现
1.我对类的理解
首先,我先说说我对类的理解:类是包含了一系列【属性/方法】的集合,可以通过类的构造函数创建一个实例对象(例如人类是一个类,而每一个人就是一个实例对象),而这个实例对象中会包含两方面内容:
a.类的所有非静态【属性/方法】
非静态【属性/方法】就是每一个实例所特有的,属于个性。(例如每个人的名字都不相同,而名字这个属性就是一个非静态属性)
b.类的所有静态【属性/方法】
静态【属性/方法】就是每一个实例所共享的,属于共性。(例如每个人都要吃饭,而吃饭这个方法就是一个非静态方法)
2.Javascript对类的实现
a.利用函数创建类,利用new关键字生成实例对象
(话不多说,先上代码,以下没有特别说明的话,我都会先上代码,然后进行解释说明)
// 代码2.2.a function Human() { console.log('create human here') } var fakeperson = Human() // undefined var person = new Human() // {}
这里Human既是一个普通函数,也是一个类的构造函数,当调用Human()的时候,它作为一个普通函数会被执行,会输出create human here,但是没有返回值(即返回undefined);而当调用new Human()时,也会输出create human here并且返回一个对象。因为我们用Human这个函数来构造对象,所以我们也把Human称作构造函数。所以通过定义构造函数,就相当于定义了一个类,通过new关键字,即可生成一个实例化的对象。
b.利用构造函数实现非静态【属性/方法】
// 代码2.2.b function Human(name) { this.name = name } var person_1 = new Human('Jack') var person_2 = new Human('Rose') console.log(person_1.name) // Jack console.log(person_2.name) // Rose
这里的Human构造函数中多了一个参数并且函数体中多了一句this.name = name,这句话的中的this指针指向new关键字返回的实例化对象,所以根据构造函数参数的不同,其生成的对象中的具有的属性name的值也会不同。而这里的name就是这个类的** 非静态【属性/方法】**
c.利用prototype实现静态【属性/方法】
这里因为要用到原型链的知识,所以放到原型链后面说。
三、原型链
1.类的prototype是什么?
在Javascript中,每当我们定义一个构造函数,Javascript引擎就会自动为这个类中添加一个prototype(也被称作原型)
2.对象的proto是什么?
在Javascript中,每当我们使用new创建一个对象时,Javascript引擎就会自动为这个对象中添加一个proto属性,并让其指向其类的prototype
// 代码3.2 function Human(name) { this.name = name } console.log(Human.prototype) var person_test1 = new Human('Test1') var person_test2 = new Human('Test2') console.log(person_test1.__proto__) console.log(person_test2.__proto__) console.log(Human.prototype === person_test1.__proto__) // true console.log(Human.prototype === person_test2.__proto__) // true
我们会发现Human.prototype是一个对象,Human类的实例化对象person_test1、person_test2下都有一个属性proto也是对象,并且它们都等于Human.prototype,我们知道在Javascript中引用类型的相等意味着他们所指向的是同一个对象。所以我们可以得到结论,任何一个实例化对象的proto属性都指向其类的prototype。
3.对象的proto有什么作用?
// 代码3.3 var Pproto = { name:'jack' } var person = { __proto__:Pproto } console.log(person.name) // jack person.name = 'joker' console.log(person.name) // joker
我们发现最开始我们并没有给person定义name属性,为什么console出来jack呢?这就是Javascript著名的原型链的结果啦。话不多说,先上图:
当我们访问person.name时,发生了什么呢?
首先它会访问person对象本身的属性,如果本身没有定义name属性的话,它会去寻找它的 proto属性对象,在这个例子中person的 proto属性对应的是Pproto对象,所以person的 proto指向了Pproto,然后我们发现Pproto对象是具有name属性的,那么person.name就到此为止,返回了jack,但是如果我们又给person加上了一个自身的属性name呢?这时,再次person.name就不会再寻找 proto了,因为person本身已经具有了name属性,而且其值为joker,所以这里会返回joker.
我们注意到上图中Pproto的proto指向了Object,这是因为每一个通过字面量的方式创建出来的对象它们都默认是Object类的对象,所以它们的proto自然指向Object.prototype。
4.利用prototype实现静态【属性/方法】
// 代码3.4 function Human(name) { this.name = name } Human.prototype.eat = function () { console.log('I eat!') } var person_1 = new Human('Jack') var person_2 = new Human('Rose') person_1.eat() // I eat! person_2.eat() // I eat! console.log(person_1.eat === person_2.eat) // true
这里我们在构造函数外多写了一句:Human.prototype.eat = function() {...} 这样以后每个通过Human实例化的对象的proto都会指向Human.prototype,并且根据上述原型链知识,我们可以知道只要构造函数中没有定义同名的非静态【属性/方法】,那么每个对象访问say方法时,访问的其实都是Human.prototype.say方法,这样我们就利用prototype实现了类的静态【属性/方法】,所有的对象实现了共有的特性,那就是eat
四、继承的实现
1.我对继承的理解
假如有n(n>=2)个类,他们的一些【属性/方法】不一样,但是也有一些【属性/方法】是相同的,所以我们每次定义它们的时候都要重复的去定义这些相同的【属性/方法】,那样岂不是很烦?所以一些牛逼的程序员想到,能不能像儿子继承父亲的基因一样,让这些类也像“儿子们”一样去“继承”他们的“父亲”(而这里的父亲就是包含他们所具有的相同的【属性/方法】)。这样我们就可以多定义一个类,把它叫做父类,在它的里面包含所有的这些子类所具有的相同的【属性/方法】,然后通过继承的方式,让所有的子类都可以访问这些【属性/方法】,而不用每次都在子类的定义中去定义这些【属性/方法】了。
2.原型链实现继承(让子类继承了父类的静态【属性/方法】)
// 代码4.1 function Father() { } Father.prototype.say = function() { console.log('I am talking...') } function Son() { } var sonObj_1 = new Son() console.log(sonObj_1.say) // undefined // 原型链实现继承的关键代码 Son.prototype = new Father() var sonObj_2 = new Son() console.log(sonObj_2.say) // function() {...}
看到这句Son.prototype = new Father()你可能有点蒙圈,没关系,我先上个原型链的图,你分分钟就能明白了
对着图我们想一想,首先,一开始Son、Father两个类没有什么关系,所以在访问say的时候肯定是undefined,但是当我们使用了Son.prototype = new Father()后,我们知道通过new Son()生成的对象都会有 proto属性,而这个属性指向Son.prototype,而这里我们又让它等于了一个Father的对象,而Father类又定义了静态方法say,所以这里我们的sonObj_2通过沿着原型链寻找,寻找到了say方法,于是就可以访问到Father类的静态方法say了。这样就实现了子类继承了父类的静态【属性/方法】,那么如何让子类继承父类的非静态【属性/方法】呢?
3.构造函数实现继承(让子类继承了父类的非静态【属性/方法】)
// 代码4.3 function Father(name) { this.name = name } function Son() { Father.apply(this, arguments) this.sing = function() { console.log(this.name + ' is singing...') } } var sonObj_1 = new Son('jack') var sonObj_2 = new Son('rose') sonObj_1.sing() // jack is singing... sonObj_2.sing() // rose is singing...
在这个例子中,通过在Son的构造函数中利用apply函数,执行了Father的构造函数,所以每一个Son对象实例化的过程中都会执行Father的构造函数,从而得到name属性,这样,每一个Son实例化的Son对象都会有不同的name属性值,于是就实现了子类继承了父类的非静态【属性/方法】
4.组合方式实现继承(组合 原型链继承 + 构造函数继承)
顾名思义,就是结合上述两种方法,然后同时实现对父类的静态及非静态【属性/方法】的继承,代码如下:
// 代码4.4 function Father(name) { this.name = name } Father.prototype.sayName = function() { console.log('My name is ' + this.name) } function Son() { Father.apply(this, arguments) } Son.prototype = new Father('father') var sonObj_1 = new Son('jack') var sonObj_2 = new Son('rose') sonObj_1.sayName() // My name is jack sonObj_2.sayName() // My name is rose
这里子类Son没有一个自己的方法,它的sayName方法继承自父类的静态方法sayName,构造函数中继承了父类的构造函数方法,所以得到了非静态的name属性,因此它的实例对象都可以调用静态方法sayName,但是因为它们各自的name不同,所以打印出来的name的值也不同。看到这里,大家可能认为这已经是一种完美无缺的Javascript的继承方式了,但是还差一丢丢,因为原型链继承不是一种纯粹的继承原型的方式,它有副作用,为什么呢?因为在我们调用Son.prototype = new Father()的时候,不仅仅使Son的原型指向了一个Father的实例对象,而且还让Father的构造函数执行了一遍,这样就会执行this.name = name;所以这个Father对象就不纯粹了,它具有了name属性,并且值为father,那为什么之后我们访问的时候访问不到这个值呢?这又是因为原型链的原因啦,话不多说先上图:
所以这里父类的构造函数在进行原型链继承的时候也执行了一次,并且在原型链上生成了一个我们永远也不需要访问的name属性,而这肯定是占内存的(想象一下name不是一个字符串,而是一个对象),那么我们怎么能让原型链继承更纯粹一点呢?让它只继承原型(静态【属性/方法】)呢?
5.寄生组合方式实现继承
为了让原型链继承的更纯粹,这里我们引入一个Super函数,让Father的原型寄生在Super的原型上,然后让Son去继承Super,最后我们把这个过程放到一个闭包内,这样Super就不会污染全局变量啦,话不多说上代码:
// 代码4.4 function Father(name) { this.name = name } Father.prototype.sayName = function() { console.log('My name is ' + this.name) } function Son() { Father.apply(this, arguments) } (function () { function Super(){} Super.prototype = Father.prototype Son.prototype = new Super() }()) var sonObj_1 = new Son('jack')
这个时候再去打印sonObj1就会发现,它的原型中已经没有name属性啦,如下所示:

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
更深入的理解 Python 中的迭代
Visual Studio 的应用周期管理Application Lifecycle Management(ALM)项目 —— Ranger 是一个志愿者社区,它提供专业的指导、实践经验、以及开发者社区的漏洞修补解决方案。它创建于 2006 年,作为微软内部社区去 “将产品组与大家相连接,并去除推广阻力”。 在 2009 时,社区已经有超过 200 位成员,这导致了协作和计划面临很大的挑战,在依赖和手工流程上产生了瓶颈,并导致了开发者社区不断增加的延迟和各种报怨。在 2010 时,计划进一步去扩充包括微软最有价值专家(MVP)在内的分布在全球的社区。 这个社区被分割成十几个活跃的团队。每个团队都致力于通过它的生命周期去设计、构建和支持一个指导或处理项目。在以前,团队的瓶颈在团队管理级别上,原因是严格的、瀑布式的流程和高度依赖一个或多个项目经理。在制作、发布和“为什么、做什么、和怎么做”驱动的决定上,项目经理都要介入其中。另外,缺乏一个实时的指标阻止了团队对他们的解决方案效率的监控,以及对来自社区的关于 bug 和常见问题的关注。 是时候去寻找一些做好这些事情的方法了,更好地实现开发者社...
- 下一篇
5月28日云栖精选夜读丨阿里安全猎户座实验室:最新恶意软件VPNFilter的技术解析与防护建议
研究结果表明,VPNFilter是一个可扩展性强、有较好健壮性、高水平及非常危险的安全威胁,高度模块化的框架允许快速更改操作目标设备,同时为情报收集和寻找攻击平台提供支撑。VPNFilter破坏性较强,可以通过烧坏用户的设备来掩盖踪迹,比简单地删除恶意软件痕迹更深入,同时VPNFilter恶意软件的组件允许盗窃网站凭证和监控Modbus SCADA协议。 热点热议 阿里安全猎户座实验室:最新恶意软件VPNFilter的技术解析与防护建议 作者:华蒙 阿里研究院副院长杨健:新零售领动数字经济 作者:技术小能手 阿里巴巴副总裁刘松:工业互联网如何驱动制造业数字化转型 作者:技术小能手 知识整理 破解“人工智能人才培养难”需综合发力 作者:技术小能手 AI不是魔法:人工智能的能与不能 作者:技术小能手 关于神经网络,这里有你想要了解的一切! 作者:【方向】 Python库大全(涵盖了Python应用的方方面面),建议收藏留用! 作者:q1622479435 【干货】计算机视觉视频理解领域的经典方法和最新成果 作者:技术小能手 美文回顾 进入深度学习之前,想清楚这些问题! 作者:【方向】 比传...
相关文章
文章评论
共有0条评论来说两句吧...