一切皆對象
咱們經(jīng)常聽到JS中“一切皆對象”?有沒有問想過這是什么意思?其它語言也有“一切皆對象”之說,如Python。但是Python中的對象不僅僅是像JS對象這樣的存放值和值的容器。Python中的對象是一個類。JS中有類似的東西,但JS中的“對象”只是鍵和值的容器:
var obj = { name: "Tom", age: 34 }
實際上,JS中的對象是一種“啞”類型,但很多其他實體似乎都是從對象派生出來的。甚至是數(shù)組,在JS中創(chuàng)建一個數(shù)組,如下所示:
var arr = [1,2,3,4,5]
然后用typeof運算符檢查類型,會看到一個令人驚訝的結(jié)果:
typeof arr "object"
看來數(shù)組是一種特殊的對象!即使JS中的函數(shù)也是對象。如果你深入挖掘,還有更多,創(chuàng)建一個函數(shù),該函數(shù)就會附加一些方法:
var a = function(){ return false; }
a.toString()
輸出:
"function(){ return false; }"
咱們并沒有在函數(shù)聲明toString方法,所以在底層一定還有東西。它從何而來?Object有一個名為.toString的方法。似乎咱們的函數(shù)具有相同的Object方法。
Object.toString()
這時咱們使用瀏覽器控制臺來查看默認(rèn)被附加的函數(shù)和屬性,這個謎團(tuán)就會變得更加復(fù)雜:
誰把這些方法放在函數(shù)呢。 JS中的函數(shù)是一種特殊的對象,這會不會是個暗示? 再看看上面的圖片:我們的函數(shù)中有一個名為prototype的奇怪命名屬性,這又是什么鬼?
JS中的prototype是一個對象。它就像一個背包,附著在大多數(shù)JS內(nèi)置對象上。例如 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'
注意內(nèi)置對象有大寫字母:
- String
- Number
- Boolean
- Object
- Symbol
- Null
- Undefined
以下除了Object是類型之外,其它是JS的基本類型。另一方面,內(nèi)置對象就像JS類型的鏡像,也用作函數(shù)。例如,可以使用String作為函數(shù)將數(shù)字轉(zhuǎn)換為字符串:
String(34)
現(xiàn)在回到“prototype”。prototype是所有公共方法和屬性的宿主,從祖先派生的“子”對象可以從使用祖先的方法和屬性。也就是說,給定一個原始 prototype,咱們可以創(chuàng)建新的對象,這些對象將使用一個原型作為公共函數(shù)的真實源,不 Look see see。
假設(shè)有個要求創(chuàng)建一個聊天應(yīng)用程序,有個人物對象。這個人物可以發(fā)送消息,登錄時,會收到一個問候。
根據(jù)需求咱們很容易定義這個么一 Person 對象:
var Person = {
name: "noname",
age: 0,
greet: function() {
console.log(`Hello ${this.name}`);
}
};
你可能會想知道,為什么這里要使用字面量的方式來聲明 Person 對象。稍后會詳細(xì)說明,現(xiàn)在該 Person 為“模型”。通過這個模型,咱們使用 Object.create() 來創(chuàng)建以為這個模型為基礎(chǔ)的對象。
創(chuàng)建和鏈接對象
JS中對象似乎以某種方式鏈接在一起,Object.create()說明了這一點,此方法從原始對象開始創(chuàng)建新對象,再來創(chuàng)建一個新Person 對象:
var Person = {
name: "noname",
age: 0,
greet: function() {
console.log(`Hello ${this.name}`);
}
};
var Tom = Object.create(Person);
現(xiàn)在,Tom 是一個新的對象,但是咱們沒有指定任何新的方法或?qū)傩裕匀豢梢栽L問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
現(xiàn)在,可以從一個共同的祖先開始創(chuàng)建新的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將另一個對象作為第二個參數(shù),可以在其中為新對象指定鍵和值:
var Tom = Object.create(Person, {
age: {
value: 34
},
name: {
value: "Tom"
}
});
以這種方式配置的屬性默認(rèn)情況下不可寫,不可枚舉,不可配置。不可寫意味著之后無法更改該屬性,更改會被忽略:
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 循環(huán)中顯示,例如:
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
}
});
現(xiàn)在,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
暫時不要過于擔(dān)心“this”。拉下來會詳細(xì)介紹。暫且先記住,“this”是對函數(shù)執(zhí)行的某個對象的引用。在咱們的例子中,greet() 在Tom的上下文中運行,因此可以訪問“this.name”。
構(gòu)建JAVAScript對象
目前為止,只介紹了關(guān)于“prototype”的一點知識 ,還有玩了一會 Object.create()之外但咱們沒有直接使用它。隨著時間的推移出現(xiàn)了一個新的模式:構(gòu)造函數(shù)。使用函數(shù)創(chuàng)建新對象聽起來很合理, 假設(shè)你想將Person對象轉(zhuǎn)換為函數(shù),你可以用以下方式:
function Person(name, age) {
var newPerson = {};
newPerson.age = age;
newPerson.name = name;
newPerson.greet = function() {
console.log("Hello " + newPerson.name);
};
return newPerson;
}
因此,不需要到處調(diào)用object.create(),只需將Person作為函數(shù)調(diào)用:
var me = Person("Valentino");
構(gòu)造函數(shù)模式有助于封裝一系列JS對象的創(chuàng)建和配置。在這里, 咱們使用字面量的方式創(chuàng)建對象。這是一種從面向?qū)ο笳Z言借用的約定,其中類名開頭要大寫。
上面的例子有一個嚴(yán)重的問題:每次咱們創(chuàng)建一個新對象時,一遍又一遍地重復(fù)創(chuàng)建greet()函數(shù)。可以使用Object.create(),它會在對象之間創(chuàng)建鏈接,創(chuàng)建次數(shù)只有一次。首先,咱們將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"
這種方式比剛開始會點,還可以進(jìn)一步優(yōu)化就是使用prototype,prototype是一個對象,可以在上面擴(kuò)展屬性,方法等等。
Person.prototype.greet = function() {
console.log("Hello " + this.name);
};
移除了personMethods。調(diào)整Object.create的參數(shù),否則新對象不會自動鏈接到共同的祖先:
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"
現(xiàn)在公共方法的來源是Person.prototype。使用JS中的new運算符,可以消除Person中的所有噪聲,并且只需要為this分配參數(shù)。
下面代碼:
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關(guān)鍵字,被稱為“構(gòu)造函數(shù)調(diào)用”,new 干了三件事情
- 創(chuàng)建一個空對象
- 將空對象的proto指向構(gòu)造函數(shù)的prototype
- 使用空對象作為上下文的調(diào)用構(gòu)造函數(shù)
- function Person(name, age) {
- this.name = name;
- this.age = age;
- }
根據(jù)上面描述的,new Person("Valentino") 做了:
- 創(chuàng)建一個空對象:var obj = {}
- 將空對象的proto__`指向構(gòu)造函數(shù)的 prototype:`obj.__proto = Person().prototype
- 使用空對象作為上下文調(diào)用構(gòu)造函數(shù):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
當(dāng)然,如果使用構(gòu)造函數(shù)調(diào)用構(gòu)造對象,Object.getPrototypeOf也可以工作。但是應(yīng)該檢查原型對象,而不是構(gòu)造函數(shù)本身:
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運算符也可以用于測試構(gòu)造函數(shù)的prototype屬性是否出現(xiàn)在對象的原型鏈中的任何位置。老實說,這個名字有點誤導(dǎo),因為JS中沒有“實例”。在真正的面向?qū)ο笳Z言中,實例是從類創(chuàng)建的新對象。請考慮Python中的示例。咱們有一個名為Person的類,咱們從該類創(chuàng)建一個名為“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關(guān)鍵字。現(xiàn)在,咱們可以使用isinstance方法檢查tom是否是Person的實例
isinstance(tom, Person) // Output: True
Tom也是Python中“object”的一個實例,下面的代碼也返回true:
isinstance(tom, object) // Output: True
根據(jù)isinstance文檔,“如果對象參數(shù)是類參數(shù)的實例,或者是它的(直接、間接或虛擬)子類的實例,則返回true”。咱們在這里討論的是類。現(xiàn)在讓咱們看看instanceof做了什么。咱們將從JS中的Person函數(shù)開始創(chuàng)建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!");
}
因此,可以得出結(jié)論: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的一個內(nèi)在特征,它從另一種稱為Self的語言中借用了原型系統(tǒng)。當(dāng)訪問greet()時,JS引擎會檢查該方法是否可直接在Tom上使用。如果不是,搜索將繼續(xù)向上鏈接,直到找到該方法。
“鏈”是Tom連接的原型對象的層次結(jié)構(gòu)。在我們的例子中,Tom是Person類型的對象,因此Tom的原型連接到Person.prototype。而Person.prototype是Object類型的對象,因此共享相同的Object.prototype原型。如果在Person.prototype上沒有g(shù)reet(),則搜索將繼續(xù)向上鏈接,直到到達(dá)Object.prototype。這就是咱們所說的“原型鏈”。
保護(hù)對象不受操縱
大多數(shù)情況下,JS 對象“可擴(kuò)展”是必要的,這樣咱們可以向?qū)ο筇砑有聦傩浴5行┣闆r下,我們希望對象不受進(jìn)一步操縱。考慮一個簡單的對象:
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
默認(rèn)情況下,每個人都可以向該對象添加新屬性
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
superImportantObject.anotherProperty = "Hei!";
console.log(superImportantObject.anotherProperty); // Hei!
Object.preventExtensions()方法讓一個對象變的不可擴(kuò)展,也就是永遠(yuǎn)不能再添加新的屬性。
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
Object.preventExtensions(superImportantObject);
superImportantObject.anotherProperty = "Hei!";
console.log(superImportantObject.anotherProperty); // undefined
這種技術(shù)對于“保護(hù)”代碼中的關(guān)鍵對象非常方便。JS 中還有許多預(yù)先創(chuàng)建的對象,它們都是為擴(kuò)展而關(guān)閉的,從而阻止開發(fā)人員在這些對象上添加新屬性。這就是“重要”對象的情況,比如XMLHttpRequest的響應(yīng)。瀏覽器供應(yīng)商禁止在響應(yīng)對象上添加新屬性
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”對象上內(nèi)部調(diào)用Object.preventExtensions來完成的。您還可以使用Object.isExtensible方法檢查對象是否受到保護(hù)。如果對象是可擴(kuò)展的,它將返回true:
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
Object.isExtensible(superImportantObject) && console.log("我是可擴(kuò)展的");
如果對象不可擴(kuò)展的,它將返回false:
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
Object.preventExtensions(superImportantObject);
Object.isExtensible(superImportantObject) ||
console.log("我是不可擴(kuò)展的!");
當(dāng)然,對象的現(xiàn)有屬性可以更改甚至刪除
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
Object.preventExtensions(superImportantObject);
delete superImportantObject.property1;
superImportantObject.property2 = "yeees";
console.log(superImportantObject); // { property2: 'yeees' }
現(xiàn)在,為了防止這種操作,可以將每個屬性定義為不可寫和不可配置。為此,有一個方法叫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
有大量關(guān)于ES6 類的文章,所以在這里只討論幾點。JS是一種真正的面向?qū)ο笳Z言嗎?看起來是這樣的,如果咱們看看這段代碼
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中引入了類。但是在這一點上,咱們應(yīng)該清楚JS中沒有“真正的”類。一切都只是一個對象,盡管有關(guān)鍵字class,“原型系統(tǒng)”仍然存在。新的JS版本是向后兼容的,這意味著在現(xiàn)有功能的基礎(chǔ)上添加了新功能,這些新功能中的大多數(shù)都是遺留代碼的語法糖。
總結(jié)
JS中的幾乎所有東西都是一個對象。從字面上看。JS對象是鍵和值的容器,也可能包含函數(shù)。Object是JS中的基本構(gòu)建塊:因此可以從共同的祖先開始創(chuàng)建其他自定義對象。然后咱們可以通過語言的內(nèi)在特征將對象鏈接在一起:原型系統(tǒng)。
從公共對象開始,可以創(chuàng)建共享原始“父”的相同屬性和方法的其他對象。但是它的工作方式不是通過將方法和屬性復(fù)制到每個孩子,就像OOP語言那樣。在JS中,每個派生對象都保持與父對象的連接。使用Object.create或使用所謂的構(gòu)造函數(shù)創(chuàng)建新的自定義對象。與new關(guān)鍵字配對,構(gòu)造函數(shù)類似于模仿傳統(tǒng)的OOP類。
思考
- 如何創(chuàng)建不可變的 JS 對象?
- 什么是構(gòu)造函數(shù)調(diào)用?
- 什么是構(gòu)造函數(shù)?
- “prototype” 是什么?
- 可以描述一下 new 在底層下做了哪些事嗎?
作者:valentinogagliardi
譯者:前端小智
來源:github






