javascript 創建對象——類,繼承


注:

a.本文所有代碼在chrome瀏覽器下測試通過,建議讀者也安裝一個chrome瀏覽器;

b.本文所述遵循ECMA-262第五版規範

c.本文輸出使用console.log函數,請按F12進入調試模式,觀看控制檯輸出;

d.源碼鏈接地址

e.轉載請註明出處.


1.什麼是對象?

javascript本身,是沒有類的概念的,只有對象的概念,除了基本類型(string,number,boolean,null,undefined)外,其餘均是對象,就連function也是對象.
那麼,什麼是對象?!javascript中的對象,類似於一組鍵值對的集合.你甚至可以以鍵值對的方式來操作javascript中的對象,就像這樣:

var myDog= new Object();
myDog["name"] = "Odie";
myDog["color"] = "Yellow";
console.log(myDog ["name"] );
console.log(myDog ["color"] );


跟這種方式創建對象的效果是一樣的:

var myDog = new Object();
myDog .name = "Odie";
myDog .color = "Yellow";
console.log(myDog.name );
console.log(myDog.color );


顯然,第二種訪問方式更加方便.通常只有在並不確定我們訪問的對象的屬性名字的時候(比如json數據),纔會使用這種方式訪問.當然,如果你想起一個類似"hello world"(有空格)這樣奇葩的屬性名字,那你就必須使用鍵值對的方式創建對象啦.


2.如何創建對象?


exampleA:


//字面量方式創建對象,簡單方便,可以認爲,這是exampleB方式的簡寫
var myDog= {name: "Odie", color: "Yellow"};

exampleB:
//使用new操作符創建對象,再追加屬性
var myDog= new Object();
myDog.name = "Odie";
myDog.color = "Yellow";

在js中,如果你賦值操作的屬性沒有,就會創建一個,1話題中就已經用過這種方式了.exampleA中自然也可以這樣繼續追加屬性,A和B最大的區別在於創建對象的方式(主要區別在第一行),而不在於追加屬性上.值得注意是隻有在賦值操作時,纔會這樣,你做一個訪問的操作,它自然是不會創建的(記住這一點,在後面的話題中很重要).
簡單驗證一下:
var myDog= {name: "Odie", color: "Yellow"};
console.log(myDog.age);//undefined
for(var pro in myDog){console.log(pro);}//name,color,沒有age

exampleC:

//一個很少使用的方式
var myDog= Object.create(new Object());
myDog.name = "Odie";
myDog.color = "Yellow";

這種方式着實很少用,它有什麼用途在這裏不好講,留到後面吧.
其實A、B、C三種方式創建對象還是有差別的,在這裏也不好說,也留在後面吧.

exampleD:


//如果是簡單的對象,似乎exampleA是最方便的,但當你要創建複雜的對象,亦或是大量類似的對象時,就應當考慮下面這個方法了.

function Dog(name, color) {

this.name = name;
this.color = color;
}
var myDog= new Dog("Odie", "Yellow");

等等,爲什麼new了一個function呢?!學過其他OO語言的人都會覺的怪怪的.之前說過,javascript根本就沒有類這個概念,我們輸出一下瀏覽器內置的Object和Date:

console.log(Object); 
console.log(Date);

在chrome中的結果:
function Object() { [native code] }
function Date() { [native code] }
在firefox中的結果:
[object Function]
[object Function]

真相了,Object和Date也不是所謂的類,而是function!在javascript中,就是使用"new function(參數)"這種方式創建對象的.我們稱這種function爲構造函數,exampleB和exampleD是同一種方式,只是在exampleD中,使用了自己定義的構造函數.

那麼在 "new 構造函數(參數)" 的過程中,都發生了什麼呢?!

1.創建一個新對象//對象的隱性引用"__proto__"指向構造函數的"prototype"(先忽略後面這半句);
2.將構造函數的 "this" 指向新對象;
3.執行構造函數(初始化,向新對象添加屬性);
4.返回這個對象.

