js面向對象理解
ECMAScript 有兩種開發模式:1.函數式(過程化),2.面向對象(OOP)。面向對象的語言有一個標志,那就是類的概念,而通過類可以創建任意多個具有相同屬性和方法的對象。但是,ECMAScript 沒有類的概念,因此它的對象也與基於類的語言中的對象有所不同。
js(如果沒有作特殊說明,本文中的js僅包含ES5以內的內容)本身是沒有class類型的,但是每個函數都有一個prototype屬性。prototype指向一個對象,當函數作為構造函數時,prototype則起到類似class的作用。
一.創建對象
創建一個對象,然后給這個對象新建屬性和方法。
var box = new Object(); //創建一個Object 對象 box.name = 'Lee'; //創建一個name 屬性並賦值 box.age = 100; //創建一個age 屬性並賦值 box.run = function () { //創建一個run()方法並返回值 return this.name + this.age + '運行中...'; }; alert(box.run()); //輸出屬性和方法的值
為了解決多個類似對象聲明的問題,我們可以使用一種叫做工廠模式的方法,這種方法就是為了解決實例化對象產生大量重復的問題。
function createObject(name, age) { //集中實例化的函數 var obj = new Object(); obj.name = name; obj.age = age; obj.run = function () { return this.name + this.age + '運行中...'; }; return obj; } var box1 = createObject('Lee', 100); //第一個實例 var box2 = createObject('Jack', 200); //第二個實例 alert(box1.run()); alert(box2.run()); //保持獨立
工廠模式解決了重復實例化的問題,但是它有許多問題,創建不同對象其中屬性和方法都會重復建立,消耗內存;還有函數識別問題等等。
二.構造函數的方法
構造函數的方法有一些規范:
1)函數名和實例化構造名相同且大寫,(PS:非強制,但這么寫有助於區分構造函數和
普通函數);
2)通過構造函數創建對象,必須使用new 運算符。
function Box(name, age) { //構造函數模式 this.name = name; this.age = age; this.run = function () { return this.name + this.age + '運行中...'; }; } var box1 = new Box('Lee', 100); //new Box()即可 var box2 = new Box('Jack', 200); alert(box1.run()); alert(box1 instanceof Box); //很清晰的識別他從屬於Box
構造函數可以創建對象執行的過程:
1)當使用了構造函數,並且new 構造函數(),那么就后台執行了new Object();
2)將構造函數的作用域給新對象,(即new Object()創建出的對象),而函數體內的this 就
代表new Object()出來的對象。
3)執行構造函數內的代碼;
4)返回新對象(后台直接返回)。
注:
1)構造函數和普通函數的唯一區別,就是他們調用的方式不同。只不過,構造函數也是函數,必須用new 運算符來調用,否則就是普通函數。
2)this就是代表當前作用域對象的引用。如果在全局范圍this 就代表window 對象,如果在構造函數體內,就代表當前的構造函數所聲明的對象。
這種方法解決了函數識別問題,但消耗內存問題沒有解決。同時又帶來了一個新的問題,全局中的this 在對象調用的時候是Box 本身,而當作普通函數調用的時候,this 又代表window。即this作用域的問題。
三.原型
我們創建的每個函數都有一個prototype(原型)屬性,這個屬性是一個對象,它的用途是包含可以由特定類型的所有實例共享的屬性和方法。邏輯上可以這么理解:prototype 通過調用構造函數而創建的那個對象的原型對象。使用原型的好處可以讓所有對象實例共享它所包含的屬性和方法。也就是說,不必在構造函數中定義對象信息,而是可以直接將這些信息添加到原型中。
function Box() {} //聲明一個構造函數 Box.prototype.name = 'Lee'; //在原型里添加屬性 Box.prototype.age = 100; Box.prototype.run = function () { //在原型里添加方法 return this.name + this.age + '運行中...'; };
構造函數的聲明方式和原型模式的聲明方式存儲情況如下:
所以,它解決了消耗內存問題。當然它也可以解決this作用域等問題。
我們經常把屬性(一些在實例化對象時屬性值改變的),定義在構造函數內;把公用的方法添加在原型上面,也就是混合方式構造對象(構造方法+原型方式):
var person = function(name){ this.name = name }; person.prototype.getName = function(){ return this.name; } var zjh = new person(‘zhangjiahao’); zjh.getName(); //zhangjiahao
下面詳細介紹原型:
1.原型對象
每個javascript對象都有一個原型對象,這個對象在不同的解釋器下的實現不同。比如在firefox下,每個對象都有一個隱藏的__proto__屬性,這個屬性就是“原型對象”的引用。
2.原型鏈
由於原型對象本身也是對象,根據上邊的定義,它也有自己的原型,而它自己的原型對象又可以有自己的原型,這樣就組成了一條鏈,這個就是原型鏈,JavaScritp引擎在訪問對象的屬性時,如果在對象本身中沒有找到,則會去原型鏈中查找,如果找到,直接返回值,如果整個鏈都遍歷且沒有找到屬性,則返回undefined.原型鏈一般實現為一個鏈表,這樣就可以按照一定的順序來查找。
1)__proto__和prototype
JS在創建對象(不論是普通對象還是函數對象)的時候,都有一個叫做__proto__的內置屬性,用於指向創建它的函數對象的原型對象prototype。以上面的例子為例:
console.log(zjh.__proto__ === person.prototype) //true
同樣,person.prototype對象也有__proto__屬性,它指向創建它的函數對象(Object)的prototype
console.log(person.prototype.__proto__ === Object.prototype) //true
繼續,Object.prototype對象也有__proto__屬性,但它比較特殊,為null
console.log(Object.prototype.__proto__) //null
我們把這個有__proto__串起來的直到Object.prototype.__proto__為null的鏈叫做原型鏈。如下圖:
2)constructor
原型對象prototype中都有個預定義的constructor屬性,用來引用它的函數對象。這是一種循環引用
person.prototype.constructor === person //true Function.prototype.constructor === Function //true Object.prototype.constructor === Object //true
3)為加深對理解,我們再舉一個例子:
function Task(id){ this.id = id; } Task.prototype.status = "STOPPED"; Task.prototype.execute = function(args){ return "execute task_"+this.id+"["+this.status+"]:"+args; } var task1 = new Task(1); var task2 = new Task(2); task1.status = "ACTIVE"; task2.status = "STARTING"; print(task1.execute("task1")); print(task2.execute("task2"));
execute task_1[ACTIVE]:task1
execute task_2[STARTING]:task2
構造器會自動為task1,task2兩個對象設置原型對象Task.prototype,這個對象被Task(在此最為構造器)的prototype屬性引用,參看下圖中的箭頭指向。
由於Task本身仍舊是函數,因此其”__proto__”屬性為Function.prototype, 而內建的函數原型對象的”__proto__”屬性則為Object.prototype對象。最后Obejct.prototype的”__proto__”值為null。
總結:
實例對象的__proto__指向,其構造函數的原型;構造函數原型的constructor指向對應的構造函數。構造函數的prototype獲得構造函數的原型。
有時某種原因constructor指向有問題,可以通過
constructor:構造函數名;//constructor : Task
重新指向。
四.繼承
繼承是面向對象中一個比較核心的概念。其他正統面向對象語言都會用兩種方式實現繼承:一個是接口實現,一個是繼承。而ECMAScript 只支持繼承,不支持接口實現,而實現繼承的方式依靠原型鏈完成。
在JavaScript 里,被繼承的函數稱為超類型(父類,基類也行,其他語言叫法),繼承的函數稱為子類型(子類,派生類)
1.call+遍歷
屬性使用對象冒充(call)(實質上是改變了this指針的指向)繼承基類,方法用遍歷基類原型。
function A() { this.abc=12; } A.prototype.show=function () { alert(this.abc); }; //繼承A function B() { //繼承屬性;this->new B() A.call(this); //有參數可以傳參數A.call(this,name,age) } //繼承方法;B.prototype=A.prototype; for(var i in A.prototype) { B.prototype[i]=A.prototype[i]; } //添加自己的方法 B.prototype.fn=function () { alert('abc'); }; var objB=new B(); var objA=new A();objB.show();
可以實現多繼承。
2.寄生組合繼承
主要是Desk.prototype = new Box(); Desk 繼承了Box,通過原型,形成鏈條。主要通過臨時中轉函數和寄生函數實現。
臨時中轉函數:基於已有的對象創建新對象,同時還不必因此創建自定義類型
寄生函數:目的是為了封裝創建對象的過程
//臨時中轉函數 function obj(o) { //o表示將要傳遞進入的一個對象 function F() {} //F構造是一個臨時新建的對象,用來存儲傳遞過來的對象 F.prototype = o; //將o對象實例賦值給F構造的原型對象 return new F(); //最后返回這個得到傳遞過來對象的對象實例 } //寄生函數 function create(box, desk) { var f = obj(box.prototype); f.constructor = desk; //調整原型構造指針 desk.prototype = f; } function Box(name) { this.name = name; this.arr = ['apple','pear','orange']; } Box.prototype.run = function () { return this.name; }; function Desk(name, age) { Box.call(this, name); this.age = age; } //通過寄生組合繼承實現繼承 create(Box, Desk); //這句話用來替代Desk.prototype = new Box(); var desk = new Desk('Lee',100); desk.arr.push('peach'); alert(desk.arr); alert(desk.run());
臨時中轉函數和寄生函數主要做的工作流程:
臨時中轉函數:返回的是基類的實例對象函數
寄生函數:將返回的基類的實例對象函數的constructor指向派生類,派生類的prototype指向基類的實例對象函數(是一個函數原型),從而實現繼承。
-------------------------------------------------------------------------------------------------------------------------------------
完
轉載需注明轉載字樣,標注原作者和原博文地址。
更多閱讀: