寫給Android/Java開發者的JavaScript精解(3)
在JavaScript中,對象是最重要的概念,因為除了基本數據類型,其他的一切都是對象。為此,JavaScript提供了多樣的創建對象的方法。同時,函數又是極為特殊的一種對象,因此,JavaScript針對函數也做了許多巧妙的使用優化。
六、在JavaScript中,創建對象的方法有多少?
在前面兩篇文章中,已經涉及了三種創建對象的方法:
- new Object()
- Object.create()
- { }
三種方法都比較簡便。在ES6中,針對大括號({ })語法創建對象進一步做了優化。1、再探大括號法創建對象
優化一:函數屬性的簡寫
以前,為對象定義函數要這樣寫:
var obj = {
add: function(a,b){
return a + b;
}
}
現在可以這樣寫了:
var obj = {
add(a,b){
return a + b;
}
}
第二種寫法乍看是第一種寫法的語法糖,然而二者卻不完全等價,第二種寫法中,函數內部也可以通過add來調用自身,因為第二種寫法本質上是這樣的:
var obj = {
add: function add(a,b){
return a + b;
}
}
function 后面的add僅限于在函數體內使用,這一點在該系列第一篇中已經講明。
優化二:基本數據屬性和對象屬性的優化
假如以前你這樣定義了一個對象:
var a = "foo",
b = 42,
c = {};
var o = {
a: a,
b: b,
c: c
};
那么現在,你可以這樣寫了:
var a = "foo",
b = 42,
c = {};
var o = { a, b, c };
這種寫法就完全是一種語法糖了,簡單講,對于以 prop : prop 方式定義的屬性,也就是屬性名與代表屬性值的變量名稱相同,就可以直接寫 prop。
優化三:屬性名也可以用表達式
var i = 0;
var a = {
["foo" + ++i]: i,
["foo" + ++i]: i,
["foo" + ++i]: i
};
上述代碼在創建對象時,JavaScript會首先把中括號([])中的內容當作表達式進行求值,求值的結果再轉為字符串作為屬性的名稱。因此,對于對象a,我們有:
a.foo1 == 1; // true
a.foo2 == 2 ; // true
a.foo3 == 3 ; // true
2、用函數創建對象
本系列第一篇中,我們指出,函數是一種特殊的對象,最大的特殊之處在于它是可以被調用的(a object which can be called !),其基本的調用方法與Java是一致的。
實際上, 任何一個函數 還有一種特殊的調用方式,姑且稱之為 new 調用 ,這種調用會創建一個對象返回。
舉例如下:
function add(a,b){
var result = a + b ;
return result ;
}
這是一個再普通不過的一個函數,我們可以這樣使用它:
var sum = add(5,9) ; //sum的值為14
然而,我們還可以這樣使用它:
var obj = new add(5,9) ; // typeof obj == object
上面,我們僅僅是在正常的調用前面加了一個關鍵字 new ,整個函數的執行邏輯就完全發生了變化,最大的變化是它不再返回函數體內的return語句中的值,而是返回了一個對象!
我想,每一個從Java轉來學JavaScript的人,看到這樣的情況,都會覺得不可思議吧。
有沒有感到在JavaScript的世界中,函數作為一個特殊的對象,似乎凌駕于普通對象之上了?這貨竟然可以生成對象!
確實是這樣的,如果你學習過JavaScript,你應該會聽過一句話,在JavaScript中,函數是一等公民,說的就是函數的這種特殊性。
讓我們沉下心來,看看上面的new調用到底是怎么執行的:
- JavaScript引擎執行到 new 調用所在的行時,它立馬明白了,這里不是一個普通的函數調用,而是一個new調用,用戶想要通過函數調用生成一個對象,于是JavaScript創建出來一個新的對象,姑且稱其為 obj 。
- 然后,JavaScript引擎會將函數的prototype屬性所指向的對象設為 obj 的原型。
實際上,每個函數都有一個prototype屬性。當你用一個函數創建一個對象時,新建對象的原型會被自動設置為函數prototype屬性指向的對象。 - 然后,JavaScript引擎會把關鍵字 this 綁定到新創建的對象 obj 上,也就是說,之后在函數體內對this關鍵字的操作就是對新創建的對象 obj 的操作。
- 之后,JavaScript引擎會根據用戶在new調用中傳入的參數(本例中為5和9,不同的函數要求的參數也不相同,也可以沒有參數)來一句一句執行函數, 如果函數最后沒有return語句,那么當函數體執行完畢后,JavaScript會直接把對象 obj 返回給調用者。如果函數最后有return語句,JavaScript會判斷一下return語句中的返回值是不是一個對象,如果是一個對象,那么就把這個對象返回給調用者,如果return語句返回的是一個基本數據類型,而不是一個對象,那么JavaScript仍然把對象 obj 返回給調用者。在本例中,JavaScript會首先執行語句 var result = a + b ; 然后遇到了return語句,JavaScript發現這個return語句返回的是一個基本數據類型,不是一個對象,于是它把對象 obj 返回給了我們。
注:在本例中,整個函數體內我們沒有對關鍵字 this 進行任何操作,所以對象 obj 一直沒有發生什么變化。
觀察上面創建對象的過程,我們發現有幾點比較別扭:
- add函數首字母是小寫,new add(5,9)形式上不夠優美,在面向對象語言實踐中,我們更習慣首字母大寫的new調用。
- 語句 var result = a + b ; 對最后的對象沒有什么影響,浪費CPU資源
- return語句最后返回一個數字,也沒有什么用,還讓JavaScript多了一次判斷
于是,大家就 約定(僅僅是一個約定,不是語言本身的要求):
- 當你創建一個專門用來生成對象的函數時,就把函數名字的首字母大寫
- 函數體內只保留對最后生成對象有影響的語句,也就是對this有影響的語句
- 不要最后的return語句,確保this所代表的對象能夠返回給用戶
下面就是一個符合上面約定的例子:
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
你可以這樣來使用Person函數:
var milter = new Person("milter", 31, "男");
進一步思考一個問題:上面代碼中,我們創建了一個Person對象milter,但此時milter只具有三個屬性,并不具有方法,也就是行為能力,那么如何為Person對象添加方法呢?
上面我們提到,當JavaScript看到我們用 new 調用一個函數時,它會首先創建一個對象,并將函數的prototype屬性指向的對象作為新建對象的原型。因此,我們可以在Person.prototype中添加方法,這樣,所有用函數Person創建的對象都會具有我們添加的方法。
例如,我們可以為Person添加一個run方法:
Person.prototype.run = function (){
// running
}
那么,所有用Person函數創建的對象現在都具有了run方法,如:
milter.run() ;
神奇的是,當我們改變Person.prototype時,那些在改變之前用Person創建的對象也會隨之改變。因為所有用Person創建的對象的原型是同一個Person.prototype對象。這是體現JavaScript動態性的典型例子。
現在我們知道,用Person創建的對象的原型是Person.prototype,那么,問題來了,Person的原型又是什么呢?
答案是:所有的函數的原型都是一個特殊的對象: Function.prototype ,該對象中包含了作為一個函數的普遍屬性和方法,而Function.prototype的原型又是Object.prototype。所以,Person的原型鏈是:
Person --->Function.prototype---->Object.prototype
而用Person創建的對象的原型鏈是:
milter--->Person.prototype---->Object.prototype
也就是說,函數的原型和函數創建的對象的原型不是一回事,一定要搞清楚,初學者很容易將二者混為一談。
明白了這一點,我們也就知道:
Person.eat = function (){
//eating
}
為Person添加的方法eat并不會被用Person創建的對象所繼承,它屬于Person函數本身的方法。
只要使用JavaScript的人都遵守上面的約定,那么,每當你看到一個名字首字母大寫的函數,你就應當立即反應過來,這個函數是用來創建對象的,你應當用 new調用 來使用它,而不是將它當作普通函數使用。
3、用class創建對象
看到class,你可能會以為JavaScript中也引入了類的概念,然而實際情況可能會讓你失望,用class創建對象,其本質還是用函數創建對象,只不過是包裝成了類的形式而已。一個class其實就是一個函數。
我們知道,創建函數有兩種方式:聲明的方式和表達式的方式,這一點在本系列第1篇中有具體的分析。和創建函數一樣,也有兩種方法創建一個class,也是聲明的方式和表達式的方式,如下所示:
//聲明的方式創建class
class Person {
constructor(name, age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
};
上述代碼以 聲明的方式 定義了一個class Person。此時,創建一個Person對象的方法是:
var person = new Person('milter',31,'男');
我們發現,這個class Person和上一小節中的函數Person非常像,二者創建對象的方法甚至完全一樣!最明顯的不一樣的地方是class Person中多了一個方法 constructor,這個方法在每個class中 有且只有唯一的一個 ,其作用是初始化用該類創建的對象。
從本質上看, class Person就是函數Person的一個 語法糖 ,它使得定義一個創建對象的函數更加容易,語義更加清晰,對我們這些從Java轉過來的程序員更加友好。除此之外,這里沒有任何神秘的東西。
如果你測試一下 class Person 的類型,像這樣
typeof Person
你會得到結果 function 。說明class Person 本質上就是一個函數。
但是class Person與函數Person有一點小差別。以聲明形式定義的函數Person可以在函數定義之前使用,但是,以聲明形式定義的class Person,卻必須在定義之后才能使用,這點需要在使用中注意。
我們知道,對于函數Person,可以用表達式的方式定義,如下所示:
var Person = function (name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
同理,class Person也可以用表達式的方式定義如下:
//表達式的方式創建class
var Person = class {
constructor(name, age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
};
在用表達式方式定義函數時,你可以在function 后面添加一個名字,以便在函數體內使用該函數,同樣,你也可以在class 后面添加一個名字,以便在class內部使用該class。
在用函數語法創建對象中(參見上一小節),為對象添加 共同的 方法和屬性需要在Person.prototype中添加,為函數Person本身添加方法和屬性需要直接在Person中添加。利用class語法創建對象對此做了很大的優化,請看如下代碼:
//聲明的方式創建class
class Person {
constructor(name, age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
run(){
//running
}
static sextype(){
return ['男','女'] ;
}
};
上述代碼中,我們進一步為Person創建了兩個方法,一個是普通方法run,一個是static方法sextype(請注意:各個方法之間既無逗號分隔,也無分號分隔)。二者有什么區別呢?
普通方法將會被設置成Person.prototype的方法,static方法將會被設置成Person本身的方法。如下圖所示:
Person | Person.prototype |
---|---|
sextype | run |
由此,我們知道,sextype是不會被class Person創建的對象繼承的,而只能通過Person.sextype的方法調用。run會被class Person創建的對象繼承。
可以看到,當我們將class Person還原成它的本質 函數 后,我們就能明白class中的static方法和普通方法的區別,也很容易理解為什么在對象中沒法調用static方法。
好奇的你可能會問,那constructor方法被放到哪里了呢?答案是:Person.prototype,它會被所有的對象繼承。
但是由于這個方法的特殊性,JavaScript不允許我們直接調用它。也就是說,你不能這樣調用:
milter.constructor('lucy',18,'女'); //語法錯誤
class 語法還允許我們使用extends和super關鍵字,來看一個例子:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return this.x+':'+this.y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return super.toString() + ' in ' + this.color;
}
}
友情提示:Point 和ColorPoint本質上都是函數。
extends關鍵字在這里主要有三個作用:
-
將ColorPoint的原型設為 Point,所以此時ColorPoint的原型鏈成了:
ColorPoint --->Point---->Function.prototype---->Object.prototype
由此我們可以知道,ColorPoint將會繼承Point中的靜態方法。
- 將ColorPoint.prototype的原型設為Point.prototype,所以此時ColorPoint.prototype的原型鏈成了:
ColorPoint.prototype---->Point.prototype---->Object.prototype
由此我們知道,用ColorPoint創建的對象將會繼承Point中的普通方法。 - 強制ColorPoint在其constructor方法中調用Point的constructor方法,調用語法為:
super();
另一個使用super關鍵字的地方是:在ColorPoint的toString方法中,通過super.toString()來調用Point中的toString(),這一點和我們在Java中的用法一樣,不再贅述。
小結:本文中,我們學習了用大括號法創建對象的許多簡便寫法。更重要的是學習了用函數創建對象和用class創建對象的方法,并從內在原理上分析了二者的統一性,從本質上認識到了class語法只是對JavaScript的對象原型繼承的一層包裝而已。在JavaScript中是沒有什么類的概念的。
來自:http://www.jianshu.com/p/6e71ea7d769b