摘要:其工作原理我已經在第一篇做了大部分的闡述我尚未提及的是在創建新對象的時候,會賦予新對象一個屬性指向構造器的屬性。
第四篇拖了很久了,真是有點不好意思。實話實說,拖延很久的原因主要是沒想好怎么寫,因為這一篇的主題比較有挑戰性:原型和基于原型的繼承——啊~我終于說出口了,這下沒借口拖延了==
原型我(個人)不喜歡的,就是講原型時上來就拿類做比較的,所以我不會這樣講。不過我的確講過構造器函數,在這方面和類多多少少有共通之處。我的建議是:忘掉類。有很多觀點認為“類”學的泛濫是面向對象的過度發展,是一種悲哀,以至于有太多的開發者幾乎把面向對象和類劃上了等號。在學習原型之前,我請你先記住并品味這句話:
prototype 和 __proto__</>復制代碼
面向對象設計的精髓在于“抽象”二字,類是實現實體抽象的一種手段,但不是唯一一種。
事先聲明:永遠,永遠不要在真實的代碼里使用 __proto__ 屬性,在本文里用它純粹是用于研究!很快我們會講到它的替代品,抱歉請忍耐。
在 JavaScript 里,函數是對象(等學完了這一篇,不妨研究一下函數究竟是怎么就成了對象的?),對象嘛,毫無意外的就會有屬性(方法也是屬性),然后毫無意外的 prototype 就是函數的一個屬性,最后毫無意外的 prototype 屬性也是一個對象。瞧,多么順理成章的事情:
</>復制代碼
function foo() {}
foo.prototype; // 里面有啥自己去看
好吧,那 prototype 有啥用?呃,如果你把函數就當做函數來用,那它壓根沒用。不過,若你把函數當作構造器來用的話,新生成的對象就可以直接訪問到 prototype 對象里的屬性。
</>復制代碼
// 要充當構造器了,按慣例把首字母大寫
function Foo() {}
var f = new Foo();
f.constructor; // function Foo() {}
想一下,f 的 constructor 屬性哪里來的?如果你想不明白,請用 console.dir(Foo.prototype) 一探究竟。
這說明了一個問題:
</>復制代碼
函數的原型屬性不是給函數自己用的,而是給用函數充當構造器創建的對象使用的。
令人疑惑的是,prototype 屬性存在于 Foo 函數對象內,那么由 Foo 創建的實例對象 f 是怎么訪問到 prototype 的呢?是通過復制 prototype 對象嗎?接著上面的代碼我們繼續來看:
</>復制代碼
f.__proto__; // Foo {}
Foo.prototype; // Foo {}
f.__proto__ === Foo.prototype; // true
哦~不是復制過來的,而是一個叫做 __proto__ 的屬性指向了構造器的 prototype 對象呀。
沒錯!這就是原型機制的精髓所在,讓我們來總結一下所有的細節(包括隱含在表象之下的):
函數擁有 prototype 屬性,但是函數自己不用它
函數充當構造器的時候可以創建出新的對象,這需要 new 操作符的配合。其工作原理我已經在第一篇做了大部分的闡述
我尚未提及的是:new 在創建新對象的時候,會賦予新對象一個屬性指向構造器的 prototype 屬性。這個新的屬性在某些瀏覽器環境內叫做 __proto__
當訪問一個對象的屬性(包括方法)時,首先查找這個對象自身有沒有該屬性,如果沒有就查找它的原型(也就是 __proto__ 指向的 prototype 對象),如果還沒有就查找原型的原型(prototype 也有它自己的 __proto__,指向更上一級的 prototype 對象),依此類推一直找到 Object 為止
OK,上面的第四點事實上就是 JavaScript 的對象屬性查找機制。由此可見:
</>復制代碼
原型的意義就在于為對象的屬性查找機制提供一個方向,或者說一條路線
一個對象,它有許多屬性,其中有一個屬性指向了另外一個對象的原型屬性;而后者也有一個屬性指向了再另外一個對象的原型屬性。這就像一條一環套一環的鎖鏈一樣,并且從這條鎖鏈的任何一點尋找下去,最后都能找到鏈條的起點,即 Object;因此,我們也把這種機制稱作:原型鏈。
現在,我希望統一一下所使用的術語(至少在本文范圍內):
函數的 prototype 屬性:我們叫它 原型屬性 或 原型對象
對象的 __proto__ 屬性:我們叫它 原型
例如:
Foo 的原型屬性(或原型對象) = Foo.prototype
f 的原型 = f.__proto__
統一術語的原因在于,盡管 Foo.prototype 和 f.__proto__ 是等價的,但是 prototype 和 __proto__ 并不一樣。當考慮一個固定的對象時,它的 prototype 是給原型鏈的下方使用的,而它的 __proto__ 則指向了原型鏈的上方;因此,一旦我們說“原型屬性”或者“原型對象”,那么就暗示著這是給它的子子孫孫們用的,而說“原型”則是暗示這是從它的父輩繼承過來的。
再換一種說法:對象的原型屬性或原型對象不是給自己用的,而對象的原型是可以直接使用的。
__proto__ 的問題既然 __proto__ 可以訪問到對象的原型,那么為什么禁止在實際中使用呢?
這是一個設計上的失誤,導致 __proto__ 屬性是可以被修改的,同時意味著 JavaScript 的屬性查找機制會因此而“癱瘓”,所以強烈的不建議使用它。
如果你確實要通過一個對象訪問其原型,ES5 提供了一個新方法:
</>復制代碼
Object.getPrototypeOf(f) // Foo {}
這是安全的,盡管放心使用。考慮到低版本瀏覽器的兼容性問題,可以使用 es5-shim
自有屬性和原型屬性的區別由于對象的原型是一個引用而不是賦值,所以更改原型的屬性會立刻作用于所有的實例對象。這一特性非常適用于為對象定義實例方法:
</>復制代碼
function Person(name) {
this.name = name;
}
Person.prototype.greeting = function () {
return "你好,我叫" + this.name;
};
var p1 = new Person("張三");
var p2 = new Person("李四");
p1.greeting(); // 你好,我叫張三
p2.greeting(); // 你好,我叫李四
/* 改變實例方法的行為:*/
Person.prototype.greeting = function () {
return "你好,我叫" + this.name + ",很高興認識你!";
};
/* 觀察其影響:*/
p1.greeting(); // 你好,我叫張三,很高興認識你!
p2.greeting(); // 你好,我叫李四,很高興認識你!
然而,改變自有屬性則不同,它只會對新創建的實例對象產生影響,接上例:
</>復制代碼
function Person(name) {
this.name = "超人";
}
/* 不影響已存在的實例對象 */
p1.greeting(); // 你好,我叫張三,很高興認識你!
/* 只影響新創建的實例對象 */
var p3 = new Person("王五");
p3.greeting(); // 你好,我叫超人,很高興認識你!
這個例子看起來有點無厘頭,沒啥大用,不過它的精神在于:在現實世界中,復雜對象的行為或許會根據情況對其進行重寫,但是我們不希望改變對象的內部狀態;或者,我們會實現繼承,去覆蓋父級對象的某些行為而不引向其他相同的部分。在這些情況下,原型會給予我們最大程度的靈活性。
我們如何知道屬性是自有的還是來自于原型的?上代碼~
</>復制代碼
p1.hasOwnProperty("name"); // true
p1.hasOwnProperty("greeting"); // false
p1.constructor.prototype.hasOwnProperty("greeting"); // true
Object.getPrototypeOf(p1).hasOwnProperty("greeting"); // true
代碼很簡單,就不用過度解釋了,注意最后兩句實際上等價的寫法。
小心 constructor剛才的這一句代碼:p1.constructor.prototype.hasOwnProperty("greeting");,其實暗含了一個有趣的問題。
對象 p1 能夠訪問自己的構造器,這要謝謝原型為它提供了 constructor 屬性。接著通過 constructor 屬性又可以反過來訪問到原型對象,這似乎是一個圈圈,我們來試驗一下:
</>復制代碼
p1.constructor === p1.constructor.prototype.constructor; // true
p1.constructor === p1.constructor.prototype.constructor.prototype.constructor; // true
還真是!不過我們不是因為好玩才研究這個的。
盡管我們說:更改原型對象的屬性會立即作用于所有的實例對象,但是如果你完全覆蓋了原型對象,事情就變得詭異起來了:(閱讀接下來的例子,請一句一句驗證自己心中所想)
</>復制代碼
function Person(name) {
this.name = name;
}
var p1 = new Person("張三");
Person.prototype.greeting = function () {
return "你好,我叫" + this.name;
};
p1.name; // 張三
p1.greeting(); // 你好,我叫張三
p1.constructor === Person; // true
/* so far so good, but... */
Person.prototype = {
say: function () {
return "你好,我叫" + this.name;
}
};
p1.say(); // TypeError: Object # has no method "say"
p1.constructor.prototype; // Object { say: function }
呃?Person 的原型屬性里明明有 say 方法呀?原型對象不是即時生效的嗎?
原型繼承若是只為了創建一種對象,原型的作用就無法全部發揮出來。我們會進一步利用原型和原型鏈的特性來拓展我們的代碼,實現基于原型的繼承。
原型繼承是一個非常大的話題范圍,慢慢地你會發現,盡管原型繼承看起來沒有類繼承那么的規整(相對而言),但是它卻更加靈活。無論是單繼承還是多繼承,甚至是 Mixin 及其他連名字都說不上來的繼承方式,原型繼承都有辦法實現,并且往往不止一種辦法。
不過讓我們先從簡單的開始:
</>復制代碼
function Person() {
this.klass = "人類";
}
Person.prototype.toString = function () {
return this.klass;
};
Person.prototype.greeting = function () {
return "大家好,我叫" + this.name + ", 我是一名" + this.toString() + "。";
};
function Programmer(name) {
this.name = name;
this.klass = "程序員";
}
Programmer.prototype = new Person();
Programmer.prototype.constructor = Programmer;
這是一個非常好的例子,它向我們揭示了以下要點:
</>復制代碼
var someone = new Programmer("張三");
someone.name; // 張三
someone.toString(); // 程序員
someone.greeting(); // ?大家好,我叫張三, 我是一名程序員。
我來捋一遍:
倒數第二行,new Person() 創建了對象,然后賦給了 Programmer.prototype 于是構造器的原型屬性就變成了 Person 的實例對象。
因為 Person 對象擁有重寫過的 toString() 方法,并且這個方法返回的是宿主對象的 klass 屬性,所以我們可以給 Programmer 定義一個 greeting() 方法,并在其中使用繼承而來的 toString()。
當 someone 對象調用 toString() 方法的時候,this 指向的是它自己,所以能夠輸出 程序員 而不是 人類。
還沒完,繼續看:
</>復制代碼
// 因為 Programmer.prototype.constructor = Programmer; 我們才能得到:
someone.constructor === Programmer; ?// true
// 這些結果體現了何謂“鏈式”原型繼承
??someone instanceof Programmer; ?// true
??someone instanceof Person; //? true
??someone instanceof Object; ?// true
方法重載
上例其實已經實現了對 toString() 方法的重載(這個方法的始祖對象是 Object.prototype),秉承同樣的精神,我們自己寫的子構造器同樣可以通過原型屬性來重載父構造器提供的方法:
</>復制代碼
Programmer.prototype.toString = function () {
return this.klass + "(碼農)";
}
var codingFarmer = new Programmer("張三");
codingFarmer.greeting(); // 大家好,我叫張三, 我是一名程序員(碼農)。
屬性查找與方法重載的矛盾
思維活躍反應快的同學或許已經在想了:
</>復制代碼
為什么一定要把父類的實例賦給子類的原型屬性,而不是直接用父類的原型屬性呢?
好問題!這個想法非常有道理,而且這么一來我們還可以減少屬性查找的次數,因為向上查找的時候跳過了父類實例的 __proto__,直接找到了(如上例)Person.prototype。
然而不這么做的理由也很簡單,如果你這么做了:
</>復制代碼
Programmer.prototype = Person.prototype;
由于 Javascript 是引用賦值,因此等號兩端的兩個屬性等于指向了同一個對象,那么一旦你在子類對方法進行重載,連帶著父類的方法也一起變化了,這就失去了重載的意義。因此只有在確定不需要重載的時候才可以這么做。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87462.html
摘要:函數式編程前端掘金引言面向對象編程一直以來都是中的主導范式。函數式編程是一種強調減少對程序外部狀態產生改變的方式。 JavaScript 函數式編程 - 前端 - 掘金引言 面向對象編程一直以來都是JavaScript中的主導范式。JavaScript作為一門多范式編程語言,然而,近幾年,函數式編程越來越多得受到開發者的青睞。函數式編程是一種強調減少對程序外部狀態產生改變的方式。因此,...
摘要:目錄導語包裝對象的理解三大包裝對象的知識點小結導語包裝對象是為了彌補基本數據類型的非對象特性而產生的,對于基本類型值而言,本來是不存在屬性和方法的,但是我們可以在使用字面量創建字符串時,調用例如的方法,那么其內在原理究竟是什么呢閱讀完本篇文 目錄 導語 1. 包裝對象的理解 2. 三大包裝對象的知識點 3. 小結 導語 包裝對象是為了彌補基本數據類型的非對象特性而產生的,對于基本類型...
摘要:但是它們其實并不是二選一的關系并不是只能用或者。正因為如此,指令有時也被稱為虛擬指令。這是因為是采用基于棧的虛擬機的機制。聲明模塊的全局變量。。下文預告現在你已經了解了模塊的工作原理,下面將會介紹為什么運行的更快。 作者:Lin Clark 編譯:胡子大哈 翻譯原文:http://huziketang.com/blog/posts/detail?postId=58c77641a6d8...
摘要:之前一篇文章我們詳細說明了變量對象,而這里,我們將詳細說明作用域鏈。而的作用域鏈,則同時包含了這三個變量對象,所以的執行上下文可如下表示。下圖展示了閉包的作用域鏈。其中為當前的函數調用棧,為當前正在被執行的函數的作用域鏈,為當前的局部變量。 showImg(https://segmentfault.com/img/remote/1460000008329355);初學JavaScrip...
摘要:首先來講講阮一峰的文章中的兩道思考題。環境記錄包含包含了函數內部聲明的局部變量和參數變量,外部引用指向了外部函數對象的上下文執行場景。 本文最主要講講JavaScript閉包和this綁定相關的我的小發現,鑒于這方面的基礎知識已經有很多很好的文章講過了,所以基本的就不講了,推薦看看酷殼上的理解Javascript的閉包和阮一峰的學習Javascript閉包(Closure),寫的都非常...
閱讀 2820·2021-10-08 10:04
閱讀 3265·2021-09-10 11:20
閱讀 530·2019-08-30 10:54
閱讀 3319·2019-08-29 17:25
閱讀 2306·2019-08-29 16:24
閱讀 894·2019-08-29 12:26
閱讀 1451·2019-08-23 18:35
閱讀 1937·2019-08-23 17:53