一切皆對象
咱們經常聽到JS中“一切皆對象”?有沒有問想過這是什么意思?其它語言也有“一切皆對象”之說,如Python。但是Python中的對象不僅僅是像JS對象這樣的存放值和值的容器。Python中的對象是一個類。JS中有類似的東西,但JS中的“對象”只是鍵和值的容器:
var obj = { name: "Tom", age: 34 }
實際上,JS中的對象是一種“啞”類型,但很多其他實體似乎都是從對象派生出來的。甚至是數組,在JS中創建一個數組,如下所示:
var arr = [1,2,3,4,5]
然后用typeof運算符檢查類型,會看到一個令人驚訝的結果:
typeof arr "object"
看來數組是一種特殊的對象!即使JS中的函數也是對象。如果你深入挖掘,還有更多,創建一個函數,該函數就會附加一些方法:
var a = function(){ return false; } a.toString()
輸出:
"function(){ return false; }"
咱們并沒有在函數聲明toString方法,所以在底層一定還有東西。它從何而來?Object有一個名為.toString的方法。似乎咱們的函數具有相同的Object方法。
Object.toString()
這時咱們使用瀏覽器控制臺來查看默認被附加的函數和屬性,這個謎團就會變得更加復雜:

誰把這些方法放在函數呢。 JS中的函數是一種特殊的對象,這會不會是個暗示? 再看看上面的圖片:我們的函數中有一個名為prototype的奇怪命名屬性,這又是什么鬼?
JS中的prototype是一個對象。它就像一個背包,附著在大多數JS內置對象上。例如 Object, Function, Array, Date, Error,都有一個“prototype”:
typeof Object.prototype // 'object' typeof Date.prototype // 'object' typeof String.prototype // 'object' typeof Number.prototype // 'object' typeof Array.prototype // 'object' typeof Error.prototype // 'object'
注意內置對象有大寫字母:
- String
- Number
- Boolean
- Object
- Symbol
- Null
- Undefined
以下除了Object是類型之外,其它是JS的基本類型。另一方面,內置對象就像JS類型的鏡像,也用作函數。例如,可以使用String作為函數將數字轉換為字符串:
String(34)
現在回到“prototype”。prototype是所有公共方法和屬性的宿主,從祖先派生的“子”對象可以從使用祖先的方法和屬性。也就是說,給定一個原始 prototype,咱們可以創建新的對象,這些對象將使用一個原型作為公共函數的真實源,不 Look see see。
假設有個要求創建一個聊天應用程序,有個人物對象。這個人物可以發送消息,登錄時,會收到一個問候。
根據需求咱們很容易定義這個么一 Person 對象:
var Person = { name: "noname", age: 0, greet: function() { console.log(`Hello ${this.name}`); } };
你可能會想知道,為什么這里要使用字面量的方式來聲明 Person 對象。稍后會詳細說明,現在該 Person 為“模型”。通過這個模型,咱們使用 Object.create() 來創建以為這個模型為基礎的對象。
創建和鏈接對象
JS中對象似乎以某種方式鏈接在一起,Object.create()說明了這一點,此方法從原始對象開始創建新對象,再來創建一個新Person 對象:
var Person = { name: "noname", age: 0, greet: function() { console.log(`Hello ${this.name}`); } }; var Tom = Object.create(Person);
現在,Tom 是一個新的對象,但是咱們沒有指定任何新的方法或屬性,但它仍然可以訪問Person中的name和age 屬性。
var Person = { name: "noname", age: 0, greet: function() { console.log(`Hello ${this.name}`); } }; var Tom = Object.create(Person); var tomAge = Tom.age; var tomName = Tom.name; console.log(`${tomAge} ${tomName}`); // Output: 0 noname
現在,可以從一個共同的祖先開始創建新的person。但奇怪的是,新對象仍然與原始對象保持連接,這不是一個大問題,因為“子”對象可以自定義屬性和方法
var Person = { name: "noname", age: 0, greet: function() { console.log(`Hello ${this.name}`); } }; var Tom = Object.create(Person); Tom.age = 34; Tom.name = "Tom"; var tomAge = Tom.age; var tomName = Tom.name; console.log(`${tomAge} ${tomName}`); // Output: 34 Tom
這種方式被稱為“屏蔽”原始屬性。還有另一種將屬性傳遞給新對象的方法。Object.create將另一個對象作為第二個參數,可以在其中為新對象指定鍵和值:
var Tom = Object.create(Person, { age: { value: 34 }, name: { value: "Tom" } });
以這種方式配置的屬性默認情況下不可寫,不可枚舉,不可配置。不可寫意味著之后無法更改該屬性,更改會被忽略:
var Tom = Object.create(Person, { age: { value: 34 }, name: { value: "Tom" } }); Tom.age = 80; Tom.name = "evilchange"; var tomAge = Tom.age; var tomName = Tom.name; Tom.greet(); console.log(`${tomAge} ${tomName}`); // Hello Tom // 34 Tom
不可枚舉意味著屬性不會在 for...in 循環中顯示,例如:
for (const key in Tom) { console.log(key); } // Output: greet
但是正如咱們所看到的,由于JS引擎沿著原型鏈向上查找,在“父”對象上找到greet屬性。最后,不可配置意味著屬性既不能修改也不能刪除。
Tom.age = 80; Tom.name = "evilchange"; delete Tom.name; var tomAge = Tom.age; var tomName = Tom.name; console.log(`${tomAge} ${tomName}`); // 34 Tom
如果要更改屬性的行為,只需配writable(可寫性),configurable(可配置),enumerable(可枚舉)屬性即可。
var Tom = Object.create(Person, { age: { value: 34, enumerable: true, writable: true, configurable: true }, name: { value: "Tom", enumerable: true, writable: true, configurable: true } });
現在,Tom也可以通過以下方式訪問greet():
var Person = { name: "noname", age: 0, greet: function() { console.log(`Hello ${this.name}`); } }; var Tom = Object.create(Person); Tom.age = 34; Tom.name = "Tom"; var tomAge = Tom.age; var tomName = Tom.name; Tom.greet(); console.log(`${tomAge} ${tomName}`); // Hello Tom // 34 Tom
暫時不要過于擔心“this”。拉下來會詳細介紹。暫且先記住,“this”是對函數執行的某個對象的引用。在咱們的例子中,greet() 在Tom的上下文中運行,因此可以訪問“this.name”。
構建JAVAScript對象
目前為止,只介紹了關于“prototype”的一點知識 ,還有玩了一會 Object.create()之外但咱們沒有直接使用它。隨著時間的推移出現了一個新的模式:構造函數。使用函數創建新對象聽起來很合理, 假設你想將Person對象轉換為函數,你可以用以下方式:
function Person(name, age) { var newPerson = {}; newPerson.age = age; newPerson.name = name; newPerson.greet = function() { console.log("Hello " + newPerson.name); }; return newPerson; }
因此,不需要到處調用object.create(),只需將Person作為函數調用:
var me = Person("Valentino");
構造函數模式有助于封裝一系列JS對象的創建和配置。在這里, 咱們使用字面量的方式創建對象。這是一種從面向對象語言借用的約定,其中類名開頭要大寫。
上面的例子有一個嚴重的問題:每次咱們創建一個新對象時,一遍又一遍地重復創建greet()函數。可以使用Object.create(),它會在對象之間創建鏈接,創建次數只有一次。首先,咱們將greet()方法移到外面的一個對象上。然后,可以使用Object.create()將新對象鏈接到該公共對象:
var personMethods = { greet: function() { console.log("Hello " + this.name); } }; function Person(name, age) { // greet lives outside now var newPerson = Object.create(personMethods); newPerson.age = age; newPerson.name = name; return newPerson; } var me = Person("Valentino"); me.greet(); // Output: "Hello Valentino"
這種方式比剛開始會點,還可以進一步優化就是使用prototype,prototype是一個對象,可以在上面擴展屬性,方法等等。
Person.prototype.greet = function() { console.log("Hello " + this.name); };
移除了personMethods。調整Object.create的參數,否則新對象不會自動鏈接到共同的祖先:
function Person(name, age) { // greet lives outside now var newPerson = Object.create(Person.prototype); newPerson.age = age; newPerson.name = name; return newPerson; } Person.prototype.greet = function() { console.log("Hello " + this.name); }; var me = Person("Valentino"); me.greet(); // Output: "Hello Valentino"
現在公共方法的來源是Person.prototype。使用JS中的new運算符,可以消除Person中的所有噪聲,并且只需要為this分配參數。
下面代碼:
function Person(name, age) { // greet lives outside now var newPerson = Object.create(Person.prototype); newPerson.age = age; newPerson.name = name; return newPerson; }
改成:
function Person(name, age) { this.name = name; this.age = age; }
完整代碼:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.greet = function() { console.log("Hello " + this.name); }; var me = new Person("Valentino"); me.greet(); // Output: "Hello Valentino"
注意,使用new關鍵字,被稱為“構造函數調用”,new 干了三件事情
- 創建一個空對象
- 將空對象的proto指向構造函數的prototype
- 使用空對象作為上下文的調用構造函數
- function Person(name, age) {
- this.name = name;
- this.age = age;
- }
根據上面描述的,new Person("Valentino") 做了:
- 創建一個空對象:var obj = {}
- 將空對象的proto__`指向構造函數的 prototype:`obj.__proto = Person().prototype
- 使用空對象作為上下文調用構造函數:Person.call(obj)
檢查原型鏈
檢查JS對象之間的原型鏈接有很多種方法。例如,Object.getPrototypeOf是一個返回任何給定對象原型的方法。考慮以下代碼:
var Person = { name: "noname", age: 0, greet: function() { console.log(`Hello ${this.name}`); } }; var Tom = Object.create(Person);
檢查Person是否是Tom的原型:
var tomPrototype = Object.getPrototypeOf(Tom); console.log(tomPrototype === Person); // Output: true
當然,如果使用構造函數調用構造對象,Object.getPrototypeOf也可以工作。但是應該檢查原型對象,而不是構造函數本身:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.greet = function() { console.log("Hello " + this.name); }; var me = new Person("Valentino"); var mePrototype = Object.getPrototypeOf(me); console.log(mePrototype === Person.prototype); // Output: true
除了Object.getPrototypeOf之外,還有另一個方法isPrototypeOf。該方法用于測試一個對象是否存在于另一個對象的原型鏈上,如下所示,檢查 me 是否在 Person.prototype 上:
Person.prototype.isPrototypeOf(me) && console.log('Yes I am!')
instanceof運算符也可以用于測試構造函數的prototype屬性是否出現在對象的原型鏈中的任何位置。老實說,這個名字有點誤導,因為JS中沒有“實例”。在真正的面向對象語言中,實例是從類創建的新對象。請考慮Python中的示例。咱們有一個名為Person的類,咱們從該類創建一個名為“tom”的新實例:
class Person(): def __init__(self, age, name): self.age = age; self.name = name; def __str__(self): return f'{self.name}' tom = Person(34, 'Tom')
注意,在Python中沒有new關鍵字。現在,咱們可以使用isinstance方法檢查tom是否是Person的實例
isinstance(tom, Person) // Output: True
Tom也是Python中“object”的一個實例,下面的代碼也返回true:
isinstance(tom, object) // Output: True
根據isinstance文檔,“如果對象參數是類參數的實例,或者是它的(直接、間接或虛擬)子類的實例,則返回true”。咱們在這里討論的是類。現在讓咱們看看instanceof做了什么。咱們將從JS中的Person函數開始創建tom(因為沒有真正的類)
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.greet = function() { console.log(`Hello ${this.name}`); }; var tom = new Person(34, "Tom");
使用isinstance方法檢查tom是否是Person和 Object 的實例
if (tom instanceof Object) { console.log("Yes I am!"); } if (tom instanceof Person) { console.log("Yes I am!"); }
因此,可以得出結論:JS對象的原型總是連接到直接的“父對象”和Object.prototype。沒有像Python或Java這樣的類。JS是由對象組成,那么什么是原型鏈呢?如果你注意的話,咱們提到過幾次“原型鏈”。JS對象可以訪問代碼中其他地方定義的方法,這看起來很神奇。再次考慮下面的例子:
var Person = { name: "noname", age: 0, greet: function() { console.log(`Hello ${this.name}`); } }; var Tom = Object.create(Person); Tom.greet();
即使該方法不直接存在于“Tom”對象上,Tom也可以訪問greet()。
這是JS的一個內在特征,它從另一種稱為Self的語言中借用了原型系統。當訪問greet()時,JS引擎會檢查該方法是否可直接在Tom上使用。如果不是,搜索將繼續向上鏈接,直到找到該方法。
“鏈”是Tom連接的原型對象的層次結構。在我們的例子中,Tom是Person類型的對象,因此Tom的原型連接到Person.prototype。而Person.prototype是Object類型的對象,因此共享相同的Object.prototype原型。如果在Person.prototype上沒有greet(),則搜索將繼續向上鏈接,直到到達Object.prototype。這就是咱們所說的“原型鏈”。
保護對象不受操縱
大多數情況下,JS 對象“可擴展”是必要的,這樣咱們可以向對象添加新屬性。但有些情況下,我們希望對象不受進一步操縱。考慮一個簡單的對象:
var superImportantObject = { property1: "some string", property2: "some other string" };
默認情況下,每個人都可以向該對象添加新屬性
var superImportantObject = { property1: "some string", property2: "some other string" }; superImportantObject.anotherProperty = "Hei!"; console.log(superImportantObject.anotherProperty); // Hei!
Object.preventExtensions()方法讓一個對象變的不可擴展,也就是永遠不能再添加新的屬性。
var superImportantObject = { property1: "some string", property2: "some other string" }; Object.preventExtensions(superImportantObject); superImportantObject.anotherProperty = "Hei!"; console.log(superImportantObject.anotherProperty); // undefined
這種技術對于“保護”代碼中的關鍵對象非常方便。JS 中還有許多預先創建的對象,它們都是為擴展而關閉的,從而阻止開發人員在這些對象上添加新屬性。這就是“重要”對象的情況,比如XMLHttpRequest的響應。瀏覽器供應商禁止在響應對象上添加新屬性
var request = new XMLHttpRequest(); request.open("GET", "https://jsonplaceholder.typicode.com/posts"); request.send(); request.onload = function() { this.response.arbitraryProp = "我是新添加的屬性"; console.log(this.response.arbitraryProp); // undefined };
這是通過在“response”對象上內部調用Object.preventExtensions來完成的。您還可以使用Object.isExtensible方法檢查對象是否受到保護。如果對象是可擴展的,它將返回true:
var superImportantObject = { property1: "some string", property2: "some other string" }; Object.isExtensible(superImportantObject) && console.log("我是可擴展的");
如果對象不可擴展的,它將返回false:
var superImportantObject = { property1: "some string", property2: "some other string" }; Object.preventExtensions(superImportantObject); Object.isExtensible(superImportantObject) || console.log("我是不可擴展的!");
當然,對象的現有屬性可以更改甚至刪除
var superImportantObject = { property1: "some string", property2: "some other string" }; Object.preventExtensions(superImportantObject); delete superImportantObject.property1; superImportantObject.property2 = "yeees"; console.log(superImportantObject); // { property2: 'yeees' }
現在,為了防止這種操作,可以將每個屬性定義為不可寫和不可配置。為此,有一個方法叫Object.defineProperties。
var superImportantObject = {}; Object.defineProperties(superImportantObject, { property1: { configurable: false, writable: false, enumerable: true, value: "some string" }, property2: { configurable: false, writable: false, enumerable: true, value: "some other string" } });
或者,更方便的是,可以在原始對象上使用Object.freeze:
var superImportantObject = { property1: "some string", property2: "some other string" }; Object.freeze(superImportantObject);
Object.freeze工作方式與Object.preventExtensions相同,并且它使所有對象的屬性不可寫且不可配置。唯一的缺點是“Object.freeze”僅適用于對象的第一級:嵌套對象不受操作的影響。
class
有大量關于ES6 類的文章,所以在這里只討論幾點。JS是一種真正的面向對象語言嗎?看起來是這樣的,如果咱們看看這段代碼
class Person { constructor(name) { this.name = name; } greet() { console.log(`Hello ${this.name}`); } }
語法與Python等其他編程語言中的類非常相似:
class Person: def __init__(self, name): self.name = name def greet(self): return 'Hello' + self.name
或 php
class Person { public $name; public function __construct($name){ $this->name = $name; } public function greet(){ echo 'Hello ' . $this->name; } }
ES6中引入了類。但是在這一點上,咱們應該清楚JS中沒有“真正的”類。一切都只是一個對象,盡管有關鍵字class,“原型系統”仍然存在。新的JS版本是向后兼容的,這意味著在現有功能的基礎上添加了新功能,這些新功能中的大多數都是遺留代碼的語法糖。
總結
JS中的幾乎所有東西都是一個對象。從字面上看。JS對象是鍵和值的容器,也可能包含函數。Object是JS中的基本構建塊:因此可以從共同的祖先開始創建其他自定義對象。然后咱們可以通過語言的內在特征將對象鏈接在一起:原型系統。
從公共對象開始,可以創建共享原始“父”的相同屬性和方法的其他對象。但是它的工作方式不是通過將方法和屬性復制到每個孩子,就像OOP語言那樣。在JS中,每個派生對象都保持與父對象的連接。使用Object.create或使用所謂的構造函數創建新的自定義對象。與new關鍵字配對,構造函數類似于模仿傳統的OOP類。
思考
- 如何創建不可變的 JS 對象?
- 什么是構造函數調用?
- 什么是構造函數?
- “prototype” 是什么?
- 可以描述一下 new 在底層下做了哪些事嗎?
作者:valentinogagliardi
譯者:前端小智
來源:github