對象這個詞如雷貫耳,同樣出名的一句話:XXX語言中一切皆為對象!
對象究竟是什么?什么叫面向?qū)ο缶幊蹋?/p>
理解對象
對象(object),臺灣譯作物件,是面向?qū)ο螅∣bject Oriented)中的術(shù)語,既表示客觀世界問題空間(Namespace)中的某個具體的事物,又表示軟件系統(tǒng)解空間中的基本元素。
在軟件系統(tǒng)中,對象具有唯一的標識符,對象包括屬性(Properties)和方法(Methods),屬性就是需要記憶的信息,方法就是對象能夠提供的服務(wù)。在面向?qū)ο螅∣bject Oriented)的軟件中,對象(Object)是某一個類(Class)的實例(Instance)。 —— 維基百科
對象是從我們現(xiàn)實生活中抽象出來的一個概念,俗話說物以類聚,人以群分,我們也經(jīng)常說有一類人,他們專業(yè)給隔壁家制造驚喜,也就是我們說的老王
這里面就有兩個重要概念
- 類:無論是物以類聚,還是有一類人,這里說的類并不是實際存在的事物,是一些特征、是一些規(guī)則等
- 老王:這是個實物,是現(xiàn)實存在,和類的關(guān)系就是符合類的描述
對應到計算機術(shù)語,類就是class,定義了一些特點(屬性 property)和行為(方法 method),比如說給隔壁制造驚喜的這類人有幾個特征
- 長相文質(zhì)彬彬,為人和善
- 姓王
同時這些人還有技能(行為)
- 幫隔壁修下水道
- 親切問候?qū)Ψ絻鹤?/li>
我們剛才就描述了一個類,用代碼表示就是
class LaoWang {
string name;
string familyNmae = "wang";
bool isKind = true;
LaoWang(string name) {
this.name = name;
}
void fixPipe() {
statement
}
void greetSon() {
statement
}
}
符合這些特點并且有上述行為能力的,我們稱之為老王,從描述我們就可以看出來LaoWang
不是指某個人,而是指一類人,符合上述描述的都可能是老王!用計算機術(shù)語說就是沒個活蹦亂跳的老王都是類LaoWang
的實例。用代碼描述就是
LaoWang lw1 = new LaoWang("yi");
LaoWang lw2 = new LaoWang("er");
...
LaoWang lw1000000 = new LaoWang("baiwan");
可以看出我們能夠根據(jù)類LaoWang
實例化出成千百萬個老王來,老王不是一個人在戰(zhàn)斗!
封裝
剛才我們說的已經(jīng)涉及到了對象的一個重要特性——封裝
以前我們可能會有這樣的描述
王一長相文質(zhì)彬彬,為人和善,姓王,有技能幫隔壁修下水道、親切問候?qū)Ψ絻鹤?王二長相文質(zhì)彬彬,為人和善,姓王,有技能幫隔壁修下水道、親切問候?qū)Ψ絻鹤?王三長相文質(zhì)彬彬,為人和善,姓王,有技能幫隔壁修下水道、親切問候?qū)Ψ絻鹤?王四長相文質(zhì)彬彬,為人和善,姓王,有技能幫隔壁修下水道、親切問候?qū)Ψ絻鹤?...
王百萬長相文質(zhì)彬彬,為人和善,姓王,有技能幫隔壁修下水道、親切問候?qū)Ψ絻鹤?
有了對象的思想我們可以這樣說了,首先定義一類人
有那么一類人
1. 長相文質(zhì)彬彬,為人和善
2. 姓王
同時這些人還有技能(行為)
1. 幫隔壁修下水道
2. 親切問候?qū)Ψ絻鹤?
然后是實例化,也就是對號入座
王一是老王
王二是老王
...
王百萬是老王
也就是我們通過類來描述一套規(guī)則,其中包括
- 屬性
- 行為
對于這個類實例化出的對象,也就是副歌這個類描述的對象,不用去關(guān)心對象細節(jié),我們認為符合類的描述,就會有類規(guī)定的屬性和方法,至于每個方法具體實現(xiàn)細節(jié)不去關(guān)注,比如老王怎么給人修水管,我知道他有修水管的技能,然后用的時候讓他去修就好了(只要不修我家的)
我們稱這種隱藏細節(jié)的特征叫做封裝
JavaScript 對象
因為JavaScript是基于原型(prototype)的,沒有類的概念(ES6有了,這個暫且不談),我們能接觸到的都是對象,真正做到了一切皆為對象
所以我們再說對象就有些模糊了,很多同學會搞混類型的對象和對象本身這個概念,我們在接下來的術(shù)語中不提對象,我們使用和Java類似的方式,方便理解
function People(name) {
this.name = name;
this.printName = function() {
console.log(name);
};
}
這是一個函數(shù),也是對象,我們稱之為類
var p1 = new People('Byron');
p1是People類new出來的對象,我們稱之為實例
類和實例的關(guān)系用我們碼農(nóng)的專業(yè)眼光看起來是這樣的
類就是搬磚的模具,實例就是根據(jù)模具印出來的磚塊,一個模具可以印出(實例化)多個實例,每個實例都符合類的特征,這個例子和我們JavaScript中概念很像
在Java中類不能稱之為對象,如同老王
是一個概念、規(guī)則的集合,但是在JavaScript中,本身沒有類的概念,我們需要用對象模擬出類,然后用類去創(chuàng)建對象
我們的例子中模具雖然是“類”,但同時也是個存在的實物,是個對象,我們?yōu)榱朔奖憷斫猓Q之為類
Object
我們知道JavaScript有null
、undefined
、number
、boolean
、string
五種簡單類型,null
和undefined
分別表示沒有聲明和聲明后沒有初始化的變量、對象,是兩個簡單的值,其余三個有對應的包裝對象Number
、Boolean
、String
其它的就都是object
類型了,比如常用的Array
、Date
、RegExp
等,我們最常用的Function
也是個對象,雖然
typeof function(){}; // "function"
但是Function實例和其它類型的實例沒有什么區(qū)別,都是對象,只不過typeof
操作符對其做了特殊處理
在JavaScript中使用對象很簡單,使用new操作符執(zhí)行Obejct
函數(shù)就可以構(gòu)建一個最基本的對象
var obj = new Object();
我們稱new 調(diào)用的函數(shù)為構(gòu)造函數(shù),構(gòu)造函數(shù)和普通函數(shù)區(qū)別僅僅在于是否使用了new
來調(diào)用,它們的返回值也會不同
所謂“構(gòu)造函數(shù)”,就是專門用來生成“對象”的函數(shù)。它提供模板,作為對象的基本結(jié)構(gòu)。一個構(gòu)造函數(shù),可以生成多個對象,這些對象都有相同的結(jié)構(gòu)
我們可以通過.
來位對象添加屬性和方法
obj.name = 'Byron';
obj.printName = function(){
console.log(obj.name);
};
這么寫比較麻煩,我們可以使用字面量來創(chuàng)建一個對象,下面的寫法和上面等價
var obj = {
name: 'Byron',
printNmae: function() {
console.log(obj.name);
}
}
構(gòu)造對象
我們可以拋開類,使用字面量來構(gòu)造一個對象
var obj1 = {
nick: 'Byron',
age: 20,
printName: function() {
console.log(obj1.nick);
}
}
var obj2 = {
nick: 'Casper',
age: 25,
printName: function() {
console.log(obj2.nick);
}
}
問題
這樣構(gòu)造有兩個明顯問題
- 太麻煩了,每次構(gòu)建一個對象都是復制一遍代碼
- 如果想個性化,只能通過手工賦值,使用者必需了解對象詳細
這兩個問題其實也是我們不能拋開類的重要原因,也是類的作用
使用函數(shù)做自動化
function createObj(nick, age) {
var obj = {
nick: nick,
age: age,
printName: function() {
console.log(this.nick);
}
};
return obj;
}
var obj3 = createObj('Byron', 30);
obj3.printName();
我們通過創(chuàng)建一個函數(shù)來實現(xiàn)自動創(chuàng)建對象的過程,至于個性化通過參數(shù)實現(xiàn),開發(fā)者不必關(guān)注細節(jié),只需要傳入指定參數(shù)即可
問題
這種方法解決了構(gòu)造過程復雜,需要了解細節(jié)的問題,但是構(gòu)造出來的對象類型都是Object,沒有識別度
有型一些
要想讓我們構(gòu)造出的函數(shù)有型一些,我們需要了解一些額外知識
function作為構(gòu)造函數(shù)(通過new操作符調(diào)用)的時候會返回一個類型為function的name的對象
function可以接受參數(shù),可以根據(jù)參數(shù)來創(chuàng)建相同類型不同值的對象
function實例作用域內(nèi)有一個constructor屬性,這個屬性就可以指示其構(gòu)造器
new
new 運算符接受一個函數(shù) F 及其參數(shù):new F(arguments...)。這一過程分為三步:
創(chuàng)建類的實例。這步是把一個空的對象的 proto 屬性設(shè)置為 F.prototype 。
初始化實例。函數(shù) F 被傳入?yún)?shù)并調(diào)用,關(guān)鍵字 this 被設(shè)定為該實例。
返回實例。
根據(jù)這幾個特性,我們可以改造一下創(chuàng)建對象的方式
function Person(nick, age) {
this.nick = nick;
this.age = age;
this.sayName = function() {
console.log(this.nick);
}
}
var p1 = new Person();
instanceof
instanceof是一個操作符,可以判斷對象是否為某個類型的實例
p1 instanceof Person; // true
p1 instanceof Object;// true
instanceof判斷的是對象
1 instanceof Number; // false
問題
構(gòu)造函數(shù)在解決了上面所有問題,同時為實例帶來了類型,但可以注意到每個實例printName方法實際上作用一樣,但是每個實例要重復一遍,大量對象存在的時候是浪費內(nèi)存
構(gòu)造函數(shù)
任何函數(shù)使用new表達式就是構(gòu)造函數(shù)
每個函數(shù)都自動添加一個名稱為prototype
屬性,這是一個對象每個對象都有一個內(nèi)部屬性proto
(規(guī)范中沒有指定這個名稱,但是瀏覽器都這么實現(xiàn)的) 指向其類型的prototype屬性,類的實例也是對象,其****proto****屬性指向“類”的prototype
prototype
通過圖示我們可以看出一些端倪,實例可以通過__prop__
訪問到其類型的prototype屬性,這就意味著類的prototype對象可以作為一個公共容器,供所有實例訪問。
抽象重復
我們剛才的問題可以通過這個手段解決
所有實例都會通過原型鏈引用到類型的prototype
prototype相當于特定類型所有實例都可以訪問到的一個公共容器
重復的東西移動到公共容器里放一份就可以了
看下代碼
function Person(nick, age) {
this.nick = nick;
this.age = age;
}
Person.prototype.sayName = function() {
console.log(this.nick);
}
var p1 = new Person(1, 2);
console.log(p1);
console.dir(p1);
p1.sayName(); //1
這時候我們對應的關(guān)系是這樣的
What's this?
由于運行期綁定的特性,JavaScript 中的 this
含義非常多,它可以是全局對象、當前對象或者任意對象,這完全取決于函數(shù)的調(diào)用方式
隨著函數(shù)使用場合的不同,this的值會發(fā)生變化。但是有一個總的原則,那就是this指的是,調(diào)用函數(shù)的那個對象
作為函數(shù)調(diào)用
在函數(shù)被直接調(diào)用時this
綁定到全局對象。在瀏覽器中,window
就是該全局對象
console.log(this);
function fn1() {
console.log(this);
}
fn1();
內(nèi)部函數(shù)
函數(shù)嵌套產(chǎn)生的內(nèi)部函數(shù)的this
不是其父函數(shù),仍然是全局變量
function fn0() {
function fn() {
console.log(this);
}
fn();
}
fn0();
setTimeout、setInterval
這兩個方法執(zhí)行的函數(shù)this也是全局對象
document.addEventListener('click', function(e) {
console.log(this);
setTimeout(function() {
console.log(this);
}, 1000);
}, false);
作為構(gòu)造函數(shù)調(diào)用
所謂構(gòu)造函數(shù),就是通過這個函數(shù)生成一個新對象(object)。這時,this就指這個新對象
new 運算符接受一個函數(shù) F 及其參數(shù):new F(arguments...)。這一過程分為三步:
創(chuàng)建類的實例。這步是把一個空的對象的 proto 屬性設(shè)置為 F.prototype 。
初始化實例。函數(shù) F 被傳入?yún)?shù)并調(diào)用,關(guān)鍵字 this 被設(shè)定為該實例。
返回實例。
看例子
function Person(name) {
this.name = name;
}
Person.prototype.printName = function() {
console.log(this.name);
};
var p1 = new Person('Byron');
var p2 = new Person('Casper');
var p3 = new Person('Vincent');
p1.printName();
p2.printName();
p3.printName();
作為對象方法調(diào)用
var obj1 = {
name: 'Byron',
fn: function() {
console.log(this);
}
};
obj1.fn();
小陷阱
var fn2 = obj1;
fn2();
DOM對象綁定事件
在事件處理程序中this
代表事件源DOM對象(低版本IE有bug,指向了window)
document.addEventListener('click', function(e) {
console.log(this);
var _document = this;
setTimeout(function() {
console.log(this);
console.log(_document);
}, 200);
}, false);
Function.prototype.bind
bind,返回一個新函數(shù),并且使函數(shù)內(nèi)部的this為傳入的第一個參數(shù)
var obj1 = {
name: 'Byron',
fn: function() {
console.log(this);
}
};
obj1.fn();
var fn3 = obj1.fn.bind(obj1);
fn3();
使用call和apply設(shè)置this
call apply,調(diào)用一個函數(shù),傳入函數(shù)執(zhí)行上下文及參數(shù)
fn.call(context, param1, param2...)
fn.apply(context, paramArray)
語法很簡單,第一個參數(shù)都是希望設(shè)置的this
對象,不同之處在于call
方法接收參數(shù)列表,而apply
接收參數(shù)數(shù)組
fn2.call(obj1);
fn2.apply(obj1);
caller
在函數(shù)A
調(diào)用函數(shù)B
時,被調(diào)用函數(shù)B
會自動生成一個caller
屬性,指向調(diào)用它的函數(shù)對象,如果函數(shù)當前未被調(diào)用,或并非被其他函數(shù)調(diào)用,則caller
為null
function fn4() {
console.log(fn4.caller);
function fn() {
console.log(fn.caller);
}
fn();
}
fn4();
arguments
在函數(shù)調(diào)用時,會自動在該函數(shù)內(nèi)部生成一個名為 arguments的隱藏對象
該對象類似于數(shù)組,可以使用[]運算符獲取函數(shù)調(diào)用時傳遞的實參
只有函數(shù)被調(diào)用時,arguments對象才會創(chuàng)建,未調(diào)用時其值為null
function fn5(name, age) {
console.log(arguments);
name = 'XXX';
console.log(arguments);
arguments[1] = 30;
console.log(arguments);
}
fn5('Byron', 20);
function arg() {
console.log(arguments)
}
arg()
callee
當函數(shù)被調(diào)用時,它的arguments.callee對象就會指向自身,也就是一個對自己的引用
由于arguments在函數(shù)被調(diào)用時才有效,因此arguments.callee在函數(shù)未調(diào)用時是不存在的(即null.callee),且解引用它會產(chǎn)生異常
function fn6() {
console.log(arguments.callee);
}
fn6();
匿名函數(shù)特好用
var i = 0;
window.onclick = function() {
console.log(i);
if (i < 5) {
i++;
setTimeout(arguments.callee, 200); //運行它自己了哈哈
}
}
函數(shù)的執(zhí)行環(huán)境
JavaScript中的函數(shù)既可以被當作普通函數(shù)執(zhí)行,也可以作為對象的方法執(zhí)行,這是導致 this 含義如此豐富的主要原因
一個函數(shù)被執(zhí)行時,會創(chuàng)建一個執(zhí)行環(huán)境(ExecutionContext),函數(shù)的所有的行為均發(fā)生在此執(zhí)行環(huán)境中,構(gòu)建該執(zhí)行環(huán)境時,JavaScript 首先會創(chuàng)建 arguments
變量,其中包含調(diào)用函數(shù)時傳入的參數(shù)
接下來創(chuàng)建作用域鏈,然后初始化變量。首先初始化函數(shù)的形參表,值為 arguments變量中對應的值,如果 arguments變量中沒有對應值,則該形參初始化為undefined
。
如果該函數(shù)中含有內(nèi)部函數(shù),則初始化這些內(nèi)部函數(shù)。如果沒有,繼續(xù)初始化該函數(shù)內(nèi)定義的局部變量,需要注意的是此時這些變量初始化為 undefined
,其賦值操作在執(zhí)行環(huán)境(ExecutionContext)創(chuàng)建成功后,函數(shù)執(zhí)行時才會執(zhí)行,這點對于我們理解JavaScript中的變量作用域非常重要,最后為this
變量賦值,會根據(jù)函數(shù)調(diào)用方式的不同,賦給this
全局對象,當前對象等
至此函數(shù)的執(zhí)行環(huán)境(ExecutionContext)創(chuàng)建成功,函數(shù)開始逐行執(zhí)行,所需變量均從之前構(gòu)建好的執(zhí)行環(huán)境(ExecutionContext)中讀取
三種變量
實例變量:(this)類的實例才能訪問到的變量
靜態(tài)變量:(屬性)直接類型對象能訪問到的變量
私有變量:(局部變量)當前作用域內(nèi)有效的變量
看個例子
function ClassA() {
var a = 1; //私有變量,只有函數(shù)內(nèi)部可以訪問
this.b = 2; //實例變量,只有實例可以訪問
}
ClassA.c = 3; // 靜態(tài)變量,也就是屬性,類型訪問
console.log(a); // error
console.log(ClassA.b) // undefined
console.log(ClassA.c) //3
var classa = new ClassA();
console.log(classa.a); //undefined
console.log(classa.b); // 2
console.log(classa.c); //undefined
原型鏈和繼承
在一切開始之前回顧一下類
、實例
、prototype
、__proto__
的關(guān)系
function Person(nick, age) {
this.nick = nick;
this.age = age;
}
Person.prototype.sayName = function() {
console.log(this.nick);
}
var p1 = new Person();
p1.sayName();
- 我們通過函數(shù)定義了類
Person
,類(函數(shù))自動獲得屬性prototype
- 每個類的實例都會有一個內(nèi)部屬性
__proto__
,指向類的prototype
屬性
有趣的現(xiàn)象
我們定義一個數(shù)組,調(diào)用其valueOf
方法
[1, 2, 3].valueOf(); // [1, 2, 3]
很奇怪的是我們在數(shù)組的類型Array中并不能找到valueOf
的定義,根據(jù)之前的理論那么極有可能定義在了Array的prototype
中用于實例共享方法,查看一下
我們發(fā)現(xiàn)Array
的prototype
里面并未包含valueOf
等定義,那么valueOf
是哪里來的呢?
一個有趣的現(xiàn)象是我們在Object實例的__proto__
屬性(也就是Object的prototype屬性)中找到了找到了這個方法
那么Array的實例為什么同樣可以查找到Object的prototype里面定義的方法呢?
查找valueOf過程
因為任何類的prototype
屬性本質(zhì)上都是個類Object
的實例,所以prototype
也和其它實例一樣也有個__proto__
內(nèi)部屬性,指向其類型Object
的prototype
我們大概可以知道為什么了,自己的類的prototype找不到的話,還會找prototypr的類型的prototype屬性,這樣層層向上查找
大概過程是這樣的
記當前對象位obj,查找obj屬性、方法,找到后返回
沒有找到,通過obj的
__proto__
屬性,找到其類型Array
的prototype
屬性(記為prop)繼續(xù)查找,找到后返回沒有找到,把prop記為obj做遞歸重復步驟一,通過類似方法找到prop的類型
Object
的 prototype進行查找,找到返回
類型
之前介紹過instanceof
操作符,判斷一個對象是不是某個類型的實例
[1, 2, 3] instanceof Array; //true
可以看到[1, 2, 3]
是類型Array
的實例
[1, 2, 3] instanceof Object; //true
這個結(jié)果有些非議所思,怎么又是Array的實例,又是Object的實例,這不是亂套了
其實這個現(xiàn)象在日常生活中很常見其實,比如我們有兩種類型
- 類人猿
- 動物
我們發(fā)現(xiàn)黑猩猩即是類人猿這個類的物種(實例),也是動物的實例
是不是悟出其中的門道了,類人猿是動物的一種,也就是說我們的兩個類型之間有一種父子關(guān)系
這就是傳說中的繼承,JavaScript正是通過原型鏈實現(xiàn)繼承機制的
繼承
繼承是指一個對象直接使用另一對象的屬性和方法。
JavaScript并不提供原生的繼承機制,我們自己實現(xiàn)的方式很多,介紹一種最為通用的
通過上面定義我們可以看出我們?nèi)绻麑崿F(xiàn)了兩點的話就可以說我們實現(xiàn)了繼承
- 得到一個類的屬性
- 得到一個類的方法
我們分開討論一下,先定義兩個個類
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.printName = function() {
console.log(this.name);
};
function Male(age) {
this.age = age;
}
Male.prototype.printAge = function() {
console.log(this.age);
};
屬性獲取
對象屬性的獲取是通過構(gòu)造函數(shù)的執(zhí)行,我們在一個類中執(zhí)行另外一個類的構(gòu)造函數(shù),就可以把屬性賦值到自己內(nèi)部,但是我們需要把環(huán)境改到自己的作用域內(nèi),這就要借助我們講過的函數(shù)call
了
改造一些Male
function Male(name, sex, age){
Person.call(this, name, sex);
this.age = age;
}
Male.prototype.printAge = function(){
console.log(this.age);
};
實例化結(jié)果
var m = new Male('Byron', 'male', 26);
console.log(m.sex); // "male"
方法獲取
我們知道類的方法都定義在了prototype里面,所以只要我們把子類的prototype改為父類的prototype的備份就好了
Male.prototype = Object.create(Person.prototype);
這里我們通過Object.createclone
了一個新的prototype而不是直接把Person.prtotype直接賦值,因為引用關(guān)系,這樣會導致后續(xù)修改子類的prototype也修改了父類的prototype,因為修改的是一個值
另外Object.create
是ES5方法,之前版本通過遍歷屬性也可以實現(xiàn)淺拷貝
這樣做需要注意一點就是對子類添加方法,必須在修改其prototype之后,如果在之前會被覆蓋掉
Male.prototype.printAge = function() {
console.log(this.age);
};
Male.prototype = Object.create(Person.prototype);
這樣的話,printAge
方法在賦值后就沒了,因此得這么寫
function Male(name, sex, age) {
Person.call(this, name, sex);
this.age = age;
}
Male.prototype = Object.create(Person.prototype);
Male.prototype.printAge = function() {
console.log(this.age);
};
這樣寫貌似沒問題了,但是有個問題就是我們知道prototype對象有一個屬性constructor
指向其類型,因為我們復制的父元素的prototype,這時候constructor
屬性指向是不對的,導致我們判斷類型出錯
Male.prototype.constructor; //Person
因此我們需要再重新指定一下constructor屬性到自己的類型
最終方案
我們可以通過一個函數(shù)實現(xiàn)剛才說的內(nèi)容
function inherit(superType, subType) {
var _prototype = Object.create(superType.prototype);
_prototype.constructor = subType;
subType.prototype = _prototype;
}
使用方式
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.printName = function() {
console.log(this.name);
};
function Male(name, sex, age) {
Person.call(this, name, sex);
this.age = age;
}
inherit(Person, Male);
// 在繼承函數(shù)之后寫自己的方法,否則會被覆蓋
Male.prototype.printAge = function() {
console.log(this.age);
};
var m = new Male('Byron', 'm', 26);
m.printName();
這樣我們就在JavaScript中實現(xiàn)了繼承
hasOwnProperty
有同學可能會問一個問題,繼承之后Male的實例也有了Person的方法,那么怎么判斷某個是自己的還是父類的?
hasOwnPerperty
是Object.prototype
的一個方法,可以判斷一個對象是否包含自定義屬性而不是原型鏈上的屬性,hasOwnProperty
是JavaScript中唯一一個處理屬性但是不查找原型鏈的函數(shù)
m.hasOwnProperty('name'); // true
m.hasOwnProperty('printName'); // false
Male.prototype.hasOwnProperty('printAge'); // true