摘要:基于原型的繼承誠惶誠恐的寫下這篇文章。無論是還是,都是面向?qū)ο蟮摹⑦@個(gè)新對象作為構(gòu)造函數(shù)的執(zhí)行上下文其指向這個(gè)對象,并執(zhí)行構(gòu)造函數(shù)返回這個(gè)對象原型繼承我們來定義一個(gè)簡單的類和它的原型我們在原型上定義了一個(gè)方法。
源碼: https://github.com/RobinQu/Programing-In-Javascript/blob/master/chapters/JavaScript_Core/Object_Oriented_Javascript/Javascript_Prototypal_Inheritance.md
原文: http://pij.robinqu.me/JavaScript_Core/Object_Oriented_Javascript/Javascript_Prototypal_Inheritance.html
本文需要補(bǔ)充更多例子
本文存在批注,但該網(wǎng)站的Markdown編輯器不支持,所以無法正常展示,請到原文參考。
基于原型的JavaScript繼承誠惶誠恐的寫下這篇文章。用JavaScript實(shí)現(xiàn)繼承模型,已經(jīng)是非常成熟的技術(shù),各種大牛也已經(jīng)寫過各式的經(jīng)驗(yàn)總結(jié)和最佳實(shí)踐。在這里,我只能就我所能,寫下我自己的思考和總結(jié)。
在閱讀之前,我們先假設(shè)幾個(gè)在面向?qū)ο缶幊讨械母拍钍谴蠹沂煜さ模?/p>
類, Class
構(gòu)造函數(shù), Constructor
繼承, Inheritance
實(shí)例, Instance
實(shí)力化, Instantiation
方法, Method
多態(tài), Polymorphism
接口, Interface
由于講解這些概念是十分復(fù)雜的,所以還請參閱其他資料。
了解原型面向?qū)ο笫钱?dāng)代編程的主流思想。無論是C++還是Java,都是面向?qū)ο蟮摹?yán)格上來講,JavaScript并不是面向?qū)ο蟮?,而是“基于對象的?Object-based),因?yàn)樗拇_缺乏面向?qū)ο罄锏暮芏嗵匦?,例如?/p>
繼承
接口
多態(tài)
...
但再另一方面,JavaScript是基于原型(Prototype)的對象系統(tǒng)。它的繼承體系,叫做原型鏈繼承。不同于繼承樹形式的經(jīng)典對象系統(tǒng),基于原型的對象系統(tǒng)中,對象的屬性和方法是從一個(gè)對象原型(或模板)上拷貝或代理(Delegation)的。JavaScript也不是唯一使用這種繼承方法的編程語言,其他的例子如:
Lisp
Lua
...
那么,prototype在哪里呢?
訪問構(gòu)造函數(shù)的原型// 訪問Array的原型 Array.prototype
// 訪問自定義函數(shù)Foo的原型 var Foo = function() {} Foo.prototype訪問一個(gè)實(shí)例的原型
__proto__不是標(biāo)準(zhǔn)屬性,但是被大多數(shù)瀏覽器支持
var a = {} a.__proto__;
使用ES5的Object.getPrototypeOf:
Object.getPrototypeOf([]) === Array.prototype;
再來點(diǎn)繞彎的:
[].constructor.prototype === Array.prototypenew關(guān)鍵字
大多數(shù)面向?qū)ο笳Z言,都有new關(guān)鍵字。他們大多和一個(gè)構(gòu)造函數(shù)一起使用,能夠?qū)嵗粋€(gè)類。JavaScript的new關(guān)鍵字是異曲同工的。
等等,不是說JavaScript不支持經(jīng)典繼承么!的確,其實(shí)new的含義,在JavaScript中,嚴(yán)格意義上是有區(qū)別的。
當(dāng)我們,執(zhí)行
new F()
實(shí)際上是得到了一個(gè)從F.prototype繼承而來的一個(gè)對象。這個(gè)說法來自Douglas的很早之前的一篇文章1。在如今,如果要理解原型繼承中new的意義,還是這樣理解最好。
如果我們要描述new的工作流程,一個(gè)接近的可能流程如下:
分配一個(gè)空對象
設(shè)置相關(guān)屬性、方法,例如constructor和F.prototype上的各式方法、屬性。注意,這里執(zhí)行的并不是拷貝,而是代理。后文會(huì)講解這點(diǎn)。
將這個(gè)新對象作為構(gòu)造函數(shù)的執(zhí)行上下文(其this指向這個(gè)對象),并執(zhí)行構(gòu)造函數(shù)
返回這個(gè)對象
原型繼承我們來定義一個(gè)簡單的“類”和它的原型:
var Foo = function() {}; Foo.prototype.bar = function() { console.log("haha"); }; Foo.prototype.foo = function() { console.log("foo"); };
我們在原型上定義了一個(gè)bar方法。看看我們怎么使用它:
var foo = new Foo(); foo.bar(); // => "haha" foo.foo(); // => "foo"
我們要繼承Foo:
var SuperFoo = function() { Foo.apply(this, arguments); }; SuperFoo.prototype = new Foo(); SuperFoo.prototype.bar = function() { console.log("haha, haha"); }; var superFoo = new SuperFoo(); superFoo.foo(); // => "foo" superFoo.bar(); // => "haha, haha"
注意到幾個(gè)要點(diǎn):
在SuperFoo中,我們執(zhí)行了父級構(gòu)造函數(shù)
在SuperFoo中,我們讓然可以調(diào)用foo方法,即使SuperFoo上沒有定義這個(gè)方法。這是繼承的一種表現(xiàn):我們可以訪問父類的方法
在SuperFoo中,我們重新定義了bar方法,實(shí)現(xiàn)了方法的重載
我們仔細(xì)想想第二點(diǎn)和第三點(diǎn)。我們新指定的bar方法到底保存到哪里了?foo方法是如何找到的?
原型鏈要回答上面的問題,必須要介紹原型鏈這個(gè)模型。相比樹狀結(jié)構(gòu)的經(jīng)典類型系統(tǒng),原型繼承采取了另一種線性模型。
當(dāng)我們要在對象上查找一個(gè)屬性或方法時(shí):
在對象本身查找,如果沒有找到,進(jìn)行下一步
在該對象的構(gòu)造函數(shù)自己的prototype對象上查找,如果沒有找到進(jìn)行下一步
獲取該對象的構(gòu)造函數(shù)的prototype對象作為當(dāng)前對象;如果當(dāng)前對象存在prototype,就能繼續(xù),否則不存在則查找失敗,退出;在該對象上查找,如果沒有找到,將前面提到的“當(dāng)前對象”作為起始對象,重復(fù)步驟3
這樣的遞歸查找終究是有終點(diǎn)的,因?yàn)?
Object.prototype.__proto__ === null
也就是Object構(gòu)造函數(shù)上,prototype這個(gè)對象的構(gòu)造函數(shù)上已經(jīng)沒有prototype了。
我們來看之前Foo和SuperFoo的例子,我們抽象出成員查找的流程如下:
superFoo本身 => SuperFoo.prototype => Foo.prototype => Object.prototype
解讀原型鏈的查找流程:
superFoo本身意味著superFoo這個(gè)實(shí)例有除了能夠從原型上獲取屬性和方法,本身也有存儲(chǔ)屬性、方法的能力。我們稱其為own property,我們也有不少相關(guān)的方法來操作:
obj.hasOwnProperty(name)
Object.getOwnPropertyNames(obj)
Object.getOwnPropertyDescriptor(obj)
SuperFoo.prototype:
回憶一下這句SuperFoo.prototype = new Foo();,也就是說SuperFoo.prototoye就是這個(gè)新創(chuàng)建的這個(gè)Foo類型的對象
這也就解釋了為啥我們能訪問到Foo.prototype上的方法和屬性了
也就是說,我們要在這個(gè)新建的Foo對象的本地屬性和方法中查找
Foo.prototype:
查找到這一次層,純粹是因?yàn)槲覀冎贫?b>SuperFoo.prototype的值,回想上一條
Object.prototype
這是該原型鏈的最后一環(huán),因?yàn)?b>Object.prototype這個(gè)對象的原型是null,我們無法繼續(xù)查找
這是JavaScript中所有對象的祖先,上面定義了一個(gè)簡單對象上存在的屬性和方法,例如toString
那么,當(dāng)在SuperFoo上添加bar方法呢?這時(shí),JavaScript引擎會(huì)在SuperFoo.prototype的本地添加bar這個(gè)方法。當(dāng)你再次查找bar方法時(shí),按照我們之前說明的流程,會(huì)優(yōu)先找到這個(gè)新添加的方法,而不會(huì)找到再原型鏈更后面的Foo.prototype.bar。
也就是說,我們既沒有刪掉或改寫原來的bar方法,也沒有引入特殊的查找邏輯。
模擬更多的經(jīng)典繼承基本到這里,繼承的大部分原理和行為都已經(jīng)介紹完畢了。但是如何將這些看似簡陋的東西封裝成最簡單的、可重復(fù)使用的工具呢?本文的后半部分將一步一步來介紹如何編寫一個(gè)大體可用的對象系統(tǒng)。
熱身準(zhǔn)備幾個(gè)小技巧,以便我們在后面使用。
beget如果要以一個(gè)對象作為原型,創(chuàng)建一個(gè)新對象:
function beget(o) { function F() {} F.prototype = o; return new F(); } var foo = beget({bar:"bar"}); foo.bar === "bar"; //true
理解這些應(yīng)該困難。我們構(gòu)造了一個(gè)臨時(shí)構(gòu)造函數(shù),讓它的prototype指向我們所期望的原型,然后返回這個(gè)構(gòu)造函數(shù)所創(chuàng)建的實(shí)例。有一些細(xì)節(jié):
我們不喜歡直接做A.prototype = B.prototype這樣的事情,因?yàn)槟銓ψ宇惖男薷?,有可能直接影響到父類以及父類的所有?shí)例。大多數(shù)情況下這不是你想看到的結(jié)果
新建F的實(shí)例,創(chuàng)建了一個(gè)本地對象,可以持有(own)自身的屬性和方法,便可以支持之后的任意修改?;貞浺幌?b>superFoo.bar方法。
如果你使用的JavaScript引擎支持Object.create,那么同樣的事情就更簡單:
Object.create({bar:"bar"});
要注意Object.create的區(qū)別:
我們可以創(chuàng)建沒有原型的對象: Object.create(null)
我們可以配置創(chuàng)建的對象,參閱Object.create的文檔2
我們不必去運(yùn)行一遍父類構(gòu)造函數(shù),這樣可以避免不需要的副作用
函數(shù)的序列化、解義JavaScript的函數(shù)可以在運(yùn)行時(shí)很方便的獲取其字符串表達(dá):
var f = function(a) {console.log("a")}; f.toString(); // "function(a) {console.log("a")};"
這樣的能力其實(shí)時(shí)很強(qiáng)大的,你去問問Java和C++工程師該如何做到這點(diǎn)吧。
這意味著,我們可以去分析函數(shù)的字符串表達(dá)來做到:
了解函數(shù)的函數(shù)列表
了解函數(shù)體的實(shí)際內(nèi)容
了解一個(gè)函數(shù)是否有別名
...
動(dòng)態(tài)的thisJavaScript中的this是在運(yùn)行時(shí)綁定的,我們往往需要用到這個(gè)特性,例如:
var A = function() {}; A.methodA = function() { console.log(this === A); }; A.methodA();// => true
以上這段代碼有如下細(xì)節(jié):
A.methodA()運(yùn)行時(shí),其上下文對象指定的是A,所以this指向了A
我們可以用這個(gè)來模擬“類的靜態(tài)方法或類方法”
我們能夠通過這里的this引用到類(構(gòu)造函數(shù))本身
若干版本 最簡單版本單純實(shí)現(xiàn)一個(gè)extend方法:
var extend = function(Base) { var Class = function() { Base.apply(this, arguments); }, F; if(Object.create) { Class.prototype = Object.create(Base.prototype); } else { F = function() {}; F.prototype = Base.prototype; Class.prototype = new F(); } Class.prototype.constructor = Class; return Class; }; var Foo = function(name) { this.name = name; }; Foo.prototype.bar = function() { return "bar"; }; var SuperFoo = extend(Foo); var superFoo = new SuperFoo("super"); console.log(superFoo.name);// => "super" console.log(superFoo.bar());// => "bar"
由于過于簡單,我就不做講解了。
更復(fù)雜的例子我們需要一個(gè)根對象XObject
根對象有各種繼承方法,并能傳入一些子類的方法和屬性
我們要復(fù)用上個(gè)例子里的extend,但是會(huì)有修改
var extend = function(Base) { var Class = function() { Base.apply(this, arguments); }, F; if(Object.create) { Class.prototype = Object.create(Base.prototype); } else { F = function() {}; F.prototype = Base.prototype; Class.prototype = new F(); } Class.prototype.constructor = Class; return Class; }; var merge = function(target, source) { var k; for(k in source) { if(source.hasOwnProperty(k)) { target[k] = source[k]; } } return target; }; // Base Contstructor var XObject = function() {}; XObject.extend = function(props) { var Class = extend(this); if(props) { merge(Class.prototype, props); } // copy `extend` // should not use code like this; will throw at ES6 // Class.extend = arguments.callee; Class.extend = XObject.extend; return Class; }; var Foo = XObject.extend({ bar: function() { return "bar"; }, name: "foo" }); var SuperFoo = Foo.extend({ name: "superfoo", bar: function() { return "super bar"; } }); var foo = new Foo(); console.log(foo.bar()); // => "bar" console.log(foo.name); // => "foo" var superFoo = new SuperFoo(); console.log(superFoo.name); // => "superfoo" console.log(superFoo.bar()); // => "super bar"
上面的例子中,
XObject是我們對象系統(tǒng)的根類
XObject.extend可以接受一個(gè)包含屬性和方法的對象來定義子類
XObject的所有子類,都沒有定義構(gòu)造函數(shù)邏輯的機(jī)會(huì)!真是難以接受的:
我們偏好一個(gè)類上的init方法來初始化對象,而將構(gòu)造函數(shù)本身最簡化
繞開工廠方法的實(shí)現(xiàn)過程中,參數(shù)傳遞如何傳遞到構(gòu)造函數(shù)的問題
可以支持更多新的特性,例如super屬性、mixin特性等
總結(jié),然后呢?我們解決了一部分問題,又發(fā)現(xiàn)了一些新問題。但本文的主要內(nèi)容在這里就結(jié)束了。一個(gè)更具實(shí)際意義的對象系統(tǒng),實(shí)際隨處可見,Ember和Angular中的根類。他們都有更強(qiáng)大的功能,例如:
Ember中的binding,setter、getter
Angular中的函數(shù)依賴注入
...
但是,這些框架中對象系統(tǒng)的出發(fā)點(diǎn)都在本文所闡述的內(nèi)容之中。如果作為教學(xué),John Resig在2008年的一篇博客中3,總結(jié)了一個(gè)現(xiàn)代JavaScript框架中的對象系統(tǒng)的雛形。我創(chuàng)建了docco代碼注解來立即這段代碼,本文也會(huì)結(jié)束在這段代碼的注解。
還有一些更高級的話題和技巧,會(huì)在另外一篇文章中給出。
http://javascript.crockford.com/prototypal.html??
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create??
http://ejohn.org/blog/simple-javascript-inheritance/??
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/78139.html
摘要:首先,需要來理清一些基礎(chǔ)的計(jì)算機(jī)編程概念編程哲學(xué)與設(shè)計(jì)模式計(jì)算機(jī)編程理念源自于對現(xiàn)實(shí)抽象的哲學(xué)思考,面向?qū)ο缶幊淌瞧湟环N思維方式,與它并駕齊驅(qū)的是另外兩種思路過程式和函數(shù)式編程。 JavaScript 中的原型機(jī)制一直以來都被眾多開發(fā)者(包括本人)低估甚至忽視了,這是因?yàn)榻^大多數(shù)人沒有想要深刻理解這個(gè)機(jī)制的內(nèi)涵,以及越來越多的開發(fā)者缺乏計(jì)算機(jī)編程相關(guān)的基礎(chǔ)知識。對于這樣的開發(fā)者來說 J...
摘要:基于原型的面向?qū)ο笤诨谠偷恼Z言中如并不存在這種區(qū)別它只有對象不論是構(gòu)造函數(shù),實(shí)例,原型本身都是對象。允許動(dòng)態(tài)地向單個(gè)的對象或者整個(gè)對象集中添加或移除屬性。為了解決以上兩個(gè)問題,提供了構(gòu)造函數(shù)創(chuàng)建對象的方式。 showImg(https://segmentfault.com/img/remote/1460000013229218); 一. 重新認(rèn)識面向?qū)ο?1. JavaScript...
摘要:基于原型的面向?qū)ο笤诨谠偷恼Z言中如并不存在這種區(qū)別它只有對象不論是構(gòu)造函數(shù),實(shí)例,原型本身都是對象。允許動(dòng)態(tài)地向單個(gè)的對象或者整個(gè)對象集中添加或移除屬性。為了解決以上兩個(gè)問題,提供了構(gòu)造函數(shù)創(chuàng)建對象的方式。 showImg(https://segmentfault.com/img/remote/1460000013229218); 一. 重新認(rèn)識面向?qū)ο?1. JavaScript...
摘要:很多情況下,通常一個(gè)人類,即創(chuàng)建了一個(gè)具體的對象。對象就是數(shù)據(jù),對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍(lán)圖或原型。在中,對象通過對類的實(shí)體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實(shí)例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
閱讀 2767·2023-04-26 00:42
閱讀 2898·2021-09-24 10:34
閱讀 3926·2021-09-24 09:48
閱讀 4260·2021-09-03 10:28
閱讀 2662·2019-08-30 15:56
閱讀 2843·2019-08-30 15:55
閱讀 3339·2019-08-29 12:46
閱讀 2314·2019-08-28 17:52