精讀JavaScript模式(三),new一個構造函數究竟發生了什么?


一、前言

上個月底,爸爸因為事故突然離世,說心里話,現在看到'去世','爸爸'這樣的字眼,眼淚都會忍不住在眼眶打轉,還是需要時間治愈。最近也只是零碎的看了下東西,始終沉不下心去讀書,直到今天還是決定撿起之前看的JS模式。

前面兩篇博客大概記錄了書中前兩章節我覺得一些好用的知識,從這篇開始就是第三章--直接量和構造函數了,難度也不算大,最近下班了在公司花點時間慢慢寫。

從第三篇開始,我想在介紹每個知識點前,先以概要的形式將這一部分介紹的東西以問題的形式列出來,方便大家帶着問題去讀,讀完了再回頭看問題,看自己能不能答出來,這樣也方便檢驗自己對於知識的掌握情況。

二、對象直接量

概要:什么是對象,什么是對象直接量寫法,空對象不是真正的空對象

我們可以將js中的對象簡單理解為名值對組成的散列表,其中值都是名的屬性,值可以是原始值(string,number...),也可以是對象,而當對象是函數時,我們一般稱之為方法。

js中自定義的對象任何時候都是可變的,內置本地對象的屬性也是可以修改的,比如你可以先創建一個空對象,然后在給它添加一些屬性或方法。而在創建對象時,對象直接量寫法是較為理想的方式。

var me = {};
me.name = "echo";
me.getName = function () {
  return me.name;
};

你可以刪除對象的某個屬性或方法

delete me.name;

其實我們創建一個對象也沒必要像上面先創建空對象,再一步步添加屬性方式,在對象創建時可以同時把你需要定義的屬性同時添加好。

let me = {
  name = "echo",
  getName = function () {
    return this.name;
  }
};

那么這種直接用 = 創建對象的方式就是對象直接量的寫法,很直接不是嗎。對象直接量語法包括:

• 將對象主體包含在一對花括號內。
• 對象內的屬性或方法之間使用逗號分隔。最后一個名值對后也可以有逗號,但 在IE下會報錯,所以盡量不要在最后一個屬性或方法后加逗號。
• 屬性名和值之間使用冒號分隔
• 如果將對象賦值給一個變量,不要忘了在右括號之后補上分號

我在上方提到的空對象可以說是算是一種簡稱,它們並不是真正的空對象,即便申明一個{},它也會從Object.prototype繼承很多屬性和方法,但是我們其實有方法創建嚴格意義上的空對象,我在上一篇文章中就列出了兩種方法,有興趣可以去看看。

三、通過構造函數創建對象

概要:為什么推薦對象直接量寫法而不是構造函數寫法

盡管JS中沒有類的概念,但當你想快速創建多個具有共同特征的實例時還是可以使用構造函數,JS中內置了不少構造函數,例如Object(),Date(),String()等等。

我們用構造函數的寫法來創造上面的對象:

var me = new Object();
me.name = "echo";
me.getName = function () {
  return me.name;
}

相比之下,對象直接量的寫法與構造函數寫法相比代碼更少,推薦直接量寫法的還有兩個原因

一是它可以強調對象是一個簡單的可變的散列表,而不必一定派生自某個類。

二是當你使用Object()創建對象時,解析器需要順着作用域鏈開始查找,直到找到Object構造函數為止,而直接量的寫法是不會存在作用域解析行為。

 四、自定義構造函數

概要:當你new一個構造函數時發生了什么?

除了對象直接量和內置構造函數之外,我們還可以通過自定義的構造函數來創建實例對象,像這樣。

var Person = function () {
  this.name = "echo";
  this.sayName = function () {
    console.log('my name is '+ this.name);
  };
}
var me = new Person();
me.sayName();//my name is echo

說個小插曲,這里我自定義的構造函數名Person的字母P其實可以小寫,但我們都知道,內置構造函數都是大寫開頭,所以為了讓構造函數更為醒目,推薦首字母大寫!

很奇怪對不對,我們new一個構造函數得到一個實例,這個實例就繼承了構造函數的屬性方法,那new這個過程中到底發生了什么?

1.創建一個空對象,將它的引用賦給this,繼承函數的原型。

2.通過this將屬性和方法添加至這個對象。

3.最后返回this指向的新對象。

我們用代碼模擬這三句話,就像這樣:

var Person = function () {
  // var this = {};
  this.name = "echo";
  this.sayName = function () {
    console.log('my name is '+ this.name);
  };
  // return this; 這里隱性返回的其實就是上面創建的空對象,這個空對象被賦予了name屬性和一個sayName方法
}
var me = new Person();
me.sayName();//my name is echo

 在這段代碼中,sayName()方法被添加到了this中,但有個問題,不管我們執行幾次new Person(),sayName()方法會反復的被添加到this中,且每次sayName()方法都會在內存中新開內存。

當我們所有實例中的sayName()方法都是一模一樣時,這種做法是很浪費內存的,推薦做法是將sayName()方法添加在Person的原型中。

var Person = function () {
  this.name = "echo";
};
Person.prototype.sayName = function () {
  console.log('my name is '+ this.name);
};
var me = new Person();
me.sayName();//my name is echo

