寫給Android/Java開發者的JavaScript精解(1)
作為一個多年Javaer,我學習JavaScript的方式就是比較著學,努力辨識清楚Java與JavaScript的同與異,在比較中加深理解認識,最后達到學會JavaScript的目的。
許多程序語言都有自己的口號,Java的口號是:“write once,run everywhere!”同樣,JavaScript也有自己的口號,那就是“everything is object!”我的學習也是從這句話開始的,為此我需要搞清楚以下問題:
- 在JavaScript中,對象(object)到底是什么?
- 在JavaScript中,基本數據類型是對象嗎?
- 在JavaScript中,數組是對象嗎?
- 在JavaScript中,函數是對象嗎?
- 在JavaScript中,對象能繼承嗎?
- 在JavaScript中,創建對象的方法有多少?
- 在JavaScript中,如何使用函數這種特殊的對象(this,屬性使用,構建對象使用,閉包使用,柯里化,默認值,rest和spread)?
搞清楚上面的問題后,我又探究了如下問題: - 在JavaScript中,閉包是什么?
- 在JavaScript中,變量有作用域嗎?
- 在JavaScript中,有哪些奇技淫巧是Java所沒有的(模板字符串,數組解構賦值,getter,setter,Generator ,For of,modules)?
弄清這些問題后,再去學習React Native,就不會有語言上的困難了,真的好開心!終于能夠與React Native愉快地玩耍了!
一、在JavaScript中,對象(object)到底是什么?
一言以蔽之,在JavaScript中,對象就是一個Map,里面只有鍵值對,除此之外,啥也沒有!
1、創建對象
來,讓我們創建一個對象:
var person = new Object();
熟悉的new出現了,很親切。var 是個新東西,來看看它是什么。
在Java中我們要聲明一個變量,必須指明它的類型,如下所示:
Person person;
Dog dog;
Cat cat;
之所以必須指明變量的類型,是因為Java是一個靜態類型的語言,如果不聲明,編譯器將無法理解我們聲明的變量。與Java正好相反,JavaScript是一個動態類型的語言,聲明變量的時候不需要指明變量的類型,只需一個var 關鍵字就可以了,變量可以是任意類型,只有在你使用變量的時候,JavaScript才會動態確認變量的類型。
2、添加屬性
在對象包含的鍵值對中,鍵被稱為對象的屬性,值被稱為相應的屬性值。所有的鍵都是字符串,而值可以是任意的類型。
讓我們為上面的對象添加兩個屬性:
person['name'] = 'milter';
person['age'] = 31;
這里又有一個新東西,在JavaScript中,單引號和雙引號都可以用來創建字符串,二者是一樣的。
下面我們就可以訪問這些屬性值了:
var name = person['name'] ; // milter
var age = person['age'] ; // 31
3、簡便方法
以上創建對象、添加屬性、訪問屬性的方式非常笨拙:
- 添加屬性和訪問屬性的語法呆板(需頻繁輸入[] 和' ')
- 一次只能添加一個屬性
為了解決創建對象和添加屬性的笨拙,JavaScript提供了創建對象的大括號({ })語法:
var person ={'name' : 'milter', 'age': 31 };
這樣,可在創建對象的同時為其賦予多個屬性。
問題還沒有解決,上面賦予屬性時還需要寫單引號,訪問屬性時還要寫中括號和單引號。
為此,JavaScript進一步規定:如果代表屬性的字符串符合JavaScript的標識符命名規范,那么就可以省去字符串外面的單引號,同時可以使用object.prop語法訪問屬性。
JavaScript的標識符命名規范如下:
- 區分大小寫,Myname與myname是兩個不同的標識符。
- 標識符首字符可以是以下劃線(_)、美元符($)或者字母開始,不能是數字。
- 標識符中其它字符可以是下劃線(_)、美元符($)、字母或數字組成的。
- 關鍵字和保留字不能作為標識符。
顯然,我們的name和age屬性符合這個規范,所以現在我們可以這樣寫了:
var person ={ name : 'milter', age: 31 };
var name = person.name ; // milter
var age = person.age ; // 31
更酷的是,我們可以在對象創建出來后,繼續為它添加或刪除屬性,如下:
person.sex = 'man' ;
以上為person對象添加了叫sex的屬性,其值為字符串'man'。
delete person.sex ;
以上刪除了對象person中的sex屬性。
你可能很想問,對象中的方法難道也是屬性,也是鍵值對?答案是YES,后面講到函數時會細說,暫且按下。
二、 在JavaScript中,基本數據類型是對象嗎?
JavaScript中的基本數據類型有6種:
- String
- Number
- Boolean
- Symbol
- undefined
- null
與Java相比,String 和Boolean含義基本沒變。
獲取一個String值的方式在上文中已經提到,就是用單引號或者雙引號括起一段字符即可。
Boolean只有兩個值 true和false,不用自己創建,直接拿來用就可以。
Number只能是雙精度浮點數,就是Java中的double。
Symbol是個新類型,在ES6中引進。它的每個值都是獨一無二的,獲取一個Symbol值的語法是 : Symbol(String)。
例如:
var symb1 = Symbol('one');
var symb2 = Symbol('one');
var symb3 = Symbol();
var symb4 = Symbol();
由上可見,String是可選的,不管有沒有String,都能用這種語法獲取一個Symbol值。String只是為這個Symbol值增加了一個描述而已。Symbol的值都是獨一無二的,是指你不能兩次獲得一樣的Symbol值(正如人不可能兩次踏入同一條河流,請叫我哲學家)。比如Number這種數據類型,我可以反復獲取1這個值,并把它賦給不同的變量:
var number1 = 1 ;
var number2 = 1 ;
number1和number2是相等的,String和Boolean也可以這樣做。
但是Symbol不是這樣的!。上文中, symb1 != symb2,symb3 != symb4。
從Java轉過來的人,感到這種寫法很別扭,總想在Symbol前加上一個new,千萬不要這樣,JavaScript是不允許的。
請牢記,這里沒有創建任何對象,僅僅是創建了一個獨一無二的值。
Symbol很像Java中的UUID,Symbol()就相當于UUID.randomUUID()。
undefined和null這兩種數據類型都只有一個值,就是它們的類型名。undefined用于表示變量未初始化,null用于表示對象為空。
回到本節的問題,基本數據類型是對象嗎?答案是 NO!
說它們不是對象,原因有兩點:
- 它們是不可變的。上文中,創建出person對象后,還能繼續為它增加和刪除屬性,也可以改變原有屬性的值。基本數據類型只有一個值,它們沒有什么屬性可修改或添刪。而且一旦獲得一個值,這個值就不可能改變。這一點,很像Java中String的不可變性。
- 它們的比較和傳遞都是基于值的,對象的比較和傳遞是基于引用的。例如:
var a = 'one' ;
var b = 'one';
var c = {one:1};
var d = {one:1};
var e = c ;
上面,a和b是相等的,因為兩個變量有著同樣的值,c和d是不相等的,因為兩個變量指向兩個不同的對象,但是e和c是相等的,因為它們都指向同一個對象。如果你這樣做:
e.one = 2 ;
那么你會發現 c.one也會變成2。
正如Java中有基本數據類型的包裝類(Integer和int ,Double和double等)并可以自動裝拆箱一樣,JavaScript也有類似的機制,而且實現的更為徹底。
回頭看看六種基本數據類型,發現前四種的首字母是大寫的,那是因為JavaScript為它們創建了包裝對象,最后兩個undefined和null首字母是小寫的,因為JavaScript沒有為它們提供包裝對象。以String為例,代碼
var str = new String('one');
會用字符串'one'創建一個字符串對象str,String對象有許多操作字符串的方法可供你調用。Number和Boolean也與之類似,你可以
var num = new Number(3.1415);
var bool = new Boolean(true) ;
可以使用 typeof語法來驗證它們是不是對象。
typeof bool //object
typeof num //object
typeof str //object
但是Symbol是個例外,你不能使用new語法創建一個Symbol值的包裝對象,JavaScript是非常不鼓勵將Symbol值包裝成對象的,在實際使用中,這樣的需求非常非常少!如果你特別想將一個Symbol值包裝成對象,只能這樣
var sym = Symbol();
var wrapSymbol = new Object(sym);
同樣,與Java有自動裝拆箱一樣,JavaScript也有類似的機制,且更靈活。簡單講,當你把基本數據當作對象來使用時,它就會自動變成一個對象(undefined和null除外,原因前面已說明)。
舉例如下:
var hello ='hello';
var str = hello.slice(1); //str 的值是'ello'
這點比Java要方便!
其原理是:JavaScript 發現我們對基本數據類型hello進行了slice方法調用,它會用hello的值創建一個臨時的包裝對象,即new String(hello),然后在這個包裝對象上調用slice(1),返回基本數據類型字符串'ello'。隨后,這個臨時的包裝對象就會被銷毀。
三、在JavaScript中,數組是對象嗎?
基本數據類型不是對象,JavaScript的口號已經自己打臉。現在繼續來看數組。先說答案:數組是對象!
JavaScript中,可以這樣定義一個數組。
var arr = [ 'one', 'two', 'three' ];
乍一看,似乎沒有鍵值對,但是,JavaScript會把上面的代碼轉化成下面這樣:
var arr = { '0':'one', '1':'two', '2':'three' } ;
由此我們知道,數組屬于對象確定無疑!!
顯然按照第一種寫法更加簡潔直觀,你可以認為它是定義數組的語法糖。
現在,我們來訪問數組中的元素。按照前面學習的對象知識,似乎應該這樣訪問數組元素:
arr.0 , arr.1, arr.2
實際情況是,這是不允許的!為什么呢?
前面我們講過,只有屬性字符串符合JavaScript的標識符命名規范時,才能用圓點語法(object.prop)訪問對象的屬性。而在上面的數組中,屬性字符串是'0','1','2',它們都是以數字開頭的字符串,不符合標識符命名規范,所以我們不能用圓點語法訪問數組中的元素。
那怎么訪問?用基本的訪問對象屬性的方法,如下:
arr['0'], arr['1'], arr['2']
鑒于數組使用的頻率實在太高,讓用戶每次訪問數組元素都要寫單引號,太繁瑣,JavaScript對數組對象做了專門的優化,可以讓你省去單引號,直接這樣寫:
arr[0], arr[1], arr[2]
優化的原理非常簡單,在背后,JavaScript會自動將中括號中的東西兩側加上單引號。
但是這樣就會帶來一個非常微妙的問題,假如我們這樣寫:
arr[01]
那么,JavaScript就會把它轉化成這樣:
arr['01']
很顯然,對象 arr 中沒有'01'這個屬性,因為字符串'1'和'01'是不相等的,為避免這樣的錯誤,JavaScript就禁止了在訪問數組時輸入開頭為0的數字,它會報出 Invalid Number錯誤,提示你修改。
另外,JavaScript數組對象有一個length屬性,非常迷惑人,它并不是數組中元素的數量,請看下面的代碼:
var arr = ['one', ,'three'];
在這個數組中,只有兩個元素,但是arr.length的值仍然是3。JavaScript會把上面的代碼轉化成這樣:
var arr = {'0':'one','1':undefined,'2':'three'};
如果測試 arr[1] == undefined,你會得到結果true。
嚴格來說,arr.length的值等于最后一個元素的index再加上1。
四、在JavaScript中,函數是對象嗎?
在JavaScript中,函數首先具有Java中方法的性質,就是接收輸入,進行操作,根據需要產生輸出。
同樣,先說答案:函數是對象!
1、函數的定義
JavaScript中,定義一個函數有兩種方式:聲明的方式和表達式的方式。
- 聲明的方式:
function foo1 (a,b) { return a+b ; };
- 表達式的方式:
var foo2 = function (a,b) { return a + b; };
上面第一種方式定義了一個名為foo1的函數,第二種方式定義了一個名為foo2的函數,二者有什么區別?
最主要的區別是可用時機不一樣。假設foo1和foo2存在于同一個js文件中,js引擎加載并執行該js文件的具體過程是:
首先掃描整個文件,找出并加載foo1;
然后逐條語句解釋執行,文件中任何地方出現的foo1調用(不管是在foo1定義之前還是之后)都可以被正確處理,因為js引擎已經提前加載了該函數。
當執行到定義foo2函數的行時,才會加載這個函數,在之后的語句中才能調用foo2函數,如果在定義foo2函數的行之前使用foo2,程序將會報錯。
簡單講,聲明方式定義的函數會在程序執行之前加載,程序文件的任何地方都可以使用聲明方式定義的函數。表達式的方式定義的函數只有在執行到定義函數所在的行時,才會加載,才能在之后的語句中使用該函數。
問題來了,看看下面這種定義函數的方式:
var foo2 = function foo3(a,b) {
return a + b;
};
咦!這是什么鬼?它屬于哪種函數定義方式?這個函數的名字到底是foo2呢還是foo3呢?為什么要搞這么詭異的函數定義方式?
簡言之,它屬于表達式的方式定義的函數,函數的名字是foo2,foo3存在的唯一目的就是為了在該函數內部使用該函數(實現遞歸),在程序的其他地方foo3都是沒有意義的,在這一點上,可以認為foo3的作用域在該函數的內部。
現在可以來解釋對象的方法問題了。對象的方法也是一個屬性,此時,屬性的名字就是函數的名字,屬性的值就是一個函數。舉例說明如下:
var obj = {
add: function(a,b){
return a+b;
}
};
上面的代碼中,對象obj有一個屬性add,它的值是一個函數,函數的名字就是屬性的名字add,所以,你可以這樣使用obj:
var result = obj.add(4,9); // result == 13
上述定義函數的方式屬于表達式方式,也就是說,只有對象被創建后,add函數才會被加載。對象也可以使用聲明方式定義的函數作為屬性值,如下所示:
var obj = { };
obj.add1 = add ;
function add(a,b){
return a+b;
};
上面的代碼中,我們把以聲明方式定義的函數add賦值給obj的一個名為add1的屬性,此時你就可以這樣使用了:
var result = obj.add1(4,9); //result == 13
那么問題來了,此時的add1和add是什么關系?僅僅是一個函數的兩個名字嗎?答案是: NO!這涉及到函數的另一個概念scope,后面會講,暫且按下。
理解了對象的方法也是鍵值對后,我們可以更徹底地理解文章開頭的那句話:
在JavaScript中,對象就是一個Map,里面只有鍵值對,除此之外,啥也沒有!
搞清楚了函數的定義方式,理解了對象的方法也是鍵值對,我們來看本節主題:函數也是對象!
我們以上面定義的函數foo1(聲明方式定義)和foo2(表達式方式定義)為例來說明。
函數是對象,最明顯的證據就是我們可以為它增加、刪除、修改屬性。如下:
- 增加屬性:
foo1.prop1 = 'first prop';
foo1.prop2 = 'second prop';
foo2.prop1 = 'first prop';
foo2.prop2 = 'second prop'; - 修改屬性:
foo1.prop1 = 'changed prop';
foo2.prop1 = 'changed prop'; - 刪除屬性:
delete foo1.prop1;
delete foo2.prop1;
事實上,我們可以把函數當作一個特殊的對象,這個對象是可以被調用的(a object which can be called!)。除了這點特殊性,函數就與一般對象沒有什么明顯的區別了。
小結:現在,我們已經解決文章開頭提出的10個問題的前4個,這讓我們非常徹底地理解了JavaScript中的那句口號:“Everything is object”。我們獲得了如下知識:
- 對象就是鍵值對的集合,除此之外,它啥也沒有。
- JavaScript有6種基本數據類型,它們不是對象!
- 數組是對象,雖然乍一看并不像。
- 函數也是對象,只是有點特殊,是一種可以被調用的對象。
動手寫這個系列的初衷,是想給廣大的Android開發者學習React Native提供一個有針對性、精煉的JavaScript快速入門教程,默認大家對Java都比較熟悉。所以,對JavaScript和Java比較相似的地方都沒有涉及,比如 for 、while、if語句等,而把重點放在JavaScript明顯區別于Java的內容上,同時盡量從Java和JavaScript比較的角度來寫,目的是方便大家的理解。
另外,由于是為React Native學習做準備,所以對JavaScript中與網頁處理相關的內容也盡量避開了。
來自:http://www.jianshu.com/p/1b1b1110708d