經過以上步驟,一個新的對象就創建出來了.


構造函數與其他函數有什麼區別麼?!
唯一區別是你在調用構造函數上需要使用new關鍵字,但在語法上並沒有區別,所有的函數都有prototype和this引用,構造函數只是定義的時候通常首字母大寫,調用的時候記得使用new關鍵字,但這都靠自覺偷笑,javascript本身沒有語法強制規定什麼樣的函數纔是構造函數.你不用new關鍵字調用構造函數(非strict mode)亦或你new一個普通的函數也不會報錯,當然,結果自然不是你想要的(後面會講會發生什麼).

this是什麼東西?!

this是function內部的一個引用,跟其他OO語言類似,它指向函數據以執行的對象.所有的函數都有這麼一個引用.

什麼叫據以執行的對象?!

不太好形容這個東西,大概可以理解爲,這個函數運行在哪個對象之下(還是不大好理解),直接舉例吧.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//輸出自己的this
function log_this() {
    console.log(this);
}
 
//初始化
window.onload = function() {
     
    //example_one
    log_division("example_one");
    log_this();//window對象
    //example_two假設你有一個btn按鈕   
    document.getElementById("btn").onclick = log_this;//點擊-->dom對象
    //example_three
    log_division("example_three");
    var temp = {};//exampleA中創建對象的方法哦
    temp.logThis = log_this;//function也是對象,可以這麼給對象添加function屬性,上面那個onclick監聽回調函數的原理,跟這個類似
    temp.logThis();//Object   
}

這回好理解了吧,this指向哪裏,是根據運行環境有關的,this指向它的運行環境對象,one例子當中,它就指向了window,two中就指向了dom對象,three中就指向了自定義對象.因此,如果你不用new調用構造函數,就會將屬性添加到window上.//題外話,在strict mode下,這麼做會報錯的.

如何定義對象的函數呢?!

你可以這樣:

?
1
2
3
4
5
6
7
function Dog(name, color) {
    this.name = name;
    this.color = color;
    this.sayName = function(){
        console.log(this.name);
    };
}

前文說過,function也是對象,這就相當於在執行構造函數的過程中,給對象增加了一個類型爲function的sayName屬性,但這樣做有一個問題,就是每次執行構造函數的過程中,都會創建一個新的函數對象,顯然,所有的Dog對象共享一個sayName函數就可以了.按照目前所講的知識,你自然可以這樣:


?
1
2
3
4
5
6
7
8
function Dog(name, color) {
    this.name = name;
    this.color = color;
    this.sayName = sayName;
}
function sayName() {
    console.log(this.name);
}

在這裏,定義了一個sayName的全局函數,這樣,每次執行構造函數時,就不會重新創建一個新的sayName函數了.但這樣做,想給對象定義多個函數,就要定義多個的全局函數.而當你想要定義多種對象時,全局函數的數量就會爆炸,變的讓你無法掌控.


3.prototype!!!!!


幸好,可以使用函數的prototype這個對象,看下面的代碼:

?
1
2
3
4
5
6
7
8
9
function Dog(name, color) {
    this.name = name;
    this.color = color;
}
Dog.prototype.sayName = function() {
    console.log(this.name);
};
var myDog = new Dog("Odie", "Yellow");
myDog.sayName();//Odie

再次強調,function也是對象(除了基本類型都是對象).在javascript中,每個function都有一個prototype對象.那麼,"Dog.prototype.sayName = function(){...};"這種寫法就是給Dog的ptototype增加一個名字爲sayName的function對象.

咦?!,在上述的代碼中,新對象myDog是如何指向了Dog的prototype的sayName函數(有點繞)呢?!

還記得"new 構造函數(參數)" 的過程中,都發生了什麼麼?!
1.創建一個新對象,對象的隱性引用"__proto__"指向構造函數的"prototype";
2.將構造函數的 "this" 指向新對象;
3.執行構造函數(初始化,向新對象添加屬性);
4.返回這個對象.


下面對前面忽略的後半句進行解釋.

每一個對象都有一個指向它的構造函數的prototype的隱性的引用(在構造的時候被賦值).

什麼是隱性引用,就是你無法看到的,你無法使用myDog.prototype這樣的方式獲取prototype(在firefox和chrome中可以通過myDog.__proto__來訪問).但是你卻可以使用它的屬性!所以你可以使用"myDog.sayName();"這種方式調用構造函數Dog的prototype的sayName函數.


如果對象的屬性和對象的構造函數的prototype的屬性重名了呢?!

自然是先訪問對象本身的屬性了.每當訪問一個對象的屬性時,先從對象自身查找,如果它自己沒有,再查找它的__proto__指向的prototype對象有沒有這個屬性.

如果還沒有呢?!

不會停止! 別忘了,prototype也是對象,它也有一個__proto__隱性引用,它會繼續根據這個引用查找下去...查找下去...查找下去...查找下去,直到這個__proto__引用指向null爲止.
在上面的例子中,很快就指向了null了.

var myDog = new Dog("Odie", "Yellow");
console.log(myDog.__proto__);
console.log(myDog.__proto__.__proto__);
console.log(myDog.__proto__.__proto__.__proto__);

在chrome中的結果:
Dog {say: function}//還有更詳細的內容
Object {}
null
在firefox中的結果:
[object Object]
[object Object]
null

正是javascript這種鏈式查找的機制,使"繼承"成爲了可能(此處Dog繼承Object).這個在後面講.

javascript中的prototype實現了對象的共享機制,由同一個構造函數創造出來的對象,都有一個指向構造函數的prototype的隱性指針,再通過javascript訪問屬性的查找機制,就實現了共享.這類似於其他OO語言中的類的靜態變量和靜態函數.既然通過prototype共享function,自然也可以共享屬性.


?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Dog(name, color) {
    this.name = name;
    this.color = color;
}
 
Dog.prototype.sayName = function() {
    console.log(this.name);
};
 
Dog.prototype.kind = "Dog";
 
var myDog = new Dog("Odie", "Yellow");
var youDog = new Dog("Oalive", "Black");
 
//通過對象讀取
console.log(myDog.kind);//Dog
console.log(youDog.kind);//Dog
 
//但不能通過對象修改prototype屬性
myDog.kind = "cat";
console.log(myDog.kind);//cat
console.log(youDog.kind);//dog


上面這段代碼,定義了一個prototype的kind屬性,可以看到,可以直接通過對象訪問該屬性,卻不可以通過對象進行修改.

爲什麼呢?!

前面講過,每個對象都有一個指向prototype的隱性引用,這個引用是你摸不到的,"__proto__"這種寫法只是chrome和firefox的瀏覽器支持,它並不是javascript標準(退一步說,即使是標準,你修改的方式也該是myDog1.__proto__.kind = "dog").你之所以能共通過對象訪問到全局屬性,是由於javascript查找機制決定的,在你訪問一個屬性時,在自身找不到,會繼續向對象隱性引用指向的prototype繼續找.

在最初講對象的時候,特意強調了一下,訪問一個對象的屬性時,如果沒有,並不會給對象追加一個屬性,當時沒有說後半句,後半句就是它會繼續向它所指向prototype對象查找.這是javascript對訪問屬性時的處理方式.

而賦值操作呢,不會有這個查找的過程,如果沒有,直接追加一個屬性!所以myDog1.kind = "dog";這是給myDog1追加了一個kind屬性,而對myDog1訪問kind屬性時,在自身就找到了這個屬性,自然就不會繼續查找了.


那怎麼修改prototype的屬性呢?

誰有prototype"正常"的引用呢——function——也就是構造函數嘛.

Dog.prototype.kind = "dog";

也可以是這樣,但__proto__不是標準,即使是,也不建議這麼寫:

myDog1.__proto__.kind = "dog";

A.B.C三種創建對象方式的不同之處 

exampleA:

//字面量方式創建對象,簡單方便,可以認爲,這是exampleB方式的簡寫
var myDog = {name: "Odie", color: "Yellow"};

exampleB:

//使用new操作符創建對象,再追加屬性
var myDog = new Object();
myDog.name = "Odie";
myDog.color = "Yellow";

exampleC:

//一個很少使用的方式
var myDog = Object.create(new Object());
myDog.name = "Odie";
myDog.color = "Yellow";

A相當於B的簡寫方式,唯一的區別就是,通常,在瀏覽器中,A這種方式創建對象是不調用Object的構造函數的.

C呢,之前沒有解釋,這個方式跟A.B兩種區別很大.

首先要說明Object.create(prototype,descriptors)這個函數,它可以創建一個指定原型的對象,參數prototype就是想要給創建出來的對象添加的指定原型//忽略後面的參數(它是可選的).
"var myDog = Object.create(new Object());"就是創建了一個以Object的實例爲原型的對象,就是創建出來的對象的隱性指針指向了Object的實例.

過程類似於:
function(proto){
fucntion F(){};
F.prototype = proto;
return new F();
}

輸出一下myDog的__proto__;

console.log(myDog.__proto__);//Object {}
console.log(myDog.__proto__.__proto__);//Object {}
console.log(myDog.__proto__.__proto__.__proto__);//null

爲什麼前兩個都是"Object{}"呢,因爲這是以Object的實例做爲prototype的,myDog的__proto__指向Object的實例,而實例才真正指向Object的prototype.

如果這麼寫:
var myDog = Object.create(Object.prototype);
就和
var myDog = new Object();
是一樣的了.

原型鏈圖
\
加載中...

CF --> constructZ喎�"/kf/ware/vc/" target="_blank" class="keylink">vciBmdW5jdGlvbiAtLT65udTsuq/K/Txicj4KQ0ZwIC0tPmNvbnN0cnVjdG9yIGZ1bmN0aW9uIHByb3RvdHlwZSAtLT65udTsuq/K/bXE1K3QzTxicj4KY2YxLS1jZjUgLS0+IENGubnU7LXEyrXA/Txicj4K0OnP3yAtLT4g0v7Q1NL908M8YnI+Csq1z98gLS0+INX9s6PS/dPDPGJyPgo8YnI+CmNmMS0tY2Y11eLQqbbUz/O5ss/tQ0a1xHByb3RvdHlwZTs8YnI+CkNGcLG+ye3SssrHttTP89Ky09DSu7j20OnP37XE0v7Q1NL908M7PGJyPgpDRtKyyse21M/zLNKy09DSu7j20OnP37XE0v7Q1NL908Ms1sHT2srHyrLDtCzV4tKqv7Tkr8DAxvfKx9T1w7TKtc/WtcTByy48YnI+Cjxicj4KPHN0cm9uZz5wcm90b3R5cGXSstPQ0ru49ta4z/LL/LXEubnU7Lqvyv21xNL908M8L3N0cm9uZz48YnI+Cjxicj4Kyc/D5rXEzbzDu9PQserD9yzG5Mq1cHJvdG90eXBl0rLT0NK7uPbWuM/yubnU7Lqvyv21xNL908MsvdDX9mNvbnN0cnVjdG9yLtXiw7TLtbK7zKvXvMi3LNOmuMPLtcO/uPZwcm90b3R5cGXSstPQ0ru49ta4z/LP4NOmtcRmdW5jdGlvbrXE0v3Tw6OocHJvdG90eXBlsqKyu8rHubnU7Lqvyv22wNPQtcSjqS48YnI+Cjxicj4K0vK0yyzKx7/J0tTNqLn9ttTP86Oovq25/XByb3RvdHlwZaOpt8POymNvbnN0cnVjdG9ytcQuPGJyPgo8YnI+CnZhciBteURvZyA9IG5ldyBEb2co"Odie", "Yellow");
console.log(myDog.constructor);