關於new一個構造函數到底發生了什么,我在前面說會隱性的新建一個空對象賦予this,還是那句話,這里的空對象並不是嚴格意義上的空,它還是繼承了Person的原型,准確來說應該是這樣。

// var this = Object.create(Person.prototype);

 五、構造函數的返回值

概要:構造函數能返回什么?默認返回什么?

當我們new一個構造函數總是會返回一個對象,默認返回this所指向的對象。如果我們沒有在構造函數內為this賦予任何屬性,則會返回一個集成了構造函數原型,沒有自己屬性的'空對象'。(如果讀不懂這句話,請結合new發生的過程去理解)

盡管我們沒在構造函數內寫return語句,也會隱性的返回this,但其實我們可以返回自定義的對象。像這樣:

var Person = function () {
  this.name = "echo";
  var that = {};
  that.name = "wl";
  return that;
};
var me = new Person();
me.name;//wl

構造函數可以返回任意對象,只要你返回的是個對象。假設你返回的不是對象,程序也不會報錯,但這個返回值會被忽略,最終還是隱性的返回this所指向的對象。

var Person = function () {
  this.name = "echo";
  var name = "wl";
  return name;
};
var me = new Person();
me.name; //echo

六、強制使用new 的模式

概要:不使用new調用構造函數會怎樣?構造函數內能自定義對象嗎?不使用new也能繼承構造函數原型的做法

構造函數與普通函數無異,只是調用需要使用new,當我們不使用new調用時,語法也不會出錯,但函數中的this會指向全局對象(非嚴格模式是window)。

var Person = function () {
  this.name = "echo";
};
var me = Person();
window.name//echo

在這段代碼中實際上創建了一個全局對象屬性name,你可以通過window.name訪問到它。那么說到這里對於構造函數我們強調兩點。

1.構造函數名首字母大寫

2.調用構造函數使用new

遵守這些約定肯定是好的,但在實際開發的構造函數中,我們常常看看使用that等其它字面量代替this的做法。這么做的目的是為了確保構造函數按照自己定義的方式執行,而不存創建空對象賦予this等隱性不可見的行為,更可預測。

var Person = function () {
  var that = {};
  that.name = "echo";
  return that;
};
var me = new Person();
me.name;//echo

對於上述代碼中,我們使用that代替了this,使用that只是一種命名約定,你可以使用self,me甚至任意非js語言保留字的字段。

或者that都不創建,直接返回一個對象。

var Person = function () {
  return {
    name : "echo"
  };
};
var me = new Person();
var you = Person();
me.name//echo
you.name//echo

這種寫法不管我們是否使用new去調用,都能得到一個實例,但這種模式丟失了原型,所有的實例都不會繼承Person()原型上的屬性。

var Person = function () {
  return {
    name:'echo'
  }
};
Person.prototype.sayName = function () {
  console.log(1);
};

me.sayName();//報錯,自定義對象未指向Person,沒繼承Person的方法
you.sayName();//報錯,同上

我們在前面說,不使用new時,this指向window(非嚴格模式),無法繼承Person的任何屬性。

var Person = function () {
  this.name = "echo"
};
Person.prototype.sayName = function () {
  console.log(1);
};
var me = new Person();
var you = Person();
me.name;//echo
you.name;//報錯,此時的name是window的屬性
me.sayName();//1
you.sayName();//報錯,在實例化過程中this指向window,並未繼承Person的方法

那有辦法可以讓不使用new情況下實例也能繼承Person屬性的做法嗎,當然有,比如調用自身的構造函數:

var Person = function () {
  if(!(this instanceof Person)){
    return new Person();
  }
  this.name = "echo"
};
Person.prototype.sayName = function () {
  console.log(1);
};
var me = new Person();
var you = Person();
me.name;//echo
you.name;//echo
me.sayName();//1
you.sayName();//1

看到沒,沒使用new的實例you也繼承了Person的屬性和方法,如果看不懂那應該是對於instanceof運算符不太了解,這里順帶說下

instanceof運算符用於測試構造函數的prototype屬性是否出現在對象的原型鏈中的任何位置---MDN

object instanceof constructor   object:要檢測的對象.    constructor:某個構造函數

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
var auto = new Car('Honda', 'Accord', 1998);
console.log(auto instanceof Car);//true
console.log(auto instanceof Object);//true

那么上述代碼中,new Person()好理解,this就是隱性返回的實例,this instanceof Person為true跳過判斷,直接走new一個構造函數時發生的過程,得到實例自然會繼承Person的屬性和方法。

Person()呢,this指向window,很明顯this instanceof Person為false,假假為真,執行判斷內的代碼new Person();同理,也走了new過程的三部曲,得到的實例也繼承了Person的屬性和方法。

有疑問或者錯誤也歡迎大家指出來。

最近總是覺得沒什么值得開心的事情,趁着雙十二,給自己換了個鍵盤,畢竟每天都是要寫代碼的,也算換一個心情。

盡管除了吃飯交房租就沒什么開銷,還是挺心疼的。

如果大家對於new過程還有疑惑,以及如何實現一個new方法,歡迎閱讀博主這篇文章 js new一個對象的過程,實現一個簡單的new方法

那么就先寫到這里了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM