原文:https://css-tricks.com/understanding-javascript-constructors/
理解構(gòu)造函數(shù)對(duì)于真正理解JavaScript語言至關(guān)重要。從技術(shù)上講,JavaScript沒有類,但是它用構(gòu)造函數(shù)和原型來為JavaScript帶來類似的功能。實(shí)際上,在ES2015中引入的類聲明只是作為現(xiàn)有的基于原型繼承的語法糖,并沒有真正為語言添加任何額外的功能。
在本教程中,我們將詳細(xì)討論構(gòu)造函數(shù),并了解JavaScript如何利用它們來創(chuàng)建對(duì)象。
創(chuàng)建和使用構(gòu)造函數(shù)
構(gòu)造函數(shù)與常規(guī)函數(shù)類似,但我們將它們與new關(guān)鍵字一起使用。有兩種類型的構(gòu)造函數(shù):內(nèi)置的構(gòu)造函數(shù),如數(shù)組和對(duì)象,這些構(gòu)造函數(shù)在運(yùn)行時(shí)在執(zhí)行環(huán)境中自動(dòng)可用;以及自定義構(gòu)造函數(shù),它為您自己的對(duì)象類型定義屬性和方法。
當(dāng)您希望創(chuàng)建具有相同屬性和方法的多個(gè)類似對(duì)象時(shí),構(gòu)造函數(shù)非常有用。將構(gòu)造函數(shù)的名稱大寫以區(qū)別于常規(guī)函數(shù)是一種慣例??紤]以下代碼:
function Book() {
// unfinished code
}
var myBook = new Book();
代碼的最后一行創(chuàng)建了Book的一個(gè)實(shí)例并將其賦值給一個(gè)變量。雖然Book構(gòu)造函數(shù)不做任何事情,但myBook仍然是它的一個(gè)實(shí)例。正如您所看到的,這個(gè)函數(shù)與常規(guī)函數(shù)之間沒有區(qū)別,除了它是用new關(guān)鍵字調(diào)用的,而且函數(shù)名是大寫的。
決定實(shí)例的類型
為了查明一個(gè)對(duì)象是否是另一個(gè)對(duì)象的實(shí)例,我們使用instanceof操作符:
myBook instanceof Book // true
myBook instanceof String // false
注意,如果instanceof操作符的右邊不是一個(gè)函數(shù),它將拋出一個(gè)錯(cuò)誤:
myBook instanceof {};
// TypeError: invalid 'instanceof' operand ({})
另一種查找實(shí)例類型的方法是使用構(gòu)造函數(shù)屬性??紤]以下代碼片段:
myBook.constructor === Book; // true
myBook的構(gòu)造函數(shù)屬性指向Book,因此嚴(yán)格的相等運(yùn)算符返回true。JavaScript中的每個(gè)對(duì)象都從其原型繼承一個(gè)構(gòu)造函數(shù)屬性,該屬性指向創(chuàng)建對(duì)象的構(gòu)造函數(shù):
var s = new String("text");
s.constructor === String; // true
"text".constructor === String; // true
var o = new Object();
o.constructor === Object; // true
var o = {};
o.constructor === Object; // true
var a = new Array();
a.constructor === Array; // true
[].constructor === Array; // true
但是請(qǐng)注意,使用constructor屬性檢查實(shí)例的類型通常被認(rèn)為是不好的做法,因?yàn)樗赡鼙桓采w。
自定義構(gòu)造函數(shù)
構(gòu)造函數(shù)就像一個(gè)餅干切刀,用來制作具有相同屬性和方法的多個(gè)對(duì)象??紤]以下示例:
function Book(name, year) {
this.name = name;
this.year = '(' + year + ')';
}
Book構(gòu)造函數(shù)需要兩個(gè)參數(shù):name和year。使用new關(guān)鍵字調(diào)用構(gòu)造函數(shù)時(shí),它將接收到的參數(shù)分配給當(dāng)前實(shí)例的name和year屬性,如下所示:
var firstBook = new Book("Pro AngularJS", 2014);
var secondBook = new Book("Secrets Of The JavaScript Ninja", 2013);
var thirdBook = new Book("JavaScript Patterns", 2010);
console.log(firstBook.name, firstBook.year);
console.log(secondBook.name, secondBook.year);
console.log(thirdBook.name, thirdBook.year);
此代碼將以下內(nèi)容記錄到控制臺(tái):

可以看到,通過使用不同的參數(shù)調(diào)用book構(gòu)造函數(shù),我們可以快速構(gòu)建大量不同的book對(duì)象。這與JavaScript在其內(nèi)置構(gòu)造函數(shù)(如Array()和Date()中使用的模式完全相同。
Object.defineProperty() 方法
可以在構(gòu)造函數(shù)中使用Object.defineProperty()方法來幫助執(zhí)行所有必要的屬性設(shè)置??紤]以下構(gòu)造函數(shù):
function Book(name) {
Object.defineProperty(this, "name", {
get: function() {
return "Book: " + name;
},
set: function(newName) {
name = newName;
},
configurable: false
});
}
var myBook = new Book("Single Page Web Applications");
console.log(myBook.name); // Book: Single Page Web Applications
// we cannot delete the name property because "configurable" is set to false
delete myBook.name;
console.log(myBook.name); // Book: Single Page Web Applications
// but we can change the value of the name property
myBook.name = "Testable JavaScript";
console.log(myBook.name); // Book: Testable JavaScript
這段代碼使用Object.defineProperty()來定義訪問器屬性。訪問器屬性不包括任何屬性或方法,但它們定義了在讀取屬性時(shí)要調(diào)用的getter和在寫入屬性時(shí)要調(diào)用的setter。
getter將返回一個(gè)值,而setter將接收賦給屬性的值作為參數(shù)。上面的構(gòu)造函數(shù)返回一個(gè)實(shí)例,該實(shí)例的name屬性可以設(shè)置或更改,但不能刪除。當(dāng)我們獲得name的值時(shí),getter將字符串Book:添加到名稱并返回它。
對(duì)象字面量(Object literal)優(yōu)于構(gòu)造函數(shù)
JavaScript語言有9個(gè)內(nèi)置的構(gòu)造函數(shù):Object()、Array()、String()、Number()、Boolean()、Date()、Function()、Error()和RegExp()。在創(chuàng)建值時(shí),我們可以自由地使用對(duì)象字面量(Object literal)或構(gòu)造函數(shù)。但是,對(duì)象字面量不僅更容易讀取,而且運(yùn)行速度更快,因?yàn)樗鼈兛梢栽诮馕鰰r(shí)進(jìn)行優(yōu)化。因此,對(duì)于簡單的對(duì)象,最好使用對(duì)象字面量。
// a number object
// numbers have a toFixed() method
var obj = new Object(5);
obj.toFixed(2); // 5.00
// we can achieve the same result using literals
var num = 5;
num.toFixed(2); // 5.00
// a string object
// strings have a slice() method
var obj = new String("text");
obj.slice(0,2); // "te"
// same as above
var string = "text";
string.slice(0,2); // "te"
正如您所看到的,對(duì)象字面量和構(gòu)造函數(shù)之間幾乎沒有任何區(qū)別。更有趣的是,仍然可以在字面量上調(diào)用方法。當(dāng)對(duì)字面量調(diào)用方法時(shí),JavaScript會(huì)自動(dòng)將文字轉(zhuǎn)換為臨時(shí)對(duì)象,以便該方法可以執(zhí)行操作。一旦不再需要臨時(shí)對(duì)象,JavaScript就會(huì)丟棄它。
使用new關(guān)鍵字是必須的
一定要記住在所有構(gòu)造函數(shù)之前使用new關(guān)鍵字。如果您不小心忘記了new,您將修改全局對(duì)象而不是新創(chuàng)建的對(duì)象??紤]以下示例:
function Book(name, year) {
console.log(this);
this.name = name;
this.year = year;
}
var myBook = Book("js book", 2014);
console.log(myBook instanceof Book);
console.log(window.name, window.year);
var myBook = new Book("js book", 2014);
console.log(myBook instanceof Book);
console.log(myBook.name, myBook.year);
下面是這段代碼記錄到控制臺(tái)的內(nèi)容:

當(dāng)我們調(diào)用沒有new的Book構(gòu)造函數(shù)時(shí),實(shí)際上是在調(diào)用沒有返回語句的函數(shù)。因此,在構(gòu)造函數(shù)中,它指向window(而不是myBook),并創(chuàng)建兩個(gè)全局變量。但是,當(dāng)我們使用new調(diào)用函數(shù)時(shí),上下文將從全局(窗口)切換到實(shí)例。這個(gè)正確地指向myBook。
注意,在嚴(yán)格模式下,這段代碼會(huì)拋出一個(gè)錯(cuò)誤,因?yàn)閲?yán)格模式的設(shè)計(jì)是為了防止程序員不小心調(diào)用沒有new關(guān)鍵字的構(gòu)造函數(shù)。
范圍安全的構(gòu)造函數(shù)
正如我們所看到的,構(gòu)造函數(shù)只是一個(gè)函數(shù),因此可以在不使用new關(guān)鍵字的情況下調(diào)用它。但是,對(duì)于沒有經(jīng)驗(yàn)的程序員來說,這可能是bug的來源。作用域安全的構(gòu)造函數(shù)被設(shè)計(jì)成返回相同的結(jié)果,而不管調(diào)用它時(shí)是否帶有new,因此它不會(huì)遇到這些問題。
大多數(shù)內(nèi)置構(gòu)造函數(shù)(如Object、Regex和Array)都是作用域安全的。它們使用一種特殊的模式來確定如何調(diào)用構(gòu)造函數(shù)。如果沒有使用new,它們將通過使用new再次調(diào)用構(gòu)造函數(shù)來返回對(duì)象的適當(dāng)實(shí)例??紤]以下代碼:
function Fn(argument) {
// if "this" is not an instance of the constructor
// it means it was called without new
if (!(this instanceof Fn)) {
// call the constructor again with new
return new Fn(argument);
}
}
因此,我們的構(gòu)造函數(shù)的作用域安全版本應(yīng)該是這樣的:
function Book(name, year) {
if (!(this instanceof Book)) {
return new Book(name, year);
}
this.name = name;
this.year = year;
}
var person1 = new Book("js book", 2014);
var person2 = Book("js book", 2014);
console.log(person1 instanceof Book); // true
console.log(person2 instanceof Book); // true
結(jié)論
重要的是要理解ES2015中引入的類聲明只是作為現(xiàn)有的基于原型的繼承的語法糖,并沒有向JavaScript添加任何新東西。構(gòu)造函數(shù)和原型是JavaScript定義相似和相關(guān)對(duì)象的主要方式。
在本文中,我們已經(jīng)很好地了解了JavaScript構(gòu)造函數(shù)的工作方式。我們了解到構(gòu)造函數(shù)與常規(guī)函數(shù)類似,但是它們與new關(guān)鍵字一起使用。我們了解了構(gòu)造函數(shù)如何使我們能夠快速創(chuàng)建具有相同屬性和方法的多個(gè)類似對(duì)象,以及為什么instanceof操作符是確定實(shí)例類型的最安全方法。最后,我們討論了作用域安全的構(gòu)造函數(shù),可以使用new或不使用new來調(diào)用它們。