深入學習JavaScript中的原型prototype
javascript 是一種 prototype based programming 的語言, 而與我們通常的 class based programming 有很大 的區(qū)別,我列舉重要的幾點如下:
1.函數(shù)是first class object, 也就是說函數(shù)與對象具有相同的語言地位
2.沒有類,只有對象
3.函數(shù)也是一種對象,所謂的函數(shù)對象
4.對象是按 引用 來傳遞的
那么這種 prototype based programming 的語言如何實現(xiàn)繼承呢(OO的一大基本要素), 這也便是 prototype 的由來.
看下面的代碼片斷:
function foo(a, b, c) { return a*b*c; } alert(foo.length); alert(typeof foo.constructor); alert(typeof foo.call); alert(typeof foo.apply); alert(typeof foo.prototype);
對于上面的代碼,用瀏覽器運行后你會發(fā)現(xiàn):
1.length: 提供的是函數(shù)的參數(shù)個數(shù)
2.prototype: 是一個object
3.其它三個都是function
而對于任何一個函數(shù)的聲明,它都將會具有上面所述的5個property(方法或者屬性).
下面我們主要看下prototype.
// prototype function Person(name, gender) { this.name = name; this.gender = gender; this.whoAreYou = function(){//這個也是所謂的closure, 內部函數(shù)可以訪問外部函數(shù)的變量 var res = "I'm " + this.name + " and I'm a " + this.gender +"."; return res; }; } // 那么在由Person創(chuàng)建的對象便具有了下面的幾個屬性 Person.prototype.age = 24; Person.prototype.getAge = function(){ return this.age; }; flag = true; if (flag) { var fun = new Person("Tower", "male"); alert(fun.name); alert(fun.gender); alert(fun.whoAreYou()); alert(fun.getAge()); } Person.prototype.salary = 10000; Person.prototype.getSalary = function(){ return this.name + " can earn about " + this.salary + "RMB each month." ; }; // 下面就是最神奇的地方, 我們改變了Person的prototype,而這個改變是在創(chuàng)建fun之后 // 而這個改變使得fun也具有了相同的屬性和方法 // 繼承的意味即此 if (flag) { alert(fun.getSalary()); alert(fun.constructor.prototype.age);//而這個相當于你直接調用 Person.prototype.age alert(Person.prototype.age); }
從上面的示例中我們可以發(fā)現(xiàn),對于prototype的方法或者屬性,我們可以 動態(tài)地 增加, 而由其創(chuàng)建的 對象自動會 繼承 相關的方法和屬性.
另外,每個對象都有一個 constructor 屬性,用于指向創(chuàng)建其的函數(shù)對象,如上例中的 fun.constructor 指向的 就是 Person.
那么一個疑問就自然產生了, 函數(shù)對象中自身聲明的方法和屬性與prototype聲明的對象有什么差別?
有下面幾個差別:
1.自身聲明的方法和屬性是 靜態(tài)的, 也就是說你在聲明后,試圖再去增加新的方法或者修改已有的方法,并不會 對由其創(chuàng)建的對象產生影響, 也即 繼承 失敗
2.而prototype可以動態(tài)地增加新的方法或者修改已有的方法, 從而是 動態(tài)的 ,一旦 父函數(shù)對象 聲明了相關 的prototype屬性,由其創(chuàng)建的對象會 自動繼承 這些prototype的屬性.
繼續(xù)上面的例子:
flag = true; // 函數(shù)內部聲明的方法是靜態(tài)的,無法傳遞的 Person.school = "ISCAS"; Person.whoAreYou = function(){ return "zhutao"; };//動態(tài)更改聲明期的方法,并不會影響由其創(chuàng)建的對象的方法, 即所謂的 靜態(tài) if (flag) { alert(Person.school); alert(fun.school);//輸出的是 "undefined" alert(Person.whoAreYou()); //輸出 zhutao alert(fun.whoAreYou()); // I'm Tower and I'm a male. } Person.prototype.getSalary = function(){ return "I can earn 1000000 USD"; }; if (flag) { alert(fun.getSalary());//已經繼承了改變, 即所謂的 動態(tài) }
既然有函數(shù)對象本身的屬性, 也有prototype的屬性, 那么是由其創(chuàng)建的對象是如何搜索相應的屬性的呢?
基本是按照下面的流程和順序來進行.
1.先去搜索函數(shù)對象本身的屬性,如果找到立即執(zhí)行
2.如果1沒有找到,則會去搜索prototype屬性,有2種結果,找到則直接執(zhí)行,否則繼續(xù)搜索 父對象 的 父對象 的prototype, 直至找到,或者到達 prototype chain 的結尾(結尾會是Object對象)
上面也回答如果函數(shù)對象本身的屬性與prototype屬性相同(重名)時的解決方式, 函數(shù)本身的對象 優(yōu)先 .
prototype 的典型示例
用過 jQuery 或者 Prototype 庫的朋友可能知道,這些庫中通常都會有 trim 這個方法。
示例:
String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); };
trim 用法:
' foo bar '.trim(); // 'foo bar'
但是這樣做又有一個缺點,因為比較新版本的瀏覽器中的 JavaScript 引擎在 String 對象中本身就提供了 trim 方法, 那么我們自己定義的 trim 就會覆寫它自帶的 trim。其實,我們在定義 trim 方法之前,可以做個簡單的檢測,看是否需要自己添加這個方法:
if(!String.prototype.trim) { String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); }; }
原型鏈
JavaScript 中定義或實例化任何一個對象的時候,它都會被附加一個名為 __proto__ 的隱藏屬性,原型鏈正是依靠這個屬性才得以形成。但是千萬別直接訪問 __proto__ 屬性,因為有些瀏覽器并不支持直接訪問它。另外 __proto__ 和 對象的 prototype 屬性也不是一回事,它們各自有各自的用途。
怎么理解呢?其實,當我們創(chuàng)建 myObject 函數(shù)時,實際上是創(chuàng)建了一個 Function 類型的對象:
console.log(typeof myObject); // function
這里要說明一下,F(xiàn)unction 是 JavaScript 中預定義的一個對象,所以它也有自己預定義的屬性(如 length 和 arguments)和方法(如 call 和 apply),當然也有 __proto__,以此實現(xiàn)原型鏈。也就是說,JavaScript 引擎內可能有類似如下的代碼片段:
Function.prototype = { arguments: null, length: 0, call: function() { // secret code }, apply: function(){ // secret code }, ... };
事實上,JavaScript 引擎代碼不可能這樣簡單,這里只是描述一下原型鏈是如何工作的。
我們定義了一個函數(shù) myObject,它還有一個參數(shù) name,但是并沒有給它任何其它屬性,例如 length 或者其它方法,如 call。那么下面這段代碼為啥能正常執(zhí)行呢?
console.log(myObject.length); // 結果:1,是參數(shù)的個數(shù)
這是因為我們定義 myObject 時,同時也給它定義了一個 __proto__ 屬性,并賦值為 Function.prototype(參考前面的代碼片段),所以我們能夠像訪問其它屬性一樣訪問 myObject.length,即使我們并沒有定義這個屬性,因為它會順著 __proto__ 原型鏈往上去找 length,最終在 Function 里面找到了。
那為什么找到的 length 屬性的值是 1,而不是 0 呢,是什么時候給它賦值的呢?由于 myObject 是 Function 的一個實例:
console.log(myObject instanceof Function); // true console.log(myObject === Function); // false
當實例化一個對象的時候,對象的 __proto__ 屬性會被賦值為其構造者的原型對象,在本示例中就是 Function,此時構造器回去計算參數(shù)的個數(shù),改變 length 的值。
console.log(myObject.__proto__ === Function.prototype); // true
而當我們用 new 關鍵字創(chuàng)建一個新的實例時,新對象的 __proto__ 將會被賦值為 myObject.prototype,因為現(xiàn)在的構造函數(shù)為 myObject,而非 Function。
var myInstance = new myObject('foo'); console.log(myInstance.__proto__ === myObject.prototype); // true
新對象除了能訪問 Function.prototype 中繼承下來的 call 和 apply 外,還能訪問從 myObject 中繼承下來的 getName 方法:
console.log(myInstance.getName()); // foo var mySecondInstance = new myObject('bar'); console.log(mySecondInstance.getName()); // bar console.log(myInstance.getName()); // foo
其實這相當于把原型對象當做一個藍本,然后可以根據(jù)這個藍本創(chuàng)建 N 個新的對象。
再看一個多重prototype鏈的例子:
// 多重prototype鏈的例子 function Employee(name) { this.name = ""; this.dept = "general"; this.gender = "unknown"; } function WorkerBee() { this.projects = []; this.hasCar = false; } WorkerBee.prototype = new Employee; // 第一層prototype鏈 function Engineer() { this.dept = "engineer"; //覆蓋了 "父對象" this.language = "javascript"; } Engineer.prototype = new WorkerBee; // 第二層prototype鏈 var jay = new Engineer("Jay"); if (flag) { alert(jay.dept); //engineer, 找到的是自己的屬性 alert(jay.hasCar); // false, 搜索到的是自己上一層的屬性 alert(jay.gender); // unknown, 搜索到的是自己上二層的屬性 }
上面這個示例的對象關系如下:
相關文章
為JS擴展Array.prototype.indexOf引發(fā)的問題及解決辦法
這篇文章主要介紹了為JS擴展Array.prototype.indexOf引發(fā)的問題及解決辦法,需要的朋友可以參考下2015-01-01FF(火狐)瀏覽器無法執(zhí)行window.close()解決方案
這篇文章主要介紹了FF(火狐)瀏覽器無法執(zhí)行window.close()解決方案,需要的朋友可以參考下2014-11-11簡介JavaScript中toUpperCase()方法的使用
這篇文章主要介紹了JavaScript中的toUpperCase()方法的使用,是JS入門學習中的基礎知識,需要的朋友可以參考下2015-06-06