js中的函數有兩種調用方式,一種是通過普通的聲明之后進行的調用。一種是通過new關鍵字進行構造調用。普通的調用就是依次執行函數內部的函數語句,如果有返回值則返回返回值,如果沒有則函數內部的聲明周期結束。但是,函數還有另一個調用方式,使用new關鍵字,將函數當做構造函數。js中沒有所謂獨立出來的構造函數的概念,所有的函數都用同樣的方式聲明,所以有了new這個關鍵字,js(ES6之前)只能通過這種方式實現構造器的構造。那么使用new關鍵字跟普通的調用有什么區別呢?
使用new關鍵字,比普通的函數調用,主要分為以下四個步驟:
- 建立一個空對象 。
- 將函數內部的this修正到上面新建的對象上來。
- 繼續執行函數的其它語句,并且將上面新建的對象的[[prototype]]屬性(ES6中稱為"__proto__")指向該構造函數。
- 如果函數沒有返回值或著函數有返回值,但是該返回值不是對象的話,則返回第一步建立的對象;如果函數有返回值,且返回值是對象的話,則忽略第一步建立的對象,返回默認返回的對象。
稍后我們會對上面的四句話逐一解析,首先我們看一看函數調用的兩種方式:
var fun = function () {
var a = 2;
console.log(a * 2);
}
//新建函數fun
fun(); //4
new fun(); //4
這樣看來,兩者似乎沒有區別,但是這里要注意,使用new進行構造調用時,函數是有返回值的。
...
var return1 = fun(); //4
var return2 = new fun(); //4
console.log(return1); //undefined
console.log(return2); //fun {}
上面的return2并沒有返回一個函數。
...
typeof return2; //object
Object.prototype.toString.call(return2); //[object Object]
其實它會返回一個空的對象。這也就是上面使用new關鍵詞的第一步,內部創建一個新的空對象。
那么當函數內部有this時,結果會是怎樣的呢?
var fun = function () {
this.a = 2;
console.log(this.a * 2);
}
fun(); //4
new fun(); //4
var return1 = fun(); //4
var return2 = new fun(); //4
console.log(return1); //undefined
console.log(return2); //{ a: 2 }
使用new關鍵字后,在函數內部如果出現了this,則自動將this指向內部新建的對象上。最后返回時,因為this的緣故,對象上新建了a屬性,并且賦值返回。
修正定義的對象Object的[[prototype]]
雖然實例上的[[prototype]]屬性__proto__是ES6才作為規范出版的。但是在這之前chrome已經支持__proto__屬性,他指向對象的原型。
原型的問題相當復雜,單獨拿出來也可以當好幾篇文章的量來講。但是這并不是本文的重點。但是每一個對象從根部來說,繼承自Object。而Object.prototype上面定義了一些方法,有類似toString,valueOf等等等方法。對于對象來說,支持通過屬性鏈和方法鏈向上查詢。所以在一個對象實例中,如果沒有定義toString方法,但它還可以向上查詢,找到原型中的toString方法,進行調用。
同時的,有很多元素通過Object實現繼承。比如Function, Array, RegExp等等對象,它們也是對象,但是卻是繼承來自Object。
在這里,內部定義的對象,讓他繼承來自構造函數。
最后一步,也是最容易被忽略的一步,那就是當構造函數存在返回值時,并且返回值為對象時,返回對象而不返回之前定義的對象。
var fun = function () {
this.a = 1;
return {
a: 2
}
}
var obj = new fun(); //{ a:2 }
當然,上面說的Function,Array,RegExp也算Object的一種,如果返回他們同樣也會阻止默認的對象返回。
var fun = function () {
this.a = 1;
return function () {
this.a = 2;
}
}
var obj = new fun(); //function () { this.a = 2; }