怎麼證明constructor是prototype的屬性而不是對象myDog1的呢?!

Object有一個hasOwnProperty(name)函數來判斷:

console.log(myDog.hasOwnProperty("constructor"));//false
console.log(myDog.hasOwnProperty("hasOwnProperty"));//false,hasOwnProperty是Object的prototype的函數哦.
console.log(myDog.hasOwnProperty("sayName"));//false,這是Dog的prototype的函數
console.log(myDog.hasOwnProperty("name"));//這個纔是自己的屬性

4繼承

javascript的對象可以分爲兩部分,自己的屬性和從prototype共享的屬性.
那麼,想要在javascript中實現繼承,就需要:

1.子構造函數在執行過程中,也構造父構造函數構造出的屬性;
2.繼承父構造函數的共享屬性——將子構造函數的prototype對象的隱性引用__proto__指向父構造函數的prototype,這樣根據javascript的查找機制,就可以共享父類的prototype了;

javascript沒有類,只有對象,非要向其他OO語言靠攏的話,可以認爲把上述的父構造函數替換爲父類.

構造父構造函數構造的屬性

如果能在子構造函數構造的過程中,把它的this直接傳遞給父構造函數,在跑一遍父構造函數就好了.

javascript確實提供了這樣一個方式.就是function對象call函數.

call函數的用法

還記得function的this引用吧——就是函數據以執行的對象的引用(可以叫做函數的運行環境引用,亦或者叫函數的上下文引用),使用call函數,就可以指定函數運行時this引用指向的對象.

call(thisObj,arg0,arg1....);

thisObj這個參數就是你要給函數運行時指定的對象.
arg0,arg1....是可選的,是函數運行時真正的參數.

用call函數繼承父構造函數的屬性:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//父構造函數
function SuperType(name) {
    this.name = name;
}
//子構造函數
function SubType(name, age) {
    SuperType.call(this, name);//調用父構造函數
    this.age = age;
}
 
 
var sub = new SubType("js", 20);
console.log(sub.name);//js
console.log(sub.age);//20
console.log(sub.hasOwnProperty("name"));//true
console.log(sub.hasOwnProperty("age"));//true

前半部分就這麼實現了,so easy是吧.

繼承父構造函數的共享屬性

還記得那個create函數麼——創建一個指定原型的對象——可以指定被創建出來的對象的隱性引用(__proto__)指向那個對象.
那創建一個隱性引用指向父構造函數的prototype的對象:

var obj = Object.create(SuperType.prototype);

這是什麼?!
這不就是我們想要的子構造函數的prototype對象麼!!

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//父構造函數
function SuperType(name) {
    this.name = name;
}
 
SuperType.prototype.sayName = function() {
    console.log(this.name);
}
//子構造函數
function SubType(name, age) {
    SuperType.call(this, name);//調用父構造函數
    this.age = age;
}
 
SubType.prototype = Object.create(SuperType.prototype);
 
SubType.prototype.sayAge = function() {
    console.log(this.age);
}
 
var sub = new SubType("js", 20);
console.log(sub.name);//js
console.log(sub.age);//20
sub.sayName();//js
sub.sayAge();//20
 
console.log(sub.constructor);//function SuperType{...}

啊呀,sub的prototypetype怎麼指向SuperType了?!

從頭找,sub本身沒有constructor這個屬性,向他的原型找;
它的原型被修改了——"Object.create(SuperType.prototype)",這個對象也沒有constructor屬相啊,繼續向它的原型找;
那就是SuperType.prototype了,它的constructor指向了SuperType.


沒關係,只要小小的改動一下就好了.

SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
console.log(sub.constructor);//function subType{...}

只要給SuperType.prototype加一個constructor屬性就可以了.

繼承就這樣完成了~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章