摘要:基于原型的繼承誠惶誠恐的寫下這篇文章。無論是還是,都是面向對象的。將這個新對象作為構造函數的執行上下文其指向這個對象,并執行構造函數返回這個對象原型繼承我們來定義一個簡單的類和它的原型我們在原型上定義了一個方法。
源碼: 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
本文需要補充更多例子
本文存在批注,但該網站的Markdown編輯器不支持,所以無法正常展示,請到原文參考。
基于原型的JavaScript繼承誠惶誠恐的寫下這篇文章。用JavaScript實現繼承模型,已經是非常成熟的技術,各種大牛也已經寫過各式的經驗總結和最佳實踐。在這里,我只能就我所能,寫下我自己的思考和總結。
在閱讀之前,我們先假設幾個在面向對象編程中的概念是大家熟悉的:
類, Class
構造函數, Constructor
繼承, Inheritance
實例, Instance
實力化, Instantiation
方法, Method
多態, Polymorphism
接口, Interface
由于講解這些概念是十分復雜的,所以還請參閱其他資料。
了解原型面向對象是當代編程的主流思想。無論是C++還是Java,都是面向對象的。嚴格上來講,JavaScript并不是面向對象的,而是“基于對象的”(Object-based),因為它的確缺乏面向對象里的很多特性,例如:
繼承
接口
多態
...
但再另一方面,JavaScript是基于原型(Prototype)的對象系統。它的繼承體系,叫做原型鏈繼承。不同于繼承樹形式的經典對象系統,基于原型的對象系統中,對象的屬性和方法是從一個對象原型(或模板)上拷貝或代理(Delegation)的。JavaScript也不是唯一使用這種繼承方法的編程語言,其他的例子如:
Lisp
Lua
...
那么,prototype在哪里呢?
訪問構造函數的原型// 訪問Array的原型 Array.prototype
// 訪問自定義函數Foo的原型 var Foo = function() {} Foo.prototype訪問一個實例的原型
__proto__不是標準屬性,但是被大多數瀏覽器支持
var a = {} a.__proto__;
使用ES5的Object.getPrototypeOf:
Object.getPrototypeOf([]) === Array.prototype;
再來點繞彎的:
[].constructor.prototype === Array.prototypenew關鍵字
大多數面向對象語言,都有new關鍵字。他們大多和一個構造函數一起使用,能夠實例化一個類。JavaScript的new關鍵字是異曲同工的。
等等,不是說JavaScript不支持經典繼承么!的確,其實new的含義,在JavaScript中,嚴格意義上是有區別的。
當我們,執行
new F()
實際上是得到了一個從F.prototype繼承而來的一個對象。這個說法來自Douglas的很早之前的一篇文章1。在如今,如果要理解原型繼承中new的意義,還是這樣理解最好。
如果我們要描述new的工作流程,一個接近的可能流程如下:
分配一個空對象
設置相關屬性、方法,例如constructor和F.prototype上的各式方法、屬性。注意,這里執行的并不是拷貝,而是代理。后文會講解這點。
將這個新對象作為構造函數的執行上下文(其this指向這個對象),并執行構造函數
返回這個對象
原型繼承我們來定義一個簡單的“類”和它的原型:
var Foo = function() {}; Foo.prototype.bar = function() { console.log("haha"); }; Foo.prototype.foo = function() { console.log("foo"); };
我們在原型上定義了一個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"
注意到幾個要點:
在SuperFoo中,我們執行了父級構造函數
在SuperFoo中,我們讓然可以調用foo方法,即使SuperFoo上沒有定義這個方法。這是繼承的一種表現:我們可以訪問父類的方法
在SuperFoo中,我們重新定義了bar方法,實現了方法的重載
我們仔細想想第二點和第三點。我們新指定的bar方法到底保存到哪里了?foo方法是如何找到的?
原型鏈要回答上面的問題,必須要介紹原型鏈這個模型。相比樹狀結構的經典類型系統,原型繼承采取了另一種線性模型。
當我們要在對象上查找一個屬性或方法時:
在對象本身查找,如果沒有找到,進行下一步
在該對象的構造函數自己的prototype對象上查找,如果沒有找到進行下一步
獲取該對象的構造函數的prototype對象作為當前對象;如果當前對象存在prototype,就能繼續,否則不存在則查找失敗,退出;在該對象上查找,如果沒有找到,將前面提到的“當前對象”作為起始對象,重復步驟3
這樣的遞歸查找終究是有終點的,因為:
Object.prototype.__proto__ === null
也就是Object構造函數上,prototype這個對象的構造函數上已經沒有prototype了。
我們來看之前Foo和SuperFoo的例子,我們抽象出成員查找的流程如下:
superFoo本身 => SuperFoo.prototype => Foo.prototype => Object.prototype
解讀原型鏈的查找流程:
superFoo本身意味著superFoo這個實例有除了能夠從原型上獲取屬性和方法,本身也有存儲屬性、方法的能力。我們稱其為own property,我們也有不少相關的方法來操作:
obj.hasOwnProperty(name)
Object.getOwnPropertyNames(obj)
Object.getOwnPropertyDescriptor(obj)
SuperFoo.prototype:
回憶一下這句SuperFoo.prototype = new Foo();,也就是說SuperFoo.prototoye就是這個新創建的這個Foo類型的對象
這也就解釋了為啥我們能訪問到Foo.prototype上的方法和屬性了
也就是說,我們要在這個新建的Foo對象的本地屬性和方法中查找
Foo.prototype:
查找到這一次層,純粹是因為我們制定了SuperFoo.prototype的值,回想上一條
Object.prototype
這是該原型鏈的最后一環,因為Object.prototype這個對象的原型是null,我們無法繼續查找
這是JavaScript中所有對象的祖先,上面定義了一個簡單對象上存在的屬性和方法,例如toString
那么,當在SuperFoo上添加bar方法呢?這時,JavaScript引擎會在SuperFoo.prototype的本地添加bar這個方法。當你再次查找bar方法時,按照我們之前說明的流程,會優先找到這個新添加的方法,而不會找到再原型鏈更后面的Foo.prototype.bar。
也就是說,我們既沒有刪掉或改寫原來的bar方法,也沒有引入特殊的查找邏輯。
模擬更多的經典繼承基本到這里,繼承的大部分原理和行為都已經介紹完畢了。但是如何將這些看似簡陋的東西封裝成最簡單的、可重復使用的工具呢?本文的后半部分將一步一步來介紹如何編寫一個大體可用的對象系統。
熱身準備幾個小技巧,以便我們在后面使用。
beget如果要以一個對象作為原型,創建一個新對象:
function beget(o) { function F() {} F.prototype = o; return new F(); } var foo = beget({bar:"bar"}); foo.bar === "bar"; //true
理解這些應該困難。我們構造了一個臨時構造函數,讓它的prototype指向我們所期望的原型,然后返回這個構造函數所創建的實例。有一些細節:
我們不喜歡直接做A.prototype = B.prototype這樣的事情,因為你對子類的修改,有可能直接影響到父類以及父類的所有實例。大多數情況下這不是你想看到的結果
新建F的實例,創建了一個本地對象,可以持有(own)自身的屬性和方法,便可以支持之后的任意修改。回憶一下superFoo.bar方法。
如果你使用的JavaScript引擎支持Object.create,那么同樣的事情就更簡單:
Object.create({bar:"bar"});
要注意Object.create的區別:
我們可以創建沒有原型的對象: Object.create(null)
我們可以配置創建的對象,參閱Object.create的文檔2
我們不必去運行一遍父類構造函數,這樣可以避免不需要的副作用
函數的序列化、解義JavaScript的函數可以在運行時很方便的獲取其字符串表達:
var f = function(a) {console.log("a")}; f.toString(); // "function(a) {console.log("a")};"
這樣的能力其實時很強大的,你去問問Java和C++工程師該如何做到這點吧。
這意味著,我們可以去分析函數的字符串表達來做到:
了解函數的函數列表
了解函數體的實際內容
了解一個函數是否有別名
...
動態的thisJavaScript中的this是在運行時綁定的,我們往往需要用到這個特性,例如:
var A = function() {}; A.methodA = function() { console.log(this === A); }; A.methodA();// => true
以上這段代碼有如下細節:
A.methodA()運行時,其上下文對象指定的是A,所以this指向了A
我們可以用這個來模擬“類的靜態方法或類方法”
我們能夠通過這里的this引用到類(構造函數)本身
若干版本 最簡單版本單純實現一個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"
由于過于簡單,我就不做講解了。
更復雜的例子我們需要一個根對象XObject
根對象有各種繼承方法,并能傳入一些子類的方法和屬性
我們要復用上個例子里的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 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是我們對象系統的根類
XObject.extend可以接受一個包含屬性和方法的對象來定義子類
XObject的所有子類,都沒有定義構造函數邏輯的機會!真是難以接受的:
我們偏好一個類上的init方法來初始化對象,而將構造函數本身最簡化
繞開工廠方法的實現過程中,參數傳遞如何傳遞到構造函數的問題
可以支持更多新的特性,例如super屬性、mixin特性等
總結,然后呢?我們解決了一部分問題,又發現了一些新問題。但本文的主要內容在這里就結束了。一個更具實際意義的對象系統,實際隨處可見,Ember和Angular中的根類。他們都有更強大的功能,例如:
Ember中的binding,setter、getter
Angular中的函數依賴注入
...
但是,這些框架中對象系統的出發點都在本文所闡述的內容之中。如果作為教學,John Resig在2008年的一篇博客中3,總結了一個現代JavaScript框架中的對象系統的雛形。我創建了docco代碼注解來立即這段代碼,本文也會結束在這段代碼的注解。
還有一些更高級的話題和技巧,會在另外一篇文章中給出。
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/??
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/78139.html
摘要:首先,需要來理清一些基礎的計算機編程概念編程哲學與設計模式計算機編程理念源自于對現實抽象的哲學思考,面向對象編程是其一種思維方式,與它并駕齊驅的是另外兩種思路過程式和函數式編程。 JavaScript 中的原型機制一直以來都被眾多開發者(包括本人)低估甚至忽視了,這是因為絕大多數人沒有想要深刻理解這個機制的內涵,以及越來越多的開發者缺乏計算機編程相關的基礎知識。對于這樣的開發者來說 J...
摘要:基于原型的面向對象在基于原型的語言中如并不存在這種區別它只有對象不論是構造函數,實例,原型本身都是對象。允許動態地向單個的對象或者整個對象集中添加或移除屬性。為了解決以上兩個問題,提供了構造函數創建對象的方式。 showImg(https://segmentfault.com/img/remote/1460000013229218); 一. 重新認識面向對象 1. JavaScript...
摘要:基于原型的面向對象在基于原型的語言中如并不存在這種區別它只有對象不論是構造函數,實例,原型本身都是對象。允許動態地向單個的對象或者整個對象集中添加或移除屬性。為了解決以上兩個問題,提供了構造函數創建對象的方式。 showImg(https://segmentfault.com/img/remote/1460000013229218); 一. 重新認識面向對象 1. JavaScript...
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
閱讀 2650·2023-04-26 00:42
閱讀 2799·2021-09-24 10:34
閱讀 3810·2021-09-24 09:48
閱讀 4145·2021-09-03 10:28
閱讀 2576·2019-08-30 15:56
閱讀 2771·2019-08-30 15:55
閱讀 3254·2019-08-29 12:46
閱讀 2244·2019-08-28 17:52