javascript 是一種 prototype based programming 的語言, 而與我們通常的 class based programming 有很大 的區別,我列舉重要的幾點如下:
1.函數是first class object, 也就是說函數與物件有相同的語言地位
2.沒有類別,只有物件
3.函數也是一種物件,所謂的函數物件
4.物件是按 引用 來傳遞的
那麼這種 prototype based programming 的語言如何實現繼承呢(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);
對於上面的程式碼,用瀏覽器運行後你會發現:
1.length: 提供的是函數的參數個數
2.prototype: 是一個object
3.其它三個都是function
而對於任何一個函數的宣告,它將會具有上面所述的5個property(方法或屬性).
下面我們主要看下prototype.
// prototype function Person(name, gender) { this.name = name; this.gender = gender; this.whoAreYou = function(){//这个也是所谓的closure, 内部函数可以访问外部函数的变量 var res = "I'm " + this.name + " and I'm a " + this.gender +"."; return res; }; } // 那么在由Person创建的对象便具有了下面的几个属性 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,而这个改变是在创建fun之后 // 而这个改变使得fun也具有了相同的属性和方法 // 继承的意味即此 if (flag) { alert(fun.getSalary()); alert(fun.constructor.prototype.age);//而这个相当于你直接调用 Person.prototype.age alert(Person.prototype.age); }
從上面的範例我們可以發現,對於prototype的方法或屬性,我們可以 動態地 增加, 而由其建立的 物件自動會 繼承 相關的方法和屬性.
另外,每個物件都有一個 constructor 屬性,用來指向創建其的函數物件,如上例中的 fun.constructor 指向的 就是 Person.
那麼一個疑問就自然產生了, 函數對像中自身聲明的方法和屬性與prototype聲明的對像有什麼差別?
有下面幾個差異:
1.自身聲明的方法和屬性是靜態的, 也就是說你在聲明後,試圖再去增加新的方法或者修改已有的方法,並不會對由其創建的對象產生影響, 也即繼承失敗
2.而prototype可以動態地增加新的方法或修改已有的方法, 從而是 動態的 ,一旦 父函數物件 宣告了相關 的prototype屬性,由其建立的物件會 自動繼承 這些prototype的屬性.
繼續上面的例子:
flag = true; // 函数内部声明的方法是静态的,无法传递的 Person.school = "ISCAS"; Person.whoAreYou = function(){ return "zhutao"; };//动态更改声明期的方法,并不会影响由其创建的对象的方法, 即所谓的 静态 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());//已经继承了改变, 即所谓的 动态 }
既然有函數物件本身的屬性, 也有prototype的屬性, 那麼是由其建立的物件是如何搜尋對應的屬性的呢?
基本上是按照下面的流程和順序來進行.
1.先去搜尋函數物件本身的屬性,如果找到立即執行
2.如果1沒有找到,則會去搜尋prototype屬性,有2種結果,找到則直接執行,否則繼續搜尋父對象的父對象的prototype, 直至找到,或者到達prototype chain 的結尾(結尾會是Object對象)
上面也回答如果函數物件本身的屬性與prototype屬性相同(重名)時的解決方式, 函數本身的物件 優先 .
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 屬性也不是一回事,它們各自有各自的用途。
怎麼理解呢?其實,當我們建立 myObject 函數時,實際上是建立了一個 Function 類型的物件:
console.log(typeof myObject); // function
這裡要說明一下,Function 是JavaScript 中預先定義的一個對象,所以它也有自己預先定義的屬性(如length 和arguments)和方法(如call 和apply),當然也有__proto__,以此實現原型鏈。也就是說,JavaScript 引擎內可能有類似如下的程式碼片段:
Function.prototype = { arguments: null, length: 0, call: function() { // secret code }, apply: function(){ // secret code }, ... };
事實上,JavaScript 引擎程式碼不可能這麼簡單,這裡只是描述一下原型鍊是如何運作的。
我們定義了一個函數 myObject,它還有一個參數 name,但是並沒有給它任何其它屬性,例如 length 或其它方法,如 call。那麼下面這段程式碼為啥能正常執行呢?
console.log(myObject.length); // 结果:1,是参数的个数
这是因为我们定义 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,此时构造器回去计算参数的个数,改变 length 的值。
console.log(myObject.__proto__ === Function.prototype); // true
而当我们用 new 关键字创建一个新的实例时,新对象的 __proto__ 将会被赋值为 myObject.prototype,因为现在的构造函数为 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
其实这相当于把原型对象当做一个蓝本,然后可以根据这个蓝本创建 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, 搜索到的是自己上二层的属性 }
上面这个示例的对象关系如下: