作者:valentinogagliardi
譯者:前端小智
來源:github
阿里雲最近在做活動,低至2折,有興趣可以看看: promotion.aliyun.com/ntms/yunpar…
為了保證的可讀性,本文采用意譯而非直譯。
一切皆物件
我們們經常聽到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
在底層下做了哪些事嗎?
程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
交流(歡迎加入群,群工作日都會發紅包,互動討論技術)
阿里雲最近在做活動,低至2折,有興趣可以看看:promotion.aliyun.com/ntms/yunpar…
乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。
因為篇幅的限制,今天的分享只到這裡。如果大家想了解更多的內容的話,可以去掃一掃每篇文章最下面的二維碼,然後關注我們們的微信公眾號,瞭解更多的資訊和有價值的內容。
每次整理文章,一般都到2點才睡覺,一週4次左右,挺苦的,還望支援,給點鼓勵