寫給Android/Java開發者的JavaScript精解(3)

Luc24K 8年前發布 | 6K 次閱讀 Java Android開發 移動開發 JavaScript

在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調用到底是怎么執行的:

  1. JavaScript引擎執行到 new 調用所在的行時,它立馬明白了,這里不是一個普通的函數調用,而是一個new調用,用戶想要通過函數調用生成一個對象,于是JavaScript創建出來一個新的對象,姑且稱其為 obj
  2. 然后,JavaScript引擎會將函數的prototype屬性所指向的對象設為 obj 的原型。
    實際上,每個函數都有一個prototype屬性。當你用一個函數創建一個對象時,新建對象的原型會被自動設置為函數prototype屬性指向的對象。
  3. 然后,JavaScript引擎會把關鍵字 this 綁定到新創建的對象 obj 上,也就是說,之后在函數體內對this關鍵字的操作就是對新創建的對象 obj 的操作。
  4. 之后,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

 

 本文由用戶 Luc24K 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!